thread_order 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: b2d68b153c8a232b3fdcb3efc2ec6799abf9e11a
4
- data.tar.gz: 5f340011a9259a523b6fa3dca0494ad8f17887aa
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZmFjZjVkN2Y3Y2Y0M2UwMzNiODEwMzM5YWMwZjA2NjBkMzhkZGU2Zg==
5
+ data.tar.gz: !binary |-
6
+ ZThiYjJhMDE4YzFkMzZkNWM2YWIxNzMyOTExNDgyZjJlM2MzYmRjYw==
5
7
  SHA512:
6
- metadata.gz: 6247a9d7f485371a1725e7098a2dde38b4376a822ecfcb6ea7a69c36d8a1abc7d60bded9f12c3c5297bd635c05e4c87c271bc73054f19724068e2f2a62ed979c
7
- data.tar.gz: 988632f09cc15d15fa705e87a561d86b26c8f5f079df692c8d288865cd578674f1db67371e8363e08e66adc59ce778b6bdc3a91ea9e74a9cc7de2f126c828591
8
+ metadata.gz: !binary |-
9
+ NDhhN2IyYzA0Y2UyZmZkZWI5NDU1MDdmYWE0Y2E3ZDE3YTFkZjRlZjgwODIy
10
+ OTIxMjliZDc1NmZkMTU4NjA5OTYzYjliYmI1N2QxNjdhNGVjMTZlZDc5OGFj
11
+ N2UwY2U3ZjQ5ZDdlY2RkMWE4OThjYzNlMjU0MTEwZTc4MjM0NGQ=
12
+ data.tar.gz: !binary |-
13
+ MTI1ZDZkMGE1NGM0MzIzZTdiMGRjMDAyNjJhMTlmNWE4NWY5MWFjOTQyOTg5
14
+ NDJiMTI5YzJhNzE3MTIxNzYzNzRkOGEzMTcwNzQ4ZTBjMTM3YTZiZWVlODk3
15
+ YWRlMjM5YTg3YmY0ZTdmYTVlYjYxYWQ1ZmMwZmUwMDkwM2E0OTQ=
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  *.gem
2
2
  Gemfile.lock
3
3
  tmp/
4
+ .rbx
data/Readme.md CHANGED
@@ -9,3 +9,54 @@ Its purpose is to enable reasoning about thread order.
9
9
  * Tested on 1.8.7 - 2.2, JRuby, Rbx
10
10
  * It has no external dependencies
11
11
  * It does not depend on the stdlib.
12
+
13
+ Example
14
+ -------
15
+
16
+ ```ruby
17
+ # A somewhat contrived class we're going to test.
18
+ class MyQueue
19
+ attr_reader :array
20
+
21
+ def initialize
22
+ @array, @mutex = [], Mutex.new
23
+ end
24
+
25
+ def enqueue
26
+ @mutex.synchronize { @array << yield }
27
+ end
28
+ end
29
+
30
+
31
+
32
+ require 'rspec/autorun'
33
+ require 'thread_order'
34
+
35
+ RSpec.describe MyQueue do
36
+ let(:queue) { described_class.new }
37
+ let(:order) { ThreadOrder.new }
38
+ after { order.apocalypse! } # ensure everything gets cleaned up (technically redundant for our one example, but it's a good practice)
39
+
40
+ it 'is threadsafe on enqueue' do
41
+ # will execute in a thread, can be invoked by name
42
+ order.declare :concurrent_enqueue do
43
+ queue.enqueue { :concurrent }
44
+ end
45
+
46
+ # this enqueue will block until the mutex puts the other one to sleep
47
+ queue.enqueue do
48
+ order.pass_to :concurrent_enqueue, resume_on: :sleep
49
+ :main
50
+ end
51
+
52
+ order.join_all # concurrent_enqueue may still be asleep
53
+ expect(queue.array).to eq [:main, :concurrent]
54
+ end
55
+ end
56
+
57
+ # >> MyQueue
58
+ # >> is threadsafe on enqueue
59
+ # >>
60
+ # >> Finished in 0.00131 seconds (files took 0.08687 seconds to load)
61
+ # >> 1 example, 0 failures
62
+ ```
@@ -1,73 +1,102 @@
1
1
  require 'thread_order/mutex'
2
2
 
3
3
  class ThreadOrder
4
+ Error = Class.new RuntimeError
5
+ CannotResume = Class.new Error
6
+
7
+ # Note that this must tbe initialized in a threadsafe environment
8
+ # Otherwise, syncing may occur before the mutex is set
4
9
  def initialize
10
+ @mutex = Mutex.new
5
11
  @bodies = {}
6
12
  @threads = []
7
- @queue = [] # we may not have thread stdlib required, so may not have Queue class
8
- @mutex = Mutex.new
9
- @worker = Thread.new { loop { work } }
10
- @worker.abort_on_exception = true
13
+ @queue = [] # Queue is in stdlib, but half the purpose of this lib is to avoid such deps, so using an array in a Mutex
14
+ @worker = Thread.new do
15
+ Thread.current.abort_on_exception = true
16
+ Thread.current[:thread_order_name] = :internal_worker
17
+ loop { break if :shutdown == work() }
18
+ end
11
19
  end
12
20
 
13
21
  def declare(name, &block)
14
- @bodies[name] = block
22
+ sync { @bodies[name] = block }
15
23
  end
16
24
 
17
25
  def current
18
26
  Thread.current[:thread_order_name]
19
27
  end
20
28
 
21
- def pass_to(name, options)
22
- parent = Thread.current
29
+ def pass_to(name, options={})
23
30
  child = nil
24
- resume_event = extract_resume_event! options
25
- resume_if = lambda do |event|
26
- return unless event == sync { resume_event }
27
- parent.wakeup
28
- end
29
-
31
+ parent = Thread.current
32
+ resume_event = extract_resume_event!(options)
30
33
  enqueue do
31
- child = Thread.new do
32
- enqueue { @threads << child }
33
- sync { resume_event } == :sleep &&
34
- enqueue { watch_for_sleep(child) { resume_if.call :sleep } }
35
- begin
36
- enqueue { resume_if.call :run }
37
- Thread.current[:thread_order_name] = name
38
- @bodies.fetch(name).call
39
- rescue Exception => error
40
- enqueue { parent.raise error }
41
- raise
42
- ensure
43
- enqueue { resume_if.call :exit }
44
- end
34
+ sync do
35
+ @threads << Thread.new {
36
+ child = Thread.current
37
+ child[:thread_order_name] = name
38
+ body = sync { @bodies.fetch(name) }
39
+ wait_until { parent.stop? }
40
+ :run == resume_event && parent.wakeup
41
+ wake_on_sleep = lambda do
42
+ child.status == 'sleep' ? parent.wakeup :
43
+ child.status == nil ? :noop :
44
+ child.status == false ? parent.raise(CannotResume.new "#{name} exited instead of sleeping") :
45
+ enqueue(&wake_on_sleep)
46
+ end
47
+ :sleep == resume_event && enqueue(&wake_on_sleep)
48
+ begin
49
+ body.call parent
50
+ rescue Exception => e
51
+ enqueue { parent.raise e }
52
+ raise
53
+ ensure
54
+ :exit == resume_event && enqueue { parent.wakeup }
55
+ end
56
+ }
45
57
  end
46
58
  end
47
-
48
59
  sleep
49
60
  child
50
61
  end
51
62
 
63
+ def join_all
64
+ sync { @threads }.each { |th| th.join }
65
+ end
66
+
52
67
  def apocalypse!(thread_method=:kill)
53
68
  enqueue do
54
69
  @threads.each(&thread_method)
55
70
  @queue.clear
56
- @worker.kill
71
+ :shutdown
57
72
  end
58
73
  @worker.join
59
74
  end
60
75
 
76
+ def enqueue(&block)
77
+ sync { @queue << block if @worker.alive? }
78
+ end
79
+
80
+ def wait_until(&condition)
81
+ return if condition.call
82
+ thread = Thread.current
83
+ wake_when_true = lambda do
84
+ if thread.stop? && condition.call
85
+ thread.wakeup
86
+ else
87
+ enqueue(&wake_when_true)
88
+ end
89
+ end
90
+ enqueue(&wake_when_true)
91
+ sleep
92
+ end
93
+
61
94
  private
62
95
 
63
96
  def sync(&block)
64
97
  @mutex.synchronize(&block)
65
98
  end
66
99
 
67
- def enqueue(&block)
68
- sync { @queue << block }
69
- end
70
-
71
100
  def work
72
101
  task = sync { @queue.shift }
73
102
  task ||= lambda { Thread.pass }
@@ -78,18 +107,8 @@ class ThreadOrder
78
107
  resume_on = options.delete :resume_on
79
108
  options.any? &&
80
109
  raise(ArgumentError, "Unknown options: #{options.inspect}")
81
- resume_on && ![:run, :exit, :sleep].include?(resume_on) and
110
+ resume_on && ![:run, :exit, :sleep, nil].include?(resume_on) and
82
111
  raise(ArgumentError, "Unknown status: #{resume_on.inspect}")
83
- resume_on
84
- end
85
-
86
- def watch_for_sleep(thread, &cb)
87
- if thread.status == false || thread.status == nil
88
- # noop, dead threads dream no dreams
89
- elsif thread.status == 'sleep'
90
- cb.call
91
- else
92
- enqueue { watch_for_sleep(thread, &cb) }
93
- end
112
+ resume_on || :none
94
113
  end
95
114
  end
@@ -1,21 +1,21 @@
1
- # On > 1.9, this is in core.
2
- # On 1.8.7, it's in the stdlib.
3
- # We don't want to load the stdlib, b/c this is a test tool, and can affect the test environment,
4
- # causing tests to pass where they should fail.
5
- #
6
- # So we're transcribing it here, from.
7
- # It is based on this implementation: https://github.com/ruby/ruby/blob/v1_8_7_374/lib/thread.rb#L56
8
- # If it's not already defined. Some methods we don't need are deleted.
9
- # Anything I don't understand is left in.
10
1
  class ThreadOrder
11
2
  Mutex = if defined? ::Mutex
3
+ # On 1.9 and up, this is in core, so we just use the real one
12
4
  ::Mutex
13
5
  else
6
+
7
+ # On 1.8.7, it's in the stdlib.
8
+ # We don't want to load the stdlib, b/c this is a test tool, and can affect the test environment,
9
+ # causing tests to pass where they should fail.
10
+ #
11
+ # So we're transcribing/modifying it from https://github.com/ruby/ruby/blob/v1_8_7_374/lib/thread.rb#L56
12
+ # Some methods we don't need are deleted.
13
+ # Anything I don't understand (there's quite a bit, actually) is left in.
14
14
  Class.new do
15
15
  def initialize
16
16
  @waiting = []
17
17
  @locked = false;
18
- @waiting.taint # enable tainted comunication
18
+ @waiting.taint
19
19
  self.taint
20
20
  end
21
21
 
@@ -1,3 +1,3 @@
1
1
  class ThreadOrder
2
- VERSION = '1.0.0'
2
+ VERSION = '1.1.0'
3
3
  end
data/spec/run CHANGED
@@ -34,25 +34,25 @@ get_gem "rspec-core" "https://rubygems.org/downloads/rspec-core-3.2.1.ge
34
34
  get_gem "rspec-support" "https://rubygems.org/downloads/rspec-support-3.2.2.gem" &&
35
35
  get_gem "rspec-expectations" "https://rubygems.org/downloads/rspec-expectations-3.2.0.gem" &&
36
36
  get_gem "rspec-mocks" "https://rubygems.org/downloads/rspec-mocks-3.2.1.gem" &&
37
- get_gem "diff-lcs" "https://rubygems.org/downloads/diff-lcs-1.2.5.gem"
37
+ get_gem "diff-lcs" "https://rubygems.org/downloads/diff-lcs-1.2.5.gem" || exit 1
38
38
 
39
39
 
40
40
  # run specs
41
- cd $project_root
41
+ cd "$project_root"
42
42
 
43
43
  export PATH="$project_root/tmp/rspec-core/exe:$PATH"
44
44
 
45
- opts=""
46
- opts=" -I $project_root/tmp/diff-lcs/lib $opts"
47
- opts=" -I $project_root/tmp/rspec/lib $opts"
48
- opts=" -I $project_root/tmp/rspec-core/lib $opts"
49
- opts=" -I $project_root/tmp/rspec-expectations/lib $opts"
50
- opts=" -I $project_root/tmp/rspec-mocks/lib $opts"
51
- opts=" -I $project_root/tmp/rspec-support/lib $opts"
45
+ opts=()
46
+ opts+=(-I "$project_root/tmp/diff-lcs/lib")
47
+ opts+=(-I "$project_root/tmp/rspec/lib")
48
+ opts+=(-I "$project_root/tmp/rspec-core/lib")
49
+ opts+=(-I "$project_root/tmp/rspec-expectations/lib")
50
+ opts+=(-I "$project_root/tmp/rspec-mocks/lib")
51
+ opts+=(-I "$project_root/tmp/rspec-support/lib")
52
52
 
53
53
  if `ruby -e "exit RUBY_VERSION != '1.8.7'"`
54
54
  then
55
- opts="--disable-gems $opts"
55
+ opts+=(--disable-gems)
56
56
  fi
57
57
 
58
- ruby $opts -S rspec
58
+ ruby "${opts[@]}" -S rspec --colour --fail-fast --format documentation
@@ -45,35 +45,75 @@ RSpec.describe ThreadOrder do
45
45
  order.declare(:t) { Thread.exit }
46
46
  order.pass_to :t, :resume_on => :exit
47
47
  end
48
+
49
+ it 'passes the parent to the thread' do
50
+ parent = nil
51
+ order.declare(:t) { |p| parent = p }
52
+ order.pass_to :t, :resume_on => :exit
53
+ expect(parent).to eq Thread.current
54
+ end
55
+
56
+ it 'sleeps until woken if it does not provide a :resume_on key' do
57
+ order.declare(:t) { |parent|
58
+ order.enqueue {
59
+ expect(parent.status).to eq 'sleep'
60
+ parent.wakeup
61
+ }
62
+ }
63
+ order.pass_to :t
64
+ end
65
+
66
+ it 'blows up if it is waiting on another thread to sleep and that thread exits instead' do
67
+ expect {
68
+ order.declare(:t1) { :exits_instead_of_sleeping }
69
+ order.pass_to :t1, :resume_on => :sleep
70
+ }.to raise_error ThreadOrder::CannotResume, /t1 exited/
71
+ end
72
+ end
73
+
74
+ describe 'error types' do
75
+ it 'has a toplevel lib error: ThreadOrder::Error which is a RuntimeError' do
76
+ expect(ThreadOrder::Error.superclass).to eq RuntimeError
77
+ end
78
+
79
+ specify 'all behavioural errors it raises inherit from ThreadOrder::Error' do
80
+ expect(ThreadOrder::CannotResume.superclass).to eq ThreadOrder::Error
81
+ end
48
82
  end
49
83
 
50
84
  describe 'errors in children' do
51
85
  specify 'are raised in the child' do
52
- child = nil
53
- order.declare(:err) { child = Thread.current; raise 'the roof' }
54
- order.pass_to :err, :resume_on => :exit rescue nil
55
- child.join rescue nil
56
- expect(child.status).to eq nil
86
+ order.declare(:err) { sleep }
87
+ child = order.pass_to :err, :resume_on => :sleep
88
+ begin
89
+ child.raise RuntimeError.new('the roof')
90
+ sleep
91
+ rescue RuntimeError => e
92
+ expect(e.message).to eq 'the roof'
93
+ else
94
+ raise 'expected an error'
95
+ end
57
96
  end
58
97
 
59
98
  specify 'are raised in the parent' do
60
- order.declare(:err) { raise Exception, 'to the rules' }
61
99
  expect {
62
- order.pass_to :err, :resume_on => :run
63
- loop { :noop }
100
+ order.declare(:err) { raise Exception, "to the rules" }
101
+ order.pass_to :err, :resume_on => :exit
102
+ sleep
64
103
  }.to raise_error Exception, 'to the rules'
65
104
  end
66
105
 
67
106
  specify 'even if the parent is asleep' do
107
+ order.declare(:err) { sleep }
68
108
  parent = Thread.current
69
- order.declare(:err) {
70
- :noop until parent.status == 'sleep'
71
- raise 'the roof'
72
- }
109
+ child = order.pass_to :err, :resume_on => :sleep
73
110
  expect {
74
- order.pass_to :err, :resume_on => :run
111
+ order.enqueue {
112
+ expect(parent.status).to eq 'sleep'
113
+ child.raise Exception.new 'to the rules'
114
+ }
75
115
  sleep
76
- }.to raise_error RuntimeError, 'the roof'
116
+ }.to raise_error Exception, 'to the rules'
77
117
  end
78
118
  end
79
119
 
@@ -127,4 +167,87 @@ RSpec.describe ThreadOrder do
127
167
  to raise_error(ArgumentError, /bad_key/)
128
168
  end
129
169
  end
170
+
171
+ describe 'join_all' do
172
+ it 'joins with all the child threads' do
173
+ parent = Thread.current
174
+ children = []
175
+
176
+ order.declare(:t1) do
177
+ order.pass_to :t2, :resume_on => :run
178
+ children << Thread.current
179
+ end
180
+
181
+ order.declare(:t2) do
182
+ children << Thread.current
183
+ end
184
+
185
+ order.pass_to :t1, :resume_on => :run
186
+ order.join_all
187
+ statuses = children.map { |th| th.status }
188
+ expect(statuses).to eq [false, false] # none are alive
189
+ end
190
+ end
191
+
192
+ describe 'synchronization' do
193
+ it 'allows any thread to enqueue work' do
194
+ seen = []
195
+
196
+ order.declare :enqueueing do |parent|
197
+ order.enqueue do
198
+ order.enqueue { seen << 2 }
199
+ order.enqueue { seen << 3 }
200
+ order.enqueue { parent.wakeup }
201
+ seen << 1
202
+ end
203
+ end
204
+
205
+ order.pass_to :enqueueing
206
+ expect(seen).to eq [1, 2, 3]
207
+ end
208
+
209
+ it 'allows a thread to put itself to sleep until some condition is met' do
210
+ i = 0
211
+ increment = lambda do
212
+ i += 1
213
+ order.enqueue(&increment)
214
+ end
215
+ increment.call
216
+ order.wait_until { i > 20_000 } # 100k is too slow on 1.8.7, but 10k is too fast on 2.2.0
217
+ expect(i).to be > 20_000
218
+ end
219
+ end
220
+
221
+ describe 'apocalypse!' do
222
+ it 'kills threads that are still alive' do
223
+ order.declare(:t) { sleep }
224
+ child = order.pass_to :t, :resume_on => :sleep
225
+ expect(child).to receive(:kill).and_call_original
226
+ expect(child).to_not receive(:join)
227
+ order.apocalypse!
228
+ end
229
+
230
+ it 'can be overridden to call a different method than kill' do
231
+ # for some reason, the mock calling original join doesn't work
232
+ order.declare(:t) { sleep }
233
+ child = order.pass_to :t, :resume_on => :run
234
+ expect(child).to_not receive(:kill)
235
+ joiner = Thread.new { order.apocalypse! :join }
236
+ Thread.pass until child.status == 'sleep' # can't use wait_until b/c that occurs within the worker, which is apocalypsizing
237
+ child.wakeup
238
+ joiner.join
239
+ end
240
+
241
+ it 'can call apocalypse! any number of times without harm' do
242
+ order.declare(:t) { sleep }
243
+ order.pass_to :t, :resume_on => :sleep
244
+ 100.times { order.apocalypse! }
245
+ end
246
+
247
+ it 'does not enqueue events after the apocalypse' do
248
+ order.apocalypse!
249
+ thread = Thread.current
250
+ order.enqueue { thread.raise "Should not happen" }
251
+ end
252
+ end
130
253
  end
@@ -1,11 +1,11 @@
1
- require_relative 'lib/thread_order/version'
1
+ require File.expand_path('../lib/thread_order/version', __FILE__)
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'thread_order'
5
5
  s.version = ThreadOrder::VERSION
6
6
  s.licenses = ['MIT']
7
7
  s.summary = "Test helper for ordering threaded code"
8
- s.description = "Test helper for ordering threaded code (does not depend on gems or stdlib, tested on 1.8.7 - 2.2)."
8
+ s.description = "Test helper for ordering threaded code (does not depend on gems or stdlib, tested on 1.8.7 - 2.2, rbx, jruby)."
9
9
  s.authors = ["Josh Cheek"]
10
10
  s.email = 'josh.cheek@gmail.com'
11
11
  s.files = `git ls-files`.split("\n")
metadata CHANGED
@@ -1,38 +1,38 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thread_order
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Cheek
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-08 00:00:00.000000000 Z
11
+ date: 2015-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ~>
18
18
  - !ruby/object:Gem::Version
19
19
  version: '3.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ~>
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.0'
27
27
  description: Test helper for ordering threaded code (does not depend on gems or stdlib,
28
- tested on 1.8.7 - 2.2).
28
+ tested on 1.8.7 - 2.2, rbx, jruby).
29
29
  email: josh.cheek@gmail.com
30
30
  executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
- - ".gitignore"
35
- - ".travis.yml"
34
+ - .gitignore
35
+ - .travis.yml
36
36
  - Gemfile
37
37
  - License.txt
38
38
  - Readme.md
@@ -52,17 +52,17 @@ require_paths:
52
52
  - lib
53
53
  required_ruby_version: !ruby/object:Gem::Requirement
54
54
  requirements:
55
- - - ">="
55
+ - - ! '>='
56
56
  - !ruby/object:Gem::Version
57
57
  version: '0'
58
58
  required_rubygems_version: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - ">="
60
+ - - ! '>='
61
61
  - !ruby/object:Gem::Version
62
62
  version: '0'
63
63
  requirements: []
64
64
  rubyforge_project:
65
- rubygems_version: 2.4.5
65
+ rubygems_version: 2.4.1
66
66
  signing_key:
67
67
  specification_version: 4
68
68
  summary: Test helper for ordering threaded code