6
6
use React \EventLoop \LoopInterface ;
7
7
use React \Mysql \Io \Connection ;
8
8
use React \Mysql \Io \Factory ;
9
+ use React \Promise \Deferred ;
9
10
use React \Promise \Promise ;
10
11
use React \Socket \ConnectorInterface ;
11
12
use React \Stream \ReadableStreamInterface ;
@@ -58,6 +59,13 @@ class MysqlClient extends EventEmitter
58
59
/** @var ?Connection */
59
60
private $ connection ;
60
61
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
+
61
69
/**
62
70
* set to true only between calling `quit()` and the connection closing in response
63
71
*
@@ -77,44 +85,6 @@ public function __construct(
77
85
$ this ->uri = $ uri ;
78
86
}
79
87
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
-
118
88
/**
119
89
* Performs an async query.
120
90
*
@@ -176,12 +146,18 @@ private function getConnection()
176
146
*/
177
147
public function query ($ sql , array $ params = [])
178
148
{
179
- if ($ this ->closed ) {
149
+ if ($ this ->closed || $ this -> quitting ) {
180
150
return \React \Promise \reject (new Exception ('Connection closed ' ));
181
151
}
182
152
183
153
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
+ });
185
161
});
186
162
}
187
163
@@ -246,13 +222,22 @@ public function query($sql, array $params = [])
246
222
*/
247
223
public function queryStream ($ sql , $ params = [])
248
224
{
249
- if ($ this ->closed ) {
225
+ if ($ this ->closed || $ this -> quitting ) {
250
226
throw new Exception ('Connection closed ' );
251
227
}
252
228
253
229
return \React \Promise \Stream \unwrapReadable (
254
230
$ 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 ;
256
241
})
257
242
);
258
243
}
@@ -279,12 +264,17 @@ public function queryStream($sql, $params = [])
279
264
*/
280
265
public function ping ()
281
266
{
282
- if ($ this ->closed ) {
267
+ if ($ this ->closed || $ this -> quitting ) {
283
268
return \React \Promise \reject (new Exception ('Connection closed ' ));
284
269
}
285
270
286
271
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
+ });
288
278
});
289
279
}
290
280
@@ -312,7 +302,7 @@ public function ping()
312
302
*/
313
303
public function quit ()
314
304
{
315
- if ($ this ->closed ) {
305
+ if ($ this ->closed || $ this -> quitting ) {
316
306
return \React \Promise \reject (new Exception ('Connection closed ' ));
317
307
}
318
308
@@ -379,7 +369,77 @@ public function close()
379
369
$ this ->connecting = null ;
380
370
}
381
371
372
+ // clear all outstanding commands
373
+ foreach ($ this ->pending as $ deferred ) {
374
+ $ deferred ->reject (new \RuntimeException ('Connection closed ' ));
375
+ }
376
+ $ this ->pending = [];
377
+
382
378
$ this ->emit ('close ' );
383
379
$ this ->removeAllListeners ();
384
380
}
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
+ }
385
445
}
0 commit comments