Skip to content

feat(rcu): route Rapid Cache Ultra writes to the gRPC appendable writer - #1135

Open
yuxin00j wants to merge 7 commits into
fsspec:mainfrom
yuxin00j:feat-rcu-write-path
Open

yuxin00j wants to merge 7 commits into
fsspec:mainfrom
yuxin00j:feat-rcu-write-path

Conversation

@yuxin00j

@yuxin00j yuxin00j commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Writes to Rapid Cache Ultra (RCU) buckets now go through the gRPC appendable writer (AsyncAppendableObjectWriter) that zonal buckets already use, with storage_class="RAPID". An RCU bucket defaults new objects to STANDARD, which appendable writes reject; with RAPID the object is written to the zonal cache, and the server copies it to regional storage once it is finalized. For that reason finalize_on_close now defaults to True on RCU buckets, while zonal buckets keep False. Handling RCU being disabled while a process is running will come in a separate PR.

Depends on #1134. Only the last five commits are new here.

Changes

  • Write routing (gcsfs/extended_gcsfs.py, gcsfs/core.py):
    • These check _uses_grpc_data_path instead of _is_zonal_bucket: _put_file, _pipe_file, and the initiate_upload and simple_upload helpers in both modules. Unknown layouts and buckets with a plain Rapid Cache stay on HTTP.
    • _open returns a ZonalFile for all supported read and write modes on RCU buckets ("+" not in mode).
    • The new ExtendedGcsFileSystem._init_aaow opens every appendable writer (for ZonalFile, put_file, pipe_file and the upload helpers). It passes storage_class="RAPID" only for RCU buckets. zb_hns_utils.init_aaow (gcsfs/zb_hns_utils.py) gets a storage_class argument, which it forwards only when it is set.
    • _cp_file and _merge still check _is_zonal_bucket, so on RCU buckets they keep using the JSON API.
  • finalize_on_close (gcsfs/extended_gcsfs.py, gcsfs/zonal_file.py):
    • The filesystem and ZonalFile defaults change from False to None.
    • In open, ZonalFile, put_file, pipe_file and simple_upload, None resolves to True on RCU buckets and to False on zonal buckets.
    • Precedence: a per-call value first, then the filesystem attribute, then that default.
    • If put_file, pipe_file or simple_upload fails partway, the writer is closed without finalizing, so a partial object is never finalized.
  • Write failures (gcsfs/extended_gcsfs.py, gcsfs/zb_hns_utils.py, gcsfs/zonal_file.py; these also apply to zonal buckets):
    • close_aaow re-raises a failed finalize instead of only logging it, so put_file, pipe_file, simple_upload, upload_chunk and closing a ZonalFile no longer report an unfinalized write as successful. Closes that do not finalize stay best-effort.
    • If the closing flush of a ZonalFile fails, the writer is still closed without finalizing, and the flush error is raised.
    • If the writer fails to open, the ZonalFile becomes close-only so close() does not retry opening the writer.
    • put_file opens the local file before the writer, so a missing or unreadable local file raises without touching the remote object, as on HTTP.
    • Once the writer is open, put_file and pipe_file update the dircache even when the write fails, and a ZonalFile whose closing flush fails drops its cached path and parent listing, because the object has already been replaced.
  • Ignored upload parameters (gcsfs/zonal_file.py): the appendable writer ignores content_type, metadata, fixed_key_metadata and kms_key_name. ZonalFile write modes on zonal and RCU buckets now log a warning when any of them is set. put_file, pipe_file and the upload helpers already do this.
  • Behavior changes on RCU buckets compared with writing over HTTP:
    • An overwrite replaces the existing object as soon as the writer opens (on the first write for open()), so a write that fails partway leaves the partially written, unfinalized object in place of the old one. Over HTTP, the old object stayed intact until the upload completed.
    • "ab" appends through the gRPC writer. It fails on a finalized or non-appendable object instead of overwriting it, so it can only extend objects that were left unfinalized, for example written with finalize_on_close=False.
    • autocommit=False and transactions no longer defer the write, since ZonalFile.discard() only logs a warning.
    • if_generation_match values above 0 raise NotImplementedError until the SDK writer accepts that argument. 0, mode="create" and "xb" still work.
  • Dependencies (pyproject.toml): require google-cloud-storage>=3.15.0, the first release whose AsyncAppendableObjectWriter accepts storage_class.
  • Tests:
    • gcsfs/tests/conftest.py: zonal_write_mocks takes a BucketLayout param (RAPID_WRITE_LAYOUTS: the zonal layout and both RCU layouts) and exposes the expected storage_class; it skips the RCU layouts on real GCS. Every open now gets an open writer that carries its bucket and object name.
    • gcsfs/tests/test_zonal.py, gcsfs/tests/test_zonal_file.py: the existing write, commit and empty-file tests also run on the RCU layouts and check storage_class. New tests cover every write mode on RCU, the ignored-parameter warning for each parameter, the full finalize_on_close precedence matrix, failed put_file, pipe_file and simple_upload calls closing the writer without finalizing, a failed closing flush still closing the writer, a failed writer open not being retried on close, a missing local file not opening the writer, the dircache update after a failed write, and a failed finalize propagating from put_file, pipe_file and simple_upload through the real close_aaow.
    • gcsfs/tests/test_zonal_unit.py: cp_file and merge on RCU buckets use the core implementation, Rapid Cache buckets keep writing over HTTP, and core.simple_upload and core.initiate_upload hand RCU buckets to the gRPC helpers.
    • gcsfs/tests/test_zb_hns_utils.py: init_aaow forwards storage_class only when it is set, and close_aaow re-raises a failed finalize.
    • The RCU cases use mocks; the live end-to-end suites do not have an RCU bucket yet.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors bucket type detection in gcsfs by replacing the BucketType enum with a more granular BucketLayout dataclass that tracks StorageClass, CachingTier, and NamespaceLayout. This change enables support for Rapid Cache Ultra (RCU) buckets alongside Zonal buckets, allowing them to use the high-performance gRPC data path. The default finalization behavior on close is updated to default to False for Zonal buckets and True for RCU buckets. The review feedback highlights potential resource leaks in _put_file, _pipe_file, and simple_upload if a layout lookup fails after a successful write, suggesting that finalize_on_close be resolved inside the try blocks to ensure the writer is always safely closed.

Comment thread gcsfs/extended_gcsfs.py Resolved
Comment thread gcsfs/extended_gcsfs.py Resolved
Comment thread gcsfs/extended_gcsfs.py Resolved
@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.37%. Comparing base (c783299) to head (0e47110).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1135      +/-   ##
==========================================
+ Coverage   92.24%   92.37%   +0.13%     
==========================================
  Files          20       20              
  Lines        4047     4094      +47     
==========================================
+ Hits         3733     3782      +49     
+ Misses        314      312       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

zhixiangli

This comment was marked as outdated.

@zhixiangli zhixiangli left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked against a real RCU bucket (STANDARD + RAPID_CACHE_ULTRA) and a Zonal bucket with google-cloud-storage 3.17.0. RCU writes do land as RAPID and are finalized by default, as described. Details are in the inline comments:

  1. A failed overwrite on RCU leaves the partial upload as the live object (behavior change, not documented).
  2. Finalize failures are swallowed, so the write is reported as successful.
  3. ZonalFile leaks the write stream if the closing flush fails (now reachable for RCU writes).
  4. Nit: a failed "xb" open calls init_aaow twice.
  5. Test gaps.

I tested the fixes suggested for 2–4 against the real RCU bucket, and the existing unit tests still pass with them applied.

Two test gaps that can't be anchored inline (outside the diff):

  • test_zonal_file_commit and test_zonal_file_lazy_init_creates_empty_file_on_close only run on Zonal. Running them on RAPID_WRITE_LAYOUTS would cover commit() turning finalize_on_close from True to False, and an empty file closing with RAPID and finalize.
  • Every test mocks the writer, so the suite also passes on google-cloud-storage 3.12. A cheap guard: assert storage_class is in inspect.signature(AsyncAppendableObjectWriter.__init__).parameters.

Comment thread gcsfs/extended_gcsfs.py Resolved
Comment thread gcsfs/extended_gcsfs.py Resolved
Comment thread gcsfs/extended_gcsfs.py Resolved
Comment thread gcsfs/zonal_file.py Resolved
Comment thread gcsfs/tests/test_zonal_file.py Resolved
Comment thread gcsfs/tests/test_zonal_file.py Resolved
@yuxin00j
yuxin00j force-pushed the feat-rcu-write-path branch from 8a1ebd3 to 339728c Compare November 2, 2026 10:23
@yuxin00j

yuxin00j commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Done for the commit and empty-file tests; skipped the SDK signature test since CI runs pip install -e . (google-cloud-storage>=3.15.0) and older versions fail with TypeError on the first RCU write.

@yuxin00j
yuxin00j requested a review from zhixiangli November 2, 2026 12:40

@zhixiangli zhixiangli left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. Routing RCU writes through the appendable writer with storage_class="RAPID" works as described. I checked the write paths against google-cloud-storage 3.17, with the gRPC transport replaced by an in-memory server that reproduces GCS appendable-write behavior. An overwrite replaces the object as soon as the writer opens, and only flushed bytes survive a broken stream.

Because the overwrite happens at writer open, a write that fails now changes the bucket. Before this PR, RCU wrote over HTTP and a failed write left the object unchanged. Two places don't account for that yet: the _put_file ordering and the cache maintenance on failure (inline comments 1 and 2). I'd ask for those to be fixed before merging. The other inline comments are minor.

Comment thread gcsfs/extended_gcsfs.py Resolved
Comment thread gcsfs/extended_gcsfs.py Resolved
Comment thread gcsfs/zonal_file.py Resolved
Comment thread gcsfs/tests/conftest.py Resolved
Comment thread gcsfs/tests/test_zonal_file.py Resolved
…yout tests

- Add a skip_if_grpc_read fixture that skips when TEST_BUCKET is a Zonal
  or RCU bucket (both read over gRPC), and use it in the test_core.py
  tests that assert HTTP read internals:
  test_cat_file_routing_and_thresholds, test_cat_file_explicit_concurrency
  and test_user_agent_includes_cache_type_and_source_in_read (replacing
  its inline zonal-only check).
- Cover the HIERARCHICAL_RAPID_CACHE and UNKNOWN layouts in
  test_get_threshold_for_disk_reads_http_buckets_delegates_to_super.
- Drop the unused _is_zonal_bucket patch from the
  test_fetch_range_split_concurrent_* tests; _fetch_range_split does not
  check the bucket type.
- zb_hns_utils.close_aaow re-raises a failed finalize, so put_file,
  pipe_file, simple_upload, upload_chunk and ZonalFile.close() no longer
  report an unfinalized write as successful. Closes that do not finalize
  stay best-effort.
- ZonalFile._close_impl still closes the writer, without finalizing,
  when the closing flush fails, and then raises the flush error.
- ZonalFile._ensure_aaow marks the file force-flushed when the writer
  fails to open, so close() does not open it again.
- Document that on zonal and RCU buckets an overwrite replaces the
  existing object as soon as the writer opens, so a failed write leaves
  the partially written object in its place, unfinalized.
- Tests: a failed finalize in close_aaow, a failed closing flush, a
  failed "xb" writer open, one ignored upload parameter per warning
  case, the finalize_on_close precedence matrix, and the commit and
  empty-file tests on the RCU layouts.
On Zonal and RCU buckets the appendable writer replaces the existing
object as soon as it opens, so a write that fails after that point has
already changed the bucket.

- _put_file opens the local file before the writer, so a missing or
  unreadable lpath raises without touching the object, as on the HTTP
  path.
- Once the writer is open, _put_file and _pipe_file update the dircache
  in a finally, so a failed append or finalize no longer leaves the old
  listing cached.
- ZonalFile._close_impl invalidates the path and its parent when the
  closing flush fails, since fsspec's close() then skips that step.

Tests:
- _put_file with a missing lpath raises FileNotFoundError without
  opening the writer.
- The put/pipe append-failure tests also check the dircache update, and
  a failed ZonalFile closing flush drops the cached parent listing.
- With the real close_aaow, a failed finalize propagates from _put_file,
  _pipe_file and simple_upload, the writer is closed once, and put/pipe
  still update the dircache.
- zonal_write_mocks gives every open an open writer that carries its
  bucket and object name, so later opens in a test are closed too. The
  manual _is_stream_open resets are gone, and test_zonal_file_overwrite
  checks that both opens finalize their writer.
@yuxin00j
yuxin00j force-pushed the feat-rcu-write-path branch from 339728c to 0e47110 Compare November 3, 2026 04:15
@yuxin00j
yuxin00j requested a review from zhixiangli November 3, 2026 06:52

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants