feat: dynamic include-values source for the keep-filter#107
Merged
Conversation
The keep-filter's allowlist can now be sourced at runtime from an HTTP
endpoint (JSON array of scalars) instead of being fixed at startup —
MILLPOND_INCLUDE_VALUES_URL + mode 'shadow' (static stays authoritative,
endpoint observed for diff metrics) or 'authoritative' (polled set live,
static as bootstrap seed). build(cfg) returns one source object; the
filter reads current() per batch. Millpond stays endpoint-agnostic: the
URL and an optional auth header are plain config.
The safety semantics follow the consequence asymmetry (an erroneous
addition writes surplus rows; an erroneous removal silently drops
records with no recovery):
- additions apply on first sight; removals need N consecutive
successful ACCEPTED polls absent — failed and refused polls advance
nothing (a refusal must never pre-charge the removal countdown)
- poll failures keep the last-known-good set; staleness is a gauge
- refusal guards keep the whole poll out: empty arrays (never removal
evidence, never an acceptable first state), bulk removals (> half a
multi-value set at once), and int<->str type flips (a flipped set
fails the filter's values->column cast and drops whole batches)
- authoritative startup BLOCKS until the first successful poll — the
damping state is in-memory, so proceeding on a stale static bootstrap
after a restart would perform de-facto removals with zero evidence;
a halt is recoverable from Kafka, a silent drop is not
- millpond_include_values_mode{mode} reports what each replica actually
runs so a fleet-level shadow->authoritative flip gate cannot pass
vacuously on a replica that never got the URL
_fetch refuses redirects (urllib re-sends the auth header cross-host),
caps responses at 4MB, jitters polls +/-10%. Config validation refuses
URL-without-keep-filter, MODE/auth-without-URL, lone auth halves.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The keep-filter's allowlist can now be sourced at runtime from an HTTP endpoint instead of being fixed at startup.
MILLPOND_INCLUDE_VALUES_URLnames a URL returning a JSON array of scalars, polled on a background thread (60s default, ±10% jitter, optional auth header). Millpond stays endpoint-agnostic — nothing in the code knows what the values mean.Two modes, designed for shadow-first rollout:
shadow(default): the staticMILLPOND_FILTER_VALUESlist stays authoritative; the endpoint is polled purely for observability (shadow_only_static/shadow_only_remotediff gauges, staleness timestamp). Flip gate: symmetric difference at zero across the fleet.authoritative: the polled set drives the filter; the static list seeds the initial held set.See the README's new "Dynamic allowlist source" section for the full static × dynamic interaction table, and AGENT.md for the invariants.
Safety semantics
Shaped by the consequence asymmetry — an erroneous addition writes surplus rows a downstream reader ignores; an erroneous removal silently drops records with no recovery:
REMOVAL_POLLS(default 5) consecutive successful accepted polls absent; failed and refused polls advance nothing.refused_total{reason}):empty(an empty array is never removal evidence and never an acceptable first state),bulk_removal(> half of a multi-value set confirmed at once),type_flip(int↔str — one junk element would flip the normalized set to strings, the filter's values→column cast then fails, and whole batches drop asfilter_field_missingwith offsets committed: the worst silent-loss path, now refused instead).millpond_include_values_mode{mode}reports what each replica actually runs so fleet-level flip gates can't pass vacuously on a replica that never got the URL._fetchrefuses redirects (urllib re-sends the custom auth header cross-host), caps responses at 4 MB.Testing
Authored with an agent; adversarial lead-QE and principal-SWE review agents ran against the initial version — their findings (refusal pre-charge attack, type-flip batch-drop chain, restart amnesia, vacuous flip gate, redirect header leak, response cap, jitter, mode-without-URL validation) are all incorporated with regression tests. Checks actually run per the AGENT.md pre-push checklist:
just lint/just fmt-checkcleanjust test— 604 unit tests passed (35 new)just test-integration— 23 passedjust test-e2e— 4 passed (full DuckLake stack)Deployment
Inert until a URL is configured — with
MILLPOND_INCLUDE_VALUES_*unset, behavior is byte-identical to today (verified by the untouched filter test suite). Chart wiring and the endpoint itself ship separately.