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.
pull/350/head
David Wilson 6 years ago
parent 8e9b5ad576
commit 7458dfae85

@ -692,15 +692,29 @@ class Connection(ansible.plugins.connection.ConnectionBase):
: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 @@ class Connection(ansible.plugins.connection.ConnectionBase):
"""
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 @@ class Connection(ansible.plugins.connection.ConnectionBase):
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 @@ class Connection(ansible.plugins.connection.ConnectionBase):
# 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

Loading…
Cancel
Save