@kaikybrofc/omnizap-system 2.2.4 → 2.2.6
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/.env.example +5 -0
- package/.prettierrc +16 -0
- package/README.md +13 -13
- package/app/modules/stickerPackModule/autoPackCollectorService.js +63 -8
- package/app/modules/stickerPackModule/catalogHandlers/catalogAdminHttp.js +68 -0
- package/app/modules/stickerPackModule/catalogHandlers/catalogAuthHttp.js +34 -0
- package/app/modules/stickerPackModule/catalogHandlers/catalogPublicHttp.js +179 -0
- package/app/modules/stickerPackModule/catalogHandlers/catalogUploadHttp.js +92 -0
- package/app/modules/stickerPackModule/catalogRouter.js +79 -0
- package/app/modules/stickerPackModule/domainEventOutboxRepository.js +243 -0
- package/app/modules/stickerPackModule/domainEvents.js +61 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +21 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +19 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +55 -15
- package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +238 -0
- package/app/modules/stickerPackModule/stickerDomainEventBus.js +71 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +198 -0
- package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
- package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +570 -536
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +44 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +18 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +51 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +191 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +301 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +111 -10
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +21 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +59 -7
- package/app/observability/metrics.js +169 -0
- package/app/services/featureFlagService.js +137 -0
- package/database/index.js +5 -0
- package/database/migrations/20260228_0022_sticker_scale_indexes.sql +16 -0
- package/database/migrations/20260228_0023_sticker_pack_score_snapshot.sql +25 -0
- package/database/migrations/20260228_0024_domain_event_outbox.sql +42 -0
- package/database/migrations/20260228_0025_sticker_worker_task_idempotency_dlq.sql +23 -0
- package/database/migrations/20260228_0026_feature_flags.sql +21 -0
- package/ecosystem.prod.config.cjs +70 -9
- package/index.js +26 -0
- package/kaikybrofc-omnizap-system-2.2.6.tgz +0 -0
- package/observability/sticker-catalog-slo.md +83 -0
- package/observability/sticker-scale-hardening-rollout.md +128 -0
- package/package.json +7 -35
- package/public/assets/images/brand-icon-192.png +0 -0
- package/public/assets/images/brand-logo-128.webp +0 -0
- package/public/assets/images/hero-banner-1280.avif +0 -0
- package/public/assets/images/hero-banner-1280.jpg +0 -0
- package/public/assets/images/hero-banner-1280.webp +0 -0
- package/public/assets/images/hero-banner-720.avif +0 -0
- package/public/assets/images/hero-banner-720.webp +0 -0
- package/public/index.html +120 -18
- package/public/js/apps/homeApp.js +469 -353
- package/public/robots.txt +9 -0
- package/public/sitemap.xml +28 -0
- package/scripts/sticker-catalog-loadtest.mjs +208 -0
- package/scripts/sticker-worker-task.mjs +122 -0
- package/observability/mysql-exporter.cnf +0 -5
|
@@ -4,6 +4,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
4
4
|
|
|
5
5
|
import logger from '../../utils/logger/loggerModule.js';
|
|
6
6
|
import { downloadMediaMessage, extractMediaDetails } from '../../config/baileysConfig.js';
|
|
7
|
+
import { isFeatureEnabled } from '../../services/featureFlagService.js';
|
|
7
8
|
import {
|
|
8
9
|
createStickerAsset,
|
|
9
10
|
findLatestStickerAssetByOwner,
|
|
@@ -14,6 +15,12 @@ import {
|
|
|
14
15
|
import { ensureStickerAssetClassified } from './stickerClassificationService.js';
|
|
15
16
|
import { STICKER_PACK_ERROR_CODES, StickerPackError } from './stickerPackErrors.js';
|
|
16
17
|
import { normalizeOwnerJid } from './stickerPackUtils.js';
|
|
18
|
+
import {
|
|
19
|
+
getStickerObjectStorageUrl,
|
|
20
|
+
isStickerObjectStorageEnabled,
|
|
21
|
+
readStickerFromObjectStorage,
|
|
22
|
+
uploadStickerToObjectStorage,
|
|
23
|
+
} from './stickerObjectStorageService.js';
|
|
17
24
|
|
|
18
25
|
/**
|
|
19
26
|
* Camada de storage local para assets de figurinha do sistema de packs.
|
|
@@ -23,9 +30,29 @@ const TEMP_ROOT = path.join(process.cwd(), 'temp', 'sticker-pack-assets');
|
|
|
23
30
|
const DEFAULT_MAX_STICKER_BYTES = 2 * 1024 * 1024;
|
|
24
31
|
const MAX_STICKER_BYTES = Math.max(64 * 1024, Number(process.env.STICKER_PACK_MAX_STICKER_BYTES) || DEFAULT_MAX_STICKER_BYTES);
|
|
25
32
|
const LAST_STICKER_TTL_MS = Math.max(60_000, Number(process.env.STICKER_PACK_LAST_CACHE_TTL_MS) || 6 * 60 * 60 * 1000);
|
|
33
|
+
const OBJECT_STORAGE_UPLOAD_TIMEOUT_MS = Math.max(
|
|
34
|
+
1_000,
|
|
35
|
+
Number(process.env.STICKER_OBJECT_STORAGE_UPLOAD_TIMEOUT_MS) || 10_000,
|
|
36
|
+
);
|
|
26
37
|
|
|
27
38
|
const lastStickerCache = new Map();
|
|
28
39
|
|
|
40
|
+
const withTimeout = (promise, ms) =>
|
|
41
|
+
Promise.race([
|
|
42
|
+
Promise.resolve(promise),
|
|
43
|
+
new Promise((_, reject) => {
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
reject(new Error(`timeout_after_${ms}ms`));
|
|
46
|
+
}, ms);
|
|
47
|
+
}),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const isObjectStorageDeliveryEnabled = async (subjectKey = '') =>
|
|
51
|
+
isFeatureEnabled('enable_object_storage_delivery', {
|
|
52
|
+
fallback: false,
|
|
53
|
+
subjectKey: subjectKey || 'object_storage_delivery',
|
|
54
|
+
});
|
|
55
|
+
|
|
29
56
|
/**
|
|
30
57
|
* Converte JID para um token seguro no caminho de disco.
|
|
31
58
|
*
|
|
@@ -182,6 +209,31 @@ const ensureStorageForAsset = async ({ ownerJid, sha256, buffer }) => {
|
|
|
182
209
|
return targetPath;
|
|
183
210
|
};
|
|
184
211
|
|
|
212
|
+
const uploadExternalStorageBestEffort = async ({ ownerJid, sha256, buffer, mimetype = 'image/webp', assetId = null }) => {
|
|
213
|
+
if (!isStickerObjectStorageEnabled()) return;
|
|
214
|
+
const canUpload = await isObjectStorageDeliveryEnabled(normalizeOwnerJid(ownerJid) || sha256);
|
|
215
|
+
if (!canUpload) return;
|
|
216
|
+
try {
|
|
217
|
+
await withTimeout(
|
|
218
|
+
uploadStickerToObjectStorage({
|
|
219
|
+
ownerJid,
|
|
220
|
+
sha256,
|
|
221
|
+
buffer,
|
|
222
|
+
mimetype,
|
|
223
|
+
}),
|
|
224
|
+
OBJECT_STORAGE_UPLOAD_TIMEOUT_MS,
|
|
225
|
+
);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
logger.warn('Falha no upload best-effort para object storage.', {
|
|
228
|
+
action: 'sticker_object_storage_upload_best_effort_failed',
|
|
229
|
+
owner_jid: ownerJid || null,
|
|
230
|
+
asset_id: assetId || null,
|
|
231
|
+
sha256: sha256 || null,
|
|
232
|
+
error: error?.message,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
185
237
|
/**
|
|
186
238
|
* Extrai detalhes de mídia sticker de uma mensagem.
|
|
187
239
|
*
|
|
@@ -253,6 +305,13 @@ async function persistStickerAssetBuffer({ ownerJid, buffer, mimetype = 'image/w
|
|
|
253
305
|
error: error?.message,
|
|
254
306
|
});
|
|
255
307
|
});
|
|
308
|
+
await uploadExternalStorageBestEffort({
|
|
309
|
+
ownerJid: normalizedOwner,
|
|
310
|
+
sha256,
|
|
311
|
+
buffer,
|
|
312
|
+
mimetype,
|
|
313
|
+
assetId: repaired.id,
|
|
314
|
+
});
|
|
256
315
|
return repaired;
|
|
257
316
|
}
|
|
258
317
|
|
|
@@ -265,6 +324,13 @@ async function persistStickerAssetBuffer({ ownerJid, buffer, mimetype = 'image/w
|
|
|
265
324
|
error: error?.message,
|
|
266
325
|
});
|
|
267
326
|
});
|
|
327
|
+
await uploadExternalStorageBestEffort({
|
|
328
|
+
ownerJid: normalizedOwner,
|
|
329
|
+
sha256,
|
|
330
|
+
buffer,
|
|
331
|
+
mimetype,
|
|
332
|
+
assetId: existing.id,
|
|
333
|
+
});
|
|
268
334
|
return existing;
|
|
269
335
|
}
|
|
270
336
|
|
|
@@ -293,6 +359,13 @@ async function persistStickerAssetBuffer({ ownerJid, buffer, mimetype = 'image/w
|
|
|
293
359
|
error: error?.message,
|
|
294
360
|
});
|
|
295
361
|
});
|
|
362
|
+
await uploadExternalStorageBestEffort({
|
|
363
|
+
ownerJid: normalizedOwner,
|
|
364
|
+
sha256,
|
|
365
|
+
buffer,
|
|
366
|
+
mimetype,
|
|
367
|
+
assetId: created.id,
|
|
368
|
+
});
|
|
296
369
|
return created;
|
|
297
370
|
} catch (error) {
|
|
298
371
|
if (error?.code === 'ER_DUP_ENTRY') {
|
|
@@ -479,19 +552,46 @@ export async function resolveStickerAssetForCommand({
|
|
|
479
552
|
* @throws {StickerPackError} Quando o arquivo não puder ser lido.
|
|
480
553
|
*/
|
|
481
554
|
export async function readStickerAssetBuffer(asset) {
|
|
482
|
-
if (
|
|
483
|
-
|
|
555
|
+
if (asset?.storage_path) {
|
|
556
|
+
try {
|
|
557
|
+
return await fs.readFile(asset.storage_path);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
const externalBuffer = await readStickerFromObjectStorage(asset).catch(() => null);
|
|
560
|
+
if (Buffer.isBuffer(externalBuffer) && externalBuffer.length) {
|
|
561
|
+
return externalBuffer;
|
|
562
|
+
}
|
|
563
|
+
throw new StickerPackError(
|
|
564
|
+
STICKER_PACK_ERROR_CODES.STORAGE_ERROR,
|
|
565
|
+
`Não foi possível ler a figurinha em disco (${asset.storage_path}).`,
|
|
566
|
+
error,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
484
569
|
}
|
|
485
570
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
throw new StickerPackError(
|
|
490
|
-
STICKER_PACK_ERROR_CODES.STORAGE_ERROR,
|
|
491
|
-
`Não foi possível ler a figurinha em disco (${asset.storage_path}).`,
|
|
492
|
-
error,
|
|
493
|
-
);
|
|
571
|
+
const externalBuffer = await readStickerFromObjectStorage(asset).catch(() => null);
|
|
572
|
+
if (Buffer.isBuffer(externalBuffer) && externalBuffer.length) {
|
|
573
|
+
return externalBuffer;
|
|
494
574
|
}
|
|
575
|
+
|
|
576
|
+
throw new StickerPackError(STICKER_PACK_ERROR_CODES.STORAGE_ERROR, 'Caminho do sticker não encontrado no storage.');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export async function getStickerAssetExternalUrl(
|
|
580
|
+
asset,
|
|
581
|
+
{
|
|
582
|
+
secure = true,
|
|
583
|
+
expiresInSeconds = 300,
|
|
584
|
+
} = {},
|
|
585
|
+
) {
|
|
586
|
+
if (!asset) return null;
|
|
587
|
+
const canUseExternalDelivery = await isObjectStorageDeliveryEnabled(
|
|
588
|
+
normalizeOwnerJid(asset?.owner_jid || '') || String(asset?.id || asset?.sha256 || ''),
|
|
589
|
+
);
|
|
590
|
+
if (!canUseExternalDelivery) return null;
|
|
591
|
+
return getStickerObjectStorageUrl(asset, {
|
|
592
|
+
secure,
|
|
593
|
+
expiresInSeconds,
|
|
594
|
+
});
|
|
495
595
|
}
|
|
496
596
|
|
|
497
597
|
/**
|
|
@@ -503,5 +603,6 @@ export function getStickerStorageConfig() {
|
|
|
503
603
|
return {
|
|
504
604
|
storageRoot: STORAGE_ROOT,
|
|
505
605
|
maxStickerBytes: MAX_STICKER_BYTES,
|
|
606
|
+
objectStorageEnabled: isStickerObjectStorageEnabled(),
|
|
506
607
|
};
|
|
507
608
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logger from '../../utils/logger/loggerModule.js';
|
|
2
2
|
import { setQueueDepth } from '../../observability/metrics.js';
|
|
3
|
+
import { isFeatureEnabled } from '../../services/featureFlagService.js';
|
|
3
4
|
import { runStickerClassificationCycle } from './stickerClassificationBackgroundRuntime.js';
|
|
4
5
|
import { runStickerAutoPackByTagsCycle } from './stickerAutoPackByTagsRuntime.js';
|
|
5
6
|
import {
|
|
@@ -24,6 +25,8 @@ const STARTUP_DELAY_MS = Math.max(1_000, Number(process.env.STICKER_WORKER_PIPEL
|
|
|
24
25
|
const SCHEDULER_INTERVAL_MS = Math.max(2_000, Number(process.env.STICKER_WORKER_PIPELINE_SCHEDULER_INTERVAL_MS) || 15_000);
|
|
25
26
|
const POLLER_INTERVAL_MS = Math.max(1_000, Number(process.env.STICKER_WORKER_PIPELINE_POLLER_INTERVAL_MS) || 4_000);
|
|
26
27
|
const WORKER_RETRY_DELAY_SECONDS = Math.max(5, Math.min(3600, Number(process.env.STICKER_WORKER_PIPELINE_RETRY_DELAY_SECONDS) || 45));
|
|
28
|
+
const INLINE_POLLER_ENABLED = parseEnvBool(process.env.STICKER_WORKER_PIPELINE_INLINE_POLLER_ENABLED, true);
|
|
29
|
+
const INLINE_POLLER_FORCE_ENABLED = parseEnvBool(process.env.STICKER_WORKER_PIPELINE_INLINE_POLLER_FORCE_ENABLED, false);
|
|
27
30
|
|
|
28
31
|
const TASK_CADENCE_MS = {
|
|
29
32
|
classification_cycle: Math.max(10_000, Number(process.env.STICKER_WORKER_CLASSIFICATION_CADENCE_MS) || 120_000),
|
|
@@ -36,6 +39,10 @@ const TASK_PRIORITY = {
|
|
|
36
39
|
curation_cycle: 55,
|
|
37
40
|
rebuild_cycle: 50,
|
|
38
41
|
};
|
|
42
|
+
const PIPELINE_PROCESS_COHORT_KEY =
|
|
43
|
+
String(process.env.STICKER_WORKER_PIPELINE_COHORT_KEY || process.env.HOSTNAME || process.pid)
|
|
44
|
+
.trim()
|
|
45
|
+
|| 'pipeline';
|
|
39
46
|
|
|
40
47
|
let startupHandle = null;
|
|
41
48
|
let schedulerHandle = null;
|
|
@@ -62,6 +69,16 @@ const refreshQueueDepthMetrics = async () => {
|
|
|
62
69
|
setQueueDepth('sticker_worker_tasks_failed', failed);
|
|
63
70
|
};
|
|
64
71
|
|
|
72
|
+
const canRunInlinePoller = async () => {
|
|
73
|
+
if (!INLINE_POLLER_ENABLED) return false;
|
|
74
|
+
if (INLINE_POLLER_FORCE_ENABLED) return true;
|
|
75
|
+
const dedicatedWorkersEnabled = await isFeatureEnabled('enable_worker_dedicated_processes', {
|
|
76
|
+
fallback: false,
|
|
77
|
+
subjectKey: `pipeline:inline_poller:${PIPELINE_PROCESS_COHORT_KEY}`,
|
|
78
|
+
});
|
|
79
|
+
return !dedicatedWorkersEnabled;
|
|
80
|
+
};
|
|
81
|
+
|
|
65
82
|
const scheduleTaskIfNeeded = async (taskType) => {
|
|
66
83
|
if (!taskQueueAvailable) return;
|
|
67
84
|
const cadence = TASK_CADENCE_MS[taskType];
|
|
@@ -81,6 +98,7 @@ const scheduleTaskIfNeeded = async (taskType) => {
|
|
|
81
98
|
taskType,
|
|
82
99
|
payload: { scheduled_by: 'sticker_worker_pipeline' },
|
|
83
100
|
priority: TASK_PRIORITY[taskType] || 50,
|
|
101
|
+
idempotencyKey: `pipeline:${taskType}:${Math.floor(now / cadence)}`,
|
|
84
102
|
});
|
|
85
103
|
nextScheduleByTask.set(taskType, now + cadence);
|
|
86
104
|
};
|
|
@@ -150,6 +168,8 @@ const pollerTick = async () => {
|
|
|
150
168
|
runningPoll = true;
|
|
151
169
|
|
|
152
170
|
try {
|
|
171
|
+
if (!(await canRunInlinePoller())) return;
|
|
172
|
+
|
|
153
173
|
let progressed = false;
|
|
154
174
|
|
|
155
175
|
progressed = (await processSingleTaskType('classification_cycle')) || progressed;
|
|
@@ -191,6 +211,7 @@ export const startStickerWorkerPipeline = () => {
|
|
|
191
211
|
startup_delay_ms: STARTUP_DELAY_MS,
|
|
192
212
|
scheduler_interval_ms: SCHEDULER_INTERVAL_MS,
|
|
193
213
|
poller_interval_ms: POLLER_INTERVAL_MS,
|
|
214
|
+
inline_poller_enabled: INLINE_POLLER_ENABLED,
|
|
194
215
|
cadence_ms: TASK_CADENCE_MS,
|
|
195
216
|
});
|
|
196
217
|
|
|
@@ -20,6 +20,19 @@ const clampInt = (value, fallback, min, max) => {
|
|
|
20
20
|
return Math.max(min, Math.min(max, Math.floor(numeric)));
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
const CLAIM_LOCK_TIMEOUT_SECONDS = clampInt(
|
|
24
|
+
process.env.STICKER_WORKER_TASK_LOCK_TIMEOUT_SECONDS,
|
|
25
|
+
15 * 60,
|
|
26
|
+
30,
|
|
27
|
+
24 * 60 * 60,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const normalizeIdempotencyKey = (value) =>
|
|
31
|
+
String(value || '')
|
|
32
|
+
.trim()
|
|
33
|
+
.replace(/[^a-zA-Z0-9_:-]/g, '')
|
|
34
|
+
.slice(0, 180);
|
|
35
|
+
|
|
23
36
|
const parseJson = (value, fallback = null) => {
|
|
24
37
|
if (value === null || value === undefined) return fallback;
|
|
25
38
|
if (typeof value === 'object') return value;
|
|
@@ -48,6 +61,7 @@ const normalizeRow = (row) => {
|
|
|
48
61
|
id: Number(row.id),
|
|
49
62
|
task_type: row.task_type,
|
|
50
63
|
payload: parseJson(row.payload, {}),
|
|
64
|
+
idempotency_key: row.idempotency_key || null,
|
|
51
65
|
priority: Number(row.priority || 0),
|
|
52
66
|
scheduled_at: row.scheduled_at || null,
|
|
53
67
|
status: row.status,
|
|
@@ -63,7 +77,7 @@ const normalizeRow = (row) => {
|
|
|
63
77
|
};
|
|
64
78
|
|
|
65
79
|
export async function enqueueWorkerTask(
|
|
66
|
-
{ taskType, payload = {}, priority = 50, scheduledAt = null, maxAttempts = 5 },
|
|
80
|
+
{ taskType, payload = {}, priority = 50, scheduledAt = null, maxAttempts = 5, idempotencyKey = '' },
|
|
67
81
|
connection = null,
|
|
68
82
|
) {
|
|
69
83
|
const normalizedTaskType = normalizeTaskType(taskType);
|
|
@@ -73,12 +87,26 @@ export async function enqueueWorkerTask(
|
|
|
73
87
|
const safeMaxAttempts = clampInt(maxAttempts, 5, 1, 20);
|
|
74
88
|
const safeScheduledAt = scheduledAt ? new Date(scheduledAt) : null;
|
|
75
89
|
const scheduledValue = safeScheduledAt && Number.isFinite(safeScheduledAt.valueOf()) ? safeScheduledAt : null;
|
|
90
|
+
const normalizedIdempotencyKey = normalizeIdempotencyKey(idempotencyKey) || null;
|
|
76
91
|
|
|
77
92
|
await executeQuery(
|
|
78
93
|
`INSERT INTO ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
79
|
-
(task_type, payload, priority, scheduled_at, status, attempts, max_attempts)
|
|
80
|
-
VALUES (?, ?, ?, COALESCE(?, UTC_TIMESTAMP()), 'pending', 0, ?)
|
|
81
|
-
|
|
94
|
+
(task_type, idempotency_key, payload, priority, scheduled_at, status, attempts, max_attempts)
|
|
95
|
+
VALUES (?, ?, ?, ?, COALESCE(?, UTC_TIMESTAMP()), 'pending', 0, ?)
|
|
96
|
+
ON DUPLICATE KEY UPDATE
|
|
97
|
+
payload = IF(status IN ('pending', 'failed'), VALUES(payload), payload),
|
|
98
|
+
priority = GREATEST(priority, VALUES(priority)),
|
|
99
|
+
scheduled_at = LEAST(scheduled_at, VALUES(scheduled_at)),
|
|
100
|
+
status = IF(status = 'failed' AND attempts < max_attempts, 'pending', status),
|
|
101
|
+
updated_at = UTC_TIMESTAMP()`,
|
|
102
|
+
[
|
|
103
|
+
normalizedTaskType,
|
|
104
|
+
normalizedIdempotencyKey,
|
|
105
|
+
JSON.stringify(payload || {}),
|
|
106
|
+
safePriority,
|
|
107
|
+
scheduledValue,
|
|
108
|
+
safeMaxAttempts,
|
|
109
|
+
],
|
|
82
110
|
connection,
|
|
83
111
|
);
|
|
84
112
|
return true;
|
|
@@ -92,7 +120,10 @@ export async function hasPendingWorkerTask(taskType, connection = null) {
|
|
|
92
120
|
`SELECT id
|
|
93
121
|
FROM ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
94
122
|
WHERE task_type = ?
|
|
95
|
-
AND
|
|
123
|
+
AND (
|
|
124
|
+
status IN ('pending', 'processing')
|
|
125
|
+
OR (status = 'failed' AND attempts < max_attempts)
|
|
126
|
+
)
|
|
96
127
|
LIMIT 1`,
|
|
97
128
|
[normalizedTaskType],
|
|
98
129
|
connection,
|
|
@@ -106,8 +137,11 @@ export async function claimWorkerTask({ taskType, allowRetryFailed = true } = {}
|
|
|
106
137
|
|
|
107
138
|
const workerToken = randomUUID();
|
|
108
139
|
const statusClause = allowRetryFailed
|
|
109
|
-
?
|
|
110
|
-
|
|
140
|
+
? `(status = 'pending'
|
|
141
|
+
OR (status = 'failed' AND attempts < max_attempts)
|
|
142
|
+
OR (status = 'processing' AND locked_at <= (UTC_TIMESTAMP() - INTERVAL ${CLAIM_LOCK_TIMEOUT_SECONDS} SECOND)))`
|
|
143
|
+
: `(status = 'pending'
|
|
144
|
+
OR (status = 'processing' AND locked_at <= (UTC_TIMESTAMP() - INTERVAL ${CLAIM_LOCK_TIMEOUT_SECONDS} SECOND)))`;
|
|
111
145
|
|
|
112
146
|
await executeQuery(
|
|
113
147
|
`UPDATE ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
@@ -187,6 +221,24 @@ export async function failWorkerTask(
|
|
|
187
221
|
[message, taskId],
|
|
188
222
|
connection,
|
|
189
223
|
);
|
|
224
|
+
|
|
225
|
+
await executeQuery(
|
|
226
|
+
`INSERT INTO ${TABLES.STICKER_WORKER_TASK_DLQ}
|
|
227
|
+
(task_id, task_type, payload, idempotency_key, attempts, max_attempts, priority, last_error)
|
|
228
|
+
SELECT id, task_type, payload, idempotency_key, attempts, max_attempts, priority, last_error
|
|
229
|
+
FROM ${TABLES.STICKER_WORKER_TASK_QUEUE}
|
|
230
|
+
WHERE id = ?
|
|
231
|
+
AND status = 'failed'
|
|
232
|
+
ON DUPLICATE KEY UPDATE
|
|
233
|
+
attempts = VALUES(attempts),
|
|
234
|
+
max_attempts = VALUES(max_attempts),
|
|
235
|
+
priority = VALUES(priority),
|
|
236
|
+
last_error = VALUES(last_error),
|
|
237
|
+
failed_at = CURRENT_TIMESTAMP`,
|
|
238
|
+
[taskId],
|
|
239
|
+
connection,
|
|
240
|
+
).catch(() => null);
|
|
241
|
+
|
|
190
242
|
return true;
|
|
191
243
|
}
|
|
192
244
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
|
|
3
3
|
import http from 'node:http';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { URL } from 'node:url';
|
|
5
6
|
import client from 'prom-client';
|
|
6
7
|
import logger from '../utils/logger/loggerModule.js';
|
|
@@ -32,6 +33,7 @@ const METRICS_PORT = parseEnvNumber(process.env.METRICS_PORT, 9102);
|
|
|
32
33
|
const METRICS_HOST = process.env.METRICS_HOST || '0.0.0.0';
|
|
33
34
|
const METRICS_PATH = process.env.METRICS_PATH || '/metrics';
|
|
34
35
|
const METRICS_SERVICE = process.env.METRICS_SERVICE_NAME || process.env.ECOSYSTEM_NAME || 'omnizap';
|
|
36
|
+
const HTTP_SLO_TARGET_MS = Math.max(50, parseEnvNumber(process.env.HTTP_SLO_TARGET_MS, 750));
|
|
35
37
|
|
|
36
38
|
const QUERY_THRESHOLDS_MS = parseThresholds(process.env.DB_QUERY_ALERT_THRESHOLDS, [500, 1000]);
|
|
37
39
|
|
|
@@ -46,6 +48,55 @@ const normalizeLabel = (value, fallback = 'unknown') => {
|
|
|
46
48
|
return String(value);
|
|
47
49
|
};
|
|
48
50
|
|
|
51
|
+
const normalizeRequestId = (value) => {
|
|
52
|
+
const token = String(value || '')
|
|
53
|
+
.trim()
|
|
54
|
+
.replace(/[^a-zA-Z0-9._:-]/g, '')
|
|
55
|
+
.slice(0, 120);
|
|
56
|
+
return token || randomUUID();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const normalizeHttpMethod = (method) => {
|
|
60
|
+
const normalized = String(method || '').trim().toUpperCase();
|
|
61
|
+
if (!normalized) return 'UNKNOWN';
|
|
62
|
+
if (normalized.length > 12) return normalized.slice(0, 12);
|
|
63
|
+
return normalized;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const toStatusClass = (statusCode) => {
|
|
67
|
+
const numeric = Number(statusCode);
|
|
68
|
+
if (!Number.isFinite(numeric) || numeric < 100) return 'unknown';
|
|
69
|
+
const head = Math.floor(numeric / 100);
|
|
70
|
+
return `${head}xx`;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const resolveRouteGroup = ({ pathname, metricsPath, catalogConfig = null } = {}) => {
|
|
74
|
+
if (pathname?.startsWith(metricsPath)) return 'metrics';
|
|
75
|
+
if (pathname === '/sitemap.xml') return 'sitemap';
|
|
76
|
+
if (pathname === '/api/marketplace/stats') return 'marketplace_stats';
|
|
77
|
+
|
|
78
|
+
const apiBasePath = catalogConfig?.apiBasePath || '';
|
|
79
|
+
const webPath = catalogConfig?.webPath || '';
|
|
80
|
+
const dataPublicPath = catalogConfig?.dataPublicPath || '';
|
|
81
|
+
const userProfilePath = catalogConfig?.userProfilePath || '';
|
|
82
|
+
|
|
83
|
+
if (apiBasePath && (pathname === apiBasePath || pathname?.startsWith(`${apiBasePath}/`))) {
|
|
84
|
+
if (pathname === `${apiBasePath}/auth/google/session` || pathname === `${apiBasePath}/me` || pathname === `${apiBasePath}/admin/session`) {
|
|
85
|
+
return 'catalog_api_auth';
|
|
86
|
+
}
|
|
87
|
+
if (pathname === `${apiBasePath}/create` || /\/(manage|finalize|stickers-upload|publish-state)(\/|$)/.test(pathname || '')) {
|
|
88
|
+
return 'catalog_api_upload';
|
|
89
|
+
}
|
|
90
|
+
if (pathname?.startsWith(`${apiBasePath}/admin`)) return 'catalog_api_admin';
|
|
91
|
+
return 'catalog_api_public';
|
|
92
|
+
}
|
|
93
|
+
if (dataPublicPath && (pathname === dataPublicPath || pathname?.startsWith(`${dataPublicPath}/`))) return 'catalog_data_asset';
|
|
94
|
+
if (userProfilePath && (pathname === userProfilePath || pathname === `${userProfilePath}/`)) return 'catalog_user_profile';
|
|
95
|
+
if (webPath && (pathname === webPath || pathname?.startsWith(`${webPath}/`))) return 'catalog_web';
|
|
96
|
+
|
|
97
|
+
return 'other';
|
|
98
|
+
};
|
|
99
|
+
|
|
49
100
|
const ensureMetrics = () => {
|
|
50
101
|
if (!METRICS_ENABLED) return null;
|
|
51
102
|
if (metrics) return metrics;
|
|
@@ -108,6 +159,25 @@ const ensureMetrics = () => {
|
|
|
108
159
|
labelNames: ['queue'],
|
|
109
160
|
registers: [registry],
|
|
110
161
|
}),
|
|
162
|
+
httpRequestsTotal: new client.Counter({
|
|
163
|
+
name: 'omnizap_http_requests_total',
|
|
164
|
+
help: 'Total de requests HTTP por rota lógica',
|
|
165
|
+
labelNames: ['route_group', 'method', 'status_class'],
|
|
166
|
+
registers: [registry],
|
|
167
|
+
}),
|
|
168
|
+
httpRequestDurationMs: new client.Histogram({
|
|
169
|
+
name: 'omnizap_http_request_duration_ms',
|
|
170
|
+
help: 'Latência de requests HTTP em ms por rota lógica',
|
|
171
|
+
buckets: [5, 10, 20, 40, 75, 120, 200, 350, 500, 750, 1000, 2000, 5000, 10000],
|
|
172
|
+
labelNames: ['route_group', 'method', 'status_class'],
|
|
173
|
+
registers: [registry],
|
|
174
|
+
}),
|
|
175
|
+
httpSloViolationTotal: new client.Counter({
|
|
176
|
+
name: 'omnizap_http_slo_violation_total',
|
|
177
|
+
help: 'Total de requests HTTP acima do SLO de latência',
|
|
178
|
+
labelNames: ['route_group', 'method'],
|
|
179
|
+
registers: [registry],
|
|
180
|
+
}),
|
|
111
181
|
messagesUpsertDurationMs: new client.Histogram({
|
|
112
182
|
name: 'omnizap_messages_upsert_duration_ms',
|
|
113
183
|
help: 'Duracao de processamento de messages.upsert em ms',
|
|
@@ -296,6 +366,25 @@ const ensureMetrics = () => {
|
|
|
296
366
|
help: 'Taxa de packs completos em relacao ao target_size (0-1)',
|
|
297
367
|
registers: [registry],
|
|
298
368
|
}),
|
|
369
|
+
stickerClassificationCycleDurationMs: new client.Histogram({
|
|
370
|
+
name: 'omnizap_sticker_classification_cycle_duration_ms',
|
|
371
|
+
help: 'Duração do ciclo de classificação de sticker em ms',
|
|
372
|
+
buckets: [50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000],
|
|
373
|
+
labelNames: ['status'],
|
|
374
|
+
registers: [registry],
|
|
375
|
+
}),
|
|
376
|
+
stickerClassificationCycleTotal: new client.Counter({
|
|
377
|
+
name: 'omnizap_sticker_classification_cycle_total',
|
|
378
|
+
help: 'Total de ciclos de classificação de sticker',
|
|
379
|
+
labelNames: ['status'],
|
|
380
|
+
registers: [registry],
|
|
381
|
+
}),
|
|
382
|
+
stickerClassificationAssetsTotal: new client.Counter({
|
|
383
|
+
name: 'omnizap_sticker_classification_assets_total',
|
|
384
|
+
help: 'Total de assets processados/classificados/falhos por ciclo',
|
|
385
|
+
labelNames: ['outcome'],
|
|
386
|
+
registers: [registry],
|
|
387
|
+
}),
|
|
299
388
|
};
|
|
300
389
|
|
|
301
390
|
return metrics;
|
|
@@ -314,6 +403,10 @@ export const startMetricsServer = () => {
|
|
|
314
403
|
if (!METRICS_ENABLED || serverStarted) return;
|
|
315
404
|
ensureMetrics();
|
|
316
405
|
server = http.createServer(async (req, res) => {
|
|
406
|
+
const requestStartedAt = Date.now();
|
|
407
|
+
const requestId = normalizeRequestId(req.headers['x-request-id']);
|
|
408
|
+
res.setHeader('X-Request-Id', requestId);
|
|
409
|
+
|
|
317
410
|
const host = req.headers.host || `${METRICS_HOST}:${METRICS_PORT}`;
|
|
318
411
|
let parsedUrl;
|
|
319
412
|
try {
|
|
@@ -322,9 +415,32 @@ export const startMetricsServer = () => {
|
|
|
322
415
|
parsedUrl = new URL(req.url || '/', 'http://localhost');
|
|
323
416
|
}
|
|
324
417
|
const pathname = parsedUrl.pathname;
|
|
418
|
+
let routeGroup = resolveRouteGroup({
|
|
419
|
+
pathname,
|
|
420
|
+
metricsPath: METRICS_PATH,
|
|
421
|
+
catalogConfig: null,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
res.once('finish', () => {
|
|
425
|
+
recordHttpRequest({
|
|
426
|
+
durationMs: Date.now() - requestStartedAt,
|
|
427
|
+
method: req.method,
|
|
428
|
+
statusCode: res.statusCode,
|
|
429
|
+
routeGroup,
|
|
430
|
+
});
|
|
431
|
+
});
|
|
325
432
|
|
|
326
433
|
try {
|
|
327
434
|
const stickerCatalogModule = await loadStickerCatalogModule();
|
|
435
|
+
const catalogConfig =
|
|
436
|
+
typeof stickerCatalogModule.getStickerCatalogConfig === 'function'
|
|
437
|
+
? stickerCatalogModule.getStickerCatalogConfig()
|
|
438
|
+
: null;
|
|
439
|
+
routeGroup = resolveRouteGroup({
|
|
440
|
+
pathname,
|
|
441
|
+
metricsPath: METRICS_PATH,
|
|
442
|
+
catalogConfig,
|
|
443
|
+
});
|
|
328
444
|
const handledByCatalog = await stickerCatalogModule.maybeHandleStickerCatalogRequest(req, res, {
|
|
329
445
|
pathname,
|
|
330
446
|
url: parsedUrl,
|
|
@@ -422,6 +538,28 @@ export const setQueueDepth = (queue, depth) => {
|
|
|
422
538
|
m.queueDepth.set({ queue: normalizeLabel(queue, 'unknown') }, value);
|
|
423
539
|
};
|
|
424
540
|
|
|
541
|
+
export const recordHttpRequest = ({ durationMs, method, statusCode, routeGroup }) => {
|
|
542
|
+
const m = ensureMetrics();
|
|
543
|
+
if (!m) return;
|
|
544
|
+
const duration = Number(durationMs);
|
|
545
|
+
if (!Number.isFinite(duration) || duration < 0) return;
|
|
546
|
+
|
|
547
|
+
const labels = {
|
|
548
|
+
route_group: normalizeLabel(routeGroup, 'other'),
|
|
549
|
+
method: normalizeHttpMethod(method),
|
|
550
|
+
status_class: toStatusClass(statusCode),
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
m.httpRequestsTotal.inc(labels);
|
|
554
|
+
m.httpRequestDurationMs.observe(labels, duration);
|
|
555
|
+
if (duration >= HTTP_SLO_TARGET_MS) {
|
|
556
|
+
m.httpSloViolationTotal.inc({
|
|
557
|
+
route_group: labels.route_group,
|
|
558
|
+
method: labels.method,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
425
563
|
export const setDbInFlight = (value) => {
|
|
426
564
|
const m = ensureMetrics();
|
|
427
565
|
if (!m) return;
|
|
@@ -732,3 +870,34 @@ export const recordStickerAutoPackCycle = ({
|
|
|
732
870
|
m.stickerAutoPackFillRate.set(Math.max(0, Math.min(1, fill)));
|
|
733
871
|
}
|
|
734
872
|
};
|
|
873
|
+
|
|
874
|
+
export const recordStickerClassificationCycle = ({
|
|
875
|
+
status = 'ok',
|
|
876
|
+
durationMs,
|
|
877
|
+
processed = 0,
|
|
878
|
+
classified = 0,
|
|
879
|
+
failed = 0,
|
|
880
|
+
} = {}) => {
|
|
881
|
+
const m = ensureMetrics();
|
|
882
|
+
if (!m) return;
|
|
883
|
+
|
|
884
|
+
const cycleStatus = normalizeLabel(status, 'ok').slice(0, 24);
|
|
885
|
+
const duration = Number(durationMs);
|
|
886
|
+
if (Number.isFinite(duration) && duration >= 0) {
|
|
887
|
+
m.stickerClassificationCycleDurationMs.observe({ status: cycleStatus }, duration);
|
|
888
|
+
}
|
|
889
|
+
m.stickerClassificationCycleTotal.inc({ status: cycleStatus });
|
|
890
|
+
|
|
891
|
+
const processedValue = Number(processed);
|
|
892
|
+
if (Number.isFinite(processedValue) && processedValue > 0) {
|
|
893
|
+
m.stickerClassificationAssetsTotal.inc({ outcome: 'processed' }, processedValue);
|
|
894
|
+
}
|
|
895
|
+
const classifiedValue = Number(classified);
|
|
896
|
+
if (Number.isFinite(classifiedValue) && classifiedValue > 0) {
|
|
897
|
+
m.stickerClassificationAssetsTotal.inc({ outcome: 'classified' }, classifiedValue);
|
|
898
|
+
}
|
|
899
|
+
const failedValue = Number(failed);
|
|
900
|
+
if (Number.isFinite(failedValue) && failedValue > 0) {
|
|
901
|
+
m.stickerClassificationAssetsTotal.inc({ outcome: 'failed' }, failedValue);
|
|
902
|
+
}
|
|
903
|
+
};
|