Skip to content

Commit 472ae14

Browse files
committed
Force-close previous (dis)connection when creating new connection
1 parent 160b3f2 commit 472ae14

File tree

2 files changed

+138
-2
lines changed

2 files changed

+138
-2
lines changed

src/Io/LazyConnection.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ class LazyConnection extends EventEmitter implements ConnectionInterface
2121
private $closed = false;
2222
private $busy = false;
2323

24+
/**
25+
* @var ConnectionInterface|null
26+
*/
27+
private $disconnecting;
28+
2429
private $loop;
2530
private $idlePeriod = 60.0;
2631
private $idleTimer;
@@ -45,6 +50,12 @@ private function connecting()
4550
return $this->connecting;
4651
}
4752

53+
// force-close connection if still waiting for previous disconnection
54+
if ($this->disconnecting !== null) {
55+
$this->disconnecting->close();
56+
$this->disconnecting = null;
57+
}
58+
4859
$this->connecting = $connecting = $this->factory->createConnection($this->uri);
4960
$this->connecting->then(function (ConnectionInterface $connection) {
5061
// connection completed => remember only until closed
@@ -81,7 +92,18 @@ private function idle()
8192
if ($this->pending < 1 && $this->idlePeriod >= 0) {
8293
$this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () {
8394
$this->connecting->then(function (ConnectionInterface $connection) {
84-
$connection->quit();
95+
$this->disconnecting = $connection;
96+
$connection->quit()->then(
97+
function () {
98+
// successfully disconnected => remove reference
99+
$this->disconnecting = null;
100+
},
101+
function () use ($connection) {
102+
// soft-close failed => force-close connection
103+
$connection->close();
104+
$this->disconnecting = null;
105+
}
106+
);
85107
});
86108
$this->connecting = null;
87109
$this->idleTimer = null;
@@ -184,6 +206,12 @@ public function close()
184206

185207
$this->closed = true;
186208

209+
// force-close connection if still waiting for previous disconnection
210+
if ($this->disconnecting !== null) {
211+
$this->disconnecting->close();
212+
$this->disconnecting = null;
213+
}
214+
187215
// either close active connection or cancel pending connection attempt
188216
if ($this->connecting !== null) {
189217
$this->connecting->then(function (ConnectionInterface $connection) {

tests/Io/LazyConnectionTest.php

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ public function testPingWillNotForwardErrorFromUnderlyingConnection()
8686

8787
public function testPingFollowedByIdleTimerWillQuitUnderlyingConnection()
8888
{
89-
$base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(array('ping', 'quit'))->disableOriginalConstructor()->getMock();
89+
$base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(array('ping', 'quit', 'close'))->disableOriginalConstructor()->getMock();
9090
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
9191
$base->expects($this->once())->method('quit')->willReturn(\React\Promise\resolve());
92+
$base->expects($this->never())->method('close');
9293

9394
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
9495
$factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base));
@@ -111,6 +112,68 @@ public function testPingFollowedByIdleTimerWillQuitUnderlyingConnection()
111112
$timeout();
112113
}
113114

115+
public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWhenQuitFails()
116+
{
117+
$base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(array('ping', 'quit', 'close'))->disableOriginalConstructor()->getMock();
118+
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
119+
$base->expects($this->once())->method('quit')->willReturn(\React\Promise\reject());
120+
$base->expects($this->once())->method('close');
121+
122+
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
123+
$factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base));
124+
125+
$timeout = null;
126+
$timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
127+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
128+
$loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) {
129+
$timeout = $cb;
130+
return true;
131+
}))->willReturn($timer);
132+
133+
$connection = new LazyConnection($factory, '', $loop);
134+
135+
$connection->on('close', $this->expectCallableNever());
136+
137+
$connection->ping();
138+
139+
$this->assertNotNull($timeout);
140+
$timeout();
141+
}
142+
143+
public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatingSecondConnection()
144+
{
145+
$base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(array('ping', 'quit', 'close'))->disableOriginalConstructor()->getMock();
146+
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
147+
$base->expects($this->once())->method('quit')->willReturn(new Promise(function () { }));
148+
$base->expects($this->once())->method('close');
149+
150+
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
151+
$factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls(
152+
\React\Promise\resolve($base),
153+
new Promise(function () { })
154+
);
155+
156+
$timeout = null;
157+
$timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
158+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
159+
$loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) {
160+
$timeout = $cb;
161+
return true;
162+
}))->willReturn($timer);
163+
164+
$connection = new LazyConnection($factory, '', $loop);
165+
166+
$connection->on('close', $this->expectCallableNever());
167+
168+
$connection->ping();
169+
170+
$this->assertNotNull($timeout);
171+
$timeout();
172+
173+
$connection->ping();
174+
}
175+
176+
114177
public function testQueryReturnsPendingPromiseAndWillNotStartTimerWhenConnectionIsPending()
115178
{
116179
$deferred = new Deferred();
@@ -615,6 +678,51 @@ public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectio
615678
$connection->close();
616679
}
617680

681+
public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuitIsStillPending()
682+
{
683+
$base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock();
684+
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
685+
$base->expects($this->once())->method('quit')->willReturn(new Promise(function () { }));
686+
$base->expects($this->once())->method('close');
687+
688+
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
689+
$factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base));
690+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
691+
$connection = new LazyConnection($factory, '', $loop);
692+
693+
$connection->ping();
694+
$connection->quit();
695+
$connection->close();
696+
}
697+
698+
public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionWhenQuitIsStillPending()
699+
{
700+
$base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock();
701+
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
702+
$base->expects($this->once())->method('quit')->willReturn(new Promise(function () { }));
703+
$base->expects($this->once())->method('close');
704+
705+
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
706+
$factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base));
707+
708+
$timeout = null;
709+
$timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
710+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
711+
$loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) {
712+
$timeout = $cb;
713+
return true;
714+
}))->willReturn($timer);
715+
716+
$connection = new LazyConnection($factory, '', $loop);
717+
718+
$connection->ping();
719+
720+
$this->assertNotNull($timeout);
721+
$timeout();
722+
723+
$connection->close();
724+
}
725+
618726
public function testCloseTwiceAfterPingEmitsCloseEventOnceWhenConnectionIsPending()
619727
{
620728
$promise = new Promise(function () { });

0 commit comments

Comments
 (0)