合并 master 最新 OSS 上传优化
合入 origin/master 的动画生成 OSS 上传优化 保留现役编辑器 Agent LLM client 与退役隔离边界 保留角色动画 OSS 客户端复用与并发限制
This commit is contained in:
@@ -16,6 +16,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-20 角色动画帧 OSS 请求使用专用连接池、并发保护与结构化重试
|
||||
|
||||
- 背景:角色动作逐帧流水线会同时发起源帧 PUT、透明帧 PUT 和最终帧 HEAD;原路径每次请求新建 `reqwest::Client`,且 OSS 请求错误丢失 HTTP 状态和 timeout/connect/transport 分类,多个动画任务叠加时无法在进程级限制 OSS 在途请求,也无法安全区分 PUT 与 HEAD 的失败。
|
||||
- 决策:`AppState` 仅为角色动画帧初始化一次 OSS HTTP Client 和 8 路 `Semaphore`。全帧 Future 仍保持 `buffer_unordered(frame_count.max(1))`,BgFilter、阿里云抠图和本地处理不占 OSS permit;每次 PUT/HEAD 网络 attempt 单独获取 permit,退避期间释放。`platform-oss` 保留 `OssErrorKind::Request`,但在 `OssError::Request` 中保留 operation、status、timeout、connect、transport、OSS code、OSS request-id 和原脱敏 message,并为动画帧提供 3 次 attempt、250ms/500ms 退避的 PUT/HEAD 独立重试。仅无响应传输错误、timeout、OSS PutObject 的 `400 + RequestTimeout`、PUT 400 错误体读取失败(未解析出 `Code`,按 timeout/transport 归类)、408、429 和 5xx 可重试;动作帧 PUT 对 400 错误体最多读取 16 KiB,只提取 `Code` 和响应头优先的 `x-oss-request-id`,不记录完整 XML;错误体读取超时/断流时保留已读字节,已解析出的 `Code` 优先生效。除 `RequestTimeout` 与该错误体读取失败情形外的确定性 4xx、配置、签名、URL、空请求体、抠图和素材登记错误不重试。重试体在 platform-oss 内一次转为可复用 `Bytes`,每次重新签名和构造 Request,不复制整帧字节。
|
||||
- 失败语义:最终帧 PUT 成功后才执行 HEAD;HEAD 失败只重试 HEAD,不重复 PUT。任一帧最终失败仍排空已启动的 Future、整段动作退款并禁止发布缺帧动画,帧结果继续按原始序号排序。
|
||||
- 影响范围:`state.rs`、`platform-oss/lib.rs`、`character_animation_assets.rs`、对应 Cargo 依赖和架构 / 运维文档;不改变其他 OSS 调用方、BgFilter/阿里云降级、worker、计费退款、SpacetimeDB schema/DTO 或前端接口。
|
||||
- 验证方式:`cargo test -p platform-oss --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server character_animation --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-18 图片生成 K 档由 provider 直接生成
|
||||
|
||||
- 背景:旧 gpt-image-2 尺寸表会把 2K 竖版回落到 `1024x1536`,图标入口又使用固定 `360x360 / 512x512` 占位;角色去背景结果变小时还会直接放大整张透明成品,导致 UI 显示的 2K 与模型实际生成清晰度不一致。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -407,6 +407,10 @@ curl -fsS --max-time 5 http://127.0.0.1/ >/dev/null
|
||||
curl -fsS --max-time 5 http://127.0.0.1/api/editor/showcase/resources >/dev/null
|
||||
```
|
||||
|
||||
### 角色动画帧 OSS 排障
|
||||
|
||||
角色动画源帧 PUT、透明帧 PUT 和最终帧 HEAD 使用 `AppState` 内同一个 OSS HTTP Client/连接池,并受进程级 8 路 OSS permit 保护;BgFilter、阿里云抠图和本地处理不占用该 permit。每个 OSS attempt 最多 3 次(首次 + 2 次重试),退避为 250ms、500ms;只重试 timeout、无 HTTP 响应传输错误、OSS PutObject 的 `400 + RequestTimeout`、PUT 400 错误体读取失败(未解析出 `Code`,按 timeout/transport 归类)、408、429 和 500–599。动作帧 PUT 收到 400 时只读取最多 16 KiB OSS 错误 XML,提取 `Code` 和 `RequestId`;`oss_request_id` 优先使用响应头 `x-oss-request-id`,XML 字段只作回退。错误体读取超时/断流不再按确定性 400 处理:已解析出的 `Code` 优先生效;未解析出 `Code` 时按读取失败原因置 `timeout`/`transport` 并重试,message 追加「错误响应体读取失败」。日志字段包括 `frame_index`、`object_key`、`operation=source_put|final_put|final_head`、`attempt`、`max_attempts`、`retryable`、`will_retry`、`retry_delay_ms`、`permit_wait_ms`、`timeout`、`connect`、`transport`、`oss_code`、`oss_request_id`、`status` 和 `elapsed_ms`。`请求 OSS 失败` 时,`timeout/connect/transport=true` 表示传输类失败;`status=400, oss_code=RequestTimeout, timeout=true`、`status=429` 或 `500–599` 表示暂时性失败,PUT 的 `status=400`、`oss_code` 为空且 `timeout=true` 或 `transport=true`(message 含「错误响应体读取失败」)同样是暂时性失败。除 `RequestTimeout` 和该错误体读取失败两类例外外,其他 400、401/403/404、配置、URL 和签名错误是确定性失败,不会重试。最终帧 HEAD 失败只会重试 HEAD,不会重复 PUT;如果任一帧最终失败,确认整段动作已排空已启动 Future,并检查任务按现有契约退款且没有发布缺帧动画。
|
||||
|
||||
## 生产运维
|
||||
|
||||
生产部署当前口径:
|
||||
@@ -600,7 +604,7 @@ OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs,但本地日
|
||||
- api-server 会随 metrics 发送进程级指标:`process.memory.usage`、`process.memory.virtual`、`process.cpu.time`、`genarrative.process.cpu.usage_percent`、`process.thread.count`、`genarrative.process.memory.private`;Windows 额外发送 `process.windows.handle.count`,Linux 额外发送 `process.unix.file_descriptor.count`。这些指标只描述当前进程,不携带请求、用户或作品 label。
|
||||
- HTTP 运行态补充发送 `genarrative.http.server.response_bodies.in_flight` 与 `genarrative.http.server.request_permits.available`,后者带低基数 `pool=default|gallery|detail|admin` label,用于区分业务 handler / 背压 permit 是否仍被占用;拼图广场热点缓存补充发送 `genarrative.puzzle_gallery.cache.*` 指标,记录 fresh hit、stale hit、未命中、后台刷新开始 / 失败、重建耗时和预序列化 data JSON 字节数。
|
||||
- 外部 API 失败统一发送 OTLP 并落库。当前 VectorEngine `gpt-image-2` 图片生成 / 编辑失败由 `platform-image` provider 输出结构化日志字段,字段包括 provider、endpoint、failure_stage、status、source、source_chain、source_chain_depth、timeout、retryable、latency_ms、prompt_chars、reference_image_count、image_model、request_params 和 raw_excerpt;图片编辑请求参数日志还会带 reference_image_bytes_total,并在 request_params.referenceImages 中记录每个 multipart `image` part 的 fileName、mimeType 和 bytes,不记录 API key 或原始图片 bytes;`api-server` 再记录指标 `genarrative.external_api.failures{provider,failure_stage,status_class,retryable}`,并写入 `tracking_event`,`event_key = external_api_call_failure`、`module_key = external-api`、`scope_kind = module`、`scope_id = provider`。调用方能拿到身份上下文时,失败事件还会在行级 `user_id` / `owner_user_id` / `profile_id` 和 `metadata_json.userId` / `metadata_json.profileId` / `metadata_json.requestId` / `metadata_json.errorSource` 中记录触发者、草稿 / 作品作用域、请求标识和传输错误链。排障时先按 provider / failureStage 聚合,再下钻 userId / profileId,最后结合 request 日志、errorSource 和上游响应 excerpt 判断是限流、超时、解析失败还是未返回图片。
|
||||
- OSS 平台适配器也输出结构化日志,覆盖 `sign_post_object`、`sign_get_object_url`、`head_object` 和 `put_object`。排查资产签名、上传或确认失败时,先按 `provider=aliyun-oss` 与 `operation` 过滤,再看 `object_key` / `key_prefix`、`status`、`status_class`、`error_kind`、`content_length`、`content_type` 和 `elapsed_ms`;日志不得包含 AccessKey、policy、signature、Authorization header 或完整 signed URL。排查 generated 图片重复下载时,先确认前端输入是否为 `/generated-*` legacy path 或可归一化的 `https://*.oss-*.aliyuncs.com/generated-*`;正确链路应先调 `/api/assets/read-url`,再由浏览器请求 signed URL,且同一路径、同一 `refreshKey` 版本和未临近过期的 signed URL 应复用。新上传 generated 私有对象应带 `Cache-Control: public, max-age=31536000, immutable`;旧对象若只有 `ETag` / `Last-Modified`,浏览器会走 304 协商缓存而不是长期强缓存,可通过刷新 OSS 元数据或 CDN 配置补齐。
|
||||
- OSS 平台适配器也输出结构化日志,覆盖 `sign_post_object`、`sign_get_object_url`、`head_object` 和 `put_object`。排查资产签名、上传或确认失败时,先按 `provider=aliyun-oss` 与 `operation` 过滤,再看 `object_key` / `key_prefix`、`status`、`status_class`、`error_kind`、`content_length`、`content_type` 和 `elapsed_ms`;角色动画逐帧额外按 `frame_index`、`operation=source_put|final_put|final_head`、`attempt/max_attempts`、`will_retry`、`oss_code` 和 `oss_request_id` 对齐同一对象的请求尝试。`请求 OSS 失败` 时,`timeout/connect/transport=true` 表示传输类失败,OSS PutObject 的 `status=400, oss_code=RequestTimeout, timeout=true`、`status=429` 或 `500–599` 表示暂时性失败,PUT 的 `status=400`、`oss_code` 为空且 `timeout=true` 或 `transport=true`(message 含「错误响应体读取失败」,即 400 错误体读取超时/断流)也会重试;除这两类例外外,其他 400、401/403/404、配置、URL 和签名错误是确定性失败,不会重试。最终帧 HEAD 失败只会重试 HEAD,不会重复 PUT。日志不得包含 AccessKey、policy、signature、Authorization header、完整 signed URL 或 OSS 错误响应体;`oss_request_id` 只用于关联 OSS 服务端排障。排查 generated 图片重复下载时,先确认前端输入是否为 `/generated-*` legacy path 或可归一化的 `https://*.oss-*.aliyuncs.com/generated-*`;正确链路应先调 `/api/assets/read-url`,再由浏览器请求 signed URL,且同一路径、同一 `refreshKey` 版本和未临近过期的 signed URL 应复用。新上传 generated 私有对象应带 `Cache-Control: public, max-age=31536000, immutable`;旧对象若只有 `ETag` / `Last-Modified`,浏览器会走 304 协商缓存而不是长期强缓存,可通过刷新 OSS 元数据或 CDN 配置补齐。
|
||||
- SpacetimeDB 观测分为两类:procedure / reducer 调用继续用 `genarrative.spacetime.procedure.*`,订阅本地 cache 读使用 `genarrative.spacetime.read.*`。`read=list_puzzle_gallery` 表示拼图广场当前从 `puzzle_gallery_card_view` 本地 cache 读取,不再每个 HTTP 请求调用 `list_puzzle_gallery` procedure。
|
||||
- 本地 Windows 直连压测的内存高水位要结合 K6 VU / 连接数解释。250 RPS 下过高 `PREALLOCATED_VUS` 可能让 300 个本地 Established 连接把 `api-server` private memory 瞬时推到 GB 级,且 `/healthz` 小响应也能复现;若压测结束后回落、`response_bodies.in_flight` 和背压 permit 未显示业务积压,应优先按连接 / 发送链路高水位处理,而不是判断为 SpacetimeDB 或 JSON 缓存泄漏。
|
||||
- Rider 的 Logs 面板只展示 log event 自身字段,不会自动展开父 span 的全部 attributes;请求完成日志会直接带 `request_id`、`http.request.method`、`http.route`、`url.scheme`、`url.path`、`http.response.status_code`、`status_class`、`latency_ms` 和 `slow_request`,完整链路继续到 Traces 面板按 trace/span 查看。
|
||||
|
||||
Generated
+1
@@ -4125,6 +4125,7 @@ name = "platform-oss"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"hmac",
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
||||
@@ -27,7 +27,7 @@ use module_assets::{
|
||||
};
|
||||
use platform_oss::{
|
||||
LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest,
|
||||
OssSignedGetObjectUrlRequest,
|
||||
OssRequestAttemptContext, OssSignedGetObjectUrlRequest,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
@@ -2420,8 +2420,10 @@ async fn process_and_persist_editor_character_animation_frame(
|
||||
audit: &crate::external_api_audit::ExternalApiAuditContext,
|
||||
) -> Result<ProcessedEditorCharacterAnimationFrame, AppError> {
|
||||
// 中文注释:每一帧只要求自己的绿幕源图先落 OSS,不再等待整批源图全部上传完成。
|
||||
let source_put = put_character_animation_object(
|
||||
let source_put = put_character_animation_frame_object(
|
||||
state,
|
||||
frame_index + 1,
|
||||
"source_put",
|
||||
LegacyAssetPrefix::Animations,
|
||||
vec![
|
||||
"editor".to_string(),
|
||||
@@ -2469,8 +2471,10 @@ async fn process_and_persist_editor_character_animation_frame(
|
||||
false,
|
||||
)?;
|
||||
let content_type = finalized.mime_type.clone();
|
||||
let put_result = put_character_animation_object(
|
||||
let put_result = put_character_animation_frame_object(
|
||||
state,
|
||||
frame_index + 1,
|
||||
"final_put",
|
||||
LegacyAssetPrefix::Animations,
|
||||
vec![
|
||||
"editor".to_string(),
|
||||
@@ -2497,6 +2501,7 @@ async fn process_and_persist_editor_character_animation_frame(
|
||||
task_id,
|
||||
put_result.object_key.clone(),
|
||||
content_type,
|
||||
frame_index + 1,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2754,6 +2759,39 @@ async fn publish_single_animation_action(
|
||||
})
|
||||
}
|
||||
|
||||
async fn put_character_animation_frame_object(
|
||||
state: &AppState,
|
||||
frame_index: usize,
|
||||
operation: &'static str,
|
||||
prefix: LegacyAssetPrefix,
|
||||
path_segments: Vec<String>,
|
||||
file_name: String,
|
||||
content_type: String,
|
||||
body: Vec<u8>,
|
||||
metadata: BTreeMap<String, String>,
|
||||
) -> Result<platform_oss::OssPutObjectResponse, AppError> {
|
||||
require_oss_client(state)?
|
||||
.put_object_with_retry(
|
||||
state.character_animation_oss_http_client(),
|
||||
OssPutObjectRequest {
|
||||
prefix,
|
||||
path_segments,
|
||||
file_name,
|
||||
content_type: Some(content_type),
|
||||
access: OssObjectAccess::Private,
|
||||
metadata,
|
||||
body,
|
||||
},
|
||||
state.character_animation_oss_io_limiter(),
|
||||
OssRequestAttemptContext {
|
||||
frame_index,
|
||||
operation,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(map_character_animation_oss_error)
|
||||
}
|
||||
|
||||
async fn put_character_animation_object(
|
||||
state: &AppState,
|
||||
prefix: LegacyAssetPrefix,
|
||||
@@ -2886,13 +2924,27 @@ async fn confirm_editor_character_animation_frame_asset_object(
|
||||
task_id: &str,
|
||||
object_key: String,
|
||||
content_type: String,
|
||||
frame_index: usize,
|
||||
) -> Result<module_assets::ConfirmAssetObjectResult, AppError> {
|
||||
confirm_editor_character_animation_asset_object(
|
||||
let oss_client = require_oss_client(state)?;
|
||||
let head = oss_client
|
||||
.head_object_with_retry(
|
||||
state.character_animation_oss_http_client(),
|
||||
OssHeadObjectRequest { object_key },
|
||||
state.character_animation_oss_io_limiter(),
|
||||
OssRequestAttemptContext {
|
||||
frame_index,
|
||||
operation: "final_head",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(map_character_animation_oss_error)?;
|
||||
confirm_editor_character_animation_asset_object_from_head(
|
||||
state,
|
||||
owner_user_id,
|
||||
source_layer_id,
|
||||
task_id,
|
||||
object_key,
|
||||
head,
|
||||
content_type,
|
||||
EDITOR_CHARACTER_ANIMATION_ASSET_KIND,
|
||||
)
|
||||
@@ -2913,6 +2965,27 @@ async fn confirm_editor_character_animation_asset_object(
|
||||
.head_object(&reqwest::Client::new(), OssHeadObjectRequest { object_key })
|
||||
.await
|
||||
.map_err(map_character_animation_oss_error)?;
|
||||
confirm_editor_character_animation_asset_object_from_head(
|
||||
state,
|
||||
owner_user_id,
|
||||
source_layer_id,
|
||||
task_id,
|
||||
head,
|
||||
content_type,
|
||||
asset_kind,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn confirm_editor_character_animation_asset_object_from_head(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
source_layer_id: &str,
|
||||
task_id: &str,
|
||||
head: platform_oss::OssHeadObjectResponse,
|
||||
content_type: String,
|
||||
asset_kind: &str,
|
||||
) -> Result<module_assets::ConfirmAssetObjectResult, AppError> {
|
||||
let now_micros = current_utc_micros();
|
||||
let record = state
|
||||
.spacetime_client()
|
||||
@@ -6218,13 +6291,13 @@ mod tests {
|
||||
"async fn process_and_persist_editor_character_animation_frame",
|
||||
"async fn publish_animation_set",
|
||||
&[
|
||||
"put_character_animation_object",
|
||||
"put_character_animation_frame_object",
|
||||
"green-screen-frame",
|
||||
"remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
"source_put.object_key.as_str()",
|
||||
"bgfilter_request_timeout_ms",
|
||||
"finalize_animation_frame_payload",
|
||||
"put_character_animation_object",
|
||||
"put_character_animation_frame_object",
|
||||
"animation_frame",
|
||||
],
|
||||
);
|
||||
|
||||
@@ -48,6 +48,7 @@ use crate::work_author::{
|
||||
};
|
||||
|
||||
const ADMIN_ROLE: &str = "admin";
|
||||
pub(crate) const CHARACTER_ANIMATION_OSS_MAX_CONCURRENCY: usize = 8;
|
||||
|
||||
pub type HttpRequestPermitPool = Semaphore;
|
||||
|
||||
@@ -259,6 +260,8 @@ pub struct AppStateInner {
|
||||
editor_agent_llm_client: Option<LlmClient>,
|
||||
matting_client: Option<MattingClient>,
|
||||
editor_bgfilter_http_client: reqwest::Client,
|
||||
character_animation_oss_http_client: reqwest::Client,
|
||||
character_animation_oss_io_limiter: Arc<Semaphore>,
|
||||
#[cfg(any())]
|
||||
creative_agent_executor: Arc<MockLangChainRustAgentExecutor>,
|
||||
// Phase 1 任务 E 的 creative session facade 暂存在 api-server。
|
||||
@@ -502,6 +505,9 @@ impl AppState {
|
||||
let editor_agent_llm_client = build_editor_agent_llm_client(&config)?;
|
||||
let matting_client = build_matting_client(&config)?;
|
||||
let editor_bgfilter_http_client = build_editor_bgfilter_http_client(&config)?;
|
||||
let character_animation_oss_http_client = build_character_animation_oss_http_client()?;
|
||||
let character_animation_oss_io_limiter =
|
||||
Arc::new(Semaphore::new(CHARACTER_ANIMATION_OSS_MAX_CONCURRENCY));
|
||||
let http_request_permit_pools = HttpRequestPermitPools::from_config(&config);
|
||||
let (profile_recharge_order_updates, _) = broadcast::channel(128);
|
||||
|
||||
@@ -544,6 +550,8 @@ impl AppState {
|
||||
editor_agent_llm_client,
|
||||
matting_client,
|
||||
editor_bgfilter_http_client,
|
||||
character_animation_oss_http_client,
|
||||
character_animation_oss_io_limiter,
|
||||
#[cfg(any())]
|
||||
creative_agent_executor: Arc::new(MockLangChainRustAgentExecutor),
|
||||
#[cfg(any())]
|
||||
@@ -1260,6 +1268,14 @@ impl AppState {
|
||||
&self.editor_bgfilter_http_client
|
||||
}
|
||||
|
||||
pub fn character_animation_oss_http_client(&self) -> &reqwest::Client {
|
||||
&self.character_animation_oss_http_client
|
||||
}
|
||||
|
||||
pub fn character_animation_oss_io_limiter(&self) -> Arc<Semaphore> {
|
||||
self.character_animation_oss_io_limiter.clone()
|
||||
}
|
||||
|
||||
#[cfg(any())]
|
||||
pub fn creative_agent_executor(&self) -> Arc<MockLangChainRustAgentExecutor> {
|
||||
self.creative_agent_executor.clone()
|
||||
@@ -1991,6 +2007,21 @@ fn build_editor_bgfilter_http_client(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_character_animation_oss_http_client() -> Result<reqwest::Client, AppStateInitError> {
|
||||
reqwest::Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_secs(30))
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.pool_idle_timeout(std::time::Duration::from_secs(300))
|
||||
.pool_max_idle_per_host(8)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(60))
|
||||
.build()
|
||||
.map_err(|error| {
|
||||
AppStateInitError::DependencyUnavailable(format!(
|
||||
"初始化角色动画 OSS HTTP Client 失败:{error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn build_wechat_client(config: &AppConfig) -> WechatClient {
|
||||
WechatClient::new(WechatConfig {
|
||||
app_id: config.wechat_mini_program_app_id.clone(),
|
||||
@@ -2120,6 +2151,22 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn app_state_reuses_character_animation_oss_client_and_eight_permits() {
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
|
||||
assert!(std::ptr::eq(
|
||||
state.character_animation_oss_http_client(),
|
||||
state.character_animation_oss_http_client(),
|
||||
));
|
||||
assert_eq!(
|
||||
state
|
||||
.character_animation_oss_io_limiter()
|
||||
.available_permits(),
|
||||
CHARACTER_ANIMATION_OSS_MAX_CONCURRENCY
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_generation_pricing_typed_record_round_trips() {
|
||||
let expected = crate::editor_generation_config::parse_editor_generation_pricing_json(
|
||||
|
||||
@@ -6,13 +6,15 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
time = { workspace = true, features = ["formatting"] }
|
||||
tokio = { workspace = true, features = ["sync", "time"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||
tokio = { workspace = true, features = ["macros", "rt", "net", "io-util"] }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user