1use std::collections::BTreeMap;
39use std::sync::atomic::AtomicUsize;
40use std::sync::atomic::Ordering;
41
42use serde::Deserialize;
43use serde::Serialize;
44
45use crate::fd::OpenFileModel;
46use crate::fd::SharedOpenFileChannel;
47use crate::fd::SharedOpenFileError;
48use crate::types::DetPid;
49use crate::types::DetTid;
50use crate::types::OpenFileId;
51
52pub const CHUNK_LEN: usize = 1 << 20;
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub enum OpenFileControl {
61 Take {
63 id: OpenFileId,
65 },
66 TakeChunk {
68 id: OpenFileId,
70 sequence: u64,
72 offset: usize,
74 },
75 PublishChunk {
78 id: OpenFileId,
80 sequence: u64,
82 offset: usize,
84 bytes: Vec<u8>,
86 last: bool,
88 },
89 Promote {
92 id: OpenFileId,
94 },
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub enum OpenFileControlReply {
100 Taken {
103 sequence: u64,
105 length: usize,
107 chunk: Vec<u8>,
109 },
110 Chunk(Vec<u8>),
112 Accepted,
114 Published,
116 Promoting {
118 sequence: u64,
120 },
121}
122
123fn encode_model(model: &OpenFileModel) -> Result<Vec<u8>, SharedOpenFileError> {
124 bincode::serde::encode_to_vec(model, bincode::config::legacy())
125 .map_err(|error| SharedOpenFileError(format!("encoding an open file model: {error}")))
126}
127
128fn decode_model(bytes: &[u8]) -> Result<OpenFileModel, SharedOpenFileError> {
129 let (model, used) =
130 bincode::serde::decode_from_slice::<OpenFileModel, _>(bytes, bincode::config::legacy())
131 .map_err(|error| {
132 SharedOpenFileError(format!("decoding an open file model: {error}"))
133 })?;
134 if used != bytes.len() {
135 return Err(SharedOpenFileError(format!(
136 "an open file model left {} undecoded bytes",
137 bytes.len() - used
138 )));
139 }
140 Ok(model)
141}
142
143fn chunk_at(bytes: &[u8], offset: usize) -> Result<Vec<u8>, SharedOpenFileError> {
144 if offset > bytes.len() {
145 return Err(SharedOpenFileError(format!(
146 "chunk offset {offset} is past the model's {} bytes",
147 bytes.len()
148 )));
149 }
150 let end = bytes.len().min(offset + CHUNK_LEN);
151 Ok(bytes[offset..end].to_vec())
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157struct Holder {
158 process: DetPid,
159 thread: DetTid,
160}
161
162#[derive(Debug)]
163struct Lease {
164 holder: Holder,
165 sequence: u64,
166 outgoing: Vec<u8>,
168 incoming: Vec<u8>,
170}
171
172#[derive(Debug)]
173struct Canonical {
174 model: Option<OpenFileModel>,
176 lease: Option<Lease>,
177}
178
179#[derive(Debug, Default)]
181pub struct SharedOpenFileStore {
182 entries: BTreeMap<OpenFileId, Canonical>,
183 next_sequence: u64,
184}
185
186impl SharedOpenFileStore {
187 #[cfg_attr(not(test), allow(dead_code))]
190 pub(crate) fn insert(&mut self, model: OpenFileModel) -> Result<(), SharedOpenFileError> {
191 let id = model.id();
192 if self.entries.contains_key(&id) {
193 return Err(SharedOpenFileError(format!("{id:?} is already shared")));
194 }
195 self.entries.insert(
196 id,
197 Canonical {
198 model: Some(model),
199 lease: None,
200 },
201 );
202 Ok(())
203 }
204
205 pub(crate) fn handle(
208 &mut self,
209 process: DetPid,
210 thread: DetTid,
211 request: OpenFileControl,
212 ) -> Result<OpenFileControlReply, SharedOpenFileError> {
213 let holder = Holder { process, thread };
214 match request {
215 OpenFileControl::Promote { id } => {
216 if self.entries.contains_key(&id) {
217 return Err(SharedOpenFileError(format!("{id:?} is already shared")));
218 }
219 self.next_sequence += 1;
220 let sequence = self.next_sequence;
221 self.entries.insert(
222 id,
223 Canonical {
224 model: None,
225 lease: Some(Lease {
226 holder,
227 sequence,
228 outgoing: Vec::new(),
229 incoming: Vec::new(),
230 }),
231 },
232 );
233 Ok(OpenFileControlReply::Promoting { sequence })
234 }
235 OpenFileControl::Take { id } => {
236 let sequence = self.next_sequence + 1;
237 let canonical = self.entry(id)?;
238 if let Some(lease) = &canonical.lease {
239 return Err(SharedOpenFileError(format!(
240 "{id:?} is already leased to thread {} (lease {})",
241 lease.holder.thread, lease.sequence
242 )));
243 }
244 let model = canonical.model.as_ref().ok_or_else(|| {
245 SharedOpenFileError(format!("{id:?} promotion is incomplete"))
246 })?;
247 let outgoing = encode_model(model)?;
248 let length = outgoing.len();
249 let chunk = chunk_at(&outgoing, 0)?;
250 canonical.lease = Some(Lease {
251 holder,
252 sequence,
253 outgoing,
254 incoming: Vec::new(),
255 });
256 self.next_sequence = sequence;
257 Ok(OpenFileControlReply::Taken {
258 sequence,
259 length,
260 chunk,
261 })
262 }
263 OpenFileControl::TakeChunk {
264 id,
265 sequence,
266 offset,
267 } => {
268 let canonical = self.entry(id)?;
269 if canonical.model.is_none() {
270 return Err(SharedOpenFileError(format!(
271 "{id:?} promotion is incomplete"
272 )));
273 }
274 let lease = Self::current_lease(canonical, id, holder, sequence)?;
275 Ok(OpenFileControlReply::Chunk(chunk_at(
276 &lease.outgoing,
277 offset,
278 )?))
279 }
280 OpenFileControl::PublishChunk {
281 id,
282 sequence,
283 offset,
284 bytes,
285 last,
286 } => {
287 if bytes.len() > CHUNK_LEN {
288 return Err(SharedOpenFileError(format!(
289 "{id:?}: oversized publish chunk"
290 )));
291 }
292 let canonical = self.entry(id)?;
293 let lease = Self::current_lease(canonical, id, holder, sequence)?;
294 if offset != lease.incoming.len() {
295 return Err(SharedOpenFileError(format!(
296 "{id:?}: publish chunk at {offset}, expected {}",
297 lease.incoming.len()
298 )));
299 }
300 lease.incoming.extend_from_slice(&bytes);
301 if !last {
302 return Ok(OpenFileControlReply::Accepted);
303 }
304 let model = decode_model(&lease.incoming)?;
305 if model.id() != id {
306 return Err(SharedOpenFileError(format!(
307 "{id:?}: published the model of {:?}",
308 model.id()
309 )));
310 }
311 canonical.model = Some(model);
312 canonical.lease = None;
313 Ok(OpenFileControlReply::Published)
314 }
315 }
316 }
317
318 pub(crate) fn end_leases_of(&mut self, process: DetPid) -> usize {
321 let mut ended = 0;
322 for canonical in self.entries.values_mut() {
323 if canonical
324 .lease
325 .as_ref()
326 .is_some_and(|lease| lease.holder.process == process)
327 {
328 canonical.lease = None;
329 ended += 1;
330 }
331 }
332 self.entries
333 .retain(|_, canonical| canonical.model.is_some() || canonical.lease.is_some());
334 ended
335 }
336
337 fn entry(&mut self, id: OpenFileId) -> Result<&mut Canonical, SharedOpenFileError> {
338 self.entries
339 .get_mut(&id)
340 .ok_or_else(|| SharedOpenFileError(format!("{id:?} is not shared")))
341 }
342
343 fn current_lease(
344 canonical: &mut Canonical,
345 id: OpenFileId,
346 holder: Holder,
347 sequence: u64,
348 ) -> Result<&mut Lease, SharedOpenFileError> {
349 match &mut canonical.lease {
350 Some(lease) if lease.sequence == sequence && lease.holder == holder => Ok(lease),
351 _ => Err(SharedOpenFileError(format!(
352 "{id:?}: lease {sequence} of thread {} is not the current lease",
353 holder.thread
354 ))),
355 }
356 }
357
358 #[cfg(test)]
359 fn model(&self, id: OpenFileId) -> &OpenFileModel {
360 self.entries[&id].model.as_ref().expect("committed model")
361 }
362
363 #[cfg(test)]
364 pub(crate) fn is_leased(&self, id: OpenFileId) -> bool {
365 self.entries[&id].lease.is_some()
366 }
367}
368
369pub trait OpenFileControlTransport: Send + Sync {
373 fn call(&self, request: OpenFileControl) -> Result<OpenFileControlReply, SharedOpenFileError>;
375}
376
377pub struct ChunkedOpenFileChannel<T> {
387 transport: T,
388 peak_buffered: AtomicUsize,
390}
391
392impl<T: OpenFileControlTransport> ChunkedOpenFileChannel<T> {
393 pub fn new(transport: T) -> Self {
395 Self {
396 transport,
397 peak_buffered: AtomicUsize::new(0),
398 }
399 }
400
401 fn note_buffered(&self, bytes: usize) {
402 self.peak_buffered.fetch_max(bytes, Ordering::Relaxed);
403 }
404}
405
406fn unexpected(reply: OpenFileControlReply) -> SharedOpenFileError {
407 SharedOpenFileError(format!("unexpected open file control reply {reply:?}"))
408}
409
410struct TakeReader<'a, T> {
413 channel: &'a ChunkedOpenFileChannel<T>,
414 id: OpenFileId,
415 sequence: u64,
416 length: usize,
417 chunk: Vec<u8>,
418 position: usize,
419 received: usize,
421 failure: Option<SharedOpenFileError>,
424}
425
426impl<T: OpenFileControlTransport> TakeReader<'_, T> {
427 fn fail(&mut self, error: SharedOpenFileError) -> std::io::Error {
428 let io = std::io::Error::other(error.0.clone());
429 self.failure = Some(error);
430 io
431 }
432
433 fn fetch(&mut self) -> std::io::Result<()> {
434 self.chunk = Vec::new();
437 self.position = 0;
438 let reply = self.channel.transport.call(OpenFileControl::TakeChunk {
439 id: self.id,
440 sequence: self.sequence,
441 offset: self.received,
442 });
443 match reply {
444 Ok(OpenFileControlReply::Chunk(chunk))
445 if !chunk.is_empty()
446 && chunk.len() <= CHUNK_LEN
447 && self.received + chunk.len() <= self.length =>
448 {
449 self.received += chunk.len();
450 self.chunk = chunk;
451 self.position = 0;
452 self.channel.note_buffered(self.chunk.len());
453 Ok(())
454 }
455 Ok(other) => Err(self.fail(unexpected(other))),
456 Err(error) => Err(self.fail(error)),
457 }
458 }
459
460 fn consumed_everything(&self) -> bool {
462 self.received == self.length && self.position == self.chunk.len()
463 }
464}
465
466impl<T: OpenFileControlTransport> std::io::Read for TakeReader<'_, T> {
467 fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
468 if self.position == self.chunk.len() {
469 if self.received == self.length {
470 return Ok(0);
471 }
472 self.fetch()?;
473 }
474 let count = out.len().min(self.chunk.len() - self.position);
475 out[..count].copy_from_slice(&self.chunk[self.position..self.position + count]);
476 self.position += count;
477 Ok(count)
478 }
479}
480
481struct PublishWriter<'a, T> {
483 channel: &'a ChunkedOpenFileChannel<T>,
484 id: OpenFileId,
485 sequence: u64,
486 offset: usize,
488 buffer: Vec<u8>,
489 failure: Option<SharedOpenFileError>,
490}
491
492impl<T: OpenFileControlTransport> PublishWriter<'_, T> {
493 fn send(&mut self, last: bool) -> Result<(), SharedOpenFileError> {
494 let bytes = std::mem::take(&mut self.buffer);
495 let length = bytes.len();
496 let reply = self.channel.transport.call(OpenFileControl::PublishChunk {
497 id: self.id,
498 sequence: self.sequence,
499 offset: self.offset,
500 bytes,
501 last,
502 })?;
503 match (reply, last) {
504 (OpenFileControlReply::Published, true) | (OpenFileControlReply::Accepted, false) => {
505 self.offset += length;
506 Ok(())
507 }
508 (other, _) => Err(unexpected(other)),
509 }
510 }
511}
512
513impl<T: OpenFileControlTransport> std::io::Write for PublishWriter<'_, T> {
514 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
515 if self.buffer.len() == CHUNK_LEN
516 && let Err(error) = self.send(false)
517 {
518 let io = std::io::Error::other(error.0.clone());
519 self.failure = Some(error);
520 return Err(io);
521 }
522 let count = bytes.len().min(CHUNK_LEN - self.buffer.len());
523 self.buffer.extend_from_slice(&bytes[..count]);
524 self.channel.note_buffered(self.buffer.len());
525 Ok(count)
526 }
527
528 fn flush(&mut self) -> std::io::Result<()> {
529 Ok(())
530 }
531}
532
533impl<T: OpenFileControlTransport> SharedOpenFileChannel for ChunkedOpenFileChannel<T> {
534 fn promote(&self, model: &OpenFileModel) -> Result<(), SharedOpenFileError> {
535 let sequence = match self
536 .transport
537 .call(OpenFileControl::Promote { id: model.id() })?
538 {
539 OpenFileControlReply::Promoting { sequence } => sequence,
540 other => return Err(unexpected(other)),
541 };
542 self.publish_model(model, sequence)
543 }
544
545 fn take(&self, id: OpenFileId) -> Result<crate::fd::OpenFileLease, SharedOpenFileError> {
546 let (sequence, length, chunk) = match self.transport.call(OpenFileControl::Take { id })? {
547 OpenFileControlReply::Taken {
548 sequence,
549 length,
550 chunk,
551 } if chunk.len() <= CHUNK_LEN.min(length) => (sequence, length, chunk),
552 other => return Err(unexpected(other)),
553 };
554 self.note_buffered(chunk.len());
555 let mut reader = TakeReader {
556 channel: self,
557 id,
558 sequence,
559 length,
560 received: chunk.len(),
561 chunk,
562 position: 0,
563 failure: None,
564 };
565 let decoded = bincode::serde::decode_from_std_read::<OpenFileModel, _, _>(
566 &mut reader,
567 bincode::config::legacy(),
568 );
569 if let Some(error) = reader.failure.take() {
570 return Err(error);
571 }
572 let model = decoded.map_err(|error| {
573 SharedOpenFileError(format!("decoding an open file model: {error}"))
574 })?;
575 if !reader.consumed_everything() {
576 return Err(SharedOpenFileError(format!(
577 "{id:?}: the model did not use exactly its {length} encoded bytes"
578 )));
579 }
580 Ok(crate::fd::OpenFileLease {
581 id,
582 sequence,
583 model,
584 })
585 }
586
587 fn publish(&self, lease: crate::fd::OpenFileLease) -> Result<(), SharedOpenFileError> {
588 self.publish_model(&lease.model, lease.sequence)
589 }
590}
591
592impl<T: OpenFileControlTransport> ChunkedOpenFileChannel<T> {
593 fn publish_model(
594 &self,
595 model: &OpenFileModel,
596 sequence: u64,
597 ) -> Result<(), SharedOpenFileError> {
598 let mut writer = PublishWriter {
599 channel: self,
600 id: model.id(),
601 sequence,
602 offset: 0,
603 buffer: Vec::new(),
604 failure: None,
605 };
606 let encoded =
607 bincode::serde::encode_into_std_write(model, &mut writer, bincode::config::legacy());
608 if let Some(error) = writer.failure.take() {
609 return Err(error);
610 }
611 encoded.map_err(|error| {
612 SharedOpenFileError(format!("encoding an open file model: {error}"))
613 })?;
614 writer.send(true)
616 }
617}
618
619#[cfg(test)]
620mod tests {
621 use std::sync::Mutex;
622
623 use nix::fcntl::OFlag;
624
625 use super::*;
626 use crate::fd::DetFd;
627 use crate::fd::FdType;
628
629 const PROCESS: DetPid = DetPid::from_raw(40);
630 const THREAD: DetTid = DetTid::from_raw(40);
631
632 fn model(id: OpenFileId, path: &str) -> OpenFileModel {
633 DetFd::new(3, OFlag::O_RDWR, FdType::Regular, id)
634 .with_path(path)
635 .model_for_test()
636 }
637
638 struct Direct<'a>(&'a Mutex<SharedOpenFileStore>);
641
642 impl OpenFileControlTransport for Direct<'_> {
643 fn call(
644 &self,
645 request: OpenFileControl,
646 ) -> Result<OpenFileControlReply, SharedOpenFileError> {
647 self.0.lock().unwrap().handle(PROCESS, THREAD, request)
648 }
649 }
650
651 #[test]
652 fn take_and_publish_replace_the_canonical_model() {
653 let id = OpenFileId::new(THREAD, 0);
654 let store = Mutex::new(SharedOpenFileStore::default());
655 store.lock().unwrap().insert(model(id, "/old")).unwrap();
656 let channel = ChunkedOpenFileChannel::new(Direct(&store));
657
658 let mut lease = channel.take(id).unwrap();
659 assert_eq!(lease.model.path_for_test(), Some("/old".into()));
660 assert!(store.lock().unwrap().is_leased(id));
661 lease.model = model(id, "/new");
662 channel.publish(lease).unwrap();
663
664 let store = store.lock().unwrap();
665 assert!(!store.is_leased(id));
666 assert_eq!(store.model(id).path_for_test(), Some("/new".into()));
667 }
668
669 #[test]
670 fn promotion_commits_once_before_another_holder_can_take() {
671 let id = OpenFileId::new(THREAD, 800);
672 let mut store = SharedOpenFileStore::default();
673 let OpenFileControlReply::Promoting { sequence } = store
674 .handle(PROCESS, THREAD, OpenFileControl::Promote { id })
675 .unwrap()
676 else {
677 panic!("promotion reply")
678 };
679 assert!(
680 store
681 .handle(PROCESS, THREAD, OpenFileControl::Promote { id })
682 .is_err()
683 );
684 assert!(
685 store
686 .handle(PROCESS, THREAD, OpenFileControl::Take { id })
687 .is_err()
688 );
689 assert!(
690 store
691 .handle(
692 PROCESS,
693 THREAD,
694 OpenFileControl::TakeChunk {
695 id,
696 sequence,
697 offset: 0
698 }
699 )
700 .is_err()
701 );
702 let child = DetTid::from_raw(41);
703 let publish = OpenFileControl::PublishChunk {
704 id,
705 sequence,
706 offset: 0,
707 bytes: encode_model(&model(id, "/promoted")).unwrap(),
708 last: true,
709 };
710 assert!(store.handle(child, child, publish.clone()).is_err());
711 store.handle(PROCESS, THREAD, publish.clone()).unwrap();
712 assert!(store.handle(PROCESS, THREAD, publish).is_err());
713 assert_eq!(store.model(id).path_for_test(), Some("/promoted".into()));
714 assert!(matches!(
715 store
716 .handle(child, child, OpenFileControl::Take { id })
717 .unwrap(),
718 OpenFileControlReply::Taken { .. }
719 ));
720 assert_eq!(store.end_leases_of(child), 1);
721 assert_eq!(store.model(id).path_for_test(), Some("/promoted".into()));
722
723 let incomplete = OpenFileId::new(THREAD, 801);
724 store
725 .handle(PROCESS, THREAD, OpenFileControl::Promote { id: incomplete })
726 .unwrap();
727 assert_eq!(store.end_leases_of(PROCESS), 1);
728 assert!(!store.entries.contains_key(&incomplete));
729 }
730
731 #[test]
732 fn promotion_streams_the_existing_model_without_a_second_codec() {
733 let id = OpenFileId::new(THREAD, 802);
734 let store = Mutex::new(SharedOpenFileStore::default());
735 let channel = ChunkedOpenFileChannel::new(Direct(&store));
736 let path = format!("/{}", "p".repeat(CHUNK_LEN + 17));
737 channel.promote(&model(id, &path)).unwrap();
738 assert!(channel.promote(&model(id, "/replacement")).is_err());
739 let lease = channel.take(id).unwrap();
740 assert_eq!(lease.model.path_for_test(), Some(path.into()));
741 channel.publish(lease).unwrap();
742 assert!(channel.peak_buffered.load(Ordering::Relaxed) <= CHUNK_LEN);
743 }
744
745 struct Recording<'a> {
748 store: &'a Mutex<SharedOpenFileStore>,
749 largest: Mutex<usize>,
750 messages: Mutex<usize>,
751 }
752
753 impl OpenFileControlTransport for Recording<'_> {
754 fn call(
755 &self,
756 request: OpenFileControl,
757 ) -> Result<OpenFileControlReply, SharedOpenFileError> {
758 if let OpenFileControl::PublishChunk { bytes, .. } = &request {
759 let mut largest = self.largest.lock().unwrap();
760 *largest = (*largest).max(bytes.len());
761 }
762 let reply = self.store.lock().unwrap().handle(PROCESS, THREAD, request);
763 if let Ok(
764 OpenFileControlReply::Taken { chunk, .. } | OpenFileControlReply::Chunk(chunk),
765 ) = &reply
766 {
767 let mut largest = self.largest.lock().unwrap();
768 *largest = (*largest).max(chunk.len());
769 }
770 *self.messages.lock().unwrap() += 1;
771 reply
772 }
773 }
774
775 #[test]
780 fn a_model_larger_than_a_frame_moves_in_bounded_chunks() {
781 let id = OpenFileId::new(THREAD, 1);
782 let long = format!("/{}", "x".repeat((16 << 20) + 17));
783 let store = Mutex::new(SharedOpenFileStore::default());
784 store.lock().unwrap().insert(model(id, &long)).unwrap();
785 let channel = ChunkedOpenFileChannel::new(Recording {
786 store: &store,
787 largest: Mutex::new(0),
788 messages: Mutex::new(0),
789 });
790
791 let lease = channel.take(id).unwrap();
792 assert_eq!(lease.model.path_for_test(), Some(long.clone().into()));
793 channel.publish(lease).unwrap();
794 assert_eq!(
795 store.lock().unwrap().model(id).path_for_test(),
796 Some(long.into())
797 );
798 let transport = &channel.transport;
799 assert!(*transport.largest.lock().unwrap() <= CHUNK_LEN);
800 let peak = channel.peak_buffered.load(Ordering::Relaxed);
801 assert!(
802 peak > 0 && peak <= CHUNK_LEN,
803 "held {peak} encoded bytes at once"
804 );
805 assert!(*transport.messages.lock().unwrap() >= 34);
807 }
808
809 #[test]
813 fn a_partial_or_wrong_publish_commits_nothing() {
814 let id = OpenFileId::new(THREAD, 4);
815 let mut store = SharedOpenFileStore::default();
816 store.insert(model(id, "/old")).unwrap();
817 let take = |store: &mut SharedOpenFileStore| match store
818 .handle(PROCESS, THREAD, OpenFileControl::Take { id })
819 .unwrap()
820 {
821 OpenFileControlReply::Taken { sequence, .. } => sequence,
822 other => panic!("expected a lease, got {other:?}"),
823 };
824 let publish = |store: &mut SharedOpenFileStore, sequence, offset, bytes: &[u8], last| {
825 store.handle(
826 PROCESS,
827 THREAD,
828 OpenFileControl::PublishChunk {
829 id,
830 sequence,
831 offset,
832 bytes: bytes.to_vec(),
833 last,
834 },
835 )
836 };
837 let new = encode_model(&model(id, "/new")).unwrap();
838 let (head, tail) = new.split_at(new.len() / 2);
839
840 let sequence = take(&mut store);
841 assert_eq!(
842 publish(&mut store, sequence, 0, head, false),
843 Ok(OpenFileControlReply::Accepted)
844 );
845 assert_eq!(store.model(id).path_for_test(), Some("/old".into()));
846 assert!(store.is_leased(id));
847 assert!(publish(&mut store, sequence, head.len() + 1, tail, true).is_err());
849 assert!(publish(&mut store, sequence, head.len() - 1, tail, true).is_err());
850 assert_eq!(store.model(id).path_for_test(), Some("/old".into()));
851 assert_eq!(store.end_leases_of(PROCESS), 1);
853 assert_eq!(store.model(id).path_for_test(), Some("/old".into()));
854 assert!(publish(&mut store, sequence, head.len(), tail, true).is_err());
855
856 let other = encode_model(&model(OpenFileId::new(THREAD, 5), "/other")).unwrap();
858 let sequence = take(&mut store);
859 assert!(publish(&mut store, sequence, 0, &other, true).is_err());
860 assert_eq!(store.model(id).path_for_test(), Some("/old".into()));
861 }
862
863 #[test]
864 fn a_second_take_and_a_stale_publish_are_refused() {
865 let id = OpenFileId::new(THREAD, 2);
866 let mut store = SharedOpenFileStore::default();
867 store.insert(model(id, "/a")).unwrap();
868
869 let OpenFileControlReply::Taken { sequence, .. } = store
870 .handle(PROCESS, THREAD, OpenFileControl::Take { id })
871 .unwrap()
872 else {
873 panic!("expected a lease");
874 };
875 assert!(
876 store
877 .handle(PROCESS, THREAD, OpenFileControl::Take { id })
878 .is_err()
879 );
880 let bytes = encode_model(&model(id, "/b")).unwrap();
881 for (thread, sequence) in [(THREAD, sequence + 1), (DetTid::from_raw(41), sequence)] {
884 assert!(
885 store
886 .handle(
887 PROCESS,
888 thread,
889 OpenFileControl::PublishChunk {
890 id,
891 sequence,
892 offset: 0,
893 bytes: bytes.clone(),
894 last: true,
895 },
896 )
897 .is_err()
898 );
899 assert!(store.is_leased(id));
900 }
901 assert_eq!(store.model(id).path_for_test(), Some("/a".into()));
902 }
903
904 #[test]
905 fn the_death_of_the_holder_ends_its_lease_and_keeps_the_model() {
906 let id = OpenFileId::new(THREAD, 3);
907 let mut store = SharedOpenFileStore::default();
908 store.insert(model(id, "/kept")).unwrap();
909 let OpenFileControlReply::Taken { sequence, .. } = store
910 .handle(PROCESS, THREAD, OpenFileControl::Take { id })
911 .unwrap()
912 else {
913 panic!("expected a lease");
914 };
915
916 assert_eq!(store.end_leases_of(DetPid::from_raw(99)), 0);
917 assert_eq!(store.end_leases_of(PROCESS), 1);
918 assert!(!store.is_leased(id));
919 assert_eq!(store.model(id).path_for_test(), Some("/kept".into()));
920 assert!(
922 store
923 .handle(
924 PROCESS,
925 THREAD,
926 OpenFileControl::PublishChunk {
927 id,
928 sequence,
929 offset: 0,
930 bytes: encode_model(&model(id, "/late")).unwrap(),
931 last: true,
932 },
933 )
934 .is_err()
935 );
936 assert_eq!(store.model(id).path_for_test(), Some("/kept".into()));
937 }
938}