qron 0.9.0 → 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 +4 -4
- data/CHANGELOG.md +11 -1
- data/README.md +56 -3
- data/lib/qron.rb +180 -39
- data/qron.gemspec +2 -1
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0b1115e1f100badef504e6d0837e35049add1811e63f4bcd5c588001f32aec5
|
4
|
+
data.tar.gz: 8df3d58f2c5cf42669372d0ac427c94a47aa3c5a48e364cde5cca55b7d8dbb2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6dd7efe16ccda47ebe916fb75ad99cf63100399ca55b1c586502b987f04697aad3fff5a871ccae4a1c731223e07f9f08687e4feb0ef8cf0d1746c1f4fed186e
|
7
|
+
data.tar.gz: 75dec291fa552c5f4ab0f9d9141db0853035f67f85c2b6f5afd744f2b34fde90c7a5d8dccb0201a34d03aa8eb5345a70f95de082f177c9b277d638c050a1ab0b
|
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,17 @@
|
|
2
2
|
# CHANGELOG.md
|
3
3
|
|
4
4
|
|
5
|
-
##
|
5
|
+
## qron 1.0.0 released 2025-04-01
|
6
|
+
|
7
|
+
* Implement "settings"
|
8
|
+
* Implement reload: true
|
9
|
+
* Rework scheduling thread sleep timing
|
10
|
+
* Implement @reboot schedule
|
11
|
+
* Implement #on_error and #on_tab_error
|
12
|
+
* Implement #on_tick
|
13
|
+
|
14
|
+
|
15
|
+
## qron 0.9.0 released 2025-03-23
|
6
16
|
|
7
17
|
* Initial release
|
8
18
|
|
data/README.md
CHANGED
@@ -10,9 +10,10 @@ A stupid Ruby cron thread that wakes up from time to time to perform according
|
|
10
10
|
to what's written in a crontab.
|
11
11
|
|
12
12
|
Given `etc/qrontab_dev`:
|
13
|
-
```
|
14
|
-
|
15
|
-
* * * * *
|
13
|
+
```ruby
|
14
|
+
@reboot p [ :hello, "just started" ]
|
15
|
+
* * * * * p [ :hello, :min, Time.now ]
|
16
|
+
* * * * * * p [ :hello, :sec, Time.now ]
|
16
17
|
```
|
17
18
|
|
18
19
|
and
|
@@ -40,6 +41,58 @@ Uses [fugit](https://github.com/floraison/fugit) for cron parsing and
|
|
40
41
|
|
41
42
|
A little brother to [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler).
|
42
43
|
|
44
|
+
|
45
|
+
### `reload: true`
|
46
|
+
|
47
|
+
Specifying `reload: true` when initializing tells the `Qron` instance to reload its crontab file at every tick.
|
48
|
+
|
49
|
+
(Qron ticks usually every minute, unless it has one or more second precision crons specified, in which case it ticks every second).
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
require 'qron'
|
53
|
+
|
54
|
+
q = Qron.new(tab: 'etc/qrontab_dev', reload: true)
|
55
|
+
```
|
56
|
+
|
57
|
+
### Timezones
|
58
|
+
|
59
|
+
It's OK to use timezones in the qrontab file:
|
60
|
+
```ruby
|
61
|
+
30 * * * * Asia/Tokyo p [ :tokyo, :min, Time.now ]
|
62
|
+
30 4 1,15 * 5 Europe/Budapest p [ :budapest, :min, Time.now ]
|
63
|
+
```
|
64
|
+
|
65
|
+
|
66
|
+
### "Settings"
|
67
|
+
|
68
|
+
A qrontab file accepts, cron and commands but also "settings" that set
|
69
|
+
variables in the context passed to commands:
|
70
|
+
```ruby
|
71
|
+
#
|
72
|
+
# settings
|
73
|
+
|
74
|
+
a = 1 + 2
|
75
|
+
b = Time.now
|
76
|
+
|
77
|
+
#
|
78
|
+
# actual crons
|
79
|
+
|
80
|
+
* * * * * * pp [ :ctx, ctx ]
|
81
|
+
```
|
82
|
+
where the puts might output something like:
|
83
|
+
```ruby
|
84
|
+
[ :ctx,
|
85
|
+
{ time: 'Time instance...',
|
86
|
+
cron: 'Fugit::Cron instance...',
|
87
|
+
command: 'pp [ :ctx, ctx ]',
|
88
|
+
qron: 'The Qron instance...',
|
89
|
+
a: 3,
|
90
|
+
b: 'Time instance...' } ]
|
91
|
+
```
|
92
|
+
|
93
|
+
A context is instantied and prepare for each command when it triggers.
|
94
|
+
|
95
|
+
|
43
96
|
## LICENSE
|
44
97
|
|
45
98
|
MIT, see [LICENSE.txt](LICENSE.txt)
|
data/lib/qron.rb
CHANGED
@@ -6,14 +6,24 @@ require 'stagnum'
|
|
6
6
|
|
7
7
|
class Qron
|
8
8
|
|
9
|
-
VERSION = '0.
|
9
|
+
VERSION = '1.0.0'.freeze
|
10
10
|
|
11
11
|
attr_reader :options
|
12
|
-
attr_reader :tab, :thread, :started, :
|
12
|
+
attr_reader :tab, :thread, :started, :work_pool
|
13
|
+
attr_reader :tab_res, :tab_mtime
|
14
|
+
attr_reader :listeners
|
13
15
|
|
14
16
|
def initialize(opts={})
|
15
17
|
|
16
18
|
@options = opts
|
19
|
+
@options[:reload] = false unless opts.has_key?(:reload)
|
20
|
+
|
21
|
+
@tab = nil
|
22
|
+
@tab_res = nil
|
23
|
+
@tab_mtime = Time.now
|
24
|
+
|
25
|
+
@booted = false
|
26
|
+
@listeners = []
|
17
27
|
|
18
28
|
start unless opts[:start] == false
|
19
29
|
end
|
@@ -21,28 +31,29 @@ class Qron
|
|
21
31
|
def start
|
22
32
|
|
23
33
|
@started = Time.now
|
24
|
-
@last_sec = @started.to_i
|
25
34
|
|
26
35
|
@work_pool ||=
|
27
|
-
Stagnum::Pool.new("
|
36
|
+
Stagnum::Pool.new("qron-#{Qron::VERSION}-pool", @options[:workers] || 3)
|
28
37
|
|
29
38
|
@thread =
|
30
39
|
Thread.new do
|
40
|
+
Thread.current[:name] =
|
41
|
+
@options[:thread_name] || "qron-#{Qron::VERSION}-thread"
|
31
42
|
loop do
|
32
43
|
break if @started == nil
|
33
44
|
now = Time.now
|
34
|
-
|
35
|
-
|
36
|
-
sleep 0.7 + (0.5 * rand)
|
45
|
+
tick(now)
|
46
|
+
sleep(determine_sleep_time(now))
|
37
47
|
end
|
38
48
|
end
|
39
|
-
|
40
|
-
# TODO rescue perform...
|
41
49
|
end
|
42
50
|
|
43
51
|
def stop
|
44
52
|
|
45
53
|
@started = nil
|
54
|
+
|
55
|
+
@thread.kill
|
56
|
+
@thread = nil
|
46
57
|
end
|
47
58
|
|
48
59
|
def join
|
@@ -50,52 +61,182 @@ class Qron
|
|
50
61
|
@thread && @thread.join
|
51
62
|
end
|
52
63
|
|
53
|
-
|
64
|
+
# In some deployments, another thread ticks the qron instance. So #tick(now)
|
65
|
+
# is a public method.
|
66
|
+
#
|
67
|
+
def tick(now)
|
68
|
+
|
69
|
+
fetch_tab
|
70
|
+
|
71
|
+
@tab.each do |cron, command|
|
72
|
+
|
73
|
+
perform(now, cron, command) if cron_match?(cron, now)
|
74
|
+
end
|
54
75
|
|
55
|
-
|
76
|
+
@booted = true
|
77
|
+
|
78
|
+
trigger_event(:on_tick, time: now)
|
79
|
+
end
|
80
|
+
|
81
|
+
def fetch_tab
|
82
|
+
|
83
|
+
return @tab if @tab && @options[:reload] == false
|
56
84
|
|
57
85
|
t = @options[:crontab] || @options[:tab] || 'qrontab'
|
86
|
+
m = mtime(t)
|
58
87
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
next a if l == ''
|
67
|
-
next a if l.start_with?('#')
|
68
|
-
ll5 = l.split(/\s+/, 6)
|
69
|
-
ll6 = ll5.pop.split(/\s+/, 2)
|
70
|
-
ll5 = ll5.join(' ')
|
71
|
-
ll6, r = *ll6
|
72
|
-
c = Fugit::Cron.parse("#{ll5} #{ll6}")
|
73
|
-
unless c
|
74
|
-
c = Fugit::Cron.parse(ll5)
|
75
|
-
r = "#{ll6} #{r}"
|
76
|
-
end
|
77
|
-
a << [ c, r ]
|
78
|
-
a }
|
88
|
+
if m > @tab_mtime
|
89
|
+
@tab = nil
|
90
|
+
@tab_tempo = nil
|
91
|
+
end
|
92
|
+
@tab_mtime = m
|
93
|
+
|
94
|
+
@tab ||= parse(t)
|
79
95
|
end
|
80
96
|
|
81
|
-
def
|
97
|
+
def on_tab_error(&block); @listeners << [ :on_tab_error, block ]; end
|
98
|
+
#def on_tick_error(&block); @listeners << [ :on_tick_error, block ]; end
|
99
|
+
def on_perform_error(&block); @listeners << [ :on_perform_error, block ]; end
|
82
100
|
|
83
|
-
|
101
|
+
def on_error(&block)
|
102
|
+
@listeners << [ :on_tab_error, block ]
|
103
|
+
@listeners << [ :on_perform_error, block ]
|
104
|
+
end
|
84
105
|
|
85
|
-
|
106
|
+
def on_tick(&block); @listeners << [ :on_tick, block ]; end
|
107
|
+
|
108
|
+
def trigger_event(event_name, ctx)
|
86
109
|
|
87
|
-
|
110
|
+
@listeners.each { |name, block| block.call(ctx) if name == event_name }
|
111
|
+
end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
def mtime(t)
|
116
|
+
|
117
|
+
if t.is_a?(String) && t.count("\n") < 1 && File.exist?(t)
|
118
|
+
File.mtime(t)
|
119
|
+
else
|
120
|
+
Time.now
|
88
121
|
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def parse(t)
|
125
|
+
|
126
|
+
case t
|
127
|
+
when Array then parse_lines(t)
|
128
|
+
when /\n/ then parse_lines(t.lines)
|
129
|
+
when String then parse_file(t)
|
130
|
+
else fail(ArgumentError.new("cannot parse instance of #{t.class}"))
|
131
|
+
end
|
132
|
+
|
133
|
+
rescue => err
|
89
134
|
|
90
|
-
|
135
|
+
trigger_event(:on_tab_error, time: Time.now, error: err)
|
136
|
+
|
137
|
+
[]
|
91
138
|
end
|
92
139
|
|
93
|
-
def
|
140
|
+
def parse_file(path)
|
141
|
+
|
142
|
+
parse_lines(File.readlines(path))
|
143
|
+
end
|
94
144
|
|
95
|
-
|
145
|
+
def parse_lines(ls)
|
146
|
+
|
147
|
+
ls.map { |l| parse_line(l) }.compact
|
148
|
+
end
|
96
149
|
|
97
|
-
|
150
|
+
def parse_line(l)
|
151
|
+
|
152
|
+
l = l.strip
|
153
|
+
|
154
|
+
return nil if l == ''
|
155
|
+
return nil if l.start_with?('#')
|
156
|
+
|
157
|
+
parse_setting(l) ||
|
158
|
+
parse_special(l) ||
|
159
|
+
parse_cron(l, 7) || parse_cron(l, 6) || parse_cron(l, 5) ||
|
160
|
+
fail(ArgumentError.new("could not parse }#{l}{"))
|
161
|
+
end
|
162
|
+
|
163
|
+
def parse_setting(line)
|
164
|
+
|
165
|
+
m = line.match(/^([a-z][_0-9a-zA-Z]*)\s+=\s+(.+)$/)
|
166
|
+
|
167
|
+
m ? [ 'setting', "ctx[:#{m[1]}] = #{m[2]}" ] : nil
|
168
|
+
end
|
169
|
+
|
170
|
+
def parse_special(line)
|
171
|
+
|
172
|
+
line.start_with?(/@reboot\s/) ?
|
173
|
+
[ '@reboot', line.split(/\s+/, 2).last ] :
|
174
|
+
nil
|
175
|
+
end
|
176
|
+
|
177
|
+
def parse_cron(line, word_count)
|
178
|
+
|
179
|
+
ll = line.split(/\s+/, word_count + 1)
|
180
|
+
c, r = Fugit::Cron.parse(ll.take(word_count).join(' ')), ll.last
|
181
|
+
|
182
|
+
c ? [ c, r] : nil
|
183
|
+
end
|
184
|
+
|
185
|
+
def cron_match?(cron, time)
|
186
|
+
|
187
|
+
if cron == '@reboot'
|
188
|
+
@booted == false
|
189
|
+
elsif cron.is_a?(Fugit::Cron)
|
190
|
+
cron.match?(time)
|
191
|
+
else
|
192
|
+
false # well...
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def perform(now, cron, command)
|
197
|
+
|
198
|
+
@work_pool.enqueue(make_context(now, cron, command)) do |ctx|
|
199
|
+
|
200
|
+
Kernel.eval("Proc.new { |ctx| #{command} }").call(ctx)
|
201
|
+
|
202
|
+
rescue => err
|
203
|
+
|
204
|
+
trigger_event(:on_perform_error, time: Time.now, error: err)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def make_context(now, cron, command)
|
209
|
+
|
210
|
+
ctx = { time: now, cron: cron, command: command, qron: self }
|
211
|
+
|
212
|
+
@tab.each do |c, command|
|
213
|
+
|
214
|
+
Kernel.eval("Proc.new { |ctx| #{command} }").call(ctx) if c == 'setting'
|
98
215
|
end
|
216
|
+
|
217
|
+
ctx
|
218
|
+
end
|
219
|
+
|
220
|
+
def determine_sleep_time(now)
|
221
|
+
|
222
|
+
@tab_res ||=
|
223
|
+
@tab.find { |c, _| c.is_a?(Fugit::Cron) && c.resolution == :second } ?
|
224
|
+
:second : :minute
|
225
|
+
|
226
|
+
res = @tab_res == :second ? 1.0 : 60.0
|
227
|
+
|
228
|
+
res - (now.to_f % res) + 0.021
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
|
233
|
+
# Should it be part of fugit?
|
234
|
+
#
|
235
|
+
class Fugit::Cron
|
236
|
+
|
237
|
+
def resolution
|
238
|
+
|
239
|
+
seconds == [ 0 ] ? :minute : :second
|
99
240
|
end
|
100
241
|
end
|
101
242
|
|
data/qron.gemspec
CHANGED
@@ -37,7 +37,8 @@ A Ruby thread that wakes up in time to perform what's ordered in its crontab
|
|
37
37
|
"#{s.name}.gemspec",
|
38
38
|
]
|
39
39
|
|
40
|
-
|
40
|
+
s.add_runtime_dependency 'fugit', '~> 1.11'
|
41
|
+
s.add_runtime_dependency 'stagnum', '~> 1.0'
|
41
42
|
|
42
43
|
s.add_development_dependency 'probatio', '~> 1.0'
|
43
44
|
|
metadata
CHANGED
@@ -1,14 +1,42 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: qron
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Mettraux
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-04-01 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: fugit
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '1.11'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '1.11'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: stagnum
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.0'
|
12
40
|
- !ruby/object:Gem::Dependency
|
13
41
|
name: probatio
|
14
42
|
requirement: !ruby/object:Gem::Requirement
|