-
Notifications
You must be signed in to change notification settings - Fork 536
Stand By Feed and ChangeFeed pull model #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
d479a9e
Adding initial files
ealsur a2ad3e9
Using Etag for continuation
ealsur f75b398
Removing unused
ealsur 8276100
Refactoring to reduce variables
ealsur 4609093
Refactoring to use CompositeToken
ealsur c023cb7
Adding feed test
ealsur aa9aa66
Refactor through Options
ealsur d55e7c0
Adding public methods and comments
ealsur 9fb1f9e
Routing through the point transport handler
ealsur 79a6657
Moving to outer if
ealsur 528e9ce
Adding split logic
ealsur 1f49d7d
Adding unit tests
ealsur b33ae42
Adding logic to detect invalid continuation tokens
ealsur d040a3d
Adding JSON validation
ealsur d81aede
Routing based on PKRangeId
ealsur 1855c9a
Renaming and adding more tests
ealsur e0e265f
Moving logic into the token
ealsur ba394c8
Forcing refresh on split
ealsur e10b32c
Addressing final coments
ealsur f3b6bbe
Addressing feedback
ealsur 8aaac26
Added test to cover CT passing
ealsur 45f635c
Refactoring and adding pkrangedelegate
ealsur a0413ba
Argument checks
ealsur 15b1164
Moving contract to CosmosRequestMessage
ealsur 1cff0bd
Refactoring make EnsureInitialized async
ealsur 274f5bc
Moving tests to a new file
ealsur bebc2fe
Adding PKrange assert
ealsur 468ee11
Refactored back to parameters outside Options
ealsur a725365
UT split
ealsur bcb85f1
Adding Start* checks
ealsur 14934d6
Adding new tests and renames
ealsur 95da24c
Addressing comments
ealsur 221bcf7
Refactoring for cache tests
ealsur 1a753b5
Adding comments to tests
ealsur f14f826
Adding factory method
ealsur 39cd566
Merge branch 'master' into users/ealsur/cfpull
kirankumarkolli 62f2037
Addressing comments
ealsur cbc99fa
Refactoring PKRange outside options
ealsur a80f4cc
Merge branch 'users/ealsur/cfpull' of https://github.com/Azure/azure-…
ealsur 854e8ba
Addressing comments
ealsur 5100279
Removing StartFromBeginning
ealsur 3bb1e17
Removing extra lines
ealsur da7432a
Removing unnecessary ToList
ealsur File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
184 changes: 184 additions & 0 deletions
184
Microsoft.Azure.Cosmos/src/Query/StandByFeedContinuationToken.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| //------------------------------------------------------------ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| //------------------------------------------------------------ | ||
|
|
||
| namespace Microsoft.Azure.Cosmos.Query | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since change feed is no longer part of the query pipeline maybe we should move it to a new namespace. |
||
| { | ||
| using System.Collections.Generic; | ||
| using System; | ||
| using System.Linq; | ||
| using Newtonsoft.Json; | ||
| using System.Threading.Tasks; | ||
| using System.Diagnostics; | ||
|
|
||
| /// <summary> | ||
| /// Stand by continuation token representing a contiguous read over all the ranges with continuation state across all ranges. | ||
|
ealsur marked this conversation as resolved.
|
||
| /// </summary> | ||
| /// <remarks> | ||
| /// The StandByFeed token represents the state of continuation tokens across all Partition Key Ranges and can be used to sequentially read the Change Feed for each range while maintaining a global state by serializing the values (and allowing deserialization). | ||
| /// </remarks> | ||
| internal class StandByFeedContinuationToken | ||
|
ealsur marked this conversation as resolved.
|
||
| { | ||
| internal delegate Task<IReadOnlyList<Documents.PartitionKeyRange>> PartitionKeyRangeCacheDelegate(string containerRid, Documents.Routing.Range<string> ranges, bool forceRefresh); | ||
|
|
||
| private readonly string containerRid; | ||
| private readonly PartitionKeyRangeCacheDelegate pkRangeCacheDelegate; | ||
| private readonly string inputContinuationToken; | ||
|
|
||
| private Queue<CompositeContinuationToken> compositeContinuationTokens; | ||
|
ealsur marked this conversation as resolved.
|
||
| private CompositeContinuationToken currentToken; | ||
|
|
||
| public static async Task<StandByFeedContinuationToken> CreateAsync( | ||
| string containerRid, | ||
| string initialStandByFeedContinuationToken, | ||
| PartitionKeyRangeCacheDelegate pkRangeCacheDelegate) | ||
| { | ||
| StandByFeedContinuationToken standByFeedContinuationToken = new StandByFeedContinuationToken(containerRid, initialStandByFeedContinuationToken, pkRangeCacheDelegate); | ||
| await standByFeedContinuationToken.EnsureInitializedAsync(); | ||
| return standByFeedContinuationToken; | ||
| } | ||
|
|
||
| private StandByFeedContinuationToken( | ||
| string containerRid, | ||
| string initialStandByFeedContinuationToken, | ||
| PartitionKeyRangeCacheDelegate pkRangeCacheDelegate) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(containerRid)) throw new ArgumentNullException(nameof(containerRid)); | ||
| if (pkRangeCacheDelegate == null) throw new ArgumentNullException(nameof(pkRangeCacheDelegate)); | ||
|
|
||
| this.containerRid = containerRid; | ||
| this.pkRangeCacheDelegate = pkRangeCacheDelegate; | ||
| this.inputContinuationToken = initialStandByFeedContinuationToken; | ||
| } | ||
|
|
||
| public async Task<Tuple<CompositeContinuationToken, string>> GetCurrentTokenAsync(bool forceRefresh = false) | ||
| { | ||
| Debug.Assert(this.compositeContinuationTokens != null); | ||
| IReadOnlyList<Documents.PartitionKeyRange> resolvedRanges = await this.TryGetOverlappingRangesAsync(this.currentToken.Range, forceRefresh: forceRefresh); | ||
| if (resolvedRanges.Count > 1) | ||
| { | ||
| this.HandleSplit(resolvedRanges); | ||
| } | ||
|
|
||
|
ealsur marked this conversation as resolved.
|
||
| return new Tuple<CompositeContinuationToken, string>(this.currentToken, resolvedRanges[0].Id); | ||
| } | ||
|
|
||
| public void MoveToNextToken() | ||
| { | ||
|
ealsur marked this conversation as resolved.
|
||
| CompositeContinuationToken recentToken = this.compositeContinuationTokens.Dequeue(); | ||
| this.compositeContinuationTokens.Enqueue(recentToken); | ||
| this.currentToken = this.compositeContinuationTokens.Peek(); | ||
| } | ||
|
|
||
| public new string ToString() | ||
| { | ||
| Debug.Assert(this.compositeContinuationTokens != null); | ||
| if (this.compositeContinuationTokens == null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| return JsonConvert.SerializeObject(this.compositeContinuationTokens); | ||
| } | ||
|
|
||
| private void HandleSplit(IReadOnlyList<Documents.PartitionKeyRange> keyRanges) | ||
| { | ||
| if (keyRanges == null) throw new ArgumentNullException(nameof(keyRanges)); | ||
|
|
||
| // Update current | ||
| Documents.PartitionKeyRange firstRange = keyRanges[0]; | ||
| this.currentToken.Range = new Documents.Routing.Range<string>(firstRange.MinInclusive, firstRange.MaxExclusive, true, false); | ||
| // Add children | ||
| foreach (Documents.PartitionKeyRange keyRange in keyRanges.Skip(1)) | ||
| { | ||
| this.compositeContinuationTokens.Enqueue(new CompositeContinuationToken() | ||
| { | ||
| Range = new Documents.Routing.Range<string>(keyRange.MinInclusive, keyRange.MaxExclusive, true, false), | ||
| Token = this.currentToken.Token | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| private async Task EnsureInitializedAsync() | ||
|
ealsur marked this conversation as resolved.
|
||
| { | ||
| if (this.compositeContinuationTokens == null) | ||
| { | ||
| IEnumerable<CompositeContinuationToken> tokens = await this.BuildCompositeTokensAsync(this.inputContinuationToken); | ||
|
|
||
| this.InitializeCompositeTokens(tokens); | ||
|
|
||
| Debug.Assert(this.compositeContinuationTokens.Count > 0); | ||
| } | ||
| } | ||
|
|
||
| private async Task<IEnumerable<CompositeContinuationToken>> BuildCompositeTokensAsync(string initialContinuationToken) | ||
| { | ||
| if (string.IsNullOrEmpty(initialContinuationToken)) | ||
| { | ||
| // Initialize composite token with all the ranges | ||
| IReadOnlyList<Documents.PartitionKeyRange> allRanges = await this.pkRangeCacheDelegate( | ||
| this.containerRid, | ||
| new Documents.Routing.Range<string>( | ||
| Documents.Routing.PartitionKeyInternal.MinimumInclusiveEffectivePartitionKey, | ||
| Documents.Routing.PartitionKeyInternal.MaximumExclusiveEffectivePartitionKey, | ||
| isMinInclusive: true, | ||
| isMaxInclusive: false), | ||
| false); | ||
|
|
||
| Debug.Assert(allRanges.Count != 0); | ||
| // Initial state for a scenario where user does not provide any initial continuation token. | ||
| // StartTime and StartFromBeginning can handle the logic if the user wants to start reading from any particular point in time | ||
| // After the first iteration, token will be updated with a recent value | ||
| return allRanges.Select(e => new CompositeContinuationToken() | ||
| { | ||
| Range = new Documents.Routing.Range<string>(e.MinInclusive, e.MaxExclusive, isMinInclusive: true, isMaxInclusive: false), | ||
| Token = null, | ||
|
ealsur marked this conversation as resolved.
|
||
| }); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| return JsonConvert.DeserializeObject<List<CompositeContinuationToken>>(initialContinuationToken); | ||
| } | ||
| catch(JsonReaderException ex) | ||
| { | ||
| throw new ArgumentOutOfRangeException($"Provided token has an invalid format: {initialContinuationToken}", ex); | ||
| } | ||
| } | ||
|
|
||
| private void InitializeCompositeTokens(IEnumerable<CompositeContinuationToken> tokens) | ||
|
ealsur marked this conversation as resolved.
|
||
| { | ||
| this.compositeContinuationTokens = new Queue<CompositeContinuationToken>(); | ||
|
|
||
| foreach (CompositeContinuationToken token in tokens) | ||
| { | ||
| this.compositeContinuationTokens.Enqueue(token); | ||
| } | ||
|
|
||
| this.currentToken = this.compositeContinuationTokens.Peek(); | ||
|
ealsur marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private async Task<IReadOnlyList<Documents.PartitionKeyRange>> TryGetOverlappingRangesAsync( | ||
| Documents.Routing.Range<string> targetRange, | ||
| bool forceRefresh = false) | ||
| { | ||
| Debug.Assert(targetRange != null); | ||
|
|
||
| IReadOnlyList<Documents.PartitionKeyRange> keyRanges = await this.pkRangeCacheDelegate( | ||
| this.containerRid, | ||
| new Documents.Routing.Range<string>( | ||
| targetRange.Min, | ||
| targetRange.Max, | ||
| isMaxInclusive: true, | ||
| isMinInclusive: false), | ||
| forceRefresh); | ||
|
|
||
| if (keyRanges.Count == 0) | ||
| { | ||
| throw new ArgumentOutOfRangeException("RequestContinuation", $"Token contains invalid range {targetRange.Min}-{targetRange.Max}"); | ||
| } | ||
|
|
||
| return keyRanges; | ||
| } | ||
| } | ||
| } | ||
82 changes: 82 additions & 0 deletions
82
Microsoft.Azure.Cosmos/src/RequestOptions/CosmosChangeFeedRequestOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| //------------------------------------------------------------ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| //------------------------------------------------------------ | ||
|
|
||
| namespace Microsoft.Azure.Cosmos | ||
| { | ||
| using System; | ||
| using System.Diagnostics; | ||
| using System.Globalization; | ||
| using Microsoft.Azure.Documents; | ||
|
|
||
| /// <summary> | ||
| /// The Cosmos Change Feed request options | ||
| /// </summary> | ||
| internal class CosmosChangeFeedRequestOptions : CosmosRequestOptions | ||
|
ealsur marked this conversation as resolved.
|
||
| { | ||
| internal const string IfNoneMatchAllHeaderValue = "*"; | ||
|
|
||
| /// <summary> | ||
| /// Specifies a particular point in time to start to read the change feed. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// In order to read the Change Feed from the beginning, set this to DateTime.MinValue. | ||
| /// </remarks> | ||
| public virtual DateTime? StartTime { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Fill the CosmosRequestMessage headers with the set properties | ||
| /// </summary> | ||
| /// <param name="request">The <see cref="CosmosRequestMessage"/></param> | ||
| public override void FillRequestOptions(CosmosRequestMessage request) | ||
| { | ||
| // Check if no Continuation Token is present | ||
| if (string.IsNullOrEmpty(request.Headers.IfNoneMatch)) | ||
|
ealsur marked this conversation as resolved.
kirankumarkolli marked this conversation as resolved.
|
||
| { | ||
| if (this.StartTime == null) | ||
| { | ||
| request.Headers.IfNoneMatch = CosmosChangeFeedRequestOptions.IfNoneMatchAllHeaderValue; | ||
| } | ||
| else if (this.StartTime != null) | ||
| { | ||
| request.Headers.Add(HttpConstants.HttpHeaders.IfModifiedSince, this.StartTime.Value.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture)); | ||
| } | ||
| } | ||
|
|
||
| request.Headers.Add(HttpConstants.HttpHeaders.A_IM, HttpConstants.A_IMHeaderValues.IncrementalFeed); | ||
|
|
||
| base.FillRequestOptions(request); | ||
| } | ||
|
|
||
| internal static void FillPartitionKeyRangeId(CosmosRequestMessage request, string partitionKeyRangeId) | ||
| { | ||
| Debug.Assert(request != null); | ||
|
|
||
| if (!string.IsNullOrEmpty(partitionKeyRangeId)) | ||
| { | ||
| request.PartitionKeyRangeId = partitionKeyRangeId; | ||
| } | ||
| } | ||
|
|
||
| internal static void FillContinuationToken(CosmosRequestMessage request, string continuationToken) | ||
| { | ||
| Debug.Assert(request != null); | ||
|
kirankumarkolli marked this conversation as resolved.
|
||
|
|
||
| if (!string.IsNullOrWhiteSpace(continuationToken)) | ||
|
ealsur marked this conversation as resolved.
|
||
| { | ||
| // On REST level, change feed is using IfNoneMatch/ETag instead of continuation | ||
| request.Headers.IfNoneMatch = continuationToken; | ||
| } | ||
| } | ||
|
|
||
| internal static void FillMaxItemCount(CosmosRequestMessage request, int? maxItemCount) | ||
| { | ||
| Debug.Assert(request != null); | ||
|
|
||
| if (maxItemCount.HasValue) | ||
| { | ||
| request.Headers.Add(HttpConstants.HttpHeaders.PageSize, maxItemCount.Value.ToString(CultureInfo.InvariantCulture)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.