funifier-mcp 0.2.25 → 0.2.27
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/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +5 -2
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +1011 -77
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/core/api-client.d.ts +21 -1
- package/dist/core/api-client.d.ts.map +1 -1
- package/dist/core/api-client.js +154 -1
- package/dist/core/api-client.js.map +1 -1
- package/dist/core/constants.d.ts +14 -0
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +14 -0
- package/dist/core/constants.js.map +1 -1
- package/dist/core/types/Folder.d.ts +16 -0
- package/dist/core/types/Folder.d.ts.map +1 -0
- package/dist/core/types/Folder.js +3 -0
- package/dist/core/types/Folder.js.map +1 -0
- package/dist/core/types/FolderContent.d.ts +10 -0
- package/dist/core/types/FolderContent.d.ts.map +1 -0
- package/dist/core/types/FolderContent.js +3 -0
- package/dist/core/types/FolderContent.js.map +1 -0
- package/dist/core/types/FolderContentType.d.ts +10 -0
- package/dist/core/types/FolderContentType.d.ts.map +1 -0
- package/dist/core/types/FolderContentType.js +3 -0
- package/dist/core/types/FolderContentType.js.map +1 -0
- package/dist/core/types/FolderLog.d.ts +11 -0
- package/dist/core/types/FolderLog.d.ts.map +1 -0
- package/dist/core/types/FolderLog.js +3 -0
- package/dist/core/types/FolderLog.js.map +1 -0
- package/dist/core/types/index.d.ts +4 -0
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js +4 -0
- package/dist/core/types/index.js.map +1 -1
- package/dist/mcp/bundle.js +121 -87
- package/dist/mcp/check-update.d.ts +2 -0
- package/dist/mcp/check-update.d.ts.map +1 -0
- package/dist/mcp/check-update.js +44 -0
- package/dist/mcp/check-update.js.map +1 -0
- package/dist/mcp/index.js +5 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/_char-guard.js +1 -1
- package/dist/mcp/tools/_char-guard.js.map +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
- package/dist/mcp/tools/_fetch-current.js +12 -0
- package/dist/mcp/tools/_fetch-current.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +33 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts +4 -0
- package/dist/mcp/tools/folder.d.ts.map +1 -0
- package/dist/mcp/tools/folder.js +68 -0
- package/dist/mcp/tools/folder.js.map +1 -0
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +5 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +26 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +192 -1
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/datasource-funifier-docs/.search-index.json +0 -17318
- package/datasource-funifier-docs/.skills-map.json +0 -73
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,58 +1,1111 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `scheduler`
|
|
2
2
|
|
|
3
3
|
**Acesso Studio:** `/studio/scheduler`
|
|
4
|
-
**API Endpoint:** `/v3/scheduler`
|
|
4
|
+
**API Endpoint:** `/v3/scheduler` (gamificação) · `/v3/system/scheduler/*` (system) · `/v3/util/cron/status` (status agregado)
|
|
5
|
+
**Coleção MongoDB:** `scheduler` (configurações por gamificação) · `scheduler_log` (logs de execução) · `scheduler` no banco do sistema (índice de totais por apiKey)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
---
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
## 1. Visão Geral
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
O módulo `scheduler` executa rotinas em horários determinados por expressões cron Quartz. Cada gamificação tem suas próprias configurações de scheduler armazenadas em MongoDB e os jobs são instalados em um único `Scheduler` Quartz (StdScheduler) mantido na JVM pelo `SchedulerSystem` (singleton system-level).
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
- Para debitar pontos de jogadores inativos
|
|
14
|
-
- Para enviar lembretes automáticos de metas
|
|
15
|
-
- Para campanhas temporais com início/fim automático
|
|
13
|
+
O scheduler opera em dois modos mutuamente exclusivos:
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
- **Script mode** — executa um bloco Groovy arbitrário dentro de uma sandbox (`SecureASTCustomizer` + `TriggerExpressionChecker`). Selecionado quando os campos `entity`, `event` e `item` **não** estão todos preenchidos.
|
|
16
|
+
- **Entity mode** — despacha para um `SchedulerCallback` registrado por entidade na `ManagerFactory`. Selecionado quando `entity` E `event` E `item` estão preenchidos simultaneamente. O `script` é ignorado neste modo.
|
|
18
17
|
|
|
19
|
-
O
|
|
18
|
+
O Quartz `Scheduler` é compartilhado pelo processo. Ele hospeda três tipos de job:
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
- `FunifierJob` — schedulers de gamificação (group = `apiKey`)
|
|
21
|
+
- `ReportSchedulerJob` — relatórios system-level (group = `report._id`)
|
|
22
|
+
- `ServerPingJob` — ping do servidor para o servidor central (group = `server._id`)
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
2. Se precisar de uma classe não importada, use o nome completo: `com.example.MinhaClasse`
|
|
25
|
-
3. **`manager`** (ManagerFactory) está disponível como campo da classe
|
|
26
|
-
4. **Método principal:** `void execute()` (sem parâmetros)
|
|
27
|
-
5. **Timeout padrão de 10 segundos** — pode ser customizado via campo `timeout` (em **segundos**) na API: `POST /v3/scheduler` com `{"_id": "scheduler_id", "timeout": 30}`. Este campo não aparece no Studio.
|
|
28
|
-
6. Scripts rodam em **Groovy** — cuidado com `$` em GStrings
|
|
29
|
-
7. Use apenas **ASCII em comentários**
|
|
24
|
+
Não há persistência de estado do Quartz: o MongoDB é a fonte de verdade. Em cada `contextInitialized` o Quartz é repovoado a partir do banco. Reiniciar o processo perde os contadores de limite diário (`SchedulerStatistics` é em memória).
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
Integrações principais:
|
|
32
27
|
|
|
33
|
-
|
|
28
|
+
- **LastMile** (`LastMileManager`) — único callback registrado para `entity = last_mile`. Cria/remove scheduler entity-bound automaticamente.
|
|
29
|
+
- **Bonus** (`BonusManager`) — código de criação de scheduler está totalmente comentado; apenas o cleanup no `delete` continua ativo.
|
|
30
|
+
- **App** (`AppManager.funifierCreateScheduler`) — instala schedulers a partir de templates ao publicar um app.
|
|
31
|
+
- **GameManager.delete** — chama `getSchedulerManager().deleteAll()` em cascata ao excluir uma gamificação.
|
|
32
|
+
- **ReportManager / ServerManager** — usam o Quartz compartilhado via `SystemFactory.getSchedulerManager().getQuartzScheduler()`, mas com jobs e coleções próprios (não passam pela coleção `scheduler`).
|
|
33
|
+
- **StatisticManager** — gerencia o limite diário (`GamificationLimits.dailySchedulerExecutions`, default 100).
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
- [ ] Definir expressão CRON
|
|
37
|
-
- [ ] Escrever script Java
|
|
38
|
-
- [ ] Testar em ambiente de homologação
|
|
35
|
+
---
|
|
39
36
|
|
|
40
|
-
##
|
|
37
|
+
## 2. Arquitetura e Fluxos
|
|
41
38
|
|
|
42
|
-
###
|
|
43
|
-
**Método:** GET
|
|
44
|
-
**Endpoint:** `/v3/scheduler`
|
|
39
|
+
### 2.1 Inicialização do servidor — `SchedulerSystem`
|
|
45
40
|
|
|
46
|
-
|
|
47
|
-
**Método:** POST
|
|
48
|
-
**Endpoint:** `/v3/scheduler`
|
|
41
|
+
Construtor (executado quando `SystemFactory` é instanciado):
|
|
49
42
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
```
|
|
44
|
+
new StdSchedulerFactory().getScheduler()
|
|
45
|
+
quartzScheduler.startDelayed(10) // delay de 10 segundos
|
|
46
|
+
```
|
|
53
47
|
|
|
54
|
-
|
|
48
|
+
A inicialização em si dos jobs ocorre em `Configuration.contextInitialized` (web listener / boot do JBoss), que chama `SystemFactory.getInstance().getSchedulerManager().contextInitialized()`:
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
```
|
|
51
|
+
1. Lê coleção scheduler do banco do sistema:
|
|
52
|
+
distinct("_id").query({total: {$gt: 0}}) ← retorna lista de apiKeys com schedulers
|
|
53
|
+
2. Para cada apiKey:
|
|
54
|
+
ManagerFactory.getInstance(apiKey).getJongoConnection().getCollection("scheduler")
|
|
55
|
+
.find({active: true})
|
|
56
|
+
→ para cada SchedulerConfig:
|
|
57
|
+
JobDetail = FunifierJob com identidade (scheduler.id, apiKey)
|
|
58
|
+
CronTrigger com identidade (scheduler.id + "trigger", apiKey)
|
|
59
|
+
com timezone aplicado se não vazio
|
|
60
|
+
quartzScheduler.scheduleJob(job, trigger)
|
|
61
|
+
3. system_report: find({active: true})
|
|
62
|
+
→ ReportSchedulerJob com identidade (report._id, report._id)
|
|
63
|
+
4. system_server: find()
|
|
64
|
+
→ se server.active: ServerPingJob com identidade (server._id, server._id)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> **Atenção operacional:** schedulers de uma gamificação só são recarregados após reboot se houver um documento `{_id: <apiKey>, total: N}` (N > 0) no banco do **sistema**, coleção `scheduler`. Esse documento é mantido por `SchedulerSystem.addTotal(apiKey, count)` chamado dentro de `SchedulerManager.add` / `delete` / `deleteAll` / `deleteAllByEntity*`. Se essa coleção for limpa manualmente, os schedulers da gamificação ficam órfãos no banco e não voltam ao Quartz.
|
|
68
|
+
|
|
69
|
+
`Configuration.contextDestroyed` chama `quartzScheduler.shutdown(true)`.
|
|
70
|
+
|
|
71
|
+
### 2.2 Pipeline principal — `FunifierJob → SchedulerRunner.run`
|
|
72
|
+
|
|
73
|
+
```mermaid
|
|
74
|
+
flowchart LR
|
|
75
|
+
Q([Quartz fires CronTrigger]) --> FJ[FunifierJob.execute]
|
|
76
|
+
FJ -->|group=apiKey<br/>name=scheduler.id| MF[ManagerFactory.getInstance]
|
|
77
|
+
MF --> FB[schedulerManager.findById]
|
|
78
|
+
FB --> RUN[SchedulerRunner.run]
|
|
79
|
+
RUN --> SM[StatisticManager.newSchedulerExecution]
|
|
80
|
+
SM --> LIMIT{limite<br/>diário?}
|
|
81
|
+
LIMIT -->|excedido| ERR[exception<br/>+log+return]
|
|
82
|
+
LIMIT -->|ok| MODE{entity+event<br/>+item != null?}
|
|
83
|
+
MODE -->|sim| ENTITY[runEntity]
|
|
84
|
+
MODE -->|não| SCRIPT[runScript]
|
|
85
|
+
ENTITY --> LOG[SchedulerManager.addLog]
|
|
86
|
+
SCRIPT --> LOG
|
|
87
|
+
LOG --> R[(scheduler_log)]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Sequência detalhada (números = ordem de execução):
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
1. FunifierJob.execute(context)
|
|
94
|
+
- apiKey = context.getJobDetail().getKey().getGroup()
|
|
95
|
+
- schedulerId = context.getJobDetail().getKey().getName()
|
|
96
|
+
- manager = ManagerFactory.getInstance(apiKey)
|
|
97
|
+
- scheduler = manager.getSchedulerManager().findById(schedulerId)
|
|
98
|
+
- new SchedulerRunner().run(scheduler, manager)
|
|
99
|
+
|
|
100
|
+
2. SchedulerRunner.run(scheduler, manager)
|
|
101
|
+
- start = currentTimeMillis()
|
|
102
|
+
- sm = SystemFactory.getStatisticManager(apiKey)
|
|
103
|
+
- limitOk = sm.newSchedulerExecution(scheduler.id) // incrementa contadores e devolve true/false
|
|
104
|
+
- if !limitOk:
|
|
105
|
+
exceptions += "You have exceeded your scheduler daily executions limits"
|
|
106
|
+
(NÃO chama addLog, NÃO executa script/entity)
|
|
107
|
+
- else:
|
|
108
|
+
if (scheduler.entity != null && scheduler.event != null && scheduler.item != null):
|
|
109
|
+
runEntity(...)
|
|
110
|
+
else:
|
|
111
|
+
runScript(...)
|
|
112
|
+
addLog(new SchedulerLog(id=String(dailyCount), item=scheduler.id, out, err, ms))
|
|
113
|
+
|
|
114
|
+
3. Retorno
|
|
115
|
+
- Map { scheduler, exceptions, outputs, millis }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
> **Inconsistência confirmada no código:** quando o limite diário é excedido, `addLog` **não** é chamado — não fica rastro em `scheduler_log` dessa execução pulada. Apenas o `System.out.println` é emitido.
|
|
119
|
+
>
|
|
120
|
+
> **Inconsistência confirmada no código:** se `scheduler == null` na chamada `FunifierJob.execute` (ID removido do banco mas job ainda no Quartz), `SchedulerRunner.run` recebe null e lança `NullPointerException` na chamada `sm.newSchedulerExecution(scheduler.id)`. Há um `//TODO excluir a scheduler` em `FunifierJob.java:31` reconhecendo esse caso sem tratamento.
|
|
121
|
+
|
|
122
|
+
### 2.3 Fluxo `runEntity` (modo entity)
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
1. callback = manager.getSchedulerCallback(scheduler.entity)
|
|
126
|
+
2. if callback != null:
|
|
127
|
+
callback.schedulerCallback(scheduler, exceptions, outputs)
|
|
128
|
+
3. else: // entidade sem callback registrado
|
|
129
|
+
exceptions += "SchedulerCallback does not exist for entity " + scheduler.entity
|
|
130
|
+
s = manager.getSchedulerManager().findById(scheduler.id)
|
|
131
|
+
if s != null:
|
|
132
|
+
exceptions += "SchedulerConfig " + scheduler.id + " is now disabled"
|
|
133
|
+
s.active = false
|
|
134
|
+
manager.getSchedulerManager().add(s) // re-save com active=false
|
|
135
|
+
// efeito colateral: deleteJob(scheduler.id) no Quartz; updated atualizado
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Callbacks resolvidos por `ManagerFactory.getSchedulerCallback(entity)`:
|
|
139
|
+
|
|
140
|
+
| `entity` recebido | Callback retornado | Origem |
|
|
141
|
+
|-------------------|--------------------|--------|
|
|
142
|
+
| `last_mile` (= `Entity.LAST_MILE.collection`) | `LastMileManager` | Ativo |
|
|
143
|
+
| `bonus` (= `Entity.BONUS.collection`) | — | **Bloco comentado em `ManagerFactory.java:240-243`**; retorna `null` |
|
|
144
|
+
| qualquer outro | `null` | Falha-aberto: auto-desabilita o scheduler |
|
|
145
|
+
|
|
146
|
+
### 2.4 Fluxo `runScript` — cache de classes compiladas
|
|
147
|
+
|
|
148
|
+
```mermaid
|
|
149
|
+
flowchart LR
|
|
150
|
+
A[runScript] --> B{scheduler.updated<br/>== null?}
|
|
151
|
+
B -->|sim| C[add scheduler<br/>seta updated=now]
|
|
152
|
+
B -->|não| D[lê lastCompilation<br/>de dates map]
|
|
153
|
+
C --> D
|
|
154
|
+
D --> E{clazz cached<br/>E<br/>lastCompilation > scheduler.updated?}
|
|
155
|
+
E -->|sim| F[executor com clazz cached]
|
|
156
|
+
E -->|não| G[getScript + compile]
|
|
157
|
+
G --> H[compiled.put + dates.put now]
|
|
158
|
+
H --> F
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Pseudocódigo:
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
runScript(scheduler, manager, exceptions, outputs):
|
|
165
|
+
if scheduler.updated == null:
|
|
166
|
+
manager.getSchedulerManager().add(scheduler) // upsert + seta updated
|
|
167
|
+
lastCompilation = manager.getSchedulerManager().dates.get(scheduler.id)
|
|
168
|
+
clazz = manager.getSchedulerManager().compiled.get(scheduler.id)
|
|
169
|
+
if clazz != null AND lastCompilation != null AND lastCompilation > scheduler.updated:
|
|
170
|
+
executor(clazz, scheduler, manager, exceptions, outputs)
|
|
171
|
+
else:
|
|
172
|
+
script = getScript(scheduler) // gera wrapper Groovy + imports
|
|
173
|
+
try:
|
|
174
|
+
clazz = compile(script) // SecureASTCustomizer + GroovyClassLoader
|
|
175
|
+
manager.getSchedulerManager().compiled.put(scheduler.id, clazz)
|
|
176
|
+
manager.getSchedulerManager().dates.put(scheduler.id, now)
|
|
177
|
+
executor(...)
|
|
178
|
+
catch CompilationFailedException:
|
|
179
|
+
exceptions += e.getMessage()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`compiled` e `dates` são `Map<String, Class>` e `Map<String, Date>` em `SchedulerManager` (instância por gamificação, em memória JVM). Em cluster multi-node cada JVM mantém o seu cache; o campo `updated` no documento serve como sinal cross-node: salvar em outro nó incrementa `updated`, o que invalida o cache local na próxima execução.
|
|
183
|
+
|
|
184
|
+
### 2.5 Geração do wrapper Groovy — `SchedulerRunner.getScript`
|
|
185
|
+
|
|
186
|
+
O wrapper inclui imports e infraestrutura padrão. Pseudocódigo da string montada:
|
|
187
|
+
|
|
188
|
+
```groovy
|
|
189
|
+
import groovyx.net.http.*
|
|
190
|
+
import groovy.json.*
|
|
191
|
+
import groovy.transform.TimedInterrupt
|
|
192
|
+
import com.mashape.unirest.http.* // Unirest
|
|
193
|
+
import com.funifier.engine.action.Action, ActionLog
|
|
194
|
+
import com.funifier.engine.achievement.Achievement
|
|
195
|
+
import com.funifier.engine.lottery.Lottery, LotteryTicket
|
|
196
|
+
import com.funifier.engine.challenge.Challenge, ChallengeRule, ChallengeRuleFilter, Requirement, RewardPoint
|
|
197
|
+
import com.funifier.engine.constant.Operator, TimeScale
|
|
198
|
+
import com.funifier.engine.guid.Guid
|
|
199
|
+
import com.funifier.engine.level.Level
|
|
200
|
+
import com.funifier.engine.notify.Item, Notification, NotificationDefinition
|
|
201
|
+
import com.funifier.engine.player.Player
|
|
202
|
+
import com.funifier.engine.point.Point
|
|
203
|
+
import com.funifier.engine.team.Team
|
|
204
|
+
import com.funifier.engine.mail.FunifierMail
|
|
205
|
+
import com.funifier.engine.util.DateUtil, JsonUtil, ExcelUtil, HttpUtil, HttpClientFunifier, MustacheUtils
|
|
206
|
+
import com.funifier.engine.integration.trigger.GameDao
|
|
207
|
+
import com.funifier.controller.ManagerFactory
|
|
208
|
+
|
|
209
|
+
@TimedInterrupt(value = 2000L, unit = TimeUnit.SECONDS)
|
|
210
|
+
class FunifierScheduler {
|
|
211
|
+
<SCRIPT DO USUÁRIO> // colado literalmente
|
|
212
|
+
|
|
213
|
+
ManagerFactory manager = null
|
|
214
|
+
void setManager(ManagerFactory c) { manager = c }
|
|
215
|
+
|
|
216
|
+
GameDao database = null
|
|
217
|
+
void setDatabase(GameDao c) { database = c }
|
|
218
|
+
|
|
219
|
+
List output = new ArrayList()
|
|
220
|
+
void println(msg) { output.add(msg) }
|
|
221
|
+
void addAllOutput(List out) { out.addAll(output) }
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
> **`@TimedInterrupt(value=2000L)`** é um teto absoluto de **2000 segundos** aplicado pelo Groovy. O timeout operacional via `scheduler.timeout` (default 30s) é menor e atua via `Future.cancel(true)` no executor.
|
|
226
|
+
|
|
227
|
+
### 2.6 Compilação — `SchedulerRunner.compile`
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
CompilerConfiguration conf
|
|
231
|
+
SecureASTCustomizer customizer
|
|
232
|
+
customizer.addExpressionCheckers(new TriggerExpressionChecker()) // blacklist
|
|
233
|
+
customizer.setTokensWhitelist([ EQUAL, PLUS, MINUS, MULTIPLY, DIVIDE, MOD, POWER,
|
|
234
|
+
PLUS_PLUS, PLUS_EQUAL, MINUS_MINUS,
|
|
235
|
+
COMPARE_EQUAL, COMPARE_NOT_EQUAL,
|
|
236
|
+
COMPARE_LESS_THAN, COMPARE_LESS_THAN_EQUAL,
|
|
237
|
+
COMPARE_GREATER_THAN, COMPARE_GREATER_THAN_EQUAL,
|
|
238
|
+
LOGICAL_OR, LOGICAL_OR_EQUAL,
|
|
239
|
+
LOGICAL_AND, LOGICAL_AND_EQUAL,
|
|
240
|
+
LEFT_SQUARE_BRACKET, RIGHT_SQUARE_BRACKET ])
|
|
241
|
+
conf.addCompilationCustomizers(customizer)
|
|
242
|
+
new GroovyClassLoader(Thread.currentThread().getContextClassLoader(), conf).parseClass(script)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
A whitelist de tokens exclui operadores comuns como `<<`, `>>`, `&`, `|`, `^`, `?:`, `?.`, `..`, etc. Isso restringe drasticamente as construções permitidas — qualquer uso desses operadores no script causa `SecurityException`.
|
|
246
|
+
|
|
247
|
+
### 2.7 Execução isolada — `SchedulerRunner.executor`
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
timeout = (scheduler.timeout != null && scheduler.timeout > 0) ? scheduler.timeout : 30
|
|
251
|
+
ExecutorService executor = Executors.newSingleThreadExecutor() // NOVO executor por execução
|
|
252
|
+
future = executor.submit(Callable {
|
|
253
|
+
GroovyObject obj = clazz.newInstance()
|
|
254
|
+
obj.invokeMethod("setManager", [manager])
|
|
255
|
+
obj.invokeMethod("setDatabase", [new GameDao(jongo, manager)])
|
|
256
|
+
obj.invokeMethod("execute", [scheduler]) // contrato do usuário
|
|
257
|
+
obj.invokeMethod("addAllOutput", [outputs])
|
|
258
|
+
return scheduler.id
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
try future.get(timeout, TimeUnit.SECONDS)
|
|
262
|
+
catch TimeoutException, InterruptedException, ExecutionException, SecurityException,
|
|
263
|
+
MultipleCompilationErrorsException, Exception:
|
|
264
|
+
exceptions += "Timeout after N seconds (timeout allowed " + timeout + " seconds)" // se Timeout/Interrupted
|
|
265
|
+
exceptions += e.getMessage()
|
|
266
|
+
future.cancel(true)
|
|
267
|
+
executor.shutdownNow()
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
> **Bug latente:** se `clazz.newInstance()` lançar exception **antes** do `executor.submit`, `future` continua `null` e o `catch` chamará `future.cancel(true)` → `NullPointerException` que será capturado pelo último `catch (Exception)`.
|
|
271
|
+
|
|
272
|
+
> **Bug latente:** `executor` (ExecutorService) é instanciado por execução mas o `executor.shutdownNow()` só roda quando há exception. No caminho feliz nada chama `shutdown`/`shutdownNow`, então a thread fica em "core size = 0" e expira sozinha — não há vazamento de threads, mas há criação repetitiva de pools.
|
|
273
|
+
|
|
274
|
+
### 2.8 `SchedulerManager.add` — criar ou atualizar
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
add(scheduler):
|
|
278
|
+
if scheduler.id == null: scheduler.id = Guid.newShortGuid()
|
|
279
|
+
if scheduler.creation == null: scheduler.creation = new Date()
|
|
280
|
+
scheduler.updated = new Date() // SEMPRE atualizado
|
|
281
|
+
|
|
282
|
+
compile(scheduler): // valida script + cron
|
|
283
|
+
runner = new SchedulerRunner()
|
|
284
|
+
script = runner.getScript(scheduler)
|
|
285
|
+
runner.compile(script) // CompilationFailedException
|
|
286
|
+
new CronExpression(scheduler.cron) // ParseException
|
|
287
|
+
|
|
288
|
+
jongo.getCollection("scheduler").save(scheduler) // upsert por _id
|
|
289
|
+
quartzScheduler.deleteJob(jobKey(scheduler.id, apiKey)) // SEMPRE deleta job antigo
|
|
290
|
+
|
|
291
|
+
if scheduler.active:
|
|
292
|
+
job = newJob(FunifierJob.class).withIdentity(scheduler.id, apiKey)
|
|
293
|
+
cron = scheduler.timezone non-empty
|
|
294
|
+
? cronSchedule(scheduler.cron).inTimeZone(scheduler.timezone)
|
|
295
|
+
: cronSchedule(scheduler.cron) // timezone padrão da JVM
|
|
296
|
+
trigger = newTrigger().withIdentity(scheduler.id + "trigger", apiKey).withSchedule(cron)
|
|
297
|
+
quartzScheduler.scheduleJob(job, trigger)
|
|
298
|
+
|
|
299
|
+
SystemFactory.getSchedulerManager().addTotal(apiKey, c.count()) // ÍNDICE de totais
|
|
300
|
+
compiled.remove(scheduler.id) // invalida cache local
|
|
301
|
+
// OBS: dates NÃO é removido aqui — apenas em delete()
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
> **Compilação dupla observada:** `SchedulerRest.insert` chama `manager.compile(scheduler)` E em seguida `manager.add(scheduler)` (que recompila internamente). Não é um bug funcional, apenas trabalho duplicado.
|
|
305
|
+
|
|
306
|
+
### 2.9 Diagrama de interação entre módulos
|
|
307
|
+
|
|
308
|
+
```mermaid
|
|
309
|
+
sequenceDiagram
|
|
310
|
+
participant Boot as Configuration<br/>contextInitialized
|
|
311
|
+
participant Sys as SchedulerSystem
|
|
312
|
+
participant Quartz as Quartz Scheduler
|
|
313
|
+
participant Mgr as SchedulerManager<br/>(per apiKey)
|
|
314
|
+
participant Run as SchedulerRunner
|
|
315
|
+
|
|
316
|
+
Boot->>Sys: contextInitialized()
|
|
317
|
+
Sys->>Sys: lê scheduler<br/>{total:{$gt:0}}
|
|
318
|
+
loop cada apiKey
|
|
319
|
+
Sys->>Mgr: ManagerFactory.getInstance(apiKey)
|
|
320
|
+
Mgr->>Quartz: scheduleJob(FunifierJob, CronTrigger)
|
|
321
|
+
end
|
|
322
|
+
Sys->>Quartz: scheduleJob(ReportSchedulerJob)
|
|
323
|
+
Sys->>Quartz: scheduleJob(ServerPingJob)
|
|
324
|
+
|
|
325
|
+
Note over Quartz: ... cron dispara ...
|
|
326
|
+
|
|
327
|
+
Quartz->>Run: FunifierJob.execute(context)
|
|
328
|
+
Run->>Mgr: findById(scheduler.id)
|
|
329
|
+
Run->>Run: StatisticManager.newSchedulerExecution
|
|
330
|
+
alt entity+event+item preenchidos
|
|
331
|
+
Run->>Mgr: getSchedulerCallback(entity)
|
|
332
|
+
Mgr-->>Run: LastMileManager (ou null)
|
|
333
|
+
Run->>Mgr: callback.schedulerCallback
|
|
334
|
+
else script mode
|
|
335
|
+
Run->>Run: getScript + compile (com cache)
|
|
336
|
+
Run->>Run: executor.submit(invokeMethod)
|
|
337
|
+
end
|
|
338
|
+
Run->>Mgr: addLog(SchedulerLog)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### 2.10 Integração automática LastMile → Scheduler
|
|
342
|
+
|
|
343
|
+
```mermaid
|
|
344
|
+
flowchart LR
|
|
345
|
+
A([POST /v3/last_mile]) --> B[LastMileManager.insert]
|
|
346
|
+
B --> C[deleteAllByEntityItemEvent<br/>last_mile, id, notify]
|
|
347
|
+
C --> Q1[(remove jobs antigos<br/>do Quartz)]
|
|
348
|
+
B --> D{lastmile.scheduler<br/>!= null?}
|
|
349
|
+
D -->|sim| E[clone scheduler do payload<br/>FORÇA: id=lastmile.id,<br/>active=true,<br/>timezone=securityManager.find.timezone,<br/>name=lastmile.title,<br/>entity=last_mile, event=notify,<br/>item=lastmile.id,<br/>script=""]
|
|
350
|
+
E --> F[schedulerManager.add]
|
|
351
|
+
F --> Q2[(scheduleJob no Quartz)]
|
|
352
|
+
D -->|não| G[c.save lastmile]
|
|
353
|
+
F --> G
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`LastMileManager.insert` **sobrescreve** os seguintes campos do scheduler vindo do cliente, ignorando os valores enviados:
|
|
357
|
+
|
|
358
|
+
- `scheduler.id ← lastmile.id`
|
|
359
|
+
- `scheduler.active ← true`
|
|
360
|
+
- `scheduler.timezone ← securityManager.find().getTimeZone()` (timezone da gamificação)
|
|
361
|
+
- `scheduler.name ← lastmile.title`
|
|
362
|
+
- `scheduler.entity ← "last_mile"`
|
|
363
|
+
- `scheduler.event ← "notify"`
|
|
364
|
+
- `scheduler.item ← lastmile.id`
|
|
365
|
+
- `scheduler.script ← ""` (string vazia)
|
|
366
|
+
|
|
367
|
+
`SchedulerConfig.clone()` copia apenas: `id, name, cron, script, creation, active, timezone, extra, entity, event, item`. Os campos `time, timeout, updated, techniques, style, references` são **silenciosamente descartados** no clone — passar qualquer um deles no payload `lastmile.scheduler` é inerte. Em seguida `LastMileManager.insert` sobrescreve `id, active, timezone, name, entity, event, item, script`. Resultado: do payload original sobrevivem apenas `cron`, `extra` e `creation`. O `event` enviado pelo cliente é descartado em favor de `"notify"`.
|
|
368
|
+
|
|
369
|
+
`LastMileManager.delete` chama `deleteAllByEntityItem(Entity.LAST_MILE.collection, id)` — apaga TODOS os schedulers daquele lastmile, independente de `event`.
|
|
370
|
+
|
|
371
|
+
`BonusManager.delete` igualmente chama `deleteAllByEntityItem(Entity.BONUS.collection, id)` — cleanup vestigial; já não há código que crie esses schedulers (o bloco em `BonusManager.insert:44-67` está totalmente comentado).
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## 3. Estrutura dos Objetos
|
|
376
|
+
|
|
377
|
+
### 3.1 `SchedulerConfig` — documento principal (coleção `scheduler` da gamificação)
|
|
378
|
+
|
|
379
|
+
`@JsonIgnoreProperties(ignoreUnknown=true)` — campos desconhecidos no payload são **silenciosamente descartados** pelo Jackson.
|
|
380
|
+
|
|
381
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
382
|
+
|-------|------|--------|-------------|-----------|
|
|
383
|
+
| `_id` | String | `Guid.newShortGuid()` no `add` se null | — | Identificador único na gamificação |
|
|
384
|
+
| `name` | String | — | não | Nome descritivo |
|
|
385
|
+
| `cron` | String | — | **sim** | Expressão cron Quartz (6 campos). Validada por `new CronExpression(...)` |
|
|
386
|
+
| `script` | String | — | condicional | Código Groovy. Ignorado em modo entity. Obrigatório em modo script |
|
|
387
|
+
| `creation` | Date | `new Date()` no `add` se null | — | Data de criação |
|
|
388
|
+
| `active` | boolean | `false` | não | Quando `false`, documento existe mas job não é instalado no Quartz |
|
|
389
|
+
| `timezone` | String | — | não | Timezone IANA (ex: `America/Sao_Paulo`). Se null ou vazio, usa timezone padrão da JVM |
|
|
390
|
+
| `extra` | `Map<String,Object>` | `{}` | não | Dados arbitrários acessíveis em `scheduler.extra` no script |
|
|
391
|
+
| `entity` | String | — | condicional | Nome da coleção da entidade dona do callback (ex: `last_mile`). Ativa modo entity quando E `event` E `item` também preenchidos |
|
|
392
|
+
| `event` | String | — | condicional | Evento dentro da entidade (ex: `notify`) |
|
|
393
|
+
| `item` | String | — | condicional | ID do item específico (passado em `scheduler.item` ao callback) |
|
|
394
|
+
| `time` | Date | — | — | Horário de referência da execução. Persistido mas só preenchido pelo endpoint `/execute/{id}?time=...`; em execuções Quartz normais chega `null` |
|
|
395
|
+
| `timeout` | Long | `30` | não | Timeout do script em **segundos**. Se null ou `<= 0`, usa 30 |
|
|
396
|
+
| `updated` | Date | `new Date()` em todo `add()` | — | Timestamp do último save; usado como sinal de invalidação de cache cross-node |
|
|
397
|
+
| `techniques` | `List<String>` | `[]` | não | Códigos de técnicas de jogo associadas (GT codes) |
|
|
398
|
+
| `style` | `ObjectStyle` | null | não | Atributos visuais para o Studio: `{background: String, visible: Boolean}` |
|
|
399
|
+
| `references` | `List<Reference>` | null | não | Relacionamentos com outros componentes da estratégia (gamification graph) |
|
|
400
|
+
|
|
401
|
+
#### Campos computados (não persistem como tais)
|
|
402
|
+
|
|
403
|
+
- **Modo de operação** — derivado de `(entity, event, item)`. Os três precisam estar simultaneamente não-null (`!= null`) para acionar `runEntity`. Qualquer um null cai em `runScript`. Não há validação prévia: um scheduler com `entity = "bonus"` e os outros campos é aceito pelo POST e só falha na execução (auto-desabilita).
|
|
404
|
+
|
|
405
|
+
#### Campos silenciosamente sobrescritos no save
|
|
406
|
+
|
|
407
|
+
- `_id` recebe ShortGuid se null.
|
|
408
|
+
- `creation` recebe `new Date()` se null.
|
|
409
|
+
- `updated` recebe `new Date()` em **toda** chamada `add()`, sobrescrevendo o valor do payload.
|
|
410
|
+
|
|
411
|
+
#### Diferença schema vs runtime
|
|
412
|
+
|
|
413
|
+
- `time` é persistido no MongoDB e pode ser definido via payload no POST, mas no fluxo Quartz normal nunca é lido — o `SchedulerRunner.run` não popula `scheduler.time` antes da execução. Único caminho operacional: `GET /v3/scheduler/execute/{id}?time=...` faz `scheduler.time = date` antes de chamar `run`. Salvar `time` num POST regular é portanto inerte na cron normal.
|
|
414
|
+
- `style` e `references` existem para o Studio e nunca são lidos pelo `SchedulerRunner`.
|
|
415
|
+
- `techniques` é uma lista de códigos GT mas não há lógica de execução dentro do módulo scheduler que processe esse campo — é metadado para a UI.
|
|
416
|
+
|
|
417
|
+
#### Enums e constantes referenciados
|
|
418
|
+
|
|
419
|
+
- `Entity.SCHEDULER.collection = "scheduler"`
|
|
420
|
+
- `Entity.SCHEDULER_LOG.collection = "scheduler_log"`
|
|
421
|
+
- `Entity.LAST_MILE.collection = "last_mile"` (valor aceito como `entity`)
|
|
422
|
+
- `Entity.BONUS.collection = "bonus"` (valor aceito como `entity` mas sem callback registrado)
|
|
423
|
+
- `LastMileManager.EVENT_NOTIFY = "notify"`
|
|
424
|
+
- `GamificationLimits.DEFAULT_DAILY_SCHEDULER_EXECUTIONS_LIMIT = 100`
|
|
425
|
+
- `Reference.WAY_FROM = "from"`, `Reference.WAY_TO = "to"`
|
|
426
|
+
|
|
427
|
+
### 3.2 `SchedulerLog` — documento de log (coleção `scheduler_log`)
|
|
428
|
+
|
|
429
|
+
`@JsonIgnoreProperties(ignoreUnknown=true)`.
|
|
430
|
+
|
|
431
|
+
| Campo | Tipo | Padrão | Descrição |
|
|
432
|
+
|-------|------|--------|-----------|
|
|
433
|
+
| `_id` | String | `String.valueOf(sm.getSchedulerStatistics().dailySchedulerExecutions)` | **Não é UUID** — é o valor inteiro do contador diário em memória, convertido para string. Após reinício do servidor o contador reinicia em 1 — IDs colidem entre dias/restarts e o `c.save()` faz upsert por `_id` (sobrescreve log anterior com o mesmo número). |
|
|
434
|
+
| `item` | String | — | ID do scheduler que foi executado |
|
|
435
|
+
| `time` | Date | `new Date()` no construtor | Momento do registro |
|
|
436
|
+
| `out` | `List<String>` | `null` quando lista vazia | Outputs de `println()` durante execução. Construtor faz: `out = (out != null && out.size() > 0) ? out : null` |
|
|
437
|
+
| `err` | `List<String>` | `null` quando lista vazia | Mensagens de exceção. Mesma regra de `out` |
|
|
438
|
+
| `ms` | long | — | Tempo total de `SchedulerRunner.run` em milissegundos |
|
|
439
|
+
|
|
440
|
+
> Como `out` e `err` são gravados como `null` quando vazios (não como `[]`), para filtrar execuções com erros use `q={err:{$ne:null}}`.
|
|
441
|
+
|
|
442
|
+
### 3.3 `Reference` — subentidade de relacionamento
|
|
443
|
+
|
|
444
|
+
Compartilhada com `Trigger`. Documenta como o scheduler se relaciona com outros componentes (consumo de pontos, vínculo a achievement, etc).
|
|
445
|
+
|
|
446
|
+
| Campo | Tipo | Descrição |
|
|
447
|
+
|-------|------|-----------|
|
|
448
|
+
| `way` | String | `"from"` ou `"to"`; sentido da seta no grafo do Studio |
|
|
449
|
+
| `linkLabel` | String | Texto sobre a seta |
|
|
450
|
+
| `_id` | String | ID do componente referenciado |
|
|
451
|
+
| `type` | String | Coleção do componente (ex: `Entity.POINT.collection`) |
|
|
452
|
+
| `title` | String | Título descritivo |
|
|
453
|
+
|
|
454
|
+
### 3.4 `ObjectStyle` — subentidade de visual
|
|
455
|
+
|
|
456
|
+
| Campo | Tipo | Descrição |
|
|
457
|
+
|-------|------|-----------|
|
|
458
|
+
| `background` | String | Cor de fundo / asset visual no Studio |
|
|
459
|
+
| `visible` | Boolean | Visibilidade na UI |
|
|
460
|
+
|
|
461
|
+
### 3.5 `SchedulerStatistics` — estado em memória (não persistido autonomamente)
|
|
462
|
+
|
|
463
|
+
Mantido por `StatisticManager` (um por gamificação). Persistido apenas como parte de `STATISTIC_LOG` (coleção do sistema).
|
|
464
|
+
|
|
465
|
+
| Campo | Tipo | Descrição |
|
|
466
|
+
|-------|------|-----------|
|
|
467
|
+
| `lastSchedulerExecution` | Date | Última execução registrada |
|
|
468
|
+
| `hourlySchedulerExecutions` | long | Contador na hora corrente. Reset ao mudar `HOUR_OF_DAY` |
|
|
469
|
+
| `dailySchedulerExecutions` | long | Contador no dia corrente. Reset ao mudar `DAY_OF_YEAR` |
|
|
470
|
+
| `monthlySchedulerExecutions` | long | Contador no mês corrente. Reset ao mudar `MONTH` |
|
|
471
|
+
| `dailySchedulerExecutionsLimit` | long | Cópia de `GamificationLimits.dailySchedulerExecutions` (default 100) |
|
|
472
|
+
| `triggers` | `Map<String, SchedulerDetailStatistics>` | Contadores por scheduler.id |
|
|
473
|
+
|
|
474
|
+
> O comparador no `newSchedulerExecution` usa `<=`: incrementa o contador primeiro, depois testa `dailySchedulerExecutions <= dailySchedulerExecutionsLimit`. Com limite 100, a 100ª chamada incrementa para 100, passa no teste (`100 <= 100`) e retorna `true`; a 101ª retorna `false`. Permite exatamente `limit` execuções por dia.
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## 4. Endpoints
|
|
479
|
+
|
|
480
|
+
### `GET /v3/scheduler`
|
|
481
|
+
|
|
482
|
+
Listagem com filtros via aggregate pipeline (não `find` direto).
|
|
483
|
+
|
|
484
|
+
| Aspecto | Detalhe |
|
|
485
|
+
|---------|---------|
|
|
486
|
+
| Finalidade | Listar configurações de scheduler da gamificação |
|
|
487
|
+
| Autenticação | Bearer token |
|
|
488
|
+
| Implementação | `MongoCollection.aggregate(...)` com `$match` + `$sort` + `$limit` |
|
|
489
|
+
| Resposta | Array JSON com campos null removidos (`JsonUtil.toJsonRemoveNullFields`) |
|
|
490
|
+
|
|
491
|
+
**Query params:**
|
|
492
|
+
|
|
493
|
+
| Param | Tipo | Descrição |
|
|
494
|
+
|-------|------|-----------|
|
|
495
|
+
| `id` | String | Filtra `_id` exato |
|
|
496
|
+
| `q` | String | Cláusula MongoDB **raw injetada** no `$match` |
|
|
497
|
+
| `published_min` | String | Lower bound aplicado em `creation`. RFC3339 ou keyword (`-1d`, `-30m`, `-1w`, `y/M/d/h/m/s/w`) |
|
|
498
|
+
| `published_max` | String | Upper bound em `creation` |
|
|
499
|
+
| `orderby` | String | Campo de `$sort` (também injetado raw) |
|
|
500
|
+
| `reverse` | String→boolean | `"true"` → `-1` (DESC); senão `1` (ASC) |
|
|
501
|
+
| `max_results` | String→int | Default `100` quando `<= 0` |
|
|
502
|
+
|
|
503
|
+
**Comportamento real:**
|
|
504
|
+
|
|
505
|
+
- `q` é concatenado literalmente: `query.append(", " + q)` — qualquer operador MongoDB válido funciona, inclusive `$where`. Não há sanitização. **Surface de injeção.**
|
|
506
|
+
- `orderby` também é concatenado literalmente em `{$sort: {<orderby>: #}}`. Não há lista permitida.
|
|
507
|
+
- Parsing de `published_min`/`published_max` tenta primeiro `parseZonedDateToLocalDate` (RFC3339) e cai em `fromKeyword` se null.
|
|
508
|
+
- O `findAll(id, q, ...)` aceita id no path do match, mas a rota também aceita id como query param; se ambos forem usados o id do query param prevalece.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
### `GET /v3/scheduler/log`
|
|
513
|
+
|
|
514
|
+
Logs de execução com paginação.
|
|
515
|
+
|
|
516
|
+
| Aspecto | Detalhe |
|
|
517
|
+
|---------|---------|
|
|
518
|
+
| Finalidade | Listar `SchedulerLog` com paginação por `Range` header |
|
|
519
|
+
| Implementação | `PaginationUtil.getPageResult(aggregate, range, 0, 100)` |
|
|
520
|
+
|
|
521
|
+
**Query params:**
|
|
522
|
+
|
|
523
|
+
| Param | Tipo | Descrição |
|
|
524
|
+
|-------|------|-----------|
|
|
525
|
+
| `item` | String | Filtra logs de um scheduler específico (`log.item == schedulerId`) |
|
|
526
|
+
| `q` | String | Cláusula MongoDB raw (mesmo padrão de injeção do `GET /v3/scheduler`) |
|
|
527
|
+
| `published_min` | String | Lower bound aplicado em `time` (não `creation`) |
|
|
528
|
+
| `published_max` | String | Upper bound em `time` |
|
|
529
|
+
| `orderby` | String | Campo de ordenação raw |
|
|
530
|
+
| `reverse` | String→boolean | DESC quando `true` |
|
|
531
|
+
| `max_results` | String→int | Default `100` |
|
|
532
|
+
|
|
533
|
+
**Header:**
|
|
534
|
+
|
|
535
|
+
| Header | Descrição |
|
|
536
|
+
|--------|-----------|
|
|
537
|
+
| `Range` | Formato `items=A-B`, ex `items=0-99` |
|
|
538
|
+
|
|
539
|
+
**Resposta:** payload paginado padrão da Funifier (`PaginationUtil`). Resposta contém o campo `Content-Range` apropriado quando o handler é `Callback.paginatedCallback`.
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
### `GET /v3/scheduler/{id}`
|
|
544
|
+
|
|
545
|
+
Retorna `SchedulerConfig` por `_id`. Campos null removidos. Sem 404 explícito: se `findById` retornar null, a resposta é vazia / JSON `{}`.
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
### `GET /v3/scheduler/execute/{id}`
|
|
550
|
+
|
|
551
|
+
Execução manual para debug.
|
|
552
|
+
|
|
553
|
+
| Aspecto | Detalhe |
|
|
554
|
+
|---------|---------|
|
|
555
|
+
| Finalidade | Disparar o scheduler imediatamente, fora da agenda Quartz |
|
|
556
|
+
| Efeito colateral | Consome quota diária; grava `scheduler_log` |
|
|
557
|
+
| Diferença vs. cron | Não respeita `active=false` — executa mesmo desativado |
|
|
558
|
+
|
|
559
|
+
**Query params:**
|
|
560
|
+
|
|
561
|
+
| Param | Tipo | Descrição |
|
|
562
|
+
|-------|------|-----------|
|
|
563
|
+
| `time` | String | Sobrescreve `scheduler.time` antes da execução. Aceita RFC3339, keyword (`-1d`), ou epoch millis (`Long.parseLong`). Tentativas em cascata; se todas falharem, `scheduler.time` mantém o valor original (não é sobrescrito) |
|
|
564
|
+
|
|
565
|
+
**Resposta:** `{ scheduler, exceptions, outputs, millis }` com null removidos.
|
|
566
|
+
|
|
567
|
+
> Se `scheduler == null` (id inexistente), `scheduler.time = date` lança `NullPointerException` antes mesmo da chamada do `run`.
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
### `GET /v3/scheduler/next/{id}`
|
|
572
|
+
|
|
573
|
+
Retorna as **próximas 10 datas** de execução após uma referência.
|
|
574
|
+
|
|
575
|
+
**Query params:**
|
|
576
|
+
|
|
577
|
+
| Param | Tipo | Descrição |
|
|
578
|
+
|-------|------|-----------|
|
|
579
|
+
| `reference` | String | Data de referência. RFC3339, keyword, ou `null`. Se null, usa `new Date()` |
|
|
580
|
+
|
|
581
|
+
**Comportamento real:** `nextExecutionTimesAfter` faz um loop fixo de 10 iterações chamando `CronExpression.getNextValidTimeAfter`. Se em alguma iteração retornar `null` (sem próxima ocorrência), o loop quebra e retorna a lista parcial. Datas vêm formatadas via `DateUtil.formatDateToZonedDate`.
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
### `POST /v3/scheduler`
|
|
586
|
+
|
|
587
|
+
Criar ou atualizar um scheduler.
|
|
588
|
+
|
|
589
|
+
| Aspecto | Detalhe |
|
|
590
|
+
|---------|---------|
|
|
591
|
+
| Finalidade | Upsert por `_id` |
|
|
592
|
+
| Full replace ou patch | **Full replace** — `Jongo.save()` substitui o documento |
|
|
593
|
+
| Validação | Compila script Groovy **+** valida cron antes de salvar |
|
|
594
|
+
| HTTP status | **Sempre `201 Created`**, inclusive em erro de compilação |
|
|
595
|
+
| Content-Type | `application/json` |
|
|
596
|
+
|
|
597
|
+
**Resposta sucesso:**
|
|
598
|
+
|
|
599
|
+
```json
|
|
600
|
+
{ "scheduler": { ... }, "status": "OK" }
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Resposta erro de compilação ou cron inválido:**
|
|
604
|
+
|
|
605
|
+
```json
|
|
606
|
+
{ "scheduler": { ... }, "status": "ERROR", "message": "<mensagem>" }
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**Comportamento real:**
|
|
610
|
+
|
|
611
|
+
1. `manager.compile(scheduler)` valida script + cron. Se falhar com `CompilationFailedException` ou `ParseException`, retorna `status: ERROR` no body com HTTP 201.
|
|
612
|
+
2. **Não captura** `SchedulerException` (de operações Quartz) — esta sobe pela stack e produz HTTP 500.
|
|
613
|
+
3. `manager.add(scheduler)` recompila o script (compilação dupla) e:
|
|
614
|
+
- Gera `_id` ShortGuid se não fornecido
|
|
615
|
+
- Seta `creation = now` se null
|
|
616
|
+
- Seta `updated = now` (sempre)
|
|
617
|
+
- Upsert no MongoDB
|
|
618
|
+
- Remove job antigo do Quartz e recria se `active=true`
|
|
619
|
+
- Invalida `compiled[scheduler.id]`
|
|
620
|
+
- Atualiza índice de totais do sistema
|
|
621
|
+
|
|
622
|
+
**Exemplo:**
|
|
623
|
+
|
|
624
|
+
```json
|
|
625
|
+
POST /v3/scheduler
|
|
626
|
+
{
|
|
627
|
+
"active": true,
|
|
628
|
+
"name": "Notificação diária",
|
|
629
|
+
"cron": "0 0/20 * * * ?",
|
|
630
|
+
"timezone": "America/Sao_Paulo",
|
|
631
|
+
"script": "void execute(scheduler){ println 'every 20 minutes for ' + scheduler.extra.company; }",
|
|
632
|
+
"extra": { "company": "funifier" }
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
### `DELETE /v3/scheduler/{id}`
|
|
639
|
+
|
|
640
|
+
Remove documento + job Quartz + entradas de `compiled` E `dates` + atualiza índice de totais.
|
|
641
|
+
|
|
642
|
+
Resposta: `204 No Content`. Não há verificação se o ID existe — `c.remove()` é idempotente.
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
### `PUT /v3/scheduler/compile/{id}`
|
|
647
|
+
|
|
648
|
+
Limpa **apenas** o cache `compiled` (não `dates`).
|
|
649
|
+
|
|
650
|
+
| Path | Efeito |
|
|
651
|
+
|------|--------|
|
|
652
|
+
| `/compile/all` | Itera `findAll()` e chama `manager.clear()` em cada — remove cada `compiled[id]` |
|
|
653
|
+
| `/compile/<id>` | `findById(id)` e se não-null chama `clear()` |
|
|
654
|
+
|
|
655
|
+
> A próxima execução ainda comparará `dates[id] > scheduler.updated`. Como `clear()` não toca em `dates`, se `dates[id]` permanecer maior que `updated`, o teste `clazz != null && lastCompilation > updated` falha (clazz é null), forçando recompilação — comportamento desejado. Mas o `dates` permanece "sujo": após recompilar, `dates.put(id, now)` sobrescreve.
|
|
656
|
+
|
|
657
|
+
Resposta: `200 OK` com `{ "_id": "<id>" }`.
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
### `GET /v3/system/scheduler/status` (system-only)
|
|
662
|
+
|
|
663
|
+
Expõe `SchedulerSystem.getStatus()`. Retorna estado agregado:
|
|
664
|
+
|
|
665
|
+
```json
|
|
666
|
+
{
|
|
667
|
+
"isStarted": true,
|
|
668
|
+
"jobGroupNames": [ "<apikey>", "<reportId>", "<serverId>" ],
|
|
669
|
+
"triggerGroupNames": [ "..." ],
|
|
670
|
+
"pausedTriggers": [],
|
|
671
|
+
"metadata": { ... },
|
|
672
|
+
"schedulers": [
|
|
673
|
+
{ "_id": "<schedulerId>trigger", "next": "...", "prev": "..." }
|
|
674
|
+
],
|
|
675
|
+
"references": [
|
|
676
|
+
"report_<reportId>",
|
|
677
|
+
"gamification_<apikey>_sheduler_<schedulerId>"
|
|
678
|
+
]
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
> **Typo no payload de produção:** `"gamification_<apikey>_sheduler_<id>"` — "sheduler" sem 'c'. Está hardcoded em `SchedulerSystem.getStatus:153`.
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
686
|
+
### `GET /v3/system/scheduler/jobs` (system-only)
|
|
687
|
+
|
|
688
|
+
Expõe `SchedulerSystem.getAllJobKeys()`. Itera todos os jobs do Quartz, resolve cada `group` na coleção `game` (campo `_id == group`) para anexar nome da gamificação, apiKey e nome do scheduler. Retorna lista de:
|
|
689
|
+
|
|
690
|
+
```json
|
|
691
|
+
[
|
|
692
|
+
{
|
|
693
|
+
"groupName": "<apikey ou reportId ou serverId>",
|
|
694
|
+
"jobName": "<schedulerId>",
|
|
695
|
+
"gamification": "<game.name>",
|
|
696
|
+
"account": "<game.accountId>",
|
|
697
|
+
"schedulerName": "<scheduler.name ou 'SCHEDULER DONT EXIST'>",
|
|
698
|
+
"nextExecution": "Mon Jan 01 09:00:00 UTC 2025",
|
|
699
|
+
"isActive": "Yes"
|
|
700
|
+
}
|
|
701
|
+
]
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
> Se o job está no Quartz mas foi removido do MongoDB, `schedulerName` aparece como `"SCHEDULER DONT EXIST"`. Útil para detectar drift.
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
### `GET /v3/util/cron/status` (gamificação, com Bearer)
|
|
709
|
+
|
|
710
|
+
Alias acessível para o mesmo `SchedulerSystem.getStatus()` — duplicação intencional para acesso por consumidores de gamificação.
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
## 5. Regras de Negócio
|
|
715
|
+
|
|
716
|
+
**Modo de operação derivado, não declarado.** Não há campo `mode` — é uma derivação implícita: `(entity != null && event != null && item != null)` → entity; senão → script. Schedulers parcialmente preenchidos (ex. `entity` mas sem `event`) caem em script silenciosamente.
|
|
717
|
+
|
|
718
|
+
**Modo entity ignora `script`.** No fluxo `runEntity` o campo `script` nunca é avaliado. O `LastMileManager.insert` aproveita isso para gravar `script = ""` (vazio) em schedulers entity-bound — economiza memória mas confunde quem inspeciona a coleção.
|
|
719
|
+
|
|
720
|
+
**Validação dupla no upsert.** `SchedulerRest.insert` chama `compile()` e depois `add()` (que recompila internamente). Em fluxos via `AppManager.funifierCreateScheduler`, `LastMileManager.insert` ou outros caminhos internos, apenas `add()` é chamado — também valida.
|
|
721
|
+
|
|
722
|
+
**Auto-disable silencioso por callback ausente.** Se `entity` for uma string para a qual `ManagerFactory.getSchedulerCallback` retorna null, o scheduler é re-salvo com `active=false` na primeira execução. **A primeira execução consome quota** mas executa um `runEntity` no-op que termina com exception. O usuário não é notificado fora do `err` do log.
|
|
723
|
+
|
|
724
|
+
**Limite diário.** `dailySchedulerExecutionsLimit` vem de `GamificationLimits` (coleção `gamification_limits` no banco do sistema). Default `100/dia`. Limite só bloqueia execuções de schedulers — não bloqueia o agendamento. Se atingido, `runEntity`/`runScript` nem é chamado, mas o log também não é gravado.
|
|
725
|
+
|
|
726
|
+
**Contadores em memória.** `SchedulerStatistics` reinicia ao detectar mudança de hora/dia/mês na próxima execução (não no relógio). Reiniciar o processo zera os contadores antes do banco persistir (apenas `STATISTIC_LOG` é gravado periodicamente em outros caminhos — verificar com [statistic_log]).
|
|
727
|
+
|
|
728
|
+
**Upsert por `_id`.** Tanto `add()` quanto `c.save()` fazem upsert. Reusar `_id` em um POST substitui o documento inteiro — não é PATCH.
|
|
729
|
+
|
|
730
|
+
**Cron Quartz, não Unix.** 6 campos (`seg min hor dia mês dia-semana`). Diferenças relevantes vs cron Unix:
|
|
731
|
+
- Dia-da-semana usa códigos `MON–SUN` ou números 1–7 (1=Domingo no Quartz).
|
|
732
|
+
- `?` é obrigatório em um dos campos `dia` ou `dia-semana` (não pode-se especificar ambos).
|
|
733
|
+
- Suporta `/` para incremento, `L` para "last", `W` para weekday, `#` para "nth day-of-week".
|
|
734
|
+
|
|
735
|
+
**Timezone.** Se `scheduler.timezone` é `null` ou string vazia, o cron usa o timezone padrão da JVM. Em containers/produção isso é normalmente UTC — o que muda o instante de disparo vs. o esperado pelo usuário do Studio.
|
|
736
|
+
|
|
737
|
+
**Identidade Quartz.** Jobs de gamificação têm `JobKey(scheduler.id, apiKey)`. Triggers têm `TriggerKey(scheduler.id + "trigger", apiKey)`. Não há risco de colisão entre gamificações; há risco de colisão com schedulers system se um `apiKey` coincidir com `server._id` ou `report._id` (improvável na prática).
|
|
738
|
+
|
|
739
|
+
**Cluster.** O Quartz é local (`StdSchedulerFactory.getScheduler()` sem `quartz.properties` para JDBC store). Em múltiplos nós, **cada nó executa o cron independentemente** — o scheduler vai disparar N vezes em uma topologia de N nós. O campo `updated` apenas serve para sincronização do cache de compilação.
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
## 6. Comportamentos Automáticos
|
|
744
|
+
|
|
745
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
746
|
+
|---------------|---------|---------|--------------|
|
|
747
|
+
| Auto `_id` via ShortGuid | `add()` sem `_id` | Gera identificador | MongoDB |
|
|
748
|
+
| Auto `creation` | `add()` sem `creation` | Seta `new Date()` | MongoDB |
|
|
749
|
+
| Auto `updated` | Toda chamada `add()` | Sobrescreve `updated` com `new Date()` | MongoDB + invalida cache cross-node |
|
|
750
|
+
| Recompilação e re-agendamento do job | Toda `add()` | `quartzScheduler.deleteJob(...)` SEMPRE; `scheduleJob` apenas se `active=true` | Memória Quartz |
|
|
751
|
+
| Atualização do índice de totais | `add()`, `delete()`, `deleteAll()`, `deleteAllByEntity*` | `{_id: apiKey, total: count}` na coleção `scheduler` do sistema | MongoDB sistema |
|
|
752
|
+
| Invalidação de `compiled` | `add()` e `delete()` | Remove entrada da `Map<String, Class>` | Memória JVM |
|
|
753
|
+
| Invalidação de `dates` | `delete()` apenas | Remove entrada da `Map<String, Date>` | Memória JVM |
|
|
754
|
+
| Auto-disable por callback ausente | Execução em modo entity quando `getSchedulerCallback(entity) == null` | Re-save com `active=false` + job removido do Quartz | MongoDB + Quartz |
|
|
755
|
+
| Cascade delete no game | `GameManager.delete(apiKey)` | `schedulerManager.deleteAll()` | MongoDB + Quartz + sistema |
|
|
756
|
+
| Cascade no LastMile.insert | `LastMileManager.insert(lastmile)` | `deleteAllByEntityItemEvent("last_mile", id, "notify")` + `add(scheduler clonado e sobrescrito)` | MongoDB + Quartz |
|
|
757
|
+
| Cascade no LastMile.delete | `LastMileManager.delete(id)` | `deleteAllByEntityItem("last_mile", id)` (qualquer `event`) | MongoDB + Quartz |
|
|
758
|
+
| Cleanup vestigial no Bonus.delete | `BonusManager.delete(id)` | `deleteAllByEntityItem("bonus", id)` — limpa schedulers que o `insert` não mais cria | MongoDB + Quartz |
|
|
759
|
+
| Instalação a partir de app template | `AppManager.funifierCreateScheduler(payload)` | Desserializa JSON em `SchedulerConfig` e chama `add()` | MongoDB + Quartz |
|
|
760
|
+
| Boot bootstrap | `Configuration.contextInitialized` → `SchedulerSystem.contextInitialized` | Recarrega todos schedulers ativos no Quartz | Quartz (memória) |
|
|
761
|
+
| Shutdown | `Configuration.contextDestroyed` → `quartzScheduler.shutdown(true)` | Para todos os jobs aguardando finalização | — |
|
|
762
|
+
|
|
763
|
+
### 6.1 Fluxo de cascade ao deletar uma gamificação
|
|
764
|
+
|
|
765
|
+
```mermaid
|
|
766
|
+
flowchart LR
|
|
767
|
+
A([DELETE game]) --> B[GameManager.delete]
|
|
768
|
+
B --> C[schedulerManager.deleteAll]
|
|
769
|
+
C --> D[(loop SchedulerConfigs)]
|
|
770
|
+
D --> E[quartz.deleteJob<br/>por scheduler]
|
|
771
|
+
D --> F[c.drop scheduler collection]
|
|
772
|
+
F --> G[SystemFactory.deleteTotal apiKey]
|
|
773
|
+
G --> H[(remove de scheduler<br/>do sistema)]
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
---
|
|
777
|
+
|
|
778
|
+
## 7. Suportado vs NÃO Suportado
|
|
779
|
+
|
|
780
|
+
### ✅ Suportado
|
|
781
|
+
|
|
782
|
+
- Criar/atualizar/deletar scheduler via REST (`POST`, `DELETE`).
|
|
783
|
+
- Listar com filtros via aggregate pipeline (`q`, `published_min/max`, `orderby`, `reverse`, `max_results`).
|
|
784
|
+
- Listar logs com paginação por header `Range`.
|
|
785
|
+
- Execução manual via `/v3/scheduler/execute/{id}` com sobrescrita opcional de `time`.
|
|
786
|
+
- Cálculo das próximas 10 datas via `/v3/scheduler/next/{id}`.
|
|
787
|
+
- Limpeza manual de cache de compilação via `/v3/scheduler/compile/{id|all}`.
|
|
788
|
+
- Timezone IANA por scheduler.
|
|
789
|
+
- Timeout configurável por scheduler (em segundos, default 30).
|
|
790
|
+
- Sandbox Groovy via `SecureASTCustomizer` (blacklist + tokens whitelist).
|
|
791
|
+
- Limite diário por gamificação (default 100/dia, configurável em `gamification_limits`).
|
|
792
|
+
- Cache de classes Groovy compiladas com invalidação por `updated`.
|
|
793
|
+
- Modo entity para `last_mile` (único callback ativo).
|
|
794
|
+
- Status agregado do Quartz via `/v3/util/cron/status` e `/v3/system/scheduler/status`.
|
|
795
|
+
- Listagem completa dos JobKeys do Quartz via `/v3/system/scheduler/jobs`.
|
|
796
|
+
- Instalação programática via `AppManager.funifierCreateScheduler` (a partir de templates de app).
|
|
797
|
+
- Cascade automático em `LastMileManager.insert/delete` e `GameManager.delete`.
|
|
798
|
+
|
|
799
|
+
### ❌ NÃO Suportado
|
|
800
|
+
|
|
801
|
+
- **PATCH parcial**: `POST` substitui o documento inteiro. Não há endpoint de update parcial.
|
|
802
|
+
- **Modo entity para `bonus`**: o bloco em `ManagerFactory.getSchedulerCallback` está totalmente comentado (linhas 240-243). Qualquer scheduler com `entity = "bonus"` é auto-desabilitado na primeira execução. O método `schedulerCallback` em `BonusManager.java:354-359` também está dentro de bloco comentado.
|
|
803
|
+
- **Modo entity para qualquer entidade fora `last_mile`**: o dispatcher tem só uma entrada ativa.
|
|
804
|
+
- **Retry em falha**: execuções com exception ou timeout não são re-tentadas; aguardam o próximo disparo cron.
|
|
805
|
+
- **Persistência dos contadores de limite**: `SchedulerStatistics` é em memória; restart zera. O snapshot vai parar em `STATISTIC_LOG` apenas em outros caminhos do `StatisticManager`.
|
|
806
|
+
- **HTTP non-201 em erro de validação**: o `POST` sempre retorna `201 Created` mesmo com `status: "ERROR"` no body. Falhas de compilação Groovy e parse de cron NÃO produzem `400 Bad Request`.
|
|
807
|
+
- **Idempotência do `SchedulerLog._id`**: o `_id` é o contador diário em memória como string (`"1"`, `"2"`, ...). Após restart o contador reinicia; o `save()` faz upsert — logs com mesmo `_id` se sobrescrevem entre dias/restarts.
|
|
808
|
+
- **404 quando scheduler não existe**: `GET /v3/scheduler/{id}` para id inexistente retorna body vazio (JSON `{}`) com HTTP 200, não `404`. `GET /v3/scheduler/execute/{id}` lança `NullPointerException` (HTTP 500) — o `null` do `findById` não é tratado.
|
|
809
|
+
- **Validação de combinação `entity` + `event` + `item`**: aceita combinações parciais que caem silenciosamente em script mode.
|
|
810
|
+
- **Lock cluster-wide**: em múltiplos nós, cada nó executa o cron independentemente. Não há leader election — para evitar duplicação use lógica no script.
|
|
811
|
+
- **Cron Unix (5 campos)**: apenas Quartz (6 campos). Expressões Unix-style são rejeitadas no `new CronExpression(...)`.
|
|
812
|
+
- **Tokens excluídos da whitelist do sandbox**: `<<`, `>>`, `&`, `|`, `^`, `?:`, `?.`, `..` causam erro de compilação. Operações de bit, navegação null-safe e ranges (`1..10`) não estão liberadas.
|
|
813
|
+
- **`@TimedInterrupt` configurável**: o teto absoluto de 2000 segundos no wrapper é hardcoded em `SchedulerRunner.getScript:200`.
|
|
814
|
+
- **Snapshot de `time` por execução cron**: `scheduler.time` só é preenchido pelo endpoint `/execute` — em execuções Quartz é `null`. Para usar o horário corrente no script, use `new Date()` diretamente.
|
|
815
|
+
- **Pausar / despausar via REST**: não há endpoint. Setar `active=false` via POST + re-salvar é o caminho.
|
|
816
|
+
- **Limpeza orfan do Quartz**: se o documento for removido do MongoDB diretamente (via Database REST `/v3/database/scheduler`), o job no Quartz permanece até reboot ou intervenção manual.
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## 8. Segurança e Permissões
|
|
821
|
+
|
|
822
|
+
### Autenticação
|
|
823
|
+
|
|
824
|
+
Todas as rotas de `/v3/scheduler/*` usam `@BeanParam AuthBean` → Bearer token. O `apiKey` derivado do token é usado para:
|
|
825
|
+
|
|
826
|
+
- Buscar o `ManagerFactory` da gamificação (`FrontController.getInstance(apiKey).getManagerFactory()`).
|
|
827
|
+
- Identidade do job Quartz: `JobKey(scheduler.id, apiKey)` — isolamento por gamificação.
|
|
828
|
+
- Conexão MongoDB já namespaced em `manager.getJongoConnection()`.
|
|
829
|
+
|
|
830
|
+
As rotas `/v3/system/scheduler/*` não exigem `AuthBean` por código no `SchedulerRest` (system-side), mas dependem da configuração do servidor (geralmente atrás de filtro de IP/role).
|
|
831
|
+
|
|
832
|
+
### Sandbox Groovy
|
|
833
|
+
|
|
834
|
+
Duas camadas:
|
|
835
|
+
|
|
836
|
+
**1. `TriggerExpressionChecker` — blacklist (compartilhado com Trigger):**
|
|
837
|
+
|
|
838
|
+
Classes proibidas (match em `expression.getType().getName()`):
|
|
839
|
+
- `java.lang.System`
|
|
840
|
+
- `java.lang.ProcessBuilder`
|
|
841
|
+
- `java.io.File`
|
|
842
|
+
- `groovy.lang.GroovyShell`
|
|
843
|
+
- `groovy.lang.GroovyObject`
|
|
844
|
+
|
|
845
|
+
Expressões proibidas (match por **substring** em `expression.getText()`):
|
|
846
|
+
- `.execute`
|
|
847
|
+
- `.getDB`
|
|
848
|
+
- `.getMongo`
|
|
849
|
+
- `.dropDatabase`
|
|
850
|
+
|
|
851
|
+
**2. Whitelist de tokens** (apenas operadores listados são permitidos):
|
|
852
|
+
|
|
853
|
+
`=`, `+`, `-`, `*`, `/`, `%`, `**`, `++`, `+=`, `--`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `||`, `||=`, `&&`, `&&=`, `[`, `]`
|
|
854
|
+
|
|
855
|
+
### Limitações conhecidas do sandbox
|
|
856
|
+
|
|
857
|
+
- **Match por substring**, não por AST semântico: uma expressão que evite literalmente `.execute` (ex.: indireção via reflexão ou `eval`) pode passar pelo filtro. Concatenação de strings (`'.exec' + 'ute'`) não é resolvida em tempo de compilação por este checker.
|
|
858
|
+
- **Apenas tipos diretos** são bloqueados — referências por string e reflexão geral (`Class.forName`) não são impedidas por essa lista.
|
|
859
|
+
- O `GroovyClassLoader` usa `Thread.currentThread().getContextClassLoader()` — scripts compilam contra todo o classpath da aplicação. Qualquer classe Funifier ou de terceiros disponível em runtime é acessível desde que não passe pelos filtros acima.
|
|
860
|
+
- O `executor.shutdownNow()` envia `Thread.interrupt()`, mas em loops Groovy puros sem checagem de `Thread.interrupted()`, a thread pode continuar até o `@TimedInterrupt` (2000s).
|
|
861
|
+
|
|
862
|
+
### Superfícies de injeção
|
|
863
|
+
|
|
864
|
+
- **`q` em `GET /v3/scheduler`** — concatenado raw no `$match`. Operadores como `$where` (JavaScript eval no Mongo) podem ser usados para exfiltrar dados ou consumir recursos.
|
|
865
|
+
- **`q` em `GET /v3/scheduler/log`** — mesmo padrão.
|
|
866
|
+
- **`orderby` em ambos** — concatenado raw em `{$sort: {<orderby>: #}}`. Não permite escrita mas permite enumerar campos.
|
|
867
|
+
- **`script` no `POST`** — sandbox protege contra acesso direto a APIs perigosas, mas todo o classpath fica acessível.
|
|
868
|
+
|
|
869
|
+
### Isolamento por tenant
|
|
870
|
+
|
|
871
|
+
- Conexão MongoDB e `JobKey` por `apiKey` garantem isolamento horizontal.
|
|
872
|
+
- `compiled`/`dates` são por `SchedulerManager` (uma instância por `ManagerFactory`, ou seja, por gamificação).
|
|
873
|
+
|
|
874
|
+
---
|
|
875
|
+
|
|
876
|
+
## 9. Observabilidade e Troubleshooting
|
|
877
|
+
|
|
878
|
+
### Diagnóstico rápido
|
|
879
|
+
|
|
880
|
+
```
|
|
881
|
+
# Existência e estado:
|
|
882
|
+
GET /v3/scheduler/<id>
|
|
883
|
+
|
|
884
|
+
# Quartz vê o job? (lista próximas datas):
|
|
885
|
+
GET /v3/scheduler/next/<id>?reference=-0d
|
|
886
|
+
|
|
887
|
+
# Estado agregado do Quartz para esta gamificação:
|
|
888
|
+
GET /v3/util/cron/status
|
|
889
|
+
→ checar references["gamification_<apikey>_sheduler_<id>"]
|
|
890
|
+
→ checar schedulers[].next != null
|
|
891
|
+
|
|
892
|
+
# Executar manualmente para reproduzir:
|
|
893
|
+
GET /v3/scheduler/execute/<id>
|
|
894
|
+
→ inspecionar response.exceptions e response.outputs
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### Consultas úteis a logs
|
|
898
|
+
|
|
899
|
+
```
|
|
900
|
+
# Últimas 10 execuções:
|
|
901
|
+
GET /v3/scheduler/log?item=<id>&orderby=time&reverse=true&max_results=10
|
|
902
|
+
|
|
903
|
+
# Execuções com erro:
|
|
904
|
+
GET /v3/scheduler/log?item=<id>&q={"err":{$ne:null}}&orderby=time&reverse=true
|
|
905
|
+
|
|
906
|
+
# Execuções no último dia:
|
|
907
|
+
GET /v3/scheduler/log?item=<id>&published_min=-1d&orderby=time&reverse=true
|
|
908
|
+
|
|
909
|
+
# Execuções lentas (>5s):
|
|
910
|
+
GET /v3/scheduler/log?item=<id>&q={"ms":{$gte:5000}}&orderby=ms&reverse=true
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
### Erros comuns e causas
|
|
914
|
+
|
|
915
|
+
| Sintoma | Causa provável | Verificação |
|
|
916
|
+
|---------|----------------|-------------|
|
|
917
|
+
| Scheduler não executa após criar | `active=false`, cron inválida apesar do POST aceitar, ou exception silenciosa no `scheduleJob` | `GET /v3/scheduler/<id>` (active?); `GET /v3/scheduler/next/<id>` (lista vazia indica não-agendado) |
|
|
918
|
+
| Scheduler parou após N execuções | Limite diário atingido | Comparar com `dailySchedulerExecutions` em `getSchedulerStatistics()` (não exposto em REST — usar log: `q={"err":{$ne:null}}`) |
|
|
919
|
+
| Scheduler executa em horário errado | `timezone` null ou JVM em UTC | Conferir campo `timezone`; usar `/next/{id}` para validar |
|
|
920
|
+
| `POST` retornou `status: OK` mas job não roda | `active=false` no payload OU `scheduleJob` falhou silenciosamente após `c.save` | Sempre passar `"active": true` quando quiser execução; verificar `/next/{id}` |
|
|
921
|
+
| Scheduler some após reboot | Documento `{_id: apiKey, total: N}` ausente em `scheduler` do sistema | Inserir manualmente (`{_id: <apiKey>, total: <count>}`) ou refazer um POST que reativa o índice via `addTotal` |
|
|
922
|
+
| `runEntity` falha com `SchedulerCallback does not exist` | `entity` referencia coleção sem callback (`bonus`, custom, typo) | Mover para `script` ou usar `entity: "last_mile"` |
|
|
923
|
+
| HTTP 500 em `/v3/scheduler/execute/<id>` | `findById` retornou null (ID inexistente) | Verificar com `GET /v3/scheduler/<id>` antes de executar |
|
|
924
|
+
| Script executa código antigo | Cache de `compiled` desatualizado vs `updated` em outro nó | `PUT /v3/scheduler/compile/<id>` em todos os nós; alternativamente `POST` para forçar `updated = now` |
|
|
925
|
+
| Compilação falha com mensagem `Token X is not allowed` | Uso de operador fora da whitelist (`<<`, `?.`, `..`, etc.) | Reescrever sem o operador |
|
|
926
|
+
| Compilação falha com `Type X is not authorized` | Uso direto de `System`, `File`, `ProcessBuilder`, `GroovyShell`, `GroovyObject` | Usar APIs do `manager`/`database` |
|
|
927
|
+
| Logs com `_id` duplicado entre dias | Contador `dailySchedulerExecutions` reiniciou após reboot | Usar `time` + `item` para identificar; aceitar como limitação |
|
|
928
|
+
|
|
929
|
+
### Verificar drift Quartz vs MongoDB (system-only)
|
|
930
|
+
|
|
931
|
+
```
|
|
932
|
+
GET /v3/system/scheduler/jobs
|
|
933
|
+
→ procurar entradas com schedulerName = "SCHEDULER DONT EXIST"
|
|
934
|
+
→ indica job no Quartz sem documento no banco (drift)
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
## 10. Exemplos Práticos
|
|
940
|
+
|
|
941
|
+
### Exemplo mínimo funcional
|
|
942
|
+
|
|
943
|
+
```json
|
|
944
|
+
POST /v3/scheduler
|
|
945
|
+
{
|
|
946
|
+
"active": true,
|
|
947
|
+
"name": "Hello daily",
|
|
948
|
+
"cron": "0 0 9 * * ?",
|
|
949
|
+
"timezone": "America/Sao_Paulo",
|
|
950
|
+
"script": "void execute(scheduler){ println 'ran at ' + new Date() }"
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
### Exemplo avançado com `extra`, `timeout` e uso de `manager`/`database`
|
|
955
|
+
|
|
956
|
+
```json
|
|
957
|
+
POST /v3/scheduler
|
|
958
|
+
{
|
|
959
|
+
"_id": "debit_inactive_xp",
|
|
960
|
+
"active": true,
|
|
961
|
+
"name": "Debitar XP de inativos (30 dias)",
|
|
962
|
+
"cron": "0 0 2 * * ?",
|
|
963
|
+
"timezone": "America/Sao_Paulo",
|
|
964
|
+
"timeout": 120,
|
|
965
|
+
"extra": {
|
|
966
|
+
"diasInatividade": 30,
|
|
967
|
+
"pontoTipo": "xp",
|
|
968
|
+
"pontosDebitar": 10
|
|
969
|
+
},
|
|
970
|
+
"script": "void execute(scheduler){\n def dias = scheduler.extra.diasInatividade\n def pontos = scheduler.extra.pontosDebitar\n def tipo = scheduler.extra.pontoTipo\n def ref = DateUtil.addDays(new Date(), -dias)\n def players = database.find('player', '{\"lastAction\":{\"$lt\":#}}', ref)\n for(def p : players){\n manager.getPointManager().add(p._id, tipo, -pontos, null)\n }\n println 'processed ' + players.size() + ' players'\n}"
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
### Debug com tempo simulado
|
|
975
|
+
|
|
976
|
+
```
|
|
977
|
+
# Simular execução como se fosse 2024-12-31 23:59 BRT
|
|
978
|
+
GET /v3/scheduler/execute/debit_inactive_xp?time=2024-12-31T23:59:00-03:00
|
|
979
|
+
|
|
980
|
+
# Resposta:
|
|
981
|
+
{
|
|
982
|
+
"scheduler": { "_id": "debit_inactive_xp", "time": "2024-12-31T23:59:00-03:00", ... },
|
|
983
|
+
"exceptions": [],
|
|
984
|
+
"outputs": ["processed 42 players"],
|
|
985
|
+
"millis": 1834
|
|
986
|
+
}
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### Listar próximas execuções a partir de uma data
|
|
990
|
+
|
|
991
|
+
```
|
|
992
|
+
GET /v3/scheduler/next/debit_inactive_xp?reference=2025-01-01T00:00:00-03:00
|
|
993
|
+
→
|
|
994
|
+
[
|
|
995
|
+
"2025-01-02T02:00:00-03:00",
|
|
996
|
+
"2025-01-03T02:00:00-03:00",
|
|
997
|
+
"2025-01-04T02:00:00-03:00",
|
|
998
|
+
"..."
|
|
999
|
+
]
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### Forçar recompilação após mudança em cluster
|
|
1003
|
+
|
|
1004
|
+
```
|
|
1005
|
+
PUT /v3/scheduler/compile/debit_inactive_xp
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
### Anti-pattern: script sem assinatura `void execute(scheduler)`
|
|
1009
|
+
|
|
1010
|
+
```groovy
|
|
1011
|
+
// ERRADO — o executor faz invokeMethod("execute", [scheduler]) literalmente.
|
|
1012
|
+
// Sem este método exato, GroovyObject lança MissingMethodException.
|
|
1013
|
+
println 'hello'
|
|
1014
|
+
|
|
1015
|
+
// CORRETO
|
|
1016
|
+
void execute(scheduler){ println 'hello' }
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
### Anti-pattern: declarar `import` no script
|
|
1020
|
+
|
|
1021
|
+
```groovy
|
|
1022
|
+
// ERRADO — getScript() já injeta imports padrão antes da class FunifierScheduler.
|
|
1023
|
+
// imports adicionais dentro da class causam CompilationFailedException.
|
|
1024
|
+
import com.funifier.engine.player.Player
|
|
1025
|
+
void execute(scheduler){ ... }
|
|
1026
|
+
|
|
1027
|
+
// CORRETO — classes Funifier comuns (Player, Achievement, Lottery, Action, etc.)
|
|
1028
|
+
// já estão importadas pelo wrapper. Use diretamente.
|
|
1029
|
+
void execute(scheduler){
|
|
1030
|
+
Player p = manager.getPlayerManager().findById("playerId")
|
|
1031
|
+
}
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
### Anti-pattern: scheduler entity com entidade não suportada
|
|
1035
|
+
|
|
1036
|
+
```json
|
|
1037
|
+
// ERRADO — primeira execução vai gravar exception + setar active=false
|
|
1038
|
+
{
|
|
1039
|
+
"active": true,
|
|
1040
|
+
"cron": "0 0 9 * * ?",
|
|
1041
|
+
"entity": "bonus",
|
|
1042
|
+
"event": "notify",
|
|
1043
|
+
"item": "bonus_id_123"
|
|
1044
|
+
}
|
|
1045
|
+
// Erro registrado: "SchedulerCallback does not exist for entity bonus"
|
|
1046
|
+
// Estado pós-execução: active=false (persistido) + job removido do Quartz
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
### Anti-pattern: cron Unix (5 campos)
|
|
1050
|
+
|
|
1051
|
+
```
|
|
1052
|
+
// ERRADO — Quartz precisa de 6 campos
|
|
1053
|
+
"cron": "0 9 * * *"
|
|
1054
|
+
|
|
1055
|
+
// CORRETO — Quartz com campo de segundos e `?` em um dos dia/dia-semana
|
|
1056
|
+
"cron": "0 0 9 * * ?"
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
### Anti-pattern: depender de `scheduler.time` em execução cron
|
|
1060
|
+
|
|
1061
|
+
```groovy
|
|
1062
|
+
// ERRADO — scheduler.time é null em execuções Quartz normais
|
|
1063
|
+
void execute(scheduler){
|
|
1064
|
+
def ref = scheduler.time // null
|
|
1065
|
+
// ...
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// CORRETO — use new Date() ou scheduler.extra.referenceTime
|
|
1069
|
+
void execute(scheduler){
|
|
1070
|
+
def ref = new Date()
|
|
1071
|
+
// ...
|
|
1072
|
+
}
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
### Anti-pattern: usar operador fora da whitelist
|
|
1076
|
+
|
|
1077
|
+
```groovy
|
|
1078
|
+
// ERRADO — operadores ?., .., ?:, << não estão na whitelist
|
|
1079
|
+
void execute(scheduler){
|
|
1080
|
+
def name = scheduler?.extra?.user?.name // ?. bloqueado
|
|
1081
|
+
for(int i in 1..10) { ... } // .. bloqueado
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// CORRETO — usar checagem null explícita e for clássico
|
|
1085
|
+
void execute(scheduler){
|
|
1086
|
+
def name = (scheduler != null && scheduler.extra != null && scheduler.extra.user != null) ? scheduler.extra.user.name : null
|
|
1087
|
+
for(int i = 1; i <= 10; i++) { ... }
|
|
1088
|
+
}
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
---
|
|
1092
|
+
|
|
1093
|
+
## Checklist de Configuração
|
|
1094
|
+
|
|
1095
|
+
- [ ] `active: true` definido (default é `false` — o documento é salvo mas o job NÃO é agendado)
|
|
1096
|
+
- [ ] `cron` é Quartz com 6 campos (`seg min hor dia mês diasem`) e usa `?` em um dos campos `dia`/`dia-semana`
|
|
1097
|
+
- [ ] `timezone` IANA preenchido — não confiar no default da JVM (geralmente UTC em containers)
|
|
1098
|
+
- [ ] **Script mode**: `script` contém exatamente `void execute(scheduler){ ... }` (assinatura literal)
|
|
1099
|
+
- [ ] **Script mode**: nenhum `import` no script (já injetados pelo wrapper)
|
|
1100
|
+
- [ ] **Script mode**: nenhum dos operadores fora da whitelist (`<<`, `>>`, `&`, `|`, `^`, `?:`, `?.`, `..`)
|
|
1101
|
+
- [ ] **Entity mode**: três campos preenchidos (`entity`, `event`, `item`) e `entity` referencia uma coleção COM callback registrado em `ManagerFactory.getSchedulerCallback`
|
|
1102
|
+
- [ ] **Entity mode**: para `last_mile`, sempre criado pelo `LastMileManager.insert` — não criar manualmente (ele sobrescreve campos)
|
|
1103
|
+
- [ ] `timeout` ajustado se a operação puder demorar mais de 30s
|
|
1104
|
+
- [ ] Validar POST: ler o `status` no body — HTTP é sempre 201, mesmo em erro de compilação
|
|
1105
|
+
- [ ] Após criar, validar via `GET /v3/scheduler/next/<id>` que retorna lista não-vazia
|
|
1106
|
+
- [ ] Armadilha: scheduler com `entity` definido mas sem callback é **auto-desabilitado na primeira execução**, consumindo quota
|
|
1107
|
+
- [ ] Armadilha: scheduler somem após restart se o índice `{_id: apiKey, total: N}` da coleção `scheduler` do sistema for limpo
|
|
1108
|
+
- [ ] Armadilha: `c.save()` no POST é full-replace — preserve campos que não quiser perder
|
|
1109
|
+
- [ ] Armadilha: parâmetros `q` e `orderby` nos GET são concatenados raw no MongoDB; trate-os como confiáveis apenas com produtores também confiáveis
|
|
1110
|
+
- [ ] Armadilha: em cluster multi-node, cada nó executa o cron — dedupe na lógica do script
|
|
1111
|
+
- [ ] Armadilha: `SchedulerLog._id` é um contador diário em memória; após restart pode haver upsert sobre logs antigos
|