1use serde::{Deserialize, Serialize};
4
5use eredu_core::cache::{CacheBlockId, CacheTier};
6
7#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum CacheStoragePhase {
11 Device,
13 DemotingToHost,
15 HostUnbacked,
17 HostWriting,
19 HostBacked,
21 DiskReady,
23 DiskReading,
25}
26
27impl CacheStoragePhase {
28 pub const fn tier(self) -> CacheTier {
30 match self {
31 Self::Device | Self::DemotingToHost => CacheTier::Device,
32 Self::HostUnbacked | Self::HostWriting | Self::HostBacked => CacheTier::Host,
33 Self::DiskReady | Self::DiskReading => CacheTier::Disk,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum CacheIoOperationKind {
42 Write,
44 Read,
46}
47
48#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
50pub struct CacheIoOperationKey {
51 pub generation: u64,
53 pub id: CacheBlockId,
55 pub kind: CacheIoOperationKind,
57}
58
59pub trait CacheHostDemotionOperation {
61 fn block_id(&self) -> &CacheBlockId;
63
64 fn operation_id(&self) -> u64;
66}
67
68pub trait CacheIoOperation {
70 fn key(&self) -> &CacheIoOperationKey;
72}
73
74#[derive(Debug)]
76pub struct CacheHostPromotion<H> {
77 id: CacheBlockId,
78 source: CacheStoragePhase,
79 host: H,
80}
81
82#[derive(Debug, Clone)]
88pub struct CacheBlockStorage<D, H, B, HD, IO> {
89 id: CacheBlockId,
90 phase: CacheStoragePhase,
91 device: Option<D>,
92 host: Option<H>,
93 backing: Option<B>,
94 host_demotion: Option<HD>,
95 io: Option<IO>,
96}
97
98impl<D, H, B, HD, IO> CacheBlockStorage<D, H, B, HD, IO> {
99 pub fn device(id: CacheBlockId, device: D, backing: Option<B>) -> Self {
101 Self {
102 id,
103 phase: CacheStoragePhase::Device,
104 device: Some(device),
105 host: None,
106 backing,
107 host_demotion: None,
108 io: None,
109 }
110 }
111
112 pub fn host(id: CacheBlockId, host: H, backing: Option<B>) -> Self {
114 Self {
115 id,
116 phase: if backing.is_some() {
117 CacheStoragePhase::HostBacked
118 } else {
119 CacheStoragePhase::HostUnbacked
120 },
121 device: None,
122 host: Some(host),
123 backing,
124 host_demotion: None,
125 io: None,
126 }
127 }
128
129 pub fn disk(id: CacheBlockId, backing: B) -> Self {
131 Self {
132 id,
133 phase: CacheStoragePhase::DiskReady,
134 device: None,
135 host: None,
136 backing: Some(backing),
137 host_demotion: None,
138 io: None,
139 }
140 }
141
142 pub const fn phase(&self) -> CacheStoragePhase {
144 self.phase
145 }
146
147 pub const fn id(&self) -> &CacheBlockId {
149 &self.id
150 }
151
152 pub const fn tier(&self) -> CacheTier {
154 self.phase.tier()
155 }
156
157 pub fn device_resource(&self) -> Option<&D> {
159 self.device.as_ref()
160 }
161
162 pub fn host_resource(&self) -> Option<&H> {
164 self.host.as_ref()
165 }
166
167 pub fn backing(&self) -> Option<&B> {
169 self.backing.as_ref()
170 }
171
172 pub fn host_demotion(&self) -> Option<&HD> {
174 self.host_demotion.as_ref()
175 }
176
177 pub fn io(&self) -> Option<&IO> {
179 self.io.as_ref()
180 }
181}
182
183impl<D, H, B, HD: CacheHostDemotionOperation, IO: CacheIoOperation>
184 CacheBlockStorage<D, H, B, HD, IO>
185{
186 pub fn io_matches(&self, key: &CacheIoOperationKey) -> bool {
188 self.io.as_ref().is_some_and(|io| io.key() == key)
189 }
190
191 pub fn fail_io_if_matches(&mut self, key: &CacheIoOperationKey) -> bool {
193 if self.io_matches(key) {
194 self.fail_io(key)
195 .expect("matching pending I/O has a valid source phase");
196 true
197 } else {
198 false
199 }
200 }
201
202 pub fn begin_host_demotion(&mut self, operation: HD) -> Result<(), CacheStorageError> {
204 self.require_phase(CacheStoragePhase::Device)?;
205 self.require_block(operation.block_id())?;
206 if self.backing.is_some() {
207 return Err(CacheStorageError::BackingAlreadyExists);
208 }
209 self.host_demotion = Some(operation);
210 self.phase = CacheStoragePhase::DemotingToHost;
211 Ok(())
212 }
213
214 pub fn finish_host_demotion(
216 &mut self,
217 operation_id: u64,
218 host: H,
219 ) -> Result<(D, HD), CacheStorageError> {
220 self.require_host_demotion(operation_id)?;
221 let device = self
222 .device
223 .take()
224 .expect("demoting phase retains device resources");
225 let operation = self
226 .host_demotion
227 .take()
228 .expect("demoting phase retains its exact operation");
229 self.host = Some(host);
230 self.phase = CacheStoragePhase::HostUnbacked;
231 Ok((device, operation))
232 }
233
234 pub fn fail_host_demotion(&mut self, operation_id: u64) -> Result<HD, CacheStorageError> {
236 self.require_host_demotion(operation_id)?;
237 let operation = self
238 .host_demotion
239 .take()
240 .expect("demoting phase retains its exact operation");
241 self.phase = CacheStoragePhase::Device;
242 Ok(operation)
243 }
244
245 pub fn release_device_to_disk(&mut self) -> Result<D, CacheStorageError> {
247 self.require_phase(CacheStoragePhase::Device)?;
248 if self.backing.is_none() {
249 return Err(CacheStorageError::BackingRequired);
250 }
251 let device = self
252 .device
253 .take()
254 .expect("device phase owns device resources");
255 self.phase = CacheStoragePhase::DiskReady;
256 Ok(device)
257 }
258
259 pub fn release_host_to_disk(&mut self) -> Result<H, CacheStorageError> {
261 self.require_phase(CacheStoragePhase::HostBacked)?;
262 let host = self
263 .host
264 .take()
265 .expect("host-backed phase owns host resources");
266 self.phase = CacheStoragePhase::DiskReady;
267 Ok(host)
268 }
269
270 pub fn begin_write(&mut self, operation: IO) -> Result<(), CacheStorageError> {
272 self.require_phase(CacheStoragePhase::HostUnbacked)?;
273 self.require_block(&operation.key().id)?;
274 self.require_kind(operation.key(), CacheIoOperationKind::Write)?;
275 self.io = Some(operation);
276 self.phase = CacheStoragePhase::HostWriting;
277 Ok(())
278 }
279
280 pub fn finish_write(
282 &mut self,
283 key: &CacheIoOperationKey,
284 backing: B,
285 ) -> Result<(H, IO), CacheStorageError> {
286 self.require_io(CacheStoragePhase::HostWriting, key)?;
287 let host = self
288 .host
289 .take()
290 .expect("host-writing phase retains host resources");
291 let operation = self.io.take().expect("host-writing phase retains I/O");
292 self.backing = Some(backing);
293 self.phase = CacheStoragePhase::DiskReady;
294 Ok((host, operation))
295 }
296
297 pub fn begin_read(&mut self, operation: IO) -> Result<(), CacheStorageError> {
299 self.require_phase(CacheStoragePhase::DiskReady)?;
300 self.require_block(&operation.key().id)?;
301 self.require_kind(operation.key(), CacheIoOperationKind::Read)?;
302 self.io = Some(operation);
303 self.phase = CacheStoragePhase::DiskReading;
304 Ok(())
305 }
306
307 pub fn finish_read(
309 &mut self,
310 key: &CacheIoOperationKey,
311 host: H,
312 ) -> Result<IO, CacheStorageError> {
313 self.require_io(CacheStoragePhase::DiskReading, key)?;
314 let operation = self.io.take().expect("disk-reading phase retains I/O");
315 self.host = Some(host);
316 self.phase = CacheStoragePhase::HostBacked;
317 Ok(operation)
318 }
319
320 pub fn fail_io(&mut self, key: &CacheIoOperationKey) -> Result<IO, CacheStorageError> {
322 let stable = match self.phase {
323 CacheStoragePhase::HostWriting => CacheStoragePhase::HostUnbacked,
324 CacheStoragePhase::DiskReading => CacheStoragePhase::DiskReady,
325 actual => return Err(CacheStorageError::IoNotPending { actual }),
326 };
327 self.require_io(self.phase, key)?;
328 let operation = self.io.take().expect("pending phase retains I/O");
329 self.phase = stable;
330 Ok(operation)
331 }
332
333 pub fn promote_host(&mut self, device: D) -> Result<CacheHostPromotion<H>, CacheStorageError> {
335 if !matches!(
336 self.phase,
337 CacheStoragePhase::HostUnbacked | CacheStoragePhase::HostBacked
338 ) {
339 return Err(CacheStorageError::InvalidPhase {
340 expected: CacheStoragePhase::HostUnbacked,
341 actual: self.phase,
342 });
343 }
344 let source = self.phase;
345 let host = self
346 .host
347 .take()
348 .expect("stable host phase owns host resources");
349 self.device = Some(device);
350 self.phase = CacheStoragePhase::Device;
351 Ok(CacheHostPromotion {
352 id: self.id.clone(),
353 source,
354 host,
355 })
356 }
357
358 pub fn restore_host(
360 &mut self,
361 promotion: CacheHostPromotion<H>,
362 ) -> Result<D, CacheStorageError> {
363 self.require_phase(CacheStoragePhase::Device)?;
364 self.require_block(&promotion.id)?;
365 let expected_source = if self.backing.is_some() {
366 CacheStoragePhase::HostBacked
367 } else {
368 CacheStoragePhase::HostUnbacked
369 };
370 if promotion.source != expected_source {
371 return Err(CacheStorageError::InvalidPhase {
372 expected: expected_source,
373 actual: promotion.source,
374 });
375 }
376 let device = self
377 .device
378 .take()
379 .expect("device phase owns device resources");
380 self.host = Some(promotion.host);
381 self.phase = promotion.source;
382 Ok(device)
383 }
384
385 fn require_phase(&self, expected: CacheStoragePhase) -> Result<(), CacheStorageError> {
386 if self.phase == expected {
387 Ok(())
388 } else {
389 Err(CacheStorageError::InvalidPhase {
390 expected,
391 actual: self.phase,
392 })
393 }
394 }
395
396 fn require_host_demotion(&self, operation_id: u64) -> Result<(), CacheStorageError> {
397 self.require_phase(CacheStoragePhase::DemotingToHost)?;
398 let actual = self
399 .host_demotion
400 .as_ref()
401 .expect("demoting phase retains its exact operation")
402 .operation_id();
403 if actual == operation_id {
404 Ok(())
405 } else {
406 Err(CacheStorageError::HostDemotionMismatch {
407 expected: actual,
408 actual: operation_id,
409 })
410 }
411 }
412
413 fn require_io(
414 &self,
415 phase: CacheStoragePhase,
416 key: &CacheIoOperationKey,
417 ) -> Result<(), CacheStorageError> {
418 self.require_phase(phase)?;
419 let expected = self
420 .io
421 .as_ref()
422 .expect("pending I/O phase retains its exact operation")
423 .key();
424 if expected == key {
425 Ok(())
426 } else {
427 Err(CacheStorageError::IoMismatch {
428 expected: Box::new(expected.clone()),
429 actual: Box::new(key.clone()),
430 })
431 }
432 }
433
434 fn require_kind(
435 &self,
436 key: &CacheIoOperationKey,
437 expected: CacheIoOperationKind,
438 ) -> Result<(), CacheStorageError> {
439 if key.kind == expected {
440 Ok(())
441 } else {
442 Err(CacheStorageError::IoKindMismatch {
443 expected,
444 actual: key.kind,
445 })
446 }
447 }
448
449 fn require_block(&self, actual: &CacheBlockId) -> Result<(), CacheStorageError> {
450 if actual == &self.id {
451 Ok(())
452 } else {
453 Err(CacheStorageError::BlockMismatch {
454 expected: Box::new(self.id.clone()),
455 actual: Box::new(actual.clone()),
456 })
457 }
458 }
459}
460
461#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
463pub enum CacheStorageError {
464 #[error("cache storage block mismatch: expected {expected:?}, observed {actual:?}")]
466 BlockMismatch {
467 expected: Box<CacheBlockId>,
469 actual: Box<CacheBlockId>,
471 },
472 #[error("cache storage phase is {actual:?}, expected {expected:?}")]
474 InvalidPhase {
475 expected: CacheStoragePhase,
477 actual: CacheStoragePhase,
479 },
480 #[error("cache storage phase {actual:?} has no pending I/O")]
482 IoNotPending {
483 actual: CacheStoragePhase,
485 },
486 #[error("cache storage already has a durable backing")]
488 BackingAlreadyExists,
489 #[error("cache storage requires a durable backing")]
491 BackingRequired,
492 #[error("host demotion operation mismatch: expected {expected}, observed {actual}")]
494 HostDemotionMismatch {
495 expected: u64,
497 actual: u64,
499 },
500 #[error("cache I/O operation mismatch: expected {expected:?}, observed {actual:?}")]
502 IoMismatch {
503 expected: Box<CacheIoOperationKey>,
505 actual: Box<CacheIoOperationKey>,
507 },
508 #[error("cache I/O kind is {actual:?}, expected {expected:?}")]
510 IoKindMismatch {
511 expected: CacheIoOperationKind,
513 actual: CacheIoOperationKind,
515 },
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use eredu_core::cache::CacheRepresentation;
522
523 #[derive(Debug, Clone, Eq, PartialEq)]
524 struct HostOp {
525 id: CacheBlockId,
526 operation_id: u64,
527 }
528
529 impl CacheHostDemotionOperation for HostOp {
530 fn block_id(&self) -> &CacheBlockId {
531 &self.id
532 }
533
534 fn operation_id(&self) -> u64 {
535 self.operation_id
536 }
537 }
538
539 #[derive(Debug, Clone, Eq, PartialEq)]
540 struct IoOp(CacheIoOperationKey);
541
542 impl CacheIoOperation for IoOp {
543 fn key(&self) -> &CacheIoOperationKey {
544 &self.0
545 }
546 }
547
548 fn block() -> CacheBlockId {
549 CacheBlockId {
550 session_id: 1,
551 global_layer: 2,
552 representation: CacheRepresentation::KeyValue,
553 start: 0,
554 end: 4,
555 rank: None,
556 }
557 }
558
559 fn io(kind: CacheIoOperationKind, generation: u64) -> IoOp {
560 IoOp(CacheIoOperationKey {
561 generation,
562 id: block(),
563 kind,
564 })
565 }
566
567 #[test]
568 fn exact_host_demotion_commits_or_rolls_back_without_losing_device_state() {
569 let id = block();
570 let mut storage = CacheBlockStorage::<_, String, String, _, IoOp>::device(
571 id.clone(),
572 "device".to_owned(),
573 None,
574 );
575 storage
576 .begin_host_demotion(HostOp {
577 id,
578 operation_id: 7,
579 })
580 .unwrap();
581 assert_eq!(storage.phase(), CacheStoragePhase::DemotingToHost);
582 assert_eq!(
583 storage.finish_host_demotion(8, "host".to_owned()),
584 Err(CacheStorageError::HostDemotionMismatch {
585 expected: 7,
586 actual: 8,
587 })
588 );
589 assert_eq!(
590 storage.device_resource().map(String::as_str),
591 Some("device")
592 );
593 storage.fail_host_demotion(7).unwrap();
594 assert_eq!(storage.phase(), CacheStoragePhase::Device);
595 }
596
597 #[test]
598 fn write_read_and_promotion_follow_one_exact_transaction_chain() {
599 let mut storage = CacheBlockStorage::<String, String, String, HostOp, _>::host(
600 block(),
601 "host".to_owned(),
602 None,
603 );
604 let write = io(CacheIoOperationKind::Write, 3);
605 storage.begin_write(write.clone()).unwrap();
606 let stale = io(CacheIoOperationKind::Write, 2);
607 assert!(matches!(
608 storage.finish_write(stale.key(), "disk".to_owned()),
609 Err(CacheStorageError::IoMismatch { .. })
610 ));
611 let (host, observed) = storage
612 .finish_write(write.key(), "disk".to_owned())
613 .unwrap();
614 assert_eq!(host, "host");
615 assert_eq!(observed, write);
616 assert_eq!(storage.phase(), CacheStoragePhase::DiskReady);
617
618 let read = io(CacheIoOperationKind::Read, 4);
619 storage.begin_read(read.clone()).unwrap();
620 storage
621 .finish_read(read.key(), "host-2".to_owned())
622 .unwrap();
623 assert_eq!(storage.phase(), CacheStoragePhase::HostBacked);
624 let promotion = storage.promote_host("device-2".to_owned()).unwrap();
625 assert_eq!(promotion.host, "host-2");
626 assert_eq!(storage.backing().map(String::as_str), Some("disk"));
627 storage.release_device_to_disk().unwrap();
628 assert_eq!(storage.phase(), CacheStoragePhase::DiskReady);
629 }
630
631 #[test]
632 fn failed_io_restores_the_stable_source_phase() {
633 let mut write = CacheBlockStorage::<String, _, String, HostOp, _>::host(block(), 3u8, None);
634 let write_op = io(CacheIoOperationKind::Write, 1);
635 write.begin_write(write_op.clone()).unwrap();
636 assert_eq!(write.fail_io(write_op.key()).unwrap(), write_op);
637 assert_eq!(write.phase(), CacheStoragePhase::HostUnbacked);
638
639 let mut read =
640 CacheBlockStorage::<String, u8, _, HostOp, _>::disk(block(), "disk".to_owned());
641 let read_op = io(CacheIoOperationKind::Read, 1);
642 read.begin_read(read_op.clone()).unwrap();
643 assert_eq!(read.fail_io(read_op.key()).unwrap(), read_op);
644 assert_eq!(read.phase(), CacheStoragePhase::DiskReady);
645 }
646
647 #[test]
648 fn operation_for_another_block_fails_without_changing_phase() {
649 let owned = block();
650 let mut foreign = block();
651 foreign.global_layer += 1;
652 let mut storage =
653 CacheBlockStorage::<String, u8, String, HostOp, _>::host(owned.clone(), 3, None);
654 let operation = IoOp(CacheIoOperationKey {
655 generation: 1,
656 id: foreign.clone(),
657 kind: CacheIoOperationKind::Write,
658 });
659
660 assert_eq!(
661 storage.begin_write(operation),
662 Err(CacheStorageError::BlockMismatch {
663 expected: Box::new(owned),
664 actual: Box::new(foreign),
665 })
666 );
667 assert_eq!(storage.phase(), CacheStoragePhase::HostUnbacked);
668 }
669
670 #[test]
671 fn phase_and_operation_identity_round_trip_portably() {
672 let key = io(CacheIoOperationKind::Read, 9).0;
673 let encoded = serde_json::to_string(&(CacheStoragePhase::DiskReading, &key)).unwrap();
674 let decoded: (CacheStoragePhase, CacheIoOperationKey) =
675 serde_json::from_str(&encoded).unwrap();
676 assert_eq!(decoded, (CacheStoragePhase::DiskReading, key));
677 }
678}