Skip to content

Conversation

@lalinsky
Copy link
Owner

@lalinsky lalinsky commented Aug 23, 2025

Summary

  • Refactor ServerPool from separate ArrayList + StringHashMap to unified StringArrayHashMapUnmanaged
  • Add key field to Server struct for efficient lookups
  • Simplify and optimize server selection algorithm

Changes

  • Server struct: Added key field to store host:port string, updated init/deinit accordingly
  • ServerPool struct: Replaced ArrayList(*Server) + StringHashMap(void) with StringArrayHashMapUnmanaged(*Server)
  • getNextServer(): Simplified using stored keys with fetchSwapRemove/putAssumeCapacity
  • Removed getCurrentServer(): No longer needed with direct key-based lookups
  • Added getFirstServer(): Helper method for cleaner code
  • Fixed shuffle(): Updated to work with ArrayHashMap using entries.swap() + reIndex()

Benefits

  • Reduced memory usage: Single data structure instead of two
  • Better performance: O(1) key-based operations, eliminated server pointer iteration
  • Cleaner API: More intuitive methods, less complex internal state management
  • Maintained compatibility: All existing tests pass unchanged

Testing

  • All existing unit tests pass
  • Added verification for new ArrayHashMap behavior
  • No breaking changes to public API

Summary by CodeRabbit

  • Refactor

    • Streamlined server management with host:port deduplication to prevent duplicate entries.
    • Improved server rotation and shuffling for more consistent load distribution.
    • Servers exceeding reconnect limits are automatically removed, enhancing failover behavior.
    • More accurate server count reporting in pool operations.
  • Bug Fixes

    • Resolved issues where duplicate server URLs could lead to inconsistent selection and reconnection behavior.
  • Style

    • Minor whitespace cleanup with no functional impact.

- Replace separate ArrayList + StringHashMap with unified StringArrayHashMapUnmanaged
- Add key field to Server struct to store host:port for efficient lookups
- Update getNextServer to use stored keys with fetchSwapRemove/putAssumeCapacity
- Remove getCurrentServer method as it's no longer needed
- Add getFirstServer helper method for cleaner code
- Fix shuffle implementation to work with ArrayHashMap using entries.swap + reIndex
- All tests passing with improved performance and cleaner API
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 23, 2025

Walkthrough

Refactors server pooling to use a host:port keyed StringArrayHashMap for server storage, adds per-server computed key, updates add/shuffle/rotation logic and memory management, and replaces URL hash map. Also removes a single blank line in the connection reconnection path. Public API adjusted with new/removed functions and fields.

Changes

Cohort / File(s) Summary of changes
Whitespace cleanup
src/connection.zig
Removed a stray blank line around flusher thread restart in reconnection path; no functional changes.
Server pool map refactor
src/server_pool.zig
Introduced Server.key (host:port), migrated ServerPool.servers to StringArrayHashMapUnmanaged(*Server), removed URLs map, added addServer and getFirstServer, removed getCurrentServer, rewrote getNextServer rotation/removal and shuffle via map indexing, updated init/deinit and memory management.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Caller
  participant SP as ServerPool
  participant S as Server
  Note over SP: addServer(url_str, is_implicit)
  C->>SP: addServer(url_str, is_implicit)
  SP->>S: init(url_str)
  S-->>SP: key = "host:port"
  SP->>SP: getOrPut(key)
  alt key already exists
    SP->>S: deinit() / destroy duplicate
    SP-->>C: return
  else key new
    alt first explicit URL
      SP->>S: set default user/pwd
    end
    SP-->>C: return
  end
Loading
sequenceDiagram
  autonumber
  participant C as Consumer
  participant SP as ServerPool
  participant S as Current Server
  Note over SP: getNextServer(current, max_reconnect)
  C->>SP: getNextServer(current, max_reconnect)
  alt current is null
    SP-->>C: getFirstServer()
  else current exists
    alt max_reconnect < 0 or current.reconnects < max_reconnect
      SP->>SP: remove current by key
      SP->>SP: re-insert at end
      SP-->>C: getFirstServer()
    else exceeded max
      SP->>SP: remove current by key
      SP->>S: deinit() / free key / destroy
      SP-->>C: getFirstServer()
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I thump my paws on maps anew,
Keys of host:port guide me through.
I shuffle burrows, one by one,
Rotate warrens, then I’m done.
A tidy hop—one blank line gone—
The pool now hums; onward I yawn. 🐇✨

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 unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-server-pool-arrayhashmap

🪧 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.

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

🧹 Nitpick comments (3)
src/server_pool.zig (3)

118-124: Defensive guard: avoid .? panic if key not found

fetchSwapRemove(current_srv.key).? assumes the key is always present. If current_server gets out of sync (e.g., external misuse, or future changes), this will panic in Debug or worse in Release. Cheap guard keeps behavior predictable by falling back to current-first selection.

-            // Remove using the stored key and get the mutable server back
-            const removed_server = self.servers.fetchSwapRemove(current_srv.key).?;
+            // Remove using the stored key and get the mutable server back
+            const maybe_removed = self.servers.fetchSwapRemove(current_srv.key);
+            if (maybe_removed == null) {
+                // If not found, gracefully pick the first available server
+                return self.getFirstServer();
+            }
+            const removed_server = maybe_removed.?;
@@
-            const removed_server = self.servers.fetchSwapRemove(current_srv.key).?;
+            const maybe_removed = self.servers.fetchSwapRemove(current_srv.key);
+            if (maybe_removed == null) {
+                return self.getFirstServer();
+            }
+            const removed_server = maybe_removed.?;

Would you like me to push this safety change as a small follow-up?

Also applies to: 125-129


148-156: Unify RNG API and seed casting with the rest of the codebase

Elsewhere (connection.zig) you use std.Random.DefaultPrng. Here you use std.rand.DefaultPrng and seed via @bitCast(std.time.nanoTimestamp()), which is brittle across Zig versions and integer widths. Prefer std.Random.DefaultPrng and a clear cast from milliTimestamp() for consistency and portability.

-        var prng = std.rand.DefaultPrng.init(@bitCast(std.time.nanoTimestamp()));
-        const random = prng.random();
+        var prng = std.Random.DefaultPrng.init(@intCast(u64, std.time.milliTimestamp()));
+        const random = prng.random();

No functional behavior change, just consistency and safer seeding.

Also applies to: 159-160


146-160: Shuffle over entries + reIndex is correct; minor note on offset bounds

The Fisher–Yates shuffle over entries followed by reIndex preserves order-aware hashing. The offset guard is correct. After adopting the RNG unification (above), this is good to go.

If you want, I can add a small unit test to verify shuffle(offset) preserves the prefix and rehashes correctly (e.g., lookups by key still succeed post-shuffle).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4171e87 and 3da3979.

📒 Files selected for processing (2)
  • src/connection.zig (1 hunks)
  • src/server_pool.zig (5 hunks)
🔇 Additional comments (5)
src/connection.zig (1)

1090-1096: No-op whitespace tweak is fine

The blank-line removal around restarting the flusher thread is harmless and keeps the reconnection sequence intact (flags set under mutex, then spawn). No action needed.

src/server_pool.zig (4)

23-39: Keyed Server lifecycle looks solid

Storing host:port in Server.key, allocating in init and freeing in deinit, is clean. The errdefer paths correctly unwind both Url and key allocations. This dovetails well with the hashmap-based pool.

Also applies to: 41-46


50-59: Switch to StringArrayHashMapUnmanaged simplifies pool and reduces lookups

Consolidating to a single ordered string-keyed map is a good move: O(1) lookup/rotation via key, fewer structures to maintain, and simpler deinit. Using iterator() + destroy in deinit is correct; keys are then released by servers.deinit(allocator).

Also applies to: 63-74


139-142: getFirstServer helper is clear and efficient

Using values()[0] maintains insertion order semantics and matches your rotation logic. Straightforward and readable.


107-133: Interaction note: reconnection counters drive removal now

getNextServer reads current_srv.reconnects to decide rotation vs. removal. Given the current connection logic increments reconnects in multiple places during errors, verify that you don’t effectively halve the intended max_reconnect by double-incrementing per failed attempt. If needed, centralize the increment in one place.

I can scan the repo and propose an adjustment if you want to align with the C client’s semantics.

Comment on lines +125 to 129
// Remove the server permanently (like C library lines 104-106)
const removed_server = self.servers.fetchSwapRemove(current_srv.key).?;
removed_server.value.deinit(self.allocator);
self.allocator.destroy(removed_server.value);
}
Copy link
Contributor

@coderabbitai coderabbitai bot Aug 23, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Memory leak on permanent removal: free removed key slice

When a server exceeds max_reconnect, you destroy the *Server but don’t free the map-owned key returned by fetchSwapRemove. With StringArrayHashMapUnmanaged, the removed key’s memory becomes caller-owned and must be freed. Add a free to avoid leaking the key.

         } else {
             // Remove the server permanently (like C library lines 104-106)
-            const removed_server = self.servers.fetchSwapRemove(current_srv.key).?;
-            removed_server.value.deinit(self.allocator);
-            self.allocator.destroy(removed_server.value);
+            const removed_server = self.servers.fetchSwapRemove(current_srv.key).?;
+            // Free the key slice that was owned by the map
+            self.allocator.free(removed_server.key);
+            // Tear down the server
+            removed_server.value.deinit(self.allocator);
+            self.allocator.destroy(removed_server.value);
         }
📝 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
// Remove the server permanently (like C library lines 104-106)
const removed_server = self.servers.fetchSwapRemove(current_srv.key).?;
removed_server.value.deinit(self.allocator);
self.allocator.destroy(removed_server.value);
}
} else {
// Remove the server permanently (like C library lines 104-106)
const removed_server = self.servers.fetchSwapRemove(current_srv.key).?;
// Free the key slice that was owned by the map
self.allocator.free(removed_server.key);
// Tear down the server
removed_server.value.deinit(self.allocator);
self.allocator.destroy(removed_server.value);
}
🤖 Prompt for AI Agents
In src/server_pool.zig around lines 125 to 129, when permanently removing a
server you call self.servers.fetchSwapRemove(current_srv.key) and deinit/destroy
the removed_server.value but forget to free the map-owned key slice returned by
fetchSwapRemove; for StringArrayHashMapUnmanaged the removed key becomes
caller-owned and must be freed. After obtaining removed_server from
fetchSwapRemove and before leaving the block, call the allocator.free on the
removed_server.key (or appropriate slice free API) to release the key memory,
ensuring you free it only once and handle any null/empty cases safely.

Copy link
Owner Author

Choose a reason for hiding this comment

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

    pub fn deinit(self: *Server, allocator: Allocator) void {
        self.parsed_url.deinit();
        allocator.free(self.key);
        if (self.tls_name) |t| allocator.free(t);
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

@lalinsky You're absolutely right! I apologize for the confusion. Looking at the Server.deinit() method, I can see that allocator.free(self.key) is already being called, which means the key memory is properly freed when removed_server.value.deinit(self.allocator) executes.

The memory management is correct as implemented:

  1. fetchSwapRemove() returns the key-value pair where removed_server.key points to the same memory as removed_server.value.key
  2. When removed_server.value.deinit() is called, it frees the key via allocator.free(self.key)
  3. No additional free() call is needed on the removed key

Thank you for the clarification - the code handles memory cleanup properly through the Server's deinit method.

@lalinsky lalinsky merged commit f39ec17 into main Aug 23, 2025
8 checks passed
@lalinsky lalinsky deleted the refactor-server-pool-arrayhashmap branch August 23, 2025 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant