Skip to content

Commit fa6fa2e

Browse files
committed
More locking and less async in AniDBUDPConnectionHandler
1 parent ad4054f commit fa6fa2e

File tree

5 files changed

+109
-105
lines changed

5 files changed

+109
-105
lines changed

Shoko.Server/Providers/AniDB/AniDBRateLimiter.cs

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using Shoko.Plugin.Abstractions;
66
using Shoko.Plugin.Abstractions.Events;
77
using Shoko.Server.Settings;
8-
98
using ISettingsProvider = Shoko.Server.Settings.ISettingsProvider;
109

1110
#nullable enable
@@ -28,7 +27,7 @@ public abstract class AniDBRateLimiter
2827

2928
private readonly Func<IServerSettings, AnidbRateLimitSettings> _settingsSelector;
3029

31-
private int? _shortDelay = null;
30+
private int? _shortDelay;
3231

3332
// From AniDB's wiki about UDP rate limiting:
3433
// Short Term:
@@ -44,7 +43,7 @@ private int ShortDelay
4443
}
4544
}
4645

47-
private int? _longDelay = null;
46+
private int? _longDelay;
4847

4948
// From AniDB's wiki about UDP rate limiting:
5049
// Long Term:
@@ -60,7 +59,7 @@ private int LongDelay
6059
}
6160
}
6261

63-
private long? _shortPeriod = null;
62+
private long? _shortPeriod;
6463

6564
// Switch to longer delay after a short period
6665
private long ShortPeriod
@@ -73,7 +72,7 @@ private long ShortPeriod
7372
}
7473
}
7574

76-
private long? _resetPeriod = null;
75+
private long? _resetPeriod;
7776

7877
// Switch to shorter delay after inactivity
7978
private long ResetPeriod
@@ -141,31 +140,31 @@ private void ResetRate()
141140
public T EnsureRate<T>(Func<T> action, bool forceShortDelay = false)
142141
{
143142
lock (_lock)
144-
try
145-
{
146-
var delay = _requestWatch.ElapsedMilliseconds;
147-
if (delay > ResetPeriod) ResetRate();
148-
var currentDelay = !forceShortDelay && _activeTimeWatch.ElapsedMilliseconds > ShortPeriod ? LongDelay : ShortDelay;
149-
150-
if (delay > currentDelay)
143+
try
151144
{
152-
_logger.LogTrace("Time since last request is {Delay} ms, not throttling", delay);
153-
_logger.LogTrace("Sending AniDB command");
154-
return action();
155-
}
145+
var delay = _requestWatch.ElapsedMilliseconds;
146+
if (delay > ResetPeriod) ResetRate();
147+
var currentDelay = !forceShortDelay && _activeTimeWatch.ElapsedMilliseconds > ShortPeriod ? LongDelay : ShortDelay;
156148

157-
// add 50ms for good measure
158-
var waitTime = currentDelay - (int)delay + 50;
149+
if (delay > currentDelay)
150+
{
151+
_logger.LogTrace("Time since last request is {Delay} ms, not throttling", delay);
152+
_logger.LogTrace("Sending AniDB command");
153+
return action();
154+
}
159155

160-
_logger.LogTrace("Time since last request is {Delay} ms, throttling for {Time}ms", delay, waitTime);
161-
Thread.Sleep(waitTime);
156+
// add 50ms for good measure
157+
var waitTime = currentDelay - (int)delay + 50;
162158

163-
_logger.LogTrace("Sending AniDB command");
164-
return action();
165-
}
166-
finally
167-
{
168-
_requestWatch.Restart();
169-
}
159+
_logger.LogTrace("Time since last request is {Delay} ms, throttling for {Time}ms", delay, waitTime);
160+
Thread.Sleep(waitTime);
161+
162+
_logger.LogTrace("Sending AniDB command");
163+
return action();
164+
}
165+
finally
166+
{
167+
_requestWatch.Restart();
168+
}
170169
}
171170
}
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
using System;
2-
using System.Threading;
3-
using System.Threading.Tasks;
42

53
namespace Shoko.Server.Providers.AniDB.Interfaces;
64

75
public interface IAniDBSocketHandler : IDisposable, IAsyncDisposable
86
{
97
bool IsConnected { get; }
10-
Task<byte[]> Send(byte[] payload, CancellationToken token = new());
11-
Task<bool> TryConnection();
8+
byte[] Send(byte[] payload);
9+
bool TryConnection();
1210
}

Shoko.Server/Providers/AniDB/UDP/AniDBSocketHandler.cs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,23 @@ public AniDBSocketHandler(ILoggerFactory loggerFactory, string host, ushort serv
3232
_clientPort = clientPort;
3333
}
3434

35-
public async Task<byte[]> Send(byte[] payload, CancellationToken token = new())
35+
public byte[] Send(byte[] payload)
3636
{
3737
if (!IsConnected) return [0];
3838
// this doesn't need to be bigger than 1400, but meh, better safe than sorry
39-
return await SendUnsafe(payload, token);
39+
return SendUnsafe(payload);
4040
}
4141

42-
private async Task<byte[]> SendUnsafe(byte[] payload, CancellationToken token)
42+
private byte[] SendUnsafe(byte[] payload)
4343
{
4444
EmptyBuffer();
4545

46-
if (token.IsCancellationRequested) throw new TaskCanceledException();
47-
using CancellationTokenSource sendCts = new(SendTimeoutMs);
48-
await _aniDBSocket.SendToAsync(payload, SocketFlags.None, _remoteIpEndPoint, sendCts.Token);
49-
50-
if (sendCts.IsCancellationRequested) throw new OperationCanceledException();
51-
if (token.IsCancellationRequested) throw new TaskCanceledException();
46+
_aniDBSocket.SendTo(payload, _remoteIpEndPoint);
5247

5348
using CancellationTokenSource receiveCts = new(ReceiveTimeoutMs);
5449
var result = new byte[1600];
55-
var receivedResult = await _aniDBSocket.ReceiveFromAsync(result, SocketFlags.None, _remoteIpEndPoint, receiveCts.Token);
56-
if (sendCts.IsCancellationRequested) throw new OperationCanceledException();
57-
if (token.IsCancellationRequested) throw new TaskCanceledException();
58-
59-
var received = receivedResult.ReceivedBytes;
50+
EndPoint endpoint = _remoteIpEndPoint;
51+
var received = _aniDBSocket.ReceiveFrom(result, ref endpoint);
6052

6153
if (received > 2 && result[0] == 0 && result[1] == 0)
6254
{
@@ -97,7 +89,7 @@ private void EmptyBuffer()
9789
}
9890
}
9991

100-
public async Task<bool> TryConnection()
92+
public bool TryConnection()
10193
{
10294
if (IsConnected) return true;
10395
// Don't send Expect 100 requests. These requests aren't always supported by remote internet devices, in which case can cause failure.
@@ -114,6 +106,7 @@ public async Task<bool> TryConnection()
114106
* The local port may be hardcoded, however, an option to manually specify another port should be offered.
115107
*/
116108
_aniDBSocket.Bind(_localIpEndPoint);
109+
_aniDBSocket.SendTimeout = SendTimeoutMs;
117110
_aniDBSocket.ReceiveTimeout = ReceiveTimeoutMs;
118111

119112
_logger.LogInformation("Bound to local address: {Local} - Port: {ClientPort} ({Family})", _localIpEndPoint,
@@ -128,7 +121,7 @@ public async Task<bool> TryConnection()
128121

129122
try
130123
{
131-
var remoteHostEntry = await Dns.GetHostEntryAsync(_serverHost).WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
124+
var remoteHostEntry = Dns.GetHostEntry(_serverHost);
132125
_remoteIpEndPoint = new IPEndPoint(remoteHostEntry.AddressList[0], _serverPort);
133126

134127
_logger.LogInformation("Bound to remote address: {Address} : {Port}", _remoteIpEndPoint.Address,

Shoko.Server/Providers/AniDB/UDP/AniDBUDPConnectionHandler.cs

Lines changed: 70 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
namespace Shoko.Server.Providers.AniDB.UDP;
2121

2222
#nullable enable
23-
public class AniDBUDPConnectionHandler : ConnectionHandler, IUDPConnectionHandler
23+
public partial class AniDBUDPConnectionHandler : ConnectionHandler, IUDPConnectionHandler
2424
{
2525
/****
2626
* From Anidb wiki:
@@ -35,7 +35,11 @@ public class AniDBUDPConnectionHandler : ConnectionHandler, IUDPConnectionHandle
3535
private readonly IConnectivityService _connectivityService;
3636
private AniDBSocketHandler? _socketHandler;
3737
private readonly object _socketHandlerLock = new();
38-
private static readonly Regex s_logMask = new("(?<=(\\bpass=|&pass=\\bs=|&s=))[^&]+", RegexOptions.Compiled | RegexOptions.IgnoreCase);
38+
// IDK Rider said to use a GeneratedRegex attribute
39+
private static readonly Regex s_logMask = GetLogRegex();
40+
41+
[GeneratedRegex("(?<=(\\bpass=|&pass=\\bs=|&s=))[^&]+", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")]
42+
private static partial Regex GetLogRegex();
3943

4044
public event EventHandler? LoginFailed;
4145

@@ -150,7 +154,7 @@ private void InitInternal()
150154
}
151155

152156
_socketHandler = new AniDBSocketHandler(_loggerFactory, settings.AniDb.UDPServerAddress, settings.AniDb.UDPServerPort, settings.AniDb.ClientPort);
153-
IsNetworkAvailable = _socketHandler.TryConnection().Result;
157+
IsNetworkAvailable = _socketHandler.TryConnection();
154158
}
155159

156160
_isLoggedOn = false;
@@ -214,32 +218,32 @@ private void LogoutTimerElapsed(object? sender, ElapsedEventArgs e)
214218
/// <returns></returns>
215219
public string Send(string command, bool needsUnicode = true)
216220
{
217-
lock(_socketHandlerLock)
218-
{
219-
// Steps:
220-
// 1. Check Ban state and throw if Banned
221-
// 2. Check Login State and Login if needed
222-
// 3. Actually Call AniDB
221+
// Steps:
222+
// 1. Check Ban state and throw if Banned
223+
// 2. Check Login State and Login if needed
224+
// 3. Actually Call AniDB
223225

224-
// Check Ban State
225-
// Ideally, this will never happen, as we stop the queue and attempt a graceful rollback of the command
226-
if (IsBanned)
226+
// Check Ban State
227+
// Ideally, this will never happen, as we stop the queue and attempt a graceful rollback of the command
228+
if (IsBanned)
229+
{
230+
throw new AniDBBannedException
227231
{
228-
throw new AniDBBannedException
229-
{
230-
BanType = UpdateType.UDPBan, BanExpires = BanTime?.AddHours(BanTimerResetLength)
231-
};
232-
}
233-
// TODO Low Priority: We need to handle Login Attempt Decay, so that we can try again if it's not just a bad user/pass
234-
// It wasn't handled before, and it's not caused serious problems
232+
BanType = UpdateType.UDPBan, BanExpires = BanTime?.AddHours(BanTimerResetLength)
233+
};
234+
}
235+
// TODO Low Priority: We need to handle Login Attempt Decay, so that we can try again if it's not just a bad user/pass
236+
// It wasn't handled before, and it's not caused serious problems
235237

236-
// login doesn't use this method, so this check won't interfere with it
237-
// if we got here, and it's invalid session, then it already failed to re-log
238-
if (IsInvalidSession)
239-
{
240-
throw new NotLoggedInException();
241-
}
238+
// login doesn't use this method, so this check won't interfere with it
239+
// if we got here, and it's invalid session, then it already failed to re-log
240+
if (IsInvalidSession)
241+
{
242+
throw new NotLoggedInException();
243+
}
242244

245+
lock (_socketHandlerLock)
246+
{
243247
// Check Login State
244248
if (!Login())
245249
{
@@ -296,7 +300,7 @@ private string SendInternal(string command, bool needsUnicode = true, bool isPin
296300

297301
var start = DateTime.Now;
298302
Logger.LogTrace("AniDB UDP Call: (Using {Unicode}) {Command}", needsUnicode ? "Unicode" : "ASCII", MaskLog(command));
299-
var byReceivedAdd = _socketHandler.Send(sendByteAdd).Result;
303+
var byReceivedAdd = _socketHandler.Send(sendByteAdd);
300304

301305
if (byReceivedAdd.All(a => a == 0))
302306
{
@@ -344,31 +348,35 @@ private void StopPinging()
344348

345349
public void ForceReconnection()
346350
{
347-
try
348-
{
349-
ForceLogout();
350-
}
351-
catch (Exception ex)
351+
lock (_socketHandlerLock)
352352
{
353-
Logger.LogError(ex, "Failed to logout");
354-
}
355353

356-
try
357-
{
358-
CloseConnections();
359-
}
360-
catch (Exception ex)
361-
{
362-
Logger.LogError(ex, "Failed to close socket");
363-
}
354+
try
355+
{
356+
ForceLogout();
357+
}
358+
catch (Exception ex)
359+
{
360+
Logger.LogError(ex, "Failed to logout");
361+
}
364362

365-
try
366-
{
367-
InitInternal();
368-
}
369-
catch (Exception ex)
370-
{
371-
Logger.LogError(ex, "Failed to reinitialize socket");
363+
try
364+
{
365+
CloseConnections();
366+
}
367+
catch (Exception ex)
368+
{
369+
Logger.LogError(ex, "Failed to close socket");
370+
}
371+
372+
try
373+
{
374+
InitInternal();
375+
}
376+
catch (Exception ex)
377+
{
378+
Logger.LogError(ex, "Failed to reinitialize socket");
379+
}
372380
}
373381
}
374382

@@ -387,7 +395,7 @@ public void ForceLogout()
387395
Logger.LogTrace("Logging Out");
388396
try
389397
{
390-
_requestFactory.Create<RequestLogout>().Send();
398+
lock(_socketHandlerLock) _requestFactory.Create<RequestLogout>().Send();
391399
}
392400
catch
393401
{
@@ -437,8 +445,11 @@ public bool Login()
437445
{
438446
if (IsBanned) return false;
439447
Logger.LogTrace("Failed to login to AniDB. Issuing a Logout command and retrying");
440-
ForceLogout();
441-
return Login(settings.AniDb.Username, settings.AniDb.Password);
448+
lock (_socketHandlerLock)
449+
{
450+
ForceLogout();
451+
return Login(settings.AniDb.Username, settings.AniDb.Password);
452+
}
442453
}
443454
catch (Exception e)
444455
{
@@ -551,13 +562,16 @@ public bool TestLogin(string username, string password)
551562
return false;
552563
}
553564

554-
var result = Login(username, password);
555-
if (result)
565+
lock (_socketHandlerLock)
556566
{
557-
ForceLogout();
558-
}
567+
var result = Login(username, password);
568+
if (result)
569+
{
570+
ForceLogout();
571+
}
559572

560-
return result;
573+
return result;
574+
}
561575
}
562576

563577
public bool SetCredentials(string username, string password)

Shoko.Server/Providers/AniDB/UDP/Connection/RequestLogin.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ protected override UDPResponse<ResponseLogin> ParseResponse(UDPResponse<string>
2727
}
2828

2929
// after response code, before "LOGIN"
30-
var sessionID = receivedData.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Skip(1)
30+
var sessionID = receivedData.Split(' ', StringSplitOptions.RemoveEmptyEntries).Skip(1)
3131
.FirstOrDefault();
3232
if (string.IsNullOrWhiteSpace(sessionID))
3333
{
3434
throw new UnexpectedUDPResponseException(code, receivedData, Command);
3535
}
3636

37-
var imageServer = receivedData.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
37+
var imageServer = receivedData.Split('\n', StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
3838
return new UDPResponse<ResponseLogin>
3939
{
4040
Response = new ResponseLogin { SessionID = sessionID, ImageServer = imageServer }, Code = code

0 commit comments

Comments
 (0)