tako docs guide reference api
GitHub

singleton.md — tako.Singleton

An exactly-one loop, everywhere: one instance runs the body across all instances and all revisions, with clean handoff between deploys. The machinery is min=1/max=1 plus the §4.4 lease — the loop executes only while holding the lease, so even mid-deploy (two revisions briefly live) exactly one instance works. min=1/max=1 alone is NOT the guarantee; the lease is.

Use it for genuinely SERIAL work. Shop's invoicer assigns bookkeeping-grade invoice numbers — strictly sequential, no gaps, no duplicates — which is what makes it a Singleton and not a worker.

Declare

From examples/shop/app.go:

Invoicer = tako.Singleton(assignInvoices)

Invoicer is a tako.Handler — what SingletonDown names when the loop stops running.

Singleton takes only its loop: the declaring variable is the identity — Invoicer deploys as shop-invoicer — which is the same rule that names every other service tako creates. Singleton accepts a tako.SingletonConfig whose Footprint honors only CPU and Memory fields — the scaling knobs (min=1/max=1/concurrency=1) are the exactly-one machinery and are framework-owned.

Use

The body is func(Ctx) error — a loop that watches ctx.Done() (a lost lease cancels it) and returns to be restarted:

// assignInvoices is the singleton loop: sweep un-invoiced orders into
// strictly sequential numbers, forever, exactly once across every
// instance and revision (the lease is the guarantee).
func assignInvoices(ctx tako.Ctx) error {
	for {
		if _, err := invoiceSweep(ctx); err != nil {
			return err
		}
		select {
		case <-ctx.Done():
			return nil
		case <-time.After(2 * time.Second):
		}
	}
}

Returning — error or not — is a restart with a 5s backoff, narrated: ♾ singleton invoicer returned — restarting in 5s.

Local

Under tako dev the loop is a goroutine in the app process — the only instance — and the in-process lease still guards against doubled registration. The loop is invisible by design when idle; a productive sweep logs, and that line in the dev console IS the singleton working: invoicer: assigned invoice numbers orders=3 through=3.

Cloud

tako deploy creates a dedicated service (shop: shop-invoicer) with the singleton knobs — CPU 1, 512Mi, concurrency 1, min 1 / max 1 — that admits nobody's traffic but health checks. Its generated main runs RunSingletonLoop: campaign forever for the singleton-<name> lease (an object in the framework bucket, the same §4.4 mechanism the deploy lease uses), run the body while holding it, restart with the 5s backoff when it returns, resume campaigning when the lease is lost. Handoff needs no cleanup — a crashed holder's lease TTL (30s) expires and the next campaigner takes over.

Cost-honest: min=1 means one instance always on, billing continuously whether there is work or not. That standing instance is the price of exactly-one.

Test it

Factor one iteration out of the loop and test it directly — shop's invoiceSweep pattern (examples/shop/api_test.go, TestInvoiceNumbersAreStrictlySequential):

ctx := runtime.NewCtx(t.Context())
n, err := invoiceSweep(ctx)
if err != nil || n != 3 {
	t.Fatalf("sweep = %d, %v", n, err)
}

A second sweep finds nothing: numbering is idempotent and the sequence never rewinds.

Errors you'll meet

Not yet