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