Skip to main content

zeppelin_embed/vfs/
mod.rs

1//! Synchronous virtual-filesystem seam for crash and fault injection.
2
3#[cfg(any(test, feature = "test-support"))]
4pub mod crash;
5#[cfg(any(test, feature = "test-support"))]
6pub mod fault;
7
8use std::fs::{File, OpenOptions};
9use std::io::{IoSlice, Read, Seek, SeekFrom, Write};
10#[cfg(unix)]
11use std::os::fd::AsRawFd;
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14use std::sync::atomic::{AtomicU64, Ordering};
15
16/// Storage-ordering primitive requested by a persisted commit.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum SyncKind {
19    /// Order prior writes before later writes without requesting durable media.
20    ///
21    /// Linux has no barrier-only primitive, so `ordered` durability degrades
22    /// to `fdatasync` on Linux.
23    Barrier,
24    /// Flush prior writes through the platform's durable-media primitive.
25    Full,
26}
27
28/// One open append-only file owned by a WAL writer.
29pub trait VfsFile: Send {
30    /// Appends every supplied byte without reopening the path.
31    fn append(&mut self, bytes: &[u8]) -> std::io::Result<()>;
32    /// Appends one logical group from discontiguous owned buffers.
33    ///
34    /// The default preserves compatibility for test filesystems. Production
35    /// files override it with one vectored write sequence, so WAL visibility
36    /// and persistence can share encoded record storage without coalescing it.
37    fn append_vectored(&mut self, buffers: &mut [IoSlice<'_>]) -> std::io::Result<()> {
38        for buffer in buffers {
39            self.append(buffer)?;
40        }
41        Ok(())
42    }
43    /// Synchronizes this already-open handle using the named primitive.
44    fn sync(&self, kind: SyncKind) -> std::io::Result<()>;
45}
46
47/// Minimal synchronous filesystem operations used by persisted commits.
48pub trait Vfs: Send + Sync {
49    /// Supplies an optional counter for mmap-backed segment payload accesses.
50    #[doc(hidden)]
51    fn segment_data_read_counter(&self) -> Option<Arc<AtomicU64>> {
52        None
53    }
54    /// Admits a store directory, creating missing ancestors when requested,
55    /// and reports whether the resulting path is a directory.
56    fn ensure_directory(&self, path: &Path, create: bool) -> std::io::Result<bool>;
57    /// Opens an existing path and returns its byte length.
58    fn open(&self, path: &Path) -> std::io::Result<u64>;
59    /// Opens an existing path as an owned file suitable for memory mapping.
60    ///
61    /// Test filesystems without a native file descriptor return
62    /// [`std::io::ErrorKind::Unsupported`]. Consequently `MemoryVfs`,
63    /// `CrashVfs`, and memory-backed `FaultVfs` images cannot publish or reopen
64    /// a sealed segment; use a path-backed VFS when exercising that lifecycle.
65    fn open_for_map(&self, path: &Path) -> std::io::Result<File>;
66    /// Reads an entire file.
67    fn read(&self, path: &Path) -> std::io::Result<Vec<u8>>;
68    /// Reads at most `length` bytes beginning at `offset`.
69    fn read_range(&self, path: &Path, offset: u64, length: usize) -> std::io::Result<Vec<u8>>;
70    /// Creates or truncates a file and writes all bytes.
71    fn write(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()>;
72    /// Opens or creates one file for handle-oriented append-only writes.
73    fn open_append(&self, path: &Path) -> std::io::Result<Box<dyn VfsFile>>;
74    /// Atomically renames one path over another according to platform semantics.
75    fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()>;
76    /// Synchronizes a file or directory using an explicit platform primitive.
77    fn sync(&self, path: &Path, kind: SyncKind) -> std::io::Result<()>;
78    /// Lists direct children of a directory.
79    fn list(&self, directory: &Path) -> std::io::Result<Vec<PathBuf>>;
80    /// Deletes one file.
81    fn delete(&self, path: &Path) -> std::io::Result<()>;
82}
83
84/// Standard-library-backed production filesystem.
85#[derive(Clone, Copy, Debug, Default)]
86pub struct StdVfs;
87
88struct StdVfsFile(File);
89
90impl VfsFile for StdVfsFile {
91    fn append(&mut self, bytes: &[u8]) -> std::io::Result<()> {
92        self.0.write_all(bytes)
93    }
94
95    fn append_vectored(&mut self, buffers: &mut [IoSlice<'_>]) -> std::io::Result<()> {
96        let mut remaining = buffers;
97        while !remaining.is_empty() {
98            let written = self.0.write_vectored(remaining)?;
99            if written == 0 {
100                return Err(std::io::Error::new(
101                    std::io::ErrorKind::WriteZero,
102                    "failed to append vectored WAL group",
103                ));
104            }
105            IoSlice::advance_slices(&mut remaining, written);
106        }
107        Ok(())
108    }
109
110    fn sync(&self, kind: SyncKind) -> std::io::Result<()> {
111        sync_file(&self.0, kind)
112    }
113}
114
115impl Vfs for StdVfs {
116    fn ensure_directory(&self, path: &Path, create: bool) -> std::io::Result<bool> {
117        if create {
118            std::fs::create_dir_all(path)?;
119        }
120        Ok(std::fs::metadata(path)?.is_dir())
121    }
122
123    fn open(&self, path: &Path) -> std::io::Result<u64> {
124        Ok(File::open(path)?.metadata()?.len())
125    }
126
127    fn open_for_map(&self, path: &Path) -> std::io::Result<File> {
128        File::open(path)
129    }
130
131    fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
132        let mut file = File::open(path)?;
133        let length = usize::try_from(file.metadata()?.len()).map_err(|_| {
134            std::io::Error::new(std::io::ErrorKind::InvalidData, "file length exceeds usize")
135        })?;
136        let mut bytes = Vec::with_capacity(length);
137        file.read_to_end(&mut bytes)?;
138        Ok(bytes)
139    }
140
141    fn read_range(&self, path: &Path, offset: u64, length: usize) -> std::io::Result<Vec<u8>> {
142        let mut file = File::open(path)?;
143        file.seek(SeekFrom::Start(offset))?;
144        let mut bytes = vec![0_u8; length];
145        let mut filled = 0_usize;
146        while filled < length {
147            let remaining = bytes.get_mut(filled..).ok_or_else(|| {
148                std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid read range")
149            })?;
150            let read = file.read(remaining)?;
151            if read == 0 {
152                break;
153            }
154            filled = filled.saturating_add(read);
155        }
156        bytes.truncate(filled);
157        Ok(bytes)
158    }
159
160    fn write(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
161        let mut file = OpenOptions::new()
162            .create(true)
163            .truncate(true)
164            .write(true)
165            .open(path)?;
166        file.write_all(bytes)
167    }
168
169    fn open_append(&self, path: &Path) -> std::io::Result<Box<dyn VfsFile>> {
170        let file = OpenOptions::new().create(true).append(true).open(path)?;
171        Ok(Box::new(StdVfsFile(file)))
172    }
173
174    fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()> {
175        std::fs::rename(from, to)
176    }
177
178    fn sync(&self, path: &Path, kind: SyncKind) -> std::io::Result<()> {
179        sync_file(&File::open(path)?, kind)
180    }
181
182    fn list(&self, directory: &Path) -> std::io::Result<Vec<PathBuf>> {
183        std::fs::read_dir(directory)?
184            .map(|entry| entry.map(|value| value.path()))
185            .collect()
186    }
187
188    fn delete(&self, path: &Path) -> std::io::Result<()> {
189        std::fs::remove_file(path)
190    }
191}
192
193/// Byte/call-counting decorator used by deterministic open-cost gates.
194pub struct CountingVfs<V> {
195    inner: V,
196    counters: Arc<CountingVfsCounters>,
197    read_path_filter: Option<PathBuf>,
198}
199
200#[derive(Default)]
201struct CountingVfsCounters {
202    open_calls: AtomicU64,
203    open_for_map_calls: AtomicU64,
204    read_calls: AtomicU64,
205    filtered_read_calls: AtomicU64,
206    read_bytes: AtomicU64,
207    segment_bytes_read: Arc<AtomicU64>,
208    write_calls: AtomicU64,
209    bytes_written: AtomicU64,
210    append_calls: AtomicU64,
211    bytes_appended: AtomicU64,
212    rename_calls: AtomicU64,
213    delete_calls: AtomicU64,
214    barrier_sync_calls: AtomicU64,
215    full_sync_calls: AtomicU64,
216    handle_barrier_sync_calls: AtomicU64,
217    handle_full_sync_calls: AtomicU64,
218}
219
220struct CountingVfsFile {
221    inner: Box<dyn VfsFile>,
222    counters: Arc<CountingVfsCounters>,
223}
224
225impl VfsFile for CountingVfsFile {
226    fn append(&mut self, bytes: &[u8]) -> std::io::Result<()> {
227        self.inner.append(bytes)?;
228        self.counters.append_calls.fetch_add(1, Ordering::Relaxed);
229        self.counters
230            .bytes_appended
231            .fetch_add(bytes.len() as u64, Ordering::Relaxed);
232        Ok(())
233    }
234
235    fn append_vectored(&mut self, buffers: &mut [IoSlice<'_>]) -> std::io::Result<()> {
236        let bytes = buffers
237            .iter()
238            .fold(0_usize, |total, buffer| total.saturating_add(buffer.len()));
239        self.inner.append_vectored(buffers)?;
240        self.counters.append_calls.fetch_add(1, Ordering::Relaxed);
241        self.counters
242            .bytes_appended
243            .fetch_add(bytes as u64, Ordering::Relaxed);
244        Ok(())
245    }
246
247    fn sync(&self, kind: SyncKind) -> std::io::Result<()> {
248        self.inner.sync(kind)?;
249        match kind {
250            SyncKind::Barrier => &self.counters.handle_barrier_sync_calls,
251            SyncKind::Full => &self.counters.handle_full_sync_calls,
252        }
253        .fetch_add(1, Ordering::Relaxed);
254        Ok(())
255    }
256}
257
258impl<V> CountingVfs<V> {
259    /// Wraps a filesystem with zeroed counters.
260    #[must_use]
261    pub fn new(inner: V) -> Self {
262        Self {
263            inner,
264            counters: Arc::new(CountingVfsCounters::default()),
265            read_path_filter: None,
266        }
267    }
268
269    /// Counts successful reads of one exact path separately from global reads.
270    #[must_use]
271    pub fn with_read_path_filter(mut self, path: impl Into<PathBuf>) -> Self {
272        self.read_path_filter = Some(path.into());
273        self
274    }
275
276    /// Returns the wrapped filesystem.
277    #[must_use]
278    pub const fn inner(&self) -> &V {
279        &self.inner
280    }
281
282    /// Runs a test probe with mmap-backed segment payload accounting enabled.
283    #[doc(hidden)]
284    pub fn audit_segment_data_reads<T>(&self, operation: impl FnOnce() -> T) -> T {
285        let _audit = crate::segment::reader::install_data_read_audit(Some(Arc::clone(
286            &self.counters.segment_bytes_read,
287        )));
288        operation()
289    }
290
291    /// Returns observed open calls.
292    #[must_use]
293    pub fn open_calls(&self) -> u64 {
294        self.counters.open_calls.load(Ordering::Relaxed)
295    }
296
297    /// Returns observed mapping-file open calls.
298    #[must_use]
299    pub fn open_for_map_calls(&self) -> u64 {
300        self.counters.open_for_map_calls.load(Ordering::Relaxed)
301    }
302
303    /// Returns observed read calls.
304    #[must_use]
305    pub fn read_calls(&self) -> u64 {
306        self.counters.read_calls.load(Ordering::Relaxed)
307    }
308
309    /// Returns successful reads matching the configured exact path.
310    #[must_use]
311    pub fn filtered_read_calls(&self) -> u64 {
312        self.counters.filtered_read_calls.load(Ordering::Relaxed)
313    }
314
315    /// Returns exact bytes returned by reads.
316    #[must_use]
317    pub fn read_bytes(&self) -> u64 {
318        self.counters.read_bytes.load(Ordering::Relaxed)
319    }
320
321    /// Returns exact bytes read from immutable segment paths.
322    #[must_use]
323    pub fn segment_bytes_read(&self) -> u64 {
324        self.counters.segment_bytes_read.load(Ordering::Relaxed)
325    }
326
327    /// Returns observed whole-file write calls.
328    #[must_use]
329    pub fn write_calls(&self) -> u64 {
330        self.counters.write_calls.load(Ordering::Relaxed)
331    }
332
333    /// Returns exact bytes supplied to successful whole-file writes.
334    #[must_use]
335    pub fn bytes_written(&self) -> u64 {
336        self.counters.bytes_written.load(Ordering::Relaxed)
337    }
338
339    /// Returns observed successful handle append calls.
340    #[must_use]
341    pub fn append_calls(&self) -> u64 {
342        self.counters.append_calls.load(Ordering::Relaxed)
343    }
344
345    /// Returns exact bytes supplied to successful handle appends.
346    #[must_use]
347    pub fn bytes_appended(&self) -> u64 {
348        self.counters.bytes_appended.load(Ordering::Relaxed)
349    }
350
351    /// Returns observed successful rename calls.
352    #[must_use]
353    pub fn rename_calls(&self) -> u64 {
354        self.counters.rename_calls.load(Ordering::Relaxed)
355    }
356
357    /// Returns observed successful delete calls.
358    #[must_use]
359    pub fn delete_calls(&self) -> u64 {
360        self.counters.delete_calls.load(Ordering::Relaxed)
361    }
362
363    /// Returns path-based barrier synchronization calls.
364    #[must_use]
365    pub fn barrier_sync_calls(&self) -> u64 {
366        self.counters.barrier_sync_calls.load(Ordering::Relaxed)
367    }
368
369    /// Returns path-based full synchronization calls.
370    #[must_use]
371    pub fn full_sync_calls(&self) -> u64 {
372        self.counters.full_sync_calls.load(Ordering::Relaxed)
373    }
374
375    /// Returns open-handle barrier synchronization calls.
376    #[must_use]
377    pub fn handle_barrier_sync_calls(&self) -> u64 {
378        self.counters
379            .handle_barrier_sync_calls
380            .load(Ordering::Relaxed)
381    }
382
383    /// Returns open-handle full synchronization calls.
384    #[must_use]
385    pub fn handle_full_sync_calls(&self) -> u64 {
386        self.counters.handle_full_sync_calls.load(Ordering::Relaxed)
387    }
388
389    /// Resets all counters without changing the wrapped filesystem.
390    pub fn reset(&self) {
391        self.counters.open_calls.store(0, Ordering::Relaxed);
392        self.counters.open_for_map_calls.store(0, Ordering::Relaxed);
393        self.counters.read_calls.store(0, Ordering::Relaxed);
394        self.counters
395            .filtered_read_calls
396            .store(0, Ordering::Relaxed);
397        self.counters.read_bytes.store(0, Ordering::Relaxed);
398        self.counters.segment_bytes_read.store(0, Ordering::Relaxed);
399        self.counters.write_calls.store(0, Ordering::Relaxed);
400        self.counters.bytes_written.store(0, Ordering::Relaxed);
401        self.counters.append_calls.store(0, Ordering::Relaxed);
402        self.counters.bytes_appended.store(0, Ordering::Relaxed);
403        self.counters.rename_calls.store(0, Ordering::Relaxed);
404        self.counters.delete_calls.store(0, Ordering::Relaxed);
405        self.counters.barrier_sync_calls.store(0, Ordering::Relaxed);
406        self.counters.full_sync_calls.store(0, Ordering::Relaxed);
407        self.counters
408            .handle_barrier_sync_calls
409            .store(0, Ordering::Relaxed);
410        self.counters
411            .handle_full_sync_calls
412            .store(0, Ordering::Relaxed);
413    }
414}
415
416impl<V: Vfs> Vfs for CountingVfs<V> {
417    fn segment_data_read_counter(&self) -> Option<Arc<AtomicU64>> {
418        Some(Arc::clone(&self.counters.segment_bytes_read))
419    }
420
421    fn ensure_directory(&self, path: &Path, create: bool) -> std::io::Result<bool> {
422        self.inner.ensure_directory(path, create)
423    }
424
425    fn open(&self, path: &Path) -> std::io::Result<u64> {
426        self.counters.open_calls.fetch_add(1, Ordering::Relaxed);
427        self.inner.open(path)
428    }
429
430    fn open_for_map(&self, path: &Path) -> std::io::Result<File> {
431        self.counters
432            .open_for_map_calls
433            .fetch_add(1, Ordering::Relaxed);
434        self.inner.open_for_map(path)
435    }
436
437    fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
438        let bytes = self.inner.read(path)?;
439        self.counters.read_calls.fetch_add(1, Ordering::Relaxed);
440        if self.read_path_filter.as_deref() == Some(path) {
441            self.counters
442                .filtered_read_calls
443                .fetch_add(1, Ordering::Relaxed);
444        }
445        self.counters
446            .read_bytes
447            .fetch_add(bytes.len() as u64, Ordering::Relaxed);
448        if is_segment_path(path) {
449            self.counters
450                .segment_bytes_read
451                .fetch_add(bytes.len() as u64, Ordering::Relaxed);
452        }
453        Ok(bytes)
454    }
455
456    fn read_range(&self, path: &Path, offset: u64, length: usize) -> std::io::Result<Vec<u8>> {
457        let bytes = self.inner.read_range(path, offset, length)?;
458        self.counters.read_calls.fetch_add(1, Ordering::Relaxed);
459        if self.read_path_filter.as_deref() == Some(path) {
460            self.counters
461                .filtered_read_calls
462                .fetch_add(1, Ordering::Relaxed);
463        }
464        self.counters
465            .read_bytes
466            .fetch_add(bytes.len() as u64, Ordering::Relaxed);
467        if is_segment_path(path) {
468            self.counters
469                .segment_bytes_read
470                .fetch_add(bytes.len() as u64, Ordering::Relaxed);
471        }
472        Ok(bytes)
473    }
474
475    fn write(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
476        self.inner.write(path, bytes)?;
477        self.counters.write_calls.fetch_add(1, Ordering::Relaxed);
478        self.counters
479            .bytes_written
480            .fetch_add(bytes.len() as u64, Ordering::Relaxed);
481        Ok(())
482    }
483
484    fn open_append(&self, path: &Path) -> std::io::Result<Box<dyn VfsFile>> {
485        Ok(Box::new(CountingVfsFile {
486            inner: self.inner.open_append(path)?,
487            counters: Arc::clone(&self.counters),
488        }))
489    }
490
491    fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()> {
492        self.inner.rename(from, to)?;
493        self.counters.rename_calls.fetch_add(1, Ordering::Relaxed);
494        Ok(())
495    }
496
497    fn sync(&self, path: &Path, kind: SyncKind) -> std::io::Result<()> {
498        self.inner.sync(path, kind)?;
499        match kind {
500            SyncKind::Barrier => &self.counters.barrier_sync_calls,
501            SyncKind::Full => &self.counters.full_sync_calls,
502        }
503        .fetch_add(1, Ordering::Relaxed);
504        Ok(())
505    }
506
507    fn list(&self, directory: &Path) -> std::io::Result<Vec<PathBuf>> {
508        self.inner.list(directory)
509    }
510
511    fn delete(&self, path: &Path) -> std::io::Result<()> {
512        self.inner.delete(path)?;
513        self.counters.delete_calls.fetch_add(1, Ordering::Relaxed);
514        Ok(())
515    }
516}
517
518fn is_segment_path(path: &Path) -> bool {
519    path.file_name()
520        .and_then(|name| name.to_str())
521        .is_some_and(|name| name.starts_with("segment-") && name.ends_with(".zseg"))
522}
523
524#[cfg(any(target_os = "macos", target_os = "ios"))]
525fn sync_file(file: &File, kind: SyncKind) -> std::io::Result<()> {
526    let result = match kind {
527        SyncKind::Barrier => crate::sys::darwin::barrier_fsync(file.as_raw_fd()),
528        SyncKind::Full => crate::sys::darwin::full_fsync(file.as_raw_fd()),
529    };
530    result.map_err(std::io::Error::other)
531}
532
533#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]
534fn sync_file(file: &File, kind: SyncKind) -> std::io::Result<()> {
535    let result = unsafe {
536        // SAFETY: both calls accept an owned live descriptor and report failures through errno.
537        match kind {
538            SyncKind::Barrier => libc::fdatasync(file.as_raw_fd()),
539            SyncKind::Full => libc::fsync(file.as_raw_fd()),
540        }
541    };
542    if result == -1 {
543        Err(std::io::Error::last_os_error())
544    } else {
545        Ok(())
546    }
547}
548
549#[cfg(not(unix))]
550fn sync_file(_: &File, kind: SyncKind) -> std::io::Result<()> {
551    Err(std::io::Error::new(
552        std::io::ErrorKind::Unsupported,
553        format!("{kind:?} synchronization requires a supported file-descriptor platform"),
554    ))
555}
556
557#[cfg(test)]
558mod tests {
559    use std::path::Path;
560
561    use super::crash::MemoryVfs;
562    use super::{CountingVfs, SyncKind, Vfs};
563
564    #[test]
565    fn crash_support_api_gate_is_identical_in_debug_and_release() {
566        let source = include_str!("mod.rs");
567        let gate = source
568            .lines()
569            .zip(source.lines().skip(1))
570            .find_map(|(line, next)| (next == "pub mod crash;").then_some(line));
571        assert_eq!(gate, Some("#[cfg(any(test, feature = \"test-support\"))]"));
572    }
573
574    #[test]
575    #[allow(clippy::expect_used)]
576    fn counting_vfs_counts_every_mutation_and_sync_kind_exactly() {
577        let inner = MemoryVfs::new();
578        inner
579            .insert("/source", b"abc".to_vec())
580            .expect("seed source");
581        let counting = CountingVfs::new(inner);
582
583        assert_eq!(counting.open(Path::new("/source")).expect("open"), 3);
584        assert_eq!(counting.read(Path::new("/source")).expect("read"), b"abc");
585        assert_eq!(
586            counting
587                .read_range(Path::new("/source"), 1, 2)
588                .expect("range"),
589            b"bc"
590        );
591        counting.write(Path::new("/write"), b"1234").expect("write");
592        let mut handle = counting
593            .open_append(Path::new("/append"))
594            .expect("open append");
595        handle.append(b"12").expect("first append");
596        handle.append(b"345").expect("second append");
597        handle.sync(SyncKind::Barrier).expect("handle barrier");
598        handle.sync(SyncKind::Full).expect("handle full");
599        counting
600            .sync(Path::new("/write"), SyncKind::Barrier)
601            .expect("path barrier");
602        counting
603            .sync(Path::new("/write"), SyncKind::Full)
604            .expect("path full");
605        counting
606            .rename(Path::new("/write"), Path::new("/renamed"))
607            .expect("rename");
608        counting.delete(Path::new("/renamed")).expect("delete");
609
610        assert_eq!(
611            [
612                counting.open_calls(),
613                counting.read_calls(),
614                counting.read_bytes(),
615                counting.write_calls(),
616                counting.bytes_written(),
617                counting.append_calls(),
618                counting.bytes_appended(),
619                counting.rename_calls(),
620                counting.delete_calls(),
621                counting.barrier_sync_calls(),
622                counting.full_sync_calls(),
623                counting.handle_barrier_sync_calls(),
624                counting.handle_full_sync_calls(),
625            ],
626            [1, 2, 5, 1, 4, 2, 5, 1, 1, 1, 1, 1, 1],
627            "CountingVfs must expose every exact VFS and open-handle cost"
628        );
629        counting.reset();
630        assert_eq!(
631            [
632                counting.open_calls(),
633                counting.read_calls(),
634                counting.read_bytes(),
635                counting.write_calls(),
636                counting.bytes_written(),
637                counting.append_calls(),
638                counting.bytes_appended(),
639                counting.rename_calls(),
640                counting.delete_calls(),
641                counting.barrier_sync_calls(),
642                counting.full_sync_calls(),
643                counting.handle_barrier_sync_calls(),
644                counting.handle_full_sync_calls(),
645            ],
646            [0; 13],
647            "reset must zero decorator and shared-handle counters"
648        );
649    }
650}