graphql-anycable 1.3.1 → 1.3.3
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/.github/workflows/lint.yml +2 -2
- data/.github/workflows/release.yml +1 -1
- data/.github/workflows/test.yml +5 -1
- data/.rubocop/rspec.yml +1 -1
- data/.rubocop.yml +1 -2
- data/CHANGELOG.md +21 -0
- data/README.md +12 -5
- data/gemfiles/rubocop.gemfile +3 -4
- data/graphql-anycable.gemspec +5 -1
- data/lib/graphql/anycable/cleaner.rb +112 -25
- data/lib/graphql/anycable/config.rb +1 -0
- data/lib/graphql/anycable/version.rb +1 -1
- data/lib/graphql/subscriptions/anycable_subscriptions.rb +12 -10
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba22f385767198acc338809a6b60237248dfc15ecdd2f279a31628e6dad013e8
|
|
4
|
+
data.tar.gz: 6e59e6c8119969ee8358219b080abecb05df38d6ffe5549adbc58869f214872d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3479edd8cf0548d649ca3a0aaaeea728a8030c22342b50cb41beacca14b2ab7391bc9fc1cd2783ca861a07c8049b4ef1256e735fc97468469372213a822c9201
|
|
7
|
+
data.tar.gz: f6595e5f63dd71d375ec8d90816ae102839a8fda29bfc3f3400af10dedbcbf0cd4fb587d1462233b1a1b733801dfa5af8f172fc1f0906c4b4ba4db6e5250eebf
|
data/.github/workflows/lint.yml
CHANGED
|
@@ -24,10 +24,10 @@ jobs:
|
|
|
24
24
|
env:
|
|
25
25
|
BUNDLE_GEMFILE: "gemfiles/rubocop.gemfile"
|
|
26
26
|
steps:
|
|
27
|
-
- uses: actions/checkout@
|
|
27
|
+
- uses: actions/checkout@v6
|
|
28
28
|
- uses: ruby/setup-ruby@v1
|
|
29
29
|
with:
|
|
30
|
-
ruby-version: 3.
|
|
30
|
+
ruby-version: 3.4
|
|
31
31
|
bundler-cache: true
|
|
32
32
|
- name: Lint Ruby code with RuboCop
|
|
33
33
|
run: |
|
data/.github/workflows/test.yml
CHANGED
|
@@ -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@
|
|
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
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 1.3.3 - 2026-08-10
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Subscriptions are no longer leaked when a channel carries more than one of them. The first subscription's id now names the channel and later subscriptions are added to that same set. On disconnect, the channel's set is read and all subscriptions in it are deleted instead of only the most recent one as before. [@jjb] ([#58](https://github.com/anycable/graphql-anycable/pull/58))
|
|
15
|
+
|
|
16
|
+
- Further optimize cleanup tasks with pipelining their reads and removals across keys. [@jjb] ([#57](https://github.com/anycable/graphql-anycable/pull/57))
|
|
17
|
+
|
|
18
|
+
## 1.3.2 - 2026-08-05
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- 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))
|
|
23
|
+
|
|
24
|
+
## 1.3.1 - 2025-03-29
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Remove `clean:events` from `clean_expired_subscriptions.rake`. [@Geesu] ([#46](https://github.com/anycable/graphql-anycable/pull/46))
|
|
29
|
+
|
|
10
30
|
## 1.3.0 - 2024-08-13
|
|
11
31
|
|
|
12
32
|
### Changed
|
|
@@ -211,6 +231,7 @@ Technical release to test publishing via GitHub Actions.
|
|
|
211
231
|
|
|
212
232
|
Initial version: store subscriptions on redis, re-execute queries in sync. [@Envek]
|
|
213
233
|
|
|
234
|
+
[@jjb]: https://github.com/jjb "John Bachir"
|
|
214
235
|
[@prog-supdex]: https://github.com/prog-supdex "Igor Platonov"
|
|
215
236
|
[@ilyasgaraev]: https://github.com/ilyasgaraev "Ilyas Garaev"
|
|
216
237
|
[@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
|
[](https://badge.fury.io/rb/graphql-anycable)
|
|
6
6
|
[](https://github.com/anycable/graphql-anycable/actions/workflows/test.yml)
|
|
7
7
|
|
|
8
|
-
<
|
|
9
|
-
|
|
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:
|
|
@@ -240,11 +246,12 @@ As in AnyCable there is no place to store subscription data in-memory, it should
|
|
|
240
246
|
}
|
|
241
247
|
```
|
|
242
248
|
|
|
243
|
-
4. Channel subscriptions: `graphql-channel:#{
|
|
249
|
+
4. Channel subscriptions: `graphql-channel:#{channel_id}` set containing identifiers for all subscriptions created on one ActionCable channel, used to delete them on client disconnect. The channel is named after its first subscription's id (stored in the channel's AnyCable state); every later subscription on the same channel is added to that same set.
|
|
244
250
|
|
|
245
251
|
```sh
|
|
246
|
-
SMEMBERS graphql-channel:
|
|
252
|
+
SMEMBERS graphql-channel:52ee8d65-275e-4d22-94af-313129116388
|
|
247
253
|
=> 52ee8d65-275e-4d22-94af-313129116388
|
|
254
|
+
a1b2c3d4-5678-90ab-cdef-111213141516
|
|
248
255
|
```
|
|
249
256
|
|
|
250
257
|
## Stats
|
data/gemfiles/rubocop.gemfile
CHANGED
data/graphql-anycable.gemspec
CHANGED
|
@@ -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", "
|
|
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,30 @@ module GraphQL
|
|
|
16
16
|
return unless config.subscription_expiration_seconds
|
|
17
17
|
return unless config.use_redis_object_on_cleanup
|
|
18
18
|
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
redis
|
|
47
|
-
|
|
31
|
+
each_key_batch(redis, adapter::SUBSCRIPTIONS_PREFIX) do |keys|
|
|
32
|
+
bulk_readable, oversized = partition_by_size(redis, keys, :scard)
|
|
33
|
+
|
|
34
|
+
each_capped_chunk(bulk_readable) do |chunk|
|
|
35
|
+
members = redis.pipelined { |pipeline| chunk.each { |key| pipeline.smembers(key) } }
|
|
36
|
+
remove_stale(redis, adapter::SUBSCRIPTION_PREFIX, chunk.zip(members), :srem)
|
|
37
|
+
end
|
|
48
38
|
|
|
49
|
-
|
|
39
|
+
oversized.each do |key|
|
|
40
|
+
each_batch(redis.sscan_each(key, count: redis_scan_count)) do |subscription_ids|
|
|
41
|
+
remove_stale(redis, adapter::SUBSCRIPTION_PREFIX, [[key, subscription_ids]], :srem)
|
|
42
|
+
end
|
|
50
43
|
end
|
|
51
44
|
end
|
|
52
45
|
end
|
|
@@ -54,12 +47,20 @@ module GraphQL
|
|
|
54
47
|
|
|
55
48
|
def clean_topic_fingerprints
|
|
56
49
|
AnyCable.with_redis do |redis|
|
|
57
|
-
redis
|
|
58
|
-
redis.zremrangebyscore(key, "-inf", "0")
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
each_key_batch(redis, adapter::FINGERPRINTS_PREFIX) do |keys|
|
|
51
|
+
redis.pipelined { |pipeline| keys.each { |key| pipeline.zremrangebyscore(key, "-inf", "0") } }
|
|
52
|
+
|
|
53
|
+
bulk_readable, oversized = partition_by_size(redis, keys, :zcard)
|
|
54
|
+
|
|
55
|
+
each_capped_chunk(bulk_readable) do |chunk|
|
|
56
|
+
fingerprints = redis.pipelined { |pipeline| chunk.each { |key| pipeline.zrange(key, 0, -1) } }
|
|
57
|
+
remove_stale(redis, adapter::SUBSCRIPTIONS_PREFIX, chunk.zip(fingerprints), :zrem)
|
|
58
|
+
end
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
oversized.each do |key|
|
|
61
|
+
each_batch(redis.zscan_each(key, count: redis_scan_count)) do |members|
|
|
62
|
+
remove_stale(redis, adapter::SUBSCRIPTIONS_PREFIX, [[key, members.map(&:first)]], :zrem)
|
|
63
|
+
end
|
|
63
64
|
end
|
|
64
65
|
end
|
|
65
66
|
end
|
|
@@ -67,6 +68,88 @@ module GraphQL
|
|
|
67
68
|
|
|
68
69
|
private
|
|
69
70
|
|
|
71
|
+
def clean_idle_keys(prefix)
|
|
72
|
+
AnyCable.with_redis do |redis|
|
|
73
|
+
each_key_batch(redis, prefix) do |keys|
|
|
74
|
+
idle_times = redis.pipelined do |pipeline|
|
|
75
|
+
keys.each { |key| pipeline.object("IDLETIME", key) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
expired = keys.reject.with_index { |_key, index| idle_times[index]&.<= config.subscription_expiration_seconds }
|
|
79
|
+
redis.del(*expired) unless expired.empty?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Splits keys into those small enough to read in one go and those that have to be iterated
|
|
85
|
+
# with a cursor. Most collections hold only a handful of members, and reading many of them
|
|
86
|
+
# per round trip is what keeps cleanup bearable when there are a lot of keys; the rare huge
|
|
87
|
+
# ones still have to be iterated, or they would blow up memory in the process.
|
|
88
|
+
def partition_by_size(redis, keys, size_command)
|
|
89
|
+
sizes = redis.pipelined do |pipeline|
|
|
90
|
+
keys.each { |key| pipeline.public_send(size_command, key) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
bulk_readable, oversized = keys.zip(sizes).partition { |_key, size| size <= redis_scan_count }
|
|
94
|
+
[bulk_readable, oversized.map(&:first)]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Yields key chunks whose combined size stays within redis_scan_count.
|
|
98
|
+
def each_capped_chunk(sized_keys)
|
|
99
|
+
chunk, total = [], 0
|
|
100
|
+
|
|
101
|
+
sized_keys.each do |key, size|
|
|
102
|
+
if chunk.any? && total + size > redis_scan_count
|
|
103
|
+
yield chunk
|
|
104
|
+
chunk, total = [], 0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
chunk << key
|
|
108
|
+
total += size
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
yield chunk if chunk.any?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Removes the members which no longer have a corresponding key in redis, given `[key, members]`
|
|
115
|
+
# pairs for one or more collections. Takes two round trips for the whole lot, however many
|
|
116
|
+
# collections and members it is handed: one for the existence checks and one for the removals.
|
|
117
|
+
def remove_stale(redis, prefix, members_by_key, remove_command)
|
|
118
|
+
pairs = members_by_key.flat_map { |key, members| members.map { |member| [key, member] } }
|
|
119
|
+
return if pairs.empty?
|
|
120
|
+
|
|
121
|
+
stale = missing_key_pairs(redis, prefix, pairs)
|
|
122
|
+
return if stale.empty?
|
|
123
|
+
|
|
124
|
+
redis.pipelined do |pipeline|
|
|
125
|
+
stale.group_by(&:first).each do |key, key_pairs|
|
|
126
|
+
pipeline.public_send(remove_command, key, key_pairs.map(&:last))
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Iterates over the keys matching the given prefix in batches, to allow pipelining.
|
|
132
|
+
def each_key_batch(redis, prefix, &block)
|
|
133
|
+
each_batch(redis.scan_each(match: "#{redis_key(prefix)}*", count: redis_scan_count), &block)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Consumes a lazy enumerator (SCAN family) in batches to keep memory usage bounded.
|
|
137
|
+
def each_batch(enumerator, &block)
|
|
138
|
+
enumerator.each_slice(redis_scan_count, &block)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns the `[key, member]` pairs whose member has no corresponding key in redis anymore,
|
|
142
|
+
# checking all of them in a single round trip. The member cannot be looked up on its own,
|
|
143
|
+
# since the same member may well appear in more than one collection.
|
|
144
|
+
def missing_key_pairs(redis, prefix, pairs)
|
|
145
|
+
exists = redis.pipelined do |pipeline|
|
|
146
|
+
pairs.each { |_key, member| pipeline.exists?(redis_key(prefix) + member) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Reject pairs whose member has a corresponding key in Redis in `exists` array of pipelined responses.
|
|
150
|
+
pairs.reject.with_index { |_pair, index| exists[index] }
|
|
151
|
+
end
|
|
152
|
+
|
|
70
153
|
def adapter
|
|
71
154
|
GraphQL::Subscriptions::AnyCableSubscriptions
|
|
72
155
|
end
|
|
@@ -75,6 +158,10 @@ module GraphQL
|
|
|
75
158
|
GraphQL::AnyCable.config
|
|
76
159
|
end
|
|
77
160
|
|
|
161
|
+
def redis_scan_count
|
|
162
|
+
config.redis_scan_count.to_i
|
|
163
|
+
end
|
|
164
|
+
|
|
78
165
|
def redis_key(prefix)
|
|
79
166
|
"#{config.redis_prefix}-#{prefix}"
|
|
80
167
|
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
|
|
@@ -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
|
|
@@ -129,8 +127,12 @@ module GraphQL
|
|
|
129
127
|
|
|
130
128
|
raise GraphQL::AnyCable::ChannelConfigurationError unless channel
|
|
131
129
|
|
|
132
|
-
# Store
|
|
133
|
-
|
|
130
|
+
# Store the channel's id in its state to cleanup on disconnect. A channel may carry more
|
|
131
|
+
# than one subscription, so the first subscription's id names the channel and every later
|
|
132
|
+
# subscription is added to that same set: otherwise each new subscription would overwrite
|
|
133
|
+
# the stored id, and #delete_channel_subscriptions could only ever find the last one.
|
|
134
|
+
channel_id = read_channel_id(channel) || subscription_id
|
|
135
|
+
write_channel_id(channel, channel_id)
|
|
134
136
|
|
|
135
137
|
events.each do |event|
|
|
136
138
|
channel.stream_from(redis_key(SUBSCRIPTIONS_PREFIX) + event.fingerprint)
|
|
@@ -146,14 +148,14 @@ module GraphQL
|
|
|
146
148
|
|
|
147
149
|
with_redis do |redis|
|
|
148
150
|
redis.multi do |pipeline|
|
|
149
|
-
pipeline.sadd(redis_key(CHANNEL_PREFIX) +
|
|
151
|
+
pipeline.sadd(redis_key(CHANNEL_PREFIX) + channel_id, [subscription_id])
|
|
150
152
|
pipeline.mapped_hmset(redis_key(SUBSCRIPTION_PREFIX) + subscription_id, data)
|
|
151
153
|
events.each do |event|
|
|
152
154
|
pipeline.zincrby(redis_key(FINGERPRINTS_PREFIX) + event.topic, 1, event.fingerprint)
|
|
153
155
|
pipeline.sadd(redis_key(SUBSCRIPTIONS_PREFIX) + event.fingerprint, [subscription_id])
|
|
154
156
|
end
|
|
155
157
|
next unless config.subscription_expiration_seconds
|
|
156
|
-
pipeline.expire(redis_key(CHANNEL_PREFIX) +
|
|
158
|
+
pipeline.expire(redis_key(CHANNEL_PREFIX) + channel_id, config.subscription_expiration_seconds)
|
|
157
159
|
pipeline.expire(redis_key(SUBSCRIPTION_PREFIX) + subscription_id, config.subscription_expiration_seconds)
|
|
158
160
|
end
|
|
159
161
|
end
|
|
@@ -179,7 +181,7 @@ module GraphQL
|
|
|
179
181
|
def delete_channel_subscriptions(channel)
|
|
180
182
|
raise(ArgumentError, "Please pass channel instance to #{__method__} in your #unsubscribed method") if channel.is_a?(String)
|
|
181
183
|
|
|
182
|
-
channel_id =
|
|
184
|
+
channel_id = read_channel_id(channel)
|
|
183
185
|
|
|
184
186
|
# Missing in case disconnect happens before #execute
|
|
185
187
|
return unless channel_id
|
|
@@ -215,7 +217,8 @@ module GraphQL
|
|
|
215
217
|
|
|
216
218
|
private
|
|
217
219
|
|
|
218
|
-
|
|
220
|
+
# Channel cleanup key kept in AnyCable istate as "sid"
|
|
221
|
+
def read_channel_id(channel)
|
|
219
222
|
return channel.instance_variable_get(:@__sid__) if channel.instance_variable_defined?(:@__sid__)
|
|
220
223
|
|
|
221
224
|
istate = fetch_channel_istate(channel)
|
|
@@ -225,7 +228,7 @@ module GraphQL
|
|
|
225
228
|
channel.instance_variable_set(:@__sid__, istate["sid"])
|
|
226
229
|
end
|
|
227
230
|
|
|
228
|
-
def
|
|
231
|
+
def write_channel_id(channel, val)
|
|
229
232
|
channel.connection.anycable_socket.istate["sid"] = val
|
|
230
233
|
channel.instance_variable_set(:@__sid__, val)
|
|
231
234
|
end
|
|
@@ -249,4 +252,3 @@ module GraphQL
|
|
|
249
252
|
end
|
|
250
253
|
end
|
|
251
254
|
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.
|
|
4
|
+
version: 1.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrey Novikov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-08-10 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:
|