diff --git a/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadata.cs b/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadata.cs index c5bd4642fa..38bf63c79b 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadata.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/FullFidelity/ChangeFeedMetadata.cs @@ -19,55 +19,36 @@ namespace Microsoft.Azure.Cosmos #endif class ChangeFeedMetadata { - /// - /// New instance of meta data for created. - /// - /// - /// - /// - /// - public ChangeFeedMetadata( - DateTime conflictResolutionTimestamp, - long lsn, - ChangeFeedOperationType operationType, - long previousLsn) - { - this.ConflictResolutionTimestamp = conflictResolutionTimestamp; - this.Lsn = lsn; - this.OperationType = operationType; - this.PreviousLsn = previousLsn; - } - /// /// The conflict resolution timestamp. /// [JsonProperty(PropertyName = "crts", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(UnixDateTimeConverter))] - public DateTime ConflictResolutionTimestamp { get; } + public DateTime ConflictResolutionTimestamp { get; internal set; } /// /// The current logical sequence number. /// [JsonProperty(PropertyName = "lsn", NullValueHandling = NullValueHandling.Ignore)] - public long Lsn { get; } + public long Lsn { get; internal set; } /// /// The change feed operation type. /// [JsonProperty(PropertyName = "operationType")] [JsonConverter(typeof(StringEnumConverter))] - public ChangeFeedOperationType OperationType { get; } + public ChangeFeedOperationType OperationType { get; internal set; } /// /// The previous logical sequence number. /// [JsonProperty(PropertyName = "previousImageLSN", NullValueHandling = NullValueHandling.Ignore)] - public long PreviousLsn { get; } + public long PreviousLsn { get; internal set; } /// /// Used to distinquish explicit deletes (e.g. via DeleteItem) from deletes caused by TTL expiration (a collection may define time-to-live policy for documents). /// [JsonProperty(PropertyName = "timeToLiveExpired", NullValueHandling= NullValueHandling.Ignore)] - public bool IsTimeToLiveExpired { get; } + public bool IsTimeToLiveExpired { get; internal set; } } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/GetChangeFeedProcessorBuilderWithAllVersionsAndDeletesTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/GetChangeFeedProcessorBuilderWithAllVersionsAndDeletesTests.cs index 7217993fd5..0479bbb353 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/GetChangeFeedProcessorBuilderWithAllVersionsAndDeletesTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/GetChangeFeedProcessorBuilderWithAllVersionsAndDeletesTests.cs @@ -6,11 +6,14 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests.ChangeFeed { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos.ChangeFeed.Utils; + using Microsoft.Azure.Cosmos.Services.Management.Tests; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Newtonsoft.Json; using Newtonsoft.Json.Linq; [TestClass] @@ -29,6 +32,115 @@ public async Task Cleanup() await base.TestCleanup(); } + [TestMethod] + [Timeout(300000)] + [TestCategory("LongRunning")] + [Owner("philipthomas-MSFT")] + [Description("Scenario: When a document is created with ttl set, there should be 1 create and 1 delete that will appear for that " + + "document when using ChangeFeedProcessor with AllVersionsAndDeletes set as the ChangeFeedMode.")] + public async Task WhenADocumentIsCreatedWithTtlSetThenTheDocumentIsDeletedTestsAsync() + { + ContainerInternal monitoredContainer = await this.CreateMonitoredContainer(ChangeFeedMode.AllVersionsAndDeletes); + Exception exception = default; + int ttlInSeconds = 5; + Stopwatch stopwatch = new(); + ManualResetEvent allDocsProcessed = new ManualResetEvent(false); + + ChangeFeedProcessor processor = monitoredContainer + .GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes(processorName: "processor", onChangesDelegate: (ChangeFeedProcessorContext context, IReadOnlyCollection> docs, CancellationToken token) => + { + // NOTE(philipthomas-MSFT): Please allow these Logger.LogLine because TTL on items will purge at random times so I am using this to test when ran locally using emulator. + + Logger.LogLine($"@ {DateTime.Now}, {nameof(stopwatch)} -> CFP AVAD took '{stopwatch.ElapsedMilliseconds}' to read document CRUD in feed."); + Logger.LogLine($"@ {DateTime.Now}, {nameof(docs)} -> {JsonConvert.SerializeObject(docs)}"); + + foreach (ChangeFeedItem change in docs) + { + if (change.Metadata.OperationType == ChangeFeedOperationType.Create) + { + // current + Assert.AreEqual(expected: "1", actual: change.Current.id.ToString()); + Assert.AreEqual(expected: "1", actual: change.Current.pk.ToString()); + Assert.AreEqual(expected: "Testing TTL on CFP.", actual: change.Current.description.ToString()); + Assert.AreEqual(expected: ttlInSeconds, actual: change.Current.ttl.ToObject()); + + // metadata + Assert.IsTrue(DateTime.TryParse(s: change.Metadata.ConflictResolutionTimestamp.ToString(), out _), message: "Invalid csrt must be a datetime value."); + Assert.IsTrue(change.Metadata.Lsn > 0, message: "Invalid lsn must be a long value."); + Assert.IsFalse(change.Metadata.IsTimeToLiveExpired); + + // previous + Assert.IsNull(change.Previous); + } + else if (change.Metadata.OperationType == ChangeFeedOperationType.Delete) + { + // current + Assert.IsNull(change.Current.id); + + // metadata + Assert.IsTrue(DateTime.TryParse(s: change.Metadata.ConflictResolutionTimestamp.ToString(), out _), message: "Invalid csrt must be a datetime value."); + Assert.IsTrue(change.Metadata.Lsn > 0, message: "Invalid lsn must be a long value."); + Assert.IsTrue(change.Metadata.IsTimeToLiveExpired); + + // previous + Assert.AreEqual(expected: "1", actual: change.Previous.id.ToString()); + Assert.AreEqual(expected: "1", actual: change.Previous.pk.ToString()); + Assert.AreEqual(expected: "Testing TTL on CFP.", actual: change.Previous.description.ToString()); + Assert.AreEqual(expected: ttlInSeconds, actual: change.Previous.ttl.ToObject()); + + // stop after reading delete since it is the last document in feed. + stopwatch.Stop(); + allDocsProcessed.Set(); + } + else + { + Assert.Fail("Invalid operation."); + } + } + + return Task.CompletedTask; + }) + .WithInstanceName(Guid.NewGuid().ToString()) + .WithLeaseContainer(this.LeaseContainer) + .WithErrorNotification((leaseToken, error) => + { + exception = error.InnerException; + + return Task.CompletedTask; + }) + .Build(); + + stopwatch.Start(); + + // NOTE(philipthomas-MSFT): Please allow these Logger.LogLine because TTL on items will purge at random times so I am using this to test when ran locally using emulator. + + Logger.LogLine($"@ {DateTime.Now}, CFProcessor starting..."); + + await processor.StartAsync(); + + try + { + await Task.Delay(GetChangeFeedProcessorBuilderWithAllVersionsAndDeletesTests.ChangeFeedSetupTime); + await monitoredContainer.CreateItemAsync(new { id = "1", pk = "1", description = "Testing TTL on CFP.", ttl = ttlInSeconds }, partitionKey: new PartitionKey("1")); + + // NOTE(philipthomas-MSFT): Please allow these Logger.LogLine because TTL on items will purge at random times so I am using this to test when ran locally using emulator. + + Logger.LogLine($"@ {DateTime.Now}, Document created."); + + bool receivedDelete = allDocsProcessed.WaitOne(250000); + Assert.IsTrue(receivedDelete, "Timed out waiting for docs to process"); + + if (exception != default) + { + Assert.Fail(exception.ToString()); + } + } + finally + { + await processor.StopAsync(); + } + } + [TestMethod] [Owner("philipthomas-MSFT")] [Description("Scenario: When a document is created, then updated, and finally deleted, there should be 3 changes that will appear for that " + @@ -467,6 +579,7 @@ private async Task CreateMonitoredContainer(ChangeFeedMode ch if (changeFeedMode == ChangeFeedMode.AllVersionsAndDeletes) { properties.ChangeFeedPolicy.FullFidelityRetention = TimeSpan.FromMinutes(5); + properties.DefaultTimeToLive = -1; } ContainerResponse response = await this.database.CreateContainerAsync(properties, diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json index b43ef3d0e7..4a4b7b08dc 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json @@ -89,7 +89,7 @@ "Attributes": [ "JsonPropertyAttribute" ], - "MethodInfo": "Boolean IsTimeToLiveExpired;CanRead:True;CanWrite:False;Boolean get_IsTimeToLiveExpired();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + "MethodInfo": "Boolean IsTimeToLiveExpired;CanRead:True;CanWrite:True;Boolean get_IsTimeToLiveExpired();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, "Int64 get_Lsn()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", @@ -110,14 +110,14 @@ "Attributes": [ "JsonPropertyAttribute" ], - "MethodInfo": "Int64 Lsn;CanRead:True;CanWrite:False;Int64 get_Lsn();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + "MethodInfo": "Int64 Lsn;CanRead:True;CanWrite:True;Int64 get_Lsn();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, "Int64 PreviousLsn[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = 1, PropertyName = \"previousImageLSN\")]": { "Type": "Property", "Attributes": [ "JsonPropertyAttribute" ], - "MethodInfo": "Int64 PreviousLsn;CanRead:True;CanWrite:False;Int64 get_PreviousLsn();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + "MethodInfo": "Int64 PreviousLsn;CanRead:True;CanWrite:True;Int64 get_PreviousLsn();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, "Microsoft.Azure.Cosmos.ChangeFeedOperationType get_OperationType()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", @@ -132,7 +132,7 @@ "JsonConverterAttribute", "JsonPropertyAttribute" ], - "MethodInfo": "Microsoft.Azure.Cosmos.ChangeFeedOperationType OperationType;CanRead:True;CanWrite:False;Microsoft.Azure.Cosmos.ChangeFeedOperationType get_OperationType();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + "MethodInfo": "Microsoft.Azure.Cosmos.ChangeFeedOperationType OperationType;CanRead:True;CanWrite:True;Microsoft.Azure.Cosmos.ChangeFeedOperationType get_OperationType();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, "System.DateTime ConflictResolutionTimestamp[Newtonsoft.Json.JsonPropertyAttribute(NullValueHandling = 1, PropertyName = \"crts\")]-[Newtonsoft.Json.JsonConverterAttribute(typeof(Microsoft.Azure.Documents.UnixDateTimeConverter))]": { "Type": "Property", @@ -140,7 +140,7 @@ "JsonConverterAttribute", "JsonPropertyAttribute" ], - "MethodInfo": "System.DateTime ConflictResolutionTimestamp;CanRead:True;CanWrite:False;System.DateTime get_ConflictResolutionTimestamp();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + "MethodInfo": "System.DateTime ConflictResolutionTimestamp;CanRead:True;CanWrite:True;System.DateTime get_ConflictResolutionTimestamp();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, "System.DateTime get_ConflictResolutionTimestamp()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", @@ -149,10 +149,10 @@ ], "MethodInfo": "System.DateTime get_ConflictResolutionTimestamp();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Void .ctor(System.DateTime, Int64, Microsoft.Azure.Cosmos.ChangeFeedOperationType, Int64)": { + "Void .ctor()": { "Type": "Constructor", "Attributes": [], - "MethodInfo": "[Void .ctor(System.DateTime, Int64, Microsoft.Azure.Cosmos.ChangeFeedOperationType, Int64), Void .ctor(System.DateTime, Int64, Microsoft.Azure.Cosmos.ChangeFeedOperationType, Int64)]" + "MethodInfo": "[Void .ctor(), Void .ctor()]" } }, "NestedTypes": {}