mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
fix: race on cluster init by waiting for corrosion service to become ready with schema applied
This commit is contained in:
@@ -108,11 +108,13 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
if err := cc.corroService.Restart(ctx); err != nil {
|
if err := cc.corroService.Restart(ctx); err != nil {
|
||||||
return fmt.Errorf("restart corrosion service: %w", err)
|
return fmt.Errorf("restart corrosion service: %w", err)
|
||||||
}
|
}
|
||||||
|
slog.Info("Corrosion service restarted.")
|
||||||
} else {
|
} else {
|
||||||
slog.Info("Starting corrosion service.")
|
slog.Info("Starting corrosion service.")
|
||||||
if err := cc.corroService.Start(ctx); err != nil {
|
if err := cc.corroService.Start(ctx); err != nil {
|
||||||
return fmt.Errorf("start corrosion service: %w", err)
|
return fmt.Errorf("start corrosion service: %w", err)
|
||||||
}
|
}
|
||||||
|
slog.Info("Corrosion service started.")
|
||||||
}
|
}
|
||||||
|
|
||||||
errGroup, ctx := errgroup.WithContext(ctx)
|
errGroup, ctx := errgroup.WithContext(ctx)
|
||||||
|
|||||||
@@ -27,29 +27,33 @@ type DockerService struct {
|
|||||||
User string
|
User string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDockerService(cli *client.Client, image, name, dataDir string) *DockerService {
|
|
||||||
return &DockerService{
|
|
||||||
Client: cli,
|
|
||||||
Image: image,
|
|
||||||
Name: name,
|
|
||||||
DataDir: dataDir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DockerService) Start(ctx context.Context) error {
|
func (s *DockerService) Start(ctx context.Context) error {
|
||||||
_, err := s.Client.ContainerInspect(ctx, s.Name)
|
_, err := s.Client.ContainerInspect(ctx, s.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if client.IsErrNotFound(err) {
|
if !client.IsErrNotFound(err) {
|
||||||
return s.startNewContainer(ctx)
|
return fmt.Errorf("inspect container %q: %w", s.Name, err)
|
||||||
|
}
|
||||||
|
if err = s.startNewContainer(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Container already exists.
|
||||||
|
// TODO: recreate only if the container configuration has to be changed.
|
||||||
|
if err = s.Client.ContainerRemove(ctx, s.Name, container.RemoveOptions{Force: true}); err != nil {
|
||||||
|
return fmt.Errorf("remove container %q: %w", s.Name, err)
|
||||||
|
}
|
||||||
|
if err = s.startNewContainer(ctx); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return fmt.Errorf("inspect container %q: %w", s.Name, err)
|
|
||||||
}
|
|
||||||
// TODO: recreate only if the container configuration has to be changed.
|
|
||||||
if err = s.Client.ContainerRemove(ctx, s.Name, container.RemoveOptions{Force: true}); err != nil {
|
|
||||||
return fmt.Errorf("remove container %q: %w", s.Name, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.startNewContainer(ctx)
|
slog.Debug("Waiting for corrosion service to be ready.")
|
||||||
|
if err = WaitReady(ctx, s.DataDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Debug("Corrosion service is ready.")
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerService) Stop(ctx context.Context) error {
|
func (s *DockerService) Stop(ctx context.Context) error {
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
package corroservice
|
package corroservice
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/psviderski/uncloud/internal/corrosion"
|
||||||
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Start(ctx context.Context) error
|
Start(ctx context.Context) error
|
||||||
@@ -8,3 +18,45 @@ type Service interface {
|
|||||||
Restart(ctx context.Context) error
|
Restart(ctx context.Context) error
|
||||||
Running() bool
|
Running() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitReady waits for the Corrosion service to be ready with the uncloud schema applied.
|
||||||
|
func WaitReady(ctx context.Context, dataDir string) error {
|
||||||
|
// Read the config file to get the API address.
|
||||||
|
configPath := filepath.Join(dataDir, "config.toml")
|
||||||
|
configData, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err = toml.Unmarshal(configData, &config); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
corro, err := corrosion.NewAPIClient(config.API.Addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create corrosion API client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corrosion starts serving the API before applying the schema. Query the cluster table with exponential backoff
|
||||||
|
// to check if the uncloud schema has been applied.
|
||||||
|
checkReady := func() error {
|
||||||
|
rows, err := corro.QueryContext(ctx, "SELECT 1 FROM cluster LIMIT 1")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("query cluster table: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b := backoff.NewExponentialBackOff(
|
||||||
|
backoff.WithInitialInterval(50*time.Millisecond),
|
||||||
|
backoff.WithMaxInterval(1*time.Second),
|
||||||
|
backoff.WithMaxElapsedTime(15*time.Second),
|
||||||
|
)
|
||||||
|
if err = backoff.Retry(checkReady, backoff.WithContext(b, ctx)); err != nil {
|
||||||
|
return fmt.Errorf("corrosion service did not become ready: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultSystemdUnit = "uncloud-corrosion.service"
|
const DefaultSystemdUnit = "uncloud-corrosion.service"
|
||||||
@@ -44,20 +43,15 @@ func (s *SystemdService) startOrRestart(ctx context.Context, cmd string) error {
|
|||||||
if _, err := exec.Command("systemctl", cmd, s.Unit).Output(); err != nil {
|
if _, err := exec.Command("systemctl", cmd, s.Unit).Output(); err != nil {
|
||||||
return fmt.Errorf("systemctl %s %s: %w", cmd, s.Unit, err)
|
return fmt.Errorf("systemctl %s %s: %w", cmd, s.Unit, err)
|
||||||
}
|
}
|
||||||
slog.Info(fmt.Sprintf("Corrosion systemd service %sed.", cmd), "unit", s.Unit)
|
slog.Debug(fmt.Sprintf("Corrosion systemd service %sed.", cmd), "unit", s.Unit)
|
||||||
|
|
||||||
// Optimistically wait for the corrosion service to start and initialise the database schema before proceeding.
|
slog.Debug("Waiting for corrosion service to be ready.")
|
||||||
timer := time.NewTimer(2 * time.Second)
|
if err := WaitReady(ctx, s.DataDir); err != nil {
|
||||||
defer timer.Stop()
|
return err
|
||||||
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
slog.Debug("Corrosion service is ready.")
|
||||||
// TODO: run a goroutine to check the status of the service and log any errors in the uncloud log.
|
|
||||||
s.running = true
|
s.running = true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
if err := m.config.CorrosionService.Start(ctx); err != nil {
|
if err := m.config.CorrosionService.Start(ctx); err != nil {
|
||||||
return fmt.Errorf("start corrosion service: %w", err)
|
return fmt.Errorf("start corrosion service: %w", err)
|
||||||
}
|
}
|
||||||
|
slog.Info("Corrosion service started.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use an errgroup to coordinate error handling and graceful shutdown of multiple machine components.
|
// Use an errgroup to coordinate error handling and graceful shutdown of multiple machine components.
|
||||||
|
|||||||
Reference in New Issue
Block a user