Fix Sse client api (#6685)

* Use reqwest eventsource for get_events api

* await for Event::Open before returning stream

* fmt

* Merge branch 'unstable' into sse-client-fix

* Ignore lint
This commit is contained in:
Pawan Dhananjay
2024-12-18 05:35:58 +05:30
committed by GitHub
parent 1315c94adb
commit 2662dc7f8f
5 changed files with 65 additions and 29 deletions

28
Cargo.lock generated
View File

@@ -2576,6 +2576,7 @@ dependencies = [
"proto_array", "proto_array",
"psutil", "psutil",
"reqwest", "reqwest",
"reqwest-eventsource",
"sensitive_url", "sensitive_url",
"serde", "serde",
"serde_json", "serde_json",
@@ -2977,6 +2978,17 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "eventsource-stream"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab"
dependencies = [
"futures-core",
"nom",
"pin-project-lite",
]
[[package]] [[package]]
name = "execution_engine_integration" name = "execution_engine_integration"
version = "0.1.0" version = "0.1.0"
@@ -7179,6 +7191,22 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "reqwest-eventsource"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f529a5ff327743addc322af460761dff5b50e0c826b9e6ac44c3195c50bb2026"
dependencies = [
"eventsource-stream",
"futures-core",
"futures-timer",
"mime",
"nom",
"pin-project-lite",
"reqwest",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "resolv-conf" name = "resolv-conf"
version = "0.7.0" version = "0.7.0"

View File

@@ -2796,6 +2796,7 @@ async fn finalizes_after_resuming_from_db() {
); );
} }
#[allow(clippy::large_stack_frames)]
#[tokio::test] #[tokio::test]
async fn revert_minority_fork_on_resume() { async fn revert_minority_fork_on_resume() {
let validator_count = 16; let validator_count = 16;

View File

@@ -27,6 +27,7 @@ slashing_protection = { workspace = true }
mediatype = "0.19.13" mediatype = "0.19.13"
pretty_reqwest_error = { workspace = true } pretty_reqwest_error = { workspace = true }
derivative = { workspace = true } derivative = { workspace = true }
reqwest-eventsource = "0.5.0"
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true } tokio = { workspace = true }

View File

@@ -27,6 +27,7 @@ use reqwest::{
Body, IntoUrl, RequestBuilder, Response, Body, IntoUrl, RequestBuilder, Response,
}; };
pub use reqwest::{StatusCode, Url}; pub use reqwest::{StatusCode, Url};
use reqwest_eventsource::{Event, EventSource};
pub use sensitive_url::{SensitiveError, SensitiveUrl}; pub use sensitive_url::{SensitiveError, SensitiveUrl};
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use ssz::Encode; use ssz::Encode;
@@ -52,6 +53,8 @@ pub const SSZ_CONTENT_TYPE_HEADER: &str = "application/octet-stream";
pub enum Error { pub enum Error {
/// The `reqwest` client raised an error. /// The `reqwest` client raised an error.
HttpClient(PrettyReqwestError), HttpClient(PrettyReqwestError),
/// The `reqwest_eventsource` client raised an error.
SseClient(reqwest_eventsource::Error),
/// The server returned an error message where the body was able to be parsed. /// The server returned an error message where the body was able to be parsed.
ServerMessage(ErrorMessage), ServerMessage(ErrorMessage),
/// The server returned an error message with an array of errors. /// The server returned an error message with an array of errors.
@@ -93,6 +96,13 @@ impl Error {
pub fn status(&self) -> Option<StatusCode> { pub fn status(&self) -> Option<StatusCode> {
match self { match self {
Error::HttpClient(error) => error.inner().status(), Error::HttpClient(error) => error.inner().status(),
Error::SseClient(error) => {
if let reqwest_eventsource::Error::InvalidStatusCode(status, _) = error {
Some(*status)
} else {
None
}
}
Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(),
Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(),
Error::StatusCode(status) => Some(*status), Error::StatusCode(status) => Some(*status),
@@ -2592,16 +2602,29 @@ impl BeaconNodeHttpClient {
.join(","); .join(",");
path.query_pairs_mut().append_pair("topics", &topic_string); path.query_pairs_mut().append_pair("topics", &topic_string);
Ok(self let mut es = EventSource::get(path);
.client // If we don't await `Event::Open` here, then the consumer
.get(path) // will not get any Message events until they start awaiting the stream.
.send() // This is a way to register the stream with the sse server before
.await? // message events start getting emitted.
.bytes_stream() while let Some(event) = es.next().await {
.map(|next| match next { match event {
Ok(bytes) => EventKind::from_sse_bytes(bytes.as_ref()), Ok(Event::Open) => break,
Err(e) => Err(Error::HttpClient(e.into())), Err(err) => return Err(Error::SseClient(err)),
})) // This should never happen as we are guaranteed to get the
// Open event before any message starts coming through.
Ok(Event::Message(_)) => continue,
}
}
Ok(Box::pin(es.filter_map(|event| async move {
match event {
Ok(Event::Open) => None,
Ok(Event::Message(message)) => {
Some(EventKind::from_sse_bytes(&message.event, &message.data))
}
Err(err) => Some(Err(Error::SseClient(err))),
}
})))
} }
/// `POST validator/duties/sync/{epoch}` /// `POST validator/duties/sync/{epoch}`

View File

@@ -13,7 +13,7 @@ use serde_json::Value;
use ssz::{Decode, DecodeError}; use ssz::{Decode, DecodeError};
use ssz_derive::{Decode, Encode}; use ssz_derive::{Decode, Encode};
use std::fmt::{self, Display}; use std::fmt::{self, Display};
use std::str::{from_utf8, FromStr}; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use types::beacon_block_body::KzgCommitments; use types::beacon_block_body::KzgCommitments;
@@ -1153,24 +1153,7 @@ impl<E: EthSpec> EventKind<E> {
} }
} }
pub fn from_sse_bytes(message: &[u8]) -> Result<Self, ServerError> { pub fn from_sse_bytes(event: &str, data: &str) -> Result<Self, ServerError> {
let s = from_utf8(message)
.map_err(|e| ServerError::InvalidServerSentEvent(format!("{:?}", e)))?;
let mut split = s.split('\n');
let event = split
.next()
.ok_or_else(|| {
ServerError::InvalidServerSentEvent("Could not parse event tag".to_string())
})?
.trim_start_matches("event:");
let data = split
.next()
.ok_or_else(|| {
ServerError::InvalidServerSentEvent("Could not parse data tag".to_string())
})?
.trim_start_matches("data:");
match event { match event {
"attestation" => Ok(EventKind::Attestation(serde_json::from_str(data).map_err( "attestation" => Ok(EventKind::Attestation(serde_json::from_str(data).map_err(
|e| ServerError::InvalidServerSentEvent(format!("Attestation: {:?}", e)), |e| ServerError::InvalidServerSentEvent(format!("Attestation: {:?}", e)),