when-do 1.0.0
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/.gitignore +20 -0
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +77 -0
- data/Rakefile +1 -0
- data/bin/when-do +8 -0
- data/config/when.yml.example +8 -0
- data/lib/when-do/cli.rb +101 -0
- data/lib/when-do/do.rb +153 -0
- data/lib/when-do.rb +116 -0
- data/spec/lib/when-do/cli_spec.rb +84 -0
- data/spec/lib/when-do/do_spec.rb +137 -0
- data/spec/lib/when-do_spec.rb +85 -0
- data/spec/spec_helper.rb +3 -0
- data/when-do.gemspec +30 -0
- metadata +193 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b2fb127851bb13ec2d4211aeaa3f0e8c8dd8fb0e
|
4
|
+
data.tar.gz: 4bd1dab3156b0e7cc905f563594d586a1632db32
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 363a63837074ccd823fe7caaa000c56cffcebbe0ad09d584da0b05d0a42f138dead9fbc07762ee27bd9075e2b3d53ada2065386b89e3ab137c154d88c7614e8e
|
7
|
+
data.tar.gz: 7ec2a8f9e72ecf1e6a629d500d1df66950e9b10dce13cadd00d96d572a40718ffa95121fa15fcaeb6723cf057ccdaf625d294f6234d2d90d1b658c020024075d
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 TH
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# When-do
|
2
|
+
|
3
|
+
Reads schedules from Redis and moves them onto a Redis list at the correct time.
|
4
|
+
|
5
|
+
Supports
|
6
|
+
|
7
|
+
* Dynamic cron schedules
|
8
|
+
* Delayed queueing
|
9
|
+
|
10
|
+
Schedules are not cached and can be changed at will. Redundant processes can run without duplication of jobs as long as they point to the same Redis. Jobs will not be double-queued when DST resets time backwards, but will be skipped when DST moves time forward. Leap seconds should be fine.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
gem 'when-do'
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install when-do
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
From your project's main directory:
|
28
|
+
|
29
|
+
$ when-do -init
|
30
|
+
|
31
|
+
Rename the example config and tweak as needed. For example, try setting your ```:worker_queue_key: 'queues:default'``` so Sidekiq picks up your jobs. Use symbols for all keys in the config.
|
32
|
+
|
33
|
+
Then run in your console with default config:
|
34
|
+
|
35
|
+
$ when-do
|
36
|
+
|
37
|
+
Or start a daemon that uses your config file and writes a pid file.
|
38
|
+
|
39
|
+
$ when-do -d -c 'config/when.yml' -e development
|
40
|
+
|
41
|
+
I'd recommend using Monit to manage and monitor daemons. When-do is designed to let a different process handle failure notifications/restarting.
|
42
|
+
|
43
|
+
Then, from your app...
|
44
|
+
|
45
|
+
Queue a job now:
|
46
|
+
|
47
|
+
When.enqueue(WorkerClass, args, more_args, ...)
|
48
|
+
|
49
|
+
Queue a job later (minute precision):
|
50
|
+
|
51
|
+
When.enqueue_at(Time.now + 60, WorkerClass, args, more_args, ...)
|
52
|
+
|
53
|
+
Schedule a job (only numbers are supported in cron strings):
|
54
|
+
|
55
|
+
When.schedule('schedule_name', '0 * * * *', WorkerClass, args, more_args, ...)
|
56
|
+
|
57
|
+
Unschedule a job:
|
58
|
+
|
59
|
+
When.unschedule('schedule_name')
|
60
|
+
|
61
|
+
Clear schedules:
|
62
|
+
|
63
|
+
When.unschedule_all
|
64
|
+
|
65
|
+
Check schedules:
|
66
|
+
|
67
|
+
When.schedules
|
68
|
+
|
69
|
+
On my 2013 Macbook Air, 100k schedules can be analyzed in <3 seconds.
|
70
|
+
|
71
|
+
## Contributing
|
72
|
+
|
73
|
+
1. Fork it
|
74
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
75
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
76
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
77
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/when-do
ADDED
data/lib/when-do/cli.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'when-cron'
|
2
|
+
require 'json'
|
3
|
+
require 'yaml'
|
4
|
+
require 'redis'
|
5
|
+
|
6
|
+
module When
|
7
|
+
class CLI
|
8
|
+
CANONICAL_KEYS = {
|
9
|
+
:sk => :schedule_key,
|
10
|
+
:wq => :worker_queue_key,
|
11
|
+
:dq => :delayed_queue_key,
|
12
|
+
:lp => :log_path,
|
13
|
+
:ll => :log_level,
|
14
|
+
:c => :config_path,
|
15
|
+
:rc => :redis_config_path,
|
16
|
+
:d => :daemonize,
|
17
|
+
:e => :environment,
|
18
|
+
:pf => :pid_file_path
|
19
|
+
}
|
20
|
+
|
21
|
+
def init
|
22
|
+
cl_opts = canonicalize_keys(argv_to_hash)
|
23
|
+
copy_example_config if cl_opts.has_key? :init
|
24
|
+
@options = merge_redis_config_file(merge_config_file(cl_opts))
|
25
|
+
end
|
26
|
+
|
27
|
+
def options
|
28
|
+
@options ||= init
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def copy_example_config
|
34
|
+
bin_path = File.expand_path(File.dirname(__FILE__))
|
35
|
+
system('mkdir config')
|
36
|
+
if system("cp #{bin_path}/../../config/when.yml.example config/")
|
37
|
+
puts "Example config copied to /config/when.yml.example"
|
38
|
+
else
|
39
|
+
puts 'Failed to copy example config.'
|
40
|
+
end
|
41
|
+
exit
|
42
|
+
end
|
43
|
+
|
44
|
+
def argv
|
45
|
+
ARGV
|
46
|
+
end
|
47
|
+
|
48
|
+
def argv_to_hash
|
49
|
+
argv.each.with_index.inject({}) do |opts, (arg, i)|
|
50
|
+
if arg[0] == '-'
|
51
|
+
values = argv_values_for(i)
|
52
|
+
key = arg.gsub(/\A-*/, '').to_sym
|
53
|
+
if values.count <= 1
|
54
|
+
opts[key] = values.first
|
55
|
+
else
|
56
|
+
opts[key] = values
|
57
|
+
end
|
58
|
+
end
|
59
|
+
opts
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def argv_values_for(key_index)
|
64
|
+
first_val = key_index + 1
|
65
|
+
argv[first_val..-1].each_with_index do |arg, i|
|
66
|
+
return argv[first_val...first_val + i] if arg[0] == '-'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def canonicalize_keys(commandline_opts)
|
71
|
+
commandline_opts.inject({}) do |opts, (k, v)|
|
72
|
+
if new_key = CANONICAL_KEYS[k]
|
73
|
+
opts[new_key] = v
|
74
|
+
else
|
75
|
+
opts[k] = v
|
76
|
+
end
|
77
|
+
opts
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def merge_config_file(commandline_opts)
|
82
|
+
config_path = commandline_opts[:config_path]
|
83
|
+
file_config = load_opts(config_path)
|
84
|
+
env = commandline_opts[:environment] || :development
|
85
|
+
file_opts = file_config[env.to_sym] || {}
|
86
|
+
file_opts.merge(commandline_opts)
|
87
|
+
end
|
88
|
+
|
89
|
+
def merge_redis_config_file(merged_opts)
|
90
|
+
merged_opts.merge(redis_opts: load_opts(merged_opts[:redis_config_path]))
|
91
|
+
end
|
92
|
+
|
93
|
+
def load_opts(path)
|
94
|
+
if path
|
95
|
+
YAML.load(File.read(path))
|
96
|
+
else
|
97
|
+
{}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/when-do/do.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'when-cron'
|
2
|
+
require 'json'
|
3
|
+
require 'redis'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module When
|
7
|
+
class Do
|
8
|
+
attr_reader :schedule_key, :worker_queue_key, :delayed_queue_key, :redis, :logger, :pid_file_path
|
9
|
+
|
10
|
+
def initialize(opts={})
|
11
|
+
@logger = init_logger(opts[:log_path], opts[:log_level])
|
12
|
+
|
13
|
+
Process.daemon(true) if opts.has_key?(:daemonize)
|
14
|
+
|
15
|
+
@pid_file_path = opts[:pid_file_path]
|
16
|
+
if pid_file_path
|
17
|
+
File.open(pid_file_path, 'w') { |f| f.write(Process.pid) }
|
18
|
+
end
|
19
|
+
|
20
|
+
redis_opts = opts[:redis_opts] || {}
|
21
|
+
@redis = Redis.new(redis_opts)
|
22
|
+
|
23
|
+
@schedule_key = opts[:schedule_key] || 'when:schedules'
|
24
|
+
@worker_queue_key = opts[:worker_queue_key] || 'when:queue:default'
|
25
|
+
@delayed_queue_key = opts[:delayed_queue_key] || 'when:delayed'
|
26
|
+
end
|
27
|
+
|
28
|
+
def start_loop
|
29
|
+
logger.info("Starting...")
|
30
|
+
logger.info { "Schedule key: '#{schedule_key}', worker queue key: '#{worker_queue_key}', delayed queue key: '#{delayed_queue_key}'" }
|
31
|
+
logger.info { "PID file: #{pid_file_path}" } if pid_file_path
|
32
|
+
|
33
|
+
loop do
|
34
|
+
sleep_until_next_minute
|
35
|
+
logger.debug { "Using #{`ps -o rss -p #{Process.pid}`.chomp.split("\n").last.to_i} kb of memory." }
|
36
|
+
analyze_in_child_process
|
37
|
+
end
|
38
|
+
|
39
|
+
rescue SystemExit => e
|
40
|
+
raise e
|
41
|
+
|
42
|
+
rescue SignalException => e
|
43
|
+
logger.info(e.inspect)
|
44
|
+
File.delete(pid_file_path) if pid_file_path
|
45
|
+
raise e
|
46
|
+
|
47
|
+
rescue Exception => e
|
48
|
+
([e.inspect] + e.backtrace).each { |line| logger.fatal(line) }
|
49
|
+
raise e
|
50
|
+
end
|
51
|
+
|
52
|
+
def analyze_in_child_process
|
53
|
+
if pid = fork
|
54
|
+
Thread.new {
|
55
|
+
pid, status = Process.wait2(pid)
|
56
|
+
if status.exitstatus != 0
|
57
|
+
raise "Child (pid: #{pid} exited with non-zero status. Check logs."
|
58
|
+
end
|
59
|
+
}.abort_on_exception = true
|
60
|
+
else
|
61
|
+
analyze
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def analyze
|
66
|
+
['HUP', 'INT', 'TERM'].each { |sig| Signal.trap(sig) { }}
|
67
|
+
started_at = Time.now
|
68
|
+
if running?(started_at)
|
69
|
+
logger.info('Another process is already analyzing.')
|
70
|
+
else
|
71
|
+
logger.debug { 'Analyzing.' }
|
72
|
+
queue_scheduled(started_at)
|
73
|
+
queue_delayed(started_at)
|
74
|
+
end
|
75
|
+
exit
|
76
|
+
end
|
77
|
+
|
78
|
+
def running?(started_at)
|
79
|
+
day_key = build_day_key(started_at)
|
80
|
+
min_key = build_min_key(started_at)
|
81
|
+
|
82
|
+
logger.debug { "Checking Redis using day_key: '#{day_key}' and min_key: '#{min_key}'"}
|
83
|
+
check_and_set_analyzed = redis.multi do
|
84
|
+
redis.hget(day_key, min_key)
|
85
|
+
redis.hset(day_key, min_key, 't')
|
86
|
+
redis.expire(day_key, 60 * 60 * 24)
|
87
|
+
end
|
88
|
+
|
89
|
+
check_and_set_analyzed[0]
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_day_key(started_at)
|
93
|
+
"#{schedule_key}:#{started_at.to_s.split(' ')[0]}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_min_key(started_at)
|
97
|
+
"#{started_at.hour}:#{started_at.min}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def queue_scheduled(started_at)
|
101
|
+
schedules = redis.hvals(schedule_key)
|
102
|
+
logger.info("Analyzing #{schedules.count} schedules.")
|
103
|
+
scheduled_jobs = schedules.inject([]) do |jobs, s|
|
104
|
+
schedule = JSON.parse(s)
|
105
|
+
cron = When::Cron.new(schedule['cron'])
|
106
|
+
if cron == started_at
|
107
|
+
jobs << { 'jid' => SecureRandom.uuid, 'class' => schedule['class'], 'args' => schedule['args'] }.to_json
|
108
|
+
end
|
109
|
+
jobs
|
110
|
+
end
|
111
|
+
logger.debug { "Found #{scheduled_jobs.count} schedules due to be queued." }
|
112
|
+
enqueue(scheduled_jobs) if scheduled_jobs.any?
|
113
|
+
end
|
114
|
+
|
115
|
+
def queue_delayed(started_at)
|
116
|
+
logger.info("Checking for delayed jobs.")
|
117
|
+
delayed_jobs = redis.multi do
|
118
|
+
redis.zrevrangebyscore(delayed_queue_key, started_at.to_i, '-inf')
|
119
|
+
redis.zremrangebyscore(delayed_queue_key, '-inf', started_at.to_i)
|
120
|
+
end[0]
|
121
|
+
logger.debug { "Found #{delayed_jobs.count} delayed jobs." }
|
122
|
+
enqueue(delayed_jobs) if delayed_jobs.any?
|
123
|
+
end
|
124
|
+
|
125
|
+
def enqueue(jobs)
|
126
|
+
jobs.each do |job|
|
127
|
+
logger.info("Queueing: #{job}")
|
128
|
+
end
|
129
|
+
success = redis.lpush(worker_queue_key, jobs)
|
130
|
+
unless success > 0
|
131
|
+
raise "Failed to queue all jobs. Redis returned #{success}."
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def sleep_until_next_minute
|
138
|
+
to_sleep = 62 - Time.now.sec #handle up to 2 leap seconds
|
139
|
+
logger.debug { "Sleeping #{to_sleep} seconds."}
|
140
|
+
sleep(to_sleep)
|
141
|
+
end
|
142
|
+
|
143
|
+
def init_logger(log_path, log_level)
|
144
|
+
logger = if log_path
|
145
|
+
Logger.new(log_path, 100, 10_240_000)
|
146
|
+
else
|
147
|
+
Logger.new(STDOUT)
|
148
|
+
end
|
149
|
+
logger.level = log_level.to_i || 1
|
150
|
+
logger
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/lib/when-do.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'json'
|
3
|
+
require 'yaml'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module When
|
7
|
+
DEFAULT_CONFIG = {
|
8
|
+
schedule_key: 'when:schedules',
|
9
|
+
worker_queue_key: 'when:queue:default',
|
10
|
+
delayed_queue_key: 'when:delayed'
|
11
|
+
}
|
12
|
+
|
13
|
+
def self.schedule(name, cron, klass, *args)
|
14
|
+
schedule = {'class' => klass.to_s, 'cron' => cron, 'args' => args}
|
15
|
+
if redis.hset(schedule_key, name.to_s, schedule.to_json)
|
16
|
+
logger.info("Scheduled '#{name}' => #{schedule}.")
|
17
|
+
else
|
18
|
+
msg = "Could not schedule '#{name}' => #{schedule}."
|
19
|
+
logger.fatal(msg)
|
20
|
+
raise msg
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.unschedule(name)
|
25
|
+
json_sched = redis.hget(schedule_key, name.to_s)
|
26
|
+
schedule = JSON.parse(json_sched) if json_sched
|
27
|
+
if redis.hdel(schedule_key, name.to_s) > 0
|
28
|
+
logger.info("Unscheduled '#{name}' => #{schedule}.")
|
29
|
+
true
|
30
|
+
else
|
31
|
+
logger.warn("Could not unschedule '#{name}'. No schedule by that name was found.")
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.unschedule_all
|
37
|
+
count = redis.del(schedule_key)
|
38
|
+
logger.info("Cleared #{count} schedules.")
|
39
|
+
count
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.schedules
|
43
|
+
schedules = redis.hgetall(schedule_key)
|
44
|
+
schedules.each { |k, v| schedules[k] = JSON.parse(v) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.enqueue_at(time, klass, *args)
|
48
|
+
job = { 'jid' => SecureRandom.uuid, 'class' => klass, 'args' => args }
|
49
|
+
if redis.zadd(delayed_queue_key, time.to_i, job.to_json)
|
50
|
+
logger.info("Delayed: will enqueue #{job} to run at #{time}.")
|
51
|
+
job['jid']
|
52
|
+
else
|
53
|
+
msg = "Failed to enqueue #{job} to run at #{time}."
|
54
|
+
logger.fatal(msg)
|
55
|
+
raise msg
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.enqueue(klass, *args)
|
60
|
+
job = { 'jid' => SecureRandom.uuid, 'class' => klass, 'args' => args }
|
61
|
+
if redis.lpush(worker_queue_key, job.to_json) > 0
|
62
|
+
job['jid']
|
63
|
+
else
|
64
|
+
msg = "Failed to enqueue #{job}."
|
65
|
+
logger.fatal(msg)
|
66
|
+
raise msg
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.redis
|
71
|
+
@redis ||= if config[:redis_config_path]
|
72
|
+
Redis.new(YAML.load(File.read(config[:redis_config_path])))
|
73
|
+
else
|
74
|
+
Redis.new
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.redis=(redis)
|
79
|
+
logger.info("Resetting redis to #{redis.inspect}")
|
80
|
+
@redis = redis
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.config
|
84
|
+
@config ||= DEFAULT_CONFIG
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.config=(new_config)
|
88
|
+
logger.info("Setting config to #{new_config.inspect}")
|
89
|
+
@config = new_config
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.logger
|
93
|
+
@logger ||= if config[:log_path]
|
94
|
+
Logger.new(config[:log_path], 100, 10_240_000)
|
95
|
+
else
|
96
|
+
Logger.new(STDOUT)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.logger=(new_logger)
|
101
|
+
logger.info("Changing logger to #{new_logger.inspect}")
|
102
|
+
@logger = new_logger
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.schedule_key
|
106
|
+
config[:schedule_key] || 'when:schedules'
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.delayed_queue_key
|
110
|
+
config[:delayed_queue_key] || 'when:delayed'
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.worker_queue_key
|
114
|
+
config[:worker_queue_key] || 'when:queue:default'
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'when-do/cli'
|
3
|
+
|
4
|
+
describe When::CLI do
|
5
|
+
let(:cli) { When::CLI.new }
|
6
|
+
describe '#options' do
|
7
|
+
context 'argv has an option with no following value' do
|
8
|
+
before do
|
9
|
+
cli.stub(:argv).and_return(['-option'])
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'puts { :option => nil } into the options hash' do
|
13
|
+
expect(cli.options[:option]).to eq nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'argv has an option with one following value' do
|
18
|
+
before do
|
19
|
+
cli.stub(:argv).and_return(['-option', 'value'])
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'puts { :option => "value" } into the options hash' do
|
23
|
+
expect(cli.options[:option]).to eq('value')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'argv has an option with several following values' do
|
28
|
+
before do
|
29
|
+
cli.stub(:argv).and_return(['-option', 'value1', 'value2', 'value3'])
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'puts { :option => ["value1", "value2", "value3"] } into the options hash' do
|
33
|
+
expect(cli.options[:option]).to eq(['value1', 'value2', 'value3'])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'argv has complex options and values with several following values' do
|
38
|
+
before do
|
39
|
+
cli.stub(:argv).and_return(['-option1', '-option2', 'value2-1', '-option3', 'value3-1', 'value3-2', 'value3-3'])
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'builds the options hash' do
|
43
|
+
expect(cli.options[:option1]).to eq(nil)
|
44
|
+
expect(cli.options[:option2]).to eq('value2-1')
|
45
|
+
expect(cli.options[:option3]).to eq(['value3-1', 'value3-2', 'value3-3'])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'argv includes a path to a config file' do
|
50
|
+
context 'but no additional command line options' do
|
51
|
+
before do
|
52
|
+
cli.stub(:argv).and_return(['-c', 'a file path', '-e', 'test'])
|
53
|
+
File.stub(:read).with('a file path').and_return({test: {yaml: 'config'}}.to_yaml)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'adds options from the config file' do
|
57
|
+
expect(cli.options[:yaml]).to eq('config')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'and additional command line options' do
|
62
|
+
before do
|
63
|
+
cli.stub(:argv).and_return(['-c', 'a file path', '-override', 'overridden'])
|
64
|
+
File.stub(:read).with('a file path').and_return({override: 'not overridden'}.to_yaml)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'command line arguments override options from the config file' do
|
68
|
+
expect(cli.options[:override]).to eq('overridden')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'argv includes a path to a redis config file' do
|
74
|
+
before do
|
75
|
+
cli.stub(:argv).and_return(['-rc', 'a file path'])
|
76
|
+
File.stub(:read).with('a file path').and_return({redis_opt: 'redis opt'}.to_yaml)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'adds options from the redis config file under :redis_opts' do
|
80
|
+
expect(cli.options[:redis_opts][:redis_opt]).to eq('redis opt')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'when-do/do'
|
3
|
+
require 'when-do'
|
4
|
+
|
5
|
+
describe When::Do do
|
6
|
+
let(:test_redis_index) { 11 }
|
7
|
+
let(:redis) { Redis.new(db: test_redis_index) }
|
8
|
+
let(:when_do) { When::Do.new(redis_opts: {db: test_redis_index}) }
|
9
|
+
let(:started_at) { Time.now }
|
10
|
+
|
11
|
+
before do
|
12
|
+
When.logger.level = 5
|
13
|
+
when_do.logger.level = 5
|
14
|
+
When.redis = redis
|
15
|
+
redis.flushall
|
16
|
+
end
|
17
|
+
|
18
|
+
after do
|
19
|
+
redis.flushall
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#running?' do
|
23
|
+
let(:day_key) { when_do.build_day_key(started_at) }
|
24
|
+
let(:min_key) { when_do.build_min_key(started_at) }
|
25
|
+
|
26
|
+
context 'the corresponding day hash and minute key exist in redis' do
|
27
|
+
before do
|
28
|
+
redis.hset(day_key, min_key, 't')
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'returns true' do
|
32
|
+
expect(when_do.running?(started_at)).to be_true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'the corresponding day hash and minute key do not exist in redis' do
|
37
|
+
it 'sets the day hash and minute key' do
|
38
|
+
expect { when_do.running?(started_at) }.to change { redis.hget(day_key, min_key) }.from(nil).to be_true
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'returns false' do
|
42
|
+
expect(when_do.running?(started_at)).to be_false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#queue_scheduled' do
|
48
|
+
context 'a scheduled item has a matching cron' do
|
49
|
+
let(:args) { ['arg1', 'arg2', 3, { 'more' => 'args' }] }
|
50
|
+
let(:klass) { String }
|
51
|
+
before do
|
52
|
+
When.schedule('test schedule', '* * * * *', klass, *args)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'drops a job onto the queue' do
|
56
|
+
expect { when_do.queue_scheduled(started_at) }
|
57
|
+
.to change { redis.lpop(when_do.worker_queue_key) }
|
58
|
+
.from(nil)
|
59
|
+
.to be_true
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'includes the correct arguments' do
|
63
|
+
when_do.queue_scheduled(started_at)
|
64
|
+
job = JSON.parse(redis.lpop(when_do.worker_queue_key))
|
65
|
+
expect(job['args']).to eq args
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'includes the correct class' do
|
69
|
+
when_do.queue_scheduled(started_at)
|
70
|
+
job = JSON.parse(redis.lpop(when_do.worker_queue_key))
|
71
|
+
expect(job['class']).to eq klass.name
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'a scheduled item does not have a matching cron' do
|
76
|
+
before do
|
77
|
+
When.schedule('test schedule', String, '0 0 0 0 0')
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'does not add an item to the queue' do
|
81
|
+
expect { when_do.queue_scheduled(started_at) }
|
82
|
+
.not_to change { redis.lpop(when_do.worker_queue_key) }
|
83
|
+
.from(nil)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe '#queue_delayed' do
|
89
|
+
context 'a delayed item is due to be queue' do
|
90
|
+
let(:args) { ['arg1', 'arg2', 3, { 'more' => 'args' }] }
|
91
|
+
let(:klass) { String }
|
92
|
+
before do
|
93
|
+
When.enqueue_at(started_at - 1, String, *args)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'drops a job onto the queue' do
|
97
|
+
expect { when_do.queue_delayed(started_at) }
|
98
|
+
.to change { redis.lpop(when_do.worker_queue_key) }
|
99
|
+
.from(nil)
|
100
|
+
.to be_true
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'includes the correct arguments' do
|
104
|
+
when_do.queue_delayed(started_at)
|
105
|
+
job = JSON.parse(redis.lpop(when_do.worker_queue_key))
|
106
|
+
expect(job['args']).to eq args
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'includes the correct class' do
|
110
|
+
when_do.queue_delayed(started_at)
|
111
|
+
job = JSON.parse(redis.lpop(when_do.worker_queue_key))
|
112
|
+
expect(job['class']).to eq klass.name
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'a delayed item is not yet due to be queued' do
|
117
|
+
before do
|
118
|
+
When.enqueue_at(started_at + 1, String)
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'does not add an item to the queue' do
|
122
|
+
expect { when_do.queue_delayed(started_at) }
|
123
|
+
.not_to change { redis.lpop(when_do.worker_queue_key) }
|
124
|
+
.from(nil)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe '#enqueue' do
|
130
|
+
it 'places jobs onto the work queue' do
|
131
|
+
expect { when_do.enqueue(['junk', 'jobs']) }
|
132
|
+
.to change { redis.llen(when_do.worker_queue_key) }
|
133
|
+
.from(0)
|
134
|
+
.to(2)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'when-do'
|
3
|
+
|
4
|
+
describe When do
|
5
|
+
let(:redis) { When.redis }
|
6
|
+
|
7
|
+
before do
|
8
|
+
When.logger.level = 5
|
9
|
+
When.redis = Redis.new(db: 11)
|
10
|
+
redis.flushall
|
11
|
+
end
|
12
|
+
|
13
|
+
after do
|
14
|
+
redis.flushall
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#schedule' do
|
18
|
+
it 'adds data to the schedules hash in redis' do
|
19
|
+
When.schedule('test_schedule', '* * * * *', Object, 'arg1', 'arg2')
|
20
|
+
expect(redis.hget(When.schedule_key, 'test_schedule')).to eq "{\"class\":\"Object\",\"cron\":\"* * * * *\",\"args\":[\"arg1\",\"arg2\"]}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#unschedule' do
|
25
|
+
it 'removes data from the schedules hash in redis' do
|
26
|
+
When.schedule('test_schedule', '* * * * *', Object)
|
27
|
+
expect(redis.hget(When.schedule_key, 'test_schedule')).to eq "{\"class\":\"Object\",\"cron\":\"* * * * *\",\"args\":[]}"
|
28
|
+
When.unschedule('test_schedule')
|
29
|
+
expect(redis.hget(When.schedule_key, 'test_schedule')).to be_nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#enqueue_at' do
|
34
|
+
let(:now) { Time.now }
|
35
|
+
let(:args) { ['arg1', 'arg2', 3, {'more' => 'args'} ] }
|
36
|
+
let(:klass) { String }
|
37
|
+
|
38
|
+
it 'adds an item to the delayed list' do
|
39
|
+
expect { When.enqueue_at(now, String) }
|
40
|
+
.to change { redis.zrange(When.delayed_queue_key, 0, -1).count }
|
41
|
+
.from(0).to(1)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'adds the correct score' do
|
45
|
+
When.enqueue_at(now, String)
|
46
|
+
score = redis.zrange(When.delayed_queue_key, 0, -1, with_scores: true).first.last
|
47
|
+
expect(score).to eq now.to_i.to_f
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'adds the correct args' do
|
51
|
+
When.enqueue_at(now, String, *args)
|
52
|
+
new_args = JSON.parse(redis.zrange(When.delayed_queue_key, 0, -1).first)['args']
|
53
|
+
expect(new_args).to eq args
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'adds the correct class' do
|
57
|
+
When.enqueue_at(now, klass, *args)
|
58
|
+
new_args = JSON.parse(redis.zrange(When.delayed_queue_key, 0, -1).first)['class']
|
59
|
+
expect(new_args).to eq klass.name
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#enqueue' do
|
64
|
+
let(:args) { ['arg1', 'arg2', 3, {'more' => 'args'} ] }
|
65
|
+
let(:klass) { String }
|
66
|
+
|
67
|
+
it 'adds an item to the worker queue' do
|
68
|
+
expect { When.enqueue(klass) }
|
69
|
+
.to change { redis.llen(When.worker_queue_key) }
|
70
|
+
.from(0).to(1)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'adds the correct args' do
|
74
|
+
When.enqueue(klass, *args)
|
75
|
+
enqueued_args = JSON.parse(redis.rpop(When.worker_queue_key))['args']
|
76
|
+
expect(enqueued_args).to eq args
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'adds the correct class' do
|
80
|
+
When.enqueue(klass)
|
81
|
+
enqueued_class = JSON.parse(redis.rpop(When.worker_queue_key))['class']
|
82
|
+
expect(enqueued_class).to eq klass.name
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/when-do.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "when-do"
|
7
|
+
spec.version = '1.0.0'
|
8
|
+
spec.authors = ["TH"]
|
9
|
+
spec.email = ["tylerhartland7@gmail.com"]
|
10
|
+
spec.description = %q{Queues jobs when you want.}
|
11
|
+
spec.summary = %q{Queues jobs when you want.}
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency 'when-cron'
|
21
|
+
spec.add_runtime_dependency 'redis'
|
22
|
+
spec.add_runtime_dependency 'json'
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "guard"
|
27
|
+
spec.add_development_dependency "rspec"
|
28
|
+
spec.add_development_dependency "guard-rspec"
|
29
|
+
spec.add_development_dependency "pry"
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: when-do
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- TH
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: when-cron
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: redis
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: json
|
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: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: guard
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: guard-rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: Queues jobs when you want.
|
140
|
+
email:
|
141
|
+
- tylerhartland7@gmail.com
|
142
|
+
executables:
|
143
|
+
- when-do
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- ".gitignore"
|
148
|
+
- ".rspec"
|
149
|
+
- Gemfile
|
150
|
+
- Guardfile
|
151
|
+
- LICENSE.txt
|
152
|
+
- README.md
|
153
|
+
- Rakefile
|
154
|
+
- bin/when-do
|
155
|
+
- config/when.yml.example
|
156
|
+
- lib/when-do.rb
|
157
|
+
- lib/when-do/cli.rb
|
158
|
+
- lib/when-do/do.rb
|
159
|
+
- spec/lib/when-do/cli_spec.rb
|
160
|
+
- spec/lib/when-do/do_spec.rb
|
161
|
+
- spec/lib/when-do_spec.rb
|
162
|
+
- spec/spec_helper.rb
|
163
|
+
- when-do.gemspec
|
164
|
+
homepage: ''
|
165
|
+
licenses:
|
166
|
+
- MIT
|
167
|
+
metadata: {}
|
168
|
+
post_install_message:
|
169
|
+
rdoc_options: []
|
170
|
+
require_paths:
|
171
|
+
- lib
|
172
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
173
|
+
requirements:
|
174
|
+
- - ">="
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: '0'
|
177
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
requirements: []
|
183
|
+
rubyforge_project:
|
184
|
+
rubygems_version: 2.1.11
|
185
|
+
signing_key:
|
186
|
+
specification_version: 4
|
187
|
+
summary: Queues jobs when you want.
|
188
|
+
test_files:
|
189
|
+
- spec/lib/when-do/cli_spec.rb
|
190
|
+
- spec/lib/when-do/do_spec.rb
|
191
|
+
- spec/lib/when-do_spec.rb
|
192
|
+
- spec/spec_helper.rb
|
193
|
+
has_rdoc:
|