66use React \EventLoop \LoopInterface ;
77use React \Mysql \Io \Connection ;
88use React \Mysql \Io \Factory ;
9+ use React \Promise \Deferred ;
910use React \Promise \Promise ;
1011use React \Socket \ConnectorInterface ;
1112use React \Stream \ReadableStreamInterface ;
@@ -58,6 +59,13 @@ class MysqlClient extends EventEmitter
5859 /** @var ?Connection */
5960 private $ connection ;
6061
62+ /**
63+ * array of outstanding connection requests to send next commands once a connection becomes ready
64+ *
65+ * @var array<int,Deferred<Connection>>
66+ */
67+ private $ pending = [];
68+
6169 /**
6270 * set to true only between calling `quit()` and the connection closing in response
6371 *
@@ -77,44 +85,6 @@ public function __construct(
7785 $ this ->uri = $ uri ;
7886 }
7987
80- /**
81- * @return PromiseInterface<Connection>
82- */
83- private function getConnection ()
84- {
85- // happy path: reuse existing connection unless it is already closing after an idle timeout
86- if ($ this ->connection !== null && ($ this ->quitting || $ this ->connection ->state !== Connection::STATE_CLOSING )) {
87- return \React \Promise \resolve ($ this ->connection );
88- }
89-
90- if ($ this ->connecting !== null ) {
91- return $ this ->connecting ;
92- }
93-
94- // force-close connection if still waiting for previous disconnection
95- if ($ this ->connection !== null ) {
96- assert ($ this ->connection ->state === Connection::STATE_CLOSING );
97- $ this ->connection ->close ();
98- }
99-
100- // create new connection if not already connected or connecting
101- $ this ->connecting = $ connecting = $ this ->factory ->createConnection ($ this ->uri );
102- $ this ->connecting ->then (function (Connection $ connection ) {
103- $ this ->connection = $ connection ;
104- $ this ->connecting = null ;
105-
106- // connection completed => remember only until closed
107- $ connection ->on ('close ' , function () {
108- $ this ->connection = null ;
109- });
110- }, function () {
111- // connection failed => discard connection attempt
112- $ this ->connecting = null ;
113- });
114-
115- return $ connecting ;
116- }
117-
11888 /**
11989 * Performs an async query.
12090 *
@@ -176,12 +146,18 @@ private function getConnection()
176146 */
177147 public function query ($ sql , array $ params = [])
178148 {
179- if ($ this ->closed ) {
149+ if ($ this ->closed || $ this -> quitting ) {
180150 return \React \Promise \reject (new Exception ('Connection closed ' ));
181151 }
182152
183153 return $ this ->getConnection ()->then (function (Connection $ connection ) use ($ sql , $ params ) {
184- return $ connection ->query ($ sql , $ params );
154+ return $ connection ->query ($ sql , $ params )->then (function (MysqlResult $ result ) use ($ connection ) {
155+ $ this ->handleConnectionReady ($ connection );
156+ return $ result ;
157+ }, function (\Exception $ e ) use ($ connection ) {
158+ $ this ->handleConnectionReady ($ connection );
159+ throw $ e ;
160+ });
185161 });
186162 }
187163
@@ -246,13 +222,22 @@ public function query($sql, array $params = [])
246222 */
247223 public function queryStream ($ sql , $ params = [])
248224 {
249- if ($ this ->closed ) {
225+ if ($ this ->closed || $ this -> quitting ) {
250226 throw new Exception ('Connection closed ' );
251227 }
252228
253229 return \React \Promise \Stream \unwrapReadable (
254230 $ this ->getConnection ()->then (function (Connection $ connection ) use ($ sql , $ params ) {
255- return $ connection ->queryStream ($ sql , $ params );
231+ $ stream = $ connection ->queryStream ($ sql , $ params );
232+
233+ $ stream ->on ('end ' , function () use ($ connection ) {
234+ $ this ->handleConnectionReady ($ connection );
235+ });
236+ $ stream ->on ('error ' , function () use ($ connection ) {
237+ $ this ->handleConnectionReady ($ connection );
238+ });
239+
240+ return $ stream ;
256241 })
257242 );
258243 }
@@ -279,12 +264,17 @@ public function queryStream($sql, $params = [])
279264 */
280265 public function ping ()
281266 {
282- if ($ this ->closed ) {
267+ if ($ this ->closed || $ this -> quitting ) {
283268 return \React \Promise \reject (new Exception ('Connection closed ' ));
284269 }
285270
286271 return $ this ->getConnection ()->then (function (Connection $ connection ) {
287- return $ connection ->ping ();
272+ return $ connection ->ping ()->then (function () use ($ connection ) {
273+ $ this ->handleConnectionReady ($ connection );
274+ }, function (\Exception $ e ) use ($ connection ) {
275+ $ this ->handleConnectionReady ($ connection );
276+ throw $ e ;
277+ });
288278 });
289279 }
290280
@@ -312,7 +302,7 @@ public function ping()
312302 */
313303 public function quit ()
314304 {
315- if ($ this ->closed ) {
305+ if ($ this ->closed || $ this -> quitting ) {
316306 return \React \Promise \reject (new Exception ('Connection closed ' ));
317307 }
318308
@@ -379,7 +369,77 @@ public function close()
379369 $ this ->connecting = null ;
380370 }
381371
372+ // clear all outstanding commands
373+ foreach ($ this ->pending as $ deferred ) {
374+ $ deferred ->reject (new \RuntimeException ('Connection closed ' ));
375+ }
376+ $ this ->pending = [];
377+
382378 $ this ->emit ('close ' );
383379 $ this ->removeAllListeners ();
384380 }
381+
382+
383+ /**
384+ * @return PromiseInterface<Connection>
385+ */
386+ private function getConnection ()
387+ {
388+ $ deferred = new Deferred ();
389+
390+ // force-close connection if still waiting for previous disconnection due to idle timer
391+ if ($ this ->connection !== null && $ this ->connection ->state === Connection::STATE_CLOSING ) {
392+ $ this ->connection ->close ();
393+ $ this ->connection = null ;
394+ }
395+
396+ // happy path: reuse existing connection unless it is currently busy executing another command
397+ if ($ this ->connection !== null && !$ this ->connection ->isBusy ()) {
398+ $ deferred ->resolve ($ this ->connection );
399+ return $ deferred ->promise ();
400+ }
401+
402+ // queue pending connection request until connection becomes ready
403+ $ this ->pending [] = $ deferred ;
404+
405+ // create new connection if not already connected or connecting
406+ if ($ this ->connection === null && $ this ->connecting === null ) {
407+ $ this ->connecting = $ this ->factory ->createConnection ($ this ->uri );
408+ $ this ->connecting ->then (function (Connection $ connection ) {
409+ // connection completed => remember only until closed
410+ $ this ->connecting = null ;
411+ $ this ->connection = $ connection ;
412+ $ connection ->on ('close ' , function () {
413+ $ this ->connection = null ;
414+ });
415+
416+ // handle first command from queue when connection is ready
417+ $ this ->handleConnectionReady ($ connection );
418+ }, function (\Exception $ e ) {
419+ // connection failed => discard connection attempt
420+ $ this ->connecting = null ;
421+
422+ foreach ($ this ->pending as $ key => $ deferred ) {
423+ $ deferred ->reject ($ e );
424+ unset($ this ->pending [$ key ]);
425+ }
426+ });
427+ }
428+
429+ return $ deferred ->promise ();
430+ }
431+
432+ private function handleConnectionReady (Connection $ connection )
433+ {
434+ $ deferred = \reset ($ this ->pending );
435+ if ($ deferred === false ) {
436+ // nothing to do if there are no outstanding connection requests
437+ return ;
438+ }
439+
440+ assert ($ deferred instanceof Deferred);
441+ unset($ this ->pending [\key ($ this ->pending )]);
442+
443+ $ deferred ->resolve ($ connection );
444+ }
385445}
0 commit comments