| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- <?php
- namespace Libs;
- /**
- * Gerencia a conexão com o banco PostgreSQL.
- *
- * A aplicação roda como processo de longa duração (framework-X / ReactPHP),
- * portanto criar uma conexão PDO nova a cada chamada desperdiça recursos e
- * pode esgotar o pool de conexões do servidor. Aqui reutilizamos uma única
- * conexão persistente por processo (singleton), validando se ela continua
- * viva e reconectando quando necessário.
- */
- final class Database
- {
- private static ?\PDO $connection = null;
- private static float $lastCheckedAt = 0.0;
- /**
- * Intervalo mínimo entre validações de "liveness" da conexão.
- * Dentro de uma mesma requisição (que faz várias queries em sequência),
- * evita repetir o round-trip de "SELECT 1" a cada chamada de pdo().
- */
- private const HEALTHCHECK_INTERVAL_SECONDS = 2.0;
- public static function pdo(): \PDO
- {
- if (self::$connection instanceof \PDO) {
- $now = microtime(true);
- // Só revalida a conexão se passou o intervalo mínimo desde a
- // última checagem; caso contrário reaproveita direto.
- if ($now - self::$lastCheckedAt < self::HEALTHCHECK_INTERVAL_SECONDS) {
- return self::$connection;
- }
- if (self::isAlive(self::$connection)) {
- self::$lastCheckedAt = $now;
- return self::$connection;
- }
- }
- self::$connection = self::connect();
- self::$lastCheckedAt = microtime(true);
- return self::$connection;
- }
- /**
- * Verifica se a conexão em cache ainda está utilizável.
- * Se o banco reiniciou ou derrubou a conexão, força a reconexão.
- */
- private static function isAlive(\PDO $pdo): bool
- {
- try {
- $pdo->query('SELECT 1');
- return true;
- } catch (\PDOException $e) {
- Logger::warning('Database connection lost, reconnecting', ['error' => $e->getMessage()]);
- return false;
- }
- }
- private static function connect(): \PDO
- {
- $host = self::env('DB_HOST', '127.0.0.1');
- $port = self::env('DB_PORT', '5432');
- $name = $_ENV['DB_NAME'] ?? '';
- $user = $_ENV['DB_USER'] ?? '';
- $pass = $_ENV['DB_PASS'] ?? '';
- if ($name === '') {
- Logger::error('DB_NAME is not configured');
- throw new \RuntimeException('DB_NAME is not configured.');
- }
- if (($_ENV['DB_USER'] ?? '') === '') {
- Logger::warning('DB_USER is empty; using empty database user');
- }
- $dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $host, $port, $name);
- try {
- $pdo = new \PDO($dsn, $user, $pass, [
- \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
- \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
- \PDO::ATTR_PERSISTENT => true,
- // Emula prepares no cliente: evita o round-trip extra de
- // Parse/Describe server-side a cada query. Com o banco remoto
- // (~160ms de latência por ida-e-volta), isso reduz o custo de
- // cada query de ~3 round-trips para 1.
- \PDO::ATTR_EMULATE_PREPARES => true,
- ]);
- } catch (\PDOException $e) {
- Logger::error('Failed to connect to database', [
- 'host' => $host,
- 'port' => $port,
- 'name' => $name,
- 'error' => $e->getMessage(),
- ]);
- throw $e;
- }
- Logger::info('Database connection established', ['host' => $host, 'port' => $port, 'name' => $name]);
- return $pdo;
- }
- /**
- * Lê uma variável de ambiente registrando em log quando cai no valor default.
- * Torna visível o uso silencioso de defaults (ex.: host/porta ausentes).
- */
- private static function env(string $key, string $default): string
- {
- $value = $_ENV[$key] ?? null;
- if ($value === null || $value === '') {
- Logger::warning('Environment variable missing, using default', ['key' => $key, 'default' => $default]);
- return $default;
- }
- return (string) $value;
- }
- }
|