Calculate createdLocallyAt using server clock offset#6199
Calculate createdLocallyAt using server clock offset#6199VelikovPetar wants to merge 6 commits intodevelopfrom
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
WalkthroughA new ServerClockOffset utility class is introduced to estimate server time via NTP-style synchronization using WebSocket health checks. The dependency is integrated through ChatClient, ChatModule, and ChatSocket to synchronize local timestamps with server time during message creation and connection lifecycle events. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant Socket as ChatSocket
participant Offset as ServerClockOffset
participant State as State Service
Client->>Socket: initiateConnection()
activate Socket
Socket->>Offset: onConnectionStarted()
activate Offset
Offset->>Offset: recordConnectionStart(localTime)
deactivate Offset
Socket->>Socket: createWebSocket()
Note over Socket: Connection Established
Socket->>Offset: onConnected(serverTime)
activate Offset
Offset->>Offset: calibrate(serverTime, localTime)
Offset->>Offset: calculateOffset via NTP midpoint
deactivate Offset
Socket->>State: notifyConnected()
rect rgba(100, 200, 150, 0.5)
Note over Socket: Health Check Loop
Socket->>Offset: onHealthCheckSent()
activate Offset
Offset->>Offset: recordHealthCheckSent(localTime)
deactivate Offset
Socket->>Socket: sendHealthMessage()
Socket->>Offset: onHealthCheck(serverTime)
activate Offset
Offset->>Offset: refineOffset via RTT & NTP
deactivate Offset
end
deactivate Socket
Client->>Client: createMessage()
Client->>Offset: estimatedServerTime()
activate Offset
Offset->>Offset: return localTime + offset
deactivate Offset
Client->>Client: setMessageCreatedLocallyAt()
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/socket/ChatSocket.kt`:
- Around line 66-70: The health-check timestamp is recorded before confirming
the echo was actually sent; update the checkCallback so that when
chatSocketStateService.currentState is a State.Connected and you obtain the
event, you call sendEvent(it) first and only invoke
serverClockOffset.onHealthCheckSent() if sendEvent(it) returns true (i.e., the
echo was successfully sent). Ensure you reference checkCallback,
State.Connected, sendEvent(it), and serverClockOffset.onHealthCheckSent() when
making the change.
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/internal/ServerClockOffset.kt`:
- Around line 44-55: The compound read/modify/write on volatile fields
(offsetMs, bestRttMs, healthCheckSentAtMs, connectionStartedAtMs) in
onConnected() and onHealthCheck() is racy; wrap those multi-field state
transitions in a mutual-exclusion block using a dedicated lock (e.g., private
val stateLock = Any()) and replace the check-then-set sequences with
synchronized(stateLock) { ... } blocks around the code that updates bestRttMs
and offsetMs so stale comparisons cannot overwrite better values; keep
single-field setters (onConnectionStarted(), onHealthCheckSent()) unchanged and
update the KDoc to state that compound operations are synchronized via stateLock
rather than relying solely on `@Volatile`.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (12)
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/socket/ChatSocket.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/internal/ServerClockOffset.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientConnectionTests.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientTest.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/DependencyResolverTest.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/MockClientBuilder.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/BaseChatClientTest.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/debugger/ChatClientDebuggerTest.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/socket/FakeChatSocket.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/utils/internal/ServerClockOffsetTest.kt
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/socket/ChatSocket.kt
Show resolved
Hide resolved
...id-client/src/main/java/io/getstream/chat/android/client/utils/internal/ServerClockOffset.kt
Show resolved
Hide resolved
…_using_server_clock_offset_calculation
…_using_server_clock_offset_calculation
…_using_server_clock_offset_calculation
|


Goal
When a device clock is skewed relative to the server,
createdLocallyAttimestamps assigned to locally-sent messages can be incorrect. This causes cross-user ordering issues — messages from a user with a fast clock appear in the future relative to messages from other users.Implementation
Introduced
ServerClockOffset, an NTP-style estimator that tracks the offset between the local device clock and the server clock using WebSocket round-trips:onConnectionStarted()— records local time before opening the WebSocket.onConnected(serverTime)— computes initial offset from theConnectedEventtimestamp using the NTP midpoint formula:offset = (sentAt + receivedAt) / 2 - serverTime.onHealthCheckSent()— records local time before each health check echo.onHealthCheck(serverTime)— refines the offset from eachHealthEvent, accepting only the sample with the lowest RTT (min-RTT selection minimises network-asymmetry error).estimatedServerTime()— returnsDate(localTime - offset), used inChatClient.ensureCreatedLocallyAt()instead of the raw local clock.ServerClockOffsetis constructed once perChatClientinstance, wired throughChatModuleintoChatSocket(for calibration) andChatClient(for consumption). All mutable state uses@Volatilefor thread safety.UI Changes
clock-before.mov
clock-after.mov
Testing
ServerClockOffsetTestcovers:onConnectedwith and without a pairedonConnectionStartedonHealthCheckmin-RTT selection and stale-sample rejectionmaxRttMsboundary conditionsServerClockOffsetviaFakeChatSocketandMockClientBuilder.Summary by CodeRabbit