1use std::collections::BTreeMap;
2use std::sync::{Arc, Mutex, MutexGuard};
3use std::time::{Duration, Instant};
4
5use arc_swap::ArcSwap;
6
7use anyhow::Result;
8use thiserror::Error;
9
10use lora_analyzer::Analyzer;
11use lora_compiler::{CompiledQuery, Compiler};
12use lora_executor::{
13 classify_stream, compiled_result_columns, project_rows, ExecuteOptions, ExecutionContext,
14 Executor, LoraValue, MutableExecutionContext, MutableExecutor, MutablePullExecutor,
15 PullExecutor, QueryResult, Row, RowSource,
16};
17use lora_parser::parse_query;
18use lora_store::{InMemoryGraph, MutationEvent, MutationRecorder};
19use lora_wal::WalRecorder;
20
21use crate::error::LoraError;
22use crate::snapshot::ManagedSnapshotStore;
23use crate::stream::QueryStream;
24use crate::wal::write_scope::ensure_wal_not_poisoned;
25
26#[derive(Debug, Clone, PartialEq, Eq, Error)]
32pub enum TransactionError {
33 #[error("transaction is already closed")]
34 AlreadyClosed,
35
36 #[error("transaction has no live graph guard")]
37 NoGraphGuard,
38
39 #[error("transaction has no staged graph")]
40 NoStagedGraph,
41
42 #[error("cannot commit transaction while a streaming cursor is still active")]
43 CursorActiveCommit,
44
45 #[error("cannot start a new statement while a streaming cursor is still active")]
46 CursorActiveStatement,
47
48 #[error("cannot execute mutating query in read-only transaction")]
49 ReadOnlyMutation,
50
51 #[error("streaming write cursor requires a ReadWrite transaction")]
52 StreamingRequiresReadWrite,
53
54 #[error("read-only transaction cannot publish staged graph")]
55 ReadOnlyCommit,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum TransactionMode {
61 ReadOnly,
63 ReadWrite,
65}
66
67pub(crate) enum LiveStoreGuard<'db> {
77 Read(Arc<InMemoryGraph>),
78 Write(WriteLease<'db>),
79}
80
81pub(crate) struct WriteLease<'db> {
86 pub(crate) _writer_lock: MutexGuard<'db, ()>,
88 pub(crate) store: Arc<ArcSwap<InMemoryGraph>>,
90 pub(crate) snapshot: Arc<InMemoryGraph>,
93}
94
95impl LiveStoreGuard<'_> {
96 fn as_graph(&self) -> &InMemoryGraph {
97 match self {
98 Self::Read(arc) => arc,
99 Self::Write(lease) => &lease.snapshot,
100 }
101 }
102}
103
104pub(crate) struct Savepoint {
109 staged: Option<InMemoryGraph>,
110 buffer_len: usize,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub(crate) enum TxStreamOutcome {
118 Exhausted,
119 Interrupted,
120}
121
122impl TxStreamOutcome {
123 fn should_restore_savepoint(self, rollback_on_drop: bool) -> bool {
124 matches!(self, Self::Interrupted) && rollback_on_drop
125 }
126}
127
128pub(crate) struct TxCursorLease {
135 handle: Arc<Mutex<TxInner>>,
136 rollback_on_drop: bool,
137 finalized: bool,
138}
139
140impl TxCursorLease {
141 pub(crate) fn new(handle: Arc<Mutex<TxInner>>, rollback_on_drop: bool) -> Self {
142 Self {
143 handle,
144 rollback_on_drop,
145 finalized: false,
146 }
147 }
148
149 pub(crate) fn finalize(&mut self, outcome: TxStreamOutcome) {
150 if self.finalized {
151 return;
152 }
153 finalize_tx_stream(&self.handle, outcome, self.rollback_on_drop);
154 self.finalized = true;
155 }
156}
157
158impl Drop for TxCursorLease {
159 fn drop(&mut self) {
160 self.finalize(TxStreamOutcome::Interrupted);
161 }
162}
163
164pub(crate) struct BufferingRecorder {
176 buffer: Arc<Mutex<Vec<MutationEvent>>>,
177}
178
179impl BufferingRecorder {
180 pub(crate) fn new(buffer: Arc<Mutex<Vec<MutationEvent>>>) -> Self {
181 Self { buffer }
182 }
183}
184
185impl MutationRecorder for BufferingRecorder {
186 fn record(&self, event: MutationEvent) {
187 if let Ok(mut buf) = self.buffer.lock() {
188 buf.push(event);
189 }
190 }
191}
192
193pub(crate) struct TxInner {
198 pub(crate) staged: Option<InMemoryGraph>,
202 pub(crate) buffer: Arc<Mutex<Vec<MutationEvent>>>,
206 pub(crate) pending_savepoint: Option<Savepoint>,
210 pub(crate) cursor_active: bool,
214 pub(crate) closed: bool,
218 pub(crate) mode: TransactionMode,
220 pub(crate) buffer_mutations: bool,
224}
225
226pub struct Transaction<'db> {
242 pub(crate) live: Option<LiveStoreGuard<'db>>,
243 pub(crate) inner: Arc<Mutex<TxInner>>,
244 pub(crate) wal: Option<Arc<WalRecorder>>,
245 pub(crate) snapshots: Option<Arc<ManagedSnapshotStore>>,
246 mode: TransactionMode,
247}
248
249impl<'db> Transaction<'db> {
250 pub(crate) fn new(
252 live: LiveStoreGuard<'db>,
253 wal: Option<Arc<WalRecorder>>,
254 snapshots: Option<Arc<ManagedSnapshotStore>>,
255 mode: TransactionMode,
256 ) -> Self {
257 let buffer_mutations = wal.is_some();
258 let inner = TxInner {
259 staged: None,
260 buffer: Arc::new(Mutex::new(Vec::new())),
261 pending_savepoint: None,
262 cursor_active: false,
263 closed: false,
264 mode,
265 buffer_mutations,
266 };
267 Self {
268 live: Some(live),
269 inner: Arc::new(Mutex::new(inner)),
270 wal,
271 snapshots,
272 mode,
273 }
274 }
275
276 pub fn mode(&self) -> TransactionMode {
278 self.mode
279 }
280
281 pub fn execute(
284 &mut self,
285 query: &str,
286 options: Option<ExecuteOptions>,
287 ) -> Result<QueryResult, LoraError> {
288 self.execute_with_params(query, options, BTreeMap::new())
289 }
290
291 pub fn execute_with_timeout(
293 &mut self,
294 query: &str,
295 options: Option<ExecuteOptions>,
296 timeout: Duration,
297 ) -> Result<QueryResult, LoraError> {
298 let deadline = Instant::now()
299 .checked_add(timeout)
300 .unwrap_or_else(Instant::now);
301 let rows =
302 self.execute_rows_with_params_deadline(query, BTreeMap::new(), Some(deadline))?;
303 Ok(project_rows(rows, options.unwrap_or_default()))
304 }
305
306 pub fn execute_with_params(
308 &mut self,
309 query: &str,
310 options: Option<ExecuteOptions>,
311 params: BTreeMap<String, LoraValue>,
312 ) -> Result<QueryResult, LoraError> {
313 let rows = self.execute_rows_with_params_deadline(query, params, None)?;
314 Ok(project_rows(rows, options.unwrap_or_default()))
315 }
316
317 pub fn execute_with_params_timeout(
320 &mut self,
321 query: &str,
322 options: Option<ExecuteOptions>,
323 params: BTreeMap<String, LoraValue>,
324 timeout: Duration,
325 ) -> Result<QueryResult, LoraError> {
326 let deadline = Instant::now()
327 .checked_add(timeout)
328 .unwrap_or_else(Instant::now);
329 let rows = self.execute_rows_with_params_deadline(query, params, Some(deadline))?;
330 Ok(project_rows(rows, options.unwrap_or_default()))
331 }
332
333 pub fn execute_rows(&mut self, query: &str) -> Result<Vec<Row>, LoraError> {
336 self.execute_rows_with_params(query, BTreeMap::new())
337 }
338
339 pub fn execute_rows_with_params(
342 &mut self,
343 query: &str,
344 params: BTreeMap<String, LoraValue>,
345 ) -> Result<Vec<Row>, LoraError> {
346 Ok(self.execute_rows_with_params_deadline(query, params, None)?)
347 }
348
349 fn execute_rows_with_params_deadline(
350 &mut self,
351 query: &str,
352 params: BTreeMap<String, LoraValue>,
353 deadline: Option<Instant>,
354 ) -> Result<Vec<Row>> {
355 let compiled = self.compile_in_tx(query)?;
356 self.execute_rows_compiled_deadline(&compiled, params, deadline)
357 }
358
359 fn execute_rows_compiled_deadline(
360 &mut self,
361 compiled: &CompiledQuery,
362 params: BTreeMap<String, LoraValue>,
363 deadline: Option<Instant>,
364 ) -> Result<Vec<Row>> {
365 if self.is_read_only_unchecked() {
367 self.precheck_open_no_savepoint()?;
368 let live = self.live.as_ref().ok_or(TransactionError::NoGraphGuard)?;
369 let storage = live.as_graph();
370 let executor = Executor::with_deadline(ExecutionContext { storage, params }, deadline);
371 return executor
372 .execute_compiled_rows(compiled)
373 .map_err(anyhow::Error::from);
374 }
375
376 let mut inner = self.begin_statement()?;
378 let is_mutating = classify_stream(compiled).is_mutating();
379
380 if !is_mutating {
381 return match inner.staged.as_ref() {
387 Some(staged) => {
388 let executor = Executor::with_deadline(
389 ExecutionContext {
390 storage: staged,
391 params,
392 },
393 deadline,
394 );
395 executor
396 .execute_compiled_rows(compiled)
397 .map_err(anyhow::Error::from)
398 }
399 None => {
400 drop(inner);
401 let live = self.live.as_ref().ok_or(TransactionError::NoGraphGuard)?;
402 let storage = live.as_graph();
403 let executor =
404 Executor::with_deadline(ExecutionContext { storage, params }, deadline);
405 executor
406 .execute_compiled_rows(compiled)
407 .map_err(anyhow::Error::from)
408 }
409 };
410 }
411
412 let clone_savepoint_graph = inner.staged.is_some();
416 self.ensure_staged_locked(&mut inner)?;
417 let savepoint = Some(take_savepoint(&inner, clone_savepoint_graph));
418
419 let exec_result: ExecResultRows = {
420 let staged = inner.staged_mut()?;
421 let mut executor = MutableExecutor::with_deadline(
422 MutableExecutionContext {
423 storage: staged,
424 params,
425 },
426 deadline,
427 );
428 executor
429 .execute_compiled_rows(compiled)
430 .map_err(anyhow::Error::from)
431 };
432
433 match exec_result {
434 Ok(rows) => Ok(rows),
435 Err(err) => {
436 restore_savepoint(&mut inner, savepoint);
437 Err(err)
438 }
439 }
440 }
441
442 pub(crate) fn open_streaming_compiled_autocommit(
471 &mut self,
472 compiled: Arc<CompiledQuery>,
473 params: BTreeMap<String, LoraValue>,
474 ) -> Result<Box<dyn RowSource + 'static>> {
475 if self.is_read_only_unchecked() {
476 return Err(TransactionError::StreamingRequiresReadWrite.into());
477 }
478
479 let mut inner = self.begin_statement()?;
480 self.ensure_staged_locked(&mut inner)?;
481 inner.activate_cursor();
482
483 let staged_ptr: *mut InMemoryGraph = inner
487 .staged
488 .as_mut()
489 .expect("ensure_staged_locked guarantees Some")
490 as *mut _;
491 drop(inner);
492
493 let storage_static: &'static mut InMemoryGraph = unsafe { &mut *staged_ptr };
499 let compiled_static: &'static CompiledQuery =
500 unsafe { std::mem::transmute::<&CompiledQuery, _>(compiled.as_ref()) };
501
502 let cursor = MutablePullExecutor::new(storage_static, params)
506 .open_compiled(compiled_static)
507 .map_err(|e| {
508 if let Ok(mut inner) = self.inner.lock() {
512 discard_transaction_state(&mut inner);
513 }
514 self.live.take();
515 anyhow::Error::from(e)
516 })?;
517
518 Ok(Box::new(StreamingCursorWithArc {
524 cursor,
525 _compiled: compiled,
526 }))
527 }
528
529 fn compile_in_tx(&self, query: &str) -> Result<CompiledQuery> {
535 let document = parse_query(query)?;
536 let resolved = {
537 let inner = self.lock_inner_unchecked();
538 if let Some(staged) = &inner.staged {
539 let mut analyzer = Analyzer::new(staged);
540 analyzer.analyze(&document)?
541 } else {
542 drop(inner);
543 let live = self.live.as_ref().ok_or(TransactionError::NoGraphGuard)?;
544 let mut analyzer = Analyzer::new(live.as_graph());
545 analyzer.analyze(&document)?
546 }
547 };
548 Ok(Compiler::compile(&resolved))
549 }
550
551 fn ensure_staged_locked(&self, inner: &mut MutexGuard<'_, TxInner>) -> Result<()> {
555 if inner.staged.is_some() {
556 return Ok(());
557 }
558 let live = self.live.as_ref().ok_or(TransactionError::NoGraphGuard)?;
559 let mut staged: InMemoryGraph = live.as_graph().clone();
560 if matches!(inner.mode, TransactionMode::ReadWrite) && inner.buffer_mutations {
561 staged.set_mutation_recorder(Some(
562 Arc::new(BufferingRecorder::new(inner.buffer.clone())) as Arc<dyn MutationRecorder>,
563 ));
564 }
565 inner.staged = Some(staged);
566 Ok(())
567 }
568
569 pub fn stream(&mut self, query: &str) -> Result<QueryStream<'static>, LoraError> {
571 self.stream_with_params(query, BTreeMap::new())
572 }
573
574 pub fn stream_with_params(
577 &mut self,
578 query: &str,
579 params: BTreeMap<String, LoraValue>,
580 ) -> Result<QueryStream<'static>, LoraError> {
581 let compiled = Arc::new(self.compile_in_tx(query)?);
582 let columns = compiled_result_columns(&compiled);
583 Ok(self.stream_compiled(compiled, columns, params)?)
584 }
585
586 pub(crate) fn stream_compiled(
590 &mut self,
591 compiled: Arc<CompiledQuery>,
592 columns: Vec<String>,
593 params: BTreeMap<String, LoraValue>,
594 ) -> Result<QueryStream<'static>> {
595 let mut inner = self.begin_statement()?;
596 let is_mutating = classify_stream(&compiled).is_mutating();
597 if matches!(inner.mode, TransactionMode::ReadOnly) && is_mutating {
598 return Err(TransactionError::ReadOnlyMutation.into());
599 }
600
601 let clone_savepoint_graph = inner.staged.is_some();
606 self.ensure_staged_locked(&mut inner)?;
607 inner.activate_cursor();
608
609 let rollback_on_drop = is_mutating;
610 if rollback_on_drop {
611 inner.pending_savepoint = Some(take_savepoint(&inner, clone_savepoint_graph));
612 } else {
613 inner.pending_savepoint = None;
614 }
615
616 let staged_ptr: *mut InMemoryGraph = inner
617 .staged
618 .as_mut()
619 .expect("ensure_staged_locked guarantees Some")
620 as *mut _;
621 drop(inner);
622
623 let compiled_static: &'static CompiledQuery =
624 unsafe { std::mem::transmute::<&CompiledQuery, _>(compiled.as_ref()) };
625 let cursor: Result<Box<dyn RowSource + 'static>> = if is_mutating {
626 let storage_static: &'static mut InMemoryGraph = unsafe { &mut *staged_ptr };
627 MutablePullExecutor::new(storage_static, params)
628 .open_compiled(compiled_static)
629 .map(|cursor| {
630 Box::new(StreamingCursorWithArc {
631 cursor,
632 _compiled: compiled.clone(),
633 }) as Box<dyn RowSource + 'static>
634 })
635 .map_err(anyhow::Error::from)
636 } else {
637 let storage_static: &'static InMemoryGraph = unsafe { &*staged_ptr };
638 PullExecutor::new(storage_static, params)
639 .open_compiled(compiled_static)
640 .map(|cursor| {
641 Box::new(StreamingCursorWithArc {
642 cursor,
643 _compiled: compiled.clone(),
644 }) as Box<dyn RowSource + 'static>
645 })
646 .map_err(anyhow::Error::from)
647 };
648
649 match cursor {
650 Ok(cursor) => Ok(QueryStream::for_tx_cursor(
651 cursor,
652 columns,
653 TxCursorLease::new(self.inner.clone(), rollback_on_drop),
654 )),
655 Err(err) => {
656 finalize_tx_stream(&self.inner, TxStreamOutcome::Interrupted, rollback_on_drop);
657 Err(err)
658 }
659 }
660 }
661
662 pub fn commit(mut self) -> Result<(), LoraError> {
669 let CommitState {
670 staged,
671 buffer_events,
672 mode,
673 } = self.take_commit_state()?;
674
675 let wrote_wal_commit = self.replay_commit_wal(mode, buffer_events)?;
676 self.publish_staged_graph(mode, staged, wrote_wal_commit)?;
677
678 self.live.take();
679 Ok(())
680 }
681
682 fn take_commit_state(&self) -> Result<CommitState> {
683 let mut inner = self.inner.lock().unwrap();
684 if inner.cursor_active {
685 return Err(TransactionError::CursorActiveCommit.into());
686 }
687 if inner.closed {
688 return Err(TransactionError::AlreadyClosed.into());
689 }
690
691 let mode = inner.mode;
692 let staged = inner.staged.take();
696 let buffer_events = std::mem::take(&mut *inner.buffer.lock().unwrap());
697 inner.closed = true;
698
699 Ok(CommitState {
700 staged,
701 buffer_events,
702 mode,
703 })
704 }
705
706 fn replay_commit_wal(
707 &self,
708 mode: TransactionMode,
709 buffer_events: Vec<MutationEvent>,
710 ) -> Result<bool> {
711 let Some(rec) = &self.wal else {
712 return Ok(false);
713 };
714
715 if !matches!(mode, TransactionMode::ReadWrite) {
716 ensure_wal_not_poisoned(rec)?;
717 return Ok(false);
718 }
719
720 Ok(rec.commit_events(buffer_events)?.wrote())
721 }
722
723 fn publish_staged_graph(
724 &mut self,
725 mode: TransactionMode,
726 staged: Option<InMemoryGraph>,
727 wrote_wal_commit: bool,
728 ) -> Result<()> {
729 if !matches!(mode, TransactionMode::ReadWrite) {
730 return Ok(());
731 }
732
733 let Some(mut staged) = staged else {
734 return Ok(());
735 };
736
737 staged.set_mutation_recorder(None);
742 let wal = self.wal.clone();
743 if let Some(rec) = &wal {
744 staged.set_mutation_recorder(Some(rec.clone() as Arc<dyn MutationRecorder>));
745 }
746
747 let live = self.live.as_mut().ok_or(TransactionError::NoGraphGuard)?;
748 let lease = match live {
749 LiveStoreGuard::Write(lease) => lease,
750 LiveStoreGuard::Read(_) => {
751 return Err(TransactionError::ReadOnlyCommit.into());
752 }
753 };
754
755 if wrote_wal_commit {
756 if let (Some(snapshots), Some(rec)) = (&self.snapshots, wal.as_ref()) {
757 snapshots.observe_commit(&staged, rec)?;
758 }
759 }
760
761 lease.store.store(Arc::new(staged));
765
766 Ok(())
767 }
768
769 pub fn rollback(mut self) -> Result<(), LoraError> {
772 let mut inner = self.inner.lock().unwrap();
773 if inner.closed {
774 return Err(TransactionError::AlreadyClosed.into());
775 }
776 discard_transaction_state(&mut inner);
777 drop(inner);
778 self.live.take();
779 Ok(())
780 }
781
782 fn begin_statement(&self) -> Result<MutexGuard<'_, TxInner>> {
788 let inner = self.inner.lock().unwrap();
789 if inner.closed {
790 return Err(TransactionError::AlreadyClosed.into());
791 }
792 if inner.cursor_active {
793 return Err(TransactionError::CursorActiveStatement.into());
794 }
795 Ok(inner)
796 }
797
798 fn precheck_open_no_savepoint(&self) -> Result<()> {
802 let inner = self.inner.lock().unwrap();
803 if inner.closed {
804 return Err(TransactionError::AlreadyClosed.into());
805 }
806 if inner.cursor_active {
807 return Err(TransactionError::CursorActiveStatement.into());
808 }
809 Ok(())
810 }
811
812 fn is_read_only_unchecked(&self) -> bool {
816 matches!(self.mode, TransactionMode::ReadOnly)
817 }
818
819 fn lock_inner_unchecked(&self) -> MutexGuard<'_, TxInner> {
820 self.inner
821 .lock()
822 .unwrap_or_else(|poisoned| poisoned.into_inner())
823 }
824
825 pub(crate) fn release_streaming_cursor(&self) {
826 if let Ok(mut inner) = self.inner.lock() {
827 inner.release_cursor();
828 }
829 }
830}
831
832type ExecResultRows = Result<Vec<Row>>;
833
834struct CommitState {
835 staged: Option<InMemoryGraph>,
836 buffer_events: Vec<MutationEvent>,
837 mode: TransactionMode,
838}
839
840impl TxInner {
841 fn staged_mut(&mut self) -> Result<&mut InMemoryGraph> {
842 self.staged
843 .as_mut()
844 .ok_or(TransactionError::NoStagedGraph.into())
845 }
846
847 fn activate_cursor(&mut self) {
848 self.cursor_active = true;
849 }
850
851 fn release_cursor(&mut self) {
852 self.cursor_active = false;
853 }
854
855 fn clear_pending_savepoint(&mut self) {
856 self.pending_savepoint = None;
857 }
858
859 fn restore_pending_savepoint(&mut self) {
860 if let Some(sp) = self.pending_savepoint.take() {
861 apply_savepoint(self, sp);
862 }
863 }
864
865 fn finalize_stream(&mut self, outcome: TxStreamOutcome, rollback_on_drop: bool) {
866 self.release_cursor();
867
868 if self.closed {
869 discard_transaction_state(self);
870 return;
871 }
872
873 if outcome.should_restore_savepoint(rollback_on_drop) {
874 self.restore_pending_savepoint();
875 } else {
876 self.clear_pending_savepoint();
877 }
878 }
879}
880
881struct StreamingCursorWithArc {
886 cursor: Box<dyn RowSource + 'static>,
887 _compiled: Arc<CompiledQuery>,
888}
889
890impl RowSource for StreamingCursorWithArc {
891 fn next_row(&mut self) -> lora_executor::ExecResult<Option<Row>> {
892 self.cursor.next_row()
893 }
894}
895
896fn finalize_tx_stream(
897 handle: &Arc<Mutex<TxInner>>,
898 outcome: TxStreamOutcome,
899 rollback_on_drop: bool,
900) {
901 if let Ok(mut inner) = handle.lock() {
902 inner.finalize_stream(outcome, rollback_on_drop);
903 }
904}
905
906fn discard_transaction_state(inner: &mut TxInner) {
907 inner.clear_pending_savepoint();
909 inner.release_cursor();
910 inner.staged = None;
911 if let Ok(mut buf) = inner.buffer.lock() {
912 buf.clear();
913 }
914 inner.closed = true;
915}
916
917fn take_savepoint(inner: &TxInner, clone_staged: bool) -> Savepoint {
918 let buffer_len = inner.buffer.lock().ok().map(|b| b.len()).unwrap_or(0);
919 Savepoint {
920 staged: if clone_staged {
921 inner.staged.as_ref().cloned()
922 } else {
923 None
924 },
925 buffer_len,
926 }
927}
928
929fn restore_savepoint(inner: &mut TxInner, savepoint: Option<Savepoint>) {
930 if let Some(sp) = savepoint {
931 apply_savepoint(inner, sp);
932 }
933}
934
935fn apply_savepoint(inner: &mut TxInner, sp: Savepoint) {
936 if let Ok(mut buf) = inner.buffer.lock() {
937 buf.truncate(sp.buffer_len);
938 }
939
940 let Some(mut graph) = sp.staged else {
941 inner.staged = None;
942 return;
943 };
944
945 if matches!(inner.mode, TransactionMode::ReadWrite) && inner.buffer_mutations {
949 graph.set_mutation_recorder(Some(
950 Arc::new(BufferingRecorder::new(inner.buffer.clone())) as Arc<dyn MutationRecorder>
951 ));
952 }
953 inner.staged = Some(graph);
954}
955
956impl Drop for Transaction<'_> {
957 fn drop(&mut self) {
958 if let Ok(mut inner) = self.inner.lock() {
962 if !inner.closed {
963 if inner.cursor_active {
964 inner.closed = true;
970 } else {
971 discard_transaction_state(&mut inner);
972 }
973 }
974 }
975 }
976}