feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
119
internal/workercluster/bootstrap.go
Обычный файл
119
internal/workercluster/bootstrap.go
Обычный файл
@@ -0,0 +1,119 @@
|
||||
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
|
||||
}
|
||||
Ссылка в новой задаче
Block a user