Skip to content

Commit b24fdc5

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 b24fdc5

File tree

10 files changed

+273
-96
lines changed

10 files changed

+273
-96
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: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ use thiserror::Error;
2020

2121
/// The current version and keys used in the database.
2222
pub mod current {
23-
use super::{v1, Version};
23+
use super::{v2, Version};
2424

25-
pub const VERSION: Version = Version::V1;
26-
pub use v1::keys;
25+
pub const VERSION: Version = Version::V2;
26+
pub use v2::keys;
2727
}
2828

2929
/// Opens a connection to the IndexedDB database and takes care of upgrading it
@@ -49,18 +49,21 @@ pub async fn open_and_upgrade_db(name: &str) -> Result<Database, OpenDbError> {
4949
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
5050
#[repr(u32)]
5151
pub enum Version {
52-
/// Version 0 of the database, for details see [`v0`]
52+
/// Version 0 of the database, for details see [`v0`].
5353
V0 = 0,
54-
/// Version 1 of the database, for details see [`v1`]
54+
/// Version 1 of the database, for details see [`v1`].
5555
V1 = 1,
56+
/// Version 2 of the database, for details see [`v2`].
57+
V2 = 2,
5658
}
5759

5860
impl Version {
5961
/// Upgrade the database to the next version, if one exists.
6062
pub fn upgrade(self, db: &Database) -> Result<Option<Self>, Error> {
6163
match self {
6264
Self::V0 => v0::upgrade(db).map(Some),
63-
Self::V1 => Ok(None),
65+
Self::V1 => v1::upgrade(db).map(Some),
66+
Self::V2 => Ok(None),
6467
}
6568
}
6669
}
@@ -76,6 +79,7 @@ impl TryFrom<u32> for Version {
7679
match value {
7780
0 => Ok(Version::V0),
7881
1 => Ok(Version::V1),
82+
2 => Ok(Version::V2),
7983
v => Err(UnknownVersionError(v)),
8084
}
8185
}
@@ -197,4 +201,31 @@ pub mod v1 {
197201
db.create_object_store(keys::GAPS).with_key_path(keys::GAPS_KEY_PATH.into()).build()?;
198202
Ok(())
199203
}
204+
205+
/// Upgrade database from `v1` to `v2`
206+
pub fn upgrade(db: &Database) -> Result<Version, Error> {
207+
v2::empty_leases(db)?;
208+
Ok(Version::V2)
209+
}
210+
}
211+
212+
mod v2 {
213+
use indexed_db_futures::{transaction::TransactionMode, Build};
214+
215+
// Re-use all the same keys from `v1`.
216+
pub use super::v1::keys;
217+
use super::*;
218+
219+
/// The format of [`Lease`][super::super::types::Lease] is changing. Let's
220+
/// erase previous values.
221+
pub fn empty_leases(db: &Database) -> Result<(), Error> {
222+
let transaction =
223+
db.transaction(keys::LEASES).with_mode(TransactionMode::Readwrite).build()?;
224+
let object_store = transaction.object_store(keys::LEASES)?;
225+
226+
// Remove all previous leases.
227+
object_store.clear()?;
228+
229+
Ok(())
230+
}
200231
}

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))]

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use std::time::Duration;
1616

1717
use matrix_sdk_base::{
18+
cross_process_lock::CrossProcessLockGeneration,
1819
deserialized_responses::TimelineEvent,
1920
event_cache::store::extract_event_relation,
2021
linked_chunk::{ChunkIdentifier, LinkedChunkId, OwnedLinkedChunkId},
@@ -29,13 +30,7 @@ pub struct Lease {
2930
pub key: String,
3031
pub holder: String,
3132
pub expiration: Duration,
32-
}
33-
34-
impl Lease {
35-
/// Determines whether the lease is expired at a given time `t`
36-
pub fn has_expired(&self, t: Duration) -> bool {
37-
self.expiration < t
38-
}
33+
pub generation: CrossProcessLockGeneration,
3934
}
4035

4136
/// Representation of a [`Chunk`](matrix_sdk_base::linked_chunk::Chunk)

0 commit comments

Comments
 (0)