Skip to content

Commit 8e365ed

Browse files
committed
graphql: Read a file of expensive queries and return an error for them
Read the file /etc/graph-node/expensive-queries.txt (one GraphQL query per line), parse the queries and respond to any query that has the same ShapeHash as one of the included queries with a TooExpensive error
1 parent c65d602 commit 8e365ed

File tree

4 files changed

+67
-10
lines changed

4 files changed

+67
-10
lines changed

graph/src/data/query/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub enum QueryExecutionError {
5454
ScalarCoercionError(Pos, String, q::Value, String),
5555
TooComplex(u64, u64), // (complexity, max_complexity)
5656
TooDeep(u8), // max_depth
57+
TooExpensive,
5758
UndefinedFragment(String),
5859
// Using slow and prefetch query resolution yield different results
5960
IncorrectPrefetchResult { slow: q::Value, prefetch: q::Value },
@@ -209,6 +210,7 @@ impl fmt::Display for QueryExecutionError {
209210
EventStreamError => write!(f, "error in the subscription event stream"),
210211
FulltextQueryRequiresFilter => write!(f, "fulltext search queries can only use EntityFilter::Equal"),
211212
SubscriptionsDisabled => write!(f, "subscriptions are disabled"),
213+
TooExpensive => write!(f, "query is too expensive")
212214
}
213215
}
214216
}

graphql/src/runner.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use futures01::future;
22
use graphql_parser::query as q;
3-
use std::collections::BTreeMap;
3+
use std::collections::{BTreeMap, HashMap};
44
use std::env;
55
use std::str::FromStr;
66
use std::sync::Arc;
@@ -9,12 +9,12 @@ use std::time::{Duration, Instant};
99
use crate::prelude::{
1010
object, object_value, QueryExecutionOptions, StoreResolver, SubscriptionExecutionOptions,
1111
};
12-
use crate::query::execute_query;
12+
use crate::query::{execute_query, shape_hash::shape_hash};
1313
use crate::subscription::execute_prepared_subscription;
1414
use graph::prelude::{
1515
o, EthereumBlockPointer, GraphQlRunner as GraphQlRunnerTrait, Logger, Query,
1616
QueryExecutionError, QueryResult, QueryResultFuture, Store, StoreError, SubgraphDeploymentId,
17-
SubgraphDeploymentStore, Subscription, SubscriptionResultFuture,
17+
SubgraphDeploymentStore, Subscription, SubscriptionError, SubscriptionResultFuture,
1818
};
1919

2020
use lazy_static::lazy_static;
@@ -23,6 +23,7 @@ use lazy_static::lazy_static;
2323
pub struct GraphQlRunner<S> {
2424
logger: Logger,
2525
store: Arc<S>,
26+
expensive: HashMap<u64, Arc<q::Document>>,
2627
}
2728

2829
lazy_static! {
@@ -53,10 +54,15 @@ where
5354
S: Store + SubgraphDeploymentStore,
5455
{
5556
/// Creates a new query runner.
56-
pub fn new(logger: &Logger, store: Arc<S>) -> Self {
57+
pub fn new(logger: &Logger, store: Arc<S>, expensive: &Vec<Arc<q::Document>>) -> Self {
58+
let expensive = expensive
59+
.into_iter()
60+
.map(|doc| (shape_hash(&doc), doc.clone()))
61+
.collect::<HashMap<_, _>>();
5762
GraphQlRunner {
5863
logger: logger.new(o!("component" => "GraphQlRunner")),
5964
store,
65+
expensive,
6066
}
6167
}
6268

@@ -138,6 +144,14 @@ where
138144
Ok(QueryResult::new(Some(q::Value::Object(values))))
139145
}
140146
}
147+
148+
pub fn check_too_expensive(&self, query: &Query) -> Result<(), Vec<QueryExecutionError>> {
149+
if self.expensive.contains_key(&shape_hash(&query.document)) {
150+
Err(vec![QueryExecutionError::TooExpensive])
151+
} else {
152+
Ok(())
153+
}
154+
}
141155
}
142156

143157
impl<S> GraphQlRunnerTrait for GraphQlRunner<S>
@@ -160,14 +174,19 @@ where
160174
max_depth: Option<u8>,
161175
max_first: Option<u32>,
162176
) -> QueryResultFuture {
163-
let result = match self.execute(query, max_complexity, max_depth, max_first) {
164-
Ok(result) => result,
165-
Err(e) => QueryResult::from(e),
166-
};
177+
let result = self
178+
.check_too_expensive(&query)
179+
.and_then(|_| self.execute(query, max_complexity, max_depth, max_first))
180+
.unwrap_or_else(|e| QueryResult::from(e));
167181
Box::new(future::ok(result))
168182
}
169183

170184
fn run_subscription(&self, subscription: Subscription) -> SubscriptionResultFuture {
185+
if let Err(errs) = self.check_too_expensive(&subscription.query) {
186+
let err = SubscriptionError::GraphQLError(errs);
187+
return Box::new(future::result(Err(err)));
188+
}
189+
171190
let query = match crate::execution::Query::new(
172191
subscription.query,
173192
*GRAPHQL_MAX_COMPLEXITY,

graphql/tests/query.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ fn execute_query_document_with_variables(
230230
query: q::Document,
231231
variables: Option<QueryVariables>,
232232
) -> QueryResult {
233-
let runner = GraphQlRunner::new(&*LOGGER, STORE.clone());
233+
let runner = GraphQlRunner::new(&*LOGGER, STORE.clone(), &vec![]);
234234
let query = Query::new(Arc::new(api_test_schema()), query, variables);
235235

236236
return_err!(runner.execute(query, None, None, None))

node/src/main.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use lazy_static::lazy_static;
55
use prometheus::Registry;
66
use std::collections::HashMap;
77
use std::env;
8+
use std::io::{BufRead, BufReader};
9+
use std::path::Path;
810
use std::str::FromStr;
911
use std::time::Duration;
1012
use tokio::sync::mpsc;
@@ -34,6 +36,7 @@ use graph_store_postgres::{
3436
ChainHeadUpdateListener as PostgresChainHeadUpdateListener, Store as DieselStore, StoreConfig,
3537
SubscriptionManager,
3638
};
39+
use graphql_parser::query as q;
3740

3841
lazy_static! {
3942
// Default to an Ethereum reorg threshold to 50 blocks
@@ -68,6 +71,33 @@ enum ConnectionType {
6871
WS,
6972
}
7073

74+
fn read_expensive_queries() -> Result<Vec<Arc<q::Document>>, std::io::Error> {
75+
// A file with a list of expensive queries, one query per line
76+
// Attempts to run these queries will return a
77+
// QueryExecutionError::TooExpensive to clients
78+
const EXPENSIVE_QUERIES: &str = "/etc/graph-node/expensive-queries.txt";
79+
let path = Path::new(EXPENSIVE_QUERIES);
80+
let mut queries = Vec::new();
81+
if path.exists() {
82+
let file = std::fs::File::open(path)?;
83+
let reader = BufReader::new(file);
84+
for line in reader.lines() {
85+
let line = line?;
86+
let query = graphql_parser::parse_query(&line).map_err(|e| {
87+
let msg = format!(
88+
"invalid GraphQL query in {}: {}\n{}",
89+
EXPENSIVE_QUERIES,
90+
e.to_string(),
91+
line
92+
);
93+
std::io::Error::new(std::io::ErrorKind::InvalidData, msg)
94+
})?;
95+
queries.push(Arc::new(query));
96+
}
97+
}
98+
Ok(queries)
99+
}
100+
71101
// Saturating the blocking threads can cause all sorts of issues, so set a large maximum.
72102
// Ideally we'd use semaphores to not use more blocking threads than DB connections,
73103
// but for now this is necessary.
@@ -523,6 +553,8 @@ async fn main() {
523553
)))
524554
};
525555

556+
let expensive_queries = read_expensive_queries().unwrap();
557+
526558
graph::spawn(
527559
futures::stream::FuturesOrdered::from_iter(stores_eth_adapters.into_iter().map(
528560
|(network_name, eth_adapter)| {
@@ -570,7 +602,11 @@ async fn main() {
570602
.and_then(move |stores| {
571603
let generic_store = stores.values().next().expect("error creating stores");
572604

573-
let graphql_runner = Arc::new(GraphQlRunner::new(&logger, generic_store.clone()));
605+
let graphql_runner = Arc::new(GraphQlRunner::new(
606+
&logger,
607+
generic_store.clone(),
608+
&expensive_queries,
609+
));
574610
let mut graphql_server = GraphQLQueryServer::new(
575611
&logger_factory,
576612
graphql_metrics_registry,

0 commit comments

Comments
 (0)