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
12 changes: 12 additions & 0 deletions src/NATS.Client.KeyValueStore/Internal/NatsKVWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,18 @@ private async Task CommandLoop()
_ => operation,
};
}
else if (headers.TryGetValue(NatsKVStore.NatsMarkerReason, out var markerReasonValues))
{
var reason = markerReasonValues.Last();
if (reason is "MaxAge" or "Purge")
{
operation = NatsKVOperation.Purge;
Comment on lines +213 to +215

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we distinguish between MaxAge and Purge with a NatsKVOperation.MaxAge? I can see this being useful information in NatsKvEntry

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

i thought that too but i think that's the kv api design that we don't want to expose too much detail of the underlying storage cc @ripienaar https://github.com/nats-io/nats-architecture-and-design/blob/main/adr/ADR-8.md#retrieving-values

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah, I didn’t realize it was in the ADR to treat as Purge. Thanks for that link, implementation looks correct.

}
else if (reason is "Remove")
{
operation = NatsKVOperation.Del;
}
}

if (headers is { Code: 100, MessageText: "FlowControl Request" })
{
Expand Down
17 changes: 17 additions & 0 deletions src/NATS.Client.KeyValueStore/NatsKVStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public enum NatsKVOperation
/// </summary>
public class NatsKVStore : INatsKVStore
{
internal const string NatsMarkerReason = "Nats-Marker-Reason";
private const string NatsExpectedLastSubjectSequence = "Nats-Expected-Last-Subject-Sequence";
private const string KVOperation = "KV-Operation";
private const string NatsRollup = "Nats-Rollup";
Expand Down Expand Up @@ -421,6 +422,22 @@ public async ValueTask<NatsResult<NatsKVEntry<T>>> TryGetEntryAsync<T>(string ke
if (!Enum.TryParse(operationValues[0], ignoreCase: true, out operation))
return InvalidOperationException;
}
else if (headers.TryGetValue(NatsMarkerReason, out var markerReasonValues))
{
var reason = markerReasonValues.Last();
if (reason is "MaxAge" or "Purge")
{
operation = NatsKVOperation.Purge;
Comment thread
caleblloyd marked this conversation as resolved.
}
else if (reason is "Remove")
{
operation = NatsKVOperation.Del;
}
else
{
return InvalidOperationException;
}
}

if (operation is NatsKVOperation.Del or NatsKVOperation.Purge)
{
Expand Down
50 changes: 50 additions & 0 deletions tests/NATS.Client.KeyValueStore.Tests/KeyValueStoreTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,56 @@ public async Task SetsSubjectDeleteMarkerTTL()
Assert.Equal(TimeSpan.FromSeconds(2), info.Info.Config.SubjectDeleteMarkerTTL);
}

[SkipIfNatsServer(versionEarlierThan: "2.11")]
public async Task SubjectDeleteMarkerTTL_enabled_removals_should_be_interpreted_as_Operation_Purge()
{
await using var server = await NatsServer.StartJSAsync();
await using var nats = await server.CreateClientConnectionAsync();

var js = new NatsJSContext(nats);
var kv = new NatsKVContext(js);

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var cancellationToken = cts.Token;

var store = await kv.CreateStoreAsync(
new NatsKVConfig("kv1")
{
AllowMsgTTL = true,
SubjectDeleteMarkerTTL = TimeSpan.FromHours(1),
MaxAge = TimeSpan.FromSeconds(4),
},
cancellationToken: cancellationToken);

var r1 = await store.CreateAsync("foo", "LOCKED", cancellationToken: cancellationToken);
Assert.Equal(1ul, r1);

var create = Task.Run(
async () =>
{
await Task.Delay(6000, cancellationToken);
Console.WriteLine("6 seconds passed — creating...");
return await store.CreateAsync("foo", "LOCKED", ttl: TimeSpan.FromSeconds(1), cancellationToken: cancellationToken);
},
cancellationToken);

var checkOps = new List<NatsKVOperation>();
await foreach (var entry in store.WatchAsync<string>("foo", opts: new() { IncludeHistory = true }, cancellationToken: cancellationToken))
{
checkOps.Add(entry.Operation);
if (entry.Revision == 3)
break;
}

Assert.Equal(3, checkOps.Count);
Assert.Equal(NatsKVOperation.Put, checkOps[0]);
Assert.Equal(NatsKVOperation.Purge, checkOps[1]);
Assert.Equal(NatsKVOperation.Put, checkOps[2]);

var r2 = await create;
Assert.Equal(3ul, r2);
}

[SkipIfNatsServer(versionEarlierThan: "2.11")]
public async Task TestMessageNeverExpire()
{
Expand Down