You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 Module unless 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
Loading
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
Loading
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:
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)
iferr!=nil {
m.logger.Debug().Msg("creating new persistence peerstore...")
pstoreProviderModule=persABP.NewPersistencePeerstoreProvider(m.GetBus())
} elseifpstoreProviderModule!=nil {
m.logger.Debug().Msg("loaded persistence peerstore...")
}
pstoreProvider, ok:=pstoreProviderModule.(providers.PeerstoreProvider)
if!ok {
returnfmt.Errorf("unknown peerstore provider type: %T", pstoreProviderModule)
}
m.pstoreProvider=pstoreProviderreturnnil
}
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
// setupPeerstoreProvider attempts to retrieve the peerstore provider from the// bus, if one is registered, otherwise returns a new `persistencePeerstoreProvider`.func (m*p2pModule) setupPeerstoreProvider() (errerror) {
m.logger.Debug().Msg("setupPeerstoreProvider")
m.pstoreProvider, err=m.GetBus().GetPeerstoreProviderSubmodule()
iferr!=nil {
m.logger.Debug().Msg("creating new persistence peerstore...")
m.pstoreProvider=persABP.NewPersistencePeerstoreProvider(m.GetBus())
} else {
m.logger.Debug().Msg("loaded persistence peerstore...")
}
returnnil
}
Deliverable
Add the Submodule interface definition
Remove InitializableModule#Create() (unnecessary as of ModuleFactoryWithOptions embedding)
Rename InitializableModule to InjectableModule
Add retrieval methods for submodules to the Bus inteface (like each module has)
Map ModuleRegistry module names to InjectableModules (instead of Modules)
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 LocalNet is still functioning correctly by following the instructions at docs/development/README.md
k8s LocalNet: verify a k8s LocalNet is still functioning correctly by following the instructions here
Objective
Improve the dependency injection experience for submodules (e.g. P2P's
PeerstoreProvider, telemetry'sTimeSeriesAgent, etc.). I think this may generally apply to submodules which need to embed thebase_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
Moduleunless appropriate).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 --|> ModuleConcrete Example
Below,
rpcPeerstoreProviderMUST implementModuleso that it can traverse theModuleRegistrydependency 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 ofInterruptableModuleand being additionally over-constrained by theInitializableModule#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 --|> InterruptableModuleRequiring
rpcPeerstoreProvider(the injectee) to implementInitializableModulefosters boilerplate around the respective constructor function and everywhere it's injected. Here is an excerpt fromp2p/providers/peerstore_provider/rpc/provider.go:The
Create()function isn't currently used anywhere in the codebase, same goes for therpcPeerstoreProvider#Create()method, which only serves to satisfy theInitializableModuleinterface requirement. This increases complexity and reduces readability and maintainability on both the injectee and injector side in my opinion.Here is an excerpt from
p2pModulewhich illustrates the complexity this design introduces on the injector side (which will present everywhere a submodule is retrieved from theModuleRegistry):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 --|> IntegratableModuleDeliverable
Submoduleinterface definitionInitializableModule#Create()(unnecessary as ofModuleFactoryWithOptionsembedding)InitializableModuletoInjectableModuleBusinteface (like each module has)ModuleRegistrymodule names toInjectableModules (instead ofModules)Non-goals / Non-deliverables
General issue deliverables
Testing Methodology
make test_allLocalNetis still functioning correctly by following the instructions at docs/development/README.mdk8s LocalNetis still functioning correctly by following the instructions hereCreator: @bryanchriswhite