thimble 0.0.8 → 0.2.0
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.
- checksums.yaml +5 -5
- data/lib/{Manager.rb → manager.rb} +41 -21
- data/lib/{QueueItem.rb → queue_item.rb} +6 -4
- data/lib/{Thimble.rb → thimble.rb} +46 -40
- data/lib/{ThimbleQueue.rb → thimble_queue.rb} +47 -34
- metadata +10 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1fbffaeb62da2fbbb2c0cfa474aa2411b0bc32e32736af38f20e83890921c3af
|
4
|
+
data.tar.gz: 78ce3f45be02b118af41229ecd82ede236e3b8817cb9f7dfe742aad6171a560f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8df7d0f0444515437e2d70038d399f85d3732456b299c2ab92f128abc5b7cfbde4f9ec71221c2b1a0c6e76dd9dcfefe91358d27582086ed5135ea9dc4974062a
|
7
|
+
data.tar.gz: 61f615dd222a89da1624f96ea35ca308db5fd2137e59395753e4db2dab6c66917d224667d158525b64574eee18c06156960f99e70b5263dce2746536e257ad56
|
@@ -1,11 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Thread.abort_on_exception = true
|
4
|
+
|
1
5
|
module Thimble
|
2
6
|
class Manager
|
3
7
|
attr_reader :max_workers, :batch_size, :queue_size, :worker_type
|
4
|
-
|
5
|
-
|
6
|
-
raise ArgumentError
|
7
|
-
|
8
|
-
|
8
|
+
|
9
|
+
def initialize(max_workers: 6, batch_size: 1000, queue_size: 1000, worker_type: :fork)
|
10
|
+
raise ArgumentError, 'worker type must be either :fork or :thread' unless %i[thread fork].include?(worker_type)
|
11
|
+
unless worker_type == :thread || Process.respond_to?(:fork)
|
12
|
+
raise ArgumentError, 'Your system does not respond to fork please use threads.'
|
13
|
+
end
|
14
|
+
raise ArgumentError, 'max_workers must be greater than 0' if max_workers < 1
|
15
|
+
raise ArgumentError, 'batch size must be greater than 0' if batch_size < 1
|
16
|
+
|
9
17
|
@worker_type = worker_type
|
10
18
|
@max_workers = max_workers
|
11
19
|
@batch_size = batch_size
|
@@ -14,16 +22,20 @@ module Thimble
|
|
14
22
|
@current_workers = {}
|
15
23
|
end
|
16
24
|
|
25
|
+
# @return [TrueClass, FalseClass]
|
17
26
|
def worker_available?
|
18
27
|
@current_workers.size < @max_workers
|
19
28
|
end
|
20
29
|
|
30
|
+
# @return [TrueClass, FalseClass]
|
21
31
|
def working?
|
22
|
-
@current_workers.size
|
32
|
+
@current_workers.size.positive?
|
23
33
|
end
|
24
34
|
|
35
|
+
# @param [Object] id
|
25
36
|
def sub_worker(worker, id)
|
26
|
-
raise
|
37
|
+
raise 'Worker must contain a pid!' if worker.pid.nil?
|
38
|
+
|
27
39
|
new_worker = OpenStruct.new
|
28
40
|
new_worker.worker = worker
|
29
41
|
new_worker.id = id
|
@@ -32,40 +44,44 @@ module Thimble
|
|
32
44
|
end
|
33
45
|
end
|
34
46
|
|
47
|
+
# @param [Object] worker
|
35
48
|
def rem_worker(worker)
|
36
49
|
@mutex.synchronize do
|
37
50
|
@current_workers.delete(worker.pid)
|
38
51
|
end
|
39
52
|
end
|
40
53
|
|
54
|
+
# @param [Object] id
|
41
55
|
def current_workers(id)
|
42
56
|
@mutex.synchronize do
|
43
|
-
@current_workers.select { |
|
57
|
+
@current_workers.select { |_k, v| v.id == id }
|
44
58
|
end
|
45
59
|
end
|
46
60
|
|
47
|
-
|
61
|
+
# @param [Object] batch
|
62
|
+
# @param [Proc] block
|
63
|
+
# @return [Object]
|
64
|
+
def get_worker(batch, &block)
|
48
65
|
@mutex.synchronize do
|
49
66
|
if @worker_type == :fork
|
50
|
-
get_fork_worker(batch, &
|
67
|
+
get_fork_worker(batch, &block)
|
51
68
|
else
|
52
|
-
get_thread_worker(batch, &
|
69
|
+
get_thread_worker(batch, &block)
|
53
70
|
end
|
54
71
|
end
|
55
72
|
end
|
56
73
|
|
74
|
+
# @param [Object] batch
|
57
75
|
def get_fork_worker(batch)
|
58
76
|
rd, wr = IO.pipe
|
59
77
|
tup = OpenStruct.new
|
60
78
|
pid = fork do
|
61
|
-
Signal.trap(
|
62
|
-
rd.close
|
79
|
+
Signal.trap('HUP') { exit }
|
80
|
+
rd.close
|
63
81
|
t = Marshal.dump(batch.item.map do |item|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
e
|
68
|
-
end
|
82
|
+
yield item.item
|
83
|
+
rescue Exception => e
|
84
|
+
e
|
69
85
|
end)
|
70
86
|
wr.write(t)
|
71
87
|
wr.close
|
@@ -76,6 +92,8 @@ module Thimble
|
|
76
92
|
tup
|
77
93
|
end
|
78
94
|
|
95
|
+
# @param [Object] batch
|
96
|
+
# @param [Proc] block
|
79
97
|
def get_thread_worker(batch)
|
80
98
|
tup = OpenStruct.new
|
81
99
|
tup.pid = Thread.new do
|
@@ -87,12 +105,14 @@ module Thimble
|
|
87
105
|
tup
|
88
106
|
end
|
89
107
|
|
108
|
+
# @return [Thimble::Manager]
|
90
109
|
def self.deterministic
|
91
|
-
|
110
|
+
new(max_workers: 1, batch_size: 1, queue_size: 1)
|
92
111
|
end
|
93
112
|
|
113
|
+
# @return [Thimble::Manager]
|
94
114
|
def self.small
|
95
|
-
|
115
|
+
new(max_workers: 1, batch_size: 3, queue_size: 3)
|
96
116
|
end
|
97
117
|
end
|
98
|
-
end
|
118
|
+
end
|
@@ -1,8 +1,11 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
2
4
|
module Thimble
|
3
5
|
class QueueItem
|
4
6
|
attr_reader :id, :item
|
5
|
-
|
7
|
+
|
8
|
+
def initialize(item, name = 'Item')
|
6
9
|
@id = Digest::SHA256.digest(rand(10**100).to_s + Time.now.to_i.to_s)
|
7
10
|
@item = item
|
8
11
|
@name = name
|
@@ -11,6 +14,5 @@ module Thimble
|
|
11
14
|
def to_s
|
12
15
|
"#{@name}: #{@item} ID: #{@id}"
|
13
16
|
end
|
14
|
-
|
15
17
|
end
|
16
|
-
end
|
18
|
+
end
|
@@ -1,41 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
2
|
-
require_relative '
|
3
|
-
require_relative '
|
4
|
-
require_relative '
|
3
|
+
require_relative 'manager'
|
4
|
+
require_relative 'thimble_queue'
|
5
|
+
require_relative 'queue_item'
|
5
6
|
require 'io/wait'
|
6
7
|
require 'ostruct'
|
7
8
|
|
8
9
|
module Thimble
|
9
|
-
|
10
10
|
class Thimble < ThimbleQueue
|
11
|
-
def initialize(array, manager = Manager.new, result = nil, name =
|
12
|
-
raise ArgumentError
|
13
|
-
|
11
|
+
def initialize(array, manager = Manager.new, result = nil, name = 'Main')
|
12
|
+
raise ArgumentError, 'You need to pass a manager to Thimble!' unless manager.instance_of?(Manager)
|
13
|
+
|
14
|
+
unless array.respond_to? :each
|
15
|
+
raise ArgumentError,
|
16
|
+
'There needs to be an iterable object passed to Thimble to start.'
|
17
|
+
end
|
18
|
+
|
14
19
|
@result = if result.nil?
|
15
|
-
|
16
|
-
|
17
|
-
|
20
|
+
ThimbleQueue.new(array.size, 'Result')
|
21
|
+
else
|
22
|
+
result
|
23
|
+
end
|
24
|
+
unless @result.instance_of?(ThimbleQueue) && !@result.closed?
|
25
|
+
raise ArgumentError,
|
26
|
+
'result needs to be an open ThimbleQueue'
|
18
27
|
end
|
19
|
-
|
28
|
+
|
20
29
|
@manager = manager
|
21
30
|
@running = true
|
22
31
|
super(array.size, name)
|
23
32
|
@logger.debug("loading thimble #{name}")
|
24
|
-
array.each {|item| push(item)}
|
33
|
+
array.each { |item| push(item) }
|
25
34
|
@logger.debug("finished loading thimble #{name}")
|
26
|
-
close
|
35
|
+
close
|
27
36
|
end
|
28
37
|
|
29
38
|
# This will use the manager and transform your thimble queue.
|
30
|
-
# requires a block
|
39
|
+
# requires a block
|
31
40
|
# @return [ThimbleQueue]
|
32
|
-
def map
|
41
|
+
def map(&block)
|
33
42
|
@logger.debug("starting map in #{@name} with id #{Thread.current.object_id}")
|
34
43
|
@running = true
|
35
|
-
while @running
|
36
|
-
|
37
|
-
end
|
38
|
-
@result.close()
|
44
|
+
manage_workers(&block) while @running
|
45
|
+
@result.close
|
39
46
|
@logger.debug("finishing map in #{@name} with id #{Thread.current.object_id}")
|
40
47
|
@result
|
41
48
|
end
|
@@ -44,46 +51,47 @@ module Thimble
|
|
44
51
|
# Will return the result instantly, so you can use it for next stage processing.
|
45
52
|
# requires a block
|
46
53
|
# @return [ThimbleQueue]
|
47
|
-
|
54
|
+
# @param [Proc] block
|
55
|
+
def map_async(&block)
|
48
56
|
@logger.debug("starting async map in #{@name} with id #{Thread.current.object_id}")
|
49
57
|
@logger.debug("queue: #{@queue}")
|
50
58
|
Thimble.async do
|
51
|
-
map
|
59
|
+
map(&block)
|
52
60
|
end
|
53
61
|
@logger.debug("finished async map in #{@name} with id #{Thread.current.object_id}")
|
54
62
|
@result
|
55
63
|
end
|
56
64
|
|
57
|
-
# Will perform anything handed to this asynchronously.
|
65
|
+
# Will perform anything handed to this asynchronously.
|
58
66
|
# Requires a block
|
59
67
|
# @return [Thread]
|
60
|
-
def self.async
|
61
|
-
Thread.new
|
62
|
-
yield e
|
63
|
-
end
|
68
|
+
def self.async(&block)
|
69
|
+
Thread.new(&block)
|
64
70
|
end
|
65
71
|
|
66
72
|
private
|
73
|
+
|
67
74
|
def get_batch
|
68
75
|
batch = []
|
69
76
|
while batch.size < @manager.batch_size
|
70
77
|
item = self.next
|
71
78
|
if item.nil?
|
72
|
-
return nil if batch.size
|
73
|
-
|
79
|
+
return nil if batch.size.zero?
|
80
|
+
|
81
|
+
return QueueItem.new(batch, 'Batch')
|
74
82
|
else
|
75
83
|
batch << item
|
76
84
|
end
|
77
85
|
end
|
78
|
-
QueueItem.new(batch,
|
86
|
+
QueueItem.new(batch, 'Batch')
|
79
87
|
end
|
80
88
|
|
81
|
-
def manage_workers
|
82
|
-
@manager.current_workers(@id).each do |
|
89
|
+
def manage_workers(&block)
|
90
|
+
@manager.current_workers(@id).each do |_pid, pair|
|
83
91
|
get_result(pair.worker)
|
84
92
|
end
|
85
|
-
while
|
86
|
-
@manager.sub_worker(
|
93
|
+
while @manager.worker_available? && batch = get_batch
|
94
|
+
@manager.sub_worker(@manager.get_worker(batch, &block), @id)
|
87
95
|
end
|
88
96
|
@running = false if !@manager.working? && !batch
|
89
97
|
end
|
@@ -95,20 +103,18 @@ module Thimble
|
|
95
103
|
loadedResult = Marshal.load(piped_result)
|
96
104
|
loadedResult.each { |r| raise r if r.class <= Exception }
|
97
105
|
push_result(loadedResult)
|
98
|
-
Process.kill(
|
99
|
-
@manager.rem_worker(tuple)
|
100
|
-
end
|
101
|
-
else
|
102
|
-
if tuple.done == true
|
103
|
-
push_result(tuple.result)
|
106
|
+
Process.kill('HUP', tuple.pid)
|
104
107
|
@manager.rem_worker(tuple)
|
105
108
|
end
|
109
|
+
elsif tuple.done == true
|
110
|
+
push_result(tuple.result)
|
111
|
+
@manager.rem_worker(tuple)
|
106
112
|
end
|
107
113
|
end
|
108
114
|
|
109
115
|
def push_result(result)
|
110
116
|
if result.respond_to? :each
|
111
|
-
result.each {|r| @result.push(r)}
|
117
|
+
result.each { |r| @result.push(r) }
|
112
118
|
else
|
113
119
|
@result.push(result)
|
114
120
|
end
|
@@ -1,13 +1,16 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'logger'
|
3
|
-
require_relative '
|
4
|
+
require_relative 'queue_item'
|
4
5
|
|
5
6
|
module Thimble
|
7
|
+
# noinspection RubyTooManyInstanceVariablesInspection
|
6
8
|
class ThimbleQueue
|
7
|
-
include Enumerable
|
8
|
-
attr_reader :size
|
9
9
|
def initialize(size, name)
|
10
|
-
|
10
|
+
unless size >= 1
|
11
|
+
raise ArgumentError, "make sure there is a size for the queue greater than 1! size received #{size}"
|
12
|
+
end
|
13
|
+
|
11
14
|
@id = Digest::SHA256.digest(rand(10**100).to_s + Time.now.to_i.to_s)
|
12
15
|
@name = name
|
13
16
|
@size = size
|
@@ -17,51 +20,56 @@ module Thimble
|
|
17
20
|
@close_now = false
|
18
21
|
@empty = ConditionVariable.new
|
19
22
|
@full = ConditionVariable.new
|
20
|
-
@logger = Logger.new(
|
23
|
+
@logger = Logger.new($stdout)
|
21
24
|
@logger.sev_threshold = Logger::UNKNOWN
|
22
25
|
end
|
23
26
|
|
24
|
-
|
27
|
+
include Enumerable
|
28
|
+
attr_reader :size
|
29
|
+
|
30
|
+
def set_logger(level)
|
25
31
|
@logger.sev_threshold = level
|
26
32
|
end
|
27
33
|
|
28
34
|
def each
|
29
|
-
while item = self.next
|
35
|
+
while (item = self.next)
|
30
36
|
yield item.item
|
31
37
|
end
|
32
38
|
end
|
33
39
|
|
34
40
|
# Returns the size of the ThimbleQueue
|
35
|
-
# @return [
|
41
|
+
# @return [Integer]
|
36
42
|
def length
|
37
43
|
size
|
38
44
|
end
|
39
45
|
|
40
46
|
# Will concatenate an enumerable to the ThimbleQueue
|
41
|
-
# @param [Enumerable]
|
42
47
|
# @return [ThimbleQueue]
|
48
|
+
# @param [Module<Enumerable>] other
|
43
49
|
def +(other)
|
44
|
-
raise ArgumentError
|
50
|
+
raise ArgumentError, '+ requires another Enumerable!' unless other.class < Enumerable
|
51
|
+
|
45
52
|
merged_thimble = ThimbleQueue.new(length + other.length, @name)
|
46
|
-
|
47
|
-
other.each {|item| merged_thimble.push(item)}
|
53
|
+
each { |item| merged_thimble.push(item) }
|
54
|
+
other.each { |item| merged_thimble.push(item) }
|
48
55
|
merged_thimble
|
49
56
|
end
|
50
57
|
|
51
58
|
# Returns the first item in the queue
|
52
59
|
# @return [Object]
|
53
60
|
def next
|
54
|
-
@mutex.synchronize
|
55
|
-
|
61
|
+
@mutex.synchronize do
|
62
|
+
until @close_now
|
56
63
|
a = @queue.shift
|
57
64
|
@logger.debug("#{@name}'s queue shifted to: #{a}")
|
58
65
|
if !a.nil?
|
59
66
|
@full.broadcast
|
60
67
|
@empty.broadcast
|
61
68
|
return a
|
62
|
-
else
|
69
|
+
else
|
63
70
|
@logger.debug("#{@name}'s queue is currently closed?: #{closed?}")
|
64
71
|
return nil if closed?
|
72
|
+
|
65
73
|
@empty.wait(@mutex)
|
66
74
|
end
|
67
75
|
end
|
@@ -69,46 +77,49 @@ module Thimble
|
|
69
77
|
end
|
70
78
|
|
71
79
|
# This will push whatever it is handed to the queue
|
72
|
-
# @param [Object]
|
73
|
-
def push(
|
74
|
-
raise
|
75
|
-
|
80
|
+
# @param [Object] input_item
|
81
|
+
def push(input_item)
|
82
|
+
raise 'Queue is closed!' if @closed
|
83
|
+
|
84
|
+
@logger.debug("Pushing into #{@name} values: #{input_item}")
|
76
85
|
@mutex.synchronize do
|
77
|
-
|
86
|
+
until offer(input_item)
|
78
87
|
@full.wait(@mutex)
|
79
88
|
@logger.debug("#{@name} is waiting on full")
|
80
89
|
end
|
81
90
|
@empty.broadcast
|
82
91
|
end
|
83
|
-
@logger.debug("Finished pushing int #{@name}: #{
|
92
|
+
@logger.debug("Finished pushing int #{@name}: #{input_item}")
|
84
93
|
end
|
85
94
|
|
86
95
|
# This will flatten any nested arrays out and feed them one at
|
87
96
|
# a time to the queue.
|
88
|
-
# @param [Object, Enumerable]
|
89
97
|
# @return [nil]
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
98
|
+
# @param [Object] input_item
|
99
|
+
def push_flat(input_item)
|
100
|
+
raise 'Queue is closed!' if @closed
|
101
|
+
|
102
|
+
@logger.debug("Pushing flat into #{@name} values: #{input_item}")
|
103
|
+
if input_item.respond_to? :each
|
104
|
+
input_item.each { |item| push(item) }
|
95
105
|
else
|
96
106
|
@mutex.synchronize do
|
97
|
-
|
107
|
+
until offer(input_item)
|
98
108
|
@logger.debug("#{@name} is waiting on full")
|
99
109
|
@full.wait(@mutex)
|
100
110
|
end
|
101
111
|
@empty.broadcast
|
102
112
|
end
|
103
113
|
end
|
104
|
-
@logger.debug("Finished pushing flat into #{@name} values: #{
|
114
|
+
@logger.debug("Finished pushing flat into #{@name} values: #{input_item}")
|
105
115
|
end
|
106
116
|
|
107
|
-
# Closes the
|
117
|
+
# Closes the ThimbleQueue
|
108
118
|
# @param [TrueClass, FalseClass]
|
109
119
|
# @return [nil]
|
110
120
|
def close(now = false)
|
111
|
-
raise ArgumentError
|
121
|
+
raise ArgumentError, 'now must be true or false' unless [true, false].include?(now)
|
122
|
+
|
112
123
|
@logger.debug("#{@name} is closing")
|
113
124
|
@mutex.synchronize do
|
114
125
|
@closed = true
|
@@ -123,7 +134,7 @@ module Thimble
|
|
123
134
|
# @return [Array[Object]]
|
124
135
|
def to_a
|
125
136
|
a = []
|
126
|
-
while item = self.next
|
137
|
+
while (item = self.next)
|
127
138
|
a << item.item
|
128
139
|
end
|
129
140
|
a
|
@@ -136,6 +147,8 @@ module Thimble
|
|
136
147
|
end
|
137
148
|
|
138
149
|
private
|
150
|
+
|
151
|
+
# @param [Object] x
|
139
152
|
def offer(x)
|
140
153
|
if @queue.size < @size
|
141
154
|
@queue << QueueItem.new(x)
|
@@ -146,4 +159,4 @@ module Thimble
|
|
146
159
|
end
|
147
160
|
end
|
148
161
|
end
|
149
|
-
end
|
162
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: thimble
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kovanda
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-06-29 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Thimble is a ruby gem for parallelism and concurrency. It allows you
|
14
14
|
to decide if you want to use separate processes, or if you want to use threads in
|
@@ -19,15 +19,15 @@ executables: []
|
|
19
19
|
extensions: []
|
20
20
|
extra_rdoc_files: []
|
21
21
|
files:
|
22
|
-
- lib/
|
23
|
-
- lib/
|
24
|
-
- lib/
|
25
|
-
- lib/
|
22
|
+
- lib/manager.rb
|
23
|
+
- lib/queue_item.rb
|
24
|
+
- lib/thimble.rb
|
25
|
+
- lib/thimble_queue.rb
|
26
26
|
homepage: https://github.com/akovanda/thimble
|
27
27
|
licenses:
|
28
28
|
- MIT
|
29
29
|
metadata: {}
|
30
|
-
post_install_message:
|
30
|
+
post_install_message:
|
31
31
|
rdoc_options: []
|
32
32
|
require_paths:
|
33
33
|
- lib
|
@@ -42,9 +42,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
42
|
- !ruby/object:Gem::Version
|
43
43
|
version: '0'
|
44
44
|
requirements: []
|
45
|
-
|
46
|
-
|
47
|
-
signing_key:
|
45
|
+
rubygems_version: 3.3.7
|
46
|
+
signing_key:
|
48
47
|
specification_version: 4
|
49
48
|
summary: Concurrency and Parallelism gem that uses blocks to move data
|
50
49
|
test_files: []
|