See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Replace adonisjs-scheduler with standalone scheduler:run command

Uses croner directly in a dedicated scheduler:run ace command instead of
the adonisjs-scheduler package's queue-worker integration. Scheduler now
runs in its own container, decoupling scheduled dispatch from backfill
job execution.

Drops adonisjs-scheduler from the runtime (see pnpm-lock.yaml shrink),
removes the console-only scheduler preload in adonisrc.ts, and strips
the hand-wired scheduler start path out of commands/queue_work.ts.

Threshold scan fires every 60s via croner's { protect: true } guard.
THRESHOLDS temporarily lowered to [10, 100] for live testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+44 -134
-13
adonisrc.ts
··· 37 37 ]) 38 38 return new ListLoader([QueueWork]) 39 39 }, 40 - () => import('adonisjs-scheduler/commands'), 41 40 ], 42 41 43 42 /* ··· 71 70 () => import('#providers/clickhouse_provider'), 72 71 () => import('#providers/posthog_provider'), 73 72 () => import('@adonisjs/otel/otel_provider'), 74 - { 75 - file: () => import('adonisjs-scheduler/scheduler_provider'), 76 - environment: ['console'], 77 - }, 78 73 ], 79 74 80 75 /* ··· 89 84 () => import('#start/routes'), 90 85 () => import('#start/kernel'), 91 86 () => import('#start/validator'), 92 - { 93 - file: () => import('#start/scheduler'), 94 - // Scheduled work is dispatched inside queue-worker via `node ace 95 - // queue:work`. The scheduler provider is only registered in `console`, 96 - // so this preload must be console-only to avoid a binding-resolution 97 - // error when `pnpm dev` boots the web server. 98 - environment: ['console'], 99 - }, 100 87 ], 101 88 102 89 /*
+2 -2
app/jobs/threshold_scan_job.ts
··· 14 14 * Thresholds scanned by this job. Ordered ascending so that 15 15 * findPostsAtOrAboveThreshold can be called with the smallest value. 16 16 */ 17 - const THRESHOLDS = [1000, 10_000] as const 17 + const THRESHOLDS = [10, 100] as const 18 18 19 19 const WINDOW_DAYS = 7 20 20 ··· 52 52 * configured virality thresholds (1k, 10k) and fires a Discord webhook the 53 53 * first time each post crosses each threshold. 54 54 * 55 - * Dispatched every 60 seconds by `start/scheduler.ts`. 55 + * Dispatched every 60 seconds by `commands/scheduler_run.ts`. 56 56 */ 57 57 @inject() 58 58 export default class ThresholdScanJob extends Job<Record<string, never>> {
-26
commands/queue_work.ts
··· 1 1 import QueueWorkBase from '@adonisjs/queue/commands/queue_work' 2 - import { Worker as SchedulerWorker } from 'adonisjs-scheduler' 3 2 4 3 /** 5 4 * Local shadow of @adonisjs/queue's queue:work command. ··· 10 9 * forces Node's default-action exit. We wait for super.run() to finish 11 10 * (boringnode's shutdown handler has already drained the pool by then), then 12 11 * terminate the app and exit explicitly. 13 - * 14 - * We also boot `adonisjs-scheduler`'s Worker inside this same process so the 15 - * scheduled `ThresholdScanJob` dispatches fire from the queue-worker container, 16 - * avoiding a dedicated scheduler entrypoint (see docs/superpowers/specs/ 17 - * 2026-04-14-firehose-virality-webhook-design.md §"Threshold poll"). 18 12 */ 19 13 export default class QueueWork extends QueueWorkBase { 20 - private schedulerWorker?: SchedulerWorker 21 - 22 14 async run() { 23 15 try { 24 - this.schedulerWorker = new SchedulerWorker(this.app) 25 - await this.schedulerWorker.start() 26 - } catch (error) { 27 - this.logger.error( 28 - `Scheduler worker failed to start: ${ 29 - error instanceof Error ? error.message : String(error) 30 - }` 31 - ) 32 - } 33 - 34 - try { 35 16 await super.run() 36 17 } catch (error) { 37 18 this.logger.error(error instanceof Error ? error.message : String(error)) 38 19 this.exitCode = 1 39 20 } 40 21 41 - if (this.schedulerWorker) { 42 - try { 43 - await this.schedulerWorker.stop() 44 - } catch { 45 - /* ignore */ 46 - } 47 - } 48 22 try { 49 23 await this.app.terminate() 50 24 } catch {
+33
commands/scheduler_run.ts
··· 1 + import { BaseCommand } from '@adonisjs/core/ace' 2 + import type { CommandOptions } from '@adonisjs/core/types/ace' 3 + import { Cron } from 'croner' 4 + import ThresholdScanJob from '#jobs/threshold_scan_job' 5 + 6 + /** 7 + * Scheduler entrypoint. Runs in its own container (docker-compose `scheduler`) 8 + * so scheduled work doesn't compete with the queue worker's backfill jobs. 9 + * Single replica only — no distributed locking. See the firehose virality 10 + * webhook design doc for the reasoning behind the 60s threshold scan cadence. 11 + */ 12 + export default class SchedulerRun extends BaseCommand { 13 + static commandName = 'scheduler:run' 14 + static description = 'Run scheduled jobs (every-minute threshold scan)' 15 + static options: CommandOptions = { startApp: true, staysAlive: true } 16 + 17 + async run() { 18 + const job = new Cron('* * * * *', { protect: true }, async () => { 19 + try { 20 + await ThresholdScanJob.dispatch({}) 21 + } catch (error) { 22 + this.logger.error( 23 + `scheduler: threshold scan dispatch failed: ${ 24 + error instanceof Error ? error.message : String(error) 25 + }` 26 + ) 27 + } 28 + }) 29 + 30 + this.app.terminating(() => job.stop()) 31 + this.logger.info('scheduler started (threshold scan every 60s)') 32 + } 33 + }
+9 -66
pnpm-lock.yaml
··· 59 59 '@vinejs/vine': 60 60 specifier: ^4.3.0 61 61 version: 4.3.1 62 - adonisjs-scheduler: 63 - specifier: ^2.7.0 64 - version: 2.7.0(@adonisjs/core@7.3.1(@adonisjs/assembler@8.4.0(typescript@6.0.2))(@vinejs/vine@4.3.1)(edge.js@6.5.0)(pino-pretty@13.1.3)(youch@4.1.1)) 65 62 better-sqlite3: 66 63 specifier: ^12.8.0 67 64 version: 12.8.0 65 + croner: 66 + specifier: ^10.0.1 67 + version: 10.0.1 68 68 edge.js: 69 69 specifier: ^6.5.0 70 70 version: 6.5.0 ··· 2307 2307 engines: {node: '>=0.4.0'} 2308 2308 hasBin: true 2309 2309 2310 - adonisjs-scheduler@2.7.0: 2311 - resolution: {integrity: sha512-Twy3MN8X1Xe0Qsx5ab8vwccHRo7Fe2y/X4nY25rvlPnMP2mptiQc/QCYlk3ZW6ZtEgIpsUtbDF5OLqJpy/3wjQ==} 2312 - peerDependencies: 2313 - '@adonisjs/core': ^6.2.0 || ^7.0.0-0 2314 - 2315 2310 agent-base@7.1.4: 2316 2311 resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} 2317 2312 engines: {node: '>= 14'} ··· 2357 2352 astring@1.9.0: 2358 2353 resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} 2359 2354 hasBin: true 2360 - 2361 - async-lock@1.4.1: 2362 - resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} 2363 2355 2364 2356 async-mutex@0.5.0: 2365 2357 resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} ··· 2498 2490 chevrotain@11.2.0: 2499 2491 resolution: {integrity: sha512-mHCHTxM51nCklUw9RzRVc0DLjAh/SAUPM4k/zMInlTIo25ldWXOZoPt7XEIk/LwoT4lFVmJcu9g5MHtx371x3A==} 2500 2492 2501 - chokidar@4.0.3: 2502 - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 2503 - engines: {node: '>= 14.16.0'} 2504 - 2505 2493 chokidar@5.0.0: 2506 2494 resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} 2507 2495 engines: {node: '>= 20.19.0'} ··· 2601 2589 cron-parser@5.5.0: 2602 2590 resolution: {integrity: sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww==} 2603 2591 engines: {node: '>=18'} 2592 + 2593 + croner@10.0.1: 2594 + resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} 2595 + engines: {node: '>=18.0'} 2604 2596 2605 2597 cross-spawn@7.0.6: 2606 2598 resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} ··· 2752 2744 resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} 2753 2745 engines: {node: '>=10.0.0'} 2754 2746 2755 - emoji-regex@10.6.0: 2756 - resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} 2757 - 2758 2747 emoji-regex@8.0.0: 2759 2748 resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 2760 2749 ··· 3615 3604 resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} 3616 3605 engines: {node: '>=10'} 3617 3606 3618 - node-cron@3.0.3: 3619 - resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==} 3620 - engines: {node: '>=6.0.0'} 3621 - 3622 3607 node-domexception@1.0.0: 3623 3608 resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} 3624 3609 engines: {node: '>=10.5.0'} ··· 3927 3912 resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} 3928 3913 engines: {node: '>= 6'} 3929 3914 3930 - readdirp@4.1.2: 3931 - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} 3932 - engines: {node: '>= 14.18.0'} 3933 - 3934 3915 readdirp@5.0.0: 3935 3916 resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} 3936 3917 engines: {node: '>= 20.19.0'} ··· 4130 4111 string-width@4.2.3: 4131 4112 resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 4132 4113 engines: {node: '>=8'} 4133 - 4134 - string-width@7.2.0: 4135 - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} 4136 - engines: {node: '>=18'} 4137 4114 4138 4115 string-width@8.2.0: 4139 4116 resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} ··· 4380 4357 util-deprecate@1.0.2: 4381 4358 resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 4382 4359 4383 - uuid@8.3.2: 4384 - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} 4385 - hasBin: true 4386 - 4387 4360 validate-npm-package-license@3.0.4: 4388 4361 resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} 4389 4362 ··· 6799 6772 6800 6773 acorn@8.16.0: {} 6801 6774 6802 - adonisjs-scheduler@2.7.0(@adonisjs/core@7.3.1(@adonisjs/assembler@8.4.0(typescript@6.0.2))(@vinejs/vine@4.3.1)(edge.js@6.5.0)(pino-pretty@13.1.3)(youch@4.1.1)): 6803 - dependencies: 6804 - '@adonisjs/core': 7.3.1(@adonisjs/assembler@8.4.0(typescript@6.0.2))(@vinejs/vine@4.3.1)(edge.js@6.5.0)(pino-pretty@13.1.3)(youch@4.1.1) 6805 - async-lock: 1.4.1 6806 - chokidar: 4.0.3 6807 - cron-parser: 5.5.0 6808 - luxon: 3.7.2 6809 - node-cron: 3.0.3 6810 - string-width: 7.2.0 6811 - 6812 6775 agent-base@7.1.4: {} 6813 6776 6814 6777 ajv@6.14.0: ··· 6841 6804 assertion-error@2.0.1: {} 6842 6805 6843 6806 astring@1.9.0: {} 6844 - 6845 - async-lock@1.4.1: {} 6846 6807 6847 6808 async-mutex@0.5.0: 6848 6809 dependencies: ··· 6965 6926 '@chevrotain/utils': 11.2.0 6966 6927 lodash-es: 4.17.23 6967 6928 6968 - chokidar@4.0.3: 6969 - dependencies: 6970 - readdirp: 4.1.2 6971 - 6972 6929 chokidar@5.0.0: 6973 6930 dependencies: 6974 6931 readdirp: 5.0.0 ··· 7057 7014 dependencies: 7058 7015 luxon: 3.7.2 7059 7016 7017 + croner@10.0.1: {} 7018 + 7060 7019 cross-spawn@7.0.6: 7061 7020 dependencies: 7062 7021 path-key: 3.1.1 ··· 7180 7139 emittery@1.2.1: {} 7181 7140 7182 7141 emoji-regex-xs@2.0.1: {} 7183 - 7184 - emoji-regex@10.6.0: {} 7185 7142 7186 7143 emoji-regex@8.0.0: {} 7187 7144 ··· 7957 7914 dependencies: 7958 7915 semver: 7.7.4 7959 7916 7960 - node-cron@3.0.3: 7961 - dependencies: 7962 - uuid: 8.3.2 7963 - 7964 7917 node-domexception@1.0.0: {} 7965 7918 7966 7919 node-fetch@3.3.2: ··· 8300 8253 inherits: 2.0.4 8301 8254 string_decoder: 1.3.0 8302 8255 util-deprecate: 1.0.2 8303 - 8304 - readdirp@4.1.2: {} 8305 8256 8306 8257 readdirp@5.0.0: {} 8307 8258 ··· 8534 8485 is-fullwidth-code-point: 3.0.0 8535 8486 strip-ansi: 6.0.1 8536 8487 8537 - string-width@7.2.0: 8538 - dependencies: 8539 - emoji-regex: 10.6.0 8540 - get-east-asian-width: 1.5.0 8541 - strip-ansi: 7.2.0 8542 - 8543 8488 string-width@8.2.0: 8544 8489 dependencies: 8545 8490 get-east-asian-width: 1.5.0 ··· 8757 8702 punycode: 2.3.1 8758 8703 8759 8704 util-deprecate@1.0.2: {} 8760 - 8761 - uuid@8.3.2: {} 8762 8705 8763 8706 validate-npm-package-license@3.0.4: 8764 8707 dependencies:
-27
start/scheduler.ts
··· 1 - /* 2 - |-------------------------------------------------------------------------- 3 - | Scheduler 4 - |-------------------------------------------------------------------------- 5 - | 6 - | Scheduled work dispatched by `adonisjs-scheduler`. The scheduler Worker is 7 - | booted inside `node ace queue:work` (see commands/queue_work.ts) so these 8 - | jobs run in the same container as the existing backfill worker. 9 - | 10 - */ 11 - 12 - import scheduler from 'adonisjs-scheduler/services/main' 13 - import ThresholdScanJob from '#jobs/threshold_scan_job' 14 - 15 - /** 16 - * Every 60 seconds, enqueue a ThresholdScanJob so the queue worker picks it 17 - * up and scans `like_counts_daily` for posts crossing the firehose virality 18 - * thresholds. `withoutOverlapping` avoids piling up duplicate schedulings if 19 - * the dispatch itself is slow. 20 - */ 21 - scheduler 22 - .call(async () => { 23 - await ThresholdScanJob.dispatch({}) 24 - }) 25 - .everyMinute() 26 - .withoutOverlapping() 27 - .tag('firehose-threshold-scan')