Skip to main content

zarrs/
config.rs

1//! `zarrs` global configuration options.
2//!
3//! See [`Config`] for the list of options.
4
5use std::sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
6
7use serde::{Deserialize, Serialize};
8
9use crate::array::ArrayMetadataOptions;
10use crate::group::GroupMetadataOptions;
11use zarrs_codec::{CodecMetadataOptions, CodecOptions};
12
13/// Global configuration options for the `zarrs` crate.
14///
15/// <div class="warning">
16/// Serialisation/deserialisation of the config does NOT currently include the extension alias maps.
17/// This will be addressed in a future breaking release.
18/// </div>
19///
20/// Retrieve the global [`Config`] with [`global_config`] and modify it with [`global_config_mut`].
21///
22/// ## Codec / Chunk Options
23///
24/// ### Validate Checksums
25///  > default: [`true`]
26///
27/// [`CodecOptions::validate_checksums()`] defaults to [`Config::validate_checksums()`].
28///
29/// If validate checksums is enabled, checksum codecs (e.g. `crc32c`, `fletcher32`) will validate that encoded data matches stored checksums, otherwise validation is skipped.
30/// Note that regardless of this configuration option, checksum codecs may skip validation when partial decoding.
31///
32/// ### Store Empty Chunks
33///  > default: [`false`]
34///
35/// [`CodecOptions::store_empty_chunks()`] defaults to [`Config::store_empty_chunks()`].
36///
37/// If `false`, empty chunks (where all elements match the fill value) will not be stored.
38/// This incurs a computational overhead as each element must be tested for equality to the fill value before a chunk is encoded.
39/// If `true`, the aforementioned test is skipped and empty chunks will be stored.
40/// Note that empty chunks must still be stored explicitly (e.g. with [`Array::store_chunk`](crate::array::Array::store_chunk)).
41///
42/// ### Codec Concurrent Target
43/// > default: [`std::thread::available_parallelism`]`()`
44///
45/// [`CodecOptions::concurrent_target()`] defaults to [`Config::codec_concurrent_target()`].
46///
47/// The default number of concurrent operations to target for codec encoding and decoding.
48/// Limiting concurrent operations is needed to reduce memory usage and improve performance.
49/// Concurrency is unconstrained if the concurrent target if set to zero.
50///
51/// Note that the default codec concurrent target can be overridden for any encode/decode operation.
52/// This is performed automatically for many array operations (see the [chunk concurrent minimum](#chunk-concurrent-minimum) option).
53///
54/// ### Chunk Concurrent Minimum
55/// > default: `4`
56///
57/// Array operations involving multiple chunks can tune the chunk and codec concurrency to improve performance/reduce memory usage.
58/// This option sets the preferred minimum chunk concurrency.
59/// The concurrency of internal codecs is adjusted to accomodate for the chunk concurrency in accordance with the concurrent target set in the [`CodecOptions`] parameter of an encode or decode method.
60///
61/// ### Experimental Partial Encoding
62/// > default: [`false`]
63///
64/// If `true`, [`Array::store_chunk_subset`](crate::array::Array::store_chunk_subset) and [`Array::store_array_subset`](crate::array::Array::store_array_subset) and variants can use partial encoding.
65/// This is relevant when using the sharding codec, as it enables subchunks to be written without reading and writing entire shards.
66///
67/// This is an experimental feature for now until it has more comprehensively tested and support is added in the async API.
68///
69/// ## Metadata Options
70///
71/// ### Codec Store Metadata If Encode Only
72/// > default: [`true`]
73///
74/// Some codecs perform potentially irreversible transformations during encoding that decoders do not need to be aware of.
75/// If this option is `false`, codecs with this behaviour will not write their metadata.
76/// This enables arrays to be consumed by other Zarr V3 implementations that do not support the codec.
77/// Currently, this options only affects the `bitround` codec.
78///
79/// ### Metadata Convert Version
80/// > default: [`MetadataConvertVersion::Default`] (keep existing version)
81///
82/// Determines the Zarr version of metadata created with [`Array::metadata_opt`](crate::array::Array::metadata_opt) and [`Group::metadata_opt`](crate::group::Group::metadata_opt).
83/// These methods are used internally by the `store_metadata` and `store_metadata_opt` methods of [`crate::array::Array`] and [`crate::group::Group`].
84///
85/// ### Metadata Erase Version
86/// > default: [`MetadataEraseVersion::Default`] (erase existing version)
87///
88/// The default behaviour for the `erase_metadata` methods of [`crate::array::Array`] and [`crate::group::Group`].
89/// Determines whether to erase metadata of a specific Zarr version, the same version as the array/group was created with, or all known versions.
90///
91/// ### Include `zarrs` Metadata
92/// > default: [`true`]
93///
94/// [`ArrayMetadataOptions::include_zarrs_metadata`](crate::array::ArrayMetadataOptions::include_zarrs_metadata) defaults to [`Config::include_zarrs_metadata`].
95///
96/// If true, array metadata generated with [`Array::metadata_opt`](crate::array::Array::metadata_opt) (used internally by [`Array::store_metadata`](crate::array::Array::store_metadata)) includes the `zarrs` version and a link to its source code.
97/// For example:
98/// ```json
99/// "_zarrs": {
100///    "description": "This array was created with zarrs",
101///    "repository": "https://github.com/zarrs/zarrs",
102///    "version": "0.15.0"
103///  }
104/// ```
105///
106/// ### Convert Aliased Extension Names
107/// > default: [`false`]
108///
109/// If true, then aliased extension names will be replaced by the standard name if metadata is resaved.
110/// This sets the default for the association option of [`crate::array::ArrayMetadataOptions`].
111///
112/// ### Use Consolidated Metadata
113/// > default: [`UseConsolidatedMetadata::Auto`]
114///
115/// Controls whether [`crate::node::Node::open`], [`crate::hierarchy::Hierarchy::open`], and their async/`_opt` variants
116/// use the `consolidated_metadata` field of a Zarr V3 root group instead of listing children from storage.
117/// See [`UseConsolidatedMetadata`] for the available modes.
118///
119/// Consolidated metadata is a snapshot. If the hierarchy has been modified after the snapshot was written,
120/// the consolidated copy may be out of date. Set this to [`UseConsolidatedMetadata::Never`] to force re-discovery.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122#[allow(clippy::struct_excessive_bools)]
123pub struct Config {
124    validate_checksums: bool,
125    store_empty_chunks: bool,
126    codec_concurrent_target: usize,
127    chunk_concurrent_minimum: usize,
128    codec_store_metadata_if_encode_only: bool,
129    metadata_convert_version: MetadataConvertVersion,
130    metadata_erase_version: MetadataEraseVersion,
131    include_zarrs_metadata: bool,
132    experimental_partial_encoding: bool,
133    convert_aliased_extension_names: bool,
134    use_consolidated_metadata: UseConsolidatedMetadata,
135}
136
137#[allow(clippy::derivable_impls)]
138impl Default for Config {
139    fn default() -> Self {
140        Self {
141            validate_checksums: true,
142            store_empty_chunks: false,
143            codec_concurrent_target: rayon::current_num_threads(),
144            chunk_concurrent_minimum: 4,
145            codec_store_metadata_if_encode_only: true,
146            metadata_convert_version: MetadataConvertVersion::default(),
147            metadata_erase_version: MetadataEraseVersion::default(),
148            include_zarrs_metadata: true,
149            experimental_partial_encoding: false,
150            convert_aliased_extension_names: false,
151            use_consolidated_metadata: UseConsolidatedMetadata::default(),
152        }
153    }
154}
155
156impl Config {
157    /// Get the codec options.
158    #[must_use]
159    pub fn codec_options(&self) -> CodecOptions {
160        CodecOptions::default()
161            .with_validate_checksums(self.validate_checksums)
162            .with_store_empty_chunks(self.store_empty_chunks)
163            .with_concurrent_target(self.codec_concurrent_target)
164            .with_chunk_concurrent_minimum(self.chunk_concurrent_minimum)
165            .with_experimental_partial_encoding(self.experimental_partial_encoding)
166    }
167
168    /// Get the codec metadata options.
169    #[must_use]
170    pub fn codec_metadata_options(&self) -> CodecMetadataOptions {
171        CodecMetadataOptions::default()
172            .with_codec_store_metadata_if_encode_only(self.codec_store_metadata_if_encode_only)
173    }
174
175    /// Get the group metadata options.
176    #[must_use]
177    pub fn group_metadata_options(&self) -> crate::group::GroupMetadataOptions {
178        GroupMetadataOptions::default().with_metadata_convert_version(self.metadata_convert_version)
179    }
180
181    /// Get the array metadata options.
182    #[must_use]
183    pub fn array_metadata_options(&self) -> ArrayMetadataOptions {
184        ArrayMetadataOptions::default()
185            .with_codec_metadata_options(self.codec_metadata_options())
186            .with_metadata_convert_version(self.metadata_convert_version)
187            .with_include_zarrs_metadata(self.include_zarrs_metadata)
188            .with_convert_aliased_extension_names(self.convert_aliased_extension_names)
189    }
190
191    /// Get the [validate checksums](#validate-checksums) configuration.
192    #[must_use]
193    pub fn validate_checksums(&self) -> bool {
194        self.validate_checksums
195    }
196
197    /// Set the [validate checksums](#validate-checksums) configuration.
198    pub fn set_validate_checksums(&mut self, validate_checksums: bool) -> &mut Self {
199        self.validate_checksums = validate_checksums;
200        self
201    }
202
203    /// Get the [store empty chunks](#store-empty-chunks) configuration.
204    #[must_use]
205    pub fn store_empty_chunks(&self) -> bool {
206        self.store_empty_chunks
207    }
208
209    /// Set the [store empty chunks](#store-empty-chunks) configuration.
210    pub fn set_store_empty_chunks(&mut self, store_empty_chunks: bool) -> &mut Self {
211        self.store_empty_chunks = store_empty_chunks;
212        self
213    }
214
215    /// Get the [codec concurrent target](#codec-concurrent-target) configuration.
216    #[must_use]
217    pub fn codec_concurrent_target(&self) -> usize {
218        self.codec_concurrent_target
219    }
220
221    /// Set the [codec concurrent target](#codec-concurrent-target) configuration.
222    pub fn set_codec_concurrent_target(&mut self, concurrent_target: usize) -> &mut Self {
223        self.codec_concurrent_target = concurrent_target;
224        self
225    }
226
227    /// Get the [chunk concurrent minimum](#chunk-concurrent-minimum) configuration.
228    #[must_use]
229    pub fn chunk_concurrent_minimum(&self) -> usize {
230        self.chunk_concurrent_minimum
231    }
232
233    /// Set the [chunk concurrent minimum](#chunk-concurrent-minimum) configuration.
234    pub fn set_chunk_concurrent_minimum(&mut self, concurrent_minimum: usize) -> &mut Self {
235        self.chunk_concurrent_minimum = concurrent_minimum;
236        self
237    }
238
239    /// Get the [codec store metadata if encode only](#codec-store-metadata-if-encode-only) configuration.
240    #[must_use]
241    pub fn codec_store_metadata_if_encode_only(&self) -> bool {
242        self.codec_store_metadata_if_encode_only
243    }
244
245    /// Set the [codec store metadata if encode only](#codec-store-metadata-if-encode-only) configuration.
246    pub fn set_codec_store_metadata_if_encode_only(&mut self, enabled: bool) -> &mut Self {
247        self.codec_store_metadata_if_encode_only = enabled;
248        self
249    }
250
251    /// Get the [metadata convert version](#metadata-convert-version) configuration.
252    #[must_use]
253    pub fn metadata_convert_version(&self) -> MetadataConvertVersion {
254        self.metadata_convert_version
255    }
256
257    /// Set the [metadata convert version](#metadata-convert-version) configuration.
258    pub fn set_metadata_convert_version(&mut self, version: MetadataConvertVersion) -> &mut Self {
259        self.metadata_convert_version = version;
260        self
261    }
262
263    /// Get the [metadata erase version](#metadata-erase-version) configuration.
264    #[must_use]
265    pub fn metadata_erase_version(&self) -> MetadataEraseVersion {
266        self.metadata_erase_version
267    }
268
269    /// Set the [metadata erase version](#metadata-erase-version) configuration.
270    pub fn set_metadata_erase_version(&mut self, version: MetadataEraseVersion) -> &mut Self {
271        self.metadata_erase_version = version;
272        self
273    }
274
275    /// Get the [include zarrs metadata](#include-zarrs-metadata) configuration.
276    #[must_use]
277    pub fn include_zarrs_metadata(&self) -> bool {
278        self.include_zarrs_metadata
279    }
280
281    /// Set the [include zarrs metadata](#include-zarrs-metadata) configuration.
282    pub fn set_include_zarrs_metadata(&mut self, include_zarrs_metadata: bool) -> &mut Self {
283        self.include_zarrs_metadata = include_zarrs_metadata;
284        self
285    }
286
287    /// Get the [experimental partial encoding](#experimental-partial-encoding) configuration.
288    #[must_use]
289    pub fn experimental_partial_encoding(&self) -> bool {
290        self.experimental_partial_encoding
291    }
292
293    /// Set the [experimental partial encoding](#experimental-partial-encoding) configuration.
294    pub fn set_experimental_partial_encoding(
295        &mut self,
296        experimental_partial_encoding: bool,
297    ) -> &mut Self {
298        self.experimental_partial_encoding = experimental_partial_encoding;
299        self
300    }
301
302    /// Set the [convert aliased extension names](#convert-aliased-extension-names) configuration.
303    #[must_use]
304    pub fn convert_aliased_extension_names(&self) -> bool {
305        self.convert_aliased_extension_names
306    }
307
308    /// Set the [convert aliased extension names](#convert-aliased-extension-names) configuration.
309    pub fn set_convert_aliased_extension_names(
310        &mut self,
311        convert_aliased_extension_names: bool,
312    ) -> &mut Self {
313        self.convert_aliased_extension_names = convert_aliased_extension_names;
314        self
315    }
316
317    /// Get the [use consolidated metadata](#use-consolidated-metadata) configuration.
318    #[must_use]
319    pub fn use_consolidated_metadata(&self) -> UseConsolidatedMetadata {
320        self.use_consolidated_metadata
321    }
322
323    /// Set the [use consolidated metadata](#use-consolidated-metadata) configuration.
324    pub fn set_use_consolidated_metadata(
325        &mut self,
326        use_consolidated_metadata: UseConsolidatedMetadata,
327    ) -> &mut Self {
328        self.use_consolidated_metadata = use_consolidated_metadata;
329        self
330    }
331}
332
333static CONFIG: LazyLock<RwLock<Config>> = LazyLock::new(|| RwLock::new(Config::default()));
334
335/// Returns a reference to the global `zarrs` configuration.
336///
337/// # Panics
338/// This function panics if the underlying lock has been poisoned and might panic if the global config is already held by the current thread.
339pub fn global_config() -> RwLockReadGuard<'static, Config> {
340    CONFIG.read().unwrap()
341}
342
343/// Returns a mutable reference to the global `zarrs` configuration.
344///
345/// # Panics
346/// This function panics if the underlying lock has been poisoned and might panic if the global config is already held by the current thread.
347pub fn global_config_mut() -> RwLockWriteGuard<'static, Config> {
348    CONFIG.write().unwrap()
349}
350
351/// The metadata version to retrieve.
352///
353/// Used with [`crate::array::Array::open_opt`], [`crate::group::Group::open_opt`].
354pub enum MetadataRetrieveVersion {
355    /// Either Zarr V3 or V2. V3 is prioritised over V2 if found.
356    Default,
357    /// Zarr V3.
358    V3,
359    /// Zarr V2.
360    V2,
361}
362
363/// Version options for [`Array::store_metadata`](crate::array::Array::store_metadata) and [`Group::store_metadata`](crate::group::Group::store_metadata), and their async variants.
364#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
365pub enum MetadataConvertVersion {
366    /// Write the same version as the input metadata.
367    #[default]
368    Default,
369    /// Write Zarr V3 metadata. Zarr V2 metadata will not be automatically removed if it exists.
370    V3,
371}
372
373/// Controls whether `consolidated_metadata` (if present in a Zarr V3 root group) is used to populate
374/// child nodes when opening a [`Node`](crate::node::Node) or [`Hierarchy`](crate::hierarchy::Hierarchy).
375///
376/// Consolidated metadata is a snapshot of the hierarchy embedded in the root group. Using it
377/// avoids `list_dir` calls and per-node metadata reads, but it may be stale if the hierarchy was
378/// modified after the snapshot was written.
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
380pub enum UseConsolidatedMetadata {
381    /// Use consolidated metadata if it is present on the root group; otherwise fall back to listing storage.
382    #[default]
383    Auto,
384    /// Require consolidated metadata to be present on the root group. If absent, opening fails.
385    Must,
386    /// Never use consolidated metadata, even if it is present. Always re-discover children from storage.
387    Never,
388}
389
390/// Version options for [`Array::erase_metadata`](crate::array::Array::erase_metadata) and [`Group::erase_metadata`](crate::group::Group::erase_metadata), and their async variants.
391#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
392pub enum MetadataEraseVersion {
393    /// Erase the same version as the input metadata.
394    #[default]
395    Default,
396    /// Erase all metadata.
397    All,
398    /// Erase Zarr V3 metadata.
399    V3,
400    /// Erase Zarr V2 metadata.
401    V2,
402}
403
404#[cfg(test)]
405mod tests {
406    use serial_test::serial;
407
408    use super::*;
409
410    #[ignore]
411    #[test]
412    #[serial]
413    fn config_validate_checksums() {
414        *global_config_mut() = Config::default();
415        assert!(global_config().validate_checksums());
416        global_config_mut().set_validate_checksums(false);
417        assert!(!global_config().validate_checksums());
418        global_config_mut().set_validate_checksums(true);
419        *global_config_mut() = Config::default();
420    }
421
422    #[ignore]
423    #[test]
424    #[serial]
425    fn config_serialize_deserialize_update() {
426        *global_config_mut() = Config::default();
427
428        global_config_mut().set_validate_checksums(false);
429        let serialized = serde_json::to_string(&*global_config()).unwrap();
430
431        global_config_mut().set_validate_checksums(true);
432        assert!(global_config().validate_checksums());
433
434        let restored_config: Config = serde_json::from_str(&serialized).unwrap();
435        assert!(!restored_config.validate_checksums());
436
437        *global_config_mut() = restored_config;
438        assert!(!global_config().validate_checksums());
439
440        *global_config_mut() = Config::default();
441    }
442}