Documentation
¶
Overview ¶
Package internal is every implementation of the task module. Nothing outside modules/task can import it, which is the compiler enforcing idea 3: a consumer takes contracts.Service, and taking anything else does not build.
Index ¶
- func BreachOnArrival(svc contracts.Service) func(context.Context, db.Tx[db.Tenant], *contracts.Task) error
- func RegisterRoutes(api *httpx.API, spec rest.Spec[*contracts.Task], svc contracts.Service)
- func SLASweep(tenants jobs.TenantLister, svc contracts.Service, every time.Duration) jobs.Job
- type Service
- func (s *Service) Assign(ctx context.Context, tx db.Tx[db.Tenant], id, assignee uuid.UUID) (*contracts.Task, error)
- func (s *Service) CheckSLA(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID) (*contracts.Task, error)
- func (s *Service) Resolve(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID, resolution string) (*contracts.Task, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BreachOnArrival ¶
func BreachOnArrival(svc contracts.Service) func(context.Context, db.Tx[db.Tenant], *contracts.Task) error
BreachOnArrival is the create route's hook: a task whose deadline has already passed — an integration replaying yesterday's alarms, a form filled in late — is breached now rather than within a minute of the next sweep.
It runs inside the request's transaction, after the row and its created event, so an error here rolls the create back. That is the property that makes a hook worth having over a subscriber, which could only ever run after the create had already committed. The event it publishes is declared by the Spec's HookEvents, because nothing reads a hook to find out what it emits.
func RegisterRoutes ¶
RegisterRoutes mounts the three lifecycle commands on the same resource the Spec mounts the five CRUD routes on.
They are routes rather than fields of a PATCH because each is a rule about the state the task is in and each publishes an event: a caller who could write status="resolved" through the generic update would close the loop with no resolution time and tell nobody. spec.Immutable is the other half of that argument — it refuses those fields at the PATCH — and rest.Command is the part all three commands share, so what is written here is only what each command is.
func SLASweep ¶
SLASweep is the module's periodic work: the one thing an outbox cannot express, because a deadline passing is not something that happened to anybody (docs/adr/0004). Every tick, in every tenant, every task whose deadline has gone by unresolved is handed to CheckSLA.
One instance in the cluster runs it per tick — kit/jobs takes an advisory lock named after the job — and each task gets its own transaction.
Types ¶
type Service ¶
type Service struct{}
Service is the task lifecycle. It has no fields: everything a command needs arrives with the transaction it is given, which is what lets one instance serve a request, a job and an event handler at once, and what makes the whole module constructible with no dependency graph at all.
func NewService ¶
func NewService() *Service
NewService returns the lifecycle commands. It takes nothing, on purpose: see the type. module.go constructs it.
func (*Service) Assign ¶
func (s *Service) Assign(ctx context.Context, tx db.Tx[db.Tenant], id, assignee uuid.UUID) (*contracts.Task, error)
Assign makes assignee responsible, and acknowledges the task if nobody had taken it. The same person twice is the same task and no second event: a retried click must not appear in a workload dashboard twice.
func (*Service) CheckSLA ¶
func (s *Service) CheckSLA(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID) (*contracts.Task, error)
CheckSLA records a breach, once. The sweep calls it every minute for every task whose deadline has passed, so "once" is the whole contract: the stored flag is read and written in one transaction, and the row lock the update takes makes two sweeps racing on one task one breach and one event.
func (*Service) Resolve ¶
func (s *Service) Resolve(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID, resolution string) (*contracts.Task, error)
Resolve closes the loop. Repeating it with the resolution already recorded, or with none, changes nothing; a different one on a resolved task is a conflict rather than an overwrite, because the account a task gives of itself is the auditable part and a retry is not a correction.