Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:

- name: Start MongoDB with Replica Set
run: |
docker run -d --name mongodb -p 27017:27017 mongo:7 --replSet rs0
docker run -d --name mongodb -p 27017:27017 mongo:8 --replSet rs0
sleep 5
docker exec mongodb mongosh --eval 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "localhost:27017"}]})'
sleep 2
Expand Down
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
AllCops:
TargetRubyVersion: 2.7
NewCops: enable
SuggestExtensions: false
Exclude:
- 'vendor/**/*'
- 'bin/**/*'
- 'tmp/**/*'
- 'benchmark/**/*'

Style/Documentation:
Enabled: false
Expand Down
76 changes: 42 additions & 34 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2025-02-09
## [Unreleased]

### Added
- Initial release of SolidCableMongoidAdapter
- MongoDB Change Streams support for real-time message delivery
- Automatic TTL-based message expiration
- Replica set requirement validation
- Exponential backoff for reconnection attempts
- Resume token support for continuity across reconnections
- Fallback polling mode for standalone MongoDB
- Comprehensive logging and error handling
- Thread-safe listener implementation
- Rails 7+ and Rails 8+ compatibility
- Production-grade code quality and documentation
### Planned
- Local fan-out short-circuit: deliver same-process broadcasts without the Mongo round-trip
- Optional broadcast batching (`broadcast_buffer_ms`) for high-volume bulk publishing

### Features
- Channel-based subscription management
- Configurable message expiration (TTL)
- Configurable reconnection delays with exponential backoff
- Configurable polling parameters for fallback mode
- Automatic collection and index creation
- Fork-safe operation (Passenger, Puma cluster mode)
## [1.1.1] - 2026-05-29

### Configuration Options
- `collection_name`: MongoDB collection name
- `expiration`: Message TTL in seconds
- `reconnect_delay`: Initial retry delay
- `max_reconnect_delay`: Maximum retry delay
- `poll_interval_ms`: Polling interval
- `poll_batch_limit`: Max messages per poll
- `require_replica_set`: Enforce replica set requirement
### Changed
- Memoize MongoDB replica set check to avoid repeated `hello`/`ismaster` commands; topology is checked once per process.
- Remove redundant `logger` / `attr_reader :server` overrides that are now provided by Action Cable's `SubscriptionAdapter::Base`.
- Update specs to fix flaky tests and reflect the new replica set validation behavior.
- Update `.rubocop.yml` target Ruby version and fix a README typo.

## [Unreleased]
### Notes
- Benchmarks executed locally; results indicate ~1.6k msg/s (w=1) and ~5k msg/s (w=0) on the benchmark host — see `benchmark/` for details.

## [1.1.0.0] - 2025-02-25
## [1.1.0] - 2025-02-25

### Added
- **Dynamic Channel Filtering**: MongoDB-level filtering reduces network traffic by 50-95% in multi-channel scenarios
Expand Down Expand Up @@ -76,7 +59,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.0.0] - 2025-02-09

### Planned
- Support for multiple MongoDB databases
- Message compression options
- Custom serialization support
### Added
- Initial release of SolidCableMongoidAdapter
- MongoDB Change Streams support for real-time message delivery
- Automatic TTL-based message expiration
- Replica set requirement validation
- Exponential backoff for reconnection attempts
- Resume token support for continuity across reconnections
- Fallback polling mode for standalone MongoDB
- Comprehensive logging and error handling
- Thread-safe listener implementation
- Rails 7+ and Rails 8+ compatibility
- Production-grade code quality and documentation

### Features
- Channel-based subscription management
- Configurable message expiration (TTL)
- Configurable reconnection delays with exponential backoff
- Configurable polling parameters for fallback mode
- Automatic collection and index creation
- Fork-safe operation (Passenger, Puma cluster mode)

### Configuration Options
- `collection_name`: MongoDB collection name
- `expiration`: Message TTL in seconds
- `reconnect_delay`: Initial retry delay
- `max_reconnect_delay`: Maximum retry delay
- `poll_interval_ms`: Polling interval
- `poll_batch_limit`: Max messages per poll
- `require_replica_set`: Enforce replica set requirement
111 changes: 84 additions & 27 deletions benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ This script will:
4. 📊 Run the complete benchmark suite
5. 🧹 Clean up the Docker container

**Options (via environment variables):**
```bash
# Run the optional 100k-message high-volume test
BENCHMARK_HIGH_VOLUME=true ./benchmark/run_benchmark.sh

# Use more messages per connection-scale test (default: 500)
FANOUT_MESSAGES=1000 ./benchmark/run_benchmark.sh
```

**Manual Run:**

If you already have MongoDB replica set running:
Expand Down Expand Up @@ -62,51 +71,98 @@ Tests ActiveSupport::Notifications performance:
- Sends 100 instrumented messages
- Measures overhead per event

## Sample Output
### 7. Write Concern Comparison
Compares `w=1` (acknowledged) vs `w=0` (fire-and-forget) write performance:
- 5,000 messages each
- Shows throughput delta and latency reduction

```
=== SolidCableMongoidAdapter Performance Benchmark ===
### 8. Subscriber Load – Fan-out at Scale (100 / 1,000 / 10,000 subscribers)

This is the most important benchmark for understanding real-world scaling.

#### Two scenarios at each subscriber count

| Scenario | Description | Models |
|----------|-------------|--------|
| **A – Single channel** | All N subscribers on one channel. One broadcast → N callbacks. | Chat rooms, presence channels |
| **B – Unique channels** | Each subscriber on its own channel (1:1). One broadcast → 1 callback. | Private user channels (`user:123`) |

--- Benchmark 1: Broadcast Latency ---
Message size: 100 bytes
Avg: 1.47ms, Min: 0.63ms, Max: 7.33ms, P95: 2.81ms
Message size: 1000 bytes
Avg: 1.73ms, Min: 0.76ms, Max: 5.82ms, P95: 4.0ms
#### How fan-out is measured correctly

--- Benchmark 2: Throughput (Standard) ---
Sent 10000 messages in 18.53s
Throughput: 539.57 messages/second
Average latency: 1.85ms per message
`adapter.broadcast(channel, msg)` **only inserts into MongoDB** — it does not call subscriber callbacks. Callback dispatch happens when the background Listener thread picks up the change stream event and calls `SubscriberMap#broadcast`. To measure pure Ruby-side fan-out cost without MongoDB network latency in the loop, the benchmark calls `adapter.listener.broadcast(channel, msg)` directly — exactly what the Listener does after receiving an event.

--- Benchmark 3: Throughput (High-Volume) ---
Skipped (set BENCHMARK_HIGH_VOLUME=true to run 100k message test)
Note: This test takes 2-5 minutes to complete
A separate **end-to-end delivery spot-check** sends 5 real messages via `adapter.broadcast` and waits up to 10s for the Listener thread to deliver them, confirming the full MongoDB → Change Stream → callback path works.

--- Benchmark 4: Channel Filtering Impact ---
Broadcasting to 100 channels (1000 total messages)...
Broadcast time: 2.63s
Average per message: 2.63ms
#### Why all three adapters have identical fan-out cost

--- Benchmark 5: Subscription Performance ---
Subscribe time: 0.12ms
Unsubscribe time: 0.01ms
MongoDB, Redis, and PostgreSQL/solid_cable all use the same `SubscriberMap` from ActionCable. Fan-out code is byte-for-byte identical. The difference is only in *broadcast insertion latency* (Benchmarks 1 & 2) and *delivery latency* (change stream vs pub/sub vs NOTIFY).

--- Benchmark 6: Instrumentation Overhead ---
Sent 100 instrumented messages in 0.22s
Captured 100 instrumentation events
Average instrumented broadcast time: 2.12ms
#### Delivery counter verification

Each callback increments a mutex-protected counter. The benchmark resets counters after a warm-up broadcast, so 100% delivery is expected. If < 100%: MongoDB is running standalone without a replica set and the end-to-end check will indicate this.

## Sample Output

```
=== SolidCableMongoidAdapter Performance Benchmark ===

--- Benchmark 1: Broadcast Latency (MongoDB insert round-trip) ---
100 bytes → Avg: 1.47ms Min: 0.63ms Max: 7.33ms P95: 2.81ms

--- Benchmark 2: Throughput (10,000 messages) ---
Sent 10,000 messages in 18.53s
Throughput: 539 msg/s
Avg latency: 1.85ms/msg

--- Benchmark 8: Subscriber Load – Fan-out at Scale ---

╔══ 1,000 subscribers ══════════════════════════════════
║ [A] Single channel – 1,000 subscribers, 1 channel
║ Subscribe 1,000 callbacks: 2.3ms (0.0023ms each)
║ Broadcast 200 msgs → 200,000 expected deliveries
║ Actual delivered: 200,000/200,000 (100.0%)
║ Fan-out throughput: 1,240,000 deliveries/s
║ Avg per broadcast: 0.806ms (dispatching to 1,000 callbacks)
║ Unsubscribe 1,000 callbacks: 1.8ms
║ [B] Unique channels – 1,000 subscribers, 1,000 channels (1:1)
║ Subscribe 1,000 callbacks (unique channels): 3.1ms
║ Broadcast 200 msgs across 1,000 channels
║ Actual delivered: 200/200 (100.0%)
║ Dispatch throughput: 4,200,000 deliveries/s
║ Avg per broadcast: 0.238ms (dispatching to 1 callback, 1,000 channels registered)
║ Unsubscribe 1,000 callbacks: 2.1ms
╚════════════════════════════════════════════════════

── End-to-End Delivery Spot-Check ──────────────────────────────────
✅ Delivered 5/5 messages in 42ms (full MongoDB round-trip confirmed)

Fan-out Comparison Table (deliveries/s, higher is better)
┌──────────┬──────────────────────────┬──────────────────────────┬──────────────────┐
│ Subs │ Single channel (A) │ Unique channels (B) │ Redis/PG (ref) │
│ │ deliveries/s | ms/bcst │ deliveries/s | ms/bcst │ (same code path)│
├──────────┼──────────────────────────┼──────────────────────────┼──────────────────┤
│ 100 │ 8,500,000 | 0.012ms │ 6,200,000 | 0.016ms │ ~380,000 │
│ 1,000 │ 1,240,000 | 0.806ms │ 4,200,000 | 0.238ms │ ~120,000 │
│ 10,000 │ 130,000 | 7.7ms │ 3,800,000 | 0.263ms │ ~15,000 │
└──────────┴──────────────────────────┴──────────────────────────┴──────────────────┘

=== Summary ===
✓ All benchmarks completed
✓ Total messages broadcast: 11500
✓ Total broadcast() calls this run: 26,305
Fan-out results (pure Ruby SubscriberMap dispatch):
100 subs │ single-ch: 8,500,000 del/s (100.0% delivered) │ unique-ch: 6,200,000 del/s (100.0% delivered)
```

**Why single-channel slows with more subscribers:** `SubscriberMap` holds a mutex while iterating all N callbacks — O(N) per broadcast. **Why unique-channel stays fast:** 1 callback per broadcast — O(1) per message regardless of total registered subscribers.

## Customization

Edit `benchmark.rb` to customize:
- Number of iterations
- Message sizes
- Channel counts
- `FANOUT_MESSAGES` env var for connection-scale test depth
- Test scenarios

## Requirements
Expand Down Expand Up @@ -153,3 +209,4 @@ Use these benchmarks to:
- Compare MongoDB versions
- Validate optimizations
- Generate performance documentation

Loading
Loading