cztop 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +16 -0
  3. data/README.md +61 -56
  4. data/cztop.gemspec +2 -4
  5. data/lib/cztop/actor.rb +26 -22
  6. data/lib/cztop/certificate.rb +19 -11
  7. data/lib/cztop/message.rb +5 -1
  8. data/lib/cztop/send_receive_methods.rb +106 -2
  9. data/lib/cztop/socket/types.rb +18 -0
  10. data/lib/cztop/version.rb +1 -1
  11. data/lib/cztop/zsock_options.rb +16 -2
  12. metadata +3 -66
  13. data/.github/workflows/coverage.yml +0 -20
  14. data/.github/workflows/draft_api.yml +0 -27
  15. data/.github/workflows/stable_api.yml +0 -26
  16. data/.gitignore +0 -10
  17. data/.projections.json +0 -4
  18. data/.rspec +0 -2
  19. data/.rubocop.yml +0 -175
  20. data/.yardopts +0 -1
  21. data/Rakefile +0 -6
  22. data/ci/install-libczmq +0 -22
  23. data/ci/install-libzmq +0 -22
  24. data/examples/async/.gitignore +0 -1
  25. data/examples/async/Gemfile +0 -5
  26. data/examples/async/README.md +0 -1
  27. data/examples/async/async.rb +0 -35
  28. data/examples/ruby_actor/actor.rb +0 -100
  29. data/examples/simple_req_rep/rep.rb +0 -12
  30. data/examples/simple_req_rep/req.rb +0 -35
  31. data/examples/taxi_system/.gitignore +0 -2
  32. data/examples/taxi_system/Makefile +0 -2
  33. data/examples/taxi_system/README.gsl +0 -115
  34. data/examples/taxi_system/README.md +0 -276
  35. data/examples/taxi_system/broker.rb +0 -97
  36. data/examples/taxi_system/client.rb +0 -34
  37. data/examples/taxi_system/generate_keys.rb +0 -24
  38. data/examples/taxi_system/start_broker.sh +0 -2
  39. data/examples/taxi_system/start_clients.sh +0 -11
  40. data/examples/weather_pub_sub/pub.rb +0 -24
  41. data/examples/weather_pub_sub/sub.rb +0 -33
  42. data/lib/cztop/async.rb +0 -124
  43. data/perf/README.md +0 -80
  44. data/perf/inproc_lat.rb +0 -49
  45. data/perf/inproc_thru.rb +0 -42
  46. data/perf/local_lat.rb +0 -35
  47. data/perf/remote_lat.rb +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 066d9600730db24e49b9b83fee30840882432df13dde7372f9e7f969b3774b33
4
- data.tar.gz: 67ee0941443c6a9eadf7d27b4bc084e00d20c2dc7ca4448fc6459edc93a88efb
3
+ metadata.gz: 0a5218cd3606b597506aa29ea2bc0ccfe2ccd8030f1a0ce1c6ea90791fe7c88e
4
+ data.tar.gz: 3ad76dccf7e6bad9aff9a19f679bb3420dcf08ec742b109ff6c0a688f3fce4e4
5
5
  SHA512:
6
- metadata.gz: c8852f4b214935ef5b0302a7a6481a2f51148dfc6fac939ca9d9b5f665ab06d8242418bdbe996edb9ad6fe010a84a540fd9b24e379cdd5b3197fd3c7a80f0478
7
- data.tar.gz: 506f6e6b9a927e8c68d60c410d3ed84abfae1edef435c43baae8855eb2f02409815692eb4cf18501298ab5c8f9c988cdd48d308a93eb20d1ae136bf51c8283fe
6
+ metadata.gz: 36ac2f1e2b1f0ddfb78f3cd77fc7fbfdb78591c1c379b3fb96b92d80566fc8e07e39f666c78b1b2aa6771fcd2fb22d6e6b7a173040b9c489053af27030689220
7
+ data.tar.gz: ef8061d7082e73bf757335d5c6cd550aeb39dfbdb54314b893e44350e9065ea96874d09b89720fbbe4b98873a0e1b575f76f91266025dcb2d5d45b88b5c11c90
data/CHANGES.md CHANGED
@@ -1,3 +1,19 @@
1
+ 1.2.0 (1/7/2024)
2
+ -----
3
+ * refactor waiting for socket readability/writability
4
+ * fix ROUTER socket hanging in busy loop if ZMQ_ROUTER_MANDATORY flag is set and sending a message while no peers are connected
5
+ * same for unconnected CLIENT sockets
6
+ * slim down packaged gem
7
+ * modernize examples
8
+ * require Ruby 3.2
9
+ * some exceptions changed from EAGAIN to IO::TimeoutError
10
+ * Certificate.load and .new_from raise NotImplementedError if CURVE is not available
11
+
12
+ 1.1.2 (1/5/2024)
13
+ -----
14
+ * refactor to make code Fiber Scheduler agnostic
15
+ * remove Async::IO::CZTopSocket
16
+
1
17
  1.1.1 (1/4/2024)
2
18
  -----
3
19
  * speed up Async::IO#wait_readable and #wait_writable
data/README.md CHANGED
@@ -13,22 +13,22 @@ mechanisms (like CURVE).
13
13
 
14
14
  ## Example with Async
15
15
 
16
+ See [this example](https://github.com/paddor/cztop/blob/master/examples/async.rb):
17
+
16
18
  ```ruby
17
19
  #! /usr/bin/env ruby
18
20
 
19
- require 'cztop/async'
21
+ require 'cztop'
20
22
 
21
23
  Async do |task|
22
24
  task.async do |t|
23
25
  socket = CZTop::Socket::REP.new("inproc://req_rep_example")
24
- io = Async::IO.try_convert socket
25
-
26
26
  socket.options.rcvtimeo = 50 # ms
27
27
 
28
28
  loop do
29
- msg = io.receive
29
+ msg = socket.receive
30
30
  puts "<<< #{msg.to_a.inspect}"
31
- io << msg.to_a.map(&:upcase)
31
+ socket << msg.to_a.map(&:upcase)
32
32
  rescue IO::TimeoutError
33
33
  break
34
34
  end
@@ -38,11 +38,10 @@ Async do |task|
38
38
 
39
39
  task.async do
40
40
  socket = CZTop::Socket::REQ.new("inproc://req_rep_example")
41
- io = Async::IO.try_convert socket
42
41
 
43
42
  10.times do |i|
44
- io << "foobar ##{i}"
45
- msg = io.receive
43
+ socket << "foobar ##{i}"
44
+ msg = socket.receive
46
45
  puts ">>> #{msg.to_a.inspect}"
47
46
  end
48
47
 
@@ -54,6 +53,8 @@ end
54
53
 
55
54
  Output:
56
55
  ```
56
+ $ cd examples
57
+ $ time ./async.rb
57
58
  <<< ["foobar #0"]
58
59
  >>> ["FOOBAR #0"]
59
60
  <<< ["foobar #1"]
@@ -76,61 +77,20 @@ Output:
76
77
  >>> ["FOOBAR #9"]
77
78
  REQ done.
78
79
  REP done.
79
- 0.46user 0.09system 0:00.60elapsed 90%CPU (0avgtext+0avgdata 47296maxresident)k
80
- 0inputs+0outputs (0major+13669minor)pagefaults 0swaps
81
- ```
82
-
83
- ## Overview
84
-
85
- ### Class Hierarchy
86
80
 
87
- Here's an overview of the core classes:
81
+ ________________________________________________________
82
+ Executed in 401.51 millis fish external
83
+ usr time 308.44 millis 605.00 micros 307.83 millis
84
+ sys time 40.08 millis 278.00 micros 39.81 millis
88
85
 
89
- * [CZTop](http://www.rubydoc.info/gems/cztop/CZTop)
90
- * [Actor](http://www.rubydoc.info/gems/cztop/CZTop)
91
- * [Authenticator](http://www.rubydoc.info/gems/cztop/CZTop/Authenticator)
92
- * [Beacon](http://www.rubydoc.info/gems/cztop/CZTop/Beacon)
93
- * [Certificate](http://www.rubydoc.info/gems/cztop/CZTop/Certificate)
94
- * [CertStore](http://www.rubydoc.info/gems/cztop/CZTop/CertStore)
95
- * [Config](http://www.rubydoc.info/gems/cztop/CZTop/Config)
96
- * [Frame](http://www.rubydoc.info/gems/cztop/CZTop/Frame)
97
- * [Message](http://www.rubydoc.info/gems/cztop/CZTop/Message)
98
- * [Monitor](http://www.rubydoc.info/gems/cztop/CZTop/Monitor)
99
- * [Metadata](http://www.rubydoc.info/gems/cztop/CZTop/Metadata)
100
- * [Proxy](http://www.rubydoc.info/gems/cztop/CZTop/Proxy)
101
- * [Poller](http://www.rubydoc.info/gems/cztop/CZTop/Poller) (based on `zmq_poller_*()` functions)
102
- * [Aggregated](http://www.rubydoc.info/gems/cztop/CZTop/Poller/Aggregated)
103
- * [ZPoller](http://www.rubydoc.info/gems/cztop/CZTop/Poller/ZPoller)
104
- * [Socket](http://www.rubydoc.info/gems/cztop/CZTop/Socket)
105
- * [REQ](http://www.rubydoc.info/gems/cztop/CZTop/Socket/REQ) < Socket
106
- * [REP](http://www.rubydoc.info/gems/cztop/CZTop/Socket/REP) < Socket
107
- * [ROUTER](http://www.rubydoc.info/gems/cztop/CZTop/Socket/ROUTER) < Socket
108
- * [DEALER](http://www.rubydoc.info/gems/cztop/CZTop/Socket/DEALER) < Socket
109
- * [PUSH](http://www.rubydoc.info/gems/cztop/CZTop/Socket/PUSH) < Socket
110
- * [PULL](http://www.rubydoc.info/gems/cztop/CZTop/Socket/PULL) < Socket
111
- * [PUB](http://www.rubydoc.info/gems/cztop/CZTop/Socket/PUB) < Socket
112
- * [SUB](http://www.rubydoc.info/gems/cztop/CZTop/Socket/SUB) < Socket
113
- * [XPUB](http://www.rubydoc.info/gems/cztop/CZTop/Socket/XPUB) < Socket
114
- * [XSUB](http://www.rubydoc.info/gems/cztop/CZTop/Socket/XSUB) < Socket
115
- * [PAIR](http://www.rubydoc.info/gems/cztop/CZTop/Socket/PAIR) < Socket
116
- * [STREAM](http://www.rubydoc.info/gems/cztop/CZTop/Socket/STREAM) < Socket
117
- * [CLIENT](http://www.rubydoc.info/gems/cztop/CZTop/Socket/CLIENT) < Socket
118
- * [SERVER](http://www.rubydoc.info/gems/cztop/CZTop/Socket/SERVER) < Socket
119
- * [RADIO](http://www.rubydoc.info/gems/cztop/CZTop/Socket/RADIO) < Socket
120
- * [DISH](http://www.rubydoc.info/gems/cztop/CZTop/Socket/DISH) < Socket
121
- * [SCATTER](http://www.rubydoc.info/gems/cztop/CZTop/Socket/SCATTER) < Socket
122
- * [GATHER](http://www.rubydoc.info/gems/cztop/CZTop/Socket/GATHER) < Socket
123
- * [Z85](http://www.rubydoc.info/gems/cztop/CZTop/Z85)
124
- * [Padded](http://www.rubydoc.info/gems/cztop/CZTop/Z85/Padded) < Z85
125
- * [Pipe](http://www.rubydoc.info/gems/cztop/CZTop/Z85/Pipe)
126
- * [ZAP](http://www.rubydoc.info/gems/cztop/CZTop/ZAP)
86
+ ```
127
87
 
128
- More information in the [API documentation](http://www.rubydoc.info/github/paddor/cztop).
88
+ ## Overview
129
89
 
130
90
  ### Features
131
91
 
132
92
  * Ruby idiomatic API
133
- * compatible with [Async](https://github.com/socketry/async) / [Async::IO](https://github.com/socketry/async-io)
93
+ * Fiber Scheduler aware
134
94
  * errors as exceptions
135
95
  * CURVE security
136
96
  * supports CZMQ DRAFT API
@@ -175,6 +135,51 @@ Or install it yourself as:
175
135
 
176
136
  $ gem install cztop
177
137
 
138
+ ### Class Hierarchy
139
+
140
+ Here's an overview of the core classes:
141
+
142
+ * [CZTop](http://www.rubydoc.info/gems/cztop/CZTop)
143
+ * [Actor](http://www.rubydoc.info/gems/cztop/CZTop)
144
+ * [Authenticator](http://www.rubydoc.info/gems/cztop/CZTop/Authenticator)
145
+ * [Beacon](http://www.rubydoc.info/gems/cztop/CZTop/Beacon)
146
+ * [Certificate](http://www.rubydoc.info/gems/cztop/CZTop/Certificate)
147
+ * [CertStore](http://www.rubydoc.info/gems/cztop/CZTop/CertStore)
148
+ * [Config](http://www.rubydoc.info/gems/cztop/CZTop/Config)
149
+ * [Frame](http://www.rubydoc.info/gems/cztop/CZTop/Frame)
150
+ * [Message](http://www.rubydoc.info/gems/cztop/CZTop/Message)
151
+ * [Monitor](http://www.rubydoc.info/gems/cztop/CZTop/Monitor)
152
+ * [Metadata](http://www.rubydoc.info/gems/cztop/CZTop/Metadata)
153
+ * [Proxy](http://www.rubydoc.info/gems/cztop/CZTop/Proxy)
154
+ * [Poller](http://www.rubydoc.info/gems/cztop/CZTop/Poller) (based on `zmq_poller_*()` functions)
155
+ * [Aggregated](http://www.rubydoc.info/gems/cztop/CZTop/Poller/Aggregated)
156
+ * [ZPoller](http://www.rubydoc.info/gems/cztop/CZTop/Poller/ZPoller)
157
+ * [Socket](http://www.rubydoc.info/gems/cztop/CZTop/Socket)
158
+ * [REQ](http://www.rubydoc.info/gems/cztop/CZTop/Socket/REQ) < Socket
159
+ * [REP](http://www.rubydoc.info/gems/cztop/CZTop/Socket/REP) < Socket
160
+ * [ROUTER](http://www.rubydoc.info/gems/cztop/CZTop/Socket/ROUTER) < Socket
161
+ * [DEALER](http://www.rubydoc.info/gems/cztop/CZTop/Socket/DEALER) < Socket
162
+ * [PUSH](http://www.rubydoc.info/gems/cztop/CZTop/Socket/PUSH) < Socket
163
+ * [PULL](http://www.rubydoc.info/gems/cztop/CZTop/Socket/PULL) < Socket
164
+ * [PUB](http://www.rubydoc.info/gems/cztop/CZTop/Socket/PUB) < Socket
165
+ * [SUB](http://www.rubydoc.info/gems/cztop/CZTop/Socket/SUB) < Socket
166
+ * [XPUB](http://www.rubydoc.info/gems/cztop/CZTop/Socket/XPUB) < Socket
167
+ * [XSUB](http://www.rubydoc.info/gems/cztop/CZTop/Socket/XSUB) < Socket
168
+ * [PAIR](http://www.rubydoc.info/gems/cztop/CZTop/Socket/PAIR) < Socket
169
+ * [STREAM](http://www.rubydoc.info/gems/cztop/CZTop/Socket/STREAM) < Socket
170
+ * [CLIENT](http://www.rubydoc.info/gems/cztop/CZTop/Socket/CLIENT) < Socket
171
+ * [SERVER](http://www.rubydoc.info/gems/cztop/CZTop/Socket/SERVER) < Socket
172
+ * [RADIO](http://www.rubydoc.info/gems/cztop/CZTop/Socket/RADIO) < Socket
173
+ * [DISH](http://www.rubydoc.info/gems/cztop/CZTop/Socket/DISH) < Socket
174
+ * [SCATTER](http://www.rubydoc.info/gems/cztop/CZTop/Socket/SCATTER) < Socket
175
+ * [GATHER](http://www.rubydoc.info/gems/cztop/CZTop/Socket/GATHER) < Socket
176
+ * [Z85](http://www.rubydoc.info/gems/cztop/CZTop/Z85)
177
+ * [Padded](http://www.rubydoc.info/gems/cztop/CZTop/Z85/Padded) < Z85
178
+ * [Pipe](http://www.rubydoc.info/gems/cztop/CZTop/Z85/Pipe)
179
+ * [ZAP](http://www.rubydoc.info/gems/cztop/CZTop/ZAP)
180
+
181
+ More information in the [API documentation](http://www.rubydoc.info/github/paddor/cztop).
182
+
178
183
  ## Documentation
179
184
 
180
185
  The API should be fairly straight-forward to anyone who is familiar with CZMQ
data/cztop.gemspec CHANGED
@@ -12,13 +12,13 @@ Gem::Specification.new do |spec|
12
12
  spec.description = 'CZMQ binding based on the generated low-level FFI bindings of CZMQ'
13
13
  spec.homepage = "https://rubygems.org/gems/cztop"
14
14
  spec.license = "ISC"
15
- spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = "https://github.com/paddor/cztop"
19
19
  spec.metadata["changelog_uri"] = "https://github.com/paddor/cztop/blob/master/CHANGELOG.md"
20
20
 
21
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match?(%r{^(\.|Rakefile|spec/|examples/|ci/|perf/)}) }
22
22
  spec.bindir = "exe"
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ["lib"]
@@ -32,10 +32,8 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency "rspec-given", "~> 3.8.0"
33
33
  spec.add_development_dependency "pry"
34
34
  spec.add_development_dependency "yard"
35
- spec.add_development_dependency "rubocop", "~> 1.36.0"
36
35
 
37
36
  if RUBY_VERSION >= '3.1'
38
37
  spec.add_development_dependency "async", ">= 2.0.1"
39
- spec.add_development_dependency "async-io"
40
38
  end
41
39
  end
data/lib/cztop/actor.rb CHANGED
@@ -12,7 +12,7 @@ module CZTop
12
12
  #
13
13
  # = About termination
14
14
  # Actors should be terminated explicitly, either by calling {#terminate}
15
- # from the current process or sending them the "$TERM" command (from
15
+ # from the current process or sending them the {TERMINATE} command (from
16
16
  # outside). Not terminating them explicitly might make the process block at
17
17
  # exit.
18
18
  #
@@ -48,12 +48,20 @@ module CZTop
48
48
  class DeadActorError < RuntimeError; end
49
49
 
50
50
 
51
+ # the command which causes an actor handler to terminate
52
+ TERMINATE = '$TERM'
53
+
54
+
55
+ # timeout to use when sending the actor a message
56
+ SEND_TIMEOUT = 20 # ms
57
+
58
+
51
59
  # @return [Exception] the exception that crashed this actor, if any
52
60
  attr_reader :exception
53
61
 
54
62
 
55
63
  # Creates a new actor. Either pass a callback directly or a block. The
56
- # block will be called for every received message.
64
+ # callback/block will be called for every received message.
57
65
  #
58
66
  # In case the given callback is an FFI::Pointer (to a C function), it's
59
67
  # used as-is. It is expected to do the handshake (signal) itself.
@@ -71,8 +79,9 @@ module CZTop
71
79
  @callback = callback || handler
72
80
  @callback = shim(@callback) unless @callback.is_a? ::FFI::Pointer
73
81
  ffi_delegate = Zactor.new(@callback, c_args)
82
+
74
83
  attach_ffi_delegate(ffi_delegate)
75
- options.sndtimeo = 20 # ms # see #<<
84
+ options.sndtimeo = SEND_TIMEOUT # see #<<
76
85
  end
77
86
 
78
87
 
@@ -83,11 +92,11 @@ module CZTop
83
92
  # @raise [IO::EAGAINWaitWritable, RuntimeError] anything that could be
84
93
  # raised by {Message#send_to}
85
94
  # @note Normally this method is asynchronous, but if the message is
86
- # "$TERM", it blocks until the actor is terminated.
95
+ # {TERMINATE}, it blocks until the actor is terminated.
87
96
  def <<(message)
88
97
  message = Message.coerce(message)
89
98
 
90
- if TERM == message[0]
99
+ if TERMINATE == message[0]
91
100
  # NOTE: can't just send this to the actor. The sender might call
92
101
  # #terminate immediately, which most likely causes a hang due to race
93
102
  # conditions.
@@ -99,7 +108,7 @@ module CZTop
99
108
 
100
109
  message.send_to(self)
101
110
  end
102
- rescue IO::EAGAINWaitWritable
111
+ rescue IO::EAGAINWaitWritable, IO::TimeoutError
103
112
  # The sndtimeo has been reached.
104
113
  #
105
114
  # This should fix the race condition (mainly on JRuby) between
@@ -111,9 +120,8 @@ module CZTop
111
120
  # at least when using a Ruby handler.
112
121
  #
113
122
  # In case of a C function handler, it MUST NOT crash and only
114
- # terminate when being sent the "$TERM" message using #terminate (so
115
- # #await_handler_death can set
116
- # @running to false).
123
+ # terminate when being sent the {TERMINATE} message using #terminate (so
124
+ # #await_handler_death can set @running to false).
117
125
  retry
118
126
  end
119
127
  end
@@ -138,13 +146,13 @@ module CZTop
138
146
  # it.
139
147
  # @param message [Message] the request to the actor
140
148
  # @return [Message] the actor's response
141
- # @raise [ArgumentError] if the message is "$TERM" (use {#terminate})
149
+ # @raise [ArgumentError] if the message is {TERMINATE} (use {#terminate})
142
150
  def request(message)
143
151
  @mtx.synchronize do
144
152
  raise DeadActorError unless @running
145
153
 
146
154
  message = Message.coerce(message)
147
- raise ArgumentError, 'use #terminate' if TERM == message[0]
155
+ raise ArgumentError, 'use #terminate' if TERMINATE == message[0]
148
156
 
149
157
  message.send_to(self)
150
158
  Message.receive_from(self)
@@ -189,11 +197,11 @@ module CZTop
189
197
  @mtx.synchronize do
190
198
  return false unless @running
191
199
 
192
- Message.new(TERM).send_to(self)
200
+ Message.new(TERMINATE).send_to(self)
193
201
  await_handler_death
194
202
  true
195
203
  end
196
- rescue IO::EAGAINWaitWritable
204
+ rescue IO::EAGAINWaitWritable, IO::TimeoutError
197
205
  # same as in #<<
198
206
  retry
199
207
  end
@@ -236,8 +244,8 @@ module CZTop
236
244
  end
237
245
 
238
246
  process_messages(handler)
239
- rescue Exception
240
- @exception = $ERROR_INFO
247
+ rescue Exception => e
248
+ @exception = e
241
249
  ensure
242
250
  signal_shimmed_handler_death
243
251
  end
@@ -251,16 +259,12 @@ module CZTop
251
259
  end
252
260
 
253
261
 
254
- # the command which causes an actor handler to terminate
255
- TERM = '$TERM'
256
-
257
-
258
262
  # Successively receive messages that were sent to the actor and
259
263
  # yield them to the given handler to process them. The a pipe (a
260
264
  # {Socket::PAIR} socket) is also passed to the handler so it can send back
261
265
  # the result of a command, if needed.
262
266
  #
263
- # When a message is "$TERM", or when the waiting for a message is
267
+ # When a message is {TERMINATE}, or when the waiting for a message is
264
268
  # interrupted, execution is aborted and the actor will terminate.
265
269
  #
266
270
  # @param handler [Proc, #call] the handler used to process messages
@@ -274,7 +278,7 @@ module CZTop
274
278
  rescue Interrupt
275
279
  break
276
280
  else
277
- break if TERM == message[0]
281
+ break if TERMINATE == message[0]
278
282
  end
279
283
 
280
284
  handler.call(message, @pipe)
@@ -294,7 +298,7 @@ module CZTop
294
298
  #
295
299
  # This is needed to avoid the race condition between zactor_destroy()
296
300
  # which will wait for a signal from the handler in case it was able to
297
- # send the "$TERM" command, and the @callback which might still haven't
301
+ # send the {TERMINATE} command, and the @callback which might still haven't
298
302
  # returned, but doesn't receive any messages anymore.
299
303
  #
300
304
  # @return [void]
@@ -8,19 +8,15 @@ module CZTop
8
8
  extend CZTop::HasFFIDelegate::ClassMethods
9
9
  include ::CZMQ::FFI
10
10
 
11
- unless ::CZMQ::FFI::Zsys.has_curve
12
- def self.new(...)
13
- fail NotImplementedError
14
- end
15
- end
16
-
17
-
18
11
  # Warns if CURVE security isn't available.
19
- # @return [void]
12
+ # @return [Boolean] whether it's available
20
13
  def self.check_curve_availability
21
- return if Zsys.has_curve
22
-
23
- warn "CZTop: CURVE isn't available. Consider installing libsodium."
14
+ if Zsys.has_curve
15
+ true
16
+ else
17
+ warn "CZTop: CURVE isn't available. Consider installing libsodium."
18
+ false
19
+ end
24
20
  end
25
21
 
26
22
 
@@ -57,6 +53,18 @@ module CZTop
57
53
  from_ffi_delegate(ptr)
58
54
  end
59
55
 
56
+ unless ::CZMQ::FFI::Zsys.has_curve
57
+ def self.new(...)
58
+ fail NotImplementedError
59
+ end
60
+ def self.load(...)
61
+ fail NotImplementedError
62
+ end
63
+ def self.new_from(...)
64
+ fail NotImplementedError
65
+ end
66
+ end
67
+
60
68
 
61
69
  # Initialize a new in-memory certificate with random keys.
62
70
  def initialize
data/lib/cztop/message.rb CHANGED
@@ -49,7 +49,7 @@ module CZTop
49
49
  # @raise [IO::EAGAINWaitWritable] if the send timeout has been reached
50
50
  # (see {ZsockOptions::OptionsAccessor#sndtimeo=})
51
51
  # @raise [SocketError] if the message can't be routed to the destination
52
- # (either if ROUTER_MANDATORY flag is set on a {Socket::ROUTER} socket
52
+ # (either if ZMQ_ROUTER_MANDATORY flag is set on a {Socket::ROUTER} socket
53
53
  # and the peer isn't connected or its SNDHWM is reached (see
54
54
  # {ZsockOptions::OptionsAccessor#router_mandatory=}, or if it's
55
55
  # a {Socket::SERVER} socket and there's no connected CLIENT
@@ -61,6 +61,8 @@ module CZTop
61
61
  # returns with failure. Please report as bug.
62
62
  #
63
63
  def send_to(destination)
64
+ destination.wait_writable
65
+
64
66
  rc = Zmsg.send(ffi_delegate, destination)
65
67
  return if rc.zero?
66
68
 
@@ -79,6 +81,8 @@ module CZTop
79
81
  # @raise [SystemCallError] for any other error code set after +zmsg_recv+
80
82
  # returns with failure. Please report as bug.
81
83
  def self.receive_from(source)
84
+ source.wait_readable
85
+
82
86
  delegate = Zmsg.recv(source)
83
87
  return from_ffi_delegate(delegate) unless delegate.null?
84
88
 
@@ -10,7 +10,7 @@ module CZTop
10
10
  # Sends a message.
11
11
  #
12
12
  # @param message [Message, String, Array<parts>] the message to send
13
- # @raise [IO::EAGAINWaitWritable] if send timeout has been reached (see
13
+ # @raise [IO::EAGAINWaitWritable, IO::TimeoutError] if send timeout has been reached (see
14
14
  # {ZsockOptions::OptionsAccessor#sndtimeo=})
15
15
  # @raise [Interrupt, ArgumentError, SystemCallError] anything raised by
16
16
  # {Message#send_to}
@@ -26,7 +26,7 @@ module CZTop
26
26
  # Receives a message.
27
27
  #
28
28
  # @return [Message]
29
- # @raise [IO::EAGAINWaitReadable] if receive timeout has been reached (see
29
+ # @raise [IO::EAGAINWaitReadable, IO::TimeoutError] if receive timeout has been reached (see
30
30
  # {ZsockOptions::OptionsAccessor#rcvtimeo=})
31
31
  # @raise [Interrupt, ArgumentError, SystemCallError] anything raised by
32
32
  # {Message.receive_from}
@@ -35,5 +35,109 @@ module CZTop
35
35
  Message.receive_from(self)
36
36
  end
37
37
 
38
+
39
+ JIFFY = 0.015 # 15 ms
40
+
41
+
42
+ # Waits for socket to become readable.
43
+ # @param timeout [Numeric, nil] timeout in seconds
44
+ # @return [true] if readable within timeout
45
+ # @raise [IO::EAGAINWaitReadable, IO::TimeoutError] if timeout has been reached
46
+ def wait_readable(timeout = read_timeout)
47
+ return true if readable?
48
+
49
+ @fd_io ||= to_io
50
+
51
+ if timeout
52
+ timeout_at = now + timeout
53
+
54
+ while true
55
+ @fd_io.wait_readable(timeout)
56
+ break if readable? # NOTE: ZMQ FD can't be trusted
57
+ raise ::IO::TimeoutError if now >= timeout_at
58
+
59
+ # HACK for edge case: avoid hogging CPU if FD for socket type doesn't block and just insists
60
+ sleep JIFFY
61
+ end
62
+ else
63
+ until readable?
64
+ @fd_io.wait_readable
65
+
66
+ # HACK for edge case: avoid hogging CPU if FD for socket type doesn't block and just insists
67
+ sleep JIFFY
68
+ end
69
+ end
70
+
71
+ true
72
+ end
73
+
74
+
75
+ # Waits for socket to become writable.
76
+ # @param timeout [Numeric, nil] timeout in seconds
77
+ # @return [true] if writable within timeout
78
+ # @raise [IO::EAGAINWaitReadable, IO::TimeoutError] if timeout has been reached
79
+ def wait_writable(timeout = write_timeout)
80
+ return true if writable?
81
+
82
+ @fd_io ||= to_io
83
+
84
+ if timeout
85
+ timeout_at = now + timeout
86
+
87
+ while true
88
+ @fd_io.wait_writable(timeout)
89
+ break if writable? # NOTE: ZMQ FD can't be trusted
90
+ raise ::IO::TimeoutError if now >= timeout_at
91
+
92
+ # HACK for edge case: avoid hogging CPU if FD for socket type doesn't block and just insists
93
+ sleep JIFFY
94
+ end
95
+ else
96
+ until writable?
97
+ @fd_io.wait_writable
98
+
99
+ # HACK for edge case: avoid hogging CPU if FD for socket type doesn't block and just insists
100
+ sleep JIFFY
101
+ end
102
+ end
103
+
104
+ true
105
+ end
106
+
107
+
108
+ # @return [Float, nil] the timeout in seconds used by {IO#wait_readable}
109
+ def read_timeout
110
+ timeout = options.rcvtimeo
111
+
112
+ if timeout <= 0
113
+ timeout = nil
114
+ else
115
+ timeout = timeout.to_f / 1000
116
+ end
117
+
118
+ timeout
119
+ end
120
+
121
+
122
+ # @return [Float, nil] the timeout in seconds used by {IO#wait_writable}
123
+ def write_timeout
124
+ timeout = options.sndtimeo
125
+
126
+ if timeout <= 0
127
+ timeout = nil
128
+ else
129
+ timeout = timeout.to_f / 1000
130
+ end
131
+
132
+ timeout
133
+ end
134
+
135
+
136
+ private
137
+
138
+
139
+ def now
140
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
141
+ end
38
142
  end
39
143
  end
@@ -76,6 +76,15 @@ module CZTop
76
76
  attach_ffi_delegate(Zsock.new_client(endpoints))
77
77
  end
78
78
 
79
+
80
+ # @raise [SocketError] if no peer is connected
81
+ def wait_writable(...)
82
+ if !writable?
83
+ fail SocketError, "no peer connected"
84
+ end
85
+
86
+ super
87
+ end
79
88
  end
80
89
 
81
90
 
@@ -160,6 +169,15 @@ module CZTop
160
169
  self << message
161
170
  end
162
171
 
172
+
173
+ # @raise [SocketError] if ZMQ_ROUTER_MANDATORY option and message is currently not routable
174
+ def wait_writable(...)
175
+ if options.router_mandatory? && !writable?
176
+ fail SocketError, "no peer connected"
177
+ end
178
+
179
+ super
180
+ end
163
181
  end
164
182
 
165
183
 
data/lib/cztop/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module CZTop
4
4
 
5
- VERSION = '1.1.1'
5
+ VERSION = '1.2.0'
6
6
 
7
7
  end
@@ -43,6 +43,12 @@ module CZTop
43
43
  end
44
44
 
45
45
 
46
+ # @return [IO] IO for FD
47
+ def to_io
48
+ IO.for_fd fd, autoclose: false
49
+ end
50
+
51
+
46
52
  # Used to access the options of a {Socket} or {Actor}.
47
53
  class OptionsAccessor
48
54
 
@@ -319,11 +325,19 @@ module CZTop
319
325
 
320
326
  # @!endgroup
321
327
 
322
- # Accept only routable messages on ROUTER sockets. Default is off.
323
- # @param bool [Boolean] whether to error if a message isn't routable
328
+ # ZMQ_ROUTER_MANDATORY: Accept only routable messages on ROUTER sockets. Default is off.
329
+ # @param bool [Boolean] whether to raise a SocketError if a message isn't routable
324
330
  # (either if the that peer isn't connected or its SNDHWM is reached)
331
+ # @see https://libzmq.readthedocs.io/en/latest/zmq_setsockopt.html#_zmq_router_mandatory_accept_only_routable_messages_on_router_sockets
325
332
  def router_mandatory=(bool)
326
333
  Zsock.set_router_mandatory(@zocket, bool ? 1 : 0)
334
+ @router_mandatory = bool # NOTE: no way to read this option, so we need to remember
335
+ end
336
+
337
+
338
+ # @return [Boolean] whether ZMQ_ROUTER_MANDATORY has been set
339
+ def router_mandatory?
340
+ @router_mandatory
327
341
  end
328
342
 
329
343