Skip to content

Temporary workaround: do not probe object-storage listing keys as JSON - #2393

Merged
filimonov merged 1 commit into
antalya-26.6from
fix/antalya-26.6/no-json-probe-on-listing-keys
Sep 17, 2026
Merged

filimonov merged 1 commit into
antalya-26.6from
fix/antalya-26.6/no-json-probe-on-listing-keys

Conversation

@filimonov

Copy link
Copy Markdown
Member

RelativePathWithMetadata(String) builds a CommandInTaskResponse for every string it receives, which includes every key returned by listObjects / iterate of every object storage, not only the {"retry_after_us": N} answers of StorageObjectStorageStableTaskDistributor it exists for. Each ordinary key was parsed with Poco::JSON::Parser and threw one caught JSONException.

Under ASan this is a leak: the fake stack frame of Poco::JSON::ParserImpl::parseImpl that exits by exception is never collected (it is re-entered at the same stack depth and FakeStack::GC frees only frames strictly below the next allocation). One leaked frame per listed key per thread; once the size class holds 8192 frames every __asan_stack_malloc_1 scans all of them and every small function on that thread runs ~100x slower. On the amd_asan_ubsan, cas s3 storage lane, where CAS lists constantly, 27.7% of all CPU samples of a run sit in that scan and 02435_rollback_cancelled_queries sorts 1M rows in 200 s instead of 2 s. Full analysis with the CI numbers, the live-server fake-stack dump and a 40-line standalone reproduction is in the linked issue.

This is a temporary workaround: only strings that start with { after whitespace are parsed now. Object keys never do, the distributor answer always does, so the swarm protocol is unchanged. The proper fix is to stop sending the command in the path field (a separate ObjectInfo kind or the versioned cluster-function protocol, the direction of #1360), after which this probe can be removed.

Related: #2362
Related: #1360

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):

Object-storage listings no longer try to parse every listed key as a JSON swarm command; this removes one thrown-and-caught exception per listed key on every object storage and the ASan fake-stack leak that made the CAS-S3 sanitizer lane up to 100x slower.

Documentation entry for user-facing changes

  • Documentation is written (mandatory for new features)

CI/CD Options

Exclude tests:

  • Fast test
  • Integration Tests
  • Stateless tests
  • Stateful tests
  • Unit tests
  • Performance tests
  • Aarch64 tests
  • All with ASAN
  • All with TSAN
  • All with MSAN
  • All with UBSAN
  • All with Coverage
  • All Regression
  • Disable CI Cache

Regression jobs to run:

  • Fast suites (mostly <1h)
  • Aggregate Functions (2h)
  • Alter (1.5h)
  • Benchmark (30m)
  • CAS (content-addressed storage; Antalya only)
  • ClickHouse Keeper (1h)
  • Iceberg (2h)
  • LDAP (1h)
  • OAuth (5m)
  • Parquet (1.5h)
  • RBAC (1.5h)
  • SSL Server (1h)
  • S3 (2h)
  • S3 Export (2h)
  • Swarms (30m)
  • Tiered Storage (2h)

🤖 Generated with Claude Code

`RelativePathWithMetadata(String)` builds a `CommandInTaskResponse` for every
string it receives, which includes every key returned by `listObjects` /
`iterate` of every object storage, not only the `{"retry_after_us": N}`
answers of `StorageObjectStorageStableTaskDistributor` it exists for. Each
ordinary key was parsed with `Poco::JSON::Parser` and threw one caught
`JSONException`.

Under ASan this is a leak: the fake stack frame of
`Poco::JSON::ParserImpl::parseImpl` that exits by exception is never
collected (it is re-entered at the same stack depth and `FakeStack::GC`
frees only frames strictly below the next allocation). One leaked frame per
listed key per thread; once the size class holds 8192 frames every
`__asan_stack_malloc_1` scans all of them and every small function on that
thread runs ~100x slower. On the `amd_asan_ubsan, cas s3 storage` lane,
where CAS lists constantly, 27.7% of all CPU samples of a run sit in that
scan and `02435_rollback_cancelled_queries` sorts 1M rows in 200 s instead
of 2 s.

Only strings that start with `{` after whitespace are parsed now; object
keys never do, the distributor answer always does. The proper fix is to stop
sending the command in the path field (a separate `ObjectInfo` kind or the
versioned cluster-function protocol, the direction of
#1360), after which this probe
can be removed.

Related: #2362

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Workflow [PR], commit [2083e59]

@ilejn ilejn 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.

Don't get ASAN part of the problem.
As a workaround is fine anyway.

@filimonov

Copy link
Copy Markdown
Member Author

CI on this PR (run 35120487077, ASan CAS-S3 lane 2/2): 02435_rollback_cancelled_queries passes in 147 s. Before the fix a single sort inside one insert of this test took ~205 s and the test timed out.

For scale, in the same run: ASan without S3 = 66 s, release binary with CAS-S3 = 58 s, release binary without S3 = 27 s. The remaining ASan CAS-S3 / ASan ratio (2.2x) matches the S3 / no-S3 ratio of the non-sanitized builds, so what is left is S3 insert cost, not the fake-stack scan.

@filimonov
filimonov merged commit 66cb75e into antalya-26.6 Sep 17, 2026
363 of 381 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants