Skip to content

Commit 7458dfa

Browse files
committed
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.
1 parent 8e9b5ad commit 7458dfa

File tree

2 files changed

+44
-13
lines changed

2 files changed

+44
-13
lines changed

ansible_mitogen/connection.py

+40-9
Original file line numberDiff line numberDiff line change
@@ -692,15 +692,29 @@ def call_async(self, func, *args, **kwargs):
692692
:param bool use_login_context:
693693
If present and :data:`True`, send the call to the login account
694694
context rather than the optional become user context.
695+
696+
:param bool no_reply:
697+
If present and :data:`True`, send the call with no ``reply_to``
698+
header, causing the context to execute it entirely asynchronously,
699+
and to log any exception thrown. This allows avoiding a roundtrip
700+
in places where the outcome of a call is highly likely to succeed,
701+
and subsequent actions will fail regardless with a meaningful
702+
exception if the no_reply call failed.
703+
695704
:returns:
696-
mitogen.core.Receiver that receives the function call result.
705+
:class:`mitogen.core.Receiver` that receives the function call result.
697706
"""
698707
self._connect()
708+
699709
if kwargs.pop('use_login_context', None):
700710
call_context = self.login_context
701711
else:
702712
call_context = self.context
703-
return call_context.call_async(func, *args, **kwargs)
713+
714+
if kwargs.pop('no_reply', None):
715+
return call_context.call_no_reply(func, *args, **kwargs)
716+
else:
717+
return call_context.call_async(func, *args, **kwargs)
704718

705719
def call(self, func, *args, **kwargs):
706720
"""
@@ -713,7 +727,10 @@ def call(self, func, *args, **kwargs):
713727
"""
714728
t0 = time.time()
715729
try:
716-
return self.call_async(func, *args, **kwargs).get().unpickle()
730+
recv = self.call_async(func, *args, **kwargs)
731+
if recv is None: # no_reply=True
732+
return None
733+
return recv.get().unpickle()
717734
finally:
718735
LOG.debug('Call took %d ms: %r', 1000 * (time.time() - t0),
719736
mitogen.parent.CallSpec(func, args, kwargs))
@@ -786,19 +803,33 @@ def fetch_file(self, in_path, out_path):
786803

787804
def put_data(self, out_path, data, mode=None, utimes=None):
788805
"""
789-
Implement put_file() by caling the corresponding
790-
ansible_mitogen.target function in the target.
806+
Implement put_file() by caling the corresponding ansible_mitogen.target
807+
function in the target, transferring small files inline.
791808
792809
:param str out_path:
793810
Remote filesystem path to write.
794811
:param byte data:
795812
File contents to put.
796813
"""
814+
# no_reply=True here avoids a roundrip that 99% of the time will report
815+
# a successful response. If the file transfer fails, the target context
816+
# will dump an exception into the logging framework, which will appear
817+
# on console, and the missing file will cause the subsequent task step
818+
# to fail regardless. This is safe since CALL_FUNCTION is presently
819+
# single-threaded for each target, so subsequent steps cannot execute
820+
# until the transfer RPC has completed.
797821
self.call(ansible_mitogen.target.write_path,
798822
mitogen.utils.cast(out_path),
799823
mitogen.core.Blob(data),
800824
mode=mode,
801-
utimes=utimes)
825+
utimes=utimes,
826+
no_reply=True)
827+
828+
#: Maximum size of a small file before switching to streaming file
829+
#: transfer. This should really be the same as
830+
#: mitogen.services.FileService.IO_SIZE, however the message format has
831+
#: slightly more overhead, so just randomly subtract 4KiB.
832+
SMALL_FILE_LIMIT = mitogen.core.CHUNK_SIZE - 4096
802833

803834
def put_file(self, in_path, out_path):
804835
"""
@@ -817,14 +848,14 @@ def put_file(self, in_path, out_path):
817848
# If the file is sufficiently small, just ship it in the argument list
818849
# rather than introducing an extra RTT for the child to request it from
819850
# FileService.
820-
if st.st_size <= 32768:
851+
if st.st_size <= self.SMALL_FILE_LIMIT:
821852
fp = open(in_path, 'rb')
822853
try:
823-
s = fp.read(32769)
854+
s = fp.read(self.SMALL_FILE_LIMIT + 1)
824855
finally:
825856
fp.close()
826857

827-
# Ensure file was not growing during call.
858+
# Ensure did not grow during read.
828859
if len(s) == st.st_size:
829860
return self.put_data(out_path, s, mode=st.st_mode,
830861
utimes=(st.st_atime, st.st_mtime))

docs/ansible.rst

+4-4
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,10 @@ where readers may observe inconsistent file contents.
312312
Performance
313313
^^^^^^^^^^^
314314

315-
One roundtrip initiates a transfer larger than 32KiB, while smaller transfers
316-
are embedded in the initiating RPC. For tools operating via SSH multiplexing, 4
317-
roundtrips are required to configure the IO channel, in addition to the time to
318-
start the local and remote processes.
315+
One roundtrip initiates a transfer larger than 124 KiB, while smaller transfers
316+
are embedded in a 0-roundtrip remote call. For tools operating via SSH
317+
multiplexing, 4 roundtrips are required to configure the IO channel, in
318+
addition to the time to start the local and remote processes.
319319

320320
An invocation of ``scp`` with an empty ``.profile`` over a 30 ms link takes
321321
~140 ms, wasting 110 ms per invocation, rising to ~2,000 ms over a 400 ms

0 commit comments

Comments
 (0)