resque-admin-scheduler 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/AUTHORS.md +81 -0
- data/CHANGELOG.md +456 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +6 -0
- data/Gemfile +4 -0
- data/LICENSE +23 -0
- data/README.md +691 -0
- data/Rakefile +21 -0
- data/exe/resque-scheduler +5 -0
- data/lib/resque/scheduler/cli.rb +147 -0
- data/lib/resque/scheduler/configuration.rb +73 -0
- data/lib/resque/scheduler/delaying_extensions.rb +324 -0
- data/lib/resque/scheduler/env.rb +89 -0
- data/lib/resque/scheduler/extension.rb +13 -0
- data/lib/resque/scheduler/failure_handler.rb +11 -0
- data/lib/resque/scheduler/lock/base.rb +61 -0
- data/lib/resque/scheduler/lock/basic.rb +27 -0
- data/lib/resque/scheduler/lock/resilient.rb +78 -0
- data/lib/resque/scheduler/lock.rb +4 -0
- data/lib/resque/scheduler/locking.rb +104 -0
- data/lib/resque/scheduler/logger_builder.rb +72 -0
- data/lib/resque/scheduler/plugin.rb +31 -0
- data/lib/resque/scheduler/scheduling_extensions.rb +141 -0
- data/lib/resque/scheduler/server/views/delayed.erb +63 -0
- data/lib/resque/scheduler/server/views/delayed_schedules.erb +20 -0
- data/lib/resque/scheduler/server/views/delayed_timestamp.erb +26 -0
- data/lib/resque/scheduler/server/views/requeue-params.erb +23 -0
- data/lib/resque/scheduler/server/views/scheduler.erb +58 -0
- data/lib/resque/scheduler/server/views/search.erb +72 -0
- data/lib/resque/scheduler/server/views/search_form.erb +8 -0
- data/lib/resque/scheduler/server.rb +268 -0
- data/lib/resque/scheduler/signal_handling.rb +40 -0
- data/lib/resque/scheduler/tasks.rb +25 -0
- data/lib/resque/scheduler/util.rb +39 -0
- data/lib/resque/scheduler/version.rb +7 -0
- data/lib/resque/scheduler.rb +447 -0
- data/lib/resque-scheduler.rb +4 -0
- data/resque-scheduler.gemspec +60 -0
- metadata +330 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Scheduler
|
5
|
+
module Lock
|
6
|
+
class Base
|
7
|
+
attr_reader :key
|
8
|
+
attr_accessor :timeout
|
9
|
+
|
10
|
+
def initialize(key, options = {})
|
11
|
+
@key = key
|
12
|
+
|
13
|
+
# 3 minute default timeout
|
14
|
+
@timeout = options[:timeout] || 60 * 3
|
15
|
+
end
|
16
|
+
|
17
|
+
# Attempts to acquire the lock. Returns true if successfully acquired.
|
18
|
+
def acquire!
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def value
|
23
|
+
@value ||= [hostname, process_id].join(':')
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns true if you currently hold the lock.
|
27
|
+
def locked?
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
# Releases the lock.
|
32
|
+
def release!
|
33
|
+
Resque.redis.del(key) == 1
|
34
|
+
end
|
35
|
+
|
36
|
+
# Releases the lock iff we own it
|
37
|
+
def release
|
38
|
+
locked? && release!
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# Extends the lock by `timeout` seconds.
|
44
|
+
def extend_lock!
|
45
|
+
Resque.redis.expire(key, timeout)
|
46
|
+
end
|
47
|
+
|
48
|
+
def hostname
|
49
|
+
local_hostname = Socket.gethostname
|
50
|
+
Socket.gethostbyname(local_hostname).first
|
51
|
+
rescue
|
52
|
+
local_hostname
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_id
|
56
|
+
Process.pid
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
require_relative 'base'
|
3
|
+
|
4
|
+
module Resque
|
5
|
+
module Scheduler
|
6
|
+
module Lock
|
7
|
+
class Basic < Base
|
8
|
+
def acquire!
|
9
|
+
if Resque.redis.setnx(key, value)
|
10
|
+
extend_lock!
|
11
|
+
true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def locked?
|
16
|
+
if Resque.redis.get(key) == value
|
17
|
+
extend_lock!
|
18
|
+
|
19
|
+
return true if Resque.redis.get(key) == value
|
20
|
+
end
|
21
|
+
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
require_relative 'base'
|
3
|
+
|
4
|
+
module Resque
|
5
|
+
module Scheduler
|
6
|
+
module Lock
|
7
|
+
class Resilient < Base
|
8
|
+
def acquire!
|
9
|
+
evalsha(:acquire, [key], [value]).to_i == 1
|
10
|
+
end
|
11
|
+
|
12
|
+
def locked?
|
13
|
+
evalsha(:locked, [key], [value]).to_i == 1
|
14
|
+
end
|
15
|
+
|
16
|
+
def timeout=(seconds)
|
17
|
+
if locked?
|
18
|
+
@timeout = seconds
|
19
|
+
@locked_sha = nil
|
20
|
+
@acquire_sha = nil
|
21
|
+
end
|
22
|
+
@timeout
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def evalsha(script, keys, argv, refresh: false)
|
28
|
+
sha_method_name = "#{script}_sha"
|
29
|
+
Resque.redis.evalsha(
|
30
|
+
send(sha_method_name, refresh),
|
31
|
+
keys: keys,
|
32
|
+
argv: argv
|
33
|
+
)
|
34
|
+
rescue Redis::CommandError => e
|
35
|
+
if e.message =~ /NOSCRIPT/
|
36
|
+
refresh = true
|
37
|
+
retry
|
38
|
+
end
|
39
|
+
raise
|
40
|
+
end
|
41
|
+
|
42
|
+
def locked_sha(refresh = false)
|
43
|
+
@locked_sha = nil if refresh
|
44
|
+
|
45
|
+
@locked_sha ||=
|
46
|
+
Resque.redis.script(:load, <<-EOF.gsub(/^ {14}/, ''))
|
47
|
+
if redis.call('GET', KEYS[1]) == ARGV[1]
|
48
|
+
then
|
49
|
+
redis.call('EXPIRE', KEYS[1], #{timeout})
|
50
|
+
|
51
|
+
if redis.call('GET', KEYS[1]) == ARGV[1]
|
52
|
+
then
|
53
|
+
return 1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
return 0
|
58
|
+
EOF
|
59
|
+
end
|
60
|
+
|
61
|
+
def acquire_sha(refresh = false)
|
62
|
+
@acquire_sha = nil if refresh
|
63
|
+
|
64
|
+
@acquire_sha ||=
|
65
|
+
Resque.redis.script(:load, <<-EOF.gsub(/^ {14}/, ''))
|
66
|
+
if redis.call('SETNX', KEYS[1], ARGV[1]) == 1
|
67
|
+
then
|
68
|
+
redis.call('EXPIRE', KEYS[1], #{timeout})
|
69
|
+
return 1
|
70
|
+
else
|
71
|
+
return 0
|
72
|
+
end
|
73
|
+
EOF
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
# ### Locking the scheduler process
|
4
|
+
#
|
5
|
+
# There are two places in resque-scheduler that need to be synchonized in order
|
6
|
+
# to be able to run redundant scheduler processes while ensuring jobs don't get
|
7
|
+
# queued multiple times when the master process changes.
|
8
|
+
#
|
9
|
+
# 1) Processing the delayed queues (jobs that are created from
|
10
|
+
# enqueue_at/enqueue_in, etc) 2) Processing the scheduled (cron-like) jobs from
|
11
|
+
# rufus-scheduler
|
12
|
+
#
|
13
|
+
# Protecting the delayed queues (#1) is relatively easy. A simple SETNX in
|
14
|
+
# redis would suffice. However, protecting the scheduled jobs is trickier
|
15
|
+
# because the clocks on machines could be slightly off or actual firing times
|
16
|
+
# could vary slightly due to load. If scheduler A's clock is slightly ahead of
|
17
|
+
# scheduler B's clock (since they are on different machines), when scheduler A
|
18
|
+
# dies, we need to ensure that scheduler B doesn't queue jobs that A already
|
19
|
+
# queued before it's death. (This all assumes that it is better to miss a few
|
20
|
+
# scheduled jobs than it is to run them multiple times for the same iteration.)
|
21
|
+
#
|
22
|
+
# To avoid queuing multiple jobs in the case of master fail-over, the master
|
23
|
+
# should remain the master as long as it can rather than a simple SETNX which
|
24
|
+
# would result in the master roll being passed around frequently.
|
25
|
+
#
|
26
|
+
# Locking Scheme: Each resque-scheduler process attempts to get the master lock
|
27
|
+
# via SETNX. Once obtained, it sets the expiration for 3 minutes
|
28
|
+
# (configurable). The master process continually updates the timeout on the
|
29
|
+
# lock key to be 3 minutes in the future in it's loop(s) (see `run`) and when
|
30
|
+
# jobs come out of rufus-scheduler (see `load_schedule_job`). That ensures
|
31
|
+
# that a minimum of 3 minutes must pass since the last queuing operation before
|
32
|
+
# a new master is chosen. If, for whatever reason, the master fails to update
|
33
|
+
# the expiration for 3 minutes, the key expires and the lock is up for grabs.
|
34
|
+
# If miraculously the original master comes back to life, it will realize it is
|
35
|
+
# no longer the master and stop processing jobs.
|
36
|
+
#
|
37
|
+
# The clocks on the scheduler machines can then be up to 3 minutes off from
|
38
|
+
# each other without the risk of queueing the same scheduled job twice during a
|
39
|
+
# master change. The catch is, in the event of a master change, no scheduled
|
40
|
+
# jobs will be queued during those 3 minutes. So, there is a trade off: the
|
41
|
+
# higher the timeout, the less likely scheduled jobs will be fired twice but
|
42
|
+
# greater chances of missing scheduled jobs. The lower the timeout, less
|
43
|
+
# likely jobs will be missed, greater the chances of jobs firing twice. If you
|
44
|
+
# don't care about jobs firing twice or are certain your machines' clocks are
|
45
|
+
# well in sync, a lower timeout is preferable. One thing to keep in mind: this
|
46
|
+
# only effects *scheduled* jobs - delayed jobs will never be lost or skipped
|
47
|
+
# since eventually a master will come online and it will process everything
|
48
|
+
# that is ready (no matter how old it is). Scheduled jobs work like cron - if
|
49
|
+
# you stop cron, no jobs fire while it's stopped and it doesn't fire jobs that
|
50
|
+
# were missed when it starts up again.
|
51
|
+
|
52
|
+
require_relative 'lock'
|
53
|
+
|
54
|
+
module Resque
|
55
|
+
module Scheduler
|
56
|
+
module Locking
|
57
|
+
def master_lock
|
58
|
+
@master_lock ||= build_master_lock
|
59
|
+
end
|
60
|
+
|
61
|
+
def supports_lua?
|
62
|
+
redis_master_version >= 2.5
|
63
|
+
end
|
64
|
+
|
65
|
+
def master?
|
66
|
+
master_lock.acquire! || master_lock.locked?
|
67
|
+
end
|
68
|
+
|
69
|
+
def release_master_lock!
|
70
|
+
warn "#{self}\#release_master_lock! is deprecated because it does " \
|
71
|
+
"not respect lock ownership. Use #{self}\#release_master_lock " \
|
72
|
+
"instead (at #{caller.first}"
|
73
|
+
|
74
|
+
master_lock.release!
|
75
|
+
end
|
76
|
+
|
77
|
+
def release_master_lock
|
78
|
+
master_lock.release
|
79
|
+
rescue Errno::EAGAIN, Errno::ECONNRESET, Redis::CannotConnectError
|
80
|
+
@master_lock = nil
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def build_master_lock
|
86
|
+
if supports_lua?
|
87
|
+
Resque::Scheduler::Lock::Resilient.new(master_lock_key)
|
88
|
+
else
|
89
|
+
Resque::Scheduler::Lock::Basic.new(master_lock_key)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def master_lock_key
|
94
|
+
lock_prefix = ENV['RESQUE_SCHEDULER_MASTER_LOCK_PREFIX'] || ''
|
95
|
+
lock_prefix += ':' if lock_prefix != ''
|
96
|
+
"#{Resque.redis.namespace}:#{lock_prefix}resque_scheduler_master_lock"
|
97
|
+
end
|
98
|
+
|
99
|
+
def redis_master_version
|
100
|
+
Resque.redis.info['redis_version'].to_f
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
require 'mono_logger'
|
4
|
+
|
5
|
+
module Resque
|
6
|
+
module Scheduler
|
7
|
+
# Just builds a logger, with specified verbosity and destination.
|
8
|
+
# The simplest example:
|
9
|
+
#
|
10
|
+
# Resque::Scheduler::LoggerBuilder.new.build
|
11
|
+
class LoggerBuilder
|
12
|
+
# Initializes new instance of the builder
|
13
|
+
#
|
14
|
+
# Pass :opts Hash with
|
15
|
+
# - :quiet if logger needs to be silent for all levels. Default - false
|
16
|
+
# - :verbose if there is a need in debug messages. Default - false
|
17
|
+
# - :log_dev to output logs into a desired file. Default - STDOUT
|
18
|
+
# - :format log format, either 'text' or 'json'. Default - 'text'
|
19
|
+
#
|
20
|
+
# Example:
|
21
|
+
#
|
22
|
+
# LoggerBuilder.new(
|
23
|
+
# :quiet => false, :verbose => true, :log_dev => 'log/scheduler.log'
|
24
|
+
# )
|
25
|
+
def initialize(opts = {})
|
26
|
+
@quiet = !!opts[:quiet]
|
27
|
+
@verbose = !!opts[:verbose]
|
28
|
+
@log_dev = opts[:log_dev] || $stdout
|
29
|
+
@format = opts[:format] || 'text'
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns an instance of MonoLogger
|
33
|
+
def build
|
34
|
+
logger = MonoLogger.new(@log_dev)
|
35
|
+
logger.level = level
|
36
|
+
logger.formatter = send(:"#{@format}_formatter")
|
37
|
+
logger
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def level
|
43
|
+
if @verbose && !@quiet
|
44
|
+
MonoLogger::DEBUG
|
45
|
+
elsif !@quiet
|
46
|
+
MonoLogger::INFO
|
47
|
+
else
|
48
|
+
MonoLogger::FATAL
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def text_formatter
|
53
|
+
proc do |severity, datetime, _progname, msg|
|
54
|
+
"resque-scheduler: [#{severity}] #{datetime.iso8601}: #{msg}\n"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def json_formatter
|
59
|
+
proc do |severity, datetime, progname, msg|
|
60
|
+
require 'json'
|
61
|
+
JSON.dump(
|
62
|
+
name: 'resque-scheduler',
|
63
|
+
progname: progname,
|
64
|
+
level: severity,
|
65
|
+
timestamp: datetime.iso8601,
|
66
|
+
msg: msg
|
67
|
+
) + "\n"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Scheduler
|
5
|
+
module Plugin
|
6
|
+
def self.hooks(job, pattern)
|
7
|
+
job.methods.grep(/^#{pattern}/).sort
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.run_hooks(job, pattern, *args)
|
11
|
+
results = hooks(job, pattern).map do |hook|
|
12
|
+
job.send(hook, *args)
|
13
|
+
end
|
14
|
+
|
15
|
+
results.all? { |result| result != false }
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.run_before_delayed_enqueue_hooks(klass, *args)
|
19
|
+
run_hooks(klass, 'before_delayed_enqueue', *args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.run_before_schedule_hooks(klass, *args)
|
23
|
+
run_hooks(klass, 'before_schedule', *args)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.run_after_schedule_hooks(klass, *args)
|
27
|
+
run_hooks(klass, 'after_schedule', *args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Scheduler
|
5
|
+
module SchedulingExtensions
|
6
|
+
# Accepts a new schedule configuration of the form:
|
7
|
+
#
|
8
|
+
# {
|
9
|
+
# "MakeTea" => {
|
10
|
+
# "every" => "1m" },
|
11
|
+
# "some_name" => {
|
12
|
+
# "cron" => "5/* * * *",
|
13
|
+
# "class" => "DoSomeWork",
|
14
|
+
# "args" => "work on this string",
|
15
|
+
# "description" => "this thing works it"s butter off" },
|
16
|
+
# ...
|
17
|
+
# }
|
18
|
+
#
|
19
|
+
# Hash keys can be anything and are used to describe and reference
|
20
|
+
# the scheduled job. If the "class" argument is missing, the key
|
21
|
+
# is used implicitly as "class" argument - in the "MakeTea" example,
|
22
|
+
# "MakeTea" is used both as job name and resque worker class.
|
23
|
+
#
|
24
|
+
# Any jobs that were in the old schedule, but are not
|
25
|
+
# present in the new schedule, will be removed.
|
26
|
+
#
|
27
|
+
# :cron can be any cron scheduling string
|
28
|
+
#
|
29
|
+
# :every can be used in lieu of :cron. see rufus-scheduler's 'every'
|
30
|
+
# usage for valid syntax. If :cron is present it will take precedence
|
31
|
+
# over :every.
|
32
|
+
#
|
33
|
+
# :class must be a resque worker class. If it is missing, the job name
|
34
|
+
# (hash key) will be used as :class.
|
35
|
+
#
|
36
|
+
# :args can be any yaml which will be converted to a ruby literal and
|
37
|
+
# passed in a params. (optional)
|
38
|
+
#
|
39
|
+
# :rails_envs is the list of envs where the job gets loaded. Envs are
|
40
|
+
# comma separated (optional)
|
41
|
+
#
|
42
|
+
# :description is just that, a description of the job (optional). If
|
43
|
+
# params is an array, each element in the array is passed as a separate
|
44
|
+
# param, otherwise params is passed in as the only parameter to
|
45
|
+
# perform.
|
46
|
+
def schedule=(schedule_hash)
|
47
|
+
@non_persistent_schedules = nil
|
48
|
+
prepared_schedules = prepare_schedules(schedule_hash)
|
49
|
+
|
50
|
+
prepared_schedules.each do |schedule, config|
|
51
|
+
set_schedule(schedule, config, false)
|
52
|
+
end
|
53
|
+
|
54
|
+
# ensure only return the successfully saved data!
|
55
|
+
reload_schedule!
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the schedule hash
|
59
|
+
def schedule
|
60
|
+
@schedule ||= all_schedules
|
61
|
+
@schedule || {}
|
62
|
+
end
|
63
|
+
|
64
|
+
# reloads the schedule from redis and memory
|
65
|
+
def reload_schedule!
|
66
|
+
@schedule = all_schedules
|
67
|
+
end
|
68
|
+
|
69
|
+
# gets the schedules as it exists in redis
|
70
|
+
def all_schedules
|
71
|
+
non_persistent_schedules.merge(persistent_schedules)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Create or update a schedule with the provided name and configuration.
|
75
|
+
#
|
76
|
+
# Note: values for class and custom_job_class need to be strings,
|
77
|
+
# not constants.
|
78
|
+
#
|
79
|
+
# Resque.set_schedule('some_job', {:class => 'SomeJob',
|
80
|
+
# :every => '15mins',
|
81
|
+
# :queue => 'high',
|
82
|
+
# :args => '/tmp/poop'})
|
83
|
+
#
|
84
|
+
# Preventing a reload is optional and available to batch operations
|
85
|
+
def set_schedule(name, config, reload = true)
|
86
|
+
persist = config.delete(:persist) || config.delete('persist')
|
87
|
+
|
88
|
+
if persist
|
89
|
+
redis.hset(:persistent_schedules, name, encode(config))
|
90
|
+
else
|
91
|
+
non_persistent_schedules[name] = decode(encode(config))
|
92
|
+
end
|
93
|
+
|
94
|
+
redis.sadd(:schedules_changed, name)
|
95
|
+
reload_schedule! if reload
|
96
|
+
end
|
97
|
+
|
98
|
+
# retrive the schedule configuration for the given name
|
99
|
+
def fetch_schedule(name)
|
100
|
+
schedule[name]
|
101
|
+
end
|
102
|
+
|
103
|
+
# remove a given schedule by name
|
104
|
+
def remove_schedule(name)
|
105
|
+
non_persistent_schedules.delete(name)
|
106
|
+
redis.hdel(:persistent_schedules, name)
|
107
|
+
redis.sadd(:schedules_changed, name)
|
108
|
+
|
109
|
+
reload_schedule!
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
# we store our non-persistent schedules in this hash
|
115
|
+
def non_persistent_schedules
|
116
|
+
@non_persistent_schedules ||= {}
|
117
|
+
end
|
118
|
+
|
119
|
+
# reads the persistent schedules from redis
|
120
|
+
def persistent_schedules
|
121
|
+
redis.hgetall(:persistent_schedules).tap do |h|
|
122
|
+
h.each do |name, config|
|
123
|
+
h[name] = decode(config)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def prepare_schedules(schedule_hash)
|
129
|
+
prepared_hash = {}
|
130
|
+
schedule_hash.each do |name, job_spec|
|
131
|
+
job_spec = job_spec.dup
|
132
|
+
unless job_spec.key?('class') || job_spec.key?(:class)
|
133
|
+
job_spec['class'] = name
|
134
|
+
end
|
135
|
+
prepared_hash[name] = job_spec
|
136
|
+
end
|
137
|
+
prepared_hash
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
<h1>Delayed Jobs</h1>
|
2
|
+
<%- size = resque.delayed_queue_schedule_size %>
|
3
|
+
|
4
|
+
<%= scheduler_view :search_form, layout: false %>
|
5
|
+
|
6
|
+
<p style="font-color: red; font-weight: bold;">
|
7
|
+
<%= @error_message %>
|
8
|
+
</p>
|
9
|
+
|
10
|
+
<p class='intro'>
|
11
|
+
This list below contains the timestamps for scheduled delayed jobs.
|
12
|
+
Server local time: <%= Time.now %>
|
13
|
+
</p>
|
14
|
+
|
15
|
+
<p class='sub'>
|
16
|
+
Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%= size %></b> timestamps
|
17
|
+
</p>
|
18
|
+
|
19
|
+
<table>
|
20
|
+
<tr>
|
21
|
+
<th></th>
|
22
|
+
<th>Timestamp</th>
|
23
|
+
<th>Job count</th>
|
24
|
+
<th>Class</th>
|
25
|
+
<th>Args</th>
|
26
|
+
<th>All schedules</th>
|
27
|
+
</tr>
|
28
|
+
<% resque.delayed_queue_peek(start, 20).each do |timestamp| %>
|
29
|
+
<tr>
|
30
|
+
<td>
|
31
|
+
<form action="<%= u "/delayed/queue_now" %>" method="post">
|
32
|
+
<input type="hidden" name="timestamp" value="<%= timestamp.to_i %>">
|
33
|
+
<input type="submit" value="Queue now">
|
34
|
+
</form>
|
35
|
+
</td>
|
36
|
+
<td><a href="<%= u "delayed/#{timestamp}" %>"><%= format_time(Time.at(timestamp)) %></a></td>
|
37
|
+
<td><%= delayed_timestamp_size = resque.delayed_timestamp_size(timestamp) %></td>
|
38
|
+
<% job = resque.delayed_timestamp_peek(timestamp, 0, 1).first %>
|
39
|
+
<td>
|
40
|
+
<% if job && delayed_timestamp_size == 1 %>
|
41
|
+
<%= h(job['class']) %>
|
42
|
+
<% else %>
|
43
|
+
<a href="<%= u "delayed/#{timestamp}" %>">see details</a>
|
44
|
+
<% end %>
|
45
|
+
</td>
|
46
|
+
<td><%= h(show_job_arguments(job['args'])) if job && delayed_timestamp_size == 1 %></td>
|
47
|
+
<td>
|
48
|
+
<% if job %>
|
49
|
+
<a href="<%=u URI("/delayed/jobs/#{job['class']}?args=" + URI.encode(job['args'].to_json)) %>">All schedules</a>
|
50
|
+
<% end %>
|
51
|
+
</td>
|
52
|
+
</tr>
|
53
|
+
<% end %>
|
54
|
+
</table>
|
55
|
+
|
56
|
+
<% if size > 0 %>
|
57
|
+
<br>
|
58
|
+
<form method="POST" action="<%=u 'delayed/clear'%>" class='clear-delayed'>
|
59
|
+
<input type='submit' name='' value='Clear Delayed Jobs' />
|
60
|
+
</form>
|
61
|
+
<% end %>
|
62
|
+
|
63
|
+
<%= partial :next_more, :start => start, :size => size %>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<h1>Delayed jobs scheduled for <%= params[:klass] %> (<%= show_job_arguments(@args) %>)</h1>
|
2
|
+
|
3
|
+
<table class='jobs'>
|
4
|
+
<tr>
|
5
|
+
<th>Timestamp</th>
|
6
|
+
</tr>
|
7
|
+
|
8
|
+
<% @timestamps.each do |t| %>
|
9
|
+
<tr>
|
10
|
+
<td>
|
11
|
+
<%= Time.at(t) %>
|
12
|
+
</td>
|
13
|
+
</tr>
|
14
|
+
<% end %>
|
15
|
+
<% if @timestamps.empty? %>
|
16
|
+
<tr>
|
17
|
+
<td class='no-data'>There are no such jobs scheduled.</td>
|
18
|
+
</tr>
|
19
|
+
<% end %>
|
20
|
+
</table>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<% timestamp = params[:timestamp].to_i %>
|
2
|
+
|
3
|
+
<h1>Delayed jobs scheduled for <%= format_time(Time.at(timestamp)) %></h1>
|
4
|
+
|
5
|
+
<p class='sub'>Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%=size = resque.delayed_timestamp_size(timestamp)%></b> jobs</p>
|
6
|
+
|
7
|
+
<table class='jobs'>
|
8
|
+
<tr>
|
9
|
+
<th>Class</th>
|
10
|
+
<th>Args</th>
|
11
|
+
</tr>
|
12
|
+
<% jobs = resque.delayed_timestamp_peek(timestamp, start, 20) %>
|
13
|
+
<% jobs.each do |job| %>
|
14
|
+
<tr>
|
15
|
+
<td class='class'><%= job['class'] %></td>
|
16
|
+
<td class='args'><%=h show_job_arguments(job['args']) %></td>
|
17
|
+
</tr>
|
18
|
+
<% end %>
|
19
|
+
<% if jobs.empty? %>
|
20
|
+
<tr>
|
21
|
+
<td class='no-data' colspan='2'>There are no pending jobs scheduled for this time.</td>
|
22
|
+
</tr>
|
23
|
+
<% end %>
|
24
|
+
</table>
|
25
|
+
|
26
|
+
<%= partial :next_more, :start => start, :size => size %>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<h1><%= @job_name %></h1>
|
2
|
+
|
3
|
+
<p class='intro'>
|
4
|
+
This job requires parameters:
|
5
|
+
</p>
|
6
|
+
|
7
|
+
<form style="float:left" action="<%= u "/schedule/requeue_with_params" %>" method="post">
|
8
|
+
<table>
|
9
|
+
<% @parameters.each do |key, value| %>
|
10
|
+
<% value ||= {} %>
|
11
|
+
<tr>
|
12
|
+
<td>
|
13
|
+
<%= key %>
|
14
|
+
<% if value['description'] || value[:description] %>
|
15
|
+
<span style="border-bottom:1px dotted;" title="<%=value ['description'] || value[:description] %>">(?)</span>
|
16
|
+
<% end %>:
|
17
|
+
</td>
|
18
|
+
<td><input type="text" name="<%= key %>" value="<%= value['default'] || value[:default] %>"></td>
|
19
|
+
<% end %>
|
20
|
+
</table>
|
21
|
+
<input type="hidden" name="job_name" value="<%= @job_name %>">
|
22
|
+
<input type="submit" value="Queue now">
|
23
|
+
</form>
|