Skip to content

Commit

Permalink
[GraphQL/Config] Config for functional groups
Browse files Browse the repository at this point in the history
Introduce config for functional groups to enable and disable groups of
features in the schema, so that different operators can choose to run
the RPC service with different sets of features enabled (for example
running public nodes without analytics).

The precise set of functional groups may shift -- the current list is
derived from the list in the RFC (#13700).

This PR only introduces the config, and not the logic in the schema to
listen to the functional group config.  The config is read from a TOML
file whose path is passed as a command-line parameter.  It is stored
as data in the schema's context so that it can later be accessed by
whatever system limits access to endpoints by flags.

A stub "experiments" section has also been added as a place to keep
ad-hoc experimental flags (to gate in-progress features).

This PR also brings the `ServerConfig` (renamed to `ConnectionConfig`)
into the same module, and applies a common pattern to ensure there's a
single source of truth for default values (the `Default` impl for the
config structs).

Stack:

- #13745

Test Plan:

New unit tests:

```
sui-graphql-rpc$ cargo nextest run
sui-graphql-rpc$ cargo run
```
  • Loading branch information
amnn committed Sep 12, 2023
1 parent ddd53fa commit d918066
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 72 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/sui-graphql-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2021"


[dependencies]
anyhow.workspace = true
async-graphql = {workspace = true, features = ["dataloader"] }
async-graphql-axum = { version = "5.0.10" }
async-trait.workspace = true
Expand All @@ -22,6 +23,7 @@ serde_with.workspace = true
telemetry-subscribers.workspace = true
tracing.workspace = true
tokio.workspace = true
toml.workspace = true
thiserror.workspace = true
uuid.workspace = true

Expand Down
16 changes: 10 additions & 6 deletions crates/sui-graphql-rpc/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@ use std::path::PathBuf;
)]
pub enum Command {
GenerateSchema {
/// Path to output GraphQL schema to, in SDL format.
#[clap(short, long)]
file: Option<PathBuf>,
},
StartServer {
/// URL of the RPC server for data fetching
#[clap(short, long, default_value = "https://fullnode.testnet.sui.io:443/")]
rpc_url: String,
#[clap(short, long)]
rpc_url: Option<String>,
/// Port to bind the server to
#[clap(short, long, default_value = "8000")]
port: u16,
#[clap(short, long)]
port: Option<u16>,
/// Host to bind the server to
#[clap(long, default_value = "127.0.0.1")]
host: String,
#[clap(long)]
host: Option<String>,
/// Path to TOML file containing configuration for service.
#[clap(short, long)]
config: Option<PathBuf>,
},
}
133 changes: 133 additions & 0 deletions crates/sui-graphql-rpc/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use std::collections::BTreeSet;

use serde::Deserialize;

use crate::functional_group::FunctionalGroup;

/// Configuration on connections for the RPC, passed in as command-line arguments.
pub struct ConnectionConfig {
pub(crate) port: u16,
pub(crate) host: String,
pub(crate) rpc_url: String,
}

/// Configuration on features supported by the RPC, passed in a TOML-based file.
#[derive(Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct ServiceConfig {
#[serde(default)]
pub(crate) enabled_features: BTreeSet<FunctionalGroup>,

#[serde(default)]
pub(crate) experiments: Experiments,
}

#[derive(Deserialize, Debug, Eq, PartialEq, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Experiments {
// Add experimental flags here, to provide access to them through-out the GraphQL
// implementation.
#[cfg(test)]
test_flag: bool,
}

impl ConnectionConfig {
pub fn new(port: Option<u16>, host: Option<String>, rpc_url: Option<String>) -> Self {
let default = Self::default();
Self {
port: port.unwrap_or(default.port),
host: host.unwrap_or(default.host),
rpc_url: rpc_url.unwrap_or(default.rpc_url),
}
}
}

impl ServiceConfig {
pub fn read(contents: &str) -> Result<Self, toml::de::Error> {
toml::de::from_str::<Self>(contents)
}
}

impl Default for ConnectionConfig {
fn default() -> Self {
Self {
port: 8000,
host: "127.0.0.1".to_string(),
rpc_url: "https://fullnode.testnet.sui.io:443/".to_string(),
}
}
}

impl Default for ServiceConfig {
fn default() -> Self {
use FunctionalGroup as G;

Self {
enabled_features: BTreeSet::from([
G::Analytics,
G::Coins,
G::DynamicFields,
G::NameServer,
G::Packages,
G::Subscriptions,
G::SystemState,
]),
experiments: Experiments::default(),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_read_service_config() {
let actual = ServiceConfig::read(
r#" enabled-features = [
"coins",
"name-server",
]
"#,
)
.unwrap();

use FunctionalGroup as G;
let expect = ServiceConfig {
enabled_features: BTreeSet::from([G::Coins, G::NameServer]),
experiments: Experiments::default(),
};

assert_eq!(actual, expect)
}

#[test]
fn test_read_empty_service_config() {
let actual = ServiceConfig::read("").unwrap();
let expect = ServiceConfig {
enabled_features: BTreeSet::new(),
experiments: Experiments::default(),
};
assert_eq!(actual, expect);
}

#[test]
fn test_read_experiemts_in_service_config() {
let actual = ServiceConfig::read(
r#" [experiments]
test-flag = true
"#,
)
.unwrap();

let expect = ServiceConfig {
enabled_features: BTreeSet::new(),
experiments: Experiments { test_flag: true },
};

assert_eq!(actual, expect)
}
}
32 changes: 32 additions & 0 deletions crates/sui-graphql-rpc/src/functional_group.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use serde::Deserialize;

/// Logical Groups categorise APIs exposed by GraphQL. Groups can be enabled or disabled based on
/// settings in the RPC's TOML configuration file.
#[derive(Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum FunctionalGroup {
/// Statistics about how the network was running (TPS, top packages, APY, etc)
Analytics,

/// Coin metadata, per-address coin and balance information.
Coins,

/// Querying an object's dynamic fields.
DynamicFields,

/// SuiNS name and reverse name look-up.
NameServer,

/// Struct and function signatures, and popular packages.
Packages,

/// Transaction and Event subscriptions.
Subscriptions,

/// Information about the system that changes from epoch to epoch (protocol config, committee,
/// reference gas price).
SystemState,
}
5 changes: 4 additions & 1 deletion crates/sui-graphql-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@
// SPDX-License-Identifier: Apache-2.0

pub mod commands;
pub mod config;
pub mod server;

mod context_data;
mod error;
mod extensions;
mod functional_group;
mod types;

use crate::types::query::Query;
use async_graphql::*;
use types::owner::ObjectOwner;

use crate::types::query::Query;

pub fn schema_sdl_export() -> String {
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.register_output_type::<ObjectOwner>()
Expand Down
24 changes: 17 additions & 7 deletions crates/sui-graphql-rpc/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use std::fs;
use std::path::PathBuf;

use clap::Parser;
use sui_graphql_rpc::commands::Command;
use sui_graphql_rpc::config::{ConnectionConfig, ServiceConfig};
use sui_graphql_rpc::schema_sdl_export;
use sui_graphql_rpc::server::simple_server::start_example_server;
use sui_graphql_rpc::server::simple_server::ServerConfig;

#[tokio::main]
async fn main() {
Expand All @@ -24,15 +27,22 @@ async fn main() {
rpc_url,
port,
host,
config,
} => {
let config = ServerConfig {
port,
host,
rpc_url,
};
let conn = ConnectionConfig::new(port, host, rpc_url);
let service_config = service_config(config);

println!("Starting server...");
start_example_server(Some(config)).await;
start_example_server(conn, service_config).await;
}
}
}

fn service_config(path: Option<PathBuf>) -> ServiceConfig {
let Some(path) = path else {
return ServiceConfig::default();
};

let contents = fs::read_to_string(path).expect("Reading configuration");
ServiceConfig::read(&contents).expect("Deserializing configuration")
}
29 changes: 6 additions & 23 deletions crates/sui-graphql-rpc/src/server/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ use axum::middleware;
use axum::{routing::IntoMakeService, Router};
use std::any::Any;

pub(crate) const DEFAULT_PORT: u16 = 8000;
pub(crate) const DEFAULT_HOST: &str = "127.0.0.1";

pub(crate) struct Server {
pub server: hyper::Server<hyper::server::conn::AddrIncoming, IntoMakeService<Router>>,
}
Expand All @@ -26,37 +23,23 @@ impl Server {
}

pub(crate) struct ServerBuilder {
port: Option<u16>,
host: Option<String>,
port: u16,
host: String,

schema: SchemaBuilder<Query, EmptyMutation, EmptySubscription>,
}

impl ServerBuilder {
pub fn new() -> Self {
pub fn new(port: u16, host: String) -> Self {
Self {
port: None,
host: None,
port,
host,
schema: async_graphql::Schema::build(Query, EmptyMutation, EmptySubscription),
}
}

pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}

pub fn host(mut self, host: String) -> Self {
self.host = Some(host);
self
}

pub fn address(&self) -> String {
format!(
"{}:{}",
self.host.as_ref().unwrap_or(&DEFAULT_HOST.to_string()),
self.port.unwrap_or(DEFAULT_PORT)
)
format!("{}:{}", self.host, self.port)
}

pub fn context_data(mut self, context_data: impl Any + Send + Sync) -> Self {
Expand Down
Loading

0 comments on commit d918066

Please sign in to comment.