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
341pub struct DcpServerHandle {
342 tcp_addr: Option<SocketAddr>,
343 quic_addr: Option<SocketAddr>,
344 shutdown: watch::Sender<bool>,
345 tasks: Vec<JoinHandle<()>>,
346}
347
348impl DcpServerHandle {
349 #[must_use]
350 pub fn tcp_addr(&self) -> Option<SocketAddr> {
351 self.tcp_addr
352 }
353
354 #[must_use]
355 pub fn quic_addr(&self) -> Option<SocketAddr> {
356 self.quic_addr
357 }
358
359 pub async fn shutdown(mut self) {
360 let _ = self.shutdown.send(true);
361 for task in self.tasks.drain(..) {
362 task.abort();
363 let _ = task.await;
364 }
365 }
366}
367
368impl Drop for DcpServerHandle {
369 fn drop(&mut self) {
370 let _ = self.shutdown.send(true);
371 for task in &self.tasks {
372 task.abort();
373 }
374 }
375}
376
377async fn run_tcp_listener(
378 listener: TcpListener,
379 state: Arc<DcpServerState>,
380 mut shutdown: watch::Receiver<bool>,
381) {
382 loop {
383 tokio::select! {
384 changed = shutdown.changed() => {
385 if changed.is_err() || *shutdown.borrow() {
386 break;
387 }
388 }
389 accepted = listener.accept() => {
390 let Ok((stream, _peer)) = accepted else {
391 break;
392 };
393 let state = Arc::clone(&state);
394 tokio::spawn(async move {
395 let _ = run_tcp_connection(stream, state).await;
396 });
397 }
398 }
399 }
400}
401
402async fn run_tcp_connection(stream: TcpStream, state: Arc<DcpServerState>) -> DcpResult<()> {
403 stream.set_nodelay(true)?;
404 let (reader, writer) = stream.into_split();
405 run_connection(reader, writer, state).await
406}
407
408async fn run_quic_listener(
409 endpoint: quinn::Endpoint,
410 state: Arc<DcpServerState>,
411 mut shutdown: watch::Receiver<bool>,
412) {
413 loop {
414 tokio::select! {
415 changed = shutdown.changed() => {
416 if changed.is_err() || *shutdown.borrow() {
417 endpoint.close(quinn::VarInt::from_u32(0), b"DCP shutdown");
418 break;
419 }
420 }
421 incoming = endpoint.accept() => {
422 let Some(incoming) = incoming else {
423 break;
424 };
425 let state = Arc::clone(&state);
426 let shutdown = shutdown.clone();
427 tokio::spawn(async move {
428 if let Ok(connection) = incoming.await {
429 run_quic_connection(connection, state, shutdown).await;
430 }
431 });
432 }
433 }
434 }
435}
436
437async fn run_quic_connection(
438 connection: quinn::Connection,
439 state: Arc<DcpServerState>,
440 mut shutdown: watch::Receiver<bool>,
441) {
442 loop {
443 tokio::select! {
444 changed = shutdown.changed() => {
445 if changed.is_err() || *shutdown.borrow() {
446 connection.close(quinn::VarInt::from_u32(0), b"DCP shutdown");
447 break;
448 }
449 }
450 accepted = connection.accept_bi() => {
451 let Ok((send, recv)) = accepted else {
452 break;
453 };
454 let state = Arc::clone(&state);
455 tokio::spawn(async move {
456 let _ = run_connection(recv, send, state).await;
457 });
458 }
459 }
460 }
461}
462
463async fn run_connection<R, W>(mut reader: R, writer: W, state: Arc<DcpServerState>) -> DcpResult<()>
464where
465 R: AsyncRead + Unpin + Send + 'static,
466 W: AsyncWrite + Unpin + Send + 'static,
467{
468 let (outbound, outbound_receiver) = mpsc::channel(state.config.frame_buffer.max(1));
469 let writer_task = tokio::spawn(write_loop(writer, outbound_receiver));
470 let mut subscriptions = Vec::new();
471
472 let Some(first) = read_frame(&mut reader).await? else {
473 return Err(DcpError::Closed);
474 };
475 let hello = match first.frame {
476 Some(dcp_frame::Frame::Hello(hello)) => hello,
477 _ => {
478 let response =
479 Response::error(0, ResponseStatus::BadRequest, "first frame must be Hello");
480 send_frame(&outbound, DcpFrame::response(response)).await?;
481 return Err(DcpError::Protocol("first frame must be Hello".to_owned()));
482 }
483 };
484
485 let hello_response = negotiate_hello(&state, &hello);
486 let accepted = hello_response.response_status() == ResponseStatus::Ok;
487 send_frame(&outbound, DcpFrame::response(hello_response)).await?;
488 if !accepted {
489 return Ok(());
490 }
491
492 while let Some(frame) = read_frame(&mut reader).await? {
493 let request = match frame.frame {
494 Some(dcp_frame::Frame::Request(request)) => request,
495 _ => {
496 return Err(DcpError::Protocol(
497 "client sent non-request frame after hello".to_owned(),
498 ));
499 }
500 };
501 if matches!(&request.command, Some(request::Command::OpenShardPipe(_))) {
502 send_frame(
503 &outbound,
504 DcpFrame::response(Response::ok(request.request_id, Vec::new())),
505 )
506 .await?;
507 run_shard_pipe_connection(reader, outbound.clone(), Arc::clone(&state)).await?;
508 break;
509 }
510 let (response, subscription) =
511 dispatch_request(Arc::clone(&state), request, outbound.clone()).await;
512 send_frame(&outbound, DcpFrame::response(response)).await?;
513 if let Some(subscription) = subscription {
514 subscriptions.push(subscription);
515 }
516 }
517
518 for subscription in subscriptions {
519 subscription.abort();
520 }
521 drop(outbound);
522 writer_task.await??;
523 Ok(())
524}
525
526async fn run_shard_pipe_connection<R>(
527 mut reader: R,
528 outbound: mpsc::Sender<DcpFrame>,
529 state: Arc<DcpServerState>,
530) -> DcpResult<()>
531where
532 R: AsyncRead + Unpin,
533{
534 while let Some(frame) = read_frame(&mut reader).await? {
535 let pipe = match frame.frame {
536 Some(dcp_frame::Frame::ShardPipe(pipe)) => pipe,
537 _ => {
538 return Err(DcpError::Protocol(
539 "shard pipe connection received a non-pipe frame".to_owned(),
540 ));
541 }
542 };
543 dispatch_shard_pipe(Arc::clone(&state), pipe).await?;
544 }
545 drop(outbound);
546 Ok(())
547}
548
549async fn dispatch_shard_pipe(state: Arc<DcpServerState>, pipe: ShardPipeFrame) -> DcpResult<()> {
550 let provider = sharding_view_provider(&state)?;
551 for batch in pipe.forwards {
552 provider
553 .forward_shard_envelopes(batch, DEFAULT_CLUSTER_REQUEST_TIMEOUT)
554 .await?;
555 }
556 for reply in pipe.replies {
557 provider.complete_sharding_ask(reply).await?;
558 }
559 Ok(())
560}
561
562async fn write_loop<W>(mut writer: W, mut outbound: mpsc::Receiver<DcpFrame>) -> DcpResult<()>
563where
564 W: AsyncWrite + Unpin,
565{
566 while let Some(frame) = outbound.recv().await {
567 write_frame(&mut writer, &frame).await?;
568 }
569 let _ = writer.shutdown().await;
570 Ok(())
571}
572
573async fn send_frame(sender: &mpsc::Sender<DcpFrame>, frame: DcpFrame) -> DcpResult<()> {
574 sender.send(frame).await.map_err(|_| DcpError::Closed)
575}
576
577fn negotiate_hello(state: &DcpServerState, hello: &Hello) -> Response {
578 match protocol_major(&hello.protocol_version) {
579 Some(DCP_PROTOCOL_MAJOR) => {}
580 Some(other) => {
581 return Response::error(
582 0,
583 ResponseStatus::ProtocolMismatch,
584 format!("unsupported DCP major version: {other}"),
585 );
586 }
587 None => {
588 return Response::error(
589 0,
590 ResponseStatus::ProtocolMismatch,
591 format!("invalid DCP protocol version: {}", hello.protocol_version),
592 );
593 }
594 }
595
596 if let Some(expected) = &state.config.auth_token {
597 let actual = hello
598 .auth
599 .as_ref()
600 .map(|auth| auth.bearer_token.as_str())
601 .unwrap_or_default();
602 if actual != expected {
603 return Response::error(0, ResponseStatus::Unauthorized, "invalid DCP token");
604 }
605 }
606
607 Response::ok(0, state.config.node_id.as_bytes().to_vec())
608}
609
610fn protocol_major(version: &str) -> Option<u32> {
611 version.split('.').next()?.parse().ok()
612}
613
614async fn dispatch_request(
615 state: Arc<DcpServerState>,
616 request: Request,
617 outbound: mpsc::Sender<DcpFrame>,
618) -> (Response, Option<JoinHandle<()>>) {
619 let request_id = request.request_id;
620 let deadline = request.deadline_ms;
621 let dispatch = async {
622 let command = request.command.ok_or_else(|| {
623 DcpError::response(ResponseStatus::BadRequest, "request missing command")
624 })?;
625 dispatch_command(state, request_id, command, outbound).await
626 };
627
628 let result = if deadline == 0 {
629 dispatch.await
630 } else {
631 match tokio::time::timeout(Duration::from_millis(deadline), dispatch).await {
632 Ok(result) => result,
633 Err(_) => Err(DcpError::response(
634 ResponseStatus::DeadlineExceeded,
635 "request deadline exceeded",
636 )),
637 }
638 };
639
640 match result {
641 Ok(result) => result,
642 Err(error) => (response_for_error(request_id, error), None),
643 }
644}
645
646async fn dispatch_command(
647 state: Arc<DcpServerState>,
648 request_id: u64,
649 command: request::Command,
650 outbound: mpsc::Sender<DcpFrame>,
651) -> DcpResult<(Response, Option<JoinHandle<()>>)> {
652 match command {
653 request::Command::ListJobs(_) => {
654 let registry = state.registry.clone();
655 let jobs = registry_call(move || registry.list()).await?;
656 let payload = JobList {
657 jobs: jobs.iter().map(wire_job_status).collect(),
658 }
659 .encode_to_vec();
660 Ok((Response::ok(request_id, payload), None))
661 }
662 request::Command::StartJob(start) => {
663 if start.factory_name.trim().is_empty() || start.instance_name.trim().is_empty() {
664 return Err(DcpError::response(
665 ResponseStatus::BadRequest,
666 "StartJob requires factory_name and instance_name",
667 ));
668 }
669 let mut spec = state.factories.build(
670 &start.factory_name,
671 start.instance_name.clone(),
672 start.params,
673 )?;
674 if let Some(cluster) = start.cluster {
675 spec = spec.with_cluster_metadata(cluster_metadata_from_wire(cluster)?);
676 }
677 let registry = state.registry.clone();
678 let name = spec.name.clone();
679 let status = registry_call(move || {
680 registry.submit(spec)?;
681 registry.start(name)
682 })
683 .await?;
684 Ok((status_response(request_id, &status), None))
685 }
686 request::Command::DrainJob(drain) => {
687 if drain.cluster {
688 let provider = cluster_view_provider(&state)?;
689 let timeout = request_timeout(0);
690 let payload = provider
691 .drain_cluster_job(drain.name, timeout)
692 .await?
693 .encode_to_vec();
694 return Ok((Response::ok(request_id, payload), None));
695 }
696 let registry = state.registry.clone();
697 let status = registry_call(move || registry.drain(drain.name)).await?;
698 Ok((status_response(request_id, &status), None))
699 }
700 request::Command::StopJob(stop) => {
701 if stop.cluster {
702 let provider = cluster_view_provider(&state)?;
703 let timeout = request_timeout(0);
704 let payload = provider
705 .stop_cluster_job(stop.name, timeout)
706 .await?
707 .encode_to_vec();
708 return Ok((Response::ok(request_id, payload), None));
709 }
710 let registry = state.registry.clone();
711 let status = registry_call(move || registry.stop(stop.name)).await?;
712 Ok((status_response(request_id, &status), None))
713 }
714 request::Command::RestartJob(restart) => {
715 if restart.cluster {
716 return Err(DcpError::response(
717 ResponseStatus::BadRequest,
718 "cluster restart is not implemented in v0.10; drain/stop and submit again",
719 ));
720 }
721 let registry = state.registry.clone();
722 let status = registry_call(move || registry.restart(restart.name)).await?;
723 Ok((status_response(request_id, &status), None))
724 }
725 request::Command::JobStatus(status) => {
726 if status.cluster {
727 let provider = cluster_view_provider(&state)?;
728 let timeout = request_timeout(0);
729 let payload = provider
730 .cluster_job_status(status.name, timeout)
731 .await?
732 .encode_to_vec();
733 return Ok((Response::ok(request_id, payload), None));
734 }
735 let registry = state.registry.clone();
736 let status = registry_call(move || registry.status(status.name)).await?;
737 Ok((status_response(request_id, &status), None))
738 }
739 request::Command::SubscribeEvents(_) => {
740 let registry = state.registry.clone();
741 let subscription = spawn_event_subscription(request_id, registry, outbound);
742 Ok((Response::ok(request_id, Vec::new()), Some(subscription)))
743 }
744 request::Command::SubscribeMetrics(metrics) => {
745 let interval = if metrics.interval_ms == 0 {
746 state.config.metrics_interval
747 } else {
748 Duration::from_millis(metrics.interval_ms)
749 };
750 let subscription = spawn_metrics_subscription(
751 request_id,
752 state.instrumentation.clone(),
753 interval.max(Duration::from_millis(1)),
754 outbound,
755 );
756 Ok((Response::ok(request_id, Vec::new()), Some(subscription)))
757 }
758 request::Command::GetConfig(get) => {
759 let value = state
760 .config_store
761 .lock()
762 .expect("DCP config store poisoned")
763 .get(&get.key)
764 .cloned();
765 let payload = ConfigValue {
766 key: get.key,
767 value: value.clone().unwrap_or_default(),
768 existed: value.is_some(),
769 }
770 .encode_to_vec();
771 Ok((Response::ok(request_id, payload), None))
772 }
773 request::Command::PutConfig(put) => {
774 let existed = state
775 .config_store
776 .lock()
777 .expect("DCP config store poisoned")
778 .insert(put.key.clone(), put.value.clone())
779 .is_some();
780 let payload = ConfigValue {
781 key: put.key,
782 value: put.value,
783 existed,
784 }
785 .encode_to_vec();
786 Ok((Response::ok(request_id, payload), None))
787 }
788 request::Command::ListClusterJobs(list) => {
789 let provider = cluster_view_provider(&state)?;
790 let timeout = request_timeout(list.timeout_ms);
791 let payload = provider.list_cluster_jobs(timeout).await?.encode_to_vec();
792 Ok((Response::ok(request_id, payload), None))
793 }
794 request::Command::ClusterNodeInfo(info) => {
795 let provider = cluster_view_provider(&state)?;
796 let timeout = request_timeout(info.timeout_ms);
797 let payload = provider.cluster_node_info(timeout).await?.encode_to_vec();
798 Ok((Response::ok(request_id, payload), None))
799 }
800 request::Command::SubmitClusterJob(submit) => {
801 let provider = cluster_view_provider(&state)?;
802 let timeout = request_timeout(submit.timeout_ms);
803 let payload = provider
804 .submit_cluster_job(submit, timeout)
805 .await?
806 .encode_to_vec();
807 Ok((Response::ok(request_id, payload), None))
808 }
809 request::Command::RememberClusterAssignment(remember) => {
810 let provider = cluster_view_provider(&state)?;
811 provider.remember_cluster_assignment(remember).await?;
812 Ok((Response::ok(request_id, Vec::new()), None))
813 }
814 request::Command::AllocateShard(allocate) => {
815 let provider = sharding_view_provider(&state)?;
816 let timeout = request_timeout(allocate.timeout_ms);
817 let payload = provider
818 .allocate_shard(allocate, timeout)
819 .await?
820 .encode_to_vec();
821 Ok((Response::ok(request_id, payload), None))
822 }
823 request::Command::RememberShardAllocations(remember) => {
824 let provider = sharding_view_provider(&state)?;
825 provider.remember_shard_allocations(remember).await?;
826 Ok((Response::ok(request_id, Vec::new()), None))
827 }
828 request::Command::GetShardAllocations(get) => {
829 let provider = sharding_view_provider(&state)?;
830 let timeout = request_timeout(0);
831 let payload = provider
832 .get_shard_allocations(get.type_name, timeout)
833 .await?
834 .encode_to_vec();
835 Ok((Response::ok(request_id, payload), None))
836 }
837 request::Command::ForwardShardEnvelopes(batch) => {
838 let provider = sharding_view_provider(&state)?;
839 let timeout = request_timeout(0);
840 let payload = provider
841 .forward_shard_envelopes(batch, timeout)
842 .await?
843 .encode_to_vec();
844 Ok((Response::ok(request_id, payload), None))
845 }
846 request::Command::CompleteShardingAsk(reply) => {
847 let provider = sharding_view_provider(&state)?;
848 provider.complete_sharding_ask(reply).await?;
849 Ok((Response::ok(request_id, Vec::new()), None))
850 }
851 request::Command::OpenShardPipe(_) => Err(DcpError::response(
852 ResponseStatus::BadRequest,
853 "OpenShardPipe must be handled by the DCP connection loop",
854 )),
855 }
856}
857
858fn cluster_view_provider(state: &DcpServerState) -> DcpResult<Arc<dyn ClusterViewProvider>> {
859 state
860 .cluster_view
861 .lock()
862 .expect("DCP cluster view provider poisoned")
863 .clone()
864 .ok_or_else(|| {
865 DcpError::response(
866 ResponseStatus::Failed,
867 "cluster view is not configured on this datum-agent",
868 )
869 })
870}
871
872fn sharding_view_provider(state: &DcpServerState) -> DcpResult<Arc<dyn ShardingViewProvider>> {
873 state
874 .sharding_view
875 .lock()
876 .expect("DCP sharding view provider poisoned")
877 .clone()
878 .ok_or_else(|| {
879 DcpError::response(
880 ResponseStatus::Failed,
881 "cluster sharding is not configured on this datum-agent",
882 )
883 })
884}
885
886fn request_timeout(timeout_ms: u64) -> Duration {
887 if timeout_ms == 0 {
888 DEFAULT_CLUSTER_REQUEST_TIMEOUT
889 } else {
890 Duration::from_millis(timeout_ms)
891 }
892}
893
894async fn registry_call<T, F>(call: F) -> DcpResult<T>
895where
896 T: Send + 'static,
897 F: FnOnce() -> AgentResult<T> + Send + 'static,
898{
899 Ok(tokio::task::spawn_blocking(call).await??)
900}
901
902fn status_response(request_id: u64, status: &RegistryJobStatus) -> Response {
903 Response::ok(request_id, wire_job_status(status).encode_to_vec())
904}
905
906fn response_for_error(request_id: u64, error: DcpError) -> Response {
907 match error {
908 DcpError::Response { status, message } => Response::error(request_id, status, message),
909 DcpError::Agent(error) => {
910 let status = match &error {
911 AgentError::InvalidJobName => ResponseStatus::BadRequest,
912 AgentError::JobNotFound(_) => ResponseStatus::NotFound,
913 AgentError::JobAlreadyExists(_) | AgentError::JobAlreadyRunning(_) => {
914 ResponseStatus::Conflict
915 }
916 AgentError::DrainUnsupported(_)
917 | AgentError::JobNotRunning(_)
918 | AgentError::RestartLimitExceeded(_) => ResponseStatus::Failed,
919 AgentError::RegistryClosed | AgentError::Actor(_) | AgentError::Stream(_) => {
920 ResponseStatus::Failed
921 }
922 };
923 Response::error(request_id, status, error.to_string())
924 }
925 other => Response::error(request_id, ResponseStatus::Failed, other.to_string()),
926 }
927}
928
929fn spawn_event_subscription(
930 subscription_id: u64,
931 registry: JobRegistryHandle,
932 outbound: mpsc::Sender<DcpFrame>,
933) -> JoinHandle<()> {
934 tokio::task::spawn_blocking(move || {
935 let Ok(queue) = registry.events().run_with(Sink::queue()) else {
936 return;
937 };
938 while let Ok(Some(event)) = queue.pull() {
939 let frame = DcpFrame::event(subscription_id, wire_event(&event));
940 if outbound.blocking_send(frame).is_err() {
941 break;
942 }
943 }
944 })
945}
946
947fn spawn_metrics_subscription(
948 subscription_id: u64,
949 instrumentation: StreamInstrumentationRegistry,
950 interval: Duration,
951 outbound: mpsc::Sender<DcpFrame>,
952) -> JoinHandle<()> {
953 tokio::spawn(async move {
954 let mut ticker = tokio::time::interval(interval);
955 loop {
956 ticker.tick().await;
957 let sample = MetricSample {
958 timestamp_ms: system_time_ms(SystemTime::now()),
959 streams: instrumentation
960 .snapshots()
961 .iter()
962 .map(wire_stream_metric)
963 .collect(),
964 };
965 let frame = DcpFrame::metric(subscription_id, sample);
966 match outbound.try_send(frame) {
967 Ok(()) => {}
968 Err(mpsc::error::TrySendError::Full(_)) => {}
969 Err(mpsc::error::TrySendError::Closed(_)) => break,
970 }
971 }
972 })
973}
974
975pub(crate) fn wire_job_status(status: &RegistryJobStatus) -> WireJobStatus {
976 let now = Instant::now();
977 let cluster = status.cluster.as_ref();
978 WireJobStatus {
979 name: status.name.clone(),
980 job_id: status.job_id.0,
981 state: format!("{:?}", status.state),
982 desired_state: format!("{:?}", status.desired_state),
983 generation: status.generation,
984 starts_total: status.starts_total,
985 restarts_total: status.restarts_total,
986 last_start_at_ms: status.last_start_at.map(system_time_ms),
987 last_exit_at_ms: status.last_exit_at.map(system_time_ms),
988 last_exit_reason: status
989 .last_exit_reason
990 .as_ref()
991 .map(exit_reason_text)
992 .unwrap_or_default(),
993 backoff_remaining_ms: status
994 .backoff_until
995 .map(|deadline| duration_ms(deadline.saturating_duration_since(now))),
996 drain_remaining_ms: status
997 .drain_deadline
998 .map(|deadline| duration_ms(deadline.saturating_duration_since(now))),
999 drain_supported: status.drain_supported,
1000 active_streams: status.active_streams.map(|streams| streams as u64),
1001 cluster_job: cluster.is_some(),
1002 factory_name: cluster
1003 .map(|metadata| metadata.factory_name.clone())
1004 .unwrap_or_default(),
1005 placement: cluster.map(|metadata| wire_placement_spec(&metadata.placement)),
1006 coordinator_node_id: cluster
1007 .map(|metadata| metadata.coordinator_node.clone())
1008 .unwrap_or_default(),
1009 placement_node_id: cluster
1010 .map(|metadata| metadata.assigned_node.clone())
1011 .unwrap_or_default(),
1012 placement_generation: cluster
1013 .map(|metadata| metadata.placement_generation)
1014 .unwrap_or(0),
1015 placement_history: cluster
1016 .map(|metadata| {
1017 metadata
1018 .history
1019 .iter()
1020 .map(wire_placement_history)
1021 .collect()
1022 })
1023 .unwrap_or_default(),
1024 params: cluster
1025 .map(|metadata| metadata.params.clone().into_iter().collect())
1026 .unwrap_or_default(),
1027 }
1028}
1029
1030pub(crate) fn wire_placement_spec(spec: &RegistryPlacementSpec) -> PlacementSpec {
1031 let (strategy, pinned_node_id) = match &spec.strategy {
1032 RegistryPlacementStrategy::LeastJobs => (PlacementStrategy::LeastJobs, String::new()),
1033 RegistryPlacementStrategy::Pinned { node_id } => {
1034 (PlacementStrategy::Pinned, node_id.clone())
1035 }
1036 };
1037 PlacementSpec {
1038 role_constraint: spec.role_constraint.clone().unwrap_or_default(),
1039 strategy: strategy as i32,
1040 pinned_node_id,
1041 }
1042}
1043
1044pub(crate) fn placement_spec_from_wire(
1045 spec: Option<PlacementSpec>,
1046) -> DcpResult<RegistryPlacementSpec> {
1047 let spec = spec.unwrap_or(PlacementSpec {
1048 role_constraint: String::new(),
1049 strategy: PlacementStrategy::LeastJobs as i32,
1050 pinned_node_id: String::new(),
1051 });
1052 let role_constraint = normalize_optional_string(spec.role_constraint);
1053 let strategy =
1054 match PlacementStrategy::try_from(spec.strategy).unwrap_or(PlacementStrategy::LeastJobs) {
1055 PlacementStrategy::LeastJobs => RegistryPlacementStrategy::LeastJobs,
1056 PlacementStrategy::Pinned => {
1057 let node_id = spec.pinned_node_id.trim().to_owned();
1058 if node_id.is_empty() {
1059 return Err(DcpError::response(
1060 ResponseStatus::BadRequest,
1061 "Pinned placement requires pinned_node_id",
1062 ));
1063 }
1064 RegistryPlacementStrategy::Pinned { node_id }
1065 }
1066 };
1067 Ok(RegistryPlacementSpec {
1068 role_constraint,
1069 strategy,
1070 })
1071}
1072
1073fn wire_placement_history(
1074 history: &ClusterPlacementHistory,
1075) -> crate::dcp::proto::ClusterPlacementHistory {
1076 crate::dcp::proto::ClusterPlacementHistory {
1077 generation: history.generation,
1078 from_node_id: history.from_node.clone().unwrap_or_default(),
1079 to_node_id: history.to_node.clone(),
1080 reason: history.reason.clone(),
1081 timestamp_ms: system_time_ms(history.timestamp),
1082 }
1083}
1084
1085fn placement_history_from_wire(
1086 history: crate::dcp::proto::ClusterPlacementHistory,
1087) -> ClusterPlacementHistory {
1088 ClusterPlacementHistory {
1089 generation: history.generation,
1090 from_node: normalize_optional_string(history.from_node_id),
1091 to_node: history.to_node_id,
1092 reason: history.reason,
1093 timestamp: UNIX_EPOCH + Duration::from_millis(history.timestamp_ms),
1094 }
1095}
1096
1097pub(crate) fn cluster_metadata_from_wire(
1098 cluster: ClusterJobStart,
1099) -> DcpResult<ClusterJobMetadata> {
1100 Ok(ClusterJobMetadata {
1101 factory_name: cluster.factory_name,
1102 params: cluster.params.into_iter().collect::<BTreeMap<_, _>>(),
1103 placement: placement_spec_from_wire(cluster.placement)?,
1104 coordinator_node: cluster.coordinator_node_id,
1105 assigned_node: cluster.assigned_node_id,
1106 placement_generation: cluster.placement_generation,
1107 history: cluster
1108 .history
1109 .into_iter()
1110 .map(placement_history_from_wire)
1111 .collect(),
1112 })
1113}
1114
1115pub(crate) fn wire_cluster_job_start(metadata: &ClusterJobMetadata) -> ClusterJobStart {
1116 ClusterJobStart {
1117 factory_name: metadata.factory_name.clone(),
1118 params: metadata.params.clone().into_iter().collect(),
1119 placement: Some(wire_placement_spec(&metadata.placement)),
1120 coordinator_node_id: metadata.coordinator_node.clone(),
1121 assigned_node_id: metadata.assigned_node.clone(),
1122 placement_generation: metadata.placement_generation,
1123 history: metadata
1124 .history
1125 .iter()
1126 .map(wire_placement_history)
1127 .collect(),
1128 }
1129}
1130
1131fn wire_event(event: &JobEvent) -> Event {
1132 let (kind, detail) = event_kind_text(&event.kind);
1133 Event {
1134 sequence: event.sequence,
1135 timestamp_ms: system_time_ms(event.timestamp),
1136 name: event.name.clone(),
1137 job_id: event.job_id.0,
1138 generation: event.generation,
1139 kind,
1140 detail,
1141 }
1142}
1143
1144fn wire_stream_metric(snapshot: &StreamInstrumentationSnapshot) -> StreamMetric {
1145 StreamMetric {
1146 id: snapshot.id.get(),
1147 name: snapshot.name.clone(),
1148 elements_through: snapshot.elements_through,
1149 restarts: snapshot.restarts,
1150 state: instrumentation_state_text(snapshot.state).to_owned(),
1151 started_at_ms: system_time_ms(snapshot.started_at),
1152 state_changed_at_ms: system_time_ms(snapshot.state_changed_at),
1153 finished_at_ms: snapshot.finished_at.map(system_time_ms),
1154 uptime_ms: duration_ms(snapshot.uptime),
1155 }
1156}
1157
1158fn instrumentation_state_text(state: StreamInstrumentationState) -> &'static str {
1159 match state {
1160 StreamInstrumentationState::Running => "Running",
1161 StreamInstrumentationState::Draining => "Draining",
1162 StreamInstrumentationState::Completed => "Completed",
1163 StreamInstrumentationState::Failed => "Failed",
1164 }
1165}
1166
1167fn event_kind_text(kind: &JobEventKind) -> (String, String) {
1168 match kind {
1169 JobEventKind::Submitted => ("Submitted".to_owned(), String::new()),
1170 JobEventKind::Started => ("Started".to_owned(), String::new()),
1171 JobEventKind::Failed { reason } => ("Failed".to_owned(), exit_reason_text(reason)),
1172 JobEventKind::RestartScheduled { delay } => (
1173 "RestartScheduled".to_owned(),
1174 format!("delay_ms={}", duration_ms(*delay)),
1175 ),
1176 JobEventKind::Restarted {
1177 previous_generation,
1178 } => (
1179 "Restarted".to_owned(),
1180 format!("previous_generation={previous_generation}"),
1181 ),
1182 JobEventKind::Draining => ("Draining".to_owned(), String::new()),
1183 JobEventKind::Drained => ("Drained".to_owned(), String::new()),
1184 JobEventKind::Stopped { reason } => ("Stopped".to_owned(), exit_reason_text(reason)),
1185 JobEventKind::Completed => ("Completed".to_owned(), String::new()),
1186 }
1187}
1188
1189fn exit_reason_text(reason: &JobExitReason) -> String {
1190 match reason {
1191 JobExitReason::Completed => "Completed".to_owned(),
1192 JobExitReason::Failed(error) => format!("Failed({error})"),
1193 JobExitReason::Drained => "Drained".to_owned(),
1194 JobExitReason::Stopped => "Stopped".to_owned(),
1195 JobExitReason::DrainTimedOut => "DrainTimedOut".to_owned(),
1196 }
1197}
1198
1199fn system_time_ms(time: SystemTime) -> u64 {
1200 time.duration_since(UNIX_EPOCH)
1201 .map(duration_ms)
1202 .unwrap_or(0)
1203}
1204
1205fn duration_ms(duration: Duration) -> u64 {
1206 duration.as_millis().min(u128::from(u64::MAX)) as u64
1207}
1208
1209fn normalize_optional_string(value: String) -> Option<String> {
1210 let value = value.trim().to_owned();
1211 if value.is_empty() { None } else { Some(value) }
1212}
1213
1214fn ensure_loopback(addr: SocketAddr) -> DcpResult<()> {
1215 if addr.ip().is_loopback() {
1216 return Ok(());
1217 }
1218 Err(DcpError::Protocol(format!(
1219 "plaintext DCP TCP listener must bind loopback, got {addr}"
1220 )))
1221}
1222
1223fn client_bind_addr(remote_addr: SocketAddr) -> SocketAddr {
1224 if remote_addr.is_ipv6() {
1225 SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)
1226 } else {
1227 SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
1228 }
1229}
1230
1231pub(crate) async fn connect_quic_stream(
1232 addr: SocketAddr,
1233 server_name: &str,
1234 client_config: quinn::ClientConfig,
1235) -> DcpResult<(
1236 quinn::Endpoint,
1237 quinn::Connection,
1238 quinn::RecvStream,
1239 quinn::SendStream,
1240)> {
1241 let mut endpoint = quinn::Endpoint::client(client_bind_addr(addr))?;
1242 endpoint.set_default_client_config(client_config);
1243 let connection = endpoint
1244 .connect(addr, server_name)
1245 .map_err(|error| DcpError::Protocol(error.to_string()))?
1246 .await
1247 .map_err(|error| DcpError::Protocol(error.to_string()))?;
1248 let (send, recv) = connection
1249 .open_bi()
1250 .await
1251 .map_err(|error| DcpError::Protocol(error.to_string()))?;
1252 Ok((endpoint, connection, recv, send))
1253}