Skip to content
Merged
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
49 changes: 49 additions & 0 deletions Keybinding.Test/ProfileCreateConcurrencyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Keybinding.Test;

using ktsu.Keybinding.Core.Models;
using ktsu.Keybinding.Core.Services;

[TestClass]
public class ProfileCreateConcurrencyTests
{
private const int ThreadCount = 8;
private const int Trials = 500;

public TestContext TestContext { get; set; } = null!;

[TestMethod]
public void CreateProfile_CalledConcurrentlyForOneId_ReturnsTheStoredProfileToEveryCaller()
{
for (int trial = 0; trial < Trials; trial++)
{
ProfileManager profiles = new();
Profile[] returned = new Profile[ThreadCount];
using Barrier barrier = new(ThreadCount);

Thread[] threads = [.. Enumerable.Range(0, ThreadCount).Select(i => new Thread(() =>
{
barrier.SignalAndWait(TestContext.CancellationToken);
returned[i] = profiles.CreateProfile("p", "P");
}))];

foreach (Thread thread in threads)
{
thread.Start();
}

foreach (Thread thread in threads)
{
thread.Join();
}

Profile? stored = profiles.GetProfile("p");
Assert.IsNotNull(stored);
for (int i = 0; i < ThreadCount; i++)
{
Assert.AreSame(stored, returned[i], $"Trial {trial}: caller {i} got a Profile that was never stored, so its bindings would be lost.");
}
}
}
}
14 changes: 4 additions & 10 deletions Keybinding/Services/ProfileManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
{
if (string.IsNullOrWhiteSpace(id))
{
throw new ArgumentException("Profile ID cannot be null or whitespace", nameof(id));

Check warning on line 35 in Keybinding/Services/ProfileManager.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Profile ID cannot be null or whitespace' 6 times.

Check warning on line 35 in Keybinding/Services/ProfileManager.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Profile ID cannot be null or whitespace' 6 times.

Check warning on line 35 in Keybinding/Services/ProfileManager.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Profile ID cannot be null or whitespace' 6 times.

Check warning on line 35 in Keybinding/Services/ProfileManager.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Profile ID cannot be null or whitespace' 6 times.

Check warning on line 35 in Keybinding/Services/ProfileManager.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'Profile ID cannot be null or whitespace' 6 times.
}

if (string.IsNullOrWhiteSpace(name))
Expand All @@ -42,16 +42,10 @@

string normalizedId = id.Trim();

// Return existing profile if it already exists
if (_profiles.TryGetValue(normalizedId, out Profile? existingProfile))
{
return existingProfile;
}

// Create new profile
Profile newProfile = new(normalizedId, name.Trim(), description);
_profiles.TryAdd(normalizedId, newProfile);
return newProfile;
// Return the existing profile, or store a new one, in one atomic step: a separate lookup
// and add let two concurrent callers each return their own Profile while only one of
// them was stored (ktsu-dev/Keybinding#112)
return _profiles.GetOrAdd(normalizedId, key => new Profile(key, name.Trim(), description));
}

/// <inheritdoc/>
Expand Down Expand Up @@ -79,7 +73,7 @@
{
return string.IsNullOrWhiteSpace(profileId)
? throw new ArgumentException("Profile ID cannot be null or whitespace", nameof(profileId))
: _profiles.TryGetValue(profileId.Trim(), out Profile? profile) ? profile : null;

Check warning on line 76 in Keybinding/Services/ProfileManager.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 76 in Keybinding/Services/ProfileManager.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 76 in Keybinding/Services/ProfileManager.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 76 in Keybinding/Services/ProfileManager.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 76 in Keybinding/Services/ProfileManager.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.
}

/// <inheritdoc/>
Expand Down
Loading