CronR 0.1.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/lib/CronR.rb +11 -0
- data/lib/CronR/Cron.rb +198 -0
- data/lib/CronR/CronJob.rb +140 -0
- data/lib/CronR/utils.rb +124 -0
- metadata +47 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c9bd8a1c64b404ecffe432983d48a7d168e6c539
|
4
|
+
data.tar.gz: 8b153052e7e12c4cf79382731ec0fe4944694f45
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bd1eb218a75187eef9d1a1f11e1049a541fd9f40ab136cd1ad4cc55998ea40d75f83256705af845d3ddd780ed8ab8b769352e925934e439553958eb734b02128
|
7
|
+
data.tar.gz: 121c5104873409d3cc58551ecc84926271176a5b26cf2d796161a04efa377befe5ead289279bb88d102e944348cf25881e0560475a2e512fa5e793d1dd6b70a1
|
data/lib/CronR.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# The files in this directory are part of CronR, a ruby library.
|
2
|
+
# Copyright (C) 2014 Daniel Bush
|
3
|
+
# This program is distributed under the terms of the MIT License. A
|
4
|
+
# copy of the license should be enclosed with this project in the file
|
5
|
+
# LICENSE.txt.
|
6
|
+
|
7
|
+
module CronR
|
8
|
+
require_relative 'CronR/utils'
|
9
|
+
require_relative 'CronR/Cron'
|
10
|
+
require_relative 'CronR/CronJob'
|
11
|
+
end
|
data/lib/CronR/Cron.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
# The files in this directory are part of CronR, a ruby library.
|
2
|
+
# Copyright (C) 2014 Daniel Bush
|
3
|
+
# This program is distributed under the terms of the MIT License. A
|
4
|
+
# copy of the license should be enclosed with this project in the file
|
5
|
+
# LICENSE.txt.
|
6
|
+
|
7
|
+
require_relative 'utils'
|
8
|
+
|
9
|
+
module CronR
|
10
|
+
|
11
|
+
# This is an array that is modified to represent a cron table along
|
12
|
+
# with the necessary machinery to run it.
|
13
|
+
#
|
14
|
+
# cron = Cron.new
|
15
|
+
# # Or
|
16
|
+
# cron = Cron.new(job1,job2,...)
|
17
|
+
#
|
18
|
+
# To start cron:
|
19
|
+
#
|
20
|
+
# cron.start
|
21
|
+
#
|
22
|
+
# Jobs are instances of CronJob (although these are just glorified
|
23
|
+
# hashes).
|
24
|
+
# In fact, Cron expects job instances to respond to:
|
25
|
+
#
|
26
|
+
# runnable?(time) => if we can run now
|
27
|
+
# once? => this job is a one-off
|
28
|
+
#
|
29
|
+
# @see CronJob
|
30
|
+
#
|
31
|
+
# Cron uses ruby's Time object to establish what the curren time is.
|
32
|
+
#
|
33
|
+
# cron.time => current time
|
34
|
+
#
|
35
|
+
# You can however override cron's idea of the current time.
|
36
|
+
# eg using ActiveSupport time zones:
|
37
|
+
#
|
38
|
+
# cron.time {
|
39
|
+
# Time.use_zone("Sydney") {
|
40
|
+
# Time.zone.now
|
41
|
+
# }
|
42
|
+
# }
|
43
|
+
#
|
44
|
+
# cron.time => <the time in new timezone>
|
45
|
+
#
|
46
|
+
# To alter cron after it's started (remember, it's just an array):
|
47
|
+
#
|
48
|
+
# cron.suspend {|arr|
|
49
|
+
# ... do something with cron ...
|
50
|
+
# ... arr == this cron instance ...
|
51
|
+
# arr << cron_job # for example
|
52
|
+
# }
|
53
|
+
#
|
54
|
+
# #suspend will wait for the thread dispatching jobs to be safely
|
55
|
+
# sleeping.
|
56
|
+
#
|
57
|
+
# To gracefully stop cron:
|
58
|
+
#
|
59
|
+
# cron.stop {
|
60
|
+
# ... do something now we've stopped ...
|
61
|
+
# }
|
62
|
+
#
|
63
|
+
#
|
64
|
+
# RUNNABLE ITEMS
|
65
|
+
#
|
66
|
+
# If a runnable item carries a proc, we don't run it - the thread
|
67
|
+
# used in Cron is just for doing cron.
|
68
|
+
# So runnable items are basically enqueued to @schedule.
|
69
|
+
# @schedule is just a queue.
|
70
|
+
# You can create an thread and have it dequeue this queue.
|
71
|
+
# You can also replace queue with something. It should respond
|
72
|
+
# to #enq and #deq and be thread-safe. One example might be
|
73
|
+
# to have #enq insert a record into a table.
|
74
|
+
|
75
|
+
class Cron < Array
|
76
|
+
|
77
|
+
attr_reader :thread,:mutex,:stopped,:suspended
|
78
|
+
attr_accessor :debug,:queue
|
79
|
+
|
80
|
+
# *items should consist of 0 or more items of form:
|
81
|
+
# [job_id(string),CronJob.new(...),thing]
|
82
|
+
|
83
|
+
def initialize *jobs
|
84
|
+
super()
|
85
|
+
@stopped = true
|
86
|
+
@suspended = false
|
87
|
+
@queue = Queue.new
|
88
|
+
jobs.each{|job|
|
89
|
+
self.push(job)
|
90
|
+
}
|
91
|
+
@mutex = Mutex.new
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get current time.
|
95
|
+
#
|
96
|
+
# If passed a block, the block will be used to get the time.
|
97
|
+
|
98
|
+
def time &block
|
99
|
+
if block_given? then
|
100
|
+
@time = block
|
101
|
+
else
|
102
|
+
@time ||= lambda{Time.now}
|
103
|
+
@time.call
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Check each item in this array, if runnable push it on a queue.
|
108
|
+
#
|
109
|
+
# We assume that this item is or simlar to CronJob. We don't call
|
110
|
+
# cron_job#job. That is the work for another thread esp. if #job
|
111
|
+
# is a Proc.
|
112
|
+
|
113
|
+
def run time=nil
|
114
|
+
|
115
|
+
puts "[cron] run called #{Time.now}" if @debug
|
116
|
+
time = self.time if time.nil?
|
117
|
+
self.each{|cron_job|
|
118
|
+
ok,details = cron_job.runnable?(time)
|
119
|
+
if ok then
|
120
|
+
@queue.enq(cron_job)
|
121
|
+
if cron_job.once? then
|
122
|
+
self.delete(cron_job)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
}
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
# Start cron.
|
130
|
+
#
|
131
|
+
# Will wake up every minute and perform #run.
|
132
|
+
|
133
|
+
def start debug=false,method=:every_minute,*args
|
134
|
+
@stopped = false
|
135
|
+
@suspended = false
|
136
|
+
@dead = Queue.new
|
137
|
+
@thread = CronR::Utils.send(method,debug,*args) {
|
138
|
+
time = self.time
|
139
|
+
@mutex.synchronize {
|
140
|
+
if @stopped then
|
141
|
+
# It's important we put something on this queue ONLY AFTER
|
142
|
+
# we've acquired the mutex...
|
143
|
+
@dead.enq(true)
|
144
|
+
true
|
145
|
+
elsif @suspended then
|
146
|
+
else
|
147
|
+
self.run(time)
|
148
|
+
end
|
149
|
+
}
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
# Suspend the thread.
|
154
|
+
#
|
155
|
+
# If block is given, it will only be called once the thread is
|
156
|
+
# sleeping again.
|
157
|
+
# You can use this to safely modify this array.
|
158
|
+
|
159
|
+
def suspend &block
|
160
|
+
if block_given? then
|
161
|
+
@mutex.synchronize {
|
162
|
+
begin
|
163
|
+
@suspended = true
|
164
|
+
block.call(self)
|
165
|
+
ensure
|
166
|
+
@suspended = false
|
167
|
+
end
|
168
|
+
}
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Gracefully stop the thread.
|
173
|
+
#
|
174
|
+
# If block is given, it will be called once the thread has
|
175
|
+
# stopped.
|
176
|
+
|
177
|
+
def stop &block
|
178
|
+
if block_given? then
|
179
|
+
@stopped = true
|
180
|
+
@suspended = false
|
181
|
+
# Wait till something is put on the dead queue...
|
182
|
+
# This stops us from acquiring the mutex until after @thread
|
183
|
+
# has processed @stopped set to true.
|
184
|
+
sig = @dead.deq
|
185
|
+
# The cron thread should be dead now, or wrapping up (with the
|
186
|
+
# acquired mutex)...
|
187
|
+
@mutex.synchronize {
|
188
|
+
while @thread.alive?
|
189
|
+
sleep 0.2
|
190
|
+
end
|
191
|
+
block.call(self)
|
192
|
+
}
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# The files in this directory are part of CronR, a ruby library.
|
2
|
+
# Copyright (C) 2014 Daniel Bush
|
3
|
+
# This program is distributed under the terms of the MIT License. A
|
4
|
+
# copy of the license should be enclosed with this project in the file
|
5
|
+
# LICENSE.txt.
|
6
|
+
|
7
|
+
module CronR
|
8
|
+
|
9
|
+
# An instance (cp) of this class (a subclass of hash) represents the
|
10
|
+
# standard cron parameters along with an id and something
|
11
|
+
# representing the job (eg a lambda).
|
12
|
+
#
|
13
|
+
# cp = CronJob.new('id1')
|
14
|
+
# => '* * * * *' cron job id1 with no job
|
15
|
+
# cp = CronJob.new('id2') {... a proc...}
|
16
|
+
# => '* * * * *' cron job id1 with a proc
|
17
|
+
# => #job => will 'call' the proc
|
18
|
+
# cp = CronJob.new('id2')
|
19
|
+
# cp.job = {...} => a non-proc job
|
20
|
+
#
|
21
|
+
# CRON PARAMETERS:
|
22
|
+
#
|
23
|
+
# cp[s] where s can be :minute,:hour,:day,:dow,:month .
|
24
|
+
#
|
25
|
+
# cp[s] = true
|
26
|
+
# ie '*' in a crontab
|
27
|
+
# cp[s] = i (i.kind_of?(Fixnum))
|
28
|
+
# => for minutes we have 0-5, hours 0-23, etc
|
29
|
+
# cp[s] = [i,...] (array)
|
30
|
+
# => cp[:minute] =
|
31
|
+
# cp[s] = (0..58).step(2)
|
32
|
+
# => equivalent to */2 in cron
|
33
|
+
#
|
34
|
+
# For each component: [:minute,:hour,:day,:dow,:month] we can
|
35
|
+
# then interpret the settings as:
|
36
|
+
#
|
37
|
+
# [true,true,true,true,true]
|
38
|
+
# => run every minute of every hour of every month of every day of
|
39
|
+
# the week
|
40
|
+
# [5,true,true,true,true]
|
41
|
+
# => run 5 minutes past the hour
|
42
|
+
# [(0..55).step(5),true,true,true,true]
|
43
|
+
# => run every 5 minutes
|
44
|
+
# [[10,30,50],true,true,true,true]
|
45
|
+
# => run on 10th, 30th and 50th minute of the hour
|
46
|
+
|
47
|
+
class CronJob < Hash
|
48
|
+
|
49
|
+
def initialize id,minute=true,hour=true,day=true,month=true,dow=true,&block
|
50
|
+
super() {nil}
|
51
|
+
self.set(minute,hour,day,month,dow)
|
52
|
+
self[:id] = id
|
53
|
+
if block_given? then
|
54
|
+
self.job &block
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def set minute=true,hour=true,day=true,month=true,dow=true
|
59
|
+
self[:minute] = minute
|
60
|
+
self[:hour] = hour
|
61
|
+
self[:day] = day
|
62
|
+
self[:month] = month
|
63
|
+
self[:dow] = dow # 0=sunday,...,6=Saturday
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get job or set job via block.
|
67
|
+
#
|
68
|
+
# cj.job
|
69
|
+
# cj.job {|cj|...}
|
70
|
+
|
71
|
+
def job &block
|
72
|
+
if block_given? then
|
73
|
+
#self[:job] = block.call(self)
|
74
|
+
self[:job] = block
|
75
|
+
else
|
76
|
+
self[:job]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def job= thing
|
81
|
+
self[:job] = thing
|
82
|
+
end
|
83
|
+
|
84
|
+
# Run the job.
|
85
|
+
#
|
86
|
+
# This is a convenience method to handle calling proc based
|
87
|
+
# :job's.
|
88
|
+
|
89
|
+
def run
|
90
|
+
case self[:job]
|
91
|
+
when Proc
|
92
|
+
self[:job].call
|
93
|
+
else
|
94
|
+
self[:job]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Return true if job is runnable at the given time.
|
99
|
+
#
|
100
|
+
# Note we expect an instance of Time.
|
101
|
+
# ActiveSupport can be used to give us time zones in Time.
|
102
|
+
#
|
103
|
+
# Example
|
104
|
+
# ok,details = runnable?(Time.now)
|
105
|
+
|
106
|
+
def runnable? time
|
107
|
+
result = [:minute,:hour,:day,:dow,:month].map{|ct|
|
108
|
+
if self[ct] == true then
|
109
|
+
true
|
110
|
+
else
|
111
|
+
case ct
|
112
|
+
when :month,:day,:hour
|
113
|
+
val = time.send(ct)
|
114
|
+
when :dow
|
115
|
+
val = time.wday
|
116
|
+
when :minute
|
117
|
+
val = time.min
|
118
|
+
end
|
119
|
+
|
120
|
+
case self[ct]
|
121
|
+
when Numeric # Should be Fixnum
|
122
|
+
self[ct] == val
|
123
|
+
else # Assume array-like thing...
|
124
|
+
self[ct].include?(val)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
}
|
128
|
+
# Everything should be true to make us eligible for running:
|
129
|
+
[result.inject(true){|s,v| s && v},result]
|
130
|
+
end
|
131
|
+
|
132
|
+
# Return true if the job is intended to only be run as a one-off.
|
133
|
+
|
134
|
+
def once?
|
135
|
+
self[:once] || false
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
data/lib/CronR/utils.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# The files in this directory are part of CronR, a ruby ibrary.
|
2
|
+
# Copyright (C) 2014 Daniel Bush
|
3
|
+
# This program is distributed under the terms of the MIT License. A
|
4
|
+
# copy of the license should be enclosed with this project in the file
|
5
|
+
# LICENSE.txt.
|
6
|
+
|
7
|
+
module CronR
|
8
|
+
module Utils
|
9
|
+
|
10
|
+
# Wake up every minute and call &block.
|
11
|
+
#
|
12
|
+
# Returns the thread that does the waking up.
|
13
|
+
# If &block returns true, then stop.
|
14
|
+
|
15
|
+
def self.every_minute debug=false,&block
|
16
|
+
mil = 1000000
|
17
|
+
secs = 60.0
|
18
|
+
Thread.new {
|
19
|
+
now = Time.now
|
20
|
+
wait = secs-now.sec-now.usec.to_f/mil
|
21
|
+
p "[every_minute] sleeping #{wait}" if debug
|
22
|
+
sleep wait
|
23
|
+
loop {
|
24
|
+
result = block.call
|
25
|
+
if result==true then
|
26
|
+
break
|
27
|
+
end
|
28
|
+
now = Time.now
|
29
|
+
wait = secs-now.sec-now.usec.to_f/mil
|
30
|
+
p "[every_minute] sleeping #{wait}" if debug
|
31
|
+
sleep(wait)
|
32
|
+
}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Wake up every 'secs' number of seconds and call block.
|
37
|
+
#
|
38
|
+
# We measure 'secs' after the function call. So it is not aligned
|
39
|
+
# to anything.
|
40
|
+
|
41
|
+
def self.every secs,debug=false,&block
|
42
|
+
Thread.new {
|
43
|
+
loop {
|
44
|
+
result = block.call
|
45
|
+
if result==true then
|
46
|
+
break
|
47
|
+
end
|
48
|
+
p "[every] sleeping #{secs}" if debug
|
49
|
+
sleep secs
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Make a function that returns time to wait till the next secs-th
|
55
|
+
# second of each minute.
|
56
|
+
#
|
57
|
+
# MOTIVATION
|
58
|
+
#
|
59
|
+
# every_minute wakes up every minute on the minute.
|
60
|
+
# Can we wake up every several seconds, on the second?
|
61
|
+
# Can we break a minute up into intervals starting from the 0th
|
62
|
+
# second in the minute?
|
63
|
+
#
|
64
|
+
# l = make_waitsecs_time(3)
|
65
|
+
# wait = l.call
|
66
|
+
# sleep(wait)
|
67
|
+
# # So, right *now*:
|
68
|
+
# # Time.now.sec ~ s where (0..57).step(3).to_a.include?(s)
|
69
|
+
#
|
70
|
+
|
71
|
+
def self.make_waitsecs_time secs
|
72
|
+
if secs < 1 || secs > 60 then
|
73
|
+
raise "secs must be 1-60"
|
74
|
+
end
|
75
|
+
mil = 1000000
|
76
|
+
lambda{|time=nil|
|
77
|
+
time = Time.now if time.nil?
|
78
|
+
sec = time.sec
|
79
|
+
usec = time.usec
|
80
|
+
frac = usec.to_f/mil
|
81
|
+
remainder = secs - ((sec+1) % secs)
|
82
|
+
remainder -= frac
|
83
|
+
if remainder < 0
|
84
|
+
# TODO if frac is almost zero, return zero here?
|
85
|
+
remainder + secs
|
86
|
+
else
|
87
|
+
remainder
|
88
|
+
end
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
# Make a function that returns time to wait till the next secs-th
|
93
|
+
# second of the unix epoch.
|
94
|
+
#
|
95
|
+
# MOTIVATION
|
96
|
+
#
|
97
|
+
# make_waitsecs_time breaks up the minute starting from the 0th second
|
98
|
+
# and the maximum cycle is therefore 60.
|
99
|
+
# Here we can apply '% secs' for arbitrary value 'secs' up to the current
|
100
|
+
# epoch, although such large values would be of little use.
|
101
|
+
|
102
|
+
def self.make_waitsecs_epoch secs
|
103
|
+
if secs < 1 then
|
104
|
+
raise "secs must be > 1"
|
105
|
+
end
|
106
|
+
mil = 1000000
|
107
|
+
lambda{|time=nil|
|
108
|
+
time = Time.now if time.nil?
|
109
|
+
sec = time.to_i # unix timestamp
|
110
|
+
usec = time.tv_usec # fraction unix component
|
111
|
+
frac = usec.to_f/mil
|
112
|
+
remainder = secs - ((sec+1) % secs)
|
113
|
+
remainder -= frac
|
114
|
+
if remainder < 0
|
115
|
+
# TODO if frac is almost zero, return zero here?
|
116
|
+
remainder + secs
|
117
|
+
else
|
118
|
+
remainder
|
119
|
+
end
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: CronR
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Bush
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-19 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Simple, thread-based, light-weight cron implementation.
|
14
|
+
email: dlb.id.au@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/CronR.rb
|
20
|
+
- lib/CronR/utils.rb
|
21
|
+
- lib/CronR/CronJob.rb
|
22
|
+
- lib/CronR/Cron.rb
|
23
|
+
homepage: http://github.com/danielbush/RubyCron
|
24
|
+
licenses:
|
25
|
+
- MIT
|
26
|
+
metadata: {}
|
27
|
+
post_install_message:
|
28
|
+
rdoc_options: []
|
29
|
+
require_paths:
|
30
|
+
- lib
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - '>='
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
requirements: []
|
42
|
+
rubyforge_project:
|
43
|
+
rubygems_version: 2.1.10
|
44
|
+
signing_key:
|
45
|
+
specification_version: 4
|
46
|
+
summary: An implementation of cron in ruby.
|
47
|
+
test_files: []
|