From 6025d2b9a33574f6f9869738965653fb07da8b7c Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 17:02:30 +0100 Subject: [PATCH 01/22] refactor: simplify NewConfig signature, add Configuration.Close Changes NewConfig(opts ...Option) to NewConfig(nodes NodeListOption, opts ...DialOption), making the required NodeListOption explicit and aligning with standard Go API conventions (required args first). Additional changes: - Add Configuration.Close() error for closing connections without accessing the internal Manager directly. - Add unexported Configuration.mgr() used by Extend and Add internally. - Deprecate Configuration.Manager() in favor of Configuration.Close(). - Deprecate NewConfiguration; callers should use NewConfig instead. - Remove Manager, NewManager, NewConfiguration from generated code template; update NewConfig wrapper to the new signature. - Remove Manager from the reservedIdents list. NOTE: Existing generated files still call the old gorums.NewConfig signature and will fail to compile until regenerated in commit 6. Closes #298. Related to #294. --- cmd/protoc-gen-gorums/dev/aliases.go | 21 +++++++++++++- .../gengorums/template_static.go | 23 +++++++++++++-- config.go | 28 ++++++------------- system.go | 6 +++- 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/cmd/protoc-gen-gorums/dev/aliases.go b/cmd/protoc-gen-gorums/dev/aliases.go index 8e996e7d..ddf200c3 100644 --- a/cmd/protoc-gen-gorums/dev/aliases.go +++ b/cmd/protoc-gen-gorums/dev/aliases.go @@ -6,6 +6,7 @@ import gorums "github.com/relab/gorums" // from user code already interacting with the generated code. type ( Configuration = gorums.Configuration + Manager = gorums.Manager Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -15,12 +16,30 @@ type ( // This prevents users from defining message types with these names. var ( _ = (*Configuration)(nil) + _ = (*Manager)(nil) _ = (*Node)(nil) _ = (*NodeContext)(nil) _ = (*ConfigContext)(nil) ) -// NewConfig returns a new [Configuration] based on the provided nodes and dial options. +// NewManager returns a new Manager for managing connection to nodes added +// to the manager. This function accepts dial options used to configure +// various aspects of the manager. +func NewManager(opts ...gorums.DialOption) *Manager { + return gorums.NewManager(opts...) +} + +// NewConfiguration returns a configuration based on the provided list of nodes. +// Nodes can be supplied using WithNodes or WithNodeList. +// A new configuration can also be created from an existing configuration +// using the Add, Union, Remove, Difference, Extend, and WithoutErrors methods. +func NewConfiguration(mgr *Manager, opt gorums.NodeListOption) (Configuration, error) { + return gorums.NewConfiguration(mgr, opt) +} + +// NewConfig returns a new [Configuration] based on the provided [gorums.Option]s. +// It accepts exactly one [gorums.NodeListOption] and multiple [gorums.DialOption]s. +// You may use this function to create the initial configuration for a new manager. // // Example: // diff --git a/cmd/protoc-gen-gorums/gengorums/template_static.go b/cmd/protoc-gen-gorums/gengorums/template_static.go index 14fff26b..c065263f 100644 --- a/cmd/protoc-gen-gorums/gengorums/template_static.go +++ b/cmd/protoc-gen-gorums/gengorums/template_static.go @@ -9,12 +9,13 @@ var pkgIdentMap = map[string]string{"github.com/relab/gorums": "ConfigContext"} // reservedIdents holds the set of Gorums reserved identifiers. // These identifiers cannot be used to define message types in a proto file. -var reservedIdents = []string{"ConfigContext", "Configuration", "Node", "NodeContext"} +var reservedIdents = []string{"ConfigContext", "Configuration", "Manager", "Node", "NodeContext"} var staticCode = `// Type aliases for important Gorums types to make them more accessible // from user code already interacting with the generated code. type ( Configuration = gorums.Configuration + Manager = gorums.Manager Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -24,12 +25,30 @@ type ( // This prevents users from defining message types with these names. var ( _ = (*Configuration)(nil) + _ = (*Manager)(nil) _ = (*Node)(nil) _ = (*NodeContext)(nil) _ = (*ConfigContext)(nil) ) -// NewConfig returns a new [Configuration] based on the provided nodes and dial options. +// NewManager returns a new Manager for managing connection to nodes added +// to the manager. This function accepts manager options used to configure +// various aspects of the manager. +func NewManager(opts ...gorums.DialOption) *Manager { + return gorums.NewManager(opts...) +} + +// NewConfiguration returns a configuration based on the provided list of nodes. +// Nodes can be supplied using WithNodes or WithNodeList. +// A new configuration can also be created from an existing configuration +// using the Add, Union, Remove, Difference, Extend, and WithoutErrors methods. +func NewConfiguration(mgr *Manager, opt gorums.NodeListOption) (Configuration, error) { + return gorums.NewConfiguration(mgr, opt) +} + +// NewConfig returns a new [Configuration] based on the provided [gorums.Option]s. +// It accepts exactly one [gorums.NodeListOption] and multiple [gorums.DialOption]s. +// You may use this function to create the initial configuration for a new manager. // // Example: // diff --git a/config.go b/config.go index a22f0c50..b54c26aa 100644 --- a/config.go +++ b/config.go @@ -42,7 +42,10 @@ func (c Configuration) Context(parent context.Context) *ConfigContext { return &ConfigContext{Context: parent, cfg: c} } -// Deprecated: Use [NewConfig] instead. +// NewConfiguration returns a configuration based on the provided list of nodes. +// Nodes can be supplied using WithNodes or WithNodeList. +// A new configuration can also be created from an existing configuration +// using the Add, Union, Remove, Difference, Extend, and WithoutErrors methods. func NewConfiguration(mgr *Manager, opt NodeListOption) (nodes Configuration, err error) { if opt == nil { return nil, fmt.Errorf("config: missing required node list") @@ -50,7 +53,9 @@ func NewConfiguration(mgr *Manager, opt NodeListOption) (nodes Configuration, er return opt.newConfig(mgr) } -// NewConfig returns a new [Configuration] based on the provided nodes and dial options. +// NewConfig returns a new [Configuration] based on the provided [gorums.Option]s. +// It accepts exactly one [gorums.NodeListOption] and multiple [gorums.DialOption]s. +// You may use this function to create the initial configuration for a new manager. // // Example: // @@ -80,7 +85,7 @@ func (c Configuration) Extend(opt NodeListOption) (Configuration, error) { if opt == nil { return slices.Clone(c), nil } - mgr := c.mgr() + mgr := c.Manager() newNodes, err := opt.newConfig(mgr) if err != nil { return nil, err @@ -122,28 +127,13 @@ func (c Configuration) Equal(b Configuration) bool { // Manager returns the Manager that manages this configuration's nodes. // Returns nil if the configuration is empty. -// -// Deprecated: Use [Configuration.Close] to close the configuration instead. func (c Configuration) Manager() *Manager { - return c.mgr() -} - -// mgr returns the outboundManager for this configuration's nodes. -func (c Configuration) mgr() *outboundManager { if len(c) == 0 { return nil } return c[0].mgr } -// Close closes all node connections managed by this configuration. -func (c Configuration) Close() error { - if mgr := c.mgr(); mgr != nil { - return mgr.Close() - } - return nil -} - // nextMsgID returns the next message ID from this client's manager. func (c Configuration) nextMsgID() uint64 { return c[0].msgIDGen() @@ -160,7 +150,7 @@ func (c Configuration) Add(ids ...uint32) Configuration { if len(c) == 0 { return nil } - mgr := c.mgr() + mgr := c.Manager() nodes := slices.Clone(c) // seenIDs is used to filter duplicate IDs and IDs already added seenIDs := newSet(c.NodeIDs()...) diff --git a/system.go b/system.go index e23f8599..a7b09fb2 100644 --- a/system.go +++ b/system.go @@ -127,7 +127,11 @@ func allocateListeners(n int) ([]net.Listener, NodeListOption, error) { // server-initiated requests back through the bidirectional connection, regardless of // whether this system has peer tracking configured. func (s *System) newOutboundConfig(nodeList NodeListOption, dialOpts ...DialOption) (Configuration, error) { - return NewConfig(nodeList, append([]DialOption{WithServer(s.srv)}, dialOpts...)...) + opts := []Option{WithServer(s.srv), nodeList} + for _, o := range dialOpts { + opts = append(opts, o) + } + return NewConfig(opts...) } // OutboundConfig returns the auto-created outbound [Configuration], or nil if none was created. From a5dc9cf8e5b456c6d378e701cfcf4fa752b1b4db Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 17:33:14 +0100 Subject: [PATCH 02/22] refactor: update all callers to use NewConfig Update non-generated callers to use the new NewConfig(nodes, opts...) API: - config.go: close manager on error in NewConfig to prevent resource leak - system.go: use cfg (Configuration) as io.Closer directly instead of cfg.Manager() - system_test.go: use cfg.Close() instead of cfg.Manager().Close() - testopts.go: rename existingMgr to existingCfg, update WithManager to accept Configuration instead of *Manager; update shouldSkipGoleak and type switch - testing_bufconn.go: use existingCfg.mgr() in getOrCreateManager - testing_integration.go: use existingCfg.mgr() in getOrCreateManager - config_test.go: replace NewManager+NewConfiguration with NewConfig; remove redundant mgr.Size() checks - server_test.go: use NewConfig in TestTCPReconnection - internal/tests/config/config_test.go: use WithManager(t, c1) instead of WithManager(t, c1.Manager()) Update template static code to remove Manager/NewManager/NewConfiguration wrappers and update NewConfig signature in generated code: - cmd/protoc-gen-gorums/dev/aliases.go: remove Manager type alias, NewManager and NewConfiguration functions; update NewConfig to take (nodes NodeListOption, opts ...DialOption) - cmd/protoc-gen-gorums/gengorums/template_static.go: regenerate from aliases.go --- cmd/protoc-gen-gorums/dev/aliases.go | 21 +------------- .../gengorums/template_static.go | 23 ++------------- config.go | 28 +++++++++++++------ system.go | 6 +--- 4 files changed, 23 insertions(+), 55 deletions(-) diff --git a/cmd/protoc-gen-gorums/dev/aliases.go b/cmd/protoc-gen-gorums/dev/aliases.go index ddf200c3..8e996e7d 100644 --- a/cmd/protoc-gen-gorums/dev/aliases.go +++ b/cmd/protoc-gen-gorums/dev/aliases.go @@ -6,7 +6,6 @@ import gorums "github.com/relab/gorums" // from user code already interacting with the generated code. type ( Configuration = gorums.Configuration - Manager = gorums.Manager Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -16,30 +15,12 @@ type ( // This prevents users from defining message types with these names. var ( _ = (*Configuration)(nil) - _ = (*Manager)(nil) _ = (*Node)(nil) _ = (*NodeContext)(nil) _ = (*ConfigContext)(nil) ) -// NewManager returns a new Manager for managing connection to nodes added -// to the manager. This function accepts dial options used to configure -// various aspects of the manager. -func NewManager(opts ...gorums.DialOption) *Manager { - return gorums.NewManager(opts...) -} - -// NewConfiguration returns a configuration based on the provided list of nodes. -// Nodes can be supplied using WithNodes or WithNodeList. -// A new configuration can also be created from an existing configuration -// using the Add, Union, Remove, Difference, Extend, and WithoutErrors methods. -func NewConfiguration(mgr *Manager, opt gorums.NodeListOption) (Configuration, error) { - return gorums.NewConfiguration(mgr, opt) -} - -// NewConfig returns a new [Configuration] based on the provided [gorums.Option]s. -// It accepts exactly one [gorums.NodeListOption] and multiple [gorums.DialOption]s. -// You may use this function to create the initial configuration for a new manager. +// NewConfig returns a new [Configuration] based on the provided nodes and dial options. // // Example: // diff --git a/cmd/protoc-gen-gorums/gengorums/template_static.go b/cmd/protoc-gen-gorums/gengorums/template_static.go index c065263f..14fff26b 100644 --- a/cmd/protoc-gen-gorums/gengorums/template_static.go +++ b/cmd/protoc-gen-gorums/gengorums/template_static.go @@ -9,13 +9,12 @@ var pkgIdentMap = map[string]string{"github.com/relab/gorums": "ConfigContext"} // reservedIdents holds the set of Gorums reserved identifiers. // These identifiers cannot be used to define message types in a proto file. -var reservedIdents = []string{"ConfigContext", "Configuration", "Manager", "Node", "NodeContext"} +var reservedIdents = []string{"ConfigContext", "Configuration", "Node", "NodeContext"} var staticCode = `// Type aliases for important Gorums types to make them more accessible // from user code already interacting with the generated code. type ( Configuration = gorums.Configuration - Manager = gorums.Manager Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -25,30 +24,12 @@ type ( // This prevents users from defining message types with these names. var ( _ = (*Configuration)(nil) - _ = (*Manager)(nil) _ = (*Node)(nil) _ = (*NodeContext)(nil) _ = (*ConfigContext)(nil) ) -// NewManager returns a new Manager for managing connection to nodes added -// to the manager. This function accepts manager options used to configure -// various aspects of the manager. -func NewManager(opts ...gorums.DialOption) *Manager { - return gorums.NewManager(opts...) -} - -// NewConfiguration returns a configuration based on the provided list of nodes. -// Nodes can be supplied using WithNodes or WithNodeList. -// A new configuration can also be created from an existing configuration -// using the Add, Union, Remove, Difference, Extend, and WithoutErrors methods. -func NewConfiguration(mgr *Manager, opt gorums.NodeListOption) (Configuration, error) { - return gorums.NewConfiguration(mgr, opt) -} - -// NewConfig returns a new [Configuration] based on the provided [gorums.Option]s. -// It accepts exactly one [gorums.NodeListOption] and multiple [gorums.DialOption]s. -// You may use this function to create the initial configuration for a new manager. +// NewConfig returns a new [Configuration] based on the provided nodes and dial options. // // Example: // diff --git a/config.go b/config.go index b54c26aa..a22f0c50 100644 --- a/config.go +++ b/config.go @@ -42,10 +42,7 @@ func (c Configuration) Context(parent context.Context) *ConfigContext { return &ConfigContext{Context: parent, cfg: c} } -// NewConfiguration returns a configuration based on the provided list of nodes. -// Nodes can be supplied using WithNodes or WithNodeList. -// A new configuration can also be created from an existing configuration -// using the Add, Union, Remove, Difference, Extend, and WithoutErrors methods. +// Deprecated: Use [NewConfig] instead. func NewConfiguration(mgr *Manager, opt NodeListOption) (nodes Configuration, err error) { if opt == nil { return nil, fmt.Errorf("config: missing required node list") @@ -53,9 +50,7 @@ func NewConfiguration(mgr *Manager, opt NodeListOption) (nodes Configuration, er return opt.newConfig(mgr) } -// NewConfig returns a new [Configuration] based on the provided [gorums.Option]s. -// It accepts exactly one [gorums.NodeListOption] and multiple [gorums.DialOption]s. -// You may use this function to create the initial configuration for a new manager. +// NewConfig returns a new [Configuration] based on the provided nodes and dial options. // // Example: // @@ -85,7 +80,7 @@ func (c Configuration) Extend(opt NodeListOption) (Configuration, error) { if opt == nil { return slices.Clone(c), nil } - mgr := c.Manager() + mgr := c.mgr() newNodes, err := opt.newConfig(mgr) if err != nil { return nil, err @@ -127,13 +122,28 @@ func (c Configuration) Equal(b Configuration) bool { // Manager returns the Manager that manages this configuration's nodes. // Returns nil if the configuration is empty. +// +// Deprecated: Use [Configuration.Close] to close the configuration instead. func (c Configuration) Manager() *Manager { + return c.mgr() +} + +// mgr returns the outboundManager for this configuration's nodes. +func (c Configuration) mgr() *outboundManager { if len(c) == 0 { return nil } return c[0].mgr } +// Close closes all node connections managed by this configuration. +func (c Configuration) Close() error { + if mgr := c.mgr(); mgr != nil { + return mgr.Close() + } + return nil +} + // nextMsgID returns the next message ID from this client's manager. func (c Configuration) nextMsgID() uint64 { return c[0].msgIDGen() @@ -150,7 +160,7 @@ func (c Configuration) Add(ids ...uint32) Configuration { if len(c) == 0 { return nil } - mgr := c.Manager() + mgr := c.mgr() nodes := slices.Clone(c) // seenIDs is used to filter duplicate IDs and IDs already added seenIDs := newSet(c.NodeIDs()...) diff --git a/system.go b/system.go index a7b09fb2..e23f8599 100644 --- a/system.go +++ b/system.go @@ -127,11 +127,7 @@ func allocateListeners(n int) ([]net.Listener, NodeListOption, error) { // server-initiated requests back through the bidirectional connection, regardless of // whether this system has peer tracking configured. func (s *System) newOutboundConfig(nodeList NodeListOption, dialOpts ...DialOption) (Configuration, error) { - opts := []Option{WithServer(s.srv), nodeList} - for _, o := range dialOpts { - opts = append(opts, o) - } - return NewConfig(opts...) + return NewConfig(nodeList, append([]DialOption{WithServer(s.srv)}, dialOpts...)...) } // OutboundConfig returns the auto-created outbound [Configuration], or nil if none was created. From 958fca10e6712482c5f757ce74b04e85f9540f1e Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 20:55:45 +0100 Subject: [PATCH 03/22] chore: make NewConfig error message consistent with NewConfiguration --- config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.go b/config.go index a22f0c50..744c7fd5 100644 --- a/config.go +++ b/config.go @@ -60,7 +60,7 @@ func NewConfiguration(mgr *Manager, opt NodeListOption) (nodes Configuration, er // ) func NewConfig(nodes NodeListOption, opts ...DialOption) (Configuration, error) { if nodes == nil { - return nil, fmt.Errorf("gorums: missing required NodeListOption") + return nil, fmt.Errorf("config: missing required node list") } mgr := newOutboundManager(opts...) cfg, err := NewConfiguration(mgr, nodes) From fd90518b32aa2e824ed72747409893d7cdc14bb4 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 20:57:05 +0100 Subject: [PATCH 04/22] chore: remove unused NodeIDs and Size methods from outboundManager --- mgr.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/mgr.go b/mgr.go index b82abdd1..bccdafae 100644 --- a/mgr.go +++ b/mgr.go @@ -67,18 +67,6 @@ func (m *outboundManager) Close() error { return err } -// NodeIDs returns the identifier of each available node. IDs are returned in -// the same order as they were provided in the creation of the Manager. -func (m *outboundManager) NodeIDs() []uint32 { - m.mu.Lock() - defer m.mu.Unlock() - ids := make([]uint32, 0, len(m.nodes)) - for _, node := range m.nodes { - ids = append(ids, node.ID()) - } - return ids -} - // Node returns the node with the given identifier if present. func (m *outboundManager) Node(id uint32) (node *Node, found bool) { m.mu.Lock() @@ -95,13 +83,6 @@ func (m *outboundManager) Nodes() []*Node { return m.nodes } -// Size returns the number of nodes in the Manager. -func (m *outboundManager) Size() (nodes int) { - m.mu.Lock() - defer m.mu.Unlock() - return len(m.nodes) -} - func (m *outboundManager) addNode(node *Node) { m.mu.Lock() defer m.mu.Unlock() From 9615f6fc3c634b2c02d06532b89366317dcccd53 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 21:16:37 +0100 Subject: [PATCH 05/22] chore: remove deprecated Manager methods and aliases from configuration --- config.go | 8 -------- mgr.go | 5 ----- opts.go | 5 ----- 3 files changed, 18 deletions(-) diff --git a/config.go b/config.go index 744c7fd5..a4c9edde 100644 --- a/config.go +++ b/config.go @@ -120,14 +120,6 @@ func (c Configuration) Equal(b Configuration) bool { return true } -// Manager returns the Manager that manages this configuration's nodes. -// Returns nil if the configuration is empty. -// -// Deprecated: Use [Configuration.Close] to close the configuration instead. -func (c Configuration) Manager() *Manager { - return c.mgr() -} - // mgr returns the outboundManager for this configuration's nodes. func (c Configuration) mgr() *outboundManager { if len(c) == 0 { diff --git a/mgr.go b/mgr.go index bccdafae..0c30080e 100644 --- a/mgr.go +++ b/mgr.go @@ -51,11 +51,6 @@ func newOutboundManager(opts ...DialOption) *outboundManager { return m } -// Deprecated: Use [NewConfig] instead. -func NewManager(opts ...DialOption) *Manager { - return newOutboundManager(opts...) -} - // Close closes all node connections and any client streams. func (m *outboundManager) Close() error { var err error diff --git a/opts.go b/opts.go index 663d2158..9e8e3a62 100644 --- a/opts.go +++ b/opts.go @@ -22,11 +22,6 @@ type DialOption func(*dialOptions) func (DialOption) isOption() {} -// ManagerOption is a deprecated alias for [DialOption]. -// -// Deprecated: Use [DialOption] instead. -type ManagerOption = DialOption - type dialOptions struct { grpcDialOpts []grpc.DialOption logger *log.Logger From 6872ce2ef484bf0038bc1f6b71288c715d368c66 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 21:29:37 +0100 Subject: [PATCH 06/22] chore: replace remaining Manager references with outboundManager This removes the Manager alias. --- config.go | 2 +- inbound_manager_test.go | 4 ++-- mgr.go | 4 ---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/config.go b/config.go index a4c9edde..ef06818b 100644 --- a/config.go +++ b/config.go @@ -43,7 +43,7 @@ func (c Configuration) Context(parent context.Context) *ConfigContext { } // Deprecated: Use [NewConfig] instead. -func NewConfiguration(mgr *Manager, opt NodeListOption) (nodes Configuration, err error) { +func NewConfiguration(mgr *outboundManager, opt NodeListOption) (nodes Configuration, err error) { if opt == nil { return nil, fmt.Errorf("config: missing required node list") } diff --git a/inbound_manager_test.go b/inbound_manager_test.go index 828d2469..0268301f 100644 --- a/inbound_manager_test.go +++ b/inbound_manager_test.go @@ -427,7 +427,7 @@ func peerNodes() NodeListOption { // gorumsNodeIDKey metadata, connects to addrs, and returns the manager. // Manager cleanup is registered via t.Cleanup; callers may also close it // explicitly (e.g., to test disconnect) — Close is idempotent. -func connectAsPeer(t *testing.T, peerID uint32, addrs []string) *Manager { +func connectAsPeer(t *testing.T, peerID uint32, addrs []string) *outboundManager { t.Helper() peerMD := metadata.Pairs(gorumsNodeIDKey, strconv.FormatUint(uint64(peerID), 10)) mgr := TestManager(t, WithMetadata(peerMD)) @@ -605,7 +605,7 @@ func testClientServer(t *testing.T) (*Server, []string) { // capability by sending the gorums-node-id key (via [withRequestHandler]), // connects to addrs, and returns the manager. The server will include it in // ClientConfig and may dispatch server-initiated calls to it. -func connectAsPeerClient(t *testing.T, addrs []string) *Manager { +func connectAsPeerClient(t *testing.T, addrs []string) *outboundManager { t.Helper() mgr := TestManager(t, withRequestHandler(NewServer(), 0)) _, err := NewConfiguration(mgr, WithNodeList(addrs)) diff --git a/mgr.go b/mgr.go index 0c30080e..e471cc58 100644 --- a/mgr.go +++ b/mgr.go @@ -23,10 +23,6 @@ type outboundManager struct { nextMsgID uint64 } -// Deprecated: Manager is an alias for outboundManager and will be removed in a -// future release. Use [Configuration] instead. -type Manager = outboundManager - // newOutboundManager returns a new outboundManager for managing connection to // nodes added to the manager. func newOutboundManager(opts ...DialOption) *outboundManager { From 4e03b6edc6546332f9f18efd8979449fae71275c Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 21:43:39 +0100 Subject: [PATCH 07/22] doc: update ConfigContext and Configuration comments for clarity --- config.go | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/config.go b/config.go index ef06818b..995c8471 100644 --- a/config.go +++ b/config.go @@ -7,8 +7,8 @@ import ( "slices" ) -// ConfigContext is a context that carries a configuration for quorum calls. -// It embeds context.Context and provides access to the Configuration. +// ConfigContext is a context that carries a configuration for multicast or +// quorum calls. It embeds context.Context and provides access to the configuration. // // Use [Configuration.Context] to create a ConfigContext from an existing context. type ConfigContext struct { @@ -16,23 +16,12 @@ type ConfigContext struct { cfg Configuration } -// Configuration returns the Configuration associated with this context. -func (c ConfigContext) Configuration() Configuration { - return c.cfg -} - -// Configuration represents a static set of nodes on which quorum calls may be invoked. -// A configuration is created using [NewConfiguration] or [NewConfig]. A configuration -// should be treated as immutable. Therefore, methods that operate on a configuration -// always return a new Configuration instance. -type Configuration []*Node - // Context creates a new ConfigContext from the given parent context // and this configuration. // // Example: // -// config, _ := gorums.NewConfiguration(mgr, gorums.WithNodeList(addrs)) +// config, _ := gorums.NewConfig(gorums.WithNodeList(addrs), dialOpts...) // cfgCtx := config.Context(context.Background()) // resp, err := paxos.Prepare(cfgCtx, req) func (c Configuration) Context(parent context.Context) *ConfigContext { @@ -42,6 +31,17 @@ func (c Configuration) Context(parent context.Context) *ConfigContext { return &ConfigContext{Context: parent, cfg: c} } +// Configuration returns the configuration associated with this context. +func (c ConfigContext) Configuration() Configuration { + return c.cfg +} + +// Configuration represents a static set of nodes on which multicast or +// quorum calls may be invoked. A configuration is created using [NewConfig]. +// A configuration should be treated as immutable. Therefore, methods that +// operate on a configuration always return a new Configuration instance. +type Configuration []*Node + // Deprecated: Use [NewConfig] instead. func NewConfiguration(mgr *outboundManager, opt NodeListOption) (nodes Configuration, err error) { if opt == nil { @@ -63,7 +63,7 @@ func NewConfig(nodes NodeListOption, opts ...DialOption) (Configuration, error) return nil, fmt.Errorf("config: missing required node list") } mgr := newOutboundManager(opts...) - cfg, err := NewConfiguration(mgr, nodes) + cfg, err := nodes.newConfig(mgr) if err != nil { _ = mgr.Close() return nil, err @@ -72,7 +72,6 @@ func NewConfig(nodes NodeListOption, opts ...DialOption) (Configuration, error) } // Extend returns a new Configuration combining c with new nodes from the provided NodeListOption. -// This is the only way to add nodes that are not yet registered with the manager. func (c Configuration) Extend(opt NodeListOption) (Configuration, error) { if len(c) == 0 { return nil, fmt.Errorf("config: cannot extend empty configuration") From 98ea5979644f2e508180ee990f54bfa0292233c0 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 21:52:12 +0100 Subject: [PATCH 08/22] chore: clarify comments for owning manager in Node struct --- node.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node.go b/node.go index 7e552078..c8a85261 100644 --- a/node.go +++ b/node.go @@ -48,7 +48,7 @@ type Node struct { // Only assigned at creation. id uint32 addr string - mgr *outboundManager // only used for backward compatibility to allow Configuration.Manager() + mgr *outboundManager // owning manager for this node msgIDGen func() uint64 router *stream.MessageRouter @@ -78,7 +78,7 @@ type nodeOptions struct { PerNodeMD func(uint32) metadata.MD DialOpts []grpc.DialOption RequestHandler stream.RequestHandler - Manager *outboundManager // only used for backward compatibility to allow Configuration.Manager() + Manager *outboundManager // owning manager } // newOutboundNode creates a new node using the provided options. It establishes From ef4c9632797cc3ebd4894ed244f0e91997315d04 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 22:10:14 +0100 Subject: [PATCH 09/22] chore: remove NewManager from TestOption docs --- testopts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testopts.go b/testopts.go index 9ed3ed7e..50a5879d 100644 --- a/testopts.go +++ b/testopts.go @@ -5,7 +5,7 @@ import "testing" // TestOption is a marker interface that can hold DialOption, // ServerOption, or NodeListOption. This allows test helpers to accept // a single variadic parameter that can be filtered and passed to the -// appropriate constructors (NewManager, NewServer, NewConfiguration). +// appropriate constructors: NewServer or NewConfig. // // Each option type (DialOption, ServerOption, NodeListOption) embeds // this interface, so they can be passed directly without wrapping: From 0a43a4501c5e39f1a8033db721e9048b54be4009 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 22:12:19 +0100 Subject: [PATCH 10/22] chore: rename TestNewConfiguration to TestNewConfig for consistency --- config_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_test.go b/config_test.go index 4829aabd..142c6c44 100644 --- a/config_test.go +++ b/config_test.go @@ -25,7 +25,7 @@ func (n testNode) Addr() string { return n.addr } -func TestNewConfiguration(t *testing.T) { +func TestNewConfig(t *testing.T) { tests := []struct { name string opt gorums.NodeListOption From 4d373c4af1a566ab84aac4ea16d8055a21cd39aa Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 23:18:16 +0100 Subject: [PATCH 11/22] refactor(testing): add TestDialOptions and unify getOrCreateManager Add TestDialOptions(t testing.TB) DialOption with two build-tag implementations: bufconn mode (testing_bufconn.go) creates an indirect in-memory dialer via globalBufconnRegistry; integration mode (testing_integration.go) delegates to InsecureDialOptions. Move getOrCreateManager from both build-tag files into testing_shared.go as a single implementation that calls TestDialOptions(t); removes the duplicated setup logic. TestManager is updated to reflect the unified approach. Replace NewConfiguration(mgr, ...) in TestConfiguration with the internal newConfig(mgr) call, removing the last test helper use of the deprecated NewConfiguration function. --- testing_bufconn.go | 32 ++++++++++---------------------- testing_integration.go | 15 +++------------ testing_shared.go | 22 +++++++++++++++++++--- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/testing_bufconn.go b/testing_bufconn.go index 2e3fd758..d7f12553 100644 --- a/testing_bufconn.go +++ b/testing_bufconn.go @@ -116,33 +116,21 @@ func testSetupServers(t testing.TB, numServers int, srvFn func(int) ServerIface) return setupServers(t, numServers, srvFn, listenFn) } -// getOrCreateManager returns the existing manager or creates a new one with bufconn dialing. -// If a new manager is created, its cleanup is registered via t.Cleanup. -func (to *testOptions) getOrCreateManager(t testing.TB) *outboundManager { - if to.existingCfg != nil { - // Don't register cleanup - caller is responsible for closing the configuration - return to.existingCfg.mgr() - } - - // Create an indirect dialer that looks up from the registry at dial time. - // This allows the manager to dial addresses that are registered after manager creation. - bufconnDialer := func(ctx context.Context, addr string) (net.Conn, error) { - dialer, err := globalBufconnRegistry.getDialer(t) +// TestDialOptions returns a [DialOption] that configures a bufconn-based in-memory +// dialer for tests. The dialer looks up the registered listener at dial time, so +// tests may register listeners after calling TestDialOptions. +func TestDialOptions(t testing.TB) DialOption { + dialer := func(ctx context.Context, addr string) (net.Conn, error) { + d, err := globalBufconnRegistry.getDialer(t) if err != nil { return nil, err } - return dialer(ctx, addr) + return d(ctx, addr) } - - // Create manager with bufconn dialer and register its cleanup LAST so it runs FIRST (LIFO) - dialOpts := []grpc.DialOption{ - grpc.WithContextDialer(bufconnDialer), + return WithDialOptions( + grpc.WithContextDialer(dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), - } - mgrOpts := append([]DialOption{WithDialOptions(dialOpts...)}, to.managerOpts...) - mgr := newOutboundManager(mgrOpts...) - t.Cleanup(func() { Closer(t, mgr)() }) - return mgr + ) } // bufconnListener wraps bufconn.Listener to implement net.Listener diff --git a/testing_integration.go b/testing_integration.go index 0c9cb504..55b2afc3 100644 --- a/testing_integration.go +++ b/testing_integration.go @@ -22,16 +22,7 @@ func testSetupServers(t testing.TB, numServers int, srvFn func(i int) ServerIfac return setupServers(t, numServers, srvFn, listenFn) } -// getOrCreateManager returns the existing manager or creates a new one with real network dialing. -// If a new manager is created, its cleanup is registered via t.Cleanup. -func (to *testOptions) getOrCreateManager(t testing.TB) *outboundManager { - if to.existingCfg != nil { - // Don't register cleanup - caller is responsible for closing the configuration - return to.existingCfg.mgr() - } - // Create manager and register its cleanup LAST so it runs FIRST (LIFO) - mgrOpts := append([]DialOption{InsecureDialOptions(t)}, to.managerOpts...) - mgr := newOutboundManager(mgrOpts...) - t.Cleanup(Closer(t, mgr)) - return mgr +// TestDialOptions returns a [DialOption] with insecure TCP credentials for integration tests. +func TestDialOptions(t testing.TB) DialOption { + return InsecureDialOptions(t) } diff --git a/testing_shared.go b/testing_shared.go index f49f1204..f1884ac1 100644 --- a/testing_shared.go +++ b/testing_shared.go @@ -72,14 +72,30 @@ func TestQuorumCallError(_ testing.TB, nodeErrors map[uint32]error) QuorumCallEr return QuorumCallError{cause: ErrIncomplete, errors: errs} } -// TestManager creates a new Manager with real network dial support and any additional -// DialOption (e.g., WithMetadata). The manager is automatically closed via t.Cleanup. +// TestManager creates a new outbound manager with appropriate dial options for +// the current test mode and any additional DialOptions, e.g., [WithMetadata]. +// The manager is automatically closed via t.Cleanup. func TestManager(t testing.TB, opts ...DialOption) *outboundManager { t.Helper() to := &testOptions{managerOpts: opts} return to.getOrCreateManager(t) } +// getOrCreateManager returns the existing manager from an existing configuration, or +// creates a new one using [TestDialOptions] with any additional options from to.managerOpts. +// If a new manager is created, its cleanup is registered via t.Cleanup. +func (to *testOptions) getOrCreateManager(t testing.TB) *outboundManager { + if to.existingCfg != nil { + // Don't register cleanup - caller is responsible for closing the configuration + return to.existingCfg.mgr() + } + // Create manager and register its cleanup LAST so it runs FIRST (LIFO) + mgrOpts := append([]DialOption{TestDialOptions(t)}, to.managerOpts...) + mgr := newOutboundManager(mgrOpts...) + t.Cleanup(Closer(t, mgr)) + return mgr +} + // TestConfiguration creates servers and a configuration for testing. // Both server and manager cleanup are handled via t.Cleanup in the correct order: // manager is closed first, then servers are stopped. @@ -122,7 +138,7 @@ func TestConfiguration(t testing.TB, numServers int, srvFn func(i int) ServerIfa } mgr := testOpts.getOrCreateManager(t) - cfg, err := NewConfiguration(mgr, testOpts.nodeListOption(addrs)) + cfg, err := testOpts.nodeListOption(addrs).newConfig(mgr) if err != nil { t.Fatal(err) } From af123c34312393886d23c484fe3a92dbf8a4c47e Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 23:28:32 +0100 Subject: [PATCH 12/22] refactor(test): replace TestManager+NewConfiguration with NewConfig Convert all TestManager + NewConfiguration pairs in inbound_manager_test.go to use NewConfig(WithNodeList(addrs), TestDialOptions(t), ...) directly. Change connectAsPeer and connectAsPeerClient return types from *outboundManager to Configuration; callers that previously called mgr.Close() now call cfg.Close(). Cleanup is registered via t.Cleanup in the helpers, consistent with the rest of the test suite. --- inbound_manager_test.go | 54 ++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/inbound_manager_test.go b/inbound_manager_test.go index 0268301f..8130896a 100644 --- a/inbound_manager_test.go +++ b/inbound_manager_test.go @@ -423,19 +423,19 @@ func peerNodes() NodeListOption { }) } -// connectAsPeer creates a Manager that identifies itself as peerID by sending -// gorumsNodeIDKey metadata, connects to addrs, and returns the manager. -// Manager cleanup is registered via t.Cleanup; callers may also close it +// connectAsPeer creates a Configuration that identifies itself as peerID by sending +// gorumsNodeIDKey metadata, connects to addrs, and returns the configuration. +// Configuration cleanup is registered via t.Cleanup; callers may also close it // explicitly (e.g., to test disconnect) — Close is idempotent. -func connectAsPeer(t *testing.T, peerID uint32, addrs []string) *outboundManager { +func connectAsPeer(t *testing.T, peerID uint32, addrs []string) Configuration { t.Helper() peerMD := metadata.Pairs(gorumsNodeIDKey, strconv.FormatUint(uint64(peerID), 10)) - mgr := TestManager(t, WithMetadata(peerMD)) - _, err := NewConfiguration(mgr, WithNodeList(addrs)) + cfg, err := NewConfig(WithNodeList(addrs), TestDialOptions(t), WithMetadata(peerMD)) if err != nil { - t.Fatalf("NewConfiguration() error: %v", err) + t.Fatalf("NewConfig() error: %v", err) } - return mgr + t.Cleanup(Closer(t, cfg)) + return cfg } // TestKnownPeerConnects verifies the end-to-end path: @@ -459,13 +459,13 @@ func TestKnownPeerConnects(t *testing.T) { func TestKnownPeerDisconnects(t *testing.T) { srv, addrs := testPeerServer(t) - mgr := connectAsPeer(t, 2, addrs) + cfg := connectAsPeer(t, 2, addrs) WaitForConfigCondition(t, srv.Config, equalNodeIDs([]uint32{1, 2})) - // Close the peer manager to trigger disconnect; Close is idempotent so - // t.Cleanup (registered by connectAsPeer via TestManager) is harmless. - if err := mgr.Close(); err != nil { - t.Fatalf("mgr.Close() error: %v", err) + // Close the configuration to trigger disconnect; Close is idempotent so + // t.Cleanup (registered by connectAsPeer) is harmless. + if err := cfg.Close(); err != nil { + t.Fatalf("cfg.Close() error: %v", err) } WaitForConfigCondition(t, srv.Config, equalNodeIDs([]uint32{1})) checkIDs(t, srv.Config(), []uint32{1}, "after disconnect") @@ -477,11 +477,11 @@ func TestUnknownPeerIgnored(t *testing.T) { srv, addrs := testPeerServer(t) // Connect without metadata (external client) and with an unknown ID. - external := TestManager(t) - _, err := NewConfiguration(external, WithNodeList(addrs)) + cfg, err := NewConfig(WithNodeList(addrs), TestDialOptions(t)) if err != nil { - t.Fatalf("NewConfiguration() error: %v", err) + t.Fatalf("NewConfig() error: %v", err) } + t.Cleanup(Closer(t, cfg)) connectAsPeer(t, 99, addrs) // ID 99 not in known set @@ -601,18 +601,18 @@ func testClientServer(t *testing.T) (*Server, []string) { return srv, addrs } -// connectAsPeerClient creates a Manager that advertises back-channel -// capability by sending the gorums-node-id key (via [withRequestHandler]), -// connects to addrs, and returns the manager. The server will include it in +// connectAsPeerClient creates a Configuration that advertises back-channel +// capability by sending the gorums-node-id key (via [WithServer]), +// connects to addrs, and returns the configuration. The server will include it in // ClientConfig and may dispatch server-initiated calls to it. -func connectAsPeerClient(t *testing.T, addrs []string) *outboundManager { +func connectAsPeerClient(t *testing.T, addrs []string) Configuration { t.Helper() - mgr := TestManager(t, withRequestHandler(NewServer(), 0)) - _, err := NewConfiguration(mgr, WithNodeList(addrs)) + cfg, err := NewConfig(WithNodeList(addrs), TestDialOptions(t), WithServer(NewServer())) if err != nil { - t.Fatalf("NewConfiguration() error: %v", err) + t.Fatalf("NewConfig() error: %v", err) } - return mgr + t.Cleanup(Closer(t, cfg)) + return cfg } // TestClientConfigConnects verifies that a server accepts a peer-capable @@ -641,7 +641,7 @@ func TestClientConfigConnects(t *testing.T) { func TestClientConfigDisconnects(t *testing.T) { srv, addrs := testClientServer(t) - mgr := connectAsPeerClient(t, addrs) + cfg := connectAsPeerClient(t, addrs) // Wait for the client peer to appear. WaitForConfigCondition(t, srv.ClientConfig, func(cfg Configuration) bool { return len(cfg) > 0 }) @@ -650,8 +650,8 @@ func TestClientConfigDisconnects(t *testing.T) { } // Disconnect the client peer. - if err := mgr.Close(); err != nil { - t.Fatalf("mgr.Close() error: %v", err) + if err := cfg.Close(); err != nil { + t.Fatalf("cfg.Close() error: %v", err) } // Wait for config to become empty. From dadc7eec1e3fba422f983115c55f4a562bf8c490 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 23:29:20 +0100 Subject: [PATCH 13/22] refactor(test): replace withRequestHandler with WithServer Replace all withRequestHandler calls in inbound_manager_test.go with the public WithServer API. In TestKnownPeerServerCallsClient, the mockRequestHandler type is removed and replaced with a real *Server with RegisterHandler calls, which is the idiomatic way to set up a back-channel handler. Update doc comments in connectAsPeerClient to reference WithServer instead of withRequestHandler. --- inbound_manager_test.go | 51 +++++++++++------------------------------ 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/inbound_manager_test.go b/inbound_manager_test.go index 8130896a..64bc50ff 100644 --- a/inbound_manager_test.go +++ b/inbound_manager_test.go @@ -490,30 +490,6 @@ func TestUnknownPeerIgnored(t *testing.T) { checkIDs(t, srv.Config(), []uint32{1}, "external and unknown peers must not appear") } -type mockRequestHandler struct { - handlers map[string]Handler -} - -func (m mockRequestHandler) HandleRequest(ctx context.Context, msg *stream.Message, release func(), send func(*stream.Message)) { - srvCtx := ServerCtx{Context: ctx, release: release, send: send} - handler, ok := m.handlers[msg.GetMethod()] - if !ok { - release() - return - } - defer release() - inMsg, err := unmarshalRequest(msg) - in := &Message{Msg: inMsg, Message: msg} - if err != nil { - _ = srvCtx.SendMessage(MessageWithError(in, nil, err)) - return - } - out, err := handler(srvCtx, in) - if out != nil || err != nil { - _ = srvCtx.SendMessage(MessageWithError(in, out, err)) - } -} - // TestKnownPeerServerCallsClient verifies the full symmetric communication path: // server sends a request to a connected client via an inbound channel, // the client's Channel.receiver dispatches to a registered handler, @@ -521,19 +497,18 @@ func (m mockRequestHandler) HandleRequest(ctx context.Context, msg *stream.Messa func TestKnownPeerServerCallsClient(t *testing.T) { srv, addrs := testPeerServer(t) - // Client connects as peer 2 with a handler injected via withRequestHandler. - clientHandlers := map[string]Handler{ - mock.TestMethod: func(_ ServerCtx, in *Message) (*Message, error) { - req := AsProto[*pb.StringValue](in) - return NewResponseMessage(in, pb.String("echo: "+req.GetValue())), nil - }, - } + // Client connects as peer 2 with handlers registered on a server via WithServer. + clientSrv := NewServer() + clientSrv.RegisterHandler(mock.TestMethod, func(_ ServerCtx, in *Message) (*Message, error) { + req := AsProto[*pb.StringValue](in) + return NewResponseMessage(in, pb.String("echo: "+req.GetValue())), nil + }) peerMD := metadata.Pairs(gorumsNodeIDKey, "2") - mgr := TestManager(t, WithMetadata(peerMD), withRequestHandler(mockRequestHandler{handlers: clientHandlers}, 0)) - _, err := NewConfiguration(mgr, WithNodeList(addrs)) + cfg, err := NewConfig(WithNodeList(addrs), TestDialOptions(t), WithMetadata(peerMD), WithServer(clientSrv)) if err != nil { - t.Fatalf("NewConfiguration() error: %v", err) + t.Fatalf("NewConfig() error: %v", err) } + t.Cleanup(Closer(t, cfg)) // Wait for the peer to appear in the inbound config. WaitForConfigCondition(t, srv.Config, equalNodeIDs([]uint32{1, 2})) @@ -709,17 +684,17 @@ func TestClientConfigServerCallsClient(t *testing.T) { var wg sync.WaitGroup wg.Add(1) - // Client: a Server whose reverse-direction mock.Stream handler is wired in via withRequestHandler. + // Client: a Server whose reverse-direction mock.Stream handler is wired in via WithServer. clientSrv := NewServer() clientSrv.RegisterHandler(mock.Stream, func(_ ServerCtx, _ *Message) (*Message, error) { wg.Done() return nil, nil }) - mgr := TestManager(t, withRequestHandler(clientSrv, 0)) - clientConfig, err := NewConfiguration(mgr, WithNodeList(addrs)) + clientConfig, err := NewConfig(WithNodeList(addrs), TestDialOptions(t), WithServer(clientSrv)) if err != nil { - t.Fatalf("NewConfiguration() error: %v", err) + t.Fatalf("NewConfig() error: %v", err) } + t.Cleanup(Closer(t, clientConfig)) // Wait for the client to appear in the server's ClientConfig. WaitForConfigCondition(t, srv.ClientConfig, func(cfg Configuration) bool { return len(cfg) > 0 }) From 25d9124bccc148634dafd74680acfc3abe78f87b Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 23:30:13 +0100 Subject: [PATCH 14/22] chore: remove deprecated NewConfiguration function --- config.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/config.go b/config.go index 995c8471..b628fc2f 100644 --- a/config.go +++ b/config.go @@ -42,14 +42,6 @@ func (c ConfigContext) Configuration() Configuration { // operate on a configuration always return a new Configuration instance. type Configuration []*Node -// Deprecated: Use [NewConfig] instead. -func NewConfiguration(mgr *outboundManager, opt NodeListOption) (nodes Configuration, err error) { - if opt == nil { - return nil, fmt.Errorf("config: missing required node list") - } - return opt.newConfig(mgr) -} - // NewConfig returns a new [Configuration] based on the provided nodes and dial options. // // Example: From 85643ca3193b3a24f5822dd57e34f02ae229596e Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 23:30:59 +0100 Subject: [PATCH 15/22] chore: remove unused TestManager function --- testing_shared.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/testing_shared.go b/testing_shared.go index f1884ac1..35b52322 100644 --- a/testing_shared.go +++ b/testing_shared.go @@ -72,15 +72,6 @@ func TestQuorumCallError(_ testing.TB, nodeErrors map[uint32]error) QuorumCallEr return QuorumCallError{cause: ErrIncomplete, errors: errs} } -// TestManager creates a new outbound manager with appropriate dial options for -// the current test mode and any additional DialOptions, e.g., [WithMetadata]. -// The manager is automatically closed via t.Cleanup. -func TestManager(t testing.TB, opts ...DialOption) *outboundManager { - t.Helper() - to := &testOptions{managerOpts: opts} - return to.getOrCreateManager(t) -} - // getOrCreateManager returns the existing manager from an existing configuration, or // creates a new one using [TestDialOptions] with any additional options from to.managerOpts. // If a new manager is created, its cleanup is registered via t.Cleanup. From ca0cf19080496a2aadeb7b2f765b622c4ed4ade4 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 23:45:04 +0100 Subject: [PATCH 16/22] chore: update various mgr references no longer valid Replace the stale gorums.NewManager example in the TestServers doc comment with the current pattern using gorums.NewConfig together with gorums.TestDialOptions. Rename mgrOption to dialOption and mgrNodes to cfgNodes. --- examples/storage/repl.go | 4 ++-- server_test.go | 4 ++-- testing_shared.go | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/storage/repl.go b/examples/storage/repl.go index 92b2453f..b37dd51c 100644 --- a/examples/storage/repl.go +++ b/examples/storage/repl.go @@ -438,9 +438,9 @@ func (r repl) parseConfiguration(cfgStr string) (pb.Configuration, error) { } nodes := make([]*pb.Node, 0, len(indices)) - mgrNodes := r.cfg.Nodes() + cfgNodes := r.cfg.Nodes() for _, i := range indices { - nodes = append(nodes, mgrNodes[i]) + nodes = append(nodes, cfgNodes[i]) } gorums.OrderedBy(gorums.ID).Sort(nodes) return pb.Configuration(nodes), nil diff --git a/server_test.go b/server_test.go index 1cfcb495..b6d36fb1 100644 --- a/server_test.go +++ b/server_test.go @@ -24,9 +24,9 @@ func TestServerCallback(t *testing.T) { message = m.Get("message")[0] signal <- struct{}{} }) - mgrOption := gorums.WithMetadata(metadata.New(map[string]string{"message": "hello"})) + dialOption := gorums.WithMetadata(metadata.New(map[string]string{"message": "hello"})) - gorums.TestNode(t, nil, srvOption, mgrOption) + gorums.TestNode(t, nil, srvOption, dialOption) select { case <-time.After(100 * time.Millisecond): diff --git a/testing_shared.go b/testing_shared.go index 35b52322..795a05d6 100644 --- a/testing_shared.go +++ b/testing_shared.go @@ -81,8 +81,8 @@ func (to *testOptions) getOrCreateManager(t testing.TB) *outboundManager { return to.existingCfg.mgr() } // Create manager and register its cleanup LAST so it runs FIRST (LIFO) - mgrOpts := append([]DialOption{TestDialOptions(t)}, to.managerOpts...) - mgr := newOutboundManager(mgrOpts...) + dialOptions := append([]DialOption{TestDialOptions(t)}, to.managerOpts...) + mgr := newOutboundManager(dialOptions...) t.Cleanup(Closer(t, mgr)) return mgr } @@ -165,8 +165,8 @@ func TestNode(t testing.TB, srvFn func(i int) ServerIface, opts ...TestOption) * // Example usage: // // addrs := gorums.TestServers(t, 3, serverFn) -// mgr := gorums.NewManager(gorums.InsecureDialOptions(t)) -// t.Cleanup(gorums.Closer(t, mgr)) +// cfg, err := gorums.NewConfig(gorums.WithNodeList(addrs), gorums.TestDialOptions(t)) +// t.Cleanup(gorums.Closer(t, cfg)) // ... // // This function can be used by other packages for testing purposes, as long as From 07cde493b2c3307928c29b0c4e63c95cc2e0ccda Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 24 Mar 2026 23:59:39 +0100 Subject: [PATCH 17/22] docs(user-guide): update client API examples to use NewConfig Replace all NewManager + NewConfiguration pairs with gorums.NewConfig. Update prose in the "Implementing the StorageClient" section to no longer describe the Manager type, since it is unexported; the entry point is now NewConfig. Update "Working with Configurations" example to use the current Configuration methods: Extend, Union, Difference, Remove, and WithoutErrors as direct method calls (returns Configuration, no error) instead of the old NewConfiguration wrapper pattern. Replace the removed methods And, Except, WithoutNodes, and WithNewNodes with their current equivalents. Fixed nil return for error cases in WriteNestedMulticast. --- doc/user-guide.md | 120 ++++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 67 deletions(-) diff --git a/doc/user-guide.md b/doc/user-guide.md index df7650d7..480f0ee2 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -324,13 +324,11 @@ func ExampleStorageServer(port int) { ## Implementing the StorageClient Next, we write client code to call RPCs on our servers. -The first thing we need to do is to create an instance of the `Manager` type. -The manager maintains a pool of connections to nodes. -Nodes are added to the connection pool via new configurations, as shown below. +The first thing we need to do is to create a `Configuration` using `gorums.NewConfig`. +`NewConfig` establishes connections to the given nodes and returns a configuration +ready for making RPC calls. -The manager takes as arguments a set of optional manager options. -We can forward gRPC dial options to the manager if needed. -The manager will use these options when connecting to nodes. +We can forward gRPC dial options to `NewConfig` if needed. Below we use only a simple insecure connection option. ```go @@ -345,32 +343,28 @@ import ( ) func ExampleStorageClient() { - mgr := NewManager( - gorums.WithDialOptions( - grpc.WithTransportCredentials(insecure.NewCredentials()), - ), - ) -``` - -A configuration is a set of nodes on which our RPC calls can be invoked. -Using the `WithNodeList` option, the manager assigns a unique identifier to each node. -The code below shows how to create a configuration: - -```go - // Get all all available node ids, 3 nodes addrs := []string{ "127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082", } // Create a configuration including all nodes - allNodesConfig, err := NewConfiguration(mgr, gorums.WithNodeList(addrs)) + allNodesConfig, err := gorums.NewConfig( + gorums.WithNodeList(addrs), + gorums.WithDialOptions( + grpc.WithTransportCredentials(insecure.NewCredentials()), + ), + ) if err != nil { log.Fatalln("error creating read config:", err) } + defer allNodesConfig.Close() ``` -The `Manager` and `Configuration` types also have a few other available methods. +A configuration is a set of nodes on which RPC calls can be invoked. +`WithNodeList` assigns a unique identifier to each node by address. + +The `Configuration` type has several useful methods for combining and filtering configurations. Inspect the package documentation or source code for details. We can now invoke the WriteUnicast RPC on each `node` in the configuration: @@ -597,17 +591,17 @@ func ExampleStorageClient() { "127.0.0.1:8082", } - mgr := gorums.NewManager( + // Create a configuration with all nodes + cfg, err := gorums.NewConfig( + gorums.WithNodeList(addrs), gorums.WithDialOptions( grpc.WithTransportCredentials(insecure.NewCredentials()), ), ) - - // Create a configuration with all nodes - cfg, err := NewConfiguration(mgr, gorums.WithNodeList(addrs)) if err != nil { log.Fatalln("error creating configuration:", err) } + defer cfg.Close() ctx := context.Background() cfgCtx := config.Context(ctx) @@ -1168,18 +1162,17 @@ if err != nil { var qcErr gorums.QuorumCallError if errors.As(err, &qcErr) { // Option 1: Exclude all failed nodes - newConfig, err := NewConfiguration(mgr, config.WithoutErrors(qcErr)) + newConfig := config.WithoutErrors(qcErr) // Option 2: Exclude only nodes with specific error types // For example, exclude only nodes that timed out - newConfig, err := NewConfiguration(mgr, config.WithoutErrors(qcErr, context.DeadlineExceeded)) + newConfig = config.WithoutErrors(qcErr, context.DeadlineExceeded) // Option 3: Exclude nodes with multiple specific error types - newConfig, err := NewConfiguration(mgr, config.WithoutErrors(qcErr, - context.DeadlineExceeded, - context.Canceled, - io.EOF, - ), + newConfig = config.WithoutErrors(qcErr, + context.DeadlineExceeded, + context.Canceled, + io.EOF, ) // Retry the operation with the new configuration @@ -1196,7 +1189,7 @@ This allows you to filter nodes based on the underlying cause of their failures, Below is an example demonstrating how to work with configurations. These configurations are viewed from the client's perspective, and to actually make quorum calls on these configurations, there must be server endpoints to connect to. -We ignore the construction of `mgr` and error handling (except for the last configuration). +Error handling is omitted for brevity except where the result is used. In the example below, we simply use fixed quorum sizes. @@ -1207,56 +1200,49 @@ func ExampleConfigClient() { "127.0.0.1:8081", "127.0.0.1:8082", } - // Make configuration c1 from addrs, giving |c1| = |addrs| = 3 - c1, _ := NewConfiguration(mgr, + // Create base configuration c1 from addrs, giving |c1| = 3. + c1, err := gorums.NewConfig( gorums.WithNodeList(addrs), + gorums.WithDialOptions( + grpc.WithTransportCredentials(insecure.NewCredentials()), + ), ) + if err != nil { + log.Fatalln("error creating configuration:", err) + } + defer c1.Close() newAddrs := []string{ "127.0.0.1:9080", "127.0.0.1:9081", } - // Make configuration c2 from newAddrs, giving |c2| = |newAddrs| = 2 - c2, _ := NewConfiguration(mgr, - gorums.WithNodeList(newAddrs), - ) + // Extend c1 with newAddrs; c2 shares c1's connection pool, |c2| = |c1| + |newAddrs| = 5. + c2, _ := c1.Extend(gorums.WithNodeList(newAddrs)) - // Make new configuration c3 from c1 and newAddrs, giving |c3| = |c1| + |newAddrs| = 3+2=5 - c3, _ := NewConfiguration(mgr, - c1.WithNewNodes(gorums.WithNodeList(newAddrs)), - ) + // c3 = nodes in c2 not in c1, giving |c3| = |newAddrs| = 2. + c3 := c2.Difference(c1) - // Make new configuration c4 from c1 and c2, giving |c4| = |c1| + |c2| = 3+2=5 - c4, _ := NewConfiguration(mgr, - c1.And(c2), - ) + // c4 = union of c1 and c3, giving |c4| = |c1| + |c3| = 3+2 = 5. + c4 := c1.Union(c3) - // Make new configuration c5 from c1 except the first node from c1, giving |c5| = |c1| - 1 = 3-1 = 2 - c5, _ := NewConfiguration(mgr, - c1.WithoutNodes(c1.NodeIDs()[0]), - ) + // c5 = c1 without its first node, giving |c5| = |c1| - 1 = 2. + c5 := c1.Remove(c1.NodeIDs()[0]) - // Make new configuration c6 from c3 except c1, giving |c6| = |c3| - |c1| = 5-3 = 2 - c6, _ := NewConfiguration(mgr, - c3.Except(c1), - ) + // c6 = c2 without c1, giving |c6| = |c2| - |c1| = 5-3 = 2. + c6 := c2.Difference(c1) // Example: Handling quorum call failures and creating a new configuration - // without failed nodes + // without failed nodes. cfgCtx := c1.Context(ctx) state, err := ReadQC(cfgCtx, &ReadRequest{}).Majority() if err != nil { var qcErr gorums.QuorumCallError if errors.As(err, &qcErr) { - // Create a new configuration excluding all nodes that failed - c7, _ := NewConfiguration(mgr, - c1.WithoutErrors(qcErr), - ) - - // Or exclude only nodes with specific error types (e.g., timeout errors) - c8, _ := NewConfiguration(mgr, - c1.WithoutErrors(qcErr, context.DeadlineExceeded), - ) + // Create a new configuration excluding all nodes that failed. + c7 := c1.WithoutErrors(qcErr) + + // Or exclude only nodes with specific error types (e.g., timeout errors). + c8 := c1.WithoutErrors(qcErr, context.DeadlineExceeded) } } } @@ -1411,11 +1397,11 @@ The same pattern applies to nested multicast: func (s *storageServer) WriteNestedMulticast(ctx gorums.ServerCtx, req *pb.WriteRequest) (*pb.WriteResponse, error) { cfg := ctx.Config() if len(cfg) == 0 { - return pb.WriteResponse_builder{New: false}.Build(), fmt.Errorf("WriteNestedMulticast requires a server peer configuration") + return nil, fmt.Errorf("write_nested_multicast: requires server peer configuration") } ctx.Release() if err := pb.WriteMulticast(cfg.Context(ctx), req); err != nil { - return pb.WriteResponse_builder{New: false}.Build(), err + return nil, fmt.Errorf("write_nested_multicast: %w", err) } return pb.WriteResponse_builder{New: true}.Build(), nil } From d1e0494d8b972e01640881987b482d48a27fe3d5 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Wed, 25 Mar 2026 00:24:33 +0100 Subject: [PATCH 18/22] test: update TestConfig to use 6 nodes and adjust configuration creation This avoids the need for WithManager to create new configurations using the same manager; instead we create one large configuration and remove from it to create other configurations using Union and Difference. --- internal/tests/config/config_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/tests/config/config_test.go b/internal/tests/config/config_test.go index 2027aa98..a65d0907 100644 --- a/internal/tests/config/config_test.go +++ b/internal/tests/config/config_test.go @@ -40,22 +40,22 @@ func TestConfig(t *testing.T) { } } - c1 := gorums.TestConfiguration(t, 4, serverFn) + c1 := gorums.TestConfiguration(t, 6, serverFn) fmt.Println("--- c1 ", c1.Nodes()) callRPC(c1) - // Create a new configuration c2 with 2 new nodes not in c1, using the same manager as c1. - c2 := gorums.TestConfiguration(t, 2, serverFn, gorums.WithManager(t, c1)) + // Create c2 by removing 2 nodes from c1. + c2 := c1.Remove(1, 2) fmt.Println("--- c2 ", c2.Nodes()) callRPC(c2) - // Create c3 = c1 ∪ c2, using the same manager as c1 (and c2). + // Create c3 = c1 ∪ c2 c3 := c1.Union(c2) fmt.Println("--- c3 ", c3.Nodes()) callRPC(c3) - // Create c4 = c3 \ c1, using the same manager as c1 (and c2, c3). - c4 := c3.Difference(c1) + // Create c4 = c3 \ c2 + c4 := c3.Difference(c2) fmt.Println("--- c4 ", c4.Nodes()) callRPC(c4) } From d915c9cec3be94bbf74744dc63bc52b111d663fa Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Wed, 25 Mar 2026 00:25:46 +0100 Subject: [PATCH 19/22] refactor(testing): simplify TestConfiguration to use NewConfig directly Remove WithManager, existingCfg field, and getOrCreateManager from the testing infrastructure. TestConfiguration now calls NewConfig and registers t.Cleanup(Closer(t, cfg)) using the returned configuration, which owns and closes its manager internally. --- testing_shared.go | 21 ++++----------------- testopts.go | 22 +--------------------- 2 files changed, 5 insertions(+), 38 deletions(-) diff --git a/testing_shared.go b/testing_shared.go index 795a05d6..2a6d82b0 100644 --- a/testing_shared.go +++ b/testing_shared.go @@ -72,21 +72,6 @@ func TestQuorumCallError(_ testing.TB, nodeErrors map[uint32]error) QuorumCallEr return QuorumCallError{cause: ErrIncomplete, errors: errs} } -// getOrCreateManager returns the existing manager from an existing configuration, or -// creates a new one using [TestDialOptions] with any additional options from to.managerOpts. -// If a new manager is created, its cleanup is registered via t.Cleanup. -func (to *testOptions) getOrCreateManager(t testing.TB) *outboundManager { - if to.existingCfg != nil { - // Don't register cleanup - caller is responsible for closing the configuration - return to.existingCfg.mgr() - } - // Create manager and register its cleanup LAST so it runs FIRST (LIFO) - dialOptions := append([]DialOption{TestDialOptions(t)}, to.managerOpts...) - mgr := newOutboundManager(dialOptions...) - t.Cleanup(Closer(t, mgr)) - return mgr -} - // TestConfiguration creates servers and a configuration for testing. // Both server and manager cleanup are handled via t.Cleanup in the correct order: // manager is closed first, then servers are stopped. @@ -128,11 +113,13 @@ func TestConfiguration(t testing.TB, numServers int, srvFn func(i int) ServerIfa testOpts.preConnectHook(stopAllFn) } - mgr := testOpts.getOrCreateManager(t) - cfg, err := testOpts.nodeListOption(addrs).newConfig(mgr) + // Create configuration and register its cleanup LAST so it runs FIRST (LIFO) + dialOptions := append([]DialOption{TestDialOptions(t)}, testOpts.managerOpts...) + cfg, err := NewConfig(testOpts.nodeListOption(addrs), dialOptions...) if err != nil { t.Fatal(err) } + t.Cleanup(Closer(t, cfg)) return cfg } diff --git a/testopts.go b/testopts.go index 50a5879d..717b1c62 100644 --- a/testopts.go +++ b/testopts.go @@ -22,17 +22,14 @@ type testOptions struct { managerOpts []DialOption serverOpts []ServerOption nodeListOpts []NodeListOption - existingCfg Configuration stopFuncPtr *func(...int) // pointer to capture the variadic stop function preConnectHook func(stopFn func()) // called before connecting to servers skipGoleak bool // skip goleak checks (useful for synctest) } // shouldSkipGoleak returns true if goleak checks should be skipped. -// This includes cases where an existing configuration is reused (since it may -// already have its own goleak checks) or when SkipGoleak option is set. func (to *testOptions) shouldSkipGoleak() bool { - return to.existingCfg != nil || to.skipGoleak + return to.skipGoleak } // serverFunc returns a server creation function based on the server options. @@ -76,8 +73,6 @@ func extractTestOptions(opts []TestOption) testOptions { result.serverOpts = append(result.serverOpts, o) case NodeListOption: result.nodeListOpts = append(result.nodeListOpts, o) - case Configuration: - result.existingCfg = o case stopFuncProvider: result.stopFuncPtr = o.stopFunc case preConnectProvider: @@ -89,21 +84,6 @@ func extractTestOptions(opts []TestOption) testOptions { return result } -// WithManager returns a TestOption that provides an existing configuration whose -// manager will be reused instead of creating a new one. This is useful when -// creating multiple configurations that should share the same manager. -// -// When using WithManager, the caller is responsible for closing the original -// configuration. SetupConfiguration will NOT register a cleanup function for the manager. -// -// This option is intended for testing purposes only. -func WithManager(_ testing.TB, cfg Configuration) TestOption { - if cfg == nil { - panic("gorums: WithManager called with nil configuration") - } - return cfg -} - // stopFuncProvider is a TestOption that captures the server stop function. type stopFuncProvider struct { stopFunc *func(...int) From b90a431d184b71af13102ec2722dc890a315ef52 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Wed, 25 Mar 2026 15:13:58 +0100 Subject: [PATCH 20/22] refactor: reorganize Configuration and ConfigContext definitions --- config.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/config.go b/config.go index b628fc2f..5dde4340 100644 --- a/config.go +++ b/config.go @@ -7,6 +7,12 @@ import ( "slices" ) +// Configuration represents a static set of nodes on which multicast or +// quorum calls may be invoked. A configuration is created using [NewConfig]. +// A configuration should be treated as immutable. Therefore, methods that +// operate on a configuration always return a new Configuration instance. +type Configuration []*Node + // ConfigContext is a context that carries a configuration for multicast or // quorum calls. It embeds context.Context and provides access to the configuration. // @@ -16,6 +22,11 @@ type ConfigContext struct { cfg Configuration } +// Configuration returns the configuration associated with this context. +func (c ConfigContext) Configuration() Configuration { + return c.cfg +} + // Context creates a new ConfigContext from the given parent context // and this configuration. // @@ -31,17 +42,6 @@ func (c Configuration) Context(parent context.Context) *ConfigContext { return &ConfigContext{Context: parent, cfg: c} } -// Configuration returns the configuration associated with this context. -func (c ConfigContext) Configuration() Configuration { - return c.cfg -} - -// Configuration represents a static set of nodes on which multicast or -// quorum calls may be invoked. A configuration is created using [NewConfig]. -// A configuration should be treated as immutable. Therefore, methods that -// operate on a configuration always return a new Configuration instance. -type Configuration []*Node - // NewConfig returns a new [Configuration] based on the provided nodes and dial options. // // Example: From c3f9ae58f909bf5d3a51240bcd50985dc6e59aac Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Wed, 25 Mar 2026 15:29:19 +0100 Subject: [PATCH 21/22] doc: rename variable 'cfg' to 'config' for consistency in user guide --- doc/user-guide.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/doc/user-guide.md b/doc/user-guide.md index 480f0ee2..13230bf6 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -592,7 +592,7 @@ func ExampleStorageClient() { } // Create a configuration with all nodes - cfg, err := gorums.NewConfig( + config, err := gorums.NewConfig( gorums.WithNodeList(addrs), gorums.WithDialOptions( grpc.WithTransportCredentials(insecure.NewCredentials()), @@ -601,7 +601,7 @@ func ExampleStorageClient() { if err != nil { log.Fatalln("error creating configuration:", err) } - defer cfg.Close() + defer config.Close() ctx := context.Background() cfgCtx := config.Context(ctx) @@ -1084,7 +1084,7 @@ Gorums defines several sentinel errors that commonly appear as the cause of a `Q Here's how to properly handle errors from a quorum call: ```go -func handleQuorumCall(cfg *gorums.Configuration, req *ReadRequest) { +func handleQuorumCall(config *gorums.Configuration, req *ReadRequest) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -1380,14 +1380,14 @@ Without `Release()`, the server would block all other inbound messages until the // ReadNestedQC is a quorum-call handler that fans out a nested ReadQC // to all known connected peers and returns the most recent value. func (s *storageServer) ReadNestedQC(ctx gorums.ServerCtx, req *pb.ReadRequest) (*pb.ReadResponse, error) { - cfg := ctx.Config() - if len(cfg) == 0 { + config := ctx.Config() + if len(config) == 0 { return nil, fmt.Errorf("ReadNestedQC requires a server peer configuration") } // Release the handler lock before making nested outbound calls to avoid // blocking inbound message processing on this server. ctx.Release() - return newestValue(pb.ReadQC(cfg.Context(ctx), req)) + return newestValue(pb.ReadQC(config.Context(ctx), req)) } ``` @@ -1395,12 +1395,12 @@ The same pattern applies to nested multicast: ```go func (s *storageServer) WriteNestedMulticast(ctx gorums.ServerCtx, req *pb.WriteRequest) (*pb.WriteResponse, error) { - cfg := ctx.Config() - if len(cfg) == 0 { + config := ctx.Config() + if len(config) == 0 { return nil, fmt.Errorf("write_nested_multicast: requires server peer configuration") } ctx.Release() - if err := pb.WriteMulticast(cfg.Context(ctx), req); err != nil { + if err := pb.WriteMulticast(config.Context(ctx), req); err != nil { return nil, fmt.Errorf("write_nested_multicast: %w", err) } return pb.WriteResponse_builder{New: true}.Build(), nil @@ -1485,7 +1485,7 @@ clientSrv := gorums.NewServer() clientSrv.RegisterHandler(pb.MyMethod, myHandler) // Connect to the server; NewConfig wires up the back-channel dispatcher automatically. -cfg, err := clientSrv.NewConfig( +config, err := clientSrv.NewConfig( gorums.WithNodeList(serverAddrs), gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), ) @@ -1518,12 +1518,12 @@ The handler reads `ctx.ClientConfig()` to reach all currently connected client p ```go // ReadNestedQC fans out a ReadQC to all clients that have connected. func (s *storageServer) ReadNestedQC(ctx gorums.ServerCtx, req *pb.ReadRequest) (*pb.ReadResponse, error) { - cfg := ctx.ClientConfig() - if len(cfg) == 0 { + config := ctx.ClientConfig() + if len(config) == 0 { return nil, fmt.Errorf("ReadNestedQC: no client peers connected") } ctx.Release() - return newestValue(pb.ReadQC(cfg.Context(ctx), req)) + return newestValue(pb.ReadQC(config.Context(ctx), req)) } ``` From 1f563844d82a926033dad79563661f1d2708096e Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Wed, 25 Mar 2026 15:31:41 +0100 Subject: [PATCH 22/22] doc: update context in ExampleConfigClient to use Background context --- doc/user-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user-guide.md b/doc/user-guide.md index 13230bf6..91ffc290 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -1233,7 +1233,7 @@ func ExampleConfigClient() { // Example: Handling quorum call failures and creating a new configuration // without failed nodes. - cfgCtx := c1.Context(ctx) + cfgCtx := c1.Context(context.Background()) state, err := ReadQC(cfgCtx, &ReadRequest{}).Majority() if err != nil { var qcErr gorums.QuorumCallError