job_contracts 0.1.0 → 0.1.3

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: 8ccfdc1005a713277b03897aaf58f87db30e04d9697e92f462c8712eeb4faf8d
4
- data.tar.gz: 3f3a4d679a61701c986cd858ca259b1208a2f3d82bd94681b96e884252fb4403
3
+ metadata.gz: b3ed33558c769a0bc1ef0d74e1c066b5dd3de20069d6f0ea9a37177294080969
4
+ data.tar.gz: 8d89d059123f45dfcc3ce399cb51e13e23d6f6b3b6a7c7a93d06ffd2130f8de4
5
5
  SHA512:
6
- metadata.gz: 41d343fa07382f35997fe9a6a75410fcfaf8f599b42cf9421349ae4084ed49c5b5467579b83fd326992c27bf4d3ec64bbb38882ee370e297d8688dcd571131c4
7
- data.tar.gz: 95336d3dd5464cbb7731ce25c946f5502a7cbc2158a7ea73ef727726b9226e7d660a735351bc5260f783cf6e3ab1a6bc93adeac7c15464a81288863cd70896f9
6
+ metadata.gz: 4a575f78c5fc94884dcc453bcc4931f12dbe30f904f5cc9915d69c291cf88f82b6ec84dd0ce85067cbd6647b8db4e232b8e3ee6f0201056dc01d137a7fdf1dc3
7
+ data.tar.gz: 43ea19008133a39f25661912029c23a42523b462f9cc882a819cd989c8f5ced4bcb7046a0d1657d8b29439647335e911974c7295bcf4fcc0daa5e16d3acd96aa
data/README.md CHANGED
@@ -1,12 +1,276 @@
1
+ [![Lines of Code](http://img.shields.io/badge/lines_of_code-232-brightgreen.svg?style=flat)](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/)
2
+ [![Code Quality](https://app.codacy.com/project/badge/Grade/f604d4bc6db0474c802ef51182732488)](https://www.codacy.com/gh/hopsoft/job_contracts/dashboard?utm_source=github.com&utm_medium=referral&utm_content=hopsoft/job_contracts&utm_campaign=Badge_Grade)
3
+ ![Tests](https://github.com/hopsoft/job_contracts/actions/workflows/test.yml/badge.svg)
4
+ [![Gem Version](https://badge.fury.io/rb/job_contracts.svg)](https://badge.fury.io/rb/job_contracts)
5
+
1
6
  # Job Contracts
2
7
 
3
- ## Enforceable contracts for background jobs
8
+ ## Test-like assurances for jobs
9
+
10
+ Have you ever wanted to prevent a background job from writing to the database or perhaps ensure that it completes within a fixed amount of time?
11
+
12
+ Contracts allow you to easily enforce guarantees like this.
13
+
14
+ <!-- Tocer[start]: Auto-generated, don't remove. -->
15
+
16
+ ## Table of Contents
17
+
18
+ - [Why use Contracts?](#why-use-contracts)
19
+ - [Quick Start](#quick-start)
20
+ - [Contracts](#contracts)
21
+ - [Breach of Contract](#breach-of-contract)
22
+ - [Anatomy of a Contract](#anatomy-of-a-contract)
23
+ - [Defining a Contract](#defining-a-contract)
24
+ - [Using a Contract](#using-a-contract)
25
+ - [Worker Formation/Topology](#worker-formationtopology)
26
+ - [Advanced Usage](#advanced-usage)
27
+ - [Sidekiq](#sidekiq)
28
+ - [Todo](#todo)
29
+ - [License](#license)
30
+ - [Sponsors](#sponsors)
31
+
32
+ <!-- Tocer[finish]: Auto-generated, don't remove. -->
33
+
34
+ ## Why use Contracts?
35
+
36
+ - Organize your code for better reuse, consistency, and maintainability
37
+ - Refine your telemetry and instrumentation efforts
38
+ - Improve job performance via enforced *(SLAs/SLOs/SLIs)*
39
+ - Monitor and manage job queue backpressure
40
+ - Improve your worker formation/topology to support high throughput
41
+
42
+ ## Quick Start
43
+
44
+ Imagine you want to ensure a specific job completes within 5 seconds of being enqueued.
45
+
46
+ ```ruby
47
+ class ImportantJob < ApplicationJob
48
+ include JobContracts::Contractable
49
+
50
+ queue_as :default
51
+ add_contract JobContracts::DurationContract.new(max: 5.seconds)
52
+
53
+ def perform
54
+ # logic...
55
+ end
56
+
57
+ # default callback that's invoked if the contract is breached
58
+ def contract_breached!(contract)
59
+ # handle breach...
60
+ end
61
+ end
62
+ ```
63
+
64
+ *How to handle a [__breach of contract__](#breach-of-contract).*
65
+
66
+ ## Contracts
67
+
68
+ A contract is an agreement that a job should fulfill.
69
+ Failing to satisfy the contract is considered a __breach of contract__.
70
+
71
+ Contracts help you track `actual` results and compare them to `expected` outcomes.
72
+ For example, this project has a default set of contracts that verify the following:
73
+
74
+ - That a job will [execute within a set amount of time](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/duration_contract.rb)
75
+ - That a job is only [performed on a specific queue](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/queue_name_contract.rb)
76
+ - That a job [does not write to the database](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/read_only_contract.rb)
77
+
78
+ ### Breach of Contract
79
+
80
+ A __breach of contract__ is similar to a test failure; however, the breach can be handled in many different ways.
81
+
82
+ - Log and instrument the breach and continue
83
+ - Halt processing of the job and all other contracts and raise an exception
84
+ - Move the job to a queue where the contract will not be enforced
85
+ - etc...
86
+
87
+ *Mix and match any combination of these options to support your requirements.*
88
+
89
+ ### Anatomy of a Contract
90
+
91
+ Contracts support the following constructor arguments.
92
+
93
+ - __`trigger`__ `[Symbol] (:before, *:after)` - when contract enforcement takes place, *before or after perform*
94
+ - __`halt`__ `[Boolean] (true, *false)` - indicates whether or not to stop processing when the contract is breached
95
+ - __`queues`__ `[Array<String,Symbol>]` - a list of queue names where this contract will be enforced _(defaults to the configured queue, or `*` if the queue has not beeen configured)_
96
+ - __`expected`__ `[Hash]` - a dictionary of contract expectations
97
+
98
+ ### Defining a Contract
99
+
100
+ Here's a contrived, but simple, example that ensures the first argument passed to perform fits within a specific range of values.
101
+
102
+ ```ruby
103
+ # app/contracts/argument_contract.rb
104
+ class ArgumentContract < JobContracts::Contract
105
+ def initialize(range:)
106
+ # enforced on all queues
107
+ super queues: ["*"], expected: {range: range}
108
+ end
109
+
110
+ def enforce!(contractable)
111
+ actual[:argument] = contractable.arguments.first
112
+ self.satisfied = expected[:range].cover?(actual[:argument])
113
+ super
114
+ end
115
+ end
116
+ ```
117
+
118
+ ### Using a Contract
119
+
120
+ Here's how to use the `ArgumentContract` in a job.
121
+
122
+ ```ruby
123
+ # app/jobs/argument_example_job.rb
124
+ class ArgumentExampleJob < ApplicationJob
125
+ include JobContracts::Contractable
126
+
127
+ queue_as :default
128
+ add_contract ArgumentContract.new(range: (1..10))
129
+
130
+ def perform(arg)
131
+ # logic...
132
+ end
133
+
134
+ # default callback that's invoked if the contract is breached
135
+ def contract_breached!(contract)
136
+ # handle breach...
137
+ end
138
+ end
139
+ ```
4
140
 
5
- ## Todos
141
+ This job will help ensure that the argument passed to perform is between 1 and 10.
142
+ *It's up to you to determine how to handle a breach of contract.*
6
143
 
7
- - [ ] Add documentation
8
- - [ ] Add automated tests
144
+ ## Worker Formation/Topology
145
+
146
+ Thoughtful Rails applications often use specialized worker formations.
147
+
148
+ A simple formation might be to use two sets of workers.
149
+ One set dedicated to fast low-latency jobs with plenty of dedicated compute resources *(CPUs, processes, threads, etc...)*,
150
+ with another set dedicated to slower jobs that uses fewer compute resources.
151
+
152
+ <img width="593" alt="Untitled 2 2022-04-29 15-06-13" src="https://user-images.githubusercontent.com/32920/166069103-e316dcc7-e601-43d0-90df-ad0eda20409b.png">
153
+
154
+ Say we determine that fast low-latency jobs should __not__ write to the database.
155
+
156
+ We can use a [`ReadOnlyContract`](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/read_only_contract.rb)
157
+ to enforce this decision. If the contract is breached, we can notify our apm/monitoring service and re-enqueue the job to a slower queue *(worker set)* where database writes are permitted.
158
+ This will ensure that our fast low-latency queue doesn't get clogged with slow-running jobs.
159
+
160
+ Here's an example job implementation that accomplishes this.
161
+
162
+ ```ruby
163
+ class FastJob < ApplicationJob
164
+ include JobContracts::Contractable
165
+
166
+ # Configure the queue before adding contracts
167
+ # It will be used as the default enforcement queue for contracts
168
+ queue_as :critical
169
+
170
+ # Only enforces on the critical queue
171
+ # This allows us to halt job execution and reenqueue the job to a different queue
172
+ # where the contract will not be enforced
173
+ #
174
+ # NOTE: the arg `queues: [:critical]` is default behavior in this example
175
+ # we're setting it explicitly here for illustration purposes
176
+ add_contract JobContracts::ReadOnlyContract.new(queues: [:critical])
177
+
178
+ def perform
179
+ # logic that shouldn't write to the database,
180
+ # but might accidentally due to complex or opaque internals
181
+ end
182
+
183
+ def contract_breached!(contract)
184
+ # log and notify apm/monitoring service
185
+
186
+ # re-enqueue to a different queue
187
+ # where the database write will be permitted
188
+ # i.e. where the contract will not be enforced
189
+ enqueue queue: :default
190
+ end
191
+ end
192
+ ```
193
+
194
+ *Worker formations can be designed in countless ways to handle incredibly sophisticated requirements and operational constraints.
195
+ The only real limitation is your creativity.*
196
+
197
+ ## Advanced Usage
198
+
199
+ It's possible to override the default callback method that handles contract breaches.
200
+
201
+ ```ruby
202
+ class ImportantJob < ApplicationJob
203
+ include JobContracts::Contractable
204
+
205
+ queue_as :default
206
+ on_contract_breach :take_action
207
+ add_contract JobContracts::DurationContract.new(max: 5.seconds)
208
+
209
+ def perform
210
+ # logic...
211
+ end
212
+
213
+ def take_action(contract)
214
+ # handle breach...
215
+ end
216
+ end
217
+ ```
218
+
219
+ ```ruby
220
+ class ImportantJob < ApplicationJob
221
+ include JobContracts::Contractable
222
+
223
+ queue_as :default
224
+ on_contract_breach -> (contract) { # take action... }
225
+
226
+ add_contract JobContracts::DurationContract.new(max: 5.seconds)
227
+
228
+ def perform
229
+ # logic...
230
+ end
231
+ end
232
+ ```
233
+
234
+ ## Sidekiq
235
+
236
+ `Sidekiq::Job`s are also supported.
237
+
238
+ ```ruby
239
+ class ImportantJob
240
+ include Sidekiq::Job
241
+ include JobContracts::SidekiqContractable
242
+
243
+ sidekiq_options queue: :default
244
+ add_contract JobContracts::DurationContract.new(max: 1.second)
245
+
246
+ def perform
247
+ # logic...
248
+ end
249
+
250
+ # default callback that's invoked if the contract is breached
251
+ def contract_breached!(contract)
252
+ # handle breach...
253
+ end
254
+ end
255
+ ```
256
+
257
+ ## Todo
258
+
259
+ - [ ] Sidekiq tests
9
260
 
10
261
  ## License
11
262
 
12
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
263
+ The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
264
+
265
+ ## Sponsors
266
+
267
+ This project is sponsored by [Orbit.love](https://orbit.love/?utm_source=github&utm_medium=repo&utm_campaign=hopsoft&utm_content=job_contracts) *(mission control for your community)*.
268
+
269
+ <a href="https://orbit.love/?utm_source=github&utm_medium=repo&utm_campaign=hopsoft&utm_content=job_contracts">
270
+ <img height="50" src="https://user-images.githubusercontent.com/32920/166343064-55f92cdb-c81b-4f85-80a8-167bfda73c85.png"></img>
271
+ </a>
272
+
273
+ ---
274
+
275
+ This effort was partly inspired by a presentation at [Sin City Ruby](https://www.sincityruby.com/) from our friends on the platform team at [Gusto](https://gusto.com/).
276
+ Their presentation validated some of my prior solutions aimed at accomplishing similar goals and motivated me to extract that work into a GEM.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/setup"
2
4
 
3
5
  require "bundler/gem_tasks"
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
1
5
  module JobContracts
2
6
  # Universal mixin for jobs/workers
3
7
  module Contractable
@@ -5,33 +9,43 @@ module JobContracts
5
9
 
6
10
  module Prepends
7
11
  extend ActiveSupport::Concern
12
+ include MonitorMixin
8
13
 
9
14
  def perform(*args)
10
- should_perform = true
11
- self.class.contracts_to_enforce_before_perform.each do |contract|
12
- contract.enforce! self
13
- should_perform = false if contract.breached? && contract.halt?
15
+ # fetch sidekiq job/worker metadata on main thread
16
+ try :sidekiq_job_metadata
17
+
18
+ halted = false
19
+ contracts.select(&:before?).each do |contract|
20
+ contract.enforce! self unless halted
21
+ halted = true if contract.breached? && contract.halt?
14
22
  end
15
- super if should_perform
16
- self.class.contracts_to_enforce_after_perform.each do |contract|
17
- contract.enforce! self
23
+ super unless halted
24
+ ensure
25
+ # enforce after contracts in a separate thread to ensure that any perform related behavior
26
+ # defined in ContractablePrepends will finish executing before we invoke contract.enforce!
27
+ Thread.new do
28
+ sleep 0
29
+ synchronize do
30
+ contracts.select(&:after?).each do |contract|
31
+ contract.enforce! self unless halted
32
+ end
33
+ end
18
34
  end
19
35
  end
20
36
  end
21
37
 
22
38
  module ClassMethods
23
- attr_reader :after_contract_breach_callback
24
-
25
- def contracts_to_enforce_before_perform
26
- @contracts_to_enforce_before_perform ||= Set.new
39
+ def contracts
40
+ @contracts ||= Set.new
27
41
  end
28
42
 
29
- def contracts_to_enforce_after_perform
30
- @contracts_to_enforce_after_perform ||= Set.new
43
+ def on_contract_breach(value = nil, &block)
44
+ @on_contract_breach_callback = value || block
31
45
  end
32
46
 
33
- def after_contract_breach(value = nil, &block)
34
- @after_contract_breach_callback = value || block
47
+ def on_contract_breach_callback
48
+ @on_contract_breach_callback ||= :contract_breached!
35
49
  end
36
50
 
37
51
  def add_contract(contract)
@@ -45,21 +59,21 @@ module JobContracts
45
59
 
46
60
  prepend JobContracts::Contractable::Prepends
47
61
 
48
- if contract.trigger == :before
49
- contracts_to_enforce_before_perform << contract
50
- else
51
- contracts_to_enforce_after_perform << contract
52
- end
62
+ contract.queues << queue_name.to_s if contract.queues.blank? && queue_name.present?
63
+ contract.queues << "*" if contract.queues.blank?
64
+ contracts << contract
53
65
  end
54
66
  end
55
67
 
56
- def after_contract_breach(contract)
57
- method = self.class.after_contract_breach_callback
58
- case method
59
- when Proc then method.call(contract)
60
- when String, Symbol then send(method, contract)
61
- else raise NotImplementedError
62
- end
68
+ delegate :contracts, to: "self.class"
69
+
70
+ def breached_contracts
71
+ @breached_contracts ||= Set.new
72
+ end
73
+
74
+ # Default callback
75
+ def contract_breached!
76
+ # noop / override in job subclasses
63
77
  end
64
78
  end
65
79
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "contractable"
2
4
 
3
5
  module JobContracts
@@ -6,41 +8,32 @@ module JobContracts
6
8
  extend ActiveSupport::Concern
7
9
  include Contractable
8
10
 
9
- class MetadataNotFoundError < StandardError; end
10
-
11
- def metadata
12
- hit = nil
13
- begin
14
- attempts ||= 1
15
- hit = Sidekiq::WorkSet.new.find do |_pid, _tid, work|
16
- work.dig("payload", "jid") == jid
17
- end
18
- raise MetadataNotFoundError if hit.blank?
19
- rescue MetadataNotFoundError
20
- # The WorkSet only updates every 5 seconds
21
- # SEE: https://github.com/mperham/sidekiq/wiki/API#workers
22
- # Re-attempt up to 10 times with a simple backoff strategy (up to 5.5 seconds)
23
- # TODO: Is there a faster and more reliable way to fetch the job's metadata after perform has begun?
24
- # May need to query Redis directly if the data is still in there at this point
25
- attempts += 1
26
- if attempts <= 10
27
- sleep 0.1 * attempts
28
- retry
29
- end
11
+ module ClassMethods
12
+ # Matches the ActiveJob API
13
+ def queue_name
14
+ sidekiq_options_hash["queue"]
30
15
  end
16
+ end
31
17
 
32
- hit&.last || {}
18
+ # Metadata used to enqueue the job
19
+ def sidekiq_job_hash
20
+ @sidekiq_job_hash ||= {}
33
21
  end
34
22
 
35
23
  # Matches the ActiveJob API
36
24
  def queue_name
37
- metadata["queue"]
25
+ sidekiq_job_hash["queue"]
38
26
  end
39
27
 
40
28
  # Matches the ActiveJob API
41
29
  def enqueued_at
42
- seconds = metadata.dig("payload", "enqueued_at")
30
+ seconds = sidekiq_job_hash["enqueued_at"]
43
31
  (seconds ? Time.at(seconds) : nil)&.iso8601.to_s
44
32
  end
33
+
34
+ # Matches the ActiveJob API
35
+ def arguments
36
+ sidekiq_job_hash["args"] || []
37
+ end
45
38
  end
46
39
  end
@@ -1,33 +1,36 @@
1
- require "observer"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module JobContracts
4
4
  class Contract
5
- include Observable
5
+ attr_reader :trigger, :queues
6
6
 
7
- attr_reader :trigger
8
-
9
- def initialize(trigger: :after, halt: false, **kwargs)
7
+ def initialize(trigger: :after, halt: false, queues: [], expected: {})
10
8
  @trigger = trigger.to_sym
11
9
  @halt = halt
12
- expect.merge! kwargs
10
+ @queues = Set.new(queues.map(&:to_s))
11
+ self.expected.merge! expected
13
12
  end
14
13
 
15
- def expect
16
- @expect ||= HashWithIndifferentAccess.new
14
+ def expected
15
+ @expected ||= HashWithIndifferentAccess.new
17
16
  end
18
17
 
19
18
  def actual
20
19
  @actual ||= HashWithIndifferentAccess.new
21
20
  end
22
21
 
22
+ def should_enforce?(contractable)
23
+ return true if queues.include?("*")
24
+ queues.include? contractable.queue_name.to_s
25
+ end
26
+
23
27
  # Method to be implemented by subclasses
24
28
  # NOTE: subclasses should update `actual`, set `satisfied`, and call `super`
25
29
  def enforce!(contractable)
26
- add_observer contractable, :after_contract_breach
27
- changed if breached?
28
- notify_observers self
29
- ensure
30
- delete_observer contractable
30
+ return unless should_enforce?(contractable)
31
+ return if satisfied?
32
+ contractable.breached_contracts << self
33
+ invoke_contract_breach_callback contractable
31
34
  end
32
35
 
33
36
  def satisfied?
@@ -42,8 +45,33 @@ module JobContracts
42
45
  !!@halt
43
46
  end
44
47
 
48
+ def before?
49
+ trigger == :before
50
+ end
51
+
52
+ def after?
53
+ trigger == :after
54
+ end
55
+
56
+ def to_h
57
+ HashWithIndifferentAccess.new(
58
+ name: self.class.name,
59
+ trigger: trigger,
60
+ halt: halt?,
61
+ queues: queues.to_a,
62
+ expected: expected,
63
+ actual: actual
64
+ )
65
+ end
66
+
45
67
  protected
46
68
 
47
69
  attr_accessor :satisfied
70
+
71
+ def invoke_contract_breach_callback(contractable)
72
+ callback = contractable.class.on_contract_breach_callback
73
+ callback = contractable.method(callback.to_sym) unless callback.is_a?(Proc)
74
+ callback.call self
75
+ end
48
76
  end
49
77
  end
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "contract"
2
4
 
3
5
  module JobContracts
4
6
  class DurationContract < Contract
5
- def initialize(duration:)
6
- super
7
+ def initialize(max:, queues: ["*"])
8
+ super queues: queues, expected: {max: max}
7
9
  end
8
10
 
9
11
  def enforce!(contractable)
10
12
  actual[:duration] = (Time.current - Time.parse(contractable.enqueued_at)).seconds
11
- self.satisfied = actual[:duration] < expect[:duration].seconds
13
+ self.satisfied = actual[:duration] < expected[:max].seconds
12
14
  super
13
15
  end
14
16
  end
@@ -1,15 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "contract"
2
4
 
3
5
  module JobContracts
4
6
  class QueueNameContract < Contract
5
7
  def initialize(queue_name:)
6
- super trigger: :before, halt: true, queue_name: queue_name
8
+ super(
9
+ trigger: :before,
10
+ halt: true,
11
+ queues: ["*"],
12
+ expected: {queue_name: queue_name.to_s}
13
+ )
7
14
  end
8
15
 
9
16
  def enforce!(contractable)
10
- queue_name = contractable.queue_name
11
- actual[:queue_name] = queue_name
12
- self.satisfied = queue_name.to_s == expect[:queue_name].to_s
17
+ actual[:queue_name] = contractable.queue_name.to_s
18
+ self.satisfied = contractable.queue_name.to_s == expected[:queue_name]
13
19
  super
14
20
  end
15
21
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "contract"
2
4
 
3
5
  module JobContracts
@@ -6,7 +8,12 @@ module JobContracts
6
8
  extend ActiveSupport::Concern
7
9
 
8
10
  def perform(*args)
9
- ActiveRecord::Base.while_preventing_writes do
11
+ contract = contracts.find { |c| c.is_a?(ReadOnlyContract) }
12
+ if contract.should_enforce?(self)
13
+ ActiveRecord::Base.while_preventing_writes do
14
+ super
15
+ end
16
+ else
10
17
  super
11
18
  end
12
19
  rescue ActiveRecord::ReadOnlyError => error
@@ -21,11 +28,8 @@ module JobContracts
21
28
  end
22
29
  end
23
30
 
24
- # def initialize
25
- # super trigger: :before
26
- # end
27
-
28
31
  def enforce!(contractable)
32
+ self.satisfied = true
29
33
  if contractable.read_only_error.present?
30
34
  actual[:error] = contractable.read_only_error.message
31
35
  self.satisfied = false
@@ -1,4 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+ require_relative "sidekiq_job_hash_middleware"
5
+
1
6
  module JobContracts
2
7
  class Railtie < ::Rails::Railtie
8
+ initializer "job_contracts.register_sidekiq_middleware" do
9
+ Sidekiq.server_middleware.add SidekiqJobHashMiddleware
10
+ end
3
11
  end
4
12
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobContracts
4
+ class SidekiqJobHashMiddleware
5
+ def call(worker, job, _queue)
6
+ worker.instance_variable_set :@sidekiq_job_hash, job
7
+ yield
8
+ end
9
+ end
10
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JobContracts
2
- VERSION = "0.1.0"
4
+ VERSION = "0.1.3"
3
5
  end
data/lib/job_contracts.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "job_contracts/version"
2
4
  require "job_contracts/railtie"
3
5
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: job_contracts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Hopkins
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-22 00:00:00.000000000 Z
11
+ date: 2022-05-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -42,17 +42,73 @@ dependencies:
42
42
  name: standard
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: magic_frozen_string_literal
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
46
74
  - !ruby/object:Gem::Version
47
- version: 1.10.0
75
+ version: '0'
48
76
  type: :development
49
77
  prerelease: false
50
78
  version_requirements: !ruby/object:Gem::Requirement
51
79
  requirements:
52
- - - "~>"
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-doc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: tocer
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
53
109
  - !ruby/object:Gem::Version
54
- version: 1.10.0
55
- description: Enforceable contracts for background jobs
110
+ version: '0'
111
+ description: Enforceable contracts for jobs
56
112
  email:
57
113
  - natehop@gmail.com
58
114
  executables: []
@@ -70,6 +126,7 @@ files:
70
126
  - lib/job_contracts/contracts/queue_name_contract.rb
71
127
  - lib/job_contracts/contracts/read_only_contract.rb
72
128
  - lib/job_contracts/railtie.rb
129
+ - lib/job_contracts/sidekiq_job_hash_middleware.rb
73
130
  - lib/job_contracts/version.rb
74
131
  homepage: https://github.com/hopsoft/job_contracts
75
132
  licenses:
@@ -93,8 +150,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
150
  - !ruby/object:Gem::Version
94
151
  version: '0'
95
152
  requirements: []
96
- rubygems_version: 3.3.7
153
+ rubygems_version: 3.3.3
97
154
  signing_key:
98
155
  specification_version: 4
99
- summary: Enforceable contracts for background jobs
156
+ summary: Enforceable contracts for jobs
100
157
  test_files: []