fix(ingestor): warn, throttled and bounded, when observerIATAWhitelist drops a region - #134
Conversation
handleMessage drops a message whose topic region is not in observerIATAWhitelist without logging anything, so a legitimate region missing from the list loses all its traffic unnoticed. iata_drop_log_test.go drives the real handleMessage and reads the log: one warning per dropped region (normalized code, names the setting), none for repeats, none for allowed traffic or an empty whitelist, one escaped and bounded line for a hostile region segment, and a bounded number of lines, including a shared overflow warning, for thousands of distinct regions. On master the four warning cases fail (0 lines); the allowed/empty control passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TcZHooUiiknVWbECVWzk8
…t drops a region (#110) A message whose topic region is not in observerIATAWhitelist is now logged once per region and re-logged at most every iataWarnIntervalSec (default 6h) while that region keeps arriving: MQTT [src] [region-filter] dropping region "GOT": not in observerIATAWhitelist; further messages from this region suppressed for 6h0m0s The region is a topic segment the publisher controls, so: - the per-region state is bounded to 512 keys of at most 32 bytes; - beyond that, drops share one overflow warning with the same interval (never silent); - entries older than the interval are reclaimed when the table is full, so the first codes seen cannot own it; - a sweep runs only when an entry can have expired (a lower bound on the oldest entry), so a hostile feed at the cap costs O(1) per drop: about 83 ns/op, against 13-24 us/op with a sweep on every drop; - the code is quoted with %q and truncated, so it cannot forge or stretch log lines. Allowed traffic and an empty whitelist are unchanged. The optional key is documented in config.example.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TcZHooUiiknVWbECVWzk8
Independent review of
|
| Criterion | Result |
|---|---|
| Fixed, grep-friendly warning with the normalized code | Met [F]: [region-filter] dropping region "GOT"; whitelist check and warning use the same ToUpper(TrimSpace) normalization |
| Throttled per code, configurable interval, safe default | Met [F]: tests, and mutants M7 and M9 are caught |
| Per-code state strictly bounded | Met [F]: 512 keys of at most 32 bytes; mutants M1 and M5 are caught |
| Beyond the cap: bounded, shared overflow warning | Met in code [F]. Tests cover the first overflow line, but not its escaping (finding 2) or re-arming (finding 3) |
| Expired entries reclaimed | Met [F]: mutant M3 is caught. The amortization bound is not covered across intervals (finding 1) |
| Allowed traffic and an empty whitelist unchanged | Met [F]: the warning only runs on the existing reject branch (main.go:687); TestIATAWhitelistAllowedAndEmptyAreSilent and the existing whitelist tests pass |
| Tests for throttling, expiry, more attacker values than the cap, overflow, reclamation | Met, with the gaps above |
Ingestor suite with -race |
Met [F]: go test -race -count=1 ./... gives ok github.com/corescope/ingestor 420.080s, 0 data races |
| Optional key documented | Met [F]: config.example.json |
Test-first and mutants
- [F] Commit A: 4 of the 5 new integration tests fail ("got 0" lines); the allowed/empty control passes. Head: all 21 IATA tests pass.
- [F] Twelve mutants, nine caught:
| Mutant | Result |
|---|---|
| M1 no cap | 4 fail |
| M2 overflow silent | 3 fail |
| M3 no reclamation | 2 fail |
M4 %s in the per-region line |
2 fail |
M4b %s in the overflow line |
survives (finding 2) |
| M5 no truncation | 2 fail |
M6 main.go does not call the warning |
4 fail |
| M7 no per-region throttle | 5 fail |
M8 oldest not refreshed after a sweep |
survives (finding 1) |
| M9 interval ignores config | 2 fail |
| M10 overflow never re-arms | survives (finding 3) |
Performance
- [F]
BenchmarkIATADropThrottleHostileAtCap: 27.3 / 27.5 / 28.0 ns/op, 0 B/op, 0 allocs/op (12-core arm64, so faster than the PR's 4-core numbers). This confirms O(1) per drop within one interval. - [F] Across intervals the sweep is amortized, not O(1): 1,024 sweeps per 30,000 hostile drops in the probe above. Each sweep removes at least the expired oldest entry, so the worst case is on the order of 512 sweeps × 512 entries per interval. That is harmless at a 6 h default.
- [F] Allowed traffic pays nothing:
time.Now()and the throttle run only on the reject branch.
Security and invariants
- [F] Log injection: both lines use
%q. The per-region line is tested; the overflow line is not (finding 2). - [F] Bounded structures:
map[string]time.Timewith at most 512 keys of at most 32 bytes. No newmap[string]interface{}. - [F] The change is only in
cmd/ingestor/.cmd/serveris untouched, so themode=roinvariant holds. - [F]
go vetis clean;gofmt -lon the touched files is clean.
Not verified
- A live broker, a real foreign feed and staging. The PR's "Not verified" list says the same, and it is honest.
- The effect of config hot-reload (if any) on the throttle state. I did not look for a reload path [A].
|
I found one reproducible issue that should be fixed before merge: [P2] A large positive
Please validate before multiplication and either reject the value, fall back to the default, or clamp it to the maximum representable duration. A regression test should load the large value through Everything else I checked was clean: the current merge result is conflict-free; the focused IATA tests pass normally and 5x under |
…118) master (#134, iata_drop_log_test.go) already declares captureLog, so the package test build failed on the PR's merge with master. The #118 helper is now captureLog118; no behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FcyXW5RdFzLZhuL1ntAsY
Relates to #110
Summary
Until now, a message whose MQTT topic region was not in
observerIATAWhitelistwas dropped without a trace. A legitimate region that was left off the list lost all its traffic and nothing appeared in the log.The ingestor now writes one grep-friendly line per dropped region:
iataWarnIntervalSec(default 6 h).Upstream reference (read only):
Kpa-clawbot/CoreScope#2067.Plan and design
Autonomous run, so the plan is written here instead of waiting for approval (AGENTS.md rule 5).
9e68cd44).iata_drop_log_test.godrives the realhandleMessageand reads the log it writes. It is red on master.d9f4c328).cmd/ingestor/iata_drop_warn.go(new)%q, so control characters cannot break or forge log lines.Wiring and configuration
config.go. New optionaliataWarnIntervalSec. A value of 0 or less means the 6 h default. The throttle state is unexported.main.go. On the existing reject branch only,cfg.warnIATADrop(tag, parts[1], time.Now())is called beforereturn. Allowed traffic and an empty whitelist take exactly the same path as before.config.example.json. Documents the key and the log line.Where the fork differs from upstream
%s. Here it is quoted with%q.%.0fh), which prints "0h" for a 90-second interval. Here it is printed as a duration.Config and customizer (AGENTS.md rule 8).
iataWarnIntervalSecis an ingestor setting, not a UI value, so it has no customizer counterpart.Acceptance criteria
TestIATAWhitelistDropIsLoggedOncePerRegion("GOT",observerIATAWhitelist,[region-filter])TestIATADropThrottlePerRegionAndExpiry,TestIATAWarnIntervalDefaultAndConfigured,TestIATAWarnIntervalFromJSONTestIATADropThrottleIsBoundedWithSharedOverflow(20,000 distinct codes),TestNormalizeIATAForWarnTestIATAWhitelistDropManyDistinctRegionsStaysBoundedAndVisible(3,000 regions throughhandleMessage)TestIATADropThrottleReclaimsExpiredSlots; sweeps amortized:TestIATADropThrottleSweepsAreAmortizedTestIATAWhitelistAllowedAndEmptyAreSilent; the existingTestHandleMessageObserverIATAWhitelistand IATA filter tests still passTestIATADropThrottleConcurrent(16 goroutines)-raceconfig.example.jsonTests
Reproduction
go test -run TestIATAWhitelist ./cmd/ingestoron commit9e68cd44: the 4 warning cases fail with "got 0" lines. The allowed/empty control passes.IATAtests pass (21 including the existing ones).Full suite with the race detector
cd cmd/ingestor && go test -race -count=1 -timeout 60m ./....ok github.com/corescope/ingestor 1030.112s, no data races.-raceon this 4-core sandbox:TestNeighborEdgesBuilderDeltaScanalone ran for 4 minutes. CI runs the ingestor without-race, with-timeout 20m.Mutation checks (each run through the IATA tests, then restored)
%sinstead of%qPerformance (
BenchmarkIATADropThrottleHostileAtCap, a new region on every drop with the table full, 4 cores)Allowed traffic pays nothing: the warning runs only on the reject branch.
Static checks.
go vet ./...is clean, andgofmt -lis clean on the touched files. Other ingestor files that are not gofmt-clean were so already.Not verified
Overlap with my other open PRs
cmd/ingestor/config.go,cmd/ingestor/main.goandconfig.example.json.cmd/ingestor/config.goandconfig.example.json, in different places.🤖 Generated with Claude Code
https://claude.ai/code/session_019TcZHooUiiknVWbECVWzk8
Generated by Claude Code