cztop 0.1.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.
- 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
|