perf: read gallery hot paths from spacetime cache
This commit is contained in:
@@ -16,6 +16,15 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-05-16 公开作品列表短期由 BFF 订阅读模型缓存
|
||||||
|
|
||||||
|
- 背景:作品列表压测和实时性讨论中,曾考虑让浏览器前端直接订阅公开作品列表,减少 HTTP 拉取和 BFF 压力。
|
||||||
|
- 决策:本轮不直接把作品列表整体交给前端订阅。短期继续由 `api-server` / BFF 通过 `spacetime-client` 长期订阅 SpacetimeDB 公开 read model 并读取本地 cache,维持首屏、排序、字段归一、权限降级和 HTTP fallback。中期可以新增或统一稳定的专用公开作品列表 read model,例如 `public_work_gallery_entry`,作为前端可选直连订阅对象。
|
||||||
|
- 边界:前端不得直接订阅 `puzzle_work_profile`、`custom_world_profile` 等领域源表,也不得在前端自行 join、聚合或执行公开权限逻辑;这些逻辑必须先沉到后端投影 / read model。
|
||||||
|
- 影响范围:发现页、推荐流、各玩法公开广场、`api-server` 公开列表缓存、SpacetimeDB public view / public 读模型设计。
|
||||||
|
- 验证方式:新增公开作品列表订阅能力时,检查前端只消费专用 public read model 或 BFF HTTP DTO;检查源表 row shape、权限判断和跨玩法聚合没有下沉到前端页面。
|
||||||
|
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||||
|
|
||||||
## 2026-05-16 api-server OpenTelemetry 统一补齐 traces metrics logs
|
## 2026-05-16 api-server OpenTelemetry 统一补齐 traces metrics logs
|
||||||
|
|
||||||
- 背景:压测与运行观测需要把 HTTP、SpacetimeDB 调用和应用日志串起来,同时保留本地 `journalctl` / 文件日志做故障排障。
|
- 背景:压测与运行观测需要把 HTTP、SpacetimeDB 调用和应用日志串起来,同时保留本地 `journalctl` / 文件日志做故障排障。
|
||||||
|
|||||||
@@ -99,6 +99,14 @@
|
|||||||
- 验证:搜索 `server-rs/crates/spacetime-client/src/puzzle.rs` 不应再出现 gallery 主路径调用 `list_puzzle_gallery_then`;执行 `cargo check --manifest-path server-rs/Cargo.toml -p spacetime-client`、`cargo check --manifest-path server-rs/Cargo.toml -p api-server` 和 schema/runtime access 检查。
|
- 验证:搜索 `server-rs/crates/spacetime-client/src/puzzle.rs` 不应再出现 gallery 主路径调用 `list_puzzle_gallery_then`;执行 `cargo check --manifest-path server-rs/Cargo.toml -p spacetime-client`、`cargo check --manifest-path server-rs/Cargo.toml -p api-server` 和 schema/runtime access 检查。
|
||||||
- 关联:`server-rs/crates/spacetime-module/src/puzzle.rs`、`server-rs/crates/spacetime-client/src/lib.rs`、`server-rs/crates/spacetime-client/src/puzzle.rs`、`/api/runtime/puzzle/gallery`。
|
- 关联:`server-rs/crates/spacetime-module/src/puzzle.rs`、`server-rs/crates/spacetime-client/src/lib.rs`、`server-rs/crates/spacetime-client/src/puzzle.rs`、`/api/runtime/puzzle/gallery`。
|
||||||
|
|
||||||
|
## 自定义世界广场和创作入口配置不要每次 HTTP 请求调用只读 procedure
|
||||||
|
|
||||||
|
- 现象:`/api/runtime/custom-world-gallery` 每次请求调用 `list_custom_world_gallery_entries` procedure;入口熔断中间件每个玩法请求调用 `get_creation_entry_config` procedure,50RPS 以上会把 SpacetimeDB procedure 调用变成热点。
|
||||||
|
- 原因:`custom_world_gallery_entry`、`creation_entry_config` 和 `creation_entry_type_config` 已经是可订阅读模型或配置表,但 HTTP 路径仍按“请求到来再查 procedure”处理。
|
||||||
|
- 处理:`spacetime-client` 长连接订阅 `custom_world_gallery_entry`、`public_work_play_daily_stat` 的 `custom-world` 桶、`creation_entry_config` 和 `creation_entry_type_config`;custom-world gallery 从本地 cache 排序并聚合 7 日播放数;入口配置优先读订阅 cache,cache 缺失时用最近一次成功内存快照,再兜底调用 `get_creation_entry_config` 完成旧库兼容。旧 `list_custom_world_gallery_entries` procedure 只允许作为旧库缺少 gallery 行时的一次性同步兜底。
|
||||||
|
- 验证:搜索 `server-rs/crates/spacetime-client/src/custom_world.rs`,gallery 主路径应是 `read_after_connect` 读取 `custom_world_gallery_entry()`;搜索 `server-rs/crates/spacetime-client/src/runtime.rs`,`get_creation_entry_config` 应优先读取 `creation_entry_config()` 和 `creation_entry_type_config()`。执行 `cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`。
|
||||||
|
- 关联:`server-rs/crates/spacetime-client/src/lib.rs`、`server-rs/crates/spacetime-client/src/custom_world.rs`、`server-rs/crates/spacetime-client/src/runtime.rs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||||
|
|
||||||
## 陶泥儿 logo 生图慢请求先缩短 prompt 并单张串行
|
## 陶泥儿 logo 生图慢请求先缩短 prompt 并单张串行
|
||||||
|
|
||||||
- 现象:使用 VectorEngine `gpt-image-2-all` 生成陶泥儿 logo 概念图时,部分 prompt 会超过 10 分钟仍无响应,或返回 `429` / `当前分组上游负载已饱和`;同一批次里后续图片会被前面的慢请求拖住。
|
- 现象:使用 VectorEngine `gpt-image-2-all` 生成陶泥儿 logo 概念图时,部分 prompt 会超过 10 分钟仍无响应,或返回 `429` / `当前分组上游负载已饱和`;同一批次里后续图片会被前面的慢请求拖住。
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ npm run check:server-rs-ddd
|
|||||||
3. 删除字段、改名、重排字段、改类型或修改字段属性前,必须先询问用户并确认迁移计划。
|
3. 删除字段、改名、重排字段、改类型或修改字段属性前,必须先询问用户并确认迁移计划。
|
||||||
4. Vec 字段不要直接写无法 const 求值的 default;需要默认空集合时优先使用 `Option<Vec<T>>` 加 `#[default(None::<Vec<T>>)]`,业务层归一为空数组。
|
4. Vec 字段不要直接写无法 const 求值的 default;需要默认空集合时优先使用 `Option<Vec<T>>` 加 `#[default(None::<Vec<T>>)]`,业务层归一为空数组。
|
||||||
5. 运行态读表必须按已声明索引访问。只要 table 上存在覆盖查询前缀的 `#[index(...)]` 或主键 / unique accessor,列表、详情、快照组装和计数都先用对应 accessor `.filter(...)` / `.find(...)`,再在内存中处理索引无法覆盖的残余条件;不得用 `.iter().filter(...)` 扫整表替代现成索引。
|
5. 运行态读表必须按已声明索引访问。只要 table 上存在覆盖查询前缀的 `#[index(...)]` 或主键 / unique accessor,列表、详情、快照组装和计数都先用对应 accessor `.filter(...)` / `.find(...)`,再在内存中处理索引无法覆盖的残余条件;不得用 `.iter().filter(...)` 扫整表替代现成索引。
|
||||||
6. 面向公开列表的只读投影优先做成 public view,并由 `api-server` 的 `spacetime-client` 长期订阅后读本地 cache。不要让 HTTP 列表接口每次请求都调用 procedure 重新组装全量列表;需要请求时间窗口的轻量统计可订阅公开统计表后在 `api-server` 本地聚合,需要写入副作用的详情、点赞、游玩记录仍可走 procedure / reducer。
|
6. 面向公开列表的只读投影优先做成 public view / public 读模型表,并由 `api-server` 的 `spacetime-client` 长期订阅后读本地 cache。短期不把作品列表整体交给浏览器前端直接订阅;不要让 HTTP 列表接口每次请求都调用 procedure 重新组装全量列表。需要请求时间窗口的轻量统计可订阅公开统计表后在 `api-server` 本地聚合,需要写入副作用的详情、点赞、游玩记录仍可走 procedure / reducer。中期如要让前端可选直连订阅,只能新增或统一稳定的专用 public read model,例如 `public_work_gallery_entry`,并保持字段、排序键、公开权限和降级语义由后端投影定义;前端不得直接订阅 `puzzle_work_profile`、`custom_world_profile` 等领域源表,也不得自己做 join、聚合或权限逻辑。首屏、排序、字段归一、权限降级和 HTTP fallback 仍由 `api-server` BFF 维持。
|
||||||
7. 多列索引按 SpacetimeDB 绑定生成的元组参数直接传入,例如 `.filter((source_type, profile_id, played_day))`;前缀查询只传前缀元组,例如 `.filter((scope_kind, scope_id.as_str()))`。不要为了绕过类型问题退回整表遍历。
|
7. 多列索引按 SpacetimeDB 绑定生成的元组参数直接传入,例如 `.filter((source_type, profile_id, played_day))`;前缀查询只传前缀元组,例如 `.filter((scope_kind, scope_id.as_str()))`。不要为了绕过类型问题退回整表遍历。
|
||||||
8. procedure result 必须返回 typed snapshot / typed value。`spacetime-client` mapper 不得再通过 `row_json/session_json/work_json/items_json/run_json/event_json/feedback_json: Option<String>` 做跨层 JSON 字符串传输,也不得在 mapper 里反序列化旧 `*JsonRecord` 兼容结构。业务内部持久化字段如 `profile_payload_json`、`levels_json` 等不属于 procedure result 载荷例外,仍按各自表契约处理。
|
8. procedure result 必须返回 typed snapshot / typed value。`spacetime-client` mapper 不得再通过 `row_json/session_json/work_json/items_json/run_json/event_json/feedback_json: Option<String>` 做跨层 JSON 字符串传输,也不得在 mapper 里反序列化旧 `*JsonRecord` 兼容结构。业务内部持久化字段如 `profile_payload_json`、`levels_json` 等不属于 procedure result 载荷例外,仍按各自表契约处理。
|
||||||
9. 修改后运行:
|
9. 修改后运行:
|
||||||
@@ -293,6 +293,7 @@ npm run check:server-rs-ddd
|
|||||||
|
|
||||||
- Rust 结构体:`CustomWorldGalleryEntry`
|
- Rust 结构体:`CustomWorldGalleryEntry`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/custom_world/mod.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/custom_world/mod.rs`
|
||||||
|
- 作用:自定义世界公开作品列表读模型。`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM custom_world_gallery_entry` 与 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'custom-world'`,`/api/runtime/custom-world-gallery` 从本地 cache 排序并聚合 `recentPlayCount7d`,不再每个 HTTP 请求调用 `list_custom_world_gallery_entries` procedure。旧 procedure 只用于兼容旧库缺少 gallery 读模型行时的一次性同步兜底。
|
||||||
|
|
||||||
### `custom_world_profile`
|
### `custom_world_profile`
|
||||||
|
|
||||||
@@ -465,13 +466,31 @@ npm run check:server-rs-ddd
|
|||||||
- Rust 结构体:`PuzzleWorkProfileRow`
|
- Rust 结构体:`PuzzleWorkProfileRow`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/puzzle.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/puzzle.rs`
|
||||||
|
|
||||||
### `puzzle_gallery_view`
|
### SpacetimeDB view:`puzzle_gallery_view`
|
||||||
|
|
||||||
- Rust view:`puzzle_gallery_view`
|
- Rust view:`puzzle_gallery_view`
|
||||||
- 返回类型:`Vec<PuzzleWorkProfile>`
|
- 返回类型:`Vec<PuzzleWorkProfile>`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/puzzle.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/puzzle.rs`
|
||||||
- 说明:拼图广场公开列表投影,只暴露 `publication_status = Published` 的作品;`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM puzzle_gallery_view` 与 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'` 后,从本地 cache 构造 `/api/runtime/puzzle/gallery` 响应,并在本地按当前请求时间聚合 `recentPlayCount7d`,不再每个 HTTP 请求调用 `list_puzzle_gallery` procedure。
|
- 说明:拼图广场公开列表投影,只暴露 `publication_status = Published` 的作品;`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM puzzle_gallery_view` 与 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'` 后,从本地 cache 构造 `/api/runtime/puzzle/gallery` 响应,并在本地按当前请求时间聚合 `recentPlayCount7d`,不再每个 HTTP 请求调用 `list_puzzle_gallery` procedure。
|
||||||
|
|
||||||
|
### api-server 长期订阅读模型
|
||||||
|
|
||||||
|
`spacetime-client` 建立每个池连接时会等待下列订阅初始同步:
|
||||||
|
|
||||||
|
- `SELECT * FROM puzzle_gallery_view`
|
||||||
|
- `SELECT * FROM custom_world_gallery_entry`
|
||||||
|
|
||||||
|
下列订阅用于统计或配置缓存,订阅失败不会让公开列表连接整体不可用,调用方保留兼容兜底:
|
||||||
|
|
||||||
|
- `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'`
|
||||||
|
- `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'custom-world'`
|
||||||
|
- `SELECT * FROM creation_entry_config`
|
||||||
|
- `SELECT * FROM creation_entry_type_config`
|
||||||
|
|
||||||
|
`GET /api/creation-entry/config` 和入口熔断优先从订阅 cache 读取创作入口配置;cache 缺失时使用最近一次成功读取的内存快照,再兜底调用 `get_creation_entry_config` procedure 完成空库种子或旧库兼容。
|
||||||
|
|
||||||
|
未来可选:若发现页、推荐流和各玩法广场需要统一给浏览器前端直接订阅公开作品列表,只新增 / 统一专用 public read model,例如 `public_work_gallery_entry`。该 read model 必须是后端投影后的公开作品卡片契约,覆盖作品类型、公开作品号、标题、摘要、封面、作者展示名、排序键、公开统计和入口开关后的可见性,不暴露玩法领域源表 row shape。前端可选择订阅这个稳定投影来减少 HTTP 拉取,但不能订阅 `puzzle_work_profile`、`custom_world_profile` 等源表后自行拼装列表;BFF 仍保留首屏、SEO / 分享、旧客户端、订阅失败和灰度期间的 HTTP fallback。
|
||||||
|
|
||||||
### `quest_log`
|
### `quest_log`
|
||||||
|
|
||||||
- Rust 结构体:`QuestLog`
|
- Rust 结构体:`QuestLog`
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ Jenkins 按 web / api / Spacetime module / build / deploy / publish 拆分
|
|||||||
- `genarrative-api.service` 设置 `LimitNOFILE=65535`、`TasksMax=2048`;上线后用 `systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax` 和 `cat /proc/$(pidof api-server)/limits` 核对。
|
- `genarrative-api.service` 设置 `LimitNOFILE=65535`、`TasksMax=2048`;上线后用 `systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax` 和 `cat /proc/$(pidof api-server)/limits` 核对。
|
||||||
- Nginx `/api/` 与 `/admin/api/` 通过 `genarrative_api` upstream 代理到 `127.0.0.1:8082`,upstream keepalive 为 64;压测时看 `/var/log/nginx/genarrative.access.log` 中的 `request_time`、`upstream_connect_time`、`upstream_header_time`、`upstream_response_time`、`upstream_status`、`request_id`。
|
- Nginx `/api/` 与 `/admin/api/` 通过 `genarrative_api` upstream 代理到 `127.0.0.1:8082`,upstream keepalive 为 64;压测时看 `/var/log/nginx/genarrative.access.log` 中的 `request_time`、`upstream_connect_time`、`upstream_header_time`、`upstream_response_time`、`upstream_status`、`request_id`。
|
||||||
- 作品列表 K6 脚本一次 iteration 默认请求两个公开接口,因此约 50 HTTP req/s 的目标命令使用 `SCENARIO=spike START_RPS=5 PEAK_RPS=25 HOLD=60s END_RPS=5 DETAIL_RATIO=0 npm run loadtest:k6:works`。
|
- 作品列表 K6 脚本一次 iteration 默认请求两个公开接口,因此约 50 HTTP req/s 的目标命令使用 `SCENARIO=spike START_RPS=5 PEAK_RPS=25 HOLD=60s END_RPS=5 DETAIL_RATIO=0 npm run loadtest:k6:works`。
|
||||||
|
- 作品列表短期继续由 `api-server` / BFF 订阅 SpacetimeDB 公开 read model 后读本地 cache,不让浏览器前端直接订阅完整列表;未来如新增 `public_work_gallery_entry` 等专用公开作品列表 read model,前端只可订阅该稳定投影,不能订阅玩法源表后自行 join、聚合或判断权限。
|
||||||
- 50 HTTP req/s 验收目标为 `http_req_failed < 1%`、`p95 < 2s`、`dropped_iterations = 0`,同时压测窗口内 Nginx 无新增 502。
|
- 50 HTTP req/s 验收目标为 `http_req_failed < 1%`、`p95 < 2s`、`dropped_iterations = 0`,同时压测窗口内 Nginx 无新增 502。
|
||||||
|
|
||||||
OpenTelemetry 现阶段可选 OTLP traces / metrics / logs,但本地日志与 Nginx 文件日志仍保留:
|
OpenTelemetry 现阶段可选 OTLP traces / metrics / logs,但本地日志与 Nginx 文件日志仍保留:
|
||||||
|
|||||||
@@ -181,6 +181,55 @@ impl SpacetimeClient {
|
|||||||
pub async fn list_custom_world_gallery_entries(
|
pub async fn list_custom_world_gallery_entries(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<CustomWorldGalleryEntryRecord>, SpacetimeClientError> {
|
) -> Result<Vec<CustomWorldGalleryEntryRecord>, SpacetimeClientError> {
|
||||||
|
let records = self.read_custom_world_gallery_entries_from_cache().await?;
|
||||||
|
if !records.is_empty()
|
||||||
|
|| self
|
||||||
|
.custom_world_gallery_legacy_sync_attempted
|
||||||
|
.swap(true, std::sync::atomic::Ordering::SeqCst)
|
||||||
|
{
|
||||||
|
return Ok(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
.sync_custom_world_gallery_entries_via_legacy_procedure()
|
||||||
|
.await;
|
||||||
|
self.read_custom_world_gallery_entries_from_cache().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_custom_world_gallery_entries_from_cache(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<CustomWorldGalleryEntryRecord>, SpacetimeClientError> {
|
||||||
|
self.read_after_connect(move |connection| {
|
||||||
|
let recent_play_counts = public_work_recent_play_counts(connection, "custom-world");
|
||||||
|
let mut entries = connection
|
||||||
|
.db()
|
||||||
|
.custom_world_gallery_entry()
|
||||||
|
.iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
entries.sort_by(|left, right| {
|
||||||
|
right
|
||||||
|
.published_at
|
||||||
|
.cmp(&left.published_at)
|
||||||
|
.then(right.updated_at.cmp(&left.updated_at))
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| {
|
||||||
|
let recent_play_count_7d = recent_play_counts
|
||||||
|
.get(&entry.profile_id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(0);
|
||||||
|
map_custom_world_gallery_entry_row(entry, recent_play_count_7d)
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_custom_world_gallery_entries_via_legacy_procedure(
|
||||||
|
&self,
|
||||||
|
) -> Result<(), SpacetimeClientError> {
|
||||||
self.call_after_connect(
|
self.call_after_connect(
|
||||||
"list_custom_world_gallery_entries",
|
"list_custom_world_gallery_entries",
|
||||||
move |connection, sender| {
|
move |connection, sender| {
|
||||||
@@ -188,8 +237,8 @@ impl SpacetimeClient {
|
|||||||
.procedures()
|
.procedures()
|
||||||
.list_custom_world_gallery_entries_then(move |_, result| {
|
.list_custom_world_gallery_entries_then(move |_, result| {
|
||||||
let mapped = result
|
let mapped = result
|
||||||
.map_err(SpacetimeClientError::from_sdk_error)
|
.map(|_| ())
|
||||||
.and_then(map_custom_world_gallery_list_result);
|
.map_err(SpacetimeClientError::from_sdk_error);
|
||||||
send_once(&sender, mapped);
|
send_once(&sender, mapped);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ pub mod story_runtime;
|
|||||||
pub mod visual_novel;
|
pub mod visual_novel;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
error::Error,
|
error::Error,
|
||||||
fmt,
|
fmt,
|
||||||
sync::atomic::{AtomicBool, Ordering},
|
sync::atomic::{AtomicBool, Ordering},
|
||||||
@@ -225,7 +226,7 @@ use module_story::{
|
|||||||
use shared_kernel::format_timestamp_micros;
|
use shared_kernel::format_timestamp_micros;
|
||||||
use spacetimedb_sdk::{DbContext, Table};
|
use spacetimedb_sdk::{DbContext, Table};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::{OwnedSemaphorePermit, Semaphore, oneshot},
|
sync::{OwnedSemaphorePermit, RwLock, Semaphore, oneshot},
|
||||||
time::timeout,
|
time::timeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -257,6 +258,8 @@ pub struct AuthStoreSnapshotImportRecord {
|
|||||||
pub struct SpacetimeClient {
|
pub struct SpacetimeClient {
|
||||||
config: SpacetimeClientConfig,
|
config: SpacetimeClientConfig,
|
||||||
pool: Arc<SpacetimeConnectionPool>,
|
pool: Arc<SpacetimeConnectionPool>,
|
||||||
|
creation_entry_config_cache: Arc<RwLock<Option<CreationEntryConfigRecord>>>,
|
||||||
|
custom_world_gallery_legacy_sync_attempted: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -269,6 +272,8 @@ pub enum SpacetimeClientError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PROCEDURE_TIMEOUT: Duration = Duration::from_secs(30);
|
const DEFAULT_PROCEDURE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
const PUBLIC_WORK_PLAY_DAY_MICROS: i64 = 86_400_000_000;
|
||||||
|
const PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS: i64 = 7;
|
||||||
|
|
||||||
type ProcedureResultSender<T> =
|
type ProcedureResultSender<T> =
|
||||||
Arc<Mutex<Option<oneshot::Sender<Result<T, SpacetimeClientError>>>>>;
|
Arc<Mutex<Option<oneshot::Sender<Result<T, SpacetimeClientError>>>>>;
|
||||||
@@ -286,7 +291,7 @@ struct PooledConnectionSlot {
|
|||||||
|
|
||||||
struct PooledConnection {
|
struct PooledConnection {
|
||||||
connection: DbConnection,
|
connection: DbConnection,
|
||||||
_gallery_subscription: Vec<SubscriptionHandle>,
|
_read_model_subscriptions: Vec<SubscriptionHandle>,
|
||||||
runner: Option<JoinHandle<()>>,
|
runner: Option<JoinHandle<()>>,
|
||||||
broken: Arc<AtomicBool>,
|
broken: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
@@ -321,7 +326,12 @@ impl SpacetimeClient {
|
|||||||
permits: Arc::new(Semaphore::new(pool_size)),
|
permits: Arc::new(Semaphore::new(pool_size)),
|
||||||
});
|
});
|
||||||
|
|
||||||
Self { config, pool }
|
Self {
|
||||||
|
config,
|
||||||
|
pool,
|
||||||
|
creation_entry_config_cache: Arc::new(RwLock::new(None)),
|
||||||
|
custom_world_gallery_legacy_sync_attempted: Arc::new(AtomicBool::new(false)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn call_after_connect<T>(
|
async fn call_after_connect<T>(
|
||||||
@@ -415,6 +425,14 @@ impl SpacetimeClient {
|
|||||||
final_result
|
final_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn cache_creation_entry_config(&self, config: CreationEntryConfigRecord) {
|
||||||
|
*self.creation_entry_config_cache.write().await = Some(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_cached_creation_entry_config(&self) -> Option<CreationEntryConfigRecord> {
|
||||||
|
self.creation_entry_config_cache.read().await.clone()
|
||||||
|
}
|
||||||
|
|
||||||
async fn acquire_connection(&self) -> Result<PooledConnectionLease, SpacetimeClientError> {
|
async fn acquire_connection(&self) -> Result<PooledConnectionLease, SpacetimeClientError> {
|
||||||
let permit = timeout(
|
let permit = timeout(
|
||||||
self.config.procedure_timeout,
|
self.config.procedure_timeout,
|
||||||
@@ -503,19 +521,19 @@ impl SpacetimeClient {
|
|||||||
.map_err(|_| SpacetimeClientError::Timeout)?
|
.map_err(|_| SpacetimeClientError::Timeout)?
|
||||||
.map_err(|_| SpacetimeClientError::ConnectDropped)??;
|
.map_err(|_| SpacetimeClientError::ConnectDropped)??;
|
||||||
|
|
||||||
let gallery_subscription = self
|
let read_model_subscriptions = self
|
||||||
.subscribe_puzzle_gallery_views(&connection, broken.clone())
|
.subscribe_cached_read_models(&connection, broken.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(PooledConnection {
|
Ok(PooledConnection {
|
||||||
connection,
|
connection,
|
||||||
_gallery_subscription: gallery_subscription,
|
_read_model_subscriptions: read_model_subscriptions,
|
||||||
runner: Some(runner),
|
runner: Some(runner),
|
||||||
broken,
|
broken,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn subscribe_puzzle_gallery_views(
|
async fn subscribe_cached_read_models(
|
||||||
&self,
|
&self,
|
||||||
connection: &DbConnection,
|
connection: &DbConnection,
|
||||||
broken: Arc<AtomicBool>,
|
broken: Arc<AtomicBool>,
|
||||||
@@ -523,37 +541,67 @@ impl SpacetimeClient {
|
|||||||
let mut subscriptions = Vec::new();
|
let mut subscriptions = Vec::new();
|
||||||
for query in [
|
for query in [
|
||||||
"SELECT * FROM puzzle_gallery_view",
|
"SELECT * FROM puzzle_gallery_view",
|
||||||
|
"SELECT * FROM custom_world_gallery_entry",
|
||||||
] {
|
] {
|
||||||
let (sender, receiver) = oneshot::channel::<Result<(), SpacetimeClientError>>();
|
let subscription = self
|
||||||
let applied_sender = Arc::new(Mutex::new(Some(sender)));
|
.subscribe_cached_read_model_query(connection, broken.clone(), query, true)
|
||||||
let on_applied_sender = applied_sender.clone();
|
.await?;
|
||||||
let on_error_sender = applied_sender.clone();
|
|
||||||
let broken_flag = broken.clone();
|
|
||||||
let subscription = connection
|
|
||||||
.subscription_builder()
|
|
||||||
.on_applied(move |_| {
|
|
||||||
send_connect_once(&on_applied_sender, Ok(()));
|
|
||||||
})
|
|
||||||
.on_error(move |_, error| {
|
|
||||||
broken_flag.store(true, Ordering::SeqCst);
|
|
||||||
send_connect_once(
|
|
||||||
&on_error_sender,
|
|
||||||
Err(SpacetimeClientError::Procedure(error.to_string())),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.subscribe(query);
|
|
||||||
|
|
||||||
timeout(self.config.procedure_timeout, receiver)
|
|
||||||
.await
|
|
||||||
.map_err(|_| SpacetimeClientError::Timeout)?
|
|
||||||
.map_err(|_| SpacetimeClientError::ConnectDropped)??;
|
|
||||||
|
|
||||||
subscriptions.push(subscription);
|
subscriptions.push(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for query in [
|
||||||
|
"SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'",
|
||||||
|
"SELECT * FROM public_work_play_daily_stat WHERE source_type = 'custom-world'",
|
||||||
|
"SELECT * FROM creation_entry_config",
|
||||||
|
"SELECT * FROM creation_entry_type_config",
|
||||||
|
] {
|
||||||
|
if let Ok(subscription) = self
|
||||||
|
.subscribe_cached_read_model_query(connection, broken.clone(), query, false)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
subscriptions.push(subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(subscriptions)
|
Ok(subscriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn subscribe_cached_read_model_query(
|
||||||
|
&self,
|
||||||
|
connection: &DbConnection,
|
||||||
|
broken: Arc<AtomicBool>,
|
||||||
|
query: &'static str,
|
||||||
|
mark_broken_on_error: bool,
|
||||||
|
) -> Result<SubscriptionHandle, SpacetimeClientError> {
|
||||||
|
let (sender, receiver) = oneshot::channel::<Result<(), SpacetimeClientError>>();
|
||||||
|
let applied_sender = Arc::new(Mutex::new(Some(sender)));
|
||||||
|
let on_applied_sender = applied_sender.clone();
|
||||||
|
let on_error_sender = applied_sender.clone();
|
||||||
|
let broken_flag = broken.clone();
|
||||||
|
let subscription = connection
|
||||||
|
.subscription_builder()
|
||||||
|
.on_applied(move |_| {
|
||||||
|
send_connect_once(&on_applied_sender, Ok(()));
|
||||||
|
})
|
||||||
|
.on_error(move |_, error| {
|
||||||
|
if mark_broken_on_error {
|
||||||
|
broken_flag.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
send_connect_once(
|
||||||
|
&on_error_sender,
|
||||||
|
Err(SpacetimeClientError::Procedure(error.to_string())),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.subscribe(query);
|
||||||
|
|
||||||
|
timeout(self.config.procedure_timeout, receiver)
|
||||||
|
.await
|
||||||
|
.map_err(|_| SpacetimeClientError::Timeout)?
|
||||||
|
.map_err(|_| SpacetimeClientError::ConnectDropped)??;
|
||||||
|
|
||||||
|
Ok(subscription)
|
||||||
|
}
|
||||||
|
|
||||||
async fn release_connection(&self, mut lease: PooledConnectionLease) {
|
async fn release_connection(&self, mut lease: PooledConnectionLease) {
|
||||||
let mut slot_guard = self.pool.slots[lease.slot_index].lock().await;
|
let mut slot_guard = self.pool.slots[lease.slot_index].lock().await;
|
||||||
slot_guard.in_use = false;
|
slot_guard.in_use = false;
|
||||||
@@ -581,6 +629,39 @@ impl SpacetimeClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn current_unix_micros() -> i64 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_micros() as i64)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_public_work_day() -> i64 {
|
||||||
|
current_unix_micros().div_euclid(PUBLIC_WORK_PLAY_DAY_MICROS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn public_work_recent_play_counts(
|
||||||
|
connection: &DbConnection,
|
||||||
|
source_type: &str,
|
||||||
|
) -> HashMap<String, u32> {
|
||||||
|
let current_day = current_public_work_day();
|
||||||
|
let first_day = current_day - (PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS - 1);
|
||||||
|
let mut counts = HashMap::new();
|
||||||
|
|
||||||
|
for row in connection.db().public_work_play_daily_stat().iter() {
|
||||||
|
if row.source_type != source_type
|
||||||
|
|| row.played_day < first_day
|
||||||
|
|| row.played_day > current_day
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let entry: &mut u32 = counts.entry(row.profile_id).or_insert(0);
|
||||||
|
*entry = (*entry).saturating_add(row.play_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
counts
|
||||||
|
}
|
||||||
|
|
||||||
impl SpacetimeClientError {
|
impl SpacetimeClientError {
|
||||||
pub(crate) fn from_sdk_error(error: impl fmt::Display) -> Self {
|
pub(crate) fn from_sdk_error(error: impl fmt::Display) -> Self {
|
||||||
Self::Procedure(error.to_string())
|
Self::Procedure(error.to_string())
|
||||||
|
|||||||
@@ -830,6 +830,48 @@ pub(crate) fn map_creation_entry_config_procedure_result(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_creation_entry_config_record_from_rows(
|
||||||
|
header: CreationEntryConfig,
|
||||||
|
mut creation_types: Vec<CreationEntryTypeConfig>,
|
||||||
|
) -> CreationEntryConfigRecord {
|
||||||
|
creation_types.sort_by(|left, right| {
|
||||||
|
left.sort_order
|
||||||
|
.cmp(&right.sort_order)
|
||||||
|
.then_with(|| left.id.cmp(&right.id))
|
||||||
|
});
|
||||||
|
|
||||||
|
module_runtime::build_creation_entry_config_response(
|
||||||
|
module_runtime::CreationEntryConfigSnapshot {
|
||||||
|
config_id: header.config_id,
|
||||||
|
start_card: module_runtime::CreationEntryStartCardSnapshot {
|
||||||
|
title: header.start_title,
|
||||||
|
description: header.start_description,
|
||||||
|
idle_badge: header.start_idle_badge,
|
||||||
|
busy_badge: header.start_busy_badge,
|
||||||
|
},
|
||||||
|
type_modal: module_runtime::CreationEntryTypeModalSnapshot {
|
||||||
|
title: header.modal_title,
|
||||||
|
description: header.modal_description,
|
||||||
|
},
|
||||||
|
creation_types: creation_types
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| module_runtime::CreationEntryTypeSnapshot {
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
subtitle: item.subtitle,
|
||||||
|
badge: item.badge,
|
||||||
|
image_src: item.image_src,
|
||||||
|
visible: item.visible,
|
||||||
|
open: item.open,
|
||||||
|
sort_order: item.sort_order,
|
||||||
|
updated_at_micros: item.updated_at.to_micros_since_unix_epoch(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
updated_at_micros: header.updated_at.to_micros_since_unix_epoch(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn map_creation_entry_config_snapshot(
|
fn map_creation_entry_config_snapshot(
|
||||||
snapshot: CreationEntryConfigSnapshot,
|
snapshot: CreationEntryConfigSnapshot,
|
||||||
) -> module_runtime::CreationEntryConfigSnapshot {
|
) -> module_runtime::CreationEntryConfigSnapshot {
|
||||||
@@ -1437,20 +1479,6 @@ pub(crate) fn map_custom_world_library_detail_result(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn map_custom_world_gallery_list_result(
|
|
||||||
result: CustomWorldGalleryListResult,
|
|
||||||
) -> Result<Vec<CustomWorldGalleryEntryRecord>, SpacetimeClientError> {
|
|
||||||
if !result.ok {
|
|
||||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result
|
|
||||||
.entries
|
|
||||||
.into_iter()
|
|
||||||
.map(map_custom_world_gallery_entry_snapshot)
|
|
||||||
.collect::<Result<Vec<_>, _>>()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn map_custom_world_library_mutation_result(
|
pub(crate) fn map_custom_world_library_mutation_result(
|
||||||
result: CustomWorldLibraryMutationResult,
|
result: CustomWorldLibraryMutationResult,
|
||||||
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
||||||
@@ -2676,6 +2704,38 @@ pub(crate) fn map_custom_world_gallery_entry_snapshot(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_custom_world_gallery_entry_row(
|
||||||
|
row: CustomWorldGalleryEntry,
|
||||||
|
recent_play_count_7d: u32,
|
||||||
|
) -> CustomWorldGalleryEntryRecord {
|
||||||
|
CustomWorldGalleryEntryRecord {
|
||||||
|
owner_user_id: row.owner_user_id,
|
||||||
|
profile_id: row.profile_id,
|
||||||
|
public_work_code: row.public_work_code,
|
||||||
|
author_public_user_code: row.author_public_user_code,
|
||||||
|
visibility: "published".to_string(),
|
||||||
|
published_at: Some(format_timestamp_micros(
|
||||||
|
row.published_at.to_micros_since_unix_epoch(),
|
||||||
|
)),
|
||||||
|
updated_at: format_timestamp_micros(row.updated_at.to_micros_since_unix_epoch()),
|
||||||
|
author_display_name: row.author_display_name,
|
||||||
|
world_name: row.world_name,
|
||||||
|
subtitle: row.subtitle,
|
||||||
|
summary_text: row.summary_text,
|
||||||
|
cover_image_src: row.cover_image_src,
|
||||||
|
theme_mode: format_custom_world_theme_mode(map_custom_world_theme_mode_back(
|
||||||
|
row.theme_mode,
|
||||||
|
))
|
||||||
|
.to_string(),
|
||||||
|
playable_npc_count: row.playable_npc_count,
|
||||||
|
landmark_count: row.landmark_count,
|
||||||
|
play_count: row.play_count,
|
||||||
|
remix_count: row.remix_count,
|
||||||
|
like_count: row.like_count,
|
||||||
|
recent_play_count_7d,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn map_custom_world_published_profile_compile_snapshot(
|
pub(crate) fn map_custom_world_published_profile_compile_snapshot(
|
||||||
snapshot: CustomWorldPublishedProfileCompileSnapshot,
|
snapshot: CustomWorldPublishedProfileCompileSnapshot,
|
||||||
) -> Result<CustomWorldPublishedProfileCompileRecord, SpacetimeClientError> {
|
) -> Result<CustomWorldPublishedProfileCompileRecord, SpacetimeClientError> {
|
||||||
|
|||||||
@@ -5,45 +5,6 @@ use crate::module_bindings::delete_puzzle_work_procedure::delete_puzzle_work;
|
|||||||
use crate::module_bindings::record_puzzle_work_like_procedure::record_puzzle_work_like;
|
use crate::module_bindings::record_puzzle_work_like_procedure::record_puzzle_work_like;
|
||||||
use crate::module_bindings::remix_puzzle_work_procedure::remix_puzzle_work;
|
use crate::module_bindings::remix_puzzle_work_procedure::remix_puzzle_work;
|
||||||
use crate::module_bindings::save_puzzle_ui_background_procedure::save_puzzle_ui_background;
|
use crate::module_bindings::save_puzzle_ui_background_procedure::save_puzzle_ui_background;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
const PUBLIC_WORK_PLAY_DAY_MICROS: i64 = 86_400_000_000;
|
|
||||||
const PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS: i64 = 7;
|
|
||||||
|
|
||||||
fn current_unix_micros() -> i64 {
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map(|duration| duration.as_micros() as i64)
|
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_public_work_day() -> i64 {
|
|
||||||
current_unix_micros().div_euclid(PUBLIC_WORK_PLAY_DAY_MICROS)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn puzzle_gallery_recent_play_counts(connection: &DbConnection) -> HashMap<String, u32> {
|
|
||||||
let current_day = current_public_work_day();
|
|
||||||
let first_day = current_day - (PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS - 1);
|
|
||||||
let mut counts = HashMap::new();
|
|
||||||
|
|
||||||
for row in connection
|
|
||||||
.db()
|
|
||||||
.public_work_play_daily_stat()
|
|
||||||
.iter()
|
|
||||||
{
|
|
||||||
if row.source_type != "puzzle"
|
|
||||||
|| row.played_day < first_day
|
|
||||||
|| row.played_day > current_day
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let entry: &mut u32 = counts.entry(row.profile_id).or_insert(0);
|
|
||||||
*entry = (*entry).saturating_add(row.play_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
counts
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SpacetimeClient {
|
impl SpacetimeClient {
|
||||||
pub async fn create_puzzle_agent_session(
|
pub async fn create_puzzle_agent_session(
|
||||||
@@ -443,9 +404,13 @@ impl SpacetimeClient {
|
|||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<PuzzleWorkProfileRecord>, SpacetimeClientError> {
|
) -> Result<Vec<PuzzleWorkProfileRecord>, SpacetimeClientError> {
|
||||||
self.read_after_connect(move |connection| {
|
self.read_after_connect(move |connection| {
|
||||||
let mut items = connection.db().puzzle_gallery_view().iter().collect::<Vec<_>>();
|
let mut items = connection
|
||||||
|
.db()
|
||||||
|
.puzzle_gallery_view()
|
||||||
|
.iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
|
items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
|
||||||
let recent_play_counts = puzzle_gallery_recent_play_counts(connection);
|
let recent_play_counts = public_work_recent_play_counts(connection, "puzzle");
|
||||||
Ok(items
|
Ok(items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|item| {
|
.map(|item| {
|
||||||
|
|||||||
@@ -3,6 +3,49 @@ use super::*;
|
|||||||
impl SpacetimeClient {
|
impl SpacetimeClient {
|
||||||
pub async fn get_creation_entry_config(
|
pub async fn get_creation_entry_config(
|
||||||
&self,
|
&self,
|
||||||
|
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
|
||||||
|
match self
|
||||||
|
.read_after_connect(move |connection| {
|
||||||
|
let config_id = module_runtime::CREATION_ENTRY_CONFIG_GLOBAL_ID.to_string();
|
||||||
|
let header = connection
|
||||||
|
.db()
|
||||||
|
.creation_entry_config()
|
||||||
|
.config_id()
|
||||||
|
.find(&config_id)
|
||||||
|
.ok_or_else(|| SpacetimeClientError::missing_snapshot("创作入口配置快照"))?;
|
||||||
|
let creation_types = connection
|
||||||
|
.db()
|
||||||
|
.creation_entry_type_config()
|
||||||
|
.iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(build_creation_entry_config_record_from_rows(
|
||||||
|
header,
|
||||||
|
creation_types,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(config) => {
|
||||||
|
self.cache_creation_entry_config(config.clone()).await;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
if let Some(config) = self.read_cached_creation_entry_config().await {
|
||||||
|
return Ok(config);
|
||||||
|
}
|
||||||
|
match self.fetch_creation_entry_config_via_procedure().await {
|
||||||
|
Ok(config) => {
|
||||||
|
self.cache_creation_entry_config(config.clone()).await;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
Err(fallback_error) => Err(fallback_error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_creation_entry_config_via_procedure(
|
||||||
|
&self,
|
||||||
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
|
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
|
||||||
self.call_after_connect("get_creation_entry_config", move |connection, sender| {
|
self.call_after_connect("get_creation_entry_config", move |connection, sender| {
|
||||||
connection
|
connection
|
||||||
@@ -22,20 +65,26 @@ impl SpacetimeClient {
|
|||||||
input: module_runtime::CreationEntryTypeAdminUpsertInput,
|
input: module_runtime::CreationEntryTypeAdminUpsertInput,
|
||||||
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
|
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
|
||||||
let procedure_input: CreationEntryTypeAdminUpsertInput = input.into();
|
let procedure_input: CreationEntryTypeAdminUpsertInput = input.into();
|
||||||
self.call_after_connect(
|
let config = self
|
||||||
"upsert_creation_entry_type_config",
|
.call_after_connect(
|
||||||
move |connection, sender| {
|
"upsert_creation_entry_type_config",
|
||||||
connection
|
move |connection, sender| {
|
||||||
.procedures()
|
connection
|
||||||
.upsert_creation_entry_type_config_then(procedure_input, move |_, result| {
|
.procedures()
|
||||||
let mapped = result
|
.upsert_creation_entry_type_config_then(
|
||||||
.map_err(SpacetimeClientError::from_sdk_error)
|
procedure_input,
|
||||||
.and_then(map_creation_entry_config_procedure_result);
|
move |_, result| {
|
||||||
send_once(&sender, mapped);
|
let mapped = result
|
||||||
});
|
.map_err(SpacetimeClientError::from_sdk_error)
|
||||||
},
|
.and_then(map_creation_entry_config_procedure_result);
|
||||||
)
|
send_once(&sender, mapped);
|
||||||
.await
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
self.cache_creation_entry_config(config.clone()).await;
|
||||||
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_runtime_settings(
|
pub async fn get_runtime_settings(
|
||||||
|
|||||||
@@ -52,17 +52,10 @@ fn spacetime_metrics() -> &'static SpacetimeMetrics {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record_procedure(
|
fn record_procedure(procedure: &'static str, duration: Duration, failed: bool) {
|
||||||
procedure: &'static str,
|
|
||||||
duration: Duration,
|
|
||||||
failed: bool,
|
|
||||||
) {
|
|
||||||
let labels = vec![
|
let labels = vec![
|
||||||
KeyValue::new("procedure", procedure),
|
KeyValue::new("procedure", procedure),
|
||||||
KeyValue::new(
|
KeyValue::new("status_class", if failed { "error" } else { "ok" }),
|
||||||
"status_class",
|
|
||||||
if failed { "error" } else { "ok" },
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
let metrics = spacetime_metrics();
|
let metrics = spacetime_metrics();
|
||||||
metrics.calls.add(1, &labels);
|
metrics.calls.add(1, &labels);
|
||||||
|
|||||||
Reference in New Issue
Block a user