omq-zstd 0.4.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 +7 -0
- data/DESIGN.md +162 -0
- data/LICENSE +15 -0
- data/README.md +163 -0
- data/RFC.md +453 -0
- data/lib/omq/transport/zstd_tcp/codec.rb +163 -0
- data/lib/omq/transport/zstd_tcp/connection.rb +162 -0
- data/lib/omq/transport/zstd_tcp/transport.rb +253 -0
- data/lib/omq/transport/zstd_tcp.rb +26 -0
- data/lib/omq/zstd/version.rb +7 -0
- data/lib/omq/zstd.rb +4 -0
- metadata +79 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
module OMQ
|
|
6
|
+
module Transport
|
|
7
|
+
module ZstdTcp
|
|
8
|
+
class ZstdConnection < SimpleDelegator
|
|
9
|
+
# @return [Integer, nil] wire bytesize of the last received message
|
|
10
|
+
#
|
|
11
|
+
attr_reader :last_wire_size_in
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def initialize(conn, codec)
|
|
15
|
+
super(conn)
|
|
16
|
+
@codec = codec
|
|
17
|
+
@dict_shipped = false
|
|
18
|
+
@recv_dict = nil
|
|
19
|
+
@last_wire_size_in = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def send_message(parts)
|
|
24
|
+
compressed = @codec.compress_parts(parts)
|
|
25
|
+
ship_dict!
|
|
26
|
+
__getobj__.send_message(compressed)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_message(parts)
|
|
31
|
+
compressed = @codec.compress_parts(parts)
|
|
32
|
+
ship_dict!
|
|
33
|
+
__getobj__.write_message(compressed)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def write_messages(messages)
|
|
38
|
+
compressed = messages.map { |parts| @codec.compress_parts(parts) }
|
|
39
|
+
ship_dict!
|
|
40
|
+
__getobj__.write_messages(compressed)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def receive_message
|
|
45
|
+
loop do
|
|
46
|
+
parts = __getobj__.receive_message
|
|
47
|
+
decoded = decode_parts(parts)
|
|
48
|
+
if decoded
|
|
49
|
+
@last_wire_size_in = parts.sum { |p| p.bytesize }
|
|
50
|
+
return decoded
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def respond_to?(name, include_private = false)
|
|
57
|
+
return false if name == :write_wire
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def ship_dict!
|
|
66
|
+
return if @dict_shipped
|
|
67
|
+
|
|
68
|
+
dict_bytes = @codec.send_dict_bytes
|
|
69
|
+
return unless dict_bytes
|
|
70
|
+
|
|
71
|
+
__getobj__.write_message([dict_bytes])
|
|
72
|
+
@dict_shipped = true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def decode_parts(parts)
|
|
77
|
+
budget = @codec.max_message_size
|
|
78
|
+
decoded = []
|
|
79
|
+
all_dicts = true
|
|
80
|
+
|
|
81
|
+
parts.each do |wire|
|
|
82
|
+
plaintext = decode_part(wire, budget)
|
|
83
|
+
if plaintext
|
|
84
|
+
all_dicts = false
|
|
85
|
+
budget -= plaintext.bytesize if budget
|
|
86
|
+
decoded << plaintext
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
all_dicts ? nil : decoded
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def decode_part(wire, budget)
|
|
95
|
+
raise ProtocolError, "short frame" if wire.bytesize < 4
|
|
96
|
+
|
|
97
|
+
head = wire.byteslice(0, 4)
|
|
98
|
+
|
|
99
|
+
case head
|
|
100
|
+
when Codec::NUL_PREAMBLE
|
|
101
|
+
plaintext = wire.byteslice(4, wire.bytesize - 4) || "".b
|
|
102
|
+
enforce_budget!(plaintext.bytesize, budget)
|
|
103
|
+
plaintext
|
|
104
|
+
when Codec::ZSTD_MAGIC
|
|
105
|
+
decode_zstd_frame(wire, budget)
|
|
106
|
+
when Codec::ZDICT_MAGIC
|
|
107
|
+
install_recv_dict(wire)
|
|
108
|
+
nil
|
|
109
|
+
else
|
|
110
|
+
raise ProtocolError, "unrecognized preamble: #{head.unpack1('H*')}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def decode_zstd_frame(wire, budget)
|
|
116
|
+
fcs = @codec.parse_frame_content_size(wire)
|
|
117
|
+
raise ProtocolError, "Zstd frame missing Frame_Content_Size" if fcs.nil?
|
|
118
|
+
|
|
119
|
+
if budget && fcs > budget
|
|
120
|
+
raise ProtocolError, "declared FCS #{fcs} exceeds limit #{budget}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
decompress_opts = budget ? { max_output_size: budget } : {}
|
|
124
|
+
|
|
125
|
+
if @recv_dict
|
|
126
|
+
@recv_dict.decompress(wire, **decompress_opts)
|
|
127
|
+
else
|
|
128
|
+
RZstd.decompress(wire, **decompress_opts)
|
|
129
|
+
end
|
|
130
|
+
rescue RZstd::DecompressError => e
|
|
131
|
+
raise ProtocolError, "decompression failed: #{e.message}"
|
|
132
|
+
rescue RZstd::MissingContentSizeError => e
|
|
133
|
+
raise ProtocolError, "Zstd frame missing Frame_Content_Size (#{e.message})"
|
|
134
|
+
rescue RZstd::OutputSizeLimitError => e
|
|
135
|
+
raise ProtocolError, "declared FCS exceeds limit (#{e.message})"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def install_recv_dict(wire)
|
|
140
|
+
if wire.bytesize < 8
|
|
141
|
+
raise ProtocolError, "dict frame too short"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if wire.bytesize > Codec::MAX_DICT_SIZE
|
|
145
|
+
raise ProtocolError, "dict exceeds #{Codec::MAX_DICT_SIZE} bytes"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
@recv_dict = RZstd::Dictionary.new(wire.b)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def enforce_budget!(size, budget)
|
|
153
|
+
return if budget.nil?
|
|
154
|
+
return if size <= budget
|
|
155
|
+
|
|
156
|
+
raise ProtocolError, "decompressed message size exceeds maximum"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "io/stream"
|
|
6
|
+
|
|
7
|
+
module OMQ
|
|
8
|
+
module Transport
|
|
9
|
+
module ZstdTcp
|
|
10
|
+
SCHEME = "zstd+tcp"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# WeakKeyMap keyed by Engine — the Codec for a socket lives as long
|
|
14
|
+
# as the Engine (and therefore the socket) lives. When the socket is
|
|
15
|
+
# closed and the Engine is GC'd, the entry disappears automatically.
|
|
16
|
+
#
|
|
17
|
+
@codecs = ObjectSpace::WeakKeyMap.new
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# Creates a bound zstd+tcp listener.
|
|
22
|
+
#
|
|
23
|
+
# @param endpoint [String] e.g. "zstd+tcp://127.0.0.1:5555"
|
|
24
|
+
# @param engine [Engine]
|
|
25
|
+
# @param level [Integer] Zstd compression level
|
|
26
|
+
# @param dict [String, nil] user-supplied dictionary bytes
|
|
27
|
+
# @return [Listener]
|
|
28
|
+
#
|
|
29
|
+
def listener(endpoint, engine, level: -3, dict: nil, **)
|
|
30
|
+
codec = codec_for(engine, level: level, dict: dict)
|
|
31
|
+
|
|
32
|
+
host, port = parse_endpoint(endpoint)
|
|
33
|
+
host = normalize_bind_host(host)
|
|
34
|
+
servers = ::Socket.tcp_server_sockets(host, port)
|
|
35
|
+
|
|
36
|
+
if servers.empty?
|
|
37
|
+
raise ::Socket::ResolutionError, "no addresses for #{host.inspect}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
actual_port = servers.first.local_address.ip_port
|
|
41
|
+
display_host = host || "*"
|
|
42
|
+
host_part = display_host.include?(":") ? "[#{display_host}]" : display_host
|
|
43
|
+
resolved = "#{SCHEME}://#{host_part}:#{actual_port}"
|
|
44
|
+
|
|
45
|
+
Listener.new(resolved, servers, actual_port, engine, codec)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Creates a zstd+tcp dialer for an endpoint.
|
|
50
|
+
#
|
|
51
|
+
# @param endpoint [String] e.g. "zstd+tcp://127.0.0.1:5555"
|
|
52
|
+
# @param engine [Engine]
|
|
53
|
+
# @param level [Integer] Zstd compression level
|
|
54
|
+
# @param dict [String, nil] user-supplied dictionary bytes
|
|
55
|
+
# @return [Dialer]
|
|
56
|
+
#
|
|
57
|
+
def dialer(endpoint, engine, level: -3, dict: nil, **)
|
|
58
|
+
codec = codec_for(engine, level: level, dict: dict)
|
|
59
|
+
Dialer.new(endpoint, engine, codec)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def validate_endpoint!(endpoint)
|
|
64
|
+
host, _port = parse_endpoint(endpoint)
|
|
65
|
+
host = normalize_connect_host(host)
|
|
66
|
+
Addrinfo.getaddrinfo(host, nil, nil, :STREAM) if host
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_endpoint(endpoint)
|
|
71
|
+
uri = URI.parse(endpoint)
|
|
72
|
+
[uri.hostname, uri.port]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def normalize_bind_host(host)
|
|
77
|
+
return nil if host == "*"
|
|
78
|
+
host
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def normalize_connect_host(host)
|
|
83
|
+
host == "*" ? "127.0.0.1" : host
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def connect_timeout(options)
|
|
88
|
+
ri = options.reconnect_interval
|
|
89
|
+
ri.is_a?(Range) ? ri.end : [ri * 10, 30].min
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Returns the shared Codec for this engine (socket), creating one
|
|
97
|
+
# on first call. Stored in a WeakKeyMap keyed by engine — the
|
|
98
|
+
# entry is automatically removed when the engine is GC'd.
|
|
99
|
+
#
|
|
100
|
+
# Inside a Ractor the module-level @codecs ivar is inaccessible,
|
|
101
|
+
# so we fall back to creating a fresh Codec per call. This is
|
|
102
|
+
# fine: each Ractor worker owns a single socket with one endpoint,
|
|
103
|
+
# so there is nothing to share.
|
|
104
|
+
#
|
|
105
|
+
# @param engine [Engine]
|
|
106
|
+
# @param level [Integer]
|
|
107
|
+
# @param dict [String, nil]
|
|
108
|
+
# @return [Codec]
|
|
109
|
+
#
|
|
110
|
+
def codec_for(engine, level:, dict:)
|
|
111
|
+
@codecs[engine] ||= Codec.new level: level, dict: dict,
|
|
112
|
+
max_message_size: engine.options.max_message_size
|
|
113
|
+
|
|
114
|
+
rescue Ractor::IsolationError
|
|
115
|
+
Codec.new level: level, dict: dict,
|
|
116
|
+
max_message_size: engine.options.max_message_size
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# A zstd+tcp dialer — stateful factory for outgoing connections.
|
|
122
|
+
#
|
|
123
|
+
# Holds the Codec (compression cache, training state, dict) and
|
|
124
|
+
# wraps new connections with {ZstdConnection}.
|
|
125
|
+
#
|
|
126
|
+
class Dialer
|
|
127
|
+
# @return [String] the endpoint this dialer connects to
|
|
128
|
+
#
|
|
129
|
+
attr_reader :endpoint
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# @param endpoint [String]
|
|
133
|
+
# @param engine [Engine]
|
|
134
|
+
# @param codec [Codec]
|
|
135
|
+
#
|
|
136
|
+
def initialize(endpoint, engine, codec)
|
|
137
|
+
@endpoint = endpoint
|
|
138
|
+
@engine = engine
|
|
139
|
+
@codec = codec
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Establishes a TCP connection to the endpoint.
|
|
144
|
+
#
|
|
145
|
+
# @return [void]
|
|
146
|
+
#
|
|
147
|
+
def connect
|
|
148
|
+
host, port = ZstdTcp.parse_endpoint(@endpoint)
|
|
149
|
+
host = ZstdTcp.normalize_connect_host(host)
|
|
150
|
+
sock = ::Socket.tcp host, port, connect_timeout: ZstdTcp.connect_timeout(@engine.options)
|
|
151
|
+
|
|
152
|
+
TCP.apply_buffer_sizes sock, @engine.options
|
|
153
|
+
|
|
154
|
+
@engine.handle_connected IO::Stream::Buffered.wrap(sock), endpoint: @endpoint
|
|
155
|
+
rescue
|
|
156
|
+
sock&.close
|
|
157
|
+
raise
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# Wraps a raw ZMTP connection with Zstd compression.
|
|
162
|
+
#
|
|
163
|
+
# @param conn [Protocol::ZMTP::Connection]
|
|
164
|
+
# @return [ZstdConnection]
|
|
165
|
+
#
|
|
166
|
+
def wrap_connection(conn)
|
|
167
|
+
ZstdConnection.new(conn, @codec)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# A bound zstd+tcp listener.
|
|
174
|
+
#
|
|
175
|
+
class Listener
|
|
176
|
+
# @return [String] resolved endpoint with actual port
|
|
177
|
+
#
|
|
178
|
+
attr_reader :endpoint
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# @return [Integer] bound port
|
|
182
|
+
#
|
|
183
|
+
attr_reader :port
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# @param endpoint [String] resolved endpoint URI
|
|
187
|
+
# @param servers [Array<Socket>]
|
|
188
|
+
# @param port [Integer] bound port number
|
|
189
|
+
# @param engine [Engine]
|
|
190
|
+
# @param codec [Codec]
|
|
191
|
+
#
|
|
192
|
+
def initialize(endpoint, servers, port, engine, codec)
|
|
193
|
+
@endpoint = endpoint
|
|
194
|
+
@servers = servers
|
|
195
|
+
@port = port
|
|
196
|
+
@engine = engine
|
|
197
|
+
@codec = codec
|
|
198
|
+
@tasks = []
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# Wraps a raw ZMTP connection with Zstd compression.
|
|
203
|
+
#
|
|
204
|
+
# @param conn [Protocol::ZMTP::Connection]
|
|
205
|
+
# @return [ZstdConnection]
|
|
206
|
+
#
|
|
207
|
+
def wrap_connection(conn)
|
|
208
|
+
ZstdConnection.new(conn, @codec)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Spawns accept loop tasks under +parent_task+.
|
|
213
|
+
#
|
|
214
|
+
# @param parent_task [Async::Task]
|
|
215
|
+
# @yieldparam io [IO::Stream::Buffered]
|
|
216
|
+
#
|
|
217
|
+
def start_accept_loops(parent_task, &on_accepted)
|
|
218
|
+
@tasks = @servers.map do |server|
|
|
219
|
+
parent_task.async(transient: true, annotation: "zstd+tcp accept #{@endpoint}") do
|
|
220
|
+
loop do
|
|
221
|
+
client, _addr = server.accept
|
|
222
|
+
|
|
223
|
+
Async::Task.current.defer_stop do
|
|
224
|
+
TCP.apply_buffer_sizes(client, @engine.options)
|
|
225
|
+
|
|
226
|
+
stream = IO::Stream::Buffered.wrap(client)
|
|
227
|
+
|
|
228
|
+
on_accepted.call stream
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
rescue Async::Stop
|
|
232
|
+
rescue IOError
|
|
233
|
+
ensure
|
|
234
|
+
server.close rescue nil
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# Stops the listener and closes all server sockets.
|
|
241
|
+
#
|
|
242
|
+
# @return [void]
|
|
243
|
+
#
|
|
244
|
+
def stop
|
|
245
|
+
@tasks.each(&:stop)
|
|
246
|
+
@servers.each { |s| s.close rescue nil }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# OMQ Zstd+TCP transport — adds zstd+tcp:// endpoint support.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# require "omq/zstd"
|
|
7
|
+
#
|
|
8
|
+
# push = OMQ::PUSH.new
|
|
9
|
+
# push.connect("zstd+tcp://127.0.0.1:5555", level: -3)
|
|
10
|
+
|
|
11
|
+
require "omq"
|
|
12
|
+
require "rzstd"
|
|
13
|
+
|
|
14
|
+
require_relative "zstd_tcp/codec"
|
|
15
|
+
require_relative "zstd_tcp/connection"
|
|
16
|
+
require_relative "zstd_tcp/transport"
|
|
17
|
+
|
|
18
|
+
module OMQ
|
|
19
|
+
module Transport
|
|
20
|
+
module ZstdTcp
|
|
21
|
+
class ProtocolError < StandardError; end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
OMQ::Engine.transports["zstd+tcp"] = OMQ::Transport::ZstdTcp
|
data/lib/omq/zstd.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: omq-zstd
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.4.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Patrik Wenger
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: omq
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.23'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.23'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rzstd
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.2'
|
|
40
|
+
description: Adds zstd+tcp:// endpoint support to OMQ with per-frame Zstd compression,
|
|
41
|
+
bounded decompression, in-band dictionary shipping, and sender-side dictionary training.
|
|
42
|
+
email:
|
|
43
|
+
- paddor@gmail.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- DESIGN.md
|
|
49
|
+
- LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- RFC.md
|
|
52
|
+
- lib/omq/transport/zstd_tcp.rb
|
|
53
|
+
- lib/omq/transport/zstd_tcp/codec.rb
|
|
54
|
+
- lib/omq/transport/zstd_tcp/connection.rb
|
|
55
|
+
- lib/omq/transport/zstd_tcp/transport.rb
|
|
56
|
+
- lib/omq/zstd.rb
|
|
57
|
+
- lib/omq/zstd/version.rb
|
|
58
|
+
homepage: https://github.com/paddor/omq-zstd
|
|
59
|
+
licenses:
|
|
60
|
+
- ISC
|
|
61
|
+
metadata: {}
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '3.3'
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 4.0.6
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: Zstd+TCP transport for OMQ
|
|
79
|
+
test_files: []
|