multi-background-job 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 102602d0d8739d98e0baa6a10f9581bb91c4e486ce03ff1d0b3a1448c21756c3
4
+ data.tar.gz: 8079bfc60fc76222ae8ec886dd8dd076ee17d6903a218afe79c1b6d9cc9c95ac
5
+ SHA512:
6
+ metadata.gz: 5a62442c88fa86368af365c8df9299390ff059994f91bdacfeee9bf8492a495e77951ecb305566d838b50f14d385ed94ee24979d31fb08e2a851ad56578b9f3c
7
+ data.tar.gz: fb87cf4bcf49808a43766e7c6340d9f24d9eeb4ba1ca15b600a922a80ba89fbab8e62dbc4c48052bb12aed137e7557de40cec5888aabf1480cebc6da23d595ef
@@ -0,0 +1,35 @@
1
+ name: Specs
2
+ on:
3
+ push:
4
+ branches: [ master ]
5
+ pull_request:
6
+ branches: [ master ]
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ services:
12
+ redis:
13
+ image: redis
14
+ options: >-
15
+ --health-cmd "redis-cli ping"
16
+ --health-interval 10s
17
+ --health-timeout 5s
18
+ --health-retries 5
19
+ ports:
20
+ - 6379:6379
21
+ steps:
22
+ - uses: actions/checkout@v2
23
+ - name: Set up Ruby
24
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
25
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
26
+ # uses: ruby/setup-ruby@v1
27
+ uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
28
+ with:
29
+ ruby-version: 2.6
30
+ - name: Install dependencies
31
+ run: bundle install
32
+ - name: Run tests
33
+ run: bundle exec rspec
34
+ env:
35
+ REDIS_URL: redis://localhost:6379
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.env
3
+ /.rspec_status
4
+ /.yardoc
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1 @@
1
+ ruby 2.6.3
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in multi-background-job.gemspec
4
+ gemspec
5
+
6
+ gem 'rake', '~> 12.0'
7
+ gem 'pry'
8
+ gem 'awesome_print'
9
+ gem 'dotenv'
10
+ gem 'rspec'
11
+ gem 'timecop'
12
+ gem 'faktory_worker_ruby', require: false
@@ -0,0 +1,55 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ multi-background-job (0.1.0)
5
+ connection_pool
6
+ multi_json
7
+ redis
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ awesome_print (1.8.0)
13
+ coderay (1.1.3)
14
+ connection_pool (2.2.3)
15
+ diff-lcs (1.4.4)
16
+ dotenv (2.7.6)
17
+ faktory_worker_ruby (1.0.0)
18
+ connection_pool (~> 2.2, >= 2.2.2)
19
+ method_source (1.0.0)
20
+ multi_json (1.15.0)
21
+ pry (0.13.1)
22
+ coderay (~> 1.1)
23
+ method_source (~> 1.0)
24
+ rake (12.3.3)
25
+ redis (4.2.2)
26
+ rspec (3.9.0)
27
+ rspec-core (~> 3.9.0)
28
+ rspec-expectations (~> 3.9.0)
29
+ rspec-mocks (~> 3.9.0)
30
+ rspec-core (3.9.3)
31
+ rspec-support (~> 3.9.3)
32
+ rspec-expectations (3.9.2)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.9.0)
35
+ rspec-mocks (3.9.1)
36
+ diff-lcs (>= 1.2.0, < 2.0)
37
+ rspec-support (~> 3.9.0)
38
+ rspec-support (3.9.3)
39
+ timecop (0.9.1)
40
+
41
+ PLATFORMS
42
+ ruby
43
+
44
+ DEPENDENCIES
45
+ awesome_print
46
+ dotenv
47
+ faktory_worker_ruby
48
+ multi-background-job!
49
+ pry
50
+ rake (~> 12.0)
51
+ rspec
52
+ timecop
53
+
54
+ BUNDLED WITH
55
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Marcos G. Zimmermann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,137 @@
1
+ # MultiBackgroundJob
2
+
3
+ This library provices an centralized interface to push jobs to a variety of queuing backends. Thus allowing to send jobs to multiple external services. If you are running a [Ruby on Rails](https://github.com/rails/rails) application consider using [Active Jobs](https://github.com/rails/rails/tree/master/activejob). ActiveJobs integrates with a wider range of services and builtin support.
4
+
5
+ Supported Services:
6
+ * Faktory (Faktory::Client is used as depency to push jobs)
7
+ * Sidekiq (Sidekiq gem is not a depenency. We are using redis connection to push jobs)
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'multi-background-job'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle install
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install multi-background-job
24
+
25
+ ## Usage
26
+
27
+
28
+ This gem should work with default configurations if you have `REDIS_URL` and/or `FAKTORY_URL` environment variables correctly defined. But you can use the `MultiBackgroundJob#configure` method to customize default settings.
29
+
30
+ ```ruby
31
+ MultiBackgroundJob.configure do |config|
32
+ config.config_path = 'config/background_jobs.yml' # You can use an YAML file. (Default to nil)
33
+ config.redis_pool_size = 10 # Connection Pool size for redis. (Default to 5)
34
+ config.redis_pool_timeout = 10 # Connection Pool timeouf for redis. (Default to 5)
35
+ config.redis_namespace = 'app-name' # The prefix of redis storage keys. (Default to multi-bg)
36
+ config.redis_config = { path: "/tmp/redis.sock" } # List of configurations to be passed along to the Redis.new. (Default to {})
37
+ config.workers = { # List of workers and its configurations. (Default to {})
38
+ 'Accounts::ConfirmationEmailWorker' => { retry: 5, queue: 'mailing' },
39
+ }
40
+ config.strict = false # Only allow to push jobs to known workers. See `config.workers`. (Default to true)
41
+ end
42
+ ```
43
+
44
+ ### Client Setup
45
+
46
+ You can use the DSL to start building worker and push to background job services.
47
+
48
+ ```ruby
49
+ # Enqueue the 'Accounts::ConfirmationEmailWorker' job with 'User', 1 arguments
50
+ # to the sidekiq "other_mailing" queue
51
+ MultiBackgroundJob['Accounts::ConfirmationEmailWorker', queue: 'other_mailing' ]
52
+ .with_args('User', 1)
53
+ .push(to: :sidekiq)
54
+
55
+ # Schedule the 'Accounts::ConfirmationEmailWorker' job with 'User', 1 arguments
56
+ # to the sidekiq "other_mailing" queue to be executed in one hour.
57
+ MultiBackgroundJob['Accounts::ConfirmationEmailWorker', queue: 'other_mailing' ]
58
+ .with_args('User', 1)
59
+ .in(1.hour)
60
+ .push(to: :sidekiq)
61
+
62
+ # Enqueue the 'Accounts::ConfirmationEmailWorker' job with 'User', 2 arguments
63
+ # to the faktory "mailing" queue(Using :queu from global config.workers definition)
64
+ MultiBackgroundJob['Accounts::ConfirmationEmailWorker']
65
+ .with_args('User', 2)
66
+ .push(to: :faktory)
67
+ ```
68
+
69
+ MultiBackgroundJob is not required as a dependency of backend servers if you are only use it to push jobs(** Except when you are using middleware like **UniqueJobs Middleware** of next section)
70
+
71
+ ### Server Setup
72
+
73
+ This is only a necessary step in the case of using the **UniqueJobs Middleware** of next section.
74
+
75
+ Example of sidekiq worker:
76
+
77
+ ```diff
78
+ class Accounts::ConfirmationEmailWorker
79
+ + extend MultiBackgroundJob.for(:sidekiq, queue: :mailing)
80
+ - include Sidekiq::Worker
81
+ - sidekiq_options queue: :mailing
82
+
83
+ def perform(resource_type, resource_id); end;
84
+ end
85
+ ```
86
+
87
+ Example of faktory worker:
88
+
89
+ ```diff
90
+ class Accounts::ConfirmationEmailWorker
91
+ + extend MultiBackgroundJob.for(:sidekiq, queue: :mailing)
92
+ - include Faktory::Job
93
+ - faktory_options queue: :mailing
94
+
95
+ def perform(resource_type, resource_id); end;
96
+ end
97
+ ```
98
+
99
+ Now when you call `Accounts::ConfirmationEmailWorker.perform_async` or `Accounts::ConfirmationEmailWorker.perform_in` it will use this gem to push jobs to the backend server.
100
+
101
+ Note that settings defined througth the worker class have greater weight then the ones defined from global `MultiBackgroundJob.config.workers`. And the `MultiBackgroundJob.config.workers` have greater weight then both `Sidekiq.default_worker_options` or `Faktory.default_job_options`.
102
+
103
+ ### Unique Jobs
104
+
105
+ This library provides one experimental technology to avoid enqueue duplicated jobs. Pro versions of sidekiq and faktory provides this functionality. But this project exposes a mechanism to make this control using `Redis`. It's not required by default. You can load this function by require and initialize the `UniqueJob` middleware according to the service(`:faktory` or `:sidekiq`).
106
+
107
+ ```ruby
108
+ require 'multi_background_job/middleware/unique_job'
109
+ MultiBackgroundJob::Middleware::UniqueJob::bootstrap(service: :sidekiq)
110
+ # Or
111
+ MultiBackgroundJob::Middleware::UniqueJob::bootstrap(service: :faktory)
112
+ ```
113
+
114
+ After that just define the `:uniq` settings by worker
115
+
116
+ ```ruby
117
+ MultiBackgroundJob['Mailing::SignUpWorker', uniq: { across: :queue, timeout: 120 }]
118
+ .with_args('User', 1)
119
+ .push(to: :sidekiq)
120
+ ```
121
+
122
+ You can globally disable/enable this function with the `MultiBackgroundJob.config.unique_job_active = <true|false>`
123
+
124
+ ## Development
125
+
126
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
127
+
128
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
129
+
130
+ ## Contributing
131
+
132
+ Bug reports and pull requests are welcome on GitHub at https://github.com/marcosgz/multi-background-job.
133
+
134
+
135
+ ## License
136
+
137
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'dotenv/load'
5
+ require 'pry'
6
+ require 'awesome_print'
7
+ require 'multi_background_job'
8
+
9
+ MultiBackgroundJob.configure do |config|
10
+ config.strict = false
11
+ end
12
+
13
+ Pry.start
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './multi_background_job'
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'time'
5
+ require 'securerandom'
6
+ require 'multi_json'
7
+
8
+ require_relative './multi_background_job/version'
9
+ require_relative 'multi_background_job/errors'
10
+ require_relative 'multi_background_job/config'
11
+ require_relative 'multi_background_job/worker'
12
+ require_relative 'multi_background_job/adapters/adapter'
13
+ require_relative 'multi_background_job/adapters/sidekiq'
14
+ require_relative 'multi_background_job/adapters/faktory'
15
+
16
+ # This is a central point of our background job queue system.
17
+ # We have more external services like API and Lme that queue jobs for pipeline processing.
18
+ # So that way all services can share the same codebase and avoid incompatibility issues
19
+ #
20
+ # Example:
21
+ #
22
+ # Standard job.
23
+ # MultiBackgroundJob['UserWorker', queue: 'default']
24
+ # .with_args(1)
25
+ # .push(to: :sidekiq)
26
+ #
27
+ # Schedule the time when a job will be executed.
28
+ # MultiBackgroundJob['UserWorker']
29
+ # .with_args(1)
30
+ # .at(timestamp)
31
+ # .push(to: :sidekiq)
32
+ # MultiBackgroundJob['UserWorker']
33
+ # .with_args(1)
34
+ # .in(10.minutes)
35
+ # .push(to: :sidekiq)
36
+ #
37
+ # Unique jobs.
38
+ # MultiBackgroundJob['UserWorker', uniq: { across: :queue, timeout: 1.minute, unlock_policy: :start }]
39
+ # .with_args(1)
40
+ # .push(to: :sidekiq)
41
+ module MultiBackgroundJob
42
+ SERVICES = {
43
+ sidekiq: Adapters::Sidekiq,
44
+ faktory: Adapters::Faktory,
45
+ }
46
+
47
+ # @param worker_class [String] The worker class name
48
+ # @param options [Hash] Options that will be passed along to the worker instance
49
+ # @return [MultiBackgroundJob::Worker] An instance of worker
50
+ def self.[](worker_class, **options)
51
+ Worker.new(worker_class, **config.worker_options(worker_class).merge(options))
52
+ end
53
+
54
+ def self.jid
55
+ SecureRandom.hex(12)
56
+ end
57
+
58
+ def self.for(service, **options)
59
+ require_relative "multi_background_job/workers/#{service}"
60
+ service = service.to_sym
61
+ worker_options = options.merge(service: service)
62
+ module_name = service.to_s.split(/_/i).collect!{ |w| w.capitalize }.join
63
+ mod = Workers.const_get(module_name)
64
+ mod.module_eval do
65
+ define_method(:bg_worker_options) do
66
+ worker_options
67
+ end
68
+ end
69
+ mod
70
+ end
71
+
72
+ def self.config
73
+ @config ||= Config.new
74
+ end
75
+
76
+ def self.configure(&block)
77
+ return unless block_given?
78
+
79
+ config.instance_eval(&block)
80
+ @redis_pool = nil
81
+ config
82
+ end
83
+
84
+ def self.redis_pool
85
+ @redis_pool ||= ConnectionPool.new(config.redis_pool) do
86
+ Redis.new(config.redis_config)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MultiBackgroundJob
4
+ module Adapters
5
+ class Adapter
6
+ # Push the worker job to the service
7
+ # @param _worker [MultiBackgroundJob::Worker] An instance of background worker
8
+ # @abstract Child classes should override this method
9
+ def self.push(_worker)
10
+ raise NotImplemented
11
+ end
12
+
13
+ # Coerces the raw payload into an instance of Worker
14
+ # @param payload [Object] the object that should be coerced to a Worker
15
+ # @options options [Hash] list of options that will be passed along to the Worker instance
16
+ # @return [MultiBackgroundJob::Worker] and instance of MultiBackgroundJob::Worker
17
+ # @abstract Child classes should override this method
18
+ def self.coerce_to_worker(payload, **options)
19
+ raise NotImplemented
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MultiBackgroundJob
4
+ module Adapters
5
+ # This is a Faktory adapter that converts MultiBackgroundJob::Worker object into a faktory readable format
6
+ # and then push the jobs into the service.
7
+ class Faktory < Adapter
8
+ attr_reader :worker, :queue
9
+
10
+ def initialize(worker)
11
+ @worker = worker
12
+ @queue = worker.options.fetch(:queue, 'default')
13
+
14
+ @payload = worker.payload.merge(
15
+ 'jobtype' => worker.worker_class,
16
+ 'queue' => @queue,
17
+ 'retry' => parse_retry(worker.options[:retry]),
18
+ )
19
+ @payload['created_at'] ||= Time.now.to_f
20
+ end
21
+
22
+ # Coerces the raw payload into an instance of Worker
23
+ # @param payload [Hash] The job as json from redis
24
+ # @options options [Hash] list of options that will be passed along to the Worker instance
25
+ # @return [MultiBackgroundJob::Worker] and instance of MultiBackgroundJob::Worker
26
+ def self.coerce_to_worker(payload, **options)
27
+ raise(Error, 'invalid payload') unless payload.is_a?(Hash)
28
+ raise(Error, 'invalid payload') unless payload['jobtype'].is_a?(String)
29
+
30
+ options[:retry] ||= payload['retry'] if payload.key?('retry')
31
+ options[:queue] ||= payload['queue'] if payload.key?('queue')
32
+
33
+ MultiBackgroundJob[payload['jobtype'], **options].tap do |worker|
34
+ worker.with_args(*Array(payload['args'])) if payload.key?('args')
35
+ worker.with_job_jid(payload['jid']) if payload.key?('jid')
36
+ worker.created_at(payload['created_at']) if payload.key?('created_at')
37
+ worker.enqueued_at(payload['enqueued_at']) if payload.key?('enqueued_at')
38
+ worker.at(payload['at']) if payload.key?('at')
39
+ worker.unique(payload['uniq']) if payload.key?('uniq')
40
+ end
41
+ end
42
+
43
+ # Initializes adapter and push job into the faktory service
44
+ #
45
+ # @param worker [MultiBackgroundJob::Worker] An instance of MultiBackgroundJob::Worker
46
+ # @return [Hash] Job payload
47
+ # @see push method for more details
48
+ def self.push(worker)
49
+ new(worker).push
50
+ end
51
+
52
+ # Push job to Faktory
53
+ # * If job has the 'at' key. Then schedule it
54
+ # * Otherwise enqueue for immediate execution
55
+ #
56
+ # @raise [MultiBackgroundJob::Error] raise and error when faktory dependency is not loaded
57
+ # @return [Hash] Payload that was sent to server
58
+ def push
59
+ unless Object.const_defined?(:Faktory)
60
+ raise MultiBackgroundJob::Error, <<~ERR
61
+ Faktory client for ruby is not loaded. You must install and require https://github.com/contribsys/faktory_worker_ruby.
62
+ ERR
63
+ end
64
+ @payload['enqueued_at'] ||= Time.now.to_f
65
+ {'created_at' => false, 'enqueued_at' => false, 'at' => true}.each do |field, past_remove|
66
+ # Optimization to enqueue something now that is scheduled to go out now or in the past
67
+ if (time = @payload.delete(field)) &&
68
+ (!past_remove || (past_remove && time > Time.now.to_f))
69
+ @payload[field] = parse_time(time)
70
+ end
71
+ end
72
+
73
+ pool = Thread.current[:faktory_via_pool] || ::Faktory.server_pool
74
+ ::Faktory.client_middleware.invoke(@payload, pool) do
75
+ pool.with do |c|
76
+ c.push(@payload)
77
+ end
78
+ end
79
+ @payload
80
+ end
81
+
82
+ protected
83
+
84
+ # Convert worker retry value acording to the Go struct datatype.
85
+ #
86
+ # * 25 is the default.
87
+ # * 0 means the job is completely ephemeral. No matter if it fails or succeeds, it will be discarded.
88
+ # * -1 means the job will go straight to the Dead set if it fails, no retries.
89
+ def parse_retry(value)
90
+ case value
91
+ when Numeric then value.to_i
92
+ when false then -1
93
+ else
94
+ 25
95
+ end
96
+ end
97
+
98
+ def parse_time(value)
99
+ case value
100
+ when Numeric then Time.at(value).to_datetime.rfc3339(9)
101
+ when Time then value.to_datetime.rfc3339(9)
102
+ when DateTime then value.rfc3339(9)
103
+ end
104
+ end
105
+
106
+ def to_json(value)
107
+ MultiJson.dump(value, mode: :compat)
108
+ end
109
+ end
110
+ end
111
+ end