Skip to content

Commit a593739

Browse files
committed
feat(indexeddb): Add Lease::generation in crypto, media, and event cache stores.
This patch adds `Lease::generation` support in the crypto, media and event cache stores. For the crypto store, we add the new `lease_locks` object store/table. Previously, `Lease` was stored in `core`, but without any prefix, it's easy to overwrite another records, it's dangerous. The sad thing is that it's hard to delete the existing leases in `core` because the keys aren't known. See the comment in the code explaining the tradeoff. For media and event cache stores, the already existing `leases` object store/table is cleared so that we can change the format of `Lease` easily.
1 parent 1fe566d commit a593739

File tree

10 files changed

+277
-106
lines changed

10 files changed

+277
-106
lines changed

crates/matrix-sdk-indexeddb/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ default = ["e2e-encryption", "state-store", "event-cache-store"]
1818
event-cache-store = ["dep:matrix-sdk-base", "media-store"]
1919
media-store = ["dep:matrix-sdk-base"]
2020
state-store = ["dep:matrix-sdk-base", "growable-bloom-filter"]
21-
e2e-encryption = ["dep:matrix-sdk-crypto"]
21+
e2e-encryption = ["dep:matrix-sdk-base", "dep:matrix-sdk-crypto"]
2222
testing = ["matrix-sdk-crypto?/testing"]
2323
experimental-encrypted-state-events = [
2424
"matrix-sdk-crypto?/experimental-encrypted-state-events"

crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ mod v10_to_v11;
3333
mod v11_to_v12;
3434
mod v12_to_v13;
3535
mod v13_to_v14;
36+
mod v14_to_v15;
3637
mod v5_to_v7;
3738
mod v7;
3839
mod v7_to_v8;
@@ -176,6 +177,10 @@ pub async fn open_and_upgrade_db(
176177
v13_to_v14::schema_bump(name).await?;
177178
}
178179

180+
if old_version < 15 {
181+
v14_to_v15::schema_add(name).await?;
182+
}
183+
179184
// If you add more migrations here, you'll need to update
180185
// `tests::EXPECTED_SCHEMA_VERSION`.
181186

@@ -278,7 +283,7 @@ mod tests {
278283
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
279284

280285
/// The schema version we expect after we open the store.
281-
const EXPECTED_SCHEMA_VERSION: u32 = 14;
286+
const EXPECTED_SCHEMA_VERSION: u32 = 15;
282287

283288
/// Adjust this to test do a more comprehensive perf test
284289
const NUM_RECORDS_FOR_PERF: usize = 2_000;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
use indexed_db_futures::{error::OpenDbError, Build};
16+
17+
use crate::crypto_store::{keys, migrations::do_schema_upgrade, Result};
18+
19+
/// Perform the schema upgrade v14 to v15, add the `lease_locks` table.
20+
///
21+
/// Note: it's not trivial to delete old lease locks in the `keys::CORE` table,
22+
/// because we don't know which keys were associated to them. We consider it's
23+
/// fine because it represents a tiny amount of data (maybe 2 rows, with the
24+
/// “`Lease` type” being quite small). To achieve so, we could read all rows in
25+
/// `keys::CORE`, try to deserialize to the `Lease` type, and act accordingly,
26+
/// but each store may have its own `Lease` type. Also note that this
27+
/// `matrix-sdk-indexeddb`
28+
pub(crate) async fn schema_add(name: &str) -> Result<(), OpenDbError> {
29+
do_schema_upgrade(name, 15, |tx, _| {
30+
tx.db().create_object_store(keys::LEASE_LOCKS).build()?;
31+
Ok(())
32+
})
33+
.await
34+
}

crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ use indexed_db_futures::{
3030
KeyRange,
3131
};
3232
use js_sys::Array;
33+
use matrix_sdk_base::cross_process_lock::{
34+
CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION,
35+
};
3336
use matrix_sdk_crypto::{
3437
olm::{
3538
Curve25519PublicKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
@@ -97,6 +100,8 @@ mod keys {
97100

98101
pub const RECEIVED_ROOM_KEY_BUNDLES: &str = "received_room_key_bundles";
99102

103+
pub const LEASE_LOCKS: &str = "lease_locks";
104+
100105
// keys
101106
pub const STORE_CIPHER: &str = "store_cipher";
102107
pub const ACCOUNT: &str = "account";
@@ -1571,53 +1576,68 @@ impl_crypto_store! {
15711576
lease_duration_ms: u32,
15721577
key: &str,
15731578
holder: &str,
1574-
) -> Result<bool> {
1579+
) -> Result<Option<CrossProcessLockGeneration>> {
15751580
// As of 2023-06-23, the code below hasn't been tested yet.
15761581
let key = JsValue::from_str(key);
1577-
let txn =
1578-
self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?;
1579-
let object_store = txn.object_store(keys::CORE)?;
1582+
let txn = self
1583+
.inner
1584+
.transaction(keys::LEASE_LOCKS)
1585+
.with_mode(TransactionMode::Readwrite)
1586+
.build()?;
1587+
let object_store = txn.object_store(keys::LEASE_LOCKS)?;
15801588

1581-
#[derive(serde::Deserialize, serde::Serialize)]
1589+
#[derive(Deserialize, Serialize)]
15821590
struct Lease {
15831591
holder: String,
1584-
expiration_ts: u64,
1592+
expiration: u64,
1593+
generation: CrossProcessLockGeneration,
15851594
}
15861595

1587-
let now_ts: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
1588-
let expiration_ts = now_ts + lease_duration_ms as u64;
1589-
1590-
let prev = object_store.get(&key).await?;
1591-
match prev {
1592-
Some(prev) => {
1593-
let lease: Lease = self.serializer.deserialize_value(prev)?;
1594-
if lease.holder == holder || lease.expiration_ts < now_ts {
1595-
object_store
1596-
.put(
1597-
&self.serializer.serialize_value(&Lease {
1598-
holder: holder.to_owned(),
1599-
expiration_ts,
1600-
})?,
1601-
)
1602-
.with_key(key)
1603-
.build()?;
1604-
Ok(true)
1596+
let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
1597+
let expiration = now + lease_duration_ms as u64;
1598+
1599+
let lease = match object_store.get(&key).await? {
1600+
Some(entry) => {
1601+
let mut lease: Lease = self.serializer.deserialize_value(entry)?;
1602+
1603+
if lease.holder == holder {
1604+
// We had the lease before, extend it.
1605+
lease.expiration = expiration;
1606+
1607+
Some(lease)
16051608
} else {
1606-
Ok(false)
1609+
// We didn't have it.
1610+
if lease.expiration < now {
1611+
// Steal it!
1612+
lease.holder = holder.to_owned();
1613+
lease.expiration = expiration;
1614+
lease.generation += 1;
1615+
1616+
Some(lease)
1617+
} else {
1618+
// We tried our best.
1619+
None
1620+
}
16071621
}
16081622
}
16091623
None => {
1610-
object_store
1611-
.put(
1612-
&self
1613-
.serializer
1614-
.serialize_value(&Lease { holder: holder.to_owned(), expiration_ts })?,
1615-
)
1616-
.with_key(key)
1617-
.build()?;
1618-
Ok(true)
1624+
let lease = Lease {
1625+
holder: holder.to_owned(),
1626+
expiration,
1627+
generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
1628+
};
1629+
1630+
Some(lease)
16191631
}
1620-
}
1632+
};
1633+
1634+
Ok(if let Some(lease) = lease {
1635+
object_store.put(&self.serializer.serialize_value(&lease)?).with_key(key).build()?;
1636+
1637+
Some(lease.generation)
1638+
} else {
1639+
None
1640+
})
16211641
}
16221642
}
16231643

crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@
1515
use indexed_db_futures::{
1616
database::Database,
1717
error::{DomException, Error, OpenDbError},
18+
transaction::Transaction,
1819
};
1920
use thiserror::Error;
2021

2122
/// The current version and keys used in the database.
2223
pub mod current {
23-
use super::{v1, Version};
24+
use super::{v2, Version};
2425

25-
pub const VERSION: Version = Version::V1;
26-
pub use v1::keys;
26+
pub const VERSION: Version = Version::V2;
27+
pub use v2::keys;
2728
}
2829

2930
/// Opens a connection to the IndexedDB database and takes care of upgrading it
@@ -35,7 +36,7 @@ pub async fn open_and_upgrade_db(name: &str) -> Result<Database, OpenDbError> {
3536
.with_on_upgrade_needed(|event, transaction| {
3637
let mut version = Version::try_from(event.old_version() as u32)?;
3738
while version < current::VERSION {
38-
version = match version.upgrade(transaction.db())? {
39+
version = match version.upgrade(transaction)? {
3940
Some(next) => next,
4041
None => current::VERSION, /* No more upgrades to apply, jump forward! */
4142
};
@@ -49,18 +50,21 @@ pub async fn open_and_upgrade_db(name: &str) -> Result<Database, OpenDbError> {
4950
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
5051
#[repr(u32)]
5152
pub enum Version {
52-
/// Version 0 of the database, for details see [`v0`]
53+
/// Version 0 of the database, for details see [`v0`].
5354
V0 = 0,
54-
/// Version 1 of the database, for details see [`v1`]
55+
/// Version 1 of the database, for details see [`v1`].
5556
V1 = 1,
57+
/// Version 2 of the database, for details see [`v2`].
58+
V2 = 2,
5659
}
5760

5861
impl Version {
5962
/// Upgrade the database to the next version, if one exists.
60-
pub fn upgrade(self, db: &Database) -> Result<Option<Self>, Error> {
63+
pub fn upgrade(self, transaction: &Transaction<'_>) -> Result<Option<Self>, Error> {
6164
match self {
62-
Self::V0 => v0::upgrade(db).map(Some),
63-
Self::V1 => Ok(None),
65+
Self::V0 => v0::upgrade(transaction).map(Some),
66+
Self::V1 => v1::upgrade(transaction).map(Some),
67+
Self::V2 => Ok(None),
6468
}
6569
}
6670
}
@@ -76,6 +80,7 @@ impl TryFrom<u32> for Version {
7680
match value {
7781
0 => Ok(Version::V0),
7882
1 => Ok(Version::V1),
83+
2 => Ok(Version::V2),
7984
v => Err(UnknownVersionError(v)),
8085
}
8186
}
@@ -96,8 +101,8 @@ pub mod v0 {
96101
use super::*;
97102

98103
/// Upgrade database from `v0` to `v1`
99-
pub fn upgrade(db: &Database) -> Result<Version, Error> {
100-
v1::create_object_stores(db)?;
104+
pub fn upgrade(transaction: &Transaction<'_>) -> Result<Version, Error> {
105+
v1::create_object_stores(transaction.db())?;
101106
Ok(Version::V1)
102107
}
103108
}
@@ -197,4 +202,27 @@ pub mod v1 {
197202
db.create_object_store(keys::GAPS).with_key_path(keys::GAPS_KEY_PATH.into()).build()?;
198203
Ok(())
199204
}
205+
206+
/// Upgrade database from `v1` to `v2`
207+
pub fn upgrade(transaction: &Transaction<'_>) -> Result<Version, Error> {
208+
v2::empty_leases(transaction)?;
209+
Ok(Version::V2)
210+
}
211+
}
212+
213+
mod v2 {
214+
// Re-use all the same keys from `v1`.
215+
pub use super::v1::keys;
216+
use super::*;
217+
218+
/// The format of [`Lease`][super::super::types::Lease] is changing. Let's
219+
/// erase previous values.
220+
pub fn empty_leases(transaction: &Transaction<'_>) -> Result<(), Error> {
221+
let object_store = transaction.object_store(keys::LEASES)?;
222+
223+
// Remove all previous leases.
224+
object_store.clear()?;
225+
226+
Ok(())
227+
}
200228
}

crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
use std::{rc::Rc, time::Duration};
1818

1919
use indexed_db_futures::{database::Database, Build};
20+
#[cfg(target_family = "wasm")]
21+
use matrix_sdk_base::cross_process_lock::{
22+
CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION,
23+
};
2024
use matrix_sdk_base::{
2125
event_cache::{store::EventCacheStore, Event, Gap},
2226
linked_chunk::{
@@ -103,29 +107,57 @@ impl EventCacheStore for IndexeddbEventCacheStore {
103107
lease_duration_ms: u32,
104108
key: &str,
105109
holder: &str,
106-
) -> Result<bool, IndexeddbEventCacheStoreError> {
110+
) -> Result<Option<CrossProcessLockGeneration>, IndexeddbEventCacheStoreError> {
107111
let _timer = timer!("method");
108112

109-
let now = Duration::from_millis(MilliSecondsSinceUnixEpoch::now().get().into());
110-
111113
let transaction =
112114
self.transaction(&[Lease::OBJECT_STORE], IdbTransactionMode::Readwrite)?;
113115

114-
if let Some(lease) = transaction.get_lease_by_id(key).await? {
115-
if lease.holder != holder && !lease.has_expired(now) {
116-
return Ok(false);
116+
let now = Duration::from_millis(MilliSecondsSinceUnixEpoch::now().get().into());
117+
let expiration = now + Duration::from_millis(lease_duration_ms.into());
118+
119+
let lease = match transaction.get_lease_by_id(key).await? {
120+
Some(mut lease) => {
121+
if lease.holder == holder {
122+
// We had the lease before, extend it.
123+
lease.expiration = expiration;
124+
125+
Some(lease)
126+
} else {
127+
// We didn't have it.
128+
if lease.expiration < now {
129+
// Steal it!
130+
lease.holder = holder.to_owned();
131+
lease.expiration = expiration;
132+
lease.generation += 1;
133+
134+
Some(lease)
135+
} else {
136+
// We tried our best.
137+
None
138+
}
139+
}
117140
}
118-
}
141+
None => {
142+
let lease = Lease {
143+
key: key.to_owned(),
144+
holder: holder.to_owned(),
145+
expiration,
146+
generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
147+
};
148+
149+
Some(lease)
150+
}
151+
};
119152

120-
transaction
121-
.put_lease(&Lease {
122-
key: key.to_owned(),
123-
holder: holder.to_owned(),
124-
expiration: now + Duration::from_millis(lease_duration_ms.into()),
125-
})
126-
.await?;
127-
transaction.commit().await?;
128-
Ok(true)
153+
Ok(if let Some(lease) = lease {
154+
transaction.put_lease(&lease).await?;
155+
transaction.commit().await?;
156+
157+
Some(lease.generation)
158+
} else {
159+
None
160+
})
129161
}
130162

131163
#[instrument(skip(self, updates))]

0 commit comments

Comments
 (0)