rocketjob 3.4.3 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
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