Skip to content

Remote exc type registry#426

Open
goodboy wants to merge 19 commits intomainfrom
remote_exc_type_registry
Open

Remote exc type registry#426
goodboy wants to merge 19 commits intomainfrom
remote_exc_type_registry

Conversation

@goodboy
Copy link
Owner

@goodboy goodboy commented Mar 25, 2026

Add reg_err_types() for custom remote exc registry

Summary

  • f3726cf9 Add reg_err_types() public
    API in _exceptions enabling user-app code to
    register custom Exception types for cross-actor
    IPC relay and re-raise.
  • 1dc45e7a Fix crash when an unregistered
    remote error type is relayed — .src_type and
    unpack_error() now log a warning and degrade
    gracefully instead of raising TypeError.
  • e4bdf7ca .boxed_type_str falls back
    to the IPC-msg-encoded type-name string when the
    type object can't be resolved locally; returns
    '<unknown>' when no IPC msg exists.
  • 1dc45e7a .unwrap_remoteactor_err()
    falls back to RuntimeError wrapping the original
    traceback when src_type is unresolvable.
  • 39b4f083 Add get_runtime_vars()
    copy-accessor to _state.
  • af5fcacf c1743fab Add
    comprehensive tests.test_reg_err_types covering
    unit-level reg_err_types() / get_err_type()
    plumbing, IPC-level registered custom error
    round-trips, and unregistered error relay with
    string-level info preservation.
  • f6620390 Add /pr-msg skill for
    cross-service PR/patch-request description
    generation.

Motivation

Previously, if a remote actor raised a custom
Exception subclass unknown to the receiving side,
tractor would crash internally (in .src_type or
unpack_error()) with an opaque TypeError. This
made 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 registration
hook, and the graceful-fallback path ensures even
unregistered types don't break the error relay
pipeline.

Scopes changed

  • f3726cf9
    tractor._exceptions.reg_err_types() — new
    public API for custom remote error type
    registration.
    • 1dc45e7a e4bdf7ca Also
      adjusts .RemoteActorError.src_type,
      .boxed_type_str,
      .unwrap_remoteactor_err() and
      unpack_error() in that module for graceful
      fallback.
  • 39b4f083
    tractor._state.get_runtime_vars() — new
    copy-accessor for the actor's runtime variables
    dict.
  • af5fcacf c1743fab
    tests.test_reg_err_types — new test module:
    unit plumbing + IPC integration (335 lines).
  • f6620390 .claude.skills.pr-msg
    new /pr-msg skill with SKILL.md and
    format-reference.md.

(this pr content was generated in some part by
claude-code)

Copilot AI review requested due to automatic review settings March 25, 2026 00:15
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
def get_err_type(type_name: str) -> BaseException|None:
def get_err_type(type_name: str) -> Type[BaseException] | None:

Copilot uses AI. Check for mistakes.
Comment on lines 453 to 469
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
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
# TODO, maybe support also serializing the
# `ExceptionGroup.exeptions: list[BaseException]` set under
# certain conditions?
# `ExceptionGroup.exeptions: list[BaseException]`
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling in comment: ExceptionGroup.exeptions should be ExceptionGroup.exceptions.

Suggested change
# `ExceptionGroup.exeptions: list[BaseException]`
# `ExceptionGroup.exceptions: list[BaseException]`

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +132
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 (
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

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
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reg_err_types() docstring has a typo: “recevier side” -> “receiver side”.

Suggested change
remotely; enables them being re-raised on the recevier side of
remotely; enables them being re-raised on the receiver side of

Copilot uses AI. Check for mistakes.
Comment on lines +203 to +209
log.debug(
f'Register custom exception,\n'
f'{exc_type!r}\n'
)
setattr(
_this_mod,
exc_type.__name__,
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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,

Copilot uses AI. Check for mistakes.
goodboy added 13 commits March 25, 2026 03:35
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
@goodboy goodboy force-pushed the remote_exc_type_registry branch from e4bdf7c to d05d471 Compare March 25, 2026 17:31
goodboy added 6 commits March 25, 2026 16:19
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
@goodboy goodboy force-pushed the remote_exc_type_registry branch from d05d471 to 7af5405 Compare March 25, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants