promise_pool 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ba178cdd8145f5c65340a8599ffc9dbd4775ccef
4
+ data.tar.gz: a978b9d5072e8038f9cb2e6884432bc9e4f0a6ac
5
+ SHA512:
6
+ metadata.gz: d531803ba5e8d28b438da3d152e8315ba7ce0de796ee781bcf0f0c2de23f7bf598bed2e9693d548ed05414cd8bd623b34f754588b1cb4ac3ac2bdf4dc898848e
7
+ data.tar.gz: 406aa62fec4d02adf8eaf0088a0ed8ac7e08a70cf147d01c8ab7d41cbc588541385ff4a1633d18fb3c6e848860a4cb7f9f28c9b8c361f358006071c0954b0257
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /pkg/
2
+ /coverage/
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "task"]
2
+ path = task
3
+ url = git://github.com/godfat/gemgem
data/.travis.yml ADDED
@@ -0,0 +1,15 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.1
5
+ - 2.2
6
+ - 2.3.0
7
+ - rbx
8
+ - jruby-9
9
+
10
+ before_install:
11
+ - rvm get head
12
+ - rvm reload
13
+ - rvm use --install $TRAVIS_RUBY_VERSION --binary --latest
14
+ install: 'bundle install --retry=3'
15
+ script: 'ruby -vr bundler/setup -S rake test'
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+
2
+ source 'https://rubygems.org/'
3
+
4
+ gemspec
5
+
6
+ gem 'rake'
7
+ gem 'pork'
8
+ gem 'muack'
9
+
10
+ gem 'simplecov', :require => false if ENV['COV']
11
+ gem 'coveralls', :require => false if ENV['CI']
12
+
13
+ platforms :rbx do
14
+ gem 'rubysl-singleton' # used in rake
15
+ end
data/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # promise_pool [![Build Status](https://secure.travis-ci.org/godfat/promise_pool.png?branch=master)](http://travis-ci.org/godfat/promise_pool) [![Coverage Status](https://coveralls.io/repos/godfat/promise_pool/badge.png)](https://coveralls.io/r/godfat/promise_pool) [![Join the chat at https://gitter.im/godfat/promise_pool](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/godfat/promise_pool)
2
+
3
+ by Lin Jen-Shin ([godfat](http://godfat.org))
4
+
5
+ ## LINKS:
6
+
7
+ * [github](https://github.com/godfat/promise_pool)
8
+ * [rubygems](https://rubygems.org/gems/promise_pool)
9
+ * [rdoc](http://rdoc.info/projects/godfat/promise_pool)
10
+
11
+ ## DESCRIPTION:
12
+
13
+ promise_pool
14
+
15
+ ## FEATURES:
16
+
17
+ * promise_pool
18
+
19
+ ## WHY?
20
+
21
+ promise_pool
22
+
23
+ ## REQUIREMENTS:
24
+
25
+ * Tested with MRI (official CRuby), Rubinius and JRuby.
26
+ * gem [timers][]
27
+
28
+ [timers]: https://github.com/celluloid/timers
29
+
30
+ ## INSTALLATION:
31
+
32
+ ``` shell
33
+ gem install promise_pool
34
+ ```
35
+
36
+ Or if you want development version, put this in Gemfile:
37
+
38
+ ``` ruby
39
+ gem 'promise_pool', :git => 'git://github.com/godfat/promise_pool.git',
40
+ :submodules => true
41
+ ```
42
+
43
+ ## CHANGES:
44
+
45
+ * [CHANGES](CHANGES.md)
46
+
47
+ ## CONTRIBUTORS:
48
+
49
+ * Lin Jen-Shin (@godfat)
50
+
51
+ ## LICENSE:
52
+
53
+ Apache License 2.0
54
+
55
+ Copyright (c) 2016, Lin Jen-Shin (godfat)
56
+
57
+ Licensed under the Apache License, Version 2.0 (the "License");
58
+ you may not use this file except in compliance with the License.
59
+ You may obtain a copy of the License at
60
+
61
+ <http://www.apache.org/licenses/LICENSE-2.0>
62
+
63
+ Unless required by applicable law or agreed to in writing, software
64
+ distributed under the License is distributed on an "AS IS" BASIS,
65
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
66
+ See the License for the specific language governing permissions and
67
+ limitations under the License.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+
2
+ begin
3
+ require "#{dir = File.dirname(__FILE__)}/task/gemgem"
4
+ rescue LoadError
5
+ sh 'git submodule update --init'
6
+ exec Gem.ruby, '-S', $PROGRAM_NAME, *ARGV
7
+ end
8
+
9
+ Gemgem.init(dir) do |s|
10
+ require 'promise_pool/version'
11
+ s.name = 'promise_pool'
12
+ s.version = PromisePool::VERSION
13
+ s.add_runtime_dependency('timers', '>=4.0.1')
14
+ end
@@ -0,0 +1,5 @@
1
+
2
+ require 'promise_pool/promise'
3
+ require 'promise_pool/promise_eager'
4
+ require 'promise_pool/thread_pool'
5
+ require 'promise_pool/timer'
@@ -0,0 +1,12 @@
1
+
2
+ module PromisePool
3
+ class Future < BasicObject
4
+ def initialize promise
5
+ @promise = promise
6
+ end
7
+
8
+ def method_missing msg, *args, &block
9
+ @promise.yield.__send__(msg, *args, &block)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,163 @@
1
+
2
+ require 'thread'
3
+ require 'promise_pool/future'
4
+
5
+ module PromisePool
6
+ class Promise
7
+ def self.claim value
8
+ promise = new
9
+ promise.fulfill(value)
10
+ promise
11
+ end
12
+
13
+ def self.backtrace
14
+ Thread.current[:promise_pool_backtrace] || []
15
+ end
16
+
17
+ # should never raise!
18
+ def self.set_backtrace e
19
+ e.set_backtrace((e.backtrace || caller) + backtrace)
20
+ end
21
+
22
+ def initialize timer=nil
23
+ self.value = self.error = self.result = nil
24
+ self.resolved = self.called = false
25
+
26
+ self.k = []
27
+ self.timer = timer
28
+ self.condv = ConditionVariable.new
29
+ self.mutex = Mutex.new
30
+ end
31
+
32
+ # called in client thread
33
+ def defer pool=nil
34
+ backtrace = caller + self.class.backtrace # retain the backtrace so far
35
+ if pool
36
+ mutex.synchronize do
37
+ # still timing it out if the task never processed
38
+ timer.on_timeout{ cancel_task } if timer
39
+ self.task = pool.defer(mutex) do
40
+ Thread.current[:promise_pool_backtrace] = backtrace
41
+ protected_yield{ yield }
42
+ Thread.current[:promise_pool_backtrace] = nil
43
+ end
44
+ end
45
+ else
46
+ self.thread = Thread.new do
47
+ Thread.current[:promise_pool_backtrace] = backtrace
48
+ protected_yield{ yield }
49
+ end
50
+ end
51
+ self
52
+ end
53
+
54
+ def call
55
+ self.thread = Thread.current # set working thread
56
+ protected_yield{ yield } # avoid any exception and do the job
57
+ end
58
+
59
+ def future
60
+ Future.new(self)
61
+ end
62
+
63
+ # called in client thread (client.wait)
64
+ def wait
65
+ # it might be awaken by some other futures!
66
+ mutex.synchronize{ condv.wait(mutex) until resolved? } unless resolved?
67
+ end
68
+
69
+ # called in client thread (from the future (e.g. body))
70
+ def yield
71
+ wait
72
+ mutex.synchronize{ callback }
73
+ end
74
+
75
+ # called in requesting thread after the request is done
76
+ def fulfill value
77
+ mutex.synchronize{ fulfilling(value) }
78
+ end
79
+
80
+ # called in requesting thread if something goes wrong or timed out
81
+ def reject error
82
+ mutex.synchronize{ rejecting(error) }
83
+ end
84
+
85
+ # append your actions, which would be called when we're calling back
86
+ def then &action
87
+ k << action
88
+ self
89
+ end
90
+
91
+ def resolved?
92
+ resolved
93
+ end
94
+
95
+ protected
96
+ attr_accessor :value, :error, :result, :resolved, :called,
97
+ :k, :timer, :condv, :mutex, :task, :thread
98
+
99
+ private
100
+ def fulfilling value
101
+ self.value = value
102
+ resolve
103
+ end
104
+
105
+ def rejecting error
106
+ self.error = error
107
+ resolve
108
+ end
109
+
110
+ def resolve
111
+ self.resolved = true
112
+ yield if block_given?
113
+ ensure
114
+ condv.broadcast # client or response might be waiting
115
+ end
116
+
117
+ # called in a new thread if pool_size == 0, otherwise from the pool
118
+ # i.e. requesting thread
119
+ def protected_yield
120
+ value = if timer
121
+ timeout_protected_yield{ yield }
122
+ else
123
+ yield
124
+ end
125
+ fulfill(value)
126
+ rescue Exception => err
127
+ self.class.set_backtrace(err)
128
+ reject(err)
129
+ end
130
+
131
+ def timeout_protected_yield
132
+ # timeout might already be set for thread_pool (pool_size > 0)
133
+ timer.on_timeout{ cancel_task } unless timer
134
+ yield
135
+ ensure
136
+ timer.cancel
137
+ end
138
+
139
+ # called in client thread, when yield is called
140
+ def callback
141
+ return result if called
142
+ self.result = k.inject(error || value){ |r, i| i.call(r) }
143
+ ensure
144
+ self.called = true
145
+ end
146
+
147
+ # timeout!
148
+ def cancel_task
149
+ mutex.synchronize do
150
+ if resolved?
151
+ # do nothing if it's already done
152
+ elsif t = thread || task.thread
153
+ t.raise(timer.error) # raise Timeout::Error to working thread
154
+ else
155
+ # task was queued and never started, just cancel it and
156
+ # fulfill the promise with Timeout::Error
157
+ task.cancel
158
+ rejecting(timer.error)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,36 @@
1
+
2
+ require 'promise_pool/promise'
3
+
4
+ module PromisePool
5
+ class PromiseEager < Promise
6
+ attr_accessor :error_callback
7
+
8
+ def initialize timer=nil, &error_callback
9
+ super(timer)
10
+ self.error_callback = error_callback
11
+ end
12
+
13
+ def resolved?
14
+ super && called
15
+ end
16
+
17
+ private
18
+ def resolve
19
+ super{ callback } # under ASYNC callback, should call immediately
20
+ rescue Exception => err
21
+ self.class.set_backtrace(err)
22
+ call_error_callback(err)
23
+ end
24
+
25
+ # log user callback error, should never raise
26
+ def call_error_callback err
27
+ if error_callback
28
+ error_callback.call(err)
29
+ else
30
+ warn "#{self.class}: ERROR: #{err}\n from #{err.backtrace.inspect}"
31
+ end
32
+ rescue Exception => e
33
+ Thread.main.raise(e) if !!$DEBUG
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+
2
+ require 'thread'
3
+
4
+ module PromisePool
5
+ class Queue
6
+ def initialize
7
+ @queue = []
8
+ @condv = ConditionVariable.new
9
+ end
10
+
11
+ def size
12
+ @queue.size
13
+ end
14
+
15
+ def << task
16
+ queue << task
17
+ condv.signal
18
+ end
19
+
20
+ def pop mutex, timeout=60
21
+ if queue.empty?
22
+ condv.wait(mutex, timeout)
23
+ queue.shift || lambda{ |_| false } # shutdown idle workers
24
+ else
25
+ queue.shift
26
+ end
27
+ end
28
+
29
+ def clear
30
+ queue.clear
31
+ end
32
+
33
+ protected
34
+ attr_reader :queue, :condv
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+
2
+ module PromisePool
3
+ class Task < Struct.new(:job, :mutex, :thread, :cancelled)
4
+ # this should never fail
5
+ def call working_thread
6
+ mutex.synchronize do
7
+ return if cancelled
8
+ self.thread = working_thread
9
+ end
10
+ job.call
11
+ true
12
+ end
13
+
14
+ def cancel
15
+ self.cancelled = true
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+
2
+ require 'promise_pool'
3
+ require 'pork/auto'
4
+ require 'muack'
5
+
6
+ Pork::Executor.include(Muack::API)
7
+ include PromisePool
@@ -0,0 +1,70 @@
1
+
2
+ # reference implementation: puma
3
+ # https://github.com/puma/puma/blob/v2.7.1/lib/puma/thread_pool.rb
4
+
5
+ require 'thread'
6
+ require 'promise_pool/queue'
7
+ require 'promise_pool/task'
8
+
9
+ module PromisePool
10
+ class ThreadPool
11
+ attr_reader :workers
12
+ attr_accessor :idle_time, :max_size
13
+
14
+ def initialize max_size, idle_time=60
15
+ @max_size = max_size
16
+ @idle_time = idle_time
17
+ @queue = Queue.new
18
+ @mutex = Mutex.new
19
+ @workers = []
20
+ @waiting = 0
21
+ end
22
+
23
+ def size
24
+ workers.size
25
+ end
26
+
27
+ def defer promise_mutex, &job
28
+ mutex.synchronize do
29
+ task = Task.new(job, promise_mutex)
30
+ queue << task
31
+ spawn_worker if waiting < queue.size && workers.size < max_size
32
+ task
33
+ end
34
+ end
35
+
36
+ def trim force=false
37
+ mutex.synchronize do
38
+ queue << lambda{ |_| false } if force || waiting > 0
39
+ end
40
+ end
41
+
42
+ # Block on shutting down, and should not add more jobs while shutting down
43
+ def shutdown
44
+ workers.size.times{ trim(true) }
45
+ workers.first.join && trim(true) until workers.empty?
46
+ mutex.synchronize{ queue.clear }
47
+ end
48
+
49
+ protected
50
+ attr_reader :queue, :mutex, :condv, :waiting
51
+
52
+ private
53
+ def spawn_worker
54
+ workers << Thread.new{
55
+ Thread.current.abort_on_exception = true
56
+
57
+ task = nil
58
+ begin
59
+ mutex.synchronize do
60
+ @waiting += 1
61
+ task = queue.pop(mutex, idle_time)
62
+ @waiting -= 1
63
+ end
64
+ end while task.call(Thread.current)
65
+
66
+ mutex.synchronize{ workers.delete(Thread.current) }
67
+ }
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,60 @@
1
+
2
+ require 'thread'
3
+ require 'timers'
4
+
5
+ module PromisePool
6
+ class Timer
7
+ @mutex = Mutex.new
8
+ @interval = 1
9
+
10
+ singleton_class.module_eval do
11
+ attr_accessor :interval
12
+
13
+ def group
14
+ @group ||= @mutex.synchronize{ @group ||= group_new }
15
+ end
16
+
17
+ private
18
+ def group_new
19
+ g = Timers::Group.new
20
+ g.every(interval){}
21
+ @thread = Thread.new do
22
+ begin
23
+ g.wait
24
+ rescue => e
25
+ warn "#{self.class}: ERROR: #{e}\n from #{e.backtrace.inspect}"
26
+ end while g.count > 1
27
+ @group = nil
28
+ end
29
+ g
30
+ end
31
+ end
32
+
33
+ attr_accessor :timeout, :error, :timer
34
+ def initialize timeout, error, &block
35
+ self.timeout = timeout
36
+ self.error = error
37
+ self.block = block
38
+ start if block_given?
39
+ end
40
+
41
+ def on_timeout &block
42
+ self.block = block
43
+ start if block_given?
44
+ end
45
+
46
+ # should never raise!
47
+ def cancel
48
+ timer.cancel if timer
49
+ self.block = nil
50
+ end
51
+
52
+ def start
53
+ return if timeout.nil? || timeout.zero?
54
+ self.timer = self.class.group.after(timeout){ block.call if block }
55
+ end
56
+
57
+ protected
58
+ attr_accessor :block
59
+ end
60
+ end
@@ -0,0 +1,4 @@
1
+
2
+ module PromisePool
3
+ VERSION = '0.1.0'
4
+ end
@@ -0,0 +1,59 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # stub: promise_pool 0.1.0 ruby lib
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "promise_pool"
6
+ s.version = "0.1.0"
7
+
8
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
+ s.require_paths = ["lib"]
10
+ s.authors = ["Lin Jen-Shin (godfat)"]
11
+ s.date = "2016-01-21"
12
+ s.description = "promise_pool"
13
+ s.email = ["godfat (XD) godfat.org"]
14
+ s.files = [
15
+ ".gitignore",
16
+ ".gitmodules",
17
+ ".travis.yml",
18
+ "Gemfile",
19
+ "README.md",
20
+ "Rakefile",
21
+ "lib/promise_pool.rb",
22
+ "lib/promise_pool/future.rb",
23
+ "lib/promise_pool/promise.rb",
24
+ "lib/promise_pool/promise_eager.rb",
25
+ "lib/promise_pool/queue.rb",
26
+ "lib/promise_pool/task.rb",
27
+ "lib/promise_pool/test.rb",
28
+ "lib/promise_pool/thread_pool.rb",
29
+ "lib/promise_pool/timer.rb",
30
+ "lib/promise_pool/version.rb",
31
+ "promise_pool.gemspec",
32
+ "task/README.md",
33
+ "task/gemgem.rb",
34
+ "test/test_pool.rb",
35
+ "test/test_promise.rb",
36
+ "test/test_promise_eager.rb",
37
+ "test/test_timer.rb"]
38
+ s.homepage = "https://github.com/godfat/promise_pool"
39
+ s.licenses = ["Apache License 2.0"]
40
+ s.rubygems_version = "2.5.1"
41
+ s.summary = "promise_pool"
42
+ s.test_files = [
43
+ "test/test_pool.rb",
44
+ "test/test_promise.rb",
45
+ "test/test_promise_eager.rb",
46
+ "test/test_timer.rb"]
47
+
48
+ if s.respond_to? :specification_version then
49
+ s.specification_version = 4
50
+
51
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
52
+ s.add_runtime_dependency(%q<timers>, [">= 4.0.1"])
53
+ else
54
+ s.add_dependency(%q<timers>, [">= 4.0.1"])
55
+ end
56
+ else
57
+ s.add_dependency(%q<timers>, [">= 4.0.1"])
58
+ end
59
+ end
data/task/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Gemgem
2
+
3
+ ## DESCRIPTION:
4
+
5
+ Provided tasks:
6
+
7
+ rake clean # Remove ignored files
8
+ rake gem:build # Build gem
9
+ rake gem:install # Install gem
10
+ rake gem:release # Release gem
11
+ rake gem:spec # Generate gemspec
12
+ rake test # Run tests in memory
13
+
14
+ ## REQUIREMENTS:
15
+
16
+ * Tested with MRI (official CRuby) 1.9.3, 2.0.0, Rubinius and JRuby.
17
+
18
+ ## INSTALLATION:
19
+
20
+ git submodule add git://github.com/godfat/gemgem.git task
21
+
22
+ And in Rakefile:
23
+
24
+ ``` ruby
25
+ begin
26
+ require "#{dir = File.dirname(__FILE__)}/task/gemgem"
27
+ rescue LoadError
28
+ sh 'git submodule update --init'
29
+ exec Gem.ruby, '-S', $PROGRAM_NAME, *ARGV
30
+ end
31
+
32
+ Gemgem.init(dir) do |s|
33
+ s.name = 'your-gem'
34
+ s.version = '0.1.0'
35
+ end
36
+ ```
37
+
38
+ ## LICENSE:
39
+
40
+ Apache License 2.0
41
+
42
+ Copyright (c) 2011-2013, Lin Jen-Shin (godfat)
43
+
44
+ Licensed under the Apache License, Version 2.0 (the "License");
45
+ you may not use this file except in compliance with the License.
46
+ You may obtain a copy of the License at
47
+
48
+ <http://www.apache.org/licenses/LICENSE-2.0>
49
+
50
+ Unless required by applicable law or agreed to in writing, software
51
+ distributed under the License is distributed on an "AS IS" BASIS,
52
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
53
+ See the License for the specific language governing permissions and
54
+ limitations under the License.
data/task/gemgem.rb ADDED
@@ -0,0 +1,316 @@
1
+
2
+ module Gemgem
3
+ class << self
4
+ attr_accessor :dir, :spec, :spec_create
5
+ end
6
+
7
+ module_function
8
+ def gem_tag ; "#{spec.name}-#{spec.version}" ; end
9
+ def gem_path ; "#{pkg_dir}/#{gem_tag}.gem" ; end
10
+ def spec_path ; "#{dir}/#{spec.name}.gemspec" ; end
11
+ def pkg_dir ; "#{dir}/pkg" ; end
12
+ def escaped_dir; @escaped_dir ||= Regexp.escape(dir); end
13
+
14
+ def init dir, &block
15
+ self.dir = dir
16
+ $LOAD_PATH.unshift("#{dir}/lib")
17
+ ENV['RUBYLIB'] = "#{dir}/lib:#{ENV['RUBYLIB']}"
18
+ ENV['PATH'] = "#{dir}/bin:#{ENV['PATH']}"
19
+ self.spec_create = block
20
+ end
21
+
22
+ def create
23
+ spec = Gem::Specification.new do |s|
24
+ s.authors = ['Lin Jen-Shin (godfat)']
25
+ s.email = ['godfat (XD) godfat.org']
26
+
27
+ s.description = description.join
28
+ s.summary = description.first
29
+ s.license = readme['LICENSE'].sub(/.+\n\n/, '').lines.first.strip
30
+
31
+ s.date = Time.now.strftime('%Y-%m-%d')
32
+ s.files = gem_files
33
+ s.test_files = test_files
34
+ s.executables = bin_files
35
+ end
36
+ spec_create.call(spec)
37
+ spec.homepage ||= "https://github.com/godfat/#{spec.name}"
38
+ self.spec = spec
39
+ end
40
+
41
+ def gem_install
42
+ require 'rubygems/commands/install_command'
43
+ # read ~/.gemrc
44
+ Gem.use_paths(Gem.configuration[:gemhome], Gem.configuration[:gempath])
45
+ Gem::Command.extra_args = Gem.configuration[:gem]
46
+
47
+ # setup install options
48
+ cmd = Gem::Commands::InstallCommand.new
49
+ cmd.handle_options([])
50
+
51
+ # install
52
+ install = Gem::Installer.new(gem_path, cmd.options)
53
+ install.install
54
+ puts "\e[35mGem installed: \e[33m#{strip_path(install.gem_dir)}\e[0m"
55
+ end
56
+
57
+ def gem_spec
58
+ create
59
+ write
60
+ end
61
+
62
+ def gem_build
63
+ require 'fileutils'
64
+ require 'rubygems/package'
65
+ gem = nil
66
+ Dir.chdir(dir) do
67
+ gem = Gem::Package.build(Gem::Specification.load(spec_path))
68
+ FileUtils.mkdir_p(pkg_dir)
69
+ FileUtils.mv(gem, pkg_dir) # gem is relative path, but might be ok
70
+ end
71
+ puts "\e[35mGem built: \e[33m#{strip_path("#{pkg_dir}/#{gem}")}\e[0m"
72
+ end
73
+
74
+ def gem_release
75
+ sh_git('tag', gem_tag)
76
+ sh_git('push')
77
+ sh_git('push', '--tags')
78
+ sh_gem('push', gem_path)
79
+ end
80
+
81
+ def gem_check
82
+ unless git('status', '--porcelain').empty?
83
+ puts("\e[35mWorking copy is not clean.\e[0m")
84
+ exit(3)
85
+ end
86
+
87
+ ver = spec.version.to_s
88
+
89
+ if ENV['VERSION'].nil?
90
+ puts("\e[35mExpected " \
91
+ "\e[33mVERSION\e[35m=\e[33m#{ver}\e[0m")
92
+ exit(1)
93
+
94
+ elsif ENV['VERSION'] != ver
95
+ puts("\e[35mExpected \e[33mVERSION\e[35m=\e[33m#{ver} " \
96
+ "\e[35mbut got\n " \
97
+ "\e[33mVERSION\e[35m=\e[33m#{ENV['VERSION']}\e[0m")
98
+ exit(2)
99
+ end
100
+ end
101
+
102
+ def test
103
+ return if test_files.empty?
104
+
105
+ if ENV['COV'] || ENV['CI']
106
+ require 'simplecov'
107
+ if ENV['CI']
108
+ begin
109
+ require 'coveralls'
110
+ SimpleCov.formatter = Coveralls::SimpleCov::Formatter
111
+ rescue LoadError => e
112
+ puts "Cannot load coveralls, skip: #{e}"
113
+ end
114
+ end
115
+ SimpleCov.start do
116
+ add_filter('test/')
117
+ add_filter('test.rb')
118
+ end
119
+ end
120
+
121
+ test_files.each{ |file| require "#{dir}/#{file[0..-4]}" }
122
+ end
123
+
124
+ def clean
125
+ return if ignored_files.empty?
126
+
127
+ require 'fileutils'
128
+ trash = File.expand_path("~/.Trash/#{spec.name}")
129
+ puts "Move the following files into: \e[35m#{strip_path(trash)}\e[33m"
130
+
131
+ ignored_files.each do |file|
132
+ from = "#{dir}/#{file}"
133
+ to = "#{trash}/#{File.dirname(file)}"
134
+ puts strip_path(from)
135
+
136
+ FileUtils.mkdir_p(to)
137
+ FileUtils.mv(from, to)
138
+ end
139
+
140
+ print "\e[0m"
141
+ end
142
+
143
+ def write
144
+ File.open(spec_path, 'w'){ |f| f << split_lines(spec.to_ruby) }
145
+ end
146
+
147
+ def split_lines ruby
148
+ ruby.gsub(/(.+?)\s*=\s*\[(.+?)\]/){ |s|
149
+ if $2.index(',')
150
+ "#{$1} = [\n #{$2.split(',').map(&:strip).join(",\n ")}]"
151
+ else
152
+ s
153
+ end
154
+ }
155
+ end
156
+
157
+ def strip_path path
158
+ strip_home_path(strip_cwd_path(path))
159
+ end
160
+
161
+ def strip_home_path path
162
+ path.sub(ENV['HOME'], '~')
163
+ end
164
+
165
+ def strip_cwd_path path
166
+ path.sub(Dir.pwd, '.')
167
+ end
168
+
169
+ def git *args
170
+ `git --git-dir=#{dir}/.git #{args.join(' ')}`
171
+ end
172
+
173
+ def sh_git *args
174
+ Rake.sh('git', "--git-dir=#{dir}/.git", *args)
175
+ end
176
+
177
+ def sh_gem *args
178
+ Rake.sh(Gem.ruby, '-S', 'gem', *args)
179
+ end
180
+
181
+ def glob path=dir
182
+ Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
183
+ end
184
+
185
+ def readme
186
+ @readme ||=
187
+ if (path = "#{Gemgem.dir}/README.md") && File.exist?(path)
188
+ ps = "##{File.read(path)}".
189
+ scan(/((#+)[^\n]+\n\n.+?(?=(\n\n\2[^#\n]+\n)|\Z))/m).map(&:first)
190
+ ps.inject('HEADER' => ps.first){ |r, s, i|
191
+ r[s[/\w+/]] = s
192
+ r
193
+ }
194
+ else
195
+ {}
196
+ end
197
+ end
198
+
199
+ def description
200
+ # JRuby String#lines is returning an enumerator
201
+ @description ||= (readme['DESCRIPTION']||'').sub(/.+\n\n/, '').lines.to_a
202
+ end
203
+
204
+ def all_files
205
+ @all_files ||= fold_files(glob).sort
206
+ end
207
+
208
+ def fold_files files
209
+ files.inject([]){ |r, path|
210
+ if File.file?(path) && path !~ %r{/\.git(/|$)} &&
211
+ (rpath = path[%r{^#{escaped_dir}/(.*$)}, 1])
212
+ r << rpath
213
+ elsif File.symlink?(path) # walk into symlinks...
214
+ r.concat(fold_files(glob(File.expand_path(path,
215
+ File.readlink(path)))))
216
+ else
217
+ r
218
+ end
219
+ }
220
+ end
221
+
222
+ def gem_files
223
+ @gem_files ||= all_files.reject{ |f|
224
+ f =~ ignored_pattern && !git_files.include?(f)
225
+ }
226
+ end
227
+
228
+ def test_files
229
+ @test_files ||= gem_files.grep(%r{^test/(.+?/)*test_.+?\.rb$})
230
+ end
231
+
232
+ def bin_files
233
+ @bin_files ||= gem_files.grep(%r{^bin/}).map{ |f| File.basename(f) }
234
+ end
235
+
236
+ def git_files
237
+ @git_files ||= if File.exist?("#{dir}/.git")
238
+ git('ls-files').split("\n")
239
+ else
240
+ []
241
+ end
242
+ end
243
+
244
+ def ignored_files
245
+ @ignored_files ||= all_files.grep(ignored_pattern)
246
+ end
247
+
248
+ def ignored_pattern
249
+ @ignored_pattern ||= if gitignore.empty?
250
+ /^$/
251
+ else
252
+ Regexp.new(expand_patterns(gitignore).join('|'))
253
+ end
254
+ end
255
+
256
+ def expand_patterns pathes
257
+ # http://git-scm.com/docs/gitignore
258
+ pathes.flat_map{ |path|
259
+ # we didn't implement negative pattern for now
260
+ Regexp.escape(path).sub(%r{^/}, '^').gsub(/\\\*/, '[^/]*')
261
+ }
262
+ end
263
+
264
+ def gitignore
265
+ @gitignore ||= if File.exist?(path = "#{dir}/.gitignore")
266
+ File.read(path).lines.
267
+ reject{ |l| l == /^\s*(#|\s+$)/ }.map(&:strip)
268
+ else
269
+ []
270
+ end
271
+ end
272
+ end
273
+
274
+ namespace :gem do
275
+
276
+ desc 'Install gem'
277
+ task :install => [:build] do
278
+ Gemgem.gem_install
279
+ end
280
+
281
+ desc 'Build gem'
282
+ task :build => [:spec] do
283
+ Gemgem.gem_build
284
+ end
285
+
286
+ desc 'Generate gemspec'
287
+ task :spec do
288
+ Gemgem.gem_spec
289
+ end
290
+
291
+ desc 'Release gem'
292
+ task :release => [:spec, :check, :build] do
293
+ Gemgem.gem_release
294
+ end
295
+
296
+ task :check do
297
+ Gemgem.gem_check
298
+ end
299
+
300
+ end # of gem namespace
301
+
302
+ desc 'Run tests'
303
+ task :test do
304
+ Gemgem.test
305
+ end
306
+
307
+ desc 'Trash ignored files'
308
+ task :clean => ['gem:spec'] do
309
+ Gemgem.clean
310
+ end
311
+
312
+ task :default do
313
+ # Is there a reliable way to do this in the current process?
314
+ # It failed miserably before between Rake versions...
315
+ exec "#{Gem.ruby} -S #{$PROGRAM_NAME} -f #{Rake.application.rakefile} -T"
316
+ end
data/test/test_pool.rb ADDED
@@ -0,0 +1,74 @@
1
+
2
+ require 'promise_pool/test'
3
+
4
+ describe PromisePool::ThreadPool do
5
+ before do
6
+ @pool = ThreadPool.new(3)
7
+ @promise = Promise.new
8
+ end
9
+
10
+ after do
11
+ @pool.shutdown
12
+ @pool.size.should.eq 0
13
+ end
14
+
15
+ would 'work, reject, yield' do
16
+ @pool.max_size = 1
17
+ flag = 0
18
+ @promise.defer(@pool) do
19
+ flag.should.eq 0
20
+ flag += 1
21
+ raise 'boom'
22
+ end.yield
23
+ flag.should.eq 1
24
+ @promise.send(:error).message.should.eq 'boom'
25
+ end
26
+
27
+ would 'work, fulfill, yield' do
28
+ value = 'body'
29
+ @pool.max_size = 2
30
+ flag = 0
31
+ @promise.defer(@pool) do
32
+ flag.should.eq 0
33
+ flag += 1
34
+ value
35
+ end
36
+ @promise.future.should.eq value
37
+ @promise.send(:value).should.eq value
38
+ @promise.send(:result).should.eq value
39
+ @promise.should.resolved?
40
+ flag.should.eq 1
41
+ end
42
+
43
+ would 'work, check body', :groups => [:only] do
44
+ flag = 0
45
+ result = @promise.defer(@pool) do
46
+ flag.should.eq 0
47
+ flag += 1
48
+ end.future
49
+ result.should.eq 1
50
+ flag.should.eq 1
51
+ end
52
+
53
+ would 'call in thread pool if pool_size > 0' do
54
+ @pool.max_size = 1
55
+ flag = 0
56
+ rd, wr = IO.pipe
57
+ @promise.defer(@pool) do
58
+ rd.gets
59
+ flag.should.eq 0
60
+ flag += 1
61
+ raise 'nnf'
62
+ end
63
+ p1 = Promise.new
64
+ p1.defer(@pool) do # block until promise #0 is done because max_size == 1
65
+ flag.should.eq 1
66
+ flag += 1
67
+ raise 'boom'
68
+ end
69
+ wr.puts # start promise #0
70
+ @promise.yield
71
+ p1.yield # block until promise #1 is done
72
+ flag.should.eq 2
73
+ end
74
+ end
@@ -0,0 +1,34 @@
1
+
2
+ require 'promise_pool/test'
3
+
4
+ describe PromisePool::Promise do
5
+ would 'claim' do
6
+ value = 'body'
7
+ Promise.claim(value).future.should.eq value
8
+ end
9
+
10
+ would 'then then then' do
11
+ plusone = lambda{ |r| r + 1 }
12
+ promise = Promise.new
13
+ 2.times{ promise.then(&plusone).then(&plusone).then(&plusone) }
14
+ promise.fulfill(0)
15
+ promise.future.should.eq 6
16
+ end
17
+
18
+ after do
19
+ Muack.verify
20
+ end
21
+
22
+ would 'call in a new thread if no pool' do
23
+ thread = nil
24
+ rd, wr = IO.pipe
25
+ mock(Thread).new.with_any_args.peek_return do |t|
26
+ thread = t
27
+ wr.puts
28
+ end
29
+ Promise.new.defer do
30
+ rd.gets
31
+ Thread.current.should.eq thread
32
+ end.yield
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+
2
+ require 'promise_pool/test'
3
+
4
+ describe PromisePool::PromiseEager do
5
+ would 'call error_callback on errors' do
6
+ errors = []
7
+ promise = PromiseEager.new(&errors.method(:<<))
8
+
9
+ promise.then do |err|
10
+ err.message.should.eq 'boom'
11
+ raise 'nnf'
12
+ end
13
+
14
+ promise.defer do
15
+ raise 'boom'
16
+ end.wait
17
+
18
+ errors.map(&:message).should.eq ['nnf']
19
+ end
20
+
21
+ after do
22
+ Muack.verify
23
+ end
24
+
25
+ would 'warn if there is no error_callback' do
26
+ promise = PromiseEager.new
27
+
28
+ mock(promise).warn(is_a(String)) do |msg|
29
+ msg.should.start_with?("PromisePool::PromiseEager: ERROR: nnf\n")
30
+ end
31
+
32
+ promise.then do |value|
33
+ value.should.eq 'value'
34
+ raise 'nnf'
35
+ end
36
+
37
+ promise.defer do
38
+ 'value'
39
+ end.wait
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+
2
+ require 'promise_pool/test'
3
+
4
+ describe PromisePool::Timer do
5
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: promise_pool
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Lin Jen-Shin (godfat)
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: timers
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.0.1
27
+ description: promise_pool
28
+ email:
29
+ - godfat (XD) godfat.org
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gitignore"
35
+ - ".gitmodules"
36
+ - ".travis.yml"
37
+ - Gemfile
38
+ - README.md
39
+ - Rakefile
40
+ - lib/promise_pool.rb
41
+ - lib/promise_pool/future.rb
42
+ - lib/promise_pool/promise.rb
43
+ - lib/promise_pool/promise_eager.rb
44
+ - lib/promise_pool/queue.rb
45
+ - lib/promise_pool/task.rb
46
+ - lib/promise_pool/test.rb
47
+ - lib/promise_pool/thread_pool.rb
48
+ - lib/promise_pool/timer.rb
49
+ - lib/promise_pool/version.rb
50
+ - promise_pool.gemspec
51
+ - task/README.md
52
+ - task/gemgem.rb
53
+ - test/test_pool.rb
54
+ - test/test_promise.rb
55
+ - test/test_promise_eager.rb
56
+ - test/test_timer.rb
57
+ homepage: https://github.com/godfat/promise_pool
58
+ licenses:
59
+ - Apache License 2.0
60
+ metadata: {}
61
+ post_install_message:
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubyforge_project:
77
+ rubygems_version: 2.5.1
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: promise_pool
81
+ test_files:
82
+ - test/test_pool.rb
83
+ - test/test_promise.rb
84
+ - test/test_promise_eager.rb
85
+ - test/test_timer.rb