rekiq 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bc545471fb04e687f4cf1cc9e9289db9060b579c
4
+ data.tar.gz: 74cc308381099f8711c6a27b16f3875c2d29168f
5
+ SHA512:
6
+ metadata.gz: fae12ce36fd971421f930c9195434b186c968cc8a82cec9c546a332f9551500635ca30f0d83d35426d25ea0b844596a89ef06e48400bef1d5d988cc01b77ab18
7
+ data.tar.gz: 730dbbba9558c1a9d80e30f2f8707ad102de3faf272e404c9ff65ea155fafd803ae01d99548725645176d1011087ade597e7032d0351913432548bf30cc9c538
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
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
+ example/tmp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+ --format documentation
3
+ --format html -o "tmp/rspec.html"
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ rekiq
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.1.1
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.1
4
+ - 2.0.0
5
+ - 1.9.3
6
+ script: CODECLIMATE_REPO_TOKEN=13221c16cb92c5dc8312b2f2f9f4960271e67845ba5e2852dbba3d786ccf2c81 bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rekiq.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 junhanamaki
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.
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # Rekiq
2
+
3
+ [![Build Status](https://travis-ci.org/junhanamaki/rekiq.svg?branch=master)](https://travis-ci.org/junhanamaki/rekiq)
4
+ [![Code Climate](https://codeclimate.com/github/junhanamaki/rekiq.png)](https://codeclimate.com/github/junhanamaki/rekiq)
5
+ [![Test Coverage](https://codeclimate.com/github/junhanamaki/rekiq/coverage.png)](https://codeclimate.com/github/junhanamaki/rekiq)
6
+ [![Dependency Status](https://gemnasium.com/junhanamaki/rekiq.svg)](https://gemnasium.com/junhanamaki/rekiq)
7
+
8
+ **Rekiq is a recurring worker extension for
9
+ [Sidekiq](https://github.com/mperham/sidekiq).**
10
+
11
+ Rekiq extends Sidekiq and adds functionality to schedule recurring workers.
12
+
13
+ Sidekiq is an amazing gem that allows us delegate time consuming work to a
14
+ worker, or even to schedule a time for the worker to start. Now wouldn't it be
15
+ nice if it also allowed us to schedule a worker to do work recurringly? That's
16
+ what rekiq purposes to do.
17
+
18
+ In pratical means, rekiq allows you to schedule a worker to repeat the same
19
+ work friday at 23:00, for example.
20
+
21
+ ## Requirements
22
+
23
+ Tested with:
24
+
25
+ * Ruby version 2.1.1, 2.0.0 and 1.9.3
26
+ * Sidekiq 3.2.1
27
+ * ice_cube 0.12.1
28
+
29
+ ## Installation
30
+
31
+ Add this line to your application's Gemfile:
32
+
33
+ gem 'rekiq', git: 'https://github.com/junhanamaki/rekiq'
34
+
35
+ And then execute:
36
+
37
+ $ bundle
38
+
39
+ Or compile source by hand, since for now it's not published.
40
+
41
+ ## Usage
42
+
43
+ Require rekiq after sidekiq:
44
+
45
+ require 'sidekiq'
46
+ require 'rekiq'
47
+
48
+ We need a 'schedule' object (responsible for returning the time at which the
49
+ worker should start) which must respond to method next_occurrence and
50
+ receives one argument of type Time more at [schedule](). For our example we'll use gem
51
+ [ice_cube](https://github.com/seejohnrun/ice_cube) (don't forget to require it):
52
+
53
+ # define worker as normal
54
+ class ExampleWorker
55
+ include Sidekiq::Worker
56
+
57
+ def perform(arg1, arg2)
58
+ # Do some work
59
+ end
60
+ end
61
+
62
+ # create schedule for worker to repeat every friday at 2am
63
+ schedule = IceCube::Schedule.new do |s|
64
+ s.rrule IceCube::Rule.daily.day(:friday).hour_of_day(2)
65
+ end
66
+
67
+ # now just start your worker
68
+ ExampleWorker.perform_recurringly(schedule, 'argument_1', 'argument_2')
69
+
70
+ You can use your own schedule object, configure worker to reschedule before or
71
+ after work is done, set margin, and much more! So please check
72
+ [wiki](https://github.com/junhanamaki/rekiq/wiki) for more details.
73
+
74
+ ## Contributing
75
+
76
+ 1. Fork it ( https://github.com/[my-github-username]/rekiq/fork )
77
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
78
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
79
+ 4. Push to the branch (`git push origin my-new-feature`)
80
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,6 @@
1
+ :verbose: false
2
+ :pidfile: ./tmp/sidekiq.pid
3
+ # :logfile: ./tmp/log/sidekiq.log
4
+ :concurrency: 5
5
+ :queues:
6
+ - [rekiq_test_worker, 10]
@@ -0,0 +1,30 @@
1
+ # install gem and run with:
2
+ # bundle exec sidekiq -C example/sidekiq.yml -r ./example/test_app.rb
3
+
4
+ require 'ice_cube'
5
+ require 'sidekiq'
6
+ require 'rekiq'
7
+
8
+ # define sidekiq worker as you normally would
9
+ class TestWorker1
10
+ include Sidekiq::Worker
11
+
12
+ sidekiq_options queue: "rekiq_test_worker", retry: false
13
+
14
+ def perform(arg1, arg2)
15
+ puts "\n\nhello from TestWorker1, arg1 is #{arg1}, arg2 is #{arg2}" \
16
+ "scheduled work time was #{scheduled_work_time}\n\n"
17
+ end
18
+ end
19
+
20
+ # create ice cube schedule
21
+ schedule = IceCube::Schedule.new(Time.now) do |s|
22
+ s.rrule IceCube::Rule.minutely
23
+ end
24
+
25
+ # invoke method
26
+ TestWorker1.perform_recurringly(
27
+ schedule,
28
+ ['Rekiq', 'ola', '!!!'],
29
+ { 'complex' => { 'hash' => 'woot!' } }
30
+ )
@@ -0,0 +1,33 @@
1
+ module Rekiq
2
+ class Configuration
3
+ attr_accessor :reschedule_post_work, :schedule_expired,
4
+ :expiration_margin
5
+
6
+ def initialize
7
+ # if work is rescheduled after or before the worker completes
8
+ self.reschedule_post_work = false
9
+
10
+ # if expired works are to be scheduled
11
+ # an expired work is a work that has a time bellow current_time - margin
12
+ self.schedule_expired = false
13
+
14
+ # indicates the margin after which a work is considered expired
15
+ # default to 0
16
+ self.expiration_margin = 0
17
+ end
18
+ end
19
+
20
+ class << self
21
+ def configure
22
+ yield configuration
23
+ end
24
+
25
+ def configuration
26
+ @configuration ||= Configuration.new
27
+ end
28
+
29
+ def reset_configuration
30
+ @configuration = Configuration.new
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ module Rekiq
2
+ class StandardError < ::StandardError; end
3
+ class SidekiqNotLoaded < StandardError; end
4
+ end
data/lib/rekiq/job.rb ADDED
@@ -0,0 +1,103 @@
1
+ require 'yaml'
2
+ require 'rekiq/schedule_format_validator'
3
+
4
+ module Rekiq
5
+ class Job
6
+ include ActiveModel::Validations
7
+ include ActiveModel::Validations::Callbacks
8
+
9
+ attr_accessor :schedule, :shift, :reschedule_post_work, :schedule_expired,
10
+ :expiration_margin
11
+
12
+ validates :schedule, 'rekiq::_schedule_format' => true
13
+ validates :shift, numericality: true
14
+ validates :reschedule_post_work, :schedule_expired,
15
+ inclusion: { in: [true, false], allow_nil: true }
16
+ validates :expiration_margin,
17
+ numericality: { greater_than_or_equal_to: 0, allow_nil: true }
18
+
19
+ def self.from_short_key_hash(hash)
20
+ hash['schedule'] = YAML::load(hash['sch'])
21
+ hash['shift'] = hash['sft']
22
+ hash['reschedule_post_work'] = hash['rpw']
23
+ hash['schedule_expired'] = hash['se']
24
+ hash['expiration_margin'] = hash['em']
25
+
26
+ new(hash)
27
+ end
28
+
29
+ def initialize(attributes = {})
30
+ self.schedule = attributes['schedule']
31
+ self.shift = attributes['shift'] || 0
32
+ self.reschedule_post_work = attributes['reschedule_post_work']
33
+ self.schedule_expired = attributes['schedule_expired']
34
+ self.expiration_margin = attributes['expiration_margin']
35
+ end
36
+
37
+ def to_short_key_hash
38
+ {
39
+ 'sch' => YAML::dump(schedule),
40
+ 'sft' => shift,
41
+ 'rpw' => reschedule_post_work,
42
+ 'se' => schedule_expired,
43
+ 'em' => expiration_margin
44
+ }
45
+ end
46
+
47
+ def next_work_time(from = Time.now)
48
+ shifted_from = shift > 0 ? from - shift : from
49
+
50
+ search_next_work_time(shifted_from)
51
+ end
52
+
53
+ def next_work_time_from_work_time(from)
54
+ shifted_from = from - shift
55
+
56
+ search_next_work_time(shifted_from)
57
+ end
58
+
59
+ def reschedule_post_work?
60
+ unless reschedule_post_work.nil?
61
+ reschedule_post_work
62
+ else
63
+ Rekiq.configuration.reschedule_post_work
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def search_next_work_time(from)
70
+ if schedule_expired?
71
+ from = schedule.next_occurrence(from)
72
+ work_time = from.nil? ? nil : from + shift
73
+ else
74
+ begin
75
+ from = schedule.next_occurrence(from)
76
+ work_time = from.nil? ? nil : from + shift
77
+ end until work_time.nil? || work_time > expiration_time
78
+ end
79
+
80
+ work_time
81
+ end
82
+
83
+ def schedule_expired?
84
+ unless schedule_expired.nil?
85
+ schedule_expired
86
+ else
87
+ Rekiq.configuration.schedule_expired
88
+ end
89
+ end
90
+
91
+ def expiration_margin_val
92
+ unless expiration_margin.nil?
93
+ expiration_margin
94
+ else
95
+ Rekiq.configuration.expiration_margin
96
+ end
97
+ end
98
+
99
+ def expiration_time
100
+ Time.now - expiration_margin_val
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,14 @@
1
+ module Rekiq
2
+ module Middleware
3
+ class Utils
4
+ def call(worker, msg, queue)
5
+ if worker.respond_to?(:scheduled_work_time) and
6
+ msg.key?('rq:at')
7
+ worker.scheduled_work_time = Time.at(msg['rq:at']).utc
8
+ end
9
+
10
+ yield
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,58 @@
1
+ require 'sidekiq'
2
+ require 'sidekiq/util'
3
+ require 'rekiq/configuration'
4
+ require 'rekiq/job'
5
+ require 'rekiq/scheduler'
6
+
7
+ module Rekiq
8
+ module Middleware
9
+ class WorkOverseer
10
+ include ::Sidekiq::Util
11
+
12
+ attr_accessor :worker_name, :queue, :args, :job, :addon,
13
+ :scheduled_work_time
14
+
15
+ def call(worker, msg, queue)
16
+ return yield unless msg['rq:job']
17
+
18
+ self.worker_name = worker.class.name
19
+ self.queue = queue
20
+ self.args = msg['args']
21
+ self.job = Job.from_short_key_hash(msg['rq:job'])
22
+ self.addon = msg['rq:addon']
23
+
24
+ if msg['retry_count'].nil?
25
+ self.scheduled_work_time = Time.at(msg['rq:at'].to_f)
26
+ reschedule_post_work = job.reschedule_post_work?
27
+
28
+ if reschedule_post_work
29
+ begin
30
+ yield
31
+ ensure
32
+ reschedule
33
+ end
34
+ else
35
+ reschedule
36
+ yield
37
+ end
38
+ else
39
+ yield
40
+ end
41
+ end
42
+
43
+ def reschedule
44
+ jid, work_time =
45
+ Rekiq::Scheduler
46
+ .new(worker_name, queue, args, job, addon)
47
+ .schedule_from_work_time(scheduled_work_time)
48
+
49
+ unless jid.nil?
50
+ logger.info "recurring work for #{worker_name} scheduled for " \
51
+ "#{work_time} with jid #{jid}"
52
+ else
53
+ logger.info 'recurrence terminated, job terminated'
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,16 @@
1
+ require 'active_model'
2
+
3
+ module Rekiq
4
+ class ScheduleFormatValidator < ActiveModel::EachValidator
5
+ def validate_each(record, attribute, value)
6
+ unless value.respond_to?(:next_occurrence) and
7
+ value.method(:next_occurrence).arity.abs > 0
8
+ record.errors[attribute] <<
9
+ "invalid value for #{attribute}, value must be an object that " \
10
+ 'responds to next_occurrence, and that receives at least one ' \
11
+ 'argument of type Time, representing Time from which to calculate ' \
12
+ 'next occurrence time'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ module Rekiq
2
+ class Scheduler
3
+ attr_accessor :worker_name, :queue, :args, :job, :addon, :work_time
4
+
5
+ def initialize(worker_name, queue, args, job, addon)
6
+ self.worker_name = worker_name
7
+ self.queue = queue
8
+ self.args = args
9
+ self.job = job
10
+ self.addon = addon
11
+ end
12
+
13
+ def schedule(from = Time.now)
14
+ self.work_time = job.next_work_time(from)
15
+
16
+ work_time.nil? ? nil : [schedule_work, work_time]
17
+ end
18
+
19
+ def schedule_from_work_time(from)
20
+ self.work_time = job.next_work_time_from_work_time(from)
21
+
22
+ work_time.nil? ? nil : [schedule_work, work_time]
23
+ end
24
+
25
+ private
26
+
27
+ def schedule_work
28
+ client_args = {
29
+ 'at' => work_time.to_f,
30
+ 'queue' => queue,
31
+ 'class' => worker_name,
32
+ 'args' => args,
33
+ 'rq:job' => job.to_short_key_hash,
34
+ 'rq:at' => work_time.to_f
35
+ }.tap do |hash|
36
+ hash['rq:addon'] = addon unless addon.nil?
37
+ end
38
+
39
+ Sidekiq::Client.push(client_args)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module Rekiq
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,62 @@
1
+ require 'rekiq/exceptions'
2
+ require 'rekiq/job'
3
+ require 'rekiq/scheduler'
4
+
5
+ module Rekiq
6
+ module Worker
7
+ class Configuration
8
+ attr_accessor :shift, :reschedule_post_work, :schedule_expired,
9
+ :expiration_margin, :addon
10
+
11
+ def append_to_msg(addon)
12
+ self.addon = addon
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def perform_recurringly(schedule, *args)
18
+ config = Configuration.new
19
+ yield config if block_given?
20
+
21
+ job =
22
+ Rekiq::Job
23
+ .new 'schedule' => schedule,
24
+ 'shift' => config.shift,
25
+ 'reschedule_post_work' => config.reschedule_post_work,
26
+ 'schedule_expired' => config.schedule_expired,
27
+ 'expiration_margin' => config.expiration_margin
28
+
29
+ queue = get_sidekiq_options['queue']
30
+
31
+ jid, work_time =
32
+ Rekiq::Scheduler
33
+ .new(name, queue, args, job, config.addon)
34
+ .schedule
35
+
36
+ return if jid.nil?
37
+
38
+ ::Sidekiq.logger.info "recurring work for #{name} scheduled for " \
39
+ "#{work_time} with jid #{jid}"
40
+
41
+ jid
42
+ rescue StandardError => e
43
+ raise Rekiq::StandardError,
44
+ 'unable to schedule worker',
45
+ e.backtrace
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ module Sidekiq
52
+ module Worker
53
+ attr_accessor :scheduled_work_time
54
+
55
+ original_included_method = method(:included)
56
+
57
+ define_singleton_method :included do |base|
58
+ original_included_method.call(base)
59
+ base.extend(Rekiq::Worker::ClassMethods)
60
+ end
61
+ end
62
+ end
data/lib/rekiq.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'rekiq/version'
2
+ require 'rekiq/exceptions'
3
+
4
+ unless defined?(Sidekiq)
5
+ raise Rekiq::SidekiqNotLoaded,
6
+ 'sidekiq must be required before requiring rekiq'
7
+ end
8
+
9
+ require 'rekiq/middleware/work_overseer'
10
+ require 'rekiq/middleware/utils'
11
+ require 'rekiq/worker'
12
+
13
+ module Rekiq
14
+
15
+ end
16
+
17
+ Sidekiq.configure_server do |config|
18
+ config.server_middleware do |chain|
19
+ chain.add Rekiq::Middleware::Utils
20
+ chain.add Rekiq::Middleware::WorkOverseer
21
+ end
22
+ end
data/rekiq.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rekiq/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rekiq"
8
+ spec.version = Rekiq::VERSION
9
+ spec.authors = ["junhanamaki"]
10
+ spec.email = ["jun.hanamaki@gmail.com"]
11
+ spec.summary = %q{recurring worker extension for sidekiq}
12
+ spec.description = %q{}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.6'
22
+ spec.add_development_dependency 'rspec', '~> 3.0'
23
+ spec.add_development_dependency 'simplecov', '~> 0.9'
24
+ spec.add_development_dependency 'codeclimate-test-reporter', '~> 0.3'
25
+ spec.add_development_dependency 'factory_girl', '~> 4.4'
26
+ spec.add_development_dependency 'jazz_hands', '~> 0.5'
27
+ spec.add_development_dependency 'ice_cube', '~> 0.12'
28
+ spec.add_development_dependency 'sidekiq', '~> 3.2'
29
+
30
+ spec.add_runtime_dependency 'activemodel', '~> 4.1'
31
+ end
@@ -0,0 +1,15 @@
1
+ require 'ice_cube'
2
+
3
+ FactoryGirl.define do
4
+ factory :job, class: Rekiq::Job do
5
+ shift 0
6
+ schedule { IceCube::Schedule.new(Time.now + 3600) }
7
+
8
+ trait :randomized_attributes do
9
+ shift { [*0..100].sample }
10
+ reschedule_post_work { [nil, false, true].sample }
11
+ schedule_expired { [nil, false, true].sample }
12
+ expiration_margin { [*0..100].sample }
13
+ end
14
+ end
15
+ end