Documentation
¶
Overview ¶
Package foundation boots the application.
It is the single place where an application is composed. One difference matters: the Application is built ONCE, at process start, not per request, so nothing here may assume request scope.
Where Application came from ¶
Application was kernel.Kernel until this package landed, and github.com/arandu-io/framework/kernel is now the old name pointing here. Nothing in this file has a hesape/foundation counterpart.
What is aliased and what is declared ¶
The composition vocabulary is github.com/arandu-io/hesape/foundation and is aliased, not restated: Bootable, Background, Closable, Diagnostic, Health, Schedulable, Migratable, Scope, Global, PerTenant, Task, Migration and ReloadTagger are the hesape types under the same names. A module written against either name is one type to the compiler.
Three names are declared here, each for a reason recorded on the declaration in module.go:
Module names a *http.Router, and hesape/foundation.Module names
a *routing.Router -- http.Router is the one envelope the
request bridge could not make an alias
RendererProvider hesape/foundation has none, deliberately: it names the
boot sequence that looks for it, and that sequence is here
Locker it is retired, and the events bridge kept it --
hesape/cache.Locks cannot be built from what
github.com/arandu-io/kv implements
FormatRoutes is neither: it is a call through to hesape/routing.FormatRoutes, which the alias on http.Route makes possible without translating anything.
There is no list of built-in commands ¶
This package answers no Builtins() []console.Command -- the set a project's own binary would dispatch without the aru CLI. Of the eleven commands a project dispatches today, only serve, routes and Version are answerable from an Application at all; the rest need a *data.DB it does not hold, project code it does not know, or a queue store from a module that imports this one. Letting them register themselves would be a container under another name, and there is no container.
What is written instead is the switch in the skeleton's bootstrap/console.go. It is in the project's own repository, the whole set reads at once, and adding one is writing a case.
Index ¶
- Constants
- func FormatRoutes(routes []*fhttp.Route) string
- type Application
- func (a *Application) Boot(ctx context.Context) error
- func (a *Application) Config() bootstrap.Configuration
- func (a *Application) Diagnose(ctx context.Context) []string
- func (a *Application) Handler() http.Handler
- func (a *Application) Logger() *slog.Logger
- func (a *Application) Migrations() []Migration
- func (a *Application) Recorder() *observability.Recorder
- func (a *Application) Register(mods ...Module) *Application
- func (a *Application) Routes() []*fhttp.Route
- func (a *Application) Run(ctx context.Context) error
- func (a *Application) Shutdown() error
- func (a *Application) Tasks() []Task
- func (a *Application) Use(mw ...fhttp.Middleware) *Application
- type Background
- type Bootable
- type Closable
- type Diagnostic
- type Health
- type Locker
- type Migratable
- type Migration
- type Module
- type ReloadTagger
- type RendererProvider
- type Schedulable
- type Scope
- type Task
Constants ¶
const ( // Global runs the task once for the whole instance. // // It gets the zero Grant, because 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 = hfoundation.Global // PerTenant expands the task to every active tenant, each with its own // Grant and its own lock. PerTenant = hfoundation.PerTenant )
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.
The table itself is hesape/routing.FormatRoutes, and this is a call through rather than a copy: fhttp.Route is an alias for routing.Route, so the slice needs no translation and there is one implementation of the format. A wrapper and not an alias, because Go has no alias form for a function.
Types ¶
type Application ¶
type Application struct {
// contains filtered or unexported fields
}
Application holds the composed application: configuration, modules, the global middleware pipeline and the router.
func New ¶
func New(cfg bootstrap.Configuration) *Application
New assembles the Application. It opens no connection and listens on no port -- that is Boot and Run.
func (*Application) Boot ¶
func (a *Application) Boot(ctx context.Context) error
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 (*Application) Config ¶
func (a *Application) Config() bootstrap.Configuration
Config returns the configuration the Application was built with.
func (*Application) Diagnose ¶
func (a *Application) Diagnose(ctx context.Context) []string
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 (*Application) Handler ¶
func (a *Application) Handler() http.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 (*Application) Logger ¶
func (a *Application) Logger() *slog.Logger
Logger returns the root logger. Request handlers must use observability.Log(ctx) instead, which carries the request id.
func (*Application) Migrations ¶
func (a *Application) Migrations() []Migration
Migrations collects the migrations of every module, in registration order. Hand the result to migrations.Migrator.RunPending.
This is the module half of migration discovery, and migrations.Register is the other half: a module is a value the Application already holds, so it is asked; an application's own migrations live in a package nothing calls, so they announce themselves from init(). What the two must never both hold is one migration -- the registry orders by name and this orders by registration, so a migration in both has two positions and no rule that says which one wins.
func (*Application) Recorder ¶
func (a *Application) 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:
app.Use(middleware.Observe(dev, cfg.Observability.TracingSecret, app.Recorder()))
w := jobs.NewWorker(store, jobs.WorkerOptions{Recorder: app.Recorder()})
scheduler.NewModule(app.Tasks(), scheduler.Options{Recorder: app.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.
It is not wired automatically because the pipeline is assembled in the project, in the open, and a middleware that reached back into the Application for state would be the kind of hidden coupling the explicit wiring exists to avoid.
func (*Application) Register ¶
func (a *Application) Register(mods ...Module) *Application
Register adds modules in the order they will be booted. Order matters: a module may depend on another one already being up.
func (*Application) Routes ¶
func (a *Application) Routes() []*fhttp.Route
Routes returns the registered routes. It is empty before Boot, because a module only registers its routes when it boots.
func (*Application) Run ¶
func (a *Application) Run(ctx context.Context) error
Run starts the server and blocks until SIGINT or SIGTERM, then shuts down gracefully.
func (*Application) Shutdown ¶
func (a *Application) Shutdown() error
Shutdown stops the server and closes the modules in reverse registration order, which is the only order that respects dependencies between them.
func (*Application) Tasks ¶
func (a *Application) Tasks() []Task
Tasks collects the scheduled work from every registered module, in registration order.
Same shape as Migrations(): the module declares, the Application collects, and the scheduler module runs. Pass it to scheduler.NewModule, which is why that one is registered last.
func (*Application) Use ¶
func (a *Application) Use(mw ...fhttp.Middleware) *Application
Use adds global middleware to the pipeline. The pipeline order is the order of execution on the way in, and its reverse on the way out.
type Background ¶
type Background = hfoundation.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.
The alias drops one thing this interface used to say: it embedded Module, and hesape/foundation.Background declares Start alone. Nothing changes at a call site -- Register takes a Module, so anything the Application asks for a loop is already one -- and the embed was a second statement of a fact the registration already enforced.
type Bootable ¶
type Bootable = hfoundation.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 Closable ¶
type Closable = hfoundation.Closable
Closable is optional: implement it to release resources on shutdown.
type Diagnostic ¶
type Diagnostic = hfoundation.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.
type Health ¶
type Health = hfoundation.Health
Health is optional and feeds `aru doctor` and the /_arandu/health endpoint.
type Locker ¶
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.
Why it is still here ¶
It is retired in favour of one kind of lock in the collection, cache.Lock. The events bridge did not delete it, because hesape/cache.Locks is a concrete issuer over a store that acquires and releases by owner, and a Locker -- which only knows how to run a function under a lock it takes and gives back itself -- cannot be turned into one. events.Locker is an alias to this name and kv asserts against it, so deleting the declaration deletes the wiring rather than the duplication.
It moved with the rest of the package instead, and stays one declaration.
type Migratable ¶
type Migratable = hfoundation.Migratable
Migratable is optional: the module declares its migrations, and the Application collects them from every registered module in registration order.
type Migration ¶
type Migration = hfoundation.Migration
Migration is a versioned, immutable-once-published schema change.
It is an alias, not a copy: the migration runner lives in hesape, and a module must be able to hand its migrations straight to it. Both sides resolve to hesape/database/migrations.Migration, which is what data.Migration already aliases.
It is an interface -- embed migrations.BaseMigration and only GetName and Up are left to write.
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 *http.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.
Why this one is declared and not aliased ¶
hesape/foundation.Module names a *routing.Router, and this one names a *http.Router. Those are two types: http.Router is the one envelope the request bridge could not turn into an alias -- it carries the renderer and the flash, and hesape/routing.Router deliberately carries neither -- so the two interfaces cannot be the same interface while that envelope exists.
Aliasing this name to the hesape one would change the signature every module in every project is written against, in the two repositories this framework ships and in every application built on it. A bridge that changes a signature is not a bridge. It becomes an alias the day http.Router stops being an envelope, and not before.
type ReloadTagger ¶
type ReloadTagger = hfoundation.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 there and satisfied here. The Application 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 RendererProvider ¶
RendererProvider is optional: the module supplies the view renderer.
The view package implements it. This package cannot import that package -- it implements 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 Application asks every module whether it brings a renderer, before any route is registered.
Two modules providing one is a wiring mistake, and the Application refuses to boot rather than pick one.
Why this one is declared and not aliased ¶
hesape/foundation has no counterpart: its doc says so in as many words, and the reason is that declaring one from up there would be a second definition of the boot sequence that looks for it. The renderer type is not the divergence -- http.Renderer is an alias for hesape/http.Renderer, which is what hesape/view.Module.Renderer already returns, so that module satisfies this interface structurally and without importing this package.
type Schedulable ¶
type Schedulable = hfoundation.Schedulable
Schedulable is optional: the module declares its scheduled work.
type Task ¶
type Task = hfoundation.Task
Task is scheduled work.
The shape mirrors Migrations(): the module declares, the Application collects, and nothing runs until something asks. What a module never does is start its own goroutine.
The alias holds because Task.Action and Task.Run name security.Action and security.Grant, and both of those are already aliases for the hesape/auth types the hesape declaration uses.