Skip to content

Document the built-in scheduler component plugin (5.2)#592

Open
jcohen-hdb wants to merge 9 commits into
mainfrom
scheduler-component-docs
Open

Document the built-in scheduler component plugin (5.2)#592
jcohen-hdb wants to merge 9 commits into
mainfrom
scheduler-component-docs

Conversation

@jcohen-hdb

Copy link
Copy Markdown
Member

Documents the new built-in scheduler component plugin shipping in HarperFast/harper#1828 (issue HarperFast/harper#951):

  • New reference page reference/components/scheduler.md: the scheduler: config surface, supported cron syntax, interval durations, timezone/DST semantics, the handler contract (JobRunContext, idempotency requirement), and cluster behavior (leader election, failover timing, missed-occurrence catch-up, run state).
  • Row in the Built-In table in reference/components/overview.md and a sidebar entry.
  • 5.2 release-notes entry under a new Components section.

Draft until the feature PR itself leaves draft; the two should land together. npm run build and npm run format:check pass locally (the one broken-anchor warning on the 5.1 release-notes page is pre-existing on main).

Generated by an LLM (Claude Fable 5) pairing with @jcohen-hdb.

🤖 Generated with Claude Code

New reference page for the scheduler (config surface, cron syntax,
intervals, timezone/DST semantics, handler contract and idempotency,
cluster leader election and catch-up behavior), a row in the built-in
components table, sidebar entry, and a 5.2 release-notes entry.

Companion to HarperFast/harper#1828.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-592

This preview will update automatically when you push new commits.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the new scheduler component, which enables recurring jobs to be defined via cron or interval schedules within a component's configuration. The changes include adding a new documentation page, updating the components overview, adding release notes, and registering the new page in the documentation sidebar. A review comment suggests improving the readability of the provided JavaScript example by extracting the time calculation into a named constant.

Comment thread reference/components/scheduler.md Outdated
Record cleanup is built into Harper (table expiration/eviction), so it was
a misleading flagship example; point readers there instead and demonstrate
the scheduler with jobs only it can do: a daily dataset snapshot (which
also demonstrates idempotent handler design) and a scheduled external-API
pull.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to pr-592 July 16, 2026 17:06 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-592

This preview will update automatically when you push new commits.

@jcohen-hdb
jcohen-hdb marked this pull request as ready for review July 16, 2026 17:10
@jcohen-hdb
jcohen-hdb requested a review from a team as a code owner July 16, 2026 17:10

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scheduler reference has three behavior-contract mismatches with the companion core implementation. Please address these before merge.

Comment thread reference/components/scheduler.md Outdated

<VersionBadge version="v5.2.0" />

The scheduler is a built-in plugin that runs recurring jobs declared in a component's configuration. Harper invokes a designated export from your component on a cron or interval schedule, and in a cluster each job fires exactly once per occurrence - on a single, automatically elected node - rather than once per node or worker.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The companion implementation has no distributed lock/CAS and explicitly allows duplicate delivery during failover, DST fall-back, and split-brain recovery. Calling this "exactly once per occurrence" contradicts the idempotency warning below and may lead users to omit deduplication. Describe this as leader-coordinated single execution under normal operation with possible duplicates, and update the same absolute claim in the 5.2 release notes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in af1a837: the intro now describes leader-coordinated single execution under normal operation, with explicit duplicate-delivery cases (failover, DST fall-back, split-brain recovery) tied to the idempotency requirement. The 5.2 release-notes entry carried the same overstatement and is aligned too. (AI-generated response)

Comment thread reference/components/scheduler.md Outdated

**Handlers should be idempotent.** Harper's clustering has no distributed lock, so leadership failover and daylight-saving fall-back can occasionally deliver the same logical occurrence twice. Design handlers so that running twice for one occurrence is harmless.

Runs of the same job never overlap: if a run is still going when its next occurrence arrives, that occurrence is skipped (with a log entry) rather than stacked.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation does not consistently skip an occurrence when a handler outlasts its cadence. It arms the next timer only after the handler settles; an overdue interval then runs immediately, while the cron catch-up sweep can run the latest missed occurrence afterward. This matters because a slow interval handler can execute back-to-back. Either align the implementation with this statement or document the actual late/catch-up behavior.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in af1a837, documenting the implementation as-is: runs never overlap; missed time is not queued; an overdue interval job runs back-to-back after a slow handler; cron makes up at most its single most recent missed occurrence (context.catchUp = true). (AI-generated response)

Comment thread reference/components/scheduler.md Outdated

### `interval`

Type: `string` (duration)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema accepts interval as either a duration string or a number of seconds, so Type: string is incomplete. Runtime validation also rejects intervals longer than 365 days, but only the one-second minimum is documented. List both accepted types and both bounds so configurations do not fail unexpectedly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in af1a837: the field now reads 'Type: string (duration) or number (seconds)' and documents both bounds (1 second to 365 days). (AI-generated response)

Unquoted leading-asterisk crons are YAML aliases and @macros are reserved
characters; a space before # in a handler reference silently drops the
export name. Surfaced by audit of the feature PR. Also fixes a leftover
cleanupOldRecords reference from the example rework.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to pr-592 July 17, 2026 15:32 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-592

This preview will update automatically when you push new commits.

- 'exactly once per occurrence' -> leader-coordinated single execution
  under normal operation, with possible duplicate delivery during
  failover/DST fall-back/split-brain recovery (consistent with the
  idempotency requirement); release-notes entry aligned too
- overlap semantics corrected: runs never overlap, but an overdue
  interval job runs back-to-back after a slow handler, and cron makes up
  at most its most recent missed occurrence
- interval documents both accepted types (duration string or number of
  seconds) and both bounds (1s to 365 days)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jcohen-hdb
jcohen-hdb requested a review from kriszyp July 17, 2026 17:12
@github-actions
github-actions Bot temporarily deployed to pr-592 July 17, 2026 17:14 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-592

This preview will update automatically when you push new commits.

@heskew heskew left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the documented config surface against #1828's implementation: job keys (name/cron/interval/timezone/handler) match scheduler.ts's KNOWN_JOB_KEYS exactly, the "exactly one of cron/interval" rule and the module#export handler format match, and the context props (jobName/scheduledAt/catchUp) match engine.ts's handler call. Accurate as written — just worth landing this alongside/after #1828. LGTM.

🤖 Reviewed via cross-model pipeline; approved by @heskew.

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — clean, thorough addition; the clustering/idempotency edge-case coverage is great.

One fix worth making to the example (reference/components/scheduler.md:97): spread the untrusted external payload before the explicit keys, so an id in the API response can't silently override our 'latest' id and redirect the write:

await tables.ExchangeRates.put({ ...rates, id: 'latest', fetchedAt: context.scheduledAt });

(As written — ...rates last — a rogue id key from the external API would win.) Always spread untrusted objects first so explicit keys override them.

— Claude

Review nit (kriszyp): as written, a rogue `id` in the external API
response would override the explicit 'latest' id and redirect the write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-592

This preview will update automatically when you push new commits.

@github-actions
github-actions Bot temporarily deployed to pr-592 July 17, 2026 20:44 Inactive
@jcohen-hdb

Copy link
Copy Markdown
Member Author

Good catch @kriszyp — fixed in caf0396. The example now spreads ...rates before the explicit id/fetchedAt, so an id in the external payload can't redirect the write. Applied your exact form.

(AI-generated response)

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this Jake!


```javascript
// Nightly snapshot: roll the current state of a table into a dated snapshot record
export async function snapshotMetrics(context) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, non-blocking: tables.Devices.search([]) is valid (Table.ts accepts a bare array as conditions directly), but resource-api.md only documents the search({}) object form for an unconditioned scan. Consider using search({}) here to match the idiom taught elsewhere, so a reader who cross-references resource-api.md finds the exact form used in this example.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 20b6a4b — switched the example to search({}) to match the documented unconditioned-scan form in resource-api.md. Leaving the thread for you to resolve. (AI-generated response)

@kriszyp

kriszyp commented Jul 20, 2026

Copy link
Copy Markdown
Member

Following up from review finding 4 on the companion PR (HarperFast/harper#1828): the Missed Occurrences section on the main scheduler page already documents this correctly ("Only the single most recent missed occurrence is made up, not every occurrence missed during an outage") — good.

The one spot that's still double-fire-only is the "What's New" changelog blurb:

...with heartbeat-based failover, catch-up for missed occurrences, and per-job run state recorded in a replicated system table (handlers should be idempotent, as failover can occasionally deliver an occurrence twice).

Suggest appending the flip side for consistency with the main page, e.g.:

...(handlers should be idempotent, as failover can occasionally deliver an occurrence twice; conversely, catch-up only backfills the single most recent missed occurrence, not a full backlog).

Low priority wording nit, not blocking. (Note: matching JSDoc clarification landed on the harper side in cd2c2b961.)

🤖 Generated with Claude Code

Matches the unconditioned-scan form documented in resource-api.md
(review nit from kriszyp).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jcohen-hdb

Copy link
Copy Markdown
Member Author

Review nit addressed (20b6a4b) — this is otherwise ready.

Holding the merge intentionally: this documents the scheduler component that landed in core main via HarperFast/harper#1828 but has not shipped in a released Harper version yet. Since the reference docs are consumer-facing, this should merge only once the scheduler is in a released version (and carry the matching <VersionBadge>). Please don't merge ahead of that release. @kriszyp — flagging so it isn't merged early; happy to update the version badge when the release lands.

(AI-generated comment on behalf of @jcohen-hdb)

@github-actions
github-actions Bot temporarily deployed to pr-592 July 20, 2026 15:12 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-592

This preview will update automatically when you push new commits.

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these are some small nits, but maybe can help refine the docs.


### Run State

Each job's last run time, status, duration, and last error (if any) are recorded in the `hdb_scheduler_state` system table, alongside the leader lease. This state replicates across the cluster so a newly promoted leader knows what has already run.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The companion implementation's DESIGN.md explicitly notes that, on constrained/directional replication topologies, the scheduler lease may not reach every node. In that supported configuration, nodes can each elect themselves and execute every occurrence continuously, rather than only duplicating occasionally during failover. Please qualify the cluster-once guarantee here (and in the intro/release-note wording) as requiring hdb_scheduler_state to reach every participating node, and call out the topology limitation alongside the idempotency requirement.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 004cec8: new 'Replication Topology' section under Cluster Behavior states the guarantee holds only where hdb_scheduler_state replicates, describes the every-node-self-elects failure mode on constrained/directional topologies (distinct from the occasional failover duplicate), and points at replication.databases scoping; the intro now carries the same qualification with an anchor link. (AI-generated response)

Comment thread reference/components/scheduler.md Outdated

// Scheduled pull from an external API into a Harper table
export async function syncExchangeRates(context) {
const response = await fetch('https://api.example.com/rates');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we give this request an application-level deadline and document the failure behavior? Core records a rejected handler as an attempted run (lastRunAt/lastStatus: error) and does not retry or catch it up, so one transient 503 on a daily job loses that occurrence. If the response body never settles, the stronger failure is that the single-flight job never reschedules at all. Please use something like signal: AbortSignal.timeout(...) and state that handlers needing delivery guarantees must implement bounded retry/backoff.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 004cec8: the example fetch now uses AbortSignal.timeout(10_000) with a comment explaining why (single-flight — an unsettled request blocks rescheduling), and a new 'A failed run is not retried' paragraph documents the failure semantics (lastStatus: error, occurrence not made up) and tells handlers needing delivery guarantees to implement bounded retry/backoff. (AI-generated response)

Comment thread reference/components/scheduler.md Outdated

## Configuration

In your component's `config.yaml`, use the `scheduler` key to declare jobs:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please warn that creating config.yaml replaces Harper's default component config rather than merging with it. A previously config-less app copying this scheduler-only sample loses rest, graphqlSchema, roles, jsResource, fastifyRoutes, and static; its endpoints can disappear and the example's expected table globals may no longer load. A link to the Components overview's Default Configuration section plus an instruction to retain/redeclare the plugins the app uses would prevent that copy/paste failure.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 004cec8: the Configuration intro now warns that creating a config.yaml replaces the default component config (no merging), links to the Components overview's Default Configuration section, and names the plugins to keep declared. (AI-generated response)

Comment thread reference/components/scheduler.md Outdated

Type: `string`

An IANA timezone (for example `America/Chicago`) the cron expression is evaluated in. Defaults to the server's timezone. Only valid with `cron`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In core, this default is resolved from the local runtime on whichever node is currently scheduler leader. If cluster nodes use different host timezones, failover can shift a 0 2 * * * job by hours and can create an extra or missed expected wall-clock run. Please recommend an explicit timezone for clustered deployments unless every participating node is guaranteed to use the same timezone.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 004cec8: the timezone default is now described as the current leader's host timezone, with an explicit recommendation to set timezone in clusters whose nodes may differ — including the concrete failure (a failover shifting '0 2 * * *' by hours). (AI-generated response)

Comment thread reference/components/scheduler.md Outdated
const rates = await response.json();
// Spread the untrusted API payload first so our explicit keys win - otherwise
// a rogue `id` in the response could redirect the write to another record
await tables.ExchangeRates.put({ ...rates, id: 'latest', fetchedAt: context.scheduledAt });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting explicit keys after ...rates correctly protects id and fetchedAt, but this still persists every other field from a payload the example calls untrusted. Harper schemas are flexible by default, so undeclared properties are not automatically rejected; a malformed upstream can write arbitrary extra fields or unexpected shapes into latest. Could the example validate/destructure the expected fields before put() while retaining the current explicit-key-last ordering?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 004cec8: the example now destructures and type-checks the expected fields before put() (explicit keys still last), with a comment noting Harper's flexible schemas would otherwise persist whatever shape the upstream sent. (AI-generated response)

…nded external calls, config-replacement warning, leader-local timezone caveat, payload validation

- Qualify the cluster-once guarantee: it requires hdb_scheduler_state to
  reach every participating node; new Replication Topology section covers
  the constrained/directional-topology failure mode
- Bound the example's external fetch with AbortSignal.timeout and document
  failure semantics (failed runs are recorded, not retried)
- Warn that creating config.yaml replaces the default component config
- Recommend explicit timezone for clusters (default resolves on the
  current leader's host)
- Validate/destructure the API payload in the example instead of spreading
  an untrusted shape
- Release notes: note catch-up backfills only the most recent miss

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jcohen-hdb

Copy link
Copy Markdown
Member Author

Changelog nit also addressed in 004cec8 — took your suggested wording verbatim (catch-up backfills only the single most recent missed occurrence). All five inline threads from today's review are addressed in the same commit; left them open for you to resolve. (AI-generated comment)

@github-actions
github-actions Bot temporarily deployed to pr-592 July 20, 2026 18:58 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-592

This preview will update automatically when you push new commits.

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are down to some small minor nits.

Comment thread reference/components/scheduler.md Outdated

**A failed run is not retried.** If a handler throws (or its outbound request times out), the run is recorded with `lastStatus: error` and the occurrence is not made up - the job simply fires again at its next scheduled time. Handlers that need delivery guarantees should implement their own bounded retry/backoff inside the handler.

Runs of the same job never overlap. Missed time is not queued up, but it is also not always skipped outright: when a run outlasts its cadence, an interval job's next run starts promptly after the previous one finishes (a slow handler can therefore run back-to-back), and a cron job makes up at most its single most recent missed occurrence (delivered with `context.catchUp` set to `true`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we qualify this as "On a single scheduler leader, runs of the same job never overlap" and mention that duplicate deliveries across leaders can overlap? The implementation's job.running guard is process-local. If node A is partitioned while a slow handler is in flight and node B promotes, B can invoke the same logical occurrence before A finishes. The existing idempotency warning covers duplicate results, but the absolute wording here could make readers assume concurrent external side effects or read/modify/write work are impossible.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in c01c5d6: now reads 'On a single scheduler leader, runs of the same job never overlap' with an explicit note that the guard is per-leader and the duplicate-delivery cases can overlap in time across nodes — so idempotency must cover concurrent execution, not just repeated execution. (AI-generated response)

Comment thread reference/components/scheduler.md Outdated

One node in the cluster - the scheduler leader - runs all scheduled jobs; the others watch. Leader election is automatic and requires no configuration:

- The leader maintains a heartbeat in the replicated `system` database. If it stops heartbeating (crash, shutdown, partition) for more than five minutes, the next node in line promotes itself; with default timings, failover completes within about six and a half minutes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we scope the ~6.5-minute estimate to the case where the first eligible successor is alive? The 5-minute stale threshold plus the 75-second watcher interval supports that case, but promotionWaitMs adds a 150-second escalation rung for each earlier eligible node that does not promote. For example, with [A, B, C], if stale leader A and preferred successor B are both unavailable, C can take materially longer than this sentence suggests.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in c01c5d6: the estimate is now scoped to 'when the first eligible successor is up,' with the escalation ladder called out (each earlier down successor adds ~2.5 minutes before the next node claims). (AI-generated response)

… to first-successor-alive

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-592

This preview will update automatically when you push new commits.

@github-actions
github-actions Bot temporarily deployed to pr-592 July 20, 2026 19:15 Inactive
Comment thread reference/components/scheduler.md Outdated

**A failed run is not retried.** If a handler throws (or its outbound request times out), the run is recorded with `lastStatus: error` and the occurrence is not made up - the job simply fires again at its next scheduled time. Handlers that need delivery guarantees should implement their own bounded retry/backoff inside the handler.

On a single scheduler leader, runs of the same job never overlap. Note this guard is per-leader: in the rare duplicate-delivery cases above (a partitioned leader with a slow handler still in flight while its successor promotes), the two deliveries of one occurrence run on different nodes and CAN overlap in time - so idempotency must cover concurrent execution, not just repeated execution. Missed time is not queued up, but it is also not always skipped outright: when a run outlasts its cadence, an interval job's next run starts promptly after the previous one finishes (a slow handler can therefore run back-to-back), and a cron job makes up at most its single most recent missed occurrence (delivered with `context.catchUp` set to `true`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guarantee also has a reload/redeploy boundary. unregisterComponentJobs() removes the old registered object and clears its timer, but it does not cancel a handler already awaited by executeJob(); registration then creates a new object with running: false, so the replacement can fire while the old handler is still in flight on the same leader. Could we qualify this as non-overlap for one unchanged registration and explicitly warn that reload/redeploy can overlap an in-flight run? Otherwise a user may rely on this sentence for non-idempotent mutations and see concurrent execution during a normal deploy.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 7031a07: the guarantee now reads 'For a single, unchanged job registration on one scheduler leader' with both piercing boundaries called out — cross-node duplicate delivery and the reload/redeploy in-flight overlap — framed as reasons idempotency must cover concurrent execution. (AI-generated response)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-592

This preview will update automatically when you push new commits.

total++;
if (device.status === 'active') active++;
}
// Keyed by day, so a duplicate delivery of the same occurrence just rewrites

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the date as the primary key prevents duplicate rows, but it does not make this handler concurrency-idempotent. The duplicate deliveries described below can overlap: one run can scan 100 active devices, another scan 101 and write first, then the slower older run can overwrite the record back to 100. Could we use a genuinely deterministic/atomic example, or call this duplicate-row avoidance and explain that concurrent executions need stable input, reconciliation, or an application-specific atomic claim?


### Missed Occurrences

If the most recent occurrence of a cron job was missed - the leader was down, a failover was in progress, or daylight-saving time skipped the slot - the scheduler fires one catch-up run for it (with `context.catchUp` set to `true`). Only the single most recent missed occurrence is made up, not every occurrence missed during an outage. Interval jobs resume their cadence from their last recorded run.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we reconcile this DST example with line 63? A spring-forward gap occurrence is mapped to the shifted instant rather than skipped, so a normally armed run fires there with catchUp: false. It can still be missed because the leader was down, but DST itself does not turn that normal run into catch-up. Removing daylight-saving time skipped the slot (or explaining the distinct missed-shifted-occurrence case) would keep the context.catchUp contract unambiguous.

| [`loadEnv`](../environment-variables/overview.md) | Load environment variables from `.env` files |
| [`rest`](../rest/overview.md) | Enable automatic REST endpoint generation |
| [`roles`](../users-and-roles/overview.md) | Define role-based access control from YAML files |
| [`scheduler`](./scheduler.md) | Run recurring jobs on a cron or interval schedule |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new entry is explicitly a plugin, but the table is headed Built-In Extensions Reference; the same page distinguishes plugins from deprecated extensions above. Could we rename the heading to Built-In Components Reference or Built-In Plugins and Extensions Reference so the navigation does not classify scheduler as an extension?

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.

3 participants