Skip to content

Commit

Permalink
Simplify async pattern in EOBot. Rely primarily on CancellationToken …
Browse files Browse the repository at this point in the history
…as a signal for when work is cancelled.

Consolidate console output calls to ConsoleHelper
Remove IBotFrameworkOutputHandler and implementation.
  • Loading branch information
ethanmoffat committed May 12, 2021
1 parent f9bb9a8 commit 816ee02
Show file tree
Hide file tree
Showing 9 changed files with 44 additions and 183 deletions.
19 changes: 0 additions & 19 deletions EOBot/ArgumentsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public class ArgumentsParser
public int NumBots { get; private set; }
public int SimultaneousBots { get; private set; }

public bool WaitForTermination { get; private set; }
public int InitDelay { get; private set; }

public string Account { get; private set; }
Expand Down Expand Up @@ -68,10 +67,6 @@ public ArgumentsParser(string[] args)
if (!ParseNumBots(pair))
return;
break;
case "wait":
if (!ParseWaitFlag(pair[1]))
return;
break;
case "initdelay":
if (!ParseInitDelay(pair[1]))
return;
Expand Down Expand Up @@ -148,20 +143,6 @@ private bool ParseNumBots(string[] pair)
return true;
}

private bool ParseWaitFlag(string waitFlag)
{
var validFlags = new[] {"0", "no", "false", "1", "yes", "true"};
if (!validFlags.Contains(waitFlag))
{
Error = ArgsError.InvalidWaitFlag;
return false;
}

WaitForTermination = validFlags.ToList().FindIndex(x => x == waitFlag) > 2;

return true;
}

private bool ParseInitDelay(string initDelay)
{
int delay;
Expand Down
77 changes: 4 additions & 73 deletions EOBot/BotBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,15 @@ namespace EOBot
internal abstract class BotBase : IBot
{
//base class implementation - should not be modified in derived classes
private Thread _workerThread;
private AutoResetEvent _terminationEvent;
private CancellationTokenSource _cancelTokenSource;
private bool _initialized;

protected readonly int _index;
protected bool TerminationRequested => _cancelTokenSource != null && _cancelTokenSource.IsCancellationRequested;

public event Action WorkCompleted;

protected BotBase(int botIndex)
{
_index = botIndex;

_terminationEvent = new AutoResetEvent(false);
_cancelTokenSource = new CancellationTokenSource();
}

//all bots are going to want to do the init handshake with the server
Expand Down Expand Up @@ -77,81 +70,19 @@ public virtual async Task InitializeAsync(string host, int port)
_initialized = true;
}

/// <summary>
/// Run the bot
/// </summary>
/// <param name="waitForTermination">True to keep running until Terminate() is called, false otherwise</param>
public void Run(bool waitForTermination)
public async Task RunAsync(CancellationToken ct)
{
if (!_initialized)
throw new InvalidOperationException("Must call Initialize() before calling Run()");

var doWorkAndWait = new ThreadStart(DoWorkAndWaitForTermination);
var doWork = new ThreadStart(DoWorkOnly);
throw new InvalidOperationException("Initialize must be called before calling RunAsync");

_workerThread = new Thread(waitForTermination ? doWorkAndWait : doWork);
_workerThread.Start();
await DoWorkAsync(ct);
WorkCompleted?.Invoke();
}

/// <summary>
/// Abstract worker method. Override with custom work logic for the bot to execute
/// </summary>
/// <param name="ct">A cancellation token that will be signalled when Terminate() is called</param>
protected abstract Task DoWorkAsync(CancellationToken ct);

private async void DoWorkOnly()
{
await DoWorkAsync(_cancelTokenSource.Token);
FireWorkCompleted();
}

private async void DoWorkAndWaitForTermination()
{
await DoWorkAsync(_cancelTokenSource.Token);
_terminationEvent.WaitOne();
FireWorkCompleted();
}

/// <summary>
/// Terminate the bot. Ends execution as soon as is convenient.
/// </summary>
public void Terminate()
{
_cancelTokenSource.Cancel();
_terminationEvent.Set();
}

private void FireWorkCompleted()
{
if (WorkCompleted != null)
WorkCompleted();
}

public void Dispose()
{
Dispose(true);
}

~BotBase()
{
Dispose(false);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Terminate();

_workerThread?.Join();

_terminationEvent?.Dispose();
_terminationEvent = null;

_cancelTokenSource?.Dispose();
_cancelTokenSource = null;
}
}
}
}
28 changes: 0 additions & 28 deletions EOBot/BotConsoleOutputHandler.cs

This file was deleted.

48 changes: 19 additions & 29 deletions EOBot/BotFramework.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ sealed class BotFramework : IDisposable
{
public const int NUM_BOTS_MAX = 25;

private readonly CancellationTokenSource _cancellationTokenSource;
private readonly List<IBot> _botsList;
private readonly IBotFrameworkOutputHandler _outputHandler;
private readonly string _host;
private readonly ushort _port;

Expand All @@ -20,11 +20,12 @@ sealed class BotFramework : IDisposable
private int _numBots;
private bool _terminating;

public BotFramework(IBotFrameworkOutputHandler outputHandler, ArgumentsParser parsedArgs)
public BotFramework(ArgumentsParser parsedArgs)
{
_outputHandler = outputHandler;
if(outputHandler == null || parsedArgs == null)
throw new ArgumentNullException("One or more arguments to framework is null", new Exception());
if (parsedArgs == null)
throw new ArgumentNullException(nameof(parsedArgs));

_cancellationTokenSource = new CancellationTokenSource();

var numberOfBots = parsedArgs.NumBots;
var simultaneousBots = parsedArgs.SimultaneousBots;
Expand Down Expand Up @@ -66,18 +67,18 @@ public async Task InitializeAsync(IBotFactory botFactory, int delayBetweenInitsM
}
catch(Exception ex)
{
_outputHandler.OutputBotInitializationFailed(ex.Message);
ConsoleHelper.WriteMessage(ConsoleHelper.Type.Error, ex.Message, ConsoleColor.DarkRed);
numFailed++;
continue;
}

_outputHandler.OutputBotInitializationSucceeded(i);
ConsoleHelper.WriteMessage(ConsoleHelper.Type.None, $"Bot {i} initialized.");
Thread.Sleep(delayBetweenInitsMS); //minimum for this is 1sec server-side
}

if (numFailed > 0)
{
_outputHandler.OutputWarnSomeBotsFailed();
ConsoleHelper.WriteMessage(ConsoleHelper.Type.Warning, "Some bot instances failed to initialize. These bot instances will not be run.", ConsoleColor.DarkYellow);
_numBots -= numFailed;
}
else if (numFailed == _numBots)
Expand All @@ -88,38 +89,31 @@ public async Task InitializeAsync(IBotFactory botFactory, int delayBetweenInitsM
_initialized = true;
}

public void Run(bool waitForTermination)
public async Task RunAsync()
{
if(!_initialized)
throw new InvalidOperationException("Must call Initialize() before running!");

_outputHandler.OutputAllBotsAreRunning(waitForTermination);
var botTasks = new List<Task>();

ConsoleHelper.WriteMessage(ConsoleHelper.Type.None, "Bot framework run has started.\n");
for (int i = 0; i < _numBots; ++i)
{
_doneSignal.WaitOne();
//acquire mutex for bot
//semaphore limits number of concurrently running bots based on cmd-line param
_botsList[i].Run(waitForTermination);
botTasks.Add(_botsList[i].RunAsync(_cancellationTokenSource.Token));
}

await Task.WhenAll(botTasks).ConfigureAwait(false);
}

public void TerminateBots()
{
_terminating = true;
if (!_initialized) return;

_botsList.ForEach(_bot => _bot.Terminate());
_botsList.Clear();
}

public void WaitForBotsToComplete()
{
if (!_initialized) return;

for (int i = 0; i < _numBots; ++i)
{
_doneSignal.WaitOne();
}
_cancellationTokenSource.Cancel();
}

~BotFramework()
Expand All @@ -137,12 +131,8 @@ private void Dispose(bool disposing)
{
if (disposing)
{
_botsList.ForEach(bot => bot.Dispose());
if (_doneSignal != null)
{
_doneSignal.Dispose();
_doneSignal = null;
}
_doneSignal?.Dispose();
_cancellationTokenSource?.Dispose();
}
}
}
Expand Down
2 changes: 0 additions & 2 deletions EOBot/EOBot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,12 @@
<Compile Include="ArgumentsParser.cs" />
<Compile Include="BotHelper.cs" />
<Compile Include="BotBase.cs" />
<Compile Include="BotConsoleOutputHandler.cs" />
<Compile Include="BotException.cs" />
<Compile Include="BotFramework.cs" />
<Compile Include="ConsoleHelper.cs" />
<Compile Include="DependencyMaster.cs" />
<Compile Include="IBot.cs" />
<Compile Include="IBotFactory.cs" />
<Compile Include="IBotFrameworkOutputHandler.cs" />
<Compile Include="NamesList.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
Expand Down
10 changes: 3 additions & 7 deletions EOBot/IBot.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace EOBot
{
public interface IBot : IDisposable
public interface IBot
{
/// <summary>
/// Event that is called when work for the Bot has been completed
Expand All @@ -19,11 +20,6 @@ public interface IBot : IDisposable
/// Run logic for the bot instance. Called automatically by the framework.
/// </summary>
/// <param name="waitForTermination">True to wait until a call to Terminate() is made, false otherwise</param>
void Run(bool waitForTermination);

/// <summary>
/// Forcibly terminate a bot instance that is running or waiting for termination (ie regardless of state)
/// </summary>
void Terminate();
Task RunAsync(CancellationToken cancellationToken);
}
}
10 changes: 0 additions & 10 deletions EOBot/IBotFrameworkOutputHandler.cs

This file was deleted.

0 comments on commit 816ee02

Please sign in to comment.