sidekiq-merger 0.0.1 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,113 @@
1
+ require_relative "redis"
2
+ require "active_support/core_ext/hash/indifferent_access"
3
+
4
+ class Sidekiq::Merger::Merge
5
+ class << self
6
+ def all
7
+ redis = Sidekiq::Merger::Redis.new
8
+
9
+ redis.all.map { |full_merge_key| initialize_with_full_merge_key(full_merge_key, redis: redis) }
10
+ end
11
+
12
+ def initialize_with_full_merge_key(full_merge_key, options = {})
13
+ keys = full_merge_key.split(":")
14
+ raise "Invalid merge key" if keys.size < 3
15
+ worker_class = keys[0].camelize.constantize
16
+ queue = keys[1]
17
+ merge_key = keys[2]
18
+ new(worker_class, queue, merge_key, options)
19
+ end
20
+
21
+ def initialize_with_args(worker_class, queue, args, options = {})
22
+ new(worker_class, queue, merge_key(worker_class, args), options)
23
+ end
24
+
25
+ def merge_key(worker_class, args)
26
+ options = get_options(worker_class)
27
+ merge_key = options["key"]
28
+ if merge_key.respond_to?(:call)
29
+ merge_key.call(args)
30
+ else
31
+ merge_key
32
+ end
33
+ end
34
+
35
+ def get_options(worker_class)
36
+ (worker_class.get_sidekiq_options["merger"] || {}).with_indifferent_access
37
+ end
38
+ end
39
+
40
+ attr_reader :worker_class, :queue, :merge_key
41
+
42
+ def initialize(worker_class, queue, merge_key, redis: Sidekiq::Merger::Redis.new)
43
+ @worker_class = worker_class
44
+ @queue = queue
45
+ @merge_key = merge_key
46
+ @redis = redis
47
+ end
48
+
49
+ def add(args, execution_time)
50
+ if !options[:unique] || !@redis.exists?(full_merge_key, args)
51
+ @redis.push(full_merge_key, args, execution_time)
52
+ end
53
+ end
54
+
55
+ def delete(args)
56
+ @redis.delete(full_merge_key, args)
57
+ end
58
+
59
+ def delete_all
60
+ @redis.delete_all(full_merge_key)
61
+ end
62
+
63
+ def size
64
+ @redis.merge_size(full_merge_key)
65
+ end
66
+
67
+ def flush
68
+ msgs = []
69
+
70
+ if @redis.lock(full_merge_key, Sidekiq::Merger::Config.lock_ttl)
71
+ msgs = @redis.pluck(full_merge_key)
72
+ end
73
+
74
+ unless msgs.empty?
75
+ Sidekiq::Client.push(
76
+ "class" => worker_class,
77
+ "queue" => queue,
78
+ "args" => msgs,
79
+ "merged" => true
80
+ )
81
+ end
82
+ end
83
+
84
+ def can_flush?
85
+ !execution_time.nil? && execution_time < Time.now
86
+ end
87
+
88
+ def full_merge_key
89
+ @full_merge_key ||= [worker_class.name.to_s.underscore, queue, merge_key].join(":")
90
+ end
91
+
92
+ def all_args
93
+ @redis.get(full_merge_key)
94
+ end
95
+
96
+ def execution_time
97
+ @execution_time ||= @redis.execution_time(full_merge_key)
98
+ end
99
+
100
+ def ==(other)
101
+ self.worker_class == other.worker_class &&
102
+ self.queue == other.queue &&
103
+ self.merge_key == other.merge_key
104
+ end
105
+
106
+ private
107
+
108
+ def options
109
+ @options ||= self.class.get_options(worker_class)
110
+ rescue NameError
111
+ {}
112
+ end
113
+ end
@@ -1,18 +1,20 @@
1
- require_relative "batch"
1
+ require_relative "merge"
2
2
 
3
3
  class Sidekiq::Merger::Middleware
4
- def call(worker_class, msg, queue, _redis_pool = nil)
4
+ def call(worker_class, msg, queue, redis_pool = nil)
5
5
  return yield if defined?(Sidekiq::Testing) && Sidekiq::Testing.inline?
6
6
 
7
7
  worker_class = worker_class.camelize.constantize if worker_class.is_a?(String)
8
8
  options = worker_class.get_sidekiq_options
9
9
 
10
10
  if !msg["at"].nil? && options.key?("merger")
11
- Sidekiq::Merger::Batch
11
+ Sidekiq::Merger::Merge
12
12
  .initialize_with_args(worker_class, queue, msg["args"])
13
13
  .add(msg["args"], msg["at"])
14
+ false
14
15
  else
15
- yield
16
+ msg["args"] = [msg["args"]] unless msg.delete("merged")
17
+ yield(worker_class, msg, queue, redis_pool)
16
18
  end
17
19
  end
18
20
  end
@@ -13,12 +13,16 @@ class Sidekiq::Merger::Redis
13
13
  end
14
14
  return true
15
15
  SCRIPT
16
- conn.eval(script, [], [batches_key, msg_key("*"), lock_key("*")])
16
+ conn.eval(script, [], [merges_key, unique_msg_key("*"), msg_key("*"), lock_key("*")])
17
17
  end
18
18
  end
19
19
 
20
- def batches_key
21
- "#{KEY_PREFIX}:batches"
20
+ def merges_key
21
+ "#{KEY_PREFIX}:merges"
22
+ end
23
+
24
+ def unique_msg_key(key)
25
+ "#{KEY_PREFIX}:unique_msg:#{key}"
22
26
  end
23
27
 
24
28
  def msg_key(key)
@@ -39,33 +43,45 @@ class Sidekiq::Merger::Redis
39
43
  end
40
44
 
41
45
  def push(key, msg, execution_time)
46
+ msg_json = msg.to_json
42
47
  redis do |conn|
43
48
  conn.multi do
44
- conn.sadd(batches_key, key)
45
- conn.setnx(time_key(key), execution_time.to_json)
46
- conn.sadd(msg_key(key), msg.to_json)
49
+ conn.sadd(merges_key, key)
50
+ conn.setnx(time_key(key), execution_time.to_i)
51
+ conn.lpush(msg_key(key), msg_json)
52
+ conn.sadd(unique_msg_key(key), msg_json)
47
53
  end
48
54
  end
49
55
  end
50
56
 
51
57
  def delete(key, msg)
52
- redis { |conn| conn.srem(msg_key(key), msg.to_json) }
58
+ msg_json = msg.to_json
59
+ redis do |conn|
60
+ conn.multi do
61
+ conn.srem(unique_msg_key(key), msg_json)
62
+ conn.lrem(msg_key(key), msg_json)
63
+ end
64
+ end
53
65
  end
54
66
 
55
67
  def execution_time(key)
56
- redis { |conn| Time.parse(conn.get(time_key(key))) rescue nil }
68
+ redis do |conn|
69
+ t = conn.get(time_key(key))
70
+ Time.at(t.to_i) unless t.nil?
71
+ end
57
72
  end
58
73
 
59
- def batch_size(key)
60
- redis { |conn| conn.scard(msg_key(key)) }
74
+ def merge_size(key)
75
+ redis { |conn| conn.llen(msg_key(key)) }
61
76
  end
62
77
 
63
78
  def exists?(key, msg)
64
- redis { |conn| conn.sismember(msg_key(key), msg.to_json) }
79
+ msg_json = msg.to_json
80
+ redis { |conn| conn.sismember(unique_msg_key(key), msg_json) }
65
81
  end
66
82
 
67
83
  def all
68
- redis { |conn| conn.smembers(batches_key) }
84
+ redis { |conn| conn.smembers(merges_key) }
69
85
  end
70
86
 
71
87
  def lock(key, ttl)
@@ -74,33 +90,44 @@ class Sidekiq::Merger::Redis
74
90
 
75
91
  def get(key)
76
92
  msgs = []
77
- redis do |conn|
78
- msgs = conn.smembers(msg_key(key))
79
- end
93
+ redis { |conn| msgs = conn.lrange(msg_key(key), 0, -1) }
80
94
  msgs.map { |msg| JSON.parse(msg) }
81
95
  end
82
96
 
83
97
  def pluck(key)
84
98
  msgs = []
85
99
  redis do |conn|
86
- msgs = conn.smembers(msg_key(key))
87
- conn.del(msg_key(key))
88
- conn.del(time_key(key))
89
- conn.srem(batches_key, key)
100
+ conn.multi do
101
+ msgs = conn.lrange(msg_key(key), 0, -1)
102
+ conn.del(unique_msg_key(key))
103
+ conn.del(msg_key(key))
104
+ conn.del(time_key(key))
105
+ conn.srem(merges_key, key)
106
+ end
90
107
  end
91
- msgs.map { |msg| JSON.parse(msg) }
108
+ extract_future_value(msgs).map { |msg| JSON.parse(msg) }
92
109
  end
93
110
 
94
111
  def delete_all(key)
95
112
  redis do |conn|
96
- conn.del(msg_key(key))
97
- conn.del(time_key(key))
98
- conn.del(lock_key(key))
99
- conn.srem(batches_key, key)
113
+ conn.multi do
114
+ conn.del(unique_msg_key(key))
115
+ conn.del(msg_key(key))
116
+ conn.del(time_key(key))
117
+ conn.del(lock_key(key))
118
+ conn.srem(merges_key, key)
119
+ end
100
120
  end
101
121
  end
102
122
 
103
123
  private
104
124
 
105
- delegate :batches_key, :msg_key, :time_key, :lock_key, :redis, to: "self.class"
125
+ delegate :merges_key, :msg_key, :unique_msg_key, :time_key, :lock_key, :redis, to: "self.class"
126
+
127
+ def extract_future_value(future)
128
+ while future.value.is_a?(Redis::FutureNotReady)
129
+ sleep(0.001)
130
+ end
131
+ future.value
132
+ end
106
133
  end
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module Merger
3
- VERSION = "0.0.1".freeze
3
+ VERSION = "0.0.4".freeze
4
4
  end
5
5
  end
@@ -0,0 +1,41 @@
1
+ <header class="row">
2
+ <div class="col-sm-5">
3
+ <h3>Merged jobs</h3>
4
+ </div>
5
+ </header>
6
+
7
+ <div class="container">
8
+ <div class="row">
9
+ <div class="col-sm-12">
10
+ <% if true %>
11
+ <table class="table table-striped table-bordered table-white" style="width: 100%; margin: 0; table-layout:fixed;">
12
+ <thead>
13
+ <th style="width: 25%">Worker</th>
14
+ <th style="width: 15%">Queue</th>
15
+ <th style="width: 10%">Count</th>
16
+ <th style="width: 20%">All Args</th>
17
+ <th style="width: 20%">Execution time</th>
18
+ <th style="width: 10%">Actions</th>
19
+ </thead>
20
+ <% @merges.each do |merge| %>
21
+ <tr>
22
+ <td><%= merge.worker_class %></td>
23
+ <td><%= merge.queue %></td>
24
+ <td><%= merge.size %></td>
25
+ <td><%= merge.all_args %></td>
26
+ <td><%= merge.execution_time || "&ndash;"%></td>
27
+ <td>
28
+ <form action="<%= "#{root_path}merger/#{URI.encode_www_form_component merge.full_merge_key}/delete" %>" method="post">
29
+ <%= csrf_tag %>
30
+ <input class="btn btn-danger btn-xs" type="submit" name="delete" value="Delete" data-confirm="Are you sure you want to delete this merge?" />
31
+ </form>
32
+ </td>
33
+ </tr>
34
+ <% end %>
35
+ </table>
36
+ <% else %>
37
+ <div class="alert alert-success">No recurring jobs found.</div>
38
+ <% end %>
39
+ </div>
40
+ </div>
41
+ </div>
@@ -0,0 +1,22 @@
1
+ require "sidekiq/web"
2
+
3
+ module Sidekiq::Merger::Web
4
+ VIEWS = File.expand_path("views", File.dirname(__FILE__))
5
+
6
+ def self.registered(app)
7
+ app.get "/merger" do
8
+ @merges = Sidekiq::Merger::Merge.all
9
+ erb File.read(File.join(VIEWS, "index.erb")), locals: { view_path: VIEWS }
10
+ end
11
+
12
+ app.post "/merger/:full_merge_key/delete" do
13
+ full_merge_key = URI.decode_www_form_component params[:full_merge_key]
14
+ merge = Sidekiq::Merger::Merge.initialize_with_full_merge_key(full_merge_key)
15
+ merge.delete_all
16
+ redirect "#{root_path}/merger"
17
+ end
18
+ end
19
+ end
20
+
21
+ Sidekiq::Web.register(Sidekiq::Merger::Web)
22
+ Sidekiq::Web.tabs["Merger"] = "merger"
data/misc/web_ui.png ADDED
Binary file
@@ -24,16 +24,14 @@ Gem::Specification.new do |spec|
24
24
 
25
25
  spec.required_ruby_version = [">= 2.2.2", "< 2.5"]
26
26
 
27
- spec.add_development_dependency "bundler", "~> 1.13"
28
- spec.add_development_dependency "rake", "~> 10.0"
29
- spec.add_development_dependency "rspec", "~> 3.0"
30
- spec.add_development_dependency "simplecov"
31
- spec.add_development_dependency "timecop"
32
- spec.add_development_dependency "rubocop"
33
- spec.add_development_dependency "pry"
34
- spec.add_development_dependency "coveralls"
27
+ spec.add_development_dependency "rake", ">= 10.0", "< 13"
28
+ spec.add_development_dependency "rspec", ">= 3.0", "< 4"
29
+ spec.add_development_dependency "simplecov", "~> 0.12"
30
+ spec.add_development_dependency "timecop", "~> 0.8"
31
+ spec.add_development_dependency "rubocop", "~> 0.47"
32
+ spec.add_development_dependency "coveralls", "~> 0.8"
35
33
 
36
- spec.add_dependency "sidekiq", ">= 3.4.0"
37
- spec.add_dependency "concurrent-ruby"
38
- spec.add_dependency "activesupport", ">= 3.2.0"
34
+ spec.add_runtime_dependency "sidekiq", ">= 3.4", "< 5"
35
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
36
+ spec.add_runtime_dependency "activesupport", ">= 3.2", "< 6"
39
37
  end
@@ -4,13 +4,13 @@ describe Sidekiq::Merger::Flusher do
4
4
  subject { described_class.new(Sidekiq.logger) }
5
5
 
6
6
  describe "#call" do
7
- let(:active_batch) { double(full_batch_key: "active", can_flush?: true, flush: nil) }
8
- let(:inactive_batch) { double(full_batch_key: "inactive", can_flush?: false, flush: nil) }
9
- let(:batches) { [active_batch, inactive_batch] }
10
- it "adds the args to the batch" do
11
- allow(Sidekiq::Merger::Batch).to receive(:all).and_return batches
12
- expect(active_batch).to receive(:flush)
13
- expect(inactive_batch).not_to receive(:flush)
7
+ let(:active_merge) { double(full_merge_key: "active", can_flush?: true, flush: nil) }
8
+ let(:inactive_merge) { double(full_merge_key: "inactive", can_flush?: false, flush: nil) }
9
+ let(:merges) { [active_merge, inactive_merge] }
10
+ it "adds the args to the merge" do
11
+ allow(Sidekiq::Merger::Merge).to receive(:all).and_return merges
12
+ expect(active_merge).to receive(:flush)
13
+ expect(inactive_merge).not_to receive(:flush)
14
14
 
15
15
  subject.flush
16
16
  end
@@ -1,6 +1,6 @@
1
1
  require "spec_helper"
2
2
 
3
- describe Sidekiq::Merger::Batch do
3
+ describe Sidekiq::Merger::Merge do
4
4
  subject { described_class.new(worker_class, queue, "foo", redis: redis) }
5
5
  let(:redis) { Sidekiq::Merger::Redis.new }
6
6
  let(:queue) { "queue" }
@@ -27,8 +27,8 @@ describe Sidekiq::Merger::Batch do
27
27
  describe ".all" do
28
28
  it "returns all the keys" do
29
29
  redis.redis do |conn|
30
- conn.sadd("sidekiq-merger:batches", "string:foo:xxx")
31
- conn.sadd("sidekiq-merger:batches", "numeric:bar:yyy")
30
+ conn.sadd("sidekiq-merger:merges", "string:foo:xxx")
31
+ conn.sadd("sidekiq-merger:merges", "numeric:bar:yyy")
32
32
  end
33
33
 
34
34
  expect(described_class.all).to contain_exactly(
@@ -40,18 +40,18 @@ describe Sidekiq::Merger::Batch do
40
40
  context "including invalid key" do
41
41
  it "raises an error" do
42
42
  redis.redis do |conn|
43
- conn.sadd("sidekiq-merger:batches", "string:foo:xxx")
44
- conn.sadd("sidekiq-merger:batches", "invalid")
43
+ conn.sadd("sidekiq-merger:merges", "string:foo:xxx")
44
+ conn.sadd("sidekiq-merger:merges", "invalid")
45
45
  end
46
46
  expect {
47
47
  described_class.all
48
- }.to raise_error RuntimeError, "Invalid batch key"
48
+ }.to raise_error RuntimeError, "Invalid merge key"
49
49
  end
50
50
  end
51
51
  end
52
52
 
53
53
  describe ".initialize_with_args" do
54
- it "provides batch_key from args" do
54
+ it "provides merge_key from args" do
55
55
  expect(described_class).to receive(:new).with(worker_class, queue, "[1,2,3]", anything)
56
56
  described_class.initialize_with_args(worker_class, queue, [1, 2, 3])
57
57
  end
@@ -62,14 +62,28 @@ describe Sidekiq::Merger::Batch do
62
62
  end
63
63
 
64
64
  describe "#add" do
65
- it "adds the args in lazy batch" do
65
+ it "adds the args in lazy merge" do
66
66
  expect(redis).to receive(:push).with("name:queue:foo", [1, 2, 3], execution_time)
67
67
  subject.add([1, 2, 3], execution_time)
68
68
  end
69
+ context "with unique option" do
70
+ let(:options) { { key: -> (args) { args.to_json }, unique: true } }
71
+ it "adds the args in lazy merge" do
72
+ expect(redis).to receive(:push).with("name:queue:foo", [1, 2, 3], execution_time)
73
+ subject.add([1, 2, 3], execution_time)
74
+ end
75
+ context "the args has alredy been added" do
76
+ before { subject.add([1, 2, 3], execution_time) }
77
+ it "adds the args in lazy merge" do
78
+ expect(redis).not_to receive(:push)
79
+ subject.add([1, 2, 3], execution_time)
80
+ end
81
+ end
82
+ end
69
83
  end
70
84
 
71
85
  describe "#delete" do
72
- it "adds the args in lazy batch" do
86
+ it "adds the args in lazy merge" do
73
87
  expect(redis).to receive(:delete).with("name:queue:foo", [1, 2, 3])
74
88
  subject.delete([1, 2, 3])
75
89
  end
@@ -87,7 +101,8 @@ describe Sidekiq::Merger::Batch do
87
101
  expect(Sidekiq::Client).to receive(:push).with(
88
102
  "class" => worker_class,
89
103
  "queue" => queue,
90
- "args" => [[1, 2, 3], [2, 3, 4]]
104
+ "args" => a_collection_containing_exactly([1, 2, 3], [2, 3, 4]),
105
+ "merged" => true
91
106
  )
92
107
 
93
108
  subject.flush
@@ -95,19 +110,18 @@ describe Sidekiq::Merger::Batch do
95
110
  end
96
111
 
97
112
  describe "#can_flush?" do
98
- let(:options) { { flush_interval: 10.seconds } }
99
- context "it has not get anything in batch" do
113
+ context "it has not get anything in merge" do
100
114
  it "returns false" do
101
115
  expect(subject.can_flush?).to eq false
102
116
  end
103
117
  end
104
- context "it has not passed the interval time" do
118
+ context "it has not passed the execution time" do
105
119
  it "returns false" do
106
120
  subject.add([], execution_time)
107
121
  expect(subject.can_flush?).to eq false
108
122
  end
109
123
  end
110
- context "it has passed the interval time" do
124
+ context "it has passed the execution time" do
111
125
  it "returns true" do
112
126
  subject.add([], execution_time)
113
127
  Timecop.travel(10.seconds)
@@ -116,9 +130,9 @@ describe Sidekiq::Merger::Batch do
116
130
  end
117
131
  end
118
132
 
119
- describe "#full_batch_key" do
120
- it "returns full batch key" do
121
- expect(subject.full_batch_key).to eq "name:queue:foo"
133
+ describe "#full_merge_key" do
134
+ it "returns full merge key" do
135
+ expect(subject.full_merge_key).to eq "name:queue:foo"
122
136
  end
123
137
  end
124
138
  end