diff --git a/app/Jobs/IngestShellyMessage.php b/app/Jobs/IngestShellyMessage.php index 25f60f6..91949f8 100644 --- a/app/Jobs/IngestShellyMessage.php +++ b/app/Jobs/IngestShellyMessage.php @@ -53,11 +53,12 @@ class IngestShellyMessage implements ShouldQueue $observedAt = $this->observedAt ?? (int) round(microtime(true) * 1_000_000); $observedTime = \Illuminate\Support\Carbon::createFromTimestampMs(intdiv($observedAt, 1000)); - // Presence reflects when the message was RECEIVED, and only advances — queue latency, - // retries or stale messages can never refresh (or rewind) last_seen incorrectly. - if ($device->last_seen_at === null || $observedTime->gt($device->last_seen_at)) { - $device->forceFill(['last_seen_at' => $observedTime])->save(); - } + // Presence reflects when the message was RECEIVED, and only ever advances. The guard + // lives in the WHERE clause (one atomic UPDATE), so concurrent workers can't rewind it + // via read-then-write — queue latency, retries or stale messages never mark it wrong. + Device::whereKey($device->id) + ->where(fn ($q) => $q->whereNull('last_seen_at')->orWhere('last_seen_at', '<', $observedTime)) + ->update(['last_seen_at' => $observedTime, 'updated_at' => now()]); foreach (ShellyNormalizer::normalize($component, $data) as $update) { $entity = $device->entities()->firstOrCreate(