package main import ( "context" "fmt" "log/slog" "time" "github.com/hashicorp/serf/serf" crdt "github.com/ipfs/go-ds-crdt" ) // Implements the Broadcaster interface. type SerfBroadcaster struct { ctx context.Context serf *serf.Serf nextCh chan []byte } // The broadcaster can be shut down by cancelling the given context. This must be done before Closing // the crdt.Datastore, otherwise things may hang. func NewSerfBroadcaster(ctx context.Context, serf *serf.Serf) *SerfBroadcaster { return &SerfBroadcaster{ ctx: ctx, serf: serf, nextCh: make(chan []byte), } } func (b *SerfBroadcaster) Broadcast(bytes []byte) error { slog.Debug("Broadcasting head nodes to peers", "size", len(bytes)) // Other peers are not allowed to coalesce this event by name as the payload may differ. // TODO: decode CRDTBroadcast from bytes and embed the nodes content in the payload as well to save round trip. if err := b.serf.UserEvent("heads", bytes, false); err != nil { return fmt.Errorf("broadcast heads event: %w", err) } return nil } func (b *SerfBroadcaster) Next() ([]byte, error) { select { case bytes := <-b.nextCh: // Blocks until a new heads event is received. return bytes, nil case <-b.ctx.Done(): return nil, crdt.ErrNoMoreBroadcast } } func (b *SerfBroadcaster) HandleEvent(event serf.Event) { select { case <-b.ctx.Done(): return default: } slog.Debug("Received event in broadcaster", "event", event) userEvent, ok := event.(serf.UserEvent) if !ok { // Ignore non-user events. return } if userEvent.Name != "heads" { // Ignore non-heads user events. return } start := time.Now() select { case b.nextCh <- userEvent.Payload: slog.Debug("Handled heads event", "duration", time.Since(start)) case <-b.ctx.Done(): } } var _ crdt.Broadcaster = (*SerfBroadcaster)(nil)