mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-12-14 18:00:26 +00:00
wip
This commit is contained in:
parent
c945176049
commit
68f772a13a
@ -1,17 +1,19 @@
|
|||||||
using System;
|
using CryptoExchange.Net.Objects;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
|
using CryptoExchange.Net.Testing.Implementations;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@ -44,7 +46,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
socket.CanConnect = canConnect;
|
socket.CanConnect = canConnect;
|
||||||
|
|
||||||
//act
|
//act
|
||||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
var connectResult = client.SubClient.ConnectSocketSub(
|
||||||
|
new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
Assert.That(connectResult.Success == canConnect);
|
Assert.That(connectResult.Success == canConnect);
|
||||||
@ -59,7 +62,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
});
|
});
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
var rstEvent = new ManualResetEvent(false);
|
var rstEvent = new ManualResetEvent(false);
|
||||||
Dictionary<string, string> result = null;
|
Dictionary<string, string> result = null;
|
||||||
|
|
||||||
@ -92,7 +95,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
});
|
});
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
var rstEvent = new ManualResetEvent(false);
|
var rstEvent = new ManualResetEvent(false);
|
||||||
string original = null;
|
string original = null;
|
||||||
|
|
||||||
@ -123,7 +126,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
});
|
});
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
client.SubClient.ConnectSocketSub(sub);
|
||||||
|
|
||||||
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||||
@ -146,8 +149,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var socket2 = client.CreateSocket();
|
var socket2 = client.CreateSocket();
|
||||||
socket1.CanConnect = true;
|
socket1.CanConnect = true;
|
||||||
socket2.CanConnect = true;
|
socket2.CanConnect = true;
|
||||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
|
var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket1), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
var sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
client.SubClient.ConnectSocketSub(sub1);
|
client.SubClient.ConnectSocketSub(sub1);
|
||||||
client.SubClient.ConnectSocketSub(sub2);
|
client.SubClient.ConnectSocketSub(sub2);
|
||||||
var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||||
@ -173,7 +176,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = false;
|
socket.CanConnect = false;
|
||||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||||
@ -194,7 +197,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
});
|
});
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
|
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||||
@ -217,7 +220,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
});
|
});
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
|
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||||
|
|||||||
@ -18,6 +18,7 @@ using CryptoExchange.Net.SharedApis;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
@ -94,6 +95,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public Subscription TestSubscription { get; private set; } = null;
|
public Subscription TestSubscription { get; private set; } = null;
|
||||||
|
|
||||||
|
public override JsonSerializerOptions JsonSerializerOptions => new JsonSerializerOptions();
|
||||||
|
|
||||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions) : base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions) : base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Caching
|
namespace CryptoExchange.Net.Caching
|
||||||
{
|
{
|
||||||
internal class MemoryCache
|
internal class MemoryCache
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _lock = new Lock();
|
||||||
|
#else
|
||||||
private readonly object _lock = new object();
|
private readonly object _lock = new object();
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a new cache entry. Will override an existing entry if it already exists
|
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@ -49,7 +50,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal ILogger _logger;
|
protected internal ILogger _logger;
|
||||||
|
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _versionLock = new Lock();
|
||||||
|
#else
|
||||||
private readonly object _versionLock = new object();
|
private readonly object _versionLock = new object();
|
||||||
|
#endif
|
||||||
private Version _exchangeVersion;
|
private Version _exchangeVersion;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>12.0</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -381,13 +381,14 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Queue updates received from a websocket subscriptions and process them async
|
/// Queue updates and process them async
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The queued update type</typeparam>
|
/// <typeparam name="T">The queued update type</typeparam>
|
||||||
/// <param name="subscribeCall">The subscribe call</param>
|
/// <param name="subscribeCall">The subscribe call</param>
|
||||||
/// <param name="asyncHandler">The async update handler</param>
|
/// <param name="asyncHandler">The async update handler</param>
|
||||||
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||||
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||||
|
/// <param name="ct">Cancellation token to stop the processing</param>
|
||||||
public static async Task ProcessQueuedAsync<T>(
|
public static async Task ProcessQueuedAsync<T>(
|
||||||
Func<Action<T>, Task> subscribeCall,
|
Func<Action<T>, Task> subscribeCall,
|
||||||
Func<T, Task> asyncHandler,
|
Func<T, Task> asyncHandler,
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
@ -19,6 +17,9 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters);
|
IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create high performance websocket
|
||||||
|
/// </summary>
|
||||||
IHighPerfWebsocket CreateHighPerfWebsocket(ILogger logger, WebSocketParameters parameters, PipeWriter pipeWriter);
|
IHighPerfWebsocket CreateHighPerfWebsocket(ILogger logger, WebSocketParameters parameters, PipeWriter pipeWriter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -158,6 +158,9 @@ namespace CryptoExchange.Net
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for all of the ValueTasks to complete
|
||||||
|
/// </summary>
|
||||||
public static async ValueTask WhenAll(IReadOnlyList<ValueTask> tasks)
|
public static async ValueTask WhenAll(IReadOnlyList<ValueTask> tasks)
|
||||||
{
|
{
|
||||||
if (tasks.Count == 0)
|
if (tasks.Count == 0)
|
||||||
@ -184,6 +187,9 @@ namespace CryptoExchange.Net
|
|||||||
await Task.WhenAll(toAwait!).ConfigureAwait(false);
|
await Task.WhenAll(toAwait!).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for all of the ValueTasks to complete
|
||||||
|
/// </summary>
|
||||||
public static ValueTask WhenAll(IEnumerable<ValueTask> tasks)
|
public static ValueTask WhenAll(IEnumerable<ValueTask> tasks)
|
||||||
{
|
{
|
||||||
return WhenAll(tasks.ToList());
|
return WhenAll(tasks.ToList());
|
||||||
|
|||||||
@ -14,6 +14,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
{
|
{
|
||||||
private static readonly Task<bool> _completed = Task.FromResult(true);
|
private static readonly Task<bool> _completed = Task.FromResult(true);
|
||||||
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _waitsLock = new Lock();
|
||||||
|
#else
|
||||||
|
private readonly object _waitsLock = new object();
|
||||||
|
#endif
|
||||||
private bool _signaled;
|
private bool _signaled;
|
||||||
private readonly bool _reset;
|
private readonly bool _reset;
|
||||||
|
|
||||||
@ -38,7 +43,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Task<bool> waiter = _completed;
|
Task<bool> waiter = _completed;
|
||||||
lock (_waits)
|
lock (_waitsLock)
|
||||||
{
|
{
|
||||||
if (_signaled)
|
if (_signaled)
|
||||||
{
|
{
|
||||||
@ -57,7 +62,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
|
|
||||||
registration = ct.Register(() =>
|
registration = ct.Register(() =>
|
||||||
{
|
{
|
||||||
lock (_waits)
|
lock (_waitsLock)
|
||||||
{
|
{
|
||||||
tcs.TrySetResult(false);
|
tcs.TrySetResult(false);
|
||||||
|
|
||||||
@ -85,7 +90,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Set()
|
public void Set()
|
||||||
{
|
{
|
||||||
lock (_waits)
|
lock (_waitsLock)
|
||||||
{
|
{
|
||||||
if (!_reset)
|
if (!_reset)
|
||||||
{
|
{
|
||||||
@ -106,7 +111,9 @@ namespace CryptoExchange.Net.Objects
|
|||||||
toRelease.TrySetResult(true);
|
toRelease.TrySetResult(true);
|
||||||
}
|
}
|
||||||
else if (!_signaled)
|
else if (!_signaled)
|
||||||
|
{
|
||||||
_signaled = true;
|
_signaled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Sockets
|
namespace CryptoExchange.Net.Objects.Sockets
|
||||||
@ -14,14 +15,14 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
private readonly HighPerfSocketConnection _connection;
|
private readonly HighPerfSocketConnection _connection;
|
||||||
internal readonly HighPerfSubscription _subscription;
|
internal readonly HighPerfSubscription _subscription;
|
||||||
|
|
||||||
private object _eventLock = new object();
|
#if NET9_0_OR_GREATER
|
||||||
private bool _connectionEventsSubscribed = true;
|
private readonly Lock _eventLock = new Lock();
|
||||||
private List<Action> _connectionClosedEventHandlers = new List<Action>();
|
#else
|
||||||
|
private readonly object _eventLock = new object();
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
private bool _connectionEventsSubscribed = true;
|
||||||
/// Event when the status of the subscription changes
|
private readonly List<Action> _connectionClosedEventHandlers = new List<Action>();
|
||||||
/// </summary>
|
|
||||||
public event Action<SubscriptionStatus>? SubscriptionStatusChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event when the connection is closed and will not be reconnected
|
/// Event when the connection is closed and will not be reconnected
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Sockets
|
namespace CryptoExchange.Net.Objects.Sockets
|
||||||
@ -14,7 +15,12 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
private readonly SocketConnection _connection;
|
private readonly SocketConnection _connection;
|
||||||
internal readonly Subscription _subscription;
|
internal readonly Subscription _subscription;
|
||||||
|
|
||||||
private object _eventLock = new object();
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _eventLock = new Lock();
|
||||||
|
#else
|
||||||
|
private readonly object _eventLock = new object();
|
||||||
|
#endif
|
||||||
|
|
||||||
private bool _connectionEventsSubscribed = true;
|
private bool _connectionEventsSubscribed = true;
|
||||||
private List<Action> _connectionClosedEventHandlers = new List<Action>();
|
private List<Action> _connectionClosedEventHandlers = new List<Action>();
|
||||||
private List<Action> _connectionLostEventHandlers = new List<Action>();
|
private List<Action> _connectionLostEventHandlers = new List<Action>();
|
||||||
|
|||||||
@ -75,8 +75,6 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ReceiveBufferSize { get; set; } = null;
|
public int? ReceiveBufferSize { get; set; } = null;
|
||||||
|
|
||||||
public PipeWriter? PipeWriter { get; set; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -22,7 +22,11 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable
|
public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable
|
||||||
{
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _bookLock = new Lock();
|
||||||
|
#else
|
||||||
private readonly object _bookLock = new object();
|
private readonly object _bookLock = new object();
|
||||||
|
#endif
|
||||||
|
|
||||||
private OrderBookStatus _status;
|
private OrderBookStatus _status;
|
||||||
private UpdateSubscription? _subscription;
|
private UpdateSubscription? _subscription;
|
||||||
|
|||||||
@ -33,7 +33,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal static int _lastStreamId;
|
internal static int _lastStreamId;
|
||||||
private static readonly object _streamIdLock = new();
|
#if NET9_0_OR_GREATER
|
||||||
|
private static readonly Lock _streamIdLock = new Lock();
|
||||||
|
#else
|
||||||
|
private static readonly object _streamIdLock = new object();
|
||||||
|
#endif
|
||||||
private static readonly ArrayPool<byte> _receiveBufferPool = ArrayPool<byte>.Shared;
|
private static readonly ArrayPool<byte> _receiveBufferPool = ArrayPool<byte>.Shared;
|
||||||
|
|
||||||
private readonly AsyncResetEvent _sendEvent;
|
private readonly AsyncResetEvent _sendEvent;
|
||||||
@ -64,7 +68,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Received messages lock
|
/// Received messages lock
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly object _receivedMessagesLock;
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _receivedMessagesLock = new Lock();
|
||||||
|
#else
|
||||||
|
private readonly object _receivedMessagesLock = new object();
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log
|
/// Log
|
||||||
@ -152,7 +160,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_sendEvent = new AsyncResetEvent();
|
_sendEvent = new AsyncResetEvent();
|
||||||
_sendBuffer = new ConcurrentQueue<SendItem>();
|
_sendBuffer = new ConcurrentQueue<SendItem>();
|
||||||
_ctsSource = new CancellationTokenSource();
|
_ctsSource = new CancellationTokenSource();
|
||||||
_receivedMessagesLock = new object();
|
|
||||||
_receiveBufferSize = websocketParameters.ReceiveBufferSize ?? _defaultReceiveBufferSize;
|
_receiveBufferSize = websocketParameters.ReceiveBufferSize ?? _defaultReceiveBufferSize;
|
||||||
|
|
||||||
_closeSem = new SemaphoreSlim(1, 1);
|
_closeSem = new SemaphoreSlim(1, 1);
|
||||||
@ -460,11 +467,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
if (_socket.State == WebSocketState.CloseReceived)
|
if (_socket.State == WebSocketState.CloseReceived)
|
||||||
{
|
{
|
||||||
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (_socket.State == WebSocketState.Open)
|
else if (_socket.State == WebSocketState.Open)
|
||||||
{
|
{
|
||||||
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||||
var startWait = DateTime.UtcNow;
|
var startWait = DateTime.UtcNow;
|
||||||
while (_socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Aborted)
|
while (_socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Aborted)
|
||||||
{
|
{
|
||||||
@ -482,7 +489,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// So socket might go to aborted state, might still be open
|
// So socket might go to aborted state, might still be open
|
||||||
}
|
}
|
||||||
|
|
||||||
_ctsSource.Cancel();
|
if (!_disposed)
|
||||||
|
_ctsSource.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using System;
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets.HighPerf
|
namespace CryptoExchange.Net.Sockets.HighPerf
|
||||||
{
|
{
|
||||||
@ -17,7 +16,7 @@ namespace CryptoExchange.Net.Sockets.HighPerf
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan Interval { get; set; }
|
public TimeSpan Interval { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delegate for getting the query
|
/// Delegate for getting the request which should be send
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<HighPerfSocketConnection, object> GetRequestDelegate { get; set; } = null!;
|
public Func<HighPerfSocketConnection, object> GetRequestDelegate { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ using Microsoft.Extensions.Logging;
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using System.Diagnostics;
|
|
||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -32,12 +31,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of subscriptions on this connection
|
/// The amount of subscriptions on this connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int UserSubscriptionCount => _subscriptions.Length;
|
public int UserSubscriptionCount => Subscriptions.Length;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a copy of the current message subscriptions
|
/// Get a copy of the current message subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HighPerfSubscription[] Subscriptions => _subscriptions;
|
public abstract HighPerfSubscription[] Subscriptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If connection is made
|
/// If connection is made
|
||||||
@ -86,16 +85,27 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
/// <summary>
|
||||||
private SocketStatus _status;
|
/// Logger
|
||||||
|
/// </summary>
|
||||||
|
protected readonly ILogger _logger;
|
||||||
|
|
||||||
private readonly IMessageSerializer _serializer;
|
private readonly IMessageSerializer _serializer;
|
||||||
protected readonly JsonSerializerOptions _serializerOptions;
|
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
||||||
protected readonly Pipe _pipe;
|
private SocketStatus _status;
|
||||||
private Task? _processTask;
|
private Task? _processTask;
|
||||||
private CancellationTokenSource _cts = new CancellationTokenSource();
|
|
||||||
|
|
||||||
protected abstract HighPerfSubscription[] _subscriptions { get; }
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializer options
|
||||||
|
/// </summary>
|
||||||
|
protected readonly JsonSerializerOptions _serializerOptions;
|
||||||
|
/// <summary>
|
||||||
|
/// The pipe the websocket will write to
|
||||||
|
/// </summary>
|
||||||
|
protected readonly Pipe _pipe;
|
||||||
|
/// <summary>
|
||||||
|
/// Update type
|
||||||
|
/// </summary>
|
||||||
public abstract Type UpdateType { get; }
|
public abstract Type UpdateType { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -119,10 +129,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
public HighPerfSocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient, JsonSerializerOptions serializerOptions, string tag)
|
public HighPerfSocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient, JsonSerializerOptions serializerOptions, string tag)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_pipe = new Pipe(new PipeOptions
|
_pipe = new Pipe();
|
||||||
{
|
|
||||||
//ReaderScheduler
|
|
||||||
});
|
|
||||||
_serializerOptions = serializerOptions;
|
_serializerOptions = serializerOptions;
|
||||||
ApiClient = apiClient;
|
ApiClient = apiClient;
|
||||||
Tag = tag;
|
Tag = tag;
|
||||||
@ -139,6 +146,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_serializer = apiClient.CreateSerializer();
|
_serializer = apiClient.CreateSerializer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process message from the pipe
|
||||||
|
/// </summary>
|
||||||
protected abstract Task ProcessAsync(CancellationToken ct);
|
protected abstract Task ProcessAsync(CancellationToken ct);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -156,7 +166,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
protected virtual async Task HandleCloseAsync()
|
protected virtual async Task HandleCloseAsync()
|
||||||
{
|
{
|
||||||
Status = SocketStatus.Closed;
|
Status = SocketStatus.Closed;
|
||||||
_cts.Cancel();
|
_cts.CancelAfter(TimeSpan.FromSeconds(1)); // Cancel after 1 second to make sure we process pending messages from the pipe
|
||||||
|
|
||||||
if (ApiClient.highPerfSocketConnections.ContainsKey(SocketId))
|
if (ApiClient.highPerfSocketConnections.ContainsKey(SocketId))
|
||||||
ApiClient.highPerfSocketConnections.TryRemove(SocketId, out _);
|
ApiClient.highPerfSocketConnections.TryRemove(SocketId, out _);
|
||||||
@ -193,12 +203,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieve the underlying socket
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IHighPerfWebsocket GetSocket() => _socket;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Close the connection
|
/// Close the connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -211,7 +215,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (ApiClient.socketConnections.ContainsKey(SocketId))
|
if (ApiClient.socketConnections.ContainsKey(SocketId))
|
||||||
ApiClient.socketConnections.TryRemove(SocketId, out _);
|
ApiClient.socketConnections.TryRemove(SocketId, out _);
|
||||||
|
|
||||||
foreach (var subscription in _subscriptions)
|
foreach (var subscription in Subscriptions)
|
||||||
{
|
{
|
||||||
if (subscription.CancellationTokenRegistration.HasValue)
|
if (subscription.CancellationTokenRegistration.HasValue)
|
||||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||||
@ -235,7 +239,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (subscription.CancellationTokenRegistration.HasValue)
|
if (subscription.CancellationTokenRegistration.HasValue)
|
||||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||||
|
|
||||||
var anyOtherSubscriptions = _subscriptions.Any(x => x != subscription);
|
var anyOtherSubscriptions = Subscriptions.Any(x => x != subscription);
|
||||||
|
|
||||||
if (anyOtherSubscriptions)
|
if (anyOtherSubscriptions)
|
||||||
await UnsubscribeAsync(subscription).ConfigureAwait(false);
|
await UnsubscribeAsync(subscription).ConfigureAwait(false);
|
||||||
@ -413,11 +417,19 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public class HighPerfSocketConnection<T> : HighPerfSocketConnection
|
public class HighPerfSocketConnection<T> : HighPerfSocketConnection
|
||||||
{
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _listenersLock = new Lock();
|
||||||
|
#else
|
||||||
private readonly object _listenersLock = new object();
|
private readonly object _listenersLock = new object();
|
||||||
private List<HighPerfSubscription<T>> _typedSubscriptions;
|
#endif
|
||||||
protected override HighPerfSubscription[] _subscriptions
|
|
||||||
|
private readonly List<HighPerfSubscription<T>> _typedSubscriptions;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override HighPerfSubscription[] Subscriptions
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@ -426,42 +438,65 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override Type UpdateType => typeof(T);
|
public override Type UpdateType => typeof(T);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
public HighPerfSocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient, JsonSerializerOptions serializerOptions, string tag) : base(logger, socketFactory, parameters, apiClient, serializerOptions, tag)
|
public HighPerfSocketConnection(ILogger logger, IWebsocketFactory socketFactory, WebSocketParameters parameters, SocketApiClient apiClient, JsonSerializerOptions serializerOptions, string tag) : base(logger, socketFactory, parameters, apiClient, serializerOptions, tag)
|
||||||
{
|
{
|
||||||
_typedSubscriptions = new List<HighPerfSubscription<T>>();
|
_typedSubscriptions = new List<HighPerfSubscription<T>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a subscription to this connection
|
/// Add a new subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="subscription"></param>
|
|
||||||
public bool AddSubscription(HighPerfSubscription<T> subscription)
|
public bool AddSubscription(HighPerfSubscription<T> subscription)
|
||||||
{
|
{
|
||||||
if (Status != SocketStatus.None && Status != SocketStatus.Connected)
|
if (Status != SocketStatus.None && Status != SocketStatus.Connected)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
//lock (_listenersLock)
|
|
||||||
_typedSubscriptions.Add(subscription);
|
_typedSubscriptions.Add(subscription);
|
||||||
|
|
||||||
//_logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount);
|
_logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a subscription
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscription"></param>
|
||||||
public void RemoveSubscription(HighPerfSubscription<T> subscription)
|
public void RemoveSubscription(HighPerfSubscription<T> subscription)
|
||||||
{
|
{
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
_typedSubscriptions.Remove(subscription);
|
_typedSubscriptions.Remove(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task ProcessAsync(CancellationToken ct)
|
protected override async Task ProcessAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
await foreach (var update in JsonSerializer.DeserializeAsyncEnumerable<T>(_pipe.Reader, true, _serializerOptions, ct).ConfigureAwait(false))
|
await foreach (var update in JsonSerializer.DeserializeAsyncEnumerable<T>(_pipe.Reader, true, _serializerOptions, ct).ConfigureAwait(false))
|
||||||
|
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
{
|
{
|
||||||
var tasks = _typedSubscriptions.Select(sub => sub.HandleAsync(update!));
|
var tasks = _typedSubscriptions.Select(sub =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return sub.HandleAsync(update!);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
sub.InvokeExceptionHandler(ex);
|
||||||
|
_logger.UserMessageProcessingFailed(SocketId, ex.Message, ex);
|
||||||
|
return new ValueTask();
|
||||||
|
}
|
||||||
|
});
|
||||||
await LibraryHelpers.WhenAll(tasks).ConfigureAwait(false);
|
await LibraryHelpers.WhenAll(tasks).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -20,11 +19,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int TotalInvocations { get; set; }
|
public int TotalInvocations { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Logger
|
|
||||||
/// </summary>
|
|
||||||
protected readonly ILogger _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cancellation token registration
|
/// Cancellation token registration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -48,9 +42,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HighPerfSubscription(ILogger logger)
|
public HighPerfSubscription()
|
||||||
{
|
{
|
||||||
_logger = logger;
|
|
||||||
Id = ExchangeHelpers.NextId();
|
Id = ExchangeHelpers.NextId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +87,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
Exception?.Invoke(e);
|
Exception?.Invoke(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -105,13 +97,17 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected HighPerfSubscription(ILogger logger, Func<TUpdateType, ValueTask> handler) : base(logger)
|
protected HighPerfSubscription(Func<TUpdateType, ValueTask> handler) : base()
|
||||||
{
|
{
|
||||||
_handler = handler;
|
_handler = handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle an update
|
||||||
|
/// </summary>
|
||||||
public ValueTask HandleAsync(TUpdateType update)
|
public ValueTask HandleAsync(TUpdateType update)
|
||||||
{
|
{
|
||||||
|
TotalInvocations++;
|
||||||
return _handler.Invoke(update);
|
return _handler.Invoke(update);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,20 +6,16 @@ using CryptoExchange.Net.Objects.Sockets;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets
|
namespace CryptoExchange.Net.Sockets
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A wrapper around the ClientWebSocket
|
/// A high performance websocket client implementation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class HighPerfWebSocketClient : IHighPerfWebsocket
|
public class HighPerfWebSocketClient : IHighPerfWebsocket
|
||||||
{
|
{
|
||||||
@ -43,8 +39,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
private const int _defaultReceiveBufferSize = 4096;
|
private const int _defaultReceiveBufferSize = 4096;
|
||||||
private const int _sendBufferSize = 4096;
|
private const int _sendBufferSize = 4096;
|
||||||
|
|
||||||
private byte[] _commaBytes = new byte[] { 44 };
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log
|
/// Log
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -90,12 +84,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_closeSem = new SemaphoreSlim(1, 1);
|
_closeSem = new SemaphoreSlim(1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void UpdateProxy(ApiProxy? proxy)
|
|
||||||
{
|
|
||||||
Parameters.Proxy = proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual async Task<CallResult> ConnectAsync(CancellationToken ct)
|
public virtual async Task<CallResult> ConnectAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
@ -169,7 +157,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (e is WebSocketException we)
|
if (e is WebSocketException we)
|
||||||
{
|
{
|
||||||
#if (NET6_0_OR_GREATER)
|
#if (NET6_0_OR_GREATER)
|
||||||
if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests)
|
if (_socket!.HttpStatusCode == HttpStatusCode.TooManyRequests)
|
||||||
{
|
{
|
||||||
return new CallResult(new ServerRateLimitError(we.Message, we));
|
return new CallResult(new ServerRateLimitError(we.Message, we));
|
||||||
}
|
}
|
||||||
@ -296,19 +284,20 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
if (_socket!.State == WebSocketState.CloseReceived)
|
if (_socket!.State == WebSocketState.CloseReceived)
|
||||||
{
|
{
|
||||||
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (_socket.State == WebSocketState.Open)
|
else if (_socket.State == WebSocketState.Open)
|
||||||
{
|
{
|
||||||
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||||
|
|
||||||
var startWait = DateTime.UtcNow;
|
var startWait = DateTime.UtcNow;
|
||||||
while (_socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Aborted)
|
while (_processing && _socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Aborted)
|
||||||
{
|
{
|
||||||
// Wait until we receive close confirmation
|
// Wait until we receive close confirmation
|
||||||
await Task.Delay(10).ConfigureAwait(false);
|
await Task.Delay(10).ConfigureAwait(false);
|
||||||
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1))
|
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1))
|
||||||
break; // Wait for max 1 second, then just abort the connection
|
break; // Wait for max 1 second, then just abort the connection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@ -318,7 +307,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// So socket might go to aborted state, might still be open
|
// So socket might go to aborted state, might still be open
|
||||||
}
|
}
|
||||||
|
|
||||||
_ctsSource.Cancel();
|
if (!_disposed)
|
||||||
|
_ctsSource.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -342,9 +332,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
#if NETSTANDARD2_1 || NET8_0_OR_GREATER
|
#if NETSTANDARD2_1 || NET8_0_OR_GREATER
|
||||||
private async Task ReceiveLoopAsync()
|
private async Task ReceiveLoopAsync()
|
||||||
{
|
{
|
||||||
|
Exception? exitException = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Exception? exitException = null;
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
@ -356,7 +346,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
receiveResult = await _socket!.ReceiveAsync(_pipeWriter.GetMemory(_receiveBufferSize), _ctsSource.Token).ConfigureAwait(false);
|
receiveResult = await _socket!.ReceiveAsync(_pipeWriter.GetMemory(_receiveBufferSize), _ctsSource.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
// Advance the writer to communicate which part the memory was written
|
// Advance the writer to communicate which part of the memory was written
|
||||||
_pipeWriter.Advance(receiveResult.Count);
|
_pipeWriter.Advance(receiveResult.Count);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
@ -371,7 +361,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (_closeTask?.IsCompleted != false)
|
if (_closeTask?.IsCompleted != false)
|
||||||
_closeTask = CloseInternalAsync();
|
_closeTask = CloseInternalAsync();
|
||||||
|
|
||||||
// canceled
|
|
||||||
exitException = ex;
|
exitException = ex;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -388,42 +377,39 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
|
||||||
{
|
|
||||||
// Connection closed
|
|
||||||
if (_socket.State == WebSocketState.CloseReceived)
|
|
||||||
{
|
|
||||||
// Close received means it server initiated, we should send a confirmation and close the socket
|
|
||||||
//_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
|
|
||||||
if (_closeTask?.IsCompleted != false)
|
|
||||||
_closeTask = CloseInternalAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Means the socket is now closed and we were the one initiating it
|
|
||||||
//_logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiveResult.EndOfMessage)
|
if (receiveResult.EndOfMessage)
|
||||||
{
|
{
|
||||||
// Write a comma to split the json data for the reader
|
// Flush the full message
|
||||||
// This will also flush the written bytes
|
|
||||||
var flushResult = await _pipeWriter.FlushAsync().ConfigureAwait(false);
|
var flushResult = await _pipeWriter.FlushAsync().ConfigureAwait(false);
|
||||||
if (flushResult.IsCompleted)
|
if (flushResult.IsCompleted)
|
||||||
{
|
{
|
||||||
// Flush indicated that the reader is no longer listening
|
// Flush indicated that the reader is no longer listening, so we should stop writing
|
||||||
if (_closeTask?.IsCompleted != false)
|
if (_closeTask?.IsCompleted != false)
|
||||||
_closeTask = CloseInternalAsync();
|
_closeTask = CloseInternalAsync();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await _pipeWriter.CompleteAsync(exitException).ConfigureAwait(false);
|
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
// Connection closed
|
||||||
|
if (_socket.State == WebSocketState.CloseReceived)
|
||||||
|
{
|
||||||
|
// Close received means it's server initiated, we should send a confirmation and close the socket
|
||||||
|
_logger.SocketReceivedCloseMessage(Id, _socket.CloseStatus?.ToString() ?? string.Empty, _socket.CloseStatusDescription ?? string.Empty);
|
||||||
|
if (_closeTask?.IsCompleted != false)
|
||||||
|
_closeTask = CloseInternalAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Means the socket is now closed and we were the one initiating it
|
||||||
|
_logger.SocketReceivedCloseConfirmation(Id, _socket.CloseStatus?.ToString() ?? string.Empty, _socket.CloseStatusDescription ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -432,32 +418,27 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Make sure we at least let the owner know there was an error
|
// Make sure we at least let the owner know there was an error
|
||||||
_logger.SocketReceiveLoopStoppedWithException(Id, e);
|
_logger.SocketReceiveLoopStoppedWithException(Id, e);
|
||||||
|
|
||||||
await _pipeWriter.CompleteAsync(e).ConfigureAwait(false);
|
exitException = e;
|
||||||
|
|
||||||
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
if (_closeTask?.IsCompleted != false)
|
if (_closeTask?.IsCompleted != false)
|
||||||
_closeTask = CloseInternalAsync();
|
_closeTask = CloseInternalAsync();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
await _pipeWriter.CompleteAsync(exitException).ConfigureAwait(false);
|
||||||
_logger.SocketReceiveLoopFinished(Id);
|
_logger.SocketReceiveLoopFinished(Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loop for receiving and reassembling data
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
private async Task ReceiveLoopAsync()
|
private async Task ReceiveLoopAsync()
|
||||||
{
|
{
|
||||||
byte[] rentedBuffer = _receiveBufferPool.Rent(_receiveBufferSize);
|
byte[] rentedBuffer = _receiveBufferPool.Rent(_receiveBufferSize);
|
||||||
var buffer = new ArraySegment<byte>(rentedBuffer);
|
var buffer = new ArraySegment<byte>(rentedBuffer);
|
||||||
|
Exception? exitException = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Exception? exitException = null;
|
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
@ -480,7 +461,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (_closeTask?.IsCompleted != false)
|
if (_closeTask?.IsCompleted != false)
|
||||||
_closeTask = CloseInternalAsync();
|
_closeTask = CloseInternalAsync();
|
||||||
|
|
||||||
// canceled
|
|
||||||
exitException = ex;
|
exitException = ex;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -497,6 +477,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (receiveResult.Count > 0)
|
||||||
|
await _pipeWriter.WriteAsync(buffer.AsMemory(0, receiveResult.Count)).ConfigureAwait(false);
|
||||||
|
|
||||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
// Connection closed
|
// Connection closed
|
||||||
@ -515,12 +498,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await _pipeWriter.WriteAsync(buffer.AsMemory(0, receiveResult.Count)).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await _pipeWriter.CompleteAsync(exitException).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -529,13 +508,14 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Make sure we at least let the owner know there was an error
|
// Make sure we at least let the owner know there was an error
|
||||||
_logger.SocketReceiveLoopStoppedWithException(Id, e);
|
_logger.SocketReceiveLoopStoppedWithException(Id, e);
|
||||||
|
|
||||||
await _pipeWriter.CompleteAsync(e).ConfigureAwait(false);
|
exitException = e;
|
||||||
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
if (_closeTask?.IsCompleted != false)
|
if (_closeTask?.IsCompleted != false)
|
||||||
_closeTask = CloseInternalAsync();
|
_closeTask = CloseInternalAsync();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
await _pipeWriter.CompleteAsync(exitException).ConfigureAwait(false);
|
||||||
|
|
||||||
_receiveBufferPool.Return(rentedBuffer, true);
|
_receiveBufferPool.Return(rentedBuffer, true);
|
||||||
_logger.SocketReceiveLoopFinished(Id);
|
_logger.SocketReceiveLoopFinished(Id);
|
||||||
|
|||||||
@ -1,30 +1,57 @@
|
|||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets
|
namespace CryptoExchange.Net.Sockets
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Socket connection
|
||||||
|
/// </summary>
|
||||||
public interface ISocketConnection
|
public interface ISocketConnection
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The API client the connection belongs to
|
||||||
|
/// </summary>
|
||||||
SocketApiClient ApiClient { get; set; }
|
SocketApiClient ApiClient { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the connection has been authenticated
|
||||||
|
/// </summary>
|
||||||
bool Authenticated { get; set; }
|
bool Authenticated { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the connection is established
|
||||||
|
/// </summary>
|
||||||
bool Connected { get; }
|
bool Connected { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Connection URI
|
||||||
|
/// </summary>
|
||||||
Uri ConnectionUri { get; }
|
Uri ConnectionUri { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Id
|
||||||
|
/// </summary>
|
||||||
int SocketId { get; }
|
int SocketId { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Tag
|
||||||
|
/// </summary>
|
||||||
string Tag { get; set; }
|
string Tag { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Closed event
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
event Action? ConnectionClosed;
|
event Action? ConnectionClosed;
|
||||||
|
/// <summary>
|
||||||
|
/// Connect the websocket
|
||||||
|
/// </summary>
|
||||||
Task<CallResult> ConnectAsync(CancellationToken ct);
|
Task<CallResult> ConnectAsync(CancellationToken ct);
|
||||||
|
/// <summary>
|
||||||
|
/// Close the connection
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
Task CloseAsync();
|
Task CloseAsync();
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose
|
||||||
|
/// </summary>
|
||||||
void Dispose();
|
void Dispose();
|
||||||
|
|
||||||
//ValueTask<CallResult> SendStringAsync(int requestId, string data, int weight);
|
|
||||||
//ValueTask<CallResult> SendAsync<T>(int requestId, T obj, int weight);
|
|
||||||
//ValueTask<CallResult> SendBytesAsync(int requestId, byte[] data, int weight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -253,7 +253,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool _pausedActivity;
|
private bool _pausedActivity;
|
||||||
private readonly object _listenersLock;
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _listenersLock = new Lock();
|
||||||
|
#else
|
||||||
|
private readonly object _listenersLock = new object();
|
||||||
|
#endif
|
||||||
private readonly List<IMessageProcessor> _listeners;
|
private readonly List<IMessageProcessor> _listeners;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private SocketStatus _status;
|
private SocketStatus _status;
|
||||||
@ -306,7 +310,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_socket.OnError += HandleErrorAsync;
|
_socket.OnError += HandleErrorAsync;
|
||||||
_socket.GetReconnectionUrl = GetReconnectionUrlAsync;
|
_socket.GetReconnectionUrl = GetReconnectionUrlAsync;
|
||||||
|
|
||||||
_listenersLock = new object();
|
|
||||||
_listeners = new List<IMessageProcessor>();
|
_listeners = new List<IMessageProcessor>();
|
||||||
|
|
||||||
_serializer = apiClient.CreateSerializer();
|
_serializer = apiClient.CreateSerializer();
|
||||||
|
|||||||
@ -39,7 +39,11 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
public static int lastId = 0;
|
public static int lastId = 0;
|
||||||
public static object lastIdLock = new object();
|
#if NET9_0_OR_GREATER
|
||||||
|
public static readonly Lock lastIdLock = new Lock();
|
||||||
|
#else
|
||||||
|
public static readonly object lastIdLock = new object();
|
||||||
|
#endif
|
||||||
|
|
||||||
public TestSocket(string address)
|
public TestSocket(string address)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,6 +8,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Trackers.Klines
|
namespace CryptoExchange.Net.Trackers.Klines
|
||||||
@ -31,7 +32,11 @@ namespace CryptoExchange.Net.Trackers.Klines
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock for accessing _data
|
/// Lock for accessing _data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly object _lock = new object();
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _lock = new Lock();
|
||||||
|
#else
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
#endif
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The last time the window was applied
|
/// The last time the window was applied
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Trackers.Trades
|
namespace CryptoExchange.Net.Trackers.Trades
|
||||||
@ -42,7 +43,11 @@ namespace CryptoExchange.Net.Trackers.Trades
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock for accessing _data
|
/// Lock for accessing _data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly object _lock = new object();
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _lock = new Lock();
|
||||||
|
#else
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
#endif
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the snapshot has been set
|
/// Whether the snapshot has been set
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user