Permalink
Browse files

ansible: avoid roundtrip for small file transfers.

Calls to connect.put_file() where the file is sufficiently small enough
to fit in a single RPC proceed without waiting for an RPC response. If
the write fails the target context will log an exception, and any
subsequent step depending on the written file will fail.

I verified every built-in action plugin for file transfer calls, and
they all depend on the transferred file in the following step, so this
should be safe.

Reduces template/copy actions to 2-RTT, loop-20-templates.yml runtime
reduced from 30 seconds to 10 seconds over a 250ms link compared to
v0.2.2, and from 123 seconds compared to vanilla with pipelining
enabled.
  • Loading branch information...
dw committed Aug 20, 2018
1 parent 8e9b5ad commit 7458dfae855d86512e85c2a1068ec80eac925ad7
Showing with 44 additions and 13 deletions.
  1. +40 −9 ansible_mitogen/connection.py
  2. +4 −4 docs/ansible.rst
@@ -692,15 +692,29 @@ def call_async(self, func, *args, **kwargs):
:param bool use_login_context:
If present and :data:`True`, send the call to the login account
context rather than the optional become user context.
:param bool no_reply:
If present and :data:`True`, send the call with no ``reply_to``
header, causing the context to execute it entirely asynchronously,
and to log any exception thrown. This allows avoiding a roundtrip
in places where the outcome of a call is highly likely to succeed,
and subsequent actions will fail regardless with a meaningful
exception if the no_reply call failed.
:returns:
mitogen.core.Receiver that receives the function call result.
:class:`mitogen.core.Receiver` that receives the function call result.
"""
self._connect()
if kwargs.pop('use_login_context', None):
call_context = self.login_context
else:
call_context = self.context
return call_context.call_async(func, *args, **kwargs)
if kwargs.pop('no_reply', None):
return call_context.call_no_reply(func, *args, **kwargs)
else:
return call_context.call_async(func, *args, **kwargs)
def call(self, func, *args, **kwargs):
"""
@@ -713,7 +727,10 @@ def call(self, func, *args, **kwargs):
"""
t0 = time.time()
try:
return self.call_async(func, *args, **kwargs).get().unpickle()
recv = self.call_async(func, *args, **kwargs)
if recv is None: # no_reply=True
return None
return recv.get().unpickle()
finally:
LOG.debug('Call took %d ms: %r', 1000 * (time.time() - t0),
mitogen.parent.CallSpec(func, args, kwargs))
@@ -786,19 +803,33 @@ def fetch_file(self, in_path, out_path):
def put_data(self, out_path, data, mode=None, utimes=None):
"""
Implement put_file() by caling the corresponding
ansible_mitogen.target function in the target.
Implement put_file() by caling the corresponding ansible_mitogen.target
function in the target, transferring small files inline.
:param str out_path:
Remote filesystem path to write.
:param byte data:
File contents to put.
"""
# no_reply=True here avoids a roundrip that 99% of the time will report
# a successful response. If the file transfer fails, the target context
# will dump an exception into the logging framework, which will appear
# on console, and the missing file will cause the subsequent task step
# to fail regardless. This is safe since CALL_FUNCTION is presently
# single-threaded for each target, so subsequent steps cannot execute
# until the transfer RPC has completed.
self.call(ansible_mitogen.target.write_path,
mitogen.utils.cast(out_path),
mitogen.core.Blob(data),
mode=mode,
utimes=utimes)
utimes=utimes,
no_reply=True)
#: Maximum size of a small file before switching to streaming file
#: transfer. This should really be the same as
#: mitogen.services.FileService.IO_SIZE, however the message format has
#: slightly more overhead, so just randomly subtract 4KiB.
SMALL_FILE_LIMIT = mitogen.core.CHUNK_SIZE - 4096
def put_file(self, in_path, out_path):
"""
@@ -817,14 +848,14 @@ def put_file(self, in_path, out_path):
# If the file is sufficiently small, just ship it in the argument list
# rather than introducing an extra RTT for the child to request it from
# FileService.
if st.st_size <= 32768:
if st.st_size <= self.SMALL_FILE_LIMIT:
fp = open(in_path, 'rb')
try:
s = fp.read(32769)
s = fp.read(self.SMALL_FILE_LIMIT + 1)
finally:
fp.close()
# Ensure file was not growing during call.
# Ensure did not grow during read.
if len(s) == st.st_size:
return self.put_data(out_path, s, mode=st.st_mode,
utimes=(st.st_atime, st.st_mtime))
@@ -312,10 +312,10 @@ where readers may observe inconsistent file contents.
Performance
^^^^^^^^^^^
One roundtrip initiates a transfer larger than 32KiB, while smaller transfers
are embedded in the initiating RPC. For tools operating via SSH multiplexing, 4
roundtrips are required to configure the IO channel, in addition to the time to
start the local and remote processes.
One roundtrip initiates a transfer larger than 124 KiB, while smaller transfers
are embedded in a 0-roundtrip remote call. For tools operating via SSH
multiplexing, 4 roundtrips are required to configure the IO channel, in
addition to the time to start the local and remote processes.
An invocation of ``scp`` with an empty ``.profile`` over a 30 ms link takes
~140 ms, wasting 110 ms per invocation, rising to ~2,000 ms over a 400 ms

0 comments on commit 7458dfa

Please sign in to comment.