1mod log;
21mod types;
22
23pub use log::{FileTransitionLog, MemoryTransitionLog, TransitionLog};
24pub use types::{JournalEntry, Transition, TransitionKind};
25
26use std::{
27 collections::{BTreeMap, BTreeSet},
28 io,
29 ops::{Deref, DerefMut},
30};
31
32use data::{Flow, LinkReference, LinksConstants};
33
34use super::macros::forward;
35use crate::{Doublets, Error, Fuse, Link, Links, ReadHandler, WriteHandler};
36
37struct Pending<T: LinkReference> {
38 id: u64,
39 before: Vec<Link<T>>,
40 start: usize,
41 failed: bool,
42}
43
44pub struct TransactionsDecorator<T: LinkReference, L: Doublets<T>, J: TransitionLog<T>> {
64 links: L,
65 journal: J,
66 transitions: Vec<Transition<T>>,
67 pending: Option<Pending<T>>,
68 next_id: u64,
69 next_sequence: u64,
70 poisoned: bool,
71 tracing: bool,
72 rollback_error: Option<String>,
73}
74
75impl<T: LinkReference, L: Doublets<T>, J: TransitionLog<T>> TransactionsDecorator<T, L, J> {
76 pub fn new(links: L, journal: J) -> Result<Self, Error<T>> {
81 let mut store = Self {
82 links,
83 journal,
84 transitions: Vec::new(),
85 pending: None,
86 next_id: 1,
87 next_sequence: 1,
88 poisoned: false,
89 tracing: false,
90 rollback_error: None,
91 };
92 store.recover()?;
93 Ok(store)
94 }
95
96 pub fn begin_transaction(&mut self) -> Result<Transaction<'_, T, L, J>, Error<T>> {
101 self.begin()?;
102 Ok(Transaction { store: self })
103 }
104
105 #[must_use]
107 pub const fn inner(&self) -> &L {
108 &self.links
109 }
110
111 #[must_use]
113 pub const fn journal(&self) -> &J {
114 &self.journal
115 }
116
117 #[must_use]
119 pub fn transitions(&self) -> &[Transition<T>] {
120 &self.transitions
121 }
122
123 #[must_use]
125 pub const fn is_poisoned(&self) -> bool {
126 self.poisoned
127 }
128
129 pub fn set_tracing(&mut self, enabled: bool) {
132 self.tracing = enabled;
133 }
134
135 #[must_use]
140 pub fn rollback_error(&self) -> Option<&str> {
141 self.rollback_error.as_deref()
142 }
143
144 pub fn into_parts(mut self) -> Result<(L, J), Error<T>> {
146 self.rollback()?;
147 Ok((self.links, self.journal))
148 }
149
150 fn begin(&mut self) -> Result<(), Error<T>> {
151 if self.poisoned {
152 return Err(failure(
153 "transaction journal is poisoned; reopen for recovery",
154 ));
155 }
156 if self.pending.is_some() {
157 return Err(failure("nested transactions are not supported"));
158 }
159 let id = self.next_id;
160 self.next_id = id
161 .checked_add(1)
162 .ok_or_else(|| failure("transaction id exhausted"))?;
163 let before = snapshot(&self.links);
164 self.append(&JournalEntry::Begin {
165 transaction_id: id,
166 snapshot: before.clone(),
167 })?;
168 self.pending = Some(Pending {
169 id,
170 before,
171 start: self.transitions.len(),
172 failed: false,
173 });
174 if self.tracing {
175 eprintln!("[doublets transactions] begin {id}");
176 }
177 Ok(())
178 }
179
180 fn append(&mut self, entry: &JournalEntry<T>) -> Result<(), Error<T>> {
181 if let Err(error) = self.journal.append(entry) {
182 self.poisoned = true;
183 return Err(error.into());
184 }
185 Ok(())
186 }
187
188 fn commit(&mut self) -> Result<(), Error<T>> {
189 let Some(pending) = &self.pending else {
190 return Ok(());
191 };
192 if pending.failed || self.poisoned {
193 return Err(failure("failed transaction cannot be committed"));
194 }
195 let id = pending.id;
196 let entry = JournalEntry::Commit {
197 transaction_id: id,
198 snapshot: snapshot(&self.links),
199 };
200 self.append(&entry)?;
201 if self.tracing {
202 eprintln!("[doublets transactions] commit {id}");
203 }
204 self.pending = None;
205 Ok(())
206 }
207
208 fn rollback(&mut self) -> Result<(), Error<T>> {
209 let Some(pending) = &self.pending else {
210 return Ok(());
211 };
212 let id = pending.id;
213 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
214 restore(&mut self.links, &pending.before)
215 }))
216 .unwrap_or_else(|_| {
217 Err(failure(
218 "wrapped store panicked while restoring transaction",
219 ))
220 });
221 if let Err(error) = result {
222 self.poisoned = true;
223 self.rollback_error = Some(error.to_string());
224 return Err(error);
225 }
226 self.pending = None;
227 if self.poisoned {
228 return Ok(());
229 }
230 if self.tracing {
231 eprintln!("[doublets transactions] rollback {id}");
232 }
233 if let Err(error) = self.append(&JournalEntry::Rollback { transaction_id: id }) {
234 self.rollback_error = Some(error.to_string());
235 return Err(error);
236 }
237 Ok(())
238 }
239
240 fn recover(&mut self) -> Result<(), Error<T>> {
241 let entries = self.journal.read_entries()?;
242 let mut pending = None;
243 let mut desired = None;
244 for entry in entries {
245 match entry {
246 JournalEntry::Begin {
247 transaction_id,
248 snapshot,
249 } => {
250 if pending.is_some() || transaction_id < self.next_id {
251 return Err(failure("invalid transaction begin order"));
252 }
253 validate(&self.links, &snapshot)?;
254 self.next_id = transaction_id
255 .checked_add(1)
256 .ok_or_else(|| failure("transaction id exhausted"))?;
257 pending = Some(transaction_id);
258 desired = Some(snapshot);
259 }
260 JournalEntry::Transition(transition) => {
261 transition.validate(&self.links)?;
262 if pending != Some(transition.transaction_id)
263 || transition.sequence < self.next_sequence
264 {
265 return Err(failure("invalid transition order"));
266 }
267 self.next_sequence = transition
268 .sequence
269 .checked_add(1)
270 .ok_or_else(|| failure("transition sequence exhausted"))?;
271 self.transitions.push(transition);
272 }
273 JournalEntry::Commit {
274 transaction_id,
275 snapshot,
276 } => {
277 if pending != Some(transaction_id) {
278 return Err(failure("commit without matching begin"));
279 }
280 validate(&self.links, &snapshot)?;
281 desired = Some(snapshot);
282 pending = None;
283 }
284 JournalEntry::Rollback { transaction_id } => {
285 if pending != Some(transaction_id) {
286 return Err(failure("rollback without matching begin"));
287 }
288 pending = None;
289 }
290 }
291 }
292 if let Some(desired) = desired {
293 restore(&mut self.links, &desired)?;
294 }
295 if let Some(transaction_id) = pending {
296 self.append(&JournalEntry::Rollback { transaction_id })?;
297 }
298 Ok(())
299 }
300
301 fn write<F>(&mut self, handler: WriteHandler<'_, T>, operation: F) -> Result<Flow, Error<T>>
302 where
303 F: FnOnce(&mut L, WriteHandler<'_, T>) -> Result<Flow, Error<T>>,
304 {
305 if self.poisoned {
306 return Err(failure(
307 "transaction journal is poisoned; reopen for recovery",
308 ));
309 }
310 let automatic = self.pending.is_none();
311 if automatic {
312 self.begin()?;
313 }
314 let pending = self
315 .pending
316 .as_mut()
317 .ok_or_else(|| failure("no active transaction"))?;
318 if pending.failed {
319 return Err(failure("transaction has a failed write; roll it back"));
320 }
321 let id = pending.id;
322 let mut handler = Fuse::new(handler);
323 let mut log_error = None;
324 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
325 operation(&mut self.links, &mut |before, after| {
326 if log_error.is_some() {
327 return Flow::Break;
328 }
329 if before != after {
330 let Some(next) = self.next_sequence.checked_add(1) else {
331 log_error = Some(io::Error::other("transition sequence exhausted"));
332 return Flow::Break;
333 };
334 let transition =
335 Transition::new(id, self.next_sequence, before.clone(), after.clone());
336 if self.tracing {
337 eprintln!("[doublets transactions] {transition:?}");
338 }
339 self.transitions.push(transition.clone());
341 self.next_sequence = next;
342 if let Err(error) = self.journal.append(&JournalEntry::Transition(transition)) {
343 log_error = Some(error);
344 return Flow::Break;
345 }
346 }
347 handler.call(before, after)
348 })
349 }));
350 let result = match result {
351 Ok(result) => result,
352 Err(panic) => {
353 pending.failed = true;
354 if automatic {
355 let _ = self.rollback();
356 }
357 std::panic::resume_unwind(panic);
358 }
359 };
360 let result = if let Some(error) = log_error {
361 self.poisoned = true;
362 Err(error.into())
363 } else {
364 result
365 };
366 if result.is_err() {
367 pending.failed = true;
368 }
369 if automatic {
370 match result {
371 Ok(flow) => {
372 if let Err(error) = self.commit() {
373 self.rollback()?;
374 return Err(error);
375 }
376 Ok(flow)
377 }
378 Err(error) => {
379 self.rollback()?;
380 Err(error)
381 }
382 }
383 } else {
384 result
385 }
386 }
387}
388
389impl<T: LinkReference, L: Doublets<T>, J: TransitionLog<T>> Links<T>
390 for TransactionsDecorator<T, L, J>
391{
392 forward!(count_links, each_links);
393
394 fn create_links(
395 &mut self,
396 query: &[T],
397 handler: WriteHandler<'_, T>,
398 ) -> Result<Flow, Error<T>> {
399 self.write(handler, |links, handler| links.create_links(query, handler))
400 }
401 fn update_links(
402 &mut self,
403 query: &[T],
404 change: &[T],
405 handler: WriteHandler<'_, T>,
406 ) -> Result<Flow, Error<T>> {
407 self.write(handler, |links, handler| {
408 links.update_links(query, change, handler)
409 })
410 }
411 fn delete_links(
412 &mut self,
413 query: &[T],
414 handler: WriteHandler<'_, T>,
415 ) -> Result<Flow, Error<T>> {
416 self.write(handler, |links, handler| links.delete_links(query, handler))
417 }
418}
419
420impl<T: LinkReference, L: Doublets<T>, J: TransitionLog<T>> Doublets<T>
421 for TransactionsDecorator<T, L, J>
422{
423 fn get_link(&self, index: T) -> Option<Link<T>> {
424 self.links.get_link(index)
425 }
426}
427
428#[must_use = "commit or rollback the transaction; dropping it rolls back"]
436pub struct Transaction<'a, T: LinkReference, L: Doublets<T>, J: TransitionLog<T>> {
437 store: &'a mut TransactionsDecorator<T, L, J>,
438}
439
440impl<T: LinkReference, L: Doublets<T>, J: TransitionLog<T>> Transaction<'_, T, L, J> {
441 pub fn commit(self) -> Result<(), Error<T>> {
443 self.store.commit()
444 }
445 pub fn rollback(self) -> Result<(), Error<T>> {
447 self.store.rollback()
448 }
449 #[must_use]
451 pub fn transitions(&self) -> &[Transition<T>] {
452 let start = self
453 .store
454 .pending
455 .as_ref()
456 .map_or(self.store.transitions.len(), |p| p.start);
457 &self.store.transitions[start..]
458 }
459}
460
461impl<T: LinkReference, L: Doublets<T>, J: TransitionLog<T>> Drop for Transaction<'_, T, L, J> {
462 fn drop(&mut self) {
463 let _ = self.store.rollback();
464 }
465}
466
467impl<T: LinkReference, L: Doublets<T>, J: TransitionLog<T>> Deref for Transaction<'_, T, L, J> {
468 type Target = TransactionsDecorator<T, L, J>;
469 fn deref(&self) -> &Self::Target {
470 self.store
471 }
472}
473impl<T: LinkReference, L: Doublets<T>, J: TransitionLog<T>> DerefMut for Transaction<'_, T, L, J> {
474 fn deref_mut(&mut self) -> &mut Self::Target {
475 self.store
476 }
477}
478
479impl<T: LinkReference, L: Doublets<T>, J: TransitionLog<T>> Links<T> for Transaction<'_, T, L, J> {
481 fn constants(&self) -> &LinksConstants<T> {
482 self.store.constants()
483 }
484 fn count_links(&self, query: &[T]) -> T {
485 self.store.count_links(query)
486 }
487 fn each_links(&self, query: &[T], handler: ReadHandler<'_, T>) -> Flow {
488 self.store.each_links(query, handler)
489 }
490 fn create_links(
491 &mut self,
492 query: &[T],
493 handler: WriteHandler<'_, T>,
494 ) -> Result<Flow, Error<T>> {
495 self.store.create_links(query, handler)
496 }
497 fn update_links(
498 &mut self,
499 query: &[T],
500 change: &[T],
501 handler: WriteHandler<'_, T>,
502 ) -> Result<Flow, Error<T>> {
503 self.store.update_links(query, change, handler)
504 }
505 fn delete_links(
506 &mut self,
507 query: &[T],
508 handler: WriteHandler<'_, T>,
509 ) -> Result<Flow, Error<T>> {
510 self.store.delete_links(query, handler)
511 }
512}
513impl<T: LinkReference, L: Doublets<T>, J: TransitionLog<T>> Doublets<T>
514 for Transaction<'_, T, L, J>
515{
516 fn get_link(&self, index: T) -> Option<Link<T>> {
517 self.store.get_link(index)
518 }
519}
520
521fn failure<T: LinkReference>(message: &str) -> Error<T> {
522 io::Error::other(message).into()
523}
524
525fn snapshot<T: LinkReference, L: Doublets<T>>(links: &L) -> Vec<Link<T>> {
526 let mut snapshot = Vec::new();
527 links.each_links(&[], &mut |link| {
528 snapshot.push(link);
529 Flow::Continue
530 });
531 snapshot.sort_by_key(|link| link.index);
532 snapshot
533}
534
535fn validate<T: LinkReference, L: Doublets<T>>(
536 links: &L,
537 desired: &[Link<T>],
538) -> Result<(), Error<T>> {
539 let mut indexes = BTreeSet::new();
540 for link in desired {
541 if !links.constants().is_internal(link.index) || !indexes.insert(link.index) {
542 return Err(failure(
543 "snapshot contains an invalid or duplicate link address",
544 ));
545 }
546 }
547 Ok(())
548}
549
550fn restore<T: LinkReference, L: Doublets<T>>(
551 links: &mut L,
552 desired: &[Link<T>],
553) -> Result<(), Error<T>> {
554 validate(links, desired)?;
555 let current = snapshot(links);
556 let desired: BTreeMap<_, _> = desired.iter().map(|link| (link.index, link)).collect();
557 if current.iter().eq(desired.values().copied()) {
558 return Ok(());
559 }
560 let null = T::from_byte(0);
563 for link in ¤t {
564 if link.source != null || link.target != null {
565 links.update(link.index, null, null)?;
566 }
567 }
568 let mut allocated: BTreeSet<_> = current.iter().map(|link| link.index).collect();
569 let mut missing: BTreeSet<_> = desired
570 .keys()
571 .copied()
572 .filter(|index| !allocated.contains(index))
573 .collect();
574 let limit = current
575 .iter()
576 .map(|link| link.index)
577 .chain(desired.keys().copied())
578 .max()
579 .unwrap_or(null);
580 while !missing.is_empty() {
581 let index = links.create()?;
582 if !allocated.insert(index) {
583 return Err(failure("store did not allocate a fresh link address"));
584 }
585 if index > limit {
586 return Err(failure("store cannot restore requested link addresses"));
587 }
588 missing.remove(&index);
589 }
590 for index in allocated.into_iter().rev() {
591 if !desired.contains_key(&index) {
592 links.delete(index)?;
593 }
594 }
595 for link in desired.values() {
596 if link.source != null || link.target != null {
597 links.update(link.index, link.source, link.target)?;
598 }
599 }
600 Ok(())
601}