diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 30d23a4f..966f86c1 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -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)) diff --git a/docs/ansible.rst b/docs/ansible.rst index 790d0369..514708bd 100644 --- a/docs/ansible.rst +++ b/docs/ansible.rst @@ -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