From 58d0c18e25d06b3316cdab7f398997f126f9ffb1 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Sat, 2 May 2026 01:50:10 +0200 Subject: [PATCH] yssd --- modules/mining-checker/src/Api/Router.php | 167 ++++++++++++++++++++-- 1 file changed, 159 insertions(+), 8 deletions(-) diff --git a/modules/mining-checker/src/Api/Router.php b/modules/mining-checker/src/Api/Router.php index 3bc0535..bbc9f95 100644 --- a/modules/mining-checker/src/Api/Router.php +++ b/modules/mining-checker/src/Api/Router.php @@ -69,6 +69,10 @@ final class Router $this->releaseSessionLock(); $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); $path = trim($relativePath, '/'); + $this->debug->add('router.handle.start', [ + 'path' => $path, + 'method' => $method, + ]); if ($path === 'v1/health') { $this->respond(['ok' => true, 'module' => 'mining-checker']); @@ -285,11 +289,20 @@ final class Router throw new ApiException('Ressource nicht gefunden.', 404, ['resource' => $resource, 'method' => $method]); } catch (ApiException $exception) { + $this->debug->add('router.handle.api_exception', [ + 'status' => $exception->statusCode(), + 'message' => $exception->getMessage(), + 'context' => $exception->context(), + ]); Http::json([ 'error' => $exception->getMessage(), 'context' => $exception->context(), ], $exception->statusCode()); } catch (\Throwable $exception) { + $this->debug->add('router.handle.error', [ + 'type' => get_debug_type($exception), + 'message' => $exception->getMessage(), + ]); Http::json([ 'error' => 'Unerwarteter Mining-Checker Fehler.', 'context' => ['message' => $exception->getMessage()], @@ -300,7 +313,13 @@ final class Router private function bootstrap(string $projectKey): array { $startedAt = microtime(true); - $settings = $this->safeRead(fn () => $this->settings($projectKey), [ + $this->debug->add('bootstrap.start', [ + 'project_key' => $projectKey, + 'measurement_limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT, + 'snapshot_limit' => self::BOOTSTRAP_SNAPSHOT_LIMIT, + ]); + + $settings = $this->safeTimed('bootstrap.settings', fn () => $this->settings($projectKey), [ 'project_key' => $projectKey, 'baseline_measured_at' => null, 'baseline_coins_total' => null, @@ -319,19 +338,38 @@ final class Router 'miner_offers' => [], 'purchased_miners' => [], 'measurement_rates' => [], + ], ['project_key' => $projectKey]); + $measurements = $this->safeTimed('bootstrap.measurements', fn () => $this->bootstrapMeasurements($projectKey, $settings), [], [ + 'project_key' => $projectKey, ]); - $measurements = $this->safeRead(fn () => $this->bootstrapMeasurements($projectKey, $settings), []); - $targets = $this->safeRead(fn () => $this->targets($projectKey), []); - $dashboards = $this->safeRead(fn () => $this->dashboards($projectKey), []); - $fxSnapshots = $this->safeRead(fn () => $this->measurementFxSnapshots($measurements, self::BOOTSTRAP_SNAPSHOT_LIMIT), []); - $summary = $this->safeRead(fn () => $this->analytics()->buildSummary($measurements, $settings, $targets), [ + $targets = $this->safeTimed('bootstrap.targets', fn () => $this->targets($projectKey), [], [ + 'project_key' => $projectKey, + ]); + $dashboards = $this->safeTimed('bootstrap.dashboards', fn () => $this->dashboards($projectKey), [], [ + 'project_key' => $projectKey, + ]); + $fxSnapshots = $this->safeTimed('bootstrap.fx_snapshots', fn () => $this->measurementFxSnapshots($measurements, self::BOOTSTRAP_SNAPSHOT_LIMIT), [], [ + 'measurement_count' => is_array($measurements) ? count($measurements) : 0, + ]); + $summary = $this->safeTimed('bootstrap.summary', fn () => $this->analytics()->buildSummary($measurements, $settings, $targets), [ 'latest_measurement' => $measurements !== [] ? $measurements[array_key_last($measurements)] : null, 'baseline' => $settings, 'targets' => [], 'payouts' => [], 'miner_offers' => [], + ], [ + 'measurement_count' => is_array($measurements) ? count($measurements) : 0, + 'target_count' => is_array($targets) ? count($targets) : 0, ]); $measurementCount = is_array($measurements) ? count($measurements) : 0; + $this->debug->add('bootstrap.end', [ + 'project_key' => $projectKey, + 'duration_ms' => round((microtime(true) - $startedAt) * 1000, 2), + 'measurement_count' => $measurementCount, + 'target_count' => is_array($targets) ? count($targets) : 0, + 'dashboard_count' => is_array($dashboards) ? count($dashboards) : 0, + 'snapshot_count' => is_array($fxSnapshots) ? count($fxSnapshots) : 0, + ]); return [ 'project' => $this->repository()->getProject($projectKey), @@ -783,22 +821,39 @@ final class Router } $startedAt = microtime(true); + $this->debug->add('legacy_fx_migrate.start', [ + 'project_key' => $projectKey, + ]); module_fn('fx-rates', 'ensure_schema'); $legacyRows = $this->repository()->listAllFxRates(); $legacyFetches = $this->groupLegacyFxFetches($legacyRows); $fxRepository = module_fn('fx-rates', 'repository'); + $this->debug->add('legacy_fx_migrate.loaded', [ + 'legacy_rows' => count($legacyRows), + 'legacy_fetches' => count($legacyFetches), + ]); $fetchIdMap = []; $importedFetches = 0; $reusedFetches = 0; $migratedRates = 0; + $legacyFetchIndex = 0; foreach ($legacyFetches as $legacyFetchId => $legacyFetch) { $this->assertRequestWithinBudget($startedAt, 'Legacy-FX-Migration dauert zu lange.'); + $legacyFetchIndex++; $existing = $this->findExistingFxFetchForLegacy($fxRepository, $legacyFetch); if (is_array($existing) && !empty($existing['id'])) { $fetchIdMap[$legacyFetchId] = (int) $existing['id']; $reusedFetches++; + if ($legacyFetchIndex % 50 === 0) { + $this->debug->add('legacy_fx_migrate.fetch_progress', [ + 'processed' => $legacyFetchIndex, + 'total' => count($legacyFetches), + 'reused_fetches' => $reusedFetches, + 'imported_fetches' => $importedFetches, + ]); + } continue; } @@ -816,19 +871,33 @@ final class Router } $importedFetches++; $migratedRates += count($saved['rates'] ?? []); + if ($legacyFetchIndex % 50 === 0) { + $this->debug->add('legacy_fx_migrate.fetch_progress', [ + 'processed' => $legacyFetchIndex, + 'total' => count($legacyFetches), + 'reused_fetches' => $reusedFetches, + 'imported_fetches' => $importedFetches, + ]); + } } $measurements = $this->repository()->listAllMeasurements($projectKey); $measurementRatesById = $this->groupMeasurementRatesByMeasurementId( $this->repository()->listMeasurementRates($projectKey) ); + $this->debug->add('legacy_fx_migrate.measurements_loaded', [ + 'measurement_count' => count($measurements), + 'measurement_rate_groups' => count($measurementRatesById), + ]); $updatedMeasurements = 0; $reusedMeasurements = 0; $unresolvedMeasurements = 0; + $measurementIndex = 0; foreach ($measurements as $measurement) { $this->assertRequestWithinBudget($startedAt, 'Legacy-FX-Migration dauert zu lange.'); + $measurementIndex++; $measurementId = is_numeric($measurement['id'] ?? null) ? (int) $measurement['id'] : 0; if ($measurementId <= 0) { continue; @@ -837,6 +906,15 @@ final class Router $currentFetchId = is_numeric($measurement['fx_fetch_id'] ?? null) ? (int) $measurement['fx_fetch_id'] : 0; if ($currentFetchId > 0 && $this->fx()->snapshotByFetchId($currentFetchId, null, null) !== null) { $reusedMeasurements++; + if ($measurementIndex % 100 === 0) { + $this->debug->add('legacy_fx_migrate.measurement_progress', [ + 'processed' => $measurementIndex, + 'total' => count($measurements), + 'updated' => $updatedMeasurements, + 'reused' => $reusedMeasurements, + 'unresolved' => $unresolvedMeasurements, + ]); + } continue; } @@ -849,13 +927,44 @@ final class Router if (!is_numeric($resolvedFetchId) || (int) $resolvedFetchId <= 0) { $unresolvedMeasurements++; + if ($measurementIndex % 100 === 0) { + $this->debug->add('legacy_fx_migrate.measurement_progress', [ + 'processed' => $measurementIndex, + 'total' => count($measurements), + 'updated' => $updatedMeasurements, + 'reused' => $reusedMeasurements, + 'unresolved' => $unresolvedMeasurements, + ]); + } continue; } $this->repository()->setMeasurementFxFetchId($projectKey, $measurementId, (int) $resolvedFetchId); $updatedMeasurements++; + if ($measurementIndex % 100 === 0) { + $this->debug->add('legacy_fx_migrate.measurement_progress', [ + 'processed' => $measurementIndex, + 'total' => count($measurements), + 'updated' => $updatedMeasurements, + 'reused' => $reusedMeasurements, + 'unresolved' => $unresolvedMeasurements, + ]); + } } + $this->debug->add('legacy_fx_migrate.end', [ + 'duration_ms' => round((microtime(true) - $startedAt) * 1000, 2), + 'legacy_fetches_found' => count($legacyFetches), + 'legacy_rates_found' => count($legacyRows), + 'fx_fetches_imported' => $importedFetches, + 'fx_fetches_reused' => $reusedFetches, + 'fx_rates_imported' => $migratedRates, + 'measurements_checked' => count($measurements), + 'measurements_updated' => $updatedMeasurements, + 'measurements_reused' => $reusedMeasurements, + 'measurements_unresolved' => $unresolvedMeasurements, + ]); + return [ 'message' => 'Legacy-FX-Rates wurden nach fx-rates migriert und Messpunkte aktualisiert.', 'legacy_fetches_found' => count($legacyFetches), @@ -1148,6 +1257,11 @@ final class Router private function bootstrapMeasurements(string $projectKey, array $settings): array { $rows = $this->repository()->listMeasurements($projectKey, self::BOOTSTRAP_MEASUREMENT_LIMIT); + $this->debug->add('bootstrap.measurements.loaded', [ + 'project_key' => $projectKey, + 'row_count' => count($rows), + 'limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT, + ]); return $this->analytics()->enrichMeasurements($rows, $settings); } @@ -2145,6 +2259,10 @@ final class Router if (function_exists('set_time_limit')) { @set_time_limit(15); } + $this->debug->add('runtime.guards', [ + 'time_limit_sec' => 15, + 'budget_sec' => self::LONG_REQUEST_BUDGET_SECONDS, + ]); } private function releaseSessionLock(): void @@ -2160,8 +2278,41 @@ final class Router private function assertRequestWithinBudget(float $startedAt, string $message): void { - if ((microtime(true) - $startedAt) > self::LONG_REQUEST_BUDGET_SECONDS) { - throw new ApiException($message, 503, ['timeout' => true]); + $elapsed = microtime(true) - $startedAt; + if ($elapsed > self::LONG_REQUEST_BUDGET_SECONDS) { + $this->debug->add('request.budget_exceeded', [ + 'elapsed_ms' => round($elapsed * 1000, 2), + 'budget_ms' => round(self::LONG_REQUEST_BUDGET_SECONDS * 1000, 2), + 'message' => $message, + ]); + throw new ApiException($message, 503, ['timeout' => true, 'elapsed_ms' => round($elapsed * 1000, 2)]); + } + } + + private function safeTimed(string $event, callable $callback, mixed $fallback = null, array $context = []): mixed + { + $startedAt = microtime(true); + $this->debug->add($event . '.start', $context); + + try { + $result = $callback(); + $meta = [ + 'duration_ms' => round((microtime(true) - $startedAt) * 1000, 2), + ]; + if (is_array($result)) { + $meta['count'] = count($result); + } + $this->debug->add($event . '.end', $context + $meta); + return $result; + } catch (\Throwable $exception) { + $meta = [ + 'duration_ms' => round((microtime(true) - $startedAt) * 1000, 2), + 'fallback_used' => true, + 'error' => $exception->getMessage(), + 'type' => get_debug_type($exception), + ]; + $this->debug->add($event . '.error', $context + $meta); + return $fallback; } }