Pool.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. <?php
  2. /**
  3. * This file is part of workerman.
  4. *
  5. * Licensed under The MIT License
  6. * For full copyright and license information, please see the MIT-LICENSE.txt
  7. * Redistributions of files must retain the above copyright notice.
  8. *
  9. * @author walkor<walkor@workerman.net>
  10. * @copyright walkor<walkor@workerman.net>
  11. * @link http://www.workerman.net/
  12. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  13. */
  14. declare(strict_types=1);
  15. namespace Workerman\Coroutine;
  16. use Closure;
  17. use Psr\Log\LoggerInterface;
  18. use stdClass;
  19. use Throwable;
  20. use WeakMap;
  21. use Workerman\Coroutine;
  22. use Workerman\Coroutine\Exception\PoolException;
  23. use Workerman\Coroutine\Utils\DestructionWatcher;
  24. use Workerman\Timer;
  25. use Workerman\Worker;
  26. /**
  27. * Class Pool
  28. */
  29. class Pool implements PoolInterface
  30. {
  31. /**
  32. * @var Channel
  33. */
  34. protected Channel $channel;
  35. /**
  36. * @var int
  37. */
  38. protected int $minConnections = 1;
  39. /**
  40. * @var WeakMap
  41. */
  42. protected WeakMap $connections;
  43. /**
  44. * @var ?object
  45. */
  46. protected ?object $nonCoroutineConnection = null;
  47. /**
  48. * @var WeakMap
  49. */
  50. protected WeakMap $lastUsedTimes;
  51. /**
  52. * @var WeakMap
  53. */
  54. protected WeakMap $lastHeartbeatTimes;
  55. /**
  56. * @var Closure|null
  57. */
  58. protected ?Closure $connectionCreateHandler = null;
  59. /**
  60. * @var Closure|null
  61. */
  62. protected ?Closure $connectionDestroyHandler = null;
  63. /**
  64. * @var Closure|null
  65. */
  66. protected ?Closure $connectionHeartbeatHandler = null;
  67. /**
  68. * @var float
  69. */
  70. protected float $idleTimeout = 60;
  71. /**
  72. * @var float
  73. */
  74. protected float $heartbeatInterval = 50;
  75. /**
  76. * @var float
  77. */
  78. protected float $waitTimeout = 10;
  79. /**
  80. * @var LoggerInterface|Closure|null
  81. */
  82. protected LoggerInterface|Closure|null $logger = null;
  83. /**
  84. * @var array|string[]
  85. */
  86. private array $configurableProperties = [
  87. 'minConnections',
  88. 'idleTimeout',
  89. 'heartbeatInterval',
  90. 'waitTimeout',
  91. ];
  92. /**
  93. * Constructor.
  94. *
  95. * @param int $maxConnections
  96. * @param array $config
  97. */
  98. public function __construct(protected int $maxConnections = 1, protected array $config = [])
  99. {
  100. foreach ($config as $key => $value) {
  101. $camelCaseKey = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key))));
  102. if (in_array($camelCaseKey, $this->configurableProperties, true)) {
  103. $this->$camelCaseKey = $value;
  104. }
  105. }
  106. $this->channel = new Channel($maxConnections);
  107. $this->lastUsedTimes = new WeakMap();
  108. $this->lastHeartbeatTimes = new WeakMap();
  109. $this->connections = new WeakMap();
  110. if (Worker::isRunning()) {
  111. Timer::repeat(1, function () {
  112. $this->checkConnections();
  113. });
  114. }
  115. }
  116. /**
  117. * Set the connection creator.
  118. *
  119. * @param callable $connectionCreateHandler
  120. * @return $this
  121. */
  122. public function setConnectionCreator(callable $connectionCreateHandler): self
  123. {
  124. $this->connectionCreateHandler = $connectionCreateHandler;
  125. return $this;
  126. }
  127. /**
  128. * Set the connection closer.
  129. *
  130. * @param callable $connectionDestroyHandler
  131. * @return $this
  132. */
  133. public function setConnectionCloser(callable $connectionDestroyHandler): self
  134. {
  135. $this->connectionDestroyHandler = $connectionDestroyHandler;
  136. return $this;
  137. }
  138. /**
  139. * Set the connection heartbeat checker.
  140. *
  141. * @param callable $connectionHeartbeatHandler
  142. * @return $this
  143. */
  144. public function setHeartbeatChecker(callable $connectionHeartbeatHandler): self
  145. {
  146. $this->connectionHeartbeatHandler = $connectionHeartbeatHandler;
  147. return $this;
  148. }
  149. /**
  150. * Get connection.
  151. *
  152. * @return object
  153. * @throws Throwable
  154. */
  155. public function get(): object
  156. {
  157. if (!Coroutine::isCoroutine()) {
  158. if (!$this->nonCoroutineConnection) {
  159. $this->nonCoroutineConnection = $this->createConnection();
  160. }
  161. return $this->nonCoroutineConnection;
  162. }
  163. $num = $this->channel->length();
  164. if ($num === 0 && $this->getConnectionCount() < $this->maxConnections) {
  165. return $this->createConnection();
  166. }
  167. $connection = $this->channel->pop($this->waitTimeout);
  168. if (!$connection) {
  169. throw new PoolException("Failed to get a connection from the pool within the wait timeout ($this->waitTimeout seconds). The connection pool is exhausted.");
  170. }
  171. $this->lastUsedTimes[$connection] = time();
  172. return $connection;
  173. }
  174. /**
  175. * Put connection to pool.
  176. *
  177. * @param object $connection
  178. * @return void
  179. * @throws Throwable
  180. */
  181. public function put(object $connection): void
  182. {
  183. // This connection does not belong to the connection pool.
  184. // It may have been closed by $this->closeConnection($connection).
  185. if (!isset($this->connections[$connection])) {
  186. throw new PoolException('The connection does not belong to the connection pool.');
  187. }
  188. if ($connection === $this->nonCoroutineConnection) {
  189. return;
  190. }
  191. try {
  192. $this->channel->push($connection);
  193. } catch (Throwable $throwable) {
  194. $this->closeConnection($connection);
  195. throw $throwable;
  196. }
  197. }
  198. /**
  199. * Check if the connection is valid.
  200. *
  201. * @param $connection
  202. * @return bool
  203. */
  204. protected function isValidConnection($connection): bool
  205. {
  206. return is_object($connection);
  207. }
  208. /**
  209. * Create connection.
  210. *
  211. * @return object
  212. * @throws Throwable
  213. */
  214. public function createConnection(): object
  215. {
  216. if ($this->getConnectionCount() >= $this->maxConnections) {
  217. throw new PoolException('CreateConnection failed, maximum connection limit reached.');
  218. }
  219. // Create a placeholder to ensure the correct value of getConnectionCount().
  220. $placeholder = new stdClass;
  221. $this->connections[$placeholder] = 0;
  222. try {
  223. // Coroutines will switch here, so we need $placeholder to ensure the correct value of getConnectionCount().
  224. $connection = ($this->connectionCreateHandler)();
  225. if (!$this->isValidConnection($connection)) {
  226. throw new PoolException('CreateConnection failed, expected a connection object, but got ' . gettype($connection) . '.');
  227. }
  228. unset($this->connections[$placeholder]);
  229. $this->connections[$connection] = $this->lastUsedTimes[$connection] = $this->lastHeartbeatTimes[$connection] = time();
  230. } catch (Throwable $throwable) {
  231. unset($this->connections[$placeholder]);
  232. throw $throwable;
  233. }
  234. return $connection;
  235. }
  236. /**
  237. * Close the connection and remove the connection from the connection pool.
  238. *
  239. * @param object $connection
  240. * @return void
  241. */
  242. public function closeConnection(object $connection): void
  243. {
  244. if (!isset($this->connections[$connection])) {
  245. return;
  246. }
  247. // Mark this connection as no longer belonging to the connection pool.
  248. unset($this->lastUsedTimes[$connection], $this->lastHeartbeatTimes[$connection], $this->connections[$connection]);
  249. if ($this->nonCoroutineConnection === $connection) {
  250. $this->nonCoroutineConnection = null;
  251. }
  252. if (!$this->connectionDestroyHandler) {
  253. return;
  254. }
  255. try {
  256. ($this->connectionDestroyHandler)($connection);
  257. } catch (Throwable $throwable) {
  258. $this->log($throwable);
  259. }
  260. }
  261. /**
  262. * Cleanup idle connections.
  263. *
  264. * @return void
  265. */
  266. protected function checkConnections(): void
  267. {
  268. $num = $this->channel->length();
  269. $time = time();
  270. for($i = $num; $i > 0; $i--) {
  271. $connection = $this->channel->pop(0.001);
  272. if (!$connection) {
  273. return;
  274. }
  275. $lastUsedTime = $this->lastUsedTimes[$connection];
  276. if ($time - $lastUsedTime > $this->idleTimeout && $this->channel->length() >= $this->minConnections) {
  277. $this->closeConnection($connection);
  278. continue;
  279. }
  280. $this->trySendHeartbeat($connection) && $this->channel->push($connection);
  281. }
  282. if ($this->nonCoroutineConnection) {
  283. $this->trySendHeartbeat($this->nonCoroutineConnection);
  284. }
  285. }
  286. /**
  287. * Try to send heartbeat.
  288. *
  289. * @param $connection
  290. * @return bool
  291. */
  292. private function trySendHeartbeat($connection): bool
  293. {
  294. $lastHeartbeatTime = $this->lastHeartbeatTimes[$connection] ?? 0;
  295. $time = time();
  296. if ($this->connectionHeartbeatHandler && $time - $lastHeartbeatTime >= $this->heartbeatInterval) {
  297. try {
  298. ($this->connectionHeartbeatHandler)($connection);
  299. $this->lastHeartbeatTimes[$connection] = $time;
  300. } catch (Throwable $throwable) {
  301. $this->log($throwable);
  302. $this->closeConnection($connection);
  303. return false;
  304. }
  305. }
  306. return true;
  307. }
  308. /**
  309. * Get the number of connections in the connection pool.
  310. *
  311. * @return int
  312. */
  313. public function getConnectionCount(): int
  314. {
  315. return count($this->connections);
  316. }
  317. /**
  318. * Close connections.
  319. *
  320. * @return void
  321. */
  322. public function closeConnections(): void
  323. {
  324. $num = $this->channel->length();
  325. for ($i = $num; $i > 0; $i--) {
  326. $connection = $this->channel->pop(0.001);
  327. if (!$connection) {
  328. return;
  329. }
  330. $this->closeConnection($connection);
  331. }
  332. $this->nonCoroutineConnection && $this->closeConnection($this->nonCoroutineConnection);
  333. }
  334. /**
  335. * Log.
  336. *
  337. * @param $message
  338. * @return void
  339. */
  340. protected function log($message): void
  341. {
  342. if (!$this->logger) {
  343. echo $message . PHP_EOL;
  344. return;
  345. }
  346. if ($this->logger instanceof Closure) {
  347. ($this->logger)($message);
  348. return;
  349. }
  350. $this->logger->info((string)$message);
  351. }
  352. }