@objectstack/service-queue 17.0.0-rc.3 → 17.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +251 -7
- package/README.md +29 -0
- package/dist/index.cjs +101 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -2
- package/dist/index.d.ts +76 -2
- package/dist/index.js +99 -3
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,257 @@
|
|
|
1
1
|
# @objectstack/service-queue
|
|
2
2
|
|
|
3
|
-
## 17.0.0-rc.
|
|
4
|
-
|
|
5
|
-
###
|
|
6
|
-
|
|
3
|
+
## 17.0.0-rc.4
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e98fb14: fix(service-queue): `sys_job_queue` no longer grows forever — `completed` rows expire on a declared 7-day retention (#5179)
|
|
8
|
+
|
|
9
|
+
`DbQueueAdapter` marked a delivered message `status: 'completed'` and then
|
|
10
|
+
**nothing ever touched that row again**. `purge()` had zero production callers
|
|
11
|
+
(tests only), `purgeFailed()` is a manual dead-letter API, and the object
|
|
12
|
+
declared no lifecycle policy at all — so every queue delivery left a permanent
|
|
13
|
+
row, which since #5160 means one permanent row per queued email.
|
|
14
|
+
|
|
15
|
+
`sys_job_queue` now declares an ADR-0057 policy and the platform
|
|
16
|
+
`LifecycleService` enforces it on its existing hourly sweep:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
lifecycle: {
|
|
20
|
+
class: 'transient',
|
|
21
|
+
retention: { maxAge: '7d', onlyWhen: { status: 'completed' } },
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Only `completed` rows are swept.** `pending` / `running` are live work, and
|
|
26
|
+
`failed` / `dlq` are the dead-letter queue — they exist to wait for a human, so
|
|
27
|
+
they are never deleted automatically at any age. `listFailed()` / `replay()` /
|
|
28
|
+
`purgeFailed()` remain the only way a dead letter leaves the table. This is
|
|
29
|
+
also why the policy is `retention` (age + row filter) rather than a `ttl` on
|
|
30
|
+
`completed_at`: TTL has no row filter, and `dlq` rows stamp `completed_at` too.
|
|
31
|
+
|
|
32
|
+
**No new configuration, and no new sweeper.** ADR-0057 §3.3 puts one reaper in
|
|
33
|
+
the platform rather than one per plugin — the same call the sibling
|
|
34
|
+
`sys_job_run` (30d) already makes. Any kernel with a data engine already runs
|
|
35
|
+
it, its per-sweep `[lifecycle] sweep: … ~N rows reaped` line now accounts for
|
|
36
|
+
this table too, and the window is overridable per environment through the
|
|
37
|
+
`lifecycle` settings namespace without touching code.
|
|
38
|
+
|
|
39
|
+
**The dedup window is now an enforced invariant, not a coincidence.** Publish
|
|
40
|
+
dedups against a terminal row by comparing its `created_at` to
|
|
41
|
+
`idempotencyWindowMs` (default 24h), and the reaper cuts off on that same
|
|
42
|
+
`created_at` axis — so retention (7d) ≥ dedup window is what keeps "duplicate
|
|
43
|
+
publishes inside the window are suppressed" true. `DbQueueAdapter` reads the
|
|
44
|
+
declared window (new export `completedRetentionWindowMs()`) and **throws at
|
|
45
|
+
construction** if `idempotencyWindowMs` is configured longer than it, instead of
|
|
46
|
+
silently degrading into duplicate deliveries days later. If you raise
|
|
47
|
+
`idempotencyWindowMs` past 7 days, raise the object's declared retention (or the
|
|
48
|
+
`lifecycle` settings override) to match — the error message names both numbers.
|
|
49
|
+
|
|
50
|
+
`class: 'transient'` is deliberate: `telemetry`/`event`/`audit` classes
|
|
51
|
+
relocate their table to the dedicated `telemetry` datasource wherever one is
|
|
52
|
+
registered (ADR-0057 §3.6), and moving a live work queue's storage would be a
|
|
53
|
+
migration, not a cleanup.
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- 7c2f7dd: fix(objectql,service-queue): a `lifecycle` settings override can no longer undercut a consumer's retention floor (#5195)
|
|
58
|
+
|
|
59
|
+
ADR-0057 P4 lets an operator override any object's retention window per
|
|
60
|
+
environment and per tenant through the `lifecycle` settings namespace. Until now
|
|
61
|
+
the only validation on that override was **does it parse** — and a retention
|
|
62
|
+
window is not only the operator's business: other code can depend on the rows
|
|
63
|
+
still being there.
|
|
64
|
+
|
|
65
|
+
`sys_job_queue` is the worked example. `DbQueueAdapter` deduplicates publishes by
|
|
66
|
+
comparing a terminal row's `created_at` against its idempotency window, so the
|
|
67
|
+
dedup check only means anything while that row still exists; #5179 made the
|
|
68
|
+
ordering an invariant by refusing, at construction, an idempotency window longer
|
|
69
|
+
than the object's **declared** retention. A settings override the constructor
|
|
70
|
+
cannot see walks straight around it:
|
|
71
|
+
|
|
72
|
+
```jsonc
|
|
73
|
+
// lifecycle → retention_overrides
|
|
74
|
+
{ "sys_job_queue": { "maxAge": "1h" } }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
completed rows are reaped an hour after they are written, publish keeps
|
|
78
|
+
deduplicating against 24h, and duplicate deliveries resume **with nothing in any
|
|
79
|
+
log**.
|
|
80
|
+
|
|
81
|
+
**New: retention floors.** A consumer may now declare, at runtime, the shortest
|
|
82
|
+
window its own contract survives:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
lifecycle.registerRetentionFloor("sys_job_queue", {
|
|
86
|
+
policy: "retention", // or 'ttl'
|
|
87
|
+
minWindowMs: 24 * 60 * 60 * 1000,
|
|
88
|
+
declaredBy: "com.objectstack.service.queue",
|
|
89
|
+
consequence: "…what silently breaks below it",
|
|
90
|
+
remedy: "…the settings change that makes an override legal",
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- An override below the floor — **global or tenant-scoped** — is **rejected**,
|
|
95
|
+
and the declared window keeps running. Not clamped to the floor: clamping
|
|
96
|
+
would enforce a third number written in neither the declaration nor the
|
|
97
|
+
settings, and that number would move whenever an unrelated package changed
|
|
98
|
+
its floor. Rejection has exactly one fallback, the declaration, which is
|
|
99
|
+
already how an unparseable override resolves.
|
|
100
|
+
- The rejection is `error`-level and carries both the consequence and the fix,
|
|
101
|
+
because what it prevents leaves the system looking entirely healthy. It is
|
|
102
|
+
also on the sweep report as `LifecycleSweepReport.floorViolations` — machine-
|
|
103
|
+
readable, every sweep.
|
|
104
|
+
- A **declared** window below a registered floor is reported the same way and
|
|
105
|
+
still enforced: refusing to reap would trade a broken consumer contract for
|
|
106
|
+
the unbounded table #5179 just closed.
|
|
107
|
+
- Objects with no registered floor are completely unaffected — P4 overrides
|
|
108
|
+
behave exactly as before.
|
|
109
|
+
|
|
110
|
+
Floors are runtime wiring, not spec surface (the same call ADR-0057's reap-guard
|
|
111
|
+
amendment makes), plus a reason of their own: the queue's floor **is**
|
|
112
|
+
`DbQueueAdapterOptions.idempotencyWindowMs`, a per-kernel construction option, so
|
|
113
|
+
a static key on the object's `lifecycle` block could only ever be a second copy
|
|
114
|
+
of it that drifts. No `packages/spec` change.
|
|
115
|
+
|
|
116
|
+
`QueueServicePlugin` registers `sys_job_queue`'s floor on `kernel:ready`,
|
|
117
|
+
carrying the window the adapter was actually constructed with — so a non-default
|
|
118
|
+
`db.idempotencyWindowMs` is covered too. The ordering is now enforced from both
|
|
119
|
+
sides: the constructor rejects a too-long `idempotencyWindowMs`, the floor
|
|
120
|
+
rejects a too-short `maxAge`.
|
|
121
|
+
|
|
122
|
+
New exports from `@objectstack/objectql`: `LifecycleRetentionFloor`,
|
|
123
|
+
`LifecycleFloorViolation`, plus `LifecycleService.registerRetentionFloor()`.
|
|
124
|
+
`LifecycleLoggerLike` gained an optional `error()` (absent ⇒ falls back to
|
|
125
|
+
`warn`), and `LifecycleSweepReport` gained `floorViolations`.
|
|
126
|
+
|
|
127
|
+
- Updated dependencies [9fe9c1d]
|
|
128
|
+
- Updated dependencies [d4e0809]
|
|
129
|
+
- Updated dependencies [f724f69]
|
|
130
|
+
- Updated dependencies [28ad90e]
|
|
131
|
+
- Updated dependencies [f8644c7]
|
|
132
|
+
- Updated dependencies [306ca50]
|
|
133
|
+
- Updated dependencies [978fed2]
|
|
134
|
+
- Updated dependencies [cfc293f]
|
|
135
|
+
- Updated dependencies [de70b42]
|
|
136
|
+
- Updated dependencies [fb3d99b]
|
|
137
|
+
- Updated dependencies [cdfbee2]
|
|
138
|
+
- Updated dependencies [29c6c9d]
|
|
139
|
+
- Updated dependencies [d21c001]
|
|
140
|
+
- Updated dependencies [f1cc3a3]
|
|
141
|
+
- Updated dependencies [ddc2527]
|
|
142
|
+
- Updated dependencies [553a47f]
|
|
143
|
+
- Updated dependencies [a3a884d]
|
|
144
|
+
- Updated dependencies [cfed092]
|
|
7
145
|
- Updated dependencies [2e284b2]
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
146
|
+
- Updated dependencies [1b49eaf]
|
|
147
|
+
- Updated dependencies [0161c7f]
|
|
148
|
+
- Updated dependencies [e900015]
|
|
149
|
+
- Updated dependencies [b5bdf48]
|
|
150
|
+
- Updated dependencies [a019e52]
|
|
151
|
+
- Updated dependencies [64fc6d5]
|
|
152
|
+
- Updated dependencies [b746aa0]
|
|
153
|
+
- Updated dependencies [947d4f9]
|
|
154
|
+
- Updated dependencies [eaaf03c]
|
|
155
|
+
- Updated dependencies [d17df80]
|
|
156
|
+
- Updated dependencies [7d0e7b5]
|
|
157
|
+
- Updated dependencies [6513c17]
|
|
158
|
+
- Updated dependencies [c142ced]
|
|
159
|
+
- Updated dependencies [eda599e]
|
|
160
|
+
- Updated dependencies [c001422]
|
|
161
|
+
- Updated dependencies [77022a9]
|
|
162
|
+
- Updated dependencies [52760bf]
|
|
163
|
+
- Updated dependencies [5543020]
|
|
164
|
+
- Updated dependencies [880d343]
|
|
165
|
+
- Updated dependencies [6e82972]
|
|
166
|
+
- Updated dependencies [4615a18]
|
|
167
|
+
- Updated dependencies [7f62706]
|
|
168
|
+
- Updated dependencies [667fa44]
|
|
169
|
+
- Updated dependencies [37e38d1]
|
|
170
|
+
- Updated dependencies [1eb13a0]
|
|
171
|
+
- Updated dependencies [c52e608]
|
|
172
|
+
- Updated dependencies [4dfd002]
|
|
173
|
+
- Updated dependencies [77be690]
|
|
174
|
+
- Updated dependencies [811c30c]
|
|
175
|
+
- Updated dependencies [b49ccfd]
|
|
176
|
+
- Updated dependencies [85d95e7]
|
|
177
|
+
- Updated dependencies [168f60f]
|
|
178
|
+
- Updated dependencies [244ca86]
|
|
179
|
+
- Updated dependencies [546ab3c]
|
|
180
|
+
- Updated dependencies [0b51bb6]
|
|
181
|
+
- Updated dependencies [d9971d3]
|
|
182
|
+
- Updated dependencies [eb3e650]
|
|
183
|
+
- Updated dependencies [abeb375]
|
|
184
|
+
- Updated dependencies [ef4efa8]
|
|
185
|
+
- Updated dependencies [cbb6a5c]
|
|
186
|
+
- Updated dependencies [795b6e1]
|
|
187
|
+
- Updated dependencies [175d789]
|
|
188
|
+
- Updated dependencies [55dbbba]
|
|
189
|
+
- Updated dependencies [72c3c86]
|
|
190
|
+
- Updated dependencies [7f1a635]
|
|
191
|
+
- Updated dependencies [e98fb14]
|
|
192
|
+
- Updated dependencies [0f2fdcd]
|
|
193
|
+
- Updated dependencies [8ffa8b9]
|
|
194
|
+
- Updated dependencies [674ac99]
|
|
195
|
+
- Updated dependencies [1b9a53b]
|
|
196
|
+
- Updated dependencies [502564d]
|
|
197
|
+
- Updated dependencies [471839d]
|
|
198
|
+
- Updated dependencies [46365ab]
|
|
199
|
+
- Updated dependencies [b508244]
|
|
200
|
+
- Updated dependencies [594508e]
|
|
201
|
+
- Updated dependencies [1c625ca]
|
|
202
|
+
- Updated dependencies [71f205d]
|
|
203
|
+
- Updated dependencies [414395b]
|
|
204
|
+
- Updated dependencies [c5adfe1]
|
|
205
|
+
- Updated dependencies [26e1029]
|
|
206
|
+
- Updated dependencies [108ba8d]
|
|
207
|
+
- Updated dependencies [b4ad984]
|
|
208
|
+
- Updated dependencies [a9f32df]
|
|
209
|
+
- Updated dependencies [aeb9b27]
|
|
210
|
+
- Updated dependencies [7d27da0]
|
|
211
|
+
- Updated dependencies [089767f]
|
|
212
|
+
- Updated dependencies [e4c8b6c]
|
|
213
|
+
- Updated dependencies [acb10f6]
|
|
214
|
+
- Updated dependencies [1c3da1f]
|
|
215
|
+
- Updated dependencies [a34fd2e]
|
|
216
|
+
- Updated dependencies [889ae47]
|
|
217
|
+
- Updated dependencies [4f4c3fb]
|
|
218
|
+
- Updated dependencies [7adc841]
|
|
219
|
+
- Updated dependencies [4845f85]
|
|
220
|
+
- Updated dependencies [7b005b4]
|
|
221
|
+
- Updated dependencies [94f7b6a]
|
|
222
|
+
- Updated dependencies [5c94f83]
|
|
223
|
+
- Updated dependencies [73e576f]
|
|
224
|
+
- Updated dependencies [c5a5996]
|
|
225
|
+
- Updated dependencies [ae490ef]
|
|
226
|
+
- Updated dependencies [f61c8cf]
|
|
227
|
+
- Updated dependencies [e3ef52b]
|
|
228
|
+
- Updated dependencies [07f1822]
|
|
229
|
+
- Updated dependencies [04fab5e]
|
|
230
|
+
- Updated dependencies [efedd28]
|
|
231
|
+
- Updated dependencies [5278e11]
|
|
232
|
+
- Updated dependencies [23dba62]
|
|
233
|
+
- Updated dependencies [ba98e26]
|
|
234
|
+
- Updated dependencies [f104bab]
|
|
235
|
+
- Updated dependencies [fc5f536]
|
|
236
|
+
- Updated dependencies [f8cfbb4]
|
|
237
|
+
- Updated dependencies [c89d18c]
|
|
238
|
+
- Updated dependencies [aac90a5]
|
|
239
|
+
- Updated dependencies [1e6ab15]
|
|
240
|
+
- Updated dependencies [c87ef70]
|
|
241
|
+
- Updated dependencies [3cb0618]
|
|
242
|
+
- Updated dependencies [32a0874]
|
|
243
|
+
- Updated dependencies [7055c22]
|
|
244
|
+
- Updated dependencies [785a748]
|
|
245
|
+
- Updated dependencies [3af0354]
|
|
246
|
+
- Updated dependencies [866ff16]
|
|
247
|
+
- Updated dependencies [5a85e67]
|
|
248
|
+
- Updated dependencies [c183a12]
|
|
249
|
+
- Updated dependencies [8064b07]
|
|
250
|
+
- Updated dependencies [4a56dbd]
|
|
251
|
+
- Updated dependencies [06df4fa]
|
|
252
|
+
- @objectstack/spec@17.0.0-rc.4
|
|
253
|
+
- @objectstack/core@17.0.0-rc.4
|
|
254
|
+
- @objectstack/platform-objects@17.0.0-rc.4
|
|
11
255
|
|
|
12
256
|
## 17.0.0-rc.2
|
|
13
257
|
|
package/README.md
CHANGED
|
@@ -70,6 +70,35 @@ new QueueServicePlugin({
|
|
|
70
70
|
new QueueServicePlugin({ adapter: 'memory' });
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
### Retention — how `sys_job_queue` stays bounded
|
|
74
|
+
|
|
75
|
+
Delivered messages are not kept forever. `sys_job_queue` declares an ADR-0057
|
|
76
|
+
lifecycle policy and the platform `LifecycleService` (shipped with
|
|
77
|
+
`@objectstack/objectql`, armed on every kernel that has data) enforces it — no
|
|
78
|
+
configuration, no extra scheduler:
|
|
79
|
+
|
|
80
|
+
| Row state | What happens |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `completed` | deleted **7 days** after `created_at` |
|
|
83
|
+
| `pending` / `running` | never swept — live work |
|
|
84
|
+
| `failed` / `dlq` | never swept — the dead-letter queue waits for a human (`listFailed` / `replay` / `purgeFailed`) |
|
|
85
|
+
|
|
86
|
+
Two consequences worth knowing:
|
|
87
|
+
|
|
88
|
+
- **`idempotencyWindowMs` must not exceed the retention window.** Dedup against
|
|
89
|
+
a terminal message compares its `created_at` to that window, so a longer
|
|
90
|
+
setting would start accepting duplicates the moment the row was swept. The
|
|
91
|
+
`db` adapter throws at construction instead of degrading quietly.
|
|
92
|
+
- **The window is overridable per environment** through the `lifecycle`
|
|
93
|
+
settings namespace (`maxAge` per object), like every other ADR-0057 policy —
|
|
94
|
+
but **not below the idempotency window**. On startup this plugin registers a
|
|
95
|
+
*retention floor* with the `LifecycleService` carrying the window the adapter
|
|
96
|
+
was actually constructed with; a global or tenant-scoped override under it is
|
|
97
|
+
**rejected** at sweep time (the declared window keeps running) and logged at
|
|
98
|
+
`error` naming the consequence and the two settings that would make it legal.
|
|
99
|
+
So the ordering is enforced from both sides: the constructor rejects a
|
|
100
|
+
too-long `idempotencyWindowMs`, the floor rejects a too-short `maxAge`.
|
|
101
|
+
|
|
73
102
|
## Service API
|
|
74
103
|
|
|
75
104
|
Implements `IQueueService` from `@objectstack/spec/contracts`:
|
package/dist/index.cjs
CHANGED
|
@@ -22,12 +22,13 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DbQueueAdapter: () => DbQueueAdapter,
|
|
24
24
|
MemoryQueueAdapter: () => MemoryQueueAdapter,
|
|
25
|
-
QueueServicePlugin: () => QueueServicePlugin
|
|
25
|
+
QueueServicePlugin: () => QueueServicePlugin,
|
|
26
|
+
completedRetentionWindowMs: () => completedRetentionWindowMs
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
28
29
|
|
|
29
30
|
// src/queue-service-plugin.ts
|
|
30
|
-
var
|
|
31
|
+
var import_audit2 = require("@objectstack/platform-objects/audit");
|
|
31
32
|
|
|
32
33
|
// src/memory-queue-adapter.ts
|
|
33
34
|
var MemoryQueueAdapter = class {
|
|
@@ -83,6 +84,9 @@ var MemoryQueueAdapter = class {
|
|
|
83
84
|
}
|
|
84
85
|
};
|
|
85
86
|
|
|
87
|
+
// src/db-queue-adapter.ts
|
|
88
|
+
var import_audit = require("@objectstack/platform-objects/audit");
|
|
89
|
+
|
|
86
90
|
// src/common.ts
|
|
87
91
|
var SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
|
|
88
92
|
function uid(prefix) {
|
|
@@ -93,6 +97,21 @@ function uid(prefix) {
|
|
|
93
97
|
function nowIso(clock) {
|
|
94
98
|
return (clock?.now() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
95
99
|
}
|
|
100
|
+
var LIFECYCLE_UNIT_MS = {
|
|
101
|
+
h: 36e5,
|
|
102
|
+
d: 864e5,
|
|
103
|
+
w: 7 * 864e5,
|
|
104
|
+
y: 365 * 864e5
|
|
105
|
+
};
|
|
106
|
+
function lifecycleDurationMs(literal) {
|
|
107
|
+
const m = /^(\d+)(h|d|w|y)$/.exec(literal);
|
|
108
|
+
if (!m) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`[service-queue] invalid lifecycle duration literal '${literal}' \u2014 expected <n><unit> with unit h|d|w|y (e.g. '7d')`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return Number(m[1]) * LIFECYCLE_UNIT_MS[m[2]];
|
|
114
|
+
}
|
|
96
115
|
function parseJson(raw, fallback) {
|
|
97
116
|
if (raw == null) return fallback;
|
|
98
117
|
if (typeof raw === "string") {
|
|
@@ -108,6 +127,15 @@ function parseJson(raw, fallback) {
|
|
|
108
127
|
|
|
109
128
|
// src/db-queue-adapter.ts
|
|
110
129
|
var QUEUE_TABLE = "sys_job_queue";
|
|
130
|
+
function completedRetentionWindowMs() {
|
|
131
|
+
const maxAge = import_audit.SysJobQueue.lifecycle?.retention?.maxAge;
|
|
132
|
+
if (!maxAge) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"[service-queue] sys_job_queue no longer declares lifecycle.retention \u2014 DbQueueAdapter dedups against terminal rows by `created_at` window and relies on that declared retention to keep them (ADR-0057, #5179). Restore the declaration in @objectstack/platform-objects rather than sweeping the table from here."
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return lifecycleDurationMs(maxAge);
|
|
138
|
+
}
|
|
111
139
|
var DbQueueAdapter = class {
|
|
112
140
|
constructor(args) {
|
|
113
141
|
this.handlers = /* @__PURE__ */ new Map();
|
|
@@ -125,6 +153,42 @@ var DbQueueAdapter = class {
|
|
|
125
153
|
autoStart: o.autoStart ?? true,
|
|
126
154
|
workerId: o.workerId ?? uid("worker")
|
|
127
155
|
};
|
|
156
|
+
const retentionMs = completedRetentionWindowMs();
|
|
157
|
+
if (this.opts.idempotencyWindowMs > retentionMs) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`[service-queue] idempotencyWindowMs (${this.opts.idempotencyWindowMs}ms) exceeds the retention window sys_job_queue declares for completed rows (${retentionMs}ms, lifecycle.retention.maxAge \u2014 ADR-0057). Terminal-row dedup is evaluated by \`created_at\` against that same window, so the longer setting would silently accept duplicates once a row is reaped. Lower idempotencyWindowMs, or raise the declared retention (both windows are measured from \`created_at\`).`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** The configured dedup window (ms) — the number the floor below is made of. */
|
|
164
|
+
get idempotencyWindowMs() {
|
|
165
|
+
return this.opts.idempotencyWindowMs;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* [#5195] The retention floor `sys_job_queue` must satisfy for this adapter's
|
|
169
|
+
* dedup contract to mean anything, handed to `LifecycleService`
|
|
170
|
+
* (`registerRetentionFloor`) by `QueueServicePlugin`.
|
|
171
|
+
*
|
|
172
|
+
* The constructor check above only reads the object's **declaration**. ADR-0057
|
|
173
|
+
* P4 lets an operator override that window per environment/tenant through the
|
|
174
|
+
* `lifecycle` settings namespace, which the constructor cannot see: set
|
|
175
|
+
* `lifecycle.retention_overrides.sys_job_queue.maxAge = '1h'` and completed
|
|
176
|
+
* rows vanish an hour after they are written while publish keeps dedupping
|
|
177
|
+
* against a 24h window — duplicate deliveries resume, with nothing in any log.
|
|
178
|
+
* Registering the floor is what closes that door, and it carries the number
|
|
179
|
+
* this adapter was actually CONSTRUCTED with rather than a static copy of the
|
|
180
|
+
* default (a per-kernel option cannot live in the object's declaration).
|
|
181
|
+
*/
|
|
182
|
+
retentionFloor() {
|
|
183
|
+
const ms = this.opts.idempotencyWindowMs;
|
|
184
|
+
const literal = `${Math.ceil(ms / 36e5)}h`;
|
|
185
|
+
return {
|
|
186
|
+
policy: "retention",
|
|
187
|
+
minWindowMs: ms,
|
|
188
|
+
declaredBy: "com.objectstack.service.queue",
|
|
189
|
+
consequence: `DbQueueAdapter dedups sys_job_queue publishes by comparing created_at against its ${ms}ms idempotency window, so a shorter retention deletes the very rows that check reads \u2014 duplicate deliveries resume silently, with nothing in any log.`,
|
|
190
|
+
remedy: `set lifecycle.retention_overrides.sys_job_queue.maxAge to '${literal}' or longer, or lower QueueServicePlugin's db.idempotencyWindowMs to the window you actually want (both are measured from created_at).`
|
|
191
|
+
};
|
|
128
192
|
}
|
|
129
193
|
// ── IQueueService ────────────────────────────────────────────────
|
|
130
194
|
async publish(queue, data, options) {
|
|
@@ -456,7 +520,7 @@ var QueueServicePlugin = class {
|
|
|
456
520
|
scope: "system",
|
|
457
521
|
defaultDatasource: "cloud",
|
|
458
522
|
namespace: "sys",
|
|
459
|
-
objects: [
|
|
523
|
+
objects: [import_audit2.SysJobQueue]
|
|
460
524
|
});
|
|
461
525
|
} catch (err) {
|
|
462
526
|
ctx.logger.warn("QueueServicePlugin: manifest service unavailable; sys_job_queue not registered", err);
|
|
@@ -492,6 +556,7 @@ var QueueServicePlugin = class {
|
|
|
492
556
|
logger: ctx.logger,
|
|
493
557
|
options: this.options.db
|
|
494
558
|
});
|
|
559
|
+
this.registerRetentionFloor(ctx, this.dbAdapter);
|
|
495
560
|
try {
|
|
496
561
|
ctx.replaceService?.("queue", this.dbAdapter);
|
|
497
562
|
this.dbAdapter.start();
|
|
@@ -501,6 +566,37 @@ var QueueServicePlugin = class {
|
|
|
501
566
|
}
|
|
502
567
|
});
|
|
503
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* [#5195] Register the adapter's retention floor with the platform
|
|
571
|
+
* LifecycleService. Best-effort: a kernel without a lifecycle service has no
|
|
572
|
+
* sweeper either, so there is no override for anything to bypass.
|
|
573
|
+
*
|
|
574
|
+
* The lookup is typed to {@link LifecycleFloorRegistrar} — the slot's
|
|
575
|
+
* contract as this package consumes it — rather than erased to `any`
|
|
576
|
+
* (#4127/#4251). The `typeof … === 'function'` probe stays because the method
|
|
577
|
+
* is genuinely optional (a lifecycle service predating floors), but it is now
|
|
578
|
+
* a check the compiler can see rather than one `any` was hiding.
|
|
579
|
+
*/
|
|
580
|
+
registerRetentionFloor(ctx, adapter) {
|
|
581
|
+
let lifecycle;
|
|
582
|
+
try {
|
|
583
|
+
lifecycle = ctx.getService("lifecycle");
|
|
584
|
+
} catch {
|
|
585
|
+
lifecycle = void 0;
|
|
586
|
+
}
|
|
587
|
+
if (!lifecycle || typeof lifecycle.registerRetentionFloor !== "function") return;
|
|
588
|
+
try {
|
|
589
|
+
lifecycle.registerRetentionFloor(import_audit2.SysJobQueue.name, adapter.retentionFloor());
|
|
590
|
+
ctx.logger.info(
|
|
591
|
+
`QueueServicePlugin: registered a ${adapter.idempotencyWindowMs}ms retention floor on ${import_audit2.SysJobQueue.name} with the lifecycle service (settings overrides below it are rejected)`
|
|
592
|
+
);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
ctx.logger.error(
|
|
595
|
+
"QueueServicePlugin: the lifecycle service rejected the sys_job_queue retention floor. A `lifecycle` settings override may now shorten sys_job_queue.retention below the idempotency window, in which case publish would silently re-accept duplicates (#5195). Fix the floor registration, or keep lifecycle.retention_overrides.sys_job_queue unset.",
|
|
596
|
+
err
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
504
600
|
async destroy() {
|
|
505
601
|
await this.dbAdapter?.stop();
|
|
506
602
|
}
|
|
@@ -509,6 +605,7 @@ var QueueServicePlugin = class {
|
|
|
509
605
|
0 && (module.exports = {
|
|
510
606
|
DbQueueAdapter,
|
|
511
607
|
MemoryQueueAdapter,
|
|
512
|
-
QueueServicePlugin
|
|
608
|
+
QueueServicePlugin,
|
|
609
|
+
completedRetentionWindowMs
|
|
513
610
|
});
|
|
514
611
|
//# sourceMappingURL=index.cjs.map
|