Fix R15: make the last_seen update atomic (no read-then-write race)
Two concurrent workers could both read the old last_seen_at, pass the in-memory comparison and save in reverse order, rewinding presence. The guard now lives in the WHERE clause of a single conditional UPDATE, so last_seen_at only ever advances even under concurrent ingestion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/phase-1-bootstrap
parent
23ef993cb2
commit
d950abe375
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue