From abdb0f1fe714d4ed93409a290c5e2b193f8f6e73 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:11:46 +0200 Subject: [PATCH 01/16] gorums: fold System into Server System wrapped a Server, a listener, and a list of closers. Every method on it either delegated to the Server or managed the listener, so a user had two types to reason about, two Stop methods, and two places to look for the peer configuration. Registering a service went through a callback that received the Server the caller already had. Server now owns its listener. WithAddr records the address, ListenAndServe binds it, Serve accepts an externally supplied one, Addr reports whichever is in effect, and Stop closes it. NewLocalServers replaces NewLocalSystems and returns servers with their listeners preallocated, so a test knows every address before anything serves. Services register directly on the Server, which is what the generated registration functions already take. The closer list goes with System: a caller that needs a resource closed on shutdown can close it itself, and nothing in the repository used the returned error. WithServerOptions goes too. It existed only to smuggle ServerOptions through NewSystem's dial-option list, and NewServer takes them directly. system_test.go becomes server_e2e_test.go. Its two closer-registry tests are removed with the feature they tested, and the listener test that only applied to NewSystem's eager bind now covers NewLocalServers instead. --- callopts_test.go | 35 +-- doc/user-guide.md | 4 +- examples/storage/server.go | 49 ++- gorumstest/gorumstest.go | 22 +- inbound_manager.go | 2 +- local_servers.go | 96 ++++++ opts.go | 14 - opts_test.go | 24 +- server.go | 87 +++++- system_test.go => server_e2e_test.go | 447 ++++++++++----------------- system.go | 218 ------------- 11 files changed, 408 insertions(+), 590 deletions(-) create mode 100644 local_servers.go rename system_test.go => server_e2e_test.go (61%) delete mode 100644 system.go diff --git a/callopts_test.go b/callopts_test.go index b8cec261..43ae4b89 100644 --- a/callopts_test.go +++ b/callopts_test.go @@ -13,26 +13,27 @@ import ( pb "google.golang.org/protobuf/types/known/wrapperspb" ) -// testSystems returns n started Gorums systems on random localhost ports. -// It is the in-package counterpart of gorumstest.Systems, which this file -// cannot use: gorumstest imports gorums, so importing it from package gorums's -// own tests would create an import cycle. -func testSystems(t testing.TB, n int) []*System { +// testLocalServers returns n started Gorums servers forming a symmetric peer +// group on random localhost ports. It is the in-package counterpart of +// gorumstest.LocalServers, which this file cannot use: gorumstest imports +// gorums, so importing it from package gorums's own tests would create an +// import cycle. +func testLocalServers(t testing.TB, n int) []*Server { t.Helper() if _, ok := t.(*testing.B); !ok { t.Cleanup(func() { goleak.VerifyNone(t) }) } - systems, stop, err := NewLocalSystems(n, WithLocalDialOptions( + srvs, stop, err := NewLocalServers(n, WithLocalDialOptions( WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), )) if err != nil { t.Fatal(err) } t.Cleanup(stop) - for _, sys := range systems { - go sys.Serve() + for _, srv := range srvs { + go srv.ListenAndServe() } - return systems + return srvs } // testWaitUntil polls predicate until it returns true or timeout elapses. @@ -79,20 +80,18 @@ func TestCallOptionsIgnoreErrors(t *testing.T) { func TestCallOptionsIgnoreErrorsResourceLeak(t *testing.T) { // Previously leaked because fire-and-forget multicast still registered in router. // Now fixed: no replyChan → no ResponseChan → no Register. - systems := testSystems(t, 3) - for _, sys := range systems { - sys.RegisterService(nil, func(srv *Server) { - srv.RegisterHandler(mock.TestMethod, func(_ ServerCtx, _ *Message) (*Message, error) { - return nil, nil - }) + servers := testLocalServers(t, 3) + for _, srv := range servers { + srv.RegisterHandler(mock.TestMethod, func(_ ServerCtx, _ *Message) (*Message, error) { + return nil, nil }) } - for _, sys := range systems { - sys.WaitForPeers(t.Context(), func(cfg Configuration) bool { + for _, srv := range servers { + srv.WaitForPeers(t.Context(), func(cfg Configuration) bool { return cfg.Size() == 3 }) } - cfg := systems[0].OutboundConfig() + cfg := servers[0].PeerConfig() ctx := testTimeoutContext(t, 5*time.Second) for i := range 1000 { Multicast(cfg.Context(ctx), pb.String(fmt.Sprintf("mc-%d", i)), mock.TestMethod, IgnoreErrors()) diff --git a/doc/user-guide.md b/doc/user-guide.md index 30ed4ea3..5aec0e63 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -1776,7 +1776,7 @@ Use `WaitForPeers` to wait for enough peers to connect before issuing calls. A symmetric server re-establishes an outbound stream proactively when it drops while idle, rather than waiting for the next local send. Without this, a peer would remain absent from the remote's `ConnectedPeers()` until that side happened to send something. -The storage example uses `gorums.NewLocalSystems`, which calls `WithPeers` automatically for each system. +The storage example uses `gorums.NewLocalServers`, which calls `WithPeers` automatically for each server. Register a `WithPeerChange` callback to react each time the connected-peer configuration changes. See [WithPeerChange Callback](#withpeerchange-callback) for details and an example. @@ -1876,7 +1876,7 @@ gorumsSrv := gorums.NewServer( For example, a local test cluster: ```go -systems, stop, err := gorums.NewLocalSystems(4) +servers, stop, err := gorums.NewLocalServers(4) ``` > **Note:** The `nread` and `nwrite` commands in the storage REPL example use `ctx.PeerConfig()` (the static server-to-server direction) rather than `ctx.ConnectedClients()`. diff --git a/examples/storage/server.go b/examples/storage/server.go index 7d845daf..32e5b502 100644 --- a/examples/storage/server.go +++ b/examples/storage/server.go @@ -32,32 +32,31 @@ func runServer(address string, peers []string, srvOpt gorums.ServerOption) error return err } insecureDial := gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())) - sys, err := gorums.NewSystem(address, - gorums.WithServerOptions(srvOpt, gorums.WithPeers(myID, peerList, insecureDial)), - insecureDial, + srv := gorums.NewServer( + gorums.WithAddr(address), + gorums.WithPeers(myID, peerList, insecureDial), + srvOpt, ) - if err != nil { - return fmt.Errorf("failed to create system on %q: %w", address, err) - } // catch signals in order to shut down gracefully signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - registerAndServe(sys, myID) + registerAndServe(srv, myID) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - if err := sys.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + if err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { return cfg.Size() == len(peers) }); err != nil { return fmt.Errorf("peers did not connect in time: %w", err) } - log.Printf("Started storage server on %s\n", sys.Addr()) + log.Printf("Started storage server on %s\n", srv.Addr()) <-signals - return sys.Stop() + srv.Stop() + return nil } // runLocalCluster starts four in-process servers for local testing. @@ -65,26 +64,26 @@ func runServer(address string, peers []string, srvOpt gorums.ServerOption) error // call stop when the cluster is no longer needed. func runLocalCluster(srvOpts gorums.ServerOption) ([]string, func(), error) { dialOpts := gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())) - systems, stop, err := gorums.NewLocalSystems(4, + servers, stop, err := gorums.NewLocalServers(4, gorums.WithLocalServerOptions(srvOpts), gorums.WithLocalDialOptions(dialOpts), ) if err != nil { - return nil, nil, fmt.Errorf("failed to create local systems: %w", err) + return nil, nil, fmt.Errorf("failed to create local servers: %w", err) } - addrs := make([]string, len(systems)) - for i, sys := range systems { - addrs[i] = sys.Addr() - registerAndServe(sys, uint32(i+1)) + addrs := make([]string, len(servers)) + for i, srv := range servers { + addrs[i] = srv.Addr() + registerAndServe(srv, uint32(i+1)) } - // Wait for all systems to see each other before opening the client REPL. + // Wait for all servers to see each other before opening the client REPL. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - for _, sys := range systems { - if err := sys.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { - return cfg.Size() == len(systems) + for _, srv := range servers { + if err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + return cfg.Size() == len(servers) }); err != nil { stop() return nil, nil, fmt.Errorf("cluster failed to connect: %w", err) @@ -111,15 +110,13 @@ func peerConfig(address string, peers []string) (uint32, gorums.NodeListOption, return uint32(idx + 1), gorums.WithNodeList(sorted), nil } -// registerAndServe registers the storage service on sys and starts serving in +// registerAndServe registers the storage service on srv and starts serving in // a background goroutine. The server log output is labelled with the node ID. -func registerAndServe(sys *gorums.System, id uint32) { +func registerAndServe(srv *gorums.Server, id uint32) { storage := newStorageServer(os.Stderr, fmt.Sprintf("node %d", id)) - sys.RegisterService(nil, func(srv *gorums.Server) { - pb.RegisterStorageServer(srv, storage) - }) + pb.RegisterStorageServer(srv, storage) go func() { - if err := sys.Serve(); err != nil { + if err := srv.ListenAndServe(); err != nil { log.Printf("Server error: %v", err) } }() diff --git a/gorumstest/gorumstest.go b/gorumstest/gorumstest.go index 6409b7f6..a90719af 100644 --- a/gorumstest/gorumstest.go +++ b/gorumstest/gorumstest.go @@ -227,13 +227,13 @@ func Servers(t testing.TB, numServers int, srvFn func(i int) gorums.ServerIface) return addrs } -// Systems returns n started Gorums systems on random localhost ports (see -// [gorums.NewLocalSystems]). Each system auto-creates a peer -// [gorums.Configuration] over the group, accessible via -// [gorums.System.OutboundConfig]. The systems are automatically stopped when -// the test finishes via t.Cleanup. Any [gorums.ServerOption]s are applied to -// every server. -func Systems(t testing.TB, n int, opts ...gorums.ServerOption) []*gorums.System { +// LocalServers returns n started Gorums servers forming a symmetric peer +// group on random localhost ports (see [gorums.NewLocalServers]). Each +// server auto-creates a peer [gorums.Configuration] over the group, accessible +// via [gorums.Server.PeerConfig]. The servers are automatically stopped +// when the test finishes via t.Cleanup. Any [gorums.ServerOption]s are +// applied to every server. +func LocalServers(t testing.TB, n int, opts ...gorums.ServerOption) []*gorums.Server { t.Helper() // Skip goleak check for benchmarks @@ -242,7 +242,7 @@ func Systems(t testing.TB, n int, opts ...gorums.ServerOption) []*gorums.System t.Cleanup(func() { goleak.VerifyNone(t) }) } - systems, stop, err := gorums.NewLocalSystems(n, + srvs, stop, err := gorums.NewLocalServers(n, gorums.WithLocalServerOptions(opts...), gorums.WithLocalDialOptions(InsecureDialOptions(t)), ) @@ -253,11 +253,11 @@ func Systems(t testing.TB, n int, opts ...gorums.ServerOption) []*gorums.System // Register server cleanup SECOND so it runs BEFORE goleak check t.Cleanup(stop) - for _, sys := range systems { - go sys.Serve() + for _, srv := range srvs { + go srv.ListenAndServe() } - return systems + return srvs } // Closer returns a cleanup function that closes the given io.Closer. diff --git a/inbound_manager.go b/inbound_manager.go index 84315356..eae7fbf5 100644 --- a/inbound_manager.go +++ b/inbound_manager.go @@ -446,7 +446,7 @@ func (im *inboundManager) WaitForClients(ctx context.Context, cond func(Configur } // close signals all waiters to stop and prevents new waits from blocking. -// Called from [System.Stop]. +// Called from [Server.Stop]. func (im *inboundManager) close() { im.stopOnce.Do(func() { close(im.stopCh) }) } diff --git a/local_servers.go b/local_servers.go new file mode 100644 index 00000000..569db712 --- /dev/null +++ b/local_servers.go @@ -0,0 +1,96 @@ +package gorums + +import "net" + +// localServerOptions accumulates the options [NewLocalServers] applies to +// every server it creates. +type localServerOptions struct { + serverOpts []ServerOption + dialOpts []DialOption +} + +// LocalServerOption configures [NewLocalServers]. Use [WithLocalServerOptions] +// and [WithLocalDialOptions] to build one. +type LocalServerOption func(*localServerOptions) + +// WithLocalServerOptions applies opts to every server created by [NewLocalServers]. +func WithLocalServerOptions(opts ...ServerOption) LocalServerOption { + return func(o *localServerOptions) { + o.serverOpts = append(o.serverOpts, opts...) + } +} + +// WithLocalDialOptions applies opts to every server's peer configuration +// created by [NewLocalServers]. +func WithLocalDialOptions(opts ...DialOption) LocalServerOption { + return func(o *localServerOptions) { + o.dialOpts = append(o.dialOpts, opts...) + } +} + +// NewLocalServers creates n Gorums servers listening on random localhost ports. +// +// Each server is assigned a node ID from 1 to n. Every server tracks and calls +// all the other servers. Use [WithLocalServerOptions] to add [ServerOption]s +// to every server, and [WithLocalDialOptions] to add [DialOption]s to each +// server's peer connections. +// +// The returned servers are not started; call [Server.ListenAndServe] after +// registering any services. The returned stop function stops all servers and +// closes all allocated listeners and peer configurations. If listener +// allocation fails, all listeners acquired so far are closed before returning +// the error. +func NewLocalServers(n int, opts ...LocalServerOption) ([]*Server, func(), error) { + var localOpts localServerOptions + for _, opt := range opts { + if opt != nil { + opt(&localOpts) + } + } + listeners, nodeList, err := allocateListeners(n) + if err != nil { + return nil, nil, err + } + servers := make([]*Server, n) + for i := range n { + myID := uint32(i + 1) + serverOpts := append( + []ServerOption{WithPeers(myID, nodeList, localOpts.dialOpts...)}, + localOpts.serverOpts..., + ) + srv := NewServer(serverOpts...) + srv.setListener(listeners[i]) + servers[i] = srv + } + stop := func() { + for i, srv := range servers { + if srv != nil { + srv.Stop() + } else if listeners[i] != nil { + _ = listeners[i].Close() + } + } + } + return servers, stop, nil +} + +// allocateListeners pre-allocates n TCP listeners on random localhost ports and +// returns them along with a [NodeListOption] containing their addresses. If any +// listener fails to open, all previously opened listeners are closed before +// returning the error. +func allocateListeners(n int) ([]net.Listener, NodeListOption, error) { + listeners := make([]net.Listener, n) + addrs := make([]string, n) + for i := range n { + lis, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + for j := range i { + _ = listeners[j].Close() + } + return nil, nil, err + } + listeners[i] = lis + addrs[i] = lis.Addr().String() + } + return listeners, WithNodeList(addrs), nil +} diff --git a/opts.go b/opts.go index a29605ef..001827c4 100644 --- a/opts.go +++ b/opts.go @@ -22,7 +22,6 @@ type dialOptions struct { handler stream.RequestHandler localNodeID uint32 // if non-zero, skip setting handler on this node ID inboundMgr *inboundManager // set by WithBackChannel; enables eager reconnect for symmetric nodes - srvOpts []ServerOption // applied only by NewSystem } // DefaultSendBufferSize is the per-node send queue capacity used when no @@ -118,16 +117,3 @@ func withServer(srv *Server) DialOption { o.metadata = metadata.Join(o.metadata, metadataWithNodeID(srv.NodeID())) } } - -// WithServerOptions bundles [ServerOption]s into a [DialOption] for use with -// [NewSystem]. It has no effect when passed to [NewConfig]. -// Nil options are silently ignored. -func WithServerOptions(opts ...ServerOption) DialOption { - return func(o *dialOptions) { - for _, opt := range opts { - if opt != nil { - o.srvOpts = append(o.srvOpts, opt) - } - } - } -} diff --git a/opts_test.go b/opts_test.go index a295070e..7812528d 100644 --- a/opts_test.go +++ b/opts_test.go @@ -6,35 +6,35 @@ import ( "google.golang.org/grpc/metadata" ) -// TestWithServerOptionsFiltersNil verifies that WithServerOptions silently drops -// nil ServerOptions rather than storing them, which would cause a panic when -// NewSystem later calls NewServer with the collected options. -func TestWithServerOptionsFiltersNil(t *testing.T) { - opts := newDialOptions() - WithServerOptions(nil, WithBufferSizes(8, 8), nil)(&opts) - if got := len(opts.srvOpts); got != 1 { - t.Errorf("WithServerOptions: got %d srvOpts, want 1 (nil options must be dropped)", got) +// TestNewServerToleratesNilOptions verifies that NewServer skips nil +// ServerOptions rather than panicking, so callers that thread an optional +// option (for example [NewLocalServers]) can pass nil. +func TestNewServerToleratesNilOptions(t *testing.T) { + srv := NewServer(nil, WithBufferSizes(8, 8), nil) + if srv == nil { + t.Fatal("NewServer returned nil") } + srv.Stop() } // TestWithMetadataJoinsInsteadOfOverwrites verifies that WithMetadata joins its // argument with any previously set metadata rather than overwriting it. This is -// important when WithServer is applied before a user-supplied WithMetadata, -// because the node-id key set by WithServer must survive the subsequent +// important when WithBackChannel is applied before a user-supplied WithMetadata, +// because the node-id key set by WithBackChannel must survive the subsequent // WithMetadata call. func TestWithMetadataJoinsInsteadOfOverwrites(t *testing.T) { const nodeIDKey = "x-gorums-node-id" opts := newDialOptions() - // Simulate what WithServer does: set node-id metadata first. + // Simulate what WithBackChannel does: set node-id metadata first. opts.metadata = metadata.Join(opts.metadata, metadata.Pairs(nodeIDKey, "42")) // Now apply a user-supplied WithMetadata; it must not clobber the node-id. WithMetadata(metadata.Pairs("x-custom", "hello"))(&opts) if vals := opts.metadata.Get(nodeIDKey); len(vals) == 0 { - t.Errorf("WithMetadata overwrote %q metadata set by WithServer; got none", nodeIDKey) + t.Errorf("WithMetadata overwrote %q metadata set by WithBackChannel; got none", nodeIDKey) } if vals := opts.metadata.Get("x-custom"); len(vals) == 0 { t.Errorf("WithMetadata did not retain user-supplied key %q", "x-custom") diff --git a/server.go b/server.go index 442641be..52756812 100644 --- a/server.go +++ b/server.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "sync" "github.com/relab/gorums/internal/stream" "google.golang.org/grpc" @@ -22,6 +23,7 @@ type serverOptions struct { myID uint32 peerNodes NodeListOption // Peers to track as they connect; set by WithPeers. onConfigChange func(Configuration) // Callback registered via WithPeerChange. + listenAddr string // Listener address recorded by WithAddr; bound by ListenAndServe. outboundNodes NodeListOption // Nodes this server calls; set by WithPeers. outboundDialOpts []DialOption } @@ -109,13 +111,26 @@ func WithPeerChange(callback func(Configuration)) ServerOption { } } +// WithAddr records the address that [Server.ListenAndServe] binds. +// It only stores the address; nothing is resolved or bound until +// [Server.ListenAndServe] is called. +func WithAddr(addr string) ServerOption { + return func(o *serverOptions) { + o.listenAddr = addr + } +} + // Server serves all ordering based RPCs using registered handlers. type Server struct { srv *stream.Server grpcServer *grpc.Server handlers map[string]Handler interceptors []Interceptor - outbound Configuration // peer config built by WithPeers; nil if unused + + mu sync.Mutex // guards lis + lis net.Listener // active listener; set by Serve, ListenAndServe, or NewLocalServers + listenAddr string // address recorded by WithAddr + outbound Configuration // peer config built by WithPeers; nil if unused *inboundManager } @@ -146,6 +161,7 @@ func NewServer(opts ...ServerOption) *Server { grpcServer: grpc.NewServer(serverOpts.grpcOpts...), handlers: make(map[string]Handler), interceptors: serverOpts.interceptors, + listenAddr: serverOpts.listenAddr, } s.inboundManager = newInboundManager( serverOpts.myID, @@ -235,22 +251,81 @@ func (s *Server) HandleRequest(ctx context.Context, reqMsg *stream.Message, rele srvCtx.SendMessage(MessageWithError(in, out, err)) } -// Serve starts serving on the listener. +// Serve serves on the externally supplied listener and records it so that +// [Server.Addr] reports its address and [Server.Stop] closes it. The server +// takes lifecycle responsibility for the listener once Serve is called: Stop +// closes it even though gRPC also closes it when Serve returns. func (s *Server) Serve(listener net.Listener) error { + s.setListener(listener) return s.grpcServer.Serve(listener) } +// ListenAndServe binds the address recorded by [WithAddr] and serves on it. +// When the server was created by [NewLocalServers], it serves on the +// preallocated listener instead. It returns a clear error if no listen address +// was configured, or the bind error if the address is invalid or cannot be +// bound. When the configured address uses port 0, [Server.Addr] reports the +// actual bound address after this method creates the listener. +func (s *Server) ListenAndServe() error { + s.mu.Lock() + lis := s.lis + s.mu.Unlock() + if lis == nil { + if s.listenAddr == "" { + return fmt.Errorf("gorums: ListenAndServe requires a listen address; use WithAddr") + } + var err error + lis, err = net.Listen("tcp", s.listenAddr) + if err != nil { + return err + } + s.setListener(lis) + } + return s.grpcServer.Serve(lis) +} + +// setListener records lis as the server's active listener. +func (s *Server) setListener(lis net.Listener) { + s.mu.Lock() + s.lis = lis + s.mu.Unlock() +} + +// Addr returns the bound listener address once the server has a listener. +// Before binding, it returns the address configured with [WithAddr]. +// If neither exists, it returns the empty string. +func (s *Server) Addr() string { + s.mu.Lock() + lis := s.lis + s.mu.Unlock() + if lis != nil { + return lis.Addr().String() + } + return s.listenAddr +} + // GracefulStop waits for all RPCs to finish before stopping. func (s *Server) GracefulStop() { s.grpcServer.GracefulStop() } -// Stop stops the server immediately and releases the resources it owns, -// including the peer [Configuration] built by [WithPeers]. It does not use -// gRPC graceful stop, because one-way methods do not respond and would block -// indefinitely. Stop is safe to call more than once. +// Stop stops the server immediately and releases the resources it owns. It +// unblocks any [Server.WaitForPeers] and [Server.WaitForClients] callers, stops +// the gRPC server, closes the listener owned by [Server.Serve], +// [Server.ListenAndServe], or [NewLocalServers], and closes the peer +// [Configuration] built by [WithPeers]. It does not use gRPC graceful stop, +// because one-way methods do not respond and would block indefinitely. Stop is +// safe to call before serving starts, and safe to call more than once. func (s *Server) Stop() { + // Unblock any WaitForPeers / WaitForClients callers. + s.inboundManager.close() s.grpcServer.Stop() + s.mu.Lock() + lis := s.lis + s.mu.Unlock() + if lis != nil { + _ = lis.Close() + } if s.outbound != nil { _ = s.outbound.Close() } diff --git a/system_test.go b/server_e2e_test.go similarity index 61% rename from system_test.go rename to server_e2e_test.go index 7934e021..3ab61c8d 100644 --- a/system_test.go +++ b/server_e2e_test.go @@ -17,109 +17,17 @@ import ( pb "google.golang.org/protobuf/types/known/wrapperspb" ) -type mockCloser struct { - closed bool - err error -} - -func (m *mockCloser) Close() error { - m.closed = true - return m.err -} - -func TestSystemStopClosesRegisteredServices(t *testing.T) { - sys, err := gorums.NewSystem("127.0.0.1:0") +// TestNewLocalServersStopBeforeServeClosesListeners verifies that the stop function +// returned by NewLocalServers closes all pre-allocated listeners even when none of +// the servers has had Serve called yet, so no file descriptors are leaked. +func TestNewLocalServersStopBeforeServeClosesListeners(t *testing.T) { + servers, stop, err := gorums.NewLocalServers(3, gorums.WithLocalDialOptions(gorumstest.InsecureDialOptions(t))) if err != nil { - t.Fatalf("Failed to create system: %v", err) - } - - closer1 := &mockCloser{} - closer2 := &mockCloser{} - - sys.RegisterService(closer1, func(*gorums.Server) { - // In a real scenario, we would register a Gorums service here. - }) - sys.RegisterService(closer2, func(*gorums.Server) { - // Register another service or just use the callback. - }) - - go func() { - // Serve acts as a blocking call, so run in goroutine - if err := sys.Serve(); err != nil { - // Serve returns error on Stop usually (or net closed) - t.Logf("Serve returned: %v", err) - } - }() - - // Give it a moment to start - time.Sleep(10 * time.Millisecond) - - // Stop the system - if err := sys.Stop(); err != nil { - t.Errorf("Stop returned error: %v", err) - } - - if !closer1.closed { - t.Error("closer1 was not closed") + t.Fatalf("NewLocalServers: %v", err) } - if !closer2.closed { - t.Error("closer2 was not closed") - } -} - -func TestSystemStopReturnsCloserError(t *testing.T) { - sys, err := gorums.NewSystem("127.0.0.1:0") - if err != nil { - t.Fatalf("Failed to create system: %v", err) - } - - errCloser := &mockCloser{err: errors.New("closer error")} - - sys.RegisterService(errCloser, func(*gorums.Server) {}) - - go func() { - _ = sys.Serve() - }() - time.Sleep(10 * time.Millisecond) - - err = sys.Stop() - if err == nil { - t.Error("expected error from Stop, got nil") - } -} - -// TestSystemStopBeforeServeClosesListener verifies that Stop closes the -// pre-allocated listener even when Serve was never called, so no file -// descriptor is leaked. -func TestSystemStopBeforeServeClosesListener(t *testing.T) { - sys, err := gorums.NewSystem("127.0.0.1:0") - if err != nil { - t.Fatalf("NewSystem: %v", err) - } - addr := sys.Addr() - if err := sys.Stop(); err != nil { - t.Fatalf("Stop: %v", err) - } - // The listener should be closed now; re-binding to the same address must succeed. - lis, err := net.Listen("tcp", addr) - if err != nil { - t.Errorf("expected to re-bind to %s after Stop (without Serve), got: %v", addr, err) - return - } - _ = lis.Close() -} - -// TestNewLocalSystemsStopBeforeServeClosesListeners verifies that the stop function -// returned by NewLocalSystems closes all pre-allocated listeners even when none of -// the systems has had Serve called yet, so no file descriptors are leaked. -func TestNewLocalSystemsStopBeforeServeClosesListeners(t *testing.T) { - systems, stop, err := gorums.NewLocalSystems(3, gorums.WithLocalDialOptions(gorumstest.InsecureDialOptions(t))) - if err != nil { - t.Fatalf("NewLocalSystems: %v", err) - } - addrs := make([]string, len(systems)) - for i, sys := range systems { - addrs[i] = sys.Addr() + addrs := make([]string, len(servers)) + for i, srv := range servers { + addrs[i] = srv.Addr() } stop() // called before any Serve() // Every pre-allocated listener must be closed; re-binding must succeed. @@ -133,27 +41,20 @@ func TestNewLocalSystemsStopBeforeServeClosesListeners(t *testing.T) { } } -func TestSystemSymmetricConfigurationConnectsAllPeers(t *testing.T) { - systems := gorumstest.Systems(t, 3) - - // Outbound config is auto-created by NewLocalSystems. - // (NodeID is automatically included in connection metadata) - for _, sys := range systems { - sys.RegisterService(nil, func(*gorums.Server) { - // Register mock handlers for the server sides if needed for other tests - }) - } +func TestServerSymmetricConfigurationConnectsAllPeers(t *testing.T) { + servers := gorumstest.LocalServers(t, 3) - // Wait for connections to establish - for i, sys := range systems { + // The peer configuration is auto-created by NewLocalServers, which also + // includes each server's node ID in its connection metadata. + for i, srv := range servers { ctx := gorumstest.Context(t, 5*time.Second) - if err := sys.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { - return cfg.Size() == len(systems) + if err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + return cfg.Size() == len(servers) }); err != nil { - t.Fatalf("system %d: WaitForPeers: %v", i+1, err) + t.Fatalf("server %d: WaitForPeers: %v", i+1, err) } - if got := sys.ConnectedPeers().Size(); got != len(systems) { - t.Fatalf("system %d config size: %d, expected: %d", i+1, got, len(systems)) + if got := srv.ConnectedPeers().Size(); got != len(servers) { + t.Fatalf("server %d config size: %d, expected: %d", i+1, got, len(servers)) } } } @@ -173,63 +74,69 @@ func waitWithTimeout(t *testing.T, wg *sync.WaitGroup) { } } -func awaitSystemReady(t *testing.T, systems []*gorums.System) { +func awaitServerReady(t *testing.T, servers []*gorums.Server) { t.Helper() - for _, sys := range systems { + for _, srv := range servers { ctx := gorumstest.Context(t, 5*time.Second) - if err := sys.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { - return cfg.Size() == len(systems) + if err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + return cfg.Size() == len(servers) }); err != nil { - t.Fatalf("awaitSystemReady: %v", err) + t.Fatalf("awaitServerReady: %v", err) } } } -// awaitClientReady waits until the server's ClientConfig contains n connected peers. -func awaitClientReady(t *testing.T, sys *gorums.System, n int) { +// awaitClientReady waits until the server's ConnectedClients contains n connected peers. +func awaitClientReady(t *testing.T, srv *gorums.Server, n int) { t.Helper() ctx := gorumstest.Context(t, 5*time.Second) - if err := sys.WaitForClients(ctx, func(cfg gorums.Configuration) bool { + if err := srv.WaitForClients(ctx, func(cfg gorums.Configuration) bool { return cfg.Size() == n }); err != nil { t.Fatalf("awaitClientReady: %v", err) } } -// createClientServerSystems creates a server system and a client for back-channel testing. -// The server automatically tracks anonymous clients and can dispatch reverse-direction -// calls to them via [ServerCtx.ClientConfig]. +// createClientServerPair creates a server and a client for back-channel testing. +// The server automatically tracks anonymous clients and can dispatch +// reverse-direction calls to them via [gorums.ServerCtx.ConnectedClients]. // The client is a standalone [*gorums.Server] (no listener needed) whose registered handlers // are reachable by the server over the existing bidirectional gRPC stream. The returned // [gorums.Configuration] is the client's outbound config pointing at the server. -func createClientServerSystems(t *testing.T) (*gorums.System, *gorums.Server, gorums.Configuration) { +func createClientServerPair(t *testing.T) (*gorums.Server, *gorums.Server, gorums.Configuration) { t.Helper() - // Server side: accepts anonymous clients for reverse-direction calls. - sys, err := gorums.NewSystem("127.0.0.1:0") + // Bind the listener up front so the client knows the address before the + // server starts serving on it. + lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } + // Server side: accepts anonymous clients for reverse-direction calls. + srv := gorums.NewServer() + // Client side: a plain Server whose handlers the server can invoke via the back-channel. // No listener is required — dispatch is over the client's outbound gRPC stream. clientSrv := gorums.NewServer() - // The client dials the server; WithServer wires up the back-channel dispatcher. - nodeList := gorums.WithNodeList([]string{sys.Addr()}) + // The client dials the server; WithBackChannel wires up the back-channel dispatcher. + nodeList := gorums.WithNodeList([]string{lis.Addr().String()}) cfg, err := gorums.NewConfig(nodeList, gorums.WithBackChannel(clientSrv), gorumstest.InsecureDialOptions(t)) if err != nil { t.Fatal(err) } - go func() { _ = sys.Serve() }() + go func() { _ = srv.Serve(lis) }() - t.Cleanup(func() { - _ = cfg.Close() - _ = sys.Stop() - }) + // Registered in reverse order so cleanup (LIFO) closes the client's + // outbound config before stopping clientSrv and then srv, which stops the + // server and closes lis. + t.Cleanup(srv.Stop) + t.Cleanup(clientSrv.Stop) + t.Cleanup(gorumstest.Closer(t, cfg)) - return sys, clientSrv, cfg + return srv, clientSrv, cfg } // stringEchoHandler returns a handler that replies with prefix+": "+request value. @@ -269,7 +176,7 @@ func outerChainedHandler( t.Helper() return func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) - t.Logf("System %d received outer request: %s", myID, req.GetValue()) + t.Logf("Server %d received outer request: %s", myID, req.GetValue()) // Release the NodeStream mutex before making the inner quorum call. // Without this, the NodeStream's Recv loop cannot read the inner-call // responses off the wire while this handler is blocked waiting for them, @@ -292,17 +199,15 @@ func outerChainedHandler( } } -func TestSystemSymmetricConfigurationRoutesQuorumCalls(t *testing.T) { - systems := gorumstest.Systems(t, 3) +func TestServerSymmetricConfigurationRoutesQuorumCalls(t *testing.T) { + servers := gorumstest.LocalServers(t, 3) - // Register mock handler to each system - for _, sys := range systems { - sys.RegisterService(nil, func(srv *gorums.Server) { - srv.RegisterHandler(mock.TestMethod, stringEchoHandler("echo")) - }) + // Register mock handler on each server + for _, srv := range servers { + srv.RegisterHandler(mock.TestMethod, stringEchoHandler("echo")) } - awaitSystemReady(t, systems) + awaitServerReady(t, servers) // type alias short hand for the responses type type respType = *gorums.Responses[*pb.StringValue] @@ -328,8 +233,8 @@ func TestSystemSymmetricConfigurationRoutesQuorumCalls(t *testing.T) { }, } - // Use the auto-created outbound config from system 0. - cfg := systems[0].OutboundConfig() + // Use the auto-created peer config from server 0. + cfg := servers[0].PeerConfig() // Sub tests for each response type logic across symmetric routing for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -353,25 +258,23 @@ func TestSystemSymmetricConfigurationRoutesQuorumCalls(t *testing.T) { } } -func TestSystemSymmetricConfigurationRoutesMulticast(t *testing.T) { - systems := gorumstest.Systems(t, 3) +func TestServerSymmetricConfigurationRoutesMulticast(t *testing.T) { + servers := gorumstest.LocalServers(t, 3) var wg sync.WaitGroup - wg.Add(len(systems)) - - // Register mock handler to each system - for _, sys := range systems { - sys.RegisterService(nil, func(srv *gorums.Server) { - srv.RegisterHandler(mock.Stream, func(_ gorums.ServerCtx, _ *gorums.Message) (*gorums.Message, error) { - wg.Done() - return nil, nil - }) + wg.Add(len(servers)) + + // Register mock handler on each server + for _, srv := range servers { + srv.RegisterHandler(mock.Stream, func(_ gorums.ServerCtx, _ *gorums.Message) (*gorums.Message, error) { + wg.Done() + return nil, nil }) } - awaitSystemReady(t, systems) + awaitServerReady(t, servers) - cfg := systems[0].OutboundConfig() + cfg := servers[0].PeerConfig() ctx := gorumstest.Context(t, 2*time.Second) err := gorums.Multicast( cfg.Context(ctx), @@ -385,8 +288,8 @@ func TestSystemSymmetricConfigurationRoutesMulticast(t *testing.T) { waitWithTimeout(t, &wg) } -func TestSystemHandlerCanMulticastViaConfig(t *testing.T) { - systems := gorumstest.Systems(t, 3) +func TestServerHandlerCanMulticastViaConfig(t *testing.T) { + servers := gorumstest.LocalServers(t, 3) // 3 servers receive the outer multicast. Each server multicasts to a config of 3 nodes. // The self-node's handler is invoked locally, so each server sends to all 3 nodes. @@ -394,38 +297,36 @@ func TestSystemHandlerCanMulticastViaConfig(t *testing.T) { var wg sync.WaitGroup wg.Add(9) - for i, sys := range systems { - sys.RegisterService(nil, func(srv *gorums.Server) { - srv.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { - t.Logf("System %d received multicast on %v: %v", i+1, mock.TestMethod, in.Msg) - // Release before the nested multicast: the peer configuration - // includes the local node, whose in-process dispatch waits for - // this handler's dispatch lock. - ctx.Release() - if cfg := ctx.PeerConfig(); cfg.Size() == 3 { - err := gorums.Multicast( - cfg.Context(t.Context()), - pb.String("inner-multicast"), - mock.Stream, - ) - if err != nil { - return nil, err // failed to multicast - } + for i, srv := range servers { + srv.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + t.Logf("Server %d received multicast on %v: %v", i+1, mock.TestMethod, in.Msg) + // Release before the nested multicast: the peer configuration + // includes the local node, whose in-process dispatch waits for + // this handler's dispatch lock. + ctx.Release() + if cfg := ctx.PeerConfig(); cfg.Size() == 3 { + err := gorums.Multicast( + cfg.Context(t.Context()), + pb.String("inner-multicast"), + mock.Stream, + ) + if err != nil { + return nil, err // failed to multicast } - return nil, nil // one-way - }) + } + return nil, nil // one-way + }) - srv.RegisterHandler(mock.Stream, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { - t.Logf("System %d received multicast on %v: %v", i+1, mock.Stream, in.Msg) - wg.Done() - return nil, nil - }) + srv.RegisterHandler(mock.Stream, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + t.Logf("Server %d received multicast on %v: %v", i+1, mock.Stream, in.Msg) + wg.Done() + return nil, nil }) } - awaitSystemReady(t, systems) + awaitServerReady(t, servers) - cfg := systems[0].OutboundConfig() + cfg := servers[0].PeerConfig() ctx := gorumstest.Context(t, 2*time.Second) err := gorums.Multicast( cfg.Context(ctx), @@ -439,7 +340,7 @@ func TestSystemHandlerCanMulticastViaConfig(t *testing.T) { waitWithTimeout(t, &wg) } -func TestSystemHandlerCanChainQuorumCallViaConfig(t *testing.T) { +func TestServerHandlerCanChainQuorumCallViaConfig(t *testing.T) { type respType = *gorums.Responses[*pb.StringValue] // seqAll drains the Results iterator to exhaustion and returns the last value. @@ -471,19 +372,17 @@ func TestSystemHandlerCanChainQuorumCallViaConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - systems := gorumstest.Systems(t, 3) + servers := gorumstest.LocalServers(t, 3) - for i, sys := range systems { + for i, srv := range servers { myID := i + 1 - sys.RegisterService(nil, func(srv *gorums.Server) { - srv.RegisterHandler(mock.TestMethod, outerChainedHandler(t, myID, false, mock.EchoMethod, tt.innerFn)) - srv.RegisterHandler(mock.EchoMethod, stringEchoHandler("inner-echo")) - }) + srv.RegisterHandler(mock.TestMethod, outerChainedHandler(t, myID, false, mock.EchoMethod, tt.innerFn)) + srv.RegisterHandler(mock.EchoMethod, stringEchoHandler("inner-echo")) } - awaitSystemReady(t, systems) + awaitServerReady(t, servers) - cfg := systems[0].OutboundConfig() + cfg := servers[0].PeerConfig() ctx := gorumstest.Context(t, 2*time.Second) responses := gorums.QuorumCall[*pb.StringValue, *pb.StringValue]( @@ -505,19 +404,17 @@ func TestSystemHandlerCanChainQuorumCallViaConfig(t *testing.T) { } } -func TestSystemHandlerCanChainQuorumCallViaClientConfig(t *testing.T) { - sysServer, clientSrv, cfgClient := createClientServerSystems(t) +func TestServerHandlerCanChainQuorumCallViaConnectedClients(t *testing.T) { + srvServer, clientSrv, cfgClient := createClientServerPair(t) // Server: outer handler fans out an inner quorum call on EchoMethod to all // client peers and returns whichever responds first. - sysServer.RegisterService(nil, func(srv *gorums.Server) { - srv.RegisterHandler(mock.TestMethod, outerChainedHandler(t, 1, true, mock.EchoMethod, (*gorums.Responses[*pb.StringValue]).First)) - }) + srvServer.RegisterHandler(mock.TestMethod, outerChainedHandler(t, 1, true, mock.EchoMethod, (*gorums.Responses[*pb.StringValue]).First)) - // Client: handles EchoMethod calls dispatched back by the server via ClientConfig. + // Client: handles EchoMethod calls dispatched back by the server via ConnectedClients. clientSrv.RegisterHandler(mock.EchoMethod, stringEchoHandler("client-echo")) - awaitClientReady(t, sysServer, 1) + awaitClientReady(t, srvServer, 1) ctx := gorumstest.Context(t, 2*time.Second) responses := gorums.QuorumCall[*pb.StringValue, *pb.StringValue]( @@ -537,29 +434,27 @@ func TestSystemHandlerCanChainQuorumCallViaClientConfig(t *testing.T) { } } -func TestSystemHandlerCanMulticastViaClientConfig(t *testing.T) { - sysServer, clientSrv, cfgClient := createClientServerSystems(t) +func TestServerHandlerCanMulticastViaConnectedClients(t *testing.T) { + srvServer, clientSrv, cfgClient := createClientServerPair(t) // Outer multicast from client triggers the server handler once. // The server fans out an inner multicast via ClientConfig (1 client) -> 1 message. var wg sync.WaitGroup wg.Add(1) - sysServer.RegisterService(nil, func(srv *gorums.Server) { - srv.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { - t.Logf("SERVER received multicast: %v", in.Msg) - if cfg := ctx.ConnectedClients(); cfg != nil && cfg.Size() == 1 { - err := gorums.Multicast( - cfg.Context(t.Context()), - pb.String("inner-call"), - mock.Stream, - ) - if err != nil { - return nil, err // failed to multicast - } + srvServer.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + t.Logf("SERVER received multicast: %v", in.Msg) + if cfg := ctx.ConnectedClients(); cfg != nil && cfg.Size() == 1 { + err := gorums.Multicast( + cfg.Context(t.Context()), + pb.String("inner-call"), + mock.Stream, + ) + if err != nil { + return nil, err // failed to multicast } - return nil, nil // one-way - }) + } + return nil, nil // one-way }) // Client handles the reverse-direction multicast dispatched by the server. @@ -569,7 +464,7 @@ func TestSystemHandlerCanMulticastViaClientConfig(t *testing.T) { return nil, nil }) - awaitClientReady(t, sysServer, 1) + awaitClientReady(t, srvServer, 1) ctx := gorumstest.Context(t, 2*time.Second) err := gorums.Multicast( @@ -584,7 +479,7 @@ func TestSystemHandlerCanMulticastViaClientConfig(t *testing.T) { waitWithTimeout(t, &wg) } -// TestSystemLocalDispatchContention verifies that sequential quorum calls remain +// TestServerLocalDispatchContention verifies that sequential quorum calls remain // correct when an earlier call returns before all replicas have replied and the // next call starts immediately on the same configuration. // @@ -592,22 +487,20 @@ func TestSystemHandlerCanMulticastViaClientConfig(t *testing.T) { // Concurrent quorum calls (from separate goroutines) violate the FIFO ordering // contract (see doc/ordering.md) and are therefore not tested. // -// Each subtest creates its own isolated systems so that goroutines left over +// Each subtest creates its own isolated servers so that goroutines left over // from one subtest cannot contaminate the next. -func TestSystemLocalDispatchContention(t *testing.T) { - startSystems := func(t *testing.T) gorums.Configuration { +func TestServerLocalDispatchContention(t *testing.T) { + startServers := func(t *testing.T) gorums.Configuration { t.Helper() - systems := gorumstest.Systems(t, 3) - for _, sys := range systems { - sys.RegisterService(nil, func(srv *gorums.Server) { - srv.RegisterHandler(mock.TestMethod, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { - req := gorums.AsProto[*pb.StringValue](in) - return gorums.NewResponseMessage(in, pb.String("echo: "+req.GetValue())), nil - }) + servers := gorumstest.LocalServers(t, 3) + for _, srv := range servers { + srv.RegisterHandler(mock.TestMethod, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + req := gorums.AsProto[*pb.StringValue](in) + return gorums.NewResponseMessage(in, pb.String("echo: "+req.GetValue())), nil }) } - awaitSystemReady(t, systems) - return systems[0].OutboundConfig() + awaitServerReady(t, servers) + return servers[0].PeerConfig() } const delay = 2000 * time.Millisecond @@ -615,7 +508,7 @@ func TestSystemLocalDispatchContention(t *testing.T) { // SequentialMajorityThenAll exercises the common case where a quorum-sized // result is returned first and a full-result call follows immediately after. t.Run("SequentialMajorityThenAll", func(t *testing.T) { - cfg := startSystems(t) + cfg := startServers(t) const iterations = 500 for i := range iterations { ctx, cancel := context.WithTimeout(t.Context(), delay) @@ -651,7 +544,7 @@ func TestSystemLocalDispatchContention(t *testing.T) { prev := runtime.GOMAXPROCS(1) defer runtime.GOMAXPROCS(prev) - cfg := startSystems(t) + cfg := startServers(t) const iterations = 500 for i := range iterations { ctx, cancel := context.WithTimeout(t.Context(), delay) @@ -681,7 +574,7 @@ func TestSystemLocalDispatchContention(t *testing.T) { }) } -// TestSystemLocalDispatchContentionSlowReplica verifies that a slow local +// TestServerLocalDispatchContentionSlowReplica verifies that a slow local // replica does not prevent a new quorum call from making progress on replies // from the remote replicas. // @@ -689,34 +582,30 @@ func TestSystemLocalDispatchContention(t *testing.T) { // local replica is intentionally delayed, then immediately issues an All call. // The All call must observe remote progress right away and complete once the // delayed local reply is finally allowed through. -func TestSystemLocalDispatchContentionSlowReplica(t *testing.T) { - systems := gorumstest.Systems(t, 3) +func TestServerLocalDispatchContentionSlowReplica(t *testing.T) { + servers := gorumstest.LocalServers(t, 3) - // blocker delays system 0's handler so its reply is intentionally late. + // blocker delays server 0's handler so its reply is intentionally late. blocker := make(chan struct{}) closeBlocker := sync.OnceFunc(func() { close(blocker) }) t.Cleanup(closeBlocker) // safety net: unblock handler goroutines on test exit - for i, sys := range systems { + for i, srv := range servers { if i == 0 { - // System 0 (self-node): block until signaled. - sys.RegisterService(nil, func(srv *gorums.Server) { - srv.RegisterHandler(mock.TestMethod, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { - <-blocker - req := gorums.AsProto[*pb.StringValue](in) - return gorums.NewResponseMessage(in, pb.String("echo: "+req.GetValue())), nil - }) + // Server 0 (self-node): block until signaled. + srv.RegisterHandler(mock.TestMethod, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + <-blocker + req := gorums.AsProto[*pb.StringValue](in) + return gorums.NewResponseMessage(in, pb.String("echo: "+req.GetValue())), nil }) } else { - // Systems 1, 2 (remote): respond immediately. - sys.RegisterService(nil, func(srv *gorums.Server) { - srv.RegisterHandler(mock.TestMethod, stringEchoHandler("echo")) - }) + // Servers 1, 2 (remote): respond immediately. + srv.RegisterHandler(mock.TestMethod, stringEchoHandler("echo")) } } - awaitSystemReady(t, systems) - cfg := systems[0].OutboundConfig() + awaitServerReady(t, servers) + cfg := servers[0].PeerConfig() // Step 1: First(1) succeeds from a remote response while the local reply is // still blocked. @@ -763,11 +652,11 @@ func TestSystemLocalDispatchContentionSlowReplica(t *testing.T) { func TestWaitForPeers(t *testing.T) { t.Run("ConditionAlreadyMet", func(t *testing.T) { - systems := gorumstest.Systems(t, 3) - awaitSystemReady(t, systems) + servers := gorumstest.LocalServers(t, 3) + awaitServerReady(t, servers) ctx := gorumstest.Context(t, 2*time.Second) - if err := systems[0].WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + if err := servers[0].WaitForPeers(ctx, func(cfg gorums.Configuration) bool { return cfg.Size() == 3 }); err != nil { t.Fatalf("WaitForPeers: %v", err) @@ -775,10 +664,10 @@ func TestWaitForPeers(t *testing.T) { }) t.Run("ConditionMetAfterConnect", func(t *testing.T) { - systems := gorumstest.Systems(t, 3) + servers := gorumstest.LocalServers(t, 3) ctx := gorumstest.Context(t, 5*time.Second) - if err := systems[0].WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + if err := servers[0].WaitForPeers(ctx, func(cfg gorums.Configuration) bool { return cfg.Size() == 3 }); err != nil { t.Fatalf("WaitForPeers: %v", err) @@ -786,16 +675,13 @@ func TestWaitForPeers(t *testing.T) { }) t.Run("ContextCancelled", func(t *testing.T) { - sys, err := gorums.NewSystem("127.0.0.1:0") - if err != nil { - t.Fatalf("NewSystem: %v", err) - } - go func() { _ = sys.Serve() }() - t.Cleanup(func() { _ = sys.Stop() }) + srv := gorums.NewServer(gorums.WithAddr("127.0.0.1:0")) + go func() { _ = srv.ListenAndServe() }() + t.Cleanup(srv.Stop) ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond) defer cancel() - err = sys.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { return cfg.Size() == 3 // never true }) if !errors.Is(err, context.DeadlineExceeded) { @@ -803,23 +689,20 @@ func TestWaitForPeers(t *testing.T) { } }) - t.Run("SystemStopped", func(t *testing.T) { - sys, err := gorums.NewSystem("127.0.0.1:0") - if err != nil { - t.Fatalf("NewSystem: %v", err) - } - go func() { _ = sys.Serve() }() + t.Run("ServerStopped", func(t *testing.T) { + srv := gorums.NewServer(gorums.WithAddr("127.0.0.1:0")) + go func() { _ = srv.ListenAndServe() }() errCh := make(chan error, 1) go func() { - errCh <- sys.WaitForPeers(context.Background(), func(cfg gorums.Configuration) bool { + errCh <- srv.WaitForPeers(context.Background(), func(cfg gorums.Configuration) bool { return cfg.Size() == 3 // never true }) }() // Give WaitForPeers time to enter the select. time.Sleep(20 * time.Millisecond) - _ = sys.Stop() + srv.Stop() select { case err := <-errCh: @@ -832,14 +715,14 @@ func TestWaitForPeers(t *testing.T) { }) t.Run("ConcurrentWaiters", func(t *testing.T) { - systems := gorumstest.Systems(t, 3) + servers := gorumstest.LocalServers(t, 3) const waiters = 5 errCh := make(chan error, waiters) for range waiters { ctx := gorumstest.Context(t, 5*time.Second) go func(ctx context.Context) { - errCh <- systems[0].WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + errCh <- servers[0].WaitForPeers(ctx, func(cfg gorums.Configuration) bool { return cfg.Size() == 3 }) }(ctx) @@ -853,10 +736,10 @@ func TestWaitForPeers(t *testing.T) { }) t.Run("ClientConfig", func(t *testing.T) { - sysServer, _, _ := createClientServerSystems(t) + srvServer, _, _ := createClientServerPair(t) ctx := gorumstest.Context(t, 5*time.Second) - if err := sysServer.WaitForClients(ctx, func(cfg gorums.Configuration) bool { + if err := srvServer.WaitForClients(ctx, func(cfg gorums.Configuration) bool { return cfg.Size() == 1 }); err != nil { t.Fatalf("WaitForClients: %v", err) diff --git a/system.go b/system.go deleted file mode 100644 index ebdc0e3e..00000000 --- a/system.go +++ /dev/null @@ -1,218 +0,0 @@ -package gorums - -import ( - "context" - "errors" - "io" - "net" -) - -// System encapsulates the state of a Gorums system, including the server, -// listener, and any registered closers (e.g. managers). -type System struct { - closers []io.Closer - srv *Server - lis net.Listener -} - -// NewSystem creates a new Gorums System listening on the specified address. -// Accepts any [DialOption]s. Server options may be passed via [WithServerOptions]; -// pass [WithPeers] there to have the server build a peer [Configuration], -// accessible via [System.OutboundConfig]. -func NewSystem(addr string, opts ...DialOption) (*System, error) { - dialOpts := newDialOptions() - for _, opt := range opts { - opt(&dialOpts) - } - lis, err := net.Listen("tcp", addr) - if err != nil { - return nil, err - } - return &System{ - srv: NewServer(dialOpts.srvOpts...), - lis: lis, - }, nil -} - -// localServerOptions accumulates the options [NewLocalSystems] applies to every -// system it creates. -type localServerOptions struct { - serverOpts []ServerOption - dialOpts []DialOption -} - -// LocalServerOption configures [NewLocalSystems]. Use [WithLocalServerOptions] -// and [WithLocalDialOptions] to build one. -type LocalServerOption func(*localServerOptions) - -// WithLocalServerOptions applies opts to every server created by [NewLocalSystems]. -func WithLocalServerOptions(opts ...ServerOption) LocalServerOption { - return func(o *localServerOptions) { - o.serverOpts = append(o.serverOpts, opts...) - } -} - -// WithLocalDialOptions applies opts to every server's peer configuration -// created by [NewLocalSystems]. -func WithLocalDialOptions(opts ...DialOption) LocalServerOption { - return func(o *localServerOptions) { - o.dialOpts = append(o.dialOpts, opts...) - } -} - -// NewLocalSystems creates n Gorums systems listening on random localhost ports. -// -// Each system is assigned a node ID in the range 1..n and is configured to -// communicate with the others using the generated local node list. A peer -// [Configuration] is created automatically for each system and is available via -// [System.OutboundConfig]. -// -// Use [WithLocalServerOptions] to add [ServerOption]s to every server, and -// [WithLocalDialOptions] to add [DialOption]s to every server's peer -// connections. -// -// The returned systems are not started. Call [System.Serve] after registering -// any services. The returned stop function stops all systems and should be -// called when they are no longer needed. -// -// If listener allocation fails, all listeners acquired so far are closed before -// returning the error. -func NewLocalSystems(n int, opts ...LocalServerOption) ([]*System, func(), error) { - var localOpts localServerOptions - for _, opt := range opts { - if opt != nil { - opt(&localOpts) - } - } - listeners, nodeList, err := allocateListeners(n) - if err != nil { - return nil, nil, err - } - systems := make([]*System, n) - for i := range n { - myID := uint32(i + 1) - sysSrvOpts := append( - []ServerOption{WithPeers(myID, nodeList, localOpts.dialOpts...)}, - localOpts.serverOpts..., - ) - systems[i] = &System{ - srv: NewServer(sysSrvOpts...), - lis: listeners[i], - } - } - stop := func() { - for _, sys := range systems { - _ = sys.Stop() - } - } - return systems, stop, nil -} - -// allocateListeners pre-allocates n TCP listeners on random localhost ports and -// returns them along with a [NodeListOption] containing their addresses. If any -// listener fails to open, all previously opened listeners are closed before -// returning the error. -func allocateListeners(n int) ([]net.Listener, NodeListOption, error) { - listeners := make([]net.Listener, n) - addrs := make([]string, n) - for i := range n { - lis, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - for j := range i { - _ = listeners[j].Close() - } - return nil, nil, err - } - listeners[i] = lis - addrs[i] = lis.Addr().String() - } - return listeners, WithNodeList(addrs), nil -} - -// OutboundConfig returns the server's peer [Configuration], or nil if the -// server was not configured with [WithPeers]. -func (s *System) OutboundConfig() Configuration { - return s.srv.PeerConfig() -} - -// Addr returns the address the system is listening on. -func (s *System) Addr() string { - return s.lis.Addr().String() -} - -// ConnectedPeers returns the currently reachable subset of the server's peer -// [Configuration], including this node. -// An empty (non-nil) Configuration is returned if no known peers are connected. -// The returned slice is replaced atomically on each connect/disconnect; -// thus, retaining a reference to an old configuration is safe. -func (s *System) ConnectedPeers() Configuration { - return s.srv.ConnectedPeers() -} - -// ConnectedClients returns a [Configuration] of all connected client peers -// that can accept server-initiated requests. -// An empty (non-nil) Configuration is returned if no client peers are connected. -// The returned slice is replaced atomically on each connect/disconnect; -// thus, retaining a reference to an old configuration is safe. -func (s *System) ConnectedClients() Configuration { - return s.srv.ConnectedClients() -} - -// WaitForPeers blocks until cond returns true for the current connected-peer -// [Configuration], or until ctx is cancelled or the system is stopped. -// The condition is checked immediately against the current configuration, -// so it may return without blocking if the condition is already satisfied. -func (s *System) WaitForPeers(ctx context.Context, cond func(Configuration) bool) error { - return s.srv.WaitForPeers(ctx, cond) -} - -// WaitForClients blocks until cond returns true for the current -// client-peer [Configuration], or until ctx is cancelled or the system is stopped. -// The condition is checked immediately against the current configuration, -// so it may return without blocking if the condition is already satisfied. -func (s *System) WaitForClients(ctx context.Context, cond func(Configuration) bool) error { - return s.srv.WaitForClients(ctx, cond) -} - -// RegisterService registers the service with the server using the provided register function. -// The closer is added to the list of closers to be closed when the system is stopped. -// -// Example usage: -// -// gs := NewSystem(lis) -// impl := &srvImpl{} -// gs.RegisterService(nil, func(srv *Server) { -// pb.RegisterMultiPaxosServer(srv, impl) -// }) -func (s *System) RegisterService(closer io.Closer, registerFunc func(*Server)) { - if closer != nil { - s.closers = append(s.closers, closer) - } - registerFunc(s.srv) -} - -// Serve starts the server. -func (s *System) Serve() error { - return s.srv.Serve(s.lis) -} - -// Stop stops the Gorums server and closes all registered closers. -// It immediately closes all open connections and listeners. It cancels -// all active RPCs on the server side and the corresponding pending RPCs -// on the client side will get notified by connection errors. -// It is safe to call Stop before [System.Serve] to avoid resource leaks. -func (s *System) Stop() (errs error) { - // Unblock any WaitForPeers / WaitForClients callers. - s.srv.close() - // We cannot use graceful stop here since multicast methods does not - // respond to the client, and thus would block indefinitely. - s.srv.Stop() - // Always close the listener explicitly. If Serve was called, gRPC - // already closed it and the second Close is a no-op error we discard. - // If Serve was never called, this is the only place that closes it. - _ = s.lis.Close() - for _, closer := range s.closers { - errs = errors.Join(errs, closer.Close()) - } - return errs -} From 1076c2662407c05c2f7380ab6ba2bf0634521214 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:16:09 +0200 Subject: [PATCH 02/16] gorums: rename Configuration to Config Configuration is the longest identifier in the public API and appears in almost every signature. Config says the same thing, matches ConfigContext and NewConfig which already used the shorter form, and removes the mismatch where a type named Configuration was constructed by a function named NewConfig. ConfigContext.Configuration becomes ConfigContext.Config for the same reason. Config joins the reserved identifiers injected into generated code, so a proto message named Config now collides where one named Configuration used to. Two test protos are adjusted: the reserved-name failure fixture asserts on the new name, and internal/tests/config renames its Config RPC to Read, which also reads better for a quorum call returning a value. --- AGENTS.md | 2 +- callopts_test.go | 2 +- client_interceptor.go | 8 +- cmd/protoc-gen-gorums/dev/aliases.go | 2 +- .../gengorums/gorums_bundle_test.go | 2 +- config.go | 76 +++++++++---------- config_opts.go | 18 ++--- config_test.go | 10 +-- doc/dev-guide.md | 2 +- doc/migration.md | 18 ++--- doc/user-guide.md | 38 +++++----- examples/storage/repl.go | 20 ++--- examples/storage/server.go | 4 +- gorumstest/gorumstest.go | 8 +- handler.go | 12 +-- inbound_manager.go | 46 +++++------ inbound_manager_test.go | 44 +++++------ .../failing/reservednames/reserved.proto | 6 +- internal/tests/config/config.proto | 2 +- internal/tests/config/config_test.go | 8 +- internal/tests/oneway/oneway_test.go | 2 +- node.go | 8 +- node_test.go | 8 +- opts.go | 2 +- responses_test.go | 2 +- server.go | 34 ++++----- server_e2e_test.go | 24 +++--- 27 files changed, 204 insertions(+), 204 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c287c793..3261e0f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -192,7 +192,7 @@ Use integration mode for performance benchmarking and network-specific validatio Gorums provides custom protobuf options defined in `gorums.proto`: - Method-level options for quorum call types -- Configuration options for RPC behavior +- Config options for RPC behavior - See `doc/user-guide.md` for details ## Documentation diff --git a/callopts_test.go b/callopts_test.go index 43ae4b89..a2ba75cf 100644 --- a/callopts_test.go +++ b/callopts_test.go @@ -87,7 +87,7 @@ func TestCallOptionsIgnoreErrorsResourceLeak(t *testing.T) { }) } for _, srv := range servers { - srv.WaitForPeers(t.Context(), func(cfg Configuration) bool { + srv.WaitForPeers(t.Context(), func(cfg Config) bool { return cfg.Size() == 3 }) } diff --git a/client_interceptor.go b/client_interceptor.go index 5a0e8b19..c6c1ae09 100644 --- a/client_interceptor.go +++ b/client_interceptor.go @@ -40,7 +40,7 @@ type QuorumInterceptor[Req, Resp msg] func(ctx *ClientCtx[Req, Resp], next Respo // It exposes the request, configuration, metadata about the call, and the response iterator. type ClientCtx[Req, Resp msg] struct { context.Context - config Configuration + config Config request Req method string msgID uint64 @@ -79,7 +79,7 @@ func newQuorumCallClientCtx[Req, Resp msg]( streaming bool, interceptors []any, ) *ClientCtx[Req, Resp] { - config := ctx.Configuration() + config := ctx.Config() n := config.Size() if streaming { n *= 10 @@ -112,7 +112,7 @@ func newMulticastClientCtx[Req msg]( waitForSend bool, interceptors []any, ) *ClientCtx[Req, *emptypb.Empty] { - config := ctx.Configuration() + config := ctx.Config() var replyChan chan NodeResponse[*stream.Message] if waitForSend { replyChan = make(chan NodeResponse[*stream.Message], config.Size()) @@ -141,7 +141,7 @@ func (c *ClientCtx[Req, Resp]) Request() Req { } // Config returns the configuration (set of nodes) for this quorum call. -func (c *ClientCtx[Req, Resp]) Config() Configuration { +func (c *ClientCtx[Req, Resp]) Config() Config { return c.config } diff --git a/cmd/protoc-gen-gorums/dev/aliases.go b/cmd/protoc-gen-gorums/dev/aliases.go index 8d39c416..a657ece5 100644 --- a/cmd/protoc-gen-gorums/dev/aliases.go +++ b/cmd/protoc-gen-gorums/dev/aliases.go @@ -16,7 +16,7 @@ import gorums "github.com/relab/gorums" // automatically discover them and add them to the reserved identifiers list. type ( - Configuration = gorums.Configuration + Config = gorums.Config Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext diff --git a/cmd/protoc-gen-gorums/gengorums/gorums_bundle_test.go b/cmd/protoc-gen-gorums/gengorums/gorums_bundle_test.go index 5073096b..576f10ec 100644 --- a/cmd/protoc-gen-gorums/gengorums/gorums_bundle_test.go +++ b/cmd/protoc-gen-gorums/gengorums/gorums_bundle_test.go @@ -12,7 +12,7 @@ import ( func TestReservedIdentifiers(t *testing.T) { pkg := loadPackage("github.com/relab/gorums/cmd/protoc-gen-gorums/dev") _, got := findIdentifiers(pkg) - want := []string{"ConfigContext", "Configuration", "Node", "NodeContext"} + want := []string{"Config", "ConfigContext", "Node", "NodeContext"} if !slices.Equal(got, want) { t.Errorf("generated static surface changed:\ngot: %v\nwant: %v\nIf intentional, update aliases.go and this want slice.", got, want) } diff --git a/config.go b/config.go index 2ab981ba..2d95ab83 100644 --- a/config.go +++ b/config.go @@ -8,23 +8,23 @@ import ( "time" ) -// Configuration represents a static set of nodes on which multicast or +// Config 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 +// operate on a configuration always return a new Config instance. +type Config []*Node // 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. +// Use [Config.Context] to create a ConfigContext from an existing context. type ConfigContext struct { context.Context - cfg Configuration + cfg Config } -// Configuration returns the configuration associated with this context. -func (c ConfigContext) Configuration() Configuration { +// Config returns the configuration associated with this context. +func (c ConfigContext) Config() Config { return c.cfg } @@ -36,14 +36,14 @@ func (c ConfigContext) Configuration() Configuration { // 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 { +func (c Config) Context(parent context.Context) *ConfigContext { if len(c) == 0 { panic("gorums: Context called on an empty configuration") } return &ConfigContext{Context: parent, cfg: c} } -// NewConfig returns a new [Configuration] based on the provided nodes and dial options. +// NewConfig returns a new [Config] based on the provided nodes and dial options. // // Example: // @@ -51,7 +51,7 @@ func (c Configuration) Context(parent context.Context) *ConfigContext { // gorums.WithNodeList([]string{"localhost:8080", "localhost:8081", "localhost:8082"}), // gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), // ) -func NewConfig(nodes NodeListOption, opts ...DialOption) (Configuration, error) { +func NewConfig(nodes NodeListOption, opts ...DialOption) (Config, error) { if nodes == nil { return nil, fmt.Errorf("gorums: missing required node list") } @@ -64,8 +64,8 @@ func NewConfig(nodes NodeListOption, opts ...DialOption) (Configuration, error) return cfg, nil } -// Extend returns a new Configuration combining c with new nodes from the provided NodeListOption. -func (c Configuration) Extend(opt NodeListOption) (Configuration, error) { +// Extend returns a new Config combining c with new nodes from the provided NodeListOption. +func (c Config) Extend(opt NodeListOption) (Config, error) { if len(c) == 0 { return nil, fmt.Errorf("gorums: cannot extend empty configuration") } @@ -81,7 +81,7 @@ func (c Configuration) Extend(opt NodeListOption) (Configuration, error) { } // NodeIDs returns a slice of this configuration's Node IDs. -func (c Configuration) NodeIDs() []uint32 { +func (c Config) NodeIDs() []uint32 { ids := make([]uint32, len(c)) for i, node := range c { ids[i] = node.ID() @@ -90,17 +90,17 @@ func (c Configuration) NodeIDs() []uint32 { } // Nodes returns the nodes in this configuration. -func (c Configuration) Nodes() []*Node { +func (c Config) Nodes() []*Node { return c } // Size returns the number of nodes in this configuration. -func (c Configuration) Size() int { +func (c Config) Size() int { return len(c) } // Equal returns true if configurations b and c have the same set of nodes. -func (c Configuration) Equal(b Configuration) bool { +func (c Config) Equal(b Config) bool { if len(c) != len(b) { return false } @@ -113,7 +113,7 @@ func (c Configuration) Equal(b Configuration) bool { } // mgr returns the outboundManager for this configuration's nodes. -func (c Configuration) mgr() *outboundManager { +func (c Config) mgr() *outboundManager { if len(c) == 0 { return nil } @@ -121,7 +121,7 @@ func (c Configuration) mgr() *outboundManager { } // Close closes all node connections managed by this configuration. -func (c Configuration) Close() error { +func (c Config) Close() error { if mgr := c.mgr(); mgr != nil { return mgr.Close() } @@ -129,18 +129,18 @@ func (c Configuration) Close() error { } // nextMsgID returns the next message ID from this client's manager. -func (c Configuration) nextMsgID() uint64 { +func (c Config) nextMsgID() uint64 { return c[0].msgIDGen() } // Contains reports whether c contains a node with the given ID. -func (c Configuration) Contains(id uint32) bool { +func (c Config) Contains(id uint32) bool { return slices.ContainsFunc(c, func(n *Node) bool { return n.id == id }) } -// Add returns a new Configuration containing nodes from c and nodes with the specified IDs. +// Add returns a new Config containing nodes from c and nodes with the specified IDs. // Duplicate IDs and IDs not found in the manager are ignored. -func (c Configuration) Add(ids ...uint32) Configuration { +func (c Config) Add(ids ...uint32) Config { if len(c) == 0 { return nil } @@ -160,9 +160,9 @@ func (c Configuration) Add(ids ...uint32) Configuration { return nodes } -// Union returns a new Configuration containing all nodes from both c and other. +// Union returns a new Config containing all nodes from both c and other. // Duplicate nodes are included only once. -func (c Configuration) Union(other Configuration) Configuration { +func (c Config) Union(other Config) Config { if len(c) == 0 { return slices.Clone(other) } @@ -172,13 +172,13 @@ func (c Configuration) Union(other Configuration) Configuration { return c.Add(other.NodeIDs()...) } -// Remove returns a new Configuration excluding nodes with the specified IDs. -func (c Configuration) Remove(ids ...uint32) Configuration { +// Remove returns a new Config excluding nodes with the specified IDs. +func (c Config) Remove(ids ...uint32) Config { if len(c) == 0 { return nil } removeSet := newSet(ids...) - nodes := make(Configuration, 0, len(c)) + nodes := make(Config, 0, len(c)) for _, n := range c { if !removeSet.contains(n.id) { nodes = append(nodes, n) @@ -187,8 +187,8 @@ func (c Configuration) Remove(ids ...uint32) Configuration { return nodes } -// Difference returns a new Configuration with nodes from c that are not in other. -func (c Configuration) Difference(other Configuration) Configuration { +// Difference returns a new Config with nodes from c that are not in other. +func (c Config) Difference(other Config) Config { if len(c) == 0 { return nil } @@ -198,7 +198,7 @@ func (c Configuration) Difference(other Configuration) Configuration { return c.Remove(other.NodeIDs()...) } -// SortBy returns a new Configuration with nodes ordered by the given comparator. +// SortBy returns a new Config with nodes ordered by the given comparator. // The original configuration is not modified. // // Use this with the built-in node comparator functions [ID], [LastNodeError], @@ -224,7 +224,7 @@ func (c Configuration) Difference(other Configuration) Configuration { // sliced to a smaller subset, e.g., cfg.SortBy(gorums.Latency)[:quorumSize]. // See the "Latency-Based Node Selection" section of the user guide for // guidance on sub-configuration sizing and re-sort frequency. -func (c Configuration) SortBy(cmp func(*Node, *Node) int) Configuration { +func (c Config) SortBy(cmp func(*Node, *Node) int) Config { if len(c) == 0 { return nil } @@ -243,12 +243,12 @@ func (c Configuration) SortBy(cmp func(*Node, *Node) int) Configuration { // sub-configuration. Typical examples: // // // Latency-based top-k subset: -// cfg.Watch(ctx, 5*time.Second, func(c gorums.Configuration) gorums.Configuration { +// cfg.Watch(ctx, 5*time.Second, func(c gorums.Config) gorums.Config { // return c.SortBy(gorums.Latency)[:quorumSize] // }) // // // Skip failed nodes first, then pick fastest: -// cfg.Watch(ctx, 5*time.Second, func(c gorums.Configuration) gorums.Configuration { +// cfg.Watch(ctx, 5*time.Second, func(c gorums.Config) gorums.Config { // return c.WithoutErrors(lastErr).SortBy(gorums.Latency)[:quorumSize] // }) // @@ -257,14 +257,14 @@ func (c Configuration) SortBy(cmp func(*Node, *Node) int) Configuration { // the next tick to re-evaluate. // // The goroutine exits and the channel is closed when ctx is cancelled. -func (c Configuration) Watch(ctx context.Context, interval time.Duration, derive func(Configuration) Configuration) <-chan Configuration { +func (c Config) Watch(ctx context.Context, interval time.Duration, derive func(Config) Config) <-chan Config { if interval <= 0 { panic("gorums: Watch interval must be positive") } if derive == nil { panic("gorums: Watch derive function must be non-nil") } - ch := make(chan Configuration, 1) + ch := make(chan Config, 1) go func() { defer close(ch) current := derive(c) @@ -291,11 +291,11 @@ func (c Configuration) Watch(ctx context.Context, interval time.Duration, derive return ch } -// WithoutErrors returns a new Configuration excluding nodes that failed in the +// WithoutErrors returns a new Config excluding nodes that failed in the // given QuorumCallError. If specific error types are provided, only nodes whose // errors match one of those types (using errors.Is) will be excluded. // If no error types are provided, all failed nodes are excluded. -func (c Configuration) WithoutErrors(err QuorumCallError, errorTypes ...error) Configuration { +func (c Config) WithoutErrors(err QuorumCallError, errorTypes ...error) Config { if len(c) == 0 { return nil } @@ -319,7 +319,7 @@ func (c Configuration) WithoutErrors(err QuorumCallError, errorTypes ...error) C } } // Build configuration with remaining nodes. - nodes := make(Configuration, 0, len(c)) + nodes := make(Config, 0, len(c)) for _, node := range c { if !excludeSet.contains(node.id) { nodes = append(nodes, node) diff --git a/config_opts.go b/config_opts.go index 93f001a8..0cbff232 100644 --- a/config_opts.go +++ b/config_opts.go @@ -10,10 +10,10 @@ import ( // NodeListOption must be implemented by node providers. It is used by both the // Manager (outbound) and by inboundManager (inbound) via newConfig. type NodeListOption interface { - newConfig(nodeRegistry) (Configuration, error) + newConfig(nodeRegistry) (Config, error) } -// nodeRegistry abstracts the node management operations required to build a Configuration. +// nodeRegistry abstracts the node management operations required to build a Config. // Implemented by Manager and inboundManager. type nodeRegistry interface { Nodes() []*Node @@ -34,7 +34,7 @@ func WithNodes[T NodeAddress](nodes map[uint32]T) NodeListOption { type nodeMap[T NodeAddress] map[uint32]T -func (nm nodeMap[T]) newConfig(registry nodeRegistry) (Configuration, error) { +func (nm nodeMap[T]) newConfig(registry nodeRegistry) (Config, error) { if len(nm) == 0 { return nil, fmt.Errorf("gorums: missing required node map") } @@ -59,7 +59,7 @@ func WithNodeList(addrsList []string) NodeListOption { type nodeList []string -func (nl nodeList) newConfig(registry nodeRegistry) (Configuration, error) { +func (nl nodeList) newConfig(registry nodeRegistry) (Config, error) { if len(nl) == 0 { return nil, fmt.Errorf("gorums: missing required node addresses") } @@ -74,14 +74,14 @@ func (nl nodeList) newConfig(registry nodeRegistry) (Configuration, error) { return builder.configuration(), nil } -// nodeBuilder helps construct a Configuration while tracking addresses to prevent duplicates. +// nodeBuilder helps construct a Config while tracking addresses to prevent duplicates. // It encapsulates the common logic shared between WithNodes and WithNodeList. type nodeBuilder struct { registry nodeRegistry addrToID map[string]uint32 // normalized address -> node ID idToNode map[uint32]*Node // existing node ID -> node maxID uint32 // maximum existing node ID - nodes Configuration + nodes Config } // newNodeBuilder creates a new nodeBuilder initialized with existing nodes from the registry. @@ -101,7 +101,7 @@ func newNodeBuilder(registry nodeRegistry, capacity int) *nodeBuilder { addrToID: addrToID, idToNode: idToNode, maxID: maxID, - nodes: make(Configuration, 0, capacity), + nodes: make(Config, 0, capacity), } } @@ -138,8 +138,8 @@ func (b *nodeBuilder) add(id uint32, addr string) error { return nil } -// configuration returns the built Configuration, sorted by ID. -func (b *nodeBuilder) configuration() Configuration { +// configuration returns the built Config, sorted by ID. +func (b *nodeBuilder) configuration() Config { slices.SortFunc(b.nodes, ID) return b.nodes } diff --git a/config_test.go b/config_test.go index 8ea5a587..a7f508e3 100644 --- a/config_test.go +++ b/config_test.go @@ -123,7 +123,7 @@ func TestNewConfig(t *testing.T) { } func TestEmptyConfiguration(t *testing.T) { - var empty gorums.Configuration + var empty gorums.Config populated, err := gorums.NewConfig(gorums.WithNodeList(nodes), gorumstest.InsecureDialOptions(t)) if err != nil { @@ -174,7 +174,7 @@ func TestEmptyConfiguration(t *testing.T) { }) t.Run("Equal", func(t *testing.T) { - var otherEmpty gorums.Configuration + var otherEmpty gorums.Config if !empty.Equal(otherEmpty) { t.Fatal("empty.Equal(otherEmpty) = false, want true") } @@ -202,7 +202,7 @@ func TestEmptyConfiguration(t *testing.T) { }) t.Run("UnionWithEmptyNil", func(t *testing.T) { - var otherEmpty gorums.Configuration + var otherEmpty gorums.Config if got := empty.Union(otherEmpty); got != nil { t.Fatalf("empty.Union(otherEmpty) = %v, want nil", got) } @@ -292,7 +292,7 @@ func TestConfigurationSortBy(t *testing.T) { }) t.Run("Empty/ReturnsNil", func(t *testing.T) { - var empty gorums.Configuration + var empty gorums.Config if got := empty.SortBy(gorums.ID); got != nil { t.Fatalf("empty.SortBy(ID) = %v, want nil", got) } @@ -627,7 +627,7 @@ func TestConfigurationImmutability(t *testing.T) { t.Cleanup(gorumstest.Closer(t, c1)) // Test Union with empty returns a clone, not the original - var emptyConfig gorums.Configuration + var emptyConfig gorums.Config c2 := c1.Union(emptyConfig) if !c1.Equal(c2) { t.Errorf("c1.Equal(c2) = false, want true") diff --git a/doc/dev-guide.md b/doc/dev-guide.md index 45042aa1..f399700a 100644 --- a/doc/dev-guide.md +++ b/doc/dev-guide.md @@ -23,7 +23,7 @@ That block currently consists of four type aliases: ```go type ( - Configuration = gorums.Configuration + Config = gorums.Config Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext diff --git a/doc/migration.md b/doc/migration.md index 32881c22..01a9a117 100644 --- a/doc/migration.md +++ b/doc/migration.md @@ -41,10 +41,10 @@ func (qs *QSpec) ReadQF(_ *ReadRequest, replies map[uint32]*State) (*State, bool return newestState(replies), true } -// Configuration created with QuorumSpec +// Config created with QuorumSpec cfg, _ := mgr.NewConfiguration(&QSpec{2}, gorums.WithNodeList(addrs)) -// Method calls on Configuration +// Method calls on Config reply, err := cfg.Read(ctx, &ReadRequest{}) ``` @@ -70,7 +70,7 @@ func newestValue(responses *gorums.Responses[*ReadResponse]) (*ReadResponse, err return newest, nil } -// Configuration created without QuorumSpec +// Config created without QuorumSpec config, _ := gorums.NewConfiguration(mgr, gorums.WithNodeList(addrs)) // Generic function with ConfigContext @@ -96,11 +96,11 @@ make genproto This generates new `*_gorums.pb.go` files with: -- Type aliases for `Manager`, `Configuration`, `Node` +- Type aliases for `Manager`, `Config`, `Node` - Generic functions for quorum calls (e.g., `ReadQC`, `WriteQC`) - Terminal methods on `*gorums.Responses[T]` -### Step 2: Update Configuration Creation +### Step 2: Update Config Creation Remove QuorumSpec from configuration creation. @@ -184,7 +184,7 @@ func newestState(responses *gorums.Responses[*State]) (*State, error) { ### Step 4: Update Call Sites -Change from Configuration methods to generic functions with ConfigContext. +Change from Config methods to generic functions with ConfigContext. **Before:** @@ -584,9 +584,9 @@ replies := responses.Results().IgnoreErrors().CollectAll() // map[uint32]*Proto result, err := CustomAggregationQF(replies) // Returns *CustomType ``` -## Configuration Manipulation +## Config Manipulation -Configuration manipulation APIs remain largely unchanged: +Config manipulation APIs remain largely unchanged: ```go // Combine configurations @@ -832,7 +832,7 @@ func firstValid(responses *gorums.Responses[*State]) (*State, error) { ### "Cannot use cfg.Read: undefined" -**Problem:** Configuration no longer has RPC methods. +**Problem:** Config no longer has RPC methods. **Solution:** Use generic functions with ConfigContext: diff --git a/doc/user-guide.md b/doc/user-guide.md index 5aec0e63..592c5d5d 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -175,7 +175,7 @@ message WriteRequest { For the `unicast` and `multicast` call types, the response message type will be unused by Gorums. -> **Reserved message names:** The following names are reserved by Gorums and cannot be used as proto message type names in your `.proto` files: `Configuration`, `Node`, `NodeContext`, `ConfigContext`. +> **Reserved message names:** The following names are reserved by Gorums and cannot be used as proto message type names in your `.proto` files: `Config`, `Node`, `NodeContext`, `ConfigContext`. > Using any of these names will cause a compile error in the generated code because Gorums injects type aliases with these names into every generated `_gorums.pb.go` file. ### Compiling the Service Definition @@ -329,7 +329,7 @@ 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 a `Configuration` using `gorums.NewConfig`. +The first thing we need to do is to create a `Config` using `gorums.NewConfig`. `NewConfig` establishes connections to the given nodes and returns a configuration ready for making RPC calls. @@ -369,7 +369,7 @@ func ExampleStorageClient() { 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. +The `Config` 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: @@ -430,7 +430,7 @@ Each terminal method blocks until the threshold is met or the context is cancele ### Using Terminal Methods ```go -func ExampleTerminalMethods(config *Configuration) { +func ExampleTerminalMethods(config *Config) { ctx := context.Background() cfgCtx := config.Context(ctx) @@ -1115,7 +1115,7 @@ func NoFooAllowedInterceptor[T interface{ GetKey() string }](ctx gorums.ServerCt } ``` -## Server Configuration Callbacks +## Server Config Callbacks Two server options expose hooks that fire at connection or configuration change time. Both are passed to `gorums.NewServer` as `ServerOption` values. @@ -1175,7 +1175,7 @@ config, err := gorums.NewConfig( **Signature:** ```go -gorums.WithPeerChange(func(cfg gorums.Configuration) { ... }) +gorums.WithPeerChange(func(cfg gorums.Config) { ... }) ``` **When it runs:** after every change to the connected-peer configuration. @@ -1200,7 +1200,7 @@ ready := make(chan struct{}, 1) gorumsSrv := gorums.NewServer( gorums.WithPeers(myNodeID, gorums.WithNodeList(peerAddrs), dialOpts...), - gorums.WithPeerChange(func(cfg gorums.Configuration) { + gorums.WithPeerChange(func(cfg gorums.Config) { if len(cfg) >= quorumSize { select { case ready <- struct{}{}: @@ -1217,7 +1217,7 @@ log.Println("quorum ready, starting to serve") The self-node is always present in `cfg`, so a three-node cluster (`quorumSize = 2`) will fire the signal as soon as a single remote peer connects. -## Waiting for Configuration +## Waiting for Config `Server.WaitForPeers` and `Server.WaitForClients` block until a condition on the configuration is satisfied, or until the context is cancelled or the server is stopped. They replace the need to poll `ConnectedPeers()` in a loop and eliminate the latency and CPU overhead of polling. @@ -1226,7 +1226,7 @@ They replace the need to poll `ConnectedPeers()` in a loop and eliminate the lat // Block until all three known peers are connected. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() -if err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { +if err := srv.WaitForPeers(ctx, func(cfg gorums.Config) bool { return cfg.Size() == 3 }); err != nil { log.Fatal("peers did not connect in time:", err) @@ -1241,7 +1241,7 @@ The condition is checked immediately against the current configuration, so the c Use this when you need a quorum of static cluster members to be present before beginning to serve requests. ```go -err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { +err := srv.WaitForPeers(ctx, func(cfg gorums.Config) bool { return cfg.Size() >= quorumSize }) ``` @@ -1252,7 +1252,7 @@ err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { Use this when a server should not proceed until a minimum number of clients have registered. ```go -err := srv.WaitForClients(ctx, func(cfg gorums.Configuration) bool { +err := srv.WaitForClients(ctx, func(cfg gorums.Config) bool { return cfg.Size() >= expectedClients }) ``` @@ -1300,7 +1300,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(config *gorums.Configuration, req *ReadRequest) { +func handleQuorumCall(config *gorums.Config, req *ReadRequest) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -1473,7 +1473,7 @@ sub-configurations, and what to watch out for when doing so. ### The Latency Comparator -`gorums.Latency` is a comparator function compatible with `slices.SortFunc` and `Configuration.SortBy`. +`gorums.Latency` is a comparator function compatible with `slices.SortFunc` and `Config.SortBy`. The comparator orders nodes ascending by their current latency estimates; nodes without any measurements (freshly created, never sent traffic) are sorted last. @@ -1497,7 +1497,7 @@ sorted := cfg.SortBy(func(a, b *gorums.Node) int { }) ``` -### Using a Smaller Fast Configuration +### Using a Smaller Fast Config The most practical use of latency-based selection is reducing the quorum size to the fastest subset of nodes. Sending to fewer nodes lowers tail latency without weakening correctness, as long as the subset still meets your quorum threshold. @@ -1546,10 +1546,10 @@ As a rule of thumb: * **After a topology change** (node added or removed), derive the sub-configuration from the new full configuration rather than sorting an outdated one. -A simple periodic refresh pattern using `Configuration.Watch`: +A simple periodic refresh pattern using `Config.Watch`: ```go -updates := allNodesCfg.Watch(ctx, 5*time.Second, func(c gorums.Configuration) gorums.Configuration { +updates := allNodesCfg.Watch(ctx, 5*time.Second, func(c gorums.Config) gorums.Config { return c.SortBy(gorums.Latency)[:quorumSize] }) fastCfg := <-updates // initial snapshot, available before the first tick @@ -1583,7 +1583,7 @@ var lastQCErr gorums.QuorumCallError // zero value excludes no nodes // mu.Lock(); lastQCErr = qcErr; mu.Unlock() // } -updates := allNodesCfg.Watch(ctx, 5*time.Second, func(c gorums.Configuration) gorums.Configuration { +updates := allNodesCfg.Watch(ctx, 5*time.Second, func(c gorums.Config) gorums.Config { mu.Lock() qcErr := lastQCErr mu.Unlock() @@ -1753,7 +1753,7 @@ The `nread` and `nwrite` commands trigger server-side nested quorum calls and ne A server handler (the server method itself) can act as a client and issue its own quorum calls to other nodes. These are called *nested quorum calls*, because one quorum call triggers another from inside the server handler. -`ServerCtx.PeerConfig()` returns the `Configuration` of the peers the server was configured with via `gorums.WithPeers`. +`ServerCtx.PeerConfig()` returns the `Config` of the peers the server was configured with via `gorums.WithPeers`. This makes it straightforward for a handler to fan out a sub-request to the rest of the cluster. It is the full peer set, not the reachable subset, so a quorum size derived from it inside a handler does not shift as peers connect and disconnect; use `ctx.ConnectedPeers()` to observe reachability. @@ -1843,7 +1843,7 @@ The client sees a single quorum call, but internally each receiving node fans ou ## Reverse Direction Calls with ServerCtx.ConnectedClients -`ServerCtx.ConnectedClients()` returns a `Configuration` of all currently connected *client peers* — nodes that connected to this server dynamically, rather than being pre-configured with `WithPeers`. +`ServerCtx.ConnectedClients()` returns a `Config` of all currently connected *client peers* — nodes that connected to this server dynamically, rather than being pre-configured with `WithPeers`. A handler can use this configuration to make outbound calls back towards those clients, reversing the usual direction of communication. This pattern is particularly useful when clients are behind a firewall and cannot accept inbound connections. diff --git a/examples/storage/repl.go b/examples/storage/repl.go index 68b6f84c..802c7248 100644 --- a/examples/storage/repl.go +++ b/examples/storage/repl.go @@ -59,11 +59,11 @@ The command performs the write quorum call on node 0 and 2 const delayOutput = 200 * time.Millisecond type repl struct { - cfg pb.Configuration + cfg pb.Config term *term.Terminal } -func newRepl(cfg pb.Configuration) *repl { +func newRepl(cfg pb.Config) *repl { return &repl{ cfg: cfg, term: term.NewTerminal(struct { @@ -92,7 +92,7 @@ func (r repl) ReadLine() (string, error) { // Repl runs an interactive Read-eval-print loop, that allows users to run commands that perform // RPCs and quorum calls using the configuration. -func Repl(defaultCfg pb.Configuration) error { +func Repl(defaultCfg pb.Config) error { r := newRepl(defaultCfg) fmt.Println(help) @@ -313,7 +313,7 @@ func (repl) writeRPC(args []string, node *pb.Node) { fmt.Println("Write OK") } -func (repl) readQC(args []string, config pb.Configuration) { +func (repl) readQC(args []string, config pb.Config) { if len(args) < 1 { fmt.Println("Read requires a key to read.") return @@ -334,7 +334,7 @@ func (repl) readQC(args []string, config pb.Configuration) { fmt.Printf("%s = %s\n", args[0], resp.GetValue()) } -func (repl) creadQC(args []string, config pb.Configuration) { +func (repl) creadQC(args []string, config pb.Config) { if len(args) < 1 { fmt.Println("Correctable Read requires a key to read.") return @@ -365,7 +365,7 @@ func (repl) creadQC(args []string, config pb.Configuration) { fmt.Println("Correctable read finished") } -func (repl) writeQC(args []string, config pb.Configuration) { +func (repl) writeQC(args []string, config pb.Config) { if len(args) < 2 { fmt.Println("Write requires a key and a value to write.") return @@ -388,7 +388,7 @@ func (repl) writeQC(args []string, config pb.Configuration) { fmt.Println("Write OK") } -func (repl) readNestedQC(args []string, config pb.Configuration) { +func (repl) readNestedQC(args []string, config pb.Config) { if len(args) < 1 { fmt.Println("Read requires a key to read.") return @@ -409,7 +409,7 @@ func (repl) readNestedQC(args []string, config pb.Configuration) { fmt.Printf("%s = %s\n", args[0], resp.GetValue()) } -func (repl) writeNestedMulticast(args []string, config pb.Configuration) { +func (repl) writeNestedMulticast(args []string, config pb.Config) { if len(args) < 2 { fmt.Println("Write requires a key and a value to write.") return @@ -432,7 +432,7 @@ func (repl) writeNestedMulticast(args []string, config pb.Configuration) { fmt.Println("Nested write OK") } -func (r repl) parseConfiguration(cfgStr string) (pb.Configuration, error) { +func (r repl) parseConfiguration(cfgStr string) (pb.Config, error) { indices, err := parseIndices(cfgStr, r.cfg.Size()) if err != nil { return nil, err @@ -444,7 +444,7 @@ func (r repl) parseConfiguration(cfgStr string) (pb.Configuration, error) { nodes = append(nodes, cfgNodes[i]) } slices.SortFunc(nodes, gorums.ID) - return pb.Configuration(nodes), nil + return pb.Config(nodes), nil } func parseIndices(cfgStr string, numNodes int) (indices []int, err error) { diff --git a/examples/storage/server.go b/examples/storage/server.go index 32e5b502..076b09e8 100644 --- a/examples/storage/server.go +++ b/examples/storage/server.go @@ -46,7 +46,7 @@ func runServer(address string, peers []string, srvOpt gorums.ServerOption) error ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - if err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + if err := srv.WaitForPeers(ctx, func(cfg gorums.Config) bool { return cfg.Size() == len(peers) }); err != nil { return fmt.Errorf("peers did not connect in time: %w", err) @@ -82,7 +82,7 @@ func runLocalCluster(srvOpts gorums.ServerOption) ([]string, func(), error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() for _, srv := range servers { - if err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + if err := srv.WaitForPeers(ctx, func(cfg gorums.Config) bool { return cfg.Size() == len(servers) }); err != nil { stop() diff --git a/gorumstest/gorumstest.go b/gorumstest/gorumstest.go index a90719af..590a4088 100644 --- a/gorumstest/gorumstest.go +++ b/gorumstest/gorumstest.go @@ -123,7 +123,7 @@ func startServers(t testing.TB, numServers int, srvFn func(i int) gorums.ServerI // // This is the recommended way to set up tests that need both servers and a configuration. // It ensures proper cleanup and detects goroutine leaks. -func Config(t testing.TB, numServers int, srvFn func(i int) gorums.ServerIface, opts ...Option) gorums.Configuration { +func Config(t testing.TB, numServers int, srvFn func(i int) gorums.ServerIface, opts ...Option) gorums.Config { t.Helper() testOpts := extractTestOptions(opts) @@ -160,12 +160,12 @@ func Config(t testing.TB, numServers int, srvFn func(i int) gorums.ServerIface, return cfg } -// NoDialedConfig returns a [gorums.Configuration] over addrs whose nodes are +// NoDialedConfig returns a [gorums.Config] over addrs whose nodes are // never actually dialed: gRPC connections are established lazily on the first // RPC, so tests that only need a valid configuration to construct calls, // without ever completing one, don't need a running server behind it. If addrs // is empty, a single unreachable sentinel address is used. -func NoDialedConfig(t testing.TB, addrs ...string) gorums.Configuration { +func NoDialedConfig(t testing.TB, addrs ...string) gorums.Config { t.Helper() if len(addrs) == 0 { addrs = []string{"127.0.0.1:65535"} @@ -229,7 +229,7 @@ func Servers(t testing.TB, numServers int, srvFn func(i int) gorums.ServerIface) // LocalServers returns n started Gorums servers forming a symmetric peer // group on random localhost ports (see [gorums.NewLocalServers]). Each -// server auto-creates a peer [gorums.Configuration] over the group, accessible +// server auto-creates a peer [gorums.Config] over the group, accessible // via [gorums.Server.PeerConfig]. The servers are automatically stopped // when the test finishes via t.Cleanup. Any [gorums.ServerOption]s are // applied to every server. diff --git a/handler.go b/handler.go index f1e67ff7..6d449eee 100644 --- a/handler.go +++ b/handler.go @@ -72,12 +72,12 @@ func (ctx *ServerCtx) SendMessage(out *Message) { } } -// PeerConfig returns the [Configuration] of the peers the server was configured +// PeerConfig returns the [Config] of the peers the server was configured // with via [WithPeers], or nil if it was not used. It is the full peer set, not // the currently reachable subset, so quorum sizes derived from it inside a // handler do not shift as peers connect and disconnect. Use // [ServerCtx.ConnectedPeers] to observe reachability. -func (ctx *ServerCtx) PeerConfig() Configuration { +func (ctx *ServerCtx) PeerConfig() Config { if ctx.srv == nil { return nil } @@ -86,19 +86,19 @@ func (ctx *ServerCtx) PeerConfig() Configuration { // ConnectedPeers returns the currently reachable subset of // [ServerCtx.PeerConfig]; see [Server.ConnectedPeers]. -func (ctx *ServerCtx) ConnectedPeers() Configuration { +func (ctx *ServerCtx) ConnectedPeers() Config { if ctx.srv == nil { return nil } return ctx.srv.ConnectedPeers() } -// ConnectedClients returns a [Configuration] of all connected clients capable of +// ConnectedClients returns a [Config] of all connected clients capable of // receiving reverse-direction calls from the server. -// An empty (non-nil) Configuration is returned if no client peers are connected. +// An empty (non-nil) Config is returned if no client peers are connected. // The returned slice is replaced atomically on each connect/disconnect; // thus, retaining a reference to an old configuration is safe. -func (ctx *ServerCtx) ConnectedClients() Configuration { +func (ctx *ServerCtx) ConnectedClients() Config { if ctx.srv == nil { return nil } diff --git a/inbound_manager.go b/inbound_manager.go index eae7fbf5..42788830 100644 --- a/inbound_manager.go +++ b/inbound_manager.go @@ -57,7 +57,7 @@ func metadataWithNodeID(id uint32) metadata.MD { // inboundManager manages server-side awareness of connected peers. It is // configured at construction time with a fixed set of known peers, registers -// them as they connect, and maintains an auto-updated [Configuration] that +// them as they connect, and maintains an auto-updated [Config] that // can be used for server-initiated quorum calls, multicast, and other call types. // // Clients that specify node ID 0 in their metadata are assumed to be capable @@ -72,14 +72,14 @@ type inboundManager struct { myID uint32 // this server's own NodeID; always present in inboundCfg knownNodes map[uint32]*Node // pre-created configured peers, including self when configured clientNodes map[uint32]*Node // dynamically assigned peer-capable clients - peerConfig Configuration // the server's peer Configuration; set once by setPeerConfig - config Configuration // auto-updated connectivity-filtered subset of peerConfig, sorted by ID - inboundCfg Configuration // auto-updated slice of known peers with an inbound stream, sorted by ID - clientConfig Configuration // auto-updated slice of client peers, sorted by ID + peerConfig Config // the server's peer Config; set once by setPeerConfig + config Config // auto-updated connectivity-filtered subset of peerConfig, sorted by ID + inboundCfg Config // auto-updated slice of known peers with an inbound stream, sorted by ID + clientConfig Config // auto-updated slice of client peers, sorted by ID nextMsgID atomic.Uint64 // counter for server-initiated message IDs sendBufferSize uint // send buffer size for inbound channels handler stream.RequestHandler // handler for dispatching incoming requests on all inbound nodes - onConfigChange func(Configuration) // optional; called after each known-peer config change + onConfigChange func(Config) // optional; called after each known-peer config change nextClientID uint64 // next candidate ID for a client peer; uint64 represents exhaustion configCh chan struct{} // closed and replaced on each config/clientConfig change; protected by mu stopCh chan struct{} // closed on shutdown to unblock waiters; never replaced @@ -101,7 +101,7 @@ const clientIDStart = 1 << 20 // installed on the self-node (if present) to enable in-process dispatch without // a network round-trip. Panics on configuration errors (invalid addresses, // duplicate nodes, etc.) -func newInboundManager(myID uint32, opt NodeListOption, sendBuffer uint, onConfigChange func(Configuration), handler stream.RequestHandler) *inboundManager { +func newInboundManager(myID uint32, opt NodeListOption, sendBuffer uint, onConfigChange func(Config), handler stream.RequestHandler) *inboundManager { im := &inboundManager{ myID: myID, knownNodes: make(map[uint32]*Node), @@ -134,10 +134,10 @@ func (im *inboundManager) Nodes() []*Node { }) } -// ConnectedPeers returns the current connected-peer [Configuration]; see +// ConnectedPeers returns the current connected-peer [Config]; see // [Server.ConnectedPeers]. Before setPeerConfig installs a peer configuration, // it falls back to the inbound view. -func (im *inboundManager) ConnectedPeers() Configuration { +func (im *inboundManager) ConnectedPeers() Config { if im == nil { return nil } @@ -146,11 +146,11 @@ func (im *inboundManager) ConnectedPeers() Configuration { return im.config } -// setPeerConfig installs the server's peer [Configuration], from which the +// setPeerConfig installs the server's peer [Config], from which the // connected-peer view is derived. It is called once by [NewServer] after the // peer configuration is built; stream-state changes observed before that are // picked up by the rebuild here. -func (im *inboundManager) setPeerConfig(cfg Configuration) { +func (im *inboundManager) setPeerConfig(cfg Config) { im.mu.Lock() defer im.mu.Unlock() im.peerConfig = cfg @@ -170,18 +170,18 @@ func (im *inboundManager) peerStreamChanged(uint32, bool) { // inboundPeers returns the known peers with an inbound stream open to this // server, plus the local node. Test-only: production code observes // connectivity through ConnectedPeers. -func (im *inboundManager) inboundPeers() Configuration { +func (im *inboundManager) inboundPeers() Config { im.mu.RLock() defer im.mu.RUnlock() return im.inboundCfg } -// ConnectedClients returns a [Configuration] of all connected clients capable of +// ConnectedClients returns a [Config] of all connected clients capable of // receiving reverse-direction calls from the server. -// An empty (non-nil) Configuration is returned if no client peers are connected. +// An empty (non-nil) Config is returned if no client peers are connected. // The returned slice is replaced atomically on each connect/disconnect; // thus, retaining a reference to an old configuration is safe. -func (im *inboundManager) ConnectedClients() Configuration { +func (im *inboundManager) ConnectedClients() Config { if im == nil { return nil } @@ -350,13 +350,13 @@ func (im *inboundManager) nextAvailableClientID() (uint32, error) { // configuration is installed it falls back to the inbound view. // Callers must hold the lock. func (im *inboundManager) rebuildConfig() { - inboundCfg := make(Configuration, 0, len(im.knownNodes)) + inboundCfg := make(Config, 0, len(im.knownNodes)) for id, node := range im.knownNodes { if id == im.myID || node.channel.Load() != nil { inboundCfg = append(inboundCfg, node) } } - clientCfg := make(Configuration, 0, len(im.clientNodes)) + clientCfg := make(Config, 0, len(im.clientNodes)) for _, node := range im.clientNodes { if node.channel.Load() != nil { clientCfg = append(clientCfg, node) @@ -369,7 +369,7 @@ func (im *inboundManager) rebuildConfig() { cfg := inboundCfg if im.peerConfig != nil { - cfg = make(Configuration, 0, len(im.peerConfig)) + cfg = make(Config, 0, len(im.peerConfig)) for _, node := range im.peerConfig { if node.ID() == im.myID || node.isUp() { cfg = append(cfg, node) @@ -418,10 +418,10 @@ func (im *inboundManager) waitForConfig(ctx context.Context, cond func() bool) e } // WaitForPeers blocks until cond returns true for the current connected-peer -// [Configuration], or until ctx is cancelled or the server is stopped. +// [Config], or until ctx is cancelled or the server is stopped. // The cond function receives the current connected-peer configuration and must // not acquire any additional locks. -func (im *inboundManager) WaitForPeers(ctx context.Context, cond func(Configuration) bool) error { +func (im *inboundManager) WaitForPeers(ctx context.Context, cond func(Config) bool) error { return im.waitForConfig(ctx, func() bool { return cond(im.config) }) @@ -429,17 +429,17 @@ func (im *inboundManager) WaitForPeers(ctx context.Context, cond func(Configurat // waitForInbound blocks until cond returns true for the current inbound view. // Test-only counterpart of WaitForPeers. -func (im *inboundManager) waitForInbound(ctx context.Context, cond func(Configuration) bool) error { +func (im *inboundManager) waitForInbound(ctx context.Context, cond func(Config) bool) error { return im.waitForConfig(ctx, func() bool { return cond(im.inboundCfg) }) } // WaitForClients blocks until cond returns true for the current client-peer -// [Configuration], or until ctx is cancelled or the server is stopped. +// [Config], or until ctx is cancelled or the server is stopped. // The cond function receives the current client-peer configuration and must not // acquire any additional locks. -func (im *inboundManager) WaitForClients(ctx context.Context, cond func(Configuration) bool) error { +func (im *inboundManager) WaitForClients(ctx context.Context, cond func(Config) bool) error { return im.waitForConfig(ctx, func() bool { return cond(im.clientConfig) }) diff --git a/inbound_manager_test.go b/inbound_manager_test.go index 3dad62be..e49b100e 100644 --- a/inbound_manager_test.go +++ b/inbound_manager_test.go @@ -252,14 +252,14 @@ func TestNodeID(t *testing.T) { // checkIDs asserts that cfg.NodeIDs() equals wantIDs, reporting label in any // failure message. -func checkIDs(t *testing.T, cfg Configuration, wantIDs []uint32, label string) { +func checkIDs(t *testing.T, cfg Config, wantIDs []uint32, label string) { t.Helper() if got := cfg.NodeIDs(); !slices.Equal(got, wantIDs) { t.Errorf("%s: config IDs = %v; want %v", label, got, wantIDs) } } -// TestAcceptPeerUpdatesConfig checks that the Configuration is correctly +// TestAcceptPeerUpdatesConfig checks that the Config is correctly // updated through sequences of peer connections and disconnections // (via AcceptPeer and its returned cleanup function), including out-of-order // connection, stream breakage followed by reconnect, and idempotent cleanups. @@ -469,7 +469,7 @@ func TestOnConfigChangeCallbackFiringOnConstruction(t *testing.T) { 1: {"127.0.0.1:9081"}, 2: {"127.0.0.1:9082"}, 3: {"127.0.0.1:9083"}, - }), 0, func(cfg Configuration) { + }), 0, func(cfg Config) { calls = append(calls, slices.Clone(cfg.NodeIDs())) }, nil) @@ -490,7 +490,7 @@ func TestOnConfigChangeCallbackPeerConnectDisconnect(t *testing.T) { 1: {"127.0.0.1:9081"}, 2: {"127.0.0.1:9082"}, 3: {"127.0.0.1:9083"}, - }), 0, func(cfg Configuration) { + }), 0, func(cfg Config) { snapshots = append(snapshots, slices.Clone(cfg.NodeIDs())) }, nil) @@ -525,7 +525,7 @@ func TestOnConfigChangeCallbackMultiplePeers(t *testing.T) { 1: {"127.0.0.1:9081"}, 2: {"127.0.0.1:9082"}, 3: {"127.0.0.1:9083"}, - }), 0, func(cfg Configuration) { + }), 0, func(cfg Config) { snapshots = append(snapshots, slices.Clone(cfg.NodeIDs())) }, nil) @@ -566,7 +566,7 @@ func TestOnConfigChangeCallbackIdempotentCleanup(t *testing.T) { im := newInboundManager(1, WithNodes(map[uint32]testNode{ 1: {"127.0.0.1:9081"}, 2: {"127.0.0.1:9082"}, - }), 0, func(_ Configuration) { + }), 0, func(_ Config) { callCount++ }, nil) @@ -589,8 +589,8 @@ func TestOnConfigChangeCallbackIdempotentCleanup(t *testing.T) { } // mustWaitForInbound blocks until cond returns true for srv's inbound peer -// Configuration, or fails the test after a 2-second timeout. -func mustWaitForInbound(t *testing.T, srv *Server, cond func(Configuration) bool) { +// Config, or fails the test after a 2-second timeout. +func mustWaitForInbound(t *testing.T, srv *Server, cond func(Config) bool) { t.Helper() ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() @@ -600,8 +600,8 @@ func mustWaitForInbound(t *testing.T, srv *Server, cond func(Configuration) bool } // mustWaitForClients blocks until cond returns true for srv's client-peer -// Configuration, or fails the test after a 2-second timeout. -func mustWaitForClients(t *testing.T, srv *Server, cond func(Configuration) bool) { +// Config, or fails the test after a 2-second timeout. +func mustWaitForClients(t *testing.T, srv *Server, cond func(Config) bool) { t.Helper() ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() @@ -622,8 +622,8 @@ func testPeerServer(t *testing.T) (*Server, []string) { return srv, addrs } -func equalNodeIDs(ids []uint32) func(Configuration) bool { - return func(cfg Configuration) bool { +func equalNodeIDs(ids []uint32) func(Config) bool { + return func(cfg Config) bool { return slices.Equal(cfg.NodeIDs(), ids) } } @@ -636,11 +636,11 @@ func peerNodes() NodeListOption { }) } -// connectAsPeer creates a Configuration that identifies itself as peerID by sending +// connectAsPeer creates a Config 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 +// Config 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) Configuration { +func connectAsPeer(t *testing.T, peerID uint32, addrs []string) Config { t.Helper() peerMD := metadata.Pairs(gorumsNodeIDKey, strconv.FormatUint(uint64(peerID), 10)) cfg, err := NewConfig(WithNodeList(addrs), testDialOptions(t), WithMetadata(peerMD)) @@ -789,11 +789,11 @@ func testClientServer(t *testing.T) (*Server, []string) { return srv, addrs } -// connectAsPeerClient creates a Configuration that advertises back-channel +// connectAsPeerClient creates a Config 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) Configuration { +func connectAsPeerClient(t *testing.T, addrs []string) Config { t.Helper() cfg, err := NewConfig(WithNodeList(addrs), testDialOptions(t), WithBackChannel(NewServer())) if err != nil { @@ -814,7 +814,7 @@ func TestClientConfigConnects(t *testing.T) { connectAsPeerClient(t, addrs) // Client peer should appear with auto-assigned ID >= clientIDStart. - mustWaitForClients(t, srv, func(cfg Configuration) bool { return len(cfg) > 0 }) + mustWaitForClients(t, srv, func(cfg Config) bool { return len(cfg) > 0 }) cfg := srv.ConnectedClients() if len(cfg) != 1 { t.Fatalf("ClientConfig has %d nodes; want 1", len(cfg)) @@ -832,7 +832,7 @@ func TestClientConfigDisconnects(t *testing.T) { cfg := connectAsPeerClient(t, addrs) // Wait for the client peer to appear. - mustWaitForClients(t, srv, func(cfg Configuration) bool { return len(cfg) > 0 }) + mustWaitForClients(t, srv, func(cfg Config) bool { return len(cfg) > 0 }) if len(srv.ConnectedClients()) != 1 { t.Fatalf("ClientConfig has %d nodes; want 1", len(srv.ConnectedClients())) } @@ -843,7 +843,7 @@ func TestClientConfigDisconnects(t *testing.T) { } // Wait for config to become empty. - mustWaitForClients(t, srv, func(cfg Configuration) bool { return len(cfg) == 0 }) + mustWaitForClients(t, srv, func(cfg Config) bool { return len(cfg) == 0 }) checkIDs(t, srv.ConnectedClients(), []uint32{}, "after disconnect") } @@ -863,7 +863,7 @@ func TestClientConfigMixedMode(t *testing.T) { connectAsPeerClient(t, addrs) // Wait for 1 dynamic node. - mustWaitForClients(t, srv, func(cfg Configuration) bool { return len(cfg) == 1 }) + mustWaitForClients(t, srv, func(cfg Config) bool { return len(cfg) == 1 }) dynCfg := srv.ConnectedClients() if len(dynCfg) != 1 { t.Fatalf("ClientConfig has %d nodes; want 1", len(dynCfg)) @@ -910,7 +910,7 @@ func TestClientConfigServerCallsClient(t *testing.T) { t.Cleanup(testCloser(t, clientConfig)) // Wait for the client to appear in the server's ClientConfig. - mustWaitForClients(t, srv, func(cfg Configuration) bool { return len(cfg) > 0 }) + mustWaitForClients(t, srv, func(cfg Config) bool { return len(cfg) > 0 }) // Trigger: client multicasts TestMethod to the server; server fans it back via ClientConfig. ctx := testTimeoutContext(t, 2*time.Second) diff --git a/internal/testprotos/failing/reservednames/reserved.proto b/internal/testprotos/failing/reservednames/reserved.proto index 13399cf5..78b2f31e 100644 --- a/internal/testprotos/failing/reservednames/reserved.proto +++ b/internal/testprotos/failing/reservednames/reserved.proto @@ -11,13 +11,13 @@ option features.field_presence = IMPLICIT; import "gorums.proto"; service Reserved { - rpc ConfTest(Configuration) returns (Manager) {} - rpc QuorumCall(Configuration) returns (Manager) { + rpc ConfTest(Config) returns (Manager) {} + rpc QuorumCall(Config) returns (Manager) { option (gorums.quorumcall) = true; } } -message Configuration { +message Config { string Conf = 1; } message Manager { diff --git a/internal/tests/config/config.proto b/internal/tests/config/config.proto index 10cdd817..e62fe67d 100644 --- a/internal/tests/config/config.proto +++ b/internal/tests/config/config.proto @@ -8,7 +8,7 @@ option features.field_presence = IMPLICIT; import "gorums.proto"; service ConfigTest { - rpc Config(Request) returns (Response) { + rpc Read(Request) returns (Response) { option (gorums.quorumcall) = true; } } diff --git a/internal/tests/config/config_test.go b/internal/tests/config/config_test.go index 04993146..11656c37 100644 --- a/internal/tests/config/config_test.go +++ b/internal/tests/config/config_test.go @@ -11,7 +11,7 @@ import ( type cfgSrv struct{} -func (cfgSrv) Config(_ gorums.ServerCtx, req *Request) (resp *Response, err error) { +func (cfgSrv) Read(_ gorums.ServerCtx, req *Request) (resp *Response, err error) { return Response_builder{ Num: req.GetNum(), }.Build(), nil @@ -23,14 +23,14 @@ func serverFn(_ int) gorums.ServerIface { return srv } -// TestConfig creates and combines multiple configurations and invokes the Config RPC +// TestConfig creates and combines multiple configurations and invokes the Read RPC // method on the different configurations created below. func TestConfig(t *testing.T) { - callRPC := func(config Configuration) { + callRPC := func(config Config) { cfgCtx := config.Context(context.Background()) for i := range 5 { // Use the new terminal method API - wait for a majority - resp, err := Config(cfgCtx, + resp, err := Read(cfgCtx, Request_builder{Num: uint64(i)}.Build()).Majority() if err != nil { t.Fatal(err) diff --git a/internal/tests/oneway/oneway_test.go b/internal/tests/oneway/oneway_test.go index 38845089..94b225a6 100644 --- a/internal/tests/oneway/oneway_test.go +++ b/internal/tests/oneway/oneway_test.go @@ -40,7 +40,7 @@ func (s *onewaySrv) Multicast(_ gorums.ServerCtx, r *oneway.Request) { // setupWithNodeMap sets up servers and configuration with sequential node IDs // (1, 2, 3, ...) matching the server array indices. This is needed for tests like // TestMulticastPerNode that verify per-node message transformations based on node ID. -func setupWithNodeMap(t testing.TB, cfgSize int) (cfg oneway.Configuration, srvs []*onewaySrv) { +func setupWithNodeMap(t testing.TB, cfgSize int) (cfg oneway.Config, srvs []*onewaySrv) { t.Helper() srvs = make([]*onewaySrv, cfgSize) for i := range cfgSize { diff --git a/node.go b/node.go index 0581bd14..970510bb 100644 --- a/node.go +++ b/node.go @@ -308,21 +308,21 @@ func (n *Node) LastErr() error { // - A step-change in latency takes several round trips to settle because // each new sample contributes only 20% of the new value. // -// Use the [Latency] comparator with [Configuration.SortBy] to order nodes +// Use the [Latency] comparator with [Config.SortBy] to order nodes // by their current observed latency. func (n *Node) Latency() time.Duration { return n.router.Latency() } // ID compares nodes by their identifier in increasing order. -// It is compatible with [slices.SortFunc] and [Configuration.SortBy]. +// It is compatible with [slices.SortFunc] and [Config.SortBy]. var ID = func(a, b *Node) int { return cmp.Compare(a.id, b.id) } // LastNodeError compares nodes by their LastErr() status. // Nodes with no error sort before nodes with an error. -// It is compatible with [slices.SortFunc] and [Configuration.SortBy]. +// It is compatible with [slices.SortFunc] and [Config.SortBy]. var LastNodeError = func(a, b *Node) int { aErr := a.LastErr() bErr := b.LastErr() @@ -338,7 +338,7 @@ var LastNodeError = func(a, b *Node) int { // Latency compares nodes by their current latency estimate in ascending order. // Nodes with no measurement yet (negative latency value) sort after nodes with a -// measurement. It is compatible with [slices.SortFunc] and [Configuration.SortBy]. +// measurement. It is compatible with [slices.SortFunc] and [Config.SortBy]. var Latency = func(a, b *Node) int { la, lb := a.Latency(), b.Latency() // Note: cmp.Compare alone would sort negative sentinel values first diff --git a/node_test.go b/node_test.go index 29381778..4831bed6 100644 --- a/node_test.go +++ b/node_test.go @@ -138,7 +138,7 @@ func TestConfigurationWatch(t *testing.T) { } // allNodes has five nodes; top-3 by ascending latency are 2(10ms), 3(20ms), 1(30ms). - allNodes := Configuration{ + allNodes := Config{ makeNodeWithLatency(1, 30*time.Millisecond), makeNodeWithLatency(2, 10*time.Millisecond), makeNodeWithLatency(3, 20*time.Millisecond), @@ -146,7 +146,7 @@ func TestConfigurationWatch(t *testing.T) { makeNodeWithLatency(5, 50*time.Millisecond), } const quorumSize = 3 - fastTop3 := func(c Configuration) Configuration { return c.SortBy(Latency)[:quorumSize] } + fastTop3 := func(c Config) Config { return c.SortBy(Latency)[:quorumSize] } t.Run("EmitsInitialSnapshot", func(t *testing.T) { // Use a very long interval so only the initial emission fires. @@ -182,8 +182,8 @@ func TestConfigurationWatch(t *testing.T) { n1 := makeNodeWithLatency(1, 10*time.Millisecond) n2 := makeNodeWithLatency(2, 30*time.Millisecond) n3 := makeNodeWithLatency(3, 20*time.Millisecond) - cfg := Configuration{n1, n2, n3} - top2 := func(c Configuration) Configuration { return c.SortBy(Latency)[:2] } + cfg := Config{n1, n2, n3} + top2 := func(c Config) Config { return c.SortBy(Latency)[:2] } const interval = 20 * time.Millisecond updates := cfg.Watch(t.Context(), interval, top2) diff --git a/opts.go b/opts.go index 001827c4..b88e1c2d 100644 --- a/opts.go +++ b/opts.go @@ -93,7 +93,7 @@ func WithMetadata(md metadata.MD) DialOption { // servers it dials. It panics if srv is nil. // // A server that calls its own peers does not need this option: [WithPeers] -// installs the back channel on the peer [Configuration] it builds. +// installs the back channel on the peer [Config] it builds. // // NodeID semantics: // - If srv.NodeID() == 0, the remote treats this connection as an anonymous diff --git a/responses_test.go b/responses_test.go index 70bd23bb..9b1de195 100644 --- a/responses_test.go +++ b/responses_test.go @@ -32,7 +32,7 @@ func makeClientCtx[Req, Resp msg](t *testing.T, numNodes int, responses []NodeRe } close(resultChan) - config := make(Configuration, numNodes) + config := make(Config, numNodes) for i := range numNodes { config[i] = &Node{id: uint32(i + 1)} } diff --git a/server.go b/server.go index 52756812..956cf143 100644 --- a/server.go +++ b/server.go @@ -21,10 +21,10 @@ type serverOptions struct { interceptors []Interceptor // Peer management options myID uint32 - peerNodes NodeListOption // Peers to track as they connect; set by WithPeers. - onConfigChange func(Configuration) // Callback registered via WithPeerChange. - listenAddr string // Listener address recorded by WithAddr; bound by ListenAndServe. - outboundNodes NodeListOption // Nodes this server calls; set by WithPeers. + peerNodes NodeListOption // Peers to track as they connect; set by WithPeers. + onConfigChange func(Config) // Callback registered via WithPeerChange. + listenAddr string // Listener address recorded by WithAddr; bound by ListenAndServe. + outboundNodes NodeListOption // Nodes this server calls; set by WithPeers. outboundDialOpts []DialOption } @@ -81,11 +81,11 @@ func WithInterceptors(i ...Interceptor) ServerOption { // WithPeers configures the server to both track and call a fixed set of peer // servers. The myID parameter is this server's own node ID; it is always -// present in the peer [Configuration] so that quorum thresholds account for +// present in the peer [Config] so that quorum thresholds account for // the local replica, and calls to it are served in-process without a network // round-trip. // -// The server builds the peer [Configuration] itself, available from +// The server builds the peer [Config] itself, available from // [Server.PeerConfig], applying opts to the connections it establishes. To // observe which peers are currently reachable, use [Server.ConnectedPeers]. // @@ -102,10 +102,10 @@ func WithPeers(myID uint32, nodes NodeListOption, opts ...DialOption) ServerOpti } // WithPeerChange registers a callback invoked after each change to the peer -// [Configuration] (peer connect or disconnect). The callback runs while +// [Config] (peer connect or disconnect). The callback runs while // internal locks are held, so it must not call [Server.ConnectedPeers] or other // blocking methods; use it only to signal or copy, not for long work. -func WithPeerChange(callback func(Configuration)) ServerOption { +func WithPeerChange(callback func(Config)) ServerOption { return func(o *serverOptions) { o.onConfigChange = callback } @@ -127,10 +127,10 @@ type Server struct { handlers map[string]Handler interceptors []Interceptor - mu sync.Mutex // guards lis - lis net.Listener // active listener; set by Serve, ListenAndServe, or NewLocalServers - listenAddr string // address recorded by WithAddr - outbound Configuration // peer config built by WithPeers; nil if unused + mu sync.Mutex // guards lis + lis net.Listener // active listener; set by Serve, ListenAndServe, or NewLocalServers + listenAddr string // address recorded by WithAddr + outbound Config // peer config built by WithPeers; nil if unused *inboundManager } @@ -183,19 +183,19 @@ func NewServer(opts ...ServerOption) *Server { return s } -// newPeerConfig builds the outbound [Configuration] this server uses to call +// newPeerConfig builds the outbound [Config] this server uses to call // other servers. It installs the server as the back-channel request handler so // the remote can dispatch requests back over the same connection. -func (s *Server) newPeerConfig(nodes NodeListOption, dialOpts []DialOption) (Configuration, error) { +func (s *Server) newPeerConfig(nodes NodeListOption, dialOpts []DialOption) (Config, error) { opts := append([]DialOption{withServer(s)}, dialOpts...) return NewConfig(nodes, opts...) } -// PeerConfig returns the [Configuration] of the peers configured with +// PeerConfig returns the [Config] of the peers configured with // [WithPeers], or nil if [WithPeers] was not used. Calls on the returned // configuration reach the peers over connections this server establishes; // calls on the local node are served in-process. -func (s *Server) PeerConfig() Configuration { +func (s *Server) PeerConfig() Config { return s.outbound } @@ -313,7 +313,7 @@ func (s *Server) GracefulStop() { // unblocks any [Server.WaitForPeers] and [Server.WaitForClients] callers, stops // the gRPC server, closes the listener owned by [Server.Serve], // [Server.ListenAndServe], or [NewLocalServers], and closes the peer -// [Configuration] built by [WithPeers]. It does not use gRPC graceful stop, +// [Config] built by [WithPeers]. It does not use gRPC graceful stop, // because one-way methods do not respond and would block indefinitely. Stop is // safe to call before serving starts, and safe to call more than once. func (s *Server) Stop() { diff --git a/server_e2e_test.go b/server_e2e_test.go index 3ab61c8d..2797557c 100644 --- a/server_e2e_test.go +++ b/server_e2e_test.go @@ -48,7 +48,7 @@ func TestServerSymmetricConfigurationConnectsAllPeers(t *testing.T) { // includes each server's node ID in its connection metadata. for i, srv := range servers { ctx := gorumstest.Context(t, 5*time.Second) - if err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + if err := srv.WaitForPeers(ctx, func(cfg gorums.Config) bool { return cfg.Size() == len(servers) }); err != nil { t.Fatalf("server %d: WaitForPeers: %v", i+1, err) @@ -78,7 +78,7 @@ func awaitServerReady(t *testing.T, servers []*gorums.Server) { t.Helper() for _, srv := range servers { ctx := gorumstest.Context(t, 5*time.Second) - if err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + if err := srv.WaitForPeers(ctx, func(cfg gorums.Config) bool { return cfg.Size() == len(servers) }); err != nil { t.Fatalf("awaitServerReady: %v", err) @@ -90,7 +90,7 @@ func awaitServerReady(t *testing.T, servers []*gorums.Server) { func awaitClientReady(t *testing.T, srv *gorums.Server, n int) { t.Helper() ctx := gorumstest.Context(t, 5*time.Second) - if err := srv.WaitForClients(ctx, func(cfg gorums.Configuration) bool { + if err := srv.WaitForClients(ctx, func(cfg gorums.Config) bool { return cfg.Size() == n }); err != nil { t.Fatalf("awaitClientReady: %v", err) @@ -102,8 +102,8 @@ func awaitClientReady(t *testing.T, srv *gorums.Server, n int) { // reverse-direction calls to them via [gorums.ServerCtx.ConnectedClients]. // The client is a standalone [*gorums.Server] (no listener needed) whose registered handlers // are reachable by the server over the existing bidirectional gRPC stream. The returned -// [gorums.Configuration] is the client's outbound config pointing at the server. -func createClientServerPair(t *testing.T) (*gorums.Server, *gorums.Server, gorums.Configuration) { +// [gorums.Config] is the client's outbound config pointing at the server. +func createClientServerPair(t *testing.T) (*gorums.Server, *gorums.Server, gorums.Config) { t.Helper() // Bind the listener up front so the client knows the address before the @@ -490,7 +490,7 @@ func TestServerHandlerCanMulticastViaConnectedClients(t *testing.T) { // Each subtest creates its own isolated servers so that goroutines left over // from one subtest cannot contaminate the next. func TestServerLocalDispatchContention(t *testing.T) { - startServers := func(t *testing.T) gorums.Configuration { + startServers := func(t *testing.T) gorums.Config { t.Helper() servers := gorumstest.LocalServers(t, 3) for _, srv := range servers { @@ -656,7 +656,7 @@ func TestWaitForPeers(t *testing.T) { awaitServerReady(t, servers) ctx := gorumstest.Context(t, 2*time.Second) - if err := servers[0].WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + if err := servers[0].WaitForPeers(ctx, func(cfg gorums.Config) bool { return cfg.Size() == 3 }); err != nil { t.Fatalf("WaitForPeers: %v", err) @@ -667,7 +667,7 @@ func TestWaitForPeers(t *testing.T) { servers := gorumstest.LocalServers(t, 3) ctx := gorumstest.Context(t, 5*time.Second) - if err := servers[0].WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + if err := servers[0].WaitForPeers(ctx, func(cfg gorums.Config) bool { return cfg.Size() == 3 }); err != nil { t.Fatalf("WaitForPeers: %v", err) @@ -681,7 +681,7 @@ func TestWaitForPeers(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond) defer cancel() - err := srv.WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + err := srv.WaitForPeers(ctx, func(cfg gorums.Config) bool { return cfg.Size() == 3 // never true }) if !errors.Is(err, context.DeadlineExceeded) { @@ -695,7 +695,7 @@ func TestWaitForPeers(t *testing.T) { errCh := make(chan error, 1) go func() { - errCh <- srv.WaitForPeers(context.Background(), func(cfg gorums.Configuration) bool { + errCh <- srv.WaitForPeers(context.Background(), func(cfg gorums.Config) bool { return cfg.Size() == 3 // never true }) }() @@ -722,7 +722,7 @@ func TestWaitForPeers(t *testing.T) { for range waiters { ctx := gorumstest.Context(t, 5*time.Second) go func(ctx context.Context) { - errCh <- servers[0].WaitForPeers(ctx, func(cfg gorums.Configuration) bool { + errCh <- servers[0].WaitForPeers(ctx, func(cfg gorums.Config) bool { return cfg.Size() == 3 }) }(ctx) @@ -739,7 +739,7 @@ func TestWaitForPeers(t *testing.T) { srvServer, _, _ := createClientServerPair(t) ctx := gorumstest.Context(t, 5*time.Second) - if err := srvServer.WaitForClients(ctx, func(cfg gorums.Configuration) bool { + if err := srvServer.WaitForClients(ctx, func(cfg gorums.Config) bool { return cfg.Size() == 1 }); err != nil { t.Fatalf("WaitForClients: %v", err) From 96bdaf77767eaaa8901d8396e1448f3784c67150 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:17:59 +0200 Subject: [PATCH 03/16] gorums: rename ServerCtx to ServerContext ServerCtx was the only abbreviated type in the public API. It embeds a context.Context and is the server-side counterpart of ConfigContext and NodeContext, neither of which abbreviates, so the short form was inconsistent rather than concise. Generated handler signatures carry this type, so the name appears in every service implementation. --- callopts_test.go | 2 +- .../dev/generated_code_test.go | 2 +- .../gengorums/template_server.go | 2 +- doc/ordering.md | 4 +- doc/user-guide.md | 50 +++++++++---------- examples/interceptors/server_interceptors.go | 10 ++-- examples/storage/server.go | 18 +++---- gorumstest/servers.go | 16 +++--- handler.go | 28 +++++------ inbound_manager_test.go | 8 +-- internal/tests/config/config_test.go | 2 +- .../tests/correctable/correctable_test.go | 4 +- internal/tests/metadata/metadata_test.go | 4 +- internal/tests/oneway/oneway_test.go | 4 +- internal/tests/ordering/order_test.go | 4 +- internal/tests/tls/tls_test.go | 2 +- .../tests/unresponsive/unreponsive_test.go | 2 +- opts.go | 4 +- server.go | 4 +- server_e2e_test.go | 22 ++++---- server_test.go | 10 ++-- 21 files changed, 101 insertions(+), 101 deletions(-) diff --git a/callopts_test.go b/callopts_test.go index a2ba75cf..187e3da0 100644 --- a/callopts_test.go +++ b/callopts_test.go @@ -82,7 +82,7 @@ func TestCallOptionsIgnoreErrorsResourceLeak(t *testing.T) { // Now fixed: no replyChan → no ResponseChan → no Register. servers := testLocalServers(t, 3) for _, srv := range servers { - srv.RegisterHandler(mock.TestMethod, func(_ ServerCtx, _ *Message) (*Message, error) { + srv.RegisterHandler(mock.TestMethod, func(_ ServerContext, _ *Message) (*Message, error) { return nil, nil }) } diff --git a/cmd/protoc-gen-gorums/dev/generated_code_test.go b/cmd/protoc-gen-gorums/dev/generated_code_test.go index 3d8b9398..d16cfebc 100644 --- a/cmd/protoc-gen-gorums/dev/generated_code_test.go +++ b/cmd/protoc-gen-gorums/dev/generated_code_test.go @@ -22,7 +22,7 @@ const quorumCallMethod = "dev.ZorumsService.QuorumCall" func quorumCallServer(_ int) gorums.ServerIface { srv := gorums.NewServer() - srv.RegisterHandler(quorumCallMethod, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(quorumCallMethod, func(_ gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*dev.Request](in) resp := &dev.Response{} resp.SetResult(int64(len(req.GetValue()))) diff --git a/cmd/protoc-gen-gorums/gengorums/template_server.go b/cmd/protoc-gen-gorums/gengorums/template_server.go index 53a4d4ba..fb294c8a 100644 --- a/cmd/protoc-gen-gorums/gengorums/template_server.go +++ b/cmd/protoc-gen-gorums/gengorums/template_server.go @@ -1,7 +1,7 @@ package gengorums var serverVariables = ` -{{$context := use "gorums.ServerCtx" .GenFile}} +{{$context := use "gorums.ServerContext" .GenFile}} ` var serverInterface = ` diff --git a/doc/ordering.md b/doc/ordering.md index f9469404..e7e6bd58 100644 --- a/doc/ordering.md +++ b/doc/ordering.md @@ -54,7 +54,7 @@ type Server interface { // Runs in its own goroutine. // Server waits until the handler returns // or until the handler calls Release() on the context object. - RPC(gorums.ServerCtx, *Request) (*Response, error) + RPC(gorums.ServerContext, *Request) (*Response, error) } ``` @@ -69,7 +69,7 @@ Hence, the penalty for running server handlers synchronously is reduced while st Below is an example of how such a handler could be written: ```go -func (s *testSrv) AsyncHandler(ctx gorums.ServerCtx, req *Request) (resp *Response, err error) { +func (s *testSrv) AsyncHandler(ctx gorums.ServerContext, req *Request) (resp *Response, err error) { // do synchronous work response := &Response{ InOrder: s.isInOrder(req.GetNum()), diff --git a/doc/user-guide.md b/doc/user-guide.md index 592c5d5d..a524b09a 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -222,11 +222,11 @@ And this is our server interface: ```go type StorageServer interface { - ReadRPC(ctx gorums.ServerCtx, request *ReadRequest) (response *ReadResponse, err error) - WriteUnicast(ctx gorums.ServerCtx, request *WriteRequest) - WriteMulticast(ctx gorums.ServerCtx, request *WriteRequest) - ReadQC(ctx gorums.ServerCtx, request *ReadRequest) (response *ReadResponse, err error) - ReadCorrectable(ctx gorums.ServerCtx, request *ReadRequest, send func(response *ReadResponse) error) error + ReadRPC(ctx gorums.ServerContext, request *ReadRequest) (response *ReadResponse, err error) + WriteUnicast(ctx gorums.ServerContext, request *WriteRequest) + WriteMulticast(ctx gorums.ServerContext, request *WriteRequest) + ReadQC(ctx gorums.ServerContext, request *ReadRequest) (response *ReadResponse, err error) + ReadCorrectable(ctx gorums.ServerContext, request *ReadRequest, send func(response *ReadResponse) error) error } ``` @@ -247,13 +247,13 @@ type storageSrv struct { state *ReadResponse } -func (srv *storageSrv) ReadRPC(_ gorums.ServerCtx, req *ReadRequest) (resp *ReadResponse, err error) { +func (srv *storageSrv) ReadRPC(_ gorums.ServerContext, req *ReadRequest) (resp *ReadResponse, err error) { srv.mut.Lock() defer srv.mut.Unlock() return srv.state, nil } -func (srv *storageSrv) WriteUnicast(_ gorums.ServerCtx, req *WriteRequest) { +func (srv *storageSrv) WriteUnicast(_ gorums.ServerContext, req *WriteRequest) { srv.mut.Lock() defer srv.mut.Unlock() if req.GetTime().AsTime().After(srv.state.GetTime().AsTime()) { @@ -261,7 +261,7 @@ func (srv *storageSrv) WriteUnicast(_ gorums.ServerCtx, req *WriteRequest) { } } -func (srv *storageSrv) WriteMulticast(_ gorums.ServerCtx, req *WriteRequest) { +func (srv *storageSrv) WriteMulticast(_ gorums.ServerContext, req *WriteRequest) { srv.mut.Lock() defer srv.mut.Unlock() if req.GetTime().AsTime().After(srv.state.GetTime().AsTime()) { @@ -269,13 +269,13 @@ func (srv *storageSrv) WriteMulticast(_ gorums.ServerCtx, req *WriteRequest) { } } -func (srv *storageSrv) ReadQC(_ gorums.ServerCtx, req *ReadRequest) (resp *ReadResponse, err error) { +func (srv *storageSrv) ReadQC(_ gorums.ServerContext, req *ReadRequest) (resp *ReadResponse, err error) { srv.mut.Lock() defer srv.mut.Unlock() return srv.state, nil } -func (srv *storageSrv) ReadCorrectable(_ gorums.ServerCtx, req *ReadRequest, send func(response *ReadResponse) error) error { +func (srv *storageSrv) ReadCorrectable(_ gorums.ServerContext, req *ReadRequest, send func(response *ReadResponse) error) error { srv.mut.Lock() defer srv.mut.Unlock() return send(srv.state) @@ -291,12 +291,12 @@ There are some important things to note about implementing the server interfaces To guarantee messages from different senders are executed in-order at the different servers, you must use a total ordering protocol. * Errors should be returned using the [`status` package](https://pkg.go.dev/google.golang.org/grpc/status?tab=doc). * Handlers run synchronously, and hence a long-running handler will prevent other messages from being handled. - To help solve this problem, our `ServerCtx` objects have a `Release()` function that releases the handler's lock on the server, + To help solve this problem, our `ServerContext` objects have a `Release()` function that releases the handler's lock on the server, which allows the next request to be processed. After `ctx.Release()` has been called, the handler may run concurrently with the handlers for the next requests. The handler automatically calls `ctx.Release()` after returning. ```go - func (srv *storageSrv) ReadRPC(ctx gorums.ServerCtx, req *ReadRequest) (resp *ReadResponse, err error) { + func (srv *storageSrv) ReadRPC(ctx gorums.ServerContext, req *ReadRequest) (resp *ReadResponse, err error) { // any code running before this will be executed in-order ctx.Release() // after Release() has been called, a new request handler may be started, @@ -1050,7 +1050,7 @@ Gorums also supports server-side interceptors that wrap inbound RPC handlers, si A server-side interceptor implements the `gorums.Interceptor` signature: ```go -type Interceptor func(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) +type Interceptor func(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) ``` You can pass multiple interceptors when starting a Gorums server. They can perform logging, latency injection, metadata insertion, and request validation before sending the request to the handler. @@ -1061,7 +1061,7 @@ Below are several examples based on the `examples/interceptors` package. ```go func LoggingInterceptor(addr string) gorums.Interceptor { - return func(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { + return func(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { req := gorums.AsProto[proto.Message](in) log.Printf("[%s]: LoggingInterceptor(incoming): Method=%s, Message=%s", addr, in.GetMethod(), req) @@ -1081,13 +1081,13 @@ func LoggingInterceptor(addr string) gorums.Interceptor { Interceptors can inject arbitrary delays based on client properties or attach metadata to the incoming requests: ```go -func DelayedInterceptor(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { +func DelayedInterceptor(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { delay := 50 * time.Millisecond time.Sleep(delay) return next(ctx, in) } -func MetadataInterceptor(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { +func MetadataInterceptor(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { // Inject a custom metadata field for the handler entry := gorums.MetadataEntry_builder{ Key: "customKey", @@ -1105,7 +1105,7 @@ A server interceptor can also stop a request from reaching the handler entirely. ```go // NoFooAllowedInterceptor rejects requests for messages with key "foo". -func NoFooAllowedInterceptor[T interface{ GetKey() string }](ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { +func NoFooAllowedInterceptor[T interface{ GetKey() string }](ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { if req, ok := gorums.AsProto[proto.Message](in).(T); ok { if req.GetKey() == "foo" { return nil, fmt.Errorf("requests for key 'foo' are not allowed") @@ -1748,12 +1748,12 @@ greeting = gorums The `nread` and `nwrite` commands trigger server-side nested quorum calls and nested multicasts, which are described in the following sections. -## Nested Quorum Calls with ServerCtx.PeerConfig +## Nested Quorum Calls with ServerContext.PeerConfig A server handler (the server method itself) can act as a client and issue its own quorum calls to other nodes. These are called *nested quorum calls*, because one quorum call triggers another from inside the server handler. -`ServerCtx.PeerConfig()` returns the `Config` of the peers the server was configured with via `gorums.WithPeers`. +`ServerContext.PeerConfig()` returns the `Config` of the peers the server was configured with via `gorums.WithPeers`. This makes it straightforward for a handler to fan out a sub-request to the rest of the cluster. It is the full peer set, not the reachable subset, so a quorum size derived from it inside a handler does not shift as peers connect and disconnect; use `ctx.ConnectedPeers()` to observe reachability. @@ -1790,7 +1790,7 @@ Without `Release()`, the server would block all other inbound messages until the ```go // 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) { +func (s *storageServer) ReadNestedQC(ctx gorums.ServerContext, req *pb.ReadRequest) (*pb.ReadResponse, error) { config := ctx.PeerConfig() if len(config) == 0 { return nil, fmt.Errorf("read_nested_qc: requires a server peer configuration") @@ -1805,7 +1805,7 @@ func (s *storageServer) ReadNestedQC(ctx gorums.ServerCtx, req *pb.ReadRequest) The same pattern applies to nested multicast: ```go -func (s *storageServer) WriteNestedMulticast(ctx gorums.ServerCtx, req *pb.WriteRequest) (*pb.WriteResponse, error) { +func (s *storageServer) WriteNestedMulticast(ctx gorums.ServerContext, req *pb.WriteRequest) (*pb.WriteResponse, error) { config := ctx.PeerConfig() if len(config) == 0 { return nil, fmt.Errorf("write_nested_multicast: requires server peer configuration") @@ -1841,9 +1841,9 @@ sequenceDiagram The client sees a single quorum call, but internally each receiving node fans out to all of its peers and returns the freshest value found across the whole cluster. -## Reverse Direction Calls with ServerCtx.ConnectedClients +## Reverse Direction Calls with ServerContext.ConnectedClients -`ServerCtx.ConnectedClients()` returns a `Config` of all currently connected *client peers* — nodes that connected to this server dynamically, rather than being pre-configured with `WithPeers`. +`ServerContext.ConnectedClients()` returns a `Config` of all currently connected *client peers* — nodes that connected to this server dynamically, rather than being pre-configured with `WithPeers`. A handler can use this configuration to make outbound calls back towards those clients, reversing the usual direction of communication. This pattern is particularly useful when clients are behind a firewall and cannot accept inbound connections. @@ -1929,7 +1929,7 @@ The handler reads `ctx.ConnectedClients()` to reach all currently connected clie ```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) { +func (s *storageServer) ReadNestedQC(ctx gorums.ServerContext, req *pb.ReadRequest) (*pb.ReadResponse, error) { config := ctx.ConnectedClients() if len(config) == 0 { return nil, fmt.Errorf("read_nested_qc: no client peers connected") @@ -1939,7 +1939,7 @@ func (s *storageServer) ReadNestedQC(ctx gorums.ServerCtx, req *pb.ReadRequest) } ``` -The key difference from `ServerCtx.PeerConfig()` is the direction of each per-node connection: +The key difference from `ServerContext.PeerConfig()` is the direction of each per-node connection: | Method | Connection direction | Typical use case | | -------------------- | ----------------------------------------------- | -------------------------------------------------- | diff --git a/examples/interceptors/server_interceptors.go b/examples/interceptors/server_interceptors.go index 2a001e9e..d2f0b0d3 100644 --- a/examples/interceptors/server_interceptors.go +++ b/examples/interceptors/server_interceptors.go @@ -11,7 +11,7 @@ import ( ) func LoggingInterceptor(addr string) gorums.Interceptor { - return func(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { + return func(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { req := gorums.AsProto[proto.Message](in) log.Printf("[%s]: LoggingInterceptor(incoming): Method=%s, Message=%s", addr, in.GetMethod(), req) start := time.Now() @@ -24,7 +24,7 @@ func LoggingInterceptor(addr string) gorums.Interceptor { } } -func LoggingSimpleInterceptor(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { +func LoggingSimpleInterceptor(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { req := gorums.AsProto[proto.Message](in) log.Printf("LoggingSimpleInterceptor(incoming): Method=%s, Message=%v)", in.GetMethod(), req) out, err := next(ctx, in) @@ -33,7 +33,7 @@ func LoggingSimpleInterceptor(ctx gorums.ServerCtx, in *gorums.Message, next gor return out, err } -func DelayedInterceptor(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { +func DelayedInterceptor(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { // delay based on sending node address delay := 0 * time.Millisecond peer, ok := peer.FromContext(ctx) @@ -55,7 +55,7 @@ func DelayedInterceptor(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Ha } /** NoFooAllowedInterceptor rejects requests for messages with key "foo". */ -func NoFooAllowedInterceptor[T interface{ GetKey() string }](ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { +func NoFooAllowedInterceptor[T interface{ GetKey() string }](ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { if req, ok := gorums.AsProto[proto.Message](in).(T); ok { log.Printf("NoFooAllowedInterceptor: Received request for key '%s'", req.GetKey()) if req.GetKey() == "foo" { @@ -66,7 +66,7 @@ func NoFooAllowedInterceptor[T interface{ GetKey() string }](ctx gorums.ServerCt return next(ctx, in) } -func MetadataInterceptor(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { +func MetadataInterceptor(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { log.Printf("MetadataInterceptor: Adding custom metadata to message(customKey=customValue)") // Add a custom metadata field entry := gorums.MetadataEntry_builder{ diff --git a/examples/storage/server.go b/examples/storage/server.go index 076b09e8..c2ec481f 100644 --- a/examples/storage/server.go +++ b/examples/storage/server.go @@ -167,24 +167,24 @@ func (rw rawWriter) Write(p []byte) (n int, err error) { } // ReadRPC is an RPC handler -func (s *storageServer) ReadRPC(_ gorums.ServerCtx, req *pb.ReadRequest) (resp *pb.ReadResponse, err error) { +func (s *storageServer) ReadRPC(_ gorums.ServerContext, req *pb.ReadRequest) (resp *pb.ReadResponse, err error) { return s.Read(req) } // WriteRPC is an RPC handler -func (s *storageServer) WriteRPC(_ gorums.ServerCtx, req *pb.WriteRequest) (resp *pb.WriteResponse, err error) { +func (s *storageServer) WriteRPC(_ gorums.ServerContext, req *pb.WriteRequest) (resp *pb.WriteResponse, err error) { return s.Write(req) } // WriteUnicast is an RPC handler for one-way unicast writes. -func (s *storageServer) WriteUnicast(_ gorums.ServerCtx, req *pb.WriteRequest) { +func (s *storageServer) WriteUnicast(_ gorums.ServerContext, req *pb.WriteRequest) { if _, err := s.Write(req); err != nil { s.logger.Printf("WriteUnicast error: %v", err) } } // WriteMulticast is an RPC handler for one-way multicast writes. -func (s *storageServer) WriteMulticast(_ gorums.ServerCtx, req *pb.WriteRequest) { +func (s *storageServer) WriteMulticast(_ gorums.ServerContext, req *pb.WriteRequest) { _, err := s.Write(req) if err != nil { s.logger.Printf("Write error: %v", err) @@ -192,17 +192,17 @@ func (s *storageServer) WriteMulticast(_ gorums.ServerCtx, req *pb.WriteRequest) } // ReadQC is an RPC handler for a quorum call. -func (s *storageServer) ReadQC(_ gorums.ServerCtx, req *pb.ReadRequest) (resp *pb.ReadResponse, err error) { +func (s *storageServer) ReadQC(_ gorums.ServerContext, req *pb.ReadRequest) (resp *pb.ReadResponse, err error) { return s.Read(req) } // WriteQC is an RPC handler for a quorum call. -func (s *storageServer) WriteQC(_ gorums.ServerCtx, req *pb.WriteRequest) (resp *pb.WriteResponse, err error) { +func (s *storageServer) WriteQC(_ gorums.ServerContext, req *pb.WriteRequest) (resp *pb.WriteResponse, err error) { return s.Write(req) } // ReadCorrectable is an RPC handler for a correctable quorum call. It sends multiple responses. -func (s *storageServer) ReadCorrectable(_ gorums.ServerCtx, req *pb.ReadRequest, send func(response *pb.ReadResponse)) { +func (s *storageServer) ReadCorrectable(_ gorums.ServerContext, req *pb.ReadRequest, send func(response *pb.ReadResponse)) { resp, err := s.Read(req) if err != nil { s.logger.Printf("ReadCorrectable error: %v", err) @@ -215,7 +215,7 @@ func (s *storageServer) ReadCorrectable(_ gorums.ServerCtx, req *pb.ReadRequest, // ReadNestedQC is a quorum-call handler that performs a nested quorum call // using the server's peer configuration from WithPeers. -func (s *storageServer) ReadNestedQC(ctx gorums.ServerCtx, req *pb.ReadRequest) (resp *pb.ReadResponse, err error) { +func (s *storageServer) ReadNestedQC(ctx gorums.ServerContext, req *pb.ReadRequest) (resp *pb.ReadResponse, err error) { cfg := ctx.PeerConfig() if len(cfg) == 0 { return nil, fmt.Errorf("read_nested_qc: requires server peer configuration") @@ -227,7 +227,7 @@ func (s *storageServer) ReadNestedQC(ctx gorums.ServerCtx, req *pb.ReadRequest) // WriteNestedMulticast is a quorum-call handler that performs a nested multicast // using the server's peer configuration from WithPeers. -func (s *storageServer) WriteNestedMulticast(ctx gorums.ServerCtx, req *pb.WriteRequest) (resp *pb.WriteResponse, err error) { +func (s *storageServer) WriteNestedMulticast(ctx gorums.ServerContext, req *pb.WriteRequest) (resp *pb.WriteResponse, err error) { cfg := ctx.PeerConfig() if len(cfg) == 0 { return nil, fmt.Errorf("write_nested_multicast: requires server peer configuration") diff --git a/gorumstest/servers.go b/gorumstest/servers.go index aa89680f..7b518feb 100644 --- a/gorumstest/servers.go +++ b/gorumstest/servers.go @@ -21,7 +21,7 @@ func DefaultServer(i int) gorums.ServerIface { func defaultTestServer(i int, opts ...gorums.ServerOption) gorums.ServerIface { srv := gorums.NewServer(opts...) ts := testSrv{val: int32((i + 1) * 10)} - srv.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) resp, err := ts.Test(ctx, req) if err != nil { @@ -29,7 +29,7 @@ func defaultTestServer(i int, opts ...gorums.ServerOption) gorums.ServerIface { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler(mock.GetValueMethod, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(mock.GetValueMethod, func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.Int32Value](in) resp, err := ts.GetValue(ctx, req) if err != nil { @@ -44,11 +44,11 @@ type testSrv struct { val int32 } -func (testSrv) Test(_ gorums.ServerCtx, _ *pb.StringValue) (*pb.StringValue, error) { +func (testSrv) Test(_ gorums.ServerContext, _ *pb.StringValue) (*pb.StringValue, error) { return pb.String(""), nil } -func (ts testSrv) GetValue(_ gorums.ServerCtx, _ *pb.Int32Value) (*pb.Int32Value, error) { +func (ts testSrv) GetValue(_ gorums.ServerContext, _ *pb.Int32Value) (*pb.Int32Value, error) { return pb.Int32(ts.val), nil } @@ -57,7 +57,7 @@ func (ts testSrv) GetValue(_ gorums.ServerCtx, _ *pb.Int32Value) (*pb.Int32Value // [Node], or [Servers]. func EchoServerFn(_ int) gorums.ServerIface { srv := gorums.NewServer() - srv.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) resp, err := echoSrv{}.Test(ctx, req) if err != nil { @@ -72,7 +72,7 @@ func EchoServerFn(_ int) gorums.ServerIface { // echoSrv implements a simple echo server handler for testing type echoSrv struct{} -func (echoSrv) Test(_ gorums.ServerCtx, req *pb.StringValue) (*pb.StringValue, error) { +func (echoSrv) Test(_ gorums.ServerContext, req *pb.StringValue) (*pb.StringValue, error) { return pb.String("echo: " + req.GetValue()), nil } @@ -81,7 +81,7 @@ func (echoSrv) Test(_ gorums.ServerCtx, req *pb.StringValue) (*pb.StringValue, e // argument to [Config], [Node], or [Servers]. func StreamServerFn(_ int) gorums.ServerIface { srv := gorums.NewServer() - srv.RegisterHandler(mock.Stream, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(mock.Stream, func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) val := req.GetValue() @@ -103,7 +103,7 @@ func StreamServerFn(_ int) gorums.ServerIface { // argument to [Config], [Node], or [Servers]. func StreamBenchmarkServerFn(_ int) gorums.ServerIface { srv := gorums.NewServer() - srv.RegisterHandler(mock.Stream, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(mock.Stream, func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) val := req.GetValue() diff --git a/handler.go b/handler.go index 6d449eee..97a3fccf 100644 --- a/handler.go +++ b/handler.go @@ -25,17 +25,17 @@ type MetadataEntry_builder = stream.MetadataEntry_builder type ( // Handler processes a request and returns a response. - Handler func(ServerCtx, *Message) (*Message, error) + Handler func(ServerContext, *Message) (*Message, error) // Interceptor intercepts and may modify incoming requests and outgoing responses. - // It receives a ServerCtx, the incoming Message, and a Handler representing + // It receives a ServerContext, the incoming Message, and a Handler representing // the next element in the chain. It returns a Message and an error. - Interceptor func(ServerCtx, *Message, Handler) (*Message, error) + Interceptor func(ServerContext, *Message, Handler) (*Message, error) ) -// ServerCtx is a context that is passed from the Gorums server to the handler. +// ServerContext is a context that is passed from the Gorums server to the handler. // It allows the handler to release its lock on the server, allowing the next // request to be processed. This happens automatically when the handler returns. -type ServerCtx struct { +type ServerContext struct { context.Context release func() send func(*stream.Message) @@ -45,7 +45,7 @@ type ServerCtx struct { // Release releases this handler's lock on the server, which allows the next request // to be processed concurrently. Use Release only when the handler no longer needs // exclusive access to the server's state. It is safe to call Release multiple times. -func (ctx *ServerCtx) Release() { +func (ctx *ServerContext) Release() { if ctx.release != nil { ctx.release() } @@ -56,7 +56,7 @@ func (ctx *ServerCtx) Release() { // and sent to the client; the stream is not closed. // // This function should only be used by generated code. -func (ctx *ServerCtx) SendMessage(out *Message) { +func (ctx *ServerContext) SendMessage(out *Message) { // If Msg is set, marshal it to payload before sending. if out.Msg != nil && len(out.GetPayload()) == 0 { payload, err := proto.Marshal(out.Msg) @@ -76,8 +76,8 @@ func (ctx *ServerCtx) SendMessage(out *Message) { // with via [WithPeers], or nil if it was not used. It is the full peer set, not // the currently reachable subset, so quorum sizes derived from it inside a // handler do not shift as peers connect and disconnect. Use -// [ServerCtx.ConnectedPeers] to observe reachability. -func (ctx *ServerCtx) PeerConfig() Config { +// [ServerContext.ConnectedPeers] to observe reachability. +func (ctx *ServerContext) PeerConfig() Config { if ctx.srv == nil { return nil } @@ -85,8 +85,8 @@ func (ctx *ServerCtx) PeerConfig() Config { } // ConnectedPeers returns the currently reachable subset of -// [ServerCtx.PeerConfig]; see [Server.ConnectedPeers]. -func (ctx *ServerCtx) ConnectedPeers() Config { +// [ServerContext.PeerConfig]; see [Server.ConnectedPeers]. +func (ctx *ServerContext) ConnectedPeers() Config { if ctx.srv == nil { return nil } @@ -98,7 +98,7 @@ func (ctx *ServerCtx) ConnectedPeers() Config { // An empty (non-nil) Config is returned if no client peers are connected. // The returned slice is replaced atomically on each connect/disconnect; // thus, retaining a reference to an old configuration is safe. -func (ctx *ServerCtx) ConnectedClients() Config { +func (ctx *ServerContext) ConnectedClients() Config { if ctx.srv == nil { return nil } @@ -110,7 +110,7 @@ func (ctx *ServerCtx) ConnectedClients() Config { // to facilitate routing the response back to the caller on the client side. // The payload, error status, and metadata entries are left empty; the error status // of the response can be set using [MessageWithError], and the payload will -// be marshaled by [ServerCtx.SendMessage]. This function is safe for concurrent use. +// be marshaled by [ServerContext.SendMessage]. This function is safe for concurrent use. // // This function should only be used in generated code. func NewResponseMessage(in *Message, resp proto.Message) *Message { @@ -175,7 +175,7 @@ func chainInterceptors(final Handler, interceptors ...Interceptor) Handler { for i := len(interceptors) - 1; i >= 0; i-- { curr := interceptors[i] next := handler - handler = func(ctx ServerCtx, in *Message) (*Message, error) { + handler = func(ctx ServerContext, in *Message) (*Message, error) { return curr(ctx, in, next) } } diff --git a/inbound_manager_test.go b/inbound_manager_test.go index e49b100e..c1e0c216 100644 --- a/inbound_manager_test.go +++ b/inbound_manager_test.go @@ -712,7 +712,7 @@ func TestKnownPeerServerCallsClient(t *testing.T) { // 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) { + clientSrv.RegisterHandler(mock.TestMethod, func(_ ServerContext, in *Message) (*Message, error) { req := AsProto[*pb.StringValue](in) return NewResponseMessage(in, pb.String("echo: "+req.GetValue())), nil }) @@ -882,11 +882,11 @@ func TestClientConfigMixedMode(t *testing.T) { } // TestClientConfigServerCallsClient verifies that a server dispatches a reverse-direction -// multicast to a connected client via [ServerCtx.ConnectedClients]. +// multicast to a connected client via [ServerContext.ConnectedClients]. func TestClientConfigServerCallsClient(t *testing.T) { // Register the server handler before starting so it is present before clients arrive. srv := NewServer() - srv.RegisterHandler(mock.TestMethod, func(ctx ServerCtx, _ *Message) (*Message, error) { + srv.RegisterHandler(mock.TestMethod, func(ctx ServerContext, _ *Message) (*Message, error) { if clients := ctx.ConnectedClients(); len(clients) > 0 { _ = Multicast(clients.Context(ctx), pb.String("ping"), mock.Stream) } @@ -899,7 +899,7 @@ func TestClientConfigServerCallsClient(t *testing.T) { // 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) { + clientSrv.RegisterHandler(mock.Stream, func(_ ServerContext, _ *Message) (*Message, error) { wg.Done() return nil, nil }) diff --git a/internal/tests/config/config_test.go b/internal/tests/config/config_test.go index 11656c37..cef426f2 100644 --- a/internal/tests/config/config_test.go +++ b/internal/tests/config/config_test.go @@ -11,7 +11,7 @@ import ( type cfgSrv struct{} -func (cfgSrv) Read(_ gorums.ServerCtx, req *Request) (resp *Response, err error) { +func (cfgSrv) Read(_ gorums.ServerContext, req *Request) (resp *Response, err error) { return Response_builder{ Num: req.GetNum(), }.Build(), nil diff --git a/internal/tests/correctable/correctable_test.go b/internal/tests/correctable/correctable_test.go index 874b0459..c993ab2d 100644 --- a/internal/tests/correctable/correctable_test.go +++ b/internal/tests/correctable/correctable_test.go @@ -101,11 +101,11 @@ type testSrv struct { n int } -func (testSrv) Correctable(_ gorums.ServerCtx, _ *Request) (*Response, error) { +func (testSrv) Correctable(_ gorums.ServerContext, _ *Request) (*Response, error) { return Response_builder{Level: 1}.Build(), nil } -func (srv testSrv) CorrectableStream(_ gorums.ServerCtx, _ *Request, send func(response *Response)) { +func (srv testSrv) CorrectableStream(_ gorums.ServerContext, _ *Request, send func(response *Response)) { for i := range srv.n { send(Response_builder{Level: int32(i + 1)}.Build()) } diff --git a/internal/tests/metadata/metadata_test.go b/internal/tests/metadata/metadata_test.go index d5050167..529bd46a 100644 --- a/internal/tests/metadata/metadata_test.go +++ b/internal/tests/metadata/metadata_test.go @@ -16,7 +16,7 @@ import ( type testSrv struct{} -func (testSrv) IDFromMD(ctx gorums.ServerCtx, _ *emptypb.Empty) (resp *NodeID, err error) { +func (testSrv) IDFromMD(ctx gorums.ServerContext, _ *emptypb.Empty) (resp *NodeID, err error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, status.Error(codes.NotFound, "metadata unavailable") @@ -32,7 +32,7 @@ func (testSrv) IDFromMD(ctx gorums.ServerCtx, _ *emptypb.Empty) (resp *NodeID, e return NodeID_builder{ID: id}.Build(), nil } -func (testSrv) WhatIP(ctx gorums.ServerCtx, _ *emptypb.Empty) (resp *IPAddr, err error) { +func (testSrv) WhatIP(ctx gorums.ServerContext, _ *emptypb.Empty) (resp *IPAddr, err error) { peerInfo, ok := peer.FromContext(ctx) if !ok { return nil, status.Error(codes.NotFound, "Peer info unavailable") diff --git a/internal/tests/oneway/oneway_test.go b/internal/tests/oneway/oneway_test.go index 94b225a6..529593f4 100644 --- a/internal/tests/oneway/oneway_test.go +++ b/internal/tests/oneway/oneway_test.go @@ -21,7 +21,7 @@ type onewaySrv struct { received chan *oneway.Request } -func (s *onewaySrv) Unicast(_ gorums.ServerCtx, r *oneway.Request) { +func (s *onewaySrv) Unicast(_ gorums.ServerContext, r *oneway.Request) { if s.benchmark { return } @@ -29,7 +29,7 @@ func (s *onewaySrv) Unicast(_ gorums.ServerCtx, r *oneway.Request) { s.wg.Done() } -func (s *onewaySrv) Multicast(_ gorums.ServerCtx, r *oneway.Request) { +func (s *onewaySrv) Multicast(_ gorums.ServerContext, r *oneway.Request) { if s.benchmark { return } diff --git a/internal/tests/ordering/order_test.go b/internal/tests/ordering/order_test.go index 0c7b68d5..09eff8e9 100644 --- a/internal/tests/ordering/order_test.go +++ b/internal/tests/ordering/order_test.go @@ -60,13 +60,13 @@ func (s *testSrv) isInOrder(num uint64) bool { return false } -func (s *testSrv) QuorumCall(_ gorums.ServerCtx, req *Request) (resp *Response, err error) { +func (s *testSrv) QuorumCall(_ gorums.ServerContext, req *Request) (resp *Response, err error) { return Response_builder{ InOrder: s.isInOrder(req.GetNum()), }.Build(), nil } -func (s *testSrv) UnaryRPC(_ gorums.ServerCtx, req *Request) (resp *Response, err error) { +func (s *testSrv) UnaryRPC(_ gorums.ServerContext, req *Request) (resp *Response, err error) { return Response_builder{ InOrder: s.isInOrder(req.GetNum()), }.Build(), nil diff --git a/internal/tests/tls/tls_test.go b/internal/tests/tls/tls_test.go index d934cf5d..f80f5dcb 100644 --- a/internal/tests/tls/tls_test.go +++ b/internal/tests/tls/tls_test.go @@ -15,7 +15,7 @@ import ( type testSrv struct{} -func (testSrv) TestTLS(ctx gorums.ServerCtx, _ *Request) (resp *Response, err error) { +func (testSrv) TestTLS(ctx gorums.ServerContext, _ *Request) (resp *Response, err error) { peerInfo, ok := peer.FromContext(ctx) if !ok || peerInfo.AuthInfo.AuthType() != "tls" { return Response_builder{OK: false}.Build(), nil diff --git a/internal/tests/unresponsive/unreponsive_test.go b/internal/tests/unresponsive/unreponsive_test.go index 208c2d35..a096721a 100644 --- a/internal/tests/unresponsive/unreponsive_test.go +++ b/internal/tests/unresponsive/unreponsive_test.go @@ -12,7 +12,7 @@ import ( type testSrv struct{} -func (testSrv) TestUnresponsive(ctx gorums.ServerCtx, _ *Empty) (resp *Empty, err error) { +func (testSrv) TestUnresponsive(ctx gorums.ServerContext, _ *Empty) (resp *Empty, err error) { <-ctx.Done() return nil, nil } diff --git a/opts.go b/opts.go index b88e1c2d..cfa62cbc 100644 --- a/opts.go +++ b/opts.go @@ -97,9 +97,9 @@ func WithMetadata(md metadata.MD) DialOption { // // NodeID semantics: // - If srv.NodeID() == 0, the remote treats this connection as an anonymous -// client and tracks reverse-direction calls via [ServerCtx.ClientConfig]. +// client and tracks reverse-direction calls via [ServerContext.ClientConfig]. // - If srv.NodeID() > 0, the remote treats this connection as a known peer -// and routes requests via [ServerCtx.Config]. +// and routes requests via [ServerContext.Config]. func WithBackChannel(srv *Server) DialOption { if srv == nil { panic("gorums: WithBackChannel called with nil server") diff --git a/server.go b/server.go index 956cf143..18977ebf 100644 --- a/server.go +++ b/server.go @@ -137,7 +137,7 @@ type Server struct { // NewServer returns a new instance of [Server]. // // The server tracks connected clients that are capable of receiving reverse-direction -// calls from the server; these clients are accessible via [ServerCtx.ConnectedClients] +// calls from the server; these clients are accessible via [ServerContext.ConnectedClients] // and [Server.ConnectedClients]. If [WithPeers] is provided, the server additionally // tracks and calls a fixed set of peer servers, accessible via [Server.PeerConfig] // and, filtered by reachability, [Server.ConnectedPeers]. @@ -219,7 +219,7 @@ func (s *Server) RegisterHandler(method string, handler Handler) { // It is responsible for releasing the mutex when the handler chain is done, // unless already released by the handler itself, or an interceptor in the chain. func (s *Server) HandleRequest(ctx context.Context, reqMsg *stream.Message, release func(), send func(*stream.Message)) { - srvCtx := ServerCtx{ + srvCtx := ServerContext{ Context: ctx, release: release, send: send, diff --git a/server_e2e_test.go b/server_e2e_test.go index 2797557c..a5cd45b0 100644 --- a/server_e2e_test.go +++ b/server_e2e_test.go @@ -99,7 +99,7 @@ func awaitClientReady(t *testing.T, srv *gorums.Server, n int) { // createClientServerPair creates a server and a client for back-channel testing. // The server automatically tracks anonymous clients and can dispatch -// reverse-direction calls to them via [gorums.ServerCtx.ConnectedClients]. +// reverse-direction calls to them via [gorums.ServerContext.ConnectedClients]. // The client is a standalone [*gorums.Server] (no listener needed) whose registered handlers // are reachable by the server over the existing bidirectional gRPC stream. The returned // [gorums.Config] is the client's outbound config pointing at the server. @@ -141,13 +141,13 @@ func createClientServerPair(t *testing.T) (*gorums.Server, *gorums.Server, gorum // stringEchoHandler returns a handler that replies with prefix+": "+request value. func stringEchoHandler(prefix string) gorums.Handler { - return func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + return func(_ gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) return gorums.NewResponseMessage(in, pb.String(prefix+": "+req.GetValue())), nil } } -func configContext(ctx gorums.ServerCtx, client bool) (*gorums.ConfigContext, error) { +func configContext(ctx gorums.ServerContext, client bool) (*gorums.ConfigContext, error) { if client { clients := ctx.ConnectedClients() if len(clients) == 0 { @@ -174,7 +174,7 @@ func outerChainedHandler( respFn func(*gorums.Responses[*pb.StringValue]) (*pb.StringValue, error), ) gorums.Handler { t.Helper() - return func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + return func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) t.Logf("Server %d received outer request: %s", myID, req.GetValue()) // Release the NodeStream mutex before making the inner quorum call. @@ -266,7 +266,7 @@ func TestServerSymmetricConfigurationRoutesMulticast(t *testing.T) { // Register mock handler on each server for _, srv := range servers { - srv.RegisterHandler(mock.Stream, func(_ gorums.ServerCtx, _ *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(mock.Stream, func(_ gorums.ServerContext, _ *gorums.Message) (*gorums.Message, error) { wg.Done() return nil, nil }) @@ -298,7 +298,7 @@ func TestServerHandlerCanMulticastViaConfig(t *testing.T) { wg.Add(9) for i, srv := range servers { - srv.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { t.Logf("Server %d received multicast on %v: %v", i+1, mock.TestMethod, in.Msg) // Release before the nested multicast: the peer configuration // includes the local node, whose in-process dispatch waits for @@ -317,7 +317,7 @@ func TestServerHandlerCanMulticastViaConfig(t *testing.T) { return nil, nil // one-way }) - srv.RegisterHandler(mock.Stream, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(mock.Stream, func(_ gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { t.Logf("Server %d received multicast on %v: %v", i+1, mock.Stream, in.Msg) wg.Done() return nil, nil @@ -442,7 +442,7 @@ func TestServerHandlerCanMulticastViaConnectedClients(t *testing.T) { var wg sync.WaitGroup wg.Add(1) - srvServer.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srvServer.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { t.Logf("SERVER received multicast: %v", in.Msg) if cfg := ctx.ConnectedClients(); cfg != nil && cfg.Size() == 1 { err := gorums.Multicast( @@ -458,7 +458,7 @@ func TestServerHandlerCanMulticastViaConnectedClients(t *testing.T) { }) // Client handles the reverse-direction multicast dispatched by the server. - clientSrv.RegisterHandler(mock.Stream, func(_ gorums.ServerCtx, _ *gorums.Message) (*gorums.Message, error) { + clientSrv.RegisterHandler(mock.Stream, func(_ gorums.ServerContext, _ *gorums.Message) (*gorums.Message, error) { t.Log("CLIENT received inner multicast") wg.Done() return nil, nil @@ -494,7 +494,7 @@ func TestServerLocalDispatchContention(t *testing.T) { t.Helper() servers := gorumstest.LocalServers(t, 3) for _, srv := range servers { - srv.RegisterHandler(mock.TestMethod, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(mock.TestMethod, func(_ gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) return gorums.NewResponseMessage(in, pb.String("echo: "+req.GetValue())), nil }) @@ -593,7 +593,7 @@ func TestServerLocalDispatchContentionSlowReplica(t *testing.T) { for i, srv := range servers { if i == 0 { // Server 0 (self-node): block until signaled. - srv.RegisterHandler(mock.TestMethod, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(mock.TestMethod, func(_ gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { <-blocker req := gorums.AsProto[*pb.StringValue](in) return gorums.NewResponseMessage(in, pb.String("echo: "+req.GetValue())), nil diff --git a/server_test.go b/server_test.go index 021100f0..849140f3 100644 --- a/server_test.go +++ b/server_test.go @@ -41,7 +41,7 @@ func TestServerCallback(t *testing.T) { } func appendStringInterceptor(inStr, outStr string) gorums.Interceptor { - return func(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { + return func(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) // update the underlying request gorums.Message's message field (pb.StringValue in this case) req.Value += inStr @@ -65,7 +65,7 @@ func appendStringInterceptor(inStr, outStr string) gorums.Interceptor { type interceptorSrv struct{} -func (interceptorSrv) Test(_ gorums.ServerCtx, req *pb.StringValue) (*pb.StringValue, error) { +func (interceptorSrv) Test(_ gorums.ServerContext, req *pb.StringValue) (*pb.StringValue, error) { return pb.String(req.GetValue() + "server-"), nil } @@ -78,7 +78,7 @@ func TestServerInterceptorsChain(t *testing.T) { appendStringInterceptor("i2in-", "i2out-"), )) // register final handler which appends "final-" to the request value - s.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + s.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) resp, err := interceptorSrv.Test(ctx, req) if err != nil { @@ -148,7 +148,7 @@ func TestWithBufferSizesProcessesRequests(t *testing.T) { // underlying TCP connection is broken. func TestTCPReconnection(t *testing.T) { srv := gorums.NewServer() - srv.RegisterHandler(mock.TestMethod, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler(mock.TestMethod, func(_ gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) return gorums.NewResponseMessage(in, req), nil }) @@ -202,7 +202,7 @@ func TestTCPReconnection(t *testing.T) { } srv2 := gorums.NewServer() - srv2.RegisterHandler(mock.TestMethod, func(_ gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv2.RegisterHandler(mock.TestMethod, func(_ gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) return gorums.NewResponseMessage(in, req), nil }) From bb46d5bcfa111c42c6ae2644276e2e3c9ff21ea9 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:18:22 +0200 Subject: [PATCH 04/16] gorums: rename NodeListOption to NodeSource The type is not an option. It is a value that produces the nodes of a configuration, implemented by WithNodes for a map and WithNodeList for an address slice, and it is a required argument to NewConfig and WithPeers rather than an optional one. Calling it an Option put it in the same category as DialOption and ServerOption, which are functional options that may be omitted. NodeSource says what it is: the source the configuration draws its nodes from. --- config.go | 6 +++--- config_opts.go | 12 ++++++------ config_test.go | 4 ++-- examples/storage/server.go | 2 +- gorumstest/gorumstest.go | 2 +- gorumstest/options.go | 16 ++++++++-------- inbound_manager.go | 6 +++--- inbound_manager_test.go | 12 ++++++------ local_servers.go | 4 ++-- server.go | 12 ++++++------ 10 files changed, 38 insertions(+), 38 deletions(-) diff --git a/config.go b/config.go index 2d95ab83..cd60ca1b 100644 --- a/config.go +++ b/config.go @@ -51,7 +51,7 @@ func (c Config) Context(parent context.Context) *ConfigContext { // gorums.WithNodeList([]string{"localhost:8080", "localhost:8081", "localhost:8082"}), // gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), // ) -func NewConfig(nodes NodeListOption, opts ...DialOption) (Config, error) { +func NewConfig(nodes NodeSource, opts ...DialOption) (Config, error) { if nodes == nil { return nil, fmt.Errorf("gorums: missing required node list") } @@ -64,8 +64,8 @@ func NewConfig(nodes NodeListOption, opts ...DialOption) (Config, error) { return cfg, nil } -// Extend returns a new Config combining c with new nodes from the provided NodeListOption. -func (c Config) Extend(opt NodeListOption) (Config, error) { +// Extend returns a new Config combining c with new nodes from the provided NodeSource. +func (c Config) Extend(opt NodeSource) (Config, error) { if len(c) == 0 { return nil, fmt.Errorf("gorums: cannot extend empty configuration") } diff --git a/config_opts.go b/config_opts.go index 0cbff232..50463e53 100644 --- a/config_opts.go +++ b/config_opts.go @@ -7,9 +7,9 @@ import ( "slices" ) -// NodeListOption must be implemented by node providers. It is used by both the +// NodeSource must be implemented by node providers. It is used by both the // Manager (outbound) and by inboundManager (inbound) via newConfig. -type NodeListOption interface { +type NodeSource interface { newConfig(nodeRegistry) (Config, error) } @@ -25,10 +25,10 @@ type NodeAddress interface { Addr() string } -// WithNodes returns a NodeListOption containing the provided mapping from +// WithNodes returns a NodeSource containing the provided mapping from // application-specific IDs to types implementing NodeAddress. // Node IDs must be greater than 0. -func WithNodes[T NodeAddress](nodes map[uint32]T) NodeListOption { +func WithNodes[T NodeAddress](nodes map[uint32]T) NodeSource { return nodeMap[T](nodes) } @@ -49,11 +49,11 @@ func (nm nodeMap[T]) newConfig(registry nodeRegistry) (Config, error) { return builder.configuration(), nil } -// WithNodeList returns a NodeListOption for the provided list of node addresses. +// WithNodeList returns a NodeSource for the provided list of node addresses. // Unique Node IDs are generated sequentially starting from the maximum existing // node ID plus one, or from 1 if no nodes exist, preventing conflicts with // existing nodes. -func WithNodeList(addrsList []string) NodeListOption { +func WithNodeList(addrsList []string) NodeSource { return nodeList(addrsList) } diff --git a/config_test.go b/config_test.go index a7f508e3..93fbee7f 100644 --- a/config_test.go +++ b/config_test.go @@ -32,7 +32,7 @@ func (n testNode) Addr() string { func TestNewConfig(t *testing.T) { tests := []struct { name string - opt gorums.NodeListOption + opt gorums.NodeSource wantSize int wantErr string }{ @@ -335,7 +335,7 @@ func TestConfigurationExtend(t *testing.T) { tests := []struct { name string initialNodes []string - extendOpt gorums.NodeListOption + extendOpt gorums.NodeSource wantSize int wantErr string }{ diff --git a/examples/storage/server.go b/examples/storage/server.go index c2ec481f..1ad975a4 100644 --- a/examples/storage/server.go +++ b/examples/storage/server.go @@ -100,7 +100,7 @@ func runLocalCluster(srvOpts gorums.ServerOption) ([]string, func(), error) { // assignment regardless of the order of addresses in the input list. // The server's own address must be included in the peer list. // It returns an error if the server's address is not found in the peer list. -func peerConfig(address string, peers []string) (uint32, gorums.NodeListOption, error) { +func peerConfig(address string, peers []string) (uint32, gorums.NodeSource, error) { sorted := slices.Clone(peers) slices.Sort(sorted) idx := slices.Index(sorted, address) diff --git a/gorumstest/gorumstest.go b/gorumstest/gorumstest.go index 590a4088..5d3b9ed5 100644 --- a/gorumstest/gorumstest.go +++ b/gorumstest/gorumstest.go @@ -119,7 +119,7 @@ func startServers(t testing.TB, numServers int, srvFn func(i int) gorums.ServerI // Optional [Option] values can be provided to customize the manager, server, or configuration. // // By default, nodes are assigned sequential IDs (1, 2, 3, ...) matching the server -// creation order. This can be overridden by providing a [gorums.NodeListOption]. +// creation order. This can be overridden by providing a [gorums.NodeSource]. // // This is the recommended way to set up tests that need both servers and a configuration. // It ensures proper cleanup and detects goroutine leaks. diff --git a/gorumstest/options.go b/gorumstest/options.go index ca431b76..1dd4a03a 100644 --- a/gorumstest/options.go +++ b/gorumstest/options.go @@ -7,18 +7,18 @@ import ( ) // Option is a marker interface that can hold a [gorums.DialOption], -// [gorums.ServerOption], or [gorums.NodeListOption]. This allows test helpers +// [gorums.ServerOption], or [gorums.NodeSource]. This allows test helpers // to accept a single variadic parameter that can be filtered and passed to the // appropriate constructors: [gorums.NewServer] or [gorums.NewConfig]. // // Each option type (gorums.DialOption, gorums.ServerOption, -// gorums.NodeListOption) satisfies this interface already, since it is just an +// gorums.NodeSource) satisfies this interface already, since it is just an // alias for any, so they can be passed directly without wrapping: // // gorumstest.Config(t, 3, nil, // gorums.WithBackoff(...), // DialOption // gorums.WithBufferSizes(10, 10), // ServerOption -// gorums.WithNodes(...), // NodeListOption +// gorums.WithNodes(...), // NodeSource // ) type Option any @@ -26,7 +26,7 @@ type Option any type testOptions struct { managerOpts []gorums.DialOption serverOpts []gorums.ServerOption - nodeListOpts []gorums.NodeListOption + nodeListOpts []gorums.NodeSource 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) @@ -56,11 +56,11 @@ func (to *testOptions) serverFunc(srvFn func(i int) gorums.ServerIface) func(i i return srvFn } -// nodeListOption returns the appropriate NodeListOption for the configuration. +// nodeListOption returns the appropriate NodeSource for the configuration. // It uses provided options if available, otherwise defaults to WithNodeList. -func (to *testOptions) nodeListOption(addrs []string) gorums.NodeListOption { +func (to *testOptions) nodeListOption(addrs []string) gorums.NodeSource { if len(to.nodeListOpts) > 0 { - // Use the last provided NodeListOption (allows overriding) + // Use the last provided NodeSource (allows overriding) return to.nodeListOpts[len(to.nodeListOpts)-1] } // Default: use WithNodeList which generates unique IDs based on max(manager.NodeIDs()) + 1 @@ -76,7 +76,7 @@ func extractTestOptions(opts []Option) testOptions { result.managerOpts = append(result.managerOpts, o) case gorums.ServerOption: result.serverOpts = append(result.serverOpts, o) - case gorums.NodeListOption: + case gorums.NodeSource: result.nodeListOpts = append(result.nodeListOpts, o) case stopFuncProvider: result.stopFuncPtr = o.stopFunc diff --git a/inbound_manager.go b/inbound_manager.go index 42788830..6f4e6853 100644 --- a/inbound_manager.go +++ b/inbound_manager.go @@ -94,14 +94,14 @@ type inboundManager struct { const clientIDStart = 1 << 20 // newInboundManager creates an inboundManager for this server whose NodeID is myID. -// If opt is non-nil, the inboundManager is configured with the given NodeListOption -// defining the set of known peers. If myID is present in the NodeListOption it is +// If opt is non-nil, the inboundManager is configured with the given NodeSource +// defining the set of known peers. If myID is present in the NodeSource it is // immediately included in the Config as the self-node, so that quorum thresholds // account for the local replica from the moment of construction. The handler is // installed on the self-node (if present) to enable in-process dispatch without // a network round-trip. Panics on configuration errors (invalid addresses, // duplicate nodes, etc.) -func newInboundManager(myID uint32, opt NodeListOption, sendBuffer uint, onConfigChange func(Config), handler stream.RequestHandler) *inboundManager { +func newInboundManager(myID uint32, opt NodeSource, sendBuffer uint, onConfigChange func(Config), handler stream.RequestHandler) *inboundManager { im := &inboundManager{ myID: myID, knownNodes: make(map[uint32]*Node), diff --git a/inbound_manager_test.go b/inbound_manager_test.go index c1e0c216..d15e3dcb 100644 --- a/inbound_manager_test.go +++ b/inbound_manager_test.go @@ -75,10 +75,10 @@ type testNode struct { func (n testNode) Addr() string { return n.addr } -// Compile-time assertions: both node providers satisfy NodeListOption. +// Compile-time assertions: both node providers satisfy NodeSource. var ( - _ NodeListOption = nodeMap[testNode](nil) - _ NodeListOption = nodeList(nil) + _ NodeSource = nodeMap[testNode](nil) + _ NodeSource = nodeList(nil) ) // mockBidiStream is a minimal stream.BidiStream for testing inboundManager. @@ -132,7 +132,7 @@ func newTestInboundManager(t *testing.T, myID uint32) *inboundManager { func TestNewInboundManager(t *testing.T) { tests := []struct { name string - opt NodeListOption + opt NodeSource wantIDs []uint32 wantCfgIDs []uint32 // expected Config IDs after construction wantPanic string // if non-empty, expect panic containing this substring @@ -628,8 +628,8 @@ func equalNodeIDs(ids []uint32) func(Config) bool { } } -// peerNodes creates the peer NodeListOption used by the E2E tests. -func peerNodes() NodeListOption { +// peerNodes creates the peer NodeSource used by the E2E tests. +func peerNodes() NodeSource { return WithNodes(map[uint32]testNode{ 1: {"127.0.0.1:9001"}, 2: {"127.0.0.1:9002"}, diff --git a/local_servers.go b/local_servers.go index 569db712..84892a47 100644 --- a/local_servers.go +++ b/local_servers.go @@ -75,10 +75,10 @@ func NewLocalServers(n int, opts ...LocalServerOption) ([]*Server, func(), error } // allocateListeners pre-allocates n TCP listeners on random localhost ports and -// returns them along with a [NodeListOption] containing their addresses. If any +// returns them along with a [NodeSource] containing their addresses. If any // listener fails to open, all previously opened listeners are closed before // returning the error. -func allocateListeners(n int) ([]net.Listener, NodeListOption, error) { +func allocateListeners(n int) ([]net.Listener, NodeSource, error) { listeners := make([]net.Listener, n) addrs := make([]string, n) for i := range n { diff --git a/server.go b/server.go index 18977ebf..c9ac3ffa 100644 --- a/server.go +++ b/server.go @@ -21,10 +21,10 @@ type serverOptions struct { interceptors []Interceptor // Peer management options myID uint32 - peerNodes NodeListOption // Peers to track as they connect; set by WithPeers. - onConfigChange func(Config) // Callback registered via WithPeerChange. - listenAddr string // Listener address recorded by WithAddr; bound by ListenAndServe. - outboundNodes NodeListOption // Nodes this server calls; set by WithPeers. + peerNodes NodeSource // Peers to track as they connect; set by WithPeers. + onConfigChange func(Config) // Callback registered via WithPeerChange. + listenAddr string // Listener address recorded by WithAddr; bound by ListenAndServe. + outboundNodes NodeSource // Nodes this server calls; set by WithPeers. outboundDialOpts []DialOption } @@ -92,7 +92,7 @@ func WithInterceptors(i ...Interceptor) ServerOption { // The returned option only records the peer set; the [NewServer] call that // receives it panics if the node source is invalid, for example if it contains // a duplicate or malformed address. -func WithPeers(myID uint32, nodes NodeListOption, opts ...DialOption) ServerOption { +func WithPeers(myID uint32, nodes NodeSource, opts ...DialOption) ServerOption { return func(o *serverOptions) { o.myID = myID o.peerNodes = nodes @@ -186,7 +186,7 @@ func NewServer(opts ...ServerOption) *Server { // newPeerConfig builds the outbound [Config] this server uses to call // other servers. It installs the server as the back-channel request handler so // the remote can dispatch requests back over the same connection. -func (s *Server) newPeerConfig(nodes NodeListOption, dialOpts []DialOption) (Config, error) { +func (s *Server) newPeerConfig(nodes NodeSource, dialOpts []DialOption) (Config, error) { opts := append([]DialOption{withServer(s)}, dialOpts...) return NewConfig(nodes, opts...) } From e9dd8fe79ec1e96e4c1572f5e2d2bf99aba8a117 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:19:02 +0200 Subject: [PATCH 05/16] gorums: name the dial and interceptor options for what they configure WithDialOptions took grpc.DialOptions and returned a gorums.DialOption, so a reader had to know which of the two "dial options" was meant at each call site. It becomes WithGRPCDialOptions, which names the options it forwards. Interceptor and WithInterceptors are server-side only, but nothing in the names said so, and a client-side interceptor type exists on the call path. They become ServerInterceptor and WithServerInterceptors. --- callopts_test.go | 2 +- config.go | 2 +- doc/migration.md | 6 ++--- doc/user-guide.md | 28 ++++++++++---------- examples/interceptors/server_interceptors.go | 2 +- examples/storage/client.go | 2 +- examples/storage/main.go | 4 +-- examples/storage/server.go | 4 +-- gorumstest/gorumstest.go | 4 +-- handler.go | 6 ++--- inbound_manager_test.go | 2 +- internal/tests/tls/tls_test.go | 2 +- opts.go | 4 +-- server.go | 8 +++--- server_test.go | 4 +-- 15 files changed, 40 insertions(+), 40 deletions(-) diff --git a/callopts_test.go b/callopts_test.go index 187e3da0..ff2bb9d5 100644 --- a/callopts_test.go +++ b/callopts_test.go @@ -24,7 +24,7 @@ func testLocalServers(t testing.TB, n int) []*Server { t.Cleanup(func() { goleak.VerifyNone(t) }) } srvs, stop, err := NewLocalServers(n, WithLocalDialOptions( - WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), + WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), )) if err != nil { t.Fatal(err) diff --git a/config.go b/config.go index cd60ca1b..6ae42b48 100644 --- a/config.go +++ b/config.go @@ -49,7 +49,7 @@ func (c Config) Context(parent context.Context) *ConfigContext { // // cfg, err := NewConfig( // gorums.WithNodeList([]string{"localhost:8080", "localhost:8081", "localhost:8082"}), -// gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), +// gorums.WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), // ) func NewConfig(nodes NodeSource, opts ...DialOption) (Config, error) { if nodes == nil { diff --git a/doc/migration.md b/doc/migration.md index 01a9a117..3ee6f399 100644 --- a/doc/migration.md +++ b/doc/migration.md @@ -107,7 +107,7 @@ Remove QuorumSpec from configuration creation. **Before:** ```go -mgr := NewManager(gorums.WithDialOptions(...)) +mgr := NewManager(gorums.WithGRPCDialOptions(...)) cfg, err := mgr.NewConfiguration( &QSpec{quorumSize: 2}, // ❌ Remove QuorumSpec gorums.WithNodeList(addrs), @@ -117,7 +117,7 @@ cfg, err := mgr.NewConfiguration( **After:** ```go -mgr := gorums.NewManager(gorums.WithDialOptions(...)) +mgr := gorums.NewManager(gorums.WithGRPCDialOptions(...)) cfg, err := gorums.NewConfiguration(mgr, gorums.WithNodeList(addrs)) ``` @@ -126,7 +126,7 @@ Or use the convenience function that creates both: ```go cfg, err := gorums.NewConfig( gorums.WithNodeList(addrs), - gorums.WithDialOptions(...), + gorums.WithGRPCDialOptions(...), ) ``` diff --git a/doc/user-guide.md b/doc/user-guide.md index a524b09a..a44aa987 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -356,7 +356,7 @@ func ExampleStorageClient() { // Create a configuration including all nodes allNodesConfig, err := gorums.NewConfig( gorums.WithNodeList(addrs), - gorums.WithDialOptions( + gorums.WithGRPCDialOptions( grpc.WithTransportCredentials(insecure.NewCredentials()), ), ) @@ -653,7 +653,7 @@ func ExampleStorageClient() { // Create a configuration with all nodes config, err := gorums.NewConfig( gorums.WithNodeList(addrs), - gorums.WithDialOptions( + gorums.WithGRPCDialOptions( grpc.WithTransportCredentials(insecure.NewCredentials()), ), ) @@ -864,7 +864,7 @@ func RequireAllSuccess(resp *gorums.Responses[*Response]) (*Response, error) { Gorums provides interceptors to transform requests and responses on a per-node basis. Interceptors are passed as call options and can be chained together. -### MapRequest Interceptor +### MapRequest ServerInterceptor Transform requests before sending to each node: @@ -880,7 +880,7 @@ resp, err := WriteQC(cfgCtx, req, ).Majority() ``` -### MapResponse Interceptor +### MapResponse ServerInterceptor Transform responses received from each node: @@ -950,7 +950,7 @@ resp, err := ReadQC(cfgCtx, req, ).Majority() ``` -#### Example: Logging Interceptor +#### Example: Logging ServerInterceptor Create a logging interceptor that wraps the response iterator: @@ -987,7 +987,7 @@ resp, err := ReadQC(cfgCtx, req, ).Majority() ``` -#### Example: Response Filtering Interceptor +#### Example: Response Filtering ServerInterceptor Filter out responses that don't meet certain criteria: @@ -1021,7 +1021,7 @@ resp, err := ReadQC(cfgCtx, req, ).Majority() ``` -#### Example: Counting Interceptor +#### Example: Counting ServerInterceptor Count responses passing through the interceptor: @@ -1047,20 +1047,20 @@ func CountingInterceptor[Req, Resp proto.Message]( ### Server-Side Interceptors Gorums also supports server-side interceptors that wrap inbound RPC handlers, similar to gRPC server interceptors. -A server-side interceptor implements the `gorums.Interceptor` signature: +A server-side interceptor implements the `gorums.ServerInterceptor` signature: ```go -type Interceptor func(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) +type ServerInterceptor func(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) ``` You can pass multiple interceptors when starting a Gorums server. They can perform logging, latency injection, metadata insertion, and request validation before sending the request to the handler. Below are several examples based on the `examples/interceptors` package. -#### Server-Side Logging Interceptor +#### Server-Side Logging ServerInterceptor ```go -func LoggingInterceptor(addr string) gorums.Interceptor { +func LoggingInterceptor(addr string) gorums.ServerInterceptor { return func(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { req := gorums.AsProto[proto.Message](in) log.Printf("[%s]: LoggingInterceptor(incoming): Method=%s, Message=%s", addr, in.GetMethod(), req) @@ -1164,7 +1164,7 @@ The connecting client attaches the metadata with `WithMetadata`: config, err := gorums.NewConfig( gorums.WithNodeList(addrs), gorums.WithMetadata(metadata.New(map[string]string{"client-id": "replica-3"})), - gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), + gorums.WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), ) ``` @@ -1419,7 +1419,7 @@ func ExampleConfigClient() { // Create base configuration c1 from addrs, giving |c1| = 3. c1, err := gorums.NewConfig( gorums.WithNodeList(addrs), - gorums.WithDialOptions( + gorums.WithGRPCDialOptions( grpc.WithTransportCredentials(insecure.NewCredentials()), ), ) @@ -1899,7 +1899,7 @@ clientSrv.RegisterHandler(pb.MyMethod, myHandler) config, err := gorums.NewConfig( gorums.WithNodeList(serverAddrs), gorums.WithBackChannel(clientSrv), - gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), + gorums.WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), ) ``` diff --git a/examples/interceptors/server_interceptors.go b/examples/interceptors/server_interceptors.go index d2f0b0d3..9b3b1f17 100644 --- a/examples/interceptors/server_interceptors.go +++ b/examples/interceptors/server_interceptors.go @@ -10,7 +10,7 @@ import ( "google.golang.org/protobuf/proto" ) -func LoggingInterceptor(addr string) gorums.Interceptor { +func LoggingInterceptor(addr string) gorums.ServerInterceptor { return func(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { req := gorums.AsProto[proto.Message](in) log.Printf("[%s]: LoggingInterceptor(incoming): Method=%s, Message=%s", addr, in.GetMethod(), req) diff --git a/examples/storage/client.go b/examples/storage/client.go index 0314e6f6..d8307d1b 100644 --- a/examples/storage/client.go +++ b/examples/storage/client.go @@ -15,7 +15,7 @@ func runClient(addresses []string) error { } cfg, err := gorums.NewConfig( gorums.WithNodeList(addresses), - gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), + gorums.WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), ) if err != nil { return err diff --git a/examples/storage/main.go b/examples/storage/main.go index 5639a2f4..c8bca3e3 100644 --- a/examples/storage/main.go +++ b/examples/storage/main.go @@ -73,7 +73,7 @@ func parseInterceptors(ic string) gorums.ServerOption { if ic == "" { return nil } - var ics []gorums.Interceptor + var ics []gorums.ServerInterceptor for name := range strings.SplitSeq(ic, ",") { switch strings.TrimSpace(name) { case "logging": @@ -88,5 +88,5 @@ func parseInterceptors(ic string) gorums.ServerOption { log.Fatalf("Unknown interceptor: %s", name) } } - return gorums.WithInterceptors(ics...) + return gorums.WithServerInterceptors(ics...) } diff --git a/examples/storage/server.go b/examples/storage/server.go index 1ad975a4..acac58bb 100644 --- a/examples/storage/server.go +++ b/examples/storage/server.go @@ -31,7 +31,7 @@ func runServer(address string, peers []string, srvOpt gorums.ServerOption) error if err != nil { return err } - insecureDial := gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())) + insecureDial := gorums.WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())) srv := gorums.NewServer( gorums.WithAddr(address), gorums.WithPeers(myID, peerList, insecureDial), @@ -63,7 +63,7 @@ func runServer(address string, peers []string, srvOpt gorums.ServerOption) error // It returns the server addresses and a stop function. The caller must // call stop when the cluster is no longer needed. func runLocalCluster(srvOpts gorums.ServerOption) ([]string, func(), error) { - dialOpts := gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())) + dialOpts := gorums.WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())) servers, stop, err := gorums.NewLocalServers(4, gorums.WithLocalServerOptions(srvOpts), gorums.WithLocalDialOptions(dialOpts), diff --git a/gorumstest/gorumstest.go b/gorumstest/gorumstest.go index 5d3b9ed5..babb1e6e 100644 --- a/gorumstest/gorumstest.go +++ b/gorumstest/gorumstest.go @@ -89,7 +89,7 @@ func Collect[T any](t testing.TB, timeout time.Duration, want int, ch <-chan T) // InsecureDialOptions returns a [gorums.DialOption] with insecure transport // credentials for testing. func InsecureDialOptions(_ testing.TB) gorums.DialOption { - return gorums.WithDialOptions( + return gorums.WithGRPCDialOptions( grpc.WithTransportCredentials(insecure.NewCredentials()), ) } @@ -99,7 +99,7 @@ func InsecureDialOptions(_ testing.TB) gorums.DialOption { // bufconn dialer in the default build, or insecure real-network credentials // under the integration build tag. func DialOptions(t testing.TB) gorums.DialOption { - return gorums.WithDialOptions(servers.DialOptions(t)...) + return gorums.WithGRPCDialOptions(servers.DialOptions(t)...) } // startServers starts numServers servers via srvFn, adapting srvFn's diff --git a/handler.go b/handler.go index 97a3fccf..334c9adc 100644 --- a/handler.go +++ b/handler.go @@ -26,10 +26,10 @@ type MetadataEntry_builder = stream.MetadataEntry_builder type ( // Handler processes a request and returns a response. Handler func(ServerContext, *Message) (*Message, error) - // Interceptor intercepts and may modify incoming requests and outgoing responses. + // ServerInterceptor intercepts and may modify incoming requests and outgoing responses. // It receives a ServerContext, the incoming Message, and a Handler representing // the next element in the chain. It returns a Message and an error. - Interceptor func(ServerContext, *Message, Handler) (*Message, error) + ServerInterceptor func(ServerContext, *Message, Handler) (*Message, error) ) // ServerContext is a context that is passed from the Gorums server to the handler. @@ -167,7 +167,7 @@ func AsProto[T proto.Message](msg *Message) T { // returns a Handler that executes the chain. The execution order is the same as the // order of the interceptors in the slice: the first element is executed first, and // the last element calls the final handler (the server method). -func chainInterceptors(final Handler, interceptors ...Interceptor) Handler { +func chainInterceptors(final Handler, interceptors ...ServerInterceptor) Handler { if len(interceptors) == 0 { return final } diff --git a/inbound_manager_test.go b/inbound_manager_test.go index d15e3dcb..1c0bb039 100644 --- a/inbound_manager_test.go +++ b/inbound_manager_test.go @@ -46,7 +46,7 @@ func testStartServers(t testing.TB, numServers int, srvFn func(i int) ServerIfac // testDialOptions returns a DialOption for connecting to servers started by // testStartServers. func testDialOptions(t testing.TB) DialOption { - return WithDialOptions(servers.DialOptions(t)...) + return WithGRPCDialOptions(servers.DialOptions(t)...) } // testCloser returns a cleanup function that closes the given io.Closer. diff --git a/internal/tests/tls/tls_test.go b/internal/tests/tls/tls_test.go index f80f5dcb..a05a1d5e 100644 --- a/internal/tests/tls/tls_test.go +++ b/internal/tests/tls/tls_test.go @@ -44,7 +44,7 @@ func TestTLSConnection(t *testing.T) { RegisterTLSServer(srv, &testSrv{}) return srv } - node := gorumstest.Node(t, srvFn, gorums.WithDialOptions( + node := gorumstest.Node(t, srvFn, gorums.WithGRPCDialOptions( grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(cp, "")), )) diff --git a/opts.go b/opts.go index cfa62cbc..5c33b0fd 100644 --- a/opts.go +++ b/opts.go @@ -40,9 +40,9 @@ func newDialOptions() dialOptions { } } -// WithDialOptions returns a DialOption which sets any gRPC dial options +// WithGRPCDialOptions returns a DialOption which sets any gRPC dial options // the client should use when initially connecting to each node in its pool. -func WithDialOptions(opts ...grpc.DialOption) DialOption { +func WithGRPCDialOptions(opts ...grpc.DialOption) DialOption { return func(o *dialOptions) { o.grpcDialOpts = append(o.grpcDialOpts, opts...) } diff --git a/server.go b/server.go index c9ac3ffa..b5316f9c 100644 --- a/server.go +++ b/server.go @@ -18,7 +18,7 @@ type serverOptions struct { sendBufferSize uint grpcOpts []grpc.ServerOption connectCallback func(context.Context) - interceptors []Interceptor + interceptors []ServerInterceptor // Peer management options myID uint32 peerNodes NodeSource // Peers to track as they connect; set by WithPeers. @@ -67,13 +67,13 @@ func WithConnectCallback(callback func(context.Context)) ServerOption { } } -// WithInterceptors registers server-side interceptors to run for every incoming request. +// WithServerInterceptors registers server-side interceptors to run for every incoming request. // Interceptors are executed for each registered handler. Interceptors may modify both // the request and/or response messages, or perform additional actions before or after // calling the next handler in the chain. Interceptors are executed in the order they are // provided: the first element is executed first, and the last element calls the actual // server method handler. -func WithInterceptors(i ...Interceptor) ServerOption { +func WithServerInterceptors(i ...ServerInterceptor) ServerOption { return func(opts *serverOptions) { opts.interceptors = append(opts.interceptors, i...) } @@ -125,7 +125,7 @@ type Server struct { srv *stream.Server grpcServer *grpc.Server handlers map[string]Handler - interceptors []Interceptor + interceptors []ServerInterceptor mu sync.Mutex // guards lis lis net.Listener // active listener; set by Serve, ListenAndServe, or NewLocalServers diff --git a/server_test.go b/server_test.go index 849140f3..91e6ec21 100644 --- a/server_test.go +++ b/server_test.go @@ -40,7 +40,7 @@ func TestServerCallback(t *testing.T) { } } -func appendStringInterceptor(inStr, outStr string) gorums.Interceptor { +func appendStringInterceptor(inStr, outStr string) gorums.ServerInterceptor { return func(ctx gorums.ServerContext, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { req := gorums.AsProto[*pb.StringValue](in) // update the underlying request gorums.Message's message field (pb.StringValue in this case) @@ -73,7 +73,7 @@ func TestServerInterceptorsChain(t *testing.T) { // set up a server with two interceptors: i1, i2 interceptorServerFn := func(_ int) gorums.ServerIface { interceptorSrv := &interceptorSrv{} - s := gorums.NewServer(gorums.WithInterceptors( + s := gorums.NewServer(gorums.WithServerInterceptors( appendStringInterceptor("i1in-", "i1out"), appendStringInterceptor("i2in-", "i2out-"), )) From 03a586b81888acc1ffea638c06fade8096118c9b Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:19:24 +0200 Subject: [PATCH 06/16] gorums: rename QuorumCallError.NodeErrors to NumErrors NodeErrors returns a count, not the errors. A caller reading err.NodeErrors() would reasonably expect a slice of node errors, which is what Unwrap provides. NumErrors states that it is a count. --- doc/user-guide.md | 4 ++-- errors.go | 4 ++-- errors_test.go | 4 ++-- quorumcall_test.go | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/user-guide.md b/doc/user-guide.md index a44aa987..09760a86 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -1283,7 +1283,7 @@ A `QuorumCallError` is returned when a quorum call fails. It provides the following methods: * **`Cause() error`** - Returns the underlying cause of the failure (e.g., `ErrIncomplete`, `ErrSendFailure`) -* **`NodeErrors() int`** - Returns the number of nodes that failed +* **`NumErrors() int`** - Returns the number of nodes that failed * **`Unwrap() []error`** - Supports error unwrapping for use with `errors.Is` and `errors.As` The error implements Go's standard error unwrapping interface, allowing `errors.Is()` and `errors.As()` to check both the direct cause and any wrapped node-specific errors. @@ -1312,7 +1312,7 @@ func handleQuorumCall(config *gorums.Config, req *ReadRequest) { var qcErr gorums.QuorumCallError if errors.As(err, &qcErr) { log.Printf("Quorum call failed: %v", qcErr.Cause()) - log.Printf("Failed nodes: %d", qcErr.NodeErrors()) + log.Printf("Failed nodes: %d", qcErr.NumErrors()) // Handle specific cause types if errors.Is(err, gorums.ErrIncomplete) { diff --git a/errors.go b/errors.go index 8c2ddc9e..c1cc2ff2 100644 --- a/errors.go +++ b/errors.go @@ -56,8 +56,8 @@ func (e QuorumCallError) Cause() error { return e.cause } -// NodeErrors returns the number of nodes that failed during the quorum call. -func (e QuorumCallError) NodeErrors() int { +// NumErrors returns the number of nodes that failed during the quorum call. +func (e QuorumCallError) NumErrors() int { return len(e.errors) } diff --git a/errors_test.go b/errors_test.go index 5b5f3765..d3a35e07 100644 --- a/errors_test.go +++ b/errors_test.go @@ -126,8 +126,8 @@ func TestQuorumCallErrorAccessors(t *testing.T) { if got := tt.qcErr.Cause(); got != tt.wantCause { t.Errorf("QuorumCallError.Cause() = %v, want %v", got, tt.wantCause) } - if got := tt.qcErr.NodeErrors(); got != tt.wantNodeErrors { - t.Errorf("QuorumCallError.NodeErrors() = %d, want %d", got, tt.wantNodeErrors) + if got := tt.qcErr.NumErrors(); got != tt.wantNodeErrors { + t.Errorf("QuorumCallError.NumErrors() = %d, want %d", got, tt.wantNodeErrors) } }) } diff --git a/quorumcall_test.go b/quorumcall_test.go index 707d43f9..16c8c6f4 100644 --- a/quorumcall_test.go +++ b/quorumcall_test.go @@ -34,8 +34,8 @@ func checkQuorumCall(t *testing.T, gotErr, wantErr error, expectedNodeErrors ... // Validate QuorumCallError details if expectedNodeErrors provided if len(expectedNodeErrors) > 0 { var qcErr gorums.QuorumCallError - if errors.As(gotErr, &qcErr) && qcErr.NodeErrors() != expectedNodeErrors[0] { - t.Errorf("Expected %d node errors, got %d", expectedNodeErrors[0], qcErr.NodeErrors()) + if errors.As(gotErr, &qcErr) && qcErr.NumErrors() != expectedNodeErrors[0] { + t.Errorf("Expected %d node errors, got %d", expectedNodeErrors[0], qcErr.NumErrors()) return false } } From b144327ba657262fd7602e0921f34e5c2ef78c57 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:20:36 +0200 Subject: [PATCH 07/16] gorums: rename Config.SortBy to Sort and prefix its comparators with By SortBy read as though it took a key, not a comparison function; Sort matches slices.Sort and slices.SortFunc, which is what it delegates to. The comparators ID, Latency, and LastNodeError shared their names with the Node methods they read, so ID could mean either the comparator or the accessor depending on context, and a test sorting by latency had to write slices.SortFunc(nodes, Latency) next to node.Latency(). They become ByID, ByLatency, and ByLastError, which read correctly at the call site: c.Sort(ByLatency). --- config.go | 28 ++++++++++++++-------------- config_opts.go | 2 +- config_test.go | 26 +++++++++++++------------- doc/user-guide.md | 26 +++++++++++++------------- examples/storage/repl.go | 2 +- inbound_manager.go | 6 +++--- node.go | 16 ++++++++-------- node_test.go | 20 ++++++++++---------- 8 files changed, 63 insertions(+), 63 deletions(-) diff --git a/config.go b/config.go index 6ae42b48..32e9cc5e 100644 --- a/config.go +++ b/config.go @@ -156,7 +156,7 @@ func (c Config) Add(ids ...uint32) Config { } } } - slices.SortFunc(nodes, ID) + slices.SortFunc(nodes, ByID) return nodes } @@ -198,33 +198,33 @@ func (c Config) Difference(other Config) Config { return c.Remove(other.NodeIDs()...) } -// SortBy returns a new Config with nodes ordered by the given comparator. +// Sort returns a new Config with nodes ordered by the given comparator. // The original configuration is not modified. // -// Use this with the built-in node comparator functions [ID], [LastNodeError], -// and [Latency]: +// Use this with the built-in node comparator functions [ByID], [ByLastError], +// and [ByLatency]: // -// fastest := cfg.SortBy(gorums.Latency) // ascending by latency -// healthy := cfg.SortBy(gorums.LastNodeError) // no-error nodes first +// fastest := cfg.Sort(gorums.ByLatency) // ascending by latency +// healthy := cfg.Sort(gorums.ByLastError) // no-error nodes first // // Comparators can be combined for multi-key ordering: // -// cfg.SortBy(func(a, b *Node) int { -// if r := gorums.LastNodeError(a, b); r != 0 { +// cfg.Sort(func(a, b *Node) int { +// if r := gorums.ByLastError(a, b); r != 0 { // return r // } -// return gorums.Latency(a, b) +// return gorums.ByLatency(a, b) // }) // -// SortBy uses a stable sort, so nodes with equal comparator values retain +// Sort uses a stable sort, so nodes with equal comparator values retain // their original relative order. // // Note: quorum calls contact every node in the configuration regardless of // order. Sorting only affects which nodes are selected when the result is -// sliced to a smaller subset, e.g., cfg.SortBy(gorums.Latency)[:quorumSize]. +// sliced to a smaller subset, e.g., cfg.Sort(gorums.ByLatency)[:quorumSize]. // See the "Latency-Based Node Selection" section of the user guide for // guidance on sub-configuration sizing and re-sort frequency. -func (c Config) SortBy(cmp func(*Node, *Node) int) Config { +func (c Config) Sort(cmp func(*Node, *Node) int) Config { if len(c) == 0 { return nil } @@ -244,12 +244,12 @@ func (c Config) SortBy(cmp func(*Node, *Node) int) Config { // // // Latency-based top-k subset: // cfg.Watch(ctx, 5*time.Second, func(c gorums.Config) gorums.Config { -// return c.SortBy(gorums.Latency)[:quorumSize] +// return c.Sort(gorums.ByLatency)[:quorumSize] // }) // // // Skip failed nodes first, then pick fastest: // cfg.Watch(ctx, 5*time.Second, func(c gorums.Config) gorums.Config { -// return c.WithoutErrors(lastErr).SortBy(gorums.Latency)[:quorumSize] +// return c.WithoutErrors(lastErr).Sort(gorums.ByLatency)[:quorumSize] // }) // // The returned channel has a buffer of 1. If the consumer is slow and has not diff --git a/config_opts.go b/config_opts.go index 50463e53..07f7cc5a 100644 --- a/config_opts.go +++ b/config_opts.go @@ -140,7 +140,7 @@ func (b *nodeBuilder) add(id uint32, addr string) error { // configuration returns the built Config, sorted by ID. func (b *nodeBuilder) configuration() Config { - slices.SortFunc(b.nodes, ID) + slices.SortFunc(b.nodes, ByID) return b.nodes } diff --git a/config_test.go b/config_test.go index 93fbee7f..26e94c81 100644 --- a/config_test.go +++ b/config_test.go @@ -251,13 +251,13 @@ func TestConfigurationSortBy(t *testing.T) { t.Cleanup(gorumstest.Closer(t, cfg)) t.Run("SortByID", func(t *testing.T) { - sorted := cfg.SortBy(gorums.ID) + sorted := cfg.Sort(gorums.ByID) if sorted.Size() != cfg.Size() { t.Fatalf("sorted.Size() = %d, want %d", sorted.Size(), cfg.Size()) } for i := 1; i < sorted.Size(); i++ { if sorted[i].ID() < sorted[i-1].ID() { - t.Errorf("SortBy(ID): not sorted at position %d (id %d < id %d)", + t.Errorf("Sort(ID): not sorted at position %d (id %d < id %d)", i, sorted[i].ID(), sorted[i-1].ID()) } } @@ -270,7 +270,7 @@ func TestConfigurationSortBy(t *testing.T) { t.Fatalf("expected no latency measurement on fresh node, got %v", n.Latency()) } } - sorted := cfg.SortBy(gorums.Latency) + sorted := cfg.Sort(gorums.ByLatency) if sorted.Size() != cfg.Size() { t.Fatalf("sorted.Size() = %d, want %d", sorted.Size(), cfg.Size()) } @@ -278,36 +278,36 @@ func TestConfigurationSortBy(t *testing.T) { // Latency comparator returns 0 for every latency pair (the latencies are equal), // so a stable sort must preserve the original order. if got, want := sorted.NodeIDs(), cfg.NodeIDs(); !slices.Equal(got, want) { - t.Errorf("SortBy(Latency) with all-unmeasured nodes changed order: got %v, want %v", got, want) + t.Errorf("Sort(Latency) with all-unmeasured nodes changed order: got %v, want %v", got, want) } }) t.Run("ReturnsNewConfiguration", func(t *testing.T) { - sorted := cfg.SortBy(gorums.ID) + sorted := cfg.Sort(gorums.ByID) cfgSlice := cfg.Nodes() sortedSlice := sorted.Nodes() if len(cfgSlice) > 0 && len(sortedSlice) > 0 && &cfgSlice[0] == &sortedSlice[0] { - t.Error("SortBy returned same backing array — violates immutability") + t.Error("Sort returned same backing array — violates immutability") } }) t.Run("Empty/ReturnsNil", func(t *testing.T) { var empty gorums.Config - if got := empty.SortBy(gorums.ID); got != nil { - t.Fatalf("empty.SortBy(ID) = %v, want nil", got) + if got := empty.Sort(gorums.ByID); got != nil { + t.Fatalf("empty.Sort(ID) = %v, want nil", got) } - if got := empty.SortBy(gorums.Latency); got != nil { - t.Fatalf("empty.SortBy(Latency) = %v, want nil", got) + if got := empty.Sort(gorums.ByLatency); got != nil { + t.Fatalf("empty.Sort(Latency) = %v, want nil", got) } }) t.Run("SortByLastNodeErrorThenLatency", func(t *testing.T) { // Composing two comparators should not panic and must return a valid config. - sorted := cfg.SortBy(func(a, b *gorums.Node) int { - if r := gorums.LastNodeError(a, b); r != 0 { + sorted := cfg.Sort(func(a, b *gorums.Node) int { + if r := gorums.ByLastError(a, b); r != 0 { return r } - return gorums.Latency(a, b) + return gorums.ByLatency(a, b) }) if sorted.Size() != cfg.Size() { t.Fatalf("composed sort size = %d, want %d", sorted.Size(), cfg.Size()) diff --git a/doc/user-guide.md b/doc/user-guide.md index 09760a86..f396963e 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -1473,27 +1473,27 @@ sub-configurations, and what to watch out for when doing so. ### The Latency Comparator -`gorums.Latency` is a comparator function compatible with `slices.SortFunc` and `Config.SortBy`. +`gorums.ByLatency` is a comparator function compatible with `slices.SortFunc` and `Config.Sort`. The comparator orders nodes ascending by their current latency estimates; nodes without any measurements (freshly created, never sent traffic) are sorted last. ```go // Sort all nodes by ascending latency. -sorted := cfg.SortBy(gorums.Latency) +sorted := cfg.Sort(gorums.ByLatency) // Pick the two fastest nodes. -fast2 := cfg.SortBy(gorums.Latency)[:2] +fast2 := cfg.Sort(gorums.ByLatency)[:2] ``` Comparators can be chained for multi-key ordering. For example, healthy nodes first, then by latency within each group: ```go -sorted := cfg.SortBy(func(a, b *gorums.Node) int { - if r := gorums.LastNodeError(a, b); r != 0 { +sorted := cfg.Sort(func(a, b *gorums.Node) int { + if r := gorums.ByLastError(a, b); r != 0 { return r } - return gorums.Latency(a, b) + return gorums.ByLatency(a, b) }) ``` @@ -1508,7 +1508,7 @@ const f = 2 // tolerated failures (n = 2f+1) quorumSize := n/2 + 1 // simple majority for crash-fault tolerance = 3 // Re-derive the fast sub-configuration periodically (see guidance below). -fastCfg := allNodesCfg.SortBy(gorums.Latency)[:quorumSize] +fastCfg := allNodesCfg.Sort(gorums.ByLatency)[:quorumSize] fastCfgCtx := fastCfg.Context(ctx) reply, err := ReadQC(fastCfgCtx, &ReadRequest{Key: "x"}).Majority() @@ -1520,7 +1520,7 @@ first, then pick the fastest of those that remain: ```go var qcErr gorums.QuorumCallError if errors.As(err, &qcErr) { - fastCfg = cfg.WithoutErrors(qcErr).SortBy(gorums.Latency)[:quorumSize] + fastCfg = cfg.WithoutErrors(qcErr).Sort(gorums.ByLatency)[:quorumSize] } ``` @@ -1530,7 +1530,7 @@ if errors.As(err, &qcErr) { ### How Often to Re-Sort -`SortBy` returns a snapshot of the ordering at one point in time. +`Sort` returns a snapshot of the ordering at one point in time. Latency measurements change as network conditions shift; the snapshot does not auto-update. @@ -1540,7 +1540,7 @@ As a rule of thumb: A periodic goroutine or a lazy re-sort at the start of each request batch both work well. * **On every single call** is usually unnecessary and wastes allocations. - Each `SortBy` clones the node slice. + Each `Sort` clones the node slice. * **After a failed quorum call**, always re-evaluate: a node that caused the failure should be excluded via `WithoutErrors` before re-sorting. * **After a topology change** (node added or removed), derive the sub-configuration @@ -1550,7 +1550,7 @@ A simple periodic refresh pattern using `Config.Watch`: ```go updates := allNodesCfg.Watch(ctx, 5*time.Second, func(c gorums.Config) gorums.Config { - return c.SortBy(gorums.Latency)[:quorumSize] + return c.Sort(gorums.ByLatency)[:quorumSize] }) fastCfg := <-updates // initial snapshot, available before the first tick @@ -1587,7 +1587,7 @@ updates := allNodesCfg.Watch(ctx, 5*time.Second, func(c gorums.Config) gorums.Co mu.Lock() qcErr := lastQCErr mu.Unlock() - return c.WithoutErrors(qcErr).SortBy(gorums.Latency)[:quorumSize] + return c.WithoutErrors(qcErr).Sort(gorums.ByLatency)[:quorumSize] }) ``` @@ -1601,7 +1601,7 @@ on it: * **No traffic → no measurement.** The estimate is only updated on successful responses. A node that has never received a response returns a negative value. - `SortBy(gorums.Latency)` pushes such nodes to the end of the slice, so you + `Sort(gorums.ByLatency)` pushes such nodes to the end of the slice, so you will not accidentally pick an unmeasured node when slicing the front. * **Staleness.** If traffic to a node stops, the estimate holds its last diff --git a/examples/storage/repl.go b/examples/storage/repl.go index 802c7248..39beac70 100644 --- a/examples/storage/repl.go +++ b/examples/storage/repl.go @@ -443,7 +443,7 @@ func (r repl) parseConfiguration(cfgStr string) (pb.Config, error) { for _, i := range indices { nodes = append(nodes, cfgNodes[i]) } - slices.SortFunc(nodes, gorums.ID) + slices.SortFunc(nodes, gorums.ByID) return pb.Config(nodes), nil } diff --git a/inbound_manager.go b/inbound_manager.go index 6f4e6853..5ae65ddf 100644 --- a/inbound_manager.go +++ b/inbound_manager.go @@ -362,8 +362,8 @@ func (im *inboundManager) rebuildConfig() { clientCfg = append(clientCfg, node) } } - slices.SortFunc(inboundCfg, ID) - slices.SortFunc(clientCfg, ID) + slices.SortFunc(inboundCfg, ByID) + slices.SortFunc(clientCfg, ByID) im.inboundCfg = inboundCfg im.clientConfig = clientCfg @@ -375,7 +375,7 @@ func (im *inboundManager) rebuildConfig() { cfg = append(cfg, node) } } - slices.SortFunc(cfg, ID) + slices.SortFunc(cfg, ByID) } cfgChanged := !slices.Equal(im.config, cfg) im.config = cfg diff --git a/node.go b/node.go index 970510bb..39601c86 100644 --- a/node.go +++ b/node.go @@ -308,22 +308,22 @@ func (n *Node) LastErr() error { // - A step-change in latency takes several round trips to settle because // each new sample contributes only 20% of the new value. // -// Use the [Latency] comparator with [Config.SortBy] to order nodes +// Use the [ByLatency] comparator with [Config.Sort] to order nodes // by their current observed latency. func (n *Node) Latency() time.Duration { return n.router.Latency() } // ID compares nodes by their identifier in increasing order. -// It is compatible with [slices.SortFunc] and [Config.SortBy]. -var ID = func(a, b *Node) int { +// It is compatible with [slices.SortFunc] and [Config.Sort]. +var ByID = func(a, b *Node) int { return cmp.Compare(a.id, b.id) } -// LastNodeError compares nodes by their LastErr() status. +// ByLastError compares nodes by their LastErr() status. // Nodes with no error sort before nodes with an error. -// It is compatible with [slices.SortFunc] and [Config.SortBy]. -var LastNodeError = func(a, b *Node) int { +// It is compatible with [slices.SortFunc] and [Config.Sort]. +var ByLastError = func(a, b *Node) int { aErr := a.LastErr() bErr := b.LastErr() switch { @@ -338,8 +338,8 @@ var LastNodeError = func(a, b *Node) int { // Latency compares nodes by their current latency estimate in ascending order. // Nodes with no measurement yet (negative latency value) sort after nodes with a -// measurement. It is compatible with [slices.SortFunc] and [Config.SortBy]. -var Latency = func(a, b *Node) int { +// measurement. It is compatible with [slices.SortFunc] and [Config.Sort]. +var ByLatency = func(a, b *Node) int { la, lb := a.Latency(), b.Latency() // Note: cmp.Compare alone would sort negative sentinel values first // (as the smallest numbers), making unmeasured nodes appear fastest. diff --git a/node_test.go b/node_test.go index 4831bed6..9e71a82c 100644 --- a/node_test.go +++ b/node_test.go @@ -34,7 +34,7 @@ func TestNodeSort(t *testing.T) { t.Run("ByID", func(t *testing.T) { ns := slices.Clone(nodes) - slices.SortFunc(ns, ID) + slices.SortFunc(ns, ByID) for i := 1; i < len(ns); i++ { if ns[i].id < ns[i-1].id { t.Error("by id: not sorted") @@ -45,7 +45,7 @@ func TestNodeSort(t *testing.T) { t.Run("ByLastNodeError", func(t *testing.T) { ns := slices.Clone(nodes) - slices.SortFunc(ns, LastNodeError) + slices.SortFunc(ns, ByLastError) for i := 1; i < len(ns); i++ { if ns[i].LastErr() == nil && ns[i-1].LastErr() != nil { t.Error("by error: not sorted") @@ -57,10 +57,10 @@ func TestNodeSort(t *testing.T) { t.Run("ByLastNodeErrorThenID", func(t *testing.T) { ns := slices.Clone(nodes) slices.SortFunc(ns, func(a, b *Node) int { - if c := LastNodeError(a, b); c != 0 { + if c := ByLastError(a, b); c != 0 { return c } - return ID(a, b) + return ByID(a, b) }) // Expect: 42 (no err), 100 (no err), 99 (err), 101 (err). wantIDs := []uint32{42, 100, 99, 101} @@ -81,7 +81,7 @@ func TestNodeSort(t *testing.T) { makeNodeWithLatency(3, -1*time.Second), // no measurement makeNodeWithLatency(4, 20*time.Millisecond), } - slices.SortFunc(ns, Latency) + slices.SortFunc(ns, ByLatency) // Expected: 2 (10ms), 4 (20ms), 1 (30ms), 3 (no data). wantIDs := []uint32{2, 4, 1, 3} for i, n := range ns { @@ -99,7 +99,7 @@ func TestNodeSort(t *testing.T) { makeNodeWithLatency(2, -1*time.Second), makeNodeWithLatency(3, -1*time.Second), } - slices.SortStableFunc(ns, Latency) + slices.SortStableFunc(ns, ByLatency) wantIDs := []uint32{1, 2, 3} for i, n := range ns { if n.id != wantIDs[i] { @@ -116,10 +116,10 @@ func TestNodeSort(t *testing.T) { makeNodeWithLatency(7, 20*time.Millisecond), } slices.SortFunc(ns, func(a, b *Node) int { - if r := Latency(a, b); r != 0 { + if r := ByLatency(a, b); r != 0 { return r } - return ID(a, b) + return ByID(a, b) }) // Expected: 5 (10ms), 7 (20ms, lower id), 10 (20ms, higher id). wantIDs := []uint32{5, 7, 10} @@ -146,7 +146,7 @@ func TestConfigurationWatch(t *testing.T) { makeNodeWithLatency(5, 50*time.Millisecond), } const quorumSize = 3 - fastTop3 := func(c Config) Config { return c.SortBy(Latency)[:quorumSize] } + fastTop3 := func(c Config) Config { return c.Sort(ByLatency)[:quorumSize] } t.Run("EmitsInitialSnapshot", func(t *testing.T) { // Use a very long interval so only the initial emission fires. @@ -183,7 +183,7 @@ func TestConfigurationWatch(t *testing.T) { n2 := makeNodeWithLatency(2, 30*time.Millisecond) n3 := makeNodeWithLatency(3, 20*time.Millisecond) cfg := Config{n1, n2, n3} - top2 := func(c Config) Config { return c.SortBy(Latency)[:2] } + top2 := func(c Config) Config { return c.Sort(ByLatency)[:2] } const interval = 20 * time.Millisecond updates := cfg.Watch(t.Context(), interval, top2) From 43068b8295fe638967825ae8f721b68bf25ce0b3 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:20:58 +0200 Subject: [PATCH 08/16] gorums: rename RPCCall to RemoteCall RPCCall repeats itself: the C in RPC already stands for call. RemoteCall keeps the distinction from the in-process local node path without the stutter, and sits alongside QuorumCall, Multicast, and Unicast as one of the call types. rpc.go and rpc_test.go are renamed to match. --- cmd/protoc-gen-gorums/gengorums/template_rpc.go | 2 +- rpc.go => remote_call.go | 4 ++-- rpc_test.go => remote_call_test.go | 10 +++++----- server_test.go | 10 +++++----- 4 files changed, 13 insertions(+), 13 deletions(-) rename rpc.go => remote_call.go (82%) rename rpc_test.go => remote_call_test.go (80%) diff --git a/cmd/protoc-gen-gorums/gengorums/template_rpc.go b/cmd/protoc-gen-gorums/gengorums/template_rpc.go index 18d18974..0bb56f01 100644 --- a/cmd/protoc-gen-gorums/gengorums/template_rpc.go +++ b/cmd/protoc-gen-gorums/gengorums/template_rpc.go @@ -12,7 +12,7 @@ var rpcComment = ` var rpcVar = ` {{$genFile := .GenFile}} {{$nodeContext := "NodeContext"}} -{{$rpc := use "gorums.RPCCall" .GenFile}} +{{$rpc := use "gorums.RemoteCall" .GenFile}} {{$_ := use "gorums.EnforceVersion" .GenFile}} ` diff --git a/rpc.go b/remote_call.go similarity index 82% rename from rpc.go rename to remote_call.go index d90116f6..573e2055 100644 --- a/rpc.go +++ b/remote_call.go @@ -2,10 +2,10 @@ package gorums import "github.com/relab/gorums/internal/stream" -// RPCCall executes a remote procedure call on the node. +// RemoteCall executes a remote procedure call on the node. // // This method should be used by generated code only. -func RPCCall[Req, Resp msg](ctx *NodeContext, req Req, method string) (Resp, error) { +func RemoteCall[Req, Resp msg](ctx *NodeContext, req Req, method string) (Resp, error) { replyChan := make(chan NodeResponse[*stream.Message], 1) reqMsg, err := stream.NewMessage(ctx, ctx.nextMsgID(), method, req) if err != nil { diff --git a/rpc_test.go b/remote_call_test.go similarity index 80% rename from rpc_test.go rename to remote_call_test.go index fc8a1ac3..3c08bb94 100644 --- a/rpc_test.go +++ b/remote_call_test.go @@ -18,7 +18,7 @@ func TestRPCCallSuccess(t *testing.T) { ctx := gorumstest.Context(t, 5*time.Second) nodeCtx := node.Context(ctx) - response, err := gorums.RPCCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String(""), mock.TestMethod) + response, err := gorums.RemoteCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String(""), mock.TestMethod) if err != nil { t.Fatalf("Unexpected error, got: %v, want: %v", err, nil) } @@ -35,7 +35,7 @@ func TestRPCCallDownedNode(t *testing.T) { ctx := gorumstest.Context(t, 5*time.Second) nodeCtx := node.Context(ctx) - response, err := gorums.RPCCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String(""), mock.TestMethod) + response, err := gorums.RemoteCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String(""), mock.TestMethod) if err == nil { t.Fatalf("Expected error, got: %v, want: %v", err, fmt.Errorf("rpc error: code = Unavailable desc = stream is down")) } @@ -51,7 +51,7 @@ func TestRPCCallTimedOut(t *testing.T) { time.Sleep(50 * time.Millisecond) defer cancel() nodeCtx := node.Context(ctx) - response, err := gorums.RPCCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String(""), mock.TestMethod) + response, err := gorums.RemoteCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String(""), mock.TestMethod) if err == nil { t.Fatalf("Expected error, got: %v, want: %v", err, fmt.Errorf("context deadline exceeded")) } @@ -65,7 +65,7 @@ func TestRPCCallTypeMismatch(t *testing.T) { ctx := gorumstest.Context(t, 5*time.Second) nodeCtx := node.Context(ctx) - response, err := gorums.RPCCall[*pb.StringValue, *pb.Int32Value](nodeCtx, pb.String(""), mock.TestMethod) + response, err := gorums.RemoteCall[*pb.StringValue, *pb.Int32Value](nodeCtx, pb.String(""), mock.TestMethod) if err != gorums.ErrTypeMismatch { t.Fatalf("Expected error, got: %v, want: %v", err, gorums.ErrTypeMismatch) } @@ -82,7 +82,7 @@ func TestRPCCallConcurrentAccess(t *testing.T) { var wg sync.WaitGroup for range concurrency { wg.Go(func() { - _, err := gorums.RPCCall[*pb.StringValue, *pb.StringValue](node.Context(t.Context()), pb.String(""), mock.TestMethod) + _, err := gorums.RemoteCall[*pb.StringValue, *pb.StringValue](node.Context(t.Context()), pb.String(""), mock.TestMethod) if err != nil { errCh <- err } diff --git a/server_test.go b/server_test.go index 91e6ec21..efb2a5b2 100644 --- a/server_test.go +++ b/server_test.go @@ -92,7 +92,7 @@ func TestServerInterceptorsChain(t *testing.T) { ctx := gorumstest.Context(t, 5*time.Second) nodeCtx := node.Context(ctx) - res, err := gorums.RPCCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String("client-"), mock.TestMethod) + res, err := gorums.RemoteCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String("client-"), mock.TestMethod) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -130,7 +130,7 @@ func TestWithBufferSizesProcessesRequests(t *testing.T) { for i := range concurrency { wg.Go(func() { nodeCtx := node.Context(ctx) - _, errs[i] = gorums.RPCCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String(""), mock.TestMethod) + _, errs[i] = gorums.RemoteCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String(""), mock.TestMethod) }) } wg.Wait() @@ -173,7 +173,7 @@ func TestTCPReconnection(t *testing.T) { // Send first message ctx := gorumstest.Context(t, time.Second) nodeCtx := node.Context(ctx) - _, err = gorums.RPCCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String("1"), mock.TestMethod) + _, err = gorums.RemoteCall[*pb.StringValue, *pb.StringValue](nodeCtx, pb.String("1"), mock.TestMethod) if err != nil { t.Fatalf("First call failed: %v", err) } @@ -188,7 +188,7 @@ func TestTCPReconnection(t *testing.T) { // Sending now should fail or timeout ctx2 := gorumstest.Context(t, 200*time.Millisecond) nodeCtx2 := node.Context(ctx2) - _, err = gorums.RPCCall[*pb.StringValue, *pb.StringValue](nodeCtx2, pb.String("2"), mock.TestMethod) + _, err = gorums.RemoteCall[*pb.StringValue, *pb.StringValue](nodeCtx2, pb.String("2"), mock.TestMethod) if err == nil { // It might succeed if it just queued it? But we wait for response. } else { @@ -217,7 +217,7 @@ func TestTCPReconnection(t *testing.T) { // Send message again ctx3 := gorumstest.Context(t, 2*time.Second) nodeCtx3 := node.Context(ctx3) - _, err = gorums.RPCCall[*pb.StringValue, *pb.StringValue](nodeCtx3, pb.String("3"), mock.TestMethod) + _, err = gorums.RemoteCall[*pb.StringValue, *pb.StringValue](nodeCtx3, pb.String("3"), mock.TestMethod) if err != nil { t.Errorf("Call after reconnection failed: %v", err) } From 9279338829df27de85cf04f3895555fb7448761b Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 19:52:59 +0200 Subject: [PATCH 09/16] gorums: rename the rpc method option to remotecall The proto method option that selects a plain remote call is named rpc, so a reader of gorums.proto sees rpc next to quorumcall, multicast, and unicast, none of which repeat the C in RPC. It becomes remotecall, matching the RemoteCall function it selects and the generated file it names. The option is set by the generator rather than by hand, which the declaration now says. --- cmd/protoc-gen-gorums/gengorums/gorums.go | 8 ++++---- .../gengorums/template_rpc.go | 18 +++++++++--------- gorums.proto | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cmd/protoc-gen-gorums/gengorums/gorums.go b/cmd/protoc-gen-gorums/gengorums/gorums.go index 5425b95f..0e360dfb 100644 --- a/cmd/protoc-gen-gorums/gengorums/gorums.go +++ b/cmd/protoc-gen-gorums/gengorums/gorums.go @@ -255,10 +255,10 @@ var gorumsCallTypesInfo = map[string]*callTypeInfo{ "types": {template: dataTypes}, "server": {template: server}, - callTypeName(gorums.E_Rpc): { - extInfo: gorums.E_Rpc, - docName: "rpc", - template: rpcCall, + callTypeName(gorums.E_Remotecall): { + extInfo: gorums.E_Remotecall, + docName: "remotecall", + template: remoteCall, chkFn: func(m *protogen.Method) bool { return !hasMethodOption(m, gorumsCallTypes...) }, diff --git a/cmd/protoc-gen-gorums/gengorums/template_rpc.go b/cmd/protoc-gen-gorums/gengorums/template_rpc.go index 0bb56f01..392494d0 100644 --- a/cmd/protoc-gen-gorums/gengorums/template_rpc.go +++ b/cmd/protoc-gen-gorums/gengorums/template_rpc.go @@ -1,6 +1,6 @@ package gengorums -var rpcComment = ` +var remoteCallComment = ` {{$comments := .Method.Comments.Leading}} {{if ne $comments ""}} {{$comments -}} @@ -9,22 +9,22 @@ var rpcComment = ` {{end -}} ` -var rpcVar = ` +var remoteCallVar = ` {{$genFile := .GenFile}} {{$nodeContext := "NodeContext"}} {{$rpc := use "gorums.RemoteCall" .GenFile}} {{$_ := use "gorums.EnforceVersion" .GenFile}} ` -var rpcSignature = `func {{$method}}(ctx *{{$nodeContext}}, in *{{$in}}) (*{{$out}}, error) { +var remoteCallSignature = `func {{$method}}(ctx *{{$nodeContext}}, in *{{$in}}) (*{{$out}}, error) { ` -var rpcBody = ` return {{$rpc}}[*{{$in}}, *{{$out}}](ctx, in, "{{$fullName}}") +var remoteCallBody = ` return {{$rpc}}[*{{$in}}, *{{$out}}](ctx, in, "{{$fullName}}") } ` -var rpcCall = commonVariables + - rpcVar + - rpcComment + - rpcSignature + - rpcBody +var remoteCall = commonVariables + + remoteCallVar + + remoteCallComment + + remoteCallSignature + + remoteCallBody diff --git a/gorums.proto b/gorums.proto index c78497d1..72268ef1 100644 --- a/gorums.proto +++ b/gorums.proto @@ -9,7 +9,7 @@ import "google/protobuf/descriptor.proto"; extend google.protobuf.MethodOptions { // call types - bool rpc = 50001; + bool remotecall = 50001; // only for internal use; no need to set manually bool unicast = 50002; bool multicast = 50003; bool quorumcall = 50004; From 3ed4dd3aa550eb2c6b0b77941f482e63cae1ccf6 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:21:58 +0200 Subject: [PATCH 10/16] gorums: rename Message.Msg to Message.Proto The envelope holds a decoded protobuf message alongside the wire message it came in, and both were reachable as Msg: the field, and the embedded stream.Message. Proto names the field for what it holds and removes the collision with the embedded type's own name. --- handler.go | 14 +++++++------- handler_test.go | 26 +++++++++++++------------- server.go | 2 +- server_e2e_test.go | 6 +++--- server_test.go | 2 +- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/handler.go b/handler.go index 334c9adc..7a609e98 100644 --- a/handler.go +++ b/handler.go @@ -13,7 +13,7 @@ import ( // It is used by both server and client handler chains to carry the application-level // message alongside the stream-level envelope. type Message struct { - Msg proto.Message + Proto proto.Message *stream.Message } @@ -57,9 +57,9 @@ func (ctx *ServerContext) Release() { // // This function should only be used by generated code. func (ctx *ServerContext) SendMessage(out *Message) { - // If Msg is set, marshal it to payload before sending. - if out.Msg != nil && len(out.GetPayload()) == 0 { - payload, err := proto.Marshal(out.Msg) + // If Proto is set, marshal it to payload before sending. + if out.Proto != nil && len(out.GetPayload()) == 0 { + payload, err := proto.Marshal(out.Proto) if err == nil { out.SetPayload(payload) } else { @@ -126,7 +126,7 @@ func NewResponseMessage(in *Message, resp proto.Message) *Message { // Status is left empty; it can be set by MessageWithError if needed } return &Message{ - Msg: resp, + Proto: resp, Message: msgBuilder.Build(), } } @@ -154,10 +154,10 @@ func MessageWithError(in, out *Message, err error) *Message { // the zero value of T is returned. func AsProto[T proto.Message](msg *Message) T { var zero T - if msg == nil || msg.Msg == nil { + if msg == nil || msg.Proto == nil { return zero } - if req, ok := msg.Msg.(T); ok { + if req, ok := msg.Proto.(T); ok { return req } return zero diff --git a/handler_test.go b/handler_test.go index ca1f4387..0b3d9047 100644 --- a/handler_test.go +++ b/handler_test.go @@ -43,27 +43,27 @@ func TestNewResponseMessage(t *testing.T) { }, { name: "NilReq/NilResp/StreamIn/StreamOut", - in: &gorums.Message{Msg: nil, Message: streamIn}, + in: &gorums.Message{Proto: nil, Message: streamIn}, resp: nil, - want: &gorums.Message{Msg: (*config.Response)(nil), Message: streamOut}, + want: &gorums.Message{Proto: (*config.Response)(nil), Message: streamOut}, }, { name: "NilReq/Resp/StreamIn/StreamOut", - in: &gorums.Message{Msg: nil, Message: streamIn}, + in: &gorums.Message{Proto: nil, Message: streamIn}, resp: resp, - want: &gorums.Message{Msg: resp, Message: streamOut}, + want: &gorums.Message{Proto: resp, Message: streamOut}, }, { name: "Req/NilResp/StreamIn/StreamOut", - in: &gorums.Message{Msg: req, Message: streamIn}, + in: &gorums.Message{Proto: req, Message: streamIn}, resp: nil, - want: &gorums.Message{Msg: (*config.Response)(nil), Message: streamOut}, + want: &gorums.Message{Proto: (*config.Response)(nil), Message: streamOut}, }, { name: "Req/Resp/StreamIn/StreamOut", - in: &gorums.Message{Msg: req, Message: streamIn}, + in: &gorums.Message{Proto: req, Message: streamIn}, resp: resp, - want: &gorums.Message{Msg: resp, Message: streamOut}, + want: &gorums.Message{Proto: resp, Message: streamOut}, }, } @@ -79,11 +79,11 @@ func TestNewResponseMessage(t *testing.T) { if got == nil { t.Fatalf("NewResponseMessage returned nil, want non-nil") } - if (tt.want.Msg == nil) != (got.Msg == nil) { - t.Errorf("Msg field: want nil=%v, got nil=%v", tt.want.Msg == nil, got.Msg == nil) - } else if tt.want.Msg != nil && got.Msg != nil { - if diff := cmp.Diff(tt.want.Msg, got.Msg, protocmp.Transform()); diff != "" { - t.Errorf("Msg field mismatch (-want, +got):\n%s", diff) + if (tt.want.Proto == nil) != (got.Proto == nil) { + t.Errorf("Proto field: want nil=%v, got nil=%v", tt.want.Proto == nil, got.Proto == nil) + } else if tt.want.Proto != nil && got.Proto != nil { + if diff := cmp.Diff(tt.want.Proto, got.Proto, protocmp.Transform()); diff != "" { + t.Errorf("Proto field mismatch (-want, +got):\n%s", diff) } } if diff := cmp.Diff(tt.want.Message, got.Message, protocmp.Transform()); diff != "" { diff --git a/server.go b/server.go index b5316f9c..f4721d89 100644 --- a/server.go +++ b/server.go @@ -235,7 +235,7 @@ func (s *Server) HandleRequest(ctx context.Context, reqMsg *stream.Message, rele } msg, err := unmarshalRequest(reqMsg) - in := &Message{Msg: msg, Message: reqMsg} + in := &Message{Proto: msg, Message: reqMsg} if err != nil { srvCtx.SendMessage(MessageWithError(in, nil, err)) return diff --git a/server_e2e_test.go b/server_e2e_test.go index a5cd45b0..cb683c31 100644 --- a/server_e2e_test.go +++ b/server_e2e_test.go @@ -299,7 +299,7 @@ func TestServerHandlerCanMulticastViaConfig(t *testing.T) { for i, srv := range servers { srv.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { - t.Logf("Server %d received multicast on %v: %v", i+1, mock.TestMethod, in.Msg) + t.Logf("Server %d received multicast on %v: %v", i+1, mock.TestMethod, in.Proto) // Release before the nested multicast: the peer configuration // includes the local node, whose in-process dispatch waits for // this handler's dispatch lock. @@ -318,7 +318,7 @@ func TestServerHandlerCanMulticastViaConfig(t *testing.T) { }) srv.RegisterHandler(mock.Stream, func(_ gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { - t.Logf("Server %d received multicast on %v: %v", i+1, mock.Stream, in.Msg) + t.Logf("Server %d received multicast on %v: %v", i+1, mock.Stream, in.Proto) wg.Done() return nil, nil }) @@ -443,7 +443,7 @@ func TestServerHandlerCanMulticastViaConnectedClients(t *testing.T) { wg.Add(1) srvServer.RegisterHandler(mock.TestMethod, func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { - t.Logf("SERVER received multicast: %v", in.Msg) + t.Logf("SERVER received multicast: %v", in.Proto) if cfg := ctx.ConnectedClients(); cfg != nil && cfg.Size() == 1 { err := gorums.Multicast( cfg.Context(t.Context()), diff --git a/server_test.go b/server_test.go index efb2a5b2..dc06bb8d 100644 --- a/server_test.go +++ b/server_test.go @@ -47,7 +47,7 @@ func appendStringInterceptor(inStr, outStr string) gorums.ServerInterceptor { req.Value += inStr // We do not need to re-marshal into the payload here. - // The next handler in the chain will access req via gorums.AsProto(in) which reads in.Msg. + // The next handler in the chain will access req via gorums.AsProto(in) which reads in.Proto. // call the next handler out, err := next(ctx, in) From 6dffd9f09a926d440b93bab1b2f4baa481803229 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:23:38 +0200 Subject: [PATCH 11/16] gorums: rename ClientCtx to CallContext and regroup the root files ClientCtx was the last abbreviated type in the public API, and it is not client-specific: it carries the context and state of a call, and an interceptor receives it on both the quorum and one-way paths. CallContext names that. QuorumInterceptor becomes ClientInterceptor to match, since it now applies to any client-side call, not only quorum calls. Files are renamed so that a reader can find a declaration from its subject: mgr.go -> outbound_manager.go opts.go -> dial_options.go config_opts.go -> node_source.go handler.go -> server_handler.go client_interceptor.go -> call_context.go with the matching test files, plus async_test.go, correctable_test.go, and quorumcall_test.go grouped under the call_ prefix they all exercise, and opts_test.go widened to options_test.go. --- async_test.go => call_async_test.go | 0 ...test.go => call_client_interceptor_test.go | 10 +-- client_interceptor.go => call_context.go | 74 +++++++++---------- ...ctable_test.go => call_correctable_test.go | 0 quorumcall_test.go => call_quorum_test.go | 0 callopts.go | 2 +- callopts_test.go | 2 +- opts.go => dial_options.go | 0 doc/user-guide.md | 16 ++-- multicast.go | 2 +- config_opts.go => node_source.go | 0 opts_test.go => options_test.go | 0 mgr.go => outbound_manager.go | 0 quorumcall.go | 2 +- responses.go | 2 +- responses_test.go | 8 +- handler.go => server_handler.go | 0 handler_test.go => server_handler_test.go | 0 18 files changed, 59 insertions(+), 59 deletions(-) rename async_test.go => call_async_test.go (100%) rename client_interceptor_test.go => call_client_interceptor_test.go (96%) rename client_interceptor.go => call_context.go (80%) rename correctable_test.go => call_correctable_test.go (100%) rename quorumcall_test.go => call_quorum_test.go (100%) rename opts.go => dial_options.go (100%) rename config_opts.go => node_source.go (100%) rename opts_test.go => options_test.go (100%) rename mgr.go => outbound_manager.go (100%) rename handler.go => server_handler.go (100%) rename handler_test.go => server_handler_test.go (100%) diff --git a/async_test.go b/call_async_test.go similarity index 100% rename from async_test.go rename to call_async_test.go diff --git a/client_interceptor_test.go b/call_client_interceptor_test.go similarity index 96% rename from client_interceptor_test.go rename to call_client_interceptor_test.go index 76ddedc4..fe8bba96 100644 --- a/client_interceptor_test.go +++ b/call_client_interceptor_test.go @@ -14,7 +14,7 @@ import ( // LoggingInterceptor is a custom interceptor that logs each response. func LoggingInterceptor[Req, Resp proto.Message]( - ctx *gorums.ClientCtx[Req, Resp], + ctx *gorums.CallContext[Req, Resp], next gorums.ResponseSeq[Resp], ) gorums.ResponseSeq[Resp] { _ = ctx.Method() // Access method name (could be used for logging) @@ -31,8 +31,8 @@ func LoggingInterceptor[Req, Resp proto.Message]( // FilterInterceptor returns an interceptor that filters responses based on a predicate. func FilterInterceptor[Req, Resp proto.Message]( keep func(resp gorums.NodeResponse[Resp]) bool, -) gorums.QuorumInterceptor[Req, Resp] { - return func(ctx *gorums.ClientCtx[Req, Resp], next gorums.ResponseSeq[Resp]) gorums.ResponseSeq[Resp] { +) gorums.ClientInterceptor[Req, Resp] { + return func(ctx *gorums.CallContext[Req, Resp], next gorums.ResponseSeq[Resp]) gorums.ResponseSeq[Resp] { _ = ctx.Method() // Access method name (could be used for filtering) return func(yield func(gorums.NodeResponse[Resp]) bool) { for resp := range next { @@ -49,8 +49,8 @@ func FilterInterceptor[Req, Resp proto.Message]( // CountingInterceptor counts the number of responses passing through. func CountingInterceptor[Req, Resp proto.Message]( counter *int, -) gorums.QuorumInterceptor[Req, Resp] { - return func(_ *gorums.ClientCtx[Req, Resp], next gorums.ResponseSeq[Resp]) gorums.ResponseSeq[Resp] { +) gorums.ClientInterceptor[Req, Resp] { + return func(_ *gorums.CallContext[Req, Resp], next gorums.ResponseSeq[Resp]) gorums.ResponseSeq[Resp] { return func(yield func(gorums.NodeResponse[Resp]) bool) { for resp := range next { *counter++ diff --git a/client_interceptor.go b/call_context.go similarity index 80% rename from client_interceptor.go rename to call_context.go index c6c1ae09..b20e6311 100644 --- a/client_interceptor.go +++ b/call_context.go @@ -10,21 +10,21 @@ import ( "google.golang.org/protobuf/types/known/emptypb" ) -// QuorumInterceptor intercepts and processes quorum calls, allowing modification of +// ClientInterceptor intercepts and processes quorum calls, allowing modification of // requests, responses, and aggregation logic. Interceptors can be chained together. // // Type parameters: // - Req: The request message type sent to nodes // - Resp: The response message type from individual nodes // -// The interceptor receives the ClientCtx for metadata access, the current response +// The interceptor receives the CallContext for metadata access, the current response // iterator (next), and returns a new response iterator. This pattern allows // interceptors to wrap the response stream with custom logic. // // Custom interceptors can be created like this: // // func LoggingInterceptor[Req, Resp proto.Message]( -// ctx *gorums.ClientCtx[Req, Resp], +// ctx *gorums.CallContext[Req, Resp], // next gorums.ResponseSeq[Resp], // ) gorums.ResponseSeq[Resp] { // return func(yield func(gorums.NodeResponse[Resp]) bool) { @@ -34,11 +34,11 @@ import ( // } // } // } -type QuorumInterceptor[Req, Resp msg] func(ctx *ClientCtx[Req, Resp], next ResponseSeq[Resp]) ResponseSeq[Resp] +type ClientInterceptor[Req, Resp msg] func(ctx *CallContext[Req, Resp], next ResponseSeq[Resp]) ResponseSeq[Resp] -// ClientCtx provides context and access to the quorum call state for interceptors. +// CallContext provides context and access to the quorum call state for interceptors. // It exposes the request, configuration, metadata about the call, and the response iterator. -type ClientCtx[Req, Resp msg] struct { +type CallContext[Req, Resp msg] struct { context.Context config Config request Req @@ -66,25 +66,25 @@ type ClientCtx[Req, Resp msg] struct { } // sendNow triggers request dispatch exactly once. -func (c *ClientCtx[Req, Resp]) sendNow() { +func (c *CallContext[Req, Resp]) sendNow() { c.sendOnce.Do(c.send) } -// newQuorumCallClientCtx constructs a ClientCtx for quorum calls (two-way, always returns responses). +// newQuorumCallContext constructs a CallContext for quorum calls (two-way, always returns responses). // A reply channel is always created; streaming controls both its buffer size and the response iterator type. -func newQuorumCallClientCtx[Req, Resp msg]( +func newQuorumCallContext[Req, Resp msg]( ctx *ConfigContext, req Req, method string, streaming bool, interceptors []any, -) *ClientCtx[Req, Resp] { +) *CallContext[Req, Resp] { config := ctx.Config() n := config.Size() if streaming { n *= 10 } - clientCtx := &ClientCtx[Req, Resp]{ + clientCtx := &CallContext[Req, Resp]{ Context: ctx, config: config, request: req, @@ -102,22 +102,22 @@ func newQuorumCallClientCtx[Req, Resp msg]( return clientCtx } -// newMulticastClientCtx constructs a ClientCtx for multicast (one-way, no responses). +// newMulticastCallContext constructs a CallContext for multicast (one-way, no responses). // A reply channel is created only when waitForSend=true (blocking send); fire-and-forget // calls receive a nil channel, meaning no router entry is registered. -func newMulticastClientCtx[Req msg]( +func newMulticastCallContext[Req msg]( ctx *ConfigContext, req Req, method string, waitForSend bool, interceptors []any, -) *ClientCtx[Req, *emptypb.Empty] { +) *CallContext[Req, *emptypb.Empty] { config := ctx.Config() var replyChan chan NodeResponse[*stream.Message] if waitForSend { replyChan = make(chan NodeResponse[*stream.Message], config.Size()) } - clientCtx := &ClientCtx[Req, *emptypb.Empty]{ + clientCtx := &CallContext[Req, *emptypb.Empty]{ Context: ctx, config: config, request: req, @@ -132,31 +132,31 @@ func newMulticastClientCtx[Req msg]( } // ------------------------------------------------------------------------- -// ClientCtx Methods +// CallContext Methods // ------------------------------------------------------------------------- // Request returns the original request message for this quorum call. -func (c *ClientCtx[Req, Resp]) Request() Req { +func (c *CallContext[Req, Resp]) Request() Req { return c.request } // Config returns the configuration (set of nodes) for this quorum call. -func (c *ClientCtx[Req, Resp]) Config() Config { +func (c *CallContext[Req, Resp]) Config() Config { return c.config } // Method returns the name of the RPC method being called. -func (c *ClientCtx[Req, Resp]) Method() string { +func (c *CallContext[Req, Resp]) Method() string { return c.method } // Nodes returns the slice of nodes in this configuration. -func (c *ClientCtx[Req, Resp]) Nodes() []*Node { +func (c *CallContext[Req, Resp]) Nodes() []*Node { return c.config.Nodes() } // Node returns the node with the given ID. -func (c *ClientCtx[Req, Resp]) Node(id uint32) *Node { +func (c *CallContext[Req, Resp]) Node(id uint32) *Node { nodes := c.config.Nodes() index := slices.IndexFunc(nodes, func(n *Node) bool { return n.ID() == id @@ -168,21 +168,21 @@ func (c *ClientCtx[Req, Resp]) Node(id uint32) *Node { } // Size returns the number of nodes in this configuration. -func (c *ClientCtx[Req, Resp]) Size() int { +func (c *CallContext[Req, Resp]) Size() int { return c.config.Size() } // reportNodeError sends an error response for the given node to replyChan. // It is a no-op for fire-and-forget calls where replyChan is nil. -func (c *ClientCtx[Req, Resp]) reportNodeError(nodeID uint32, err error) { +func (c *CallContext[Req, Resp]) reportNodeError(nodeID uint32, err error) { if c.replyChan != nil { c.replyChan <- NodeResponse[*stream.Message]{NodeID: nodeID, Err: err} } } // enqueue sends a stream.Request to the given node, populating the shared -// fields from ClientCtx so call sites only need to supply the message. -func (c *ClientCtx[Req, Resp]) enqueue(n *Node, msg *stream.Message) { +// fields from CallContext so call sites only need to supply the message. +func (c *CallContext[Req, Resp]) enqueue(n *Node, msg *stream.Message) { n.Enqueue(stream.Request{ Ctx: c.Context, Msg: msg, @@ -195,10 +195,10 @@ func (c *ClientCtx[Req, Resp]) enqueue(n *Node, msg *stream.Message) { // applyInterceptors chains the given interceptors, wrapping the response sequence. // Each interceptor receives the current response sequence and returns a new one. // Interceptors are applied in order, with each wrapping the previous result. -func (c *ClientCtx[Req, Resp]) applyInterceptors(interceptors []any) { +func (c *CallContext[Req, Resp]) applyInterceptors(interceptors []any) { responseSeq := c.responseSeq for _, ic := range interceptors { - interceptor := ic.(QuorumInterceptor[Req, Resp]) + interceptor := ic.(ClientInterceptor[Req, Resp]) responseSeq = interceptor(c, responseSeq) } c.responseSeq = responseSeq @@ -207,7 +207,7 @@ func (c *ClientCtx[Req, Resp]) applyInterceptors(interceptors []any) { // send dispatches requests to all nodes. It delegates to sendWithPerNodeTransformation // if any per-node request transformations are registered. Otherwise, it uses sendShared // to marshal the request once and send the same message to all nodes. -func (c *ClientCtx[Req, Resp]) send() { +func (c *CallContext[Req, Resp]) send() { if len(c.reqTransforms) == 0 { c.sendShared() } else { @@ -217,7 +217,7 @@ func (c *ClientCtx[Req, Resp]) send() { // sendShared marshals the request once and enqueues the shared message to all nodes. // On marshal error, it reports the error to every node and returns early. -func (c *ClientCtx[Req, Resp]) sendShared() { +func (c *CallContext[Req, Resp]) sendShared() { sharedMsg, err := stream.NewMessage(c.Context, c.msgID, c.method, c.request) if err != nil { // Marshaling fails identically for all nodes; report and return. @@ -233,7 +233,7 @@ func (c *ClientCtx[Req, Resp]) sendShared() { // sendWithPerNodeTransformation applies per-node request transformations before // marshaling and enqueues each individually transformed message to its node. -func (c *ClientCtx[Req, Resp]) sendWithPerNodeTransformation() { +func (c *CallContext[Req, Resp]) sendWithPerNodeTransformation() { for _, n := range c.config { streamMsg := c.transformAndMarshal(n) if streamMsg == nil { @@ -246,7 +246,7 @@ func (c *ClientCtx[Req, Resp]) sendWithPerNodeTransformation() { // transformAndMarshal applies transformations to the request for the given node, // then marshals it into a stream.Message. Returns nil if transformation fails // or marshaling fails (in which case the error is reported via reportNodeError). -func (c *ClientCtx[Req, Resp]) transformAndMarshal(n *Node) *stream.Message { +func (c *CallContext[Req, Resp]) transformAndMarshal(n *Node) *stream.Message { transformedRequest := c.request for _, transform := range c.reqTransforms { transformedRequest = transform(transformedRequest, n) @@ -266,7 +266,7 @@ func (c *ClientCtx[Req, Resp]) transformAndMarshal(n *Node) *stream.Message { // defaultResponseSeq returns an iterator that yields at most c.expectedReplies responses // from nodes until the context is canceled or all expected responses are received. -func (c *ClientCtx[Req, Resp]) defaultResponseSeq() ResponseSeq[Resp] { +func (c *CallContext[Req, Resp]) defaultResponseSeq() ResponseSeq[Resp] { return func(yield func(NodeResponse[Resp]) bool) { // Trigger sending on first iteration c.sendNow() @@ -286,7 +286,7 @@ func (c *ClientCtx[Req, Resp]) defaultResponseSeq() ResponseSeq[Resp] { // streamingResponseSeq returns an iterator that yields responses as they arrive // from nodes until the context is canceled or breaking from the range loop. -func (c *ClientCtx[Req, Resp]) streamingResponseSeq() ResponseSeq[Resp] { +func (c *CallContext[Req, Resp]) streamingResponseSeq() ResponseSeq[Resp] { return func(yield func(NodeResponse[Resp]) bool) { // Trigger sending on first iteration c.sendNow() @@ -314,8 +314,8 @@ func (c *ClientCtx[Req, Resp]) streamingResponseSeq() ResponseSeq[Resp] { // The fn receives the original request and a node, and returns the transformed // request to send to that node. If the function returns an invalid message or nil, // an ErrSkipNode error is sent for that node, indicating it was skipped. -func MapRequest[Req, Resp msg](fn func(Req, *Node) Req) QuorumInterceptor[Req, Resp] { - return func(ctx *ClientCtx[Req, Resp], next ResponseSeq[Resp]) ResponseSeq[Resp] { +func MapRequest[Req, Resp msg](fn func(Req, *Node) Req) ClientInterceptor[Req, Resp] { + return func(ctx *CallContext[Req, Resp], next ResponseSeq[Resp]) ResponseSeq[Resp] { if fn != nil { ctx.reqTransforms = append(ctx.reqTransforms, fn) } @@ -327,8 +327,8 @@ func MapRequest[Req, Resp msg](fn func(Req, *Node) Req) QuorumInterceptor[Req, R // // The fn receives the response from a node and the node itself, and returns the // transformed response. -func MapResponse[Req, Resp msg](fn func(Resp, *Node) Resp) QuorumInterceptor[Req, Resp] { - return func(ctx *ClientCtx[Req, Resp], next ResponseSeq[Resp]) ResponseSeq[Resp] { +func MapResponse[Req, Resp msg](fn func(Resp, *Node) Resp) ClientInterceptor[Req, Resp] { + return func(ctx *CallContext[Req, Resp], next ResponseSeq[Resp]) ResponseSeq[Resp] { if fn == nil { return next } diff --git a/correctable_test.go b/call_correctable_test.go similarity index 100% rename from correctable_test.go rename to call_correctable_test.go diff --git a/quorumcall_test.go b/call_quorum_test.go similarity index 100% rename from quorumcall_test.go rename to call_quorum_test.go diff --git a/callopts.go b/callopts.go index 758fb38c..8488e9ad 100644 --- a/callopts.go +++ b/callopts.go @@ -41,7 +41,7 @@ func IgnoreErrors() CallOption { // resp, err := ReadQC(ctx, req, // gorums.Interceptors(loggingInterceptor, filterInterceptor), // ).Majority() -func Interceptors[Req, Resp proto.Message](interceptors ...QuorumInterceptor[Req, Resp]) CallOption { +func Interceptors[Req, Resp proto.Message](interceptors ...ClientInterceptor[Req, Resp]) CallOption { return func(o *callOptions) { for _, interceptor := range interceptors { o.interceptors = append(o.interceptors, interceptor) diff --git a/callopts_test.go b/callopts_test.go index ff2bb9d5..e7aa5e62 100644 --- a/callopts_test.go +++ b/callopts_test.go @@ -113,7 +113,7 @@ func TestCallOptionsIgnoreErrorsResourceLeak(t *testing.T) { } func BenchmarkGetCallOptions(b *testing.B) { - interceptor := func(_ *ClientCtx[msg, msg], next ResponseSeq[msg]) ResponseSeq[msg] { return next } + interceptor := func(_ *CallContext[msg, msg], next ResponseSeq[msg]) ResponseSeq[msg] { return next } tests := []struct { numOpts int }{ diff --git a/opts.go b/dial_options.go similarity index 100% rename from opts.go rename to dial_options.go diff --git a/doc/user-guide.md b/doc/user-guide.md index f396963e..651420cd 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -919,12 +919,12 @@ Beyond the built-in `MapRequest` and `MapResponse` interceptors, you can create A custom interceptor has the signature: ```go -func(ctx *gorums.ClientCtx[Req, Resp], next gorums.ResponseSeq[Resp]) gorums.ResponseSeq[Resp] +func(ctx *gorums.CallContext[Req, Resp], next gorums.ResponseSeq[Resp]) gorums.ResponseSeq[Resp] ``` The interceptor receives: -* `ctx` - the `ClientCtx` providing access to: +* `ctx` - the `CallContext` providing access to: * `.Request()` - the original request * `.Config()` - the configuration being used * `.Method()` - the RPC method name @@ -956,7 +956,7 @@ Create a logging interceptor that wraps the response iterator: ```go func LoggingInterceptor[Req, Resp proto.Message]( - ctx *gorums.ClientCtx[Req, Resp], + ctx *gorums.CallContext[Req, Resp], next gorums.ResponseSeq[Resp], ) gorums.ResponseSeq[Resp] { startTime := time.Now() @@ -994,8 +994,8 @@ Filter out responses that don't meet certain criteria: ```go func FilterInterceptor[Req, Resp proto.Message]( shouldInclude func(Resp) bool, -) gorums.QuorumInterceptor[Req, Resp] { - return func(ctx *gorums.ClientCtx[Req, Resp], next gorums.ResponseSeq[Resp]) gorums.ResponseSeq[Resp] { +) gorums.ClientInterceptor[Req, Resp] { + return func(ctx *gorums.CallContext[Req, Resp], next gorums.ResponseSeq[Resp]) gorums.ResponseSeq[Resp] { return func(yield func(gorums.NodeResponse[Resp]) bool) { for resp := range next { // Skip responses that don't pass the filter @@ -1028,8 +1028,8 @@ Count responses passing through the interceptor: ```go func CountingInterceptor[Req, Resp proto.Message]( counter *int, -) gorums.QuorumInterceptor[Req, Resp] { - return func(_ *gorums.ClientCtx[Req, Resp], next gorums.ResponseSeq[Resp]) gorums.ResponseSeq[Resp] { +) gorums.ClientInterceptor[Req, Resp] { + return func(_ *gorums.CallContext[Req, Resp], next gorums.ResponseSeq[Resp]) gorums.ResponseSeq[Resp] { return func(yield func(gorums.NodeResponse[Resp]) bool) { for resp := range next { *counter++ @@ -1042,7 +1042,7 @@ func CountingInterceptor[Req, Resp proto.Message]( } ``` -**Note:** Custom interceptors can be defined in any package. The `ClientCtx` type and `QuorumInterceptor` signature are exported from the gorums package. +**Note:** Custom interceptors can be defined in any package. The `CallContext` type and `ClientInterceptor` signature are exported from the gorums package. ### Server-Side Interceptors diff --git a/multicast.go b/multicast.go index 93e9d5c6..9e12ed0a 100644 --- a/multicast.go +++ b/multicast.go @@ -22,7 +22,7 @@ func Multicast[Req msg](ctx *ConfigContext, req Req, method string, opts ...Call callOpts := getCallOptions(opts...) waitForSend := !callOpts.ignoreErrors - clientCtx := newMulticastClientCtx(ctx, req, method, waitForSend, callOpts.interceptors) + clientCtx := newMulticastCallContext(ctx, req, method, waitForSend, callOpts.interceptors) // Send messages immediately (multicast doesn't use lazy sending) clientCtx.sendNow() diff --git a/config_opts.go b/node_source.go similarity index 100% rename from config_opts.go rename to node_source.go diff --git a/opts_test.go b/options_test.go similarity index 100% rename from opts_test.go rename to options_test.go diff --git a/mgr.go b/outbound_manager.go similarity index 100% rename from mgr.go rename to outbound_manager.go diff --git a/quorumcall.go b/quorumcall.go index e0e3b816..201a88eb 100644 --- a/quorumcall.go +++ b/quorumcall.go @@ -50,6 +50,6 @@ func invokeQuorumCall[Req, Resp msg]( opts ...CallOption, ) *Responses[Resp] { callOpts := getCallOptions(opts...) - clientCtx := newQuorumCallClientCtx[Req, Resp](ctx, req, method, streaming, callOpts.interceptors) + clientCtx := newQuorumCallContext[Req, Resp](ctx, req, method, streaming, callOpts.interceptors) return NewResponses(clientCtx) } diff --git a/responses.go b/responses.go index 90a2600a..11187006 100644 --- a/responses.go +++ b/responses.go @@ -161,7 +161,7 @@ type starter interface { sendNow() } -func NewResponses[Req, Resp msg](ctx *ClientCtx[Req, Resp]) *Responses[Resp] { +func NewResponses[Req, Resp msg](ctx *CallContext[Req, Resp]) *Responses[Resp] { return &Responses[Resp]{ seq: ctx.responseSeq, size: ctx.Size(), diff --git a/responses_test.go b/responses_test.go index 9b1de195..595e4d23 100644 --- a/responses_test.go +++ b/responses_test.go @@ -9,9 +9,9 @@ import ( pb "google.golang.org/protobuf/types/known/wrapperspb" ) -// makeClientCtx is a helper to create a ClientCtx with mock responses for unit tests. -// It creates a channel with the provided responses and returns a ClientCtx. -func makeClientCtx[Req, Resp msg](t *testing.T, numNodes int, responses []NodeResponse[msg]) *ClientCtx[Req, Resp] { +// makeClientCtx is a helper to create a CallContext with mock responses for unit tests. +// It creates a channel with the provided responses and returns a CallContext. +func makeClientCtx[Req, Resp msg](t *testing.T, numNodes int, responses []NodeResponse[msg]) *CallContext[Req, Resp] { t.Helper() resultChan := make(chan NodeResponse[*stream.Message], len(responses)) @@ -37,7 +37,7 @@ func makeClientCtx[Req, Resp msg](t *testing.T, numNodes int, responses []NodeRe config[i] = &Node{id: uint32(i + 1)} } - c := &ClientCtx[Req, Resp]{ + c := &CallContext[Req, Resp]{ Context: t.Context(), config: config, replyChan: resultChan, diff --git a/handler.go b/server_handler.go similarity index 100% rename from handler.go rename to server_handler.go diff --git a/handler_test.go b/server_handler_test.go similarity index 100% rename from handler_test.go rename to server_handler_test.go From 83dca98344f790f2c72cab2e62d9df1478f2df26 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:24:37 +0200 Subject: [PATCH 12/16] doc: reorganize the documentation directory migration.md becomes migration-v2-to-v3.md. Its title already scoped it to the v2-to-v3 iterator migration, but the filename read as though it were the only migration guide, which it is not. design-doc-layering.md is removed. It is a research note that predates the current typed interceptor model rather than maintained documentation, so it belongs in the private work area, where it is already tracked, alongside the other deferred proposals. --- doc/design-doc-layering.md | 150 -------------------- doc/{migration.md => migration-v2-to-v3.md} | 0 2 files changed, 150 deletions(-) delete mode 100644 doc/design-doc-layering.md rename doc/{migration.md => migration-v2-to-v3.md} (100%) diff --git a/doc/design-doc-layering.md b/doc/design-doc-layering.md deleted file mode 100644 index 46b73c76..00000000 --- a/doc/design-doc-layering.md +++ /dev/null @@ -1,150 +0,0 @@ -# Design Document for Layering with gRPC and Gorums - -Gorums wants to support new features that require processing of metadata -on the server side, that should not be visible to the server method implementations, and likewise for clients invoking an RPC method, should not need to concern themselves with these details. - -Examples of such layers includes - -* A layer to attach a configuration data structure to replies to allow a client to receive information about changes to the configuration (group membership), e.g. in a process-centric model. Similarly, the client proxy should be able to attach an epoch number that the server-side proxy will use to determine if the client has the most up-to-date configuration. These data elements should not be exposed to the client and server side code, and should seamlessly be filtered before a call is passed to the upper server-side method, and correspondingly the client-side caller. - -* Another application of such layering is to pass context information about a request to other processes from a server to another server in an all-to-all communication pattern. That is, a server may implement a server-side quorum function to receive such context information, which can then be used to determine if a quorum has been received and thus decide on the next action, passing on the context information as needed by the protocol being implemented. - -* The Raft implementation presented in Meland's thesis, while not using any layering, had some design challenges in deciding how to structure the Key-Value storage application vs the Raft protocol implementation. Perhaps a layering approach could alleviate this tension. - -* A more formalized layer approach can replace the current solution for `custom_return_type` and `per_node_arg` options, and the various proposed `call_adapter` options (partially implemented in the `call-adapter` branch). - -Frausing's thesis provided two implementations of such layering, one based on interceptors and the metadata support in gRPC, and one where the layer was designed as two separate gRPC services, one with a prefix `g` to separate it from the actual service that applications should use. Please see Frausing's thesis for additional details, and his code on [GitHub](https://github.com/tfrausin/reconf). There is also a question and an answer in this [gRPC issue](https://github.com/grpc/grpc-go/issues/2091). - -The metadata approach is not type safe because everything must be converted to a string, and in Frausing's implementation, this requires a custom marshaling and unmarshaling implementation. Moreover, it seems that there is a larger overhead with this approach compared to the alternative with using separate gRPC interfaces. - -## The interceptor approach - -However, it would be interesting to consider (as suggested in the gRPC issue linked above) a generic implementation that will marshal/unmarshal a protobuf structure to/from a json string that can be passed as part of the metadata feature of gRPC. What do I mean by generic here? Well, the content of the metadata to be passed in the metadata structure of an RPC request/response message should be defined as a proto message type, and the marshaling functions should be automatic. Basically, the code generator should produce marshaling functions for the metadata, and a layer-specific method that accepts the request/response object and unmarshaled metadata type as generated by the protobuf for that metadata object. - -We should essentially generate the interceptor code that extract things and calls out to a function like this: - -```go -import pbm "github.com/relab/reconf/cfgproto" - -func (c *pbm.Config) Read(ctx context.Context, cfg pbm.Config) (pb.Config, error) { - if c.Epoch() == cfg.Epoch() { - return nil, nil - } - if c.Epoch() > cfg.Epoch() { - // client has old configuration; return current cfg - return cfg, nil - } else { - // client has newer configuration; return error??? - return nil, err - } -} -``` - -Not sure exactly what should be the generic signature for this thing. - -## Example using the `g` prefix approach, but with different naming scheme - -Here is an example of a `Storage` implementation with a reconfiguration layer. - -Assume the following protobuf message type `Config`, which is the metadata needed by the reconfiguration layer. - -```protobuf -message Config { - uint64 epoch = 1; - repeat string addr = 2; -} -``` - -Note that, we don't need to maintain two separate message types for the actual configuration (list of machines) and the epoch, since if the list of machines is empty, it won't take any space in the message. Empty fields will simply be removed during marshaling. - -On the server-side, our implementation of the reconfiguration layer would look something like this: - -**Caveat**: The following is not quite correct because we can't have both `cfg` and `req` arguments to the `Read` call below. So necessary adjustments must be made. In fact, the client-side is more accurately specified. But I'm still not sure I like it very much. - -```go -import pbm "github.com/relab/reconf/cfgproto" - -func (c *pbm.Config) Read(ctx context.Context, cfg pbm.Config, req pb.ReadRequest) (pb.State, error) { - if c.Epoch() == cfg.Epoch() { - return c.upper.Read(ctx, req) - } else { - return nil, fmt.Errorf("wrong epoch") - } -} - -func (s *pb.StorageSrv) Read(ctx context.Context, req pb.ReadRequest) (pb.State, error) { - // actual storage implementation -} -``` - -Here is an alternative design where we return the updated `pbm.Config`: - -```go -func (c *pbm.Config) Read(ctx context.Context, cfg pbm.Config, req pb.ReadRequest) (pb.State, pbm.Config, error) { - curEpoch, callEpoch := c.Epoch(), cfg.Epoch() - if curEpoch == callEpoch { - s, err := c.upper.Read(ctx, req) - return s, nil, err - } - if curEpoch > callEpoch { - return nil, curEpoch, fmt.Errorf("wrong epoch") - } else { - // server has outdated epoch; must reconfigure - } -} -``` - -On the client side, we would still call the `Read` method as if it were a regular gRPC method: - -```go - // the mgr returns custom config objects that produce appropriate metadata for the reconfiguration layer. - config, err := mgr.NewConfiguration(ids, qspec) - rreply, err := config.Read(ctx, &pb.ReadRequest{}) -``` - -The client-side reconfiguration layer (proxy), would then be implemented as follows: - -```go -func (c *reconfStorageClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*State, error) { - currConfig := c.GetConfig() - inArg := &pb.ConfigReadRequest{Config: currConfig, ReadRequest: in} - out := new(ConfigState) - err := grpc.Invoke(ctx, "/dev.ReconfStorage/Read", inArg, out, c.cc, opts...) - if err != nil { - return nil, err - } - if out.Epoch() != currConfig.Epoch() { - c.UpdateConfig(out.GetConfig()) - } - realOut := out.GetState() - return realOut, nil -} -``` - -Note that we need to generate a `ReconfStorage` proto to represent a combined interface: - -```protobuf -service ReconfStorage { - rpc Read(ConfigReadRequest) ConfigState; -} - -message ConfigReadRequest { - Config config = 1; - ReadRequest req = 2; -} - -message ConfigState { - Config confg = 1; - State state = 2; -} -``` - -Note that the `c.cc` connection points to the reconfiguration layer's `Read` method (in the `ReconfStorage` interface), which holds a combination of both the `ReadRequest` message and the `Config` message types. - -## Open Questions - -1. If our code generator produces these additional layering methods for us, do we need to register the server implementing all interfaces (both `pbm.Config` and `pb.Storage`) or can it be replaced with the top-layer only? - -2. Can we support multiple layers? For example, if we want to support both reconfiguration and all-to-all communication, and these features are best kept separate? - -3. The approach above is not so flexible in that a new layer will probably want to define templates for code generation. diff --git a/doc/migration.md b/doc/migration-v2-to-v3.md similarity index 100% rename from doc/migration.md rename to doc/migration-v2-to-v3.md From 0e522069558e77038e0ed06a70586eec4974d66c Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 11 Aug 2026 18:25:31 +0200 Subject: [PATCH 13/16] gorums: regenerate for the renamed API Generated output only, produced by make genproto. The type aliases injected into every generated file follow the Config rename, handler signatures follow the ServerContext rename, and the bundled static template follows both. The recorded protoc version also advances, since these files were last generated with an older release. --- cmd/protoc-gen-gorums/dev/zorums.pb.go | 2 +- .../dev/zorums_multicast_gorums.pb.go | 2 +- .../dev/zorums_quorumcall_gorums.pb.go | 2 +- ...s.pb.go => zorums_remotecall_gorums.pb.go} | 4 +- .../dev/zorums_server_gorums.pb.go | 54 +++++++++---------- .../dev/zorums_types_gorums.pb.go | 2 +- .../dev/zorums_unicast_gorums.pb.go | 2 +- .../gengorums/template_static.go | 6 +-- examples/storage/proto/storage.pb.go | 2 +- examples/storage/proto/storage_gorums.pb.go | 44 +++++++-------- gorums.pb.go | 18 ++++--- internal/stream/stream.pb.go | 2 +- internal/stream/stream_grpc.pb.go | 2 +- internal/tests/config/config.pb.go | 12 ++--- internal/tests/config/config_gorums.pb.go | 18 +++---- internal/tests/correctable/correctable.pb.go | 2 +- .../correctable/correctable_gorums.pb.go | 12 ++--- internal/tests/metadata/metadata.pb.go | 2 +- internal/tests/metadata/metadata_gorums.pb.go | 16 +++--- internal/tests/oneway/oneway.pb.go | 2 +- internal/tests/oneway/oneway_gorums.pb.go | 12 ++--- internal/tests/ordering/order.pb.go | 2 +- internal/tests/ordering/order_gorums.pb.go | 14 ++--- internal/tests/tls/tls.pb.go | 2 +- internal/tests/tls/tls_gorums.pb.go | 10 ++-- .../tests/unresponsive/unresponsive.pb.go | 2 +- .../unresponsive/unresponsive_gorums.pb.go | 10 ++-- 27 files changed, 130 insertions(+), 128 deletions(-) rename cmd/protoc-gen-gorums/dev/{zorums_rpc_gorums.pb.go => zorums_remotecall_gorums.pb.go} (84%) diff --git a/cmd/protoc-gen-gorums/dev/zorums.pb.go b/cmd/protoc-gen-gorums/dev/zorums.pb.go index 35949012..2312798e 100644 --- a/cmd/protoc-gen-gorums/dev/zorums.pb.go +++ b/cmd/protoc-gen-gorums/dev/zorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.1 // source: zorums.proto package dev diff --git a/cmd/protoc-gen-gorums/dev/zorums_multicast_gorums.pb.go b/cmd/protoc-gen-gorums/dev/zorums_multicast_gorums.pb.go index 7ad08d79..7ee52b73 100644 --- a/cmd/protoc-gen-gorums/dev/zorums_multicast_gorums.pb.go +++ b/cmd/protoc-gen-gorums/dev/zorums_multicast_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: zorums.proto package dev diff --git a/cmd/protoc-gen-gorums/dev/zorums_quorumcall_gorums.pb.go b/cmd/protoc-gen-gorums/dev/zorums_quorumcall_gorums.pb.go index 1b3d36d9..6641c24b 100644 --- a/cmd/protoc-gen-gorums/dev/zorums_quorumcall_gorums.pb.go +++ b/cmd/protoc-gen-gorums/dev/zorums_quorumcall_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: zorums.proto package dev diff --git a/cmd/protoc-gen-gorums/dev/zorums_rpc_gorums.pb.go b/cmd/protoc-gen-gorums/dev/zorums_remotecall_gorums.pb.go similarity index 84% rename from cmd/protoc-gen-gorums/dev/zorums_rpc_gorums.pb.go rename to cmd/protoc-gen-gorums/dev/zorums_remotecall_gorums.pb.go index 72d63ebd..9c336b03 100644 --- a/cmd/protoc-gen-gorums/dev/zorums_rpc_gorums.pb.go +++ b/cmd/protoc-gen-gorums/dev/zorums_remotecall_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: zorums.proto package dev @@ -20,5 +20,5 @@ const ( // GRPCCall plain gRPC call; testing that Gorums can ignore these, but that // they are added to the _grpc.pb.go generated file. func GRPCCall(ctx *NodeContext, in *Request) (*Response, error) { - return gorums.RPCCall[*Request, *Response](ctx, in, "dev.ZorumsService.GRPCCall") + return gorums.RemoteCall[*Request, *Response](ctx, in, "dev.ZorumsService.GRPCCall") } diff --git a/cmd/protoc-gen-gorums/dev/zorums_server_gorums.pb.go b/cmd/protoc-gen-gorums/dev/zorums_server_gorums.pb.go index 9fa5e1f5..7cbd96d9 100644 --- a/cmd/protoc-gen-gorums/dev/zorums_server_gorums.pb.go +++ b/cmd/protoc-gen-gorums/dev/zorums_server_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: zorums.proto package dev @@ -20,23 +20,23 @@ const ( // ZorumsService is the server-side API for the ZorumsService Service type ZorumsServiceServer interface { - GRPCCall(gorums.ServerCtx, *Request) (*Response, error) - QuorumCall(gorums.ServerCtx, *Request) (*Response, error) - QuorumCallEmpty(gorums.ServerCtx, *emptypb.Empty) (*Response, error) - QuorumCallEmpty2(gorums.ServerCtx, *Request) (*emptypb.Empty, error) - Multicast(gorums.ServerCtx, *Request) - Multicast2(gorums.ServerCtx, *Request) - Multicast3(gorums.ServerCtx, *Request) - Multicast4(gorums.ServerCtx, *emptypb.Empty) - QuorumCallStream(gorums.ServerCtx, *Request, func(*Response)) - QuorumCallStreamWithEmpty(gorums.ServerCtx, *Request, func(*emptypb.Empty)) - QuorumCallStreamWithEmpty2(gorums.ServerCtx, *emptypb.Empty, func(*Response)) - Unicast(gorums.ServerCtx, *Request) - Unicast2(gorums.ServerCtx, *Request) + GRPCCall(gorums.ServerContext, *Request) (*Response, error) + QuorumCall(gorums.ServerContext, *Request) (*Response, error) + QuorumCallEmpty(gorums.ServerContext, *emptypb.Empty) (*Response, error) + QuorumCallEmpty2(gorums.ServerContext, *Request) (*emptypb.Empty, error) + Multicast(gorums.ServerContext, *Request) + Multicast2(gorums.ServerContext, *Request) + Multicast3(gorums.ServerContext, *Request) + Multicast4(gorums.ServerContext, *emptypb.Empty) + QuorumCallStream(gorums.ServerContext, *Request, func(*Response)) + QuorumCallStreamWithEmpty(gorums.ServerContext, *Request, func(*emptypb.Empty)) + QuorumCallStreamWithEmpty2(gorums.ServerContext, *emptypb.Empty, func(*Response)) + Unicast(gorums.ServerContext, *Request) + Unicast2(gorums.ServerContext, *Request) } func RegisterZorumsServiceServer(srv *gorums.Server, impl ZorumsServiceServer) { - srv.RegisterHandler("dev.ZorumsService.GRPCCall", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.GRPCCall", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) resp, err := impl.GRPCCall(ctx, req) if err != nil { @@ -44,7 +44,7 @@ func RegisterZorumsServiceServer(srv *gorums.Server, impl ZorumsServiceServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("dev.ZorumsService.QuorumCall", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.QuorumCall", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) resp, err := impl.QuorumCall(ctx, req) if err != nil { @@ -52,7 +52,7 @@ func RegisterZorumsServiceServer(srv *gorums.Server, impl ZorumsServiceServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("dev.ZorumsService.QuorumCallEmpty", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.QuorumCallEmpty", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*emptypb.Empty](in) resp, err := impl.QuorumCallEmpty(ctx, req) if err != nil { @@ -60,7 +60,7 @@ func RegisterZorumsServiceServer(srv *gorums.Server, impl ZorumsServiceServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("dev.ZorumsService.QuorumCallEmpty2", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.QuorumCallEmpty2", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) resp, err := impl.QuorumCallEmpty2(ctx, req) if err != nil { @@ -68,27 +68,27 @@ func RegisterZorumsServiceServer(srv *gorums.Server, impl ZorumsServiceServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("dev.ZorumsService.Multicast", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.Multicast", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) impl.Multicast(ctx, req) return nil, nil }) - srv.RegisterHandler("dev.ZorumsService.Multicast2", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.Multicast2", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) impl.Multicast2(ctx, req) return nil, nil }) - srv.RegisterHandler("dev.ZorumsService.Multicast3", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.Multicast3", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) impl.Multicast3(ctx, req) return nil, nil }) - srv.RegisterHandler("dev.ZorumsService.Multicast4", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.Multicast4", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*emptypb.Empty](in) impl.Multicast4(ctx, req) return nil, nil }) - srv.RegisterHandler("dev.ZorumsService.QuorumCallStream", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.QuorumCallStream", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) impl.QuorumCallStream(ctx, req, func(resp *Response) { out := gorums.NewResponseMessage(in, resp) @@ -96,7 +96,7 @@ func RegisterZorumsServiceServer(srv *gorums.Server, impl ZorumsServiceServer) { }) return nil, nil }) - srv.RegisterHandler("dev.ZorumsService.QuorumCallStreamWithEmpty", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.QuorumCallStreamWithEmpty", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) impl.QuorumCallStreamWithEmpty(ctx, req, func(resp *emptypb.Empty) { out := gorums.NewResponseMessage(in, resp) @@ -104,7 +104,7 @@ func RegisterZorumsServiceServer(srv *gorums.Server, impl ZorumsServiceServer) { }) return nil, nil }) - srv.RegisterHandler("dev.ZorumsService.QuorumCallStreamWithEmpty2", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.QuorumCallStreamWithEmpty2", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*emptypb.Empty](in) impl.QuorumCallStreamWithEmpty2(ctx, req, func(resp *Response) { out := gorums.NewResponseMessage(in, resp) @@ -112,12 +112,12 @@ func RegisterZorumsServiceServer(srv *gorums.Server, impl ZorumsServiceServer) { }) return nil, nil }) - srv.RegisterHandler("dev.ZorumsService.Unicast", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.Unicast", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) impl.Unicast(ctx, req) return nil, nil }) - srv.RegisterHandler("dev.ZorumsService.Unicast2", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("dev.ZorumsService.Unicast2", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) impl.Unicast2(ctx, req) return nil, nil diff --git a/cmd/protoc-gen-gorums/dev/zorums_types_gorums.pb.go b/cmd/protoc-gen-gorums/dev/zorums_types_gorums.pb.go index 0bca6159..114873be 100644 --- a/cmd/protoc-gen-gorums/dev/zorums_types_gorums.pb.go +++ b/cmd/protoc-gen-gorums/dev/zorums_types_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: zorums.proto package dev diff --git a/cmd/protoc-gen-gorums/dev/zorums_unicast_gorums.pb.go b/cmd/protoc-gen-gorums/dev/zorums_unicast_gorums.pb.go index a5f6ab50..64d529ab 100644 --- a/cmd/protoc-gen-gorums/dev/zorums_unicast_gorums.pb.go +++ b/cmd/protoc-gen-gorums/dev/zorums_unicast_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: zorums.proto package dev diff --git a/cmd/protoc-gen-gorums/gengorums/template_static.go b/cmd/protoc-gen-gorums/gengorums/template_static.go index e098426a..23f77f4a 100644 --- a/cmd/protoc-gen-gorums/gengorums/template_static.go +++ b/cmd/protoc-gen-gorums/gengorums/template_static.go @@ -5,11 +5,11 @@ package gengorums // pkgIdentMap maps from package name to one of the package's identifiers. // These identifiers are used by the Gorums protoc plugin to generate import statements. -var pkgIdentMap = map[string]string{"github.com/relab/gorums": "ConfigContext"} +var pkgIdentMap = map[string]string{"github.com/relab/gorums": "Config"} // 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{"Config", "ConfigContext", "Node", "NodeContext"} var staticCode = `// The type aliases below are useful Gorums types that we make accessible // from generated code. These names therefore become reserved identifiers, @@ -25,7 +25,7 @@ var staticCode = `// The type aliases below are useful Gorums types that we make // automatically discover them and add them to the reserved identifiers list. type ( - Configuration = gorums.Configuration + Config = gorums.Config Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext diff --git a/examples/storage/proto/storage.pb.go b/examples/storage/proto/storage.pb.go index 87cda7a1..d7b5ad42 100644 --- a/examples/storage/proto/storage.pb.go +++ b/examples/storage/proto/storage.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.1 // source: storage/proto/storage.proto package proto diff --git a/examples/storage/proto/storage_gorums.pb.go b/examples/storage/proto/storage_gorums.pb.go index 842055bc..44145625 100644 --- a/examples/storage/proto/storage_gorums.pb.go +++ b/examples/storage/proto/storage_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: storage/proto/storage.proto package proto @@ -32,7 +32,7 @@ const ( // automatically discover them and add them to the reserved identifiers list. type ( - Configuration = gorums.Configuration + Config = gorums.Config Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -56,13 +56,13 @@ var _ emptypb.Empty // ReadRPC executes a Read RPC on a single node and // returns the value for the provided key. func ReadRPC(ctx *NodeContext, in *ReadRequest) (*ReadResponse, error) { - return gorums.RPCCall[*ReadRequest, *ReadResponse](ctx, in, "proto.Storage.ReadRPC") + return gorums.RemoteCall[*ReadRequest, *ReadResponse](ctx, in, "proto.Storage.ReadRPC") } // WriteRPC executes a Write RPC on a single node and // returns true if the value was updated. func WriteRPC(ctx *NodeContext, in *WriteRequest) (*WriteResponse, error) { - return gorums.RPCCall[*WriteRequest, *WriteResponse](ctx, in, "proto.Storage.WriteRPC") + return gorums.RemoteCall[*WriteRequest, *WriteResponse](ctx, in, "proto.Storage.WriteRPC") } // WriteUnicast executes a one-way Write unicast call on a single node. @@ -124,19 +124,19 @@ func ReadCorrectable(ctx *ConfigContext, in *ReadRequest, opts ...gorums.CallOpt // Storage is the server-side API for the Storage Service type StorageServer interface { - ReadRPC(gorums.ServerCtx, *ReadRequest) (*ReadResponse, error) - WriteRPC(gorums.ServerCtx, *WriteRequest) (*WriteResponse, error) - WriteUnicast(gorums.ServerCtx, *WriteRequest) - WriteMulticast(gorums.ServerCtx, *WriteRequest) - ReadQC(gorums.ServerCtx, *ReadRequest) (*ReadResponse, error) - WriteQC(gorums.ServerCtx, *WriteRequest) (*WriteResponse, error) - ReadNestedQC(gorums.ServerCtx, *ReadRequest) (*ReadResponse, error) - WriteNestedMulticast(gorums.ServerCtx, *WriteRequest) (*WriteResponse, error) - ReadCorrectable(gorums.ServerCtx, *ReadRequest, func(*ReadResponse)) + ReadRPC(gorums.ServerContext, *ReadRequest) (*ReadResponse, error) + WriteRPC(gorums.ServerContext, *WriteRequest) (*WriteResponse, error) + WriteUnicast(gorums.ServerContext, *WriteRequest) + WriteMulticast(gorums.ServerContext, *WriteRequest) + ReadQC(gorums.ServerContext, *ReadRequest) (*ReadResponse, error) + WriteQC(gorums.ServerContext, *WriteRequest) (*WriteResponse, error) + ReadNestedQC(gorums.ServerContext, *ReadRequest) (*ReadResponse, error) + WriteNestedMulticast(gorums.ServerContext, *WriteRequest) (*WriteResponse, error) + ReadCorrectable(gorums.ServerContext, *ReadRequest, func(*ReadResponse)) } func RegisterStorageServer(srv *gorums.Server, impl StorageServer) { - srv.RegisterHandler("proto.Storage.ReadRPC", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("proto.Storage.ReadRPC", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*ReadRequest](in) resp, err := impl.ReadRPC(ctx, req) if err != nil { @@ -144,7 +144,7 @@ func RegisterStorageServer(srv *gorums.Server, impl StorageServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("proto.Storage.WriteRPC", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("proto.Storage.WriteRPC", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*WriteRequest](in) resp, err := impl.WriteRPC(ctx, req) if err != nil { @@ -152,17 +152,17 @@ func RegisterStorageServer(srv *gorums.Server, impl StorageServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("proto.Storage.WriteUnicast", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("proto.Storage.WriteUnicast", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*WriteRequest](in) impl.WriteUnicast(ctx, req) return nil, nil }) - srv.RegisterHandler("proto.Storage.WriteMulticast", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("proto.Storage.WriteMulticast", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*WriteRequest](in) impl.WriteMulticast(ctx, req) return nil, nil }) - srv.RegisterHandler("proto.Storage.ReadQC", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("proto.Storage.ReadQC", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*ReadRequest](in) resp, err := impl.ReadQC(ctx, req) if err != nil { @@ -170,7 +170,7 @@ func RegisterStorageServer(srv *gorums.Server, impl StorageServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("proto.Storage.WriteQC", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("proto.Storage.WriteQC", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*WriteRequest](in) resp, err := impl.WriteQC(ctx, req) if err != nil { @@ -178,7 +178,7 @@ func RegisterStorageServer(srv *gorums.Server, impl StorageServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("proto.Storage.ReadNestedQC", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("proto.Storage.ReadNestedQC", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*ReadRequest](in) resp, err := impl.ReadNestedQC(ctx, req) if err != nil { @@ -186,7 +186,7 @@ func RegisterStorageServer(srv *gorums.Server, impl StorageServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("proto.Storage.WriteNestedMulticast", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("proto.Storage.WriteNestedMulticast", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*WriteRequest](in) resp, err := impl.WriteNestedMulticast(ctx, req) if err != nil { @@ -194,7 +194,7 @@ func RegisterStorageServer(srv *gorums.Server, impl StorageServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("proto.Storage.ReadCorrectable", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("proto.Storage.ReadCorrectable", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*ReadRequest](in) impl.ReadCorrectable(ctx, req, func(resp *ReadResponse) { out := gorums.NewResponseMessage(in, resp) diff --git a/gorums.pb.go b/gorums.pb.go index 6d52b563..41a1db1c 100644 --- a/gorums.pb.go +++ b/gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.1 // source: gorums.proto package gorums @@ -26,8 +26,8 @@ var file_gorums_proto_extTypes = []protoimpl.ExtensionInfo{ ExtendedType: (*descriptorpb.MethodOptions)(nil), ExtensionType: (*bool)(nil), Field: 50001, - Name: "gorums.rpc", - Tag: "varint,50001,opt,name=rpc", + Name: "gorums.remotecall", + Tag: "varint,50001,opt,name=remotecall", Filename: "gorums.proto", }, { @@ -60,8 +60,8 @@ var file_gorums_proto_extTypes = []protoimpl.ExtensionInfo{ var ( // call types // - // optional bool rpc = 50001; - E_Rpc = &file_gorums_proto_extTypes[0] + // optional bool remotecall = 50001; + E_Remotecall = &file_gorums_proto_extTypes[0] // only for internal use; no need to set manually // optional bool unicast = 50002; E_Unicast = &file_gorums_proto_extTypes[1] // optional bool multicast = 50003; @@ -74,8 +74,10 @@ var File_gorums_proto protoreflect.FileDescriptor const file_gorums_proto_rawDesc = "" + "\n" + - "\fgorums.proto\x12\x06gorums\x1a google/protobuf/descriptor.proto:2\n" + - "\x03rpc\x12\x1e.google.protobuf.MethodOptions\x18ц\x03 \x01(\bR\x03rpc::\n" + + "\fgorums.proto\x12\x06gorums\x1a google/protobuf/descriptor.proto:@\n" + + "\n" + + "remotecall\x12\x1e.google.protobuf.MethodOptions\x18ц\x03 \x01(\bR\n" + + "remotecall::\n" + "\aunicast\x12\x1e.google.protobuf.MethodOptions\x18҆\x03 \x01(\bR\aunicast:>\n" + "\tmulticast\x12\x1e.google.protobuf.MethodOptions\x18ӆ\x03 \x01(\bR\tmulticast:@\n" + "\n" + @@ -86,7 +88,7 @@ var file_gorums_proto_goTypes = []any{ (*descriptorpb.MethodOptions)(nil), // 0: google.protobuf.MethodOptions } var file_gorums_proto_depIdxs = []int32{ - 0, // 0: gorums.rpc:extendee -> google.protobuf.MethodOptions + 0, // 0: gorums.remotecall:extendee -> google.protobuf.MethodOptions 0, // 1: gorums.unicast:extendee -> google.protobuf.MethodOptions 0, // 2: gorums.multicast:extendee -> google.protobuf.MethodOptions 0, // 3: gorums.quorumcall:extendee -> google.protobuf.MethodOptions diff --git a/internal/stream/stream.pb.go b/internal/stream/stream.pb.go index 795ad2f5..8333eba6 100644 --- a/internal/stream/stream.pb.go +++ b/internal/stream/stream.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.1 // source: internal/stream/stream.proto package stream diff --git a/internal/stream/stream_grpc.pb.go b/internal/stream/stream_grpc.pb.go index c40925d9..a7fd7d0b 100644 --- a/internal/stream/stream_grpc.pb.go +++ b/internal/stream/stream_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.1 -// - protoc v7.34.1 +// - protoc v7.35.1 // source: internal/stream/stream.proto package stream diff --git a/internal/tests/config/config.pb.go b/internal/tests/config/config.pb.go index 1d843a8f..a9776288 100644 --- a/internal/tests/config/config.pb.go +++ b/internal/tests/config/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.1 // source: config/config.proto package config @@ -158,10 +158,10 @@ const file_config_config_proto_rawDesc = "" + "\x03Num\x18\x01 \x01(\x04R\x03Num\"0\n" + "\bResponse\x12\x12\n" + "\x04Name\x18\x01 \x01(\tR\x04Name\x12\x10\n" + - "\x03Num\x18\x02 \x01(\x04R\x03Num2?\n" + + "\x03Num\x18\x02 \x01(\x04R\x03Num2=\n" + "\n" + - "ConfigTest\x121\n" + - "\x06Config\x12\x0f.config.Request\x1a\x10.config.Response\"\x04\xa0\xb5\x18\x01B+Z$github.com/relab/gorums/tests/config\x92\x03\x02\b\x02b\beditionsp\xe8\a" + "ConfigTest\x12/\n" + + "\x04Read\x12\x0f.config.Request\x1a\x10.config.Response\"\x04\xa0\xb5\x18\x01B+Z$github.com/relab/gorums/tests/config\x92\x03\x02\b\x02b\beditionsp\xe8\a" var file_config_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_config_config_proto_goTypes = []any{ @@ -169,8 +169,8 @@ var file_config_config_proto_goTypes = []any{ (*Response)(nil), // 1: config.Response } var file_config_config_proto_depIdxs = []int32{ - 0, // 0: config.ConfigTest.Config:input_type -> config.Request - 1, // 1: config.ConfigTest.Config:output_type -> config.Response + 0, // 0: config.ConfigTest.Read:input_type -> config.Request + 1, // 1: config.ConfigTest.Read:output_type -> config.Response 1, // [1:2] is the sub-list for method output_type 0, // [0:1] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name diff --git a/internal/tests/config/config_gorums.pb.go b/internal/tests/config/config_gorums.pb.go index 84728d83..ac5d039f 100644 --- a/internal/tests/config/config_gorums.pb.go +++ b/internal/tests/config/config_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: config/config.proto package config @@ -31,7 +31,7 @@ const ( // automatically discover them and add them to the reserved identifiers list. type ( - Configuration = gorums.Configuration + Config = gorums.Config Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -43,29 +43,29 @@ type AsyncResponse = *gorums.Async[*Response] // CorrectableResponse is a correctable object for quorum calls returning *Response. type CorrectableResponse = *gorums.Correctable[*Response] -// Config is a quorum call invoked on all nodes in the configuration, +// Read is a quorum call invoked on all nodes in the configuration, // with the same argument in. Use terminal methods like Majority(), First(), // or Threshold(n) to retrieve the aggregated result. // // Example: // -// resp, err := Config(ctx, in).Majority() -func Config(ctx *ConfigContext, in *Request, opts ...gorums.CallOption) *gorums.Responses[*Response] { +// resp, err := Read(ctx, in).Majority() +func Read(ctx *ConfigContext, in *Request, opts ...gorums.CallOption) *gorums.Responses[*Response] { return gorums.QuorumCall[*Request, *Response]( - ctx, in, "config.ConfigTest.Config", + ctx, in, "config.ConfigTest.Read", opts..., ) } // ConfigTest is the server-side API for the ConfigTest Service type ConfigTestServer interface { - Config(gorums.ServerCtx, *Request) (*Response, error) + Read(gorums.ServerContext, *Request) (*Response, error) } func RegisterConfigTestServer(srv *gorums.Server, impl ConfigTestServer) { - srv.RegisterHandler("config.ConfigTest.Config", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("config.ConfigTest.Read", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) - resp, err := impl.Config(ctx, req) + resp, err := impl.Read(ctx, req) if err != nil { return nil, err } diff --git a/internal/tests/correctable/correctable.pb.go b/internal/tests/correctable/correctable.pb.go index c1207c51..46c0d03c 100644 --- a/internal/tests/correctable/correctable.pb.go +++ b/internal/tests/correctable/correctable.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.1 // source: correctable/correctable.proto package correctable diff --git a/internal/tests/correctable/correctable_gorums.pb.go b/internal/tests/correctable/correctable_gorums.pb.go index 3cb41e8f..266bfdee 100644 --- a/internal/tests/correctable/correctable_gorums.pb.go +++ b/internal/tests/correctable/correctable_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: correctable/correctable.proto package correctable @@ -31,7 +31,7 @@ const ( // automatically discover them and add them to the reserved identifiers list. type ( - Configuration = gorums.Configuration + Config = gorums.Config Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -74,12 +74,12 @@ func CorrectableStream(ctx *ConfigContext, in *Request, opts ...gorums.CallOptio // CorrectableTest is the server-side API for the CorrectableTest Service type CorrectableTestServer interface { - Correctable(gorums.ServerCtx, *Request) (*Response, error) - CorrectableStream(gorums.ServerCtx, *Request, func(*Response)) + Correctable(gorums.ServerContext, *Request) (*Response, error) + CorrectableStream(gorums.ServerContext, *Request, func(*Response)) } func RegisterCorrectableTestServer(srv *gorums.Server, impl CorrectableTestServer) { - srv.RegisterHandler("correctable.CorrectableTest.Correctable", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("correctable.CorrectableTest.Correctable", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) resp, err := impl.Correctable(ctx, req) if err != nil { @@ -87,7 +87,7 @@ func RegisterCorrectableTestServer(srv *gorums.Server, impl CorrectableTestServe } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("correctable.CorrectableTest.CorrectableStream", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("correctable.CorrectableTest.CorrectableStream", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) impl.CorrectableStream(ctx, req, func(resp *Response) { out := gorums.NewResponseMessage(in, resp) diff --git a/internal/tests/metadata/metadata.pb.go b/internal/tests/metadata/metadata.pb.go index 94ede83a..a3a0c80f 100644 --- a/internal/tests/metadata/metadata.pb.go +++ b/internal/tests/metadata/metadata.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.1 // source: metadata/metadata.proto package metadata diff --git a/internal/tests/metadata/metadata_gorums.pb.go b/internal/tests/metadata/metadata_gorums.pb.go index a30a5087..3833aca1 100644 --- a/internal/tests/metadata/metadata_gorums.pb.go +++ b/internal/tests/metadata/metadata_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: metadata/metadata.proto package metadata @@ -32,7 +32,7 @@ const ( // automatically discover them and add them to the reserved identifiers list. type ( - Configuration = gorums.Configuration + Config = gorums.Config Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -40,22 +40,22 @@ type ( // IDFromMD returns the 'id' field from the metadata. func IDFromMD(ctx *NodeContext, in *emptypb.Empty) (*NodeID, error) { - return gorums.RPCCall[*emptypb.Empty, *NodeID](ctx, in, "metadata.MetadataTest.IDFromMD") + return gorums.RemoteCall[*emptypb.Empty, *NodeID](ctx, in, "metadata.MetadataTest.IDFromMD") } // WhatIP returns the address of the client that calls it. func WhatIP(ctx *NodeContext, in *emptypb.Empty) (*IPAddr, error) { - return gorums.RPCCall[*emptypb.Empty, *IPAddr](ctx, in, "metadata.MetadataTest.WhatIP") + return gorums.RemoteCall[*emptypb.Empty, *IPAddr](ctx, in, "metadata.MetadataTest.WhatIP") } // MetadataTest is the server-side API for the MetadataTest Service type MetadataTestServer interface { - IDFromMD(gorums.ServerCtx, *emptypb.Empty) (*NodeID, error) - WhatIP(gorums.ServerCtx, *emptypb.Empty) (*IPAddr, error) + IDFromMD(gorums.ServerContext, *emptypb.Empty) (*NodeID, error) + WhatIP(gorums.ServerContext, *emptypb.Empty) (*IPAddr, error) } func RegisterMetadataTestServer(srv *gorums.Server, impl MetadataTestServer) { - srv.RegisterHandler("metadata.MetadataTest.IDFromMD", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("metadata.MetadataTest.IDFromMD", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*emptypb.Empty](in) resp, err := impl.IDFromMD(ctx, req) if err != nil { @@ -63,7 +63,7 @@ func RegisterMetadataTestServer(srv *gorums.Server, impl MetadataTestServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("metadata.MetadataTest.WhatIP", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("metadata.MetadataTest.WhatIP", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*emptypb.Empty](in) resp, err := impl.WhatIP(ctx, req) if err != nil { diff --git a/internal/tests/oneway/oneway.pb.go b/internal/tests/oneway/oneway.pb.go index 3855a758..59cb19f7 100644 --- a/internal/tests/oneway/oneway.pb.go +++ b/internal/tests/oneway/oneway.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.1 // source: oneway/oneway.proto package oneway diff --git a/internal/tests/oneway/oneway_gorums.pb.go b/internal/tests/oneway/oneway_gorums.pb.go index f3c0881d..5e7c76ca 100644 --- a/internal/tests/oneway/oneway_gorums.pb.go +++ b/internal/tests/oneway/oneway_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: oneway/oneway.proto package oneway @@ -31,7 +31,7 @@ const ( // automatically discover them and add them to the reserved identifiers list. type ( - Configuration = gorums.Configuration + Config = gorums.Config Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -51,17 +51,17 @@ func Multicast(ctx *ConfigContext, in *Request, opts ...gorums.CallOption) error // OnewayTest is the server-side API for the OnewayTest Service type OnewayTestServer interface { - Unicast(gorums.ServerCtx, *Request) - Multicast(gorums.ServerCtx, *Request) + Unicast(gorums.ServerContext, *Request) + Multicast(gorums.ServerContext, *Request) } func RegisterOnewayTestServer(srv *gorums.Server, impl OnewayTestServer) { - srv.RegisterHandler("oneway.OnewayTest.Unicast", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("oneway.OnewayTest.Unicast", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) impl.Unicast(ctx, req) return nil, nil }) - srv.RegisterHandler("oneway.OnewayTest.Multicast", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("oneway.OnewayTest.Multicast", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) impl.Multicast(ctx, req) return nil, nil diff --git a/internal/tests/ordering/order.pb.go b/internal/tests/ordering/order.pb.go index 6fefa047..965c5f90 100644 --- a/internal/tests/ordering/order.pb.go +++ b/internal/tests/ordering/order.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.1 // source: ordering/order.proto package ordering diff --git a/internal/tests/ordering/order_gorums.pb.go b/internal/tests/ordering/order_gorums.pb.go index 6fb7815b..7404af37 100644 --- a/internal/tests/ordering/order_gorums.pb.go +++ b/internal/tests/ordering/order_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: ordering/order.proto package ordering @@ -31,7 +31,7 @@ const ( // automatically discover them and add them to the reserved identifiers list. type ( - Configuration = gorums.Configuration + Config = gorums.Config Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -59,17 +59,17 @@ func QuorumCall(ctx *ConfigContext, in *Request, opts ...gorums.CallOption) *gor // UnaryRPC is an RPC call invoked on the node in ctx. func UnaryRPC(ctx *NodeContext, in *Request) (*Response, error) { - return gorums.RPCCall[*Request, *Response](ctx, in, "ordering.GorumsTest.UnaryRPC") + return gorums.RemoteCall[*Request, *Response](ctx, in, "ordering.GorumsTest.UnaryRPC") } // GorumsTest is the server-side API for the GorumsTest Service type GorumsTestServer interface { - QuorumCall(gorums.ServerCtx, *Request) (*Response, error) - UnaryRPC(gorums.ServerCtx, *Request) (*Response, error) + QuorumCall(gorums.ServerContext, *Request) (*Response, error) + UnaryRPC(gorums.ServerContext, *Request) (*Response, error) } func RegisterGorumsTestServer(srv *gorums.Server, impl GorumsTestServer) { - srv.RegisterHandler("ordering.GorumsTest.QuorumCall", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("ordering.GorumsTest.QuorumCall", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) resp, err := impl.QuorumCall(ctx, req) if err != nil { @@ -77,7 +77,7 @@ func RegisterGorumsTestServer(srv *gorums.Server, impl GorumsTestServer) { } return gorums.NewResponseMessage(in, resp), nil }) - srv.RegisterHandler("ordering.GorumsTest.UnaryRPC", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("ordering.GorumsTest.UnaryRPC", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) resp, err := impl.UnaryRPC(ctx, req) if err != nil { diff --git a/internal/tests/tls/tls.pb.go b/internal/tests/tls/tls.pb.go index 35b6796e..688b29f2 100644 --- a/internal/tests/tls/tls.pb.go +++ b/internal/tests/tls/tls.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.1 // source: tls/tls.proto package tls diff --git a/internal/tests/tls/tls_gorums.pb.go b/internal/tests/tls/tls_gorums.pb.go index 50ca6706..f6ae84f6 100644 --- a/internal/tests/tls/tls_gorums.pb.go +++ b/internal/tests/tls/tls_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: tls/tls.proto package tls @@ -31,7 +31,7 @@ const ( // automatically discover them and add them to the reserved identifiers list. type ( - Configuration = gorums.Configuration + Config = gorums.Config Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -39,16 +39,16 @@ type ( // TestTLS is an RPC call invoked on the node in ctx. func TestTLS(ctx *NodeContext, in *Request) (*Response, error) { - return gorums.RPCCall[*Request, *Response](ctx, in, "tls.TLS.TestTLS") + return gorums.RemoteCall[*Request, *Response](ctx, in, "tls.TLS.TestTLS") } // TLS is the server-side API for the TLS Service type TLSServer interface { - TestTLS(gorums.ServerCtx, *Request) (*Response, error) + TestTLS(gorums.ServerContext, *Request) (*Response, error) } func RegisterTLSServer(srv *gorums.Server, impl TLSServer) { - srv.RegisterHandler("tls.TLS.TestTLS", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("tls.TLS.TestTLS", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Request](in) resp, err := impl.TestTLS(ctx, req) if err != nil { diff --git a/internal/tests/unresponsive/unresponsive.pb.go b/internal/tests/unresponsive/unresponsive.pb.go index 05d36d79..a46b3022 100644 --- a/internal/tests/unresponsive/unresponsive.pb.go +++ b/internal/tests/unresponsive/unresponsive.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.1 // source: unresponsive/unresponsive.proto package unresponsive diff --git a/internal/tests/unresponsive/unresponsive_gorums.pb.go b/internal/tests/unresponsive/unresponsive_gorums.pb.go index 6ce49eba..256406f9 100644 --- a/internal/tests/unresponsive/unresponsive_gorums.pb.go +++ b/internal/tests/unresponsive/unresponsive_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.11.0-devel -// protoc v7.34.1 +// protoc v7.35.1 // source: unresponsive/unresponsive.proto package unresponsive @@ -31,7 +31,7 @@ const ( // automatically discover them and add them to the reserved identifiers list. type ( - Configuration = gorums.Configuration + Config = gorums.Config Node = gorums.Node NodeContext = gorums.NodeContext ConfigContext = gorums.ConfigContext @@ -39,16 +39,16 @@ type ( // TestUnresponsive is an RPC call invoked on the node in ctx. func TestUnresponsive(ctx *NodeContext, in *Empty) (*Empty, error) { - return gorums.RPCCall[*Empty, *Empty](ctx, in, "unresponsive.Unresponsive.TestUnresponsive") + return gorums.RemoteCall[*Empty, *Empty](ctx, in, "unresponsive.Unresponsive.TestUnresponsive") } // Unresponsive is the server-side API for the Unresponsive Service type UnresponsiveServer interface { - TestUnresponsive(gorums.ServerCtx, *Empty) (*Empty, error) + TestUnresponsive(gorums.ServerContext, *Empty) (*Empty, error) } func RegisterUnresponsiveServer(srv *gorums.Server, impl UnresponsiveServer) { - srv.RegisterHandler("unresponsive.Unresponsive.TestUnresponsive", func(ctx gorums.ServerCtx, in *gorums.Message) (*gorums.Message, error) { + srv.RegisterHandler("unresponsive.Unresponsive.TestUnresponsive", func(ctx gorums.ServerContext, in *gorums.Message) (*gorums.Message, error) { req := gorums.AsProto[*Empty](in) resp, err := impl.TestUnresponsive(ctx, req) if err != nil { From 39e91a0648f424ededf69620c0e96b925e1676df Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Wed, 12 Aug 2026 12:22:25 +0200 Subject: [PATCH 14/16] gorums: name the comparator doc comments after their renamed vars Config.SortBy became Sort and the comparators gained a By prefix, but the doc comments on ByID and ByLatency kept their old subjects. A Go doc comment must open with the name it documents, so as written neither rendered as documentation for its var. --- node.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node.go b/node.go index 39601c86..deeb2356 100644 --- a/node.go +++ b/node.go @@ -314,7 +314,7 @@ func (n *Node) Latency() time.Duration { return n.router.Latency() } -// ID compares nodes by their identifier in increasing order. +// ByID compares nodes by their identifier in increasing order. // It is compatible with [slices.SortFunc] and [Config.Sort]. var ByID = func(a, b *Node) int { return cmp.Compare(a.id, b.id) @@ -336,7 +336,7 @@ var ByLastError = func(a, b *Node) int { } } -// Latency compares nodes by their current latency estimate in ascending order. +// ByLatency compares nodes by their current latency estimate in ascending order. // Nodes with no measurement yet (negative latency value) sort after nodes with a // measurement. It is compatible with [slices.SortFunc] and [Config.Sort]. var ByLatency = func(a, b *Node) int { From d27a32174a70fb9ec1c3bc54defa2f4c3150c446 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Wed, 12 Aug 2026 12:24:24 +0200 Subject: [PATCH 15/16] gorums: drop the unused node parameter in the interceptor test The identity MapRequest transform names a node it never reads. Naming it _ says the parameter is deliberately ignored, which is what the two neighbouring benchmark cases already do. --- call_client_interceptor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/call_client_interceptor_test.go b/call_client_interceptor_test.go index fe8bba96..a9e47d43 100644 --- a/call_client_interceptor_test.go +++ b/call_client_interceptor_test.go @@ -161,7 +161,7 @@ func TestCustomInterceptorWithMapRequest(t *testing.T) { CountingInterceptor[*pb.StringValue, *pb.StringValue](&count), // Built-in: transform request (identity transform for this test) gorums.MapRequest[*pb.StringValue, *pb.StringValue]( - func(req *pb.StringValue, node *gorums.Node) *pb.StringValue { + func(req *pb.StringValue, _ *gorums.Node) *pb.StringValue { return req }, ), From 200352f42b4b18394c39d07100ea9ab871f58f5b Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Wed, 12 Aug 2026 14:19:21 +0200 Subject: [PATCH 16/16] doc: correct the interceptor labels and correctable server signature Five headings under "Interceptors for Request/Response Transformation" called their subjects ServerInterceptors. MapRequest, MapResponse and the three worked examples all take a CallContext and wrap a ResponseSeq, so they are ClientInterceptors; the server-side section below them is unaffected. The StorageServer interface and its worked implementation also gave ReadCorrectable a send callback returning an error, and an error return of its own. Generated streaming-correctable methods take func(*Response) and return nothing, so neither was implementable as written. --- doc/user-guide.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/user-guide.md b/doc/user-guide.md index 651420cd..c1a58848 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -226,7 +226,7 @@ type StorageServer interface { WriteUnicast(ctx gorums.ServerContext, request *WriteRequest) WriteMulticast(ctx gorums.ServerContext, request *WriteRequest) ReadQC(ctx gorums.ServerContext, request *ReadRequest) (response *ReadResponse, err error) - ReadCorrectable(ctx gorums.ServerContext, request *ReadRequest, send func(response *ReadResponse) error) error + ReadCorrectable(ctx gorums.ServerContext, request *ReadRequest, send func(response *ReadResponse)) } ``` @@ -275,10 +275,10 @@ func (srv *storageSrv) ReadQC(_ gorums.ServerContext, req *ReadRequest) (resp *R return srv.state, nil } -func (srv *storageSrv) ReadCorrectable(_ gorums.ServerContext, req *ReadRequest, send func(response *ReadResponse) error) error { +func (srv *storageSrv) ReadCorrectable(_ gorums.ServerContext, req *ReadRequest, send func(response *ReadResponse)) { srv.mut.Lock() defer srv.mut.Unlock() - return send(srv.state) + send(srv.state) } ``` @@ -864,7 +864,7 @@ func RequireAllSuccess(resp *gorums.Responses[*Response]) (*Response, error) { Gorums provides interceptors to transform requests and responses on a per-node basis. Interceptors are passed as call options and can be chained together. -### MapRequest ServerInterceptor +### MapRequest ClientInterceptor Transform requests before sending to each node: @@ -880,7 +880,7 @@ resp, err := WriteQC(cfgCtx, req, ).Majority() ``` -### MapResponse ServerInterceptor +### MapResponse ClientInterceptor Transform responses received from each node: @@ -950,7 +950,7 @@ resp, err := ReadQC(cfgCtx, req, ).Majority() ``` -#### Example: Logging ServerInterceptor +#### Example: Logging ClientInterceptor Create a logging interceptor that wraps the response iterator: @@ -987,7 +987,7 @@ resp, err := ReadQC(cfgCtx, req, ).Majority() ``` -#### Example: Response Filtering ServerInterceptor +#### Example: Response Filtering ClientInterceptor Filter out responses that don't meet certain criteria: @@ -1021,7 +1021,7 @@ resp, err := ReadQC(cfgCtx, req, ).Majority() ``` -#### Example: Counting ServerInterceptor +#### Example: Counting ClientInterceptor Count responses passing through the interceptor: