Report nightly seat usage to the billing provider
Go · Go · advanced · modification
Moves seat billing from the monthly CSV that finance uploaded by hand to daily metered usage. `errgroup` with `SetLimit(8)` keeps us inside the provider's concurrency limit, every call gets a 10-second timeout, and each org is reported under an idempotency key, so on-call can simply rerun the job after a failure. The summary line lists the failed org ids, and any failure is returned to the scheduler so it pages. Tested against the provider's sandbox with 40 test customers: all 40 reported, and a rerun was deduplicated.
Go 1.22 worker; the module already requires `golang.org/x/sync` (v0.10). `usage.Store` (existing) has `ActiveOrgs(ctx) ([]Org, error)` (`Org` has `ID int64` and `BillingCustomerID string`) and `SeatsUsed(ctx, orgID int64, day time.Time) (int64, error)`; its queries run under the pool's 5-second statement timeout. `deps` in `cmd/worker/main.go` (unchanged) already carries `UsageStore usage.Store` and `Billing *billing.Client`. `billing.Client` (existing) has `ReportUsage(ctx context.Context, customerID string, quantity int64, idempotencyKey string) error` over HTTPS, which returns an error for any non-2xx answer or a cancelled context. `scheduler.Scheduler.Daily(name, at string, fn func(ctx context.Context) error)` runs `fn` once a day at a UTC time, never overlaps two runs of the same job, and pages on-call when `fn` returns an error. About 3,000 active orgs; the provider deduplicates idempotency keys for 30 days. A night the worker was down is backfilled by hand.
Requirements
- A nightly job at 02:00 UTC reports, for every active org, yesterday's (UTC) seat count to the billing provider with `ReportUsage(ctx, org.BillingCustomerID, seats, key)`. The idempotency key is `seats-<orgID>-<YYYY-MM-DD>`, so a rerun never double-bills.
- Orgs are independent. Now and then the provider answers 4xx for a customer that is closed or suspended on its side, and a single call can time out. One org's failure must not affect any other org: every other org is still reported that night.
- Each provider call gives up after 10 seconds. At most 8 calls run at once (the provider's documented limit for our plan).
- When the reporting pass ends, one info-level log line gives the day, how many orgs were reported and the ids of those that failed. If any org failed, the job returns an error so the scheduler pages on-call; failed orgs are retried by rerunning the job, which the idempotency key makes safe.
Files touched
- cmd/worker/jobs.go
- internal/usage/job.go
--- cmd/worker/jobs.go
import (
+ "time"
+
"example.com/seats/internal/scheduler"
+ "example.com/seats/internal/usage"
)
func registerJobs(s *scheduler.Scheduler, d deps) {
s.Daily("sessions-cleanup", "01:00", d.Sessions.Run)
+
+ seats := &usage.Job{Store: d.UsageStore, Billing: d.Billing, Now: time.Now}
+ s.Daily("seat-usage-report", "02:00", seats.Run)
}
--- internal/usage/job.go
+package usage
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "sync"
+ "time"
+
+ "golang.org/x/sync/errgroup"
+)
+
+const (