cztop 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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