Skip to main content

detcore/
shared_open_files.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! The canonical models of open file descriptions that several guest
10//! processes share, and the leases through which a process reaches them.
11//!
12//! A backend that runs Detcore inside each guest process cannot share one
13//! `Arc` between processes, so a description that a fork shares lives here,
14//! in the global state, and each process's handle is `Shared` (see
15//! `crate::fd::OpenFileState`). A `DetFd` method on a shared handle takes the
16//! model, runs its synchronous body on it, and publishes it back, inside one
17//! method call.
18//!
19//! - Only the thread that holds the scheduler's serial grant may take, so
20//!   access is exclusive and happens in canonical order.
21//! - The global state keeps the canonical model while a lease is out and sends
22//!   a copy. A publish replaces the model only when it names the current lease,
23//!   so a late, duplicate or foreign publish is refused. If the holder's
24//!   process dies mid-lease, the lease ends and its unpublished change is
25//!   discarded.
26//! - Models move in chunks of at most [`CHUNK_LEN`] bytes, because a transport
27//!   frame is bounded and a model (a directory or procfs snapshot) is not.
28//!
29//! Production promotion currently selects random-device descriptions in shared
30//! Detcore; the model and protocol remain generic. Committed models live until
31//! GlobalState teardown, including after their last guest descriptor closes.
32//! Memory therefore scales with distinct promoted OFDs and their model sizes,
33//! not merely with currently open descriptors.
34//!
35//! Every message here is control traffic: it changes no clock, request slot,
36//! scheduler membership or turn.
37
38use 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
52/// The largest model chunk one message carries. Transport frames are limited
53/// to 16 MiB. The chunk is much smaller because a guest process transfers it
54/// inside the runtime's fixed callback heap, where the transport's own frame
55/// buffers coexist with it.
56pub const CHUNK_LEN: usize = 1 << 20;
57
58/// One control request about a shared open file description.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub enum OpenFileControl {
61    /// Start a lease on `id` and return the model's first chunk.
62    Take {
63        /// The shared open file description.
64        id: OpenFileId,
65    },
66    /// Return the chunk of the leased model that starts at `offset`.
67    TakeChunk {
68        /// The shared open file description.
69        id: OpenFileId,
70        /// The lease, as [`OpenFileControlReply::Taken`] named it.
71        sequence: u64,
72        /// Where the chunk starts in the encoded model.
73        offset: usize,
74    },
75    /// Append `bytes` at `offset` to the model being published under the
76    /// lease. The last chunk commits the model and ends the lease.
77    PublishChunk {
78        /// The shared open file description.
79        id: OpenFileId,
80        /// The lease being published.
81        sequence: u64,
82        /// Where `bytes` start in the encoded model.
83        offset: usize,
84        /// This chunk of the encoded model.
85        bytes: Vec<u8>,
86        /// Whether this is the last chunk.
87        last: bool,
88    },
89    /// Reserve a new identity for promotion. The holder publishes its local
90    /// model through the ordinary chunked lease before exposing shared handles.
91    Promote {
92        /// The local description being promoted.
93        id: OpenFileId,
94    },
95}
96
97/// The answer to an [`OpenFileControl`] request.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub enum OpenFileControlReply {
100    /// A lease started: its sequence, the encoded model's length, and its
101    /// first chunk.
102    Taken {
103        /// Names the lease in every later message about it.
104        sequence: u64,
105        /// The encoded model's length in bytes.
106        length: usize,
107        /// The encoded model's first bytes, at most `CHUNK_LEN` (1 MiB).
108        chunk: Vec<u8>,
109    },
110    /// A further chunk of the leased model.
111    Chunk(Vec<u8>),
112    /// A non-final publish chunk was accepted.
113    Accepted,
114    /// The model was committed and the lease ended.
115    Published,
116    /// A promotion lease started. Its initial model is not yet available.
117    Promoting {
118        /// Names the lease in every publish chunk.
119        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/// Who holds a lease: the process, for settlement at its death, and the
155/// thread that took it.
156#[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    /// The encoded copy being taken.
167    outgoing: Vec<u8>,
168    /// The encoded model being published, chunk by chunk.
169    incoming: Vec<u8>,
170}
171
172#[derive(Debug)]
173struct Canonical {
174    /// Absent only while the initial promotion is being published.
175    model: Option<OpenFileModel>,
176    lease: Option<Lease>,
177}
178
179/// The global store of shared open file descriptions.
180#[derive(Debug, Default)]
181pub struct SharedOpenFileStore {
182    entries: BTreeMap<OpenFileId, Canonical>,
183    next_sequence: u64,
184}
185
186impl SharedOpenFileStore {
187    /// Makes `model` the canonical model of its open file description. The
188    /// share transaction at a fork is the caller.
189    #[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    /// Answers one control request from `thread` of `process`. The caller has
206    /// already checked that `thread` holds the scheduler's serial grant.
207    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    /// Ends every lease held by `process`, discarding their unpublished
319    /// changes. Called when the process is retired.
320    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
369/// Carries one [`OpenFileControl`] request to the global state and returns
370/// its answer, synchronously. A backend implements this over its existing
371/// coordinator connection.
372pub trait OpenFileControlTransport: Send + Sync {
373    /// Sends `request` and waits for the answer.
374    fn call(&self, request: OpenFileControl) -> Result<OpenFileControlReply, SharedOpenFileError>;
375}
376
377/// A [`SharedOpenFileChannel`] that moves models in chunks over an
378/// [`OpenFileControlTransport`].
379///
380/// It streams both ways: a take decodes the model while it fetches the
381/// chunks, and a publish sends each chunk as soon as the encoder fills it. So
382/// the process never holds more than one chunk of the encoding, beside the
383/// model itself and the transport's frame for that chunk. That matters inside a guest process, where the runtime's
384/// callback heap is small and fixed: a model that fits there as a local
385/// description must also fit while it is taken or published.
386pub struct ChunkedOpenFileChannel<T> {
387    transport: T,
388    /// The most encoded bytes a take or publish has held at once.
389    peak_buffered: AtomicUsize,
390}
391
392impl<T: OpenFileControlTransport> ChunkedOpenFileChannel<T> {
393    /// Wraps `transport`.
394    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
410/// Reads a leased model's encoding one chunk at a time, fetching the next
411/// chunk only when the decoder has consumed the current one.
412struct 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    /// Encoded bytes received so far, the current chunk included.
420    received: usize,
421    /// The transport's own error, kept so the caller reports it rather than
422    /// the decoder's.
423    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        // The consumed chunk is released before the next one arrives, so the
435        // two never coexist.
436        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    /// Whether the decoder consumed exactly the whole encoding.
461    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
481/// Collects a model's encoding and sends each full chunk as it fills.
482struct PublishWriter<'a, T> {
483    channel: &'a ChunkedOpenFileChannel<T>,
484    id: OpenFileId,
485    sequence: u64,
486    /// Encoded bytes already sent.
487    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        // The last chunk, possibly empty, commits the model.
615        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    /// Calls the store directly, as the global state does after its grant
639    /// check.
640    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    /// Calls the store directly and records the largest model chunk any one
746    /// message carried.
747    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    /// A model larger than the 16 MiB transport frame moves in chunks of at
776    /// most `CHUNK_LEN`, both ways, and the channel never holds more than one
777    /// chunk of its encoding: it decodes while it fetches and sends while it
778    /// encodes.
779    #[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        // At least seventeen chunks each way for a model over 16 MiB.
806        assert!(*transport.messages.lock().unwrap() >= 34);
807    }
808
809    /// Until the last chunk of a publish arrives, the old model stays
810    /// canonical; a chunk at the wrong offset, or a model of another
811    /// description, is refused and commits nothing.
812    #[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        // A gap or an overlap is refused.
848        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        // The holder dies before the last chunk: its partial publish is gone.
852        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        // A complete publish of another description's model is refused.
857        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        // A publish under another sequence, or from another thread, is refused
882        // and leaves the lease in place.
883        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        // The dead holder's late publish names no current lease.
921        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}