Closed maan2003 closed 1 year ago
As things are RN, I don't understand what is this change doing.
Manmeet is trying to get it so clients can subscribe to events as websocket subscriptions, versus polling over websockets which we do currently. @Maan2003 could you add a little more to the description of the PR?
@m1sterc001guy: could you take a look at the database changes?
Manmeet is trying to get it so clients can subscribe to events as websocket subscriptions, versus polling over websockets which we do currently.
I'm confused then why all the changes are about database transactions. :D
Sorry, I forgot to mark this as draft. This wasn't complete then. Now mostly is. I have updated the PR description.
Thank you all for the reviews. I have simplified the design.
Also wasm is problem. tokio::Notify
isn't available on wasm, and I don't know any suitable replacement.
Update: tokio is better now, we can use tokio
with features sync
on wasm!
Do we ever need to wait for a key prefix? That's when the hash based approach might be bad at.
We could introduce an associated constant for DB keys of the form
const PREFIXES: &'static [usize] = &[8, 40];
and hash every prefix and add it as a separate event.
let serialized_key: &[u8] = …;
for prefix in PREFIXES {
events.add_key_event(serialized_key[0..prefix]);
}
That way we could subscribe to any DatabaseKeyPrefix
and not just DatabaseKey
s.
Notification is general mechanism that can notify for any hashable key.
(Should move Notification
outside of database, and pass it during begin transaction)
Database and Database transaction offer helper over for DBkeys. If we need notification for prefixes, we can add helpers (notify_prefix and wait_prefix) which should be straightforward to implement.
const PREFIXES: &'static [usize] = &[8, 40]; ......
Wow! that is clever. Took some time to understand.
Wow! that is clever. Took some time to understand.
In that case it will be good to document well ^^
Moving ModuleDecoderRegister
into Database
. It simplifies lot of interfaces.
Separated out some database changes into a new PR which should easier to review in isolation.
You won't like the Databaase to Api Endpoint commit. But, because api endpoints have to wait for a key, it can't just work with single transaction :/
But, because api endpoints have to wait for a key, it can't just work with single transaction :/
I think not using transactions and instead awaiting changes is ok. In cases we really need the consistency we can first await all the notifications and then do a read in a separate tx. But I think in most cases we want one value we directly wait for anyway.
It's important that api here enforces correct race-condition-free usage, otherwise we are going to find ourselves debugging random hangs and delays all over the place.
We should not have a standalone wait_key
, because the user must do things in the following order:
If one checks the key first, then register for notification later - it's a bug.
Therefore we should have an API like:
dbtx.get_and_wait(key, |value| {
value.is_some() // or whatever else check the user desires
});
or something similar.
We should not have a standalone wait_key, because the user must do things in the following order:
- register for notifications
- check the key
- if not how they want, sleep
I don't follow. This is not the case for wait_key
RN. It does everything in single call. It is case of Notifications::wait
but that is a low level api.
@dpc
@Maan2003 What @dpc is getting at is that currently we can only safely await the presence of a key, not a certain value. For that we need to:
You can't impl this safely (right order of 1+2) with the current API afaik.
Got it :+1:
What @dpc is getting at is that currently we can only safely await the presence of a key, not a certain value.
@elsirion @Maan2003
Current the user can do:
let value = loop {
let value = dbtx.get(key);
if is_key_good(&value) {
break value;
}
dbtx.wait_key();
}
This has a race condition and can lead to (possibly indefinite) hang. We should have API that can't be misused here, because I suck at spotting issues like this :D
update: notifications are now automatic for keys that NOTIFY
flag set.
Therefore we should have an API like: dbtx.get_and_wait(key, |value| { value.is_some() // or whatever else check the user desires })
implemented, with name wait_key_check
I am not sure about client side changes. need help with strategies. @jkitman
The Retry404 strategy is not needed now.
main change from client's perspective : example /fetch_transaction
earlier server responded with 404 error if transaction wasn't processed
now server waits until the transaction is processed and then responds. or times out with 408 error after 60 sec.
I am not sure about client side changes. need help with strategies.
In general, I think the strategies can be modified to work with the non-polling API. They will subscribe, wait for enough responses that match, then return the result.
The 404 can be eliminated probably, to be replaced with awaiting a transaction id (either accepted or rejected).
UPDATE: first two commits need heavy rebasing (#1331)
Doesn't work at all.
Biggest problem: no way to merge notify_queue from module_tx
to parent_tx
.
One way is to Arc<Mutex<NotifyQueue>>
. But that is too hacky for my liking.
I am inclined to just store a module_instance: Option<ModuleInstanceId>
in DatabaseTransaction
itself instead of #1331
cc @m1sterc001guy
Biggest problem: no way to merge notify_queue from module_tx to parent_tx.
Oh, interesting.
Can you elaborate what's wrong?
@Maan2003 Oh, I think I see it better after a bit of investigation.
Architecturally the problem here is that current functionality tries to implement sending notifications at the top layer (dyn-wrapper DatabaseTransaction
) while IsolateDatabaseTransaction
works on dyn IDatabaseTransaction
wrapping layer.
I think one approach is to move notification sending to be a under the IDatabaseTransaction
: just like IsolatedDatabaseTransaction
currently is.
struct NotifyOnKeyUpdateDatabaseTransaction {
inner_tx: Box<dyn DatabaseTransactio>,
notify: /* some mechanism to send out notifications that is shareable and clonable, see below */,
}
impl NotifyOnKeyUpdateDatabaseTransaction {
fn new(inner: Box<dyn DatabaseTransaction< notify: /* buckets of something */) -> Self { ... }
}
impl IDatabaseTransaction for NotifyOnKeyUpdateDatabaseTransaction {
// impl all methods by calling inner, then sending a notification
}
I also don't know if Notify
is the right primitive to use. It requires registriation, because it's a mpsc channel. Instead we want spmc channel (like tokio::sync::broadcast which is actualy mpmc). This way the part that is notifying can just do a notification once, unconditionally, and anyone that has the receiving end will be woken up, This avoids any registration.
The NotifyOnKeyUpdateDatabaseTransaction
holds N buckets of brodcast::Sender
and just self.buckets[hash].send()
.
The receiving end grabs the self.buckets[hash].clone()
as a "registration" and then .recv()
on it when wants to wait for any updates on this bucket.
So:
dyn IDatabaseTransaction
from the db we wrap it immediately in NotifyOnKeyUpdateDatabaseTransaction
with the copy of notification buckets (from a one main copy in Database
?).IsolatedDatabaseTransaction
is used to wrap the database transaction, it wraps the notification layer as well, which means we get notification delivered with raw keys as expected.I'm unclear on the wait
operation on the IsolatedDatabaseTransaction
. If someone does a wait on that, they probably want the key prefix added to what they are waiting on. But that also means that the wait
operations need to become part of IDatabaseTransaction
trait.
Sorry if I got something wrong, as I have limited time to spend on this interesting problem, RN. Hopefully you can make it work with some changes. If not I'll be happy to discuss more and help one I find more time.
implementing at transaction level is an interesting idea, but we don't have access DatabaseKey::NOTIFY
there.
I am thinking of a hybrid approach. Storing Box<NotifyTx<dyn IDatabaseTransaction>>
inside DatabaseTransaction.
EDIT: static typing won't work, because of need to make nested isolatedtransaction.
This way the part that is notifying can just do a notification once, unconditionally, and anyone that has the receiving end will be woken up, This avoids any registration.
But clone the receiver end is the registration step here. :shrug:
Not sure how that is different from "getting a Notified
future"
But that also means that the wait operations need to become part of IDatabaseTransaction trait.
not possible, you have to start a transaction every time to check if notification was fake or not.
But, we will need a IsolatedDatabase anyways.
issue with wrapping NotifyingTx inside IsolatedTx: NotifyingTx needs to work with DatabaseKey
s and IsolatedTx works with bytes.
we can add a notify: bool
wherever we pass a key
in IDatabaseTransaction
but that also feels weird.
IMO best way is to implement module isolation and notification at DatabaseTransaction
level.
ping @dpc
issue with wrapping NotifyingTx inside IsolatedTx: NotifyingTx needs to work with DatabaseKeys and IsolatedTx works with bytes.
Everything should work with bytes only, no? Serialization/Deserialization is just a dyn-wrapper extra utilities for better usability.
we can add a notify: bool wherever we pass a key in IDatabaseTransaction but that also feels weird.
True, this is a fundamental mismatch. Fist, I don't know if it's worth optimizing. Just send notifications for everything. We are about to do big IO, trying to save some relativey cheap operation seems premature. Make it work, make it right, make it fast - in that order. If we ever figure out that this is a problem, we'll get back and optimize.
Passing an extra bool
(or just some kind of enum KeyType
) doesn't feel too bad to me. It would only happen in IDatabaseTranasaction
which is invisible to the user (for most part). I even kind of like it.
Finally done. I am satisfied with this solution. and no changes to IDatabaseTransaction.
Rest of commits still need rebase, will do soon.
Patch coverage: 78.08
% and project coverage change: +0.05
:tada:
Comparison is base (
f70af44
) 63.57% compared to head (ade6761
) 63.63%.
:mega: This organization is not using Codecov’s GitHub App Integration. We recommend you install it so Codecov can continue to function properly for your repositories. Learn more
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Do you have feedback about the report comment? Let us know in this issue.
This is ready for review. I don't understand the error message. It is really weird.
**BROKEN** plugin-ln_gateway: error=Federation client operation error: MintApiError(FederationError({PeerId(0): Rpc(Call(Custom(ErrorObject { code: ServerError(400), message: \"High level transaction error: The transaction's signature is invalid\", data: None }))), PeerId(1): Rpc(Call(Custom(ErrorObject { code: ServerError(400), message: \"High level transaction error: The transaction's signature is invalid\", data: None })))}))
This is consistently reproducible locally (./scripts/latency-tests.sh)
LGTM, but when
? :D
I have reduced the scope of this PR from removing all polling to just remove polling for /fetch_transaction. This is much smaller change and also passes CI.
Removing polling from module endpoints requires Contexts types to elegantly give access to wait_key
, without just passing a database directly.
Don't merge yet, timeout and task groups integration for api endpoints is still pending.
task groups integration for api endpoints
this is not needed because the api server shuts down correctly already.
arewelandityet.com
Really happy we have this now!
Remain tasks:
[x] integrating task groups
Currently, we use polling to check for updates(say "fetch_transaction").
What we need is to subscribe for change in web server task, and push notification for these changes in the main task. But we can't immediately notify the web server task because the transaction is not yet committed. And the database won't contain the value.
So mainly I see two solution:
Another Attempt #273