diff --git a/chord/client.go b/chord/client.go new file mode 100644 index 0000000..d1460fc --- /dev/null +++ b/chord/client.go @@ -0,0 +1,100 @@ +package chord + +import ( + "context" + "errors" + "fmt" + "net/netip" +) + +// Client represents the communications a Chord node performs. +type Client interface { + // FindSuccessor asks s to find the first peer in the network preceding id. + FindSuccessor(ctx context.Context, s Peer, id ID) (Peer, error) + // Notify tells s we believe n to be its predecessor. + Notify(ctx context.Context, n *Node, s Peer) error + // Neighbors requests a peer's beliefs about its own neighbors. + Neighbors(ctx context.Context, p Peer) (pred Peer, succ []Peer, err error) +} + +// TODO(branden): FindSuccessor should be plural; if we have multiple keys to +// search, we shouldn't have to do the whole query for all of them, especially +// considering we can sort by increasing distance from the origin and then do +// the query in linear time. + +// Join creates a new node joining an existing Chord network by communicating +// with any peer already in the network. +func Join(ctx context.Context, cl Client, addr netip.AddrPort, np Peer) (*Node, error) { + if !addr.IsValid() { + return nil, errors.New("chord: cannot join with invalid address") + } + if !np.IsValid() { + return nil, errors.New("chord: invalid peer") + } + self := Address(addr) + p, err := cl.FindSuccessor(ctx, np, self.id) + if err != nil { + return nil, fmt.Errorf("couldn't query own successor: %w", err) + } + // Talk to the peer to verify connection. + // We also get replication info this way. + // TODO(branden): We could just create our node with this succ and stabilize, + // but the paper doesn't in Figure 6, so we won't. + _, succ, err := cl.Neighbors(ctx, p) + if err != nil { + return nil, fmt.Errorf("joined successor failed: %w", err) + } + // Make our successor list by shifting up theirs. + // TODO(branden): need to be more careful about capacity? + // we're always at 1 in this project anyway... + s := make([]Peer, len(succ)) + s[0] = p + copy(s[1:], succ) + r := &Node{ + self: self, + succ: s, + fingers: make([]Peer, 0, 8*len(ID{})), + } + return r, nil +} + +// Stabilize recomputes n's successor. +func Stabilize(ctx context.Context, cl Client, n *Node) error { + pred, _, err := cl.Neighbors(ctx, n.Successor()) + if err != nil { + // TODO(zeph): replication + return err + } + if pred.IsValid() && contains(n.self.id, n.Successor().id, pred.id) && pred.id != n.Successor().id { + // Shift in the new successor. + copy(n.succ[1:], n.succ) + n.succ[0] = pred + } + return cl.Notify(ctx, n, n.Successor()) +} + +// Notify informs n that p considers n to be p's successor. +func Notify(n *Node, np Peer) { + if !n.pred.IsValid() || contains(n.pred.id, n.self.id, np.id) && np.id != n.self.id { + n.pred = np + return + } +} + +// FixFingers recomputes n's finger table. +func FixFingers(ctx context.Context, cl Client, n *Node) error { + // TODO(branden): need arithmetic on IDs to do this + return errors.New("not implemented") +} + +// CheckPredecessor checks the health of n's predecessor. +// Any error is considered a health check failure. +func CheckPredecessor(ctx context.Context, cl Client, n *Node) { + if !n.pred.IsValid() { + return + } + _, _, err := cl.Neighbors(ctx, n.pred) + if err != nil { + n.pred = Peer{} + } +}