1use super::{CommitHeadPublishError, CommitPlan};
5use crate::wal::PreparedWalSegment;
6use bytes::Bytes;
7use loonfs_api::wire::control::{
8 encode_control_object, ControlObjectKind, HeadState, HeadStateEnvelope, WalSegmentPointer,
9};
10use loonfs_api::ChangeSeq;
11use loonfs_objectstore::keys::wal_head;
12use loonfs_objectstore::ObjectStoreError;
13use loonfs_objectstore::{ObjectMetadata, ObjectStore};
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct PreparedCommitHeadPublish {
18 pub object_key: String,
19 pub resulting_head: HeadState,
20 pub envelope: HeadStateEnvelope,
21 pub encoded_bytes: Vec<u8>,
22}
23
24pub fn prepare_commit_head_publish(
25 current_head: &HeadState,
26 plan: &CommitPlan,
27 wal: &PreparedWalSegment,
28) -> Result<PreparedCommitHeadPublish, CommitHeadPublishError> {
29 if current_head.namespace_id != plan.namespace_id {
30 return Err(CommitHeadPublishError::NamespaceMismatch {
31 head: current_head.namespace_id.clone(),
32 plan: plan.namespace_id.clone(),
33 });
34 }
35
36 let wal_payload = &wal.envelope.payload;
37 if wal_payload.namespace_id != current_head.namespace_id {
38 return Err(CommitHeadPublishError::WalSegmentNamespaceMismatch {
39 head: current_head.namespace_id.clone(),
40 wal: wal_payload.namespace_id.clone(),
41 });
42 }
43 if wal_payload.writer_epoch != current_head.writer_epoch {
44 return Err(CommitHeadPublishError::WalSegmentWriterEpochMismatch {
45 expected: current_head.writer_epoch,
46 actual: wal_payload.writer_epoch,
47 });
48 }
49
50 if wal_payload.records.is_empty() {
51 return Err(CommitHeadPublishError::EmptyWalSegment);
52 }
53
54 if wal_payload.base_head_seq != current_head.seq {
55 return Err(CommitHeadPublishError::WalSegmentBaseHeadSeqMismatch {
56 expected: current_head.seq,
57 actual: wal_payload.base_head_seq,
58 });
59 }
60
61 let expected_start_seq = ChangeSeq(
62 current_head
63 .seq
64 .0
65 .checked_add(1)
66 .ok_or(CommitHeadPublishError::SeqOverflow)?,
67 );
68 if wal_payload.start_seq != expected_start_seq {
69 return Err(CommitHeadPublishError::WalSegmentStartSeqMismatch {
70 expected: expected_start_seq,
71 actual: wal_payload.start_seq,
72 });
73 }
74
75 if wal_payload.end_seq != plan.assigned_seq {
76 return Err(CommitHeadPublishError::WalSegmentEndSeqMismatch {
77 expected: plan.assigned_seq,
78 actual: wal_payload.end_seq,
79 });
80 }
81
82 let object_key = wal_head(current_head.namespace_id.as_str());
83 let new_tip = wal.envelope.pointer(wal.object_key.clone());
84 let resulting_head = HeadState {
85 namespace_id: current_head.namespace_id.clone(),
86 content_store_id: current_head.content_store_id.clone(),
90 fork_basis: current_head.fork_basis.clone(),
91 seq: plan.assigned_seq,
92 head_commit_id: plan.commit_id.clone(),
93 writer_epoch: current_head.writer_epoch,
94 writer: current_head.writer.clone(),
95 next_inode_id: plan.resulting_next_inode_id,
96 recent_segments: next_recent_segments(current_head, new_tip.clone()),
97 visible_wal_tip: Some(new_tip),
98 state: current_head.state,
99 };
100 current_head
101 .ensure_successor_identity(&resulting_head)
102 .map_err(CommitHeadPublishError::HeadIdentityDrift)?;
103 let envelope =
104 HeadStateEnvelope::from_state(ControlObjectKind::WalHead, resulting_head.clone()).map_err(
105 |err| CommitHeadPublishError::Codec {
106 object_key: object_key.clone(),
107 message: err.to_string(),
108 },
109 )?;
110 let encoded_bytes =
111 encode_control_object(&envelope).map_err(|err| CommitHeadPublishError::Codec {
112 object_key: object_key.clone(),
113 message: err.to_string(),
114 })?;
115
116 Ok(PreparedCommitHeadPublish {
117 object_key,
118 resulting_head,
119 envelope,
120 encoded_bytes,
121 })
122}
123
124const RECENT_SEGMENTS_LIMIT: usize = 32;
130
131fn next_recent_segments(
132 current_head: &HeadState,
133 new_tip: WalSegmentPointer,
134) -> Vec<WalSegmentPointer> {
135 let mut recent = Vec::with_capacity(RECENT_SEGMENTS_LIMIT);
136 recent.push(new_tip);
137 if current_head.recent_segments.is_empty() {
138 recent.extend(current_head.visible_wal_tip.iter().cloned());
141 } else {
142 recent.extend(current_head.recent_segments.iter().cloned());
143 }
144 recent.truncate(RECENT_SEGMENTS_LIMIT);
145 recent
146}
147
148pub async fn publish_commit_head<S: ObjectStore + ?Sized>(
149 store: &S,
150 expected_head_etag: &str,
151 prepared: &PreparedCommitHeadPublish,
152) -> Result<ObjectMetadata, CommitHeadPublishError> {
153 if expected_head_etag.trim().is_empty() {
154 return Err(CommitHeadPublishError::EmptyExpectedHeadEtag);
155 }
156
157 store
158 .compare_and_swap(
159 &prepared.object_key,
160 expected_head_etag,
161 Bytes::copy_from_slice(&prepared.encoded_bytes),
162 )
163 .await
164 .map_err(|error| map_object_store_error(&prepared.object_key, error))
165}
166
167fn map_object_store_error(object_key: &str, err: ObjectStoreError) -> CommitHeadPublishError {
168 match err {
169 ObjectStoreError::PreconditionFailed { .. } => CommitHeadPublishError::StaleHead,
170 ObjectStoreError::Transport { message, .. } => {
173 CommitHeadPublishError::OutcomeUnknown(message)
174 }
175 other => CommitHeadPublishError::Store {
176 object_key: object_key.to_owned(),
177 message: other.to_string(),
178 },
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use loonfs_objectstore::keys::wal_segment as wal_segment_key;
186
187 #[test]
188 fn head_cas_transport_failure_maps_to_unknown_outcome_not_failure() {
189 assert_eq!(
190 map_object_store_error(
191 "namespaces/demo/control/head.json",
192 ObjectStoreError::transport("namespaces/demo/control/head.json", "timeout"),
193 ),
194 CommitHeadPublishError::OutcomeUnknown("timeout".to_owned())
195 );
196 assert_eq!(
197 map_object_store_error(
198 "namespaces/demo/control/head.json",
199 ObjectStoreError::PreconditionFailed {
200 object_key: "namespaces/demo/control/head.json".to_owned(),
201 },
202 ),
203 CommitHeadPublishError::StaleHead
204 );
205 assert!(matches!(
206 map_object_store_error(
207 "namespaces/demo/control/head.json",
208 ObjectStoreError::NotFound {
209 object_key: "namespaces/demo/control/head.json".to_owned(),
210 },
211 ),
212 CommitHeadPublishError::Store { .. }
213 ));
214 }
215 use loonfs_api::wire::control::WriterBlock;
216 use loonfs_api::wire::wal::{WalCommitPayload, WalSegmentEnvelope, WalSegmentPayload};
217 use loonfs_api::{CommitId, InodeId, NamespaceId, WalSegmentId, WriterEpoch};
218
219 fn head(namespace_id: NamespaceId, seq: ChangeSeq) -> HeadState {
220 HeadState {
221 namespace_id,
222 content_store_id: loonfs_api::ContentStoreId::parse(
223 "cs_0123456789abcdef0123456789abcdef",
224 )
225 .expect("content store id"),
226 fork_basis: None,
227 seq,
228 head_commit_id: CommitId::parse("c_00000000000000000000000000000000")
229 .expect("commit id"),
230 writer_epoch: WriterEpoch(1),
231 writer: Some(WriterBlock {
232 writer_id: "writer-a".to_owned(),
233 acquired_at_ms: 1_000,
234 }),
235 next_inode_id: InodeId(10),
236 visible_wal_tip: None,
237 recent_segments: Vec::new(),
238 state: Default::default(),
239 }
240 }
241
242 fn plan(namespace_id: NamespaceId, assigned_seq: ChangeSeq) -> CommitPlan {
243 CommitPlan {
244 namespace_id,
245 commit_id: CommitId::parse("publish-plan").expect("valid commit id"),
246 apply_after_seq: ChangeSeq(assigned_seq.0.saturating_sub(1)),
247 assigned_seq,
248 validated_ops: Vec::new(),
249 resulting_next_inode_id: InodeId(10),
250 }
251 }
252
253 fn wal_segment(
254 namespace_id: NamespaceId,
255 base_head_seq: ChangeSeq,
256 start_seq: ChangeSeq,
257 end_seq: ChangeSeq,
258 record_count: usize,
259 ) -> PreparedWalSegment {
260 let records = (0..record_count)
261 .map(|index| {
262 let offset = u64::try_from(index).expect("test index");
263 let seq = ChangeSeq(start_seq.0 + offset);
264 WalCommitPayload {
265 seq,
266 commit_id: CommitId::parse(format!("publish-record-{index}"))
267 .expect("valid commit id"),
268 semantic_commit_fingerprint: format!("fingerprint-{index}"),
269 committed_at_ms: 4_200,
270 message: None,
271 deltas: Vec::new(),
272 }
273 })
274 .collect();
275 let segment_id =
276 WalSegmentId::parse("00000000000000000001-aaaaaaaaaaaaaaaa").expect("valid segment id");
277 let payload = WalSegmentPayload {
278 namespace_id: namespace_id.clone(),
279 segment_id: segment_id.clone(),
280 writer_epoch: WriterEpoch(1),
281 prev_visible_segment: None,
282 base_head_seq,
283 start_seq,
284 end_seq,
285 records,
286 };
287 let envelope = WalSegmentEnvelope::from_payload(payload).expect("wal envelope");
288 PreparedWalSegment {
289 object_key: wal_segment_key(namespace_id.as_str(), segment_id.as_str()),
290 segment_id,
291 envelope,
292 encoded_bytes: Vec::new(),
293 }
294 }
295
296 #[test]
297 fn head_publish_accepts_segment_connecting_current_head_to_plan() {
298 let namespace_id = NamespaceId::parse("demo").expect("valid namespace id");
299 let current_head = head(namespace_id.clone(), ChangeSeq(7));
300 let plan = plan(namespace_id.clone(), ChangeSeq(9));
301 let wal = wal_segment(namespace_id, ChangeSeq(7), ChangeSeq(8), ChangeSeq(9), 2);
302
303 let prepared =
304 prepare_commit_head_publish(¤t_head, &plan, &wal).expect("prepare head publish");
305
306 assert_eq!(prepared.resulting_head.seq, ChangeSeq(9));
307 assert_eq!(
308 prepared.resulting_head.visible_wal_tip,
309 Some(wal.envelope.pointer(wal.object_key.clone()))
310 );
311 }
312
313 #[test]
314 fn head_publish_seeds_recent_segments_from_the_prior_tip() {
315 let namespace_id = NamespaceId::parse("demo").expect("valid namespace id");
318 let mut current_head = head(namespace_id.clone(), ChangeSeq(7));
319 let prior = wal_segment(
320 namespace_id.clone(),
321 ChangeSeq(5),
322 ChangeSeq(6),
323 ChangeSeq(7),
324 2,
325 );
326 let prior_tip = prior.envelope.pointer(prior.object_key.clone());
327 current_head.visible_wal_tip = Some(prior_tip.clone());
328 let plan = plan(namespace_id.clone(), ChangeSeq(9));
329 let wal = wal_segment(namespace_id, ChangeSeq(7), ChangeSeq(8), ChangeSeq(9), 2);
330
331 let prepared =
332 prepare_commit_head_publish(¤t_head, &plan, &wal).expect("prepare head publish");
333
334 let new_tip = wal.envelope.pointer(wal.object_key.clone());
335 assert_eq!(
336 prepared.resulting_head.recent_segments,
337 vec![new_tip.clone(), prior_tip]
338 );
339 assert_eq!(prepared.resulting_head.visible_wal_tip, Some(new_tip));
340 }
341
342 #[test]
343 fn head_publish_prepends_the_tip_and_truncates_recent_segments() {
344 let namespace_id = NamespaceId::parse("demo").expect("valid namespace id");
345 let mut current_head = head(namespace_id.clone(), ChangeSeq(100));
346 let filler = |index: u64| {
347 let segment = wal_segment(
348 namespace_id.clone(),
349 ChangeSeq(index),
350 ChangeSeq(index + 1),
351 ChangeSeq(index + 1),
352 1,
353 );
354 segment.envelope.pointer(segment.object_key.clone())
355 };
356 current_head.recent_segments = (0..32).rev().map(filler).collect();
357 let oldest = current_head
358 .recent_segments
359 .last()
360 .cloned()
361 .expect("oldest");
362 let plan = plan(namespace_id.clone(), ChangeSeq(101));
363 let wal = wal_segment(
364 namespace_id,
365 ChangeSeq(100),
366 ChangeSeq(101),
367 ChangeSeq(101),
368 1,
369 );
370
371 let prepared =
372 prepare_commit_head_publish(¤t_head, &plan, &wal).expect("prepare head publish");
373
374 let recent = &prepared.resulting_head.recent_segments;
375 assert_eq!(recent.len(), 32);
376 assert_eq!(recent[0], wal.envelope.pointer(wal.object_key.clone()));
377 assert!(!recent.contains(&oldest), "oldest hint must fall off");
378 }
379
380 #[test]
381 fn head_publish_rejects_segment_base_after_current_head() {
382 let namespace_id = NamespaceId::parse("demo").expect("valid namespace id");
383 let current_head = head(namespace_id.clone(), ChangeSeq(7));
384 let plan = plan(namespace_id.clone(), ChangeSeq(9));
385 let wal = wal_segment(namespace_id, ChangeSeq(8), ChangeSeq(9), ChangeSeq(9), 1);
386
387 assert!(matches!(
388 prepare_commit_head_publish(¤t_head, &plan, &wal),
389 Err(CommitHeadPublishError::WalSegmentBaseHeadSeqMismatch {
390 expected: ChangeSeq(7),
391 actual: ChangeSeq(8),
392 })
393 ));
394 }
395
396 #[test]
397 fn head_publish_rejects_segment_base_before_current_head() {
398 let namespace_id = NamespaceId::parse("demo").expect("valid namespace id");
399 let current_head = head(namespace_id.clone(), ChangeSeq(7));
400 let plan = plan(namespace_id.clone(), ChangeSeq(9));
401 let wal = wal_segment(namespace_id, ChangeSeq(6), ChangeSeq(7), ChangeSeq(9), 3);
402
403 assert!(matches!(
404 prepare_commit_head_publish(¤t_head, &plan, &wal),
405 Err(CommitHeadPublishError::WalSegmentBaseHeadSeqMismatch {
406 expected: ChangeSeq(7),
407 actual: ChangeSeq(6),
408 })
409 ));
410 }
411
412 #[test]
413 fn head_publish_rejects_empty_segment() {
414 let namespace_id = NamespaceId::parse("demo").expect("valid namespace id");
415 let current_head = head(namespace_id.clone(), ChangeSeq(7));
416 let plan = plan(namespace_id.clone(), ChangeSeq(9));
417 let wal = wal_segment(namespace_id, ChangeSeq(7), ChangeSeq(8), ChangeSeq(9), 0);
418
419 assert!(matches!(
420 prepare_commit_head_publish(¤t_head, &plan, &wal),
421 Err(CommitHeadPublishError::EmptyWalSegment)
422 ));
423 }
424
425 #[test]
426 fn head_publish_rejects_segment_start_that_skips_current_head() {
427 let namespace_id = NamespaceId::parse("demo").expect("valid namespace id");
428 let current_head = head(namespace_id.clone(), ChangeSeq(7));
429 let plan = plan(namespace_id.clone(), ChangeSeq(9));
430 let wal = wal_segment(namespace_id, ChangeSeq(7), ChangeSeq(9), ChangeSeq(9), 1);
431
432 assert!(matches!(
433 prepare_commit_head_publish(¤t_head, &plan, &wal),
434 Err(CommitHeadPublishError::WalSegmentStartSeqMismatch {
435 expected: ChangeSeq(8),
436 actual: ChangeSeq(9),
437 })
438 ));
439 }
440
441 #[test]
442 fn head_publish_rejects_segment_end_that_differs_from_plan() {
443 let namespace_id = NamespaceId::parse("demo").expect("valid namespace id");
444 let current_head = head(namespace_id.clone(), ChangeSeq(7));
445 let plan = plan(namespace_id.clone(), ChangeSeq(9));
446 let wal = wal_segment(namespace_id, ChangeSeq(7), ChangeSeq(8), ChangeSeq(10), 3);
447
448 assert!(matches!(
449 prepare_commit_head_publish(¤t_head, &plan, &wal),
450 Err(CommitHeadPublishError::WalSegmentEndSeqMismatch {
451 expected: ChangeSeq(9),
452 actual: ChangeSeq(10),
453 })
454 ));
455 }
456
457 #[test]
458 fn head_publish_rejects_segment_namespace_mismatch() {
459 let current_head = head(
460 NamespaceId::parse("demo").expect("valid namespace id"),
461 ChangeSeq(7),
462 );
463 let plan = plan(
464 NamespaceId::parse("demo").expect("valid namespace id"),
465 ChangeSeq(9),
466 );
467 let wal = wal_segment(
468 NamespaceId::parse("other").expect("valid namespace id"),
469 ChangeSeq(7),
470 ChangeSeq(8),
471 ChangeSeq(9),
472 2,
473 );
474
475 assert!(matches!(
476 prepare_commit_head_publish(¤t_head, &plan, &wal),
477 Err(CommitHeadPublishError::WalSegmentNamespaceMismatch { head, wal })
478 if head == NamespaceId::parse("demo").expect("valid namespace id") && wal == NamespaceId::parse("other").expect("valid namespace id")
479 ));
480 }
481}