@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
|
@@ -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
|
-
|
|
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
|
|
|
Binary file
|
|
@@ -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`
|