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 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
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ logs
19
+ log
20
+ config/*.yml
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+ # Specify your gem's dependencies in when.gemspec
3
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :rspec, all_after_pass: true, all_on_start: true do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/when-do/cli'
4
+ require_relative '../lib/when-do/do'
5
+
6
+ cli = When::CLI.new
7
+ cli.init
8
+ When::Do.new(cli.options).start_loop
@@ -0,0 +1,8 @@
1
+ :development:
2
+ :schedule_key: 'when:schedules'
3
+ :worker_queue_key: 'when:queue:default'
4
+ :delayed_queue_key: 'when:delayed'
5
+ :log_path: 'log/when-do.log'
6
+ :log_level: 1
7
+ # :pid_file_path: 'tmp/pids/when-do.pid'
8
+ # :redis_config_path: 'config/redis.yml'
@@ -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
@@ -0,0 +1,3 @@
1
+ RSpec.configure do |config|
2
+ config.order = 'random'
3
+ end
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: