chord/chord/client.go
2025-03-15 20:29:47 -04:00

171 lines
5.0 KiB
Go

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 peer that most closely follows a key.
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)
// Bye tells p that its predecessor or successor is leaving
// and had the given successor list.
Bye(ctx context.Context, p, n Peer, succ []Peer) error
// SayBye tells p to leave the network.
SayBye(ctx context.Context, p Peer) error
// Get asks s for a saved value.
Get(ctx context.Context, s Peer, id ID) (string, error)
// Set asks s to save a value.
Set(ctx context.Context, s Peer, id ID, v string) 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.
// FindSuccessor gets the peer that most closely follows a key.
func Find(ctx context.Context, cl Client, n *Node, id ID) (Peer, error) {
p, ok := n.Closest(id)
if ok {
return p, nil
}
p, err := cl.FindSuccessor(ctx, p, id)
return p, err
}
// TODO(branden): Get and Set should be plural for the same reasons.
// Get gets a value in the Chord network.
func Get(ctx context.Context, cl Client, n *Node, key ID) (string, error) {
p, err := Find(ctx, cl, n, key)
if err != nil {
return "", err
}
return cl.Get(ctx, p, key)
}
// Set saves a value in the Chord network.
func Set(ctx context.Context, cl Client, n *Node, key ID, val string) error {
p, err := Find(ctx, cl, n, key)
if err != nil {
return fmt.Errorf("couldn't find peer to save key: %w", err)
}
return cl.Set(ctx, p, key, val)
}
// 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{})),
data: make(map[ID]string),
}
return r, nil
}
// Leave causes n to exit the network gracefully,
// handing off its data to its successor.
func Leave(ctx context.Context, cl Client, n *Node) error {
pred, succ := n.Neighbors(nil)
if succ[0].addr == n.self.addr {
// Last node in the network. No data transfer.
return nil
}
if err := cl.Bye(ctx, succ[0], n.self, succ); err != nil {
return err
}
if pred.IsValid() {
if err := cl.Bye(ctx, pred, n.self, succ); err != nil {
return err
}
}
n.mu.Lock()
defer n.mu.Unlock()
for k, v := range n.data {
if err := cl.Set(ctx, succ[0], k, v); err != nil {
return err
}
}
return 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 fmt.Errorf("acquiring successor neighbors for stabilization: %w", 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
}
if err := cl.Notify(ctx, n, n.Successor()); err != nil {
return fmt.Errorf("notifying successor: %w", err)
}
return nil
}
// 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{}
}
}