cztop 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +31 -0
- data/.yardopts +1 -0
- data/AUTHORS +1 -0
- data/CHANGES.md +3 -0
- data/Gemfile +10 -0
- data/Guardfile +61 -0
- data/LICENSE +5 -0
- data/Procfile +3 -0
- data/README.md +408 -0
- data/Rakefile +6 -0
- data/bin/console +7 -0
- data/bin/setup +7 -0
- data/ci-scripts/install-deps +9 -0
- data/cztop.gemspec +36 -0
- data/examples/ruby_actor/actor.rb +100 -0
- data/examples/simple_req_rep/rep.rb +12 -0
- data/examples/simple_req_rep/req.rb +35 -0
- data/examples/taxi_system/.gitignore +2 -0
- data/examples/taxi_system/Makefile +2 -0
- data/examples/taxi_system/README.gsl +115 -0
- data/examples/taxi_system/README.md +276 -0
- data/examples/taxi_system/broker.rb +98 -0
- data/examples/taxi_system/client.rb +34 -0
- data/examples/taxi_system/generate_keys.rb +24 -0
- data/examples/taxi_system/start_broker.sh +2 -0
- data/examples/taxi_system/start_clients.sh +11 -0
- data/lib/cztop/actor.rb +308 -0
- data/lib/cztop/authenticator.rb +97 -0
- data/lib/cztop/beacon.rb +96 -0
- data/lib/cztop/certificate.rb +176 -0
- data/lib/cztop/config/comments.rb +66 -0
- data/lib/cztop/config/serialization.rb +82 -0
- data/lib/cztop/config/traversing.rb +157 -0
- data/lib/cztop/config.rb +119 -0
- data/lib/cztop/frame.rb +158 -0
- data/lib/cztop/has_ffi_delegate.rb +85 -0
- data/lib/cztop/message/frames.rb +74 -0
- data/lib/cztop/message.rb +191 -0
- data/lib/cztop/monitor.rb +102 -0
- data/lib/cztop/poller.rb +334 -0
- data/lib/cztop/polymorphic_zsock_methods.rb +24 -0
- data/lib/cztop/proxy.rb +149 -0
- data/lib/cztop/send_receive_methods.rb +35 -0
- data/lib/cztop/socket/types.rb +207 -0
- data/lib/cztop/socket.rb +106 -0
- data/lib/cztop/version.rb +3 -0
- data/lib/cztop/z85.rb +157 -0
- data/lib/cztop/zsock_options.rb +334 -0
- data/lib/cztop.rb +55 -0
- data/perf/README.md +79 -0
- data/perf/inproc_lat.rb +49 -0
- data/perf/inproc_thru.rb +42 -0
- data/perf/local_lat.rb +35 -0
- data/perf/remote_lat.rb +26 -0
- metadata +297 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
module CZTop
|
2
|
+
class Message
|
3
|
+
|
4
|
+
# @return [Integer] number of frames
|
5
|
+
# @see content_size
|
6
|
+
def size
|
7
|
+
frames.count
|
8
|
+
end
|
9
|
+
|
10
|
+
# Access to this {Message}'s {Frame}s.
|
11
|
+
# @return [FramesAccessor]
|
12
|
+
def frames
|
13
|
+
FramesAccessor.new(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Used to access a {Message}'s {Frame}s.
|
17
|
+
class FramesAccessor
|
18
|
+
include Enumerable
|
19
|
+
|
20
|
+
# @param message [Message]
|
21
|
+
def initialize(message)
|
22
|
+
@message = message
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the last frame of this message.
|
26
|
+
# @return [Frame] first frame of Message
|
27
|
+
# @return [nil] if there are no frames
|
28
|
+
def first
|
29
|
+
first = @message.ffi_delegate.first
|
30
|
+
return nil if first.null?
|
31
|
+
Frame.from_ffi_delegate(first)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the last frame of this message.
|
35
|
+
# @return [Frame] last {Frame} of {Message}
|
36
|
+
# @return [nil] if there are no frames
|
37
|
+
def last
|
38
|
+
last = @message.ffi_delegate.last
|
39
|
+
return nil if last.null?
|
40
|
+
Frame.from_ffi_delegate(last)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Index access to a frame/frames of this message, just like with an
|
44
|
+
# array.
|
45
|
+
# @overload [](index)
|
46
|
+
# @param index [Integer] index of {Frame} within {Message}
|
47
|
+
# @overload [](*args)
|
48
|
+
# @note See Array#[] for details.
|
49
|
+
# @return [Frame] frame Message
|
50
|
+
# @return [nil] if there are no corresponding frames
|
51
|
+
def [](*args)
|
52
|
+
case args
|
53
|
+
when [0] then first # speed up
|
54
|
+
when [-1] then last # speed up
|
55
|
+
else to_a[*args]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Yields all frames for this message to the given block.
|
60
|
+
# @note Not thread safe.
|
61
|
+
# @yieldparam frame [Frame]
|
62
|
+
# @return [self]
|
63
|
+
def each
|
64
|
+
first = first()
|
65
|
+
return unless first
|
66
|
+
yield first
|
67
|
+
while frame = @message.ffi_delegate.next and not frame.null?
|
68
|
+
yield Frame.from_ffi_delegate(frame)
|
69
|
+
end
|
70
|
+
return self
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
module CZTop
|
2
|
+
# Represents a CZMQ::FFI::Zmsg.
|
3
|
+
class Message
|
4
|
+
include HasFFIDelegate
|
5
|
+
extend CZTop::HasFFIDelegate::ClassMethods
|
6
|
+
include ::CZMQ::FFI
|
7
|
+
|
8
|
+
# Coerces an object into a {Message}.
|
9
|
+
# @param msg [Message, String, Frame, Array<String>, Array<Frame>]
|
10
|
+
# @return [Message]
|
11
|
+
# @raise [ArgumentError] if it can't be coerced
|
12
|
+
def self.coerce(msg)
|
13
|
+
case msg
|
14
|
+
when Message
|
15
|
+
return msg
|
16
|
+
when String, Frame, Array
|
17
|
+
return new(msg)
|
18
|
+
else
|
19
|
+
raise ArgumentError, "cannot coerce message: %p" % msg
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param parts [String, Frame, Array<String>, Array<Frame>] initial parts
|
24
|
+
# of the message
|
25
|
+
def initialize(parts = nil)
|
26
|
+
attach_ffi_delegate(Zmsg.new)
|
27
|
+
Array(parts).each { |part| self << part } if parts
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Boolean] if this message is empty or not
|
31
|
+
def empty?
|
32
|
+
content_size.zero?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Send {Message} to a {Socket} or {Actor}.
|
36
|
+
#
|
37
|
+
# @note Do NOT use this {Message} anymore afterwards. Its native
|
38
|
+
# counterpart will have been destroyed.
|
39
|
+
#
|
40
|
+
# @param destination [Socket, Actor] where to send this message to
|
41
|
+
# @return [void]
|
42
|
+
#
|
43
|
+
# @raise [IO::EAGAINWaitWritable] if the send timeout has been reached
|
44
|
+
# (see {ZsockOptions::OptionsAccessor#sndtimeo=})
|
45
|
+
# @raise [SocketError] if the message can't be routed to the destination
|
46
|
+
# (either if ROUTER_MANDATORY flag is set on a {Socket::ROUTER} socket
|
47
|
+
# and the peer isn't connected or its SNDHWM is reached (see
|
48
|
+
# {ZsockOptions::OptionsAccessor#router_mandatory=}, or if it's
|
49
|
+
# a {Socket::SERVER} socket and there's no connected CLIENT
|
50
|
+
# corresponding
|
51
|
+
# to the given routing ID)
|
52
|
+
# @raise [ArgumentError] if the message is invalid, e.g. when trying to
|
53
|
+
# send a multi-part message over a CLIENT/SERVER socket
|
54
|
+
# @raise [SystemCallError] for any other error code set after +zmsg_send+
|
55
|
+
# returns with failure. Please report as bug.
|
56
|
+
#
|
57
|
+
def send_to(destination)
|
58
|
+
rc = Zmsg.send(ffi_delegate, destination)
|
59
|
+
return if rc == 0
|
60
|
+
raise_zmq_err
|
61
|
+
rescue Errno::EAGAIN
|
62
|
+
raise IO::EAGAINWaitWritable
|
63
|
+
end
|
64
|
+
|
65
|
+
# Receive a {Message} from a {Socket} or {Actor}.
|
66
|
+
# @param source [Socket, Actor]
|
67
|
+
# @return [Message] the newly received message
|
68
|
+
# @raise [IO::EAGAINWaitReadable] if the receive timeout has been reached
|
69
|
+
# (see {ZsockOptions::OptionsAccessor#rcvtimeo=})
|
70
|
+
# @raise [Interrupt] if interrupted while waiting for a message
|
71
|
+
# @raise [SystemCallError] for any other error code set after +zmsg_recv+
|
72
|
+
# returns with failure. Please report as bug.
|
73
|
+
def self.receive_from(source)
|
74
|
+
delegate = Zmsg.recv(source)
|
75
|
+
return from_ffi_delegate(delegate) unless delegate.null?
|
76
|
+
HasFFIDelegate.raise_zmq_err
|
77
|
+
rescue Errno::EAGAIN
|
78
|
+
raise IO::EAGAINWaitReadable
|
79
|
+
end
|
80
|
+
|
81
|
+
# Append a frame to this message.
|
82
|
+
# @param frame [String, Frame] what to append
|
83
|
+
# @raise [ArgumentError] if frame has an invalid type
|
84
|
+
# @raise [SystemCallError] if this fails
|
85
|
+
# @note If you provide a {Frame}, do NOT use that frame afterwards
|
86
|
+
# anymore, as its native counterpart will have been destroyed.
|
87
|
+
# @return [self] so it can be chained
|
88
|
+
def <<(frame)
|
89
|
+
case frame
|
90
|
+
when String
|
91
|
+
# NOTE: can't use addstr because the data might be binary
|
92
|
+
mem = FFI::MemoryPointer.from_string(frame)
|
93
|
+
rc = ffi_delegate.addmem(mem, mem.size - 1) # without NULL byte
|
94
|
+
when Frame
|
95
|
+
rc = ffi_delegate.append(frame.ffi_delegate)
|
96
|
+
else
|
97
|
+
raise ArgumentError, "invalid frame: %p" % frame
|
98
|
+
end
|
99
|
+
raise_zmq_err unless rc == 0
|
100
|
+
self
|
101
|
+
end
|
102
|
+
|
103
|
+
# Prepend a frame to this message.
|
104
|
+
# @param frame [String, Frame] what to prepend
|
105
|
+
# @raise [ArgumentError] if frame has an invalid type
|
106
|
+
# @raise [SystemCallError] if this fails
|
107
|
+
# @note If you provide a {Frame}, do NOT use that frame afterwards
|
108
|
+
# anymore, as its native counterpart will have been destroyed.
|
109
|
+
# @return [void]
|
110
|
+
def prepend(frame)
|
111
|
+
case frame
|
112
|
+
when String
|
113
|
+
# NOTE: can't use pushstr because the data might be binary
|
114
|
+
mem = FFI::MemoryPointer.from_string(frame)
|
115
|
+
rc = ffi_delegate.pushmem(mem, mem.size - 1) # without NULL byte
|
116
|
+
when Frame
|
117
|
+
rc = ffi_delegate.prepend(frame.ffi_delegate)
|
118
|
+
else
|
119
|
+
raise ArgumentError, "invalid frame: %p" % frame
|
120
|
+
end
|
121
|
+
raise_zmq_err unless rc == 0
|
122
|
+
end
|
123
|
+
|
124
|
+
# Removes first part from message and returns it as a string.
|
125
|
+
# @return [String, nil] first part, if any, or nil
|
126
|
+
def pop
|
127
|
+
# NOTE: can't use popstr because the data might be binary
|
128
|
+
ptr = ffi_delegate.pop
|
129
|
+
return nil if ptr.null?
|
130
|
+
Frame.from_ffi_delegate(ptr).to_s
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [Integer] size of this message in bytes
|
134
|
+
# @see size
|
135
|
+
def content_size
|
136
|
+
ffi_delegate.content_size
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns all frames as strings in an array. This is useful if for quick
|
140
|
+
# inspection of the message.
|
141
|
+
# @note It'll read all frames in the message and turn them into Ruby
|
142
|
+
# strings. This can be a problem if the message is huge/has huge frames.
|
143
|
+
# @return [Array<String>] all frames
|
144
|
+
def to_a
|
145
|
+
frames.map(&:to_s)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Inspects this {Message}.
|
149
|
+
# @return [String] shows class, number of frames, content size, and
|
150
|
+
# content (only if it's up to 200 bytes)
|
151
|
+
def inspect
|
152
|
+
"#<%s:0x%x frames=%i content_size=%i content=%s>" % [
|
153
|
+
self.class,
|
154
|
+
to_ptr.address,
|
155
|
+
size,
|
156
|
+
content_size,
|
157
|
+
content_size <= 500 ? to_a.inspect : "[...]"
|
158
|
+
]
|
159
|
+
end
|
160
|
+
|
161
|
+
# Return a frame's content.
|
162
|
+
# @return [String] the frame's content, if it exists
|
163
|
+
# @return [nil] if frame doesn't exist at given index
|
164
|
+
def [](frame_index)
|
165
|
+
frame = frames[frame_index] or return nil
|
166
|
+
frame.to_s
|
167
|
+
end
|
168
|
+
|
169
|
+
# Gets the routing ID.
|
170
|
+
# @note This only set when the frame came from a {CZTop::Socket::SERVER}
|
171
|
+
# socket.
|
172
|
+
# @return [Integer] the routing ID, or 0 if unset
|
173
|
+
ffi_delegate :routing_id
|
174
|
+
|
175
|
+
# Sets a new routing ID.
|
176
|
+
# @note This is used when the message is sent to a {CZTop::Socket::SERVER}
|
177
|
+
# socket.
|
178
|
+
# @param new_routing_id [Integer] new routing ID
|
179
|
+
# @raise [ArgumentError] if new routing ID is not an Integer
|
180
|
+
# @raise [RangeError] if new routing ID is out of +uint32_t+ range
|
181
|
+
# @return [new_routing_id]
|
182
|
+
def routing_id=(new_routing_id)
|
183
|
+
raise ArgumentError unless new_routing_id.is_a? Integer
|
184
|
+
|
185
|
+
# need to raise manually, as FFI lacks this feature.
|
186
|
+
# @see https://github.com/ffi/ffi/issues/473
|
187
|
+
raise RangeError if new_routing_id < 0
|
188
|
+
ffi_delegate.set_routing_id(new_routing_id)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require "pry"
|
2
|
+
|
3
|
+
module CZTop
|
4
|
+
# CZMQ monitor. Listen for socket events.
|
5
|
+
#
|
6
|
+
# This is implemented using an {Actor}.
|
7
|
+
#
|
8
|
+
# @note This works only on connection oriented transports, like TCP, IPC,
|
9
|
+
# and TIPC.
|
10
|
+
# @see http://api.zeromq.org/czmq3-0:zmonitor
|
11
|
+
# @see http://api.zeromq.org/4-1:zmq-socket-monitor
|
12
|
+
class Monitor
|
13
|
+
include ::CZMQ::FFI
|
14
|
+
|
15
|
+
# function pointer to the `zmonitor()` function
|
16
|
+
ZMONITOR_FPTR = ::CZMQ::FFI.ffi_libraries.each do |dl|
|
17
|
+
fptr = dl.find_function("zmonitor")
|
18
|
+
break fptr if fptr
|
19
|
+
end
|
20
|
+
raise LoadError, "couldn't find zmonitor()" if ZMONITOR_FPTR.nil?
|
21
|
+
|
22
|
+
# @param socket [Socket, Actor] the socket or actor to monitor
|
23
|
+
def initialize(socket)
|
24
|
+
@actor = Actor.new(ZMONITOR_FPTR, socket)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Actor] the actor behind this monitor
|
28
|
+
attr_reader :actor
|
29
|
+
|
30
|
+
# Terminates the monitor.
|
31
|
+
# @return [void]
|
32
|
+
def terminate
|
33
|
+
@actor.terminate
|
34
|
+
end
|
35
|
+
|
36
|
+
# Enable verbose logging of commands and activity.
|
37
|
+
# @return [void]
|
38
|
+
def verbose!
|
39
|
+
@actor << "VERBOSE"
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Array<String>] types of valid events
|
43
|
+
EVENTS = %w[
|
44
|
+
CONNECTED
|
45
|
+
CONNECT_DELAYED
|
46
|
+
CONNECT_RETRIED
|
47
|
+
LISTENING
|
48
|
+
BIND_FAILED
|
49
|
+
ACCEPTED
|
50
|
+
ACCEPT_FAILED
|
51
|
+
CLOSED
|
52
|
+
CLOSE_FAILED
|
53
|
+
DISCONNECTED
|
54
|
+
MONITOR_STOPPED
|
55
|
+
ALL
|
56
|
+
]
|
57
|
+
|
58
|
+
# Configure monitor to listen for specific events.
|
59
|
+
# @param events [String] one or more events from {EVENTS}
|
60
|
+
# @return [void]
|
61
|
+
def listen(*events)
|
62
|
+
events.each do |event|
|
63
|
+
EVENTS.include?(event) or
|
64
|
+
raise ArgumentError, "invalid event: #{event.inspect}"
|
65
|
+
end
|
66
|
+
@actor << [ "LISTEN", *events ]
|
67
|
+
end
|
68
|
+
|
69
|
+
# Start the monitor. After this, you can read events using {#next}.
|
70
|
+
# @return [void]
|
71
|
+
def start
|
72
|
+
@actor << "START"
|
73
|
+
@actor.wait
|
74
|
+
end
|
75
|
+
|
76
|
+
# Get next event. This blocks until the next event is available.
|
77
|
+
# @example
|
78
|
+
# socket = CZTop::Socket::ROUTER.new("tcp://127.0.0.1:5050")
|
79
|
+
# # ... normal stuff with socket
|
80
|
+
#
|
81
|
+
# # do this in another thread, or using a Poller, so it's possible to
|
82
|
+
# # interact with the socket and the monitor
|
83
|
+
# Thread.new do
|
84
|
+
# monitor = CZTop::Monitor.new(socket)
|
85
|
+
# monitor.listen "CONNECTED", "DISCONNECTED"
|
86
|
+
# while event = monitor.next
|
87
|
+
# case event[0]
|
88
|
+
# when "CONNECTED"
|
89
|
+
# puts "a client has connected"
|
90
|
+
# when "DISCONNECTED"
|
91
|
+
# puts "a client has disconnected"
|
92
|
+
# end
|
93
|
+
# end
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# @return [String] one of the events from {EVENTS}, something like
|
97
|
+
# <tt>["ACCEPTED", "73", "tcp://127.0.0.1:55585"]</tt>
|
98
|
+
def next
|
99
|
+
@actor.receive
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/cztop/poller.rb
ADDED
@@ -0,0 +1,334 @@
|
|
1
|
+
module CZTop
|
2
|
+
# A non-trivial socket poller.
|
3
|
+
#
|
4
|
+
# It can poll for reading and writing, and supports getting back an array of
|
5
|
+
# readable/writable sockets after the call to {#wait}. The reason for this
|
6
|
+
# feature is to be able to use it in Celluloid::ZMQ, where in a call to
|
7
|
+
# Celluloid::ZMQ::Reactor#run_once all readable/writable sockets need to be
|
8
|
+
# processed.
|
9
|
+
#
|
10
|
+
# This implementation is NOT based on zpoller. Reasons:
|
11
|
+
#
|
12
|
+
# * zpoller can only poll for reading
|
13
|
+
#
|
14
|
+
# It's also NOT based on `zmq_poller()`. Reasons:
|
15
|
+
#
|
16
|
+
# * zmq_poller() doesn't exist in older versions of ZMQ < 4.2
|
17
|
+
#
|
18
|
+
# Possible future implementation on +zmq_poller()+ might work like this, to
|
19
|
+
# support getting an array of readable/writable sockets:
|
20
|
+
#
|
21
|
+
# * in {#wait}, poll with normal timeout
|
22
|
+
# * then poll again with zero timeout until no more sockets, accumulate
|
23
|
+
# results
|
24
|
+
#
|
25
|
+
# = Limitations
|
26
|
+
#
|
27
|
+
# This poller can't poll for writing on CLIENT/SERVER sockets.
|
28
|
+
# Implementation could be adapted to support them using
|
29
|
+
# {CZTop::Poller::ZPoller}, at least for reading. But it'd make the code
|
30
|
+
# ugly.
|
31
|
+
#
|
32
|
+
class Poller
|
33
|
+
# CZTop's interface to the low-level +zmq_poll()+ function.
|
34
|
+
module ZMQ
|
35
|
+
|
36
|
+
POLL = 1
|
37
|
+
POLLIN = 1
|
38
|
+
POLLOUT = 2
|
39
|
+
POLLERR = 4
|
40
|
+
|
41
|
+
extend ::FFI::Library
|
42
|
+
lib_name = 'libzmq'
|
43
|
+
lib_paths = ['/usr/local/lib', '/opt/local/lib', '/usr/lib64']
|
44
|
+
.map { |path| "#{path}/#{lib_name}.#{::FFI::Platform::LIBSUFFIX}" }
|
45
|
+
ffi_lib lib_paths + [lib_name]
|
46
|
+
|
47
|
+
# Represents a struct of type +zmq_pollitem_t+.
|
48
|
+
class PollItem < FFI::Struct
|
49
|
+
##
|
50
|
+
# shamelessly taken from https://github.com/mtortonesi/ruby-czmq-ffi
|
51
|
+
#
|
52
|
+
|
53
|
+
|
54
|
+
FD_TYPE = if FFI::Platform::IS_WINDOWS && FFI::Platform::ADDRESS_SIZE == 64
|
55
|
+
# On Windows, zmq.h defines fd as a SOCKET, which is 64 bits on x64.
|
56
|
+
:uint64
|
57
|
+
else
|
58
|
+
:int
|
59
|
+
end
|
60
|
+
|
61
|
+
layout :socket, :pointer,
|
62
|
+
:fd, FD_TYPE,
|
63
|
+
:events, :short,
|
64
|
+
:revents, :short
|
65
|
+
|
66
|
+
# @return [Boolean] whether the socket is readable
|
67
|
+
def readable?
|
68
|
+
(self[:revents] & POLLIN) > 0
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Boolean] whether the socket is writable
|
72
|
+
def writable?
|
73
|
+
(self[:revents] & POLLOUT) > 0
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
opts = {
|
78
|
+
blocking: true # only necessary on MRI to deal with the GIL.
|
79
|
+
}
|
80
|
+
|
81
|
+
#ZMQ_EXPORT int zmq_poll (zmq_pollitem_t *items, int nitems, long timeout);
|
82
|
+
attach_function :poll, :zmq_poll, [:pointer, :int, :long], :int, **opts
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param readers [Socket, Actor] sockets to poll for input
|
86
|
+
def initialize(*readers)
|
87
|
+
@readers = {}
|
88
|
+
@writers = {}
|
89
|
+
@readables = []
|
90
|
+
@writables = []
|
91
|
+
@rebuild_needed = true
|
92
|
+
readers.each { |r| add_reader(r) }
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Array<CZTop::Socket>] registered reader sockets
|
96
|
+
def readers
|
97
|
+
@readers.values
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [Array<CZTop::Socket>] registered writer sockets
|
101
|
+
def writers
|
102
|
+
@writers.values
|
103
|
+
end
|
104
|
+
|
105
|
+
# Adds a socket to be polled for reading.
|
106
|
+
# @param socket [Socket, Actor] the socket
|
107
|
+
# @return [void]
|
108
|
+
# @raise [ArgumentError] if it's not a socket
|
109
|
+
def add_reader(socket)
|
110
|
+
raise ArgumentError unless socket.is_a?(Socket) || socket.is_a?(Actor)
|
111
|
+
ptr = CZMQ::FFI::Zsock.resolve(socket) # get low-level handle
|
112
|
+
@readers[ptr.to_i] = socket
|
113
|
+
@rebuild_needed = true
|
114
|
+
end
|
115
|
+
|
116
|
+
# Removes a previously registered reader socket. Won't raise if you're
|
117
|
+
# trying to remove a socket that's not registered.
|
118
|
+
# @param socket [Socket, Actor] the socket
|
119
|
+
# @return [void]
|
120
|
+
# @raise [ArgumentError] if it's not a socket
|
121
|
+
def remove_reader(socket)
|
122
|
+
raise ArgumentError unless socket.is_a?(Socket) || socket.is_a?(Actor)
|
123
|
+
ptr = CZMQ::FFI::Zsock.resolve(socket) # get low-level handle
|
124
|
+
@readers.delete(ptr.to_i) and @rebuild_needed = true
|
125
|
+
end
|
126
|
+
|
127
|
+
# Adds a socket to be polled for writing.
|
128
|
+
# @param socket [Socket, Actor] the socket
|
129
|
+
# @return [void]
|
130
|
+
# @raise [ArgumentError] if it's not a socket
|
131
|
+
def add_writer(socket)
|
132
|
+
raise ArgumentError unless socket.is_a?(Socket) || socket.is_a?(Actor)
|
133
|
+
ptr = CZMQ::FFI::Zsock.resolve(socket) # get low-level handle
|
134
|
+
@writers[ptr.to_i] = socket
|
135
|
+
@rebuild_needed = true
|
136
|
+
end
|
137
|
+
|
138
|
+
# Removes a previously registered writer socket. Won't raise if you're
|
139
|
+
# trying to remove a socket that's not registered.
|
140
|
+
# @param socket [Socket, Actor] the socket
|
141
|
+
# @return [void]
|
142
|
+
# @raise [ArgumentError] if it's not a socket
|
143
|
+
def remove_writer(socket)
|
144
|
+
raise ArgumentError unless socket.is_a?(Socket) || socket.is_a?(Actor)
|
145
|
+
ptr = CZMQ::FFI::Zsock.resolve(socket) # get low-level handle
|
146
|
+
@writers.delete(ptr.to_i) and @rebuild_needed = true
|
147
|
+
end
|
148
|
+
|
149
|
+
# Waits for registered sockets to become readable or writable, depending
|
150
|
+
# on what you're interested in.
|
151
|
+
#
|
152
|
+
# @param timeout [Integer] how long to wait in ms, or 0 to avoid blocking,
|
153
|
+
# or -1 to wait indefinitely
|
154
|
+
# @return [Socket, Actor] the first readable socket
|
155
|
+
# @return [nil] if the timeout expired or
|
156
|
+
# @raise [Interrupt] if the timeout expired or
|
157
|
+
def wait(timeout = -1)
|
158
|
+
rebuild if @rebuild_needed
|
159
|
+
@readables = @writables = nil
|
160
|
+
|
161
|
+
num = ZMQ.poll(@items_ptr, @nitems, timeout)
|
162
|
+
HasFFIDelegate.raise_zmq_err if num == -1
|
163
|
+
|
164
|
+
return nil if num == 0
|
165
|
+
return readables[0] if readables.any?
|
166
|
+
|
167
|
+
# TODO: handle CLIENT/SERVER sockets using ZPoller
|
168
|
+
# if threadsafe_sockets.any?
|
169
|
+
# zpoller.wait(0)
|
170
|
+
# end
|
171
|
+
end
|
172
|
+
|
173
|
+
# @return [Array<CZTop::Socket>] readable sockets (memoized)
|
174
|
+
def readables
|
175
|
+
@readables ||= @reader_items.select(&:readable?).map do |item|
|
176
|
+
ptr = item[:socket]
|
177
|
+
@readers[ ptr.to_i ]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# @return [Array<CZTop::Socket>] writable sockets (memoized)
|
182
|
+
def writables
|
183
|
+
@writables ||= @writer_items.select(&:writable?).map do |item|
|
184
|
+
ptr = item[:socket]
|
185
|
+
@writers[ ptr.to_i ]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
# Rebuilds the list of `poll_item_t`.
|
192
|
+
# @return [void]
|
193
|
+
def rebuild
|
194
|
+
@nitems = @readers.size + @writers.size
|
195
|
+
@items_ptr = FFI::MemoryPointer.new(ZMQ::PollItem, @nitems)
|
196
|
+
@items_ptr.autorelease = true
|
197
|
+
|
198
|
+
# memory addresses
|
199
|
+
mem = Enumerator.new do |y|
|
200
|
+
@nitems.times { |i| y << @items_ptr + i * ZMQ::PollItem.size }
|
201
|
+
end
|
202
|
+
|
203
|
+
@reader_items = @readers.map{|_,s| new_item(mem.next, s, ZMQ::POLLIN) }
|
204
|
+
@writer_items = @writers.map{|_,s| new_item(mem.next, s, ZMQ::POLLOUT) }
|
205
|
+
|
206
|
+
@rebuild_needed = false
|
207
|
+
end
|
208
|
+
|
209
|
+
# @param address [FFI::Pointer] allocated memory address for this item
|
210
|
+
# @param socket [CZTop::Socket] socket we're interested in
|
211
|
+
# @param events [Integer] the events we're interested in
|
212
|
+
# @return [ZMQ::PollItem] a new item for
|
213
|
+
def new_item(address, socket, events)
|
214
|
+
item = ZMQ::PollItem.new(address)
|
215
|
+
item[:socket] = CZMQ::FFI::Zsock.resolve(socket)
|
216
|
+
item[:fd] = 0
|
217
|
+
item[:events] = events
|
218
|
+
item[:revents] = 0
|
219
|
+
item
|
220
|
+
end
|
221
|
+
|
222
|
+
# This is the trivial poller based on zpoller. It only supports polling
|
223
|
+
# for reading, but it also supports doing that on CLIENT/SERVER sockets,
|
224
|
+
# which is useful for {CZTop::Poller}.
|
225
|
+
#
|
226
|
+
# @see http://api.zeromq.org/czmq3-0:zpoller
|
227
|
+
class ZPoller
|
228
|
+
include HasFFIDelegate
|
229
|
+
extend CZTop::HasFFIDelegate::ClassMethods
|
230
|
+
include ::CZMQ::FFI
|
231
|
+
|
232
|
+
# Initializes the Poller. At least one reader has to be given.
|
233
|
+
# @param reader [Socket, Actor] socket to poll for input
|
234
|
+
# @param readers [Socket, Actor] any additional sockets to poll for input
|
235
|
+
def initialize(reader, *readers)
|
236
|
+
@sockets = {} # to keep references and return same instances
|
237
|
+
ptr = Zpoller.new(reader,
|
238
|
+
*readers.flat_map {|r| [ :pointer, r ] },
|
239
|
+
:pointer, nil)
|
240
|
+
attach_ffi_delegate(ptr)
|
241
|
+
remember_socket(reader)
|
242
|
+
readers.each { |r| remember_socket(r) }
|
243
|
+
end
|
244
|
+
|
245
|
+
# Adds another reader socket to the poller.
|
246
|
+
# @param reader [Socket, Actor] socket to poll for input
|
247
|
+
# @return [void]
|
248
|
+
# @raise [SystemCallError] if this fails
|
249
|
+
def add(reader)
|
250
|
+
rc = ffi_delegate.add(reader)
|
251
|
+
raise_zmq_err("unable to add socket %p" % reader) if rc == -1
|
252
|
+
remember_socket(reader)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Removes a reader socket from the poller.
|
256
|
+
# @param reader [Socket, Actor] socket to remove
|
257
|
+
# @return [void]
|
258
|
+
# @raise [ArgumentError] if socket was invalid, e.g. it wasn't registered
|
259
|
+
# in this poller
|
260
|
+
# @raise [SystemCallError] if this fails for another reason
|
261
|
+
def remove(reader)
|
262
|
+
rc = ffi_delegate.remove(reader)
|
263
|
+
raise_zmq_err("unable to remove socket %p" % reader) if rc == -1
|
264
|
+
forget_socket(reader)
|
265
|
+
end
|
266
|
+
|
267
|
+
# Wait and return the first socket that becomes readable.
|
268
|
+
# @param timeout [Integer] how long to wait in ms, or 0 to avoid blocking,
|
269
|
+
# or -1 to wait indefinitely
|
270
|
+
# @return [Socket, Actor]
|
271
|
+
# @return [nil] if the timeout expired or
|
272
|
+
# @raise [Interrupt] if the timeout expired or
|
273
|
+
def wait(timeout = -1)
|
274
|
+
ptr = ffi_delegate.wait(timeout)
|
275
|
+
if ptr.null?
|
276
|
+
raise Interrupt if ffi_delegate.terminated
|
277
|
+
return nil
|
278
|
+
end
|
279
|
+
return socket_by_ptr(ptr)
|
280
|
+
end
|
281
|
+
|
282
|
+
# Tells the zpoller to ignore interrupts. By default, {#wait} will return
|
283
|
+
# immediately if it detects an interrupt (when +zsys_interrupted+ is set
|
284
|
+
# to something other than zero). Calling this method will supress this
|
285
|
+
# behavior.
|
286
|
+
# @return [void]
|
287
|
+
def ignore_interrupts
|
288
|
+
ffi_delegate.ignore_interrupts
|
289
|
+
end
|
290
|
+
|
291
|
+
# By default the poller stops if the process receives a SIGINT or SIGTERM
|
292
|
+
# signal. This makes it impossible to shut-down message based architectures
|
293
|
+
# like zactors. This method lets you switch off break handling. The default
|
294
|
+
# nonstop setting is off (false).
|
295
|
+
#
|
296
|
+
# Setting this will cause {#wait} to never raise.
|
297
|
+
#
|
298
|
+
# @param flag [Boolean] whether the poller should run nonstop
|
299
|
+
def nonstop=(flag)
|
300
|
+
ffi_delegate.set_nonstop(flag)
|
301
|
+
end
|
302
|
+
|
303
|
+
private
|
304
|
+
|
305
|
+
# Remembers the socket so a call to {#wait} can return with the exact same
|
306
|
+
# instance of {Socket}, and it also makes sure the socket won't get
|
307
|
+
# GC'd.
|
308
|
+
# @param socket [Socket, Actor] the socket instance to remember
|
309
|
+
# @return [void]
|
310
|
+
def remember_socket(socket)
|
311
|
+
@sockets[socket.to_ptr.to_i] = socket
|
312
|
+
end
|
313
|
+
|
314
|
+
# Forgets the socket because it has been removed from the poller.
|
315
|
+
# @param socket [Socket, Actor] the socket instance to forget
|
316
|
+
# @return [void]
|
317
|
+
def forget_socket(socket)
|
318
|
+
@sockets.delete(socket.to_ptr.to_i)
|
319
|
+
end
|
320
|
+
|
321
|
+
# Gets the previously remembered socket associated to the given pointer.
|
322
|
+
# @param ptr [FFI::Pointer] the pointer to a socket
|
323
|
+
# @return [Socket, Actor] the socket associated to the given pointer
|
324
|
+
# @raise [SystemCallError] if no socket is registered under given pointer
|
325
|
+
def socket_by_ptr(ptr)
|
326
|
+
@sockets[ptr.to_i] or
|
327
|
+
# NOTE: This should never happen, since #wait will return nil if
|
328
|
+
# +zpoller_wait+ returned NULL. But it's better to fail early in case
|
329
|
+
# it ever returns a wrong pointer.
|
330
|
+
raise_zmq_err("no socket known for pointer #{ptr.inspect}")
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|