mirror of
https://github.com/endofunky/sidetiq.git
synced 2022-11-09 13:53:30 -05:00
Move to actor-based concurrency.
Using plain threads in Sidekiq's Celluloid-based actor model is a little bit like riding a motorcycle without a helmet, so let's try to fit in a bit better.
This commit is contained in:
parent
37cc6976bf
commit
e0ebcaacd9
15 changed files with 126 additions and 184 deletions
|
@ -3,10 +3,12 @@
|
||||||
require 'sidekiq'
|
require 'sidekiq'
|
||||||
require 'sidetiq'
|
require 'sidetiq'
|
||||||
|
|
||||||
|
Sidekiq.logger.level = Logger::DEBUG
|
||||||
|
|
||||||
Sidekiq.options[:poll_interval] = 1
|
Sidekiq.options[:poll_interval] = 1
|
||||||
|
|
||||||
Sidekiq.configure_server do |config|
|
Sidekiq.configure_server do |config|
|
||||||
Sidetiq::Clock.start!
|
Sidetiq.clock.start!
|
||||||
end
|
end
|
||||||
|
|
||||||
class MyWorker
|
class MyWorker
|
||||||
|
|
|
@ -6,23 +6,43 @@ require 'socket'
|
||||||
# gems
|
# gems
|
||||||
require 'ice_cube'
|
require 'ice_cube'
|
||||||
require 'sidekiq'
|
require 'sidekiq'
|
||||||
|
require 'celluloid'
|
||||||
|
|
||||||
# internal
|
# internal
|
||||||
require 'sidetiq/api'
|
|
||||||
require 'sidetiq/config'
|
require 'sidetiq/config'
|
||||||
|
require 'sidetiq/logging'
|
||||||
|
require 'sidetiq/api'
|
||||||
require 'sidetiq/clock'
|
require 'sidetiq/clock'
|
||||||
require 'sidetiq/lock'
|
require 'sidetiq/lock'
|
||||||
require 'sidetiq/logging'
|
|
||||||
require 'sidetiq/middleware'
|
|
||||||
require 'sidetiq/schedule'
|
require 'sidetiq/schedule'
|
||||||
require 'sidetiq/schedulable'
|
require 'sidetiq/schedulable'
|
||||||
require 'sidetiq/version'
|
require 'sidetiq/version'
|
||||||
|
|
||||||
|
# actor topology
|
||||||
|
require 'sidetiq/actor/clock'
|
||||||
|
require 'sidetiq/supervisor'
|
||||||
|
|
||||||
# The Sidetiq namespace.
|
# The Sidetiq namespace.
|
||||||
module Sidetiq
|
module Sidetiq
|
||||||
include Sidetiq::API
|
include Sidetiq::API
|
||||||
include Sidetiq::Logging
|
|
||||||
|
|
||||||
# Expose all instance methods as singleton methods.
|
# Expose all instance methods as singleton methods.
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
|
class << self
|
||||||
|
# Public: Setter for the Sidetiq logger.
|
||||||
|
attr_writer :logger
|
||||||
|
end
|
||||||
|
|
||||||
|
# Public: Reader for the Sidetiq logger.
|
||||||
|
#
|
||||||
|
# Defaults to `Sidekiq.logger`.
|
||||||
|
def logger
|
||||||
|
@logger ||= Sidekiq.logger
|
||||||
|
end
|
||||||
|
|
||||||
|
# Public: Returns the Sidetiq::Clock actor.
|
||||||
|
def clock
|
||||||
|
Sidetiq::Supervisor.clock
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
32
lib/sidetiq/actor/clock.rb
Normal file
32
lib/sidetiq/actor/clock.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
module Sidetiq
|
||||||
|
module Actor
|
||||||
|
class Clock < Sidetiq::Clock
|
||||||
|
include Celluloid
|
||||||
|
|
||||||
|
# Public: Starts and supervises the clock actor.
|
||||||
|
def self.start!
|
||||||
|
actor.start!
|
||||||
|
end
|
||||||
|
|
||||||
|
# Public: Starts the clock loop.
|
||||||
|
def start!
|
||||||
|
debug "Sidetiq::Clock start"
|
||||||
|
loop!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def loop!
|
||||||
|
after([time { tick }, 0].max) do
|
||||||
|
loop!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def time
|
||||||
|
start = gettime
|
||||||
|
yield
|
||||||
|
Sidetiq.config.resolution - (gettime.to_f - start.to_f)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,7 +8,7 @@ module Sidetiq
|
||||||
|
|
||||||
# Public: Returns a Hash of Sidetiq::Schedule instances.
|
# Public: Returns a Hash of Sidetiq::Schedule instances.
|
||||||
def schedules
|
def schedules
|
||||||
Clock.schedules.dup
|
clock.schedules.dup
|
||||||
end
|
end
|
||||||
|
|
||||||
# Public: Currently scheduled recurring jobs.
|
# Public: Currently scheduled recurring jobs.
|
||||||
|
|
|
@ -1,25 +1,11 @@
|
||||||
module Sidetiq
|
module Sidetiq
|
||||||
configure do |config|
|
|
||||||
config.priority = Thread.main.priority
|
|
||||||
config.resolution = 1
|
|
||||||
config.lock_expire = 1000
|
|
||||||
config.utc = false
|
|
||||||
end
|
|
||||||
|
|
||||||
# Public: The Sidetiq clock.
|
# Public: The Sidetiq clock.
|
||||||
class Clock
|
class Clock
|
||||||
include Singleton
|
include Logging
|
||||||
|
|
||||||
# Internal: Returns a hash of Sidetiq::Schedule instances.
|
# Internal: Returns a hash of Sidetiq::Schedule instances.
|
||||||
attr_reader :schedules
|
attr_reader :schedules
|
||||||
|
|
||||||
# Internal: Returns the clock thread.
|
|
||||||
attr_reader :thread
|
|
||||||
|
|
||||||
def self.method_missing(meth, *args, &block) # :nodoc:
|
|
||||||
instance.__send__(meth, *args, &block)
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize # :nodoc:
|
def initialize # :nodoc:
|
||||||
super
|
super
|
||||||
@schedules = {}
|
@schedules = {}
|
||||||
|
@ -74,56 +60,6 @@ module Sidetiq
|
||||||
Sidetiq.config.utc ? Time.now.utc : Time.now
|
Sidetiq.config.utc ? Time.now.utc : Time.now
|
||||||
end
|
end
|
||||||
|
|
||||||
# Public: Starts the clock unless it is already running.
|
|
||||||
#
|
|
||||||
# Examples
|
|
||||||
#
|
|
||||||
# start!
|
|
||||||
# # => Thread
|
|
||||||
#
|
|
||||||
# Returns the Thread instance of the clock thread.
|
|
||||||
def start!
|
|
||||||
return if ticking?
|
|
||||||
|
|
||||||
Sidetiq.logger.info "Sidetiq::Clock start"
|
|
||||||
|
|
||||||
@thread = Thread.start { clock { tick } }
|
|
||||||
@thread.abort_on_exception = true
|
|
||||||
@thread.priority = Sidetiq.config.priority
|
|
||||||
@thread
|
|
||||||
end
|
|
||||||
|
|
||||||
# Public: Stops the clock if it is running.
|
|
||||||
#
|
|
||||||
# Examples
|
|
||||||
#
|
|
||||||
# stop!
|
|
||||||
# # => nil
|
|
||||||
#
|
|
||||||
# Returns nil.
|
|
||||||
def stop!
|
|
||||||
if ticking?
|
|
||||||
@thread.kill
|
|
||||||
Sidetiq.logger.info "Sidetiq::Clock stop"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Public: Returns the status of the clock.
|
|
||||||
#
|
|
||||||
# Examples
|
|
||||||
#
|
|
||||||
# ticking?
|
|
||||||
# # => false
|
|
||||||
#
|
|
||||||
# start!
|
|
||||||
# ticking?
|
|
||||||
# # => true
|
|
||||||
#
|
|
||||||
# Returns true or false.
|
|
||||||
def ticking?
|
|
||||||
@thread && @thread.alive?
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def enqueue(worker, time, redis)
|
def enqueue(worker, time, redis)
|
||||||
|
@ -132,7 +68,7 @@ module Sidetiq
|
||||||
next_run = (redis.get("#{key}:next") || -1).to_f
|
next_run = (redis.get("#{key}:next") || -1).to_f
|
||||||
|
|
||||||
if next_run < time_f
|
if next_run < time_f
|
||||||
Sidetiq.logger.info "Sidetiq::Clock enqueue #{worker.name} (at: #{time_f}) (last: #{next_run})"
|
info "Enqueue: #{worker.name} (at: #{time_f}) (last: #{next_run})"
|
||||||
|
|
||||||
redis.mset("#{key}:last", next_run, "#{key}:next", time_f)
|
redis.mset("#{key}:last", next_run, "#{key}:next", time_f)
|
||||||
|
|
||||||
|
@ -146,23 +82,6 @@ module Sidetiq
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def clock
|
|
||||||
loop do
|
|
||||||
sleep_time = time { yield }
|
|
||||||
|
|
||||||
if sleep_time > 0
|
|
||||||
Thread.pass
|
|
||||||
sleep sleep_time
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def time
|
|
||||||
start = gettime
|
|
||||||
yield
|
|
||||||
Sidetiq.config.resolution - (gettime.to_f - start.to_f)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
module Sidetiq
|
module Sidetiq
|
||||||
class Lock # :nodoc: all
|
class Lock # :nodoc: all
|
||||||
|
include Logging
|
||||||
|
|
||||||
attr_reader :key, :timeout
|
attr_reader :key, :timeout
|
||||||
|
|
||||||
OWNER = "#{Socket.gethostname}:#{Process.pid}"
|
OWNER = "#{Socket.gethostname}:#{Process.pid}"
|
||||||
|
@ -12,13 +14,13 @@ module Sidetiq
|
||||||
def synchronize
|
def synchronize
|
||||||
Sidekiq.redis do |redis|
|
Sidekiq.redis do |redis|
|
||||||
if lock(redis)
|
if lock(redis)
|
||||||
Sidetiq.logger.debug "Sidetiq::Clock lock #{key}"
|
debug "Sidetiq::Clock lock #{key}"
|
||||||
|
|
||||||
begin
|
begin
|
||||||
yield redis
|
yield redis
|
||||||
ensure
|
ensure
|
||||||
unlock(redis)
|
unlock(redis)
|
||||||
Sidetiq.logger.debug "Sidetiq::Clock unlock #{key}"
|
debug "Sidetiq::Clock unlock #{key}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
module Sidetiq
|
module Sidetiq
|
||||||
# Public: Sidetiq logging interface.
|
# Public: Sidetiq logging interface.
|
||||||
module Logging
|
module Logging
|
||||||
# Public: Setter for the Sidetiq logger.
|
%w(fatal error warn info debug).each do |level|
|
||||||
attr_writer :logger
|
level = level.to_sym
|
||||||
|
|
||||||
# Public: Reader for the Sidetiq logger.
|
define_method(level) do |msg|
|
||||||
#
|
Sidetiq.logger.__send__(level, "[Sidetiq] #{msg}")
|
||||||
# Defaults to `Sidekiq.logger`.
|
end
|
||||||
def logger
|
|
||||||
@logger ||= Sidekiq.logger
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
module Sidetiq
|
|
||||||
class Middleware
|
|
||||||
def initialize
|
|
||||||
@clock = Sidetiq::Clock.instance
|
|
||||||
end
|
|
||||||
|
|
||||||
def call(*args)
|
|
||||||
# Restart the clock if the thread died.
|
|
||||||
if !@clock.ticking?
|
|
||||||
Sidetiq.logger.warn "Sidetiq::Clock thread died. Restarting..."
|
|
||||||
@clock.start!
|
|
||||||
end
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Sidekiq.configure_server do |config|
|
|
||||||
config.server_middleware do |chain|
|
|
||||||
chain.add Sidetiq::Middleware
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ module Sidetiq
|
||||||
# end
|
# end
|
||||||
module Schedulable
|
module Schedulable
|
||||||
module ClassMethods
|
module ClassMethods
|
||||||
|
include Logging
|
||||||
|
|
||||||
# Public: Returns a Float timestamp of the last scheduled run.
|
# Public: Returns a Float timestamp of the last scheduled run.
|
||||||
def last_scheduled_occurrence
|
def last_scheduled_occurrence
|
||||||
get_timestamp "last"
|
get_timestamp "last"
|
||||||
|
@ -23,15 +25,14 @@ module Sidetiq
|
||||||
end
|
end
|
||||||
|
|
||||||
def tiq(*args, &block) # :nodoc:
|
def tiq(*args, &block) # :nodoc:
|
||||||
Sidetiq.logger.warn "DEPRECATION WARNING: Sidetiq::Schedulable#tiq" <<
|
warn "DEPRECATION WARNING: Sidetiq::Schedulable#tiq" <<
|
||||||
" is deprecated and will be removed. Use" <<
|
" is deprecated and will be removed. Use" <<
|
||||||
" Sidetiq::Schedulable#recurrence instead."
|
" Sidetiq::Schedulable#recurrence instead."
|
||||||
recurrence(*args, &block)
|
recurrence(*args, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
def recurrence(options = {}, &block) # :nodoc:
|
def recurrence(options = {}, &block) # :nodoc:
|
||||||
clock = Sidetiq::Clock.instance
|
schedule = Sidetiq.clock.schedule_for(self)
|
||||||
schedule = clock.schedule_for(self)
|
|
||||||
schedule.instance_eval(&block)
|
schedule.instance_eval(&block)
|
||||||
schedule.set_options(options)
|
schedule.set_options(options)
|
||||||
end
|
end
|
||||||
|
|
36
lib/sidetiq/supervisor.rb
Normal file
36
lib/sidetiq/supervisor.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
module Sidetiq
|
||||||
|
class Supervisor < Celluloid::SupervisionGroup
|
||||||
|
supervise Sidetiq::Actor::Clock, as: :sidetiq_clock
|
||||||
|
|
||||||
|
class << self
|
||||||
|
include Logging
|
||||||
|
|
||||||
|
def clock
|
||||||
|
run! if Celluloid::Actor[:sidetiq_clock].nil?
|
||||||
|
|
||||||
|
Celluloid::Actor[:sidetiq_clock]
|
||||||
|
end
|
||||||
|
|
||||||
|
def run!
|
||||||
|
motd
|
||||||
|
debug "Sidetiq::Supervisor start"
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
motd
|
||||||
|
debug "Sidetiq::Supervisor start (foreground)"
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def motd
|
||||||
|
info "Sidetiq v#{VERSION::STRING} booting ..."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -5,28 +5,18 @@ module Sidetiq
|
||||||
VIEWS = File.expand_path('views', File.dirname(__FILE__))
|
VIEWS = File.expand_path('views', File.dirname(__FILE__))
|
||||||
|
|
||||||
def self.registered(app)
|
def self.registered(app)
|
||||||
app.helpers do
|
|
||||||
def sidetiq_clock
|
|
||||||
Sidetiq::Clock.instance
|
|
||||||
end
|
|
||||||
|
|
||||||
def sidetiq_schedules
|
|
||||||
sidetiq_clock.schedules
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
app.get "/sidetiq" do
|
app.get "/sidetiq" do
|
||||||
@schedules = sidetiq_schedules
|
@schedules = Sidetiq.schedules
|
||||||
@time = sidetiq_clock.gettime
|
@time = Sidetiq.clock.gettime
|
||||||
erb File.read(File.join(VIEWS, 'sidetiq.erb'))
|
erb File.read(File.join(VIEWS, 'sidetiq.erb'))
|
||||||
end
|
end
|
||||||
|
|
||||||
app.get "/sidetiq/:name" do
|
app.get "/sidetiq/:name" do
|
||||||
halt 404 unless (name = params[:name])
|
halt 404 unless (name = params[:name])
|
||||||
|
|
||||||
@time = sidetiq_clock.gettime
|
@time = Sidetiq.clock.gettime
|
||||||
|
|
||||||
@worker, @schedule = sidetiq_schedules.select do |worker, _|
|
@worker, @schedule = Sidetiq.schedules.select do |worker, _|
|
||||||
worker.name == name
|
worker.name == name
|
||||||
end.flatten
|
end.flatten
|
||||||
|
|
||||||
|
@ -36,7 +26,7 @@ module Sidetiq
|
||||||
app.post "/sidetiq/:name/trigger" do
|
app.post "/sidetiq/:name/trigger" do
|
||||||
halt 404 unless (name = params[:name])
|
halt 404 unless (name = params[:name])
|
||||||
|
|
||||||
worker, _ = sidetiq_schedules.select do |w, _|
|
worker, _ = Sidetiq.schedules.select do |w, _|
|
||||||
w.name == name
|
w.name == name
|
||||||
end.flatten
|
end.flatten
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,9 @@ Gem::Specification.new do |gem|
|
||||||
gem.require_paths = ["lib"]
|
gem.require_paths = ["lib"]
|
||||||
gem.extensions = []
|
gem.extensions = []
|
||||||
|
|
||||||
gem.add_dependency 'sidekiq', '~> 2.14.0'
|
gem.add_dependency 'sidekiq', '~> 2.14.0'
|
||||||
gem.add_dependency 'ice_cube', '~> 0.11.0'
|
gem.add_dependency 'celluloid', '>= 0.14.1'
|
||||||
|
gem.add_dependency 'ice_cube', '~> 0.11.0'
|
||||||
|
|
||||||
gem.add_development_dependency 'rake'
|
gem.add_development_dependency 'rake'
|
||||||
gem.add_development_dependency 'sinatra'
|
gem.add_development_dependency 'sinatra'
|
||||||
|
|
|
@ -13,6 +13,12 @@ require 'sidekiq/testing'
|
||||||
require 'sidetiq'
|
require 'sidetiq'
|
||||||
require 'sidetiq/web'
|
require 'sidetiq/web'
|
||||||
|
|
||||||
|
class Sidetiq::Supervisor
|
||||||
|
def self.clock
|
||||||
|
@clock ||= Sidetiq::Clock.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Keep the test output clean.
|
# Keep the test output clean.
|
||||||
Sidetiq.logger = Logger.new(nil)
|
Sidetiq.logger = Logger.new(nil)
|
||||||
|
|
||||||
|
@ -40,7 +46,7 @@ class Sidetiq::TestCase < MiniTest::Unit::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
def clock
|
def clock
|
||||||
@clock ||= Sidetiq::Clock.instance
|
Sidetiq.clock
|
||||||
end
|
end
|
||||||
|
|
||||||
# Blatantly stolen from Sidekiq's test suite.
|
# Blatantly stolen from Sidekiq's test suite.
|
||||||
|
|
|
@ -1,30 +1,6 @@
|
||||||
require_relative 'helper'
|
require_relative 'helper'
|
||||||
|
|
||||||
class TestClock < Sidetiq::TestCase
|
class TestClock < Sidetiq::TestCase
|
||||||
def test_delegates_to_instance
|
|
||||||
Sidetiq::Clock.instance.expects(:foo).once
|
|
||||||
Sidetiq::Clock.foo
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_start_stop
|
|
||||||
refute clock.ticking?
|
|
||||||
assert_nil clock.thread
|
|
||||||
|
|
||||||
clock.start!
|
|
||||||
Thread.pass
|
|
||||||
sleep 0.01
|
|
||||||
|
|
||||||
assert clock.ticking?
|
|
||||||
assert_kind_of Thread, clock.thread
|
|
||||||
|
|
||||||
clock.stop!
|
|
||||||
Thread.pass
|
|
||||||
sleep 0.01
|
|
||||||
|
|
||||||
refute clock.ticking?
|
|
||||||
refute clock.thread.alive?
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_gettime_seconds
|
def test_gettime_seconds
|
||||||
assert_equal clock.gettime.tv_sec, Time.now.tv_sec
|
assert_equal clock.gettime.tv_sec, Time.now.tv_sec
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
require_relative 'helper'
|
|
||||||
|
|
||||||
class TestMiddleware < Sidetiq::TestCase
|
|
||||||
def middleware
|
|
||||||
Sidetiq::Middleware.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_restarts_clock
|
|
||||||
clock.stubs(:ticking?).returns(false)
|
|
||||||
clock.expects(:start!).once
|
|
||||||
middleware.call {}
|
|
||||||
|
|
||||||
clock.stubs(:ticking?).returns(true)
|
|
||||||
clock.expects(:start!).never
|
|
||||||
middleware.call {}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue