-
Notifications
You must be signed in to change notification settings - Fork 33
Description
Objective
Improve the dependency injection experience for submodules (e.g. P2P's PeerstoreProvider, telemetry's TimeSeriesAgent, etc.). I think this may generally apply to submodules which need to embed the base_modules.InterruptableModule.
Origin Document
Observation made while working on #732. In order to complete the peerstore provider refactor (#804).
When retrieving modules from the bus (via the module registry), they are asserted to the correct interface type by their respective Bus#Get_X_Module() method. In contrast, retrieving submodules from the registry must be done directly (at the time of writing) which requires additional type assertions and boilerplate in each place any submodule is retrieved from the bus.
Goals
- Support simplification of submodule interfaces (i.e. don't embed
Moduleunless appropriate). - Support dependency injection of submodules with a developer experience on par with that of modules.
classDiagram
class IntegratableModule {
<<interface>>
+GetBus() Bus
+SetBus(bus Bus)
}
class ModuleFactoryWithOptions {
<<interface>>
Create(bus Bus, options ...ModuleOption) (Module, error)
}
class Module {
<<interface>>
}
class InjectableModule {
<<interface>>
GetModuleName() string
}
class InterruptableModule {
<<interface>>
Start() error
Stop() error
}
Module --|> InjectableModule
Module --|> IntegratableModule
Module --|> InterruptableModule
Module --|> ModuleFactoryWithOptions
class Submodule {
<<interface>>
}
Submodule --|> InjectableModule
Submodule --|> IntegratableModule
class exampleSubmodule
class exampleSubmoduleFactory {
<<interface>>
Create(...) (exampleSubmodule, error)
}
exampleSubmodule --|> Submodule
exampleSubmodule --|> exampleSubmoduleFactory
exampleModule --|> Module
Concrete Example
Below, rpcPeerstoreProvider MUST implement Module so that it can traverse the ModuleRegistry dependency injection system that we currently have, as it's used outside of the P2P module in the CLI. This results in it embedding the noop implementations of InterruptableModule and being additionally over-constrained by the InitializableModule#Create() interface method:
classDiagram
class IntegratableModule {
<<interface>>
+GetBus() Bus
+SetBus(bus Bus)
}
class PeerstoreProvider {
<<interface>>
+GetStakedPeerstoreAtHeight(height int) (Peerstore, error)
+GetUnstakedPeerstore() (Peerstore, error)
}
class rpcPeerstoreProvider
rpcPeerstoreProvider --|> PeerstoreProvider
rpcPeerstoreProvider --|> Module
class Module {
<<interface>>
}
class InitializableModule {
<<interface>>
Create(bus Bus, options ...ModuleOption) (Module, error)
GetModuleName() string
}
class InterruptableModule {
<<interface>>
Start() error
Stop() error
}
Module --|> InitializableModule
Module --|> IntegratableModule
Module --|> InterruptableModule
Requiring rpcPeerstoreProvider (the injectee) to implement InitializableModule fosters boilerplate around the respective constructor function and everywhere it's injected. Here is an excerpt from p2p/providers/peerstore_provider/rpc/provider.go:
func NewRPCPeerstoreProvider(options ...modules.ModuleOption) *rpcPeerstoreProvider {
rpcPSP := &rpcPeerstoreProvider{
rpcURL: fmt.Sprintf("http://%s:%s", rpcHost, defaults.DefaultRPCPort), // TODO: Make port configurable
}
for _, o := range options {
o(rpcPSP)
}
rpcPSP.initRPCClient()
return rpcPSP
}
func Create(bus modules.Bus, options ...modules.ModuleOption) (modules.Module, error) {
return new(rpcPeerstoreProvider).Create(bus, options...)
}
func (*rpcPeerstoreProvider) Create(bus modules.Bus, options ...modules.ModuleOption) (modules.Module, error) {
return NewRPCPeerstoreProvider(options...), nil
}The Create() function isn't currently used anywhere in the codebase, same goes for the rpcPeerstoreProvider#Create() method, which only serves to satisfy the InitializableModule interface requirement. This increases complexity and reduces readability and maintainability on both the injectee and injector side in my opinion.
Here is an excerpt from p2pModule which illustrates the complexity this design introduces on the injector side (which will present everywhere a submodule is retrieved from the ModuleRegistry):
// setupPeerstoreProvider attempts to retrieve the peerstore provider from the
// bus, if one is registered, otherwise returns a new `persistencePeerstoreProvider`.
func (m *p2pModule) setupPeerstoreProvider() error {
m.logger.Debug().Msg("setupPeerstoreProvider")
pstoreProviderModule, err := m.GetBus().GetModulesRegistry().GetModule(peerstore_provider.ModuleName)
if err != nil {
m.logger.Debug().Msg("creating new persistence peerstore...")
pstoreProviderModule = persABP.NewPersistencePeerstoreProvider(m.GetBus())
} else if pstoreProviderModule != nil {
m.logger.Debug().Msg("loaded persistence peerstore...")
}
pstoreProvider, ok := pstoreProviderModule.(providers.PeerstoreProvider)
if !ok {
return fmt.Errorf("unknown peerstore provider type: %T", pstoreProviderModule)
}
m.pstoreProvider = pstoreProvider
return nil
}I would prefer to be able to do something like this:
classDiagram
class IntegratableModule {
<<interface>>
+GetBus() Bus
+SetBus(bus Bus)
}
class PeerstoreProvider {
<<interface>>
+GetStakedPeerstoreAtHeight(height int) (Peerstore, error)
+GetUnstakedPeerstore() (Peerstore, error)
}
class rpcPeerstoreProvider
rpcPeerstoreProvider --|> PeerstoreProvider
rpcPeerstoreProvider --|> Submodule
rpcPeerstoreProvider --|> rpcPeerstoreProviderFactory
class Submodule {
<<interface>>
}
class InjectableModule {
<<interface>>
GetModuleName() string
}
class rpcPeerstoreProviderOption
class rpcPeerstoreProviderFactory {
<<interface>>
Create(bus Bus, options ...rpcPeerstoreProviderOption) (rpcPeerstoreProvider, error)
}
rpcPeerstoreProviderFactory --* rpcPeerstoreProviderOption
Submodule --|> InjectableModule
Submodule --|> IntegratableModule
var (
_ modules.Submodule = &rpcPeerstoreProvider{}
_ peerstore_provider.PeerstoreProvider = &rpcPeerstoreProvider{}
_ rpcPeerstoreProviderFactory = &rpcPeerstoreProvider{}
)
type rpcPeerstoreProviderOption func(*rpcPeerstoreProvider)
type rpcPeerstoreProviderFactory = modules.FactoryWithOptions[peerstore_provider.PeerstoreProvider, rpcPeerstoreProviderOption]
func Create(options ...modules.ModuleOption) *rpcPeerstoreProvider {
return new(rpcPeerstoreProvider).Create(nil, options...)
}
func (*rpcPeerstoreProvider) Create(bus modules.Bus, options ...rpcPeerstoreProviderOption) (peerstore_provider.PeerstoreProvider, error) {
rpcPSP := &rpcPeerstoreProvider{
rpcURL: fmt.Sprintf("http://%s:%s", rpcHost, defaults.DefaultRPCPort), // TODO: Make port configurable
}
for _, o := range options {
o(rpcPSP)
}
rpcPSP.initRPCClient()
return rpcPSP
}// setupPeerstoreProvider attempts to retrieve the peerstore provider from the
// bus, if one is registered, otherwise returns a new `persistencePeerstoreProvider`.
func (m *p2pModule) setupPeerstoreProvider() (err error) {
m.logger.Debug().Msg("setupPeerstoreProvider")
m.pstoreProvider, err = m.GetBus().GetPeerstoreProviderSubmodule()
if err != nil {
m.logger.Debug().Msg("creating new persistence peerstore...")
m.pstoreProvider = persABP.NewPersistencePeerstoreProvider(m.GetBus())
} else {
m.logger.Debug().Msg("loaded persistence peerstore...")
}
return nil
}Deliverable
- Add the
Submoduleinterface definition - Remove
InitializableModule#Create()(unnecessary as ofModuleFactoryWithOptionsembedding) - Rename
InitializableModuletoInjectableModule - Add retrieval methods for submodules to the
Businteface (like each module has) - Map
ModuleRegistrymodule names toInjectableModules (instead ofModules)
Non-goals / Non-deliverables
- Unnecessarily refactoring existing submodules
- Refactoring unrelated module registry or consumer code
General issue deliverables
- Update the appropriate CHANGELOG(s)
- Update any relevant local/global README(s)
- Update relevant source code tree explanations
- Add or update any relevant or supporting mermaid diagrams
Testing Methodology
- All tests:
make test_all - LocalNet: verify a
LocalNetis still functioning correctly by following the instructions at docs/development/README.md - k8s LocalNet: verify a
k8s LocalNetis still functioning correctly by following the instructions here
Creator: @bryanchriswhite
Metadata
Metadata
Assignees
Labels
Type
Projects
Status