165 lines
5.1 KiB
Markdown
165 lines
5.1 KiB
Markdown
---
|
|
title: Lifecycle
|
|
description: 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
func (c *Core) ServiceStartup(ctx context.Context, options any) error
|
|
```
|
|
|
|
`ServiceStartup` does two things, in order:
|
|
|
|
1. Calls `OnStartup(ctx)` on every `Startable` service, in registration order.
|
|
2. 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
|
|
|
|
```go
|
|
func (c *Core) ServiceShutdown(ctx context.Context) error
|
|
```
|
|
|
|
`ServiceShutdown` does three things, in order:
|
|
|
|
1. Broadcasts an `ActionServiceShutdown{}` message via the message bus.
|
|
2. Calls `OnShutdown(ctx)` on every `Stoppable` service, in reverse registration order.
|
|
3. Waits for any in-flight `PerformAsync` background 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 |
|
|
|
|
```go
|
|
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`:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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](services.md) -- how services are registered
|
|
- [Messaging](messaging.md) -- the `ACTION` broadcast used during lifecycle
|
|
- [Configuration](configuration.md) -- `WithServiceLock` and other options
|