Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 170 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,74 +46,196 @@ These steps should ensure a smooth transition to the latest version of `indexer-

[Contributions guide](/contributing.md)

### Supported request and response format examples
## Supported request and response format examples

```bash
curl http://localhost:7600/
```
```
Service is up and running
```
✗ curl http://localhost:7300/
Ready to roll!

✗ curl http://localhost:7300/health
{"healthy":true}

✗ curl http://localhost:7300/version
{"version":"0.1.0","dependencies":{}}
```bash
curl http://localhost:7600/version
```
```json
{ "version":"0.1.0", "dependencies": {..} }
```

✗ curl http://localhost:7300/operator/info
{"publicKey":"0xacb05407d78129b5717bb51712d3e23a78a10929"}
```bash
curl http://localhost:7600/info
```
```json
{ "publicKey": "0xacb05407d78129b5717bb51712d3e23a78a10929" }
```

# Subgraph queries
# Checks for receipts and authorization
✗ curl -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer token-for-graph-node-query-endpoint' --data '{"query": "{_meta{block{number}}}"}' http://localhost:7300/subgraphs/id/QmacQnSgia4iDPWHpeY6aWxesRFdb8o5DKZUx96zZqEWrB
"{\"data\":{\"_meta\":{\"block\":{\"number\":9425787}}}}"

## Checks for receipts and authorization
```bash
curl -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer token-for-graph-node-query-endpoint' \
--data '{"query": "{_meta{block{number}}}"}' \
http://localhost:7600/subgraphs/id/QmacQnSgia4iDPWHpeY6aWxesRFdb8o5DKZUx96zZqEWrB
```
```json
{
"attestable": true,
"graphQLResponse": "{\"data\":{\"_meta\":{\"block\":{\"number\":10666745}}}}"
}
```
# Takes hex representation for subgraphs deployment id aside from IPFS hash representation
✗ curl -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer token-for-graph-node-query-endpoint' --data '{"query": "{_meta{block{number}}}"}' http://localhost:7300/subgraphs/id/0xb655ca6f49e73728a102219726ff678d61d8fb792874792e9f0d9887dc616600
"{\"data\":{\"_meta\":{\"block\":{\"number\":9425787}}}}"
```bash
curl -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer token-for-graph-node-query-endpoint' \
--data '{"query": "{_meta{block{number}}}"}' \
http://localhost:7600/subgraphs/id/0xb655ca6f49e73728a102219726ff678d61d8fb792874792e9f0d9887dc616600
```
```json
{
"attestable": true,
"graphQLResponse": "{\"data\":{\"_meta\":{\"block\":{\"number\":10666745}}}}"
}
```

# Free query auth token check failed
✗ curl -X POST -H 'Content-Type: application/json' -H 'Authorization: blah' --data '{"query": "{_meta{block{number}}}"}' http://localhost:7300/subgraphs/id/0xb655ca6f49e73728a102219726ff678d61d8fb792874792e9f0d9887dc616600
"Invalid Tap-Receipt header provided"%

# Subgraph health check
✗ curl http://localhost:7300/subgraphs/health/QmVhiE4nax9i86UBnBmQCYDzvjWuwHShYh7aspGPQhU5Sj
"Subgraph deployment is up to date"%
```bash
curl -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: blah' \
--data '{"query": "{_meta{block{number}}}"}' \
http://localhost:7600/subgraphs/id/0xb655ca6f49e73728a102219726ff678d61d8fb792874792e9f0d9887dc616600
```
```json
{
"message":"No valid receipt or free query auth token provided"
}
```
## Subgraph health check
```bash
curl http://localhost:7600/subgraphs/health/QmVhiE4nax9i86UBnBmQCYDzvjWuwHShYh7aspGPQhU5Sj
```
```json
{
"health": "healthy"
}
```
## Unfound subgraph
✗ curl http://localhost:7300/subgraphs/health/QmacQnSgia4iDPWHpeY6aWxesRFdb8o5DKZUx96zZqEWrB
"Invalid indexing status"%
```bash
curl http://localhost:7600/subgraphs/health/QmacQnSgia4iDPWHpeY6aWxesRFdb8o5DKZUx96zZqEWrB
```
```json
{
"error": "Deployment not found"
}
```

# Network queries
# Checks for auth and configuration to serve-network-subgraph
✗ curl -X POST -H 'Content-Type: application/json' -H 'Authorization: token-for-network-subgraph' --data '{"query": "{_meta{block{number}}}"}' http://localhost:7300/network
"Not enabled or authorized query"
## Checks for auth and configuration to serve-network-subgraph

```bash
curl -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: token-for-network-subgraph' \
--data '{"query": "{_meta{block{number}}}"}' \
http://localhost:7600/network
```
```json
{
"message":"No valid receipt or free query auth token provided"
}
```

# Indexing status resolver - Route supported root field queries to graph node status endpoint
✗ curl -X POST -H 'Content-Type: application/json' --data '{"query": "{blockHashFromNumber(network:\"goerli\", blockNumber: 9069120)}"}' http://localhost:7300/status
{"data":{"blockHashFromNumber":"e1e5472636db73ba5496aee098dc21310683c95eb30fc46f9ba6c36d8b28d58e"}}%

# Indexing status resolver -
✗ curl -X POST -H 'Content-Type: application/json' --data '{"query": "{indexingStatuses {subgraph health} }"}' http://localhost:7300/status
{"data":{"indexingStatuses":[{"subgraph":"QmVhiE4nax9i86UBnBmQCYDzvjWuwHShYh7aspGPQhU5Sj","health":"healthy"},{"subgraph":"QmWVtsWk8Pqn3zY3czDjyoVreshRLmoz9jko3mQ4uvxQDj","health":"healthy"},{"subgraph":"QmacQnSgia4iDPWHpeY6aWxesRFdb8o5DKZUx96zZqEWrB","health":"healthy"}]}}

# Indexing status resolver - Filter out the unsupported queries
✗ curl -X POST -H 'Content-Type: application/json' --data '{"query": "{_meta{block{number}}}"}' http://localhost:7300/status
{"errors":[{"locations":[{"line":1,"column":2}],"message":"Type `Query` has no field `_meta`"}]}%
```bash
curl -X POST \
-H 'Content-Type: application/json' \
--data '{"query": "{blockHashFromNumber(network:\"mainnet\", blockNumber: 21033)}"}' \
http://localhost:7600/status
```
```json
{
"data": {
"blockHashFromNumber": "0x6d8daae97a562b1fff22162515452acdd817c3d3c5cde1497b7d9eb6666a957e"
}
}
```

######## Cost server - read-only graphql query
curl -X GET -H 'Content-Type: application/json' --data '{"query": "{ costModel(deployment: \"Qmb5Ysp5oCUXhLA8NmxmYKDAX2nCMnh7Vvb5uffb9n5vss\") { deployment model variables }} "}' http://localhost:7300/cost
## Indexing status resolver -
```bash
curl -X POST \
-H 'Content-Type: application/json' \
--data '{"query": "{indexingStatuses {subgraph health}}"}' \
http://localhost:7600/status
```
```json
{
"data": {
"indexingStatuses": [
{
"subgraph": "QmVhiE4nax9i86UBnBmQCYDzvjWuwHShYh7aspGPQhU5Sj",
"health": "healthy"
},
{
"subgraph": "QmWVtsWk8Pqn3zY3czDjyoVreshRLmoz9jko3mQ4uvxQDj",
"health": "healthy"
},
{
"subgraph": "QmacQnSgia4iDPWHpeY6aWxesRFdb8o5DKZUx96zZqEWrB",
"health": "healthy"
}
]
}
}
```

curl -X GET -H 'Content-Type: application/json' --data '{"query": "{ costModel(deployment: \"Qmb5Ysp5oCUXhLA8NmxmYKDAX2nCMnh7Vvb5uffb9n5vss\") { deployment model variables }} "}' http://localhost:7300/cost
{"data":{"costModel":{"deployment":"0xbd499f7673ca32ef4a642207a8bebdd0fb03888cf2678b298438e3a1ae5206ea","model":"default => 0.00025;","variables":null}}}%
## Indexing status resolver - Filter out the unsupported queries
```bash
curl -X POST \
-H 'Content-Type: application/json' \
--data '{"query": "{_meta{block{number}}}"}' \
http://localhost:7600/status
```
```json
{
"errors": [
{
"locations": [
{
"line": 1,
"column": 2
}
],
"message": "Type `Query` has no field `_meta`"
}
]
}
```

curl -X GET -H 'Content-Type: application/json' --data '{"query": "{ costModel(deployment: \"Qmb5Ysp5oCUXhLA8NmxmYKDAX2nCMnh7Vvb5uffb9n5vas\") { deployment model variables }} "}' http://localhost:7300/cost
{"data":{"costModel":null}}%
## Cost server - read-only graphql query

curl -X GET -H 'Content-Type: application/json' --data '{"query": "{ costModel(deployment: \"Qmb5Ysp5oCUXhLA8NmxmYKDAX2nCMnh7Vvb5uffb9n5vss\") { deployment odel variables }} "}' http://localhost:7300/cost
{"errors":[{"message":"Cannot query field \"odel\" on type \"CostModel\". Did you mean \"model\"?","locations":[{"line":1,"column":88}]}]}%
```bash
curl -X GET \
-H 'Content-Type: application/json' \
--data '{"query": "{ costModels(deployments: [\"Qmb5Ysp5oCUXhLA8NmxmYKDAX2nCMnh7Vvb5uffb9n5vss\"]) { deployment model variables }} "}' \
http://localhost:7300/cost
```
```json
{
"data": {
"costModels": [
{
"deployment": "0xbd499f7673ca32ef4a642207a8bebdd0fb03888cf2678b298438e3a1ae5206ea",
"model": "default => 0.00025;",
"variables": null
}
]
}
}
```

curl -X GET -H 'Content-Type: application/json' --data '{"query": "{ costModels(deployments: [\"Qmb5Ysp5oCUXhLA8NmxmYKDAX2nCMnh7Vvb5uffb9n5vss\"]) { deployment model variables }} "}' http://localhost:7300/cost
{"data":{"costModels":[{"deployment":"0xbd499f7673ca32ef4a642207a8bebdd0fb03888cf2678b298438e3a1ae5206ea","model":"default => 0.00025;","variables":null}]}}%

```

## Dependency choices

Expand Down
117 changes: 117 additions & 0 deletions common/src/indexer_service/http/health.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2023-, Edge & Node, GraphOps, and Semiotic Labs.
// SPDX-License-Identifier: Apache-2.0

use crate::subgraph_client::Query;
use axum::{
extract::Path,
response::{IntoResponse, Response as AxumResponse},
Extension, Json,
};
use reqwest::StatusCode;
use serde::Deserialize;
use serde_json::json;
use thiserror::Error;

use super::GraphNodeConfig;

#[derive(Deserialize, Debug)]
struct Response {
data: SubgraphData,
}

#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct SubgraphData {
indexingStatuses: Vec<IndexingStatus>,
}

#[derive(Deserialize, Debug)]
struct IndexingStatus {
health: Health,
}

#[derive(Deserialize, Debug)]
#[allow(non_camel_case_types)]
enum Health {
healthy,
unhealthy,
failed,
}

impl Health {
fn as_str(&self) -> &str {
match self {
Health::healthy => "healthy",
Health::unhealthy => "unhealthy",
Health::failed => "failed",
}
}
}

#[derive(Debug, Error)]
pub enum CheckHealthError {
#[error("Graph node config not found")]
GraphNodeConfigNotFound,
#[error("Deployment not found")]
DeploymentNotFound,
#[error("Failed to process query")]
QueryForwardingError,
}

impl IntoResponse for CheckHealthError {
fn into_response(self) -> AxumResponse {
let (status, error_message) = match &self {
CheckHealthError::GraphNodeConfigNotFound => {
(StatusCode::NOT_FOUND, "Graph node config not found")
}
CheckHealthError::DeploymentNotFound => (StatusCode::NOT_FOUND, "Deployment not found"),
CheckHealthError::QueryForwardingError => {
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to process query")
}
};

let body = serde_json::json!({
"error": error_message,
});

(status, Json(body)).into_response()
}
}

pub async fn health(
Path(deployment_id): Path<String>,
Extension(graph_node): Extension<Option<GraphNodeConfig>>,
) -> Result<impl IntoResponse, CheckHealthError> {
let url = if let Some(graph_node) = graph_node {
graph_node.status_url
} else {
return Err(CheckHealthError::GraphNodeConfigNotFound);
};

let body = Query::new_with_variables(
r#"
query indexingStatuses($ids: [String!]!) {
indexingStatuses(subgraphs: $ids) {
health
}
}
"#,
[("ids", json!([deployment_id]))],
);

let client = reqwest::Client::new();
let response = client.post(url).json(&body).send().await;
let response = response.expect("Failed to get response");
let response_json: Result<Response, reqwest::Error> = response.json().await;

match response_json {
Ok(res) => {
if res.data.indexingStatuses.is_empty() {
return Err(CheckHealthError::DeploymentNotFound);
};
let health_status = res.data.indexingStatuses[0].health.as_str();
Ok(Json(json!({ "health": health_status })))
}
Err(_) => Err(CheckHealthError::QueryForwardingError),
}
}
6 changes: 6 additions & 0 deletions common/src/indexer_service/http/indexer_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use tracing::{info, info_span};

use crate::escrow_accounts::EscrowAccounts;
use crate::escrow_accounts::EscrowAccountsError;
use crate::indexer_service::http::health::health;
use crate::{
address::public_key,
indexer_service::http::static_subgraph::static_subgraph_request_handler,
Expand Down Expand Up @@ -352,6 +353,11 @@ impl IndexerService {
.route("/info", get(operator_address))
.layer(misc_rate_limiter);

// Check subgraph Health
misc_routes = misc_routes
.route("/subgraph/health/:deployment_id", get(health))
.route_layer(Extension(options.config.graph_node));

// Rate limits by allowing bursts of 50 requests and requiring 20ms of
// time between consecutive requests after that, effectively rate
// limiting to 50 req/s.
Expand Down
1 change: 1 addition & 0 deletions common/src/indexer_service/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

mod config;
mod health;
mod indexer_service;
mod request_handler;
mod static_subgraph;
Expand Down
Loading
Loading