Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/skade

skade-flight/src/server.rs

//! Bind and serve. TLS rides tonic's `tls-ring` behind the `tls` feature.

use std::net::SocketAddr;
use std::sync::Arc;

use arrow_flight::flight_service_server::FlightServiceServer;
use tonic::transport::Server;

use crate::auth::{Auth, interceptor};
use crate::engine::Engine;
use crate::service::SkadeFlightService;

#[derive(Debug, Clone, Default)]
pub struct ServeOptions {
    /// PEM cert + key for TLS; optional client CA for mTLS.
    #[cfg(feature = "tls")]
    pub tls: Option<TlsOptions>,
}

#[cfg(feature = "tls")]
#[derive(Debug, Clone)]
pub struct TlsOptions {
    pub cert_pem: Vec<u8>,
    pub key_pem: Vec<u8>,
    pub client_ca_pem: Option<Vec<u8>>,
}

fn builder(opts: ServeOptions) -> Result<Server, tonic::transport::Error> {
    #[allow(unused_mut)]
    let mut b = Server::builder();
    // HTTP/2 max frame size. h2's default is 16 KiB: a 100 MB scan is ~6 500
    // DATA frames, each a state-machine turn. MEASURED 2026-09-09, 2M rows × 6
    // cols on loopback, both peers at 1 MiB: 23.5M → 39.8M rows/s, raw wire
    // 1.2 → 2.0 GB/s. Window size and batch size made no difference — this did.
    // The frame size a peer may SEND is what the RECEIVER advertises, so this
    // setting governs what clients send us (DoPut, handshake) and the client's
    // own setting governs our DoGet stream; gRPC C++ (the ODBC driver) and the
    // Go ADBC driver both advertise more than h2's default.
    // SKADE_FLIGHT_H2_FRAME_KB overrides (clamped to h2's legal 16 KiB..16 MiB-1).
    let frame = std::env::var("SKADE_FLIGHT_H2_FRAME_KB")
        .ok()
        .and_then(|v| v.parse::<u32>().ok())
        .map(|kb| kb << 10)
        .unwrap_or(1 << 20)
        .clamp(16 * 1024, (1 << 24) - 1);
    b = b.max_frame_size(frame);
    #[cfg(feature = "tls")]
    if let Some(t) = opts.tls {
        use tonic::transport::{Certificate, Identity, ServerTlsConfig};
        let mut cfg = ServerTlsConfig::new().identity(Identity::from_pem(t.cert_pem, t.key_pem));
        if let Some(ca) = t.client_ca_pem {
            cfg = cfg.client_ca_root(Certificate::from_pem(ca));
        }
        b = b.tls_config(cfg)?;
    }
    #[cfg(not(feature = "tls"))]
    let _ = opts;
    Ok(b)
}

/// Serve until `shutdown` resolves (or forever with `futures::future::pending()`).
pub async fn serve(
    engine: Arc<Engine>,
    auth: Arc<Auth>,
    addr: SocketAddr,
    opts: ServeOptions,
    shutdown: impl std::future::Future<Output = ()>,
) -> Result<(), tonic::transport::Error> {
    let svc = SkadeFlightService::new(engine, auth.clone());
    let svc = FlightServiceServer::with_interceptor(svc, interceptor(auth));
    builder(opts)?.add_service(svc).serve_with_shutdown(addr, shutdown).await
}

/// Bind an ephemeral or fixed listener first (so the caller knows the port),
/// then serve on it. What the tests, the bench and a `--listen 127.0.0.1:0` use.
pub async fn serve_on(
    engine: Arc<Engine>,
    auth: Arc<Auth>,
    listener: tokio::net::TcpListener,
    opts: ServeOptions,
    shutdown: impl std::future::Future<Output = ()>,
) -> Result<(), tonic::transport::Error> {
    let svc = SkadeFlightService::new(engine, auth.clone());
    let svc = FlightServiceServer::with_interceptor(svc, interceptor(auth));
    // tonic's own incoming, so accepted sockets get TCP_NODELAY like `serve`'s
    // do. MEASURED 2026-09-09: a hand-rolled accept loop handed tonic raw
    // sockets and every small RPC paid Nagle + delayed-ACK — 42 ms per
    // GetFlightInfo on loopback, 83 ms per GetSqlInfo (two round trips).
    let incoming = tonic::transport::server::TcpIncoming::from(listener).with_nodelay(Some(true));
    builder(opts)?.add_service(svc).serve_with_incoming_shutdown(incoming, shutdown).await
}