Skip to content

feat(governance): non-blocking LiveTrackEventDispatcher for /runtime/log#1776

Open
viswa-uipath wants to merge 1 commit into
mainfrom
feat/event-async
Open

feat(governance): non-blocking LiveTrackEventDispatcher for /runtime/log#1776
viswa-uipath wants to merge 1 commit into
mainfrom
feat/event-async

Conversation

@viswa-uipath

@viswa-uipath viswa-uipath commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

The runtime's TrackEventAuditSink invokes its injected track_event callable synchronously on the agent's hook thread. Wiring UiPathPlatformGovernanceProvider.track_event directly would block agent execution on the POST /runtime/log HTTP round-trip.

LiveTrackEventDispatcher wraps provider.track_event in a bounded ThreadPoolExecutor. The host wires dispatcher.dispatch into AuditManager(track_event=...) instead — same kwargs, but the HTTP work runs on a background worker.

What's in

  • LiveTrackEventDispatcher (packages/uipath-platform/src/uipath/platform/governance/_live_track_event_dispatcher.py)
    • Bounded pool (default max_workers=10, mirrors LiveTrackingSpanProcessor)
    • dispatch(*, event_name, data=None, operation_id=None) — drop-in for the provider's track_event signature
    • Fire-and-forget contract: dispatch returns immediately; worker exceptions are caught and logged at debug
    • shutdown(wait=True) drains in-flight submissions; idempotent (safe under multiple atexit hooks)
  • Exported on uipath.platform.governance alongside the existing UiPathPlatformGovernanceProvider
  • Tests (7 cases): non-blocking dispatch proven via threading.Event sync, kwarg forwarding, default flow-through (data=None, operation_id=None), exception swallowing, concurrency under burst (20 submissions), shutdown drain semantics, shutdown idempotence
  • Bumps: uipath-platform 0.1.84 → 0.1.85 (new public class needs a fresh wheel — 0.1.84 is already on PyPI)

Why a separate adapter (not built into the provider)

The provider's track_event is the synchronous platform call. Tests and other internal seams might legitimately want to block on it. Wrapping it in a thread pool by default would force every caller into a fire-and-forget shape and hide HTTP errors. Keeping the non-blocking concern as a separate class lets the host opt into it deliberately for the runtime-sink wiring path.

Test plan

  • uv run ruff check . — clean
  • uv run ruff format --check . — clean
  • uv run mypy src tests — clean (205 source files)
  • uv run pytest tests/services/test_live_track_event_dispatcher.py tests/services/test_governance_service.py tests/services/test_governance_provider.py — 63 passed
  • uv sync --locked — no drift across all three workspace packages
  • PyPI version check: 0.1.85 not yet published; 0.1.84 is the current latest

🤖 Generated with Claude Code

Development Packages

uipath-platform

[project]
dependencies = [
  # Exact version (copy-paste ready):
  "uipath-platform==0.1.85.dev1017767059",

  # Any version from this PR (uncomment to use a range instead):
  # "uipath-platform>=0.1.85.dev1017760000,<0.1.85.dev1017770000",
]

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

[tool.uv.sources]
uipath-platform = { index = "testpypi" }

uipath

[project]
dependencies = [
  # Exact version (copy-paste ready):
  "uipath==2.12.0.dev1017767059",

  # Any version from this PR (uncomment to use a range instead):
  # "uipath>=2.12.0.dev1017760000,<2.12.0.dev1017770000",
]

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

[tool.uv.sources]
uipath = { index = "testpypi" }
uipath-platform = { index = "testpypi" }

[tool.uv]
override-dependencies = ["uipath-platform==0.1.85.dev1017767059"]

Copilot AI review requested due to automatic review settings June 30, 2026 15:56
@github-actions github-actions Bot added test:uipath-langchain Triggers tests in the uipath-langchain-python repository test:uipath-integrations labels Jun 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 new non-blocking adapter in uipath-platform to dispatch governance track_event calls from a background thread pool, so runtime audit logging won’t block on the /runtime/log HTTP round-trip.

Changes:

  • Introduces LiveTrackEventDispatcher to submit provider.track_event(...) work to a ThreadPoolExecutor and provides a shutdown() drain hook.
  • Exports the new dispatcher from uipath.platform.governance and adds a dedicated pytest suite validating non-blocking behavior, forwarding, exception swallowing, and shutdown semantics.
  • Bumps uipath-platform version to 0.1.85 and updates workspace lockfiles accordingly.

Reviewed changes

Copilot reviewed 4 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/uipath/uv.lock Updates workspace lock to uipath-platform==0.1.85.
packages/uipath-platform/uv.lock Updates package lock to uipath-platform==0.1.85.
packages/uipath-platform/src/uipath/platform/governance/_live_track_event_dispatcher.py Adds the non-blocking dispatcher implementation around provider.track_event.
packages/uipath-platform/src/uipath/platform/governance/init.py Exports LiveTrackEventDispatcher on the governance public surface.
packages/uipath-platform/tests/services/test_live_track_event_dispatcher.py Adds dispatcher unit tests (non-blocking, forwarding, concurrency, shutdown).
packages/uipath-platform/pyproject.toml Bumps uipath-platform version to 0.1.85.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

viswa-uipath added a commit that referenced this pull request Jun 30, 2026
…eTrackEventDispatcher

Addresses Copilot review on #1776:

- **Bounded in-flight cap.** ``ThreadPoolExecutor``'s own work queue is
  unbounded — without an explicit cap, a slow backend would accumulate
  submissions in memory without limit. Added a
  ``threading.BoundedSemaphore(max_workers * inflight_oversubscription)``
  that ``dispatch`` acquires non-blocking; saturated calls drop with a
  warning rather than queueing forever. Mirrors the pattern in
  ``GuardrailCompensator`` so both governance dispatchers share one
  shape.
- **Post-shutdown safety.** ``executor.submit`` raises ``RuntimeError``
  after :meth:`shutdown`. Wrapped in try/except so a late dispatch (e.g.
  from an atexit hook after the pool drained) can't crash the hook
  thread.
- **Worker tracebacks.** Added ``exc_info=True`` to the worker-failure
  debug log so backend / serialisation failures surface a stack trace
  for diagnostics.
- **Docstring** rewritten to describe the actual failure modes (saturated
  pool, pool shut down, worker exception) — replaces the prior
  inaccurate claim that ``executor.submit`` provided backpressure.

Tests
-----

- ``test_dispatch_after_shutdown_is_silent`` — dispatch after shutdown
  doesn't raise and doesn't call the provider.
- ``test_dispatch_drops_when_inflight_saturated`` — fills the in-flight
  cap with blocked workers, asserts the next submission is dropped and
  the provider is never called for the dropped event.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The runtime's TrackEventAuditSink invokes its injected ``track_event``
callable synchronously on the agent's hook thread. Wiring the
platform provider's method directly would block agent execution on
governance telemetry's HTTP round-trip (the ``POST /runtime/log``
write to App Insights).

LiveTrackEventDispatcher wraps ``provider.track_event`` in a bounded
``ThreadPoolExecutor`` (default 10 workers, matching
``LiveTrackingSpanProcessor`` and sized for the bursty-but-not-
sustained shape of governance events). The host wires
``dispatcher.dispatch`` into ``AuditManager(track_event=...)``
instead of the provider's method — same kwargs, but the HTTP work
runs on a background worker.

Failure modes — all silent on the hook thread
---------------------------------------------

- **Saturated pool**: ThreadPoolExecutor's own queue is unbounded, so
  the dispatcher uses a ``threading.BoundedSemaphore`` capped at
  ``max_workers * inflight_oversubscription`` (default 10 × 4 = 40).
  When the cap is reached, ``dispatch`` drops with a warning rather
  than queueing forever. Mirrors GuardrailCompensator so both
  governance dispatchers share one shape.
- **Pool shut down**: ``executor.submit`` raises ``RuntimeError`` after
  shutdown — caught, semaphore slot released, logged at debug. A
  late dispatch (e.g. atexit hook after the pool drained) never
  crashes the hook thread.
- **Worker exception**: provider HTTP call may raise (serialisation,
  5xx, transport). Worker catches, logs at debug with
  ``exc_info=True`` for diagnostics, releases the in-flight slot.

``shutdown(wait=True)`` drains in-flight submissions so process
teardown doesn't lose telemetry. Idempotent — safe to call from
multiple atexit hooks.

Public-surface discipline
-------------------------

The class is intentionally **not** re-exported from
``governance/__init__.py``. It's host-wiring glue for the runtime
sink, not a customer-facing API — same principle radu applied to
``_track_event`` on ``GovernanceService`` (PR #1745): internal seams
shouldn't sit on the docs-facing ``uipath.platform.governance``
import path that ``mkdocstrings`` auto-discovers. Internal callers
use the explicit private path:

    from uipath.platform.governance._live_track_event_dispatcher import (
        LiveTrackEventDispatcher,
    )

Tests cover the boundary contract (dispatch non-blocking via a
``threading.Event`` sync proof), kwarg forwarding, default
flow-through, exception swallowing, concurrency under burst,
shutdown drain, shutdown idempotence, post-shutdown silent dispatch,
and saturated-pool drop semantics.

Bumps:
- uipath-platform 0.1.84 → 0.1.85 (new class needs a fresh wheel;
  ``0.1.84`` is already on PyPI)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚨 Heads up: uipath-integrations cross-tests are FAILING 🚨

Your changes may break one or more integrations in uipath-integrations-python:

  • uipath-openai-agents
  • uipath-google-adk
  • uipath-agent-framework
  • uipath-llamaindex
  • uipath-pydantic-ai

⚠️ These checks are NOT enforced by branch protection rules. Please review the failures before merging.

🔍 Inspect the failed run →

@sonarqubecloud

Copy link
Copy Markdown

@radu-mocanu radu-mocanu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

changes look good but please follow-up with the async variant as well. we should not rely on synchronous API calls

@github-actions

Copy link
Copy Markdown

🚨 Heads up: uipath-langchain cross-tests are FAILING 🚨

Your changes may break the uipath-langchain-python integration.

⚠️ These checks are NOT enforced by branch protection rules. Please review the failures before merging.

🔍 Inspect the failed run →

@viswa-uipath viswa-uipath added the build:dev Create a dev build from the pr label Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build:dev Create a dev build from the pr test:uipath-integrations test:uipath-langchain Triggers tests in the uipath-langchain-python repository

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants