hatchery_node/
uplink_calls.rs1use crate::restream_session::{NodeSession, NodeUplink, UpstreamKeys};
14use hatchery_core::ChannelKind;
15use hatchery_enroll::NodeIdentity;
16use serde_json::Value;
17use std::net::SocketAddr;
18use std::sync::Arc;
19use std::time::{Duration, Instant};
20use tokio::sync::Mutex;
21
22pub const NODE_CALL_CHANNEL: u32 = 2;
24const HANDSHAKE: Duration = Duration::from_secs(8);
25const CALL_TIMEOUT: Duration = Duration::from_secs(15);
26const OPEN_BACKOFF: Duration = Duration::from_secs(60);
28
29#[derive(Default)]
30struct Link {
31 uplink: Option<Arc<NodeUplink>>,
32 session: Option<NodeSession>,
33 open_not_before: Option<Instant>,
35}
36
37#[derive(Clone)]
39pub struct NodeCalls {
40 link: Arc<Mutex<Link>>,
41 node_id: String,
42 keys: Option<UpstreamKeys>,
43 legacy: Option<(SocketAddr, Arc<NodeIdentity>)>,
44}
45
46#[derive(Debug)]
47pub enum CallError {
48 Refused,
49 Unavailable(String),
50}
51
52impl NodeCalls {
53 pub fn new(node_id: String, keys: Option<UpstreamKeys>, legacy: Option<(SocketAddr, Arc<NodeIdentity>)>) -> Self {
54 Self { link: Arc::default(), node_id, keys, legacy }
55 }
56
57 pub async fn set_uplink(&self, uplink: Option<Arc<NodeUplink>>) {
59 let mut l = self.link.lock().await;
60 l.uplink = uplink;
61 l.session = None;
62 l.open_not_before = None;
64 }
65
66 pub async fn call(&self, op: &str, payload: &Value) -> Result<Value, CallError> {
67 if let Some(keys) = self.keys {
68 let mut l = self.link.lock().await;
69 if let Some(uplink) = l.uplink.clone() {
70 if l.session.is_none() {
71 let gated = l.open_not_before.is_some_and(|t| Instant::now() < t);
72 if gated {
73 } else {
75 match uplink.open(&self.node_id, NODE_CALL_CHANNEL, ChannelKind::M4a, keys, HANDSHAKE).await {
76 Ok(s) => {
77 tracing::info!("node calls over the uplink: channel open");
78 l.session = Some(s);
79 l.open_not_before = None;
80 }
81 Err(e) => {
82 tracing::warn!(error = %e, "node-call channel open failed; backing off {}s", OPEN_BACKOFF.as_secs());
83 l.open_not_before = Some(Instant::now() + OPEN_BACKOFF);
84 }
85 }
86 }
87 }
88 if let Some(session) = l.session.as_mut() {
89 return match session.call(op, payload, CALL_TIMEOUT).await {
90 Ok(v) => Ok(v),
91 Err(e) if e.starts_with("403") => Err(CallError::Refused),
92 Err(e) if e.chars().take(3).all(|c| c.is_ascii_digit()) && e.contains(':') => {
93 Err(CallError::Unavailable(e))
95 }
96 Err(e) => {
97 l.session = None;
100 l.open_not_before = Some(Instant::now() + OPEN_BACKOFF);
101 Err(CallError::Unavailable(e))
102 }
103 };
104 }
105 }
106 }
107 match &self.legacy {
108 Some((c2, identity)) => match crate::enroll_client::node_call(*c2, identity, &self.node_id, op, payload).await {
109 Ok(v) => Ok(v),
110 Err(crate::enroll_client::EnrollClientError::Refused) => Err(CallError::Refused),
111 Err(e) => Err(CallError::Unavailable(format!("{e:?}"))),
112 },
113 None => Err(CallError::Unavailable("no uplink".into())),
114 }
115 }
116}