secondhand 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README.md +286 -0
- data/Rakefile +82 -0
- data/lib/quartz/log4j-1.2.14.jar +0 -0
- data/lib/quartz/quartz-1.8.4.jar +0 -0
- data/lib/quartz/slf4j-api-1.6.0.jar +0 -0
- data/lib/quartz/slf4j-log4j12-1.6.0.jar +0 -0
- data/lib/secondhand.rb +89 -0
- data/lib/secondhand/job.rb +58 -0
- data/lib/secondhand/jobs.rb +25 -0
- data/lib/secondhand/logging.rb +30 -0
- data/lib/secondhand/scheduler.rb +51 -0
- data/lib/secondhand/trigger.rb +24 -0
- data/lib/secondhand/version.rb +3 -0
- data/test/integration/test_secondhand_run.rb +48 -0
- data/test/test_helper.rb +32 -0
- data/test/unit/test_job.rb +45 -0
- data/test/unit/test_jobs.rb +20 -0
- data/test/unit/test_logger.rb +30 -0
- data/test/unit/test_scheduler.rb +33 -0
- data/test/unit/test_secondhand.rb +29 -0
- data/test/unit/test_trigger.rb +22 -0
- metadata +84 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2010 Amphora Reseach Systems Ltd.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,286 @@
|
|
1
|
+
Secondhand
|
2
|
+
==========
|
3
|
+
|
4
|
+
Secondhand is a ruby-friendly wrapper around the [Quartz Job Scheduler][0].
|
5
|
+
Create jobs, tell Secondhand when they should run, then sit back and enjoy.
|
6
|
+
|
7
|
+
_Secondhand is JRuby only what with Quartz being Java and all._
|
8
|
+
|
9
|
+
|
10
|
+
Overview
|
11
|
+
--------
|
12
|
+
|
13
|
+
Secondhand was created to:
|
14
|
+
|
15
|
+
* provide a clean API to job scheduling (with few, if any, dependencies)
|
16
|
+
* give you more control over job resource utilization/allocation
|
17
|
+
* take advantage of the Quartz service
|
18
|
+
|
19
|
+
You can schedule jobs that will run right away, on a specific date, or use
|
20
|
+
a repeating schedule with a familiar cron syntax. Secondhand allows you to
|
21
|
+
adjust the number of Quartz worker threads, and create jobs in a way that makes
|
22
|
+
sense in your environment.
|
23
|
+
|
24
|
+
Secondhand does not try to be a full interface to all the power and
|
25
|
+
capabilities of Quartz. Think of it more like a nice subset that allows you
|
26
|
+
to have robust job scheduling without learning all about Quartz. (You can always
|
27
|
+
get to the underlying objects and tinker if you need to.)
|
28
|
+
|
29
|
+
Like Resque, Jobs in Secondhand can be any class or module that responds to
|
30
|
+
`perform` and, like some of the other Quartz+Ruby libraries out there, a job
|
31
|
+
can be a block/proc. (Secondhand jobs don't take parameters [yet].)
|
32
|
+
|
33
|
+
|
34
|
+
Workers
|
35
|
+
-------
|
36
|
+
|
37
|
+
Quartz spawns worker threads to execute jobs when triggers are fired. Secondhand
|
38
|
+
defaults to 5 workers as recommended in the Quartz documentation - while Quartz
|
39
|
+
defaults to 10. Note: Quartz will block itself if all worker threads are busy
|
40
|
+
when a trigger fires.
|
41
|
+
|
42
|
+
|
43
|
+
Jobs
|
44
|
+
----
|
45
|
+
|
46
|
+
A job in Secondhand might look like this:
|
47
|
+
|
48
|
+
class Scan
|
49
|
+
def self.perform # class method
|
50
|
+
do_some_work
|
51
|
+
log_it
|
52
|
+
do_some_more_work
|
53
|
+
close_connections
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# To run this job immediately you would use:
|
58
|
+
|
59
|
+
Secondhand.schedule Scan
|
60
|
+
|
61
|
+
|
62
|
+
### OR
|
63
|
+
|
64
|
+
You can go with the block syntax for an immediate job:
|
65
|
+
|
66
|
+
Secondhand.schedule :the_scan do
|
67
|
+
do_some_work
|
68
|
+
log_it
|
69
|
+
do_some_more_work
|
70
|
+
close_connections
|
71
|
+
end
|
72
|
+
|
73
|
+
This scheduled job is added to the Quartz scheduler and associated with a
|
74
|
+
trigger. In the case of the jobs above, the trigger is a `SimpleTrigger` that
|
75
|
+
executes immediately.
|
76
|
+
|
77
|
+
*Important*
|
78
|
+
Each job must have a unique name. Secondhand calls #to_s on the first
|
79
|
+
parameter and uses that for the job name. This means that you can't schedule the
|
80
|
+
same class twice [yet].
|
81
|
+
|
82
|
+
While the block syntax is nice to look at it does have some drawbacks. Object
|
83
|
+
allocation happens when the block is created and references to these objects
|
84
|
+
are held for the lifetime of the block. In our case, this lifetime is while
|
85
|
+
the Quartz service is running since the job is scheduled, stored, and called by
|
86
|
+
a trigger.
|
87
|
+
|
88
|
+
Having the ability to just schedule a class/module that responds to `perform` as
|
89
|
+
a job means that only a reference to that constant is held by the Quartz service
|
90
|
+
and object allocation happens when `perform` is executed.
|
91
|
+
|
92
|
+
Thinking of these jobs as atomic units of work may help when deciding how they
|
93
|
+
should be scheduled in your application.
|
94
|
+
|
95
|
+
## Persisted Jobs
|
96
|
+
|
97
|
+
Secondhand just uses the default Quartz RAM job scheduler. This provides no
|
98
|
+
durability for jobs to live after your application exists. It is up to you to
|
99
|
+
make sure they get rescheduled.
|
100
|
+
|
101
|
+
|
102
|
+
Usage
|
103
|
+
-----
|
104
|
+
|
105
|
+
The scheduler must be started before you schedule jobs.
|
106
|
+
|
107
|
+
Secondhand.start(NUM_THREADS)
|
108
|
+
|
109
|
+
Then you get to scheduling ...
|
110
|
+
|
111
|
+
... job that runs immediately after being scheduled:
|
112
|
+
|
113
|
+
Secondhand.schedule JobClass
|
114
|
+
|
115
|
+
... block that runs immediately:
|
116
|
+
|
117
|
+
Secondhand.schedule :some_job do
|
118
|
+
# some job stuff
|
119
|
+
end
|
120
|
+
|
121
|
+
... job that runs on Friday, Dec 10 at 11:53:35 PST:
|
122
|
+
|
123
|
+
Secondhand.schedule JobClass, :at => "Fri Dec 10 22:53:35 -0800 2010"
|
124
|
+
|
125
|
+
... job that runs two minutes from now:
|
126
|
+
|
127
|
+
Secondhand.schedule JobClass, :at => Time.now + 120
|
128
|
+
|
129
|
+
... job that runs every 15 seconds every day:
|
130
|
+
|
131
|
+
Secondhand.schedule JobClass, :with_cron => "0/15 * * * * ? *"
|
132
|
+
|
133
|
+
## :at
|
134
|
+
|
135
|
+
Pass in the :at options to provide a specific moment in time for a job to be
|
136
|
+
triggered. This is useful when you want to run a job once - perhaps in response
|
137
|
+
to some user input.
|
138
|
+
|
139
|
+
## :with_cron
|
140
|
+
|
141
|
+
Use :with_cron to provide Secondhand with a cron expression that indicates when
|
142
|
+
a job should be triggered. This expression is passed straight to Quartz so all
|
143
|
+
options are supported. Just send in a string with the following format:
|
144
|
+
|
145
|
+
_From the Quartz CronExpression documentation:_
|
146
|
+
|
147
|
+
> Cron expressions are comprised of 6 required fields and one optional field
|
148
|
+
> separated by white space. The fields respectively are described as follows:
|
149
|
+
|
150
|
+
Field Name Allowed Values Allowed Special Characters
|
151
|
+
|
152
|
+
Seconds 0-59 , - * /
|
153
|
+
Minutes 0-59 , - * /
|
154
|
+
Hours 0-23 , - * /
|
155
|
+
Day-of-month 1-31 , - * ? / L W
|
156
|
+
Month 1-12 or JAN-DEC , - * /
|
157
|
+
Day-of-Week 1-7 or SUN-SAT , - * ? / L #
|
158
|
+
Year (Optional) empty, 1970-2199 , - * /
|
159
|
+
|
160
|
+
> The `*` character is used to specify all values. For example, "*" in the
|
161
|
+
> minute field means "every minute".
|
162
|
+
>
|
163
|
+
> The `?` character is allowed for the day-of-month and day-of-week fields. It
|
164
|
+
> is used to specify 'no specific value'. This is useful when you need to
|
165
|
+
> specify something in one of the two fields, but not the other.
|
166
|
+
>
|
167
|
+
> The `-` character is used to specify ranges For example "10-12" in the hour
|
168
|
+
> field means "the hours 10, 11 and 12".
|
169
|
+
>
|
170
|
+
> The `,` character is used to specify additional values. For example
|
171
|
+
> "MON,WED,FRI" in the day-of-week field means "the days Monday, Wednesday,
|
172
|
+
> and Friday".
|
173
|
+
>
|
174
|
+
> The `/` character is used to specify increments. For example "0/15" in the
|
175
|
+
> seconds field means "the seconds 0, 15, 30, and 45". And "5/15" in the seconds
|
176
|
+
> field means "the seconds 5, 20, 35, and 50". Specifying '*' before the '/' is
|
177
|
+
> equivalent to specifying 0 is the value to start with. Essentially, for each
|
178
|
+
> field in the expression, there is a set of numbers that can be turned on or
|
179
|
+
> off. For seconds and minutes, the numbers range from 0 to 59. For hours 0 to
|
180
|
+
> 23, for days of the month 0 to 31, and for months 1 to 12. The "/" character
|
181
|
+
> simply helps you turn on every "nth" value in the given set. Thus "7/6" in the
|
182
|
+
> month field only turns on month "7", it does NOT mean every 6th month, please
|
183
|
+
> note that subtlety.
|
184
|
+
>
|
185
|
+
> The `L` character is allowed for the day-of-month and day-of-week fields. This
|
186
|
+
> character is short-hand for "last", but it has different meaning in each of
|
187
|
+
> the two fields. For example, the value "L" in the day-of-month field means
|
188
|
+
> "the last day of the month" - day 31 for January, day 28 for February on
|
189
|
+
> non-leap years.
|
190
|
+
> If used in the day-of-week field by itself, it simply means "7" or "SAT". But
|
191
|
+
> if used in the day-of-week field after another value, it means "the last xxx
|
192
|
+
> day of the month" - for example "6L" means "the last friday of the month".
|
193
|
+
> When using the 'L' option, it is important not to specify lists, or ranges of
|
194
|
+
> values, as you'll get confusing results.
|
195
|
+
>
|
196
|
+
> The `W` character is allowed for the day-of-month field. This character is
|
197
|
+
> used to specify the weekday (Monday-Friday) nearest the given day. As an
|
198
|
+
> example, if you were to specify "15W" as the value for the day-of-month field,
|
199
|
+
> the meaning is: "the nearest weekday to the 15th of the month". So if the 15th
|
200
|
+
> is a Saturday, the trigger will fire on Friday the 14th. If the 15th is a
|
201
|
+
> Sunday, the trigger will fire on Monday the 16th. If the 15th is a Tuesday,
|
202
|
+
> then it will fire on Tuesday the 15th. However if you specify "1W" as the
|
203
|
+
> value for day-of-month, and the 1st is a Saturday, the trigger will fire on
|
204
|
+
> Monday the 3rd, as it will not 'jump' over the boundary of a month's days.
|
205
|
+
> The 'W' character can only be specified when the day-of-month is a single day,
|
206
|
+
> not a range or list of days.
|
207
|
+
>
|
208
|
+
> The `L` and `W` characters can also be combined for the day-of-month
|
209
|
+
> expression to yield `LW`, which translates to "last weekday of the month".
|
210
|
+
>
|
211
|
+
> The `#` character is allowed for the day-of-week field. This character is used
|
212
|
+
> to specify "the nth" XXX day of the month. For example, the value of "6#3" in
|
213
|
+
> the day-of-week field means the third Friday of the month (day 6 = Friday and
|
214
|
+
> "#3" = the 3rd one in the month). Other examples: "2#1" = the first Monday of
|
215
|
+
> the month and "4#5" = the fifth Wednesday of the month. Note that if you
|
216
|
+
> specify "#5" and there is not 5 of the given day-of-week in the month, then
|
217
|
+
> no firing will occur that month. If the '#' character is used, there can only
|
218
|
+
> be one expression in the day-of-week field ("3#1,6#3" is not valid, since
|
219
|
+
> there are two expressions).
|
220
|
+
>
|
221
|
+
> The legal characters and the names of months and days of the week are not case
|
222
|
+
> sensitive.
|
223
|
+
>
|
224
|
+
> NOTES:
|
225
|
+
>
|
226
|
+
> Support for specifying both a day-of-week and a day-of-month value is not
|
227
|
+
> complete (you'll need to use the '?' character in one of these fields).
|
228
|
+
> Overflowing ranges is supported - that is, having a larger number on the left
|
229
|
+
> hand side than the right. You might do 22-2 to catch 10 o'clock at night until
|
230
|
+
> 2 o'clock in the morning, or you might have NOV-FEB. It is very important to
|
231
|
+
> note that overuse of overflowing ranges creates ranges that don't make sense
|
232
|
+
> and no effort has been made to determine which interpretation CronExpression
|
233
|
+
> chooses. An example would be "0 0 14-6 ? * FRI-MON".
|
234
|
+
|
235
|
+
|
236
|
+
Installation
|
237
|
+
------------
|
238
|
+
|
239
|
+
Coming soon to a command line near you.
|
240
|
+
|
241
|
+
|
242
|
+
## Quartz
|
243
|
+
|
244
|
+
Version supported in this release: 1.8.4
|
245
|
+
|
246
|
+
_Other version may work, but the tests and jars included are for 1.8.4._
|
247
|
+
|
248
|
+
All the jars needed for Quartz are in `lib\quartz`. They are used by Secondhand
|
249
|
+
unless JRuby::Rack is detected. If JRuby::Rack is present, it is assumed that
|
250
|
+
you will have the jars under WEB-INF or get them on the classpath however you
|
251
|
+
choose to.
|
252
|
+
|
253
|
+
|
254
|
+
What's in a name?
|
255
|
+
----------------
|
256
|
+
Secondhand was inspired from several other Quartz+Ruby implementations:
|
257
|
+
|
258
|
+
* quartz-jruby: <https://github.com/artha42/quartz-jruby>
|
259
|
+
* jruby-quartz: <https://github.com/techwhizbang/jruby-quartz>
|
260
|
+
|
261
|
+
Chiefly, quartz-jruby demonstrated what was needed to get the Quartz JobFactory
|
262
|
+
replaced with a Ruby class and playing nicely with the other components. Thanks
|
263
|
+
vagmi!
|
264
|
+
|
265
|
+
The jobs were taken straight from Resque <https://github.com/defunkt/resque> as
|
266
|
+
well as some other flourishes here and there.
|
267
|
+
|
268
|
+
The project started as a fork of quartz-jruby so I figured it was a second-hand
|
269
|
+
project and the clock reference wasn't bad either.
|
270
|
+
|
271
|
+
|
272
|
+
Contributors
|
273
|
+
------------
|
274
|
+
|
275
|
+
Don Morrison (@elskwid)
|
276
|
+
|
277
|
+
|
278
|
+
License
|
279
|
+
-------
|
280
|
+
|
281
|
+
Copyright (c) 2010 Amphora Research Systems Ltd.
|
282
|
+
|
283
|
+
Secondhand is licensed under the MIT license. See LICENSE for details.
|
284
|
+
|
285
|
+
|
286
|
+
[0]: http://www.quartz-scheduler.org/
|
data/Rakefile
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "rake/gempackagetask"
|
5
|
+
require "rake/rdoctask"
|
6
|
+
|
7
|
+
require "rake/testtask"
|
8
|
+
Rake::TestTask.new do |t|
|
9
|
+
t.libs << "test"
|
10
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
11
|
+
t.verbose = true
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
task :default => ["test"]
|
16
|
+
|
17
|
+
# This builds the actual gem. For details of what all these options
|
18
|
+
# mean, and other ones you can add, check the documentation here:
|
19
|
+
#
|
20
|
+
# http://rubygems.org/read/chapter/20
|
21
|
+
#
|
22
|
+
spec = Gem::Specification.new do |s|
|
23
|
+
|
24
|
+
# Change these as appropriate
|
25
|
+
s.name = "secondhand"
|
26
|
+
s.version = "0.1.0"
|
27
|
+
s.summary = "Secondhand is a ruby-friendly wrapper around the Quartz Job Scheduler. Create jobs, tell Secondhand when they should run, then sit back and enjoy."
|
28
|
+
s.author = "Don Morrison"
|
29
|
+
s.email = "elskwid@gmail.com"
|
30
|
+
s.homepage = "https://github.com/amphora/secondhand"
|
31
|
+
|
32
|
+
s.has_rdoc = true
|
33
|
+
s.extra_rdoc_files = %w(README.md)
|
34
|
+
s.rdoc_options = %w(--main README.md)
|
35
|
+
|
36
|
+
# Add any extra files to include in the gem
|
37
|
+
s.files = %w(LICENSE Rakefile README.md) + Dir.glob("{test,lib}/**/*")
|
38
|
+
s.require_paths = ["lib"]
|
39
|
+
|
40
|
+
# If you want to depend on other gems, add them here, along with any
|
41
|
+
# relevant versions
|
42
|
+
# s.add_dependency("some_other_gem", "~> 0.1.0")
|
43
|
+
|
44
|
+
# If your tests use any gems, include them here
|
45
|
+
# s.add_development_dependency("mocha") # for example
|
46
|
+
end
|
47
|
+
|
48
|
+
# This task actually builds the gem. We also regenerate a static
|
49
|
+
# .gemspec file, which is useful if something (i.e. GitHub) will
|
50
|
+
# be automatically building a gem for this project. If you're not
|
51
|
+
# using GitHub, edit as appropriate.
|
52
|
+
#
|
53
|
+
# To publish your gem online, install the 'gemcutter' gem; Read more
|
54
|
+
# about that here: http://gemcutter.org/pages/gem_docs
|
55
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
56
|
+
pkg.gem_spec = spec
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "Build the gemspec file #{spec.name}.gemspec"
|
60
|
+
task :gemspec do
|
61
|
+
file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
|
62
|
+
File.open(file, "w") {|f| f << spec.to_ruby }
|
63
|
+
end
|
64
|
+
|
65
|
+
# If you don't want to generate the .gemspec file, just remove this line. Reasons
|
66
|
+
# why you might want to generate a gemspec:
|
67
|
+
# - using bundler with a git source
|
68
|
+
# - building the gem without rake (i.e. gem build blah.gemspec)
|
69
|
+
# - maybe others?
|
70
|
+
task :package => :gemspec
|
71
|
+
|
72
|
+
# Generate documentation
|
73
|
+
Rake::RDocTask.new do |rd|
|
74
|
+
rd.main = "README.md"
|
75
|
+
rd.rdoc_files.include("README.md", "lib/**/*.rb")
|
76
|
+
rd.rdoc_dir = "rdoc"
|
77
|
+
end
|
78
|
+
|
79
|
+
desc 'Clear out RDoc and generated packages'
|
80
|
+
task :clean => [:clobber_rdoc, :clobber_package] do
|
81
|
+
rm "#{spec.name}.gemspec"
|
82
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/lib/secondhand.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'java'
|
2
|
+
|
3
|
+
# If JRuby::Rack::VERSION is defined then Secondhand is in a servlet.
|
4
|
+
# Secondhand does not need to require jar archives in this case
|
5
|
+
# as those should be in WEB-INF/lib and already in the classpath
|
6
|
+
unless defined?(JRuby::Rack::VERSION)
|
7
|
+
# jars
|
8
|
+
require 'quartz/log4j-1.2.14.jar'
|
9
|
+
require 'quartz/quartz-1.8.4.jar'
|
10
|
+
require 'quartz/slf4j-api-1.6.0.jar'
|
11
|
+
require 'quartz/slf4j-log4j12-1.6.0.jar'
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'secondhand/logging'
|
15
|
+
require 'secondhand/job'
|
16
|
+
require 'secondhand/jobs'
|
17
|
+
require 'secondhand/scheduler'
|
18
|
+
require 'secondhand/trigger'
|
19
|
+
require 'secondhand/version'
|
20
|
+
|
21
|
+
module Secondhand
|
22
|
+
import java.lang.System
|
23
|
+
import org.quartz.impl.StdScheduler
|
24
|
+
import org.quartz.impl.StdSchedulerFactory
|
25
|
+
|
26
|
+
extend Scheduler
|
27
|
+
extend self
|
28
|
+
|
29
|
+
# Raised when the scheduler is used before being initialized
|
30
|
+
class NoSchedulerError < RuntimeError; end
|
31
|
+
|
32
|
+
DEFAULT_GROUP = "SECONDHAND"
|
33
|
+
DEFAULT_THREAD_COUNT = 5
|
34
|
+
@scheduler = nil
|
35
|
+
|
36
|
+
def reset_thread_count
|
37
|
+
@thread_count = DEFAULT_THREAD_COUNT
|
38
|
+
end
|
39
|
+
|
40
|
+
def thread_count=(threads)
|
41
|
+
@thread_count = threads
|
42
|
+
end
|
43
|
+
|
44
|
+
def thread_count
|
45
|
+
reset_thread_count unless @thread_count
|
46
|
+
@thread_count
|
47
|
+
end
|
48
|
+
|
49
|
+
def start(threads = nil)
|
50
|
+
initialize_scheduler(threads)
|
51
|
+
scheduler.start
|
52
|
+
end
|
53
|
+
alias_method :run, :start
|
54
|
+
|
55
|
+
def initialize_scheduler(threads = nil)
|
56
|
+
@thread_count = threads if threads
|
57
|
+
# Quartz uses this property to set the number of worker threads
|
58
|
+
System.set_property("org.quartz.threadPool.threadCount", @thread_count.to_s)
|
59
|
+
# Stop Quartz from looking for updates
|
60
|
+
System.set_property("org.quartz.scheduler.skipUpdateCheck", "true")
|
61
|
+
# Initialize the default scheduler
|
62
|
+
@scheduler = StdSchedulerFactory.default_scheduler
|
63
|
+
# Set up our custom job factory
|
64
|
+
@scheduler.job_factory = Job::Factory.instance
|
65
|
+
end
|
66
|
+
|
67
|
+
def schedule(job, opts={}, &block)
|
68
|
+
name = job.to_s
|
69
|
+
scheduler.schedule_job(Job.create(name, block ? block : job), Trigger.create(name, opts))
|
70
|
+
end
|
71
|
+
|
72
|
+
# Shortcuts
|
73
|
+
|
74
|
+
# Raise an error if the scheduler isn't ready - with Quartz we have an order
|
75
|
+
# of operations to follow. This also lets us pass the threads in with .start
|
76
|
+
def scheduler
|
77
|
+
raise NoSchedulerError, "Scheduler isn't initialized. Call Secondhand.start before scheduling jobs." unless @scheduler
|
78
|
+
@scheduler
|
79
|
+
end
|
80
|
+
|
81
|
+
def jobs
|
82
|
+
Jobs
|
83
|
+
end
|
84
|
+
|
85
|
+
def job_names
|
86
|
+
Jobs.names
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'singleton' # for factory
|
2
|
+
|
3
|
+
module Secondhand
|
4
|
+
import org.quartz.JobDetail
|
5
|
+
import org.quartz.spi.JobFactory
|
6
|
+
|
7
|
+
module Job
|
8
|
+
|
9
|
+
def self.create(name, work)
|
10
|
+
# Store the job and work for our job factory impl
|
11
|
+
Details.new(name, Secondhand::DEFAULT_GROUP, work)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Quartz croaks with a Ruby created JobClass so, hijack the job here
|
15
|
+
# and bend it to our will
|
16
|
+
class Details < Java::OrgQuartz::JobDetail
|
17
|
+
attr_accessor :job
|
18
|
+
|
19
|
+
def initialize(name, group, work)
|
20
|
+
super()
|
21
|
+
# name & group are required by Quartz
|
22
|
+
set_name name.to_s
|
23
|
+
set_group group.to_s
|
24
|
+
# add the execute method if needed
|
25
|
+
work.extend Job::Runner unless work.respond_to?(:execute)
|
26
|
+
@job = work
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Override validate so Quartz doesn't throw an error about the JobClass
|
31
|
+
def validate; end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
# Thin wrapper around a job to provide the expected Quartz interface
|
36
|
+
module Runner
|
37
|
+
def execute(context = nil)
|
38
|
+
self.respond_to?(:call) ? self.call : self.perform
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Create custom JobFactory to hand out jobs from a Ruby collection
|
43
|
+
# and not from the Quartz job list. Avoids problems with JRuby and
|
44
|
+
# interfaces to and from Ruby.
|
45
|
+
class Factory
|
46
|
+
include Singleton
|
47
|
+
include JobFactory
|
48
|
+
|
49
|
+
# Quartz calls new_job on the factory to get the job details
|
50
|
+
# * event = execution-time data about the job (from Quartz)
|
51
|
+
def new_job(event)
|
52
|
+
# Get a job from the factory - Quartz calls #execute(context) on this
|
53
|
+
event.get_job_detail.job
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Secondhand
|
2
|
+
module Jobs
|
3
|
+
extend self
|
4
|
+
|
5
|
+
# Provide an array-like method to get to the job details
|
6
|
+
def [](name)
|
7
|
+
details = scheduler.get_job_detail(name, group)
|
8
|
+
details.job if details
|
9
|
+
end
|
10
|
+
|
11
|
+
def names
|
12
|
+
Array(scheduler.get_job_names(DEFAULT_GROUP))
|
13
|
+
end
|
14
|
+
|
15
|
+
# Shortcuts
|
16
|
+
|
17
|
+
def scheduler
|
18
|
+
Secondhand.scheduler
|
19
|
+
end
|
20
|
+
|
21
|
+
def group
|
22
|
+
Secondhand::DEFAULT_GROUP
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Secondhand
|
4
|
+
module Logger
|
5
|
+
import org.apache.log4j
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def logger
|
9
|
+
Java::OrgApache::log4j::Logger.get_logger("Secondhand")
|
10
|
+
end
|
11
|
+
|
12
|
+
def debug(msg)
|
13
|
+
logger.log(Level::DEBUG, msg)
|
14
|
+
end
|
15
|
+
|
16
|
+
def info(msg)
|
17
|
+
logger.log(Level::INFO, msg)
|
18
|
+
end
|
19
|
+
|
20
|
+
def error(msg)
|
21
|
+
logger.log(Level::ERROR, msg)
|
22
|
+
end
|
23
|
+
|
24
|
+
def warn(msg)
|
25
|
+
logger.log(Level::WARN, msg)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Secondhand
|
4
|
+
module Scheduler
|
5
|
+
extend Forwardable
|
6
|
+
extend self
|
7
|
+
|
8
|
+
# Add some friendly faces
|
9
|
+
def_delegators :scheduler, :shutdown,
|
10
|
+
:stop,
|
11
|
+
:stop_now,
|
12
|
+
:started?,
|
13
|
+
:running?,
|
14
|
+
:paused?,
|
15
|
+
:standing_by?,
|
16
|
+
:shutdown?,
|
17
|
+
:stopped?
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
# Add some sugar on StdScheduler
|
22
|
+
class Java::OrgQuartzImpl::StdScheduler
|
23
|
+
def run
|
24
|
+
start
|
25
|
+
end
|
26
|
+
|
27
|
+
def stop_now
|
28
|
+
shutdown
|
29
|
+
end
|
30
|
+
|
31
|
+
def stop
|
32
|
+
shutdown(true) # waits for all jobs to finish
|
33
|
+
end
|
34
|
+
|
35
|
+
def running?
|
36
|
+
is_started
|
37
|
+
end
|
38
|
+
|
39
|
+
def paused?
|
40
|
+
is_in_standby_mode
|
41
|
+
end
|
42
|
+
alias_method :standing_by?, :paused?
|
43
|
+
|
44
|
+
def stopped?
|
45
|
+
is_shutdown
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Secondhand
|
2
|
+
import org.quartz.CronTrigger
|
3
|
+
import org.quartz.SimpleTrigger
|
4
|
+
|
5
|
+
class Trigger
|
6
|
+
|
7
|
+
# Return correct Quartz Trigger
|
8
|
+
def self.create(name, opts)
|
9
|
+
args = ["#{name}_trigger", Secondhand::DEFAULT_GROUP]
|
10
|
+
|
11
|
+
# with_cron trumps other options
|
12
|
+
if opts[:with_cron]
|
13
|
+
args << opts[:with_cron]
|
14
|
+
CronTrigger.new(*(args).compact)
|
15
|
+
else
|
16
|
+
args << opts[:at] || opts[:on]
|
17
|
+
SimpleTrigger.new(*(args).compact)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
class TestSecondhandRun < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
Secondhand.reset_thread_count
|
7
|
+
org.apache.log4j.BasicConfigurator.configure
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_five_second_cron_job
|
11
|
+
Secondhand.start(1)
|
12
|
+
assert Secondhand.started?
|
13
|
+
|
14
|
+
# This job will say 'something' every 5 seconds
|
15
|
+
Secondhand.schedule SaySomething, :with_cron => "0/5 * * * * ?"
|
16
|
+
assert_equal SaySomething, Secondhand.jobs["SaySomething"]
|
17
|
+
|
18
|
+
# This job will say 'block something' every 10 seconds
|
19
|
+
Secondhand.schedule "my_block", :with_cron => "0/10 * * * * ?" do
|
20
|
+
puts "block something [10 sec]"
|
21
|
+
end
|
22
|
+
|
23
|
+
# This job will say 'One shot job!' as soon as it is scheduled
|
24
|
+
Secondhand.schedule "one_shot" do
|
25
|
+
puts "One shot job!"
|
26
|
+
end
|
27
|
+
|
28
|
+
puts "sleeping while the scheduler runs ..."
|
29
|
+
sleep 20
|
30
|
+
|
31
|
+
Secondhand.stop
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
class SaySomething
|
37
|
+
# You could add the logging methods to your class
|
38
|
+
# extend Secondhand::Logger
|
39
|
+
|
40
|
+
def self.perform
|
41
|
+
puts "something [5sec]"
|
42
|
+
# Or you can just call the logger
|
43
|
+
# Secondhand::Logger.info "something [5sec] --> through Log4J"
|
44
|
+
|
45
|
+
# With extend Logging from above
|
46
|
+
# self.info "another thing ==> through Log4j"
|
47
|
+
end
|
48
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "/../lib"))
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
# project
|
7
|
+
require 'secondhand'
|
8
|
+
|
9
|
+
# BasicConfigurator.configure uses a ConsoleAppender by default
|
10
|
+
# uncomment to see all the Quartz logging magic!
|
11
|
+
# org.apache.log4j.BasicConfigurator.configure
|
12
|
+
|
13
|
+
## NullAppender silences log4j messages
|
14
|
+
import org.apache.log4j.varia.NullAppender
|
15
|
+
org.apache.log4j.BasicConfigurator.configure(NullAppender.new)
|
16
|
+
|
17
|
+
module TestHelper
|
18
|
+
#
|
19
|
+
end
|
20
|
+
|
21
|
+
class Test::Unit::TestCase
|
22
|
+
include TestHelper
|
23
|
+
end
|
24
|
+
|
25
|
+
## utility classes for tests
|
26
|
+
class TestSimpleJob
|
27
|
+
def self.perform
|
28
|
+
"ALLOK"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class TestSimpleJob2 < TestSimpleJob; end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
class TestJob < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
Secondhand.start(1)
|
7
|
+
@factory = Secondhand::Job::Factory.instance
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_simple_job_details
|
11
|
+
details = Secondhand::Job.create "TestSimpleJob2", TestSimpleJob2
|
12
|
+
assert_kind_of Java::OrgQuartz::JobDetail, details
|
13
|
+
assert_equal TestSimpleJob2, details.job
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_simple_class_job
|
17
|
+
details = Secondhand::Job.create "TestSimpleJob", TestSimpleJob
|
18
|
+
assert_equal TestSimpleJob, details.job
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_simple_named_job_block
|
22
|
+
details = Secondhand::Job.create "some_job", Proc.new{"Some job text"}
|
23
|
+
assert_equal "Some job text", details.job.call
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_runner_module_on_class
|
27
|
+
details = Secondhand::Job.create "TestSimpleJob2", TestSimpleJob2
|
28
|
+
assert details.job.respond_to?(:execute)
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_runner_module_on_block
|
32
|
+
details = Secondhand::Job.create "some_other_job", Proc.new{"Some other job text"}
|
33
|
+
assert details.job.respond_to?(:execute)
|
34
|
+
end
|
35
|
+
|
36
|
+
# job factory tests
|
37
|
+
def test_kind_of
|
38
|
+
assert_kind_of Java::OrgQuartz::spi::JobFactory, @factory
|
39
|
+
assert_kind_of Secondhand::Job::Factory, @factory
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_respond_to_new_job
|
43
|
+
assert @factory.respond_to?(:new_job)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
class TestJobs < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
Secondhand.start(1)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_bracket_method
|
10
|
+
# jobs aren't in this list until they are scheduled - quartz limitation
|
11
|
+
Secondhand.schedule TestSimpleJob
|
12
|
+
assert_equal TestSimpleJob, Secondhand::Jobs["TestSimpleJob"]
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_names
|
16
|
+
Secondhand.schedule TestSimpleJob2
|
17
|
+
assert Secondhand::Jobs.names.include?("TestSimpleJob2")
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
class TestLogger < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
end
|
7
|
+
|
8
|
+
class IncludeLogger
|
9
|
+
include Secondhand::Logger
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def test_included_logger
|
14
|
+
inst = IncludeLogger.new
|
15
|
+
assert inst.respond_to?(:logger)
|
16
|
+
assert inst.respond_to?(:warn)
|
17
|
+
end
|
18
|
+
|
19
|
+
class ExtendLogger
|
20
|
+
extend Secondhand::Logger
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_extended_logger
|
24
|
+
klass = ExtendLogger
|
25
|
+
assert klass.respond_to?(:logger)
|
26
|
+
assert klass.respond_to?(:error)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
class TestScheduler < Test::Unit::TestCase
|
4
|
+
import org.quartz.impl.StdScheduler
|
5
|
+
|
6
|
+
def setup
|
7
|
+
Secondhand.reset_thread_count
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_default_thread_count
|
11
|
+
assert_equal 5, Secondhand.thread_count
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_thread_count_setter
|
15
|
+
Secondhand.thread_count = 2
|
16
|
+
assert_equal 2, Secondhand.thread_count
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_scheduler_instance
|
20
|
+
Secondhand.initialize_scheduler
|
21
|
+
assert_instance_of StdScheduler, Secondhand.scheduler
|
22
|
+
assert_equal Secondhand.scheduler, Secondhand.scheduler
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_state_and_argument
|
26
|
+
sh = Secondhand
|
27
|
+
sh.start(1)
|
28
|
+
assert sh.running?
|
29
|
+
assert sh.started?
|
30
|
+
assert_equal 1, sh.thread_count
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
class TestSecondhand < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
# Secondhand.jobs.clear
|
7
|
+
Secondhand.start(1)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_simple_class_job
|
11
|
+
Secondhand.schedule TestSimpleJob
|
12
|
+
assert_equal TestSimpleJob, Secondhand.jobs["TestSimpleJob"]
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_simple_named_job_block
|
16
|
+
Secondhand.schedule "some_job" do
|
17
|
+
"Some job text"
|
18
|
+
end
|
19
|
+
assert Secondhand.jobs["some_job"].is_a? Proc
|
20
|
+
assert_equal "Some job text", Secondhand.jobs["some_job"].call
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_schedule_simple_job
|
24
|
+
date = Time.now + 120 # 120 seconds from now
|
25
|
+
Secondhand.schedule TestSimpleJob2, :at => date
|
26
|
+
assert_equal TestSimpleJob2, Secondhand.jobs["TestSimpleJob2"]
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
class TestTrigger < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_at
|
9
|
+
date = "Dec 10 22:53:35 -0800 2011"
|
10
|
+
sched = Secondhand::Trigger.create("some_job", :at => Time.parse(date))
|
11
|
+
assert_kind_of Java::OrgQuartz::SimpleTrigger, sched
|
12
|
+
assert_equal Time.parse(date), Time.parse(sched.get_start_time.to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_with_cron
|
16
|
+
exp = "0 0 12 * * ? *"
|
17
|
+
sched = Secondhand::Trigger.create("some_cronjob", :with_cron => exp)
|
18
|
+
assert_kind_of Java::OrgQuartz::CronTrigger, sched
|
19
|
+
assert_equal exp, sched.get_cron_expression
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: secondhand
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Don Morrison
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-12-13 00:00:00 -08:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description:
|
22
|
+
email: elskwid@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files:
|
28
|
+
- README.md
|
29
|
+
files:
|
30
|
+
- LICENSE
|
31
|
+
- Rakefile
|
32
|
+
- README.md
|
33
|
+
- test/test_helper.rb
|
34
|
+
- test/integration/test_secondhand_run.rb
|
35
|
+
- test/unit/test_job.rb
|
36
|
+
- test/unit/test_jobs.rb
|
37
|
+
- test/unit/test_logger.rb
|
38
|
+
- test/unit/test_scheduler.rb
|
39
|
+
- test/unit/test_secondhand.rb
|
40
|
+
- test/unit/test_trigger.rb
|
41
|
+
- lib/secondhand.rb
|
42
|
+
- lib/quartz/log4j-1.2.14.jar
|
43
|
+
- lib/quartz/quartz-1.8.4.jar
|
44
|
+
- lib/quartz/slf4j-api-1.6.0.jar
|
45
|
+
- lib/quartz/slf4j-log4j12-1.6.0.jar
|
46
|
+
- lib/secondhand/job.rb
|
47
|
+
- lib/secondhand/jobs.rb
|
48
|
+
- lib/secondhand/logging.rb
|
49
|
+
- lib/secondhand/scheduler.rb
|
50
|
+
- lib/secondhand/trigger.rb
|
51
|
+
- lib/secondhand/version.rb
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: https://github.com/amphora/secondhand
|
54
|
+
licenses: []
|
55
|
+
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options:
|
58
|
+
- --main
|
59
|
+
- README.md
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
requirements: []
|
77
|
+
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 1.3.6
|
80
|
+
signing_key:
|
81
|
+
specification_version: 3
|
82
|
+
summary: Secondhand is a ruby-friendly wrapper around the Quartz Job Scheduler. Create jobs, tell Secondhand when they should run, then sit back and enjoy.
|
83
|
+
test_files: []
|
84
|
+
|