@gobing-ai/ts-infra 0.5.3 → 0.5.5
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/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +13 -2
- package/dist/job-queue/db-job-queue.d.ts +4 -0
- package/dist/job-queue/db-job-queue.d.ts.map +1 -1
- package/dist/job-queue/db-job-queue.js +47 -17
- package/package.json +6 -6
- package/src/api-client.ts +14 -2
- package/src/job-queue/db-job-queue.ts +46 -19
package/dist/api-client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,kBAAkB,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAUhD,0GAA0G;AAC1G,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACnC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;CAC/C;AACD,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB;AAED,mHAAmH;AACnH,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACrD,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,uFAAuF;AACvF,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB;AACD;;;GAGG;AACH,qBAAa,QAAS,SAAQ,KAAK;aAEX,MAAM,EAAE,MAAM;aACd,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM;CAKnC;
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
package/dist/api-client.js
CHANGED
|
@@ -20,6 +20,13 @@ export class APIError extends Error {
|
|
|
20
20
|
this.name = 'APIError';
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/** Task 0086 R17: upper bound for error bodies kept on APIError. */
|
|
24
|
+
const MAX_ERROR_BODY = 4096;
|
|
25
|
+
function truncateBody(text) {
|
|
26
|
+
return text.length <= MAX_ERROR_BODY
|
|
27
|
+
? text
|
|
28
|
+
: `${text.slice(0, MAX_ERROR_BODY)}…[truncated ${text.length - MAX_ERROR_BODY} chars]`;
|
|
29
|
+
}
|
|
23
30
|
function sanitizeUrlForObservability(url) {
|
|
24
31
|
try {
|
|
25
32
|
const parsed = new URL(url);
|
|
@@ -165,7 +172,9 @@ export class APIClient {
|
|
|
165
172
|
'http.request.method': method,
|
|
166
173
|
'error.type': 'Timeout',
|
|
167
174
|
});
|
|
168
|
-
const timeoutError = new APIError(0,
|
|
175
|
+
const timeoutError = new APIError(0,
|
|
176
|
+
// Task 0086 R17: observable URL only — the raw URL can carry query-string secrets.
|
|
177
|
+
`Request timed out after ${timeoutMs}ms: ${method} ${observableUrl}`);
|
|
169
178
|
this.emitRequestError(method, observableUrl, errorForObservability(timeoutError));
|
|
170
179
|
callerError = timeoutError;
|
|
171
180
|
throw new Error(errorForObservability(timeoutError));
|
|
@@ -204,7 +213,9 @@ export class APIClient {
|
|
|
204
213
|
'http.request.method': method,
|
|
205
214
|
'error.type': `HTTP_${response.status}`,
|
|
206
215
|
});
|
|
207
|
-
|
|
216
|
+
// Task 0086 R17: bound the stored body — a huge HTML error
|
|
217
|
+
// page must not blow up logs or telemetry payloads.
|
|
218
|
+
throw new APIError(response.status, truncateBody(text));
|
|
208
219
|
}
|
|
209
220
|
const contentType = response.headers.get('content-type') ?? '';
|
|
210
221
|
if (contentType.includes('application/json')) {
|
|
@@ -39,6 +39,8 @@ export declare class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
39
39
|
private running;
|
|
40
40
|
private inFlight;
|
|
41
41
|
private pollPromise;
|
|
42
|
+
/** Task 0086 R6: set by stop() so an in-flight processOnce claims no further batches. */
|
|
43
|
+
private stopRequested;
|
|
42
44
|
constructor(dao: QueueJobDao, config?: QueueConsumerConfig);
|
|
43
45
|
register(type: string, handler: JobHandler<T>): void;
|
|
44
46
|
start(): Promise<void>;
|
|
@@ -47,6 +49,8 @@ export declare class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
47
49
|
stats(): Promise<QueueStats>;
|
|
48
50
|
/** Process one batch immediately. Useful for tests, schedulers, and manual drains. */
|
|
49
51
|
processOnce(): Promise<number>;
|
|
52
|
+
/** Run one claimed batch to settlement, tracking in-flight for drain. */
|
|
53
|
+
private runClaimed;
|
|
50
54
|
private schedule;
|
|
51
55
|
private poll;
|
|
52
56
|
private processJob;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db-job-queue.d.ts","sourceRoot":"","sources":["../../src/job-queue/db-job-queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAkB,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EACR,WAAW,EAKd,MAAM,WAAW,CAAC;AAWnB,OAAO,KAAK,EAAE,cAAc,EAAO,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAQ7G,kFAAkF;AAClF,qBAAa,UAAU,CAAC,CAAC,GAAG,OAAO,CAAE,YAAW,QAAQ,CAAC,CAAC,CAAC;IAEnD,QAAQ,CAAC,GAAG,EAAE,WAAW;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;gBADf,GAAG,EAAE,WAAW,EACR,MAAM,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,YAAA;IAG7C,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAO5E,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC,CAAA;KAAE,GAAG,cAAc,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAY3F,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;CAGrC;AAcD,sGAAsG;AACtG,qBAAa,eAAe,CAAC,CAAC,GAAG,OAAO,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"db-job-queue.d.ts","sourceRoot":"","sources":["../../src/job-queue/db-job-queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAkB,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EACR,WAAW,EAKd,MAAM,WAAW,CAAC;AAWnB,OAAO,KAAK,EAAE,cAAc,EAAO,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAQ7G,kFAAkF;AAClF,qBAAa,UAAU,CAAC,CAAC,GAAG,OAAO,CAAE,YAAW,QAAQ,CAAC,CAAC,CAAC;IAEnD,QAAQ,CAAC,GAAG,EAAE,WAAW;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;gBADf,GAAG,EAAE,WAAW,EACR,MAAM,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,YAAA;IAG7C,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAO5E,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC,CAAA;KAAE,GAAG,cAAc,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAY3F,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;CAGrC;AAcD,sGAAsG;AACtG,qBAAa,eAAe,CAAC,CAAC,GAAG,OAAO,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;IA2B7D,OAAO,CAAC,QAAQ,CAAC,GAAG;IA1BxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoC;IAChE,kGAAkG;IAClG,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAgB;IACjD,wFAAwF;IACxF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2E;IAC1G;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgE;IAC1F,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,WAAW,CAA8B;IACjD,yFAAyF;IACzF,OAAO,CAAC,aAAa,CAAS;gBAGT,GAAG,EAAE,WAAW,EACjC,MAAM,GAAE,mBAAwB;IAyBpC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;IAI9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAuDrB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQvC,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;IAIlC,sFAAsF;IAChF,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC;IA8BpC,yEAAyE;YAC3D,UAAU;IAcxB,OAAO,CAAC,QAAQ;YAWF,IAAI;YAgBJ,UAAU;IAsHxB;;;;;OAKG;YACW,iBAAiB;YA2BjB,WAAW;CAyC5B"}
|
|
@@ -77,6 +77,8 @@ export class DBQueueConsumer {
|
|
|
77
77
|
running = false;
|
|
78
78
|
inFlight = 0;
|
|
79
79
|
pollPromise = null;
|
|
80
|
+
/** Task 0086 R6: set by stop() so an in-flight processOnce claims no further batches. */
|
|
81
|
+
stopRequested = false;
|
|
80
82
|
constructor(dao, config = {}) {
|
|
81
83
|
this.dao = dao;
|
|
82
84
|
this.pollInterval = nonNegativeFiniteConfig('pollInterval', config.pollInterval ?? 1_000);
|
|
@@ -104,6 +106,7 @@ export class DBQueueConsumer {
|
|
|
104
106
|
if (this.running)
|
|
105
107
|
return;
|
|
106
108
|
this.running = true;
|
|
109
|
+
this.stopRequested = false;
|
|
107
110
|
this.schedule(0);
|
|
108
111
|
const sink = this.eventSink;
|
|
109
112
|
if (sink !== undefined) {
|
|
@@ -120,6 +123,7 @@ export class DBQueueConsumer {
|
|
|
120
123
|
}
|
|
121
124
|
async stop() {
|
|
122
125
|
const wasRunning = this.running;
|
|
126
|
+
this.stopRequested = true;
|
|
123
127
|
this.running = false;
|
|
124
128
|
if (this.timer !== null) {
|
|
125
129
|
clearTimeout(this.timer);
|
|
@@ -186,25 +190,42 @@ export class DBQueueConsumer {
|
|
|
186
190
|
return traceAsync('queue.poll', async () => {
|
|
187
191
|
await this.dao.resetStuckJobs(this.visibilityTimeout);
|
|
188
192
|
await this.dao.failExpiredJobs();
|
|
189
|
-
|
|
193
|
+
// Task 0086 R6: claim only as much as can still start. A stop()
|
|
194
|
+
// landing mid-cycle stops further claims; already-claimed attempts
|
|
195
|
+
// still settle. With defaults (maxConcurrency === batchSize) this is
|
|
196
|
+
// exactly one claim per cycle, the same as before.
|
|
197
|
+
let claimed = 0;
|
|
190
198
|
let processed = 0;
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
await
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
while (claimed < this.batchSize && !this.stopRequested) {
|
|
200
|
+
const requested = Math.min(this.maxConcurrency, this.batchSize - claimed);
|
|
201
|
+
const jobs = await this.dao.claimReady(requested, { leaseMs: this.visibilityTimeout });
|
|
202
|
+
// A short claim means the queue is drained: stop instead of
|
|
203
|
+
// re-arming jobs that were just retried within this cycle.
|
|
204
|
+
if (jobs.length < requested) {
|
|
205
|
+
claimed += jobs.length;
|
|
206
|
+
await this.runClaimed(jobs, () => (processed += 1));
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
claimed += jobs.length;
|
|
210
|
+
await this.runClaimed(jobs, () => (processed += 1));
|
|
203
211
|
}
|
|
204
|
-
addSpanAttributes({ 'queue.claimed':
|
|
212
|
+
addSpanAttributes({ 'queue.claimed': claimed, 'queue.processed': processed });
|
|
205
213
|
return processed;
|
|
206
214
|
});
|
|
207
215
|
}
|
|
216
|
+
/** Run one claimed batch to settlement, tracking in-flight for drain. */
|
|
217
|
+
async runClaimed(jobs, onProcessed) {
|
|
218
|
+
await Promise.all(jobs.map(async (job) => {
|
|
219
|
+
this.inFlight += 1;
|
|
220
|
+
try {
|
|
221
|
+
await this.processJob(job);
|
|
222
|
+
onProcessed();
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
this.inFlight -= 1;
|
|
226
|
+
}
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
208
229
|
schedule(delay) {
|
|
209
230
|
this.timer = setTimeout(() => {
|
|
210
231
|
// Retained so stop() can await the cycle it interrupts; poll() contains its
|
|
@@ -325,7 +346,11 @@ export class DBQueueConsumer {
|
|
|
325
346
|
}
|
|
326
347
|
}
|
|
327
348
|
else {
|
|
328
|
-
|
|
349
|
+
// Task 0086 R10: gate metric + event on the fenced write — a
|
|
350
|
+
// lost lease means another consumer owns the job now.
|
|
351
|
+
const applied = await this.dao.markCompleted(record.id, token);
|
|
352
|
+
if (!applied)
|
|
353
|
+
return;
|
|
329
354
|
getQueueJobCompletedTotal().add(1, { type: job.type });
|
|
330
355
|
const completed = {
|
|
331
356
|
jobId: job.id,
|
|
@@ -381,7 +406,9 @@ export class DBQueueConsumer {
|
|
|
381
406
|
const attempts = job.attempts + 1;
|
|
382
407
|
const message = error instanceof Error ? error.message : String(error);
|
|
383
408
|
if (attempts >= job.maxRetries) {
|
|
384
|
-
await this.dao.markFailed(job.id, attempts, message, attemptToken);
|
|
409
|
+
const applied = await this.dao.markFailed(job.id, attempts, message, attemptToken);
|
|
410
|
+
if (!applied)
|
|
411
|
+
return; // Task 0086 R10: fenced write lost — no metric, no event.
|
|
385
412
|
getQueueJobFailedTotal().add(1, { type: job.type });
|
|
386
413
|
const failed = {
|
|
387
414
|
jobId: job.id,
|
|
@@ -397,7 +424,10 @@ export class DBQueueConsumer {
|
|
|
397
424
|
}
|
|
398
425
|
const delay = Math.min(this.maxDelay, this.baseDelay * 2 ** Math.max(0, attempts - 1));
|
|
399
426
|
const nextRetryAt = Date.now() + delay;
|
|
400
|
-
|
|
427
|
+
// Task 0086 R10: gate the retry event on the fenced write.
|
|
428
|
+
const retryApplied = await this.dao.markForRetry(job.id, attempts, message, nextRetryAt, attemptToken);
|
|
429
|
+
if (!retryApplied)
|
|
430
|
+
return;
|
|
401
431
|
const retrying = {
|
|
402
432
|
jobId: job.id,
|
|
403
433
|
type: job.type,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-infra",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "@gobing-ai/ts-infra — Infrastructure backbone: event bus, job queue, scheduler, telemetry, API client, and logging.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -78,12 +78,12 @@
|
|
|
78
78
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-infra-v<version> && git push --tags' && exit 1"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"@gobing-ai/ts-utils": "^0.5.
|
|
81
|
+
"@gobing-ai/ts-utils": "^0.5.5",
|
|
82
82
|
"@logtape/logtape": "^2.0.0"
|
|
83
83
|
},
|
|
84
84
|
"peerDependencies": {
|
|
85
|
-
"@gobing-ai/ts-db": "^0.5.
|
|
86
|
-
"@gobing-ai/ts-runtime": "^0.5.
|
|
85
|
+
"@gobing-ai/ts-db": "^0.5.5",
|
|
86
|
+
"@gobing-ai/ts-runtime": "^0.5.5",
|
|
87
87
|
"@opentelemetry/api": "^1.9.0",
|
|
88
88
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
89
89
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
@@ -116,8 +116,8 @@
|
|
|
116
116
|
}
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
|
-
"@gobing-ai/ts-db": "^0.5.
|
|
120
|
-
"@gobing-ai/ts-runtime": "^0.5.
|
|
119
|
+
"@gobing-ai/ts-db": "^0.5.5",
|
|
120
|
+
"@gobing-ai/ts-runtime": "^0.5.5",
|
|
121
121
|
"@types/bun": "1.3.14",
|
|
122
122
|
"@opentelemetry/api": "^1.9.0",
|
|
123
123
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
package/src/api-client.ts
CHANGED
|
@@ -75,6 +75,15 @@ export class APIError extends Error {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** Task 0086 R17: upper bound for error bodies kept on APIError. */
|
|
79
|
+
const MAX_ERROR_BODY = 4096;
|
|
80
|
+
|
|
81
|
+
function truncateBody(text: string): string {
|
|
82
|
+
return text.length <= MAX_ERROR_BODY
|
|
83
|
+
? text
|
|
84
|
+
: `${text.slice(0, MAX_ERROR_BODY)}…[truncated ${text.length - MAX_ERROR_BODY} chars]`;
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
function sanitizeUrlForObservability(url: string): string {
|
|
79
88
|
try {
|
|
80
89
|
const parsed = new URL(url);
|
|
@@ -250,7 +259,8 @@ export class APIClient {
|
|
|
250
259
|
});
|
|
251
260
|
const timeoutError = new APIError(
|
|
252
261
|
0,
|
|
253
|
-
|
|
262
|
+
// Task 0086 R17: observable URL only — the raw URL can carry query-string secrets.
|
|
263
|
+
`Request timed out after ${timeoutMs}ms: ${method} ${observableUrl}`,
|
|
254
264
|
);
|
|
255
265
|
this.emitRequestError(method, observableUrl, errorForObservability(timeoutError));
|
|
256
266
|
callerError = timeoutError;
|
|
@@ -305,7 +315,9 @@ export class APIClient {
|
|
|
305
315
|
'http.request.method': method,
|
|
306
316
|
'error.type': `HTTP_${response.status}`,
|
|
307
317
|
});
|
|
308
|
-
|
|
318
|
+
// Task 0086 R17: bound the stored body — a huge HTML error
|
|
319
|
+
// page must not blow up logs or telemetry payloads.
|
|
320
|
+
throw new APIError(response.status, truncateBody(text));
|
|
309
321
|
}
|
|
310
322
|
|
|
311
323
|
const contentType = response.headers.get('content-type') ?? '';
|
|
@@ -92,6 +92,8 @@ export class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
92
92
|
private running = false;
|
|
93
93
|
private inFlight = 0;
|
|
94
94
|
private pollPromise: Promise<void> | null = null;
|
|
95
|
+
/** Task 0086 R6: set by stop() so an in-flight processOnce claims no further batches. */
|
|
96
|
+
private stopRequested = false;
|
|
95
97
|
|
|
96
98
|
constructor(
|
|
97
99
|
private readonly dao: QueueJobDao,
|
|
@@ -127,6 +129,7 @@ export class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
127
129
|
async start(): Promise<void> {
|
|
128
130
|
if (this.running) return;
|
|
129
131
|
this.running = true;
|
|
132
|
+
this.stopRequested = false;
|
|
130
133
|
this.schedule(0);
|
|
131
134
|
const sink = this.eventSink;
|
|
132
135
|
if (sink !== undefined) {
|
|
@@ -144,6 +147,7 @@ export class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
144
147
|
|
|
145
148
|
async stop(): Promise<void> {
|
|
146
149
|
const wasRunning = this.running;
|
|
150
|
+
this.stopRequested = true;
|
|
147
151
|
this.running = false;
|
|
148
152
|
if (this.timer !== null) {
|
|
149
153
|
clearTimeout(this.timer);
|
|
@@ -214,29 +218,46 @@ export class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
214
218
|
await this.dao.resetStuckJobs(this.visibilityTimeout);
|
|
215
219
|
await this.dao.failExpiredJobs();
|
|
216
220
|
|
|
217
|
-
|
|
221
|
+
// Task 0086 R6: claim only as much as can still start. A stop()
|
|
222
|
+
// landing mid-cycle stops further claims; already-claimed attempts
|
|
223
|
+
// still settle. With defaults (maxConcurrency === batchSize) this is
|
|
224
|
+
// exactly one claim per cycle, the same as before.
|
|
225
|
+
let claimed = 0;
|
|
218
226
|
let processed = 0;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}),
|
|
232
|
-
);
|
|
227
|
+
while (claimed < this.batchSize && !this.stopRequested) {
|
|
228
|
+
const requested = Math.min(this.maxConcurrency, this.batchSize - claimed);
|
|
229
|
+
const jobs = await this.dao.claimReady(requested, { leaseMs: this.visibilityTimeout });
|
|
230
|
+
// A short claim means the queue is drained: stop instead of
|
|
231
|
+
// re-arming jobs that were just retried within this cycle.
|
|
232
|
+
if (jobs.length < requested) {
|
|
233
|
+
claimed += jobs.length;
|
|
234
|
+
await this.runClaimed(jobs, () => (processed += 1));
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
claimed += jobs.length;
|
|
238
|
+
await this.runClaimed(jobs, () => (processed += 1));
|
|
233
239
|
}
|
|
234
240
|
|
|
235
|
-
addSpanAttributes({ 'queue.claimed':
|
|
241
|
+
addSpanAttributes({ 'queue.claimed': claimed, 'queue.processed': processed });
|
|
236
242
|
return processed;
|
|
237
243
|
});
|
|
238
244
|
}
|
|
239
245
|
|
|
246
|
+
/** Run one claimed batch to settlement, tracking in-flight for drain. */
|
|
247
|
+
private async runClaimed(jobs: QueueJobRecord[], onProcessed: () => void): Promise<void> {
|
|
248
|
+
await Promise.all(
|
|
249
|
+
jobs.map(async (job) => {
|
|
250
|
+
this.inFlight += 1;
|
|
251
|
+
try {
|
|
252
|
+
await this.processJob(job);
|
|
253
|
+
onProcessed();
|
|
254
|
+
} finally {
|
|
255
|
+
this.inFlight -= 1;
|
|
256
|
+
}
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
240
261
|
private schedule(delay: number): void {
|
|
241
262
|
this.timer = setTimeout(() => {
|
|
242
263
|
// Retained so stop() can await the cycle it interrupts; poll() contains its
|
|
@@ -359,7 +380,10 @@ export class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
359
380
|
});
|
|
360
381
|
}
|
|
361
382
|
} else {
|
|
362
|
-
|
|
383
|
+
// Task 0086 R10: gate metric + event on the fenced write — a
|
|
384
|
+
// lost lease means another consumer owns the job now.
|
|
385
|
+
const applied = await this.dao.markCompleted(record.id, token);
|
|
386
|
+
if (!applied) return;
|
|
363
387
|
getQueueJobCompletedTotal().add(1, { type: job.type });
|
|
364
388
|
const completed: QueueJobCompletedDetail = {
|
|
365
389
|
jobId: job.id,
|
|
@@ -421,7 +445,8 @@ export class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
421
445
|
const attempts = job.attempts + 1;
|
|
422
446
|
const message = error instanceof Error ? error.message : String(error);
|
|
423
447
|
if (attempts >= job.maxRetries) {
|
|
424
|
-
await this.dao.markFailed(job.id, attempts, message, attemptToken);
|
|
448
|
+
const applied = await this.dao.markFailed(job.id, attempts, message, attemptToken);
|
|
449
|
+
if (!applied) return; // Task 0086 R10: fenced write lost — no metric, no event.
|
|
425
450
|
getQueueJobFailedTotal().add(1, { type: job.type });
|
|
426
451
|
const failed: QueueJobFailedDetail = {
|
|
427
452
|
jobId: job.id,
|
|
@@ -438,7 +463,9 @@ export class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
438
463
|
|
|
439
464
|
const delay = Math.min(this.maxDelay, this.baseDelay * 2 ** Math.max(0, attempts - 1));
|
|
440
465
|
const nextRetryAt = Date.now() + delay;
|
|
441
|
-
|
|
466
|
+
// Task 0086 R10: gate the retry event on the fenced write.
|
|
467
|
+
const retryApplied = await this.dao.markForRetry(job.id, attempts, message, nextRetryAt, attemptToken);
|
|
468
|
+
if (!retryApplied) return;
|
|
442
469
|
const retrying: QueueJobRetryingDetail = {
|
|
443
470
|
jobId: job.id,
|
|
444
471
|
type: job.type,
|