indexeddb: move v8_to_v10 migration into its own file

Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
This commit is contained in:
Andy Balaam
2024-02-02 15:01:03 +00:00
parent 8d87e32f8b
commit edf23dbd2e
2 changed files with 196 additions and 182 deletions

View File

@@ -22,6 +22,8 @@ use crate::{
IndexeddbCryptoStoreError,
};
mod v8_to_v10;
mod old_keys {
/// Old format of the `inbound_group_sessions` store which lacked indexes or
/// a sensible structure
@@ -416,188 +418,6 @@ async fn prepare_data_for_v8(name: &str, serializer: &IndexeddbSerializer) -> Re
Ok(())
}
/// Migration code that moves from inbound_group_sessions2 to
/// inbound_group_sessions3, shrinking the values stored in each record.
///
/// The migration 8->9 creates the new store inbound_group_sessions3.
/// Then we move the data into the new store.
/// The migration 9->10 deletes the old store inbound_group_sessions2.
mod v8_to_v10 {
use indexed_db_futures::{
idb_object_store::IdbObjectStore,
request::{IdbOpenDbRequestLike, OpenDbRequest},
IdbDatabase, IdbIndex, IdbKeyPath, IdbQuerySource, IdbVersionChangeEvent,
};
use matrix_sdk_crypto::olm::InboundGroupSession;
use tracing::{debug, info};
use wasm_bindgen::JsValue;
use web_sys::{DomException, IdbIndexParameters, IdbTransactionMode};
use super::Result;
use crate::{
crypto_store::{
indexeddb_serializer::IndexeddbSerializer, keys, migrations::old_keys,
InboundGroupSessionIndexedDbObject,
},
IndexeddbCryptoStoreError,
};
/// The objects we store in the inbound_group_sessions2 indexeddb object
/// store (in schemas v7 and v8)
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct InboundGroupSessionIndexedDbObject2 {
/// (Possibly encrypted) serialisation of a
/// [`matrix_sdk_crypto::olm::group_sessions::PickledInboundGroupSession`]
/// structure.
pub pickled_session: Vec<u8>,
/// Whether the session data has yet to be backed up.
///
/// Since we only need to be able to find entries where this is `true`,
/// we skip serialization in cases where it is `false`. That has
/// the effect of omitting it from the indexeddb index.
///
/// We also use a custom serializer because bools can't be used as keys
/// in indexeddb.
#[serde(
default,
skip_serializing_if = "std::ops::Not::not",
with = "crate::serialize_bool_for_indexeddb"
)]
pub needs_backup: bool,
}
fn add_nonunique_index<'a>(
object_store: &'a IdbObjectStore<'a>,
name: &str,
key_path: &str,
) -> Result<IdbIndex<'a>, DomException> {
let mut params = IdbIndexParameters::new();
params.unique(false);
object_store.create_index_with_params(name, &IdbKeyPath::str(key_path), &params)
}
async fn do_schema_upgrade<F>(name: &str, version: u32, f: F) -> Result<(), DomException>
where
F: Fn(&IdbDatabase) -> Result<(), JsValue> + 'static,
{
info!("IndexeddbCryptoStore upgrade schema -> v{version} starting");
let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, version)?;
db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| f(evt.db())));
let db = db_req.await?;
db.close();
info!("IndexeddbCryptoStore upgrade schema -> v{version} complete");
Ok(())
}
pub(crate) async fn upgrade_scheme_to_v9_create_inbound_group_sessions3(
name: &str,
) -> Result<(), DomException> {
do_schema_upgrade(name, 9, |db| {
let object_store = db.create_object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
add_nonunique_index(
&object_store,
keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX,
"needs_backup",
)?;
// See https://github.com/element-hq/element-web/issues/26892#issuecomment-1906336076
// for the plan concerning this property and index. At time of writing, it is
// unused, and needs_backup is still used.
add_nonunique_index(
&object_store,
keys::INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX,
"backed_up_to",
)?;
Ok(())
})
.await
}
pub(crate) async fn migrate_data_before_v10_populate_inbound_group_sessions3(
name: &str,
serializer: &IndexeddbSerializer,
) -> Result<()> {
info!("IndexeddbCryptoStore migrate data before v10 starting");
let db = IdbDatabase::open(name)?.await?;
let txn = db.transaction_on_multi_with_mode(
&[old_keys::INBOUND_GROUP_SESSIONS_V2, keys::INBOUND_GROUP_SESSIONS_V3],
IdbTransactionMode::Readwrite,
)?;
let inbound_group_sessions2 = txn.object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?;
let inbound_group_sessions3 = txn.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
let row_count = inbound_group_sessions2.count()?.await?;
info!(row_count, "Shrinking inbound_group_session records");
// Iterate through all rows
if let Some(cursor) = inbound_group_sessions2.open_cursor()?.await? {
let mut idx = 0;
loop {
idx += 1;
if idx % 100 == 0 {
debug!("Migrating session {idx} of {row_count}");
}
// Deserialize the session from the old store
let old_value: InboundGroupSessionIndexedDbObject2 =
serde_wasm_bindgen::from_value(cursor.value())?;
let session = InboundGroupSession::from_pickle(
serializer.deserialize_value_from_bytes(&old_value.pickled_session)?,
)
.map_err(|e| IndexeddbCryptoStoreError::CryptoStoreError(e.into()))?;
// Calculate its key in the new table
let new_key = serializer.encode_key(
keys::INBOUND_GROUP_SESSIONS_V3,
(&session.room_id, session.session_id()),
);
// Serialize the session in the new format
// This is much the same as [`IndexeddbStore::serialize_inbound_group_session`].
let new_value = InboundGroupSessionIndexedDbObject::new(
serializer.maybe_encrypt_value(session.pickle().await)?,
!session.backed_up(),
);
// Write it to the new store
inbound_group_sessions3
.add_key_val(&new_key, &serde_wasm_bindgen::to_value(&new_value)?)?;
// Continue to the next record, or stop if we're done
if !cursor.continue_cursor()?.await? {
debug!("Migrated {idx} sessions.");
break;
}
}
}
txn.await.into_result()?;
db.close();
info!("IndexeddbCryptoStore upgrade data before v10 finished");
Ok(())
}
pub(crate) async fn upgrade_scheme_to_v10_delete_inbound_group_sessions2(
name: &str,
) -> Result<(), DomException> {
do_schema_upgrade(name, 10, |db| {
db.delete_object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?;
Ok(())
})
.await
}
}
#[cfg(all(test, target_arch = "wasm32"))]
mod tests {
use std::{future::Future, sync::Arc};

View File

@@ -0,0 +1,194 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Migration code that moves from inbound_group_sessions2 to
//! inbound_group_sessions3, shrinking the values stored in each record.
//!
//! The migration 8->9 creates the new store inbound_group_sessions3.
//! Then we move the data into the new store.
//! The migration 9->10 deletes the old store inbound_group_sessions2.
use indexed_db_futures::{
idb_object_store::IdbObjectStore,
request::{IdbOpenDbRequestLike, OpenDbRequest},
IdbDatabase, IdbIndex, IdbKeyPath, IdbQuerySource, IdbVersionChangeEvent,
};
use matrix_sdk_crypto::olm::InboundGroupSession;
use tracing::{debug, info};
use wasm_bindgen::JsValue;
use web_sys::{DomException, IdbIndexParameters, IdbTransactionMode};
use super::Result;
use crate::{
crypto_store::{
indexeddb_serializer::IndexeddbSerializer, keys, migrations::old_keys,
InboundGroupSessionIndexedDbObject,
},
IndexeddbCryptoStoreError,
};
/// The objects we store in the inbound_group_sessions2 indexeddb object
/// store (in schemas v7 and v8)
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct InboundGroupSessionIndexedDbObject2 {
/// (Possibly encrypted) serialisation of a
/// [`matrix_sdk_crypto::olm::group_sessions::PickledInboundGroupSession`]
/// structure.
pub pickled_session: Vec<u8>,
/// Whether the session data has yet to be backed up.
///
/// Since we only need to be able to find entries where this is `true`,
/// we skip serialization in cases where it is `false`. That has
/// the effect of omitting it from the indexeddb index.
///
/// We also use a custom serializer because bools can't be used as keys
/// in indexeddb.
#[serde(
default,
skip_serializing_if = "std::ops::Not::not",
with = "crate::serialize_bool_for_indexeddb"
)]
pub needs_backup: bool,
}
fn add_nonunique_index<'a>(
object_store: &'a IdbObjectStore<'a>,
name: &str,
key_path: &str,
) -> Result<IdbIndex<'a>, DomException> {
let mut params = IdbIndexParameters::new();
params.unique(false);
object_store.create_index_with_params(name, &IdbKeyPath::str(key_path), &params)
}
async fn do_schema_upgrade<F>(name: &str, version: u32, f: F) -> Result<(), DomException>
where
F: Fn(&IdbDatabase) -> Result<(), JsValue> + 'static,
{
info!("IndexeddbCryptoStore upgrade schema -> v{version} starting");
let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, version)?;
db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| f(evt.db())));
let db = db_req.await?;
db.close();
info!("IndexeddbCryptoStore upgrade schema -> v{version} complete");
Ok(())
}
pub(crate) async fn upgrade_scheme_to_v9_create_inbound_group_sessions3(
name: &str,
) -> Result<(), DomException> {
do_schema_upgrade(name, 9, |db| {
let object_store = db.create_object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
add_nonunique_index(
&object_store,
keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX,
"needs_backup",
)?;
// See https://github.com/element-hq/element-web/issues/26892#issuecomment-1906336076
// for the plan concerning this property and index. At time of writing, it is
// unused, and needs_backup is still used.
add_nonunique_index(
&object_store,
keys::INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX,
"backed_up_to",
)?;
Ok(())
})
.await
}
pub(crate) async fn migrate_data_before_v10_populate_inbound_group_sessions3(
name: &str,
serializer: &IndexeddbSerializer,
) -> Result<()> {
info!("IndexeddbCryptoStore migrate data before v10 starting");
let db = IdbDatabase::open(name)?.await?;
let txn = db.transaction_on_multi_with_mode(
&[old_keys::INBOUND_GROUP_SESSIONS_V2, keys::INBOUND_GROUP_SESSIONS_V3],
IdbTransactionMode::Readwrite,
)?;
let inbound_group_sessions2 = txn.object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?;
let inbound_group_sessions3 = txn.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
let row_count = inbound_group_sessions2.count()?.await?;
info!(row_count, "Shrinking inbound_group_session records");
// Iterate through all rows
if let Some(cursor) = inbound_group_sessions2.open_cursor()?.await? {
let mut idx = 0;
loop {
idx += 1;
if idx % 100 == 0 {
debug!("Migrating session {idx} of {row_count}");
}
// Deserialize the session from the old store
let old_value: InboundGroupSessionIndexedDbObject2 =
serde_wasm_bindgen::from_value(cursor.value())?;
let session = InboundGroupSession::from_pickle(
serializer.deserialize_value_from_bytes(&old_value.pickled_session)?,
)
.map_err(|e| IndexeddbCryptoStoreError::CryptoStoreError(e.into()))?;
// Calculate its key in the new table
let new_key = serializer.encode_key(
keys::INBOUND_GROUP_SESSIONS_V3,
(&session.room_id, session.session_id()),
);
// Serialize the session in the new format
// This is much the same as [`IndexeddbStore::serialize_inbound_group_session`].
let new_value = InboundGroupSessionIndexedDbObject::new(
serializer.maybe_encrypt_value(session.pickle().await)?,
!session.backed_up(),
);
// Write it to the new store
inbound_group_sessions3
.add_key_val(&new_key, &serde_wasm_bindgen::to_value(&new_value)?)?;
// Continue to the next record, or stop if we're done
if !cursor.continue_cursor()?.await? {
debug!("Migrated {idx} sessions.");
break;
}
}
}
txn.await.into_result()?;
db.close();
info!("IndexeddbCryptoStore upgrade data before v10 finished");
Ok(())
}
pub(crate) async fn upgrade_scheme_to_v10_delete_inbound_group_sessions2(
name: &str,
) -> Result<(), DomException> {
do_schema_upgrade(name, 10, |db| {
db.delete_object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?;
Ok(())
})
.await
}