1use reifydb_value::{
5 fragment::Fragment,
6 value::{duration::Duration, temporal::parse::duration::parse_duration},
7};
8use serde::{Deserialize, Serialize};
9
10pub const DEFAULT_MAX_N: u32 = 1;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct WireClaimRequest {
14 pub queue: String,
15 pub worker: String,
16 #[serde(default)]
17 pub max_n: Option<u32>,
18 #[serde(default)]
19 pub lease_ttl: Option<String>,
20 #[serde(default)]
21 pub wait_for: Option<String>,
22}
23
24#[derive(Debug, Clone)]
25pub struct ClaimRequest {
26 pub queue: String,
27 pub worker: String,
28 pub max_n: u32,
29 pub lease_ttl: Option<Duration>,
30 pub wait_for: Duration,
31}
32
33impl WireClaimRequest {
34 pub fn into_claim_request(self) -> Result<ClaimRequest, String> {
35 Ok(ClaimRequest {
36 queue: self.queue,
37 worker: self.worker,
38 max_n: self.max_n.unwrap_or(DEFAULT_MAX_N),
39 lease_ttl: parse_optional_duration("lease_ttl", self.lease_ttl.as_deref())?,
40 wait_for: parse_optional_duration("wait_for", self.wait_for.as_deref())?
41 .unwrap_or(Duration::zero()),
42 })
43 }
44}
45
46fn parse_optional_duration(field: &str, raw: Option<&str>) -> Result<Option<Duration>, String> {
47 let Some(raw) = raw else {
48 return Ok(None);
49 };
50
51 parse_duration(Fragment::internal(raw))
52 .map(Some)
53 .map_err(|err| format!("invalid {field} value '{raw}': {err:?}"))
54}
55
56#[cfg(not(reifydb_single_threaded))]
57pub mod native {
58 use std::{collections::HashMap, sync::Arc};
59
60 use reifydb_core::{
61 actors::server::Operation, interface::catalog::id::QueueId, metrics::execution::ExecutionMetrics,
62 };
63 use reifydb_engine::queue::{lookup::find_queue_id, wake::QueueWakeRegistry};
64 use reifydb_runtime::sync::waiter::WaiterHandle;
65 use reifydb_value::{
66 params::Params,
67 value::{Value, duration::Duration, frame::frame::Frame, identity::IdentityId},
68 };
69 use tokio::{
70 sync::oneshot,
71 time::{Instant, timeout_at},
72 };
73 use tracing::instrument;
74
75 use super::ClaimRequest;
76 use crate::{
77 dispatch::dispatch,
78 execute::ExecuteError,
79 interceptor::{RequestContext, RequestMetadata},
80 state::AppState,
81 };
82
83 const CLAIM_WITH_TTL: &str = "CALL queue::claim($worker, $queue, $max_n, $lease_ttl)";
84
85 #[instrument(name = "dispatch::claim", level = "debug", skip_all, fields(queue = %request.queue))]
86 pub async fn dispatch_claim(
87 state: &AppState,
88 identity: IdentityId,
89 request: ClaimRequest,
90 metadata: RequestMetadata,
91 ) -> Result<(Vec<Frame>, ExecutionMetrics), ExecuteError> {
92 let lease_ttl = request.lease_ttl.unwrap_or_else(|| state.claim_lease_ttl());
93 let params = claim_params(&request.queue, &request.worker, request.max_n, lease_ttl);
94
95 let attempt = || RequestContext {
96 identity,
97 operation: Operation::Command,
98 rql: CLAIM_WITH_TTL.to_string(),
99 params: params.clone(),
100 metadata: metadata.clone(),
101 };
102
103 let first = dispatch(state, attempt()).await?;
104 let budget = request.wait_for.min(state.claim_wait_max());
105 if !budget.is_positive() || claimed_any(&first.0) {
106 return Ok(first);
107 }
108
109 let Some(queue_id) = find_queue_id(state.engine(), identity, &request.queue) else {
110 return Ok(first);
111 };
112 let registry = state.engine().queue_wake();
113 let deadline = Instant::now() + budget.to_std();
114
115 loop {
116 let mut guard = ParkGuard::park(®istry, queue_id);
117
118 let attempted = dispatch(state, attempt()).await?;
119 if claimed_any(&attempted.0) {
120 guard.forward_if_consumed();
121 return Ok(attempted);
122 }
123
124 if timeout_at(deadline, guard.notified()).await.is_err() {
125 return Ok(attempted);
126 }
127 }
128 }
129
130 struct ParkGuard<'a> {
131 registry: &'a QueueWakeRegistry,
132 queue: QueueId,
133 waiter: Arc<WaiterHandle>,
134 notified: Option<oneshot::Receiver<()>>,
135 }
136
137 impl<'a> ParkGuard<'a> {
138 fn park(registry: &'a QueueWakeRegistry, queue: QueueId) -> Self {
139 let (sender, receiver) = oneshot::channel();
140 let waiter = Arc::new(WaiterHandle::with_callback(Box::new(move || {
141 let _ = sender.send(());
142 })));
143 registry.register(queue, waiter.clone());
144
145 Self {
146 registry,
147 queue,
148 waiter,
149 notified: Some(receiver),
150 }
151 }
152
153 async fn notified(&mut self) {
154 if let Some(receiver) = self.notified.take() {
155 let _ = receiver.await;
156 }
157 }
158
159 fn forward_if_consumed(&self) {
160 if self.waiter.wait_timeout(Duration::zero()) {
161 self.registry.nudge(self.queue, 1);
162 }
163 }
164 }
165
166 impl Drop for ParkGuard<'_> {
167 fn drop(&mut self) {
168 self.registry.deregister(self.queue, &self.waiter);
169 }
170 }
171
172 fn claim_params(queue: &str, worker: &str, max_n: u32, lease_ttl: Duration) -> Params {
173 Params::Named(Arc::new(HashMap::from_iter([
174 ("worker".to_string(), Value::Utf8(worker.to_string())),
175 ("queue".to_string(), Value::Utf8(queue.to_string())),
176 ("max_n".to_string(), Value::Uint4(max_n)),
177 ("lease_ttl".to_string(), Value::Duration(lease_ttl)),
178 ])))
179 }
180
181 fn claimed_any(frames: &[Frame]) -> bool {
182 frames.iter().any(|frame| frame.row_count() > 0)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 fn wire(wait_for: Option<&str>, lease_ttl: Option<&str>, max_n: Option<u32>) -> WireClaimRequest {
191 WireClaimRequest {
192 queue: "app::jobs".to_string(),
193 worker: "w1".to_string(),
194 max_n,
195 lease_ttl: lease_ttl.map(str::to_string),
196 wait_for: wait_for.map(str::to_string),
197 }
198 }
199
200 #[test]
201 fn test_an_absent_wait_for_is_a_non_blocking_claim() {
202 let request = wire(None, None, None).into_claim_request().unwrap();
205
206 assert!(request.wait_for.is_zero());
207 assert_eq!(request.max_n, 1, "a claim with no max_n asks for a single item");
208 assert!(request.lease_ttl.is_none(), "an absent lease ttl must defer to the server default");
209 }
210
211 #[test]
212 fn test_durations_are_parsed_from_their_rql_literal_form() {
213 let request = wire(Some("25s"), Some("2m"), Some(10)).into_claim_request().unwrap();
214
215 assert_eq!(request.wait_for.as_nanos().unwrap(), 25_000_000_000);
216 assert_eq!(request.lease_ttl.unwrap().as_nanos().unwrap(), 120_000_000_000);
217 assert_eq!(request.max_n, 10);
218 }
219
220 #[test]
221 fn test_a_malformed_duration_names_the_field_it_came_from() {
222 let err = wire(Some("soon"), None, None).into_claim_request().unwrap_err();
225 assert!(err.contains("wait_for"), "error must name the offending field, got: {err}");
226
227 let err = wire(None, Some("forever"), None).into_claim_request().unwrap_err();
228 assert!(err.contains("lease_ttl"), "error must name the offending field, got: {err}");
229 }
230}