feat: http server and cmd crate etc. (#15)

* feat: adds cmd crate and http server

* feat: impl sql http handler

* feat: convert all arrow array types

* feat: adds query test

* feat: adds test for datanode

* fix: format

* feat: refactor state.rs

* feat: adds collect test

* fix: by code review

* fix: style
This commit is contained in:
dennis zhuang
2022-05-06 17:34:29 +08:00
committed by GitHub
parent ffc4e4eb0c
commit f0db878bf5
47 changed files with 1648 additions and 181 deletions
+16
View File
@@ -6,4 +6,20 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.5"
axum-macros = "0.2"
common-recordbatch = {path = "../common/recordbatch" }
hyper = { version = "0.14", features = ["full"] }
query = { path = "../query" }
serde = "1.0"
serde_json = "1.0"
snafu = "0.7"
table = { path = "../table" }
tokio = { version = "1.18", features = ["full"] }
tower = { version = "0.4", features = ["full"]}
tower-http = { version ="0.3", features = ["full"]}
[dev-dependencies.arrow]
package = "arrow2"
version="0.10"
features = ["io_csv", "io_json", "io_parquet", "io_parquet_compression", "io_ipc", "ahash", "compute", "serde_types"]
+33
View File
@@ -0,0 +1,33 @@
use std::sync::Arc;
use query::catalog::memory;
use query::catalog::CatalogListRef;
use snafu::ResultExt;
use crate::error::{QuerySnafu, Result};
use crate::instance::{Instance, InstanceRef};
use crate::server::Services;
/// DataNode service.
pub struct DataNode {
services: Services,
_catalog_list: CatalogListRef,
_instance: InstanceRef,
}
impl DataNode {
pub fn new() -> Result<DataNode> {
let catalog_list = memory::new_memory_catalog_list().context(QuerySnafu)?;
let instance = Arc::new(Instance::new(catalog_list.clone()));
Ok(Self {
services: Services::new(instance.clone()),
_catalog_list: catalog_list,
_instance: instance,
})
}
pub async fn start(&self) -> Result<()> {
self.services.start().await
}
}
+9 -2
View File
@@ -1,8 +1,15 @@
use hyper::Error as HyperError;
use query::error::Error as QueryError;
use snafu::Snafu;
/// business error of datanode.
#[derive(Debug, Snafu)]
#[snafu(display("DataNode error"))]
pub struct Error;
#[snafu(visibility(pub))]
pub enum Error {
#[snafu(display("Query error: {}", source))]
Query { source: QueryError },
#[snafu(display("Http error: {}", source))]
Hyper { source: HyperError },
}
pub type Result<T> = std::result::Result<T, Error>;
+73
View File
@@ -0,0 +1,73 @@
use std::sync::Arc;
use query::catalog::CatalogListRef;
use query::query_engine::{Output, QueryEngineFactory, QueryEngineRef};
use snafu::ResultExt;
use crate::error::{QuerySnafu, Result};
// An abstraction to read/write services.
pub struct Instance {
// Query service
query_engine: QueryEngineRef,
// Catalog list
_catalog_list: CatalogListRef,
}
pub type InstanceRef = Arc<Instance>;
impl Instance {
pub fn new(catalog_list: CatalogListRef) -> Self {
let factory = QueryEngineFactory::new(catalog_list.clone());
let query_engine = factory.query_engine().clone();
Self {
query_engine,
_catalog_list: catalog_list,
}
}
pub async fn execute_sql(&self, sql: &str) -> Result<Output> {
let logical_plan = self.query_engine.sql_to_plan(sql).context(QuerySnafu)?;
self.query_engine
.execute(&logical_plan)
.await
.context(QuerySnafu)
}
}
#[cfg(test)]
mod tests {
use arrow::array::UInt64Array;
use common_recordbatch::util;
use query::catalog::memory;
use super::*;
#[tokio::test]
async fn test_execute_sql() {
let catalog_list = memory::new_memory_catalog_list().unwrap();
let instance = Instance::new(catalog_list);
let output = instance
.execute_sql("select sum(number) from numbers limit 20")
.await
.unwrap();
match output {
Output::RecordBatch(recordbatch) => {
let numbers = util::collect(recordbatch).await.unwrap();
let columns = numbers[0].df_recordbatch.columns();
assert_eq!(1, columns.len());
assert_eq!(columns[0].len(), 1);
assert_eq!(
*columns[0].as_any().downcast_ref::<UInt64Array>().unwrap(),
UInt64Array::from_slice(&[4950])
);
}
_ => unreachable!(),
}
}
}
+4 -18
View File
@@ -1,21 +1,7 @@
mod catalog;
pub mod datanode;
mod error;
mod processors;
mod rpc;
mod instance;
mod server;
use crate::error::Result;
use crate::rpc::Services;
/// DataNode service.
pub struct DataNode {
services: Services,
}
impl DataNode {
/// Shutdown the datanode service gracefully.
pub async fn shutdown(&self) -> Result<()> {
self.services.shutdown().await?;
unimplemented!()
}
}
pub use crate::datanode::DataNode;
-10
View File
@@ -1,10 +0,0 @@
use crate::error::Result;
/// All rpc services.
pub struct Services {}
impl Services {
pub async fn shutdown(&self) -> Result<()> {
unimplemented!()
}
}
+24
View File
@@ -0,0 +1,24 @@
mod grpc;
mod http;
use http::HttpServer;
use crate::error::Result;
use crate::instance::InstanceRef;
/// All rpc services.
pub struct Services {
http_server: HttpServer,
}
impl Services {
pub fn new(instance: InstanceRef) -> Self {
Self {
http_server: HttpServer::new(instance),
}
}
pub async fn start(&self) -> Result<()> {
self.http_server.start().await
}
}
+1
View File
@@ -0,0 +1 @@
mod processors;
+122
View File
@@ -0,0 +1,122 @@
mod handler;
use std::net::SocketAddr;
use std::time::Duration;
use axum::{
error_handling::HandleErrorLayer,
response::IntoResponse,
response::{Json, Response},
routing::get,
BoxError, Extension, Router,
};
use common_recordbatch::{util, RecordBatch};
use query::Output;
use serde::Serialize;
use snafu::ResultExt;
use tower::{timeout::TimeoutLayer, ServiceBuilder};
use tower_http::trace::TraceLayer;
use crate::error::{HyperSnafu, Result};
use crate::server::InstanceRef;
/// Http server
pub struct HttpServer {
instance: InstanceRef,
}
#[derive(Serialize)]
pub enum JsonOutput {
AffectedRows(usize),
Rows(Vec<RecordBatch>),
}
#[derive(Serialize)]
pub struct JsonResponse {
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
output: Option<JsonOutput>,
}
impl IntoResponse for JsonResponse {
fn into_response(self) -> Response {
Json(self).into_response()
}
}
impl JsonResponse {
fn with_error(error: Option<String>) -> Self {
JsonResponse {
success: false,
error,
output: None,
}
}
fn with_output(output: Option<JsonOutput>) -> Self {
JsonResponse {
success: true,
error: None,
output,
}
}
/// Create a json response from query result
async fn from_output(output: Result<Output>) -> Self {
match output {
Ok(Output::AffectedRows(rows)) => {
Self::with_output(Some(JsonOutput::AffectedRows(rows)))
}
Ok(Output::RecordBatch(stream)) => match util::collect(stream).await {
Ok(rows) => Self::with_output(Some(JsonOutput::Rows(rows))),
Err(e) => Self::with_error(Some(format!("Recordbatch error: {}", e))),
},
Err(e) => Self::with_error(Some(format!("Query engine output error: {}", e))),
}
}
}
async fn shutdown_signal() {
// Wait for the CTRL+C signal
// It has an issue on chrome: https://github.com/sigp/lighthouse/issues/478
tokio::signal::ctrl_c()
.await
.expect("failed to install CTRL+C signal handler");
}
impl HttpServer {
pub fn new(instance: InstanceRef) -> Self {
Self { instance }
}
pub async fn start(&self) -> Result<()> {
let app = Router::new().route("/sql", get(handler::sql)).layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(handle_error))
.layer(TraceLayer::new_for_http())
.layer(Extension(self.instance.clone()))
// TODO configure timeout
.layer(TimeoutLayer::new(Duration::from_secs(30))),
);
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
// TODO(dennis): log
println!("Datanode HTTP server is listening on {}", addr);
let server = axum::Server::bind(&addr).serve(app.into_make_service());
let graceful = server.with_graceful_shutdown(shutdown_signal());
graceful.await.context(HyperSnafu)?;
Ok(())
}
}
/// handle error middleware
async fn handle_error(err: BoxError) -> Json<JsonResponse> {
Json(JsonResponse {
success: false,
error: Some(format!("Unhandled internal error: {}", err)),
output: None,
})
}
+75
View File
@@ -0,0 +1,75 @@
// http handlers
use std::collections::HashMap;
use axum::extract::{Extension, Query};
use crate::instance::InstanceRef;
use crate::server::http::JsonResponse;
/// Handler to execute sql
#[axum_macros::debug_handler]
pub async fn sql(
Extension(instance): Extension<InstanceRef>,
Query(params): Query<HashMap<String, String>>,
) -> JsonResponse {
if let Some(sql) = params.get("sql") {
JsonResponse::from_output(instance.execute_sql(sql).await).await
} else {
JsonResponse::with_error(Some("sql parameter is required.".to_string()))
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use query::catalog::memory;
use super::*;
use crate::instance::Instance;
use crate::server::http::JsonOutput;
fn create_params() -> Query<HashMap<String, String>> {
let mut map = HashMap::new();
map.insert(
"sql".to_string(),
"select sum(number) from numbers limit 20".to_string(),
);
Query(map)
}
fn create_extension() -> Extension<InstanceRef> {
let catalog_list = memory::new_memory_catalog_list().unwrap();
let instance = Arc::new(Instance::new(catalog_list));
Extension(instance)
}
#[tokio::test]
async fn test_sql_not_provided() {
let extension = create_extension();
let json = sql(extension, Query(HashMap::default())).await;
assert!(!json.success);
assert_eq!(Some("sql parameter is required.".to_string()), json.error);
assert!(json.output.is_none());
}
#[tokio::test]
async fn test_sql_output_rows() {
let query = create_params();
let extension = create_extension();
let json = sql(extension, query).await;
assert!(json.success);
assert!(json.error.is_none());
assert!(json.output.is_some());
match json.output.unwrap() {
JsonOutput::Rows(rows) => {
assert_eq!(1, rows.len());
}
_ => unreachable!(),
}
}
}