link_cli/transactions/
recovery.rs1use std::path::Path;
12
13use doublets::data::LinkReference;
14
15use crate::error::LinkError;
16use crate::storage::LinksStorage;
17
18use super::log::TransitionLogStore;
19use super::types::{
20 GenericTransition, LogRetentionPolicy, TransitionKind, APPLIED_MARKER_PREFIX,
21 COMMIT_MARKER_PREFIX, ROLLBACK_MARKER_PREFIX, TRANSITION_NAME_PREFIX,
22};
23use super::{insert_ordered, new_transaction_id, now_unix_ms, GenericTransactionsDecorator};
24
25impl<T, S, L> GenericTransactionsDecorator<T, S, L>
26where
27 T: LinkReference,
28 S: LinksStorage<T>,
29 L: TransitionLogStore,
30{
31 pub fn apply_transition(&mut self, transition: &GenericTransition<T>) {
34 self.replaying = true;
35 self.try_apply_transition(transition, false);
36 self.replaying = false;
37 }
38
39 pub fn revert_transition(&mut self, transition: &GenericTransition<T>) {
42 self.replaying = true;
43 self.try_revert_transition(transition);
44 self.replaying = false;
45 }
46
47 pub(super) fn try_apply_transition(
48 &mut self,
49 transition: &GenericTransition<T>,
50 record_applied: bool,
51 ) {
52 let zero = T::from_byte(0);
53 let result: Result<(), LinkError> = match transition.kind {
54 TransitionKind::Create => {
55 if transition.after.index != zero && !self.inner.link_exists(transition.after.index)
56 {
57 self.inner
58 .ensure_link_created(transition.after.index)
59 .and_then(|_| {
60 self.inner
61 .update_link(
62 transition.after.index,
63 transition.after.source,
64 transition.after.target,
65 )
66 .map(|_| ())
67 })
68 } else {
69 Ok(())
70 }
71 }
72 TransitionKind::Update => {
73 if transition.after.index != zero && self.inner.link_exists(transition.after.index)
74 {
75 self.inner
76 .update_link(
77 transition.after.index,
78 transition.after.source,
79 transition.after.target,
80 )
81 .map(|_| ())
82 } else {
83 Ok(())
84 }
85 }
86 TransitionKind::Delete => {
87 if transition.before.index != zero
88 && self.inner.link_exists(transition.before.index)
89 {
90 self.inner.delete_link(transition.before.index).map(|_| ())
91 } else {
92 Ok(())
93 }
94 }
95 };
96 if let Err(e) = result {
97 if self.trace {
98 eprintln!(
99 "[Transactions] Failed to apply transition seq={}: {e}",
100 transition.sequence
101 );
102 }
103 }
104 if record_applied {
105 let _ = self.mark_applied(transition);
106 }
107 }
108
109 pub(super) fn try_revert_transition(&mut self, transition: &GenericTransition<T>) {
110 let zero = T::from_byte(0);
111 let result: Result<(), LinkError> = match transition.kind {
112 TransitionKind::Create => {
113 if transition.after.index != zero && self.inner.link_exists(transition.after.index)
114 {
115 self.inner.delete_link(transition.after.index).map(|_| ())
116 } else {
117 Ok(())
118 }
119 }
120 TransitionKind::Update => {
121 if transition.before.index != zero
122 && self.inner.link_exists(transition.before.index)
123 {
124 self.inner
125 .update_link(
126 transition.before.index,
127 transition.before.source,
128 transition.before.target,
129 )
130 .map(|_| ())
131 } else {
132 Ok(())
133 }
134 }
135 TransitionKind::Delete => {
136 if transition.before.index != zero
137 && !self.inner.link_exists(transition.before.index)
138 {
139 self.inner
140 .ensure_link_created(transition.before.index)
141 .and_then(|_| {
142 self.inner
143 .update_link(
144 transition.before.index,
145 transition.before.source,
146 transition.before.target,
147 )
148 .map(|_| ())
149 })
150 } else {
151 Ok(())
152 }
153 }
154 };
155 if let Err(e) = result {
156 if self.trace {
157 eprintln!(
158 "[Transactions] Failed to revert transition seq={}: {e}",
159 transition.sequence
160 );
161 }
162 }
163 }
164
165 pub(super) fn mark_applied(
166 &mut self,
167 transition: &GenericTransition<T>,
168 ) -> Result<(), LinkError> {
169 if self.applied.insert(transition.sequence) {
170 self.write_marker(&format!("{APPLIED_MARKER_PREFIX}{}", transition.sequence))?;
171 if transition.sequence > self.applied_sequence {
172 self.applied_sequence = transition.sequence;
173 }
174 }
175 Ok(())
176 }
177
178 pub fn recover(&mut self) -> Result<(), LinkError> {
190 self.log.clear();
191 self.committed.clear();
192 self.rolled_back.clear();
193 self.applied.clear();
194 self.sequence_counter = 0;
195 self.applied_sequence = 0;
196
197 for entry in self.log_store.read_log_entries()? {
198 if let Some(payload) = entry.strip_prefix(TRANSITION_NAME_PREFIX) {
199 match GenericTransition::<T>::parse(payload) {
200 Ok(transition) => {
201 insert_ordered(&mut self.log, transition);
202 if transition.sequence > self.sequence_counter {
203 self.sequence_counter = transition.sequence;
204 }
205 }
206 Err(LinkError::AddressOutOfRange(value)) => {
207 return Err(LinkError::AddressOutOfRange(value))
208 }
209 Err(error) => {
210 if self.trace {
211 eprintln!("[Transactions] Skipping unreadable log entry: {error}");
212 }
213 }
214 }
215 } else if let Some(rest) = entry.strip_prefix(COMMIT_MARKER_PREFIX) {
216 if let Ok(tx_id) = u128::from_str_radix(rest, 16) {
217 self.committed.insert(tx_id);
218 }
219 } else if let Some(rest) = entry.strip_prefix(ROLLBACK_MARKER_PREFIX) {
220 if let Ok(tx_id) = u128::from_str_radix(rest, 16) {
221 self.rolled_back.insert(tx_id);
222 }
223 } else if let Some(rest) = entry.strip_prefix(APPLIED_MARKER_PREFIX) {
224 if let Ok(seq) = rest.parse::<i64>() {
225 self.applied.insert(seq);
226 if seq > self.applied_sequence {
227 self.applied_sequence = seq;
228 }
229 }
230 }
231 }
232
233 let log_snapshot: Vec<GenericTransition<T>> = self.log.clone();
235 self.replaying = true;
236 for transition in &log_snapshot {
237 if !self.committed.contains(&transition.transaction_id) {
238 continue;
239 }
240 if self.applied.contains(&transition.sequence) {
241 continue;
242 }
243 self.try_apply_transition(transition, true);
244 }
245 let mut pending_tx_ids: Vec<u128> = Vec::new();
247 for transition in log_snapshot.iter().rev() {
248 if self.committed.contains(&transition.transaction_id) {
249 continue;
250 }
251 if self.rolled_back.contains(&transition.transaction_id) {
252 continue;
253 }
254 self.try_revert_transition(transition);
255 if !pending_tx_ids.contains(&transition.transaction_id) {
256 pending_tx_ids.push(transition.transaction_id);
257 }
258 }
259 self.replaying = false;
260 for tx_id in pending_tx_ids {
261 self.rolled_back.insert(tx_id);
262 self.write_marker(&format!("{ROLLBACK_MARKER_PREFIX}{tx_id:032x}"))?;
263 }
264 Ok(())
265 }
266
267 pub(super) fn enforce_retention(&mut self) -> Result<(), LinkError> {
268 match self.retention_policy.clone() {
269 LogRetentionPolicy::Infinite => Ok(()),
270 LogRetentionPolicy::Sized { max_transitions } => self.enforce_sized(max_transitions),
271 LogRetentionPolicy::Chunked {
272 chunk_size,
273 archive_directory,
274 } => self.enforce_chunked(chunk_size, &archive_directory),
275 }
276 }
277
278 fn enforce_sized(&mut self, max_transitions: u64) -> Result<(), LinkError> {
279 if max_transitions == 0 {
280 return Ok(());
281 }
282 while self.log.len() as u64 > max_transitions {
283 let head = self.log[0];
284 if !self.applied.contains(&head.sequence) {
285 self.replaying = true;
286 self.try_apply_transition(&head, true);
287 self.replaying = false;
288 if !self.applied.contains(&head.sequence) {
289 break; }
291 }
292 self.log.remove(0);
293 if self.trace {
294 eprintln!(
295 "[Transactions] Dropped applied transition seq={} per sized retention.",
296 head.sequence
297 );
298 }
299 }
300 Ok(())
301 }
302
303 fn enforce_chunked(
304 &mut self,
305 chunk_size: u64,
306 archive_directory: &Path,
307 ) -> Result<(), LinkError> {
308 if chunk_size == 0 {
309 return Ok(());
310 }
311 if (self.log.len() as u64) < chunk_size {
312 return Ok(());
313 }
314 let chunk: Vec<GenericTransition<T>> =
315 self.log.iter().take(chunk_size as usize).copied().collect();
316 for transition in &chunk {
317 if !self.applied.contains(&transition.sequence) {
318 self.replaying = true;
319 self.try_apply_transition(transition, true);
320 self.replaying = false;
321 if !self.applied.contains(&transition.sequence) {
322 return Ok(()); }
324 }
325 }
326 std::fs::create_dir_all(archive_directory).map_err(|error| {
327 LinkError::StorageError(format!(
328 "failed to create archive dir {}: {error}",
329 archive_directory.display()
330 ))
331 })?;
332 let timestamp = now_unix_ms();
333 let file_name = format!(
334 "transitions-chunk-{timestamp}-{:032x}.log",
335 new_transaction_id()
336 );
337 let path = archive_directory.join(file_name);
338 use std::io::Write;
339 let mut file = std::fs::File::create(&path).map_err(|error| {
340 LinkError::StorageError(format!(
341 "failed to create archive file {}: {error}",
342 path.display()
343 ))
344 })?;
345 for transition in &chunk {
346 writeln!(file, "{}", transition.serialize())?;
347 }
348 file.flush()?;
349 if self.trace {
350 eprintln!(
351 "[Transactions] Archived {} transitions to {}.",
352 chunk.len(),
353 path.display()
354 );
355 }
356 self.log.drain(0..chunk.len());
357 Ok(())
358 }
359}