-
Notifications
You must be signed in to change notification settings - Fork 8
Open
Description
The CommandService.TryPrepareCommandAsync method throws an ArgumentException when called multiple times at the same time. I've set up a small test environment that prepares a command 4 times at the same time - I think it's possible for example in a Discord bot which is in a larger amount of servers and two or more users may execute a command at the same time. If the same command is executed one after another, the exception is not thrown.
The exception:
System.ArgumentException: An item with the same key has already been added. Key: __default
at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
at Remora.Commands.Services.CommandTreeAccessor.TryGetNamedTree(String treeName, CommandTree& tree)
at Remora.Commands.Services.CommandService.TryPrepareCommandAsync(String commandString, IServiceProvider services, TokenizerOptions tokenizerOptions, TreeSearchOptions searchOptions, String treeName, CancellationToken ct)
The code snippet I used:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Remora.Commands.Attributes;
using Remora.Commands.Extensions;
using Remora.Commands.Groups;
using Remora.Commands.Services;
using Remora.Commands.Tokenization;
using Remora.Commands.Trees;
using Remora.Extensions.Options.Immutable;
using Remora.Results;
ServiceCollection services = new();
services
.AddCommands()
.AddCommandTree()
.WithCommandGroup<MyFirstGroup>()
.Finish();
// https://github.com/Remora/Remora.Discord/blob/2bc5fc43a035dbb04840c89c1cec38e240c95f7e/Remora.Discord.Commands/Extensions/ServiceCollectionExtensions.cs#L213
// The Configure extension is from the Remora.Extensions.Options.Immutable package
services.Configure<TokenizerOptions>(static opt => opt);
services.Configure<TreeSearchOptions>(static opt =>
opt with { KeyComparison = StringComparison.OrdinalIgnoreCase });
ServiceProvider provider = services.BuildServiceProvider();
CommandService commandService = provider.GetRequiredService<CommandService>();
IOptions<TokenizerOptions> tokenizerOptions = provider.GetRequiredService<IOptions<TokenizerOptions>>();
IOptions<TreeSearchOptions> treeSearchOptions = provider.GetRequiredService<IOptions<TreeSearchOptions>>();
// Simulating a situation where multiple commands may be prepared at almost the same time
for(int i = 0; i < 4; i++)
{
_ = Task.Run(async () =>
{
Result<PreparedCommand> prepareCommandResult =
await commandService.TryPrepareCommandAsync(
"do-thing",
provider,
tokenizerOptions.Value,
treeSearchOptions.Value);
if(!prepareCommandResult.IsDefined(out PreparedCommand? preparedCommand))
{
return;
}
await commandService.TryExecuteAsync(preparedCommand, provider);
});
// The exception is not thrown if we add a small delay
// await Task.Delay(TimeSpan.FromSeconds(0.5));
}
Console.ReadKey();
public class MyFirstGroup : CommandGroup
{
[Command("do-thing")]
public Task<IResult> MyCommand()
=> Task.FromResult<IResult>(Result.Success);
}Metadata
Metadata
Assignees
Labels
No labels