procrastinator 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 48d4ad814bc4f44c3f3af37309040106da854571
4
- data.tar.gz: cf4cbc98185f4511a8ea3daaaa4c7aeffa997f4f
3
+ metadata.gz: 46f0ec23b44a55033b241f8f1eea274a99dcc481
4
+ data.tar.gz: 84476d93786e0e63f53e11cbf913c8df9f641134
5
5
  SHA512:
6
- metadata.gz: fb464436151e500f902c3c2ce289d9baefadea247b4ea14197cbc351d84dde58d779f20162b537dcf123b55f4311599e4d21891577d74775d680ec7766b21b84
7
- data.tar.gz: 4761c5cdabf41895a16fc5693590a39e030c797e889ebada79327e0c419f6ba39ccd9c32fa1bcf077c14c7a5e06860a6ced3944dfc0bdf5a433f782702656229
6
+ metadata.gz: 0f54aba2184d65a6dba03a756688565bfc6d45520ea521a0ce488353ea81c02ff1946d072145b176bc5ddc643f01670256bfb9b73819f70a7961d10f63ce1712
7
+ data.tar.gz: 8181179a9437191dc43b34e61e67c7f846a83633962d7f3f41b1a69696b22769e85b5a15a1f8672c03af7c1f3fb8d0f3290fea6cef908e3bcf8b7da2128589b8
data/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # Procrastinator
2
2
 
3
- Procrastinator is a framework-independent job scheduling gem. It creates a subprocess for each queue that performs
4
- tasks at the appropriate times.
3
+ Procrastinator is a framework-independent job scheduling gem to allow your app to put stuff of until later. It creates
4
+ a subprocess for each queue to performs tasks at the designated times. Or maybe later, depending on how busy it is.
5
+
6
+ Don't worry, it'll get done eventually.
5
7
 
6
8
  ## Installation
7
9
 
@@ -38,12 +40,12 @@ procrastinator.delay(queue: :cleanup, run_at: Time.now + 3600, task: ClearTempDa
38
40
 
39
41
  Read on for more details on each step.
40
42
 
41
- ###`Procrastinator.setup`
43
+ ### Setup Phase
42
44
  The setup phase first defines which queues are available and the persistence strategy to use for reading
43
45
  and writing tasks. It then starts up a sub process for working on each queue within that environment.
44
46
 
45
47
 
46
- #### Persister Strategy
48
+ #### Declaring a Persistence Strategy
47
49
  The persister instance is the last step between Procrastinator and your data storage (eg. database). As core Procrastinator is framework-agnostic, it needs you to provide an object that knows how to read and write task data.
48
50
 
49
51
  Your [strategy](https://en.wikipedia.org/wiki/Strategy_pattern) class is required to provide the following methods:
@@ -71,7 +73,7 @@ If the strategy does not have all of these methods, Procrastinator will explode
71
73
 
72
74
  Notice that the times are all given as unix epoch timestamps. This is to avoid any confusion with timezones, and it is recommended that you store times in this manner for the same reason.
73
75
 
74
- ####Defining Queues
76
+ #### Defining Queues
75
77
  `Procrastinator.setup` requires a block be provided, and that in the block call `#define_queue` be called on the provided environment. Define queue takes a queue name symbol and these properies as a hash
76
78
 
77
79
  * :timeout - Time, in seconds, after which it should fail tasks in this queue for taking too long to execute.
@@ -89,36 +91,37 @@ Procrastinator.setup(some_persister) do |env|
89
91
  end
90
92
  ```
91
93
 
92
- #### Queue Sub-Processes
94
+ #### Sub-Processes
93
95
  Each queue is worked in a separate process.
94
96
 
95
97
  <!-- , and each process multi-threaded to handle more than one task at a time. This should help prevent a single task from clogging up the whole queue, or a single queue clogging up the entire system. -->
96
98
 
97
- The sub-processes check every 5 seconds that the parent process is still alive. If there is no process with the parent's process ID, the sub-process will self-exit.
99
+ The sub-processes checks that the parent process is still alive every 5 seconds. If there is no process with the parent's PID, the sub-process will self-exit.
98
100
 
99
- ###`Environment#delay`
100
- When there are multiple queues defined, you are required to provide a queue name:
101
+ ###Scheduling Tasks For Later
102
+ Procrastinator will let you be lazy:
101
103
 
102
104
  ```ruby
103
105
  procrastinator = Procrastinator.setup(task_persister) do |env|
104
106
  env.define_queue(:email)
105
- env.define_queue(:cleanup)
106
107
  end
107
108
 
108
- procrastinator.delay(:email, task: EmailReminder.new('bob@example.com'))
109
+ procrastinator.delay(task: EmailReminder.new('bob@example.com'))
109
110
  ```
110
111
 
111
- Otherwise, Procrastinator will let you be lazy:
112
+ ... unless there are multiple queues defined. Thne you must provide a queue name with your task:
112
113
 
113
114
  ```ruby
114
115
  procrastinator = Procrastinator.setup(task_persister) do |env|
115
116
  env.define_queue(:email)
117
+ env.define_queue(:cleanup)
116
118
  end
117
119
 
118
- procrastinator.delay(task: EmailReminder.new('bob@example.com'))
120
+ procrastinator.delay(:email, task: EmailReminder.new('bob@example.com'))
119
121
  ```
120
122
 
121
- You can set when the particular task is to be run and/or when it should expire. Be aware that neither is guaranteed to be real-time; a task will run as soon as it possible after `run_at` is passed, and will expire once it attempts to run after `expire_at` has passed.
123
+ You can set when the particular task is to be run and/or when it should expire. Be aware that the task is not guaranteed
124
+ to run at a precise time; the only promise is that the task will get run some time after `run_at`, unless it's after `expire_at`.
122
125
 
123
126
  ```ruby
124
127
  procrastinator = Procrastinator.setup(task_persister) do |env|
@@ -145,7 +148,7 @@ procrastinator.delay(expire_at: , task: EmailGreeting.new('bob@example.com'))
145
148
  procrastinator.delay(expire_at: nil, task: EmailGreeting.new('bob@example.com'))
146
149
  ```
147
150
 
148
- #### Task
151
+ #### Task Definition
149
152
  Like the persister provided to `.setup`, your task is a strategy object that fills in the details of what to do. For this,
150
153
  your task **must provide** a `#run` method:
151
154
 
@@ -168,6 +171,35 @@ Tasks that fail have their `run_at` rescheduled on an increasing delay according
168
171
 
169
172
  Both failing and final_failing will cause the error timestamp and reason to be stored in `:last_fail_at` and `:last_error`.
170
173
 
174
+ ### Testing With Procrastinator
175
+ Procrastinator uses multi-threading and multi-processing internally, which is a nightmare for testing. Fortunately for you,
176
+ Test Mode will disable all of that, and rely on your tests to tell it when to tick.
177
+
178
+ Enable Test Mode by setting `Procrastinator.test_mode` to `true` before setting up, or by calling enable_test_mode on
179
+ the procrastination environment.
180
+
181
+ ```ruby
182
+ # all further calls to `Procrastinator.setup` will produce a procrastination environment where Test Mode is enabled
183
+ Procrastinator.test_mode = true
184
+
185
+ # or you can also enable it directly in the setup
186
+ env = Procrastinator.setup do |env|
187
+ env.enable_test_mode
188
+
189
+ # other settings...
190
+ end
191
+ ```
192
+
193
+ In your tests, tell the procrastinator environment to work off one item from its queues:
194
+
195
+ ```
196
+ # works one task on all queues
197
+ env.act
198
+
199
+ # provide queue names to works one task on just those queues
200
+ env.act(:cleanup, :email)
201
+ ```
202
+
171
203
  ## Contributing
172
204
  Bug reports and pull requests are welcome on GitHub at
173
205
  [https://github.com/TenjinInc/procrastinator](https://github.com/TenjinInc/procrastinator).
@@ -5,17 +5,27 @@ require 'procrastinator/environment'
5
5
 
6
6
 
7
7
  module Procrastinator
8
+ @@test_mode = false
9
+
8
10
  def self.setup(persister, &block)
9
11
  raise ArgumentError.new('Procrastinator.setup must be given a block') if block.nil?
10
12
 
11
- env = Environment.new(persister)
13
+ env = Environment.new(persister: persister, test_mode: @@test_mode)
12
14
 
13
15
  yield(env)
14
16
 
15
- raise RuntimeError.new('setup block did not define any queues') if env.queues.empty?
17
+ raise RuntimeError.new('setup block did not define any queues') if env.queue_definitions.empty?
16
18
 
17
19
  env.spawn_workers
18
20
 
19
21
  env
20
22
  end
23
+
24
+ def self.test_mode=(value)
25
+ @@test_mode = value
26
+ end
27
+
28
+ def self.test_mode
29
+ @@test_mode
30
+ end
21
31
  end
@@ -1,50 +1,74 @@
1
1
  module Procrastinator
2
2
  class Environment
3
- attr_reader :persister, :queues, :processes
3
+ attr_reader :persister, :queue_definitions, :queue_workers, :processes, :test_mode
4
4
 
5
- def initialize(persister)
5
+ def initialize(persister:, test_mode: false)
6
6
  raise ArgumentError.new('persister cannot be nil') if persister.nil?
7
7
 
8
8
  [:read_tasks, :create_task, :update_task, :delete_task].each do |method|
9
9
  raise MalformedPersisterError.new("persister must repond to ##{method}") unless persister.respond_to? method
10
10
  end
11
11
 
12
- @persister = persister
13
- @queues = {}
14
- @processes = []
12
+ @persister = persister
13
+ @test_mode = test_mode
14
+ @queue_definitions = {}
15
+ @queue_workers = []
16
+ @processes = []
15
17
  end
16
18
 
17
19
  def define_queue(name, properties={})
18
20
  raise ArgumentError.new('queue name cannot be nil') if name.nil?
19
21
 
20
- @queues[name] = properties
22
+ @queue_definitions[name] = properties
21
23
  end
22
24
 
23
25
  def spawn_workers
24
- @queues.each do |name, props|
25
- pid = fork do
26
- Process.setproctitle("#{name}-queue-worker")
26
+ if @test_mode
27
+ @queue_definitions.each do |name, props|
28
+ @queue_workers << QueueWorker.new(props.merge(name: name, persister: @persister))
29
+ end
30
+ else
31
+ @queue_definitions.each do |name, props|
32
+ pid = fork do
33
+ Process.setproctitle("#{name}-queue-worker")
34
+
35
+ worker = QueueWorker.new(props.merge(name: name, persister: @persister))
27
36
 
28
- worker = QueueWorker.new(props.merge(name: name, persister: @persister))
37
+ monitor_parent
29
38
 
30
- monitor_parent
39
+ worker.work
40
+ end
31
41
 
32
- worker.work
42
+ Process.detach(pid) unless pid.nil?
43
+ @processes << pid
33
44
  end
45
+ end
46
+ end
47
+
48
+ def act(*queue_names)
49
+ unless @test_mode
50
+ raise RuntimeError.new('Procrastinator.act called outside Test Mode. Enable test mode by setting Procrastinator.test_mode = true before running setup')
51
+ end
34
52
 
35
- Process.detach(pid) unless pid.nil?
36
- @processes << pid
53
+ if queue_names.empty?
54
+ @queue_workers.each do |worker|
55
+ worker.act
56
+ end
57
+ else
58
+ queue_names.each do |name|
59
+ @queue_workers.find { |worker| worker.name == name }.act
60
+ end
37
61
  end
38
62
  end
39
63
 
40
64
  def delay(queue: nil, run_at: Time.now.to_i, expire_at: nil, task:)
41
65
  raise ArgumentError.new('task may not be nil') if task.nil?
42
66
  raise MalformedTaskError.new('given task does not support #run method') unless task.respond_to? :run
43
- if queue.nil? && @queues.size > 1
44
- raise ArgumentError.new('queue must be specified when more than one is registered')
67
+ if queue.nil? && @queue_definitions.size > 1
68
+ 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(', ')}")
45
69
  else
46
- queue ||= @queues.keys.first
47
- raise ArgumentError.new(%Q{there is no "#{queue}" queue registered in this environment}) if @queues[queue].nil?
70
+ queue ||= @queue_definitions.keys.first
71
+ raise ArgumentError.new(%Q{there is no "#{queue}" queue registered in this environment}) if @queue_definitions[queue].nil?
48
72
  end
49
73
 
50
74
  @persister.create_task(queue: queue,
@@ -54,6 +78,10 @@ module Procrastinator
54
78
  task: YAML.dump(task))
55
79
  end
56
80
 
81
+ def enable_test_mode
82
+ @test_mode = true
83
+ end
84
+
57
85
  private
58
86
  def monitor_parent
59
87
  heartbeat_thread = Thread.new(Process.ppid) do |ppid|
@@ -1,6 +1,6 @@
1
1
  module Procrastinator
2
2
  class QueueWorker
3
- DEFAULT_TIMEOUT = 3600 # seconds = one hour
3
+ DEFAULT_TIMEOUT = 3600 # in seconds; one hour total
4
4
  DEFAULT_MAX_ATTEMPTS = 20
5
5
  DEFAULT_UPDATE_PERIOD = 10 # seconds
6
6
  DEFAULT_MAX_TASKS = 10
@@ -34,21 +34,25 @@ module Procrastinator
34
34
  loop do
35
35
  sleep(@update_period)
36
36
 
37
- # shuffling and re-sorting to avoid worst case O(n^2) on quicksort
38
- # when receiving already sorted data. Ideally, we'd use a better algo, but this will do for now
39
- tasks = @persister.read_tasks(@name).shuffle.sort_by { |t| t[:run_at] }
37
+ act
38
+ end
39
+ end
40
+
41
+ def act
42
+ # shuffling and re-sorting to avoid worst case O(n^2) on quicksort
43
+ # when receiving already sorted data. Ideally, we'd use a better algo, but this will do for now
44
+ tasks = @persister.read_tasks(@name).shuffle.sort_by { |t| t[:run_at] }
40
45
 
41
- tasks.first(@max_tasks).each do |task_data|
42
- if Time.now.to_i >= task_data[:run_at].to_i
43
- tw = TaskWorker.new(task_data)
46
+ tasks.first(@max_tasks).each do |task_data|
47
+ if Time.now.to_i >= task_data[:run_at].to_i
48
+ tw = TaskWorker.new(task_data)
44
49
 
45
- tw.work
50
+ tw.work
46
51
 
47
- if tw.successful?
48
- @persister.delete_task(task_data[:id])
49
- else
50
- @persister.update_task(tw.to_hash.merge(queue: @name))
51
- end
52
+ if tw.successful?
53
+ @persister.delete_task(task_data[:id])
54
+ else
55
+ @persister.update_task(tw.to_hash.merge(queue: @name))
52
56
  end
53
57
  end
54
58
  end
@@ -53,7 +53,7 @@ module Procrastinator
53
53
  else
54
54
  try_hook(:fail, e)
55
55
 
56
- @last_error = %Q[Task failed: #{e.message}\n #{e.backtrace.join("\n")}]
56
+ @last_error = %Q[Task failed: #{e.message}\n#{e.backtrace.join("\n")}]
57
57
 
58
58
  reschedule
59
59
  end
@@ -1,3 +1,3 @@
1
1
  module Procrastinator
2
- VERSION = '0.2.3'
2
+ VERSION = '0.3.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: procrastinator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin Miller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-02-25 00:00:00.000000000 Z
11
+ date: 2016-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler