graphql-anycable 1.1.7 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d792c5cf2b52398bfe6c09e768a1c3af484209aca0bbfa810eccb25480d791d2
4
- data.tar.gz: 29eb4a0c90b0cef15040ce6c67493e832c7efe191ee90f98e1e1de4eb2c89eb0
3
+ metadata.gz: bafa1a80487eeb78627ed0752751eb488dd33ca9709d38b205d61d310f97d7ae
4
+ data.tar.gz: 821cba31fd376a471c9db5757a2da3e51d936f618f372a624fab1515dea5d4e6
5
5
  SHA512:
6
- metadata.gz: 276d5e8243fd288480ec7420eb7151e4dcb6802df9255fb2bb581d45f6f9b5d6eb582cc789d0bde4f1541bdbf17b5bab6e5c9244adb264ce24c0bafce4a57266
7
- data.tar.gz: a7237e211c05268f1a74da0a32946b97f1f737046c1c7920b5b786f5755026f8c1529c41a3783e3ec8d41a42d37d5b52ea8672523c095bc9e42138c04f309ab3
6
+ metadata.gz: fdad5b3cfcc372ec326ebfa9641ecdc81d5f586aca7fc54edb5bc1985d4e0e8fabd3ba93eb25cf8e9f55556a7d5410a2971596ab52936706199cdb6c6f3faa10
7
+ data.tar.gz: f71265d7dc1f0afdb4162e2d6a00d87c92bd53aa484429a5dff138e7f53db754b75a55137d999314f6c600eee3d88d661b6ab91a87b1bdd9f52a0317a7d9e43a
@@ -10,35 +10,33 @@ on:
10
10
 
11
11
  jobs:
12
12
  test:
13
- name: "GraphQL-Ruby ${{ matrix.graphql }} on Ruby ${{ matrix.ruby }} (use_client_id: ${{ matrix.client_id }}) Redis v${{ matrix.redis_version }}"
13
+ name: "GraphQL-Ruby ${{ matrix.graphql }} AnyCable ${{ matrix.anycable }} on Ruby ${{ matrix.ruby }} Redis ${{ matrix.redis_version }}"
14
+ # Skip running tests for local pull requests (use push event instead), run only for foreign ones
15
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login != github.event.pull_request.base.repo.owner.login
14
16
  runs-on: ubuntu-latest
15
17
  strategy:
16
18
  fail-fast: false
17
19
  matrix:
18
20
  include:
21
+ - ruby: "3.3"
22
+ graphql: '~> 2.3'
23
+ anycable: '~> 1.5'
24
+ redis_version: latest
25
+ - ruby: "3.2"
26
+ graphql: '~> 2.2.0'
27
+ anycable: '~> 1.4.0'
28
+ redis_version: '7.2'
19
29
  - ruby: "3.1"
20
30
  graphql: '~> 2.0.0'
21
- client_id: 'false'
22
- anycable_rails: '~> 1.3'
23
- redis_version: latest
24
- - ruby: "3.0"
25
- graphql: '~> 1.13.0'
26
- client_id: 'false'
27
- anycable_rails: '~> 1.2.0'
28
- redis_version: 5.0.4
29
- - ruby: 2.7
30
- graphql: '~> 1.12.0'
31
- client_id: 'true'
32
- anycable_rails: '~> 1.1.0'
33
- redis_version: 4.0.14
34
- container:
35
- image: ruby:${{ matrix.ruby }}
36
- env:
37
- CI: true
38
- GRAPHQL_RUBY_VERSION: ${{ matrix.graphql }}
39
- ANYCABLE_RAILS_VERSION: ${{ matrix.anycable_rails }}
40
- GRAPHQL_ANYCABLE_USE_CLIENT_PROVIDED_UNIQ_ID: ${{ matrix.client_id }}
41
- REDIS_URL: redis://redis:6379
31
+ anycable: '~> 1.3.0'
32
+ redis_version: '6.2'
33
+ env:
34
+ CI: true
35
+ GRAPHQL_RUBY_VERSION: ${{ matrix.graphql }}
36
+ ANYCABLE_VERSION: ${{ matrix.anycable }}
37
+ ANYCABLE_RAILS_VERSION: ${{ matrix.anycable }}
38
+ GRAPHQL_ANYCABLE_USE_CLIENT_PROVIDED_UNIQ_ID: ${{ matrix.client_id }}
39
+ REDIS_URL: redis://localhost:6379
42
40
  services:
43
41
  redis:
44
42
  image: redis:${{ matrix.redis_version }}
@@ -47,21 +45,13 @@ jobs:
47
45
  --health-interval 10s
48
46
  --health-timeout 5s
49
47
  --health-retries 5
48
+ ports:
49
+ - 6379:6379
50
50
  steps:
51
- - uses: actions/checkout@v3
52
- - uses: actions/cache@v3
51
+ - uses: actions/checkout@v4
52
+ - uses: ruby/setup-ruby@v1
53
53
  with:
54
- path: vendor/bundle
55
- key: bundle-${{ matrix.ruby }}-${{ matrix.graphql }}-${{ matrix.anycable }}-${{ hashFiles('**/*.gemspec') }}-${{ hashFiles('**/Gemfile') }}
56
- restore-keys: |
57
- bundle-${{ matrix.ruby }}-${{ matrix.graphql }}-${{ matrix.anycable }}-
58
- bundle-${{ matrix.ruby }}-
59
- - name: Upgrade Bundler to 2.0 (for older Rubies)
60
- run: gem install bundler -v '~> 2.0'
61
- - name: Bundle install
62
- run: |
63
- bundle config path vendor/bundle
64
- bundle install
65
- bundle update
54
+ ruby-version: ${{ matrix.ruby }}
55
+ bundler-cache: true
66
56
  - name: Run RSpec
67
57
  run: bundle exec rspec
data/CHANGELOG.md CHANGED
@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
 
8
8
  ## Unreleased
9
9
 
10
- ## 1.1.7 - 2024-05-08
10
+ ## 1.2.0 - 2024-05-07
11
11
 
12
12
  ### Changed
13
13
 
@@ -17,6 +17,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
17
17
 
18
18
  See https://github.com/anycable/graphql-anycable/issues/43 for details.
19
19
 
20
+ ### Removed
21
+
22
+ - Handling of client-provided channel identifiers. **BREAKING CHANGE**
23
+
24
+ Please make sure that you have changed your channel `disconnected` method to pass channel instance to GraphQL-AnyCable's `delete_channel_subscriptions` method.
25
+ See [release notes for version 1.1.0](https://github.com/anycable/graphql-anycable/releases/tag/v1.1.0) for details.
26
+
27
+ - Handling of pre-1.0 subscriptions data.
28
+
29
+ If you're still using version 0.5 or below, please upgrade to 1.0 or 1.1 first with `handle_legacy_subscriptions` setting enabled.
30
+ See [release notes for version 1.0.0](https://github.com/anycable/graphql-anycable/releases/tag/v1.0.0) for details.
31
+
20
32
  ## 1.1.6 - 2023-08-03
21
33
 
22
34
  ### Fixed
@@ -185,8 +197,6 @@ Technical release to test publishing via GitHub Actions.
185
197
 
186
198
  Initial version: store subscriptions on redis, re-execute queries in sync. [@Envek]
187
199
 
188
- [@ilyasgaraev]: https://github.com/ilyasgaraev "Ilyas Garaev"
189
- [@smasry]: https://github.com/smasry "Samer Masry"
190
200
  [@gsamokovarov]: https://github.com/gsamokovarov "Genadi Samokovarov"
191
201
  [@bibendi]: https://github.com/bibendi "Misha Merkushin"
192
202
  [@FX-HAO]: https://github.com/FX-HAO "Fuxin Hao"
data/Gemfile CHANGED
@@ -7,9 +7,9 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
7
7
  # Specify your gem's dependencies in graphql-anycable.gemspec
8
8
  gemspec
9
9
 
10
- gem "graphql", ENV.fetch("GRAPHQL_RUBY_VERSION", "~> 1.12")
11
- gem "anycable", ENV.fetch("ANYCABLE_VERSION", "~> 1.0")
12
- gem "anycable-rails", ENV.fetch("ANYCABLE_RAILS_VERSION", "~> 1.3")
10
+ gem "graphql", ENV.fetch("GRAPHQL_RUBY_VERSION", "~> 2.3")
11
+ gem "anycable", ENV.fetch("ANYCABLE_VERSION", "~> 1.5")
12
+ gem "anycable-rails", ENV.fetch("ANYCABLE_RAILS_VERSION", "~> 1.5")
13
13
 
14
14
  group :development, :test do
15
15
  gem "pry"
data/README.md CHANGED
@@ -140,8 +140,7 @@ GraphQL-AnyCable uses [anyway_config] to configure itself. There are several pos
140
140
  ```.env
141
141
  GRAPHQL_ANYCABLE_SUBSCRIPTION_EXPIRATION_SECONDS=604800
142
142
  GRAPHQL_ANYCABLE_USE_REDIS_OBJECT_ON_CLEANUP=true
143
- GRAPHQL_ANYCABLE_HANDLE_LEGACY_SUBSCRIPTIONS=false
144
- GRAPHQL_ANYCABLE_USE_CLIENT_PROVIDED_UNIQ_ID=false
143
+ GRAPHQL_ANYCABLE_REDIS_PREFIX=graphql
145
144
  ```
146
145
 
147
146
  2. YAML configuration files (note that this is `config/graphql_anycable.yml`, *not* `config/anycable.yml`):
@@ -151,8 +150,7 @@ GraphQL-AnyCable uses [anyway_config] to configure itself. There are several pos
151
150
  production:
152
151
  subscription_expiration_seconds: 300 # 5 minutes
153
152
  use_redis_object_on_cleanup: false # For restricted redis installations
154
- handle_legacy_subscriptions: false # For seamless upgrade from pre-1.0 versions
155
- use_client_provided_uniq_id: false # To avoid problems with non-uniqueness of Apollo channel identifiers
153
+ redis_prefix: graphql # You can configure redis_prefix for anycable-graphql subscription prefixes. Default value "graphql"
156
154
  ```
157
155
 
158
156
  3. Configuration from your application code:
@@ -160,11 +158,48 @@ GraphQL-AnyCable uses [anyway_config] to configure itself. There are several pos
160
158
  ```ruby
161
159
  GraphQL::AnyCable.configure do |config|
162
160
  config.subscription_expiration_seconds = 3600 # 1 hour
161
+ config.redis_prefix = "graphql" # on our side, we add `-` ourselves after the redis_prefix
163
162
  end
164
163
  ```
165
164
 
166
165
  And any other way provided by [anyway_config]. Check its documentation!
167
166
 
167
+ ## Emergency actions
168
+ In situations when you don't set `subscription_expiration_seconds`, have a lot of inactive subscriptions, and `GraphQL::AnyCable::Cleaner` does`t help in that,
169
+ you can do the following actions for clearing subscriptions
170
+
171
+ 1. Set `config.subscription_expiration_seconds`. After that, the new subscriptions will have `TTL`
172
+ 2. Run the script
173
+ ```ruby
174
+ redis = GraphQL::AnyCable.redis
175
+ config = GraphQL::AnyCable.config
176
+
177
+ # do it for subscriptions
178
+ redis.scan_each("graphql-subscription:*") do |key|
179
+ redis.expire(key, config.subscription_expiration_seconds) if redis.ttl(key) < 0
180
+ # or you can just remove it immediately
181
+ # redis.del(key) if redis.ttl(key) < 0
182
+ end
183
+
184
+ # do it for channels
185
+ redis.scan_each("graphql-channel:*") do |key|
186
+ redis.expire(key, config.subscription_expiration_seconds) if redis.ttl(key) < 0
187
+ # or you can just remove it immediately
188
+ # redis.del(key) if redis.ttl(key) < 0
189
+ end
190
+ ```
191
+
192
+ Or you can change the `redis_prefix` in the `configuration` and then remove all records with the old_prefix
193
+ For instance:
194
+
195
+ 1. Change the `redis_prefix`. The default `redis_prefix` is `graphql`
196
+ 2. Run the ruby script, which remove all records with `old prefix`
197
+ ```ruby
198
+ redis.scan_each("graphql-*") do |key|
199
+ redis.del(key)
200
+ end
201
+ ```
202
+
168
203
  ## Data model
169
204
 
170
205
  As in AnyCable there is no place to store subscription data in-memory, it should be persisted somewhere to be retrieved on `GraphQLSchema.subscriptions.trigger` and sent to subscribed clients. `graphql-anycable` uses the same Redis database as AnyCable itself.
@@ -183,13 +218,6 @@ As in AnyCable there is no place to store subscription data in-memory, it should
183
218
  => 52ee8d65-275e-4d22-94af-313129116388
184
219
  ```
185
220
 
186
- > For backward compatibility with pre-1.0 versions of this gem older `graphql-event:#{event.topic}` set containing subscription identifiers is also supported.
187
- >
188
- > ```
189
- > SMEMBERS graphql-event:1:myStats:
190
- > => 52ee8d65-275e-4d22-94af-313129116388
191
- > ```
192
-
193
221
  3. Subscription data: `graphql-subscription:#{subscription_id}` hash contains everything required to evaluate subscription on trigger and create data for client.
194
222
 
195
223
  ```
@@ -202,13 +230,112 @@ As in AnyCable there is no place to store subscription data in-memory, it should
202
230
  }
203
231
  ```
204
232
 
205
- 4. Channel subscriptions: `graphql-channel:#{channel_id}` set containing identifiers for subscriptions created in ActionCable channel to delete them on client disconnect.
233
+ 4. Channel subscriptions: `graphql-channel:#{subscription_id}` set containing identifiers for subscriptions created in ActionCable channel to delete them on client disconnect.
206
234
 
207
235
  ```
208
236
  SMEMBERS graphql-channel:17420c6ed9e
209
237
  => 52ee8d65-275e-4d22-94af-313129116388
210
238
  ```
211
239
 
240
+ ## Stats
241
+
242
+ You can grab Redis subscription statistics by calling
243
+
244
+ ```ruby
245
+ GraphQL::AnyCable.stats
246
+ ```
247
+
248
+ It will return a total of the amount of the key with the following prefixes
249
+
250
+ ```
251
+ graphql-subscription
252
+ graphql-fingerprints
253
+ graphql-subscriptions
254
+ graphql-channel
255
+ ```
256
+
257
+ The response will look like this
258
+
259
+ ```json
260
+ {
261
+ "total": {
262
+ "subscription":22646,
263
+ "fingerprints":3200,
264
+ "subscriptions":20101,
265
+ "channel": 4900
266
+ }
267
+ }
268
+ ```
269
+
270
+ You can also grab the number of subscribers grouped by subscriptions
271
+
272
+ ```ruby
273
+ GraphQL::AnyCable.stats(include_subscriptions: true)
274
+ ```
275
+
276
+ It will return the response that contains `subscriptions`
277
+
278
+ ```json
279
+ {
280
+ "total": {
281
+ "subscription":22646,
282
+ "fingerprints":3200,
283
+ "subscriptions":20101,
284
+ "channel": 4900
285
+ },
286
+ "subscriptions": {
287
+ "productCreated": 11323,
288
+ "productUpdated": 11323
289
+ }
290
+ }
291
+ ```
292
+
293
+ Also, you can set another `scan_count`, if needed.
294
+ The default value is 1_000
295
+
296
+ ```ruby
297
+ GraphQL::AnyCable.stats(scan_count: 100)
298
+ ```
299
+
300
+ We can set statistics data to [Yabeda][] for tracking amount of subscriptions
301
+
302
+ ```ruby
303
+ # config/initializers/metrics.rb
304
+ Yabeda.configure do
305
+ group :graphql_anycable_statistics do
306
+ gauge :subscriptions_count, comment: "Number of graphql-anycable subscriptions"
307
+ end
308
+ end
309
+ ```
310
+
311
+ ```ruby
312
+ # in your app
313
+ statistics = GraphQL::AnyCable.stats[:total]
314
+
315
+ statistics.each do |key , value|
316
+ Yabeda.graphql_anycable_statistics.subscriptions_count.set({name: key}, value)
317
+ end
318
+ ```
319
+
320
+ Or you can use `collect`
321
+ ```ruby
322
+ # config/initializers/metrics.rb
323
+ Yabeda.configure do
324
+ group :graphql_anycable_statistics do
325
+ gauge :subscriptions_count, comment: "Number of graphql-anycable subscriptions"
326
+ end
327
+
328
+ collect do
329
+ statistics = GraphQL::AnyCable.stats[:total]
330
+
331
+ statistics.each do |redis_prefix, value|
332
+ graphql_anycable_statistics.subscriptions_count.set({name: redis_prefix}, value)
333
+ end
334
+ end
335
+ end
336
+ ```
337
+
338
+
212
339
  ## Testing applications which use `graphql-anycable`
213
340
 
214
341
  You can pass custom redis-server URL to AnyCable using ENV variable.
@@ -268,3 +395,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
268
395
  [AnyCable]: https://github.com/anycable/anycable "Polyglot replacement for Ruby WebSocket servers with Action Cable protocol"
269
396
  [LiteCable]: https://github.com/palkan/litecable "Lightweight Action Cable implementation (Rails-free)"
270
397
  [anyway_config]: https://github.com/palkan/anyway_config "Ruby libraries and applications configuration on steroids!"
398
+ [Yabeda]: https://github.com/yabeda-rb/yabeda "Extendable solution for easy setup of monitoring in your Ruby apps"
@@ -8,7 +8,6 @@ module GraphQL
8
8
  def clean
9
9
  clean_channels
10
10
  clean_subscriptions
11
- clean_events
12
11
  clean_fingerprint_subscriptions
13
12
  clean_topic_fingerprints
14
13
  end
@@ -17,7 +16,7 @@ module GraphQL
17
16
  return unless config.subscription_expiration_seconds
18
17
  return unless config.use_redis_object_on_cleanup
19
18
 
20
- redis.scan_each(match: "#{adapter::CHANNEL_PREFIX}*") do |key|
19
+ redis.scan_each(match: "#{redis_key(adapter::CHANNEL_PREFIX)}*") do |key|
21
20
  idle = redis.object("IDLETIME", key)
22
21
  next if idle&.<= config.subscription_expiration_seconds
23
22
 
@@ -29,7 +28,7 @@ module GraphQL
29
28
  return unless config.subscription_expiration_seconds
30
29
  return unless config.use_redis_object_on_cleanup
31
30
 
32
- redis.scan_each(match: "#{adapter::SUBSCRIPTION_PREFIX}*") do |key|
31
+ redis.scan_each(match: "#{redis_key(adapter::SUBSCRIPTION_PREFIX)}*") do |key|
33
32
  idle = redis.object("IDLETIME", key)
34
33
  next if idle&.<= config.subscription_expiration_seconds
35
34
 
@@ -37,25 +36,10 @@ module GraphQL
37
36
  end
38
37
  end
39
38
 
40
- def clean_events
41
- return unless config.handle_legacy_subscriptions
42
-
43
- redis.scan_each(match: "#{adapter::SUBSCRIPTION_EVENTS_PREFIX}*") do |key|
44
- subscription_id = key.sub(/\A#{adapter::SUBSCRIPTION_EVENTS_PREFIX}/, "")
45
- next if redis.exists?(adapter::SUBSCRIPTION_PREFIX + subscription_id)
46
-
47
- redis.smembers(key).each do |event_topic|
48
- redis.srem(adapter::EVENT_PREFIX + event_topic, subscription_id)
49
- end
50
-
51
- redis.del(key)
52
- end
53
- end
54
-
55
39
  def clean_fingerprint_subscriptions
56
- redis.scan_each(match: "#{adapter::SUBSCRIPTIONS_PREFIX}*") do |key|
40
+ redis.scan_each(match: "#{redis_key(adapter::SUBSCRIPTIONS_PREFIX)}*") do |key|
57
41
  redis.smembers(key).each do |subscription_id|
58
- next if redis.exists?(adapter::SUBSCRIPTION_PREFIX + subscription_id)
42
+ next if redis.exists?(redis_key(adapter::SUBSCRIPTION_PREFIX) + subscription_id)
59
43
 
60
44
  redis.srem(key, subscription_id)
61
45
  end
@@ -63,10 +47,10 @@ module GraphQL
63
47
  end
64
48
 
65
49
  def clean_topic_fingerprints
66
- redis.scan_each(match: "#{adapter::FINGERPRINTS_PREFIX}*") do |key|
50
+ redis.scan_each(match: "#{redis_key(adapter::FINGERPRINTS_PREFIX)}*") do |key|
67
51
  redis.zremrangebyscore(key, '-inf', '0')
68
52
  redis.zrange(key, 0, -1).each do |fingerprint|
69
- next if redis.exists?(adapter::SUBSCRIPTIONS_PREFIX + fingerprint)
53
+ next if redis.exists?(redis_key(adapter::SUBSCRIPTIONS_PREFIX) + fingerprint)
70
54
 
71
55
  redis.zrem(key, fingerprint)
72
56
  end
@@ -86,6 +70,10 @@ module GraphQL
86
70
  def config
87
71
  GraphQL::AnyCable.config
88
72
  end
73
+
74
+ def redis_key(prefix)
75
+ "#{config.redis_prefix}-#{prefix}"
76
+ end
89
77
  end
90
78
  end
91
79
  end
@@ -10,8 +10,7 @@ module GraphQL
10
10
 
11
11
  attr_config subscription_expiration_seconds: nil
12
12
  attr_config use_redis_object_on_cleanup: true
13
- attr_config handle_legacy_subscriptions: false
14
- attr_config use_client_provided_uniq_id: true
13
+ attr_config redis_prefix: "graphql" # Here, we set clear redis_prefix without any hyphen. The hyphen is added at the end of this value on our side.
15
14
  end
16
15
  end
17
16
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module AnyCable
5
+ # Calculates amount of Graphql Redis keys
6
+ # (graphql-subscription, graphql-fingerprints, graphql-subscriptions, graphql-channel)
7
+ # Also, calculate the number of subscribers grouped by subscriptions
8
+ class Stats
9
+ SCAN_COUNT_RECORDS_AMOUNT = 1_000
10
+
11
+ attr_reader :scan_count, :include_subscriptions
12
+
13
+ def initialize(scan_count: SCAN_COUNT_RECORDS_AMOUNT, include_subscriptions: false)
14
+ @scan_count = scan_count
15
+ @include_subscriptions = include_subscriptions
16
+ end
17
+
18
+ def collect
19
+ total_subscriptions_result = {total: {}}
20
+
21
+ list_prefixes_keys.each do |name, prefix|
22
+ total_subscriptions_result[:total][name] = count_by_scan(match: "#{prefix}*")
23
+ end
24
+
25
+ if include_subscriptions
26
+ total_subscriptions_result[:subscriptions] = group_subscription_stats
27
+ end
28
+
29
+ total_subscriptions_result
30
+ end
31
+
32
+ private
33
+
34
+ # Counting all keys, that match the pattern with iterating by count
35
+ def count_by_scan(match:)
36
+ sb_amount = 0
37
+ cursor = '0'
38
+
39
+ loop do
40
+ cursor, result = redis.scan(cursor, match: match, count: scan_count)
41
+ sb_amount += result.count
42
+
43
+ break if cursor == '0'
44
+ end
45
+
46
+ sb_amount
47
+ end
48
+
49
+ # Calculate subscribes, grouped by subscriptions
50
+ def group_subscription_stats
51
+ subscription_groups = {}
52
+
53
+ redis.scan_each(match: "#{list_prefixes_keys[:fingerprints]}*", count: scan_count) do |fingerprint_key|
54
+ subscription_name = fingerprint_key.gsub(/#{list_prefixes_keys[:fingerprints]}|:/, "")
55
+ subscription_groups[subscription_name] = 0
56
+
57
+ redis.zscan_each(fingerprint_key) do |data|
58
+ redis.sscan_each("#{list_prefixes_keys[:subscriptions]}#{data[0]}") do |subscription_key|
59
+ next unless redis.exists?("#{list_prefixes_keys[:subscription]}#{subscription_key}")
60
+
61
+ subscription_groups[subscription_name] += 1
62
+ end
63
+ end
64
+ end
65
+
66
+ subscription_groups
67
+ end
68
+
69
+ def list_prefixes_keys
70
+ {
71
+ subscription: redis_key(adapter::SUBSCRIPTION_PREFIX),
72
+ fingerprints: redis_key(adapter::FINGERPRINTS_PREFIX),
73
+ subscriptions: redis_key(adapter::SUBSCRIPTIONS_PREFIX),
74
+ channel: redis_key(adapter::CHANNEL_PREFIX)
75
+ }
76
+ end
77
+
78
+ def adapter
79
+ GraphQL::Subscriptions::AnyCableSubscriptions
80
+ end
81
+
82
+ def redis
83
+ GraphQL::AnyCable.redis
84
+ end
85
+
86
+ def config
87
+ GraphQL::AnyCable.config
88
+ end
89
+
90
+ def redis_key(prefix)
91
+ "#{config.redis_prefix}-#{prefix}"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -18,11 +18,6 @@ namespace :graphql do
18
18
  GraphQL::AnyCable::Cleaner.clean_subscriptions
19
19
  end
20
20
 
21
- # Clean up legacy subscription_ids from events for expired subscriptions
22
- task :events do
23
- GraphQL::AnyCable::Cleaner.clean_events
24
- end
25
-
26
21
  # Clean up subscription_ids from event fingerprints for expired subscriptions
27
22
  task :fingerprint_subscriptions do
28
23
  GraphQL::AnyCable::Cleaner.clean_fingerprint_subscriptions
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module AnyCable
5
- VERSION = "1.1.7"
5
+ VERSION = "1.2.0"
6
6
  end
7
7
  end
@@ -56,13 +56,10 @@ module GraphQL
56
56
 
57
57
  def_delegators :"GraphQL::AnyCable", :redis, :config
58
58
 
59
- SUBSCRIPTION_PREFIX = "graphql-subscription:" # HASH: Stores subscription data: query, context, …
60
- FINGERPRINTS_PREFIX = "graphql-fingerprints:" # ZSET: To get fingerprints by topic
61
- SUBSCRIPTIONS_PREFIX = "graphql-subscriptions:" # SET: To get subscriptions by fingerprint
62
- CHANNEL_PREFIX = "graphql-channel:" # SET: Auxiliary structure for whole channel's subscriptions cleanup
63
- # For backward compatibility:
64
- EVENT_PREFIX = "graphql-event:"
65
- SUBSCRIPTION_EVENTS_PREFIX = "graphql-subscription-events:"
59
+ SUBSCRIPTION_PREFIX = "subscription:" # HASH: Stores subscription data: query, context, …
60
+ FINGERPRINTS_PREFIX = "fingerprints:" # ZSET: To get fingerprints by topic
61
+ SUBSCRIPTIONS_PREFIX = "subscriptions:" # SET: To get subscriptions by fingerprint
62
+ CHANNEL_PREFIX = "channel:" # SET: Auxiliary structure for whole channel's subscriptions cleanup
66
63
 
67
64
  # @param serializer [<#dump(obj), #load(string)] Used for serializing messages before handing them to `.broadcast(msg)`
68
65
  def initialize(serializer: Serialize, **rest)
@@ -73,15 +70,13 @@ module GraphQL
73
70
  # An event was triggered.
74
71
  # Re-evaluate all subscribed queries and push the data over ActionCable.
75
72
  def execute_all(event, object)
76
- execute_legacy(event, object) if config.handle_legacy_subscriptions
77
-
78
- fingerprints = redis.zrange(FINGERPRINTS_PREFIX + event.topic, 0, -1)
73
+ fingerprints = redis.zrange(redis_key(FINGERPRINTS_PREFIX) + event.topic, 0, -1)
79
74
  return if fingerprints.empty?
80
75
 
81
76
  fingerprint_subscription_ids = Hash[fingerprints.zip(
82
77
  redis.pipelined do |pipeline|
83
78
  fingerprints.map do |fingerprint|
84
- pipeline.smembers(SUBSCRIPTIONS_PREFIX + fingerprint)
79
+ pipeline.smembers(redis_key(SUBSCRIPTIONS_PREFIX) + fingerprint)
85
80
  end
86
81
  end
87
82
  )]
@@ -99,25 +94,14 @@ module GraphQL
99
94
  def execute_grouped(fingerprint, subscription_ids, event, object)
100
95
  return if subscription_ids.empty?
101
96
 
102
- subscription_id = subscription_ids.find { |sid| redis.exists?(SUBSCRIPTION_PREFIX + sid) }
97
+ subscription_id = subscription_ids.find { |sid| redis.exists?(redis_key(SUBSCRIPTION_PREFIX) + sid) }
103
98
  return unless subscription_id # All subscriptions has expired but haven't cleaned up yet
104
99
 
105
100
  result = execute_update(subscription_id, event, object)
106
101
  return unless result
107
102
 
108
103
  # Having calculated the result _once_, send the same payload to all subscribers
109
- deliver(SUBSCRIPTIONS_PREFIX + fingerprint, result)
110
- end
111
-
112
- # For migration from pre-1.0 graphql-anycable gem
113
- def execute_legacy(event, object)
114
- redis.smembers(EVENT_PREFIX + event.topic).each do |subscription_id|
115
- next unless redis.exists?(SUBSCRIPTION_PREFIX + subscription_id)
116
- result = execute_update(subscription_id, event, object)
117
- next unless result
118
-
119
- deliver(SUBSCRIPTION_PREFIX + subscription_id, result)
120
- end
104
+ deliver(redis_key(SUBSCRIPTIONS_PREFIX) + fingerprint, result)
121
105
  end
122
106
 
123
107
  # Disable this method as there is no fingerprint (it can be retrieved from subscription though)
@@ -142,14 +126,11 @@ module GraphQL
142
126
 
143
127
  raise GraphQL::AnyCable::ChannelConfigurationError unless channel
144
128
 
145
- channel_uniq_id = config.use_client_provided_uniq_id? ? channel.params["channelId"] : subscription_id
146
-
147
129
  # Store subscription_id in the channel state to cleanup on disconnect
148
- write_subscription_id(channel, channel_uniq_id)
149
-
130
+ write_subscription_id(channel, subscription_id)
150
131
 
151
132
  events.each do |event|
152
- channel.stream_from(SUBSCRIPTIONS_PREFIX + event.fingerprint)
133
+ channel.stream_from(redis_key(SUBSCRIPTIONS_PREFIX) + event.fingerprint)
153
134
  end
154
135
 
155
136
  data = {
@@ -161,22 +142,22 @@ module GraphQL
161
142
  }
162
143
 
163
144
  redis.multi do |pipeline|
164
- pipeline.sadd(CHANNEL_PREFIX + channel_uniq_id, [subscription_id])
165
- pipeline.mapped_hmset(SUBSCRIPTION_PREFIX + subscription_id, data)
145
+ pipeline.sadd(redis_key(CHANNEL_PREFIX) + subscription_id, [subscription_id])
146
+ pipeline.mapped_hmset(redis_key(SUBSCRIPTION_PREFIX) + subscription_id, data)
166
147
  events.each do |event|
167
- pipeline.zincrby(FINGERPRINTS_PREFIX + event.topic, 1, event.fingerprint)
168
- pipeline.sadd(SUBSCRIPTIONS_PREFIX + event.fingerprint, [subscription_id])
148
+ pipeline.zincrby(redis_key(FINGERPRINTS_PREFIX) + event.topic, 1, event.fingerprint)
149
+ pipeline.sadd(redis_key(SUBSCRIPTIONS_PREFIX) + event.fingerprint, [subscription_id])
169
150
  end
170
151
  next unless config.subscription_expiration_seconds
171
- pipeline.expire(CHANNEL_PREFIX + channel_uniq_id, config.subscription_expiration_seconds)
172
- pipeline.expire(SUBSCRIPTION_PREFIX + subscription_id, config.subscription_expiration_seconds)
152
+ pipeline.expire(redis_key(CHANNEL_PREFIX) + subscription_id, config.subscription_expiration_seconds)
153
+ pipeline.expire(redis_key(SUBSCRIPTION_PREFIX) + subscription_id, config.subscription_expiration_seconds)
173
154
  end
174
155
  end
175
156
 
176
157
  # Return the query from "storage" (in redis)
177
158
  def read_subscription(subscription_id)
178
159
  redis.mapped_hmget(
179
- "#{SUBSCRIPTION_PREFIX}#{subscription_id}",
160
+ "#{redis_key(SUBSCRIPTION_PREFIX)}#{subscription_id}",
180
161
  :query_string, :variables, :context, :operation_name
181
162
  ).tap do |subscription|
182
163
  return if subscription.values.all?(&:nil?) # Redis returns hash with all nils for missing key
@@ -188,17 +169,17 @@ module GraphQL
188
169
  end
189
170
 
190
171
  def delete_subscription(subscription_id)
191
- events = redis.hget(SUBSCRIPTION_PREFIX + subscription_id, :events)
172
+ events = redis.hget(redis_key(SUBSCRIPTION_PREFIX) + subscription_id, :events)
192
173
  events = events ? JSON.parse(events) : {}
193
174
  fingerprint_subscriptions = {}
194
175
  redis.pipelined do |pipeline|
195
176
  events.each do |topic, fingerprint|
196
- pipeline.srem(SUBSCRIPTIONS_PREFIX + fingerprint, subscription_id)
197
- score = pipeline.zincrby(FINGERPRINTS_PREFIX + topic, -1, fingerprint)
198
- fingerprint_subscriptions[FINGERPRINTS_PREFIX + topic] = score
177
+ pipeline.srem(redis_key(SUBSCRIPTIONS_PREFIX) + fingerprint, subscription_id)
178
+ score = pipeline.zincrby(redis_key(FINGERPRINTS_PREFIX) + topic, -1, fingerprint)
179
+ fingerprint_subscriptions[redis_key(FINGERPRINTS_PREFIX) + topic] = score
199
180
  end
200
181
  # Delete subscription itself
201
- pipeline.del(SUBSCRIPTION_PREFIX + subscription_id)
182
+ pipeline.del(redis_key(SUBSCRIPTION_PREFIX) + subscription_id)
202
183
  end
203
184
  # Clean up fingerprints that doesn't have any subscriptions left
204
185
  redis.pipelined do |pipeline|
@@ -206,33 +187,21 @@ module GraphQL
206
187
  pipeline.zremrangebyscore(key, '-inf', '0') if score.value.zero?
207
188
  end
208
189
  end
209
- delete_legacy_subscription(subscription_id)
210
- end
211
-
212
- def delete_legacy_subscription(subscription_id)
213
- return unless config.handle_legacy_subscriptions
214
-
215
- events = redis.smembers(SUBSCRIPTION_EVENTS_PREFIX + subscription_id)
216
- redis.pipelined do
217
- events.each do |event_topic|
218
- redis.srem(EVENT_PREFIX + event_topic, subscription_id)
219
- end
220
- redis.del(SUBSCRIPTION_EVENTS_PREFIX + subscription_id)
221
- end
222
190
  end
223
191
 
224
192
  # The channel was closed, forget about it and its subscriptions
225
- def delete_channel_subscriptions(channel_or_id)
226
- # For backward compatibility
227
- channel_id = channel_or_id.is_a?(String) ? channel_or_id : read_subscription_id(channel_or_id)
193
+ def delete_channel_subscriptions(channel)
194
+ raise(ArgumentError, "Please pass channel instance to #{__method__} in your #unsubscribed method") if channel.is_a?(String)
195
+
196
+ channel_id = read_subscription_id(channel)
228
197
 
229
198
  # Missing in case disconnect happens before #execute
230
199
  return unless channel_id
231
200
 
232
- redis.smembers(CHANNEL_PREFIX + channel_id).each do |subscription_id|
201
+ redis.smembers(redis_key(CHANNEL_PREFIX) + channel_id).each do |subscription_id|
233
202
  delete_subscription(subscription_id)
234
203
  end
235
- redis.del(CHANNEL_PREFIX + channel_id)
204
+ redis.del(redis_key(CHANNEL_PREFIX) + channel_id)
236
205
  end
237
206
 
238
207
  private
@@ -268,6 +237,10 @@ module GraphQL
268
237
  channel.connection.socket.istate
269
238
  end
270
239
  end
240
+
241
+ def redis_key(prefix)
242
+ "#{config.redis_prefix}-#{prefix}"
243
+ end
271
244
  end
272
245
  end
273
246
  end
@@ -6,20 +6,19 @@ require_relative "graphql/anycable/version"
6
6
  require_relative "graphql/anycable/cleaner"
7
7
  require_relative "graphql/anycable/config"
8
8
  require_relative "graphql/anycable/railtie" if defined?(Rails)
9
+ require_relative "graphql/anycable/stats"
9
10
  require_relative "graphql/subscriptions/anycable_subscriptions"
10
11
 
11
12
  module GraphQL
12
13
  module AnyCable
13
14
  def self.use(schema, **options)
14
- if config.use_client_provided_uniq_id?
15
- warn "[Deprecated] Using client provided channel uniq IDs could lead to unexpected behaviour, " \
16
- "please, set GraphQL::AnyCable.config.use_client_provided_uniq_id = false or GRAPHQL_ANYCABLE_USE_CLIENT_PROVIDED_UNIQ_ID=false, " \
17
- "and update the `#unsubscribed` callback code according to the latest docs."
18
- end
19
-
20
15
  schema.use GraphQL::Subscriptions::AnyCableSubscriptions, **options
21
16
  end
22
17
 
18
+ def self.stats(**options)
19
+ Stats.new(**options).collect
20
+ end
21
+
23
22
  module_function
24
23
 
25
24
  def redis
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-anycable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.7
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Novikov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-08 00:00:00.000000000 Z
11
+ date: 2024-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anycable-core
@@ -191,6 +191,7 @@ files:
191
191
  - lib/graphql/anycable/config.rb
192
192
  - lib/graphql/anycable/errors.rb
193
193
  - lib/graphql/anycable/railtie.rb
194
+ - lib/graphql/anycable/stats.rb
194
195
  - lib/graphql/anycable/tasks/clean_expired_subscriptions.rake
195
196
  - lib/graphql/anycable/version.rb
196
197
  - lib/graphql/subscriptions/anycable_subscriptions.rb