@friggframework/core 2.0.0--canary.657.8f335eb.0 → 2.0.0--canary.657.b14221f.0
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/CLAUDE.md +6 -3
- package/core/Worker.js +98 -53
- package/handlers/backend-utils.js +21 -2
- package/package.json +5 -5
package/CLAUDE.md
CHANGED
|
@@ -570,9 +570,12 @@ back so it runs at `retryAt`:
|
|
|
570
570
|
would be lost with the acknowledged message.
|
|
571
571
|
- With a `processId` in the message, the worker writes
|
|
572
572
|
`Process.context.rateLimit = { status: 'WAITING' | 'EXHAUSTED', mechanism,
|
|
573
|
-
retryAt, reason, module, deferrals, updatedAt }
|
|
574
|
-
|
|
575
|
-
|
|
573
|
+
retryAt, reason, module, deferrals, updatedAt }`. `EXHAUSTED` means a cap ended
|
|
574
|
+
the deferrals, or a visibility change came on the last delivery. The value is
|
|
575
|
+
one per process, so it holds the latest known wait: a new wait replaces it only
|
|
576
|
+
when its `retryAt` is later, a `WAITING` value never hides an `EXHAUSTED` one,
|
|
577
|
+
and after a deferred or redelivered message ran the worker sets it to `null`
|
|
578
|
+
only when it is `WAITING` and its `retryAt` is past or within 5 s.
|
|
576
579
|
- Delivery stays at least once. The new message is sent before the old one is
|
|
577
580
|
acknowledged, so a crash between the two can run the work twice.
|
|
578
581
|
- Handlers check `delivery.isLastAttempt` first, then rethrow. The delay and
|
package/core/Worker.js
CHANGED
|
@@ -182,60 +182,101 @@ class Worker {
|
|
|
182
182
|
return { outcome: 'failed' };
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
185
|
+
const tier = {
|
|
186
|
+
record,
|
|
187
|
+
queueName,
|
|
188
|
+
body,
|
|
189
|
+
next: withDeferral(body, deferral),
|
|
190
|
+
error,
|
|
191
|
+
delivery,
|
|
192
|
+
retryAt,
|
|
193
|
+
waitMs,
|
|
194
|
+
details,
|
|
195
|
+
state,
|
|
196
|
+
};
|
|
197
|
+
if (waitMs <= MAX_DELAY_SECONDS * 1000)
|
|
198
|
+
return this._deferWithDelay(tier);
|
|
199
|
+
const scheduled = await this._deferWithSchedule(tier);
|
|
200
|
+
if (scheduled?.outcome) return scheduled;
|
|
201
|
+
return this._deferWithVisibility({
|
|
202
|
+
...tier,
|
|
203
|
+
scheduleError: scheduled?.scheduleError,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async _deferWithDelay({
|
|
208
|
+
queueName,
|
|
209
|
+
body,
|
|
210
|
+
next,
|
|
211
|
+
error,
|
|
212
|
+
waitMs,
|
|
213
|
+
details,
|
|
214
|
+
state,
|
|
215
|
+
}) {
|
|
216
|
+
const delaySeconds = Math.ceil(waitMs / 1000);
|
|
217
|
+
try {
|
|
218
|
+
const queueUrl = await this._queueUrl(queueName);
|
|
219
|
+
await this.send({ ...next, QueueUrl: queueUrl }, delaySeconds);
|
|
220
|
+
} catch (cause) {
|
|
221
|
+
return { outcome: 'skipped', skipped: 'send_failed', cause };
|
|
204
222
|
}
|
|
223
|
+
await this._recordWait(body, error, state('WAITING', 'delay'));
|
|
224
|
+
log.warn('Record deferred', {
|
|
225
|
+
eventName: 'frigg.worker.record_deferred',
|
|
226
|
+
mechanism: 'delay',
|
|
227
|
+
delaySeconds,
|
|
228
|
+
...details,
|
|
229
|
+
error,
|
|
230
|
+
});
|
|
231
|
+
return { outcome: 'acked' };
|
|
232
|
+
}
|
|
205
233
|
|
|
206
|
-
|
|
234
|
+
/** `acked`, `{ scheduleError }` when the schedule failed, null with no scheduler. */
|
|
235
|
+
async _deferWithSchedule({
|
|
236
|
+
record,
|
|
237
|
+
body,
|
|
238
|
+
next,
|
|
239
|
+
error,
|
|
240
|
+
retryAt,
|
|
241
|
+
details,
|
|
242
|
+
state,
|
|
243
|
+
}) {
|
|
207
244
|
const scheduler = this.getSchedulerService();
|
|
208
|
-
if (scheduler)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (cause?.name !== 'ConflictException') scheduleError = cause;
|
|
221
|
-
}
|
|
222
|
-
if (!scheduleError) {
|
|
223
|
-
await this._recordWait(
|
|
224
|
-
body,
|
|
225
|
-
error,
|
|
226
|
-
state('WAITING', 'schedule')
|
|
227
|
-
);
|
|
228
|
-
log.warn('Record deferred', {
|
|
229
|
-
eventName: 'frigg.worker.record_deferred',
|
|
230
|
-
mechanism: 'schedule',
|
|
231
|
-
scheduleName,
|
|
232
|
-
...details,
|
|
233
|
-
error,
|
|
234
|
-
});
|
|
235
|
-
return { outcome: 'acked' };
|
|
236
|
-
}
|
|
245
|
+
if (!scheduler) return null;
|
|
246
|
+
const scheduleName = `frigg-defer-${record.messageId || randomUUID()}`;
|
|
247
|
+
try {
|
|
248
|
+
await scheduler.scheduleOneTime({
|
|
249
|
+
scheduleName,
|
|
250
|
+
scheduleAt: retryAt,
|
|
251
|
+
queueResourceId: record.eventSourceARN,
|
|
252
|
+
payload: next,
|
|
253
|
+
});
|
|
254
|
+
} catch (cause) {
|
|
255
|
+
if (cause?.name !== 'ConflictException')
|
|
256
|
+
return { scheduleError: cause };
|
|
237
257
|
}
|
|
258
|
+
await this._recordWait(body, error, state('WAITING', 'schedule'));
|
|
259
|
+
log.warn('Record deferred', {
|
|
260
|
+
eventName: 'frigg.worker.record_deferred',
|
|
261
|
+
mechanism: 'schedule',
|
|
262
|
+
scheduleName,
|
|
263
|
+
...details,
|
|
264
|
+
error,
|
|
265
|
+
});
|
|
266
|
+
return { outcome: 'acked' };
|
|
267
|
+
}
|
|
238
268
|
|
|
269
|
+
async _deferWithVisibility({
|
|
270
|
+
record,
|
|
271
|
+
queueName,
|
|
272
|
+
body,
|
|
273
|
+
error,
|
|
274
|
+
delivery,
|
|
275
|
+
waitMs,
|
|
276
|
+
details,
|
|
277
|
+
state,
|
|
278
|
+
scheduleError,
|
|
279
|
+
}) {
|
|
239
280
|
const visibilityTimeout = Math.min(
|
|
240
281
|
Math.ceil(waitMs / 1000),
|
|
241
282
|
MAX_VISIBILITY_TIMEOUT_SECONDS
|
|
@@ -301,13 +342,17 @@ class Worker {
|
|
|
301
342
|
* @param {Error} error The RateLimitError.
|
|
302
343
|
* @param {{status: 'WAITING'|'EXHAUSTED', mechanism: string, deferrals: number, retryAt: Date}} state
|
|
303
344
|
*/
|
|
304
|
-
async recordRateLimitWait() {
|
|
345
|
+
async recordRateLimitWait() {
|
|
346
|
+
// A subclass keeps the run state; nothing to do by default.
|
|
347
|
+
}
|
|
305
348
|
|
|
306
349
|
/**
|
|
307
350
|
* Hook: a message that was put back for a rate limit ran without error.
|
|
308
351
|
* @param {Object} body The parsed message body.
|
|
309
352
|
*/
|
|
310
|
-
async clearRateLimitWait() {
|
|
353
|
+
async clearRateLimitWait() {
|
|
354
|
+
// A subclass keeps the run state; nothing to do by default.
|
|
355
|
+
}
|
|
311
356
|
|
|
312
357
|
_queueUrl(queueName) {
|
|
313
358
|
this._queueUrls ??= new Map();
|
|
@@ -330,8 +375,8 @@ class Worker {
|
|
|
330
375
|
}
|
|
331
376
|
|
|
332
377
|
async _clearDeferredState(body, delivery) {
|
|
333
|
-
|
|
334
|
-
|
|
378
|
+
const redelivered = delivery.receiveCount > 1;
|
|
379
|
+
if (readDeferral(body).deferrals === 0 && !redelivered) return;
|
|
335
380
|
await this._runStateHook('clear', () => this.clearRateLimitWait(body));
|
|
336
381
|
}
|
|
337
382
|
|
|
@@ -22,6 +22,15 @@ const {
|
|
|
22
22
|
} = require('../integrations/utils/map-integration-dto');
|
|
23
23
|
const { processNotFound } = require('../integrations/use-cases/process-errors');
|
|
24
24
|
|
|
25
|
+
const CLEAR_RATE_LIMIT_TOLERANCE_MS = 5_000;
|
|
26
|
+
|
|
27
|
+
const keepsCurrentRateLimit = (current, next) => {
|
|
28
|
+
if (!current) return false;
|
|
29
|
+
if (next.status === 'EXHAUSTED') return false;
|
|
30
|
+
if (current.status === 'EXHAUSTED') return true;
|
|
31
|
+
return Date.parse(current.retryAt) > next.retryAt.getTime();
|
|
32
|
+
};
|
|
33
|
+
|
|
25
34
|
const loadRouterFromObject = (IntegrationClass, routerObject) => {
|
|
26
35
|
const router = Router();
|
|
27
36
|
const { path, method, event } = routerObject;
|
|
@@ -168,7 +177,11 @@ const createQueueWorker = (integrationClass) => {
|
|
|
168
177
|
async recordRateLimitWait(body, error, state) {
|
|
169
178
|
const processId = body.data?.processId;
|
|
170
179
|
if (!processId) return;
|
|
171
|
-
|
|
180
|
+
const processRepository = createProcessRepository();
|
|
181
|
+
const process = await processRepository.findById(processId);
|
|
182
|
+
if (keepsCurrentRateLimit(process?.context?.rateLimit, state))
|
|
183
|
+
return;
|
|
184
|
+
await processRepository.applyProcessUpdate(processId, {
|
|
172
185
|
set: {
|
|
173
186
|
'context.rateLimit': {
|
|
174
187
|
status: state.status,
|
|
@@ -186,7 +199,13 @@ const createQueueWorker = (integrationClass) => {
|
|
|
186
199
|
async clearRateLimitWait(body) {
|
|
187
200
|
const processId = body?.data?.processId;
|
|
188
201
|
if (!processId) return;
|
|
189
|
-
|
|
202
|
+
const processRepository = createProcessRepository();
|
|
203
|
+
const process = await processRepository.findById(processId);
|
|
204
|
+
const current = process?.context?.rateLimit;
|
|
205
|
+
if (current?.status !== 'WAITING') return;
|
|
206
|
+
const remainingMs = Date.parse(current.retryAt) - Date.now();
|
|
207
|
+
if (remainingMs > CLEAR_RATE_LIMIT_TOLERANCE_MS) return;
|
|
208
|
+
await processRepository.applyProcessUpdate(processId, {
|
|
190
209
|
set: { 'context.rateLimit': null },
|
|
191
210
|
});
|
|
192
211
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.657.
|
|
4
|
+
"version": "2.0.0--canary.657.b14221f.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@friggframework/eslint-config": "2.0.0--canary.657.
|
|
52
|
-
"@friggframework/prettier-config": "2.0.0--canary.657.
|
|
53
|
-
"@friggframework/test": "2.0.0--canary.657.
|
|
51
|
+
"@friggframework/eslint-config": "2.0.0--canary.657.b14221f.0",
|
|
52
|
+
"@friggframework/prettier-config": "2.0.0--canary.657.b14221f.0",
|
|
53
|
+
"@friggframework/test": "2.0.0--canary.657.b14221f.0",
|
|
54
54
|
"@prisma/client": "^6.19.3",
|
|
55
55
|
"@types/lodash": "4.17.15",
|
|
56
56
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"publishConfig": {
|
|
91
91
|
"access": "public"
|
|
92
92
|
},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "b14221fca51737284a61407b42e003637930465e"
|
|
94
94
|
}
|