docker: clear routes on docker disconnect, reload routes on connection restore

This commit is contained in:
yusing 2025-02-23 13:11:21 +08:00
parent 2c21387ad9
commit 3e1a7a0dc5
4 changed files with 73 additions and 23 deletions

View file

@ -213,7 +213,7 @@ func (w *Watcher) expires() time.Time {
return w.lastReset.Add(w.IdleTimeout) return w.lastReset.Add(w.IdleTimeout)
} }
func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventCh <-chan events.Event, errCh <-chan gperr.Error) { func (w *Watcher) getEventCh(dockerWatcher *watcher.DockerWatcher) (eventCh <-chan events.Event, errCh <-chan gperr.Error) {
eventCh, errCh = dockerWatcher.EventsWithOptions(w.Task().Context(), watcher.DockerListOptions{ eventCh, errCh = dockerWatcher.EventsWithOptions(w.Task().Context(), watcher.DockerListOptions{
Filters: watcher.NewDockerFilter( Filters: watcher.NewDockerFilter(
watcher.DockerFilterContainer, watcher.DockerFilterContainer,

View file

@ -6,6 +6,7 @@ import (
"github.com/yusing/go-proxy/internal/route/provider/types" "github.com/yusing/go-proxy/internal/route/provider/types"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/watcher" "github.com/yusing/go-proxy/internal/watcher"
eventsPkg "github.com/yusing/go-proxy/internal/watcher/events"
) )
type EventHandler struct { type EventHandler struct {
@ -29,10 +30,19 @@ func (p *Provider) newEventHandler() *EventHandler {
func (handler *EventHandler) Handle(parent task.Parent, events []watcher.Event) { func (handler *EventHandler) Handle(parent task.Parent, events []watcher.Event) {
oldRoutes := handler.provider.routes oldRoutes := handler.provider.routes
isForceReload := false
for _, event := range events {
if event.Action == eventsPkg.ActionForceReload {
isForceReload = true
break
}
}
newRoutes, err := handler.provider.loadRoutes() newRoutes, err := handler.provider.loadRoutes()
if err != nil { if err != nil {
handler.errs.Add(err) handler.errs.Add(err)
if len(newRoutes) == 0 { if len(newRoutes) == 0 && !isForceReload {
return return
} }
} }

View file

@ -2,19 +2,22 @@ package watcher
import ( import (
"context" "context"
"errors"
"time" "time"
docker_events "github.com/docker/docker/api/types/events" docker_events "github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
D "github.com/yusing/go-proxy/internal/docker" "github.com/docker/docker/client"
"github.com/yusing/go-proxy/internal/docker"
"github.com/yusing/go-proxy/internal/gperr" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/watcher/events" "github.com/yusing/go-proxy/internal/watcher/events"
) )
type ( type (
DockerWatcher struct { DockerWatcher struct {
host string host string
client *D.SharedClient client *docker.SharedClient
clientOwned bool clientOwned bool
} }
DockerListOptions = docker_events.ListOptions DockerListOptions = docker_events.ListOptions
@ -42,38 +45,66 @@ var (
)} )}
dockerWatcherRetryInterval = 3 * time.Second dockerWatcherRetryInterval = 3 * time.Second
reloadTrigger = Event{
Type: events.EventTypeDocker,
Action: events.ActionForceReload,
ActorAttributes: map[string]string{},
ActorName: "",
ActorID: "",
}
) )
func DockerFilterContainerNameID(nameOrID string) filters.KeyValuePair { func DockerFilterContainerNameID(nameOrID string) filters.KeyValuePair {
return filters.Arg("container", nameOrID) return filters.Arg("container", nameOrID)
} }
func NewDockerWatcher(host string) DockerWatcher { func NewDockerWatcher(host string) *DockerWatcher {
return DockerWatcher{ return &DockerWatcher{
host: host, host: host,
clientOwned: true, clientOwned: true,
} }
} }
func NewDockerWatcherWithClient(client *D.SharedClient) DockerWatcher { func NewDockerWatcherWithClient(client *docker.SharedClient) *DockerWatcher {
return DockerWatcher{ return &DockerWatcher{
client: client, client: client,
} }
} }
func (w DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan gperr.Error) { func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan gperr.Error) {
return w.EventsWithOptions(ctx, optionsDefault) return w.EventsWithOptions(ctx, optionsDefault)
} }
func (w DockerWatcher) Close() { func (w *DockerWatcher) Close() {
if w.clientOwned && w.client.Connected() { if w.clientOwned && w.client.Connected() {
w.client.Close() w.client.Close()
} }
} }
func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerListOptions) (<-chan Event, <-chan gperr.Error) { func (w *DockerWatcher) parseError(err error) gperr.Error {
eventCh := make(chan Event, 100) if errors.Is(err, context.DeadlineExceeded) {
errCh := make(chan gperr.Error, 10) return gperr.New("docker client connection timeout")
}
if client.IsErrConnectionFailed(err) {
return gperr.New("docker client connection failure")
}
return gperr.Wrap(err)
}
func (w *DockerWatcher) checkConnection(ctx context.Context) bool {
ctx, cancel := context.WithTimeout(ctx, dockerWatcherRetryInterval)
defer cancel()
_, err := w.client.Ping(ctx)
if err != nil {
return false
}
return true
}
func (w *DockerWatcher) EventsWithOptions(ctx context.Context, options DockerListOptions) (<-chan Event, <-chan gperr.Error) {
eventCh := make(chan Event)
errCh := make(chan gperr.Error)
go func() { go func() {
defer func() { defer func() {
@ -84,7 +115,7 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
if !w.client.Connected() { if !w.client.Connected() {
var err error var err error
w.client, err = D.ConnectClient(w.host) w.client, err = docker.ConnectClient(w.host)
attempts := 0 attempts := 0
retryTicker := time.NewTicker(dockerWatcherRetryInterval) retryTicker := time.NewTicker(dockerWatcherRetryInterval)
for err != nil { for err != nil {
@ -95,7 +126,7 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
retryTicker.Stop() retryTicker.Stop()
return return
case <-retryTicker.C: case <-retryTicker.C:
w.client, err = D.ConnectClient(w.host) w.client, err = docker.ConnectClient(w.host)
} }
} }
retryTicker.Stop() retryTicker.Stop()
@ -104,7 +135,7 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
defer w.Close() defer w.Close()
cEventCh, cErrCh := w.client.Events(ctx, options) cEventCh, cErrCh := w.client.Events(ctx, options)
defer logging.Debug().Str("host", w.host).Msg("docker watcher closed")
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -126,15 +157,22 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
if err == nil { if err == nil {
continue continue
} }
errCh <- gperr.Wrap(err) errCh <- w.parseError(err)
// trigger reload (clear routes)
eventCh <- reloadTrigger
for !w.checkConnection(ctx) {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
default: case <-time.After(dockerWatcherRetryInterval):
time.Sleep(dockerWatcherRetryInterval) continue
cEventCh, cErrCh = w.client.Events(ctx, options)
} }
} }
// connection successful, trigger reload (reload routes)
eventCh <- reloadTrigger
// reopen event channel
cEventCh, cErrCh = w.client.Events(ctx, options)
}
} }
}() }()

View file

@ -34,6 +34,8 @@ const (
ActionContainerDie ActionContainerDie
ActionContainerDestroy ActionContainerDestroy
ActionForceReload
actionContainerWakeMask = ActionContainerCreate | ActionContainerStart | ActionContainerUnpause actionContainerWakeMask = ActionContainerCreate | ActionContainerStart | ActionContainerUnpause
actionContainerSleepMask = ActionContainerKill | ActionContainerStop | ActionContainerPause | ActionContainerDie actionContainerSleepMask = ActionContainerKill | ActionContainerStop | ActionContainerPause | ActionContainerDie
) )