From 0ca2c7dd479d17435a224190a4414dcb5a87d985 Mon Sep 17 00:00:00 2001 From: Max Radermacher Date: Thu, 25 Jul 2024 01:45:20 -0500 Subject: [PATCH] Add MessageTimestampGenerator --- Signal.xcodeproj/project.pbxproj | 8 +++ Signal/Calls/CallService.swift | 2 +- Signal/Calls/IndividualCall.swift | 2 +- .../Interactions/OWSDynamicOutgoingMessage.m | 2 +- .../Interactions/OWSStaticOutgoingMessage.m | 2 +- .../Messages/Interactions/TSInteraction.m | 4 +- .../Messages/MessageTimestampGenerator.swift | 49 +++++++++++++++++++ .../MessageTimestampGeneratorTest.swift | 28 +++++++++++ .../Messages/OWSMessageDecrypter.swift | 2 +- .../AttachmentMultisend.swift | 2 +- ...ngStoryMessage+TSAttachmentMultisend.swift | 2 +- SignalUI/Sending/ThreadUtil+SignalUI.swift | 4 +- 12 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 SignalServiceKit/Messages/MessageTimestampGenerator.swift create mode 100644 SignalServiceKit/Messages/MessageTimestampGeneratorTest.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 4c36acdd15..afb938cf9d 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -544,6 +544,8 @@ 500AEE052A4B68E200371F05 /* WallpaperStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500AEE042A4B68E200371F05 /* WallpaperStore.swift */; }; 500AEE072A4DF48700371F05 /* ChatColorSettingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500AEE062A4DF48700371F05 /* ChatColorSettingStore.swift */; }; 500AEE092A4E09AD00371F05 /* AuthorMergeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500AEE082A4E09AD00371F05 /* AuthorMergeObserver.swift */; }; + 500BAD802C519F2D00B4CD7F /* MessageTimestampGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500BAD7E2C519F2D00B4CD7F /* MessageTimestampGenerator.swift */; }; + 500BAD822C519F3600B4CD7F /* MessageTimestampGeneratorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500BAD7F2C519F2D00B4CD7F /* MessageTimestampGeneratorTest.swift */; }; 500FB6182915B86D00257951 /* UITableView+ReusableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500FB6172915B86D00257951 /* UITableView+ReusableCell.swift */; }; 500FE4E0288A11B000FA090C /* ConversationViewController+GiftBadges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500FE4DF288A11AF00FA090C /* ConversationViewController+GiftBadges.swift */; }; 500FE4E2288A373100FA090C /* BadgeGiftingAlreadyRedeemedSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500FE4E1288A373100FA090C /* BadgeGiftingAlreadyRedeemedSheet.swift */; }; @@ -3579,6 +3581,8 @@ 500AEE042A4B68E200371F05 /* WallpaperStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperStore.swift; sourceTree = ""; }; 500AEE062A4DF48700371F05 /* ChatColorSettingStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatColorSettingStore.swift; sourceTree = ""; }; 500AEE082A4E09AD00371F05 /* AuthorMergeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorMergeObserver.swift; sourceTree = ""; }; + 500BAD7E2C519F2D00B4CD7F /* MessageTimestampGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageTimestampGenerator.swift; sourceTree = ""; }; + 500BAD7F2C519F2D00B4CD7F /* MessageTimestampGeneratorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageTimestampGeneratorTest.swift; sourceTree = ""; }; 500FB6172915B86D00257951 /* UITableView+ReusableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableCell.swift"; sourceTree = ""; }; 500FE48E2886148800FA090C /* CachedBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedBadge.swift; sourceTree = ""; }; 500FE4DF288A11AF00FA090C /* ConversationViewController+GiftBadges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+GiftBadges.swift"; sourceTree = ""; }; @@ -10940,6 +10944,8 @@ F9C5C954289453B100548EEE /* MessageSender+SenderKey.swift */, F9C5C8D5289453B100548EEE /* MessageSender.swift */, F9C5C976289453B100548EEE /* MessageSendLog.swift */, + 500BAD7E2C519F2D00B4CD7F /* MessageTimestampGenerator.swift */, + 500BAD7F2C519F2D00B4CD7F /* MessageTimestampGeneratorTest.swift */, 50F9460F2AD768AF002EF293 /* MockIdentityManager.swift */, 502C696F2B06CE9C00012867 /* OutgoingAttachmentInfo.swift */, F96A534228A1AE7B003262D4 /* OutgoingGroupUpdateMessage.swift */, @@ -14520,6 +14526,7 @@ F9C5CC64289453B300548EEE /* MessageSendLog.swift in Sources */, F9C5CC19289453B300548EEE /* MessageSticker.swift in Sources */, 66E793E52BC0D8A600929E5E /* MessageStickerManager.swift in Sources */, + 500BAD802C519F2D00B4CD7F /* MessageTimestampGenerator.swift in Sources */, C16AFAC92BE9CA2700838FFB /* MetadataStreamTransform.swift in Sources */, 721BC7EC2BC8253600648981 /* MimeTypeUtil.swift in Sources */, 501052642BDAEEDC0097DDC5 /* MobileCoinExternal.pb.swift in Sources */, @@ -15357,6 +15364,7 @@ F9426246289B1B5500460798 /* MessageSendJobQueueTest.swift in Sources */, F9426293289B1B5600460798 /* MessageSendLogTests.swift in Sources */, 6633B3932BACF3EB003AFF60 /* MessageStickerSerializationTest.swift in Sources */, + 500BAD822C519F3600B4CD7F /* MessageTimestampGeneratorTest.swift in Sources */, D9F399B92A9EA1FA001599EC /* MockDBV2Test.swift in Sources */, F942624C289B1B5500460798 /* ModelReadCacheTest.swift in Sources */, F9426256289B1B5500460798 /* NSData+ImageTest.swift in Sources */, diff --git a/Signal/Calls/CallService.swift b/Signal/Calls/CallService.swift index 3385bf3d4d..0343363ee9 100644 --- a/Signal/Calls/CallService.swift +++ b/Signal/Calls/CallService.swift @@ -928,7 +928,7 @@ extension CallService: GroupCallObserver { self.groupCallManager.updateGroupCallModelsForPeek( peekInfo: peekInfo, groupThread: groupThread, - triggerEventTimestamp: Date.ows_millisecondTimestamp(), + triggerEventTimestamp: MessageTimestampGenerator.sharedInstance.generateTimestamp(), tx: tx ) } diff --git a/Signal/Calls/IndividualCall.swift b/Signal/Calls/IndividualCall.swift index 26413258af..96cfd18711 100644 --- a/Signal/Calls/IndividualCall.swift +++ b/Signal/Calls/IndividualCall.swift @@ -240,7 +240,7 @@ public class IndividualCall: CustomDebugStringConvertible { offerMediaType: offerMediaType, state: .dialing, thread: thread, - sentAtTimestamp: Date.ows_millisecondTimestamp() + sentAtTimestamp: MessageTimestampGenerator.sharedInstance.generateTimestamp() ) } diff --git a/SignalServiceKit/Messages/Interactions/OWSDynamicOutgoingMessage.m b/SignalServiceKit/Messages/Interactions/OWSDynamicOutgoingMessage.m index 94253c1817..08c5c13491 100644 --- a/SignalServiceKit/Messages/Interactions/OWSDynamicOutgoingMessage.m +++ b/SignalServiceKit/Messages/Interactions/OWSDynamicOutgoingMessage.m @@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN plainTextDataBlock:(DynamicOutgoingMessageBlock)block { return [self initWithThread:thread - timestamp:[NSDate ows_millisecondTimeStamp] + timestamp:[MessageTimestampGenerator.sharedInstance generateTimestamp] transaction:transaction plainTextDataBlock:block]; } diff --git a/SignalServiceKit/Messages/Interactions/OWSStaticOutgoingMessage.m b/SignalServiceKit/Messages/Interactions/OWSStaticOutgoingMessage.m index a6c9cccbb6..bf56a637ef 100644 --- a/SignalServiceKit/Messages/Interactions/OWSStaticOutgoingMessage.m +++ b/SignalServiceKit/Messages/Interactions/OWSStaticOutgoingMessage.m @@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN transaction:(SDSAnyReadTransaction *)transaction { return [self initWithThread:thread - timestamp:[NSDate ows_millisecondTimeStamp] + timestamp:[MessageTimestampGenerator.sharedInstance generateTimestamp] plaintextData:plaintextData transaction:transaction]; } diff --git a/SignalServiceKit/Messages/Interactions/TSInteraction.m b/SignalServiceKit/Messages/Interactions/TSInteraction.m index 9901618a0d..76fcac3c17 100644 --- a/SignalServiceKit/Messages/Interactions/TSInteraction.m +++ b/SignalServiceKit/Messages/Interactions/TSInteraction.m @@ -61,7 +61,9 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value) - (instancetype)initWithUniqueId:(NSString *)uniqueId thread:(TSThread *)thread { - return [self initWithUniqueId:uniqueId timestamp:NSDate.ows_millisecondTimeStamp thread:thread]; + return [self initWithUniqueId:uniqueId + timestamp:[MessageTimestampGenerator.sharedInstance generateTimestamp] + thread:thread]; } - (instancetype)initWithUniqueId:(NSString *)uniqueId timestamp:(uint64_t)timestamp thread:(TSThread *)thread diff --git a/SignalServiceKit/Messages/MessageTimestampGenerator.swift b/SignalServiceKit/Messages/MessageTimestampGenerator.swift new file mode 100644 index 0000000000..20066579cd --- /dev/null +++ b/SignalServiceKit/Messages/MessageTimestampGenerator.swift @@ -0,0 +1,49 @@ +// +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import Foundation + +/// Generates timestamps for messages/envelopes. +@objc +public class MessageTimestampGenerator: NSObject { + private let rangeToAvoid = AtomicValue?>(nil, lock: .init()) + private let nowMs: () -> UInt64 + + @objc + public static let sharedInstance = MessageTimestampGenerator() + + public init(nowMs: @escaping () -> UInt64 = NSDate.ows_millisecondTimeStamp) { + self.nowMs = nowMs + } + + /// Generates a new timestamp from the device's local clock. + /// + /// Performs a few heuristics to try and avoid generating the same timestamp + /// repeatedly when called in a tight loop. + @objc + public func generateTimestamp() -> UInt64 { + let generatedTimestamp = max(nowMs(), 1) + return rangeToAvoid.update { rangeToAvoid in + let newRangeToAvoid = Self.avoidAndExtendRange(rangeToAvoid, proposedValue: generatedTimestamp) + rangeToAvoid = newRangeToAvoid + return newRangeToAvoid.upperBound + } + } + + private static func avoidAndExtendRange( + _ oldRange: ClosedRange?, + proposedValue: UInt64 + ) -> ClosedRange { + if let oldRange, oldRange.contains(proposedValue) { + // If we have a range from the last value, ensure that the new one is + // higher. We track the range to handle cases where `generateTimestamp()` + // is called twice for `t1` and then once for `t1 + 1`. + return oldRange.lowerBound...(oldRange.upperBound + 1) + } else { + // Otherwise, we assume there's no conflict and return `proposedValue`. + return proposedValue...proposedValue + } + } +} diff --git a/SignalServiceKit/Messages/MessageTimestampGeneratorTest.swift b/SignalServiceKit/Messages/MessageTimestampGeneratorTest.swift new file mode 100644 index 0000000000..42e7a0f7d3 --- /dev/null +++ b/SignalServiceKit/Messages/MessageTimestampGeneratorTest.swift @@ -0,0 +1,28 @@ +// +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import XCTest + +@testable import SignalServiceKit + +class MessageTimestampGeneratorTest: XCTestCase { + func testGenerateTimestamp() { + var nowMs: UInt64 = 0 + let generator = MessageTimestampGenerator(nowMs: { return nowMs }) + + nowMs = 1 + let ts1 = generator.generateTimestamp() + let ts2 = generator.generateTimestamp() + nowMs = 2 + let ts3 = generator.generateTimestamp() + nowMs = 4 + let ts4 = generator.generateTimestamp() + + XCTAssertEqual(ts1, 1) + XCTAssertEqual(ts2, 2) + XCTAssertEqual(ts3, 3) + XCTAssertEqual(ts4, 4) + } +} diff --git a/SignalServiceKit/Messages/OWSMessageDecrypter.swift b/SignalServiceKit/Messages/OWSMessageDecrypter.swift index 24e00a09a1..7568e60044 100644 --- a/SignalServiceKit/Messages/OWSMessageDecrypter.swift +++ b/SignalServiceKit/Messages/OWSMessageDecrypter.swift @@ -756,7 +756,7 @@ public class OWSMessageDecrypter: OWSMessageHandler { let errorMessage = TSErrorMessage.failedDecryption( forSender: placeholder.sender, thread: thread, - timestamp: NSDate.ows_millisecondTimeStamp() + timestamp: MessageTimestampGenerator.sharedInstance.generateTimestamp() ) errorMessage.anyInsert(transaction: tx) self.notificationPresenter.notifyUser(forErrorMessage: errorMessage, thread: thread, transaction: tx) diff --git a/SignalUI/AttachmentMultisend/AttachmentMultisend.swift b/SignalUI/AttachmentMultisend/AttachmentMultisend.swift index 8b957b94ea..ebbe30b054 100644 --- a/SignalUI/AttachmentMultisend/AttachmentMultisend.swift +++ b/SignalUI/AttachmentMultisend/AttachmentMultisend.swift @@ -635,7 +635,7 @@ public class AttachmentMultisend { } let storyMessage = try StoryMessage.createAndInsert( - timestamp: Date.ows_millisecondTimestamp(), + timestamp: MessageTimestampGenerator.sharedInstance.generateTimestamp(), authorAci: self.localAci, groupId: groupId, manifest: manifest, diff --git a/SignalUI/AttachmentMultisend/TSResource/OutgoingStoryMessage+TSAttachmentMultisend.swift b/SignalUI/AttachmentMultisend/TSResource/OutgoingStoryMessage+TSAttachmentMultisend.swift index 5776be7663..c304ab4e07 100644 --- a/SignalUI/AttachmentMultisend/TSResource/OutgoingStoryMessage+TSAttachmentMultisend.swift +++ b/SignalUI/AttachmentMultisend/TSResource/OutgoingStoryMessage+TSAttachmentMultisend.swift @@ -130,7 +130,7 @@ extension OutgoingStoryMessage { ) let storyMessage = try StoryMessage.createAndInsert( - timestamp: Date.ows_millisecondTimestamp(), + timestamp: MessageTimestampGenerator.sharedInstance.generateTimestamp(), authorAci: DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction.asV2Read)!.aci, groupId: (thread as? TSGroupThread)?.groupId, manifest: storyManifest, diff --git a/SignalUI/Sending/ThreadUtil+SignalUI.swift b/SignalUI/Sending/ThreadUtil+SignalUI.swift index 10eee633f1..03beecfa55 100644 --- a/SignalUI/Sending/ThreadUtil+SignalUI.swift +++ b/SignalUI/Sending/ThreadUtil+SignalUI.swift @@ -19,7 +19,7 @@ extension ThreadUtil { ) { AssertIsOnMainThread() - let messageTimestamp = Date.ows_millisecondTimestamp() + let messageTimestamp = MessageTimestampGenerator.sharedInstance.generateTimestamp() let benchEventId = sendMessageBenchEventStart(messageTimestamp: messageTimestamp) self.enqueueSendQueue.async { @@ -74,7 +74,7 @@ extension ThreadUtil { ) { AssertIsOnMainThread() - let messageTimestamp = Date.ows_millisecondTimestamp() + let messageTimestamp = MessageTimestampGenerator.sharedInstance.generateTimestamp() let benchEventId = sendMessageBenchEventStart(messageTimestamp: messageTimestamp) self.enqueueSendQueue.async {