graphql-anycable 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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