Skip to content

Commit

Permalink
Force-close previous (dis)connection when creating new connection
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Nov 11, 2018
1 parent 160b3f2 commit 472ae14
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 2 deletions.
30 changes: 29 additions & 1 deletion src/Io/LazyConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ class LazyConnection extends EventEmitter implements ConnectionInterface
private $closed = false;
private $busy = false;

/**
* @var ConnectionInterface|null
*/
private $disconnecting;

private $loop;
private $idlePeriod = 60.0;
private $idleTimer;
Expand All @@ -45,6 +50,12 @@ private function connecting()
return $this->connecting;
}

// force-close connection if still waiting for previous disconnection
if ($this->disconnecting !== null) {
$this->disconnecting->close();
$this->disconnecting = null;
}

$this->connecting = $connecting = $this->factory->createConnection($this->uri);
$this->connecting->then(function (ConnectionInterface $connection) {
// connection completed => remember only until closed
Expand Down Expand Up @@ -81,7 +92,18 @@ private function idle()
if ($this->pending < 1 && $this->idlePeriod >= 0) {
$this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () {
$this->connecting->then(function (ConnectionInterface $connection) {
$connection->quit();
$this->disconnecting = $connection;
$connection->quit()->then(
function () {
// successfully disconnected => remove reference
$this->disconnecting = null;
},
function () use ($connection) {
// soft-close failed => force-close connection
$connection->close();
$this->disconnecting = null;
}
);
});
$this->connecting = null;
$this->idleTimer = null;
Expand Down Expand Up @@ -184,6 +206,12 @@ public function close()

$this->closed = true;

// force-close connection if still waiting for previous disconnection
if ($this->disconnecting !== null) {
$this->disconnecting->close();
$this->disconnecting = null;
}

// either close active connection or cancel pending connection attempt
if ($this->connecting !== null) {
$this->connecting->then(function (ConnectionInterface $connection) {
Expand Down
110 changes: 109 additions & 1 deletion tests/Io/LazyConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ public function testPingWillNotForwardErrorFromUnderlyingConnection()

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

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

public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWhenQuitFails()
{
$base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(array('ping', 'quit', 'close'))->disableOriginalConstructor()->getMock();
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
$base->expects($this->once())->method('quit')->willReturn(\React\Promise\reject());
$base->expects($this->once())->method('close');

$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
$factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base));

$timeout = null;
$timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
$loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) {
$timeout = $cb;
return true;
}))->willReturn($timer);

$connection = new LazyConnection($factory, '', $loop);

$connection->on('close', $this->expectCallableNever());

$connection->ping();

$this->assertNotNull($timeout);
$timeout();
}

public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatingSecondConnection()
{
$base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(array('ping', 'quit', 'close'))->disableOriginalConstructor()->getMock();
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
$base->expects($this->once())->method('quit')->willReturn(new Promise(function () { }));
$base->expects($this->once())->method('close');

$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
$factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls(
\React\Promise\resolve($base),
new Promise(function () { })
);

$timeout = null;
$timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
$loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) {
$timeout = $cb;
return true;
}))->willReturn($timer);

$connection = new LazyConnection($factory, '', $loop);

$connection->on('close', $this->expectCallableNever());

$connection->ping();

$this->assertNotNull($timeout);
$timeout();

$connection->ping();
}


public function testQueryReturnsPendingPromiseAndWillNotStartTimerWhenConnectionIsPending()
{
$deferred = new Deferred();
Expand Down Expand Up @@ -615,6 +678,51 @@ public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectio
$connection->close();
}

public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuitIsStillPending()
{
$base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock();
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
$base->expects($this->once())->method('quit')->willReturn(new Promise(function () { }));
$base->expects($this->once())->method('close');

$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
$factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base));
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
$connection = new LazyConnection($factory, '', $loop);

$connection->ping();
$connection->quit();
$connection->close();
}

public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionWhenQuitIsStillPending()
{
$base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock();
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
$base->expects($this->once())->method('quit')->willReturn(new Promise(function () { }));
$base->expects($this->once())->method('close');

$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
$factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base));

$timeout = null;
$timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
$loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) {
$timeout = $cb;
return true;
}))->willReturn($timer);

$connection = new LazyConnection($factory, '', $loop);

$connection->ping();

$this->assertNotNull($timeout);
$timeout();

$connection->close();
}

public function testCloseTwiceAfterPingEmitsCloseEventOnceWhenConnectionIsPending()
{
$promise = new Promise(function () { });
Expand Down

0 comments on commit 472ae14

Please sign in to comment.