solid_cable 4.0.2 → 4.1.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/README.md +35 -1
- data/app/jobs/solid_cable/trim_job.rb +1 -15
- data/app/models/solid_cable/message/encryption.rb +15 -0
- data/app/models/solid_cable/message.rb +11 -5
- data/lib/action_cable/subscription_adapter/solid_cable.rb +25 -16
- data/lib/generators/solid_cable/install/templates/config/cable.yml +2 -0
- data/lib/generators/solid_cable/install/templates/db/cable_schema.rb +0 -1
- data/lib/generators/solid_cable/update/templates/db/migrate/remove_channel_index.rb +11 -0
- data/lib/generators/solid_cable/update/update_generator.rb +2 -0
- data/lib/solid_cable/batched_broadcaster.rb +99 -0
- data/lib/solid_cable/configuration.rb +104 -0
- data/lib/solid_cable/engine.rb +8 -0
- data/lib/solid_cable/trimming.rb +23 -0
- data/lib/solid_cable/version.rb +1 -1
- data/lib/solid_cable.rb +14 -55
- metadata +21 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8c6572058c2cfbbfe229f6bc376f1459611b40005e66396c82b419282535263
|
|
4
|
+
data.tar.gz: 6a480dc962405e9c97ab4efa82e47132e26bbfbc2a159f67f3c484de25655092
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35e2ecf675522441a1b4c8c088e63950aa2b7506a8cbdace50fae576e1de8902dbcb5e551587903e66648d1fc47ee8778480517631a37648ee9afcfbb96896a5
|
|
7
|
+
data.tar.gz: addcc08fd66aa27e4aa717b5ce1f1508efba528da6285489ba0676b2b044f37237ce065f8e2f6ab792d21bfc9989e5525550e6bd23da07cb4f0a8a6528af4fe0
|
data/README.md
CHANGED
|
@@ -88,11 +88,45 @@ The options are:
|
|
|
88
88
|
- `trim_batch_size` - the batch size to use when deleting old records (default: `100`)
|
|
89
89
|
- `reconnect_attempts` - Supports a number of connection attempts or an array of
|
|
90
90
|
durations to wait between attempts. (Defaults to 1 retry attempt)
|
|
91
|
+
- `encrypt` - whether to encrypt message payloads with Active Record Encryption.
|
|
92
|
+
(Defaults to false)
|
|
93
|
+
|
|
94
|
+
### Enabling encryption
|
|
95
|
+
|
|
96
|
+
Solid Cable can encrypt stored message payloads with Active Record Encryption. Add
|
|
97
|
+
`encrypt: true` to the Solid Cable environment in `config/cable.yml`:
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
production:
|
|
101
|
+
adapter: solid_cable
|
|
102
|
+
encrypt: true
|
|
103
|
+
connects_to:
|
|
104
|
+
database:
|
|
105
|
+
writing: cable
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Your application must also be [configured to use Active Record Encryption](https://guides.rubyonrails.org/active_record_encryption.html#setup).
|
|
109
|
+
Solid Cable uses the binary MessagePack serializer by default, so your application
|
|
110
|
+
must include the `msgpack` gem.
|
|
111
|
+
|
|
112
|
+
Since encryption context properties contain Ruby objects, they cannot be set in
|
|
113
|
+
`config/cable.yml`. Set them in an initializer instead:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
# config/initializers/solid_cable.rb
|
|
117
|
+
SolidCable.configuration.encryption_context_properties = {
|
|
118
|
+
encryptor: ActiveRecord::Encryption::Encryptor.new,
|
|
119
|
+
message_serializer: ActiveRecord::Encryption::MessageSerializer.new
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Active Record Encryption does not support encrypted binary columns on PostgreSQL
|
|
124
|
+
with Rails 7. Solid Cable raises during boot for that unsupported combination.
|
|
91
125
|
|
|
92
126
|
|
|
93
127
|
## Trimming
|
|
94
128
|
|
|
95
|
-
Messages are autotrimmed based upon the `message_retention` setting to determine how long messages are to be kept around. If no `message_retention` is given or parsing fails, it defaults to `1.day`.
|
|
129
|
+
Messages are autotrimmed based upon the `message_retention` setting to determine how long messages are to be kept around. If no `message_retention` is given or parsing fails, it defaults to `1.day`. For every message written, Solid Cable attempts to trim twice as many expired messages.
|
|
96
130
|
|
|
97
131
|
Autotrimming can negatively impact performance slightly depending on your workload because it is potentially doing a delete on broadcast. If
|
|
98
132
|
you would prefer, you can disable autotrimming by setting `autotrim: false` and you can manually enqueue the job later, `SolidCable::TrimJob.perform_later`, or run it on a recurring interval out of band.
|
|
@@ -2,26 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidCable
|
|
4
4
|
class TrimJob < ActiveJob::Base
|
|
5
|
-
def perform
|
|
6
|
-
return unless trim?
|
|
7
|
-
|
|
5
|
+
def perform(trim_batch_size: ::SolidCable.trim_batch_size)
|
|
8
6
|
::SolidCable::Message.transaction do
|
|
9
7
|
ids = ::SolidCable::Message.trimmable.non_blocking_lock.
|
|
10
8
|
limit(trim_batch_size).pluck(:id)
|
|
11
9
|
::SolidCable::Message.where(id: ids).delete_all
|
|
12
10
|
end
|
|
13
11
|
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
def trim_batch_size
|
|
17
|
-
::SolidCable.trim_batch_size
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def trim?
|
|
21
|
-
expires_per_write = (1 / trim_batch_size.to_f) * ::SolidCable.trim_chance
|
|
22
|
-
|
|
23
|
-
!::SolidCable.autotrim? ||
|
|
24
|
-
rand < (expires_per_write - expires_per_write.floor)
|
|
25
|
-
end
|
|
26
12
|
end
|
|
27
13
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidCable
|
|
4
|
+
class Message
|
|
5
|
+
module Encryption
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
if SolidCable.encrypt?
|
|
10
|
+
encrypts :payload, **SolidCable.encryption_context_properties, support_unencrypted_data: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidCable
|
|
4
4
|
class Message < SolidCable::Record
|
|
5
|
+
include Encryption
|
|
6
|
+
|
|
5
7
|
scope :trimmable, lambda {
|
|
6
8
|
where(created_at: ...::SolidCable.message_retention.ago)
|
|
7
9
|
}
|
|
8
|
-
scope :broadcastable, lambda { |
|
|
9
|
-
|
|
10
|
-
where(id: (last_id.to_i + 1)..).order(:id)
|
|
10
|
+
scope :broadcastable, lambda { |channel_hashes, last_id|
|
|
11
|
+
select(column_names.excluding("created_at", "updated_at")).
|
|
12
|
+
where(channel_hash: channel_hashes).where(id: (last_id.to_i + 1)..).order(:id)
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
class << self
|
|
@@ -16,8 +18,12 @@ module SolidCable
|
|
|
16
18
|
channel_hash: channel_hash_for(channel) })
|
|
17
19
|
end
|
|
18
20
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
+
def broadcast_batch(broadcasts)
|
|
22
|
+
created_at = Time.current
|
|
23
|
+
insert_all broadcasts.map { |message|
|
|
24
|
+
{ created_at:, channel: message.channel,
|
|
25
|
+
payload: message.payload, channel_hash: channel_hash_for(message.channel) }
|
|
26
|
+
}
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
# Need to unpack this as a signed integer since Postgresql and SQLite
|
|
@@ -20,23 +20,25 @@ module ActionCable
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
@listener = nil
|
|
23
|
+
@broadcaster = nil
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def broadcast(channel, payload)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
::SolidCable::TrimJob.perform_now if ::SolidCable.autotrim?
|
|
27
|
+
broadcaster.broadcast(channel, payload)
|
|
29
28
|
end
|
|
30
29
|
|
|
31
|
-
def subscribe(channel,
|
|
32
|
-
listener.add_subscriber(channel,
|
|
30
|
+
def subscribe(channel, subscriber, success_callback = nil)
|
|
31
|
+
listener.add_subscriber(channel, subscriber, success_callback)
|
|
33
32
|
end
|
|
34
33
|
|
|
35
|
-
def unsubscribe(channel,
|
|
36
|
-
listener.remove_subscriber(channel,
|
|
34
|
+
def unsubscribe(channel, subscriber)
|
|
35
|
+
listener.remove_subscriber(channel, subscriber)
|
|
37
36
|
end
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
def shutdown
|
|
39
|
+
@broadcaster&.shutdown
|
|
40
|
+
@listener&.shutdown
|
|
41
|
+
end
|
|
40
42
|
|
|
41
43
|
private
|
|
42
44
|
def listener
|
|
@@ -45,6 +47,12 @@ module ActionCable
|
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
def broadcaster
|
|
51
|
+
@broadcaster || @mutex.synchronize do
|
|
52
|
+
@broadcaster ||= ::SolidCable::BatchedBroadcaster.new
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
48
56
|
def pubsub_executor
|
|
49
57
|
@pubsub_executor ||=
|
|
50
58
|
if respond_to?(:executor, true)
|
|
@@ -122,12 +130,12 @@ module ActionCable
|
|
|
122
130
|
end
|
|
123
131
|
|
|
124
132
|
def add_channel(channel, on_success)
|
|
125
|
-
channels[channel] = last_message_id
|
|
133
|
+
channels[::SolidCable::Message.channel_hash_for(channel)] = last_message_id
|
|
126
134
|
on_success.call if on_success
|
|
127
135
|
end
|
|
128
136
|
|
|
129
137
|
def remove_channel(channel)
|
|
130
|
-
channels.delete(channel)
|
|
138
|
+
channels.delete(::SolidCable::Message.channel_hash_for(channel))
|
|
131
139
|
end
|
|
132
140
|
|
|
133
141
|
def invoke_callback(*)
|
|
@@ -147,26 +155,27 @@ module ActionCable
|
|
|
147
155
|
end
|
|
148
156
|
|
|
149
157
|
def broadcast_messages
|
|
150
|
-
current_channels = channels.dup
|
|
151
|
-
|
|
152
158
|
::SolidCable::Message.
|
|
153
|
-
broadcastable(
|
|
154
|
-
each do |message|
|
|
159
|
+
broadcastable(channels.keys, last_id).each do |message|
|
|
155
160
|
should_broadcast_message = false
|
|
156
|
-
channels.compute_if_present(message.
|
|
161
|
+
channels.compute_if_present(message.channel_hash) do |channel_last_id|
|
|
157
162
|
break if channel_last_id >= message.id
|
|
158
163
|
|
|
159
164
|
should_broadcast_message = true
|
|
160
165
|
message.id
|
|
161
166
|
end
|
|
162
167
|
|
|
163
|
-
broadcast(message
|
|
168
|
+
broadcast(message) if should_broadcast_message
|
|
164
169
|
self.last_id = message.id
|
|
165
170
|
end
|
|
166
171
|
|
|
167
172
|
self.reconnect_attempt = 0
|
|
168
173
|
end
|
|
169
174
|
|
|
175
|
+
def broadcast(message)
|
|
176
|
+
super(message.channel, message.payload)
|
|
177
|
+
end
|
|
178
|
+
|
|
170
179
|
def with_polling_volume
|
|
171
180
|
if ::SolidCable.silence_polling? && ActiveRecord::Base.logger
|
|
172
181
|
ActiveRecord::Base.logger.silence { yield }
|
|
@@ -4,7 +4,6 @@ ActiveRecord::Schema[7.1].define(version: 1) do
|
|
|
4
4
|
t.binary "payload", limit: 536870912, null: false
|
|
5
5
|
t.datetime "created_at", null: false
|
|
6
6
|
t.integer "channel_hash", limit: 8, null: false
|
|
7
|
-
t.index ["channel"], name: "index_solid_cable_messages_on_channel"
|
|
8
7
|
t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
|
|
9
8
|
t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
|
|
10
9
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RemoveChannelIndex < ActiveRecord::Migration[7.2]
|
|
4
|
+
def up
|
|
5
|
+
remove_index :solid_cable_messages, :channel, if_exists: true
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def down
|
|
9
|
+
add_index :solid_cable_messages, :channel, if_not_exists: true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -11,5 +11,7 @@ class SolidCable::UpdateGenerator < Rails::Generators::Base
|
|
|
11
11
|
def copy_files
|
|
12
12
|
migration_template "db/migrate/create_compact_channel.rb",
|
|
13
13
|
"db/cable_migrate/create_compact_channel.rb"
|
|
14
|
+
migration_template "db/migrate/remove_channel_index.rb",
|
|
15
|
+
"db/cable_migrate/remove_channel_index.rb"
|
|
14
16
|
end
|
|
15
17
|
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent"
|
|
4
|
+
|
|
5
|
+
module SolidCable
|
|
6
|
+
class BatchedBroadcaster
|
|
7
|
+
include Trimming
|
|
8
|
+
|
|
9
|
+
Stopped = Class.new(StandardError)
|
|
10
|
+
Message = Struct.new(:channel, :payload, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
def initialize(batch_size: SolidCable.writer_batch_size, batch_delay: SolidCable.writer_batch_delay)
|
|
13
|
+
@batch_size = batch_size
|
|
14
|
+
@batch_delay = batch_delay
|
|
15
|
+
@queue = Queue.new
|
|
16
|
+
@background = Concurrent::FixedThreadPool.new(1, max_queue: 100, fallback_policy: :discard)
|
|
17
|
+
|
|
18
|
+
@thread = Thread.new do
|
|
19
|
+
Thread.current.name = "solid_cable_writer"
|
|
20
|
+
Thread.current.abort_on_exception = true
|
|
21
|
+
listen_for_initial_messages
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def broadcast(channel, payload)
|
|
26
|
+
message = Message.new(channel:, payload:)
|
|
27
|
+
|
|
28
|
+
queue.enq message
|
|
29
|
+
rescue ClosedQueueError
|
|
30
|
+
raise Stopped, "Solid Cable writer has stopped"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def shutdown
|
|
34
|
+
queue.close
|
|
35
|
+
thread.join
|
|
36
|
+
background.shutdown
|
|
37
|
+
background.wait_for_termination
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
attr_reader :batch_size, :batch_delay, :queue, :thread, :background
|
|
42
|
+
|
|
43
|
+
def listen_for_initial_messages
|
|
44
|
+
loop do
|
|
45
|
+
message = queue.pop
|
|
46
|
+
|
|
47
|
+
break if message.nil?
|
|
48
|
+
|
|
49
|
+
collect_batch(message)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def collect_batch(first_message)
|
|
54
|
+
batch = [ first_message ]
|
|
55
|
+
deadline = monotonic_time + batch_delay
|
|
56
|
+
|
|
57
|
+
drain_queue_into(batch)
|
|
58
|
+
wait_for_messages_until(batch, deadline)
|
|
59
|
+
|
|
60
|
+
flush(batch)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def drain_queue_into(batch)
|
|
64
|
+
while batch.size < batch_size && (message = queue.pop(timeout: 0))
|
|
65
|
+
batch << message
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def wait_for_messages_until(batch, deadline)
|
|
70
|
+
while batch.size < batch_size && (remaining = deadline - monotonic_time).positive?
|
|
71
|
+
message = queue.pop(timeout: remaining)
|
|
72
|
+
break if message.nil?
|
|
73
|
+
|
|
74
|
+
batch << message
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def flush(batch)
|
|
79
|
+
Rails.application.executor.wrap do
|
|
80
|
+
SolidCable::Message.broadcast_batch(batch)
|
|
81
|
+
track_writes(batch.size) if SolidCable.autotrim?
|
|
82
|
+
end
|
|
83
|
+
rescue StandardError => error
|
|
84
|
+
Rails.error.report(error)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def async(&block)
|
|
88
|
+
background << -> do
|
|
89
|
+
Rails.application.executor.wrap(&block)
|
|
90
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
91
|
+
Rails.error.report(error)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def monotonic_time
|
|
96
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module SolidCable
|
|
2
|
+
class Configuration
|
|
3
|
+
def initialize(**options)
|
|
4
|
+
@options = ActiveSupport::InheritableOptions.new(options)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
attr_writer :connects_to, :silence_polling, :polling_interval,
|
|
8
|
+
:message_retention, :autotrim, :trim_batch_size, :use_skip_locked,
|
|
9
|
+
:reconnect_attempts, :writer_batch_size, :writer_batch_delay,
|
|
10
|
+
:encrypt, :encryption_context_properties
|
|
11
|
+
|
|
12
|
+
def connects_to
|
|
13
|
+
@connects_to ||= options.connects_to.to_h.deep_transform_values(&:to_sym)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def silence_polling?
|
|
17
|
+
return @silence_polling if defined?(@silence_polling)
|
|
18
|
+
|
|
19
|
+
@silence_polling = options.silence_polling != false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def polling_interval
|
|
23
|
+
@polling_interval ||=
|
|
24
|
+
parse_duration(options.polling_interval, default: 0.1.seconds)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def message_retention
|
|
28
|
+
@message_retention ||= parse_duration(options.message_retention, default: 1.day)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def autotrim?
|
|
32
|
+
return @autotrim if defined?(@autotrim)
|
|
33
|
+
|
|
34
|
+
@autotrim = options.autotrim != false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def trim_batch_size
|
|
38
|
+
@trim_batch_size ||=
|
|
39
|
+
if (size = options.trim_batch_size.to_i) < 2
|
|
40
|
+
100
|
|
41
|
+
else
|
|
42
|
+
size
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def use_skip_locked
|
|
47
|
+
return @use_skip_locked if defined?(@use_skip_locked)
|
|
48
|
+
|
|
49
|
+
@use_skip_locked = options.use_skip_locked != false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def reconnect_attempts
|
|
53
|
+
@reconnect_attempts ||= begin
|
|
54
|
+
attempts = options[:reconnect_attempts] || 1
|
|
55
|
+
attempts = Array.new(attempts, 0) if attempts.is_a?(Integer)
|
|
56
|
+
attempts
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def writer_batch_size
|
|
61
|
+
@writer_batch_size ||= [ (options.writer_batch_size || 4).to_i, 1 ].max
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def writer_batch_delay
|
|
65
|
+
@writer_batch_delay ||=
|
|
66
|
+
[ parse_duration(options.writer_batch_delay, default: 0.001.seconds), 0 ].max
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def encrypt?
|
|
70
|
+
return @encrypt if defined?(@encrypt)
|
|
71
|
+
|
|
72
|
+
@encrypt = options.encrypt.present?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def encryption_context_properties
|
|
76
|
+
return @encryption_context_properties if defined?(@encryption_context_properties)
|
|
77
|
+
|
|
78
|
+
@encryption_context_properties = options.encryption_context_properties&.deep_symbolize_keys
|
|
79
|
+
@encryption_context_properties ||= default_encryption_context_properties if encrypt?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
attr_reader :options
|
|
84
|
+
|
|
85
|
+
def default_encryption_context_properties
|
|
86
|
+
require "active_record/encryption/message_pack_message_serializer"
|
|
87
|
+
|
|
88
|
+
{
|
|
89
|
+
encryptor: ActiveRecord::Encryption::Encryptor.new(compress: true),
|
|
90
|
+
# Binary column only serializer that is 40% more efficient than the default MessageSerializer
|
|
91
|
+
message_serializer: ActiveRecord::Encryption::MessagePackMessageSerializer.new
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def parse_duration(duration, default:)
|
|
96
|
+
if duration.present?
|
|
97
|
+
*amount, units = duration.to_s.split(".")
|
|
98
|
+
amount.join(".").to_f.public_send(units)
|
|
99
|
+
else
|
|
100
|
+
default
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/solid_cable/engine.rb
CHANGED
|
@@ -3,5 +3,13 @@
|
|
|
3
3
|
module SolidCable
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
5
|
isolate_namespace SolidCable
|
|
6
|
+
|
|
7
|
+
config.after_initialize do
|
|
8
|
+
if SolidCable.encrypt? && Record.lease_connection.adapter_name == "PostgreSQL" && Rails::VERSION::MAJOR == 7
|
|
9
|
+
raise \
|
|
10
|
+
"Cannot enable encryption for Solid Cable: in Rails 7, Active Record Encryption does not support " \
|
|
11
|
+
"encrypting binary columns on PostgreSQL"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
6
14
|
end
|
|
7
15
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidCable
|
|
4
|
+
module Trimming
|
|
5
|
+
# For every write that we do, we attempt to delete TRIM_MULTIPLIER times as
|
|
6
|
+
# many records. This ensures there is downward pressure on the message count
|
|
7
|
+
# while there is old data to delete.
|
|
8
|
+
TRIM_MULTIPLIER = 2
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
def track_writes(count)
|
|
12
|
+
trim_batches(count).times { async { TrimJob.perform_now } }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def trim_batches(count)
|
|
16
|
+
trims_per_write = (1 / SolidCable.trim_batch_size.to_f) * TRIM_MULTIPLIER
|
|
17
|
+
batches = (count * trims_per_write).floor
|
|
18
|
+
overflow_batch_chance = count * trims_per_write - batches
|
|
19
|
+
batches += 1 if rand < overflow_batch_chance
|
|
20
|
+
batches
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/solid_cable/version.rb
CHANGED
data/lib/solid_cable.rb
CHANGED
|
@@ -2,70 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
require "solid_cable/version"
|
|
4
4
|
require "solid_cable/engine"
|
|
5
|
+
require "solid_cable/configuration"
|
|
6
|
+
require "solid_cable/trimming"
|
|
7
|
+
require "solid_cable/batched_broadcaster"
|
|
5
8
|
require "action_cable/subscription_adapter/solid_cable"
|
|
6
9
|
|
|
7
10
|
module SolidCable
|
|
8
11
|
class << self
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
cable_config.silence_polling != false
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def polling_interval
|
|
18
|
-
parse_duration(cable_config.polling_interval, default: 0.1.seconds)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def message_retention
|
|
22
|
-
parse_duration(cable_config.message_retention, default: 1.day)
|
|
23
|
-
end
|
|
12
|
+
delegate :connects_to, :silence_polling?, :polling_interval,
|
|
13
|
+
:message_retention, :autotrim?, :trim_batch_size, :use_skip_locked,
|
|
14
|
+
:reconnect_attempts, :writer_batch_size, :writer_batch_delay,
|
|
15
|
+
:encrypt?, :encryption_context_properties,
|
|
16
|
+
to: :configuration
|
|
24
17
|
|
|
25
|
-
def
|
|
26
|
-
|
|
18
|
+
def configuration
|
|
19
|
+
@configuration ||= Configuration.new(**Rails.application.config_for("cable"))
|
|
27
20
|
end
|
|
28
21
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
100
|
|
32
|
-
else
|
|
33
|
-
size
|
|
34
|
-
end
|
|
22
|
+
def configure(**options)
|
|
23
|
+
@configuration = Configuration.new(**options)
|
|
35
24
|
end
|
|
36
25
|
|
|
37
|
-
def
|
|
38
|
-
|
|
26
|
+
def reset_configuration!
|
|
27
|
+
@configuration = nil
|
|
39
28
|
end
|
|
40
|
-
|
|
41
|
-
# For every write that we do, we attempt to delete trim_chance times as
|
|
42
|
-
# many records. This ensures there is downward pressure on the cache size
|
|
43
|
-
# while there is valid data to delete. Read this as 'every time the trim job
|
|
44
|
-
# runs theres a trim_multiplier chance this trims'. Adjust number to make it
|
|
45
|
-
# more or less likely to trim. Only works like this if trim_batch_size is
|
|
46
|
-
# 100
|
|
47
|
-
def trim_chance
|
|
48
|
-
2
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def reconnect_attempts
|
|
52
|
-
attempts = cable_config.fetch(:reconnect_attempts, 1)
|
|
53
|
-
attempts = Array.new(attempts, 0) if attempts.is_a?(Integer)
|
|
54
|
-
attempts
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
58
|
-
def cable_config
|
|
59
|
-
Rails.application.config_for("cable")
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def parse_duration(duration, default:)
|
|
63
|
-
if duration.present?
|
|
64
|
-
*amount, units = duration.to_s.split(".")
|
|
65
|
-
amount.join(".").to_f.public_send(units)
|
|
66
|
-
else
|
|
67
|
-
default
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
29
|
end
|
|
71
30
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_cable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0
|
|
4
|
+
version: 4.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nick Pezza
|
|
@@ -65,6 +65,20 @@ dependencies:
|
|
|
65
65
|
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '7.2'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: msgpack
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
68
82
|
- !ruby/object:Gem::Dependency
|
|
69
83
|
name: minitest
|
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -91,6 +105,7 @@ files:
|
|
|
91
105
|
- Rakefile
|
|
92
106
|
- app/jobs/solid_cable/trim_job.rb
|
|
93
107
|
- app/models/solid_cable/message.rb
|
|
108
|
+
- app/models/solid_cable/message/encryption.rb
|
|
94
109
|
- app/models/solid_cable/record.rb
|
|
95
110
|
- lib/action_cable/subscription_adapter/solid_cable.rb
|
|
96
111
|
- lib/generators/solid_cable/install/USAGE
|
|
@@ -99,9 +114,13 @@ files:
|
|
|
99
114
|
- lib/generators/solid_cable/install/templates/db/cable_schema.rb
|
|
100
115
|
- lib/generators/solid_cable/update/USAGE
|
|
101
116
|
- lib/generators/solid_cable/update/templates/db/migrate/create_compact_channel.rb
|
|
117
|
+
- lib/generators/solid_cable/update/templates/db/migrate/remove_channel_index.rb
|
|
102
118
|
- lib/generators/solid_cable/update/update_generator.rb
|
|
103
119
|
- lib/solid_cable.rb
|
|
120
|
+
- lib/solid_cable/batched_broadcaster.rb
|
|
121
|
+
- lib/solid_cable/configuration.rb
|
|
104
122
|
- lib/solid_cable/engine.rb
|
|
123
|
+
- lib/solid_cable/trimming.rb
|
|
105
124
|
- lib/solid_cable/version.rb
|
|
106
125
|
- lib/tasks/solid_cable_tasks.rake
|
|
107
126
|
homepage: https://github.com/rails/solid_cable
|
|
@@ -125,7 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
125
144
|
- !ruby/object:Gem::Version
|
|
126
145
|
version: '0'
|
|
127
146
|
requirements: []
|
|
128
|
-
rubygems_version: 4.0.
|
|
147
|
+
rubygems_version: 4.0.20
|
|
129
148
|
specification_version: 4
|
|
130
149
|
summary: Database-backed Action Cable backend.
|
|
131
150
|
test_files: []
|