use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
time::{Duration, Instant},
};
use crate::config::{
demand_control::DemandControlMode,
override_subgraph_urls::{OverrideSubgraphUrlsConfig, UrlOrExpression},
subscriptions::{SubscriptionProtocol, SupergraphSubscriptionsConfig},
traffic_shaping::{
DurationOrExpression, StatusCodeMatcher, SupergraphTrafficShapingConfig,
WebSocketExecuteMode,
},
};
use crate::executor::executors::inflight::InFlightMap;
use crate::telemetry::logging::{summary, targets};
use crate::telemetry::TelemetryContext;
use crate::vrl::expressions::{
CompileExpression, DurationOrProgram, ExecutableProgram, ExpressionCompileError, ProgramHints,
ValueOrProgram, VrlFunction, VrlProgram, VrlValue,
};
use dashmap::DashMap;
use futures::{stream::BoxStream, FutureExt};
use hive_console_sdk::circuit_breaker::{CircuitBreakerBuilder, CircuitBreakerError};
use http::{StatusCode, Uri};
use hyper_util::{
client::legacy::Client,
rt::{TokioExecutor, TokioTimer},
};
use recloser::AsyncRecloser;
use tokio::sync::Semaphore;
use tracing::{debug, error};
use crate::executor::{
execution::{
client_request_details::ClientRequestDetails, demand_control::DemandControlExecutionContext,
},
executors::{
common::{SubgraphExecutionRequest, SubgraphExecutor, SubgraphExecutorBoxedArc},
error::SubgraphExecutorError,
http::{HTTPSubgraphExecutor, HttpClient, SubgraphHttpResponse},
http_callback::{CallbackSubscriptionsMap, HttpCallbackSubgraphExecutor},
tls::{build_https_client_config, build_https_connector, get_merged_tls_config},
websocket::WsSubgraphExecutor,
websocket_pool::{WebSocketConnectionId, WebSocketPool},
},
hooks::on_subgraph_execute::{
OnSubgraphExecuteEndHookPayload, OnSubgraphExecuteStartHookPayload,
},
plugin_context::PluginRequestState,
plugin_trait::{EndControlFlow, StartControlFlow},
plugins::hooks,
response::subgraph_response::SubgraphResponse,
};
type SubgraphName = String;
type SubgraphEndpoint = String;
type ExecutorsBySubgraphMap =
DashMap<SubgraphName, DashMap<SubgraphEndpoint, SubgraphExecutorBoxedArc>>;
type StaticEndpointsBySubgraphMap = DashMap<SubgraphName, SubgraphEndpoint>;
type ExpressionEndpointsBySubgraphMap = HashMap<SubgraphName, VrlProgram>;
type TimeoutsBySubgraph = DashMap<SubgraphName, DurationOrProgram>;
#[derive(Default)]
struct GlobalSubgraphUrlOverride {
ignored_subgraphs: Vec<SubgraphName>,
program: Option<VrlProgram>,
}
impl GlobalSubgraphUrlOverride {
fn new(all_url_config: Option<&str>) -> Result<Self, SubgraphExecutorError> {
let Some(expression) = all_url_config else {
return Ok(Self::default());
};
let program = expression.compile_expression(None).map_err(|err| {
SubgraphExecutorError::EndpointExpressionBuild("all".to_string(), err.diagnostics)
})?;
Ok(Self {
ignored_subgraphs: Vec::new(),
program: Some(program),
})
}
fn ignore_subgraph(&mut self, name: SubgraphName) {
self.ignored_subgraphs.push(name);
}
fn get_expression_for_subgraph(&self, name: &str) -> Option<&VrlProgram> {
(!self.ignored_subgraphs.iter().any(|n| n.as_str() == name))
.then_some(self.program.as_ref())
.flatten()
}
}
#[derive(Clone)]
struct SubgraphCircuitBreaker {
recloser: AsyncRecloser,
error_status_codes: Arc<Vec<StatusCodeMatcher>>,
}
type CircuitBreakersBySubgraph = DashMap<SubgraphName, SubgraphCircuitBreaker>;
lazy_static::lazy_static! {
static ref DEFAULT_CIRCUIT_BREAKER_ERROR_STATUS_CODES: Arc<Vec<StatusCodeMatcher>> = Arc::new(
vec![
StatusCodeMatcher::Exact(StatusCode::INTERNAL_SERVER_ERROR),
StatusCodeMatcher::Exact(StatusCode::BAD_GATEWAY),
StatusCodeMatcher::Exact(StatusCode::SERVICE_UNAVAILABLE),
StatusCodeMatcher::Exact(StatusCode::GATEWAY_TIMEOUT),
],
);
}
struct ResolvedSubgraphConfig<'a> {
client: Arc<HttpClient>,
timeout_config: &'a DurationOrExpression,
dedupe_enabled: bool,
}
pub type InflightRequestsMap = InFlightMap<u64, (SubgraphHttpResponse, u64)>;
#[derive(Clone)]
pub struct HttpCallbackRuntimeConfig {
pub public_url: Uri,
pub heartbeat_interval: Duration,
}
struct SubgraphExecutorConfig {
traffic_shaping: SupergraphTrafficShapingConfig,
override_subgraph_urls: OverrideSubgraphUrlsConfig,
subscriptions: SupergraphSubscriptionsConfig,
callback: Option<HttpCallbackRuntimeConfig>,
}
pub struct SubgraphExecutorMap {
http_executors_by_subgraph: ExecutorsBySubgraphMap,
subscription_executors_by_subgraph: ExecutorsBySubgraphMap,
static_endpoints_by_subgraph: StaticEndpointsBySubgraphMap,
expression_endpoints_by_subgraph: ExpressionEndpointsBySubgraphMap,
all_endpoint_expression: GlobalSubgraphUrlOverride,
timeouts_by_subgraph: TimeoutsBySubgraph,
circuit_breakers_by_subgraph: CircuitBreakersBySubgraph,
global_timeout: DurationOrProgram,
config: Arc<SubgraphExecutorConfig>,
client: Arc<HttpClient>,
semaphores_by_origin: DashMap<String, Arc<Semaphore>>,
max_connections_per_host: usize,
in_flight_requests: InflightRequestsMap,
telemetry_context: Arc<TelemetryContext>,
callback_subscriptions: CallbackSubscriptionsMap,
websocket_pool: Arc<WebSocketPool>,
}
impl SubgraphExecutorMap {
fn new(
config: Arc<SubgraphExecutorConfig>,
global_timeout: DurationOrProgram,
telemetry_context: Arc<TelemetryContext>,
) -> Result<Self, SubgraphExecutorError> {
let mut client_builder = Client::builder(TokioExecutor::new());
client_builder
.pool_timer(TokioTimer::new())
.pool_idle_timeout(config.traffic_shaping.all.pool_idle_timeout)
.pool_max_idle_per_host(config.traffic_shaping.max_connections_per_host);
if config.traffic_shaping.all.allow_only_http2 {
client_builder.http2_only(true);
}
let client: HttpClient = client_builder.build(build_https_connector(
config.traffic_shaping.all.tls.as_ref(),
)?);
let max_connections_per_host = config.traffic_shaping.max_connections_per_host;
Ok(SubgraphExecutorMap {
http_executors_by_subgraph: Default::default(),
subscription_executors_by_subgraph: Default::default(),
static_endpoints_by_subgraph: Default::default(),
expression_endpoints_by_subgraph: Default::default(),
all_endpoint_expression: Default::default(),
config,
client: Arc::new(client),
semaphores_by_origin: Default::default(),
max_connections_per_host,
in_flight_requests: InFlightMap::default(),
timeouts_by_subgraph: Default::default(),
circuit_breakers_by_subgraph: Default::default(),
global_timeout,
telemetry_context,
callback_subscriptions: Arc::new(DashMap::new()),
websocket_pool: Arc::new(WebSocketPool::default()),
})
}
pub fn from_http_endpoint_map(
subgraph_endpoint_map: &HashMap<SubgraphName, String>,
traffic_shaping: SupergraphTrafficShapingConfig,
override_subgraph_urls: OverrideSubgraphUrlsConfig,
subscriptions: SupergraphSubscriptionsConfig,
callback: Option<HttpCallbackRuntimeConfig>,
telemetry_context: Arc<TelemetryContext>,
active_callback_subscriptions: CallbackSubscriptionsMap,
) -> Result<Self, SubgraphExecutorError> {
let config = Arc::new(SubgraphExecutorConfig {
traffic_shaping,
override_subgraph_urls,
subscriptions,
callback,
});
let global_timeout =
compile_duration_or_expression(&config.traffic_shaping.all.request_timeout, None)
.map_err(|err| {
SubgraphExecutorError::RequestTimeoutExpressionBuild(
"all".to_string(),
err.diagnostics,
)
})?;
let mut subgraph_executor_map =
SubgraphExecutorMap::new(config.clone(), global_timeout, telemetry_context)?;
subgraph_executor_map.callback_subscriptions = active_callback_subscriptions;
let mut global_url_override =
GlobalSubgraphUrlOverride::new(config.override_subgraph_urls.get_all_url())?;
for (subgraph_name, original_endpoint_str) in subgraph_endpoint_map.iter() {
let endpoint_config = config
.override_subgraph_urls
.get_subgraph_url(subgraph_name);
let endpoint_str = match endpoint_config {
Some(UrlOrExpression::Url(url)) => {
global_url_override.ignore_subgraph(subgraph_name.clone());
url
}
Some(UrlOrExpression::Expression { expression }) => {
global_url_override.ignore_subgraph(subgraph_name.clone());
subgraph_executor_map
.register_endpoint_expression(subgraph_name, expression)?;
original_endpoint_str
}
None => original_endpoint_str,
};
subgraph_executor_map.register_static_endpoint(subgraph_name, endpoint_str);
subgraph_executor_map.register_executor(subgraph_name, endpoint_str, false)?;
subgraph_executor_map.register_subgraph_timeout(subgraph_name)?;
subgraph_executor_map.register_circuit_breaker(subgraph_name)?;
}
subgraph_executor_map.all_endpoint_expression = global_url_override;
Ok(subgraph_executor_map)
}
pub fn callback_subscriptions(&self) -> CallbackSubscriptionsMap {
self.callback_subscriptions.clone()
}
pub async fn execute<'exec>(
&self,
subgraph_name: &'exec str,
mut execution_request: SubgraphExecutionRequest<'exec>,
client_request: &ClientRequestDetails<'exec>,
plugin_req_state: Option<&'exec PluginRequestState<'exec>>,
demand_control_ctx: Option<&DemandControlExecutionContext>,
) -> Result<SubgraphResponse<'exec>, SubgraphExecutorError> {
if let Some(demand_control_opts) = demand_control_ctx {
if let Some(subgraph_max_cost) = demand_control_opts
.subgraphs
.blocked_subgraphs
.get(subgraph_name)
{
let estimated_cost = demand_control_opts
.evaluation
.estimated_cost_for_subgraph(subgraph_name);
match demand_control_opts.subgraphs.enforcement_mode {
DemandControlMode::Enforce => {
tracing::warn!(
target: targets::DEMAND_CONTROL,
subgraph = subgraph_name,
estimated_cost,
subgraph_max_cost = *subgraph_max_cost,
"skipping subgraph fetch: estimated cost exceeds subgraph budget"
);
return Err(SubgraphExecutorError::CostEstimatedTooExpensive);
}
DemandControlMode::Measure => {
tracing::warn!(
target: targets::DEMAND_CONTROL,
subgraph = subgraph_name,
estimated_cost,
subgraph_max_cost = *subgraph_max_cost,
"subgraph budget exceeded: estimated cost exceeds subgraph budget (not enforced)"
);
}
}
}
}
let endpoint_str = self.resolve_endpoint(subgraph_name, client_request)?;
let mut executor = self.get_or_create_http_executor(subgraph_name, &endpoint_str)?;
let http_executor = executor.clone();
let timeout = self.resolve_subgraph_timeout(subgraph_name, client_request)?;
let mut on_end_callbacks = vec![];
let mut execution_result: Option<SubgraphResponse<'exec>> = None;
if let Some(plugin_req_state) = plugin_req_state.as_ref() {
let mut start_payload = OnSubgraphExecuteStartHookPayload {
router_http_request: &plugin_req_state.router_http_request,
context: &plugin_req_state.context,
request_context: plugin_req_state
.request_context
.for_plugin::<hooks::OnSubgraphExecute>(),
subgraph_name,
executor,
execution_request,
};
for plugin in plugin_req_state.plugins.as_ref() {
let result = plugin.on_subgraph_execute(start_payload).await;
start_payload = result.payload;
match result.control_flow {
StartControlFlow::Proceed => {
}
StartControlFlow::EndWithResponse(response) => {
debug!(target: targets::EXECUTOR, subgraph = subgraph_name, "execution was skipped due to response override by a plugin");
execution_result = Some(response);
break;
}
StartControlFlow::OnEnd(callback) => {
on_end_callbacks.push(callback);
}
}
}
execution_request = start_payload.execution_request;
executor = start_payload.executor;
}
if execution_result.is_none() && Arc::ptr_eq(&executor, &http_executor) {
if self
.config
.subscriptions
.get_protocol_for_subgraph(subgraph_name)
== SubscriptionProtocol::WebSocket
{
let reuse_connections = self
.config
.traffic_shaping
.websocket_reuse_connections(subgraph_name);
match self
.config
.traffic_shaping
.websocket_execute_mode(subgraph_name)
{
WebSocketExecuteMode::Http => {
}
WebSocketExecuteMode::ReuseExisting if reuse_connections => {
let pooled =
execution_request
.connection_fingerprint
.and_then(|fingerprint| {
self.subscription_executors_by_subgraph
.get(subgraph_name)
.and_then(|endpoints| {
endpoints.get(&endpoint_str).and_then(|executor| {
let id = WebSocketConnectionId::new(
subgraph_name,
executor.endpoint().clone(),
fingerprint,
);
self.websocket_pool.get_initialized(&id)
})
})
});
self.telemetry_context
.metrics
.websocket_pool
.record_connection_lookup(subgraph_name, pooled.is_some());
if let Some(pooled) = pooled {
executor = Arc::new(
Box::new(pooled) as Box<dyn SubgraphExecutor + Send + Sync>
);
}
}
WebSocketExecuteMode::Websocket => {
executor =
self.get_or_create_subscription_executor(subgraph_name, &endpoint_str)?;
}
WebSocketExecuteMode::ReuseExisting => {
}
}
}
}
let mut execution_result = match execution_result {
Some(execution_result) => execution_result,
None => {
debug!(target: targets::EXECUTOR, operation = execution_request.query, dedupe = execution_request.dedupe, subgraph = subgraph_name, executor = executor.executor_name(), "executing subgraph request");
summary::record(|s| s.record_subgraph(subgraph_name));
let call_started_at = Instant::now();
let exec_fut = executor.execute(execution_request, timeout, plugin_req_state);
let circuit_breaker = self
.circuit_breakers_by_subgraph
.get(subgraph_name)
.map(|r| r.value().clone());
let result = match circuit_breaker {
Some(circuit_breaker) => {
let SubgraphCircuitBreaker {
recloser,
error_status_codes,
} = circuit_breaker;
let exec_fut = exec_fut.map(move |exec_res| match exec_res {
Ok(succ_res) => {
if succ_res.status.is_some_and(|status| {
error_status_codes.iter().any(|m| m.matches(status))
}) {
Err(SubgraphExecutorError::InternalServerError(succ_res.into()))
} else {
Ok(succ_res)
}
}
Err(err) => Err(err),
});
let circuit_breaker_metrics =
&self.telemetry_context.metrics.circuit_breaker;
recloser
.call(exec_fut)
.map(|exec_res| match exec_res {
Err(recloser::Error::Inner(e)) => {
circuit_breaker_metrics.record_failure(subgraph_name);
match e {
SubgraphExecutorError::InternalServerError(succ_ress) => {
Ok(*succ_ress)
}
other_err => Err(other_err),
}
}
Err(recloser::Error::Rejected) => {
error!(target: targets::EXECUTOR, subgraph = subgraph_name, executor = executor.executor_name(), "circuit breaker rejected");
circuit_breaker_metrics.record_short_circuit(subgraph_name);
Err(SubgraphExecutorError::CircuitBreakerRejected)
}
Ok(res) => {
circuit_breaker_metrics.record_success(subgraph_name);
Ok(res)
}
})
.await
}
None => exec_fut.await,
};
summary::record(|s| {
s.record_subgraph_call_duration(subgraph_name, call_started_at.elapsed())
});
result?
}
};
if !on_end_callbacks.is_empty() {
if let Some(plugin_req_state) = plugin_req_state.as_ref() {
let mut end_payload = OnSubgraphExecuteEndHookPayload {
context: &plugin_req_state.context,
request_context: plugin_req_state
.request_context
.for_plugin::<hooks::OnSubgraphExecute>(),
execution_result,
};
for callback in on_end_callbacks {
let result = callback(end_payload);
end_payload = result.payload;
match result.control_flow {
EndControlFlow::Proceed => {
}
EndControlFlow::EndWithResponse(response) => {
end_payload.execution_result = response;
}
}
}
execution_result = end_payload.execution_result;
}
}
let error_count = execution_result
.errors
.as_ref()
.map(|e| e.len())
.unwrap_or(0);
debug!(target: targets::EXECUTOR,
subgraph = subgraph_name,
executor = executor.executor_name(),
error_count,
partial_response = error_count > 0 && !execution_result.data.is_null(),
http_status = execution_result.status.map(|s| s.as_u16()).unwrap_or(0),
"subgraph execution completed"
);
Ok(execution_result)
}
pub async fn subscribe<'exec>(
&self,
subgraph_name: &str,
execution_request: SubgraphExecutionRequest<'exec>,
client_request: &ClientRequestDetails<'exec>,
) -> Result<
BoxStream<'static, Result<SubgraphResponse<'static>, SubgraphExecutorError>>,
SubgraphExecutorError,
> {
let endpoint_str = self.resolve_endpoint(subgraph_name, client_request)?;
let executor = self.get_or_create_subscription_executor(subgraph_name, &endpoint_str)?;
let timeout = self.resolve_subgraph_timeout(subgraph_name, client_request)?;
debug!(target: targets::EXECUTOR, operation = execution_request.query, dedupe = execution_request.dedupe, subgraph = subgraph_name, executor = executor.executor_name(), "subscribing subgraph request");
summary::record(|s| s.record_subgraph(subgraph_name));
let call_started_at = Instant::now();
let subscribe_fut = executor.subscribe(execution_request, timeout);
let circuit_breaker = self
.circuit_breakers_by_subgraph
.get(subgraph_name)
.map(|r| r.value().clone());
let result = match circuit_breaker {
Some(SubgraphCircuitBreaker { recloser, .. }) => {
let circuit_breaker_metrics = &self.telemetry_context.metrics.circuit_breaker;
recloser
.call(subscribe_fut)
.map(|res| match res {
Ok(stream) => {
circuit_breaker_metrics.record_success(subgraph_name);
Ok(stream)
}
Err(recloser::Error::Inner(e)) => {
circuit_breaker_metrics.record_failure(subgraph_name);
Err(e)
}
Err(recloser::Error::Rejected) => {
circuit_breaker_metrics.record_short_circuit(subgraph_name);
Err(SubgraphExecutorError::CircuitBreakerRejected)
}
})
.await
}
None => subscribe_fut.await,
};
summary::record(|s| {
s.record_subgraph_call_duration(subgraph_name, call_started_at.elapsed())
});
result
}
fn resolve_subgraph_timeout(
&self,
subgraph_name: &str,
client_request: &ClientRequestDetails<'_>,
) -> Result<Option<Duration>, SubgraphExecutorError> {
self.timeouts_by_subgraph
.get(subgraph_name)
.map(|t| {
let global_timeout_duration =
resolve_timeout(&self.global_timeout, client_request, None)?;
resolve_timeout(t.value(), client_request, Some(global_timeout_duration))
})
.transpose()
}
fn resolve_endpoint(
&self,
subgraph_name: &str,
client_request: &ClientRequestDetails<'_>,
) -> Result<String, SubgraphExecutorError> {
let expression = self
.expression_endpoints_by_subgraph
.get(subgraph_name)
.or_else(|| {
self.all_endpoint_expression
.get_expression_for_subgraph(subgraph_name)
});
if let Some(expression) = expression {
let original_url_value = VrlValue::Bytes(
self.static_endpoints_by_subgraph
.get(subgraph_name)
.map(|endpoint| endpoint.value().clone())
.ok_or_else(|| SubgraphExecutorError::StaticEndpointNotFound)?
.into(),
);
let subgraph_value =
VrlValue::Object(BTreeMap::from([("name".into(), subgraph_name.into())]));
let value = VrlValue::Object(BTreeMap::from([
("request".into(), client_request.into()),
("default".into(), original_url_value),
("subgraph".into(), subgraph_value),
]));
let endpoint_result = expression.execute(value).map_err(|err| {
SubgraphExecutorError::EndpointExpressionResolutionFailure(err.to_string())
})?;
match endpoint_result.as_str() {
Some(s) => Ok(s.to_string()),
None => Err(SubgraphExecutorError::EndpointExpressionWrongType),
}
} else {
self.static_endpoints_by_subgraph
.get(subgraph_name)
.map(|e| e.value().clone())
.ok_or_else(|| SubgraphExecutorError::StaticEndpointNotFound)
}
}
fn get_or_create_http_executor(
&self,
subgraph_name: &str,
endpoint_str: &str,
) -> Result<SubgraphExecutorBoxedArc, SubgraphExecutorError> {
if let Some(executor) = self
.http_executors_by_subgraph
.get(subgraph_name)
.and_then(|endpoints| endpoints.get(endpoint_str).map(|e| e.clone()))
{
return Ok(executor);
}
self.register_executor(subgraph_name, endpoint_str, false)
}
fn get_or_create_subscription_executor(
&self,
subgraph_name: &str,
endpoint_str: &str,
) -> Result<SubgraphExecutorBoxedArc, SubgraphExecutorError> {
if let Some(executor) = self
.subscription_executors_by_subgraph
.get(subgraph_name)
.and_then(|endpoints| endpoints.get(endpoint_str).map(|e| e.clone()))
{
return Ok(executor);
}
self.register_executor(subgraph_name, endpoint_str, true)
}
fn register_endpoint_expression(
&mut self,
subgraph_name: &str,
expression: &str,
) -> Result<(), SubgraphExecutorError> {
let program = expression.compile_expression(None).map_err(|err| {
SubgraphExecutorError::EndpointExpressionBuild(
subgraph_name.to_string(),
err.diagnostics,
)
})?;
self.expression_endpoints_by_subgraph
.insert(subgraph_name.to_string(), program);
Ok(())
}
fn register_static_endpoint(&self, subgraph_name: &str, endpoint_str: &str) {
self.static_endpoints_by_subgraph
.insert(subgraph_name.to_string(), endpoint_str.to_string());
}
fn convert_to_websocket_endpoint(
&self,
subgraph_name: &str,
endpoint_uri: &Uri,
) -> Result<Uri, SubgraphExecutorError> {
let ws_scheme = match endpoint_uri.scheme_str() {
Some("https") => "wss",
_ => "ws",
};
let path_and_query = self
.config
.subscriptions
.get_websocket_path(subgraph_name)
.or_else(|| endpoint_uri.path_and_query().map(|path| path.as_str()))
.unwrap_or_default();
Uri::builder()
.scheme(ws_scheme)
.authority(
endpoint_uri
.authority()
.map(|authority| authority.as_str())
.unwrap_or_default(),
)
.path_and_query(path_and_query)
.build()
.map_err(|error| {
SubgraphExecutorError::WebSocketEndpointBuildFailure(
format!(
"{}://{}{}",
ws_scheme,
endpoint_uri
.authority()
.map(|authority| authority.as_str())
.unwrap_or_default(),
path_and_query
),
error,
)
})
}
fn register_executor(
&self,
subgraph_name: &str,
endpoint_str: &str,
for_subscription: bool,
) -> Result<SubgraphExecutorBoxedArc, SubgraphExecutorError> {
let endpoint_uri = endpoint_str.parse::<Uri>().map_err(|e| {
SubgraphExecutorError::EndpointParseFailure(endpoint_str.to_string(), e)
})?;
let origin = format!(
"{}://{}:{}",
endpoint_uri.scheme_str().unwrap_or("http"),
endpoint_uri.host().unwrap_or(""),
endpoint_uri.port_u16().unwrap_or_else(|| {
match endpoint_uri.scheme_str() {
Some("https") | Some("wss") => 443,
_ => 80,
}
})
);
let semaphore = self
.semaphores_by_origin
.entry(origin)
.or_insert_with(|| Arc::new(Semaphore::new(self.max_connections_per_host)))
.clone();
let protocol = if for_subscription {
self.config
.subscriptions
.get_protocol_for_subgraph(subgraph_name)
} else {
SubscriptionProtocol::HTTP
};
match protocol {
SubscriptionProtocol::HTTP => {
let subgraph_config = self.resolve_subgraph_config(subgraph_name)?;
let http_executor = HTTPSubgraphExecutor::new(
subgraph_name.to_string(),
endpoint_uri,
subgraph_config.client,
semaphore,
subgraph_config.dedupe_enabled,
self.in_flight_requests.clone(),
self.telemetry_context.clone(),
self.config.subscriptions.subgraph_buffer_capacity,
)
.to_boxed_arc();
self.http_executors_by_subgraph
.entry(subgraph_name.to_string())
.or_default()
.insert(endpoint_str.to_string(), http_executor.clone());
Ok(http_executor)
}
SubscriptionProtocol::WebSocket => {
let ws_endpoint_uri =
self.convert_to_websocket_endpoint(subgraph_name, &endpoint_uri)?;
let tls_config = get_merged_tls_config(
self.config.traffic_shaping.all.tls.as_ref(),
self.config
.traffic_shaping
.subgraphs
.get(subgraph_name)
.and_then(|s| s.tls.as_ref()),
);
let ws_tls_config = match tls_config.as_ref() {
Some(tls) => Some(Arc::new(build_https_client_config(Some(tls))?)),
None => None,
};
let ws_executor = WsSubgraphExecutor::new(
subgraph_name.to_string(),
ws_endpoint_uri,
ws_tls_config,
self.config.subscriptions.subgraph_buffer_capacity,
self.telemetry_context.clone(),
self.websocket_pool.clone(),
self.config.traffic_shaping.pool_idle_timeout(subgraph_name),
self.config
.traffic_shaping
.websocket_reuse_connections(subgraph_name),
)
.to_boxed_arc();
self.subscription_executors_by_subgraph
.entry(subgraph_name.to_string())
.or_default()
.insert(endpoint_str.to_string(), ws_executor.clone());
Ok(ws_executor)
}
SubscriptionProtocol::HTTPCallback => {
let callback_config = self
.config
.callback
.as_ref()
.ok_or_else(|| SubgraphExecutorError::HttpCallbackNotConfigured)?;
let heartbeat_interval_ms = callback_config.heartbeat_interval.as_millis() as u64;
let subgraph_config = self.resolve_subgraph_config(subgraph_name)?;
let callback_executor = HttpCallbackSubgraphExecutor::new(
subgraph_name.to_string(),
endpoint_uri,
subgraph_config.client,
callback_config.public_url.to_string(),
heartbeat_interval_ms,
self.callback_subscriptions.clone(),
self.telemetry_context.clone(),
)
.to_boxed_arc();
self.subscription_executors_by_subgraph
.entry(subgraph_name.to_string())
.or_default()
.insert(endpoint_str.to_string(), callback_executor.clone());
Ok(callback_executor)
}
}
}
fn resolve_subgraph_config<'a>(
&'a self,
subgraph_name: &'a str,
) -> Result<ResolvedSubgraphConfig<'a>, SubgraphExecutorError> {
let mut config = ResolvedSubgraphConfig {
client: self.client.clone(),
timeout_config: &self.config.traffic_shaping.all.request_timeout,
dedupe_enabled: self.config.traffic_shaping.all.dedupe_enabled,
};
let Some(subgraph_config) = self.config.traffic_shaping.subgraphs.get(subgraph_name) else {
return Ok(config);
};
let pool_idle_timeout = self.config.traffic_shaping.pool_idle_timeout(subgraph_name);
let subgraph_allow_only_http2 = subgraph_config
.allow_only_http2
.unwrap_or(self.config.traffic_shaping.all.allow_only_http2);
if pool_idle_timeout != self.config.traffic_shaping.all.pool_idle_timeout
|| subgraph_config.tls.is_some()
|| subgraph_allow_only_http2 != self.config.traffic_shaping.all.allow_only_http2
{
let tls_config = get_merged_tls_config(
self.config.traffic_shaping.all.tls.as_ref(),
subgraph_config.tls.as_ref(),
);
let mut client_builder = Client::builder(TokioExecutor::new());
client_builder
.pool_timer(TokioTimer::new())
.pool_idle_timeout(pool_idle_timeout)
.pool_max_idle_per_host(self.max_connections_per_host);
if subgraph_allow_only_http2 {
client_builder.http2_only(true);
}
config.client =
Arc::new(client_builder.build(build_https_connector(tls_config.as_ref())?));
}
if let Some(dedupe_enabled) = subgraph_config.dedupe_enabled {
config.dedupe_enabled = dedupe_enabled;
}
if let Some(custom_timeout) = &subgraph_config.request_timeout {
config.timeout_config = custom_timeout;
}
Ok(config)
}
fn register_subgraph_timeout(&self, subgraph_name: &str) -> Result<(), SubgraphExecutorError> {
if self.timeouts_by_subgraph.contains_key(subgraph_name) {
return Ok(());
}
let timeout_config = self
.config
.traffic_shaping
.subgraphs
.get(subgraph_name)
.and_then(|s| s.request_timeout.as_ref())
.unwrap_or(&self.config.traffic_shaping.all.request_timeout);
let timeout_prog = compile_duration_or_expression(timeout_config, None).map_err(|err| {
SubgraphExecutorError::RequestTimeoutExpressionBuild(
subgraph_name.to_string(),
err.diagnostics,
)
})?;
self.timeouts_by_subgraph
.insert(subgraph_name.to_string(), timeout_prog);
Ok(())
}
fn register_circuit_breaker(&self, subgraph_name: &str) -> Result<(), SubgraphExecutorError> {
if self
.circuit_breakers_by_subgraph
.contains_key(subgraph_name)
{
return Ok(());
}
let global_circuit_breaker_cfg = self.config.traffic_shaping.all.circuit_breaker.as_ref();
let subgraph_circuit_breaker_cfg = self
.config
.traffic_shaping
.subgraphs
.get(subgraph_name)
.and_then(|s| s.circuit_breaker.as_ref());
let circuit_breaker_enabled = subgraph_circuit_breaker_cfg
.and_then(|c| c.enabled)
.or_else(|| global_circuit_breaker_cfg.and_then(|c| c.enabled))
.unwrap_or(false);
if circuit_breaker_enabled {
let mut builder = CircuitBreakerBuilder::default();
if let Some(error_threshold) = subgraph_circuit_breaker_cfg
.and_then(|c| c.error_threshold)
.or_else(|| global_circuit_breaker_cfg.and_then(|c| c.error_threshold))
{
let error_threshold = error_threshold.as_f64() as f32;
if !error_threshold.is_finite() {
return Err(SubgraphExecutorError::CircuitBreakerCreationError(
CircuitBreakerError::InvalidErrorThreshold(error_threshold),
subgraph_name.to_string(),
));
}
builder = builder.error_threshold(error_threshold);
}
if let Some(volume_threshold) = subgraph_circuit_breaker_cfg
.and_then(|c| c.volume_threshold)
.or_else(|| global_circuit_breaker_cfg.and_then(|c| c.volume_threshold))
{
builder = builder.volume_threshold(volume_threshold);
}
if let Some(reset_timeout) = subgraph_circuit_breaker_cfg
.and_then(|c| c.reset_timeout)
.or_else(|| global_circuit_breaker_cfg.and_then(|c| c.reset_timeout))
{
builder = builder.reset_timeout(reset_timeout);
}
if let Some(half_open_attempts) = subgraph_circuit_breaker_cfg
.and_then(|c| c.half_open_attempts)
.or_else(|| global_circuit_breaker_cfg.and_then(|c| c.half_open_attempts))
{
builder = builder.half_open_attempts(half_open_attempts);
}
let recloser = builder.build_async().map_err(|e| {
SubgraphExecutorError::CircuitBreakerCreationError(e, subgraph_name.to_string())
})?;
let error_status_codes = subgraph_circuit_breaker_cfg
.and_then(|c| c.error_status_codes.as_ref())
.or_else(|| global_circuit_breaker_cfg.and_then(|c| c.error_status_codes.as_ref()))
.map(|codes| Arc::new(codes.clone()))
.unwrap_or_else(|| DEFAULT_CIRCUIT_BREAKER_ERROR_STATUS_CODES.clone());
self.circuit_breakers_by_subgraph.insert(
subgraph_name.to_string(),
SubgraphCircuitBreaker {
recloser,
error_status_codes,
},
);
self.telemetry_context
.metrics
.circuit_breaker
.register_subgraph(subgraph_name);
}
Ok(())
}
}
fn resolve_timeout(
duration_or_program: &DurationOrProgram,
client_request: &ClientRequestDetails<'_>,
default_timeout: Option<Duration>,
) -> Result<Duration, SubgraphExecutorError> {
duration_or_program
.resolve(|| {
let mut context_map = BTreeMap::new();
context_map.insert("request".into(), client_request.into());
if let Some(default) = default_timeout {
context_map.insert(
"default".into(),
VrlValue::Integer(default.as_millis() as i64),
);
}
VrlValue::Object(context_map)
})
.map_err(|err| SubgraphExecutorError::TimeoutExpressionResolution(err.to_string()))
}
pub fn compile_duration_or_expression(
config: &DurationOrExpression,
fns: Option<&[Box<dyn VrlFunction>]>,
) -> Result<ValueOrProgram<Duration>, ExpressionCompileError> {
match config {
DurationOrExpression::Duration(dur) => Ok(ValueOrProgram::Value(*dur)),
DurationOrExpression::Expression { expression } => {
let program = expression.as_str().compile_expression(fns)?;
let hints = ProgramHints::from_program(&program);
Ok(ValueOrProgram::Program(Box::new(program), hints))
}
}
}