├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── eldritch.gemspec ├── examples ├── async_block.rb ├── async_method_with_class.rb ├── jacobi.rb ├── matrix_multiplication.rb ├── parallel_sort.rb ├── password_cracker.rb ├── simple_async_method.rb └── together_simple.rb ├── lib ├── eldritch.rb └── eldritch │ ├── core_ext │ └── thread.rb │ ├── dsl.rb │ ├── group.rb │ ├── interrupted_error.rb │ ├── safe.rb │ ├── task.rb │ └── version.rb └── spec ├── dsl_spec.rb ├── eldritch_spec.rb ├── group_spec.rb ├── spec_helper.rb ├── task_spec.rb └── thread_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by http://www.gitignore.io 2 | 3 | ### Ruby ### 4 | *.gem 5 | *.rbc 6 | /.config 7 | /coverage/ 8 | /InstalledFiles 9 | /pkg/ 10 | /spec/reports/ 11 | /test/tmp/ 12 | /test/version_tmp/ 13 | /tmp/ 14 | 15 | ## Documentation cache and generated files: 16 | /.yardoc/ 17 | /_yardoc/ 18 | /doc/ 19 | /rdoc/ 20 | 21 | ## Environment normalisation: 22 | /.bundle/ 23 | /lib/bundler/man/ 24 | 25 | # for a library or gem, you might want to ignore these files since the code is 26 | # intended to run in multiple environments; otherwise, check them in: 27 | Gemfile.lock 28 | .ruby-version 29 | .ruby-gemset 30 | 31 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 32 | .rvmrc 33 | 34 | 35 | ### Intellij ### 36 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode 37 | 38 | ## Directory-based project format 39 | .idea/ 40 | # if you remove the above rule, at least ignore user-specific stuff: 41 | # .idea/workspace.xml 42 | # .idea/tasks.xml 43 | # and these sensitive or high-churn files: 44 | # .idea/dataSources.ids 45 | # .idea/dataSources.xml 46 | # .idea/sqlDataSources.xml 47 | # .idea/dynamic.xml 48 | 49 | ## File-based project format 50 | *.ipr 51 | *.iws 52 | *.iml 53 | 54 | ## Additional for IntelliJ 55 | out/ 56 | 57 | # generated by mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # generated by JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # generated by Crashlytics plugin (for Android Studio and Intellij) 64 | com_crashlytics_export_strings.xml 65 | 66 | # Created by http://www.gitignore.io 67 | 68 | ### vim ### 69 | [._]*.s[a-w][a-z] 70 | [._]s[a-w][a-z] 71 | *.un~ 72 | Session.vim 73 | .netrwhist 74 | *~ 75 | 76 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format p 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2 4 | - 2.3 5 | - 2.4 6 | - 2.5 7 | 8 | matrix: 9 | allow_failures: 10 | - rvm: 2.5 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem 'pry' 7 | gem 'coveralls', require: false 8 | end -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Boris Bera, François Genois 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Eldritch 2 | ======== 3 | 4 | [![Build Status](http://travis-ci.org/dotboris/eldritch.svg?branch=master)](http://travis-ci.org/dotboris/eldritch) 5 | [![Coverage Status](http://coveralls.io/repos/dotboris/eldritch/badge.png)](http://coveralls.io/r/dotboris/eldritch) 6 | [![Code Climate](http://codeclimate.com/github/dotboris/eldritch.png)](http://codeclimate.com/github/dotboris/eldritch) 7 | 8 | _The dark arts of concurrent programming._ 9 | 10 | A DSL that adds parallel programming constructs to make your life a little 11 | easier. 12 | 13 | Usage 14 | ----- 15 | 16 | 1. Install it `gem install eldritch` 17 | 1. Require it `require 'eldritch'` 18 | 1. Use it (see features below) 19 | 20 | By default eldritch will inject the DSL into the global scope. If you don't want 21 | this, you can require `eldritch/safe` instead of `eldritch`. 22 | 23 | ```ruby 24 | require 'eldricth/safe' 25 | 26 | class MyClass 27 | include Eldritch::DSL 28 | extend Eldritch::DSL 29 | 30 | # The DSL is available in this class 31 | end 32 | ``` 33 | 34 | Development 35 | ----------- 36 | 37 | ### Setup 38 | 39 | ```sh 40 | bundler install 41 | ``` 42 | 43 | ### Running tests 44 | 45 | ```sh 46 | bundler exec rake 47 | ``` 48 | 49 | ### Running examples 50 | 51 | ```sh 52 | ruby -I lib examples/{your favorite example}.rb 53 | ``` 54 | 55 | ### Generate doc 56 | 57 | ```sh 58 | bundle exec rake doc 59 | ``` 60 | 61 | Features 62 | -------- 63 | 64 | ### async methods 65 | 66 | Async methods run concurrently when called. The caller is returned control right 67 | away and the method runs in the background. 68 | 69 | ```ruby 70 | require 'eldritch' 71 | 72 | # define an async method 73 | async def send_email(email) 74 | # ... 75 | end 76 | 77 | send_email(some_email) # runs in the background 78 | ``` 79 | 80 | #### ruby 1.9.3 and 2.0.0 81 | 82 | For all versions of ruby before 2.1.0, you need to define async methods like so: 83 | 84 | ```ruby 85 | def foo 86 | # stuff 87 | end 88 | async :foo 89 | ``` 90 | 91 | Since ruby 2.1.0, def returns the name of the method defined as a symbol. This 92 | allows for the cleaner `async def foo` syntax. 93 | 94 | ### async blocks 95 | 96 | Async blocks are run concurrently. 97 | 98 | ```ruby 99 | require 'eldritch' 100 | 101 | async do 102 | # runs in the background 103 | end 104 | ``` 105 | 106 | ### tasks 107 | 108 | Async blocks and async methods both return tasks. These can be used to interact 109 | with the async block/method. 110 | 111 | ```ruby 112 | require 'eldritch' 113 | 114 | task = async do 115 | # calculate something that will take a long time 116 | end 117 | 118 | # we need to result of the task 119 | res = 2 + task.value # waits for the task to finish 120 | ``` 121 | 122 | ### together blocks 123 | 124 | Together blocks are used to control all async blocks and methods within them as 125 | a group. Before exiting, together blocks wait for all their async calls to be 126 | done before returning. 127 | 128 | ```ruby 129 | require 'eldritch' 130 | 131 | together do 132 | 1000.times do 133 | async do 134 | # do some work 135 | end 136 | end 137 | end 138 | # all 1000 tasks are done 139 | ``` 140 | 141 | These blocks can also take an argument. This argument is a group that can be 142 | used to control the async calls in the block. See the documentation for 143 | Eldritch::Group for more information. 144 | 145 | ```ruby 146 | require 'eldritch' 147 | 148 | together do |group| 149 | 5.times do 150 | async do 151 | # do something 152 | group.interrupt if some_condition # stops all other tasks 153 | end 154 | end 155 | end 156 | ``` 157 | 158 | A note on GIL 159 | ------------- 160 | 161 | MRI has this nasty little feature called a _GIL_ or _Global Interpreter Lock_. 162 | This lock makes it so that only one thread can run at a time. Let's say that you 163 | have 4 cores, running threaded code on MRI will only make use of 1 core. 164 | Sometimes, you might not gain a speed boost if you make code parallel. This 165 | could the case even if theory says otherwise. 166 | 167 | Not all ruby implementations use a _GIL_. For example, jRuby does not use a 168 | _GIL_. 169 | 170 | If your ruby implementation has a _GIL_, you will probably see a speed boost if 171 | your code does a lot of IO or anything that's blocking. In that case running on 172 | a single core is not that much of a hindrance, because most of the threads will 173 | be blocked and your code should run more often. 174 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'bundler/gem_tasks' 3 | require 'rspec/core/rake_task' 4 | require 'yard' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | YARD::Rake::YardocTask.new(:doc) 8 | 9 | task :default => :spec 10 | -------------------------------------------------------------------------------- /eldritch.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'eldritch/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'eldritch' 8 | spec.version = Eldritch::VERSION 9 | spec.authors = ['Boris Bera', 'François Genois'] 10 | spec.email = %w(bera.boris@gmail.com frankgenerated@gmail.com) 11 | spec.summary = 'DSL that adds concurrent programming concepts to ' \ 12 | 'make your life easier.' 13 | spec.description = 'Adds support for async methods and async blocks. ' \ 14 | 'Adds a together block that allows async ' \ 15 | 'methods/blocks to be controlled as a group.' 16 | spec.homepage = 'https://github.com/dotboris/eldritch' 17 | spec.license = 'MIT' 18 | 19 | spec.files = `git ls-files -z`.split("\x0") 20 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 21 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 22 | spec.require_paths = ['lib'] 23 | 24 | spec.add_dependency 'reentrant_mutex', '~> 1.1.0' 25 | 26 | spec.add_development_dependency 'bundler', '~> 1.5' 27 | spec.add_development_dependency 'rake', '~> 11.0' 28 | spec.add_development_dependency 'rspec', '~> 2.14' 29 | spec.add_development_dependency 'yard', '~> 0.9.11' 30 | end 31 | -------------------------------------------------------------------------------- /examples/async_block.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'eldritch' 3 | 4 | async do 5 | puts 'starting long running task' 6 | sleep(1) 7 | puts 'long running task done' 8 | end 9 | 10 | puts 'doing something else' 11 | 12 | # waiting for everyone to finish 13 | (Thread.list - [Thread.current]).each &:join -------------------------------------------------------------------------------- /examples/async_method_with_class.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'eldritch' 3 | 4 | class BabysFirstClass 5 | async def foo(arg) 6 | puts "starting long running task with #{arg}" 7 | sleep(1) 8 | puts 'long running task done' 9 | end 10 | end 11 | 12 | obj = BabysFirstClass.new 13 | 14 | puts 'calling foo' 15 | obj.foo('stuff') 16 | puts 'doing something else' 17 | 18 | # waiting for everyone to stop 19 | (Thread.list - [Thread.current]).each &:join -------------------------------------------------------------------------------- /examples/jacobi.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'eldritch' 3 | 4 | def print_matrix(matrix) 5 | matrix.each do |line| 6 | formatted = line.map { |i| '% 10.5f' % i } 7 | puts "[ #{formatted.join(', ')} ]" 8 | end 9 | end 10 | 11 | def create_matrix(n, m) 12 | Array.new(n+2).map{[-1] * (m+2)} 13 | end 14 | 15 | matrix = [ 16 | [-1, -1, -1, -1, -1, -1], 17 | [-1, 1, 2, 3, 4, -1], 18 | [-1, 1, 2, 3, 4, -1], 19 | [-1, 1, 2, 3, 4, -1], 20 | [-1, 1, 2, 3, 4, -1], 21 | [-1, -1, -1, -1, -1, -1] 22 | ] 23 | 24 | epsilon = 0.001 25 | 26 | print_matrix matrix 27 | puts 28 | 29 | height = matrix.length - 2 30 | width = matrix.first.length - 2 31 | 32 | iteration = 1 33 | begin 34 | next_matrix = create_matrix(height, width) 35 | 36 | together do 37 | (1..height).each do |r| 38 | async do 39 | (1..width).each do |c| 40 | neighbors = [c-1, c+1].product([r-1, r+1]).map{|i, j| matrix[i][j]} 41 | next_matrix[r][c] = neighbors.reduce(:+) / 4.0 42 | end 43 | end 44 | end 45 | end 46 | 47 | diff = matrix.flatten.zip(next_matrix.flatten).map{|i, j| (i - j).abs}.max 48 | 49 | matrix = next_matrix 50 | 51 | print_matrix matrix 52 | puts "iteration = #{iteration}; diff = #{diff}" 53 | puts 54 | 55 | iteration += 1 56 | end while diff > epsilon -------------------------------------------------------------------------------- /examples/matrix_multiplication.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'eldritch' 3 | 4 | def print_matrix(matrix) 5 | matrix.each {|line| puts line.inspect} 6 | end 7 | 8 | a = [[1, 2, 3], 9 | [4, 5, 6]] 10 | 11 | b = [[5, 6], 12 | [7, 8], 13 | [9, 10]] 14 | b_t = b.transpose 15 | 16 | puts 'matrix A:' 17 | print_matrix(a) 18 | 19 | puts 'matrix B:' 20 | print_matrix(b) 21 | 22 | c = [[0, 0], 23 | [0, 0]] 24 | 25 | together do 26 | a.each_with_index do |row, y| 27 | b_t.each_with_index do |col, x| 28 | async do 29 | # scalar product 30 | c[y][x] = row.zip(col).map{|i, j| i*j}.reduce(:+) 31 | end 32 | end 33 | end 34 | end 35 | 36 | puts 'A x B = ' 37 | print_matrix(c) -------------------------------------------------------------------------------- /examples/parallel_sort.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'eldritch' 3 | 4 | def merge(a, b) 5 | merged = [] 6 | 7 | until b.empty? || a.empty? do 8 | if a.first <= b.first 9 | merged += a.take_while { |i| (i <= b.first) } 10 | a = a.drop_while { |i| i <= b.first } 11 | else 12 | merged += b.take_while { |i| i <= a.first } 13 | b = b.drop_while { |i| i <= a.first } 14 | end 15 | end 16 | merged + (a.empty? ? b : a) 17 | end 18 | 19 | def parallel_merge_sort(array) 20 | return array if array.size <= 1 21 | 22 | mid = (array.length / 2).floor 23 | 24 | first = async { parallel_merge_sort(array.slice(0, mid)) } 25 | second = parallel_merge_sort(array.slice(mid, array.length - mid)) 26 | 27 | merge(second, first.value) 28 | end 29 | 30 | nums = (1..25).to_a.shuffle 31 | 32 | puts "values: #{nums.join(', ')}" 33 | puts 'sorting...' 34 | sorted = parallel_merge_sort(nums) 35 | puts "values: #{sorted.join(', ')}" 36 | -------------------------------------------------------------------------------- /examples/password_cracker.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'eldritch' 3 | require 'digest/md5' 4 | 5 | if ARGV.size < 2 6 | puts 'Cracks 4 lowercase letter password hashed using MD5' 7 | puts 8 | puts 'usage: password_cracker.rb ' 9 | puts ' threads: the number of threads to run' 10 | puts ' hash: MD5 hash to crack' 11 | puts 12 | puts 'example:' 13 | puts ' password_cracker.rb 4 31d7c3e829be03400641f80b821ef728' 14 | puts ' prints "butts"' 15 | exit 1 16 | end 17 | 18 | threads = ARGV.shift.to_i 19 | hash = ARGV.shift 20 | 21 | # generate all possible 4 lowercase letter passwords 22 | passwords = ('a'..'z').to_a.repeated_permutation(4).lazy.map &:join 23 | 24 | together do |group| 25 | # cut the passwords into slices 26 | passwords.each_slice(passwords.size/threads) do |slice| 27 | async do 28 | slice.each do |password| 29 | if hash == Digest::MD5.hexdigest(password) 30 | group.synchronize do 31 | puts password 32 | 33 | # stop the other tasks 34 | group.interrupt 35 | break 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /examples/simple_async_method.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'eldritch' 3 | 4 | async def foo 5 | puts 'starting long running task' 6 | sleep(1) 7 | puts 'long running task done' 8 | end 9 | 10 | puts 'calling foo' 11 | foo 12 | puts 'doing something else' 13 | 14 | # waiting for everybody to stop 15 | (Thread.list - [Thread.current]).each &:join -------------------------------------------------------------------------------- /examples/together_simple.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'eldritch' 3 | 4 | together do 5 | (1..10).each do |i| 6 | async { puts i } 7 | end 8 | end 9 | 10 | puts 'all done' -------------------------------------------------------------------------------- /lib/eldritch.rb: -------------------------------------------------------------------------------- 1 | require 'eldritch/safe' 2 | 3 | Eldritch.inject_dsl -------------------------------------------------------------------------------- /lib/eldritch/core_ext/thread.rb: -------------------------------------------------------------------------------- 1 | class Thread 2 | attr_writer :eldritch_group 3 | attr_accessor :eldritch_task 4 | 5 | def eldritch_group 6 | @eldritch_group ||= Eldritch::NilGroup.new 7 | end 8 | 9 | def in_eldritch_group? 10 | !eldritch_group.nil? 11 | end 12 | end -------------------------------------------------------------------------------- /lib/eldritch/dsl.rb: -------------------------------------------------------------------------------- 1 | module Eldritch 2 | # Provides DSL for: 3 | # - {#async async methods} 4 | # - {#async async blocks} 5 | # - {#together together blocks} 6 | # - {#sync sync keyword} 7 | module DSL 8 | # Creates an asynchronous method or starts an async block 9 | # 10 | # If a block is passed, this will be an async block. 11 | # Otherwise this method will create an async method. 12 | # 13 | # When an async block is called, it will yield the block in a new thread. 14 | # 15 | # async do 16 | # # will run in parallel 17 | # end 18 | # #=> 19 | # 20 | # When called, async methods behave exactly like async blocks. 21 | # 22 | # async def foo 23 | # # will run in parallel 24 | # end 25 | # 26 | # foo 27 | # #=> 28 | # If you are using ruby < 2.1.0, you will need to define async methods like so: 29 | # 30 | # def foo 31 | # # will run in parallel 32 | # end 33 | # async :foo 34 | # 35 | # @param [Symbol] method the name of the async method. 36 | # @return [Task] a task representing the async method or block 37 | # (only for async block and async method call) 38 | def async(method=nil, &block) 39 | if block 40 | async_block(&block) 41 | else 42 | async_method(method) 43 | end 44 | end 45 | 46 | # Allows async methods to be called like synchronous methods 47 | # 48 | # sync send_email(42) # send_mail is async 49 | # 50 | # @param [Task] task a task returned by {#async} 51 | # @return whatever the method has returned 52 | def sync(task) 53 | task.value 54 | end 55 | 56 | # Creates a group of async call and blocks 57 | # 58 | # When async blocks and calls are inside a together block, they can act as a group. 59 | # 60 | # A together block waits for all the async call/blocks that were started within itself to stop before continuing. 61 | # 62 | # together do 63 | # 5.times do 64 | # async { sleep(1) } 65 | # end 66 | # end 67 | # # waits for all 5 async blocks to complete 68 | # 69 | # A together block will also yield a {Group}. This can be used to interact with the other async calls/blocks. 70 | # 71 | # together do |group| 72 | # 5.times do 73 | # async do 74 | # # stop everyone else 75 | # group.interrupt if something? 76 | # end 77 | # end 78 | # end 79 | # 80 | # @yield [Group] group of async blocks/calls 81 | # @see Group Group class 82 | def together 83 | old = Thread.current.eldritch_group 84 | 85 | group = Group.new 86 | Thread.current.eldritch_group = group 87 | 88 | yield group 89 | 90 | group.join_all 91 | Thread.current.eldritch_group = old 92 | end 93 | 94 | private 95 | 96 | def async_block(&block) 97 | task = Task.new do 98 | begin 99 | block.call 100 | rescue InterruptedError 101 | # exit silently 102 | end 103 | end 104 | Thread.current.eldritch_group << task 105 | task 106 | end 107 | 108 | def async_method(method) 109 | new_method = async_method_name(method) 110 | target = self.kind_of?(Module) ? self : self.class 111 | 112 | target.send :alias_method, new_method, method 113 | target.send :define_method, method do |*args| 114 | async { send(new_method, *args) } 115 | end 116 | end 117 | 118 | def async_method_name(method) 119 | "__async_#{method}".to_sym 120 | end 121 | end 122 | end -------------------------------------------------------------------------------- /lib/eldritch/group.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'reentrant_mutex' 3 | 4 | module Eldritch 5 | # Represents a group of {Task tasks} or {DSL#async async calls/block}. 6 | # It is used to act upon all the tasks in the group. 7 | class Group 8 | def initialize 9 | @tasks = [] 10 | @mutex = ReentrantMutex.new 11 | @accept = true 12 | end 13 | 14 | # @return [Array] the other async calls/blocks in the group 15 | def others 16 | @tasks - [Thread.current.eldritch_task] 17 | end 18 | 19 | def <<(task) 20 | @mutex.synchronize do 21 | if @accept 22 | @tasks << task 23 | task.start 24 | end 25 | end 26 | end 27 | 28 | # Yields the block in mutual exclusion with all the async calls/tasks 29 | # 30 | # @yield 31 | def synchronize(&block) 32 | @mutex.synchronize { block.call } 33 | end 34 | 35 | def join_all 36 | @tasks.each &:join 37 | end 38 | 39 | # Aborts the other async calls/blocks in the group 40 | # 41 | # *Warning*: This call will directly kill underlying threads. This isn't very safe. 42 | # 43 | # @see Task#abort 44 | def abort 45 | @mutex.synchronize do 46 | @accept = false 47 | others.each &:abort 48 | end 49 | end 50 | 51 | # Interrupts the other async calls/blocks in the group 52 | # 53 | # Interruptions are done using exceptions that can be caught by the async calls/blocks to perform cleanup. 54 | # 55 | # @see Task#interrupt 56 | def interrupt 57 | @mutex.synchronize do 58 | @accept = false 59 | others.each &:interrupt 60 | end 61 | end 62 | end 63 | 64 | class NilGroup 65 | def <<(task) 66 | task.start 67 | end 68 | 69 | def nil? 70 | true 71 | end 72 | end 73 | end -------------------------------------------------------------------------------- /lib/eldritch/interrupted_error.rb: -------------------------------------------------------------------------------- 1 | module Eldritch 2 | class InterruptedError < RuntimeError 3 | end 4 | end -------------------------------------------------------------------------------- /lib/eldritch/safe.rb: -------------------------------------------------------------------------------- 1 | require 'eldritch/version' 2 | require 'eldritch/core_ext/thread' 3 | require 'eldritch/task' 4 | require 'eldritch/dsl' 5 | require 'eldritch/group' 6 | require 'eldritch/interrupted_error' 7 | 8 | module Eldritch 9 | # Injects the DSL in the main 10 | # 11 | # This is automatically called when you call 12 | # require 'eldritch' 13 | # 14 | # If you do not want to contaminate the main you can require +eldritch/safe+ and 15 | # include or extend Eldricth::DSL yourself. 16 | # 17 | # require 'eldritch/safe' 18 | # module Sandbox 19 | # include Eldritch::DSL # for async blocks, together and sync 20 | # extend Eldritch::DSL # for async method declaration 21 | # end 22 | def self.inject_dsl 23 | Object.send :include, Eldritch::DSL 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/eldritch/task.rb: -------------------------------------------------------------------------------- 1 | module Eldritch 2 | # Runs a block in parallel and allows for interaction with said block 3 | class Task 4 | # @return [Thread] underlying ruby thread 5 | attr_reader :thread 6 | 7 | # Creates a new Task instance 8 | # 9 | # _Note_: this method does not yield the block directly this is done by {#start} 10 | # 11 | # @yield [Task] the task itself 12 | def initialize(&block) 13 | @block = block 14 | end 15 | 16 | # Starts the task 17 | # 18 | # This will yield the task to the block passed in the constructor. 19 | # 20 | # task = Eldritch::Task.new do |task| 21 | # # do something 22 | # end 23 | # task.start # calls the block in parallel 24 | def start 25 | @thread = Thread.new &@block 26 | @thread.eldritch_task = self 27 | end 28 | 29 | # Waits for the task to complete 30 | def join 31 | @thread.join 32 | unset_thread_task 33 | end 34 | 35 | # The return value of the task 36 | # 37 | # If the task is still running, it will block until it is done and then fetch the return value. 38 | # 39 | # @return whatever the block returns 40 | def value 41 | val = @thread.value 42 | unset_thread_task 43 | val 44 | end 45 | 46 | # Forces the task to end 47 | # 48 | # This kills the underlying thread. This is a dangerous call. 49 | def abort 50 | @thread.kill 51 | unset_thread_task 52 | end 53 | 54 | # Interrupts the task 55 | # 56 | # This is done by raising an {InterruptedError} in the task block. 57 | # This can be caught to perform cleanup before exiting. 58 | # Tasks started with {DSL#async} will automatically handle the exception and stop cleanly. 59 | # You can still handle the exception yourself. 60 | def interrupt 61 | @thread.raise InterruptedError.new 62 | unset_thread_task 63 | end 64 | 65 | private 66 | 67 | def unset_thread_task 68 | @thread.eldritch_task = nil 69 | end 70 | end 71 | end -------------------------------------------------------------------------------- /lib/eldritch/version.rb: -------------------------------------------------------------------------------- 1 | module Eldritch 2 | VERSION = '1.1.3'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'eldritch/dsl' 3 | require 'eldritch/task' 4 | require 'eldritch/group' 5 | 6 | describe Eldritch::DSL do 7 | let(:klass) do Class.new do 8 | extend Eldritch::DSL 9 | include Eldritch::DSL 10 | end 11 | end 12 | 13 | describe '#sync' do 14 | it 'should call task.value' do 15 | task = double(:task) 16 | expect(task).to receive(:value) 17 | klass.sync(task) 18 | end 19 | end 20 | 21 | describe '#together' do 22 | it 'should create a new group' do 23 | expect(Eldritch::Group).to receive(:new).and_return(double('group').as_null_object) 24 | 25 | klass.together {} 26 | end 27 | 28 | it 'should set the current thread group' do 29 | group = double('group').as_null_object 30 | allow(Eldritch::Group).to receive(:new).and_return(group) 31 | allow(Thread.current).to receive(:eldritch_group=).with(anything) 32 | 33 | expect(Thread.current).to receive(:eldritch_group=).with(group) 34 | 35 | klass.together {} 36 | end 37 | 38 | it 'should join on all tasks' do 39 | group = double('group').as_null_object 40 | allow(Eldritch::Group).to receive(:new).and_return(group) 41 | 42 | expect(group).to receive(:join_all) 43 | 44 | klass.together {} 45 | end 46 | 47 | it 'should reset the previous group when it is done' do 48 | group = double('group').as_null_object 49 | old_group = double('old group').as_null_object 50 | allow(Eldritch::Group).to receive(:new).and_return(group) 51 | allow(Thread.current).to receive(:eldritch_group).and_return(old_group) 52 | 53 | klass.together {} 54 | 55 | expect(Thread.current.eldritch_group).to eql(old_group) 56 | end 57 | 58 | it 'should yield it the new group' do 59 | group = double('group').as_null_object 60 | allow(Eldritch::Group).to receive(:new).and_return(group) 61 | 62 | expect{ |b| klass.together &b }.to yield_with_args(group) 63 | end 64 | end 65 | 66 | describe '#async' do 67 | let(:task) { double('task').as_null_object } 68 | let(:group) { double('group').as_null_object } 69 | 70 | before do 71 | call_me = nil 72 | allow(Eldritch::Task).to receive(:new) do |&block| 73 | call_me = block 74 | task 75 | end 76 | 77 | allow(group).to receive(:<<) do 78 | call_me.call(task) 79 | end 80 | 81 | allow(Thread.current).to receive(:eldritch_group).and_return(group) 82 | end 83 | 84 | context 'with 0 arguments' do 85 | it 'should add itself to the group' do 86 | expect(group).to receive(:<<).with(task) 87 | 88 | klass.async {} 89 | end 90 | 91 | it 'should return a task' do 92 | expect(klass.async {}).to eql(task) 93 | end 94 | 95 | it 'should eat any interrupted errors' do 96 | block = proc { raise Eldritch::InterruptedError } 97 | 98 | expect{klass.async &block}.not_to raise_error 99 | end 100 | end 101 | 102 | context 'with 1 argument' do 103 | before do 104 | klass.class_eval do 105 | def foo; end 106 | async :foo 107 | end 108 | end 109 | 110 | it 'should create a __async method' do 111 | expect(klass.new).to respond_to(:__async_foo) 112 | end 113 | 114 | it 'should redefine the method' do 115 | expect(klass).to receive(:define_method).with(:foo) 116 | 117 | klass.class_eval do 118 | def foo; end 119 | async :foo 120 | end 121 | end 122 | 123 | describe 'async method' do 124 | it 'should call the original' do 125 | instance = klass.new 126 | expect(instance).to receive(:__async_foo) 127 | 128 | instance.foo 129 | end 130 | 131 | it 'should pass all arguments' do 132 | klass.class_eval do 133 | def foo(_,_,_); end 134 | async :foo 135 | end 136 | instance = klass.new 137 | expect(instance).to receive(:__async_foo).with(1,2,3) 138 | 139 | instance.foo(1,2,3) 140 | end 141 | end 142 | end 143 | end 144 | end -------------------------------------------------------------------------------- /spec/eldritch_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Eldritch do 4 | it 'should have a version number' do 5 | Eldritch::VERSION.should_not be_nil 6 | end 7 | 8 | describe '#inject_dsl' do 9 | it 'should include the dsl in Object' do 10 | expect(Object).to receive(:include).with(Eldritch::DSL) 11 | 12 | Eldritch.inject_dsl 13 | end 14 | 15 | it 'should allow classes to respond to dsl methods' do 16 | Eldritch.inject_dsl 17 | klass = Class.new 18 | 19 | expect(klass).to respond_to(:async) 20 | expect(klass).to respond_to(:sync) 21 | expect(klass).to respond_to(:together) 22 | end 23 | 24 | it 'should allow objects to respond to dsl methods' do 25 | Eldritch.inject_dsl 26 | obj = Object.new 27 | 28 | expect(obj).to respond_to(:async) 29 | expect(obj).to respond_to(:sync) 30 | expect(obj).to respond_to(:together) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/group_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'eldritch/group' 3 | 4 | describe Eldritch::Group do 5 | let(:group) { Eldritch::Group.new } 6 | 7 | describe '#<<' do 8 | it 'should start the task' do 9 | task = double('task') 10 | expect(task).to receive(:start) 11 | 12 | group << task 13 | end 14 | 15 | it 'should add the task to the list' do 16 | task = double('task').as_null_object 17 | 18 | group << task 19 | 20 | expect(group.others).to include(task) 21 | end 22 | 23 | context 'after it is aborted or interrupted' do 24 | before do 25 | group.abort 26 | end 27 | 28 | it 'should not start the task' do 29 | task = double('task') 30 | 31 | expect(task).not_to receive(:start) 32 | 33 | group << task 34 | end 35 | 36 | it 'should not add the task to the list' do 37 | task = double('task') 38 | 39 | group << task 40 | 41 | expect(group.others).not_to include(task) 42 | end 43 | end 44 | end 45 | 46 | describe '#others' do 47 | it 'should return an empty array when there is only one task' do 48 | task = double('task').as_null_object 49 | allow(Thread.current).to receive(:eldritch_task).and_return(task) 50 | 51 | group << task 52 | 53 | expect(group.others).to be_kind_of(Array) 54 | expect(group.others).to be_empty 55 | end 56 | 57 | it 'should return all the task except the current one' do 58 | task = double('task').as_null_object 59 | allow(Thread.current).to receive(:eldritch_task).and_return(task) 60 | other_task = double('other task').as_null_object 61 | 62 | group << task 63 | group << other_task 64 | 65 | expect(group.others).to eql([other_task]) 66 | end 67 | end 68 | 69 | describe '#join_all' do 70 | it 'should call join on all tasks' do 71 | task = double('task') 72 | allow(task).to receive(:start) 73 | group << task 74 | 75 | expect(task).to receive(:join) 76 | 77 | group.join_all 78 | end 79 | end 80 | 81 | describe '#synchronize' do 82 | it 'should yield' do 83 | expect{|b| group.synchronize &b}.to yield_control 84 | end 85 | end 86 | 87 | describe '#abort' do 88 | it 'should call abort on all tasks' do 89 | task = double('task').as_null_object 90 | expect(task).to receive(:abort) 91 | 92 | group << task 93 | group.abort 94 | end 95 | 96 | it 'should not call abort on current task' do 97 | task = double('task').as_null_object 98 | expect(task).not_to receive(:abort) 99 | allow(Thread.current).to receive(:eldritch_task).and_return(task) 100 | 101 | group << task 102 | group.abort 103 | end 104 | end 105 | 106 | describe '#interrupt' do 107 | it 'should call interrupt on all tasks' do 108 | task = double('task').as_null_object 109 | expect(task).to receive(:interrupt) 110 | 111 | group << task 112 | group.interrupt 113 | end 114 | 115 | it 'should not call interrupt on current task' do 116 | task = double('task').as_null_object 117 | expect(task).not_to receive(:interrupt) 118 | allow(Thread.current).to receive(:eldritch_task).and_return(task) 119 | 120 | group << task 121 | group.interrupt 122 | end 123 | end 124 | end 125 | 126 | describe Eldritch::NilGroup do 127 | let(:group) { Eldritch::NilGroup.new } 128 | 129 | describe '#<<' do 130 | it 'should call the task' do 131 | task = double('task') 132 | expect(task).to receive(:start) 133 | 134 | group << task 135 | end 136 | end 137 | 138 | describe '#nil?' do 139 | it 'should be true' do 140 | expect(group.nil?).to be_truthy 141 | end 142 | end 143 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! 3 | 4 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 5 | require 'eldritch/safe' 6 | -------------------------------------------------------------------------------- /spec/task_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'eldritch/task' 3 | 4 | describe Eldritch::Task do 5 | let(:task) { Eldritch::Task.new {} } 6 | let(:thread) { double(:thread).as_null_object } 7 | before do 8 | allow(Thread).to receive(:new).and_yield.and_return(thread) 9 | end 10 | 11 | it 'should not start a thread on init' do 12 | expect(task.thread).to be_nil 13 | end 14 | 15 | describe '#start' do 16 | it 'should create a thread' do 17 | task.start 18 | 19 | expect(task.thread).not_to be_nil 20 | end 21 | 22 | it 'should call the block' do 23 | expect do |b| 24 | task = Eldritch::Task.new &b 25 | task.start 26 | end.to yield_control 27 | end 28 | 29 | it 'should start a thread' do 30 | expect(Thread).to receive(:new) 31 | 32 | task.start 33 | end 34 | 35 | it 'should set the thread task' do 36 | expect(thread).to receive(:eldritch_task=).with(task) 37 | 38 | task.start 39 | end 40 | end 41 | 42 | describe '#join' do 43 | it 'should join the thread' do 44 | task.start 45 | 46 | expect(task.thread).to receive(:join) 47 | task.join 48 | end 49 | 50 | it 'should set the thread task to nil' do 51 | task.start 52 | 53 | expect(thread).to receive(:eldritch_task=).with(nil) 54 | task.join 55 | end 56 | end 57 | 58 | describe '#value' do 59 | it 'should set the thread task to nil' do 60 | task.start 61 | 62 | expect(thread).to receive(:eldritch_task=).with(nil) 63 | task.value 64 | end 65 | 66 | it 'should call Thread#value' do 67 | task.start 68 | expect(thread).to receive(:value) 69 | task.value 70 | end 71 | 72 | it 'should return what Thread#value returns' do 73 | task.start 74 | allow(thread).to receive(:value).and_return(42) 75 | expect(task.value).to eql(42) 76 | end 77 | end 78 | 79 | describe '#abort' do 80 | it 'should set the thread task to nil' do 81 | task.start 82 | expect(thread).to receive(:eldritch_task=).with(nil) 83 | 84 | task.abort 85 | end 86 | 87 | it 'should kill the thread' do 88 | expect(thread).to receive(:kill) 89 | 90 | task.start 91 | task.abort 92 | end 93 | end 94 | 95 | describe '#interrupt' do 96 | it 'should set the thread task to nil' do 97 | task.start 98 | expect(thread).to receive(:eldritch_task=).with(nil) 99 | 100 | task.interrupt 101 | end 102 | 103 | it 'should raise an interrupted error on the thread' do 104 | expect(thread).to receive(:raise).with(kind_of(Eldritch::InterruptedError)) 105 | 106 | task.start 107 | 108 | task.interrupt 109 | end 110 | end 111 | end -------------------------------------------------------------------------------- /spec/thread_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'eldritch/core_ext/thread' 3 | 4 | describe Thread do 5 | let(:thread) { Thread.new {} } 6 | 7 | it 'should have group accessor' do 8 | expect(thread).to respond_to(:eldritch_group) 9 | expect(thread).to respond_to(:eldritch_group=) 10 | end 11 | 12 | it 'should have a task accessor' do 13 | expect(thread).to respond_to(:eldritch_task) 14 | expect(thread).to respond_to(:eldritch_task=) 15 | end 16 | 17 | describe '#group' do 18 | it 'should return the togther previously set' do 19 | group = double('group') 20 | thread.eldritch_group = group 21 | expect(thread.eldritch_group).to eql(group) 22 | end 23 | 24 | it 'should return a NilGroup when none are set' do 25 | expect(thread.eldritch_group).to be_a Eldritch::NilGroup 26 | end 27 | end 28 | 29 | describe '#in_group?' do 30 | it 'should be false when group is nil' do 31 | thread.eldritch_group = nil 32 | expect(thread.in_eldritch_group?).to be_falsey 33 | end 34 | 35 | it 'should be false when group is a NilGroup' do 36 | thread.eldritch_group = Eldritch::NilGroup.new 37 | expect(thread.in_eldritch_group?).to be_falsey 38 | end 39 | 40 | it 'should be true when group is set' do 41 | thread.eldritch_group = 2 42 | expect(thread.in_eldritch_group?).to be_truthy 43 | end 44 | end 45 | end --------------------------------------------------------------------------------