From bde8d4c7ccc6887113e12bfa33e418e0ed48d4e2 Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 20 Mar 2026 17:59:43 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20lifecycle=20context=20=E2=80=94=20Core.?= =?UTF-8?q?Context()=20for=20cooperative=20shutdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Core holds context.Context + CancelFunc - New() creates background context - ServiceStartup creates fresh context from caller's ctx (restart safe) - ServiceShutdown cancels context before draining tasks - c.Context() accessor lets task handlers check Done() for graceful exit Co-Authored-By: Virgil --- pkg/core/contract.go | 1 + pkg/core/core.go | 4 ++++ pkg/core/runtime.go | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/core/contract.go b/pkg/core/contract.go index 2f52e6b..15ba428 100644 --- a/pkg/core/contract.go +++ b/pkg/core/contract.go @@ -86,6 +86,7 @@ func New(opts ...Options) *Core { services: &serviceRegistry{services: make(map[string]*Service)}, commands: &commandRegistry{commands: make(map[string]*Command)}, } + c.ctx, c.cancel = context.WithCancel(context.Background()) if len(opts) > 0 { cp := make(Options, len(opts[0])) diff --git a/pkg/core/core.go b/pkg/core/core.go index b88b0ed..1ec66d0 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -6,6 +6,7 @@ package core import ( + "context" "sync" "sync/atomic" ) @@ -29,6 +30,8 @@ type Core struct { ipc *Ipc // c.IPC() — Message bus for IPC i18n *I18n // c.I18n() — Internationalisation and locale collection + ctx context.Context + cancel context.CancelFunc taskIDCounter atomic.Uint64 wg sync.WaitGroup shutdown atomic.Bool @@ -48,6 +51,7 @@ func (c *Core) Log() *ErrorLog { return c.log } func (c *Core) Cli() *Cli { return c.cli } func (c *Core) IPC() *Ipc { return c.ipc } func (c *Core) I18n() *I18n { return c.i18n } +func (c *Core) Context() context.Context { return c.ctx } func (c *Core) Core() *Core { return c } // --- IPC (uppercase aliases) --- diff --git a/pkg/core/runtime.go b/pkg/core/runtime.go index a81afbd..6eae703 100644 --- a/pkg/core/runtime.go +++ b/pkg/core/runtime.go @@ -34,6 +34,7 @@ func (r *ServiceRuntime[T]) Config() *Config { return r.core.Config() } // ServiceStartup runs OnStart for all registered services that have one. func (c *Core) ServiceStartup(ctx context.Context, options any) Result { c.shutdown.Store(false) + c.ctx, c.cancel = context.WithCancel(ctx) startables := c.Startables() if startables.OK { for _, s := range startables.Value.([]*Service) { @@ -53,11 +54,10 @@ func (c *Core) ServiceStartup(ctx context.Context, options any) Result { // ServiceShutdown drains background tasks, then stops all registered services. func (c *Core) ServiceShutdown(ctx context.Context) Result { c.shutdown.Store(true) + c.cancel() // signal all context-aware tasks to stop c.ACTION(ActionServiceShutdown{}) // Drain background tasks before stopping services. - // On timeout, the waiter goroutine persists until tasks complete — - // this is inherent to sync.WaitGroup (no cancel mechanism). done := make(chan struct{}) go func() { c.wg.Wait()