Skip to content

Commit 970dcf1

Browse files
committed
🎉 interator-sidekiq created.
0 parents  commit 970dcf1

File tree

14 files changed

+836
-0
lines changed

14 files changed

+836
-0
lines changed

‎.github/ci.yml‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
- '*'
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Check out repository code
12+
uses: actions/checkout@v1
13+
14+
- name: Setup Ruby
15+
uses: ruby/setup-ruby@v1
16+
with:
17+
ruby-version: '3.0.3'
18+
bundler-cache: true
19+
20+
- name: Rspec - Build and run tests
21+
run: |
22+
gem install bundler
23+
bundle install --jobs 4 --retry 3
24+
bundle exec rspec spec

‎.gitignore‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
*.log
2+
*.gem
3+
*.rbc
4+
.bundle
5+
.config
6+
.yardoc
7+
Gemfile.lock
8+
InstalledFiles
9+
_yardoc
10+
doc/
11+
lib/bundler/man
12+
pkg
13+
rdoc
14+
spec/reports
15+
test/tmp
16+
test/version_tmp
17+
tmp
18+
*.bundle
19+
*.so
20+
*.o
21+
*.a
22+
mkmf.log
23+
.ruby-version
24+
.ruby-gemset
25+
.idea
26+
.gs
27+
.byebug_history
28+
covarage

‎.rspec‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
--color
2+
--require spec_helper

‎Gemfile‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
source 'https://rubygems.org'
4+
5+
gemspec
6+
7+
gem 'bundler'
8+
gem 'rake'
9+
gem 'rspec'

‎LICENSE.txt‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Copyright (c) 2022, Gabriel Rocha
2+
3+
MIT License
4+
5+
Permission is hereby granted, free of charge, to any person obtaining
6+
a copy of this software and associated documentation files (the
7+
"Software"), to deal in the Software without restriction, including
8+
without limitation the rights to use, copy, modify, merge, publish,
9+
distribute, sublicense, and/or sell copies of the Software, and to
10+
permit persons to whom the Software is furnished to do so, subject to
11+
the following conditions:
12+
13+
The above copyright notice and this permission notice shall be
14+
included in all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

‎README.md‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Interactor::Sidekiq
2+
3+
Provides [Interactor](https://github.com/collectiveidea/interactor) with asynchronous action using [Sidekiq](https://github.com/mperham/sidekiq).
4+
5+
## Installation
6+
7+
```ruby
8+
gem 'interactor-sidekiq', '~> 1.0'
9+
```
10+
11+
## #async_call
12+
You can now add asynchronous behavior to both types of inetagents (basic interactors and organizers).
13+
14+
```ruby
15+
class RegularAction
16+
include Interactor
17+
18+
def call
19+
{ context: context.variable }
20+
end
21+
end
22+
```
23+
With the above example we can already use **async_call**.
24+
25+
```sh
26+
>> RegularAction.call(key: 'value')
27+
#<Interactor::Context key="value">
28+
>> Sidekiq::Queues.jobs_by_queue
29+
{}
30+
```
31+
```sh
32+
>> RegularAction.async_call(key: 'value')
33+
#<Interactor::Context key="value">
34+
>> Sidekiq::Queues.jobs_by_queue
35+
{"default"=>[{"retry"=>true, "queue"=>"default", "args"=>["{\"key\":\"value\",\"interactor_class\":\"RegularAction\"}"], "class"=>"Interactor::SidekiqWorker::Worker", "jid"=>"91a374e10e584b02cb84eec3", "created_at"=>1656283783.3459146, "enqueued_at"=>1656283783.3459556}]}
36+
```
37+
38+
You can pass the **sidekiq_options** and **sidekiq_scheduling_options** to customize the behavior of the **async_call** method.
39+
40+
#### Passing options from sidekiq
41+
42+
To set custom [sidekiq_options] (https://github.com/mperham/sidekiq/wiki/Advanced-Options#workers) you can add `sidekiq_options` class method in your interactors - these options will be passed to Sidekiq `` set ` before scheduling the asynchronous worker.
43+
44+
#### Passing scheduling options
45+
46+
In order to be able to schedule jobs for future execution following [Scheduled Jobs](https://github.com/mperham/sidekiq/wiki/Scheduled-Jobs), you can add the `sidekiq_schedule_options` class method in your subscriber definition - these options will be passed to Sidekiq's `perform_in` method when the worker is called.
47+
48+
```sh
49+
>> RegularAction.async_call(message: 'hello!', sidekiq_options: { queue: :low_priority }, sidekiq_schedule_options: { perform_in: 5 })
50+
51+
Interactor::Context message: 'hello!', sidekiq_options: { queue: :low_priority }, sidekiq_schedule_options: { perform_in: 5 }
52+
```
53+
54+
## Failure
55+
56+
If you pass invalid parameters to sidekiq, you will get an immediate return with the error message.
57+
```sh
58+
>> result = RegularAction.async_call(message: 'hello!', sidekiq_schedule_options: "error")
59+
#<Interactor::Context key="value", sidekiq_options="bad error message", error="undefined method `transform_keys' for \"bad error message\":String">
60+
>> result.failure?
61+
true
62+
>> Sidekiq::Queues.jobs_by_queue
63+
{}
64+
```
65+
66+
## Interactor::Async
67+
68+
Now you need an interactor to always assume asynchronous behavior using: **Interator::Async**.
69+
70+
#### Passing handle sidekiq exception
71+
72+
When executing the perform method in sidekiq there may be a problem, thinking about it we have already made it possible for you to handle this error.
73+
**If the context is failed during invocation of the interactor in background, the Interactor::Failure is raised**.
74+
75+
76+
```ruby
77+
class AsyncAction
78+
include Interactor::Async
79+
80+
def call
81+
{ context: context.variable }
82+
end
83+
84+
def self.sidekiq_options
85+
{ queue: :low_priority }
86+
end
87+
88+
def self.sidekiq_schedule_options
89+
{ perform_in: 5 }
90+
end
91+
92+
def self.handle_sidekiq_exception(error)
93+
# Integrate with Application Monitoring and Error Tracking Software
94+
end
95+
end
96+
```
97+
98+
## Compatibility
99+
100+
The same Ruby versions as Sidekiq are offically supported, but it should work
101+
with any 2.x syntax Ruby including JRuby and Rubinius.
102+
103+
## Running Specs
104+
105+
```
106+
bundle exec rspec
107+
```
108+
109+
## License
110+
111+
MIT

‎Rakefile‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# frozen_string_literal: true
2+
3+
require 'bundler/gem_tasks'

‎interactor-sidekiq.gemspec‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
lib = File.expand_path('lib', __dir__)
4+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5+
require 'interactor/sidekiq/version'
6+
7+
Gem::Specification.new do |spec|
8+
spec.name = 'interactor-sidekiq'
9+
spec.version = Interactor::Sidekiq::VERSION
10+
spec.authors = ['Gabriel Rocha']
11+
spec.email = ['[email protected]']
12+
spec.summary = 'Async Interactor using Sidekiq'
13+
spec.description = 'Async Interactor using Sidekiq'
14+
spec.homepage = 'https://github.com/gabrielras/interactor-sidekiq'
15+
spec.license = 'MIT'
16+
17+
spec.files = `git ls-files -z`.split("\x0")
18+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20+
spec.require_paths = ['lib']
21+
22+
spec.add_dependency 'interactor', '~> 3.0'
23+
spec.add_dependency 'sidekiq', '>=4.1'
24+
end

‎lib/interactor/sidekiq.rb‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# frozen_string_literal: true
2+
3+
require 'interactor'
4+
require 'sidekiq'
5+
6+
module Interactor
7+
# Internal: Install Interactor's behavior in the given class.
8+
def self.included(base)
9+
base.class_eval do
10+
extend ClassMethods
11+
extend SidekiqWorker
12+
include Hooks
13+
14+
# Public: Gets the Interactor::Context of the Interactor instance.
15+
attr_reader :context
16+
end
17+
end
18+
19+
# based on Sidekiq 4.x #delay method, which is not enabled by default in Sidekiq 5.x
20+
# https://github.com/mperham/sidekiq/blob/4.x/lib/sidekiq/extensions/generic_proxy.rb
21+
# https://github.com/mperham/sidekiq/blob/4.x/lib/sidekiq/extensions/class_methods.rb
22+
23+
module SidekiqWorker
24+
class Worker
25+
include ::Sidekiq::Worker
26+
27+
def perform(context)
28+
interactor_class(context).sync_call(context.except(:interactor_class))
29+
rescue Exception => e
30+
if interactor_class(context).respond_to?(:handle_sidekiq_exception)
31+
interactor_class(context).handle_sidekiq_exception(e)
32+
else
33+
raise e
34+
end
35+
end
36+
37+
private
38+
39+
def interactor_class(context)
40+
Module.const_get context[:interactor_class]
41+
end
42+
end
43+
44+
def sync_call(context = {})
45+
new(context).tap(&:run!).context
46+
end
47+
48+
def async_call(context = {})
49+
options = handle_sidekiq_options(context)
50+
schedule_options = delay_sidekiq_schedule_options(context)
51+
52+
Worker.set(options).perform_in(schedule_options.fetch(:delay, 0), handle_context_for_sidekiq(context))
53+
new(context.to_h).context
54+
rescue Exception => e
55+
begin
56+
new(context.to_h).context.fail!(error: e.message)
57+
rescue Failure => e
58+
e.context
59+
end
60+
end
61+
62+
private
63+
64+
def handle_context_for_sidekiq(context)
65+
context.to_h.merge(interactor_class: to_s).to_json
66+
end
67+
68+
def handle_sidekiq_options(context)
69+
if context[:sidekiq_options].nil?
70+
respond_to?(:sidekiq_options) ? sidekiq_options : { queue: :default }
71+
else
72+
context[:sidekiq_options]
73+
end
74+
end
75+
76+
def delay_sidekiq_schedule_options(context)
77+
options = handle_sidekiq_schedule_options(context)
78+
return {} unless options.key?(:perform_in) || options.key?(:perform_at)
79+
80+
{ delay: options[:perform_in] || options[:perform_at] }
81+
end
82+
83+
def handle_sidekiq_schedule_options(context)
84+
if context[:sidekiq_schedule_options].nil?
85+
respond_to?(:sidekiq_schedule_options) ? sidekiq_schedule_options : { delay: 0 }
86+
else
87+
context[:sidekiq_schedule_options]
88+
end
89+
end
90+
end
91+
92+
module Async
93+
def self.included(base)
94+
base.class_eval do
95+
include Interactor
96+
97+
extend ClassMethods
98+
end
99+
end
100+
101+
module ClassMethods
102+
def call(context = {})
103+
default_async_call(context)
104+
end
105+
106+
def call!(context = {})
107+
default_async_call(context)
108+
end
109+
110+
private
111+
112+
def default_async_call(context)
113+
async_call(context)
114+
end
115+
end
116+
end
117+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module Interactor
4+
module Sidekiq
5+
VERSION = '1.0.0'
6+
end
7+
end

0 commit comments

Comments
 (0)