rocketjob 3.4.3 → 3.5.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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +27 -0
  3. data/bin/rocketjob +1 -1
  4. data/lib/rocket_job/active_worker.rb +4 -3
  5. data/lib/rocket_job/cli.rb +13 -12
  6. data/lib/rocket_job/config.rb +17 -13
  7. data/lib/rocket_job/dirmon_entry.rb +88 -91
  8. data/lib/rocket_job/extensions/mongo/logging.rb +8 -4
  9. data/lib/rocket_job/extensions/mongoid/factory.rb +8 -6
  10. data/lib/rocket_job/extensions/rocket_job_adapter.rb +2 -4
  11. data/lib/rocket_job/heartbeat.rb +7 -8
  12. data/lib/rocket_job/job_exception.rb +6 -5
  13. data/lib/rocket_job/jobs/dirmon_job.rb +5 -7
  14. data/lib/rocket_job/jobs/housekeeping_job.rb +3 -2
  15. data/lib/rocket_job/jobs/on_demand_job.rb +104 -0
  16. data/lib/rocket_job/jobs/simple_job.rb +0 -2
  17. data/lib/rocket_job/jobs/upload_file_job.rb +100 -0
  18. data/lib/rocket_job/performance.rb +15 -10
  19. data/lib/rocket_job/plugins/cron.rb +7 -124
  20. data/lib/rocket_job/plugins/document.rb +8 -10
  21. data/lib/rocket_job/plugins/job/callbacks.rb +0 -1
  22. data/lib/rocket_job/plugins/job/logger.rb +0 -1
  23. data/lib/rocket_job/plugins/job/model.rb +15 -20
  24. data/lib/rocket_job/plugins/job/persistence.rb +3 -13
  25. data/lib/rocket_job/plugins/job/state_machine.rb +1 -2
  26. data/lib/rocket_job/plugins/job/throttle.rb +16 -12
  27. data/lib/rocket_job/plugins/job/worker.rb +15 -19
  28. data/lib/rocket_job/plugins/processing_window.rb +2 -2
  29. data/lib/rocket_job/plugins/restart.rb +3 -4
  30. data/lib/rocket_job/plugins/retry.rb +2 -3
  31. data/lib/rocket_job/plugins/singleton.rb +2 -3
  32. data/lib/rocket_job/plugins/state_machine.rb +19 -23
  33. data/lib/rocket_job/rocket_job.rb +4 -5
  34. data/lib/rocket_job/server.rb +35 -41
  35. data/lib/rocket_job/version.rb +2 -2
  36. data/lib/rocket_job/worker.rb +22 -21
  37. data/lib/rocketjob.rb +2 -0
  38. data/test/config/mongoid.yml +2 -2
  39. data/test/config_test.rb +0 -2
  40. data/test/dirmon_entry_test.rb +161 -134
  41. data/test/dirmon_job_test.rb +80 -78
  42. data/test/job_test.rb +0 -2
  43. data/test/jobs/housekeeping_job_test.rb +0 -1
  44. data/test/jobs/on_demand_job_test.rb +59 -0
  45. data/test/jobs/upload_file_job_test.rb +99 -0
  46. data/test/plugins/cron_test.rb +1 -3
  47. data/test/plugins/job/callbacks_test.rb +8 -13
  48. data/test/plugins/job/defaults_test.rb +0 -1
  49. data/test/plugins/job/logger_test.rb +2 -4
  50. data/test/plugins/job/model_test.rb +1 -2
  51. data/test/plugins/job/persistence_test.rb +0 -2
  52. data/test/plugins/job/state_machine_test.rb +0 -2
  53. data/test/plugins/job/throttle_test.rb +6 -8
  54. data/test/plugins/job/worker_test.rb +1 -2
  55. data/test/plugins/processing_window_test.rb +0 -2
  56. data/test/plugins/restart_test.rb +0 -1
  57. data/test/plugins/retry_test.rb +1 -2
  58. data/test/plugins/singleton_test.rb +0 -2
  59. data/test/plugins/state_machine_event_callbacks_test.rb +1 -2
  60. data/test/plugins/state_machine_test.rb +0 -2
  61. data/test/plugins/transaction_test.rb +5 -7
  62. data/test/test_db.sqlite3 +0 -0
  63. data/test/test_helper.rb +2 -1
  64. metadata +42 -36
@@ -4,22 +4,26 @@ module Mongo
4
4
  class Monitoring
5
5
  class CommandLogSubscriber
6
6
  include SemanticLogger::Loggable
7
- self.logger.name = 'Mongo'
7
+ logger.name = 'Mongo'
8
8
 
9
9
  def started(event)
10
10
  @event_command = event.command
11
11
  end
12
12
 
13
13
  def succeeded(event)
14
- logger.debug(message: prefix(event), duration: (event.duration * 1000), payload: @event_command)
14
+ logger.debug(message: prefix(event),
15
+ duration: (event.duration * 1000),
16
+ payload: @event_command)
15
17
  end
16
18
 
17
19
  def failed(event)
18
- logger.debug(message: "#{prefix(event)} Failed: #{event.message}", duration: (event.duration * 1000), payload: @event_command)
20
+ logger.debug(message: "#{prefix(event)} Failed: #{event.message}",
21
+ duration: (event.duration * 1000),
22
+ payload: @event_command)
19
23
  end
20
24
 
21
25
  def prefix(event)
22
- "#{event.address.to_s} | #{event.database_name}.#{event.command_name}"
26
+ "#{event.address} | #{event.database_name}.#{event.command_name}"
23
27
  end
24
28
  end
25
29
  end
@@ -1,13 +1,15 @@
1
1
  require 'mongoid/factory'
2
2
 
3
3
  module RocketJob
4
- module MongoidFactory
5
- def from_db(*args)
6
- super(*args)
7
- rescue NameError => exc
8
- RocketJob::Job.instantiate(attributes, selected_fields)
4
+ module Mongoid
5
+ module Factory
6
+ def from_db(*args)
7
+ super(*args)
8
+ rescue NameError
9
+ RocketJob::Job.instantiate(attributes, selected_fields)
10
+ end
9
11
  end
10
12
  end
11
13
  end
12
14
 
13
- ::Mongoid::Factory.include(RocketJob::MongoidFactory)
15
+ ::Mongoid::Factory.extend(RocketJob::Mongoid::Factory)
@@ -70,20 +70,18 @@ module ActiveJob
70
70
  job
71
71
  end
72
72
 
73
- private
74
-
75
73
  def self.active_job_params(active_job)
76
74
  params = {
77
75
  description: active_job.class.name,
78
76
  data: active_job.serialize,
79
77
  active_job_id: active_job.job_id,
80
78
  active_job_class: active_job.class.name,
81
- active_job_queue: active_job.queue_name,
79
+ active_job_queue: active_job.queue_name
82
80
  }
83
81
  params[:priority] = active_job.priority if active_job.respond_to?(:priority) && active_job.priority
84
82
  params
85
83
  end
86
-
84
+ private_class_method :active_job_params
87
85
  end
88
86
  end
89
87
  end
@@ -16,23 +16,22 @@ module RocketJob
16
16
  #
17
17
 
18
18
  # Percentage utilization for the server process alone
19
- #field :process_cpu, type: Integer
19
+ # field :process_cpu, type: Integer
20
20
  # Kilo Bytes used by the server process (Virtual & Physical)
21
- #field :process_mem_phys_kb, type: Integer
22
- #field :process_mem_virt_kb, type: Integer
21
+ # field :process_mem_phys_kb, type: Integer
22
+ # field :process_mem_virt_kb, type: Integer
23
23
 
24
24
  #
25
25
  # System Information. Future.
26
26
  #
27
27
 
28
28
  # Percentage utilization for the host machine
29
- #field :host_cpu, type: Integer
29
+ # field :host_cpu, type: Integer
30
30
  # Kilo Bytes Available on the host machine (Physical)
31
- #field :host_mem_avail_phys_kbytes, type: Float
32
- #field :host_mem_avail_virt_kbytes, type: Float
31
+ # field :host_mem_avail_phys_kbytes, type: Float
32
+ # field :host_mem_avail_virt_kbytes, type: Float
33
33
 
34
34
  # If available
35
- #field :load_average, type: Float
35
+ # field :load_average, type: Float
36
36
  end
37
37
  end
38
-
@@ -22,13 +22,14 @@ module RocketJob
22
22
  field :record_number, type: Integer
23
23
 
24
24
  # Returns [JobException] built from the supplied exception
25
- def self.from_exception(exc)
25
+ def self.from_exception(exc, **args)
26
26
  new(
27
- class_name: exc.class.name,
28
- message: exc.message,
29
- backtrace: exc.backtrace || []
27
+ args.merge(
28
+ class_name: exc.class.name,
29
+ message: exc.message,
30
+ backtrace: exc.backtrace || []
31
+ )
30
32
  )
31
33
  end
32
-
33
34
  end
34
35
  end
@@ -40,7 +40,7 @@ module RocketJob
40
40
  # Start a new job when this one completes, fails, or aborts
41
41
  include RocketJob::Plugins::Restart
42
42
 
43
- self.priority = 40
43
+ self.priority = 30
44
44
 
45
45
  # Number of seconds between directory scans. Default 5 mins
46
46
  field :check_seconds, type: Float, default: 300.0, copy_on_restart: true
@@ -72,11 +72,10 @@ module RocketJob
72
72
  DirmonEntry.enabled.each do |entry|
73
73
  entry.each do |pathname|
74
74
  # BSON Keys cannot contain periods
75
- key = pathname.to_s.gsub('.', '_')
76
- previous_size = previous_file_names[key]
77
- if size = check_file(entry, pathname, previous_size)
78
- new_file_names[key] = size
79
- end
75
+ key = pathname.to_s.tr('.', '_')
76
+ previous_size = previous_file_names[key]
77
+ size = check_file(entry, pathname, previous_size)
78
+ new_file_names[key] = size if size
80
79
  end
81
80
  end
82
81
  self.previous_file_names = new_file_names
@@ -96,7 +95,6 @@ module RocketJob
96
95
  size
97
96
  end
98
97
  end
99
-
100
98
  end
101
99
  end
102
100
  end
@@ -54,12 +54,13 @@ module RocketJob
54
54
  end
55
55
 
56
56
  RocketJob::Job.aborted.where(completed_at: {'$lte' => aborted_retention.seconds.ago}).destroy_all if aborted_retention
57
- RocketJob::Job.completed.where(completed_at: {'$lte' => completed_retention.seconds.ago}).destroy_all if completed_retention
57
+ if completed_retention
58
+ RocketJob::Job.completed.where(completed_at: {'$lte' => completed_retention.seconds.ago}).destroy_all
59
+ end
58
60
  RocketJob::Job.failed.where(completed_at: {'$lte' => failed_retention.seconds.ago}).destroy_all if failed_retention
59
61
  RocketJob::Job.paused.where(completed_at: {'$lte' => paused_retention.seconds.ago}).destroy_all if paused_retention
60
62
  RocketJob::Job.queued.where(created_at: {'$lte' => queued_retention.seconds.ago}).destroy_all if queued_retention
61
63
  end
62
-
63
64
  end
64
65
  end
65
66
  end
@@ -0,0 +1,104 @@
1
+ # Generalized Job.
2
+ #
3
+ # Create or schedule a generalized job for one off fixes or cleanups.
4
+ #
5
+ # Example: Iterate over all rows in a table:
6
+ # code = <<~CODE
7
+ # User.unscoped.all.order('updated_at DESC').each |user|
8
+ # user.cleanse_attributes!
9
+ # user.save!
10
+ # end
11
+ # CODE
12
+ #
13
+ # RocketJob::Jobs::OnDemandJob.create!(
14
+ # code: code,
15
+ # description: 'Cleanse users'
16
+ # )
17
+ #
18
+ # Example: Test job in a console:
19
+ # code = <<~CODE
20
+ # User.unscoped.all.order('updated_at DESC').each |user|
21
+ # user.cleanse_attributes!
22
+ # user.save!
23
+ # end
24
+ # CODE
25
+ #
26
+ # job = RocketJob::Jobs::OnDemandJob.new(code: code, description: 'cleanse users')
27
+ # job.perform_now
28
+ #
29
+ # Example: Pass input data:
30
+ # code = <<~CODE
31
+ # puts data['a'] * data['b']
32
+ # CODE
33
+ #
34
+ # RocketJob::Jobs::OnDemandJob.create!(
35
+ # code: code,
36
+ # data: {'a' => 10, 'b' => 2}
37
+ # )
38
+ #
39
+ # Example: Retain output:
40
+ # code = <<~CODE
41
+ # {'value' => data['a'] * data['b']}
42
+ # CODE
43
+ #
44
+ # RocketJob::Jobs::OnDemandJob.create!(
45
+ # code: code,
46
+ # collect_output: true,
47
+ # data: {'a' => 10, 'b' => 2}
48
+ # )
49
+ #
50
+ # Example: Schedule the job to run nightly at 2am Eastern:
51
+ #
52
+ # RocketJob::Jobs::OnDemandJob.create!(
53
+ # cron_schedule: '0 2 * * * America/New_York',
54
+ # code: code
55
+ # )
56
+ #
57
+ # Example: Change the job priority, description, etc.
58
+ #
59
+ # RocketJob::Jobs::OnDemandJob.create!(
60
+ # code: code,
61
+ # description: 'Cleanse users',
62
+ # priority: 30
63
+ # )
64
+ #
65
+ # Example: Automatically retry up to 5 times on failure:
66
+ #
67
+ # RocketJob::Jobs::OnDemandJob.create!(
68
+ # retry_limit: 5
69
+ # code: code
70
+ # )
71
+ module RocketJob
72
+ module Jobs
73
+ class OnDemandJob < RocketJob::Job
74
+ include RocketJob::Plugins::Cron
75
+ include RocketJob::Plugins::Retry
76
+
77
+ self.priority = 90
78
+ self.description = 'Generalized Job'
79
+ self.destroy_on_complete = false
80
+ self.retry_limit = 0
81
+
82
+ # Be sure to store key names only as Strings, not Symbols
83
+ field :data, type: Hash, default: {}
84
+ field :code, type: String
85
+
86
+ validates :code, presence: true
87
+ validates_each :code do |job, attr, _value|
88
+ begin
89
+ job.send(:load_code)
90
+ rescue Exception => exc
91
+ job.errors.add(attr, "Failed to parse :code, #{exc.inspect}")
92
+ end
93
+ end
94
+
95
+ before_perform :load_code
96
+
97
+ private
98
+
99
+ def load_code
100
+ instance_eval("def perform\n#{code}\nend", __FILE__, __LINE__)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,11 +1,9 @@
1
1
  module RocketJob
2
2
  module Jobs
3
-
4
3
  class SimpleJob < RocketJob::Job
5
4
  # No operation, used for performance testing
6
5
  def perform
7
6
  end
8
7
  end
9
-
10
8
  end
11
9
  end
@@ -0,0 +1,100 @@
1
+ require 'fileutils'
2
+ begin
3
+ require 'iostreams'
4
+ rescue LoadError
5
+ # Optional dependency
6
+ end
7
+
8
+ module RocketJob
9
+ module Jobs
10
+ # Job to upload a file into another job.
11
+ #
12
+ # Intended for use by DirmonJob to upload a file into a specified job.
13
+ #
14
+ # Can be used directly for any job class, as long as that job responds
15
+ # to `#upload`.
16
+ class UploadFileJob < RocketJob::Job
17
+ self.priority = 30
18
+
19
+ # Name of the job class to instantiate and upload the file into.
20
+ field :job_class_name, type: String, user_editable: true
21
+
22
+ # Properties to assign to the job when it is created.
23
+ field :properties, type: Hash, default: {}, user_editable: true
24
+
25
+ # File to upload
26
+ field :upload_file_name, type: String, user_editable: true
27
+
28
+ # The original Input file name.
29
+ # Used by #upload to extract the IOStreams when present.
30
+ field :original_file_name, type: String, user_editable: true
31
+
32
+ # Optionally set the job id for the downstream job
33
+ # Useful when for example the archived file should contain the job id for the downstream job.
34
+ field :job_id, type: BSON::ObjectId
35
+
36
+ validates_presence_of :upload_file_name, :job_class_name
37
+ validate :job_is_a_rocket_job
38
+ validate :job_implements_upload
39
+ validate :file_exists
40
+
41
+ # Create the job and upload the file into it.
42
+ def perform
43
+ job = job_class.new(properties)
44
+ job.id = job_id if job_id
45
+ upload_file(job)
46
+ job.save!
47
+ rescue StandardError => exc
48
+ # Prevent partial uploads
49
+ job&.cleanup! if job.respond_to?(:cleanup!)
50
+ raise(exc)
51
+ end
52
+
53
+ private
54
+
55
+ def job_class
56
+ @job_class ||= job_class_name.constantize
57
+ rescue NameError
58
+ nil
59
+ end
60
+
61
+ def upload_file(job)
62
+ if job.respond_to?(:upload)
63
+ if original_file_name && defined?(IOStreams)
64
+ streams = IOStreams.streams_for_file_name(original_file_name)
65
+ job.upload(upload_file_name, streams: streams)
66
+ else
67
+ job.upload(upload_file_name)
68
+ end
69
+ elsif job.respond_to?(:upload_file_name=)
70
+ job.upload_file_name = upload_file_name
71
+ elsif job.respond_to?(:full_file_name=)
72
+ job.full_file_name = upload_file_name
73
+ else
74
+ raise(ArgumentError, "Model #{job_class_name} must implement '#upload', or have attribute 'upload_file_name' or 'full_file_name'")
75
+ end
76
+ end
77
+
78
+ # Validates job_class is a Rocket Job
79
+ def job_is_a_rocket_job
80
+ klass = job_class
81
+ return if klass.nil? || klass.ancestors&.include?(RocketJob::Job)
82
+ errors.add(:job_class_name, "Model #{job_class_name} must be defined and inherit from RocketJob::Job")
83
+ end
84
+
85
+ VALID_INSTANCE_METHODS = %i[upload upload_file_name= full_file_name=].freeze
86
+
87
+ # Validates job_class is a Rocket Job
88
+ def job_implements_upload
89
+ klass = job_class
90
+ return if klass.nil? || klass.instance_methods.any? { |m| VALID_INSTANCE_METHODS.include?(m) }
91
+ errors.add(:job_class_name, "#{job_class} must implement any one of: :#{VALID_INSTANCE_METHODS.join(' :')} instance methods")
92
+ end
93
+
94
+ def file_exists
95
+ return if upload_file_name.nil? || File.exist?(upload_file_name)
96
+ errors.add(:upload_file_name, "Upload file: #{upload_file_name} does not exist.")
97
+ end
98
+ end
99
+ end
100
+ end
@@ -18,7 +18,9 @@ module RocketJob
18
18
  # Loads the queue with jobs to be processed once the queue is loaded.
19
19
  # Retain the first and last job for timings, all others are destroyed on completion.
20
20
  def run_test_case(count = self.count)
21
- raise 'Please start servers before starting the performance test' if RocketJob::Server.where(:state.in => ['running', 'paused']).count == 0
21
+ if RocketJob::Server.where(:state.in => %w[running paused]).count.zero?
22
+ raise 'Please start servers before starting the performance test'
23
+ end
22
24
 
23
25
  count_running_workers
24
26
 
@@ -33,21 +35,19 @@ module RocketJob
33
35
  running += server.heartbeat.workers unless server.zombie?
34
36
  end
35
37
  puts "Waiting for #{running} workers"
36
- break if running == 0
38
+ break if running.zero?
37
39
  sleep 1
38
40
  end
39
41
 
40
42
  puts 'Enqueuing jobs'
41
43
  first = RocketJob::Jobs::SimpleJob.create!(priority: 1, destroy_on_complete: false)
42
- (count - 2).times { |i| RocketJob::Jobs::SimpleJob.create! }
44
+ (count - 2).times { RocketJob::Jobs::SimpleJob.create! }
43
45
  last = RocketJob::Jobs::SimpleJob.create!(priority: 100, destroy_on_complete: false)
44
46
 
45
47
  puts 'Resuming workers'
46
48
  RocketJob::Server.resume_all
47
49
 
48
- while (!last.reload.completed?)
49
- sleep 3
50
- end
50
+ sleep 3 until last.reload.completed?
51
51
 
52
52
  duration = last.reload.completed_at - first.reload.started_at
53
53
  first.destroy
@@ -67,13 +67,19 @@ module RocketJob
67
67
  # Parse command line options
68
68
  def parse(argv)
69
69
  parser = OptionParser.new do |o|
70
- o.on('-c', '--count COUNT', 'Count of jobs to enqueue') do |arg|
70
+ o.on('-c',
71
+ '--count COUNT',
72
+ 'Count of jobs to enqueue') do |arg|
71
73
  self.count = arg.to_i
72
74
  end
73
- o.on('-m', '--mongo MONGO_CONFIG_FILE_NAME', 'Path and filename of config file. Default: config/mongoid.yml') do |arg|
75
+ o.on('-m',
76
+ '--mongo MONGO_CONFIG_FILE_NAME',
77
+ 'Path and filename of config file. Default: config/mongoid.yml') do |arg|
74
78
  self.mongo_config = arg
75
79
  end
76
- o.on('-e', '--environment ENVIRONMENT', 'The environment to run the app on (Default: RAILS_ENV || RACK_ENV || development)') do |arg|
80
+ o.on('-e',
81
+ '--environment ENVIRONMENT',
82
+ 'The environment to run the app on (Default: RAILS_ENV || RACK_ENV || development)') do |arg|
77
83
  self.environment = arg
78
84
  end
79
85
  end
@@ -95,6 +101,5 @@ module RocketJob
95
101
  end
96
102
  puts "Running: #{workers} workers, distributed across #{servers} servers"
97
103
  end
98
-
99
104
  end
100
105
  end