Skip to main content

pagesift_api/
limits.rs

1//! 按 API key 的限流和配额:每分钟请求数(令牌桶)、同时进行的任务数、每天(UTC)的页面数。
2//! 超限时返回 Firecrawl 兼容的 429 错误体。没配置任何限额时什么都不检查。
3//!
4//! 当天已用的页数写进存储(有的话),重启不会把配额清零。
5//!
6//! 多服务器网关上([`Limiter::with_ledger`])每分钟请求数和每日页数改记在 `pagesift-cluster` 的账本里:
7//! 用量是追加的冻结 / 结算记录,崩溃重放、温备接管后一致;令牌桶在账本里,接管时由最近一分钟的
8//! 请求记录重建。单主机不经过账本,行为不变。
9
10use std::collections::HashMap;
11use std::sync::{Arc, Mutex};
12use std::time::{Instant, SystemTime, UNIX_EPOCH};
13
14use pagesift_store::Store;
15
16use crate::error::{ApiError, ErrorCode};
17
18/// 一个 API key 的限额,各项不设就是不限。
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
20pub struct KeyLimits {
21    /// 日志、监控和配额记录里用的名字;不设时用 key 的指纹,key 本身不会写进日志或磁盘。
22    pub name: Option<String>,
23    pub requests_per_minute: Option<u32>,
24    pub max_concurrent_jobs: Option<usize>,
25    pub pages_per_day: Option<u64>,
26}
27
28/// 发起请求的调用方:鉴权中间件放进请求扩展里。
29#[derive(Debug, Clone)]
30pub(crate) struct Caller {
31    pub account: Arc<str>,
32    pub limits: Arc<KeyLimits>,
33}
34
35impl Caller {
36    pub(crate) fn new(key: Option<&str>, limits: Arc<KeyLimits>) -> Self {
37        let account = match (&limits.name, key) {
38            (Some(n), _) => n.clone(),
39            (None, Some(k)) => fingerprint(k),
40            (None, None) => "anonymous".into(),
41        };
42        Self {
43            account: account.into(),
44            limits,
45        }
46    }
47}
48
49impl Caller {
50    /// 恢复任务时按保存的名字找回调用方。
51    pub(crate) fn named(account: &str, limits: Arc<KeyLimits>) -> Self {
52        Self {
53            account: account.into(),
54            limits,
55        }
56    }
57}
58
59/// key 的指纹(FNV-1a 64 位),稳定、不可逆到能用的程度,用来区分调用方。
60fn fingerprint(key: &str) -> String {
61    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
62    for b in key.bytes() {
63        h ^= u64::from(b);
64        h = h.wrapping_mul(0x0100_0000_01b3);
65    }
66    format!("key-{h:016x}")
67}
68
69struct Bucket {
70    tokens: f64,
71    last: Instant,
72}
73
74#[derive(Clone, Copy)]
75struct Usage {
76    day: u64,
77    /// 已结算、真正消费掉的页数;写盘只写这个值。
78    settled: u64,
79    /// 已预留、还没结算的页数。只在内存里,进程重启即消失,绝不写盘。
80    held: u64,
81}
82
83impl Usage {
84    fn used(&self) -> u64 {
85        self.settled + self.held
86    }
87}
88
89#[derive(Default)]
90pub(crate) struct Limiter {
91    buckets: Mutex<HashMap<Arc<str>, Bucket>>,
92    usage: Mutex<HashMap<Arc<str>, Usage>>,
93    store: Option<Store>,
94    ledger: Option<Arc<pagesift_cluster::Ledger>>,
95    /// 每个账户一把写盘锁:同一账户的配额快照按顺序落盘,旧的快照不会覆盖新的。
96    write_locks: Mutex<HashMap<Arc<str>, Arc<tokio::sync::Mutex<()>>>>,
97}
98
99fn rate_error(rpm: u32, wait: u64) -> ApiError {
100    ApiError::new(
101        ErrorCode::RateLimitExceeded,
102        format!(
103            "rate limit exceeded: {rpm} requests per minute for this API key; retry after {wait}s"
104        ),
105    )
106    .with_retry_after(wait)
107}
108
109fn ledger_error(e: impl std::fmt::Display) -> ApiError {
110    ApiError::new(ErrorCode::Internal, format!("quota ledger: {e}"))
111}
112
113fn today() -> u64 {
114    SystemTime::now()
115        .duration_since(UNIX_EPOCH)
116        .map_or(0, |d| d.as_secs() / 86_400)
117}
118
119fn secs_to_midnight() -> u64 {
120    let now = SystemTime::now()
121        .duration_since(UNIX_EPOCH)
122        .map_or(0, |d| d.as_secs());
123    86_400 - now % 86_400
124}
125
126fn usage_key(account: &str) -> String {
127    format!("quota/{account}")
128}
129
130impl Limiter {
131    pub(crate) fn new(store: Option<Store>) -> Self {
132        Self {
133            store,
134            ..Default::default()
135        }
136    }
137
138    /// 网关:限额记在账本里。`accounts` 是全部账户(名字 → 限额),事先登记。
139    pub(crate) fn with_ledger(
140        ledger: Arc<pagesift_cluster::Ledger>,
141        accounts: &HashMap<String, Arc<KeyLimits>>,
142    ) -> Self {
143        for (name, l) in accounts {
144            ledger.set_limits(
145                name,
146                pagesift_cluster::AccountLimits {
147                    pages_per_day: l.pages_per_day,
148                    requests_per_minute: l.requests_per_minute,
149                },
150            );
151        }
152        Self {
153            ledger: Some(ledger),
154            ..Default::default()
155        }
156    }
157
158    /// 每分钟请求数:容量等于每分钟请求数的令牌桶,匀速补充,允许短时突发。
159    pub(crate) fn check_rate(&self, c: &Caller) -> Result<(), ApiError> {
160        let Some(rpm) = c.limits.requests_per_minute else {
161            return Ok(());
162        };
163        if let Some(l) = &self.ledger {
164            use pagesift_cluster::Refusal;
165            return match l.check_rate(&c.account) {
166                Ok(()) => Ok(()),
167                Err(Refusal::Rate { retry_after, .. }) => Err(rate_error(
168                    rpm,
169                    retry_after.as_secs_f64().ceil().max(1.0) as u64,
170                )),
171                Err(Refusal::Quota { .. }) => Err(quota_error(c)),
172                Err(Refusal::Error(e)) => Err(ledger_error(e)),
173            };
174        }
175        let cap = f64::from(rpm.max(1));
176        let per_sec = cap / 60.0;
177        let now = Instant::now();
178        let mut buckets = self.buckets.lock().unwrap_or_else(|e| e.into_inner());
179        let b = buckets.entry(c.account.clone()).or_insert(Bucket {
180            tokens: cap,
181            last: now,
182        });
183        b.tokens = (b.tokens + now.duration_since(b.last).as_secs_f64() * per_sec).min(cap);
184        b.last = now;
185        if b.tokens >= 1.0 {
186            b.tokens -= 1.0;
187            return Ok(());
188        }
189        let wait = ((1.0 - b.tokens) / per_sec).ceil().max(1.0) as u64;
190        Err(rate_error(rpm, wait))
191    }
192
193    /// 同时进行的任务数是否还能再加一个。
194    pub(crate) fn check_jobs(&self, c: &Caller, active: usize) -> Result<(), ApiError> {
195        match c.limits.max_concurrent_jobs {
196            Some(max) if active >= max => Err(ApiError::new(
197                ErrorCode::ConcurrencyLimitExceeded,
198                format!("concurrency limit exceeded: at most {max} crawl or batch jobs may run at once for this API key"),
199            )
200            .with_retry_after(5)),
201            _ => Ok(()),
202        }
203    }
204
205    fn load_usage(&self, account: &Arc<str>, day: u64) -> Usage {
206        // 第一次用到这个调用方(或者跨天了):从存储里读当天的记录
207        let stored = self
208            .store
209            .as_ref()
210            .and_then(|s| s.tasks().kv_get(usage_key(account)).ok().flatten())
211            .and_then(|v| {
212                let text = String::from_utf8(v).ok()?;
213                let (d, n) = text.split_once(' ')?;
214                Some(Usage {
215                    day: d.parse().ok()?,
216                    settled: n.parse().ok()?,
217                    held: 0,
218                })
219            })
220            .filter(|u| u.day == day);
221        stored.unwrap_or(Usage {
222            day,
223            settled: 0,
224            held: 0,
225        })
226    }
227
228    fn usage_entry<'a>(
229        &self,
230        map: &'a mut HashMap<Arc<str>, Usage>,
231        account: &Arc<str>,
232        day: u64,
233    ) -> &'a mut Usage {
234        if map.get(account).is_none_or(|u| u.day != day) {
235            let u = self.load_usage(account, day);
236            map.insert(account.clone(), u);
237        }
238        map.get_mut(account).expect("inserted above")
239    }
240
241    fn usage(&self, account: &Arc<str>) -> Usage {
242        let day = today();
243        let mut map = self.usage.lock().unwrap_or_else(|e| e.into_inner());
244        *self.usage_entry(&mut map, account, day)
245    }
246
247    /// 当天还剩多少页;没设配额时是 `None`。已预留但还没结算的页也算在用量里。
248    pub(crate) fn remaining_pages(&self, c: &Caller) -> Option<u64> {
249        let max = c.limits.pages_per_day?;
250        if let Some(l) = &self.ledger {
251            return l.balance(&c.account);
252        }
253        Some(max.saturating_sub(self.usage(&c.account).used()))
254    }
255
256    pub(crate) fn check_quota(&self, c: &Caller) -> Result<(), ApiError> {
257        if self.remaining_pages(c) == Some(0) {
258            return Err(quota_error(c));
259        }
260        Ok(())
261    }
262
263    /// 可计费操作发出之前原子地预留一页。返回的 [`PageHold`] 在成功时 `commit`,
264    /// 失败/取消时直接丢弃即自动退还([`Drop`] 里完成),不会出现 check-then-act 的并发窗口。
265    ///
266    /// 账本模式下,进程在预留尚未结算时崩溃,冻结会保留到当天结束(UTC):最多是崩溃瞬间
267    /// 在途的页数,不会双重扣费;单机模式的内存预留则随进程消失。
268    pub(crate) async fn reserve_page(self: &Arc<Self>, c: &Caller) -> Result<PageHold, ApiError> {
269        let day = today();
270        let amount = 1;
271        let Some(_) = c.limits.pages_per_day else {
272            return Ok(PageHold {
273                limiter: self.clone(),
274                account: c.account.clone(),
275                day,
276                amount,
277                ledger: None,
278                tracked: false,
279                settled: false,
280            });
281        };
282        if let Some(ledger) = &self.ledger {
283            let hold = ledger
284                .reserve(&c.account, amount)
285                .map_err(|err| match err {
286                    pagesift_cluster::Refusal::Error(err) => ledger_error(err),
287                    _ => quota_error(c),
288                })?;
289            return Ok(PageHold {
290                limiter: self.clone(),
291                account: c.account.clone(),
292                day,
293                amount,
294                ledger: Some(hold),
295                tracked: true,
296                settled: false,
297            });
298        }
299        let max = c.limits.pages_per_day.unwrap_or(0);
300        {
301            let mut map = self.usage.lock().unwrap_or_else(|e| e.into_inner());
302            let u = self.usage_entry(&mut map, &c.account, day);
303            if u.used().saturating_add(amount) > max {
304                return Err(quota_error(c));
305            }
306            u.held += amount;
307        }
308        Ok(PageHold {
309            limiter: self.clone(),
310            account: c.account.clone(),
311            day,
312            amount,
313            ledger: None,
314            tracked: true,
315            settled: false,
316        })
317    }
318
319    /// 退还预留(没有结算就丢弃)。只退当天的记录,跨天时旧记录已经作废。
320    fn refund(&self, account: &Arc<str>, day: u64, amount: u64) {
321        let mut map = self.usage.lock().unwrap_or_else(|e| e.into_inner());
322        if let Some(u) = map.get_mut(account)
323            && u.day == day
324        {
325            u.held = u.held.saturating_sub(amount);
326        }
327    }
328
329    /// 结算:把 `amount` 从在途预留移到已结算,然后只持久化已结算值。
330    /// 同一账户的写盘用一把锁串行化,保证旧快照不会在新快照之后落盘。
331    async fn settle_usage(&self, account: &Arc<str>, day: u64, amount: u64) {
332        let lock = {
333            let mut locks = self.write_locks.lock().unwrap_or_else(|e| e.into_inner());
334            locks
335                .entry(account.clone())
336                .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
337                .clone()
338        };
339        let _guard = lock.lock().await;
340        let (day, settled) = {
341            let mut map = self.usage.lock().unwrap_or_else(|e| e.into_inner());
342            let Some(u) = map.get_mut(account) else {
343                return;
344            };
345            // 跨天:旧日的额度已经作废,不再记到新的一天
346            if u.day != day {
347                return;
348            }
349            u.held = u.held.saturating_sub(amount);
350            u.settled = u.settled.saturating_add(amount);
351            (u.day, u.settled)
352        };
353        self.write_usage(account, day, settled).await;
354    }
355
356    async fn write_usage(&self, account: &Arc<str>, day: u64, settled: u64) {
357        let Some(store) = &self.store else {
358            return;
359        };
360        let key = usage_key(account);
361        let value = format!("{day} {settled}");
362        let r = store
363            .blocking(move |s| s.tasks().kv_put(key, value.as_bytes()))
364            .await;
365        if let Err(e) = r {
366            tracing::warn!(error = %e, "saving page quota usage failed");
367        }
368    }
369}
370
371/// 一页的预留。`commit` 结算;未结算就丢弃时自动退还(失败、取消、panic 都走同一条路)。
372pub(crate) struct PageHold {
373    limiter: Arc<Limiter>,
374    account: Arc<str>,
375    day: u64,
376    amount: u64,
377    ledger: Option<pagesift_cluster::Hold>,
378    tracked: bool,
379    settled: bool,
380}
381
382impl PageHold {
383    pub(crate) async fn commit(mut self) {
384        self.settled = true;
385        if !self.tracked {
386            return;
387        }
388        if let Some(ledger) = &self.limiter.ledger {
389            if let Some(hold) = self.ledger.take()
390                && let Err(e) = ledger.settle(&hold, self.amount)
391            {
392                tracing::warn!(error = %e, "settling page quota failed");
393            }
394            return;
395        }
396        self.limiter
397            .settle_usage(&self.account, self.day, self.amount)
398            .await;
399    }
400}
401
402impl Drop for PageHold {
403    fn drop(&mut self) {
404        if self.settled || !self.tracked {
405            return;
406        }
407        if let Some(ledger) = &self.limiter.ledger {
408            if let Some(hold) = self.ledger.take()
409                && let Err(e) = ledger.release(&hold)
410            {
411                tracing::warn!(error = %e, "releasing page quota failed");
412            }
413            return;
414        }
415        self.limiter.refund(&self.account, self.day, self.amount);
416    }
417}
418
419pub(crate) fn quota_error(c: &Caller) -> ApiError {
420    ApiError::new(
421        ErrorCode::QuotaExceeded,
422        format!(
423            "daily page quota exceeded: {} pages per day for this API key",
424            c.limits.pages_per_day.unwrap_or(0)
425        ),
426    )
427    .with_retry_after(secs_to_midnight())
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    fn caller(limits: KeyLimits) -> Caller {
435        Caller::new(Some("secret-key"), Arc::new(limits))
436    }
437
438    #[test]
439    fn token_bucket_allows_a_burst_then_429s() {
440        let l = Limiter::default();
441        let c = caller(KeyLimits {
442            requests_per_minute: Some(3),
443            ..Default::default()
444        });
445        for _ in 0..3 {
446            l.check_rate(&c).unwrap();
447        }
448        let e = l.check_rate(&c).unwrap_err();
449        assert_eq!(e.code, ErrorCode::RateLimitExceeded);
450        assert!(e.retry_after.unwrap() >= 1);
451        // 别的调用方有自己的桶
452        let other = Caller::new(Some("other"), c.limits.clone());
453        l.check_rate(&other).unwrap();
454        // 不限速的调用方不受影响
455        l.check_rate(&caller(KeyLimits::default())).unwrap();
456    }
457
458    #[test]
459    fn accounts_use_names_or_fingerprints() {
460        let named = Caller::new(
461            Some("k"),
462            Arc::new(KeyLimits {
463                name: Some("team-a".into()),
464                ..Default::default()
465            }),
466        );
467        assert_eq!(&*named.account, "team-a");
468        let anon = caller(KeyLimits::default());
469        assert!(anon.account.starts_with("key-") && !anon.account.contains("secret"));
470        assert_eq!(&*Caller::new(None, Arc::default()).account, "anonymous");
471    }
472
473    /// 网关:限额记在账本里,行为与单主机一致(桶先突发后 429,配额用完 QUOTA_EXCEEDED)。
474    #[tokio::test]
475    async fn ledger_backed_limits_match_the_local_ones() {
476        let ledger = Arc::new(
477            pagesift_cluster::Ledger::open(pagesift_cluster::LedgerConfig::new(
478                pagesift_cluster::MonoClock::new(),
479                false,
480            ))
481            .unwrap(),
482        );
483        let limits = Arc::new(KeyLimits {
484            requests_per_minute: Some(2),
485            pages_per_day: Some(2),
486            ..Default::default()
487        });
488        let c = Caller::new(Some("k"), limits.clone());
489        let accounts = HashMap::from([(c.account.to_string(), limits)]);
490        let l = Arc::new(Limiter::with_ledger(ledger, &accounts));
491        l.check_rate(&c).unwrap();
492        l.check_rate(&c).unwrap();
493        let e = l.check_rate(&c).unwrap_err();
494        assert_eq!(e.code, ErrorCode::RateLimitExceeded);
495        assert!(e.retry_after.unwrap() >= 1);
496        assert_eq!(l.remaining_pages(&c), Some(2));
497        l.reserve_page(&c).await.unwrap().commit().await;
498        assert!(l.check_quota(&c).is_ok());
499        l.reserve_page(&c).await.unwrap().commit().await;
500        assert_eq!(l.remaining_pages(&c), Some(0));
501        assert_eq!(
502            l.check_quota(&c).unwrap_err().code,
503            ErrorCode::QuotaExceeded
504        );
505        // 不限额的调用方不受影响
506        l.check_rate(&caller(KeyLimits::default())).unwrap();
507    }
508
509    #[tokio::test]
510    async fn concurrent_page_reservations_stop_at_quota() {
511        let c = caller(KeyLimits {
512            pages_per_day: Some(1),
513            ..Default::default()
514        });
515        let limiter = Arc::new(Limiter::new(None));
516        let (a, b) = tokio::join!(limiter.reserve_page(&c), limiter.reserve_page(&c));
517        assert_ne!(a.is_ok(), b.is_ok());
518        assert_eq!(limiter.remaining_pages(&c), Some(0));
519        // 丢掉未结算的预留:额度回来
520        drop(a);
521        drop(b);
522        assert_eq!(limiter.remaining_pages(&c), Some(1));
523    }
524
525    #[tokio::test]
526    async fn twenty_concurrent_reservations_allow_exactly_one() {
527        let c = Arc::new(caller(KeyLimits {
528            pages_per_day: Some(1),
529            ..Default::default()
530        }));
531        let limiter = Arc::new(Limiter::new(None));
532        let mut tasks = Vec::new();
533        for _ in 0..20 {
534            let (limiter, c) = (limiter.clone(), c.clone());
535            tasks.push(tokio::spawn(async move { limiter.reserve_page(&c).await }));
536        }
537        let mut granted = 0;
538        let mut holds = Vec::new();
539        for task in tasks {
540            match task.await.unwrap() {
541                Ok(hold) => {
542                    granted += 1;
543                    holds.push(hold);
544                }
545                Err(e) => assert_eq!(e.code, ErrorCode::QuotaExceeded),
546            }
547        }
548        assert_eq!(granted, 1);
549        assert_eq!(limiter.remaining_pages(&c), Some(0));
550        drop(holds);
551        assert_eq!(limiter.remaining_pages(&c), Some(1));
552    }
553
554    #[tokio::test]
555    async fn committed_pages_survive_restart_and_holds_refund() {
556        let dir = tempfile::tempdir().unwrap();
557        let c = caller(KeyLimits {
558            pages_per_day: Some(2),
559            ..Default::default()
560        });
561        let limiter = Arc::new(Limiter::new(Some(Store::open_dir(dir.path()).unwrap())));
562        let hold = limiter.reserve_page(&c).await.unwrap();
563        assert_eq!(limiter.remaining_pages(&c), Some(1));
564        drop(hold);
565        assert_eq!(limiter.remaining_pages(&c), Some(2));
566        let hold = limiter.reserve_page(&c).await.unwrap();
567        hold.commit().await;
568        assert_eq!(limiter.remaining_pages(&c), Some(1));
569        drop(limiter);
570        // 重启:已结算的 1 页还在,未结算的预留不落盘
571        let limiter = Limiter::new(Some(Store::open_dir(dir.path()).unwrap()));
572        assert_eq!(limiter.remaining_pages(&c), Some(1));
573    }
574
575    #[tokio::test]
576    async fn failed_reservation_is_not_persisted() {
577        let dir = tempfile::tempdir().unwrap();
578        let c = caller(KeyLimits {
579            pages_per_day: Some(2),
580            ..Default::default()
581        });
582        let limiter = Arc::new(Limiter::new(Some(Store::open_dir(dir.path()).unwrap())));
583        let a = limiter.reserve_page(&c).await.unwrap();
584        let b = limiter.reserve_page(&c).await.unwrap();
585        a.commit().await;
586        // B 失败:只退内存里的在途预留,磁盘上不能留下这一页
587        drop(b);
588        assert_eq!(limiter.remaining_pages(&c), Some(1));
589        drop(limiter);
590        let limiter = Limiter::new(Some(Store::open_dir(dir.path()).unwrap()));
591        assert_eq!(limiter.remaining_pages(&c), Some(1));
592    }
593
594    #[tokio::test]
595    async fn concurrent_commits_persist_every_settled_page_in_order() {
596        let dir = tempfile::tempdir().unwrap();
597        let c = Arc::new(caller(KeyLimits {
598            pages_per_day: Some(20),
599            ..Default::default()
600        }));
601        let limiter = Arc::new(Limiter::new(Some(Store::open_dir(dir.path()).unwrap())));
602        let mut tasks = Vec::new();
603        for _ in 0..20 {
604            let (limiter, c) = (limiter.clone(), c.clone());
605            tasks.push(tokio::spawn(async move {
606                let hold = limiter.reserve_page(&c).await.unwrap();
607                hold.commit().await;
608            }));
609        }
610        for task in tasks {
611            task.await.unwrap();
612        }
613        assert_eq!(limiter.remaining_pages(&c), Some(0));
614        drop(limiter);
615        // 任何一次乱序写盘都会让重启后的已用页数少于 20
616        let limiter = Limiter::new(Some(Store::open_dir(dir.path()).unwrap()));
617        assert_eq!(limiter.remaining_pages(&c), Some(0));
618    }
619
620    #[tokio::test]
621    async fn ledger_reservations_are_atomic_across_limiter_instances() {
622        let ledger = Arc::new(
623            pagesift_cluster::Ledger::open(pagesift_cluster::LedgerConfig::new(
624                pagesift_cluster::MonoClock::new(),
625                false,
626            ))
627            .unwrap(),
628        );
629        let limits = Arc::new(KeyLimits {
630            pages_per_day: Some(1),
631            ..Default::default()
632        });
633        let c = Arc::new(Caller::new(Some("k"), limits.clone()));
634        let accounts = HashMap::from([(c.account.to_string(), limits)]);
635        let a = Arc::new(Limiter::with_ledger(ledger.clone(), &accounts));
636        let b = Arc::new(Limiter::with_ledger(ledger.clone(), &accounts));
637        let mut tasks = Vec::new();
638        for i in 0..20 {
639            let (l, c) = (if i % 2 == 0 { a.clone() } else { b.clone() }, c.clone());
640            tasks.push(tokio::spawn(async move { l.reserve_page(&c).await }));
641        }
642        let mut holds = Vec::new();
643        let mut granted = 0;
644        for task in tasks {
645            match task.await.unwrap() {
646                Ok(hold) => {
647                    granted += 1;
648                    holds.push(hold);
649                }
650                Err(e) => assert_eq!(e.code, ErrorCode::QuotaExceeded),
651            }
652        }
653        assert_eq!(granted, 1);
654        assert_eq!(a.remaining_pages(&c), Some(0));
655        drop(holds);
656        assert_eq!(a.remaining_pages(&c), Some(1));
657    }
658
659    #[tokio::test]
660    async fn page_quota_survives_a_restart() {
661        let dir = tempfile::tempdir().unwrap();
662        let c = caller(KeyLimits {
663            pages_per_day: Some(5),
664            ..Default::default()
665        });
666        {
667            let store = Store::open_dir(dir.path()).unwrap();
668            let l = Arc::new(Limiter::new(Some(store)));
669            assert_eq!(l.remaining_pages(&c), Some(5));
670            for _ in 0..4 {
671                l.reserve_page(&c).await.unwrap().commit().await;
672            }
673            assert!(l.check_quota(&c).is_ok());
674            l.reserve_page(&c).await.unwrap().commit().await;
675            assert_eq!(
676                l.check_quota(&c).unwrap_err().code,
677                ErrorCode::QuotaExceeded
678            );
679        }
680        let l = Limiter::new(Some(Store::open_dir(dir.path()).unwrap()));
681        assert_eq!(l.remaining_pages(&c), Some(0));
682        let jobs = caller(KeyLimits {
683            max_concurrent_jobs: Some(1),
684            ..Default::default()
685        });
686        assert!(l.check_jobs(&jobs, 0).is_ok());
687        assert_eq!(
688            l.check_jobs(&jobs, 1).unwrap_err().code,
689            ErrorCode::ConcurrencyLimitExceeded
690        );
691    }
692}