diff --git a/Makefile b/Makefile index ed5eae18..73ea05e1 100755 --- a/Makefile +++ b/Makefile @@ -42,3 +42,6 @@ rapid-crash: sudo docker run --restart=always --name test_crash debian:bookworm-slim /bin/cat &&\ sleep 3 &&\ sudo docker rm -f test_crash + +debug-list-containers: + bash -c 'echo -e "GET /containers/json HTTP/1.0\r\n" | sudo netcat -U /var/run/docker.sock | tail -n +9 | jq' \ No newline at end of file diff --git a/src/docker/client.go b/src/docker/client.go index e6f654ad..baebb3f4 100644 --- a/src/docker/client.go +++ b/src/docker/client.go @@ -16,6 +16,8 @@ type Client struct { key string refCount *atomic.Int32 *client.Client + + l logrus.FieldLogger } func (c Client) DaemonHostname() string { @@ -23,10 +25,13 @@ func (c Client) DaemonHostname() string { return url.Hostname() } +func (c Client) Connected() bool { + return c.Client != nil +} + // if the client is still referenced, this is no-op -func (c Client) Close() error { - if c.refCount.Load() > 0 { - c.refCount.Add(-1) +func (c *Client) Close() error { + if c.refCount.Add(-1) > 0 { return nil } @@ -34,7 +39,15 @@ func (c Client) Close() error { defer clientMapMu.Unlock() delete(clientMap, c.key) - return c.Client.Close() + client := c.Client + c.Client = nil + + c.l.Debugf("client closed") + + if client != nil { + return client.Close() + } + return nil } // ConnectClient creates a new Docker client connection to the specified host. @@ -94,12 +107,16 @@ func ConnectClient(host string) (Client, E.NestedError) { return Client{}, err } - clientMap[host] = Client{ + c := Client{ Client: client, key: host, refCount: &atomic.Int32{}, + l: logger.WithField("docker_client", client.DaemonHost()), } - clientMap[host].refCount.Add(1) + c.refCount.Add(1) + c.l.Debugf("client connected") + + clientMap[host] = c return clientMap[host], nil } diff --git a/src/docker/container.go b/src/docker/container.go index 97e5e8a4..9a7aa3ba 100644 --- a/src/docker/container.go +++ b/src/docker/container.go @@ -10,17 +10,18 @@ import ( ) type ProxyProperties struct { - DockerHost string `yaml:"docker_host" json:"docker_host"` - ContainerName string `yaml:"container_name" json:"container_name"` - ImageName string `yaml:"image_name" json:"image_name"` - Aliases []string `yaml:"aliases" json:"aliases"` - IsExcluded bool `yaml:"is_excluded" json:"is_excluded"` - FirstPort string `yaml:"first_port" json:"first_port"` - IdleTimeout string `yaml:"idle_timeout" json:"idle_timeout"` - WakeTimeout string `yaml:"wake_timeout" json:"wake_timeout"` - StopMethod string `yaml:"stop_method" json:"stop_method"` - StopTimeout string `yaml:"stop_timeout" json:"stop_timeout"` // stop_method = "stop" only - StopSignal string `yaml:"stop_signal" json:"stop_signal"` // stop_method = "stop" | "kill" only + DockerHost string `yaml:"-" json:"docker_host"` + ContainerName string `yaml:"-" json:"container_name"` + ImageName string `yaml:"-" json:"image_name"` + Aliases []string `yaml:"-" json:"aliases"` + IsExcluded bool `yaml:"-" json:"is_excluded"` + FirstPort string `yaml:"-" json:"first_port"` + IdleTimeout string `yaml:"-" json:"idle_timeout"` + WakeTimeout string `yaml:"-" json:"wake_timeout"` + StopMethod string `yaml:"-" json:"stop_method"` + StopTimeout string `yaml:"-" json:"stop_timeout"` // stop_method = "stop" only + StopSignal string `yaml:"-" json:"stop_signal"` // stop_method = "stop" | "kill" only + Running bool `yaml:"-" json:"running"` } type Container struct { @@ -42,6 +43,7 @@ func FromDocker(c *types.Container, dockerHost string) (res Container) { StopMethod: res.getDeleteLabel(LabelStopMethod), StopTimeout: res.getDeleteLabel(LabelStopTimeout), StopSignal: res.getDeleteLabel(LabelStopSignal), + Running: c.Status == "running", } return } diff --git a/src/docker/idlewatcher/watcher.go b/src/docker/idlewatcher/watcher.go index e02dd3c0..a42a2e8f 100644 --- a/src/docker/idlewatcher/watcher.go +++ b/src/docker/idlewatcher/watcher.go @@ -15,10 +15,13 @@ import ( E "github.com/yusing/go-proxy/error" P "github.com/yusing/go-proxy/proxy" PT "github.com/yusing/go-proxy/proxy/fields" + W "github.com/yusing/go-proxy/watcher" + event "github.com/yusing/go-proxy/watcher/events" ) type watcher struct { *P.ReverseProxyEntry + client D.Client refCount atomic.Int32 @@ -26,6 +29,7 @@ type watcher struct { stopByMethod StopCallback wakeCh chan struct{} wakeDone chan E.NestedError + running atomic.Bool ctx context.Context cancel context.CancelFunc @@ -36,7 +40,7 @@ type watcher struct { type ( WakeDone <-chan error WakeFunc func() WakeDone - StopCallback func() (bool, E.NestedError) + StopCallback func() E.NestedError ) func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) { @@ -51,6 +55,7 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) { if w, ok := watcherMap[entry.ContainerName]; ok { w.refCount.Add(1) + w.ReverseProxyEntry = entry return w, nil } @@ -67,8 +72,9 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) { l: logger.WithField("container", entry.ContainerName), } w.refCount.Add(1) - + w.running.Store(entry.ContainerRunning) w.stopByMethod = w.getStopCallback() + watcherMap[w.ContainerName] = w go func() { @@ -84,13 +90,14 @@ func Unregister(containerName string) { defer watcherMapMu.Unlock() if w, ok := watcherMap[containerName]; ok { - if w.refCount.Load() == 0 { + if w.refCount.Add(-1) > 0 { + return + } + if w.cancel != nil { w.cancel() - close(w.wakeCh) - delete(watcherMap, containerName) - } else { - w.refCount.Add(-1) } + w.client.Close() + delete(watcherMap, containerName) } } @@ -131,19 +138,26 @@ func (w *watcher) PatchRoundTripper(rtp http.RoundTripper) roundTripper { } func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*http.Response, error) { - timeout := time.After(w.WakeTimeout) w.wakeCh <- struct{}{} + + if w.running.Load() { + return origRoundTrip(req) + } + timeout := time.After(w.WakeTimeout) + for { + if w.running.Load() { + return origRoundTrip(req) + } select { + case <-req.Context().Done(): + return nil, req.Context().Err() case err := <-w.wakeDone: if err != nil { return nil, err.Error() } - return origRoundTrip(req) case <-timeout: - resp := loadingResponse - resp.TLS = req.TLS - return &resp, nil + return getLoadingResponse(), nil } } } @@ -178,36 +192,23 @@ func (w *watcher) containerStatus() (string, E.NestedError) { return json.State.Status, nil } -func (w *watcher) wakeIfStopped() (bool, E.NestedError) { - failure := E.Failure("wake") +func (w *watcher) wakeIfStopped() E.NestedError { status, err := w.containerStatus() if err.HasError() { - return false, failure.With(err) + return err } // "created", "running", "paused", "restarting", "removing", "exited", or "dead" switch status { case "exited", "dead": - err = E.From(w.containerStart()) + return E.From(w.containerStart()) case "paused": - err = E.From(w.containerUnpause()) + return E.From(w.containerUnpause()) case "running": - return false, nil + w.running.Store(true) + return nil default: - return false, failure.With(E.Unexpected("container state", status)) - } - - if err.HasError() { - return false, failure.With(err) - } - - status, err = w.containerStatus() - if err.HasError() { - return false, failure.With(err) - } else if status != "running" { - return false, failure.With(E.Unexpected("container state", status)) - } else { - return true, nil + return E.Unexpected("container state", status) } } @@ -223,19 +224,15 @@ func (w *watcher) getStopCallback() StopCallback { default: panic("should not reach here") } - return func() (bool, E.NestedError) { + return func() E.NestedError { status, err := w.containerStatus() if err.HasError() { - return false, E.FailWith("stop", err) + return err } if status != "running" { - return false, nil - } - err = E.From(cb()) - if err.HasError() { - return false, E.FailWith("stop", err) + return nil } - return true, nil + return E.From(cb()) } } @@ -244,55 +241,69 @@ func (w *watcher) watch() { w.ctx = watcherCtx w.cancel = watcherCancel + dockerWatcher := W.NewDockerWatcherWithClient(w.client) + + defer close(w.wakeCh) + + dockerEventCh, dockerEventErrCh := dockerWatcher.EventsWithOptions(w.ctx, W.DockerListOptions{ + Filters: W.NewDockerFilter( + W.DockerFilterContainer, + W.DockerrFilterContainerName(w.ContainerName), + W.DockerFilterStart, + W.DockerFilterStop, + W.DockerFilterDie, + W.DockerFilterKill, + W.DockerFilterPause, + W.DockerFilterUnpause, + ), + }) + ticker := time.NewTicker(w.IdleTimeout) defer ticker.Stop() for { select { case <-mainLoopCtx.Done(): - watcherCancel() + w.cancel() case <-watcherCtx.Done(): w.l.Debug("stopped") return + case err := <-dockerEventErrCh: + if err != nil && err.IsNot(context.Canceled) { + w.l.Error(E.FailWith("docker watcher", err)) + } + case e := <-dockerEventCh: + switch e.Action { + case event.ActionDockerStartUnpause: + w.running.Store(true) + w.l.Infof("%s %s", e.ActorName, e.Action) + case event.ActionDockerStopPause: + w.running.Store(false) + w.l.Infof("%s %s", e.ActorName, e.Action) + } case <-ticker.C: w.l.Debug("timeout") - stopped, err := w.stopByMethod() - if err.HasError() { - w.l.Error(err.Extraf("stop method: %s", w.StopMethod)) - } else if stopped { - w.l.Infof("%s: ok", w.StopMethod) - } else { - ticker.Stop() + ticker.Stop() + if err := w.stopByMethod(); err != nil && err.IsNot(context.Canceled) { + w.l.Error(E.FailWith("stop", err).Extraf("stop method: %s", w.StopMethod)) } case <-w.wakeCh: - w.l.Debug("wake received") - go func() { - started, err := w.wakeIfStopped() - if err != nil { - w.l.Error(err) - } else if started { - w.l.Infof("awaken") - ticker.Reset(w.IdleTimeout) - } - w.wakeDone <- err // this is passed to roundtrip - }() + w.l.Debug("wake signal received") + ticker.Reset(w.IdleTimeout) + err := w.wakeIfStopped() + if err != nil && err.IsNot(context.Canceled) { + w.l.Error(E.FailWith("wake", err)) + } + select { + case w.wakeDone <- err: // this is passed to roundtrip + default: + } } } } -var ( - mainLoopCtx context.Context - mainLoopCancel context.CancelFunc - mainLoopWg sync.WaitGroup - - watcherMap = make(map[string]*watcher) - watcherMapMu sync.Mutex - - newWatcherCh = make(chan *watcher) - - logger = logrus.WithField("module", "idle_watcher") - - loadingResponse = http.Response{ +func getLoadingResponse() *http.Response { + return &http.Response{ StatusCode: http.StatusAccepted, Header: http.Header{ "Content-Type": {"text/html"}, @@ -305,6 +316,19 @@ var ( Body: io.NopCloser(bytes.NewReader((loadingPage))), ContentLength: int64(len(loadingPage)), } +} + +var ( + mainLoopCtx context.Context + mainLoopCancel context.CancelFunc + mainLoopWg sync.WaitGroup + + watcherMap = make(map[string]*watcher) + watcherMapMu sync.Mutex + + newWatcherCh = make(chan *watcher) + + logger = logrus.WithField("module", "idle_watcher") loadingPage = []byte(` @@ -317,12 +341,16 @@ var (
-Container is starting... Please wait
+