Skip to content

feat: hot-reloadable IP/UID filter via FILTER_CONFIG_PATH - #8

Open
nicholasxuu wants to merge 1 commit into
masterfrom
feat/file-based-filter-provider
Open

feat: hot-reloadable IP/UID filter via FILTER_CONFIG_PATH#8
nicholasxuu wants to merge 1 commit into
masterfrom
feat/file-based-filter-provider

Conversation

@nicholasxuu

Copy link
Copy Markdown

Move IP/UID allow+deny lists from startup-only env vars into an independently mounted YAML file (typically a Kubernetes ConfigMap) so the high-churn filter rules can be updated without restarting ratelimit pods.

New env var FILTER_CONFIG_PATH (default empty):

  • empty -> existing WHITELIST_IP_NET / BLACKLIST_IP_NET /
    WHITELIST_UID / BLACKLIST_UID env-var behavior is
    preserved exactly (fully backward compatible)
  • non-empty -> filters load from the YAML at that path; fsnotify
    watches the file's parent directory (K8s ConfigMap
    symlink swap requires dir-level watch) and reloads
    atomically on change

Reload semantics:

  • successful reload -> atomic.Pointer swap; new rules apply to all subsequent requests with no lock
  • yaml/cidr error -> keep previous filter, log + counter
  • file deleted -> keep previous filter (prevents a transient
    ConfigMap disappearance from flipping the
    gate to deny-all)
  • initial load fails -> startup panic (matches the existing CIDR-
    env-var error contract)

Hot-path cleanup:

  • shouldRateLimitWorker no longer calls settings.NewSettings() per request; ForceFlag, OnlyLogOnLimit and the filter provider are snapshotted at NewService() construction and read directly. At 20-30k QPS this avoids one envconfig reflection + one filter reconstruction per request.

Lifecycle:

  • Runner stores the filter.Provider and Stop()s it on shutdown so the fsnotify watcher fd + watch goroutine are released cleanly (no-op for the static env-var fallback).

Files:

  • src/filter/loader.go - YAML decode, CIDR validation
  • src/filter/provider.go - Provider interface, static + file impls
  • src/settings/settings.go - new FILTER_CONFIG_PATH field
  • src/service/ratelimit.go - inject Provider, remove per-request
    NewSettings()
  • src/service_cmd/runner/runner.go - wire Provider, Stop chain
  • test/filter/* - 37 assertions, ECP/BVA + pairwise table
    in file headers, -race clean
  • test/service/ratelimit_test.go - 4 NewService call sites updated

Deploy: mount the filter ConfigMap as a directory (NOT subPath - subPath mounts do not receive inotify events) and point FILTER_CONFIG_PATH at the file inside it.

Move IP/UID allow+deny lists from startup-only env vars into an
independently mounted YAML file (typically a Kubernetes ConfigMap)
so the high-churn filter rules can be updated without restarting
ratelimit pods.

New env var FILTER_CONFIG_PATH (default empty):
- empty   -> existing WHITELIST_IP_NET / BLACKLIST_IP_NET /
             WHITELIST_UID / BLACKLIST_UID env-var behavior is
             preserved exactly (fully backward compatible)
- non-empty -> filters load from the YAML at that path; fsnotify
             watches the file's parent directory (K8s ConfigMap
             symlink swap requires dir-level watch) and reloads
             atomically on change

Reload semantics:
- successful reload  -> atomic.Pointer swap; new rules apply to
                        all subsequent requests with no lock
- yaml/cidr error    -> keep previous filter, log + counter
- file deleted       -> keep previous filter (prevents a transient
                        ConfigMap disappearance from flipping the
                        gate to deny-all)
- initial load fails -> startup panic (matches the existing CIDR-
                        env-var error contract)

Hot-path cleanup:
- shouldRateLimitWorker no longer calls settings.NewSettings() per
  request; ForceFlag, OnlyLogOnLimit and the filter provider are
  snapshotted at NewService() construction and read directly. At
  20-30k QPS this avoids one envconfig reflection + one filter
  reconstruction per request.

Lifecycle:
- Runner stores the filter.Provider and Stop()s it on shutdown so
  the fsnotify watcher fd + watch goroutine are released cleanly
  (no-op for the static env-var fallback).

Files:
- src/filter/loader.go    - YAML decode, CIDR validation
- src/filter/provider.go  - Provider interface, static + file impls
- src/settings/settings.go - new FILTER_CONFIG_PATH field
- src/service/ratelimit.go - inject Provider, remove per-request
                             NewSettings()
- src/service_cmd/runner/runner.go - wire Provider, Stop chain
- test/filter/*           - 37 assertions, ECP/BVA + pairwise table
                            in file headers, -race clean
- test/service/ratelimit_test.go - 4 NewService call sites updated

Deploy: mount the filter ConfigMap as a directory (NOT subPath -
subPath mounts do not receive inotify events) and point
FILTER_CONFIG_PATH at the file inside it.

Co-Authored-By: Claude Opus 4.7 <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