120 строки
3.0 KiB
Go
120 строки
3.0 KiB
Go
package workercluster
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// BootstrapResult is the outcome of a Bootstrap call.
|
|
type BootstrapResult struct {
|
|
// Leader is the node id that ended up holding leadership when
|
|
// bootstrap finished. For a 1-node bootstrap this is always the
|
|
// local node.
|
|
Leader string
|
|
|
|
// Voters is the final voter set as observed on the local node.
|
|
Voters []string
|
|
}
|
|
|
|
// Bootstrap starts a fresh cluster from the first node. It is a
|
|
// convenience wrapper around New + Start + BootstrapCluster for the
|
|
// common single-node bootstrap case. Production code that needs
|
|
// custom timeouts should call New / Start directly.
|
|
func Bootstrap(ctx context.Context, opts *Options) (*Cluster, BootstrapResult, error) {
|
|
opts.Bootstrap = true
|
|
opts.Seed = Peer{}
|
|
c, err := New(opts)
|
|
if err != nil {
|
|
return nil, BootstrapResult{}, err
|
|
}
|
|
if err := c.Start(ctx); err != nil {
|
|
return nil, BootstrapResult{}, err
|
|
}
|
|
|
|
if err := waitForLeader(ctx, c, 10*time.Second); err != nil {
|
|
_ = c.Shutdown(ctx)
|
|
return nil, BootstrapResult{}, err
|
|
}
|
|
|
|
stats := c.Stats()
|
|
return c, BootstrapResult{Leader: stats.Leader, Voters: votersFromStats(&stats)}, nil
|
|
}
|
|
|
|
// JoinCluster brings up a new node that joins an existing cluster via
|
|
// the given seed peer. It blocks until the local node is a voter in
|
|
// the raft configuration.
|
|
func JoinCluster(ctx context.Context, opts *Options, seed Peer) (*Cluster, error) {
|
|
opts.Bootstrap = false
|
|
opts.Seed = seed
|
|
c, err := New(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := c.Start(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := waitForLeader(ctx, c, 15*time.Second); err != nil {
|
|
_ = c.Shutdown(ctx)
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// waitForLeader blocks until the cluster has a leader (or ctx is
|
|
// canceled, or timeout elapses).
|
|
func waitForLeader(ctx context.Context, c *Cluster, timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
for {
|
|
stats := c.Stats()
|
|
if stats.Leader != "" {
|
|
return nil
|
|
}
|
|
if time.Now().After(deadline) {
|
|
return fmt.Errorf("workercluster: no leader after %s (state=%s)", timeout, stats.State)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
|
|
// votersFromStats returns the voter WorkerIDs from the cluster stats.
|
|
// It is best-effort: the FSM membership cache is the source of truth.
|
|
func votersFromStats(s *Stats) []string {
|
|
out := make([]string, 0, len(s.Members))
|
|
for _, m := range s.Members {
|
|
if m.Role == RoleVoter {
|
|
out = append(out, m.WorkerID)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Parallel starts the given cluster starts concurrently and returns
|
|
// once all of them have completed (or the first one errors).
|
|
func Parallel(starts ...func() error) error {
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, len(starts))
|
|
for _, fn := range starts {
|
|
wg.Add(1)
|
|
go func(fn func() error) {
|
|
defer wg.Done()
|
|
if err := fn(); err != nil {
|
|
errs <- err
|
|
}
|
|
}(fn)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|