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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f06792b582165dee33117bc0433cf27e61d3d9e932765bced3d8cdb4891d0ba9
4
- data.tar.gz: 81d50418ae04e5f99b82d57d13ec52fc293ec12a2bbd34f7d0b1f6cfcbaa0a9f
3
+ metadata.gz: 1aef7361e9ee2d03dbcd1d8bc89009cd4f3af500b1834e7fab52663850fde410
4
+ data.tar.gz: 63185c05065aa3ad657189fa7c2bbd8bf2a049eee2596e4e5d441c7c881ad258
5
5
  SHA512:
6
- metadata.gz: f79b6a5e853f86aba526fc5963039d4888659315d08604b72e202b167912e0b26ba8120c53a6b0f4cf9c41cbdc30f53c96f3714597e0b1e4c00363fd69d9bba9
7
- data.tar.gz: 5106fd2c0d1f12c3cfd854bb55a1ec239eb85bd51282c2fe9905f973f041ead14798da8466a9abd19385e09f478a1ca596db639f1a2d77e8836bcad35e603d30
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 primarily targeted at MySQL and SQLite but is tested to work
7
- > with PostgreSQL. PostgreSQL has its own Action Cable adapter which utilizes
8
- > the NOTIFY command for better performance. However, the PostgreSQL adapter
9
- > does have an 8kb limit on its payload, so if you find yourself broadcasting
10
- > large payloads, Solid Cable will work without a hitch.
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
- def trim_batch_size
18
- ::SolidCable.trim_batch_size
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
- rand < (expires_per_write - expires_per_write.floor)
25
- end
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: ..::SolidCable.message_retention.ago)
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:, channel_hash: channel_hash_for(channel) })
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
- def listener
36
- @listener || @server.mutex.synchronize do
37
- @listener ||= Listener.new(@server.event_loop)
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
- class Listener < ::ActionCable::SubscriptionAdapter::SubscriberMap
42
- def initialize(event_loop)
43
- super()
40
+ class Listener < ::ActionCable::SubscriptionAdapter::SubscriberMap
41
+ def initialize(event_loop)
42
+ super()
44
43
 
45
- @event_loop = event_loop
44
+ @event_loop = event_loop
46
45
 
47
- @thread = Thread.new do
48
- Thread.current.abort_on_exception = true
49
- listen
46
+ @thread = Thread.new do
47
+ Thread.current.abort_on_exception = true
48
+ listen
49
+ end
50
50
  end
51
- end
52
51
 
53
- def listen
54
- while running?
55
- with_polling_volume { broadcast_messages }
52
+ def listen
53
+ while running?
54
+ with_polling_volume { broadcast_messages }
56
55
 
57
- sleep ::SolidCable.polling_interval
56
+ sleep ::SolidCable.polling_interval
57
+ end
58
58
  end
59
- end
60
59
 
61
- def shutdown
62
- self.running = false
63
- Thread.pass while thread.alive?
64
- end
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
- def remove_channel(channel)
72
- channels.delete(channel)
73
- end
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
- def invoke_callback(*)
76
- event_loop.post { super }
77
- end
70
+ def remove_channel(channel)
71
+ channels.delete(channel)
72
+ end
78
73
 
79
- private
74
+ def invoke_callback(*)
75
+ event_loop.post { super }
76
+ end
80
77
 
81
- attr_reader :event_loop, :thread
82
- attr_writer :running, :last_id
78
+ private
79
+ attr_reader :event_loop, :thread
80
+ attr_writer :running, :last_id
83
81
 
84
- def running?
85
- if defined?(@running)
86
- @running
87
- else
88
- self.running = true
89
- end
90
- end
82
+ def running?
83
+ if defined?(@running)
84
+ @running
85
+ else
86
+ self.running = true
87
+ end
88
+ end
91
89
 
92
- def last_id
93
- @last_id ||= ::SolidCable::Message.maximum(:id) || 0
94
- end
90
+ def last_id
91
+ @last_id ||= ::SolidCable::Message.maximum(:id) || 0
92
+ end
95
93
 
96
- def channels
97
- @channels ||= Set.new
98
- end
94
+ def channels
95
+ @channels ||= Set.new
96
+ end
99
97
 
100
- def broadcast_messages
101
- ::SolidCable::Message.broadcastable(channels, last_id).
102
- each do |message|
103
- broadcast(message.channel, message.payload)
104
- self.last_id = message.id
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
- def with_polling_volume
109
- if ::SolidCable.silence_polling?
110
- ActiveRecord::Base.logger.silence { yield }
111
- else
112
- yield
113
- end
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 "<%= console %>" in any view
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"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidCable
4
- VERSION = "3.0.0"
4
+ VERSION = "3.0.1"
5
5
  end
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) < 1
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
- 10
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
- def cable_config
53
- Rails.application.config_for("cable")
54
- end
55
-
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
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.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-13 00:00:00.000000000 Z
11
+ date: 2024-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord