subscribe to Docker container events with backoff

This commit is contained in:
Pavel Sviderski
2024-11-18 14:43:42 +10:00
parent 24371eb401
commit 54a8b09c9f
2 changed files with 140 additions and 29 deletions
+89 -6
View File
@@ -2,7 +2,10 @@ package docker
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"log/slog" "log/slog"
"time" "time"
@@ -12,18 +15,19 @@ import (
const ( const (
NetworkName = "uncloud" NetworkName = "uncloud"
UserChain = "DOCKER-USER" UserChain = "DOCKER-USER"
// EventsDebounceInterval defines how long to wait before processing the next Docker event. Multiple events
// occurring within this window will be processed together as a single event to prevent system overload.
EventsDebounceInterval = 100 * time.Millisecond
// SyncInterval defines a regular interval to sync containers to the cluster store.
SyncInterval = 30 * time.Second
) )
type Manager struct { type Manager struct {
client *client.Client client *client.Client
store *store.Store
} }
func NewManager(client *client.Client, store *store.Store) *Manager { func NewManager(client *client.Client) *Manager {
return &Manager{ return &Manager{client: client}
client: client,
store: store,
}
} }
// WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests. // WaitDaemonReady waits for the Docker daemon to start and be ready to serve requests.
@@ -53,3 +57,82 @@ func (d *Manager) WaitDaemonReady(ctx context.Context) error {
} }
return nil return nil
} }
func (d *Manager) WatchAndSyncContainers(ctx context.Context, store *store.Store) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Filter only local container events.
opts := events.ListOptions{
Filters: filters.NewArgs(
filters.Arg("scope", "local"),
filters.Arg("type", string(events.ContainerEventType)),
),
}
// Subscribe to Docker events before running the initial sync to avoid missing any events.
eventCh, errCh := d.client.Events(ctx, opts)
slog.Debug("Syncing containers to cluster store before processing Docker events.")
if err := d.syncContainersToStore(ctx, store); err != nil {
// The deferred cancel will stop the event subscription.
return fmt.Errorf("sync containers to cluster store: %w", err)
}
var (
// debouncer is used to debounce multiple Docker events into a single event sent to the debouncerCh
// to prevent system overload.
debouncer *time.Timer
debouncerCh = make(chan events.Message)
// ticker is used to trigger a regular sync of containers to the cluster store as a fallback.
ticker = time.NewTicker(SyncInterval)
)
defer ticker.Stop()
for {
select {
case e := <-eventCh:
switch e.Action {
case events.ActionStart,
events.ActionStop,
events.ActionPause,
events.ActionUnPause,
events.ActionKill,
events.ActionDie,
events.ActionOOM,
events.ActionHealthStatusHealthy,
events.ActionHealthStatusUnhealthy:
if debouncer == nil {
debouncer = time.AfterFunc(EventsDebounceInterval, func() {
debouncerCh <- e
})
}
}
case e := <-debouncerCh:
debouncer = nil
slog.Debug("Syncing containers to cluster store triggered by a Docker container event.",
"container_id", e.Actor.ID,
"container_name", e.Actor.Attributes["name"],
"action", e.Action)
if err := d.syncContainersToStore(ctx, store); err != nil {
return fmt.Errorf("sync containers to cluster store: %w", err)
}
case <-ticker.C:
slog.Debug("Syncing containers to cluster store triggered by a regular interval.",
"interval", SyncInterval)
if err := d.syncContainersToStore(ctx, store); err != nil {
return fmt.Errorf("sync containers to cluster store: %w", err)
}
case err := <-errCh:
if errors.Is(err, context.Canceled) {
return nil
}
return fmt.Errorf("receive Docker event: %w", err)
}
}
}
func (d *Manager) syncContainersToStore(ctx context.Context, store *store.Store) error {
// TODO: implement
return nil
}
+51 -23
View File
@@ -98,29 +98,10 @@ func (nc *networkController) Run(ctx context.Context) error {
}, },
) )
// Setup Docker network and iptables rules in a goroutine because it may block until the Docker daemon is ready. // Setup Docker network and synchronise containers to the cluster store.
errGroup.Go( errGroup.Go(
func() error { func() error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) return nc.prepareAndWatchDocker(ctx)
if err != nil {
return fmt.Errorf("init Docker client: %w", err)
}
defer cli.Close()
manager := docker.NewManager(cli, nc.store)
if err := manager.WaitDaemonReady(ctx); err != nil {
return fmt.Errorf("wait for Docker daemon: %w", err)
}
if err := manager.EnsureUncloudNetwork(ctx, nc.state.Network.Subnet); err != nil {
return err
}
slog.Info("Docker network configured.")
//if err := d.WatchAndSyncContainers(ctx); err != nil {
// return fmt.Errorf("watch and sync containers to store: %w", err)
//}
return nil
}, },
) )
@@ -192,11 +173,55 @@ func (nc *networkController) Run(ctx context.Context) error {
return errGroup.Wait() return errGroup.Wait()
} }
// prepareAndWatchDocker configures the Docker network and watches local Docker containers to sync them
// to the cluster store.
func (nc *networkController) prepareAndWatchDocker(ctx context.Context) error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("init Docker client: %w", err)
}
defer cli.Close()
manager := docker.NewManager(cli)
if err = manager.WaitDaemonReady(ctx); err != nil {
return fmt.Errorf("wait for Docker daemon: %w", err)
}
if err = manager.EnsureUncloudNetwork(ctx, nc.state.Network.Subnet); err != nil {
return err
}
slog.Info("Docker network configured.")
slog.Info("Watching Docker containers and syncing them to the cluster store.")
// Retry to watch and sync containers until the context is done.
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(5*time.Second),
// Retry indefinitely.
backoff.WithMaxElapsedTime(0),
), ctx)
watchAndSync := func() error {
if wErr := manager.WatchAndSyncContainers(ctx, nc.store); wErr != nil {
slog.Error("Failed to watch and sync containers to cluster store, retrying.", "err", wErr)
return wErr
}
return nil
}
if err = backoff.Retry(watchAndSync, boff); err != nil {
if errors.Is(err, context.Canceled) {
return nil
}
return fmt.Errorf("watch and sync containers to cluster store: %w", err)
}
return nil
}
// handleMachineChanges subscribes to machine changes in the cluster and reconfigures the network peers accordingly. // handleMachineChanges subscribes to machine changes in the cluster and reconfigures the network peers accordingly.
func (nc *networkController) handleMachineChanges(ctx context.Context) error { func (nc *networkController) handleMachineChanges(ctx context.Context) error {
for { for {
// Retry to subscribe to machine changes indefinitely until the context is done. // Retry to subscribe to machine changes indefinitely until the context is done.
b := backoff.WithContext(backoff.NewExponentialBackOff( boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(1*time.Second), backoff.WithInitialInterval(1*time.Second),
backoff.WithMaxInterval(60*time.Second), backoff.WithMaxInterval(60*time.Second),
backoff.WithMaxElapsedTime(0), backoff.WithMaxElapsedTime(0),
@@ -213,7 +238,7 @@ func (nc *networkController) handleMachineChanges(ctx context.Context) error {
} }
return err return err
} }
if err = backoff.Retry(subscribe, b); err != nil { if err = backoff.Retry(subscribe, boff); err != nil {
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return nil return nil
} }
@@ -229,6 +254,9 @@ func (nc *networkController) handleMachineChanges(ctx context.Context) error {
// For simplicity, reconfigure all peers on any change. // For simplicity, reconfigure all peers on any change.
for { for {
select { select {
// TODO: test when Corrosion fails and the subscription fails to resubscribe (after 1 minute). It seems
// the changes channel will be closed and this will become a busy loop. Perhaps, the outer for loop should
// be reworked as well.
case <-changes: case <-changes:
slog.Info("Cluster machines changed, reconfiguring network peers.") slog.Info("Cluster machines changed, reconfiguring network peers.")
if machines, err = nc.store.ListMachines(ctx); err != nil { if machines, err = nc.store.ListMachines(ctx); err != nil {