sidecloq 0.1.0

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: 6d0f6f9c7d36841096162c3d1ec7775220e58438
4
+ data.tar.gz: 4d11bcc58feabca4f13c0cf05f6a8d34257e5d29
5
+ SHA512:
6
+ metadata.gz: 328c80251970cb9899a5106394a5a0c206ce5317fb88be4d4f01092e6521d7f8fcd487535403d6bd917636739be852b0a0051f51ea25ab739dfbebef040ce894
7
+ data.tar.gz: 3a956934c7415080ca8a68664471cab8e86707eb914b3ae4864f58b2ada039c47e7559c76054982f0e4780f58d87172079e8a4426be107f2d41e579e9c571bbd
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.ruby-version
11
+ /.ruby-gemset
data/.travis.yml ADDED
@@ -0,0 +1,18 @@
1
+ language: ruby
2
+ sudo: false
3
+ cache: bundler
4
+ services:
5
+ - redis-server
6
+ rvm:
7
+ - 2.0.0
8
+ - 2.1.8
9
+ - 2.2.4
10
+ - ruby-head
11
+ - jruby-9.0.4.0
12
+ - jruby-head
13
+ - rbx-2.6
14
+ - rbx-2.7
15
+ matrix:
16
+ allow_failures:
17
+ - rvm: ruby-head
18
+ - rvm: jruby-head
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ platforms :mri do
6
+ gem 'pry'
7
+ gem 'minitest-utils'
8
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Matt Robinson
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.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Sidecloq
2
+
3
+ [![Build Status](https://travis-ci.org/mattyr/sidecloq.svg)](https://travis-ci.org/mattyr/sidecloq)
4
+
5
+ Another recurring job extension for Sidekiq.
6
+
7
+ ## Why
8
+
9
+ TODO: design principles, differences, inspiration (sidetiq,
10
+ sidekiq-scheduler, resque-scheduler)
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'sidecloq'
18
+ ```
19
+
20
+ Configure Sidecloq alongside your Sidekiq config. If using Rails, and
21
+ your schedule is located at config/sidecloq.yml, you don't have to do
22
+ anything (ie, omit this whole configuration block).
23
+
24
+ ```ruby
25
+ Sidcloq.configure do |config|
26
+ config[:schedule_file] =
27
+ File.join(Rails.root, "config/myschedule.yml")
28
+ end
29
+ ```
30
+
31
+ TODO: configuration options
32
+
33
+ TODO: schedule file format (like resque-scheduler)
34
+
35
+ ## Web Extension
36
+
37
+ Add Sidecloq::Web after Sidekiq::Web:
38
+
39
+ ```ruby
40
+ require 'sidekiq/web'
41
+ require 'sidecloq/web'
42
+ ```
43
+
44
+ TODO: screenshot/directions
45
+
46
+ ## Contributing
47
+
48
+ Bug reports and pull requests are welcome.
49
+
50
+ ## License
51
+
52
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
53
+
54
+ TODO: project links
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/test_*.rb']
8
+ end
9
+
10
+ task :default => :test
11
+
12
+ desc "Run the Sidekiq web interface (w/Sidecloq)"
13
+ task :web do
14
+ require 'sidekiq'
15
+ require 'sidecloq'
16
+
17
+ Sidekiq.configure_client do |config|
18
+ config.redis = { url: 'redis://localhost:6379/0', size: 1, namespace: 'sidecloq' }
19
+ end
20
+
21
+ require 'sidekiq/web'
22
+ require 'sidecloq/web'
23
+
24
+ Rack::Server.start(
25
+ app: Sidekiq::Web
26
+ )
27
+ end
@@ -0,0 +1,57 @@
1
+ module Sidecloq
2
+ # Locker obtains or waits for an exclusive lock on a key in redis
3
+ class Locker
4
+ include Utils
5
+
6
+ DEFAULT_LOCK_KEY = "sidecloq_leader_lock"
7
+
8
+ def initialize(options={})
9
+ # we keep a connection from the pool by default
10
+ @redis = options[:redis] || Sidekiq.redis_pool.checkout
11
+ @key = options[:lock_key] || DEFAULT_LOCK_KEY
12
+ @ttl = options[:ttl] || 60
13
+ @check_interval = options[:check_interval] || 15
14
+ @lock_manager = Redlock::Client.new([@redis])
15
+ @obtained_lock = Concurrent::Event.new
16
+ end
17
+
18
+ # blocks until lock is obtained, then yields
19
+ def with_lock
20
+ start
21
+ @obtained_lock.wait
22
+ yield
23
+ stop
24
+ end
25
+
26
+ def stop(timeout = nil)
27
+ if @check_task
28
+ logger.debug("Stopping locker check task")
29
+ @check_task.shutdown
30
+ @check_task.wait_for_termination(timeout)
31
+ logger.debug("Stopped locker check task")
32
+ end
33
+ end
34
+
35
+ def has_lock?
36
+ @obtained_lock.set?
37
+ end
38
+
39
+ private unless $TESTING
40
+
41
+ def start
42
+ logger.debug("Starting locker check task")
43
+ @check_task = Concurrent::TimerTask.new(execution_interval: @check_interval, run_now: true) do
44
+ get_or_refresh_lock
45
+ end
46
+ @check_task.execute
47
+ end
48
+
49
+ def get_or_refresh_lock
50
+ # redlock is in ms, not seconds
51
+ @lock = @lock_manager.lock(@key, @ttl * 1000, extend: @lock)
52
+ @obtained_lock.set if @lock
53
+ logger.debug("Leader lock #{"not " if !@lock}held")
54
+ @lock
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,48 @@
1
+ module Sidecloq
2
+ # Runner encapsulates a locker and a scheduler, running scheduler when
3
+ # "elected" leader
4
+ class Runner
5
+ include Utils
6
+
7
+ attr_reader :locker, :scheduler
8
+
9
+ def initialize(options = {})
10
+ @locker = extract_locker(options)
11
+ @scheduler = extract_scheduler(options)
12
+ end
13
+
14
+ def run
15
+ @thread = Thread.new do
16
+ logger.info("Runner starting")
17
+ @locker.with_lock do
18
+ # i am the leader
19
+ logger.info("Obtained leader lock")
20
+ @scheduler.run
21
+ end
22
+ logger.info("Runner ending")
23
+ end
24
+ end
25
+
26
+ def stop(timeout = nil)
27
+ logger.debug("Stopping runner")
28
+ if @locker.has_lock?
29
+ @scheduler.stop(timeout)
30
+ @locker.stop(timeout)
31
+ end
32
+ @thread.join if @thread
33
+ logger.debug("Stopped runner")
34
+ end
35
+
36
+ private unless $TESTING
37
+
38
+ def extract_locker(options)
39
+ return options[:locker] if options[:locker]
40
+ Locker.new(options[:locker_options] || {})
41
+ end
42
+
43
+ def extract_scheduler(options)
44
+ return options[:scheduler] if options[:scheduler]
45
+ Scheduler.new(options[:schedule], options[:scheduler_options] || {})
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,79 @@
1
+ module Sidecloq
2
+ # Schedule loads and parses recurring job specs from files, hashes, and redis
3
+ class Schedule
4
+ include Utils
5
+
6
+ REDIS_KEY = :sidecloq_schedule
7
+
8
+ attr_reader :job_specs
9
+
10
+ def initialize(specs)
11
+ @job_specs = specs
12
+ end
13
+
14
+ def self.from_yaml(filename)
15
+ from_hash(YAML.load_file(filename))
16
+ end
17
+
18
+ def self.from_redis
19
+ specs = {}
20
+
21
+ if redis { |r| r.exists(REDIS_KEY) }
22
+ specs = {}
23
+
24
+ redis { |r| r.hgetall(REDIS_KEY) }.tap do |h|
25
+ h.each do |name, config|
26
+ specs[name] = MultiJson.decode(config)
27
+ end
28
+ end
29
+ end
30
+
31
+ from_hash(specs)
32
+ end
33
+
34
+ def self.from_hash(hash)
35
+ if defined?(Rails) && hash.key?(Rails.env)
36
+ hash = hash[Rails.env]
37
+ end
38
+
39
+ specs = hash.inject({}) do |memo, (name, spec)|
40
+ memo[name] = spec.dup.tap do |s|
41
+ s['class'] = name unless spec.key?('class') || spec.key?(:class)
42
+ s['args'] = s['args'] || s[:args] || []
43
+ end
44
+ memo
45
+ end
46
+
47
+ new(specs)
48
+ end
49
+
50
+ def save_yaml(filename)
51
+ File.open(filename,'w') do |h|
52
+ h.write @job_specs.to_yaml
53
+ end
54
+ end
55
+
56
+ def save_redis
57
+ reset_redis_schedule
58
+ save_all_to_redis
59
+ end
60
+
61
+ private unless $TESTING
62
+
63
+ def reset_redis_schedule
64
+ redis do |r|
65
+ r.hkeys(REDIS_KEY).each do |k|
66
+ r.hdel(REDIS_KEY, k)
67
+ end
68
+ end
69
+ end
70
+
71
+ def save_all_to_redis
72
+ redis do |r|
73
+ @job_specs.each do |name, spec|
74
+ r.hset(REDIS_KEY, name, MultiJson.encode(spec))
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,85 @@
1
+ module Sidecloq
2
+ # Scheduler enqeues jobs according to the given schedule
3
+ class Scheduler
4
+ include Utils
5
+
6
+ def initialize(schedule, options = {})
7
+ @schedule = schedule
8
+ @options = options
9
+ end
10
+
11
+ # run queues jobs per their schedules, blocking forever
12
+ def run
13
+ logger.info("Loading schedules into redis")
14
+ sync_with_redis
15
+ logger.info("Starting scheduler")
16
+ load_schedule_into_rufus
17
+ logger.debug("Joining rufus thread")
18
+ rufus.join
19
+ logger.debug("Scheduler run ended")
20
+ end
21
+
22
+ def stop(timeout = nil)
23
+ logger.info("Stopping scheduler (timeout: #{timeout})")
24
+ if timeout
25
+ t = Concurrent::ScheduledTask.new(timeout) do
26
+ rufus.shutdown(:kill) if rufus.up?
27
+ end
28
+ Thread.new do
29
+ rufus.shutdown(:wait)
30
+ t.cancel
31
+ end
32
+ else
33
+ rufus.shutdown(:wait)
34
+ end
35
+ rufus.join
36
+ logger.info("Stopped scheduler")
37
+ end
38
+
39
+ private unless $TESTING
40
+
41
+ def rufus
42
+ @rufus ||= Rufus::Scheduler.new
43
+ end
44
+
45
+ def sync_with_redis
46
+ @schedule.save_redis
47
+ end
48
+
49
+ def load_schedule_into_rufus
50
+ logger.debug("Scheduling jobs")
51
+ @schedule.job_specs.each do |name, spec|
52
+ load_into_rufus(name, spec)
53
+ end
54
+ end
55
+
56
+ def load_into_rufus(name, spec)
57
+ # rufus will loop indefinitely trying to find the next event time if the
58
+ # cronline is impossible, like '0 5 31 2 *'
59
+ if will_never_run(spec['cron'])
60
+ logger.info("Impossible cronline detected, not scheduling #{name}: #{spec}")
61
+ else
62
+ logger.info("Scheduling #{name}: #{spec}")
63
+ rufus.cron(spec['cron']) do
64
+ safe_enqueue_job(name, spec)
65
+ end
66
+ end
67
+ end
68
+
69
+ def safe_enqueue_job(name, spec)
70
+ logger.info "enqueueing #{name}"
71
+
72
+ # failed enqeueuing should not b0rk stuff
73
+ begin
74
+ enqueue_job!(name, spec)
75
+ rescue => e
76
+ logger.info "error enqueuing #{name} - #{e.class.name}: #{e.message}"
77
+ end
78
+ end
79
+
80
+ # can raise exceptions, but shouldn't
81
+ def enqueue_job!(name, spec)
82
+ Sidekiq::Client.push(spec)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,58 @@
1
+ module Sidecloq
2
+ module Utils
3
+ class ContextLogger
4
+ def initialize(ctx)
5
+ @context = ctx
6
+ end
7
+
8
+ def method_missing(meth, *args)
9
+ Sidekiq::Logging.with_context(@context) do
10
+ Sidekiq.logger.send(meth, args)
11
+ end
12
+ end
13
+ end
14
+
15
+ def logger
16
+ @logger ||= ContextLogger.new('Sidecloq')
17
+ end
18
+
19
+ def redis(&block)
20
+ self.class.redis(&block)
21
+ end
22
+
23
+ # finds cron lines that are impossible, like '0 5 31 2 *'
24
+ # note: does not attempt to fully validate the cronline
25
+ def will_never_run(cronline)
26
+ # look for non-existent day of month
27
+ split = cronline.split(/\s+/)
28
+ if split.length > 3 &&
29
+ split[2] =~ /\d+/
30
+ split[3] =~ /\d+/
31
+
32
+ month = split[3].to_i
33
+ day = split[2].to_i
34
+ # special case for leap-year detection
35
+ return true if month == 2 && day <= 29
36
+
37
+ return !Date.valid_date?(0, month, day)
38
+
39
+ else
40
+ false
41
+ end
42
+ end
43
+
44
+ module ClassMethods
45
+ def redis(&block)
46
+ if block
47
+ Sidekiq.redis(&block)
48
+ else
49
+ Sidekiq.redis_pool.checkout
50
+ end
51
+ end
52
+ end
53
+
54
+ def self.included(klass)
55
+ klass.extend(ClassMethods)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ module Sidecloq
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,25 @@
1
+ module Sidecloq
2
+ module Web
3
+ VIEW_PATH = File.expand_path('../../../web/views', __FILE__)
4
+
5
+ def self.registered(app)
6
+
7
+ app.get '/recurring' do
8
+ @schedule = Schedule.from_redis
9
+
10
+ erb File.read(File.join(VIEW_PATH, 'recurring.erb'))
11
+ end
12
+
13
+ app.post '/recurring/:name/enqueue' do |name|
14
+ if spec = Sidecloq::Schedule.from_redis.job_specs[name]
15
+ Sidekiq::Client.push(spec)
16
+ end
17
+ redirect "#{root_path}recurring"
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+
24
+ Sidekiq::Web.register(Sidecloq::Web)
25
+ Sidekiq::Web.tabs['Recurring'] = 'recurring'
data/lib/sidecloq.rb ADDED
@@ -0,0 +1,75 @@
1
+ require 'sidekiq'
2
+ require 'concurrent'
3
+ require 'redlock'
4
+ require 'multi_json'
5
+ require 'rufus-scheduler'
6
+
7
+ require 'sidecloq/utils'
8
+ require 'sidecloq/schedule'
9
+ require 'sidecloq/locker'
10
+ require 'sidecloq/scheduler'
11
+ require 'sidecloq/runner'
12
+ require 'sidecloq/version'
13
+
14
+ module Sidecloq
15
+ def self.install
16
+ Sidekiq.configure_server do |config|
17
+ config.on(:startup) do
18
+ Sidecloq.startup
19
+ end
20
+
21
+ config.on(:shutdown) do
22
+ Sidecloq.shutdown
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.options
28
+ @options ||= {}
29
+ end
30
+
31
+ def self.options=(opts)
32
+ @options = opts
33
+ end
34
+
35
+ def self.configure
36
+ yield self
37
+ end
38
+
39
+ def self.startup
40
+ unless options[:scheduler]
41
+ options[:schedule] ||= extract_schedule
42
+ end
43
+ @runner = Runner.new(options)
44
+ @runner.run
45
+ end
46
+
47
+ def self.shutdown
48
+ @runner.stop(options[:timeout] || 10) if @runner
49
+ end
50
+
51
+ def self.extract_schedule
52
+ # do our best to do this automatically
53
+
54
+ # schedule handed to us
55
+ return options[:schedule] if options[:schedule]
56
+
57
+ # try for a file
58
+ options[:schedule_file] ||= "config/sidecloq.yml"
59
+ if File.exists?(options[:schedule_file])
60
+ return Schedule.from_yaml(options[:schedule_file])
61
+ elsif defined?(Rails)
62
+ # try rails-root-relative
63
+ full_path = File.join(Rails.root, options[:schedule_file])
64
+ if File.exists?(full_path)
65
+ options[:schedule_file] = full_path
66
+ return Schedule.from_yaml(options[:schedule_file])
67
+ end
68
+ end
69
+
70
+ # return an empty schedule
71
+ Schedule.new({})
72
+ end
73
+ end
74
+
75
+ Sidecloq.install
data/sidetoq.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sidecloq/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sidecloq"
8
+ spec.version = Sidecloq::VERSION
9
+ spec.authors = ["Matt Robinson"]
10
+ spec.email = ["robinson.matty@gmail.com"]
11
+
12
+ spec.summary = %q{Recurring jobs for Sidekiq}
13
+ spec.description = spec.summary
14
+ spec.homepage = "http://github.com/mattyr/sidecloq"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "bin"
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "sidekiq", "~> 4.0.1"
23
+ spec.add_dependency "redlock", "~> 0.1.2"
24
+ # mimics some dev dependencies of sidekiq:
25
+ spec.add_dependency "concurrent-ruby"
26
+ spec.add_dependency "sinatra", "~> 1.4", ">= 1.4.6"
27
+ spec.add_dependency "redis-namespace", "~> 1.5", ">= 1.5.2"
28
+ spec.add_dependency "multi_json", "~> 1.11"
29
+ spec.add_dependency "rufus-scheduler", "~> 3.1", ">= 3.1.10"
30
+
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "minitest"
33
+ end
@@ -0,0 +1,38 @@
1
+ <h3>Recurring Jobs</h3>
2
+
3
+ <div class="table_container">
4
+ <table class="table table-hover table-bordered table-striped table-white">
5
+ <thead>
6
+ <tr>
7
+ <th>Name</th>
8
+ <th>Description</th>
9
+ <th>Interval</th>
10
+ <th>Class</th>
11
+ <th>Queue</th>
12
+ <th>Arguments</th>
13
+ <th>Actions</th>
14
+ </tr>
15
+ </thead>
16
+
17
+ <tbody>
18
+ <% @schedule.job_specs.each do |name, job_spec| %>
19
+ <tr>
20
+ <td><%= name %></td>
21
+ <td><%= job_spec['description'] %></td>
22
+ <td><%= job_spec.fetch 'cron', job_spec['every'] %></td>
23
+ <td><%= job_spec['class'] %></td>
24
+ <td>
25
+ <a href="<%= root_path %>queues/<%= job_spec.fetch('queue', 'default') %>"><%= job_spec.fetch('queue', 'default') %></a>
26
+ </td>
27
+ <td><%= job_spec['args'] %></td>
28
+ <td>
29
+ <form action="<%= root_path %>recurring/<%= CGI.escape(name).gsub('+', '%20') %>/enqueue" method="post">
30
+ <%= csrf_tag if respond_to?(:csrf_tag) %>
31
+ <input class='btn btn-xs pull-left' type="submit" name="enque" value="<%= t('Enqueue Now') %>"/>
32
+ </form>
33
+ </td>
34
+ </tr>
35
+ <% end %>
36
+ </tbody>
37
+ </table>
38
+ </div>
metadata ADDED
@@ -0,0 +1,204 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidecloq
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Robinson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sidekiq
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.0.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: redlock
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.1.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.1.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: concurrent-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sinatra
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.4'
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 1.4.6
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - "~>"
70
+ - !ruby/object:Gem::Version
71
+ version: '1.4'
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 1.4.6
75
+ - !ruby/object:Gem::Dependency
76
+ name: redis-namespace
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.5'
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 1.5.2
85
+ type: :runtime
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: '1.5'
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: 1.5.2
95
+ - !ruby/object:Gem::Dependency
96
+ name: multi_json
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '1.11'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '1.11'
109
+ - !ruby/object:Gem::Dependency
110
+ name: rufus-scheduler
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '3.1'
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: 3.1.10
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '3.1'
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: 3.1.10
129
+ - !ruby/object:Gem::Dependency
130
+ name: rake
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - "~>"
134
+ - !ruby/object:Gem::Version
135
+ version: '10.0'
136
+ type: :development
137
+ prerelease: false
138
+ version_requirements: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - "~>"
141
+ - !ruby/object:Gem::Version
142
+ version: '10.0'
143
+ - !ruby/object:Gem::Dependency
144
+ name: minitest
145
+ requirement: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ description: Recurring jobs for Sidekiq
158
+ email:
159
+ - robinson.matty@gmail.com
160
+ executables: []
161
+ extensions: []
162
+ extra_rdoc_files: []
163
+ files:
164
+ - ".gitignore"
165
+ - ".travis.yml"
166
+ - Gemfile
167
+ - LICENSE.txt
168
+ - README.md
169
+ - Rakefile
170
+ - lib/sidecloq.rb
171
+ - lib/sidecloq/locker.rb
172
+ - lib/sidecloq/runner.rb
173
+ - lib/sidecloq/schedule.rb
174
+ - lib/sidecloq/scheduler.rb
175
+ - lib/sidecloq/utils.rb
176
+ - lib/sidecloq/version.rb
177
+ - lib/sidecloq/web.rb
178
+ - sidetoq.gemspec
179
+ - web/views/recurring.erb
180
+ homepage: http://github.com/mattyr/sidecloq
181
+ licenses:
182
+ - MIT
183
+ metadata: {}
184
+ post_install_message:
185
+ rdoc_options: []
186
+ require_paths:
187
+ - lib
188
+ required_ruby_version: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - ">="
191
+ - !ruby/object:Gem::Version
192
+ version: '0'
193
+ required_rubygems_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - ">="
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ requirements: []
199
+ rubyforge_project:
200
+ rubygems_version: 2.4.5.1
201
+ signing_key:
202
+ specification_version: 4
203
+ summary: Recurring jobs for Sidekiq
204
+ test_files: []