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.
- checksums.yaml +4 -4
- data/.dockerignore +21 -0
- data/.gemrelease +2 -0
- data/.gitignore +19 -9
- data/.travis.yml +5 -1
- data/Dockerfile +10 -0
- data/Gemfile +3 -1
- data/README.md +68 -6
- data/app/Gemfile +8 -0
- data/app/app.rb +43 -0
- data/app/config.ru +3 -0
- data/app/sidekiq.rb +25 -0
- data/app/some_worker.rb +9 -0
- data/app/unique_worker.rb +9 -0
- data/app/views/index.erb +35 -0
- data/docker-compose-common.yml +7 -0
- data/docker-compose.yml +26 -0
- data/lib/sidekiq/merger.rb +14 -18
- data/lib/sidekiq/merger/flusher.rb +4 -4
- data/lib/sidekiq/merger/merge.rb +113 -0
- data/lib/sidekiq/merger/middleware.rb +6 -4
- data/lib/sidekiq/merger/redis.rb +52 -25
- data/lib/sidekiq/merger/version.rb +1 -1
- data/lib/sidekiq/merger/views/index.erb +41 -0
- data/lib/sidekiq/merger/web.rb +22 -0
- data/misc/web_ui.png +0 -0
- data/sidekiq-merger.gemspec +9 -11
- data/spec/sidekiq/merger/flusher_spec.rb +7 -7
- data/spec/sidekiq/merger/{batch_spec.rb → merge_spec.rb} +31 -17
- data/spec/sidekiq/merger/middleware_spec.rb +12 -6
- data/spec/sidekiq/merger/redis_spec.rb +99 -69
- data/spec/spec_helper.rb +7 -2
- metadata +73 -66
- data/bin/console +0 -7
- data/bin/setup +0 -8
- data/lib/sidekiq/merger/batch.rb +0 -100
@@ -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 "
|
1
|
+
require_relative "merge"
|
2
2
|
|
3
3
|
class Sidekiq::Merger::Middleware
|
4
|
-
def call(worker_class, msg, queue,
|
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::
|
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
|
-
|
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
|
data/lib/sidekiq/merger/redis.rb
CHANGED
@@ -13,12 +13,16 @@ class Sidekiq::Merger::Redis
|
|
13
13
|
end
|
14
14
|
return true
|
15
15
|
SCRIPT
|
16
|
-
conn.eval(script, [], [
|
16
|
+
conn.eval(script, [], [merges_key, unique_msg_key("*"), msg_key("*"), lock_key("*")])
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
21
|
-
"#{KEY_PREFIX}:
|
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(
|
45
|
-
conn.setnx(time_key(key), execution_time.
|
46
|
-
conn.
|
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
|
-
|
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
|
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
|
60
|
-
redis { |conn| conn.
|
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
|
-
|
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(
|
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
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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.
|
97
|
-
|
98
|
-
|
99
|
-
|
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 :
|
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
|
@@ -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 || "–"%></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
|
data/sidekiq-merger.gemspec
CHANGED
@@ -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 "
|
28
|
-
spec.add_development_dependency "
|
29
|
-
spec.add_development_dependency "
|
30
|
-
spec.add_development_dependency "
|
31
|
-
spec.add_development_dependency "
|
32
|
-
spec.add_development_dependency "
|
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.
|
37
|
-
spec.
|
38
|
-
spec.
|
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(:
|
8
|
-
let(:
|
9
|
-
let(:
|
10
|
-
it "adds the args to the
|
11
|
-
allow(Sidekiq::Merger::
|
12
|
-
expect(
|
13
|
-
expect(
|
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::
|
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:
|
31
|
-
conn.sadd("sidekiq-merger:
|
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:
|
44
|
-
conn.sadd("sidekiq-merger:
|
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
|
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
|
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
|
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
|
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" => [
|
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
|
-
|
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
|
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
|
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 "#
|
120
|
-
it "returns full
|
121
|
-
expect(subject.
|
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
|