feat(jobs/taskspec): TaskSpec registry for asynq tasks — closes #258#355
Merged
Conversation
…closes #258 Consolidates task name, queue, retry policy, payload schema, and handler into one TaskSpec value. Producers (Enqueue) and consumers (Dispatch) dereference the same struct, so drift between them is structural rather than coordinated. - taskspec.TaskSpec — declarative descriptor with Name/Queue/MaxRetry/ Timeout/PayloadSchema/Handler. Handler signature is (ctx, []byte) so asynq does not leak into every package that declares a spec. - taskspec.Registry — concurrent-safe, first-writer-wins on duplicate Name. NewRegistry for tests; Default() singleton for production. - taskspec.Enqueue — looks up spec, validates payload against the pinned 2020-12 schema (via packages/go/jsonschemautil), and applies Queue/MaxRetry/Timeout via asynq.EnqueueContext. Typed errors: ErrNilClient, ErrUnknownTask, ErrInvalidPayload. - taskspec.Dispatch — walks the registry and wires each Handler onto an asynq.ServeMux under the spec's Name. Skips nil-Handler specs rather than panic. Tests cover register/get round-trip, duplicate rejection, sorted Names, nil-client guard, schema-validation gate, []byte passthrough (webhook delivery relies on signature stability), nil-schema bypass, mux wiring of multiple handlers, nil-Handler skip, error propagation, and the race- clean concurrent Register + Names workout. All 22 tests pass under go test -race -count=1. Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
9 tasks
tayebmokni
added a commit
that referenced
this pull request
May 18, 2026
## Summary Adds `packages/go/jobs/cron` — the leader-elected cron scheduler that completes the picture started by the asynq chassis (#362) and the taskspec registry (#355). Exactly one worker fires each scheduled task across a multi-replica deployment; failover happens within one lease TTL of leader death. Four files own the surface: - **cron.go** — `CronSpec{Name, Schedule, TaskName, Payload}` + `Registry`. First-writer-wins. The Schedule string is parsed via `robfig/cron/v3` at Register time so a typo fails at boot, not at first fire. - **lease.go** — `Lease{Key, Owner, TTL}`. `Acquire` is `SET NX PX`. `Renew` and `Release` are Lua compare-and-swap scripts keyed on `Owner` so a stale process cannot wipe a fresh leader's claim — the TOCTOU window between a naive GET + EXPIRE/DEL would re-enable that bug. - **scheduler.go** — Run-loop. Try `Acquire`; if leader, fire any due CronSpec via `taskspec.Enqueue` and `Renew` every `TTL/3`; on shutdown, `Release`. Non-leaders idle-poll every `TTL/2` with +-25% jitter to break the thundering herd. - **seed.go** — `SeedDefaults` registers the canonical `revisions.purge` daily 03:00 entry so the system has at least one cron in flight on boot. Follow-up issues add the rest of the §8.2 catalog. ## Test plan - [x] Single instance: acquires lease, fires scheduled task on tick. - [x] Two instances: only one fires (verified via per-scheduler `FireCount`). - [x] Leader dies without releasing -> follower acquires within `TTL + jitter`. - [x] Bad cron expression rejected at `Register` time (typed `ErrInvalidSchedule`). - [x] Shutdown via `ctx` cancel releases the lease (`CurrentOwner` returns `redis.Nil`). - [x] Race test: 8 schedulers contending — cumulative fires bounded by what a single instance would produce; no double-fire across lease transitions. - [x] Lease CAS guards: wrong-owner `Renew` and `Release` return `ErrNotLeader`. - [x] `go vet ./jobs/cron/...` clean. - [x] `go test -race -count=1 ./jobs/cron/...` passes (also clean across `-count=3`). Resolves #270. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
4 tasks
tayebmokni
added a commit
that referenced
this pull request
May 18, 2026
## Summary - Introduces `packages/go/plugins/abi/jobs` — the bridge between a plugin's manifest `jobs[]` declarations and the host TaskSpec + Asynq dispatch chassis (#355, #362). Mirrors the hooks bridge (#350, #364) for queued background jobs. - Defines the `gn_handle_job(name_ptr, name_len, payload_ptr, payload_len) -> i64` ABI with the same packed-i64 return shape as `gn_handle_hook`, so plugin SDKs share most of their host-call plumbing. - For each `manifest.Jobs` entry, registers a `taskspec.TaskSpec` routed to the `plugin` queue, with the asynq Task ID threaded through the envelope as an idempotency key (#363). Capability-gated on `jobs.enqueue` via the plugin capabilities Checker (#356, #344) — manifests declaring jobs without the cap are rejected before any TaskSpec installs. ## Test plan - [x] `go vet ./plugins/abi/jobs/...` clean. - [x] `go test -race -count=1 ./plugins/abi/jobs/...` — 22 cases pass: pack/unpack round-trip, envelope marshal shapes, happy-path/unknown/trap/OOM/oversize, manifest registration with correct queue + MaxRetry, capability denial leaves the registry untouched, trap returns a retryable error (no `asynq.SkipRetry`), hot-reload across two fresh registries, duplicate job surfaces `taskspec.ErrAlreadyRegistered`, and 100 concurrent task pickups for the same plugin. - [x] Full `./plugins/... ./jobs/...` test suite stays green. - [x] DCO sign-off present. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
tayebmokni
pushed a commit
that referenced
this pull request
May 19, 2026
…355 Operators tweak palette, typography, layout, and spacing through the admin without editing theme.json. Overrides land in the options table under theme_mods.<slug>; the renderer merges them at request time. Backend (apps/api/internal/admin/customizer): - GET /api/v1/admin/customizer/active returns the active theme manifest plus persisted overrides. - PUT /api/v1/admin/customizer/active validates a partial override by deep-merging onto the manifest and running the install-time theme validator; rejects unknown paths via DisallowUnknownFields. - DELETE clears the overrides (Reset; idempotent). - Gated by new theme.customize capability (admin + super_admin). Admin UI (apps/admin/src/app/appearance/customizer): - Live preview iframe driven by ?customizer=preview&overrides=<b64>. - ColorPicker (per palette entry), TypographySection (families + sizes), LayoutSection (content/wide), SpacingSection (scale). - Save submits the diff; Reset deletes overrides. Tests (backend + admin): - go test -race ./internal/admin/customizer/... — handler GET/PUT/ DELETE, validation, 401, 403, store round-trip, merge semantics. - pnpm vitest run src/app/appearance + Sidebar — CustomizerClient save/reset, preview URL, state diff, JSON-pointer paths. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com>
tayebmokni
added a commit
that referenced
this pull request
May 19, 2026
…355 (#411) ## Summary - Backend `apps/api/internal/admin/customizer`: `GET/PUT/DELETE /api/v1/admin/customizer/active`. Overrides land in the options table under `theme_mods.<slug>`. PUT validates the override by deep-merging onto the active manifest and running the install-time `theme.Validate` validator; unknown paths are rejected via `json.DisallowUnknownFields`. New `theme.customize` capability gates all routes (granted to admin + super_admin). - Admin UI `apps/admin/src/app/appearance/customizer`: live preview iframe driven by `?customizer=preview&overrides=<base64>`. ColorPicker (palette), TypographySection (families + sizes), LayoutSection (content/wide), SpacingSection (scale). Save commits the diff, Reset deletes. - Wired into `apps/api/cmd/server/main.go` (mounted next to `rum.Mount`) and the admin sidebar. ## Test plan - [x] `cd apps/api && go test -race -count=1 ./internal/admin/customizer/...` — passes (handler GET/PUT/DELETE, 401/403, merge, store, validation). - [x] `cd apps/api && go test -race -count=1 ./...` — entire api module green. - [x] `pnpm --filter @gonext/admin typecheck` — clean. - [x] `pnpm --filter @gonext/admin vitest run` — 22 files / 177 tests pass, including new `state.test.ts` (12) and `CustomizerClient.test.tsx` (7). - [ ] Manual smoke test against a running stack — load `/appearance/customizer`, tweak a color, Save → reload → palette persists; Reset → palette reverts to theme defaults. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Mohamed Tayeb Mokni <tayeb.mokni@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
packages/go/jobs/taskspec/, a single-source-of-truth declaration of every asynq task type (name, queue, retry policy, timeout, payload schema, handler).taskspec.Enqueue(validates payload against the pinned draft-2020-12 schema viajsonschemautil, applies Queue/MaxRetry/Timeout toasynq.EnqueueContext); consumers calltaskspec.Dispatch(walks the registry and wires eachHandleronto anasynq.ServeMux).packages/go/policyandpackages/go/plugins/capabilities: process-wideDefault()singleton, first-writer-wins on duplicate name, race-cleanRegister/Get/Names, sorted output.(ctx, payload []byte)so the asynq dependency does not leak into every package that declares a spec — the adapter unwrapping*asynq.Tasklives only indispatch.go.Test plan
go vet ./jobs/taskspec/...go test -race -count=1 ./jobs/taskspec/...— 22 tests, all passErrNilClient), unknown name (typedErrUnknownTask), schema-violating payload (typedErrInvalidPayload)asynq.ServeMux; nil handlers are skipped not panicked; handler errors propagate throughProcessTask