Skip to content

feat(ray): Add actor support to RayIntegration#1

Open
scorchio wants to merge 6 commits into
masterfrom
feat/ray-actor-support
Open

feat(ray): Add actor support to RayIntegration#1
scorchio wants to merge 6 commits into
masterfrom
feat/ray-actor-support

Conversation

@scorchio

Copy link
Copy Markdown
Owner

Summary

  • Adds worker-side actor method wrapping (_patch_actor_class, _wrap_actor_method, _wrap_actor_init) to capture exceptions and continue distributed traces inside Ray actor methods
  • Adds caller-side ActorHandle patching (_patch_actor_handle, _wrap_actor_class_remote) to create submit spans and inject _sentry_tracing trace headers across Ray process boundaries
  • Handles async actor __init__ and methods (required for Ray Serve compatibility) by branching on inspect.iscoroutinefunction
  • Guards Ray-internal classes (_is_ray_internal_class) from being patched to avoid driver/worker module identity mismatches
  • Uses cls.__dict__ in _patch_actor_class to avoid MRO double-wrapping when a subclass and superclass are both @ray.remote
  • Guards private method calls from _sentry_tracing injection since only public methods are wrapped on the worker side

Known limitations

  • Actor __init__ errors are captured without trace context (Ray calls __init__ directly, not via _actor_method_call, so there is no mechanism to propagate headers)
  • Submit spans always report OK status — _actor_method_call returns an ObjectRef (future); exceptions only surface at ray.get() time

🤖 Generated with Claude Code

scorchio and others added 6 commits June 30, 2026 17:34
Ray actors (classes decorated with @ray.remote) were previously
skipped by the integration. This adds:

- Worker-side tracing: each public actor method is wrapped to receive
  _sentry_tracing, continue the distributed trace, and create a
  queue.task.ray span or transaction (both static and stream modes).
- Caller-side tracing: ActorHandle._actor_method_call is patched per
  instance to create a queue.submit.ray span and propagate trace
  headers via the _sentry_tracing kwarg.
- Error capture for actor method exceptions and __init__ failures.
- Support for both @ray.remote and @ray.remote(**opts) decorator forms.

Key implementation note: Ray calls inspect.unwrap() before reading
actor method signatures, which follows __wrapped__ (set by
functools.wraps) to the original method. We delete __wrapped__ after
wrapping so Ray sees new_method's actual signature, which already
includes _sentry_tracing as a real keyword-only parameter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ray Serve actors (and other async Ray actors) use async def __init__
and async def methods. The previous implementation wrapped them with
synchronous wrappers, which called the original coroutine function but
never awaited the result — causing Ray to see an unawaited coroutine
instead of a completed init, manifesting as:

  TypeError: object of type 'coroutine' has no len()

Fix: detect inspect.iscoroutinefunction and produce async wrappers for
both _wrap_actor_init and _wrap_actor_method. Sentry's start_span and
start_transaction context managers are synchronous and can be used with
regular `with` inside async functions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…h mismatch

Ray's own actors (ServeController, ProxyActor, etc.) are defined in ray.*
modules. Worker processes import these classes by name from the module,
so setattr patches applied in the driver never reach the worker. The
driver-side handle still injected _sentry_tracing into kwargs, but the
worker's unpatched method didn't accept it, causing:

  TypeError: ServeController.get_proxies() got an unexpected keyword argument '_sentry_tracing'

Note: __init__ was unaffected because Ray's _modify_class() explicitly
copies __init__ (Class.__init__ = cls.__init__), making it a closure
that cloudpickle ships directly rather than importing by name.

Fix: introduce _is_ray_internal_class() and skip both method patching
and handle patching for any class whose __module__ starts with 'ray.'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ss guard

- test_tracing_in_async_ray_actors: catches sync-wrapper-on-async-method
  bug (unawaited coroutine crashes actor creation)
- test_ray_internal_actors_not_patched: verifies _is_ray_internal_class
  classification and that ray.* module classes are not patched with
  _sentry_tracing (driver/worker module mismatch would cause TypeError)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Guard private/dunder method calls in new_actor_method_call: _patch_actor_class only wraps public methods on the worker, so injecting _sentry_tracing into private calls would cause TypeError
- Use cls.__dict__ instead of inspect.getmembers in _patch_actor_class to avoid MRO double-wrapping when subclass and superclass are both @ray.remote decorated
- Apply functools.update_wrapper on new_class_remote to preserve any __dict__ attributes (e.g. options/bind) from the original .remote callable
- Add explanatory comment in _wrap_actor_init clarifying why del __wrapped__ is intentionally omitted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ray Serve logs "Request failed." with exc_info for every failed request via
the "ray.serve" logger, and the same exception is already captured through
the ASGI integration. DedupeIntegration cannot deduplicate the two capture
paths because Ray Serve runs the ASGI app in a separate asyncio task, so the
dedupe ContextVar never spans both, resulting in duplicate error events.

Suppressing the framework's error logger is the established pattern used by
several other integrations (aiohttp.server, django.request, celery.worker.job,
rq.worker, tornado.access). Note that ignore_logger affects error events only
-- Sentry Logs have a separate ignore list, so "Request failed." remains
visible in the Logs product; this is intentional, same as django.server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant