Files
coruna-lab/app/Console/Commands/SyncTokenviewMonitorsCommand.php
T
hashbro 51336e346a Keep Tokenview and Telegram running when log files are not writable.
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>
2026-09-15 11:40:44 +08:00

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;
}
}