protocol-zmtp 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8774efa1927a863efbec0173b00b4642fe99f809ee0df07e3ab6a164829909e
4
- data.tar.gz: a77071e52f4461323f7d839738ad56d0ed83af312ae1071b4f88ffb365ee2514
3
+ metadata.gz: 0f82192f566af0256273b9dd794ad7032c5b1f3d1b2f8a58c37235847715f6fc
4
+ data.tar.gz: b74e78954d38be3f239d96cbcac676dc564b9ab56dff16f1beff8fbcedb77b8b
5
5
  SHA512:
6
- metadata.gz: c0cc5712a1c5aabd5f360ea5fe052bb45e963f645551f0bea2a46f62d3d43d4f9f8a0a0cd3a2de63e563641d2463586b0574016db6ab394a66b78a0c7bd93924
7
- data.tar.gz: 87ae97e2a6bc9448b16a9053d959e7b7707639bb15ad168bc020f83eae0d95309b82be6a0772a38d6e56307038de013d7544e836094c5d52b4dfb114175b5a28
6
+ metadata.gz: 985cc52f11cc1ad7d24bdbec3916ee5485bda72a396413b3735254ae97430117b40ae3fd6331107e953c0b99bc3c05a997c368eeae642e64b023dd3d744381eb
7
+ data.tar.gz: 100df0f00b447bbc6799c4468b040f15e8fe633bf25d736b44931683772d5f52a50fa2646e8ce3f4768f6076d47482a9a004944f55f42a3d35720ca3f6ede27e
@@ -52,6 +52,11 @@ module Protocol
52
52
  @mutex = Mutex.new
53
53
  @max_message_size = max_message_size
54
54
  @last_received_at = nil
55
+
56
+ # Reusable scratch buffer for frame headers. Array#pack(buffer:)
57
+ # writes in place so the per-message 2-or-9 byte String allocation
58
+ # in write_frames disappears on the hot send path.
59
+ @header_buf = String.new(capacity: 9, encoding: Encoding::BINARY)
55
60
  end
56
61
 
57
62
 
@@ -90,9 +95,11 @@ module Protocol
90
95
  # @param parts [Array<String>] message frames
91
96
  # @return [void]
92
97
  def send_message(parts)
93
- @mutex.synchronize do
94
- write_frames(parts)
95
- @io.flush
98
+ with_deferred_cancel do
99
+ @mutex.synchronize do
100
+ write_frames(parts)
101
+ @io.flush
102
+ end
96
103
  end
97
104
  end
98
105
 
@@ -103,8 +110,33 @@ module Protocol
103
110
  # @param parts [Array<String>] message frames
104
111
  # @return [void]
105
112
  def write_message(parts)
106
- @mutex.synchronize do
107
- write_frames(parts)
113
+ with_deferred_cancel do
114
+ @mutex.synchronize do
115
+ write_frames(parts)
116
+ end
117
+ end
118
+ end
119
+
120
+
121
+ # Writes a batch of multi-frame messages to the buffer under a
122
+ # single mutex acquisition. Used by work-stealing send pumps that
123
+ # dequeue up to N messages at once — avoids the N lock/unlock
124
+ # pairs per batch that a plain `batch.each { write_message }`
125
+ # would incur.
126
+ #
127
+ # @param messages [Array<Array<String>>] each element is one
128
+ # multi-frame message
129
+ # @return [void]
130
+ def write_messages(messages)
131
+ with_deferred_cancel do
132
+ @mutex.synchronize do
133
+ i = 0
134
+ n = messages.size
135
+ while i < n
136
+ write_frames(messages[i])
137
+ i += 1
138
+ end
139
+ end
108
140
  end
109
141
  end
110
142
 
@@ -115,8 +147,10 @@ module Protocol
115
147
  # @param wire_bytes [String] ZMTP wire-format bytes
116
148
  # @return [void]
117
149
  def write_wire(wire_bytes)
118
- @mutex.synchronize do
119
- @io.write(wire_bytes)
150
+ with_deferred_cancel do
151
+ @mutex.synchronize do
152
+ @io.write(wire_bytes)
153
+ end
120
154
  end
121
155
  end
122
156
 
@@ -165,13 +199,15 @@ module Protocol
165
199
  # @param command [Codec::Command]
166
200
  # @return [void]
167
201
  def send_command(command)
168
- @mutex.synchronize do
169
- if @mechanism.encrypted?
170
- @io.write(@mechanism.encrypt(command.to_body, command: true))
171
- else
172
- @io.write(command.to_frame.to_wire)
202
+ with_deferred_cancel do
203
+ @mutex.synchronize do
204
+ if @mechanism.encrypted?
205
+ @io.write(@mechanism.encrypt(command.to_body, command: true))
206
+ else
207
+ @io.write(command.to_frame.to_wire)
208
+ end
209
+ @io.flush
173
210
  end
174
- @io.flush
175
211
  end
176
212
  end
177
213
 
@@ -240,14 +276,53 @@ module Protocol
240
276
 
241
277
  private
242
278
 
279
+ # Defers task cancellation around a block of wire writes so the
280
+ # peer never sees a half-written frame. Without this, an
281
+ # +Async::Cancel+ arriving between the header write and the body
282
+ # write (the unencrypted path issues two separate +@io.write+
283
+ # calls per frame) would desync the peer's framer
284
+ # unrecoverably.
285
+ #
286
+ # When called outside an Async task (test fixtures, blocking
287
+ # callers), the block runs directly -- there is no task to defer
288
+ # on. Cancellation arriving from inside the block (peer
289
+ # disconnect raising +EPIPE+/+EOFError+) propagates normally.
290
+ def with_deferred_cancel
291
+ if defined?(Async::Task) && (task = Async::Task.current?)
292
+ task.defer_cancel { yield }
293
+ else
294
+ yield
295
+ end
296
+ end
297
+
298
+
243
299
  # Writes message parts as ZMTP frames, encrypting if needed.
300
+ #
301
+ # For the unencrypted path, writes the frame header and body
302
+ # separately to the IO instead of allocating a wire String. This
303
+ # avoids copying the body just to glue a 1- or 9-byte header onto
304
+ # it -- significant for large messages where the body copy was
305
+ # the dominant allocation per send.
244
306
  def write_frames(parts)
307
+ encrypted = @mechanism.encrypted?
308
+ buf = @header_buf
309
+
245
310
  parts.each_with_index do |part, i|
246
311
  more = i < parts.size - 1
247
- if @mechanism.encrypted?
312
+ if encrypted
248
313
  @io.write(@mechanism.encrypt(part.b, more: more))
249
314
  else
250
- @io.write(Codec::Frame.new(part, more: more).to_wire)
315
+ body = part.b
316
+ size = body.bytesize
317
+ flags = more ? Codec::Frame::FLAGS_MORE : 0
318
+ buf.clear
319
+ if size > Codec::Frame::SHORT_MAX
320
+ [flags | Codec::Frame::FLAGS_LONG, size].pack("CQ>", buffer: buf)
321
+ else
322
+ [flags, size].pack("CC", buffer: buf)
323
+ end
324
+ @io.write(buf)
325
+ @io.write(body)
251
326
  end
252
327
  end
253
328
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Protocol
4
4
  module ZMTP
5
- VERSION = "0.3.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-zmtp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger