Disable msg stream backpressure by default
Half of portal API usage requires a 1 message response (`.run()`, `.run_in_actor()`) and the streaming APIs should probably be explicitly enabled for backpressure if desired by the user. This makes more sense in (psuedo) realtime systems where it's better to notify on a block then freeze without notice. Make this default behaviour with a new error to be raised: `tractor._exceptions.StreamOverrun` when a sender overruns a stream by the default size (2**6 for now). The old behavior can be enabled with `Context.open_stream(backpressure=True)` but now with warning log messages when there are overruns. Add task-linked-context error propagation using a "nursery raising" technique such that if either end of context linked pair of tasks errors, that error can be relayed to other side and raised as a form of interrupt at the receiving task's next `trio` checkpoint. This enables reliable error relay without expecting the (error) receiving task to call an API which would raise the remote exception (which it might never currently if using `tractor.MsgStream` APIs). Further internal implementation details: - define the default msg buffer size as `Actor.msg_buffer_size` - expose a `msg_buffer_size: int` kwarg from `Actor.get_context()` - maybe raise aforementioned context errors using `Context._maybe_error_from_remote_msg()` inside `Actor._push_result()` - support optional backpressure on a stream when pushing messages in `Actor._push_result()` - in `_invote()` handle multierrors raised from a `@tractor.context` entrypoint as being potentially caused by a relayed error from the remote caller task, if `Context._error` has been set then raise that error inside the `RemoteActorError` that will be relayed back to that caller more or less proxying through the source side error back to its origin.stricter_context_starting
							parent
							
								
									2680a9473d
								
							
						
					
					
						commit
						185dbc7e3f
					
				|  | @ -32,6 +32,7 @@ from ._exceptions import ( | ||||||
|     is_multi_cancelled, |     is_multi_cancelled, | ||||||
|     ContextCancelled, |     ContextCancelled, | ||||||
|     TransportClosed, |     TransportClosed, | ||||||
|  |     StreamOverrun, | ||||||
| ) | ) | ||||||
| from . import _debug | from . import _debug | ||||||
| from ._discovery import get_arbiter | from ._discovery import get_arbiter | ||||||
|  | @ -161,16 +162,27 @@ async def _invoke( | ||||||
|             # context func with support for bi-dir streaming |             # context func with support for bi-dir streaming | ||||||
|             await chan.send({'functype': 'context', 'cid': cid}) |             await chan.send({'functype': 'context', 'cid': cid}) | ||||||
| 
 | 
 | ||||||
|  |             try: | ||||||
|                 async with trio.open_nursery() as scope_nursery: |                 async with trio.open_nursery() as scope_nursery: | ||||||
|                     ctx._scope_nursery = scope_nursery |                     ctx._scope_nursery = scope_nursery | ||||||
|                     cs = scope_nursery.cancel_scope |                     cs = scope_nursery.cancel_scope | ||||||
|                     task_status.started(cs) |                     task_status.started(cs) | ||||||
|                 try: |  | ||||||
|                     await chan.send({'return': await coro, 'cid': cid}) |                     await chan.send({'return': await coro, 'cid': cid}) | ||||||
|  | 
 | ||||||
|             except trio.Cancelled as err: |             except trio.Cancelled as err: | ||||||
|                 tb = err.__traceback__ |                 tb = err.__traceback__ | ||||||
|  |                 if ctx._error is not None: | ||||||
|  |                     tb = ctx._error.__traceback__ | ||||||
|  |                     raise ctx._error | ||||||
| 
 | 
 | ||||||
|             if cs.cancelled_caught: |             except trio.MultiError as err: | ||||||
|  |                 if ctx._error is not None: | ||||||
|  |                     tb = ctx._error.__traceback__ | ||||||
|  |                     raise ctx._error from err | ||||||
|  |                 else: | ||||||
|  |                     raise | ||||||
|  | 
 | ||||||
|  |             if cs.cancelled_caught or ctx._error: | ||||||
| 
 | 
 | ||||||
|                 # TODO: pack in ``trio.Cancelled.__traceback__`` here |                 # TODO: pack in ``trio.Cancelled.__traceback__`` here | ||||||
|                 # so they can be unwrapped and displayed on the caller |                 # so they can be unwrapped and displayed on the caller | ||||||
|  | @ -314,6 +326,7 @@ class Actor: | ||||||
|     # ugh, we need to get rid of this and replace with a "registry" sys |     # ugh, we need to get rid of this and replace with a "registry" sys | ||||||
|     # https://github.com/goodboy/tractor/issues/216 |     # https://github.com/goodboy/tractor/issues/216 | ||||||
|     is_arbiter: bool = False |     is_arbiter: bool = False | ||||||
|  |     msg_buffer_size: int = 2**6 | ||||||
| 
 | 
 | ||||||
|     # nursery placeholders filled in by `_async_main()` after fork |     # nursery placeholders filled in by `_async_main()` after fork | ||||||
|     _root_n: Optional[trio.Nursery] = None |     _root_n: Optional[trio.Nursery] = None | ||||||
|  | @ -548,7 +561,7 @@ class Actor: | ||||||
|                 # now in a cancelled condition) when the local runtime here |                 # now in a cancelled condition) when the local runtime here | ||||||
|                 # is now cancelled while (presumably) in the middle of msg |                 # is now cancelled while (presumably) in the middle of msg | ||||||
|                 # loop processing. |                 # loop processing. | ||||||
|                 with trio.move_on_after(0.1) as cs: |                 with trio.move_on_after(0.5) as cs: | ||||||
|                     cs.shield = True |                     cs.shield = True | ||||||
|                     # Attempt to wait for the far end to close the channel |                     # Attempt to wait for the far end to close the channel | ||||||
|                     # and bail after timeout (2-generals on closure). |                     # and bail after timeout (2-generals on closure). | ||||||
|  | @ -611,23 +624,54 @@ class Actor: | ||||||
|         cid: str, |         cid: str, | ||||||
|         msg: dict[str, Any], |         msg: dict[str, Any], | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """Push an RPC result to the local consumer's queue. |         ''' | ||||||
|         """ |         Push an RPC result to the local consumer's queue. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|         assert chan.uid, f"`chan.uid` can't be {chan.uid}" |         assert chan.uid, f"`chan.uid` can't be {chan.uid}" | ||||||
|         ctx = self._contexts[(chan.uid, cid)] |         ctx = self._contexts[(chan.uid, cid)] | ||||||
|         send_chan = ctx._send_chan |         send_chan = ctx._send_chan | ||||||
|         assert send_chan |         assert send_chan | ||||||
| 
 | 
 | ||||||
|         # TODO: relaying far end context errors to the local |         if msg.get('error'): | ||||||
|         # context through nursery raising? |             # If this is an error message from a context opened by | ||||||
|         # if 'error' in msg: |             # ``Portal.open_context()`` we want to interrupt any ongoing | ||||||
|         #     ctx._error_from_remote_msg(msg) |             # (child) tasks within that context to be notified of the remote | ||||||
|         #     log.runtime(f"{send_chan} was terminated at remote end") |             # error relayed here. | ||||||
|  |             # | ||||||
|  |             # The reason we may want to raise the remote error immediately | ||||||
|  |             # is that there is no guarantee the associated local task(s) | ||||||
|  |             # will attempt to read from any locally opened stream any time | ||||||
|  |             # soon. | ||||||
|  |             # | ||||||
|  |             # NOTE: this only applies when | ||||||
|  |             # ``Portal.open_context()`` has been called since it is assumed | ||||||
|  |             # (currently) that other portal APIs (``Portal.run()``, | ||||||
|  |             # ``.run_in_actor()``) do their own error checking at the point | ||||||
|  |             # of the call and result processing. | ||||||
|  |             log.warning(f'Remote context for {chan.uid}:{cid} errored') | ||||||
|  |             ctx._maybe_error_from_remote_msg(msg) | ||||||
| 
 | 
 | ||||||
|         try: |         try: | ||||||
|             log.runtime(f"Delivering {msg} from {chan.uid} to caller {cid}") |             log.runtime(f"Delivering {msg} from {chan.uid} to caller {cid}") | ||||||
|             # maintain backpressure | 
 | ||||||
|  |             # XXX: we do **not** maintain backpressure and instead | ||||||
|  |             # opt to relay stream overrun errors to the sender. | ||||||
|  |             try: | ||||||
|  |                 send_chan.send_nowait(msg) | ||||||
|  |             except trio.WouldBlock: | ||||||
|  |                 log.warning(f'Caller task {cid} was overrun!?') | ||||||
|  |                 if ctx._backpressure: | ||||||
|                     await send_chan.send(msg) |                     await send_chan.send(msg) | ||||||
|  |                 else: | ||||||
|  |                     try: | ||||||
|  |                         raise StreamOverrun( | ||||||
|  |                             f'Context stream {cid} for {chan.uid} was overrun!' | ||||||
|  |                         ) | ||||||
|  |                     except StreamOverrun as err: | ||||||
|  |                         err_msg = pack_error(err) | ||||||
|  |                         err_msg['cid'] = cid | ||||||
|  |                         await chan.send(err_msg) | ||||||
| 
 | 
 | ||||||
|         except trio.BrokenResourceError: |         except trio.BrokenResourceError: | ||||||
|             # TODO: what is the right way to handle the case where the |             # TODO: what is the right way to handle the case where the | ||||||
|  | @ -644,7 +688,7 @@ class Actor: | ||||||
|         self, |         self, | ||||||
|         chan: Channel, |         chan: Channel, | ||||||
|         cid: str, |         cid: str, | ||||||
|         max_buffer_size: int = 2**6, |         msg_buffer_size: Optional[int] = None, | ||||||
| 
 | 
 | ||||||
|     ) -> Context: |     ) -> Context: | ||||||
|         ''' |         ''' | ||||||
|  | @ -660,10 +704,17 @@ class Actor: | ||||||
|         assert actor_uid |         assert actor_uid | ||||||
|         try: |         try: | ||||||
|             ctx = self._contexts[(actor_uid, cid)] |             ctx = self._contexts[(actor_uid, cid)] | ||||||
|  | 
 | ||||||
|  |             # adjust buffer size if specified | ||||||
|  |             state = ctx._send_chan._state | ||||||
|  |             if msg_buffer_size and state.max_buffer_size != msg_buffer_size: | ||||||
|  |                 state.max_buffer_size = msg_buffer_size | ||||||
|  | 
 | ||||||
|         except KeyError: |         except KeyError: | ||||||
|             send_chan: trio.MemorySendChannel |             send_chan: trio.MemorySendChannel | ||||||
|             recv_chan: trio.MemoryReceiveChannel |             recv_chan: trio.MemoryReceiveChannel | ||||||
|             send_chan, recv_chan = trio.open_memory_channel(max_buffer_size) |             send_chan, recv_chan = trio.open_memory_channel( | ||||||
|  |                 msg_buffer_size or self.msg_buffer_size) | ||||||
|             ctx = Context( |             ctx = Context( | ||||||
|                 chan, |                 chan, | ||||||
|                 cid, |                 cid, | ||||||
|  | @ -679,7 +730,8 @@ class Actor: | ||||||
|         chan: Channel, |         chan: Channel, | ||||||
|         ns: str, |         ns: str, | ||||||
|         func: str, |         func: str, | ||||||
|         kwargs: dict |         kwargs: dict, | ||||||
|  |         msg_buffer_size: Optional[int] = None, | ||||||
| 
 | 
 | ||||||
|     ) -> Context: |     ) -> Context: | ||||||
|         ''' |         ''' | ||||||
|  | @ -693,7 +745,7 @@ class Actor: | ||||||
|         ''' |         ''' | ||||||
|         cid = str(uuid.uuid4()) |         cid = str(uuid.uuid4()) | ||||||
|         assert chan.uid |         assert chan.uid | ||||||
|         ctx = self.get_context(chan, cid) |         ctx = self.get_context(chan, cid, msg_buffer_size=msg_buffer_size) | ||||||
|         log.runtime(f"Sending cmd to {chan.uid}: {ns}.{func}({kwargs})") |         log.runtime(f"Sending cmd to {chan.uid}: {ns}.{func}({kwargs})") | ||||||
|         await chan.send({'cmd': (ns, func, kwargs, self.uid, cid)}) |         await chan.send({'cmd': (ns, func, kwargs, self.uid, cid)}) | ||||||
| 
 | 
 | ||||||
|  | @ -743,7 +795,8 @@ class Actor: | ||||||
|                     if msg is None:  # loop terminate sentinel |                     if msg is None:  # loop terminate sentinel | ||||||
| 
 | 
 | ||||||
|                         log.cancel( |                         log.cancel( | ||||||
|                             f"Cancelling all tasks for {chan} from {chan.uid}") |                             f"Channerl to {chan.uid} terminated?\n" | ||||||
|  |                             "Cancelling all associated tasks..") | ||||||
| 
 | 
 | ||||||
|                         for (channel, cid) in self._rpc_tasks.copy(): |                         for (channel, cid) in self._rpc_tasks.copy(): | ||||||
|                             if channel is chan: |                             if channel is chan: | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue