File size: 5,401 Bytes
e85df1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! UltraSpeed 资源调度:本机/云端分别限流 + 元数据缓存。
//!
//! # 问题
//!
//! 本机 GPU 推理的并发度很低(一块 GPU 同时跑两个大模型会互相抢显存,
//! 甚至触发 Ollama 卸载重载)。云端 API 则有 RPM/TPM 限流。
//!
//! # 解法
//!
//! 用信号量把**本机并发压到 1**、云端控制在可控并发(默认 2),
//! 请求排队而不是互相踩踏。同时缓存 provider 的 health / models 元数据,
//! 避免每次请求都探活(探活本身也要几十毫秒)。
//!
//! 注意:调度**只影响并发度**,绝不改变模型 / prompt / 思考档 / 回复长度。

use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, LazyLock, Mutex};
use std::time::{Duration, Instant};

use tokio::sync::{Mutex as AsyncMutex, OwnedSemaphorePermit, Semaphore};

/// 本机推理并发上限(GPU 单卡最优 = 1,排队比抢显存快)。
pub const LOCAL_CONCURRENCY: usize = 1;
/// 云端并发上限(按你的 API 限额调整)。
pub const CLOUD_CONCURRENCY: usize = 2;
/// 元数据缓存有效期。
pub const METADATA_TTL: Duration = Duration::from_secs(30);

static LOCAL_PERMITS: LazyLock<Arc<Semaphore>> =
    LazyLock::new(|| Arc::new(Semaphore::new(LOCAL_CONCURRENCY)));
static CLOUD_PERMITS: LazyLock<Arc<Semaphore>> =
    LazyLock::new(|| Arc::new(Semaphore::new(CLOUD_CONCURRENCY)));

/// 申请一个执行许可:`cloud = true` 走云端池,否则走本机池。
///
/// 返回的 permit 释放时自动归还名额。调用方应尽量短持有。
pub async fn acquire(cloud: bool) -> Result<OwnedSemaphorePermit, String> {
    let pool = if cloud {
        CLOUD_PERMITS.clone()
    } else {
        LOCAL_PERMITS.clone()
    };
    pool.acquire_owned()
        .await
        .map_err(|_| "资源池已关闭".to_string())
}

// ── 凭据版本号:凭据变更后让所有缓存失效 ────────────────────────────

static REVISION: AtomicU64 = AtomicU64::new(0);

/// 当前凭据版本(缓存 key 的一部分)。
pub fn revision() -> u64 {
    REVISION.load(Ordering::Relaxed)
}

/// 凭据/配置变更后调用:让所有 provider 的元数据缓存失效。
pub fn invalidate() {
    REVISION.fetch_add(1, Ordering::Relaxed);
    if let Ok(mut cache) = CACHE.lock() {
        cache.clear();
    }
}

// ── 带 TTL 的元数据缓存 ────────────────────────────────────────────

struct Metadata {
    health: AsyncMutex<Option<(Instant, bool)>>,
    models: AsyncMutex<Option<(Instant, Result<Vec<String>, String>)>>,
}

static CACHE: LazyLock<Mutex<HashMap<String, Arc<Metadata>>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

fn entry(key: &str) -> Arc<Metadata> {
    let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
    cache
        .entry(key.to_string())
        .or_insert_with(|| {
            Arc::new(Metadata {
                health: AsyncMutex::new(None),
                models: AsyncMutex::new(None),
            })
        })
        .clone()
}

/// 读取健康状态缓存(未命中/过期返回 None)。
pub async fn cached_health(key: &str) -> Option<bool> {
    let meta = entry(key);
    let guard = meta.health.lock().await;
    match guard.as_ref() {
        Some((at, value)) if at.elapsed() < METADATA_TTL => Some(*value),
        _ => None,
    }
}

/// 写入健康状态缓存。
pub async fn store_health(key: &str, healthy: bool) {
    let meta = entry(key);
    *meta.health.lock().await = Some((Instant::now(), healthy));
}

/// 读取模型列表缓存(未命中/过期返回 None)。
pub async fn cached_models(key: &str) -> Option<Result<Vec<String>, String>> {
    let meta = entry(key);
    let guard = meta.models.lock().await;
    match guard.as_ref() {
        Some((at, value)) if at.elapsed() < METADATA_TTL => Some(value.clone()),
        _ => None,
    }
}

/// 写入模型列表缓存。
pub async fn store_models(key: &str, models: Result<Vec<String>, String>) {
    let meta = entry(key);
    *meta.models.lock().await = Some((Instant::now(), models));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn local_pool_serializes_to_one() {
        let first = acquire(false).await.unwrap();
        // 第二个申请应排队:用 try_acquire 验证名额已满
        assert!(
            LOCAL_PERMITS.clone().try_acquire_owned().is_err(),
            "本机并发必须压到 1(GPU 排队比抢显存快)"
        );
        drop(first);
        assert!(LOCAL_PERMITS.clone().try_acquire_owned().is_ok());
    }

    #[tokio::test]
    async fn metadata_cache_hits_within_ttl_and_invalidates() {
        let key = "test-provider";
        store_health(key, true).await;
        assert_eq!(cached_health(key).await, Some(true));
        store_models(key, Ok(vec!["m1".into()])).await;
        assert!(cached_models(key).await.is_some());
        invalidate();
        // invalidate 清空缓存
        assert_eq!(cached_health(key).await, None);
        assert!(cached_models(key).await.is_none());
    }

    #[test]
    fn revision_monotonically_increases() {
        let before = revision();
        invalidate();
        assert!(revision() > before);
    }
}