feat(vortex-python): Add OpenDAL-backed CosStore to the Python object-store API - #8845
Conversation
68868e4 to
64006a9
Compare
|
Hi @robert3005, could you please help review this PR when you have a moment? Thanks! |
d63454b to
1e0e128
Compare
Merging this PR will not alter performance
Comparing Footnotes
|
robert3005
left a comment
There was a problem hiding this comment.
Thanks for this — and for picking a genuinely good integration shape.
The approach is right, and I want to say that clearly before the list of problems. The two things that could have sunk an OpenDAL bridge both check out: object_store_opendal's OpendalStore implements real ranged reads (get_opts, honouring Bounded/Offset) and real multipart writes (put_multipart_opts), which is what vortex-io/src/object_store/write.rs:35 needs. That means a new provider costs ~160 lines in a leaf crate behind an off-by-default feature, with no changes to vortex-io at all. That's a shape we'd be happy to maintain.
The implementation isn't there yet. Two things need to be fixed before this can land in any form:
import vortexis broken in every build.vortex-python/python/vortex/store/_cos.pyhas anIndentationErrorat line 68, and the chainvortex/__init__.py→vortex/file.py→vortex/store/__init__.py→._cosis unconditional. Even after fixing the syntax, line 29'sfrom vortex._lib import CosStoreraisesImportErrorin any build without--features opendal, which includes every maturin wheel.- The
cos://flow the PR documents cannot succeed.make_cos_storehard-requiresendpointinpropertiesand never readsCOS_ENDPOINT, whileRegistry::resolvepasses an empty map. I reproduced this with a scratch test on your branch: withCOS_ENDPOINT,TENCENTCLOUD_SECRET_IDandTENCENTCLOUD_SECRET_KEYall set, it fails withmissing required OpenDAL store configuration: endpoint.
Behind those, the design question worth settling first: the PR ships two different CosStore classes — the Rust pyclass (a real store) and a pure-Python os.environ mutator that shadows it. The env-var one is what vortex.store.CosStore resolves to, what the ObjectStore type alias advertises, and what the new docs autoclass, but read_url(store=...) rejects it at runtime, so the docs demonstrate a TypeError. I'd suggest keeping the pyclass and dropping the env-var helper; process-global env mutation is a side channel no other store has, and it races the Rust side's std::env::vars() read in Registry::resolve.
I've left 14 inline comments. Grouped by kind:
Blocking
| # | Where | Issue |
|---|---|---|
| 1 | store/_cos.py:68 |
IndentationError; also Any and os unimported |
| 2 | store/_cos.py:29 |
unconditional import of a feature-gated pyclass |
| 3 | store/_cos.py:57 |
two conflicting CosStore designs |
| 4 | vortex-object-store-opendal/src/lib.rs:87 |
cos:// env-var configuration always fails |
| 5 | vortex-object-store-opendal/src/lib.rs:74 |
oss:// advertised in 4 places, implemented in none |
| 6 | vortex-python/src/opendal_store.rs:98 |
cargo clippy --all-features --all-targets fails |
| 7 | docs/api/python/store/opendal.rst:3 |
short title underline fails --fail-on-warning docs build |
| 8 | store/__init__.py:26 |
ruff format --check fails |
Correctness and design
| # | Where | Issue |
|---|---|---|
| 9 | vortex-jni/src/object_store.rs:57 |
COS bypasses the store cache; new Operator per request |
| 10 | vortex-python/src/object_store/registry.rs:85 |
registry precedence inverted; no caching |
| 11 | docs/api/python/store/opendal.rst:61 |
store= example passes a URL where a store-relative key is required |
| 12 | vortex-python/src/opendal_store.rs:86 |
synthetic cos://bucket URL to reach the builder |
| 13 | vortex-object-store-opendal/src/lib.rs:108 |
ClientOptions silently inapplicable to COS |
| 14 | vortex-python/src/opendal_store.rs:38 |
pyclass should be frozen |
Smaller points, not worth inline threads
- Dependency weight. This adds a second full HTTP/TLS stack —
reqwest 0.13.4withrustls-platform-verifieralongside the existingreqwest 0.12.28withrustls-native-certs— plusopendal-core,opendal-service-cos, threereqsign-*crates,mea, andjs-sys/wasm-bindgenpulled intojiff. 145 newCargo.locklines. That's acceptable behind an off-by-default feature, but note CI's--all-featuresclippy job pays the compile cost on every run. opendalandobject_store_opendalare version-pinned inline in the new crate rather than declared in[workspace.dependencies], unlike every other third-party dependency.vortex-utilsis added tovortex-pythonas a non-optional dependency but only used undercfg(feature = "opendal"). Also two spellings of the same type:vortex::utils::aliases::hash_map::HashMapinregistry.rsvsvortex_utils::aliases::hash_map::HashMapinopendal_store.rs— please pick one.writeandVortexWriteOptions.writenow acceptAnyVortexStore, but onlyread_url'sstore :docstring line was updated to mentionCosStore.- Test coverage:
cos_derives_bucket_from_hostasserts only "not aMissingConfigerror" and its comment ("build fails before we can assert bucket derivation") is wrong — I confirmed the build succeeds without credentials. Nothing covers thecos://registry resolution path, which is the path that's actually broken, and there are no Python tests forCosStore. - Stray blank line inserted between the
read_urldoc comment and#[pyfunction], and two trailing blank lines appended to the rootCargo.toml. - The PR description says the vendored
pyo3-object_storefork and[patch.crates-io]were removed, butdevelopalready has neither — that part is a no-op in this diff. Worth trimming from the description so reviewers aren't looking for it.
Checks I ran (against 1e0e128 rebased on develop @ a12c310)
| Check | Result |
|---|---|
cargo check -p vortex-object-store-opendal --all-targets |
pass |
cargo test -p vortex-object-store-opendal |
3 passed |
cargo check -p vortex-python --features opendal --all-targets |
pass |
cargo clippy -p vortex-object-store-opendal -p vortex-python --features vortex-python/opendal --all-targets |
fail (#6) |
python -m py_compile _cos.py |
fail (#1) |
ruff check / ruff format --diff |
fail (#1, #8) |
docutils parse of opendal.rst |
fail (#7) |
env-var cos:// resolution repro (scratch test, reverted) |
fail (#4) |
Not run: full Sphinx build, pytest, and basedpyright — all blocked by the import failure. No COS credentials available, so nothing end-to-end.
Happy to look again once the import break and the endpoint plumbing are sorted, and glad to discuss the one-CosStore-or-two question first if that helps — it changes what the docs and the type alias should say.
Generated by Claude Code
5dc3f72 to
fe4d545
Compare
robert3005
left a comment
There was a problem hiding this comment.
Thanks for the round of fixes — 12 of the 14 items from my earlier review are addressed and verified on fe4d545: the _cos.py syntax error, the unconditional pyclass import (now a guarded try/except ImportError with an actionable message), the two conflicting CosStore designs, the unresolvable cos:// endpoint, the oss:// claims, the clippy tests_outside_test_module lint, the RST underline, ruff format, the JNI cache bypass, the store= docs examples, the synthetic-URL round-trip (nice — CosConfig + make_cos_store is the right shape), the ClientOptions limitation, and frozen. The smaller points came along too: vortex-utils gated on the feature, one spelling of vortex_utils, updated write docstrings, taplo sorting.
Four things left, one of them a regression the caching change introduced.
The COS caching in Registry::resolve is broken and shouldn't exist as a separate path. The second resolve of a cos:// URL returns an empty path, and a sibling object in the same bucket still rebuilds the client. The underlying reason is worth more than the fix: cos:// is a bucket-in-authority scheme exactly like s3://, and s3:// already caches at depth 0 through the generic formula — so COS needs no bespoke caching and no hand-written depth. Details and a verified patch in the inline comment.
A flaky test in vortex-object-store-opendal (fails ~3 of 4 cargo test runs; nextest's process isolation masks it, so CI may not catch it).
3 new basedpyright errors, gated by CI at .github/workflows/ci.yml:94, because the new pyclass has no entry in vortex/_lib/__init__.pyi.
The docs page documents the placeholder, not the real class, since no build enables the opendal feature — so the rendered parameters contradict the prose beside them. The same stub fixes this one.
Checks I ran on fe4d545: cargo clippy -p vortex-python --all-targets (with and without --features opendal), cargo test -p vortex-object-store-opendal, cargo test -p vortex-python --lib, py_compile, ruff check, ruff format --check, basedpyright vortex-python, a docutils parse of the new RST, and a live import vortex + CosStore(...) on a default build to confirm the ImportError guard. I did not run a full Sphinx build, so the docs finding comes from inspecting what autodoc resolves to rather than from rendered HTML.
Generated by Claude Code
|
We can merge this once we fix the python lint failures. Thanks for the contribution! |
|
Looks like you will also need dco remediation commit |
done |
…-store API Signed-off-by: forwardxu <forwardxu@apache.org>
The OpenDAL-backed CosStore now holds Arc<dyn object_store::ObjectStore> directly instead of relying on the vendored fork's From<Arc<dyn ObjectStore>> for PyObjectStore constructor. This removes the [patch.crates-io] fork of pyo3-object_store entirely; the upstream 0.11.0 crate (matched to the workspace's object_store 0.13.x) is used for the built-in store classes, preserving obstore interoperability. - Remove vendor/pyo3-object_store (forked source + type hints) - Remove [patch.crates-io] section and its comment from Cargo.toml - Pin pyo3-object_store to 0.11.0 (object_store 0.13.x, no trait conflict) - CosStore stores Arc<dyn object_store::ObjectStore>; drop PyObjectStore dep - Fix clippy clone_on_ref_ptr in to_arc Signed-off-by: forwardxu <forwardxu@apache.org>
Fix taplo format check failures in CI: sort dependency tables alphabetically and drop trailing blank lines in Cargo.toml, vortex-jni/Cargo.toml, vortex-object-store-opendal/Cargo.toml, and vortex-python/Cargo.toml. Signed-off-by: forwardxu <forwardxu@apache.org>
Prefer CosConfig over URL/property round-trips, cache COS clients in JNI/Python registries, and document store= usage after dropping CosStore.apply(). Signed-off-by: forwardxu <forwardxu@apache.org>
…Store stub - registry.rs: route cos:// through a unified build_store + cache_and_resolve pipeline, with the OpenDAL branch rooted at the bucket (depth 0) like s3:// so sibling objects share a client. Pull the per-key path-walk into an entry_at helper that register and cache_and_resolve both reuse, and add Arc::ptr_eq-based tests pinning the symmetry from both sides (per-key cache, per-bucket client, prefix registration at depth > 0). - vortex-object-store-opendal: make the env-var fallback injectable so the two cos tests no longer race against the global environment under cargo test's default multi-threaded runner (Rust 2024 makes std::env::set_var unsafe for exactly this reason). Merge the two flaky tests into a single endpoint-required + env-fallback pair driven by a fixed lookup map. - vortex/_lib/__init__.pyi: declare CosStore with its full constructor signature so basedpyright sees a real symbol (fixes 3 reportAttributeAccessIssue errors against _cos.py), and so .. autoclass:: in the docs page renders the real parameters instead of the runtime placeholder. - Drop now-unnecessary # pyright: ignore[reportMissingModuleSource] comments on the store/_*.py files that import vortex._lib. Signed-off-by: forwardxu <forwardxu@apache.org>
The previous `.. autoclass:: vortex.store.CosStore` directive rendered the `_cos.py` placeholder (`(*args, **kwargs) -> None`, "Placeholder; the real implementation requires the ``opendal`` feature") on default docs builds, because the docs CI builds vortex without the opendal feature, so autodoc sees only the placeholder. Even with `CosStore` declared in `vortex/_lib/__init__.pyi`, autodoc reads the runtime class, not the stub. Replace the autoclass with a hand-written `.. py:class::` directive that embeds the real signature and per-parameter descriptions. This is the "Failing that" branch in the upstream review: write the signature and parameters into the page by hand instead of relying on autoclass. Verified locally with sphinx-build 9.1.0: the rendered HTML now shows `vortex.store.CosStore(bucket, endpoint, *, secret_id=None, secret_key=None, root=None, disable_config_load=False)` regardless of which feature flags the build environment enables. Signed-off-by: forwardxu <forwardxu@apache.org>
…Store warnings - Add `# pyright: ignore[reportMissingModuleSource]` to the 7 store module files that import the dynamically-registered `vortex._lib.store` submodule. The submodule has no Python source (only .pyi stubs, which are excluded from basedpyright), so pyright could not resolve it. This was previously fixed by the same comments; commit b990c75 incorrectly removed them assuming the `_lib/__init__.pyi` CosStore declaration covered the store submodule too, but it does not. - Add `nitpick_ignore` entry for `vortex.store._cos.CosStore` in `docs/conf.py`. The `ObjectStore` type alias resolves to this private path, but the public class is documented via `.. py:class::` in `opendal.rst` which only adds the public path `vortex.store.CosStore` to the inventory. Signed-off-by: forwardxu <forwardxu@apache.org>
Update Cargo.lock to match the latest origin/develop (b4f8d34) which includes renovatebot dependency updates (reqsign 3.2.0, reqsign-file-read-tokio 3.0.3, reqsign-tencent-cos 3.0.3) and removes conflict markers left from the rebase. Signed-off-by: forwardxu <forwardxu@apache.org>
Head branch was pushed to by a user without write access
6b37b3d to
952fcb6
Compare
opendal/object_store_opendalto 0.57.0 (in the newvortex-object-store-opendalcrate); this version enables COS versioning by default.pyo3-object_store0.11.0 from crates.io (no longer vendored via[patch.crates-io]). The built-in store classes (S3/Azure/GCS/HTTP/Local/Memory) keep theirPyObjectStoreinterop, and obstore users can still pass external stores.vortex-object-store-opendal, a new workspace crate that builds anobject_store::ObjectStorefrom an OpenDALservices::CosOpendalStore(make_opendal_store(url, properties)), reused by both the URL registry and the Python store class.CosStorepyclass (opendal feature) that wraps an OpenDAL-backed Tencent COSOpendalStoreand holds it asArc<dyn object_store::ObjectStore>. It is constructed with explicit config (bucket,endpoint,secret_id,secret_key,root,disable_config_load) and can be handed tovortex.io.read_url(store=...)/vortex.io.write(..., store=...).AnyVortexStoreinio.rssoread_url/writeaccept both the built-inpyo3-object_storeclasses and the new OpenDAL-backedCosStorevia a singlestore=argument.cos://andoss://URL resolution now go through the OpenDAL-backed store, configured via OpenDAL environment variables (TENCENTCLOUD_SECRET_ID/TENCENTCLOUD_SECRET_KEY/COS_ENDPOINT, andALIBABACLOUD_ACCESS_KEY_ID/ALIBABACLOUD_ACCESS_KEY_SECRET/OSS_ENDPOINT).cos_store_builds_object_store).Rationale for this change
Closes: #8844
Vortex's Python API already exposes concrete, standalone store objects for S3/Azure/GCS/HTTP/Local/Memory, but Tencent COS and Alibaba OSS were only reachable through URL-based resolution that relied on environment variables. This adds first-class OpenDAL-backed stores so COS/OSS can be used exactly like the other stores (construct once, pass via
store=), with better ergonomics and type checking.What changes are included in this PR?
vortex-object-store-opendalcrate (OpenDALservices-cos,opendal/object_store_opendal0.57.0)pyo3-object_storepinned to the upstream 0.11.0 (object_store 0.13.x, matching the workspace) — vendored fork removedCosStorepyclass (gated on theopendalfeature) wrapping an OpenDAL COSOpendalStore, holdingArc<dyn object_store::ObjectStore>AnyVortexStoreextractor inio.rssoread_url/writeaccept the new storecos:///oss://URL resolution backed by OpenDALWhat APIs are changed? Are there any user-facing changes?
New public API (Python,
opendalfeature):vortex._lib.CosStore(bucket, endpoint, *, secret_id=None, secret_key=None, root=None, disable_config_load=False)— a standaloneObjectStoreyou can pass tovortex.io.read_url(url, store=...)/vortex.io.write(arrays, path, store=...).vortex.store.CosStore/vortex.store.from_urlnow documentcos://andoss://OpenDAL-backed support.