Documentation
¶
Overview ¶
Package foundation boots the application.
It is the single place where an application is composed. One difference matters: what this package builds is built ONCE, at process start, not per request, so nothing here may assume request scope.
What lives here ¶
Module and its optional interfaces -- Bootable, Background, Closable, Diagnostic, Health, Schedulable, Migratable -- are the whole vocabulary of composition. A module declares; the kernel collects; nothing runs until something asks. That shape is why Task and Migration are declared here rather than in the packages that execute them: the scheduler and the migration runner are consumers, and a module must be able to hand its declarations straight to either one.
It also owns what the framework mounts for itself. Everything under internalPrefix -- the health probe, the debug console, the development reload -- answers to the framework rather than to the application, and exceptInternal is the one place that boundary is enforced. Observe installs the request id, the request logger and, in development or under an authorized tracing header, the Collector behind that console. The live reload in reload.go follows a restart in development and costs nothing anywhere else.
The boot sequence, the view renderer and the console command belong to the layer above and are not declared here. This package keeps only the vocabulary a module needs in order to declare itself, so that the adapter packages that implement Module can compile against it without depending on that layer.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Observe ¶
func Observe(dev bool, tracingSecret string, recorder *log.Recorder) pipeline.Middleware[http.Handler]
Observe installs the request id, the request-scoped logger and -- in development, or under an authorized tracing header -- the Collector.
It must come right after Recover: everything below depends on the context it builds.
tracingSecret enables the Collector outside development for requests carrying it in log.TracingHeader. Leave it empty to keep production at zero cost.
recorder is the buffer behind log.ConsolePath. Nil records nothing, which is what production does.
It returns a pipeline.Middleware of http.Handler rather than naming the HTTP layer's alias for the same type, because the HTTP layer sits above this one and importing it from here would be a cycle.
Types ¶
type Background ¶
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 ¶
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 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 ¶
type Migration = migrations.Migration
Migration is a versioned, immutable-once-published schema change.
It is an alias, not a copy: the migration runner lives in the database 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.
//
// It is what `aru routes` prints beside a route, what the route names are
// prefixed with, and what a diagnosis is attributed to. Stable means a
// rename is a new module rather than the same one under another name: the
// route names built from it are in published links and in tests.
Name() string
// Routes registers the module's HTTP routes.
//
// The routes are code rather than a file the framework loads: the router is
// handed in, so nothing is registered on a global and there is nothing to
// reset between tests.
//
// A module with no HTTP surface -- a relay, a scheduler-only module -- still
// implements it and returns having registered nothing. An empty
// implementation is one line, and an optional interface for the absence of
// routes would be a second shape to check for what a no-op already says.
Routes(r *routing.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.
What this interface names is a vertical slice: a directory with its routes, its migrations, its policies and its repository, composed by hand in bootstrap/app.go.
Everything below is optional and independent of Module: a module implements as many of these as it has reasons to, and a module that implements none of them is still a module.
type ReloadTagger ¶
ReloadTagger is what a module implements to supply the development live-reload tag.
Optional, and asked for the same way the renderer is: this package cannot import the view package -- that package imports this one in order to be a module -- so what it needs arrives through an interface declared here and satisfied there. The kernel supplies the address of its own endpoint, because the route is its own, and two constants for one address is how a client and a server come to disagree about it.
type Schedulable ¶
type Schedulable interface {
Schedule() []Task
}
Schedulable is optional: the module declares its scheduled work.
type Scope ¶
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 [auth.SystemGrant] refuses an empty // tenant -- 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 ¶
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 auth.Action
// Run does the work. It gets the Grant built from Action and the tenant.
Run func(ctx context.Context, g auth.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.