procrastinator 0.9.0 → 1.0.0.pre.rc3

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.
@@ -1,133 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Procrastinator
4
- # A QueueWorker checks for tasks to run from the loader defined in the provided config and executes them,
5
- # updating information in the task loader as necessary.
4
+ # A QueueWorker checks for tasks to run from the task store and executes them, updating information in the task
5
+ # store as necessary.
6
6
  #
7
7
  # @author Robin Miller
8
8
  class QueueWorker
9
9
  extend Forwardable
10
10
 
11
- def_delegators :@queue, :name
11
+ def_delegators :@queue, :name, :next_task
12
12
 
13
13
  # expected methods for all persistence strategies
14
14
  PERSISTER_METHODS = [:read, :update, :delete].freeze
15
15
 
16
- def initialize(queue:, config:, scheduler: nil)
17
- @queue = queue
18
- @config = config
19
- @scheduler = scheduler
16
+ def initialize(queue:, config:)
17
+ raise ArgumentError, ':queue cannot be nil' if queue.nil?
18
+ raise ArgumentError, ':config cannot be nil' if config.nil?
20
19
 
21
- @logger = nil
22
- end
23
-
24
- def work
25
- start_log
20
+ @config = config
26
21
 
27
- begin
28
- loop do
29
- sleep(@queue.update_period)
22
+ @queue = if queue.is_a? Symbol
23
+ config.queue(name: queue)
24
+ else
25
+ queue
26
+ end
30
27
 
31
- act
32
- end
33
- rescue StandardError => e
34
- raise if @config.test_mode? || !@logger
35
-
36
- @logger.fatal(e)
37
- end
28
+ @scheduler = Scheduler.new(config)
29
+ @logger = Logger.new(StringIO.new)
38
30
  end
39
31
 
40
- def act
41
- persister = @config.loader
42
-
43
- tasks = fetch_tasks(persister)
32
+ # Works on jobs forever
33
+ def work!
34
+ @logger = open_log!("#{ name }-queue-worker", @config)
35
+ @logger.info("Started worker thread to consume queue: #{ name }")
44
36
 
45
- tasks.each do |metadata|
46
- tw = build_worker(metadata)
37
+ loop do
38
+ sleep(@queue.update_period)
47
39
 
48
- tw.work
49
-
50
- if tw.successful?
51
- persister.delete(metadata.id)
52
- else
53
- persister.update(metadata.id, tw.to_h.merge(queue: @queue.name.to_s))
54
- end
40
+ work_one
55
41
  end
56
- end
57
-
58
- def long_name
59
- name = "#{ @queue.name }-queue-worker"
60
-
61
- name = "#{ @config.prefix }-#{ name }" if @config.prefix
42
+ rescue StandardError => e
43
+ @logger.fatal(e)
62
44
 
63
- name
45
+ raise
64
46
  end
65
47
 
66
- # Starts a log file and stores the logger within this queue worker.
67
- #
68
- # Separate from init because logging is context-dependent
69
- def start_log
70
- return if @logger || !@config.log_dir
48
+ # Performs exactly one task on the queue
49
+ def work_one
50
+ task = next_task(logger: @logger,
51
+ container: @config.container,
52
+ scheduler: @scheduler) || return
71
53
 
72
- @logger = Logger.new(log_target, level: @config.log_level)
73
-
74
- msg = <<~MSG
75
- ======================================================================
76
- Started worker process, #{ long_name }, to work off queue #{ @queue.name }.
77
- Worker pid=#{ Process.pid }; parent pid=#{ Process.ppid }.
78
- ======================================================================
79
- MSG
80
-
81
- @logger.info("\n#{ msg }")
82
- end
83
-
84
- private
85
-
86
- def build_worker(metadata)
87
- start_log
88
-
89
- TaskWorker.new(metadata: metadata,
90
- queue: @queue,
91
- scheduler: @scheduler,
92
- context: @config.context,
93
- logger: @logger)
94
- end
95
-
96
- def log_target
97
- return $stdout if @config.test_mode?
98
-
99
- log_path = @config.log_dir + "#{ long_name }.log"
100
-
101
- write_log_file(log_path)
54
+ begin
55
+ task.run
102
56
 
103
- log_path.to_path
104
- end
57
+ @queue.delete(task.id)
58
+ rescue StandardError => e
59
+ task.fail(e)
105
60
 
106
- def write_log_file(log_path)
107
- @config.log_dir.mkpath
108
- File.open(log_path.to_path, 'a+') do |f|
109
- f.write ''
61
+ task_info = task.to_h
62
+ id = task_info.delete(:id)
63
+ @queue.update(id, **task_info)
110
64
  end
111
65
  end
112
66
 
113
- def fetch_tasks(persister)
114
- tasks = persister.read(queue: @queue.name).map(&:to_h).reject { |t| t[:run_at].nil? }
67
+ def halt
68
+ @logger&.info("Halted worker on queue: #{ name }")
69
+ @logger&.close
70
+ end
115
71
 
116
- tasks = sort_tasks(tasks)
72
+ # Starts a log file and returns the created Logger
73
+ def open_log!(name, config)
74
+ return @logger unless config.log_level
117
75
 
118
- metas = tasks.collect do |t|
119
- TaskMetaData.new(t.delete_if { |key| !TaskMetaData::EXPECTED_DATA.include?(key) })
120
- end
76
+ log_path = config.log_dir / "#{ name }.log"
121
77
 
122
- metas.select(&:runnable?)
123
- end
78
+ config.log_dir.mkpath
79
+ FileUtils.touch(log_path)
124
80
 
125
- def sort_tasks(tasks)
126
- # shuffling and re-sorting to avoid worst case O(n^2) when receiving already sorted data
127
- # on quicksort (which is default ruby sort). It is not unreasonable that the persister could return sorted
128
- # results
129
- # Ideally, we'd use a better algo than qsort for this, but this will do for now
130
- tasks.shuffle.sort_by { |t| t[:run_at] }.first(@queue.max_tasks)
81
+ Logger.new(log_path.to_path,
82
+ config.log_shift_age, config.log_shift_size,
83
+ level: config.log_level || Logger::FATAL,
84
+ progname: name,
85
+ formatter: Config::DEFAULT_LOG_FORMATTER)
131
86
  end
132
87
  end
133
88
 
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake'
4
+
5
+ module Procrastinator
6
+ module Rake
7
+ # RakeTask builder. Provide this in your Rakefile:
8
+ #
9
+ # require 'procrastinator/rake/task'
10
+ # Procrastinator::RakeTask.new('/var/run') do
11
+ # # return your Procrastinator::Scheduler here or construct it using Procrastinator.config
12
+ # end
13
+ #
14
+ class DaemonTasks
15
+ include ::Rake::Cloneable
16
+ include ::Rake::DSL
17
+
18
+ # Shorthand for DaemonTasks.new.define
19
+ #
20
+ # @param (see #define)
21
+ # @see DaemonTasks#define
22
+ def self.define(**args, &block)
23
+ new.define(**args, &block)
24
+ end
25
+
26
+ # Defines procrastinator:start and procrastinator:stop Rake tasks that operate on the given scheduler.
27
+ #
28
+ # @param pid_path [Pathname, File, String, nil] The pid file path
29
+ # @yieldreturn scheduler [Procrastinator::Scheduler]
30
+ #
31
+ # @see Scheduler::DaemonWorking#daemonized!
32
+ def define(pid_path: nil)
33
+ raise ArgumentError, 'must provide a scheduler builder block' unless block_given?
34
+
35
+ @pid_path = Scheduler::DaemonWorking.normalize_pid pid_path
36
+
37
+ namespace :procrastinator do
38
+ desc 'Start the Procrastinator daemon'
39
+ task :start do
40
+ start(yield)
41
+ end
42
+
43
+ desc 'Show Procrastinator daemon status'
44
+ task :status do
45
+ status
46
+ end
47
+
48
+ desc 'Stop the Procrastinator daemon'
49
+ task stop: [:status] do
50
+ stop
51
+ end
52
+
53
+ desc 'Restart Procrastinator daemon'
54
+ task restart: [:stop, :start]
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def start(scheduler)
61
+ warn 'Starting Procrastinator'
62
+ scheduler.work.daemonized!(@pid_path)
63
+ end
64
+
65
+ def status
66
+ warn "Checking #{ @pid_path }..."
67
+ msg = if Scheduler::DaemonWorking.running?(@pid_path)
68
+ "Procrastinator pid #{ File.read(@pid_path) } instance running."
69
+ elsif File.exist?(@pid_path)
70
+ "Procrastinator pid #{ File.read(@pid_path) } is not running. Maybe it crashed?"
71
+ else
72
+ "Procrastinator is not running (No such file - #{ @pid_path })"
73
+ end
74
+
75
+ warn msg
76
+ end
77
+
78
+ def stop
79
+ return unless Scheduler::DaemonWorking.running?(@pid_path)
80
+
81
+ Scheduler::DaemonWorking.halt!(@pid_path)
82
+ warn "Procrastinator pid #{ File.read(@pid_path) } halted."
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'daemon_tasks'