Skip to content

feat(flagd): add FILE resolver type for local flag definition files#647

Open
aedeny wants to merge 4 commits into
open-feature:mainfrom
aedeny:main
Open

feat(flagd): add FILE resolver type for local flag definition files#647
aedeny wants to merge 4 commits into
open-feature:mainfrom
aedeny:main

Conversation

@aedeny
Copy link
Copy Markdown

@aedeny aedeny commented May 12, 2026

Add support for reading flagd feature flag definitions directly from a local file, with automatic change detection and live reload.

New resolver:

  • FileBasedResolver: reads a flagd flag definition JSON file from disk, validates it against the JSON schema, and evaluates flags locally via JsonEvaluator. Supports waiting for the file to appear (configurable timeout, default 5 min) and reloading on changes.

  • FileSystemHashWatcher: polls file content using MurmurHash-128 to detect changes. Designed for environments where filesystem events are unreliable (e.g., NFS, container-mounted volumes). Configurable polling interval (default 1 min). Baseline hash is computed synchronously on Start() to prevent race conditions.

  • When hash-based detection is not enabled, a standard FileSystemWatcher with 500ms debounce is used instead.

Configuration via FLAGD_RESOLVER=file, FLAGD_SOURCE_FILE_PATH, and FLAGD_HASH_FILE_CHANGE env vars. Builder API: WithSourceFilePath(), WithUseHashFileChangeDetection(). DI options: FlagdProviderOptions.SourceFilePath, FlagdProviderOptions.UseHashFileChangeDetection.

Error handling: FileShare.ReadWrite|Delete for safe concurrent access, schema validation on Init, ProviderError events on parse failures without crashing, ReaderWriterLockSlim protects evaluator during reloads.

Tests: 31 new tests (201 total pass) covering FileBasedResolver lifecycle and resolution, FileSystemHashWatcher polling, FlagdConfig env var parsing, FlagdProvider instantiation, and DI option mapping.

Documentation: Updated README with FILE resolver config table entries, usage examples, and hash-based detection docs.

This PR

  • adds this new feature

Related Issues

Fixes #1234523

Notes

Follow-up Tasks

How to test

@aedeny aedeny requested review from a team as code owners May 12, 2026 14:15
@aedeny
Copy link
Copy Markdown
Author

aedeny commented May 12, 2026

Related to #308

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a file-based resolver for the flagd provider, allowing flag definitions to be loaded from a local JSON file. Key additions include the FileBasedResolver, a custom FileSystemHashWatcher for reliable change detection in containerized environments, and corresponding configuration options via code and environment variables. Review feedback highlights several opportunities to improve memory efficiency and performance, such as reading and hashing file streams directly rather than using intermediate memory buffers, and optimizing directory searches during file system cache refreshes.

Comment thread src/OpenFeature.Providers.Flagd/Resolver/InProcess/FileBasedResolver.cs Outdated
Comment thread src/OpenFeature.Providers.Flagd/Resolver/InProcess/FileSystemHashWatcher.cs Outdated
Comment thread src/OpenFeature.Providers.Flagd/Resolver/InProcess/FileSystemHashWatcher.cs Outdated
@kylejuliandev
Copy link
Copy Markdown
Contributor

Hey @aedeny, thanks for contributing this change! I've noticed the DCO status is failing. Commits need to be signed off with git commit -s.

To add your Signed-off-by line to every commit in this branch:

  1. In your local branch, run: git rebase HEAD~1 --signoff
  2. Force push your changes to overwrite the branch: git push --force-with-lease origin main

Add support for reading flagd feature flag definitions directly from a local file, with automatic change detection and live reload.

New resolver:

- FileBasedResolver: reads a flagd flag definition JSON file from disk, validates it against the JSON schema, and evaluates flags locally via JsonEvaluator. Supports waiting for the file to appear (configurable timeout, default 5 min) and reloading on changes.

- FileSystemHashWatcher: polls file content using MurmurHash-128 to detect changes. Designed for environments where filesystem events are unreliable (e.g., NFS, container-mounted volumes). Configurable polling interval (default 1 min). Baseline hash is computed synchronously on Start() to prevent race conditions.

- When hash-based detection is not enabled, a standard FileSystemWatcher with 500ms debounce is used instead.

Configuration via FLAGD_RESOLVER=file, FLAGD_SOURCE_FILE_PATH, and FLAGD_HASH_FILE_CHANGE env vars. Builder API: WithSourceFilePath(), WithUseHashFileChangeDetection(). DI options: FlagdProviderOptions.SourceFilePath, FlagdProviderOptions.UseHashFileChangeDetection.

Error handling: FileShare.ReadWrite|Delete for safe concurrent access, schema validation on Init, ProviderError events on parse failures without crashing, ReaderWriterLockSlim protects evaluator during reloads.

Tests: 31 new tests (201 total pass) covering FileBasedResolver lifecycle and resolution, FileSystemHashWatcher polling, FlagdConfig env var parsing, FlagdProvider instantiation, and DI option mapping.

Documentation: Updated README with FILE resolver config table entries, usage examples, and hash-based detection docs.
Signed-off-by: Eden Yefet <edenyefet@gmail.com>
aedeny added 3 commits May 13, 2026 15:11
When a targeting rule evaluates to null (no match), the resolver
correctly falls back to the defaultVariant value but was returning
variant: null in ResolutionDetails instead of the defaultVariant name.

Assign variant = flagConfiguration.DefaultVariant before the lookup so
the returned ResolutionDetails includes the correct variant identifier.

Added test: TargetingReturnsNull_FallsBackToDefaultVariant

Signed-off-by: Eden Yefet <edenyefet@gmail.com>
@aedeny
Copy link
Copy Markdown
Author

aedeny commented May 28, 2026

Hey @kylejuliandev, is there an ETA for the review and merge of this PR? Is there anything else that is required from my part?

@kylejuliandev
Copy link
Copy Markdown
Contributor

Hey @kylejuliandev, is there an ETA for the review and merge of this PR? Is there anything else that is required from my part?

Hey @aedeny! Apologies, I have been away. I will resume my review this week 😄

}

public async ValueTask DisposeAsync()
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: is it worth adding a private bool field (e.g. _disposed) which we can check for redundant dispose calls?

Comment on lines +130 to +132
_debounceTimer?.Dispose();
_evaluatorLock.Dispose();
_cts.Dispose();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: If the timer fires while LoadFlags() is executing (holding the write lock), Shutdown() calling _evaluatorLock.Dispose() will throw SynchronizationLockException. Would it be safer to cancel the CTS first, then dispose the watcher, then dispose the lock (after draining any in-flight callbacks).

ResolverType.RPC => 8013,
ResolverType.IN_PROCESS => 8015,
_ => throw new NotImplementedException("ResolverType does not use Ports.")
_ => 8013
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thought: as you return early above I think you can leave the NotImplementedException in here


var fileWatcher = new FileSystemHashWatcher(_filePath, _logger, _fileChangePollingInterval);

fileWatcher.FileChanged += (sender, e) =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thought: we subscribe to FileChanged, does this included when a file is deleted?

FileOptions.SequentialScan))
{
var hash = _murmur128.ComputeHash(fs);
return (hash, (int)fs.Length);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thought: does the int cast overflow if the file is too large?

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.

5 participants