Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/service-job-lease-window-docblock.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@objectstack/service-job': patch
---

docs(service-job): state the scheduler leader-election guarantee with its window (#14619)

Documentation only — no runtime change, no type change, no accept/reject
behaviour moves. It ships as a patch because the docblocks are **published
bytes**: `tsup`'s declaration rollup carries `CronJobAdapter.runScheduled`'s
docblock and `DbJobAdapter.schedule`'s routing docblock into `dist/index.d.ts`
/ `dist/index.d.cts` (measured: with all comments stripped, the before/after
declaration files are byte-identical — no exported symbol moved, no signature
changed).

`CronJobAdapter.runScheduled()` holds its cluster lock for the duration of a
scheduled fire (acquired, then released in `finally`), not for the scheduling
deadline. The two docblocks stated the guarantee — "only the node that
acquires the per-job lock runs the handler" — without that window, which reads
as exactly-once per deadline. It is exactly-once only when replica clocks
agree to within the handler's runtime (the normal case on an NTP-synced
deployment); a replica whose clock lags past that window finds the lock
already released and reruns the job. `once` schedules are the sharpest case,
since a one-shot has no later tick during which a business-level
de-duplication marker could self-correct that away. The multi-node section of
[Self-Hosted Deployment](/docs/deployment/self-hosting) states the same
caveat.

⛔ The mechanism is deliberately unchanged: holding the lease keyed to the
deadline (plus takeover semantics for a leader that dies mid-fire) is a
distributed-design item with zero measured pull and no measured
skew-to-runtime ratio — this is bookkeeping, not closure. If a real
duplicate-fire incident is measured on a `once` schedule, that remedy returns
as its own card.
9 changes: 9 additions & 0 deletions content/docs/deployment/self-hosting.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -402,6 +402,15 @@ decrypt each other's secrets. All replicas must share the same
`OS_SECRET_KEY`, `OS_AUTH_SECRET`, and database. See
[Cluster](/docs/kernel/cluster).

**Scheduled jobs' leader election has a window, not a deadline.** `cron` /
`interval` / `once` schedules dedupe across replicas by holding a lock for
the duration of each fire — the mutual-exclusion window is the handler's
runtime, not the scheduling deadline. Exactly-once per deadline holds only
when replica clocks agree to within that window, which an NTP-synced
deployment gives you. A replica whose clock lags past the window finds the
lock already released and reruns the job; `once` schedules are the sharpest
case, since a one-shot has no later tick to self-correct on.

## First boot: create the admin

How the first administrator is created depends on the deployment's
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/cron-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,6 +218,15 @@ export class CronJobAdapter implements IJobService {
* that acquires the per-job lock runs the handler; peers skip. No cluster /
* in-memory driver => lock always granted => single-node unchanged. Manual
* `trigger()` bypasses this.
*
* State the guarantee WITH its window: the lock below is held for the
* duration of the fire (acquired here, released in `finally`), so it
* de-duplicates *concurrent* fires — its mutual-exclusion window is the
* handler's runtime, not the scheduling deadline. Exactly-once per deadline
* holds only when replica clocks agree to within that window (the normal
* case on an NTP-synced deployment). A replica whose clock lags past the
* window finds the lock already released and reruns the job; `once` has no
* later tick during which a business-level de-duplication marker could win.
*/
private async runScheduled(name: string): Promise<void> {
const record = this.jobs.get(name);
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/db-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,6 +138,15 @@ export class DbJobAdapter implements IJobService {
* later tick during which a business-level de-duplication marker could win, so
* every replica's copy lands inside the same short window.
*
* **State the guarantee WITH its window.** The lock `CronJobAdapter.runScheduled()`
* takes is held for the duration of the fire, not for the deadline — its
* mutual-exclusion window is the handler's runtime. So the routing above
* de-duplicates *concurrent* fires: exactly-once per deadline holds only
* when replica clocks agree to within that window (NTP-synced deployments,
* the normal case). A replica whose clock lags past the window finds the
* lock already released and reruns the job, and `once` is the sharpest case
* because there is no later tick to self-correct that away.
*
* **`once` is AT-MOST-ONCE per cluster, and deliberately so** (maintainer
* ruling 2026-09-01). Election decides *who* fires, never *that* the fire
* survives: there is no second deadline, so a leader that dies mid-fire loses
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/service-job-lease-window-docblock.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@objectstack/service-job': patch
---

docs(service-job): state the scheduler leader-election guarantee with its window (#14619)

Documentation only — no runtime change, no type change, no accept/reject
behaviour moves. It ships as a patch because the docblocks are **published
bytes**: `tsup`'s declaration rollup carries `CronJobAdapter.runScheduled`'s
docblock and `DbJobAdapter.schedule`'s routing docblock into `dist/index.d.ts`
/ `dist/index.d.cts` (measured: with all comments stripped, the before/after
declaration files are byte-identical — no exported symbol moved, no signature
changed).

`CronJobAdapter.runScheduled()` holds its cluster lock for the duration of a
scheduled fire (acquired, then released in `finally`), not for the scheduling
deadline. The two docblocks stated the guarantee — "only the node that
acquires the per-job lock runs the handler" — without that window, which reads
as exactly-once per deadline. It is exactly-once only when replica clocks
agree to within the handler's runtime (the normal case on an NTP-synced
deployment); a replica whose clock lags past that window finds the lock
already released and reruns the job. `once` schedules are the sharpest case,
since a one-shot has no later tick during which a business-level
de-duplication marker could self-correct that away. The multi-node section of
[Self-Hosted Deployment](/docs/deployment/self-hosting) states the same
caveat.

⛔ The mechanism is deliberately unchanged: holding the lease keyed to the
deadline (plus takeover semantics for a leader that dies mid-fire) is a
distributed-design item with zero measured pull and no measured
skew-to-runtime ratio — this is bookkeeping, not closure. If a real
duplicate-fire incident is measured on a `once` schedule, that remedy returns
as its own card.
9 changes: 9 additions & 0 deletions content/docs/deployment/self-hosting.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -402,6 +402,15 @@ decrypt each other's secrets. All replicas must share the same
`OS_SECRET_KEY`, `OS_AUTH_SECRET`, and database. See
[Cluster](/docs/kernel/cluster).

**Scheduled jobs' leader election has a window, not a deadline.** `cron` /
`interval` / `once` schedules dedupe across replicas by holding a lock for
the duration of each fire — the mutual-exclusion window is the handler's
runtime, not the scheduling deadline. Exactly-once per deadline holds only
when replica clocks agree to within that window, which an NTP-synced
deployment gives you. A replica whose clock lags past the window finds the
lock already released and reruns the job; `once` schedules are the sharpest
case, since a one-shot has no later tick to self-correct on.

## First boot: create the admin

How the first administrator is created depends on the deployment's
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/cron-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,6 +218,15 @@ export class CronJobAdapter implements IJobService {
* that acquires the per-job lock runs the handler; peers skip. No cluster /
* in-memory driver => lock always granted => single-node unchanged. Manual
* `trigger()` bypasses this.
*
* State the guarantee WITH its window: the lock below is held for the
* duration of the fire (acquired here, released in `finally`), so it
* de-duplicates *concurrent* fires — its mutual-exclusion window is the
* handler's runtime, not the scheduling deadline. Exactly-once per deadline
* holds only when replica clocks agree to within that window (the normal
* case on an NTP-synced deployment). A replica whose clock lags past the
* window finds the lock already released and reruns the job; `once` has no
* later tick during which a business-level de-duplication marker could win.
*/
private async runScheduled(name: string): Promise<void> {
const record = this.jobs.get(name);
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/db-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,6 +138,15 @@ export class DbJobAdapter implements IJobService {
* later tick during which a business-level de-duplication marker could win, so
* every replica's copy lands inside the same short window.
*
* **State the guarantee WITH its window.** The lock `CronJobAdapter.runScheduled()`
* takes is held for the duration of the fire, not for the deadline — its
* mutual-exclusion window is the handler's runtime. So the routing above
* de-duplicates *concurrent* fires: exactly-once per deadline holds only
* when replica clocks agree to within that window (NTP-synced deployments,
* the normal case). A replica whose clock lags past the window finds the
* lock already released and reruns the job, and `once` is the sharpest case
* because there is no later tick to self-correct that away.
*
* **`once` is AT-MOST-ONCE per cluster, and deliberately so** (maintainer
* ruling 2026-09-01). Election decides *who* fires, never *that* the fire
* survives: there is no second deadline, so a leader that dies mid-fire loses
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/service-job-lease-window-docblock.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@objectstack/service-job': patch
---

docs(service-job): state the scheduler leader-election guarantee with its window (#14619)

Documentation only — no runtime change, no type change, no accept/reject
behaviour moves. It ships as a patch because the docblocks are **published
bytes**: `tsup`'s declaration rollup carries `CronJobAdapter.runScheduled`'s
docblock and `DbJobAdapter.schedule`'s routing docblock into `dist/index.d.ts`
/ `dist/index.d.cts` (measured: with all comments stripped, the before/after
declaration files are byte-identical — no exported symbol moved, no signature
changed).

`CronJobAdapter.runScheduled()` holds its cluster lock for the duration of a
scheduled fire (acquired, then released in `finally`), not for the scheduling
deadline. The two docblocks stated the guarantee — "only the node that
acquires the per-job lock runs the handler" — without that window, which reads
as exactly-once per deadline. It is exactly-once only when replica clocks
agree to within the handler's runtime (the normal case on an NTP-synced
deployment); a replica whose clock lags past that window finds the lock
already released and reruns the job. `once` schedules are the sharpest case,
since a one-shot has no later tick during which a business-level
de-duplication marker could self-correct that away. The multi-node section of
[Self-Hosted Deployment](/docs/deployment/self-hosting) states the same
caveat.

⛔ The mechanism is deliberately unchanged: holding the lease keyed to the
deadline (plus takeover semantics for a leader that dies mid-fire) is a
distributed-design item with zero measured pull and no measured
skew-to-runtime ratio — this is bookkeeping, not closure. If a real
duplicate-fire incident is measured on a `once` schedule, that remedy returns
as its own card.
9 changes: 9 additions & 0 deletions content/docs/deployment/self-hosting.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -402,6 +402,15 @@ decrypt each other's secrets. All replicas must share the same
`OS_SECRET_KEY`, `OS_AUTH_SECRET`, and database. See
[Cluster](/docs/kernel/cluster).

**Scheduled jobs' leader election has a window, not a deadline.** `cron` /
`interval` / `once` schedules dedupe across replicas by holding a lock for
the duration of each fire — the mutual-exclusion window is the handler's
runtime, not the scheduling deadline. Exactly-once per deadline holds only
when replica clocks agree to within that window, which an NTP-synced
deployment gives you. A replica whose clock lags past the window finds the
lock already released and reruns the job; `once` schedules are the sharpest
case, since a one-shot has no later tick to self-correct on.

## First boot: create the admin

How the first administrator is created depends on the deployment's
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/cron-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,6 +218,15 @@ export class CronJobAdapter implements IJobService {
* that acquires the per-job lock runs the handler; peers skip. No cluster /
* in-memory driver => lock always granted => single-node unchanged. Manual
* `trigger()` bypasses this.
*
* State the guarantee WITH its window: the lock below is held for the
* duration of the fire (acquired here, released in `finally`), so it
* de-duplicates *concurrent* fires — its mutual-exclusion window is the
* handler's runtime, not the scheduling deadline. Exactly-once per deadline
* holds only when replica clocks agree to within that window (the normal
* case on an NTP-synced deployment). A replica whose clock lags past the
* window finds the lock already released and reruns the job; `once` has no
* later tick during which a business-level de-duplication marker could win.
*/
private async runScheduled(name: string): Promise<void> {
const record = this.jobs.get(name);
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/db-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,6 +138,15 @@ export class DbJobAdapter implements IJobService {
* later tick during which a business-level de-duplication marker could win, so
* every replica's copy lands inside the same short window.
*
* **State the guarantee WITH its window.** The lock `CronJobAdapter.runScheduled()`
* takes is held for the duration of the fire, not for the deadline — its
* mutual-exclusion window is the handler's runtime. So the routing above
* de-duplicates *concurrent* fires: exactly-once per deadline holds only
* when replica clocks agree to within that window (NTP-synced deployments,
* the normal case). A replica whose clock lags past the window finds the
* lock already released and reruns the job, and `once` is the sharpest case
* because there is no later tick to self-correct that away.
*
* **`once` is AT-MOST-ONCE per cluster, and deliberately so** (maintainer
* ruling 2026-09-01). Election decides *who* fires, never *that* the fire
* survives: there is no second deadline, so a leader that dies mid-fire loses
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/service-job-lease-window-docblock.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@objectstack/service-job': patch
---

docs(service-job): state the scheduler leader-election guarantee with its window (#14619)

Documentation only — no runtime change, no type change, no accept/reject
behaviour moves. It ships as a patch because the docblocks are **published
bytes**: `tsup`'s declaration rollup carries `CronJobAdapter.runScheduled`'s
docblock and `DbJobAdapter.schedule`'s routing docblock into `dist/index.d.ts`
/ `dist/index.d.cts` (measured: with all comments stripped, the before/after
declaration files are byte-identical — no exported symbol moved, no signature
changed).

`CronJobAdapter.runScheduled()` holds its cluster lock for the duration of a
scheduled fire (acquired, then released in `finally`), not for the scheduling
deadline. The two docblocks stated the guarantee — "only the node that
acquires the per-job lock runs the handler" — without that window, which reads
as exactly-once per deadline. It is exactly-once only when replica clocks
agree to within the handler's runtime (the normal case on an NTP-synced
deployment); a replica whose clock lags past that window finds the lock
already released and reruns the job. `once` schedules are the sharpest case,
since a one-shot has no later tick during which a business-level
de-duplication marker could self-correct that away. The multi-node section of
[Self-Hosted Deployment](/docs/deployment/self-hosting) states the same
caveat.

⛔ The mechanism is deliberately unchanged: holding the lease keyed to the
deadline (plus takeover semantics for a leader that dies mid-fire) is a
distributed-design item with zero measured pull and no measured
skew-to-runtime ratio — this is bookkeeping, not closure. If a real
duplicate-fire incident is measured on a `once` schedule, that remedy returns
as its own card.
9 changes: 9 additions & 0 deletions content/docs/deployment/self-hosting.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -402,6 +402,15 @@ decrypt each other's secrets. All replicas must share the same
`OS_SECRET_KEY`, `OS_AUTH_SECRET`, and database. See
[Cluster](/docs/kernel/cluster).

**Scheduled jobs' leader election has a window, not a deadline.** `cron` /
`interval` / `once` schedules dedupe across replicas by holding a lock for
the duration of each fire — the mutual-exclusion window is the handler's
runtime, not the scheduling deadline. Exactly-once per deadline holds only
when replica clocks agree to within that window, which an NTP-synced
deployment gives you. A replica whose clock lags past the window finds the
lock already released and reruns the job; `once` schedules are the sharpest
case, since a one-shot has no later tick to self-correct on.

## First boot: create the admin

How the first administrator is created depends on the deployment's
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/cron-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,6 +218,15 @@ export class CronJobAdapter implements IJobService {
* that acquires the per-job lock runs the handler; peers skip. No cluster /
* in-memory driver => lock always granted => single-node unchanged. Manual
* `trigger()` bypasses this.
*
* State the guarantee WITH its window: the lock below is held for the
* duration of the fire (acquired here, released in `finally`), so it
* de-duplicates *concurrent* fires — its mutual-exclusion window is the
* handler's runtime, not the scheduling deadline. Exactly-once per deadline
* holds only when replica clocks agree to within that window (the normal
* case on an NTP-synced deployment). A replica whose clock lags past the
* window finds the lock already released and reruns the job; `once` has no
* later tick during which a business-level de-duplication marker could win.
*/
private async runScheduled(name: string): Promise<void> {
const record = this.jobs.get(name);
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/db-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,6 +138,15 @@ export class DbJobAdapter implements IJobService {
* later tick during which a business-level de-duplication marker could win, so
* every replica's copy lands inside the same short window.
*
* **State the guarantee WITH its window.** The lock `CronJobAdapter.runScheduled()`
* takes is held for the duration of the fire, not for the deadline — its
* mutual-exclusion window is the handler's runtime. So the routing above
* de-duplicates *concurrent* fires: exactly-once per deadline holds only
* when replica clocks agree to within that window (NTP-synced deployments,
* the normal case). A replica whose clock lags past the window finds the
* lock already released and reruns the job, and `once` is the sharpest case
* because there is no later tick to self-correct that away.
*
* **`once` is AT-MOST-ONCE per cluster, and deliberately so** (maintainer
* ruling 2026-09-01). Election decides *who* fires, never *that* the fire
* survives: there is no second deadline, so a leader that dies mid-fire loses
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/service-job-lease-window-docblock.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@objectstack/service-job': patch
---

docs(service-job): state the scheduler leader-election guarantee with its window (#14619)

Documentation only — no runtime change, no type change, no accept/reject
behaviour moves. It ships as a patch because the docblocks are **published
bytes**: `tsup`'s declaration rollup carries `CronJobAdapter.runScheduled`'s
docblock and `DbJobAdapter.schedule`'s routing docblock into `dist/index.d.ts`
/ `dist/index.d.cts` (measured: with all comments stripped, the before/after
declaration files are byte-identical — no exported symbol moved, no signature
changed).

`CronJobAdapter.runScheduled()` holds its cluster lock for the duration of a
scheduled fire (acquired, then released in `finally`), not for the scheduling
deadline. The two docblocks stated the guarantee — "only the node that
acquires the per-job lock runs the handler" — without that window, which reads
as exactly-once per deadline. It is exactly-once only when replica clocks
agree to within the handler's runtime (the normal case on an NTP-synced
deployment); a replica whose clock lags past that window finds the lock
already released and reruns the job. `once` schedules are the sharpest case,
since a one-shot has no later tick during which a business-level
de-duplication marker could self-correct that away. The multi-node section of
[Self-Hosted Deployment](/docs/deployment/self-hosting) states the same
caveat.

⛔ The mechanism is deliberately unchanged: holding the lease keyed to the
deadline (plus takeover semantics for a leader that dies mid-fire) is a
distributed-design item with zero measured pull and no measured
skew-to-runtime ratio — this is bookkeeping, not closure. If a real
duplicate-fire incident is measured on a `once` schedule, that remedy returns
as its own card.
9 changes: 9 additions & 0 deletions content/docs/deployment/self-hosting.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -402,6 +402,15 @@ decrypt each other's secrets. All replicas must share the same
`OS_SECRET_KEY`, `OS_AUTH_SECRET`, and database. See
[Cluster](/docs/kernel/cluster).

**Scheduled jobs' leader election has a window, not a deadline.** `cron` /
`interval` / `once` schedules dedupe across replicas by holding a lock for
the duration of each fire — the mutual-exclusion window is the handler's
runtime, not the scheduling deadline. Exactly-once per deadline holds only
when replica clocks agree to within that window, which an NTP-synced
deployment gives you. A replica whose clock lags past the window finds the
lock already released and reruns the job; `once` schedules are the sharpest
case, since a one-shot has no later tick to self-correct on.

## First boot: create the admin

How the first administrator is created depends on the deployment's
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/cron-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,6 +218,15 @@ export class CronJobAdapter implements IJobService {
* that acquires the per-job lock runs the handler; peers skip. No cluster /
* in-memory driver => lock always granted => single-node unchanged. Manual
* `trigger()` bypasses this.
*
* State the guarantee WITH its window: the lock below is held for the
* duration of the fire (acquired here, released in `finally`), so it
* de-duplicates *concurrent* fires — its mutual-exclusion window is the
* handler's runtime, not the scheduling deadline. Exactly-once per deadline
* holds only when replica clocks agree to within that window (the normal
* case on an NTP-synced deployment). A replica whose clock lags past the
* window finds the lock already released and reruns the job; `once` has no
* later tick during which a business-level de-duplication marker could win.
*/
private async runScheduled(name: string): Promise<void> {
const record = this.jobs.get(name);
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/db-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,6 +138,15 @@ export class DbJobAdapter implements IJobService {
* later tick during which a business-level de-duplication marker could win, so
* every replica's copy lands inside the same short window.
*
* **State the guarantee WITH its window.** The lock `CronJobAdapter.runScheduled()`
* takes is held for the duration of the fire, not for the deadline — its
* mutual-exclusion window is the handler's runtime. So the routing above
* de-duplicates *concurrent* fires: exactly-once per deadline holds only
* when replica clocks agree to within that window (NTP-synced deployments,
* the normal case). A replica whose clock lags past the window finds the
* lock already released and reruns the job, and `once` is the sharpest case
* because there is no later tick to self-correct that away.
*
* **`once` is AT-MOST-ONCE per cluster, and deliberately so** (maintainer
* ruling 2026-09-01). Election decides *who* fires, never *that* the fire
* survives: there is no second deadline, so a leader that dies mid-fire loses
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/service-job-lease-window-docblock.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@objectstack/service-job': patch
---

docs(service-job): state the scheduler leader-election guarantee with its window (#14619)

Documentation only — no runtime change, no type change, no accept/reject
behaviour moves. It ships as a patch because the docblocks are **published
bytes**: `tsup`'s declaration rollup carries `CronJobAdapter.runScheduled`'s
docblock and `DbJobAdapter.schedule`'s routing docblock into `dist/index.d.ts`
/ `dist/index.d.cts` (measured: with all comments stripped, the before/after
declaration files are byte-identical — no exported symbol moved, no signature
changed).

`CronJobAdapter.runScheduled()` holds its cluster lock for the duration of a
scheduled fire (acquired, then released in `finally`), not for the scheduling
deadline. The two docblocks stated the guarantee — "only the node that
acquires the per-job lock runs the handler" — without that window, which reads
as exactly-once per deadline. It is exactly-once only when replica clocks
agree to within the handler's runtime (the normal case on an NTP-synced
deployment); a replica whose clock lags past that window finds the lock
already released and reruns the job. `once` schedules are the sharpest case,
since a one-shot has no later tick during which a business-level
de-duplication marker could self-correct that away. The multi-node section of
[Self-Hosted Deployment](/docs/deployment/self-hosting) states the same
caveat.

⛔ The mechanism is deliberately unchanged: holding the lease keyed to the
deadline (plus takeover semantics for a leader that dies mid-fire) is a
distributed-design item with zero measured pull and no measured
skew-to-runtime ratio — this is bookkeeping, not closure. If a real
duplicate-fire incident is measured on a `once` schedule, that remedy returns
as its own card.
9 changes: 9 additions & 0 deletions content/docs/deployment/self-hosting.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -402,6 +402,15 @@ decrypt each other's secrets. All replicas must share the same
`OS_SECRET_KEY`, `OS_AUTH_SECRET`, and database. See
[Cluster](/docs/kernel/cluster).

**Scheduled jobs' leader election has a window, not a deadline.** `cron` /
`interval` / `once` schedules dedupe across replicas by holding a lock for
the duration of each fire — the mutual-exclusion window is the handler's
runtime, not the scheduling deadline. Exactly-once per deadline holds only
when replica clocks agree to within that window, which an NTP-synced
deployment gives you. A replica whose clock lags past the window finds the
lock already released and reruns the job; `once` schedules are the sharpest
case, since a one-shot has no later tick to self-correct on.

## First boot: create the admin

How the first administrator is created depends on the deployment's
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/cron-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,6 +218,15 @@ export class CronJobAdapter implements IJobService {
* that acquires the per-job lock runs the handler; peers skip. No cluster /
* in-memory driver => lock always granted => single-node unchanged. Manual
* `trigger()` bypasses this.
*
* State the guarantee WITH its window: the lock below is held for the
* duration of the fire (acquired here, released in `finally`), so it
* de-duplicates *concurrent* fires — its mutual-exclusion window is the
* handler's runtime, not the scheduling deadline. Exactly-once per deadline
* holds only when replica clocks agree to within that window (the normal
* case on an NTP-synced deployment). A replica whose clock lags past the
* window finds the lock already released and reruns the job; `once` has no
* later tick during which a business-level de-duplication marker could win.
*/
private async runScheduled(name: string): Promise<void> {
const record = this.jobs.get(name);
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/db-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,6 +138,15 @@ export class DbJobAdapter implements IJobService {
* later tick during which a business-level de-duplication marker could win, so
* every replica's copy lands inside the same short window.
*
* **State the guarantee WITH its window.** The lock `CronJobAdapter.runScheduled()`
* takes is held for the duration of the fire, not for the deadline — its
* mutual-exclusion window is the handler's runtime. So the routing above
* de-duplicates *concurrent* fires: exactly-once per deadline holds only
* when replica clocks agree to within that window (NTP-synced deployments,
* the normal case). A replica whose clock lags past the window finds the
* lock already released and reruns the job, and `once` is the sharpest case
* because there is no later tick to self-correct that away.
*
* **`once` is AT-MOST-ONCE per cluster, and deliberately so** (maintainer
* ruling 2026-09-01). Election decides *who* fires, never *that* the fire
* survives: there is no second deadline, so a leader that dies mid-fire loses
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/service-job-lease-window-docblock.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@objectstack/service-job': patch
---

docs(service-job): state the scheduler leader-election guarantee with its window (#14619)

Documentation only — no runtime change, no type change, no accept/reject
behaviour moves. It ships as a patch because the docblocks are **published
bytes**: `tsup`'s declaration rollup carries `CronJobAdapter.runScheduled`'s
docblock and `DbJobAdapter.schedule`'s routing docblock into `dist/index.d.ts`
/ `dist/index.d.cts` (measured: with all comments stripped, the before/after
declaration files are byte-identical — no exported symbol moved, no signature
changed).

`CronJobAdapter.runScheduled()` holds its cluster lock for the duration of a
scheduled fire (acquired, then released in `finally`), not for the scheduling
deadline. The two docblocks stated the guarantee — "only the node that
acquires the per-job lock runs the handler" — without that window, which reads
as exactly-once per deadline. It is exactly-once only when replica clocks
agree to within the handler's runtime (the normal case on an NTP-synced
deployment); a replica whose clock lags past that window finds the lock
already released and reruns the job. `once` schedules are the sharpest case,
since a one-shot has no later tick during which a business-level
de-duplication marker could self-correct that away. The multi-node section of
[Self-Hosted Deployment](/docs/deployment/self-hosting) states the same
caveat.

⛔ The mechanism is deliberately unchanged: holding the lease keyed to the
deadline (plus takeover semantics for a leader that dies mid-fire) is a
distributed-design item with zero measured pull and no measured
skew-to-runtime ratio — this is bookkeeping, not closure. If a real
duplicate-fire incident is measured on a `once` schedule, that remedy returns
as its own card.
9 changes: 9 additions & 0 deletions content/docs/deployment/self-hosting.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -402,6 +402,15 @@ decrypt each other's secrets. All replicas must share the same
`OS_SECRET_KEY`, `OS_AUTH_SECRET`, and database. See
[Cluster](/docs/kernel/cluster).

**Scheduled jobs' leader election has a window, not a deadline.** `cron` /
`interval` / `once` schedules dedupe across replicas by holding a lock for
the duration of each fire — the mutual-exclusion window is the handler's
runtime, not the scheduling deadline. Exactly-once per deadline holds only
when replica clocks agree to within that window, which an NTP-synced
deployment gives you. A replica whose clock lags past the window finds the
lock already released and reruns the job; `once` schedules are the sharpest
case, since a one-shot has no later tick to self-correct on.

## First boot: create the admin

How the first administrator is created depends on the deployment's
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/cron-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,6 +218,15 @@ export class CronJobAdapter implements IJobService {
* that acquires the per-job lock runs the handler; peers skip. No cluster /
* in-memory driver => lock always granted => single-node unchanged. Manual
* `trigger()` bypasses this.
*
* State the guarantee WITH its window: the lock below is held for the
* duration of the fire (acquired here, released in `finally`), so it
* de-duplicates *concurrent* fires — its mutual-exclusion window is the
* handler's runtime, not the scheduling deadline. Exactly-once per deadline
* holds only when replica clocks agree to within that window (the normal
* case on an NTP-synced deployment). A replica whose clock lags past the
* window finds the lock already released and reruns the job; `once` has no
* later tick during which a business-level de-duplication marker could win.
*/
private async runScheduled(name: string): Promise<void> {
const record = this.jobs.get(name);
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/db-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,6 +138,15 @@ export class DbJobAdapter implements IJobService {
* later tick during which a business-level de-duplication marker could win, so
* every replica's copy lands inside the same short window.
*
* **State the guarantee WITH its window.** The lock `CronJobAdapter.runScheduled()`
* takes is held for the duration of the fire, not for the deadline — its
* mutual-exclusion window is the handler's runtime. So the routing above
* de-duplicates *concurrent* fires: exactly-once per deadline holds only
* when replica clocks agree to within that window (NTP-synced deployments,
* the normal case). A replica whose clock lags past the window finds the
* lock already released and reruns the job, and `once` is the sharpest case
* because there is no later tick to self-correct that away.
*
* **`once` is AT-MOST-ONCE per cluster, and deliberately so** (maintainer
* ruling 2026-09-01). Election decides *who* fires, never *that* the fire
* survives: there is no second deadline, so a leader that dies mid-fire loses
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/service-job-lease-window-docblock.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@objectstack/service-job': patch
---

docs(service-job): state the scheduler leader-election guarantee with its window (#14619)

Documentation only — no runtime change, no type change, no accept/reject
behaviour moves. It ships as a patch because the docblocks are **published
bytes**: `tsup`'s declaration rollup carries `CronJobAdapter.runScheduled`'s
docblock and `DbJobAdapter.schedule`'s routing docblock into `dist/index.d.ts`
/ `dist/index.d.cts` (measured: with all comments stripped, the before/after
declaration files are byte-identical — no exported symbol moved, no signature
changed).

`CronJobAdapter.runScheduled()` holds its cluster lock for the duration of a
scheduled fire (acquired, then released in `finally`), not for the scheduling
deadline. The two docblocks stated the guarantee — "only the node that
acquires the per-job lock runs the handler" — without that window, which reads
as exactly-once per deadline. It is exactly-once only when replica clocks
agree to within the handler's runtime (the normal case on an NTP-synced
deployment); a replica whose clock lags past that window finds the lock
already released and reruns the job. `once` schedules are the sharpest case,
since a one-shot has no later tick during which a business-level
de-duplication marker could self-correct that away. The multi-node section of
[Self-Hosted Deployment](/docs/deployment/self-hosting) states the same
caveat.

⛔ The mechanism is deliberately unchanged: holding the lease keyed to the
deadline (plus takeover semantics for a leader that dies mid-fire) is a
distributed-design item with zero measured pull and no measured
skew-to-runtime ratio — this is bookkeeping, not closure. If a real
duplicate-fire incident is measured on a `once` schedule, that remedy returns
as its own card.
9 changes: 9 additions & 0 deletions content/docs/deployment/self-hosting.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -402,6 +402,15 @@ decrypt each other's secrets. All replicas must share the same
`OS_SECRET_KEY`, `OS_AUTH_SECRET`, and database. See
[Cluster](/docs/kernel/cluster).

**Scheduled jobs' leader election has a window, not a deadline.** `cron` /
`interval` / `once` schedules dedupe across replicas by holding a lock for
the duration of each fire — the mutual-exclusion window is the handler's
runtime, not the scheduling deadline. Exactly-once per deadline holds only
when replica clocks agree to within that window, which an NTP-synced
deployment gives you. A replica whose clock lags past the window finds the
lock already released and reruns the job; `once` schedules are the sharpest
case, since a one-shot has no later tick to self-correct on.

## First boot: create the admin

How the first administrator is created depends on the deployment's
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/cron-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,6 +218,15 @@ export class CronJobAdapter implements IJobService {
* that acquires the per-job lock runs the handler; peers skip. No cluster /
* in-memory driver => lock always granted => single-node unchanged. Manual
* `trigger()` bypasses this.
*
* State the guarantee WITH its window: the lock below is held for the
* duration of the fire (acquired here, released in `finally`), so it
* de-duplicates *concurrent* fires — its mutual-exclusion window is the
* handler's runtime, not the scheduling deadline. Exactly-once per deadline
* holds only when replica clocks agree to within that window (the normal
* case on an NTP-synced deployment). A replica whose clock lags past the
* window finds the lock already released and reruns the job; `once` has no
* later tick during which a business-level de-duplication marker could win.
*/
private async runScheduled(name: string): Promise<void> {
const record = this.jobs.get(name);
Expand Down
9 changes: 9 additions & 0 deletions packages/services/service-job/src/db-job-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,6 +138,15 @@ export class DbJobAdapter implements IJobService {
* later tick during which a business-level de-duplication marker could win, so
* every replica's copy lands inside the same short window.
*
* **State the guarantee WITH its window.** The lock `CronJobAdapter.runScheduled()`
* takes is held for the duration of the fire, not for the deadline — its
* mutual-exclusion window is the handler's runtime. So the routing above
* de-duplicates *concurrent* fires: exactly-once per deadline holds only
* when replica clocks agree to within that window (NTP-synced deployments,
* the normal case). A replica whose clock lags past the window finds the
* lock already released and reruns the job, and `once` is the sharpest case
* because there is no later tick to self-correct that away.
*
* **`once` is AT-MOST-ONCE per cluster, and deliberately so** (maintainer
* ruling 2026-09-01). Election decides *who* fires, never *that* the fire
* survives: there is no second deadline, so a leader that dies mid-fire loses
Expand Down
Loading