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.
@@ -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
@@ -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
@@ -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
@@ -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: []