5.1 KiB
| title | description |
|---|---|
| Lifecycle | Startable and Stoppable interfaces, startup and shutdown ordering. |
Lifecycle
Core manages the startup and shutdown of services through two opt-in interfaces. Services implement one or both to participate in the application lifecycle.
Interfaces
Startable
type Startable interface {
OnStartup(ctx context.Context) error
}
Services implementing Startable have their OnStartup method called during ServiceStartup. This is the place to:
- Open database connections
- Register message bus handlers (queries, tasks)
- Start background workers
- Validate configuration
Stoppable
type Stoppable interface {
OnShutdown(ctx context.Context) error
}
Services implementing Stoppable have their OnShutdown method called during ServiceShutdown. This is the place to:
- Close database connections
- Flush buffers
- Save state
- Cancel background workers
A service can implement both interfaces:
type Service struct{}
func (s *Service) OnStartup(ctx context.Context) error {
// Initialise resources
return nil
}
func (s *Service) OnShutdown(ctx context.Context) error {
// Release resources
return nil
}
Ordering
Startup: Registration Order
Services are started in the order they were registered. If you register services A, B, C (in that order), their OnStartup methods are called as A, B, C.
Shutdown: Reverse Registration Order
Services are stopped in reverse registration order. If A, B, C were registered, their OnShutdown methods are called as C, B, A.
This ensures that services which depend on earlier services are torn down first.
c, err := core.New()
_ = c.RegisterService("database", dbService) // started 1st, stopped 3rd
_ = c.RegisterService("cache", cacheService) // started 2nd, stopped 2nd
_ = c.RegisterService("api", apiService) // started 3rd, stopped 1st
_ = c.ServiceStartup(ctx, nil) // database -> cache -> api
_ = c.ServiceShutdown(ctx) // api -> cache -> database
ServiceStartup
func (c *Core) ServiceStartup(ctx context.Context, options any) error
ServiceStartup does two things, in order:
- Calls
OnStartup(ctx)on everyStartableservice, in registration order. - Broadcasts an
ActionServiceStartup{}message via the message bus.
If any service returns an error, it is collected but does not prevent other services from starting. All errors are aggregated with errors.Join and returned together.
If the context is cancelled before all services have started, the remaining services are skipped and the context error is included in the aggregate.
ServiceShutdown
func (c *Core) ServiceShutdown(ctx context.Context) error
ServiceShutdown does three things, in order:
- Broadcasts an
ActionServiceShutdown{}message via the message bus. - Calls
OnShutdown(ctx)on everyStoppableservice, in reverse registration order. - Waits for any in-flight
PerformAsyncbackground tasks to complete (respecting the context deadline).
As with startup, errors are aggregated rather than short-circuiting. If the context is cancelled during shutdown, the remaining services are skipped but the method still waits for background tasks.
Built-in Lifecycle Messages
Core broadcasts two action messages as part of the lifecycle. You can listen for these in any registered action handler:
| Message | When |
|---|---|
ActionServiceStartup{} |
After all Startable services have been called |
ActionServiceShutdown{} |
Before Stoppable services are called |
c.RegisterAction(func(c *core.Core, msg core.Message) error {
switch msg.(type) {
case core.ActionServiceStartup:
// All services are up
case core.ActionServiceShutdown:
// Shutdown is beginning
}
return nil
})
Error Handling
Lifecycle methods never panic. All errors from individual services are collected via errors.Join and returned as a single error. You can inspect individual errors with errors.Is and errors.As:
err := c.ServiceStartup(ctx, nil)
if err != nil {
// err may contain multiple wrapped errors
if errors.Is(err, context.Canceled) {
// context was cancelled
}
}
Context Cancellation
Both ServiceStartup and ServiceShutdown respect context cancellation. If the context is cancelled or its deadline is exceeded, the remaining services are skipped:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := c.ServiceStartup(ctx, nil)
// If startup takes longer than 5 seconds, remaining services are skipped
Detection
Lifecycle interface detection happens at registration time. When you call RegisterService, Core checks whether the service implements Startable and/or Stoppable and adds it to the appropriate internal list. There is no need to declare anything beyond implementing the interface.
Related Pages
- Services -- how services are registered
- Messaging -- the
ACTIONbroadcast used during lifecycle - Configuration --
WithServiceLockand other options