Skip to content

Commit 7c89931

Browse files
anirbanmupokonski
authored andcommittedJan 21, 2020
Limit number of redis connections and fix ttl bug (#75)
* Attempting a quick workaround for gush creating a massive number of redis connection. With this design, each thread has its own redis connection (which is replaced if for some reason a new redis url is seen (should be unlikely)) * Set TTL on job key based on class not name (name includes uuid which doesn't exist in redis). Added tests to verify as well. * Clean up dependencies and fix travis CI
1 parent 682c7fe commit 7c89931

File tree

5 files changed

+54
-61
lines changed

5 files changed

+54
-61
lines changed
 

‎.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ tmp
1919
test.rb
2020
/Gushfile
2121
dump.rdb
22+
.ruby-version
23+
.ruby-gemset

‎.travis.yml

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ rvm:
44
- 2.2.2
55
- 2.3.4
66
- 2.4.1
7+
- 2.5
8+
- 2.6
9+
- 2.7
710
services:
811
- redis-server
912
email:

‎gush.gemspec

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
1818
spec.require_paths = ["lib"]
1919

2020
spec.add_dependency "activejob", ">= 4.2.7", "< 6.0"
21-
spec.add_dependency "connection_pool", "~> 2.2.1"
21+
spec.add_dependency "concurrent-ruby", "~> 1.0"
2222
spec.add_dependency "multi_json", "~> 1.11"
2323
spec.add_dependency "redis", ">= 3.2", "< 5"
2424
spec.add_dependency "redis-mutex", "~> 4.0.1"
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
2828
spec.add_dependency "colorize", "~> 0.7"
2929
spec.add_dependency "thor", "~> 0.19"
3030
spec.add_dependency "launchy", "~> 2.4"
31-
spec.add_development_dependency "bundler", "~> 1.5"
31+
spec.add_development_dependency "bundler"
3232
spec.add_development_dependency "rake", "~> 10.4"
3333
spec.add_development_dependency "rspec", '~> 3.0'
3434
spec.add_development_dependency "pry", '~> 0.10'

‎lib/gush/client.rb

+39-57
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1-
require 'connection_pool'
1+
require 'redis'
2+
require 'concurrent-ruby'
23

34
module Gush
45
class Client
56
attr_reader :configuration
67

8+
@@redis_connection = Concurrent::ThreadLocalVar.new(nil)
9+
10+
def self.redis_connection(config)
11+
cached = (@@redis_connection.value ||= { url: config.redis_url, connection: nil })
12+
return cached[:connection] if !cached[:connection].nil? && config.redis_url == cached[:url]
13+
14+
Redis.new(url: config.redis_url).tap do |instance|
15+
RedisClassy.redis = instance
16+
@@redis_connection.value = { url: config.redis_url, connection: instance }
17+
end
18+
end
19+
720
def initialize(config = Gush.configuration)
821
@configuration = config
922
end
@@ -47,9 +60,7 @@ def next_free_job_id(workflow_id, job_klass)
4760

4861
loop do
4962
job_id = SecureRandom.uuid
50-
available = connection_pool.with do |redis|
51-
!redis.hexists("gush.jobs.#{workflow_id}.#{job_klass}", job_id)
52-
end
63+
available = !redis.hexists("gush.jobs.#{workflow_id}.#{job_klass}", job_id)
5364

5465
break if available
5566
end
@@ -61,9 +72,7 @@ def next_free_workflow_id
6172
id = nil
6273
loop do
6374
id = SecureRandom.uuid
64-
available = connection_pool.with do |redis|
65-
!redis.exists("gush.workflow.#{id}")
66-
end
75+
available = !redis.exists("gush.workflow.#{id}")
6776

6877
break if available
6978
end
@@ -72,37 +81,31 @@ def next_free_workflow_id
7281
end
7382

7483
def all_workflows
75-
connection_pool.with do |redis|
76-
redis.scan_each(match: "gush.workflows.*").map do |key|
77-
id = key.sub("gush.workflows.", "")
78-
find_workflow(id)
79-
end
84+
redis.scan_each(match: "gush.workflows.*").map do |key|
85+
id = key.sub("gush.workflows.", "")
86+
find_workflow(id)
8087
end
8188
end
8289

8390
def find_workflow(id)
84-
connection_pool.with do |redis|
85-
data = redis.get("gush.workflows.#{id}")
91+
data = redis.get("gush.workflows.#{id}")
8692

87-
unless data.nil?
88-
hash = Gush::JSON.decode(data, symbolize_keys: true)
89-
keys = redis.scan_each(match: "gush.jobs.#{id}.*")
93+
unless data.nil?
94+
hash = Gush::JSON.decode(data, symbolize_keys: true)
95+
keys = redis.scan_each(match: "gush.jobs.#{id}.*")
9096

91-
nodes = keys.each_with_object([]) do |key, array|
92-
array.concat redis.hvals(key).map { |json| Gush::JSON.decode(json, symbolize_keys: true) }
93-
end
94-
95-
workflow_from_hash(hash, nodes)
96-
else
97-
raise WorkflowNotFound.new("Workflow with given id doesn't exist")
97+
nodes = keys.each_with_object([]) do |key, array|
98+
array.concat redis.hvals(key).map { |json| Gush::JSON.decode(json, symbolize_keys: true) }
9899
end
100+
101+
workflow_from_hash(hash, nodes)
102+
else
103+
raise WorkflowNotFound.new("Workflow with given id doesn't exist")
99104
end
100105
end
101106

102107
def persist_workflow(workflow)
103-
connection_pool.with do |redis|
104-
redis.set("gush.workflows.#{workflow.id}", workflow.to_json)
105-
end
108+
redis.set("gush.workflows.#{workflow.id}", workflow.to_json)
106109

107110
workflow.jobs.each {|job| persist_job(workflow.id, job) }
108111
workflow.mark_as_persisted
@@ -111,9 +114,7 @@ def persist_workflow(workflow)
111114
end
112115

113116
def persist_job(workflow_id, job)
114-
connection_pool.with do |redis|
115-
redis.hset("gush.jobs.#{workflow_id}.#{job.klass}", job.id, job.to_json)
116-
end
117+
redis.hset("gush.jobs.#{workflow_id}.#{job.klass}", job.id, job.to_json)
117118
end
118119

119120
def find_job(workflow_id, job_name)
@@ -132,31 +133,23 @@ def find_job(workflow_id, job_name)
132133
end
133134

134135
def destroy_workflow(workflow)
135-
connection_pool.with do |redis|
136-
redis.del("gush.workflows.#{workflow.id}")
137-
end
136+
redis.del("gush.workflows.#{workflow.id}")
138137
workflow.jobs.each {|job| destroy_job(workflow.id, job) }
139138
end
140139

141140
def destroy_job(workflow_id, job)
142-
connection_pool.with do |redis|
143-
redis.del("gush.jobs.#{workflow_id}.#{job.klass}")
144-
end
141+
redis.del("gush.jobs.#{workflow_id}.#{job.klass}")
145142
end
146143

147144
def expire_workflow(workflow, ttl=nil)
148145
ttl = ttl || configuration.ttl
149-
connection_pool.with do |redis|
150-
redis.expire("gush.workflows.#{workflow.id}", ttl)
151-
end
146+
redis.expire("gush.workflows.#{workflow.id}", ttl)
152147
workflow.jobs.each {|job| expire_job(workflow.id, job, ttl) }
153148
end
154149

155150
def expire_job(workflow_id, job, ttl=nil)
156151
ttl = ttl || configuration.ttl
157-
connection_pool.with do |redis|
158-
redis.expire("gush.jobs.#{workflow_id}.#{job.name}", ttl)
159-
end
152+
redis.expire("gush.jobs.#{workflow_id}.#{job.klass}", ttl)
160153
end
161154

162155
def enqueue_job(workflow_id, job)
@@ -172,16 +165,11 @@ def enqueue_job(workflow_id, job)
172165
def find_job_by_klass_and_id(workflow_id, job_name)
173166
job_klass, job_id = job_name.split('|')
174167

175-
connection_pool.with do |redis|
176-
redis.hget("gush.jobs.#{workflow_id}.#{job_klass}", job_id)
177-
end
168+
redis.hget("gush.jobs.#{workflow_id}.#{job_klass}", job_id)
178169
end
179170

180171
def find_job_by_klass(workflow_id, job_name)
181-
new_cursor, result = connection_pool.with do |redis|
182-
redis.hscan("gush.jobs.#{workflow_id}.#{job_name}", 0, count: 1)
183-
end
184-
172+
new_cursor, result = redis.hscan("gush.jobs.#{workflow_id}.#{job_name}", 0, count: 1)
185173
return nil if result.empty?
186174

187175
job_id, job = *result[0]
@@ -202,14 +190,8 @@ def workflow_from_hash(hash, nodes = [])
202190
flow
203191
end
204192

205-
def build_redis
206-
Redis.new(url: configuration.redis_url).tap do |instance|
207-
RedisClassy.redis = instance
208-
end
209-
end
210-
211-
def connection_pool
212-
@connection_pool ||= ConnectionPool.new(size: configuration.concurrency, timeout: 1) { build_redis }
193+
def redis
194+
self.class.redis_connection(configuration)
213195
end
214196
end
215197
end

‎spec/gush/client_spec.rb

+8-2
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,18 @@
9595
end
9696

9797
describe "#expire_workflow" do
98+
let(:ttl) { 2000 }
99+
98100
it "sets TTL for all Redis keys related to the workflow" do
99101
workflow = TestWorkflow.create
100102

101-
client.expire_workflow(workflow, -1)
103+
client.expire_workflow(workflow, ttl)
104+
105+
expect(redis.ttl("gush.workflows.#{workflow.id}")).to eq(ttl)
102106

103-
# => TODO - I believe fakeredis does not handle TTL the same.
107+
workflow.jobs.each do |job|
108+
expect(redis.ttl("gush.jobs.#{workflow.id}.#{job.klass}")).to eq(ttl)
109+
end
104110
end
105111
end
106112

0 commit comments

Comments
 (0)