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
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}