fix(балансы): свежий query-builder на итерацию джобы (PK violation на 2-м прогоне)
Переиспользование одного DB-билдера в цикле накапливало where-клаузы → updateOrInsert уходил в INSERT существующей строки → SQLSTATE 23505 на проде при повторном сборе. Билдер теперь создаётся внутри цикла. + тест на 2 прогона. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-2
@@ -42,14 +42,17 @@ class RefreshExternalBalancesJob implements ShouldQueue
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$table = DB::connection(self::DB_CONNECTION)->table('external_service_balances');
|
||||
|
||||
foreach ($this->providers() as $cls) {
|
||||
/** @var BalanceProvider $p */
|
||||
$p = app($cls);
|
||||
$key = $p->serviceKey();
|
||||
$reading = $p->fetch(); // не бросает
|
||||
|
||||
// Свежий query-builder на КАЖДУЮ итерацию: переиспользование одного билдера
|
||||
// накапливает where-клаузы (service_key=A AND service_key=B…) → updateOrInsert
|
||||
// ошибочно идёт в INSERT существующей строки → нарушение PK.
|
||||
$table = DB::connection(self::DB_CONNECTION)->table('external_service_balances');
|
||||
|
||||
if (! $reading->ok) {
|
||||
// Оставляем прошлый баланс, помечаем ok=false + ошибку.
|
||||
$table->updateOrInsert(
|
||||
|
||||
@@ -51,6 +51,18 @@ it('пишет балансы трёх сервисов + считает све
|
||||
expect($rows['dadata']->ok)->toBeTruthy();
|
||||
});
|
||||
|
||||
it('повторный запуск обновляет строки, а не падает на PK (свежий builder/итерация)', function () {
|
||||
app()->instance(DadataBalanceProvider::class, fakeProvider('dadata', BalanceReading::ok('dadata', 4500, 'RUB', 100)));
|
||||
app()->instance(SupplierBalanceProvider::class, fakeProvider('supplier', BalanceReading::fail('supplier', 'таймаут')));
|
||||
app()->instance(YandexCloudBalanceProvider::class, fakeProvider('yandex_cloud', BalanceReading::ok('yandex_cloud', 42000, 'RUB', 600)));
|
||||
|
||||
(new RefreshExternalBalancesJob)->handle();
|
||||
(new RefreshExternalBalancesJob)->handle(); // второй прогон не должен бросить UniqueConstraint
|
||||
|
||||
$rows = DB::connection('pgsql_supplier')->table('external_service_balances')->get();
|
||||
expect($rows)->toHaveCount(3); // строк по-прежнему 3, без дублей
|
||||
});
|
||||
|
||||
it('упавший провайдер не роняет джобу и сохраняет ошибку, остальные пишутся', function () {
|
||||
app()->instance(DadataBalanceProvider::class, fakeProvider('dadata', BalanceReading::fail('dadata', 'HTTP 403')));
|
||||
app()->instance(SupplierBalanceProvider::class, fakeProvider('supplier', BalanceReading::ok('supplier', 50000, 'RUB', null)));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Brain Status (auto-generated)
|
||||
|
||||
Last updated: 2026-06-28T04:13:25.256Z
|
||||
Last updated: 2026-06-28T04:26:08.585Z
|
||||
|
||||
| Контролёр | Состояние | Детали |
|
||||
|---|---|---|
|
||||
@@ -33,7 +33,7 @@ Last updated: 2026-06-28T04:13:25.256Z
|
||||
| enforce-coverage-verify.mjs | `enforce-coverage-verify.mjs` | 🔴 |
|
||||
| enforce-todowrite-skill-verifier.mjs | `enforce-todowrite-skill-verifier.mjs` | 🔴 |
|
||||
|
||||
Недавние escape владельца: 0 · Недавние блоки: 7
|
||||
Недавние escape владельца: 0 · Недавние блоки: 5
|
||||
|
||||
**Недавние блоки (детали):**
|
||||
|
||||
@@ -44,8 +44,6 @@ Last updated: 2026-06-28T04:13:25.256Z
|
||||
| 2026-06-27T09:25:54.127Z | bash:node -e "for (const d of ['протокол-наставника','проблема-закрытия-вопросов-протокола','содержит']) { try { const p | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:node -e "for (const d |
|
||||
| 2026-06-27T07:03:56.852Z | bash:node -e "1" 2>/dev/null; for f in docs/secretary/*/protocol.md; do printf '%6s %s\n' "$(wc -l < "$f")" "$f"; done | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:node -e "1" 2>/dev/nul |
|
||||
| 2026-06-27T05:45:19.915Z | bash:rm ~/.claude/runtime/secretary-mode-*.json | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:rm ~/.claude/runtime/s |
|
||||
| 2026-06-27T04:18:53.490Z | powershell:Stop-Process -Id 4072 -Force | floor: опасная PowerShell-команда без аварийного выхода — блок (правило 8, V1-PS); FLOOR-ESCAPE: powershell:Stop-Process |
|
||||
| 2026-06-27T04:18:44.603Z | powershell:Stop-Process -Id 4072 -Force; if ($?) { "killed 4072" }; Start-Sleep -Milliseconds 300; Get-CimInstance Win32 | floor: опасная PowerShell-команда без аварийного выхода — блок (правило 8, V1-PS); FLOOR-ESCAPE: powershell:Stop-Process |
|
||||
|
||||
## Метрики (информационные, не алерты)
|
||||
|
||||
@@ -129,9 +127,9 @@ Episodes since last run: 542 / threshold: 10
|
||||
|
||||
| PID | Имя | CPU-время | Возраст |
|
||||
|---|---|---|---|
|
||||
| 3440 | MsMpEng | 17.01ч | 220544.2ч |
|
||||
| 21928 | Code | 7.32ч | 0.0ч |
|
||||
| 1212 | svchost | 4.38ч | 10093734.7ч |
|
||||
| 3440 | MsMpEng | 17.07ч | 0.0ч |
|
||||
| 21928 | Code | 7.36ч | 451351.4ч |
|
||||
| 1212 | svchost | 4.38ч | NaNч |
|
||||
|
||||
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user