1use super::*;
5
6use crate::engine::AsyncEngineContext;
7use crate::metrics::prometheus_names::work_handler;
8use crate::metrics::work_handler_perf::{
9 WORK_HANDLER_NETWORK_TRANSIT_SECONDS, WORK_HANDLER_TIME_TO_FIRST_RESPONSE_SECONDS,
10};
11use crate::pipeline::{ManyIn, RequestStream};
12use crate::protocols::maybe_error::MaybeError;
13use futures::StreamExt;
14use prometheus::{Histogram, IntCounter, IntCounterVec, IntGauge};
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17use std::time::Instant;
18use tracing::Instrument;
19use tracing::info_span;
20
21#[derive(Clone, Debug)]
23pub struct WorkHandlerMetrics {
24 pub request_counter: IntCounter,
25 pub request_duration: Histogram,
26 pub inflight_requests: IntGauge,
27 pub request_bytes: IntCounter,
28 pub response_bytes: IntCounter,
29 pub error_counter: IntCounterVec,
30 pub cancellation_total: IntCounter,
31}
32
33impl WorkHandlerMetrics {
34 pub fn new(
35 request_counter: IntCounter,
36 request_duration: Histogram,
37 inflight_requests: IntGauge,
38 request_bytes: IntCounter,
39 response_bytes: IntCounter,
40 error_counter: IntCounterVec,
41 cancellation_total: IntCounter,
42 ) -> Self {
43 Self {
44 request_counter,
45 request_duration,
46 inflight_requests,
47 request_bytes,
48 response_bytes,
49 error_counter,
50 cancellation_total,
51 }
52 }
53
54 pub fn from_endpoint(
56 endpoint: &crate::component::Endpoint,
57 metrics_labels: Option<&[(&str, &str)]>,
58 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
59 let metrics_labels = metrics_labels.unwrap_or(&[]);
60 let metrics = endpoint.metrics();
61 let request_counter = metrics.create_intcounter(
62 work_handler::REQUESTS_TOTAL,
63 "Total number of requests processed by work handler",
64 metrics_labels,
65 )?;
66
67 let request_duration_buckets = vec![
71 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 20.0, 30.0, 60.0, 120.0,
72 300.0, 600.0,
73 ];
74 let request_duration = metrics.create_histogram(
75 work_handler::REQUEST_DURATION_SECONDS,
76 "Time spent processing requests by work handler",
77 metrics_labels,
78 Some(request_duration_buckets),
79 )?;
80
81 let inflight_requests = metrics.create_intgauge(
82 work_handler::INFLIGHT_REQUESTS,
83 "Number of requests currently being processed by work handler",
84 metrics_labels,
85 )?;
86
87 let request_bytes = metrics.create_intcounter(
88 work_handler::REQUEST_BYTES_TOTAL,
89 "Total number of bytes received in requests by work handler",
90 metrics_labels,
91 )?;
92
93 let response_bytes = metrics.create_intcounter(
94 work_handler::RESPONSE_BYTES_TOTAL,
95 "Total number of bytes sent in responses by work handler",
96 metrics_labels,
97 )?;
98
99 let error_counter = metrics.create_intcountervec(
100 work_handler::ERRORS_TOTAL,
101 "Total number of errors in work handler processing",
102 &[work_handler::ERROR_TYPE_LABEL],
103 metrics_labels,
104 )?;
105
106 let cancellation_total = metrics.create_intcounter(
107 work_handler::CANCELLATION_TOTAL,
108 "Total number of requests cancelled by work handler",
109 metrics_labels,
110 )?;
111
112 Ok(Self::new(
113 request_counter,
114 request_duration,
115 inflight_requests,
116 request_bytes,
117 response_bytes,
118 error_counter,
119 cancellation_total,
120 ))
121 }
122}
123
124struct RequestMetricsGuard {
127 inflight_requests: prometheus::IntGauge,
128 request_duration: prometheus::Histogram,
129 start_time: Instant,
130 request_id: Option<String>,
131}
132
133impl Drop for RequestMetricsGuard {
134 fn drop(&mut self) {
135 self.inflight_requests.dec();
136 self.request_duration
137 .observe(self.start_time.elapsed().as_secs_f64());
138 if let Some(request_id) = &self.request_id {
139 tracing::info!(request_id = %request_id, "request completed");
140 }
141 }
142}
143
144impl<Req: PipelineIO + Sync, Resp: PipelineIO> Ingress<Req, Resp> {
145 async fn pump_response_stream<U>(
152 &self,
153 mut stream: ManyOut<U>,
154 publisher: &StreamSender,
155 payload_codec: RequestPlanePayloadCodec,
156 ) where
157 U: Data + Serialize + MaybeError + std::fmt::Debug,
158 {
159 let context = stream.context();
160
161 let mut send_complete_final = true;
163 let mut saw_error_response = false;
164 while let Some(resp) = stream.next().await {
165 tracing::trace!("Sending response: {:?}", resp);
166 let is_error = resp.err().is_some();
167 if is_error {
168 saw_error_response = true;
169 }
170 let resp_wrapper = NetworkStreamWrapper {
171 data: Some(resp),
172 complete_final: false,
173 };
174 let resp_bytes = payload_codec
175 .encode(&resp_wrapper)
176 .expect("fatal error: invalid request-plane response object");
177 if let Some(m) = self.metrics() {
178 m.response_bytes.inc_by(resp_bytes.len() as u64);
179 }
180 if (publisher.send(resp_bytes.into()).await).is_err() {
181 send_complete_final = false;
182 if context.is_stopped() {
183 tracing::warn!("Failed to publish response for stream {}", context.id());
192 } else {
193 tracing::error!("Failed to publish response for stream {}", context.id());
195 context.stop_generating();
196 }
197 if let Some(m) = self.metrics() {
200 m.error_counter
201 .with_label_values(&[work_handler::error_types::PUBLISH_RESPONSE])
202 .inc();
203 }
204 break;
205 } else if !is_error {
206 if let Some(notifier) = self.endpoint_health_check_notifier.get() {
209 notifier.notify_one();
210 }
211 }
212 }
213 if send_complete_final {
214 let resp_wrapper = NetworkStreamWrapper::<U> {
215 data: None,
216 complete_final: true,
217 };
218 let resp_bytes = payload_codec
219 .encode(&resp_wrapper)
220 .expect("fatal error: invalid request-plane response final object");
221 if let Some(m) = self.metrics() {
222 m.response_bytes.inc_by(resp_bytes.len() as u64);
223 }
224 if (publisher.send(resp_bytes.into()).await).is_err() {
225 tracing::error!(
226 "Failed to publish complete final for stream {}",
227 context.id()
228 );
229 if let Some(m) = self.metrics() {
230 m.error_counter
231 .with_label_values(&[work_handler::error_types::PUBLISH_FINAL])
232 .inc();
233 }
234 }
235 if let (false, Some(notifier)) = (
237 saw_error_response,
238 self.endpoint_health_check_notifier.get(),
239 ) {
240 notifier.notify_one();
241 }
242 }
243 }
244
245 fn decode_control_message(
256 &self,
257 payload: Bytes,
258 ) -> Result<(RequestControlMessage, Option<Bytes>), PipelineError> {
259 let msg = TwoPartCodec::default()
260 .decode_message(payload)?
261 .into_message_type();
262
263 let (header, data) = match msg {
264 TwoPartMessageType::HeaderAndData(header, data) => (header, Some(data)),
265 TwoPartMessageType::HeaderOnly(header) => (header, None),
266 _ => {
267 if let Some(m) = self.metrics() {
268 m.error_counter
269 .with_label_values(&[work_handler::error_types::INVALID_MESSAGE])
270 .inc();
271 }
272 return Err(PipelineError::Generic(String::from(
273 "Unexpected message from work queue; expected a header-only or header-and-data TwoPartMessage",
274 )));
275 }
276 };
277
278 let control_msg: RequestControlMessage =
279 serde_json::from_slice(&header).map_err(|err| {
280 if let Some(m) = self.metrics() {
281 m.error_counter
282 .with_label_values(&[work_handler::error_types::DESERIALIZATION])
283 .inc();
284 }
285 let json_str = String::from_utf8_lossy(&header);
286 PipelineError::DeserializationError(format!(
287 "Failed deserializing to RequestControlMessage. err={err}, json_str={json_str}, header_len={}",
288 header.len(),
289 ))
290 })?;
291
292 Ok((control_msg, data))
293 }
294}
295struct ParsedRequest<Req> {
300 request: Req,
301 response_connection_info: ConnectionInfo,
302 frontend_send_ts_ns: Option<u64>,
303 payload_codec: RequestPlanePayloadCodec,
304}
305
306#[async_trait]
313trait IngressDispatch: Send + Sync {
314 type Request: PipelineIO;
315
316 async fn parse_and_build_request(
317 &self,
318 payload: Bytes,
319 ) -> Result<ParsedRequest<Self::Request>, PipelineError>;
320}
321
322#[async_trait]
323impl<T, U> IngressDispatch for Ingress<SingleIn<T>, ManyOut<U>>
324where
325 T: Data + for<'de> Deserialize<'de> + std::fmt::Debug,
326 U: Data + Serialize + MaybeError + std::fmt::Debug,
327{
328 type Request = SingleIn<T>;
329
330 async fn parse_and_build_request(
331 &self,
332 payload: Bytes,
333 ) -> Result<ParsedRequest<SingleIn<T>>, PipelineError> {
334 let (control_msg, data) = self.decode_control_message(payload)?;
335
336 let data = data.ok_or_else(|| {
339 if let Some(m) = self.metrics() {
340 m.error_counter
341 .with_label_values(&[work_handler::error_types::INVALID_MESSAGE])
342 .inc();
343 }
344 PipelineError::Generic(String::from(
345 "unary engine received a header-only envelope; expected a request payload",
346 ))
347 })?;
348 let payload_codec = control_msg.payload_codec;
349 let request_t: T = payload_codec.decode(&data).map_err(|err| {
350 if let Some(m) = self.metrics() {
351 m.error_counter
352 .with_label_values(&[work_handler::error_types::DESERIALIZATION])
353 .inc();
354 }
355 PipelineError::DeserializationError(format!(
356 "Failed deserializing {} request payload: {}",
357 payload_codec.name(),
358 err
359 ))
360 })?;
361
362 tracing::trace!(
363 request_id = %control_msg.id,
364 metadata_entries = control_msg.metadata.len(),
365 "received control message"
366 );
367 tracing::trace!("received request: {:?}", request_t);
368
369 let request: context::Context<T> =
370 Context::with_id_and_metadata(request_t, control_msg.id, control_msg.metadata);
371
372 Ok(ParsedRequest {
373 request,
374 response_connection_info: control_msg.connection_info,
375 frontend_send_ts_ns: control_msg.frontend_send_ts_ns,
376 payload_codec,
377 })
378 }
379}
380
381#[async_trait]
382impl<T, U> IngressDispatch for Ingress<ManyIn<T>, ManyOut<U>>
383where
384 T: Data + for<'de> Deserialize<'de> + std::fmt::Debug,
385 U: Data + Serialize + MaybeError + std::fmt::Debug,
386{
387 type Request = ManyIn<T>;
388
389 async fn parse_and_build_request(
390 &self,
391 payload: Bytes,
392 ) -> Result<ParsedRequest<ManyIn<T>>, PipelineError> {
393 let (control_msg, data) = self.decode_control_message(payload)?;
394
395 if data.is_some() {
400 if let Some(m) = self.metrics() {
401 m.error_counter
402 .with_label_values(&[work_handler::error_types::INVALID_MESSAGE])
403 .inc();
404 }
405 return Err(PipelineError::Generic(String::from(
406 "bidirectional engine received a non-header-only envelope",
407 )));
408 }
409
410 if !matches!(control_msg.request_type, RequestType::ManyIn) {
411 if let Some(m) = self.metrics() {
412 m.error_counter
413 .with_label_values(&[work_handler::error_types::INVALID_MESSAGE])
414 .inc();
415 }
416 return Err(PipelineError::Generic(String::from(
417 "bidirectional engine received a non-ManyIn request envelope",
418 )));
419 }
420
421 let req_stream_conn_info = control_msg
422 .request_stream_connection_info
423 .clone()
424 .ok_or_else(|| {
425 PipelineError::Generic(String::from(
426 "bidirectional control message missing request_stream_connection_info",
427 ))
428 })?;
429
430 let request_context: context::Context<()> = context::Context::with_id_and_metadata(
431 (),
432 control_msg.id.clone(),
433 control_msg.metadata.clone(),
434 );
435 let payload_codec = control_msg.payload_codec;
436 let context_arc: Arc<dyn AsyncEngineContext> = request_context.context();
437
438 let request_stream_recv = tcp::client::TcpClient::create_request_stream(
444 context_arc.clone(),
445 req_stream_conn_info,
446 None,
447 )
448 .await
449 .map_err(|e| {
450 if let Some(m) = self.metrics() {
451 m.error_counter
452 .with_label_values(&[work_handler::error_types::RESPONSE_STREAM])
453 .inc();
454 }
455 PipelineError::Generic(format!("Failed to create request stream: {e}"))
456 })?;
457
458 let (frame_tx, frame_rx) = tokio::sync::mpsc::channel::<T>(8);
463 let forwarder_ctx = context_arc.clone();
464 tokio::spawn(async move {
465 let mut rx = request_stream_recv.rx;
466 while let Some(bytes) = rx.recv().await {
467 if forwarder_ctx.is_killed() || forwarder_ctx.is_stopped() {
472 break;
473 }
474 match payload_codec.decode::<T>(&bytes) {
475 Ok(item) => {
476 if frame_tx.send(item).await.is_err() {
477 tracing::debug!(
478 "engine consumer dropped; bidirectional input forwarder exiting"
479 );
480 break;
481 }
482 }
483 Err(e) => {
484 tracing::error!(
485 error = %e,
486 codec = payload_codec.name(),
487 "failed to deserialize bidirectional request frame; killing context"
488 );
489 forwarder_ctx.kill();
490 break;
491 }
492 }
493 }
494 });
495
496 let input_stream: crate::engine::DataStream<T> =
497 Box::pin(tokio_stream::wrappers::ReceiverStream::new(frame_rx));
498 let request: ManyIn<T> = request_context.map(|_| RequestStream::new(input_stream));
499
500 Ok(ParsedRequest {
501 request,
502 response_connection_info: control_msg.connection_info,
503 frontend_send_ts_ns: control_msg.frontend_send_ts_ns,
504 payload_codec,
505 })
506 }
507}
508
509impl<Req: PipelineIO + Sync, U> Ingress<Req, ManyOut<U>>
510where
511 U: Data + Serialize + MaybeError + std::fmt::Debug,
512{
513 async fn handle_payload_shared(
521 &self,
522 payload: Bytes,
523 request_id: Option<String>,
524 ) -> Result<(), PipelineError>
525 where
526 Self: IngressDispatch<Request = Req>,
527 {
528 let t2_wallclock_ns = std::time::SystemTime::now()
529 .duration_since(std::time::UNIX_EPOCH)
530 .unwrap_or_default()
531 .as_nanos() as u64;
532 let start_time = std::time::Instant::now();
533
534 let _inflight_guard = self.metrics().map(|m| {
536 m.request_counter.inc();
537 m.inflight_requests.inc();
538 m.request_bytes.inc_by(payload.len() as u64);
539 if let Some(rid) = &request_id {
540 tracing::info!(request_id = %rid, "request received");
541 }
542 RequestMetricsGuard {
543 inflight_requests: m.inflight_requests.clone(),
544 request_duration: m.request_duration.clone(),
545 start_time,
546 request_id: request_id.clone(),
547 }
548 });
549
550 let ParsedRequest {
551 request,
552 response_connection_info,
553 frontend_send_ts_ns,
554 payload_codec,
555 } = self.parse_and_build_request(payload).await?;
556
557 if let Some(t1_ns) = frontend_send_ts_ns {
559 let transit_ns = t2_wallclock_ns.saturating_sub(t1_ns);
560 WORK_HANDLER_NETWORK_TRANSIT_SECONDS.observe(transit_ns as f64 / 1_000_000_000.0);
561 }
562
563 tracing::trace!("creating tcp response stream");
566 let mut publisher = tcp::client::TcpClient::create_response_stream(
567 request.context(),
568 response_connection_info,
569 self.metrics().map(|m| m.cancellation_total.clone()),
570 )
571 .await
572 .map_err(|e| {
573 if let Some(m) = self.metrics() {
574 m.error_counter
575 .with_label_values(&[work_handler::error_types::RESPONSE_STREAM])
576 .inc();
577 }
578 PipelineError::Generic(format!("Failed to create response stream: {e}"))
579 })?;
580
581 tracing::trace!("calling generate");
582 let stream = self
583 .segment
584 .get()
585 .expect("segment not set")
586 .generate(request)
587 .await
588 .map_err(|e| {
589 if let Some(m) = self.metrics() {
590 m.error_counter
591 .with_label_values(&[work_handler::error_types::GENERATE])
592 .inc();
593 }
594 PipelineError::GenerateError(e)
595 });
596
597 let stream = match stream {
600 Ok(stream) => {
601 tracing::trace!("Successfully generated response stream; sending prologue");
602 let _result = publisher.send_prologue(None).await;
603 WORK_HANDLER_TIME_TO_FIRST_RESPONSE_SECONDS
604 .observe(start_time.elapsed().as_secs_f64());
605 stream
606 }
607 Err(e) => {
608 let error_string = e.to_string();
609
610 #[cfg(debug_assertions)]
611 {
612 tracing::debug!(
613 "Failed to generate response stream (with debug backtrace): {:?}",
614 e
615 );
616 }
617 #[cfg(not(debug_assertions))]
618 {
619 tracing::error!("Failed to generate response stream: {error_string}");
620 }
621
622 let _result = publisher.send_prologue(Some(error_string)).await;
623 Err(e)?
624 }
625 };
626
627 self.pump_response_stream(stream, &publisher, payload_codec)
628 .await;
629
630 drop(_inflight_guard);
633
634 Ok(())
635 }
636}
637
638#[async_trait]
639impl<T, U> PushWorkHandler for Ingress<SingleIn<T>, ManyOut<U>>
640where
641 T: Data + for<'de> Deserialize<'de> + std::fmt::Debug,
642 U: Data + Serialize + MaybeError + std::fmt::Debug,
643{
644 fn add_metrics(
645 &self,
646 endpoint: &crate::component::Endpoint,
647 metrics_labels: Option<&[(&str, &str)]>,
648 ) -> Result<()> {
649 Ingress::add_metrics(self, endpoint, metrics_labels)
651 }
652
653 fn set_endpoint_health_check_notifier(&self, notifier: Arc<tokio::sync::Notify>) -> Result<()> {
654 self.endpoint_health_check_notifier
655 .set(notifier)
656 .map_err(|_| anyhow::anyhow!("Endpoint health check notifier already set"))?;
657 Ok(())
658 }
659
660 async fn handle_payload(
661 &self,
662 payload: Bytes,
663 request_id: Option<String>,
664 ) -> Result<(), PipelineError> {
665 self.handle_payload_shared(payload, request_id).await
666 }
667}
668
669#[async_trait]
670impl<T, U> PushWorkHandler for Ingress<ManyIn<T>, ManyOut<U>>
671where
672 T: Data + for<'de> Deserialize<'de> + std::fmt::Debug,
673 U: Data + Serialize + MaybeError + std::fmt::Debug,
674{
675 fn add_metrics(
676 &self,
677 endpoint: &crate::component::Endpoint,
678 metrics_labels: Option<&[(&str, &str)]>,
679 ) -> Result<()> {
680 Ingress::add_metrics(self, endpoint, metrics_labels)
682 }
683
684 fn set_endpoint_health_check_notifier(&self, notifier: Arc<tokio::sync::Notify>) -> Result<()> {
685 self.endpoint_health_check_notifier
686 .set(notifier)
687 .map_err(|_| anyhow::anyhow!("Endpoint health check notifier already set"))?;
688 Ok(())
689 }
690
691 async fn handle_payload(
692 &self,
693 payload: Bytes,
694 request_id: Option<String>,
695 ) -> Result<(), PipelineError> {
696 self.handle_payload_shared(payload, request_id).await
697 }
698}