Skip to content

Commit 8e5a45e

Browse files
author
Andrii Sudiev
committed
Fix memory leak and performance degradation. gc was not able to collect unused(without references) dataloader instances
1 parent b5016b8 commit 8e5a45e

2 files changed

Lines changed: 28 additions & 20 deletions

File tree

src/DataLoader.php

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,22 @@ class DataLoader implements DataLoaderInterface
3838
/**
3939
* @var self[]
4040
*/
41-
private static $instances = [];
41+
private static array $activeInstances = [];
4242

4343
/**
4444
* @var PromiseAdapterInterface
4545
*/
4646
private $promiseAdapter;
4747

48+
private static PromiseAdapterInterface|null $staticPromiseAdapter = null;
49+
4850
public function __construct(callable $batchLoadFn, PromiseAdapterInterface $promiseFactory, ?Option $options = null)
4951
{
5052
$this->batchLoadFn = $batchLoadFn;
5153
$this->promiseAdapter = $promiseFactory;
54+
self::$staticPromiseAdapter ??= $promiseFactory;
5255
$this->options = $options ?: new Option();
5356
$this->promiseCache = $this->options->getCacheMap();
54-
self::$instances[] = $this;
5557
}
5658

5759
/**
@@ -77,7 +79,7 @@ public function load($key)
7779
$promise = $this->getPromiseAdapter()->create(
7880
$resolve,
7981
$reject,
80-
function () {
82+
static function () {
8183
// Cancel/abort any running operations like network connections, streams etc.
8284

8385
throw new \RuntimeException('DataLoader destroyed before promise complete.');
@@ -91,6 +93,8 @@ function () {
9193
'promise' => $promise,
9294
];
9395

96+
self::$activeInstances[spl_object_id($this)] = $this;
97+
9498
// Determine if a dispatch of this queue should be scheduled.
9599
// A single dispatch should be scheduled per queue at the time when the
96100
// queue changes from "empty" to "full".
@@ -179,25 +183,21 @@ public function __destruct()
179183
}
180184
$this->await();
181185
}
182-
foreach (self::$instances as $i => $instance) {
183-
if ($this !== $instance) {
184-
continue;
185-
}
186-
unset(self::$instances[$i]);
187-
}
188186
}
189187

190188
protected function needProcess()
191189
{
192190
return count($this->queue) > 0;
193191
}
194192

195-
protected function process()
193+
protected function process(): bool
196194
{
197195
if ($this->needProcess()) {
198196
$this->getPromiseAdapter()->await();
199197
$this->dispatchQueue();
198+
return true;
200199
}
200+
return false;
201201
}
202202

203203
protected function getPromiseAdapter()
@@ -246,26 +246,20 @@ function ($reason) use (&$isPromiseCompleted, &$rejectedReason) {
246246
}
247247
}
248248

249-
if (empty(self::$instances)) {
249+
if (!self::$staticPromiseAdapter) {
250250
throw new \RuntimeException('Found no active DataLoader instance.');
251251
}
252252

253-
return self::$instances[0]->getPromiseAdapter()->await($promise, $unwrap);
253+
return self::$staticPromiseAdapter->await($promise, $unwrap);
254254
}
255255

256256
private static function awaitInstances()
257257
{
258258
do {
259259
$wait = false;
260-
$dataLoaders = self::$instances;
261260

262-
foreach ($dataLoaders as $dataLoader) {
263-
if (!$dataLoader || !$dataLoader->needProcess()) {
264-
$wait |= false;
265-
continue;
266-
}
267-
$wait = true;
268-
$dataLoader->process();
261+
foreach (self::$activeInstances as $dataLoader) {
262+
$wait |= $dataLoader->process();
269263
}
270264
} while ($wait);
271265
}
@@ -305,6 +299,7 @@ private function dispatchQueue()
305299
// Take the current loader queue, replacing it with an empty queue.
306300
$queue = $this->queue;
307301
$this->queue = [];
302+
unset(self::$activeInstances[spl_object_id($this)]);
308303
$queueLength = count($queue);
309304
// If a maxBatchSize was provided and the queue is longer, then segment the
310305
// queue into multiple batches, otherwise treat the queue as a single batch.

tests/DataLoadTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,19 @@ public function testOnDestructionAllPromiseInQueueShouldBeCancelled()
782782
$this->assertEquals($exception->getMessage(), 'DataLoader destroyed before promise complete.');
783783
}
784784

785+
public function testPromisesResolvedWithNoExternalReferences()
786+
{
787+
$resolvedValue = null;
788+
[$loader] = self::idLoader();
789+
$loader->load(1)->then(function ($v) use (&$resolvedValue) {
790+
$resolvedValue = $v;
791+
});
792+
$loader = null;
793+
DataLoader::await();
794+
795+
$this->assertSame(1, $resolvedValue, 'Promise has not been resolved');
796+
}
797+
785798
public function testCallingAwaitFunctionWhenNoInstanceOfDataLoaderShouldNotThrowError()
786799
{
787800
self::assertNull(DataLoader::await());

0 commit comments

Comments
 (0)