attempt at resolving game interface threading issues (maybe)

This commit is contained in:
RaidMax 2022-10-17 10:45:42 -05:00
parent 12357fd9f7
commit a3fa5212f5
2 changed files with 117 additions and 62 deletions

View File

@ -113,7 +113,8 @@ namespace IW4MAdmin.Application.Misc
} }
_scriptEngine = new Engine(cfg => _scriptEngine = new Engine(cfg =>
cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable), typeof(ScriptPluginExtensions)) cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable),
typeof(ScriptPluginExtensions))
.AllowClr(new[] .AllowClr(new[]
{ {
typeof(System.Net.Http.HttpClient).Assembly, typeof(System.Net.Http.HttpClient).Assembly,
@ -360,7 +361,7 @@ namespace IW4MAdmin.Application.Misc
} }
} }
} }
public T WrapDelegate<T>(Delegate act, params object[] args) public T WrapDelegate<T>(Delegate act, params object[] args)
{ {
try try
@ -401,6 +402,7 @@ namespace IW4MAdmin.Application.Misc
{ {
dynamicCommand.permission = perm.ToString(); dynamicCommand.permission = perm.ToString();
} }
string permission = dynamicCommand.permission; string permission = dynamicCommand.permission;
List<Server.Game> supportedGames = null; List<Server.Game> supportedGames = null;
var targetRequired = false; var targetRequired = false;
@ -502,10 +504,14 @@ namespace IW4MAdmin.Application.Misc
private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted) private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
{ {
var operationTimeout = TimeSpan.FromSeconds(5);
void OnComplete(IAsyncResult result) void OnComplete(IAsyncResult result)
{ {
try try
{ {
_onProcessing.Wait();
var (success, value) = (ValueTuple<bool, string>)result.AsyncState; var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined, onCompleted.DynamicInvoke(JsValue.Undefined,
new[] new[]
@ -520,7 +526,7 @@ namespace IW4MAdmin.Application.Misc
{ {
using (LogContext.PushProperty("Server", server.ToString())) using (LogContext.PushProperty("Server", server.ToString()))
{ {
_logger.LogError(ex, "Could not complete BeginGetDvar for {Filename} {@Location}", _logger.LogError(ex, "Could not invoke BeginGetDvar callback for {Filename} {@Location}",
Path.GetFileName(_fileName), ex.Location); Path.GetFileName(_fileName), ex.Location);
} }
} }
@ -528,41 +534,51 @@ namespace IW4MAdmin.Application.Misc
{ {
_logger.LogError(ex, "Could not complete {BeginGetDvar} for {Class}", nameof(BeginGetDvar), Name); _logger.LogError(ex, "Could not complete {BeginGetDvar} for {Class}", nameof(BeginGetDvar), Name);
} }
}
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
server.BeginGetDvar(dvarName, result =>
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
}
finally finally
{ {
OnComplete(result); if (_onProcessing.CurrentCount == 0)
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{ {
_onProcessing.Release(); _onProcessing.Release(1);
} }
} }
}, tokenSource.Token); }
new Thread(() =>
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(operationTimeout);
server.GetDvarAsync<string>(dvarName, token: tokenSource.Token).ContinueWith(action =>
{
if (action.IsCompletedSuccessfully)
{
OnComplete(new AsyncResult
{
IsCompleted = true,
AsyncState = (true, action.Result.Value)
});
}
else
{
OnComplete(new AsyncResult
{
IsCompleted = false,
AsyncState = (false, (string)null)
});
}
});
}).Start();
} }
private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted) private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{ {
var tokenSource = new CancellationTokenSource(); var operationTimeout = TimeSpan.FromSeconds(5);
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
void OnComplete(IAsyncResult result) void OnComplete(IAsyncResult result)
{ {
try try
{ {
_onProcessing.Wait();
var success = (bool)result.AsyncState; var success = (bool)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined, onCompleted.DynamicInvoke(JsValue.Undefined,
new[] new[]
@ -585,25 +601,40 @@ namespace IW4MAdmin.Application.Misc
{ {
_logger.LogError(ex, "Could not complete {BeginSetDvar} for {Class}", nameof(BeginSetDvar), Name); _logger.LogError(ex, "Could not complete {BeginSetDvar} for {Class}", nameof(BeginSetDvar), Name);
} }
}
server.BeginSetDvar(dvarName, dvarValue, result =>
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
}
finally finally
{ {
OnComplete(result); if (_onProcessing.CurrentCount == 0)
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{ {
_onProcessing.Release(); _onProcessing.Release(1);
} }
} }
}, tokenSource.Token); }
new Thread(() =>
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(operationTimeout);
server.SetDvarAsync(dvarName, dvarValue, token: tokenSource.Token).ContinueWith(action =>
{
if (action.IsCompletedSuccessfully)
{
OnComplete(new AsyncResult
{
IsCompleted = true,
AsyncState = true
});
}
else
{
OnComplete(new AsyncResult
{
IsCompleted = false,
AsyncState = false
});
}
});
}).Start();
} }
} }
@ -617,7 +648,6 @@ namespace IW4MAdmin.Application.Misc
return true; return true;
} }
result = JsValue.Null; result = JsValue.Null;
return false; return false;
} }

View File

@ -17,7 +17,7 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
private const int DefaultDelay = 0; private const int DefaultDelay = 0;
private const int DefaultInterval = 1000; private const int DefaultInterval = 1000;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ManualResetEventSlim _onRunningTick = new(); private readonly SemaphoreSlim _onRunningTick = new(1, 1);
private SemaphoreSlim _onDependentAction; private SemaphoreSlim _onDependentAction;
public ScriptPluginTimerHelper(ILogger<ScriptPluginTimerHelper> logger) public ScriptPluginTimerHelper(ILogger<ScriptPluginTimerHelper> logger)
@ -31,6 +31,7 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{ {
Stop(); Stop();
} }
_onRunningTick.Dispose(); _onRunningTick.Dispose();
} }
@ -50,12 +51,11 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{ {
throw new ArgumentException("Timer interval must be at least 20ms"); throw new ArgumentException("Timer interval must be at least 20ms");
} }
Stop(); Stop();
_logger.LogDebug("Starting script timer..."); _logger.LogDebug("Starting script timer...");
_onRunningTick.Set();
_timer ??= new Timer(callback => _actions(), null, delay, interval); _timer ??= new Timer(callback => _actions(), null, delay, interval);
IsRunning = true; IsRunning = true;
} }
@ -76,7 +76,7 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{ {
return; return;
} }
_logger.LogDebug("Stopping script timer..."); _logger.LogDebug("Stopping script timer...");
_timer.Change(Timeout.Infinite, Timeout.Infinite); _timer.Change(Timeout.Infinite, Timeout.Infinite);
_timer.Dispose(); _timer.Dispose();
@ -100,54 +100,79 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
_jsAction = action; _jsAction = action;
_actionName = actionName; _actionName = actionName;
_actions = OnTick; _actions = OnTickInternal;
} }
private void ReleaseThreads() private void ReleaseThreads()
{ {
_onRunningTick.Set(); _logger.LogDebug("-Releasing OnTick for timer");
if (_onRunningTick.CurrentCount == 0)
{
_onRunningTick.Release(1);
}
if (_onDependentAction?.CurrentCount != 0) if (_onDependentAction?.CurrentCount != 0)
{ {
return; return;
} }
_logger.LogDebug("-Releasing OnTick for timer");
_onDependentAction?.Release(1); _onDependentAction?.Release(1);
} }
private void OnTick()
private async void OnTickInternal()
{ {
var previousTimerRunning = false;
try try
{ {
if (!_onRunningTick.IsSet) if (_onRunningTick.CurrentCount == 0)
{ {
_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(OnTick)); nameof(OnTickInternal));
previousTimerRunning = true;
return; return;
} }
_onRunningTick.Reset(); await _onRunningTick.WaitAsync();
// the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously var tokenSource = new CancellationTokenSource();
_onDependentAction?.Wait(); tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
try
{
// 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)
{
await _onDependentAction.WaitAsync(tokenSource.Token);
}
}
catch (OperationCanceledException)
{
_logger.LogDebug("Dependent action did not release in allotted time so we are cancelling this tick");
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 });
_logger.LogDebug("OnTick took {Time}ms", (DateTime.Now - start).TotalMilliseconds); _logger.LogDebug("OnTick took {Time}ms", (DateTime.Now - start).TotalMilliseconds);
ReleaseThreads();
} }
catch (Exception ex) when (ex.InnerException is JavaScriptException jsx)
catch (Exception ex) when (ex.InnerException is JavaScriptException jsex)
{ {
_logger.LogError(jsex, _logger.LogError(jsx,
"Could not execute timer tick for script action {ActionName} [@{LocationInfo}]", _actionName, "Could not execute timer tick for script action {ActionName} [@{LocationInfo}]", _actionName,
jsex.Location); jsx.Location);
ReleaseThreads();
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Could not execute timer tick for script action {ActionName}", _actionName); _logger.LogError(ex, "Could not execute timer tick for script action {ActionName}", _actionName);
_onRunningTick.Set(); }
ReleaseThreads(); finally
{
if (!previousTimerRunning)
{
ReleaseThreads();
}
} }
} }