-
Notifications
You must be signed in to change notification settings - Fork 22
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
Provide store_batch
function to storage backend in order to save batch of values atomically
#1078
Conversation
/// Store batch of key/value pairs in one transaction | ||
async fn store_batch(&mut self, keys_values: Vec<(Bytes, Bytes)>) -> Result<(), Self::Error>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with this is that if somehow the backend does not support it we are forcing its implementation. Instead, this should be done through execute
if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a comment with a proposal. I see the problem we are facing better now. We would end up with the same problem I stated above but in a different place. But I think the other way is a bit better to reuse the current API without changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in this case we could add the call to be implemented by the storage transaction type:
/// Trait to abstract storage transactions return and operation types
pub trait StorageTransaction: Send + Sync {
type Result: Send + Sync;
type Transaction: Send + Sync;
fn batch_store(to_store: Vec<(Bytes, Bytes)>) -> Transaction;
}
That should build the computation to create a batch store transaction.
wdyt?
I feel like that I have some more "fundamental" things unclear about the I have been thinking how our transaction logic matches with the idea of database transaction. In the sense that it does something atomically from database perspective. I think that our current transaction "framework" actually doesn't really do it by default. For example, in RocksDb test, it makes multiple DB calls in a single closure but this doesn't use database implementation transaction functionality. let mut db: RocksBackend<NoStorageSerde> = RocksBackend::new(sled_settings)?;
let txn = db.txn(|db| {
let key = "foo";
let value = "bar";
db.put(key, value)?;
let result = db.get(key)?;
db.delete(key)?;
Ok(result.map(|ivec| ivec.to_vec().into()))
});
let result = db.execute(txn).await??; Here Here we could potentially ask actual db transaction from Another thing that is a bit unclear to me is how to create transaction object from an OverWatch service. Execute message takes transaction as argument. Execute {
transaction: Backend::Transaction,
reply_channel:
tokio::sync::oneshot::Sender<<Backend::Transaction as StorageTransaction>::Result>,
}
For example RocksDb transaction can be constructed like this: impl<SerdeOp> RocksBackend<SerdeOp> {
pub fn txn(
&self,
executor: impl FnOnce(&DB) -> Result<Option<Bytes>, Error> + Send + Sync + 'static,
) -> Transaction {
Transaction {
rocks: self.rocks.clone(),
executor: Box::new(executor),
}
}
} This requires caller to have access to RocksBackend and OverWatch service doesn't have it. I was thinking maybe it makes sense to change transaction type in Execute {
transaction: OverWatch::Transaction,
... Also it seems that we may need to switch to OverWatch service needs to prepare this Here So I guess my current feeling is that it's not straightforward to use existing |
Yes. The |
I am thinking that ideally we have an abstract type for the Maybe a good compromise for most use cases is to have something like (Actually I suggested to have |
I dont think we can abstract an execute generically because it hard depends on the actual underlying implementation. The only thing we can do here if abstract in the |
I agree it's good to keep KV interface the same. Perhaps another option is to implement an additional trait for db backend implementation and bring it into scope when needed. trait StorageExt {
fn write_in_batch(...)
}
impl StorageExt for rocksdb:DB {
fn write_in_batch(...)
} use StorageExt;
fn store<DB: StorageExt>() {
let exec = |db: DB| {
db.write_in_batch(...);
}
} |
Yeah, this similar extending the current |
@andrussal as per our last conversation. I think we can simply close this one right? |
|
Yep, it's not worth doing it |
1. What does this PR implement?
Provide
store_batch
function to storage backend in order to save batch of values atomically.Not sure if this is the right way to go. Just opened for comments.
The reason of creating this change is related to this PR where we should store multiple keys atomically: #1075
Justification why this function is needed in addition to existing storage interface.
Current storage interface provides general purpose
execute
function that allows to run arbitrary code within a context of a transaction. In order to call this functions, caller needs to provide backend specific transaction instance. It seems that currently it’s not straightforward to create such Transaction from an OverWatch service.Another justification to add this function to storage interface is that this should be a fairly common use case how transactions are used in the system - storing batch of values together.
Alternately could try to refactor current
execute
solution so that it doesn’t require passing backend specific Transaction instance. I started looking into it but this seems to be much larger effort than providing batch function directly.2. Does the code have enough context to be clearly understood?
Yes
3. Who are the specification authors and who is accountable for this PR?
@andrussal
4. Is the specification accurate and complete?
N/A
5. Does the implementation introduce changes in the specification?
N/A
Checklist