Replace gRPC Backend (#10)
**Rationale:** Having two separate servers and communication methods resulted in additional maintenance & the need to convert often between backend & frontend data types. By moving the backend communication off of gRPC and to just use websockets it both gives more control & allows for simplification of the implementation. #8 **Changes:** - Replaces gRPC backend. - New implementation automatically handles reconnect logic - Implements an api layer - Migrates examples to the api layer - Implements a proc macro to make command handling easier - Implements unit tests for the api layer (90+% coverage) - Implements integration tests for the proc macro (90+% coverage) Reviewed-on: #10 Co-authored-by: Sergey Savelyev <sergeysav.nn@gmail.com> Co-committed-by: Sergey Savelyev <sergeysav.nn@gmail.com>
This commit was merged in pull request #10.
This commit is contained in:
@@ -4,6 +4,7 @@ use crate::panels::PanelService;
|
||||
use actix_web::{delete, get, post, put, web, Responder};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateParam {
|
||||
@@ -13,7 +14,7 @@ struct CreateParam {
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct IdParam {
|
||||
id: String,
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
#[post("/panel")]
|
||||
@@ -22,7 +23,7 @@ pub(super) async fn new(
|
||||
data: web::Json<CreateParam>,
|
||||
) -> Result<impl Responder, HttpServerResultError> {
|
||||
let uuid = panels.create(&data.name, &data.data).await?;
|
||||
Ok(web::Json(uuid.value))
|
||||
Ok(web::Json(uuid))
|
||||
}
|
||||
|
||||
#[get("/panel")]
|
||||
@@ -38,12 +39,10 @@ pub(super) async fn get_one(
|
||||
panels: web::Data<Arc<PanelService>>,
|
||||
path: web::Path<IdParam>,
|
||||
) -> Result<impl Responder, HttpServerResultError> {
|
||||
let result = panels.read(path.id.clone().into()).await?;
|
||||
let result = panels.read(path.id).await?;
|
||||
match result {
|
||||
Some(result) => Ok(web::Json(result)),
|
||||
None => Err(HttpServerResultError::PanelUuidNotFound {
|
||||
uuid: path.id.clone(),
|
||||
}),
|
||||
None => Err(HttpServerResultError::PanelUuidNotFound { uuid: path.id }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +52,7 @@ pub(super) async fn set(
|
||||
path: web::Path<IdParam>,
|
||||
data: web::Json<PanelUpdate>,
|
||||
) -> Result<impl Responder, HttpServerResultError> {
|
||||
panels.update(path.id.clone().into(), data.0).await?;
|
||||
panels.update(path.id, data.0).await?;
|
||||
Ok(web::Json(()))
|
||||
}
|
||||
|
||||
@@ -62,6 +61,6 @@ pub(super) async fn delete(
|
||||
panels: web::Data<Arc<PanelService>>,
|
||||
path: web::Path<IdParam>,
|
||||
) -> Result<impl Responder, HttpServerResultError> {
|
||||
panels.delete(path.id.clone().into()).await?;
|
||||
panels.delete(path.id).await?;
|
||||
Ok(web::Json(()))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[get("/tlm/info/{name:[\\w\\d/_-]+}")]
|
||||
pub(super) async fn get_tlm_definition(
|
||||
@@ -36,13 +37,17 @@ struct HistoryQuery {
|
||||
resolution: i64,
|
||||
}
|
||||
|
||||
#[get("/tlm/history/{uuid:[0-9a-f]+}")]
|
||||
#[get("/tlm/history/{uuid:[0-9a-f-]+}")]
|
||||
pub(super) async fn get_tlm_history(
|
||||
data_arc: web::Data<Arc<TelemetryManagementService>>,
|
||||
uuid: web::Path<String>,
|
||||
info: web::Query<HistoryQuery>,
|
||||
) -> Result<impl Responder, HttpServerResultError> {
|
||||
let uuid = uuid.to_string();
|
||||
let Ok(uuid) = Uuid::parse_str(&uuid) else {
|
||||
return Err(HttpServerResultError::InvalidUuid {
|
||||
uuid: uuid.to_string(),
|
||||
});
|
||||
};
|
||||
trace!(
|
||||
"get_tlm_history {} from {} to {} resolution {}",
|
||||
uuid,
|
||||
|
||||
117
server/src/http/backend/connection.rs
Normal file
117
server/src/http/backend/connection.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use crate::command::command_handle::CommandHandle;
|
||||
use crate::command::service::CommandManagementService;
|
||||
use crate::telemetry::management_service::TelemetryManagementService;
|
||||
use actix_ws::{AggregatedMessage, ProtocolError, Session};
|
||||
use anyhow::bail;
|
||||
use api::messages::payload::RequestMessagePayload;
|
||||
use api::messages::{RequestMessage, ResponseMessage};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(super) struct BackendConnection {
|
||||
session: Session,
|
||||
tlm_management: Arc<TelemetryManagementService>,
|
||||
cmd_management: Arc<CommandManagementService>,
|
||||
tx: Sender<ResponseMessage>,
|
||||
commands: Vec<CommandHandle>,
|
||||
pub rx: Receiver<ResponseMessage>,
|
||||
pub should_close: bool,
|
||||
}
|
||||
|
||||
impl BackendConnection {
|
||||
pub fn new(
|
||||
session: Session,
|
||||
tlm_management: Arc<TelemetryManagementService>,
|
||||
cmd_management: Arc<CommandManagementService>,
|
||||
) -> Self {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<ResponseMessage>(128);
|
||||
Self {
|
||||
session,
|
||||
tlm_management,
|
||||
cmd_management,
|
||||
tx,
|
||||
commands: vec![],
|
||||
rx,
|
||||
should_close: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_request(&mut self, msg: RequestMessage) -> anyhow::Result<()> {
|
||||
match msg.payload {
|
||||
RequestMessagePayload::TelemetryDefinitionRequest(tlm_def) => {
|
||||
self.tx
|
||||
.send(ResponseMessage {
|
||||
uuid: Uuid::new_v4(),
|
||||
response: Some(msg.uuid),
|
||||
payload: self.tlm_management.register(tlm_def)?.into(),
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
RequestMessagePayload::TelemetryEntry(tlm_entry) => {
|
||||
self.tlm_management.add_tlm_item(tlm_entry)?;
|
||||
}
|
||||
RequestMessagePayload::GenericCallbackError(_) => todo!(),
|
||||
RequestMessagePayload::CommandDefinition(def) => {
|
||||
let cmd = self
|
||||
.cmd_management
|
||||
.register_command(msg.uuid, def, self.tx.clone())?;
|
||||
self.commands.push(cmd);
|
||||
}
|
||||
RequestMessagePayload::CommandResponse(response) => match msg.response {
|
||||
None => bail!("Command Response Payload Must Respond to a Command"),
|
||||
Some(uuid) => {
|
||||
self.cmd_management
|
||||
.handle_command_response(uuid, response)
|
||||
.await?;
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_request_message(
|
||||
&mut self,
|
||||
msg: Result<AggregatedMessage, ProtocolError>,
|
||||
) -> anyhow::Result<()> {
|
||||
let msg = msg?;
|
||||
match msg {
|
||||
AggregatedMessage::Text(data) => {
|
||||
self.handle_request(serde_json::from_str(&data)?).await?;
|
||||
}
|
||||
AggregatedMessage::Binary(_) => {
|
||||
bail!("Binary Messages Unsupported");
|
||||
}
|
||||
AggregatedMessage::Ping(bytes) => {
|
||||
self.session.pong(&bytes).await?;
|
||||
}
|
||||
AggregatedMessage::Pong(_) => {
|
||||
// Intentionally Ignore
|
||||
}
|
||||
AggregatedMessage::Close(_) => {
|
||||
self.should_close = true;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_response(&mut self, msg: ResponseMessage) -> anyhow::Result<()> {
|
||||
let msg_json = serde_json::to_string(&msg)?;
|
||||
self.session.text(msg_json).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cleanup(mut self) {
|
||||
self.rx.close();
|
||||
// Clone here to prevent conflict with the Drop trait
|
||||
let _ = self.session.clone().close(None).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BackendConnection {
|
||||
fn drop(&mut self) {
|
||||
for command in self.commands.drain(..) {
|
||||
self.cmd_management.unregister(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
server/src/http/backend/mod.rs
Normal file
60
server/src/http/backend/mod.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use futures_util::stream::StreamExt;
|
||||
mod connection;
|
||||
|
||||
use crate::command::service::CommandManagementService;
|
||||
use crate::http::backend::connection::BackendConnection;
|
||||
use crate::telemetry::management_service::TelemetryManagementService;
|
||||
use actix_web::{rt, web, HttpRequest, HttpResponse};
|
||||
use log::{error, trace};
|
||||
use std::sync::Arc;
|
||||
use tokio::select;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
async fn backend_connect(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
cancel_token: web::Data<CancellationToken>,
|
||||
telemetry_management_service: web::Data<Arc<TelemetryManagementService>>,
|
||||
command_management_service: web::Data<Arc<CommandManagementService>>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
trace!("backend_connect");
|
||||
let (res, session, stream) = actix_ws::handle(&req, stream)?;
|
||||
|
||||
let mut stream = stream
|
||||
.aggregate_continuations()
|
||||
// up to 1 MiB
|
||||
.max_continuation_size(2_usize.pow(20));
|
||||
|
||||
let cancel_token = cancel_token.get_ref().clone();
|
||||
let tlm_management = telemetry_management_service.get_ref().clone();
|
||||
let cmd_management = command_management_service.get_ref().clone();
|
||||
|
||||
rt::spawn(async move {
|
||||
let mut connection = BackendConnection::new(session, tlm_management, cmd_management);
|
||||
while !connection.should_close {
|
||||
let result = select! {
|
||||
_ = cancel_token.cancelled() => {
|
||||
connection.should_close = true;
|
||||
Ok(())
|
||||
},
|
||||
Some(msg) = connection.rx.recv() => connection.handle_response(msg).await,
|
||||
Some(msg) = stream.next() => connection.handle_request_message(msg).await,
|
||||
else => {
|
||||
connection.should_close = true;
|
||||
Ok(())
|
||||
},
|
||||
};
|
||||
if let Err(e) = result {
|
||||
error!("backend socket error: {e}");
|
||||
connection.should_close = true;
|
||||
}
|
||||
}
|
||||
connection.cleanup().await;
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn setup_backend(cfg: &mut web::ServiceConfig) {
|
||||
cfg.route("", web::get().to(backend_connect));
|
||||
}
|
||||
@@ -3,13 +3,16 @@ use actix_web::http::header::ContentType;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::HttpResponse;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum HttpServerResultError {
|
||||
#[error("Telemetry Name Not Found: {tlm}")]
|
||||
TlmNameNotFound { tlm: String },
|
||||
#[error("Invalid Uuid: {uuid}")]
|
||||
InvalidUuid { uuid: String },
|
||||
#[error("Telemetry Uuid Not Found: {uuid}")]
|
||||
TlmUuidNotFound { uuid: String },
|
||||
TlmUuidNotFound { uuid: Uuid },
|
||||
#[error("DateTime Parsing Error: {date_time}")]
|
||||
InvalidDateTime { date_time: String },
|
||||
#[error("Timed out")]
|
||||
@@ -17,7 +20,7 @@ pub enum HttpServerResultError {
|
||||
#[error("Internal Error")]
|
||||
InternalError(#[from] anyhow::Error),
|
||||
#[error("Panel Uuid Not Found: {uuid}")]
|
||||
PanelUuidNotFound { uuid: String },
|
||||
PanelUuidNotFound { uuid: Uuid },
|
||||
#[error(transparent)]
|
||||
Command(#[from] crate::command::error::Error),
|
||||
}
|
||||
@@ -26,6 +29,7 @@ impl ResponseError for HttpServerResultError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
HttpServerResultError::TlmNameNotFound { .. } => StatusCode::NOT_FOUND,
|
||||
HttpServerResultError::InvalidUuid { .. } => StatusCode::BAD_REQUEST,
|
||||
HttpServerResultError::TlmUuidNotFound { .. } => StatusCode::NOT_FOUND,
|
||||
HttpServerResultError::InvalidDateTime { .. } => StatusCode::BAD_REQUEST,
|
||||
HttpServerResultError::Timeout => StatusCode::GATEWAY_TIMEOUT,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
mod api;
|
||||
mod backend;
|
||||
mod error;
|
||||
mod websocket;
|
||||
|
||||
use crate::command::service::CommandManagementService;
|
||||
use crate::http::api::setup_api;
|
||||
use crate::http::backend::setup_backend;
|
||||
use crate::http::websocket::setup_websocket;
|
||||
use crate::panels::PanelService;
|
||||
use crate::telemetry::management_service::TelemetryManagementService;
|
||||
@@ -31,6 +33,7 @@ pub async fn setup(
|
||||
.app_data(cancel_token.clone())
|
||||
.app_data(panel_service.clone())
|
||||
.app_data(command_service.clone())
|
||||
.service(web::scope("/backend").configure(setup_backend))
|
||||
.service(web::scope("/ws").configure(setup_websocket))
|
||||
.service(web::scope("/api").configure(setup_api))
|
||||
.wrap(Logger::default())
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::telemetry::management_service::TelemetryManagementService;
|
||||
use actix_web::{rt, web, HttpRequest, HttpResponse};
|
||||
use actix_ws::{AggregatedMessage, ProtocolError, Session};
|
||||
use anyhow::anyhow;
|
||||
use futures_util::StreamExt;
|
||||
use log::{error, trace};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -14,7 +15,7 @@ use tokio::select;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::time::{sleep_until, Instant};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tonic::codegen::tokio_stream::StreamExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
@@ -23,11 +24,11 @@ fn handle_register_tlm_listener(
|
||||
data: &Arc<TelemetryManagementService>,
|
||||
request: RegisterTlmListenerRequest,
|
||||
tx: &Sender<WebsocketResponse>,
|
||||
tlm_listeners: &mut HashMap<String, CancellationToken>,
|
||||
tlm_listeners: &mut HashMap<Uuid, CancellationToken>,
|
||||
) {
|
||||
if let Some(tlm_data) = data.get_by_uuid(&request.uuid) {
|
||||
let token = CancellationToken::new();
|
||||
if let Some(token) = tlm_listeners.insert(tlm_data.definition.uuid.clone(), token.clone()) {
|
||||
if let Some(token) = tlm_listeners.insert(tlm_data.definition.uuid, token.clone()) {
|
||||
token.cancel();
|
||||
}
|
||||
let minimum_separation = Duration::from_millis(request.minimum_separation_ms as u64);
|
||||
@@ -46,7 +47,7 @@ fn handle_register_tlm_listener(
|
||||
ref_val.clone()
|
||||
};
|
||||
let _ = tx.send(TlmValueResponse {
|
||||
uuid: request.uuid.clone(),
|
||||
uuid: request.uuid,
|
||||
value,
|
||||
}.into()).await;
|
||||
now
|
||||
@@ -65,7 +66,7 @@ fn handle_register_tlm_listener(
|
||||
|
||||
fn handle_unregister_tlm_listener(
|
||||
request: UnregisterTlmListenerRequest,
|
||||
tlm_listeners: &mut HashMap<String, CancellationToken>,
|
||||
tlm_listeners: &mut HashMap<Uuid, CancellationToken>,
|
||||
) {
|
||||
if let Some(token) = tlm_listeners.remove(&request.uuid) {
|
||||
token.cancel();
|
||||
@@ -76,7 +77,7 @@ async fn handle_websocket_message(
|
||||
data: &Arc<TelemetryManagementService>,
|
||||
request: WebsocketRequest,
|
||||
tx: &Sender<WebsocketResponse>,
|
||||
tlm_listeners: &mut HashMap<String, CancellationToken>,
|
||||
tlm_listeners: &mut HashMap<Uuid, CancellationToken>,
|
||||
) {
|
||||
match request {
|
||||
WebsocketRequest::RegisterTlmListener(request) => {
|
||||
@@ -110,7 +111,7 @@ async fn handle_websocket_incoming(
|
||||
data: &Arc<TelemetryManagementService>,
|
||||
session: &mut Session,
|
||||
tx: &Sender<WebsocketResponse>,
|
||||
tlm_listeners: &mut HashMap<String, CancellationToken>,
|
||||
tlm_listeners: &mut HashMap<Uuid, CancellationToken>,
|
||||
) -> anyhow::Result<bool> {
|
||||
match msg {
|
||||
Ok(AggregatedMessage::Close(_)) => Ok(false),
|
||||
@@ -130,7 +131,7 @@ async fn handle_websocket_incoming(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn websocket_connect(
|
||||
async fn websocket_connect(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
data: web::Data<Arc<TelemetryManagementService>>,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use derive_more::From;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RegisterTlmListenerRequest {
|
||||
pub uuid: String,
|
||||
pub uuid: Uuid,
|
||||
pub minimum_separation_ms: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UnregisterTlmListenerRequest {
|
||||
pub uuid: String,
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, From)]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::telemetry::data_item::TelemetryDataItem;
|
||||
use derive_more::From;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TlmValueResponse {
|
||||
pub uuid: String,
|
||||
pub uuid: Uuid,
|
||||
pub value: Option<TelemetryDataItem>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user