chord/chord/httpnode/client.go
2025-03-13 21:07:10 -04:00

108 lines
2.7 KiB
Go

package httpnode
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"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
}
// Find asks s to find a value and the peer that owns it.
func (cl *Client) Find(ctx context.Context, s chord.Peer, id chord.ID) (chord.Peer, string, error) {
_, addr := s.Values()
if !addr.IsValid() {
return chord.Peer{}, "", errors.New("Find with invalid peer")
}
url := url.URL{
Scheme: "http",
Host: addr.String(),
Path: path.Join("/", cl.APIBase, "key", id.String()),
}
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
}
p, err := readResponse[peervalue](resp)
if err != nil {
return chord.Peer{}, "", fmt.Errorf("%w (%s)", err, resp.Status)
}
return chord.Address(p.Peer), p.Value, 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 {
_, addr := s.Values()
if !addr.IsValid() {
return errors.New("Notify with invalid peer")
}
_, self := n.Self().Values()
url := url.URL{
Scheme: "http",
Host: addr.String(),
Path: path.Join("/", cl.APIBase, "pred"),
RawQuery: url.Values{"s": {self.String()}}.Encode(),
}
req, err := http.NewRequestWithContext(ctx, "POST", url.String(), nil)
if err != nil {
return err
}
resp, err := cl.HTTP.Do(req)
if err != nil {
return err
}
// We expect the server to only be capable of responding with No Content.
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("strange response: %s", resp.Status)
}
return nil
}
// 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) {
_, addr := p.Values()
if !addr.IsValid() {
return chord.Peer{}, nil, errors.New("Neighbors with invalid peer")
}
url := url.URL{
Scheme: "http",
Host: addr.String(),
Path: path.Join("/", cl.APIBase, "neighbors"),
}
req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil)
if err != nil {
return chord.Peer{}, nil, err
}
resp, err := cl.HTTP.Do(req)
if err != nil {
return chord.Peer{}, nil, err
}
n, err := readResponse[neighbors](resp)
if err != nil {
return chord.Peer{}, nil, fmt.Errorf("%w (%s)", err, resp.Status)
}
pred = chord.Address(n.Pred)
succ = make([]chord.Peer, 0, len(n.Succ))
for _, addr := range n.Succ {
p := chord.Address(addr)
if p.IsValid() {
succ = append(succ, p)
}
}
return pred, succ, nil
}