vidspotai-shared 1.0.78 → 1.0.80
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/lib/services/aiGen/providers/google/google.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/google/google.service.js +29 -0
- package/lib/services/aiGen/providers/openai/openai.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/openai/openai.service.js +48 -10
- package/lib/services/bullmq.service.d.ts.map +1 -1
- package/lib/services/bullmq.service.js +21 -1
- package/lib/services/redisOptions.d.ts +12 -0
- package/lib/services/redisOptions.d.ts.map +1 -1
- package/lib/services/redisOptions.js +34 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAoJlB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAO/C;;;;OAIG;YACW,kBAAkB;IA+B1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAyG3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0F3C,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAsGjC;;;;;;OAMG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAqEjC,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,SAAa,EAAE,SAAS,EAAE,EAAE,iBAAiB,GAAG,MAAM;CA8BvI"}
|
|
@@ -127,6 +127,20 @@ function classifyGoogleApiError(err) {
|
|
|
127
127
|
if (httpCode === 14 || /high demand/i.test(msg)) {
|
|
128
128
|
return new errors_1.UserFacingError(msg, errors_1.USER_FACING_ERROR_CODES.VIDEO_PROVIDER_HIGH_DEMAND);
|
|
129
129
|
}
|
|
130
|
+
// INVALID_ARGUMENT 400 — narrow match: only the specific "use case is
|
|
131
|
+
// currently not supported" string, which Veo returns when our request
|
|
132
|
+
// structure doesn't match the chosen model variant's capabilities.
|
|
133
|
+
// The pre-call guards above (duration=8 for lastFrame/refs) should
|
|
134
|
+
// prevent the known cases; if we still hit this it's a NEW combo we
|
|
135
|
+
// haven't profiled — surface as CAPABILITY_MISMATCH so the user gets a
|
|
136
|
+
// useful message, AND keep the raw provider text in the error so the
|
|
137
|
+
// next entry in PROD_FIX_LOG can identify which combo broke. Generic
|
|
138
|
+
// 400s (other INVALID_ARGUMENT variants) still surface as `error` so
|
|
139
|
+
// a real platform bug isn't muted.
|
|
140
|
+
if ((status === "INVALID_ARGUMENT" || httpCode === 400) &&
|
|
141
|
+
/use case is currently not supported/i.test(msg)) {
|
|
142
|
+
return new errors_1.UserFacingError(msg, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
|
|
143
|
+
}
|
|
130
144
|
}
|
|
131
145
|
catch {
|
|
132
146
|
// Not JSON — fall through to non-JSON checks.
|
|
@@ -183,6 +197,21 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
|
|
|
183
197
|
const modelConfig = aiModels_1.aiModelConfigs[params.modelKey];
|
|
184
198
|
const modelId = modelConfig.modelId;
|
|
185
199
|
const isVeo3_1 = VEO_3_1_MODELS.has(params.modelKey);
|
|
200
|
+
// Gemini Veo cross-constraint: first+last-frame interpolation AND
|
|
201
|
+
// reference images BOTH require durationSeconds=8. Sending any other
|
|
202
|
+
// duration returns INVALID_ARGUMENT 400 "Your use case is currently not
|
|
203
|
+
// supported." with no hint about which param caused it. Surface a
|
|
204
|
+
// typed UserFacingError so the user/frontend can correct the input
|
|
205
|
+
// instead of burning a provider call + opaque rejection.
|
|
206
|
+
const needsDuration8 = !!params.lastFrameImageUrl ||
|
|
207
|
+
(isVeo3_1 && (params.referenceImageUrls?.length ?? 0) > 0);
|
|
208
|
+
if (needsDuration8 && params.duration !== undefined && params.duration !== 8) {
|
|
209
|
+
const constraint = params.lastFrameImageUrl
|
|
210
|
+
? "first-frame + last-frame interpolation"
|
|
211
|
+
: "reference images";
|
|
212
|
+
throw new errors_1.UserFacingError(`Google Veo requires an 8-second duration when using ${constraint}. ` +
|
|
213
|
+
`Please select 8s or remove the ${params.lastFrameImageUrl ? "last-frame image" : "reference images"}.`, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
|
|
214
|
+
}
|
|
186
215
|
const request = {
|
|
187
216
|
model: modelId,
|
|
188
217
|
prompt: params.prompt,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/openai/openai.service.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAElB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,MAAM,CAAS;;IAQjB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"openai.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/openai/openai.service.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAElB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,MAAM,CAAS;;IAQjB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA0E3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAsC3C,YAAY,CAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IA2ChC;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA6HjC,aAAa,CAAC,EACZ,QAAQ,EACR,UAAuB,EACvB,QAAY,EACZ,SAAiB,EACjB,SAAa,EACb,OAAO,GACR,EAAE,iBAAiB,GAAG,MAAM;CAoB9B"}
|
|
@@ -61,19 +61,57 @@ class OpenaiService extends baseAiGenProvider_service_1.BaseAiGenProviderService
|
|
|
61
61
|
};
|
|
62
62
|
// First-frame image-to-video. The Sora API requires the reference image
|
|
63
63
|
// dimensions to match `size`; the caller is responsible for that.
|
|
64
|
-
//
|
|
65
|
-
//
|
|
64
|
+
//
|
|
65
|
+
// We use the JSON variant `input_reference: { image_url }` (OpenAI
|
|
66
|
+
// fetches the image themselves) rather than the multipart `Uploadable`
|
|
67
|
+
// shape. Reason: as of late 2026 sora-2-pro started returning
|
|
68
|
+
// 400 Invalid type for 'input_reference': expected an object, but
|
|
69
|
+
// got a file instead.
|
|
70
|
+
// for multipart uploads that worked on sora-2. The JSON form is
|
|
71
|
+
// documented for both models and avoids the size mismatch entirely.
|
|
72
|
+
//
|
|
73
|
+
// openai SDK 6.3.0's typed shape for input_reference is `Uploadable`
|
|
74
|
+
// (no JSON-object overload yet), so we bypass the type with a cast.
|
|
75
|
+
// The HTTP layer serializes plain objects as JSON automatically when
|
|
76
|
+
// no Uploadable is present in the body, which is exactly what we want.
|
|
77
|
+
//
|
|
78
|
+
// Note: openai SDK v6.3.0 does NOT yet expose extensions / characters
|
|
79
|
+
// / edits endpoints — those exist in the REST API but are not surfaced.
|
|
66
80
|
if (params.inputImageUrl) {
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
// SDK 6.3.0 types input_reference as Uploadable only; the JSON
|
|
82
|
+
// object form is a runtime-supported overload that the types
|
|
83
|
+
// haven't caught up to yet. Bypass via unknown cast.
|
|
84
|
+
request.input_reference = {
|
|
85
|
+
image_url: params.inputImageUrl,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
let job;
|
|
89
|
+
try {
|
|
90
|
+
job = await this.client.videos.create(request);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// OpenAI client surfaces 400s via APIError with `status`/`message`.
|
|
94
|
+
// The most common case on sora-2-pro right now:
|
|
95
|
+
// 400 Invalid type for 'input_reference': expected an object, but got a file instead.
|
|
96
|
+
// Either the API now requires `input_reference` to be an uploaded file
|
|
97
|
+
// reference object (file_id) rather than a multipart Uploadable, or it's
|
|
98
|
+
// a transient SDK<>endpoint mismatch. Either way, this is not an
|
|
99
|
+
// ops-actionable bug — surface as a translatable capability mismatch
|
|
100
|
+
// so the user can switch model / disable image-to-video instead of
|
|
101
|
+
// firing Slack on every attempt.
|
|
102
|
+
const status = err?.status ?? err?.response?.status;
|
|
103
|
+
const message = err?.message ?? String(err);
|
|
104
|
+
if (status === 400) {
|
|
105
|
+
throw new errors_1.UserFacingError(message, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
|
|
106
|
+
}
|
|
107
|
+
if (status === 401 || status === 403) {
|
|
108
|
+
throw new errors_1.UserFacingError(message, errors_1.USER_FACING_ERROR_CODES.PROVIDER_AUTH_ERROR);
|
|
109
|
+
}
|
|
110
|
+
if (status === 429) {
|
|
111
|
+
throw new errors_1.UserFacingError(message, errors_1.USER_FACING_ERROR_CODES.VIDEO_PROVIDER_RATE_LIMITED);
|
|
72
112
|
}
|
|
73
|
-
|
|
74
|
-
request.input_reference = await (0, openai_1.toFile)(resp, filename);
|
|
113
|
+
throw err;
|
|
75
114
|
}
|
|
76
|
-
const job = await this.client.videos.create(request);
|
|
77
115
|
if (job.status === "failed") {
|
|
78
116
|
throw new Error(`OpenAI video generation failed: ${JSON.stringify(job.error)}`);
|
|
79
117
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bullmq.service.d.ts","sourceRoot":"","sources":["../../src/services/bullmq.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAgB,EAAwB,YAAY,EAAE,MAAM,SAAS,CAAC;AAItE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"bullmq.service.d.ts","sourceRoot":"","sources":["../../src/services/bullmq.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAgB,EAAwB,YAAY,EAAE,MAAM,SAAS,CAAC;AAItE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAc1C,UAAU,oBAAoB;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,cAAM,aAAa;IACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAgB;IACvC,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAAgB;IAIpC,OAAO,CAAC,UAAU,CAAC,CAAc;IACjC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAE7B,OAAO;WAQO,WAAW,CAAC,OAAO,EAAE,oBAAoB;IAOvD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,aAAa;IAOrB,sDAAsD;IACzC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBlC,gBAAgB;IACH,MAAM,CACjB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,OAAO,EACb,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAUpC,YAAY,CACjB,UAAU,EAAE,MAAM,EAAE,EACpB,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,EACtC,eAAe,CAAC,EAAE,MAAM,CACtB,MAAM,EACN;QACE,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CACF;CAkHJ;AASD,eAAO,MAAM,MAAM,eAGjB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,GAAG,CAAC"}
|
|
@@ -9,6 +9,17 @@ const bullmq_1 = require("bullmq");
|
|
|
9
9
|
const ioredis_1 = __importDefault(require("ioredis"));
|
|
10
10
|
const logger_1 = require("../utils/logger");
|
|
11
11
|
const redisOptions_1 = require("./redisOptions");
|
|
12
|
+
// Transient ioredis socket errors that BullMQ's worker `error` event surfaces
|
|
13
|
+
// verbatim. ioredis reconnects from these on its own — they're noise, not
|
|
14
|
+
// outages. Kept in sync with TRANSIENT_REDIS_ERROR_CODES in redisOptions.ts.
|
|
15
|
+
const TRANSIENT_WORKER_ERROR_CODES = new Set([
|
|
16
|
+
"ECONNRESET",
|
|
17
|
+
"ETIMEDOUT",
|
|
18
|
+
"ECONNREFUSED",
|
|
19
|
+
"EPIPE",
|
|
20
|
+
"ENOTFOUND",
|
|
21
|
+
"EAI_AGAIN",
|
|
22
|
+
]);
|
|
12
23
|
class BullMQService {
|
|
13
24
|
constructor({ redisOptions, concurrency = 10, }) {
|
|
14
25
|
this.queues = new Map();
|
|
@@ -132,9 +143,18 @@ class BullMQService {
|
|
|
132
143
|
});
|
|
133
144
|
// Worker-shell failure (Redis lost, malformed payload, processor
|
|
134
145
|
// import error, etc.). Without this handler these crashes are silent.
|
|
146
|
+
// Transient ioredis network blips (ECONNRESET/ETIMEDOUT/EPIPE) bubble
|
|
147
|
+
// through this same listener — ioredis recovers them automatically,
|
|
148
|
+
// so demote to `warn` to keep Slack quiet. Real shell failures (auth,
|
|
149
|
+
// parser, malformed payload) still surface at `error` → Slack.
|
|
135
150
|
worker.on("error", (err) => {
|
|
136
|
-
|
|
151
|
+
const code = err?.code;
|
|
152
|
+
const isTransient = !!code && TRANSIENT_WORKER_ERROR_CODES.has(code);
|
|
153
|
+
const logFn = isTransient ? logger_1.logger.warn.bind(logger_1.logger) : logger_1.logger.error.bind(logger_1.logger);
|
|
154
|
+
logFn(`BullMQ worker error`, {
|
|
137
155
|
queueName,
|
|
156
|
+
code,
|
|
157
|
+
transient: isTransient,
|
|
138
158
|
err: err?.stack ?? err?.message ?? String(err),
|
|
139
159
|
});
|
|
140
160
|
});
|
|
@@ -9,6 +9,18 @@ import type { RedisOptions } from "ioredis";
|
|
|
9
9
|
* - `retryStrategy` — exponential 200ms → 10s cap; infinite retries.
|
|
10
10
|
* - `reconnectOnError` — reconnect on READONLY (failover to a replica
|
|
11
11
|
* that has since become primary).
|
|
12
|
+
* - `keepAlive: 30_000` — send a TCP keep-alive probe every 30s. Without
|
|
13
|
+
* this, BullMQ's blocking worker connections (BRPOPLPUSH idle while
|
|
14
|
+
* waiting for jobs) get silently dropped by intermediate NATs / Redis
|
|
15
|
+
* Cloud's idle-connection killer after ~60-300s. The next read returns
|
|
16
|
+
* ECONNRESET / ETIMEDOUT and ioredis reconnects, but each drop fires
|
|
17
|
+
* `error` events on every queue's worker. Keep-alive prevents the
|
|
18
|
+
* silent-drop in the first place, which is the actual root cause of
|
|
19
|
+
* the recurring ECONNRESET / ETIMEDOUT flood we were seeing in Slack.
|
|
20
|
+
* - `connectTimeout: 15_000` — bounds new-connection establishment so a
|
|
21
|
+
* slow DNS / handshake fails fast and retryStrategy kicks in cleanly.
|
|
22
|
+
* - `noDelay: true` — disable Nagle so small commands (PING, BRPOPLPUSH
|
|
23
|
+
* wakeups) don't pile up waiting for an MTU's worth of data.
|
|
12
24
|
*/
|
|
13
25
|
export declare const sharedRedisOptions: RedisOptions;
|
|
14
26
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redisOptions.d.ts","sourceRoot":"","sources":["../../src/services/redisOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG5C
|
|
1
|
+
{"version":3,"file":"redisOptions.d.ts","sourceRoot":"","sources":["../../src/services/redisOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG5C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,EAAE,YAShC,CAAC;AAkBF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE;IAAE,EAAE,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,IAAI,CAAA;CAAE,EAC/D,KAAK,EAAE,MAAM,GACZ,IAAI,CA2BN"}
|
|
@@ -13,14 +13,44 @@ const logger_1 = require("../utils/logger");
|
|
|
13
13
|
* - `retryStrategy` — exponential 200ms → 10s cap; infinite retries.
|
|
14
14
|
* - `reconnectOnError` — reconnect on READONLY (failover to a replica
|
|
15
15
|
* that has since become primary).
|
|
16
|
+
* - `keepAlive: 30_000` — send a TCP keep-alive probe every 30s. Without
|
|
17
|
+
* this, BullMQ's blocking worker connections (BRPOPLPUSH idle while
|
|
18
|
+
* waiting for jobs) get silently dropped by intermediate NATs / Redis
|
|
19
|
+
* Cloud's idle-connection killer after ~60-300s. The next read returns
|
|
20
|
+
* ECONNRESET / ETIMEDOUT and ioredis reconnects, but each drop fires
|
|
21
|
+
* `error` events on every queue's worker. Keep-alive prevents the
|
|
22
|
+
* silent-drop in the first place, which is the actual root cause of
|
|
23
|
+
* the recurring ECONNRESET / ETIMEDOUT flood we were seeing in Slack.
|
|
24
|
+
* - `connectTimeout: 15_000` — bounds new-connection establishment so a
|
|
25
|
+
* slow DNS / handshake fails fast and retryStrategy kicks in cleanly.
|
|
26
|
+
* - `noDelay: true` — disable Nagle so small commands (PING, BRPOPLPUSH
|
|
27
|
+
* wakeups) don't pile up waiting for an MTU's worth of data.
|
|
16
28
|
*/
|
|
17
29
|
exports.sharedRedisOptions = {
|
|
18
30
|
maxRetriesPerRequest: null,
|
|
19
31
|
enableReadyCheck: true,
|
|
20
32
|
enableOfflineQueue: true,
|
|
33
|
+
keepAlive: 30000,
|
|
34
|
+
connectTimeout: 15000,
|
|
35
|
+
noDelay: true,
|
|
21
36
|
retryStrategy: (times) => Math.min(200 * Math.pow(1.5, times), 10000),
|
|
22
37
|
reconnectOnError: (err) => err.message.includes("READONLY"),
|
|
23
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* ioredis auto-reconnects from these — they are operational noise during
|
|
41
|
+
* a Redis Cloud failover, a brief TCP reset, or a sleeping connection
|
|
42
|
+
* timing out. Logging them at `error` pages Slack every time the window
|
|
43
|
+
* expires; demote to `warn` so the Slack transport (error-only) ignores
|
|
44
|
+
* them. Real persistent outages still surface via BullMQ job timeouts.
|
|
45
|
+
*/
|
|
46
|
+
const TRANSIENT_REDIS_ERROR_CODES = new Set([
|
|
47
|
+
"ECONNRESET",
|
|
48
|
+
"ETIMEDOUT",
|
|
49
|
+
"ECONNREFUSED",
|
|
50
|
+
"EPIPE",
|
|
51
|
+
"ENOTFOUND",
|
|
52
|
+
"EAI_AGAIN",
|
|
53
|
+
]);
|
|
24
54
|
/**
|
|
25
55
|
* Attach observability + error-dedupe listeners to an ioredis client.
|
|
26
56
|
* Reduces log spam during prolonged outages (e.g. DNS NXDOMAIN, network
|
|
@@ -42,9 +72,12 @@ function attachRedisListeners(client, label) {
|
|
|
42
72
|
return;
|
|
43
73
|
lastErrCode = code;
|
|
44
74
|
lastErrAt = now;
|
|
45
|
-
|
|
75
|
+
const isTransient = !!err.code && TRANSIENT_REDIS_ERROR_CODES.has(err.code);
|
|
76
|
+
const logFn = isTransient ? logger_1.logger.warn.bind(logger_1.logger) : logger_1.logger.error.bind(logger_1.logger);
|
|
77
|
+
logFn("redis: connection error", {
|
|
46
78
|
label,
|
|
47
79
|
code,
|
|
80
|
+
transient: isTransient,
|
|
48
81
|
err: err?.stack ?? err?.message ?? String(err),
|
|
49
82
|
});
|
|
50
83
|
});
|