strand 0.1.0 → 0.2.0.rc0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGELOG +13 -0
  3. data/Gemfile +3 -15
  4. data/LICENSE.txt +1 -0
  5. data/README.rdoc +12 -19
  6. data/Rakefile +10 -57
  7. data/lib/strand.rb +151 -126
  8. data/lib/strand/atc.rb +1 -1
  9. data/lib/strand/em/condition_variable.rb +57 -0
  10. data/lib/strand/em/mutex.rb +63 -0
  11. data/lib/strand/em/queue.rb +84 -0
  12. data/lib/strand/em/thread.rb +305 -0
  13. data/lib/strand/monitor.rb +193 -0
  14. data/lib/strand/version.rb +3 -0
  15. data/spec/spec_helper.rb +9 -5
  16. data/spec/strand/alive.rb +62 -0
  17. data/spec/strand/condition_variable.rb +10 -0
  18. data/spec/strand/condition_variable/broadcast.rb +61 -0
  19. data/spec/strand/condition_variable/signal.rb +62 -0
  20. data/spec/strand/condition_variable/wait.rb +20 -0
  21. data/spec/strand/current.rb +15 -0
  22. data/spec/strand/exit.rb +148 -0
  23. data/spec/strand/join.rb +60 -0
  24. data/spec/strand/local_storage.rb +98 -0
  25. data/spec/strand/mutex.rb +244 -0
  26. data/spec/strand/pass.rb +9 -0
  27. data/spec/strand/queue.rb +124 -0
  28. data/spec/strand/raise.rb +142 -0
  29. data/spec/strand/run.rb +5 -0
  30. data/spec/strand/shared.rb +14 -0
  31. data/spec/strand/sleep.rb +51 -0
  32. data/spec/strand/status.rb +44 -0
  33. data/spec/strand/stop.rb +58 -0
  34. data/spec/strand/strand.rb +32 -0
  35. data/spec/strand/value.rb +39 -0
  36. data/spec/strand/wakeup.rb +60 -0
  37. data/spec/strand_spec.rb +51 -0
  38. data/spec/support/fixtures.rb +305 -0
  39. data/spec/support/scratch.rb +17 -0
  40. data/spec/thread_spec.rb +20 -0
  41. data/strand.gemspec +23 -0
  42. metadata +72 -58
  43. data/Gemfile.lock +0 -40
  44. data/lib/strand/condition_variable.rb +0 -78
  45. data/spec/condition_variable_spec.rb +0 -82
  46. data/test/helper.rb +0 -30
  47. data/test/test_strand.rb +0 -121
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ html
3
+ pkg/
data/CHANGELOG ADDED
@@ -0,0 +1,13 @@
1
+ == 0.2.0
2
+ New features
3
+ * Strand now implements many more ::Thread methods and equivalents for ::Queue and ::Mutex
4
+ * Strand is now a module that behaves like a class. Strand.new will return a ::Thread or a Strand::EM::Thread
5
+ depending on whether it is running within the EventMachine reactor or not
6
+
7
+ Changes
8
+ * EventMachine is now an optional dependency (because it can work without EM)
9
+ * Strand.yield and Strand.resume can now take arguments (like Fiber)
10
+ * Final call to Strand.resume will return strand's value (or exception)
11
+ * Normalize access to strand local variables (converts everything to symbols interally)
12
+
13
+ == 0.1.0 - Initial version
data/Gemfile CHANGED
@@ -1,16 +1,4 @@
1
- source "http://rubygems.org"
2
- # Add dependencies required to use your gem here.
3
- # Example:
4
- # gem "activesupport", ">= 2.3.5"
1
+ source 'https://rubygems.org'
5
2
 
6
- # Add dependencies to develop your gem here.
7
- # Include everything needed to run rake, tests, features, etc.
8
- group :development do
9
- gem "bundler", "~> 1.0.0"
10
- gem "em-spec"
11
- gem "eventmachine", "~> 0.12.0"
12
- gem "jeweler", "~> 1.6.4"
13
- gem "rcov", ">= 0"
14
- gem "rr"
15
- gem "rspec"
16
- end
3
+ # Specify your gem's dependencies in strand.gemspec
4
+ gemspec
data/LICENSE.txt CHANGED
@@ -1,4 +1,5 @@
1
1
  Copyright (c) 2011 Christopher J. Bottaro
2
+ Copyright (c) 2012 Grant Gardner
2
3
 
3
4
  Permission is hereby granted, free of charge, to any person obtaining
4
5
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -1,12 +1,12 @@
1
1
  = strand
2
2
 
3
- Strand is a class that wraps Fibers and gives them Thread-like behavior by using EventMachine.
3
+ Strand is a module that provides Thread-like behavior for Fibers in EventMachine.
4
4
 
5
- Strand::ConditionVariable provides functionality for Fibers similar to Ruby's built in ConditionVariable for Threads.
5
+ http://rubygems.org/gems/strand
6
6
 
7
7
  == Like Threads
8
8
 
9
- Strand has an interface similar to Thread.
9
+ Strand has an interface identical to Thread wherever possible. The specs for Strand are based on the ruby-spec for Thread
10
10
 
11
11
  thread = Thread.new{ 1+2 }
12
12
  thread.join
@@ -18,15 +18,7 @@ Strand has an interface similar to Thread.
18
18
 
19
19
  == Fiber/EventMachine aware sleep
20
20
 
21
- Calling sleep usually results in EventMachine's reactor being blocked, but Strand defines an EM+Fiber safe sleep.
22
-
23
- Consider mimicking the following threaded code:
24
-
25
- t1 = Thread.new{ sleep(1) }
26
- t2 = Thread.new{ sleep(1) }
27
- t1.join
28
- t2.join
29
- # Roughly 1 second will have passed.
21
+ Calling ruby's Kernel.sleep will block the EventMachine reactor, but Strand defines an EM+Fiber safe sleep.
30
22
 
31
23
  Assuming that EventMachine is running:
32
24
 
@@ -34,8 +26,11 @@ Assuming that EventMachine is running:
34
26
  s2 = Strand.new{ Strand.sleep(1) }
35
27
  s1.join
36
28
  s2.join
29
+
37
30
  # Roughly 1 second will have passed.
38
31
 
32
+ When run outside of the EventMachine reactor, Strand delegates to ruby's native ::Thread so the above code would still work as expected.
33
+
39
34
  == Thread local, Fiber local and Strand local variables
40
35
 
41
36
  Thread local storage is the same as Fiber local storage in Ruby.
@@ -152,10 +147,6 @@ The output is:
152
147
  3
153
148
  3
154
149
 
155
- == RDoc
156
-
157
- {\http://doc.stochasticbytes.com/strand/index.html}[http://doc.stochasticbytes.com/strand/index.html]
158
-
159
150
  == Contributing to strand
160
151
 
161
152
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
@@ -163,11 +154,13 @@ The output is:
163
154
  * Fork the project
164
155
  * Start a feature/bugfix branch
165
156
  * Commit and push until you are happy with your contribution
166
- * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
157
+ * Make sure to add specs, preferably based on ruby-spec
167
158
  * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
168
159
 
169
160
  == Copyright
170
161
 
171
- Copyright (c) 2011 Christopher J. Bottaro. See LICENSE.txt for
172
- further details.
162
+ Copyright (c) 2011 Christopher J. Bottaro.
163
+ Copyright (c) 2012 Grant Gardner.
164
+
165
+ See LICENSE.txt for further details.
173
166
 
data/Rakefile CHANGED
@@ -1,64 +1,17 @@
1
- # encoding: utf-8
2
-
3
- require 'rubygems'
4
- require 'bundler'
5
- begin
6
- Bundler.setup(:default, :development)
7
- rescue Bundler::BundlerError => e
8
- $stderr.puts e.message
9
- $stderr.puts "Run `bundle install` to install missing gems"
10
- exit e.status_code
11
- end
12
- require 'rake'
13
-
14
- require 'jeweler'
15
- Jeweler::Tasks.new do |gem|
16
- # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
- gem.name = "strand"
18
- gem.homepage = "http://github.com/cjbottaro/strand"
19
- gem.license = "MIT"
20
- gem.summary = %Q{Make fibers behave like threads using EventMachine.}
21
- gem.description = %Q{Get thread-like behavior from fibers using EventMachine.}
22
- gem.email = "cjbottaro@alumni.cs.utexas.edu"
23
- gem.authors = ["Christopher J. Bottaro"]
24
- # dependencies defined in Gemfile
25
- end
26
- Jeweler::RubygemsDotOrgTasks.new
27
-
28
- require 'rake/testtask'
29
- Rake::TestTask.new(:test) do |test|
30
- test.libs << 'lib' << 'test'
31
- test.pattern = 'test/**/test_*.rb'
32
- test.verbose = true
33
- end
34
-
35
- require 'rcov/rcovtask'
36
- Rcov::RcovTask.new do |test|
37
- test.libs << 'test'
38
- test.pattern = 'test/**/test_*.rb'
39
- test.verbose = true
40
- test.rcov_opts << '--exclude "gems/*"'
41
- end
1
+ require "bundler/gem_tasks"
42
2
 
43
3
  require 'rspec/core'
44
4
  require 'rspec/core/rake_task'
45
- RSpec::Core::RakeTask.new(:spec) do |spec|
46
- spec.pattern = FileList['spec/**/*_spec.rb']
47
- end
5
+ require 'rdoc/task'
6
+ require 'rake/clean'
48
7
 
49
- RSpec::Core::RakeTask.new(:rcov) do |spec|
50
- spec.pattern = 'spec/**/*_spec.rb'
51
- spec.rcov = true
52
- end
8
+ RSpec::Core::RakeTask.new(:spec)
53
9
 
54
- task :default => :test
10
+ RDoc::Task.new do |rdoc|
11
+ rdoc.main = "README.rdoc"
12
+ rdoc.rdoc_files.include("README.rdoc", "CHANGELOG","lib/**/*.rb")
13
+ rdoc.title = "Strand"
14
+ end
55
15
 
56
- require 'rake/rdoctask'
57
- Rake::RDocTask.new do |rdoc|
58
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
16
+ CLOBBER.include [ "pkg/" ]
59
17
 
60
- rdoc.rdoc_dir = 'rdoc'
61
- rdoc.title = "strand #{version}"
62
- rdoc.rdoc_files.include('README*')
63
- rdoc.rdoc_files.include('lib/**/*.rb')
64
- end
data/lib/strand.rb CHANGED
@@ -1,132 +1,157 @@
1
- require "fiber"
2
- require "eventmachine"
3
- require "strand/condition_variable"
4
-
5
- class Strand
6
-
7
- @@strands = {}
8
-
9
- # The strand's underlying fiber.
10
- attr_reader :fiber
11
-
12
- # Return an array of all Strands that are alive.
13
- def self.list
14
- @@strands.values
15
- end
16
-
17
- # Get the currently running strand. Primarily used to access "strand local" variables.
18
- def self.current
19
- @@strands[Fiber.current]
20
- end
21
-
22
- # EM/fiber safe sleep.
23
- def self.sleep(seconds)
24
- fiber = Fiber.current
25
- EM::Timer.new(seconds){ fiber.resume }
26
- Fiber.yield
27
- end
28
-
29
- # Alias for Fiber.yield.
30
- def self.yield
31
- Fiber.yield
32
- end
33
-
34
- # Yield the strand, but have EM resume it on the next tick.
35
- def self.pass
36
- fiber = Fiber.current
37
- EM.next_tick{ fiber.resume }
38
- Fiber.yield
39
- end
40
-
41
- # Create and run a strand.
42
- def initialize(&block)
43
-
44
- # Initialize our "fiber local" storage.
45
- @locals = {}
46
-
47
- # Condition variable for joining.
48
- @join_cond = ConditionVariable.new
49
-
50
- # Create our fiber.
51
- @fiber = Fiber.new{ fiber_body(&block) }
52
-
53
- # Add us to the list of living strands.
54
- @@strands[@fiber] = self
55
-
56
- # Finally start the strand.
57
- resume
58
- end
59
-
60
- # Like Thread#join.
61
- # s1 = Strand.new{ Strand.sleep(1) }
62
- # s2 = Strand.new{ Strand.sleep(1) }
63
- # s1.join
64
- # s2.join
65
- def join
66
- @join_cond.wait if alive?
67
- raise @exception if @exception
68
- true
69
- end
70
-
71
- # Like Fiber#resume.
72
- def resume
73
- @fiber.resume
74
- end
75
-
76
- # Like Thread#alive? or Fiber#alive?
77
- def alive?
78
- @fiber.alive?
79
- end
80
-
81
- # Like Thread#value. Implicitly calls #join.
82
- # strand = Strand.new{ 1+2 }
83
- # strand.value # => 3
84
- def value
85
- join and @value
86
- end
87
-
88
- # Access to "strand local" variables, akin to "thread local" variables.
89
- # Strand.new do
90
- # ...
91
- # Strand.current[:connection].send(data)
92
- # ...
93
- # end
94
- def [](name)
95
- @locals[name]
96
- end
97
-
98
- # Access to "strand local" variables, akin to "thread local" variables.
99
- # Strand.new do
100
- # ...
101
- # Strand.current[:connection] = SomeConnectionClass.new(host, port)
102
- # ...
103
- # end
104
- def []=(name, value)
105
- @locals[name] = value
106
- end
107
-
108
- def inspect #:nodoc:
109
- "#<Strand:0x%s %s" % [object_id, @fiber == Fiber.current ? "run" : "yielded"]
110
- end
111
-
112
- protected
113
-
114
- def fiber_body(&block) #:nodoc:
115
- # Run the strand's block and capture the return value.
116
- begin
117
- @value = block.call
118
- rescue StandardError => e
119
- @exception = e
1
+ require 'strand/version'
2
+ require 'thread'
3
+ require 'fiber'
4
+
5
+ # This module provides a shim between using standard ruby Threads
6
+ # and the thread-like behaviour for Fibers provided by classes in
7
+ # the Strand::EM module
8
+ #
9
+ # # For the Strand::EM classes to be available
10
+ # # you must first load EventMachine
11
+ # 'require eventmachine'
12
+ #
13
+ # 'require strand'
14
+ #
15
+ # t = Strand.new() do
16
+ # # "t" is a standard ::Thread
17
+ # ...something...
18
+ # end
19
+ #
20
+ # EventMachine.run do
21
+ # t = Strand.new() do
22
+ # # "t" is a ::Strand::EM::Thread
23
+ # # which wraps a ::Fiber
24
+ # end
25
+ # end
26
+ #
27
+ # # Outside of event machine
28
+ # t = Strand.new() do
29
+ # # "t" is a raw ::Thread
30
+ # end
31
+ #
32
+ # Code using Strand that may be used in both Fiber or Thread contexts
33
+ # should take care to rescue both {FiberError} and {ThreadError} in any
34
+ # exception handling code
35
+ #
36
+ # {::Thread} methods not implemented by Strand
37
+ # * .main - although there is a root fiber it is not really equivalent to {::Thread.main}
38
+ # * .start - not implemented
39
+ # * #exclusive - not implemented
40
+ # * #critical - not implemented
41
+ # * #set_trace_func - not implemented
42
+ # * #safe_level - not implemented
43
+ # * #priority - not implemented
44
+ module Strand
45
+
46
+ # Test whether we have real fibers or a thread based fiber implmentation
47
+ t = Thread.current
48
+ ft = nil
49
+ Fiber.new { ft = Thread.current }.resume
50
+
51
+ ROOT_FIBER = Fiber.current
52
+ REAL_FIBERS = ( t == ft )
53
+
54
+ # Specifically try to enable use of Eventmachine if it is now available
55
+ def self.reload()
56
+ @em_class_map = if defined?(EventMachine) then enable_eventmachine() else nil end
120
57
  end
121
58
 
122
- # Mark the strand as finished running.
123
- @finished = true
59
+ # Private
60
+ def self.enable_eventmachine
61
+ return false unless defined?(EventMachine)
62
+
63
+ require 'strand/em/thread.rb'
64
+ require 'strand/em/queue.rb'
65
+
66
+ # Classes if eventmachine has been previously loaded
67
+ {
68
+ ::Thread => Strand::EM::Thread,
69
+ ::Kernel => Strand::EM::Thread,
70
+ ::Mutex => Strand::EM::Mutex,
71
+ ::ConditionVariable => Strand::EM::ConditionVariable,
72
+ ::Queue => Strand::EM::Queue
73
+ }
74
+ end
75
+
76
+ # If EM already required then enable it, otherwise defer until first use
77
+ reload()
78
+
79
+ # Are we running in the EventMachine reactor thread
80
+ #
81
+ # For JRuby or other interpreters where fibers are implemented with threads
82
+ # this will return true if the reactor is running and the code is called from
83
+ # *any* fiber other than the root fiber
84
+ def self.event_machine?
85
+ @em_class_map = enable_eventmachine() if @em_class_map.nil?
86
+
87
+ @em_class_map && EventMachine.reactor_running? &&
88
+ ( EventMachine.reactor_thread? || (!REAL_FIBERS && ROOT_FIBER != Fiber.current))
89
+ end
90
+
91
+ def self.delegate_class(class_key)
92
+ if self.event_machine? then @em_class_map[class_key] else class_key end
93
+ end
94
+
95
+ # ::Thread::list or EM::Thread::list
96
+ def self.list
97
+ delegate_class(::Thread).list()
98
+ end
99
+
100
+ # ::Thread::current or EM::Thread::current
101
+ def self.current
102
+ delegate_class(::Thread).current()
103
+ end
104
+
105
+ # ::Kernel::sleep or EM::Thread::sleep
106
+ # Note that passing nil will sleep forever (where Kernel.sleep raises TypeError)
107
+ def self.sleep(*args)
108
+ # Kernel.sleep treats nil as an error, but we use it to indicate sleeping forever
109
+ args.shift if args.length == 1 and args.first.nil?
110
+ delegate_class(::Kernel).sleep(*args)
111
+ end
112
+
113
+ # ::Thread::stop or EM::Thread::stop
114
+ def self.stop()
115
+ delegate_class(::Thread).stop()
116
+ end
117
+
118
+ # ::Thread::pass or EM::Thread::pass
119
+ def self.pass()
120
+ delegate_class(::Thread).pass()
121
+ end
122
+
123
+ #TODO Is there some equivalence between the root Fiber and Thread.main?
124
+
125
+ # Convenience to call Fiber.yield. Note the warning on EM::Thread::yield
126
+ def self.yield(*args)
127
+ Fiber.yield(*args)
128
+ end
129
+
130
+ # Behave like a class returning
131
+ # an instance of ::Thread, or ::Strand::EM::Thread based on
132
+ # whether the caller is within the EventMachine reactor
133
+ def self.new(*args,&block)
134
+ delegate_class(::Thread).new(*args,&block)
135
+ end
124
136
 
125
- # Delete from the list of running stands.
126
- @@strands.delete(@fiber)
137
+ module Mutex
138
+ # Return a new ::Mutex or EM::Mutex
139
+ def self.new(*args,&block)
140
+ Strand.delegate_class(::Mutex).new(*args,&block)
141
+ end
142
+ end
127
143
 
128
- # Resume anyone who called join on us.
129
- @join_cond.signal
130
- end
144
+ module ConditionVariable
145
+ # Return a new ::ConditionVariable or EM::ConditionVariable
146
+ def self.new(*args,&block)
147
+ Strand.delegate_class(::ConditionVariable).new(*args,&block)
148
+ end
149
+ end
131
150
 
151
+ module Queue
152
+ # Return a new ::Queue or EM::Queue
153
+ def self.new(*args,&block)
154
+ Strand.delegate_class(::Queue).new(*args,&block)
155
+ end
156
+ end
132
157
  end