job_contracts 0.1.0 → 0.1.3

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: 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: []