sidekiq-merger 0.0.1 → 0.0.4

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.
@@ -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