51336e346a
Daily logs created by root cron were blocking www from appending, which aborted webhook ingest and bot pushes. File channels now use 0664 plus exception-safe stacks, and writes go through SafeLog. Co-authored-by: Cursor <cursoragent@cursor.com>
210 lines
8.2 KiB
PHP
210 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\WalletAddress;
|
|
use App\Services\Tokenview\TokenviewMonitorService;
|
|
use App\Support\SafeLog;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
class SyncTokenviewMonitorsCommand extends Command
|
|
{
|
|
/** Cache key for the last run summary. */
|
|
public const LAST_RUN_KEY = 'coruna.sync-monitors.last_run';
|
|
|
|
protected $signature = 'coruna:sync-monitors
|
|
{--limit=200 : Max addresses to process per run}
|
|
{--status : Show the last run summary instead of running}';
|
|
|
|
protected $description = 'Register monitor=1 & monitor_synced=0 addresses with Tokenview (retry addresses whose initial sync failed)';
|
|
|
|
public function handle(TokenviewMonitorService $svc): int
|
|
{
|
|
if ($this->option('status')) {
|
|
return $this->printStatus();
|
|
}
|
|
|
|
$log = SafeLog::channel('tokenview');
|
|
$startedAt = microtime(true);
|
|
$startedAtStr = now()->toDateTimeString();
|
|
|
|
if (! $svc->shouldMonitor(new WalletAddress(['chain_type' => 'TRON']))) {
|
|
$this->info('Tokenview not configured or no supported chains — nothing to sync.');
|
|
$log->warning('sync-monitors run skipped: tokenview not configured or no supported chains');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$limit = (int) $this->option('limit');
|
|
$maxFailures = TokenviewMonitorService::MAX_SYNC_FAILURES;
|
|
// Only process addresses that are switched on (monitor=1), not yet
|
|
// actually registered with Tokenview (monitor_synced=0), and have not
|
|
// exhausted the retry budget (monitor_failures < MAX).
|
|
$addresses = WalletAddress::query()
|
|
->where('monitor', 1)
|
|
->where('monitor_synced', false)
|
|
->where('monitor_failures', '<', $maxFailures)
|
|
->orderByDesc('updated_at')
|
|
->limit($limit)
|
|
->get();
|
|
|
|
$pending = $addresses->count();
|
|
$log->info('sync-monitors run start', [
|
|
'started_at' => $startedAtStr,
|
|
'pending' => $pending,
|
|
'limit' => $limit,
|
|
]);
|
|
$this->info("sync-monitors starting: {$pending} unsynced address(es).");
|
|
|
|
if ($pending === 0) {
|
|
$this->info('no unsynced monitor=1 addresses.');
|
|
$this->storeLastRun($startedAt, $startedAtStr, 0, 0, 0, 0);
|
|
$log->info('sync-monitors run done: nothing to sync');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$ok = 0;
|
|
$fail = 0;
|
|
$skipped = 0;
|
|
$perAddress = [];
|
|
foreach ($addresses as $addr) {
|
|
if (! $svc->shouldMonitor($addr)) {
|
|
$skipped++;
|
|
$log->info('sync-monitors skip: unsupported chain or tokenview disabled', [
|
|
'id' => $addr->id,
|
|
'address' => $addr->address,
|
|
'chain_type' => $addr->chain_type,
|
|
]);
|
|
continue;
|
|
}
|
|
try {
|
|
$result = $svc->syncMonitor($addr);
|
|
$failures = (int) $addr->fresh()->monitor_failures;
|
|
if ($result) {
|
|
$ok++;
|
|
$log->info('sync-monitors ok', [
|
|
'id' => $addr->id,
|
|
'address' => $addr->address,
|
|
'chain_type' => $addr->chain_type,
|
|
]);
|
|
$perAddress[] = ['address' => $addr->address, 'chain' => $addr->chain_type, 'ok' => true, 'failures' => $failures];
|
|
} else {
|
|
$fail++;
|
|
$exhausted = $failures >= $maxFailures;
|
|
$log->warning('sync-monitors fail: tokenview addAddress returned false', [
|
|
'id' => $addr->id,
|
|
'address' => $addr->address,
|
|
'chain_type' => $addr->chain_type,
|
|
'failures' => $failures,
|
|
'exhausted' => $exhausted,
|
|
]);
|
|
$reason = $exhausted
|
|
? "api returned false (failures={$failures}, giving up)"
|
|
: "api returned false (failures={$failures})";
|
|
$perAddress[] = ['address' => $addr->address, 'chain' => $addr->chain_type, 'ok' => false, 'reason' => $reason, 'failures' => $failures];
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$fail++;
|
|
$failures = (int) $addr->fresh()->monitor_failures;
|
|
$this->warn(" failed: {$addr->address} ({$addr->chain_type}): {$e->getMessage()}");
|
|
$log->warning('sync-monitors fail: exception', [
|
|
'id' => $addr->id,
|
|
'address' => $addr->address,
|
|
'chain_type' => $addr->chain_type,
|
|
'error' => $e->getMessage(),
|
|
'failures' => $failures,
|
|
]);
|
|
$perAddress[] = ['address' => $addr->address, 'chain' => $addr->chain_type, 'ok' => false, 'reason' => $e->getMessage(), 'failures' => $failures];
|
|
}
|
|
}
|
|
|
|
$elapsed = round(microtime(true) - $startedAt, 3);
|
|
$this->info("synced: ok={$ok} fail={$fail} skipped={$skipped} total=".count($addresses)." in {$elapsed}s");
|
|
|
|
$log->info('sync-monitors run done', [
|
|
'started_at' => $startedAtStr,
|
|
'ok' => $ok,
|
|
'fail' => $fail,
|
|
'skipped' => $skipped,
|
|
'total' => count($addresses),
|
|
'elapsed_sec' => $elapsed,
|
|
]);
|
|
|
|
$this->storeLastRun($startedAt, $startedAtStr, $ok, $fail, $skipped, count($addresses), $perAddress);
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function storeLastRun(
|
|
float $startedAt,
|
|
string $startedAtStr,
|
|
int $ok,
|
|
int $fail,
|
|
int $skipped,
|
|
int $total,
|
|
array $perAddress = [],
|
|
): void {
|
|
Cache::put(self::LAST_RUN_KEY, [
|
|
'started_at' => $startedAtStr,
|
|
'finished_at' => now()->toDateTimeString(),
|
|
'elapsed_sec' => round(microtime(true) - $startedAt, 3),
|
|
'ok' => $ok,
|
|
'fail' => $fail,
|
|
'skipped' => $skipped,
|
|
'total' => $total,
|
|
'per_address' => $perAddress,
|
|
], now()->addDays(7));
|
|
}
|
|
|
|
private function printStatus(): int
|
|
{
|
|
$last = Cache::get(self::LAST_RUN_KEY);
|
|
if (! $last) {
|
|
$this->info('no recorded run yet — sync-monitors has not executed since the cache was cleared.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$this->info('=== coruna:sync-monitors last run ===');
|
|
$this->line('started : '.$last['started_at']);
|
|
$this->line('finished: '.$last['finished_at']);
|
|
$this->line('elapsed : '.$last['elapsed_sec'].'s');
|
|
$this->line(sprintf('ok=%d fail=%d skipped=%d total=%d',
|
|
$last['ok'], $last['fail'], $last['skipped'], $last['total']));
|
|
|
|
$maxFailures = TokenviewMonitorService::MAX_SYNC_FAILURES;
|
|
$pending = WalletAddress::query()
|
|
->where('monitor', 1)
|
|
->where('monitor_synced', false)
|
|
->where('monitor_failures', '<', $maxFailures)
|
|
->count();
|
|
$this->line("currently retryable (monitor=1 & monitor_synced=0 & failures<{$maxFailures}): {$pending}");
|
|
|
|
$exhausted = WalletAddress::query()
|
|
->where('monitor', 1)
|
|
->where('monitor_synced', false)
|
|
->where('monitor_failures', '>=', $maxFailures)
|
|
->count();
|
|
$this->line("gave up (failures>={$maxFailures}): {$exhausted}");
|
|
|
|
if (! empty($last['per_address'])) {
|
|
$this->line('');
|
|
$this->line('per-address results:');
|
|
foreach ($last['per_address'] as $r) {
|
|
$tag = $r['ok'] ? 'OK ' : 'FAIL';
|
|
$reason = $r['ok'] ? '' : ' — '.($r['reason'] ?? '');
|
|
$failures = isset($r['failures']) ? " [failures={$r['failures']}]" : '';
|
|
$this->line(sprintf(' [%s] %s (%s)%s%s', $tag, $r['address'], $r['chain'], $reason, $failures));
|
|
}
|
|
}
|
|
|
|
$logPath = storage_path('logs/tokenview/tokenview-'.now()->format('Y-m-d').'.log');
|
|
$this->line('');
|
|
$this->line('today\'s log: '.$logPath);
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|