Skip to main content

veilid_core/veilid_api/
dht_transaction.rs

1use super::*;
2use crate::storage_manager::OutboundTransactionHandle;
3
4impl_veilid_log_facility!("veilid_api");
5
6///////////////////////////////////////////////////////////////////////////////////////
7
8/// DHT Transactions the way you perform multiple simulateous atomic operations over a set of DHT records.
9///
10/// DHT operations performed out of a transaction may be processed in any order, and only operate on one subkey at a time
11/// for a given record. Transactions allow you to bind a set of operations so they all succeed, or fail together, and at the same time.
12///
13/// Transactional DHT operations can only be performed when the node is online, and will error with [VeilidAPIError::TryAgain] if offline.
14///
15/// Transactions must be committed when all of their operations are registered, or rolled back if the group of operations is to be cancelled.
16///
17/// Each transaction holds a network-side resource that the caller must release by calling [DHTTransaction::commit] or [DHTTransaction::rollback]. Dropping a [DHTTransaction] without doing either logs a warning and tears the transaction down in the background.
18#[derive(Clone)]
19#[must_use]
20pub struct DHTTransaction {
21    /// API in use
22    api: VeilidAPI,
23    /// Inner transaction
24    inner: Arc<Mutex<DHTTransactionInner>>,
25}
26
27impl fmt::Debug for DHTTransaction {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        f.debug_struct("DHTTransaction")
30            .field("handle", &self.inner.lock().opt_transaction_handle)
31            .finish()
32    }
33}
34
35impl DHTTransaction {
36    ////////////////////////////////////////////////////////////////
37
38    pub(super) fn new(api: VeilidAPI, handle: OutboundTransactionHandle) -> VeilidAPIResult<Self> {
39        let registry = api.core_context()?.registry();
40        Ok(Self {
41            api,
42            inner: Arc::new(Mutex::new(DHTTransactionInner {
43                registry,
44                opt_transaction_handle: Some(handle),
45            })),
46        })
47    }
48
49    /// Get the [VeilidAPI] object that created this [DHTTransaction].
50    pub fn api(&self) -> VeilidAPI {
51        self.api.clone()
52    }
53
54    #[must_use]
55    pub(crate) fn log_key(&self) -> &str {
56        self.api.log_key()
57    }
58
59    /// Extend the transaction with additional record keys
60    ///
61    /// Blocks on a network begin fanout for the added records and requires the node to be online. Idempotent for keys already in the transaction: returns `Ok(())` without network activity if no new records would be added.
62    ///
63    /// Errors with [VeilidAPIError::TransactionNotFound] if the transaction handle is already completed or unknown, [VeilidAPIError::MissingArgument] if `record_keys` contains duplicates, [VeilidAPIError::InvalidArgument] if the merged record set would exceed the per-transaction record limit, and [VeilidAPIError::TryAgain] (retry) if the node is offline or the begin fanout for the added records could not reach consensus.
64    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key(), transaction_handle), skip(self), ret))]
65    pub async fn extend(
66        &self,
67        record_keys: Vec<RecordKey>,
68        options: Option<TransactDHTRecordsOptions>,
69    ) -> VeilidAPIResult<()> {
70        async move {
71            let storage_manager = self.api.core_context()?.storage_manager();
72            let transaction_handle = {
73                let inner = self.inner.lock();
74                inner.opt_transaction_handle.ok_or_else(|| VeilidAPIError::transaction_not_found("transaction already completed"))?
75            };
76            tracing::Span::current().record("transaction_handle", transaction_handle.to_string());
77
78            let recorder = DurationRecorder::new("DHTTransaction::extend", |name, start| {
79                veilid_log!(self debug
80                    "{}[start={:#}](transaction_handle: {}, record_keys: {:?}, options: {:?})", name, start, transaction_handle, record_keys, options);
81            });
82            recorder.record_fut(
83                storage_manager.extend_transaction(transaction_handle, record_keys, options),
84                |name, start, dur, ret| {
85                    veilid_log!(self debug
86                        "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
87                    ret
88                },
89            ).await
90        }.await.inspect_err(log_veilid_api_error!(self))
91    }
92
93    /// Commit the transaction
94    /// All write operations are performed atomically
95    ///
96    /// Consumes the transaction and releases its network-side resource (the other half is [DHTTransaction::rollback]). Blocks on the end and commit consensus barriers and requires the node to be online. Completes the transaction exactly once: a second commit or a rollback errors with `transaction_not_found`.
97    ///
98    /// Errors with [VeilidAPIError::TransactionNotFound] if the transaction was already committed, rolled back, or is unknown, and [VeilidAPIError::TryAgain] (retry) if the node is offline or the end/commit barriers could not reach consensus.
99    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key(), transaction_handle), skip(self), ret))]
100    pub async fn commit(self) -> VeilidAPIResult<()> {
101        async {
102            let storage_manager = self.api.core_context()?.storage_manager();
103            let transaction_handle = {
104                let mut inner = self.inner.lock();
105                inner.opt_transaction_handle.take().ok_or_else(|| {
106                    VeilidAPIError::transaction_not_found("transaction already completed")
107                })?
108            };
109            tracing::Span::current().record("transaction_handle", transaction_handle.to_string());
110
111            let recorder = DurationRecorder::new("DHTTransaction::commit", |name, start| {
112                veilid_log!(self debug
113                    "{}[start={:#}](transaction_handle: {})", name, start, transaction_handle);
114            });
115            recorder
116                .record_fut(
117                    Box::pin(storage_manager.end_and_commit_transaction(transaction_handle)),
118                    |name, start, dur, ret| {
119                        veilid_log!(self debug
120                        "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
121                        ret
122                    },
123                )
124                .await
125        }
126        .await
127        .inspect_err(log_veilid_api_error!(self))
128    }
129
130    /// Rollback the transaction
131    /// No write operations are performed,
132    ///
133    /// Consumes the transaction and releases its network-side resource (the other half is [DHTTransaction::commit]). Blocks on sending rollbacks to the network and requires the node to be online. Completes the transaction exactly once: a second rollback or a commit errors with `transaction_not_found`.
134    ///
135    /// Errors with [VeilidAPIError::TransactionNotFound] if the transaction was already committed, rolled back, or is unknown, and [VeilidAPIError::TryAgain] (retry) if the node is offline.
136    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key(), transaction_handle), skip(self), ret))]
137    pub async fn rollback(self) -> VeilidAPIResult<()> {
138        async {
139            let storage_manager = self.api.core_context()?.storage_manager();
140            let transaction_handle = {
141                let mut inner = self.inner.lock();
142                inner.opt_transaction_handle.take().ok_or_else(|| {
143                    VeilidAPIError::transaction_not_found("transaction already completed")
144                })?
145            };
146            tracing::Span::current().record("transaction_handle", transaction_handle.to_string());
147
148            let recorder = DurationRecorder::new("DHTTransaction::rollback", |name, start| {
149                veilid_log!(self debug
150                    "{}[start={:#}](transaction_handle: {})", name, start, transaction_handle);
151            });
152            recorder
153                .record_fut(
154                    Box::pin(storage_manager.rollback_transaction(transaction_handle)),
155                    |name, start, dur, ret| {
156                        veilid_log!(self debug
157                        "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
158                        ret
159                    },
160                )
161                .await
162        }
163        .await
164        .inspect_err(log_veilid_api_error!(self))
165    }
166
167    /// Add a set_dht_value operation to the transaction
168    ///
169    /// * Will fail if performed offline
170    /// * Will fail if existing offline writes exist for this record key
171    ///
172    /// The writer, if specified, will override the 'default_writer' specified when the record is opened.
173    ///
174    /// Returns `None` if the value was successfully set.
175    /// Returns `Some(data)` if the value set was older than the one available on the network.
176    ///
177    /// Blocks on the per-subkey lock (unbounded) and the set RPC to the transaction's node set, which retries non-responding nodes. Each per-node RPC is bounded by `network.rpc.timeout_ms`, but the lock wait and retry rounds are not, so the whole call has no single-timeout bound.
178    ///
179    /// Errors with [VeilidAPIError::TransactionNotFound] if the transaction handle is already completed or no longer in the Begin stage, [VeilidAPIError::InvalidArgument] if `record_key` is not open in the transaction or `subkey` is outside the schema range, [VeilidAPIError::Generic] if `record_key` is malformed (unsupported kind or bad length) or the subkey has no writer, and [VeilidAPIError::TryAgain] (retry) if the node is offline or write consensus was not reached this round. A non-responding node is retried rather than surfaced as a timeout.
180    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key(), transaction_handle, data.len = data.len()), skip(self, data), ret))]
181    pub async fn set(
182        &self,
183        record_key: RecordKey,
184        subkey: ValueSubkey,
185        data: Vec<u8>,
186        options: Option<DHTTransactionSetValueOptions>,
187    ) -> VeilidAPIResult<Option<ValueData>> {
188        async move {
189            let storage_manager = self.api.core_context()?.storage_manager();
190            let transaction_handle = {
191                let inner = self.inner.lock();
192                inner
193                    .opt_transaction_handle
194                    .ok_or_else(|| VeilidAPIError::transaction_not_found("transaction already completed"))?
195            };
196            tracing::Span::current().record("transaction_handle", transaction_handle.to_string());
197            storage_manager.check_record_key(&record_key)?;
198
199            let data_len = data.len();
200            let recorder = DurationRecorder::new("DHTTransaction::set", |name, start| {
201                veilid_log!(self debug
202                    "{}[start={:#}](transaction_handle: {}, key: {}, subkey: {}, data: len={}, options: {:?})", name, start, transaction_handle, record_key, subkey, data_len, options);
203            });
204            recorder.record_fut(
205                Box::pin(storage_manager.transaction_set(
206                    transaction_handle,
207                    record_key,
208                    subkey,
209                    data,
210                    options,
211                )),
212                |name, start, dur, ret| {
213                    veilid_log!(self debug
214                        "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
215                    ret
216                },
217            ).await
218        }.await.inspect_err(log_veilid_api_error!(self))
219    }
220
221    /// Perform a get_dht_value operation inside the transaction
222    ///
223    /// * Will fail if performed offline
224    /// * Will pull the latest value from the network, will fail if the local value is newer
225    /// * Will fail if existing offline writes exist for this record key
226    ///
227    /// Returns `None` if the value subkey has not yet been set.
228    /// Returns `Some(data)` if the value subkey has valid data.
229    ///
230    /// Blocks on the per-subkey lock (unbounded) and the get RPC to the transaction's node set, which retries non-responding nodes. Each per-node RPC is bounded by `network.rpc.timeout_ms`, but the lock wait and retry rounds are not, so the whole call has no single-timeout bound.
231    ///
232    /// Errors with [VeilidAPIError::TransactionNotFound] if the transaction handle is already completed or no longer in the Begin stage, [VeilidAPIError::InvalidArgument] if `record_key` is not in the transaction or `subkey` is outside the schema range, [VeilidAPIError::Generic] if `record_key` is malformed (unsupported kind or bad length), and [VeilidAPIError::TryAgain] (retry) if the node is offline or the network did not return the value that existed at begin time. A non-responding node is retried rather than surfaced as a timeout.
233    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip(self), ret))]
234    pub async fn get(
235        &self,
236        record_key: RecordKey,
237        subkey: ValueSubkey,
238    ) -> VeilidAPIResult<Option<ValueData>> {
239        async move {
240            let storage_manager = self.api.core_context()?.storage_manager();
241            let transaction_handle = {
242                let inner = self.inner.lock();
243                inner
244                    .opt_transaction_handle
245                    .ok_or_else(|| VeilidAPIError::transaction_not_found("transaction already completed"))?
246            };
247            tracing::Span::current().record("transaction_handle", transaction_handle.to_string());
248            storage_manager.check_record_key(&record_key)?;
249
250            let recorder = DurationRecorder::new("DHTTransaction::get", |name, start| {
251                veilid_log!(self debug
252                    "{}[start={:#}](transaction_handle: {}, key: {}, subkey: {})", name, start, transaction_handle, record_key, subkey);
253            });
254            recorder.record_fut(
255                Box::pin(storage_manager.transaction_get(transaction_handle, record_key, subkey)),
256                |name, start, dur, ret| {
257                    veilid_log!(self debug
258                        "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
259                    ret
260                },
261            ).await
262        }.await.inspect_err(log_veilid_api_error!(self))
263    }
264
265    /// Perform a inspect_dht_record operation inside the transaction
266    ///
267    /// * Does not perform any network activity, as the transaction state keeps all of the required information after the begin
268    ///
269    /// For information on arguments, see [RoutingContext::inspect_dht_record]
270    ///
271    /// Returns a DHTRecordReport with the subkey ranges that were returned that overlapped the schema, and sequence numbers for each of the subkeys in the range.
272    ///
273    /// Errors with [VeilidAPIError::TransactionNotFound] if the transaction handle is already completed, unknown, or no longer in the Begin stage (End, Commit, Rollback, or Failed), [VeilidAPIError::InvalidArgument] if `record_key` is not in the transaction, and [VeilidAPIError::Generic] if `record_key` is malformed (unsupported kind or bad length) or the transaction has not started. Performs no network activity and cannot time out.
274    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key(), transaction_handle), skip(self), ret))]
275    pub async fn inspect(
276        &self,
277        record_key: RecordKey,
278        subkeys: Option<ValueSubkeyRangeSet>,
279        scope: DHTReportScope,
280    ) -> VeilidAPIResult<DHTRecordReport> {
281        async move {
282            let storage_manager = self.api.core_context()?.storage_manager();
283            let transaction_handle = {
284                let inner = self.inner.lock();
285                inner
286                    .opt_transaction_handle
287                    .ok_or_else(|| VeilidAPIError::transaction_not_found("transaction already completed"))?
288            };
289            tracing::Span::current().record("transaction_handle", transaction_handle.to_string());
290            storage_manager.check_record_key(&record_key)?;
291
292            let recorder = DurationRecorder::new("DHTTransaction::inspect", |name, start| {
293                veilid_log!(self debug
294                    "{}[start={:#}](transaction_handle: {}, record_key: {}, subkeys: {}, scope: {:?})", name, start, transaction_handle, record_key, subkeys.as_ref().map(|x| x.to_string()).unwrap_or_else(|| "None".to_string()), scope);
295            });
296            recorder.record(
297                || storage_manager.transaction_inspect(transaction_handle, record_key, subkeys, scope),
298                |name, start, dur, ret| {
299                    veilid_log!(self debug
300                        "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
301                    ret
302                },
303            )
304        }.await.inspect_err(log_veilid_api_error!(self))
305    }
306}
307//////////////////////////////////////////////////////////////////////////////////////
308
309struct DHTTransactionInner {
310    registry: VeilidComponentRegistry,
311    opt_transaction_handle: Option<OutboundTransactionHandle>,
312}
313
314impl Drop for DHTTransactionInner {
315    fn drop(&mut self) {
316        if let Some(transaction_handle) = self.opt_transaction_handle.take() {
317            let registry = &self.registry;
318            veilid_log!(registry warn "Dropped DHT transaction without commit or rollback");
319
320            let storage_manager = registry.storage_manager();
321            storage_manager.drop_transaction_sync(transaction_handle);
322        }
323    }
324}