@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.
Files changed (55) hide show
  1. package/.env.example +5 -0
  2. package/.prettierrc +16 -0
  3. package/README.md +13 -13
  4. package/app/modules/stickerPackModule/autoPackCollectorService.js +63 -8
  5. package/app/modules/stickerPackModule/catalogHandlers/catalogAdminHttp.js +68 -0
  6. package/app/modules/stickerPackModule/catalogHandlers/catalogAuthHttp.js +34 -0
  7. package/app/modules/stickerPackModule/catalogHandlers/catalogPublicHttp.js +179 -0
  8. package/app/modules/stickerPackModule/catalogHandlers/catalogUploadHttp.js +92 -0
  9. package/app/modules/stickerPackModule/catalogRouter.js +79 -0
  10. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +243 -0
  11. package/app/modules/stickerPackModule/domainEvents.js +61 -0
  12. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +21 -0
  13. package/app/modules/stickerPackModule/stickerAssetRepository.js +19 -0
  14. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +55 -15
  15. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +238 -0
  16. package/app/modules/stickerPackModule/stickerDomainEventBus.js +71 -0
  17. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +198 -0
  18. package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
  19. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +570 -536
  20. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +44 -0
  21. package/app/modules/stickerPackModule/stickerPackItemRepository.js +18 -0
  22. package/app/modules/stickerPackModule/stickerPackRepository.js +51 -0
  23. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +191 -0
  24. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +301 -0
  25. package/app/modules/stickerPackModule/stickerStorageService.js +111 -10
  26. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +21 -0
  27. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +59 -7
  28. package/app/observability/metrics.js +169 -0
  29. package/app/services/featureFlagService.js +137 -0
  30. package/database/index.js +5 -0
  31. package/database/migrations/20260228_0022_sticker_scale_indexes.sql +16 -0
  32. package/database/migrations/20260228_0023_sticker_pack_score_snapshot.sql +25 -0
  33. package/database/migrations/20260228_0024_domain_event_outbox.sql +42 -0
  34. package/database/migrations/20260228_0025_sticker_worker_task_idempotency_dlq.sql +23 -0
  35. package/database/migrations/20260228_0026_feature_flags.sql +21 -0
  36. package/ecosystem.prod.config.cjs +70 -9
  37. package/index.js +26 -0
  38. package/kaikybrofc-omnizap-system-2.2.6.tgz +0 -0
  39. package/observability/sticker-catalog-slo.md +83 -0
  40. package/observability/sticker-scale-hardening-rollout.md +128 -0
  41. package/package.json +7 -35
  42. package/public/assets/images/brand-icon-192.png +0 -0
  43. package/public/assets/images/brand-logo-128.webp +0 -0
  44. package/public/assets/images/hero-banner-1280.avif +0 -0
  45. package/public/assets/images/hero-banner-1280.jpg +0 -0
  46. package/public/assets/images/hero-banner-1280.webp +0 -0
  47. package/public/assets/images/hero-banner-720.avif +0 -0
  48. package/public/assets/images/hero-banner-720.webp +0 -0
  49. package/public/index.html +120 -18
  50. package/public/js/apps/homeApp.js +469 -353
  51. package/public/robots.txt +9 -0
  52. package/public/sitemap.xml +28 -0
  53. package/scripts/sticker-catalog-loadtest.mjs +208 -0
  54. package/scripts/sticker-worker-task.mjs +122 -0
  55. package/observability/mysql-exporter.cnf +0 -5
@@ -0,0 +1,137 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import logger from '../utils/logger/loggerModule.js';
4
+ import { executeQuery, TABLES } from '../../database/index.js';
5
+
6
+ const FEATURE_FLAG_CACHE_TTL_MS = Math.max(
7
+ 5_000,
8
+ Number(process.env.FEATURE_FLAG_CACHE_TTL_MS) || 30_000,
9
+ );
10
+
11
+ let cacheState = {
12
+ loadedAt: 0,
13
+ byName: new Map(),
14
+ tableAvailable: true,
15
+ };
16
+
17
+ const normalizeFlagName = (value) =>
18
+ String(value || '')
19
+ .trim()
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9_:-]/g, '')
22
+ .slice(0, 120);
23
+
24
+ const toPercent = (value) => {
25
+ const numeric = Number(value);
26
+ if (!Number.isFinite(numeric)) return 0;
27
+ return Math.max(0, Math.min(100, Math.floor(numeric)));
28
+ };
29
+
30
+ const toBool = (value, fallback = false) => {
31
+ if (value === true || value === 1) return true;
32
+ if (value === false || value === 0) return false;
33
+ if (value === undefined || value === null || value === '') return fallback;
34
+ const normalized = String(value).trim().toLowerCase();
35
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
36
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
37
+ return fallback;
38
+ };
39
+
40
+ const normalizeRow = (row) => ({
41
+ flag_name: normalizeFlagName(row?.flag_name),
42
+ is_enabled: toBool(row?.is_enabled, false),
43
+ rollout_percent: toPercent(row?.rollout_percent ?? 100),
44
+ });
45
+
46
+ const resolveCohortBucket = (subjectKey) => {
47
+ const normalized = String(subjectKey || '').trim();
48
+ if (!normalized) return 0;
49
+ const digest = createHash('sha1').update(normalized).digest();
50
+ const value = digest.readUInt32BE(0);
51
+ return value % 100;
52
+ };
53
+
54
+ const loadFlagsFromDatabase = async () => {
55
+ const rows = await executeQuery(
56
+ `SELECT flag_name, is_enabled, rollout_percent
57
+ FROM ${TABLES.FEATURE_FLAG}`,
58
+ [],
59
+ );
60
+
61
+ const byName = new Map();
62
+ (Array.isArray(rows) ? rows : []).forEach((row) => {
63
+ const normalized = normalizeRow(row);
64
+ if (!normalized.flag_name) return;
65
+ byName.set(normalized.flag_name, normalized);
66
+ });
67
+ return byName;
68
+ };
69
+
70
+ export const refreshFeatureFlags = async ({ force = false } = {}) => {
71
+ const now = Date.now();
72
+ const isFresh = now - cacheState.loadedAt < FEATURE_FLAG_CACHE_TTL_MS;
73
+ if (!force && isFresh) return cacheState.byName;
74
+
75
+ if (!cacheState.tableAvailable) return cacheState.byName;
76
+
77
+ try {
78
+ const byName = await loadFlagsFromDatabase();
79
+ cacheState = {
80
+ loadedAt: now,
81
+ byName,
82
+ tableAvailable: true,
83
+ };
84
+ } catch (error) {
85
+ if (error?.code === 'ER_NO_SUCH_TABLE') {
86
+ cacheState = {
87
+ ...cacheState,
88
+ tableAvailable: false,
89
+ };
90
+ logger.warn('Tabela de feature flags indisponível. Usando fallback por env/default.', {
91
+ action: 'feature_flag_table_unavailable',
92
+ });
93
+ return cacheState.byName;
94
+ }
95
+ logger.warn('Falha ao carregar feature flags. Mantendo cache anterior.', {
96
+ action: 'feature_flag_refresh_failed',
97
+ error: error?.message,
98
+ });
99
+ }
100
+
101
+ return cacheState.byName;
102
+ };
103
+
104
+ const resolveEnvFallback = (flagName, fallback) => {
105
+ const envKey = `FEATURE_${flagName.toUpperCase()}`;
106
+ return toBool(process.env[envKey], fallback);
107
+ };
108
+
109
+ export const isFeatureEnabled = async (
110
+ flagName,
111
+ { fallback = false, subjectKey = '' } = {},
112
+ ) => {
113
+ const normalizedFlagName = normalizeFlagName(flagName);
114
+ if (!normalizedFlagName) return Boolean(fallback);
115
+
116
+ const byName = await refreshFeatureFlags();
117
+ const entry = byName.get(normalizedFlagName);
118
+ if (!entry) {
119
+ return resolveEnvFallback(normalizedFlagName, fallback);
120
+ }
121
+
122
+ if (!entry.is_enabled) return false;
123
+ if (entry.rollout_percent >= 100) return true;
124
+ if (entry.rollout_percent <= 0) return false;
125
+
126
+ const bucket = resolveCohortBucket(subjectKey || normalizedFlagName);
127
+ return bucket < entry.rollout_percent;
128
+ };
129
+
130
+ export const getFeatureFlagsSnapshot = async () => {
131
+ const byName = await refreshFeatureFlags();
132
+ return Array.from(byName.values()).map((entry) => ({
133
+ flag_name: entry.flag_name,
134
+ is_enabled: Boolean(entry.is_enabled),
135
+ rollout_percent: Number(entry.rollout_percent || 0),
136
+ }));
137
+ };
package/database/index.js CHANGED
@@ -104,8 +104,13 @@ export const TABLES = {
104
104
  SEMANTIC_THEME_SUGGESTION_CACHE: 'semantic_theme_suggestion_cache',
105
105
  STICKER_PACK_ENGAGEMENT: 'sticker_pack_engagement',
106
106
  STICKER_PACK_INTERACTION_EVENT: 'sticker_pack_interaction_event',
107
+ STICKER_PACK_SCORE_SNAPSHOT: 'sticker_pack_score_snapshot',
107
108
  STICKER_ASSET_REPROCESS_QUEUE: 'sticker_asset_reprocess_queue',
108
109
  STICKER_WORKER_TASK_QUEUE: 'sticker_worker_task_queue',
110
+ STICKER_WORKER_TASK_DLQ: 'sticker_worker_task_dlq',
111
+ DOMAIN_EVENT_OUTBOX: 'domain_event_outbox',
112
+ DOMAIN_EVENT_OUTBOX_DLQ: 'domain_event_outbox_dlq',
113
+ FEATURE_FLAG: 'feature_flag',
109
114
  STICKER_WEB_GOOGLE_USER: 'sticker_web_google_user',
110
115
  STICKER_WEB_GOOGLE_SESSION: 'sticker_web_google_session',
111
116
  STICKER_WEB_ADMIN_BAN: 'sticker_web_admin_ban',
@@ -0,0 +1,16 @@
1
+ ALTER TABLE sticker_asset
2
+ ADD INDEX idx_sticker_asset_created_at (created_at);
3
+
4
+ ALTER TABLE sticker_asset_classification
5
+ ADD INDEX idx_sticker_asset_classification_confidence (confidence),
6
+ ADD INDEX idx_sticker_asset_classification_updated_at (updated_at),
7
+ ADD INDEX idx_sticker_asset_classification_version_updated (classification_version, updated_at);
8
+
9
+ ALTER TABLE sticker_pack_item
10
+ ADD INDEX idx_sticker_pack_item_sticker_id (sticker_id);
11
+
12
+ ALTER TABLE sticker_pack
13
+ ADD INDEX idx_sticker_pack_catalog_lookup (deleted_at, status, pack_status, visibility, updated_at);
14
+
15
+ ALTER TABLE sticker_pack_interaction_event
16
+ ADD INDEX idx_sticker_pack_interaction_created_at (created_at);
@@ -0,0 +1,25 @@
1
+ CREATE TABLE IF NOT EXISTS sticker_pack_score_snapshot (
2
+ pack_id CHAR(36) PRIMARY KEY,
3
+ ranking_score DECIMAL(10,6) NOT NULL DEFAULT 0,
4
+ pack_score DECIMAL(10,6) NOT NULL DEFAULT 0,
5
+ trend_score DECIMAL(10,6) NOT NULL DEFAULT 0,
6
+ quality_score DECIMAL(10,6) NOT NULL DEFAULT 0,
7
+ engagement_score DECIMAL(10,6) NOT NULL DEFAULT 0,
8
+ diversity_score DECIMAL(10,6) NOT NULL DEFAULT 0,
9
+ cohesion_score DECIMAL(10,6) NOT NULL DEFAULT 0,
10
+ sensitive_content TINYINT(1) NOT NULL DEFAULT 0,
11
+ nsfw_level ENUM('safe', 'suggestive', 'explicit') NOT NULL DEFAULT 'safe',
12
+ sticker_count INT UNSIGNED NOT NULL DEFAULT 0,
13
+ tags JSON NULL,
14
+ scores_json JSON NULL,
15
+ source_version VARCHAR(32) NOT NULL DEFAULT 'v1',
16
+ refreshed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
17
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
18
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
19
+ CONSTRAINT fk_sticker_pack_score_snapshot_pack
20
+ FOREIGN KEY (pack_id) REFERENCES sticker_pack(id)
21
+ ON DELETE CASCADE ON UPDATE CASCADE,
22
+ INDEX idx_sticker_pack_score_snapshot_ranking (ranking_score),
23
+ INDEX idx_sticker_pack_score_snapshot_trend (trend_score),
24
+ INDEX idx_sticker_pack_score_snapshot_refresh (refreshed_at)
25
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,42 @@
1
+ CREATE TABLE IF NOT EXISTS domain_event_outbox (
2
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
3
+ event_type VARCHAR(96) NOT NULL,
4
+ aggregate_type VARCHAR(96) NOT NULL,
5
+ aggregate_id VARCHAR(128) NOT NULL,
6
+ payload JSON NULL,
7
+ status ENUM('pending', 'processing', 'completed', 'failed') NOT NULL DEFAULT 'pending',
8
+ priority TINYINT UNSIGNED NOT NULL DEFAULT 50,
9
+ idempotency_key VARCHAR(180) NULL,
10
+ available_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
11
+ attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
12
+ max_attempts TINYINT UNSIGNED NOT NULL DEFAULT 10,
13
+ worker_token CHAR(36) NULL,
14
+ last_error VARCHAR(255) NULL,
15
+ locked_at TIMESTAMP NULL DEFAULT NULL,
16
+ processed_at TIMESTAMP NULL DEFAULT NULL,
17
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
18
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
19
+ UNIQUE KEY uq_domain_event_outbox_idempotency_key (idempotency_key),
20
+ INDEX idx_domain_event_outbox_status_sched (status, available_at, priority),
21
+ INDEX idx_domain_event_outbox_event_type (event_type, status, available_at),
22
+ INDEX idx_domain_event_outbox_aggregate (aggregate_type, aggregate_id, created_at),
23
+ INDEX idx_domain_event_outbox_worker_token (worker_token)
24
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
25
+
26
+ CREATE TABLE IF NOT EXISTS domain_event_outbox_dlq (
27
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
28
+ outbox_event_id BIGINT UNSIGNED NULL,
29
+ event_type VARCHAR(96) NOT NULL,
30
+ aggregate_type VARCHAR(96) NOT NULL,
31
+ aggregate_id VARCHAR(128) NOT NULL,
32
+ payload JSON NULL,
33
+ attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
34
+ max_attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
35
+ last_error VARCHAR(255) NULL,
36
+ failed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
37
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
38
+ UNIQUE KEY uq_domain_event_outbox_dlq_outbox_event (outbox_event_id),
39
+ INDEX idx_domain_event_outbox_dlq_event (event_type, failed_at),
40
+ INDEX idx_domain_event_outbox_dlq_aggregate (aggregate_type, aggregate_id, failed_at),
41
+ INDEX idx_domain_event_outbox_dlq_outbox_event_id (outbox_event_id)
42
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,23 @@
1
+ ALTER TABLE sticker_worker_task_queue
2
+ ADD COLUMN IF NOT EXISTS idempotency_key VARCHAR(180) NULL AFTER task_type;
3
+
4
+ CREATE UNIQUE INDEX uq_sticker_worker_task_idempotency_key
5
+ ON sticker_worker_task_queue (idempotency_key);
6
+
7
+ CREATE TABLE IF NOT EXISTS sticker_worker_task_dlq (
8
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
9
+ task_id BIGINT UNSIGNED NULL,
10
+ task_type ENUM('classification_cycle', 'curation_cycle', 'rebuild_cycle') NOT NULL,
11
+ payload JSON NULL,
12
+ idempotency_key VARCHAR(180) NULL,
13
+ attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
14
+ max_attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
15
+ priority TINYINT UNSIGNED NOT NULL DEFAULT 0,
16
+ last_error VARCHAR(255) NULL,
17
+ failed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
18
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
19
+ UNIQUE KEY uq_sticker_worker_task_dlq_task_id (task_id),
20
+ INDEX idx_sticker_worker_task_dlq_type_failed_at (task_type, failed_at),
21
+ INDEX idx_sticker_worker_task_dlq_task_id (task_id),
22
+ INDEX idx_sticker_worker_task_dlq_idempotency_key (idempotency_key)
23
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,21 @@
1
+ CREATE TABLE IF NOT EXISTS feature_flag (
2
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
3
+ flag_name VARCHAR(120) NOT NULL,
4
+ is_enabled TINYINT(1) NOT NULL DEFAULT 0,
5
+ rollout_percent TINYINT UNSIGNED NOT NULL DEFAULT 100,
6
+ description VARCHAR(255) NULL,
7
+ updated_by VARCHAR(120) NULL,
8
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
9
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+ UNIQUE KEY uq_feature_flag_name (flag_name),
11
+ INDEX idx_feature_flag_enabled (is_enabled, rollout_percent)
12
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
13
+
14
+ INSERT INTO feature_flag (flag_name, is_enabled, rollout_percent, description)
15
+ VALUES
16
+ ('enable_ranking_snapshot_read', 1, 100, 'Leitura HTTP do ranking/sinais a partir de snapshot'),
17
+ ('enable_domain_event_outbox', 1, 100, 'Publicacao e consumo de eventos de dominio via outbox interno'),
18
+ ('enable_worker_dedicated_processes', 0, 100, 'Ativa workers dedicados por tipo de task'),
19
+ ('enable_object_storage_delivery', 0, 100, 'Entrega de assets via object storage/CDN com URL segura')
20
+ ON DUPLICATE KEY UPDATE
21
+ description = VALUES(description);
@@ -2,6 +2,21 @@ require('dotenv').config();
2
2
 
3
3
  const appName = process.env.PM2_APP_NAME || 'omnizap-system';
4
4
 
5
+ const baseEnv = {
6
+ NODE_ENV: 'production',
7
+ COMMAND_PREFIX: '/',
8
+ LOG_LEVEL: 'info',
9
+ DB_LOG_EVERY_QUERY: 'false',
10
+ DB_MONITOR_ENABLED: 'false',
11
+ LID_BACKFILL_ON_START: 'false',
12
+ STICKER_CLASSIFICATION_BACKGROUND_ENABLED: 'true',
13
+ STICKER_REPROCESS_QUEUE_ENABLED: 'true',
14
+ STICKER_AUTO_PACK_BY_TAGS_ENABLED: 'true',
15
+ STICKER_WORKER_PIPELINE_ENABLED: 'true',
16
+ STICKER_WORKER_PIPELINE_INLINE_POLLER_ENABLED: 'true',
17
+ STICKER_DEDICATED_WORKERS_ENABLED: 'true',
18
+ };
19
+
5
20
  module.exports = {
6
21
  apps: [
7
22
  {
@@ -17,19 +32,65 @@ module.exports = {
17
32
  out_file: `logs/${appName}-out.log`,
18
33
  error_file: `logs/${appName}-error.log`,
19
34
  env: {
20
- NODE_ENV: 'production',
21
- COMMAND_PREFIX: '/',
22
- LOG_LEVEL: 'info',
23
- DB_LOG_EVERY_QUERY: 'false',
24
- DB_MONITOR_ENABLED: 'false',
25
- LID_BACKFILL_ON_START: 'false',
26
- STICKER_CLASSIFICATION_BACKGROUND_ENABLED: 'true',
27
- STICKER_REPROCESS_QUEUE_ENABLED: 'true',
28
- STICKER_AUTO_PACK_BY_TAGS_ENABLED: 'true',
35
+ ...baseEnv,
29
36
  },
30
37
  wait_ready: true,
31
38
  listen_timeout: 10000,
32
39
  kill_timeout: 5000,
33
40
  },
41
+ {
42
+ name: `${appName}-worker-classification`,
43
+ script: './scripts/sticker-worker-task.mjs',
44
+ args: '--task-type classification_cycle',
45
+ cwd: __dirname,
46
+ exec_mode: 'fork',
47
+ instances: 1,
48
+ autorestart: true,
49
+ watch: false,
50
+ max_memory_restart: '2G',
51
+ log_date_format: 'YYYY-MM-DD HH:mm:ss',
52
+ out_file: `logs/${appName}-worker-classification-out.log`,
53
+ error_file: `logs/${appName}-worker-classification-error.log`,
54
+ env: {
55
+ ...baseEnv,
56
+ },
57
+ kill_timeout: 5000,
58
+ },
59
+ {
60
+ name: `${appName}-worker-curation`,
61
+ script: './scripts/sticker-worker-task.mjs',
62
+ args: '--task-type curation_cycle',
63
+ cwd: __dirname,
64
+ exec_mode: 'fork',
65
+ instances: 1,
66
+ autorestart: true,
67
+ watch: false,
68
+ max_memory_restart: '2G',
69
+ log_date_format: 'YYYY-MM-DD HH:mm:ss',
70
+ out_file: `logs/${appName}-worker-curation-out.log`,
71
+ error_file: `logs/${appName}-worker-curation-error.log`,
72
+ env: {
73
+ ...baseEnv,
74
+ },
75
+ kill_timeout: 5000,
76
+ },
77
+ {
78
+ name: `${appName}-worker-rebuild`,
79
+ script: './scripts/sticker-worker-task.mjs',
80
+ args: '--task-type rebuild_cycle',
81
+ cwd: __dirname,
82
+ exec_mode: 'fork',
83
+ instances: 1,
84
+ autorestart: true,
85
+ watch: false,
86
+ max_memory_restart: '2G',
87
+ log_date_format: 'YYYY-MM-DD HH:mm:ss',
88
+ out_file: `logs/${appName}-worker-rebuild-out.log`,
89
+ error_file: `logs/${appName}-worker-rebuild-error.log`,
90
+ env: {
91
+ ...baseEnv,
92
+ },
93
+ kill_timeout: 5000,
94
+ },
34
95
  ],
35
96
  };
package/index.js CHANGED
@@ -39,6 +39,14 @@ import {
39
39
  startStickerWorkerPipeline,
40
40
  stopStickerWorkerPipeline,
41
41
  } from './app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js';
42
+ import {
43
+ startStickerPackScoreSnapshotRuntime,
44
+ stopStickerPackScoreSnapshotRuntime,
45
+ } from './app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js';
46
+ import {
47
+ startStickerDomainEventConsumer,
48
+ stopStickerDomainEventConsumer,
49
+ } from './app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js';
42
50
 
43
51
  /**
44
52
  * Timeout máximo para inicialização do banco (criar/verificar DB + tabelas).
@@ -216,6 +224,8 @@ async function startApp() {
216
224
  startStickerClassificationBackground();
217
225
  startStickerAutoPackByTagsBackground();
218
226
  }
227
+ startStickerPackScoreSnapshotRuntime();
228
+ startStickerDomainEventConsumer();
219
229
 
220
230
  // Backfill é opcional, rodando em background.
221
231
  const shouldBackfill = process.env.LID_BACKFILL_ON_START !== 'false';
@@ -363,6 +373,22 @@ async function shutdown(signal, error) {
363
373
  });
364
374
  }
365
375
 
376
+ try {
377
+ stopStickerPackScoreSnapshotRuntime();
378
+ } catch (snapshotError) {
379
+ logger.warn('Falha ao encerrar runtime de snapshot de score dos packs.', {
380
+ error: snapshotError?.message,
381
+ });
382
+ }
383
+
384
+ try {
385
+ stopStickerDomainEventConsumer();
386
+ } catch (consumerError) {
387
+ logger.warn('Falha ao encerrar consumidor interno de eventos de domínio.', {
388
+ error: consumerError?.message,
389
+ });
390
+ }
391
+
366
392
  // 5) Encerrar MySQL pool
367
393
  await closeDatabasePool();
368
394
 
@@ -0,0 +1,83 @@
1
+ # Sticker Catalog 10x Baseline e SLOs
2
+
3
+ Este documento define a baseline operacional da camada HTTP + pipeline de classificação para o módulo de stickers.
4
+
5
+ ## 1. Metas SLO (fase inicial)
6
+
7
+ ### HTTP catálogo (`/api/sticker-packs*`, `/stickers*`, `/api/marketplace/stats`)
8
+
9
+ - **Latência p95**: `<= 750ms`
10
+ - **Latência p99**: `<= 1500ms`
11
+ - **Taxa de erro (5xx + timeout)**: `<= 2%` por janela de 5 minutos
12
+ - **Throughput alvo**: escalar linearmente com workers/processos sem aumento abrupto do p95
13
+
14
+ ### Classificação de stickers
15
+
16
+ - **Duração média do ciclo**: `<= 10s`
17
+ - **Throughput mínimo (assets classificados/min)**: `>= 300` (ajustar por hardware)
18
+ - **Backlog de fila (`sticker_reprocess_pending`)**: tendência de queda após picos; alerta se cresce por mais de 15 min
19
+
20
+ ## 2. Métricas instrumentadas
21
+
22
+ ### HTTP
23
+
24
+ - `omnizap_http_requests_total{route_group,method,status_class}`
25
+ - `omnizap_http_request_duration_ms{route_group,method,status_class}`
26
+ - `omnizap_http_slo_violation_total{route_group,method}`
27
+
28
+ `route_group` segmenta tráfego em:
29
+
30
+ - `catalog_api_public`
31
+ - `catalog_api_auth`
32
+ - `catalog_api_admin`
33
+ - `catalog_api_upload`
34
+ - `catalog_web`
35
+ - `catalog_data_asset`
36
+ - `catalog_user_profile`
37
+ - `marketplace_stats`
38
+ - `metrics`
39
+ - `other`
40
+
41
+ ### Classificação
42
+
43
+ - `omnizap_sticker_classification_cycle_duration_ms{status}`
44
+ - `omnizap_sticker_classification_cycle_total{status}`
45
+ - `omnizap_sticker_classification_assets_total{outcome}`
46
+ - `omnizap_queue_depth{queue}`
47
+
48
+ ## 3. Tracing mínimo
49
+
50
+ - Cada request HTTP agora recebe/propaga `X-Request-Id`.
51
+ - Se o cliente enviar `X-Request-Id`, o valor é reaproveitado.
52
+ - Sem header, o servidor gera UUID.
53
+
54
+ ## 4. Baseline de carga (script local)
55
+
56
+ Script: `scripts/sticker-catalog-loadtest.mjs`
57
+
58
+ Exemplo:
59
+
60
+ ```bash
61
+ node scripts/sticker-catalog-loadtest.mjs \
62
+ --base-url http://127.0.0.1:9102 \
63
+ --duration-seconds 60 \
64
+ --concurrency 40 \
65
+ --paths "/api/sticker-packs?limit=24&sort=popular,/api/sticker-packs/stats,/api/sticker-packs/creators?limit=25" \
66
+ --out /tmp/sticker-loadtest-report.json
67
+ ```
68
+
69
+ Interpretação rápida:
70
+
71
+ - `latency_ms.p95 <= 750` = SLO de latência cumprido
72
+ - `error_rate <= 0.02` = estabilidade aceitável
73
+ - `throughput_rps` = referência para comparar antes/depois de otimizações
74
+
75
+ ## 5. Gate de rollout sugerido
76
+
77
+ 1. Capturar baseline com carga atual.
78
+ 2. Aplicar mudança de arquitetura/índice/cache.
79
+ 3. Reexecutar carga com mesmos parâmetros.
80
+ 4. Aprovar rollout apenas se:
81
+ - p95 não piorar mais de 10%
82
+ - erro não subir acima de 2%
83
+ - backlog voltar ao patamar normal em até 15 min
@@ -0,0 +1,128 @@
1
+ # Sticker 10x Hardening And Rollout
2
+
3
+ ## Scope
4
+
5
+ This runbook covers phases 4-8 of the sticker-pack scale plan:
6
+
7
+ 1. ranking snapshot read path
8
+ 2. internal outbox/event consumer
9
+ 3. dedicated workers (classification/curation/rebuild)
10
+ 4. object storage delivery with secure URLs
11
+ 5. canary rollout, rollback, and final tuning
12
+
13
+ ## Feature Flags
14
+
15
+ Flags are stored in `feature_flag`:
16
+
17
+ - `enable_ranking_snapshot_read`
18
+ - `enable_domain_event_outbox`
19
+ - `enable_worker_dedicated_processes`
20
+ - `enable_object_storage_delivery`
21
+
22
+ ### Query Current Status
23
+
24
+ ```sql
25
+ SELECT flag_name, is_enabled, rollout_percent, updated_at
26
+ FROM feature_flag
27
+ WHERE flag_name IN (
28
+ 'enable_ranking_snapshot_read',
29
+ 'enable_domain_event_outbox',
30
+ 'enable_worker_dedicated_processes',
31
+ 'enable_object_storage_delivery'
32
+ )
33
+ ORDER BY flag_name;
34
+ ```
35
+
36
+ ### Update Rollout Percent
37
+
38
+ ```sql
39
+ UPDATE feature_flag
40
+ SET is_enabled = 1, rollout_percent = 25, updated_by = 'ops'
41
+ WHERE flag_name = 'enable_worker_dedicated_processes';
42
+ ```
43
+
44
+ ### Emergency Disable
45
+
46
+ ```sql
47
+ UPDATE feature_flag
48
+ SET is_enabled = 0, rollout_percent = 0, updated_by = 'ops'
49
+ WHERE flag_name IN (
50
+ 'enable_worker_dedicated_processes',
51
+ 'enable_object_storage_delivery',
52
+ 'enable_domain_event_outbox'
53
+ );
54
+ ```
55
+
56
+ ## Canary Sequence
57
+
58
+ 1. `enable_ranking_snapshot_read`: 10% -> 50% -> 100%
59
+ 2. `enable_domain_event_outbox`: 10% -> 50% -> 100%
60
+ 3. start dedicated worker processes and set `enable_worker_dedicated_processes`: 10% -> 50% -> 100%
61
+ 4. `enable_object_storage_delivery`: 5% -> 25% -> 100%
62
+
63
+ Promotion gate for each step:
64
+
65
+ - HTTP p95 within target
66
+ - queue backlog stable (`pending`, `failed`)
67
+ - outbox DLQ not growing unexpectedly
68
+ - no sustained error-rate increase
69
+
70
+ ## Dedicated Workers
71
+
72
+ Run workers as isolated processes:
73
+
74
+ ```bash
75
+ npm run worker:sticker:classification
76
+ npm run worker:sticker:curation
77
+ npm run worker:sticker:rebuild
78
+ ```
79
+
80
+ PM2 production profile includes these workers in `ecosystem.prod.config.cjs`.
81
+
82
+ ## 10x Validation
83
+
84
+ ### HTTP Stress
85
+
86
+ ```bash
87
+ npm run loadtest:stickers -- --base-url http://127.0.0.1:9102 --duration-seconds 120 --concurrency 200 --slo-ms 750
88
+ ```
89
+
90
+ ### Queue/Worker Validation
91
+
92
+ Monitor:
93
+
94
+ - `sticker_worker_tasks_pending`
95
+ - `sticker_worker_tasks_processing`
96
+ - `sticker_worker_tasks_failed`
97
+ - `domain_event_outbox_pending`
98
+ - `domain_event_outbox_failed`
99
+
100
+ Acceptance:
101
+
102
+ - failed queues remain near zero (transient spikes allowed)
103
+ - pending queues recover after load burst
104
+ - no monotonic growth in DLQ tables
105
+
106
+ ## Rollback Plan
107
+
108
+ 1. Disable `enable_object_storage_delivery`.
109
+ 2. Disable `enable_worker_dedicated_processes` (inline poller resumes).
110
+ 3. Disable `enable_domain_event_outbox` if event flow is unstable.
111
+ 4. Keep `enable_ranking_snapshot_read` enabled only if snapshot freshness is healthy.
112
+
113
+ Data safety notes:
114
+
115
+ - tasks/events are persisted in SQL queues
116
+ - failed terminal tasks/events are preserved in DLQ tables
117
+ - local disk read path remains fallback for sticker asset serving
118
+
119
+ ## Post-Rollout Tuning
120
+
121
+ Tune these env vars after baseline:
122
+
123
+ - `STICKER_WORKER_CLASSIFICATION_CADENCE_MS`
124
+ - `STICKER_WORKER_CURATION_CADENCE_MS`
125
+ - `STICKER_WORKER_REBUILD_CADENCE_MS`
126
+ - `STICKER_DEDICATED_WORKER_POLL_INTERVAL_MS`
127
+ - `STICKER_SCORE_SNAPSHOT_REFRESH_INTERVAL_MS`
128
+ - `STICKER_OBJECT_STORAGE_SIGNED_URL_TTL_SECONDS`