OctoBase icon indicating copy to clipboard operation
OctoBase copied to clipboard

improve design for apis with transaction

Open darkskygit opened this issue 2 years ago • 4 comments

Currently all workspace/block apis require transaction, we need call the api with transaction like block.xxx(&trx, ..)

Consider providing the Workspace/Block type encapsulated by with_trx, which includes trx:

struct WrappedBlock {
  block: Block, // original block type
  trx: Transact,// transaction
}

struct WrappedWorkspace {
  workspace: Workspace, // original workspace type
  trx: Transact,// transaction
}

workspace.with_trx(|ws: WrappedWorkspace| {
  let block: WrappedBlock = ws.get(&block_id);
  block.set("key", value);
});

darkskygit avatar Mar 02 '23 19:03 darkskygit

We can add two structs: Transaction<'doc, T> and TransactionMut<'doc, T>

pub struct Transaction<'doc, T> {
    inner: &'doc T,
    txn: yrs::Transaction<'doc>,
}

impl<'doc, T> Transaction<'doc, T> {
    pub fn new(inner: &'doc T, txn: yrs::Transaction<'doc>) -> Self {
        Self { inner, txn }
    }
}

pub struct TransactionMut<'doc, T> {
    inner: &'doc T,
    txn: yrs::TransactionMut<'doc>,
}

impl<'doc, T> TransactionMut<'doc, T> {
    pub fn new(inner: &'doc T, txn: yrs::TransactionMut<'doc>) -> Self {
        Self { inner, txn }
    }

    pub fn encode_update_v1(&self) -> Vec<u8> {
        self.txn.encode_update_v1()
    }
}

impl<'doc> TransactionMut<'doc, Workspace> {
    pub fn create<B, F>(&mut self, block_id: B, flavor: F) -> Block
    where
        B: AsRef<str>,
        F: AsRef<str>,
    {
        info!("create block: {}", block_id.as_ref());
        Block::new(
            &mut self.txn,
            self.inner,
            block_id,
            flavor,
            self.inner.client_id(),
        )
    }
}

impl<'doc> TransactionMut<'doc, Block> {
    pub fn set<K, T>(&mut self, key: K, value: T)
    where
        K: AsRef<str>,
        T: Into<Any>,
    {
        self.inner.set(&mut self.txn, key.as_ref(), value)
    }
}

And add tow methods to Workspace and Block

    pub fn with_transact<T>(&self, f: impl FnOnce(crate::Transaction<'_, Self>) -> T) -> T {
        f(crate::Transaction::new(self, self.doc.transact()))
    }

    pub fn with_transact_mut<T>(&self, f: impl FnOnce(crate::TransactionMut<'_, Self>) -> T) -> T {
        f(crate::TransactionMut::new(self, self.doc.transact_mut()))
    }

If we want to add Block to Workspace, we can do this:

                workspace.with_transact_mut(|mut t| {
                    let block = t.create(&block_id, "text");

                    let count = block.with_transact_mut(|mut t| {
                        payload
                            .into_iter()
                            .filter_map(|(key, value)| {
                                serde_json::from_value::<Any>(value)
                                    .map(|value| t.set(&key, value))
                                    .ok()
                            })
                            .count()
                    });

                    Some(((count > 0).then(|| t.encode_update_v1()), block))
                })

fundon avatar Mar 03 '23 11:03 fundon

And add tow methods to Workspace and Block

    pub fn with_transact<T>(&self, f: impl FnOnce(crate::Transaction<'_, Self>) -> T) -> T {
        f(crate::Transaction::new(self, self.doc.transact()))
    }

    pub fn with_transact_mut<T>(&self, f: impl FnOnce(crate::TransactionMut<'_, Self>) -> T) -> T {
        f(crate::TransactionMut::new(self, self.doc.transact_mut()))
    }

It can be abstracted into a Trait.

If we want to add Block to Workspace, we can do this:

                workspace.with_transact_mut(|mut t| {
                    let block = t.create(&block_id, "text");

                    let count = block.with_transact_mut(|mut t| {
                        payload
                            .into_iter()
                            .filter_map(|(key, value)| {
                                serde_json::from_value::<Any>(value)
                                    .map(|value| t.set(&key, value))
                                    .ok()
                            })
                            .count()
                    });

                    Some(((count > 0).then(|| t.encode_update_v1()), block))
                })

Or

block.transaction(|mut t| {}, &mut txn);

block.transaction(|mut t| {});

let transaction = block.transaction();

fundon avatar Mar 03 '23 23:03 fundon

i prefer to abstract into traits

Another thing that can be improved is that we need to do some error handling for transform/transact_mut -- i means switch to try_transact/try_transact_mut

The transact function call try_transact in internal and unwrap it directly, this will cause thread panic in a multi-threaded environment.

It may be a good choice to rewrite with_trx as an async function:

// try to get transaction internally with try_transact and automatically retry
workspace.with_transact(|t| {
    // do something with transaction
}).await;

darkskygit avatar Mar 04 '23 06:03 darkskygit

https://github.com/toeverything/OctoBase/blob/edb3f2ce0aa697374b92c87af7b3df3703a96b7d/libs/jwst/src/workspaces/workspace.rs#L247-L249

Direct unwrap is definitely wrong.

Using try_read/try_write for lock. This also corresponds to the try_transact/try_transact_mut.

For error handling, Result<Response> is a bit better.

fundon avatar Mar 05 '23 00:03 fundon