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.
@@ -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