SqlitePool を .clone() すると何が起きるのか

Rust の Clone トレイトの仕組みと、コネクションプールが「安く」複製できるカラクリ

1. 前提: このプロジェクトでのプールの使われ方

このプロジェクト (fx bot) では、起動時に SQLite のコネクションプールを 1 個だけ作り、HTTP サーバー (axum) に渡しています。

src/setup.rs
pub async fn setup_db() -> anyhow::Result<SqlitePool> {
    let database_url = std::env::var("DATABASE_URL")?;
    let pool = SqlitePool::connect(&database_url).await?;
    Ok(pool)
}
src/http.rs
pub async fn listen(port: u16, pool: SqlitePool) {
    let app = Router::new()
        .route("/health", get(health))
        .route("/trade", post(trade))
        .with_state(pool);   // ← プールを axum に預ける
    ...
}

async fn trade(
    State(pool): State<SqlitePool>,   // ← リクエストごとに clone されて届く
    Json(req_body): Json<models::ClientRequest>,
) -> impl IntoResponse { ... }
疑問 axum は /trade にリクエストが来るたびに state を .clone() してハンドラに渡します。 「毎回プールを丸ごとコピーしてるの? DB接続が増殖するの? 重くないの?」— これが今回のテーマです。

2. Rust の clone の仕組み — 「勝手にコピーしない」言語

多くの言語では、変数を代入すると裏で自動的に参照が共有されたりコピーされたりします。Rust は逆で、代入は原則「所有権の引っ越し (move)」です。コピーしたければ、明示的に .clone() と書かなければいけません。

let a = String::from("hello");
let b = a;            // move: a は空き家になり、もう使えない
// println!("{}", a); // コンパイルエラー!

let c = String::from("world");
let d = c.clone();    // 明示的なコピー。c も d も使える

.clone() は魔法の組み込み機能ではなく、ただのトレイト (インターフェースのようなもの) のメソッドです。

pub trait Clone {
    fn clone(&self) -> Self;   // 「自分と同じ型の値を新しく作って返す」
}
ポイント clone() の中身は型ごとに自由に実装できる。 「clone = 重いコピー」とは限らず、その型が clone をどう実装しているかで意味もコストも変わります。 これが今回の話の核心です。

3. 深いクローンと浅いクローン — Vec と Arc の違い

clone の実装には大きく 2 つの流派があります。

深いクローン (Vec, String) v: Vec ptr, len, cap [1, 2, 3, ...] v2 = v.clone() ptr', len, cap [1, 2, 3, ...] 中身を丸ごと複製 = 要素数に比例して重い 参照カウント式クローン (Arc) a: Arc<T> ptr (8バイト) b = a.clone() ptr (同じ場所) 本体データ (1個だけ) refcount: 1 → 2 カウンタを +1 するだけ = 一瞬で終わる
図1: Vec の clone は本体ごと複製、Arc の clone はカウンタを増やして同じ本体を指すだけ

ArcAtomically Reference Counted の略で、「何人がこのデータを共有しているか」をカウンタで管理するスマートポインタです。

use std::sync::Arc;

let a = Arc::new(vec![1, 2, 3]);   // 本体はヒープに 1 個。refcount = 1
let b = a.clone();                 // カウンタ +1 するだけ。refcount = 2
let c = Arc::clone(&a);            // 同じ意味の別の書き方。refcount = 3

drop(b);                           // refcount = 2
drop(c);                           // refcount = 1
drop(a);                           // refcount = 0 → ここで初めて本体が解放される
たとえ話 Vec の clone は「教科書をコピー機で丸ごと複写して 2 冊にする」。 Arc の clone は「図書館の同じ 1 冊に貸出カードの名前を 1 人追加する」。 最後の 1 人が返却 (drop) したときに本が処分されます。

4. SqlitePool の正体は「Arc の薄い皮」

本題です。sqlx の Pool (= SqlitePool) の定義は、実質これだけです。

sqlx-core/src/pool/mod.rs (抜粋・簡略化)
pub struct Pool<DB: Database>(pub(crate) Arc<PoolInner<DB>>);

impl<DB: Database> Clone for Pool<DB> {
    fn clone(&self) -> Self {
        Pool(Arc::clone(&self.0))   // ← やっているのはこれだけ
    }
}

つまり pool.clone() は、DB 接続を 1 本も増やしません。実際の接続の束・待ち行列・設定はすべて PoolInner という本体に 1 個だけ存在し、clone された各ハンドルは全員そこを指しています。

pool (main.rs) ハンドル = ただのポインタ pool.clone() リクエスト A のハンドラ pool.clone() リクエスト B のハンドラ Arc<PoolInner>(本体・世界に1個) refcount: 3 接続 #1 接続 #2 接続 #3 … 空き接続の待ち行列 / 最大接続数などの設定 / セマフォ SQLite (db.sqlite3)
図2: clone で増えるのは左上の「ハンドル」だけ。接続の束 (PoolInner) は常に 1 個

sqlx のドキュメントにも明記されています: "Pool is Send, Sync and Clone. It is intended to be created once at the start of your application and then shared." (Pool は clone して共有する前提で設計されている)

pool.clone() がやること やらないこと
コスト ポインタ 1 個のコピー + 原子カウンタ +1 (数ナノ秒) メモリの大きな複製
DB 接続 既存の接続の束を共有 新しい接続を開くこと
設定・上限 max_connections などの制限も共有 (全体で 1 つ) クローンごとに上限が倍増すること

5. axum がリクエストごとに clone しても平気な理由

もう一度 src/http.rs を見てみます。

src/http.rs
let app = Router::new()
    .route("/trade", post(trade))
    .with_state(pool);          // axum が pool を保持

async fn trade(
    State(pool): State<SqlitePool>,   // 内部で state.clone() された結果
    ...
)

axum は State<S> エクストラクタのために S: Clone を要求し、リクエストが来るたびに state を clone してハンドラへ渡します。もし SqlitePool の clone が「深いクローン」だったら毎リクエストで大惨事ですが、実際は Arc のカウンタが +1 されるだけなので、秒間何千リクエスト来ても問題ありません。

実行の流れを追うとこうなります。

クライアント axum Router trade ハンドラ PoolInner POST /trade (価格データ) state.clone() → refcount +1 接続を1本借りてクエリ実行 接続をプールに返却 CSV レスポンス (ハンドラ終了 → refcount −1)
図3: 1 リクエストの一生。clone はカウンタの上げ下げ、接続は借りて返すだけ
注意: clone しても増えないからこその落とし穴 接続の上限はプール全体で共有です。ハンドラ内で接続 (pool.acquire()) を借りたまま長時間握り続けると、clone がいくつあっても全員が同じ待ち行列に並ぶため、他のリクエストが接続待ちで詰まります。「clone = 独立したプールが増える」ではない点に注意。

このプロジェクトのもう 1 つの好例が main.rs です。backtest::runner::run(file, &pool) のように &SqlitePool (参照) で渡している箇所は clone すら不要なパターンで、所有権ごと渡す必要がある http::listen(port, pool) だけが move で渡しています。「借りるだけで済むなら参照、別の場所に長く持たせるなら clone (or move)」という Rust の使い分けがそのまま現れています。

6. まとめ

3行まとめ
  1. Rust の .clone() は型ごとに実装が違う。「重いコピー」とは限らない。
  2. SqlitePoolArc<PoolInner> の薄いラッパーで、clone は参照カウンタ +1 だけ。DB 接続は 1 本も増えない。
  3. だから axum がリクエストごとに state を clone しても実質タダ。プール本体 (接続の束・上限・待ち行列) は常にアプリ全体で 1 個を共有している。