Skip to content

Commit

Permalink
Feature/update settings (#225)
Browse files Browse the repository at this point in the history
Added SetOptions method to update client settings
Added SocketConnection parameter to PeriodicQuery callback
Added setting of DefaultProxyCredentials on HttpClient instance when client is not provided by DI
Added support for overriding request time out per request
Changed max wait time for close handshake response from 5 seconds to 1 second
Fixed exception in trade tracker when there is no data in the initial snapshot
  • Loading branch information
JKorf authored Dec 23, 2024
1 parent 8605196 commit 0be1bb1
Show file tree
Hide file tree
Showing 15 changed files with 159 additions and 32 deletions.
11 changes: 11 additions & 0 deletions CryptoExchange.Net/Clients/BaseApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
}

/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;

ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
if (options.ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
}

/// <summary>
/// Dispose
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions CryptoExchange.Net/Clients/RestApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,14 @@ protected internal IDictionary<string, object> CreateParameterDictionary(IDictio
/// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();

/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
base.SetOptions(options);

RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
}

internal async Task<WebCallResult<bool>> SyncTimeAsync()
{
var timeSyncParams = GetTimeSyncInfo();
Expand Down
24 changes: 22 additions & 2 deletions CryptoExchange.Net/Clients/SocketApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ protected virtual void SetDedicatedConnection(string url, bool auth)
/// <param name="interval"></param>
/// <param name="queryDelegate"></param>
/// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback)
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{
Expand Down Expand Up @@ -422,9 +422,10 @@ public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection s
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult(result.Error)!;
}

_logger.Authenticated(socket.SocketId);
}

_logger.Authenticated(socket.SocketId);
socket.Authenticated = true;
return new CallResult(null);
}
Expand Down Expand Up @@ -710,6 +711,25 @@ public virtual async Task<CallResult> PrepareConnectionsAsync()
return new CallResult(null);
}

/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
var previousProxyIsSet = ClientOptions.Proxy != null;
base.SetOptions(options);

if ((!previousProxyIsSet && options.Proxy == null)
|| !socketConnections.Any())
{
return;
}

_logger.LogInformation("Reconnecting websockets to apply proxy");

// Update proxy, also triggers reconnect
foreach (var connection in socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}

/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions CryptoExchange.Net/Interfaces/IBaseApiClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;

Expand Down Expand Up @@ -31,5 +32,12 @@ public interface IBaseApiClient
/// <typeparam name="T"></typeparam>
/// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;

/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <typeparam name="T">Api crentials type</typeparam>
/// <param name="options">Options to set</param>
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
}
}
9 changes: 8 additions & 1 deletion CryptoExchange.Net/Interfaces/IRequestFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ public interface IRequestFactory
/// <param name="requestTimeout">Request timeout to use</param>
/// <param name="httpClient">Optional shared http client instance</param>
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null);
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);

/// <summary>
/// Update settings
/// </summary>
/// <param name="proxy">Proxy to use</param>
/// <param name="requestTimeout">Request timeout to use</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
}
}
5 changes: 5 additions & 0 deletions CryptoExchange.Net/Interfaces/IWebsocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,10 @@ public interface IWebsocket: IDisposable
/// </summary>
/// <returns></returns>
Task CloseAsync();

/// <summary>
/// Update proxy setting
/// </summary>
void UpdateProxy(ApiProxy? proxy);
}
}
29 changes: 29 additions & 0 deletions CryptoExchange.Net/Objects/Options/UpdateOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using CryptoExchange.Net.Authentication;
using System;
using System.Collections.Generic;
using System.Text;

namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Options to update
/// </summary>
public class UpdateOptions<T> where T : ApiCredentials
{
/// <summary>
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Api credentials
/// </summary>
public T? ApiCredentials { get; set; }
/// <summary>
/// Request timeout
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
}

/// <inheritdoc />
public class UpdateOptions : UpdateOptions<ApiCredentials> { }
}
55 changes: 33 additions & 22 deletions CryptoExchange.Net/Requests/RequestFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,7 @@ public class RequestFactory : IRequestFactory
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
{
if (client == null)
{
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
}
catch (PlatformNotSupportedException) { }

if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}

client = new HttpClient(handler)
{
Timeout = requestTimeout
};
}
client = CreateClient(proxy, requestTimeout);

_httpClient = client;
}
Expand All @@ -51,5 +30,37 @@ public IRequest Create(HttpMethod method, Uri uri, int requestId)

return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
}

/// <inheritdoc />
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout)
{
_httpClient = CreateClient(proxy, requestTimeout);
}

private HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
{
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }

if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}

var client = new HttpClient(handler)
{
Timeout = requestTimeout
};
return client;
}
}
}
10 changes: 8 additions & 2 deletions CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ public CryptoExchangeWebSocketClient(ILogger logger, WebSocketParameters websock
_baseAddress = $"{Uri.Scheme}://{Uri.Host}";
}

/// <inheritdoc />
public void UpdateProxy(ApiProxy? proxy)
{
Parameters.Proxy = proxy;
}

/// <inheritdoc />
public virtual async Task<CallResult> ConnectAsync()
{
Expand Down Expand Up @@ -435,8 +441,8 @@ private async Task CloseInternalAsync()
{
// Wait until we receive close confirmation
await Task.Delay(10).ConfigureAwait(false);
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5))
break; // Wait for max 5 seconds, then just abort the connection
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1))
break; // Wait for max 1 second, then just abort the connection
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ public class PeriodicTaskRegistration
/// <summary>
/// Callback after query
/// </summary>
public Action<CallResult>? Callback { get; set; }
public Action<SocketConnection, CallResult>? Callback { get; set; }
}
}
5 changes: 5 additions & 0 deletions CryptoExchange.Net/Sockets/Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public abstract class Query : IMessageProcessor
/// </summary>
public bool Completed { get; set; }

/// <summary>
/// Timeout for the request
/// </summary>
public TimeSpan? RequestTimeout { get; set; }

/// <summary>
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
/// and each symbol receives it's own confirmation response
Expand Down
18 changes: 15 additions & 3 deletions CryptoExchange.Net/Sockets/SocketConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Logging.Extensions;
using System.Threading;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Authentication;

namespace CryptoExchange.Net.Sockets
{
Expand Down Expand Up @@ -437,7 +439,7 @@ protected virtual Task HandleRequestSentAsync(int requestId)
return Task.CompletedTask;
}

query.IsSend(ApiClient.ClientOptions.RequestTimeout);
query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout);
return Task.CompletedTask;
}

Expand Down Expand Up @@ -583,6 +585,16 @@ protected virtual async Task HandleStreamMessage(WebSocketMessageType type, Read
/// <returns></returns>
public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false);

/// <summary>
/// Update the proxy setting and reconnect
/// </summary>
/// <param name="proxy">New proxy setting</param>
public async Task UpdateProxy(ApiProxy? proxy)
{
_socket.UpdateProxy(proxy);
await TriggerReconnectAsync().ConfigureAwait(false);
}

/// <summary>
/// Close the connection
/// </summary>
Expand Down Expand Up @@ -988,7 +1000,7 @@ internal async Task<CallResult> ResubscribeAsync(Subscription subscription)
/// <param name="interval">How often</param>
/// <param name="queryDelegate">Method returning the query to send</param>
/// <param name="callback">The callback for processing the response</param>
public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback)
public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{
if (queryDelegate == null)
throw new ArgumentNullException(nameof(queryDelegate));
Expand Down Expand Up @@ -1020,7 +1032,7 @@ public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<Soc
try
{
var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false);
callback?.Invoke(result);
callback?.Invoke(this, result);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ public IRequest Create(HttpMethod method, Uri uri, int requestId)
_request.RequestId = requestId;
return _request;
}

public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {}
}
}
2 changes: 2 additions & 0 deletions CryptoExchange.Net/Testing/Implementations/TestSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,7 @@ public void InvokeMessage<T>(T data)

public Task ReconnectAsync() => throw new NotImplementedException();
public void Dispose() { }

public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException();
}
}
3 changes: 2 additions & 1 deletion CryptoExchange.Net/Trackers/Trades/TradeTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,8 @@ protected void SetInitialData(IEnumerable<SharedTrade> data)
_data.Add(item);
}

_firstTimestamp = _data.Min(v => v.Timestamp);
if (_data.Any())
_firstTimestamp = _data.Min(v => v.Timestamp);

ApplyWindow(false);
}
Expand Down

0 comments on commit 0be1bb1

Please sign in to comment.