Skip to content

fix: address CodeRabbit findings from the release PR review - #798

Merged
NotYuSheng merged 1 commit into
devfrom
fix/coderabbit-review-795
Aug 20, 2026
Merged

fix: address CodeRabbit findings from the release PR review#798
NotYuSheng merged 1 commit into
devfrom
fix/coderabbit-review-795

Conversation

@NotYuSheng

Copy link
Copy Markdown
Owner

Summary

CodeRabbit reviewed PR #795 (the dev -> main release PR) and left 8 comments. Verified each against the current code before acting — not all of them held up:

  • False positive, no change: AnalysisService.java — CodeRabbit wanted the Suricata progress-bar weighting to honor the global kill-switch, but the code's own comment already documents that as an intentional tradeoff ("the per-file flag, not the effective state... being wrong costs a slightly misshapen bar" — cosmetic only).
  • Real, but deferred: PcapParserService.java's unbounded per-parse string pool. Genuine edge case (high-cardinality captures could make memory usage worse, not better), but the right fix needs a benchmarked capacity choice and a dedicated test, not a same-release patch. Filed as PcapParserService's per-parse string pool has no capacity bound #797.
  • Real and fixed here (4 findings):
    • SuricataEngine.java: clear the warm flag when the control-socket check loses contact, not just when the process exits — a hung-but-alive daemon no longer keeps reporting warm to later captures.
    • FileMapper.java: the size-based ETA fallback was missing the cold-engine ruleset-build cost the packet-count path already includes.
    • AnalysisEtaTest.java: tightened >= 2s to >= 10s to actually match MIN_ESTIMATE_SECONDS.
    • docs/configuration/environment-variables.rst + README.md: corrected the stale 25%/512MB upload-cap figure to the real 16%/327MB (default budget) everywhere it was quoted — independently caught by CodeRabbit; matches a doc/code mismatch already flagged during fix: raise Jackson's 20MB JSON string cap for report topology diagrams #793's work.
  • In my own diff from fix: raise Jackson's 20MB JSON string cap for report topology diagrams #793 (2 findings, both in check_env_passthrough.py): documented_vars() was ignoring its own path argument (Ruff flagged the unused arg too); the undocumented-var check only validated the prod compose stack, missing offline/offline-prod.

Test plan

  • mvn test -Dtest=SuricataEngineTest,AnalysisEtaTest — 14/14 passing.
  • python3 scripts/check_env_passthrough.py — still passes, now actually exercising the fixed code paths.
  • python3 -m py_compile scripts/check_env_passthrough.py — syntax OK.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QDyaaPSRNzNXRzJoS794C4

Verified each finding against the current code before applying (one, the
Suricata kill-switch weighting in AnalysisService, was a false positive —
the code's own comment documents that tradeoff as intentional and cosmetic;
left unchanged). Fixing the rest:

- SuricataEngine: clear the warm-engine flag when the control-socket check
  loses contact, not just when the daemon process itself exits, so a hung
  daemon doesn't keep reporting warm to later captures.
- FileMapper: the size-based ETA fallback was missing the one-time
  cold-engine ruleset-build cost the packet-count path already accounts
  for, under-estimating ETA when capinfos fails or packet count is
  otherwise unknown.
- AnalysisEtaTest: tightened a floor assertion from >=2s to >=10s to
  actually match FileMapper.MIN_ESTIMATE_SECONDS.
- docs/README: corrected the upload-cap figure (16% / 327MB at the
  default budget, not the stale 25% / 512MB) in every place it was
  quoted — same mismatch flagged independently during the report-fix PR.
- check_env_passthrough.py: documented_vars() now actually uses its path
  argument (resolved from the repo root) instead of silently ignoring it;
  the undocumented-var check now covers the union of all compose stacks
  instead of only "prod".

Deferred as a follow-up rather than rushed here: PcapParserService's
unbounded string pool (real edge case on high-cardinality captures, but
needs a proper capacity choice backed by a benchmark and a dedicated test,
not a same-release patch).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QDyaaPSRNzNXRzJoS794C4
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e420892d-4d11-4119-8487-970e00360825

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@NotYuSheng
NotYuSheng merged commit 1fe6984 into dev Aug 20, 2026
9 checks passed
@NotYuSheng
NotYuSheng deleted the fix/coderabbit-review-795 branch August 20, 2026 11:45
NotYuSheng added a commit that referenced this pull request Aug 20, 2026
…xes, and accumulated dev fixes (#795)

* chore: sync app version to v0.1.1-338-g2ded9ff (#774)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* docs: record why dev is protected (it was deleted by its own release) (#775)

The repository has "Automatically delete head branches" enabled, which
is what stops merged feature branches piling up. A release PR's head
branch is dev — so merging the first dev -> main release deleted dev.

Nothing was lost: main and dev are identical at exactly that moment, so
recreating dev from main restored it exactly. But any open PR based on
dev would have been orphaned, and on a normal day that is most of them.

dev now carries allow_deletions: false, matching main. GitHub will not
delete a protected branch, so the repo setting keeps tidying feature
branches and cannot touch either long-lived one. The
delete-branch-on-close workflow already treats a 422 as "protected,
leave it", so the protection is the exclusion list and no code changes.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* chore: sync app version to v0.1.1-340-g0a30809 (#776)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix: weight the progress bar from measurement, not estimate (#758) (#777)

The bar reached 23% almost immediately, sat there for 44 of a 47-second
analysis, then swept to 100% in under two. A bar motionless for most of
a job teaches people to ignore it, and an operator who cannot tell
"working" from "hung" kills a job that was fine.

The weights were a plausible guess: "Detecting applications & threats"
at 35% of the job when it was 95%, DB writes at 20% when they were 0.6%.

Two profiles now, because one vector cannot describe both. Suricata's
detection engine costs ~45s to build and ~0.3s to use (#759), so the
first capture after a restart has a completely different shape from
every capture after it. SuricataEngine.isWarm() says which we are in.

Measured shares from real runs rather than estimates:

  cold: detection 90% of the plan (measured 94.7%)
  warm: parse 21, detect 21, classify 30, extract-files 22
        (measured 21.0 / 21.0 / 29.6 / 22.2)

Verified live against the running stack. Cold now reads 5% and
"Building threat-detection ruleset (first run)" rather than 23% and
silence; warm climbs 3 -> 24 -> 44 -> 78 in step with the work.

The cold stage is still motionless for ~45s, because the bar cannot
advance inside a stage. Naming the wait as one-time setup is what
separates "hung" from "working on something known to be slow", and is
the honest fix short of reporting sub-stage progress.

Still approximate at the extremes: parsing and DB writes scale with the
capture while the rest does not, so these shares are right for a
mid-sized capture. Re-weighting from the packet count after parsing is
the next step and is deliberately not attempted here.

Verified by mutation: collapsing back to one vector fails 2, zeroing a
stage's weight fails 1.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* chore: sync app version to v0.1.1-342-g399628a (#778)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix: make the upload cap fit the heap that has to parse it (#779) (#780)

Stage 2 holds the whole capture in heap — every conversation, every
packet, payloads included — and releases nothing until the database
insert in stage 6. So the largest file we can accept is bounded by the
JVM heap.

Those two numbers were derived separately and nothing connected them.
#92 set the upload cap at 25% of the memory budget when the heap was
75%: a capture could be at most a third of the heap. #586 lowered the
heap to 50% to make room for native subprocesses and did not revisit the
cap, so the margin fell from 3x to 2x. A 468MB capture was then accepted
and died of OutOfMemoryError 25 minutes into parsing, taking the HTTP
poller thread with it — the container stayed "running" while serving
nothing.

Two definitions of one limit, drifting, with nothing checking their
relationship. The same shape as everything in #733.

  upload cap  25% -> 16% of the budget (327MB at the 2048MB default)
  FileServiceImpl stops hardcoding 500MB and reads app.max-file-size,
    so the number /system/limits shows the user and the number actually
    enforced are the same number

scripts/check_memory_budget.py asserts the relationship and fails the
build on any of the three ways it broke: widening the cap, shrinking the
heap, or re-hardcoding the limit. Verified by planting each.

This does not raise capacity. A 468MB capture is still beyond this
deployment — it is now refused in seconds with 413 rather than accepted
and failed after 25 minutes of work. Raising APP_MEMORY_MB raises both
numbers together; making capture size independent of heap needs stage 2
to stream, which stays open on #779.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* chore: sync app version to v0.1.1-344-g0395fe3 (#781)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix: give the analysis ETA a fixed-cost term (#758 follow-on) (#782)

The estimate was seconds-per-packet with no constant. That reads 91
minutes for a job of roughly 36, and 10 seconds for one that takes 49 —
the same missing term, in opposite directions, which is why it looked
like two unrelated bugs.

Measured cost per 1000 packets falls ~8x between a 250-packet capture
and a 45,000-packet one. That is a fixed cost being amortised as if it
scaled.

  seconds = fixed + per_kpkt * (packets / 1000)

Fitted by scripts/calibrate_analysis_eta.py over runs of 2k-45k packets,
which also learned to exclude cold-engine runs: one of those dragged the
detection term's fixed cost to 22s and flattened its slope to zero,
because a ruleset build is a different regime, not an outlier.

  predicted   4s /  9s / 20s / 43s
  measured  3.4s / 8.5s / 17.3s / 44.0s

Suricata's ~45s engine build is a separate one-time term, so a capture
that pays it is estimated as the different job it is — the same
cold/warm split the progress bar makes.

The port for that lives in common.stage rather than analysis.spi:
analysis already depends on file, so putting it there would close the
analysis <-> file cycle #512 slice 1 was written to break. ArchUnit
caught that; both modules depend on the port instead, as with
common.net.LocalityPolicy.

The old size-based path stays as a fallback for files with no packet
count, deliberately cruder — bytes are a poor proxy when a packet-dense
capture costs far more than its size suggests.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* chore: sync app version to v0.1.1-346-ga91ca51 (#783)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* perf: share the strings every packet repeats (#779) (#784)

A capture has a few hundred distinct addresses and a handful of
protocols, but millions of packets, and split() returns a fresh String
for every field of every packet. At ~776 bytes per PacketInfo, 1.7M
packets need ~1.26 GB against a 1 GB heap — which is exactly the
OutOfMemoryError that made a 468MB capture unanalysable.

Addresses and protocols are ~150 of those bytes and are almost entirely
duplicates. They now come from a per-parse pool.

A local map rather than String.intern(): intern's table is JVM-wide and
permanent, so a capture's addresses would outlive the analysis that read
them. This is discarded with the parse.

HONEST LIMITS. This raises the ceiling, it does not remove it: the
per-packet object count is unchanged and memory still scales with the
capture. I have not measured the end-to-end saving — doing so needs an
A/B on the same file with and without, and the shared stack was in use.
The unit tests verify the objects are genuinely shared (by identity, not
equality, since equal-but-distinct is precisely the waste); the size of
the win on a real capture is unmeasured.

The structural fix is streaming stage 2 to the database instead of
accumulating, which is written up on #779 and stays open.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* chore: sync app version to v0.1.1-348-g183d380 (#785)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* ci: run the tool-dependent tests for real, and notice when they cannot (#701) (#786)

The 4-point coverage gap between CI and a developer's machine is not a
measurement artifact. Total instructions are identical in both (64,663),
so it is not Lombok filtering, not excludes, not a stale target/ — the
same classes are analysed and 2,135 more simply execute locally.

They are all classes that shell out to a tool: PcapParserService (+739),
AnalysisService (+350), WebServerLogExtractor (+282),
DnsQueryLogExtractor (+255), HostnameResolverService (+248). This host
has tshark; ubuntu-latest does not.

Those services degrade gracefully by design, so a missing binary is not
an error: the subprocess fails to start, the catch fires, the method
returns early, and every assertion still holds. DnsQueryLogExtractorTest
reports 13 passed with tshark working and 13 passed with it replaced by
a shim that exits 127 — it would pass if the parsing were deleted.

So the tests were not skipped in CI, they were hollow, which is why the
counts matched exactly and only coverage disagreed.

CI now installs tshark, and ExternalToolsAvailableTest asserts the
precondition rather than leaving a future absence silent. Suricata is
deliberately not installed: ~700MB of ruleset and ~45s of engine build
per run (#569), already covered by the full-stack job.

A note on method: my first attempt to demonstrate this stripped PATH,
which proved nothing — /bin is a symlink to /usr/bin, so tshark stayed
reachable and both runs were identical for the wrong reason. The shim is
what actually shadows it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* chore: sync app version to v0.1.1-350-gd5c4583 (#787)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* docs: document every env var, and gate the third edge (#779 follow-on) (#788)

.env.example still promised "Max upload size = 25% of the effective
budget" and "2048 -> 512 MB max upload". #780 changed that to 16% today,
so the file was telling operators they could upload 512MB while the app
enforced 327MB — the same advertised-versus-enforced split that made a
468MB capture fail 25 minutes in.

Corrected, with the reason recorded rather than just the number.

Eleven variables compose passes were undocumented, including every one I
added today: the warm Suricata engine's five, LLM_TOOL_CALLING, the
cluster tuning knobs and the DNS/HTTP suspicion thresholds. A setting
nobody knows about is not configurable.

check_env_passthrough.py already checked compose against Spring config in
both directions. It now checks the third edge — a variable can be wired
end to end and still be invisible, because .env.example is the only place
an operator looks. Verified by planting a new compose variable and
watching it fail.

Exempt: DATABASE_*, MINIO_* and TZ, which compose derives from variables
that ARE documented (DATABASE_URL from POSTGRES_DB, MINIO_ACCESS_KEY from
MINIO_ROOT_USER). Documenting the derived name would invite someone to
set it and wonder why the source still won.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* chore: sync app version to v0.1.1-352-gf89e142 (#789)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix: raise Jackson's 20MB JSON string cap for report topology diagrams (#793)

* fix: raise Jackson's 20MB JSON string cap for report topology diagrams (#792)

Report generation embeds the topology diagram as a base64-encoded PNG in
the JSON request body; a dense enough network topology pushes that string
past Jackson's default 20MB max-string-length, rejecting the request
before it reaches the controller. Cap now scales with APP_MEMORY_MB
(2.5% of the effective budget, clamped to 8-256MB) like the existing
upload-size/timeout derivation in docker-entrypoint.sh, instead of a flat
default that's disproportionate on small/large deployments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QDyaaPSRNzNXRzJoS794C4

* fix: sync Jackson max-string-length fallback with the entrypoint's derived default

Code review on #793 caught that application.yml's fallback (52428800 = 50MB,
used only when docker-entrypoint.sh isn't in the picture — mvn spring-boot:run,
IDE runs, tests) didn't match what the entrypoint actually derives at the
documented default (APP_MEMORY_MB=2048 -> 51MB = 53477376 bytes), so a
bypass-path run silently behaved differently from the real deployment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QDyaaPSRNzNXRzJoS794C4

* ci: exempt JACKSON_MAX_STRING_LENGTH from the compose passthrough check

Same as MAX_UPLOAD_SIZE_BYTES/ANALYSIS_TIMEOUT_SECONDS: it's passed as a JVM
-D system property by backend/docker-entrypoint.sh, derived from the memory
budget, not set in compose's services.backend.environment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QDyaaPSRNzNXRzJoS794C4

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* chore: sync app version to v0.1.1-354-g153560c (#794)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore: sync app version to v0.1.1-358-g074a635 (#796)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix: address CodeRabbit findings from the release PR review (#798)

Verified each finding against the current code before applying (one, the
Suricata kill-switch weighting in AnalysisService, was a false positive —
the code's own comment documents that tradeoff as intentional and cosmetic;
left unchanged). Fixing the rest:

- SuricataEngine: clear the warm-engine flag when the control-socket check
  loses contact, not just when the daemon process itself exits, so a hung
  daemon doesn't keep reporting warm to later captures.
- FileMapper: the size-based ETA fallback was missing the one-time
  cold-engine ruleset-build cost the packet-count path already accounts
  for, under-estimating ETA when capinfos fails or packet count is
  otherwise unknown.
- AnalysisEtaTest: tightened a floor assertion from >=2s to >=10s to
  actually match FileMapper.MIN_ESTIMATE_SECONDS.
- docs/README: corrected the upload-cap figure (16% / 327MB at the
  default budget, not the stale 25% / 512MB) in every place it was
  quoted — same mismatch flagged independently during the report-fix PR.
- check_env_passthrough.py: documented_vars() now actually uses its path
  argument (resolved from the repo root) instead of silently ignoring it;
  the undocumented-var check now covers the union of all compose stacks
  instead of only "prod".

Deferred as a follow-up rather than rushed here: PcapParserService's
unbounded string pool (real edge case on high-cardinality captures, but
needs a proper capacity choice backed by a benchmark and a dedicated test,
not a same-release patch).


Claude-Session: https://claude.ai/code/session_01QDyaaPSRNzNXRzJoS794C4

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* chore: sync app version to v0.1.1-360-g1fe6984 (#799)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant