Skip to content

Conversation

@NickCao
Copy link
Collaborator

@NickCao NickCao commented Feb 20, 2025

Summary by CodeRabbit

  • Documentation

    • Updated the API reference to reflect the new asynchronous, function-based structure for network adapters.
  • Refactor

    • Redesigned various networking adapters and port forwarding components to use asynchronous context managers.
    • Streamlined client integration with improved resource cleanup and error handling.
    • Adjusted default network settings for more robust connection management.
    • Enhanced context management across multiple components using ExitStack.
    • Removed class-based structures in favor of function-based implementations for several adapters.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2025

Walkthrough

This pull request refactors multiple network adapters and related client adapters by converting class-based implementations into asynchronous context manager functions. The documentation is updated accordingly, replacing .. autoclass:: with .. autofunction:: for several adapter classes. Default parameter values in the temporary TCP listener were revised, and error handling in resource cleanup routines was enhanced. These systematic changes affect Fabric, Novnc, Pexpect, TCP/Unix portforward, Opendal adapters, and the ClientAdapter component.

Changes

File(s) Change Summary
docs/source/api-reference/adapters/network.md Updated documentation directives: replaced .. autoclass:: with .. autofunction:: for adapter classes.
packages/jumpstarter-driver-network/…/adapters/{fabric.py, novnc.py, pexpect.py, portforward.py} Refactored adapter implementations by removing class-based structures in favor of asynchronous context managers; updated parameters and wrapped functions with @blocking and @asynccontextmanager.
packages/jumpstarter-driver-opendal/…/adapter.py Converted OpendalAdapter from a class into an async context manager function, removing the dependency on ClientAdapter.
packages/jumpstarter/jumpstarter/…/adapters.py Removed the class-based ClientAdapter and introduced a new blocking decorator to wrap asynchronous context managers.
packages/jumpstarter/jumpstarter/common/tempfile.py Modified TemporaryTcpListener defaults: set local_host to "127.0.0.1" and reuse_port to True.
packages/jumpstarter/jumpstarter/common/utils.py Enhanced error handling in the serve_async, serve, and env_async functions by using a try–finally block for reliable client resource cleanup.

Sequence Diagram(s)

sequenceDiagram
    participant C as Client
    participant A as Adapter Function
    participant L as TemporaryTcpListener

    C->>A: Invoke adapter (e.g., FabricAdapter, NovncAdapter)
    A->>L: Enter async context to initialize connection
    L-->>A: Provide connection details
    A-->>C: Yield connection object for usage
    C->>A: Operate with connection
    A->>L: Exit context and cleanup resources
Loading

Possibly related PRs

  • Init UnixPortforwardAdapter #248: Addresses related refactoring in the documentation and structure for TcpPortforwardAdapter and UnixPortforwardAdapter, aligning with changes made in this PR.

Suggested reviewers

  • mangelajo

Poem

I'm a rabbit on a code-bound run,
Hopping through async fields with fun,
Classes turned functions; sleek and bright,
Context managers guide the flight,
Clean code leaps, a joyful sight! 🐇✨
Let's celebrate the new paths done!

✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@netlify
Copy link

netlify bot commented Feb 20, 2025

Deploy Preview for jumpstarter-docs failed. Why did it fail? →

Name Link
🔨 Latest commit 83d06fb
🔍 Latest deploy log https://app.netlify.com/sites/jumpstarter-docs/deploys/67bca046a221990008e3038e

@NickCao
Copy link
Collaborator Author

NickCao commented Feb 20, 2025

Followup to #301, ensures that clients are properly closed on sys.exit.

@NickCao NickCao marked this pull request as draft February 20, 2025 15:01
@NickCao NickCao force-pushed the context-manager-revamp branch from 6061bf4 to a55e0a8 Compare February 20, 2025 16:57
@NickCao NickCao changed the title Close client in the client_from_path context manager Revamp context managers, part 1 Feb 20, 2025
@NickCao NickCao marked this pull request as ready for review February 20, 2025 17:00
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
packages/jumpstarter/jumpstarter/client/adapters.py (1)

5-12: Prevent potential KeyError if 'client' is missing in kwargs.
Currently, this code expects 'client' to exist, which may lead to a runtime error if it’s not provided. Consider adding a graceful check or documentation to mitigate the risk:

 def wrapper(*args, **kwargs):
+    client = kwargs.get("client")
+    if not client:
+        raise ValueError("'client' is missing in kwargs")
     with client.portal.wrap_async_context_manager(f(*args, **kwargs)) as res:
         yield res
packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/pexpect.py (1)

12-21: Consider adding error handling for socket connection.
If sock.connect(addr) fails or times out, the async block will raise an exception. Adding a try-except block could improve resilience:

async def AsyncPexpectAdapter(*, client: DriverClient, method: str = "connect"):
    async with AsyncTcpPortforwardAdapter(client=client, method=method) as addr:
        sock = await run_sync(socket.socket, socket.AF_INET, socket.SOCK_STREAM)
-       await run_sync(sock.connect, addr)
+       try:
+           await run_sync(sock.connect, addr)
+       except OSError as err:
+           await run_sync(sock.close)
+           raise RuntimeError(f"Failed to connect to {addr}") from err

        try:
            yield fdspawn(sock)
        finally:
            await run_sync(sock.close)
packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/portforward.py (1)

10-14: Consider adding robust error/log handling.
While the handler function straightforwardly forwards data between conn and stream, it might be beneficial to log errors or exceptions to improve debugging and avoid silent failures if either side closes unexpectedly.

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py (1)

14-19: Inline handler function is effective.
The nested handler neatly ties together the connection and the client stream. It might be beneficial to add minimal logging or exception handling for improved observability.

packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py (1)

43-45: Consider enhancing error handling during close.

While using suppress(Error) is good for preventing cascading errors, consider logging suppressed errors to aid in debugging.

 async def aclose(self):
+    from logging import getLogger
+    logger = getLogger(__name__)
     with suppress(Error):
-        await self.file.close()
+        try:
+            await self.file.close()
+        except Error as e:
+            logger.debug(f"Suppressed error while closing file: {e}")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dfea238 and a55e0a8.

📒 Files selected for processing (9)
  • docs/source/api-reference/adapters/network.md (1 hunks)
  • packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/fabric.py (1 hunks)
  • packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py (1 hunks)
  • packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/pexpect.py (1 hunks)
  • packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/portforward.py (1 hunks)
  • packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py (3 hunks)
  • packages/jumpstarter/jumpstarter/client/adapters.py (1 hunks)
  • packages/jumpstarter/jumpstarter/common/tempfile.py (1 hunks)
  • packages/jumpstarter/jumpstarter/common/utils.py (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build-and-push-image (jumpstarter-dev/jumpstarter-devspace .devfile/Containerfile.client)
🔇 Additional comments (17)
packages/jumpstarter/jumpstarter/client/adapters.py (1)

1-2: Imports look appropriate for decorator-based context management.
No issues are observed. The usage of contextlib and functools helps keep this implementation concise.

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/pexpect.py (2)

2-2: Well-structured imports for asynchronous operation.
The introduction of asynccontextmanager, run_sync, and blocking is aligned with an asynchronous design that ensures proper resource usage.

Also applies to: 4-4, 7-9


24-24: Appropriate use of blocking for synchronous compatibility.
Creating PexpectAdapter from AsyncPexpectAdapter in a blocking manner ensures broad usability.

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/fabric.py (3)

1-2: Imports align well with partial application and async management.
No concerns here. This addition is clean and suits the new context-based pattern.


8-11: Imports for adapter components are coherent.
These references from local modules and jumpstarter.client.adapters integrate smoothly with the updated context manager design.


14-38: Functional approach to FabricAdapter is clear and consistent.
The shift from a class-based to a function-based async context manager aligns with the overall PR objective. The code is readable and logically structured.

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/portforward.py (5)

1-2: Imports look good.
No issues identified with these newly introduced imports.


17-30: Async context manager usage is well-structured.
Switching to @asynccontextmanager along with TemporaryTcpListener effectively ensures that resources are cleaned up once the context exits. This is a neat improvement over manually handling enter/exit calls.


33-33: Blocking wrapper usage looks appropriate.
Wrapping AsyncTcpPortforwardAdapter with blocking is consistent with the new async-to-sync approach. Ensure that this pattern is thoroughly tested in synchronous workflows.


36-43: Parallel design for Unix port forwarding.
Similar strategy to AsyncTcpPortforwardAdapter, ensuring consistent usage of async context management for Unix sockets.


46-46: Unix blocking adapter is aligned with TCP approach.
This maintains uniformity across TCP and Unix domain sockets.

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py (2)

11-13: Conversion to async context manager is clear.
Decorating NovncAdapter with @blocking and @asynccontextmanager simplifies setup/teardown logic. Good improvement over the older class-based pattern.


21-22: Yielding noVNC URL is a user-friendly design.
Returning a direct noVNC URL with the ephemeral TCP port is convenient. Verify if HTTPS on “novnc.com” is suitable for production usage or if a custom host is needed.

packages/jumpstarter/jumpstarter/common/tempfile.py (1)

30-30: Revisiting host and port reuse defaults.
Switching local_host to "127.0.0.1" is a good security measure by restricting external access. Enabling reuse_port=True can be helpful for certain parallel socket usage, but verify that it won’t introduce unintended port-sharing scenarios in your environment.

packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py (1)

48-69: LGTM! Clean conversion to async context manager.

The refactoring from class-based to function-based implementation with proper async context management is well done. The logic for handling presigned URLs and file streaming is preserved while improving resource management.

packages/jumpstarter/jumpstarter/common/utils.py (1)

21-25: LGTM! Robust error handling for client closure.

The consistent implementation of try-finally blocks across all context managers ensures proper client cleanup, even in error scenarios. This aligns perfectly with the PR's objective of proper resource management.

Also applies to: 32-36, 55-59

docs/source/api-reference/adapters/network.md (1)

6-7: LGTM! Documentation accurately reflects implementation changes.

The documentation has been properly updated to reflect the transition from class-based to function-based implementations while maintaining clear examples.

Also applies to: 10-11, 14-15, 18-19, 22-23

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/jumpstarter/jumpstarter/client/client.py (1)

53-53: Nesting ExitStack in stack=stack.enter_context(ExitStack()).

While this is valid, the reuse of the variable name stack might be slightly confusing. Consider renaming the nested stack to improve clarity.

-    stack=stack.enter_context(ExitStack()),
+    nested_stack = stack.enter_context(ExitStack()),
packages/jumpstarter/jumpstarter/common/utils.py (1)

28-38: Consider consolidating duplicate cleanup logic.

The client cleanup logic is duplicated between serve_async and serve. Consider extracting it into a helper function.

+def _cleanup_client(client):
+    if hasattr(client, "close"):
+        client.close()
+
 @contextmanager
 def serve(root_device: Driver):
     with start_blocking_portal() as portal:
         with ExitStack() as stack:
             with portal.wrap_async_context_manager(serve_async(root_device, portal, stack)) as client:
                 try:
                     yield client
                 finally:
-                    if hasattr(client, "close"):
-                        client.close()
+                    _cleanup_client(client)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a55e0a8 and 0bdb394.

📒 Files selected for processing (8)
  • packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py (1 hunks)
  • packages/jumpstarter/jumpstarter/client/base.py (3 hunks)
  • packages/jumpstarter/jumpstarter/client/client.py (3 hunks)
  • packages/jumpstarter/jumpstarter/client/lease.py (3 hunks)
  • packages/jumpstarter/jumpstarter/common/utils.py (4 hunks)
  • packages/jumpstarter/jumpstarter/config/client.py (6 hunks)
  • packages/jumpstarter/jumpstarter/config/exporter_test.py (2 hunks)
  • packages/jumpstarter/jumpstarter/listener_test.py (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (10)
  • GitHub Check: Redirect rules - jumpstarter-docs
  • GitHub Check: Header rules - jumpstarter-docs
  • GitHub Check: Pages changed - jumpstarter-docs
  • GitHub Check: pytest-matrix (3.13)
  • GitHub Check: pytest-matrix (3.12)
  • GitHub Check: pytest-matrix (3.11)
  • GitHub Check: e2e
  • GitHub Check: build-and-push-image (jumpstarter-dev/jumpstarter-devspace .devfile/Containerfile.client)
  • GitHub Check: build-and-push-image (jumpstarter-dev/jumpstarter-dev .devfile/Containerfile)
  • GitHub Check: build-and-push-image (jumpstarter-dev/jumpstarter Dockerfile)
🔇 Additional comments (23)
packages/jumpstarter/jumpstarter/config/client.py (7)

2-2: Importing ExitStack for improved context management.

This import addition looks appropriate for handling complex resource cleanup with multiple context managers.


65-69: Nested ExitStack usage within lease function.

Introducing ExitStack here is a good step towards comprehensive resource management. This ensures all acquired resources are properly cleaned up if an exception occurs.


73-74: Consistent ExitStack usage in request_lease.

Reusing the same pattern as in lease helps maintain uniform resource handling across both methods.


84-89: New request_lease_async includes a stack parameter.

Passing ExitStack into the asynchronous method centralizes resource cleanup, improving reliability should exceptions occur. Great approach.


98-98: Providing stack to Lease(...).

Successfully integrating ExitStack into Lease ensures advanced resource lifecycle management for the leased resources.


117-119: Refined lease_async signature with ExitStack.

Accepting stack mirrors the synchronous approach, giving you complete control over the context's lifetime in an async environment.


132-132: Injecting stack=stack into the Lease context.

Maintaining consistency by reusing the same stack reference ensures the correct nesting of contexts for resource cleanup.

packages/jumpstarter/jumpstarter/client/client.py (4)

2-2: Added ExitStack import.

This addition complements the asynchronous context usage of asynccontextmanager for better resource handling.


16-16: Revised client_from_path signature to accept stack.

Including the ExitStack parameter is an excellent way to unify resource management between synchronous and asynchronous contexts.


20-20: Yielding from client_from_channel with stack.

Passing stack seamlessly from client_from_path ensures a cohesive context lifecycle for channels.


26-26: Extended client_from_channel to accept stack.

Exposing stack here allows downstream usage of nested contexts while creating clients.

packages/jumpstarter/jumpstarter/client/base.py (4)

7-7: Incorporated ExitStack import.

Good step toward consolidating resource finalization under a single exit strategy.


34-34: Added stack: ExitStack field to DriverClient.

Storing ExitStack inside DriverClient centralizes cleanup logic, minimizing the risk of resource leaks.


90-90: Using self.stack.enter_context in open_stream.

Leveraging the stored ExitStack for streaming clarifies ownership of the stream resource and ensures consistent teardown.


96-96: Closing the ExitStack in close.

This definitively finalizes any pending contexts opened via open_stream. Double-check that no active tasks will inadvertently get closed.

packages/jumpstarter/jumpstarter/common/utils.py (2)

15-26: LGTM! Robust resource cleanup implementation.

The addition of ExitStack and the try-finally block ensures proper cleanup of client resources, even in case of exceptions. This aligns well with the PR's objective of ensuring proper client closure on system exit.


40-61: LGTM! Consistent error handling pattern.

The env_async function follows the same robust error handling pattern as serve_async, ensuring consistent resource cleanup across the codebase.

packages/jumpstarter/jumpstarter/listener_test.py (2)

43-57: LGTM! Proper resource management in tests.

The test correctly uses ExitStack to manage resources, ensuring proper cleanup even in test scenarios.


94-115: LGTM! Comprehensive concurrent connection testing.

The test effectively verifies concurrent connections while maintaining proper resource management through ExitStack.

packages/jumpstarter/jumpstarter/config/exporter_test.py (1)

54-65: LGTM! Proper resource management in exporter test.

The test correctly integrates ExitStack for resource management in the lease_async call.

packages/jumpstarter/jumpstarter/client/lease.py (2)

28-41: LGTM! Proper integration of ExitStack in Lease class.

The addition of the required stack field ensures proper resource management throughout the lease lifecycle.


156-160: LGTM! Consistent resource management in connect_async.

The connect_async method correctly uses the stack parameter for client creation, maintaining consistent resource management.

packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py (1)

25-25:

Details

❓ Verification inconclusive

LGTM! Improved context management implementation.

The change to use self.stack.enter_context() is a more robust approach that ensures proper cleanup of resources, especially during system exit. This aligns well with the PR objectives.

Let's verify the implementation of self.stack:


🏁 Script executed:

#!/bin/bash
# Description: Check the implementation of DriverClient's stack attribute
ast-grep --pattern $'class DriverClient {
  $$$
  def __init__($$$) {
    $$$
    self.stack = $$$
    $$$
  }
  $$$
}'

Length of output: 120


Below are additional shell script commands to further verify that the self.stack attribute is correctly initialized and that the pexpect() method is implemented properly as a context manager. Once you run these scripts and confirm that both are in place as expected, you can finalize the review.


ACTION: Verify and Finalize Context Management Implementation

The change to use self.stack.enter_context(self.pexpect()) is a more robust approach that ensures proper resource cleanup—especially during system exit—aligning with the PR objectives. However, please confirm that:

• The self.stack attribute is properly initialized (typically in the constructor of the client class).
• The pexpect() method is implemented as a valid context manager.

Once these details are verified, the refactoring can be considered complete.


🏁 Script executed:

#!/bin/bash
# Verify the assignment of self.stack in the client file
rg "self\.stack\s*=" -N packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Verify the implementation (definition) of the pexpect method in the client file
rg "def pexpect\(" -N packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py

Length of output: 121


Attention: Context Management Verification Reminder

The refactoring of the context management—using self.stack.enter_context(self.pexpect())—is a strong improvement ensuring automatic resource cleanup. We confirmed that the pexpect() method is defined as a context manager. However, our search did not reveal an assignment for the self.stack attribute in the file. Please verify that:

  • Initialization of self.stack: Ensure that self.stack is initialized (typically with an instance of ExitStack) in the constructor or an inherited initializer.
  • Context Consistency: Confirm that the lifecycle of self.stack is correctly managed so that resource cleanup occurs appropriately.

Once the initialization of self.stack is verified or corrected, the implementation looks solid in meeting the PR’s objectives.

@NickCao NickCao force-pushed the context-manager-revamp branch from 0bdb394 to 9ba37c5 Compare February 20, 2025 18:13
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/jumpstarter/jumpstarter/client/client.py (1)

16-16: LGTM! Robust resource management implementation.

The changes effectively ensure proper cleanup of client resources by:

  1. Passing the ExitStack through the client creation chain
  2. Creating nested ExitStacks for each client in the hierarchy

This implementation guarantees that resources are released in the correct order when the system exits.

Consider adding a comment explaining the purpose of stack.enter_context(ExitStack()) on line 53, as it creates a new nested ExitStack for each client:

 client = client_class(
     uuid=UUID(uuid),
     labels=report.labels,
     channel=channel,
     portal=portal,
+    # Create a new ExitStack for this client to manage its resources independently
     stack=stack.enter_context(ExitStack()),
     children={reports[k].labels["jumpstarter.dev/name"]: clients[k] for k in topo[uuid]},
 )

Also applies to: 20-20, 26-26, 53-53

packages/jumpstarter/jumpstarter/client/base.py (1)

34-34: Add documentation for the stack field.

The stack field should be documented to explain its purpose in managing client resources.

     portal: BlockingPortal
-    stack: ExitStack
+    stack: ExitStack
+    """ExitStack for managing client resources and ensuring proper cleanup on exit"""
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0bdb394 and 9ba37c5.

📒 Files selected for processing (8)
  • packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py (1 hunks)
  • packages/jumpstarter/jumpstarter/client/base.py (3 hunks)
  • packages/jumpstarter/jumpstarter/client/client.py (3 hunks)
  • packages/jumpstarter/jumpstarter/client/lease.py (2 hunks)
  • packages/jumpstarter/jumpstarter/common/utils.py (4 hunks)
  • packages/jumpstarter/jumpstarter/config/client.py (2 hunks)
  • packages/jumpstarter/jumpstarter/config/exporter_test.py (2 hunks)
  • packages/jumpstarter/jumpstarter/listener_test.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/jumpstarter/jumpstarter/config/exporter_test.py
  • packages/jumpstarter/jumpstarter/client/lease.py
  • packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py
  • packages/jumpstarter/jumpstarter/common/utils.py
  • packages/jumpstarter/jumpstarter/listener_test.py
  • packages/jumpstarter/jumpstarter/config/client.py
⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: pytest-matrix (3.13)
  • GitHub Check: pytest-matrix (3.12)
  • GitHub Check: e2e
  • GitHub Check: pytest-matrix (3.11)
  • GitHub Check: build-and-push-image (jumpstarter-dev/jumpstarter-devspace .devfile/Containerfile.client)
  • GitHub Check: build-and-push-image (jumpstarter-dev/jumpstarter-dev .devfile/Containerfile)
  • GitHub Check: build-and-push-image (jumpstarter-dev/jumpstarter Dockerfile)
🔇 Additional comments (4)
packages/jumpstarter/jumpstarter/client/client.py (1)

2-2: LGTM! Good choice using ExitStack.

The addition of ExitStack is a good choice for managing multiple context managers dynamically, which aligns well with the PR's objective of ensuring proper client closure.

packages/jumpstarter/jumpstarter/client/base.py (3)

7-7: LGTM! Consistent import organization.

The ExitStack import is correctly placed alongside other context management imports.


90-90: LGTM! Proper resource management.

Using stack.enter_context ensures that stream resources are properly managed and will be cleaned up when the client is closed.


96-96: LGTM! Comprehensive cleanup implementation.

Using stack.close() ensures all resources managed by the stack are properly cleaned up, and the integration with __del__ provides a safety net for cleanup.

@NickCao NickCao force-pushed the context-manager-revamp branch from 9ba37c5 to 82cc95e Compare February 20, 2025 18:30
Copy link
Member

@mangelajo mangelajo left a comment

Choose a reason for hiding this comment

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

Looks good, a nice improvement.

I wonder if there would be a way of getting to the "next mile", doing it at the base client :D

fr>>> from jumpstarter.common.utils import serve
>>> client = serve(PySerial(url="loop://"))
>>> cl = client.open()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: '_GeneratorContextManager' object has no attribute 'open'
>>> cl = client.__enter__()
>>> p = cl.open()
>>> dir(p)

@mangelajo mangelajo enabled auto-merge February 24, 2025 15:33
@mangelajo
Copy link
Member

oops it fails now, I guess it doesn,t work on top of the dbus without adapting the dbus now.

@NickCao
Copy link
Collaborator Author

NickCao commented Feb 24, 2025

oops it fails now, I guess it doesn,t work on top of the dbus without adapting the dbus now.

Indeed, fixing in a second.

@NickCao NickCao force-pushed the context-manager-revamp branch from 3c5b186 to 83d06fb Compare February 24, 2025 16:37
@mangelajo mangelajo merged commit ced480b into main Feb 24, 2025
12 of 17 checks passed
@NickCao NickCao deleted the context-manager-revamp branch February 24, 2025 16:40
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (9)
packages/jumpstarter/jumpstarter/client/adapters.py (1)

5-12: Add docstring to explain the decorator's purpose.

The implementation looks good and follows best practices by using @wraps and @contextmanager. However, adding a docstring would help users understand its purpose.

 def blocking(f):
+    """
+    Decorator that converts an async context manager into a blocking one.
+    
+    Args:
+        f: An async context manager function
+    Returns:
+        A synchronous context manager function
+    """
     @wraps(f)
     @contextmanager
     def wrapper(*args, **kwargs):
packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/pexpect.py (1)

10-20: Consider enhancing error handling for socket operations.

The implementation correctly manages socket resources with proper cleanup. However, socket operations could benefit from more robust error handling.

 @contextmanager
 def PexpectAdapter(*, client: DriverClient, method: str = "connect"):
     with TcpPortforwardAdapter(client=client, method=method) as addr:
-        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        sock.connect(addr)
+        sock = None
+        try:
+            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            sock.connect(addr)
+        except socket.error as e:
+            if sock:
+                sock.close()
+            raise ConnectionError(f"Failed to connect to {addr}: {e}") from e

         try:
             yield fdspawn(sock)
         finally:
-            sock.close()
+            if sock:
+                sock.close()
packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py (1)

11-32: Add docstring and enhance error handling for stream operations.

The implementation correctly manages async resources and stream forwarding. However, it would benefit from documentation and more robust error handling.

 @blocking
 @asynccontextmanager
 async def NovncAdapter(*, client: DriverClient, method: str = "connect"):
+    """
+    Creates a NoVNC connection adapter.
+    
+    Args:
+        client: The driver client
+        method: The connection method to use (default: "connect")
+    
+    Returns:
+        A NoVNC URL for connecting to the remote display
+    """
     async def handler(conn):
         async with conn:
-            async with client.stream_async(method) as stream:
-                async with WebsocketServerStream(stream=stream) as stream:
-                    async with forward_stream(conn, stream):
-                        pass
+            try:
+                async with client.stream_async(method) as stream:
+                    async with WebsocketServerStream(stream=stream) as stream:
+                        async with forward_stream(conn, stream):
+                            pass
+            except Exception as e:
+                # Log error before re-raising to aid debugging
+                import logging
+                logging.error(f"Stream forwarding failed: {e}")
+                raise
packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py (1)

48-69: Extract magic numbers and enhance error handling for presigned URLs.

The implementation is solid but would benefit from extracting constants and adding error handling for presigned URL generation.

+# Constants for configuration
+CHUNK_SIZE = 65536  # 64KB chunks for file reading
+PRESIGN_EXPIRY_SECONDS = 60
+
 @blocking
 @asynccontextmanager
 async def OpendalAdapter(
     *,
     client: DriverClient,
     operator: Operator,  # opendal.Operator for the storage backend
     path: str,  # file path in storage backend relative to the storage root
     mode: Literal["rb", "wb"] = "rb",  # binary read or binary write mode
 ):
     # if the access mode is binary read, and the storage backend supports presigned read requests
     if mode == "rb" and operator.capability().presign_read:
-        # create presigned url for the specified file with a 60 second expiration
-        presigned = await operator.to_async_operator().presign_read(path, expire_second=60)
-        yield PresignedRequestResource(
-            headers=presigned.headers, url=presigned.url, method=presigned.method
-        ).model_dump(mode="json")
+        try:
+            # create presigned url for the specified file
+            presigned = await operator.to_async_operator().presign_read(
+                path, expire_second=PRESIGN_EXPIRY_SECONDS
+            )
+            yield PresignedRequestResource(
+                headers=presigned.headers, url=presigned.url, method=presigned.method
+            ).model_dump(mode="json")
+        except Error as e:
+            raise ValueError(f"Failed to generate presigned URL for {path}: {e}") from e
packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/fabric.py (2)

14-16: Stacked decorators may require careful testing
Using both @blocking and @asynccontextmanager is a valid approach to expose an async context manager via a synchronous interface. However, verify that concurrency scenarios and error propagation behave as expected with this layering.


27-37: Consider enhanced error handling
The code correctly yields a Fabric Connection, but if TemporaryTcpListener or the connection setup fails, there is no fallback or retry. Adding try-except blocks or clearer error reporting might improve reliability.

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/dbus.py (1)

8-31: Global environment variable mutation may affect concurrency
Changing environment variables in a context manager is fine for single-threaded usage but can lead to race conditions if multiple tasks run DbusAdapter concurrently. Consider a more isolated approach (e.g., spawning separate processes or providing per-invocation configuration) to avoid collisions.

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/portforward.py (2)

10-15: Handler function clarity
The handler function clearly coordinates the connection and stream forwarding. For extra robustness, consider logging exceptions or transient errors within this block if remote endpoints disconnect unexpectedly.


17-32: TCP port-forward adapter logic
Using TemporaryTcpListener with an async context manager is clean. Watch out for ephemeral port collisions or lengthy connection times if many adapters spin up in parallel. Adding optional parameters (e.g., backlog size) or improved logging could aid debugging.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 82cc95e and 83d06fb.

📒 Files selected for processing (17)
  • docs/source/api-reference/adapters/network.md (1 hunks)
  • packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/dbus.py (1 hunks)
  • packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/fabric.py (1 hunks)
  • packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py (1 hunks)
  • packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/pexpect.py (1 hunks)
  • packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/portforward.py (1 hunks)
  • packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py (3 hunks)
  • packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py (1 hunks)
  • packages/jumpstarter/jumpstarter/client/adapters.py (1 hunks)
  • packages/jumpstarter/jumpstarter/client/base.py (3 hunks)
  • packages/jumpstarter/jumpstarter/client/client.py (3 hunks)
  • packages/jumpstarter/jumpstarter/client/lease.py (2 hunks)
  • packages/jumpstarter/jumpstarter/common/tempfile.py (1 hunks)
  • packages/jumpstarter/jumpstarter/common/utils.py (4 hunks)
  • packages/jumpstarter/jumpstarter/config/client.py (2 hunks)
  • packages/jumpstarter/jumpstarter/config/exporter_test.py (2 hunks)
  • packages/jumpstarter/jumpstarter/listener_test.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/jumpstarter/jumpstarter/client/base.py
  • packages/jumpstarter/jumpstarter/config/client.py
  • packages/jumpstarter/jumpstarter/config/exporter_test.py
  • packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py
  • packages/jumpstarter/jumpstarter/listener_test.py
  • packages/jumpstarter/jumpstarter/client/lease.py
  • packages/jumpstarter/jumpstarter/common/tempfile.py
  • packages/jumpstarter/jumpstarter/common/utils.py
  • packages/jumpstarter/jumpstarter/client/client.py
  • docs/source/api-reference/adapters/network.md
⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: pytest-matrix (3.13)
  • GitHub Check: pytest-matrix (3.12)
  • GitHub Check: pytest-matrix (3.11)
  • GitHub Check: e2e
  • GitHub Check: build-and-push-image (jumpstarter-dev/jumpstarter-devspace .devfile/Containerfile.client)
  • GitHub Check: build-and-push-image (jumpstarter-dev/jumpstarter-dev .devfile/Containerfile)
  • GitHub Check: build-and-push-image (jumpstarter-dev/jumpstarter Dockerfile)
🔇 Additional comments (7)
packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py (1)

15-46: LGTM! Well-structured stream wrapper with proper error handling.

The AsyncFileStream implementation correctly wraps opendal.AsyncFile with proper error handling and stream compatibility.

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/fabric.py (3)

1-2: Imports look good
No issues found in these import statements; usage of asynccontextmanager and partial appears appropriate for this context.


8-10: Imports for handler, DriverClient, and blocking
These newly introduced imports align well with the intended functionality of forwarding connections and providing a blocking interface for an async context.


17-17: Existing comment about enforcing keyword-only arguments
Mangelajo’s past remark remains relevant: using * ensures all parameters are passed as keywords, which can improve readability but may limit future flexibility.

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/dbus.py (1)

1-5: Initial imports
The switch from a class-based to a function-based adapter makes sense, and these imports appear consistent with that transition.

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/portforward.py (2)

1-5: Revised imports
The changes here align with the new async function-based adapters. No issues detected.


34-42: Unix domain socket forwarding
This implementation follows the same pattern as TCP forwarding and looks consistent. The approach appears sound for local IPC. Ensure that permissions and file cleanup in the temp directory are carefully handled downstream.

@NickCao
Copy link
Collaborator Author

NickCao commented Feb 24, 2025

I wonder if there would be a way of getting to the "next mile", doing it at the base client :D

This would bring us back to the "hacky" realm, there has to be at least a toplevel context manager managing the ExitStack.

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.

3 participants