acidic_job 1.0.0.pre11 → 1.0.0.pre12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53b5f12879bffb6a461e3574b96f8a41464ec83ce0a21eaa8e244261ce53a9be
4
- data.tar.gz: 7b90c9864347564706466e62c61f08eb377d8e3a33475ad40ba559de59e7b70e
3
+ metadata.gz: 1af7bca23aa5ead8fed158fb73565736555236bf4515ab73f631a3013e138f12
4
+ data.tar.gz: cc1118240efe70d71dcd7c2eb07435f3e56934c2c3e8653848a9c3467dc60b83
5
5
  SHA512:
6
- metadata.gz: '08b4a0bdd41ac80f8ac879003f455f577f93fc431acdb780dfd3e54260b07a61bf0557c3e7d99192e1ad809134b709211fd2c5efe581b797563187dcfbb34467'
7
- data.tar.gz: 66e8b3bfe0226c3e1f5168d45431a01a36ee3dfb796c855c49fc009296cb223d33922bc2db5febbe6d6453b4a01c46661b805aa1e2c80a4e37f3dce183b5e748
6
+ metadata.gz: 90073b7d2964d65886ca2fd4320481455e04218db92ab3b2ccd65bdffc527b03f8c928c58590ed62cf7734c1702687978dd5f1406ea740f13f1fbccb7c27b48d
7
+ data.tar.gz: 9f2910c1620f489a1bb5a310821ead5363671aa0ffc9a21f909894f43c2de52b056ee096f17455fb8b34bd55d8f521ca71760df3959b8b0c1f90562c50129ff4
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.0.2
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- acidic_job (1.0.0.pre11)
4
+ acidic_job (1.0.0.pre12)
5
5
  activerecord (>= 6.1.0)
6
6
  activesupport
7
7
 
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # AcidicJob
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/acidic_job.svg)](https://badge.fury.io/rb/acidic_job)
4
+ ![main workflow](https://github.com/fractaledmind/acidic_job/actions/workflows/main.yml/badge.svg)
5
+
3
6
  ### Idempotent operations for Rails apps (for ActiveJob or Sidekiq)
4
7
 
5
8
  At the conceptual heart of basically any software are "operations"—the discrete actions the software performs. Rails provides a powerful abstraction layer for building operations in the form of `ActiveJob`, or we Rubyists can use the tried and true power of pure `Sidekiq`. With either we can easily trigger from other Ruby code throughout our Rails application (controller actions, model methods, model callbacks, etc.); we can run operations both synchronously (blocking execution and then returning its response to the caller) and asychronously (non-blocking and the caller doesn't know its response); and we can also retry a specific operation if needed seamlessly.
@@ -193,16 +196,20 @@ class ExampleJob < ActiveJob::Base
193
196
  end
194
197
  ```
195
198
 
196
- These options cover the two common situations, but sometimes our systems need finer-grained control. For example, our job might take some record as the job argument, but we need to use a combination of the record identifier and record status as the foundation for the idempotency key. In these cases we can't configure the idempotency key logic at the class level, so instead we can provide the logic when enqueuing the job itself.
197
-
198
- When you call any `deliver_acidicly` or `perform_acidicly` method you can pass an optional `unique_by` argument which will be used to generate the idempotency key:
199
+ These options cover the two common situations, but sometimes our systems need finer-grained control. For example, our job might take some record as the job argument, but we need to use a combination of the record identifier and record status as the foundation for the idempotency key. In these cases you can pass a `Proc` to an `acidic_by` class method:
199
200
 
200
201
  ```ruby
201
- ExampleJob.perform_acidicly(unique_by: { id: record.id, status: record.status })
202
- UserMailer.with(user, record).deliver_acidicly(unique_by: [user.id, record.id, record.status])
202
+ class ExampleJob < ActiveJob::Base
203
+ include AcidicJob
204
+ acidic_by ->(record:) { [record.id, record.status] }
205
+
206
+ def perform(record:)
207
+ # the idempotency key will be based on whatever the values of `record.id` and `record.status` are
208
+ end
209
+ end
203
210
  ```
204
211
 
205
- As you see, the value to the `unique_by` option can be a Hash or an Array or even a simple scalar value.
212
+ > **Note:** The signature of the `acidic_by` proc _needs to match the signature_ of the job's `perform` method.
206
213
 
207
214
 
208
215
  ### Sidekiq Callbacks
@@ -7,19 +7,14 @@ module AcidicJob
7
7
  module ActionMailer
8
8
  extend ActiveSupport::Concern
9
9
 
10
- def deliver_acidicly(_options = {}, unique_by: nil)
10
+ def deliver_acidicly(_options = {})
11
11
  job_class = ::ActionMailer::MailDeliveryJob
12
12
 
13
13
  job_args = [@mailer_class.name, @action.to_s, "deliver_now", @params, *@args]
14
14
  # for Sidekiq, this depends on the Sidekiq::Serialization extension
15
15
  serialized_job = job_class.new(job_args).serialize
16
16
  acidic_identifier = job_class.respond_to?(:acidic_identifier) ? job_class.acidic_identifier : :job_id
17
- # use either [1] provided uniqueness constraint or [2] computed key
18
- key = if unique_by
19
- IdempotencyKey.generate(unique_by: unique_by, job_class: job_class.name)
20
- else
21
- IdempotencyKey.new(acidic_identifier).value_for(serialized_job)
22
- end
17
+ key = IdempotencyKey.new(acidic_identifier).value_for(serialized_job)
23
18
 
24
19
  AcidicJob::Run.create!(
25
20
  staged: true,
@@ -24,13 +24,7 @@ module AcidicJob
24
24
  raise UnsupportedExtension unless defined?(::ActiveJob) && self < ::ActiveJob::Base
25
25
 
26
26
  serialized_job = serialize_with_arguments(*args, **kwargs)
27
- # use either [1] provided uniqueness constraint or [2] computed key
28
- key = if kwargs.key?(:unique_by) || kwargs.key?("unique_by")
29
- unique_by = [kwargs[:unique_by], kwargs["unique_by"]].compact.first
30
- IdempotencyKey.generate(unique_by: unique_by, job_class: name)
31
- else
32
- IdempotencyKey.new(acidic_identifier).value_for(serialized_job)
33
- end
27
+ key = IdempotencyKey.new(acidic_identifier).value_for(serialized_job)
34
28
 
35
29
  AcidicJob::Run.create!(
36
30
  staged: true,
@@ -6,12 +6,12 @@ module AcidicJob
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  class_methods do
9
- def deliver_acidicly(recipients, unique_by: nil)
10
- new.deliver_acidicly(recipients, unique_by: unique_by)
9
+ def deliver_acidicly(recipients)
10
+ new.deliver_acidicly(recipients)
11
11
  end
12
12
  end
13
13
 
14
- def deliver_acidicly(recipients, unique_by: nil)
14
+ def deliver_acidicly(recipients)
15
15
  # THIS IS A HACK THAT COPIES AND PASTES KEY PARTS OF THE `Noticed::Base` CODE
16
16
  # IN ORDER TO ALLOW US TO TRANSACTIONALLY DELIVER NOTIFICATIONS
17
17
  # THIS IS THUS LIABLE TO BREAK WHENEVER THAT GEM IS UPDATED
@@ -37,12 +37,7 @@ module AcidicJob
37
37
  }
38
38
  serialized_job = job_class.send(:job_or_instantiate, args).serialize
39
39
  acidic_identifier = job_class.respond_to?(:acidic_identifier) ? job_class.acidic_identifier : :job_id
40
- # use either [1] provided uniqueness constraint or [2] computed key
41
- key = if unique_by
42
- IdempotencyKey.generate(unique_by: unique_by, job_class: job_class.name)
43
- else
44
- IdempotencyKey.new(acidic_identifier).value_for(serialized_job)
45
- end
40
+ key = IdempotencyKey.new(acidic_identifier).value_for(serialized_job)
46
41
 
47
42
  AcidicJob::Run.create!(
48
43
  staged: true,
@@ -34,9 +34,10 @@ module AcidicJob
34
34
  end
35
35
  end
36
36
 
37
- def serialize_job(args = [], _kwargs = nil)
37
+ def serialize_job(*args, **kwargs)
38
38
  # `@args` is only set via `deserialize`; it is not a standard Sidekiq thing
39
39
  arguments = args || @args
40
+ arguments += [kwargs] unless kwargs.empty?
40
41
  normalized_args = ::Sidekiq.load_json(::Sidekiq.dump_json(arguments))
41
42
  item = { "class" => self.class, "args" => normalized_args, "jid" => jid }
42
43
  sidekiq_options = sidekiq_options_hash || {}
@@ -58,13 +59,7 @@ module AcidicJob
58
59
  class_methods do
59
60
  def perform_acidicly(*args, **kwargs)
60
61
  serialized_job = serialize_with_arguments(*args, **kwargs)
61
- # use either [1] provided uniqueness constraint or [2] computed key
62
- key = if kwargs.key?(:unique_by) || kwargs.key?("unique_by")
63
- unique_by = [kwargs[:unique_by], kwargs["unique_by"]].compact.first
64
- IdempotencyKey.generate(unique_by: unique_by, job_class: name)
65
- else
66
- IdempotencyKey.new(acidic_identifier).value_for(serialized_job)
67
- end
62
+ key = IdempotencyKey.new(acidic_identifier).value_for(serialized_job)
68
63
 
69
64
  AcidicJob::Run.create!(
70
65
  staged: true,
@@ -2,21 +2,22 @@
2
2
 
3
3
  module AcidicJob
4
4
  class IdempotencyKey
5
- def self.generate(unique_by:, job_class:)
6
- new(:job_args).value_for({ "job_class" => job_class }, Marshal.dump(unique_by))
7
- end
8
-
9
5
  def initialize(identifier = :job_id)
10
6
  @identifier = identifier
11
7
  end
12
8
 
13
9
  def value_for(hash_or_job, *args, **kwargs)
14
- return value_from_job_args(hash_or_job, *args, **kwargs) if @identifier == :job_args
15
-
16
- value = if hash_or_job.is_a?(Hash)
17
- value_from_job_id_for_hash(hash_or_job)
10
+ value = case @identifier
11
+ when Proc
12
+ value_from_proc(hash_or_job, *args, **kwargs)
13
+ when :job_args
14
+ value_from_job_args(hash_or_job, *args, **kwargs)
18
15
  else
19
- value_from_job_id_for_obj(hash_or_job)
16
+ if hash_or_job.is_a?(Hash)
17
+ value_from_job_id_for_hash(hash_or_job)
18
+ else
19
+ value_from_job_id_for_obj(hash_or_job)
20
+ end
20
21
  end
21
22
 
22
23
  value || value_from_job_args(hash_or_job, *args, **kwargs)
@@ -62,5 +63,12 @@ module AcidicJob
62
63
 
63
64
  Digest::SHA1.hexdigest [worker_class, args, kwargs].flatten.join
64
65
  end
66
+
67
+ def value_from_proc(_hash_or_job, *args, **kwargs)
68
+ return if args.empty? && kwargs.empty?
69
+
70
+ idempotency_args = Array(@identifier.call(*args, **kwargs))
71
+ Digest::SHA1.hexdigest idempotency_args.flatten.join
72
+ end
65
73
  end
66
74
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AcidicJob
4
- VERSION = "1.0.0.pre11"
4
+ VERSION = "1.0.0.pre12"
5
5
  end
data/lib/acidic_job.rb CHANGED
@@ -50,6 +50,7 @@ module AcidicJob
50
50
  klass.instance_variable_set(:@acidic_identifier, :job_id)
51
51
  klass.define_singleton_method(:acidic_by_job_id) { @acidic_identifier = :job_id }
52
52
  klass.define_singleton_method(:acidic_by_job_args) { @acidic_identifier = :job_args }
53
+ klass.define_singleton_method(:acidic_by) { |proc| @acidic_identifier = proc }
53
54
  end
54
55
 
55
56
  included do
@@ -67,6 +68,19 @@ module AcidicJob
67
68
  end
68
69
  end
69
70
 
71
+ def initialize(*args, **kwargs)
72
+ # ensure this instance variable is always defined
73
+ @__acidic_job_steps = []
74
+ @__acidic_job_args = args
75
+ @__acidic_job_kwargs = kwargs
76
+
77
+ if method(__method__).super_method.arity.zero?
78
+ super()
79
+ else
80
+ super(*args, **kwargs)
81
+ end
82
+ end
83
+
70
84
  def with_acidity(providing: {})
71
85
  # ensure this instance variable is always defined
72
86
  @__acidic_job_steps = []
@@ -107,7 +121,7 @@ module AcidicJob
107
121
 
108
122
  acidic_identifier = self.class.acidic_identifier
109
123
  @__acidic_job_idempotency_key ||= IdempotencyKey.new(acidic_identifier)
110
- .value_for(self, @__acidic_job_args, @__acidic_job_kwargs)
124
+ .value_for(self, *@__acidic_job_args, **@__acidic_job_kwargs)
111
125
  end
112
126
  # rubocop:enable Naming/MemoizedInstanceVariableName
113
127
 
@@ -191,7 +205,7 @@ module AcidicJob
191
205
 
192
206
  ActiveRecord::Base.transaction(isolation: isolation_level) do
193
207
  run = Run.find_by(idempotency_key: idempotency_key)
194
- serialized_job = serialize_job(@__acidic_job_args, @__acidic_job_kwargs)
208
+ serialized_job = serialize_job(*@__acidic_job_args, **@__acidic_job_kwargs)
195
209
 
196
210
  if run.present?
197
211
  # Programs enqueuing multiple jobs with different parameters but the
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acidic_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre11
4
+ version: 1.0.0.pre12
5
5
  platform: ruby
6
6
  authors:
7
7
  - fractaledmind
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-13 00:00:00.000000000 Z
11
+ date: 2022-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -63,6 +63,7 @@ files:
63
63
  - ".gitignore"
64
64
  - ".rubocop.yml"
65
65
  - ".ruby_version"
66
+ - ".tool-versions"
66
67
  - Gemfile
67
68
  - Gemfile.lock
68
69
  - LICENSE