Skip to content

Commit de27f1c

Browse files
committed
Dispose of result faster when fetchRow() is used on PooledResult
1 parent ad61674 commit de27f1c

6 files changed

+137
-78
lines changed

src/CommandResult.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function __construct(
2222
) {
2323
}
2424

25-
final public function getIterator(): \Traversable
25+
final public function getIterator(): \EmptyIterator
2626
{
2727
return new \EmptyIterator;
2828
}

src/PooledResult.php

+65-38
Original file line numberDiff line numberDiff line change
@@ -15,59 +15,80 @@
1515
*/
1616
abstract class PooledResult implements Result, \IteratorAggregate
1717
{
18-
/** @var null|\Closure():void */
19-
private ?\Closure $release;
20-
2118
/** @var Future<TResult|null>|null */
2219
private ?Future $next = null;
2320

21+
/** @var \Iterator<int, array<string, TFieldValue>> */
22+
private readonly \Iterator $iterator;
23+
24+
/**
25+
* @template Tr of Result
26+
*
27+
* @param Tr $result
28+
* @param \Closure():void $release
29+
*
30+
* @return Tr
31+
*/
32+
abstract protected static function newInstanceFrom(Result $result, \Closure $release): Result;
33+
2434
/**
2535
* @param TResult $result Result object created by pooled connection or statement.
2636
* @param \Closure():void $release Callable to be invoked when the result set is destroyed.
2737
*/
28-
public function __construct(private readonly Result $result, \Closure $release)
38+
public function __construct(private readonly Result $result, private readonly \Closure $release)
2939
{
30-
$this->release = $release;
31-
3240
if ($this->result instanceof CommandResult) {
33-
$this->next = $this->fetchNextResult();
41+
$this->iterator = $this->result->getIterator();
42+
$this->next = self::fetchNextResult($this->result, $this->release);
43+
return;
3444
}
45+
46+
$next = &$this->next;
47+
$this->iterator = (static function () use (&$next, $result, $release): \Generator {
48+
try {
49+
yield from $result;
50+
} catch (\Throwable $exception) {
51+
if (!$next) {
52+
EventLoop::queue($release);
53+
}
54+
throw $exception;
55+
}
56+
57+
$next ??= self::fetchNextResult($result, $release);
58+
})();
3559
}
3660

3761
public function __destruct()
3862
{
39-
$this->dispose();
63+
EventLoop::queue(self::dispose(...), $this->iterator);
4064
}
4165

42-
/**
43-
* @param TResult $result
44-
* @param \Closure():void $release
45-
*
46-
* @return TResult
47-
*/
48-
abstract protected function newInstanceFrom(Result $result, \Closure $release): Result;
49-
50-
private function dispose(): void
66+
private static function dispose(\Iterator $iterator): void
5167
{
52-
if ($this->release !== null) {
53-
EventLoop::queue($this->release);
54-
$this->release = null;
68+
try {
69+
// Discard remaining rows in the result set.
70+
while ($iterator->valid()) {
71+
$iterator->next();
72+
}
73+
} catch (\Throwable) {
74+
// Ignore errors while discarding result.
5575
}
5676
}
5777

5878
public function getIterator(): \Traversable
5979
{
60-
try {
61-
yield from $this->result;
62-
} catch (\Throwable $exception) {
63-
$this->dispose();
64-
throw $exception;
65-
}
80+
return $this->iterator;
6681
}
6782

6883
public function fetchRow(): ?array
6984
{
70-
return $this->result->fetchRow();
85+
if (!$this->iterator->valid()) {
86+
return null;
87+
}
88+
89+
$current = $this->iterator->current();
90+
$this->iterator->next();
91+
return $current;
7192
}
7293

7394
public function getRowCount(): ?int
@@ -85,24 +106,30 @@ public function getColumnCount(): ?int
85106
*/
86107
public function getNextResult(): ?Result
87108
{
88-
return ($this->next ??= $this->fetchNextResult())->await();
109+
$this->next ??= self::fetchNextResult($this->result, $this->release);
110+
return $this->next->await();
89111
}
90112

91-
private function fetchNextResult(): Future
113+
/**
114+
* @template Tr of Result
115+
*
116+
* @param Tr $result
117+
* @param \Closure():void $release
118+
*
119+
* @return Future<Tr|null>
120+
*/
121+
private static function fetchNextResult(Result $result, \Closure $release): Future
92122
{
93-
return async(function (): ?Result {
94-
/** @var TResult|null $result */
95-
$result = $this->result->getNextResult();
123+
return async(static function () use ($result, $release): ?Result {
124+
/** @var Tr|null $result */
125+
$result = $result->getNextResult();
96126

97-
if ($result === null || $this->release === null) {
98-
$this->dispose();
127+
if ($result === null) {
128+
EventLoop::queue($release);
99129
return null;
100130
}
101131

102-
$result = $this->newInstanceFrom($result, $this->release);
103-
$this->release = null;
104-
105-
return $result;
132+
return static::newInstanceFrom($result, $release);
106133
});
107134
}
108135
}

test/ConnectionPoolTest.php

+2-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Amp\Future;
66
use Amp\PHPUnit\AsyncTestCase;
77
use Amp\Sql\Common\ConnectionPool;
8-
use Amp\Sql\Common\PooledResult;
8+
use Amp\Sql\Common\Test\Stub\StubPooledResult;
99
use Amp\Sql\Connection;
1010
use Amp\Sql\Result;
1111
use Amp\Sql\SqlConfig;
@@ -67,14 +67,7 @@ private function createPool(SqlConnector $connector, int $maxConnections = 100,
6767
->getMockForAbstractClass();
6868

6969
$pool->method('createResult')
70-
->willReturnCallback(function (Result $result, \Closure $release): PooledResult {
71-
return new class($result, $release) extends PooledResult {
72-
protected function newInstanceFrom(Result $result, \Closure $release): PooledResult
73-
{
74-
return new self($result, $release);
75-
}
76-
};
77-
});
70+
->willReturnCallback(fn (Result $result, \Closure $release) => new StubPooledResult($result, $release));
7871

7972
return $pool;
8073
}

test/PooledResultTest.php

+13-30
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
namespace Amp\Sql\Common\Test;
44

55
use Amp\PHPUnit\AsyncTestCase;
6-
use Amp\Sql\Common\PooledResult;
7-
use Amp\Sql\Result;
6+
use Amp\Sql\Common\Test\Stub\StubPooledResult;
7+
use Amp\Sql\Common\Test\Stub\StubResult;
88
use function Amp\delay;
99

1010
class PooledResultTest extends AsyncTestCase
@@ -17,33 +17,15 @@ public function testIdleConnectionsRemovedAfterTimeout()
1717
$invoked = true;
1818
};
1919

20-
$secondResult = $this->createMock(PooledResult::class);
21-
$secondResult->method('getIterator')
22-
->willReturn(new \ArrayIterator([['column' => 'value']]));
23-
$secondResult->method('getNextResult')
24-
->willReturn(null);
20+
$expectedRow = ['column' => 'value'];
2521

26-
$firstResult = $this->createMock(PooledResult::class);
27-
$firstResult->method('getIterator')
28-
->willReturn(new \ArrayIterator([['column' => 'value']]));
29-
$firstResult->method('getNextResult')
30-
->willReturn($secondResult);
22+
$secondResult = new StubResult([$expectedRow]);
23+
$firstResult = new StubResult([$expectedRow], $secondResult);
24+
$pooledResult = new StubPooledResult(new StubResult([$expectedRow], $firstResult), $release);
3125

32-
$result = $this->getMockBuilder(PooledResult::class)
33-
->setConstructorArgs([$firstResult, $release])
34-
->getMockForAbstractClass();
26+
$iterator = $pooledResult->getIterator();
3527

36-
$result->expects(self::once())
37-
->method('newInstanceFrom')
38-
->willReturnCallback(function (Result $result, \Closure $release): PooledResult {
39-
return $this->getMockBuilder(PooledResult::class)
40-
->setConstructorArgs([$result, $release])
41-
->getMockForAbstractClass();
42-
});
43-
44-
$iterator = $result->getIterator();
45-
46-
$this->assertSame(['column' => 'value'], $iterator->current());
28+
$this->assertSame($expectedRow, $iterator->current());
4729

4830
$this->assertFalse($invoked);
4931

@@ -52,15 +34,16 @@ public function testIdleConnectionsRemovedAfterTimeout()
5234

5335
$this->assertFalse($invoked); // Next result set available.
5436

55-
$result = $result->getNextResult();
56-
$iterator = $result->getIterator();
37+
$pooledResult = $pooledResult->getNextResult();
38+
$iterator = $pooledResult->getIterator();
5739

58-
$this->assertSame(['column' => 'value'], $iterator->current());
40+
$this->assertSame($expectedRow, $iterator->current());
5941

6042
$iterator->next();
6143
$this->assertFalse($iterator->valid());
6244

63-
$result->getNextResult();
45+
$pooledResult = $pooledResult->getNextResult();
46+
unset($pooledResult); // Manually unset to trigger destructor.
6447

6548
delay(0); // Tick event loop to dispose of result set.
6649

test/Stub/StubPooledResult.php

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Amp\Sql\Common\Test\Stub;
4+
5+
use Amp\Sql\Common\PooledResult;
6+
use Amp\Sql\Result;
7+
8+
final class StubPooledResult extends PooledResult
9+
{
10+
protected static function newInstanceFrom(Result $result, \Closure $release): self
11+
{
12+
return new self($result, $release);
13+
}
14+
}

test/Stub/StubResult.php

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Amp\Sql\Common\Test\Stub;
4+
5+
use Amp\Sql\Result;
6+
7+
final class StubResult implements Result, \IteratorAggregate
8+
{
9+
private readonly array $rows;
10+
11+
private int $current = 0;
12+
13+
public function __construct(array $rows, private readonly ?Result $next = null)
14+
{
15+
$this->rows = \array_values($rows);
16+
}
17+
18+
public function getIterator(): \Iterator
19+
{
20+
yield from $this->rows;
21+
}
22+
23+
public function fetchRow(): ?array
24+
{
25+
return $this->rows[$this->current++] ?? null;
26+
}
27+
28+
public function getNextResult(): ?Result
29+
{
30+
return $this->next;
31+
}
32+
33+
public function getRowCount(): ?int
34+
{
35+
return \count($this->rows);
36+
}
37+
38+
public function getColumnCount(): ?int
39+
{
40+
return null;
41+
}
42+
}

0 commit comments

Comments
 (0)