diff --git a/chord/httpnode/client.go b/chord/httpnode/client.go new file mode 100644 index 0000000..8158ac6 --- /dev/null +++ b/chord/httpnode/client.go @@ -0,0 +1,62 @@ +package httpnode + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/netip" + "net/url" + "path" + + "github.com/go-json-experiment/json" + + "git.sunturtle.xyz/zephyr/chord/chord" +) + +type Client struct { + // HTTP is the client used to make requests. + HTTP http.Client + // APIBase is the path under which the Chord API is served. + APIBase string +} + +// FindSuccessor asks s to find the first peer in the network preceding id. +func (cl *Client) FindSuccessor(ctx context.Context, s chord.Peer, id chord.ID) (chord.Peer, error) { + _, addr := s.Values() + if !addr.IsValid() { + return chord.Peer{}, errors.New("FindSuccessor with invalid peer") + } + url := url.URL{ + Scheme: "http", + Host: addr.String(), + Path: path.Join("/", cl.APIBase, "succ"), + RawQuery: url.Values{"s": {id.String()}}.Encode(), + } + req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil) + if err != nil { + return chord.Peer{}, err + } + resp, err := cl.HTTP.Do(req) + if err != nil { + return chord.Peer{}, err + } + var r response[netip.AddrPort] + if err := json.UnmarshalRead(resp.Body, &r); err != nil { + return chord.Peer{}, err + } + if r.Error != "" { + return chord.Peer{}, fmt.Errorf("%s (%s)", r.Error, resp.Status) + } + return chord.Address(*r.Data), nil +} + +// Notify tells s we believe n to be its predecessor. +func (cl *Client) Notify(ctx context.Context, n *chord.Node, s chord.Peer) error { + panic("not implemented") // TODO: Implement +} + +// Neighbors requests a peer's beliefs about its own neighbors. +func (cl *Client) Neighbors(ctx context.Context, p chord.Peer) (pred chord.Peer, succ []chord.Peer, err error) { + panic("not implemented") // TODO: Implement +} diff --git a/chord/httpnode/httpnode.go b/chord/httpnode/httpnode.go new file mode 100644 index 0000000..de76552 --- /dev/null +++ b/chord/httpnode/httpnode.go @@ -0,0 +1,15 @@ +// Package httpnode provides an implementation of a Chord client and server +// using JSON over HTTP. +package httpnode + +import "net/netip" + +type response[T any] struct { + Data *T `json:"data,omitzero"` + Error string `json:"error,omitzero"` +} + +type neighbors struct { + Succ []netip.AddrPort `json:"succ"` + Pred netip.AddrPort `json:"pred"` +} diff --git a/chord/id.go b/chord/id.go index 59480c2..def63c5 100644 --- a/chord/id.go +++ b/chord/id.go @@ -3,6 +3,7 @@ package chord import ( "bytes" "crypto/sha1" + "encoding/hex" "net/netip" ) @@ -42,3 +43,9 @@ func contains(left, right, x ID) bool { return true } } + +func (id ID) String() string { + b := make([]byte, 0, len(ID{})*2) + b = hex.AppendEncode(b, id[:]) + return string(b) +} diff --git a/chord/topology.go b/chord/topology.go index 268ab9a..89157ae 100644 --- a/chord/topology.go +++ b/chord/topology.go @@ -79,6 +79,13 @@ func Address(addr netip.AddrPort) Peer { // which was valid. func (p Peer) IsValid() bool { return p.addr.IsValid() && p.addr.Port() != 0 } +// Values returns the peer's ID and address. +func (p Peer) Values() (ID, netip.AddrPort) { + // We do this instead of exporting Peer's fields to ensure those fields + // are never mutable. If p.IsValid() then p.id == addrID(p.addr) always. + return p.id, p.addr +} + // Create creates a new Chord network using the given address as the initial node. func Create(addr netip.AddrPort) (*Node, error) { if !addr.IsValid() { diff --git a/go.mod b/go.mod index 5ad7787..ff2f898 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module git.sunturtle.xyz/zephyr/chord go 1.24.1 -require github.com/urfave/cli/v3 v3.0.0-beta1 +require ( + github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 + github.com/urfave/cli/v3 v3.0.0-beta1 +) diff --git a/go.sum b/go.sum index ebc8bbd..562188d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= +github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=