Skip to content

Commit 3cf70c6

Browse files
authored
Merge pull request #190 from clue-labs/good-connection
Refactor to move command queue to `MysqlClient` and connection logic to `Connection` class
2 parents bc5ecf3 + 836ca2d commit 3cf70c6

File tree

6 files changed

+1874
-404
lines changed

6 files changed

+1874
-404
lines changed

src/Io/Connection.php

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace React\Mysql\Io;
44

55
use Evenement\EventEmitter;
6+
use React\EventLoop\LoopInterface;
67
use React\Mysql\Commands\CommandInterface;
78
use React\Mysql\Commands\PingCommand;
89
use React\Mysql\Commands\QueryCommand;
@@ -29,30 +30,66 @@ class Connection extends EventEmitter
2930
private $executor;
3031

3132
/**
32-
* @var integer
33+
* @var int one of the state constants (may change, but should be used readonly from outside)
34+
* @see self::STATE_*
3335
*/
34-
private $state = self::STATE_AUTHENTICATED;
36+
public $state = self::STATE_AUTHENTICATED;
3537

3638
/**
3739
* @var SocketConnectionInterface
3840
*/
3941
private $stream;
4042

43+
/** @var Parser */
44+
private $parser;
45+
46+
/** @var LoopInterface */
47+
private $loop;
48+
49+
/** @var float */
50+
private $idlePeriod = 0.001;
51+
52+
/** @var ?\React\EventLoop\TimerInterface */
53+
private $idleTimer;
54+
55+
/** @var int */
56+
private $pending = 0;
57+
4158
/**
4259
* Connection constructor.
4360
*
4461
* @param SocketConnectionInterface $stream
4562
* @param Executor $executor
63+
* @param Parser $parser
64+
* @param LoopInterface $loop
65+
* @param ?float $idlePeriod
4666
*/
47-
public function __construct(SocketConnectionInterface $stream, Executor $executor)
67+
public function __construct(SocketConnectionInterface $stream, Executor $executor, Parser $parser, LoopInterface $loop, $idlePeriod)
4868
{
4969
$this->stream = $stream;
5070
$this->executor = $executor;
71+
$this->parser = $parser;
72+
73+
$this->loop = $loop;
74+
if ($idlePeriod !== null) {
75+
$this->idlePeriod = $idlePeriod;
76+
}
5177

5278
$stream->on('error', [$this, 'handleConnectionError']);
5379
$stream->on('close', [$this, 'handleConnectionClosed']);
5480
}
5581

82+
/**
83+
* busy executing some command such as query or ping
84+
*
85+
* @return bool
86+
* @throws void
87+
*/
88+
public function isBusy()
89+
{
90+
return $this->parser->isBusy() || !$this->executor->isIdle();
91+
}
92+
5693
/**
5794
* {@inheritdoc}
5895
*/
@@ -71,6 +108,7 @@ public function query($sql, array $params = [])
71108
return \React\Promise\reject($e);
72109
}
73110

111+
$this->awake();
74112
$deferred = new Deferred();
75113

76114
// store all result set rows until result set end
@@ -86,11 +124,13 @@ public function query($sql, array $params = [])
86124

87125
$rows = [];
88126

127+
$this->idle();
89128
$deferred->resolve($result);
90129
});
91130

92131
// resolve / reject status reply (response without result set)
93132
$command->on('error', function ($error) use ($deferred) {
133+
$this->idle();
94134
$deferred->reject($error);
95135
});
96136
$command->on('success', function () use ($command, $deferred) {
@@ -99,6 +139,7 @@ public function query($sql, array $params = [])
99139
$result->insertId = $command->insertId;
100140
$result->warningCount = $command->warningCount;
101141

142+
$this->idle();
102143
$deferred->resolve($result);
103144
});
104145

@@ -115,20 +156,30 @@ public function queryStream($sql, $params = [])
115156
$command = new QueryCommand();
116157
$command->setQuery($query);
117158
$this->_doCommand($command);
159+
$this->awake();
160+
161+
$stream = new QueryStream($command, $this->stream);
162+
$stream->on('close', function () {
163+
$this->idle();
164+
});
118165

119-
return new QueryStream($command, $this->stream);
166+
return $stream;
120167
}
121168

122169
public function ping()
123170
{
124171
return new Promise(function ($resolve, $reject) {
125-
$this->_doCommand(new PingCommand())
126-
->on('error', function ($reason) use ($reject) {
127-
$reject($reason);
128-
})
129-
->on('success', function () use ($resolve) {
130-
$resolve(null);
131-
});
172+
$command = $this->_doCommand(new PingCommand());
173+
$this->awake();
174+
175+
$command->on('success', function () use ($resolve) {
176+
$this->idle();
177+
$resolve(null);
178+
});
179+
$command->on('error', function ($reason) use ($reject) {
180+
$this->idle();
181+
$reject($reason);
182+
});
132183
});
133184
}
134185

@@ -137,6 +188,10 @@ public function quit()
137188
return new Promise(function ($resolve, $reject) {
138189
$command = $this->_doCommand(new QuitCommand());
139190
$this->state = self::STATE_CLOSING;
191+
192+
// mark connection as "awake" until it is closed, so never "idle"
193+
$this->awake();
194+
140195
$command->on('success', function () use ($resolve) {
141196
$resolve(null);
142197
$this->close();
@@ -158,6 +213,11 @@ public function close()
158213
$remoteClosed = $this->stream->isReadable() === false && $this->stream->isWritable() === false;
159214
$this->stream->close();
160215

216+
if ($this->idleTimer !== null) {
217+
$this->loop->cancelTimer($this->idleTimer);
218+
$this->idleTimer = null;
219+
}
220+
161221
// reject all pending commands if connection is closed
162222
while (!$this->executor->isIdle()) {
163223
$command = $this->executor->dequeue();
@@ -223,4 +283,29 @@ protected function _doCommand(CommandInterface $command)
223283

224284
return $this->executor->enqueue($command);
225285
}
286+
287+
private function awake()
288+
{
289+
++$this->pending;
290+
291+
if ($this->idleTimer !== null) {
292+
$this->loop->cancelTimer($this->idleTimer);
293+
$this->idleTimer = null;
294+
}
295+
}
296+
297+
private function idle()
298+
{
299+
--$this->pending;
300+
301+
if ($this->pending < 1 && $this->idlePeriod >= 0 && $this->state === self::STATE_AUTHENTICATED) {
302+
$this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () {
303+
// soft-close connection and emit close event afterwards both on success or on error
304+
$this->idleTimer = null;
305+
$this->quit()->then(null, function () {
306+
// ignore to avoid reporting unhandled rejection
307+
});
308+
});
309+
}
310+
}
226311
}

src/Io/Factory.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,12 @@ public function createConnection(
210210
$connecting->cancel();
211211
});
212212

213-
$connecting->then(function (SocketConnectionInterface $stream) use ($authCommand, $deferred, $uri) {
213+
$idlePeriod = isset($args['idle']) ? (float) $args['idle'] : null;
214+
$connecting->then(function (SocketConnectionInterface $stream) use ($authCommand, $deferred, $uri, $idlePeriod) {
214215
$executor = new Executor();
215216
$parser = new Parser($stream, $executor);
216217

217-
$connection = new Connection($stream, $executor);
218+
$connection = new Connection($stream, $executor, $parser, $this->loop, $idlePeriod);
218219
$command = $executor->enqueue($authCommand);
219220
$parser->start();
220221

src/Io/Parser.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ public function __construct(DuplexStreamInterface $stream, Executor $executor)
115115
});
116116
}
117117

118+
/**
119+
* busy executing some command such as query or ping
120+
*
121+
* @return bool
122+
* @throws void
123+
*/
124+
public function isBusy()
125+
{
126+
return $this->currCommand !== null;
127+
}
128+
118129
public function start()
119130
{
120131
$this->stream->on('data', [$this, 'handleData']);

0 commit comments

Comments
 (0)