Skip to content

Commit cfa28a8

Browse files
authored
feat: add client status (#18)
* 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
1 parent 1845c69 commit cfa28a8

17 files changed

+475
-36
lines changed
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using FeatBit.Sdk.Server;
2+
using Microsoft.Extensions.Diagnostics.HealthChecks;
3+
4+
namespace WebApiApp;
5+
6+
public class FeatBitHealthCheck : IHealthCheck
7+
{
8+
private readonly IFbClient _fbClient;
9+
10+
public FeatBitHealthCheck(IFbClient fbClient)
11+
{
12+
_fbClient = fbClient;
13+
}
14+
15+
public Task<HealthCheckResult> CheckHealthAsync(
16+
HealthCheckContext context,
17+
CancellationToken cancellationToken = default)
18+
{
19+
var status = _fbClient.Status;
20+
21+
var result = status switch
22+
{
23+
FbClientStatus.Ready => HealthCheckResult.Healthy(),
24+
FbClientStatus.Stale => HealthCheckResult.Degraded(),
25+
_ => HealthCheckResult.Unhealthy()
26+
};
27+
28+
return Task.FromResult(result);
29+
}
30+
}

examples/WebApiApp/Program.cs

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using FeatBit.Sdk.Server;
22
using FeatBit.Sdk.Server.Model;
33
using FeatBit.Sdk.Server.DependencyInjection;
4+
using HealthChecks.UI.Client;
5+
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
6+
using WebApiApp;
47

58
var builder = WebApplication.CreateBuilder(args);
69

@@ -13,8 +16,16 @@
1316
options.StartWaitTime = TimeSpan.FromSeconds(3);
1417
});
1518

19+
builder.Services.AddHealthChecks()
20+
.AddCheck<FeatBitHealthCheck>("FeatBit");
21+
1622
var app = builder.Build();
1723

24+
app.MapHealthChecks("/healthz", new HealthCheckOptions
25+
{
26+
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
27+
});
28+
1829
// curl -X GET --location "http://localhost:5014/variation-detail/game-runner?fallbackValue=lol"
1930
app.MapGet("/variation-detail/{flagKey}", (IFbClient fbClient, string flagKey, string fallbackValue) =>
2031
{

examples/WebApiApp/WebApiApp.csproj

+5-6
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="FeatBit.ServerSdk" Version="1.1.4"/>
10+
<!-- <PackageReference Include="FeatBit.ServerSdk" Version="1.1.4"/>-->
11+
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.5"/>
1112
</ItemGroup>
1213

13-
<!--
14-
<ItemGroup>
15-
<ProjectReference Include="..\..\src\FeatBit.ServerSdk\FeatBit.ServerSdk.csproj"/>
16-
</ItemGroup>
17-
-->
14+
<ItemGroup>
15+
<ProjectReference Include="..\..\src\FeatBit.ServerSdk\FeatBit.ServerSdk.csproj"/>
16+
</ItemGroup>
1817

1918
</Project>

src/FeatBit.ServerSdk/Concurrent/AtomicBoolean.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace FeatBit.Sdk.Server.Concurrent
1010
/// without any explicit locking. .NET's strong memory on write guarantees might already enforce
1111
/// this ordering, but the addition of the MemoryBarrier guarantees it.
1212
/// </summary>
13-
public class AtomicBoolean
13+
public sealed class AtomicBoolean
1414
{
1515
private const int FalseValue = 0;
1616
private const int TrueValue = 1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace FeatBit.Sdk.Server.Concurrent;
5+
6+
public sealed class StatusManager<TStatus> where TStatus : Enum
7+
{
8+
private TStatus _status;
9+
private readonly object _statusLock = new object();
10+
private readonly Action<TStatus> _onStatusChanged;
11+
12+
public StatusManager(TStatus initialStatus, Action<TStatus> onStatusChanged = null)
13+
{
14+
_status = initialStatus;
15+
_onStatusChanged = onStatusChanged;
16+
}
17+
18+
public TStatus Status
19+
{
20+
get
21+
{
22+
lock (_statusLock)
23+
{
24+
return _status;
25+
}
26+
}
27+
}
28+
29+
public bool CompareAndSet(TStatus expected, TStatus newStatus)
30+
{
31+
lock (_statusLock)
32+
{
33+
if (!EqualityComparer<TStatus>.Default.Equals(_status, expected))
34+
{
35+
return false;
36+
}
37+
38+
SetStatus(newStatus);
39+
return true;
40+
}
41+
}
42+
43+
public void SetStatus(TStatus newStatus)
44+
{
45+
lock (_statusLock)
46+
{
47+
if (EqualityComparer<TStatus>.Default.Equals(_status, newStatus))
48+
{
49+
return;
50+
}
51+
52+
_status = newStatus;
53+
_onStatusChanged?.Invoke(_status);
54+
}
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace FeatBit.Sdk.Server.DataSynchronizer;
2+
3+
public enum DataSynchronizerStatus
4+
{
5+
/// <summary>
6+
/// The initial state of the synchronizer when the SDK is being initialized.
7+
/// </summary>
8+
/// <remarks>
9+
/// If it encounters an error that requires it to retry initialization, the state will remain at
10+
/// <see cref="Starting"/> until it either succeeds and becomes <see cref="Stable"/>, or
11+
/// permanently fails and becomes <see cref="Stopped"/>.
12+
/// </remarks>
13+
Starting,
14+
15+
/// <summary>
16+
/// Indicates that the synchronizer is currently operational and has not had any problems since the
17+
/// last time it received data.
18+
/// </summary>
19+
/// <remarks>
20+
/// In streaming mode, this means that there is currently an open stream connection and that at least
21+
/// one initial message has been received on the stream. In polling mode, it means that the last poll
22+
/// request succeeded.
23+
/// </remarks>
24+
Stable,
25+
26+
/// <summary>
27+
/// Indicates that the synchronizer encountered an error that it will attempt to recover from.
28+
/// </summary>
29+
/// <remarks>
30+
/// In streaming mode, this means that the stream connection failed, or had to be dropped due to some
31+
/// other error, and will be retried after a backoff delay. In polling mode, it means that the last poll
32+
/// request failed, and a new poll request will be made after the configured polling interval.
33+
/// </remarks>
34+
Interrupted,
35+
36+
/// <summary>
37+
/// Indicates that the synchronizer has been permanently shut down.
38+
/// </summary>
39+
/// <remarks>
40+
/// This could be because it encountered an unrecoverable error (for instance, the Evaluation server
41+
/// rejected the SDK key: an invalid SDK key will never become valid), or because the SDK client was
42+
/// explicitly shut down.
43+
/// </remarks>
44+
Stopped
45+
}

src/FeatBit.ServerSdk/DataSynchronizer/IDataSynchronizer.cs

+19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Threading.Tasks;
23

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

13+
/// <summary>
14+
/// The current status of the data synchronizer.
15+
/// </summary>
16+
public DataSynchronizerStatus Status { get; }
17+
18+
/// <summary>An event for receiving notifications of status changes.</summary>
19+
/// <remarks>
20+
/// <para>
21+
/// Any handlers attached to this event will be notified whenever any property of the status has changed.
22+
/// See <see cref="T:FeatBit.Sdk.Server.DataSynchronizer.DataSynchronizerStatus" /> for an explanation of the meaning of each property and what could cause it
23+
/// to change.
24+
/// </para>
25+
/// <para>
26+
/// The listener should return as soon as possible so as not to block subsequent notifications.
27+
/// </para>
28+
/// </remarks>
29+
event Action<DataSynchronizerStatus> StatusChanged;
30+
1231
/// <summary>
1332
/// Starts the data synchronizer. This is called once from the <see cref="FbClient"/> constructor.
1433
/// </summary>
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
1+
using System;
12
using System.Threading.Tasks;
3+
using FeatBit.Sdk.Server.Concurrent;
24

35
namespace FeatBit.Sdk.Server.DataSynchronizer;
46

57
internal sealed class NullDataSynchronizer : IDataSynchronizer
68
{
9+
private readonly StatusManager<DataSynchronizerStatus> _statusManager;
10+
711
public bool Initialized => true;
12+
public DataSynchronizerStatus Status => _statusManager.Status;
13+
public event Action<DataSynchronizerStatus> StatusChanged;
14+
15+
public NullDataSynchronizer()
16+
{
17+
_statusManager = new StatusManager<DataSynchronizerStatus>(
18+
DataSynchronizerStatus.Stable,
19+
OnStatusChanged
20+
);
21+
}
822

923
public Task<bool> StartAsync()
1024
{
25+
_statusManager.SetStatus(DataSynchronizerStatus.Stable);
1126
return Task.FromResult(true);
1227
}
1328

1429
public Task StopAsync()
1530
{
31+
_statusManager.SetStatus(DataSynchronizerStatus.Stopped);
1632
return Task.CompletedTask;
1733
}
34+
35+
private void OnStatusChanged(DataSynchronizerStatus status) => StatusChanged?.Invoke(status);
1836
}

0 commit comments

Comments
 (0)