Database.php 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. <?php
  2. namespace Libs;
  3. /**
  4. * Gerencia a conexão com o banco PostgreSQL.
  5. *
  6. * A aplicação roda como processo de longa duração (framework-X / ReactPHP),
  7. * portanto criar uma conexão PDO nova a cada chamada desperdiça recursos e
  8. * pode esgotar o pool de conexões do servidor. Aqui reutilizamos uma única
  9. * conexão persistente por processo (singleton), validando se ela continua
  10. * viva e reconectando quando necessário.
  11. */
  12. final class Database
  13. {
  14. private static ?\PDO $connection = null;
  15. private static float $lastCheckedAt = 0.0;
  16. /**
  17. * Intervalo mínimo entre validações de "liveness" da conexão.
  18. * Dentro de uma mesma requisição (que faz várias queries em sequência),
  19. * evita repetir o round-trip de "SELECT 1" a cada chamada de pdo().
  20. */
  21. private const HEALTHCHECK_INTERVAL_SECONDS = 2.0;
  22. public static function pdo(): \PDO
  23. {
  24. if (self::$connection instanceof \PDO) {
  25. $now = microtime(true);
  26. // Só revalida a conexão se passou o intervalo mínimo desde a
  27. // última checagem; caso contrário reaproveita direto.
  28. if ($now - self::$lastCheckedAt < self::HEALTHCHECK_INTERVAL_SECONDS) {
  29. return self::$connection;
  30. }
  31. if (self::isAlive(self::$connection)) {
  32. self::$lastCheckedAt = $now;
  33. return self::$connection;
  34. }
  35. }
  36. self::$connection = self::connect();
  37. self::$lastCheckedAt = microtime(true);
  38. return self::$connection;
  39. }
  40. /**
  41. * Verifica se a conexão em cache ainda está utilizável.
  42. * Se o banco reiniciou ou derrubou a conexão, força a reconexão.
  43. */
  44. private static function isAlive(\PDO $pdo): bool
  45. {
  46. try {
  47. $pdo->query('SELECT 1');
  48. return true;
  49. } catch (\PDOException $e) {
  50. Logger::warning('Database connection lost, reconnecting', ['error' => $e->getMessage()]);
  51. return false;
  52. }
  53. }
  54. private static function connect(): \PDO
  55. {
  56. $host = self::env('DB_HOST', '127.0.0.1');
  57. $port = self::env('DB_PORT', '5432');
  58. $name = $_ENV['DB_NAME'] ?? '';
  59. $user = $_ENV['DB_USER'] ?? '';
  60. $pass = $_ENV['DB_PASS'] ?? '';
  61. if ($name === '') {
  62. Logger::error('DB_NAME is not configured');
  63. throw new \RuntimeException('DB_NAME is not configured.');
  64. }
  65. if (($_ENV['DB_USER'] ?? '') === '') {
  66. Logger::warning('DB_USER is empty; using empty database user');
  67. }
  68. $dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $host, $port, $name);
  69. try {
  70. $pdo = new \PDO($dsn, $user, $pass, [
  71. \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
  72. \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
  73. \PDO::ATTR_PERSISTENT => true,
  74. // Emula prepares no cliente: evita o round-trip extra de
  75. // Parse/Describe server-side a cada query. Com o banco remoto
  76. // (~160ms de latência por ida-e-volta), isso reduz o custo de
  77. // cada query de ~3 round-trips para 1.
  78. \PDO::ATTR_EMULATE_PREPARES => true,
  79. ]);
  80. } catch (\PDOException $e) {
  81. Logger::error('Failed to connect to database', [
  82. 'host' => $host,
  83. 'port' => $port,
  84. 'name' => $name,
  85. 'error' => $e->getMessage(),
  86. ]);
  87. throw $e;
  88. }
  89. Logger::info('Database connection established', ['host' => $host, 'port' => $port, 'name' => $name]);
  90. return $pdo;
  91. }
  92. /**
  93. * Lê uma variável de ambiente registrando em log quando cai no valor default.
  94. * Torna visível o uso silencioso de defaults (ex.: host/porta ausentes).
  95. */
  96. private static function env(string $key, string $default): string
  97. {
  98. $value = $_ENV[$key] ?? null;
  99. if ($value === null || $value === '') {
  100. Logger::warning('Environment variable missing, using default', ['key' => $key, 'default' => $default]);
  101. return $default;
  102. }
  103. return (string) $value;
  104. }
  105. }