1use 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#[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 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 let _ = Server::builder().tls_config(config.clone())?;
66 Ok(Self(config))
67 }
68}
69
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72pub struct Hc2AccountingSnapshot {
73 pub active_connections: u64,
75 pub active_subscriptions: u64,
77 pub active_sessions: u64,
79 pub pending_invocations: u64,
81 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#[derive(Debug, Clone)]
98pub struct Hc2ClientPlaneService {
99 state: Arc<ClientSurfaceState>,
100 cluster_id: Arc<str>,
101 accounting: Arc<Accounting>,
102}
103
104impl Hc2ClientPlaneService {
105 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 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 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
818pub 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#[derive(Debug, Error)]
845pub enum Hc2ServeError {
846 #[error("HC/2 requires certificate, key, and client CA paths")]
848 MissingTls,
849 #[error("failed to read HC/2 TLS material: {0}")]
851 Io(#[from] std::io::Error),
852 #[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 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}