随笔一则

  • 作者: Hantong Chen
  • 2 分钟阅读
  • 标签: 
  • Rust
use std::{borrow::Cow, hash::Hash};

struct SniMap(HashMap<PeerKey<'static>, Peer>);

impl SniMap {
    /// Get peer by SNI.
    pub(crate) fn get<'s, 'sni: 's>(&'s self, sni: &'sni str) -> Option<&'s Peer> {
        self.0.get(&PeerKey::Exact(Cow::Borrowed(sni))).or_else(|| {
            self.0.get(&PeerKey::Subdomain(Cow::Borrowed(
                sni.split_once('.').map(|(_, prior)| prior).unwrap_or(sni),
            )))
        })
    }
}

#[derive(Debug, PartialEq, Eq)]
enum PeerKey<'k> {
    /// Match exactly the SNI. This is the default mode.
    ///
    /// Will be serialized or deserialized as `example.com`.
    Exact(Cow<'k, str>),

    /// Subdomain match
    ///
    /// Should be serialized or deserialized as `domain:example.com`.
    ///
    /// ## Notice
    ///
    /// Matches direct subdomains only; does not match nested subdomains
    /// (second-level or deeper).
    ///
    /// Take `domain:example.com` as an example, it will match `*.example.com`
    /// but not `*.*.example.com`, etc.
    Subdomain(Cow<'k, str>),
}

impl Hash for PeerKey<'_> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        match self {
            PeerKey::Exact(s) => s.hash(state),
            PeerKey::Subdomain(s) => {
                state.write(b"domain:");

                s.hash(state)
            }
        }
    }
}

struct Peer {
    // ...
}

非常平平无奇的代码. 于是有新的需求: 想缓存 get 的结果 (把按 domain:example.com 匹配到的 sub.example.comfull:sub.example.com 写入表内), 达到下次读哈希表一次就能把 Peer 读出来.

有这么几个方案:

  • 改用三方库 dashmap 提供的 DashMap, 类似 RwLock 但 API 层面 是 Lock-free 的.
  • 套一层 Mutex / RwLock.
  • RefCell 毙掉, 我们需要 Send + Sync, 应该用其线程安全版本: RwLock

但事实上都是不可行的, 为什么? 欢迎 Rust 初学者自行查阅资料 (问 AI 也行)~

答案 (实际上编译器已经提示了):

  • 对于具有内部可变性的 Cell 类似物, 子类型型变要求是不变 (invariant) 的1. 本例中 DashMap 内部用了 lock_api 库的 RwLock, 这玩意内部用了 UnsafeCell, 故相应字段涉及的泛型类型 K 要求是不变的.

  • 不能返回本地变量 (拿到的 &Peer 只能活得和上锁后拿到的 Guard 一样久). 起码在当前的 API 设计下不能这么干. 当然, 写出来 get_and_map 一类的奇怪 API 也不是不行.

评价: 写 Rust 的福报