agent 0.1.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.gitignore +2 -0
  2. data/.rspec +0 -0
  3. data/Gemfile +0 -1
  4. data/Gemfile.lock +12 -12
  5. data/README.md +51 -36
  6. data/Rakefile +5 -0
  7. data/agent.gemspec +1 -0
  8. data/autotest/discover.rb +9 -1
  9. data/benchmark/multi_ruby_bench.sh +87 -0
  10. data/benchmark/sieve.rb +238 -0
  11. data/examples/agent-workers.rb +51 -0
  12. data/examples/producer-consumer.rb +15 -0
  13. data/lib/agent.rb +1 -15
  14. data/lib/agent/all.rb +22 -0
  15. data/lib/agent/blocking_once.rb +26 -0
  16. data/lib/agent/channel.rb +88 -41
  17. data/lib/agent/error.rb +15 -0
  18. data/lib/agent/errors.rb +14 -0
  19. data/lib/agent/go.rb +9 -0
  20. data/lib/agent/kernel/channel.rb +7 -0
  21. data/lib/agent/kernel/go.rb +7 -0
  22. data/lib/agent/kernel/select.rb +7 -0
  23. data/lib/agent/notifier.rb +34 -0
  24. data/lib/agent/once.rb +32 -0
  25. data/lib/agent/pop.rb +70 -0
  26. data/lib/agent/push.rb +70 -0
  27. data/lib/agent/queue.rb +133 -0
  28. data/lib/agent/queue/buffered.rb +68 -0
  29. data/lib/agent/queue/unbuffered.rb +88 -0
  30. data/lib/agent/queues.rb +50 -0
  31. data/lib/agent/selector.rb +119 -0
  32. data/lib/agent/uuid.rb +36 -0
  33. data/lib/agent/version.rb +1 -1
  34. data/lib/agent/wait_group.rb +43 -0
  35. data/spec/blocking_once_spec.rb +122 -0
  36. data/spec/channel_spec.rb +153 -82
  37. data/spec/error_spec.rb +15 -0
  38. data/spec/examples/channel_of_channels_spec.rb +17 -14
  39. data/spec/examples/producer_consumer_spec.rb +26 -16
  40. data/spec/examples/sieve_spec.rb +43 -37
  41. data/spec/go_spec.rb +17 -0
  42. data/spec/notifier_spec.rb +42 -0
  43. data/spec/once_spec.rb +91 -0
  44. data/spec/pop_spec.rb +97 -0
  45. data/spec/push_spec.rb +95 -0
  46. data/spec/queue_spec.rb +335 -37
  47. data/spec/queues_spec.rb +28 -0
  48. data/spec/selector_spec.rb +398 -0
  49. data/spec/spec_helper.rb +19 -0
  50. data/spec/uuid_spec.rb +13 -0
  51. data/spec/wait_group_spec.rb +45 -0
  52. metadata +94 -54
  53. data/lib/agent/transport/queue.rb +0 -82
  54. 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
@@ -1,15 +1 @@
1
- require 'agent/channel'
2
- require 'agent/transport/queue'
3
-
4
- module Kernel
5
- def go(*args, &blk)
6
- Thread.new do
7
- begin
8
- blk.call(*args)
9
- rescue Exception => e
10
- p e
11
- p e.backtrace
12
- end
13
- end
14
- end
15
- end
1
+ require "agent/all"
@@ -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
@@ -1,74 +1,121 @@
1
- # Channels combine communication—the exchange of a value—with synchronization—guaranteeing
2
- # that two calculations (goroutines) are in a known state.
3
- # - http://golang.org/doc/effective_go.html#channels
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, :transport, :chan
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 initialize(opts = {})
10
- @state = :active
11
- @name = opts[:name]
12
- @max = opts[:size] || 1
13
- @type = opts[:type]
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
- @chan = @transport.new(@name, @max)
21
- end
33
+ # Serialization methods
22
34
 
23
35
  def marshal_load(ary)
24
- @state, @name, @type, @direction, @transport = *ary
25
- @chan = @transport.new(@name)
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
- [@state, @name, @type, @direction, @transport]
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
- @chan.send(Marshal.dump(msg))
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 receive
43
- check_direction(:receive)
56
+ def push?; queue.push?; end
57
+ alias :send? :push?
58
+
44
59
 
45
- msg = Marshal.load(@chan.receive)
46
- check_type(msg)
60
+ # Receiving methods
47
61
 
48
- msg
62
+ def receive(options={})
63
+ check_direction(:receive)
64
+ queue.pop(options)
49
65
  end
50
66
  alias :pop :receive
51
67
 
52
- def closed?; @state == :closed; end
68
+ def pop?; queue.pop?; end
69
+ alias :receive? :pop?
70
+
71
+
72
+ # Closing methods
73
+
53
74
  def close
54
- @chan.close
55
- @state = :closed
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
- private
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
- def check_direction(direction)
65
- return if @direction == :bidirectional
66
- raise InvalidDirection if @direction != direction
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
@@ -0,0 +1,15 @@
1
+ module Agent
2
+ class Error
3
+ def initialize(message)
4
+ @message = message
5
+ end
6
+
7
+ def to_s
8
+ @message
9
+ end
10
+
11
+ def message?(message)
12
+ @message == message
13
+ end
14
+ end
15
+ end
@@ -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
@@ -0,0 +1,9 @@
1
+ require "thread"
2
+ require "agent/errors"
3
+
4
+ module Agent
5
+ def self.go!(*args)
6
+ raise Errors::BlockMissing unless block_given?
7
+ Thread.new(*args){|*a| yield(*a) }
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ require "agent/channel"
2
+
3
+ module Kernel
4
+ def channel!(*args)
5
+ Agent.channel!(*args)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require "agent/go"
2
+
3
+ module Kernel
4
+ def go!(*args, &blk)
5
+ Agent.go!(*args, &blk)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require "agent/selector"
2
+
3
+ module Kernel
4
+ def select!(&blk)
5
+ Agent.select!(&blk)
6
+ end
7
+ end
@@ -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
@@ -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
@@ -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