async-cron 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
- checksums.yaml.gz.sig +0 -0
- data/lib/async/cron/environment/scheduler.rb +34 -0
- data/lib/async/cron/period.rb +252 -0
- data/lib/async/cron/schedule/daily.rb +28 -0
- data/lib/async/cron/schedule/flags.rb +37 -0
- data/lib/async/cron/schedule/generic.rb +79 -0
- data/lib/async/cron/schedule/hourly.rb +27 -0
- data/lib/async/cron/schedule/monthly.rb +29 -0
- data/lib/async/cron/schedule/periodic.rb +82 -0
- data/lib/async/cron/schedule/weekly.rb +29 -0
- data/lib/async/cron/schedule.rb +13 -0
- data/lib/async/cron/scheduler.rb +86 -0
- data/lib/async/cron/service/scheduler.rb +37 -0
- data/lib/async/cron/time.rb +162 -0
- data/lib/async/cron/version.rb +10 -0
- data/lib/async/cron.rb +7 -0
- data/license.md +21 -0
- data/readme.md +23 -0
- data.tar.gz.sig +0 -0
- metadata +119 -0
- metadata.gz.sig +0 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bc78136142298dcbefb1b61a9673fc9ee19cdedcc1f7c49bf734efaf652370d4
|
4
|
+
data.tar.gz: 7ec5b0e30337a7299b3c6b1550d86c1cfef1fae9c811cecb21f17328ada74ccc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7c29dd28a35937ba33d423d60c541c1e80f54dd12415228853172697da5c58bbebcb8b85ffa959ae0bd28f5aff991900b9bb5628531430d7cc463aced867a950
|
7
|
+
data.tar.gz: 99ddc742c526e655d7afc5ef73199abb3ff32f137ca4277d5a389ec7b8fb724b91be0bc27204256600d86bc47868327070ff4ac91b1b61f826d1147353ca71b1
|
checksums.yaml.gz.sig
ADDED
Binary file
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative '../service/scheduler'
|
7
|
+
require_relative '../scheduler'
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Cron
|
11
|
+
module Environment
|
12
|
+
# Provides an environment for hosting a web application that uses a Falcon server.
|
13
|
+
module Scheduler
|
14
|
+
def name
|
15
|
+
super || "scheduler"
|
16
|
+
end
|
17
|
+
|
18
|
+
# The service class to use for the proxy.
|
19
|
+
# @returns [Class]
|
20
|
+
def service_class
|
21
|
+
Service::Scheduler
|
22
|
+
end
|
23
|
+
|
24
|
+
def scheduler
|
25
|
+
Cron::Scheduler.load(self.root)
|
26
|
+
end
|
27
|
+
|
28
|
+
def container_options
|
29
|
+
{count: 1}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'time'
|
7
|
+
|
8
|
+
module Async
|
9
|
+
module Cron
|
10
|
+
class Period
|
11
|
+
# Parse a string into a period.
|
12
|
+
# @parameter string [String | Nil] The string to parse.
|
13
|
+
def self.parse(string)
|
14
|
+
value, divisor = string&.split('/', 2)
|
15
|
+
|
16
|
+
if divisor
|
17
|
+
divisor = Integer(divisor)
|
18
|
+
else
|
19
|
+
divisor = 1
|
20
|
+
end
|
21
|
+
|
22
|
+
if value == '*'
|
23
|
+
value = nil
|
24
|
+
elsif value
|
25
|
+
value = value.split(',').map do |part|
|
26
|
+
if part =~ /\A(\d+)-(\d+)\z/
|
27
|
+
Range.new(Integer($1), Integer($2))
|
28
|
+
elsif part =~ /\A(-?\d+)\.\.(-?\d+)\z/
|
29
|
+
Range.new(Integer($1), Integer($2))
|
30
|
+
else
|
31
|
+
Integer(part)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
self.new(value, divisor)
|
37
|
+
end
|
38
|
+
|
39
|
+
RANGE = nil
|
40
|
+
|
41
|
+
def initialize(value = nil, divisor = 1, range: self.class::RANGE)
|
42
|
+
@value = value
|
43
|
+
@divisor = divisor
|
44
|
+
@range = range
|
45
|
+
|
46
|
+
if @values = divide(expand(@value))
|
47
|
+
# This is an optimization to avoid recalculating the successors every time:
|
48
|
+
@successors = successors(@values)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_a
|
53
|
+
@values
|
54
|
+
end
|
55
|
+
|
56
|
+
# Increment the specific time unit to the next possible value.
|
57
|
+
# @parameter time [Time] The time to increment, must be normalized.
|
58
|
+
def increment(time)
|
59
|
+
time.seconds = @successors[time.seconds]
|
60
|
+
end
|
61
|
+
|
62
|
+
def step(time)
|
63
|
+
time = Time.from(time).normalize!
|
64
|
+
|
65
|
+
increment(time)
|
66
|
+
|
67
|
+
return time
|
68
|
+
end
|
69
|
+
|
70
|
+
# Reset the specific time unit to the first value.
|
71
|
+
def reset(time)
|
72
|
+
time.seconds = @values.first
|
73
|
+
end
|
74
|
+
|
75
|
+
def include?(time)
|
76
|
+
if @values
|
77
|
+
return @values.include?(value_from(time.normalize!))
|
78
|
+
else
|
79
|
+
return true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def value_from(time)
|
86
|
+
time.seconds
|
87
|
+
end
|
88
|
+
|
89
|
+
def expand(values)
|
90
|
+
case values
|
91
|
+
when Array
|
92
|
+
flatten(values)
|
93
|
+
when Range
|
94
|
+
values.to_a
|
95
|
+
when Integer
|
96
|
+
[values]
|
97
|
+
when nil
|
98
|
+
@range&.to_a
|
99
|
+
else
|
100
|
+
raise ArgumentError, "Invalid value: #{values.inspect}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def flatten(values)
|
105
|
+
values = values.flat_map do |value|
|
106
|
+
case value
|
107
|
+
when Array
|
108
|
+
flatten(value)
|
109
|
+
when Range
|
110
|
+
value.to_a
|
111
|
+
else
|
112
|
+
value
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
values.sort!
|
117
|
+
values.uniq!
|
118
|
+
|
119
|
+
return values
|
120
|
+
end
|
121
|
+
|
122
|
+
def divide(values)
|
123
|
+
return values if @divisor == 1 or values.size <= 1
|
124
|
+
raise ArgumentError, "Invalid divisor: #{@divisor}" unless @divisor > 1
|
125
|
+
|
126
|
+
offset = values.first
|
127
|
+
filtered = {}
|
128
|
+
|
129
|
+
values.each do |value|
|
130
|
+
key = ((value - offset) / @divisor).floor
|
131
|
+
filtered[key] ||= value
|
132
|
+
end
|
133
|
+
|
134
|
+
return filtered.values
|
135
|
+
end
|
136
|
+
|
137
|
+
def successors(values)
|
138
|
+
mapped = @range.to_a
|
139
|
+
|
140
|
+
current = 0
|
141
|
+
|
142
|
+
values.each do |value|
|
143
|
+
while current < value
|
144
|
+
mapped[current] = value
|
145
|
+
current += 1
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
while current < mapped.size
|
150
|
+
mapped[current] = values.first + mapped.size
|
151
|
+
current += 1
|
152
|
+
end
|
153
|
+
|
154
|
+
return mapped
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class Seconds < Period
|
159
|
+
RANGE = 0..59
|
160
|
+
end
|
161
|
+
|
162
|
+
class Minutes < Period
|
163
|
+
RANGE = 0..59
|
164
|
+
|
165
|
+
def increment(time)
|
166
|
+
time.minutes = @successors[time.minutes]
|
167
|
+
end
|
168
|
+
|
169
|
+
def reset(time)
|
170
|
+
time.minutes = @values.first
|
171
|
+
end
|
172
|
+
|
173
|
+
def value_from(time)
|
174
|
+
time.minutes
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class Hours < Period
|
179
|
+
RANGE = 0..23
|
180
|
+
|
181
|
+
def increment(time)
|
182
|
+
time.hours = @successors[time.hours]
|
183
|
+
end
|
184
|
+
|
185
|
+
def reset(time)
|
186
|
+
time.hours = @values.first
|
187
|
+
end
|
188
|
+
|
189
|
+
def value_from(time)
|
190
|
+
time.hours
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
class Weekday < Period
|
195
|
+
RANGE = 0..6
|
196
|
+
|
197
|
+
def increment(time)
|
198
|
+
time.weekday = @successors[time.weekday]
|
199
|
+
end
|
200
|
+
|
201
|
+
def reset(time)
|
202
|
+
time.weekday = @values.first
|
203
|
+
end
|
204
|
+
|
205
|
+
def value_from(time)
|
206
|
+
time.weekday
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
class Monthday < Period
|
211
|
+
def increment(time)
|
212
|
+
current_time = time.dup
|
213
|
+
monthday = @successors[time.days] || @values.first
|
214
|
+
|
215
|
+
time.monthday = monthday
|
216
|
+
|
217
|
+
while time <= current_time
|
218
|
+
time.months += 1
|
219
|
+
time.monthday = monthday
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def reset(time)
|
224
|
+
if @values&.any?
|
225
|
+
time.monthday = @values.first
|
226
|
+
else
|
227
|
+
time.monthday = 0
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def value_from(time)
|
232
|
+
time.days
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
class Month < Period
|
237
|
+
RANGE = 0..11
|
238
|
+
|
239
|
+
def increment(time)
|
240
|
+
time.months = @successors[time.months]
|
241
|
+
end
|
242
|
+
|
243
|
+
def reset(time)
|
244
|
+
time.months = @values.first
|
245
|
+
end
|
246
|
+
|
247
|
+
def value_from(time)
|
248
|
+
time.months
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'generic'
|
7
|
+
require_relative '../period'
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Cron
|
11
|
+
module Schedule
|
12
|
+
# A schedule that runs once per day, at midnight.
|
13
|
+
class Daily < Generic
|
14
|
+
def increment(time = nil)
|
15
|
+
time = Time.from(time) || Time.now
|
16
|
+
|
17
|
+
time.seconds = 0
|
18
|
+
time.minutes = 0
|
19
|
+
time.hours = 0
|
20
|
+
time.days += 1
|
21
|
+
|
22
|
+
return time.normalize!
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
module Async
|
7
|
+
module Cron
|
8
|
+
module Schedule
|
9
|
+
class Flags
|
10
|
+
def self.parse(string)
|
11
|
+
options = {}
|
12
|
+
|
13
|
+
string&.each_char do |character|
|
14
|
+
case character
|
15
|
+
when 'D'
|
16
|
+
options[:drop] = true
|
17
|
+
when 'd'
|
18
|
+
options[:drop] = false
|
19
|
+
else
|
20
|
+
raise ArgumentError, "Invalid flag: #{character}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
self.new(**options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(drop: true)
|
28
|
+
@drop = drop
|
29
|
+
end
|
30
|
+
|
31
|
+
def drop?
|
32
|
+
@drop
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'flags'
|
7
|
+
require_relative '../time'
|
8
|
+
|
9
|
+
require 'console'
|
10
|
+
|
11
|
+
module Async
|
12
|
+
module Cron
|
13
|
+
module Schedule
|
14
|
+
class Generic
|
15
|
+
def initialize(flags = Flags.new)
|
16
|
+
@flags = flags
|
17
|
+
end
|
18
|
+
|
19
|
+
attr :flags
|
20
|
+
|
21
|
+
# Compute the next execution time after the given time.
|
22
|
+
def increment(time = nil)
|
23
|
+
Time.now
|
24
|
+
end
|
25
|
+
|
26
|
+
def run_once(time = Time.now, &block)
|
27
|
+
# The number of times the schedule has been dropped because the scheduled time was in the past:
|
28
|
+
dropped = 0
|
29
|
+
|
30
|
+
while true
|
31
|
+
# Compute the next scheduled time:
|
32
|
+
scheduled_time = increment(time)
|
33
|
+
|
34
|
+
# If the time is not advancing and we are dropping, then raise an error:
|
35
|
+
if scheduled_time <= time and dropped > 0
|
36
|
+
raise RuntimeError, "Scheduled time is not advancing: #{scheduled_time} <= #{time}"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Compute the delta until the next scheduled time:
|
40
|
+
delta = scheduled_time.delta
|
41
|
+
|
42
|
+
Console.debug(self, "Next scheduled time.", delta: delta, scheduled_time: scheduled_time, block: block)
|
43
|
+
|
44
|
+
# If the time is in the past, and we are dropping, then skip this schedule and try again:
|
45
|
+
if delta < 0 and @flags.drop?
|
46
|
+
# This would normally occur if the user code took too long to execute, causing the next scheduled time to be in the past.
|
47
|
+
Console.warn(self, "Skipping schedule because it is in the past.", delta: delta, scheduled_time: scheduled_time, block: block, dropped: dropped)
|
48
|
+
dropped += 1
|
49
|
+
next
|
50
|
+
end
|
51
|
+
|
52
|
+
break
|
53
|
+
end
|
54
|
+
|
55
|
+
Console.debug(self, "Waiting for next scheduled time.", delta: delta, scheduled_time: scheduled_time, block: block)
|
56
|
+
sleep(delta) if delta > 0
|
57
|
+
|
58
|
+
begin
|
59
|
+
invoke(scheduled_time, &block)
|
60
|
+
rescue => error
|
61
|
+
Console::Event::Failure.for(error).emit(self, delta: delta, scheduled_time: scheduled_time, block: block)
|
62
|
+
end
|
63
|
+
|
64
|
+
return time
|
65
|
+
end
|
66
|
+
|
67
|
+
def run(time = Time.now, &block)
|
68
|
+
while true
|
69
|
+
time = run_once(time, &block)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def invoke(time, &block)
|
74
|
+
yield(time)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'generic'
|
7
|
+
require_relative '../period'
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Cron
|
11
|
+
module Schedule
|
12
|
+
# A schedule that runs once per hour, on the hour.
|
13
|
+
class Hourly < Generic
|
14
|
+
def increment(time = nil)
|
15
|
+
time = Time.from(time) || Time.now
|
16
|
+
|
17
|
+
time.seconds = 0
|
18
|
+
time.minutes = 0
|
19
|
+
time.hours += 1
|
20
|
+
|
21
|
+
return time.normalize!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'generic'
|
7
|
+
require_relative '../period'
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Cron
|
11
|
+
module Schedule
|
12
|
+
# A schedule that runs once per day, at midnight.
|
13
|
+
class Monthly < Generic
|
14
|
+
def increment(time = nil)
|
15
|
+
time = Time.from(time) || Time.now
|
16
|
+
|
17
|
+
time.seconds = 0
|
18
|
+
time.minutes = 0
|
19
|
+
time.hours = 0
|
20
|
+
time.days = 0
|
21
|
+
time.months += 1
|
22
|
+
|
23
|
+
return time.normalize!
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'generic'
|
7
|
+
require_relative '../period'
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Cron
|
11
|
+
module Schedule
|
12
|
+
class Periodic < Generic
|
13
|
+
def self.parse(string)
|
14
|
+
parts = string.split(/\s+/)
|
15
|
+
|
16
|
+
if parts.last =~ /[a-zA-Z]/
|
17
|
+
flags = Flags.parse(parts.pop)
|
18
|
+
else
|
19
|
+
flags = Flags.new
|
20
|
+
end
|
21
|
+
|
22
|
+
seconds = Seconds.parse(parts[0])
|
23
|
+
minutes = Minutes.parse(parts[1])
|
24
|
+
hours = Hours.parse(parts[2])
|
25
|
+
weekday = Weekday.parse(parts[3])
|
26
|
+
monthday = Monthday.parse(parts[4])
|
27
|
+
month = Month.parse(parts[5])
|
28
|
+
|
29
|
+
return self.new(seconds, minutes, hours, weekday, monthday, month, flags)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Create a new schedule with the given parameters.
|
33
|
+
def initialize(seconds, minutes, hours, weekday, monthday, month, flags = Flags.new)
|
34
|
+
super(flags)
|
35
|
+
|
36
|
+
@seconds = seconds
|
37
|
+
@minutes = minutes
|
38
|
+
@hours = hours
|
39
|
+
@weekday = weekday
|
40
|
+
@monthday = monthday
|
41
|
+
@month = month
|
42
|
+
end
|
43
|
+
|
44
|
+
# Compute the next execution time after the given time.
|
45
|
+
def increment(time = nil)
|
46
|
+
time = Time.from(time) || Time.now
|
47
|
+
|
48
|
+
units = [@seconds, @minutes, @hours, @weekday, @monthday, @month]
|
49
|
+
index = 1
|
50
|
+
|
51
|
+
# Step the smallest unit first:
|
52
|
+
units[0].increment(time)
|
53
|
+
|
54
|
+
# Now try to fit the rest of the schedule:
|
55
|
+
while index < units.length
|
56
|
+
unit = units[index]
|
57
|
+
|
58
|
+
# puts "Checking #{unit} at #{index}... -> #{time}"
|
59
|
+
|
60
|
+
# If the unit is not already at the desired time, increment it:
|
61
|
+
unless unit.include?(time)
|
62
|
+
# puts "Incrementing #{unit} at #{index}..."
|
63
|
+
unit.increment(time)
|
64
|
+
# puts "-> #{time}"
|
65
|
+
|
66
|
+
# Reset all smaller units:
|
67
|
+
units[0...index].each do |unit|
|
68
|
+
# puts "Resetting #{unit}..."
|
69
|
+
unit.reset(time)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
index += 1
|
74
|
+
end
|
75
|
+
|
76
|
+
return time.normalize!
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'generic'
|
7
|
+
require_relative '../period'
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Cron
|
11
|
+
module Schedule
|
12
|
+
# A schedule that runs once per day, at midnight.
|
13
|
+
class Weekly < Generic
|
14
|
+
def increment(time = nil)
|
15
|
+
time = Time.from(time) || Time.now
|
16
|
+
|
17
|
+
time.seconds = 0
|
18
|
+
time.minutes = 0
|
19
|
+
time.hours = 0
|
20
|
+
# 0-6, Sunday is zero
|
21
|
+
time.weekday = 7
|
22
|
+
|
23
|
+
return time.normalize!
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
# These are mostly just for convenience:
|
7
|
+
require_relative 'schedule/hourly'
|
8
|
+
require_relative 'schedule/daily'
|
9
|
+
require_relative 'schedule/weekly'
|
10
|
+
require_relative 'schedule/monthly'
|
11
|
+
|
12
|
+
# This one gives you a lot of flexibility at the expense of being sgightly more verbose:
|
13
|
+
require_relative 'schedule/periodic'
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require 'async'
|
7
|
+
require 'async/barrier'
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Cron
|
11
|
+
class Scheduler
|
12
|
+
class Loader
|
13
|
+
def initialize(scheduler)
|
14
|
+
@scheduler = scheduler
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(schedule, &block)
|
18
|
+
@scheduler.add(schedule, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def hourly(&block)
|
22
|
+
add(Schedule::Hourly.new, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def daily(&block)
|
26
|
+
add(Schedule::Daily.new, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def weekly(&block)
|
30
|
+
add(Schedule::Weekly.new, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def monthly(&block)
|
34
|
+
add(Schedule::Monthly.new, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def periodic(specification, &block)
|
38
|
+
add(Schedule::Periodic.parse(specification), &block)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
PATH = "config/async/cron/scheduler.rb"
|
43
|
+
|
44
|
+
def self.load(root = Dir.pwd)
|
45
|
+
path = ::File.join(root, PATH)
|
46
|
+
|
47
|
+
if ::File.exist?(path)
|
48
|
+
return self.load_file(path)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.load_file(path)
|
53
|
+
scheduler = self.new
|
54
|
+
loader = Loader.new(scheduler)
|
55
|
+
|
56
|
+
loader.instance_eval(::File.read(path), path)
|
57
|
+
|
58
|
+
return scheduler
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize
|
62
|
+
@schedules = Hash.new.compare_by_identity
|
63
|
+
end
|
64
|
+
|
65
|
+
attr :schedules
|
66
|
+
|
67
|
+
def add(schedule, &block)
|
68
|
+
@schedules[schedule] = block
|
69
|
+
end
|
70
|
+
|
71
|
+
def run
|
72
|
+
Sync do
|
73
|
+
barrier = Async::Barrier.new
|
74
|
+
|
75
|
+
@schedules.each do |schedule, block|
|
76
|
+
barrier.async do
|
77
|
+
schedule.run(&block)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
barrier.wait
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2019-2024, by Samuel Williams.
|
5
|
+
# Copyright, 2020, by Daniel Evans.
|
6
|
+
|
7
|
+
require 'async/service/generic'
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Cron
|
11
|
+
module Service
|
12
|
+
class Scheduler < Async::Service::Generic
|
13
|
+
# Setup the container with the application instance.
|
14
|
+
# @parameter container [Async::Container::Generic]
|
15
|
+
def setup(container)
|
16
|
+
container_options = @evaluator.container_options
|
17
|
+
|
18
|
+
container.run(name: self.name, **container_options) do |instance|
|
19
|
+
evaluator = @environment.evaluator
|
20
|
+
|
21
|
+
Async do |task|
|
22
|
+
scheduler = evaluator.scheduler
|
23
|
+
|
24
|
+
task = Async do
|
25
|
+
scheduler.run
|
26
|
+
end
|
27
|
+
|
28
|
+
instance.ready!
|
29
|
+
|
30
|
+
task.wait
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require 'date'
|
7
|
+
|
8
|
+
module Async
|
9
|
+
module Cron
|
10
|
+
# A base-zero that supports assigning arbitrary values (both positive and negative) to all units. This simplifies computing relative times and dates by incrementing the relevant units and normalizing the result.
|
11
|
+
#
|
12
|
+
# **Note that base-zero means that the month and day start from zero, not one, as is the case with the standard Ruby Time and Date classes.** That is because the fields also accept negative values, which would be ambiguous if the month and day started from one.
|
13
|
+
class Time
|
14
|
+
include Comparable
|
15
|
+
|
16
|
+
def self.from(time)
|
17
|
+
case time
|
18
|
+
when ::Time
|
19
|
+
return self.new(time.year, time.month-1, time.day-1, time.hour, time.min, time.sec, time.utc_offset)
|
20
|
+
when ::DateTime
|
21
|
+
return self.new(time.year, time.month-1, time.day-1, time.hour, time.min, time.sec, time.offset)
|
22
|
+
when ::Date
|
23
|
+
return self.new(time.year, time.month-1, time.day-1, 0, 0, 0, 0)
|
24
|
+
when self
|
25
|
+
return time.dup
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.now
|
30
|
+
return self.from(::Time.now)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @parameter years [Integer] The number of years.
|
34
|
+
# @parameter months [Integer] The number of months.
|
35
|
+
# @parameter days [Integer] The number of days.
|
36
|
+
# @parameter hours [Integer] The number of hours.
|
37
|
+
# @parameter minutes [Integer] The number of minutes.
|
38
|
+
# @parameter seconds [Integer] The number of seconds.
|
39
|
+
# @parameter offset [Integer] The time zone offset.
|
40
|
+
def initialize(years, months, days, hours, minutes, seconds, offset)
|
41
|
+
@years = years
|
42
|
+
@months = months
|
43
|
+
@days = days
|
44
|
+
@hours = hours
|
45
|
+
@minutes = minutes
|
46
|
+
@seconds = seconds
|
47
|
+
@offset = offset
|
48
|
+
end
|
49
|
+
|
50
|
+
def freeze
|
51
|
+
return self if frozen?
|
52
|
+
|
53
|
+
@years.freeze
|
54
|
+
@months.freeze
|
55
|
+
@days.freeze
|
56
|
+
@hours.freeze
|
57
|
+
@minutes.freeze
|
58
|
+
@seconds.freeze
|
59
|
+
@offset.freeze
|
60
|
+
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_accessor :years
|
65
|
+
attr_accessor :months
|
66
|
+
attr_accessor :days
|
67
|
+
|
68
|
+
attr_accessor :hours
|
69
|
+
attr_accessor :minutes
|
70
|
+
attr_accessor :seconds
|
71
|
+
|
72
|
+
attr_accessor :offset
|
73
|
+
|
74
|
+
def weekday
|
75
|
+
to_date.wday
|
76
|
+
end
|
77
|
+
|
78
|
+
def weekday=(value)
|
79
|
+
@days += value - weekday
|
80
|
+
end
|
81
|
+
|
82
|
+
def monthday=(value)
|
83
|
+
if value >= 0
|
84
|
+
@days = value
|
85
|
+
else
|
86
|
+
@days = ::Date.new(@years, @months+1, value).day
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_a
|
91
|
+
[@years, @months, @days, @hours, @minutes, @seconds, @offset]
|
92
|
+
end
|
93
|
+
|
94
|
+
def <=> other
|
95
|
+
to_a <=> other.to_a
|
96
|
+
end
|
97
|
+
|
98
|
+
def hash
|
99
|
+
to_a.hash
|
100
|
+
end
|
101
|
+
|
102
|
+
def eql? other
|
103
|
+
to_a.eql?(other.to_a)
|
104
|
+
end
|
105
|
+
|
106
|
+
def delta
|
107
|
+
self.to_time - ::Time.now
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_time
|
111
|
+
normalized = self.dup.normalize!
|
112
|
+
|
113
|
+
return ::Time.new(normalized.years, normalized.months+1, normalized.days+1, normalized.hours, normalized.minutes, normalized.seconds, normalized.offset)
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_datetime
|
117
|
+
normalized = self.dup.normalize!
|
118
|
+
|
119
|
+
return ::DateTime.new(normalized.years, normalized.months+1, normalized.days+1, normalized.hours, normalized.minutes, normalized.seconds, normalized.offset)
|
120
|
+
end
|
121
|
+
|
122
|
+
def to_date
|
123
|
+
normalized = self.dup.normalize!
|
124
|
+
|
125
|
+
return ::Date.new(normalized.years, normalized.months+1, normalized.days+1)
|
126
|
+
end
|
127
|
+
|
128
|
+
def normalize!
|
129
|
+
seconds = self.seconds
|
130
|
+
delta_minutes = seconds / 60
|
131
|
+
seconds = seconds % 60
|
132
|
+
|
133
|
+
minutes = self.minutes + delta_minutes
|
134
|
+
delta_hours = minutes / 60
|
135
|
+
minutes = minutes % 60
|
136
|
+
|
137
|
+
hours = self.hours + delta_hours
|
138
|
+
delta_days = hours / 24
|
139
|
+
hours = hours % 24
|
140
|
+
|
141
|
+
days = self.days + delta_days
|
142
|
+
months = self.months
|
143
|
+
years = self.years
|
144
|
+
|
145
|
+
date = (Date.new(years, 1, 1) >> months) + days
|
146
|
+
|
147
|
+
@years = date.year
|
148
|
+
@months = date.month - 1
|
149
|
+
@days = date.day - 1
|
150
|
+
@hours = hours
|
151
|
+
@minutes = minutes
|
152
|
+
@seconds = seconds
|
153
|
+
|
154
|
+
return self
|
155
|
+
end
|
156
|
+
|
157
|
+
def to_s
|
158
|
+
sprintf("%04d+%02d+%02d %02d:%02d:%02d %d", years, months, days, hours, minutes, seconds, offset)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
data/lib/async/cron.rb
ADDED
data/license.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# MIT License
|
2
|
+
|
3
|
+
Copyright, 2024, by Samuel Williams.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/readme.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Async::Cron
|
2
|
+
|
3
|
+
[](https://github.com/socketry/async-cron/actions?workflow=Test)
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
## Contributing
|
8
|
+
|
9
|
+
We welcome contributions to this project.
|
10
|
+
|
11
|
+
1. Fork it.
|
12
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
13
|
+
3. Commit your changes (`git commit -am 'Add some feature'`).
|
14
|
+
4. Push to the branch (`git push origin my-new-feature`).
|
15
|
+
5. Create new Pull Request.
|
16
|
+
|
17
|
+
### Developer Certificate of Origin
|
18
|
+
|
19
|
+
This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
|
20
|
+
|
21
|
+
### Contributor Covenant
|
22
|
+
|
23
|
+
This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
|
data.tar.gz.sig
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: async-cron
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Samuel Williams
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain:
|
11
|
+
- |
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
|
14
|
+
ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
|
15
|
+
CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
|
16
|
+
MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
|
17
|
+
MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
|
18
|
+
bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
|
19
|
+
igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
|
20
|
+
9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
|
21
|
+
sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
|
22
|
+
e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
|
23
|
+
XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
|
24
|
+
RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
|
25
|
+
tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
|
26
|
+
zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
|
27
|
+
xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
|
28
|
+
BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
|
29
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
|
30
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
|
31
|
+
cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
|
32
|
+
xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
|
33
|
+
c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
|
34
|
+
8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
|
35
|
+
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
|
36
|
+
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
|
37
|
+
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
38
|
+
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
39
|
+
-----END CERTIFICATE-----
|
40
|
+
date: 2024-06-08 00:00:00.000000000 Z
|
41
|
+
dependencies:
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: async
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '2.0'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '2.0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: async-service
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
description:
|
71
|
+
email:
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- lib/async/cron.rb
|
77
|
+
- lib/async/cron/environment/scheduler.rb
|
78
|
+
- lib/async/cron/period.rb
|
79
|
+
- lib/async/cron/schedule.rb
|
80
|
+
- lib/async/cron/schedule/daily.rb
|
81
|
+
- lib/async/cron/schedule/flags.rb
|
82
|
+
- lib/async/cron/schedule/generic.rb
|
83
|
+
- lib/async/cron/schedule/hourly.rb
|
84
|
+
- lib/async/cron/schedule/monthly.rb
|
85
|
+
- lib/async/cron/schedule/periodic.rb
|
86
|
+
- lib/async/cron/schedule/weekly.rb
|
87
|
+
- lib/async/cron/scheduler.rb
|
88
|
+
- lib/async/cron/service/scheduler.rb
|
89
|
+
- lib/async/cron/time.rb
|
90
|
+
- lib/async/cron/version.rb
|
91
|
+
- license.md
|
92
|
+
- readme.md
|
93
|
+
homepage: https://github.com/socketry/async-cron
|
94
|
+
licenses:
|
95
|
+
- MIT
|
96
|
+
metadata:
|
97
|
+
documentation_uri: https://socketry.github.io/async-cron/
|
98
|
+
funding_uri: https://github.com/sponsors/ioquatix
|
99
|
+
source_code_uri: https://github.com/socketry/async-cron.git
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '3.1'
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
requirements: []
|
115
|
+
rubygems_version: 3.5.9
|
116
|
+
signing_key:
|
117
|
+
specification_version: 4
|
118
|
+
summary: A scheduling service using cron-style syntax.
|
119
|
+
test_files: []
|
metadata.gz.sig
ADDED
Binary file
|