multi-background-job 0.1.0

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.
@@ -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