Skip to content

Commit

Permalink
feat: add client status (#18)
Browse files Browse the repository at this point in the history
* add FbClientStatus

* add StatusManagerTests

* add AtomicBooleanTests.cs

* use AtomicBoolean

* set status to stable when we handle message successfully

* no need to subscribe to reconnected event

* update comment

* set status to Stable only when handle data-sync message success

* update comment

* update

* chore

* remove status manager for FbClient

* more work

* more test

* fix test

* add health check example

* add missing changes
  • Loading branch information
deleteLater authored Apr 4, 2024
1 parent 1845c69 commit cfa28a8
Show file tree
Hide file tree
Showing 17 changed files with 475 additions and 36 deletions.
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

0 comments on commit cfa28a8

Please sign in to comment.