1use std::{
5 collections::HashMap,
6 sync::{Arc, Mutex},
7 time::Duration,
8};
9
10use futures::StreamExt as _;
11use linera_base::identifiers::{ApplicationId, ChainId};
12use linera_client::chain_listener::ClientContext;
13use linera_core::worker::Reason;
14use linera_execution::{Query, QueryResponse};
15use tokio::sync::watch;
16use tokio_util::sync::CancellationToken;
17use tracing::{debug, warn};
18
19#[derive(Clone, Debug)]
21pub struct RegisteredQuery {
22 pub name: String,
24 pub query: String,
26}
27
28pub fn parse_allowed_subscription(s: &str) -> anyhow::Result<RegisteredQuery> {
30 let trimmed = s.trim();
31 let rest = trimmed
32 .strip_prefix("query")
33 .ok_or_else(|| anyhow::anyhow!("expected query to start with 'query', got: {s}"))?;
34 anyhow::ensure!(
36 rest.starts_with(char::is_whitespace),
37 "expected whitespace after 'query' keyword"
38 );
39 let rest = rest.trim_start();
40 let name = rest
42 .split(|c: char| !c.is_alphanumeric() && c != '_')
43 .next()
44 .unwrap_or_default();
45 anyhow::ensure!(
46 !name.is_empty(),
47 "expected an operation name after 'query', e.g. 'query MyQuery {{ ... }}'"
48 );
49 Ok(RegisteredQuery {
50 name: name.to_string(),
51 query: trimmed.to_string(),
52 })
53}
54
55pub fn parse_subscription_ttl(s: &str) -> Result<(String, u64), String> {
57 let (name, secs) = s
58 .split_once('=')
59 .ok_or_else(|| format!("expected format Name=Secs, got: {s}"))?;
60 let secs: u64 = secs
61 .parse()
62 .map_err(|e| format!("invalid seconds value '{secs}': {e}"))?;
63 Ok((name.to_string(), secs))
64}
65
66#[derive(Clone, Debug, Hash, Eq, PartialEq)]
68pub struct SubscriptionKey {
69 pub name: String,
71 pub chain_id: ChainId,
73 pub application_id: ApplicationId,
75}
76
77struct WatcherState {
81 sender: watch::Sender<Option<String>>,
82}
83
84pub struct QuerySubscriptionManager {
86 queries: HashMap<String, String>,
88 ttls: HashMap<String, Duration>,
90 watchers: Mutex<HashMap<SubscriptionKey, WatcherState>>,
92}
93
94impl QuerySubscriptionManager {
95 pub fn new(registered: Vec<RegisteredQuery>, ttls: HashMap<String, Duration>) -> Self {
97 let queries = registered
98 .into_iter()
99 .map(|rq| (rq.name, rq.query))
100 .collect();
101 Self {
102 queries,
103 ttls,
104 watchers: Mutex::new(HashMap::new()),
105 }
106 }
107
108 pub fn get_query(&self, name: &str) -> Option<&str> {
110 self.queries.get(name).map(|s| s.as_str())
111 }
112
113 pub fn subscribe<C: ClientContext + 'static>(
117 self: &Arc<Self>,
118 key: &SubscriptionKey,
119 context: Arc<futures::lock::Mutex<C>>,
120 token: CancellationToken,
121 ) -> anyhow::Result<watch::Receiver<Option<String>>> {
122 let query_string = self
123 .get_query(&key.name)
124 .ok_or_else(|| {
125 anyhow::anyhow!("no subscription query registered with name '{}'", key.name)
126 })?
127 .to_string();
128
129 let mut watchers = self.watchers.lock().unwrap();
130
131 if let Some(state) = watchers.get(key) {
133 return Ok(state.sender.subscribe());
134 }
135
136 let (sender, receiver) = watch::channel(None);
138 watchers.insert(
139 key.clone(),
140 WatcherState {
141 sender: sender.clone(),
142 },
143 );
144
145 let ttl = self.ttls.get(&key.name).copied();
146 let manager = Arc::clone(self);
147 let key_clone = key.clone();
148 tokio::spawn(run_query_subscription_watcher(
149 context,
150 manager,
151 key_clone,
152 query_string,
153 sender,
154 token,
155 ttl,
156 ));
157
158 Ok(receiver)
159 }
160}
161
162async fn run_query_subscription_watcher<C: ClientContext + 'static>(
164 context: Arc<futures::lock::Mutex<C>>,
165 manager: Arc<QuerySubscriptionManager>,
166 key: SubscriptionKey,
167 query_string: String,
168 sender: watch::Sender<Option<String>>,
169 token: CancellationToken,
170 ttl: Option<Duration>,
171) {
172 debug!(
173 name = %key.name,
174 chain_id = %key.chain_id,
175 application_id = %key.application_id,
176 ?ttl,
177 "starting query subscription watcher"
178 );
179
180 let notification_stream = {
181 let ctx = context.lock().await;
182 match ctx.make_chain_client(key.chain_id).await {
183 Ok(client) => match client.subscribe() {
184 Ok(stream) => stream,
185 Err(e) => {
186 warn!("failed to subscribe to chain notifications: {e}");
187 cleanup_watcher(&manager, &key);
188 return;
189 }
190 },
191 Err(e) => {
192 warn!("failed to create chain client: {e}");
193 cleanup_watcher(&manager, &key);
194 return;
195 }
196 }
197 };
198
199 let mut notification_stream = Box::pin(notification_stream);
200
201 let mut last_result: Option<String> = None;
203
204 execute_and_maybe_send(&context, &key, &query_string, &sender, &mut last_result).await;
206
207 let mut last_execution = tokio::time::Instant::now();
209 let mut pending_invalidation = false;
211
212 loop {
213 let ttl_sleep = if pending_invalidation {
215 if let Some(ttl) = ttl {
216 let elapsed = last_execution.elapsed();
217 if elapsed < ttl {
218 tokio::time::sleep(ttl - elapsed)
219 } else {
220 tokio::time::sleep(Duration::ZERO)
221 }
222 } else {
223 tokio::time::sleep(Duration::ZERO)
226 }
227 } else {
228 tokio::time::sleep(Duration::MAX)
230 };
231 tokio::pin!(ttl_sleep);
232
233 tokio::select! {
234 _ = token.cancelled() => {
235 debug!(name = %key.name, "watcher cancelled");
236 break;
237 }
238 () = &mut ttl_sleep, if pending_invalidation => {
239 pending_invalidation = false;
240 execute_and_maybe_send(
241 &context,
242 &key,
243 &query_string,
244 &sender,
245 &mut last_result,
246 )
247 .await;
248 last_execution = tokio::time::Instant::now();
249 }
250 notification = notification_stream.next() => {
251 match notification {
252 Some(n) => {
253 if matches!(n.reason, Reason::NewBlock { .. }) {
254 if ttl.is_some() {
255 if !pending_invalidation {
257 debug!(name = %key.name, "deferring invalidation until TTL expires");
258 }
259 pending_invalidation = true;
260 } else {
261 execute_and_maybe_send(
262 &context,
263 &key,
264 &query_string,
265 &sender,
266 &mut last_result,
267 )
268 .await;
269 last_execution = tokio::time::Instant::now();
270 }
271 }
272 }
273 None => {
274 debug!(name = %key.name, "notification stream ended");
275 break;
276 }
277 }
278
279 if sender.is_closed() {
281 debug!(name = %key.name, "no more subscribers, stopping watcher");
282 break;
283 }
284 }
285 }
286 }
287
288 cleanup_watcher(&manager, &key);
289}
290
291async fn execute_and_maybe_send<C: ClientContext + 'static>(
293 context: &Arc<futures::lock::Mutex<C>>,
294 key: &SubscriptionKey,
295 query_string: &str,
296 sender: &watch::Sender<Option<String>>,
297 last_result: &mut Option<String>,
298) {
299 let json_request = serde_json::json!({ "query": query_string });
301 let request_bytes = serde_json::to_vec(&json_request).unwrap();
302 let query = Query::User {
303 application_id: key.application_id,
304 bytes: request_bytes,
305 };
306
307 let result = {
308 let ctx = context.lock().await;
309 match ctx.make_chain_client(key.chain_id).await {
310 Ok(client) => client.query_application(query, None).await,
311 Err(e) => {
312 warn!(name = %key.name, "failed to create chain client: {e}");
313 return;
314 }
315 }
316 };
317
318 match result {
319 Ok((outcome, _height)) => {
320 let response_bytes = match outcome.response {
321 QueryResponse::User(bytes) => bytes,
322 QueryResponse::System(_) => {
323 warn!(name = %key.name, "unexpected system response for user query");
324 return;
325 }
326 };
327
328 let json_string = match String::from_utf8(response_bytes) {
331 Ok(s) => s,
332 Err(error) => {
333 warn!(%error, name = %key.name, "response bytes are not valid UTF-8");
334 return;
335 }
336 };
337
338 if last_result.as_ref() == Some(&json_string) {
340 return;
341 }
342
343 if let Err(e) = sender.send(Some(json_string.clone())) {
344 debug!(name = %key.name, "Failed to send graphql response: {e}");
345 }
346 *last_result = Some(json_string);
347 }
348 Err(e) => {
349 warn!(name = %key.name, "query execution failed: {e}");
350 }
351 }
352}
353
354fn cleanup_watcher(manager: &QuerySubscriptionManager, key: &SubscriptionKey) {
356 let mut watchers = manager.watchers.lock().unwrap();
357 watchers.remove(key);
358 debug!(name = %key.name, "watcher cleaned up");
359}