using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Interfaces;
using CryptoExchange.Net.Sockets.HighPerf;
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
using CryptoExchange.Net.Sockets.Interfaces;
using CryptoExchange.Net.TokenManagement;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Clients
{
///
/// Base socket API client for interaction with a websocket API
///
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
{
#region Fields
///
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
///
public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
///
/// List of socket connections currently connecting/connected
///
protected internal ConcurrentDictionary _socketConnections = new();
///
/// List of HighPerf socket connections currently connecting/connected
///
protected internal ConcurrentDictionary _highPerfSocketConnections = new();
///
/// Semaphore used while creating sockets
///
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
///
/// Keep alive interval for websocket connection
///
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
///
/// Keep alive timeout for websocket connection
///
protected TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10);
///
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
///
protected List systemSubscriptions = new();
///
/// The rate limiters
///
protected internal IRateLimitGate? RateLimiter { get; set; }
///
/// The max size a websocket message size can be
///
protected internal int? MessageSendSizeLimit { get; set; }
///
/// Periodic task registrations
///
protected List PeriodicTaskRegistrations { get; set; } = new List();
///
/// List of address to keep an alive connection to
///
protected List DedicatedConnectionConfigs { get; set; } = new List();
///
/// Whether to allow multiple subscriptions with the same topic on the same connection
///
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
///
/// Whether to allow usage of a current public WebSocket connection for an authenticated request
///
protected bool UsePublicConnectionForAuth { get; set; } = false;
///
public double IncomingKbps
{
get
{
if (_socketConnections.IsEmpty)
return 0;
return _socketConnections.Sum(s => s.Value.IncomingKbps);
}
}
///
public int CurrentConnections => _socketConnections.Count;
///
public int CurrentSubscriptions
{
get
{
if (_socketConnections.IsEmpty)
return 0;
return _socketConnections.Sum(s => s.Value.UserSubscriptionCount);
}
}
///
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
///
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
///
/// The max number of individual subscriptions on a single connection
///
public int? MaxIndividualSubscriptionsPerConnection { get; set; }
///
/// Whether or not to enforce that sequence number updates are always (lastSequenceNumber + 1)
///
public bool EnforceSequenceNumbers { get; set; }
///
/// Get the AuthenticationProvider implementation, or null if no ApiCredentials are set
///
public virtual AuthenticationProvider? GetAuthenticationProvider() => null;
///
/// Configured environment name
///
public abstract string EnvironmentName { get; }
private int _isDisposed;
#endregion
///
/// ctor
///
/// Logger factory
/// Exchange name
/// Client options
/// Base address for this API client
/// The Api client options
public SocketApiClient(
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions options,
SocketApiOptions apiOptions)
: base(loggerFactory,
exchangeName,
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
baseAddress,
options,
apiOptions)
{
}
///
/// Create a serializer instance
///
///
protected internal abstract IMessageSerializer CreateSerializer();
///
/// Keep an open connection to this url
///
///
///
protected virtual void SetDedicatedConnection(string url, bool auth)
{
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
}
///
/// Update the timestamp offset between client and server based on the timestamp
///
/// Timestamp received from the server
public virtual void UpdateTimeOffset(DateTime timestamp)
{
if (timestamp == default)
return;
TimeOffsetManager.UpdateSocketOffset(ClientName, (DateTime.UtcNow - timestamp).TotalMilliseconds);
}
///
/// Get the time offset between client and server
///
///
public virtual TimeSpan? GetTimeOffset() => TimeOffsetManager.GetSocketOffset(ClientName);
///
/// Add a query to periodically send on each connection
///
///
///
///
///
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func queryDelegate, Action? callback)
{
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{
Identifier = identifier,
Callback = callback,
Interval = interval,
QueryDelegate = queryDelegate
});
}
///
/// Connect to an url and listen for data on the BaseAddress
///
/// The subscription
/// Cancellation token for closing this subscription
///
protected virtual Task> SubscribeAsync(Subscription subscription, CancellationToken ct)
{
return SubscribeAsync(BaseAddress, subscription, ct);
}
///
/// Connect to an url and listen for data
///
/// The URL to connect to
/// The subscription
/// Cancellation token for closing this subscription
///
protected virtual async Task> SubscribeAsync(string url, Subscription subscription, CancellationToken ct)
{
bool successResult = false;
try
{
if (_disposed)
return WebSocketResult.Fail(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
if (subscription.Authenticated && GetAuthenticationProvider() == null)
{
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
return WebSocketResult.Fail(Exchange, new NoApiCredentialsError());
}
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
return WebSocketResult.Fail(Exchange, ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
SocketConnection socketConnection;
var released = false;
// Wait for a semaphore here, so we only connect 1 socket at a time.
// This is necessary for being able to see if connections can be combined
try
{
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException tce)
{
return WebSocketResult.Fail(Exchange, new CancellationRequestedError(tce));
}
try
{
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
if (!socketResult.Success)
return WebSocketResult.Fail(Exchange, socketResult.Error);
socketConnection = socketResult.Data;
// Add a subscription on the socket connection
var success = socketConnection.AddSubscription(subscription);
if (!success)
{
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
continue;
}
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
{
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
semaphoreSlim.Release();
released = true;
}
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
if (!connectResult.Success)
return WebSocketResult.Fail(Exchange, connectResult.Error!);
break;
}
}
finally
{
if (!released)
semaphoreSlim.Release();
}
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
return WebSocketResult.Fail(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
if (!subscribeResult.Success)
return WebSocketResult.Fail(Exchange, subscribeResult.Error!);
successResult = true;
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return WebSocketResult.Ok(
Exchange,
socketConnection.SocketId,
subscribeResult.ResponseTime!.Value,
subscribeResult.RequestId!.Value,
subscribeResult.Url,
new UpdateSubscription(socketConnection, subscription));
}
finally
{
if (!successResult && subscription.TokenLease != null)
_ = subscription.TokenLease.ReleaseAsync();
}
}
///
/// Connect to an url and listen for data
///
/// The URL to connect to
/// The subscription
/// The factory for creating a socket connection
/// Cancellation token for closing this subscription
///
protected virtual async Task> SubscribeHighPerfAsync(
string url,
HighPerfSubscription subscription,
IHighPerfConnectionFactory connectionFactory,
CancellationToken ct)
{
if (_disposed)
return WebSocketResult.Fail(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
HighPerfSocketConnection socketConnection;
var released = false;
// Wait for a semaphore here, so we only connect 1 socket at a time.
// This is necessary for being able to see if connections can be combined
try
{
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException tce)
{
return WebSocketResult.Fail(Exchange, new CancellationRequestedError(tce));
}
try
{
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetHighPerfSocketConnection(url, connectionFactory, ct).ConfigureAwait(false);
if (!socketResult.Success)
return WebSocketResult.Fail(Exchange, socketResult.Error);
socketConnection = socketResult.Data;
// Add a subscription on the socket connection
var success = socketConnection.AddSubscription(subscription);
if (!success)
{
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
continue;
}
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
{
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
semaphoreSlim.Release();
released = true;
}
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
if (!connectResult.Success)
return WebSocketResult.Fail(Exchange, connectResult.Error!);
break;
}
}
finally
{
if (!released)
semaphoreSlim.Release();
}
var subRequest = subscription.CreateSubscriptionQuery(socketConnection);
if (subRequest != null)
{
// Send the request and wait for answer
var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false);
if (!sendResult.Success)
{
await socketConnection.CloseAsync().ConfigureAwait(false);
return WebSocketResult.Fail(Exchange, sendResult.Error!);
}
}
if (ct != default)
{
subscription.CancellationTokenRegistration = ct.Register(async () =>
{
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
await socketConnection.CloseAsync().ConfigureAwait(false);
}, false);
}
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return WebSocketResult.Ok(
Exchange,
socketConnection.SocketId,
default,
default,
socketConnection.ConnectionUri.ToString(),
new HighPerfUpdateSubscription(socketConnection, subscription));
}
///
/// Send a query on a socket connection to the BaseAddress and wait for the response
///
/// Expected result type
/// The query
/// Cancellation token
///
protected virtual Task> QueryAsync(Query query, CancellationToken ct = default)
{
return QueryAsync(BaseAddress, query, ct);
}
///
/// Send a query on a socket connection and wait for the response
///
/// Expected result type
/// The url for the request
/// The query
/// Cancellation token
///
protected virtual async Task> QueryAsync(string url, Query query, CancellationToken ct = default)
{
if (_disposed)
return QueryResult.Fail(Exchange, new InvalidOperationError("Client disposed, can't query"));
if (ct.IsCancellationRequested)
return QueryResult.Fail(Exchange, new CancellationRequestedError());
SocketConnection socketConnection;
var released = false;
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
if (!socketResult.Success)
return QueryResult.Fail(Exchange, socketResult.Error);
socketConnection = socketResult.Data;
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
{
// Can release early when only a single sub per connection
semaphoreSlim.Release();
released = true;
}
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
if (!connectResult.Success)
return QueryResult.Fail(Exchange, connectResult.Error!);
}
finally
{
if (!released)
semaphoreSlim.Release();
}
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
return QueryResult.Fail(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
if (ct.IsCancellationRequested)
return QueryResult.Fail(Exchange, new CancellationRequestedError());
return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false);
}
///
/// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed
///
/// The connection to check
/// Whether the socket should authenticated
/// Cancellation token
///
protected virtual async Task ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
{
if (socket.Connected)
return CallResult.Ok();
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
if (!connectResult.Success)
return connectResult;
if (!authenticated || socket.Authenticated)
return CallResult.Ok();
if (socket is not SocketConnection sc)
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
if (!result.Success)
await socket.CloseAsync().ConfigureAwait(false);
return result;
}
///
/// Authenticate a socket connection
///
/// Socket to authenticate
///
public virtual async Task AuthenticateSocketAsync(SocketConnection socket)
{
if (GetAuthenticationProvider() == null)
return CallResult.Fail(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
if (authRequest != null)
{
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
if (!result.Success)
{
_logger.AuthenticationFailed(socket.SocketId);
if (socket.Connected)
await socket.CloseAsync().ConfigureAwait(false);
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return CallResult.Fail(result.Error)!;
}
_logger.Authenticated(socket.SocketId);
}
socket.Authenticated = true;
return CallResult.Ok();
}
///
/// Should return the request which can be used to authenticate a socket connection
///
///
protected internal virtual Task GetAuthenticationRequestAsync(SocketConnection connection) =>
Task.FromResult(GetAuthenticationProvider()!.GetAuthenticationQuery(this, connection));
///
/// Adds a system subscription. Used for example to reply to ping requests
///
/// The subscription
protected void AddSystemSubscription(SystemSubscription systemSubscription)
{
systemSubscriptions.Add(systemSubscription);
foreach (var connection in _socketConnections.Values)
connection.AddSubscription(systemSubscription);
}
///
/// Get the url to connect to (defaults to BaseAddress form the client options)
///
///
///
///
protected virtual Task> GetConnectionUrlAsync(string address, bool authentication)
{
return Task.FromResult(CallResult.Ok(address));
}
///
/// Get the url to reconnect to after losing a connection
///
///
///
protected internal virtual Task GetReconnectUriAsync(ISocketConnection connection)
{
return Task.FromResult(connection.ConnectionUri);
}
///
/// Update the subscription when the connection is restored after disconnecting. Can be used to update an authentication token for example.
///
/// The subscription
///
protected internal virtual Task RevitalizeRequestAsync(Subscription subscription)
{
return Task.FromResult(CallResult.Ok());
}
///
/// Whether the connection can be used for a new subscription or query with the provided parameters
///
/// The connection to check
/// The address set by the request
/// Whether the request needs an authenticated connection
/// Topic of the request
/// True if connection can be used
protected virtual bool ConnectionCanBeUsedFor(SocketConnection connection, string address, bool authenticated, string? topic = null)
{
return connection.ConnectionUriString.Equals(address.TrimEnd('/'), StringComparison.Ordinal)
&& connection.ApiClient.ClientName.Equals(ClientName, StringComparison.Ordinal)
&& (AllowTopicsOnTheSameConnection || !connection.Topics.Contains(topic));
}
///
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
///
/// The address the socket is for
/// Whether the socket should be authenticated
/// Whether a dedicated request connection should be returned
/// Cancellation token
/// The subscription topic, can be provided when multiple of the same topics are not allowed on a connection
/// The number of individual subscriptions in this subscribe request
///
protected virtual async Task> GetSocketConnection(
string address,
bool authenticated,
bool dedicatedRequestConnection,
CancellationToken ct,
string? topic = null,
int individualSubscriptionCount = 1)
{
var socketQuery = _socketConnections.Where(s => ConnectionCanBeUsedFor(s.Value, address, authenticated, topic)).Select(x => x.Value); // Don't ToList this so the query is executed again when called
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
var delayStart = DateTime.UtcNow;
var delayed = false;
while (socketQuery.Count() >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
{
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
{
if (socketQuery.Count() >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
{
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
_logger.TimeoutWaitingForReconnectingSocket();
return CallResult.Fail(new CantConnectError());
}
break;
}
delayed = true;
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
if (ct.IsCancellationRequested)
return CallResult.Fail(new CancellationRequestedError());
}
if (delayed)
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
socketQuery = socketQuery.Where(s => (s.Status == SocketStatus.None || s.Status == SocketStatus.Connected)
&& (s.Authenticated == authenticated || !authenticated || UsePublicConnectionForAuth)
&& s.Connected).ToList();
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
SocketConnection? connection = null;
if (dedicatedRequestConnection)
{
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated;
}
if (connection == null)
{
// Use an eligible non-dedicated connection for subscriptions, or as fallback when no dedicated request connection is available
connection = socketQuery
.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
.Where(s => IsConnectionEligible(s, individualSubscriptionCount, maxConnectionsReached))
.OrderBy(s => s.UserSubscriptionCount)
.FirstOrDefault();
}
if (connection != null)
return CallResult.Ok(connection);
if (maxConnectionsReached)
return CallResult.Fail(new InvalidOperationError("Max amount of socket connections reached"));
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
if (!connectionAddress.Success)
{
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
return CallResult.Fail(connectionAddress.Error);
}
if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket connection
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this);
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
if (dedicatedRequestConnection)
{
socketConnection.DedicatedRequestConnection = new DedicatedConnectionState
{
IsDedicatedRequestConnection = dedicatedRequestConnection,
Authenticated = authenticated
};
}
foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
foreach (var systemSubscription in systemSubscriptions)
socketConnection.AddSubscription(systemSubscription);
return CallResult.Ok(socketConnection);
}
///
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
///
/// The address the socket is for
/// The factory for creating a socket connection
/// Cancellation token
///
protected virtual async Task>> GetHighPerfSocketConnection(
string address,
IHighPerfConnectionFactory connectionFactory,
CancellationToken ct)
{
var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false);
if (!connectionAddress.Success)
{
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
return CallResult.Fail>(connectionAddress.Error);
}
if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket connection
var socketConnection = connectionFactory.CreateHighPerfConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this);
foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request);
return CallResult.Ok(socketConnection);
}
private bool IsConnectionEligible(SocketConnection socketConnection, int individualSubscriptionCount, bool maxConnectionsReached)
{
var currentIndividualSubscriptionCount = socketConnection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
bool lessThanBatchSubCombineTarget = socketConnection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
// Include the incoming batch so batched subscriptions cannot overshoot the configured socket target.
bool lessThanIndividualSubCombineTarget = currentIndividualSubscriptionCount + individualSubscriptionCount <= ClientOptions.SocketIndividualSubscriptionCombineTarget;
if ((!lessThanBatchSubCombineTarget || !lessThanIndividualSubCombineTarget)
&& !maxConnectionsReached)
{
return false;
}
return MaxIndividualSubscriptionsPerConnection == null
|| currentIndividualSubscriptionCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection;
}
///
/// Process an unhandled message
///
/// The socket connection
/// The type as identified
/// The data
protected internal virtual bool HandleUnhandledMessage(SocketConnection connection, string typeIdentifier, ReadOnlySpan data) => false;
///
/// Process connect rate limited
///
protected async virtual Task HandleConnectRateLimitedAsync()
{
if (ClientOptions.RateLimiterEnabled && ClientOptions.ConnectDelayAfterRateLimited.HasValue)
{
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
_logger.AddingRetryAfterGuard(retryAfter);
RateLimiter ??= new RateLimitGate("Connection");
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimitItemType.Connection).ConfigureAwait(false);
}
}
///
/// Connect a socket
///
/// The socket to connect
/// Cancellation token
///
protected virtual async Task ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct)
{
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
if (connectResult.Success)
{
if (socketConnection is SocketConnection sc)
_socketConnections.TryAdd(socketConnection.SocketId, sc);
else if (socketConnection is HighPerfSocketConnection hsc)
_highPerfSocketConnections.TryAdd(socketConnection.SocketId, hsc);
return connectResult;
}
socketConnection.Dispose();
return connectResult;
}
///
/// Get parameters for the websocket connection
///
/// The address to connect to
///
protected virtual WebSocketParameters GetWebSocketParameters(string address)
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
{
KeepAliveInterval = KeepAliveInterval,
KeepAliveTimeout = KeepAliveTimeout,
ReconnectInterval = ClientOptions.ReconnectInterval,
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
RateLimitAdmissionCallbackOptions = (def, weight) => ClientOptions.RateLimitAdmission?.Invoke(def, weight),
RateLimitAdmissionCallbackRequest = () => AdmissionOverride.Value,
Proxy = ClientOptions.Proxy,
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
ReceiveBufferSize = ClientOptions.ReceiveBufferSize
};
///
/// Unsubscribe an update subscription
///
/// The id of the subscription to unsubscribe
///
public virtual async Task UnsubscribeAsync(int subscriptionId)
{
Subscription? subscription = null;
SocketConnection? connection = null;
foreach (var socket in _socketConnections.Values.ToList())
{
subscription = socket.GetSubscription(subscriptionId);
if (subscription != null)
{
connection = socket;
break;
}
}
if (subscription == null || connection == null)
return false;
_logger.UnsubscribingSubscription(connection.SocketId, subscriptionId);
await connection.CloseAsync(subscription).ConfigureAwait(false);
return true;
}
///
/// Unsubscribe an update subscription
///
/// The subscription to unsubscribe
///
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
{
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
///
/// Unsubscribe all subscriptions
///
///
public virtual async Task UnsubscribeAllAsync()
{
var sum = _socketConnections.Sum(s => s.Value.UserSubscriptionCount) + _highPerfSocketConnections.Sum(s => s.Value.UserSubscriptionCount);
if (sum == 0)
return;
_logger.UnsubscribingAll(sum);
var tasks = new List();
var socketList = _socketConnections.Values;
foreach (var connection in socketList)
{
foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription))
tasks.Add(connection.CloseAsync(subscription));
}
var highPerfSocketList = _highPerfSocketConnections.Values;
foreach (var connection in highPerfSocketList)
tasks.Add(connection.CloseAsync());
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
///
/// Reconnect all connections
///
///
public virtual async Task ReconnectAsync()
{
_logger.ReconnectingAllConnections(_socketConnections.Count);
var tasks = new List();
{
var socketList = _socketConnections.Values;
foreach (var sub in socketList)
tasks.Add(sub.TriggerReconnectAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
///
public virtual async Task PrepareConnectionsAsync()
{
foreach (var item in DedicatedConnectionConfigs)
{
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
if (!socketResult.Success)
return CallResult.Fail(socketResult.Error);
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
if (!connectResult.Success)
return CallResult.Fail(connectResult.Error!);
}
return CallResult.Ok();
}
///
/// Log the current state of connections and subscriptions
///
public string GetSubscriptionsState(bool includeSubDetails = true)
{
return GetState(includeSubDetails).ToString();
}
///
/// Gets the state of the client
///
/// True to get details for each subscription
///
public SocketApiClientState GetState(bool includeSubDetails = true)
{
var connectionStates = new List();
foreach (var socketIdAndConnection in _socketConnections)
{
SocketConnection connection = socketIdAndConnection.Value;
SocketConnectionState connectionState = connection.GetState(includeSubDetails);
connectionStates.Add(connectionState);
}
return new SocketApiClientState(_socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates);
}
///
/// Get the current state of the client
///
/// Number of sockets for this client
/// Total number of subscriptions
/// Total download speed
/// State of each socket connection
public record SocketApiClientState(
int Connections,
int Subscriptions,
double DownloadSpeed,
List ConnectionStates)
{
///
/// Print the state of the client
///
///
///
protected virtual bool PrintMembers(StringBuilder sb)
{
sb.AppendLine();
sb.AppendLine($"\tTotal connections: {Connections}");
sb.AppendLine($"\tTotal subscriptions: {Subscriptions}");
sb.AppendLine($"\tDownload speed: {DownloadSpeed} kbps");
sb.AppendLine($"\tConnections:");
ConnectionStates.ForEach(cs =>
{
sb.AppendLine($"\t\tId: {cs.Id}");
sb.AppendLine($"\t\tAddress: {cs.Address}");
sb.AppendLine($"\t\tTotal subscriptions: {cs.Subscriptions}");
sb.AppendLine($"\t\tStatus: {cs.Status}");
sb.AppendLine($"\t\tAuthenticated: {cs.Authenticated}");
sb.AppendLine($"\t\tDownload speed: {cs.DownloadSpeed} kbps");
sb.AppendLine($"\t\tPending queries: {cs.PendingQueries}");
if (cs.SubscriptionStates?.Count > 0)
{
sb.AppendLine($"\t\tSubscriptions:");
cs.SubscriptionStates.ForEach(subState =>
{
sb.AppendLine($"\t\t\tId: {subState.Id}");
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
});
}
});
return true;
}
}
///
/// Dispose the client
///
protected override void Dispose(bool disposing)
{
if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{
if (!disposing)
return;
var tasks = new List();
{
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
if (socketList.Any())
_logger.DisposingSocketClient();
foreach (var connection in socketList)
{
tasks.Add(connection.CloseAsync());
}
}
semaphoreSlim?.Dispose();
base.Dispose(disposing);
}
}
///
/// Preprocess a stream message
///
public virtual ReadOnlySpan PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlySpan data) => data;
///
/// Create a new message converter instance
///
///
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
///
public virtual void SetOptions(UpdateOptions options)
{
var previousProxyIsSet = _proxyConfigured;
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
_proxyConfigured = options.Proxy != null;
if ((!previousProxyIsSet && options.Proxy == null)
|| _socketConnections.IsEmpty)
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in _socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
}
///
public abstract class SocketApiClient : SocketApiClient, ISocketApiClient
where TEnvironment : TradeEnvironment
{
///
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
///
public override string EnvironmentName => ClientOptions.Environment.Name;
///
/// ctor
///
protected SocketApiClient(
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions options,
SocketApiOptions apiOptions) : base(
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
{
}
}
///
public abstract class SocketApiClient : SocketApiClient, ISocketApiClient
where TApiCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
///
public TApiCredentials? ApiCredentials { get; set; }
///
public bool Authenticated => ApiCredentials != null;
///
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
///
/// ctor
///
protected SocketApiClient(
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions options,
SocketApiOptions apiOptions) : base(
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
{
ApiCredentials = options.ApiCredentials;
}
///
public virtual void SetApiCredentials(TApiCredentials credentials)
{
ApiCredentials = (TApiCredentials)credentials.Copy();
}
///
public virtual void SetOptions(UpdateOptions options)
{
base.SetOptions(options);
}
}
///
public abstract class SocketApiClient : SocketApiClient
where TAuthenticationProvider : AuthenticationProvider
where TApiCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
///
/// Auth provider initialized field
///
protected bool _authProviderInitialized = false;
///
/// Auth provider field
///
protected TAuthenticationProvider? _authenticationProvider;
///
/// The authentication provider for this API client. (null if no credentials are set)
///
public virtual TAuthenticationProvider? AuthenticationProvider
{
get
{
if (!_authProviderInitialized)
{
if (ApiCredentials != null)
_authenticationProvider = CreateAuthenticationProvider(ApiCredentials);
_authProviderInitialized = true;
}
return _authenticationProvider;
}
protected internal set => _authenticationProvider = value;
}
///
public override AuthenticationProvider? GetAuthenticationProvider() => AuthenticationProvider;
///
/// ctor
///
protected SocketApiClient(
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions options,
SocketApiOptions apiOptions) : base(
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
{
}
///
/// Create an AuthenticationProvider implementation instance based on the provided credentials
///
///
///
protected abstract TAuthenticationProvider CreateAuthenticationProvider(TApiCredentials credentials);
///
public override void SetApiCredentials(TApiCredentials credentials)
{
AuthenticationProvider = null;
_authProviderInitialized = false;
ApiCredentials = credentials;
base.SetApiCredentials(credentials);
}
///
public override void SetOptions(UpdateOptions options)
{
if (options.ApiCredentials != null)
{
AuthenticationProvider = null;
_authProviderInitialized = false;
ApiCredentials = options.ApiCredentials;
}
base.SetOptions(options);
}
}
}