omq-rfc-zstd 0.1.2 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 141f24afe8fac939ca1518c76d62bc45d8d15d456fa415be3c8dc5343af33447
4
- data.tar.gz: 2e16e4ce5413d0b181e2b293d8b9b5203133bb114527726936c466464754e8dd
3
+ metadata.gz: b2d6625572903ff642e267adb23e6a654eb791a8badb614dcbdf1fdcd90698d6
4
+ data.tar.gz: 5dc1db0f6a9e4622496df5759af9e2d409a0e002f4dd554cb41ed376774370b8
5
5
  SHA512:
6
- metadata.gz: d181d5b3e6c463537599491667287603479d3928b9d2f4991e7c4e48b1ded8e15a730e3a825a059cdf384ded81b5f83e094093d9424341126e103ab3049a3723
7
- data.tar.gz: efd7e9cb3caa37d11ed5cdb6ec374f3f85efd32ce2cfc0525552840d9dce92a0460302e8a315d73ae4b7a5bf34ce597296cf1f60a2d0083e750887635bf633f6
6
+ metadata.gz: be57dd892580e4173561769f624e3aa21759b8d42309f0aecd0c1a64c0abf5ae725718e66589938549a43c8df7664c4bb0fd4c2c7c7d6a643c4591139eeba0c5
7
+ data.tar.gz: 20998b4e1ff50da873096ee0c4c9b95fe5f144b42f01af0bffb40c00034ed2732862885c12837fb9fbf7674de5214db48787c7a75880743ab835adac36581e39
data/README.md CHANGED
@@ -20,7 +20,7 @@ See [RFC.md](RFC.md) for the full specification.
20
20
  - **Backwards compatible**: a peer that does not advertise compression is served plaintext. libzmq and other ZMTP 3.1 peers remain interoperable.
21
21
  - **Small-message friendly**: an optional shared dictionary makes compression useful even for messages in the dozens-to-hundreds-of-bytes range. Without a dictionary, the sender skips compression for frames below 512 B.
22
22
  - **Pay-per-frame**: the sender MAY skip compression on a per-frame basis. Short or incompressible frames are sent plaintext.
23
- - **Zero-config option**: `dict:auto` trains a dictionary from the first 1000 messages (or 100 KiB, whichever hits first) and ships it via the `DICT` command frame.
23
+ - **Zero-config option**: `dict:auto` trains a dictionary from the first 1000 messages (or 100 KiB, whichever hits first) and ships it via the `ZDICT` command frame.
24
24
 
25
25
  ## Non-goals
26
26
 
@@ -38,18 +38,18 @@ require "omq/rfc/zstd"
38
38
 
39
39
  # 1. No dictionary — opportunistic compression for frames ≥ 512 B
40
40
  push = OMQ::PUSH.new
41
- push.compression = OMQ::RFC::Zstd::Compression.none
41
+ push.compression = OMQ::Compression::Zstd.none
42
42
  push.connect("tcp://127.0.0.1:5555")
43
43
 
44
44
  # 2. Caller-supplied dictionary, agreed out of band
45
45
  dict = File.binread("schema.dict")
46
- push.compression = OMQ::RFC::Zstd::Compression.with_dictionary(dict)
46
+ push.compression = OMQ::Compression::Zstd.with_dictionary(dict)
47
47
 
48
48
  # 3. Caller-supplied dictionary, sent over the wire once via DICT command
49
- push.compression = OMQ::RFC::Zstd::Compression.with_dictionary(dict, inline: true)
49
+ push.compression = OMQ::Compression::Zstd.with_dictionary(dict, inline: true)
50
50
 
51
51
  # 4. Auto-trained dictionary — zero config
52
- push.compression = OMQ::RFC::Zstd::Compression.auto
52
+ push.compression = OMQ::Compression::Zstd.auto
53
53
  ```
54
54
 
55
55
  The default level is **3**. Pass `level:` to override (negative levels enable
@@ -4,7 +4,7 @@ require "rzstd"
4
4
  require_relative "constants"
5
5
 
6
6
  module OMQ
7
- module RFC
7
+ module Compression
8
8
  module Zstd
9
9
  # Pure-function frame-body codec. Implements the sender/receiver rules
10
10
  # from RFC sections 6.4 and 6.5. Stateless: all state (dictionary,
@@ -18,7 +18,7 @@ module OMQ
18
18
  # message-frame body.
19
19
  #
20
20
  # @param plaintext [String] frame payload as supplied by the user
21
- # @param compression [OMQ::RFC::Zstd::Compression] the negotiated
21
+ # @param compression [OMQ::Compression::Zstd::Compression] the negotiated
22
22
  # send-direction compression object, or nil if no profile is active
23
23
  # @return [String] frame body bytes (always binary)
24
24
  def encode_part(plaintext, compression)
@@ -51,7 +51,7 @@ module OMQ
51
51
  # or raises before any decoder allocation.
52
52
  #
53
53
  # @param body [String] wire frame body bytes
54
- # @param compression [OMQ::RFC::Zstd::Compression] the negotiated
54
+ # @param compression [OMQ::Compression::Zstd::Compression] the negotiated
55
55
  # recv-direction compression object, or nil if no profile is active
56
56
  # @param budget_remaining [Integer, nil] remaining decompressed-byte
57
57
  # budget for this multipart message; nil disables the cap
@@ -89,9 +89,11 @@ module OMQ
89
89
  def enforce_budget!(size, budget_remaining)
90
90
  return if budget_remaining.nil?
91
91
  return if size <= budget_remaining
92
+
92
93
  raise DecompressedSizeExceedsMaxError,
93
- "ZMTP-Zstd: decompressed message size exceeds maximum"
94
+ "ZMTP-Zstd: decompressed message size exceeds maximum"
94
95
  end
96
+
95
97
  end
96
98
  end
97
99
  end
@@ -7,10 +7,29 @@ require_relative "constants"
7
7
  require "thread"
8
8
 
9
9
  module OMQ
10
- module RFC
10
+ module Compression
11
11
  module Zstd
12
+
13
+ def self.none(level: DEFAULT_LEVEL, passive: false)
14
+ Compressor.new mode: :none, dictionary: nil, level: level, passive: passive
15
+ end
16
+
17
+
18
+ def self.with_dictionary(bytes, inline: false, level: DEFAULT_LEVEL, passive: false)
19
+ Compressor.new mode: inline ? :dict_inline : :dict_static,
20
+ dictionary: bytes,
21
+ level: level,
22
+ passive: passive
23
+ end
24
+
25
+
26
+ def self.auto(level: DEFAULT_LEVEL, passive: false)
27
+ Compressor.new mode: :dict_auto, dictionary: nil, level: level, passive: passive
28
+ end
29
+
30
+
12
31
  # User-facing configuration object. Assigned to an OMQ socket via
13
- # `socket.compression = OMQ::RFC::Zstd::Compression.new(...)`.
32
+ # `socket.compression = OMQ::Compression::Zstd.none/auto/with_dictionary(...)`.
14
33
  # Encapsulates the negotiated profile, the dictionaries (per
15
34
  # direction), the compression level, and the cached SHA-1 hash
16
35
  # used in the READY property.
@@ -38,25 +57,9 @@ module OMQ
38
57
  # (whichever comes first), installs it into the send
39
58
  # slot, and ships it via ZDICT. The recv slot is
40
59
  # populated when the peer's ZDICT arrives.
41
- class Compression
60
+ class Compressor
42
61
  attr_reader :sentinel, :profile, :level, :mode, :send_dict_bytes
43
62
 
44
- def self.none(level: DEFAULT_LEVEL, passive: false)
45
- new(mode: :none, dictionary: nil, level: level, passive: passive)
46
- end
47
-
48
- def self.with_dictionary(bytes, inline: false, level: DEFAULT_LEVEL, passive: false)
49
- new(
50
- mode: inline ? :dict_inline : :dict_static,
51
- dictionary: bytes,
52
- level: level,
53
- passive: passive,
54
- )
55
- end
56
-
57
- def self.auto(level: DEFAULT_LEVEL, passive: false)
58
- new(mode: :dict_auto, dictionary: nil, level: level, passive: passive)
59
- end
60
63
 
61
64
  # When +passive: true+, the socket advertises the profile and
62
65
  # decodes incoming compressed frames, but never compresses
@@ -99,14 +102,17 @@ module OMQ
99
102
  end
100
103
  end
101
104
 
105
+
102
106
  def has_send_dictionary?
103
107
  !@send_dictionary.nil?
104
108
  end
105
109
 
110
+
106
111
  def has_recv_dictionary?
107
112
  !@recv_dictionary.nil?
108
113
  end
109
114
 
115
+
110
116
  # True if this side was configured as a passive sender
111
117
  # (RFC Sec. 6.4 "Passive senders"): advertise the profile and
112
118
  # decompress incoming frames, but never compress outgoing
@@ -117,11 +123,13 @@ module OMQ
117
123
  @passive == true
118
124
  end
119
125
 
126
+
120
127
  def min_compress_bytes
121
128
  return Float::INFINITY if passive?
122
129
  has_send_dictionary? ? MIN_COMPRESS_BYTES_DICT : MIN_COMPRESS_BYTES_NO_DICT
123
130
  end
124
131
 
132
+
125
133
  def compress(plaintext)
126
134
  if @send_dictionary
127
135
  @send_dictionary.compress(plaintext)
@@ -130,6 +138,7 @@ module OMQ
130
138
  end
131
139
  end
132
140
 
141
+
133
142
  # Bounded single-shot decompression. The `max_output_size:` cap is
134
143
  # enforced inside the Rust extension: the frame's Frame_Content_Size
135
144
  # header is read first, and MissingContentSizeError /
@@ -143,6 +152,7 @@ module OMQ
143
152
  end
144
153
  end
145
154
 
155
+
146
156
  # Match this compression's advertised profile against a peer's
147
157
  # X-Compression property value (comma-separated profile list).
148
158
  # Returns the matched profile string, or nil for no match.
@@ -152,6 +162,7 @@ module OMQ
152
162
  peer_profiles.include?(@profile) ? @profile : nil
153
163
  end
154
164
 
165
+
155
166
  # Install a dictionary into the send slot. Used internally by
156
167
  # auto-mode after training: the trained dict is installed here
157
168
  # and the bytes stashed for shipping via ZDICT.
@@ -160,8 +171,9 @@ module OMQ
160
171
  @send_dictionary = RZstd::Dictionary.new(@send_dict_bytes, level: @level)
161
172
  end
162
173
 
174
+
163
175
  # Install a dictionary into the recv slot. Called by the
164
- # CompressionConnection wrapper when a ZDICT command frame
176
+ # Connection wrapper when a ZDICT command frame
165
177
  # arrives from the peer.
166
178
  def install_recv_dictionary(bytes)
167
179
  @recv_dictionary = RZstd::Dictionary.new(bytes.b, level: @level)
@@ -214,8 +226,10 @@ module OMQ
214
226
  end
215
227
  end
216
228
 
229
+
217
230
  private
218
231
 
232
+
219
233
  def maybe_train!
220
234
  return unless @samples_count >= AUTO_DICT_SAMPLE_COUNT ||
221
235
  @samples_bytes >= AUTO_DICT_SAMPLE_BYTES
@@ -232,6 +246,7 @@ module OMQ
232
246
  @samples = nil
233
247
  end
234
248
  end
249
+
235
250
  end
236
251
  end
237
252
  end
@@ -6,8 +6,9 @@ require_relative "codec"
6
6
  require_relative "constants"
7
7
 
8
8
  module OMQ
9
- module RFC
9
+ module Compression
10
10
  module Zstd
11
+
11
12
  # Wraps a Protocol::ZMTP::Connection to transparently apply the
12
13
  # ZMTP-Zstd sender/receiver rules (RFC sections 6.4 and 6.5) at
13
14
  # the frame-body level.
@@ -25,7 +26,9 @@ module OMQ
25
26
  # different profile or dictionary), so the fan-out optimization
26
27
  # in +OMQ::Routing::FanOut+ falls back to per-connection
27
28
  # +write_message+.
28
- class CompressionConnection < SimpleDelegator
29
+ #
30
+ class Connection < SimpleDelegator
31
+
29
32
  # Mixed-in so +is_a?+ against Protocol::ZMTP::Connection still
30
33
  # matches the wrapped instance.
31
34
  module TransparentDelegator
@@ -35,12 +38,14 @@ module OMQ
35
38
  alias_method :kind_of?, :is_a?
36
39
  end
37
40
 
41
+
38
42
  include TransparentDelegator
39
43
 
44
+
40
45
  # @param conn [Protocol::ZMTP::Connection] underlying connection
41
- # @param send_compression [OMQ::RFC::Zstd::Compression, nil]
46
+ # @param send_compression [OMQ::Compression::Zstd::Compressor, nil]
42
47
  # compression object used on outgoing parts; nil = no-op
43
- # @param recv_compression [OMQ::RFC::Zstd::Compression, nil]
48
+ # @param recv_compression [OMQ::Compression::Zstd::Compressor, nil]
44
49
  # compression object used on incoming parts; nil = no-op
45
50
  # @param max_message_size [Integer, nil]
46
51
  def initialize(conn, send_compression:, recv_compression:, max_message_size: nil, engine: nil)
@@ -56,7 +61,7 @@ module OMQ
56
61
  # Cached once: is the send side an auto-training compression
57
62
  # that still needs samples? Flipped false the moment training
58
63
  # completes, so #encode_parts drops the per-message branch.
59
- @auto_sampling = send_compression.is_a?(Compression) && send_compression.auto?
64
+ @auto_sampling = send_compression.is_a?(Compressor) && send_compression.auto?
60
65
  end
61
66
 
62
67
 
@@ -120,20 +125,27 @@ module OMQ
120
125
  end
121
126
 
122
127
 
123
- # Sends the DICT command frame if the send-side compression has
128
+ # Sends the ZDICT command frame if the send-side compression has
124
129
  # an inline dictionary to ship. Called by EngineExt right after
125
130
  # the wrapper is constructed, before the recv pump starts.
126
131
  def send_initial_dict!
127
132
  return if @dict_sent
128
133
  return unless @send_compression
134
+
129
135
  # RFC Sec. 6.4: a passive sender MUST NOT emit a ZDICT frame.
130
- return if @send_compression.respond_to?(:passive?) && @send_compression.passive?
136
+ if @send_compression.respond_to?(:passive?) && @send_compression.passive?
137
+ return
138
+ end
139
+
131
140
  bytes = @send_compression.send_dict_bytes
132
141
  return unless bytes
142
+
133
143
  if bytes.bytesize > DICT_FRAME_MAX_SIZE
134
144
  raise Error, "ZMTP-Zstd: dictionary exceeds DICT_FRAME_MAX_SIZE (#{bytes.bytesize} > #{DICT_FRAME_MAX_SIZE})"
135
145
  end
146
+
136
147
  __getobj__.send_command(Protocol::ZMTP::Codec::Command.new("ZDICT", bytes))
148
+
137
149
  @dict_sent = true
138
150
  @engine&.emit_verbose_monitor_event(:zdict_sent, size: bytes.bytesize)
139
151
  end
@@ -143,6 +155,7 @@ module OMQ
143
155
 
144
156
  def handle_command_frame(frame)
145
157
  cmd = Protocol::ZMTP::Codec::Command.from_body(frame.body)
158
+
146
159
  case cmd.name
147
160
  when "ZDICT"
148
161
  install_received_dict(cmd.data)
@@ -152,9 +165,11 @@ module OMQ
152
165
 
153
166
  def install_received_dict(bytes)
154
167
  return unless @recv_compression
168
+
155
169
  if bytes.bytesize > DICT_FRAME_MAX_SIZE
156
170
  raise Error, "ZMTP-Zstd: received DICT exceeds DICT_FRAME_MAX_SIZE"
157
171
  end
172
+
158
173
  @recv_compression.install_recv_dictionary(bytes)
159
174
  @engine&.emit_verbose_monitor_event(:zdict_received, size: bytes.bytesize)
160
175
  end
@@ -194,6 +209,7 @@ module OMQ
194
209
  plaintext
195
210
  end
196
211
  end
212
+
197
213
  end
198
214
  end
199
215
  end
@@ -3,7 +3,7 @@
3
3
  require "protocol/zmtp"
4
4
 
5
5
  module OMQ
6
- module RFC
6
+ module Compression
7
7
  module Zstd
8
8
  SENTINEL_UNCOMPRESSED = "\x00\x00\x00\x00".b.freeze
9
9
  SENTINEL_ZSTD_FRAME = "\x28\xB5\x2F\xFD".b.freeze
@@ -5,14 +5,16 @@ require_relative "connection"
5
5
  require_relative "constants"
6
6
 
7
7
  module OMQ
8
- module RFC
8
+ module Compression
9
9
  module Zstd
10
+
10
11
  # Prepended onto OMQ::Engine to install a compression-aware
11
12
  # connection wrapper. The wrapper is installed unconditionally
12
13
  # at engine init time and inspects +options.compression+ on each
13
14
  # call -- the user typically sets +socket.compression =+ AFTER
14
15
  # the engine has been constructed, so the closure must look up
15
16
  # the compression object lazily.
17
+ #
16
18
  module EngineExt
17
19
  def initialize(socket_type, options)
18
20
  super
@@ -21,7 +23,7 @@ module OMQ
21
23
  next conn unless compression
22
24
  next conn unless matched_profile(conn, compression)
23
25
 
24
- wrapper = CompressionConnection.new(
26
+ wrapper = Connection.new(
25
27
  conn,
26
28
  send_compression: compression,
27
29
  recv_compression: compression,
@@ -33,17 +35,20 @@ module OMQ
33
35
  end
34
36
  end
35
37
 
38
+
36
39
  private
37
40
 
41
+
38
42
  def matched_profile(conn, compression)
39
43
  props = conn.peer_properties
40
44
  return nil unless props
41
45
  peer_value = props[PROPERTY_NAME]
42
46
  compression.match(peer_value)
43
47
  end
48
+
44
49
  end
45
50
  end
46
51
  end
47
52
  end
48
53
 
49
- OMQ::Engine.prepend(OMQ::RFC::Zstd::EngineExt)
54
+ OMQ::Engine.prepend(OMQ::Compression::Zstd::EngineExt)
@@ -4,15 +4,20 @@ require "omq"
4
4
  require_relative "constants"
5
5
 
6
6
  module OMQ
7
- module RFC
7
+ module Compression
8
8
  module Zstd
9
+
9
10
  # Prepended onto OMQ::Options to add a +compression+ attribute.
10
- # Mirrors the pattern used by omq-transport-tls for +tls_context+.
11
11
  #
12
12
  # Setting +compression+ also publishes the profile string on the
13
13
  # mechanism's metadata so the ZMTP READY command advertises
14
14
  # +X-Compression+ to the peer during handshake.
15
+ #
15
16
  module OptionsExt
17
+
18
+ attr_reader :compression
19
+
20
+
16
21
  def initialize(**kwargs)
17
22
  super
18
23
  @compression = nil
@@ -33,11 +38,9 @@ module OMQ
33
38
  mech.metadata[PROPERTY_NAME] = value.profile
34
39
  end
35
40
 
36
-
37
- attr_reader :compression
38
41
  end
39
42
  end
40
43
  end
41
44
  end
42
45
 
43
- OMQ::Options.prepend(OMQ::RFC::Zstd::OptionsExt)
46
+ OMQ::Options.prepend(OMQ::Compression::Zstd::OptionsExt)
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
- module RFC
4
+ module Compression
5
5
  module Zstd
6
- VERSION = "0.1.2"
6
+ VERSION = "0.2.0"
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "zstd/version"
4
+ require_relative "zstd/constants"
5
+ require_relative "zstd/codec"
6
+ require_relative "zstd/compressor"
7
+ require_relative "zstd/options_ext"
8
+ require_relative "zstd/socket_ext"
9
+ require_relative "zstd/connection"
10
+ require_relative "zstd/engine_ext"
data/lib/omq/rfc/zstd.rb CHANGED
@@ -1,10 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "zstd/version"
4
- require_relative "zstd/constants"
5
- require_relative "zstd/codec"
6
- require_relative "zstd/compression"
7
- require_relative "zstd/options_ext"
8
- require_relative "zstd/socket_ext"
9
- require_relative "zstd/connection"
10
- require_relative "zstd/engine_ext"
3
+ require_relative "../compression/zstd"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq-rfc-zstd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -62,15 +62,16 @@ files:
62
62
  - LICENSE
63
63
  - README.md
64
64
  - RFC.md
65
+ - lib/omq/compression/zstd.rb
66
+ - lib/omq/compression/zstd/codec.rb
67
+ - lib/omq/compression/zstd/compressor.rb
68
+ - lib/omq/compression/zstd/connection.rb
69
+ - lib/omq/compression/zstd/constants.rb
70
+ - lib/omq/compression/zstd/engine_ext.rb
71
+ - lib/omq/compression/zstd/options_ext.rb
72
+ - lib/omq/compression/zstd/socket_ext.rb
73
+ - lib/omq/compression/zstd/version.rb
65
74
  - lib/omq/rfc/zstd.rb
66
- - lib/omq/rfc/zstd/codec.rb
67
- - lib/omq/rfc/zstd/compression.rb
68
- - lib/omq/rfc/zstd/connection.rb
69
- - lib/omq/rfc/zstd/constants.rb
70
- - lib/omq/rfc/zstd/engine_ext.rb
71
- - lib/omq/rfc/zstd/options_ext.rb
72
- - lib/omq/rfc/zstd/socket_ext.rb
73
- - lib/omq/rfc/zstd/version.rb
74
75
  homepage: https://github.com/paddor/omq-rfc-zstd
75
76
  licenses:
76
77
  - ISC
File without changes