-
Notifications
You must be signed in to change notification settings - Fork 386
feat(p2p): add versioned handlers function #5548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| // Copyright 2026 The Swarm Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style | ||
| // license that can be found in the LICENSE file. | ||
|
|
||
| package p2p_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/coreos/go-semver/semver" | ||
| "github.com/ethersphere/bee/v2/pkg/p2p" | ||
| "github.com/ethersphere/bee/v2/pkg/p2p/protobuf" | ||
| "github.com/ethersphere/bee/v2/pkg/swarm" | ||
| ) | ||
|
|
||
| const ( | ||
| exampleProtocolName = "versionedping" | ||
| exampleProtocolVersion = "1.2.0" | ||
| exampleStreamName = "ping" | ||
| ) | ||
|
|
||
| // ExampleService represents a full Bee protocol service (structured like pkg/pingpong) | ||
| // supporting 3 version levels: | ||
| // - v1.2.0: Current version | ||
| // - v1.1.0: Legacy version 1.1 | ||
| // - v1.0.0: Legacy version 1.0 | ||
| type ExampleService struct { | ||
| streamer p2p.Streamer | ||
| } | ||
|
|
||
| func NewExampleService(streamer p2p.Streamer) *ExampleService { | ||
| return &ExampleService{ | ||
| streamer: streamer, | ||
| } | ||
| } | ||
|
|
||
| func (s *ExampleService) Protocol() p2p.ProtocolSpec { | ||
| return p2p.ProtocolSpec{ | ||
| Name: exampleProtocolName, | ||
| Version: exampleProtocolVersion, | ||
| StreamSpecs: []p2p.StreamSpec{ | ||
| { | ||
| Name: exampleStreamName, | ||
| Handler: p2p.NewVersionedHandlersFunc( | ||
| p2p.VersionedHandler{ | ||
| Version: semver.New("1.2.0"), // Server handler for >= 1.2.0 | ||
| Handler: func(ctx context.Context, p p2p.Peer, stream p2p.Stream) error { | ||
| w, r := protobuf.NewWriterAndReader(stream) | ||
| _, _ = w, r | ||
| fmt.Println("Server received ping on v1.2.0 handler") | ||
| return stream.FullClose() | ||
| }, | ||
| }, | ||
| p2p.VersionedHandler{ | ||
| Version: semver.New("1.1.0"), // Server handler for legacy 1.1.0 | ||
| Handler: func(ctx context.Context, p p2p.Peer, stream p2p.Stream) error { | ||
| w, r := protobuf.NewWriterAndReader(stream) | ||
| _, _ = w, r | ||
| fmt.Println("Server received ping on v1.1.0 legacy handler") | ||
| return stream.FullClose() | ||
| }, | ||
| }, | ||
| p2p.VersionedHandler{ | ||
| Version: semver.New("1.0.0"), // Server handler for legacy 1.0.0 | ||
| Handler: func(ctx context.Context, p p2p.Peer, stream p2p.Stream) error { | ||
| w, r := protobuf.NewWriterAndReader(stream) | ||
| _, _ = w, r | ||
| fmt.Println("Server received ping on v1.0.0 legacy handler") | ||
| return stream.FullClose() | ||
| }, | ||
| }, | ||
| ), | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func (s *ExampleService) Ping(ctx context.Context, peer swarm.Address) error { | ||
| stream, err := s.streamer.NewStream(ctx, peer, nil, exampleProtocolName, "1.1.0", exampleStreamName) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer stream.Close() | ||
|
|
||
| pingClient := p2p.NewVersionedHandlersFunc( | ||
| p2p.VersionedHandler{ | ||
| Version: semver.New("1.2.0"), // Current version client (>= 1.2.0) | ||
| Handler: func(ctx context.Context, _ p2p.Peer, stream p2p.Stream) error { | ||
| w, r := protobuf.NewWriterAndReader(stream) | ||
| _, _ = w, r | ||
| fmt.Println("Client sent ping using v1.2.0 format") | ||
| return nil | ||
| }, | ||
| }, | ||
| p2p.VersionedHandler{ | ||
| Version: semver.New("1.1.0"), // Legacy v1.1.0 client (1.1.0 <= v < 1.2.0) | ||
| Handler: func(ctx context.Context, _ p2p.Peer, stream p2p.Stream) error { | ||
| w, r := protobuf.NewWriterAndReader(stream) | ||
| _, _ = w, r | ||
| fmt.Println("Client sent ping using v1.1.0 legacy format") | ||
| return nil | ||
| }, | ||
| }, | ||
| p2p.VersionedHandler{ | ||
| Version: semver.New("1.0.0"), // Legacy v1.0.0 client (1.0.0 <= v < 1.1.0) | ||
| Handler: func(ctx context.Context, _ p2p.Peer, stream p2p.Stream) error { | ||
| w, r := protobuf.NewWriterAndReader(stream) | ||
| _, _ = w, r | ||
| fmt.Println("Client sent ping using v1.0.0 legacy format") | ||
| return nil | ||
| }, | ||
| }, | ||
| ) | ||
|
|
||
| return pingClient(ctx, p2p.Peer{Address: peer}, stream) | ||
| } | ||
|
|
||
| // Example_versionedProtocol demonstrates constructing a versioned P2P protocol service | ||
| // and executing versioned message exchange between client and server nodes. | ||
| func Example_versionedProtocol() { | ||
| ctx := context.Background() | ||
|
|
||
| serverSvc := NewExampleService(nil) | ||
| serverSpec := serverSvc.Protocol() | ||
| serverHandler := serverSpec.StreamSpecs[0].Handler | ||
|
|
||
| // Client streamer connects to server where version 1.1.0 is negotiated: | ||
| clientStreamer := &mockStreamer{ | ||
| supportedVersions: map[string]bool{ | ||
| "1.1.0": true, | ||
| }, | ||
| } | ||
| clientSvc := NewExampleService(clientStreamer) | ||
|
|
||
| // Client sends ping request (client-side dispatcher automatically selects v1.1.0 format) | ||
| _ = clientSvc.Ping(ctx, swarm.ZeroAddress) | ||
|
|
||
| // Server receives incoming stream (server-side dispatcher automatically selects v1.1.0 handler) | ||
| incomingStream := mockStream{ | ||
| version: "1.1.0", | ||
| } | ||
| _ = serverHandler(ctx, p2p.Peer{Address: swarm.ZeroAddress}, incomingStream) | ||
|
|
||
| // Output: | ||
| // Client sent ping using v1.1.0 legacy format | ||
| // Server received ping on v1.1.0 legacy handler | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ import ( | |
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "sort" | ||
| "time" | ||
|
|
||
| "github.com/coreos/go-semver/semver" | ||
|
|
@@ -208,6 +209,40 @@ type HandlerFunc func(context.Context, Peer, Stream) error | |
| // HandlerMiddleware decorates a HandlerFunc by returning a new one. | ||
| type HandlerMiddleware func(HandlerFunc) HandlerFunc | ||
|
|
||
| // VersionedHandler represents a HandlerFunc associated with a minimum supported Version threshold. | ||
| type VersionedHandler struct { | ||
| Version *semver.Version | ||
| Handler HandlerFunc | ||
| } | ||
|
|
||
| // NewVersionedHandlersFunc creates a new HandlerFunc that dispatches stream execution | ||
| // based on the stream version. | ||
| // | ||
| // Handlers are evaluated in descending order of Version. The first handler where | ||
| // stream.Version >= handler.Version will be executed. | ||
| func NewVersionedHandlersFunc(handlers ...VersionedHandler) HandlerFunc { | ||
| sorted := make([]VersionedHandler, len(handlers)) | ||
| copy(sorted, handlers) | ||
| sort.Slice(sorted, func(i, j int) bool { | ||
| return sorted[j].Version.LessThan(*sorted[i].Version) | ||
| }) | ||
|
|
||
| return func(ctx context.Context, p Peer, stream Stream) error { | ||
| v, err := stream.Version() | ||
| if err != nil { | ||
| return fmt.Errorf("get stream version: %w", err) | ||
| } | ||
|
|
||
| for _, h := range sorted { | ||
| if !v.LessThan(*h.Version) { | ||
| return h.Handler(ctx, p, stream) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small comment - we might want to have counter metrics on the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would be useful, but we would have to provide the metric counter somehow, preferably not to be in the pacakge space, as we have managed to keep metrics nicely isolated per service. The simplest would be to add the prometheus.Counter as the argument to the NewVersionedHandlersFunc, so that every protocol would inject its own counter. Maybe as an option construct, I will think about it a bit more and figure out something. |
||
| } | ||
| } | ||
|
|
||
| return fmt.Errorf("no handler found for stream version: %s", v.String()) | ||
| } | ||
| } | ||
|
|
||
| // HeadlerFunc is returning response headers based on the received request | ||
| // headers. | ||
| type HeadlerFunc func(Headers, swarm.Address) Headers | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we change the name of this file to something like
example_versioned_test.go? It would be just straight forward to figure what are the tests for.