empathy 0.0.1.RC0 → 0.0.1.RC2
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.
- 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
|