atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git

Blame: crates/matrix-sdk-sqlite/src/connection.rs

7ff8adafe9e4atagen2026-10-22 12:30 UTC1
// Copyright 2025 The Matrix.org Foundation C.I.C.
7ff8adafe9e4atagen2026-10-22 12:30 UTC2
//
7ff8adafe9e4atagen2026-10-22 12:30 UTC3
// Licensed under the Apache License, Version 2.0 (the "License");
7ff8adafe9e4atagen2026-10-22 12:30 UTC4
// you may not use this file except in compliance with the License.
7ff8adafe9e4atagen2026-10-22 12:30 UTC5
// You may obtain a copy of the License at
7ff8adafe9e4atagen2026-10-22 12:30 UTC6
//
7ff8adafe9e4atagen2026-10-22 12:30 UTC7
//     http://www.apache.org/licenses/LICENSE-2.0
7ff8adafe9e4atagen2026-10-22 12:30 UTC8
//
7ff8adafe9e4atagen2026-10-22 12:30 UTC9
// Unless required by applicable law or agreed to in writing, software
7ff8adafe9e4atagen2026-10-22 12:30 UTC10
// distributed under the License is distributed on an "AS IS" BASIS,
7ff8adafe9e4atagen2026-10-22 12:30 UTC11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7ff8adafe9e4atagen2026-10-22 12:30 UTC12
// See the License for the specific language governing permissions and
7ff8adafe9e4atagen2026-10-22 12:30 UTC13
// limitations under the License.
7ff8adafe9e4atagen2026-10-22 12:30 UTC14
7ff8adafe9e4atagen2026-10-22 12:30 UTC15
//! An implementation of `deadpool` for `rusqlite`.
7ff8adafe9e4atagen2026-10-22 12:30 UTC16
//!
7ff8adafe9e4atagen2026-10-22 12:30 UTC17
//! Initially, we were using `deadpool-sqlite`, that is also using `rusqlite` as
7ff8adafe9e4atagen2026-10-22 12:30 UTC18
//! the SQLite interface. However, in the implementation of
7ff8adafe9e4atagen2026-10-22 12:30 UTC19
//! [`deadpool::managed::Manager`], when recycling an object (i.e. an SQLite
7ff8adafe9e4atagen2026-10-22 12:30 UTC20
//! connection), [a SQL query is run to detect whether the connection is still
7ff8adafe9e4atagen2026-10-22 12:30 UTC21
//! alive][connection-test]. It creates performance issues:
7ff8adafe9e4atagen2026-10-22 12:30 UTC22
//!
7ff8adafe9e4atagen2026-10-22 12:30 UTC23
//! 1. It runs a prepared SQL query, which has a non-negligle cost. Imagine each
7ff8adafe9e4atagen2026-10-22 12:30 UTC24
//!    connection is used to run on average one query; when recycled, a second
7ff8adafe9e4atagen2026-10-22 12:30 UTC25
//!    query was constantly run. Even if it's a simple query, it requires to
7ff8adafe9e4atagen2026-10-22 12:30 UTC26
//!    prepare a statement, to run and to query it.
7ff8adafe9e4atagen2026-10-22 12:30 UTC27
//! 2. The SQL query was run in a blocking task. Indeed,
7ff8adafe9e4atagen2026-10-22 12:30 UTC28
//!    `deadpool_runtime::spawn_blocking` is used (via
7ff8adafe9e4atagen2026-10-22 12:30 UTC29
//!    `deadpool_sync::SyncWrapper::interact`), which includes [blocking the
7ff8adafe9e4atagen2026-10-22 12:30 UTC30
//!    thread, acquiring a lock][spawn_blocking] etc. All this has more
7ff8adafe9e4atagen2026-10-22 12:30 UTC31
//!    performance cost.
7ff8adafe9e4atagen2026-10-22 12:30 UTC32
//!
7ff8adafe9e4atagen2026-10-22 12:30 UTC33
//! Measures have shown it is a performance bottleneck for us, especially on
7ff8adafe9e4atagen2026-10-22 12:30 UTC34
//! Android. Why specifically on Android and not other systems? This is still
7ff8adafe9e4atagen2026-10-22 12:30 UTC35
//! unclear at the time of writing (2025-11-11), despites having spent several
7ff8adafe9e4atagen2026-10-22 12:30 UTC36
//! days digging and trying to find an answer to this question.
7ff8adafe9e4atagen2026-10-22 12:30 UTC37
//!
7ff8adafe9e4atagen2026-10-22 12:30 UTC38
//! We have tried to use another approach to test the aliveness of the
7ff8adafe9e4atagen2026-10-22 12:30 UTC39
//! connections without running queries. It has involved patching `rusqlite` to
7ff8adafe9e4atagen2026-10-22 12:30 UTC40
//! add more bindings to SQLite, and patching `deadpool` itself, but without any
7ff8adafe9e4atagen2026-10-22 12:30 UTC41
//! successful results.
7ff8adafe9e4atagen2026-10-22 12:30 UTC42
//!
7ff8adafe9e4atagen2026-10-22 12:30 UTC43
//! Finally, we have started questioning the reason of this test: why testing
7ff8adafe9e4atagen2026-10-22 12:30 UTC44
//! whether the connection was still alive? After all, there is no reason a
7ff8adafe9e4atagen2026-10-22 12:30 UTC45
//! connection should die in our case:
7ff8adafe9e4atagen2026-10-22 12:30 UTC46
//!
7ff8adafe9e4atagen2026-10-22 12:30 UTC47
//! - all connections are local,
7ff8adafe9e4atagen2026-10-22 12:30 UTC48
//! - all interactions are behind [WAL], which is local only,
7ff8adafe9e4atagen2026-10-22 12:30 UTC49
//! - even if for an unknown reason the connection died, using it next time
7ff8adafe9e4atagen2026-10-22 12:30 UTC50
//!   would create an error… exactly what would happen when recycling the
7ff8adafe9e4atagen2026-10-22 12:30 UTC51
//!   connection.
7ff8adafe9e4atagen2026-10-22 12:30 UTC52
//!
7ff8adafe9e4atagen2026-10-22 12:30 UTC53
//! Consequently, we have created a new implementation of `deadpool` for
7ff8adafe9e4atagen2026-10-22 12:30 UTC54
//! `rusqlite` that doesn't test the aliveness of the connections when recycled.
7ff8adafe9e4atagen2026-10-22 12:30 UTC55
//! We assume they are all alive.
7ff8adafe9e4atagen2026-10-22 12:30 UTC56
//!
7ff8adafe9e4atagen2026-10-22 12:30 UTC57
//! This implementation is, at the time of writing (2025-11-11):
7ff8adafe9e4atagen2026-10-22 12:30 UTC58
//!
7ff8adafe9e4atagen2026-10-22 12:30 UTC59
//! - 3.5 times faster on Android than `deadpool-sqlite`, removing the lock and
7ff8adafe9e4atagen2026-10-22 12:30 UTC60
//!   thread contention entirely,
7ff8adafe9e4atagen2026-10-22 12:30 UTC61
//! - 2 times faster on iOS.
7ff8adafe9e4atagen2026-10-22 12:30 UTC62
//!
7ff8adafe9e4atagen2026-10-22 12:30 UTC63
//! [connection-test]: https://github.com/deadpool-rs/deadpool/blob/d6f7d58756f0cc7bdd1f3d54d820c1332d67e4d5/crates/deadpool-sqlite/src/lib.rs#L80-L100
7ff8adafe9e4atagen2026-10-22 12:30 UTC64
//! [spawn_blocking]: https://github.com/deadpool-rs/deadpool/blob/d6f7d58756f0cc7bdd1f3d54d820c1332d67e4d5/crates/deadpool-sync/src/lib.rs#L113-L131
7ff8adafe9e4atagen2026-10-22 12:30 UTC65
//! [WAL]: https://www.sqlite.org/wal.html
7ff8adafe9e4atagen2026-10-22 12:30 UTC66
7ff8adafe9e4atagen2026-10-22 12:30 UTC67
use std::{convert::Infallible, path::PathBuf, sync::Arc, time::Duration};
7ff8adafe9e4atagen2026-10-22 12:30 UTC68
7ff8adafe9e4atagen2026-10-22 12:30 UTC69
pub use deadpool::managed::reexports::*;
7ff8adafe9e4atagen2026-10-22 12:30 UTC70
use deadpool::managed::{self, Metrics, PoolConfig, RecycleError};
7ff8adafe9e4atagen2026-10-22 12:30 UTC71
use deadpool_sync::SyncWrapper;
7ff8adafe9e4atagen2026-10-22 12:30 UTC72
use tokio::sync::Mutex;
7ff8adafe9e4atagen2026-10-22 12:30 UTC73
use tracing::{info, warn};
7ff8adafe9e4atagen2026-10-22 12:30 UTC74
7ff8adafe9e4atagen2026-10-22 12:30 UTC75
/// The default runtime used by `matrix-sdk-sqlite` for `deadpool`.
7ff8adafe9e4atagen2026-10-22 12:30 UTC76
pub const RUNTIME: Runtime = Runtime::Tokio1;
7ff8adafe9e4atagen2026-10-22 12:30 UTC77
7ff8adafe9e4atagen2026-10-22 12:30 UTC78
deadpool::managed_reexports!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC79
    "matrix-sdk-sqlite",
7ff8adafe9e4atagen2026-10-22 12:30 UTC80
    Manager,
7ff8adafe9e4atagen2026-10-22 12:30 UTC81
    managed::Object<Manager>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC82
    rusqlite::Error,
7ff8adafe9e4atagen2026-10-22 12:30 UTC83
    Infallible
7ff8adafe9e4atagen2026-10-22 12:30 UTC84
);
7ff8adafe9e4atagen2026-10-22 12:30 UTC85
7ff8adafe9e4atagen2026-10-22 12:30 UTC86
/// Type representing a connection to SQLite from the [`Pool`].
7ff8adafe9e4atagen2026-10-22 12:30 UTC87
pub type Connection = Object;
7ff8adafe9e4atagen2026-10-22 12:30 UTC88
7ff8adafe9e4atagen2026-10-22 12:30 UTC89
/// [`Manager`][managed::Manager] for creating and recycling SQLite
7ff8adafe9e4atagen2026-10-22 12:30 UTC90
/// [`Connection`]s.
7ff8adafe9e4atagen2026-10-22 12:30 UTC91
#[derive(Debug)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC92
pub struct Manager {
7ff8adafe9e4atagen2026-10-22 12:30 UTC93
    pub(crate) database_path: PathBuf,
7ff8adafe9e4atagen2026-10-22 12:30 UTC94
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC95
7ff8adafe9e4atagen2026-10-22 12:30 UTC96
impl Manager {
7ff8adafe9e4atagen2026-10-22 12:30 UTC97
    /// Creates a new [`Manager`] for a database.
7ff8adafe9e4atagen2026-10-22 12:30 UTC98
    #[must_use]
7ff8adafe9e4atagen2026-10-22 12:30 UTC99
    pub fn new(database_path: PathBuf) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC100
        Self { database_path }
7ff8adafe9e4atagen2026-10-22 12:30 UTC101
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC102
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC103
7ff8adafe9e4atagen2026-10-22 12:30 UTC104
impl managed::Manager for Manager {
7ff8adafe9e4atagen2026-10-22 12:30 UTC105
    type Type = SyncWrapper<rusqlite::Connection>;
7ff8adafe9e4atagen2026-10-22 12:30 UTC106
    type Error = rusqlite::Error;
7ff8adafe9e4atagen2026-10-22 12:30 UTC107
7ff8adafe9e4atagen2026-10-22 12:30 UTC108
    async fn create(&self) -> Result<Self::Type, Self::Error> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC109
        let path = self.database_path.clone();
7ff8adafe9e4atagen2026-10-22 12:30 UTC110
        SyncWrapper::new(RUNTIME, move || rusqlite::Connection::open(path)).await
7ff8adafe9e4atagen2026-10-22 12:30 UTC111
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC112
7ff8adafe9e4atagen2026-10-22 12:30 UTC113
    async fn recycle(
7ff8adafe9e4atagen2026-10-22 12:30 UTC114
        &self,
7ff8adafe9e4atagen2026-10-22 12:30 UTC115
        conn: &mut Self::Type,
7ff8adafe9e4atagen2026-10-22 12:30 UTC116
        _: &Metrics,
7ff8adafe9e4atagen2026-10-22 12:30 UTC117
    ) -> managed::RecycleResult<Self::Error> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC118
        if conn.is_mutex_poisoned() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC119
            return Err(RecycleError::Message(
7ff8adafe9e4atagen2026-10-22 12:30 UTC120
                "Mutex is poisoned. Connection is considered unusable.".into(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC121
            ));
7ff8adafe9e4atagen2026-10-22 12:30 UTC122
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC123
7ff8adafe9e4atagen2026-10-22 12:30 UTC124
        Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC125
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC126
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC127
7ff8adafe9e4atagen2026-10-22 12:30 UTC128
/// Gracefully close the store-owned write connection.
7ff8adafe9e4atagen2026-10-22 12:30 UTC129
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC130
/// Callers are expected to remove the enclosing [`SqliteConnections`] from the
7ff8adafe9e4atagen2026-10-22 12:30 UTC131
/// store before calling this helper so no new write acquisitions can happen
7ff8adafe9e4atagen2026-10-22 12:30 UTC132
/// through the store API.
7ff8adafe9e4atagen2026-10-22 12:30 UTC133
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC134
/// 1. Waits for any in-flight write to complete by acquiring the lock.
7ff8adafe9e4atagen2026-10-22 12:30 UTC135
/// 2. Runs a WAL checkpoint (TRUNCATE) to flush pending data to the main
7ff8adafe9e4atagen2026-10-22 12:30 UTC136
///    database file and release WAL locks.
7ff8adafe9e4atagen2026-10-22 12:30 UTC137
/// 3. Drops the store-owned `Arc` on a blocking thread.
7ff8adafe9e4atagen2026-10-22 12:30 UTC138
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC139
/// This is still best effort: if another cloned `Arc` or an
7ff8adafe9e4atagen2026-10-22 12:30 UTC140
/// `OwnedMutexGuard<Connection>` is still alive elsewhere, the underlying
7ff8adafe9e4atagen2026-10-22 12:30 UTC141
/// SQLite connection may remain alive until that handle is dropped.
7ff8adafe9e4atagen2026-10-22 12:30 UTC142
pub async fn close_connection(write_connection: Arc<Mutex<Connection>>) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC143
    // Acquire the lock to wait for any in-flight write to complete.
7ff8adafe9e4atagen2026-10-22 12:30 UTC144
    let guard = write_connection.lock().await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC145
7ff8adafe9e4atagen2026-10-22 12:30 UTC146
    // Flush WAL and release locks while we still own the connection.
7ff8adafe9e4atagen2026-10-22 12:30 UTC147
    let _ = guard
7ff8adafe9e4atagen2026-10-22 12:30 UTC148
        .interact(|raw| {
7ff8adafe9e4atagen2026-10-22 12:30 UTC149
            raw.execute_batch("PRAGMA locking_mode = NORMAL; PRAGMA wal_checkpoint(TRUNCATE);")
7ff8adafe9e4atagen2026-10-22 12:30 UTC150
                .ok();
7ff8adafe9e4atagen2026-10-22 12:30 UTC151
        })
7ff8adafe9e4atagen2026-10-22 12:30 UTC152
        .await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC153
7ff8adafe9e4atagen2026-10-22 12:30 UTC154
    drop(guard);
7ff8adafe9e4atagen2026-10-22 12:30 UTC155
7ff8adafe9e4atagen2026-10-22 12:30 UTC156
    // Drop the store-owned Arc on a blocking thread.
7ff8adafe9e4atagen2026-10-22 12:30 UTC157
    let _ = tokio::task::spawn_blocking(move || drop(write_connection)).await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC158
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC159
7ff8adafe9e4atagen2026-10-22 12:30 UTC160
/// Live database connections held by each SQLite store.
7ff8adafe9e4atagen2026-10-22 12:30 UTC161
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC162
/// Wrapped in `Option<SqliteConnections>` guarded by a `Mutex` inside each
7ff8adafe9e4atagen2026-10-22 12:30 UTC163
/// store; `Some` means the store is active, `None` means it is closed.
7ff8adafe9e4atagen2026-10-22 12:30 UTC164
pub(crate) struct SqliteConnections {
7ff8adafe9e4atagen2026-10-22 12:30 UTC165
    /// The pool of read connections.
7ff8adafe9e4atagen2026-10-22 12:30 UTC166
    pub pool: Pool,
7ff8adafe9e4atagen2026-10-22 12:30 UTC167
    /// The dedicated write connection.
7ff8adafe9e4atagen2026-10-22 12:30 UTC168
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC169
    /// This lives behind `Arc<Mutex<_>>` so stores can clone the `Arc` and
7ff8adafe9e4atagen2026-10-22 12:30 UTC170
    /// obtain an `OwnedMutexGuard<Connection>` without holding the outer
7ff8adafe9e4atagen2026-10-22 12:30 UTC171
    /// `connections` mutex across await points.
7ff8adafe9e4atagen2026-10-22 12:30 UTC172
    pub write_connection: Arc<Mutex<Connection>>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC173
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC174
7ff8adafe9e4atagen2026-10-22 12:30 UTC175
/// Close a store by taking its connections out.
7ff8adafe9e4atagen2026-10-22 12:30 UTC176
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC177
/// After this returns, any new call to `read()` or `write()` through the
7ff8adafe9e4atagen2026-10-22 12:30 UTC178
/// store will fail with [`crate::error::Error::StoreClosed`] until
7ff8adafe9e4atagen2026-10-22 12:30 UTC179
/// [`reopen_connections`] is called.
7ff8adafe9e4atagen2026-10-22 12:30 UTC180
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC181
/// Idempotent: if the store is already closed this is a no-op.
7ff8adafe9e4atagen2026-10-22 12:30 UTC182
pub(crate) async fn close_connections(connections: &Mutex<Option<SqliteConnections>>, label: &str) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC183
    let mut guard = connections.lock().await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC184
    let Some(conns) = guard.take() else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC185
        // Already closed — idempotent.
7ff8adafe9e4atagen2026-10-22 12:30 UTC186
        return;
7ff8adafe9e4atagen2026-10-22 12:30 UTC187
    };
7ff8adafe9e4atagen2026-10-22 12:30 UTC188
7ff8adafe9e4atagen2026-10-22 12:30 UTC189
    let SqliteConnections { pool, write_connection } = conns;
7ff8adafe9e4atagen2026-10-22 12:30 UTC190
7ff8adafe9e4atagen2026-10-22 12:30 UTC191
    // Close the pool. Idle read connections are dropped immediately;
7ff8adafe9e4atagen2026-10-22 12:30 UTC192
    // in-flight reads complete and their connections are discarded (not
7ff8adafe9e4atagen2026-10-22 12:30 UTC193
    // recycled) on release. New pool.get() calls return PoolError::Closed.
7ff8adafe9e4atagen2026-10-22 12:30 UTC194
    pool.close();
7ff8adafe9e4atagen2026-10-22 12:30 UTC195
7ff8adafe9e4atagen2026-10-22 12:30 UTC196
    let status = pool.status();
7ff8adafe9e4atagen2026-10-22 12:30 UTC197
    info!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC198
        size = status.size,
7ff8adafe9e4atagen2026-10-22 12:30 UTC199
        max_size = status.max_size,
7ff8adafe9e4atagen2026-10-22 12:30 UTC200
        available = status.available,
7ff8adafe9e4atagen2026-10-22 12:30 UTC201
        "{label} pause: pool closed"
7ff8adafe9e4atagen2026-10-22 12:30 UTC202
    );
7ff8adafe9e4atagen2026-10-22 12:30 UTC203
7ff8adafe9e4atagen2026-10-22 12:30 UTC204
    // Close the write connection: wait for any in-flight write to finish,
7ff8adafe9e4atagen2026-10-22 12:30 UTC205
    // run a WAL checkpoint, then drop on a blocking thread.
7ff8adafe9e4atagen2026-10-22 12:30 UTC206
    close_connection(write_connection).await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC207
7ff8adafe9e4atagen2026-10-22 12:30 UTC208
    let status = pool.status();
7ff8adafe9e4atagen2026-10-22 12:30 UTC209
    info!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC210
        size = status.size,
7ff8adafe9e4atagen2026-10-22 12:30 UTC211
        max_size = status.max_size,
7ff8adafe9e4atagen2026-10-22 12:30 UTC212
        available = status.available,
7ff8adafe9e4atagen2026-10-22 12:30 UTC213
        "{label} pause: write connection released"
7ff8adafe9e4atagen2026-10-22 12:30 UTC214
    );
7ff8adafe9e4atagen2026-10-22 12:30 UTC215
7ff8adafe9e4atagen2026-10-22 12:30 UTC216
    // Wait for any in-flight read connections to drain.
7ff8adafe9e4atagen2026-10-22 12:30 UTC217
    // The write connection has already been released above, so
7ff8adafe9e4atagen2026-10-22 12:30 UTC218
    // pool.status().size == 0 now correctly means every connection is gone
7ff8adafe9e4atagen2026-10-22 12:30 UTC219
    // and no SQLite file locks are held.
7ff8adafe9e4atagen2026-10-22 12:30 UTC220
    let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7ff8adafe9e4atagen2026-10-22 12:30 UTC221
    while pool.status().size > 0 {
7ff8adafe9e4atagen2026-10-22 12:30 UTC222
        if tokio::time::Instant::now() >= deadline {
7ff8adafe9e4atagen2026-10-22 12:30 UTC223
            let status = pool.status();
7ff8adafe9e4atagen2026-10-22 12:30 UTC224
            warn!(
7ff8adafe9e4atagen2026-10-22 12:30 UTC225
                size = status.size,
7ff8adafe9e4atagen2026-10-22 12:30 UTC226
                max_size = status.max_size,
7ff8adafe9e4atagen2026-10-22 12:30 UTC227
                available = status.available,
7ff8adafe9e4atagen2026-10-22 12:30 UTC228
                "Timed out waiting for SQLite pool connections to drain"
7ff8adafe9e4atagen2026-10-22 12:30 UTC229
            );
7ff8adafe9e4atagen2026-10-22 12:30 UTC230
            break;
7ff8adafe9e4atagen2026-10-22 12:30 UTC231
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC232
        tokio::time::sleep(Duration::from_millis(50)).await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC233
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC234
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC235
7ff8adafe9e4atagen2026-10-22 12:30 UTC236
/// Resume a store by rebuilding its connections.
7ff8adafe9e4atagen2026-10-22 12:30 UTC237
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC238
/// Idempotent: if the store is already active this is a no-op.
7ff8adafe9e4atagen2026-10-22 12:30 UTC239
pub(crate) async fn reopen_connections(
7ff8adafe9e4atagen2026-10-22 12:30 UTC240
    connections: &Mutex<Option<SqliteConnections>>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC241
    db_path: PathBuf,
7ff8adafe9e4atagen2026-10-22 12:30 UTC242
    pool_config: PoolConfig,
7ff8adafe9e4atagen2026-10-22 12:30 UTC243
    runtime_config: crate::RuntimeConfig,
7ff8adafe9e4atagen2026-10-22 12:30 UTC244
) -> crate::error::Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC245
    use crate::utils::SqliteAsyncConnExt as _;
7ff8adafe9e4atagen2026-10-22 12:30 UTC246
7ff8adafe9e4atagen2026-10-22 12:30 UTC247
    let mut guard = connections.lock().await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC248
    if guard.is_some() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC249
        // Not closed — idempotent.
7ff8adafe9e4atagen2026-10-22 12:30 UTC250
        return Ok(());
7ff8adafe9e4atagen2026-10-22 12:30 UTC251
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC252
7ff8adafe9e4atagen2026-10-22 12:30 UTC253
    // Rebuild the pool (connections are created lazily on first get()).
7ff8adafe9e4atagen2026-10-22 12:30 UTC254
    let pool =
7ff8adafe9e4atagen2026-10-22 12:30 UTC255
        Pool::builder(Manager::new(db_path)).config(pool_config).runtime(RUNTIME).build().map_err(
7ff8adafe9e4atagen2026-10-22 12:30 UTC256
            |e| crate::error::Error::InvalidData {
7ff8adafe9e4atagen2026-10-22 12:30 UTC257
                details: format!("Failed to rebuild connection pool: {e}"),
7ff8adafe9e4atagen2026-10-22 12:30 UTC258
            },
7ff8adafe9e4atagen2026-10-22 12:30 UTC259
        )?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC260
7ff8adafe9e4atagen2026-10-22 12:30 UTC261
    let write_conn = pool.get().await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC262
    // Re-apply runtime config (WAL mode, busy timeout, etc.)
7ff8adafe9e4atagen2026-10-22 12:30 UTC263
    write_conn.apply_runtime_config(runtime_config).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC264
7ff8adafe9e4atagen2026-10-22 12:30 UTC265
    *guard = Some(SqliteConnections { pool, write_connection: Arc::new(Mutex::new(write_conn)) });
7ff8adafe9e4atagen2026-10-22 12:30 UTC266
7ff8adafe9e4atagen2026-10-22 12:30 UTC267
    Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC268
}