Skip to content

[feature](cloud) Spill to object storage in cloud mode and report the traffic in SHOW DATA - #68032

Draft
mrhhsg wants to merge 5 commits into
apache:masterfrom
mrhhsg:feat/cloud-spill-s3
Draft

mrhhsg wants to merge 5 commits into
apache:masterfrom
mrhhsg:feat/cloud-spill-s3

Conversation

@mrhhsg

@mrhhsg mrhhsg commented Sep 15, 2026

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: None

Problem Summary:

Cloud-mode BEs could only spill to local disks. This PR adds spill to the instance's S3 storage vault, selected by be.conf (spill_storage_type = local | s3, mutually exclusive), while keeping the existing spill operators and part-file format. The S3 traffic generated by spill is a billing input, so it is also reported to meta-service and shown in SHOW DATA.

BE

  • RemoteSpillDataDir bound to the storage vault (spill_s3_storage_vault, default: the instance's default vault); objects live under spill/{instance_id}/{backend_id}/data/{boot_id}/{query_id}/... plus one empty marker per boot under spill/{instance_id}/{backend_id}/boots/{boot_id}. The instance id comes first because a vault can be shared by several instances (snapshot clones, rollback heirs) and backend ids are allocated per FE cluster; backend_id is the FE-assigned id (cloud_unique_id is shared by every BE added in one ADD BACKEND statement, so it cannot identify a BE). The store becomes ready lazily (no meta-service sync during startup); once ready, the GC thread lists only the boots/ directory and deletes the data of older boot generations, one generation per GC round.
  • SpillFileWriter/SpillFileReader reuse S3FileWriter/S3FileReader. Atomic capacity reservation (spill_s3_storage_limit_bytes) and a submit-time upload budget (spill_s3_max_inflight_upload_bytes, charged with the allocated capacity of every in-flight upload buffer) that blocks writers when too many upload buffers are in flight and refuses cancelled queries; FileWriterOptions gains an upload gate + done callback for this, honoured by S3FileWriter. The spill store is split into SpillDataDir (base), LocalSpillDataDir and RemoteSpillDataDir.
  • Failed parts are drained before the budget is reconciled and multipart uploads are aborted.
  • bvars/metrics: spill_remote_{read,write}_bytes, spill_remote_{get,put}_requests and per-second throughput/QPS; profile counters SpillRemote*.
  • Per-query statistics: TQueryStatistics gains spill_write_bytes_to_remote_storage / spill_read_bytes_from_remote_storage (BE side filled; FE consumers such as the audit log still show the local fields only).
  • Since-boot upload totals are reported to meta-service (report_spill_stats) about once a minute and once more at shutdown after all tasks are done, with bounded retries (best-effort: a crash loses at most one reporting interval).

Meta-service

  • report_spill_stats / get_spill_stats with key stats/{instance_id}/spill/{backend_id}: one record per BE; a report of a newer boot folds the previous process' totals into prior_boots_* (so the number of records is bounded by the number of BEs), a report of the same boot replaces without rolling back, a report of an older boot is rejected. The range is removed when the instance is recycled.
  • Recycler task recycle_expired_spill_objects removes spill objects older than spill_objects_expire_time_second (default 7 days) as a safety net for crashed BEs (S3/MOCK accessors); both the TTL sweep and the deleted-instance cleanup (also on the snapshot-enabled path) touch only this instance's spill/{instance_id}/ prefix, so a vault shared with a live instance is safe. Incomplete multipart uploads of a crashed BE are invisible to prefix listings; an AbortIncompleteMultipartUpload bucket lifecycle rule is the intended safety net for those.

FE

  • SHOW DATA PROPERTIES("entire_warehouse"="true") gains a RemoteSpillWriteSize column; the value is on the total row (spill is not attributable to a database). A meta-service failure is reported instead of showing 0.

Design notes and the local review records are kept outside the repository.

Release note

Cloud mode: spill can be written to the S3 storage vault (spill_storage_type = s3 in be.conf); SHOW DATA PROPERTIES("entire_warehouse"="true") shows the bytes uploaded by spill in the new RemoteSpillWriteSize column.

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes. New be.conf options (spill_storage_type, spill_s3_storage_vault, spill_s3_storage_limit_bytes, spill_s3_max_inflight_upload_bytes), new meta-service config spill_objects_expire_time_second, new column in SHOW DATA ... entire_warehouse.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

https://claude.ai/code/session_01Jrdwwwh8bZSnVwoCwykHse

… traffic in SHOW DATA

Cloud-mode BEs can now spill to the instance's S3 storage vault instead of
local disks, selected by be.conf (`spill_storage_type = local | s3`). The
existing spill operators and part-file format are unchanged; a remote
`SpillDataDir` binds the vault file system lazily (no meta-service sync at
startup) and `SpillFileWriter`/`SpillFileReader` reuse `S3FileWriter` and
`S3FileReader`.

BE:
- Remote spill store keyed by `spill/{cloud_unique_id}/{boot_id}/...`; other
  boot generations are cleaned by the GC thread once the store is ready.
- Atomic capacity reservation (`spill_s3_storage_limit_bytes`) and a
  submit-time upload budget (`spill_s3_max_inflight_upload_bytes`) that
  backpressures writers; `FileWriterOptions` gains an upload gate and a
  done callback for that, honoured by `S3FileWriter`.
- Failed parts are drained before the budget is reconciled; multipart
  uploads are aborted on failure.
- bvars/metrics for remote spill bytes, requests and throughput; profile
  counters for remote spill.
- Since-boot upload totals are reported to meta-service about once a
  minute and once at shutdown after all tasks are done (bounded retries).

Meta-service:
- `report_spill_stats` / `get_spill_stats`: one record per BE
  (`stats/{instance}/spill/{cloud_unique_id}`); a new boot folds the
  previous process into `prior_boots_*`, so the record count is bounded by
  the number of BEs. Records are removed with the instance.
- Recycler task `recycle_expired_spill_objects` deletes spill objects older
  than `spill_objects_expire_time_second` (S3/MOCK accessors).

FE:
- `SHOW DATA PROPERTIES("entire_warehouse"="true")` gains a
  `RemoteSpillWriteSize` column (total row) as the billing input.

Tests: `SpillFileS3Test` (in-memory object store, 16 cases incl. budget,
cancellation and upload-failure paths), `MetaServiceTest.SpillStatsTest`,
`KeysTest.StatsSpillKeyTest`, recycler tests, and a column check in
`test_show_data_warehouse.groovy`.

Claude-Session: https://claude.ai/code/session_01Jrdwwwh8bZSnVwoCwykHse
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@mrhhsg

mrhhsg commented Sep 15, 2026

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions 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.

Static verdict: REQUEST_CHANGES. The S3-backed spill path and warehouse-level SHOW DATA plumbing are present, but 16 blocking correctness and operability issues remain. The highest risks are cross-BE deletion caused by a shared identity, non-durable and non-idempotent lifetime statistics, cleanup paths that can delete live data or leak billable storage, request counters that do not measure physical provider traffic, and unbounded recovery/metadata scans.

Critical checkpoints (Part 1.3):

  1. Goal and proof: The goal is remote S3 spill with capacity control, request/byte accounting, persisted warehouse totals, cleanup, and SHOW DATA exposure. Happy-path BE/cloud/FE/regression tests were added, but the goal is not correctly achieved because identity, durability, cleanup, retry, and accounting failures remain.
  2. Scope and focus: The 47-file change is cohesive around that feature, and the cross-layer breadth is necessary, but the accepted defects mean it is not yet review-ready.
  3. Concurrency: I traced the spill-GC thread, writer/upload pools, non-blocking close, upload-budget mutex/condition variable, atomic counters, heartbeat mutation, and FDB transactions. Reachable upload callbacks are balanced and I found no separate deadlock, but release builds mask an underflow invariant (MMF-10), and transaction serialization does not fence stale generations (MMF-9).
  4. Lifecycle and SIOF: No cross-TU static-initialization dependency was found. Process boot, query/file teardown, startup cleanup, graceful/abrupt shutdown, vault binding, and multipart ownership were traced; MMF-4, MMF-6, MMF-7, MMF-8, MMF-11, MMF-12, and MMF-13 show lifecycle failures.
  5. Configuration: New mutable storage, upload-budget, and recycler controls are observed online, but negative storage limits disable the cap (MMF-3), non-positive TTLs can select live objects (MMF-7), and runtime accepts/persists an upload budget that startup rejects (MMF-15).
  6. Compatibility: The protobuf additions are additive, SHOW DATA row width is consistent, non-cloud mode avoids the RPC, and the repository's supported latest-MS/older-FE direction remains compatible. The FE-first/older-MS hypothesis was dismissed under that explicit contract.
  7. Parallel paths: Local and S3 writer paths, PutObject/UploadPart, data/footer reads, synchronous/non-blocking close, and hooks with no stats sink were checked. Local behavior remains balanced, but both read and write request accounting miss physical retries/failures (MMF-5, MMF-16).
  8. Special conditions: Remote-only selection, default-vault lookup, positive-limit gates, age cutoffs, and release underflow handling were checked. The non-positive and defensive-continue conditions are not safe (MMF-3, MMF-7, MMF-10, MMF-15).
  9. Test coverage: Ordinary round trips, rotation, cleanup retries, capacity concurrency, budget blocking/cancellation, several upload failures, meta-service aggregation, and SHOW DATA shape are covered. Missing negative coverage includes two live BEs sharing an identity, reordered/stale reports, crash before report, live identity migration, real expiry/liveness, abort failure/restart residue, paginated cleanup, churn cardinality, invalid dynamic updates, and SDK/GET retries.
  10. Test results: The changed expected SHOW DATA output is internally consistent with the five-column metadata. Per the review-only contract, I did not run builds or tests; author/CI results were not independently verified here.
  11. Observability: Profiles, workload counters, BE metrics, logs, and meta-service bvars were added, but the report RPC omits its FDB read metrics (MMF-1), physical S3 attempts are undercounted (MMF-5/MMF-16), and the persisted total itself is not reliable across crashes or identity transitions.
  12. Transactions and persistence: The FDB read-modify-write is atomic per attempt, but unequal boot IDs are not an ordering fence (MMF-9), process counters lose unreported crash suffixes (MMF-6), and retired identity records are never compacted (MMF-14). No FE EditLog change is involved.
  13. Data writes and crash behavior: S3 multipart creation/upload/abort, visible-object deletion, capacity reservation, and stats writes were traced. Crashes can lose totals and multipart ownership, deletion failure releases live capacity, and cleanup can delete another live BE or a long-running query's data (MMF-4/MMF-6/MMF-7/MMF-8/MMF-11).
  14. FE/BE variable passing: No new scattered FE-to-BE session variable path was introduced; the new MS/FE protobuf and five-column mapping are consistent. The existing heartbeat cloud identity is nevertheless shared and mutable in ways incompatible with its new use as storage/stats identity (MMF-4/MMF-13).
  15. Performance: Startup recursively materializes the entire spill namespace outside its work budget (MMF-12), and SHOW DATA range-reads every historical backend identity without pagination or compaction (MMF-14).
  16. Other issues: Three capped normal/risk convergence rounds found no additional distinct issue after the 16 inline findings. Two hypotheses were dismissed with code evidence: dynamic default-vault rebinding is not an explicit contract, and FE-first upgrade is outside the documented MS-first rollout direction.

Focus response: no additional user-provided review focus was supplied; full-scope review was performed.

Comment thread cloud/src/meta-service/meta_service.cpp Outdated
const ReportSpillStatsRequest* request,
ReportSpillStatsResponse* response,
::google::protobuf::Closure* done) {
RPC_PREPROCESS(report_spill_stats, put);

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.

report_spill_stats reads the existing record on every periodic report, but this declaration publishes only the put side of the transaction. RPC_PREPROCESS exports detailed KV counters only for the operation names passed here, and the PR adds no report-spill get bvars, so this RPC's FDB read QPS/bytes are invisible under use_detailed_metrics. Please add the get counter/byte bvars and declare this as RPC_PREPROCESS(report_spill_stats, get, put).

}
}

public Cloud.GetSpillStatsResponse getSpillStats(Cloud.GetSpillStatsRequest request)

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.

This new proxy method bypasses MetaServiceClientWrapper.executeRequest, unlike comparable calls such as getTabletStats. One UNAVAILABLE/UNKNOWN/retryable timeout or MS_TOO_BUSY response therefore immediately fails SHOW DATA PROPERTIES("entire_warehouse"="true"), and a failed client is not reconnected. Please implement this through executeWithMetrics("getSpillStats", client -> client.getSpillStats(request), Cloud.GetSpillStatsResponse::getStatus) so it retains the standard retry and reconnection semantics.

Comment thread be/src/common/config.cpp
return config == "local" || config == "s3";
});
DEFINE_String(spill_s3_storage_vault, "");
DEFINE_mInt64(spill_s3_storage_limit_bytes, "0");

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.

The documented unlimited sentinel is 0, but this mutable config accepts negative values too. SpillDataDir::_reach_limit_unlocked checks only > 0, so setting -1 at startup or dynamically silently disables the billable object-storage cap and exposes a negative limit metric. Please add a validator requiring spill_s3_storage_limit_bytes >= 0 and cover rejection of a negative update.

void SpillDataDir::init_remote_fs(io::FileSystemSPtr fs, const std::string& cloud_unique_id) {
DCHECK(_is_remote);
_fs = std::move(fs);
_remote_be_root = fmt::format("{}/{}", SPILL_DIR_PREFIX, cloud_unique_id);

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.

cloud_unique_id is not unique per BE: CloudSystemInfoService.addBackends generates one value before looping over all requested hosts, ResourceManager supports multiple nodes with that value, and FE sends it to each BE. Two live BEs can therefore both use spill/C/{boot_id}; when either starts cleanup it classifies the other's active boot as old and deletes its spill objects. Their reports also collide in one (instance_id, C) record and are repeatedly folded as restarts. Please include a persisted per-node identity such as heartbeat backend_id in both the object root and stats key, and test two simultaneously live BEs, including existing duplicate cloud IDs.

COUNTER_UPDATE(_read_file_size, bytes_read);
ExecEnv::GetInstance()->spill_file_mgr()->update_spill_read_bytes(bytes_read);
if (_is_remote) {
// One read_at() is exactly one GET request on object storage.

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.

One logical read_at() is not necessarily one object-store GET. S3FileReader::read_at_impl may issue multiple get_object attempts after throttling, and it also issues requests that ultimately fail; this callback runs only once after a successful logical read. Query, workload, and global spill GET counters therefore under-report actual request traffic. Please account attempts at the S3 request boundary (or propagate the per-call attempt delta, including failures) and test retry-then-success plus exhausted failure.

watch.start();
std::vector<io::FileInfo> files;
bool exists = false;
RETURN_IF_ERROR(fs->list(be_root, true, &files, &exists));

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.

This eager recursive list materializes every object below the BE root although cleanup needs only distinct first-level boot IDs. S3FileSystem::list_impl first drains the paginated iterator into a complete ObjectMeta vector and then builds a complete FileInfo vector, including current-boot parts. After failed cleanup/restarts this can be arbitrarily large; it ignores the GC time budget and blocks the sole spill-GC thread, which stop() joins. Please use streaming/delimiter-based generation discovery and bound deletion across GC rounds, with a multi-page residue test.

ReportSpillStatsResponse resp;
req.set_cloud_unique_id(config::cloud_unique_id);
auto* stats = req.mutable_stats();
stats->set_cloud_unique_id(config::cloud_unique_id);

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.

The reporter identity is reread from mutable config::cloud_unique_id on every call, but boot ID and counters are cumulative for the unchanged process. Dropping and re-adding the same live endpoint gives it a new FE-generated ID: after reporting 100 under C1, its next cumulative report of 120 creates C2 while C1 remains, so get_spill_stats returns 220. Please keep an immutable stats identity for the process generation while routing with the current outer ID, or make generations independently idempotent in meta-service; add a same-boot C1-to-C2 test.

msg = fmt::format("failed to create txn, err={}", err);
return;
}
std::string key0 = stats_spill_key_prefix(instance_id);

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.

This range is not bounded by the number of live BEs. Every add/replacement generates a new identity and persistent stats key, ordinary node/cluster removal never removes or folds it, and only whole-instance recycling clears the range. A long-lived elastic instance therefore scans all identities ever created in one transaction and returns every PB, eventually hitting read/response limits although FE needs only the scalar lifetime total. Please compact retired-node totals into a bounded instance aggregate with fenced finalization and add churn coverage for bounded key/read cardinality.

static_cast<void>(dir->update_capacity());
}
// Both configs are mutable; observe changes without a restart.
int64_t budget_limit = config::spill_s3_max_inflight_upload_bytes;

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.

Startup treats limit < 2 * s3_write_buffer_size as fatal, but this runtime path installs exactly that invalid value after only a warning; both operands are mutable and the update can be persisted. acquire() then admits one oversized buffer whenever inflight reaches zero, so the live setting can exceed its documented bound, and the persisted configuration prevents the next S3-spill restart. Please enforce the same cross-config invariant for dynamic updates, including buffer-size changes, or make startup accept the documented degraded mode; test live-update/restart parity.

if (_remote_write_stats == nullptr) {
return;
}
((*_remote_write_stats).*counter)++;

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.

This increments once per aggregate SDK call, not per object-store request attempt. The AWS client uses S3CustomRetryStrategy(max_s3_client_retry), so a throttled PutObject/UploadPart that succeeds on its third attempt sends three billable requests but contributes one here; an exhausted operation is likewise under-counted. _finish_part() persists this value as the warehouse PUT-request total. Please attribute initial and retry attempts at the SDK request boundary, including terminal failures, and cover retry success and exhaustion for both operations.

…w of the S3 spill path

- Object root and spill stats record are keyed by the FE-assigned backend_id
  instead of cloud_unique_id, which every BE added by one ADD BACKEND
  statement shares. SpillStatsPB gains backend_id; a live BE whose id
  changes keeps the bound id and logs a warning.
- Startup cleanup discovers old boot generations through a per-boot marker
  object (spill/{backend_id}/boots/{boot_id}, refreshed daily) and deletes
  one generation per GC round instead of listing the whole BE prefix.
- Meta-service: report_spill_stats declares its get side for the KV bvars,
  rejects reports of an older boot_id (a late duplicate or a clock that went
  backwards) and never rolls back the totals of the current boot.
- FE getSpillStats goes through executeWithMetrics for retries/reconnect.
- Validators for spill_s3_storage_limit_bytes (>= 0) and
  spill_s3_max_inflight_upload_bytes (> 0); a budget below two upload
  buffers is a warning (degraded mode) at startup as at runtime; the recycler
  skips a non-positive spill_objects_expire_time_second.
- The upload budget over-release invariant is checked in release builds.
- Reporting documented as best-effort; multipart residue guidance added to
  the config comments.

Claude-Session: https://claude.ai/code/session_01Jrdwwwh8bZSnVwoCwykHse
@mrhhsg

mrhhsg commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review. Pushed a follow-up commit addressing it; per finding:

Fixed

  • meta_service.cpp RPC_PREPROCESS: now (report_spill_stats, get, put) with the get counter/byte bvars.
  • MetaServiceProxy.getSpillStats: goes through executeWithMetrics (standard retry / reconnect / MS_TOO_BUSY handling).
  • spill_s3_storage_limit_bytes: validator rejects negative values; spill_s3_max_inflight_upload_bytes: validator rejects <= 0, and the startup check for < 2 * s3_write_buffer_size now matches the runtime path (warning + degraded one-buffer-at-a-time mode) instead of being fatal.
  • Shared identity (spill_file_manager.cpp / cloud_meta_mgr.cpp): you are right that cloud_unique_id is shared by every BE added in one statement. Both the object root and the stats key now use the FE-assigned backend_id (spill/{backend_id}/data/{boot_id}/..., stats/{instance}/spill/{backend_id}); SpillStatsPB.backend_id added, cloud_unique_id kept as an informational field. A live BE whose id changes (DROP + ADD) keeps the id it was bound with and logs a warning; the new id takes effect at restart.
  • Stale reports (meta_service.cpp fold): a report whose boot_id is older than the recorded one is rejected (INVALID_ARGUMENT) instead of being folded again; a same-boot report never rolls totals back (max). Test covers old-after-new delivery.
  • Startup cleanup (spill_file_manager.cpp list): each boot now writes a small marker spill/{backend_id}/boots/{boot_id} (refreshed daily); cleanup lists only that directory and deletes one old generation per GC round, so the listing is bounded by the number of boot generations, not by the number of spill objects.
  • spill_remote_upload_budget.cpp: DORIS_CHECK_GE, no clamp.
  • recycler.cpp: spill_objects_expire_time_second <= 0 skips the task with a warning (test added); config comment documents the "must exceed the longest query" contract.
  • Proto comment: reporting is documented as best-effort (a crash loses at most one reporting interval; graceful shutdown reports after all tasks are done).

Kept as designed (happy to discuss)

  • Request counters (spill_file_reader.cpp, s3_file_writer.cpp): they count logical SDK calls, not physical retries. Attempt-level accounting needs a per-request hook inside the SDK retry strategy (S3CustomRetryStrategy is process-global with no request context); the byte counters, which are the primary billing input, are exact. Documented as the counting unit; can be a follow-up.
  • Durable per-upload checkpoint (spill_file_manager.cpp counters): writing to meta-service on every upload is not worth the cost; the loss bound is one reporting interval and only on crash.
  • Capacity release on failed delete (spill_file.cpp): same behaviour as the local spill path; retained objects are reclaimed by the query-directory retry and the recycler.
  • Incomplete multipart uploads (spill_file_writer.cpp abort): failed uploads are aborted; residue of a crashed process is invisible to any prefix listing, and neither the BE object client nor the recycler has ListMultipartUploads. The bucket-level AbortIncompleteMultipartUpload rule is the intended safety net (now stated in the config comments).
  • Stats cardinality (get_spill_stats): with backend_id the family grows with "backends ever created" rather than with restarts. Folding retired backends on DROP BACKEND is a reasonable follow-up.
  • Recycler liveness fence: the TTL is documented as "larger than the longest query the cluster allows"; a query holding spill for more than 7 days is outside what this PR targets.

Pure refactor, no behaviour change. The spill store was one class with an
`_is_remote` flag and a branch in every method. It is now:

- spill_data_dir.{h,cpp}: `SpillDataDir`, the abstract base owning what
  both kinds share (path, spill root, byte accounting against the limit,
  metrics), and `LocalSpillDataDir` (disk probing, spill_gc directory,
  usage-based disk selection).
- remote_spill_data_dir.{h,cpp}: `RemoteSpillDataDir` (storage vault
  binding, backend_id/boot_id, data and boot-marker key layout). Cloud
  headers are only included by its .cpp.

`SpillFileManager` keeps typed views of the stores (`_local_stores` and
at most one `_remote_store`) instead of testing `is_remote()` in every
loop; writers and readers still use the virtual `is_remote()` only to pick
local or remote counters.

Claude-Session: https://claude.ai/code/session_01Jrdwwwh8bZSnVwoCwykHse
…elled queries, drop spill of deleted instances

- The upload budget is charged with the allocated capacity of every
  submitted buffer (s3_write_buffer_size), not with its payload: a partially
  filled last buffer keeps its full allocation, so with parts smaller than
  the buffer the budget did not bound memory. The done callback reports the
  same capacity. A part size below the buffer size is warned at startup.
- SpillRemoteUploadBudget::acquire checks cancellation before admitting on
  the fast path and after every wake-up, so a cancelled query never starts
  a new upload.
- recycle_deleted_instance_data deletes the spill/ prefix on the
  snapshot-enabled path too, where only referenced rowsets are recycled
  selectively; spill objects are not referenced by anything.
- Tests: PartialBufferIsChargedAtCapacity, UploadBudgetRejectsCancelledQuery
  Immediately, spill objects in recycle_deleted_instance_with_orphan_tmp_rowset.

Claude-Session: https://claude.ai/code/session_01Jrdwwwh8bZSnVwoCwykHse
…ate with every buffer

- Spill objects live under spill/{instance_id}/{backend_id}/... . A storage
  vault can be shared by several instances (snapshot clones, rollback heirs)
  and backend ids are allocated per FE cluster, so the recycler of one
  instance must never touch another instance's spill. RemoteSpillDataDir
  learns the instance id through CloudMetaMgr::get_instance_id() when it
  binds; both recycler paths (TTL sweep and deleted-instance cleanup) act on
  spill/{instance_id}/ only, and the deleted-instance cleanup is restricted
  to S3/MOCK accessors like the TTL sweep.
- S3FileWriter::close() builds the buffer of an empty object itself so that
  it passes the upload gate like every other buffer; the done callback is
  now paired with the gate on every path.
- TQueryStatistics gains spill_write_bytes_to_remote_storage /
  spill_read_bytes_from_remote_storage, filled by the BE.
- Meta-service clears server-owned SpillStatsPB fields of a report before
  storing it; comments on the budget unit, cancellation and stale reports
  updated.
- Tests: GateAndDoneCallbackReportTheSameCapacity (partial PutObject,
  multipart with partial last buffer, empty object),
  CancelledQueryCloseIssuesNoUpload, other-instance objects in the recycler
  tests and in the startup-cleanup test.

Claude-Session: https://claude.ai/code/session_01Jrdwwwh8bZSnVwoCwykHse
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