Skip to content

Commit 97e8e7f

Browse files
committed
Add interface debugger for modules
1 parent 19988be commit 97e8e7f

4 files changed

Lines changed: 475 additions & 28 deletions

File tree

DOCUMENTATION.md

Lines changed: 180 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
- [Tenant-Aware Configuration](#tenant-aware-configuration)
4040
- [Tenant Configuration Loading](#tenant-configuration-loading)
4141
- [Error Handling](#error-handling)
42+
- [Debugging and Troubleshooting](#debugging-and-troubleshooting)
43+
- [Module Interface Debugging](#module-interface-debugging)
44+
- [Common Issues](#common-issues)
45+
- [Diagnostic Tools](#diagnostic-tools)
4246
- [Common Error Types](#common-error-types)
4347
- [Error Wrapping](#error-wrapping)
4448
- [Testing Modules](#testing-modules)
@@ -717,48 +721,196 @@ if err := doSomething(); err != nil {
717721

718722
This allows for error inspection using `errors.Is` and `errors.As`.
719723

720-
## Testing Modules
724+
## Debugging and Troubleshooting
721725

722-
### Mock Application
726+
The Modular framework provides several debugging utilities to help diagnose common issues with module lifecycle, interface implementation, and service injection.
723727

724-
Modular facilitates testing with mock implementations:
728+
### Module Interface Debugging
729+
730+
#### DebugModuleInterfaces
731+
732+
Use `DebugModuleInterfaces` to check which interfaces a specific module implements:
725733

726734
```go
727-
// Create a mock application
728-
mockApp := &MockApplication{
729-
configSections: make(map[string]modular.ConfigProvider),
730-
services: make(map[string]any),
731-
}
735+
import "github.com/GoCodeAlone/modular"
736+
737+
// Debug a specific module
738+
modular.DebugModuleInterfaces(app, "your-module-name")
739+
```
740+
741+
**Output example:**
742+
```
743+
🔍 Debugging module 'web-server' (type: *webserver.Module)
744+
Memory address: 0x14000026840
745+
✅ Module
746+
✅ Configurable
747+
❌ DependencyAware
748+
✅ ServiceAware
749+
✅ Startable
750+
✅ Stoppable
751+
❌ Constructable
752+
📦 Provides 1 services, Requires 0 services
753+
```
754+
755+
#### DebugAllModuleInterfaces
732756

733-
// Register test services
734-
mockApp.RegisterService("database", &MockDatabase{})
757+
Debug all registered modules at once:
735758

736-
// Test your module
737-
module := NewMyModule()
738-
err := module.Init(mockApp)
739-
assert.NoError(t, err)
759+
```go
760+
// Debug all modules in the application
761+
modular.DebugAllModuleInterfaces(app)
740762
```
741763

742-
### Testing Services
764+
#### CompareModuleInstances
743765

744-
Test service implementations to ensure they meet interface requirements:
766+
Compare module instances before and after initialization to detect if modules are being replaced:
745767

746768
```go
747-
func TestDatabaseService(t *testing.T) {
748-
// Create mock DB
749-
db := &MockDB{}
769+
// Store reference before initialization
770+
originalModule := app.moduleRegistry["module-name"]
771+
772+
// After initialization
773+
currentModule := app.moduleRegistry["module-name"]
774+
775+
modular.CompareModuleInstances(originalModule, currentModule, "module-name")
776+
```
777+
778+
### Common Issues
779+
780+
#### 1. "Module does not implement Startable, skipping"
781+
782+
**Symptoms:** Module has a `Start` method but is reported as not implementing `Startable`.
783+
784+
**Common Causes:**
785+
786+
1. **Incorrect method signature** - Most common issue:
787+
```go
788+
// ❌ WRONG - missing context parameter
789+
func (m *MyModule) Start() error { return nil }
790+
791+
// ✅ CORRECT
792+
func (m *MyModule) Start(ctx context.Context) error { return nil }
793+
```
794+
795+
2. **Wrong context import:**
796+
```go
797+
// ❌ WRONG - old context package
798+
import "golang.org/x/net/context"
799+
800+
// ✅ CORRECT - standard library
801+
import "context"
802+
```
803+
804+
3. **Constructor returns module without Startable interface:**
805+
```go
806+
// ❌ PROBLEMATIC - returns different type
807+
func (m *MyModule) Constructor() ModuleConstructor {
808+
return func(app Application, services map[string]any) (Module, error) {
809+
return &DifferentModuleType{}, nil // Lost Startable!
810+
}
811+
}
812+
813+
// ✅ CORRECT - preserves all interfaces
814+
func (m *MyModule) Constructor() ModuleConstructor {
815+
return func(app Application, services map[string]any) (Module, error) {
816+
return m, nil // Or create new instance with all interfaces
817+
}
818+
}
819+
```
820+
821+
#### 2. Service Injection Failures
822+
823+
**Symptoms:** `"failed to inject services for module"` errors.
824+
825+
**Debugging steps:**
826+
1. Verify service names match exactly
827+
2. Check that required services are provided by other modules
828+
3. Ensure dependency order is correct
829+
4. Use interface-based matching for flexibility
830+
831+
#### 3. Module Replacement Issues
832+
833+
**Symptoms:** Module works before `Init()` but fails after.
834+
835+
**Cause:** Constructor-based injection replaces the original module instance.
836+
837+
**Solution:** Ensure your Constructor returns a module that implements all the same interfaces.
838+
839+
### Diagnostic Tools
840+
841+
#### CheckModuleStartableImplementation
842+
843+
For detailed analysis of why a module doesn't implement Startable:
844+
845+
```go
846+
import "github.com/GoCodeAlone/modular"
847+
848+
// Check specific module
849+
modular.CheckModuleStartableImplementation(yourModule)
850+
```
851+
852+
**Output includes:**
853+
- Method signature analysis
854+
- Expected vs actual parameter types
855+
- Interface compatibility check
856+
857+
#### Example Debugging Workflow
858+
859+
When troubleshooting module issues:
860+
861+
```go
862+
func debugModuleIssues(app *modular.StdApplication) {
863+
// 1. Check all modules before initialization
864+
fmt.Println("=== BEFORE INIT ===")
865+
modular.DebugAllModuleInterfaces(app)
866+
867+
// 2. Store references to original modules
868+
originalModules := make(map[string]modular.Module)
869+
for name, module := range app.SvcRegistry() {
870+
originalModules[name] = module
871+
}
750872

751-
// Verify it implements the interface
752-
var _ DatabaseService = db
873+
// 3. Initialize
874+
err := app.Init()
875+
if err != nil {
876+
fmt.Printf("Init error: %v\n", err)
877+
}
753878

754-
// Test specific methods
755-
err := db.Connect()
756-
assert.NoError(t, err)
879+
// 4. Check modules after initialization
880+
fmt.Println("=== AFTER INIT ===")
881+
modular.DebugAllModuleInterfaces(app)
757882

758-
result, err := db.Query("SELECT 1")
759-
assert.NoError(t, err)
760-
assert.NotNil(t, result)
883+
// 5. Compare instances
884+
for name, original := range originalModules {
885+
if current, exists := app.SvcRegistry()[name]; exists {
886+
modular.CompareModuleInstances(original, current, name)
887+
}
888+
}
889+
890+
// 6. Check specific problematic modules
891+
if problematicModule, exists := app.SvcRegistry()["problematic-module"]; exists {
892+
modular.CheckModuleStartableImplementation(problematicModule)
893+
}
761894
}
762895
```
763896

764-
By using dependency injection and interfaces, Modular makes it easy to test modules in isolation.
897+
#### Best Practices for Debugging
898+
899+
1. **Add debugging early:** Use debugging utilities during development, not just when issues occur.
900+
901+
2. **Check before and after Init():** Many issues occur during the initialization phase when modules are replaced via constructors.
902+
903+
3. **Verify method signatures:** Double-check that your Start/Stop methods match the expected interface signatures exactly.
904+
905+
4. **Use specific error messages:** The debugging tools provide detailed information about why interfaces aren't implemented.
906+
907+
5. **Test interface implementations:** Add compile-time checks to ensure your modules implement expected interfaces:
908+
```go
909+
// Compile-time interface check
910+
var _ modular.Startable = (*MyModule)(nil)
911+
var _ modular.Stoppable = (*MyModule)(nil)
912+
```
913+
914+
6. **Check memory addresses:** If memory addresses differ before and after Init(), your module was replaced by a constructor.
915+
916+
By using these debugging tools and following these practices, you can quickly identify and resolve module interface and lifecycle issues in your Modular applications.

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,25 @@ modcli --help
607607

608608
Each command includes interactive prompts to guide you through the process of creating modules or configurations with the features you need.
609609

610+
## 📚 Additional Resources
611+
612+
- **[Detailed Documentation](DOCUMENTATION.md)** - Comprehensive guide covering advanced topics, best practices, and in-depth examples
613+
- **[Debugging and Troubleshooting](DOCUMENTATION.md#debugging-and-troubleshooting)** - Diagnostic tools and solutions for common issues
614+
- **[Available Modules](modules/README.md)** - Complete list of pre-built modules with documentation
615+
- **[Examples](examples/)** - Working example applications demonstrating various features
616+
617+
### Having Issues?
618+
619+
If you're experiencing problems with module interfaces (e.g., "Module does not implement Startable"), check out the [debugging section](DOCUMENTATION.md#debugging-and-troubleshooting) which includes diagnostic tools like:
620+
621+
```go
622+
// Debug module interface implementations
623+
modular.DebugModuleInterfaces(app, "your-module-name")
624+
625+
// Check all modules at once
626+
modular.DebugAllModuleInterfaces(app)
627+
```
628+
610629
## License
611630

612631
[MIT License](LICENSE)

debug_module_interfaces.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package modular
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
)
7+
8+
// DebugModuleInterfaces helps debug interface implementation issues during module lifecycle
9+
func DebugModuleInterfaces(app *StdApplication, moduleName string) {
10+
module, exists := app.moduleRegistry[moduleName]
11+
if !exists {
12+
fmt.Printf("❌ Module '%s' not found in registry\n", moduleName)
13+
return
14+
}
15+
16+
fmt.Printf("🔍 Debugging module '%s' (type: %T)\n", moduleName, module)
17+
fmt.Printf(" Memory address: %p\n", module)
18+
19+
// Check all the interfaces
20+
interfaces := map[string]interface{}{
21+
"Module": (*Module)(nil),
22+
"Configurable": (*Configurable)(nil),
23+
"DependencyAware": (*DependencyAware)(nil),
24+
"ServiceAware": (*ServiceAware)(nil),
25+
"Startable": (*Startable)(nil),
26+
"Stoppable": (*Stoppable)(nil),
27+
"Constructable": (*Constructable)(nil),
28+
}
29+
30+
moduleType := reflect.TypeOf(module)
31+
for interfaceName, interfacePtr := range interfaces {
32+
interfaceType := reflect.TypeOf(interfacePtr).Elem()
33+
implements := moduleType.Implements(interfaceType)
34+
status := "❌"
35+
if implements {
36+
status = "✅"
37+
}
38+
fmt.Printf(" %s %s\n", status, interfaceName)
39+
}
40+
41+
// Check if it's a ServiceAware module
42+
if svcAware, ok := module.(ServiceAware); ok {
43+
provides := svcAware.ProvidesServices()
44+
requires := svcAware.RequiresServices()
45+
fmt.Printf(" 📦 Provides %d services, Requires %d services\n", len(provides), len(requires))
46+
47+
if constructable, isConstructable := module.(Constructable); isConstructable {
48+
fmt.Printf(" 🏗️ Has constructor - this module may be replaced during injection!\n")
49+
_ = constructable // avoid unused variable warning
50+
}
51+
}
52+
}
53+
54+
// DebugAllModuleInterfaces debugs all registered modules
55+
func DebugAllModuleInterfaces(app *StdApplication) {
56+
fmt.Printf("\n🔍 ==> DEBUG: All Module Interface Implementations <==\n")
57+
for name := range app.moduleRegistry {
58+
DebugModuleInterfaces(app, name)
59+
fmt.Println()
60+
}
61+
}
62+
63+
// CompareModuleInstances compares two module instances to see if they're the same
64+
func CompareModuleInstances(original, current Module, moduleName string) {
65+
fmt.Printf("🔍 Comparing module instances for '%s':\n", moduleName)
66+
fmt.Printf(" Original: %T at %p\n", original, original)
67+
fmt.Printf(" Current: %T at %p\n", current, current)
68+
69+
if original == current {
70+
fmt.Printf(" ✅ Same instance\n")
71+
} else {
72+
fmt.Printf(" ❌ Different instances - module was replaced!\n")
73+
}
74+
75+
// Check if both implement Startable
76+
_, originalStartable := original.(Startable)
77+
_, currentStartable := current.(Startable)
78+
79+
fmt.Printf(" Original implements Startable: %v\n", originalStartable)
80+
fmt.Printf(" Current implements Startable: %v\n", currentStartable)
81+
82+
if originalStartable && !currentStartable {
83+
fmt.Printf(" 🚨 PROBLEM: Original was Startable but replacement is not!\n")
84+
}
85+
}

0 commit comments

Comments
 (0)