1use std::{
2 collections::{BTreeMap, HashMap},
3 future::Future,
4 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
5 pin::Pin,
6 sync::{Arc, Mutex},
7 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
8};
9
10use datum::{
11 Sink, StreamInstrumentationRegistry, StreamInstrumentationSnapshot, StreamInstrumentationState,
12};
13use datum_net::quic::quinn;
14use prost::Message as ProstMessage;
15use tokio::{
16 io::{AsyncRead, AsyncWrite, AsyncWriteExt},
17 net::{TcpListener, TcpStream},
18 sync::{mpsc, watch},
19 task::JoinHandle,
20};
21
22use crate::{
23 AgentError, AgentHandle, AgentResult, ClusterJobMetadata, ClusterPlacementHistory, JobEvent,
24 JobEventKind, JobExitReason, JobRegistryHandle, JobSpec, JobStatus as RegistryJobStatus,
25 PlacementSpec as RegistryPlacementSpec, PlacementStrategy as RegistryPlacementStrategy,
26 dcp::{
27 DcpError, DcpResult,
28 frame::{read_frame, write_frame},
29 proto::{
30 ClusterJobList, ClusterJobStart, ClusterNodeList, CompleteShardingAsk, ConfigValue,
31 DCP_PROTOCOL_MAJOR, DcpFrame, Event, ForwardShardEnvelopes, Hello, JobList,
32 JobStatus as WireJobStatus, MetricSample, PlacementSpec, PlacementStrategy,
33 RememberClusterAssignment, RememberShardAllocations, Request, Response, ResponseStatus,
34 ShardAllocation, ShardAllocationRequest, ShardAllocationTable,
35 ShardEnvelopeBatchResult, ShardPipeFrame, StreamMetric, SubmitClusterJob, dcp_frame,
36 request,
37 },
38 },
39};
40
41type DcpJobFactory =
42 dyn Fn(String, HashMap<String, String>) -> AgentResult<JobSpec> + Send + Sync + 'static;
43pub type ClusterViewFuture<'a, T> = Pin<Box<dyn Future<Output = DcpResult<T>> + Send + 'a>>;
44const DEFAULT_CLUSTER_REQUEST_TIMEOUT: Duration = Duration::from_millis(750);
45
46pub trait ClusterViewProvider: Send + Sync + 'static {
49 fn submit_cluster_job(
51 &self,
52 request: SubmitClusterJob,
53 timeout: Duration,
54 ) -> ClusterViewFuture<'_, WireJobStatus>;
55
56 fn list_cluster_jobs(&self, timeout: Duration) -> ClusterViewFuture<'_, ClusterJobList>;
58
59 fn cluster_node_info(&self, timeout: Duration) -> ClusterViewFuture<'_, ClusterNodeList>;
61
62 fn cluster_job_status(
64 &self,
65 name: String,
66 timeout: Duration,
67 ) -> ClusterViewFuture<'_, WireJobStatus>;
68
69 fn drain_cluster_job(
71 &self,
72 name: String,
73 timeout: Duration,
74 ) -> ClusterViewFuture<'_, WireJobStatus>;
75
76 fn stop_cluster_job(
78 &self,
79 name: String,
80 timeout: Duration,
81 ) -> ClusterViewFuture<'_, WireJobStatus>;
82
83 fn remember_cluster_assignment(
85 &self,
86 request: RememberClusterAssignment,
87 ) -> ClusterViewFuture<'_, ()>;
88}
89
90pub trait ShardingViewProvider: Send + Sync + 'static {
94 fn allocate_shard(
97 &self,
98 request: ShardAllocationRequest,
99 timeout: Duration,
100 ) -> ClusterViewFuture<'_, ShardAllocation>;
101
102 fn remember_shard_allocations(
104 &self,
105 request: RememberShardAllocations,
106 ) -> ClusterViewFuture<'_, ()>;
107
108 fn get_shard_allocations(
110 &self,
111 type_name: String,
112 timeout: Duration,
113 ) -> ClusterViewFuture<'_, ShardAllocationTable>;
114
115 fn forward_shard_envelopes(
117 &self,
118 request: ForwardShardEnvelopes,
119 timeout: Duration,
120 ) -> ClusterViewFuture<'_, ShardEnvelopeBatchResult>;
121
122 fn complete_sharding_ask(&self, request: CompleteShardingAsk) -> ClusterViewFuture<'_, ()>;
124}
125
126#[derive(Clone, Default)]
127pub struct DcpJobFactories {
128 factories: Arc<Mutex<HashMap<String, Arc<DcpJobFactory>>>>,
129}
130
131impl DcpJobFactories {
132 #[must_use]
133 pub fn new() -> Self {
134 Self::default()
135 }
136
137 pub fn register<F>(&self, name: impl Into<String>, factory: F) -> AgentResult<()>
138 where
139 F: Fn(String, HashMap<String, String>) -> AgentResult<JobSpec> + Send + Sync + 'static,
140 {
141 let name = name.into();
142 if name.trim().is_empty() {
143 return Err(AgentError::InvalidJobName);
144 }
145 self.factories
146 .lock()
147 .expect("DCP job factories poisoned")
148 .insert(name, Arc::new(factory));
149 Ok(())
150 }
151
152 pub(crate) fn build(
153 &self,
154 factory_name: &str,
155 instance_name: String,
156 params: HashMap<String, String>,
157 ) -> DcpResult<JobSpec> {
158 let factory = self
159 .factories
160 .lock()
161 .expect("DCP job factories poisoned")
162 .get(factory_name)
163 .cloned()
164 .ok_or_else(|| {
165 DcpError::response(
166 ResponseStatus::NotFound,
167 format!("job factory not found: {factory_name}"),
168 )
169 })?;
170 Ok(factory(instance_name, params)?)
171 }
172}
173
174#[derive(Clone)]
175pub struct DcpTcpServerConfig {
176 pub addr: SocketAddr,
177}
178
179#[derive(Clone)]
180pub struct DcpQuicServerConfig {
181 pub addr: SocketAddr,
182 pub server_config: quinn::ServerConfig,
183}
184
185#[derive(Clone)]
186pub struct DcpServerConfig {
187 pub node_id: String,
188 pub tcp: Option<DcpTcpServerConfig>,
189 pub quic: Option<DcpQuicServerConfig>,
190 pub auth_token: Option<String>,
191 pub metrics_interval: Duration,
192 pub frame_buffer: usize,
193}
194
195impl Default for DcpServerConfig {
196 fn default() -> Self {
197 Self {
198 node_id: format!("datum-agent-{}", std::process::id()),
199 tcp: Some(DcpTcpServerConfig {
200 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
201 }),
202 quic: None,
203 auth_token: None,
204 metrics_interval: Duration::from_secs(1),
205 frame_buffer: 256,
206 }
207 }
208}
209
210#[derive(Clone)]
211pub struct DcpServer {
212 state: Arc<DcpServerState>,
213}
214
215struct DcpServerState {
216 registry: JobRegistryHandle,
217 instrumentation: StreamInstrumentationRegistry,
218 factories: DcpJobFactories,
219 config: DcpServerConfig,
220 config_store: Mutex<HashMap<String, String>>,
221 cluster_view: Mutex<Option<Arc<dyn ClusterViewProvider>>>,
222 sharding_view: Mutex<Option<Arc<dyn ShardingViewProvider>>>,
223}
224
225impl DcpServer {
226 #[must_use]
227 pub fn new(
228 registry: JobRegistryHandle,
229 instrumentation: StreamInstrumentationRegistry,
230 factories: DcpJobFactories,
231 config: DcpServerConfig,
232 ) -> Self {
233 Self {
234 state: Arc::new(DcpServerState {
235 registry,
236 instrumentation,
237 factories,
238 config,
239 config_store: Mutex::new(HashMap::new()),
240 cluster_view: Mutex::new(None),
241 sharding_view: Mutex::new(None),
242 }),
243 }
244 }
245
246 #[must_use]
247 pub fn from_agent(
248 agent: &AgentHandle,
249 factories: DcpJobFactories,
250 config: DcpServerConfig,
251 ) -> Self {
252 Self::new(
253 agent.registry().clone(),
254 agent.instrumentation_registry().clone(),
255 factories,
256 config,
257 )
258 }
259
260 pub fn set_cluster_view(&self, provider: Arc<dyn ClusterViewProvider>) {
263 *self
264 .state
265 .cluster_view
266 .lock()
267 .expect("DCP cluster view provider poisoned") = Some(provider);
268 }
269
270 pub fn clear_cluster_view(&self) {
273 *self
274 .state
275 .cluster_view
276 .lock()
277 .expect("DCP cluster view provider poisoned") = None;
278 }
279
280 pub fn set_sharding_view(&self, provider: Arc<dyn ShardingViewProvider>) {
282 *self
283 .state
284 .sharding_view
285 .lock()
286 .expect("DCP sharding view provider poisoned") = Some(provider);
287 }
288
289 pub fn clear_sharding_view(&self) {
292 *self
293 .state
294 .sharding_view
295 .lock()
296 .expect("DCP sharding view provider poisoned") = None;
297 }
298
299 pub async fn start(&self) -> DcpResult<DcpServerHandle> {
300 let (shutdown_sender, shutdown_receiver) = watch::channel(false);
301 let mut tasks = Vec::new();
302 let mut tcp_addr = None;
303 let mut quic_addr = None;
304
305 if let Some(tcp) = &self.state.config.tcp {
306 ensure_loopback(tcp.addr)?;
307 let listener = TcpListener::bind(tcp.addr).await?;
308 tcp_addr = Some(listener.local_addr()?);
309 let state = Arc::clone(&self.state);
310 let shutdown = shutdown_receiver.clone();
311 tasks.push(tokio::spawn(async move {
312 run_tcp_listener(listener, state, shutdown).await;
313 }));
314 }
315
316 if let Some(quic) = &self.state.config.quic {
317 let endpoint = quinn::Endpoint::server(quic.server_config.clone(), quic.addr)?;
318 quic_addr = Some(endpoint.local_addr()?);
319 let state = Arc::clone(&self.state);
320 let shutdown = shutdown_receiver.clone();
321 tasks.push(tokio::spawn(async move {
322 run_quic_listener(endpoint, state, shutdown).await;
323 }));
324 }
325
326 if tcp_addr.is_none() && quic_addr.is_none() {
327 return Err(DcpError::Protocol(
328 "DCP server has no configured listeners".to_owned(),
329 ));
330 }
331
332 Ok(DcpServerHandle {
333 tcp_addr,
334 quic_addr,
335 shutdown: shutdown_sender,
336 tasks,
337 })
338 }
339}
340
341#[must_use = "dropping DcpServerHandle immediately aborts the DCP listeners; hold it for the server's lifetime"]
342pub struct DcpServerHandle {
343 tcp_addr: Option<SocketAddr>,
344 quic_addr: Option<SocketAddr>,
345 shutdown: watch::Sender<bool>,
346 tasks: Vec<JoinHandle<()>>,
347}
348
349impl DcpServerHandle {
350 #[must_use]
351 pub fn tcp_addr(&self) -> Option<SocketAddr> {
352 self.tcp_addr
353 }
354
355 #[must_use]
356 pub fn quic_addr(&self) -> Option<SocketAddr> {
357 self.quic_addr
358 }
359
360 pub async fn shutdown(mut self) {
361 let _ = self.shutdown.send(true);
362 for task in self.tasks.drain(..) {
363 task.abort();
364 let _ = task.await;
365 }
366 }
367}
368
369impl Drop for DcpServerHandle {
370 fn drop(&mut self) {
371 let _ = self.shutdown.send(true);
372 for task in &self.tasks {
373 task.abort();
374 }
375 }
376}
377
378async fn run_tcp_listener(
379 listener: TcpListener,
380 state: Arc<DcpServerState>,
381 mut shutdown: watch::Receiver<bool>,
382) {
383 loop {
384 tokio::select! {
385 changed = shutdown.changed() => {
386 if changed.is_err() || *shutdown.borrow() {
387 break;
388 }
389 }
390 accepted = listener.accept() => {
391 let Ok((stream, _peer)) = accepted else {
392 break;
393 };
394 let state = Arc::clone(&state);
395 tokio::spawn(async move {
396 let _ = run_tcp_connection(stream, state).await;
397 });
398 }
399 }
400 }
401}
402
403async fn run_tcp_connection(stream: TcpStream, state: Arc<DcpServerState>) -> DcpResult<()> {
404 stream.set_nodelay(true)?;
405 let (reader, writer) = stream.into_split();
406 run_connection(reader, writer, state).await
407}
408
409async fn run_quic_listener(
410 endpoint: quinn::Endpoint,
411 state: Arc<DcpServerState>,
412 mut shutdown: watch::Receiver<bool>,
413) {
414 loop {
415 tokio::select! {
416 changed = shutdown.changed() => {
417 if changed.is_err() || *shutdown.borrow() {
418 endpoint.close(quinn::VarInt::from_u32(0), b"DCP shutdown");
419 break;
420 }
421 }
422 incoming = endpoint.accept() => {
423 let Some(incoming) = incoming else {
424 break;
425 };
426 let state = Arc::clone(&state);
427 let shutdown = shutdown.clone();
428 tokio::spawn(async move {
429 if let Ok(connection) = incoming.await {
430 run_quic_connection(connection, state, shutdown).await;
431 }
432 });
433 }
434 }
435 }
436}
437
438async fn run_quic_connection(
439 connection: quinn::Connection,
440 state: Arc<DcpServerState>,
441 mut shutdown: watch::Receiver<bool>,
442) {
443 loop {
444 tokio::select! {
445 changed = shutdown.changed() => {
446 if changed.is_err() || *shutdown.borrow() {
447 connection.close(quinn::VarInt::from_u32(0), b"DCP shutdown");
448 break;
449 }
450 }
451 accepted = connection.accept_bi() => {
452 let Ok((send, recv)) = accepted else {
453 break;
454 };
455 let state = Arc::clone(&state);
456 tokio::spawn(async move {
457 let _ = run_connection(recv, send, state).await;
458 });
459 }
460 }
461 }
462}
463
464async fn run_connection<R, W>(mut reader: R, writer: W, state: Arc<DcpServerState>) -> DcpResult<()>
465where
466 R: AsyncRead + Unpin + Send + 'static,
467 W: AsyncWrite + Unpin + Send + 'static,
468{
469 let (outbound, outbound_receiver) = mpsc::channel(state.config.frame_buffer.max(1));
470 let writer_task = tokio::spawn(write_loop(writer, outbound_receiver));
471 let mut subscriptions = Vec::new();
472
473 let Some(first) = read_frame(&mut reader).await? else {
474 return Err(DcpError::Closed);
475 };
476 let hello = match first.frame {
477 Some(dcp_frame::Frame::Hello(hello)) => hello,
478 _ => {
479 let response =
480 Response::error(0, ResponseStatus::BadRequest, "first frame must be Hello");
481 send_frame(&outbound, DcpFrame::response(response)).await?;
482 return Err(DcpError::Protocol("first frame must be Hello".to_owned()));
483 }
484 };
485
486 let hello_response = negotiate_hello(&state, &hello);
487 let accepted = hello_response.response_status() == ResponseStatus::Ok;
488 send_frame(&outbound, DcpFrame::response(hello_response)).await?;
489 if !accepted {
490 return Ok(());
491 }
492
493 while let Some(frame) = read_frame(&mut reader).await? {
494 let request = match frame.frame {
495 Some(dcp_frame::Frame::Request(request)) => request,
496 _ => {
497 return Err(DcpError::Protocol(
498 "client sent non-request frame after hello".to_owned(),
499 ));
500 }
501 };
502 if matches!(&request.command, Some(request::Command::OpenShardPipe(_))) {
503 send_frame(
504 &outbound,
505 DcpFrame::response(Response::ok(request.request_id, Vec::new())),
506 )
507 .await?;
508 run_shard_pipe_connection(reader, outbound.clone(), Arc::clone(&state)).await?;
509 break;
510 }
511 let (response, subscription) =
512 dispatch_request(Arc::clone(&state), request, outbound.clone()).await;
513 send_frame(&outbound, DcpFrame::response(response)).await?;
514 if let Some(subscription) = subscription {
515 subscriptions.push(subscription);
516 }
517 }
518
519 for subscription in subscriptions {
520 subscription.abort();
521 }
522 drop(outbound);
523 writer_task.await??;
524 Ok(())
525}
526
527async fn run_shard_pipe_connection<R>(
528 mut reader: R,
529 outbound: mpsc::Sender<DcpFrame>,
530 state: Arc<DcpServerState>,
531) -> DcpResult<()>
532where
533 R: AsyncRead + Unpin,
534{
535 while let Some(frame) = read_frame(&mut reader).await? {
536 let pipe = match frame.frame {
537 Some(dcp_frame::Frame::ShardPipe(pipe)) => pipe,
538 _ => {
539 return Err(DcpError::Protocol(
540 "shard pipe connection received a non-pipe frame".to_owned(),
541 ));
542 }
543 };
544 dispatch_shard_pipe(Arc::clone(&state), pipe).await?;
545 }
546 drop(outbound);
547 Ok(())
548}
549
550async fn dispatch_shard_pipe(state: Arc<DcpServerState>, pipe: ShardPipeFrame) -> DcpResult<()> {
551 let provider = sharding_view_provider(&state)?;
552 for batch in pipe.forwards {
553 provider
554 .forward_shard_envelopes(batch, DEFAULT_CLUSTER_REQUEST_TIMEOUT)
555 .await?;
556 }
557 for reply in pipe.replies {
558 provider.complete_sharding_ask(reply).await?;
559 }
560 Ok(())
561}
562
563async fn write_loop<W>(mut writer: W, mut outbound: mpsc::Receiver<DcpFrame>) -> DcpResult<()>
564where
565 W: AsyncWrite + Unpin,
566{
567 while let Some(frame) = outbound.recv().await {
568 write_frame(&mut writer, &frame).await?;
569 }
570 let _ = writer.shutdown().await;
571 Ok(())
572}
573
574async fn send_frame(sender: &mpsc::Sender<DcpFrame>, frame: DcpFrame) -> DcpResult<()> {
575 sender.send(frame).await.map_err(|_| DcpError::Closed)
576}
577
578fn negotiate_hello(state: &DcpServerState, hello: &Hello) -> Response {
579 match protocol_major(&hello.protocol_version) {
580 Some(DCP_PROTOCOL_MAJOR) => {}
581 Some(other) => {
582 return Response::error(
583 0,
584 ResponseStatus::ProtocolMismatch,
585 format!("unsupported DCP major version: {other}"),
586 );
587 }
588 None => {
589 return Response::error(
590 0,
591 ResponseStatus::ProtocolMismatch,
592 format!("invalid DCP protocol version: {}", hello.protocol_version),
593 );
594 }
595 }
596
597 if let Some(expected) = &state.config.auth_token {
598 let actual = hello
599 .auth
600 .as_ref()
601 .map(|auth| auth.bearer_token.as_str())
602 .unwrap_or_default();
603 if actual != expected {
604 return Response::error(0, ResponseStatus::Unauthorized, "invalid DCP token");
605 }
606 }
607
608 Response::ok(0, state.config.node_id.as_bytes().to_vec())
609}
610
611fn protocol_major(version: &str) -> Option<u32> {
612 version.split('.').next()?.parse().ok()
613}
614
615async fn dispatch_request(
616 state: Arc<DcpServerState>,
617 request: Request,
618 outbound: mpsc::Sender<DcpFrame>,
619) -> (Response, Option<JoinHandle<()>>) {
620 let request_id = request.request_id;
621 let deadline = request.deadline_ms;
622 let dispatch = async {
623 let command = request.command.ok_or_else(|| {
624 DcpError::response(ResponseStatus::BadRequest, "request missing command")
625 })?;
626 dispatch_command(state, request_id, command, outbound).await
627 };
628
629 let result = if deadline == 0 {
630 dispatch.await
631 } else {
632 match tokio::time::timeout(Duration::from_millis(deadline), dispatch).await {
633 Ok(result) => result,
634 Err(_) => Err(DcpError::response(
635 ResponseStatus::DeadlineExceeded,
636 "request deadline exceeded",
637 )),
638 }
639 };
640
641 match result {
642 Ok(result) => result,
643 Err(error) => (response_for_error(request_id, error), None),
644 }
645}
646
647async fn dispatch_command(
648 state: Arc<DcpServerState>,
649 request_id: u64,
650 command: request::Command,
651 outbound: mpsc::Sender<DcpFrame>,
652) -> DcpResult<(Response, Option<JoinHandle<()>>)> {
653 match command {
654 request::Command::ListJobs(_) => {
655 let registry = state.registry.clone();
656 let jobs = registry_call(move || registry.list()).await?;
657 let payload = JobList {
658 jobs: jobs.iter().map(wire_job_status).collect(),
659 }
660 .encode_to_vec();
661 Ok((Response::ok(request_id, payload), None))
662 }
663 request::Command::StartJob(start) => {
664 if start.factory_name.trim().is_empty() || start.instance_name.trim().is_empty() {
665 return Err(DcpError::response(
666 ResponseStatus::BadRequest,
667 "StartJob requires factory_name and instance_name",
668 ));
669 }
670 let mut spec = state.factories.build(
671 &start.factory_name,
672 start.instance_name.clone(),
673 start.params,
674 )?;
675 if let Some(cluster) = start.cluster {
676 spec = spec.with_cluster_metadata(cluster_metadata_from_wire(cluster)?);
677 }
678 let registry = state.registry.clone();
679 let name = spec.name.clone();
680 let status = registry_call(move || {
681 registry.submit(spec)?;
682 registry.start(name)
683 })
684 .await?;
685 Ok((status_response(request_id, &status), None))
686 }
687 request::Command::DrainJob(drain) => {
688 if drain.cluster {
689 let provider = cluster_view_provider(&state)?;
690 let timeout = request_timeout(0);
691 let payload = provider
692 .drain_cluster_job(drain.name, timeout)
693 .await?
694 .encode_to_vec();
695 return Ok((Response::ok(request_id, payload), None));
696 }
697 let registry = state.registry.clone();
698 let status = registry_call(move || registry.drain(drain.name)).await?;
699 Ok((status_response(request_id, &status), None))
700 }
701 request::Command::StopJob(stop) => {
702 if stop.cluster {
703 let provider = cluster_view_provider(&state)?;
704 let timeout = request_timeout(0);
705 let payload = provider
706 .stop_cluster_job(stop.name, timeout)
707 .await?
708 .encode_to_vec();
709 return Ok((Response::ok(request_id, payload), None));
710 }
711 let registry = state.registry.clone();
712 let status = registry_call(move || registry.stop(stop.name)).await?;
713 Ok((status_response(request_id, &status), None))
714 }
715 request::Command::RestartJob(restart) => {
716 if restart.cluster {
717 return Err(DcpError::response(
718 ResponseStatus::BadRequest,
719 "cluster restart is not implemented in v0.10; drain/stop and submit again",
720 ));
721 }
722 let registry = state.registry.clone();
723 let status = registry_call(move || registry.restart(restart.name)).await?;
724 Ok((status_response(request_id, &status), None))
725 }
726 request::Command::JobStatus(status) => {
727 if status.cluster {
728 let provider = cluster_view_provider(&state)?;
729 let timeout = request_timeout(0);
730 let payload = provider
731 .cluster_job_status(status.name, timeout)
732 .await?
733 .encode_to_vec();
734 return Ok((Response::ok(request_id, payload), None));
735 }
736 let registry = state.registry.clone();
737 let status = registry_call(move || registry.status(status.name)).await?;
738 Ok((status_response(request_id, &status), None))
739 }
740 request::Command::SubscribeEvents(_) => {
741 let registry = state.registry.clone();
742 let subscription = spawn_event_subscription(request_id, registry, outbound);
743 Ok((Response::ok(request_id, Vec::new()), Some(subscription)))
744 }
745 request::Command::SubscribeMetrics(metrics) => {
746 let interval = if metrics.interval_ms == 0 {
747 state.config.metrics_interval
748 } else {
749 Duration::from_millis(metrics.interval_ms)
750 };
751 let subscription = spawn_metrics_subscription(
752 request_id,
753 state.instrumentation.clone(),
754 interval.max(Duration::from_millis(1)),
755 outbound,
756 );
757 Ok((Response::ok(request_id, Vec::new()), Some(subscription)))
758 }
759 request::Command::GetConfig(get) => {
760 let value = state
761 .config_store
762 .lock()
763 .expect("DCP config store poisoned")
764 .get(&get.key)
765 .cloned();
766 let payload = ConfigValue {
767 key: get.key,
768 value: value.clone().unwrap_or_default(),
769 existed: value.is_some(),
770 }
771 .encode_to_vec();
772 Ok((Response::ok(request_id, payload), None))
773 }
774 request::Command::PutConfig(put) => {
775 let existed = state
776 .config_store
777 .lock()
778 .expect("DCP config store poisoned")
779 .insert(put.key.clone(), put.value.clone())
780 .is_some();
781 let payload = ConfigValue {
782 key: put.key,
783 value: put.value,
784 existed,
785 }
786 .encode_to_vec();
787 Ok((Response::ok(request_id, payload), None))
788 }
789 request::Command::ListClusterJobs(list) => {
790 let provider = cluster_view_provider(&state)?;
791 let timeout = request_timeout(list.timeout_ms);
792 let payload = provider.list_cluster_jobs(timeout).await?.encode_to_vec();
793 Ok((Response::ok(request_id, payload), None))
794 }
795 request::Command::ClusterNodeInfo(info) => {
796 let provider = cluster_view_provider(&state)?;
797 let timeout = request_timeout(info.timeout_ms);
798 let payload = provider.cluster_node_info(timeout).await?.encode_to_vec();
799 Ok((Response::ok(request_id, payload), None))
800 }
801 request::Command::SubmitClusterJob(submit) => {
802 let provider = cluster_view_provider(&state)?;
803 let timeout = request_timeout(submit.timeout_ms);
804 let payload = provider
805 .submit_cluster_job(submit, timeout)
806 .await?
807 .encode_to_vec();
808 Ok((Response::ok(request_id, payload), None))
809 }
810 request::Command::RememberClusterAssignment(remember) => {
811 let provider = cluster_view_provider(&state)?;
812 provider.remember_cluster_assignment(remember).await?;
813 Ok((Response::ok(request_id, Vec::new()), None))
814 }
815 request::Command::AllocateShard(allocate) => {
816 let provider = sharding_view_provider(&state)?;
817 let timeout = request_timeout(allocate.timeout_ms);
818 let payload = provider
819 .allocate_shard(allocate, timeout)
820 .await?
821 .encode_to_vec();
822 Ok((Response::ok(request_id, payload), None))
823 }
824 request::Command::RememberShardAllocations(remember) => {
825 let provider = sharding_view_provider(&state)?;
826 provider.remember_shard_allocations(remember).await?;
827 Ok((Response::ok(request_id, Vec::new()), None))
828 }
829 request::Command::GetShardAllocations(get) => {
830 let provider = sharding_view_provider(&state)?;
831 let timeout = request_timeout(0);
832 let payload = provider
833 .get_shard_allocations(get.type_name, timeout)
834 .await?
835 .encode_to_vec();
836 Ok((Response::ok(request_id, payload), None))
837 }
838 request::Command::ForwardShardEnvelopes(batch) => {
839 let provider = sharding_view_provider(&state)?;
840 let timeout = request_timeout(0);
841 let payload = provider
842 .forward_shard_envelopes(batch, timeout)
843 .await?
844 .encode_to_vec();
845 Ok((Response::ok(request_id, payload), None))
846 }
847 request::Command::CompleteShardingAsk(reply) => {
848 let provider = sharding_view_provider(&state)?;
849 provider.complete_sharding_ask(reply).await?;
850 Ok((Response::ok(request_id, Vec::new()), None))
851 }
852 request::Command::OpenShardPipe(_) => Err(DcpError::response(
853 ResponseStatus::BadRequest,
854 "OpenShardPipe must be handled by the DCP connection loop",
855 )),
856 }
857}
858
859fn cluster_view_provider(state: &DcpServerState) -> DcpResult<Arc<dyn ClusterViewProvider>> {
860 state
861 .cluster_view
862 .lock()
863 .expect("DCP cluster view provider poisoned")
864 .clone()
865 .ok_or_else(|| {
866 DcpError::response(
867 ResponseStatus::Failed,
868 "cluster view is not configured on this datum-agent",
869 )
870 })
871}
872
873fn sharding_view_provider(state: &DcpServerState) -> DcpResult<Arc<dyn ShardingViewProvider>> {
874 state
875 .sharding_view
876 .lock()
877 .expect("DCP sharding view provider poisoned")
878 .clone()
879 .ok_or_else(|| {
880 DcpError::response(
881 ResponseStatus::Failed,
882 "cluster sharding is not configured on this datum-agent",
883 )
884 })
885}
886
887fn request_timeout(timeout_ms: u64) -> Duration {
888 if timeout_ms == 0 {
889 DEFAULT_CLUSTER_REQUEST_TIMEOUT
890 } else {
891 Duration::from_millis(timeout_ms)
892 }
893}
894
895async fn registry_call<T, F>(call: F) -> DcpResult<T>
896where
897 T: Send + 'static,
898 F: FnOnce() -> AgentResult<T> + Send + 'static,
899{
900 Ok(tokio::task::spawn_blocking(call).await??)
901}
902
903fn status_response(request_id: u64, status: &RegistryJobStatus) -> Response {
904 Response::ok(request_id, wire_job_status(status).encode_to_vec())
905}
906
907fn response_for_error(request_id: u64, error: DcpError) -> Response {
908 match error {
909 DcpError::Response { status, message } => Response::error(request_id, status, message),
910 DcpError::Agent(error) => {
911 let status = match &error {
912 AgentError::InvalidJobName => ResponseStatus::BadRequest,
913 AgentError::JobNotFound(_) => ResponseStatus::NotFound,
914 AgentError::JobAlreadyExists(_) | AgentError::JobAlreadyRunning(_) => {
915 ResponseStatus::Conflict
916 }
917 AgentError::DrainUnsupported(_)
918 | AgentError::JobNotRunning(_)
919 | AgentError::RestartLimitExceeded(_) => ResponseStatus::Failed,
920 AgentError::RegistryClosed | AgentError::Actor(_) | AgentError::Stream(_) => {
921 ResponseStatus::Failed
922 }
923 };
924 Response::error(request_id, status, error.to_string())
925 }
926 other => Response::error(request_id, ResponseStatus::Failed, other.to_string()),
927 }
928}
929
930fn spawn_event_subscription(
931 subscription_id: u64,
932 registry: JobRegistryHandle,
933 outbound: mpsc::Sender<DcpFrame>,
934) -> JoinHandle<()> {
935 tokio::task::spawn_blocking(move || {
936 let Ok(queue) = registry.events().run_with(Sink::queue()) else {
937 return;
938 };
939 while let Ok(Some(event)) = queue.pull() {
940 let frame = DcpFrame::event(subscription_id, wire_event(&event));
941 if outbound.blocking_send(frame).is_err() {
942 break;
943 }
944 }
945 })
946}
947
948fn spawn_metrics_subscription(
949 subscription_id: u64,
950 instrumentation: StreamInstrumentationRegistry,
951 interval: Duration,
952 outbound: mpsc::Sender<DcpFrame>,
953) -> JoinHandle<()> {
954 tokio::spawn(async move {
955 let mut ticker = tokio::time::interval(interval);
956 loop {
957 ticker.tick().await;
958 let sample = MetricSample {
959 timestamp_ms: system_time_ms(SystemTime::now()),
960 streams: instrumentation
961 .snapshots()
962 .iter()
963 .map(wire_stream_metric)
964 .collect(),
965 };
966 let frame = DcpFrame::metric(subscription_id, sample);
967 match outbound.try_send(frame) {
968 Ok(()) => {}
969 Err(mpsc::error::TrySendError::Full(_)) => {}
970 Err(mpsc::error::TrySendError::Closed(_)) => break,
971 }
972 }
973 })
974}
975
976pub(crate) fn wire_job_status(status: &RegistryJobStatus) -> WireJobStatus {
977 let now = Instant::now();
978 let cluster = status.cluster.as_ref();
979 WireJobStatus {
980 name: status.name.clone(),
981 job_id: status.job_id.0,
982 state: format!("{:?}", status.state),
983 desired_state: format!("{:?}", status.desired_state),
984 generation: status.generation,
985 starts_total: status.starts_total,
986 restarts_total: status.restarts_total,
987 last_start_at_ms: status.last_start_at.map(system_time_ms),
988 last_exit_at_ms: status.last_exit_at.map(system_time_ms),
989 last_exit_reason: status
990 .last_exit_reason
991 .as_ref()
992 .map(exit_reason_text)
993 .unwrap_or_default(),
994 backoff_remaining_ms: status
995 .backoff_until
996 .map(|deadline| duration_ms(deadline.saturating_duration_since(now))),
997 drain_remaining_ms: status
998 .drain_deadline
999 .map(|deadline| duration_ms(deadline.saturating_duration_since(now))),
1000 drain_supported: status.drain_supported,
1001 active_streams: status.active_streams.map(|streams| streams as u64),
1002 cluster_job: cluster.is_some(),
1003 factory_name: cluster
1004 .map(|metadata| metadata.factory_name.clone())
1005 .unwrap_or_default(),
1006 placement: cluster.map(|metadata| wire_placement_spec(&metadata.placement)),
1007 coordinator_node_id: cluster
1008 .map(|metadata| metadata.coordinator_node.clone())
1009 .unwrap_or_default(),
1010 placement_node_id: cluster
1011 .map(|metadata| metadata.assigned_node.clone())
1012 .unwrap_or_default(),
1013 placement_generation: cluster
1014 .map(|metadata| metadata.placement_generation)
1015 .unwrap_or(0),
1016 placement_history: cluster
1017 .map(|metadata| {
1018 metadata
1019 .history
1020 .iter()
1021 .map(wire_placement_history)
1022 .collect()
1023 })
1024 .unwrap_or_default(),
1025 params: cluster
1026 .map(|metadata| metadata.params.clone().into_iter().collect())
1027 .unwrap_or_default(),
1028 }
1029}
1030
1031pub(crate) fn wire_placement_spec(spec: &RegistryPlacementSpec) -> PlacementSpec {
1032 let (strategy, pinned_node_id) = match &spec.strategy {
1033 RegistryPlacementStrategy::LeastJobs => (PlacementStrategy::LeastJobs, String::new()),
1034 RegistryPlacementStrategy::Pinned { node_id } => {
1035 (PlacementStrategy::Pinned, node_id.clone())
1036 }
1037 };
1038 PlacementSpec {
1039 role_constraint: spec.role_constraint.clone().unwrap_or_default(),
1040 strategy: strategy as i32,
1041 pinned_node_id,
1042 }
1043}
1044
1045pub(crate) fn placement_spec_from_wire(
1046 spec: Option<PlacementSpec>,
1047) -> DcpResult<RegistryPlacementSpec> {
1048 let spec = spec.unwrap_or(PlacementSpec {
1049 role_constraint: String::new(),
1050 strategy: PlacementStrategy::LeastJobs as i32,
1051 pinned_node_id: String::new(),
1052 });
1053 let role_constraint = normalize_optional_string(spec.role_constraint);
1054 let strategy =
1055 match PlacementStrategy::try_from(spec.strategy).unwrap_or(PlacementStrategy::LeastJobs) {
1056 PlacementStrategy::LeastJobs => RegistryPlacementStrategy::LeastJobs,
1057 PlacementStrategy::Pinned => {
1058 let node_id = spec.pinned_node_id.trim().to_owned();
1059 if node_id.is_empty() {
1060 return Err(DcpError::response(
1061 ResponseStatus::BadRequest,
1062 "Pinned placement requires pinned_node_id",
1063 ));
1064 }
1065 RegistryPlacementStrategy::Pinned { node_id }
1066 }
1067 };
1068 Ok(RegistryPlacementSpec {
1069 role_constraint,
1070 strategy,
1071 })
1072}
1073
1074fn wire_placement_history(
1075 history: &ClusterPlacementHistory,
1076) -> crate::dcp::proto::ClusterPlacementHistory {
1077 crate::dcp::proto::ClusterPlacementHistory {
1078 generation: history.generation,
1079 from_node_id: history.from_node.clone().unwrap_or_default(),
1080 to_node_id: history.to_node.clone(),
1081 reason: history.reason.clone(),
1082 timestamp_ms: system_time_ms(history.timestamp),
1083 }
1084}
1085
1086fn placement_history_from_wire(
1087 history: crate::dcp::proto::ClusterPlacementHistory,
1088) -> ClusterPlacementHistory {
1089 ClusterPlacementHistory {
1090 generation: history.generation,
1091 from_node: normalize_optional_string(history.from_node_id),
1092 to_node: history.to_node_id,
1093 reason: history.reason,
1094 timestamp: UNIX_EPOCH + Duration::from_millis(history.timestamp_ms),
1095 }
1096}
1097
1098pub(crate) fn cluster_metadata_from_wire(
1099 cluster: ClusterJobStart,
1100) -> DcpResult<ClusterJobMetadata> {
1101 Ok(ClusterJobMetadata {
1102 factory_name: cluster.factory_name,
1103 params: cluster.params.into_iter().collect::<BTreeMap<_, _>>(),
1104 placement: placement_spec_from_wire(cluster.placement)?,
1105 coordinator_node: cluster.coordinator_node_id,
1106 assigned_node: cluster.assigned_node_id,
1107 placement_generation: cluster.placement_generation,
1108 history: cluster
1109 .history
1110 .into_iter()
1111 .map(placement_history_from_wire)
1112 .collect(),
1113 })
1114}
1115
1116pub(crate) fn wire_cluster_job_start(metadata: &ClusterJobMetadata) -> ClusterJobStart {
1117 ClusterJobStart {
1118 factory_name: metadata.factory_name.clone(),
1119 params: metadata.params.clone().into_iter().collect(),
1120 placement: Some(wire_placement_spec(&metadata.placement)),
1121 coordinator_node_id: metadata.coordinator_node.clone(),
1122 assigned_node_id: metadata.assigned_node.clone(),
1123 placement_generation: metadata.placement_generation,
1124 history: metadata
1125 .history
1126 .iter()
1127 .map(wire_placement_history)
1128 .collect(),
1129 }
1130}
1131
1132fn wire_event(event: &JobEvent) -> Event {
1133 let (kind, detail) = event_kind_text(&event.kind);
1134 Event {
1135 sequence: event.sequence,
1136 timestamp_ms: system_time_ms(event.timestamp),
1137 name: event.name.clone(),
1138 job_id: event.job_id.0,
1139 generation: event.generation,
1140 kind,
1141 detail,
1142 }
1143}
1144
1145fn wire_stream_metric(snapshot: &StreamInstrumentationSnapshot) -> StreamMetric {
1146 StreamMetric {
1147 id: snapshot.id.get(),
1148 name: snapshot.name.clone(),
1149 elements_through: snapshot.elements_through,
1150 restarts: snapshot.restarts,
1151 state: instrumentation_state_text(snapshot.state).to_owned(),
1152 started_at_ms: system_time_ms(snapshot.started_at),
1153 state_changed_at_ms: system_time_ms(snapshot.state_changed_at),
1154 finished_at_ms: snapshot.finished_at.map(system_time_ms),
1155 uptime_ms: duration_ms(snapshot.uptime),
1156 }
1157}
1158
1159fn instrumentation_state_text(state: StreamInstrumentationState) -> &'static str {
1160 match state {
1161 StreamInstrumentationState::Running => "Running",
1162 StreamInstrumentationState::Draining => "Draining",
1163 StreamInstrumentationState::Completed => "Completed",
1164 StreamInstrumentationState::Failed => "Failed",
1165 }
1166}
1167
1168fn event_kind_text(kind: &JobEventKind) -> (String, String) {
1169 match kind {
1170 JobEventKind::Submitted => ("Submitted".to_owned(), String::new()),
1171 JobEventKind::Started => ("Started".to_owned(), String::new()),
1172 JobEventKind::Failed { reason } => ("Failed".to_owned(), exit_reason_text(reason)),
1173 JobEventKind::RestartScheduled { delay } => (
1174 "RestartScheduled".to_owned(),
1175 format!("delay_ms={}", duration_ms(*delay)),
1176 ),
1177 JobEventKind::Restarted {
1178 previous_generation,
1179 } => (
1180 "Restarted".to_owned(),
1181 format!("previous_generation={previous_generation}"),
1182 ),
1183 JobEventKind::Draining => ("Draining".to_owned(), String::new()),
1184 JobEventKind::Drained => ("Drained".to_owned(), String::new()),
1185 JobEventKind::Stopped { reason } => ("Stopped".to_owned(), exit_reason_text(reason)),
1186 JobEventKind::Completed => ("Completed".to_owned(), String::new()),
1187 }
1188}
1189
1190fn exit_reason_text(reason: &JobExitReason) -> String {
1191 match reason {
1192 JobExitReason::Completed => "Completed".to_owned(),
1193 JobExitReason::Failed(error) => format!("Failed({error})"),
1194 JobExitReason::Drained => "Drained".to_owned(),
1195 JobExitReason::Stopped => "Stopped".to_owned(),
1196 JobExitReason::DrainTimedOut => "DrainTimedOut".to_owned(),
1197 }
1198}
1199
1200fn system_time_ms(time: SystemTime) -> u64 {
1201 time.duration_since(UNIX_EPOCH)
1202 .map(duration_ms)
1203 .unwrap_or(0)
1204}
1205
1206fn duration_ms(duration: Duration) -> u64 {
1207 duration.as_millis().min(u128::from(u64::MAX)) as u64
1208}
1209
1210fn normalize_optional_string(value: String) -> Option<String> {
1211 let value = value.trim().to_owned();
1212 if value.is_empty() { None } else { Some(value) }
1213}
1214
1215fn ensure_loopback(addr: SocketAddr) -> DcpResult<()> {
1216 if addr.ip().is_loopback() {
1217 return Ok(());
1218 }
1219 Err(DcpError::Protocol(format!(
1220 "plaintext DCP TCP listener must bind loopback, got {addr}"
1221 )))
1222}
1223
1224fn client_bind_addr(remote_addr: SocketAddr) -> SocketAddr {
1225 if remote_addr.is_ipv6() {
1226 SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)
1227 } else {
1228 SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
1229 }
1230}
1231
1232pub(crate) async fn connect_quic_stream(
1233 addr: SocketAddr,
1234 server_name: &str,
1235 client_config: quinn::ClientConfig,
1236) -> DcpResult<(
1237 quinn::Endpoint,
1238 quinn::Connection,
1239 quinn::RecvStream,
1240 quinn::SendStream,
1241)> {
1242 let mut endpoint = quinn::Endpoint::client(client_bind_addr(addr))?;
1243 endpoint.set_default_client_config(client_config);
1244 let connection = endpoint
1245 .connect(addr, server_name)
1246 .map_err(|error| DcpError::Protocol(error.to_string()))?
1247 .await
1248 .map_err(|error| DcpError::Protocol(error.to_string()))?;
1249 let (send, recv) = connection
1250 .open_bi()
1251 .await
1252 .map_err(|error| DcpError::Protocol(error.to_string()))?;
1253 Ok((endpoint, connection, recv, send))
1254}