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