SqlitePool を .clone() すると何が起きるのか
Rust の Clone トレイトの仕組みと、コネクションプールが「安く」複製できるカラクリ
1. 前提: このプロジェクトでのプールの使われ方
このプロジェクト (fx bot) では、起動時に SQLite のコネクションプールを 1 個だけ作り、HTTP サーバー (axum) に渡しています。
src/setup.rspub 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 { ... }
/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 つの流派があります。
Arc は Atomically 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 → ここで初めて本体が解放される
4. SqlitePool の正体は「Arc の薄い皮」
本題です。sqlx の Pool (= SqlitePool) の定義は、実質これだけです。
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 された各ハンドルは全員そこを指しています。
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 を見てみます。
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 されるだけなので、秒間何千リクエスト来ても問題ありません。
実行の流れを追うとこうなります。
pool.acquire()) を借りたまま長時間握り続けると、clone がいくつあっても全員が同じ待ち行列に並ぶため、他のリクエストが接続待ちで詰まります。「clone = 独立したプールが増える」ではない点に注意。
このプロジェクトのもう 1 つの好例が main.rs です。backtest::runner::run(file, &pool) のように &SqlitePool (参照) で渡している箇所は clone すら不要なパターンで、所有権ごと渡す必要がある http::listen(port, pool) だけが move で渡しています。「借りるだけで済むなら参照、別の場所に長く持たせるなら clone (or move)」という Rust の使い分けがそのまま現れています。
6. まとめ
- Rust の
.clone()は型ごとに実装が違う。「重いコピー」とは限らない。 SqlitePoolはArc<PoolInner>の薄いラッパーで、clone は参照カウンタ +1 だけ。DB 接続は 1 本も増えない。- だから axum がリクエストごとに state を clone しても実質タダ。プール本体 (接続の束・上限・待ち行列) は常にアプリ全体で 1 個を共有している。