rocketjob 1.2.1 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 201f922d37f2533e56109c053e3c9f4bdef30108
4
- data.tar.gz: 00598e8fb3039d7ce629f629bf270d3cc06f2e87
3
+ metadata.gz: 538bb39f185138c3fc5c4e007ef33470da129cfd
4
+ data.tar.gz: d0edc858039527f0fb33707bd563c3dacce0a968
5
5
  SHA512:
6
- metadata.gz: 447f63cf687c4e7ac10189aff59730874ea25f3e57b7d6ca301fd4a34178e91d68d4428f5be095c2a6a4a5426d603262c3d82c78e9f3988a9ed35d76bf75bddf
7
- data.tar.gz: e6a54a1b72cb2c7192894140eea4e49e42dc29049a85a77338de773e539c23a5e1ed09a5691a0a5c238a881e99398cb07b99eddf5abdda8e044971cf433766f3
6
+ metadata.gz: 73581f734996afa564614e434015ed6400461e755607b8af6652a29eee120d318415110058b9014e6224b15b594933a12d79ee023ec8feaf8c59b6335f173392
7
+ data.tar.gz: 896bc0dbfece93ff239aa56475a733bf1b310792e47d89be8bc17dce4c00e04ab426ba3532ad7cc1a864dc0f4a0eb280f8a177899a0ff1ec77f33d160adb0b3d
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # rocketjob [![Gem Version](https://badge.fury.io/rb/rocketjob.svg)](http://badge.fury.io/rb/rocketjob) [![Build Status](https://secure.travis-ci.org/rocketjob/rocketjob.png?branch=master)](http://travis-ci.org/rocketjob/rocketjob) ![](http://ruby-gem-downloads-badge.herokuapp.com/rocketjob?type=total)
2
2
 
3
- High volume, priority based, distributed, background job processing solution for Ruby.
3
+ Enterprise Batch Processing System focused on performance, scalability, reliability, and visibility of every job in the system.
4
+
5
+ Outgrown existing solutions? Or, start small and scale up later.
6
+
7
+ Works with or without Rails.
4
8
 
5
9
  ## Status
6
10
 
@@ -22,19 +22,7 @@ module RocketJob
22
22
 
23
23
  # Create a job and process it immediately in-line by this thread
24
24
  def now(method, *args, &block)
25
- job = build(method, *args, &block)
26
- # Call validations
27
- if job.respond_to?(:validate!)
28
- job.validate!
29
- elsif job.invalid?
30
- raise(MongoMapper::DocumentNotValid, "Validation failed: #{job.errors.messages.join(', ')}")
31
- end
32
- worker = RocketJob::Worker.new(name: 'inline')
33
- worker.started
34
- job.start
35
- while job.running? && !job.work(worker)
36
- end
37
- job
25
+ build(method, *args, &block).work_now
38
26
  end
39
27
 
40
28
  # Build a Rocket Job instance
@@ -134,6 +122,8 @@ module RocketJob
134
122
  begin
135
123
  # before_perform
136
124
  call_method(perform_method, arguments, event: :before, log_level: log_level)
125
+ # Allow before perform to explicitly fail this job
126
+ return unless running?
137
127
 
138
128
  # perform
139
129
  ret = call_method(perform_method, arguments, log_level: log_level)
@@ -141,18 +131,39 @@ module RocketJob
141
131
  self.result = (ret.is_a?(Hash) || ret.is_a?(BSON::OrderedHash)) ? ret : {result: ret}
142
132
  end
143
133
 
134
+ # Only run after perform if perform did not explicitly fail the job
135
+ return unless running?
136
+
144
137
  # after_perform
145
138
  call_method(perform_method, arguments, event: :after, log_level: log_level)
146
139
 
147
- complete!
140
+ new_record? ? complete : complete!
148
141
  rescue StandardError => exc
149
- fail!(worker.name, exc) unless failed?
142
+ fail(worker.name, exc) if may_fail?
150
143
  logger.error("Exception running #{self.class.name}##{perform_method}", exc)
144
+ save! unless new_record?
151
145
  raise exc if RocketJob::Config.inline_mode
152
146
  end
153
147
  false
154
148
  end
155
149
 
150
+ # Validates and runs the work on this job now in the current thread
151
+ # Returns this job once it has finished running
152
+ def work_now
153
+ # Call validations
154
+ if respond_to?(:validate!)
155
+ validate!
156
+ elsif invalid?
157
+ raise(MongoMapper::DocumentNotValid, "Validation failed: #{errors.messages.join(', ')}")
158
+ end
159
+ worker = RocketJob::Worker.new(name: 'inline')
160
+ worker.started
161
+ start if may_start?
162
+ while running? && !work(worker)
163
+ end
164
+ self
165
+ end
166
+
156
167
  protected
157
168
 
158
169
  # Calls a method on this job, if it is defined
@@ -1,18 +1,16 @@
1
1
  # encoding: UTF-8
2
- require 'sync_attr'
3
2
  module RocketJob
4
3
  # Centralized Configuration for Rocket Jobs
5
4
  class Config
6
5
  include MongoMapper::Document
7
- include SyncAttr
8
6
 
9
7
  # Prevent data in MongoDB from re-defining the model behavior
10
8
  #self.static_keys = true
11
9
 
12
10
  # Returns the single instance of the Rocket Job Configuration for this site
13
11
  # in a thread-safe way
14
- sync_cattr_reader(:instance) do
15
- begin
12
+ def self.instance
13
+ @@instance ||= begin
16
14
  first || create
17
15
  rescue StandardError
18
16
  # In case another process has already created the first document
@@ -22,7 +20,7 @@ module RocketJob
22
20
 
23
21
  # By enabling inline_mode jobs will be called in-line
24
22
  # No worker processes will be created, nor threads created
25
- sync_cattr_accessor(:inline_mode) { false }
23
+ cattr_accessor(:inline_mode) { false }
26
24
 
27
25
  # @formatter:off
28
26
  # The maximum number of worker threads to create on any one worker
@@ -280,13 +280,16 @@ module RocketJob
280
280
 
281
281
  # Queues the job for the supplied pathname
282
282
  def later(pathname)
283
- job_class.perform_later(*arguments) do |job|
284
- job.perform_method = perform_method
285
- # Set properties
286
- properties.each_pair { |k, v| job.send("#{k}=".to_sym, v) }
287
-
288
- upload_file(job, pathname)
289
- end
283
+ job = job_class.new(
284
+ properties.merge(
285
+ arguments: arguments,
286
+ properties: properties,
287
+ perform_method: perform_method
288
+ )
289
+ )
290
+ upload_file(job, pathname)
291
+ job.save!
292
+ job
290
293
  end
291
294
 
292
295
  protected
@@ -315,9 +318,14 @@ module RocketJob
315
318
 
316
319
  # Archives the file for a job where there was no #file_store_upload or #upload method
317
320
  def upload_default(job, pathname)
318
- # The first argument must be a hash
319
- job.arguments << {} if job.arguments.size == 0
320
- job.arguments.first[:full_file_name] = archive_file(job, pathname)
321
+ full_file_name = archive_file(job, pathname)
322
+ if job.respond_to?(:full_file_name=)
323
+ job.full_file_name = full_file_name
324
+ elsif job.arguments.first.is_a?(Hash)
325
+ job.arguments.first[:full_file_name] = full_file_name
326
+ else
327
+ raise(ArgumentError, "#{job_class_name} must either have attribute 'full_file_name' or the first argument must be a Hash")
328
+ end
321
329
  end
322
330
 
323
331
  # Move the file to the archive directory
@@ -120,6 +120,20 @@ module RocketJob
120
120
 
121
121
  validates_presence_of :state, :failure_count, :created_at, :perform_method
122
122
  validates :priority, inclusion: 1..100
123
+ validates :log_level, inclusion: SemanticLogger::LEVELS + [nil]
124
+
125
+ # User definable properties in Dirmon Entry
126
+ def self.rocket_job_properties
127
+ @rocket_job_properties ||= (self == RocketJob::Job ? [] : superclass.rocket_job_properties)
128
+ end
129
+
130
+ # Add to user definable properties in Dirmon Entry
131
+ def self.public_rocket_job_properties(*properties)
132
+ rocket_job_properties.concat(properties).uniq!
133
+ end
134
+
135
+ # User definable properties in Dirmon Entry
136
+ public_rocket_job_properties :description, :priority, :perform_method, :log_level, :arguments
123
137
 
124
138
  # State Machine events and transitions
125
139
  #
@@ -41,27 +41,29 @@ module RocketJob
41
41
 
42
42
  # Number of seconds between directory scans. Default 5 mins
43
43
  key :check_seconds, Float, default: 300.0
44
+ key :previous_file_names, Hash # Hash[file_name, size]
44
45
 
45
46
  # Iterate over each Dirmon entry looking for new files
46
47
  # If a new file is found, it is not processed immediately, instead
47
48
  # it is passed to the next run of this job along with the file size.
48
49
  # If the file size has not changed, the Job is kicked off.
49
- def perform(previous_file_names={})
50
- new_file_names = check_directories(previous_file_names)
50
+ def perform
51
+ check_directories
51
52
  ensure
52
53
  # Run again in the future, even if this run fails with an exception
53
- self.class.perform_later(new_file_names || previous_file_names) do |job|
54
- job.priority = priority
55
- job.check_seconds = check_seconds
56
- job.run_at = Time.now + check_seconds
57
- end
54
+ self.class.create!(
55
+ previous_file_names: previous_file_names,
56
+ priority: priority,
57
+ check_seconds: check_seconds,
58
+ run_at: Time.now + check_seconds
59
+ )
58
60
  end
59
61
 
60
62
  protected
61
63
 
62
64
  # Checks the directories for new files, starting jobs if files have not changed
63
65
  # since the last run
64
- def check_directories(previous_file_names)
66
+ def check_directories
65
67
  new_file_names = {}
66
68
  DirmonEntry.where(state: :enabled).each do |entry|
67
69
  entry.each do |pathname|
@@ -73,7 +75,7 @@ module RocketJob
73
75
  end
74
76
  end
75
77
  end
76
- new_file_names
78
+ self.previous_file_names = new_file_names
77
79
  end
78
80
 
79
81
  # Checks if a file should result in starting a job
@@ -1,4 +1,4 @@
1
1
  # encoding: UTF-8
2
2
  module RocketJob #:nodoc
3
- VERSION = '1.2.1'
3
+ VERSION = '1.3.0'
4
4
  end
@@ -1,6 +1,5 @@
1
1
  # encoding: UTF-8
2
2
  require 'socket'
3
- require 'sync_attr'
4
3
  require 'aasm'
5
4
  module RocketJob
6
5
  # Worker
@@ -30,7 +29,6 @@ module RocketJob
30
29
  class Worker
31
30
  include MongoMapper::Document
32
31
  include AASM
33
- include SyncAttr
34
32
  include SemanticLogger::Loggable
35
33
 
36
34
  # Prevent data in MongoDB from re-defining the model behavior
@@ -164,7 +162,7 @@ module RocketJob
164
162
  @thread_pool ||= []
165
163
  end
166
164
 
167
- # Run this instance of the worker
165
+ # Management Thread
168
166
  def run
169
167
  Thread.current.name = 'rocketjob main'
170
168
  build_heartbeat unless heartbeat
@@ -182,7 +180,7 @@ module RocketJob
182
180
  'heartbeat.current_threads' => thread_pool_count
183
181
  )
184
182
 
185
- # Reload the worker model every 10 heartbeats in case its config was changed
183
+ # Reload the worker model every few heartbeats in case its config was changed
186
184
  # TODO make 3 configurable
187
185
  if count >= 3
188
186
  reload
@@ -316,11 +314,13 @@ module RocketJob
316
314
  RocketJob::Job.requeue_dead_worker(name)
317
315
  end
318
316
 
319
- # Mutex protected shutdown indicator
320
- sync_cattr_accessor :shutdown do
321
- false
317
+ # Shutdown indicator
318
+ def self.shutdown
319
+ @@shutdown
322
320
  end
323
321
 
322
+ @@shutdown = false
323
+
324
324
  # Register handlers for the various signals
325
325
  # Term:
326
326
  # Perform clean shutdown
@@ -3,6 +3,16 @@ require_relative 'jobs/test_job'
3
3
 
4
4
  # Unit Test for RocketJob::Job
5
5
  class DirmonEntryTest < Minitest::Test
6
+ class WithFullFileNameJob < RocketJob::Job
7
+ # Dirmon will store the filename in this property when starting the job
8
+ key :full_file_name, String
9
+
10
+ def perform
11
+ # Do something with the file name stored in :full_file_name
12
+ end
13
+ end
14
+
15
+
6
16
  describe RocketJob::DirmonEntry do
7
17
  describe '.config' do
8
18
  it 'support multiple databases' do
@@ -186,11 +196,17 @@ class DirmonEntryTest < Minitest::Test
186
196
  @entry = RocketJob::DirmonEntry.new(
187
197
  pattern: 'test/files/**/*',
188
198
  job_class_name: 'Jobs::TestJob',
189
- arguments: [{input: 'yes'}],
199
+ arguments: [{}],
190
200
  properties: {priority: 23, perform_method: :event},
191
201
  archive_directory: @archive_directory
192
202
  )
193
- @job = Jobs::TestJob.new
203
+ @job = Jobs::TestJob.new(
204
+ @entry.properties.merge(
205
+ arguments: @entry.arguments,
206
+ properties: @entry.properties,
207
+ perform_method: @entry.perform_method
208
+ )
209
+ )
194
210
  @file = Tempfile.new('archive')
195
211
  @file_name = @file.path
196
212
  @pathname = Pathname.new(@file_name)
@@ -222,10 +238,30 @@ class DirmonEntryTest < Minitest::Test
222
238
  end
223
239
 
224
240
  describe '#upload_default' do
225
- it 'upload' do
241
+ it 'sets full_file_name in Hash argument' do
226
242
  @entry.send(:upload_default, @job, @pathname)
227
243
  assert_equal @archive_real_name, @job.arguments.first[:full_file_name], @job.arguments
228
244
  end
245
+
246
+ it 'sets full_file_name property' do
247
+ @entry = RocketJob::DirmonEntry.new(
248
+ pattern: 'test/files/**/*',
249
+ job_class_name: 'DirmonEntryTest::WithFullFileNameJob',
250
+ archive_directory: @archive_directory
251
+ )
252
+ assert @entry.valid?, @entry.errors.messages
253
+ job = @entry.job_class.new
254
+ @entry.send(:upload_default, job, @pathname)
255
+ archive_real_name = @archive_path.join("#{job.id}_#{File.basename(@file_name)}").to_s
256
+ assert_equal archive_real_name, job.full_file_name, job.arguments
257
+ end
258
+
259
+ it 'handles non hash argument and missing property' do
260
+ @job.arguments = [1]
261
+ assert_raises ArgumentError do
262
+ @entry.send(:upload_default, @job, @pathname)
263
+ end
264
+ end
229
265
  end
230
266
 
231
267
  describe '#upload_file' do
@@ -73,8 +73,7 @@ class DirmonJobTest < Minitest::Test
73
73
  end
74
74
 
75
75
  it 'no files' do
76
- previous_file_names = {}
77
- result = @dirmon_job.send(:check_directories, previous_file_names)
76
+ result = @dirmon_job.send(:check_directories)
78
77
  assert_equal 0, result.count
79
78
  end
80
79
 
@@ -82,37 +81,40 @@ class DirmonJobTest < Minitest::Test
82
81
  create_file("#{@directory}/abc/file1", 5)
83
82
  create_file("#{@directory}/abc/file2", 10)
84
83
 
85
- previous_file_names = {}
86
- result = @dirmon_job.send(:check_directories, previous_file_names)
87
- assert_equal 2, result.count, result.inspect
88
- assert_equal 5, result.values.first, result.inspect
89
- assert_equal 10, result.values.second, result.inspect
84
+ result = @dirmon_job.send(:check_directories)
85
+ assert_equal 2, result.count, result
86
+ assert_equal 5, result.values.first, result
87
+ assert_equal 10, result.values.second, result
90
88
  end
91
89
 
92
90
  it 'allow files to grow' do
93
91
  create_file("#{@directory}/abc/file1", 5)
94
92
  create_file("#{@directory}/abc/file2", 10)
95
- previous_file_names = {}
96
- @dirmon_job.send(:check_directories, previous_file_names)
93
+ @dirmon_job.send(:check_directories)
97
94
  create_file("#{@directory}/abc/file1", 10)
98
95
  create_file("#{@directory}/abc/file2", 15)
99
- result = @dirmon_job.send(:check_directories, previous_file_names)
100
- assert_equal 2, result.count, result.inspect
101
- assert_equal 10, result.values.first, result.inspect
102
- assert_equal 15, result.values.second, result.inspect
96
+ result = @dirmon_job.send(:check_directories)
97
+ assert_equal 2, result.count, result
98
+ assert_equal 10, result.values.first, result
99
+ assert_equal 15, result.values.second, result
103
100
  end
104
101
 
105
102
  it 'start all files' do
106
103
  create_file("#{@directory}/abc/file1", 5)
107
104
  create_file("#{@directory}/abc/file2", 10)
108
- previous_file_names = @dirmon_job.send(:check_directories, {})
105
+ files = @dirmon_job.send(:check_directories)
106
+ assert_equal 2, files.count, files
107
+ assert_equal 2, @dirmon_job.previous_file_names.count, files
108
+
109
+ # files = @dirmon_job.send(:check_directories)
110
+ # assert_equal 0, files.count, files
109
111
 
110
112
  count = 0
111
113
  result = RocketJob::DirmonEntry.stub_any_instance(:later, -> path { count += 1 }) do
112
- @dirmon_job.send(:check_directories, previous_file_names)
114
+ @dirmon_job.send(:check_directories)
113
115
  end
116
+ assert_equal 0, result.count, result
114
117
  assert 2, count
115
- assert_equal 0, result.count, result.inspect
116
118
  end
117
119
 
118
120
  it 'skip files in archive directory' do
@@ -125,11 +127,11 @@ class DirmonJobTest < Minitest::Test
125
127
  FileUtils.makedirs(@entry.archive_pathname(file_pathname))
126
128
  create_file("#{@entry.archive_pathname(file_pathname)}/file3", 10)
127
129
 
128
- result = @dirmon_job.send(:check_directories, {})
130
+ result = @dirmon_job.send(:check_directories)
129
131
 
130
- assert_equal 2, result.count, result.inspect
131
- assert_equal 5, result.values.first, result.inspect
132
- assert_equal 10, result.values.second, result.inspect
132
+ assert_equal 2, result.count, result
133
+ assert_equal 5, result.values.first, result
134
+ assert_equal 10, result.values.second, result
133
135
  end
134
136
  end
135
137
 
@@ -147,10 +149,12 @@ class DirmonJobTest < Minitest::Test
147
149
  RocketJob::Jobs::DirmonJob.destroy_all
148
150
  RocketJob::Jobs::DirmonJob.stub_any_instance(:check_directories, new_file_names) do
149
151
  # perform_now does not save the job, just runs it
150
- dirmon_job = RocketJob::Jobs::DirmonJob.perform_now(previous_file_names) do |job|
151
- job.priority = 11
152
- job.check_seconds = 30
153
- end
152
+ dirmon_job = RocketJob::Jobs::DirmonJob.new(
153
+ previous_file_names: previous_file_names,
154
+ priority: 11,
155
+ check_seconds: 30
156
+ )
157
+ dirmon_job.work_now
154
158
  end
155
159
  assert dirmon_job.completed?, dirmon_job.status.inspect
156
160
 
@@ -171,14 +175,15 @@ class DirmonJobTest < Minitest::Test
171
175
  RocketJob::Jobs::DirmonJob.destroy_all
172
176
  RocketJob::Jobs::DirmonJob.stub_any_instance(:check_directories, -> previous { raise RuntimeError.new("Oh no") }) do
173
177
  # perform_now does not save the job, just runs it
174
- dirmon_job = RocketJob::Jobs::DirmonJob.perform_now do |job|
175
- job.priority = 11
176
- job.check_seconds = 30
177
- end
178
+ dirmon_job = RocketJob::Jobs::DirmonJob.create!(
179
+ priority: 11,
180
+ check_seconds: 30
181
+ )
182
+ dirmon_job.work_now
178
183
  end
179
184
  assert dirmon_job.failed?, dirmon_job.status.inspect
180
185
 
181
- # It it have enqueued another instance to run in the future
186
+ # Must have enqueued another instance to run in the future
182
187
  assert_equal 2, RocketJob::Jobs::DirmonJob.count
183
188
  assert new_dirmon_job = RocketJob::Jobs::DirmonJob.last
184
189
  assert new_dirmon_job.run_at
data/test/job_test.rb CHANGED
@@ -154,7 +154,8 @@ class JobTest < Minitest::Test
154
154
  @job.destroy_on_complete = true
155
155
  @job.start!
156
156
  assert_equal false, @job.work(@worker)
157
- assert_equal nil, RocketJob::Job.find_by_id(@job.id)
157
+ assert @job.completed?, @job.state
158
+ assert_equal 0, RocketJob::Job.where(id: @job.id).count
158
159
  end
159
160
 
160
161
  it 'silence logging when log_level is set' do
@@ -254,8 +255,9 @@ class JobTest < Minitest::Test
254
255
  it 'requeue jobs from dead workers' do
255
256
  worker_name = 'server:12345'
256
257
  @job.worker_name = worker_name
258
+ assert @job.valid?, @job.errors.messages
257
259
  @job.start!
258
- assert @job.running?
260
+ assert @job.running?, @job.state
259
261
 
260
262
  @job.requeue
261
263
  assert @job.queued?
@@ -273,7 +275,7 @@ class JobTest < Minitest::Test
273
275
  worker_name = 'server:12345'
274
276
  @job.worker_name = worker_name
275
277
  @job.start!
276
- assert @job.running?
278
+ assert @job.running?, @job.state
277
279
 
278
280
  worker_name2 = 'server:76467'
279
281
  @job2.worker_name = worker_name2
data/test/test_helper.rb CHANGED
@@ -8,7 +8,13 @@ require 'rocketjob'
8
8
  require 'awesome_print'
9
9
  require 'symmetric-encryption'
10
10
 
11
- Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
11
+ if ENV['DETAILED_TESTS'].present?
12
+ # See every test and how long it took
13
+ MiniTest::Reporters.use! MiniTest::Reporters::SpecReporter.new
14
+ else
15
+ # Only show failed tests
16
+ MiniTest::Reporters.use! MiniTest::Reporters::ProgressReporter.new
17
+ end
12
18
 
13
19
  SemanticLogger.add_appender('test.log', &SemanticLogger::Appender::Base.colorized_formatter)
14
20
  SemanticLogger.default_level = :debug
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rocketjob
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-15 00:00:00.000000000 Z
11
+ date: 2015-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aasm
@@ -94,22 +94,8 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.0'
97
- - !ruby/object:Gem::Dependency
98
- name: sync_attr
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '2.0'
104
- type: :runtime
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: '2.0'
111
- description: Next generation, high performance, priority based, distributed, background
112
- job processing solution
97
+ description: Enterprise Batch Processing System focused on performance, scalability,
98
+ reliability, and visibility of every job in the system.
113
99
  email:
114
100
  - reidmo@gmail.com
115
101
  executables:
@@ -165,7 +151,7 @@ rubyforge_project:
165
151
  rubygems_version: 2.4.5.1
166
152
  signing_key:
167
153
  specification_version: 4
168
- summary: Next generation background job processing system for Ruby, JRuby and Rubinius
154
+ summary: Enterprise Batch Processing System for Ruby, JRuby, and Rubinius
169
155
  test_files:
170
156
  - test/config/mongo.yml
171
157
  - test/dirmon_entry_test.rb