strand 0.1.0 → 0.2.0.rc0

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.
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/lib/strand/atc.rb CHANGED
@@ -1,4 +1,4 @@
1
- class Strand
1
+ module Strand
2
2
  class Atc #:nodoc:
3
3
 
4
4
  def initialize(options = {})
@@ -0,0 +1,57 @@
1
+ module Strand
2
+ module EM
3
+ # Provides for Strands (Fibers) what Ruby's ConditionVariable provides for Threads.
4
+ class ConditionVariable
5
+
6
+ # Create a new condition variable.
7
+ def initialize
8
+ @waiters = []
9
+ end
10
+
11
+ # Using a mutex for condition variables is meant to protect
12
+ # against race conditions when the signal occurs between testing whether
13
+ # a wait is needed and waiting. This situation will never occur with
14
+ # fibers, but the semantic is retained
15
+ def wait(mutex=nil,timeout = nil)
16
+
17
+ if timeout.nil? && (mutex.nil? || Numeric === mutex)
18
+ timeout = mutex
19
+ mutex = nil
20
+ end
21
+
22
+ # Get the fiber that called us.
23
+ strand = Thread.current
24
+ # Add the fiber to the list of waiters.
25
+ @waiters << strand
26
+ begin
27
+ sleeper = mutex ? mutex : Thread
28
+ sleeper.sleep(timeout)
29
+ ensure
30
+ # Remove from list of waiters.
31
+ @waiters.delete(strand)
32
+ end
33
+ self
34
+ end
35
+
36
+ def signal
37
+ # If there are no waiters, do nothing.
38
+ return self if @waiters.empty?
39
+
40
+ # Find a waiter to wake up.
41
+ waiter = @waiters.shift
42
+
43
+ # Resume it on next tick.
44
+ ::EM.next_tick{ waiter.wakeup }
45
+ self
46
+ end
47
+
48
+ def broadcast
49
+ all_waiting = @waiters.dup
50
+ @waiters.clear
51
+ ::EM.next_tick { all_waiting.each { |w| w.wakeup } }
52
+ self
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,63 @@
1
+ module Strand
2
+ module EM
3
+ class Mutex
4
+
5
+ def initialize()
6
+ @waiters = []
7
+ end
8
+
9
+ def lock()
10
+ strand = Thread.current
11
+ @waiters << strand
12
+ #The lock allows reentry but requires matching unlocks
13
+ strand.send(:yield_sleep) unless @waiters.first == strand
14
+ # Now strand has the lock, make sure it is released if the strand dies
15
+ strand.ensure_hook(self) { release() unless waiters.empty? || waiters.first != strand }
16
+ self
17
+ end
18
+
19
+ def unlock()
20
+ strand = Thread.current
21
+ raise FiberError, "not owner" unless @waiters.first == strand
22
+ release()
23
+ end
24
+
25
+ def locked?
26
+ !@waiters.empty? && @waiters.first.alive?
27
+ end
28
+
29
+ def try_lock
30
+ lock unless locked?
31
+ end
32
+
33
+ def synchronize(&block)
34
+ lock
35
+ yield
36
+ ensure
37
+ unlock
38
+ end
39
+
40
+ def sleep(timeout=nil)
41
+ unlock
42
+ begin
43
+ Thread.sleep(timeout)
44
+ ensure
45
+ lock
46
+ end
47
+ end
48
+
49
+ private
50
+ attr_reader :waiters
51
+
52
+ def release()
53
+ # release the current lock holder, and clear the strand death hook
54
+ waiters.shift.ensure_hook(self)
55
+
56
+ ::EM.next_tick do
57
+ waiters.shift until waiters.empty? || waiters.first.alive?
58
+ waiters.first.send(:wake_resume) unless waiters.empty?
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,84 @@
1
+ module Strand
2
+ module EM
3
+ # A Strand equivalent to ::Queue from thread.rb
4
+ # queue = Strand::Queue.new
5
+ #
6
+ # producer = Strand.new do
7
+ # 5.times do |i|
8
+ # Strand.sleep rand(i) # simulate expense
9
+ # queue << i
10
+ # puts "#{i} produced"
11
+ # end
12
+ # end
13
+ #
14
+ # consumer = Strand.new do
15
+ # 5.times do |i|
16
+ # value = queue.pop
17
+ # Strand.sleep rand(i/2) # simulate expense
18
+ # puts "consumed #{value}"
19
+ # end
20
+ # end
21
+ #
22
+ # consumer.join
23
+ #
24
+ class Queue
25
+
26
+ # Creates a new queue
27
+ def initialize
28
+ @mutex = Mutex.new()
29
+ @cv = ConditionVariable.new()
30
+ @q = []
31
+ @waiting = 0
32
+ end
33
+
34
+ # Pushes +obj+ to the queue
35
+ def push(obj)
36
+ @q << obj
37
+ @mutex.synchronize { @cv.signal }
38
+ end
39
+ alias :<< :push
40
+ alias :enq :push
41
+
42
+ # Retrieves data from the queue.
43
+ #
44
+ #
45
+ # If the queue is empty, the calling fiber is suspended until data is
46
+ # pushed onto the queue, unless +non_block+ is true in which case a
47
+ # +FiberError+ is raised
48
+ #
49
+ def pop(non_block=false)
50
+ raise FiberError, "queue empty" if non_block && empty?
51
+ if empty?
52
+ @waiting += 1
53
+ @mutex.synchronize { @cv.wait(@mutex) if empty? }
54
+ @waiting -= 1
55
+ end
56
+ # array.pop is like a stack, we're a FIFO
57
+ @q.shift
58
+ end
59
+ alias :shift :pop
60
+ alias :deq :pop
61
+
62
+ # Returns the length of the queue
63
+ def length
64
+ @q.length
65
+ end
66
+ alias :size :length
67
+
68
+ # Returns +true+ if the queue is empty
69
+ def empty?
70
+ @q.empty?
71
+ end
72
+
73
+ # Removes all objects from the queue
74
+ def clear
75
+ @q.clear
76
+ end
77
+
78
+ # Returns the number of fibers waiting on the queue
79
+ def num_waiting
80
+ @waiting
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,305 @@
1
+ require "fiber"
2
+ require "eventmachine"
3
+ require 'strand/em/mutex'
4
+ require 'strand/em/condition_variable'
5
+
6
+ module Strand
7
+
8
+ module EM
9
+
10
+ #Acts like a ::Thread using Fibers and EventMachine
11
+ class Thread
12
+
13
+ @@strands = {}
14
+
15
+ # The underlying fiber.
16
+ attr_reader :fiber
17
+
18
+ # Like ::Thread::list. Return an array of all EM::Threads that are alive.
19
+ def self.list
20
+ @@strands.values.select { |s| s.alive? }
21
+ end
22
+
23
+ # Like ::Thread::current. Get the currently running EM::Thread, eg to access thread local
24
+ # variables
25
+ def self.current
26
+ @@strands[Fiber.current] || ProxyThread.new(Fiber.current)
27
+ end
28
+
29
+ # Alias for Fiber::yield
30
+ # Equivalent to a thread being blocked on IO
31
+ #
32
+ # WARNING: Be very careful about using #yield with the other thread like methods
33
+ # Specifically it is important
34
+ # to ensure user calls to #resume don't conflict with the resumes that are setup via
35
+ # EM.timer or EM.next_tick as a result of #::sleep or #::pass
36
+ def self.yield(*args)
37
+ Fiber.yield(*args)
38
+ end
39
+
40
+ # Like ::Kernel::sleep. Woken by an ::EM::Timer in +seconds+ if supplied
41
+ def self.sleep(seconds=nil)
42
+
43
+ raise TypeError, "seconds #{seconds} must be a number" unless seconds.nil? or seconds.is_a? Numeric
44
+ n = Time.now
45
+
46
+ strand = current
47
+ timer = ::EM::Timer.new(seconds){ strand.__send__(:wake_resume) } unless seconds.nil?
48
+ strand.__send__(:yield_sleep,timer)
49
+
50
+ (Time.now - n).round()
51
+ end
52
+
53
+ # Like ::Thread::stop. Sleep forever (until woken)
54
+ def self.stop
55
+ self.sleep()
56
+ end
57
+
58
+ # Like ::Thread::pass.
59
+ # The fiber is resumed on the next_tick of EM's event loop
60
+ def self.pass
61
+ strand = current
62
+ ::EM.next_tick{ strand.__send__(:wake_resume) }
63
+ strand.__send__(:yield_sleep)
64
+ nil
65
+ end
66
+
67
+ # Create and run
68
+ def initialize(*args,&block)
69
+
70
+ # Create our fiber.
71
+ fiber = Fiber.new{ fiber_body(&block) }
72
+
73
+ init(fiber)
74
+
75
+ # Finally start the strand.
76
+ fiber.resume(*args)
77
+ end
78
+
79
+ # Like ::Thread#join.
80
+ # s1 = Strand.new{ Strand.sleep(1) }
81
+ # s2 = Strand.new{ Strand.sleep(1) }
82
+ # s1.join
83
+ # s2.join
84
+ def join(limit = nil)
85
+ @mutex.synchronize { @join_cond.wait(@mutex,limit) } if alive?
86
+ Kernel.raise @exception if @exception
87
+ if alive? then nil else self end
88
+ end
89
+
90
+ # Like Fiber#resume. Refer to warnings on #::yield
91
+ def resume(*args)
92
+ #TODO should only allow if @status is :run, which really means
93
+ # blocked by a call to Yield
94
+ fiber.resume(*args)
95
+ end
96
+
97
+ # Like ::Thread#alive? or Fiber#alive?
98
+ def alive?
99
+ fiber.alive?
100
+ end
101
+
102
+ # Like ::Thread#stop? Always true unless our fiber is the current fiber
103
+ def stop?
104
+ Fiber.current != fiber
105
+ end
106
+
107
+ # Like ::Thread#status
108
+ def status
109
+ case @status
110
+ when :run
111
+ #TODO - if not the current fiber
112
+ # we can only be in this state due to a yield on the
113
+ # underlying fiber, which means we are actually in sleep
114
+ # or we're a ProxyThread that is dead and not yet
115
+ # cleaned up
116
+ "run"
117
+ when :sleep
118
+ "sleep"
119
+ when :dead, :killed
120
+ false
121
+ when :exception
122
+ nil
123
+ end
124
+ end
125
+
126
+ # Like ::Thread#value. Implicitly calls #join.
127
+ # strand = Strand.new{ 1+2 }
128
+ # strand.value # => 3
129
+ def value
130
+ join and @value
131
+ end
132
+
133
+ # Like ::Thread#exit. Signals thread to wakeup and die
134
+ def exit
135
+ case @status
136
+ when :sleep
137
+ wake_resume(:exit)
138
+ when :run
139
+ throw :exit
140
+ end
141
+ end
142
+
143
+ alias :kill :exit
144
+ alias :terminate :exit
145
+
146
+ # Like ::Thread#wakeup Wakes a sleeping Thread
147
+ def wakeup
148
+ Kernel.raise FiberError, "dead strand" unless status
149
+ wake_resume()
150
+ end
151
+
152
+ # Like ::Thread#raise, raise an exception on a sleeping Thread
153
+ def raise(*args)
154
+ if fiber == Fiber.current
155
+ Kernel.raise *args
156
+ elsif status
157
+ args << RuntimeError if args.empty?
158
+ wake_resume(:raise,*args)
159
+ else
160
+ #dead strand, do nothing
161
+ end
162
+ end
163
+
164
+ alias :run :wakeup
165
+
166
+
167
+ # Access to "fiber local" variables, akin to "thread local" variables.
168
+ # Strand.new do
169
+ # ...
170
+ # Strand.current[:connection].send(data)
171
+ # ...
172
+ # end
173
+ def [](name)
174
+ raise TypeError, "name #{name} must convert to_sym" unless name and name.respond_to?(:to_sym)
175
+ @locals[name.to_sym]
176
+ end
177
+
178
+ # Access to "fiber local" variables, akin to "thread local" variables.
179
+ # Strand.new do
180
+ # ...
181
+ # Strand.current[:connection] = SomeConnectionClass.new(host, port)
182
+ # ...
183
+ # end
184
+ def []=(name, value)
185
+ raise TypeError, "name #{name} must convert to_sym" unless name and name.respond_to?(:to_sym)
186
+ @locals[name.to_sym] = value
187
+ end
188
+
189
+ # Like ::Thread#key? Is there a "fiber local" variable defined called +name+
190
+ def key?(name)
191
+ raise TypeError, "name #{name} must convert to_sym" unless name and name.respond_to?(:to_sym)
192
+ @locals.has_key?(name.to_sym)
193
+ end
194
+
195
+ # Like ::Thread#keys The set of "strand local" variable keys
196
+ def keys()
197
+ @locals.keys
198
+ end
199
+
200
+ def inspect #:nodoc:
201
+ "#<Strand::EM::Thread:0x%s %s" % [object_id, @fiber == Fiber.current ? "run" : "yielded"]
202
+ end
203
+
204
+ # Do something when the fiber completes.
205
+ def ensure_hook(key,&block)
206
+ if block_given? then
207
+ @ensure_hooks[key] = block
208
+ else
209
+ @ensure_hooks.delete(key)
210
+ end
211
+ end
212
+
213
+ protected
214
+
215
+ def fiber_body(&block) #:nodoc:
216
+ # Run the strand's block and capture the return value.
217
+ @status = :run
218
+
219
+ @value = nil, @exception = nil
220
+ catch :exit do
221
+ begin
222
+ @value = block.call
223
+ @status = :dead
224
+ rescue Exception => e
225
+ @exception = e
226
+ @status = :exception
227
+ ensure
228
+ run_ensure_hooks()
229
+ end
230
+ end
231
+
232
+ # Delete from the list of running stands.
233
+ @@strands.delete(@fiber)
234
+
235
+ # Resume anyone who called join on us.
236
+ # the synchronize is not really necessary for fibers
237
+ # but does no harm
238
+ @mutex.synchronize { @join_cond.signal() }
239
+
240
+ @value || @exception
241
+ end
242
+
243
+ private
244
+
245
+ def init(fiber)
246
+ @fiber = fiber
247
+ # Add us to the list of living strands.
248
+ @@strands[@fiber] = self
249
+
250
+ # Initialize our "fiber local" storage.
251
+ @locals = {}
252
+
253
+ # Record the status
254
+ @status = nil
255
+
256
+ # Hooks to run when the strand dies (eg by Mutex to release locks)
257
+ @ensure_hooks = {}
258
+
259
+ # Condition variable and mutex for joining.
260
+ @mutex = Mutex.new()
261
+ @join_cond = ConditionVariable.new()
262
+
263
+ end
264
+ def yield_sleep(timer=nil)
265
+ @status = :sleep
266
+ event,*args = Fiber.yield
267
+ timer.cancel if timer
268
+ case event
269
+ when :exit
270
+ @status = :killed
271
+ throw :exit
272
+ when :wake
273
+ @status = :run
274
+ when :raise
275
+ Kernel.raise *args
276
+ end
277
+ end
278
+
279
+ def wake_resume(event = :wake,*args)
280
+ fiber.resume(event,*args) if @status == :sleep
281
+ #TODO if fiber is still alive? and status = :run
282
+ # then it has been yielded from non Strand code.
283
+ # if it is not alive, and is a proxy strand then
284
+ # we can signal the condition variable from here
285
+ end
286
+
287
+ def run_ensure_hooks()
288
+ #TODO - better not throw exceptions in an ensure hook
289
+ @ensure_hooks.each { |key,hook| hook.call }
290
+ end
291
+ end
292
+
293
+ # This class is used if EM::Thread class methods are called on Fibers that were not created
294
+ # with EM::Thread.new()
295
+ class ProxyThread < Thread
296
+
297
+ #TODO start an EM periodic timer to reap dead proxythreads (running ensurehooks)
298
+ #TODO do something sensible for #value, #kill
299
+
300
+ def initialize(fiber)
301
+ init(fiber)
302
+ end
303
+ end
304
+ end
305
+ end