vinted-thread 0.1.1

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,158 @@
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
+
11
+ require 'thread'
12
+
13
+ # A future is an object that incapsulates a block which is called in a
14
+ # different thread, upon retrieval the caller gets blocked until the block has
15
+ # finished running, and its result is returned and cached.
16
+ class Thread::Future
17
+ Cancel = Class.new(Exception)
18
+
19
+ # Create a future with the passed block and optionally using the passed pool.
20
+ def initialize (pool = nil, &block)
21
+ raise ArgumentError, 'no block given' unless block
22
+
23
+ @mutex = Mutex.new
24
+
25
+ task = proc {
26
+ begin
27
+ deliver block.call
28
+ rescue Exception => e
29
+ @exception = e
30
+
31
+ deliver nil
32
+ end
33
+ }
34
+
35
+ @thread = pool ? pool.process(&task) : Thread.new(&task)
36
+
37
+ ObjectSpace.define_finalizer self, self.class.finalizer(@thread)
38
+ end
39
+
40
+ # @private
41
+ def self.finalizer (thread)
42
+ proc {
43
+ thread.raise Cancel.new
44
+ }
45
+ end
46
+
47
+ # Check if an exception has been raised.
48
+ def exception?
49
+ @mutex.synchronize {
50
+ instance_variable_defined? :@exception
51
+ }
52
+ end
53
+
54
+ # Return the raised exception.
55
+ def exception
56
+ @mutex.synchronize {
57
+ @exception
58
+ }
59
+ end
60
+
61
+ # Check if the future has been called.
62
+ def delivered?
63
+ @mutex.synchronize {
64
+ instance_variable_defined? :@value
65
+ }
66
+ end
67
+
68
+ alias realized? delivered?
69
+
70
+ # Cancel the future, {#value} will yield a Cancel exception
71
+ def cancel
72
+ return self if delivered?
73
+
74
+ @mutex.synchronize {
75
+ @thread.raise Cancel.new('future cancelled')
76
+ }
77
+
78
+ self
79
+ end
80
+
81
+ # Check if the future has been cancelled
82
+ def cancelled?
83
+ @mutex.synchronize {
84
+ @exception.is_a? Cancel
85
+ }
86
+ end
87
+
88
+ # Get the value of the future, if it's not finished running this call will block.
89
+ #
90
+ # In case the block raises an exception, it will be raised, the exception is cached
91
+ # and will be raised every time you access the value.
92
+ #
93
+ # An optional timeout can be passed which will return nil if nothing has been
94
+ # delivered.
95
+ def value (timeout = nil)
96
+ raise @exception if exception?
97
+
98
+ return @value if delivered?
99
+
100
+ @mutex.synchronize {
101
+ cond.wait(@mutex, *timeout)
102
+ }
103
+
104
+ if exception?
105
+ raise @exception
106
+ elsif delivered?
107
+ return @value
108
+ end
109
+ end
110
+
111
+ alias ~ value
112
+
113
+ # Do the same as {#value}, but return nil in case of exception.
114
+ def value! (timeout = nil)
115
+ begin
116
+ value(timeout)
117
+ rescue Exception
118
+ nil
119
+ end
120
+ end
121
+
122
+ alias ! value!
123
+
124
+ private
125
+ def cond?
126
+ instance_variable_defined? :@cond
127
+ end
128
+
129
+ def cond
130
+ @cond ||= ConditionVariable.new
131
+ end
132
+
133
+ def deliver (value)
134
+ return if delivered?
135
+
136
+ @mutex.synchronize {
137
+ @value = value
138
+
139
+ cond.broadcast if cond?
140
+ }
141
+
142
+ self
143
+ end
144
+ end
145
+
146
+ class Thread
147
+ # Helper to create a future
148
+ def self.future (pool = nil, &block)
149
+ Thread::Future.new(pool, &block)
150
+ end
151
+ end
152
+
153
+ module Kernel
154
+ # Helper to create a future.
155
+ def future (pool = nil, &block)
156
+ Thread::Future.new(pool, &block)
157
+ end
158
+ end
@@ -0,0 +1,119 @@
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
+
11
+ require 'thread'
12
+
13
+ # A pipe lets you execute various tasks on a set of data in parallel,
14
+ # each datum inserted in the pipe is passed along through queues to the various
15
+ # functions composing the pipe, the final result is inserted in the final queue.
16
+ class Thread::Pipe
17
+ # A task incapsulates a part of the pipe.
18
+ class Task
19
+ attr_accessor :input, :output
20
+
21
+ # Create a Task which will call the passed function and get input
22
+ # from the optional parameter and put output in the optional parameter.
23
+ def initialize (func, input = Queue.new, output = Queue.new)
24
+ @input = input
25
+ @output = output
26
+ @handling = false
27
+
28
+ @thread = Thread.new {
29
+ while true
30
+ value = @input.deq
31
+
32
+ @handling = true
33
+ begin
34
+ value = func.call(value)
35
+ @output.enq value
36
+ rescue Exception; end
37
+ @handling = false
38
+ end
39
+ }
40
+ end
41
+
42
+ # Check if the task has nothing to do.
43
+ def empty?
44
+ !@handling && @input.empty? && @output.empty?
45
+ end
46
+
47
+ # Stop the task.
48
+ def kill
49
+ @thread.raise
50
+ end
51
+ end
52
+
53
+ # Create a pipe using the optionally passed objects as input and
54
+ # output queue.
55
+ #
56
+ # The objects must respond to #enq and #deq, and block on #deq.
57
+ def initialize (input = Queue.new, output = Queue.new)
58
+ @tasks = []
59
+
60
+ @input = input
61
+ @output = output
62
+
63
+ ObjectSpace.define_finalizer self, self.class.finalizer(@tasks)
64
+ end
65
+
66
+ # @private
67
+ def self.finalizer (tasks)
68
+ proc {
69
+ tasks.each(&:kill)
70
+ }
71
+ end
72
+
73
+ # Add a task to the pipe, it must respond to #call and #arity,
74
+ # and #arity must return 1.
75
+ def | (func)
76
+ if func.arity != 1
77
+ raise ArgumentError, 'wrong arity'
78
+ end
79
+
80
+ Task.new(func, (@tasks.empty? ? @input : Queue.new), @output).tap {|t|
81
+ @tasks.last.output = t.input unless @tasks.empty?
82
+ @tasks << t
83
+ }
84
+
85
+ self
86
+ end
87
+
88
+ # Check if the pipe is empty.
89
+ def empty?
90
+ @input.empty? && @output.empty? && @tasks.all?(&:empty?)
91
+ end
92
+
93
+ # Insert data in the pipe.
94
+ def enq (data)
95
+ return if @tasks.empty?
96
+
97
+ @input.enq data
98
+
99
+ self
100
+ end
101
+
102
+ alias push enq
103
+ alias << enq
104
+
105
+ # Get an element from the output queue.
106
+ def deq (non_block = false)
107
+ @output.deq(non_block)
108
+ end
109
+
110
+ alias pop deq
111
+ alias ~ deq
112
+ end
113
+
114
+ class Thread
115
+ # Helper to create a pipe.
116
+ def self.| (func)
117
+ Pipe.new | func
118
+ end
119
+ end
@@ -0,0 +1,428 @@
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
+
11
+ require 'thread'
12
+
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 Thread::Pool
19
+ # A task incapsulates a block being ran by the pool and the arguments to pass
20
+ # to it.
21
+ class Task
22
+ Timeout = Class.new(Exception)
23
+ Asked = Class.new(Exception)
24
+
25
+ attr_reader :pool, :timeout, :exception, :thread, :started_at
26
+
27
+ # Create a task in the given pool which will pass the arguments to the
28
+ # block.
29
+ def initialize (pool, *args, &block)
30
+ @pool = pool
31
+ @arguments = args
32
+ @block = block
33
+
34
+ @running = false
35
+ @finished = false
36
+ @timedout = false
37
+ @terminated = false
38
+ end
39
+
40
+ def running?; @running; end
41
+ def finished?; @finished; end
42
+ def timeout?; @timedout; end
43
+ def terminated?; @terminated; end
44
+
45
+ # Execute the task in the given thread.
46
+ def execute (thread)
47
+ return if terminated? || running? || finished?
48
+
49
+ @thread = thread
50
+ @running = true
51
+ @started_at = Time.now
52
+
53
+ pool.__send__ :wake_up_timeout
54
+
55
+ begin
56
+ @block.call(*@arguments)
57
+ rescue Exception => reason
58
+ if reason.is_a? Timeout
59
+ @timedout = true
60
+ elsif reason.is_a? Asked
61
+ return
62
+ else
63
+ @exception = reason
64
+ end
65
+ end
66
+
67
+ @running = false
68
+ @finished = true
69
+ @thread = nil
70
+ end
71
+
72
+ # Raise an exception in the thread used by the task.
73
+ def raise (exception)
74
+ @thread.raise(exception)
75
+ end
76
+
77
+ # Terminate the exception with an optionally given exception.
78
+ def terminate! (exception = Asked)
79
+ return if terminated? || finished? || timeout?
80
+
81
+ @terminated = true
82
+
83
+ return unless running?
84
+
85
+ self.raise exception
86
+ end
87
+
88
+ # Force the task to timeout.
89
+ def timeout!
90
+ terminate! Timeout
91
+ end
92
+
93
+ # Timeout the task after the given time.
94
+ def timeout_after (time)
95
+ @timeout = time
96
+
97
+ pool.timeout_for self, time
98
+
99
+ self
100
+ end
101
+ end
102
+
103
+ attr_reader :min, :max, :spawned
104
+
105
+ # Create the pool with minimum and maximum threads.
106
+ #
107
+ # The pool will start with the minimum amount of threads created and will
108
+ # spawn new threads until the max is reached in case of need.
109
+ #
110
+ # A default block can be passed, which will be used to {#process} the passed
111
+ # data.
112
+ def initialize (min, max = nil, &block)
113
+ @min = min
114
+ @max = max || min
115
+ @block = block
116
+
117
+ @cond = ConditionVariable.new
118
+ @mutex = Mutex.new
119
+
120
+ @done = ConditionVariable.new
121
+ @done_mutex = Mutex.new
122
+
123
+ @todo = []
124
+ @workers = []
125
+ @timeouts = {}
126
+
127
+ @spawned = 0
128
+ @waiting = 0
129
+ @shutdown = false
130
+ @trim_requests = 0
131
+ @auto_trim = false
132
+ @idle_trim = nil
133
+
134
+ @mutex.synchronize {
135
+ min.times {
136
+ spawn_thread
137
+ }
138
+ }
139
+ end
140
+
141
+ # Check if the pool has been shut down.
142
+ def shutdown?; !!@shutdown; end
143
+
144
+ # Check if auto trimming is enabled.
145
+ def auto_trim?
146
+ @auto_trim
147
+ end
148
+
149
+ # Enable auto trimming, unneeded threads will be deleted until the minimum
150
+ # is reached.
151
+ def auto_trim!
152
+ @auto_trim = true
153
+ end
154
+
155
+ # Disable auto trimming.
156
+ def no_auto_trim!
157
+ @auto_trim = false
158
+ end
159
+
160
+ # Check if idle trimming is enabled.
161
+ def idle_trim?
162
+ !@idle_trim.nil?
163
+ end
164
+
165
+ # Enable idle trimming. Unneeded threads will be deleted after the given number of seconds of inactivity.
166
+ # The minimum number of threads is respeced.
167
+ def idle_trim!(timeout)
168
+ @idle_trim = timeout
169
+ end
170
+
171
+ # Turn of idle trimming.
172
+ def no_idle_trim!
173
+ @idle_trim = nil
174
+ end
175
+
176
+ # Resize the pool with the passed arguments.
177
+ def resize (min, max = nil)
178
+ @min = min
179
+ @max = max || min
180
+
181
+ trim!
182
+ end
183
+
184
+ # Get the amount of tasks that still have to be run.
185
+ def backlog
186
+ @mutex.synchronize {
187
+ @todo.length
188
+ }
189
+ end
190
+
191
+ # Are all tasks consumed ?
192
+ def done?
193
+ @todo.empty? and @waiting == @spawned
194
+ end
195
+
196
+ # Wait until all tasks are consumed. The caller will be blocked until then.
197
+ def wait_done
198
+ @done_mutex.synchronize {
199
+ return if done?
200
+ @done.wait @done_mutex
201
+ }
202
+ end
203
+
204
+ # Add a task to the pool which will execute the block with the given
205
+ # argument.
206
+ #
207
+ # If no block is passed the default block will be used if present, an
208
+ # ArgumentError will be raised otherwise.
209
+ def process (*args, &block)
210
+ unless block || @block
211
+ raise ArgumentError, 'you must pass a block'
212
+ end
213
+
214
+ task = Task.new(self, *args, &(block || @block))
215
+
216
+ @mutex.synchronize {
217
+ raise 'unable to add work while shutting down' if shutdown?
218
+
219
+ @todo << task
220
+
221
+ if @waiting == 0 && @spawned < @max
222
+ spawn_thread
223
+ end
224
+
225
+ @cond.signal
226
+ }
227
+
228
+ task
229
+ end
230
+
231
+ alias << process
232
+
233
+ # Trim the unused threads, if forced threads will be trimmed even if there
234
+ # are tasks waiting.
235
+ def trim (force = false)
236
+ @mutex.synchronize {
237
+ if (force || @waiting > 0) && @spawned - @trim_requests > @min
238
+ @trim_requests += 1
239
+ @cond.signal
240
+ end
241
+ }
242
+
243
+ self
244
+ end
245
+
246
+ # Force #{trim}.
247
+ def trim!
248
+ trim true
249
+ end
250
+
251
+ # Shut down the pool instantly without finishing to execute tasks.
252
+ def shutdown!
253
+ @mutex.synchronize {
254
+ @shutdown = :now
255
+ @cond.broadcast
256
+ }
257
+
258
+ wake_up_timeout
259
+
260
+ self
261
+ end
262
+
263
+ # Shut down the pool, it will block until all tasks have finished running.
264
+ def shutdown
265
+ @mutex.synchronize {
266
+ @shutdown = :nicely
267
+ @cond.broadcast
268
+ }
269
+
270
+ join
271
+
272
+ if @timeout
273
+ @shutdown = :now
274
+
275
+ wake_up_timeout
276
+
277
+ @timeout.join
278
+ end
279
+
280
+ self
281
+ end
282
+
283
+ # Join on all threads in the pool.
284
+ def join
285
+ until @workers.empty?
286
+ if worker = @workers.first
287
+ worker.join
288
+ end
289
+ end
290
+
291
+ self
292
+ end
293
+
294
+ # Define a timeout for a task.
295
+ def timeout_for (task, timeout)
296
+ unless @timeout
297
+ spawn_timeout_thread
298
+ end
299
+
300
+ @mutex.synchronize {
301
+ @timeouts[task] = timeout
302
+
303
+ wake_up_timeout
304
+ }
305
+ end
306
+
307
+ # Shutdown the pool after a given amount of time.
308
+ def shutdown_after (timeout)
309
+ Thread.new {
310
+ sleep timeout
311
+
312
+ shutdown
313
+ }
314
+
315
+ self
316
+ end
317
+
318
+ private
319
+ def wake_up_timeout
320
+ if defined? @pipes
321
+ @pipes.last.write_nonblock 'x' rescue nil
322
+ end
323
+ end
324
+
325
+ def spawn_thread
326
+ @spawned += 1
327
+
328
+ thread = Thread.new {
329
+ loop do
330
+ task = @mutex.synchronize {
331
+ if @todo.empty?
332
+ while @todo.empty?
333
+ if @trim_requests > 0
334
+ @trim_requests -= 1
335
+
336
+ break
337
+ end
338
+
339
+ break if shutdown?
340
+
341
+ @waiting += 1
342
+
343
+ report_done
344
+
345
+ if @idle_trim and @spawned > @min
346
+ check_time = Time.now + @idle_trim
347
+ @cond.wait @mutex, @idle_trim
348
+ @trim_requests += 1 if Time.now >= check_time && @spawned - @trim_requests > @min
349
+ else
350
+ @cond.wait @mutex
351
+ end
352
+
353
+ @waiting -= 1
354
+ end
355
+
356
+ break if @todo.empty? && shutdown?
357
+ end
358
+
359
+ @todo.shift
360
+ } or break
361
+
362
+ task.execute(thread)
363
+
364
+ break if @shutdown == :now
365
+
366
+ trim if auto_trim? && @spawned > @min
367
+ end
368
+
369
+ @mutex.synchronize {
370
+ @spawned -= 1
371
+ @workers.delete thread
372
+ }
373
+ }
374
+
375
+ @workers << thread
376
+
377
+ thread
378
+ end
379
+
380
+ def spawn_timeout_thread
381
+ @pipes = IO.pipe
382
+ @timeout = Thread.new {
383
+ loop do
384
+ now = Time.now
385
+ timeout = @timeouts.map {|task, time|
386
+ next unless task.started_at
387
+
388
+ now - task.started_at + task.timeout
389
+ }.compact.min unless @timeouts.empty?
390
+
391
+ readable, = IO.select([@pipes.first], nil, nil, timeout)
392
+
393
+ break if @shutdown == :now
394
+
395
+ if readable && !readable.empty?
396
+ readable.first.read_nonblock 1024
397
+ end
398
+
399
+ now = Time.now
400
+ @timeouts.each {|task, time|
401
+ next if !task.started_at || task.terminated? || task.finished?
402
+
403
+ if now > task.started_at + task.timeout
404
+ task.timeout!
405
+ end
406
+ }
407
+
408
+ @timeouts.reject! { |task, _| task.terminated? || task.finished? }
409
+
410
+ break if @shutdown == :now
411
+ end
412
+ }
413
+ end
414
+
415
+ def report_done
416
+ @done_mutex.synchronize {
417
+ @done.broadcast if done?
418
+ }
419
+ end
420
+
421
+ end
422
+
423
+ class Thread
424
+ # Helper to create a pool.
425
+ def self.pool (*args, &block)
426
+ Thread::Pool.new(*args, &block)
427
+ end
428
+ end