Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core] Improve dependency injection for submodules #810

Closed
7 tasks
bryanchriswhite opened this issue Jun 8, 2023 · 4 comments · Fixed by #855
Closed
7 tasks

[Core] Improve dependency injection for submodules #810

bryanchriswhite opened this issue Jun 8, 2023 · 4 comments · Fixed by #855
Assignees
Labels
code health Nice to have code improvement core Core infrastructure - protocol related

Comments

@bryanchriswhite
Copy link
Contributor

bryanchriswhite commented Jun 8, 2023

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 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:

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
Loading
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 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

Creator: @bryanchriswhite

@bryanchriswhite bryanchriswhite added core Core infrastructure - protocol related code health Nice to have code improvement labels Jun 8, 2023
@bryanchriswhite bryanchriswhite self-assigned this Jun 8, 2023
@bryanchriswhite bryanchriswhite added the triage It requires some decision-making at team level (it can't be worked on as it stands) label Jun 8, 2023
@Olshansk
Copy link
Member

Olshansk commented Jun 8, 2023

The Create() function isn't currently used anywhere in the codebase

It seems like as the code evolved organically, we create factory functions starting with New as well as Create. IMO, we should grep for all func NewXXX and remove them altogether.

Q1: Wdyt about the point above?

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):

Q2: What if we simply treat all submodules the same as modules and have no differentiation between the two? Would this simplify things

// setupPeerstoreProvider attempts to retrieve the peerstore provider from the
// bus, if one is registered, otherwise returns a new persistencePeerstoreProvider.

Q3: What if we just panic if you try to access an uninitialized submodule instead? Alternatively, since they all have Create factory functions, we can centralized the logic for creating the singletons based on which Factory interface it implements. Let me know if I should provide a pseudo code snippet for how this would work


In general, I'm very open to refactoring anything as long as we have time, capacity and sanity to do it. Follow up questions:

Q4: What you be able to pick this up? If so, which iteration do you think it fits in and do you think it'll be a Medium or Large effort (once we iron out the details)?

@bryanchriswhite
Copy link
Contributor Author

The Create() function isn't currently used anywhere in the codebase

It seems like as the code evolved organically, we create factory functions starting with New as well as Create. IMO, we should grep for all func NewXXX and remove them altogether.

Q1: Wdyt about the point above?

I have to think on it a bit more to figure out if I have an opinion on Create() vs NewXXX() but it sounds like we're in agreement that there is duplication currently that we can reduce.

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):

Q2: What if we simply treat all submodules the same as modules and have no differentiation between the two? Would this simplify things

I think we can for the most part with the exception of having submodules conform to the ModuleFactoryWithOptions interface. This is a problematic combination in my opinion because of the impact that it has on the usability of the constructor (as illustrated in the example). I imagine that it could be sufficient to continue to use ModuleRegistry for submodules, I'm just proposing that we make the necessary modifications to support an improved developer experience when retrieving them.

// setupPeerstoreProvider attempts to retrieve the peerstore provider from the
// bus, if one is registered, otherwise returns a new persistencePeerstoreProvider.

Q3: What if we just panic if you try to access an uninitialized submodule instead? Alternatively, since they all have Create factory functions, we can centralized the logic for creating the singletons based on which Factory interface it implements. Let me know if I should provide a pseudo code snippet for how this would work

Sounds interesting but I think I would need to see a snippet to understand what you mean. To add context to the code comment, the scenario where the peerstore is retrieved from the bus is when the CLI starts up its P2P module. Before it does so, it creates and adds an rpcPeerstoreProvider to the module registry. The the CLI P2P module subsequently comes up, it will use the rpcPeerstoreProvider instead of creating a persistencePeerstoreProvider.

In general, I'm very open to refactoring anything as long as we have time, capacity and sanity to do it. Follow up questions:

Q4: What you be able to pick this up? If so, which iteration do you think it fits in and do you think it'll be a Medium or Large effort (once we iron out the details)?

Happy to pick it up! I would estimate it as a Medium which I would distribute between making a change to the ModuleRegistry and then making the resulting refactoring changes. I get the impression that the first part is likely a minimal change but the second part holds most of the uncertainty in this issue.

All that being said, I don't see this as particularly urgent and I'm not sure that there's a place in the sprint(s) where it naturally fits but I would personally prefer to resolve this before adding any new code which injects and/or retrieves submodules from the module registry. Wdyt about making it a stretch goal for next sprint or something like that?

@bryanchriswhite
Copy link
Contributor Author

Adding some diagrams that I don't think make sense to include in the PR description but may make sense to include as documentation at some point:

Example Module

classDiagram
class IntegratableModule {
    <<interface>>
    +GetBus() Bus
    +SetBus(bus Bus)
}

class ModuleFactoryWithOptions {
    <<interface>>
    Create(bus Bus, options ...ModuleOption) (Module, error)
}

ModuleFactoryWithOptions --> Module
ModuleFactoryWithOptions --* "0..n" ModuleOption
ModuleOption --> Module

class Module {
    <<interface>>
}
class InjectableModule {
    <<interface>>
    GetModuleName() string
}
class InterruptableModule {
    <<interface>>
    Start() error
    Stop() error
}

Module --|> InjectableModule
Module --|> IntegratableModule
Module --|> InterruptableModule
Module --|> ModuleFactoryWithOptions

exampleModule --|> Module
Loading

Example Submodule without config or options

classDiagram
class IntegratableModule {
    <<interface>>
    +GetBus() Bus
    +SetBus(bus Bus)
}

class InjectableModule {
    <<interface>>
    GetModuleName() string
}


class Submodule {
    <<interface>>
}

Submodule --|> InjectableModule
Submodule --|> IntegratableModule

class exampleSubmodule
class exampleSubmoduleFactory {
    <<interface>>
    Create(bus Bus) (exampleSubmodule, error)
}

exampleSubmodule --|> Submodule
exampleSubmodule --|> exampleSubmoduleFactory
exampleSubmoduleFactory --> exampleSubmodule
Loading

Example Submodule with options

classDiagram
class IntegratableModule {
    <<interface>>
    +GetBus() Bus
    +SetBus(bus Bus)
}

class InjectableModule {
    <<interface>>
    GetModuleName() string
}


class Submodule {
    <<interface>>
}

Submodule --|> InjectableModule
Submodule --|> IntegratableModule

class exampleSubmodule
class exampleSubmoduleFactory {
    <<interface>>
    Create(bus Bus, options ...exampleSubmoduleOption) (exampleSubmodule, error)
}

exampleSubmodule --|> Submodule
exampleSubmodule --|> exampleSubmoduleFactory
exampleSubmoduleFactory --> exampleSubmodule
exampleSubmoduleFactory --* "0..n" exampleSubmoduleOption
exampleSubmoduleOption --> exampleSubmodule
Loading

Example Submodule with config

classDiagram
class IntegratableModule {
    <<interface>>
    +GetBus() Bus
    +SetBus(bus Bus)
}

class InjectableModule {
    <<interface>>
    GetModuleName() string
}


class Submodule {
    <<interface>>
}

Submodule --|> InjectableModule
Submodule --|> IntegratableModule

class exampleSubmodule
class exampleSubmoduleFactory {
    <<interface>>
    Create(bus Bus, config exampleSubmoduleConfig) (exampleSubmodule, error)
}

exampleSubmodule --|> Submodule
exampleSubmodule --|> exampleSubmoduleFactory
exampleSubmoduleFactory --> exampleSubmodule
exampleSubmoduleFactory --* exampleSubmoduleConfig
exampleSubmodule --> exampleSubmoduleConfig
Loading

Example Submodule with config & options

classDiagram
class IntegratableModule {
    <<interface>>
    +GetBus() Bus
    +SetBus(bus Bus)
}

class InjectableModule {
    <<interface>>
    GetModuleName() string
}


class Submodule {
    <<interface>>
}

Submodule --|> InjectableModule
Submodule --|> IntegratableModule

class exampleSubmodule
class exampleSubmoduleFactory {
    <<interface>>
    Create(bus Bus, config exampleCfg, option ...exampleOpt) (exampleSubmodule, error)
}

exampleSubmodule --|> Submodule
exampleSubmodule --|> exampleSubmoduleFactory
exampleSubmoduleFactory --> exampleSubmodule
exampleSubmoduleFactory --* "0..n" exampleOpt
exampleOpt --> exampleSubmodule
exampleSubmoduleFactory --* exampleCfg
exampleSubmodule --> exampleCfg
Loading

@Olshansk
Copy link
Member

@bryanchriswhite I reviewed the diagrams you provided above very carefully and am 100% aligned with the approach.

As we discussed offline, I think we should

  1. Remove all func NewXXX in the codebase
  2. Update all existing modules in the codebase in the next iteration
  3. Potentially uncover other issues (or not)
  4. Update https://github.com/pokt-network/pocket/wiki/Modules-Readme w/ what you have outlined above

Do you have any other questions or feedback or anything? From my perspective, the above is a pretty clear and well-formed solution.

bryanchriswhite added a commit that referenced this issue Jun 13, 2023
## Description

1. Simplify `PeerstoreProvider` interface
2. Extend it to support retrieval of the unstaked actor peerstore
3. Implement the extended interface in `persistencePeerstoreProvider`

### Before

```mermaid
classDiagram 

class InterruptableModule {
    <<interface>>
    Start() error
    Stop() error
}
class IntegratableModule {
    <<interface>>
    +SetBus(bus Bus)
    +GetBus() Bus
}
class InitializableModule {
    <<interface>>
    +GetModuleName() string
    +Create(bus Bus, opts ...Option) (Module, error)
}
class Module {
    <<interface>>
}

Module --|> InitializableModule
Module --|> IntegratableModule
Module --|> InterruptableModule


class PeerstoreProvider {
    <<interface>>
    +GetStakedPeerstoreAtHeight(height int) (Peerstore, error)
    +GetP2PConfig() *P2PConfig
}

class persistencePeerstoreProvider

class rpcPeerstoreProvider

persistencePeerstoreProvider --|> PeerstoreProvider

rpcPeerstoreProvider --|> PeerstoreProvider
PeerstoreProvider --|> Module
```

### After

```mermaid
classDiagram 

class IntegratableModule {
    <<interface>>
    +GetBus() Bus
    +SetBus(bus Bus)
}

class PeerstoreProvider {
    <<interface>>
    +GetStakedPeerstoreAtHeight(height int) (Peerstore, error)
    +GetUnstakedPeerstore() (Peerstore, error)
}

class persistencePeerstoreProvider
class rpcPeerstoreProvider
class p2pModule

class unstakedPeerstoreProvider {
    <<interface>>
    +GetUnstakedPeerstore() (Peerstore, error)
}

persistencePeerstoreProvider --|> PeerstoreProvider
persistencePeerstoreProvider --> p2pModule : from Bus
rpcPeerstoreProvider --> p2pModule : from Bus
p2pModule --|> unstakedPeerstoreProvider

rpcPeerstoreProvider --|> PeerstoreProvider
rpcPeerstoreProvider --|> Module
PeerstoreProvider --|> IntegratableModule

class Module {
    <<interface>>
}

Module --|> InitializableModule
Module --|> IntegratableModule
Module --|> InterruptableModule
```

## Issue

Realted:
- #810

Dependants:
- #505 
- #806

## Type of change

Please mark the relevant option(s):

- [ ] New feature, functionality or library
- [ ] Bug fix
- [ ] Code health or cleanup
- [ ] Major breaking change
- [ ] Documentation
- [ ] Other <!-- add details here if it a different type of change -->

## List of changes

- Replaced embedded `modules.Module` with simpler
`modules.IntegratableModule` in `PeerstoreProvider` interface
- Removed unused `PeerstoreProvider#GetP2PConfig()` method
- Added `PeerstoreProvider#GetUnstakedPeerstore()` method
- Added `p2pPeerstoreProvider` implementation of `PeerstoreProvider`
interface
- Added `Factory` generic type 

## Testing

- [ ] `make develop_test`; if any code changes were made
- [x] `make test_e2e` on [k8s
LocalNet](https://github.com/pokt-network/pocket/blob/main/build/localnet/README.md);
if any code changes were made
- [ ] `e2e-devnet-test` passes tests on
[DevNet](https://pocketnetwork.notion.site/How-to-DevNet-ff1598f27efe44c09f34e2aa0051f0dd);
if any code was changed
- [x] [Docker Compose
LocalNet](https://github.com/pokt-network/pocket/blob/main/docs/development/README.md);
if any major functionality was changed or introduced
- [x] [k8s
LocalNet](https://github.com/pokt-network/pocket/blob/main/build/localnet/README.md);
if any infrastructure or configuration changes were made

<!-- REMOVE this comment block after following the instructions
 If you added additional tests or infrastructure, describe it here.
 Bonus points for images and videos or gifs.
-->

## Required Checklist

- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have added, or updated, [`godoc` format
comments](https://go.dev/blog/godoc) on touched members (see:
[tip.golang.org/doc/comment](https://tip.golang.org/doc/comment))
- [ ] I have tested my changes using the available tooling
- [ ] I have updated the corresponding CHANGELOG

### If Applicable Checklist

- [ ] I have updated the corresponding README(s); local and/or global
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have added, or updated,
[mermaid.js](https://mermaid-js.github.io) diagrams in the corresponding
README(s)
- [ ] I have added, or updated, documentation and
[mermaid.js](https://mermaid-js.github.io) diagrams in `shared/docs/*`
if I updated `shared/*`README(s)
@bryanchriswhite bryanchriswhite linked a pull request Jun 26, 2023 that will close this issue
20 tasks
@bryanchriswhite bryanchriswhite removed the triage It requires some decision-making at team level (it can't be worked on as it stands) label Jun 26, 2023
bryanchriswhite added a commit that referenced this issue Jul 11, 2023
## Description

Introduces the `Submodule` interface and applies it to the P2P module's
peerstore provider.

## Issue

Deliverables 1, 2, 3, & 5:
- #810

## Type of change

Please mark the relevant option(s):

- [ ] New feature, functionality or library
- [ ] Bug fix
- [x] Code health or cleanup
- [ ] Major breaking change
- [x] Documentation
- [ ] Other <!-- add details here if it a different type of change -->

## List of changes

* fixed typo; renamee `IntegratableModule` to `IntegrableModule`
* renamed `InitializableModule` to `InjectableModule`
* removed `InjectableModule#Create()` as `Moudle` also embeds
`ModuleFactoryWithConfig`
* added `Submodule` interface type
* refactored peerstore providers as submodules
* updated module README

## Testing

- [x] `make develop_test`; if any code changes were made
- [x] `make test_e2e` on [k8s
LocalNet](https://github.com/pokt-network/pocket/blob/main/build/localnet/README.md);
if any code changes were made
- [x] `e2e-devnet-test` passes tests on
[DevNet](https://pocketnetwork.notion.site/How-to-DevNet-ff1598f27efe44c09f34e2aa0051f0dd);
if any code was changed
- [ ] [Docker Compose
LocalNet](https://github.com/pokt-network/pocket/blob/main/docs/development/README.md);
if any major functionality was changed or introduced
- [x] [k8s
LocalNet](https://github.com/pokt-network/pocket/blob/main/build/localnet/README.md);
if any infrastructure or configuration changes were made

## Required Checklist

- [x] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have added, or updated, [`godoc` format
comments](https://go.dev/blog/godoc) on touched members (see:
[tip.golang.org/doc/comment](https://tip.golang.org/doc/comment))
- [ ] I have tested my changes using the available tooling
- [ ] I have updated the corresponding CHANGELOG

### If Applicable Checklist

- [x] I have updated the corresponding README(s); local and/or global
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] I have added, or updated,
[mermaid.js](https://mermaid-js.github.io) diagrams in the corresponding
README(s)
- [ ] I have added, or updated, documentation and
[mermaid.js](https://mermaid-js.github.io) diagrams in `shared/docs/*`
if I updated `shared/*`README(s)

---------

Co-authored-by: d7t <[email protected]>
Co-authored-by: @olshansky <[email protected]>
Co-authored-by: Daniel Olshansky <[email protected]>
Co-authored-by: Daniel Olshansky <[email protected]>
h5law added a commit that referenced this issue Jul 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
code health Nice to have code improvement core Core infrastructure - protocol related
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

2 participants