graphql-anycable 0.1.0 → 0.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: c494ac593becba11ed50d2ffe1af1c16afa91e4dec8393b59a4d3e17fedaf926
4
- data.tar.gz: 15682a1ca82073419efb27600383443f0db2bbf2288b3b494d77512eace7096e
3
+ metadata.gz: cc4ead83b0cc39fdf211c97e4664932a6125ca48b6e30ea889ab4692c075bb23
4
+ data.tar.gz: 89ff31b340ed13fbe0493245556a587692f3f6211a72422c0dbee750c8e48135
5
5
  SHA512:
6
- metadata.gz: b630183f77e927586116388aa8c975bbb04242ca956a8dfcb10453afa7c6d1ba324a3eaca85cb6237d855dab4fd612ae269d380ecc0a5c3e786186c1836b0067
7
- data.tar.gz: b35bd5536f6d6460aa97af5d69aa0fab63962111a53854f429796e70d8b5b9831a550f9d19698853e4bd7b551096d90a653bd616a3f6100a7b54bc592126adee
6
+ metadata.gz: d705c044762c1bbf2baa763ac8fcfee2a45f3bfbe0c9ec7ab8a94710af255769793eb9e2932b84edc800ee00fbcaf37ecf3b4acba1e6d475b172d5773c2b4ae0
7
+ data.tar.gz: be40d20ca4ce9b35f37501646029c8bceafba7617a8d236ac66aa963f4b4f22196ea753597bfae124f357a91441ed937016766b454cc56ff649c6dbb25a40fe6
data/README.md CHANGED
@@ -97,6 +97,13 @@ Or install it yourself as:
97
97
  MySchema.subscriptions.trigger(:product_updated, {}, Product.first!, scope: account.id)
98
98
  ```
99
99
 
100
+ ## Operations
101
+
102
+ To avoid filling Redis storage with stale subscription data:
103
+
104
+ 1. Set `GRAPHQL_ANYCABLE_SUBSCRIPTION_EXPIRATION_SECONDS` environment variable to number of seconds (e.g. `604800` for 1 week). See [anyway_config] documentation to other ways of configuring this gem.
105
+ 2. Execute `rake graphql:anycable:clean_expired_subscriptions` once in a while to clean up stale subscription data
106
+
100
107
  ## Development
101
108
 
102
109
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -114,3 +121,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
114
121
  [graphql gem]: https://github.com/rmosolgo/graphql-ruby "Ruby implementation of GraphQL"
115
122
  [AnyCable]: https://github.com/anycable/anycable "Polyglot replacement for Ruby WebSocket servers with Action Cable protocol"
116
123
  [LiteCable]: https://github.com/palkan/litecable "Lightweight Action Cable implementation (Rails-free)"
124
+ [anyway_config]: https://github.com/palkan/anyway_config "Ruby libraries and applications configuration on steroids!"
@@ -26,8 +26,9 @@ Gem::Specification.new do |spec|
26
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
27
  spec.require_paths = ["lib"]
28
28
 
29
- spec.add_dependency "anycable", "~> 0.5"
30
- spec.add_dependency "graphql", "~> 1.8"
29
+ spec.add_dependency "anycable", "~> 0.5"
30
+ spec.add_dependency "anyway_config", "~> 1.3"
31
+ spec.add_dependency "graphql", "~> 1.8"
31
32
 
32
33
  spec.add_development_dependency "bundler", "~> 1.16"
33
34
  spec.add_development_dependency "fakeredis"
data/lib/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "graphql-anycable"
4
+
5
+ Dir.glob("#{File.expand_path(__dir__)}/tasks/**/*.rake").each { |f| import f }
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway"
4
+
5
+ module Graphql
6
+ module Anycable
7
+ class Config < Anyway::Config
8
+ config_name :graphql_anycable
9
+ env_prefix :graphql_anycable
10
+
11
+ attr_config subscription_expiration_seconds: nil
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module Graphql
6
+ module Anycable
7
+ class Railtie < ::Rails::Railtie
8
+ rake_tasks do
9
+ path = File.expand_path(__dir__)
10
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :graphql do
4
+ namespace :anycable do
5
+ task :clean_expired_subscriptions do
6
+ config = Graphql::Anycable::Config.new
7
+ unless config.subscription_expiration_seconds
8
+ warn "GraphQL::Anycable: No expiration set for subscriptions!"
9
+ next
10
+ end
11
+
12
+ redis = Anycable::PubSub.new.redis_conn
13
+ klass = GraphQL::Subscriptions::AnyCableSubscriptions
14
+
15
+ # 1. Clean up old channels
16
+ redis.scan_each(match: "#{klass::CHANNEL_PREFIX}*") do |key|
17
+ idle = redis.object("IDLETIME", key)
18
+ next if idle&.<= config.subscription_expiration_seconds
19
+ redis.del(key)
20
+ end
21
+
22
+ # 2. Clean up old subscriptions (they should have expired by themselves)
23
+ redis.scan_each(match: "#{klass::SUBSCRIPTION_PREFIX}*") do |key|
24
+ idle = redis.object("IDLETIME", key)
25
+ next if idle&.<= config.subscription_expiration_seconds
26
+ redis.del(key)
27
+ end
28
+
29
+ # 3. Clean up subscription_ids from events for expired subscriptions
30
+ redis.scan_each(match: "#{klass::SUBSCRIPTION_EVENTS_PREFIX}*") do |key|
31
+ subscription_id = key.sub(/\A#{klass::SUBSCRIPTION_EVENTS_PREFIX}/, "")
32
+ next if redis.exists(klass::SUBSCRIPTION_PREFIX + subscription_id)
33
+ redis.smembers(key).each do |event_topic|
34
+ redis.srem(klass::EVENT_PREFIX + event_topic, subscription_id)
35
+ end
36
+ redis.del(key)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Graphql
4
4
  module Anycable
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -53,6 +53,7 @@ module GraphQL
53
53
  class Subscriptions
54
54
  class AnyCableSubscriptions < GraphQL::Subscriptions
55
55
  SUBSCRIPTION_PREFIX = "graphql-subscription:"
56
+ SUBSCRIPTION_EVENTS_PREFIX = "graphql-subscription-events:"
56
57
  EVENT_PREFIX = "graphql-event:"
57
58
  CHANNEL_PREFIX = "graphql-channel:"
58
59
 
@@ -66,6 +67,7 @@ module GraphQL
66
67
  # Re-evaluate all subscribed queries and push the data over ActionCable.
67
68
  def execute_all(event, object)
68
69
  redis.smembers(EVENT_PREFIX + event.topic).each do |subscription_id|
70
+ next unless redis.exists(SUBSCRIPTION_PREFIX + subscription_id)
69
71
  execute(subscription_id, event, object)
70
72
  end
71
73
  end
@@ -90,15 +92,18 @@ module GraphQL
90
92
  variables: query.provided_variables.to_json,
91
93
  context: @serializer.dump(context.to_h),
92
94
  operation_name: query.operation_name,
93
- events: events.map(&:topic).to_json,
94
95
  }
95
96
 
96
97
  redis.multi do
97
98
  redis.sadd(CHANNEL_PREFIX + channel.params["channelId"], subscription_id)
98
99
  redis.mapped_hmset(SUBSCRIPTION_PREFIX + subscription_id, data)
100
+ redis.sadd(SUBSCRIPTION_EVENTS_PREFIX + subscription_id, *events.map(&:topic))
99
101
  events.each do |event|
100
102
  redis.sadd(EVENT_PREFIX + event.topic, subscription_id)
101
103
  end
104
+ next unless config.subscription_expiration_seconds
105
+ redis.expire(CHANNEL_PREFIX + channel.params["channelId"], config.subscription_expiration_seconds)
106
+ redis.expire(SUBSCRIPTION_PREFIX + subscription_id, config.subscription_expiration_seconds)
102
107
  end
103
108
  end
104
109
 
@@ -116,11 +121,12 @@ module GraphQL
116
121
  # The channel was closed, forget about it.
117
122
  def delete_subscription(subscription_id)
118
123
  # Remove subscription ids from all events
119
- events_data = redis.hget(SUBSCRIPTION_PREFIX + subscription_id, :events)
120
- events_data && JSON.parse(events_data).each do |event_topic|
124
+ events = redis.smembers(SUBSCRIPTION_EVENTS_PREFIX + subscription_id)
125
+ events.each do |event_topic|
121
126
  redis.srem(EVENT_PREFIX + event_topic, subscription_id)
122
127
  end
123
128
  # Delete subscription itself
129
+ redis.del(SUBSCRIPTION_EVENTS_PREFIX + subscription_id)
124
130
  redis.del(SUBSCRIPTION_PREFIX + subscription_id)
125
131
  end
126
132
 
@@ -140,6 +146,10 @@ module GraphQL
140
146
  def redis
141
147
  @redis ||= anycable.redis_conn
142
148
  end
149
+
150
+ def config
151
+ @config ||= Graphql::Anycable::Config.new
152
+ end
143
153
  end
144
154
  end
145
155
  end
@@ -3,6 +3,8 @@
3
3
  require "graphql"
4
4
 
5
5
  require_relative "graphql/anycable/version"
6
+ require_relative "graphql/anycable/config"
7
+ require_relative "graphql/anycable/railtie" if defined?(Rails)
6
8
  require_relative "graphql/subscriptions/anycable_subscriptions"
7
9
 
8
10
  module Graphql
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: 0.1.0
4
+ version: 0.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: 2018-08-25 00:00:00.000000000 Z
11
+ date: 2018-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anycable
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: anyway_config
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: graphql
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -112,8 +126,12 @@ files:
112
126
  - bin/console
113
127
  - bin/setup
114
128
  - graphql-anycable.gemspec
129
+ - lib/Rakefile
115
130
  - lib/graphql-anycable.rb
116
131
  - lib/graphql/anycable.rb
132
+ - lib/graphql/anycable/config.rb
133
+ - lib/graphql/anycable/railtie.rb
134
+ - lib/graphql/anycable/tasks/clean_expired_subscriptions.rake
117
135
  - lib/graphql/anycable/version.rb
118
136
  - lib/graphql/subscriptions/anycable_subscriptions.rb
119
137
  homepage: https://github.com/Envek/graphql-anycable