Skip to content

Commit 8c1eb79

Browse files
committed
feat: improve cron observability
2 parents 68cd3b1 + 73421b8 commit 8c1eb79

File tree

8 files changed

+168
-2
lines changed

8 files changed

+168
-2
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,22 @@ Where `env`: `preview`, `testnet`, or `testnet-preview` (omit for mainnet produc
213213

214214
**Required secrets:** `ALBATROSS_RPC_NODE_URL`, `NUXT_SLACK_WEBHOOK_URL`
215215

216+
### D1 Migrations
217+
218+
When adding a new SQL migration under `server/db/migrations/`, apply it to the remote D1 database.
219+
220+
For the `cron_runs` table:
221+
222+
```bash
223+
pnpm db:apply:cron-runs:mainnet
224+
```
225+
226+
Testnet:
227+
228+
```bash
229+
pnpm db:apply:cron-runs:testnet
230+
```
231+
216232
**Environments** (configured in `wrangler.json`):
217233

218234
| Environment | Dashboard URL | Trigger |

nuxt.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ export default defineNuxtConfig({
148148
tasks: true,
149149
},
150150
scheduledTasks: {
151-
// 12-hour sync: fetch missing epochs and update validator snapshots
152-
'0 */12 * * *': ['sync:epochs', 'sync:snapshot'],
151+
// 12-hour sync: wrapper task records run + executes sync tasks
152+
'0 */12 * * *': ['cron:sync'],
153153
},
154154
openAPI: {
155155
meta: { title: name, description, version },

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"test": "vitest",
2626
"db:generate": "drizzle-kit generate",
2727
"db:delete": "drizzle-kit drop",
28+
"db:apply:cron-runs:mainnet": "wrangler d1 execute validators-api-mainnet --remote --yes --command \"CREATE TABLE IF NOT EXISTS cron_runs (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, cron text NOT NULL, network text NOT NULL, git_branch text, started_at text NOT NULL, finished_at text, status text NOT NULL, error_message text, meta text);\" && wrangler d1 execute validators-api-mainnet --remote --yes --command \"CREATE INDEX IF NOT EXISTS idx_cron_runs_started_at ON cron_runs (started_at);\"",
29+
"db:apply:cron-runs:testnet": "wrangler d1 execute validators-api-testnet --remote --yes --command \"CREATE TABLE IF NOT EXISTS cron_runs (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, cron text NOT NULL, network text NOT NULL, git_branch text, started_at text NOT NULL, finished_at text, status text NOT NULL, error_message text, meta text);\" && wrangler d1 execute validators-api-testnet --remote --yes --command \"CREATE INDEX IF NOT EXISTS idx_cron_runs_started_at ON cron_runs (started_at);\"",
2830
"release": "bumpp -r && nr -r publish",
2931
"validate:json-files": "tsx scripts/validate-json-files.ts"
3032
},
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
CREATE TABLE `cron_runs` (
2+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3+
`cron` text NOT NULL,
4+
`network` text NOT NULL,
5+
`git_branch` text,
6+
`started_at` text NOT NULL,
7+
`finished_at` text,
8+
`status` text NOT NULL,
9+
`error_message` text,
10+
`meta` text
11+
);
12+
--> statement-breakpoint
13+
CREATE INDEX `idx_cron_runs_started_at` ON `cron_runs` (`started_at`);
14+

server/db/schema.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ export const activity = sqliteTable('activity', {
5959
primaryKey({ columns: [table.validatorId, table.epochNumber] }),
6060
])
6161

62+
export const cronRuns = sqliteTable('cron_runs', {
63+
id: integer('id').primaryKey({ autoIncrement: true }),
64+
cron: text('cron').notNull(),
65+
network: text('network').notNull(),
66+
gitBranch: text('git_branch'),
67+
startedAt: text('started_at').notNull(),
68+
finishedAt: text('finished_at'),
69+
status: text('status').notNull(),
70+
errorMessage: text('error_message'),
71+
meta: text('meta', { mode: 'json' }),
72+
}, table => [
73+
index('idx_cron_runs_started_at').on(table.startedAt),
74+
])
75+
6276
export const activityRelations = relations(activity, ({ one }) => ({
6377
validator: one(validators, {
6478
fields: [activity.validatorId],

server/tasks/cron/sync.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type { TaskEvent } from 'nitropack'
2+
import { consola } from 'consola'
3+
import { runTask } from 'nitropack/runtime'
4+
import { eq, tables, useDrizzle } from '~~/server/utils/drizzle'
5+
6+
const CRON_EXPRESSION = '0 */12 * * *'
7+
const TASKS: string[] = ['sync:epochs', 'sync:snapshot']
8+
9+
function toErrorString(error: unknown) {
10+
if (error instanceof Error)
11+
return `${error.message}${error.stack ? `\n${error.stack}` : ''}`
12+
return String(error)
13+
}
14+
15+
export default defineTask({
16+
meta: {
17+
name: 'cron:sync',
18+
description: 'Main scheduled sync wrapper (records cron run to D1)',
19+
},
20+
async run(event: TaskEvent) {
21+
const config = useSafeRuntimeConfig()
22+
const { nimiqNetwork, gitBranch } = config.public
23+
24+
const startedAt = new Date().toISOString()
25+
26+
let cronRunId: number | undefined
27+
try {
28+
cronRunId = await useDrizzle()
29+
.insert(tables.cronRuns)
30+
.values({
31+
cron: CRON_EXPRESSION,
32+
network: nimiqNetwork,
33+
gitBranch,
34+
startedAt,
35+
status: 'started',
36+
meta: {
37+
tasks: TASKS,
38+
payload: event.payload ?? {},
39+
},
40+
})
41+
.returning({ id: tables.cronRuns.id })
42+
.get()
43+
.then(r => r.id)
44+
}
45+
catch (error) {
46+
consola.warn('[cron:sync] unable to record start (missing migration?)', error)
47+
}
48+
49+
try {
50+
const results: Record<string, unknown> = {}
51+
52+
for (const taskName of TASKS) {
53+
consola.info(`[cron:sync] running ${taskName}`)
54+
const res = await runTask(taskName, { payload: event.payload ?? {}, context: event.context ?? {} })
55+
results[taskName] = (res as any)?.result ?? res
56+
}
57+
58+
if (cronRunId) {
59+
try {
60+
await useDrizzle()
61+
.update(tables.cronRuns)
62+
.set({
63+
finishedAt: new Date().toISOString(),
64+
status: 'success',
65+
meta: {
66+
cron: CRON_EXPRESSION,
67+
network: nimiqNetwork,
68+
tasks: TASKS,
69+
results,
70+
},
71+
})
72+
.where(eq(tables.cronRuns.id, cronRunId))
73+
.execute()
74+
}
75+
catch (error) {
76+
consola.warn('[cron:sync] unable to record success', error)
77+
}
78+
}
79+
80+
return { result: { success: true, cronRunId } }
81+
}
82+
catch (error) {
83+
const errorMessage = toErrorString(error)
84+
consola.error('[cron:sync] failed', error)
85+
86+
if (cronRunId) {
87+
try {
88+
await useDrizzle()
89+
.update(tables.cronRuns)
90+
.set({
91+
finishedAt: new Date().toISOString(),
92+
status: 'error',
93+
errorMessage,
94+
meta: {
95+
cron: CRON_EXPRESSION,
96+
network: nimiqNetwork,
97+
tasks: TASKS,
98+
},
99+
})
100+
.where(eq(tables.cronRuns.id, cronRunId))
101+
.execute()
102+
}
103+
catch (error) {
104+
consola.warn('[cron:sync] unable to record error', error)
105+
}
106+
}
107+
108+
throw error
109+
}
110+
},
111+
})

server/utils/drizzle.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ export type Activity = typeof schema.activity.$inferSelect
1414
export type NewActivity = typeof schema.activity.$inferInsert
1515
export type Score = typeof schema.scores.$inferSelect
1616
export type NewScore = typeof schema.scores.$inferInsert
17+
export type CronRun = typeof schema.cronRuns.$inferSelect
18+
export type NewCronRun = typeof schema.cronRuns.$inferInsert

wrangler.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
"account_id": "cf9baad7d68d7ee717f3339731e81dfb",
77
"compatibility_date": "2025-01-01",
88
"compatibility_flags": ["nodejs_compat"],
9+
"observability": {
10+
"enabled": true,
11+
"logs": {
12+
"enabled": false,
13+
"invocation_logs": true
14+
}
15+
},
916
"triggers": { "crons": ["0 */12 * * *"] },
1017
"vars": { "NUXT_PUBLIC_NIMIQ_NETWORK": "main-albatross" },
1118
"d1_databases": [{ "binding": "DB", "database_id": "cc9f1d25-676b-4cb3-8af6-887e85a08baa", "database_name": "validators-api-mainnet" }],

0 commit comments

Comments
 (0)