fix(traffic): a sample without counters is not zero traffic

Defaulting missing netin/netout to zero reset the baseline, so the next real
sample added the entire cumulative counter again — a customer throttled for
traffic that was counted twice. Such a sample is skipped and logged now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 23:44:02 +02:00
parent 2415dde288
commit 7941935f57
2 changed files with 46 additions and 2 deletions

View File

@ -73,8 +73,19 @@ class CollectInstanceTraffic implements ShouldBeUnique, ShouldQueue
$client = $proxmox->forHost($instance->host); $client = $proxmox->forHost($instance->host);
$status = $client->vmStatus($instance->host->node ?? 'pve', $instance->vmid); $status = $client->vmStatus($instance->host->node ?? 'pve', $instance->vmid);
$netin = (int) ($status['netin'] ?? 0); // Missing telemetry is not "zero traffic": treating it as zero resets the
$netout = (int) ($status['netout'] ?? 0); // 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( $row = InstanceTraffic::query()->firstOrCreate(
['instance_id' => $instance->id, 'period' => InstanceTraffic::currentPeriod()], ['instance_id' => $instance->id, 'period' => InstanceTraffic::currentPeriod()],

View File

@ -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) expect(InstanceTraffic::query()->where('throttled', true)->count())->toBe(0)
->and($flaky->networkRates[1042])->toBeNull(); ->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);
});