procrastinator 0.6.1 → 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.
- checksums.yaml +5 -5
- data/.gitignore +6 -1
- data/.rubocop.yml +26 -0
- data/.ruby-version +1 -1
- data/Gemfile +2 -0
- data/README.md +492 -144
- data/RELEASE_NOTES.md +44 -0
- data/Rakefile +5 -3
- data/lib/procrastinator/config.rb +149 -0
- data/lib/procrastinator/logged_task.rb +50 -0
- data/lib/procrastinator/queue.rb +206 -0
- data/lib/procrastinator/queue_worker.rb +66 -91
- data/lib/procrastinator/rake/daemon_tasks.rb +54 -0
- data/lib/procrastinator/rake/tasks.rb +3 -0
- data/lib/procrastinator/scheduler.rb +393 -0
- data/lib/procrastinator/task.rb +64 -0
- data/lib/procrastinator/task_meta_data.rb +172 -0
- data/lib/procrastinator/task_store/file_transaction.rb +76 -0
- data/lib/procrastinator/task_store/simple_comma_store.rb +161 -0
- data/lib/procrastinator/test/mocks.rb +35 -0
- data/lib/procrastinator/version.rb +3 -1
- data/lib/procrastinator.rb +29 -23
- data/procrastinator.gemspec +17 -11
- metadata +66 -28
- data/lib/procrastinator/environment.rb +0 -148
- data/lib/procrastinator/task_worker.rb +0 -120
@@ -1,148 +0,0 @@
|
|
1
|
-
module Procrastinator
|
2
|
-
class Environment
|
3
|
-
attr_reader :persister, :queue_definitions, :queue_workers, :processes, :test_mode
|
4
|
-
|
5
|
-
DEFAULT_LOG_DIRECTORY = 'log/'
|
6
|
-
|
7
|
-
def initialize(test_mode: false)
|
8
|
-
@test_mode = test_mode
|
9
|
-
@queue_definitions = {}
|
10
|
-
@queue_workers = []
|
11
|
-
@processes = []
|
12
|
-
@log_dir = DEFAULT_LOG_DIRECTORY
|
13
|
-
@log_level = Logger::INFO
|
14
|
-
end
|
15
|
-
|
16
|
-
def persister_factory(&block)
|
17
|
-
@persister_factory = block
|
18
|
-
|
19
|
-
build_persister
|
20
|
-
end
|
21
|
-
|
22
|
-
def define_queue(name, properties={})
|
23
|
-
raise ArgumentError.new('queue name cannot be nil') if name.nil?
|
24
|
-
|
25
|
-
@queue_definitions[name] = properties
|
26
|
-
end
|
27
|
-
|
28
|
-
def spawn_workers
|
29
|
-
if @test_mode
|
30
|
-
@queue_definitions.each do |name, props|
|
31
|
-
@queue_workers << QueueWorker.new(props.merge(name: name,
|
32
|
-
persister: @persister))
|
33
|
-
end
|
34
|
-
else
|
35
|
-
@queue_definitions.each do |name, props|
|
36
|
-
pid = fork do
|
37
|
-
build_persister
|
38
|
-
worker = QueueWorker.new(props.merge(name: name,
|
39
|
-
persister: @persister,
|
40
|
-
log_dir: @log_dir,
|
41
|
-
log_level: @log_level))
|
42
|
-
|
43
|
-
Process.setproctitle("#{@process_prefix ? "#{@process_prefix}-" : ''}#{worker.long_name}") # tODO: add an app name prefix
|
44
|
-
|
45
|
-
monitor_parent(worker)
|
46
|
-
|
47
|
-
worker.work
|
48
|
-
end
|
49
|
-
|
50
|
-
Process.detach(pid) unless pid.nil?
|
51
|
-
@processes << pid
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def act(*queue_names)
|
57
|
-
unless @test_mode
|
58
|
-
raise RuntimeError.new('Procrastinator.act called outside Test Mode. Enable test mode by setting Procrastinator.test_mode = true before running setup')
|
59
|
-
end
|
60
|
-
|
61
|
-
if queue_names.empty?
|
62
|
-
@queue_workers.each do |worker|
|
63
|
-
worker.act
|
64
|
-
end
|
65
|
-
else
|
66
|
-
queue_names.each do |name|
|
67
|
-
@queue_workers.find { |worker| worker.name == name }.act
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
def delay(queue: nil, run_at: Time.now.to_i, expire_at: nil, task:)
|
73
|
-
raise ArgumentError.new('task may not be nil') if task.nil?
|
74
|
-
raise MalformedTaskError.new('the provided task does not support #run method') unless task.respond_to? :run
|
75
|
-
|
76
|
-
# We're checking these on init because it's one of those extremely rare cases where you'd want to know
|
77
|
-
# incredibly early, because of the sub-processing. It's a bit belt-and suspenders, but UX is important for
|
78
|
-
# devs, too.
|
79
|
-
[:success, :fail, :final_fail].each do |method_name|
|
80
|
-
if task.respond_to?(method_name) && task.method(method_name).arity <= 0
|
81
|
-
raise MalformedTaskError.new("the provided task must accept a parameter to its ##{method_name} method")
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
if queue.nil? && @queue_definitions.size > 1
|
86
|
-
raise ArgumentError.new("queue must be specified when more than one is registered. Defined queues are: #{queue_definitions.keys.map { |k| ':' + k.to_s }.join(', ')}")
|
87
|
-
else
|
88
|
-
queue ||= @queue_definitions.keys.first
|
89
|
-
raise ArgumentError.new(%Q{there is no "#{queue}" queue registered in this environment}) if @queue_definitions[queue].nil?
|
90
|
-
end
|
91
|
-
|
92
|
-
@persister.create_task(queue: queue,
|
93
|
-
run_at: run_at.to_i,
|
94
|
-
initial_run_at: run_at.to_i,
|
95
|
-
expire_at: expire_at.nil? ? nil : expire_at.to_i,
|
96
|
-
task: YAML.dump(task))
|
97
|
-
end
|
98
|
-
|
99
|
-
def enable_test_mode
|
100
|
-
@test_mode = true
|
101
|
-
end
|
102
|
-
|
103
|
-
def log_dir(path)
|
104
|
-
@log_dir = path
|
105
|
-
end
|
106
|
-
|
107
|
-
def log_level(lvl)
|
108
|
-
@log_level = lvl
|
109
|
-
end
|
110
|
-
|
111
|
-
def process_prefix(prefix)
|
112
|
-
@process_prefix = prefix
|
113
|
-
end
|
114
|
-
|
115
|
-
private
|
116
|
-
def monitor_parent(worker)
|
117
|
-
parent_pid = Process.ppid
|
118
|
-
|
119
|
-
heartbeat_thread = Thread.new(parent_pid) do |ppid|
|
120
|
-
loop do
|
121
|
-
begin
|
122
|
-
Process.kill(0, ppid) # kill with 0 flag checks if the process exists & has permissions
|
123
|
-
rescue Errno::ESRCH
|
124
|
-
worker.log_parent_exit(ppid: ppid, pid: Process.pid)
|
125
|
-
exit
|
126
|
-
end
|
127
|
-
|
128
|
-
sleep(5)
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
heartbeat_thread.abort_on_exception = true
|
133
|
-
end
|
134
|
-
|
135
|
-
def build_persister
|
136
|
-
@persister = @persister_factory.call
|
137
|
-
|
138
|
-
raise ArgumentError.new('persister cannot be nil') if @persister.nil?
|
139
|
-
|
140
|
-
[:read_tasks, :create_task, :update_task, :delete_task].each do |method|
|
141
|
-
raise MalformedPersisterError.new("persister must repond to ##{method}") unless @persister.respond_to? method
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
class MalformedPersisterError < StandardError
|
147
|
-
end
|
148
|
-
end
|
@@ -1,120 +0,0 @@
|
|
1
|
-
require 'yaml'
|
2
|
-
|
3
|
-
module Procrastinator
|
4
|
-
class TaskWorker
|
5
|
-
attr_reader :id, :run_at, :initial_run_at, :expire_at, :task, :attempts, :last_fail_at, :last_error
|
6
|
-
|
7
|
-
def initialize(id: nil,
|
8
|
-
run_at: nil,
|
9
|
-
initial_run_at: nil,
|
10
|
-
expire_at: nil,
|
11
|
-
attempts: 0,
|
12
|
-
timeout: nil,
|
13
|
-
max_attempts: nil,
|
14
|
-
last_fail_at: nil,
|
15
|
-
last_error: nil,
|
16
|
-
task:,
|
17
|
-
logger: Logger.new(StringIO.new))
|
18
|
-
@id = id
|
19
|
-
@run_at = run_at.nil? ? nil : run_at.to_i
|
20
|
-
@initial_run_at = initial_run_at.to_i
|
21
|
-
@expire_at = expire_at.nil? ? nil : expire_at.to_i
|
22
|
-
@task = YAML.load(task)
|
23
|
-
@attempts = attempts || 0
|
24
|
-
@max_attempts = max_attempts
|
25
|
-
@timeout = timeout
|
26
|
-
@last_fail_at = last_fail_at
|
27
|
-
@last_error = last_error
|
28
|
-
@logger = logger
|
29
|
-
|
30
|
-
raise(MalformedTaskError.new('given task does not support #run method')) unless @task.respond_to? :run
|
31
|
-
raise(ArgumentError.new('timeout cannot be negative')) if timeout && timeout < 0
|
32
|
-
end
|
33
|
-
|
34
|
-
def work
|
35
|
-
@attempts += 1
|
36
|
-
|
37
|
-
begin
|
38
|
-
raise(TaskExpiredError.new("task is over its expiry time of #{@expire_at}")) if expired?
|
39
|
-
|
40
|
-
Timeout::timeout(@timeout) do
|
41
|
-
@task.run
|
42
|
-
end
|
43
|
-
|
44
|
-
try_hook(:success, @logger)
|
45
|
-
|
46
|
-
@logger.debug("Task completed: #{YAML.dump(@task)}")
|
47
|
-
|
48
|
-
@last_error = nil
|
49
|
-
@last_fail_at = nil
|
50
|
-
rescue StandardError => e
|
51
|
-
@last_fail_at = Time.now.to_i
|
52
|
-
|
53
|
-
if too_many_fails? || expired?
|
54
|
-
try_hook(:final_fail, @logger, e)
|
55
|
-
|
56
|
-
@run_at = nil
|
57
|
-
@last_error = "#{expired? ? 'Task expired' : 'Task failed too many times'}: #{e.backtrace.join("\n")}"
|
58
|
-
|
59
|
-
@logger.debug("Task failed permanently: #{YAML.dump(@task)}")
|
60
|
-
else
|
61
|
-
try_hook(:fail, @logger, e)
|
62
|
-
|
63
|
-
@last_error = %Q[Task failed: #{e.message}\n#{e.backtrace.join("\n")}]
|
64
|
-
@logger.debug("Task failed: #{YAML.dump(@task)}")
|
65
|
-
|
66
|
-
reschedule
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def successful?
|
72
|
-
if !expired? && @attempts <= 0
|
73
|
-
raise(RuntimeError, 'you cannot check for success before running #work')
|
74
|
-
end
|
75
|
-
|
76
|
-
@last_error.nil? && @last_fail_at.nil?
|
77
|
-
end
|
78
|
-
|
79
|
-
def too_many_fails?
|
80
|
-
!@max_attempts.nil? && @attempts >= @max_attempts
|
81
|
-
end
|
82
|
-
|
83
|
-
def expired?
|
84
|
-
!@expire_at.nil? && Time.now.to_i > @expire_at
|
85
|
-
end
|
86
|
-
|
87
|
-
def to_hash
|
88
|
-
{id: @id,
|
89
|
-
run_at: @run_at,
|
90
|
-
initial_run_at: @initial_run_at,
|
91
|
-
expire_at: @expire_at,
|
92
|
-
attempts: @attempts,
|
93
|
-
last_fail_at: @last_fail_at,
|
94
|
-
last_error: @last_error,
|
95
|
-
task: YAML.dump(@task)}
|
96
|
-
end
|
97
|
-
|
98
|
-
private
|
99
|
-
def try_hook(method, *params)
|
100
|
-
begin
|
101
|
-
@task.send(method, *params) if @task.respond_to? method
|
102
|
-
rescue StandardError => e
|
103
|
-
$stderr.puts "#{method.to_s.capitalize} hook error: #{e.message}"
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def reschedule
|
108
|
-
# (30 + n_attempts^4) seconds is chosen to rapidly expand
|
109
|
-
# but with the baseline of 30s to avoid hitting the disc too frequently.
|
110
|
-
|
111
|
-
@run_at += 30 + (@attempts**4)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
class TaskExpiredError < StandardError
|
116
|
-
end
|
117
|
-
|
118
|
-
class MalformedTaskError < StandardError
|
119
|
-
end
|
120
|
-
end
|