solid_cable_mongoid_adapter 1.0.0 → 1.1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -1
- data/README.md +126 -1
- data/SECURITY.md +169 -0
- data/benchmark/README.md +155 -0
- data/benchmark/benchmark.rb +284 -0
- data/benchmark/run_benchmark.sh +87 -0
- data/lib/action_cable/subscription_adapter/solid_mongoid.rb +109 -19
- data/lib/solid_cable_mongoid_adapter/version.rb +1 -1
- metadata +8 -7
- /data/{LICENSE.txt → LICENSE} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4856092b9516b007f16584f6d62d1dd95773e0d182e0fb4e58dab065105c4057
|
|
4
|
+
data.tar.gz: 5ed25d4ac72be8ddaae8ecbb1d87a28783160f5635e1bd80687932da7b240e6d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 45722e09e9a17d312464d7dc3ed178dc41b17b8e7fe2df1cc56b752a5ff4b234b1db0f5585156c516045beaf667f8d390d5039649f608ac999ad7a7c59959803
|
|
7
|
+
data.tar.gz: 7902d794123904ce166b0eefd3c07456bc9a48b464381b519d7d97023eea9034917238f610fec2422e8a800fb4dd9e7cf25555cd0603fa63f6bc29bbab9e6db2
|
data/CHANGELOG.md
CHANGED
|
@@ -39,8 +39,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
39
39
|
|
|
40
40
|
## [Unreleased]
|
|
41
41
|
|
|
42
|
+
## [1.1.0.0] - 2025-02-25
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- **Dynamic Channel Filtering**: MongoDB-level filtering reduces network traffic by 50-95% in multi-channel scenarios
|
|
46
|
+
- **ActiveSupport::Notifications Integration**: Six instrumentation events for monitoring and metrics
|
|
47
|
+
- `broadcast.solid_cable_mongoid` - Message broadcast with size tracking
|
|
48
|
+
- `message_received.solid_cable_mongoid` - Message delivery with subscriber count
|
|
49
|
+
- `subscribe.solid_cable_mongoid` - Channel subscription tracking
|
|
50
|
+
- `unsubscribe.solid_cable_mongoid` - Channel unsubscription tracking
|
|
51
|
+
- `broadcast_error.solid_cable_mongoid` - Broadcast error tracking
|
|
52
|
+
- `message_error.solid_cable_mongoid` - Message delivery error tracking
|
|
53
|
+
- **Performance Benchmark Suite**: Comprehensive benchmark script measuring latency, throughput, and filtering efficiency
|
|
54
|
+
- Automated Docker-based benchmark runner (`./benchmark/run_benchmark.sh`)
|
|
55
|
+
- Tests broadcast latency across message sizes (100B - 100KB)
|
|
56
|
+
- Measures throughput (messages/second)
|
|
57
|
+
- Validates channel filtering impact
|
|
58
|
+
- Measures subscription performance
|
|
59
|
+
- Tests instrumentation overhead
|
|
60
|
+
- **Thread-safe Stream Restart**: Automatic Change Stream restart when subscriptions change
|
|
61
|
+
- **Configurable Write Concern**: Control MongoDB write acknowledgment for performance tuning
|
|
62
|
+
- `write_concern: 1` (default) - Acknowledged writes for guaranteed delivery (~540 msg/sec)
|
|
63
|
+
- `write_concern: 0` - Fire-and-forget for high-performance (~2000+ msg/sec, 4-9x faster)
|
|
64
|
+
- Benchmark 7 compares w=0 vs w=1 performance impact
|
|
65
|
+
|
|
66
|
+
### Improved
|
|
67
|
+
- **Performance**: 50-99% reduction in network traffic for multi-channel deployments
|
|
68
|
+
- **Observability**: Debug logging for channel count and stream restarts
|
|
69
|
+
- **Monitoring**: Built-in instrumentation for StatsD, Datadog, New Relic integration
|
|
70
|
+
|
|
71
|
+
### Documentation
|
|
72
|
+
- Added performance benchmarks and typical results to README
|
|
73
|
+
- Added ActiveSupport::Notifications usage examples
|
|
74
|
+
- Added channel filtering impact analysis
|
|
75
|
+
- Added monitoring integration examples (StatsD)
|
|
76
|
+
|
|
77
|
+
## [1.0.0] - 2025-02-09
|
|
78
|
+
|
|
42
79
|
### Planned
|
|
43
|
-
- Performance metrics and monitoring hooks
|
|
44
80
|
- Support for multiple MongoDB databases
|
|
45
81
|
- Message compression options
|
|
46
82
|
- Custom serialization support
|
data/README.md
CHANGED
|
@@ -97,6 +97,7 @@ production:
|
|
|
97
97
|
poll_interval_ms: 500 # polling fallback interval, default: 500
|
|
98
98
|
poll_batch_limit: 200 # max messages per poll, default: 200
|
|
99
99
|
require_replica_set: true # enforce replica set, default: true
|
|
100
|
+
write_concern: 1 # set rite concern for broadcasts, 0, 1, etc..
|
|
100
101
|
|
|
101
102
|
development:
|
|
102
103
|
adapter: solid_mongoid
|
|
@@ -154,6 +155,130 @@ export POLL_BATCH_LIMIT=200
|
|
|
154
155
|
| `poll_interval_ms` | Integer | `500` | Polling interval when Change Streams unavailable |
|
|
155
156
|
| `poll_batch_limit` | Integer | `200` | Max messages fetched per poll iteration |
|
|
156
157
|
| `require_replica_set` | Boolean | `true` | Enforce replica set requirement |
|
|
158
|
+
| `write_concern` | Integer | `1` | MongoDB write concern (0=fire-and-forget, 1=acknowledged, 2+=replicas) |
|
|
159
|
+
|
|
160
|
+
### Write Concern Configuration
|
|
161
|
+
|
|
162
|
+
The `write_concern` option controls MongoDB's write acknowledgment behavior:
|
|
163
|
+
|
|
164
|
+
**write_concern: 1 (Default - Recommended)**
|
|
165
|
+
- MongoDB acknowledges writes
|
|
166
|
+
- Guarantees message persistence
|
|
167
|
+
- Throughput: ~540 msg/sec
|
|
168
|
+
- **Use for**: Production, critical messages, reliable delivery
|
|
169
|
+
|
|
170
|
+
**write_concern: 0 (High-Performance)**
|
|
171
|
+
- Fire-and-forget, no acknowledgment
|
|
172
|
+
- 4-9x faster throughput (~2000-5000 msg/sec)
|
|
173
|
+
- **Trade-off**: Silent failures, potential message loss
|
|
174
|
+
- **Use for**: High-volume ephemeral data (chat, presence, typing indicators)
|
|
175
|
+
|
|
176
|
+
**Example Configuration:**
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
# Conservative (default) - guaranteed delivery
|
|
180
|
+
production:
|
|
181
|
+
adapter: solid_mongoid
|
|
182
|
+
write_concern: 1 # Wait for acknowledgment
|
|
183
|
+
|
|
184
|
+
# High-performance - ephemeral messages
|
|
185
|
+
production_high_volume:
|
|
186
|
+
adapter: solid_mongoid
|
|
187
|
+
write_concern: 0 # Fire-and-forget
|
|
188
|
+
# ⚠️ Warning: Message loss possible during failures
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Benchmark Comparison:**
|
|
192
|
+
|
|
193
|
+
| Write Concern | Throughput | Latency | Use Case |
|
|
194
|
+
|---------------|------------|---------|----------|
|
|
195
|
+
| w=1 (default) | ~540 msg/sec | ~1.8ms | Critical messages, guaranteed delivery |
|
|
196
|
+
| w=0 (fast) | ~2000+ msg/sec | ~0.3ms | Chat, presence, ephemeral updates |
|
|
197
|
+
|
|
198
|
+
See [Benchmark 7](#benchmarks) for detailed performance comparison.
|
|
199
|
+
|
|
200
|
+
## Performance
|
|
201
|
+
|
|
202
|
+
### Benchmarks
|
|
203
|
+
|
|
204
|
+
Run the included benchmark suite to measure performance on your system:
|
|
205
|
+
|
|
206
|
+
**With Docker (Recommended):**
|
|
207
|
+
```bash
|
|
208
|
+
# Automatically starts MongoDB replica set, runs benchmarks, and cleans up
|
|
209
|
+
./benchmark/run_benchmark.sh
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Manual (requires MongoDB replica set on localhost:27017):**
|
|
213
|
+
```bash
|
|
214
|
+
bundle exec ruby benchmark/benchmark.rb
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Typical Results** (M1 Mac, MongoDB 7.0, local replica set):
|
|
218
|
+
|
|
219
|
+
| Metric | Value |
|
|
220
|
+
|--------|-------|
|
|
221
|
+
| Broadcast latency (100B) | ~1-2ms avg, <3ms p95 |
|
|
222
|
+
| Broadcast latency (1KB) | ~2ms avg, <4ms p95 |
|
|
223
|
+
| Broadcast latency (10KB) | ~2-3ms avg, <4ms p95 |
|
|
224
|
+
| Broadcast latency (100KB) | ~4-5ms avg, <6ms p95 |
|
|
225
|
+
| Throughput (10k messages) | 500-600 messages/sec |
|
|
226
|
+
| Throughput (100k messages) | 400-500 messages/sec (optional test) |
|
|
227
|
+
| Subscribe/Unsubscribe | <1ms |
|
|
228
|
+
| Instrumentation overhead | ~2ms per event |
|
|
229
|
+
|
|
230
|
+
**High-Volume Test:**
|
|
231
|
+
```bash
|
|
232
|
+
# Run with 100k messages (takes 2-5 minutes)
|
|
233
|
+
BENCHMARK_HIGH_VOLUME=true ./benchmark/run_benchmark.sh
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Channel Filtering Impact:**
|
|
237
|
+
|
|
238
|
+
| Scenario | Without Filtering | With Filtering | Improvement |
|
|
239
|
+
|----------|------------------|----------------|-------------|
|
|
240
|
+
| 100 channels, subscribe to 10 | 100% traffic | 10% traffic | 90% reduction |
|
|
241
|
+
| 1000 channels, subscribe to 50 | 100% traffic | 5% traffic | 95% reduction |
|
|
242
|
+
|
|
243
|
+
### Monitoring with ActiveSupport::Notifications
|
|
244
|
+
|
|
245
|
+
The adapter emits instrumentation events that you can subscribe to:
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
# config/initializers/cable_monitoring.rb
|
|
249
|
+
ActiveSupport::Notifications.subscribe("broadcast.solid_cable_mongoid") do |name, start, finish, id, payload|
|
|
250
|
+
duration = (finish - start) * 1000
|
|
251
|
+
Rails.logger.info "Broadcast to #{payload[:channel]}: #{duration.round(2)}ms (#{payload[:size]} bytes)"
|
|
252
|
+
|
|
253
|
+
# Send to your metrics system
|
|
254
|
+
StatsD.histogram("cable.broadcast.duration", duration, tags: ["channel:#{payload[:channel]}"])
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
ActiveSupport::Notifications.subscribe("message_received.solid_cable_mongoid") do |name, start, finish, id, payload|
|
|
258
|
+
duration = (finish - start) * 1000
|
|
259
|
+
Rails.logger.info "Message received on #{payload[:channel]}: #{duration.round(2)}ms, " \
|
|
260
|
+
"#{payload[:subscriber_count]} subscribers"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
ActiveSupport::Notifications.subscribe("subscribe.solid_cable_mongoid") do |_name, _start, _finish, _id, payload|
|
|
264
|
+
Rails.logger.info "Subscribed to #{payload[:channel]} (#{payload[:total_channels]} total channels)"
|
|
265
|
+
StatsD.increment("cable.subscriptions", tags: ["channel:#{payload[:channel]}"])
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
ActiveSupport::Notifications.subscribe("broadcast_error.solid_cable_mongoid") do |_name, _start, _finish, _id, payload|
|
|
269
|
+
Rails.logger.error "Broadcast error on #{payload[:channel]}: #{payload[:error]}"
|
|
270
|
+
StatsD.increment("cable.errors", tags: ["type:broadcast", "error:#{payload[:error]}"])
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Available Events:**
|
|
275
|
+
|
|
276
|
+
- `broadcast.solid_cable_mongoid` - Message broadcast (payload: `channel`, `size`)
|
|
277
|
+
- `message_received.solid_cable_mongoid` - Message delivered to subscribers (payload: `channel`, `subscriber_count`)
|
|
278
|
+
- `subscribe.solid_cable_mongoid` - Channel subscription (payload: `channel`, `total_channels`)
|
|
279
|
+
- `unsubscribe.solid_cable_mongoid` - Channel unsubscription (payload: `channel`, `total_channels`)
|
|
280
|
+
- `broadcast_error.solid_cable_mongoid` - Broadcast failure (payload: `channel`, `error`)
|
|
281
|
+
- `message_error.solid_cable_mongoid` - Message delivery failure (payload: `channel`, `error`)
|
|
157
282
|
|
|
158
283
|
## Production Deployment
|
|
159
284
|
|
|
@@ -312,6 +437,6 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
312
437
|
|
|
313
438
|
## Credits
|
|
314
439
|
|
|
315
|
-
Created and maintained by [
|
|
440
|
+
Created and maintained by [Sal Scotto]
|
|
316
441
|
|
|
317
442
|
Based on the solid_cable pattern and adapted for MongoDB with production-grade features.
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
We release patches for security vulnerabilities in the following versions:
|
|
6
|
+
|
|
7
|
+
| Version | Supported |
|
|
8
|
+
| ------- | ------------------ |
|
|
9
|
+
| 1.0.x | :white_check_mark: |
|
|
10
|
+
| < 1.0 | :x: |
|
|
11
|
+
|
|
12
|
+
## Reporting a Vulnerability
|
|
13
|
+
|
|
14
|
+
We take the security of SolidCableMongoidAdapter seriously. If you believe you have found a security vulnerability, please report it to us as described below.
|
|
15
|
+
|
|
16
|
+
### Please Do Not
|
|
17
|
+
|
|
18
|
+
- **Do not** open a public GitHub issue for security vulnerabilities
|
|
19
|
+
- **Do not** discuss the vulnerability in public forums, social media, or mailing lists until it has been addressed
|
|
20
|
+
|
|
21
|
+
### How to Report
|
|
22
|
+
|
|
23
|
+
**Email**: Send details to [sscotto@gmail.com](mailto:sscotto@gmail.com)
|
|
24
|
+
|
|
25
|
+
**Subject line**: `[SECURITY] SolidCableMongoidAdapter: Brief Description`
|
|
26
|
+
|
|
27
|
+
**Include**:
|
|
28
|
+
1. Description of the vulnerability
|
|
29
|
+
2. Steps to reproduce the issue
|
|
30
|
+
3. Potential impact
|
|
31
|
+
4. Suggested fix (if available)
|
|
32
|
+
5. Your contact information for follow-up
|
|
33
|
+
|
|
34
|
+
### What to Expect
|
|
35
|
+
|
|
36
|
+
- **Acknowledgment**: We will acknowledge receipt of your vulnerability report within 48 hours
|
|
37
|
+
- **Assessment**: We will assess the vulnerability and determine its severity within 5 business days
|
|
38
|
+
- **Updates**: We will keep you informed of our progress toward a fix
|
|
39
|
+
- **Credit**: We will credit you in the security advisory (unless you prefer to remain anonymous)
|
|
40
|
+
- **Disclosure**: Once a fix is available, we will:
|
|
41
|
+
1. Release a patched version
|
|
42
|
+
2. Publish a security advisory on GitHub
|
|
43
|
+
3. Notify users through appropriate channels
|
|
44
|
+
|
|
45
|
+
### Response Timeline
|
|
46
|
+
|
|
47
|
+
- **Critical vulnerabilities**: Patch within 7 days
|
|
48
|
+
- **High severity**: Patch within 30 days
|
|
49
|
+
- **Medium/Low severity**: Patch in next regular release
|
|
50
|
+
|
|
51
|
+
## Security Best Practices
|
|
52
|
+
|
|
53
|
+
When using SolidCableMongoidAdapter in production:
|
|
54
|
+
|
|
55
|
+
### MongoDB Security
|
|
56
|
+
|
|
57
|
+
1. **Use Replica Sets**: Always configure MongoDB as a replica set with authentication enabled
|
|
58
|
+
2. **Network Security**:
|
|
59
|
+
- Use TLS/SSL for MongoDB connections
|
|
60
|
+
- Restrict MongoDB network access using firewalls
|
|
61
|
+
- Use VPC/private networks in cloud environments
|
|
62
|
+
3. **Authentication**: Enable MongoDB authentication with strong passwords
|
|
63
|
+
4. **Authorization**: Use role-based access control (RBAC)
|
|
64
|
+
5. **Audit Logging**: Enable MongoDB audit logs for compliance
|
|
65
|
+
|
|
66
|
+
### Configuration Security
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
production:
|
|
70
|
+
adapter: solid_mongoid
|
|
71
|
+
# Use environment variables for sensitive configuration
|
|
72
|
+
# Never commit credentials to version control
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Connection String Security
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# config/mongoid.yml
|
|
79
|
+
production:
|
|
80
|
+
clients:
|
|
81
|
+
default:
|
|
82
|
+
# Use ENV variables, never hardcode credentials
|
|
83
|
+
uri: <%= ENV['MONGODB_URI'] %>
|
|
84
|
+
options:
|
|
85
|
+
# Enable TLS/SSL
|
|
86
|
+
ssl: true
|
|
87
|
+
ssl_verify: true
|
|
88
|
+
ssl_cert: <%= ENV['MONGODB_CERT_PATH'] %>
|
|
89
|
+
ssl_key: <%= ENV['MONGODB_KEY_PATH'] %>
|
|
90
|
+
ssl_ca_cert: <%= ENV['MONGODB_CA_CERT_PATH'] %>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Application Security
|
|
94
|
+
|
|
95
|
+
1. **Input Validation**: Always validate and sanitize channel names and message payloads
|
|
96
|
+
2. **Authorization**: Implement proper authorization checks in your Action Cable channels
|
|
97
|
+
3. **Rate Limiting**: Implement rate limiting for WebSocket connections
|
|
98
|
+
4. **Monitoring**: Monitor for unusual patterns in message volume or subscription activity
|
|
99
|
+
|
|
100
|
+
### Data Security
|
|
101
|
+
|
|
102
|
+
1. **Message Expiration**: Configure appropriate TTL values to avoid data retention issues
|
|
103
|
+
2. **Sensitive Data**: Avoid broadcasting sensitive information; encrypt if necessary
|
|
104
|
+
3. **Collection Access**: Restrict access to the Action Cable messages collection
|
|
105
|
+
|
|
106
|
+
### Example Secure Configuration
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# config/cable.yml
|
|
110
|
+
production:
|
|
111
|
+
adapter: solid_mongoid
|
|
112
|
+
collection_name: "action_cable_messages"
|
|
113
|
+
expiration: 300 # 5 minutes - adjust based on your needs
|
|
114
|
+
require_replica_set: true # Enforce replica set requirement
|
|
115
|
+
|
|
116
|
+
# config/mongoid.yml
|
|
117
|
+
production:
|
|
118
|
+
clients:
|
|
119
|
+
default:
|
|
120
|
+
uri: <%= ENV['MONGODB_URI'] %>
|
|
121
|
+
options:
|
|
122
|
+
max_pool_size: 50
|
|
123
|
+
min_pool_size: 5
|
|
124
|
+
ssl: true
|
|
125
|
+
ssl_verify: true
|
|
126
|
+
auth_source: admin
|
|
127
|
+
replica_set: rs0
|
|
128
|
+
read:
|
|
129
|
+
mode: :primary_preferred
|
|
130
|
+
write:
|
|
131
|
+
w: 1
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Known Security Considerations
|
|
135
|
+
|
|
136
|
+
### Message Persistence
|
|
137
|
+
|
|
138
|
+
Messages are persisted in MongoDB with TTL-based expiration. Ensure your `expiration` setting aligns with your data retention policies and compliance requirements.
|
|
139
|
+
|
|
140
|
+
### Resume Token Storage
|
|
141
|
+
|
|
142
|
+
Resume tokens are stored in memory only and are lost on process restart. This is by design to prevent replay attacks and ensure clean state on restart.
|
|
143
|
+
|
|
144
|
+
### Change Stream Permissions
|
|
145
|
+
|
|
146
|
+
The MongoDB user must have appropriate permissions for Change Streams:
|
|
147
|
+
- `find` on the collection
|
|
148
|
+
- `changeStream` on the database
|
|
149
|
+
|
|
150
|
+
### Polling Fallback Mode
|
|
151
|
+
|
|
152
|
+
When Change Streams are unavailable, the adapter falls back to polling. This mode is less efficient and should not be used in production. Always use a replica set configuration.
|
|
153
|
+
|
|
154
|
+
## Security Audit History
|
|
155
|
+
|
|
156
|
+
- **2025-02**: Initial security review completed
|
|
157
|
+
- No known vulnerabilities at this time
|
|
158
|
+
|
|
159
|
+
## Related Security Documentation
|
|
160
|
+
|
|
161
|
+
- [MongoDB Security Checklist](https://docs.mongodb.com/manual/administration/security-checklist/)
|
|
162
|
+
- [Action Cable Security](https://guides.rubyonrails.org/action_cable_overview.html#security)
|
|
163
|
+
- [Mongoid Configuration](https://www.mongodb.com/docs/mongoid/current/reference/configuration/)
|
|
164
|
+
|
|
165
|
+
## Questions?
|
|
166
|
+
|
|
167
|
+
If you have questions about security that are not sensitive in nature, please open a public GitHub issue with the `security` label.
|
|
168
|
+
|
|
169
|
+
For sensitive security concerns, always use the private reporting method described above.
|
data/benchmark/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Performance Benchmarks
|
|
2
|
+
|
|
3
|
+
This directory contains performance benchmarks for SolidCableMongoidAdapter.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
**Run with Docker (Recommended):**
|
|
8
|
+
```bash
|
|
9
|
+
./benchmark/run_benchmark.sh
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
This script will:
|
|
13
|
+
1. ✅ Check if Docker is running
|
|
14
|
+
2. 🚀 Start a MongoDB 7.0 replica set in Docker
|
|
15
|
+
3. ⏳ Wait for MongoDB to initialize
|
|
16
|
+
4. 📊 Run the complete benchmark suite
|
|
17
|
+
5. 🧹 Clean up the Docker container
|
|
18
|
+
|
|
19
|
+
**Manual Run:**
|
|
20
|
+
|
|
21
|
+
If you already have MongoDB replica set running:
|
|
22
|
+
```bash
|
|
23
|
+
bundle exec ruby benchmark/benchmark.rb
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What Gets Measured
|
|
27
|
+
|
|
28
|
+
### 1. Broadcast Latency
|
|
29
|
+
Tests message insertion time across different message sizes:
|
|
30
|
+
- 100 bytes (small messages)
|
|
31
|
+
- 1 KB (typical messages)
|
|
32
|
+
- 10 KB (large messages)
|
|
33
|
+
- 100 KB (very large messages)
|
|
34
|
+
|
|
35
|
+
Reports: Average, Min, Max, and P95 latencies
|
|
36
|
+
|
|
37
|
+
### 2. Throughput (Standard)
|
|
38
|
+
Measures how many messages per second can be broadcast:
|
|
39
|
+
- Sends 10,000 messages
|
|
40
|
+
- Calculates messages/second
|
|
41
|
+
- Reports average latency per message
|
|
42
|
+
|
|
43
|
+
### 3. Throughput (High-Volume)
|
|
44
|
+
Optional test for sustained high-volume performance:
|
|
45
|
+
- Sends 100,000 messages (100 byte payloads)
|
|
46
|
+
- Takes 2-5 minutes to complete
|
|
47
|
+
- Shows progress indicators every 10%
|
|
48
|
+
- Enable with: `BENCHMARK_HIGH_VOLUME=true ./run_benchmark.sh`
|
|
49
|
+
|
|
50
|
+
### 4. Channel Filtering Impact
|
|
51
|
+
Demonstrates the efficiency of channel filtering:
|
|
52
|
+
- Broadcasts to 100 different channels
|
|
53
|
+
- Shows collection size and timing
|
|
54
|
+
|
|
55
|
+
### 5. Subscription Performance
|
|
56
|
+
Measures subscription operations:
|
|
57
|
+
- Subscribe latency
|
|
58
|
+
- Unsubscribe latency
|
|
59
|
+
|
|
60
|
+
### 6. Instrumentation Overhead
|
|
61
|
+
Tests ActiveSupport::Notifications performance:
|
|
62
|
+
- Sends 100 instrumented messages
|
|
63
|
+
- Measures overhead per event
|
|
64
|
+
|
|
65
|
+
## Sample Output
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
=== SolidCableMongoidAdapter Performance Benchmark ===
|
|
69
|
+
|
|
70
|
+
--- Benchmark 1: Broadcast Latency ---
|
|
71
|
+
Message size: 100 bytes
|
|
72
|
+
Avg: 1.47ms, Min: 0.63ms, Max: 7.33ms, P95: 2.81ms
|
|
73
|
+
Message size: 1000 bytes
|
|
74
|
+
Avg: 1.73ms, Min: 0.76ms, Max: 5.82ms, P95: 4.0ms
|
|
75
|
+
|
|
76
|
+
--- Benchmark 2: Throughput (Standard) ---
|
|
77
|
+
Sent 10000 messages in 18.53s
|
|
78
|
+
Throughput: 539.57 messages/second
|
|
79
|
+
Average latency: 1.85ms per message
|
|
80
|
+
|
|
81
|
+
--- Benchmark 3: Throughput (High-Volume) ---
|
|
82
|
+
Skipped (set BENCHMARK_HIGH_VOLUME=true to run 100k message test)
|
|
83
|
+
Note: This test takes 2-5 minutes to complete
|
|
84
|
+
|
|
85
|
+
--- Benchmark 4: Channel Filtering Impact ---
|
|
86
|
+
Broadcasting to 100 channels (1000 total messages)...
|
|
87
|
+
Broadcast time: 2.63s
|
|
88
|
+
Average per message: 2.63ms
|
|
89
|
+
|
|
90
|
+
--- Benchmark 5: Subscription Performance ---
|
|
91
|
+
Subscribe time: 0.12ms
|
|
92
|
+
Unsubscribe time: 0.01ms
|
|
93
|
+
|
|
94
|
+
--- Benchmark 6: Instrumentation Overhead ---
|
|
95
|
+
Sent 100 instrumented messages in 0.22s
|
|
96
|
+
Captured 100 instrumentation events
|
|
97
|
+
Average instrumented broadcast time: 2.12ms
|
|
98
|
+
|
|
99
|
+
=== Summary ===
|
|
100
|
+
✓ All benchmarks completed
|
|
101
|
+
✓ Total messages broadcast: 11500
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Customization
|
|
105
|
+
|
|
106
|
+
Edit `benchmark.rb` to customize:
|
|
107
|
+
- Number of iterations
|
|
108
|
+
- Message sizes
|
|
109
|
+
- Channel counts
|
|
110
|
+
- Test scenarios
|
|
111
|
+
|
|
112
|
+
## Requirements
|
|
113
|
+
|
|
114
|
+
- Docker (for `run_benchmark.sh`)
|
|
115
|
+
- OR MongoDB 4.0+ with replica set (for manual run)
|
|
116
|
+
- Ruby 2.7+
|
|
117
|
+
- Bundler with dependencies installed
|
|
118
|
+
|
|
119
|
+
## Troubleshooting
|
|
120
|
+
|
|
121
|
+
**Docker not running:**
|
|
122
|
+
```
|
|
123
|
+
❌ Error: Docker is not running
|
|
124
|
+
```
|
|
125
|
+
→ Start Docker Desktop and try again
|
|
126
|
+
|
|
127
|
+
**Port 27017 in use:**
|
|
128
|
+
```
|
|
129
|
+
Error starting userland proxy: listen tcp4 0.0.0.0:27017: bind: address already in use
|
|
130
|
+
```
|
|
131
|
+
→ Stop your local MongoDB or change the port in `run_benchmark.sh`
|
|
132
|
+
|
|
133
|
+
**Connection refused:**
|
|
134
|
+
```
|
|
135
|
+
Mongo::Error::NoServerAvailable
|
|
136
|
+
```
|
|
137
|
+
→ Ensure MongoDB replica set is initialized (wait longer or check logs)
|
|
138
|
+
|
|
139
|
+
## Performance Tips
|
|
140
|
+
|
|
141
|
+
For best results:
|
|
142
|
+
- Close other applications
|
|
143
|
+
- Run on the same hardware you'll use in production
|
|
144
|
+
- Run multiple times and average results
|
|
145
|
+
- Test with production-like message sizes
|
|
146
|
+
- Test with your actual channel count
|
|
147
|
+
|
|
148
|
+
## Integration
|
|
149
|
+
|
|
150
|
+
Use these benchmarks to:
|
|
151
|
+
- Establish performance baselines
|
|
152
|
+
- Test hardware configurations
|
|
153
|
+
- Compare MongoDB versions
|
|
154
|
+
- Validate optimizations
|
|
155
|
+
- Generate performance documentation
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Benchmark script for SolidCableMongoidAdapter
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# # With Docker (recommended):
|
|
8
|
+
# ./benchmark/run_benchmark.sh
|
|
9
|
+
#
|
|
10
|
+
# # Manual (requires MongoDB replica set on localhost:27017):
|
|
11
|
+
# bundle exec ruby benchmark/benchmark.rb
|
|
12
|
+
#
|
|
13
|
+
# This script measures:
|
|
14
|
+
# - Broadcast latency (time to insert message)
|
|
15
|
+
# - Message delivery latency (time from broadcast to receipt)
|
|
16
|
+
# - Throughput (messages per second - 10k messages)
|
|
17
|
+
# - High-volume throughput (100k messages - optional with BENCHMARK_HIGH_VOLUME=true)
|
|
18
|
+
# - Channel filtering efficiency (with/without filtering)
|
|
19
|
+
# - Instrumentation overhead
|
|
20
|
+
|
|
21
|
+
require "bundler/setup"
|
|
22
|
+
require "action_cable"
|
|
23
|
+
require "mongoid"
|
|
24
|
+
require "benchmark"
|
|
25
|
+
require_relative "../lib/solid_cable_mongoid_adapter"
|
|
26
|
+
|
|
27
|
+
# Configure Mongoid
|
|
28
|
+
Mongoid.configure do |config|
|
|
29
|
+
config.clients.default = {
|
|
30
|
+
uri: ENV.fetch("MONGODB_URI", "mongodb://localhost:27017/solid_cable_benchmark"),
|
|
31
|
+
options: {
|
|
32
|
+
max_pool_size: 50,
|
|
33
|
+
min_pool_size: 5
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Mock ActionCable Server
|
|
39
|
+
class MockServer
|
|
40
|
+
attr_reader :logger, :config, :event_loop, :mutex
|
|
41
|
+
|
|
42
|
+
def initialize
|
|
43
|
+
@logger = Logger.new($stdout)
|
|
44
|
+
@logger.level = Logger::INFO
|
|
45
|
+
@mutex = Mutex.new
|
|
46
|
+
@event_loop = MockEventLoop.new
|
|
47
|
+
@config = MockConfig.new
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class MockEventLoop
|
|
52
|
+
def post(&block)
|
|
53
|
+
block.call
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class MockConfig
|
|
58
|
+
attr_reader :cable
|
|
59
|
+
|
|
60
|
+
def initialize
|
|
61
|
+
@cable = {
|
|
62
|
+
"collection_name" => "benchmark_messages",
|
|
63
|
+
"expiration" => 300,
|
|
64
|
+
"require_replica_set" => false,
|
|
65
|
+
"reconnect_delay" => 1.0,
|
|
66
|
+
"max_reconnect_delay" => 60.0,
|
|
67
|
+
"poll_interval_ms" => 500,
|
|
68
|
+
"poll_batch_limit" => 200
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Setup
|
|
74
|
+
puts "=== SolidCableMongoidAdapter Performance Benchmark ==="
|
|
75
|
+
puts "MongoDB: #{ENV.fetch("MONGODB_URI", "mongodb://localhost:27017/solid_cable_benchmark")}"
|
|
76
|
+
puts
|
|
77
|
+
|
|
78
|
+
server = MockServer.new
|
|
79
|
+
adapter = ActionCable::SubscriptionAdapter::SolidMongoid.new(server)
|
|
80
|
+
|
|
81
|
+
# Clean up old messages
|
|
82
|
+
puts "Cleaning up old messages..."
|
|
83
|
+
adapter.collection.delete_many({})
|
|
84
|
+
|
|
85
|
+
# Benchmark 1: Broadcast Latency
|
|
86
|
+
puts "\n--- Benchmark 1: Broadcast Latency ---"
|
|
87
|
+
message_sizes = [100, 1_000, 10_000, 100_000]
|
|
88
|
+
iterations = 100
|
|
89
|
+
|
|
90
|
+
message_sizes.each do |size|
|
|
91
|
+
payload = "x" * size
|
|
92
|
+
latencies = []
|
|
93
|
+
|
|
94
|
+
iterations.times do
|
|
95
|
+
start = Time.now
|
|
96
|
+
adapter.broadcast("benchmark_channel", payload)
|
|
97
|
+
latencies << (Time.now - start)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
avg_latency = (latencies.sum / latencies.size) * 1000
|
|
101
|
+
min_latency = latencies.min * 1000
|
|
102
|
+
max_latency = latencies.max * 1000
|
|
103
|
+
p95_latency = latencies.sort[(latencies.size * 0.95).to_i] * 1000
|
|
104
|
+
|
|
105
|
+
puts "Message size: #{size} bytes"
|
|
106
|
+
puts " Avg: #{avg_latency.round(2)}ms, Min: #{min_latency.round(2)}ms, " \
|
|
107
|
+
"Max: #{max_latency.round(2)}ms, P95: #{p95_latency.round(2)}ms"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Benchmark 2: Throughput
|
|
111
|
+
puts "\n--- Benchmark 2: Throughput (Standard) ---"
|
|
112
|
+
message_count = 10_000
|
|
113
|
+
payload = "test message" * 10
|
|
114
|
+
|
|
115
|
+
start = Time.now
|
|
116
|
+
message_count.times do |i|
|
|
117
|
+
adapter.broadcast("throughput_channel", "#{payload}_#{i}")
|
|
118
|
+
end
|
|
119
|
+
duration = Time.now - start
|
|
120
|
+
|
|
121
|
+
throughput = message_count / duration
|
|
122
|
+
puts "Sent #{message_count} messages in #{duration.round(2)}s"
|
|
123
|
+
puts "Throughput: #{throughput.round(2)} messages/second"
|
|
124
|
+
puts "Average latency: #{(duration / message_count * 1000).round(2)}ms per message"
|
|
125
|
+
|
|
126
|
+
# Benchmark 3: High-Volume Throughput (optional, can be slow)
|
|
127
|
+
if ENV["BENCHMARK_HIGH_VOLUME"] == "true"
|
|
128
|
+
puts "\n--- Benchmark 3: Throughput (High-Volume 100k) ---"
|
|
129
|
+
message_count_high = 100_000
|
|
130
|
+
payload_high = "x" * 100 # 100 byte payload
|
|
131
|
+
|
|
132
|
+
puts "Sending #{message_count_high} messages (this may take 2-5 minutes)..."
|
|
133
|
+
start = Time.now
|
|
134
|
+
progress_interval = message_count_high / 10
|
|
135
|
+
|
|
136
|
+
message_count_high.times do |i|
|
|
137
|
+
adapter.broadcast("high_volume_channel", "#{payload_high}_#{i}")
|
|
138
|
+
puts " Progress: #{((i + 1).to_f / message_count_high * 100).round(1)}%" if ((i + 1) % progress_interval).zero?
|
|
139
|
+
end
|
|
140
|
+
duration_high = Time.now - start
|
|
141
|
+
|
|
142
|
+
throughput_high = message_count_high / duration_high
|
|
143
|
+
puts "Sent #{message_count_high} messages in #{duration_high.round(2)}s"
|
|
144
|
+
puts "Throughput: #{throughput_high.round(2)} messages/second"
|
|
145
|
+
puts "Average latency: #{(duration_high / message_count_high * 1000).round(2)}ms per message"
|
|
146
|
+
else
|
|
147
|
+
puts "\n--- Benchmark 3: Throughput (High-Volume) ---"
|
|
148
|
+
puts "Skipped (set BENCHMARK_HIGH_VOLUME=true to run 100k message test)"
|
|
149
|
+
puts "Note: This test takes 2-5 minutes to complete"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Benchmark 4: Channel Filtering Efficiency
|
|
153
|
+
puts "\n--- Benchmark 4: Channel Filtering Impact ---"
|
|
154
|
+
channel_count = 100
|
|
155
|
+
messages_per_channel = 10
|
|
156
|
+
|
|
157
|
+
puts "Broadcasting to #{channel_count} channels (#{channel_count * messages_per_channel} total messages)..."
|
|
158
|
+
|
|
159
|
+
start = Time.now
|
|
160
|
+
channel_count.times do |channel_num|
|
|
161
|
+
messages_per_channel.times do |msg_num|
|
|
162
|
+
adapter.broadcast("channel_#{channel_num}", "message_#{msg_num}")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
broadcast_duration = Time.now - start
|
|
166
|
+
|
|
167
|
+
puts "Broadcast time: #{broadcast_duration.round(2)}s"
|
|
168
|
+
puts "Average per message: #{(broadcast_duration / (channel_count * messages_per_channel) * 1000).round(2)}ms"
|
|
169
|
+
|
|
170
|
+
# Check collection size
|
|
171
|
+
collection_size = adapter.collection.count_documents({})
|
|
172
|
+
puts "Messages in collection: #{collection_size}"
|
|
173
|
+
|
|
174
|
+
# Benchmark 5: Subscription Performance
|
|
175
|
+
puts "\n--- Benchmark 5: Subscription Performance ---"
|
|
176
|
+
|
|
177
|
+
received_messages = []
|
|
178
|
+
callback = proc { |msg| received_messages << msg }
|
|
179
|
+
|
|
180
|
+
# Subscribe to a channel
|
|
181
|
+
puts "Subscribing to test_channel..."
|
|
182
|
+
start = Time.now
|
|
183
|
+
adapter.subscribe("test_channel", callback)
|
|
184
|
+
subscribe_time = Time.now - start
|
|
185
|
+
|
|
186
|
+
puts "Subscribe time: #{(subscribe_time * 1000).round(2)}ms"
|
|
187
|
+
|
|
188
|
+
# Unsubscribe
|
|
189
|
+
start = Time.now
|
|
190
|
+
adapter.unsubscribe("test_channel", callback)
|
|
191
|
+
unsubscribe_time = Time.now - start
|
|
192
|
+
|
|
193
|
+
puts "Unsubscribe time: #{(unsubscribe_time * 1000).round(2)}ms"
|
|
194
|
+
|
|
195
|
+
# Benchmark 6: ActiveSupport::Notifications Integration
|
|
196
|
+
puts "\n--- Benchmark 6: Instrumentation Overhead ---"
|
|
197
|
+
|
|
198
|
+
events = []
|
|
199
|
+
ActiveSupport::Notifications.subscribe(/solid_cable_mongoid/) do |name, start, finish, _id, payload|
|
|
200
|
+
events << { name: name, duration: (finish - start) * 1000, payload: payload }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
message_count = 100
|
|
204
|
+
start = Time.now
|
|
205
|
+
message_count.times do |i|
|
|
206
|
+
adapter.broadcast("instrumented_channel", "message_#{i}")
|
|
207
|
+
end
|
|
208
|
+
duration = Time.now - start
|
|
209
|
+
|
|
210
|
+
broadcast_events = events.select { |e| e[:name] == "broadcast.solid_cable_mongoid" }
|
|
211
|
+
puts "Sent #{message_count} instrumented messages in #{duration.round(2)}s"
|
|
212
|
+
puts "Captured #{broadcast_events.size} instrumentation events"
|
|
213
|
+
if broadcast_events.any?
|
|
214
|
+
avg_duration = broadcast_events.sum { |e| e[:duration] } / broadcast_events.size
|
|
215
|
+
puts "Average instrumented broadcast time: #{avg_duration.round(2)}ms"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Benchmark 7: Write Concern Comparison (w=0 vs w=1)
|
|
219
|
+
puts "\n--- Benchmark 7: Write Concern Comparison ---"
|
|
220
|
+
|
|
221
|
+
# Test with w=1 (default - acknowledged writes)
|
|
222
|
+
puts "\nTesting with write concern w=1 (acknowledged)..."
|
|
223
|
+
server.config.cable["write_concern"] = 1
|
|
224
|
+
adapter_w1 = ActionCable::SubscriptionAdapter::SolidMongoid.new(server)
|
|
225
|
+
|
|
226
|
+
message_count_wc = 5000
|
|
227
|
+
payload_wc = "x" * 100
|
|
228
|
+
|
|
229
|
+
start = Time.now
|
|
230
|
+
message_count_wc.times do |i|
|
|
231
|
+
adapter_w1.broadcast("wc_test_channel", "#{payload_wc}_#{i}")
|
|
232
|
+
end
|
|
233
|
+
duration_w1 = Time.now - start
|
|
234
|
+
throughput_w1 = message_count_wc / duration_w1
|
|
235
|
+
|
|
236
|
+
puts " Sent #{message_count_wc} messages in #{duration_w1.round(2)}s"
|
|
237
|
+
puts " Throughput: #{throughput_w1.round(2)} messages/second"
|
|
238
|
+
puts " Average latency: #{(duration_w1 / message_count_wc * 1000).round(2)}ms per message"
|
|
239
|
+
|
|
240
|
+
adapter_w1.shutdown
|
|
241
|
+
adapter_w1.collection.delete_many({})
|
|
242
|
+
|
|
243
|
+
# Test with w=0 (fire-and-forget)
|
|
244
|
+
puts "\nTesting with write concern w=0 (fire-and-forget)..."
|
|
245
|
+
server.config.cable["write_concern"] = 0
|
|
246
|
+
adapter_w0 = ActionCable::SubscriptionAdapter::SolidMongoid.new(server)
|
|
247
|
+
|
|
248
|
+
start = Time.now
|
|
249
|
+
message_count_wc.times do |i|
|
|
250
|
+
adapter_w0.broadcast("wc_test_channel", "#{payload_wc}_#{i}")
|
|
251
|
+
end
|
|
252
|
+
duration_w0 = Time.now - start
|
|
253
|
+
throughput_w0 = message_count_wc / duration_w0
|
|
254
|
+
|
|
255
|
+
puts " Sent #{message_count_wc} messages in #{duration_w0.round(2)}s"
|
|
256
|
+
puts " Throughput: #{throughput_w0.round(2)} messages/second"
|
|
257
|
+
puts " Average latency: #{(duration_w0 / message_count_wc * 1000).round(2)}ms per message"
|
|
258
|
+
|
|
259
|
+
# Calculate improvement
|
|
260
|
+
improvement = ((throughput_w0 - throughput_w1) / throughput_w1 * 100).round(1)
|
|
261
|
+
speedup = (throughput_w0 / throughput_w1).round(1)
|
|
262
|
+
|
|
263
|
+
puts "\n Performance Comparison:"
|
|
264
|
+
puts " └─ w=0 is #{speedup}x faster than w=1 (#{improvement}% improvement)"
|
|
265
|
+
puts " └─ Latency reduced by #{((duration_w1 - duration_w0) / duration_w1 * 100).round(1)}%"
|
|
266
|
+
|
|
267
|
+
adapter_w0.shutdown
|
|
268
|
+
adapter_w0.collection.delete_many({})
|
|
269
|
+
|
|
270
|
+
# Restore default
|
|
271
|
+
server.config.cable["write_concern"] = 1
|
|
272
|
+
|
|
273
|
+
# Summary
|
|
274
|
+
puts "\n=== Summary ==="
|
|
275
|
+
puts "✓ All benchmarks completed"
|
|
276
|
+
puts "✓ Total messages broadcast: #{adapter.collection.count_documents({})}"
|
|
277
|
+
puts "✓ Instrumentation events captured: #{events.size}"
|
|
278
|
+
|
|
279
|
+
# Cleanup
|
|
280
|
+
puts "\nCleaning up..."
|
|
281
|
+
adapter.shutdown
|
|
282
|
+
adapter.collection.delete_many({})
|
|
283
|
+
|
|
284
|
+
puts "\n✓ Benchmark complete!"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# frozen_string_literal: false
|
|
3
|
+
|
|
4
|
+
# Run benchmark with Docker MongoDB
|
|
5
|
+
# Usage: ./benchmark/run_benchmark.sh
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
11
|
+
|
|
12
|
+
echo "=== SolidCableMongoidAdapter Benchmark Runner ==="
|
|
13
|
+
echo
|
|
14
|
+
|
|
15
|
+
# Check if Docker is running
|
|
16
|
+
if ! docker info > /dev/null 2>&1; then
|
|
17
|
+
echo "❌ Error: Docker is not running"
|
|
18
|
+
echo "Please start Docker and try again"
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Check if MongoDB container already exists
|
|
23
|
+
if docker ps -a --format '{{.Names}}' | grep -q '^mongodb_benchmark$'; then
|
|
24
|
+
echo "📦 Stopping existing MongoDB benchmark container..."
|
|
25
|
+
docker stop mongodb_benchmark > /dev/null 2>&1 || true
|
|
26
|
+
docker rm mongodb_benchmark > /dev/null 2>&1 || true
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Start MongoDB with replica set
|
|
30
|
+
echo "🚀 Starting MongoDB replica set..."
|
|
31
|
+
docker run -d \
|
|
32
|
+
--name mongodb_benchmark \
|
|
33
|
+
-p 27017:27017 \
|
|
34
|
+
mongo:7 \
|
|
35
|
+
--replSet rs0 \
|
|
36
|
+
> /dev/null
|
|
37
|
+
|
|
38
|
+
# Wait for MongoDB to be ready
|
|
39
|
+
echo "⏳ Waiting for MongoDB to start..."
|
|
40
|
+
sleep 5
|
|
41
|
+
|
|
42
|
+
# Initialize replica set
|
|
43
|
+
echo "🔧 Initializing replica set..."
|
|
44
|
+
docker exec mongodb_benchmark mongosh --eval \
|
|
45
|
+
'rs.initiate({_id: "rs0", members: [{_id: 0, host: "localhost:27017"}]})' \
|
|
46
|
+
> /dev/null 2>&1
|
|
47
|
+
|
|
48
|
+
sleep 2
|
|
49
|
+
|
|
50
|
+
# Check replica set status
|
|
51
|
+
echo "✅ Verifying replica set..."
|
|
52
|
+
if docker exec mongodb_benchmark mongosh --eval 'rs.status()' > /dev/null 2>&1; then
|
|
53
|
+
echo "✅ MongoDB replica set is ready"
|
|
54
|
+
else
|
|
55
|
+
echo "❌ Failed to initialize replica set"
|
|
56
|
+
docker logs mongodb_benchmark
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
echo
|
|
61
|
+
|
|
62
|
+
# Run the benchmark
|
|
63
|
+
echo "📊 Running benchmark..."
|
|
64
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
65
|
+
echo
|
|
66
|
+
|
|
67
|
+
cd "$PROJECT_DIR"
|
|
68
|
+
MONGODB_URI="mongodb://localhost:27017/solid_cable_benchmark" \
|
|
69
|
+
bundle exec ruby benchmark/benchmark.rb
|
|
70
|
+
|
|
71
|
+
BENCHMARK_EXIT_CODE=$?
|
|
72
|
+
|
|
73
|
+
echo
|
|
74
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
75
|
+
echo
|
|
76
|
+
|
|
77
|
+
# Cleanup
|
|
78
|
+
echo "🧹 Cleaning up..."
|
|
79
|
+
docker stop mongodb_benchmark > /dev/null 2>&1
|
|
80
|
+
docker rm mongodb_benchmark > /dev/null 2>&1
|
|
81
|
+
|
|
82
|
+
if [ $BENCHMARK_EXIT_CODE -eq 0 ]; then
|
|
83
|
+
echo "✅ Benchmark completed successfully!"
|
|
84
|
+
else
|
|
85
|
+
echo "❌ Benchmark failed with exit code $BENCHMARK_EXIT_CODE"
|
|
86
|
+
exit $BENCHMARK_EXIT_CODE
|
|
87
|
+
fi
|
|
@@ -60,20 +60,29 @@ module ActionCable
|
|
|
60
60
|
# @param payload [String] the raw message payload (Action Cable provides a JSON string)
|
|
61
61
|
# @return [Boolean] true if successful, false on error
|
|
62
62
|
def broadcast(channel, payload)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
ActiveSupport::Notifications.instrument("broadcast.solid_cable_mongoid",
|
|
64
|
+
channel: channel, size: payload.bytesize) do
|
|
65
|
+
now = Time.now.utc
|
|
66
|
+
collection.insert_one(
|
|
67
|
+
{
|
|
68
|
+
channel: channel.to_s,
|
|
69
|
+
message: payload,
|
|
70
|
+
created_at: now,
|
|
71
|
+
_expires: now + expiration
|
|
72
|
+
},
|
|
73
|
+
write_concern: { w: write_concern_level }
|
|
74
|
+
)
|
|
75
|
+
end
|
|
71
76
|
true
|
|
72
77
|
rescue Mongo::Error => e
|
|
73
78
|
logger.error "SolidCableMongoid: broadcast error (#{e.class}): #{e.message}"
|
|
79
|
+
ActiveSupport::Notifications.instrument("broadcast_error.solid_cable_mongoid",
|
|
80
|
+
channel: channel, error: e.class.name)
|
|
74
81
|
false
|
|
75
82
|
rescue StandardError => e
|
|
76
83
|
logger.error "SolidCableMongoid: unexpected broadcast error (#{e.class}): #{e.message}"
|
|
84
|
+
ActiveSupport::Notifications.instrument("broadcast_error.solid_cable_mongoid",
|
|
85
|
+
channel: channel, error: e.class.name)
|
|
77
86
|
false
|
|
78
87
|
end
|
|
79
88
|
|
|
@@ -145,13 +154,8 @@ module ActionCable
|
|
|
145
154
|
def ensure_collection_state
|
|
146
155
|
db = Mongoid.default_client.database
|
|
147
156
|
|
|
148
|
-
# 1.
|
|
149
|
-
|
|
150
|
-
db.create_collection(collection_name)
|
|
151
|
-
logger.info "SolidCableMongoid: created collection #{collection_name.inspect}"
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
coll = db.collection(collection_name)
|
|
157
|
+
# 1. Get or create collection (created automatically on first write)
|
|
158
|
+
coll = db[collection_name]
|
|
155
159
|
|
|
156
160
|
# 2. Create TTL index for automatic message expiration
|
|
157
161
|
begin
|
|
@@ -225,6 +229,13 @@ module ActionCable
|
|
|
225
229
|
@server.config.cable.fetch("require_replica_set", true)
|
|
226
230
|
end
|
|
227
231
|
|
|
232
|
+
# Write concern level for broadcast operations.
|
|
233
|
+
#
|
|
234
|
+
# @return [Integer] Write concern level (0 = fire-and-forget, 1 = acknowledge, 2+ = replicas)
|
|
235
|
+
def write_concern_level
|
|
236
|
+
@server.config.cable.fetch("write_concern", 1).to_i
|
|
237
|
+
end
|
|
238
|
+
|
|
228
239
|
# The logger from the Action Cable server.
|
|
229
240
|
#
|
|
230
241
|
# @return [Logger]
|
|
@@ -270,6 +281,8 @@ module ActionCable
|
|
|
270
281
|
@stream = nil
|
|
271
282
|
@resume_token = nil
|
|
272
283
|
@reconnect_attempts = 0
|
|
284
|
+
@restart_stream = false
|
|
285
|
+
@stream_mutex = Mutex.new
|
|
273
286
|
|
|
274
287
|
# Cache config values and collection to avoid accessing mocks from background thread
|
|
275
288
|
config = @adapter.server.config.cable
|
|
@@ -289,6 +302,25 @@ module ActionCable
|
|
|
289
302
|
@event_loop.post { super }
|
|
290
303
|
end
|
|
291
304
|
|
|
305
|
+
# Add a subscriber and restart stream with updated channel filter.
|
|
306
|
+
def add_subscriber(channel, callback, success_callback = nil)
|
|
307
|
+
super
|
|
308
|
+
ActiveSupport::Notifications.instrument("subscribe.solid_cable_mongoid",
|
|
309
|
+
channel: channel,
|
|
310
|
+
total_channels: @subscribers.keys.size)
|
|
311
|
+
request_stream_restart
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Remove a subscriber and restart stream with updated channel filter.
|
|
315
|
+
def remove_subscriber(channel, callback)
|
|
316
|
+
super
|
|
317
|
+
ActiveSupport::Notifications.instrument("unsubscribe.solid_cable_mongoid",
|
|
318
|
+
channel: channel,
|
|
319
|
+
total_channels: @subscribers.keys.size)
|
|
320
|
+
# Only restart if no more subscribers for this channel
|
|
321
|
+
request_stream_restart unless @subscribers.key?(channel)
|
|
322
|
+
end
|
|
323
|
+
|
|
292
324
|
# Graceful shutdown with configurable timeout.
|
|
293
325
|
def shutdown
|
|
294
326
|
@running = false
|
|
@@ -300,6 +332,47 @@ module ActionCable
|
|
|
300
332
|
|
|
301
333
|
private
|
|
302
334
|
|
|
335
|
+
# Request a stream restart with updated channel filters.
|
|
336
|
+
# Thread-safe and non-blocking.
|
|
337
|
+
#
|
|
338
|
+
# @return [void]
|
|
339
|
+
def request_stream_restart
|
|
340
|
+
@stream_mutex.synchronize { @restart_stream = true }
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Check if a stream restart has been requested.
|
|
344
|
+
#
|
|
345
|
+
# @return [Boolean]
|
|
346
|
+
def restart_requested?
|
|
347
|
+
@stream_mutex.synchronize { @restart_stream }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Clear the restart flag.
|
|
351
|
+
#
|
|
352
|
+
# @return [void]
|
|
353
|
+
def clear_restart_flag
|
|
354
|
+
@stream_mutex.synchronize { @restart_stream = false }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Build the Change Stream pipeline with channel filtering.
|
|
358
|
+
# Filters to only receive inserts for channels this process subscribes to.
|
|
359
|
+
#
|
|
360
|
+
# @return [Array<Hash>] MongoDB aggregation pipeline
|
|
361
|
+
def build_pipeline
|
|
362
|
+
subscribed_channels = @subscribers.keys
|
|
363
|
+
|
|
364
|
+
if subscribed_channels.empty?
|
|
365
|
+
# No subscribers yet, watch for inserts only
|
|
366
|
+
[{ "$match" => { "operationType" => "insert" } }]
|
|
367
|
+
else
|
|
368
|
+
# Filter by subscribed channels at MongoDB level for performance
|
|
369
|
+
[
|
|
370
|
+
{ "$match" => { "operationType" => "insert" } },
|
|
371
|
+
{ "$match" => { "fullDocument.channel" => { "$in" => subscribed_channels } } }
|
|
372
|
+
]
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
303
376
|
# Calculate reconnect delay with exponential backoff.
|
|
304
377
|
#
|
|
305
378
|
# @return [Float] seconds to wait before retry
|
|
@@ -321,11 +394,12 @@ module ActionCable
|
|
|
321
394
|
#
|
|
322
395
|
# @return [void]
|
|
323
396
|
def listen_loop
|
|
324
|
-
pipeline = [{ "$match" => { "operationType" => "insert" } }]
|
|
325
|
-
|
|
326
397
|
while @running
|
|
327
398
|
begin
|
|
328
399
|
if change_stream_supported?
|
|
400
|
+
# Build pipeline with current channel subscriptions for filtering
|
|
401
|
+
pipeline = build_pipeline
|
|
402
|
+
|
|
329
403
|
# Change Stream path (replica set / sharded)
|
|
330
404
|
opts = { max_await_time_ms: 1000 }
|
|
331
405
|
opts[:resume_after] = @resume_token if @resume_token
|
|
@@ -333,7 +407,9 @@ module ActionCable
|
|
|
333
407
|
@stream = @collection.watch(pipeline, opts)
|
|
334
408
|
enum = @stream.to_enum
|
|
335
409
|
|
|
336
|
-
|
|
410
|
+
@adapter.logger.debug "SolidCableMongoid: watching #{@subscribers.keys.size} channel(s)"
|
|
411
|
+
|
|
412
|
+
while @running && enum && !restart_requested?
|
|
337
413
|
doc = enum.try_next
|
|
338
414
|
next unless doc # nil when no event yet
|
|
339
415
|
|
|
@@ -341,6 +417,14 @@ module ActionCable
|
|
|
341
417
|
@resume_token = @stream.resume_token
|
|
342
418
|
@reconnect_attempts = 0 # Reset on successful iteration
|
|
343
419
|
end
|
|
420
|
+
|
|
421
|
+
# Handle stream restart request
|
|
422
|
+
if restart_requested?
|
|
423
|
+
@adapter.logger.debug "SolidCableMongoid: restarting stream with updated channel filter"
|
|
424
|
+
clear_restart_flag
|
|
425
|
+
close_stream
|
|
426
|
+
next # Restart loop with new pipeline
|
|
427
|
+
end
|
|
344
428
|
else
|
|
345
429
|
# Standalone fallback: polling
|
|
346
430
|
poll_for_inserts
|
|
@@ -442,9 +526,15 @@ module ActionCable
|
|
|
442
526
|
message = full["message"]
|
|
443
527
|
return unless @subscribers.key?(channel)
|
|
444
528
|
|
|
445
|
-
|
|
529
|
+
ActiveSupport::Notifications.instrument("message_received.solid_cable_mongoid",
|
|
530
|
+
channel: channel,
|
|
531
|
+
subscriber_count: @subscribers[channel]&.size || 0) do
|
|
532
|
+
broadcast(channel, message)
|
|
533
|
+
end
|
|
446
534
|
rescue StandardError => e
|
|
447
535
|
@adapter.logger.error "SolidCableMongoid: failed to handle insert (#{e.class}): #{e.message}"
|
|
536
|
+
ActiveSupport::Notifications.instrument("message_error.solid_cable_mongoid",
|
|
537
|
+
channel: channel, error: e.class.name)
|
|
448
538
|
end
|
|
449
539
|
end
|
|
450
540
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_cable_mongoid_adapter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.0
|
|
4
|
+
version: 1.1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sal Scotto
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: actioncable
|
|
@@ -83,9 +82,13 @@ files:
|
|
|
83
82
|
- ".rubocop.yml"
|
|
84
83
|
- CHANGELOG.md
|
|
85
84
|
- CONTRIBUTING.md
|
|
86
|
-
- LICENSE
|
|
85
|
+
- LICENSE
|
|
87
86
|
- README.md
|
|
88
87
|
- Rakefile
|
|
88
|
+
- SECURITY.md
|
|
89
|
+
- benchmark/README.md
|
|
90
|
+
- benchmark/benchmark.rb
|
|
91
|
+
- benchmark/run_benchmark.sh
|
|
89
92
|
- lib/action_cable/subscription_adapter/solid_mongoid.rb
|
|
90
93
|
- lib/solid_cable_mongoid_adapter.rb
|
|
91
94
|
- lib/solid_cable_mongoid_adapter/version.rb
|
|
@@ -98,7 +101,6 @@ metadata:
|
|
|
98
101
|
source_code_uri: https://github.com/washu/solid_cable_mongoid_adapter
|
|
99
102
|
changelog_uri: https://github.com/washu/solid_cable_mongoid_adapter/blob/main/CHANGELOG.md
|
|
100
103
|
rubygems_mfa_required: 'true'
|
|
101
|
-
post_install_message:
|
|
102
104
|
rdoc_options: []
|
|
103
105
|
require_paths:
|
|
104
106
|
- lib
|
|
@@ -113,8 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
113
115
|
- !ruby/object:Gem::Version
|
|
114
116
|
version: '0'
|
|
115
117
|
requirements: []
|
|
116
|
-
rubygems_version: 3.
|
|
117
|
-
signing_key:
|
|
118
|
+
rubygems_version: 3.6.9
|
|
118
119
|
specification_version: 4
|
|
119
120
|
summary: MongoDB adapter for Action Cable using Mongoid
|
|
120
121
|
test_files: []
|
/data/{LICENSE.txt → LICENSE}
RENAMED
|
File without changes
|