Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

commands cooldowns using load factors #280

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions TPP.Core/Commands/CommandProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,27 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NodaTime;
using TPP.ArgsParsing;
using TPP.Core.Utils;
using TPP.Model;
using TPP.Persistence;

namespace TPP.Core.Commands
{
public interface ICommandProcessor
{
public Task<CommandResult?> Process(string commandName, IImmutableList<string> args, Message message);
public Command? FindCommand(string commandName);
public void InstallCommand(Command command);
public void UninstallCommand(params string[] commandOrAlias);
}

/// <summary>
/// The command processor can be configured using <see cref="Command"/> instances to have commands,
/// which then get executed using the <see cref="CommandProcessor.Process"/> method.
/// </summary>
public class CommandProcessor
public class CommandProcessor : ICommandProcessor
{
/// <summary>
/// maximum execution time for a command before a warning is logged.
Expand All @@ -24,16 +35,45 @@ public class CommandProcessor
private readonly ILogger<CommandProcessor> _logger;
private readonly ICommandLogger _commandLogger;
private readonly ArgsParser _argsParser;
private readonly IClock _clock;

private readonly Dictionary<string, Command> _commands = new();

private readonly float _maxLoadFactor;
private readonly Duration _maxLoadFactorTimeframe;
private readonly float _additionalLoadFactorAtHighThreshold;
private Dictionary<User, TtlQueue<float>> _loadsPerUser = new();

/// <summary>
/// Create a new command processor instance
/// </summary>
/// <param name="logger">logger</param>
/// <param name="commandLogger">command logger</param>
/// <param name="argsParser">args parser instance</param>
/// <param name="clock">clock</param>
/// <param name="maxLoadFactor">maximum load factor before commands are silently dropped</param>
/// <param name="maxLoadFactorTimeframe">timeframe for which the load factor is computed</param>
/// <param name="additionalLoadFactorAtHighThreshold">
/// additional load to add to the load factor when a user is at their maximum load capacity.
/// It is linearly interpolated from 0 when there are no messages within the timeframe,
/// up to the supplied number multiplier when at the maximum amount of messages within the timeframe.
/// This is to have the load factor be more effective against continuous spam than sporadic bursts.</param>
public CommandProcessor(
ILogger<CommandProcessor> logger,
ICommandLogger commandLogger,
ArgsParser argsParser)
ArgsParser argsParser,
IClock clock,
float maxLoadFactor = 200f,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can be moved to class constants, E.G. DefaultMaxLoadFactor, ...
this should provide (marginally) easier fine-tuning when the time comes

Duration? maxLoadFactorTimeframe = null,
float additionalLoadFactorAtHighThreshold = 2f)
{
_logger = logger;
_commandLogger = commandLogger;
_argsParser = argsParser;
_clock = clock;
_maxLoadFactor = maxLoadFactor;
_maxLoadFactorTimeframe = maxLoadFactorTimeframe ?? Duration.FromMinutes(10);
_additionalLoadFactorAtHighThreshold = additionalLoadFactorAtHighThreshold;
}

public void InstallCommand(Command command)
Expand Down Expand Up @@ -69,8 +109,34 @@ public void UninstallCommand(params string[] commandOrAlias)
public Command? FindCommand(string commandName) =>
_commands.TryGetValue(commandName.ToLower(), out Command command) ? command : null;

private float CheckAndUpdateLoadFactorForUser(User user)
{
_loadsPerUser = _loadsPerUser
.Where(kvp => kvp.Value.Count > 0)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (!_loadsPerUser.TryGetValue(user, out TtlQueue<float>? loads))
{
loads = new TtlQueue<float>(_maxLoadFactorTimeframe, _clock);
_loadsPerUser[user] = loads;
}
float sum = loads.Sum();
float ratioFilled = Math.Min(1, sum / _maxLoadFactor);
float toAdd = 1 + ratioFilled * _additionalLoadFactorAtHighThreshold;
loads.Enqueue(toAdd);
return sum + toAdd;
}

public async Task<CommandResult?> Process(string commandName, IImmutableList<string> args, Message message)
{
float loadFactor = CheckAndUpdateLoadFactorForUser(message.User);
if (loadFactor > _maxLoadFactor)
{
_logger.LogDebug(
"command '{Command}' from user {User} ignored because load factor is {LoadFactor} " +
"for timeframe {Duration}, which is above the maximum of {MaxLoadFactor}",
commandName, message.User, loadFactor, _maxLoadFactorTimeframe, _maxLoadFactor);
return new CommandResult();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return some feedback to the user to let them know they are spamming too hard. I think the natural reaction to your command not being processed is to try again, which may lead to users just digging themselves into a deeper hole and frustrate nonmalicious users.

}
if (!_commands.TryGetValue(commandName.ToLower(), out Command command))
{
_logger.LogDebug("unknown command '{Command}'", commandName);
Expand Down
4 changes: 2 additions & 2 deletions TPP.Core/Commands/Definitions/HelpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ public class HelpCommand
Description = "Get general help, or info on a specific command like: \"!help balance\""
};

private readonly CommandProcessor _commandProcessor;
public HelpCommand(CommandProcessor commandProcessor) => _commandProcessor = commandProcessor;
private readonly ICommandProcessor _commandProcessor;
public HelpCommand(ICommandProcessor commandProcessor) => _commandProcessor = commandProcessor;

private CommandResult Execute(CommandContext context)
{
Expand Down
4 changes: 2 additions & 2 deletions TPP.Core/Modes/ModeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public sealed class ModeBase : IDisposable
private readonly ILogger<ModeBase> _logger;
private readonly IImmutableDictionary<string, IChat> _chats;
private readonly IImmutableDictionary<string, ICommandResponder> _commandResponders;
private readonly IImmutableDictionary<string, CommandProcessor> _commandProcessors;
private readonly IImmutableDictionary<string, ICommandProcessor> _commandProcessors;
private readonly IImmutableDictionary<string, IModerator> _moderators;
private readonly IImmutableDictionary<string, AdvertisePollsWorker> _advertisePollsWorkers;
private readonly IMessagequeueRepo _messagequeueRepo;
Expand Down Expand Up @@ -105,7 +105,7 @@ public ModeBase(

public void InstallAdditionalCommand(Command command)
{
foreach (CommandProcessor commandProcessor in _commandProcessors.Values)
foreach (ICommandProcessor commandProcessor in _commandProcessors.Values)
commandProcessor.InstallCommand(command);
}

Expand Down
8 changes: 4 additions & 4 deletions TPP.Core/Setups.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public static ArgsParser SetUpArgsParser(IUserRepo userRepo, PokedexData pokedex
return argsParser;
}

public static CommandProcessor SetUpCommandProcessor(
public static ICommandProcessor SetUpCommandProcessor(
ILoggerFactory loggerFactory,
ArgsParser argsParser,
Databases databases,
Expand All @@ -68,9 +68,9 @@ public static CommandProcessor SetUpCommandProcessor(
IChatModeChanger chatModeChanger,
IImmutableSet<Common.PkmnSpecies> knownSpecies)
{
var commandProcessor = new CommandProcessor(
ICommandProcessor commandProcessor = new CommandProcessor(
loggerFactory.CreateLogger<CommandProcessor>(),
databases.CommandLogger, argsParser);
databases.CommandLogger, argsParser, SystemClock.Instance);

IEnumerable<Command> commands = new[]
{
Expand Down Expand Up @@ -180,7 +180,7 @@ public static (WebsocketBroadcastServer, OverlayConnection) SetUpOverlayServer(
}

private static void SetUpDynamicCommands(
ILogger logger, CommandProcessor commandProcessor, IResponseCommandRepo responseCommandRepo)
ILogger logger, ICommandProcessor commandProcessor, IResponseCommandRepo responseCommandRepo)
{
IImmutableList<ResponseCommand> commands = responseCommandRepo.GetCommands().Result;

Expand Down
Loading