Read ahead over SFTP: take answers phpseclib has read already instead of waiting up to 10 s on the socket
This commit is contained in:
@@ -30,8 +30,11 @@ final class SftpRangeFetcher {
|
||||
public const DEPTH = 16;
|
||||
/** Ranges read ahead of the one the caller is at, in flight or waiting in the spool. */
|
||||
public const AHEAD = 8;
|
||||
/** Seconds ready() waits for any connection to answer before it waits for each in turn. */
|
||||
private const SELECT_TIMEOUT = 10;
|
||||
/**
|
||||
* Seconds ready() waits for any connection to answer before it waits for each in turn: short,
|
||||
* in case a connection holds an answer where ready() does not look.
|
||||
*/
|
||||
private const SELECT_TIMEOUT = 1;
|
||||
/** See SftpReadStream::CHUNK and MIN_CHUNK. */
|
||||
private const CHUNK = 261120;
|
||||
private const MIN_CHUNK = 32768;
|
||||
@@ -134,9 +137,16 @@ final class SftpRangeFetcher {
|
||||
foreach ($this->jobs as $c => $job) {
|
||||
$sftp = $this->connections[$c];
|
||||
$vars = get_object_vars($sftp); // only what is public: phpseclib 2's are
|
||||
// Answers phpseclib has read already: the socket does not show them. Its request
|
||||
// buffer holds whole answers read while it waited for another; its packet buffer and
|
||||
// channel buffers hold what came in with the last packet it read (small answers, such
|
||||
// as a CLOSE and the next OPEN, often arrive together).
|
||||
$buffered = $vars['requestBuffer'] ?? null;
|
||||
if (is_array($buffered) && isset($job->queue[0]) && array_key_exists($job->queue[0][0], $buffered)) {
|
||||
$ready[] = $c; // read already, while waiting for another answer
|
||||
$channels = $vars['channel_buffers'] ?? null;
|
||||
if ((is_array($buffered) && isset($job->queue[0]) && array_key_exists($job->queue[0][0], $buffered))
|
||||
|| (is_string($vars['packet_buffer'] ?? null) && $vars['packet_buffer'] !== '')
|
||||
|| (is_array($channels) && array_filter($channels) !== [])) {
|
||||
$ready[] = $c;
|
||||
continue;
|
||||
}
|
||||
$socket = $vars['fsock'] ?? null;
|
||||
|
||||
@@ -87,6 +87,25 @@ class SftpRangeFetcherTest extends TestCase {
|
||||
$this->assertSame(SftpRangeFetcher::DEPTH, $connections[1]->maxInFlight);
|
||||
}
|
||||
|
||||
public function testAnAnswerPhpseclibHasReadAlreadyIsTakenWithoutWaitingOnTheSocket(): void {
|
||||
// The fake's answers are all "read already": its socket never becomes readable, and
|
||||
// phpseclib's packet buffer holds data. Waiting on the socket would take a second a step.
|
||||
$connections = $this->connections(2);
|
||||
$sockets = [];
|
||||
foreach ($connections as $c) {
|
||||
$pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
|
||||
$sockets[] = $pair;
|
||||
$c->fsock = $pair[0];
|
||||
$c->packet_buffer = 'more';
|
||||
}
|
||||
$ranges = array_slice($this->ranges(), 0, 3);
|
||||
$start = microtime(true);
|
||||
|
||||
$this->assertSame($this->expected($ranges), self::collect(SftpRangeFetcher::fetch(self::opened($connections), '/home/', $ranges, $this->tempDir())));
|
||||
|
||||
$this->assertLessThan(1.0, microtime(true) - $start);
|
||||
}
|
||||
|
||||
public function testAServerThatCapsAReadStillGivesEveryByte(): void {
|
||||
$connections = $this->connections(3, array_fill(0, 3, ['cap' => 100000]));
|
||||
$ranges = $this->ranges();
|
||||
|
||||
@@ -468,6 +468,10 @@ final class FakeSftpConnection {
|
||||
public array $openHandles = [];
|
||||
/** @var list<string> paths of every OPEN request */
|
||||
public array $opened = [];
|
||||
/** @var resource|null like phpseclib's socket (SftpRangeFetcher waits on it) */
|
||||
public $fsock = null;
|
||||
/** like phpseclib's: what came in with the last packet and is not read yet */
|
||||
public string $packet_buffer = '';
|
||||
/** Whether the SFTP channel is open: phpseclib 2 opens it on the first SFTP call. */
|
||||
public bool $initialised = false;
|
||||
/** Packets sent */
|
||||
|
||||
Reference in New Issue
Block a user