Skip to main content

hydracache_server/
hc2.rs

1//! Production HC/2 listener backed by the existing verified client dispatch.
2
3use std::collections::BTreeMap;
4use std::pin::Pin;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::Arc;
7
8use bytes::Bytes;
9use futures_util::Stream;
10use hydracache_client_hc2::wire::client_envelope;
11use hydracache_client_hc2::wire::client_plane_alpha_server::{
12    ClientPlaneAlpha, ClientPlaneAlphaServer,
13};
14use hydracache_client_hc2::wire::invocation_request;
15use hydracache_client_hc2::wire::invocation_response;
16use hydracache_client_hc2::wire::server_envelope;
17use hydracache_client_hc2::wire::{
18    BatchResult, CacheEvent, ClientEnvelope, EventGap, HandshakeAck, InvocationRequest,
19    InvocationResponse, LockOwnershipResult, LockResult, MutationResult, ResponseMeta,
20    ServerEnvelope, SessionHeartbeat, SessionLost, StableErrorCode, SubscriptionAck, ValueResult,
21};
22use hydracache_client_hc2::{is_supported_hc2_generation, HC2_GENERATION, HC2_MINIMUM_GENERATION};
23use hydracache_client_protocol::{
24    CasExpectation, ClientErrorCode, ClientRequest, ClientRequestEnvelope, ClientResponse,
25    ClientResponseEnvelope, LockConsistency, Namespace, StructuredKey,
26};
27use hydracache_client_transport_axum::{ClientIdentity, ClientSurfaceState};
28use sha2::{Digest, Sha256};
29use thiserror::Error;
30use tokio::net::TcpListener;
31use tokio::sync::{mpsc, watch};
32use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream};
33use tonic::transport::server::{TcpConnectInfo, TlsConnectInfo};
34use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig};
35use tonic::{Request, Response, Status, Streaming};
36
37use crate::config::TlsConfig;
38
39/// Opaque, eagerly validated TLS policy for the production HC/2 listener.
40#[derive(Clone)]
41pub struct Hc2ListenerTls(ServerTlsConfig);
42
43impl std::fmt::Debug for Hc2ListenerTls {
44    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        formatter
46            .debug_struct("Hc2ListenerTls")
47            .finish_non_exhaustive()
48    }
49}
50
51impl Hc2ListenerTls {
52    /// Read and validate mandatory server identity and client trust roots.
53    pub fn from_server_config(tls: &TlsConfig) -> Result<Self, Hc2ServeError> {
54        if rustls::crypto::CryptoProvider::get_default().is_none() {
55            let _ = rustls::crypto::ring::default_provider().install_default();
56        }
57        let cert = std::fs::read(tls.cert_path.as_deref().ok_or(Hc2ServeError::MissingTls)?)?;
58        let key = std::fs::read(tls.key_path.as_deref().ok_or(Hc2ServeError::MissingTls)?)?;
59        let ca = std::fs::read(tls.ca_path.as_deref().ok_or(Hc2ServeError::MissingTls)?)?;
60        let config = ServerTlsConfig::new()
61            .identity(Identity::from_pem(cert, key))
62            .client_ca_root(Certificate::from_pem(ca));
63        // Tonic parses the PEM material here. Do this before any listener task
64        // starts so malformed identity/trust material cannot follow readiness.
65        let _ = Server::builder().tls_config(config.clone())?;
66        Ok(Self(config))
67    }
68}
69
70/// Privacy-safe HC/2 listener accounting used by readiness/drain proofs.
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72pub struct Hc2AccountingSnapshot {
73    /// Currently open authenticated gRPC streams.
74    pub active_connections: u64,
75    /// Active subscriptions across all streams.
76    pub active_subscriptions: u64,
77    /// Active fenced sessions across all streams.
78    pub active_sessions: u64,
79    /// Invocations currently executing through shared dispatch.
80    pub pending_invocations: u64,
81    /// Frames rejected before dispatch.
82    pub rejected_frames: u64,
83}
84
85#[derive(Debug, Default)]
86struct Accounting {
87    active_connections: AtomicU64,
88    active_subscriptions: AtomicU64,
89    active_sessions: AtomicU64,
90    pending_invocations: AtomicU64,
91    rejected_frames: AtomicU64,
92    next_session: AtomicU64,
93    next_watermark: AtomicU64,
94}
95
96/// Production gRPC service. Construction requires existing verified dispatch state.
97#[derive(Debug, Clone)]
98pub struct Hc2ClientPlaneService {
99    state: Arc<ClientSurfaceState>,
100    cluster_id: Arc<str>,
101    accounting: Arc<Accounting>,
102}
103
104impl Hc2ClientPlaneService {
105    /// Construct a service over the shared HC/1/RESP dispatch state.
106    pub fn new(state: Arc<ClientSurfaceState>, cluster_id: impl Into<String>) -> Self {
107        Self {
108            state,
109            cluster_id: Arc::from(cluster_id.into()),
110            accounting: Arc::new(Accounting::default()),
111        }
112    }
113
114    /// Return bounded, secret-free listener accounting.
115    pub fn accounting(&self) -> Hc2AccountingSnapshot {
116        Hc2AccountingSnapshot {
117            active_connections: self.accounting.active_connections.load(Ordering::Acquire),
118            active_subscriptions: self.accounting.active_subscriptions.load(Ordering::Acquire),
119            active_sessions: self.accounting.active_sessions.load(Ordering::Acquire),
120            pending_invocations: self.accounting.pending_invocations.load(Ordering::Acquire),
121            rejected_frames: self.accounting.rejected_frames.load(Ordering::Acquire),
122        }
123    }
124
125    /// Render the production listener's bounded aggregate accounting for the
126    /// existing internal Prometheus endpoint.
127    ///
128    /// The only label has a closed domain. Peer identities, tenant IDs,
129    /// authorities, certificate material, keys, and values are never exposed.
130    pub fn prometheus_metrics(&self) -> String {
131        let snapshot = self.accounting();
132        format!(
133            concat!(
134                "# TYPE hydracache_hc2_connections gauge\n",
135                "hydracache_hc2_connections{{transport=\"grpc_bidirectional\"}} {}\n",
136                "# TYPE hydracache_hc2_pending_invocations gauge\n",
137                "hydracache_hc2_pending_invocations{{transport=\"grpc_bidirectional\"}} {}\n",
138                "# TYPE hydracache_hc2_subscriptions gauge\n",
139                "hydracache_hc2_subscriptions{{transport=\"grpc_bidirectional\"}} {}\n",
140                "# TYPE hydracache_hc2_sessions gauge\n",
141                "hydracache_hc2_sessions{{transport=\"grpc_bidirectional\"}} {}\n",
142                "# TYPE hydracache_hc2_rejected_frames_total counter\n",
143                "hydracache_hc2_rejected_frames_total{{transport=\"grpc_bidirectional\"}} {}\n",
144            ),
145            snapshot.active_connections,
146            snapshot.pending_invocations,
147            snapshot.active_subscriptions,
148            snapshot.active_sessions,
149            snapshot.rejected_frames,
150        )
151    }
152}
153
154type ResponseStream = Pin<Box<dyn Stream<Item = Result<ServerEnvelope, Status>> + Send + 'static>>;
155
156#[tonic::async_trait]
157impl ClientPlaneAlpha for Hc2ClientPlaneService {
158    type OpenStream = ResponseStream;
159
160    async fn open(
161        &self,
162        request: Request<Streaming<ClientEnvelope>>,
163    ) -> Result<Response<Self::OpenStream>, Status> {
164        let peer_id = verified_peer_id(&request)?;
165        let mut inbound = request.into_inner();
166        let (outbound, receiver) = mpsc::channel(self.state.limits().max_streams_per_connection);
167        let service = self.clone();
168        tokio::spawn(async move {
169            let guard = ConnectionGuard::new(Arc::clone(&service.accounting));
170            if let Err(status) = serve_connection(&service, &peer_id, &mut inbound, &outbound).await
171            {
172                let _ = outbound.send(Err(status)).await;
173            }
174            drop(guard);
175        });
176        Ok(Response::new(Box::pin(ReceiverStream::new(receiver))))
177    }
178}
179
180struct ConnectionGuard {
181    accounting: Arc<Accounting>,
182}
183
184struct StreamResourceGuard {
185    accounting: Arc<Accounting>,
186    subscriptions: u64,
187    sessions: u64,
188}
189
190impl StreamResourceGuard {
191    fn new(accounting: Arc<Accounting>) -> Self {
192        Self {
193            accounting,
194            subscriptions: 0,
195            sessions: 0,
196        }
197    }
198
199    fn add_subscription(&mut self) {
200        self.subscriptions += 1;
201        self.accounting
202            .active_subscriptions
203            .fetch_add(1, Ordering::AcqRel);
204    }
205
206    fn remove_subscription(&mut self) {
207        self.subscriptions = self.subscriptions.saturating_sub(1);
208        self.accounting
209            .active_subscriptions
210            .fetch_sub(1, Ordering::AcqRel);
211    }
212
213    fn add_session(&mut self) {
214        self.sessions += 1;
215        self.accounting
216            .active_sessions
217            .fetch_add(1, Ordering::AcqRel);
218    }
219
220    fn remove_session(&mut self) {
221        self.sessions = self.sessions.saturating_sub(1);
222        self.accounting
223            .active_sessions
224            .fetch_sub(1, Ordering::AcqRel);
225    }
226}
227
228impl Drop for StreamResourceGuard {
229    fn drop(&mut self) {
230        self.accounting
231            .active_subscriptions
232            .fetch_sub(self.subscriptions, Ordering::AcqRel);
233        self.accounting
234            .active_sessions
235            .fetch_sub(self.sessions, Ordering::AcqRel);
236    }
237}
238
239impl ConnectionGuard {
240    fn new(accounting: Arc<Accounting>) -> Self {
241        accounting.active_connections.fetch_add(1, Ordering::AcqRel);
242        Self { accounting }
243    }
244}
245
246impl Drop for ConnectionGuard {
247    fn drop(&mut self) {
248        self.accounting
249            .active_connections
250            .fetch_sub(1, Ordering::AcqRel);
251    }
252}
253
254#[derive(Clone, Copy)]
255struct SessionIdentity {
256    protocol_generation: u32,
257    connection_generation: u64,
258}
259
260async fn serve_connection(
261    service: &Hc2ClientPlaneService,
262    peer_id: &str,
263    inbound: &mut Streaming<ClientEnvelope>,
264    outbound: &mpsc::Sender<Result<ServerEnvelope, Status>>,
265) -> Result<(), Status> {
266    let first = inbound
267        .message()
268        .await?
269        .ok_or_else(|| Status::unauthenticated("HC/2 handshake is required"))?;
270    let Some(client_envelope::Message::Handshake(handshake)) = first.message else {
271        reject(&service.accounting);
272        return Err(Status::failed_precondition("HC/2 handshake must be first"));
273    };
274    if !is_supported_hc2_generation(first.generation)
275        || handshake.generation != first.generation
276        || first.connection_generation == 0
277        || first.connection_generation != handshake.connection_generation
278    {
279        reject(&service.accounting);
280        return Err(Status::failed_precondition("unsupported HC/2 generation"));
281    }
282    let identity = SessionIdentity {
283        protocol_generation: first.generation,
284        connection_generation: first.connection_generation,
285    };
286    outbound
287        .send(Ok(server_envelope(
288            identity,
289            first.correlation_id,
290            server_envelope::Message::Handshake(HandshakeAck {
291                generation: identity.protocol_generation,
292                cluster_id: service.cluster_id.to_string(),
293                accepted: handshake.requested,
294                topology_epoch: 1,
295                connection_generation: identity.connection_generation,
296                minimum_generation: HC2_MINIMUM_GENERATION,
297                preferred_generation: HC2_GENERATION,
298                negotiated_generation_deprecated: identity.protocol_generation < HC2_GENERATION,
299            }),
300        )))
301        .await
302        .map_err(|_| Status::cancelled("HC/2 response stream closed"))?;
303
304    let mut subscriptions = BTreeMap::<u64, (Bytes, u64)>::new();
305    let mut sessions = BTreeMap::<Bytes, u64>::new();
306    let mut resources = StreamResourceGuard::new(Arc::clone(&service.accounting));
307    while let Some(envelope) = inbound.message().await? {
308        if envelope.generation != identity.protocol_generation
309            || envelope.connection_generation != identity.connection_generation
310            || envelope.correlation_id == 0
311        {
312            reject(&service.accounting);
313            return Err(Status::failed_precondition(
314                "invalid HC/2 envelope identity",
315            ));
316        }
317        match envelope.message {
318            Some(client_envelope::Message::Invocation(invocation)) => {
319                let event = mutation_event(&invocation);
320                let response =
321                    dispatch_invocation(service, peer_id, envelope.correlation_id, invocation);
322                let mutation_applied = matches!(
323                    response.result.as_ref(),
324                    Some(invocation_response::Result::Mutation(MutationResult {
325                        applied: true
326                    }))
327                );
328                outbound
329                    .send(Ok(server_envelope(
330                        identity,
331                        envelope.correlation_id,
332                        server_envelope::Message::Invocation(response),
333                    )))
334                    .await
335                    .map_err(|_| Status::cancelled("HC/2 response stream closed"))?;
336                if let Some((key, value, removed)) = event.filter(|_| mutation_applied) {
337                    emit_matching_events(
338                        service,
339                        identity,
340                        &key,
341                        &value,
342                        removed,
343                        &mut subscriptions,
344                        outbound,
345                    )
346                    .await?;
347                }
348            }
349            Some(client_envelope::Message::Subscribe(subscribe)) => {
350                if subscriptions.contains_key(&subscribe.subscription_id)
351                    || subscriptions.len() >= service.state.limits().max_streams_per_connection
352                {
353                    reject(&service.accounting);
354                    return Err(Status::resource_exhausted(
355                        "HC/2 subscription bound exceeded",
356                    ));
357                }
358                subscriptions.insert(
359                    subscribe.subscription_id,
360                    (subscribe.key_prefix, subscribe.resume_watermark),
361                );
362                resources.add_subscription();
363                outbound
364                    .send(Ok(server_envelope(
365                        identity,
366                        envelope.correlation_id,
367                        server_envelope::Message::Subscribed(SubscriptionAck {
368                            subscription_id: subscribe.subscription_id,
369                            watermark: subscribe.resume_watermark,
370                        }),
371                    )))
372                    .await
373                    .map_err(|_| Status::cancelled("HC/2 response stream closed"))?;
374            }
375            Some(client_envelope::Message::Unsubscribe(unsubscribe)) => {
376                if subscriptions.remove(&unsubscribe.subscription_id).is_some() {
377                    resources.remove_subscription();
378                }
379            }
380            Some(client_envelope::Message::SessionOpen(_)) => {
381                let sequence = service
382                    .accounting
383                    .next_session
384                    .fetch_add(1, Ordering::AcqRel)
385                    + 1;
386                let session_id = Bytes::copy_from_slice(&sequence.to_be_bytes());
387                sessions.insert(session_id.clone(), 1);
388                resources.add_session();
389                outbound
390                    .send(Ok(server_envelope(
391                        identity,
392                        envelope.correlation_id,
393                        server_envelope::Message::SessionHeartbeat(SessionHeartbeat {
394                            session_id,
395                            fence: 1,
396                        }),
397                    )))
398                    .await
399                    .map_err(|_| Status::cancelled("HC/2 response stream closed"))?;
400            }
401            Some(client_envelope::Message::SessionHeartbeat(heartbeat)) => {
402                let message = if sessions.get(&heartbeat.session_id) == Some(&heartbeat.fence) {
403                    server_envelope::Message::SessionHeartbeat(heartbeat)
404                } else {
405                    server_envelope::Message::SessionLost(SessionLost {
406                        session_id: heartbeat.session_id,
407                        last_fence: heartbeat.fence,
408                    })
409                };
410                outbound
411                    .send(Ok(server_envelope(
412                        identity,
413                        envelope.correlation_id,
414                        message,
415                    )))
416                    .await
417                    .map_err(|_| Status::cancelled("HC/2 response stream closed"))?;
418            }
419            Some(client_envelope::Message::SessionClose(close)) => {
420                if sessions.remove(&close.session_id).is_some() {
421                    resources.remove_session();
422                }
423            }
424            Some(client_envelope::Message::Cancel(_)) => {}
425            Some(client_envelope::Message::Handshake(_)) | None => {
426                reject(&service.accounting);
427                return Err(Status::failed_precondition("unexpected HC/2 frame"));
428            }
429        }
430    }
431    Ok(())
432}
433
434fn dispatch_invocation(
435    service: &Hc2ClientPlaneService,
436    peer_id: &str,
437    correlation_id: u64,
438    invocation: InvocationRequest,
439) -> InvocationResponse {
440    service
441        .accounting
442        .pending_invocations
443        .fetch_add(1, Ordering::AcqRel);
444    let response = dispatch_invocation_inner(service, peer_id, correlation_id, invocation);
445    service
446        .accounting
447        .pending_invocations
448        .fetch_sub(1, Ordering::AcqRel);
449    response
450}
451
452fn dispatch_invocation_inner(
453    service: &Hc2ClientPlaneService,
454    peer_id: &str,
455    correlation_id: u64,
456    invocation: InvocationRequest,
457) -> InvocationResponse {
458    let Some(meta) = invocation.meta else {
459        return wire_error(
460            StableErrorCode::StableErrorInvalidRequest,
461            "request metadata is required",
462        );
463    };
464    if meta.tenant.trim().is_empty() {
465        return wire_error(
466            StableErrorCode::StableErrorUnauthenticated,
467            "tenant is required",
468        );
469    }
470    if meta.deadline_unix_ms != 0 && meta.deadline_unix_ms <= unix_time_ms() {
471        return wire_error(
472            StableErrorCode::StableErrorDeadlineExceeded,
473            "deadline expired",
474        );
475    }
476    let identity = match ClientIdentity::new(peer_id, meta.tenant.clone()) {
477        Ok(identity) => identity,
478        Err(_) => {
479            return wire_error(
480                StableErrorCode::StableErrorUnauthenticated,
481                "invalid identity",
482            )
483        }
484    };
485    let Some(operation) = invocation.operation else {
486        return wire_error(
487            StableErrorCode::StableErrorInvalidRequest,
488            "operation is required",
489        );
490    };
491    if let invocation_request::Operation::Batch(batch) = operation {
492        let items = batch
493            .items
494            .into_iter()
495            .map(|item| {
496                let nested = InvocationRequest {
497                    meta: Some(meta.clone()),
498                    operation: item.operation.map(batch_operation),
499                };
500                dispatch_invocation_inner(service, peer_id, correlation_id, nested)
501            })
502            .collect();
503        return wire_success(Some(invocation_response::Result::Batch(BatchResult {
504            items,
505        })));
506    }
507    let request = match to_client_request(operation) {
508        Ok(request) => request,
509        Err(detail) => return wire_error(StableErrorCode::StableErrorInvalidRequest, detail),
510    };
511    let mut envelope = ClientRequestEnvelope::new(correlation_id.to_string(), request);
512    if meta.deadline_unix_ms != 0 {
513        envelope = envelope.with_deadline_ms(meta.deadline_unix_ms);
514    }
515    if !meta.idempotency_key.is_empty() {
516        envelope = envelope.with_idempotency_key(hex(&meta.idempotency_key));
517    }
518    from_dispatch_response(service.state.dispatch_verified_request(&identity, envelope))
519}
520
521fn batch_operation(
522    operation: hydracache_client_hc2::wire::batch_item::Operation,
523) -> invocation_request::Operation {
524    match operation {
525        hydracache_client_hc2::wire::batch_item::Operation::Get(value) => {
526            invocation_request::Operation::Get(value)
527        }
528        hydracache_client_hc2::wire::batch_item::Operation::Put(value) => {
529            invocation_request::Operation::Put(value)
530        }
531        hydracache_client_hc2::wire::batch_item::Operation::Delete(value) => {
532            invocation_request::Operation::Delete(value)
533        }
534        hydracache_client_hc2::wire::batch_item::Operation::CompareAndSet(value) => {
535            invocation_request::Operation::CompareAndSet(value)
536        }
537    }
538}
539
540fn to_client_request(
541    operation: invocation_request::Operation,
542) -> Result<ClientRequest, &'static str> {
543    let ns = Namespace::new("hc2").map_err(|_| "invalid namespace")?;
544    Ok(match operation {
545        invocation_request::Operation::Get(value) => ClientRequest::Get {
546            ns,
547            key: structured_key(&value.key)?,
548        },
549        invocation_request::Operation::Put(value) => ClientRequest::Put {
550            ns,
551            key: structured_key(&value.key)?,
552            value: value.value.to_vec(),
553            ttl_ms: (value.ttl_ms != 0).then_some(value.ttl_ms),
554            dimensions: Vec::new(),
555        },
556        invocation_request::Operation::Delete(value) => ClientRequest::Invalidate {
557            ns,
558            key: structured_key(&value.key)?,
559        },
560        invocation_request::Operation::CompareAndSet(value) => ClientRequest::CompareAndSet {
561            ns,
562            key: structured_key(&value.key)?,
563            expected: CasExpectation::Exact(value.expected.to_vec()),
564            new_value: value.replacement.to_vec(),
565            level: LockConsistency::Quorum,
566        },
567        invocation_request::Operation::TryLock(value) => ClientRequest::TryLock {
568            ns,
569            key: structured_key(&value.key)?,
570            lease_ms: value.lease_ms,
571            wait_ms: value.wait_ms,
572            level: LockConsistency::Quorum,
573        },
574        invocation_request::Operation::Unlock(value) => ClientRequest::Unlock {
575            ns,
576            key: structured_key(&value.key)?,
577            fence: value.fence,
578        },
579        invocation_request::Operation::RenewLock(value) => ClientRequest::RenewLockLease {
580            ns,
581            key: structured_key(&value.key)?,
582            fence: value.fence,
583            lease_ms: value.lease_ms,
584        },
585        invocation_request::Operation::LockOwnership(value) => ClientRequest::GetLockOwnership {
586            ns,
587            key: structured_key(&value.key)?,
588        },
589        invocation_request::Operation::RemoveIfValue(value) => ClientRequest::RemoveIfValue {
590            ns,
591            key: structured_key(&value.key)?,
592            expected: value.expected.to_vec(),
593            level: LockConsistency::Quorum,
594        },
595        invocation_request::Operation::Batch(_) => return Err("nested batch is not allowed"),
596    })
597}
598
599fn from_dispatch_response(response: ClientResponseEnvelope) -> InvocationResponse {
600    match response.result {
601        Ok(ClientResponse::Value { value }) => {
602            wire_success(Some(invocation_response::Result::Value(ValueResult {
603                found: value.is_some(),
604                value: value.unwrap_or_default().into(),
605                expires_at_unix_ms: 0,
606            })))
607        }
608        Ok(ClientResponse::Stored | ClientResponse::Invalidated) => wire_success(Some(
609            invocation_response::Result::Mutation(MutationResult { applied: true }),
610        )),
611        Ok(ClientResponse::CasApplied { .. }) => wire_success(Some(
612            invocation_response::Result::Mutation(MutationResult { applied: true }),
613        )),
614        Ok(ClientResponse::CasMismatch { .. }) => wire_success(Some(
615            invocation_response::Result::Mutation(MutationResult { applied: false }),
616        )),
617        Ok(ClientResponse::LockAcquired { fence }) => {
618            wire_success(Some(invocation_response::Result::Lock(LockResult {
619                acquired: true,
620                fence,
621            })))
622        }
623        Ok(ClientResponse::LockBusy) => {
624            wire_success(Some(invocation_response::Result::Lock(LockResult {
625                acquired: false,
626                fence: 0,
627            })))
628        }
629        Ok(ClientResponse::LockReleased | ClientResponse::LockLeaseRenewed) => wire_success(Some(
630            invocation_response::Result::Mutation(MutationResult { applied: true }),
631        )),
632        Ok(ClientResponse::LockOwnership { fence, locked }) => wire_success(Some(
633            invocation_response::Result::LockOwnership(LockOwnershipResult {
634                locked,
635                fence: fence.unwrap_or_default(),
636            }),
637        )),
638        Ok(_) => wire_error(
639            StableErrorCode::StableErrorUnsupported,
640            "unsupported HC/2 result",
641        ),
642        Err(error) => wire_error(map_client_error(error.code), "request rejected"),
643    }
644}
645
646fn map_client_error(code: ClientErrorCode) -> StableErrorCode {
647    match code {
648        ClientErrorCode::Unauthenticated => StableErrorCode::StableErrorUnauthenticated,
649        ClientErrorCode::Unauthorized | ClientErrorCode::ResidencyDenied => {
650            StableErrorCode::StableErrorUnauthorized
651        }
652        ClientErrorCode::TenantQuota | ClientErrorCode::RateLimited | ClientErrorCode::TooLarge => {
653            StableErrorCode::StableErrorQuotaExceeded
654        }
655        ClientErrorCode::DeadlineExceeded => StableErrorCode::StableErrorDeadlineExceeded,
656        ClientErrorCode::Conflict => StableErrorCode::StableErrorConflict,
657        ClientErrorCode::BackendUnavailable => StableErrorCode::StableErrorUnavailable,
658        ClientErrorCode::IncompatibleVersion | ClientErrorCode::MalformedFrame => {
659            StableErrorCode::StableErrorInvalidRequest
660        }
661    }
662}
663
664fn wire_success(result: Option<invocation_response::Result>) -> InvocationResponse {
665    InvocationResponse {
666        meta: Some(ResponseMeta {
667            error: StableErrorCode::StableErrorUnspecified as i32,
668            retry: hydracache_client_hc2::wire::RetryDirective::Never as i32,
669            safe_detail: String::new(),
670            topology_epoch: 1,
671        }),
672        result,
673    }
674}
675
676fn wire_error(code: StableErrorCode, detail: &'static str) -> InvocationResponse {
677    InvocationResponse {
678        meta: Some(ResponseMeta {
679            error: code as i32,
680            retry: hydracache_client_hc2::wire::RetryDirective::Never as i32,
681            safe_detail: detail.to_owned(),
682            topology_epoch: 1,
683        }),
684        result: None,
685    }
686}
687
688async fn emit_matching_events(
689    service: &Hc2ClientPlaneService,
690    identity: SessionIdentity,
691    key: &[u8],
692    value: &[u8],
693    removed: bool,
694    subscriptions: &mut BTreeMap<u64, (Bytes, u64)>,
695    outbound: &mpsc::Sender<Result<ServerEnvelope, Status>>,
696) -> Result<(), Status> {
697    for (subscription_id, (prefix, watermark)) in subscriptions.iter_mut() {
698        if key.starts_with(prefix) {
699            let next = service
700                .accounting
701                .next_watermark
702                .fetch_add(1, Ordering::AcqRel)
703                + 1;
704            if *watermark != 0 && next > watermark.saturating_add(1) {
705                outbound
706                    .send(Ok(server_envelope(
707                        identity,
708                        0,
709                        server_envelope::Message::Gap(EventGap {
710                            subscription_id: *subscription_id,
711                            after_watermark: *watermark,
712                        }),
713                    )))
714                    .await
715                    .map_err(|_| Status::cancelled("HC/2 response stream closed"))?;
716            }
717            *watermark = next;
718            outbound
719                .send(Ok(server_envelope(
720                    identity,
721                    0,
722                    server_envelope::Message::Event(CacheEvent {
723                        subscription_id: *subscription_id,
724                        watermark: next,
725                        key: Bytes::copy_from_slice(key),
726                        value: Bytes::copy_from_slice(value),
727                        removed,
728                    }),
729                )))
730                .await
731                .map_err(|_| Status::cancelled("HC/2 response stream closed"))?;
732        }
733    }
734    Ok(())
735}
736
737fn mutation_event(invocation: &InvocationRequest) -> Option<(Bytes, Bytes, bool)> {
738    match invocation.operation.as_ref()? {
739        invocation_request::Operation::Put(value) => {
740            Some((value.key.clone(), value.value.clone(), false))
741        }
742        invocation_request::Operation::Delete(value) => {
743            Some((value.key.clone(), Bytes::new(), true))
744        }
745        invocation_request::Operation::CompareAndSet(value) => {
746            Some((value.key.clone(), value.replacement.clone(), false))
747        }
748        invocation_request::Operation::TryLock(_)
749        | invocation_request::Operation::Unlock(_)
750        | invocation_request::Operation::RenewLock(_)
751        | invocation_request::Operation::LockOwnership(_) => None,
752        invocation_request::Operation::RemoveIfValue(value) => {
753            Some((value.key.clone(), Bytes::new(), true))
754        }
755        _ => None,
756    }
757}
758
759fn structured_key(value: &[u8]) -> Result<StructuredKey, &'static str> {
760    if value.is_empty() {
761        return Err("key is required");
762    }
763    StructuredKey::new(vec![hex(value)]).map_err(|_| "invalid key")
764}
765
766fn server_envelope(
767    identity: SessionIdentity,
768    correlation_id: u64,
769    message: server_envelope::Message,
770) -> ServerEnvelope {
771    ServerEnvelope {
772        generation: identity.protocol_generation,
773        connection_generation: identity.connection_generation,
774        correlation_id,
775        message: Some(message),
776    }
777}
778
779fn reject(accounting: &Accounting) {
780    accounting.rejected_frames.fetch_add(1, Ordering::Relaxed);
781}
782
783fn verified_peer_id(request: &Request<Streaming<ClientEnvelope>>) -> Result<String, Status> {
784    let connect = request
785        .extensions()
786        .get::<TlsConnectInfo<TcpConnectInfo>>()
787        .ok_or_else(|| Status::unauthenticated("verified mTLS peer is required"))?;
788    let certificates = connect
789        .peer_certs()
790        .ok_or_else(|| Status::unauthenticated("client certificate is required"))?;
791    let certificate = certificates
792        .first()
793        .ok_or_else(|| Status::unauthenticated("client certificate is required"))?;
794    Ok(format!(
795        "mtls-sha256:{}",
796        hex(&Sha256::digest(certificate.as_ref()))
797    ))
798}
799
800fn hex(bytes: &[u8]) -> String {
801    use std::fmt::Write;
802    let mut encoded = String::with_capacity(bytes.len() * 2);
803    for byte in bytes {
804        let _ = write!(&mut encoded, "{byte:02x}");
805    }
806    encoded
807}
808
809fn unix_time_ms() -> u64 {
810    std::time::SystemTime::now()
811        .duration_since(std::time::UNIX_EPOCH)
812        .unwrap_or_default()
813        .as_millis()
814        .try_into()
815        .unwrap_or(u64::MAX)
816}
817
818/// Serve the already-bound production listener with mandatory client certificates.
819pub async fn serve_hc2_listener(
820    listener: TcpListener,
821    service: Hc2ClientPlaneService,
822    tls: Hc2ListenerTls,
823    mut shutdown: watch::Receiver<bool>,
824) -> Result<(), Hc2ServeError> {
825    Server::builder()
826        .tls_config(tls.0)?
827        .add_service(
828            ClientPlaneAlphaServer::new(service)
829                .max_decoding_message_size(8 * 1024 * 1024)
830                .max_encoding_message_size(8 * 1024 * 1024),
831        )
832        .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async move {
833            while shutdown.changed().await.is_ok() {
834                if *shutdown.borrow() {
835                    break;
836                }
837            }
838        })
839        .await?;
840    Ok(())
841}
842
843/// Fail-loud HC/2 listener startup/runtime errors.
844#[derive(Debug, Error)]
845pub enum Hc2ServeError {
846    /// HC/2 was enabled without complete TLS paths.
847    #[error("HC/2 requires certificate, key, and client CA paths")]
848    MissingTls,
849    /// TLS material could not be read.
850    #[error("failed to read HC/2 TLS material: {0}")]
851    Io(#[from] std::io::Error),
852    /// Tonic rejected TLS or serving configuration.
853    #[error("HC/2 gRPC serving failed: {0}")]
854    Transport(#[from] tonic::transport::Error),
855}
856
857#[cfg(test)]
858mod tests {
859    use super::*;
860    use std::path::PathBuf;
861    use std::time::Duration;
862
863    use hydracache_client_hc2::wire::{
864        GetRequest, LockOwnershipRequest, PutRequest, RemoveIfValueRequest, RequestMeta,
865        TryLockRequest, UnlockRequest,
866    };
867    use hydracache_client_hc2::{
868        ClientConfig, ErrorCode, GrpcMtlsAdapter, GrpcMtlsConfig, Hc2Client, SubscriptionEvent,
869    };
870    use rcgen::{
871        BasicConstraints, CertificateParams, CertifiedIssuer, ExtendedKeyUsagePurpose, IsCa,
872        KeyPair,
873    };
874    use tokio::io::{AsyncReadExt, AsyncWriteExt};
875
876    fn service() -> Hc2ClientPlaneService {
877        Hc2ClientPlaneService::new(
878            Arc::new(ClientSurfaceState::new(Default::default()).unwrap()),
879            "test-cluster",
880        )
881    }
882
883    fn meta() -> RequestMeta {
884        RequestMeta {
885            deadline_unix_ms: 0,
886            idempotency_key: Bytes::new(),
887            tenant: "tenant-a".to_owned(),
888            topology_epoch: 1,
889        }
890    }
891
892    #[test]
893    fn operations_use_the_existing_verified_dispatch_state() {
894        let service = service();
895        let put = dispatch_invocation(
896            &service,
897            "verified-peer",
898            1,
899            InvocationRequest {
900                meta: Some(meta()),
901                operation: Some(invocation_request::Operation::Put(PutRequest {
902                    key: Bytes::from_static(b"key"),
903                    value: Bytes::from_static(b"value"),
904                    ttl_ms: 0,
905                })),
906            },
907        );
908        assert!(matches!(
909            put.result,
910            Some(invocation_response::Result::Mutation(MutationResult {
911                applied: true
912            }))
913        ));
914        let get = dispatch_invocation(
915            &service,
916            "verified-peer",
917            2,
918            InvocationRequest {
919                meta: Some(meta()),
920                operation: Some(invocation_request::Operation::Get(GetRequest {
921                    key: Bytes::from_static(b"key"),
922                })),
923            },
924        );
925        assert!(matches!(
926            get.result,
927            Some(invocation_response::Result::Value(ValueResult { found: true, ref value, .. }))
928                if value.as_ref() == b"value"
929        ));
930        assert_eq!(service.state.dispatch_attempts(), 2);
931        assert_eq!(service.state.state_mutations(), 1);
932        assert_eq!(service.accounting(), Hc2AccountingSnapshot::default());
933    }
934
935    #[test]
936    fn conditional_remove_and_fenced_lock_use_the_shared_dispatch() {
937        let service = service();
938        let invoke = |correlation_id, operation| {
939            dispatch_invocation(
940                &service,
941                "verified-peer",
942                correlation_id,
943                InvocationRequest {
944                    meta: Some(meta()),
945                    operation: Some(operation),
946                },
947            )
948        };
949        invoke(
950            1,
951            invocation_request::Operation::Put(PutRequest {
952                key: Bytes::from_static(b"key"),
953                value: Bytes::from_static(b"value"),
954                ttl_ms: 0,
955            }),
956        );
957        let mismatch = invoke(
958            2,
959            invocation_request::Operation::RemoveIfValue(RemoveIfValueRequest {
960                key: Bytes::from_static(b"key"),
961                expected: Bytes::from_static(b"wrong"),
962            }),
963        );
964        assert!(matches!(
965            mismatch.result,
966            Some(invocation_response::Result::Mutation(MutationResult {
967                applied: false
968            }))
969        ));
970        let removed = invoke(
971            3,
972            invocation_request::Operation::RemoveIfValue(RemoveIfValueRequest {
973                key: Bytes::from_static(b"key"),
974                expected: Bytes::from_static(b"value"),
975            }),
976        );
977        assert!(matches!(
978            removed.result,
979            Some(invocation_response::Result::Mutation(MutationResult {
980                applied: true
981            }))
982        ));
983
984        let acquired = invoke(
985            4,
986            invocation_request::Operation::TryLock(TryLockRequest {
987                key: Bytes::from_static(b"lock"),
988                lease_ms: 10_000,
989                wait_ms: 0,
990            }),
991        );
992        let fence = match acquired.result {
993            Some(invocation_response::Result::Lock(LockResult {
994                acquired: true,
995                fence,
996            })) => fence,
997            other => panic!("expected acquired lock, got {other:?}"),
998        };
999        assert_ne!(fence, 0);
1000        let ownership = invoke(
1001            5,
1002            invocation_request::Operation::LockOwnership(LockOwnershipRequest {
1003                key: Bytes::from_static(b"lock"),
1004            }),
1005        );
1006        assert!(matches!(
1007            ownership.result,
1008            Some(invocation_response::Result::LockOwnership(
1009                LockOwnershipResult {
1010                    locked: true,
1011                    fence: observed,
1012                }
1013            )) if observed == fence
1014        ));
1015        let released = invoke(
1016            6,
1017            invocation_request::Operation::Unlock(UnlockRequest {
1018                key: Bytes::from_static(b"lock"),
1019                fence,
1020            }),
1021        );
1022        assert!(matches!(
1023            released.result,
1024            Some(invocation_response::Result::Mutation(MutationResult {
1025                applied: true
1026            }))
1027        ));
1028    }
1029
1030    #[test]
1031    fn prometheus_accounting_has_only_bounded_privacy_safe_labels() {
1032        let service = service();
1033        service
1034            .accounting
1035            .active_connections
1036            .store(2, Ordering::Release);
1037        service
1038            .accounting
1039            .active_subscriptions
1040            .store(3, Ordering::Release);
1041        service
1042            .accounting
1043            .active_sessions
1044            .store(4, Ordering::Release);
1045        service
1046            .accounting
1047            .pending_invocations
1048            .store(5, Ordering::Release);
1049        service
1050            .accounting
1051            .rejected_frames
1052            .store(6, Ordering::Release);
1053
1054        let metrics = service.prometheus_metrics();
1055        assert!(metrics.contains("hydracache_hc2_connections{transport=\"grpc_bidirectional\"} 2"));
1056        assert!(metrics
1057            .contains("hydracache_hc2_pending_invocations{transport=\"grpc_bidirectional\"} 5"));
1058        assert!(metrics
1059            .contains("hydracache_hc2_rejected_frames_total{transport=\"grpc_bidirectional\"} 6"));
1060        for forbidden in [
1061            "tenant-a",
1062            "verified-peer",
1063            "test-cluster",
1064            "endpoint",
1065            "authority",
1066            "certificate",
1067        ] {
1068            assert!(!metrics.contains(forbidden));
1069        }
1070    }
1071
1072    #[test]
1073    fn missing_tenant_and_expired_deadline_fail_before_dispatch() {
1074        let service = service();
1075        let mut missing = meta();
1076        missing.tenant.clear();
1077        let response = dispatch_invocation(
1078            &service,
1079            "verified-peer",
1080            1,
1081            InvocationRequest {
1082                meta: Some(missing),
1083                operation: Some(invocation_request::Operation::Get(GetRequest {
1084                    key: Bytes::from_static(b"key"),
1085                })),
1086            },
1087        );
1088        assert_eq!(
1089            response.meta.unwrap().error,
1090            StableErrorCode::StableErrorUnauthenticated as i32
1091        );
1092        let mut expired = meta();
1093        expired.deadline_unix_ms = 1;
1094        let response = dispatch_invocation(
1095            &service,
1096            "verified-peer",
1097            2,
1098            InvocationRequest {
1099                meta: Some(expired),
1100                operation: Some(invocation_request::Operation::Get(GetRequest {
1101                    key: Bytes::from_static(b"key"),
1102                })),
1103            },
1104        );
1105        assert_eq!(
1106            response.meta.unwrap().error,
1107            StableErrorCode::StableErrorDeadlineExceeded as i32
1108        );
1109        assert_eq!(service.state.dispatch_attempts(), 0);
1110    }
1111
1112    struct TestPki {
1113        ca: String,
1114        server_cert: String,
1115        server_key: String,
1116        client_cert: String,
1117        client_key: String,
1118    }
1119
1120    fn test_pki() -> TestPki {
1121        let mut ca_params = CertificateParams::new(Vec::<String>::new()).unwrap();
1122        ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
1123        let ca = CertifiedIssuer::self_signed(ca_params, KeyPair::generate().unwrap()).unwrap();
1124
1125        let server_key = KeyPair::generate().unwrap();
1126        let mut server_params = CertificateParams::new(vec!["localhost".to_owned()]).unwrap();
1127        server_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
1128        let server_cert = server_params.signed_by(&server_key, &ca).unwrap();
1129
1130        let client_key = KeyPair::generate().unwrap();
1131        let mut client_params = CertificateParams::new(vec!["hc2-client".to_owned()]).unwrap();
1132        client_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
1133        let client_cert = client_params.signed_by(&client_key, &ca).unwrap();
1134        TestPki {
1135            ca: ca.pem(),
1136            server_cert: server_cert.pem(),
1137            server_key: server_key.serialize_pem(),
1138            client_cert: client_cert.pem(),
1139            client_key: client_key.serialize_pem(),
1140        }
1141    }
1142
1143    fn write_tls(root: &PathBuf, pki: &TestPki) -> TlsConfig {
1144        std::fs::create_dir_all(root).unwrap();
1145        let cert = root.join("server.pem");
1146        let key = root.join("server.key");
1147        let ca = root.join("clients.pem");
1148        std::fs::write(&cert, &pki.server_cert).unwrap();
1149        std::fs::write(&key, &pki.server_key).unwrap();
1150        std::fs::write(&ca, &pki.ca).unwrap();
1151        TlsConfig {
1152            enabled: true,
1153            cert_path: Some(cert),
1154            key_path: Some(key),
1155            ca_path: Some(ca),
1156            acknowledge_insecure: false,
1157        }
1158    }
1159
1160    fn adapter(addr: std::net::SocketAddr, server: &TestPki, client: &TestPki) -> GrpcMtlsAdapter {
1161        GrpcMtlsAdapter::new(
1162            GrpcMtlsConfig::new(
1163                format!("https://{addr}"),
1164                "localhost",
1165                server.ca.as_bytes(),
1166                client.client_cert.as_bytes(),
1167                client.client_key.as_bytes(),
1168            )
1169            .unwrap(),
1170        )
1171    }
1172
1173    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1174    async fn real_socket_requires_mtls_dispatches_pushes_and_drains_to_zero() {
1175        let _ = rustls::crypto::ring::default_provider().install_default();
1176        let unique = unix_time_ms();
1177        let root = PathBuf::from(format!(
1178            "target/test-hc2-production-{}-{unique}",
1179            std::process::id()
1180        ));
1181        let trusted = test_pki();
1182        let untrusted = test_pki();
1183        let tls = Hc2ListenerTls::from_server_config(&write_tls(&root, &trusted)).unwrap();
1184        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1185        let addr = listener.local_addr().unwrap();
1186        let service = service();
1187        let observed = service.clone();
1188        let (shutdown_tx, shutdown_rx) = watch::channel(false);
1189        let serving =
1190            tokio::spawn(
1191                async move { serve_hc2_listener(listener, service, tls, shutdown_rx).await },
1192            );
1193
1194        let mut plaintext = tokio::net::TcpStream::connect(addr).await.unwrap();
1195        plaintext
1196            .write_all(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")
1197            .await
1198            .unwrap();
1199        let mut byte = [0_u8; 1];
1200        let plaintext_result =
1201            tokio::time::timeout(Duration::from_secs(1), plaintext.read(&mut byte)).await;
1202        assert!(
1203            matches!(plaintext_result, Ok(Ok(0)) | Ok(Err(_)))
1204                || matches!(plaintext_result, Ok(Ok(1))) && byte[0] == 21,
1205            "plaintext must be rejected before protocol dispatch: {plaintext_result:?}"
1206        );
1207
1208        let rejected = Hc2Client::connect(
1209            &adapter(addr, &trusted, &untrusted),
1210            ClientConfig::new("untrusted", "tenant-a"),
1211        )
1212        .await
1213        .expect_err("foreign client CA must fail closed");
1214        assert_eq!(rejected.code(), ErrorCode::Unavailable);
1215
1216        let client = Hc2Client::connect(
1217            &adapter(addr, &trusted, &trusted),
1218            ClientConfig::new("claimed-name-is-not-identity", "tenant-a"),
1219        )
1220        .await
1221        .unwrap();
1222        assert_eq!(client.cluster_id(), "test-cluster");
1223        let mut subscription = client
1224            .subscribe(Bytes::from_static(b"event/"), 0)
1225            .await
1226            .unwrap();
1227        let session = client.open_session(Duration::from_secs(2)).await.unwrap();
1228        assert_eq!(session.fence().unwrap(), 1);
1229        let active_client = client.retained_state();
1230        assert_eq!(active_client.active_subscriptions, 1);
1231        assert_eq!(active_client.active_sessions, 1);
1232        client
1233            .put(
1234                Bytes::from_static(b"event/key"),
1235                Bytes::from_static(b"value"),
1236                None,
1237                None,
1238            )
1239            .await
1240            .unwrap();
1241        let value = client
1242            .get(Bytes::from_static(b"event/key"), None)
1243            .await
1244            .unwrap()
1245            .expect("stored value");
1246        assert_eq!(value.value, Bytes::from_static(b"value"));
1247        assert!(matches!(
1248            tokio::time::timeout(Duration::from_secs(1), subscription.next())
1249                .await
1250                .unwrap(),
1251            Some(SubscriptionEvent::Event(_))
1252        ));
1253        // Tear down the transport while stream-owned resources are still live.
1254        // Server accounting must be released by the stream guard rather than
1255        // depending on cooperative unsubscribe/session-close frames.
1256        client.close();
1257        drop(subscription);
1258        drop(session);
1259        let closed_client = client.retained_state();
1260        assert!(closed_client.closed);
1261        assert_eq!(closed_client.pending_invocations, 0);
1262        assert_eq!(closed_client.pending_subscriptions, 0);
1263        assert_eq!(closed_client.active_subscriptions, 0);
1264        assert_eq!(closed_client.pending_sessions, 0);
1265        assert_eq!(closed_client.active_sessions, 0);
1266        assert_eq!(
1267            closed_client.available_invocation_permits,
1268            ClientConfig::new("limits", "tenant-a")
1269                .limits
1270                .max_pending_invocations
1271        );
1272        assert_eq!(
1273            closed_client.available_subscription_permits,
1274            ClientConfig::new("limits", "tenant-a")
1275                .limits
1276                .max_subscriptions
1277        );
1278        assert_eq!(
1279            closed_client.available_session_permits,
1280            ClientConfig::new("limits", "tenant-a").limits.max_sessions
1281        );
1282
1283        for _ in 0..50 {
1284            if observed.accounting() == Hc2AccountingSnapshot::default() {
1285                break;
1286            }
1287            tokio::time::sleep(Duration::from_millis(10)).await;
1288        }
1289        assert_eq!(observed.accounting(), Hc2AccountingSnapshot::default());
1290
1291        for cardinality in [1_usize, 10, 100] {
1292            let mut clients = Vec::with_capacity(cardinality);
1293            for index in 0..cardinality {
1294                let client = Hc2Client::connect(
1295                    &adapter(addr, &trusted, &trusted),
1296                    ClientConfig::new(
1297                        format!("retention-series-{cardinality}-{index}"),
1298                        "tenant-a",
1299                    ),
1300                )
1301                .await
1302                .unwrap();
1303                clients.push(client);
1304            }
1305            for _ in 0..100 {
1306                if observed.accounting().active_connections == cardinality as u64 {
1307                    break;
1308                }
1309                tokio::time::sleep(Duration::from_millis(10)).await;
1310            }
1311            assert_eq!(
1312                observed.accounting(),
1313                Hc2AccountingSnapshot {
1314                    active_connections: cardinality as u64,
1315                    ..Hc2AccountingSnapshot::default()
1316                }
1317            );
1318            for client in &clients {
1319                let retained = client.retained_state();
1320                assert_eq!(retained.pending_invocations, 0);
1321                assert_eq!(retained.pending_subscriptions, 0);
1322                assert_eq!(retained.active_subscriptions, 0);
1323                assert_eq!(retained.pending_sessions, 0);
1324                assert_eq!(retained.active_sessions, 0);
1325                client.close();
1326            }
1327            drop(clients);
1328            for _ in 0..100 {
1329                if observed.accounting() == Hc2AccountingSnapshot::default() {
1330                    break;
1331                }
1332                tokio::time::sleep(Duration::from_millis(10)).await;
1333            }
1334            assert_eq!(observed.accounting(), Hc2AccountingSnapshot::default());
1335        }
1336        shutdown_tx.send(true).unwrap();
1337        serving.await.unwrap().unwrap();
1338        std::fs::remove_dir_all(root).unwrap();
1339    }
1340}