harden up the script timer/game interface dvar operations for multithreading

This commit is contained in:
RaidMax 2022-10-23 14:03:33 -05:00
parent f6b3eb04f2
commit f4e7d5daf9
5 changed files with 95 additions and 48 deletions

View File

@ -867,6 +867,12 @@ namespace IW4MAdmin
/// <returns></returns> /// <returns></returns>
async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token) async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token)
{ {
if (DateTime.Now - (MatchEndTime ?? MatchStartTime) < TimeSpan.FromSeconds(15))
{
ServerLogger.LogDebug("Skipping status poll attempt because the match ended recently");
return null;
}
var currentClients = GetClientsAsList(); var currentClients = GetClientsAsList();
var statusResponse = await this.GetStatusAsync(token); var statusResponse = await this.GetStatusAsync(token);
@ -874,7 +880,7 @@ namespace IW4MAdmin
{ {
return null; return null;
} }
var polledClients = statusResponse.Clients.AsEnumerable(); var polledClients = statusResponse.Clients.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots) if (Manager.GetApplicationSettings().Configuration().IgnoreBots)

View File

@ -10,6 +10,7 @@ using SharedLibraryCore.Interfaces;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -353,6 +354,13 @@ namespace IW4MAdmin.Application.Misc
var result = action.DynamicInvoke(JsValue.Undefined, args); var result = action.DynamicInvoke(JsValue.Undefined, args);
return (T)(result as JsValue)?.ToObject(); return (T)(result as JsValue)?.ToObject();
} }
catch (TargetInvocationException ex) when (ex.InnerException is JavaScriptException jsEx)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} with event type {@LocationInfo} {StackTrace}",
nameof(ExecuteAction), Path.GetFileName(_fileName), jsEx.Location, jsEx.JavaScriptStackTrace);
throw;
}
finally finally
{ {
if (_onProcessing.CurrentCount == 0) if (_onProcessing.CurrentCount == 0)
@ -370,6 +378,13 @@ namespace IW4MAdmin.Application.Misc
return (T)(act.DynamicInvoke(JsValue.Null, return (T)(act.DynamicInvoke(JsValue.Null,
args.Select(arg => JsValue.FromObject(_scriptEngine, arg)).ToArray()) as ObjectWrapper)?.ToObject(); args.Select(arg => JsValue.FromObject(_scriptEngine, arg)).ToArray()) as ObjectWrapper)?.ToObject();
} }
catch (TargetInvocationException ex) when (ex.InnerException is JavaScriptException jsEx)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} with event type {@LocationInfo} {StackTrace}",
nameof(WrapDelegate), Path.GetFileName(_fileName), jsEx.Location, jsEx.JavaScriptStackTrace);
throw;
}
finally finally
{ {
if (_onProcessing.CurrentCount == 0) if (_onProcessing.CurrentCount == 0)
@ -545,7 +560,21 @@ namespace IW4MAdmin.Application.Misc
new Thread(() => new Thread(() =>
{ {
var tokenSource = new CancellationTokenSource(); if (DateTime.Now - (server.MatchEndTime ?? server.MatchStartTime) < TimeSpan.FromSeconds(15))
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogDebug("Not getting DVar because match recently ended");
}
OnComplete(new AsyncResult
{
IsCompleted = false,
AsyncState = (false, (string)null)
});
}
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(operationTimeout); tokenSource.CancelAfter(operationTimeout);
server.GetDvarAsync<string>(dvarName, token: tokenSource.Token).ContinueWith(action => server.GetDvarAsync<string>(dvarName, token: tokenSource.Token).ContinueWith(action =>
@ -612,7 +641,21 @@ namespace IW4MAdmin.Application.Misc
new Thread(() => new Thread(() =>
{ {
var tokenSource = new CancellationTokenSource(); if (DateTime.Now - (server.MatchEndTime ?? server.MatchStartTime) < TimeSpan.FromSeconds(15))
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogDebug("Not setting DVar because match recently ended");
}
OnComplete(new AsyncResult
{
IsCompleted = false,
AsyncState = false
});
}
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(operationTimeout); tokenSource.CancelAfter(operationTimeout);
server.SetDvarAsync(dvarName, dvarValue, token: tokenSource.Token).ContinueWith(action => server.SetDvarAsync(dvarName, dvarValue, token: tokenSource.Token).ContinueWith(action =>

View File

@ -14,8 +14,11 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
private Action _actions; private Action _actions;
private Delegate _jsAction; private Delegate _jsAction;
private string _actionName; private string _actionName;
private int _interval = DefaultInterval;
private long _waitingCount;
private const int DefaultDelay = 0; private const int DefaultDelay = 0;
private const int DefaultInterval = 1000; private const int DefaultInterval = 1000;
private const int MaxWaiting = 10;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly SemaphoreSlim _onRunningTick = new(1, 1); private readonly SemaphoreSlim _onRunningTick = new(1, 1);
private SemaphoreSlim _onDependentAction; private SemaphoreSlim _onDependentAction;
@ -57,6 +60,7 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
_logger.LogDebug("Starting script timer..."); _logger.LogDebug("Starting script timer...");
_timer ??= new Timer(callback => _actions(), null, delay, interval); _timer ??= new Timer(callback => _actions(), null, delay, interval);
_interval = interval;
IsRunning = true; IsRunning = true;
} }
@ -103,55 +107,67 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
_actions = OnTickInternal; _actions = OnTickInternal;
} }
private void ReleaseThreads() private void ReleaseThreads(bool releaseOnRunning, bool releaseOnDependent)
{ {
_logger.LogDebug("-Releasing OnTick for timer"); if (releaseOnRunning && _onRunningTick.CurrentCount == 0)
if (_onRunningTick.CurrentCount == 0)
{ {
_logger.LogDebug("-Releasing OnRunning for timer");
_onRunningTick.Release(1); _onRunningTick.Release(1);
} }
if (_onDependentAction?.CurrentCount != 0) if (releaseOnDependent && _onDependentAction?.CurrentCount == 0)
{ {
return; _onDependentAction?.Release(1);
} }
_onDependentAction?.Release(1);
} }
private async void OnTickInternal() private async void OnTickInternal()
{ {
var previousTimerRunning = false; var releaseOnRunning = false;
var releaseOnDependent = false;
try try
{ {
if (_onRunningTick.CurrentCount == 0) try
{
if (Interlocked.Read(ref _waitingCount) > MaxWaiting)
{
_logger.LogWarning("Reached max number of waiting count ({WaitingCount}) for {OnTick}",
_waitingCount, nameof(OnTickInternal));
return;
}
Interlocked.Increment(ref _waitingCount);
using var tokenSource1 = new CancellationTokenSource();
tokenSource1.CancelAfter(TimeSpan.FromMilliseconds(_interval));
await _onRunningTick.WaitAsync(tokenSource1.Token);
releaseOnRunning = true;
}
catch (OperationCanceledException)
{ {
_logger.LogDebug("Previous {OnTick} is still running, so we are skipping this one", _logger.LogDebug("Previous {OnTick} is still running, so we are skipping this one",
nameof(OnTickInternal)); nameof(OnTickInternal));
previousTimerRunning = true;
return; return;
} }
await _onRunningTick.WaitAsync(); using var tokenSource = new CancellationTokenSource();
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5)); tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
try try
{ {
// the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously // the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously
if (_onDependentAction is not null) if (_onDependentAction is not null)
{ {
await _onDependentAction.WaitAsync(tokenSource.Token); await _onDependentAction.WaitAsync(tokenSource.Token);
releaseOnDependent = true;
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
_logger.LogDebug("Dependent action did not release in allotted time so we are cancelling this tick"); _logger.LogWarning("Dependent action did not release in allotted time so we are cancelling this tick");
return; return;
} }
_logger.LogDebug("+Running OnTick for timer"); _logger.LogDebug("+Running OnTick for timer");
var start = DateTime.Now; var start = DateTime.Now;
_jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined }); _jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined });
@ -160,8 +176,9 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
catch (Exception ex) when (ex.InnerException is JavaScriptException jsx) catch (Exception ex) when (ex.InnerException is JavaScriptException jsx)
{ {
_logger.LogError(jsx, _logger.LogError(jsx,
"Could not execute timer tick for script action {ActionName} [@{LocationInfo}]", _actionName, "Could not execute timer tick for script action {ActionName} [{@LocationInfo}] [{@StackTrace}]",
jsx.Location); _actionName,
jsx.Location, jsx.JavaScriptStackTrace);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -169,10 +186,8 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
} }
finally finally
{ {
if (!previousTimerRunning) ReleaseThreads(releaseOnRunning, releaseOnDependent);
{ Interlocked.Decrement(ref _waitingCount);
ReleaseThreads();
}
} }
} }

View File

@ -409,24 +409,9 @@ namespace SharedLibraryCore
public abstract Task<long> GetIdForServer(Server server = null); public abstract Task<long> GetIdForServer(Server server = null);
public string[] ExecuteServerCommand(string command, int timeoutMs = 1000)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs));
try
{
return this.ExecuteCommandAsync(command, tokenSource.Token).GetAwaiter().GetResult();
}
catch
{
return null;
}
}
public string GetServerDvar(string dvarName, int timeoutMs = 1000) public string GetServerDvar(string dvarName, int timeoutMs = 1000)
{ {
var tokenSource = new CancellationTokenSource(); using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs)); tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs));
try try
{ {
@ -440,7 +425,7 @@ namespace SharedLibraryCore
public bool SetServerDvar(string dvarName, string dvarValue, int timeoutMs = 1000) public bool SetServerDvar(string dvarName, string dvarValue, int timeoutMs = 1000)
{ {
var tokenSource = new CancellationTokenSource(); using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs)); tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs));
try try
{ {

View File

@ -18,7 +18,6 @@ using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta; using SharedLibraryCore.Dtos.Meta;
using SharedLibraryCore.Formatting;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Localization; using SharedLibraryCore.Localization;
@ -1003,9 +1002,9 @@ namespace SharedLibraryCore
return true; return true;
} }
catch (Exception e) catch (Exception ex)
{ {
logger.LogError(e, $"Could not create penalty of type {penalty.Type.ToString()}"); logger.LogError(ex, "Could not create penalty of type {PenaltyType}", penalty.Type.ToString());
} }
return false; return false;
@ -1052,8 +1051,7 @@ namespace SharedLibraryCore
{ {
await Task.WhenAny(task, Task.Delay(timeout)); await Task.WhenAny(task, Task.Delay(timeout));
} }
public static bool ShouldHideLevel(this Permission perm) public static bool ShouldHideLevel(this Permission perm)
{ {
return perm == Permission.Flagged; return perm == Permission.Flagged;