Skip to content

Commit 3215d05

Browse files
authored
Sftp perf (#328)
* Updated tests, setup.cfg * Fix regression in sftp put performance, added test * Updated changelog * Updated manifest
1 parent 32e62fd commit 3215d05

File tree

9 files changed

+51
-23
lines changed

9 files changed

+51
-23
lines changed

Changelog.rst

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Change Log
22
============
33

4+
2.7.1
5+
+++++
6+
7+
Fixes
8+
------
9+
10+
* ``copy_file`` performance would be abnormally low when copying plain text files - 100x performance increase. Binary
11+
file copying performance has also increased.
12+
13+
414
2.7.0
515
+++++
616

MANIFEST.in

-2
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,3 @@ include LICENSE
44
include COPYING
55
include COPYING.LESSER
66
recursive-exclude tests *
7-
include pssh/native/*.c
8-
include pssh/native/*.pyx

examples/sftp_copy_file.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55

66

77
with open('file_copy', 'wb') as fh:
8-
for _ in range(2000000):
9-
fh.write(b'asdfa')
8+
# 200MB
9+
for _ in range(20055120):
10+
fh.write(b'asdfartkj\n')
1011

1112

1213
fileinfo = os.stat('file_copy')
13-
client = ParallelSSHClient(['localhost'])
14+
mb_size = fileinfo.st_size / (1024000.0)
15+
client = ParallelSSHClient(['127.0.0.1'], timeout=1, num_retries=1)
16+
print(f"Starting copy of {mb_size}MB file")
1417
now = datetime.now()
1518
cmd = client.copy_file('file_copy', '/tmp/file_copy')
1619
joinall(cmd, raise_error=True)
1720
taken = datetime.now() - now
18-
mb_size = fileinfo.st_size / (1024000.0)
1921
rate = mb_size / taken.total_seconds()
20-
print("File size %sMB transfered in %s, transfer rate %s MB/s" % (
21-
mb_size, taken, rate))
22+
print("File size %sMB transfered in %s, transfer rate %s MB/s" % (mb_size, taken, rate))
23+
os.unlink('file_copy')
24+
os.unlink('/tmp/file_copy')

pssh/clients/native/single.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545

4646
class SSHClient(BaseSSHClient):
4747
"""ssh2-python (libssh2) based non-blocking SSH client."""
48+
# 2MB buffer
49+
_BUF_SIZE = 2048 * 1024
4850

4951
def __init__(self, host,
5052
user=None, password=None, port=None,
@@ -411,9 +413,11 @@ def copy_file(self, local_file, remote_file, recurse=False, sftp=None):
411413
local_file, self.host, remote_file)
412414

413415
def _sftp_put(self, remote_fh, local_file):
414-
with open(local_file, 'rb', 2097152) as local_fh:
415-
for data in local_fh:
416+
with open(local_file, 'rb', self._BUF_SIZE) as local_fh:
417+
data = local_fh.read(self._BUF_SIZE)
418+
while data:
416419
self.eagain_write(remote_fh.write, data)
420+
data = local_fh.read(self._BUF_SIZE)
417421

418422
def sftp_put(self, sftp, local_file, remote_file):
419423
mode = LIBSSH2_SFTP_S_IRUSR | \

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ tag_prefix = ''
88
max-line-length = 100
99

1010
[tool:pytest]
11-
addopts=-v --cov=pssh --cov-append --cov-report=term --cov-report=term-missing
11+
addopts=-v --cov=pssh --cov-append --cov-report=term --cov-report=term-missing --durations=10
1212
testpaths =
1313
tests

tests/embedded_server/openssh.py

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def start_server(self):
9191
pass
9292
else:
9393
logger.error(self.server_proc.stdout.read())
94+
logger.error(self.server_proc.stderr.read())
9495
raise Exception("Server could not start")
9596

9697
def stop(self):

tests/native/test_parallel_client.py

+5-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
# This file is part of parallel-ssh.
33
#
4-
# Copyright (C) 2014-2020 Panos Kittenis
4+
# Copyright (C) 2014-2021 Panos Kittenis
55
#
66
# This library is free software; you can redistribute it and/or
77
# modify it under the terms of the GNU Lesser General Public
@@ -29,11 +29,11 @@
2929
from datetime import datetime
3030
from unittest.mock import patch, MagicMock
3131

32-
from gevent import joinall, spawn, socket, Greenlet, sleep, Timeout as GTimeout
32+
from gevent import joinall, spawn, socket, sleep, Timeout as GTimeout
3333
from pssh.config import HostConfig
3434
from pssh.clients.native import ParallelSSHClient
3535
from pssh.exceptions import UnknownHostException, \
36-
AuthenticationException, ConnectionErrorException, SessionError, \
36+
AuthenticationException, ConnectionErrorException, \
3737
HostArgumentException, SFTPError, SFTPIOError, Timeout, SCPError, \
3838
PKeyFileError, ShellError, HostArgumentError, NoIPv6AddressFoundError
3939
from pssh.output import HostOutput
@@ -1042,6 +1042,7 @@ def test_per_host_tuple_args(self):
10421042
pkey=self.user_key,
10431043
num_retries=2)
10441044
output = client.run_command(cmd, host_args=host_args)
1045+
client.join()
10451046
for i, host in enumerate(hosts):
10461047
expected = [host_args[i]]
10471048
stdout = list(output[i].stdout)
@@ -1050,6 +1051,7 @@ def test_per_host_tuple_args(self):
10501051
host_args = (('arg1', 'arg2'), ('arg3', 'arg4'), ('arg5', 'arg6'),)
10511052
cmd = 'echo %s %s'
10521053
output = client.run_command(cmd, host_args=host_args)
1054+
client.join()
10531055
for i, host in enumerate(hosts):
10541056
expected = ["%s %s" % host_args[i]]
10551057
stdout = list(output[i].stdout)
@@ -1198,14 +1200,6 @@ def test_run_command_sudo(self):
11981200
self.assertEqual(len(output), len(self.client.hosts))
11991201
self.assertTrue(output[0].channel is not None)
12001202

1201-
@unittest.skipUnless(bool(os.getenv('TRAVIS')), "Not on Travis CI - skipping")
1202-
def test_run_command_sudo_var(self):
1203-
command = """for i in 1 2 3; do echo $i; done"""
1204-
output = list(self.client.run_command(
1205-
command, sudo=True)[0].stdout)
1206-
expected = ['1','2','3']
1207-
self.assertListEqual(output, expected)
1208-
12091203
def test_conn_failure(self):
12101204
"""Test connection error failure case - ConnectionErrorException"""
12111205
client = ParallelSSHClient(['127.0.0.100'], port=self.port,

tests/native/test_single_client.py

+18
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import subprocess
2020
import shutil
2121
import tempfile
22+
from tempfile import NamedTemporaryFile
2223
from pytest import raises
2324
from unittest.mock import MagicMock, call, patch
2425
from hashlib import sha256
@@ -562,6 +563,23 @@ def test_copy_file_remote_dir_relpath(self):
562563
except Exception:
563564
pass
564565

566+
def test_copy_file_with_newlines(self):
567+
with NamedTemporaryFile('wb') as temp_file:
568+
# 2MB
569+
for _ in range(200512):
570+
temp_file.write(b'asdfartkj\n')
571+
temp_file.flush()
572+
now = datetime.now()
573+
try:
574+
self.client.copy_file(os.path.abspath(temp_file.name), 'write_file')
575+
took = datetime.now() - now
576+
assert took.total_seconds() < 1
577+
finally:
578+
try:
579+
os.unlink(os.path.expanduser('~/write_file'))
580+
except OSError:
581+
pass
582+
565583
def test_sftp_mkdir_abspath(self):
566584
remote_dir = '/tmp/dir_to_create/dir1/dir2/dir3'
567585
_sftp = self.client._make_sftp()

tests/native/test_tunnel.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def test_tunnel_server_reconn(self):
9696
remote_server.start_server()
9797

9898
reconn_n = 20 # Number of reconnect attempts
99-
reconn_delay = 1 # Number of seconds to delay betwen reconnects
99+
reconn_delay = .1 # Number of seconds to delay between reconnects
100100
try:
101101
for _ in range(reconn_n):
102102
client = SSHClient(

0 commit comments

Comments
 (0)