1use std::io::Write as StdWrite;
10use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
11use std::sync::Arc;
12
13use bamboo_plugin::manifest::ServiceInputProtocol;
14use serde::Serialize;
15use tokio::io::{AsyncWrite, AsyncWriteExt};
16use tokio::process::Child;
17use tokio::sync::{mpsc, RwLock};
18use tokio_util::sync::CancellationToken;
19
20pub const DEFAULT_SERVICE_INPUT_QUEUE_CAPACITY: usize = 64;
25pub const MAX_SERVICE_INPUT_LINE_BYTES: usize = 1024 * 1024;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
31#[serde(rename_all = "snake_case")]
32pub enum ServiceInputHealth {
33 Waiting,
36 Ready,
37 BrokenStdin,
38 Stopped,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45pub struct ServiceInputStatusSnapshot {
46 pub protocol: ServiceInputProtocol,
47 #[serde(skip_serializing_if = "Option::is_none")]
50 pub generation: Option<u64>,
51 pub health: ServiceInputHealth,
52 pub queue_capacity: usize,
53 pub max_line_bytes: usize,
54 pub accepted_lines: u64,
55 pub written_lines: u64,
56 pub dropped_queue_full: u64,
57 pub dropped_stale_generation: u64,
58 pub dropped_stopped: u64,
59 pub dropped_broken_stdin: u64,
60 pub serialization_failures: u64,
61 pub oversize_lines: u64,
62 pub write_failures: u64,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
68pub enum ServiceInputSendError {
69 #[error("service input generation {generation} is stale")]
70 StaleGeneration { generation: u64 },
71 #[error("service input generation {generation} is stopped")]
72 Stopped { generation: u64 },
73 #[error("service input generation {generation} has broken stdin")]
74 BrokenStdin { generation: u64 },
75 #[error("service input generation {generation} queue is full")]
76 QueueFull { generation: u64 },
77 #[error("service input value could not be serialized as JSON")]
78 Serialization,
79 #[error("service input line exceeds the {max_bytes}-byte limit")]
80 Oversize { max_bytes: usize },
81}
82
83#[derive(Default)]
84struct ServiceInputCounters {
85 accepted_lines: AtomicU64,
86 written_lines: AtomicU64,
87 dropped_queue_full: AtomicU64,
88 dropped_stale_generation: AtomicU64,
89 dropped_stopped: AtomicU64,
90 dropped_broken_stdin: AtomicU64,
91 serialization_failures: AtomicU64,
92 oversize_lines: AtomicU64,
93 write_failures: AtomicU64,
94}
95
96fn increment(counter: &AtomicU64) {
97 let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
100 Some(value.saturating_add(1))
101 });
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105#[repr(u8)]
106enum GenerationState {
107 Active = 0,
108 Stale = 1,
109 Stopped = 2,
110 BrokenStdin = 3,
111}
112
113impl GenerationState {
114 fn from_u8(value: u8) -> Self {
115 match value {
116 0 => Self::Active,
117 1 => Self::Stale,
118 2 => Self::Stopped,
119 3 => Self::BrokenStdin,
120 _ => Self::BrokenStdin,
121 }
122 }
123}
124
125struct GenerationMeta {
126 generation: u64,
127 state: AtomicU8,
128 counters: Arc<ServiceInputCounters>,
129}
130
131impl GenerationMeta {
132 fn state(&self) -> GenerationState {
133 GenerationState::from_u8(self.state.load(Ordering::SeqCst))
134 }
135
136 fn mark_broken_stdin(&self) {
138 let _ = self.state.compare_exchange(
139 GenerationState::Active as u8,
140 GenerationState::BrokenStdin as u8,
141 Ordering::SeqCst,
142 Ordering::SeqCst,
143 );
144 }
145
146 fn mark_stale(&self) {
149 let _ = self
150 .state
151 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
152 (GenerationState::from_u8(current) != GenerationState::Stopped)
153 .then_some(GenerationState::Stale as u8)
154 });
155 }
156
157 fn mark_stopped(&self) {
160 let _ = self
161 .state
162 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
163 (GenerationState::from_u8(current) != GenerationState::Stopped)
164 .then_some(GenerationState::Stopped as u8)
165 });
166 }
167}
168
169struct CappedJsonLineWriter {
170 bytes: Vec<u8>,
171 max_json_bytes: usize,
172 exceeded: bool,
173}
174
175impl CappedJsonLineWriter {
176 fn new() -> Self {
177 Self {
178 bytes: Vec::with_capacity(1),
182 max_json_bytes: MAX_SERVICE_INPUT_LINE_BYTES.saturating_sub(1),
183 exceeded: false,
184 }
185 }
186
187 fn finish(mut self) -> Vec<u8> {
188 self.bytes.push(b'\n');
190 self.bytes
191 }
192}
193
194impl StdWrite for CappedJsonLineWriter {
195 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
196 let remaining = self.max_json_bytes.saturating_sub(self.bytes.len());
197 if buf.len() > remaining {
198 self.exceeded = true;
199 return Err(std::io::Error::other("service input line exceeds limit"));
200 }
201 let spare = self.bytes.capacity().saturating_sub(self.bytes.len());
202 let needed_spare = buf.len().saturating_add(1);
203 if needed_spare > spare {
204 self.bytes
205 .try_reserve_exact(needed_spare - spare)
206 .map_err(|_| std::io::Error::other("service input allocation failed"))?;
207 }
208 self.bytes.extend_from_slice(buf);
209 Ok(buf.len())
210 }
211
212 fn flush(&mut self) -> std::io::Result<()> {
213 Ok(())
214 }
215}
216
217#[derive(Clone)]
225pub struct ServiceInputSender {
226 meta: Arc<GenerationMeta>,
227 tx: mpsc::Sender<Vec<u8>>,
228}
229
230impl ServiceInputSender {
231 pub fn generation(&self) -> u64 {
232 self.meta.generation
233 }
234
235 fn reject_for_state(&self) -> Result<(), ServiceInputSendError> {
236 let generation = self.generation();
237 match self.meta.state() {
238 GenerationState::Active => Ok(()),
239 GenerationState::Stale => {
240 increment(&self.meta.counters.dropped_stale_generation);
241 Err(ServiceInputSendError::StaleGeneration { generation })
242 }
243 GenerationState::Stopped => {
244 increment(&self.meta.counters.dropped_stopped);
245 Err(ServiceInputSendError::Stopped { generation })
246 }
247 GenerationState::BrokenStdin => {
248 increment(&self.meta.counters.dropped_broken_stdin);
249 Err(ServiceInputSendError::BrokenStdin { generation })
250 }
251 }
252 }
253
254 pub fn try_send<T>(&self, value: &T) -> Result<(), ServiceInputSendError>
258 where
259 T: Serialize + ?Sized,
260 {
261 self.reject_for_state()?;
262 let mut writer = CappedJsonLineWriter::new();
263 if serde_json::to_writer(&mut writer, value).is_err() {
264 if writer.exceeded {
265 increment(&self.meta.counters.oversize_lines);
266 return Err(ServiceInputSendError::Oversize {
267 max_bytes: MAX_SERVICE_INPUT_LINE_BYTES,
268 });
269 }
270 increment(&self.meta.counters.serialization_failures);
271 return Err(ServiceInputSendError::Serialization);
272 }
273 let line = writer.finish();
274
275 self.reject_for_state()?;
279 match self.tx.try_send(line) {
280 Ok(()) => {
281 increment(&self.meta.counters.accepted_lines);
282 Ok(())
283 }
284 Err(mpsc::error::TrySendError::Full(_)) => {
285 increment(&self.meta.counters.dropped_queue_full);
286 Err(ServiceInputSendError::QueueFull {
287 generation: self.generation(),
288 })
289 }
290 Err(mpsc::error::TrySendError::Closed(_)) => {
291 self.meta.mark_broken_stdin();
295 self.reject_for_state()
296 }
297 }
298 }
299
300 #[cfg(test)]
301 pub(super) fn remaining_capacity(&self) -> usize {
302 self.tx.capacity()
303 }
304}
305
306struct ActiveServiceInput {
307 sender: ServiceInputSender,
308 cancel: CancellationToken,
309}
310
311pub(super) struct ServiceInputRuntime {
315 service_id: String,
316 next_generation: Arc<AtomicU64>,
317 counters: Arc<ServiceInputCounters>,
318 active: RwLock<Option<ActiveServiceInput>>,
319}
320
321impl ServiceInputRuntime {
322 pub(super) fn new(service_id: String, next_generation: Arc<AtomicU64>) -> Self {
323 Self {
324 service_id,
325 next_generation,
326 counters: Arc::new(ServiceInputCounters::default()),
327 active: RwLock::new(None),
328 }
329 }
330
331 pub(super) async fn sender(&self) -> Option<ServiceInputSender> {
332 self.active
333 .read()
334 .await
335 .as_ref()
336 .map(|active| active.sender.clone())
337 }
338
339 pub(super) async fn bind_child(
340 &self,
341 child: &mut Child,
342 ) -> Result<BoundServiceInput, ServiceInputBindError> {
343 let stdin = child
344 .stdin
345 .take()
346 .ok_or(ServiceInputBindError::MissingStdinPipe)?;
347 self.bind_writer(stdin, DEFAULT_SERVICE_INPUT_QUEUE_CAPACITY)
348 .await
349 }
350
351 async fn bind_writer<W>(
352 &self,
353 writer: W,
354 queue_capacity: usize,
355 ) -> Result<BoundServiceInput, ServiceInputBindError>
356 where
357 W: AsyncWrite + Unpin + Send + 'static,
358 {
359 let mut active = self.active.write().await;
360 if active.is_some() {
361 return Err(ServiceInputBindError::GenerationAlreadyBound);
362 }
363
364 let generation = self
365 .next_generation
366 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
367 current.checked_add(1)
368 })
369 .map(|previous| previous + 1)
370 .map_err(|_| ServiceInputBindError::GenerationExhausted)?;
371 let meta = Arc::new(GenerationMeta {
372 generation,
373 state: AtomicU8::new(GenerationState::Active as u8),
374 counters: self.counters.clone(),
375 });
376 let cancel = CancellationToken::new();
377 let (tx, rx) = mpsc::channel(queue_capacity.max(1));
378 let sender = ServiceInputSender {
379 meta: meta.clone(),
380 tx,
381 };
382 let writer_cancel = cancel.clone();
383 let service_id = self.service_id.clone();
384 let task = tokio::spawn(run_writer(service_id, meta, writer_cancel, writer, rx));
385
386 *active = Some(ActiveServiceInput {
387 sender: sender.clone(),
388 cancel: cancel.clone(),
389 });
390 Ok(BoundServiceInput {
391 sender,
392 cancel,
393 task: Some(task),
394 })
395 }
396
397 pub(super) async fn stop_active(&self) {
401 let active = self.active.write().await.take();
402 if let Some(active) = active {
403 active.sender.meta.mark_stopped();
404 active.cancel.cancel();
405 }
406 }
407
408 async fn retire_generation(&self, generation: u64, stopped: bool) {
409 let mut active = self.active.write().await;
410 if active
411 .as_ref()
412 .is_some_and(|active| active.sender.generation() == generation)
413 {
414 let active = active.take().expect("checked active generation");
415 if stopped {
416 active.sender.meta.mark_stopped();
417 } else {
418 active.sender.meta.mark_stale();
419 }
420 active.cancel.cancel();
421 }
422 }
423
424 pub(super) async fn snapshot(&self, stopped: bool) -> ServiceInputStatusSnapshot {
425 let active = self.active.read().await;
426 let (generation, health) = match active.as_ref() {
427 Some(active) => (
428 Some(active.sender.generation()),
429 match active.sender.meta.state() {
430 GenerationState::Active => ServiceInputHealth::Ready,
431 GenerationState::BrokenStdin => ServiceInputHealth::BrokenStdin,
432 GenerationState::Stale => ServiceInputHealth::Waiting,
433 GenerationState::Stopped => ServiceInputHealth::Stopped,
434 },
435 ),
436 None if stopped => (None, ServiceInputHealth::Stopped),
437 None => (None, ServiceInputHealth::Waiting),
438 };
439 ServiceInputStatusSnapshot {
440 protocol: ServiceInputProtocol::NdjsonV1,
441 generation,
442 health,
443 queue_capacity: DEFAULT_SERVICE_INPUT_QUEUE_CAPACITY,
444 max_line_bytes: MAX_SERVICE_INPUT_LINE_BYTES,
445 accepted_lines: self.counters.accepted_lines.load(Ordering::Relaxed),
446 written_lines: self.counters.written_lines.load(Ordering::Relaxed),
447 dropped_queue_full: self.counters.dropped_queue_full.load(Ordering::Relaxed),
448 dropped_stale_generation: self
449 .counters
450 .dropped_stale_generation
451 .load(Ordering::Relaxed),
452 dropped_stopped: self.counters.dropped_stopped.load(Ordering::Relaxed),
453 dropped_broken_stdin: self.counters.dropped_broken_stdin.load(Ordering::Relaxed),
454 serialization_failures: self.counters.serialization_failures.load(Ordering::Relaxed),
455 oversize_lines: self.counters.oversize_lines.load(Ordering::Relaxed),
456 write_failures: self.counters.write_failures.load(Ordering::Relaxed),
457 }
458 }
459
460 #[cfg(test)]
461 pub(super) async fn bind_writer_for_test<W>(
462 &self,
463 writer: W,
464 queue_capacity: usize,
465 ) -> Result<BoundServiceInput, ServiceInputBindError>
466 where
467 W: AsyncWrite + Unpin + Send + 'static,
468 {
469 self.bind_writer(writer, queue_capacity).await
470 }
471
472 #[cfg(test)]
473 pub(super) async fn bind_child_for_test(
474 &self,
475 child: &mut Child,
476 queue_capacity: usize,
477 ) -> Result<BoundServiceInput, ServiceInputBindError> {
478 let stdin = child
479 .stdin
480 .take()
481 .ok_or(ServiceInputBindError::MissingStdinPipe)?;
482 self.bind_writer(stdin, queue_capacity).await
483 }
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
487pub(super) enum ServiceInputBindError {
488 #[error("spawned NDJSON service has no stdin pipe")]
489 MissingStdinPipe,
490 #[error("service input generation is already bound")]
491 GenerationAlreadyBound,
492 #[error("service input generation space is exhausted")]
493 GenerationExhausted,
494}
495
496pub(super) struct BoundServiceInput {
497 sender: ServiceInputSender,
498 cancel: CancellationToken,
499 task: Option<tokio::task::JoinHandle<()>>,
500}
501
502impl BoundServiceInput {
503 pub(super) fn generation(&self) -> u64 {
504 self.sender.generation()
505 }
506
507 pub(super) async fn close(mut self, runtime: &ServiceInputRuntime, stopped: bool) {
510 runtime.retire_generation(self.generation(), stopped).await;
511 if stopped {
514 self.sender.meta.mark_stopped();
515 } else {
516 self.sender.meta.mark_stale();
517 }
518 self.cancel.cancel();
519 if let Some(task) = self.task.take() {
520 let _ = task.await;
521 }
522 }
523}
524
525impl Drop for BoundServiceInput {
526 fn drop(&mut self) {
527 self.sender.meta.mark_stale();
530 self.cancel.cancel();
531 if let Some(task) = self.task.take() {
532 task.abort();
533 }
534 }
535}
536
537async fn run_writer<W>(
538 service_id: String,
539 meta: Arc<GenerationMeta>,
540 cancel: CancellationToken,
541 mut writer: W,
542 mut rx: mpsc::Receiver<Vec<u8>>,
543) where
544 W: AsyncWrite + Unpin,
545{
546 loop {
547 let line = tokio::select! {
548 biased;
549 _ = cancel.cancelled() => break,
550 line = rx.recv() => match line {
551 Some(line) => line,
552 None => break,
553 },
554 };
555
556 let result = tokio::select! {
557 biased;
558 _ = cancel.cancelled() => break,
559 result = async {
560 writer.write_all(&line).await?;
561 writer.flush().await
562 } => result,
563 };
564 match result {
565 Ok(()) => increment(&meta.counters.written_lines),
566 Err(error) => {
567 increment(&meta.counters.write_failures);
568 meta.mark_broken_stdin();
569 tracing::warn!(
572 service_id = %service_id,
573 generation = meta.generation,
574 error_kind = ?error.kind(),
575 "service NDJSON stdin writer stopped"
576 );
577 break;
578 }
579 }
580 }
581 rx.close();
582 }