Skip to content

Fix logging and requests sync calls#64

Closed
carbonin wants to merge 1 commit intoopenshift-assisted:masterfrom
carbonin:more-async
Closed

Fix logging and requests sync calls#64
carbonin wants to merge 1 commit intoopenshift-assisted:masterfrom
carbonin:more-async

Conversation

@carbonin
Copy link
Copy Markdown
Collaborator

@carbonin carbonin commented Aug 13, 2025

The blockbuster tool revealed that both logging and the use of the requests library for getting the token and pull secret were blocking the async nature of the mcp server.

This commit handles the logging by splitting the log into a producer and consumer through a queue and using async.to_thread for the requests calls.

cc @eranco74 @omertuc

Maybe this was causing some of the slowness in your scale test
If you were only testing with "list_clusters" though, you wouldn't have been hitting the pull secret issue.
I'm not sure how you were passing the token, but I guess you were passing the access token in the request so the token fetching requests call was probably also not an issue.

This only leaves the logging which I'm not sure would be an issue with it disabled with LOG_TO_FILE=false in the container, but 🤷 now blockbuster doesn't complain so I suppose that's good.

Summary by CodeRabbit

  • New Features

    • Non-blocking, queue-backed logging with graceful shutdown; consistent console output and optional file logging preserved.
    • Unexpected errors now include full tracebacks in logs for easier troubleshooting.
  • Refactor

    • Authentication token and pull-secret retrieval are now asynchronous to improve responsiveness.
    • Pull-secret is lazily cached to reduce repeated network calls.
  • Tests

    • Test suite migrated to async to validate new asynchronous flows, custom endpoints, and error scenarios.

The blockbuster tool revealed that both logging and the use of the
requests  library for getting the token and pull secret were blocking
the async nature of the mcp server.

This commit handles the logging by splitting the log into a producer and
consumer through a queue and using async.to_thread for the requests
calls.
@openshift-ci openshift-ci Bot requested review from eranco74 and omertuc August 13, 2025 21:04
@openshift-ci
Copy link
Copy Markdown

openshift-ci Bot commented Aug 13, 2025

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: carbonin

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 13, 2025

Walkthrough

Async refactor converts token and pull-secret retrieval to awaitable functions using asyncio.to_thread for blocking HTTP calls. Logger switches to a queue-backed architecture with QueueHandler/QueueListener. Exception logging now uses log.exception. Tests are migrated to async style aligning with new interfaces. Public APIs updated accordingly.

Changes

Cohort / File(s) Summary
Async access token retrieval
server.py, tests/test_server.py
get_access_token converted to async; blocking SSO HTTP call offloaded via asyncio.to_thread; tests updated to await token retrieval across scenarios.
Async pull-secret retrieval and client updates
service_client/assisted_service_api.py, tests/test_assisted_service_api.py
Replaced property-based lazy pull-secret with async get_pull_secret using asyncio.to_thread; cached on first fetch; call sites updated to await; tests refactored to async and mock requests.post.
Exception logging update
service_client/exceptions.py
Generic exception logging changed from log.error to log.exception to include tracebacks; no control-flow changes.
Queue-backed logging pipeline
service_client/logger.py
Introduced QueueHandler/QueueListener with background processing; new private handler factories _create_file_handler/_create_stream_handler; propagation disabled; graceful shutdown via atexit.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Server
  participant SSO as SSO Endpoint

  Client->>Server: await get_access_token()
  activate Server
  Server->>Server: to_thread(blocking HTTP)
  Server->>SSO: POST /token
  SSO-->>Server: 200 OK (access_token)
  deactivate Server
  Server-->>Client: access_token
Loading
sequenceDiagram
  participant Caller
  participant IC as InventoryClient
  participant PS as Pull-Secret Service

  Caller->>IC: await get_pull_secret()
  alt cache miss
    IC->>IC: to_thread(blocking HTTP)
    IC->>PS: POST PULL_SECRET_URL (Bearer token)
    PS-->>IC: 200 OK (pull_secret)
    IC->>IC: cache pull_secret
  else cache hit
    IC->>IC: return cached pull_secret
  end
  IC-->>Caller: pull_secret
Loading
sequenceDiagram
  participant AppLogger
  participant QH as QueueHandler
  participant Q as Queue
  participant QL as QueueListener
  participant SH as StreamHandler
  participant FH as FileHandler

  AppLogger->>QH: log(record)
  QH->>Q: enqueue(record)
  Q->>QL: record
  QL->>SH: handle(record)
  QL->>FH: handle(record)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

size/XL, lgtm

Poem

I thump my paws: async cheer!
Tokens hop in threads so clear.
Secrets cached, a silky stash,
Logs queue up—no blocking crash.
With tidy traces, errors gleam—
I nibble bytes and chase the stream.
Hippity-hop, ship the dream! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the "Integrations" page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ 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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

@openshift-ci openshift-ci Bot added approved Indicates a PR has been approved by an approver from all required OWNERS files. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Aug 13, 2025
Copy link
Copy Markdown

@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: 1

♻️ Duplicate comments (2)
tests/test_assisted_service_api.py (2)

414-420: Repeated pull secret mocking can be simplified.

This is another instance of redundant pull secret mocking that could be simplified by using the existing client fixture.


446-452: Repeated pull secret mocking in infra env creation test.

This is another instance where the pull secret mocking could be removed in favor of using the pre-configured client fixture.

🧹 Nitpick comments (3)
service_client/assisted_service_api.py (1)

61-62: Consider storing the pull secret more securely.

While the current implementation caches the pull secret in memory as a plain string, consider implementing additional security measures for sensitive data:

  • Use a secure string wrapper that clears memory on deletion
  • Implement automatic expiration/refresh of cached secrets
  • Consider using a dedicated secrets management solution for production environments
tests/test_assisted_service_api.py (2)

35-39: Extract pull secret mocking to a fixture for better test maintainability.

The pull secret mocking pattern is repeated across multiple tests. Consider extracting it to a shared fixture to reduce code duplication and improve test maintainability.

Add a new fixture at the class level:

@pytest.fixture
def mock_pull_secret(self) -> Mock:
    """Mock pull secret response."""
    with patch("requests.post") as mock_post:
        mock_response = Mock()
        mock_response.text = "mock-pull-secret"
        mock_post.return_value = mock_response
        yield mock_post

Then simplify the client fixture:

 @pytest.fixture
-def client(self, mock_access_token: str) -> InventoryClient:
+def client(self, mock_access_token: str, mock_pull_secret: Mock) -> InventoryClient:
     """Create a test client instance."""
-    with patch("requests.post") as mock_post:
-        mock_response = Mock()
-        mock_response.text = "mock-pull-secret"
-        mock_post.return_value = mock_response
-        return InventoryClient(mock_access_token)
+    return InventoryClient(mock_access_token)

376-382: Remove redundant pull secret mocking in tests that don't use it.

Several test methods mock the pull secret HTTP response but never actually verify or use it. This adds unnecessary complexity to the tests.

For tests like test_create_cluster_success, test_create_cluster_single_node, and test_create_infra_env_success, consider using the existing client fixture which already handles pull secret mocking, instead of re-mocking it in each test.

-        with (
-            patch.object(client, "_installer_api") as mock_installer_api,
-            patch("requests.post") as mock_post,
-        ):
-            # Mock pull secret HTTP response
-            mock_response = Mock()
-            mock_response.text = "mock-pull-secret"
-            mock_post.return_value = mock_response
-
+        with patch.object(client, "_installer_api") as mock_installer_api:
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a78492e and c6cff59.

📒 Files selected for processing (6)
  • server.py (16 hunks)
  • service_client/assisted_service_api.py (4 hunks)
  • service_client/exceptions.py (1 hunks)
  • service_client/logger.py (3 hunks)
  • tests/test_assisted_service_api.py (10 hunks)
  • tests/test_server.py (9 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
server.py (2)
tests/test_assisted_service_api.py (1)
  • client (33-39)
service_client/assisted_service_api.py (1)
  • InventoryClient (22-518)
tests/test_server.py (1)
server.py (1)
  • get_access_token (91-130)
tests/test_assisted_service_api.py (1)
service_client/assisted_service_api.py (1)
  • get_pull_secret (43-65)
🔇 Additional comments (15)
service_client/exceptions.py (1)

49-49: LGTM! Good choice using log.exception() for better debugging.

The change from log.error() to log.exception() is a good improvement that will include the full stack trace in the logs, making debugging easier without changing the exception propagation behavior.

server.py (3)

91-91: LGTM! Function correctly converted to async.

The function signature change aligns with the PR's goal to prevent blocking the async event loop.


127-127: Good use of asyncio.to_thread for the blocking HTTP call.

Wrapping the requests.post call with asyncio.to_thread correctly prevents blocking the event loop while maintaining the existing error handling behavior.


154-154: LGTM! All call sites correctly updated to await the async token function.

All instances of get_access_token() have been properly updated to use await, maintaining consistency throughout the codebase.

Also applies to: 179-179, 212-212, 236-236, 262-262, 356-356, 419-419, 451-451, 472-472, 492-492, 518-518, 560-560, 594-594, 620-620

tests/test_server.py (2)

103-111: Test methods correctly converted to async.

The test methods have been properly converted to async and correctly await the get_access_token() function.


115-129: LGTM! All test methods properly handle async behavior.

All test methods have been correctly converted to async and properly await the async get_access_token() function while maintaining the same test coverage and scenarios.

Also applies to: 131-145, 148-164, 177-195, 208-219, 221-236

service_client/logger.py (4)

93-97: Well-structured factory function for file handlers.

The new private factory function cleanly encapsulates handler creation with the sensitive formatter.


100-104: Good separation of concerns with stream handler factory.

The stream handler factory follows the same pattern as the file handler, maintaining consistency.


120-143: Excellent implementation of non-blocking logging architecture!

The queue-based logging implementation effectively prevents blocking I/O operations from affecting the async event loop. Key benefits:

  • QueueHandler ensures logging calls return immediately
  • QueueListener processes logs on a background thread
  • Proper handling of multiple loggers with propagation disabled to avoid duplicates

145-152: Good practice with graceful shutdown handler.

The atexit handler ensures the queue listener is properly stopped when the application exits, with appropriate exception handling for best-effort cleanup.

service_client/assisted_service_api.py (3)

43-46: Well-implemented lazy loading pattern for pull secret.

The caching mechanism avoids redundant API calls while maintaining the async nature of the operation.


56-58: Good use of asyncio.to_thread for the blocking HTTP call.

Properly prevents the requests.post call from blocking the event loop while maintaining error handling.


245-245: LGTM! Consistent async/await usage for pull secret retrieval.

Both create_cluster and create_infra_env methods correctly await the async get_pull_secret() method.

Also applies to: 275-275

tests/test_assisted_service_api.py (2)

393-398: Verify the pull secret is correctly passed in cluster creation.

Good verification that the mock pull secret value is correctly passed to the API call.


465-465: Good verification of pull secret in infra env params.

The test correctly verifies that the mocked pull secret value is passed through to the infra environment creation parameters.

Comment on lines +48 to +55
with patch("requests.post") as mock_post:
mock_response = Mock()
mock_response.text = "test-pull-secret"
mock_post.return_value = mock_response

client = InventoryClient(mock_access_token)
assert client.access_token == mock_access_token
assert client.pull_secret == "test-pull-secret"
assert await client.get_pull_secret() == "test-pull-secret"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Test should verify HTTP call parameters.

The test creates a mock but doesn't verify that the HTTP request was made with the correct parameters (URL, headers, timeout). This verification is important to ensure the implementation correctly fetches the pull secret.

         with patch("requests.post") as mock_post:
             mock_response = Mock()
             mock_response.text = "test-pull-secret"
             mock_post.return_value = mock_response
 
             client = InventoryClient(mock_access_token)
             assert client.access_token == mock_access_token
             assert await client.get_pull_secret() == "test-pull-secret"
+            
+            # Verify the HTTP call was made correctly
+            mock_post.assert_called_once_with(
+                "https://api.openshift.com/api/accounts_mgmt/v1/access_token",
+                headers={"Authorization": f"Bearer {mock_access_token}"},
+                timeout=30
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
with patch("requests.post") as mock_post:
mock_response = Mock()
mock_response.text = "test-pull-secret"
mock_post.return_value = mock_response
client = InventoryClient(mock_access_token)
assert client.access_token == mock_access_token
assert client.pull_secret == "test-pull-secret"
assert await client.get_pull_secret() == "test-pull-secret"
with patch("requests.post") as mock_post:
mock_response = Mock()
mock_response.text = "test-pull-secret"
mock_post.return_value = mock_response
client = InventoryClient(mock_access_token)
assert client.access_token == mock_access_token
assert await client.get_pull_secret() == "test-pull-secret"
# Verify the HTTP call was made correctly
mock_post.assert_called_once_with(
"https://api.openshift.com/api/accounts_mgmt/v1/access_token",
headers={"Authorization": f"Bearer {mock_access_token}"},
timeout=30
)

@omertuc
Copy link
Copy Markdown
Collaborator

omertuc commented Aug 14, 2025

Ideally we should do away with requests completely and use aiohttp

@carbonin
Copy link
Copy Markdown
Collaborator Author

/hold

Going to switch the http client and test this a bit more thoroughly

@openshift-ci openshift-ci Bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Aug 14, 2025
@eranco74
Copy link
Copy Markdown
Collaborator

Let's cloes this?

@carbonin
Copy link
Copy Markdown
Collaborator Author

Yup

@carbonin carbonin closed this Aug 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants