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}