Skip to content

refactor: introduce EnvironmentIndex to make the served environment set runtime-mutable - #17

Open
gagantrivedi wants to merge 26 commits into
mainfrom
feat/environment-discovery
Open

refactor: introduce EnvironmentIndex to make the served environment set runtime-mutable#17
gagantrivedi wants to merge 26 commits into
mainfrom
feat/environment-discovery

Conversation

@gagantrivedi

@gagantrivedi gagantrivedi commented Aug 22, 2026

Copy link
Copy Markdown
Member

Thanks for submitting a PR! Please check the boxes below:

  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Contributes to Flagsmith/edge-proxy#128

Groundwork for environment auto-discovery: the served environment set becomes runtime-mutable. No behaviour change — static environment_key_pairs configs work exactly as today; discovery itself is a follow-up PR.

  • New EnvironmentIndex (src/environments.rs) replaces the startup-built key maps. Each environment is indexed under its client key and every server key; insert/remove return the replaced/removed entry so callers can invalidate the cache.
  • One resolve_key() replaces the three unknown-key gates (ser. keys still 503 on /flags//identities as today — fixed in a follow-up).
  • remove_environment stops serving an environment and clears its cached document. All index and cache mutation happens on the serial refresh task, so no re-insertion guards are needed.
  • environment_key_pairs is now optional; a startup warning fires when nothing is configured.
  • Poll-failure logs now print the client key, not the ser. secret.

Accepted, deliberately: duplicated server keys are last-one-wins.

How did you test this code?

cargo test: 51 green, including index unit tests and tests/test_remove_environment.rs (removed environments are rejected on every endpoint, for both key kinds). No existing test expectations changed. Clippy and fmt clean.

@gagantrivedi gagantrivedi changed the title refactor: make the served environment set runtime-mutable refactor: introduce EnvironmentIndex to make the served environment set runtime-mutable Aug 22, 2026
@gagantrivedi
gagantrivedi marked this pull request as ready for review August 22, 2026 09:56
@matthewelwell

Copy link
Copy Markdown

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: eaa84375-fbf1-4f83-b4df-71bcea8d229d

📝 Walkthrough

Walkthrough

The proxy now uses EnvironmentIndex to resolve client and server keys. The index supports key rotation, removal, snapshots, and server-key validity checks. EnvironmentService uses the index during polling, fetching, request validation, and environment removal. Removal clears documents, contexts, identity overrides, and endpoint caches. Configuration may omit environment key pairs. Startup logs a warning when no environments are configured.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟠 High · up to 6eead

The runtime-mutable environment index can currently mishandle colliding keys and can briefly serve data fetched for a replaced environment. These are concrete correctness risks that could expose the wrong environment’s flags or identities, so the PR is not merge-ready until collision handling and replacement validation are fixed.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7f797395-a335-4f14-b32b-be624b0136a6

📥 Commits

Reviewing files that changed from the base of the PR and between 62bb3fc and 6eeada4.

📒 Files selected for processing (8)
  • src/cache/environment.rs
  • src/config/settings.rs
  • src/environments.rs
  • src/lib.rs
  • src/main.rs
  • src/services/environment.rs
  • tests/test_lru_cache.rs
  • tests/test_remove_environment.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread src/environments.rs Outdated
Comment thread src/services/environment.rs Outdated

@matthewelwell matthewelwell left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full disclosure: I've not reviewed the tests.

Comment thread src/services/environment.rs Outdated
Comment thread src/services/environment.rs Outdated
Comment thread src/services/environment.rs Outdated
Comment on lines +139 to +141
// An environment stuck here fails every poll, keeping /health red
// until a config change — or, once reconciliation exists, until it
// removes or re-keys the environment.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't make any sense to me. How / why would an environment get 'stuck' here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't happen here but it can happen in: #18
I will move the comment to that with how

.await?
{
Some(document) => Ok(document),
// 304: upstream confirmed the cached copy is current.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about other non-200 responses?

@gagantrivedi gagantrivedi Sep 5, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are returned as error handled by the caller of this fucntion

Comment thread src/services/environment.rs Outdated
));
}
// Validation only — lookups below still use the raw presented key, so
// a server-side key 503s on this endpoint (contexts are stored under

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

503 seems wrong here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errr... this is supposed to be a todo

Comment thread src/environments.rs Outdated
pub fn resolve(&self, key: &str) -> Option<Arc<EnvironmentKeys>> {
self.by_key
.read()
.expect("environment index lock poisoned")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears a lot.

  1. Does it need to? It feels odd.
  2. Should it be a constant, instead of a hard-coded string?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was mostly forced syntax: std's lock returns a Result we had to unwrap. Switched to parking_lot's RwLock (already in our dependency tree), which doesn't poison, so it's gone.

Comment thread src/environments.rs Outdated
Comment on lines +84 to +88
/// Insert or replace an environment's keys, dropping index entries
/// for server keys the previous version no longer has. Returns the
/// replaced version, if any: request caches are keyed by presented
/// key and consulted before the key gate, so the caller owns
/// invalidating whatever is cached under keys that stopped resolving.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docstring doesn't make much sense to me. Some examples:

  1. dropping index entries for server keys the previous version no longer has - what is the 'previous version' here? The index? Why would it no longer have some server key?
  2. key gate - what is the key gate?
  3. so the caller owns invalidating whatever is cached under keys that stopped resolving - this seems like unnecessary slop?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another victim of endoint cache! let me know if it makes sense now

Comment thread src/environments.rs Outdated
/// synchronous code.
#[derive(Default)]
pub struct EnvironmentIndex {
by_key: RwLock<HashMap<String, Arc<EnvironmentKeys>>>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it's my lack of familiarity with rust, but I don't really understand what by_key is. It's used a lot in the code below, so it might be worth some specific documentation here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this better now?

@gagantrivedi
gagantrivedi force-pushed the feat/environment-discovery branch from 6eeada4 to fbec84a Compare September 5, 2026 04:58
@gagantrivedi
gagantrivedi changed the base branch from main to chore/drop-endpoint-cache September 5, 2026 04:58
Environments are now held in an EnvironmentRegistry that indexes each
record under its client key and every server key, replacing the
immutable key_mapping/server_to_client maps built at startup. Records
carry an EnvSource so statically configured environments can never be
evicted by future inventory reconciliation.

Adds EnvironmentsCache::remove_environment and
EnvironmentService::evict_environment so an environment can be
forgotten at runtime, including the endpoint caches that are consulted
before the key gate. Splits fetch_environment into a server-key
picker and a reusable fetch_document(server_key, if_modified_since).

No behaviour change for existing configs; the poll-failure log now
prints the client key instead of the server-side secret.
An omitted field now behaves like an explicitly empty list (which was
already accepted), so a future discovery-only config needs no static
pairs. Statically configured pairs keep working exactly as before.
main() now only loads settings and logging before delegating, so an
alternative binary can compose the proxy from the library crate.
The type is the domain concept of which environments the proxy serves,
not a service, so it moves out of services/ to src/environments.rs
beside cache/ — matching how comparable Rust proxies place such state
(e.g. unleash-edge's feature_cache). Drops the services re-export:
EnvironmentService is its only consumer.
Names the content: the key set of one environment (client key, server
keys, provenance). Also unabbreviates EnvSource to EnvironmentSource
and renames records() to snapshot() so no 'record' vocabulary is left.
Nothing reads it: an environment's provenance only matters to the
reconciliation that later phases introduce, and staticness is
derivable there from the immutable environment_key_pairs settings.
Reintroduce an explicit field if that derivation proves awkward.
Evict implies cache-pressure expulsion; this is a deliberate removal
from the served set, and the cache trait already calls its half
remove_environment, so the whole family now shares the verb.
A config that lists the same server key for two environments is a
misconfiguration we choose not to defend against; removal now just
drops the entry's keys unconditionally.
Endpoint-cache lookups run before the key gate, so an in-flight
request that passed the gate could write its result after
remove_environment's clears and have it served indefinitely; the poll
loop, iterating a pre-removal snapshot, could likewise re-insert a
removed environment's document and pin it until restart. Every cache
write now re-checks the index afterwards and clears what it wrote if
the key no longer resolves — remove_environment un-indexes before
clearing, so one of the two clears always runs last. Removal also
clears caches even when the key is unknown, so repeating it cleans any
residue.

The interleavings themselves aren't deterministically testable without
injection points; the tests pin each guard's behaviour instead.
Mirrors HashMap::insert and makes insert symmetric with remove: when a
later phase rotates keys via insert, the caller needs the dropped
server keys to invalidate request caches, which are keyed by presented
key and consulted before the key gate.
The duplicated-server-key failure shape, the /health-stays-red state a
key-less environment creates, and the actual reason run() lives in the
library were all design decisions living only in review threads.
A typo'd environment_key_pairs field name silently parses as an empty
set (serde ignores unknown fields), leaving a healthy-looking proxy
that rejects everything. Also pins the serde default with a test that
{} parses to an empty, valid config.
Nothing besides main.rs ever called it; the composition-binary story it
served is speculative, and it can return the day something real needs
it. The empty-config warning moves with the body.
The cache is gone (chore/drop-endpoint-cache); the per-endpoint tests
stay as plain removed-key rejections, and the residue test moves to
the environment cache.
It guarded a removal racing an in-flight fetch, which cannot happen:
every index and cache mutation runs on the serial refresh task, and
requests resolve the key before touching the cache, so an orphaned
document could not be served anyway. The unknown-key residue clear in
remove_environment goes with it — same race, same reasoning.
@gagantrivedi
gagantrivedi force-pushed the feat/environment-discovery branch from fbdd769 to 21dc629 Compare September 5, 2026 08:09
@gagantrivedi
gagantrivedi changed the base branch from chore/drop-endpoint-cache to main September 5, 2026 08:09
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.

2 participants