Skip to content

Commit f72141e

Browse files
authored
Merge pull request #791 from ruby-concurrency/ruby-association
Ruby association project
2 parents 2307958 + 3ff1cb0 commit f72141e

File tree

151 files changed

+37307
-12809
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

151 files changed

+37307
-12809
lines changed

.rspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
-I lib-edge
12
--require spec_helper
23
--color
34
--warnings

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
## Current
22

3+
## Release v1.1.5, edge v0.5.0 (10 mar 2019)
4+
5+
concurrent-ruby:
6+
7+
* fix potential leak of context on JRuby and Java 7
8+
9+
concurrent-ruby-edge:
10+
11+
* Add finalized Concurrent::Cancellation
12+
* Add finalized Concurrent::Throttle
13+
* Add finalized Concurrent::Promises::Channel
14+
* Add new Concurrent::ErlangActor
15+
316
## Release v1.1.4 (14 Dec 2018)
417

518
* (#780) Remove java_alias of 'submit' method of Runnable to let executor service work on java 11

Gemfile

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
source 'https://rubygems.org'
22

3-
require File.join(File.dirname(__FILE__ ), 'lib/concurrent/version')
3+
require File.join(File.dirname(__FILE__), 'lib/concurrent/version')
4+
require File.join(File.dirname(__FILE__ ), 'lib-edge/concurrent/edge/version')
45

56
no_path = ENV['NO_PATH']
67
options = no_path ? {} : { path: '.' }
@@ -11,26 +12,27 @@ gem 'concurrent-ruby-ext', Concurrent::VERSION, options.merge(platform: :mri)
1112

1213
group :development do
1314
gem 'rake', '~> 12.0'
14-
gem 'rake-compiler', '~> 1.0'
15+
gem 'rake-compiler', '~> 1.0', '>= 1.0.7'
1516
gem 'rake-compiler-dock', '~> 0.7.0'
1617
gem 'pry', '~> 0.11', platforms: :mri
1718
end
1819

1920
group :documentation, optional: true do
20-
gem 'yard', '~> 0.9.0', :require => false
21+
gem 'yard', '~> 0.9.0', require: false
2122
gem 'redcarpet', '~> 3.0', platforms: :mri # understands github markdown
22-
gem 'md-ruby-eval', '~> 0.3'
23+
gem 'md-ruby-eval', '~> 0.6'
2324
end
2425

2526
group :testing do
2627
gem 'rspec', '~> 3.7'
2728
gem 'timecop', '~> 0.7.4'
29+
gem 'sigdump', require: false
2830
end
2931

3032
# made opt-in since it will not install on jruby 1.7
3133
group :coverage, optional: !ENV['COVERAGE'] do
32-
gem 'simplecov', '~> 0.10.0', :require => false
33-
gem 'coveralls', '~> 0.8.2', :require => false
34+
gem 'simplecov', '~> 0.16.0', require: false
35+
gem 'coveralls', '~> 0.8.2', require: false
3436
end
3537

3638
group :benchmarks, optional: true do

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,34 @@ be obeyed though. Features developed in `concurrent-ruby-edge` are expected to m
224224
*Status: will be moved to core soon.*
225225
* [LockFreeStack](http://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/LockFreeStack.html)
226226
*Status: missing documentation and tests.*
227+
* [Promises::Channel](http://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Promises/Channel.html)
228+
A first in first out channel that accepts messages with push family of methods and returns
229+
messages with pop family of methods.
230+
Pop and push operations can be represented as futures, see `#pop_op` and `#push_op`.
231+
The capacity of the channel can be limited to support back pressure, use capacity option in `#initialize`.
232+
`#pop` method blocks ans `#pop_op` returns pending future if there is no message in the channel.
233+
If the capacity is limited the `#push` method blocks and `#push_op` returns pending future.
234+
* [Cancellation](http://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Cancellation.html)
235+
The Cancellation abstraction provides cooperative cancellation.
236+
237+
The standard methods `Thread#raise` of `Thread#kill` available in Ruby
238+
are very dangerous (see linked the blog posts bellow).
239+
Therefore concurrent-ruby provides an alternative.
240+
241+
* <https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/>
242+
* <http://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/>
243+
* <http://blog.headius.com/2008/02/rubys-threadraise-threadkill-timeoutrb.html>
244+
245+
It provides an object which represents a task which can be executed,
246+
the task has to get the reference to the object and periodically cooperatively check that it is not cancelled.
247+
Good practices to make tasks cancellable:
248+
* check cancellation every cycle of a loop which does significant work,
249+
* do all blocking actions in a loop with a timeout then on timeout check cancellation
250+
and if ok block again with the timeout
251+
* [Throttle](http://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Throttle.html)
252+
A tool managing concurrency level of tasks.
253+
* [ErlangActor](http://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ErlangActor.html)
254+
Actor implementation which matches Erlang actor behaviour.
227255

228256
## Supported Ruby versions
229257

@@ -339,6 +367,9 @@ and to the past maintainers
339367
* [Paweł Obrok](https://github.com/obrok)
340368
* [Lucas Allan](https://github.com/lucasallan)
341369

370+
and to [Ruby Association](https://www.ruby.or.jp/en/) for sponsoring a project
371+
["Enhancing Ruby’s concurrency tooling"](https://www.ruby.or.jp/en/news/20181106) in 2018.
372+
342373
## License and Copyright
343374

344375
*Concurrent Ruby* is free software released under the

Rakefile

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -16,43 +16,7 @@ edge_gemspec = Gem::Specification.load File.join(__dir__, 'concurrent-ruby-edge.
1616

1717
require 'rake/javaextensiontask'
1818

19-
class ConcurrentRubyJavaExtensionTask < Rake::JavaExtensionTask
20-
def java_classpath_arg(*args)
21-
jruby_cpath = nil
22-
23-
if RUBY_PLATFORM =~ /java/
24-
begin
25-
cpath = Java::java.lang.System.getProperty('java.class.path').split(File::PATH_SEPARATOR)
26-
cpath += Java::java.lang.System.getProperty('sun.boot.class.path').split(File::PATH_SEPARATOR)
27-
jruby_cpath = cpath.compact.join(File::PATH_SEPARATOR)
28-
rescue => e
29-
end
30-
31-
unless jruby_cpath
32-
libdir = RbConfig::CONFIG['libdir']
33-
if libdir.start_with? "classpath:"
34-
raise 'Cannot build with jruby-complete'
35-
end
36-
jruby_cpath = File.join(libdir, "jruby.jar")
37-
end
38-
end
39-
40-
unless jruby_cpath
41-
jruby_home = ENV['JRUBY_HOME']
42-
if jruby_home
43-
candidate = File.join(jruby_home, 'lib', 'jruby.jar')
44-
jruby_cpath = candidate if File.exist? candidate
45-
end
46-
end
47-
48-
raise "jruby.jar path not found" unless jruby_cpath
49-
50-
jruby_cpath += File::PATH_SEPARATOR + args.join(File::PATH_SEPARATOR) unless args.empty?
51-
jruby_cpath ? "-cp \"#{jruby_cpath}\"" : ""
52-
end
53-
end
54-
55-
ConcurrentRubyJavaExtensionTask.new('concurrent_ruby', core_gemspec) do |ext|
19+
Rake::JavaExtensionTask.new('concurrent_ruby', core_gemspec) do |ext|
5620
ext.ext_dir = 'ext/concurrent-ruby'
5721
ext.lib_dir = 'lib/concurrent'
5822
end
@@ -79,7 +43,8 @@ namespace :repackage do
7943
sh 'bundle package'
8044

8145
# build only the jar file not the whole gem for java platform, the jar is part the concurrent-ruby-x.y.z.gem
82-
RakeCompilerDock.sh 'bundle install --local && bundle exec rake lib/concurrent/concurrent_ruby.jar --trace', rubyvm: :jruby
46+
Rake::Task['lib/concurrent/concurrent_ruby.jar'].invoke
47+
8348
# build all gem files
8449
RakeCompilerDock.sh 'bundle install --local && bundle exec rake cross native package --trace'
8550
end
@@ -101,15 +66,14 @@ begin
10166

10267
RSpec::Core::RakeTask.new(:spec)
10368

104-
options = %w[ --color
105-
--backtrace
106-
--seed 1
107-
--format documentation
108-
--tag ~notravis ]
109-
11069
namespace :spec do
11170
desc '* Configured for ci'
11271
RSpec::Core::RakeTask.new(:ci) do |t|
72+
options = %w[ --color
73+
--backtrace
74+
--order defined
75+
--format documentation
76+
--tag ~notravis ]
11377
t.rspec_opts = [*options].join(' ')
11478
end
11579

@@ -184,10 +148,19 @@ begin
184148
end
185149

186150
define_yard_task = -> name do
151+
output_dir = "docs/#{name}"
152+
153+
removal_name = "remove.#{name}"
154+
task removal_name do
155+
Dir.chdir __dir__ do
156+
FileUtils.rm_rf output_dir
157+
end
158+
end
159+
187160
desc "* of #{name} into subdir #{name}"
188161
YARD::Rake::YardocTask.new(name) do |yard|
189162
yard.options.push(
190-
'--output-dir', "docs/#{name}",
163+
'--output-dir', output_dir,
191164
'--main', 'tmp/README.md',
192165
*common_yard_options)
193166
yard.files = ['./lib/**/*.rb',
@@ -196,10 +169,11 @@ begin
196169
'-',
197170
'docs-source/thread_pools.md',
198171
'docs-source/promises.out.md',
172+
'docs-source/medium-example.out.rb',
199173
'LICENSE.md',
200174
'CHANGELOG.md']
201175
end
202-
Rake::Task[name].prerequisites.push 'yard:eval_md', 'yard:update_readme'
176+
Rake::Task[name].prerequisites.push removal_name, 'yard:eval_md', 'yard:update_readme'
203177
end
204178

205179
define_yard_task.call current_yard_version_name

concurrent-ruby-edge.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
require File.join(File.dirname(__FILE__ ), 'lib/concurrent/version')
1+
require File.join(File.dirname(__FILE__ ), 'lib-edge/concurrent/edge/version')
22

33
Gem::Specification.new do |s|
44
git_files = `git ls-files`.split("\n")

docs-source/cancellation.in.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
2+
## Examples
3+
4+
**Run async task until cancelled**
5+
6+
Create cancellation and then run work in a background thread until it is cancelled.
7+
8+
```ruby
9+
cancellation, origin = Concurrent::Cancellation.new
10+
# - origin is used for cancelling, resolve it to cancel
11+
# - cancellation is passed down to tasks for cooperative cancellation
12+
async_task = Concurrent::Promises.future(cancellation) do |cancellation|
13+
# Do work repeatedly until it is cancelled
14+
do_stuff until cancellation.canceled?
15+
:stopped_gracefully
16+
end
17+
18+
sleep 0.01
19+
# Wait a bit then stop the thread by resolving the origin of the cancellation
20+
origin.resolve
21+
async_task.value!
22+
```
23+
24+
Or let it raise an error.
25+
26+
```ruby
27+
cancellation, origin = Concurrent::Cancellation.new
28+
async_task = Concurrent::Promises.future(cancellation) do |cancellation|
29+
# Do work repeatedly until it is cancelled
30+
while true
31+
cancellation.check!
32+
do_stuff
33+
end
34+
end
35+
36+
sleep 0.01
37+
# Wait a bit then stop the thread by resolving the origin of the cancellation
38+
origin.resolve
39+
async_task.result
40+
```
41+
42+
**Run additional tasks on Cancellation**
43+
44+
Cancellation can also be used to log or plan re-execution.
45+
46+
```ruby
47+
cancellation.origin.chain do
48+
# This block is executed after the Cancellation is cancelled
49+
# It can then log cancellation or e.g. plan new re-execution
50+
end
51+
```
52+
53+
**Run only for limited time – Timeout replacement**
54+
55+
Execute task for a given time then finish.
56+
Instead of letting Cancellation crate its own origin, it can be passed in as argument.
57+
The passed in origin is scheduled to be resolved in given time which then cancels the Cancellation.
58+
59+
```ruby
60+
timeout = Concurrent::Cancellation.new Concurrent::Promises.schedule(0.02)
61+
# or using shortcut helper method
62+
timeout = Concurrent::Cancellation.timeout 0.02
63+
count = Concurrent::AtomicFixnum.new
64+
Concurrent.global_io_executor.post(timeout) do |timeout|
65+
# do stuff until cancelled
66+
count.increment until timeout.canceled?
67+
end #
68+
69+
timeout.origin.wait
70+
count.value # => 177576
71+
```
72+
73+
**Parallel background processing with single cancellation**
74+
75+
Each task tries to count to 1000 but there is a randomly failing test. The
76+
tasks share single cancellation, when one of them fails it cancels the others.
77+
The failing tasks ends with an error, the other tasks are gracefully cancelled.
78+
79+
```ruby
80+
cancellation, origin = Concurrent::Cancellation.new
81+
tasks = 4.times.map do |i|
82+
Concurrent::Promises.future(cancellation, origin, i) do |cancellation, origin, i|
83+
count = 0
84+
100.times do
85+
break count = :cancelled if cancellation.canceled?
86+
count += 1
87+
sleep 0.001
88+
if rand > 0.95
89+
origin.resolve # cancel
90+
raise 'random error'
91+
end
92+
count
93+
end
94+
end
95+
end
96+
Concurrent::Promises.zip(*tasks).result #
97+
# => [false,
98+
# [:cancelled, nil, :cancelled, :cancelled],
99+
# [nil, #<RuntimeError: random error>, nil, nil]]
100+
```
101+
102+
Without the randomly failing part it produces following.
103+
104+
```ruby
105+
cancellation, origin = Concurrent::Cancellation.new
106+
tasks = 4.times.map do |i|
107+
Concurrent::Promises.future(cancellation, origin, i) do |cancellation, origin, i|
108+
count = 0
109+
100.times do
110+
break count = :cancelled if cancellation.canceled?
111+
count += 1
112+
sleep 0.001
113+
# if rand > 0.95
114+
# origin.resolve
115+
# raise 'random error'
116+
# end
117+
count
118+
end
119+
end
120+
end
121+
Concurrent::Promises.zip(*tasks).result
122+
```
123+
124+
**Combine cancellations**
125+
126+
The combination created by joining two cancellations cancels when the first or the other does.
127+
128+
```ruby
129+
cancellation_a, origin_a = Concurrent::Cancellation.new
130+
cancellation_b, origin_b = Concurrent::Cancellation.new
131+
combined_cancellation = cancellation_a.join(cancellation_b)
132+
133+
origin_a.resolve
134+
135+
cancellation_a.canceled?
136+
cancellation_b.canceled?
137+
combined_cancellation.canceled?
138+
```
139+
140+
If a different rule for joining is needed, the source can be combined manually.
141+
The manually created cancellation cancels when both the first and the other cancels.
142+
143+
```ruby
144+
cancellation_a, origin_a = Concurrent::Cancellation.new
145+
cancellation_b, origin_b = Concurrent::Cancellation.new
146+
# cancels only when both a and b is cancelled
147+
combined_cancellation = Concurrent::Cancellation.new origin_a & origin_b
148+
149+
origin_a.resolve
150+
151+
cancellation_a.canceled? #=> true
152+
cancellation_b.canceled? #=> false
153+
combined_cancellation.canceled? #=> false
154+
155+
origin_b.resolve
156+
combined_cancellation.canceled? #=> true
157+
```
158+

docs-source/cancellation.init.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
require 'concurrent-edge'
2+
3+
def do_stuff(*args)
4+
sleep 0.01
5+
:stuff
6+
end

0 commit comments

Comments
 (0)