matrix_sdk_sqlite/connection.rs
1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! An implementation of `deadpool` for `rusqlite`.
16//!
17//! Initially, we were using `deadpool-sqlite`, that is also using `rusqlite` as
18//! the SQLite interface. However, in the implementation of
19//! [`deadpool::managed::Manager`], when recycling an object (i.e. an SQLite
20//! connection), [a SQL query is run to detect whether the connection is still
21//! alive][connection-test]. It creates performance issues:
22//!
23//! 1. It runs a prepared SQL query, which has a non-negligle cost. Imagine each
24//! connection is used to run on average one query; when recycled, a second
25//! query was constantly run. Even if it's a simple query, it requires to
26//! prepare a statement, to run and to query it.
27//! 2. The SQL query was run in a blocking task. Indeed,
28//! `deadpool_runtime::spawn_blocking` is used (via
29//! `deadpool_sync::SyncWrapper::interact`), which includes [blocking the
30//! thread, acquiring a lock][spawn_blocking] etc. All this has more
31//! performance cost.
32//!
33//! Measures have shown it is a performance bottleneck for us, especially on
34//! Android. Why specifically on Android and not other systems? This is still
35//! unclear at the time of writing (2025-11-11), despites having spent several
36//! days digging and trying to find an answer to this question.
37//!
38//! We have tried to use another approach to test the aliveness of the
39//! connections without running queries. It has involved patching `rusqlite` to
40//! add more bindings to SQLite, and patching `deadpool` itself, but without any
41//! successful results.
42//!
43//! Finally, we have started questioning the reason of this test: why testing
44//! whether the connection was still alive? After all, there is no reason a
45//! connection should die in our case:
46//!
47//! - all connections are local,
48//! - all interactions are behind [WAL], which is local only,
49//! - even if for an unknown reason the connection died, using it next time
50//! would create an error… exactly what would happen when recycling the
51//! connection.
52//!
53//! Consequently, we have created a new implementation of `deadpool` for
54//! `rusqlite` that doesn't test the aliveness of the connections when recycled.
55//! We assume they are all alive.
56//!
57//! This implementation is, at the time of writing (2025-11-11):
58//!
59//! - 3.5 times faster on Android than `deadpool-sqlite`, removing the lock and
60//! thread contention entirely,
61//! - 2 times faster on iOS.
62//!
63//! [connection-test]: https://github.com/deadpool-rs/deadpool/blob/d6f7d58756f0cc7bdd1f3d54d820c1332d67e4d5/crates/deadpool-sqlite/src/lib.rs#L80-L100
64//! [spawn_blocking]: https://github.com/deadpool-rs/deadpool/blob/d6f7d58756f0cc7bdd1f3d54d820c1332d67e4d5/crates/deadpool-sync/src/lib.rs#L113-L131
65//! [WAL]: https://www.sqlite.org/wal.html
66
67use std::{convert::Infallible, path::PathBuf};
68
69pub use deadpool::managed::reexports::*;
70use deadpool::managed::{self, Metrics, RecycleError};
71use deadpool_sync::SyncWrapper;
72
73/// The default runtime used by `matrix-sdk-sqlite` for `deadpool`.
74pub const RUNTIME: Runtime = Runtime::Tokio1;
75
76deadpool::managed_reexports!(
77 "matrix-sdk-sqlite",
78 Manager,
79 managed::Object<Manager>,
80 rusqlite::Error,
81 Infallible
82);
83
84/// Type representing a connection to SQLite from the [`Pool`].
85pub type Connection = Object;
86
87/// [`Manager`][managed::Manager] for creating and recycling SQLite
88/// [`Connection`]s.
89#[derive(Debug)]
90pub struct Manager {
91 database_path: PathBuf,
92}
93
94impl Manager {
95 /// Creates a new [`Manager`] for a database.
96 #[must_use]
97 pub fn new(database_path: PathBuf) -> Self {
98 Self { database_path }
99 }
100}
101
102impl managed::Manager for Manager {
103 type Type = SyncWrapper<rusqlite::Connection>;
104 type Error = rusqlite::Error;
105
106 async fn create(&self) -> Result<Self::Type, Self::Error> {
107 let path = self.database_path.clone();
108 SyncWrapper::new(RUNTIME, move || rusqlite::Connection::open(path)).await
109 }
110
111 async fn recycle(
112 &self,
113 conn: &mut Self::Type,
114 _: &Metrics,
115 ) -> managed::RecycleResult<Self::Error> {
116 if conn.is_mutex_poisoned() {
117 return Err(RecycleError::Message(
118 "Mutex is poisoned. Connection is considered unusable.".into(),
119 ));
120 }
121
122 Ok(())
123 }
124}