diff --git a/src/KubeOps.Operator/Queue/TimedEntityQueue.cs b/src/KubeOps.Operator/Queue/TimedEntityQueue.cs index 9c1bc384..a9800ed9 100644 --- a/src/KubeOps.Operator/Queue/TimedEntityQueue.cs +++ b/src/KubeOps.Operator/Queue/TimedEntityQueue.cs @@ -33,7 +33,7 @@ internal sealed class TimedEntityQueue : IDisposable public void Enqueue(TEntity entity, TimeSpan requeueIn) { _management.AddOrUpdate( - entity.Name() ?? throw new InvalidOperationException("Cannot enqueue entities without name."), + GetKey(entity) ?? throw new InvalidOperationException("Cannot enqueue entities without name."), key => { var entry = new TimedQueueEntry(entity, requeueIn); @@ -81,15 +81,30 @@ public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken canc public void Remove(TEntity entity) { - var name = entity.Name(); - if (name is null) + var key = GetKey(entity); + if (key is null) { return; } - if (_management.Remove(name, out var task)) + if (_management.Remove(key, out var task)) { task.Cancel(); } } + + private string? GetKey(TEntity entity) + { + if (entity.Name() is null) + { + return null; + } + + if (entity.Namespace() is null) + { + return entity.Name(); + } + + return $"{entity.Namespace()}/{entity.Name()}"; + } } diff --git a/test/KubeOps.Operator.Test/Queue/TimedEntityQueue.Test.cs b/test/KubeOps.Operator.Test/Queue/TimedEntityQueue.Test.cs new file mode 100644 index 00000000..734d5d74 --- /dev/null +++ b/test/KubeOps.Operator.Test/Queue/TimedEntityQueue.Test.cs @@ -0,0 +1,49 @@ +using k8s.Models; + +using KubeOps.Operator.Queue; + +namespace KubeOps.Operator.Test.Queue; + +public class TimedEntityQueueTest +{ + [Fact] + public async Task Can_Enqueue_Multiple_Entities_With_Same_Name() + { + var queue = new TimedEntityQueue(); + + queue.Enqueue(CreateSecret("app-ns1", "secret-name"), TimeSpan.FromSeconds(1)); + queue.Enqueue(CreateSecret("app-ns2", "secret-name"), TimeSpan.FromSeconds(1)); + + var items = new List(); + + var tokenSource = new CancellationTokenSource(); + tokenSource.CancelAfter(TimeSpan.FromSeconds(2)); + + var enumerator = queue.GetAsyncEnumerator(tokenSource.Token); + + try + { + while (await enumerator.MoveNextAsync()) + { + items.Add(enumerator.Current); + } + } + catch (OperationCanceledException) + { + // We expect to timeout watching the queue so that we can assert the items received + } + + Assert.Equal(2, items.Count); + } + + private V1Secret CreateSecret(string secretNamespace, string secretName) + { + var secret = new V1Secret(); + secret.EnsureMetadata(); + + secret.Metadata.SetNamespace(secretNamespace); + secret.Metadata.Name = secretName; + + return secret; + } +}