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}