Summary
Duration-typed config fields cannot be set from the string config map. Any attempt to set one (via Operator::via_iter, from_uri, or a Configurator built from a key/value map) fails with invalid type: string, expected struct Duration. This is a core issue and is binding-agnostic — every binding routes config through the same ConfigDeserializer.
Root cause
Config fields are declared as plain std::time::Duration with the default serde derive and no string-parsing attribute:
// core/services/redis/src/config.rs:62
pub default_ttl: Option<Duration>,
serde's default Duration impl only deserializes from a { secs, nanos } struct. But ConfigDeserializer (core/core/src/raw/serde_util.rs) forwards struct to deserialize_any, which hands back the raw string. So a string value never matches the expected struct and deserialization fails.
There is no #[serde(with = "humantime_serde")] (or equivalent), and humantime is not a dependency of these crates.
Affected fields
All Duration-typed config fields:
core/services/redis/src/config.rs:62 — default_ttl
core/services/memcached/src/config.rs:47 — default_ttl
core/services/cloudflare-kv/src/config.rs:37 — default_ttl
More broadly, any config field typed Duration hits this. Services that need a duration currently work around it with a plain integer (e.g. S3's assume_role_duration_seconds: Option<u32>), so Duration is effectively a trap for any future config field.
Steps to reproduce (core, Rust)
use std::collections::HashMap;
use opendal::Operator;
let mut opts = HashMap::new();
opts.insert("endpoint".into(), "tcp://127.0.0.1:6379".into());
opts.insert("db".into(), "0".into());
opts.insert("default_ttl".into(), "5s".into()); // any string fails
// Err: ConfigInvalid "failed to deserialize config",
// source: invalid type: string "5s", expected struct Duration
let _ = Operator::via_iter("redis", opts).unwrap();
The same failure occurs for "5", "100ms", "0", etc. Only omitting the field works. It reproduces identically from any binding (e.g. Python Operator("redis", default_ttl="5s") or Operator.from_config(RedisConfig(..., default_ttl="5s"))).
Suggested fix
Add a string deserializer for Duration config fields in core, e.g. #[serde(with = "humantime_serde")] (plus the humantime/humantime-serde dependency), so values like "5s", "100ms", "1m" parse. This fixes every binding at once. Bindings can then decide how to type the field (a humantime string, and optionally a native duration type).
Note: a bare number like "5" (no unit) is not accepted by humantime and should remain an error; only explicit-unit strings should be supported.
Summary
Duration-typed config fields cannot be set from the string config map. Any attempt to set one (viaOperator::via_iter,from_uri, or aConfiguratorbuilt from a key/value map) fails withinvalid type: string, expected struct Duration. This is acoreissue and is binding-agnostic — every binding routes config through the sameConfigDeserializer.Root cause
Config fields are declared as plain
std::time::Durationwith the default serde derive and no string-parsing attribute:serde's defaultDurationimpl only deserializes from a{ secs, nanos }struct. ButConfigDeserializer(core/core/src/raw/serde_util.rs) forwardsstructtodeserialize_any, which hands back the raw string. So a string value never matches the expected struct and deserialization fails.There is no
#[serde(with = "humantime_serde")](or equivalent), andhumantimeis not a dependency of these crates.Affected fields
All
Duration-typed config fields:core/services/redis/src/config.rs:62—default_ttlcore/services/memcached/src/config.rs:47—default_ttlcore/services/cloudflare-kv/src/config.rs:37—default_ttlMore broadly, any config field typed
Durationhits this. Services that need a duration currently work around it with a plain integer (e.g. S3'sassume_role_duration_seconds: Option<u32>), soDurationis effectively a trap for any future config field.Steps to reproduce (core, Rust)
The same failure occurs for
"5","100ms","0", etc. Only omitting the field works. It reproduces identically from any binding (e.g. PythonOperator("redis", default_ttl="5s")orOperator.from_config(RedisConfig(..., default_ttl="5s"))).Suggested fix
Add a string deserializer for
Durationconfig fields in core, e.g.#[serde(with = "humantime_serde")](plus thehumantime/humantime-serdedependency), so values like"5s","100ms","1m"parse. This fixes every binding at once. Bindings can then decide how to type the field (a humantime string, and optionally a native duration type).Note: a bare number like
"5"(no unit) is not accepted by humantime and should remain an error; only explicit-unit strings should be supported.