Skip to content

Commit

Permalink
Add status endpoint to V2 API.
Browse files Browse the repository at this point in the history
  • Loading branch information
aterentic-ethernal committed Aug 14, 2023
1 parent 21e304b commit 5b0d3bd
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 41 deletions.
13 changes: 11 additions & 2 deletions src/api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub struct Server {
pub state: Arc<Mutex<State>>,
pub version: String,
pub network_version: String,
pub genesis_hash: String,
pub network: String,
}

impl Server {
Expand All @@ -41,15 +43,22 @@ impl Server {
http_server_port: port,
app_id,
..
} = self.cfg;
} = self.cfg.clone();

let port = (port.1 > 0)
.then(|| thread_rng().gen_range(port.0..=port.1))
.unwrap_or(port.0);

let v1_api = v1::routes(self.db.clone(), app_id, self.state.clone());
#[cfg(feature = "api-v2")]
let v2_api = v2::routes(self.version.clone(), self.network_version.clone());
let v2_api = v2::routes(
self.version.clone(),
self.network_version.clone(),
self.genesis_hash,
self.network,
self.state.clone(),
self.cfg,
);

let cors = warp::cors()
.allow_any_origin()
Expand Down
42 changes: 42 additions & 0 deletions src/api/v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,48 @@ Content-Type: application/json
- **version** - the Avail Light Client version
- **network_version** - Avail network version supported by the Avail Light Client

## **GET** `/v2/status`

Gets current status and active modes of the light client.

- Use cases
- Monitoring of the active light clients
- Reconfiguration verification
- Development tooling

Response:

```yaml
HTTP/1.1 200 OK
Content-Type: application/json

{
"modes": ["light", "app", "partition"],
"app_id": {app-id}, // Optional
"genesis_hash": "{genesis-hash}",
"network": "{network}",
"latest_block": {latest-block}, // Optional
"latest_synced_block": {sync-block}, // Optional
"sync_depth": {sync-depth}, // Optional
"partition": "{partition}" // Optional
}
```

- **modes** - active modes
- **app_id** - if **app** mode is active, this field contains configured application ID
- **genesis_hash** - genesis hash of the network to which the light client is connected
- **network** - network host, version and spec version light client is currently con
- **latest_block** - latest processed block
- **latest_synced_block** - the latest processed block in the sync range
- **sync_depth** - number of blocks before the latest to sync on light client start
- **partition** - if configured, displays partition which light client distributes to the peer to peer network

### Modes

- **light** - data availability sampling mode, the light client performs random sampling and calculates confidence
- **app** - light client fetches, verifies, and stores application-related data
- **partition** - light client fetches configured block partition and publishes it to the DHT

# WebSocket API

The Avail Light Client WebSocket API allows real-time communication between a client and a server over a persistent connection, enabling push notifications as an alternative to polling. Web socket API can be used on its own or in combination with HTTP API to enable different pull/push use cases.
Expand Down
54 changes: 52 additions & 2 deletions src/api/v2/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
use crate::{
api::v2::types::InternalServerError,
types::{RuntimeConfig, State},
};

use super::{
types::{Client, Clients, Subscription, SubscriptionId, Version},
types::{Client, Clients, Status, Subscription, SubscriptionId, Version},
ws,
};
use std::convert::Infallible;
use hyper::StatusCode;
use std::{
convert::Infallible,
sync::{Arc, Mutex},
};
use tracing::info;
use uuid::Uuid;
use warp::{ws::Ws, Rejection, Reply};

Expand All @@ -28,3 +38,43 @@ pub async fn ws(
// Multiple connections to the same client are currently allowed
Ok(ws.on_upgrade(move |web_socket| ws::connect(subscription_id, web_socket, clients, version)))
}

pub async fn status(
config: RuntimeConfig,
genesis_hash: String,
network: String,
state: Arc<Mutex<State>>,
) -> Result<impl Reply, impl Reply> {
let state = match state.lock() {
Ok(state) => state,
Err(error) => {
info!("Cannot acquire lock for last_block: {error}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
},
};

let latest_block = state.confidence_achieved.as_ref().map(|range| range.last);
let latest_synced_block = state
.sync_confidence_achieved
.as_ref()
.map(|range| range.last);

let status = Status {
modes: (&config).into(),
app_id: config.app_id,
genesis_hash,
network,
latest_block,
latest_synced_block,
sync_start_block: config.sync_start_block,
partition: config.block_matrix_partition,
};
Ok(status)
}

pub async fn handle_rejection(error: Rejection) -> Result<impl Reply, Rejection> {
if error.find::<InternalServerError>().is_some() {
return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
}
Err(error)
}
96 changes: 89 additions & 7 deletions src/api/v2/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
use self::types::{Clients, Version};
use std::{collections::HashMap, convert::Infallible, sync::Arc};
use crate::types::{RuntimeConfig, State};

use self::{
handlers::handle_rejection,
types::{Clients, Version},
};
use std::{
collections::HashMap,
convert::Infallible,
sync::{Arc, Mutex},
};
use tokio::sync::RwLock;
use warp::{Filter, Rejection, Reply};

Expand All @@ -19,6 +28,22 @@ fn version_route(
.map(move || version.clone())
}

fn status_route(
config: RuntimeConfig,
genesis_hash: String,
network: String,
state: Arc<Mutex<State>>,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
warp::path!("v2" / "status")
.and(warp::get())
.and(warp::any().map(move || config.clone()))
.and(warp::any().map(move || genesis_hash.clone()))
.and(warp::any().map(move || network.clone()))
.and(warp::any().map(move || state.clone()))
.then(handlers::status)
.map(types::handle_result)
}

fn subscriptions_route(
clients: Clients,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
Expand All @@ -43,32 +68,39 @@ fn ws_route(
pub fn routes(
version: String,
network_version: String,
genesis_hash: String,
network: String,
state: Arc<Mutex<State>>,
config: RuntimeConfig,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
let clients: Clients = Arc::new(RwLock::new(HashMap::new()));
let version = Version {
version,
network_version,
};
version_route(version.clone())
.or(status_route(config, genesis_hash, network, state))
.or(subscriptions_route(clients.clone()))
.or(ws_route(clients, version))
.recover(handle_rejection)
}

#[cfg(test)]
mod tests {
use crate::api::v2::types::{
Clients, DataFields, Subscription, SubscriptionId, Topics, Version,
use super::types::Client;
use crate::{
api::v2::types::{Clients, DataFields, Subscription, SubscriptionId, Topics, Version},
types::{RuntimeConfig, State},
};
use kate_recovery::matrix::Partition;
use std::{
collections::{HashMap, HashSet},
str::FromStr,
sync::Arc,
sync::{Arc, Mutex},
};
use tokio::sync::RwLock;
use uuid::Uuid;

use super::types::Client;

fn v1() -> Version {
Version {
version: "v1.0.0".to_string(),
Expand All @@ -91,6 +123,56 @@ mod tests {
);
}

#[tokio::test]
async fn status_route_defaults() {
let genesis_hash = "{genesis-hash}".to_string();
let network = "{network}".to_string();
let state = Arc::new(Mutex::new(State::default()));
let route = super::status_route(RuntimeConfig::default(), genesis_hash, network, state);
let response = warp::test::request()
.method("GET")
.path("/v2/status")
.reply(&route)
.await;

assert_eq!(
response.body(),
r#"{"modes":["light"],"genesis_hash":"{genesis-hash}","network":"{network}"}"#
);
}

#[tokio::test]
async fn status_route() {
let runtime_config = RuntimeConfig {
app_id: Some(1),
sync_start_block: Some(10),
block_matrix_partition: Some(Partition {
number: 1,
fraction: 10,
}),
..Default::default()
};
let state = Arc::new(Mutex::new(State::default()));
{
let mut state = state.lock().unwrap();
state.set_confidence_achieved(20);
state.set_sync_confidence_achieved(10);
}
let genesis_hash = "{genesis-hash}".to_string();
let network = "{network}".to_string();
let route = super::status_route(runtime_config, genesis_hash, network, state);
let response = warp::test::request()
.method("GET")
.path("/v2/status")
.reply(&route)
.await;

assert_eq!(
response.body(),
r#"{"modes":["light","app","partition"],"app_id":1,"genesis_hash":"{genesis-hash}","network":"{network}","latest_block":20,"latest_synced_block":10,"sync_start_block":10,"partition":"1/10"}"#
);
}

fn all_topics() -> HashSet<Topics> {
vec![
Topics::HeaderVerified,
Expand Down
63 changes: 63 additions & 0 deletions src/api/v2/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::anyhow;
use kate_recovery::matrix::Partition;
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
Expand All @@ -7,6 +8,13 @@ use std::{
use tokio::sync::{mpsc::UnboundedSender, RwLock};
use warp::{ws, Reply};

use crate::types::{block_matrix_partition_format, RuntimeConfig};

#[derive(Debug)]
pub struct InternalServerError {}

impl warp::reject::Reject for InternalServerError {}

#[derive(Serialize, Clone)]
pub struct Version {
pub version: String,
Expand All @@ -19,6 +27,54 @@ impl Reply for Version {
}
}

#[derive(Serialize)]
pub struct Status {
pub modes: Vec<Mode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub app_id: Option<u32>,
pub genesis_hash: String,
pub network: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub latest_block: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latest_synced_block: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sync_start_block: Option<u32>,
#[serde(
skip_serializing_if = "Option::is_none",
with = "block_matrix_partition_format"
)]
pub partition: Option<Partition>,
}

#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Mode {
Light,
App,
Partition,
}

impl From<&RuntimeConfig> for Vec<Mode> {
fn from(value: &RuntimeConfig) -> Self {
let mut result: Vec<Mode> = vec![];
result.push(Mode::Light);
if value.app_id.is_some() {
result.push(Mode::App);
}
if value.block_matrix_partition.is_some() {
result.push(Mode::Partition)
}
result
}
}

impl Reply for Status {
fn into_response(self) -> warp::reply::Response {
warp::reply::json(&self).into_response()
}
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum Topics {
Expand Down Expand Up @@ -120,3 +176,10 @@ impl From<Error> for String {
serde_json::to_string(&error).expect("Error is serializable")
}
}

pub fn handle_result(result: Result<impl Reply, impl Reply>) -> impl Reply {
match result {
Ok(ok) => ok.into_response(),
Err(err) => err.into_response(),
}
}
Loading

0 comments on commit 5b0d3bd

Please sign in to comment.