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,
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 => Ok(Vec::new()),
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,
91}
92
93impl Rail for Live {
94 fn topology(&self) -> Result<Value, RailError> {
95 let stream = self.admit(|credential| {
96 Counts::add(&self.counts.connections_opened);
97 Subscription::open(&self.server, TOPOLOGY_SUBJECT, credential).map_err(|error| {
98 Counts::add(&self.counts.connections_closed);
99 RailError::Refused(error.to_string())
100 })
101 })?;
102 Counts::add(&self.counts.subscribes);
103 let record = stream.recv_timeout(self.limits.reply_timeout());
104 drop(stream);
105 Counts::add(&self.counts.connections_closed);
106 let record = record.map_err(|_| RailError::NoRecord {
107 millis: self.limits.reply_timeout_ms,
108 })?;
109 Counts::add(&self.counts.records);
110 if record.payload().len() > self.limits.record_bytes {
111 return Err(RailError::RecordTooLarge {
112 limit: self.limits.record_bytes,
113 });
114 }
115 serde_json::from_slice(record.payload()).map_err(|error| RailError::Unreadable {
116 message: error.to_string(),
117 })
118 }
119
120 fn request(
121 &self,
122 subject: &str,
123 input: &Value,
124 reply: bool,
125 ) -> Result<Option<Value>, RailError> {
126 self.conversations.fetch_add(1, Ordering::SeqCst);
127 Counts::add(&self.counts.connections_opened);
128 let outcome = self.request_once(subject, input, reply);
129 Counts::add(&self.counts.connections_closed);
130 outcome
131 }
132
133 fn pass_for(&self, audience: &str) -> Result<String, RailError> {
134 match &self.source {
135 Source::Fixed(_) => Err(RailError::Refused(
136 "pass_for needs a [lys] table: a ticket on a fixed token cannot ask Lys for a pass"
137 .to_owned(),
138 )),
139 Source::Open => Err(RailError::Refused(
140 "pass_for needs a [lys] table: an open seat cannot ask Lys for a pass".to_owned(),
141 )),
142 Source::Lys(agent) => agent
143 .pass_for(audience)
144 .map_err(|error| RailError::Refused(error.0)),
145 }
146 }
147
148 fn tap(&self, channel: &str) -> Result<super::TapSubscription, RailError> {
149 self.admit(|credential| {
150 super::live_tap::open(&self.server, channel, credential, self.limits.record_bytes)
151 })
152 }
153
154 fn server_address(&self) -> &str {
155 &self.server
156 }
157
158 fn counts(&self) -> &Counts {
159 &self.counts
160 }
161
162 fn request_bytes(&self) -> usize {
163 self.limits.request_bytes
164 }
165
166 fn flow_steps(&self) -> usize {
167 self.limits.flow_steps
168 }
169}
170
171impl Live {
172 fn request_once(
173 &self,
174 subject: &str,
175 input: &Value,
176 reply: bool,
177 ) -> Result<Option<Value>, RailError> {
178 let mut attempted = false;
179 let config = self.admit(|credential| {
180 if attempted {
181 Counts::add(&self.counts.connections_closed);
182 Counts::add(&self.counts.connections_opened);
183 }
184 attempted = true;
185 RemoteConfig::new(
186 self.server.clone(),
187 self.channel.clone(),
188 subject.to_owned(),
189 ConnectionPoolConfig::new(1, 10, 16),
190 )
191 .and_then(|config| rail_link::connect(&self.server, config, credential))
192 .map_err(|error| RailError::Refused(error.to_string()))
193 })?;
194 let conversation = RemoteConversationHandle::new(&config);
195 Counts::add(&self.counts.requests);
196 if !reply {
197 conversation
198 .send(input)
199 .map_err(|error| RailError::Refused(error.to_string()))?;
200 return Ok(None);
201 }
202 conversation
203 .request(input)
204 .map_err(|error| RailError::Refused(error.to_string()))?;
205 let answer: Value = block_on(conversation.receive())
206 .and_then(|received| received.map_err(|error| error.to_string()))
207 .map_err(RailError::Refused)?;
208 Counts::add(&self.counts.replies);
209 Ok(Some(answer))
210 }
211}
212
213fn block_on<F: Future>(future: F) -> Result<F::Output, String> {
216 let mut future = pin!(future);
217 let waker = Waker::noop();
218 let mut context = Context::from_waker(waker);
219 match future.as_mut().poll(&mut context) {
220 Poll::Ready(value) => Ok(value),
221 Poll::Pending => Err("the synchronous transport parked unexpectedly".to_owned()),
222 }
223}
224
225#[path = "pass_renewal.rs"]
226mod pass_renewal;
227
228#[cfg(test)]
229#[path = "live_tests.rs"]
230mod tests;
231
232impl std::fmt::Debug for Source {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 f.write_str(match self {
235 Self::Fixed(_) => "FixedCredential",
236 Self::Lys(_) => "LysCredential",
237 Self::Open => "OpenCredential",
238 })
239 }
240}