├── .ruby-version ├── .ruby-gemset ├── lib ├── futuroscope │ ├── dsl.rb │ ├── version.rb │ ├── pools │ │ ├── no_pool.rb │ │ ├── worker.rb │ │ └── worker_pool.rb │ ├── convenience.rb │ ├── pool.rb │ ├── map.rb │ └── future.rb └── futuroscope.rb ├── .rspec ├── .travis.yml ├── spec ├── spec_helper.rb ├── futuroscope │ ├── pools │ │ ├── no_pool_spec.rb │ │ ├── worker_spec.rb │ │ └── worker_pool_spec.rb │ ├── map_spec.rb │ ├── convenience_spec.rb │ └── future_spec.rb └── futuroscope_spec.rb ├── Rakefile ├── .gitignore ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── futuroscope.gemspec └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.1.0 -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | futuroscope -------------------------------------------------------------------------------- /lib/futuroscope/dsl.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | --require spec_helper -------------------------------------------------------------------------------- /lib/futuroscope/version.rb: -------------------------------------------------------------------------------- 1 | module Futuroscope 2 | VERSION = "0.1.11" 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | rvm: 4 | - 1.9.3 5 | - 2.0.0 6 | - rbx-2 7 | - 2.1.2 8 | - jruby-19mode 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV["CI"] && RUBY_ENGINE == "ruby" 2 | require "coveralls" 3 | Coveralls.wear! 4 | end 5 | 6 | require "rspec/collection_matchers" 7 | require "futuroscope" 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new do |t| 5 | t.pattern = "spec/**/*_spec.rb" 6 | end 7 | 8 | task :default => :spec 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .rbx 4 | .bundle 5 | .config 6 | .yardoc 7 | Gemfile.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "guard" 4 | gem "guard-rspec" 5 | gem "guard-bundler" 6 | 7 | gem "coveralls" 8 | 9 | gem "json" 10 | gem "rubysl", platform: :rbx 11 | 12 | # Specify your gem's dependencies in futuroscope.gemspec 13 | gemspec 14 | -------------------------------------------------------------------------------- /lib/futuroscope/pools/no_pool.rb: -------------------------------------------------------------------------------- 1 | module Futuroscope 2 | module Pools 3 | # A pool, that does not actually do any thread pooling, but just runs 4 | # futures right away. 5 | class NoPool < Pool 6 | def queue(future) 7 | future.run_future 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/futuroscope/convenience.rb: -------------------------------------------------------------------------------- 1 | require "futuroscope" 2 | 3 | module Kernel 4 | def future(pool = Futuroscope.default_pool, &block) 5 | Futuroscope::Future.new(pool, &block) 6 | end 7 | end 8 | 9 | module Enumerable 10 | def future_map(&block) 11 | Futuroscope::Map.new(self).map(&block) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/futuroscope/pools/no_pool_spec.rb: -------------------------------------------------------------------------------- 1 | module Futuroscope 2 | module Pools 3 | describe NoPool do 4 | it "should run queued futures on the same thread" do 5 | pool = NoPool.new 6 | future = Future.new(pool) { Thread.current } 7 | 8 | expect(future).to eq Thread.current 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard "rspec", bundler: false do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 7 | watch("spec/spec_helper.rb") { "spec" } 8 | end 9 | 10 | guard "bundler" do 11 | watch("Gemfile") 12 | watch(/^.+\\.gemspec/) 13 | end 14 | -------------------------------------------------------------------------------- /lib/futuroscope/pool.rb: -------------------------------------------------------------------------------- 1 | module Futuroscope 2 | # A pool runs futures in an implementation-specific way. 3 | # 4 | # The default implementation is the WorkerPool, that runs futures concurrently 5 | # in threads. 6 | class Pool 7 | # Public: Enqueues a new Future into the pool. 8 | # 9 | # future - The Future to enqueue. 10 | def queue(future) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/futuroscope/map_spec.rb: -------------------------------------------------------------------------------- 1 | module Futuroscope 2 | describe Map do 3 | it "behaves like a normal map" do 4 | items = [1, 2, 3] 5 | result = Map.new(items).map do |item| 6 | sleep(item) 7 | "Item #{item}" 8 | end 9 | 10 | Timeout::timeout(4) do 11 | expect(result.first).to eq("Item 1") 12 | expect(result[1]).to eq("Item 2") 13 | expect(result.last).to eq("Item 3") 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/futuroscope_spec.rb: -------------------------------------------------------------------------------- 1 | describe Futuroscope do 2 | describe "default_pool" do 3 | it "returns a pool by default" do 4 | expect(Futuroscope.default_pool).to be_kind_of(Futuroscope::Pool) 5 | end 6 | end 7 | 8 | describe "default_pool=" do 9 | it "allows you to set a new default pool" do 10 | pool = Futuroscope::Pool.new 11 | Futuroscope.default_pool = pool 12 | expect(Futuroscope.default_pool).to equal(pool) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/futuroscope/pools/worker_spec.rb: -------------------------------------------------------------------------------- 1 | module Futuroscope 2 | module Pools 3 | describe Worker do 4 | it "asks the pool for a new job and runs the future" do 5 | future = double(:future) 6 | pool = [future] 7 | expect(future).to receive :run_future 8 | 9 | Worker.new(pool).run 10 | sleep(1) 11 | end 12 | 13 | it "notifies the pool when the worker died because there's no job" do 14 | pool = [] 15 | worker = Worker.new(pool) 16 | 17 | expect(pool).to receive(:worker_died).with(worker) 18 | worker.run 19 | sleep(1) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/futuroscope.rb: -------------------------------------------------------------------------------- 1 | require "futuroscope/version" 2 | 3 | require "futuroscope/future" 4 | 5 | require "futuroscope/pool" 6 | require "futuroscope/pools/worker" 7 | require "futuroscope/pools/worker_pool" 8 | require "futuroscope/pools/no_pool" 9 | 10 | require "futuroscope/map" 11 | 12 | module Futuroscope 13 | # Gets the default futuroscope's pool. 14 | # 15 | # Returns a Pool 16 | def self.default_pool 17 | @default_pool ||= Pools::WorkerPool.new 18 | end 19 | 20 | # Sets a new default pool. It's useful when you want to set a different 21 | # number of concurrent threads. 22 | # 23 | # Example: 24 | # Futuroscope.default_pool = Futuroscope::Pool.new(24) 25 | def self.default_pool=(pool) 26 | @default_pool = pool 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/futuroscope/convenience_spec.rb: -------------------------------------------------------------------------------- 1 | require "futuroscope/convenience" 2 | require "timeout" 3 | 4 | describe "Kernel#future" do 5 | it "adds a convenience method to ruby's kernel" do 6 | x = future{ sleep(1); 1 } 7 | y = future{ sleep(1); 2 } 8 | z = future{ sleep(1); 3 } 9 | 10 | Timeout::timeout(2.5) do 11 | expect(x + y + z).to eq(6) 12 | end 13 | end 14 | end 15 | 16 | describe "Enumerable#future_map" do 17 | it "adds a future_map method do Enumerable" do 18 | items = [1, 2, 3] 19 | map = items.future_map do |i| 20 | sleep(1) 21 | i + 1 22 | end 23 | 24 | Timeout::timeout(2.5) do 25 | expect(map.first).to eq(2) 26 | expect(map[1]).to eq(3) 27 | expect(map.last).to eq(4) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/futuroscope/map.rb: -------------------------------------------------------------------------------- 1 | require "futuroscope/future" 2 | 3 | module Futuroscope 4 | # A futuroscope map behaves like a regular map but performs all operations 5 | # using futures so they're effectively parallel. 6 | # 7 | class Map 8 | # Initializes a map with a set of items. 9 | # 10 | # items - Items in which to perform the mapping 11 | # 12 | def initialize(items) 13 | @items = items 14 | end 15 | 16 | # Maps each item with a future. 17 | # 18 | # block - A block that will be executed passing each element as a parameter 19 | # 20 | # Returns an array of futures that behave like the original objects. 21 | def map(&block) 22 | @items.map do |item| 23 | Future.new do 24 | block.call(item) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/futuroscope/pools/worker.rb: -------------------------------------------------------------------------------- 1 | module Futuroscope 2 | module Pools 3 | # A worker asks it's pool for new jobs and runs them concurrently in a 4 | # thread. 5 | class Worker 6 | # Public: Initializes a new Worker. 7 | # 8 | # pool - The worker Pool it belongs to. 9 | def initialize(pool) 10 | @pool = pool 11 | end 12 | 13 | # Runs the worker. It keeps asking the Pool for a new job. If the pool 14 | # decides there's no job use it now or in the future, it will die and the 15 | # Pool will be notified. Otherwise, it will be given a new job or blocked 16 | # until there's a new future available to process. 17 | def run 18 | @thread = Thread.new do 19 | while (future = @pool.pop) 20 | future.run_future 21 | end 22 | 23 | die 24 | end 25 | end 26 | 27 | # Public: Stops this worker. 28 | def stop 29 | @thread.kill 30 | 31 | die 32 | end 33 | 34 | private 35 | 36 | def die 37 | @pool.worker_died(self) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Josep Jaume Rey Peroy 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 | -------------------------------------------------------------------------------- /futuroscope.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "futuroscope/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "futuroscope" 8 | spec.version = Futuroscope::VERSION 9 | spec.authors = ["Josep Jaume Rey Peroy"] 10 | spec.email = ["josepjaume@gmail.com"] 11 | spec.description = %q{Futuroscope is yet another simple gem that implements the Futures concurrency pattern.} 12 | spec.summary = %q{Futuroscope is yet another simple gem that implements the Futures concurrency pattern.} 13 | spec.homepage = "" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.3" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "rspec" 24 | spec.add_development_dependency "rspec-collection_matchers" 25 | spec.add_development_dependency "rspec-mocks" 26 | spec.add_runtime_dependency "rubysl" if RUBY_ENGINE == "rbx" 27 | end 28 | -------------------------------------------------------------------------------- /spec/futuroscope/pools/worker_pool_spec.rb: -------------------------------------------------------------------------------- 1 | module Futuroscope 2 | module Pools 3 | describe WorkerPool do 4 | it "spins up a number of workers" do 5 | pool = WorkerPool.new(2..4) 6 | expect(pool.workers).to have(2).workers 7 | 8 | pool = WorkerPool.new(3..4) 9 | expect(pool.workers).to have(3).workers 10 | end 11 | 12 | describe "queue" do 13 | it "enqueues a job and runs it" do 14 | pool = WorkerPool.new 15 | future = double(:future) 16 | 17 | expect(future).to receive :run_future 18 | pool.queue future 19 | sleep(0.1) 20 | end 21 | end 22 | 23 | describe "worker control" do 24 | it "adds more workers when needed and returns to the default amount" do 25 | pool = WorkerPool.new(2..8) 26 | allow(pool).to receive(:span_chance).and_return true 27 | 10.times do 28 | Future.new(pool) { sleep(1) } 29 | end 30 | 31 | sleep(0.5) 32 | expect(pool.workers).to have(8).workers 33 | 34 | sleep(1.5) 35 | expect(pool.workers).to have(2).workers 36 | end 37 | 38 | it "allows overriding min workers real time" do 39 | pool = WorkerPool.new(2..8) 40 | pool.min_workers = 3 41 | expect(pool.workers).to have(3).workers 42 | end 43 | 44 | it "allows overriding max workers real time" do 45 | pool = WorkerPool.new(2..8) 46 | allow(pool).to receive(:span_chance).and_return true 47 | pool.max_workers = 4 48 | 49 | 10.times do 50 | Future.new(pool) { sleep(1) } 51 | end 52 | 53 | sleep(0.5) 54 | expect(pool.workers).to have(4).workers 55 | end 56 | end 57 | 58 | describe "#finalize" do 59 | it "shuts down all its workers" do 60 | pool = WorkerPool.new(2..8) 61 | 62 | pool.send(:finalize) 63 | 64 | expect(pool.workers).to have(0).workers 65 | end 66 | end 67 | 68 | describe "#span_chance" do 69 | it "returns true or false randomly" do 70 | pool = WorkerPool.new 71 | chance = pool.send(:span_chance) 72 | 73 | expect([true, false]).to include(chance) 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/futuroscope/future.rb: -------------------------------------------------------------------------------- 1 | require "thread" 2 | require "delegate" 3 | require "forwardable" 4 | 5 | module Futuroscope 6 | # A Future is an object that gets initialized with a block and will behave 7 | # exactly like the block's result, but being able to "borrow" its result from 8 | # the future. That is, will block when the result is not ready until it is, 9 | # and will return it instantly if the thread's execution already finished. 10 | # 11 | class Future < Delegator 12 | extend ::Forwardable 13 | 14 | # Initializes a future with a block and starts its execution. 15 | # 16 | # Examples: 17 | # 18 | # future = Futuroscope::Future.new { sleep(1); :edballs } 19 | # sleep(1) 20 | # puts future 21 | # => :edballs 22 | # # This will return in 1 second and not 2 if the execution wasn't 23 | # # deferred to a thread. 24 | # 25 | # pool - A pool where all the futures will be scheduled. 26 | # block - A block that will be run in the background. 27 | # 28 | # Returns a Future 29 | def initialize(pool = ::Futuroscope.default_pool, &block) 30 | @queue = ::SizedQueue.new(1) 31 | @pool = pool 32 | @block = block 33 | @mutex = Mutex.new 34 | @pool.queue self 35 | end 36 | 37 | # Semipublic: Forces this future to be run. 38 | def run_future 39 | @queue.push(value: @block.call) 40 | rescue ::Exception => e 41 | @queue.push(exception: e) 42 | end 43 | 44 | # Semipublic: Returns the future's value. Will wait for the future to be 45 | # completed or return its value otherwise. Can be called multiple times. 46 | # 47 | # Returns the Future's block execution result. 48 | def __getobj__ 49 | resolved_future_value_or_raise[:value] 50 | end 51 | 52 | def __setobj__ obj 53 | @resolved_future = { value: obj } 54 | end 55 | 56 | def marshal_dump 57 | resolved_future_value_or_raise 58 | end 59 | 60 | def marshal_load value 61 | @resolved_future = value 62 | end 63 | 64 | def_delegators :__getobj__, :class, :kind_of?, :is_a?, :nil? 65 | 66 | alias_method :future_value, :__getobj__ 67 | 68 | private 69 | 70 | def resolved_future_value_or_raise 71 | resolved = resolved_future_value 72 | 73 | Kernel.raise resolved[:exception] if resolved[:exception] 74 | resolved 75 | end 76 | 77 | def resolved_future_value 78 | @resolved_future || @mutex.synchronize do 79 | @resolved_future ||= @queue.pop 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/futuroscope/pools/worker_pool.rb: -------------------------------------------------------------------------------- 1 | require "set" 2 | require "thread" 3 | 4 | module Futuroscope 5 | module Pools 6 | # Futuroscope's pool is design to control concurency and keep it between 7 | # some certain benefits. Moreover, we warm up the threads beforehand so we 8 | # don't have to spin them up each time a future is created. 9 | class WorkerPool < Pool 10 | attr_reader :workers 11 | attr_accessor :min_workers, :max_workers 12 | 13 | # Public: Initializes a new Pool. 14 | # 15 | # thread_count - The number of workers that this pool is gonna have 16 | def initialize(range = 8..16) 17 | @min_workers = range.min 18 | @max_workers = range.max 19 | @queue = Queue.new 20 | @workers = Set.new 21 | @mutex = Mutex.new 22 | warm_up_workers 23 | end 24 | 25 | def queue(future) 26 | @mutex.synchronize do 27 | spin_worker if can_spin_extra_workers? 28 | 29 | @queue.push future 30 | end 31 | end 32 | 33 | # Internal: Pops a new job from the pool. It will return nil if there's 34 | # enough workers in the pool to take care of it. 35 | # 36 | # Returns a Future 37 | def pop 38 | @mutex.synchronize do 39 | return nil if @queue.empty? && more_workers_than_needed? 40 | end 41 | 42 | @queue.pop 43 | end 44 | 45 | # Internal: Notifies that a worker just died so it can be removed from the 46 | # pool. 47 | # 48 | # worker - A Worker 49 | def worker_died(worker) 50 | @mutex.synchronize do 51 | @workers.delete(worker) 52 | end 53 | end 54 | 55 | def min_workers=(count) 56 | @min_workers = count 57 | warm_up_workers 58 | end 59 | 60 | private 61 | 62 | def warm_up_workers 63 | @mutex.synchronize do 64 | while (@workers.length < @min_workers) 65 | spin_worker 66 | end 67 | end 68 | end 69 | 70 | def can_spin_extra_workers? 71 | @workers.length < @max_workers && span_chance 72 | end 73 | 74 | def span_chance 75 | [true, false].sample 76 | end 77 | 78 | def more_workers_than_needed? 79 | @workers.length > @min_workers 80 | end 81 | 82 | def spin_worker 83 | worker = Worker.new(self) 84 | @workers << worker 85 | worker.run 86 | end 87 | 88 | def finalize 89 | @workers.each(&:stop) 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/futuroscope/future_spec.rb: -------------------------------------------------------------------------------- 1 | require "timeout" 2 | 3 | module Futuroscope 4 | describe Future do 5 | it "will return an instant value" do 6 | future = Future.new{ :edballs } 7 | sleep(0.1) 8 | 9 | expect(future).to eq(:edballs) 10 | end 11 | 12 | it "will execute the future in the background and wait for it" do 13 | future = Future.new{ sleep(1); :edballs } 14 | 15 | sleep(1) 16 | Timeout::timeout(0.9) do 17 | expect(future).to eq(:edballs) 18 | end 19 | end 20 | 21 | it "delegates some Object methods to the original object's" do 22 | object = [1, 2, 3] 23 | future = Future.new{object} 24 | 25 | expect(future.class).to eq(Array) 26 | expect(future).to be_kind_of(Enumerable) 27 | expect(future).to be_a(Enumerable) 28 | expect(future.to_s).to eq(object.to_s) 29 | expect(Future.new { nil }).to be_nil 30 | end 31 | 32 | it "delegates missing methods" do 33 | object = [1, 2, 3] 34 | future = Future.new{object} 35 | expect(future).to_not be_empty 36 | end 37 | 38 | it "captures exceptions and re-raises them when calling the value" do 39 | future = Future.new { raise "Ed Balls" } 40 | 41 | expect { future.inspect }.to raise_error RuntimeError, "Ed Balls" 42 | end 43 | 44 | it "returns the original object when future_value gets called" do 45 | object = double 46 | future = Future.new{ object } 47 | 48 | expect(future.future_value.object_id === object.object_id).to eq(true) 49 | end 50 | 51 | it "marshals a future object by serializing the result value" do 52 | object = [1, 2, 3] 53 | future = Future.new{object} 54 | dumped = Marshal.dump(future) 55 | expect(Marshal.load(dumped).future_value).to eq(object) 56 | end 57 | 58 | it "re-raises captured exception when trying to marshal" do 59 | future = Future.new { raise "Ed Balls" } 60 | 61 | expect { Marshal.dump(future) }.to raise_error RuntimeError, "Ed Balls" 62 | end 63 | 64 | it "correctly duplicates a future object" do 65 | object = [1, 2, 3] 66 | future = Future.new { object } 67 | 68 | expect(future.dup).to eq future 69 | end 70 | 71 | it "clones correctly before being resolved" do 72 | object = [1, 2, 3] 73 | future = Future.new { sleep 1; object } 74 | clone = future.clone 75 | 76 | expect(clone).to eq object 77 | end 78 | 79 | context "when at least another thread is alive" do 80 | # If no threads are alive, the VM raises an exception, therefore we need to ensure there is one. 81 | 82 | before :each do 83 | @live_thread = Thread.new { loop { } } 84 | end 85 | 86 | 87 | it "doesn't hang when 2 threads try to obtain its result before it's finished" do 88 | test_thread = Thread.new do 89 | future = Future.new { sleep 1; 1 } 90 | f1 = Future.new { future + 1 } 91 | f2 = Future.new { future + 2 } 92 | f1.future_value 93 | f2.future_value 94 | end 95 | sleep 2.5 96 | expect(test_thread).to_not be_alive 97 | test_thread.kill 98 | end 99 | 100 | 101 | after :each do 102 | @live_thread.kill 103 | end 104 | 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Futuroscope 2 | [![Gem Version](https://badge.fury.io/rb/futuroscope.svg)](http://badge.fury.io/rb/futuroscope) 3 | [![Build Status](https://travis-ci.org/codegram/futuroscope.svg?branch=master)](https://travis-ci.org/codegram/futuroscope) 4 | [![Code Climate](https://codeclimate.com/github/codegram/futuroscope.svg)](https://codeclimate.com/github/codegram/futuroscope) 5 | [![Coverage Status](https://coveralls.io/repos/codegram/futuroscope/badge.svg?branch=master)](https://coveralls.io/r/codegram/futuroscope) 6 | [![Dependency Status](https://gemnasium.com/codegram/futuroscope.svg)](https://gemnasium.com/codegram/futuroscope) 7 | 8 | **Join a live discussion on Gitter**: [![Gitter chat](https://badges.gitter.im/codegram/futuroscope.svg)](https://gitter.im/codegram/futuroscope) 9 | 10 | Futursocope is a simple library that implements futures in ruby. Futures are a 11 | concurrency pattern meant to help you deal with threads in a simple, transparent way. 12 | 13 | It's specially useful in situations where you have calls to an expensive resource that 14 | could be done in parallel (they are not chained), but you don't wanna deal with low-level 15 | threads. HTTP calls are a good example. 16 | 17 | Also useful when you want to spin up a process that runs in the background, do some stuff 18 | in the middle, and wait for that process to return. 19 | 20 | [![The awesome Futuroscope park](http://www.futuroscope.com/uploads/images/common/attraction/8eb495bb27d1b2102bab2db33b398c2ef6a24e2f.jpg)](http://futuroscope.com) 21 | 22 | You can learn more about futures here in this excellent article from @jpignata: 23 | [Concurrency Patterns in Ruby: 24 | Futures](http://tx.pignata.com/2012/11/concurrency-patterns-in-ruby-futures.html) 25 | 26 | In Futuroscope, futures are instantiated with a simple ruby block. The future's 27 | execution will immediately start in a different thread and when you call a 28 | method on in it will be forwarded to the block's return value. 29 | 30 | If the thread didn't finish yet, it will block the program's execution until 31 | it's finished. Otherwise, it will immediately return its value. 32 | 33 | Futuroscope is tested on `MRI 1.9.3`, `MRI 2.0.0`, `MRI 2.1.0`, `Rubinius (2.2.3)` and `JRuby (1.9)`. 34 | 35 | Check out [futuroscope's post on Codegram's blog](http://thoughts.codegram.com/new-gem-released-futuroscope/) to get started. 36 | 37 | ## Installation 38 | 39 | Add this line to your application's Gemfile: 40 | 41 | gem "futuroscope" 42 | 43 | And then execute: 44 | 45 | $ bundle 46 | 47 | Or install it yourself as: 48 | 49 | $ gem install futuroscope 50 | 51 | ## Usage 52 | 53 | ### Simple futures 54 | ```Ruby 55 | require "futuroscope" 56 | 57 | x = Futuroscope::Future.new{ sleep(1); 1 } 58 | y = Futuroscope::Future.new{ sleep(1); 2 } 59 | z = Futuroscope::Future.new{ sleep(1); 3 } 60 | 61 | # This execution will actually take just one second and not three like you 62 | # would expect. 63 | 64 | puts x + y + z 65 | => 6 66 | ``` 67 | 68 | Since a `future` is actually delegating everything to the future's value, there 69 | might be some cases where you want to get the actual future's value. You can do 70 | it just by calling the `future_value` method on the future: 71 | 72 | ```Ruby 73 | string = "Ed Balls" 74 | x = future{ string } 75 | x.future_value === string 76 | # => true 77 | ``` 78 | 79 | ### Future map 80 | ```Ruby 81 | require "futuroscope" 82 | 83 | map = Futuroscope::Map.new([1, 2, 3]).map do |i| 84 | sleep(1) 85 | i + 1 86 | end 87 | 88 | puts map.first 89 | => 2 90 | 91 | puts map[1] 92 | => 3 93 | 94 | puts map.last 95 | => 4 96 | 97 | # This action will actually only take 1 second. 98 | ``` 99 | 100 | ### Convenience methods 101 | 102 | If you don't mind polluting the `Kernel` module, you can also require 103 | futuroscope's convenience `future` method: 104 | 105 | ```Ruby 106 | require "futuroscope/convenience" 107 | 108 | x = future{ sleep(1); 1 } 109 | y = future{ sleep(1); 2 } 110 | z = future{ sleep(1); 3 } 111 | 112 | puts x + y + z 113 | => 6 114 | ``` 115 | 116 | Same for a map: 117 | 118 | ```Ruby 119 | require "futuroscope/convenience" 120 | 121 | items = [1, 2, 3].future_map do |i| 122 | sleep(i) 123 | i + 1 124 | end 125 | ``` 126 | 127 | ## Considerations 128 | 129 | You should never add **side-effects** to a future. They have to be thought of 130 | like they were a local variable, with the only outcome that they're returning a 131 | value. 132 | 133 | You have to take into account that they really run in a different thread, so 134 | you'll be potentially accessing code in parallel that could not be thread-safe. 135 | 136 | If you're looking for other ways to improve your code performance via 137 | concurrency, you should probably deal directly with [Ruby's 138 | threads](http://ruby-doc.org/core-2.0/Thread.html). 139 | 140 | ## Worker Pool 141 | 142 | Futures are scheduled in a worker pool that helps managing concurrency in a way 143 | that doesn't get out of hands. Also comes with great benefits since their 144 | threads are spawned at load time (and not in runtime). 145 | 146 | The default thread pool comes with a concurrency of 8 Workers, which seems 147 | reasonable for the most use cases. It will elastically expand to the default of 148 | 16 threads and will kill them when they're not needed. 149 | 150 | The default thread pool can be configured like this: 151 | 152 | ```Ruby 153 | Futuroscope.default_pool.min_workers = 2 154 | Futuroscope.default_pool.max_workers = 16 155 | ``` 156 | 157 | Also, each future can be scheduled to a different pool like this: 158 | 159 | ```Ruby 160 | pool = Futuroscope::Pools::WorkerPool.new(16..32) 161 | 162 | future = Future.new(pool){ :edballs } 163 | 164 | # Or with the convenience method 165 | future = future(pool){ :edballs } 166 | ``` 167 | 168 | ## Disabling concurrency 169 | 170 | Sometimes you may want to run futures in the main thread, instead of worker 171 | threads, e.g. for simpler testing. Therefore you can use the 172 | `Futuroscope::Pools::NoPool`, that runs futures directly instead of queueing 173 | them. 174 | 175 | ```ruby 176 | # Globally 177 | Futuroscope.default_pool = Futuroscope::Pools::NoPool.new 178 | 179 | Futuroscope::Future.new { puts "No concurrency here" } 180 | 181 | # Locally 182 | pool = Futuroscope::Pools::NoPool.new 183 | 184 | Futuroscope::Future.new(pool) do 185 | puts "No concurrency here" 186 | end 187 | ``` 188 | 189 | ## Contributing 190 | 191 | 1. Fork it 192 | 2. Create your feature branch (`git checkout -b my-new-feature`) 193 | 3. Commit your changes (`git commit -am "Add some feature"`) 194 | 4. Push to the branch (`git push origin my-new-feature`) 195 | 5. Create new Pull Request 196 | 197 | 198 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/codegram/futuroscope/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 199 | --------------------------------------------------------------------------------