1
0
Fork 0
mirror of https://github.com/endofunky/sidetiq.git synced 2022-11-09 13:53:30 -05:00

Initial commit.

This commit is contained in:
Tobias Svensson 2013-01-31 17:42:19 +00:00
commit 08b76a3737
22 changed files with 553 additions and 0 deletions

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
lib/*.bundle
coverage
rdoc
doc
.yardoc
.bundle
Gemfile.lock
pkg
tmp

14
Gemfile Normal file
View 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
View 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
View 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
View 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
View 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

View file

@ -0,0 +1,4 @@
require 'rbconfig'
require 'mkmf'
create_makefile('sidetiq_ext')

View 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);
}

View 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
View 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'

View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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