Fix last sample sync issue by making slice boundaries inclusive

Changed boundary checks from exclusive [start, end) to inclusive [start, end] to ensure samples occurring exactly at slice boundaries are synced correctly. This fixes the issue where the last sample in the database would never get synced because it was filtered out by the exclusive end boundary check while the sync state advanced to its timestamp.

Affected syncers:
- AbstractTimeSampleSyncer (HRV, SpO2, Weight, etc.)
- StepsSyncer
- HeartRateSyncer
- SleepSyncer
This commit is contained in:
Gideon Zenz
2026-01-04 14:50:47 +01:00
parent b3eb699ed0
commit 3fe147247e
4 changed files with 10 additions and 7 deletions
@@ -102,7 +102,8 @@ internal abstract class AbstractTimeSampleSyncer<TSample : TimeSample, TRecord :
val recordsToInsert = samples.filter { val recordsToInsert = samples.filter {
val timestamp = Instant.ofEpochMilli(it.timestamp) val timestamp = Instant.ofEpochMilli(it.timestamp)
if (timestamp.isBefore(sliceStartBoundary) || !timestamp.isBefore(sliceEndBoundary)) { // Use inclusive boundaries [sliceStart, sliceEnd] to ensure last sample is included
if (timestamp.isBefore(sliceStartBoundary) || timestamp.isAfter(sliceEndBoundary)) {
logger.debug( logger.debug(
"Skipping sample for at {} as it's outside the slice {} - {}.", "Skipping sample for at {} as it's outside the slice {} - {}.",
timestamp, timestamp,
@@ -77,8 +77,8 @@ internal object HeartRateSyncer : ActivitySampleSyncer {
for (gbSample in validHRSamples) { for (gbSample in validHRSamples) {
val currentSampleTimestamp = Instant.ofEpochSecond(gbSample.timestamp.toLong()) val currentSampleTimestamp = Instant.ofEpochSecond(gbSample.timestamp.toLong())
// Ensure sample is strictly within the slice [sliceStartBoundary, sliceEndBoundary) // Use inclusive boundaries [sliceStart, sliceEnd] for the slice
if (currentSampleTimestamp.isBefore(sliceStartBoundary) || !currentSampleTimestamp.isBefore(sliceEndBoundary)) { if (currentSampleTimestamp.isBefore(sliceStartBoundary) || currentSampleTimestamp.isAfter(sliceEndBoundary)) {
skippedCount++ skippedCount++
continue continue
} }
@@ -122,8 +122,9 @@ internal object SleepSyncer : ContextualActivitySampleSyncer {
val sessionBoundaryStart = analysisSession.sleepStart.toInstant() val sessionBoundaryStart = analysisSession.sleepStart.toInstant()
val sessionBoundaryEndInclusive = analysisSession.sleepEnd.toInstant() val sessionBoundaryEndInclusive = analysisSession.sleepEnd.toInstant()
// Check if session overlaps with the current slice // Check if session overlaps with the current slice (inclusive boundaries [sliceStart, sliceEnd])
if (!sessionBoundaryStart.isBefore(sliceEndBoundary) || !sessionBoundaryEndInclusive.isAfter(sliceStartBoundary)) { // Skip if: sessionStart > sliceEnd OR sessionEnd < sliceStart
if (sessionBoundaryStart.isAfter(sliceEndBoundary) || sessionBoundaryEndInclusive.isBefore(sliceStartBoundary)) {
LOG.debug( LOG.debug(
"Skipping sleep session (identified by SleepAnalysis) for device '{}' (Timeframe: {} to {}) as it does not overlap with current slice ({} to {}).", "Skipping sleep session (identified by SleepAnalysis) for device '{}' (Timeframe: {} to {}) as it does not overlap with current slice ({} to {}).",
deviceName, deviceName,
@@ -69,8 +69,9 @@ internal object StepsSyncer : ActivitySampleSyncer {
val endTs = Instant.ofEpochSecond(currentSample.timestamp.toLong()) val endTs = Instant.ofEpochSecond(currentSample.timestamp.toLong())
val startTs = endTs.minus(1, ChronoUnit.MINUTES) val startTs = endTs.minus(1, ChronoUnit.MINUTES)
// Ensure the record's interval [startTs, endTs) overlaps with the slice [sliceStartBoundary, sliceEndBoundary) // Use inclusive boundaries [sliceStart, sliceEnd] for the slice
if (endTs.isAfter(sliceStartBoundary) && startTs.isBefore(sliceEndBoundary)) { // Overlap check: interval overlaps if endTs > sliceStart AND startTs <= sliceEnd
if (endTs.isAfter(sliceStartBoundary) && !startTs.isAfter(sliceEndBoundary)) {
stepsRecordList.add(StepsRecord(startTs, offset, endTs, offset, stepsInMinute, metadata)) stepsRecordList.add(StepsRecord(startTs, offset, endTs, offset, stepsInMinute, metadata))
} else { } else {
skippedCount++ skippedCount++