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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +31 -0
  5. data/.yardopts +1 -0
  6. data/AUTHORS +1 -0
  7. data/CHANGES.md +3 -0
  8. data/Gemfile +10 -0
  9. data/Guardfile +61 -0
  10. data/LICENSE +5 -0
  11. data/Procfile +3 -0
  12. data/README.md +408 -0
  13. data/Rakefile +6 -0
  14. data/bin/console +7 -0
  15. data/bin/setup +7 -0
  16. data/ci-scripts/install-deps +9 -0
  17. data/cztop.gemspec +36 -0
  18. data/examples/ruby_actor/actor.rb +100 -0
  19. data/examples/simple_req_rep/rep.rb +12 -0
  20. data/examples/simple_req_rep/req.rb +35 -0
  21. data/examples/taxi_system/.gitignore +2 -0
  22. data/examples/taxi_system/Makefile +2 -0
  23. data/examples/taxi_system/README.gsl +115 -0
  24. data/examples/taxi_system/README.md +276 -0
  25. data/examples/taxi_system/broker.rb +98 -0
  26. data/examples/taxi_system/client.rb +34 -0
  27. data/examples/taxi_system/generate_keys.rb +24 -0
  28. data/examples/taxi_system/start_broker.sh +2 -0
  29. data/examples/taxi_system/start_clients.sh +11 -0
  30. data/lib/cztop/actor.rb +308 -0
  31. data/lib/cztop/authenticator.rb +97 -0
  32. data/lib/cztop/beacon.rb +96 -0
  33. data/lib/cztop/certificate.rb +176 -0
  34. data/lib/cztop/config/comments.rb +66 -0
  35. data/lib/cztop/config/serialization.rb +82 -0
  36. data/lib/cztop/config/traversing.rb +157 -0
  37. data/lib/cztop/config.rb +119 -0
  38. data/lib/cztop/frame.rb +158 -0
  39. data/lib/cztop/has_ffi_delegate.rb +85 -0
  40. data/lib/cztop/message/frames.rb +74 -0
  41. data/lib/cztop/message.rb +191 -0
  42. data/lib/cztop/monitor.rb +102 -0
  43. data/lib/cztop/poller.rb +334 -0
  44. data/lib/cztop/polymorphic_zsock_methods.rb +24 -0
  45. data/lib/cztop/proxy.rb +149 -0
  46. data/lib/cztop/send_receive_methods.rb +35 -0
  47. data/lib/cztop/socket/types.rb +207 -0
  48. data/lib/cztop/socket.rb +106 -0
  49. data/lib/cztop/version.rb +3 -0
  50. data/lib/cztop/z85.rb +157 -0
  51. data/lib/cztop/zsock_options.rb +334 -0
  52. data/lib/cztop.rb +55 -0
  53. data/perf/README.md +79 -0
  54. data/perf/inproc_lat.rb +49 -0
  55. data/perf/inproc_thru.rb +42 -0
  56. data/perf/local_lat.rb +35 -0
  57. data/perf/remote_lat.rb +26 -0
  58. 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
@@ -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