十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpacetimeDB 调度表(Schedule Tables)完整指南:用 scheduled 列触发定时 Reducer 与 Procedure

SpacetimeDB 调度表(Schedule Tables)完整指南:用 scheduled 列触发定时 Reducer 与 Procedure SpacetimeDB 调度表Schedule Tables完整指南用 scheduled 列触发定时 Reducer 与 Procedure【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB本篇指南以 SpacetimeDB 官方核心概念文档《Schedule Tables》为主体围绕调度表这一把表行与定时执行绑定起来的特殊机制展开如何通过scheduleAt/ScheduleAt列在指定时间或固定间隔触发 reducer 或 procedure并深入仓库源码crates/core/src/host/scheduler.rs、crates/lib/src/scheduler.rs、crates/datastore/src/system_tables.rs等剖析调度队列、interval 补偿、行生命周期等底层实现。读完你将能在 TypeScript、C#、Rust、C 四种服务端语言中完整实现定时提醒、内容过期、周期任务、游戏计时等能力。调度表Schedule Table是 SpacetimeDB 中一类特殊的表只要表中包含一个类型为ScheduleAt的调度列向该表插入的行就会被 SpacetimeDB 的调度器接管在约定的时间点自动调用与之绑定的 reducer 或 procedure。这让发送提醒、过期条目、延迟动作、周期性维护、游戏计时事件等未来动作无需外部定时器即可在数据库内原生于线完成。:::tip 调度 Procedure Procedure 与 reducer 使用完全相同的调度模式TypeScript 中传入onSchedule选项其他语言则在scheduled属性中引用 procedure 名称。当定时任务需要发起 HTTP 请求或执行其他副作用时调度 procedure 尤其有用参见 Scheduling Procedures 中的示例。 :::定义一张调度表定义调度表的核心是两步声明一个带ScheduleAt类型列的表再把这张表与某个 reducer/procedure 建立绑定关系。:::note 为什么代码里叫 scheduled 表属性使用scheduled带 d因为它指的是被调度的 reducer——即将被调度执行的那个函数。表本身叫 schedule table调度表存储调度计划而它触发的 reducer 叫 scheduled reducer被调度的 reducer。 :::TypeScriptonSchedule绑定在 TypeScript 中绑定声明在 reducer 一侧使用onSchedule选项import { schema, table, t } from spacetimedb/server; const reminder table( { name: reminder }, { scheduledId: t.u64().primaryKey().autoInc(), scheduledAt: t.scheduleAt(), message: t.string(), } ); const spacetimedb schema({ reminder }); export default spacetimedb; export const sendReminder spacetimedb.reducer( { onSchedule: reminder }, { arg: reminder.rowType }, (_ctx, { arg }) { // Invoked automatically by the scheduler // arg.message, arg.scheduledAt, arg.scheduledId } );onSchedule将 reducer 注册为这张调度表的目标函数。由于表定义并不反向引用 reducer表与 reducer 可以拆分到不同文件而不会产生循环导入。一张调度表最多只能绑定一个 reducer 或 procedure绑定第二个会在 schema 校验阶段直接报错对应 Rust 宏中的错误提示 can only specify one scheduled reducer or procedure见 crates/bindings-macro/src/table.rs。同样的选项也适用于 procedure前提是 procedure 的返回类型为t.unit()。见 Scheduling Procedures。:::note 遗留的scheduled表选项 旧代码把绑定声明在表上通过一个前向引用的 thunk 指向被调度的 reducerconst reminder table( { name: reminder, scheduled: (): any sendReminder }, { scheduledId: t.u64().primaryKey().autoInc(), scheduledAt: t.scheduleAt(), message: t.string(), } ); export const sendReminder spacetimedb.reducer({ arg: reminder.rowType }, (_ctx, { arg }) { // Invoked automatically by the scheduler });这种写法仍然可用但前向引用迫使表与 reducer 位于同一文件并且破坏了类型推断因此需要(): any 强转。新代码请优先使用onSchedule。 :::C#Scheduled与ScheduledAt属性C# 通过[SpacetimeDB.Table]特性声明调度关系:::tip C# 调度列 在[SpacetimeDB.Table(..., ScheduledAt ...)]中ScheduledAt的值必须与该表上某个字段名完全一致且该字段类型必须是ScheduleAt例如ScheduledAt或scheduled_at。 :::using SpacetimeDB; public static partial class Module { [SpacetimeDB.Table(Accessor Reminder, Scheduled SendReminder, ScheduledAt ScheduledAt)] public partial struct Reminder { [SpacetimeDB.PrimaryKey] [SpacetimeDB.AutoInc] public ulong ScheduledId; public uint UserId; public string Message; public ScheduleAt ScheduledAt; } [SpacetimeDB.Reducer] public static void SendReminder(ReducerContext ctx, Reminder reminder) { // Process the scheduled reminder } }Rustscheduled(send_reminder)表属性Rust 侧使用#[table(accessor ..., scheduled(...))]并借助#[primary_key]#[auto_inc]的scheduled_id: u64与scheduled_at: ScheduleAt列use spacetimedb::{reducer, table, ReducerContext, ScheduleAt, Table}; use std::time::Duration; #[table(accessor reminder_schedule, scheduled(send_reminder))] pub struct Reminder { #[primary_key] #[auto_inc] scheduled_id: u64, user_id: u32, message: String, scheduled_at: ScheduleAt, } #[reducer] fn send_reminder(ctx: ReducerContext, reminder: Reminder) - Result(), String { // Process the scheduled reminder Ok(()) } #[reducer(init)] fn init(ctx: ReducerContext) { ctx.db.reminder_schedule().insert(Reminder { scheduled_id: 0, user_id: 0, message: Game tick.to_string(), scheduled_at: ScheduleAt::Interval(Duration::from_millis(50).into()), }); }从宏实现可以确认调度表必需的列约束#[table]宏会强制要求表中存在#[primary_key] #[auto_inc] scheduled_id: u64与scheduled_at: ScheduleAt若列名不同可用scheduled(my_reducer, at custom_scheduled_at)指定并做编译期类型检查见 crates/bindings-macro/src/table.rs。CSPACETIMEDB_SCHEDULE宏C 服务端模块需要满足模块版本要求通过SPACETIMEDB_SCHEDULE宏声明调度列索引struct Reminder { uint64_t scheduled_id; ScheduleAt scheduled_at; std::string message; }; SPACETIMEDB_STRUCT(Reminder, scheduled_id, scheduled_at, message) SPACETIMEDB_TABLE(Reminder, reminder, Public) FIELD_PrimaryKeyAutoInc(reminder, scheduled_id) SPACETIMEDB_SCHEDULE(reminder, 1, send_reminder) // Column 1 is scheduled_at // Reducer invoked automatically by the scheduler SPACETIMEDB_REDUCER(send_reminder, ReducerContext ctx, Reminder arg) { // Invoked automatically by the scheduler // arg.message, arg.scheduled_at, arg.scheduled_id LOG_INFO(Scheduled reminder: arg.message); return Ok(); }底层ScheduleAt是一个特殊求和类型ScheduleAt并非普通的内建类型它在 crates/lib/src/scheduler.rs 中被定义为二变体枚举Interval(TimeDuration)以固定时间间隔重复调度TimeDuration支持纳秒级精度Time(Timestamp)在某个绝对时间点一次性调度。其代数类型是一个含Intervaltime_duration与Timetimestamp两个变体的 sum 类型crates/lib/src/scheduler.rs并实现了FromTimeDuration、Fromstd::time::Duration、Fromstd::time::SystemTime、FromTimestamp等多组转换。TypeScript 侧的对应实现位于 crates/bindings-typescript/src/lib/schedule_at.ts提供ScheduleAt.interval(micros)与ScheduleAt.time(microsSinceUnixEpoch)两个工厂函数时间单位均为微秒。插入调度计划向调度表插入带scheduled_at值的行即可安排一次动作。调度计划分两种按间隔At intervals固定时间间隔重复执行例如每 5 秒一次按具体时间At specific times在绝对时间戳执行一次。间隔补偿语义间隔调度锚定在本应执行的时间点上。在当前实现中如果数据库繁忙或离线过久错过了若干个间隔 tickSpacetimeDB 会直接调度下一个未来 tick而不会把错过的 tick 补跑也不会让间隔因延迟执行而漂移。这一点在源码中有对应的单元测试验证next_interval_tick_skips_missed_ticks错过 3.5 个间隔后跳到第 4 个 tick、next_interval_tick_is_strictly_after_now_on_boundary恰好在边界时严格取下一个 tick见 crates/core/src/host/scheduler.rs。按间隔调度间隔适合游戏 tick、心跳、周期性维护等重复任务:::important TypeScriptScheduleAt的导入位置ScheduleAt从spacetimedb导入不是spacetimedb/server。请使用import { ScheduleAt } from spacetimedb;:::import { ScheduleAt } from spacetimedb; import { schema } from spacetimedb/server; const spacetimedb schema({ reminder }); // reminder table defined above export default spacetimedb; export const schedulePeriodicTasks spacetimedb.reducer((ctx) { // Schedule to run every 5 seconds (5,000,000 microseconds) ctx.db.reminder.insert({ scheduledId: 0n, scheduledAt: ScheduleAt.interval(5_000_000n), message: Check for updates, }); // Schedule to run every 100 milliseconds ctx.db.reminder.insert({ scheduledId: 0n, scheduledAt: ScheduleAt.interval(100_000n), // 100ms in microseconds message: Game tick, }); });public static partial class Module { [SpacetimeDB.Reducer] public static void SchedulePeriodicTasks(ReducerContext ctx) { // Schedule to run every 5 seconds ctx.Db.Reminder.Insert(new Reminder { ScheduledId 0, Message Check for updates, ScheduledAt new ScheduleAt.Interval(TimeSpan.FromSeconds(5)) }); // Schedule to run every 100 milliseconds ctx.Db.Reminder.Insert(new Reminder { ScheduledId 0, Message Game tick, ScheduledAt new ScheduleAt.Interval(TimeSpan.FromMilliseconds(100)) }); } }use spacetimedb::{ScheduleAt, ReducerContext, Table}; use std::time::Duration; #[spacetimedb::reducer] fn schedule_periodic_tasks(ctx: ReducerContext) { // Schedule to run every 5 seconds ctx.db.reminder().insert(Reminder { scheduled_id: 0, message: Check for updates.to_string(), scheduled_at: ScheduleAt::Interval(Duration::from_secs(5).into()), }); // Schedule to run every 100 milliseconds ctx.db.reminder().insert(Reminder { scheduled_id: 0, message: Game tick.to_string(), scheduled_at: ScheduleAt::Interval(Duration::from_millis(100).into()), }); }// Schedule to run every 5 seconds ctx.db[reminder].insert(Reminder{ 0, ScheduleAt(TimeDuration::from_seconds(5)), Check for updates }); // Schedule to run every 100 milliseconds ctx.db[reminder].insert(Reminder{ 0, ScheduleAt(TimeDuration::from_millis(100)), Game tick });调度时长的上限由于调度队列基于tokio_util::time::DelayQueue内部最大延迟约 64^6 − 1 毫秒 ≈ 2.18 年SpacetimeDB 在 crates/core/src/host/scheduler.rs 中定义了MAX_SCHEDULE_DELAY常量并在Scheduler::schedule中先校验再入队超过上限会返回ScheduleError::DelayTooLong避免用户通过一次过长的调度让整个调度器 paniccrates/core/src/host/scheduler.rs。按具体时间调度具体时间适合一次性动作例如在特定时刻发送提醒或让内容过期import { ScheduleAt } from spacetimedb; import { schema } from spacetimedb/server; const spacetimedb schema({ reminder }); // reminder table defined above export default spacetimedb; export const scheduleTimedTasks spacetimedb.reducer((ctx) { // Schedule for 10 seconds from now const tenSecondsFromNow ctx.timestamp.microsSinceUnixEpoch 10_000_000n; ctx.db.reminder.insert({ scheduledId: 0n, scheduledAt: ScheduleAt.time(tenSecondsFromNow), message: Your auction has ended, }); // Schedule for a specific Unix timestamp (microseconds since epoch) const targetTime 1735689600_000_000n; // Jan 1, 2025 00:00:00 UTC ctx.db.reminder.insert({ scheduledId: 0n, scheduledAt: ScheduleAt.time(targetTime), message: Happy New Year!, }); });using SpacetimeDB; public static partial class Module { [SpacetimeDB.Reducer] public static void ScheduleTimedTasks(ReducerContext ctx) { // Schedule for 10 seconds from now var tenSecondsFromNow ctx.Timestamp new TimeDuration(10_000_000); ctx.Db.Reminder.Insert(new Reminder { ScheduledId 0, Message Your auction has ended, ScheduledAt new ScheduleAt.Time(tenSecondsFromNow) }); // Schedule for a specific time var targetTime new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); ctx.Db.Reminder.Insert(new Reminder { ScheduledId 0, Message Happy New Year!, ScheduledAt new ScheduleAt.Time(targetTime) }); } }use spacetimedb::{ScheduleAt, ReducerContext, Table}; use std::time::Duration; #[spacetimedb::reducer] fn schedule_timed_tasks(ctx: ReducerContext) { // Schedule for 10 seconds from now let ten_seconds_from_now ctx.timestamp Duration::from_secs(10); ctx.db.reminder().insert(Reminder { scheduled_id: 0, message: Your auction has ended.to_string(), scheduled_at: ScheduleAt::Time(ten_seconds_from_now), }); // Schedule for immediate execution (current timestamp) ctx.db.reminder().insert(Reminder { scheduled_id: 0, message: Process now.to_string(), scheduled_at: ScheduleAt::Time(ctx.timestamp.clone()), }); }// Schedule for 10 seconds from now Timestamp tenSecondsFromNow ctx.timestamp TimeDuration::from_seconds(10); ctx.db[reminder].insert(Reminder{ 0, ScheduleAt(tenSecondsFromNow), Your auction has ended }); // Schedule for immediate execution (current timestamp) ctx.db[reminder].insert(Reminder{ 0, ScheduleAt(ctx.timestamp), Process now });调度的工作原理调度执行的完整流程可以概括为四步插入一行包含ScheduleAt值的记录SpacetimeDB 监控该调度表时间到达时指定的 reducer/procedure 被自动调用整行记录作为参数传入处理完成后该行通常会被删除或更新由 reducer 自行决定。调度器内部机制从 crates/core/src/host/scheduler.rs 的实现看整个调度体系包含三块关键结构系统表st_scheduled在 crates/datastore/src/system_tables.rs 中定义字段包括schedule_id、table_id、reducer_name命名空间后的名字如子模块表lib.library_scheduled_procedure、schedule_name与at_column用于登记哪张表由哪个函数调度。调度器启动时遍历该表把每张调度表中的现有行全部加载进内存队列crates/core/src/host/scheduler.rs。Scheduler与SchedulerActor前者向数据库/模块宿主提供schedule接口后者运行一个基于tokio::select!的事件循环同时监听调度消息与DelayQueue到期事件crates/core/src/host/scheduler.rs。当延迟队列中的某项到期actor 会根据函数名在模块定义中判定它是 reducer 还是 procedure分别走call_scheduled_reducer/call_scheduled_procedurecrates/core/src/host/scheduler.rs。行更新重排如果在处理前调度行被更新handle_message会先从DelayQueue移除旧 key 再插入新 key保证队列与数据库中的计划始终一致crates/core/src/host/scheduler.rs。此外调度器会记录调度函数延迟指标当实际执行时间比计划时间晚超过 30msSCHEDULED_FUNCTION_DELAY_WARNING_THRESHOLD时输出警告日志crates/core/src/host/scheduler.rs。行的生命周期SpacetimeDB 会把调度行整体作为参数传给被调度的 reducer 或 procedure但一次性one-shot调度行的删除时机因函数类型而异被调度的 procedure在执行前删除该行因此在 procedure 内部schedule_table.find(scheduled_id)会返回null调用.update()也会失败。这一点对应源码prepare_scheduled_procedure_call中的注释——scheduled procedures 不允许中途中止后重试所以必须在执行前移除调度行crates/core/src/host/scheduler.rs。被调度的 reducer在执行后删除该行因此 reducer 运行期间该行在调度表中仍然可见。间隔调度永远不会被自动删除只有一次性调度在运行后被移除。interval 行执行后由delete_scheduled_function_row读取其ScheduleAt::Interval并计算下一个 tick 返回给调度器重新入队crates/core/src/host/scheduler.rs。间隔补偿错过即跳过next_interval_tick_aftercrates/core/src/host/scheduler.rs实现间隔补偿以上一次本应执行的时刻 间隔为锚点计算自锚点至今已经过去的 tick 数然后直接跳到下一个未来的 tick 边界从而避免补跑堆积的错过的 tick避免雪崩以实际执行时刻为起点导致间隔随时间漂移。典型使用场景提醒与通知在特定时间点发送定时消息内容过期自动移除或归档旧数据延迟动作把动作排队延迟一段时间后执行周期任务周期性地执行维护或清理操作游戏机制基于计时的玩法事件建筑完成、能量恢复等。下一步学习 Reducers理解如何编写被调度的动作处理函数探索 Procedures了解可执行副作用如 HTTP 请求的调度执行模式。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表