Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,21 @@ public record FlagdProviderOptions
/// Source selector for the in-process provider. Defaults to empty string.
/// </summary>
public string SourceSelector { get; set; } = string.Empty;

/// <summary>
/// Path to the flag definition JSON file for file-based in-memory resolution.
/// Used when <see cref="ResolverType"/> is <see cref="ResolverType.FILE"/>.
/// Defaults to empty string.
/// </summary>
public string SourceFilePath { get; set; } = string.Empty;

/// <summary>
/// When true, the file watcher uses content hashing (MurmurHash) to detect changes.
/// When false, the file watcher relies on file system events from the OS.
/// File system events can be unreliable in certain containerized environments or mount types;
/// hashing always works reliably but has a higher I/O cost.
/// Used when <see cref="ResolverType"/> is <see cref="ResolverType.FILE"/>.
/// Defaults to false.
/// </summary>
public bool UseHashFileChangeDetection { get; set; } = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public static FlagdConfig ToFlagdConfig(this FlagdProviderOptions options)
.WithMaxEventStreamRetries(options.MaxEventStreamRetries)
.WithResolverType(options.ResolverType)
.WithSourceSelector(options.SourceSelector)
.WithSourceFilePath(options.SourceFilePath)
.WithUseHashFileChangeDetection(options.UseHashFileChangeDetection)
.Build();

return config;
Expand Down
97 changes: 93 additions & 4 deletions src/OpenFeature.Providers.Flagd/FlagdConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ public enum ResolverType
/// locally for in-process evaluation.
/// Evaluations are preformed in-process.
/// </summary>
IN_PROCESS
IN_PROCESS,
/// <summary>
/// This is the file-based resolving type, where flags are loaded from a local JSON file
/// and evaluated in-process without creating any gRPC streams.
/// Evaluations are preformed in-process.
/// </summary>
FILE
}

/// <summary>
Expand All @@ -38,9 +44,13 @@ public class FlagdConfig
internal const string EnvVarMaxEventStreamRetries = "FLAGD_MAX_EVENT_STREAM_RETRIES";
internal const string EnvVarResolverType = "FLAGD_RESOLVER";
internal const string EnvVarSourceSelector = "FLAGD_SOURCE_SELECTOR";
internal const string EnvVarSourceFilePath = "FLAGD_SOURCE_FILE_PATH";
internal const string EnvVarHashFileChange = "FLAGD_HASH_FILE_CHANGE";
internal const string FlagdSelectorHeaderName = "flagd-selector";
internal static int CacheSizeDefault = 10;
internal static string InProcessResolverValue = "in-process";
internal static string RpcResolverValue = "rpc";
internal static string FileResolverValue = "file";
internal static string LruCacheValue = "lru";

/// <summary>
Expand Down Expand Up @@ -173,6 +183,28 @@ public ILogger Logger
set => _logger = value;
}

/// <summary>
/// Path to the flag definition JSON file. Used when ResolverType is FILE.
/// </summary>
public string SourceFilePath
{
get => _sourceFilePath;
set => _sourceFilePath = value;
}

/// <summary>
/// When true, the file watcher uses content hashing (MurmurHash) to detect changes.
/// When false, the file watcher relies on file system events from the OS.
/// File system events can be unreliable in certain containerized environments or mount types;
/// hashing always works reliably but has a higher I/O cost.
/// Defaults to false. Used when ResolverType is FILE.
/// </summary>
public bool UseHashFileChangeDetection
{
get => _useHashFileChangeDetection;
set => _useHashFileChangeDetection = value;
}

internal bool UseCertificate => _cert.Length > 0;

private string _host;
Expand All @@ -186,6 +218,8 @@ public ILogger Logger
private string _sourceSelector;
private ILogger _logger;
private ResolverType _resolverType;
private string _sourceFilePath;
private bool _useHashFileChangeDetection;

internal FlagdConfig()
{
Expand All @@ -205,8 +239,9 @@ internal FlagdConfig()
_maxEventStreamRetries = int.TryParse(Environment.GetEnvironmentVariable(EnvVarMaxEventStreamRetries), out var maxEventStreamRetries) ? maxEventStreamRetries : 3;
}

var resolverTypeStr = Environment.GetEnvironmentVariable(EnvVarResolverType) ?? "RPC";
_resolverType = string.Equals(resolverTypeStr, InProcessResolverValue, StringComparison.OrdinalIgnoreCase) ? ResolverType.IN_PROCESS : ResolverType.RPC;
_resolverType = GetResolverTypeFromEnvironment();
_sourceFilePath = GetSourceFilePathFromEnvironment();
_useHashFileChangeDetection = GetUseHashFileChangeDetectionFromEnvironment();
}

internal Uri GetUri()
Expand All @@ -229,6 +264,35 @@ internal Uri GetUri()
}
return uri;
}

private static ResolverType GetResolverTypeFromEnvironment()
{
var resolverTypeStr = Environment.GetEnvironmentVariable(EnvVarResolverType);

if (string.IsNullOrWhiteSpace(resolverTypeStr))
{
return ResolverType.RPC;
}

if (string.Equals(resolverTypeStr, InProcessResolverValue, StringComparison.OrdinalIgnoreCase))
return ResolverType.IN_PROCESS;
if (string.Equals(resolverTypeStr, FileResolverValue, StringComparison.OrdinalIgnoreCase))
return ResolverType.FILE;

return ResolverType.RPC;
}

private static string GetSourceFilePathFromEnvironment()
{
var sourceFilePathStr = Environment.GetEnvironmentVariable(EnvVarSourceFilePath);
return string.IsNullOrWhiteSpace(sourceFilePathStr) ? string.Empty : sourceFilePathStr;
}

private static bool GetUseHashFileChangeDetectionFromEnvironment()
{
var value = Environment.GetEnvironmentVariable(EnvVarHashFileChange);
return !string.IsNullOrEmpty(value) && bool.TryParse(value, out var parsed) && parsed;
}
}

/// <summary>
Expand Down Expand Up @@ -328,6 +392,26 @@ public FlagdConfigBuilder WithSourceSelector(string sourceSelector)
return this;
}

/// <summary>
/// Path to the flag definition JSON file for file-based in-memory resolution.
/// </summary>
public FlagdConfigBuilder WithSourceFilePath(string sourceFilePath)
{
_config.SourceFilePath = sourceFilePath;
return this;
}

/// <summary>
/// Enable or disable content hashing for file change detection.
/// When true, the file watcher uses content hashing (MurmurHash) to detect changes.
/// When false, the file watcher relies on file system events from the OS.
/// </summary>
public FlagdConfigBuilder WithUseHashFileChangeDetection(bool useHash)
{
_config.UseHashFileChangeDetection = useHash;
return this;
}

/// <summary>
/// Provide a <see cref="ILogger"/> to be used by the Flagd provider.
/// </summary>
Expand All @@ -351,13 +435,18 @@ public FlagdConfig Build()

private void PreBuild()
{
if (this._config.ResolverType == ResolverType.FILE)
{
return;
}

if (this._config.Port == 0)
{
var defaultPortForResolver = this._config.ResolverType switch
{
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

};

this._config.Port = defaultPortForResolver;
Expand Down
22 changes: 19 additions & 3 deletions src/OpenFeature.Providers.Flagd/FlagdProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ public sealed class FlagdProvider : FeatureProvider
/// FLAGD_CACHE - Enable or disable the cache (default="false")
/// FLAGD_MAX_CACHE_SIZE - The maximum size of the cache (default="10")
/// FLAGD_MAX_EVENT_STREAM_RETRIES - The maximum amount of retries for establishing the EventStream
/// FLAGD_RESOLVER - The type of resolver (in-process or rpc) to be used for the provider
/// FLAGD_RESOLVER - The type of resolver (in-process, file or rpc) to be used for the provider
/// FLAGD_SOURCE_FILE_PATH - The path to the flag definition JSON file (used when FLAGD_RESOLVER="file")
/// FLAGD_HASH_FILE_CHANGE - Use content hashing for file change detection (default="false", used when FLAGD_RESOLVER="file")
/// </summary>
public FlagdProvider() : this(FlagdConfig.Builder().Build())
{
Expand All @@ -47,7 +49,9 @@ public FlagdProvider() : this(FlagdConfig.Builder().Build())
/// FLAGD_CACHE - Enable or disable the cache (default="false")
/// FLAGD_MAX_CACHE_SIZE - The maximum size of the cache (default="10")
/// FLAGD_MAX_EVENT_STREAM_RETRIES - The maximum amount of retries for establishing the EventStream
/// FLAGD_RESOLVER - The type of resolver (in-process or rpc) to be used for the provider
/// FLAGD_RESOLVER - The type of resolver (in-process, file or rpc) to be used for the provider
/// FLAGD_SOURCE_FILE_PATH - The path to the flag definition JSON file (used when FLAGD_RESOLVER="file")
/// FLAGD_HASH_FILE_CHANGE - Use content hashing for file change detection (default="false", used when FLAGD_RESOLVER="file")
/// <param name="url">The URL of the flagd server</param>
/// <exception cref="ArgumentNullException">if no url is provided.</exception>
/// </summary>
Expand All @@ -74,6 +78,19 @@ public FlagdProvider(FlagdConfig config)
var jsonSchemaValidator = new JsonSchemaValidator(_config.Logger);
_resolver = new InProcessResolver(_config, jsonSchemaValidator);
}
else if (_config.ResolverType == ResolverType.FILE)
{
if (string.IsNullOrWhiteSpace(_config.SourceFilePath))
throw new ArgumentException("SourceFilePath must be set when using ResolverType.FILE");

var jsonSchemaValidator = new JsonSchemaValidator(_config.Logger);
_resolver = new FileBasedResolver(
_config.Logger,
_config.SourceFilePath,
jsonSchemaValidator,
_config.SourceSelector,
_config.UseHashFileChangeDetection);
}
else
{
_resolver = new RpcResolver(config);
Expand Down Expand Up @@ -220,7 +237,6 @@ public override async Task<ResolutionDetails<string>> ResolveStringValueAsync(st
/// <inheritdoc/>
public override async Task<ResolutionDetails<int>> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default)
{

return await _resolver.ResolveIntegerValueAsync(flagKey, defaultValue, context).ConfigureAwait(false);
}

Expand Down
81 changes: 68 additions & 13 deletions src/OpenFeature.Providers.Flagd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,19 +156,21 @@ namespace OpenFeatureTestApp

The URI of the flagd server to which the `flagd Provider` connects to can either be passed directly to the constructor, or be configured using the following environment variables:

| Option name | Environment variable name | Type | Default | Values |
| ---------------------------- | ------------------------------ | ------- | --------- | --------------- |
| host | FLAGD_HOST | string | localhost | |
| port | FLAGD_PORT | number | 8013 | |
| tls | FLAGD_TLS | boolean | false | |
| tls certPath | FLAGD_SERVER_CERT_PATH | string | | |
| unix socket path | FLAGD_SOCKET_PATH | string | | |
| Caching | FLAGD_CACHE | string | | lru |
| Maximum cache size | FLAGD_MAX_CACHE_SIZE | number | 10 | |
| Maximum event stream retries | FLAGD_MAX_EVENT_STREAM_RETRIES | number | 3 | |
| Resolver type | FLAGD_RESOLVER | string | rpc | rpc, in-process |
| Source selector | FLAGD_SOURCE_SELECTOR | string | | |
| Logger | n/a | n/a | | |
| Option name | Environment variable name | Type | Default | Values |
| ---------------------------- | ------------------------------ | ------- | --------- | ----------------------- |
| host | FLAGD_HOST | string | localhost | |
| port | FLAGD_PORT | number | 8013 | |
| tls | FLAGD_TLS | boolean | false | |
| tls certPath | FLAGD_SERVER_CERT_PATH | string | | |
| unix socket path | FLAGD_SOCKET_PATH | string | | |
| Caching | FLAGD_CACHE | string | | lru |
| Maximum cache size | FLAGD_MAX_CACHE_SIZE | number | 10 | |
| Maximum event stream retries | FLAGD_MAX_EVENT_STREAM_RETRIES | number | 3 | |
| Resolver type | FLAGD_RESOLVER | string | rpc | rpc, in-process, file |
| Source selector | FLAGD_SOURCE_SELECTOR | string | | |
| Source file path | FLAGD_SOURCE_FILE_PATH | string | | |
| Hash file change detection | FLAGD_HASH_FILE_CHANGE | boolean | false | |
| Logger | n/a | n/a | | |

Note that if `FLAGD_SOCKET_PATH` is set, this value takes precedence, and the other variables (`FLAGD_HOST`, `FLAGD_PORT`, `FLAGD_TLS`, `FLAGD_SERVER_CERT_PATH`) are disregarded.

Expand Down Expand Up @@ -245,3 +247,56 @@ var flagdConfig = new FlagdConfigBuilder()
.WithLogger(logger)
.Build();
```

## File resolver type

The flagd provider supports a **file-based resolver mode**, which reads flag definitions from a local JSON file.
This is useful for local development, testing, or air-gapped environments where flags are distributed as files
(e.g., via ConfigMaps, volume mounts, or file sync).

The file resolver is activated by setting the `FLAGD_RESOLVER` environment variable to `file` and providing the
path to the flag definition file via `FLAGD_SOURCE_FILE_PATH`:

```shell
export FLAGD_RESOLVER=file
export FLAGD_SOURCE_FILE_PATH=/etc/flags/my-flags.json
```

Or by configuring the provider programmatically:

```csharp
using OpenFeature.Providers.Flagd;

var flagdConfig = new FlagdConfigBuilder()
.WithResolverType(ResolverType.FILE)
.WithSourceFilePath("/etc/flags/my-flags.json")
.Build();

var flagdProvider = new FlagdProvider(flagdConfig);
OpenFeature.Api.Instance.SetProvider(flagdProvider);
```

The file resolver watches for changes to the flag file and automatically reloads the configuration when changes are
detected. By default, it uses the operating system's `FileSystemWatcher` for change notification.

### Hash-based file change detection

In some environments (e.g., NFS mounts, certain container runtimes, or network file systems), native file system
events may not be reliable. For these cases, you can enable content-based change detection using MurmurHash:

```csharp
var flagdConfig = new FlagdConfigBuilder()
.WithResolverType(ResolverType.FILE)
.WithSourceFilePath("/etc/flags/my-flags.json")
.WithUseHashFileChangeDetection(true)
.Build();
```

Or via environment variable:

```shell
export FLAGD_HASH_FILE_CHANGE=true
```

When enabled, the provider polls the file at a regular interval and compares content hashes rather than relying on
OS-level file change notifications. This is more reliable but has a slightly higher I/O cost due to periodic file reads.
Loading