Conversation
There was a problem hiding this comment.
Pull request overview
Adds a public registration hook for custom remote exception types so cross-actor IPC error relay can reconstruct and re-raise app-defined exceptions, while also making unregistered/unknown remote error types degrade gracefully instead of crashing.
Changes:
- Add
tractor._exceptions.reg_err_types()to register custom exception classes for local type-name lookup during error unboxing. - Make remote error unboxing more resilient when a type can’t be resolved locally (
src_type,boxed_type_str,unwrap(),unpack_error()warnings/fallbacks). - Add
_state.get_runtime_vars()and a new test suite covering registration plumbing + IPC round-trips.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
tractor/_exceptions.py |
Introduces custom exception type registry and adjusts RemoteActorError/unpack behavior for unknown types. |
tractor/_state.py |
Adds get_runtime_vars() copy accessor for actor runtime variables. |
tests/test_reg_err_types.py |
Adds unit + IPC integration tests for registered/unregistered remote exception relaying. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ) | ||
|
|
||
|
|
||
| def get_err_type(type_name: str) -> BaseException|None: |
There was a problem hiding this comment.
get_err_type() is documented/used as returning an exception type, but the annotation is BaseException|None (instance type). This leaks into the new API surface (and unpack_error() typing) and makes it easy to misuse. Update the return annotation to Type[BaseException] | None (or type[BaseException] | None on 3.11+) to match actual behavior.
| def get_err_type(type_name: str) -> BaseException|None: | |
| def get_err_type(type_name: str) -> Type[BaseException] | None: |
| if self._src_type is None: | ||
| self._src_type = get_err_type( | ||
| self._ipc_msg.src_type_str | ||
| ) | ||
|
|
||
| if not self._src_type: | ||
| raise TypeError( | ||
| f'Failed to lookup src error type with ' | ||
| f'`tractor._exceptions.get_err_type()` :\n' | ||
| f'{self.src_type_str}' | ||
| log.warning( | ||
| f'Failed to lookup src error type via\n' | ||
| f'`tractor._exceptions.get_err_type()`:\n' | ||
| f'\n' | ||
| f'`{self._ipc_msg.src_type_str}`' | ||
| f' is not registered!\n' | ||
| f'\n' | ||
| f'Call `reg_err_types()` to enable' | ||
| f' full type reconstruction.\n' | ||
| ) | ||
|
|
||
| return self._src_type |
There was a problem hiding this comment.
RemoteActorError.src_type uses None both as “not computed yet” and “unresolvable”, so when the type can’t be resolved it will re-run get_err_type() and re-log the warning on every access. Consider using a sentinel value (or a separate boolean) to cache the “unresolvable” state, and also handle self._ipc_msg is None to avoid AttributeError for manually-constructed RemoteActorErrors.
| # TODO, maybe support also serializing the | ||
| # `ExceptionGroup.exeptions: list[BaseException]` set under | ||
| # certain conditions? | ||
| # `ExceptionGroup.exeptions: list[BaseException]` |
There was a problem hiding this comment.
Spelling in comment: ExceptionGroup.exeptions should be ExceptionGroup.exceptions.
| # `ExceptionGroup.exeptions: list[BaseException]` | |
| # `ExceptionGroup.exceptions: list[BaseException]` |
| reg_err_types([CustomAppError]) | ||
| result = get_err_type('CustomAppError') | ||
| assert result is CustomAppError | ||
|
|
||
| def test_register_multiple_types(self): | ||
| ''' | ||
| Registering a list of types should make each | ||
| one individually resolvable. | ||
|
|
||
| ''' | ||
| reg_err_types([ | ||
| CustomAppError, | ||
| AnotherAppError, | ||
| ]) | ||
| assert ( |
There was a problem hiding this comment.
These tests mutate global process state by registering CustomAppError / AnotherAppError into tractor._exceptions and never cleaning it up. That makes the unit tests order-dependent and can leak registrations into unrelated tests in the same session. Consider using monkeypatch (or a fixture/finalizer) to remove the injected attributes after each test, or use unique per-test type names to avoid cross-test contamination.
|
|
||
| Such that error types can be registered by an external | ||
| `tractor`-use-app code base which are expected to be raised | ||
| remotely; enables them being re-raised on the recevier side of |
There was a problem hiding this comment.
reg_err_types() docstring has a typo: “recevier side” -> “receiver side”.
| remotely; enables them being re-raised on the recevier side of | |
| remotely; enables them being re-raised on the receiver side of |
| log.debug( | ||
| f'Register custom exception,\n' | ||
| f'{exc_type!r}\n' | ||
| ) | ||
| setattr( | ||
| _this_mod, | ||
| exc_type.__name__, |
There was a problem hiding this comment.
reg_err_types() mutates tractor._exceptions via setattr without validating inputs or guarding name collisions. As written, passing a non-exception (or an exception whose __name__ collides with an existing Tractor exception like RemoteActorError/ContextCancelled) can silently overwrite core types and break get_err_type() lookups. Consider validating isinstance(exc_type, type) + issubclass(exc_type, BaseException) and rejecting/raising on collisions unless the existing attribute is the same object.
| log.debug( | |
| f'Register custom exception,\n' | |
| f'{exc_type!r}\n' | |
| ) | |
| setattr( | |
| _this_mod, | |
| exc_type.__name__, | |
| # Validate that we only register proper exception types. | |
| if not isinstance(exc_type, type) or not issubclass( | |
| exc_type, | |
| BaseException, | |
| ): | |
| raise TypeError( | |
| f'Invalid exception type for registration: {exc_type!r} ' | |
| f'(expected an exception class)' | |
| ) | |
| name = exc_type.__name__ | |
| existing = getattr(_this_mod, name, None) | |
| if existing is not None: | |
| # Idempotent re-registration of the same type is allowed. | |
| if existing is exc_type: | |
| log.debug( | |
| 'Exception %r already registered; skipping re-registration', | |
| name, | |
| ) | |
| continue | |
| # Prevent silent overwriting of existing exception types or other | |
| # attributes in this module. | |
| raise ValueError( | |
| 'Cannot register exception type {!r}: name collides with ' | |
| 'existing attribute {!r} on tractor._exceptions'.format( | |
| exc_type, | |
| existing, | |
| ) | |
| ) | |
| log.debug( | |
| f'Register custom exception,\n' | |
| f'{exc_type!r}\n' | |
| ) | |
| setattr( | |
| _this_mod, | |
| name, |
Expose a copy of the current actor's `_runtime_vars` dict via a public fn; TODO to convert to `RuntimeVars` struct. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Allow external app code to register custom exception types on `._exceptions` so they can be re-raised on the receiver side of an IPC dialog via `get_err_type()`. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
So we can start transition from runtime-vars `dict` to a typed struct for better clarity and wire-ready monitoring potential, as well as better traceability when . Deats, - add a new `RuntimeVars(Struct)` with all fields from `_runtime_vars` dict typed out - include `__setattr__()` with `breakpoint()` for debugging any unexpected mutations. - add `.update()` method for batch-updating compat with `dict`. - keep old `_runtime_vars: dict` in place (we need to port a ton of stuff to adjust..). (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Export the new `RuntimeVars` struct and `get_runtime_vars()` from `tractor.__init__` and improve the accessor to optionally return the struct form. Deats, - add `RuntimeVars` and `get_runtime_vars` to `__init__.py` exports; alphabetize `_state` imports. - move `get_runtime_vars()` up in `_state.py` to sit right below `_runtime_vars` dict definition. - add `as_dict: bool = True` param so callers can get either the legacy `dict` or the new `RuntimeVars` struct. - drop the old stub fn at bottom of `_state.py`. - rm stale `from .msg.pretty_struct import Struct` comment. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Refactor the test-fn deco to use `wrapt.decorator` instead of `functools.wraps` for better fn-sig preservation and optional-args support via `PartialCallableObjectProxy`. Deats, - add `timeout` and `hide_tb` deco params - wrap test-fn body with `trio.fail_after(timeout)` - consolidate per-fixture `if` checks into a loop - add `iscoroutinefunction()` type-check on wrapped fn - set `__tracebackhide__` at each wrapper level Also, - update imports for new subpkg paths: `tractor.spawn._spawn`, `tractor.discovery._addr`, `tractor.runtime._state` (see upcoming, likely large patch commit ;) (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Restructure the flat `tractor/` top-level private mods into (more nested) subpackages: - `runtime/`: `_runtime`, `_portal`, `_rpc`, `_state`, `_supervise` - `spawn/`: `_spawn`, `_entry`, `_forkserver_override`, `_mp_fixup_main` - `discovery/`: `_addr`, `_discovery`, `_multiaddr` Each subpkg `__init__.py` is kept lazy (no eager imports) to avoid circular import issues. Also, - update all intra-pkg imports across ~35 mods to use the new subpkg paths (e.g. `from .runtime._state` instead of `from ._state`) (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Adjust all `tractor._state`, `tractor._addr`, `tractor._supervise`, etc. refs in tests and examples to use the new `runtime/`, `discovery/`, `spawn/` paths. Also, - use `tractor.debug_mode()` pub API instead of `tractor._state.debug_mode()` in a few test mods - add explicit `timeout=20` to `test_respawn_consumer_task` `@tractor_test` deco call (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
(this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Move the `Arbiter` class out of `runtime._runtime` into its logical home at `discovery._registry` as `Registrar(Actor)`. This completes the long-standing terminology migration from "arbiter" to "registrar/registry" throughout the codebase. Deats, - add new `discovery/_registry.py` mod with `Registrar` class + backward-compat `Arbiter = Registrar` alias. - rename `Actor.is_arbiter` attr -> `.is_registrar`; old attr now a `@property` with `DeprecationWarning`. - `_root.py` imports `Registrar` directly for root-actor instantiation. - export `Registrar` + `Arbiter` from `tractor.__init__`. - `_runtime.py` re-imports from `discovery._registry` for backward compat. Also, - update all test files to use `.is_registrar` (`test_local`, `test_rpc`, `test_spawning`, `test_discovery`, `test_multi_program`). - update "arbiter" -> "registrar" in comments/docstrings across `_discovery.py`, `_server.py`, `_transport.py`, `_testing/pytest.py`, and examples. - drop resolved TODOs from `_runtime.py` and `_root.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
(this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
(this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Add pre-flight import/collect checks, a mod-to-test mapping table, `--lf` re-run patterns, and a known-flaky test list to avoid wasted investigation time. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
e4bdf7c to
d05d471
Compare
Add `get_cpu_state()` helper to read CPU freq settings from `/sys/devices/system/cpu/` and use it to compensate the perf time-limit when `auto-cpufreq` (or similar) scales down the max frequency. Deats, - read `*_pstate_max_freq` and `scaling_max_freq` to compute a `cpu_scaled` ratio. - when `cpu_scaled != 1.`, increase `this_fast` limit proportionally (factoring dual-threaded cores). - log a warning via `test_log` when compensating. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Factor the CPU-freq-scaling helper out of `test_legacy_one_way_streaming` into `conftest.py` alongside a new `cpu_scaling_factor()` convenience fn that returns a latency-headroom multiplier (>= 1.0). Apply it to the two other flaky-timeout tests, - `test_cancel_via_SIGINT_other_task`: 2s -> scaled - `test_example[we_are_processes.py]`: 16s -> scaled Deats, - add `get_cpu_state()` + `cpu_scaling_factor()` to `conftest.py` so all test mods can share the logic. - catch `IndexError` (empty glob) in addition to `FileNotFoundError`. - rename `factor` var -> `headroom` at call sites for clarity on intent. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Verify registered custom error types round-trip correctly over IPC via `reg_err_types()` + `get_err_type()`. Deats, - `TestRegErrTypesPlumbing`: 5 unit tests for the type-registry plumbing (register, lookup, builtins, tractor-native types, unregistered returns `None`) - `test_registered_custom_err_relayed`: IPC end-to-end for a registered `CustomAppError` checking `.boxed_type`, `.src_type`, and `.tb_str` - `test_registered_another_err_relayed`: same for `AnotherAppError` (multi-type coverage) - `test_unregistered_custom_err_fails_lookup`: `xfail` documenting that `.boxed_type` can't resolve without `reg_err_types()` registration (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Make `RemoteActorError` resilient to unresolved custom error types so that errors from remote actors always relay back to the caller - even when the user hasn't called `reg_err_types()` to register the exc type. Deats, - `.src_type`: log warning + return `None` instead of raising `TypeError` which was crashing the entire `_deliver_msg()` -> `pformat()` chain before the error could be relayed. - `.boxed_type_str`: fallback to `_ipc_msg.boxed_type_str` when the type obj can't be resolved so the type *name* is always available. - `unwrap_src_err()`: fallback to `RuntimeError` preserving original type name + traceback. - `unpack_error()`: log warning when `get_err_type()` returns `None` telling the user to call `reg_err_types()`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Drop the `xfail` test and instead add a new one that ensures the `tractor._exceptions` fixes enable graceful relay of remote-but-unregistered error types via the unboxing of just the `rae.src_type_str/boxed_type_str` content. The test also ensures a warning is included with remote error content indicating the user should register their error type for effective cross-actor re-raising. Deats, - add `test_unregistered_err_still_relayed`: verify the `RemoteActorError` IS raised with `.boxed_type` as `None` but `.src_type_str`, `.boxed_type_str`, and `.tb_str` all preserved from the IPC msg. - drop `test_unregistered_boxed_type_resolution_xfail` since the new above case covers it and we don't need to have an effectively entirely repeated test just with an inverse assert as it's last line.. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
Add a teensie unit test to match. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
d05d471 to
7af5405
Compare
Add
reg_err_types()for custom remote exc registrySummary
reg_err_types()publicAPI in
_exceptionsenabling user-app code toregister custom
Exceptiontypes for cross-actorIPC relay and re-raise.
remote error type is relayed —
.src_typeandunpack_error()now log a warning and degradegracefully instead of raising
TypeError..boxed_type_strfalls backto the IPC-msg-encoded type-name string when the
type object can't be resolved locally; returns
'<unknown>'when no IPC msg exists..unwrap_remoteactor_err()falls back to
RuntimeErrorwrapping the originaltraceback when
src_typeis unresolvable.get_runtime_vars()copy-accessor to
_state.comprehensive
tests.test_reg_err_typescoveringunit-level
reg_err_types()/get_err_type()plumbing, IPC-level registered custom error
round-trips, and unregistered error relay with
string-level info preservation.
/pr-msgskill forcross-service PR/patch-request description
generation.
Motivation
Previously, if a remote actor raised a custom
Exceptionsubclass unknown to the receiving side,tractorwould crash internally (in.src_typeorunpack_error()) with an opaqueTypeError. Thismade it impossible for downstream libs/apps to
define and relay their own error hierarchies across
IPC without monkey-patching
tractor._exceptions.reg_err_types()provides a clean registrationhook, and the graceful-fallback path ensures even
unregistered types don't break the error relay
pipeline.
Scopes changed
tractor._exceptions.reg_err_types()— newpublic API for custom remote error type
registration.
adjusts
.RemoteActorError.src_type,.boxed_type_str,.unwrap_remoteactor_err()andunpack_error()in that module for gracefulfallback.
tractor._state.get_runtime_vars()— newcopy-accessor for the actor's runtime variables
dict.tests.test_reg_err_types— new test module:unit plumbing + IPC integration (335 lines).
.claude.skills.pr-msg—new
/pr-msgskill withSKILL.mdandformat-reference.md.(this pr content was generated in some part by
claude-code)