postqueue 0.2.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,68 +1,55 @@
1
1
  require "spec_helper"
2
2
 
3
- describe "::queue.process_one" do
4
- let(:queue) { Postqueue::Base.new }
3
+ describe "error handling" do
4
+ let(:queue) do
5
+ Postqueue.new do |queue|
6
+ queue.batch_sizes["batchable"] = 10
7
+ queue.batch_sizes["other-batchable"] = 10
8
+ end
9
+ end
5
10
 
6
- class E < RuntimeError; end
11
+ let(:items) { queue.item_class.all }
12
+ let(:item) { queue.item_class.first }
7
13
 
8
- before do
9
- queue.enqueue op: "mytype", entity_id: 12
10
- end
14
+ class E < RuntimeError; end
11
15
 
12
- context "block raises an exception" do
16
+ context "when handler raises an exception" do
13
17
  before do
14
- expect { queue.process_one { |_op, _type, _ids| raise E } }.to raise_error(E)
15
- end
16
-
17
- it "reraises the exception" do
18
- # checked in before block
18
+ queue.on "mytype" do raise E end
19
+ queue.enqueue op: "mytype", entity_id: 12
19
20
  end
20
21
 
21
- it "keeps the item in the queue" do
22
+ it "reraises the exception, keeps the item in the queue and increments the failed_attempt count" do
23
+ expect { queue.process_one }.to raise_error(E)
22
24
  expect(items.map(&:entity_id)).to contain_exactly(12)
23
- end
24
-
25
- it "increments the failed_attempt count" do
26
25
  expect(items.map(&:failed_attempts)).to contain_exactly(1)
27
26
  end
28
27
  end
29
28
 
30
- context "block returns false" do
29
+ context "when no handler can be found" do
31
30
  before do
32
- @result = queue.process_one { |_op, _type, _ids| false }
33
- end
34
-
35
- it "returns false" do
36
- expect(@result).to be false
37
- end
38
-
39
- it "keeps the item in the queue" do
40
- expect(items.map(&:entity_id)).to contain_exactly(12)
31
+ queue.enqueue op: "mytype", entity_id: 12
41
32
  end
42
33
 
43
- it "increments the failed_attempt count" do
44
- expect(items.map(&:failed_attempts)).to contain_exactly(1)
34
+ it "raises a MissingHandler exception" do
35
+ expect { queue.process_one }.to raise_error(::Postqueue::MissingHandler)
45
36
  end
46
37
  end
47
38
 
48
39
  context "failed_attempts reached MAX_ATTEMPTS" do
49
40
  before do
50
- expect(Postqueue::MAX_ATTEMPTS).to be >= 3
51
- items.update_all(failed_attempts: Postqueue::MAX_ATTEMPTS)
52
-
53
- @called_block = 0
54
- @result = queue.process_one do
55
- @called_block += 1
56
- false
57
- end
41
+ expect(queue.max_attemps).to be >= 3
42
+ queue.enqueue op: "mytype", entity_id: 12
43
+ items.update_all(failed_attempts: queue.max_attemps)
44
+ queue.process_one
58
45
  end
59
46
 
60
47
  it "does not call the block" do
61
- expect(@called_block).to eq(0)
48
+ expect { queue.process_one }.not_to raise_error
62
49
  end
63
50
 
64
- it "returns nil" do
65
- expect(@result).to eq(nil)
51
+ it "returns 0" do
52
+ expect(queue.process_one).to eq(0)
66
53
  end
67
54
 
68
55
  it "does not remove the item" do
@@ -70,7 +57,7 @@ describe "::queue.process_one" do
70
57
  end
71
58
 
72
59
  it "does not increment the failed_attempts count" do
73
- expect(items.first.failed_attempts).to eq(Postqueue::MAX_ATTEMPTS)
60
+ expect(items.first.failed_attempts).to eq(queue.max_attemps)
74
61
  end
75
62
  end
76
63
  end
@@ -1,14 +1,23 @@
1
1
  require "spec_helper"
2
2
 
3
- describe "::queue.process" do
4
- class Testqueue < Postqueue::Base
5
- def batch_size(op:)
6
- _ = op
7
- 10
3
+ describe "processing" do
4
+ let(:processed_events) do
5
+ @processed_events ||= []
6
+ end
7
+
8
+ let(:queue) do
9
+ Postqueue.new do |queue|
10
+ queue.batch_sizes["batchable"] = 10
11
+ queue.batch_sizes["other-batchable"] = 10
12
+
13
+ queue.on '*' do |op, entity_ids|
14
+ processed_events << [ op, entity_ids ]
15
+ end
8
16
  end
9
17
  end
10
18
 
11
- let(:queue) { Testqueue.new }
19
+ let(:items) { queue.item_class.all }
20
+ let(:item) { queue.item_class.first }
12
21
 
13
22
  describe "basics" do
14
23
  before do
@@ -19,7 +28,7 @@ describe "::queue.process" do
19
28
 
20
29
  it "processes the first entry" do
21
30
  r = queue.process_one
22
- expect(r).to eq(["myop", [12]])
31
+ expect(r).to eq(1)
23
32
  expect(items.map(&:entity_id)).to contain_exactly(13, 14)
24
33
  end
25
34
 
@@ -27,53 +36,53 @@ describe "::queue.process" do
27
36
  queue.enqueue(op: "otherop", entity_id: 112)
28
37
 
29
38
  r = queue.process_one(op: "otherop")
30
- expect(r).to eq(["otherop", [112]])
39
+ expect(r).to eq(1)
31
40
  expect(items.map(&:entity_id)).to contain_exactly(12, 13, 14)
32
41
  end
33
42
 
34
- it "yields a block and returns its return value" do
43
+ it "yields a block and returns the processed entries" do
35
44
  queue.enqueue op: "otherop", entity_id: 112
36
- r = queue.process_one(op: "otherop") do |op, ids|
37
- expect(op).to eq("otherop")
38
- expect(ids).to eq([112])
39
- "yihaa"
40
- end
45
+ called = false
46
+ queue.process_one(op: "otherop")
47
+
48
+ op, ids = processed_events.first
49
+ expect(op).to eq("otherop")
50
+ expect(ids).to eq([112])
41
51
 
42
- expect(r).to eq("yihaa")
43
52
  expect(items.map(&:entity_id)).to contain_exactly(12, 13, 14)
44
53
  end
45
54
  end
46
55
 
47
56
  context "when having entries with different entity_type and op" do
48
57
  before do
49
- queue.enqueue op: "myop", entity_id: 12
50
- queue.enqueue op: "myop", entity_id: 13
51
- queue.enqueue op: "otherop", entity_id: 14
52
- queue.enqueue op: "myop", entity_id: 15
53
- queue.enqueue op: "otherop", entity_id: 16
58
+ queue.enqueue op: "batchable", entity_id: 12
59
+ queue.enqueue op: "batchable", entity_id: 13
60
+ queue.enqueue op: "other-batchable", entity_id: 14
61
+ queue.enqueue op: "batchable", entity_id: 15
62
+ queue.enqueue op: "other-batchable", entity_id: 16
54
63
  end
55
64
 
56
- it "processes one entries" do
65
+ it "processes one matching entry with batch_size 1" do
57
66
  r = queue.process batch_size: 1
58
- expect(r).to eq(["myop", [12]])
67
+ expect(r).to eq(1)
59
68
  expect(items.map(&:entity_id)).to contain_exactly(13, 14, 15, 16)
60
69
  end
61
70
 
62
- it "processes two entries" do
71
+ it "processes two matching entries" do
63
72
  r = queue.process batch_size: 2
64
- expect(r).to eq(["myop", [12, 13]])
73
+ expect(r).to eq(2)
65
74
  expect(items.map(&:entity_id)).to contain_exactly(14, 15, 16)
66
75
  end
67
76
 
68
- it "processes only matching entries when asked for more" do
77
+ it "processes all matching entries" do
69
78
  r = queue.process
70
- expect(r).to eq(["myop", [12, 13, 15]])
79
+ expect(r).to eq(3)
71
80
  expect(items.map(&:entity_id)).to contain_exactly(14, 16)
72
81
  end
73
82
 
74
83
  it "honors search conditions" do
75
- r = queue.process(op: "otherop")
76
- expect(r).to eq(["otherop", [14, 16]])
84
+ r = queue.process(op: "other-batchable")
85
+ expect(r).to eq(2)
77
86
  expect(items.map(&:entity_id)).to contain_exactly(12, 13, 15)
78
87
  end
79
88
  end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ describe "sync_processing" do
4
+ let(:callback_invocations) { @callback_invocations ||= [] }
5
+
6
+ before :all do
7
+ Postqueue.async_processing = false
8
+ end
9
+
10
+ after :all do
11
+ Postqueue.async_processing = true
12
+ end
13
+
14
+ let(:queue) do
15
+ Postqueue.new do |queue|
16
+ queue.on "op" do |op, entity_ids|
17
+ callback_invocations << [ op, entity_ids ]
18
+ end
19
+ end
20
+ end
21
+
22
+ let(:items) { queue.item_class.all }
23
+ let(:item) { queue.item_class.first }
24
+
25
+ context "when enqueuing in sync mode" do
26
+ before do
27
+ queue.enqueue op: "op", entity_id: 12
28
+ end
29
+
30
+ it "processed the items" do
31
+ expect(callback_invocations.length).to eq(1)
32
+ end
33
+
34
+ it "removed all items" do
35
+ expect(items.count).to eq(0)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ require "spec_helper"
2
+
3
+ describe "wildcard processing" do
4
+ let(:callback_invocations) { @callback_invocations ||= [] }
5
+
6
+ let(:queue) do
7
+ Postqueue.new do |queue|
8
+ queue.on "*" do |op, entity_ids|
9
+ callback_invocations << [ op, entity_ids ]
10
+ end
11
+ end
12
+ end
13
+
14
+ let(:items) { queue.item_class.all }
15
+ let(:item) { queue.item_class.first }
16
+
17
+ context "when enqueuing in sync mode" do
18
+ before do
19
+ queue.enqueue op: "op", entity_id: 12
20
+ queue.process
21
+ end
22
+
23
+ it "processed the items" do
24
+ expect(callback_invocations.length).to eq(1)
25
+ end
26
+
27
+ it "removed all items" do
28
+ expect(items.count).to eq(0)
29
+ end
30
+ end
31
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,3 @@
1
- path = File.expand_path("../../mpx/lib", __FILE__)
2
- $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path)
3
-
4
1
  ENV["RACK_ENV"] = "test"
5
2
 
6
3
  require "rspec"
@@ -14,15 +11,7 @@ end
14
11
  require "postqueue"
15
12
  require "./spec/support/configure_active_record"
16
13
 
17
- module Postqueue
18
- def self.logger
19
- @logger ||= Logger.new(File.open("log/test.log", "a"))
20
- end
21
- end
22
-
23
- def items
24
- Postqueue::Item.all
25
- end
14
+ Postqueue.logger = Logger.new(File.open("log/test.log", "a"))
26
15
 
27
16
  RSpec.configure do |config|
28
17
  config.run_all_when_everything_filtered = true
@@ -1,23 +1,17 @@
1
- require "active_record"
2
-
3
- $LOAD_PATH << File.dirname(__FILE__)
4
-
5
- ActiveRecord::Base.establish_connection(adapter: "postgresql",
6
- database: "postqueue_test",
7
- username: "postqueue",
8
- password: "postqueue")
9
-
10
- # require_relative "schema.rb"
11
- # require_relative "models.rb"
1
+ require_relative "./connect_active_record"
12
2
 
13
3
  Postqueue.unmigrate!
14
4
  Postqueue.migrate!
15
5
 
16
6
  RSpec.configure do |config|
17
7
  config.around(:each) do |example|
18
- ActiveRecord::Base.connection.transaction do
8
+ if example.metadata[:transactions] == false
19
9
  example.run
20
- raise ActiveRecord::Rollback, "Clean up"
10
+ else
11
+ ActiveRecord::Base.connection.transaction do
12
+ example.run
13
+ raise ActiveRecord::Rollback, "Clean up"
14
+ end
21
15
  end
22
16
  end
23
17
  end
@@ -0,0 +1,5 @@
1
+ require "active_record"
2
+ ActiveRecord::Base.establish_connection(adapter: "postgresql",
3
+ database: "postqueue_test",
4
+ username: "postqueue",
5
+ password: "postqueue")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postqueue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - radiospiel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-12-11 00:00:00.000000000 Z
11
+ date: 2016-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -145,19 +145,32 @@ extra_rdoc_files: []
145
145
  files:
146
146
  - README.md
147
147
  - lib/postqueue.rb
148
- - lib/postqueue/base.rb
149
- - lib/postqueue/base/callback.rb
150
- - lib/postqueue/base/enqueue.rb
151
- - lib/postqueue/base/processing.rb
152
- - lib/postqueue/base/select_and_lock.rb
153
148
  - lib/postqueue/item.rb
149
+ - lib/postqueue/item/enqueue.rb
150
+ - lib/postqueue/item/inserter.rb
151
+ - lib/postqueue/logger.rb
152
+ - lib/postqueue/queue.rb
153
+ - lib/postqueue/queue/callback.rb
154
+ - lib/postqueue/queue/logging.rb
155
+ - lib/postqueue/queue/processing.rb
156
+ - lib/postqueue/queue/select_and_lock.rb
154
157
  - lib/postqueue/version.rb
158
+ - lib/tracker.rb
159
+ - lib/tracker/advisory_lock.rb
160
+ - lib/tracker/migration.rb
161
+ - lib/tracker/registry.rb
162
+ - lib/tracker/tracker.sql
163
+ - spec/postqueue/concurrency_spec.rb
155
164
  - spec/postqueue/enqueue_spec.rb
165
+ - spec/postqueue/idempotent_ops_spec.rb
156
166
  - spec/postqueue/postqueue_spec.rb
157
167
  - spec/postqueue/process_errors_spec.rb
158
168
  - spec/postqueue/process_spec.rb
169
+ - spec/postqueue/syncmode_spec.rb
170
+ - spec/postqueue/wildcard_spec.rb
159
171
  - spec/spec_helper.rb
160
172
  - spec/support/configure_active_record.rb
173
+ - spec/support/connect_active_record.rb
161
174
  homepage: https://github.com/radiospiel/postqueue
162
175
  licenses:
163
176
  - MIT
@@ -1,23 +0,0 @@
1
- module Postqueue
2
- class Base
3
- def run_callback(op:, entity_ids:, &_block)
4
- queue_times = item_class.find_by_sql <<-SQL
5
- SELECT extract('epoch' from AVG(now() - created_at)) AS avg,
6
- extract('epoch' from MAX(now() - created_at)) AS max
7
- FROM #{item_class.table_name} WHERE entity_id IN (#{entity_ids.join(',')})
8
- SQL
9
- queue_time = queue_times.first
10
-
11
- # run callback.
12
- result = [ op, entity_ids ]
13
-
14
- total_processing_time = Benchmark.realtime do
15
- result = yield(*result) if block_given?
16
- end
17
-
18
- timing = Timing.new(queue_time.avg, queue_time.max, total_processing_time, total_processing_time / entity_ids.length)
19
-
20
- [ result, timing ]
21
- end
22
- end
23
- end
@@ -1,31 +0,0 @@
1
- module Postqueue
2
- class Base
3
- # Enqueues an queue item. If the operation is duplicate, and an entry with
4
- # the same combination of op and entity_id exists already, no new entry will
5
- # be added to the queue.
6
- #
7
- # [TODO] An optimized code path, talking directly to PG, might be faster by a factor of 4 or so.
8
- def enqueue(op:, entity_id:, duplicate: true)
9
- if entity_id.is_a?(Array)
10
- enqueue_many(op: op, entity_ids: entity_id, duplicate: duplicate)
11
- return
12
- end
13
-
14
- if !duplicate && item_class.where(op: op, entity_id: entity_id).present?
15
- return
16
- end
17
-
18
- item_class.create!(op: op, entity_id: entity_id)
19
- end
20
-
21
- private
22
-
23
- def enqueue_many(op:, entity_ids:, duplicate:) #:nodoc:
24
- item_class.transaction do
25
- entity_ids.each do |entity_id|
26
- enqueue(op: op, entity_id: entity_id, duplicate: duplicate)
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,57 +0,0 @@
1
- module Postqueue
2
- MAX_ATTEMPTS = 5
3
-
4
- class Base
5
- # Processes many entries
6
- #
7
- # process batch_size: 100
8
- def process(op: nil, batch_size: 100, &block)
9
- status, result = item_class.transaction do
10
- process_inside_transaction(op: op, batch_size: batch_size, &block)
11
- end
12
-
13
- raise result if status == :err
14
- result
15
- end
16
-
17
- def process_one(op: nil, &block)
18
- process(op: op, batch_size: 1, &block)
19
- end
20
-
21
- private
22
-
23
- # The actual processing. Returns [ :ok, number-of-items ] or [ :err, exception ]
24
- def process_inside_transaction(op:, batch_size:, &block)
25
- batch = select_and_lock_batch(op: op, batch_size: batch_size)
26
-
27
- match = batch.first
28
- return [ :ok, nil ] unless match
29
-
30
- entity_ids = batch.map(&:entity_id)
31
- result, timing = run_callback(op: match.op, entity_ids: entity_ids, &block)
32
-
33
- # Depending on the result either reprocess or delete all items
34
- if result == false
35
- postpone batch.map(&:id)
36
- else
37
- on_processing(match.op, entity_ids, timing)
38
- item_class.where(id: batch.map(&:id)).delete_all
39
- end
40
-
41
- [ :ok, result ]
42
- rescue => e
43
- on_exception(e, match.op, entity_ids)
44
- postpone batch.map(&:id)
45
- [ :err, e ]
46
- end
47
-
48
- def postpone(ids)
49
- item_class.connection.exec_query <<-SQL
50
- UPDATE #{item_class.table_name}
51
- SET failed_attempts = failed_attempts+1,
52
- next_run_at = next_run_at + power(failed_attempts + 1, 1.5) * interval '10 second'
53
- WHERE id IN (#{ids.join(',')})
54
- SQL
55
- end
56
- end
57
- end
@@ -1,41 +0,0 @@
1
- module Postqueue
2
- Timing = Struct.new(:avg_queue_time, :max_queue_time, :total_processing_time, :processing_time)
3
-
4
- class Base
5
- private
6
-
7
- def batch_size(op:)
8
- _ = op
9
- 1
10
- end
11
-
12
- def item_class
13
- Postqueue::Item
14
- end
15
-
16
- def logger
17
- Postqueue.logger
18
- end
19
-
20
- def on_processing(op, entity_ids, timing)
21
- msg = "processing '#{op}' for id(s) #{entity_ids.join(',')}: "
22
- msg += "processing #{entity_ids.length} items took #{'%.3f msecs' % timing.total_processing_time}"
23
-
24
- msg += ", queue_time: avg: #{'%.3f msecs' % timing.avg_queue_time}/max: #{'%.3f msecs' % timing.max_queue_time}"
25
- logger.info msg
26
- end
27
-
28
- def on_exception(exception, op, entity_ids)
29
- logger.warn "processing '#{op}' for id(s) #{entity_ids.inspect}: caught #{exception}"
30
- end
31
- end
32
-
33
- def self.logger
34
- Logger.new(STDERR)
35
- end
36
- end
37
-
38
- require "postqueue/base/enqueue"
39
- require "postqueue/base/select_and_lock"
40
- require "postqueue/base/processing"
41
- require "postqueue/base/callback"