diff --git a/app/Provisioning/Jobs/CollectInstanceTraffic.php b/app/Provisioning/Jobs/CollectInstanceTraffic.php index 363d075..4bef001 100644 --- a/app/Provisioning/Jobs/CollectInstanceTraffic.php +++ b/app/Provisioning/Jobs/CollectInstanceTraffic.php @@ -73,8 +73,19 @@ class CollectInstanceTraffic implements ShouldBeUnique, ShouldQueue $client = $proxmox->forHost($instance->host); $status = $client->vmStatus($instance->host->node ?? 'pve', $instance->vmid); - $netin = (int) ($status['netin'] ?? 0); - $netout = (int) ($status['netout'] ?? 0); + // Missing telemetry is not "zero traffic": treating it as zero resets the + // baseline, and the next real sample then adds the whole cumulative + // counter again — a customer throttled for traffic counted twice. + if (! isset($status['netin'], $status['netout'])) { + Log::warning('traffic sample without counters, skipping', [ + 'instance' => $instance->uuid, 'status' => $status['status'] ?? null, + ]); + + return; + } + + $netin = (int) $status['netin']; + $netout = (int) $status['netout']; $row = InstanceTraffic::query()->firstOrCreate( ['instance_id' => $instance->id, 'period' => InstanceTraffic::currentPeriod()], diff --git a/tests/Feature/TrafficTest.php b/tests/Feature/TrafficTest.php index e9de2da..69476be 100644 --- a/tests/Feature/TrafficTest.php +++ b/tests/Feature/TrafficTest.php @@ -218,3 +218,36 @@ it('keeps trying to release a throttle when Proxmox refuses once', function () { expect(InstanceTraffic::query()->where('throttled', true)->count())->toBe(0) ->and($flaky->networkRates[1042])->toBeNull(); }); + +it('ignores a sample that carries no counters', function () { + [$pve, , $instance] = trafficSetup(); + + $pve->counters[1042] = ['netin' => 0, 'netout' => 4_000_000]; + (new CollectInstanceTraffic)->handle($pve); + $pve->counters[1042] = ['netin' => 0, 'netout' => 10_000_000]; + (new CollectInstanceTraffic)->handle($pve); + expect(InstanceTraffic::query()->first()->tx_bytes)->toBe(6_000_000); + + // Proxmox answers without telemetry. Treating that as zero would reset the + // baseline and count the whole counter again on the next real sample. + $blind = new class extends FakeProxmoxClient + { + public function vmStatus(string $node, int $vmid): array + { + return ['status' => 'running']; + } + }; + app()->instance(ProxmoxClient::class, $blind); + (new CollectInstanceTraffic)->handle($blind); + + $row = InstanceTraffic::query()->first(); + expect($row->tx_bytes)->toBe(6_000_000) + ->and($row->last_netout)->toBe(10_000_000); // baseline untouched + + // The next good sample continues from where it left off. + app()->instance(ProxmoxClient::class, $pve); + $pve->counters[1042] = ['netin' => 0, 'netout' => 11_000_000]; + (new CollectInstanceTraffic)->handle($pve); + + expect(InstanceTraffic::query()->first()->tx_bytes)->toBe(7_000_000); +});