Repository navigation
Add the objects suite of batch operations over many S3 objects - #1320
Conversation
The objects suite runs one batch operation of fsspec, cat, get, put, pipe, copy, rm, find, or info, over a prepared set of many objects through the three APIs of the filesystem suite: PyAthena's S3FileSystem, whose batch operations fsspec runs one file at a time, AioS3FileSystem, whose coroutines are awaited on the trial's event loop, and s3fs. A case's concurrency is the filesystem's max_workers and connection pool and the batch_size of the asyncio operations. Its scales are object counts; a set has count objects of the configured size under a files prefix, the first uploaded and the rest copied, and count directories of empty objects under a listing prefix for find. The trial prepares the operation's inputs and checks and removes what it created outside the measurement. Each trial of the filesystem and objects suites also records the process CPU time of its operation, and the summary gains objects per second and CPU columns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…entence per line (GH-1317) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| return [f"{prefix}{i:06d}/{j:03d}" for i in range(count) for j in range(keys_per_object)] | ||
|
|
||
|
|
||
| def batch(fs: Any, case: Case, paths: list[str], targets: list[str], local: Path, size: int) -> Any: |
There was a problem hiding this comment.
Self-review round one (behavior and implementation)
Base 2e66e94aabeee7d8924f2b7401727e83fca91abd, head 5ddb32c510ee5381576274992d0084c1c7e4e910 (initial pass at 6cf5993d, cleanups in 5ddb32c5).
Covered: benchmarks/pyathena_bench/{objects,cases,config,aws,runner,report,filesystem,fleet,__main__}.py, config.toml, benchmarks/README.md, and the tests in test_objects.py, test_config.py, test_aws.py, test_runner.py, test_report.py, and test_fleet.py.
Traced against the resolved fsspec 2026.9.0, s3fs 2026.9.0, and PyAthena: AbstractFileSystem.cat/get/put/pipe/copy with list arguments pair the paths and call the per-file methods one at a time (the sync baseline); AsyncFileSystem._cat/_get/_put/_pipe/_copy take batch_size (PyAthena's _get forwards it in **kwargs); s3fs's _rm forwards its kwargs to DeleteObjects, so rm takes no batch size with any API; _copy forwards the directives to _cp_file → _copy_basic → copy_object for s3fs and to _copy_file → S3Core.copy_object for PyAthena; info fans out through a thread pool (sync) or fsspec.asyn._run_coros_in_chunks on the trial loop (aio) or on s3fs's IO loop via fsspec.asyn.sync. Live: all 24 API × operation trials of run_objects() returned ok against the CI bucket with a 5-object set.
Resources: the trial's created objects are deleted and its local directory removed in finally, also after a failed operation (tested); the local directory is under the supervisor's trial directory, so a killed trial leaves nothing behind. Trial statuses carry expected_objects/expected_bytes for the report's notes.
Tests: four mutations (dropping batch_size, the copy directives, the cleanup, and the set validation) each failed exactly one test.
Result: FINDINGS, two cleanups without behavior change (thread on __main__.py; the summary note lines in report.py reworded to one sentence per line). Not run: prepare_object_sets() against a stack (#1318).
There was a problem hiding this comment.
Round one repair follow-up (narrow, git range-diff 2e66e94a..5ddb32c5 2e66e94a..9b7c0e3a) for the independent-review repairs in 9b7c0e3a. Traced hunks: run_objects() now prepares inside the guarded block, captures the operation's timing in a finally that also stops the heartbeat, awaits the monitor in the outer finally only when it was created, turns a failed removal into a trial error after the measurement, and reports successful_queries_per_second as 0 for a failed trial (no division by a zero preparation time); delete_objects() raises on Errors; run_filesystem() captures its timing the same way and its error record gains the timing; Settings rejects an object size above 5 GiB. Callers: worker() and the report consume the same record keys; the error record of a failed preparation has total_seconds 0.0 and no cpu_seconds, which the report's filters tolerate. Tests: the five new or changed tests exercise each hunk, and just benchmark test passed 337/337/338; the put, rm, and copy trials re-ran live. Result: CLEAN for the repair.
There was a problem hiding this comment.
Round one repair follow-up 2 (narrow, git range-diff 2e66e94a..9b7c0e3a 2e66e94a..051dae25): run_objects() removes the local directory with shutil.rmtree only when it exists and lets its error reach the removal handler; the three test changes exercise the slow coroutine, the local removal error, and the filesystem suite's failed-transfer and failed-check timing. just benchmark test 339/339/340. Result: CLEAN.
| "--transport", | ||
| choices=("csv", "unload", "ctas", *FILESYSTEM_OPERATIONS), | ||
| # The two suites share the get, put, and copy operation names. | ||
| choices=("csv", "unload", "ctas", *{*FILESYSTEM_OPERATIONS, *OBJECTS_OPERATIONS}), |
There was a problem hiding this comment.
Round one F1 (cleanup): the --transport choices listed get, put, and copy twice, once per suite; argparse accepted that, but the help text repeated them. The choices are now a set of both suites' operations.
|
|
||
| ### Objects suite | ||
|
|
||
| The objects suite runs one batch operation over many objects through the same three APIs as the filesystem suite, `S3FileSystem` (`sync`), `AioS3FileSystem` (`aio`), and s3fs (`s3fs`), and does not query Athena. |
There was a problem hiding this comment.
Self-review round two (claims, callers, operations, evidence)
Base 2e66e94aabeee7d8924f2b7401727e83fca91abd, head 5ddb32c510ee5381576274992d0084c1c7e4e910.
Claims checked:
- The
syncAPI's batch operations run one file at a time. fsspec 2026.9.0AbstractFileSystem.catloopscat_fileper path,putcallsput_fileper source, andget/copy/pipelikewise; PyAthena'sgetandcopyfall through to them. Onlyrmbatches:S3Core.MAX_KEYS = 1000keys per DeleteObjects. - Both libraries delete in DeleteObjects requests of 1,000 keys, which no batch size affects. PyAthena's
_rmdocumentsbatch_sizeas unused and gathers the requests in threads; s3fs's_rmchunks_bulk_deleteby 1,000 with a fixedbatch_size=3and forwards other kwargs to DeleteObjects, which is whyrmis called without one. - The
batch_sizeof the aio and s3fs operations is the number of files transferred at the same time.AsyncFileSystem._cat/_get/_put/_pipe/_copypass it to_run_coros_in_chunks; PyAthena's aio coroutines each run the synchronous method in the filesystem's pool of the samemax_workers. - A set of 1,000 objects takes about 1,000 copies and 100,000 PutObject requests.
prepare_object_setsuploads the first object, copies the other 999, and putscount × listing_keys_per_objectempty keys; it then lists both prefixes (about 100 pages for the listing). cleanupremoves the sets. They live underruns/<run_id>/object-sets/, below the scratch root thatcleanupdeletes;--trials-onlykeeps them, as it keeps the single objects.cpu_secondsis the process CPU time, user and system.time.process_time()is the sum of the process's system and user CPU time, across threads.- Existing callers.
Settingsgains fields with defaults andloadaccepts[objects];Case.id,matrix, and the job IDs of the other suites are unchanged; the summary gains two columns, and the history entries refer to columns by name.run_filesystem()'s trial record gainscpu_secondsonly. - Evidence.
just benchmark testpassed in the three environments at6cf5993dand the head; four mutations each failed one test; the live 24-trial smoke ran at6cf5993d, and the head differs by the--transportchoices and two note lines.
Limits: s3fs's own retries above botocore's are not equalized, as in the filesystem suite; the request counts of the sync paths include PyAthena's per-file info calls (get checks isdir, copy reads the source size), which the measurement reports as its behavior; prepare_object_sets has not run against a stack.
Result: CLEAN; no claim corrected.
There was a problem hiding this comment.
Round two repair follow-up (claims and evidence of 9b7c0e3a). Claims checked: (a) a removal whose DeleteObjects reports errors fails the trial while keeping the measurement — delete_objects() raises on the response's Errors, and the finally rewrites the status and error of the query that holds the outcome (tested); (b) the objects of a failed preparation are removed — the rm preparation's copies land under the trial's prefix, which the finally lists and deletes (tested with a copy that fails on a named key); (c) the wall and CPU time of a failed operation are kept — captured in the inner finally before the check (tested for a failing operation and a failing removal); (d) CopyObject copies at most 5 GiB — the S3 limit on a single CopyObject, the same bound as S3Core.MULTIPART_UPLOAD_MAX_PART_SIZE; (e) a write of a failed aio batch may create an object after the removal — S3AioThreadPool.run() lets a running call continue after the awaiting task is cancelled, and the pool is reachable only privately; stated as a limit in the docstring and the README, and the run's cleanup removes such objects. PR body updated (statuses, size limit, test counts, live re-run, mutation evidence). Result: CLEAN for the repair.
There was a problem hiding this comment.
Round two repair follow-up 2 (051dae25): the PR body's removal statement now names both S3 and local removals, matching run_objects(); test counts updated. Result: CLEAN.
| write_json(path, manifest) | ||
|
|
||
|
|
||
| def prepare_object_sets( |
There was a problem hiding this comment.
Round two claim 4 (verified): one upload_fileobj, count - 1 copy_object calls with both directives, and count × listing_keys_per_object empty put_object calls, in a pool of 32 threads; the set is recorded only after both prefixes are counted. The large default set (10,000 objects) is one million puts, which the README says to prepare only for a run that measures it.
…reated (GH-1317) The objects suite's trial recorded the CPU time of an operation only when it and its check succeeded, prepared its inputs outside the cleanup, so that a failed preparation left its copies behind, and ignored the keys that a DeleteObjects request did not delete. The trial now captures the wall and CPU time of the operation in a finally clause, prepares inside the block whose finally removes the trial's objects, and makes a removal that reports errors a trial error that keeps the measurement; the filesystem suite keeps its timing the same way. The object size of the objects suite is limited to 5 GiB, the most a CopyObject request copies. The tests of the parallel copies compare unordered pairs, the copy directives are asserted by value, and the loop lag test observes a sample. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| the trial an error. The CPU time of the process during the operation is | ||
| recorded as ``cpu_seconds``, also when the operation or its check fails. | ||
| A write of the ``aio`` API that a failed batch leaves running in a thread | ||
| may create an object after the removal; the run's ``cleanup`` removes it. |
There was a problem hiding this comment.
Independent review (relayed): Codex CLI 0.164.0, model gpt-6-astra, session 01a1692c-40d4-70a0-888a-070d2f980b3b, read-only sandbox, static review of base 2e66e94aabeee7d8924f2b7401727e83fca91abd..head 5ddb32c510ee5381576274992d0084c1c7e4e910 in a detached snapshot, prompt without the PR text or prior findings. Covered: the 24 operation × API combinations (list pairing, kwargs, returns), preparation, checks and cleanup, manifest namespaces, CLI and fleet expansion, configuration, reporting and CPU timing, README and docstrings, and the 27 added or changed tests. Verdict: FINDINGS (six P2, two P3); the operation calls, list pairing, batch-size forwarding, and result interpretation were found to match the implementations.
- P2, a DeleteObjects response with per-key
Errorswas ignored, so a trial could returnokwith objects left behind. Verified and repaired in9b7c0e3a:delete_objects()raises onErrors, and a removal that fails makes the trial an error that keeps the measurement (thread below). - P2, a failed preparation bypassed the cleanup:
prepare_trial()ran before thetry/finally, so the copies of a partly preparedrmtrial stayed. Repaired: the preparation runs inside the block whosefinallyremoves the trial's objects (thread below). - P2, a write of a failed
aiobatch may finish after the removal: fsspec cancels the pending coroutines, but a blocking call already running inS3AioThreadPoolcontinues. Verified; accepted as a limit (this docstring and the README): the pool can only be drained through the filesystem's private thread pool, and the objects stay under the trial's prefix, which the run'scleanupremoves. The trial is already an error in that case. - P2, an object size above 5 GiB passed validation although the set is prepared with CopyObject. Repaired:
objects_sizeis limited to 5 GiB (thread onconfig.py). - P2,
cpu_secondswas lost when the operation or its check failed. Repaired in both suites: the wall and CPU time are captured in afinallyand kept in the error record (threads onobjects.pyandfilesystem.py). - P2, two tests compared parallel copies in order. Repaired: unordered comparisons, and the failing-copy fake fails a named key instead of a count, which raced.
- P3, the directive test used the production constant. Repaired: literal names and values.
- P3, the loop-lag test passed without a sample. Repaired: a 50 ms coroutine under the 10 ms heartbeat, asserting at least one sample.
Validation of the repairs: just benchmark test 337/337/338 passed in the three environments; the put, rm, and copy trials of the three APIs re-ran live against the CI bucket. Limits stated by the reviewer: the recording-filesystem tests establish dispatch, not the libraries' semantics, which the live smoke covers.
There was a problem hiding this comment.
Independent follow-up (relayed): Codex CLI 0.164.0, model gpt-6-astra, session 01a16941-0f4e-7463-9dac-8d995e9d6dc2, read-only static review of the repair 5ddb32c5..9b7c0e3a with the range-diff against the base and the affected contracts. Covered: preparation, deletion, the heartbeat lifecycle, timing, status, and throughput through the worker and report callers; Settings through the set preparation; the README claims; and the assertions of the three test files. Verdict: FINDINGS, three, all repaired in 051dae25:
- P2 (new): the repaired loop-lag test installed
slow_caton the class withoutself, so_catraised before sleeping and the test passed on the heartbeat's last sample. Fixed withstaticmethodand an assertion that the trial succeeded (thread below). - P2:
shutil.rmtree(..., ignore_errors=True)hid a failed removal of the local files while the README promised that a failed removal fails the trial. The trial now propagates a local removal error like an S3 one (test_a_failed_local_removal_makes_the_trial_an_error), and the PR body names both. - P3: the filesystem suite's failed-check test asserted only
total_seconds. It now assertscpu_secondstoo, and a new test covers a failed transfer.
Validation:just benchmark test339/339/340 passed. A further narrow follow-up on9b7c0e3a..051dae25is running.
There was a problem hiding this comment.
Independent follow-up 2 (relayed): Codex CLI 0.164.0, model gpt-6-astra, session 01a16949-7ac5-7091-9fed-0102be115e3a, read-only static review of the repair 9b7c0e3a..051dae25 with the range-diff. Covered: the removal handling at the end of run_objects() (absent local directory, removal exceptions, connection closure, the final record), and the repaired tests in test_objects.py and test_filesystem.py, which it traced to reject the named regressions. Verdict: CLEAN.
| if keys: | ||
| response = s3.delete_objects(Bucket=bucket, Delete={"Objects": keys, "Quiet": True}) | ||
| if response.get("Errors"): | ||
| raise RuntimeError(str(response["Errors"])) |
There was a problem hiding this comment.
Repair of independent-review finding 1 (9b7c0e3a): a DeleteObjects response that names keys it did not delete raises here; run_objects() turns it into a trial error with Removal failed: ... while keeping the operation's measurement (test_a_failed_removal_makes_the_trial_an_error).
| timing: dict[str, float] = {} | ||
| query: dict[str, Any] | ||
| try: | ||
| prepare_trial(s3, case, files, targets, local, size) |
There was a problem hiding this comment.
Repair of independent-review finding 2 (9b7c0e3a): the preparation now runs inside the block whose finally removes the trial's objects and local files, so the copies of a partly prepared rm trial are deleted and the trial is an error (test_a_failed_preparation_is_an_error_whose_copies_are_removed).
| finally: | ||
| timing = { | ||
| "total_seconds": time.perf_counter() - start, | ||
| "cpu_seconds": time.process_time() - cpu_start, |
There was a problem hiding this comment.
Repair of independent-review finding 5 (9b7c0e3a): the operation's wall and CPU time are captured in this finally, so an operation that raises, or whose check raises, keeps both in its error record.
| finally: | ||
| timing = { | ||
| "total_seconds": time.perf_counter() - start, | ||
| "cpu_seconds": time.process_time() - cpu_start, |
There was a problem hiding this comment.
Repair of independent-review finding 5 (9b7c0e3a): the filesystem suite captures its transfer's timing the same way.
| raise ValueError(f"Invalid {name}") | ||
| # The sets are prepared with CopyObject requests, which copy up to 5 GiB. | ||
| if self.objects_size > 5 * 2**30: | ||
| raise ValueError("objects size must be at most 5 GiB") |
There was a problem hiding this comment.
Repair of independent-review finding 4 (9b7c0e3a): CopyObject copies at most 5 GiB, and the set's files are copies of the first object, so a larger size is rejected before any upload.
| expected = { | ||
| **({} if api == "sync" else {"batch_size": 8}), | ||
| "TaggingDirective": "REPLACE", | ||
| "AnnotationDirective": "EXCLUDE", |
There was a problem hiding this comment.
Repair of independent-review finding 7: the directives are asserted by their literal names and values, so changing COPY_DIRECTIVES fails this test.
| assert query["cpu_seconds"] >= 0 | ||
|
|
||
| def test_loop_lag_is_sampled_for_the_aio_api_only(self, s3, tmp_path, monkeypatch): | ||
| async def slow_cat(paths, **kwargs): |
There was a problem hiding this comment.
Repair of independent-review finding 8: the coroutine takes 50 ms under the default 10 ms heartbeat, and the test requires an observed sample.
There was a problem hiding this comment.
Independent follow-up P2, repaired in 051dae25: the slow coroutine is installed as a staticmethod, so fs._cat(paths, batch_size=8) runs it; the test asserts the trial's status is ok before counting the samples.
| s3, Case("s3", "sync", "rm", "objects", suite="objects"), paths, targets, tmp_path / "r", 4 | ||
| ) | ||
| # The copies run in threads, in any order. | ||
| assert sorted(s3.copies) == [ |
There was a problem hiding this comment.
Repair of independent-review finding 6: the copies run in threads, so the pairs are compared unordered here and in the rm trial test; the fake fails a named key rather than the first call, which raced between threads.
… test (GH-1317) The objects suite ignored the errors of removing a trial's local files while the README said a failed removal fails the trial; a failure now does. The loop lag test installed its slow coroutine without self, so the operation failed before sleeping and the test passed on the heartbeat's last sample; it now runs the coroutine and asserts the trial succeeded. The filesystem suite's tests assert the timing of a failed transfer and of a failed check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
WHAT
The benchmark project gains an
objectssuite: one batch operation over a prepared set of many S3 objects, through the three APIs of the filesystem suite,S3FileSystem(sync),AioS3FileSystem(aio), and s3fs (s3fs, in its environment).--transport:cat,get,put,pipe,copy,rm,find, andinfo. ThesyncAPI calls fsspec's batch methods, which run one file at a time; theaioAPI awaits the_cat,_get,_put,_pipe,_copy,_rm, and_findcoroutines on the trial's event loop with the case'sbatch_size; s3fs calls its sync wrappers with the samebatch_size.rmtakes no batch size, since both libraries delete in DeleteObjects requests of 1,000 keys.info, which fsspec has no batch for, fans out through a thread pool of the case's concurrency forsyncand throughfsspec.asyn._run_coros_in_chunksfor the other two.[objects] counts(100, 1,000, and 10,000 by default), each objectsizebytes (1 MiB). A set has itscountobjects under afilesprefix, the first uploaded and the rest copied with boto3 in parallel, andcountdirectories oflisting_keys_per_objectempty objects under alistingprefix, whichfindlists.prepare --objects-scaleuploads a set and records it underobject_setsin the manifest, counted with ListObjectsV2;runuploads the missing sets;validate_manifestkeeps them below the run's scratch prefix;cleanupremoves them with the run.[objects] concurrency(8 and 40) as the filesystem'smax_workersand connection pool and as the batch size. Case IDs areobjects-s3-<api>-<operation>-n<concurrency>; jobs are weighed by the object count, and the s3fs API's jobs are marked for its environment as in the filesystem suite.put; boto3 copies of the set forrm), measures the operation, then checks the outcome (bytes returned, local files, objects listed under the trial's prefix, keys found, or entries returned) and deletes what the trial created. Statuses:ok,count_mismatch,size_mismatch, orerror; a failed preparation, operation, check, or removal is an error, the objects of a failed trial are removed too, a removal that fails, a DeleteObjects that reports errors or local files that cannot be removed, fails the trial while keeping the measurement, and the wall and CPU time of a failed operation are kept. Copies nameTaggingDirective=REPLACEandAnnotationDirective=EXCLUDEwith every API. The object size is limited to 5 GiB, the most a CopyObject request copies.cpu_seconds; the summary gainsObjects/sandCPU (s)columns, and notes count mismatches.config.tomlgains the[objects]section.WHY
Refs #1317, part of #1315. The filesystem suite measures the per-file parallelism of one object; the batch operations over many objects are where the implementations differ most, and the measurement (#1318) needs them in the same suite, host, and selection. The synchronous
S3FileSystemruns fsspec's sequential loops for these operations, which the results are expected to show.TEST
Tested at the PR's head commit.
just benchmark lintandjust benchmark test: 339 passed, 2 skipped in.venvand.venv-polars2; 340 passed, 1 skipped in.venv-s3fs.just lintandmarkdownlint-cli2 benchmarks/README.md: clean..venv-s3fsagainst the CI bucket with a temporary script: a set of 5 objects of 64 KiB and 10 listing keys, andrun_objects()for every operation and API (24 trials); every trial returnedokwith the expected objects and bytes, created objects and local files were removed, and the set was deleted afterwards. ThesyncAPI took about twice the time of the other two even at this count, as expected from its sequential loops. Theput,rm, andcopytrials of the three APIs were repeated after the cleanup and timing repairs.prepare_object_sets()against a stack, which needs a manifest; its boto3 calls (one upload, parallelcopy_objectwith the directives, emptyput_object, ListObjectsV2 counts) are covered by a fake, and the samecopy_objectcall shape ran live in thermpreparation. A fleet run is Measure the filesystem suite against s3fs on c8gn.2xlarge #1318.🤖 Generated with Claude Code