-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
RPC Consistency Proposal #2473
base: master
Are you sure you want to change the base?
RPC Consistency Proposal #2473
Conversation
crates/client/src/client.rs
Outdated
pub async fn query_with_headers<ResponseData, Vars>( | ||
&self, | ||
q: Operation<ResponseData, Vars>, | ||
headers: impl IntoIterator<Item = (String, String)>, |
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.
TODO: HeadersMap here?
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 assume this one https://docs.rs/http/1.2.0/http/header/struct.HeaderMap.html - makes sense 👍
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.
Forgot to mention this is done, see d9091e6
}; | ||
|
||
#[cfg(test)] | ||
mod tests; | ||
|
||
// The last known block height that was processed by the GraphQL service. | ||
pub static LAST_KNOWN_BLOCK_HEIGHT: OnceLock<AtomicU32> = OnceLock::new(); |
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.
todo: remove Static and store in Service? Should still be accessible from middleware
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.
Is this so performance critical that we can't do a database lookup for this value? I'd default to querying this from the database, to ensure we don't have any weird consistency issues when this field and the db isn't in sync. I guess this isn't a big problem right now, but it just doesn't feel right to allow for inconsistencies in a field used to synchronize with other nodes.
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.
We shouldn't use static. Each extension has access to the context and you can get ReadDatabase
from the context. ReadDatabase
knows the latest block height.
You can check how ViewExtension
works
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 agree that we should fetch the value from the DB (it is probably going to be fetched from the memtable anyway, since it's frequently updated). I don't agree that we should do this with async_graphql
Extensions, the reason being that support for headers is pretty limited in there.
- Headers do not seem to be set when building the query context in
async_graphql
(see https://github.com/async-graphql/async-graphql/blob/7f1791488463d4e9c5adcd543962173e2f6cbd34/src/schema.rs#L927).
I have confirmed this by applying the patch below to my PR and running the test. The output confirms that the header is not set by the time we execute the query.
diff --git a/crates/fuel-core/src/schema/balance.rs b/crates/fuel-core/src/schema/balance.rs
index b6b95228e8..956d5961bb 100644
--- a/crates/fuel-core/src/schema/balance.rs
+++ b/crates/fuel-core/src/schema/balance.rs
@@ -60,6 +60,10 @@ impl BalanceQuery {
#[graphql(desc = "address of the owner")] owner: Address,
#[graphql(desc = "asset_id of the coin")] asset_id: AssetId,
) -> async_graphql::Result<Balance> {
+ println!(
+ "header set: {}",
+ ctx.http_header_contains("REQUIRED_FUEL_BLOCK_HEIGHT")
+ );
let query = ctx.read_view()?;
let base_asset_id = *ctx
.data_unchecked::<ConsensusProvider>()
- Maybe we could get the header somehow from the ExtensionContext, but this is going to be an ad hoc, undocumented solution and likely not readable.
What I think we should do instead is stick with Axum's middleware: get a handle to the OnchainDatabase from the CombinedDatabase (just because I don't want to wrap the instance to the CombinedDatabase inside an Arc), and use it to fetch the value of the last height in the middleware.
Let me know what you think.
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.
Nice stuff! I have some thoughts and comments, but no showstoppers.
crates/client/src/client.rs
Outdated
pub async fn query_with_headers<ResponseData, Vars>( | ||
&self, | ||
q: Operation<ResponseData, Vars>, | ||
headers: impl IntoIterator<Item = (String, String)>, |
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 assume this one https://docs.rs/http/1.2.0/http/header/struct.HeaderMap.html - makes sense 👍
crates/client/src/client.rs
Outdated
.query_with_headers( | ||
query, | ||
vec![( | ||
"REQUIRED_FUEL_BLOCK_HEIGHT".to_string(), |
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.
Hmm, I'm not 100% satisfied with this name. Especially since we provide a block height as input to some queries. When squinting your eyes it could be confusing if the "required_bock_height" is part of the query, when it is more a notion of the state of the node to ensure we're in sync.
How would REQUIRED_CHAIN_TIP_HEIGHT
sound to you? Here it's clear that we're referring to the block height of the chain tip.
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 agree that we could have a better name (maybe CHAIN_HEAD
instead of CHAIN_TIP
) , but if we change it we have to notify other teams as well as we are deviating from the spec: see for example FuelLabs/fuels-ts#3443
crates/client/src/client.rs
Outdated
pub async fn balance_with_required_block_header( | ||
&self, | ||
owner: &Address, | ||
asset_id: Option<&AssetId>, | ||
required_block_height: u32, | ||
) -> io::Result<u128> { |
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'm a bit hesitant to have the required_block_height
be a parameter here. It feels like we're cluttering the interface and are increasing the cognitive load for our users. This feels more like a state that should be maintained by the client and not even directly exposed to the user.
I'd imagine the client maintaining a chain_tip
parameter locally which is updated whenever it receives a response, and it's appended to the header of all requests made.
What are your thoughts on this?
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.
If I understood correctly you suggest to change the client to have the headers it will send in the requests in its state. I think that makes a lot of sense, let me have a go :)
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.
That was a good suggestion :) I have implemented functionalities to set/remove headers for the client in d9091e6 and it looks much better.
}; | ||
|
||
#[cfg(test)] | ||
mod tests; | ||
|
||
// The last known block height that was processed by the GraphQL service. | ||
pub static LAST_KNOWN_BLOCK_HEIGHT: OnceLock<AtomicU32> = OnceLock::new(); |
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.
Is this so performance critical that we can't do a database lookup for this value? I'd default to querying this from the database, to ensure we don't have any weird consistency issues when this field and the db isn't in sync. I guess this isn't a big problem right now, but it just doesn't feel right to allow for inconsistencies in a field used to synchronize with other nodes.
.get() | ||
//Maybe too strict? |
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 find it weird that this can be unset. We could always default to 0 as well, which would just mean that we're not in sync yet (which I presume we're not if this value hasn't been set).
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.
It will be unset until the first block is processed. But if we are going to fetch the value directly from the DB then it won't be the case anymore. I am waiting to hear what approach @xgreenx prefers before refactoring this bit
Request as AxumRequest, | ||
Response as AxumResponse, |
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.
Nit: This is a re-export so I'd alias it to HttpRequest
, or even better just import axum::http
and keep the namespacing: http::Request
.
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.
agreed and done: 90c411f
Co-authored-by: Mårten Blankfors <[email protected]>
crates/fuel-core/src/graphql_api/required_fuel_block_height_extension.rs
Show resolved
Hide resolved
crates/fuel-core/src/graphql_api/required_fuel_block_height_extension.rs
Outdated
Show resolved
Hide resolved
crates/fuel-core/src/graphql_api/required_fuel_block_height_extension.rs
Outdated
Show resolved
Hide resolved
crates/fuel-core/src/graphql_api/required_fuel_block_height_extension.rs
Outdated
Show resolved
Hide resolved
Hi @acerone85, Could you kindly confirm if the response header for subscriptions is also intended to include |
In the current implementation there is no response header for the current_fuel_block_height. Would it be desirable to have one? |
) -> Result<&mut Self, Error> { | ||
let header_value: HeaderValue = value | ||
.try_into() | ||
.map_err(|_err| anyhow::anyhow!("Cannot parse value for header"))?; |
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.
If we now have an proper Error
, maybe we should introduce specific variant instead of anyhow
?
crates/client/src/client.rs
Outdated
for (header_name, header_value) in self.headers.iter() { | ||
request_builder = request_builder.header(header_name, header_value); | ||
} |
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.
Can we use headers()
to avoid manual loop, like:
request_builder = request_builder.headers(&self.headers);
crates/fuel-core/src/graphql_api/required_fuel_block_height_extension.rs
Outdated
Show resolved
Hide resolved
Co-authored-by: Rafał Chabowski <[email protected]>
@acerone85 I believe it would because I have seen the following scenario happening a few times when submitting TXs sequentially: flowchart TD
%% Transaction n1
A1[Start TX n1] --> A2[Query coinsToSpend to fetch resources]
A2 --> A3[Submit TX n1 using submitAndAwaitStatus]
A3 --> A4[Await status update]
A4 --> A5[Success]
A5 --> B1[Start TX n2]
%% Transaction n+1
B1 --> B2[Query coinsToSpend]
B2 --> B3[Query hits unsync node, fetching a spent UTXO on TX n1]
B3 --> B4[Submit TX n2 using submitAndAwaitStatus]
B4 --> B5[Sync node validates UTXO was spent]
B5 --> B6[Error]
|
Linked Issues/PRs
Related issue: #1897
See https://www.notion.so/fuellabs/RPC-Consistency-Proposal-V2-13b2f2293f31809bbce0d93a4c28d633 for the proposal specs.
We will implement an improvement where the server node will wait for a configurable number of blocks before returning a
PRECONDITION FAILED
response, in a follow-up PR.Entrypoint for reviews:
Manual testing
Checking that the response header is set, with a node listening on port 4000
TODO:
Description
Checklist
Breaking changes are clearly marked as such in the PR description and changelogBefore requesting review
I have created follow-up issues caused by this PR and linked them hereAfter merging, notify other teams
[Add or remove entries as needed]