Skip to content

fix: align Crons API with LangGraph Platform spec (#44)#45

Open
webup wants to merge 15 commits into
developfrom
fix/44-crons-api-alignment
Open

fix: align Crons API with LangGraph Platform spec (#44)#45
webup wants to merge 15 commits into
developfrom
fix/44-crons-api-alignment

Conversation

@webup

@webup webup commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #44.

Aligns the Crons API with the LangGraph Platform OpenAPI spec as an additive superset: every missing spec field is added and wired to real behavior, while the project's existing extensions (webhook delivery, timezone, last_* tracking, the GET /runs/crons/{cron_id} endpoint) are kept.

What changed

Schemas (models/api.py)

  • CronCreate: end_time, interrupt_before/after, on_run_completed, stream_mode, stream_subgraphs, stream_resumable, durability; extra="allow" per spec additionalProperties: true.
  • New ThreadCronCreate (separate schema with multitask_strategy, no on_run_completed).
  • CronPatch: same fields incl. multitask_strategy (now patchable).
  • CronRead: user_id, payload, metadata, end_time, next_run_date (+ next_run_at retained as a deprecated alias).
  • CronSearchRequest: metadata, sort_by, sort_order, select, limit bounds (1–1000). CronCountRequest: metadata.

Behavior

  • Run-control fields stored in kwargs_json (mirrors runs.py) and applied at dispatch.
  • end_time enforced by the scheduler (stops firing; disables once next_run crosses it; honored on the stale-tick reclaim path too).
  • on_run_completed=delete deletes the ephemeral stateless thread after a successful run only (interrupted/error runs are preserved — resumable/inspectable).
  • Startup additive migration adds new columns to pre-existing tables (create_all does not alter existing tables); guards against unsafe NOT NULL columns without a server_default.
  • Shared delete_threads_cascade (in thread_service) reused by both the thread delete endpoint and the cron scheduler.

Testing

  • Full unit/integration suite (SQLite): 696 passed. New behavior covered with multi-consequence assertions; verified non-tautological via mutation testing.
  • Real-DB e2e (tests/e2e/test_cron_api_live.py): cron CRUD + new fields + sort/metadata/select over real HTTP. Runs automatically under the existing mysql-family-checkpoint-validation and pgsql-metadata-checkpoint-validation CI jobs across seekdb / oceanbase / mysql (+ Postgres metadata). Validated locally against embedded SeekDB.

Review

Went through two rounds of code review (correctness + test-solidity); all actionable findings addressed. Notable behavior changes flagged for reviewers:

  • on_run_completed defaults to delete (spec default) — stateless crons now clean up their ephemeral threads.
  • Search limit minimum tightened from 0 to 1.

webup added 15 commits June 13, 2026 18:09
- Add additive column migration on startup so end_time/on_run_completed
  exist on pre-existing tables (create_all does not alter existing tables);
  server_default backfills legacy rows (#1)
- on_run_completed=delete no longer deletes interrupted (resumable) runs (#2)
- Enforce end_time on the stale-'started' tick reclaim path (#4)
- Share a validating stream-mode normalizer between runs and crons; empty
  list and unsupported modes are rejected, not silently dropped (#5, #11)
- Make multitask_strategy patchable; simplify patch run-control fold to a
  dict-merge; explicit-null clears a field to its default (#8, #15)
- Consistent null-field handling across cron search/get/patch responses (#9)
- Extract shared delete_threads_cascade in thread_service, reused by the
  delete endpoint and the cron scheduler (batched); removes duplication (#10, #13, #14)
- Sort tiebreaker follows the primary sort direction for stable pagination (#6)
- Name the declared-field guard; consolidate run-control default constants (#12)
…olish

- _apply_additive_migrations skips (with a loud error) any NOT NULL column
  lacking a server_default rather than emitting DDL that fails on a populated
  table — forward-looking guard for future columns
- Drop dead apply_metadata_filters import from api/crons.py
- Add tests: migration skips unsafe column; search select omits null fields
- Verify Run rows are actually deleted by delete_threads_cascade against the
  real DB, on both the stateless-cron path and the thread DELETE endpoint
  (previously only the Thread was checked; a dropped delete(Run) shipped green)
- Cover on_run_completed=delete for error runs too (parametrized with
  interrupted): only success triggers deletion
- Explicit-null clear for interrupt_before and stream_mode (stream_mode ->
  stream_modes key remap)
- Pin the metadata non-string-value search limitation; assert select projection
  still honors owner auth filters
- Add dedicated unit tests for the shared stream_modes normalizer
- Add end_time == now boundary test; assert next_run_date in the API body

Verified non-tautological via mutation testing (dropped delete(Run), weakened
the delete gate, broke null-clear — each fails the corresponding test).
…on API

The cron unit/integration tests run on in-memory SQLite. This adds e2e tests
that drive the cron HTTP surface against the live API + real MySQL-family
backend, so CI exercises the backend-divergent paths SQLite hides: the additive
startup migration's ALTER TABLE ADD COLUMN DDL, JSON column round-trips for
payload/metadata/kwargs, and JSON extraction for metadata filtering + sort_by.

Runs automatically under the existing mysql-family-checkpoint-validation and
pgsql-metadata-checkpoint-validation CI jobs (both invoke make test-checkpoints
-> pytest tests/e2e -m e2e) across seekdb, oceanbase, and mysql backends — no
workflow change needed. Validated locally against embedded SeekDB (2 passed).
- Add real-DB migration upgrade test: create a legacy table missing a column,
  run _apply_additive_migrations, assert the dialect's ALTER TABLE ADD COLUMN
  executes and backfills via server_default on the live backend (was SQLite-only)
- Add real-DB scheduler cascade-delete test: on_run_completed='delete' reconcile
  removes the stateless thread AND its run via the cross-table cascade
  (the highest-risk data-deletion path; was SQLite-only)
- Pin the numeric-metadata-filter safety invariant on the real backend (no
  cross-cron false positives, regardless of JSON coercion differences)
- New e2e_db fixture wires the in-process db_manager to the real backend so
  scheduler/migration functions can be invoked directly (async fixture + async
  tests to share pytest-asyncio's loop and avoid cross-loop engine binding)
- Record the deliberate FOR UPDATE-locking coverage boundary

Validated against embedded SeekDB: 4 cron e2e tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: The crons API is not fully aligned

1 participant