Skip to content

Commit c481a6d

Browse files
authored
RFC-6707: Capability Override Layer (#6707)
* feat: Add Capability Override Layer Signed-off-by: Xuanwo <github@xuanwo.io> * Assign number Signed-off-by: Xuanwo <github@xuanwo.io> --------- Signed-off-by: Xuanwo <github@xuanwo.io>
1 parent 9746efc commit c481a6d

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
- Proposal Name: capability_override_layer
2+
- Start Date: 2025-10-20
3+
- RFC PR: [apache/opendal#6707](https://github.com/apache/opendal/pull/6707)
4+
- Tracking Issue: [apache/opendal#6708](https://github.com/apache/opendal/issues/6708)
5+
6+
# Summary
7+
8+
Introduce a tiny `CapabilityOverrideLayer` that lets integrators adjust an accessor's *full* capability by supplying a closure that mutates the existing [`Capability`](../../types/capability.rs) instance. The layer supplements the default pipeline and replaces the service-specific configuration toggles such as `delete_max_size`, `disable_stat_with_override`, or `write_with_if_match`. We will deprecate those fields and direct users (and our own behavior harness) to the new layer instead.
9+
10+
# Motivation
11+
12+
Today we scatter capability tweaks across several service configs. S3 alone exposes `delete_max_size`, `disable_stat_with_override`, and `disable_write_with_if_match` on `S3Config` (see `core/src/services/s3/config.rs:184-208`). OSS and Azblob mirror the same knobs in their configs (`core/src/services/oss/config.rs:76-78`, `core/src/services/azblob/config.rs:78`). These options exist only to downgrade capabilities when a compatible service lacks full S3 semantics. They dilute the purpose of configuration, create redundancy between services, and complicate documentation.
13+
14+
Our behavior tests already bypass those configs and mutate the capability directly via `info().update_full_capability(...)` to emulate lower limits (`core/tests/behavior/async_delete.rs:288-329`). That ad-hoc pattern hints that the real need is an explicit hook to patch capabilities after construction, not extra config fields.
15+
16+
We want a single, explicit, and reusable spot for such adjustments that stays outside service implementations and keeps native capabilities untouched.
17+
18+
# Guide-level explanation
19+
20+
With this RFC, builders stay focused on connection details while users layer capability overrides explicitly:
21+
22+
```rust
23+
use opendal::layers::CapabilityOverrideLayer;
24+
use opendal::services::S3;
25+
26+
let op = Operator::new(S3::default().bucket("demo"))?
27+
.layer(CapabilityOverrideLayer::new(|mut cap| {
28+
// Cloudflare R2 rejects large batch deletes and stat overrides
29+
cap.delete_max_size = Some(700);
30+
cap.stat_with_override_cache_control = false;
31+
cap.stat_with_override_content_disposition = false;
32+
cap.stat_with_override_content_type = false;
33+
cap.write_with_if_match = false;
34+
cap
35+
}))
36+
.finish();
37+
```
38+
39+
The closure receives the current `Capability` (already filled by the backend) and must return the adjusted value. Only the accessor's *full* capability is patched; native capability remains intact so completion layers can still infer what the backend supports natively (`core/src/layers/complete.rs:23-62`). Downstream layers—`CorrectnessCheckLayer`, retry, delete helpers—observe the overridden full capability (`core/src/layers/correctness_check.rs:23-115`), so they continue to guard unsupported options.
40+
41+
Behavior tests and diagnostic tools can read environment variables and pass a closure to the same layer instead of mutating capability structs manually.
42+
43+
# Reference-level explanation
44+
45+
## Layer structure
46+
47+
The implementation introduces:
48+
49+
```rust
50+
pub struct CapabilityOverrideLayer {
51+
apply: Arc<dyn Fn(Capability) -> Capability + Send + Sync>,
52+
}
53+
```
54+
55+
- `CapabilityOverrideLayer::new` accepts any closure that maps the current capability to the desired full capability.
56+
- `Layer::layer` clones the closure, reads `info.full_capability()`, applies the function, and writes the result back via `info.update_full_capability`. Native capability is untouched.
57+
- The layer returns the original accessor unwrapped, so it composes with existing layers transparently.
58+
59+
## Layer placement
60+
61+
`Operator::new` currently installs `ErrorContextLayer → CompleteLayer → CorrectnessCheckLayer`. We insert `CapabilityOverrideLayer` between error context and completion:
62+
63+
```
64+
ErrorContextLayer
65+
→ CapabilityOverrideLayer
66+
→ CompleteLayer
67+
→ CorrectnessCheckLayer
68+
```
69+
70+
Positioning before `CompleteLayer` let us patch the full capability before completion synthesizes derived operations (e.g., auto-creating directories). Completion still reads the native capability to decide what to fill in, so we do not break its invariants.
71+
72+
## API exposure
73+
74+
- Rust: expose `CapabilityOverrideLayer` within `opendal::layers`.
75+
- Other language bindings wrap the same primitive with a thin helper that accepts a closure-equivalent (e.g., a lambda or struct of booleans) and forwards to the Rust layer through FFI.
76+
- No new `OperatorBuilder` method is required; users call `.layer(...)` explicitly.
77+
78+
## Deprecations and migration
79+
80+
The following config fields become deprecated aliases that internally forward to `CapabilityOverrideLayer` until they are removed:
81+
82+
- `S3Config::{batch_max_operations, delete_max_size, disable_stat_with_override, disable_write_with_if_match}` (`core/src/services/s3/config.rs:184-208`)
83+
- `OssConfig::{batch_max_operations, delete_max_size}` (`core/src/services/oss/config.rs:76-78`)
84+
- `AzblobConfig::batch_max_operations` (`core/src/services/azblob/config.rs:78`)
85+
86+
Migration path:
87+
88+
1. Emit a warning when these fields are set, pointing to the new layer-based approach.
89+
2. In a compatibility shim, convert the old configuration into a `CapabilityOverrideLayer` injected automatically (only while the fields exist).
90+
3. Behavior tests stop calling `info().update_full_capability` manually and instead always register the layer, keeping env-driven overrides centralised.
91+
4. Remove the fields in a later release once downstream bindings migrate.
92+
93+
# Drawbacks
94+
95+
- Users must write code (a closure) rather than toggling a config string. For trivial overrides this is more verbose, especially in non-Rust bindings.
96+
- The approach assumes callers are comfortable with the capability semantics; there is no compile-time guarantee that they only *downgrade* capabilities.
97+
98+
# Rationale and alternatives
99+
100+
Alternatives considered:
101+
102+
1. **Retain per-service config toggles**. This keeps the status quo but perpetuates redundancy and drifts farther from capability-centric design (`core/src/docs/rfcs/0409_accessor_capabilities.md`, `core/src/docs/rfcs/2852_native_capability.md`).
103+
2. **Parse override values during config deserialization.** That hides the layer but still demands new schema for each field and duplicates logic across services.
104+
3. **Automatic detection at runtime.** Allowing writes to fail before downgrading capabilities complicates error handling and caching, and is harder to reason about.
105+
106+
The chosen design centralises capability edits, keeps services agnostic, and matches the lightweight infrastructure we already use inside tests.
107+
108+
Not acting would leave us with an ever-growing list of service-specific toggles and scattered override logic, making future capability changes harder.
109+
110+
# Prior art
111+
112+
- Our own RFCs on capabilities already advocate for a layered approach (`core/src/docs/rfcs/0409_accessor_capabilities.md`, `core/src/docs/rfcs/2852_native_capability.md`). This proposal follows that direction by giving layers first-class control.
113+
- The behavior test harness demonstrates manual capability mutation (`core/tests/behavior/async_delete.rs:288-329`), validating that adjusting `full_capability` is viable today; we generalise it into a formal layer.
114+
115+
# Unresolved questions
116+
117+
- How should non-Rust bindings expose the closure-based API ergonomically? Do we offer predefined helpers for common toggles?
118+
- What is the precise deprecation schedule for each config field, and how noisy should the warnings be?
119+
- Do we need guardrails to prevent capability *upgrades* that claim support the backend truly lacks?
120+
121+
# Future possibilities
122+
123+
- Provide convenience constructors such as `CapabilityOverrideLayer::disable_stat_overrides()` or `::limit_delete(max)` to ease usage from other languages.
124+
- Offer an environment-driven helper (`CapabilityOverrideLayer::from_env`) for CLI tools and tests.
125+
- Build curated profiles for popular S3-compatible services (Cloudflare R2, Wasabi, etc.) once the foundational layer lands.
126+
- Investigate runtime detection or telemetry that suggests required overrides to users instead of manual discovery.

core/src/docs/rfcs/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,7 @@ pub mod rfc_6213_options_api {}
280280
/// Simulate Layer
281281
#[doc = include_str!("6678_simulate_layer.md")]
282282
pub mod rfc_6678_simulate_layer {}
283+
284+
/// Capability Override Layer
285+
#[doc = include_str!("6707_capability_override_layer.md")]
286+
pub mod rfc_6707_capability_override_layer {}

0 commit comments

Comments
 (0)