mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-02 16:58:52 +01:00
## Thinking Path > - Paperclip manages agents and plugin work. > - Plugins can declare cron jobs with saved deadlines. > - The existing scheduler queries for due jobs every 30 seconds, even when none exist. > - Empty queries keep an otherwise idle database active. > - This PR uses the shared scheduler and committed job changes to discover work. > - Existing job and run rows preserve deadlines and execution history. ## Linked Issues or Issue Description **What happened?** The plugin scheduler on the base of this stack runs empty periodic SQL. Its registration path also advances overdue timestamps. An empty replacement manifest can leave removed schedules active. **Expected behavior** No recurring SQL when no eligible jobs exist. Configured jobs wait for their saved deadline. Changes reactivate scheduling after commit. Work remains protected through drain and shutdown. **Steps to reproduce** Start the plugin scheduler with no jobs. After startup, observe queries during a three-minute idle window. The new PostgreSQL regression failed on the baseline and passes with this change. **Paperclip version or commit** Stacked on #17510 at `4d15c08428`. Deployment: source checkout, local PostgreSQL, controlled plugin RPC. Related: #7905 covers empty-manifest sync; this migration requires that behavior on both activation paths. #11571 proposes company-scoped jobs; this PR preserves the existing instance-scoped job contract. ## What Changed - Install the concurrency test barrier before due job notifications can dispatch. - Make dispatch the sole reservation owner and preserve retained or concurrently committed results when unregistering a plugin. - Replace the private interval with the existing shared deadline worker. - Signal job definition and run changes inside their owning transactions. No new tables. - Wake from plugin registration and worker status events. Retain explicit scheduler wakes received during drain. - Preserve overdue work and initialize only missing or changed schedules. - Recheck claims under a job-row lock. Keep plugin calls outside transactions and refill capacity on completion. - Commit completion and schedule advancement together. Retry failed persistence without repeating the plugin side effect. - Track admitted calls through shutdown and synchronize empty manifests. ## Verification This PR now targets master after #17510 merged as `4e27a82a4265c01f3ab4f05aaecc010a0026de75`. The normal landed-parent merge preserves the entire verified `df78ca3b5e` tree byte for byte; only final publication docs change afterwards. The PR still differs in its original seventeen paths and has no database schema or migration change. Primary test revision: `36be0bd73363680d7666d94ff5f1dd28b27f4235`, on parent `ba80060407`. Final integration of parent `51668b8240` preserves all plugin/runtime/shutdown sources from that proof and passes all 63 cases in complete plugin, approval-seeding and scoped-deadline suites at `56ce7c1d2a`, plus server types/build. Only six parent seeding/doc files changed; this is not a rerun of all eleven suites. Only the original seventeen plugin-scheduling paths differ from that parent; all other tracked files, including migration history, match it. - **247 cases pass in eleven complete suites:** plugin scheduler, shutdown, warm standby, plugin environment-driver readiness, delivery coordinator, both retention workers, approval seeding, decision expiry, scoped deadlines and startup. Server typecheck and production build pass. - The new real PostgreSQL lock test failed before the repair: shutdown remained pending while the completion update waited for a held run row. After repair, shutdown reports the statement timeout while the lock remains held; retry after release commits the saved result with exactly one plugin invocation. All 34 scheduler cases pass. - Existing regressions preserve real committed schedule changes, saved deadlines, manual/scheduled reservation, retained success and failure results during unregister, completion/unregister lock order, independent progress, rollback/outer commit, quiet configured states, drain/resume and shutdown cleanup. - The parent's complete seeding/producer run passed 522 cases at its stated revision. Earlier duplicate reservations, overwritten retained results, hourly-clock fixture failures and combined-stack CI/setup failures remain recorded in `doc/idle-plugin-jobs.md`, its dated audit and the stack ledger; they are not erased or recounted as this candidate's tests. - The completion timeout is transaction-local and defaults to thirty seconds per PostgreSQL statement. It does not bound every shutdown/network operation or make plugin side effects safe to replay. No full local repository-suite or deployed host-sleep claim is made. Commands: server Vitest for the eleven suites above; server typecheck/build. Fresh exact-head CI, Greptile and Ponytail reviews remain required before merge. ## Risks - This uses process-local notifications. Independent SQL writers and other replicas need their own wake integration. - A run left unresolved after process loss is not automatically replayed: the external effect may have occurred. Existing lifecycle/operator reconciliation must resolve it. Live-process settlement failures retry without replay. - Job definitions remain instance-scoped. This PR does not implement company-scoped job dispatch. - Cloud's plugin sleep blockers remain. Internal deadlines do not add durable external wake coverage or prove deployed sleep. - The shared deadline helper now retains direct lifecycle wakes through drain; existing coordinator and decision deadline consumers are tested. ## Model Used OpenAI Codex, GPT-6-based agent, with repository tools, code execution, and GitHub access. The exact serving model version and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
204 lines
12 KiB
Markdown
204 lines
12 KiB
Markdown
# Plugin job scheduling without idle polling
|
|
|
|
Plugin cron scheduling uses the instance delivery coordinator. It no longer
|
|
queries `plugin_jobs` every 30 seconds when there is no eligible work.
|
|
|
|
## Ownership and durable state
|
|
|
|
The plugin scheduler owns cron interpretation, dispatch eligibility, concurrency,
|
|
and run settlement. `plugin_jobs` stores schedules and their next deadlines;
|
|
`plugin_job_runs` records admitted calls and their outcomes. No table or migration
|
|
is added. The application passes its existing coordinator to `scheduler.start`.
|
|
The coordinator owns timer execution, transaction uncertainty, and drain gating.
|
|
|
|
The job store and registry signal before changes inside their transactions.
|
|
Definition sync, pause/resume, changed schedules, run creation/settlement, job
|
|
removal, and plugin removal invalidate the scheduler. Nested transactions wake
|
|
only after their outer transaction settles. An empty manifest is a real sync:
|
|
both the loader and lifecycle coordinator pause removed definitions.
|
|
Registration and worker status changes provide local lifecycle wakes. The shared
|
|
`registerDeadlineWork` helper retains explicit wakes received during drain,
|
|
including a worker becoming available without a database mutation.
|
|
|
|
## Idle and dispatch behavior
|
|
|
|
- Empty instances, manual-only jobs, invalid cron definitions, paused jobs, and
|
|
unavailable workers have no recurring scheduler SQL after reconciliation.
|
|
- Configured future jobs arm their saved next deadline. No SQL is needed between
|
|
reconciliation and that deadline unless a supported producer changes state.
|
|
- Startup reconstructs deadlines. An overdue saved timestamp is owed work and
|
|
is not replaced merely because the plugin registered again. A missed schedule
|
|
dispatches once, then computes its next cron time after completion.
|
|
- A missing timestamp is initialized from the current cron expression. A changed
|
|
cron clears the old timestamp so the new schedule can be initialized. Registry
|
|
updates lock the job before reading its current deadline, so a concurrent
|
|
completion cannot have its newly advanced deadline replaced by a stale value.
|
|
- Claims recheck the definition under a job-row lock and reject outstanding
|
|
queued, pending, or running records. Manual and scheduled dispatch share this
|
|
boundary. Worker RPC calls run outside transactions.
|
|
- Active calls consume concurrency slots and remain tracked until settlement.
|
|
Their completion requests a coalesced refresh for waiting work. Slow calls do not
|
|
prevent other available slots from being used.
|
|
- Completion and the scheduled job's next timestamp commit together. Failed
|
|
persistence retries the retained settlement, not the plugin side effect.
|
|
A concurrent schedule edit is preserved by the settlement check. Failed
|
|
settlements do not prevent independent jobs from using available capacity.
|
|
The failed job stays excluded from admission until its exact result is saved.
|
|
- Stop rejects new admissions, detaches worker subscriptions, and waits for
|
|
admitted work, then flushes retained completion results without another RPC.
|
|
If persistence still fails, stop rejects and retains the result for another
|
|
attempt; it does not report successful shutdown. Existing calls retain their
|
|
RPC timeout. A failed stop still allows application and plugin-worker cleanup;
|
|
teardown then retries the retained settlement once before reporting any remaining
|
|
error, while the database pool is open.
|
|
|
|
Producer and lifecycle wakes share a 30-second minimum refresh spacing, matching
|
|
the former scan budget. Sequential run-status writes coalesce instead of each
|
|
causing a full inventory scan. This is a bound on requested refreshes, not a
|
|
recurring timer: settled workers without a future obligation remain disarmed.
|
|
Manual dispatch still uses its direct claim path. New schedules and completion-
|
|
released capacity can wait up to that spacing before scheduled dispatch.
|
|
|
|
## Limits
|
|
|
|
This is the existing single-process writer model. Independent SQL writers and
|
|
other app replicas do not emit these local signals. The row lock prevents an
|
|
overlapping claim, but does not establish a supported multi-replica wake protocol.
|
|
|
|
A process crash after a plugin call but before durable completion leaves an
|
|
unresolved run. The scheduler does not assume that an unknown external effect is
|
|
safe to repeat. Such a run remains outstanding until explicit lifecycle/operator
|
|
reconciliation. During a live process, failed completion writes retain their
|
|
exact result and retry. This does not invent exactly-once external execution.
|
|
|
|
Plugin definitions remain instance-scoped, as on the base branch. The separate
|
|
company-scoped job proposal is not implemented here. Plugin configuration and
|
|
worker authorization retain their existing owners.
|
|
|
|
The Cloud sleep proof remains conservative for plugin work. An internal cron
|
|
deadline cannot wake a stopped host; this PR does not add external alarm coverage,
|
|
allow configured plugins to bypass idle checks, or claim measured host sleep.
|
|
|
|
## Verification
|
|
|
|
`plugin-scheduled-work.test.ts` uses real PostgreSQL and controlled worker calls.
|
|
It checks idle SQL silence with no jobs and configured future/manual-only jobs;
|
|
first writes, pause/resume, worker availability, outer commit and rollback;
|
|
restart and overdue execution; removed schedules; concurrent claims; failed
|
|
completion persistence without repeated RPC; capacity refill; drain; and shutdown.
|
|
Worker responses are simulated. Existing startup, authorization, loader, shared
|
|
coordinator, decision-expiry, and warm-standby tests cover affected consumers.
|
|
|
|
## Review repair evidence
|
|
|
|
Real PostgreSQL regressions reproduced three failures: shutdown returned with a
|
|
known completed call still marked running; one unwritable completion prevented an
|
|
independent job from starting; and a concurrent unchanged registry update restored
|
|
an old deadline. All three pass after the repair. The shutdown test initially
|
|
expected an unresolved-work hold to disappear, which was incorrect. Its corrected
|
|
baseline stops the coordinator, then observes the actual running-row failure.
|
|
A further case verifies that persistent shutdown-write failure is reported and
|
|
a later stop attempt saves the retained result without repeating the RPC.
|
|
|
|
A permanent database outage followed by forced process termination still has the
|
|
process-loss limitation above. This change does not claim durable storage for an
|
|
external effect that the database has never acknowledged.
|
|
|
|
The final focused run passes 62 cases across plugin scheduling, loader recovery,
|
|
warm standby, and the delivery coordinator. Server typechecking passes. Remote
|
|
checks and a fresh review remain required for the repaired head.
|
|
|
|
|
|
A second review reproduced 16 full inventory reads during fifteen sequential run
|
|
lifecycles on forty manual jobs. Restoring the prior 30-second spacing bounds that
|
|
burst to one read, then reconciliation returns to zero SQL. Shutdown regressions
|
|
also reproduced skipped cleanup after a settlement error. The shared teardown
|
|
boundary now always attempts remaining cleanup and retries a failed stop once;
|
|
both application teardown and plugin-loader shutdown use it. A PostgreSQL trigger
|
|
case verifies the retained result is saved after cleanup removes the write fault,
|
|
without a second worker RPC. Persistent failures remain visible.
|
|
|
|
The first expanded timing run failed fifteen assertions because the test assumed
|
|
one second was sufficient to settle a coalesced wake. Tests now wait for the actual
|
|
null/future deadline before measuring three minutes of SQL silence. The corrected
|
|
run passed all 75 cases; the additional post-cleanup retry case is verified separately
|
|
below. No production timeout or quiet assertion was relaxed.
|
|
|
|
The post-cleanup retry run passes all 48 shutdown/plugin cases (28 plugin and
|
|
20 shutdown). Together with the unchanged consumer selection this covers 76
|
|
unique cases. Server typecheck and build pass on the final round-two repair.
|
|
|
|
|
|
## Reservation and unregister races
|
|
|
|
A scheduled refresh can yield after selecting eligible jobs while a manual trigger
|
|
reserves one of those jobs. Previously the stale scheduled attempt reused the
|
|
same set entry, found the manual run under its row lock, and removed that entry.
|
|
The manual provider call was still running but no longer consumed scheduler
|
|
capacity. Dispatch now performs the synchronous reservation check and insertion
|
|
at one boundary; callers cannot remove a reservation they did not create. Refresh
|
|
also rechecks active and retained-settlement state after its database read.
|
|
|
|
Unregister now preserves runs whose real result is retained for settlement. For
|
|
other unfinished runs, it locks and rereads the current run before cancelling, so
|
|
a completion committed since discovery cannot be overwritten. The existing job
|
|
store still owns mutation notifications. Provider calls stay outside transactions;
|
|
no new periodic work or durable storage is introduced.
|
|
|
|
Real PostgreSQL regressions first reproduced both incorrect cancellation of saved
|
|
success/failure results and loss of a manual reservation after a delayed query
|
|
result. The corrected suite also checks concurrent completion during unregister,
|
|
manual/scheduled capacity, retries without another provider call, drain/resume,
|
|
startup, shutdown and quiet states. All 33 complete scheduler cases and server
|
|
TypeScript checks pass on the owning slice. The first full run crossed the next
|
|
hourly cron boundary during a retry fixture and legitimately produced extra runs;
|
|
its fixed clock now isolates that test from wall-clock start time while preserving
|
|
its assertions. Stack integration and latest-head CI are separate gates.
|
|
|
|
|
|
## Bounded completion writes
|
|
|
|
A retained completion transaction now sets a PostgreSQL statement timeout local
|
|
to that transaction, thirty seconds by default. A database row lock cannot hold
|
|
the completion attempt forever. Both ordinary reconciliation and the final
|
|
shutdown flush use the same retained writer. On timeout, the result remains in
|
|
memory and the existing recovery owner can retry it without invoking the plugin
|
|
again. Shutdown reports the persistence failure and continues through the existing
|
|
cleanup/retry boundary. The timeout resets when the transaction ends; no shared
|
|
pool setting or polling loop is added.
|
|
|
|
The bound applies to PostgreSQL statement execution and lock waits. It is not a
|
|
new bound on the plugin RPC, every shutdown operation, or a network failure before
|
|
PostgreSQL receives a statement. A retained result is process-local; after process
|
|
loss, existing durable run rows and their existing orphan policy remain the source
|
|
of truth. This change does not make arbitrary plugin side effects replayable.
|
|
|
|
A real PostgreSQL regression first observed a completion waiting for a held run
|
|
row, then demonstrated that shutdown remained pending. With the repair, PostgreSQL
|
|
cancels the write, shutdown reports failure while the lock is still held, and the
|
|
saved completion commits after release without a second plugin call. All 34
|
|
scheduler cases pass. All 247 cases in eleven complete scheduler/shutdown/startup,
|
|
warm-standby, driver-readiness, coordinator, scoped-deadline, approval/join
|
|
retention, approval seeding and decision-expiry suites pass, along with server
|
|
typecheck and production build, at `36be0bd733`. Earlier failures and their stated
|
|
revisions remain above. Fresh final-parent integration and exact-head remote
|
|
checks/reviews remain required.
|
|
|
|
|
|
Final parent `51668b8240` changes only approval-seeding behavior/tests and two
|
|
documents relative to the 247-case candidate. All plugin implementation, plugin
|
|
tests, shared deadline/coordinator and shutdown sources remain identical to that
|
|
run. After normal integration, all 63 cases in complete plugin-scheduler,
|
|
approval-seeding and scoped-deadline suites pass at `56ce7c1d2a`, plus server
|
|
typecheck/build. The original seventeen-file plugin boundary and exact parent
|
|
migration metadata are preserved. Publication waits for the parent to land so
|
|
final master reconciliation and remote checks can run together.
|
|
|
|
|
|
Approval seeding #17510 merged as `4e27a82a42`, with both final-head 5/5 reviews
|
|
and all 58 accepted checks. Plugin scheduling now integrates that actual master
|
|
parent. The complete integrated tree is byte-identical to `df78ca3b5e`, so the
|
|
247-case and final 63-case verification above retain their exact source scope.
|
|
No local checks were repeated on unchanged code. Only final publication docs
|
|
change afterwards; fresh exact-head CI and reviews remain required against master.
|