cztop 1.1.1 → 1.2.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 (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