Skip to content

v1.x — deferred collections interfaces (uploads, versions, tags, bucket config, annotations, fsspec) #12

Open

Description

@thorwhalen

Deferred from v1 (ADR-0009). Each of these is a genuinely natural collections.abc fit; they wait because v1 scope is core + large-object I/O, and because over-scoping is the top risk (zarr's v3 rewrite replaced a MutableMapping store interface with a 15-method async ABC and killed five of its own backends in the process).

Roughly in the order they should arrive:

  1. In-flight multipart uploads as a Mapping. ListMultipartUploads + AbortMultipartUpload is literally a mapping whose __delitem__ aborts: for k in uploads: del uploads[k] replaces the loop AWS's own docs tell you to write. Orphaned uploads are the classic silent S3 bill. Highest portability of this set, and client.exceptions.NoSuchUpload actually exists (one of only 13 modelled S3 error shapes), so it maps cleanly to KeyError. Key: (key, upload_id).
  2. Object versions (+ a delete-marker view). Key: (key, version_id) or version_id.
  3. Object tags (MutableMapping[str, str], ≤10 entries) and user metadata (x-amz-meta-). Both natural, both portability 2–4/5, so each needs capability detection. Note user-metadata writes are a CopyObject — surprising enough to document loudly.
  4. Bucket configuration as one Mapping: cfg['lifecycle'] = {...} over the Get/Put/Delete triples (lifecycle, cors, policy, encryption, tagging, versioning, …). One spec-driven store rather than 20 methods.
  5. Object annotations. Natively a per-object MutableMapping[str, bytes] — the first S3 feature that is a nested store rather than a flat blob store, which for a dol-shaped library is a gift: store.annotations['some/key']['embedding'] = .... Deferred on two hard facts, not design: no S3-compatible provider implements it (portability 0/5), and it needs a botocore floor too fresh for a storage library. Feature-detect with hasattr(client, 'put_object_annotation').
  6. fsspec adapter — to_fsspec() / from_fsspec(). One adapter buys pandas, dask, pyarrow, and zarr-v3-via-FsspecStore. The mandatory surface is small: _strip_protocol + ls(detail=True) + _open/_fetch_range.
  7. s3dol[fast] — obstore for the object level only (it has no bucket operations), if an independent re-measurement justifies it.
  8. s3dol.aio — async mirroring the sync surface.

Explicitly never

  • GetObjectTorrent — dead.
  • SelectObjectContent — AWS closed it to new customers in July 2024 and points at Athena / Object Lambda. A Mapping over a service new users cannot enable is negative value.
  • Native async Store ABC / zarr-v3 store. An s3dol store is already a working zarr v2 store for free. zarr v3 left MutableMapping behind deliberately, for async and byte-range coalescing; chasing it means becoming a different package. Adapt via fsspec instead.

Not ours at all → botodol

The line is mechanical — the endpoint test: can it be reached through the same boto3.client('s3', endpoint_url=...) a MinIO or R2 user already holds? If not, it isn't s3dol's. s3control is a separate service model with a required AccountId that no S3-compatible provider implements; same for s3tables, s3vectors, s3outposts, glacier. That test puts access points, batch jobs, Storage Lens, Access Grants and MRAP in botodol, and it never needs arguing.

Two cases the test resolves correctly rather than by exception: access points are consumed as bucket names (so s3dol accepts an access-point ARN wherever it accepts a bucket name — one line in the normalizer; administering them is botodol), and S3 Express / directory buckets are one row in the preset registry, not a new abstraction.

One obligation this creates: the spec-driven sub-resource store (item 4) and the paginated-list store are boto-generic, not S3-specific — the same code serves DynamoDB tables, SQS queues, Secrets Manager secrets. When built, they go in a documented public module and botodol depends on s3dol for them. Don't build a shared base package speculatively; don't let botodol reimplement them.

Activity

  1. thorwhalen commented on Aug 10, 2026

    @thorwhalen
    MemberAuthor

    ADR-0011 (merged in #17) changes the shape of three deferrals here, and makes them cheaper
    rather than closing them.

    A presigned-URL view, an ObjectInfo-from-LIST view, and object handles are no longer three
    separate features: under ADR-0011 §D2 they are three instances of one mechanism — a sibling
    Layer B KvReader over the same key space, sharing a private base with BucketReader. So
    BucketHandles lands in v1 (it is the only per-object entry point), and BucketUrls /
    BucketInfo become near-free once it exists.

    What stays deferred is the attribute form — store.urls[k] as a property on the data store.
    That is verified broken at dol 0.3.58: a wrapper does not re-wrap a Mapping-valued attribute, so
    store.urls returns an inner-keyed mapping and KeyErrors for every key the outer store has.
    Making it work needs upstream re-wrapping (i2mint/dol#10) or the .meta sidecar design; tracked
    there, not here.

    Consequence for users in v1: if you wrap the data store with a key codec, wrap the sibling in
    parallel. s3dol.handles(store) raises on a wrapped store rather than silently handing back an
    unwrapped sibling.

  2. thorwhalen commented on Aug 11, 2026

    @thorwhalen
    MemberAuthor

    ADR-0012 (merged in #18) adds
    one deferral here and removes an assumption.

    Newly deferred: anon='auto'. v1 ships anon: bool. Widening bool → bool | 'auto' later is
    fully compatible and nothing in the ecosystem uses it today. The reason to defer is the shape of
    the evidence rather than the difficulty: across three review rounds the guard count went
    4 → 6 → 7, each round finding another silent-credential-downgrade path — which is the signal
    a feature whose purpose is to guess emits. It is the only path on which s3dol could silently
    read the public view of data the caller believed was authenticated; every other credential
    path either resolves or fails loudly. Re-decide it on real diagnose() telemetry: if nobody sets
    it, the case is closed.

    Not deferred, contrary to what one might assume: provider detection from a bare endpoint URL
    supplies the full preset row in v1, including the ADR-0003 §3 checksum fix. Measured, only
    addressing_style and endpoint_url can move the wire target — checksum and payload-signing
    provably cannot — so the blast radius of detection is exactly one announced path→virtual flip,
    and all three known endpoint-passing dependents resolve to generic-s3, byte-identical to today.

    Also removed from the deferral space: a live-boto3-client escape hatch on S3Connection was
    considered and rejected rather than deferred, so it should not resurface as a v1.x item. It
    would create two permanent classes of connection — one picklable, one not — and no known
    dependent passes client=.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions