salus 0.1.2

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.
@@ -0,0 +1,482 @@
1
+ #--
2
+ # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
3
+ # Version 2, December 2004
4
+ #
5
+ # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
6
+ # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
7
+ #
8
+ # 0. You just DO WHAT THE FUCK YOU WANT TO.
9
+ #++
10
+ # borrowed from https://github.com/meh/ruby-thread
11
+
12
+ module Salus
13
+ # A pool is a container of a limited amount of threads to which you can add
14
+ # tasks to run.
15
+ #
16
+ # This is usually more performant and less memory intensive than creating a
17
+ # new thread for every task.
18
+ class ThreadPool
19
+ # A task incapsulates a block being ran by the pool and the arguments to pass
20
+ # to it.
21
+ class Task
22
+ include Observable
23
+ include Logging
24
+
25
+ Timeout = Class.new(Exception)
26
+ Asked = Class.new(Exception)
27
+
28
+ attr_reader :pool, :timeout, :exception, :thread, :started_at, :result
29
+
30
+ # Create a task in the given pool which will pass the arguments to the
31
+ # block.
32
+ def initialize(pool, *args, &block)
33
+ @pool = pool
34
+ @arguments = args
35
+ @block = block
36
+
37
+ @running = false
38
+ @finished = false
39
+ @timedout = false
40
+ @terminated = false
41
+ end
42
+
43
+ def running?
44
+ @running
45
+ end
46
+
47
+ def finished?
48
+ @finished
49
+ end
50
+
51
+ def timeout?
52
+ @timedout
53
+ end
54
+
55
+ def terminated?
56
+ @terminated
57
+ end
58
+
59
+ # Execute the task.
60
+ def execute
61
+ return if terminated? || running? || finished?
62
+
63
+ @thread = Thread.current
64
+ @running = true
65
+ @result = nil
66
+ @started_at = MonotonicTime.get
67
+
68
+ pool.__send__ :wake_up_timeout
69
+
70
+ begin
71
+ @result = @block.call(*@arguments)
72
+ notify_observers(Time.now, @result, nil)
73
+ rescue Exception => reason
74
+ notify_observers(Time.now, nil, reason)
75
+ if reason.is_a? Timeout
76
+ @timedout = true
77
+ log DEBUG, reason
78
+ elsif reason.is_a? Asked
79
+ log DEBUG, reason
80
+ return
81
+ else
82
+ @exception = reason
83
+ log ERROR, reason
84
+ raise @exception if ThreadPool.abort_on_exception
85
+ end
86
+ end
87
+
88
+ @running = false
89
+ @finished = true
90
+ @thread = nil
91
+ end
92
+
93
+ # Raise an exception in the thread used by the task.
94
+ def raise(exception)
95
+ @thread.raise(exception) if @thread
96
+ end
97
+
98
+ # Terminate the exception with an optionally given exception.
99
+ def terminate!(exception = Asked)
100
+ return if terminated? || finished? || timeout?
101
+
102
+ @terminated = true
103
+
104
+ return unless running?
105
+
106
+ self.raise exception
107
+ end
108
+
109
+ # Force the task to timeout.
110
+ def timeout!
111
+ terminate! Timeout
112
+ end
113
+
114
+ # Timeout the task after the given time.
115
+ def timeout_after(time)
116
+ @timeout = time
117
+
118
+ pool.__send__ :timeout_for, self, time
119
+
120
+ self
121
+ end
122
+ end
123
+
124
+ attr_reader :min, :max, :spawned, :waiting
125
+
126
+ # Create the pool with minimum and maximum threads.
127
+ #
128
+ # The pool will start with the minimum amount of threads created and will
129
+ # spawn new threads until the max is reached in case of need.
130
+ #
131
+ # A default block can be passed, which will be used to {#process} the passed
132
+ # data.
133
+ def initialize(min, max = nil, &block)
134
+ @min = min
135
+ @max = max || min
136
+ @block = block
137
+
138
+ @cond = ConditionVariable.new
139
+ @mutex = Mutex.new
140
+
141
+ @done = ConditionVariable.new
142
+ @done_mutex = Mutex.new
143
+
144
+ @todo = []
145
+ @workers = []
146
+ @timeouts = {}
147
+
148
+ @spawned = 0
149
+ @waiting = 0
150
+ @shutdown = false
151
+ @trim_requests = 0
152
+ @auto_trim = false
153
+ @idle_trim = nil
154
+ @timeout = nil
155
+
156
+ @mutex.synchronize {
157
+ min.times {
158
+ spawn_thread
159
+ }
160
+ }
161
+ end
162
+
163
+ # Check if the pool has been shut down.
164
+ def shutdown?
165
+ !!@shutdown
166
+ end
167
+
168
+ # Check if auto trimming is enabled.
169
+ def auto_trim?
170
+ @auto_trim
171
+ end
172
+
173
+ # Enable auto trimming, unneeded threads will be deleted until the minimum
174
+ # is reached.
175
+ def auto_trim!
176
+ @auto_trim = true
177
+
178
+ self
179
+ end
180
+
181
+ # Disable auto trimming.
182
+ def no_auto_trim!
183
+ @auto_trim = false
184
+
185
+ self
186
+ end
187
+
188
+ # Check if idle trimming is enabled.
189
+ def idle_trim?
190
+ !@idle_trim.nil?
191
+ end
192
+
193
+ # Enable idle trimming. Unneeded threads will be deleted after the given number of seconds of inactivity.
194
+ # The minimum number of threads is respeced.
195
+ def idle_trim!(timeout)
196
+ @idle_trim = timeout
197
+
198
+ self
199
+ end
200
+
201
+ # Turn of idle trimming.
202
+ def no_idle_trim!
203
+ @idle_trim = nil
204
+
205
+ self
206
+ end
207
+
208
+ # Resize the pool with the passed arguments.
209
+ def resize(min, max = nil)
210
+ @min = min
211
+ @max = max || min
212
+
213
+ trim!
214
+ end
215
+
216
+ # Get the amount of tasks that still have to be run.
217
+ def backlog
218
+ @mutex.synchronize {
219
+ @todo.length
220
+ }
221
+ end
222
+
223
+ # Are all tasks consumed?
224
+ def done?
225
+ @mutex.synchronize {
226
+ _done?
227
+ }
228
+ end
229
+
230
+ # Wait until all tasks are consumed. The caller will be blocked until then.
231
+ def wait(what = :idle)
232
+ case what
233
+ when :done
234
+ until done?
235
+ @done_mutex.synchronize {
236
+ break if _done?
237
+
238
+ @done.wait @done_mutex
239
+ }
240
+ end
241
+
242
+ when :idle
243
+ until idle?
244
+ @done_mutex.synchronize {
245
+ break if _idle?
246
+
247
+ @done.wait @done_mutex
248
+ }
249
+ end
250
+ end
251
+
252
+ self
253
+ end
254
+
255
+ # Check if there are idle workers.
256
+ def idle?
257
+ @mutex.synchronize {
258
+ _idle?
259
+ }
260
+ end
261
+
262
+ # Add a task to the pool which will execute the block with the given
263
+ # argument.
264
+ #
265
+ # If no block is passed the default block will be used if present, an
266
+ # ArgumentError will be raised otherwise.
267
+ def process(*args, &block)
268
+ unless block || @block
269
+ raise ArgumentError, 'you must pass a block'
270
+ end
271
+
272
+ task = Task.new(self, *args, &(block || @block))
273
+
274
+ @mutex.synchronize {
275
+ raise 'unable to add work while shutting down' if shutdown?
276
+
277
+ @todo << task
278
+
279
+ if @waiting == 0 && @spawned < @max
280
+ spawn_thread
281
+ end
282
+
283
+ @cond.signal
284
+ }
285
+
286
+ task
287
+ end
288
+
289
+ alias << process
290
+
291
+ # Trim the unused threads, if forced threads will be trimmed even if there
292
+ # are tasks waiting.
293
+ def trim(force = false)
294
+ @mutex.synchronize {
295
+ if (force || @waiting > 0) && @spawned - @trim_requests > @min
296
+ @trim_requests += 1
297
+ @cond.signal
298
+ end
299
+ }
300
+
301
+ self
302
+ end
303
+
304
+ # Force #{trim}.
305
+ def trim!
306
+ trim true
307
+ end
308
+
309
+ # Shut down the pool instantly without finishing to execute tasks.
310
+ def shutdown!
311
+ @mutex.synchronize {
312
+ @shutdown = :now
313
+ @cond.broadcast
314
+ }
315
+
316
+ wake_up_timeout
317
+
318
+ self
319
+ end
320
+
321
+ # Shut down the pool, it will block until all tasks have finished running.
322
+ def shutdown
323
+ @mutex.synchronize {
324
+ @shutdown = :nicely
325
+ @cond.broadcast
326
+ }
327
+
328
+ until @workers.empty?
329
+ if worker = @workers.first
330
+ worker.join
331
+ end
332
+ end
333
+
334
+ if @timeout
335
+ @shutdown = :now
336
+
337
+ wake_up_timeout
338
+
339
+ @timeout.join
340
+ end
341
+ end
342
+
343
+ # Shutdown the pool after a given amount of time.
344
+ def shutdown_after(timeout)
345
+ Thread.new {
346
+ sleep timeout
347
+
348
+ shutdown
349
+ }
350
+ end
351
+
352
+ class << self
353
+ # If true, tasks will allow raised exceptions to pass through.
354
+ #
355
+ # Similar to Thread.abort_on_exception
356
+ attr_accessor :abort_on_exception
357
+ end
358
+
359
+ private
360
+ def timeout_for(task, timeout)
361
+ unless @timeout
362
+ spawn_timeout_thread
363
+ end
364
+
365
+ @mutex.synchronize {
366
+ @timeouts[task] = timeout
367
+
368
+ wake_up_timeout
369
+ }
370
+ end
371
+
372
+ def wake_up_timeout
373
+ if defined? @pipes
374
+ @pipes.last.write_nonblock 'x' rescue nil
375
+ end
376
+ end
377
+
378
+ def spawn_thread
379
+ @spawned += 1
380
+
381
+ thread = Thread.new {
382
+ loop do
383
+ task = @mutex.synchronize {
384
+ if @todo.empty?
385
+ while @todo.empty?
386
+ if @trim_requests > 0
387
+ @trim_requests -= 1
388
+
389
+ break
390
+ end
391
+
392
+ break if shutdown?
393
+
394
+ @waiting += 1
395
+
396
+ done!
397
+
398
+ if @idle_trim and @spawned > @min
399
+ check_time = MonotonicTime.get + @idle_trim
400
+ @cond.wait @mutex, @idle_trim
401
+ @trim_requests += 1 if MonotonicTime.get >= check_time && @spawned - @trim_requests > @min
402
+ else
403
+ @cond.wait @mutex
404
+ end
405
+
406
+ @waiting -= 1
407
+ end
408
+
409
+ break if @todo.empty? && shutdown?
410
+ end
411
+
412
+ @todo.shift
413
+ } or break
414
+
415
+ task.execute
416
+
417
+ break if @shutdown == :now
418
+
419
+ trim if auto_trim? && @spawned > @min
420
+ end
421
+
422
+ @mutex.synchronize {
423
+ @spawned -= 1
424
+ @workers.delete thread
425
+ }
426
+ }
427
+
428
+ @workers << thread
429
+
430
+ thread
431
+ end
432
+
433
+ def spawn_timeout_thread
434
+ @pipes = IO.pipe
435
+ @timeout = Thread.new {
436
+ loop do
437
+ now = MonotonicTime.get
438
+ timeout = @timeouts.map {|task, time|
439
+ next unless task.started_at
440
+
441
+ now - task.started_at + task.timeout
442
+ }.compact.min unless @timeouts.empty?
443
+
444
+ readable, = IO.select([@pipes.first], nil, nil, timeout)
445
+
446
+ break if @shutdown == :now
447
+
448
+ if readable && !readable.empty?
449
+ readable.first.read_nonblock 1024
450
+ end
451
+
452
+ now = MonotonicTime.get
453
+ @timeouts.each {|task, time|
454
+ next if !task.started_at || task.terminated? || task.finished?
455
+
456
+ if now > task.started_at + task.timeout
457
+ task.timeout!
458
+ end
459
+ }
460
+
461
+ @timeouts.reject! { |task, _| task.terminated? || task.finished? }
462
+
463
+ break if @shutdown == :now
464
+ end
465
+ }
466
+ end
467
+
468
+ def done!
469
+ @done_mutex.synchronize {
470
+ @done.broadcast if _done? or _idle?
471
+ }
472
+ end
473
+
474
+ def _done?
475
+ @todo.empty? and @waiting == @spawned
476
+ end
477
+
478
+ def _idle?
479
+ @todo.length < @waiting
480
+ end
481
+ end
482
+ end