thread_order-1.1.1/0000755000004100000410000000000013370151045014203 5ustar www-datawww-datathread_order-1.1.1/.travis.yml0000644000004100000410000000064213370151045016316 0ustar www-datawww-datalanguage: ruby before_install: # jruby-head does not have bundler. - which bundle || gem install bundler script: spec/run rvm: - 1.8.7 - 1.9.2 - 1.9.3 - 2.0.0 - 2.1 - 2.2 - 2.3.7 - 2.4.4 - 2.5.1 - 2.6.0 - ruby-head - ree - jruby-18mode - jruby - jruby-head - rbx matrix: include: - rvm: jruby env: JRUBY_OPTS='--2.0' allow_failures: - rvm: rbx fast_finish: true thread_order-1.1.1/spec/0000755000004100000410000000000013370151045015135 5ustar www-datawww-datathread_order-1.1.1/spec/run0000755000004100000410000000300613370151045015666 0ustar www-datawww-data#!/bin/bash # do this stuff in a tmp dir cd "$(dirname "$0")/.." project_root=`pwd` mkdir -p tmp cd tmp # this is basically a shitty version of `gem unpack` get_gem() { name="$1" url="$2" if test -d "$name" then echo "Skipping download of $name" return else echo "Downloading $name" fi mkdir "$name" && cd "$name" && curl -L "$url" > "$name".gem && tar -xf "$name".gem && gunzip data.tar.gz && tar -xf data.tar && cd .. } # download dependencies get_gem "rspec" "https://rubygems.org/downloads/rspec-3.2.0.gem" && get_gem "rspec-core" "https://rubygems.org/downloads/rspec-core-3.2.1.gem" && get_gem "rspec-support" "https://rubygems.org/downloads/rspec-support-3.2.2.gem" && get_gem "rspec-expectations" "https://rubygems.org/downloads/rspec-expectations-3.2.0.gem" && get_gem "rspec-mocks" "https://rubygems.org/downloads/rspec-mocks-3.2.1.gem" && get_gem "diff-lcs" "https://rubygems.org/downloads/diff-lcs-1.3.gem" || exit 1 # run specs cd "$project_root" export PATH="$project_root/tmp/rspec-core/exe:$PATH" opts=() opts+=(-I "$project_root/tmp/diff-lcs/lib") opts+=(-I "$project_root/tmp/rspec/lib") opts+=(-I "$project_root/tmp/rspec-core/lib") opts+=(-I "$project_root/tmp/rspec-expectations/lib") opts+=(-I "$project_root/tmp/rspec-mocks/lib") opts+=(-I "$project_root/tmp/rspec-support/lib") if `ruby -e "exit RUBY_VERSION != '1.8.7'"` then opts+=(--disable-gems) fi ruby "${opts[@]}" -S rspec --colour --fail-fast --format documentation thread_order-1.1.1/spec/thread_order_spec.rb0000644000004100000410000002016513370151045021142 0ustar www-datawww-datainitial_loaded_features = $LOADED_FEATURES.dup.freeze require 'thread_order' RSpec.describe ThreadOrder do let(:order) { described_class.new } after { order.apocalypse! } it 'allows thread behaviour to be declared and run by name' do seen = [] order.declare(:third) { seen << :third } order.declare(:first) { seen << :first; order.pass_to :second, :resume_on => :exit } order.declare(:second) { seen << :second; order.pass_to :third, :resume_on => :exit } expect(seen).to eq [] order.pass_to :first, :resume_on => :exit expect(seen).to eq [:first, :second, :third] end it 'sleeps the thread which passed' do main_thread = Thread.current order.declare(:thread) { :noop until main_thread.status == 'sleep' } order.pass_to :thread, :resume_on => :exit # passes if it doesn't lock up end context 'resume events' do def self.test_status(name, statuses, *args, &threadmaker) it "can resume the thread when the called thread enters #{name}", *args do thread = instance_eval(&threadmaker) statuses = Array statuses expect(statuses).to include thread.status end end test_status ':run', 'run' do order.declare(:t) { loop { 1 } } order.pass_to :t, :resume_on => :run end test_status ':sleep', 'sleep' do order.declare(:t) { sleep } order.pass_to :t, :resume_on => :sleep end # can't reproduce 'dead', but apparently JRuby 1.7.19 returned # this on CI https://travis-ci.org/rspec/rspec-core/jobs/51933739 test_status ':exit', [false, 'aborting', 'dead'] do order.declare(:t) { Thread.exit } order.pass_to :t, :resume_on => :exit end it 'passes the parent to the thread' do parent = nil order.declare(:t) { |p| parent = p } order.pass_to :t, :resume_on => :exit expect(parent).to eq Thread.current end it 'sleeps until woken if it does not provide a :resume_on key' do order.declare(:t) { |parent| order.enqueue { expect(parent.status).to eq 'sleep' parent.wakeup } } order.pass_to :t end it 'blows up if it is waiting on another thread to sleep and that thread exits instead' do expect { order.declare(:t1) { :exits_instead_of_sleeping } order.pass_to :t1, :resume_on => :sleep }.to raise_error ThreadOrder::CannotResume, /t1 exited/ end end describe 'error types' do it 'has a toplevel lib error: ThreadOrder::Error which is a RuntimeError' do expect(ThreadOrder::Error.superclass).to eq RuntimeError end specify 'all behavioural errors it raises inherit from ThreadOrder::Error' do expect(ThreadOrder::CannotResume.superclass).to eq ThreadOrder::Error end end describe 'errors in children' do specify 'are raised in the child' do order.declare(:err) { sleep } child = order.pass_to :err, :resume_on => :sleep begin child.raise RuntimeError.new('the roof') sleep rescue RuntimeError => e expect(e.message).to eq 'the roof' else raise 'expected an error' end end specify 'are raised in the parent' do expect { order.declare(:err) { raise Exception, "to the rules" } order.pass_to :err, :resume_on => :exit sleep }.to raise_error Exception, 'to the rules' end specify 'even if the parent is asleep' do order.declare(:err) { sleep } parent = Thread.current child = order.pass_to :err, :resume_on => :sleep expect { order.enqueue { expect(parent.status).to eq 'sleep' child.raise Exception.new 'to the rules' } sleep }.to raise_error Exception, 'to the rules' end end it 'knows which thread is running' do thread_names = [] order.declare(:a) { thread_names << order.current order.pass_to :b, :resume_on => :exit thread_names << order.current } order.declare(:b) { thread_names << order.current } order.pass_to :a, :resume_on => :exit expect(thread_names.map(&:to_s).sort).to eq ['a', 'a', 'b'] end it 'returns nil when asked for the current thread by one it did not define' do thread_names = [] order.declare(:a) { thread_names << order.current Thread.new { thread_names << order.current }.join } expect(order.current).to eq nil order.pass_to :a, :resume_on => :exit expect(thread_names).to eq [:a, nil] end define_method :not_loaded! do |filename| # newer versions of Ruby require thread.rb somewhere, so if it was required # before any of our code was required, then don't bother with the assertion # there's no obvious way to deal with it, and it wasn't us who required it next if initial_loaded_features.include? filename loaded_filenames = $LOADED_FEATURES.map { |filepath| File.basename filepath } expect(loaded_filenames).to_not include filename end it 'is implemented without depending on the stdlib' do begin not_loaded! 'monitor.rb' not_loaded! 'thread.rb' not_loaded! 'thread.bundle' rescue RSpec::Expectations::ExpectationNotMetError pending if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' # somehow this still gets loaded in some JRubies raise end end describe 'incorrect interface usage' do it 'raises ArgumentError when told to resume on an unknown status' do order.declare(:t) { } expect { order.pass_to :t, :resume_on => :bad_status }. to raise_error(ArgumentError, /bad_status/) end it 'raises an ArgumentError when you give it unknown keys (ie you spelled resume_on wrong)' do order.declare(:t) { } expect { order.pass_to :t, :bad_key => :t }. to raise_error(ArgumentError, /bad_key/) end end describe 'join_all' do it 'joins with all the child threads' do parent = Thread.current children = [] order.declare(:t1) do order.pass_to :t2, :resume_on => :run children << Thread.current end order.declare(:t2) do children << Thread.current end order.pass_to :t1, :resume_on => :run order.join_all statuses = children.map { |th| th.status } expect(statuses).to eq [false, false] # none are alive end end describe 'synchronization' do it 'allows any thread to enqueue work' do seen = [] order.declare :enqueueing do |parent| order.enqueue do order.enqueue { seen << 2 } order.enqueue { seen << 3 } order.enqueue { parent.wakeup } seen << 1 end end order.pass_to :enqueueing expect(seen).to eq [1, 2, 3] end it 'allows a thread to put itself to sleep until some condition is met' do i = 0 increment = lambda do i += 1 order.enqueue(&increment) end increment.call order.wait_until { i > 20_000 } # 100k is too slow on 1.8.7, but 10k is too fast on 2.2.0 expect(i).to be > 20_000 end end describe 'apocalypse!' do it 'kills threads that are still alive' do order.declare(:t) { sleep } child = order.pass_to :t, :resume_on => :sleep expect(child).to receive(:kill).and_call_original expect(child).to_not receive(:join) order.apocalypse! end it 'can be overridden to call a different method than kill' do # for some reason, the mock calling original join doesn't work order.declare(:t) { sleep } child = order.pass_to :t, :resume_on => :run expect(child).to_not receive(:kill) joiner = Thread.new { order.apocalypse! :join } Thread.pass until child.status == 'sleep' # can't use wait_until b/c that occurs within the worker, which is apocalypsizing child.wakeup joiner.join end it 'can call apocalypse! any number of times without harm' do order.declare(:t) { sleep } order.pass_to :t, :resume_on => :sleep 100.times { order.apocalypse! } end it 'does not enqueue events after the apocalypse' do order.apocalypse! thread = Thread.current order.enqueue { thread.raise "Should not happen" } end end end thread_order-1.1.1/.gitignore0000644000004100000410000000003513370151045016171 0ustar www-datawww-data*.gem Gemfile.lock tmp/ .rbx thread_order-1.1.1/Readme.md0000644000004100000410000000271413370151045015726 0ustar www-datawww-data[![Build Status](https://travis-ci.org/JoshCheek/thread_order.svg)](https://travis-ci.org/JoshCheek/thread_order) ThreadOrder =========== A tool for testing threaded code. Its purpose is to enable reasoning about thread order. * Tested on 1.8.7 - 2.6, JRuby, Rbx * It has no external dependencies * It does not depend on the stdlib. Example ------- ```ruby # A somewhat contrived class we're going to test. class MyQueue attr_reader :array def initialize @array, @mutex = [], Mutex.new end def enqueue @mutex.synchronize { @array << yield } end end require 'rspec/autorun' require 'thread_order' RSpec.describe MyQueue do let(:queue) { described_class.new } let(:order) { ThreadOrder.new } after { order.apocalypse! } # ensure everything gets cleaned up (technically redundant for our one example, but it's a good practice) it 'is threadsafe on enqueue' do # will execute in a thread, can be invoked by name order.declare :concurrent_enqueue do queue.enqueue { :concurrent } end # this enqueue will block until the mutex puts the other one to sleep queue.enqueue do order.pass_to :concurrent_enqueue, resume_on: :sleep :main end order.join_all # concurrent_enqueue may still be asleep expect(queue.array).to eq [:main, :concurrent] end end # >> MyQueue # >> is threadsafe on enqueue # >> # >> Finished in 0.00131 seconds (files took 0.08687 seconds to load) # >> 1 example, 0 failures ``` thread_order-1.1.1/lib/0000755000004100000410000000000013370151045014751 5ustar www-datawww-datathread_order-1.1.1/lib/thread_order.rb0000644000004100000410000000560013370151045017741 0ustar www-datawww-datarequire 'thread_order/mutex' class ThreadOrder Error = Class.new RuntimeError CannotResume = Class.new Error # Note that this must tbe initialized in a threadsafe environment # Otherwise, syncing may occur before the mutex is set def initialize @mutex = Mutex.new @bodies = {} @threads = [] @queue = [] # Queue is in stdlib, but half the purpose of this lib is to avoid such deps, so using an array in a Mutex @worker = Thread.new do Thread.current.abort_on_exception = true Thread.current[:thread_order_name] = :internal_worker loop { break if :shutdown == work() } end end def declare(name, &block) sync { @bodies[name] = block } end def current Thread.current[:thread_order_name] end def pass_to(name, options={}) child = nil parent = Thread.current resume_event = extract_resume_event!(options) enqueue do sync do @threads << Thread.new { child = Thread.current child[:thread_order_name] = name body = sync { @bodies.fetch(name) } wait_until { parent.stop? } :run == resume_event && parent.wakeup wake_on_sleep = lambda do child.status == 'sleep' ? parent.wakeup : child.status == nil ? :noop : child.status == false ? parent.raise(CannotResume.new "#{name} exited instead of sleeping") : enqueue(&wake_on_sleep) end :sleep == resume_event && enqueue(&wake_on_sleep) begin body.call parent rescue Exception => e enqueue { parent.raise e } raise ensure :exit == resume_event && enqueue { parent.wakeup } end } end end sleep child end def join_all sync { @threads }.each { |th| th.join } end def apocalypse!(thread_method=:kill) enqueue do @threads.each(&thread_method) @queue.clear :shutdown end @worker.join end def enqueue(&block) sync { @queue << block if @worker.alive? } end def wait_until(&condition) return if condition.call thread = Thread.current wake_when_true = lambda do if thread.stop? && condition.call thread.wakeup else enqueue(&wake_when_true) end end enqueue(&wake_when_true) sleep end private def sync(&block) @mutex.synchronize(&block) end def work task = sync { @queue.shift } task ||= lambda { Thread.pass } task.call end def extract_resume_event!(options) resume_on = options.delete :resume_on options.any? && raise(ArgumentError, "Unknown options: #{options.inspect}") resume_on && ![:run, :exit, :sleep, nil].include?(resume_on) and raise(ArgumentError, "Unknown status: #{resume_on.inspect}") resume_on || :none end end thread_order-1.1.1/lib/thread_order/0000755000004100000410000000000013370151045017413 5ustar www-datawww-datathread_order-1.1.1/lib/thread_order/version.rb0000644000004100000410000000005213370151045021422 0ustar www-datawww-dataclass ThreadOrder VERSION = '1.1.1' end thread_order-1.1.1/lib/thread_order/mutex.rb0000644000004100000410000000257713370151045021115 0ustar www-datawww-dataclass ThreadOrder Mutex = if defined? ::Mutex # On 1.9 and up, this is in core, so we just use the real one ::Mutex else # On 1.8.7, it's in the stdlib. # We don't want to load the stdlib, b/c this is a test tool, and can affect the test environment, # causing tests to pass where they should fail. # # So we're transcribing/modifying it from https://github.com/ruby/ruby/blob/v1_8_7_374/lib/thread.rb#L56 # Some methods we don't need are deleted. # Anything I don't understand (there's quite a bit, actually) is left in. Class.new do def initialize @waiting = [] @locked = false; @waiting.taint self.taint end def lock while (Thread.critical = true; @locked) @waiting.push Thread.current Thread.stop end @locked = true Thread.critical = false self end def unlock return unless @locked Thread.critical = true @locked = false begin t = @waiting.shift t.wakeup if t rescue ThreadError retry end Thread.critical = false begin t.run if t rescue ThreadError end self end def synchronize lock begin yield ensure unlock end end end end end thread_order-1.1.1/Gemfile0000644000004100000410000000004613370151045015476 0ustar www-datawww-datasource 'https://rubygems.org' gemspec thread_order-1.1.1/License.txt0000644000004100000410000000206113370151045016325 0ustar www-datawww-data(The MIT License) Copyright (c) 2015 Josh Cheek Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. thread_order-1.1.1/thread_order.gemspec0000644000004100000410000000124213370151045020211 0ustar www-datawww-datarequire File.expand_path('../lib/thread_order/version', __FILE__) Gem::Specification.new do |s| s.name = 'thread_order' s.version = ThreadOrder::VERSION s.licenses = ['MIT'] s.summary = "Test helper for ordering threaded code" s.description = "Test helper for ordering threaded code (does not depend on gems or stdlib, tested on 1.8.7 - 2.2, rbx, jruby)." s.authors = ["Josh Cheek"] s.email = 'josh.cheek@gmail.com' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- spec/*`.split("\n") s.homepage = 'https://github.com/JoshCheek/thread_order' s.add_development_dependency 'rspec', '~> 3.0' end