flora is a fast and secure runtime that lets you write discord bots for your servers, with a rich TypeScript SDK, without worrying about running infrastructure. [mirror]
1
fork

Configure Feed

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

feat: `skipIfRunning` config to skip execution

+72 -9
+8
apps/runtime/src/ops/cron.rs
··· 14 14 pub event_name: String, 15 15 pub schedule: Cron, 16 16 pub next_run: DateTime<Utc>, 17 + pub skip_if_running: bool, 18 + pub is_running: bool, 17 19 } 18 20 19 21 impl Clone for CronJob { ··· 24 26 event_name: self.event_name.clone(), 25 27 schedule: Cron::from_str(&self.schedule.pattern.to_string()).unwrap(), 26 28 next_run: self.next_run, 29 + skip_if_running: self.skip_if_running, 30 + is_running: self.is_running, 27 31 } 28 32 } 29 33 } ··· 58 62 struct RegisterCronArgs { 59 63 name: String, 60 64 expr: String, 65 + #[serde(default)] 66 + skip_if_running: bool, 61 67 } 62 68 63 69 #[op2] ··· 104 110 event_name: format!("{}{}", CRON_EVENT_PREFIX, args.name), 105 111 schedule, 106 112 next_run, 113 + skip_if_running: args.skip_if_running, 114 + is_running: false, 107 115 }); 108 116 109 117 Ok(())
+29
apps/runtime/src/runtime.rs
··· 539 539 for (_gid, jobs) in reg.jobs.iter_mut() { 540 540 for job in jobs.iter_mut() { 541 541 if job.next_run <= now { 542 + if job.skip_if_running && job.is_running { 543 + info!(target: "flora:runtime", worker_id, name = job.name, "skipping cron job (still running)"); 544 + if let Ok(next) = job.schedule.find_next_occurrence(&now, false) { 545 + job.next_run = next; 546 + } 547 + continue; 548 + } 549 + job.is_running = true; 542 550 due_jobs.push(( 543 551 job.guild_id.clone(), 544 552 job.event_name.clone(), ··· 564 572 }; 565 573 566 574 let Some(runtime) = runtime else { 575 + mark_cron_not_running(cron_registry, &guild_id, &cron_name); 567 576 continue; 568 577 }; 569 578 570 579 let result = 571 580 dispatch_cron_into_runtime(runtime, event_name.clone(), payload, worker_id, limits) 572 581 .await; 582 + 583 + mark_cron_not_running(cron_registry, &guild_id, &cron_name); 584 + 573 585 if let Err(ref err) = result { 574 586 error!(target: "flora:runtime", worker_id, ?guild_id, cron_name, ?err, "cron dispatch failed"); 587 + } 588 + } 589 + } 590 + 591 + fn mark_cron_not_running( 592 + cron_registry: &SharedCronRegistry, 593 + guild_id: &Option<String>, 594 + cron_name: &str, 595 + ) { 596 + let mut reg = cron_registry.lock(); 597 + let Some(jobs) = reg.jobs.get_mut(guild_id) else { 598 + return; 599 + }; 600 + for job in jobs.iter_mut() { 601 + if job.name == cron_name { 602 + job.is_running = false; 603 + break; 575 604 } 576 605 } 577 606 }
+11
apps/www/sdk.md
··· 210 210 - `name`: The cron job name you specified 211 211 - `scheduledAt`: ISO 8601 timestamp of when the job was scheduled to run 212 212 213 + ### Options 214 + 215 + ```ts 216 + // Skip execution if previous run is still active 217 + cron('long-task', '*/5 * * * *', async (ctx) => { 218 + await someLongRunningTask() 219 + }, { skipIfRunning: true }) 220 + ``` 221 + 222 + - `skipIfRunning`: If `true`, the job won't start a new execution if the previous one is still running. Default: `false`. 223 + 213 224 Cron expressions follow standard 5-field format: `minute hour day-of-month month day-of-week`. 214 225 215 226 Limits:
+2 -2
lefthook.yml
··· 1 1 pre-commit: 2 2 parallel: true 3 - stage_fixed: true 4 3 commands: 5 4 rustfmt: 6 5 glob: '*.rs' 7 6 run: cargo fmt 8 - 7 + stage_fixed: true 9 8 dprint: 10 9 run: bun format 10 + stage_fixed: true
+3 -2
runtime-dist/runtime_prelude.js
··· 36 36 }) 37 37 } 38 38 const CRON_EVENT_PREFIX = '__cron:' 39 - globalThis.cron = function cron(name, cronExpr, handler) { 39 + globalThis.cron = function cron(name, cronExpr, handler, options) { 40 40 if (typeof name !== 'string' || !name.length) { 41 41 throw new TypeError('cron name must be a non-empty string') 42 42 } ··· 53 53 globalThis.__floraHandlers[eventName].push(handler) 54 54 core.ops.op_register_cron({ 55 55 name, 56 - expr: cronExpr 56 + expr: cronExpr, 57 + skipIfRunning: options?.skipIfRunning ?? false 57 58 }) 58 59 } 59 60 function normalizeReply(message, payload) {
+6 -1
sdk/global-types.d.ts
··· 71 71 scheduledAt: string 72 72 } 73 73 74 + interface CronOptions { 75 + skipIfRunning?: boolean | undefined 76 + } 77 + 74 78 type CronHandler = (ctx: CronContext) => void | Promise<void> 75 79 76 80 var __floraHandlers: { [x: string]: Array<Function> } ··· 89 93 function cron( 90 94 name: string, 91 95 cronExpr: string, 92 - handler: (ctx: CronContext) => void | Promise<void> 96 + handler: (ctx: CronContext) => void | Promise<void>, 97 + options?: CronOptions | undefined 93 98 ): void 94 99 95 100 // SDK exports (functions, consts, classes, types)
+13 -4
sdk/src/runtime/index.ts
··· 39 39 scheduledAt: string 40 40 } 41 41 42 + export interface CronOptions { 43 + skipIfRunning?: boolean 44 + } 45 + 42 46 export type CronHandler = (ctx: CronContext) => void | Promise<void> 43 47 44 48 declare global { ··· 47 51 function on<E extends keyof FloraEventMap>(event: E, handler: FloraEventHandler<E>): void 48 52 function __floraDispatch(event: string, payload: unknown): Promise<void> 49 53 function registerSlashCommands(commands: FlattenedSlashCommand[]): Promise<void> | undefined 50 - function cron(name: string, cronExpr: string, handler: CronHandler): void 54 + function cron(name: string, cronExpr: string, handler: CronHandler, options?: CronOptions): void 51 55 } 52 56 53 57 declare const Deno: { ··· 60 64 op_upsert_guild_commands( 61 65 options: { guildId: string; commands: FlattenedSlashCommand[] } 62 66 ): Promise<void> 63 - op_register_cron(options: { name: string; expr: string }): void 67 + op_register_cron(options: { name: string; expr: string; skipIfRunning?: boolean }): void 64 68 } 65 69 } 66 70 } ··· 132 136 globalThis.cron = function cron( 133 137 name: string, 134 138 cronExpr: string, 135 - handler: CronHandler 139 + handler: CronHandler, 140 + options?: CronOptions 136 141 ): void { 137 142 if (typeof name !== 'string' || !name.length) { 138 143 throw new TypeError('cron name must be a non-empty string') ··· 151 156 } 152 157 globalThis.__floraHandlers[eventName].push(handler) 153 158 154 - core.ops.op_register_cron({ name, expr: cronExpr }) 159 + core.ops.op_register_cron({ 160 + name, 161 + expr: cronExpr, 162 + skipIfRunning: options?.skipIfRunning ?? false 163 + }) 155 164 } 156 165 157 166 function normalizeReply(