Browse Source
fix: S3 versioning memory leak in ListObjectVersions pagination (#7813)
fix: S3 versioning memory leak in ListObjectVersions pagination (#7813)
* fix: S3 versioning memory leak in ListObjectVersions pagination This commit fixes a memory leak issue in S3 versioning buckets where ListObjectVersions with pagination (key-marker set) would collect ALL versions in the bucket before filtering, causing O(N) memory usage. Root cause: - When keyMarker was set, maxCollect was set to 0 (unlimited) - This caused findVersionsRecursively to traverse the entire bucket - All versions were collected into memory, sorted, then filtered Fix: - Updated findVersionsRecursively to accept keyMarker and versionIdMarker - Skips objects/versions before the marker during recursion (not after) - Always respects maxCollect limit (never unlimited) - Memory usage is now O(maxKeys) instead of O(total versions) Refactoring: - Introduced versionCollector struct to encapsulate collection state - Extracted helper methods for cleaner, more testable code: - matchesPrefixFilter: prefix matching logic - shouldSkipObjectForMarker: keyMarker filtering - shouldSkipVersionForMarker: versionIdMarker filtering - processVersionsDirectory: .versions directory handling - processExplicitDirectory: S3 directory object handling - processRegularFile: pre-versioning file handling - collectVersions: main recursive collection loop - processDirectory: directory entry dispatch This reduces the high QPS on 'find' and 'prefixList' operations by skipping irrelevant objects during traversal. Fixes customer-reported memory leak with high find/prefixList QPS in Grafana for S3 versioning buckets. * s3: infer version ID format from ExtLatestVersionIdKey metadata Simplified version format detection: - Removed ExtVersionIdFormatKey - no longer needed - getVersionIdFormat() now infers format from ExtLatestVersionIdKey - Uses isNewFormatVersionId() to check if latest version uses inverted format This approach is simpler because: - ExtLatestVersionIdKey is already stored in .versions directory metadata - No need for separate format metadata field - Format is naturally determined by the existing version IDspull/7394/merge
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 887 additions and 341 deletions
-
8weed/s3api/filer_multipart.go
-
4weed/s3api/s3api_object_handlers_copy.go
-
22weed/s3api/s3api_object_handlers_put.go
-
530weed/s3api/s3api_object_versioning.go
-
187weed/s3api/s3api_version_id.go
-
375weed/s3api/s3api_version_id_test.go
@ -0,0 +1,187 @@ |
|||
package s3api |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"encoding/hex" |
|||
"fmt" |
|||
"math" |
|||
"strconv" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
s3_constants "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
|||
) |
|||
|
|||
// Version ID format constants
|
|||
// New format uses inverted timestamps so newer versions sort first lexicographically
|
|||
// Old format used raw timestamps where older versions sorted first
|
|||
const ( |
|||
// Threshold to distinguish old vs new format version IDs
|
|||
// Around year 2024-2025:
|
|||
// - Old format (raw ns): ~1.7×10¹⁸ ≈ 0x17... (BELOW threshold)
|
|||
// - New format (MaxInt64 - ns): ~7.5×10¹⁸ ≈ 0x68... (ABOVE threshold)
|
|||
// We use 0x4000000000000000 (~4.6×10¹⁸) as threshold
|
|||
versionIdFormatThreshold = 0x4000000000000000 |
|||
) |
|||
|
|||
// generateVersionId creates a unique version ID
|
|||
// If useInvertedFormat is true, uses inverted timestamps so newer versions sort first
|
|||
// If false, uses raw timestamps (old format) for backward compatibility
|
|||
func generateVersionId(useInvertedFormat bool) string { |
|||
now := time.Now().UnixNano() |
|||
var timestampHex string |
|||
|
|||
if useInvertedFormat { |
|||
// INVERTED timestamp: newer versions have SMALLER values
|
|||
// This makes lexicographic sorting return newest versions first
|
|||
invertedTimestamp := math.MaxInt64 - now |
|||
timestampHex = fmt.Sprintf("%016x", invertedTimestamp) |
|||
} else { |
|||
// Raw timestamp: older versions have SMALLER values (old format)
|
|||
timestampHex = fmt.Sprintf("%016x", now) |
|||
} |
|||
|
|||
// Generate random 8 bytes for uniqueness (last 16 chars of version ID)
|
|||
randBytes := make([]byte, 8) |
|||
if _, err := rand.Read(randBytes); err != nil { |
|||
glog.Errorf("Failed to generate random bytes for version ID: %v", err) |
|||
// Fallback to timestamp-only if random generation fails
|
|||
return timestampHex + "0000000000000000" |
|||
} |
|||
|
|||
// Combine timestamp (16 chars) + random (16 chars) = 32 chars total
|
|||
randomHex := hex.EncodeToString(randBytes) |
|||
return timestampHex + randomHex |
|||
} |
|||
|
|||
// isNewFormatVersionId returns true if the version ID uses the new inverted timestamp format
|
|||
func isNewFormatVersionId(versionId string) bool { |
|||
if len(versionId) < 16 || versionId == "null" { |
|||
return false |
|||
} |
|||
// Parse the first 16 hex chars as the timestamp portion
|
|||
timestampPart, err := strconv.ParseUint(versionId[:16], 16, 64) |
|||
if err != nil { |
|||
return false |
|||
} |
|||
// New format has inverted timestamps (MaxInt64 - ns), which are ABOVE the threshold (~0x68...)
|
|||
// Old format has raw timestamps, which are BELOW the threshold (~0x17...)
|
|||
return timestampPart > versionIdFormatThreshold |
|||
} |
|||
|
|||
// getVersionTimestamp extracts the actual timestamp from a version ID,
|
|||
// handling both old (raw) and new (inverted) formats
|
|||
func getVersionTimestamp(versionId string) int64 { |
|||
if len(versionId) < 16 || versionId == "null" { |
|||
return 0 |
|||
} |
|||
timestampPart, err := strconv.ParseUint(versionId[:16], 16, 64) |
|||
if err != nil { |
|||
return 0 |
|||
} |
|||
if timestampPart > versionIdFormatThreshold { |
|||
// New format: inverted timestamp (above threshold), convert back
|
|||
return int64(math.MaxInt64 - timestampPart) |
|||
} |
|||
// Validate old format timestamp is within int64 range
|
|||
if timestampPart > math.MaxInt64 { |
|||
return 0 |
|||
} |
|||
// Old format: raw timestamp (below threshold)
|
|||
return int64(timestampPart) |
|||
} |
|||
|
|||
// compareVersionIds compares two version IDs for sorting (newest first)
|
|||
// Returns: negative if a is newer, positive if b is newer, 0 if equal
|
|||
// Handles both old and new format version IDs
|
|||
func compareVersionIds(a, b string) int { |
|||
if a == b { |
|||
return 0 |
|||
} |
|||
if a == "null" { |
|||
return 1 // null versions sort last
|
|||
} |
|||
if b == "null" { |
|||
return -1 |
|||
} |
|||
|
|||
aIsNew := isNewFormatVersionId(a) |
|||
bIsNew := isNewFormatVersionId(b) |
|||
|
|||
if aIsNew == bIsNew { |
|||
// Same format - compare lexicographically
|
|||
// For new format: smaller value = newer (correct)
|
|||
// For old format: smaller value = older (need to invert)
|
|||
if aIsNew { |
|||
// New format: lexicographic order is correct (smaller = newer)
|
|||
if a < b { |
|||
return -1 |
|||
} |
|||
return 1 |
|||
} else { |
|||
// Old format: lexicographic order is inverted (smaller = older)
|
|||
if a < b { |
|||
return 1 |
|||
} |
|||
return -1 |
|||
} |
|||
} |
|||
|
|||
// Mixed formats - compare by actual timestamp
|
|||
aTime := getVersionTimestamp(a) |
|||
bTime := getVersionTimestamp(b) |
|||
if aTime > bTime { |
|||
return -1 // a is newer
|
|||
} |
|||
if aTime < bTime { |
|||
return 1 // b is newer
|
|||
} |
|||
return 0 |
|||
} |
|||
|
|||
// getVersionedObjectDir returns the directory path for storing object versions
|
|||
func (s3a *S3ApiServer) getVersionedObjectDir(bucket, object string) string { |
|||
return s3a.option.BucketsPath + "/" + bucket + "/" + object + s3_constants.VersionsFolder |
|||
} |
|||
|
|||
// getVersionFileName returns the filename for a specific version
|
|||
func (s3a *S3ApiServer) getVersionFileName(versionId string) string { |
|||
return fmt.Sprintf("v_%s", versionId) |
|||
} |
|||
|
|||
// getVersionIdFormat checks the .versions directory to determine which version ID format to use.
|
|||
// Returns true if inverted format (new format) should be used.
|
|||
// For new .versions directories, returns true (use new format).
|
|||
// For existing directories, infers format from the latest version ID.
|
|||
func (s3a *S3ApiServer) getVersionIdFormat(bucket, object string) bool { |
|||
cleanObject := strings.TrimPrefix(object, "/") |
|||
bucketDir := s3a.option.BucketsPath + "/" + bucket |
|||
versionsPath := cleanObject + s3_constants.VersionsFolder |
|||
|
|||
// Try to get the .versions directory entry
|
|||
versionsEntry, err := s3a.getEntry(bucketDir, versionsPath) |
|||
if err != nil { |
|||
// .versions directory doesn't exist yet - use new format
|
|||
return true |
|||
} |
|||
|
|||
// Infer format from the latest version ID stored in metadata
|
|||
if versionsEntry.Extended != nil { |
|||
if latestVersionId, exists := versionsEntry.Extended[s3_constants.ExtLatestVersionIdKey]; exists { |
|||
return isNewFormatVersionId(string(latestVersionId)) |
|||
} |
|||
} |
|||
|
|||
// No latest version metadata - this is likely a new or empty directory
|
|||
// Use new format
|
|||
return true |
|||
} |
|||
|
|||
// generateVersionIdForObject generates a version ID using the appropriate format for the object.
|
|||
// For new objects, uses inverted format. For existing versioned objects, uses their existing format.
|
|||
func (s3a *S3ApiServer) generateVersionIdForObject(bucket, object string) string { |
|||
useInvertedFormat := s3a.getVersionIdFormat(bucket, object) |
|||
return generateVersionId(useInvertedFormat) |
|||
} |
|||
|
|||
@ -0,0 +1,375 @@ |
|||
package s3api |
|||
|
|||
import ( |
|||
"math" |
|||
"testing" |
|||
"time" |
|||
) |
|||
|
|||
// TestVersionIdFormatDetection tests that old and new format version IDs are correctly identified
|
|||
func TestVersionIdFormatDetection(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
versionId string |
|||
expectNew bool |
|||
}{ |
|||
// New format (inverted timestamps) - values > 0x4000000000000000
|
|||
{ |
|||
name: "new format - inverted timestamp", |
|||
versionId: "68a1b2c3d4e5f6780000000000000000", // > 0x4000...
|
|||
expectNew: true, |
|||
}, |
|||
{ |
|||
name: "new format - high value", |
|||
versionId: "7fffffffffffffff0000000000000000", // near max
|
|||
expectNew: true, |
|||
}, |
|||
// Old format (raw timestamps) - values < 0x4000000000000000
|
|||
{ |
|||
name: "old format - raw timestamp", |
|||
versionId: "179a1b2c3d4e5f670000000000000000", // ~2024-2025
|
|||
expectNew: false, |
|||
}, |
|||
{ |
|||
name: "old format - low value", |
|||
versionId: "10000000000000000000000000000000", |
|||
expectNew: false, |
|||
}, |
|||
// Edge cases
|
|||
{ |
|||
name: "null version", |
|||
versionId: "null", |
|||
expectNew: false, |
|||
}, |
|||
{ |
|||
name: "short version ID", |
|||
versionId: "abc123", |
|||
expectNew: false, |
|||
}, |
|||
{ |
|||
name: "empty version ID", |
|||
versionId: "", |
|||
expectNew: false, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
got := isNewFormatVersionId(tt.versionId) |
|||
if got != tt.expectNew { |
|||
t.Errorf("isNewFormatVersionId(%s) = %v, want %v", tt.versionId, got, tt.expectNew) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestGenerateVersionIdFormats tests that generateVersionId produces correct format based on parameter
|
|||
func TestGenerateVersionIdFormats(t *testing.T) { |
|||
// Generate old format version ID
|
|||
oldFormatId := generateVersionId(false) |
|||
if len(oldFormatId) != 32 { |
|||
t.Errorf("old format version ID length = %d, want 32", len(oldFormatId)) |
|||
} |
|||
if isNewFormatVersionId(oldFormatId) { |
|||
t.Errorf("generateVersionId(false) produced new format ID: %s", oldFormatId) |
|||
} |
|||
|
|||
// Generate new format version ID
|
|||
newFormatId := generateVersionId(true) |
|||
if len(newFormatId) != 32 { |
|||
t.Errorf("new format version ID length = %d, want 32", len(newFormatId)) |
|||
} |
|||
if !isNewFormatVersionId(newFormatId) { |
|||
t.Errorf("generateVersionId(true) produced old format ID: %s", newFormatId) |
|||
} |
|||
} |
|||
|
|||
// TestGetVersionTimestamp tests timestamp extraction from both formats
|
|||
func TestGetVersionTimestamp(t *testing.T) { |
|||
now := time.Now().UnixNano() |
|||
|
|||
// Generate old and new format IDs
|
|||
oldId := generateVersionId(false) |
|||
newId := generateVersionId(true) |
|||
|
|||
oldTs := getVersionTimestamp(oldId) |
|||
newTs := getVersionTimestamp(newId) |
|||
|
|||
// Both should be close to current time (within 1 second)
|
|||
tolerance := int64(time.Second) |
|||
|
|||
if abs(oldTs-now) > tolerance { |
|||
t.Errorf("old format timestamp diff too large: got %d, want ~%d", oldTs, now) |
|||
} |
|||
if abs(newTs-now) > tolerance { |
|||
t.Errorf("new format timestamp diff too large: got %d, want ~%d", newTs, now) |
|||
} |
|||
|
|||
// null should return 0
|
|||
if ts := getVersionTimestamp("null"); ts != 0 { |
|||
t.Errorf("getVersionTimestamp(null) = %d, want 0", ts) |
|||
} |
|||
} |
|||
|
|||
func abs(x int64) int64 { |
|||
if x < 0 { |
|||
return -x |
|||
} |
|||
return x |
|||
} |
|||
|
|||
// TestCompareVersionIdsSameFormatOld tests sorting of old format version IDs (newest first)
|
|||
func TestCompareVersionIdsSameFormatOld(t *testing.T) { |
|||
// Old format: larger hex value = newer (raw timestamp)
|
|||
older := "1700000000000000" + "0000000000000000" // older timestamp
|
|||
newer := "1800000000000000" + "0000000000000000" // newer timestamp
|
|||
|
|||
// Verify both are old format
|
|||
if isNewFormatVersionId(older) || isNewFormatVersionId(newer) { |
|||
t.Fatal("test setup error: expected old format IDs") |
|||
} |
|||
|
|||
// compareVersionIds should return negative if first arg is newer
|
|||
result := compareVersionIds(newer, older) |
|||
if result >= 0 { |
|||
t.Errorf("compareVersionIds(newer, older) = %d, want negative", result) |
|||
} |
|||
|
|||
result = compareVersionIds(older, newer) |
|||
if result <= 0 { |
|||
t.Errorf("compareVersionIds(older, newer) = %d, want positive", result) |
|||
} |
|||
|
|||
result = compareVersionIds(older, older) |
|||
if result != 0 { |
|||
t.Errorf("compareVersionIds(same, same) = %d, want 0", result) |
|||
} |
|||
} |
|||
|
|||
// TestCompareVersionIdsSameFormatNew tests sorting of new format version IDs (newest first)
|
|||
func TestCompareVersionIdsSameFormatNew(t *testing.T) { |
|||
// New format: smaller hex value = newer (inverted timestamp)
|
|||
// MaxInt64 - newer_ts < MaxInt64 - older_ts
|
|||
newer := "6800000000000000" + "0000000000000000" // smaller = newer
|
|||
older := "6900000000000000" + "0000000000000000" // larger = older
|
|||
|
|||
// Verify both are new format
|
|||
if !isNewFormatVersionId(older) || !isNewFormatVersionId(newer) { |
|||
t.Fatal("test setup error: expected new format IDs") |
|||
} |
|||
|
|||
// compareVersionIds should return negative if first arg is newer
|
|||
result := compareVersionIds(newer, older) |
|||
if result >= 0 { |
|||
t.Errorf("compareVersionIds(newer, older) = %d, want negative", result) |
|||
} |
|||
|
|||
result = compareVersionIds(older, newer) |
|||
if result <= 0 { |
|||
t.Errorf("compareVersionIds(older, newer) = %d, want positive", result) |
|||
} |
|||
} |
|||
|
|||
// TestCompareVersionIdsMixedFormats tests sorting when comparing old and new format IDs
|
|||
func TestCompareVersionIdsMixedFormats(t *testing.T) { |
|||
// Create IDs where we know the actual timestamps
|
|||
// Old format: raw timestamp
|
|||
oldFormatTs := int64(1700000000000000000) // some timestamp
|
|||
oldFormatId := createOldFormatVersionId(oldFormatTs) |
|||
|
|||
// New format: inverted timestamp (created 1 second later)
|
|||
newFormatTs := oldFormatTs + int64(time.Second) |
|||
newFormatId := createNewFormatVersionId(newFormatTs) |
|||
|
|||
// Verify formats
|
|||
if isNewFormatVersionId(oldFormatId) { |
|||
t.Fatalf("expected old format for %s", oldFormatId) |
|||
} |
|||
if !isNewFormatVersionId(newFormatId) { |
|||
t.Fatalf("expected new format for %s", newFormatId) |
|||
} |
|||
|
|||
// New format ID is newer (created 1 second later)
|
|||
result := compareVersionIds(newFormatId, oldFormatId) |
|||
if result >= 0 { |
|||
t.Errorf("compareVersionIds(newer_new_format, older_old_format) = %d, want negative", result) |
|||
} |
|||
|
|||
result = compareVersionIds(oldFormatId, newFormatId) |
|||
if result <= 0 { |
|||
t.Errorf("compareVersionIds(older_old_format, newer_new_format) = %d, want positive", result) |
|||
} |
|||
} |
|||
|
|||
// TestCompareVersionIdsNullHandling tests that null versions sort last
|
|||
func TestCompareVersionIdsNullHandling(t *testing.T) { |
|||
regular := generateVersionId(true) |
|||
|
|||
// null should sort after regular versions
|
|||
if result := compareVersionIds("null", regular); result <= 0 { |
|||
t.Errorf("compareVersionIds(null, regular) = %d, want positive (null sorts last)", result) |
|||
} |
|||
|
|||
if result := compareVersionIds(regular, "null"); result >= 0 { |
|||
t.Errorf("compareVersionIds(regular, null) = %d, want negative (null sorts last)", result) |
|||
} |
|||
} |
|||
|
|||
// Helper to create old format version ID from timestamp
|
|||
func createOldFormatVersionId(ts int64) string { |
|||
return sprintf16x(uint64(ts)) + "0000000000000000" |
|||
} |
|||
|
|||
// Helper to create new format version ID from timestamp
|
|||
func createNewFormatVersionId(ts int64) string { |
|||
inverted := uint64(math.MaxInt64 - ts) |
|||
return sprintf16x(inverted) + "0000000000000000" |
|||
} |
|||
|
|||
func sprintf16x(v uint64) string { |
|||
return sprintf("%016x", v) |
|||
} |
|||
|
|||
func sprintf(format string, v uint64) string { |
|||
result := make([]byte, 16) |
|||
for i := 15; i >= 0; i-- { |
|||
digit := v & 0xf |
|||
if digit < 10 { |
|||
result[i] = byte('0' + digit) |
|||
} else { |
|||
result[i] = byte('a' + digit - 10) |
|||
} |
|||
v >>= 4 |
|||
} |
|||
return string(result) |
|||
} |
|||
|
|||
// TestOldFormatBackwardCompatibility ensures old format versions work correctly in sorting
|
|||
func TestOldFormatBackwardCompatibility(t *testing.T) { |
|||
// Simulate a bucket that was created before the inverted format was introduced
|
|||
// All versions should be old format and should sort correctly (newest first)
|
|||
|
|||
// Create 5 old format version IDs with known timestamps
|
|||
baseTs := int64(1700000000000000000) |
|||
versions := make([]string, 5) |
|||
for i := 0; i < 5; i++ { |
|||
ts := baseTs + int64(i)*int64(time.Minute) // each 1 minute apart
|
|||
versions[i] = createOldFormatVersionId(ts) |
|||
} |
|||
|
|||
// Verify all are old format
|
|||
for i, v := range versions { |
|||
if isNewFormatVersionId(v) { |
|||
t.Fatalf("version %d should be old format: %s", i, v) |
|||
} |
|||
} |
|||
|
|||
// Verify sorting: versions[4] is newest, versions[0] is oldest
|
|||
// compareVersionIds(newer, older) should return negative
|
|||
for i := 0; i < 4; i++ { |
|||
newer := versions[i+1] |
|||
older := versions[i] |
|||
result := compareVersionIds(newer, older) |
|||
if result >= 0 { |
|||
t.Errorf("compareVersionIds(versions[%d], versions[%d]) = %d, want negative (newer first)", i+1, i, result) |
|||
} |
|||
} |
|||
|
|||
// Verify extracted timestamps are correct
|
|||
for i, v := range versions { |
|||
expectedTs := baseTs + int64(i)*int64(time.Minute) |
|||
gotTs := getVersionTimestamp(v) |
|||
if gotTs != expectedTs { |
|||
t.Errorf("getVersionTimestamp(versions[%d]) = %d, want %d", i, gotTs, expectedTs) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// TestNewFormatSorting ensures new format versions sort correctly (newest first)
|
|||
func TestNewFormatSorting(t *testing.T) { |
|||
// Create 5 new format version IDs with known timestamps
|
|||
baseTs := int64(1700000000000000000) |
|||
versions := make([]string, 5) |
|||
for i := 0; i < 5; i++ { |
|||
ts := baseTs + int64(i)*int64(time.Minute) // each 1 minute apart
|
|||
versions[i] = createNewFormatVersionId(ts) |
|||
} |
|||
|
|||
// Verify all are new format
|
|||
for i, v := range versions { |
|||
if !isNewFormatVersionId(v) { |
|||
t.Fatalf("version %d should be new format: %s", i, v) |
|||
} |
|||
} |
|||
|
|||
// Verify sorting: versions[4] is newest, versions[0] is oldest
|
|||
for i := 0; i < 4; i++ { |
|||
newer := versions[i+1] |
|||
older := versions[i] |
|||
result := compareVersionIds(newer, older) |
|||
if result >= 0 { |
|||
t.Errorf("compareVersionIds(versions[%d], versions[%d]) = %d, want negative (newer first)", i+1, i, result) |
|||
} |
|||
} |
|||
|
|||
// Verify extracted timestamps are correct
|
|||
for i, v := range versions { |
|||
expectedTs := baseTs + int64(i)*int64(time.Minute) |
|||
gotTs := getVersionTimestamp(v) |
|||
if gotTs != expectedTs { |
|||
t.Errorf("getVersionTimestamp(versions[%d]) = %d, want %d", i, gotTs, expectedTs) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// TestMixedFormatTransition simulates a bucket transitioning from old to new format
|
|||
func TestMixedFormatTransition(t *testing.T) { |
|||
baseTs := int64(1700000000000000000) |
|||
|
|||
// First 3 versions created with old format (before upgrade)
|
|||
oldVersions := make([]string, 3) |
|||
for i := 0; i < 3; i++ { |
|||
ts := baseTs + int64(i)*int64(time.Minute) |
|||
oldVersions[i] = createOldFormatVersionId(ts) |
|||
} |
|||
|
|||
// Next 3 versions created with new format (after upgrade)
|
|||
newVersions := make([]string, 3) |
|||
for i := 0; i < 3; i++ { |
|||
ts := baseTs + int64(3+i)*int64(time.Minute) // continue from where old left off
|
|||
newVersions[i] = createNewFormatVersionId(ts) |
|||
} |
|||
|
|||
// All versions in chronological order (oldest to newest)
|
|||
allVersions := append(oldVersions, newVersions...) |
|||
|
|||
// Verify mixed formats
|
|||
for i := 0; i < 3; i++ { |
|||
if isNewFormatVersionId(allVersions[i]) { |
|||
t.Errorf("allVersions[%d] should be old format", i) |
|||
} |
|||
} |
|||
for i := 3; i < 6; i++ { |
|||
if !isNewFormatVersionId(allVersions[i]) { |
|||
t.Errorf("allVersions[%d] should be new format", i) |
|||
} |
|||
} |
|||
|
|||
// Verify sorting works correctly across the format boundary
|
|||
for i := 0; i < 5; i++ { |
|||
newer := allVersions[i+1] |
|||
older := allVersions[i] |
|||
result := compareVersionIds(newer, older) |
|||
if result >= 0 { |
|||
t.Errorf("compareVersionIds(allVersions[%d], allVersions[%d]) = %d, want negative (newer first)", i+1, i, result) |
|||
} |
|||
} |
|||
|
|||
// Verify the newest (new format) version sorts before oldest (old format) when comparing directly
|
|||
newest := allVersions[5] // newest, new format
|
|||
oldest := allVersions[0] // oldest, old format
|
|||
if result := compareVersionIds(newest, oldest); result >= 0 { |
|||
t.Errorf("compareVersionIds(newest_new_format, oldest_old_format) = %d, want negative", result) |
|||
} |
|||
} |
|||
|
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue