solid_cable 3.0.0 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +52 -5
- data/app/jobs/solid_cable/trim_job.rb +7 -8
- data/app/models/solid_cable/message.rb +3 -2
- data/lib/action_cable/subscription_adapter/solid_cable.rb +60 -62
- data/lib/generators/solid_cable/install/templates/config/cable.yml +1 -1
- data/lib/generators/solid_cable/install/templates/db/cable_schema.rb +0 -3
- data/lib/solid_cable/version.rb +1 -1
- data/lib/solid_cable.rb +14 -14
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1aef7361e9ee2d03dbcd1d8bc89009cd4f3af500b1834e7fab52663850fde410
|
4
|
+
data.tar.gz: 63185c05065aa3ad657189fa7c2bbd8bf2a049eee2596e4e5d441c7c881ad258
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b8da2e04fd45d8dea8098ad3d453d06d3df53881369480157f03fff63f1d1fa615fdfcc6a162aa2b85976c3767afc25ecae725663b375edc89568470176bda3
|
7
|
+
data.tar.gz: ba1fb17d9ac2c916e815db54d4495f6cd0f53f72d63f276932fcb935958b3e7853e7bd2685c5e677b5972f98a306910b2598405bf7ead4dfabb25aa2ba74b59e
|
data/README.md
CHANGED
@@ -3,11 +3,12 @@
|
|
3
3
|
Solid Cable is a database-backed Action Cable adapter that keeps messages in a table and continously polls for updates. This makes it possible to drop the common dependency on Redis, if it isn't needed for any other purpose. Despite polling, the performance of Solid Cable is comparable to Redis in most situations. And in all circumstances, it makes it easier to deploy Rails when Redis is no longer a required dependency for Action Cable functionality.
|
4
4
|
|
5
5
|
> [!NOTE]
|
6
|
-
> Solid Cable is
|
7
|
-
>
|
8
|
-
>
|
9
|
-
>
|
10
|
-
>
|
6
|
+
> Solid Cable is tested to work with MySQL, SQLite, and PostgreSQL.
|
7
|
+
>
|
8
|
+
> Action Cable already has a [dedicated PostgreSQL adapter](https://guides.rubyonrails.org/action_cable_overview.html#postgresql-adapter),
|
9
|
+
> which utilizes the builtin `NOTIFY` command for better performance. However, that
|
10
|
+
> adapter has an 8kb limit on its payload. Solid Cable is a great alternative if you find yourself
|
11
|
+
> broadcasting large payloads, or prefer not to use the `NOTIFY` command.
|
11
12
|
|
12
13
|
## Installation
|
13
14
|
|
@@ -51,6 +52,16 @@ production:
|
|
51
52
|
|
52
53
|
Then run `db:prepare` in production to ensure the database is created and the schema is loaded.
|
53
54
|
|
55
|
+
### Single database configuration
|
56
|
+
|
57
|
+
Running Solid Cable in a separate database is recommended, but it's also possible to use a single database for both the app and Action Cable.
|
58
|
+
|
59
|
+
1. Copy the contents of `db/cable_schema.rb` into a normal migration and delete `db/cable_schema.rb`
|
60
|
+
2. Remove `connects_to` from `config/cable.yml`
|
61
|
+
3. `bin/rails db:migrate`
|
62
|
+
|
63
|
+
You won't have multiple databases, so `database.yml` doesn't need to have primary and cable database.
|
64
|
+
|
54
65
|
## Configuration
|
55
66
|
|
56
67
|
All configuration is managed via the `config/cable.yml` file. By default, it'll be configured like this:
|
@@ -207,6 +218,42 @@ rtt..................: avg=822.08ms min=51ms med=732ms max=5.05s p(90)=1
|
|
207
218
|
ws_connecting........: avg=278.08ms min=146.66ms med=236.35ms max=2.37s p(90)=318.17ms p(95)=374.98ms
|
208
219
|
```
|
209
220
|
|
221
|
+
|
222
|
+
##### PostgreSQL with Solid Cable
|
223
|
+
|
224
|
+
With a polling interval of 0.1 seconds and autotrimming enabled. This instance
|
225
|
+
was also hosted on the same machine.
|
226
|
+
|
227
|
+
100 VUs
|
228
|
+
```
|
229
|
+
rtt..................: avg=137.45ms min=48ms med=139ms max=439ms p(90)=179.1ms p(95)=204ms
|
230
|
+
ws_connecting........: avg=207.13ms min=150.29ms med=197.76ms max=443.67ms p(90)=254.44ms p(95)=263.29ms
|
231
|
+
```
|
232
|
+
250 VUs
|
233
|
+
```
|
234
|
+
rtt..................: avg=151.63ms min=49ms med=146ms max=538ms p(90)=222ms p(95)=248.04ms
|
235
|
+
ws_connecting........: avg=245.89ms min=147.18ms med=205.57ms max=30s p(90)=265.08ms p(95)=281.15ms
|
236
|
+
```
|
237
|
+
500 VUs
|
238
|
+
```
|
239
|
+
rtt..................: avg=362.79ms min=50ms med=249ms max=1.21s p(90)=757ms p(95)=844ms
|
240
|
+
ws_connecting........: avg=257.02ms min=146.13ms med=227.65ms max=2.39s p(90)=303.22ms p(95)=344.39ms
|
241
|
+
```
|
242
|
+
|
243
|
+
|
244
|
+
##### PostgreSQL with dedicated adapter
|
245
|
+
|
246
|
+
100 VUs
|
247
|
+
```
|
248
|
+
rtt..................: avg=69.76ms min=41ms med=57ms max=622ms p(90)=116ms p(95)=133ms
|
249
|
+
ws_connecting........: avg=210.97ms min=149.68ms med=196.06ms max=1.27s p(90)=259.67ms p(95)=273.17ms
|
250
|
+
```
|
251
|
+
250 VUs
|
252
|
+
```
|
253
|
+
rtt..................: avg=73.43ms min=40ms med=58ms max=698ms p(90)=126ms p(95)=141ms
|
254
|
+
ws_connecting........: avg=210.83ms min=143.01ms med=195.22ms max=1.27s p(90)=259.27ms p(95)=272.6ms
|
255
|
+
```
|
256
|
+
|
210
257
|
## License
|
211
258
|
|
212
259
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -13,15 +13,14 @@ module SolidCable
|
|
13
13
|
end
|
14
14
|
|
15
15
|
private
|
16
|
+
def trim_batch_size
|
17
|
+
::SolidCable.trim_batch_size
|
18
|
+
end
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
|
-
def trim?
|
22
|
-
expires_per_write = (1 / trim_batch_size.to_f) * ::SolidCable.trim_chance
|
20
|
+
def trim?
|
21
|
+
expires_per_write = (1 / trim_batch_size.to_f) * ::SolidCable.trim_chance
|
23
22
|
|
24
|
-
|
25
|
-
|
23
|
+
rand < (expires_per_write - expires_per_write.floor)
|
24
|
+
end
|
26
25
|
end
|
27
26
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module SolidCable
|
4
4
|
class Message < SolidCable::Record
|
5
5
|
scope :trimmable, lambda {
|
6
|
-
where(created_at:
|
6
|
+
where(created_at: ...::SolidCable.message_retention.ago)
|
7
7
|
}
|
8
8
|
scope :broadcastable, lambda { |channels, last_id|
|
9
9
|
where(channel_hash: channel_hashes_for(channels)).
|
@@ -12,7 +12,8 @@ module SolidCable
|
|
12
12
|
|
13
13
|
class << self
|
14
14
|
def broadcast(channel, payload)
|
15
|
-
insert({ channel:, payload:,
|
15
|
+
insert({ created_at: Time.current, channel:, payload:,
|
16
|
+
channel_hash: channel_hash_for(channel) })
|
16
17
|
end
|
17
18
|
|
18
19
|
def channel_hashes_for(channels)
|
@@ -31,88 +31,86 @@ module ActionCable
|
|
31
31
|
delegate :shutdown, to: :listener
|
32
32
|
|
33
33
|
private
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
34
|
+
def listener
|
35
|
+
@listener || @server.mutex.synchronize do
|
36
|
+
@listener ||= Listener.new(@server.event_loop)
|
37
|
+
end
|
38
38
|
end
|
39
|
-
end
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
|
40
|
+
class Listener < ::ActionCable::SubscriptionAdapter::SubscriberMap
|
41
|
+
def initialize(event_loop)
|
42
|
+
super()
|
44
43
|
|
45
|
-
|
44
|
+
@event_loop = event_loop
|
46
45
|
|
47
|
-
|
48
|
-
|
49
|
-
|
46
|
+
@thread = Thread.new do
|
47
|
+
Thread.current.abort_on_exception = true
|
48
|
+
listen
|
49
|
+
end
|
50
50
|
end
|
51
|
-
end
|
52
51
|
|
53
|
-
|
54
|
-
|
55
|
-
|
52
|
+
def listen
|
53
|
+
while running?
|
54
|
+
with_polling_volume { broadcast_messages }
|
56
55
|
|
57
|
-
|
56
|
+
sleep ::SolidCable.polling_interval
|
57
|
+
end
|
58
58
|
end
|
59
|
-
end
|
60
59
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
def add_channel(channel, on_success)
|
67
|
-
channels.add(channel)
|
68
|
-
event_loop.post(&on_success) if on_success
|
69
|
-
end
|
60
|
+
def shutdown
|
61
|
+
self.running = false
|
62
|
+
Thread.pass while thread.alive?
|
63
|
+
end
|
70
64
|
|
71
|
-
|
72
|
-
|
73
|
-
|
65
|
+
def add_channel(channel, on_success)
|
66
|
+
channels.add(channel)
|
67
|
+
event_loop.post(&on_success) if on_success
|
68
|
+
end
|
74
69
|
|
75
|
-
|
76
|
-
|
77
|
-
|
70
|
+
def remove_channel(channel)
|
71
|
+
channels.delete(channel)
|
72
|
+
end
|
78
73
|
|
79
|
-
|
74
|
+
def invoke_callback(*)
|
75
|
+
event_loop.post { super }
|
76
|
+
end
|
80
77
|
|
81
|
-
|
82
|
-
|
78
|
+
private
|
79
|
+
attr_reader :event_loop, :thread
|
80
|
+
attr_writer :running, :last_id
|
83
81
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
82
|
+
def running?
|
83
|
+
if defined?(@running)
|
84
|
+
@running
|
85
|
+
else
|
86
|
+
self.running = true
|
87
|
+
end
|
88
|
+
end
|
91
89
|
|
92
|
-
|
93
|
-
|
94
|
-
|
90
|
+
def last_id
|
91
|
+
@last_id ||= ::SolidCable::Message.maximum(:id) || 0
|
92
|
+
end
|
95
93
|
|
96
|
-
|
97
|
-
|
98
|
-
|
94
|
+
def channels
|
95
|
+
@channels ||= Set.new
|
96
|
+
end
|
99
97
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
98
|
+
def broadcast_messages
|
99
|
+
::SolidCable::Message.broadcastable(channels, last_id).
|
100
|
+
each do |message|
|
101
|
+
broadcast(message.channel, message.payload)
|
102
|
+
self.last_id = message.id
|
103
|
+
end
|
105
104
|
end
|
106
|
-
end
|
107
105
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
106
|
+
def with_polling_volume
|
107
|
+
if ::SolidCable.silence_polling?
|
108
|
+
ActiveRecord::Base.logger.silence { yield }
|
109
|
+
else
|
110
|
+
yield
|
111
|
+
end
|
112
|
+
end
|
114
113
|
end
|
115
|
-
end
|
116
114
|
end
|
117
115
|
end
|
118
116
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# Async adapter only works within the same process, so for manually triggering cable updates from a console,
|
2
2
|
# and seeing results in the browser, you must do so from the web console (running inside the dev process),
|
3
|
-
# not a terminal started via bin/rails console! Add "console" to any action or
|
3
|
+
# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
|
4
4
|
# to make the web console appear.
|
5
5
|
development:
|
6
6
|
adapter: async
|
@@ -1,11 +1,8 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
ActiveRecord::Schema[7.1].define(version: 1) do
|
4
2
|
create_table "solid_cable_messages", force: :cascade do |t|
|
5
3
|
t.binary "channel", limit: 1024, null: false
|
6
4
|
t.binary "payload", limit: 536870912, null: false
|
7
5
|
t.datetime "created_at", null: false
|
8
|
-
t.datetime "updated_at", null: false
|
9
6
|
t.integer "channel_hash", limit: 8, null: false
|
10
7
|
t.index ["channel"], name: "index_solid_cable_messages_on_channel"
|
11
8
|
t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
|
data/lib/solid_cable/version.rb
CHANGED
data/lib/solid_cable.rb
CHANGED
@@ -27,7 +27,7 @@ module SolidCable
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def trim_batch_size
|
30
|
-
if (size = cable_config.trim_batch_size.to_i) <
|
30
|
+
if (size = cable_config.trim_batch_size.to_i) < 2
|
31
31
|
100
|
32
32
|
else
|
33
33
|
size
|
@@ -42,24 +42,24 @@ module SolidCable
|
|
42
42
|
# many records. This ensures there is downward pressure on the cache size
|
43
43
|
# while there is valid data to delete. Read this as 'every time the trim job
|
44
44
|
# runs theres a trim_multiplier chance this trims'. Adjust number to make it
|
45
|
-
# more or less likely to trim.
|
45
|
+
# more or less likely to trim. Only works like this if trim_batch_size is
|
46
|
+
# 100
|
46
47
|
def trim_chance
|
47
|
-
|
48
|
+
2
|
48
49
|
end
|
49
50
|
|
50
51
|
private
|
52
|
+
def cable_config
|
53
|
+
Rails.application.config_for("cable")
|
54
|
+
end
|
51
55
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
amount.join(".").to_f.public_send(units)
|
60
|
-
else
|
61
|
-
default
|
56
|
+
def parse_duration(duration, default:)
|
57
|
+
if duration.present?
|
58
|
+
*amount, units = duration.to_s.split(".")
|
59
|
+
amount.join(".").to_f.public_send(units)
|
60
|
+
else
|
61
|
+
default
|
62
|
+
end
|
62
63
|
end
|
63
|
-
end
|
64
64
|
end
|
65
65
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: solid_cable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.
|
4
|
+
version: 3.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Pezza
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-09-
|
11
|
+
date: 2024-09-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|