Ctrl K
ComunidadGuías…

AGENTS/Jobs.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Durable Jobs

BoardOil deliberately runs one API instance and one serial job executor. `BoardOil.Api/Jobs/JobSchedulerService` is the only automatic host. It awaits `IJobRunner.MarkRunningJobsFailedAsync` before either of its loops begins. The scheduling loop evaluates immediately and then every minute; the execution loop claims one job at a time and waits two seconds on an empty queue. Each operation gets a fresh DI scope. Failures are logged and retried after ten seconds. Both loops use the injected `TimeProvider` and share a cancellable lifetime; shutdown awaits both.

`BoardOil.Services/Jobs/ScheduledJobService` owns schedule evaluation. Every schedule has a durable checkpoint and a stable occurrence correlation identity. If enqueueing fails partway through an occurrence, `PendingDueAtUtc` records it for retry after restart. Catch-up creates only the latest missed occurrence and correlation IDs prevent duplicate jobs. All four maintenance definitions use a `TimeOnly DailyTime` and run daily at 03:00 in the configured system timezone. `DailyOccurrenceCalculator` returns the next occurrence or the latest occurrence in an exclusive-baseline/inclusive-end interval using local calendar dates. During a clock-forward gap it uses the first valid local time; during a clock-back overlap it uses the earlier UTC instant once. Daily and Once are the supported schedule kinds. The daily maintenance schedules also supply first-registration cleanup.

Once schedules are immediately due only when their checkpoint has `RunRequested = true`. Migrations can use `RequestOnceSchedule(schedulerStateName)` to create or rearm that checkpoint; multiple requests before startup coalesce. Enqueueing uses the existing `PendingDueAtUtc` and correlation recovery. Only successful enqueueing of every target clears the request; disabled Once schedules retain it. Job execution is tracked independently. Resaving is idempotent; occasional duplicate work is acceptable, and no extra request identities or history-purge protection are added.

`ResaveAllBoardsScheduledJobDefinition` produces one `resave-board` job per board and is not armed automatically by registration. `RequestInitialBoardResave` arms the first pass for fresh installations and upgrades. `BoardResaveService` loops through live cards and calls the same `UpdateCardService.SaveAsync` implementation as ordinary updates, with the current card values. Do not put checklist or other repair rules in the job. The shared save validates and normalises the card and recalculates checklist counts even when its description is unchanged. Unchanged assignees are retained even if inactive or no longer board members; new assignments require active membership. It joins the caller's scope: ordinary updates commit before their notifications; board resaves commit one transaction and publish one board resync afterward. Count-only changes preserve logical card timestamps. Archives and catalogue styles are not rewritten by card saves. Failure rolls back that board; administrators can repeat the whole pass with Run now.

For future board normalisation, update the ordinary save rules, then add a data-only migration calling `migrationBuilder.RequestOnceSchedule("resave-all-boards-checkpoint")`. Keep repair code out of the migration. All pending migrations and bootstrap finish before the host queues work, so upgrades spanning several requests still produce one pass using the current save rules. Rolling a request migration back cannot undo jobs already queued or completed; its `Down` method leaves that runtime state alone.

`BoardOil.Services/Jobs/JobRunner` claims pending work serially and records a Running status and start log before invoking a handler. A handler owns committing its own work. The runner then persists the terminal status and log. If terminal persistence fails, it retries that save without invoking the handler again or claiming another job. On shutdown, an interrupted Running record remains Running; the next startup marks it Failed once with an interruption log. Pending and terminal records are preserved.

Add a new job type through an `IJobHandler` in the service layer, or in the API layer when it depends on API-owned services such as OAuth registration. Register one handler per type. Define a schedule through `IScheduledJobDefinition`, with a stable name and target key for fan-out occurrences. Do not add a second hosted consumer or perform the same cleanup on API startup; admin purge endpoints remain explicit operations.

At the end of each scheduling evaluation pass, `ScheduledJobService` removes checkpoint rows whose names do not belong to any registered `IScheduledJobDefinition`. This also runs after individual schedule evaluation failures. Disabled schedules, inactive Once schedules and failing registered schedules retain their checkpoints. Cancellation stops the pass without forcing cleanup. Jobs and logs remain governed by execution and history retention; checkpoint removal does not delete them.

## Administration

The admin-only history routes are `GET /api/system/jobs` and `GET /api/system/jobs/{id}`. The Jobs page and routed details share the system-jobs store and an admin-only `/hubs/system-jobs` connection. `IJobInvalidations` publishes committed job IDs or a history change; notifier failures must not change a job's persisted outcome or replay its handler.

`systemJobsStore` owns the history page and job-ID keyed details/logs. Private uppercase mutations apply list/detail responses in arrival order. Keep requested pagination so invalidation refreshes target an in-flight page selection. Retain cached logs only when the summary timestamp is unchanged; otherwise clear them. Invalidations clear affected logs and refresh the visible page and affected open detail; reconnect refreshes both. Components set visibility and the open job through store actions. Disposal clears local state, but outstanding responses and feedback may still arrive. Older statuses or missing cached logs until another refresh are accepted; do not add revision counters, timestamp ordering or stale-response retries. Connection lifecycle remains owned by the jobs context.

Schedule administration lives at `/admin/system/jobs/scheduled`. Its transport is `GET /api/system/scheduled-jobs` and `POST /api/system/scheduled-jobs/{name}/runs`. Run now queues the definition's normal occurrences for the serial worker, records the initiating admin user, and returns queued count/IDs without claiming completion or changing automatic checkpoints. Zero occurrences is a successful empty result. A partial enqueue failure may have committed some jobs; never automatically retry a Run now request. Links open the existing job history/detail routes.

The schedules store loads on page entry and refreshes after successful Run now requests. It does not subscribe to job invalidations/reconnect or poll. Its current/latest job summaries are snapshots; job details retain live updates. Per-schedule busy flags prevent duplicate submissions within one page lifetime. List responses apply in arrival order. Disposal clears local state but does not suppress outstanding responses: a late run can still show its result toast and refresh schedules after leaving the tab. These edge cases are accepted; do not add request versions or replacement lifecycle tracking. Run results are reported through toasts rather than retained in unused per-schedule result/error maps. Once schedules show `Once` for timing, `Now` when an enabled request is due, and `Not scheduled` otherwise. Manual runs preserve any automatic request.

`GET /api/system/timezone`, `PUT /api/system/timezone`, and `GET /api/system/timezone/options` expose the global `system_timezone` database setting, defaulting to UTC. Accept only IANA identifiers offered by the timezone picker, such as Europe/London; resolvable aliases absent from that list are rejected. Schedule dates use this zone; persisted timestamps remain UTC. Updates share the scheduling gate and atomically reset evaluation baselines to now. An already-recorded pending occurrence retains its identity for recovery. Existing job execution times and queued work are not rewritten. System > Configuration owns timezone selection through an inline searchable picker. Initial admin setup also offers that picker: anonymous `GET /api/auth/timezone-options` exposes only the supported catalogue and UTC default. Registration accepts `systemTimeZoneId` (UTC when omitted), validates it before mutation, and commits the admin account, timezone and schedule baseline reset together under the scheduling gate. GET/PUT `/api/system/configuration` include `systemTimeZoneId`; Save validates all editable settings before mutation and commits them with any schedule baseline reset in one write scope under the scheduling gate. Unchanged timezone values do not reset baselines. Returning to Scheduled Jobs loads the updated effective timing. The schedule API exposes `kind` as `daily` or `once`; `dailyTime` is a TimeOnly string for Daily and null for Once. The UI renders Daily timing as a local time. The UI does not expose timing/enabled editing. All mutation endpoints retain the browser CSRF policy.

## Port verification

`ScheduledJobApiIntegrationTests` covers schedule/timezone transport, actor attribution, validation and browser authorization/CSRF boundaries. `scheduledJobsStore.test.ts` covers overlap, per-row submissions and disposal. `ConfigurationTimeZoneIntegrationTests` covers combined saves, validation rejection, database rollback and unchanged schedule baselines. The scheduled-jobs browser regression uses disposable data for a real Run now, routed completion/logs and persisted timezone changes. Earlier runtime evidence remains in `JobSchedulerServiceTests.SchedulingContinuesWhileRunnerIsBlockedAndNeverStartsSecondClaim` and `JobHostIntegrationTests.RealHostRunsPendingJobAndRestartRecoversOnlyInterruptedWorkWithoutDuplicateCatchUp`; UI tests do not replace those concurrency/restart checks.