feat: v2 Phase 1 — foundation, proto schemas, RPC framework, SDK skeleton
New workspace structure with 9 crates. Adds: - proto/qpq/v1/*.proto: 11 protobuf schemas covering all 33 RPC methods - quicproquo-proto: dual codegen (capnp legacy + prost v2) - quicproquo-rpc: QUIC RPC framework (framing, server, client, middleware) - quicproquo-sdk: client SDK (QpqClient, events, conversation store) - quicproquo-server/domain/: protocol-agnostic domain types and services - justfile: build commands Wire format: [method_id:u16][req_id:u32][len:u32][protobuf] per QUIC stream. All 151 existing tests pass. Backward compatible with v1 capnp code.
This commit is contained in:
198
crates/quicproquo-rpc/src/server.rs
Normal file
198
crates/quicproquo-rpc/src/server.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
//! QUIC RPC server — accepts connections, dispatches requests to handlers.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::BytesMut;
|
||||
use quinn::{Endpoint, Incoming, RecvStream, SendStream};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::error::{RpcError, RpcStatus};
|
||||
use crate::framing::{RequestFrame, ResponseFrame, PushFrame};
|
||||
use crate::method::{HandlerResult, MethodRegistry, RequestContext};
|
||||
|
||||
/// Configuration for the RPC server.
|
||||
pub struct RpcServerConfig {
|
||||
/// QUIC listen address.
|
||||
pub listen_addr: std::net::SocketAddr,
|
||||
/// TLS server config (rustls).
|
||||
pub tls_config: Arc<rustls::ServerConfig>,
|
||||
/// ALPN protocol for the RPC service.
|
||||
pub alpn: Vec<u8>,
|
||||
}
|
||||
|
||||
/// The QUIC RPC server.
|
||||
pub struct RpcServer<S: Send + Sync + 'static> {
|
||||
endpoint: Endpoint,
|
||||
state: Arc<S>,
|
||||
registry: Arc<MethodRegistry<S>>,
|
||||
}
|
||||
|
||||
impl<S: Send + Sync + 'static> RpcServer<S> {
|
||||
/// Create and bind the QUIC endpoint. Does not start accepting yet.
|
||||
pub fn bind(
|
||||
config: RpcServerConfig,
|
||||
state: Arc<S>,
|
||||
registry: MethodRegistry<S>,
|
||||
) -> Result<Self, RpcError> {
|
||||
let mut tls = (*config.tls_config).clone();
|
||||
tls.alpn_protocols = vec![config.alpn];
|
||||
let quic_tls = quinn::crypto::rustls::QuicServerConfig::try_from(tls)
|
||||
.map_err(|e| RpcError::Connection(format!("TLS config: {e}")))?;
|
||||
let server_config = quinn::ServerConfig::with_crypto(Arc::new(quic_tls));
|
||||
|
||||
let endpoint = Endpoint::server(server_config, config.listen_addr)
|
||||
.map_err(|e| RpcError::Connection(format!("bind {}: {e}", config.listen_addr)))?;
|
||||
|
||||
info!(addr = %config.listen_addr, "RPC server bound");
|
||||
|
||||
Ok(Self {
|
||||
endpoint,
|
||||
state,
|
||||
registry: Arc::new(registry),
|
||||
})
|
||||
}
|
||||
|
||||
/// Accept connections in a loop. Spawns a task per connection.
|
||||
pub async fn serve(self) -> Result<(), RpcError> {
|
||||
info!("RPC server accepting connections");
|
||||
while let Some(incoming) = self.endpoint.accept().await {
|
||||
let state = Arc::clone(&self.state);
|
||||
let registry = Arc::clone(&self.registry);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_connection(incoming, state, registry).await {
|
||||
warn!("connection error: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the local address the server is listening on.
|
||||
pub fn local_addr(&self) -> Result<std::net::SocketAddr, RpcError> {
|
||||
self.endpoint
|
||||
.local_addr()
|
||||
.map_err(|e| RpcError::Connection(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a single QUIC connection: accept bi-directional streams for RPCs.
|
||||
async fn handle_connection<S: Send + Sync + 'static>(
|
||||
incoming: Incoming,
|
||||
state: Arc<S>,
|
||||
registry: Arc<MethodRegistry<S>>,
|
||||
) -> Result<(), RpcError> {
|
||||
let connection = incoming
|
||||
.await
|
||||
.map_err(|e| RpcError::Connection(e.to_string()))?;
|
||||
|
||||
let remote = connection.remote_address();
|
||||
debug!(remote = %remote, "new connection");
|
||||
|
||||
loop {
|
||||
let stream = connection.accept_bi().await;
|
||||
match stream {
|
||||
Ok((send, recv)) => {
|
||||
let state = Arc::clone(&state);
|
||||
let registry = Arc::clone(®istry);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_stream(send, recv, state, registry).await {
|
||||
debug!("stream error: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(quinn::ConnectionError::ApplicationClosed(_)) => {
|
||||
debug!(remote = %remote, "connection closed by peer");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(remote = %remote, "accept_bi error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle a single bi-directional stream: read request, dispatch, write response.
|
||||
async fn handle_stream<S: Send + Sync + 'static>(
|
||||
mut send: SendStream,
|
||||
mut recv: RecvStream,
|
||||
state: Arc<S>,
|
||||
registry: Arc<MethodRegistry<S>>,
|
||||
) -> Result<(), RpcError> {
|
||||
// Read the complete request from the stream.
|
||||
let mut buf = BytesMut::new();
|
||||
while let Some(chunk) = recv
|
||||
.read_chunk(65536, true)
|
||||
.await
|
||||
.map_err(|e| RpcError::Connection(e.to_string()))?
|
||||
{
|
||||
buf.extend_from_slice(&chunk.bytes);
|
||||
if buf.len() > crate::framing::MAX_PAYLOAD_SIZE + crate::framing::REQUEST_HEADER_SIZE {
|
||||
return Err(RpcError::PayloadTooLarge {
|
||||
size: buf.len(),
|
||||
max: crate::framing::MAX_PAYLOAD_SIZE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let frame = match RequestFrame::decode(&mut buf)? {
|
||||
Some(f) => f,
|
||||
None => return Err(RpcError::Decode("incomplete request frame".into())),
|
||||
};
|
||||
|
||||
let result = match registry.get(frame.method_id) {
|
||||
Some((handler, name)) => {
|
||||
debug!(method_id = frame.method_id, method = name, req_id = frame.request_id, "dispatching");
|
||||
let ctx = RequestContext {
|
||||
identity_key: None, // populated by auth middleware
|
||||
session_token: None,
|
||||
payload: frame.payload,
|
||||
};
|
||||
handler(Arc::clone(&state), ctx).await
|
||||
}
|
||||
None => {
|
||||
warn!(method_id = frame.method_id, "unknown method");
|
||||
HandlerResult::err(RpcStatus::UnknownMethod, "unknown method")
|
||||
}
|
||||
};
|
||||
|
||||
let response = ResponseFrame {
|
||||
status: result.status as u8,
|
||||
request_id: frame.request_id,
|
||||
payload: result.payload,
|
||||
};
|
||||
|
||||
let encoded = response.encode();
|
||||
send.write_all(&encoded)
|
||||
.await
|
||||
.map_err(|e| RpcError::Connection(e.to_string()))?;
|
||||
send.finish().map_err(|e| RpcError::Connection(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a push event to a client via a QUIC uni-stream.
|
||||
pub async fn send_push(
|
||||
connection: &quinn::Connection,
|
||||
event_type: u16,
|
||||
payload: bytes::Bytes,
|
||||
) -> Result<(), RpcError> {
|
||||
let mut send = connection
|
||||
.open_uni()
|
||||
.await
|
||||
.map_err(|e| RpcError::Connection(e.to_string()))?;
|
||||
|
||||
let frame = PushFrame {
|
||||
event_type,
|
||||
payload,
|
||||
};
|
||||
let encoded = frame.encode();
|
||||
send.write_all(&encoded)
|
||||
.await
|
||||
.map_err(|e| RpcError::Connection(e.to_string()))?;
|
||||
send.finish().map_err(|e| RpcError::Connection(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user