omq-zstd 0.4.3 → 0.4.5

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: 5a3ed977d7fa2b57910a209fd3db1ca683634a9ccc2d299cd3dd0f95c0030d93
4
- data.tar.gz: 4d439082a1142c335538c3ea3698cc629e6dc9fa406335386171d6457ad2a9e6
3
+ metadata.gz: 4bd678bf2436ab0d01597aaff7d536957432abcd571c36b21b6d9d82eff12061
4
+ data.tar.gz: df584bef85cac3e3e031990bb511675bdb9b4aca50fc9975f869666d26154e47
5
5
  SHA512:
6
- metadata.gz: a68d93c3c116a9fe508844058515f74733a674c6a8a085149b8bff2a8cfb089793509a5d3aed54cc56c3730ecd8386e6eb12ac500cad734e58d8976b6cc674bd
7
- data.tar.gz: 9eb6751a7682a0ebefd9680b9c48f8a1f6c31041e2f276e87064054d6d7094145a8e6a4776579ea33b9d4c10eacd6d4ce6817c6c4cc3e0d4ec1401ecd659a01c
6
+ metadata.gz: 83b6c89aec844c6c14d5afd2161173c3ef9aa915a821d5a05c78610637994fdd68a8d240ef05494136f02c7e4ec27363c5da48f8392dd017d5b728da53114a58
7
+ data.tar.gz: 87c79b3d3a07f4c6e42f823af146be14ab2c91770c3df221a7372e6c4554130fe17d5a4eebf66e9a9ec27b45ee581916ff3eb32362d10d1e21ba5c390d5373d2
data/README.md CHANGED
@@ -7,10 +7,9 @@
7
7
  Experimental Zstandard-compressed TCP transport for
8
8
  [OMQ](https://github.com/zeromq/omq.rb).
9
9
 
10
- Use [`omq-lz4`](../omq-lz4) for new compressed TCP work. OMQ.rs removed
11
- `zstd+tcp://` to reduce transport complexity after lz4rip gained
12
- dictionary training. This gem remains for research, comparison, and
13
- cases where zstd ratio matters more than transport simplicity.
10
+ Use [`omq-lz4`](../omq-lz4) for CPU- or memory-sensitive compressed TCP
11
+ work. This gem remains for research, comparison, OMQ.rs parity, and cases
12
+ where zstd ratio matters more than transport simplicity.
14
13
 
15
14
  Pick `zstd+tcp://` instead of `tcp://` and every message part on the wire is
16
15
  compressed per-part with [Zstandard](https://github.com/facebook/zstd).
@@ -80,11 +79,17 @@ The sender ships the dictionary to the receiver in-band as a one-shot
80
79
  single-part message prefixed with the dictionary sentinel
81
80
  (`37 A4 30 EC`), so the receiver does not need a copy on disk.
82
81
 
83
- **Auto-trained dictionary** (zero config, the default when no `dict:` is
84
- passed): the sender collects up to 1000 samples or 100 KiB (whichever hits
85
- first), skipping samples larger than 2048 bytes. It trains a 2 KiB dictionary,
86
- ships it inline, and switches to dictionary mode. Until then, payloads are
87
- compressed without a dictionary or sent plaintext when below the threshold.
82
+ **Auto-trained dictionary** (opt-in):
83
+
84
+ ```ruby
85
+ push.connect("zstd+tcp://127.0.0.1:5555", auto_dict: true)
86
+ ```
87
+
88
+ The sender collects up to 1000 samples or 100 KiB (whichever hits first),
89
+ skipping samples larger than 2048 bytes. It trains a 2 KiB dictionary, ships
90
+ it inline, and switches to dictionary mode. Until then, payloads are compressed
91
+ without a dictionary or sent plaintext when below the threshold. Automatic
92
+ dictionary training is off by default.
88
93
 
89
94
  ### Compression thresholds
90
95
 
@@ -152,6 +157,7 @@ transport layer. They are not delivered to the application.
152
157
  | Train max bytes | 100 KiB |
153
158
  | Train max sample length | 2048 B |
154
159
  | Dictionary capacity | 2 KiB |
160
+ | Auto dictionary training | Off by default |
155
161
 
156
162
  ## When to use it
157
163
 
data/RFC.md CHANGED
@@ -265,21 +265,26 @@ exceeds the limit MUST be rejected before decoder invocation.
265
265
  ### 7.1 Dictionary message format
266
266
 
267
267
  A dictionary is shipped as a **single-part ZMTP message** (no MORE flag)
268
- whose body begins with the dictionary sentinel:
268
+ whose body is exactly a Zstandard dictionary blob in ZDICT format. ZDICT
269
+ blobs begin with the dictionary magic `37 A4 30 EC`; this transport uses
270
+ those first 4 bytes as the dictionary sentinel:
269
271
 
270
272
  ```
271
273
  +------------------+------------------------+
272
- | 37 A4 30 EC | dictionary bytes |
273
- | (4 bytes) | (D bytes) |
274
+ | 37 A4 30 EC | rest of ZDICT blob |
275
+ | (4 bytes) | (D - 4 bytes) |
274
276
  +------------------+------------------------+
275
277
  ```
276
278
 
277
- The sentinel `37 A4 30 EC` is specific to this specification and has no
278
- relationship to Zstandard's internals. It was chosen to avoid collision
279
- with the Zstandard frame magic and the uncompressed sentinel.
279
+ The sentinel `37 A4 30 EC` is the standard Zstandard dictionary magic
280
+ (`DICT_MAGIC`, little-endian). It is intentionally reused as this
281
+ transport's dictionary discriminator because a ZDICT blob already carries
282
+ it and it does not collide with the Zstandard frame magic or the
283
+ uncompressed sentinel.
280
284
 
281
- The remaining `D` bytes are the raw dictionary as it should be passed
282
- to the Zstandard decoder's dictionary-load operation.
285
+ The complete `D` bytes, including the leading `37 A4 30 EC` magic, are
286
+ the raw dictionary as it should be passed to the Zstandard decoder's
287
+ dictionary-load operation.
283
288
 
284
289
  ### 7.2 Constraints
285
290
 
@@ -287,9 +292,10 @@ to the Zstandard decoder's dictionary-load operation.
287
292
  not set on the frame header). A dictionary sentinel in a multipart
288
293
  message's non-final or non-only part is a protocol error.
289
294
 
290
- - A dictionary message MUST NOT exceed **8 KiB** total (sentinel +
291
- dictionary bytes). A receiver that receives a dictionary message
292
- larger than 8 KiB MUST close the connection.
295
+ - A dictionary message MUST NOT exceed **8 KiB** total. Since the
296
+ message body is exactly the ZDICT blob, this is also the maximum
297
+ dictionary size. A receiver that receives a dictionary message larger
298
+ than 8 KiB MUST close the connection.
293
299
 
294
300
  - A sender MUST send at most **one** dictionary message per direction
295
301
  per connection. A receiver that receives a second dictionary message
@@ -305,9 +311,9 @@ to the Zstandard decoder's dictionary-load operation.
305
311
  When the receiver encounters a dictionary part:
306
312
 
307
313
  1. Validate the constraints in Sec. 7.2.
308
- 2. Strip the 4-byte sentinel.
309
- 3. Install the remaining bytes as the decompression dictionary for this
310
- connection.
314
+ 2. Install the complete message body as the decompression dictionary for
315
+ this connection. The leading `37 A4 30 EC` bytes are part of the ZDICT
316
+ blob and MUST NOT be stripped.
311
317
  4. Discard the message. It is not delivered to the application.
312
318
 
313
319
  If all parts of a ZMTP message are dictionary parts (which is always
@@ -361,9 +367,8 @@ support auto-training:
361
367
  | Max sample length | 2048 bytes |
362
368
  | Training algorithm | FastCOVER |
363
369
 
364
- Whether auto-training is enabled by default is an implementation
365
- choice. Applications MAY disable it or supply an out-of-band
366
- dictionary instead.
370
+ Auto-training SHOULD be disabled by default. Applications MAY enable it
371
+ explicitly or supply an out-of-band dictionary instead.
367
372
 
368
373
  ### 7.6 Dictionary ID
369
374
 
@@ -22,9 +22,14 @@ module OMQ
22
22
  attr_reader :send_dict_bytes, :max_message_size
23
23
 
24
24
 
25
- def initialize(level:, dict: nil, max_message_size: nil)
25
+ def initialize(level:, dict: nil, max_message_size: nil, auto_dict: nil)
26
26
  @level = level
27
27
  @max_message_size = max_message_size
28
+ @dict_capacity = if auto_dict.is_a?(Hash) && auto_dict[:capacity]
29
+ auto_dict[:capacity]
30
+ else
31
+ DICT_CAPACITY
32
+ end
28
33
 
29
34
  # Start with a no-dict FrameCodec. Once a dict is configured
30
35
  # (either via the `dict:` kwarg or via auto-training), this is
@@ -34,7 +39,7 @@ module OMQ
34
39
  @send_codec = Zrip::FrameCodec.new(level: @level)
35
40
  @send_dict_bytes = nil
36
41
 
37
- @training = dict.nil?
42
+ @training = !!auto_dict && dict.nil?
38
43
  @train_samples = []
39
44
  @train_bytes = 0
40
45
 
@@ -102,7 +107,7 @@ module OMQ
102
107
  return unless @train_samples.size >= TRAIN_MAX_SAMPLES ||
103
108
  @train_bytes >= TRAIN_MAX_BYTES
104
109
 
105
- trainer = Zrip::DictTrainer.new(DICT_CAPACITY)
110
+ trainer = Zrip::DictTrainer.new(@dict_capacity)
106
111
  @train_samples.each { |s| trainer.add_sample(s) }
107
112
  trained_bytes = trainer.train
108
113
 
@@ -24,10 +24,18 @@ module OMQ
24
24
  # @param engine [Engine]
25
25
  # @param level [Integer] Zstd compression level
26
26
  # @param dict [String, nil] user-supplied dictionary bytes
27
+ # @param auto_dict [true, Hash, nil] enable automatic dictionary
28
+ # training. Pass `true` for defaults or `{ capacity: N }`.
27
29
  # @return [Listener]
28
30
  #
29
- def listener(endpoint, engine, level: -3, dict: nil, **)
30
- codec = codec_for(engine, level: level, dict: dict)
31
+ def listener(endpoint, engine, level: -3, dict: nil, auto_dict: nil, **)
32
+ validate_auto_dict!(auto_dict, dict)
33
+ codec = codec_for(
34
+ engine,
35
+ level: level,
36
+ dict: dict,
37
+ auto_dict: normalize_auto_dict(auto_dict),
38
+ )
31
39
 
32
40
  host, port = parse_endpoint(endpoint)
33
41
  host = normalize_bind_host(host)
@@ -52,10 +60,18 @@ module OMQ
52
60
  # @param engine [Engine]
53
61
  # @param level [Integer] Zstd compression level
54
62
  # @param dict [String, nil] user-supplied dictionary bytes
63
+ # @param auto_dict [true, Hash, nil] enable automatic dictionary
64
+ # training. Pass `true` for defaults or `{ capacity: N }`.
55
65
  # @return [Dialer]
56
66
  #
57
- def dialer(endpoint, engine, level: -3, dict: nil, **)
58
- codec = codec_for(engine, level: level, dict: dict)
67
+ def dialer(endpoint, engine, level: -3, dict: nil, auto_dict: nil, **)
68
+ validate_auto_dict!(auto_dict, dict)
69
+ codec = codec_for(
70
+ engine,
71
+ level: level,
72
+ dict: dict,
73
+ auto_dict: normalize_auto_dict(auto_dict),
74
+ )
59
75
  Dialer.new(endpoint, engine, codec)
60
76
  end
61
77
 
@@ -107,12 +123,41 @@ module OMQ
107
123
  # @param dict [String, nil]
108
124
  # @return [Codec]
109
125
  #
110
- def codec_for(engine, level:, dict:)
111
- @codecs[engine] ||= Codec.new level: level, dict: dict,
126
+ def validate_auto_dict!(auto_dict, dict)
127
+ return unless auto_dict
128
+
129
+ if dict
130
+ raise ArgumentError, "cannot combine auto_dict: and dict:"
131
+ end
132
+
133
+ return if auto_dict == true
134
+
135
+ unless auto_dict.is_a?(Hash)
136
+ raise TypeError, "auto_dict: must be true or a Hash; got #{auto_dict.class}"
137
+ end
138
+
139
+ cap = auto_dict[:capacity]
140
+ if cap && (cap < 1 || cap > Codec::MAX_DICT_SIZE)
141
+ raise ArgumentError,
142
+ "auto_dict capacity #{cap} out of range [1, #{Codec::MAX_DICT_SIZE}]"
143
+ end
144
+ end
145
+
146
+
147
+ def normalize_auto_dict(auto_dict)
148
+ return unless auto_dict
149
+ return { capacity: Codec::DICT_CAPACITY }.freeze if auto_dict == true
150
+
151
+ { capacity: auto_dict[:capacity] || Codec::DICT_CAPACITY }.freeze
152
+ end
153
+
154
+
155
+ def codec_for(engine, level:, dict:, auto_dict:)
156
+ @codecs[engine] ||= Codec.new level: level, dict: dict, auto_dict: auto_dict,
112
157
  max_message_size: engine.options.max_message_size
113
158
 
114
159
  rescue Ractor::IsolationError
115
- Codec.new level: level, dict: dict,
160
+ Codec.new level: level, dict: dict, auto_dict: auto_dict,
116
161
  max_message_size: engine.options.max_message_size
117
162
  end
118
163
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OMQ
4
4
  module Zstd
5
- VERSION = "0.4.3"
5
+ VERSION = "0.4.5"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq-zstd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 0.1.1
32
+ version: 0.2.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.1.1
39
+ version: 0.2.0
40
40
  description: Experimental zstd+tcp:// endpoint support for OMQ with per-frame Zstd
41
41
  compression, bounded decompression, in-band dictionary shipping, and sender-side
42
42
  dictionary training.