-
Notifications
You must be signed in to change notification settings - Fork 982
[Cosmos] add container cache and pk range cache #26723
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
30 commits
Select commit
Hold shift + click to select a range
91e31b4
add container cache and pk range cache
simorenoh 05fc18d
deadlock fix
simorenoh 949725f
refresh with replaceContainer too, validate refreshes
simorenoh fd37c6e
fix err handling
simorenoh 40b17d2
fmt
simorenoh 92b1f6e
fix v2 hashing logic
simorenoh d5eb230
internal
simorenoh a4d8e22
epk routing for HPK, dual-indexing container cache
simorenoh b83d889
go fmt
simorenoh fc8a2ed
make rid based, change undefined pk behavior
simorenoh 3acd3c0
Merge branch 'main' into container-client-caches
simorenoh 9fd339a
Update cosmos_container_properties_cache.go
simorenoh 431f6a5
Merge branch 'container-client-caches' of https://github.com/simoreno…
simorenoh 5065a2a
rename tests
simorenoh b3fe80c
Address PR review comments: fix deadlock, test names, EPK comparison
simorenoh 7c127fa
fixes from reviews
simorenoh d590c9b
more fixes from copilot review
simorenoh a3a60c2
add test to verify cache is always used
simorenoh 3cd3dc8
Update cosmos_container.go
simorenoh bdf2f7b
directly add cache to unit tests
simorenoh 7a3270d
invalidate pk range cache on container recreate
simorenoh 0896a80
Update cosmos_container_properties_cache.go
simorenoh b3cdc59
add full refresh tests
simorenoh 949ae66
410 tests
simorenoh 6ab4550
fix: resolve pkDef inside retry loop and add nil guard for container …
simorenoh 10f2050
share caches among clients to same endpoint
simorenoh 3e5cc47
additional tests
simorenoh ce96d8e
fix race conditions
simorenoh 2e3e701
add normalization
simorenoh c2309d2
Update CHANGELOG.md
simorenoh 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| package azcosmos | ||
|
|
||
| import ( | ||
| "sort" | ||
|
|
||
| "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos/internal/epk" | ||
| ) | ||
|
|
||
| // collectionRoutingMap holds an immutable snapshot of partition key ranges for a | ||
| // container, sorted for efficient EPK lookups. It supports incremental merging | ||
| // when partition splits or merges occur. | ||
| type collectionRoutingMap struct { | ||
| // orderedRanges are the partition key ranges sorted by MinInclusive ascending. | ||
| orderedRanges []partitionKeyRange | ||
| // rangeByID provides O(1) lookups of ranges by their ID. | ||
| rangeByID map[string]partitionKeyRange | ||
| // goneRanges tracks parent range IDs that have been replaced by children after splits. | ||
| goneRanges map[string]bool | ||
| // changeFeedETag is the ETag for incremental change-feed refreshes. | ||
| changeFeedETag string | ||
| } | ||
|
|
||
| // newCollectionRoutingMap creates a new collectionRoutingMap from a set of ranges. | ||
| // It filters out "gone" parent ranges (identified via the Parents field on child ranges) | ||
| // and sorts the remaining ranges by MinInclusive. | ||
| func newCollectionRoutingMap(ranges []partitionKeyRange, changeFeedETag string) *collectionRoutingMap { | ||
| goneRanges := make(map[string]bool) | ||
| for _, r := range ranges { | ||
| for _, parent := range r.Parents { | ||
| goneRanges[parent] = true | ||
| } | ||
| } | ||
|
|
||
| // Filter out gone ranges | ||
| filtered := make([]partitionKeyRange, 0, len(ranges)) | ||
| for _, r := range ranges { | ||
| if !goneRanges[r.ID] { | ||
| filtered = append(filtered, r) | ||
| } | ||
| } | ||
|
|
||
| // Sort by MinInclusive using length-aware comparison for HPK boundaries | ||
| sort.Slice(filtered, func(i, j int) bool { | ||
| return epk.CompareEPK(filtered[i].MinInclusive, filtered[j].MinInclusive) < 0 | ||
| }) | ||
|
|
||
| rangeByID := make(map[string]partitionKeyRange, len(filtered)) | ||
| for _, r := range filtered { | ||
| rangeByID[r.ID] = r | ||
| } | ||
|
|
||
| return &collectionRoutingMap{ | ||
| orderedRanges: filtered, | ||
| rangeByID: rangeByID, | ||
| goneRanges: goneRanges, | ||
| changeFeedETag: changeFeedETag, | ||
| } | ||
| } | ||
|
|
||
| // tryCombine merges new ranges (from an incremental change-feed refresh) into | ||
| // the existing routing map. Returns a new collectionRoutingMap if the merge | ||
| // succeeds (produces a complete covering), or nil if the result is incomplete | ||
| // (indicating a full refresh is needed). | ||
| func (crm *collectionRoutingMap) tryCombine(newRanges []partitionKeyRange, newETag string) *collectionRoutingMap { | ||
| // Accumulate gone ranges from both existing and new ranges | ||
| combinedGone := make(map[string]bool, len(crm.goneRanges)) | ||
| for id := range crm.goneRanges { | ||
| combinedGone[id] = true | ||
| } | ||
| for _, r := range newRanges { | ||
| for _, parent := range r.Parents { | ||
| combinedGone[parent] = true | ||
| } | ||
| } | ||
|
|
||
| // Build a combined set: existing ranges (minus gone) plus new ranges (minus gone) | ||
| combinedByID := make(map[string]partitionKeyRange, len(crm.rangeByID)+len(newRanges)) | ||
| for id, r := range crm.rangeByID { | ||
| if !combinedGone[id] { | ||
| combinedByID[id] = r | ||
| } | ||
| } | ||
| for _, r := range newRanges { | ||
| if !combinedGone[r.ID] { | ||
| combinedByID[r.ID] = r | ||
| } | ||
| } | ||
|
|
||
| // Build sorted slice | ||
| combined := make([]partitionKeyRange, 0, len(combinedByID)) | ||
| for _, r := range combinedByID { | ||
| combined = append(combined, r) | ||
| } | ||
| sort.Slice(combined, func(i, j int) bool { | ||
| return epk.CompareEPK(combined[i].MinInclusive, combined[j].MinInclusive) < 0 | ||
| }) | ||
|
|
||
| // Validate completeness: ranges must form a contiguous covering | ||
| if !isCompleteSetOfRanges(combined) { | ||
| return nil | ||
| } | ||
|
|
||
| return &collectionRoutingMap{ | ||
| orderedRanges: combined, | ||
| rangeByID: combinedByID, | ||
| goneRanges: combinedGone, | ||
| changeFeedETag: newETag, | ||
| } | ||
| } | ||
|
|
||
| // isGone returns true if the given range ID has been replaced (by a split/merge). | ||
| func (crm *collectionRoutingMap) isGone(rangeID string) bool { | ||
| return crm.goneRanges[rangeID] | ||
| } | ||
|
|
||
| // getOverlappingRanges returns all partition key ranges that overlap with the | ||
| // given EPK range [minInclusive, maxExclusive). Uses binary search for O(log n) | ||
| // lookups. The ranges must be sorted and contiguous (guaranteed by construction). | ||
| func (crm *collectionRoutingMap) getOverlappingRanges(minInclusive, maxExclusive string) []partitionKeyRange { | ||
| if len(crm.orderedRanges) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| // Start: rightmost range whose MinInclusive <= minInclusive. | ||
| // Same logic as findPhysicalRangeForEPK. | ||
| startIdx := sort.Search(len(crm.orderedRanges), func(i int) bool { | ||
| return epk.CompareEPK(crm.orderedRanges[i].MinInclusive, minInclusive) > 0 | ||
| }) - 1 | ||
| if startIdx < 0 { | ||
| startIdx = 0 | ||
| } | ||
|
|
||
| // End: first range whose MinInclusive >= maxExclusive. | ||
| // All ranges from startIdx up to (but not including) endIdx overlap. | ||
| endIdx := startIdx + sort.Search(len(crm.orderedRanges)-startIdx, func(i int) bool { | ||
| return epk.CompareEPK(crm.orderedRanges[startIdx+i].MinInclusive, maxExclusive) >= 0 | ||
| }) | ||
|
|
||
| if endIdx <= startIdx { | ||
| // At minimum, include the range containing minInclusive | ||
| endIdx = startIdx + 1 | ||
| } | ||
| if endIdx > len(crm.orderedRanges) { | ||
| endIdx = len(crm.orderedRanges) | ||
| } | ||
|
|
||
| result := make([]partitionKeyRange, endIdx-startIdx) | ||
| copy(result, crm.orderedRanges[startIdx:endIdx]) | ||
| return result | ||
| } | ||
|
|
||
| // isCompleteSetOfRanges validates that the sorted ranges form a contiguous | ||
| // partition covering with no gaps or overlaps. The first range should start | ||
| // at "" and each subsequent range should start where the previous one ends. | ||
| func isCompleteSetOfRanges(ranges []partitionKeyRange) bool { | ||
| if len(ranges) == 0 { | ||
| return false | ||
| } | ||
|
|
||
| // First range must start at "" | ||
| if ranges[0].MinInclusive != "" { | ||
| return false | ||
| } | ||
|
|
||
| // Each range's MinInclusive must equal the previous range's MaxExclusive. | ||
| // Use CompareEPK for length-aware comparison — HPK containers can return | ||
| // mixed-length boundaries that are semantically equal (zero-padded). | ||
| for i := 1; i < len(ranges); i++ { | ||
| if epk.CompareEPK(ranges[i].MinInclusive, ranges[i-1].MaxExclusive) != 0 { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| // Last range must end at "FF" (the maximum EPK boundary) or be unbounded ("") | ||
| lastMax := ranges[len(ranges)-1].MaxExclusive | ||
| if lastMax != "FF" && lastMax != "" { | ||
| return false | ||
| } | ||
|
|
||
| return true | ||
| } |
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.