mirror of
https://github.com/endofunky/sidetiq.git
synced 2022-11-09 13:53:30 -05:00
Initial commit.
This commit is contained in:
commit
08b76a3737
22 changed files with 553 additions and 0 deletions
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
lib/*.bundle
|
||||
coverage
|
||||
rdoc
|
||||
doc
|
||||
.yardoc
|
||||
.bundle
|
||||
Gemfile.lock
|
||||
pkg
|
||||
tmp
|
14
Gemfile
Normal file
14
Gemfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
group :development do
|
||||
gem 'rake'
|
||||
gem 'rake-compiler'
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'simplecov'
|
||||
gem 'mocha'
|
||||
end
|
||||
|
||||
gemspec
|
||||
|
20
LICENSE
Normal file
20
LICENSE
Normal file
|
@ -0,0 +1,20 @@
|
|||
Copyright (C) 2012 by Tobias Svensson <tobias@musicglue.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
90
README.md
Normal file
90
README.md
Normal file
|
@ -0,0 +1,90 @@
|
|||
Sidetiq
|
||||
=======
|
||||
|
||||
Recurring jobs for [Sidekiq](http://mperham.github.com/sidekiq/).
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
Sidetiq provides a simple API for defining recurring workers for Sidekiq.
|
||||
|
||||
- Flexible DSL based on [ice_cube](http://seejohnrun.github.com/ice_cube/)
|
||||
- High-resolution timer using `clock_gettime(3)` (or `mach_absolute_time()` on
|
||||
Apple Mac OS X), allowing for accurate sub-second clock ticks.
|
||||
|
||||
## DEPENDENCIES
|
||||
|
||||
- [Sidekiq](http://mperham.github.com/sidekiq/)
|
||||
- [ice_cube](http://seejohnrun.github.com/ice_cube/)
|
||||
|
||||
## INSTALLATION
|
||||
|
||||
The best way to install Sidetiq is with RubyGems:
|
||||
|
||||
$ [sudo] gem install sidetiq
|
||||
|
||||
If you're installing from source, you can use [Bundler](http://gembundler.com/)
|
||||
to pick up all the gems ([more info](http://gembundler.com/bundle_install.html)):
|
||||
|
||||
$ bundle install
|
||||
|
||||
## GETTING STARTED
|
||||
|
||||
Defining recurring jobs is simple:
|
||||
|
||||
```ruby
|
||||
class MyWorker
|
||||
include Sidekiq::Worker
|
||||
include Sidetiq::Schedulable
|
||||
|
||||
# Every other month on the first monday and last tuesday at 12 o'clock.
|
||||
tiq { monthly(2).day_of_week(1 => [1], 2 => [-1]).hour_of_day(12) }
|
||||
end
|
||||
```
|
||||
|
||||
It also is possible to define multiple scheduling rules for a worker:
|
||||
|
||||
```ruby
|
||||
class MyWorker
|
||||
include Sidekiq::Worker
|
||||
include Sidetiq::Schedulable
|
||||
|
||||
tiq do
|
||||
# Every third year in March
|
||||
yearly(3).month_of_year(:march)
|
||||
|
||||
# Every fourth year in Februrary
|
||||
yearly(2).month_of_year(:februrary)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The first time the tiq method is called, Sidetiq will automatically spin up
|
||||
it's clock thread and enqueue jobs for their next occurrence using
|
||||
`#perform_at`. Note that by default Sidekiq only polls every 15 seconds.
|
||||
|
||||
If workers are spread across multiple machines multiple jobs might be enqueued
|
||||
at the same time. This can be avoided by using a locking library for Sidekiq,
|
||||
such as [sidekiq-unique-jobs](https://github.com/form26/sidekiq-unique-jobs).
|
||||
|
||||
## CONTRIBUTE
|
||||
|
||||
If you'd like to contribute to Sidetiq, start by forking my repo on GitHub:
|
||||
|
||||
[http://github.com/tobiassvn/sidetiq](http://github.com/tobiassvn/sidetiq)
|
||||
|
||||
To get all of the dependencies, install the gem first. The best way to get
|
||||
your changes merged back into core is as follows:
|
||||
|
||||
1. Clone down your fork
|
||||
1. Create a thoughtfully named topic branch to contain your change
|
||||
1. Write some code
|
||||
1. Add tests and make sure everything still passes by running `rake`
|
||||
1. If you are adding new functionality, document it in the README
|
||||
1. Do not change the version number, I will do that on my end
|
||||
1. If necessary, rebase your commits into logical chunks, without errors
|
||||
1. Push the branch up to GitHub
|
||||
1. Send a pull request to the tobiassvn/sidetiq project.
|
||||
|
||||
## LICENSE
|
||||
|
||||
Sidetiq is released under the MIT License. See LICENSE for further details.
|
11
Rakefile
Normal file
11
Rakefile
Normal file
|
@ -0,0 +1,11 @@
|
|||
require 'bundler/gem_tasks'
|
||||
require 'rake/testtask'
|
||||
require 'rake/extensiontask'
|
||||
|
||||
Rake::ExtensionTask.new('sidetiq_ext')
|
||||
|
||||
Rake::TestTask.new do |t|
|
||||
t.pattern = 'test/**/test_*.rb'
|
||||
end
|
||||
|
||||
task default: :test
|
19
examples/simple.rb
Normal file
19
examples/simple.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
require 'sidetiq'
|
||||
|
||||
# We're only loading this so we don't actually have to connect to redis.
|
||||
require 'sidekiq/testing'
|
||||
|
||||
class ExampleWorker
|
||||
include Sidekiq::Worker
|
||||
include Sidetiq::Schedulable
|
||||
|
||||
def self.perform_at(time)
|
||||
puts "Enqueued to run at #{time}"
|
||||
end
|
||||
|
||||
# Run every 2 seconds
|
||||
tiq { secondly(2) }
|
||||
end
|
||||
|
||||
puts "Hit C-c to quit."
|
||||
sleep 1000000
|
4
ext/sidetiq_ext/extconf.rb
Normal file
4
ext/sidetiq_ext/extconf.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
require 'rbconfig'
|
||||
require 'mkmf'
|
||||
create_makefile('sidetiq_ext')
|
||||
|
97
ext/sidetiq_ext/sidetiq_ext.c
Normal file
97
ext/sidetiq_ext/sidetiq_ext.c
Normal file
|
@ -0,0 +1,97 @@
|
|||
#include <ruby.h>
|
||||
#include <assert.h>
|
||||
#include "sidetiq_ext.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <sys/time.h>
|
||||
#include <sys/resource.h>
|
||||
#include <mach/mach.h>
|
||||
#include <mach/clock.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sched.h>
|
||||
#else
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
VALUE msidetiq;
|
||||
VALUE esidetiq_error;
|
||||
VALUE csidetiq_clock;
|
||||
|
||||
#ifdef __APPLE__
|
||||
static mach_timebase_info_data_t clock_gettime_inf;
|
||||
|
||||
typedef enum {
|
||||
CLOCK_REALTIME,
|
||||
CLOCK_MONOTONIC,
|
||||
CLOCK_PROCESS_CPUTIME_ID,
|
||||
CLOCK_THREAD_CPUTIME_ID
|
||||
} clockid_t;
|
||||
|
||||
int clock_gettime(clockid_t clk_id, struct timespec *tp)
|
||||
{
|
||||
kern_return_t ret;
|
||||
clock_serv_t clk;
|
||||
clock_id_t clk_serv_id;
|
||||
mach_timespec_t tm;
|
||||
uint64_t start, end, delta, nano;
|
||||
int retval = -1;
|
||||
|
||||
switch (clk_id) {
|
||||
case CLOCK_REALTIME:
|
||||
case CLOCK_MONOTONIC:
|
||||
clk_serv_id = clk_id == CLOCK_REALTIME ? CALENDAR_CLOCK : SYSTEM_CLOCK;
|
||||
if (KERN_SUCCESS == (ret = host_get_clock_service(mach_host_self(), clk_serv_id, &clk))) {
|
||||
if (KERN_SUCCESS == (ret = clock_get_time(clk, &tm))) {
|
||||
tp->tv_sec = tm.tv_sec;
|
||||
tp->tv_nsec = tm.tv_nsec;
|
||||
retval = 0;
|
||||
}
|
||||
}
|
||||
if (KERN_SUCCESS != ret) {
|
||||
errno = EINVAL;
|
||||
retval = -1;
|
||||
}
|
||||
break;
|
||||
case CLOCK_PROCESS_CPUTIME_ID:
|
||||
case CLOCK_THREAD_CPUTIME_ID:
|
||||
start = mach_absolute_time();
|
||||
if (clk_id == CLOCK_PROCESS_CPUTIME_ID) {
|
||||
getpid();
|
||||
} else {
|
||||
sched_yield();
|
||||
}
|
||||
end = mach_absolute_time();
|
||||
delta = end - start;
|
||||
if (0 == clock_gettime_inf.denom) {
|
||||
mach_timebase_info(&clock_gettime_inf);
|
||||
}
|
||||
nano = delta * clock_gettime_inf.numer / clock_gettime_inf.denom;
|
||||
tp->tv_sec = nano * 1e-9;
|
||||
tp->tv_nsec = nano - (tp->tv_sec * 1e9);
|
||||
retval = 0;
|
||||
break;
|
||||
default:
|
||||
errno = EINVAL;
|
||||
retval = -1;
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
#endif
|
||||
|
||||
static VALUE sidetiq_gettime(VALUE self)
|
||||
{
|
||||
struct timespec time;
|
||||
assert(clock_gettime(CLOCK_REALTIME, &time) == 0);
|
||||
return rb_time_nano_new(time.tv_sec, time.tv_nsec);
|
||||
}
|
||||
|
||||
void Init_sidetiq_ext()
|
||||
{
|
||||
msidetiq = rb_define_module("Sidetiq");
|
||||
esidetiq_error = rb_define_class_under(msidetiq, "Error", rb_eStandardError);
|
||||
csidetiq_clock = rb_define_class_under(msidetiq, "Clock", rb_cObject);
|
||||
rb_define_method(csidetiq_clock, "gettime", sidetiq_gettime, 0);
|
||||
}
|
||||
|
19
ext/sidetiq_ext/sidetiq_ext.h
Normal file
19
ext/sidetiq_ext/sidetiq_ext.h
Normal file
|
@ -0,0 +1,19 @@
|
|||
#ifndef __SIDETIQ_EXT_H__
|
||||
#define __SIDETIQ_EXT_H__
|
||||
#include <ruby.h>
|
||||
|
||||
typedef uint64_t sidetiq_time_t;
|
||||
|
||||
void Init_sidetiq_ext();
|
||||
static VALUE sidetiq_gettime(VALUE self);
|
||||
|
||||
/* module Sidetiq */
|
||||
extern VALUE msidetiq;
|
||||
|
||||
/* class Sidetiq::Error < StandardError */
|
||||
extern VALUE esidetiq_error;
|
||||
|
||||
/* class Sidetiq::Clock */
|
||||
extern VALUE csidetiq_clock;
|
||||
|
||||
#endif
|
19
lib/sidetiq.rb
Normal file
19
lib/sidetiq.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# stdlib
|
||||
require 'singleton'
|
||||
require 'monitor'
|
||||
require 'ostruct'
|
||||
|
||||
# gems
|
||||
require 'sidekiq'
|
||||
require 'ice_cube'
|
||||
|
||||
# c extensions
|
||||
require 'sidetiq_ext'
|
||||
|
||||
# internal
|
||||
require 'sidetiq/config'
|
||||
require 'sidetiq/clock'
|
||||
require 'sidetiq/schedule'
|
||||
require 'sidetiq/schedulable'
|
||||
require 'sidetiq/version'
|
||||
|
14
lib/sidetiq/class_methods.rb
Normal file
14
lib/sidetiq/class_methods.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
module Sidetiq
|
||||
module Schedulable
|
||||
module ClassMethods
|
||||
def tiq(&block)
|
||||
Sidetiq::Scheduler.instance.instance_eval(&block)
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(klass)
|
||||
klass.extend(Sidetiq::Schedulable::ClassMethods)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
55
lib/sidetiq/clock.rb
Normal file
55
lib/sidetiq/clock.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
module Sidetiq
|
||||
configure do |config|
|
||||
config.priority = Thread.current.priority
|
||||
config.resolution = 0.2
|
||||
end
|
||||
|
||||
class Clock
|
||||
include Singleton
|
||||
include MonitorMixin
|
||||
|
||||
attr_reader :schedules
|
||||
|
||||
def initialize
|
||||
super
|
||||
@schedules = {}
|
||||
start!
|
||||
end
|
||||
|
||||
def schedule_for(worker)
|
||||
schedules[worker] ||= Sidetiq::Schedule.new
|
||||
end
|
||||
|
||||
def tick
|
||||
@tick = gettime
|
||||
|
||||
synchronize do
|
||||
schedules.each do |worker, schedule|
|
||||
if schedule.schedule_next?(@tick)
|
||||
occurrence = schedule.next_occurrence
|
||||
Sidekiq.logger.info "Sidetiq::Clock enqueue #{worker.name} (at: #{occurrence.to_s})"
|
||||
worker.perform_at(occurrence)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def start!
|
||||
Sidekiq.logger.info "Sidetiq::Clock start"
|
||||
thr = Thread.start { clock { tick } }
|
||||
thr.abort_on_exception = true
|
||||
thr.priority = Sidetiq.config.resolution
|
||||
end
|
||||
|
||||
def clock
|
||||
loop do
|
||||
yield
|
||||
Thread.pass
|
||||
sleep Sidetiq.config.resolution
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
14
lib/sidetiq/config.rb
Normal file
14
lib/sidetiq/config.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
module Sidetiq
|
||||
class << self
|
||||
attr_writer :config
|
||||
|
||||
def configure
|
||||
yield config
|
||||
end
|
||||
|
||||
def config
|
||||
@config ||= OpenStruct.new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
19
lib/sidetiq/schedulable.rb
Normal file
19
lib/sidetiq/schedulable.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
module Sidetiq
|
||||
module Schedulable
|
||||
module ClassMethods
|
||||
def tiq(&block)
|
||||
clock = Sidetiq::Clock.instance
|
||||
worker = block.send(:binding).eval('self')
|
||||
|
||||
clock.synchronize do
|
||||
clock.schedule_for(worker).instance_eval(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(klass)
|
||||
klass.extend(Sidetiq::Schedulable::ClassMethods)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
22
lib/sidetiq/schedule.rb
Normal file
22
lib/sidetiq/schedule.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
module Sidetiq
|
||||
class Schedule < IceCube::Schedule
|
||||
def method_missing(meth, *args, &block)
|
||||
if IceCube::Rule.respond_to?(meth)
|
||||
rule = IceCube::Rule.send(meth, *args, &block)
|
||||
add_recurrence_rule(rule)
|
||||
rule
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def schedule_next?(time)
|
||||
if @last_scheduled != (no = next_occurrence(time))
|
||||
@last_scheduled = no
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
10
lib/sidetiq/version.rb
Normal file
10
lib/sidetiq/version.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
module Sidetiq
|
||||
module VERSION
|
||||
MAJOR = 0
|
||||
MINOR = 1
|
||||
PATCH = 0
|
||||
|
||||
STRING = [MAJOR, MINOR, PATCH].compact.join('.')
|
||||
end
|
||||
end
|
||||
|
24
sidetiq.gemspec
Normal file
24
sidetiq.gemspec
Normal file
|
@ -0,0 +1,24 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
lib = File.expand_path(File.join('..', 'lib'), __FILE__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
require 'sidetiq/version'
|
||||
|
||||
Gem::Specification.new do |gem|
|
||||
gem.name = "sidetiq"
|
||||
gem.version = Sidetiq::VERSION::STRING
|
||||
gem.authors = ["Tobias Svensson"]
|
||||
gem.email = ["tob@tobiassvensson.co.uk"]
|
||||
gem.description = "High-resolution job scheduler for Sidekiq"
|
||||
gem.summary = gem.description
|
||||
gem.homepage = "http://github.com/tobiassvn/sidetiq"
|
||||
gem.license = "MIT"
|
||||
|
||||
gem.files = `git ls-files`.split($/)
|
||||
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
||||
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
||||
gem.require_paths = ["lib"]
|
||||
gem.extensions = ['ext/sidetiq_ext/extconf.rb']
|
||||
|
||||
gem.add_dependency 'sidekiq'
|
||||
gem.add_dependency 'ice_cube'
|
||||
end
|
16
test/helper.rb
Normal file
16
test/helper.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
require 'simplecov'
|
||||
SimpleCov.start { add_filter "/test/" }
|
||||
require 'minitest/autorun'
|
||||
require 'mocha/setup'
|
||||
require 'sidetiq'
|
||||
require 'sidekiq/testing'
|
||||
|
||||
# Stub out Clock#start! so we don't actually loop
|
||||
module Sidetiq
|
||||
class Clock
|
||||
def start!; end
|
||||
end
|
||||
end
|
||||
|
||||
# Keep the test output clean
|
||||
Sidekiq.logger = Logger.new(nil)
|
12
test/test_clock.rb
Normal file
12
test/test_clock.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
require_relative 'helper'
|
||||
|
||||
class TestClock < MiniTest::Unit::TestCase
|
||||
def test_gettime_seconds
|
||||
assert_equal Sidetiq::Clock.instance.gettime.tv_sec, Time.now.tv_sec
|
||||
end
|
||||
|
||||
def test_gettime_nsec
|
||||
refute_nil Sidetiq::Clock.instance.gettime.tv_nsec
|
||||
end
|
||||
end
|
||||
|
7
test/test_errors.rb
Normal file
7
test/test_errors.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
require_relative 'helper'
|
||||
|
||||
class TestErrors < MiniTest::Unit::TestCase
|
||||
def test_error_superclass
|
||||
assert_equal StandardError, Sidetiq::Error.superclass
|
||||
end
|
||||
end
|
37
test/test_sidetiq.rb
Normal file
37
test/test_sidetiq.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
require_relative 'helper'
|
||||
|
||||
class TestSidetiq < MiniTest::Unit::TestCase
|
||||
class Worker
|
||||
include Sidekiq::Worker
|
||||
include Sidetiq::Schedulable
|
||||
|
||||
tiq do
|
||||
daily.hour_of_day(0)
|
||||
end
|
||||
end
|
||||
|
||||
def clock
|
||||
@clock ||= Sidetiq::Clock.instance
|
||||
end
|
||||
|
||||
def tick
|
||||
clock.tick
|
||||
end
|
||||
|
||||
def test_scheduling
|
||||
assert_equal 0, Worker.jobs.size # sanity
|
||||
|
||||
clock.stubs(:gettime).returns(Time.now + (24 * 60 * 60))
|
||||
tick
|
||||
assert_equal 1, Worker.jobs.size
|
||||
|
||||
clock.stubs(:gettime).returns(Time.now + (2 * 24 * 60 * 60))
|
||||
tick
|
||||
assert_equal 2, Worker.jobs.size
|
||||
|
||||
clock.stubs(:gettime).returns(Time.now + (2 * 24 * 60 * 60 + 1))
|
||||
tick
|
||||
assert_equal 2, Worker.jobs.size
|
||||
end
|
||||
end
|
||||
|
21
test/test_version.rb
Normal file
21
test/test_version.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
require_relative 'helper'
|
||||
|
||||
class TestVersion < MiniTest::Unit::TestCase
|
||||
def test_major
|
||||
assert_instance_of Fixnum, Sidetiq::VERSION::MAJOR
|
||||
end
|
||||
|
||||
def test_minor
|
||||
assert_instance_of Fixnum, Sidetiq::VERSION::MINOR
|
||||
end
|
||||
|
||||
def test_patch
|
||||
assert_instance_of Fixnum, Sidetiq::VERSION::PATCH
|
||||
end
|
||||
|
||||
def test_string
|
||||
assert_equal Sidetiq::VERSION::STRING, [Sidetiq::VERSION::MAJOR,
|
||||
Sidetiq::VERSION::MINOR, Sidetiq::VERSION::PATCH].compact.join('.')
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in a new issue