procrastinator 0.6.1 → 1.0.0.pre.rc2

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