graphql-anycable 1.3.1 → 1.3.2

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: eebb61e4181214e0f86bdf0cc33e7153e0a02c6c8de5e37996ca9457e6231385
4
- data.tar.gz: 6fa0f37c132f8c22454bcdf9d995ac9b0c3f40c3913b0047f7be3b91eb68d07f
3
+ metadata.gz: 14133c519749fc9821bc5e9af2c28978b620afe77a7ec03cb26a51f41ef56c66
4
+ data.tar.gz: c8f8ee0f3c378bc72b80f4df1623e2ab402dc379097af315422b742abe09e17b
5
5
  SHA512:
6
- metadata.gz: 06b6b55507a369100a71e9a30b93cf0fccef1d82c948b71693e43a4629116749453577aa0919d5de12c3bad8f6703d9652b0378c6ad90c4775ab355fe9b4ce7d
7
- data.tar.gz: d0ed4156e37e936fad396659517245b1f4540451e4b4012ec8d1478cc4c1b1d244c254b690b168f4d8d2d36a3aadf1c22643c5cde639b08c119c8ec801d97152
6
+ metadata.gz: 667c1ecce687b40287dbd930144480cc7a14e4e663ca2ab7cd29538b586d4b2e3d4fdae38a0d3b4f37187ef7d2d25aa7b1f6112d89d79db481efc79f46d49c01
7
+ data.tar.gz: fc431ad886ce163e17bd1d257491f7964fd1140ebe89365ecdca7c7f5d66c661408c51bea51a3e36bc26c1b380d421b5d7d8ab0814fb6a94bd1a0ead45bad595
@@ -24,10 +24,10 @@ jobs:
24
24
  env:
25
25
  BUNDLE_GEMFILE: "gemfiles/rubocop.gemfile"
26
26
  steps:
27
- - uses: actions/checkout@v4
27
+ - uses: actions/checkout@v6
28
28
  - uses: ruby/setup-ruby@v1
29
29
  with:
30
- ruby-version: 3.1
30
+ ruby-version: 3.4
31
31
  bundler-cache: true
32
32
  - name: Lint Ruby code with RuboCop
33
33
  run: |
@@ -13,7 +13,7 @@ jobs:
13
13
  id-token: write
14
14
  packages: write
15
15
  steps:
16
- - uses: actions/checkout@v4
16
+ - uses: actions/checkout@v6
17
17
  with:
18
18
  fetch-depth: 0 # Fetch current tag as annotated. See https://github.com/actions/checkout/issues/290
19
19
  - uses: ruby/setup-ruby@v1
@@ -18,6 +18,10 @@ jobs:
18
18
  fail-fast: false
19
19
  matrix:
20
20
  include:
21
+ - ruby: "4.0"
22
+ graphql: '~> 2.3'
23
+ anycable: '~> 1.6'
24
+ redis_version: latest
21
25
  - ruby: "3.3"
22
26
  graphql: '~> 2.3'
23
27
  anycable: '~> 1.5'
@@ -52,7 +56,7 @@ jobs:
52
56
  ports:
53
57
  - 6379:6379
54
58
  steps:
55
- - uses: actions/checkout@v4
59
+ - uses: actions/checkout@v6
56
60
  - uses: ruby/setup-ruby@v1
57
61
  with:
58
62
  ruby-version: ${{ matrix.ruby }}
data/.rubocop/rspec.yml CHANGED
@@ -1,4 +1,4 @@
1
- require:
1
+ plugins:
2
2
  - rubocop-rspec
3
3
 
4
4
  # Disable all cops by default,
data/.rubocop.yml CHANGED
@@ -2,8 +2,7 @@ inherit_mode:
2
2
  merge:
3
3
  - Exclude
4
4
 
5
- require:
6
- - standard
5
+ plugins:
7
6
  - standard-custom
8
7
  - standard-performance
9
8
  - rubocop-performance
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 1.3.2 - 2026-08-05
11
+
12
+ ### Fixed
13
+
14
+ - Cleanup tasks are much faster and use bounded memory: `SCAN`/`SSCAN`/`ZSCAN` are called with a configurable `COUNT` (new `redis_scan_count` setting, default `1000`) instead of Redis' default of 10, members are read incrementally with `SSCAN`/`ZSCAN` instead of `SMEMBERS`/`ZRANGE` (which materialized entire sets in the rake process), and the per-member `EXISTS`, `OBJECT IDLETIME`, `SREM`, `ZREM` and `DEL` calls are pipelined in batches instead of being issued one round trip at a time. [@jjb] ([#56](https://github.com/anycable/graphql-anycable/pull/56))
15
+
16
+ ## 1.3.1 - 2025-03-29
17
+
18
+ ### Fixed
19
+
20
+ - Remove `clean:events` from `clean_expired_subscriptions.rake`. [@Geesu] ([#46](https://github.com/anycable/graphql-anycable/pull/46))
21
+
10
22
  ## 1.3.0 - 2024-08-13
11
23
 
12
24
  ### Changed
@@ -211,6 +223,7 @@ Technical release to test publishing via GitHub Actions.
211
223
 
212
224
  Initial version: store subscriptions on redis, re-execute queries in sync. [@Envek]
213
225
 
226
+ [@jjb]: https://github.com/jjb "John Bachir"
214
227
  [@prog-supdex]: https://github.com/prog-supdex "Igor Platonov"
215
228
  [@ilyasgaraev]: https://github.com/ilyasgaraev "Ilyas Garaev"
216
229
  [@smasry]: https://github.com/smasry "Samer Masry"
data/README.md CHANGED
@@ -5,9 +5,9 @@ A (mostly) drop-in replacement for default ActionCable subscriptions adapter shi
5
5
  [![Gem Version](https://badge.fury.io/rb/graphql-anycable.svg)](https://badge.fury.io/rb/graphql-anycable)
6
6
  [![Tests](https://github.com/anycable/graphql-anycable/actions/workflows/test.yml/badge.svg)](https://github.com/anycable/graphql-anycable/actions/workflows/test.yml)
7
7
 
8
- <a href="https://evilmartians.com/?utm_source=graphql-anycable&utm_campaign=project_page">
9
- <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
10
- </a>
8
+ <br/>
9
+
10
+ <img src="https://cdn.evilmartians.com/badges/logo-no-label.svg" alt="Evil Martians logo" width="22" height="16" /> <b>GraphQL-AnyCable</b> is built by <b><a href="https://evilmartians.com/">Evil Martians</a></b>, an American design and engineering consultancy for <b>developer tools, AI, and cybersecurity startups</b>.
11
11
 
12
12
  ## Why?
13
13
 
@@ -134,6 +134,10 @@ To avoid filling Redis storage with stale subscription data:
134
134
 
135
135
  Heroku users should set up `use_redis_object_on_cleanup` setting to `false` due to [limitations in Heroku Redis](https://devcenter.heroku.com/articles/heroku-redis#connection-permissions).
136
136
 
137
+ The task is also available as granular sub-tasks, so you can run only some of them, or run them on different schedules: `rake graphql:anycable:clean:channels`, `rake graphql:anycable:clean:subscriptions`, `rake graphql:anycable:clean:fingerprint_subscriptions`, `rake graphql:anycable:clean:topic_fingerprints`.
138
+
139
+ Cleanup iterates over Redis with the `SCAN` family of commands and pipelines the checks it performs on each batch. Use the `redis_scan_count` setting to tune the batch size: larger values mean fewer round trips (so faster cleanup), at the cost of doing more work per Redis call.
140
+
137
141
  ## Configuration
138
142
 
139
143
  GraphQL-AnyCable uses [anyway_config] to configure itself. There are several possibilities to configure this gem:
@@ -144,6 +148,7 @@ GraphQL-AnyCable uses [anyway_config] to configure itself. There are several pos
144
148
  GRAPHQL_ANYCABLE_SUBSCRIPTION_EXPIRATION_SECONDS=604800
145
149
  GRAPHQL_ANYCABLE_USE_REDIS_OBJECT_ON_CLEANUP=true
146
150
  GRAPHQL_ANYCABLE_REDIS_PREFIX=graphql
151
+ GRAPHQL_ANYCABLE_REDIS_SCAN_COUNT=1000
147
152
  ```
148
153
 
149
154
  2. YAML configuration files (note that this is `config/graphql_anycable.yml`, *not* `config/anycable.yml`):
@@ -154,6 +159,7 @@ GraphQL-AnyCable uses [anyway_config] to configure itself. There are several pos
154
159
  subscription_expiration_seconds: 300 # 5 minutes
155
160
  use_redis_object_on_cleanup: false # For restricted redis installations
156
161
  redis_prefix: graphql # You can configure redis_prefix for anycable-graphql subscription prefixes. Default value "graphql"
162
+ redis_scan_count: 1000 # Batch size used by the cleanup rake tasks. Default value 1000
157
163
  ```
158
164
 
159
165
  3. Configuration from your application code:
@@ -1,5 +1,4 @@
1
- source "https://rubygems.org" do
2
- gem "standard", "~> 1.28"
1
+ source "https://rubygems.org"
3
2
 
4
- gem "rubocop-rspec"
5
- end
3
+ gem "standard"
4
+ gem "rubocop-rspec"
@@ -17,6 +17,10 @@ Gem::Specification.new do |spec|
17
17
  spec.homepage = "https://github.com/Envek/graphql-anycable"
18
18
  spec.license = "MIT"
19
19
 
20
+ spec.metadata = {
21
+ "rubygems_mfa_required" => "true"
22
+ }
23
+
20
24
  # Specify which files should be added to the gem when it is released.
21
25
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
26
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
@@ -34,7 +38,7 @@ Gem::Specification.new do |spec|
34
38
  spec.add_dependency "redis", ">= 4.2.0"
35
39
 
36
40
  spec.add_development_dependency "anycable-rails"
37
- spec.add_development_dependency "bundler", "~> 2.0"
41
+ spec.add_development_dependency "bundler", ">= 2.0"
38
42
  spec.add_development_dependency "rack"
39
43
  spec.add_development_dependency "railties"
40
44
  spec.add_development_dependency "rake", ">= 12.3.3"
@@ -16,37 +16,22 @@ module GraphQL
16
16
  return unless config.subscription_expiration_seconds
17
17
  return unless config.use_redis_object_on_cleanup
18
18
 
19
- AnyCable.with_redis do |redis|
20
- redis.scan_each(match: "#{redis_key(adapter::CHANNEL_PREFIX)}*") do |key|
21
- idle = redis.object("IDLETIME", key)
22
- next if idle&.<= config.subscription_expiration_seconds
23
-
24
- redis.del(key)
25
- end
26
- end
19
+ clean_idle_keys(adapter::CHANNEL_PREFIX)
27
20
  end
28
21
 
29
22
  def clean_subscriptions
30
23
  return unless config.subscription_expiration_seconds
31
24
  return unless config.use_redis_object_on_cleanup
32
25
 
33
- AnyCable.with_redis do |redis|
34
- redis.scan_each(match: "#{redis_key(adapter::SUBSCRIPTION_PREFIX)}*") do |key|
35
- idle = redis.object("IDLETIME", key)
36
- next if idle&.<= config.subscription_expiration_seconds
37
-
38
- redis.del(key)
39
- end
40
- end
26
+ clean_idle_keys(adapter::SUBSCRIPTION_PREFIX)
41
27
  end
42
28
 
43
29
  def clean_fingerprint_subscriptions
44
30
  AnyCable.with_redis do |redis|
45
- redis.scan_each(match: "#{redis_key(adapter::SUBSCRIPTIONS_PREFIX)}*") do |key|
46
- redis.smembers(key).each do |subscription_id|
47
- next if redis.exists?(redis_key(adapter::SUBSCRIPTION_PREFIX) + subscription_id)
48
-
49
- redis.srem(key, subscription_id)
31
+ each_key(redis, adapter::SUBSCRIPTIONS_PREFIX) do |key|
32
+ each_batch(redis.sscan_each(key, count: redis_scan_count)) do |subscription_ids|
33
+ stale = missing_key_ids(redis, adapter::SUBSCRIPTION_PREFIX, subscription_ids)
34
+ redis.srem(key, stale) unless stale.empty?
50
35
  end
51
36
  end
52
37
  end
@@ -54,12 +39,13 @@ module GraphQL
54
39
 
55
40
  def clean_topic_fingerprints
56
41
  AnyCable.with_redis do |redis|
57
- redis.scan_each(match: "#{redis_key(adapter::FINGERPRINTS_PREFIX)}*") do |key|
42
+ each_key(redis, adapter::FINGERPRINTS_PREFIX) do |key|
58
43
  redis.zremrangebyscore(key, "-inf", "0")
59
- redis.zrange(key, 0, -1).each do |fingerprint|
60
- next if redis.exists?(redis_key(adapter::SUBSCRIPTIONS_PREFIX) + fingerprint)
61
44
 
62
- redis.zrem(key, fingerprint)
45
+ each_batch(redis.zscan_each(key, count: redis_scan_count)) do |members|
46
+ fingerprints = members.map(&:first)
47
+ stale = missing_key_ids(redis, adapter::SUBSCRIPTIONS_PREFIX, fingerprints)
48
+ redis.zrem(key, stale) unless stale.empty?
63
49
  end
64
50
  end
65
51
  end
@@ -67,6 +53,39 @@ module GraphQL
67
53
 
68
54
  private
69
55
 
56
+ def clean_idle_keys(prefix)
57
+ AnyCable.with_redis do |redis|
58
+ each_batch(redis.scan_each(match: "#{redis_key(prefix)}*", count: redis_scan_count)) do |keys|
59
+ idle_times = redis.pipelined do |pipeline|
60
+ keys.each { |key| pipeline.object("IDLETIME", key) }
61
+ end
62
+
63
+ expired = keys.reject.with_index { |_key, index| idle_times[index]&.<= config.subscription_expiration_seconds }
64
+ redis.del(*expired) unless expired.empty?
65
+ end
66
+ end
67
+ end
68
+
69
+ # Iterates over the keys matching the given prefix, one key at a time.
70
+ def each_key(redis, prefix, &block)
71
+ redis.scan_each(match: "#{redis_key(prefix)}*", count: redis_scan_count, &block)
72
+ end
73
+
74
+ # Consumes a lazy enumerator (SCAN family) in batches to keep memory usage bounded.
75
+ def each_batch(enumerator, &block)
76
+ enumerator.each_slice(redis_scan_count, &block)
77
+ end
78
+
79
+ # Returns the ids which have no corresponding key in redis anymore, checking them in a single round trip.
80
+ def missing_key_ids(redis, prefix, ids)
81
+ exists = redis.pipelined do |pipeline|
82
+ ids.each { |id| pipeline.exists?(redis_key(prefix) + id) }
83
+ end
84
+
85
+ # Reject ids that has corresponding key in Redis in `exists` array of pipelined responses.
86
+ ids.reject.with_index { |_id, index| exists[index] }
87
+ end
88
+
70
89
  def adapter
71
90
  GraphQL::Subscriptions::AnyCableSubscriptions
72
91
  end
@@ -75,6 +94,10 @@ module GraphQL
75
94
  GraphQL::AnyCable.config
76
95
  end
77
96
 
97
+ def redis_scan_count
98
+ config.redis_scan_count.to_i
99
+ end
100
+
78
101
  def redis_key(prefix)
79
102
  "#{config.redis_prefix}-#{prefix}"
80
103
  end
@@ -11,6 +11,7 @@ module GraphQL
11
11
  attr_config subscription_expiration_seconds: nil
12
12
  attr_config use_redis_object_on_cleanup: true
13
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.
14
+ attr_config redis_scan_count: 1_000 # COUNT for SCAN commands and batch size for pipelined commands during cleanup.
14
15
  end
15
16
  end
16
17
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module AnyCable
5
- VERSION = "1.3.1"
5
+ VERSION = "1.3.2"
6
6
  end
7
7
  end
@@ -4,8 +4,6 @@ require "anycable"
4
4
  require "graphql/subscriptions"
5
5
  require "graphql/anycable/errors"
6
6
 
7
- # rubocop: disable Metrics/AbcSize, Metrics/LineLength, Metrics/MethodLength
8
-
9
7
  # A subscriptions implementation that sends data as AnyCable broadcastings.
10
8
  #
11
9
  # Since AnyCable is aimed to be compatible with ActionCable, this adapter
@@ -249,4 +247,3 @@ module GraphQL
249
247
  end
250
248
  end
251
249
  end
252
- # rubocop: enable Metrics/AbcSize, Metrics/LineLength, Metrics/MethodLength
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.3.1
4
+ version: 1.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Novikov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-03-28 00:00:00.000000000 Z
11
+ date: 2026-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anycable-core
@@ -96,14 +96,14 @@ dependencies:
96
96
  name: bundler
97
97
  requirement: !ruby/object:Gem::Requirement
98
98
  requirements:
99
- - - "~>"
99
+ - - ">="
100
100
  - !ruby/object:Gem::Version
101
101
  version: '2.0'
102
102
  type: :development
103
103
  prerelease: false
104
104
  version_requirements: !ruby/object:Gem::Requirement
105
105
  requirements:
106
- - - "~>"
106
+ - - ">="
107
107
  - !ruby/object:Gem::Version
108
108
  version: '2.0'
109
109
  - !ruby/object:Gem::Dependency
@@ -202,7 +202,8 @@ files:
202
202
  homepage: https://github.com/Envek/graphql-anycable
203
203
  licenses:
204
204
  - MIT
205
- metadata: {}
205
+ metadata:
206
+ rubygems_mfa_required: 'true'
206
207
  post_install_message:
207
208
  rdoc_options: []
208
209
  require_paths: