empathy 0.0.1.RC0 → 0.0.1.RC2
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +5 -0
- data/CHANGES.md +8 -0
- data/README.md +144 -0
- data/Rakefile +4 -7
- data/TESTING.md +27 -0
- data/empathy.gemspec +5 -1
- data/lib/empathy/em/condition_variable.rb +18 -11
- data/lib/empathy/em/monitor.rb +8 -0
- data/lib/empathy/em/mutex.rb +17 -0
- data/lib/empathy/em/queue.rb +10 -30
- data/lib/empathy/em/thread.rb +91 -31
- data/lib/empathy/object.rb +3 -3
- data/lib/empathy/version.rb +1 -1
- data/lib/empathy/with_all_of_ruby.rb +8 -0
- data/lib/empathy.rb +81 -45
- data/lib/monitor.rb +209 -0
- data/mspec/lib/mspec/empathy.rb +1 -1
- data/rubyspec/monitor_spec.rb +162 -0
- data/spec/empathy_spec.rb +7 -2
- data/spec/library_spec.rb +95 -48
- data/yard/extensions.rb +24 -0
- metadata +50 -11
- data/README.rdoc +0 -135
- data/TESTING.rdoc +0 -11
- data/lib/empathy/thread.rb +0 -8
data/.yardopts
ADDED
data/CHANGES.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
Empathy
|
2
|
+
=======================
|
3
|
+
|
4
|
+
http://rubygems.org/gems/empathy
|
5
|
+
|
6
|
+
Make EventMachine behave like standard Ruby
|
7
|
+
|
8
|
+
Empathic Threads
|
9
|
+
------------------------
|
10
|
+
|
11
|
+
{Empathy::EM} uses Fibers to provide Thread, Queue, Mutex, ConditionVariable and MonitorMixin classes that behave like the native ruby ones.
|
12
|
+
|
13
|
+
require 'eventmachine'
|
14
|
+
require 'empathy'
|
15
|
+
|
16
|
+
# start eventmachine and a main EM::Thread
|
17
|
+
Empathy.run do
|
18
|
+
thread = Empathy::EM::Thread.new do
|
19
|
+
my_thread = Empathy::EM::Thread.current
|
20
|
+
|
21
|
+
#local storage
|
22
|
+
Empathy::EM::Thread.current[:my_key] = "some value"
|
23
|
+
|
24
|
+
#pass control elsewhere
|
25
|
+
Empathy::EM::Thread.pass
|
26
|
+
|
27
|
+
Empathy::EM::Kernel.sleep(1)
|
28
|
+
|
29
|
+
1 + 2
|
30
|
+
end
|
31
|
+
|
32
|
+
thread.join
|
33
|
+
thread.value # => 3
|
34
|
+
end
|
35
|
+
|
36
|
+
Almost all Thread behaviour is provided except that one thread will never see another as "running". Where ruby's thread API raises ThreadError, Empathy::EM will raise FiberError (which is also available as {Empathy::EM::ThreadError})
|
37
|
+
|
38
|
+
Empathic code outside of the EventMachine reactor
|
39
|
+
--------------------------------------------------
|
40
|
+
|
41
|
+
If your code may run inside or outside the reactor the {Empathy} module itself provides a set of submodules that delegate to either the native ruby class when called outside of the reactor, or to the {Empathy::EM} class when called inside the reactor.
|
42
|
+
|
43
|
+
require 'eventmachine'
|
44
|
+
require 'empathy'
|
45
|
+
Empathy::Thread.current.inspect # => "Thread<...>"
|
46
|
+
|
47
|
+
Empathy::Kernel.sleep(1)
|
48
|
+
|
49
|
+
Empathy.event_machine? # => false
|
50
|
+
|
51
|
+
Empathy.run do
|
52
|
+
|
53
|
+
Empathy.event_machine? # => true
|
54
|
+
|
55
|
+
Empathy::Thread.new do
|
56
|
+
|
57
|
+
Empathy::Thread.current.inspect #=> "Empathy::EM::Thread<...>"
|
58
|
+
|
59
|
+
Empathy::Kernel.sleep(1)
|
60
|
+
|
61
|
+
begin
|
62
|
+
#...do something with threads...
|
63
|
+
rescue Empathy::ThreadError
|
64
|
+
# ...
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
Note that since Empathy::Thread and friends are modules, you cannot subclass them
|
70
|
+
|
71
|
+
Empathise with all ruby code
|
72
|
+
-------------------------------
|
73
|
+
|
74
|
+
Seamlessly Replace Ruby's native classes with the Empathy:EM ones (redefines top level constants), plus monkey patching of
|
75
|
+
{Object#sleep} and {Object#at_exit}
|
76
|
+
|
77
|
+
require 'empathy/with_all_of_ruby'
|
78
|
+
# do not run any code that uses threads outside of the reactor after the above require
|
79
|
+
|
80
|
+
Empathy.run do
|
81
|
+
t = Thread.new { 1 + 2 }
|
82
|
+
|
83
|
+
t.inspect # => "Empathy::EM::Thread<.....>"
|
84
|
+
|
85
|
+
# this will be a Fiber+EM sleep, not Kernel.sleep
|
86
|
+
sleep(4)
|
87
|
+
|
88
|
+
t.join
|
89
|
+
end
|
90
|
+
|
91
|
+
Caveat: Take care with code that subclasses Thread. This can work as long as the classes are defined after
|
92
|
+
'empathy/thread' is required.
|
93
|
+
|
94
|
+
Q: But doesn't eventmachine need to use normal threads?
|
95
|
+
|
96
|
+
A: Indeed, 'empathy/thread' also defines constants in the EventMachine namespace that refer to the original Ruby classes
|
97
|
+
|
98
|
+
Empathise a library module
|
99
|
+
----------------------------------
|
100
|
+
|
101
|
+
module MyLibary
|
102
|
+
def create_thread
|
103
|
+
Thread.new { Thread.current.inspect }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# If library will only be used inside the reactor
|
108
|
+
Empathy::EM.empathise(MyLibrary)
|
109
|
+
|
110
|
+
# If library is used both inside and outside the reactor
|
111
|
+
Empathy.empathise(MyLibrary)
|
112
|
+
|
113
|
+
See {Empathy::EM.empathise} and {Empathy.empathise}.
|
114
|
+
|
115
|
+
In both cases constants are defined in the MyLibrary namespace so that Thread, Queue etc, refer to either Empathy modules
|
116
|
+
or Empathy:EM classes. Note that any call to empathise will have the side-effect of monkey patching Object to provide EM
|
117
|
+
safe #sleep and #at_exit.
|
118
|
+
|
119
|
+
Caveat: MyLibrary must not subclass Thread etc...
|
120
|
+
|
121
|
+
Empathy::EM::IO - Implement Ruby's Socket API over EventMachine
|
122
|
+
---------------------------------------------------------------
|
123
|
+
|
124
|
+
Work in progress - see experimental socket-io branch
|
125
|
+
|
126
|
+
Contributing to empathy
|
127
|
+
---------------------------
|
128
|
+
|
129
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
130
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
131
|
+
* Fork the project
|
132
|
+
* Start a feature/bugfix branch
|
133
|
+
* Commit and push until you are happy with your contribution
|
134
|
+
* Make sure to add specs, preferably based on ruby-spec
|
135
|
+
|
136
|
+
Copyright
|
137
|
+
-----------------
|
138
|
+
|
139
|
+
Copyright (c) 2011 Christopher J. Bottaro. (Original fiber+EM concept in "strand" library)
|
140
|
+
|
141
|
+
Copyright (c) 2012,2013 Grant Gardner.
|
142
|
+
|
143
|
+
See {file:LICENSE.txt} for further details.
|
144
|
+
|
data/Rakefile
CHANGED
@@ -2,16 +2,12 @@ require "bundler/gem_tasks"
|
|
2
2
|
|
3
3
|
require 'rspec/core'
|
4
4
|
require 'rspec/core/rake_task'
|
5
|
-
require '
|
5
|
+
require 'yard'
|
6
6
|
require 'rake/clean'
|
7
7
|
|
8
8
|
RSpec::Core::RakeTask.new(:spec)
|
9
9
|
|
10
|
-
|
11
|
-
rdoc.main = "README.rdoc"
|
12
|
-
rdoc.rdoc_files.include("README.rdoc", "CHANGELOG","lib/**/*.rb")
|
13
|
-
rdoc.title = "Empathy"
|
14
|
-
end
|
10
|
+
YARD::Rake::YardocTask.new
|
15
11
|
|
16
12
|
# Create the test task.
|
17
13
|
desc 'Run mspec'
|
@@ -29,5 +25,6 @@ desc "Run tests"
|
|
29
25
|
task :test => [ :spec, :mspec ]
|
30
26
|
|
31
27
|
task :default => [ :test, :build ]
|
32
|
-
|
28
|
+
|
29
|
+
CLOBBER.include [ "pkg/","doc/" ]
|
33
30
|
|
data/TESTING.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Empathy Testing
|
2
|
+
===================
|
3
|
+
|
4
|
+
Empathy::EM
|
5
|
+
-----------------
|
6
|
+
Classes in Empathy::EM module are tested using the fully empathic method - replacing Ruby's ::Thread constant with one that points to Empathy::EM::Thread etc, and then running against the subset of rubyspec to do with threads. The rubyspecs are not changed at all but some tests are skipped (see empathy.mspec)
|
7
|
+
|
8
|
+
This also tests Empathy.run and the class replacement approach (including subclassing)
|
9
|
+
|
10
|
+
Empathy module - reactor aware
|
11
|
+
--------------------------------
|
12
|
+
|
13
|
+
We just test that the delegation works as expected, with explicit specs run under rspec - see spec/empathy_spec.rb
|
14
|
+
|
15
|
+
|
16
|
+
Empathising libaries
|
17
|
+
-------------------------------
|
18
|
+
|
19
|
+
We prove that code can reference various classes as normal, and have them actually use the ones injected by {Empathy.empathise} or {Empathy::EM.empathise}. Aside from some magic for the MonitorMixin this is really just standard ruby behaviour.
|
20
|
+
|
21
|
+
MonitorMixin
|
22
|
+
-------------------------------
|
23
|
+
|
24
|
+
There are no rubyspecs for this module, so I have converted ruby 1.9's unit tests into a simple rubyspec - see rubyspec/monitor_spec.rb
|
25
|
+
|
26
|
+
|
27
|
+
|
data/empathy.gemspec
CHANGED
@@ -16,8 +16,12 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.require_paths = ["lib"]
|
17
17
|
gem.licenses = %q{MIT}
|
18
18
|
|
19
|
+
# Empathy can be used without eventmachine
|
20
|
+
gem.add_development_dependency 'eventmachine', '~> 1.0.0'
|
21
|
+
|
19
22
|
gem.add_development_dependency 'mspec', '>= 1.5.18'
|
20
23
|
gem.add_development_dependency 'rspec'
|
21
24
|
gem.add_development_dependency 'rr'
|
22
|
-
gem.add_development_dependency '
|
25
|
+
gem.add_development_dependency 'yard'
|
26
|
+
gem.add_development_dependency 'redcarpet'
|
23
27
|
end
|
@@ -11,7 +11,8 @@ module Empathy
|
|
11
11
|
# Using a mutex for condition variables is meant to protect
|
12
12
|
# against race conditions when the signal occurs between testing whether
|
13
13
|
# a wait is needed and waiting. This situation will never occur with
|
14
|
-
# fibers, but the semantic is retained
|
14
|
+
# fibers, but the semantic is retained for compatibility with ::ConditionVariable
|
15
|
+
# @return [ConditionVariable] self
|
15
16
|
def wait(mutex=nil,timeout = nil)
|
16
17
|
|
17
18
|
if timeout.nil? && (mutex.nil? || Numeric === mutex)
|
@@ -20,31 +21,37 @@ module Empathy
|
|
20
21
|
end
|
21
22
|
|
22
23
|
# Get the fiber (Empathy::EM::Thread) that called us.
|
23
|
-
|
24
|
+
thread = Thread.current
|
24
25
|
# Add the fiber to the list of waiters.
|
25
|
-
@waiters <<
|
26
|
+
@waiters << thread
|
26
27
|
begin
|
27
28
|
sleeper = mutex ? mutex : Kernel
|
28
29
|
if timeout then sleeper.sleep(timeout) else sleeper.sleep() end
|
29
30
|
ensure
|
30
|
-
# Remove from list of waiters.
|
31
|
-
@waiters.delete(
|
31
|
+
# Remove from list of waiters. Note this doesn't run if the thread is killed
|
32
|
+
@waiters.delete(thread)
|
32
33
|
end
|
33
34
|
self
|
34
35
|
end
|
35
36
|
|
37
|
+
# Like ::ConditionVariable#signal
|
38
|
+
# @return [ConditionVariable] self
|
36
39
|
def signal
|
37
|
-
# If there are no waiters, do nothing.
|
38
|
-
return self if @waiters.empty?
|
39
40
|
|
40
|
-
# Find a waiter to wake up
|
41
|
-
|
41
|
+
# Find a waiter to wake up
|
42
|
+
until @waiters.empty?
|
43
|
+
waiter = @waiters.shift
|
44
|
+
if waiter.alive?
|
45
|
+
::EM.next_tick{ waiter.wakeup if waiter.alive? }
|
46
|
+
break;
|
47
|
+
end
|
48
|
+
end
|
42
49
|
|
43
|
-
# Resume it on next tick.
|
44
|
-
::EM.next_tick{ waiter.wakeup }
|
45
50
|
self
|
46
51
|
end
|
47
52
|
|
53
|
+
# Like ::ConditionVariable#broadcast
|
54
|
+
# @return [ConditionVariable] self
|
48
55
|
def broadcast
|
49
56
|
all_waiting = @waiters.dup
|
50
57
|
@waiters.clear
|
data/lib/empathy/em/mutex.rb
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
module Empathy
|
2
2
|
module EM
|
3
|
+
|
4
|
+
# An Empathy equivalent to ::Mutex
|
3
5
|
class Mutex
|
4
6
|
|
5
7
|
def initialize()
|
6
8
|
@waiters = []
|
7
9
|
end
|
8
10
|
|
11
|
+
# Like ::Mutex#lock, except allows reentrant locks
|
12
|
+
# @return [Mutex] self
|
9
13
|
def lock()
|
10
14
|
em_thread = Thread.current
|
11
15
|
@waiters << em_thread
|
@@ -16,16 +20,24 @@ module Empathy
|
|
16
20
|
self
|
17
21
|
end
|
18
22
|
|
23
|
+
# Like ::Mutex#unlock
|
24
|
+
# @return [Mutex] self
|
25
|
+
# @raise [FiberError] if not owner of the mutex
|
19
26
|
def unlock()
|
20
27
|
em_thread = Thread.current
|
21
28
|
raise FiberError, "not owner" unless @waiters.first == em_thread
|
22
29
|
release()
|
30
|
+
self
|
23
31
|
end
|
24
32
|
|
33
|
+
# Like ::Mutex#locked?
|
34
|
+
# @return [true,false]
|
25
35
|
def locked?
|
26
36
|
!@waiters.empty? && @waiters.first.alive?
|
27
37
|
end
|
28
38
|
|
39
|
+
# Like ::Mutex#try_lock
|
40
|
+
# @return [true,false]
|
29
41
|
def try_lock
|
30
42
|
if locked?
|
31
43
|
false
|
@@ -35,6 +47,8 @@ module Empathy
|
|
35
47
|
end
|
36
48
|
end
|
37
49
|
|
50
|
+
# Like ::Mutex#synchronize
|
51
|
+
# @return the result of the block
|
38
52
|
def synchronize(&block)
|
39
53
|
lock
|
40
54
|
yield
|
@@ -42,6 +56,9 @@ module Empathy
|
|
42
56
|
unlock
|
43
57
|
end
|
44
58
|
|
59
|
+
# Like ::Mutex#sleep
|
60
|
+
# @param [Numeric] timeout
|
61
|
+
# @return [Fixnum]
|
45
62
|
def sleep(timeout=nil)
|
46
63
|
unlock
|
47
64
|
begin
|
data/lib/empathy/em/queue.rb
CHANGED
@@ -1,26 +1,7 @@
|
|
1
1
|
module Empathy
|
2
2
|
module EM
|
3
|
+
|
3
4
|
# A Empathy equivalent to ::Queue from thread.rb
|
4
|
-
# queue = Empathy::Queue.new
|
5
|
-
#
|
6
|
-
# producer = Empathy::Thread.new do
|
7
|
-
# 5.times do |i|
|
8
|
-
# Empathy::Kernel.sleep rand(i) # simulate expense
|
9
|
-
# queue << i
|
10
|
-
# puts "#{i} produced"
|
11
|
-
# end
|
12
|
-
# end
|
13
|
-
#
|
14
|
-
# consumer = Empathy.new do
|
15
|
-
# 5.times do |i|
|
16
|
-
# value = queue.pop
|
17
|
-
# Empathy::Kernel.sleep rand(i/2) # simulate expense
|
18
|
-
# puts "consumed #{value}"
|
19
|
-
# end
|
20
|
-
# end
|
21
|
-
#
|
22
|
-
# consumer.join
|
23
|
-
#
|
24
5
|
class Queue
|
25
6
|
|
26
7
|
# Creates a new queue
|
@@ -31,7 +12,8 @@ module Empathy
|
|
31
12
|
@waiting = 0
|
32
13
|
end
|
33
14
|
|
34
|
-
#
|
15
|
+
# @param [Object] obj
|
16
|
+
# @return [void]
|
35
17
|
def push(obj)
|
36
18
|
@q << obj
|
37
19
|
@mutex.synchronize { @cv.signal }
|
@@ -40,12 +22,8 @@ module Empathy
|
|
40
22
|
alias :enq :push
|
41
23
|
|
42
24
|
# 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
|
-
#
|
25
|
+
# @param [Boolean] non_block
|
26
|
+
# @raise FiberError if non_block is true and the queue is empty
|
49
27
|
def pop(non_block=false)
|
50
28
|
raise FiberError, "queue empty" if non_block && empty?
|
51
29
|
if empty?
|
@@ -59,23 +37,25 @@ module Empathy
|
|
59
37
|
alias :shift :pop
|
60
38
|
alias :deq :pop
|
61
39
|
|
62
|
-
#
|
40
|
+
# @return [Fixnum] the length of the queue
|
63
41
|
def length
|
64
42
|
@q.length
|
65
43
|
end
|
66
44
|
alias :size :length
|
67
45
|
|
68
|
-
#
|
46
|
+
# @return [true] if the queue is empty
|
47
|
+
# @return [false] otherwise
|
69
48
|
def empty?
|
70
49
|
@q.empty?
|
71
50
|
end
|
72
51
|
|
73
52
|
# Removes all objects from the queue
|
53
|
+
# @return [void]
|
74
54
|
def clear
|
75
55
|
@q.clear
|
76
56
|
end
|
77
57
|
|
78
|
-
#
|
58
|
+
# @return [Fixnum] the number of fibers waiting on the queue
|
79
59
|
def num_waiting
|
80
60
|
@waiting
|
81
61
|
end
|