Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the store traits so they expose a transactional API #2000

Closed
Tracked by #2624 ...
bnjbvr opened this issue Jun 1, 2023 · 5 comments
Closed
Tracked by #2624 ...

Change the store traits so they expose a transactional API #2000

bnjbvr opened this issue Jun 1, 2023 · 5 comments

Comments

@bnjbvr
Copy link
Member

bnjbvr commented Jun 1, 2023

This would increase robustness in the case of the two sync loops, so that we're sure that there aren't any partial write to the db that the main loop consumer would try to read later.

@poljar
Copy link
Contributor

poljar commented Jun 1, 2023

To explain a bit further, since we do use transactions for all of our writes.

Currently all the changes we want to write are first collected into a struct named Changes. Then we call Store.save_changes() which saves everything we collected in a single transaction.

This was mostly done since sled has an API where you pass in a closure which gets executed as part of a transaction. In this closure you write your values to the db and everybody is happy.

But most other databases have a Database::get_transaction(&self) -> &Transaction interface. Crucially SQLite and IndexedDB behave this way.

We could change our store traits to reflect this new reality and get rid of the Store::save_changes(&self, changes: Changes) API and add a Store::get_transaction(&self) API instead.

This way, if we need to do reads during a write, they also become part of the transaction.

@poljar poljar changed the title Use transactions for writes into the crypto state store Change the store traits so they expose a transactional API Jun 1, 2023
@bnjbvr
Copy link
Member Author

bnjbvr commented Jun 1, 2023

Wait, turns out that sqlite implementations for both the crypto store trait and the state store trait make use of sqlite transactions. So it's a non issue, as it's already implemented, or are you suggesting something else?

@poljar
Copy link
Contributor

poljar commented Jun 1, 2023

I am.

If you take a look at the CryptoStore trait:

async fn save_changes(&self, changes: Changes) -> Result<(), Self::Error>;

We have this method, which may or may not open a transaction behind the scenes, our current implementations do so.

But we also have some other methods:

async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>, Self::Error>;
/// Put arbitrary data into the store
///
/// # Arguments
///
/// * `key` - The key to insert data into
///
/// * `value` - The value to insert
async fn set_custom_value(&self, key: &str, value: Vec<u8>) -> Result<(), Self::Error>;

Currently we can't store two custom values in a single transaction. Any reads that may happen before a save_changes() call also aren't part of the transaction which the save_changes() method will use.

If we instead have a method in the CryptoStore trait which returns a transaction, and then the transaction object exposes a set_custom_value() method, then we could store multiple values in a single transaction.

That being said, none of this is required currently, and the faux transaction using save_changes() will work just fine.

The above change to our store traits has been previously suggested by @jplatte, so he may have even more concrete ideas.

bnjbvr added a commit that referenced this issue Oct 9, 2023
This adds a new `StoreTransaction` type, that wraps a `StoreCache` and a `Store`. The idea is that it will allow write access to the `Store` (and maintains the cache at the same time), while the `Store::cache` method will only give read-only access to the store's content.

Another new data type is introduced, `PendingChanges`, that reflects `Changes` but for fields that are properly maintained in the `StoreCache` and that one can write in the `StoreTransaction`. In the future, it wouldn't be possible to use `save_pending_changes` from the outside of a `StoreTransaction` context.

The layering is the following:

- `Store` wraps the `DynCryptoStore`, contains a reference to a `StoreCache`.
- When read-only access is sufficient, one can get a handle to the cache with `Store::cache()`.
- When a write happens, then one can create a `StoreTransaction` (later, only one at a time will be allowed, by putting the `StoreCache` behind a `RwLock`; this has been deferred to not make the PR grow too much).
- Any field in the `StoreCache` will get a method to get a reference to the cached thing: it will either load from the DB if not cached, or return the previously cached value. 
- Any field that can be written to will get a method to get a mutable reference in the `StoreTransaction`: it will either load from the cache into a `PendingChanges` scratch pad, or return the scratchpad temporary value.
- When a `StoreTransaction::commit()` happens, fields are backpropagated into the DB *and* the cache. 

Then, this `StoreTransaction` is used to update a `ReadOnlyAccount` in multiple places (and usage of `ReadOnlyAccount` is minimized so as not to require a transaction or cache function call as much as possible). With this, the read-only account only exists transiently, and it's only stored long-term in the cache.

Followup PRs include:

- making the `ReadOnlyAccount` not cloneable
- remove inner mutability from the `ReadOnlyAccount`
- add a `RwLock` on the `StoreTransaction`

Part of #2624 + #2000.

---

* crypto: Replace some uses of `ReadOnlyAccount` with `StaticAccountData` and identify tests

* crypto: introduce `StoreTransaction` to modify a `ReadOnlyAccount`

* crypto: introduce `save_pending_changes`, aka `save_changes` v2

* crypto: Start using `StoreTransaction`s to save the account, get rid of `Store::save_account` + `Account::save`

* crypto: use `StoreTransaction` to save an account in `keys_for_upload`

* crypto: use `StoreTransaction` and the cache in more places

* crypto: remove `Account` from the `Changes` \o/

* crypto: remove last (test-only) callers of `Store::account()`

* crypto: move `ReadOnlyAccount` inside the cache only

* crypto: use `ReadOnlyAccount` and `Account` in fewer places

whenever we can use `StaticAccountData` in place.

* crypto: make tests rely less on OlmMachine

* crypto: Don't put the `ReadOnlyAccount` behind a RwLock just yet

+ clippy
@richvdh
Copy link
Member

richvdh commented Dec 19, 2023

This was mostly done since sled has an API where you pass in a closure which gets executed as part of a transaction. In this closure you write your values to the db and everybody is happy.

But most other databases have a Database::get_transaction(&self) -> &Transaction interface. Crucially SQLite and IndexedDB behave this way.

You have to be careful with IndexedDB transactions: in particular you can't keep them open indefinitely; they will terminate as soon as you stop giving them work to do, so you can't do any async operations. That may be fine here, but it's a footgun that needs bearing in mind.

(Indeed, IndexedDB's actual API also takes closures that you pass into IndexedDB to be executed as part of the transaction. rust-indexed-db just puts an abstraction layer over it to make it look like a regular async thing, but the abstraction is inherently leaky.)

@richvdh
Copy link
Member

richvdh commented Mar 7, 2024

We no longer believe this approach is correct.

@richvdh richvdh closed this as completed Mar 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants