coil 1.6.1 → 1.7.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6585ea0b663bc817f16b5a10f4b63dcbc7978b7a4f869468c33706672cb1204d
4
- data.tar.gz: d0cb7da9194256b429eceaae88f1a0860244da40c980470caf5caf9dfa0cf022
3
+ metadata.gz: 4d8cfb14efec96e90e76ae65f4dc955cbfc6096dd2769bffd96cec917170cc7f
4
+ data.tar.gz: 412d34f5d3692749ac166b81a3560a2c2c95b626b4aee8aa8e65cc44c264b4ba
5
5
  SHA512:
6
- metadata.gz: 7e7c8a7b98ed57bb9d25bca7fbdbfeb88ae14bc331d24494eb1964cd9eab723e6de59093bc0c6c02727b452b4d0b3753963600317b625d7ef4971dd907d9a0f5
7
- data.tar.gz: 958f4e1ad44cb749365d581cb92bf2c2864363aae0bf0b9e17254a660c1f176077d67a5a65c3f87f67d1203c1cf0ce1a9d2c085cc74ba31641517109ca580f1d
6
+ metadata.gz: 30330b66219bccd62fbceef0716efc568540ca432464c94b81b3b09e0bc4d216e41a9e8e6c4e3e9bc46ad0de6af337e382f57cd34872990895d18d14ecbc7424
7
+ data.tar.gz: a89bf2e70d720a95d47e6157567e469374d53be4e580603aed02bae0ed7f70a28bae5b3270d7c1628b253eff1c44e4a02dc88d256cf97a8b7acab10972376d58
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## main
2
2
 
3
+ ## [1.7.0](https://github.com/OdekoTeam/coil/compare/1.6.1...1.7.0) (2026-03-16)
4
+ #### Changed
5
+ - Improve retry policy ([d51ea55](https://github.com/OdekoTeam/coil/commit/d51ea553eaa41cb99a4da70a0316707780f1a79d))
6
+ - **NOTE**: this release includes additional migrations, so **be sure to run** `bundle exec rails coil:install:migrations db:migrate`
7
+
3
8
  ## [1.6.1](https://github.com/OdekoTeam/coil/compare/1.6.0...1.6.1) (2026-03-16)
4
9
  #### Fix
5
10
  - Fix migration version ([63866a2](https://github.com/OdekoTeam/coil/commit/63866a21d0d54080b2ef8a37046237524b281089))
@@ -10,8 +10,6 @@ module Coil
10
10
  # job to pick up where we left off.
11
11
  MAX_DURATION = 5.minutes
12
12
 
13
- sidekiq_options retry: 4, dead: false
14
-
15
13
  def perform(key, processor_name = self.class.to_s)
16
14
  deadline = Time.current + MAX_DURATION
17
15
  next_in_line = process_messages(key:, processor_name:, deadline:)
@@ -38,18 +36,40 @@ module Coil
38
36
  next_in_line = next_message(key:, processor_name:)
39
37
  return next_in_line if next_in_line.nil? || Time.current > deadline
40
38
 
41
- locking(key) do
42
- message = next_message(key:, processor_name:)
39
+ outcome = process_next(key:, processor_name:)
40
+ raise outcome.error if outcome.error
41
+ end
42
+ end
43
+
44
+ class Outcome
45
+ attr_accessor :error
46
+ end
47
+
48
+ def process_next(key:, processor_name:)
49
+ outcome = Outcome.new
43
50
 
44
- if message.present?
45
- around_process(message, processor_name:) do
46
- pre_process(message)
47
- message.processed(processor_name:)
48
- process(message)
51
+ locking(key) do
52
+ message = next_message(key:, processor_name:)
53
+
54
+ if message.present?
55
+ message.increment!(:processor_attempts)
56
+ begin
57
+ # Use a nested transaction and deferred error-handling to ensure the
58
+ # processor_attempts increment persists, even if the attempt fails.
59
+ ApplicationRecord.transaction(requires_new: true) do
60
+ around_process(message, processor_name:) do
61
+ pre_process(message)
62
+ message.processed(processor_name:)
63
+ process(message)
64
+ end
49
65
  end
66
+ rescue => e
67
+ outcome.error = e
50
68
  end
51
69
  end
52
70
  end
71
+
72
+ outcome
53
73
  end
54
74
 
55
75
  def around_process(message, processor_name:, &blk)
@@ -1,17 +1,28 @@
1
1
  # typed: strict
2
2
 
3
+ require "sidekiq/api"
4
+
3
5
  # The periodic job acts as a fallback mechanism, polling for messages that were
4
6
  # not enqueued and processed automatically upon create, e.g. due to a failure to
5
7
  # push a job onto the Redis queue.
6
8
  module Coil
7
9
  class TransactionalMessagesPeriodicJob < ApplicationJob
10
+ ATTEMPTS_THRESHOLD = 3
11
+
8
12
  def perform
9
- t = Time.current - TransactionalMessagesJob::MAX_DURATION
13
+ q = Sidekiq::Queue.new(Coil.sidekiq_queue)
14
+ t = Time.current - q.latency - TransactionalMessagesJob::MAX_DURATION
10
15
 
11
16
  # Identify distinct message types, their associated job types, and
12
- # the distinct keys for which we have unprocessed messages (excluding
13
- # very recent messages, since a TransactionalMessagesJob may still be
14
- # processing those). Then, enqueue the necessary jobs for those keys.
17
+ # the distinct keys for which we have unprocessed messages.
18
+ #
19
+ # Exclude very recent messages, since a TransactionalMessagesJob could
20
+ # still be processing those.
21
+ #
22
+ # Exclude keys where a processor has already initiated several attempts,
23
+ # since that's a strong indicator that automatic retries are in play.
24
+ #
25
+ # Then, enqueue the appropriate jobs.
15
26
  message_parent_class.select(:type).distinct.pluck(:type).each do |type|
16
27
  message_class = message_class_for(type)
17
28
  next unless message_class.present?
@@ -20,7 +31,8 @@ module Coil
20
31
  message_class
21
32
  .unprocessed(processor_name: job_class.name)
22
33
  .where(created_at: nil...t)
23
- .distinct
34
+ .group(:key)
35
+ .having("MAX(processor_attempts) < ?", ATTEMPTS_THRESHOLD)
24
36
  .pluck(:key)
25
37
  .each { |k| job_class.perform_async(k) }
26
38
  end
@@ -0,0 +1,18 @@
1
+ # typed: false
2
+
3
+ class AddProcessorAttemptsToInboxMessages < ActiveRecord::Migration[6.0]
4
+ def change
5
+ comment = <<~DOC.squish
6
+ Number of processor attempts that have been initiated on this message.
7
+ DOC
8
+
9
+ add_column(
10
+ :coil_inbox_messages,
11
+ :processor_attempts,
12
+ :integer,
13
+ null: false,
14
+ default: 0,
15
+ comment:
16
+ )
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # typed: false
2
+
3
+ class AddProcessorAttemptsToOutboxMessages < ActiveRecord::Migration[6.0]
4
+ def change
5
+ comment = <<~DOC.squish
6
+ Number of processor attempts that have been initiated on this message.
7
+ DOC
8
+
9
+ add_column(
10
+ :coil_outbox_messages,
11
+ :processor_attempts,
12
+ :integer,
13
+ null: false,
14
+ default: 0,
15
+ comment:
16
+ )
17
+ end
18
+ end
data/lib/coil/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Coil
5
- VERSION = "1.6.1"
5
+ VERSION = "1.7.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coil
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.1
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Brennan
@@ -262,6 +262,8 @@ files:
262
262
  - db/migrate/20250102041225_remove_old_foreign_key_from_outbox_completions.rb
263
263
  - db/migrate/20260309171521_index_inbox_messages_on_type.rb
264
264
  - db/migrate/20260309173914_index_outbox_messages_on_type.rb
265
+ - db/migrate/20260314134950_add_processor_attempts_to_inbox_messages.rb
266
+ - db/migrate/20260314162532_add_processor_attempts_to_outbox_messages.rb
265
267
  - lib/coil.rb
266
268
  - lib/coil/engine.rb
267
269
  - lib/coil/queue_locking.rb