1use std::future::Future;
5use std::pin::pin;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::task::{Context, Poll, Waker};
8
9use liminal_sdk::{
10 ConnectionPoolConfig, ConversationHandle, RemoteConfig, RemoteConversationHandle,
11 lys_pass_credential,
12};
13
14use crate::rail_link::{self, Subscription};
15use serde_json::Value;
16
17use super::{Counts, Rail, RailError};
18use crate::config::{Config, Credential, Limits};
19use crate::lys::LysAgent;
20
21pub const TOPOLOGY_SUBJECT: &str = "console.topology";
23
24pub struct Live {
26 server: String,
27 channel: String,
28 source: Source,
29 limits: Limits,
30 counts: Counts,
31 conversations: AtomicU64,
32}
33
34impl std::fmt::Debug for Live {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("Live")
37 .field("server", &self.server)
38 .field("channel", &self.channel)
39 .field("source", &self.source)
40 .field("limits", &self.limits)
41 .finish_non_exhaustive()
42 }
43}
44
45impl Live {
46 pub fn new(config: &Config) -> Result<Self, &'static str> {
51 let source = match &config.credential {
52 Credential::Token(token) => Source::Fixed(lys_pass_credential(token.as_str())),
53 Credential::Lys(table) => {
54 Source::Lys(LysAgent::new(table, config.limits.reply_timeout())?)
55 }
56 Credential::Open { .. } => Source::Open(config.credential.open_record()),
57 };
58 Ok(Self {
59 server: config.server.clone(),
60 channel: config.channel.clone(),
61 source,
62 limits: config.limits,
63 counts: Counts::default(),
64 conversations: AtomicU64::new(0),
65 })
66 }
67
68 fn credential(&self) -> Result<Vec<u8>, RailError> {
72 match &self.source {
73 Source::Fixed(bytes) => Ok(bytes.clone()),
74 Source::Open(record) => Ok(record.clone()),
75 Source::Lys(agent) => agent
76 .pass()
77 .map(|pass| lys_pass_credential(&pass))
78 .map_err(|error| RailError::Refused(error.0)),
79 }
80 }
81}
82
83enum Source {
85 Fixed(Vec<u8>),
87 Lys(LysAgent),
89 Open(Vec<u8>),
92}
93
94impl Rail for Live {
95 fn topology(&self) -> Result<Value, RailError> {
96 let stream = self.admit(|credential| {
97 Counts::add(&self.counts.connections_opened);
98 Subscription::open(&self.server, TOPOLOGY_SUBJECT, credential).map_err(|error| {
99 Counts::add(&self.counts.connections_closed);
100 RailError::Refused(error.to_string())
101 })
102 })?;
103 Counts::add(&self.counts.subscribes);
104 let record = stream.recv_timeout(self.limits.reply_timeout());
105 drop(stream);
106 Counts::add(&self.counts.connections_closed);
107 let record = record.map_err(|_| RailError::NoRecord {
108 millis: self.limits.reply_timeout_ms,
109 })?;
110 Counts::add(&self.counts.records);
111 if record.payload().len() > self.limits.record_bytes {
112 return Err(RailError::RecordTooLarge {
113 limit: self.limits.record_bytes,
114 });
115 }
116 serde_json::from_slice(record.payload()).map_err(|error| RailError::Unreadable {
117 message: error.to_string(),
118 })
119 }
120
121 fn request(
122 &self,
123 subject: &str,
124 input: &Value,
125 reply: bool,
126 ) -> Result<Option<Value>, RailError> {
127 self.conversations.fetch_add(1, Ordering::SeqCst);
128 Counts::add(&self.counts.connections_opened);
129 let outcome = self.request_once(subject, input, reply);
130 Counts::add(&self.counts.connections_closed);
131 outcome
132 }
133
134 fn pass_for(&self, audience: &str) -> Result<String, RailError> {
135 match &self.source {
136 Source::Fixed(_) => Err(RailError::Refused(
137 "pass_for needs a [lys] table: a ticket on a fixed token cannot ask Lys for a pass"
138 .to_owned(),
139 )),
140 Source::Open(_) => Err(RailError::Refused(
141 "pass_for needs a [lys] table: an open seat cannot ask Lys for a pass".to_owned(),
142 )),
143 Source::Lys(agent) => agent
144 .pass_for(audience)
145 .map_err(|error| RailError::Refused(error.0)),
146 }
147 }
148
149 fn tap(&self, channel: &str) -> Result<super::TapSubscription, RailError> {
150 self.admit(|credential| {
151 super::live_tap::open(&self.server, channel, credential, self.limits.record_bytes)
152 })
153 }
154
155 fn server_address(&self) -> &str {
156 &self.server
157 }
158
159 fn counts(&self) -> &Counts {
160 &self.counts
161 }
162
163 fn request_bytes(&self) -> usize {
164 self.limits.request_bytes
165 }
166
167 fn flow_steps(&self) -> usize {
168 self.limits.flow_steps
169 }
170}
171
172impl Live {
173 fn request_once(
174 &self,
175 subject: &str,
176 input: &Value,
177 reply: bool,
178 ) -> Result<Option<Value>, RailError> {
179 let mut attempted = false;
180 let config = self.admit(|credential| {
181 if attempted {
182 Counts::add(&self.counts.connections_closed);
183 Counts::add(&self.counts.connections_opened);
184 }
185 attempted = true;
186 RemoteConfig::new(
187 self.server.clone(),
188 self.channel.clone(),
189 subject.to_owned(),
190 ConnectionPoolConfig::new(1, 10, 16),
191 )
192 .and_then(|config| rail_link::connect(&self.server, config, credential))
193 .map_err(|error| RailError::Refused(error.to_string()))
194 })?;
195 let conversation = RemoteConversationHandle::new(&config);
196 Counts::add(&self.counts.requests);
197 if !reply {
198 conversation
199 .send(input)
200 .map_err(|error| RailError::Refused(error.to_string()))?;
201 return Ok(None);
202 }
203 conversation
204 .request(input)
205 .map_err(|error| RailError::Refused(error.to_string()))?;
206 let answer: Value = block_on(conversation.receive())
207 .and_then(|received| received.map_err(|error| error.to_string()))
208 .map_err(RailError::Refused)?;
209 Counts::add(&self.counts.replies);
210 Ok(Some(answer))
211 }
212}
213
214fn block_on<F: Future>(future: F) -> Result<F::Output, String> {
217 let mut future = pin!(future);
218 let waker = Waker::noop();
219 let mut context = Context::from_waker(waker);
220 match future.as_mut().poll(&mut context) {
221 Poll::Ready(value) => Ok(value),
222 Poll::Pending => Err("the synchronous transport parked unexpectedly".to_owned()),
223 }
224}
225
226#[path = "pass_renewal.rs"]
227mod pass_renewal;
228
229#[cfg(test)]
230#[path = "live_tests.rs"]
231mod tests;
232
233impl std::fmt::Debug for Source {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 f.write_str(match self {
236 Self::Fixed(_) => "FixedCredential",
237 Self::Lys(_) => "LysCredential",
238 Self::Open(_) => "OpenCredential",
239 })
240 }
241}