rekiq 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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