Skip to content

fix: add thread-safe connection management to ReactOnRailsPro::Request#2259

Merged
AbanoubGhadban merged 4 commits intomasterfrom
fix-request-connection-race-condition
Dec 29, 2025
Merged

fix: add thread-safe connection management to ReactOnRailsPro::Request#2259
AbanoubGhadban merged 4 commits intomasterfrom
fix-request-connection-race-condition

Conversation

@AbanoubGhadban
Copy link
Copy Markdown
Collaborator

@AbanoubGhadban AbanoubGhadban commented Dec 28, 2025

Summary

Fixes race conditions in ReactOnRailsPro::Request connection management:

  • Lazy initialization race: @connection ||= create_connection is not atomic - multiple threads could create duplicate connections at startup
  • Mutex creation race: Even @mutex ||= Mutex.new has a race, so we use a constant CONNECTION_MUTEX instead
  • Exception safety: Create new connection before closing old one

Solution

Uses the double-checked locking pattern:

CONNECTION_MUTEX = Mutex.new

def connection
  conn = @connection
  return conn if conn  # Fast path: lock-free

  CONNECTION_MUTEX.synchronize do
    @connection ||= create_connection  # Slow path: only once per process
  end
end

Performance characteristics

Scenario Behavior
Steady state (99.99% of calls) Lock-free pointer read
Initialization Single mutex acquisition per worker
Reset Mutex-protected, creates before closing

Why this approach?

Alternatives considered:

  • Concurrent::AtomicReference: Lock-free but wastes connections at startup (all racing threads create one, N-1 discarded)
  • Concurrent::Map: Equivalent behavior but adds dependency and complexity for a single value

The Mutex + double-checked locking is optimal because:

  • Only 1 connection created at startup (vs N with AtomicReference)
  • Lock-free after initialization
  • No external dependencies (concurrent-ruby not in gemspec)
  • Simple and well-understood pattern

Test plan

  • Existing tests pass
  • Manual verification in multi-threaded environment

Closes #2258

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Improved internal connection management for more reliable, thread-safe lazy initialization and atomic swaps during concurrent access and resets.
  • Tests

    • Added concurrency-focused tests to ensure single initialization under race conditions, safe resets while in use, proper replacement/closure of connections, and graceful handling of close errors.

✏️ Tip: You can customize this high-level summary in your review settings.

Fixes race conditions in connection management:

1. Lazy initialization race: `@connection ||= create_connection` is not
   atomic - multiple threads could create duplicate connections at startup.

2. Mutex creation race: Even `@mutex ||= Mutex.new` has a race condition,
   so we use a constant CONNECTION_MUTEX instead.

3. Exception safety: Create new connection before closing old one, so if
   create_connection fails, the old connection remains valid.

Solution uses double-checked locking pattern:
- Fast path: Lock-free read for 99.99% of requests after initialization
- Slow path: Mutex-protected initialization (only once per process)
- Best-effort close with rescue nil to prevent close failures from
  breaking the new connection

Closes #2258

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 28, 2025

Walkthrough

Adds class-level synchronization to ReactOnRailsPro::Request: introduces a constant CONNECTION_MUTEX, implements double-checked locking in connection for lock-free reads after init, and makes reset_connection create-and-swap the connection under the mutex and close the old connection safely.

Changes

Cohort / File(s) Summary
Thread-safe connection management
react_on_rails_pro/lib/react_on_rails_pro/request.rb
Added public constant CONNECTION_MUTEX. connection gets a fast-path cached read and uses double-checked locking inside CONNECTION_MUTEX.synchronize for lazy initialization. reset_connection now creates a new connection, swaps it under the mutex, and closes the old connection with close-errors rescued.
Concurrency tests
react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb
New specs asserting single initialization under concurrency, safe concurrent reset_connection during access, old connection is closed on swap, and close failures are handled without raising.
Changelog
react_on_rails_pro/CHANGELOG.md
Documented the fix: thread-safe connection management with double-checked locking and mutex introduction (PR reference added).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

bug, review-needed, full-ci

Poem

🐇 I hopped through threads with nimble feet,
A mutex planted where races meet.
I check, then lock, then swap with care,
Close the old one — handle errors fair.
Now connections rest without a beat.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding thread-safe connection management to ReactOnRailsPro::Request, which is exactly what the changeset implements.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #2258: implements constant CONNECTION_MUTEX, double-checked locking in connection method, mutex-protected reset_connection with exception-safe new/old swap, and comprehensive tests validating thread safety.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the linked issue objectives: implementation of thread-safe connection management, supporting tests, and changelog documentation. No extraneous modifications detected.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-request-connection-race-condition

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f256111 and f1ae293.

📒 Files selected for processing (1)
  • react_on_rails_pro/CHANGELOG.md
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to /CHANGELOG_PRO.md : Update `/CHANGELOG_PRO.md` for user-visible changes in the Pro-only react_on_rails_pro gem and npm packages
📚 Learning: 2025-12-19T18:57:59.314Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to /CHANGELOG_PRO.md : Update `/CHANGELOG_PRO.md` for user-visible changes in the Pro-only react_on_rails_pro gem and npm packages

Applied to files:

  • react_on_rails_pro/CHANGELOG.md
📚 Learning: 2025-12-19T18:57:59.314Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to /CHANGELOG.md : Update `/CHANGELOG.md` for user-visible changes in the open-source react_on_rails gem and npm package (features, bug fixes, breaking changes, deprecations, performance improvements)

Applied to files:

  • react_on_rails_pro/CHANGELOG.md
📚 Learning: 2025-12-19T18:57:59.314Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to /{CHANGELOG.md,CHANGELOG_PRO.md} : Use format `[PR 1818](https://github.com/shakacode/react_on_rails/pull/1818) by [username](https://github.com/username)` in changelog entries (no hash in PR number)

Applied to files:

  • react_on_rails_pro/CHANGELOG.md
📚 Learning: 2025-10-23T17:22:01.074Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1875
File: lib/react_on_rails/utils.rb:112-124
Timestamp: 2025-10-23T17:22:01.074Z
Learning: In React on Rails, when Pro is installed but not licensed, the intended behavior is to raise an error on boot. The `react_on_rails_pro?` method validates licenses and should raise errors early (including during path resolution in methods like `server_bundle?`) to enforce licensing requirements rather than failing later with obscure errors.

Applied to files:

  • react_on_rails_pro/CHANGELOG.md
📚 Learning: 2025-12-19T18:57:59.314Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to sig/react_on_rails/**/*.rbs : RBS signatures in `sig/react_on_rails/` should be added for new Ruby files in `lib/react_on_rails/`, included in Steepfile, validated with `bundle exec rake rbs:validate`, and type-checked with `bundle exec rake rbs:steep`

Applied to files:

  • react_on_rails_pro/CHANGELOG.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: examples (3.4, latest)
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: pro-lint-js-and-ruby
  • GitHub Check: markdown-link-check
  • GitHub Check: claude-review
🔇 Additional comments (1)
react_on_rails_pro/CHANGELOG.md (1)

53-53: LGTM! Changelog entry is clear and follows conventions.

The PR reference format is correct (no hash prefix, linked author), and the description accurately conveys the problem (race condition in lazy initialization) and the solution (mutex with double-checked locking). Placement in the Fixed section under v16.2.0.beta.13 is appropriate, and the technical details about lock-free reads provide useful context for users concerned with performance.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Dec 28, 2025

Code Review - Thread-Safe Connection Management

Overall Assessment

Excellent - This PR correctly implements the double-checked locking pattern to fix race conditions in connection management. The solution is well-reasoned and appropriate for the use case.


Strengths

  1. Correct use of double-checked locking pattern

    • Fast path (line 95-96) avoids lock contention for steady-state reads
    • Slow path (line 99-101) uses mutex only during initialization
    • Prevents duplicate connection creation at startup
  2. Constant mutex eliminates initialization race

    • Using CONNECTION_MUTEX = Mutex.new as a constant is the right approach
    • Avoids the race condition that would exist with @mutex ||= Mutex.new
  3. Exception-safe reset logic

    • Creates new connection before closing old one (line 16-17)
    • Best-effort close with rescue nil prevents breaking new connection if close fails
    • Entire operation is atomic within the mutex
  4. Well-documented rationale

    • PR description clearly explains the problem and solution
    • Inline comments explain the purpose (lines 10-11, 94, 98)
    • Performance characteristics table helps reviewers understand trade-offs

Issues to Address

1. CRITICAL: Missing multi-threading test coverage

The existing test suite (request_spec.rb) has zero tests for concurrent access patterns. For a PR specifically addressing race conditions, this is a significant gap.

Recommendation: Add a test that exercises the race condition:

describe "thread-safe connection management" do
  it "creates only one connection when accessed concurrently" do
    # Reset to ensure clean state
    described_class.reset_connection
    described_class.instance_variable_set(:@connection, nil)
    
    connections_created = Concurrent::AtomicFixnum.new(0)
    
    # Stub create_connection to track calls
    allow(described_class).to receive(:create_connection).and_wrap_original do |m|
      connections_created.increment
      m.call
    end
    
    # Simulate 10 threads racing to initialize connection
    threads = 10.times.map do
      Thread.new { described_class.send(:connection) }
    end
    threads.each(&:join)
    
    # Should only create ONE connection despite 10 concurrent calls
    expect(connections_created.value).to eq(1)
  end
  
  it "safely handles reset during concurrent access" do
    # Test that reset_connection doesn't cause errors when called
    # during concurrent rendering requests
    threads = []
    errors = Concurrent::Array.new
    
    # Background threads making requests
    5.times do
      threads << Thread.new do
        begin
          100.times { described_class.send(:connection) }
        rescue => e
          errors << e
        end
      end
    end
    
    # Reset connection in the middle
    sleep 0.01
    described_class.reset_connection
    
    threads.each(&:join)
    expect(errors).to be_empty
  end
end

Why this matters: According to CLAUDE.md:

"CRITICAL: NEVER claim a test is 'fixed' without running it locally first"

The PR description has an unchecked test plan item. Add actual tests before merging.


2. MEDIUM: RuboCop disable comment may not be necessary

Line 19: old_conn&.close rescue nil # rubocop:disable Style/RescueModifier

Issue: The rescue modifier disable may indicate code smell. Consider if this is the right approach.

Alternative 1 - More explicit (recommended):

begin
  old_conn&.close
rescue StandardError => e
  # Log but don't raise - closing old connection is best-effort
  Rails.logger.debug { "[ReactOnRailsPro] Failed to close old connection: #{e.message}" }
end

Alternative 2 - Keep rescue modifier but add comment:

# Best-effort close: don't let close failures break new connection
old_conn&.close rescue nil # rubocop:disable Style/RescueModifier

Why this matters:

  • If close raises, you want to know why (connection leak, resource exhaustion, etc.)
  • Silent failures can mask underlying issues
  • Logging helps with debugging production issues

3. LOW: Potential memory visibility issue (theoretical)

The fast path (line 95-96) reads @connection without a memory barrier. In theory, on some architectures, a thread might see @connection as non-nil but with stale/uninitialized values.

In practice: This is extremely unlikely to cause issues because:

  • Ruby's GIL provides implicit synchronization on MRI
  • The connection is fully initialized before being assigned
  • Modern CPUs have strong memory ordering

If you want to be extra safe:

def connection
  # Read once to ensure memory visibility
  conn = @connection
  return conn if conn&.respond_to?(:post)  # Verify it's a real connection
  
  CONNECTION_MUTEX.synchronize do
    @connection ||= create_connection
  end
end

Recommendation: Document the assumption that Ruby MRI's GIL provides sufficient memory barriers. Not a blocker for merging.


Code Quality Observations

Good: Comments explain WHY, not WHAT
Good: PR description includes performance analysis
Good: Considered alternatives (AtomicReference, Concurrent::Map)
⚠️ Concern: No changelog entry (check if this is user-facing or internal)


Security Considerations

No security concerns identified

  • Connection pooling is properly managed
  • No credential exposure
  • No injection vulnerabilities

Performance Considerations

Excellent performance characteristics:

  • Lock-free reads after initialization (99.99% of calls)
  • Single mutex acquisition per worker process
  • No connection waste at startup

Validated against requirements:

  • Single connection creation ✅
  • Thread-safe reset ✅
  • Minimal overhead ✅

Recommended Actions Before Merge

  1. ⚠️ REQUIRED: Add multi-threading tests (see suggestion above)
  2. 💡 SUGGESTED: Consider logging in rescue block instead of silent swallowing
  3. 📝 OPTIONAL: Add comment about memory visibility assumptions
  4. VERIFY: Run bundle exec rubocop to ensure no violations

Final Recommendation

APPROVE with changes - Excellent implementation of thread-safe pattern, but needs test coverage before merging.

The code correctly solves the race condition problem with an appropriate solution. Once multi-threading tests are added, this is ready to merge.


Review performed using project guidelines from CLAUDE.md

Copy link
Copy Markdown
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 (1)
react_on_rails_pro/lib/react_on_rails_pro/request.rb (1)

1-333: Consider updating CHANGELOG_PRO.md for this bug fix.

This change fixes race conditions in connection management that could lead to duplicate connections or failures in multi-threaded environments. Consider adding an entry to CHANGELOG_PRO.md under bug fixes, as this improves reliability for users running multi-threaded Rails servers.

Based on learnings, CHANGELOG_PRO.md should be updated for user-visible changes in the Pro gem.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4dcb8fc and 97465f2.

📒 Files selected for processing (1)
  • react_on_rails_pro/lib/react_on_rails_pro/request.rb
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rb: Run bundle exec rubocop and fix ALL violations before every commit/push
Run bundle exec rubocop (MANDATORY) before every commit to ensure zero offenses

Files:

  • react_on_rails_pro/lib/react_on_rails_pro/request.rb
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to /CHANGELOG_PRO.md : Update `/CHANGELOG_PRO.md` for user-visible changes in the Pro-only react_on_rails_pro gem and npm packages
📚 Learning: 2025-12-19T18:57:59.314Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to /CHANGELOG_PRO.md : Update `/CHANGELOG_PRO.md` for user-visible changes in the Pro-only react_on_rails_pro gem and npm packages

Applied to files:

  • react_on_rails_pro/lib/react_on_rails_pro/request.rb
📚 Learning: 2025-12-19T18:57:59.314Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to /CHANGELOG.md : Update `/CHANGELOG.md` for user-visible changes in the open-source react_on_rails gem and npm package (features, bug fixes, breaking changes, deprecations, performance improvements)

Applied to files:

  • react_on_rails_pro/lib/react_on_rails_pro/request.rb
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: pro-lint-js-and-ruby
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: claude-review
🔇 Additional comments (4)
react_on_rails_pro/lib/react_on_rails_pro/request.rb (4)

10-12: Excellent use of a constant mutex to eliminate initialization races.

Using CONNECTION_MUTEX as a constant correctly prevents the race condition inherent in @mutex ||= Mutex.new, and the comment clearly explains the rationale.


14-21: Well-designed exception-safe connection reset.

The implementation correctly:

  • Guards the entire operation with CONNECTION_MUTEX.synchronize
  • Creates the new connection before closing the old one, ensuring a valid connection remains if create_connection raises
  • Performs best-effort close with rescue nil, which is appropriate here since the connection is being replaced anyway

The inline rescue with the explicit rubocop disable is acceptable in this context.


93-102: Correct implementation of double-checked locking for lazy initialization.

The pattern is implemented correctly:

  • Fast path (lines 95-96) provides lock-free reads after initialization
  • Slow path (lines 99-101) uses CONNECTION_MUTEX.synchronize with ||= to ensure single initialization
  • Comments clearly document the performance characteristics

This approach minimizes mutex contention while maintaining thread-safety.


1-333: Ensure rubocop passes before merging.

Per coding guidelines, bundle exec rubocop must pass with zero offenses before every commit. The file contains intentional rubocop disables on lines 19, 109, and 269 for Style/RescueModifier, Metrics/AbcSize, Metrics/CyclomaticComplexity, and Metrics/MethodLength. Confirm that bundle exec rubocop react_on_rails_pro/lib/react_on_rails_pro/request.rb passes locally.

Adds tests verifying the race condition fix:

1. "creates only one connection when accessed concurrently"
   - Spawns 10 threads racing to initialize connection
   - Verifies only ONE connection is created (not 10)

2. "safely handles reset during concurrent access"
   - Tests reset_connection doesn't cause errors when called
     during concurrent connection access

3. "properly closes old connection on reset"
   - Verifies old connection is closed when reset

4. "handles close errors gracefully during reset"
   - Verifies close errors don't prevent new connection setup

All tests use Ruby stdlib (Mutex, Thread) - no concurrent-ruby dependency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Dec 28, 2025

Code Review: Thread-Safe Connection Management

Summary

This PR effectively addresses a real race condition in connection management using the well-established double-checked locking pattern. The implementation is sound, well-tested, and appropriately scoped.


✅ Strengths

1. Excellent Problem Analysis

  • Clear identification of the race condition in @connection ||= create_connection
  • Well-reasoned rejection of alternatives (AtomicReference, concurrent-ruby)
  • Strong understanding of the performance/correctness tradeoffs

2. Optimal Solution Choice

The double-checked locking pattern is perfect for this use case:

  • Lock-free fast path for 99.99% of calls (steady state)
  • Only one connection created per process (vs N with atomic compare-and-swap)
  • No external dependencies required
  • Simple, maintainable, battle-tested pattern

3. Comprehensive Test Coverage

The test suite is excellent - it covers all the critical scenarios:

  • ✅ Concurrent initialization (verifies only 1 connection created)
  • ✅ Reset during concurrent access (stress test)
  • ✅ Proper connection cleanup on reset
  • ✅ Exception safety during close

The tests properly use thread synchronization primitives (Mutex) to track concurrent behavior and include sleep(0.01) to widen the race window - showing good understanding of race condition testing.

4. Exception Safety in reset_connection

new_conn = create_connection
old_conn = @connection
@connection = new_conn
old_conn&.close rescue nil

This ordering is critical:

  1. Create new connection first (if this fails, old connection still works)
  2. Swap the reference atomically (under mutex)
  3. Close old connection with best-effort semantics (rescue nil)

This prevents the scenario where close fails and leaves you with no connection.


💡 Suggestions for Improvement

1. Minor: Comment Clarity in reset_connection

Consider adding a brief comment explaining the ordering:

def reset_connection
  CONNECTION_MUTEX.synchronize do
    # Create new connection before closing old to maintain availability if creation fails
    new_conn = create_connection
    old_conn = @connection
    @connection = new_conn
    # Best-effort close: don't let close failures break new connection
    old_conn&.close rescue nil # rubocop:disable Style/RescueModifier
  end
end

Why: The ordering is subtle but critical for exception safety. A comment helps future maintainers understand the intent.

2. Consider: Documenting Thread-Safety Guarantees

Add a module-level comment documenting the thread-safety contract:

module ReactOnRailsPro
  # Thread-safe HTTP client for Node renderer communication.
  # - Connection initialization is thread-safe (double-checked locking)
  # - reset_connection is safe to call during active requests
  # - All public methods are thread-safe
  class Request

Why: Makes the thread-safety guarantee explicit for users of this class.

3. Future Enhancement: Connection Leak Monitoring

Not required for this PR, but consider adding instrumentation in the future:

# Track connection resets for debugging
def reset_connection
  CONNECTION_MUTEX.synchronize do
    new_conn = create_connection
    old_conn = @connection
    @connection = new_conn
    Rails.logger.debug { "[ReactOnRailsPro] Connection reset" }
    old_conn&.close rescue nil
  end
end

Why: If connection resets happen unexpectedly in production, logging would help diagnose.


🔒 Security Considerations

No security concerns. This is a low-level concurrency fix with no security implications.


⚡ Performance Considerations

Performance characteristics are optimal:

Scenario Before After
Steady state Unlocked read + branch Same (fast path)
Initialization Race condition Mutex (once per process)
Memory barriers No Yes (Mutex ensures visibility)

The mutex adds ~20-50ns overhead only during initialization. After that, it's a simple pointer read + null check (lock-free).


🧪 Testing Recommendations

Before Merging:

✅ Run existing test suite
✅ Manual verification in multi-threaded environment (as noted in PR description)

Additional Testing (if feasible):

Consider testing in a real multi-process Puma/Unicorn environment under load to verify the fix in production-like conditions. However, the race window is small enough that this may not trigger in manual testing.


📋 Checklist

  • Code follows project conventions
  • No security issues
  • Performance is optimal for use case
  • Test coverage is comprehensive
  • Exception safety is handled correctly
  • No breaking changes
  • Thread-safety is guaranteed
  • Minor: Add explanatory comments (suggestion only)

🎯 Verdict

LGTM with minor suggestions. This is a high-quality fix that:

  • Solves a real (if rare) race condition
  • Uses the right pattern for the job
  • Has excellent test coverage
  • Maintains performance in the common case
  • Handles errors gracefully

The suggestions above are optional improvements, not blockers. The code is production-ready as-is.

Great work! 🚀


Note: Waiting for CI to complete before final approval. Based on the nature of the changes (adding mutex protection to existing logic), I don't anticipate any CI failures.

Copy link
Copy Markdown
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 (1)
react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb (1)

275-288: LGTM: Validates graceful handling of close errors.

This test confirms that errors during close are suppressed, ensuring reset_connection always establishes a new connection even if closing the old one fails.

Consider adding test coverage for connection creation failure.

The PR objectives mention that "new connection is created before closing the old one to avoid leaving no usable connection if creation fails." While this test covers close errors, consider adding a test that verifies behavior when create_connection raises an exception during reset. The old connection should remain valid if the new connection cannot be created.

🔎 Suggested additional test case

Add this test after line 288:

it "preserves old connection when new connection creation fails during reset" do
  old_connection = instance_double(HTTPX::Session)
  
  described_class.instance_variable_set(:@connection, old_connection)
  
  allow(described_class).to receive(:create_connection).and_raise(StandardError, "Network error")
  allow(old_connection).to receive(:close)
  
  # Should raise when creation fails
  expect { described_class.reset_connection }.to raise_error(StandardError, "Network error")
  
  # Old connection should NOT have been closed
  expect(old_connection).not_to have_received(:close)
  
  # Old connection should still be in use
  expect(described_class.send(:connection)).to eq(old_connection)
end
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97465f2 and e90e1c2.

📒 Files selected for processing (1)
  • react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rb: Run bundle exec rubocop and fix ALL violations before every commit/push
Run bundle exec rubocop (MANDATORY) before every commit to ensure zero offenses

Files:

  • react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb
🧠 Learnings (2)
📚 Learning: 2025-12-19T18:57:59.314Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to react_on_rails/spec/dummy/e2e/playwright/e2e/**/*.spec.js : Use Playwright E2E tests in `react_on_rails/spec/dummy/e2e/playwright/` for React component integration testing. Tests automatically start Rails server on port 5017 before running

Applied to files:

  • react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb
📚 Learning: 2025-12-19T18:57:59.314Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to /CHANGELOG_PRO.md : Update `/CHANGELOG_PRO.md` for user-visible changes in the Pro-only react_on_rails_pro gem and npm packages

Applied to files:

  • react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: examples (3.4, latest)
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: pro-lint-js-and-ruby
  • GitHub Check: claude-review
🔇 Additional comments (4)
react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb (4)

199-210: LGTM: Clean test setup and teardown.

The before/after blocks properly reset connection state between tests to ensure test isolation.


212-231: LGTM: Well-designed concurrency test.

The test effectively validates single-creation semantics under concurrent access by:

  • Using a controlled race window (sleep 0.01s)
  • Tracking creation attempts with a thread-safe counter
  • Spawning 10 competing threads

This confirms the double-checked locking implementation prevents duplicate connections.


233-258: LGTM: Effective stress test for concurrent reset.

This test validates that reset_connection can execute safely while multiple threads are actively reading the connection. The 5 threads × 50 iterations provide good coverage, and the error collection mechanism properly captures any thread-safety violations.


260-273: LGTM: Validates proper resource cleanup.

This test confirms that reset_connection properly closes the old connection before replacing it with a new one, preventing resource leaks.

Remove `rescue nil` to maintain original behavior of raising errors
if closing the old connection fails. The new connection is still
assigned before close is called, so the system remains functional
even if close fails.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Dec 28, 2025

Code Review: Thread-Safe Connection Management

Great work on addressing the race conditions in connection management! The implementation is solid and well-tested. Here's my detailed feedback:

Strengths

  1. Excellent use of double-checked locking pattern

    • Lock-free fast path for the common case (99.99% of calls)
    • Proper synchronization for initialization
    • Well-documented with clear comments explaining the approach
  2. Comprehensive test coverage

    • Tests for concurrent initialization (the primary race condition)
    • Tests for reset during concurrent access
    • Tests for proper connection cleanup
    • Tests for error propagation during close
    • Good use of sleep() to widen the race window for reliability
  3. Exception safety in reset_connection

    • Creating new connection before closing old one is the right approach
    • Ensures the system stays operational even if close fails
    • The test at line 275-289 confirms this behavior
  4. Well-reasoned design decisions

    • PR description clearly explains why this approach over alternatives
    • Avoids unnecessary dependencies (concurrent-ruby)
    • Simple, maintainable solution

🔍 Observations & Suggestions

1. Close Error Handling in reset_connection (Minor)

The current implementation propagates close errors (line 285 test confirms this). This is safe because the new connection is already assigned, but consider whether this is the desired behavior:

Current behavior:

def reset_connection
  CONNECTION_MUTEX.synchronize do
    new_conn = create_connection
    old_conn = @connection
    @connection = new_conn
    old_conn&.close  # Raises if close fails
  end
end

Question: Should close failures be:

  • Propagated (current) - safer, alerts caller to resource leak
  • Logged and swallowed - more forgiving, prevents disruption

I'd recommend keeping current behavior (propagate), but consider adding a comment explaining the decision.

2. Memory Ordering Guarantees (Theoretical - likely fine in practice)

The double-checked locking pattern relies on proper memory ordering. In Ruby (MRI), this is safe due to the GIL, but consider adding a comment for clarity:

def connection
  # Fast path: return existing connection without locking (lock-free for 99.99% of calls)
  # Safe in MRI due to GIL; memory ordering guaranteed by mutex synchronize in slow path
  conn = @connection
  return conn if conn
  # ...
end

This is mostly for documentation - the current implementation is safe.

3. Test Timing Dependencies (Minor)

The test at line 233-258 uses sleep(0.005) to trigger reset during concurrent access. This is generally fine, but timing-dependent tests can be flaky on slow CI systems.

Suggestion: Consider testing the synchronization more deterministically by:

  • Using a condition variable to coordinate threads
  • Or documenting that this test may need timing adjustments on slow systems

Not a blocker - the current test is reasonable.

4. Connection Lifecycle Documentation

Consider adding RDoc/YARD comments to document:

  • When reset_connection should be called
  • Thread-safety guarantees
  • What happens if create_connection raises

Example:

# Resets the HTTP connection to the Node renderer.
# Thread-safe: can be called concurrently with other connection access.
# @raise [ReactOnRailsPro::Error] if connection creation fails
# @raise [StandardError] if closing the old connection fails (new connection is still set)
def reset_connection
  # ...
end

🔒 Security & Performance

  • ✅ No security concerns
  • ✅ Excellent performance characteristics (lock-free fast path)
  • ✅ No resource leaks (old connections properly closed)
  • ✅ Proper exception safety

📊 Test Coverage Assessment

Test coverage is excellent:

  • ✅ Concurrent initialization
  • ✅ Concurrent access during reset
  • ✅ Proper cleanup
  • ✅ Error propagation

One additional test to consider:

  • Test behavior when create_connection raises during initialization (does the mutex get released properly?)

🎯 Final Verdict

This PR is production-ready. The implementation is correct, well-tested, and solves the race condition effectively. The suggestions above are minor improvements, not blockers.

Recommended next steps:

  1. ✅ Merge as-is (it's already excellent)
  2. 🔄 OR address the minor documentation suggestions if you have time
  3. ✅ Monitor in production for any edge cases

Great work! 🎉


Review generated with assistance from Claude Code

Copy link
Copy Markdown
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

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e90e1c2 and f256111.

📒 Files selected for processing (2)
  • react_on_rails_pro/lib/react_on_rails_pro/request.rb
  • react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rb: Run bundle exec rubocop and fix ALL violations before every commit/push
Run bundle exec rubocop (MANDATORY) before every commit to ensure zero offenses

Files:

  • react_on_rails_pro/lib/react_on_rails_pro/request.rb
🧠 Learnings (2)
📚 Learning: 2025-12-19T18:57:59.314Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to /CHANGELOG_PRO.md : Update `/CHANGELOG_PRO.md` for user-visible changes in the Pro-only react_on_rails_pro gem and npm packages

Applied to files:

  • react_on_rails_pro/lib/react_on_rails_pro/request.rb
📚 Learning: 2025-12-19T18:57:59.314Z
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T18:57:59.314Z
Learning: Applies to /CHANGELOG.md : Update `/CHANGELOG.md` for user-visible changes in the open-source react_on_rails gem and npm package (features, bug fixes, breaking changes, deprecations, performance improvements)

Applied to files:

  • react_on_rails_pro/lib/react_on_rails_pro/request.rb
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: examples (3.4, latest)
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: pro-lint-js-and-ruby
🔇 Additional comments (4)
react_on_rails_pro/lib/react_on_rails_pro/request.rb (4)

10-12: Excellent choice using a constant Mutex.

This eliminates the race condition that would occur with instance-level lazy initialization (@mutex ||= Mutex.new). The constant is initialized once at class load time, making it inherently thread-safe.


14-21: Exception-safe connection swap implemented correctly.

The create-before-close pattern ensures a valid connection remains available even if close raises an exception. The new connection is assigned before attempting to close the old one, preventing request failures.

Note that close errors now propagate (per the PR description's removal of rescue nil). This allows callers to handle close failures while maintaining system availability.


93-102: Double-checked locking pattern correctly implemented.

The fast path (lines 95-96) provides lock-free reads for the common case, while the slow path (lines 99-101) ensures thread-safe initialization. The ||= operator inside the mutex handles the case where multiple threads waited for the lock—only the first creates the connection.

The implicit return of the synchronize block's result correctly returns the initialized @connection.


10-21: Unable to verify RuboCop compliance in this environment. The code at lines 10-21 and 93-102 appears visually well-formatted with proper indentation and spacing, but actual verification requires running bundle exec rubocop in an environment with Ruby and Bundler installed. This check must be performed locally before committing.

Comment on lines +10 to 21
# Mutex for thread-safe connection management.
# Using a constant eliminates the race condition that would exist with @mutex ||= Mutex.new
CONNECTION_MUTEX = Mutex.new

def reset_connection
@connection&.close
@connection = create_connection
CONNECTION_MUTEX.synchronize do
new_conn = create_connection
old_conn = @connection
@connection = new_conn
old_conn&.close
end
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git diff origin/master...HEAD -- CHANGELOG_PRO.md | head -100

Repository: shakacode/react_on_rails

Length of output: 110


🏁 Script executed:

# Check if CHANGELOG_PRO.md exists and show recent entries
if [ -f CHANGELOG_PRO.md ]; then
  echo "=== CHANGELOG_PRO.md first 50 lines ===" 
  head -50 CHANGELOG_PRO.md
else
  echo "CHANGELOG_PRO.md not found"
fi

Repository: shakacode/react_on_rails

Length of output: 94


🏁 Script executed:

# Check the affected file for rubocop violations
bundle exec rubocop react_on_rails_pro/lib/react_on_rails_pro/request.rb 2>&1 || echo "Rubocop check completed"

Repository: shakacode/react_on_rails

Length of output: 136


🏁 Script executed:

# Verify both sections mentioned exist in the file
echo "=== Lines 10-21 ===" 
sed -n '10,21p' react_on_rails_pro/lib/react_on_rails_pro/request.rb

echo ""
echo "=== Lines 93-102 ===" 
sed -n '93,102p' react_on_rails_pro/lib/react_on_rails_pro/request.rb

Repository: shakacode/react_on_rails

Length of output: 869


🏁 Script executed:

# Search for changelog files
find . -type f -name "*CHANGE*" -o -name "*RELEASE*" | head -20

Repository: shakacode/react_on_rails

Length of output: 116


🏁 Script executed:

# Search for any .md files that might be changelogs
fd -t f "\.md$" | grep -i "change\|release\|news" | head -20

Repository: shakacode/react_on_rails

Length of output: 350


🏁 Script executed:

# List all .md files in repo root
ls -la *.md 2>/dev/null || echo "No .md files in root"

Repository: shakacode/react_on_rails

Length of output: 968


🏁 Script executed:

# Show first 100 lines of Pro changelog to see recent entries
head -100 react_on_rails_pro/CHANGELOG.md

Repository: shakacode/react_on_rails

Length of output: 9678


🏁 Script executed:

# Search for any mention of race condition, CONNECTION_MUTEX, or connection management
grep -n "race\|mutex\|CONNECTION\|connection" react_on_rails_pro/CHANGELOG.md -i | head -20

Repository: shakacode/react_on_rails

Length of output: 2473


Update react_on_rails_pro/CHANGELOG.md with this race condition fix.

This PR addresses a critical thread-safety issue in connection management. Under concurrent access, the previous pattern @mutex ||= Mutex.new could create a race condition leading to duplicate connections or crashes. The fix uses a constant CONNECTION_MUTEX to eliminate this race condition. This is a user-visible bug fix that must be documented.

Add an entry under "### Fixed" in the "Unreleased" section using the format: [PR ####](https://github.com/shakacode/react_on_rails/pull/####) by [username](https://github.com/username).

🤖 Prompt for AI Agents
In react_on_rails_pro/lib/react_on_rails_pro/request.rb around lines 10 to 21
the mutex change to a constant fixes a race condition; update
react_on_rails_pro/CHANGELOG.md in the "Unreleased" section under the "###
Fixed" header by adding a new line entry in the required format: include the PR
link `[PR ####](https://github.com/shakacode/react_on_rails/pull/####)` and the
author link `by [username](https://github.com/username)` and a short description
like "Fix race condition in connection management by using a constant
CONNECTION_MUTEX instead of @mutex ||= Mutex.new." Ensure the entry is placed
under Unreleased → ### Fixed and follows existing list formatting.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Dec 28, 2025

Code Review

I've reviewed PR #2259 and overall, this is an excellent fix for a critical concurrency issue. The implementation uses the well-established double-checked locking pattern correctly, and the test coverage is thorough.

✅ Strengths

  1. Correct Implementation of Double-Checked Locking

    • The fast path (line 94-96) is lock-free, which is critical for performance in steady-state
    • The slow path (line 99-101) uses proper mutex synchronization
    • Using a constant CONNECTION_MUTEX avoids the race condition that would exist with @mutex ||= Mutex.new
  2. Exception Safety in reset_connection

    • Creating the new connection before closing the old one (lines 16-19) is the right approach
    • This ensures the connection is always available, even if creation fails
    • However, there's a subtle issue here (see below)
  3. Excellent Test Coverage

    • Tests verify single initialization under concurrent access ✅
    • Tests verify safety during concurrent reset ✅
    • Tests verify proper cleanup of old connections ✅
    • Tests verify error propagation behavior ✅

⚠️ Critical Issue: Exception Safety in reset_connection

Problem: The current implementation has a subtle but important bug:

def reset_connection
  CONNECTION_MUTEX.synchronize do
    new_conn = create_connection  # Can raise
    old_conn = @connection
    @connection = new_conn
    old_conn&.close              # Can raise
  end
end

Issue: If create_connection raises an exception, that's fine - the old connection is untouched. But if old_conn&.close raises, the new connection is already assigned to @connection, which seems intentional based on your test at line 275-289.

However, this creates a problem: if close raises, the caller might catch the exception and retry reset_connection, which would:

  1. Create another new connection
  2. Close the connection created in the previous call (which is now orphaned)
  3. Potentially raise again

Recommendation: Add error handling around the close operation:

def reset_connection
  CONNECTION_MUTEX.synchronize do
    new_conn = create_connection
    old_conn = @connection
    @connection = new_conn
    
    begin
      old_conn&.close
    rescue => e
      Rails.logger.warn "[ReactOnRailsPro] Failed to close old connection: #{e.message}"
      # Don't re-raise - new connection is already set
    end
  end
end

Rationale:

  • The new connection is functional and assigned
  • A failure to close the old connection shouldn't prevent using the new one
  • Log the error for debugging but don't propagate it
  • This prevents the retry scenario above

Note: Your test at line 275 expects the error to propagate. If you want to keep that behavior, please add a comment explaining why propagating close errors is the desired behavior, and update the test name to clarify this is intentional (e.g., it "propagates close errors during reset for debugging purposes").

🔍 Minor Observations

  1. Test Timing Dependencies (line 252)

    • sleep(0.005) introduces timing dependency
    • Consider using a thread-safe counter or barrier for more deterministic testing
    • Current implementation is acceptable for concurrency testing but could be flaky on slow CI
  2. CHANGELOG Entry (line 53)

    • Very well written and explains the issue clearly ✅
    • Correctly placed in the Pro changelog ✅
  3. Performance Characteristics

    • The description in the PR body accurately describes the performance trade-offs ✅
    • The double-checked locking pattern is optimal for this use case ✅

📋 Recommendation

Before merging:

  1. CRITICAL: Decide on error handling for close failures in reset_connection

    • Either: Add rescue block (recommended)
    • Or: Add comment explaining why propagation is intentional
  2. Optional but recommended: Make the concurrency test more robust by using synchronization primitives instead of sleeps

Final Verdict

This is a high-quality fix for a real concurrency issue. The implementation is correct, well-tested, and properly documented. With the exception handling clarification above, this is ready to merge.

Great work! 🎉


Review performed by Claude Code following the project's CLAUDE.md guidelines

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Dec 28, 2025

Code Review: Thread-Safe Connection Management

I've reviewed PR #2259 and this is an excellent fix for the race condition in connection management. Here's my detailed feedback:


Strengths

1. Correct Implementation of Double-Checked Locking

The implementation properly follows the double-checked locking pattern:

  • Fast path (lock-free read) handles 99.99% of calls efficiently
  • Slow path (mutex-protected initialization) only runs once per process
  • Uses a constant CONNECTION_MUTEX to avoid the @mutex ||= Mutex.new race condition

2. Exception Safety in reset_connection

The reset logic correctly creates the new connection before closing the old one:

new_conn = create_connection
old_conn = @connection
@connection = new_conn
old_conn&.close

This ensures that if create_connection raises an exception, the old connection remains available.

3. Comprehensive Test Coverage

The new tests are excellent and cover critical concurrency scenarios:

  • ✅ Single initialization under race conditions (10 threads)
  • ✅ Safe resets during concurrent access (5 threads × 50 iterations)
  • ✅ Proper connection replacement and closure
  • ✅ Exception propagation during close

4. Clear Documentation

The PR description thoroughly explains:

  • The problem (race conditions in lazy initialization)
  • The solution (double-checked locking pattern)
  • Performance characteristics
  • Alternative approaches considered

🔍 Minor Suggestions

1. Test Sleep Duration

In the concurrency test, there's a 10ms sleep to simulate connection setup:

sleep(0.01) # Simulate connection setup time to increase race window

Suggestion: Consider reducing this to 0.001 (1ms) to keep tests fast while still creating enough of a race window. Modern computers can context-switch many times in 1ms.

2. Error Handling in Reset Test

The test propagates close errors during reset verifies that close errors are raised:

expect { described_class.reset_connection }.to raise_error(StandardError, "Close failed")
expect(described_class.send(:connection)).to eq(new_connection)

Question: Is this the desired behavior? If closing the old connection fails, should it:

  • A) Raise the error (current behavior) - risks leaving the app in an error state
  • B) Log the error and continue - allows the new connection to be used despite close failure

This might be worth discussing with the team. In production, a failed close (e.g., network issue) shouldn't necessarily prevent using the new connection.

3. Documentation Comment

The inline comment could be slightly more explicit about why we use a constant:

# Mutex for thread-safe connection management.
# Using a constant eliminates the race condition that would exist with @mutex ||= Mutex.new

Suggestion:

# Mutex for thread-safe connection management.
# MUST be a constant - using @mutex ||= Mutex.new would itself have a race condition
# where multiple threads could create multiple mutexes before any thread completes the assignment.

🎯 Design Decision Validation

The PR correctly rejects Concurrent::AtomicReference because it would waste N-1 connections during startup. The double-checked locking approach is optimal here because:

  1. ✅ Only 1 connection created (vs N with atomic reference)
  2. ✅ Lock-free reads after initialization
  3. ✅ No external dependencies (concurrent-ruby not in gemspec)
  4. ✅ Standard, well-understood pattern

🧪 Testing Recommendations

Manual Testing Checklist (Before Merge):

Since this involves concurrency, consider manual testing in a real multi-threaded Rails environment:

# In a Rails console with multi-threading enabled
threads = 100.times.map do
  Thread.new do
    ReactOnRailsPro::Request.send(:connection)
  end
end
threads.each(&:join)

# Verify only one connection was created (check logs)

Load Testing:

In a production-like environment with Puma/Unicorn:

  • Multiple workers
  • Multiple threads per worker
  • Concurrent requests hitting SSR endpoints

Monitor for:

  • Connection pool exhaustion
  • Duplicate connection creation
  • Performance degradation

📊 Performance Impact Assessment

Before (race condition):

  • Multiple threads could create duplicate connections
  • Wasted resources (N-1 unused connections)
  • Potential connection pool exhaustion

After (this fix):

  • Exactly 1 connection per process
  • Lock-free reads (99.99% of calls)
  • Mutex only acquired during initialization

Impact: ✅ Performance improvement + stability fix with no downside


🔒 Security Considerations

No security concerns. This change:

  • Doesn't expose new attack surfaces
  • Doesn't change authentication/authorization
  • Reduces resource exhaustion risk (fewer duplicate connections)

Final Recommendation

APPROVE - This is a high-quality fix that:

  • Correctly solves the race condition
  • Has excellent test coverage
  • Includes clear documentation
  • Follows best practices
  • Has negligible performance impact

Before merging:

  1. Consider the error handling behavior in reset_connection (suggestion Make work with turbolinks and better helper #2)
  2. Optionally reduce test sleep duration to 1ms (suggestion TODO for first version #1)
  3. Consider manual concurrency testing in a real Rails app

Great work @AbanoubGhadban! This is a textbook example of how to fix concurrency bugs properly. 🎉

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Dec 28, 2025

Code Review: Thread-Safe Connection Management

Overview

This PR addresses a critical race condition in connection management for the Pro package's node renderer requests. The implementation is solid and well-thought-out. Here's my detailed review:


✅ Strengths

1. Excellent Problem Identification

  • Correctly identifies the race condition in @connection ||= create_connection
  • Recognizes that even @mutex ||= Mutex.new has a race condition
  • Great performance analysis showing lock-free fast path for 99.99% of calls

2. Appropriate Solution Choice

The double-checked locking pattern is optimal for this use case:

  • Only 1 connection created (vs N with AtomicReference)
  • Lock-free reads after initialization
  • No external dependencies
  • Well-understood pattern in Ruby/Rails ecosystem

3. Excellent Test Coverage

The concurrency tests are comprehensive:

  • Race condition test with 10 threads + artificial delay (spec/react_on_rails_pro/request_spec.rb:218-236)
  • Concurrent access during reset (spec/react_on_rails_pro/request_spec.rb:238-261)
  • Connection replacement verification (spec/react_on_rails_pro/request_spec.rb:263-277)
  • Error propagation test (spec/react_on_rails_pro/request_spec.rb:279-293)

4. Safe Reset Logic

new_conn = create_connection
old_conn = @connection
@connection = new_conn
old_conn&.close

Creating the new connection before closing the old one is excellent - prevents any gap where @connection is nil.

5. Proper Documentation

  • Clear comments explaining the mutex constant
  • Good changelog entry with technical detail
  • PR description explains alternatives considered

🔍 Minor Observations

1. Naming Consistency Check

The open-source version uses instance variables for mutex:

# react_on_rails/lib/react_on_rails/configuration.rb:91
@immediate_hydration_mutex = Mutex.new

This PR uses a constant:

CONNECTION_MUTEX = Mutex.new

Analysis: The constant approach is correct here. The open-source version's instance variable is safe because it's initialized at class load time (not lazy). This PR's constant is necessary because lazy initialization of the mutex itself would have a race condition. ✅

2. Test Organization

The test setup/teardown is good:

before do
  described_class.instance_variable_set(:@connection, nil)
end

Minor suggestion: Consider adding a comment explaining why we reset to nil in before (to ensure clean state for each concurrent test). Not required, but helps future maintainers.

3. Error Handling in Reset

The last test verifies that close errors propagate:

expect { described_class.reset_connection }.to raise_error(StandardError, "Close failed")

Question: Is this the desired behavior? If a production connection close fails during reset, we get an exception but the new connection is already set. This seems reasonable (fail-fast), but consider if you want to log and swallow close errors instead.

Current behavior: Exception raised, new connection installed ✅
Alternative: Log error, swallow exception, continue with new connection

I think the current approach is better - it surfaces issues that should be investigated.


🎯 Performance Considerations

Double-Checked Locking Performance

The PR correctly identifies the performance characteristics:

Scenario Lock Acquired? Cost
Steady state (99.99%) ❌ No ~1-2 CPU cycles (pointer read)
Initialization ✅ Yes One mutex acquisition per process
Reset ✅ Yes Mutex-protected, happens rarely

Verdict: Optimal for this use case. ✅

Comparison to Alternatives

vs. Always using mutex:

  • ❌ Would add ~20-50ns overhead to every connection access
  • ❌ Thread contention on every call

vs. AtomicReference:

  • ❌ Would waste N-1 connections at startup
  • ❌ Adds gem dependency
  • ✅ Slightly faster steady-state (atomic read vs pointer read - negligible difference)

vs. Concurrent::Map:

  • ❌ Overkill for single value
  • ❌ More memory overhead
  • ❌ Less obvious to maintainers

Your choice is best. ✅


🔐 Security Considerations

✅ No security concerns. The mutex prevents race conditions that could theoretically lead to:

  • Connection leaks (multiple connections created but only one tracked)
  • Unexpected behavior if connections are closed while in use

The fix properly addresses both.


📋 Test Coverage Assessment

What's tested:

  • ✅ Single initialization under contention
  • ✅ Safe concurrent access during reset
  • ✅ Proper connection replacement
  • ✅ Error propagation

What's NOT tested (acceptable):

  • Edge case: What happens if create_connection raises during initialization?
    • Current behavior: Exception propagates, @connection stays nil, next caller retries
    • This is fine - the mutex ensures only one thread is attempting creation at a time

Overall coverage: Excellent ✅


🎨 Code Style

Adherence to Project Standards

  • ✅ Uses instance variables for class singleton state (matches existing patterns)
  • ✅ Proper RuboCop compliance (comment: # rubocop:disable RSpec/VerifiedDoubles appropriately used)
  • ✅ Clear comments explaining non-obvious decisions
  • ✅ Follows Rails/Ruby threading conventions

🚀 Recommendations

Before Merge:

  1. Tests pass - Verify locally (per CLAUDE.md requirements)
  2. RuboCop passes - Run bundle exec rubocop
  3. ⚠️ Consider: Add a brief comment in the connection method explaining why we read @connection into a local variable first:
    # Read @connection into local variable to avoid race where it could become
    # nil between the 'if conn' check and the return statement
    conn = @connection
    return conn if conn
    This is a subtle but important detail of double-checked locking in Ruby.

After Merge:

  1. Monitor production for any connection-related issues
  2. Verify that connection resets work as expected

🎓 Educational Value

This PR demonstrates best practices for Ruby concurrency:

  • Recognizing where race conditions exist
  • Choosing the right pattern for the use case
  • Writing tests that actually expose race conditions (the sleep(0.01) is perfect)
  • Explaining trade-offs in the PR description

Well done! 🎉


Final Verdict

LGTM ✅ - This is a high-quality fix for a real concurrency issue.

Merge confidence: High
Risk level: Low (well-tested, defensive implementation)


Minor Nitpick (Optional)

The test at spec/react_on_rails_pro/request_spec.rb:264 could add one more assertion:

it "properly closes old connection on reset" do
  # ... existing code ...
  
  # Optional: verify old connection is no longer accessible
  expect(described_class.send(:connection)).not_to eq(old_connection)
end

But this is covered implicitly by the eq(new_connection) assertion, so not necessary.


Great work, @AbanoubGhadban! This addresses issue #2258 thoroughly and professionally.

🤖 Review by Claude Code

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Dec 28, 2025

Code Review

Overall Assessment: High quality fix with excellent test coverage and documentation


Strengths

1. Correct Implementation of Double-Checked Locking

  • Constant mutex (CONNECTION_MUTEX) eliminates the meta-race
  • Fast path (line 94-96) provides lock-free reads for 99.99% of calls
  • Slow path (line 98-101) uses proper double-checked locking

2. Excellent Exception Safety

The reset_connection implementation creates new connection first, then atomically swaps, then cleans up old one.

3. Comprehensive Test Coverage

  • Concurrent initialization (only 1 connection created)
  • Safe reset during concurrent access
  • Proper cleanup and error propagation

4. Clear Documentation

PR description explains problem, solution, and trade-offs well.


Performance Review

Excellent characteristics:

  • Lock-free fast path (99.99% of calls)
  • Minimal contention
  • No connection waste (only 1 created vs N with AtomicReference)

Minor Suggestions (Optional)

  1. Consider adding Thread.pass in concurrency test to maximize contention
  2. Verify if Pro uses RBS type checking and add signature for CONNECTION_MUTEX
  3. Test when create_connection raises (nice-to-have)

Final Verdict

LGTM - Ready to merge after CI passes. Great work!

@AbanoubGhadban AbanoubGhadban merged commit b3615b2 into master Dec 29, 2025
17 checks passed
@AbanoubGhadban AbanoubGhadban deleted the fix-request-connection-race-condition branch December 29, 2025 18:28
AbanoubGhadban added a commit that referenced this pull request Mar 7, 2026
PR #2259 fixed thread-safe connection management, not GOAWAY
specifically. GOAWAY is normal HTTP/2 behavior handled by HTTPX's
built-in retries plugin — there was no dedicated GOAWAY bug.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix race condition in ReactOnRailsPro::Request connection management

2 participants