CronR 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|