procrastinator 0.9.0 → 1.0.0.pre.rc2

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