1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-19 20:33:03 +00:00

Time offset management (#266)

Updated time sync / time offset management for REST API's
Added time offset tracking for WebSocket API's
Added GetAuthenticationQuery virtual method on AuthenticationProvider
Updated AuthenticationProvider GetTimestamp methods to include a one second offset by default
Added AuthenticationProvider GetTimestamp methods for SocketApiClient instances
Added ClientName property on BaseApiClient, resolving to the type name
Added ObjectOrArrayConverter JsonConverterFactory implementation for resolving json data which might be returned as object or array
Added UpdateServerTime, UpdateLocalTime and DataAge properties to (I)SymbolOrderBook
Added OutputToConsoleAsync method to (I)SymbolOrderBook
Updated SymbolOrderBook string representation
Added DataTimeLocal and DataAge properties to DataEvent object
Added SocketConnection parameter to subscription HandleSubQueryResponse and HandleUnsubQueryResponse methods
This commit is contained in:
Jan Korf
2026-01-07 10:00:14 +01:00
committed by GitHub
parent 177daf903b
commit a896fffdb3
17 changed files with 536 additions and 213 deletions
@@ -13,6 +13,8 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public abstract class BaseApiClient : IDisposable, IBaseApiClient
{
private string? _clientName;
/// <summary>
/// Logger
/// </summary>
@@ -23,6 +25,21 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected bool _disposing;
/// <summary>
/// Name of the client
/// </summary>
protected internal string ClientName
{
get
{
if (_clientName != null)
return _clientName;
_clientName = GetType().Name;
return _clientName;
}
}
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
+52 -51
View File
@@ -32,12 +32,6 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// <inheritdoc />
public abstract TimeSyncInfo? GetTimeSyncInfo();
/// <inheritdoc />
public abstract TimeSpan? GetTimeOffset();
/// <inheritdoc />
public int TotalRequestsMade { get; set; }
@@ -115,6 +109,8 @@ namespace CryptoExchange.Net.Clients
options,
apiOptions)
{
TimeOffsetManager.RegisterRestApi(ClientName);
RequestFactory.Configure(options, httpClient);
}
@@ -241,11 +237,9 @@ namespace CryptoExchange.Net.Clients
{
currentTry++;
var error = await CheckTimeSync(requestId, definition).ConfigureAwait(false);
if (error != null)
return new WebCallResult<T>(error);
await CheckTimeSync(requestId, definition).ConfigureAwait(false);
error = await RateLimitAsync(
var error = await RateLimitAsync(
baseAddress,
requestId,
definition,
@@ -300,28 +294,6 @@ namespace CryptoExchange.Net.Clients
}
}
private async ValueTask<Error?> CheckTimeSync(int requestId, RequestDefinition definition)
{
if (!definition.Authenticated)
return null;
var syncTask = SyncTimeAsync();
var timeSyncInfo = GetTimeSyncInfo();
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
{
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
var syncTimeError = await syncTask.ConfigureAwait(false);
if (syncTimeError != null)
{
_logger.RestApiFailedToSyncTime(requestId, syncTimeError!.ToString());
return syncTimeError;
}
}
return null;
}
/// <summary>
/// Check rate limits for the request
/// </summary>
@@ -725,26 +697,44 @@ namespace CryptoExchange.Net.Clients
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
}
internal async ValueTask<Error?> SyncTimeAsync()
private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition)
{
var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null)
return null;
if (!definition.Authenticated)
return;
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
var lastUpdateTime = TimeOffsetManager.GetRestLastUpdateTime(ClientName);
var syncTask = CheckTimeOffsetAsync();
if (lastUpdateTime == null)
{
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return null;
}
// Initially with first request we'll need to wait for the time syncing before making the actual request.
// If it's not the first request we can just continue and let it complete in the background
await syncTask.ConfigureAwait(false);
}
return;
}
internal async ValueTask CheckTimeOffsetAsync()
{
if (!(ApiOptions.AutoTimestamp ?? ClientOptions.AutoTimestamp))
// Time syncing not enabled
return;
await TimeOffsetManager.EnterAsync(ClientName).ConfigureAwait(false);
try
{
var lastUpdateTime = TimeOffsetManager.GetRestLastUpdateTime(ClientName);
if (DateTime.UtcNow - lastUpdateTime < (ApiOptions.TimestampRecalculationInterval ?? ClientOptions.TimestampRecalculationInterval))
// Time syncing was recently done
return;
var localTime = DateTime.UtcNow;
var result = await GetServerTimestampAsync().ConfigureAwait(false);
if (!result)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return result.Error;
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
return;
}
if (TotalRequestsMade == 1)
@@ -754,18 +744,29 @@ namespace CryptoExchange.Net.Clients
result = await GetServerTimestampAsync().ConfigureAwait(false);
if (!result)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return result.Error;
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
return;
}
}
// Calculate time offset between local and server
// Estimate the offset as the round trip time / 2
var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2);
timeSyncParams.UpdateTimeOffset(offset);
timeSyncParams.TimeSyncState.Semaphore.Release();
}
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
{
_logger.LogInformation("{ClientName} Time offset within limits ({Offset}ms), set offset to 0ms", ClientName, Math.Round(offset.TotalMilliseconds));
offset = TimeSpan.Zero;
}
else
{
_logger.LogInformation("{ClientName} Time offset set to {Offset}ms", ClientName, Math.Round(offset.TotalMilliseconds));
}
return null;
TimeOffsetManager.UpdateRestOffset(ClientName, offset.TotalMilliseconds);
}
finally
{
TimeOffsetManager.Release(ClientName);
}
}
private bool ShouldCache(RequestDefinition definition)
+23 -2
View File
@@ -32,8 +32,10 @@ namespace CryptoExchange.Net.Clients
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
{
#region Fields
/// <inheritdoc/>
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
/// <inheritdoc/>
public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
@@ -181,6 +183,24 @@ namespace CryptoExchange.Net.Clients
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
}
/// <summary>
/// Update the timestamp offset between client and server based on the timestamp
/// </summary>
/// <param name="timestamp">Timestamp received from the server</param>
public virtual void UpdateTimeOffset(DateTime timestamp)
{
if (timestamp == default)
return;
TimeOffsetManager.UpdateSocketOffset(ClientName, (DateTime.UtcNow - timestamp).TotalMilliseconds);
}
/// <summary>
/// Get the time offset between client and server
/// </summary>
/// <returns></returns>
public virtual TimeSpan? GetTimeOffset() => TimeOffsetManager.GetSocketOffset(ClientName);
/// <summary>
/// Add a query to periodically send on each connection
/// </summary>
@@ -296,7 +316,7 @@ namespace CryptoExchange.Net.Clients
if (!success)
return;
subscription.HandleSubQueryResponse(response);
subscription.HandleSubQueryResponse(socketConnection, response);
subscription.Status = SubscriptionStatus.Subscribed;
if (ct != default)
{
@@ -575,7 +595,8 @@ namespace CryptoExchange.Net.Clients
/// Should return the request which can be used to authenticate a socket connection
/// </summary>
/// <returns></returns>
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
Task.FromResult(AuthenticationProvider!.GetAuthenticationQuery(this, connection));
/// <summary>
/// Adds a system subscription. Used for example to reply to ping requests