Skip to content

fix: clamp future lastHeard timestamps to current time on ingestion#5418

Merged
jamesarich merged 2 commits into
mainfrom
fix/clamp-future-lastHeard-timestamps
May 12, 2026
Merged

fix: clamp future lastHeard timestamps to current time on ingestion#5418
jamesarich merged 2 commits into
mainfrom
fix/clamp-future-lastHeard-timestamps

Conversation

@jamesarich

Copy link
Copy Markdown
Collaborator

Problem

Nodes with bad time sources report lastHeard values in the future, causing the UI to display nonsensical "last heard" times (e.g. "in 3 days").

Solution

Adds a clampTimestampToNow() utility in core:common that caps epoch-second timestamps at the current wall-clock time. Applied at all packet ingestion points:

  • MeshMessageProcessorImpl — remote node rx_time
  • NodeManagerImpl — position time, telemetry time, installNodeInfo (both last_heard and position.time)
  • TelemetryPacketHandlerImpl — telemetry-driven lastHeard

Past timestamps pass through unchanged. Only future values are clamped to now.

Testing

  • Unit tests added for clampTimestampToNow() (past, future, zero)
  • Existing core:data and core:common tests pass
  • Spotless + Detekt clean

Nodes with bad time sources can report lastHeard values in the future,
causing the UI to display nonsensical "last heard" times. This adds a
clampTimestampToNow() utility that caps epoch-second timestamps at the
current wall-clock time, applied at all packet ingestion points:

- MeshMessageProcessorImpl: remote node rx_time
- NodeManagerImpl: position time, telemetry time, installNodeInfo
- TelemetryPacketHandlerImpl: telemetry-driven lastHeard

Also clamps position.time to prevent future times in map UI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added the bugfix PR tag label May 12, 2026
@jamesarich jamesarich enabled auto-merge May 12, 2026 11:42
@codecov

codecov Bot commented May 12, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
2373 1 2372 0
View the top 1 failed test(s) by shortest run time
org.meshtastic.feature.firmware.ota.WifiOtaTransportTest::close resets transport and closes TCP connection()[jvm]
Stack Traces | 0.049s run time
kotlinx.coroutines.TimeoutCancellationException: Timed out after 5s of _virtual_ (kotlinx.coroutines.test) time. To use the real time, wrap 'withTimeout' in 'withContext(Dispatchers.Default.limitedParallelism(1))'
	at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:281)
	at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:243)
	at kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:24)
	at kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTaskUnless$kotlinx_coroutines_test(TestCoroutineScheduler.kt:98)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$workRunner$1.invokeSuspend(TestBuilders.kt:326)
	at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:42)
	at io.ktor.utils.io.ByteChannel.awaitContent(ByteChannel.kt:284)
	at io.ktor.utils.io.ByteReadChannelOperationsKt.internalReadLineTo(ByteReadChannelOperations.kt:686)
	at io.ktor.utils.io.ByteReadChannelOperationsKt.readLine(ByteReadChannelOperations.kt:586)
	at org.meshtastic.feature.firmware.ota.WifiOtaTransportTest$close resets transport and closes TCP connection$1$1.invokeSuspend(WifiOtaTransportTest.kt:214)
	at org.meshtastic.feature.firmware.ota.WifiOtaTransportTest$close resets transport and closes TCP connection$1.invokeSuspend(WifiOtaTransportTest.kt:214)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$1.invokeSuspend(TestBuilders.kt:317)
Caused by: kotlinx.coroutines.TimeoutCancellationException: Timed out after 5s of _virtual_ (kotlinx.coroutines.test) time. To use the real time, wrap 'withTimeout' in 'withContext(Dispatchers.Default.limitedParallelism(1))'
	at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:281)
	at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:243)
	at kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:24)
	at kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTaskUnless$kotlinx_coroutines_test(TestCoroutineScheduler.kt:98)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$workRunner$1.invokeSuspend(TestBuilders.kt:326)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:256)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:54)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlockingImpl(Builders.kt:30)
	at kotlinx.coroutines.BuildersKt.runBlockingImpl(Unknown Source)
	at kotlinx.coroutines.BuildersKt__Builders_concurrentKt.runBlockingK(Builders.concurrent.kt:172)
	at kotlinx.coroutines.BuildersKt.runBlockingK(Unknown Source)
	at kotlinx.coroutines.BuildersKt__Builders_concurrentKt.runBlockingK$default(Builders.concurrent.kt:157)
	at kotlinx.coroutines.BuildersKt.runBlockingK$default(Unknown Source)
	at kotlinx.coroutines.test.TestBuildersJvmKt.createTestResult(TestBuildersJvm.kt:10)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:309)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:1)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:167)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:1)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:159)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:1)
	at org.meshtastic.feature.firmware.ota.WifiOtaTransportTest.close resets transport and closes TCP connection(WifiOtaTransportTest.kt:208)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

The 'close resets transport' test used withTimeout inside runTest,
where the test scheduler's virtual time could race ahead of the actual
TCP socket FIN propagation. Wrapping the readLine assertion in
withContext(Dispatchers.Default) ensures the timeout uses real wall-clock
time, giving the socket closure time to propagate on loaded CI runners.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jamesarich jamesarich force-pushed the fix/clamp-future-lastHeard-timestamps branch from fe32771 to 014773a Compare May 12, 2026 12:16
@jamesarich jamesarich added this pull request to the merge queue May 12, 2026
@jamesarich jamesarich removed this pull request from the merge queue due to a manual request May 12, 2026
@jamesarich jamesarich merged commit 0f2b1c0 into main May 12, 2026
13 checks passed
@jamesarich jamesarich deleted the fix/clamp-future-lastHeard-timestamps branch May 12, 2026 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant