Skip to main content

datum_agent/dcp/
server.rs

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
46/// Provider installed by cluster-aware agents to serve read-only cluster DCP
47/// requests.
48pub trait ClusterViewProvider: Send + Sync + 'static {
49    /// Submit a cluster-scoped job to the deterministic placement coordinator.
50    fn submit_cluster_job(
51        &self,
52        request: SubmitClusterJob,
53        timeout: Duration,
54    ) -> ClusterViewFuture<'_, WireJobStatus>;
55
56    /// Aggregate job snapshots from local and peer nodes.
57    fn list_cluster_jobs(&self, timeout: Duration) -> ClusterViewFuture<'_, ClusterJobList>;
58
59    /// Return the local cluster membership and DCP node-session view.
60    fn cluster_node_info(&self, timeout: Duration) -> ClusterViewFuture<'_, ClusterNodeList>;
61
62    /// Return status for a cluster job, routed through the coordinator table.
63    fn cluster_job_status(
64        &self,
65        name: String,
66        timeout: Duration,
67    ) -> ClusterViewFuture<'_, WireJobStatus>;
68
69    /// Drain a cluster job on its current placement node.
70    fn drain_cluster_job(
71        &self,
72        name: String,
73        timeout: Duration,
74    ) -> ClusterViewFuture<'_, WireJobStatus>;
75
76    /// Stop a cluster job on its current placement node.
77    fn stop_cluster_job(
78        &self,
79        name: String,
80        timeout: Duration,
81    ) -> ClusterViewFuture<'_, WireJobStatus>;
82
83    /// Remember an assignment replicated by the active coordinator.
84    fn remember_cluster_assignment(
85        &self,
86        request: RememberClusterAssignment,
87    ) -> ClusterViewFuture<'_, ()>;
88}
89
90/// Provider installed by cluster sharding to serve shard allocation, region
91/// forwarding, and remote ask replies over the existing node-to-node DCP
92/// sessions.
93pub trait ShardingViewProvider: Send + Sync + 'static {
94    /// Allocate or return the cached owner for one shard. Only the deterministic
95    /// coordinator should accept this request.
96    fn allocate_shard(
97        &self,
98        request: ShardAllocationRequest,
99        timeout: Duration,
100    ) -> ClusterViewFuture<'_, ShardAllocation>;
101
102    /// Remember allocation-table rows replicated by the active coordinator.
103    fn remember_shard_allocations(
104        &self,
105        request: RememberShardAllocations,
106    ) -> ClusterViewFuture<'_, ()>;
107
108    /// Return this region's cached allocation table for coordinator takeover.
109    fn get_shard_allocations(
110        &self,
111        type_name: String,
112        timeout: Duration,
113    ) -> ClusterViewFuture<'_, ShardAllocationTable>;
114
115    /// Enqueue a batch of serialized envelopes at this region.
116    fn forward_shard_envelopes(
117        &self,
118        request: ForwardShardEnvelopes,
119        timeout: Duration,
120    ) -> ClusterViewFuture<'_, ShardEnvelopeBatchResult>;
121
122    /// Complete an ask whose reply port originated on this region.
123    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    /// Install or replace the provider used for cluster-wide read-only DCP
261    /// requests.
262    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    /// Remove the cluster view provider. Cluster DCP requests will fail until a
271    /// provider is installed again.
272    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    /// Install or replace the provider used for cluster-sharding DCP requests.
281    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    /// Remove the sharding provider. Sharding DCP requests will fail until a
290    /// provider is installed again.
291    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}