Skip to content

Replace Bun cron parser with portable implementation#1

Open
aashahin wants to merge 1 commit into
masterfrom
claude/workflows-package-review-l6h3lu
Open

Replace Bun cron parser with portable implementation#1
aashahin wants to merge 1 commit into
masterfrom
claude/workflows-package-review-l6h3lu

Conversation

@aashahin

Copy link
Copy Markdown
Owner

Summary

This PR replaces the runtime-dependent Bun cron parser with a portable, self-contained cron expression parser and next/previous occurrence engine. The new implementation works across all runtimes (Bun, Cloudflare, Node.js) without external dependencies, while maintaining full cron expression compatibility and improving reliability.

Key Changes

Cron Parser Implementation

  • Replaced Bun.cron.parse dependency with a custom parser that handles:

    • Standard 5-field cron expressions (minute, hour, DOM, month, DOW)
    • Optional 6-field format with seconds
    • Named months (JAN-DEC) and days (SUN-SAT)
    • Lists, ranges, and step values (e.g., */15, 10-50/20)
    • Proper DOM/DOW OR-ing semantics when both are restricted
    • Month boundary and leap-year handling
  • Added next/previous occurrence computation with:

    • Timezone-aware date arithmetic using UTC getters/setters on a "cron-parser frame"
    • Strict monotonic progression (each next occurrence is strictly greater than the previous)
    • Efficient backward iteration for collecting multiple past occurrences

Missed-Run Policy Improvements

  • Extracted selectDueOccurrences() function to cleanly apply missed-run policies:

    • "skip" (default): only dispatch if the most recent occurrence is within maxDelayMs (default 1 hour)
    • "catch-up-latest": always dispatch the most recent missed occurrence
    • "catch-up-all": dispatch all collected occurrences in chronological order
  • Added maxDelayMs field to cron definitions for fine-grained control over stale run handling

Runtime Improvements

  • Bun runtime:

    • Removed registerOsCron() (unsupported on Bun); throws clear error if mode: "os" is configured
    • Added graceful shutdown: stop() now waits for in-flight drain to settle before returning
    • Improved error isolation: cron dispatch failures no longer abort remaining due runs in a tick
    • Added stopping flag to prevent new work during shutdown
  • Cloudflare runtime:

    • Deterministic envelope normalization using instanceId and timestamp from Cloudflare event
    • Wrapped ctx.dispatch() calls in uniquely-named steps for proper memoization across replays
    • Added wrapNonRetryable() to translate SDK non-retryable markers to Cloudflare's NonRetryableError
    • Improved error handling in status endpoint: transient binding failures return controlled 502 instead of crashing

Adapter Enhancements

  • Redis adapter: Added configurable retention policies for terminal instances and dead letters
  • SQLite adapter: Added retention configuration with opportunistic pruning in recoverStalled()
  • HTTP adapters: Added instance name caching with LRU eviction to prevent unbounded memory growth
  • All adapters: Improved error handling and validation

Testing & Documentation

  • Comprehensive cron parser tests covering edge cases (leap years, month boundaries, DOM/DOW OR-ing, etc.)
  • New runtime.test.ts for durationToMs() and dispatch client derivation
  • New retry.test.ts for backoff delay computation with jitter
  • Updated Cloudflare tests for deterministic envelope normalization and dispatch memoization
  • Updated README with runtime support clarification (Bun native, bundler-based TypeScript toolchains)

Configuration & Build

  • Updated tsconfig.json to standalone configuration (removed extends, explicit compiler options)
  • Updated package.json to exclude test files from published package

Notable Implementation Details

  • Timezone handling: All cron arithmetic runs on a "cron-parser frame" where UTC getters/setters represent wall-clock time in the target timezone, ensuring consistency regardless of host timezone
  • Backward iteration: getPreviousCronDates() efficiently collects multiple past occurrences by

https://claude.ai/code/session_01FkcHw1PpJfVDU3VSFfGcCU

…ackaging

Multi-agent audit of the SDK confirmed 40+ defects; this applies the
verified fixes:

Durability (Redis adapter):
- Claim now flips instance status to running atomically inside the claim
  Lua script (dedicated status hash field, read via parseInstanceRow), so
  a crash mid-claim can no longer orphan a job as permanently "queued".
- requeue() and recoverStalled() re-enqueue before releasing lease and
  processing entries: crash windows now risk a tolerated duplicate claim
  instead of permanent job loss.
- recoverStalled() recovers any non-terminal expired job (not only
  "running") and processing scores store claim time, aligning staleness
  semantics with the SQLite adapter.
- Retention TTLs for terminal instances, step results, dead letters, and
  configurable idempotency TTL that always covers scheduledAt horizons.
- Redis clients are validated at construction; discrete-method clients
  (set/expire/zrangeByScore/...) work without send().

Scheduler (cron rewrite):
- Real 5/6-field cron parser: ranges, lists, steps, month/day names,
  7==Sunday, vixie DOM-vs-DOW OR rule, leap years, UTC-consistent math.
  The old fallback ignored day/month/weekday fields entirely (weekly and
  monthly schedules fired daily) and the Bun.cron API it deferred to does
  not exist.
- missedRunPolicy now has distinct semantics: skip (freshness window via
  new CronDefinition.maxDelayMs, default 1h), catch-up-latest,
  catch-up-all.

Bun runtime:
- In-process cron uses chained setTimeout timers from the parser;
  scheduler mode "os" now throws (it silently never ran before).
- stop() drains in-flight work; the claim loop halts on shutdown.
- Unknown workflow names dead-letter instead of throwing out of the
  batch; completion-persist failures are logged and retried instead of
  re-running the already-succeeded workflow; per-cron-run error isolation
  in tick().

Cloudflare runner/handler:
- ctx.dispatch is wrapped in step.do so replays cannot re-fire nested
  dispatches; legacy envelope normalization derives stable ids from the
  CF instanceId/timestamp instead of fresh randomness per replay.
- SDK non-retryable errors translate to CF NonRetryableError (lazy
  cloudflare:workflows import); numeric sleeps pass milliseconds as
  numbers (the "N milliseconds" duration string is invalid on CF);
  RetryPolicy.maxIntervalMs maps to a CF dynamic delay function.
- scheduled() isolates per-run failures; /status errors return 502
  instead of crashing; invalid scheduledAt throws a clear validation
  error; rest-adapter reports partial batch results on the thrown error.

Core:
- Successful dispatches are no longer reported as failures when an
  onDispatch hook throws; hooks are isolated and logged.
- Step timeouts: only the winning attempt settles (late completions are
  dropped, no unhandled rejections); concurrent-execution hazard
  documented.
- durationToMs no longer misparses garbage via Date.parse (cron-like
  strings throw; bare numeric strings are milliseconds).
- createRunContext derives the dispatch client from clientConfig like
  runWorkflowEnvelope; WorkflowRuntimeOptions is exported.
- Optional RetryPolicy.jitter (0..1) for desynchronized backoff.

Callback/HTTP:
- Duplicate callbackSteps stepNames are rejected (same-name steps were
  silently skipped via step-result caching).
- SignedHttpAdapter.getInstance honors timeoutMs and wraps errors in
  WorkflowSendError; ids-only responses validate count; instance-name
  caches are FIFO-bounded.

Packaging/docs:
- Standalone tsconfig (was extending a nonexistent monorepo parent);
  tests excluded from the published tarball; README updated for runtime
  support, real scheduler modes, and the new cron semantics.

Tests: 58 -> 117 passing; tsc --noEmit clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkcHw1PpJfVDU3VSFfGcCU
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.

2 participants