Status: shipped 2026-06-14
Branch: master
Commits: see git log --oneline | grep M6
This milestone completes the dedupe work started in M0. The
fixed 60s TTL is now a sliding window that refreshes on every
duplicate observation. Duplicates propagate through the rate
limit for free (no token burned). The dedupe_count is
displayed in delivery messages as an inline (×N) suffix on
the title. Per-source max-observed and a counter for "how
loud is the dupe noise" are exposed as Prometheus metrics.
| Area | Change |
|---|---|
| Algorithm | internal/dedupe/dedupe.go: single-Lua sliding-window script (was 2-RT SETNX+INCR) |
| TTL | BA_INGESTD_DEDUPE_TTL_SECONDS (default 300s, was 60s) |
| Rate limit | Dedupe runs BEFORE rate limit; only isNew=true burns a token in BOTH per-source and per-company buckets |
| Display | (×N) suffix on Telegram message title and FCM notification title when dedupe_count > 1 |
| FCM data | New data.dedupe_count field for native clients to render |
| Metrics | ba_ingestd_dedupe_collapsed_total{source} (counter) and ba_ingestd_dedupe_count_max_observed{source} (gauge) |
| Loadgen | All three loadgens (http/mqtt/ws) gain --dedupe-key to force a specific key (used by the ×N smoke) |
| Var | Default | Purpose |
|---|---|---|
BA_INGESTD_DEDUPE_TTL_SECONDS |
300 | sliding-window TTL for a dedupe entry |
internal/dedupe/dedupe.go # sliding-window Lua (was SETNX+INCR)
internal/dedupe/dedupe_test.go # 6 tests: first/dup, isolation, empty-key,
# window expires, sliding keeps alive, 1000-burst
internal/observability/maxseen.go # sync.Map[source]uint32 monotonic max tracker
internal/observability/maxseen_test.go # 5 tests incl. concurrent CAS
internal/observability/metrics.go # +2 metrics: dedupe_collapsed, dedupe_count_max
cmd/ingestd/process.go # dedupe before rate limit, only charge new
cmd/ingestd/main.go # wires MaxSeen into processDeps
cmd/deliverd-fcm/main.go # (×N) in notification title; data.dedupe_count
cmd/deliverd-telegram/main.go # (×N) in title
loadgen/cmd/{http,mqtt,ws}/main.go # --dedupe-key flag on all three
internal/config/config.go # DedupeTTLSeconds
docker-compose.yml # BA_INGESTD_DEDUPE_TTL_SECONDS=300
.env.example # BA_INGESTD_DEDUPE_TTL_SECONDS=300
-- KEYS[1] = dedupe:{source_id}:{dedupe_key}
-- ARGV[1] = window in seconds (float; sub-second rounded up
-- so test windows like 500ms still work)
local ttl = tonumber(ARGV[1])
if ttl == nil or ttl <= 0 then
return redis.error_reply("dedupe: invalid ttl")
end
local ttlInt = math.ceil(ttl)
if ttlInt < 1 then ttlInt = 1 end
local set = redis.call("SET", KEYS[1], 1, "NX", "EX", ttlInt)
if set then
return {1, 1} -- first arrival, isNew=1, count=1
end
local n = redis.call("INCR", KEYS[1])
redis.call("EXPIRE", KEYS[1], ttlInt) -- <-- THE SLIDING PART
return {0, n} -- duplicate, isNew=0, count=n
The EXPIRE refresh on every duplicate is what makes the
window slide. A steady stream of duplicates keeps the key
alive indefinitely; a pause longer than the window expires
the entry, and the next arrival is "new" again with count=1.
Send 5 alerts with the same --dedupe-key. The faketgmd
sent-log shows 5 distinct alert_ids, each with the dedupe
_count climbing 1, 2, 3, 4, 5. The 5th message text contains
the inline (×5) suffix on the title.
Asserted: faketgmd's last message text contains (×5).
The 5-burst from Step 2 should also tick:
ba_ingestd_dedupe_collapsed_total{source="prom-prod"} +4
(one new + four dupes)ba_ingestd_dedupe_count_max_observed{source="prom-prod"} = 5Asserted: collapsed delta ≥ 4 and max ≥ 5.
A new dedupe_key (timestamped) has no prior entry, so all 3
alerts are accepted as isNew=true. ws_messages_total{result="accepted"} +3.
Asserted: received=3, accepted=3.
The default per-source rate limit is 100/s. We send 600
alerts with the same dedupe_key at 300/s for 2s. The first
alert is new and burns 1 token. The other 599 are dupes
and do not burn any token. We assert that
rate_limit_hits_total{scope="source"} did NOT tick,
proving the rate limit gate is "free" for duplicates.
Asserted: rate_limit_hits_total{scope="source"} delta = 0; accepted delta ≥ 500.
Same dedupe_key from acme (prom-prod) and globex
(grafana) does NOT collide. After acme sends 3 and globex
sends 3 with the same key, each source's max_observed
gauge reads 3.
Asserted: grafana max ≥ 3, prom-prod max ≥ 1.
cd /root/.openclaw/workspace/broad-announce
bash scripts/m6_smoke.sh
Exit code = number of failed checks (0 on success).
# Send 5 alerts with the same dedupe_key, see the (×5) suffix.
for i in 1 2 3 4 5; do
BODY='{"company_id":"acme-001","source_id":"prom-prod","severity":"critical","title":"curl-burst","dedupe_key":"curl-burst-1"}'
SIG="t=$(date +%s),v1=$(echo -n "$BODY" | openssl dgst -sha256 -hmac s3cret-acme | awk '{print $2}')"
curl -sS -X POST http://localhost:8800/v1/ingest \
-H "X-BA-Key: acme-001:prom-prod:s3cret-acme" \
-H "X-BA-Signature: $SIG" \
-H "Content-Type: application/json" \
-d "$BODY"
echo
done
# Then check the live tail or the faketgmd /admin/sent endpoint.
dedupe_count in the title.
A "true single message per burst" requires the router to
collapse on dedupe_key and only emit one delivery per
source. That's M6.5 (the M6 spec hinted at it but it's
bigger than the dedupe rewrite itself).max_concurrent_connections —
the migration added the column but the M6 path doesn't yet
read it. The default 32 from the env still applies.