From 3fb02b3d58932e64464c8fc74eff66e3bf5da820 Mon Sep 17 00:00:00 2001 From: dajiaohuang <108231307+dajiaohuang@users.noreply.github.com> Date: Fri, 2 Oct 2026 01:28:45 +0800 Subject: [PATCH 1/3] fix(database): serialize push ID generation --- FirebaseDatabase/CHANGELOG.md | 2 ++ .../Sources/Utilities/FNextPushId.m | 11 ++++++++++- FirebaseDatabase/Tests/Unit/FNextPushIdTest.m | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/FirebaseDatabase/CHANGELOG.md b/FirebaseDatabase/CHANGELOG.md index 7729d530a9b..1d817487de2 100644 --- a/FirebaseDatabase/CHANGELOG.md +++ b/FirebaseDatabase/CHANGELOG.md @@ -1,3 +1,5 @@ +- [fixed] Generate Realtime Database push IDs safely when called concurrently. + # 12.19.0 - [fixed] Reconnect after significant system clock changes so that `.info/serverTimeOffset` is refreshed. (#363) diff --git a/FirebaseDatabase/Sources/Utilities/FNextPushId.m b/FirebaseDatabase/Sources/Utilities/FNextPushId.m index bfe890fa369..a33c639ef76 100644 --- a/FirebaseDatabase/Sources/Utilities/FNextPushId.m +++ b/FirebaseDatabase/Sources/Utilities/FNextPushId.m @@ -37,6 +37,13 @@ @implementation FNextPushId + (NSString *)get:(NSTimeInterval)currentTime { static long long lastPushTime = 0; static int lastRandChars[12]; + static NSLock *generationLock; + static dispatch_once_t lockOnceToken; + dispatch_once(&lockOnceToken, ^{ + generationLock = [[NSLock alloc] init]; + }); + + [generationLock lock]; long long now = (long long)(currentTime * 1000); @@ -68,7 +75,9 @@ + (NSString *)get:(NSTimeInterval)currentTime { [id appendFormat:@"%C", [PUSH_CHARS characterAtIndex:lastRandChars[i]]]; } - return [NSString stringWithString:id]; + NSString *pushId = [NSString stringWithString:id]; + [generationLock unlock]; + return pushId; } + (NSString *)from:(NSString *)fn successor:(NSString *_Nonnull)key { diff --git a/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m b/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m index 70267651fe3..634b425dac8 100644 --- a/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m +++ b/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m @@ -15,6 +15,7 @@ */ #import +#import #import "FirebaseDatabase/Sources/Utilities/FNextPushId.h" #import "FirebaseDatabase/Sources/Utilities/FUtilities.h" @@ -49,6 +50,23 @@ - (void)testSuccessorSpecialValues { @"successor(MAX_PUSH_CHAR repeated MAX_KEY_LEN times) == MAX_NAME"); } +- (void)testConcurrentPushIdsAreUniqueForTheSameTimestamp { + const size_t pushIdCount = 1000; + NSMutableArray *pushIds = [NSMutableArray arrayWithCapacity:pushIdCount]; + NSLock *pushIdsLock = [[NSLock alloc] init]; + + dispatch_apply(pushIdCount, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), + ^(size_t index) { + (void)index; + NSString *pushId = [FNextPushId get:12345.0]; + [pushIdsLock lock]; + [pushIds addObject:pushId]; + [pushIdsLock unlock]; + }); + + XCTAssertEqual([NSSet setWithArray:pushIds].count, pushIdCount); +} + - (void)testSuccessorBasic { NSString *actual = [FNextPushId from:@"test" successor:@"abc"]; NSString *expected = [NSString stringWithFormat:@"abc%@", MIN_PUSH_CHAR]; From 3a09dd92ffe3943a5c68b84eeeaf10c8f75cb5cf Mon Sep 17 00:00:00 2001 From: dajiaohuang <108231307+dajiaohuang@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:34:02 +0800 Subject: [PATCH 2/3] fix(database): release push ID synchronization on exceptions --- FirebaseDatabase/CHANGELOG.md | 4 +- .../Sources/Utilities/FNextPushId.m | 57 +++++++++---------- FirebaseDatabase/Tests/Unit/FNextPushIdTest.m | 27 ++++++--- 3 files changed, 47 insertions(+), 41 deletions(-) diff --git a/FirebaseDatabase/CHANGELOG.md b/FirebaseDatabase/CHANGELOG.md index 1d817487de2..0f882d14a62 100644 --- a/FirebaseDatabase/CHANGELOG.md +++ b/FirebaseDatabase/CHANGELOG.md @@ -1,4 +1,6 @@ -- [fixed] Generate Realtime Database push IDs safely when called concurrently. +# Unreleased +- [fixed] Fixed a race where calling childByAutoId() concurrently from multiple + threads could generate duplicate push IDs. (#16768) # 12.19.0 - [fixed] Reconnect after significant system clock changes so that diff --git a/FirebaseDatabase/Sources/Utilities/FNextPushId.m b/FirebaseDatabase/Sources/Utilities/FNextPushId.m index a33c639ef76..643476527ea 100644 --- a/FirebaseDatabase/Sources/Utilities/FNextPushId.m +++ b/FirebaseDatabase/Sources/Utilities/FNextPushId.m @@ -37,47 +37,42 @@ @implementation FNextPushId + (NSString *)get:(NSTimeInterval)currentTime { static long long lastPushTime = 0; static int lastRandChars[12]; - static NSLock *generationLock; - static dispatch_once_t lockOnceToken; - dispatch_once(&lockOnceToken, ^{ - generationLock = [[NSLock alloc] init]; - }); + @synchronized([FNextPushId class]) { - [generationLock lock]; + long long now = (long long)(currentTime * 1000); - long long now = (long long)(currentTime * 1000); + BOOL duplicateTime = now == lastPushTime; + lastPushTime = now; - BOOL duplicateTime = now == lastPushTime; - lastPushTime = now; + unichar timeStampChars[8]; + for (int i = 7; i >= 0; i--) { + timeStampChars[i] = [PUSH_CHARS characterAtIndex:(now % 64)]; + now = (long long)floor(now / 64); + } - unichar timeStampChars[8]; - for (int i = 7; i >= 0; i--) { - timeStampChars[i] = [PUSH_CHARS characterAtIndex:(now % 64)]; - now = (long long)floor(now / 64); - } + NSMutableString *id = [[NSMutableString alloc] init]; + [id appendString:[NSString stringWithCharacters:timeStampChars + length:8]]; - NSMutableString *id = [[NSMutableString alloc] init]; - [id appendString:[NSString stringWithCharacters:timeStampChars length:8]]; + if (!duplicateTime) { + for (int i = 0; i < 12; i++) { + lastRandChars[i] = (int)floor(arc4random() % 64); + } + } else { + int i = 0; + for (i = 11; i >= 0 && lastRandChars[i] == 63; i--) { + lastRandChars[i] = 0; + } + lastRandChars[i]++; + } - if (!duplicateTime) { for (int i = 0; i < 12; i++) { - lastRandChars[i] = (int)floor(arc4random() % 64); - } - } else { - int i = 0; - for (i = 11; i >= 0 && lastRandChars[i] == 63; i--) { - lastRandChars[i] = 0; + [id appendFormat:@"%C", + [PUSH_CHARS characterAtIndex:lastRandChars[i]]]; } - lastRandChars[i]++; - } - for (int i = 0; i < 12; i++) { - [id appendFormat:@"%C", [PUSH_CHARS characterAtIndex:lastRandChars[i]]]; + return [NSString stringWithString:id]; } - - NSString *pushId = [NSString stringWithString:id]; - [generationLock unlock]; - return pushId; } + (NSString *)from:(NSString *)fn successor:(NSString *_Nonnull)key { diff --git a/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m b/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m index 634b425dac8..1024ac75b52 100644 --- a/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m +++ b/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m @@ -15,7 +15,6 @@ */ #import -#import #import "FirebaseDatabase/Sources/Utilities/FNextPushId.h" #import "FirebaseDatabase/Sources/Utilities/FUtilities.h" @@ -55,18 +54,28 @@ - (void)testConcurrentPushIdsAreUniqueForTheSameTimestamp { NSMutableArray *pushIds = [NSMutableArray arrayWithCapacity:pushIdCount]; NSLock *pushIdsLock = [[NSLock alloc] init]; - dispatch_apply(pushIdCount, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), - ^(size_t index) { - (void)index; - NSString *pushId = [FNextPushId get:12345.0]; - [pushIdsLock lock]; - [pushIds addObject:pushId]; - [pushIdsLock unlock]; - }); + dispatch_apply(pushIdCount, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^(size_t index) { + (void)index; + NSString *pushId = [FNextPushId get:12345.0]; + [pushIdsLock lock]; + [pushIds addObject:pushId]; + [pushIdsLock unlock]; + }); XCTAssertEqual([NSSet setWithArray:pushIds].count, pushIdCount); } +- (void)testGenerationExceptionDoesNotBlockLaterCalls { + XCTAssertThrowsSpecificNamed([FNextPushId get:-0.001], NSException, NSRangeException); + XCTestExpectation *generated = [self expectationWithDescription:@"Generate after exception"]; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + NSString *pushId = [FNextPushId get:12345.0]; + XCTAssertEqual(pushId.length, 20U); + [generated fulfill]; + }); + [self waitForExpectationsWithTimeout:2.0 handler:nil]; +} + - (void)testSuccessorBasic { NSString *actual = [FNextPushId from:@"test" successor:@"abc"]; NSString *expected = [NSString stringWithFormat:@"abc%@", MIN_PUSH_CHAR]; From 30deadd219b37fa5cf3cc048c454166a8514b593 Mon Sep 17 00:00:00 2001 From: dajiaohuang <108231307+dajiaohuang@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:58:55 +0800 Subject: [PATCH 3/3] Avoid platform-specific exception name in monitor regression --- FirebaseDatabase/Tests/Unit/FNextPushIdTest.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m b/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m index 1024ac75b52..62cff3f109f 100644 --- a/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m +++ b/FirebaseDatabase/Tests/Unit/FNextPushIdTest.m @@ -66,7 +66,7 @@ - (void)testConcurrentPushIdsAreUniqueForTheSameTimestamp { } - (void)testGenerationExceptionDoesNotBlockLaterCalls { - XCTAssertThrowsSpecificNamed([FNextPushId get:-0.001], NSException, NSRangeException); + XCTAssertThrowsSpecific([FNextPushId get:-0.001], NSException); XCTestExpectation *generated = [self expectationWithDescription:@"Generate after exception"]; dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ NSString *pushId = [FNextPushId get:12345.0];