diff --git a/lib/Backend/SftpReadStream.php b/lib/Backend/SftpReadStream.php index 99ee417..1c9ff13 100644 --- a/lib/Backend/SftpReadStream.php +++ b/lib/Backend/SftpReadStream.php @@ -55,6 +55,13 @@ final class SftpReadStream { */ private static ?\WeakMap $chunks = null; private static int $requestId = 0x4e470000; + /** + * Connections whose server did not confirm closing a batch's files: readMany() is not used on + * them again, so handles cannot pile up. + * + * @var \WeakMap|null + */ + private static ?\WeakMap $noBatches = null; /** Streams in a row that failed; at MAX_FAILURES only the storage's own stream is used. */ private static int $failures = 0; public const MAX_FAILURES = 3; @@ -182,7 +189,7 @@ final class SftpReadStream { self::$failures++; return null; } - if ($sftp === null) { + if ($sftp === null || self::$noBatches?->offsetExists($sftp) === true) { return null; } $result = array_fill_keys($paths, null); @@ -200,8 +207,9 @@ final class SftpReadStream { $result[$path] = $contents; } if (!$closed) { - // The server did not confirm closing these files: no more batches, so handles - // cannot pile up. The rest is read the usual way, one at a time. + // The server did not confirm closing these files: no more batches on this + // connection, so handles cannot pile up. The rest is read the usual way. + self::noMoreBatches($sftp); break; } } @@ -209,6 +217,11 @@ final class SftpReadStream { return $result; } + private static function noMoreBatches(object $sftp): void { + self::$noBatches ??= new \WeakMap(); + self::$noBatches[$sftp] = true; + } + private static function disconnect(object $sftp): void { try { if (is_callable([$sftp, 'disconnect'])) { @@ -306,6 +319,7 @@ final class SftpReadStream { /** Test hook: forget the learned READ sizes and earlier failures. */ public static function reset(): void { self::$chunks = null; + self::$noBatches = null; self::$failures = 0; } diff --git a/tests/Unit/Backend/SftpReadStreamTest.php b/tests/Unit/Backend/SftpReadStreamTest.php index 2542b53..bff6a4b 100644 --- a/tests/Unit/Backend/SftpReadStreamTest.php +++ b/tests/Unit/Backend/SftpReadStreamTest.php @@ -286,6 +286,8 @@ class SftpReadStreamTest extends TestCase { $this->assertSame('file 31', $got['31']); $this->assertNull($got['32'], 'no second batch after unconfirmed closes'); $this->assertCount(SftpReadStream::BATCH, array_filter($got, fn ($v) => $v !== null)); + // Nor on a later call over the same connection. + $this->assertNull(SftpReadStream::readMany(new FakeSftpStorage($sftp), ['0'])); } public function testReadManyLeavesAMalformedAnswerToTheCaller(): void {