Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add client status #18

Merged
merged 17 commits into from
Apr 4, 2024
Merged
30 changes: 30 additions & 0 deletions examples/WebApiApp/FeatBitHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using FeatBit.Sdk.Server;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace WebApiApp;

public class FeatBitHealthCheck : IHealthCheck
{
private readonly IFbClient _fbClient;

public FeatBitHealthCheck(IFbClient fbClient)
{
_fbClient = fbClient;
}

public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var status = _fbClient.Status;

var result = status switch
{
FbClientStatus.Ready => HealthCheckResult.Healthy(),
FbClientStatus.Stale => HealthCheckResult.Degraded(),
_ => HealthCheckResult.Unhealthy()
};

return Task.FromResult(result);
}
}
11 changes: 11 additions & 0 deletions examples/WebApiApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using FeatBit.Sdk.Server;
using FeatBit.Sdk.Server.Model;
using FeatBit.Sdk.Server.DependencyInjection;
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using WebApiApp;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -13,8 +16,16 @@
options.StartWaitTime = TimeSpan.FromSeconds(3);
});

builder.Services.AddHealthChecks()
.AddCheck<FeatBitHealthCheck>("FeatBit");

var app = builder.Build();

app.MapHealthChecks("/healthz", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

// curl -X GET --location "http://localhost:5014/variation-detail/game-runner?fallbackValue=lol"
app.MapGet("/variation-detail/{flagKey}", (IFbClient fbClient, string flagKey, string fallbackValue) =>
{
Expand Down
11 changes: 5 additions & 6 deletions examples/WebApiApp/WebApiApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FeatBit.ServerSdk" Version="1.1.4"/>
<!-- <PackageReference Include="FeatBit.ServerSdk" Version="1.1.4"/>-->
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.5"/>
</ItemGroup>

<!--
<ItemGroup>
<ProjectReference Include="..\..\src\FeatBit.ServerSdk\FeatBit.ServerSdk.csproj"/>
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\src\FeatBit.ServerSdk\FeatBit.ServerSdk.csproj"/>
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/FeatBit.ServerSdk/Concurrent/AtomicBoolean.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace FeatBit.Sdk.Server.Concurrent
/// without any explicit locking. .NET's strong memory on write guarantees might already enforce
/// this ordering, but the addition of the MemoryBarrier guarantees it.
/// </summary>
public class AtomicBoolean
public sealed class AtomicBoolean
{
private const int FalseValue = 0;
private const int TrueValue = 1;
Expand Down
56 changes: 56 additions & 0 deletions src/FeatBit.ServerSdk/Concurrent/StatusManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;

namespace FeatBit.Sdk.Server.Concurrent;

public sealed class StatusManager<TStatus> where TStatus : Enum
{
private TStatus _status;
private readonly object _statusLock = new object();
private readonly Action<TStatus> _onStatusChanged;

public StatusManager(TStatus initialStatus, Action<TStatus> onStatusChanged = null)
{
_status = initialStatus;
_onStatusChanged = onStatusChanged;
}

public TStatus Status
{
get
{
lock (_statusLock)
{
return _status;
}
}
}

public bool CompareAndSet(TStatus expected, TStatus newStatus)
{
lock (_statusLock)
{
if (!EqualityComparer<TStatus>.Default.Equals(_status, expected))
{
return false;
}

SetStatus(newStatus);
return true;
}
}

public void SetStatus(TStatus newStatus)
{
lock (_statusLock)
{
if (EqualityComparer<TStatus>.Default.Equals(_status, newStatus))
{
return;
}

_status = newStatus;
_onStatusChanged?.Invoke(_status);
}
}
}
45 changes: 45 additions & 0 deletions src/FeatBit.ServerSdk/DataSynchronizer/DataSynchronizerStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace FeatBit.Sdk.Server.DataSynchronizer;

public enum DataSynchronizerStatus
{
/// <summary>
/// The initial state of the synchronizer when the SDK is being initialized.
/// </summary>
/// <remarks>
/// If it encounters an error that requires it to retry initialization, the state will remain at
/// <see cref="Starting"/> until it either succeeds and becomes <see cref="Stable"/>, or
/// permanently fails and becomes <see cref="Stopped"/>.
/// </remarks>
Starting,

/// <summary>
/// Indicates that the synchronizer is currently operational and has not had any problems since the
/// last time it received data.
/// </summary>
/// <remarks>
/// In streaming mode, this means that there is currently an open stream connection and that at least
/// one initial message has been received on the stream. In polling mode, it means that the last poll
/// request succeeded.
/// </remarks>
Stable,

/// <summary>
/// Indicates that the synchronizer encountered an error that it will attempt to recover from.
/// </summary>
/// <remarks>
/// In streaming mode, this means that the stream connection failed, or had to be dropped due to some
/// other error, and will be retried after a backoff delay. In polling mode, it means that the last poll
/// request failed, and a new poll request will be made after the configured polling interval.
/// </remarks>
Interrupted,

/// <summary>
/// Indicates that the synchronizer has been permanently shut down.
/// </summary>
/// <remarks>
/// This could be because it encountered an unrecoverable error (for instance, the Evaluation server
/// rejected the SDK key: an invalid SDK key will never become valid), or because the SDK client was
/// explicitly shut down.
/// </remarks>
Stopped
}
19 changes: 19 additions & 0 deletions src/FeatBit.ServerSdk/DataSynchronizer/IDataSynchronizer.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks;

namespace FeatBit.Sdk.Server.DataSynchronizer
Expand All @@ -9,6 +10,24 @@ public interface IDataSynchronizer
/// </summary>
public bool Initialized { get; }

/// <summary>
/// The current status of the data synchronizer.
/// </summary>
public DataSynchronizerStatus Status { get; }

/// <summary>An event for receiving notifications of status changes.</summary>
/// <remarks>
/// <para>
/// Any handlers attached to this event will be notified whenever any property of the status has changed.
/// See <see cref="T:FeatBit.Sdk.Server.DataSynchronizer.DataSynchronizerStatus" /> for an explanation of the meaning of each property and what could cause it
/// to change.
/// </para>
/// <para>
/// The listener should return as soon as possible so as not to block subsequent notifications.
/// </para>
/// </remarks>
event Action<DataSynchronizerStatus> StatusChanged;

/// <summary>
/// Starts the data synchronizer. This is called once from the <see cref="FbClient"/> constructor.
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions src/FeatBit.ServerSdk/DataSynchronizer/NullDataSynchronizer.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
using System;
using System.Threading.Tasks;
using FeatBit.Sdk.Server.Concurrent;

namespace FeatBit.Sdk.Server.DataSynchronizer;

internal sealed class NullDataSynchronizer : IDataSynchronizer
{
private readonly StatusManager<DataSynchronizerStatus> _statusManager;

public bool Initialized => true;
public DataSynchronizerStatus Status => _statusManager.Status;
public event Action<DataSynchronizerStatus> StatusChanged;

public NullDataSynchronizer()
{
_statusManager = new StatusManager<DataSynchronizerStatus>(
DataSynchronizerStatus.Stable,
OnStatusChanged
);
}

public Task<bool> StartAsync()
{
_statusManager.SetStatus(DataSynchronizerStatus.Stable);
return Task.FromResult(true);
}

public Task StopAsync()
{
_statusManager.SetStatus(DataSynchronizerStatus.Stopped);
return Task.CompletedTask;
}

private void OnStatusChanged(DataSynchronizerStatus status) => StatusChanged?.Invoke(status);
}
Loading
Loading