Skip to main content

fory_core/
context.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::buffer::{Reader, Writer};
19use crate::config::Config;
20use std::collections::HashMap;
21use std::mem;
22
23use crate::error::Error;
24use crate::meta::MetaString;
25use crate::resolver::meta_resolver::{MetaReaderResolver, MetaWriterResolver};
26use crate::resolver::meta_string_resolver::{MetaStringReaderResolver, MetaStringWriterResolver};
27use crate::resolver::{RefReader, RefWriter};
28use crate::resolver::{TypeInfo, TypeResolver};
29use crate::serializer::StructSerializer;
30use crate::type_id as types;
31use crate::TypeId;
32use std::rc::Rc;
33
34/// Thread-local context cache with fast path for single Fory instance.
35/// Uses (cached_id, context) for O(1) access when using same Fory instance repeatedly.
36/// Falls back to HashMap for multiple Fory instances per thread.
37pub struct ContextCache<T> {
38    /// Fast path: cached context for the most recently used Fory instance
39    cached_id: u64,
40    cached_context: Option<Box<T>>,
41    /// Slow path: HashMap for other Fory instances
42    others: HashMap<u64, Box<T>>,
43}
44
45impl<T> ContextCache<T> {
46    pub fn new() -> Self {
47        ContextCache {
48            cached_id: u64::MAX,
49            cached_context: None,
50            others: HashMap::new(),
51        }
52    }
53
54    #[inline(always)]
55    pub fn get_or_insert(&mut self, id: u64, create: impl FnOnce() -> Box<T>) -> &mut T {
56        if self.cached_id == id {
57            // Fast path: same Fory instance as last time
58            return self.cached_context.as_mut().unwrap();
59        }
60
61        // Check if we need to swap with cached
62        if self.cached_context.is_some() {
63            // Move current cached to others
64            let old_id = self.cached_id;
65            let old_context = self.cached_context.take().unwrap();
66            self.others.insert(old_id, old_context);
67        }
68
69        // Get or create context for new id
70        let context = self.others.remove(&id).unwrap_or_else(create);
71        self.cached_id = id;
72        self.cached_context = Some(context);
73        self.cached_context.as_mut().unwrap()
74    }
75
76    /// Like `get_or_insert`, but the create closure returns a Result.
77    /// This allows error handling during context creation without pre-fetching resources.
78    #[inline(always)]
79    pub fn get_or_insert_result<E>(
80        &mut self,
81        id: u64,
82        create: impl FnOnce() -> Result<Box<T>, E>,
83    ) -> Result<&mut T, E> {
84        if self.cached_id == id {
85            // Fast path: same Fory instance as last time
86            return Ok(self.cached_context.as_mut().unwrap());
87        }
88
89        // Check if we need to swap with cached
90        if self.cached_context.is_some() {
91            // Move current cached to others
92            let old_id = self.cached_id;
93            let old_context = self.cached_context.take().unwrap();
94            self.others.insert(old_id, old_context);
95        }
96
97        // Get or create context for new id
98        let context = match self.others.remove(&id) {
99            Some(ctx) => ctx,
100            None => create()?,
101        };
102        self.cached_id = id;
103        self.cached_context = Some(context);
104        Ok(self.cached_context.as_mut().unwrap())
105    }
106}
107
108impl<T> Default for ContextCache<T> {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114/// Serialization state container used on a single thread at a time.
115/// Sharing the same instance across threads simultaneously causes undefined behavior.
116#[allow(clippy::needless_lifetimes)]
117pub struct WriteContext<'a> {
118    // Replicated environment fields (direct access, no Arc indirection for flags)
119    type_resolver: TypeResolver,
120    compatible: bool,
121    share_meta: bool,
122    compress_string: bool,
123    xlang: bool,
124    check_struct_version: bool,
125    track_ref: bool,
126
127    // Context-specific fields
128    default_writer: Option<Writer<'a>>,
129    pub writer: Writer<'a>,
130    meta_resolver: MetaWriterResolver,
131    meta_string_resolver: MetaStringWriterResolver,
132    pub ref_writer: RefWriter,
133}
134
135#[allow(clippy::needless_lifetimes)]
136impl<'a> WriteContext<'a> {
137    pub fn new(type_resolver: TypeResolver, config: Config) -> WriteContext<'a> {
138        WriteContext {
139            type_resolver,
140            compatible: config.compatible,
141            share_meta: config.share_meta,
142            compress_string: config.compress_string,
143            xlang: config.xlang,
144            check_struct_version: config.check_struct_version,
145            track_ref: config.track_ref,
146            default_writer: None,
147            writer: Writer::from_buffer(Self::get_leak_buffer()),
148            meta_resolver: MetaWriterResolver::default(),
149            meta_string_resolver: MetaStringWriterResolver::default(),
150            ref_writer: RefWriter::new(),
151        }
152    }
153
154    #[inline(always)]
155    fn get_leak_buffer() -> &'static mut Vec<u8> {
156        Box::leak(Box::new(vec![]))
157    }
158
159    #[inline(always)]
160    pub fn attach_writer(&mut self, writer: Writer<'a>) {
161        let old = mem::replace(&mut self.writer, writer);
162        self.default_writer = Some(old);
163    }
164
165    #[inline(always)]
166    pub fn detach_writer(&mut self) {
167        let default = mem::take(&mut self.default_writer);
168        self.writer = default.unwrap();
169    }
170
171    /// Get type resolver
172    #[inline(always)]
173    pub fn get_type_resolver(&self) -> &TypeResolver {
174        &self.type_resolver
175    }
176
177    #[inline(always)]
178    pub fn get_provider_type_info(
179        &self,
180        provider_type_id: &std::any::TypeId,
181    ) -> Result<Rc<TypeInfo>, Error> {
182        self.type_resolver.get_provider_type_info(provider_type_id)
183    }
184
185    #[inline(always)]
186    pub fn get_target_type_info(
187        &self,
188        target_type_id: &std::any::TypeId,
189    ) -> Result<Rc<TypeInfo>, Error> {
190        self.type_resolver.get_target_type_info(target_type_id)
191    }
192
193    /// Check if compatible mode is enabled
194    #[inline(always)]
195    pub fn is_compatible(&self) -> bool {
196        self.compatible
197    }
198
199    /// Check if meta sharing is enabled
200    #[inline(always)]
201    pub fn is_share_meta(&self) -> bool {
202        self.share_meta
203    }
204
205    /// Check if string compression is enabled
206    #[inline(always)]
207    pub fn is_compress_string(&self) -> bool {
208        self.compress_string
209    }
210
211    /// Check if xlang mode is enabled
212    #[inline(always)]
213    pub fn is_xlang(&self) -> bool {
214        self.xlang
215    }
216
217    /// Check if class version checking is enabled
218    #[inline(always)]
219    pub fn is_check_struct_version(&self) -> bool {
220        self.check_struct_version
221    }
222
223    /// Check if reference tracking is enabled
224    #[inline(always)]
225    pub fn is_track_ref(&self) -> bool {
226        self.track_ref
227    }
228
229    /// Write type meta inline using streaming protocol.
230    /// Writes index marker with LSB indicating new type or reference.
231    #[inline(always)]
232    pub fn write_type_meta(&mut self, type_id: std::any::TypeId) -> Result<(), Error> {
233        self.meta_resolver
234            .write_type_meta(&mut self.writer, type_id, &self.type_resolver)
235    }
236
237    /// Write generated struct type info without Rust TypeId hash lookups.
238    #[inline(always)]
239    pub fn write_struct_type_info<T: StructSerializer>(&mut self) -> Result<(), Error> {
240        let rust_type_id = std::any::TypeId::of::<T>();
241        let type_index = T::type_index();
242        let type_id = self.type_resolver.get_type_id_by_index(type_index)?;
243        match type_id {
244            TypeId::STRUCT | TypeId::ENUM | TypeId::EXT | TypeId::TYPED_UNION => {
245                self.writer.write_u8(type_id as u8);
246                let user_type_id = self
247                    .type_resolver
248                    .get_user_type_id_by_index(&rust_type_id, type_index)?;
249                self.writer.write_var_u32(user_type_id);
250            }
251            TypeId::COMPATIBLE_STRUCT | TypeId::NAMED_COMPATIBLE_STRUCT => {
252                self.writer.write_u8(type_id as u8);
253                self.meta_resolver.write_type_meta_fast(
254                    &mut self.writer,
255                    rust_type_id,
256                    type_index,
257                    &self.type_resolver,
258                )?;
259            }
260            TypeId::NAMED_ENUM | TypeId::NAMED_EXT | TypeId::NAMED_STRUCT | TypeId::NAMED_UNION
261                if self.is_share_meta() =>
262            {
263                self.writer.write_u8(type_id as u8);
264                self.meta_resolver.write_type_meta_fast(
265                    &mut self.writer,
266                    rust_type_id,
267                    type_index,
268                    &self.type_resolver,
269                )?;
270            }
271            _ => {
272                self.write_provider_type_info(type_id as u32, rust_type_id)?;
273            }
274        }
275        Ok(())
276    }
277
278    pub fn write_provider_type_info(
279        &mut self,
280        wire_type_id: u32,
281        provider_type_id: std::any::TypeId,
282    ) -> Result<Rc<TypeInfo>, Error> {
283        let type_info = self
284            .type_resolver
285            .get_provider_type_info(&provider_type_id)?;
286        self.write_resolved_type_info(wire_type_id, type_info)
287    }
288
289    pub fn write_target_type_info(
290        &mut self,
291        wire_type_id: u32,
292        target_type_id: std::any::TypeId,
293    ) -> Result<Rc<TypeInfo>, Error> {
294        let type_info = self.type_resolver.get_target_type_info(&target_type_id)?;
295        self.write_resolved_type_info(wire_type_id, type_info)
296    }
297
298    #[doc(hidden)]
299    #[inline(always)]
300    pub fn write_resolved_type_info(
301        &mut self,
302        wire_type_id: u32,
303        type_info: Rc<TypeInfo>,
304    ) -> Result<Rc<TypeInfo>, Error> {
305        if types::is_internal_type(wire_type_id) {
306            self.writer.write_u8(wire_type_id as u8);
307            return Ok(type_info);
308        }
309        let wire_type_id = type_info.get_type_id();
310        let namespace = type_info.get_namespace();
311        let type_name = type_info.get_type_name();
312        self.writer.write_u8(wire_type_id as u8);
313        // should be compiled to jump table generation
314        match wire_type_id {
315            TypeId::ENUM | TypeId::STRUCT | TypeId::EXT | TypeId::TYPED_UNION => {
316                let user_type_id = type_info.get_user_type_id();
317                self.writer.write_var_u32(user_type_id);
318            }
319            TypeId::COMPATIBLE_STRUCT | TypeId::NAMED_COMPATIBLE_STRUCT => {
320                self.meta_resolver
321                    .write_resolved_type_meta(&mut self.writer, &type_info)?;
322            }
323            TypeId::NAMED_ENUM | TypeId::NAMED_EXT | TypeId::NAMED_STRUCT | TypeId::NAMED_UNION => {
324                if self.is_share_meta() {
325                    self.meta_resolver
326                        .write_resolved_type_meta(&mut self.writer, &type_info)?;
327                } else {
328                    self.write_meta_string_bytes(namespace)?;
329                    self.write_meta_string_bytes(type_name)?;
330                }
331            }
332            _ => {
333                // default case: do nothing
334            }
335        }
336        Ok(type_info)
337    }
338
339    #[inline(always)]
340    pub fn write_meta_string_bytes(&mut self, ms: Rc<MetaString>) -> Result<(), Error> {
341        self.meta_string_resolver
342            .write_meta_string_bytes(&mut self.writer, ms)
343    }
344
345    #[inline(always)]
346    pub fn reset(&mut self) {
347        self.meta_resolver.reset();
348        self.meta_string_resolver.reset();
349        self.ref_writer.reset();
350    }
351}
352
353#[allow(clippy::needless_lifetimes)]
354impl<'a> Drop for WriteContext<'a> {
355    fn drop(&mut self) {
356        unsafe {
357            drop(Box::from_raw(self.writer.bf));
358        }
359    }
360}
361
362/// Deserialization state container used on a single thread at a time.
363/// Sharing the same instance across threads simultaneously causes undefined behavior.
364pub struct ReadContext<'a> {
365    // Replicated environment fields (direct access, no Arc indirection for flags)
366    type_resolver: TypeResolver,
367    config: Config,
368    compatible: bool,
369    share_meta: bool,
370    xlang: bool,
371    max_dyn_depth: u32,
372    check_struct_version: bool,
373    check_string_read: bool,
374    pub(crate) remaining_graph_memory_bytes: usize,
375    pub(crate) remaining_unbacked_container_items: usize,
376
377    // Context-specific fields
378    pub reader: Reader<'a>,
379    pub meta_resolver: MetaReaderResolver,
380    meta_string_resolver: MetaStringReaderResolver,
381    pub ref_reader: RefReader,
382    current_depth: u32,
383}
384
385impl<'a> ReadContext<'a> {
386    pub fn new(type_resolver: TypeResolver, config: Config) -> ReadContext<'a> {
387        ReadContext {
388            type_resolver,
389            config: config.clone(),
390            compatible: config.compatible,
391            share_meta: config.share_meta,
392            xlang: config.xlang,
393            max_dyn_depth: config.max_dyn_depth,
394            check_struct_version: config.check_struct_version,
395            check_string_read: config.check_string_read,
396            remaining_graph_memory_bytes: 0,
397            remaining_unbacked_container_items: 0,
398            reader: Reader::default(),
399            meta_resolver: MetaReaderResolver::default(),
400            meta_string_resolver: MetaStringReaderResolver::default(),
401            ref_reader: RefReader::new(),
402            current_depth: 0,
403        }
404    }
405
406    /// Get type resolver
407    #[inline(always)]
408    pub fn get_type_resolver(&self) -> &TypeResolver {
409        &self.type_resolver
410    }
411
412    /// Check if compatible mode is enabled
413    #[inline(always)]
414    pub fn is_compatible(&self) -> bool {
415        self.compatible
416    }
417
418    /// Check if meta sharing is enabled
419    #[inline(always)]
420    pub fn is_share_meta(&self) -> bool {
421        self.share_meta
422    }
423
424    /// Check if xlang mode is enabled
425    #[inline(always)]
426    pub fn is_xlang(&self) -> bool {
427        self.xlang
428    }
429
430    /// Check if class version checking is enabled
431    #[inline(always)]
432    pub fn is_check_struct_version(&self) -> bool {
433        self.check_struct_version
434    }
435
436    /// Check if UTF-8 string payload validation is enabled.
437    #[inline(always)]
438    pub fn is_check_string_read(&self) -> bool {
439        self.check_string_read
440    }
441
442    /// Get maximum dynamic depth
443    #[inline(always)]
444    pub fn max_dyn_depth(&self) -> u32 {
445        self.max_dyn_depth
446    }
447
448    #[inline(always)]
449    pub fn attach_reader(&mut self, reader: Reader<'a>) {
450        self.reader = reader;
451    }
452
453    #[inline(always)]
454    #[doc(hidden)]
455    pub fn reserve_graph_memory(&mut self, bytes: usize) -> Result<(), Error> {
456        let remaining = self.remaining_graph_memory_bytes;
457        if bytes > remaining {
458            return Err(graph_memory_exceeded(
459                bytes,
460                remaining,
461                self.config.max_graph_memory_bytes,
462            ));
463        }
464        self.remaining_graph_memory_bytes = remaining - bytes;
465        Ok(())
466    }
467
468    #[inline(always)]
469    #[doc(hidden)]
470    pub fn remaining_unbacked_container_items(&self) -> usize {
471        self.remaining_unbacked_container_items
472    }
473
474    #[inline(always)]
475    #[doc(hidden)]
476    pub fn reserve_unbacked_container_items(&mut self, items: usize) -> Result<(), Error> {
477        let remaining = self.remaining_unbacked_container_items;
478        if items > remaining {
479            return Err(unbacked_container_items_exceeded(items, remaining));
480        }
481        self.remaining_unbacked_container_items = remaining - items;
482        Ok(())
483    }
484
485    #[inline(always)]
486    pub fn detach_reader(&mut self) -> Reader<'_> {
487        mem::take(&mut self.reader)
488    }
489
490    #[inline(always)]
491    pub fn get_type_info_by_index(&self, type_index: usize) -> Result<&Rc<TypeInfo>, Error> {
492        self.meta_resolver.get(type_index).ok_or_else(|| {
493            Error::type_error(format!("TypeInfo not found for type index: {}", type_index))
494        })
495    }
496
497    #[inline(always)]
498    pub fn get_meta(&self, type_index: usize) -> Result<&Rc<TypeInfo>, Error> {
499        self.get_type_info_by_index(type_index)
500    }
501
502    /// Read type meta inline using streaming protocol.
503    /// Returns the TypeInfo for this type.
504    #[inline(always)]
505    pub fn read_type_meta(&mut self) -> Result<Rc<TypeInfo>, Error> {
506        self.meta_resolver
507            .read_type_meta(&mut self.reader, &self.type_resolver, &self.config)
508    }
509
510    #[inline(always)]
511    fn read_type_meta_for(&mut self, expected: &Rc<TypeInfo>) -> Result<Rc<TypeInfo>, Error> {
512        self.meta_resolver.read_type_meta_for(
513            &mut self.reader,
514            &self.type_resolver,
515            &self.config,
516            expected,
517        )
518    }
519
520    #[inline(always)]
521    fn read_struct_type_meta_for(
522        &mut self,
523        expected: &Rc<TypeInfo>,
524    ) -> Result<Rc<TypeInfo>, Error> {
525        self.meta_resolver.read_struct_type_meta_for(
526            &mut self.reader,
527            &self.type_resolver,
528            &self.config,
529            expected,
530        )
531    }
532
533    pub fn read_any_type_info(&mut self) -> Result<Rc<TypeInfo>, Error> {
534        self.read_any_type_info_with_expected(None, false)
535    }
536
537    #[inline(always)]
538    pub(crate) fn read_type_info_for(
539        &mut self,
540        expected_target: std::any::TypeId,
541    ) -> Result<Rc<TypeInfo>, Error> {
542        let expected = self.type_resolver.get_target_type_info(&expected_target)?;
543        self.read_any_type_info_with_expected(Some(&expected), false)
544    }
545
546    #[inline(always)]
547    pub(crate) fn read_struct_type_info_for(
548        &mut self,
549        expected_target: std::any::TypeId,
550    ) -> Result<Rc<TypeInfo>, Error> {
551        let expected = self.type_resolver.get_target_type_info(&expected_target)?;
552        self.read_any_type_info_with_expected(Some(&expected), true)
553    }
554
555    fn read_any_type_info_with_expected(
556        &mut self,
557        expected: Option<&Rc<TypeInfo>>,
558        allow_structural_stub: bool,
559    ) -> Result<Rc<TypeInfo>, Error> {
560        let fory_type_id = self.reader.read_u8()? as u32;
561        // should be compiled to jump table generation
562        let type_info = match fory_type_id {
563            types::ENUM | types::STRUCT | types::EXT | types::TYPED_UNION => {
564                let user_type_id = self.reader.read_var_u32()?;
565                self.type_resolver
566                    .get_user_type_info_by_id(user_type_id)
567                    .ok_or_else(|| Error::type_error("ID harness not found"))?
568            }
569            types::COMPATIBLE_STRUCT | types::NAMED_COMPATIBLE_STRUCT => {
570                // Read type meta inline using streaming protocol
571                return match expected {
572                    Some(expected) if allow_structural_stub => {
573                        self.read_struct_type_meta_for(expected)
574                    }
575                    Some(expected) => self.read_type_meta_for(expected),
576                    None => self.read_type_meta(),
577                };
578            }
579            types::NAMED_ENUM | types::NAMED_EXT | types::NAMED_STRUCT | types::NAMED_UNION => {
580                if self.is_share_meta() {
581                    // Read type meta inline using streaming protocol
582                    return match expected {
583                        Some(expected) if allow_structural_stub => {
584                            self.read_struct_type_meta_for(expected)
585                        }
586                        Some(expected) => self.read_type_meta_for(expected),
587                        None => self.read_type_meta(),
588                    };
589                } else {
590                    self.read_named_type_info()?
591                }
592            }
593            _ => self
594                .type_resolver
595                .get_type_info_by_id(fory_type_id)
596                .ok_or_else(|| Error::type_error("ID harness not found"))?,
597        };
598        if let Some(expected) = expected {
599            let expected_target = expected
600                .get_harness()
601                .target_type_id()
602                .ok_or_else(|| Error::type_error("expected TypeInfo has no concrete target"))?;
603            let resolved_target = type_info.get_harness().target_type_id();
604            if resolved_target != Some(expected_target)
605                && !(allow_structural_stub && resolved_target.is_none())
606            {
607                return Err(Error::type_error(
608                    "resolved TypeInfo target does not match declared target",
609                ));
610            }
611        }
612        Ok(type_info)
613    }
614
615    // Name decoding and resolver fallback allocate; keep them out of the common ID and compatible
616    // dispatch body without marking successful named dispatch as cold.
617    #[inline(never)]
618    fn read_named_type_info(&mut self) -> Result<Rc<TypeInfo>, Error> {
619        let namespace = self.read_meta_string()?.to_owned();
620        let type_name = self.read_meta_string()?.to_owned();
621        let rc_namespace = Rc::from(namespace.clone());
622        let rc_type_name = Rc::from(type_name.clone());
623        self.type_resolver
624            .get_type_info_by_meta_string_name(rc_namespace, rc_type_name)
625            .or_else(|| {
626                self.type_resolver
627                    .get_type_info_by_name(namespace.original.as_str(), type_name.original.as_str())
628            })
629            .ok_or_else(|| {
630                Error::type_error(format!(
631                    "Name harness not found: namespace='{}', type='{}'",
632                    namespace.original, type_name.original
633                ))
634            })
635    }
636
637    #[inline(always)]
638    pub fn get_provider_type_info(
639        &self,
640        provider_type_id: &std::any::TypeId,
641    ) -> Result<Rc<TypeInfo>, Error> {
642        self.type_resolver.get_provider_type_info(provider_type_id)
643    }
644
645    #[inline(always)]
646    pub fn get_target_type_info(
647        &self,
648        target_type_id: &std::any::TypeId,
649    ) -> Result<Rc<TypeInfo>, Error> {
650        self.type_resolver.get_target_type_info(target_type_id)
651    }
652
653    #[inline(always)]
654    pub fn read_meta_string(&mut self) -> Result<&MetaString, Error> {
655        self.meta_string_resolver.read_meta_string(&mut self.reader)
656    }
657
658    #[inline(always)]
659    pub fn inc_depth(&mut self) -> Result<(), Error> {
660        self.current_depth += 1;
661        if self.current_depth > self.max_dyn_depth() {
662            return Err(Error::depth_exceed(format!(
663                "Maximum dynamic object nesting depth ({}) exceeded. Current depth: {}. \
664                    This may indicate a circular reference or overly deep object graph. \
665                    Consider increasing max_dyn_depth if this is expected.",
666                self.max_dyn_depth(),
667                self.current_depth
668            )));
669        }
670        Ok(())
671    }
672
673    #[inline(always)]
674    pub fn dec_depth(&mut self) {
675        // Nested readers decrement only after their child completed successfully. An error keeps
676        // the failed path's depth until the root reset owns all read-side cleanup.
677        self.current_depth = self.current_depth.saturating_sub(1);
678    }
679
680    #[inline(always)]
681    pub fn reset(&mut self) {
682        self.meta_resolver.reset();
683        self.meta_string_resolver.reset();
684        self.ref_reader.reset();
685        // Root reset is the only failure-cleanup owner for read depth.
686        self.current_depth = 0;
687        self.remaining_unbacked_container_items = 0;
688    }
689}
690
691#[cold]
692#[inline(never)]
693fn graph_memory_exceeded(bytes: usize, remaining: usize, limit: usize) -> Error {
694    Error::invalid_data(format!(
695        "estimated graph memory request {} bytes exceeds max_graph_memory_bytes remaining budget {} bytes out of effective limit {} bytes",
696        bytes, remaining, limit
697    ))
698}
699
700#[cold]
701#[inline(never)]
702fn unbacked_container_items_exceeded(items: usize, remaining: usize) -> Error {
703    Error::invalid_data(format!(
704        "container read work request {items} items exceeds max_unbacked_container_items remaining budget {remaining} items"
705    ))
706}