agent 0.1.0 → 0.9.0
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/.gitignore +2 -0
- data/.rspec +0 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +12 -12
- data/README.md +51 -36
- data/Rakefile +5 -0
- data/agent.gemspec +1 -0
- data/autotest/discover.rb +9 -1
- data/benchmark/multi_ruby_bench.sh +87 -0
- data/benchmark/sieve.rb +238 -0
- data/examples/agent-workers.rb +51 -0
- data/examples/producer-consumer.rb +15 -0
- data/lib/agent.rb +1 -15
- data/lib/agent/all.rb +22 -0
- data/lib/agent/blocking_once.rb +26 -0
- data/lib/agent/channel.rb +88 -41
- data/lib/agent/error.rb +15 -0
- data/lib/agent/errors.rb +14 -0
- data/lib/agent/go.rb +9 -0
- data/lib/agent/kernel/channel.rb +7 -0
- data/lib/agent/kernel/go.rb +7 -0
- data/lib/agent/kernel/select.rb +7 -0
- data/lib/agent/notifier.rb +34 -0
- data/lib/agent/once.rb +32 -0
- data/lib/agent/pop.rb +70 -0
- data/lib/agent/push.rb +70 -0
- data/lib/agent/queue.rb +133 -0
- data/lib/agent/queue/buffered.rb +68 -0
- data/lib/agent/queue/unbuffered.rb +88 -0
- data/lib/agent/queues.rb +50 -0
- data/lib/agent/selector.rb +119 -0
- data/lib/agent/uuid.rb +36 -0
- data/lib/agent/version.rb +1 -1
- data/lib/agent/wait_group.rb +43 -0
- data/spec/blocking_once_spec.rb +122 -0
- data/spec/channel_spec.rb +153 -82
- data/spec/error_spec.rb +15 -0
- data/spec/examples/channel_of_channels_spec.rb +17 -14
- data/spec/examples/producer_consumer_spec.rb +26 -16
- data/spec/examples/sieve_spec.rb +43 -37
- data/spec/go_spec.rb +17 -0
- data/spec/notifier_spec.rb +42 -0
- data/spec/once_spec.rb +91 -0
- data/spec/pop_spec.rb +97 -0
- data/spec/push_spec.rb +95 -0
- data/spec/queue_spec.rb +335 -37
- data/spec/queues_spec.rb +28 -0
- data/spec/selector_spec.rb +398 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/uuid_spec.rb +13 -0
- data/spec/wait_group_spec.rb +45 -0
- metadata +94 -54
- data/lib/agent/transport/queue.rb +0 -82
- data/spec/helper.rb +0 -5
@@ -0,0 +1,51 @@
|
|
1
|
+
project_lib_path = File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
|
2
|
+
$LOAD_PATH.unshift(project_lib_path)
|
3
|
+
require 'agent'
|
4
|
+
|
5
|
+
# First, we declare a new Ruby struct, which will encapsulate several arguments, and then
|
6
|
+
# declare a clientRequests channel, which will carry our Request struct. Nothing unusual,
|
7
|
+
# except that we also set the size of our channel to two – we’ll see why in a
|
8
|
+
# second.
|
9
|
+
|
10
|
+
Request = Struct.new(:args, :resultChan)
|
11
|
+
clientRequests = channel!(Request, 2)
|
12
|
+
|
13
|
+
# Now, we create a new worker block, which takes in a “reqs” object, calls receive on it
|
14
|
+
# (hint, req’s is a Channel!), sleeps for a bit, and then sends back a timestamped
|
15
|
+
# result. With the help of some Ruby syntax sugar, we then start two workers by passing
|
16
|
+
# this block to our go function.
|
17
|
+
|
18
|
+
worker = Proc.new do |reqs|
|
19
|
+
loop do
|
20
|
+
req = reqs.receive[0]
|
21
|
+
sleep 1.0
|
22
|
+
req.resultChan << [Time.now, req.args + 1].join(' : ')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# start two workers
|
27
|
+
go!(clientRequests, &worker)
|
28
|
+
go!(clientRequests, &worker)
|
29
|
+
|
30
|
+
# The rest is simple, we create two distinct requests, which carry a number and a reply
|
31
|
+
# channel, and pass them to our clientRequests pipe, on which our workers are waiting.
|
32
|
+
# Once dispatched, we simply call receive and wait for the results!
|
33
|
+
|
34
|
+
req1 = Request.new(1, channel!(String))
|
35
|
+
req2 = Request.new(2, channel!(String))
|
36
|
+
|
37
|
+
clientRequests << req1
|
38
|
+
clientRequests << req2
|
39
|
+
|
40
|
+
# retrieve results
|
41
|
+
puts req1.resultChan.receive[0] # => 2010-11-28 23:31:08 -0500 : 2
|
42
|
+
puts req2.resultChan.receive[0] # => 2010-11-28 23:31:08 -0500 : 3
|
43
|
+
|
44
|
+
# Notice something interesting? Both results came back with the same timestamp! Our
|
45
|
+
# clientRequests channel allowed for up to two messages in the pipe, which our workers
|
46
|
+
# immediately received, executed, and returned the results. Once again, not a thread
|
47
|
+
# or a mutex in sight.
|
48
|
+
|
49
|
+
clientRequests.close
|
50
|
+
req1.resultChan.close
|
51
|
+
req2.resultChan.close
|
@@ -0,0 +1,15 @@
|
|
1
|
+
project_lib_path = File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
|
2
|
+
$LOAD_PATH.unshift(project_lib_path)
|
3
|
+
require 'agent'
|
4
|
+
|
5
|
+
c = channel!(Integer)
|
6
|
+
|
7
|
+
go!(c) do |c|
|
8
|
+
i = 0
|
9
|
+
loop { c << i+= 1 }
|
10
|
+
end
|
11
|
+
|
12
|
+
p c.receive[0] # => 1
|
13
|
+
p c.receive[0] # => 2
|
14
|
+
|
15
|
+
c.close
|
data/lib/agent.rb
CHANGED
data/lib/agent/all.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "thread"
|
2
|
+
|
3
|
+
require "agent/version"
|
4
|
+
require "agent/errors"
|
5
|
+
require "agent/error"
|
6
|
+
require "agent/once"
|
7
|
+
require "agent/blocking_once"
|
8
|
+
require "agent/pop"
|
9
|
+
require "agent/push"
|
10
|
+
require "agent/notifier"
|
11
|
+
require "agent/uuid"
|
12
|
+
require "agent/wait_group"
|
13
|
+
require "agent/go"
|
14
|
+
require "agent/queues"
|
15
|
+
require "agent/queue"
|
16
|
+
require "agent/queue/buffered"
|
17
|
+
require "agent/queue/unbuffered"
|
18
|
+
require "agent/channel"
|
19
|
+
require "agent/selector"
|
20
|
+
require "agent/kernel/channel"
|
21
|
+
require "agent/kernel/go"
|
22
|
+
require "agent/kernel/select"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "agent/errors"
|
2
|
+
|
3
|
+
module Agent
|
4
|
+
class BlockingOnce < Once
|
5
|
+
def perform
|
6
|
+
@mutex.synchronize do
|
7
|
+
return nil, error if @performed
|
8
|
+
|
9
|
+
begin
|
10
|
+
value = yield
|
11
|
+
@performed = true
|
12
|
+
return value, nil
|
13
|
+
rescue Errors::Rollback
|
14
|
+
return nil, rollback_error
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def rollback_error
|
22
|
+
@rollback_error ||= Error.new("rolled back")
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
data/lib/agent/channel.rb
CHANGED
@@ -1,74 +1,121 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
require "agent/uuid"
|
2
|
+
require "agent/push"
|
3
|
+
require "agent/pop"
|
4
|
+
require "agent/queues"
|
5
|
+
require "agent/errors"
|
4
6
|
|
5
7
|
module Agent
|
8
|
+
def self.channel!(*args)
|
9
|
+
Channel.new(*args)
|
10
|
+
end
|
11
|
+
|
6
12
|
class Channel
|
7
|
-
attr_reader :name, :
|
13
|
+
attr_reader :name, :direction, :type, :max
|
14
|
+
|
15
|
+
def initialize(*args)
|
16
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
17
|
+
@type = args.shift
|
18
|
+
@max = args.shift || 0
|
19
|
+
@closed = false
|
20
|
+
@name = opts[:name] || UUID.generate
|
21
|
+
@direction = opts[:direction] || :bidirectional
|
22
|
+
@close_mutex = Mutex.new
|
23
|
+
@queue = Queues.register(@name, @type, @max)
|
24
|
+
end
|
8
25
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@direction = opts[:direction] || :bidirectional
|
15
|
-
@transport = opts[:transport] || Agent::Transport::Queue
|
26
|
+
def queue
|
27
|
+
q = @queue
|
28
|
+
raise Errors::ChannelClosed unless q
|
29
|
+
q
|
30
|
+
end
|
16
31
|
|
17
|
-
raise NoName if @name.nil?
|
18
|
-
raise Untyped if @type.nil?
|
19
32
|
|
20
|
-
|
21
|
-
end
|
33
|
+
# Serialization methods
|
22
34
|
|
23
35
|
def marshal_load(ary)
|
24
|
-
@
|
25
|
-
@
|
36
|
+
@closed, @name, @max, @type, @direction = *ary
|
37
|
+
@queue = Queues[@name]
|
38
|
+
@closed = @queue.nil? || @queue.closed?
|
26
39
|
self
|
27
40
|
end
|
28
41
|
|
29
42
|
def marshal_dump
|
30
|
-
[@
|
43
|
+
[@closed, @name, @max, @type, @direction]
|
31
44
|
end
|
32
45
|
|
33
|
-
def send(msg)
|
34
|
-
check_direction(:send)
|
35
|
-
check_type(msg)
|
36
46
|
|
37
|
-
|
47
|
+
# Sending methods
|
48
|
+
|
49
|
+
def send(object, options={})
|
50
|
+
check_direction(:send)
|
51
|
+
queue.push(object, options)
|
38
52
|
end
|
39
53
|
alias :push :send
|
40
54
|
alias :<< :send
|
41
55
|
|
42
|
-
def
|
43
|
-
|
56
|
+
def push?; queue.push?; end
|
57
|
+
alias :send? :push?
|
58
|
+
|
44
59
|
|
45
|
-
|
46
|
-
check_type(msg)
|
60
|
+
# Receiving methods
|
47
61
|
|
48
|
-
|
62
|
+
def receive(options={})
|
63
|
+
check_direction(:receive)
|
64
|
+
queue.pop(options)
|
49
65
|
end
|
50
66
|
alias :pop :receive
|
51
67
|
|
52
|
-
def
|
68
|
+
def pop?; queue.pop?; end
|
69
|
+
alias :receive? :pop?
|
70
|
+
|
71
|
+
|
72
|
+
# Closing methods
|
73
|
+
|
53
74
|
def close
|
54
|
-
@
|
55
|
-
|
75
|
+
@close_mutex.synchronize do
|
76
|
+
raise Errors::ChannelClosed if @closed
|
77
|
+
@closed = true
|
78
|
+
@queue.close
|
79
|
+
@queue = nil
|
80
|
+
Queues.delete(@name)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
def closed?; @closed; end
|
84
|
+
def open?; !@closed; end
|
85
|
+
|
86
|
+
def remove_operations(operations)
|
87
|
+
# ugly, but it overcomes the race condition without synchronization
|
88
|
+
# since instance variable access is atomic.
|
89
|
+
q = @queue
|
90
|
+
q.remove_operations(operations) if q
|
56
91
|
end
|
57
92
|
|
58
|
-
|
93
|
+
def as_send_only
|
94
|
+
as_direction_only(:send)
|
95
|
+
end
|
96
|
+
|
97
|
+
def as_receive_only
|
98
|
+
as_direction_only(:receive)
|
99
|
+
end
|
59
100
|
|
60
|
-
def check_type(msg)
|
61
|
-
raise InvalidType if !msg.is_a? @type
|
62
|
-
end
|
63
101
|
|
64
|
-
|
65
|
-
|
66
|
-
|
102
|
+
private
|
103
|
+
|
104
|
+
def as_direction_only(direction)
|
105
|
+
@close_mutex.synchronize do
|
106
|
+
raise Errors::ChannelClosed if @closed
|
107
|
+
channel!(@type, @max, :name => @name, :direction => direction)
|
67
108
|
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def check_type(object)
|
112
|
+
raise Errors::InvalidType unless object.is_a?(@type)
|
113
|
+
end
|
114
|
+
|
115
|
+
def check_direction(direction)
|
116
|
+
return if @direction == :bidirectional
|
117
|
+
raise Errors::InvalidDirection if @direction != direction
|
118
|
+
end
|
68
119
|
|
69
|
-
class InvalidDirection < Exception; end
|
70
|
-
class NoName < Exception; end
|
71
|
-
class Untyped < Exception; end
|
72
|
-
class InvalidType < Exception; end
|
73
120
|
end
|
74
121
|
end
|
data/lib/agent/error.rb
ADDED
data/lib/agent/errors.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Agent
|
2
|
+
module Errors
|
3
|
+
class BlockMissing < Exception; end
|
4
|
+
class ChannelClosed < Exception; end
|
5
|
+
class InvalidDirection < Exception; end
|
6
|
+
class Untyped < Exception; end
|
7
|
+
class InvalidType < Exception; end
|
8
|
+
class Rollback < Exception; end
|
9
|
+
class InvalidQueueSize < Exception; end
|
10
|
+
class NotImplementedError < Exception; end
|
11
|
+
class DefaultCaseAlreadyDefinedError < Exception; end
|
12
|
+
class NegativeWaitGroupCount < Exception; end
|
13
|
+
end
|
14
|
+
end
|
data/lib/agent/go.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Agent
|
2
|
+
class Notifier
|
3
|
+
attr_reader :payload
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@mutex = Mutex.new
|
7
|
+
@cvar = ConditionVariable.new
|
8
|
+
@notified = false
|
9
|
+
@payload = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def notified?
|
13
|
+
@notified
|
14
|
+
end
|
15
|
+
|
16
|
+
def wait
|
17
|
+
@mutex.synchronize do
|
18
|
+
until notified?
|
19
|
+
@cvar.wait(@mutex)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def notify(payload)
|
25
|
+
@mutex.synchronize do
|
26
|
+
return Error.new("already notified") if notified?
|
27
|
+
@payload = payload
|
28
|
+
@notified = true
|
29
|
+
@cvar.signal
|
30
|
+
return nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/agent/once.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Agent
|
2
|
+
class Once
|
3
|
+
def initialize
|
4
|
+
@mutex = Mutex.new
|
5
|
+
@performed = false
|
6
|
+
end
|
7
|
+
|
8
|
+
def perform
|
9
|
+
# optimium path
|
10
|
+
return nil, error if @performed
|
11
|
+
|
12
|
+
# slow path
|
13
|
+
@mutex.synchronize do
|
14
|
+
return nil, error if @performed
|
15
|
+
@performed = true
|
16
|
+
end
|
17
|
+
|
18
|
+
return yield, nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def performed?
|
22
|
+
@performed
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def error
|
28
|
+
@error ||= Error.new("already performed")
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/lib/agent/pop.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require "agent/errors"
|
2
|
+
|
3
|
+
module Agent
|
4
|
+
class Pop
|
5
|
+
attr_reader :uuid, :blocking_once, :notifier, :object
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
@object = nil
|
9
|
+
@uuid = options[:uuid] || UUID.generate
|
10
|
+
@blocking_once = options[:blocking_once]
|
11
|
+
@notifier = options[:notifier]
|
12
|
+
@mutex = Mutex.new
|
13
|
+
@cvar = ConditionVariable.new
|
14
|
+
@received = false
|
15
|
+
@closed = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def received?
|
19
|
+
@received
|
20
|
+
end
|
21
|
+
|
22
|
+
def closed?
|
23
|
+
@closed
|
24
|
+
end
|
25
|
+
|
26
|
+
def wait
|
27
|
+
@mutex.synchronize do
|
28
|
+
until @received || @closed
|
29
|
+
@cvar.wait(@mutex)
|
30
|
+
end
|
31
|
+
return received?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def send
|
36
|
+
@mutex.synchronize do
|
37
|
+
raise Errors::ChannelClosed if @closed
|
38
|
+
|
39
|
+
if @blocking_once
|
40
|
+
value, error = @blocking_once.perform do
|
41
|
+
@object = Marshal.load(yield)
|
42
|
+
@received = true
|
43
|
+
@cvar.signal
|
44
|
+
@notifier.notify(self) if @notifier
|
45
|
+
end
|
46
|
+
|
47
|
+
return error
|
48
|
+
else
|
49
|
+
begin
|
50
|
+
@object = Marshal.load(yield)
|
51
|
+
@received = true
|
52
|
+
@cvar.signal
|
53
|
+
@notifier.notify(self) if @notifier
|
54
|
+
rescue Errors::Rollback
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def close
|
61
|
+
@mutex.synchronize do
|
62
|
+
return if @received
|
63
|
+
@closed = true
|
64
|
+
@cvar.broadcast
|
65
|
+
@notifier.notify(self) if @notifier
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|