// Package selfupdate handles the special case where the update target // is the running service's own container. We must finish writing the // HTTP response (and flush + close the connection) before exec'ing // `docker compose up -d` against ourselves, otherwise the response is // lost when the container is replaced. package selfupdate import ( "context" "time" "github.com/shcizo/package-updater/internal/discovery" ) // IsSelf reports whether job targets the running service. // Matches on Compose project name (which is also typically the // service name for single-service stacks). func IsSelf(job discovery.Job, selfProject string) bool { return job.Project == selfProject } // innerExec is the executor abstraction we wrap. type innerExec interface { Execute(ctx context.Context, job discovery.Job) error } // Wrapped wraps an Executor with self-update-aware deferred execution. type Wrapped struct { inner innerExec selfProject string delay time.Duration } // Wrap returns a Wrapped that defers exec until after flush() for // self-updates. The delay is added after flush before exec, so the // kernel TCP buffer has time to drain. func Wrap(inner innerExec, selfProject string, delay time.Duration) *Wrapped { return &Wrapped{inner: inner, selfProject: selfProject, delay: delay} } // ExecuteWithFlush runs job, invoking flush() before exec for self-updates // and waiting `delay` after flush. For non-self jobs, exec happens // normally and flush is not invoked at all (the HTTP layer decides // when to flush in that case). func (w *Wrapped) ExecuteWithFlush(ctx context.Context, job discovery.Job, flush func()) error { if !IsSelf(job, w.selfProject) { return w.inner.Execute(ctx, job) } flush() select { case <-time.After(w.delay): case <-ctx.Done(): return ctx.Err() } return w.inner.Execute(ctx, job) }