Skip to content

Conversation

@nicoschmdt
Copy link
Contributor

@nicoschmdt nicoschmdt commented Dec 4, 2025

Part of #3417

test image: nicoschmdt/blueos-core:queryables-kraken

Summary by Sourcery

Integrate zenoh queryables into the Kraken service and expose key v2 API endpoints over zenoh alongside existing HTTP routes.

New Features:

  • Expose Kraken manifest, container, extension, and jobs v2 endpoints as zenoh queryables via a new ZenohRouter integration.

Enhancements:

  • Introduce a reusable zenoh_helper module that manages a shared zenoh session, maps FastAPI route paths to zenoh key expressions, and decorates routers to capture route metadata.
  • Wire zenoh queryable routing into the Kraken FastAPI application with a dedicated zenoh router hierarchy and ensure the zenoh session is closed on application shutdown.

@sourcery-ai
Copy link

sourcery-ai bot commented Dec 4, 2025

Reviewer's Guide

Introduce a Zenoh integration layer for the Kraken service by adding a reusable ZenohRouter helper, wiring it into the FastAPI app lifecycle, and exposing existing v2 HTTP endpoints (container, extension, jobs, manifest) as Zenoh queryables without changing their core business logic.

Sequence diagram for handling a Zenoh query to a Kraken HTTP-backed endpoint

sequenceDiagram
    actor QC as ZenohClient
    participant ZN as ZenohNetwork
    participant ZS as ZenohSession
    participant ZR as ZenohRouter_manifest
    participant EP as fetch_consolidated

    QC->>ZN: send query
    ZN->>ZS: deliver query for key_expr kraken/manifest/consolidated
    ZS->>ZR: invoke registered queryable wrapper

    activate ZR
    ZR->>ZR: extract params from query
    ZR->>EP: call async endpoint(**params)
    activate EP
    EP-->>ZR: list[RepositoryEntry]
    deactivate EP

    ZR->>ZS: query.reply(key_expr, json.dumps(response))
    deactivate ZR

    ZS->>ZN: send reply sample
    ZN-->>QC: deliver response payload
Loading

Class diagram for Zenoh helper utilities and updated routers

classDiagram
    class ZenohSession {
        - zenoh.Session session
        - zenoh.Config config
        - ThreadPoolExecutor _executor
        + __init__() void
        + zenoh_config() void
        + close() void
    }

    class ZenohRouter {
        - str prefix
        - List~Tuple~str, Callable~~~ routes
        + __init__(prefix str)
        + queryable() Callable
        + declare() void
        + include_router(router ZenohRouter) void
    }

    class FastAPI {
    }

    class APIRouter {
    }

    class route_info_decorator {
        <<function>>
    }

    class apply_route_decorator {
        <<function>>
        + apply_route_decorator(app T) T
    }

    class original_container_router_v2 {
        <<APIRouter>>
    }
    class container_router_v2 {
        <<APIRouter (decorated)>>
    }
    class original_extension_router_v2 {
        <<APIRouter>>
    }
    class extension_router_v2 {
        <<APIRouter (decorated)>>
    }
    class original_jobs_router_v2 {
        <<APIRouter>>
    }
    class jobs_router_v2 {
        <<APIRouter (decorated)>>
    }
    class original_manifest_router_v2 {
        <<APIRouter>>
    }
    class manifest_router_v2 {
        <<APIRouter (decorated)>>
    }

    class zenoh_container_router {
        <<ZenohRouter>>
    }
    class zenoh_extension_router {
        <<ZenohRouter>>
    }
    class zenoh_jobs_router {
        <<ZenohRouter>>
    }
    class zenoh_manifest_router {
        <<ZenohRouter>>
    }
    class zenoh_router {
        <<ZenohRouter kraken root>>
    }

    ZenohSession <.. ZenohRouter : uses zenoh_session

    route_info_decorator --> APIRouter : wraps HTTP verb methods
    apply_route_decorator --> route_info_decorator

    apply_route_decorator --> original_container_router_v2 : decorates
    original_container_router_v2 <|-- container_router_v2

    apply_route_decorator --> original_extension_router_v2 : decorates
    original_extension_router_v2 <|-- extension_router_v2

    apply_route_decorator --> original_jobs_router_v2 : decorates
    original_jobs_router_v2 <|-- jobs_router_v2

    apply_route_decorator --> original_manifest_router_v2 : decorates
    original_manifest_router_v2 <|-- manifest_router_v2

    zenoh_container_router --> container_router_v2 : queryable() on endpoints
    zenoh_extension_router --> extension_router_v2 : queryable() on endpoints
    zenoh_jobs_router --> jobs_router_v2 : queryable() on endpoints
    zenoh_manifest_router --> manifest_router_v2 : queryable() on endpoints

    zenoh_router --> zenoh_container_router : include_router
    zenoh_router --> zenoh_extension_router : include_router
    zenoh_router --> zenoh_jobs_router : include_router
    zenoh_router --> zenoh_manifest_router : include_router

    FastAPI --> zenoh_router : declares queryables in lifespan
Loading

File-Level Changes

Change Details Files
Add a reusable Zenoh helper module providing a global Zenoh session, router abstraction, and decorators to map FastAPI routes to Zenoh queryables.
  • Create ZenohSession to configure and open a zenoh client session with a thread pool executor and close/shutdown support.
  • Implement ZenohRouter to register queryable handlers, declare them on the Zenoh session, and compose routers via include_router.
  • Add route_info_decorator and apply_route_decorator utilities to capture FastAPI route paths for later conversion into Zenoh key expressions via sanitize_route_path.
core/libs/commonwealth/src/commonwealth/utils/zenoh_helper.py
Integrate Zenoh lifecycle and top-level router into the Kraken FastAPI application.
  • Define an async lifespan context manager that ensures the global zenoh_session is closed when the FastAPI app shuts down.
  • Pass the lifespan handler into VersionedFastAPI application initialization.
  • Instantiate a top-level ZenohRouter("kraken"), include feature-specific Zenoh routers (container, extension, jobs, manifest), and declare all queryables at app startup.
core/services/kraken/api/app.py
Expose v2 manifest endpoints as Zenoh queryables via a dedicated Zenoh router while preserving HTTP behavior.
  • Wrap the original manifest APIRouter with apply_route_decorator to record route paths while keeping the public router name manifest_router_v2.
  • Create zenoh_manifest_router = ZenohRouter("manifest") and register it in the package exports.
  • Annotate manifest fetch endpoints with @zenoh_manifest_router.queryable() so each becomes a Zenoh queryable keyed by a sanitized version of its HTTP path.
core/services/kraken/api/v2/routers/manifest.py
core/services/kraken/api/v2/routers/__init__.py
Expose v2 container endpoints as Zenoh queryables via a dedicated Zenoh router while preserving HTTP behavior.
  • Rename the base router to original_container_router_v2 and wrap it with apply_route_decorator to retain route metadata.
  • Instantiate zenoh_container_router = ZenohRouter("container") and export it through the routers package.
  • Decorate all container read/list endpoints with @zenoh_container_router.queryable() so they can be invoked over Zenoh with parameters corresponding to the HTTP path/query.
core/services/kraken/api/v2/routers/container.py
core/services/kraken/api/v2/routers/__init__.py
Expose v2 extension endpoints as Zenoh queryables via a dedicated Zenoh router while preserving HTTP behavior.
  • Rename the base router to original_extension_router_v2 and wrap it with apply_route_decorator.
  • Instantiate zenoh_extension_router = ZenohRouter("extension") and export it via all.
  • Decorate extension fetch endpoints with @zenoh_extension_router.queryable() to expose them as Zenoh queryables keyed by sanitized HTTP paths.
core/services/kraken/api/v2/routers/extension.py
core/services/kraken/api/v2/routers/__init__.py
Expose v2 jobs endpoints as Zenoh queryables via a dedicated Zenoh router while preserving HTTP behavior.
  • Rename the base router to original_jobs_router_v2 and wrap it with apply_route_decorator.
  • Instantiate zenoh_jobs_router = ZenohRouter("jobs") and export it through the routers package.
  • Add @zenoh_jobs_router.queryable() to the jobs list and get-by-identifier endpoints so they are available via Zenoh in addition to HTTP.
core/services/kraken/api/v2/routers/jobs.py
core/services/kraken/api/v2/routers/__init__.py

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The interaction between apply_route_decorator and @zenoh_*_router.queryable() looks off: route_info_decorator sets _route_path on the original function but returns the FastAPI-decorated wrapper without that attribute, and since @zenoh_router.queryable() is applied last, it sees a function with no _route_path so all Zenoh queryables end up with an empty path; consider propagating _route_path onto the wrapper returned by deco(...) or flipping the decorator order so the Zenoh decorator wraps the original function before FastAPI.
  • In ZenohRouter.queryable, each query uses asyncio.run inside a thread, which creates a new event loop per call and assumes the handler is async; you may want to support both sync and async handlers and reuse an event loop (e.g., with asyncio.run_coroutine_threadsafe on a long-lived loop) to avoid the overhead and potential issues of repeated asyncio.run.
  • The Zenoh configuration in ZenohSession.zenoh_config is currently hardcoded (e.g. tcp/127.0.0.1:7447, mode: client); if this is expected to run in different environments, consider wiring these values to existing configuration mechanisms so the service can be pointed at different Zenoh deployments without code changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The interaction between `apply_route_decorator` and `@zenoh_*_router.queryable()` looks off: `route_info_decorator` sets `_route_path` on the original function but returns the FastAPI-decorated wrapper without that attribute, and since `@zenoh_router.queryable()` is applied last, it sees a function with no `_route_path` so all Zenoh queryables end up with an empty path; consider propagating `_route_path` onto the wrapper returned by `deco(...)` or flipping the decorator order so the Zenoh decorator wraps the original function before FastAPI.
- In `ZenohRouter.queryable`, each query uses `asyncio.run` inside a thread, which creates a new event loop per call and assumes the handler is async; you may want to support both sync and async handlers and reuse an event loop (e.g., with `asyncio.run_coroutine_threadsafe` on a long-lived loop) to avoid the overhead and potential issues of repeated `asyncio.run`.
- The Zenoh configuration in `ZenohSession.zenoh_config` is currently hardcoded (e.g. `tcp/127.0.0.1:7447`, `mode: client`); if this is expected to run in different environments, consider wiring these values to existing configuration mechanisms so the service can be pointed at different Zenoh deployments without code changes.

## Individual Comments

### Comment 1
<location> `core/libs/commonwealth/src/commonwealth/utils/zenoh_helper.py:19-21` </location>
<code_context>
+    config: zenoh.Config
+    _executor: concurrent.futures.ThreadPoolExecutor | None = None
+
+    def __init__(self) -> None:
+        self.zenoh_config()
+        self.session = zenoh.open(self.config)
+
+        self._executor = concurrent.futures.ThreadPoolExecutor(
</code_context>

<issue_to_address>
**issue (bug_risk):** Guard against failures when establishing the Zenoh session at import time.

Calling `zenoh.open(self.config)` in `__init__` means an unavailable broker or invalid config will raise immediately and can prevent the service from starting cleanly. Please wrap this in try/except, log the failure, and leave `self.session` as `None` so that downstream code (e.g. `declare()`) can detect the missing session and skip registration instead of crashing the process.
</issue_to_address>

### Comment 2
<location> `core/libs/commonwealth/src/commonwealth/utils/zenoh_helper.py:71-75` </location>
<code_context>
+                zenoh_path = ""
+
+            def wrapper(query: zenoh.Query) -> None:
+                params = dict(query.parameters)  # type: ignore
+
+                async def _handle_async() -> None:
+                    try:
+                        response = await func(**params)
+                        if response is not None:
+                            query.reply(query.selector.key_expr, json.dumps(response, default=str))
</code_context>

<issue_to_address>
**question (bug_risk):** Passing raw Zenoh parameters directly to FastAPI endpoints may cause type/shape mismatches.

`query.parameters` will typically be strings/a flat mapping, but here they’re unpacked directly into the endpoint callable, skipping FastAPI’s normal parsing and validation. This risks runtime errors or incorrect types for parameters that should be ints/bools or have required/optional semantics. Consider adding an adapter that performs basic coercion/validation before calling the endpoint, or restricting which endpoints are exposed over Zenoh and validating their parameters explicitly.
</issue_to_address>

### Comment 3
<location> `core/libs/commonwealth/src/commonwealth/utils/zenoh_helper.py:65-69` </location>
<code_context>
    def queryable(self) -> Callable[[Callable[..., Any]], Callable[[zenoh.Query], None]]:
        def decorator(func: Callable[..., Any]) -> Callable[[zenoh.Query], None]:
            route_path = getattr(func, "_route_path", None)
            if route_path is not None:
                zenoh_path = sanitize_route_path(route_path)
            else:
                zenoh_path = ""

            def wrapper(query: zenoh.Query) -> None:
                params = dict(query.parameters)  # type: ignore

                async def _handle_async() -> None:
                    try:
                        response = await func(**params)
                        if response is not None:
                            query.reply(query.selector.key_expr, json.dumps(response, default=str))
                    except Exception as e:
                        error_response = {"error": str(e)}
                        query.reply(query.selector.key_expr, json.dumps(error_response))

                def run_async() -> None:
                    asyncio.run(_handle_async())

                if zenoh_session._executor:
                    zenoh_session._executor.submit(run_async)

            self.routes.append((zenoh_path, wrapper))  # type: ignore[arg-type]
            return wrapper

        return decorator

</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace if statement with if expression ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))

```suggestion
            zenoh_path = sanitize_route_path(route_path) if route_path is not None else ""
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

"mode": "client",
"connect/endpoints": ["tcp/127.0.0.1:7447"],
"adminspace": {"enabled": True},
"metadata": {"name": "zenoh-queryables"},
Copy link
Member

Choose a reason for hiding this comment

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

this should be the name of the service, otherwise all services will show as zenoh-queryables when fetching the metadata of each client, no ? (Check BlueOS zenoh network page)
also, can we somehow use the same client of the log ? or share the clients ?
We already have on in commonwealth. I believe that's better to organize it first and share a single client for both, not sure if it's the best idea, but it sounds better than having multiple clients per service.

zenoh_router.include_router(zenoh_container_router)
zenoh_router.include_router(zenoh_extension_router)
zenoh_router.include_router(zenoh_jobs_router)
zenoh_router.include_router(zenoh_manifest_router)
Copy link
Member

Choose a reason for hiding this comment

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

This include_router sounds a bit unnecessary, you could extract the router via something like...

for route in app.router.routes:
    if isinstance(route, APIRoute):
        print(route.path)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But those are ZenohRouters, they are not included in the fastapi routers. I had to declare one ZenohRouter in each file that had an APIRoute.

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