Files
Aether/apps/aether-gateway/src/maintenance/runtime/schedule.rs
T
fawney19 5d96d6673b refactor: 大规模模块拆分与代码精简,新增 ai-pipeline/data-contracts 独立 crate
- 新增 aether-ai-pipeline 和 aether-data-contracts crate,将 pipeline 逻辑与数据契约从 gateway 中解耦
- 重构 admin handlers:拆分单体模块为 auth/billing/endpoint/features/model/observability/provider/system 等独立子模块
- 合并 chat/cli 重复代码路径:精简 conversion、finalize、planner 中的 sync/chat/cli 分支
- 重构 scheduler/executor/data 层,引入 facade 模式降低模块间耦合
- 移除冗余的 intent 模块,将 plan_fallback/policy/stream_path/sync_path 迁移至 executor
- 前端适配:调整 admin API 调用和 provider 模型测试对话框
2026-04-07 02:50:19 +08:00

237 lines
8.2 KiB
Rust

use std::time::Duration;
use aether_data_contracts::DataLayerError;
use chrono::{DateTime, Datelike, TimeZone, Timelike, Utc, Weekday};
use chrono_tz::Tz;
use tracing::warn;
use crate::data::GatewayDataState;
use super::{
system_config_string, WalletDailyUsageAggregationTarget, DB_MAINTENANCE_HOUR,
DB_MAINTENANCE_MINUTE, DB_MAINTENANCE_WEEKDAY, DB_MAINTENANCE_WEEKLY_INTERVAL,
MAINTENANCE_DEFAULT_TIMEZONE, PROVIDER_CHECKIN_DEFAULT_TIME, STATS_DAILY_AGGREGATION_HOUR,
STATS_DAILY_AGGREGATION_MINUTE, STATS_HOURLY_AGGREGATION_MINUTE,
};
pub(super) async fn provider_checkin_schedule(
data: &GatewayDataState,
) -> Result<(u32, u32), DataLayerError> {
let configured =
system_config_string(data, "provider_checkin_time", PROVIDER_CHECKIN_DEFAULT_TIME).await?;
Ok(parse_hhmm_time(&configured).unwrap_or_else(|| {
warn!(
value = %configured,
fallback = PROVIDER_CHECKIN_DEFAULT_TIME,
"gateway provider checkin time invalid; falling back"
);
parse_hhmm_time(PROVIDER_CHECKIN_DEFAULT_TIME)
.expect("default provider checkin time should parse")
}))
}
pub(super) fn maintenance_timezone() -> Tz {
let configured = std::env::var("APP_TIMEZONE")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| MAINTENANCE_DEFAULT_TIMEZONE.to_string());
configured.parse().unwrap_or_else(|_| {
warn!(
timezone = %configured,
fallback = MAINTENANCE_DEFAULT_TIMEZONE,
"gateway maintenance timezone invalid; falling back"
);
MAINTENANCE_DEFAULT_TIMEZONE
.parse()
.expect("default maintenance timezone should parse")
})
}
pub(super) fn duration_until_next_db_maintenance_run(
now_utc: DateTime<Utc>,
timezone: Tz,
) -> Duration {
next_db_maintenance_run_after(now_utc, timezone)
.signed_duration_since(now_utc)
.to_std()
.unwrap_or_default()
}
pub(super) fn next_db_maintenance_run_after(now_utc: DateTime<Utc>, timezone: Tz) -> DateTime<Utc> {
let local_now = now_utc.with_timezone(&timezone);
for day_offset in 0..=7 {
let candidate_date = local_now.date_naive() + chrono::Duration::days(day_offset);
if candidate_date.weekday() != DB_MAINTENANCE_WEEKDAY {
continue;
}
let Some(candidate_naive) =
candidate_date.and_hms_opt(DB_MAINTENANCE_HOUR, DB_MAINTENANCE_MINUTE, 0)
else {
continue;
};
let candidate_local = resolve_local_scheduled_time(timezone, candidate_naive);
let Some(candidate_local) = candidate_local else {
continue;
};
if candidate_local > local_now {
return candidate_local.with_timezone(&Utc);
}
}
let fallback_date = local_now.date_naive() + DB_MAINTENANCE_WEEKLY_INTERVAL;
let fallback_naive = fallback_date
.and_hms_opt(DB_MAINTENANCE_HOUR, DB_MAINTENANCE_MINUTE, 0)
.expect("db maintenance fallback time should be valid");
let fallback_local = resolve_local_scheduled_time(timezone, fallback_naive)
.expect("db maintenance fallback local datetime should resolve");
fallback_local.with_timezone(&Utc)
}
pub(super) fn duration_until_next_daily_run(
now_utc: DateTime<Utc>,
timezone: Tz,
hour: u32,
minute: u32,
) -> Duration {
next_daily_run_after(now_utc, timezone, hour, minute)
.signed_duration_since(now_utc)
.to_std()
.unwrap_or_default()
}
pub(super) fn duration_until_next_stats_aggregation_run(now_utc: DateTime<Utc>) -> Duration {
next_stats_aggregation_run_after(now_utc)
.signed_duration_since(now_utc)
.to_std()
.unwrap_or_default()
}
pub(super) fn duration_until_next_stats_hourly_aggregation_run(now_utc: DateTime<Utc>) -> Duration {
next_stats_hourly_aggregation_run_after(now_utc)
.signed_duration_since(now_utc)
.to_std()
.unwrap_or_default()
}
pub(super) fn next_daily_run_after(
now_utc: DateTime<Utc>,
timezone: Tz,
hour: u32,
minute: u32,
) -> DateTime<Utc> {
let local_now = now_utc.with_timezone(&timezone);
for day_offset in 0..=1 {
let candidate_date = local_now.date_naive() + chrono::Duration::days(day_offset);
let Some(candidate_naive) = candidate_date.and_hms_opt(hour, minute, 0) else {
continue;
};
let candidate_local = resolve_local_scheduled_time(timezone, candidate_naive);
let Some(candidate_local) = candidate_local else {
continue;
};
if candidate_local > local_now {
return candidate_local.with_timezone(&Utc);
}
}
let fallback_date = local_now.date_naive() + chrono::Duration::days(1);
let fallback_naive = fallback_date
.and_hms_opt(hour, minute, 0)
.expect("daily fallback time should be valid");
let fallback_local = resolve_local_scheduled_time(timezone, fallback_naive)
.expect("daily fallback local datetime should resolve");
fallback_local.with_timezone(&Utc)
}
pub(super) fn next_stats_aggregation_run_after(now_utc: DateTime<Utc>) -> DateTime<Utc> {
next_daily_run_after(
now_utc,
chrono_tz::UTC,
STATS_DAILY_AGGREGATION_HOUR,
STATS_DAILY_AGGREGATION_MINUTE,
)
}
pub(super) fn next_stats_hourly_aggregation_run_after(now_utc: DateTime<Utc>) -> DateTime<Utc> {
let current_hour_slot = Utc.from_utc_datetime(
&now_utc
.date_naive()
.and_hms_opt(now_utc.hour(), STATS_HOURLY_AGGREGATION_MINUTE, 0)
.expect("stats hourly aggregation slot should be valid"),
);
if current_hour_slot > now_utc {
return current_hour_slot;
}
let next_hour = now_utc + chrono::Duration::hours(1);
Utc.from_utc_datetime(
&next_hour
.date_naive()
.and_hms_opt(next_hour.hour(), STATS_HOURLY_AGGREGATION_MINUTE, 0)
.expect("stats hourly aggregation next slot should be valid"),
)
}
pub(super) fn stats_aggregation_target_day(now_utc: DateTime<Utc>) -> DateTime<Utc> {
let previous_day = now_utc - chrono::Duration::days(1);
Utc.from_utc_datetime(
&previous_day
.date_naive()
.and_hms_opt(0, 0, 0)
.expect("stats aggregation target day should be valid"),
)
}
pub(super) fn stats_hourly_aggregation_target_hour(now_utc: DateTime<Utc>) -> DateTime<Utc> {
let previous_hour = now_utc - chrono::Duration::hours(1);
Utc.from_utc_datetime(
&previous_hour
.date_naive()
.and_hms_opt(previous_hour.hour(), 0, 0)
.expect("stats hourly aggregation target hour should be valid"),
)
}
pub(super) fn wallet_daily_usage_aggregation_target(
now_utc: DateTime<Utc>,
timezone: Tz,
) -> WalletDailyUsageAggregationTarget {
let local_today = now_utc.with_timezone(&timezone).date_naive();
let billing_date = local_today - chrono::Duration::days(1);
let next_billing_date = billing_date + chrono::Duration::days(1);
WalletDailyUsageAggregationTarget {
billing_date,
billing_timezone: timezone.to_string(),
window_start_utc: local_day_start_utc(billing_date, timezone),
window_end_utc: local_day_start_utc(next_billing_date, timezone),
}
}
pub(super) fn local_day_start_utc(date: chrono::NaiveDate, timezone: Tz) -> DateTime<Utc> {
let local_start = date
.and_hms_opt(0, 0, 0)
.and_then(|naive| resolve_local_scheduled_time(timezone, naive))
.expect("local day start should resolve");
local_start.with_timezone(&Utc)
}
pub(super) fn resolve_local_scheduled_time(
timezone: Tz,
naive: chrono::NaiveDateTime,
) -> Option<chrono::DateTime<Tz>> {
match timezone.from_local_datetime(&naive) {
chrono::LocalResult::Single(value) => Some(value),
chrono::LocalResult::Ambiguous(first, second) => Some(first.min(second)),
chrono::LocalResult::None => None,
}
}
pub(super) fn parse_hhmm_time(value: &str) -> Option<(u32, u32)> {
let (hour, minute) = value.trim().split_once(':')?;
let hour = hour.parse::<u32>().ok()?;
let minute = minute.parse::<u32>().ok()?;
(hour <= 23 && minute <= 59).then_some((hour, minute))
}