-
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.
Sorry completely lost this discussion, I think this makes sense. I guess you've already implemented this. Re-reviewing now...
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.
for context: Headers cannot be used anymore, the reason being that subscriptions will return multiple graphql responses in a single HTTP response (and therefore have a single HeaderMap), while we require that each item in the response has its associated CURRENT_FUEL_BLOCK_HEIGHT
field. Moreover the Response metadata for subscriptions is returned before the actual response body (as it is standard with HTTP).
in the end graphql has a (very undocumented) way to add an extensions
field to responses, at the same level of data/errors
field. This does not require to tweak anything at the HTTP level.
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 👍
}; | ||
|
||
#[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
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? |
This reverts commit dcec472.
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.
Looks good to me. The current solution pushes more responsibility on the caller for constructing and maintaining the headers in the client. While this is fine, I wonder if we should take more responsibility for this particular header to relieve users from having to think about it.
I'd imagine FuelClient
to just hold a required_fuel_block_height
parameter which it maintains internally (it could be exposed to users as well), and uses to construct the header when making requests. When receiving responses it would update the required_fuel_block_height
with the returned block height for subsequent requests.
crates/fuel-core/src/graphql_api/required_fuel_block_height_extension.rs
Show resolved
Hide resolved
serde_json::to_value(self.extensions.clone())?, | ||
); | ||
|
||
println!("{}", serde_json::to_string_pretty(&operation_json).unwrap()); |
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.
println!("{}", serde_json::to_string_pretty(&operation_json).unwrap()); |
.await | ||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; | ||
|
||
println!("Response: {}", response); |
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.
println!("Response: {}", response); |
let operation_object = operation_json | ||
.as_object_mut() | ||
.expect("Graphql operation is a valid json object"); |
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 don't think that you can be sure that it is an object. It would be better if you checked it via if let Value::Object() ...
let response = schema.execute(req.0).await; | ||
|
||
response.into() |
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.
let response = schema.execute(req.0).await; | |
response.into() | |
schema.execute(req.0).await.into() |
} | ||
|
||
async fn graphql_subscription_handler( | ||
schema: Extension<CoreSchema>, | ||
req: Json<Request>, | ||
) -> Sse<impl Stream<Item = anyhow::Result<Event, serde_json::Error>>> { | ||
let request = req.0; |
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.
let request = req.0; |
let stream = schema | ||
.execute_stream(req.0) | ||
.execute_stream(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.
.execute_stream(request) | |
.execute_stream(req.0) |
@@ -223,6 +250,34 @@ impl FuelClient { | |||
Self::from_str(url.as_ref()) | |||
} | |||
|
|||
pub fn set_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.
I think we don't use it and can remove
self.extensions.remove("required_fuel_block_height"); | ||
self | ||
} | ||
|
||
/// Send the GraphQL query to the client. | ||
pub async fn query<ResponseData, Vars>( |
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.
What about fn subscribe
? We also want to support setting heights for the submit_and_await_commit
like queries
|
||
#[tokio::test] | ||
async fn current_fuel_block_height_header_is_present_on_successful_request() { | ||
// TODO: Figure out a way to get the current fuel block height from FuelClient queries |
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 it is very important to extract the current height in fuel-core-client
. But yeah, it can be done in a follow up PR. Could you create an issue to track it and put link in todos(below you have the same todos).
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.
Also in proposal we wanted to return 412. But based on the implementation we just return an error. Can we customize the number?
Linked Issues/PRs
Related issue: #1897
See https://www.notion.so/fuellabs/RPC-Consistency-Proposal-V2-13b2f2293f31809bbce0d93a4c28d633 for the proposal specs.
This PR implements a variation of the proposal in the link, due to techincal limitations of the HTTP protocol when using graphql subscriptions.
for context: Headers cannot be used, the reason being that subscriptions will return multiple graphql responses in a single HTTP response (and therefore have a single
HeaderMap
), while we require that each item in the response has its associatedCURRENT_FUEL_BLOCK_HEIGHT
field. Moreover the Response metadata for subscriptions is returned before the actual response body (as it is standard with HTTP).in the end graphql has a (very undocumented) way to add an
extensions
field to responses, at the same level ofdata/errors
field. This does not require to tweak anything at the HTTP level.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
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]