procrastinator 0.9.0 → 1.0.0.pre.rc2

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,54 @@
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)
23
+ new.define(**args)
24
+ end
25
+
26
+ # Defines procrastinator:start and procrastinator:stop Rake tasks that operate on the given scheduler.
27
+ # If provided a block, that block will run in the daemon process.
28
+ #
29
+ # @param scheduler [Procrastinator::Scheduler]
30
+ # @param pid_path [Pathname, File, String, nil]
31
+ def define(scheduler:, pid_path: nil, &block)
32
+ pid_path = Scheduler::DaemonWorking.normalize_pid(pid_path)
33
+
34
+ namespace :procrastinator do
35
+ task :start do
36
+ scheduler.work.daemonized!(pid_path, &block)
37
+ end
38
+
39
+ task :status do
40
+ if Scheduler::DaemonWorking.running?(pid_path)
41
+ warn "Procrastinator instance running (pid #{ File.read(pid_path) })"
42
+ else
43
+ warn "No Procrastinator instance detected for #{ pid_path }"
44
+ end
45
+ end
46
+
47
+ task :stop do
48
+ Scheduler::DaemonWorking.halt!(pid_path)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'daemon_tasks'