Skip to main content

cranpose_core/
slots_host.rs

1use std::{
2    cell::{Cell, Ref, RefCell, RefMut},
3    rc::{Rc, Weak},
4};
5
6use crate::{
7    Applier, NodeError, RecomposeScope, SlotDebugSnapshot, SlotTable, SlotTableDebugStats,
8    composer, slot,
9};
10
11/// Holds a slot table into which a composition writes: the main composition has one, and each
12/// subcomposition has its own.
13pub struct SlotsHost {
14    storage_key: Cell<usize>,
15    pub(crate) inner: RefCell<SlotsHostInner>,
16}
17
18#[derive(Debug, Default)]
19pub(crate) struct SlotPassOutcome {
20    pub(crate) compacted: bool,
21    pub(crate) compact_anchor_registry_storage: bool,
22    pub(crate) compact_payload_storage: bool,
23    pub(crate) trim_growth_slack: bool,
24}
25
26#[derive(Default)]
27pub(crate) struct FinishedSlotPass {
28    pub(crate) outcome: SlotPassOutcome,
29    pub(crate) detached_root_children: Vec<slot::DetachedSubtree>,
30}
31
32pub(crate) struct SlotsHostInner {
33    table: SlotTable,
34    nested_hosts: Vec<Weak<SlotsHost>>,
35    lifecycle: slot::SlotLifecycleCoordinator,
36    runtime_state: Option<Rc<composer::ComposerRuntimeState>>,
37    /// The table's storage capacity when the active pass began; `None`
38    /// while no pass is active.
39    pub(crate) active_pass: Option<usize>,
40    /// The writer state of the active pass. Between passes a deep host,
41    /// such as an app's root, keeps its own; a shallow one's goes back to
42    /// the thread's spare states when its pass finishes.
43    pub(crate) pass_state: Option<Box<slot::SlotWriteSessionState>>,
44}
45
46// Passes of more hosts at once are rare; they take new states.
47const SPARE_WRITE_STATES: usize = 2;
48
49// A writer state for a pass under `folds`: one of the thread's spare states
50// when it has one.
51fn spare_write_state(folds: &Rc<slot::BranchFolds>) -> Box<slot::SlotWriteSessionState> {
52    match crate::thread_state::try_runtimes(|state| state.spare_write_states.borrow_mut().pop())
53        .flatten()
54    {
55        Some(mut state) => {
56            state.set_folds(Rc::clone(folds));
57            state
58        }
59        None => Box::new(slot::SlotWriteSessionState::new(Rc::clone(folds))),
60    }
61}
62
63fn recycle_write_state(state: Box<slot::SlotWriteSessionState>) {
64    crate::thread_state::try_runtimes(|threads| {
65        let mut spare = threads.spare_write_states.borrow_mut();
66        if spare.len() < SPARE_WRITE_STATES {
67            spare.push(state);
68        }
69    });
70}
71
72impl Drop for SlotsHost {
73    fn drop(&mut self) {
74        let storage_key = self.storage_key.get();
75        let inner = self.inner.get_mut();
76        if let Some(state) = inner.runtime_state.clone() {
77            if let Err(err) = state.dispose_retained_subtrees_for_host(
78                storage_key,
79                &mut inner.table,
80                &mut inner.lifecycle,
81            ) {
82                log::error!(
83                    "retained subtree disposal failed while dropping SlotsHost {storage_key}: {err}"
84                );
85                state.abandon_retained_subtrees_for_host(
86                    storage_key,
87                    &mut inner.table,
88                    &mut inner.lifecycle,
89                );
90            } else {
91                state.clear_host_storage_key(storage_key, &mut inner.table);
92            }
93        }
94        inner.lifecycle.dispose_slot_table(&mut inner.table);
95    }
96}
97
98impl SlotsHost {
99    /// Returns a key that identifies the slot table the host holds. It changes when the table is
100    /// replaced or taken out.
101    pub fn storage_key(&self) -> usize {
102        self.storage_key.get()
103    }
104
105    /// Creates a host around the slot table `storage`.
106    pub fn new(storage: SlotTable) -> Self {
107        let storage_key = storage.storage_id();
108        Self {
109            storage_key: Cell::new(storage_key),
110            inner: RefCell::new(SlotsHostInner {
111                table: storage,
112                nested_hosts: Vec::new(),
113                lifecycle: slot::SlotLifecycleCoordinator::default(),
114                runtime_state: None,
115                active_pass: None,
116                pass_state: None,
117            }),
118        }
119    }
120
121    /// Records `nested` as a host composed inside this one, so that forgetting effects here reaches
122    /// it too. Does nothing while this host is in use.
123    pub fn note_nested_host(&self, nested: &Rc<Self>) {
124        let Ok(mut inner) = self.inner.try_borrow_mut() else {
125            return;
126        };
127        inner.nested_hosts.retain(|held| held.upgrade().is_some());
128        if inner
129            .nested_hosts
130            .iter()
131            .any(|held| held.upgrade().is_some_and(|host| Rc::ptr_eq(&host, nested)))
132        {
133            return;
134        }
135        inner.nested_hosts.push(Rc::downgrade(nested));
136    }
137
138    pub(crate) fn forget_effects(&self) -> bool {
139        let (forgotten, nested, runtime_state) = {
140            let Ok(mut inner) = self.inner.try_borrow_mut() else {
141                return false;
142            };
143            if inner.active_pass.is_some() {
144                return false;
145            }
146            let drops = inner.table.take_effect_drops();
147            inner.nested_hosts.retain(|held| held.upgrade().is_some());
148            let nested: Vec<Rc<Self>> = inner
149                .nested_hosts
150                .iter()
151                .filter_map(Weak::upgrade)
152                .collect();
153            (drops, nested, inner.runtime_state.clone())
154        };
155        let mut any = !forgotten.is_empty();
156        drop(forgotten);
157        for host in nested {
158            any |= host.forget_effects();
159        }
160        if any {
161            self.borrow()
162                .scopes()
163                .for_each(RecomposeScope::force_recompose);
164            if let Some(runtime_state) = runtime_state {
165                runtime_state.force_recompose_retained_scopes(self.storage_key());
166            }
167        }
168        any
169    }
170
171    pub(crate) fn bind_runtime_state(&self, state: &Rc<composer::ComposerRuntimeState>) {
172        let mut inner = self.inner.borrow_mut();
173        inner.runtime_state = Some(Rc::clone(state));
174    }
175
176    pub(crate) fn rebind_orphaned_runtime_state(
177        &self,
178        state: &Rc<composer::ComposerRuntimeState>,
179    ) -> bool {
180        let inner = self.inner.borrow();
181        if inner.active_pass.is_some() {
182            log::error!("cannot rebind SlotsHost during an active pass");
183            return false;
184        }
185        let Some(bound_state) = inner.runtime_state.as_ref() else {
186            drop(inner);
187            self.bind_runtime_state(state);
188            return true;
189        };
190        if Rc::ptr_eq(bound_state, state) {
191            return true;
192        }
193        if bound_state.has_live_applier_host() {
194            return false;
195        }
196        drop(inner);
197
198        let mut inner = self.inner.borrow_mut();
199        let Some(bound_state) = inner.runtime_state.as_ref() else {
200            inner.runtime_state = Some(Rc::clone(state));
201            return true;
202        };
203        if Rc::ptr_eq(bound_state, state) {
204            return true;
205        }
206        if bound_state.has_live_applier_host() {
207            return false;
208        }
209
210        let previous_state = Rc::clone(bound_state);
211        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
212        lifecycle.flush_pending_drops();
213        let host_key = self.storage_key();
214        if previous_state
215            .dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)
216            .is_err()
217        {
218            inner.lifecycle = lifecycle;
219            return false;
220        }
221        previous_state.clear_host(self, &mut inner.table);
222        lifecycle.flush_pending_drops();
223        inner.runtime_state = Some(Rc::clone(state));
224        inner.lifecycle = lifecycle;
225        true
226    }
227
228    pub(crate) fn runtime_state(&self) -> Option<Rc<composer::ComposerRuntimeState>> {
229        self.inner.borrow().runtime_state.clone()
230    }
231
232    pub(crate) fn runtime_state_ptr(&self) -> Option<*const composer::ComposerRuntimeState> {
233        self.inner.borrow().runtime_state.as_ref().map(Rc::as_ptr)
234    }
235
236    pub(crate) fn borrow(&self) -> Ref<'_, SlotTable> {
237        Ref::map(self.inner.borrow(), |inner| &inner.table)
238    }
239
240    pub(crate) fn borrow_mut(&self) -> RefMut<'_, SlotTable> {
241        RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.table)
242    }
243
244    /// Takes the slot table out of the host after disposing what it retained. Fails if other
245    /// references to the host exist or a slot pass is active.
246    pub fn into_table(self: Rc<Self>) -> Result<SlotTable, NodeError> {
247        if Rc::strong_count(&self) != 1 {
248            return Err(NodeError::SlotHostUnavailable {
249                operation: "SlotsHost::into_table",
250                reason: "other host references are alive",
251            });
252        }
253        self.take_table_for_transfer()
254    }
255
256    fn take_table_for_transfer(&self) -> Result<SlotTable, NodeError> {
257        let inner = self.inner.borrow();
258        if inner.active_pass.is_some() {
259            return Err(NodeError::SlotHostUnavailable {
260                operation: "SlotsHost::into_table",
261                reason: "slot pass is active",
262            });
263        }
264        drop(inner);
265        let mut inner = self.inner.borrow_mut();
266        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
267        lifecycle.flush_pending_drops();
268        if let Some(state) = inner.runtime_state.clone() {
269            let host_key = self.storage_key();
270            if let Err(error) =
271                state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)
272            {
273                inner.lifecycle = lifecycle;
274                return Err(error);
275            }
276            state.clear_host(self, &mut inner.table);
277            lifecycle.flush_pending_drops();
278        }
279        let taken = std::mem::take(&mut inner.table);
280        self.storage_key.set(inner.table.storage_id());
281        inner.runtime_state = None;
282        inner.lifecycle = lifecycle;
283        Ok(taken)
284    }
285
286    /// Disposes the content of the slot table and replaces it with an empty one. Fails while a slot
287    /// pass is active.
288    pub fn reset(&self) -> Result<(), NodeError> {
289        let inner = self.inner.borrow();
290        if inner.active_pass.is_some() {
291            return Err(NodeError::SlotHostUnavailable {
292                operation: "SlotsHost::reset",
293                reason: "slot pass is active",
294            });
295        }
296        let runtime_state = inner.runtime_state.clone();
297        drop(inner);
298        let mut inner = self.inner.borrow_mut();
299        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
300        if let Some(state) = runtime_state {
301            let host_key = self.storage_key();
302            if let Err(error) =
303                state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)
304            {
305                inner.lifecycle = lifecycle;
306                return Err(error);
307            }
308            state.clear_host(self, &mut inner.table);
309        }
310        lifecycle.dispose_slot_table(&mut inner.table);
311        inner.table = SlotTable::default();
312        self.storage_key.set(inner.table.storage_id());
313        inner.runtime_state = None;
314        inner.lifecycle = slot::SlotLifecycleCoordinator::default();
315        Ok(())
316    }
317
318    pub(crate) fn abandon_after_apply_failure(&self) {
319        let inner = self.inner.borrow();
320        if inner.active_pass.is_some() {
321            log::error!("cannot abandon SlotsHost during an active pass");
322            return;
323        }
324        let runtime_state = inner.runtime_state.clone();
325        drop(inner);
326        let mut inner = self.inner.borrow_mut();
327        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
328        if let Some(state) = runtime_state {
329            let host_key = self.storage_key();
330            state.abandon_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle);
331        }
332        lifecycle.dispose_slot_table(&mut inner.table);
333        inner.table = SlotTable::default();
334        self.storage_key.set(inner.table.storage_id());
335        inner.runtime_state = None;
336        inner.lifecycle = slot::SlotLifecycleCoordinator::default();
337    }
338
339    pub(crate) fn debug_stats(&self) -> SlotTableDebugStats {
340        let inner = self.inner.borrow();
341        let local = inner.table.debug_stats();
342        let lifecycle = inner.lifecycle.debug_stats();
343        let retention = inner
344            .runtime_state
345            .clone()
346            .map(|state| state.slot_retention_debug_stats(self))
347            .unwrap_or_default();
348        SlotTableDebugStats::from_parts(local, lifecycle, retention)
349    }
350
351    #[cold]
352    #[inline(never)]
353    pub(crate) fn debug_snapshot(&self) -> SlotDebugSnapshot {
354        let inner = self.inner.borrow();
355        let mut snapshot = inner.table.debug_snapshot();
356        if let Some(state) = inner.runtime_state.clone() {
357            state.fill_slot_debug_snapshot(self, &mut snapshot);
358        }
359        snapshot
360    }
361
362    pub(crate) fn begin_pass(&self, mode: slot::SlotPassMode, folds: &Rc<slot::BranchFolds>) {
363        let mut inner = self.inner.borrow_mut();
364        let SlotsHostInner {
365            table,
366            active_pass,
367            pass_state,
368            ..
369        } = &mut *inner;
370        if active_pass.is_some() {
371            log::error!("slot pass already active for host");
372            return;
373        }
374        let state = match pass_state {
375            Some(state) => {
376                state.set_folds(Rc::clone(folds));
377                state
378            }
379            None => pass_state.insert(spare_write_state(folds)),
380        };
381        state.reset_for_pass(mode);
382        *active_pass = Some(table.storage_capacity());
383    }
384
385    pub(crate) fn has_active_pass(&self) -> bool {
386        self.inner.borrow().active_pass.is_some()
387    }
388
389    pub(crate) fn abandon_active_pass(&self) {
390        self.inner.borrow_mut().active_pass = None;
391    }
392
393    pub(crate) fn with_write_session<R>(
394        &self,
395        f: impl FnOnce(&mut slot::SlotWriteSession<'_>) -> R,
396    ) -> R {
397        let mut inner = self.inner.borrow_mut();
398        let SlotsHostInner {
399            table,
400            lifecycle,
401            active_pass,
402            pass_state,
403            ..
404        } = &mut *inner;
405        let state = pass_state
406            .as_mut()
407            .filter(|_| active_pass.is_some())
408            .expect("slot write session requires an active pass");
409        let mut session = table.write_session(lifecycle, state);
410        f(&mut session)
411    }
412
413    pub(crate) fn with_table_and_lifecycle_mut<R>(
414        &self,
415        f: impl FnOnce(&mut SlotTable, &mut slot::SlotLifecycleCoordinator) -> R,
416    ) -> R {
417        let mut inner = self.inner.borrow_mut();
418        let SlotsHostInner {
419            table, lifecycle, ..
420        } = &mut *inner;
421        f(table, lifecycle)
422    }
423
424    pub(crate) fn finish_pass(
425        &self,
426        applier: &mut dyn Applier,
427    ) -> Result<FinishedSlotPass, NodeError> {
428        let mut inner = self.inner.borrow_mut();
429        let SlotsHostInner {
430            table,
431            lifecycle,
432            active_pass,
433            pass_state,
434            ..
435        } = &mut *inner;
436        let (Some(storage_capacity), Some(state)) = (active_pass.take(), pass_state.as_mut())
437        else {
438            return Ok(FinishedSlotPass::default());
439        };
440
441        state.flush_payload_location_refreshes(table);
442
443        #[cfg(any(test, debug_assertions))]
444        if let Err(err) = state.validate(table) {
445            log::error!("slot writer invariant violation before finalize_pass: {err:?}");
446            return Err(NodeError::SlotHostUnavailable {
447                operation: "SlotsHost::finish_pass",
448                reason: "slot writer invariant violation",
449            });
450        }
451
452        let detached_root_children = {
453            let mut session = table.write_session(lifecycle, state);
454            session.finalize_pass(applier)?
455        };
456
457        let outcome = SlotPassOutcome {
458            compacted: state.request_compaction,
459            compact_anchor_registry_storage: state.request_anchor_storage_compaction,
460            compact_payload_storage: state.request_payload_storage_compaction,
461            trim_growth_slack: state.removed_nothing()
462                && table.storage_capacity() > storage_capacity,
463        };
464        if state.is_shared()
465            && let Some(state) = pass_state.take()
466        {
467            recycle_write_state(state);
468        }
469        Ok(FinishedSlotPass {
470            outcome,
471            detached_root_children,
472        })
473    }
474
475    pub(crate) fn flush_pending_drops(&self) {
476        self.inner.borrow_mut().lifecycle.flush_pending_drops();
477    }
478
479    pub(crate) fn complete_pass_cleanup(&self, outcome: &SlotPassOutcome) {
480        let mut inner = self.inner.borrow_mut();
481        let SlotsHostInner {
482            table,
483            lifecycle,
484            runtime_state,
485            ..
486        } = &mut *inner;
487        lifecycle.flush_pending_drops();
488        if outcome.compacted {
489            table.compact_storage();
490            lifecycle.compact_storage();
491        } else if outcome.trim_growth_slack {
492            table.trim_growth_slack();
493        }
494        if let Some(state) = runtime_state.clone() {
495            state.compact_table_identity_storage_for_host(
496                self,
497                table,
498                outcome.compact_anchor_registry_storage,
499                outcome.compact_payload_storage,
500            );
501        } else {
502            if outcome.compact_anchor_registry_storage {
503                table.compact_anchor_registry_storage(None);
504            }
505            if outcome.compact_payload_storage {
506                table.compact_payload_anchor_registry_storage(None);
507            }
508        }
509        table.assert_fast_integrity("slot pass cleanup");
510        #[cfg(any(test, debug_assertions))]
511        {
512            table.debug_verify();
513            if let Some(state) = runtime_state.clone() {
514                state.debug_verify_host(self, table);
515            }
516        }
517    }
518}