Documentation
¶
Overview ¶
Package kernel boots the application.
It is the single place where an application is composed. One difference matters: the Kernel boots ONCE, at process start, not per request, so nothing here may assume request scope.
Index ¶
- func FormatRoutes(routes []*httpx.Route) string
- type Background
- type Bootable
- type Closable
- type Diagnostic
- type Health
- type Kernel
- func (k *Kernel) Boot(ctx context.Context) error
- func (k *Kernel) Config() config.Config
- func (k *Kernel) Diagnose(ctx context.Context) []string
- func (k *Kernel) Handler() http.Handler
- func (k *Kernel) Logger() *slog.Logger
- func (k *Kernel) Migrations() []Migration
- func (k *Kernel) Recorder() *observability.Recorder
- func (k *Kernel) Register(mods ...Module) *Kernel
- func (k *Kernel) Routes() []*httpx.Route
- func (k *Kernel) Run(ctx context.Context) error
- func (k *Kernel) Shutdown() error
- func (k *Kernel) Tasks() []Task
- func (k *Kernel) Use(mw ...httpx.Middleware) *Kernel
- type Locker
- type Migratable
- type Migration
- type Module
- type RendererProvider
- type Schedulable
- type Scope
- type Task
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func FormatRoutes ¶
FormatRoutes renders the route table for the terminal, grouped by module and sorted by pattern. It is here, and not in the CLI, so that every project prints the same table.
Types ¶
type Background ¶ added in v0.10.0
Background is optional: the module runs a loop of its own -- the scheduler and the outbox relay do.
Start is called by Run, never by Boot, and that distinction is the difference between a process that serves and a process that does something else. Every command boots: `aru work`, `aru routes`, `aru schedule:list`, `aru migrate`. Starting the loops at boot meant every worker replica also ran a scheduler and a relay, and `aru schedule:run`, which exists to run one task by hand, started the loop that runs all of them. Found by audit.
One process, one job. The lock in the scheduler makes the duplicate harmless rather than correct, and "harmless because something else catches it" is not a design.
type Bootable ¶
Bootable is optional: implement it when the module needs to prepare state at boot -- open a pool, warm a cache, register codecs.
Boot wires; it does not run. A module that needs a loop of its own implements Background instead, and validates in Boot whatever would make that loop fail.
type Diagnostic ¶ added in v0.6.0
Diagnostic is optional: the module reports what it knows about the state of the system, in sentences a person can act on.
It feeds the error page. The most useful hint is often about something that happened outside the failing request -- the outbox stuck for four minutes, a job that has not run -- and a page that only looks at the request cannot see any of it.
Return nothing when there is nothing wrong. A diagnosis that always says something is a diagnosis nobody reads.
type Kernel ¶
type Kernel struct {
// contains filtered or unexported fields
}
Kernel holds the composed application: configuration, modules, the global middleware pipeline and the router.
func New ¶
New assembles the kernel. It opens no connection and listens on no port -- that is Boot and Run.
func (*Kernel) Boot ¶
Boot initializes modules and registers routes. It fails fast: if any module fails to boot the process does not come up. There is no silent degraded mode.
func (*Kernel) Diagnose ¶ added in v0.6.0
Diagnose asks every module that implements Diagnostic what is wrong right now, and returns what they say.
Pass it to errorpage.Options.Diagnose. It is not wired automatically because the pipeline is assembled in the open, in the application.
func (*Kernel) Handler ¶
Handler returns the composed handler: the router wrapped in the global pipeline, with the application logger installed above everything else. Useful for tests, which drive the whole stack without a socket.
The logger has to be outermost. Without it, every Log(ctx) call in a request would fall back to slog.Default() and ignore the configured handler and level, which in production means request logs in the wrong format.
func (*Kernel) Logger ¶
Logger returns the root logger. Request handlers must use observability.Log(ctx) instead, which carries the request id.
func (*Kernel) Migrations ¶
Migrations collects the migrations of every module, in registration order. Hand the result to data.Migrate.
func (*Kernel) Recorder ¶ added in v0.4.0
func (k *Kernel) Recorder() *observability.Recorder
Recorder returns the buffer behind /_arandu/debug, or nil when nothing is recording.
Pass it to middleware.Observe, and to the background loops that deserve the same page:
k.Use(middleware.Observe(k.Recorder(), cfg.TracingSecret))
w := jobs.NewWorker(store, jobs.WorkerOptions{Recorder: k.Recorder()})
scheduler.NewModule(k.Tasks(), scheduler.Options{Recorder: k.Recorder()})
The nil is the point. Outside development, without a tracing secret, there is no recorder -- so those loops build no Collector, record nothing, and cost nothing. They used to build one unconditionally and throw it away.
It is not wired automatically because the pipeline is assembled in the application, in the open, and a middleware that reached back into the kernel for state would be the kind of hidden coupling the explicit wiring exists to avoid.
func (*Kernel) Register ¶
Register adds modules in the order they will be booted. Order matters: a module may depend on another one already being up.
func (*Kernel) Routes ¶
Routes returns the registered routes. It is empty before Boot, because a module only registers its routes when it boots.
func (*Kernel) Run ¶
Run starts the server and blocks until SIGINT or SIGTERM, then shuts down gracefully.
func (*Kernel) Shutdown ¶
Shutdown stops the server and closes the modules in reverse registration order, which is the only order that respects dependencies between them.
func (*Kernel) Tasks ¶ added in v0.8.0
Tasks collects the scheduled work from every registered module, in registration order.
Same shape as Migrations(): the module declares, the kernel collects, and the scheduler module runs. Pass it to scheduler.NewModule, which is why that one is registered last.
type Locker ¶ added in v0.8.0
type Locker interface {
Run(ctx context.Context, name string, ttl time.Duration, fn func(context.Context) error) error
}
Locker is a distributed lock, for work that must happen once across replicas.
It lives here because two things need it -- the outbox relay and the scheduler -- and two identical interfaces in two packages is the duplication that the second one would create. github.com/arandu-io/kv implements it.
Nil is correct for a single replica and wrong for two. What it costs is duplicate work, which every task here has to tolerate anyway.
type Migratable ¶
type Migratable interface {
Migrations() []Migration
}
Migratable is optional: the module declares its migrations, and the Kernel collects them from every registered module in registration order.
type Migration ¶
Migration is a versioned, immutable-once-published schema change.
It is an alias, not a copy: the migration runner lives in the data package, and a module must be able to hand its migrations straight to it.
type Module ¶
type Module interface {
// Name is the stable identifier of the module: a lowercase slug, no spaces.
Name() string
// Routes registers the module's HTTP routes.
Routes(r *httpx.Router)
}
Module is the only unit of composition in the framework.
A module is a directory. It registers its own routes, its own migrations and its own dependency graph. There is no injection container and no reflection based resolution: the wiring is explicit, and the CLI generates the file that instantiates everything.
Every third-party module implements this interface and nothing else. It is the public contract of the framework -- change it and the whole ecosystem breaks, so change it with great care.
type RendererProvider ¶ added in v0.11.0
RendererProvider is optional: the module supplies the view renderer.
The view package implements it. The kernel cannot import that package -- it implements kernel.Module, so the import would be a cycle -- and an application calling a wiring function by hand is a line somebody forgets. An optional interface solves both: the kernel asks every module whether it brings a renderer, before any route is registered.
Two modules providing one is a wiring mistake, and the kernel refuses to boot rather than pick one.
type Schedulable ¶ added in v0.8.0
type Schedulable interface {
Schedule() []Task
}
Schedulable is optional: the module declares its scheduled work.
type Scope ¶ added in v0.8.0
type Scope int
Scope says whether a task runs once or once per tenant.
const ( // Global runs the task once for the whole instance. // // It gets the zero Grant, because SystemGrant refuses an empty tenant // (RULE 14) -- so a global task cannot pass any Check and cannot reach a // repository. That is a constraint rather than an oversight: global work is // cleaning temporary files, warming a cache, checking a certificate. Work // that reads a customer's rows is PerTenant, and having to say so is the // point. Global Scope = iota // PerTenant expands the task to every active tenant, each with its own // Grant and its own lock. PerTenant )
type Task ¶ added in v0.8.0
type Task struct {
// ID identifies the task in logs, in `aru schedule:list` and in the lock.
// It is stable: changing it starts a new task rather than renaming one.
ID string
// Spec is a five-field cron expression: minute hour day month weekday.
Spec string
// Scope decides Global or PerTenant.
Scope Scope
// Timeout bounds one run, and is also the lock TTL: a process that dies
// holding the lock releases it when the timeout passes.
Timeout time.Duration
// Singleton takes the distributed lock, so exactly one replica runs it.
// Set it false only for work that is harmless to do N times.
Singleton bool
// Action is what the run is authorized for. It becomes the SystemGrant the
// task receives, so a task reaches repositories the same way a request
// does -- there is no unauthorized path from the scheduler either.
Action security.Action
// Run does the work. It gets the Grant built from Action and the tenant.
Run func(ctx context.Context, g security.Grant) error
}
Task is scheduled work.
The shape mirrors Migrations(): the module declares, the kernel collects, and nothing runs until something asks. What a module never does is start its own goroutine.