diff --git a/AGENTS.md b/AGENTS.md index c287c793d..3261e0f13 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/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 95% rename from client_interceptor_test.go rename to call_client_interceptor_test.go index 76ddedc45..a9e47d430 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++ @@ -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 }, ), diff --git a/client_interceptor.go b/call_context.go similarity index 80% rename from client_interceptor.go rename to call_context.go index 5a0e8b19c..b20e63119 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,13 +34,13 @@ 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 Configuration + config Config request Req method string msgID uint64 @@ -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] { - config := ctx.Configuration() +) *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] { - config := ctx.Configuration() +) *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() Configuration { +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 99% rename from quorumcall_test.go rename to call_quorum_test.go index 707d43f97..16c8c6f41 100644 --- a/quorumcall_test.go +++ b/call_quorum_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 } } diff --git a/callopts.go b/callopts.go index 758fb38c6..8488e9ad3 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 b8cec261b..e7aa5e625 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( - WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), + srvs, stop, err := NewLocalServers(n, WithLocalDialOptions( + WithGRPCDialOptions(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(_ ServerContext, _ *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 Config) 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()) @@ -114,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/cmd/protoc-gen-gorums/dev/aliases.go b/cmd/protoc-gen-gorums/dev/aliases.go index 8d39c416c..a657ece55 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/dev/generated_code_test.go b/cmd/protoc-gen-gorums/dev/generated_code_test.go index 3d8b9398c..d16cfebc3 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/dev/zorums.pb.go b/cmd/protoc-gen-gorums/dev/zorums.pb.go index 35949012b..2312798e1 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 7ad08d796..7ee52b737 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 1b3d36d99..6641c24b7 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 72d63ebd8..9c336b039 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 9fa5e1f5a..7cbd96d9b 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 0bca61598..114873bea 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 a5f6ab508..64d529abe 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/gorums.go b/cmd/protoc-gen-gorums/gengorums/gorums.go index 5425b95ff..0e360dfbe 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/gorums_bundle_test.go b/cmd/protoc-gen-gorums/gengorums/gorums_bundle_test.go index 5073096b7..576f10ec9 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/cmd/protoc-gen-gorums/gengorums/template_rpc.go b/cmd/protoc-gen-gorums/gengorums/template_rpc.go index 18d18974b..392494d05 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.RPCCall" .GenFile}} +{{$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/cmd/protoc-gen-gorums/gengorums/template_server.go b/cmd/protoc-gen-gorums/gengorums/template_server.go index 53a4d4ba5..fb294c8a9 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/cmd/protoc-gen-gorums/gengorums/template_static.go b/cmd/protoc-gen-gorums/gengorums/template_static.go index e098426a7..23f77f4a3 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/config.go b/config.go index 2ab981ba0..32e9cc5ed 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,22 +36,22 @@ 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: // // 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 NodeListOption, opts ...DialOption) (Configuration, 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) (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 NodeSource. +func (c Config) Extend(opt NodeSource) (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 } @@ -156,13 +156,13 @@ func (c Configuration) Add(ids ...uint32) Configuration { } } } - slices.SortFunc(nodes, ID) + slices.SortFunc(nodes, ByID) 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,33 +198,33 @@ func (c Configuration) Difference(other Configuration) Configuration { return c.Remove(other.NodeIDs()...) } -// SortBy returns a new Configuration 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 Configuration) SortBy(cmp func(*Node, *Node) int) Configuration { +func (c Config) Sort(cmp func(*Node, *Node) int) Config { if len(c) == 0 { return nil } @@ -243,13 +243,13 @@ 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 { -// return c.SortBy(gorums.Latency)[:quorumSize] +// cfg.Watch(ctx, 5*time.Second, func(c gorums.Config) gorums.Config { +// return c.Sort(gorums.ByLatency)[:quorumSize] // }) // // // Skip failed nodes first, then pick fastest: -// cfg.Watch(ctx, 5*time.Second, func(c gorums.Configuration) gorums.Configuration { -// return c.WithoutErrors(lastErr).SortBy(gorums.Latency)[:quorumSize] +// cfg.Watch(ctx, 5*time.Second, func(c gorums.Config) gorums.Config { +// return c.WithoutErrors(lastErr).Sort(gorums.ByLatency)[:quorumSize] // }) // // The returned channel has a buffer of 1. If the consumer is slow and has not @@ -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_test.go b/config_test.go index 8ea5a587b..26e94c812 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 }{ @@ -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) } @@ -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.Configuration - if got := empty.SortBy(gorums.ID); got != nil { - t.Fatalf("empty.SortBy(ID) = %v, want nil", got) + var empty gorums.Config + 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()) @@ -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 }{ @@ -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/opts.go b/dial_options.go similarity index 83% rename from opts.go rename to dial_options.go index a29605efe..5c33b0fd5 100644 --- a/opts.go +++ b/dial_options.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 @@ -41,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...) } @@ -94,13 +93,13 @@ 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 -// 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") @@ -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/doc/design-doc-layering.md b/doc/design-doc-layering.md deleted file mode 100644 index 46b73c763..000000000 --- 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/dev-guide.md b/doc/dev-guide.md index 45042aa1d..f399700a8 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-v2-to-v3.md similarity index 97% rename from doc/migration.md rename to doc/migration-v2-to-v3.md index 32881c228..3ee6f399a 100644 --- a/doc/migration.md +++ b/doc/migration-v2-to-v3.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,18 +96,18 @@ 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. **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(...), ) ``` @@ -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/ordering.md b/doc/ordering.md index f9469404e..e7e6bd58e 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 30ed4ea31..c1a588480 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 @@ -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)) } ``` @@ -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,16 +269,16 @@ 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)) { srv.mut.Lock() defer srv.mut.Unlock() - return send(srv.state) + 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, @@ -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. @@ -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()), ), ) @@ -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) @@ -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 ClientInterceptor Transform requests before sending to each node: @@ -880,7 +880,7 @@ resp, err := WriteQC(cfgCtx, req, ).Majority() ``` -### MapResponse Interceptor +### MapResponse ClientInterceptor Transform responses received from each node: @@ -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 @@ -950,13 +950,13 @@ resp, err := ReadQC(cfgCtx, req, ).Majority() ``` -#### Example: Logging Interceptor +#### Example: Logging ClientInterceptor 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() @@ -987,15 +987,15 @@ resp, err := ReadQC(cfgCtx, req, ).Majority() ``` -#### Example: Response Filtering Interceptor +#### Example: Response Filtering ClientInterceptor 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 @@ -1021,15 +1021,15 @@ resp, err := ReadQC(cfgCtx, req, ).Majority() ``` -#### Example: Counting Interceptor +#### Example: Counting ClientInterceptor 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,26 +1042,26 @@ 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 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.ServerCtx, 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 { - return func(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { +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) @@ -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") @@ -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. @@ -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())), ) ``` @@ -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 }) ``` @@ -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. @@ -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() @@ -1312,7 +1312,7 @@ func handleQuorumCall(config *gorums.Configuration, 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) { @@ -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()), ), ) @@ -1473,31 +1473,31 @@ 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.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) }) ``` -### 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. @@ -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,17 +1540,17 @@ 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 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 { - return c.SortBy(gorums.Latency)[:quorumSize] +updates := allNodesCfg.Watch(ctx, 5*time.Second, func(c gorums.Config) gorums.Config { + return c.Sort(gorums.ByLatency)[:quorumSize] }) fastCfg := <-updates // initial snapshot, available before the first tick @@ -1583,11 +1583,11 @@ 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() - 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 @@ -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 `Configuration` 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. @@ -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. @@ -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 `Configuration` 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. @@ -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()`. @@ -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())), ) ``` @@ -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/errors.go b/errors.go index 8c2ddc9e3..c1cc2ff2a 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 5b5f37656..d3a35e078 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/examples/interceptors/server_interceptors.go b/examples/interceptors/server_interceptors.go index 2a001e9e6..9b3b1f178 100644 --- a/examples/interceptors/server_interceptors.go +++ b/examples/interceptors/server_interceptors.go @@ -10,8 +10,8 @@ import ( "google.golang.org/protobuf/proto" ) -func LoggingInterceptor(addr string) gorums.Interceptor { - return func(ctx gorums.ServerCtx, in *gorums.Message, next gorums.Handler) (*gorums.Message, error) { +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) 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/client.go b/examples/storage/client.go index 0314e6f60..d8307d1b6 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 5639a2f40..c8bca3e36 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/proto/storage.pb.go b/examples/storage/proto/storage.pb.go index 87cda7a15..d7b5ad423 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 842055bcc..44145625c 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/examples/storage/repl.go b/examples/storage/repl.go index 68b6f84c6..39beac707 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 @@ -443,8 +443,8 @@ func (r repl) parseConfiguration(cfgStr string) (pb.Configuration, error) { for _, i := range indices { nodes = append(nodes, cfgNodes[i]) } - slices.SortFunc(nodes, gorums.ID) - return pb.Configuration(nodes), nil + slices.SortFunc(nodes, gorums.ByID) + 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 7d845daf0..acac58bb6 100644 --- a/examples/storage/server.go +++ b/examples/storage/server.go @@ -31,60 +31,59 @@ func runServer(address string, peers []string, srvOpt gorums.ServerOption) error if err != nil { return err } - insecureDial := gorums.WithDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())) - sys, err := gorums.NewSystem(address, - gorums.WithServerOptions(srvOpt, gorums.WithPeers(myID, peerList, insecureDial)), - insecureDial, + insecureDial := gorums.WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())) + 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.Config) 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. // 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())) - systems, stop, err := gorums.NewLocalSystems(4, + dialOpts := gorums.WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())) + 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.Config) bool { + return cfg.Size() == len(servers) }); err != nil { stop() return nil, nil, fmt.Errorf("cluster failed to connect: %w", err) @@ -101,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) @@ -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) } }() @@ -170,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) @@ -195,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) @@ -218,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") @@ -230,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/gorums.pb.go b/gorums.pb.go index 6d52b5634..41a1db1ce 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/gorums.proto b/gorums.proto index c78497d15..72268ef10 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; diff --git a/gorumstest/gorumstest.go b/gorumstest/gorumstest.go index 6409b7f6c..babb1e6e0 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 @@ -119,11 +119,11 @@ 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. -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"} @@ -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.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. +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/gorumstest/options.go b/gorumstest/options.go index ca431b764..1dd4a03a1 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/gorumstest/servers.go b/gorumstest/servers.go index aa89680fe..7b518febd 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/inbound_manager.go b/inbound_manager.go index 84315356d..5ae65ddfe 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 @@ -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(Configuration), 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), @@ -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,32 +350,32 @@ 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) } } - slices.SortFunc(inboundCfg, ID) - slices.SortFunc(clientCfg, ID) + slices.SortFunc(inboundCfg, ByID) + slices.SortFunc(clientCfg, ByID) im.inboundCfg = inboundCfg im.clientConfig = clientCfg 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) } } - slices.SortFunc(cfg, ID) + slices.SortFunc(cfg, ByID) } cfgChanged := !slices.Equal(im.config, cfg) im.config = cfg @@ -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,24 +429,24 @@ 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) }) } // 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/inbound_manager_test.go b/inbound_manager_test.go index 3dad62be3..1c0bb039f 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. @@ -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 @@ -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,25 +622,25 @@ 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) } } -// 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"}, }) } -// 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)) @@ -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 }) @@ -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)) @@ -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 }) @@ -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/stream/stream.pb.go b/internal/stream/stream.pb.go index 795ad2f54..8333eba63 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 c40925d99..a7fd7d0b1 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/testprotos/failing/reservednames/reserved.proto b/internal/testprotos/failing/reservednames/reserved.proto index 13399cf52..78b2f31ec 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.pb.go b/internal/tests/config/config.pb.go index 1d843a8f9..a9776288b 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.proto b/internal/tests/config/config.proto index 10cdd8179..e62fe67d7 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_gorums.pb.go b/internal/tests/config/config_gorums.pb.go index 84728d83e..ac5d039fb 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/config/config_test.go b/internal/tests/config/config_test.go index 049931464..cef426f26 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.ServerContext, 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/correctable/correctable.pb.go b/internal/tests/correctable/correctable.pb.go index c1207c518..46c0d03c8 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 3cb41e8fd..266bfdee9 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/correctable/correctable_test.go b/internal/tests/correctable/correctable_test.go index 874b0459f..c993ab2db 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.pb.go b/internal/tests/metadata/metadata.pb.go index 94ede83ad..a3a0c80f6 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 a30a5087f..3833aca1b 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/metadata/metadata_test.go b/internal/tests/metadata/metadata_test.go index d5050167a..529bd46a2 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.pb.go b/internal/tests/oneway/oneway.pb.go index 3855a7589..59cb19f7a 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 f3c0881d9..5e7c76cac 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/oneway/oneway_test.go b/internal/tests/oneway/oneway_test.go index 388450899..529593f4d 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 } @@ -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/internal/tests/ordering/order.pb.go b/internal/tests/ordering/order.pb.go index 6fefa047e..965c5f903 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 6fb7815bb..7404af375 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/ordering/order_test.go b/internal/tests/ordering/order_test.go index 0c7b68d54..09eff8e91 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.pb.go b/internal/tests/tls/tls.pb.go index 35b6796ec..688b29f21 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 50ca67062..f6ae84f67 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/tls/tls_test.go b/internal/tests/tls/tls_test.go index d934cf5de..a05a1d5ee 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 @@ -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/internal/tests/unresponsive/unreponsive_test.go b/internal/tests/unresponsive/unreponsive_test.go index 208c2d35d..a096721a9 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/internal/tests/unresponsive/unresponsive.pb.go b/internal/tests/unresponsive/unresponsive.pb.go index 05d36d793..a46b30223 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 6ce49eba8..256406f98 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 { diff --git a/local_servers.go b/local_servers.go new file mode 100644 index 000000000..84892a474 --- /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 [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, NodeSource, 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/multicast.go b/multicast.go index 93e9d5c69..9e12ed0af 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/node.go b/node.go index 0581bd147..deeb23565 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 [Configuration.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 [Configuration.SortBy]. -var ID = func(a, b *Node) int { +// 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) } -// 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 [Configuration.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 { @@ -336,10 +336,10 @@ var LastNodeError = 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 [Configuration.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/config_opts.go b/node_source.go similarity index 81% rename from config_opts.go rename to node_source.go index 93f001a84..07f7cc5a3 100644 --- a/config_opts.go +++ b/node_source.go @@ -7,13 +7,13 @@ 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 { - newConfig(nodeRegistry) (Configuration, error) +type NodeSource interface { + 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 @@ -25,16 +25,16 @@ 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) } 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") } @@ -49,17 +49,17 @@ func (nm nodeMap[T]) newConfig(registry nodeRegistry) (Configuration, 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) } 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,9 +138,9 @@ func (b *nodeBuilder) add(id uint32, addr string) error { return nil } -// configuration returns the built Configuration, sorted by ID. -func (b *nodeBuilder) configuration() Configuration { - slices.SortFunc(b.nodes, ID) +// configuration returns the built Config, sorted by ID. +func (b *nodeBuilder) configuration() Config { + slices.SortFunc(b.nodes, ByID) return b.nodes } diff --git a/node_test.go b/node_test.go index 293817784..9e71a82c7 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} @@ -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.Sort(ByLatency)[: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.Sort(ByLatency)[:2] } const interval = 20 * time.Millisecond updates := cfg.Watch(t.Context(), interval, top2) diff --git a/opts_test.go b/options_test.go similarity index 50% rename from opts_test.go rename to options_test.go index a295070e6..7812528d6 100644 --- a/opts_test.go +++ b/options_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/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 e0e3b8163..201a88ebd 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/rpc.go b/remote_call.go similarity index 82% rename from rpc.go rename to remote_call.go index d90116f61..573e2055c 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 fc8a1ac3f..3c08bb94d 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/responses.go b/responses.go index 90a2600a6..111870068 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 70bd23bb1..595e4d238 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)) @@ -32,12 +32,12 @@ 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)} } - c := &ClientCtx[Req, Resp]{ + c := &CallContext[Req, Resp]{ Context: t.Context(), config: config, replyChan: resultChan, diff --git a/server.go b/server.go index 442641be3..f4721d893 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" @@ -17,12 +18,13 @@ type serverOptions struct { sendBufferSize uint grpcOpts []grpc.ServerOption connectCallback func(context.Context) - interceptors []Interceptor + interceptors []ServerInterceptor // Peer management options myID uint32 - peerNodes NodeListOption // Peers to track as they connect; set by WithPeers. - onConfigChange func(Configuration) // Callback registered via WithPeerChange. - 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 } @@ -65,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...) } @@ -79,18 +81,18 @@ 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]. // // 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 @@ -100,29 +102,42 @@ 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 } } +// 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 + interceptors []ServerInterceptor + + 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 } // 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]. @@ -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, @@ -167,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 NodeSource, 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 } @@ -203,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, @@ -219,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 @@ -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 +// [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() { + // 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 58% rename from system_test.go rename to server_e2e_test.go index 7934e021f..cb683c311 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") - 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") - } - if !closer2.closed { - t.Error("closer2 was not closed") - } -} - -func TestSystemStopReturnsCloserError(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) + t.Fatalf("NewLocalServers: %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.Config) 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,74 +74,80 @@ 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.Config) 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.Config) 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.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.Configuration] is the client's outbound config pointing at the server. -func createClientServerSystems(t *testing.T) (*gorums.System, *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() - // 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. 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 { @@ -267,9 +174,9 @@ 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("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.ServerContext, _ *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.ServerContext, in *gorums.Message) (*gorums.Message, error) { + 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. + 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.ServerContext, in *gorums.Message) (*gorums.Message, error) { + t.Logf("Server %d received multicast on %v: %v", i+1, mock.Stream, in.Proto) + 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,39 +434,37 @@ 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.ServerContext, in *gorums.Message) (*gorums.Message, error) { + t.Logf("SERVER received multicast: %v", in.Proto) + 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. - 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 }) - 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.Config { 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.ServerContext, 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.ServerContext, 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.Config) 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.Config) 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.Config) 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.Config) 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.Config) 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.Config) bool { return cfg.Size() == 1 }); err != nil { t.Fatalf("WaitForClients: %v", err) diff --git a/handler.go b/server_handler.go similarity index 77% rename from handler.go rename to server_handler.go index f1e67ff72..7a609e98b 100644 --- a/handler.go +++ b/server_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 } @@ -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) - // Interceptor intercepts and may modify incoming requests and outgoing responses. - // It receives a ServerCtx, the incoming Message, and a Handler representing + Handler func(ServerContext, *Message) (*Message, error) + // 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(ServerCtx, *Message, Handler) (*Message, error) + ServerInterceptor 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,10 +56,10 @@ 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) { - // If Msg is set, marshal it to payload before sending. - if out.Msg != nil && len(out.GetPayload()) == 0 { - payload, err := proto.Marshal(out.Msg) +func (ctx *ServerContext) SendMessage(out *Message) { + // 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 { @@ -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 { +// [ServerContext.ConnectedPeers] to observe reachability. +func (ctx *ServerContext) PeerConfig() Config { if ctx.srv == nil { return nil } @@ -85,20 +85,20 @@ func (ctx *ServerCtx) PeerConfig() Configuration { } // ConnectedPeers returns the currently reachable subset of -// [ServerCtx.PeerConfig]; see [Server.ConnectedPeers]. -func (ctx *ServerCtx) ConnectedPeers() Configuration { +// [ServerContext.PeerConfig]; see [Server.ConnectedPeers]. +func (ctx *ServerContext) 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 *ServerContext) ConnectedClients() Config { if ctx.srv == nil { return nil } @@ -110,7 +110,7 @@ func (ctx *ServerCtx) ConnectedClients() Configuration { // 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 { @@ -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 @@ -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 } @@ -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/handler_test.go b/server_handler_test.go similarity index 77% rename from handler_test.go rename to server_handler_test.go index ca1f43870..0b3d9047f 100644 --- a/handler_test.go +++ b/server_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_test.go b/server_test.go index 021100f0d..dc06bb8df 100644 --- a/server_test.go +++ b/server_test.go @@ -40,14 +40,14 @@ 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) { +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) 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) @@ -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 } @@ -73,12 +73,12 @@ 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-"), )) // 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 { @@ -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() @@ -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 }) @@ -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 { @@ -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 }) @@ -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) } diff --git a/system.go b/system.go deleted file mode 100644 index ebdc0e3ee..000000000 --- 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 -}