Skip to content

Commit cf32c87

Browse files
committed
feat: added support for op_compressed (zlib and zstd)
1 parent 84732f7 commit cf32c87

12 files changed

+266
-58
lines changed

lib/mongo.ex

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -442,10 +442,9 @@ defmodule Mongo do
442442
|> filter_nils()
443443

444444
opts =
445-
Keyword.drop(
446-
opts,
447-
~w(explain allow_disk_use collation bypass_document_validation hint comment read_concern)a
448-
)
445+
opts
446+
|> Keyword.drop(~w(explain allow_disk_use collation bypass_document_validation hint comment read_concern)a)
447+
|> Keyword.put_new(:compression, true)
449448

450449
get_stream(topology_pid, cmd, opts)
451450
end
@@ -496,10 +495,9 @@ defmodule Mongo do
496495
|> filter_nils()
497496

498497
opts =
499-
Keyword.drop(
500-
opts,
501-
~w(bypass_document_validation max_time projection return_document sort upsert collation w j wtimeout)a
502-
)
498+
opts
499+
|> Keyword.drop(~w(bypass_document_validation max_time projection return_document sort upsert collation w j wtimeout)a)
500+
|> Keyword.put_new(:compression, true)
503501

504502
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do
505503
{:ok,
@@ -559,10 +557,9 @@ defmodule Mongo do
559557
|> filter_nils()
560558

561559
opts =
562-
Keyword.drop(
563-
opts,
564-
~w(bypass_document_validation max_time projection return_document sort upsert collation)a
565-
)
560+
opts
561+
|> Keyword.drop(~w(bypass_document_validation max_time projection return_document sort upsert collation)a)
562+
|> Keyword.put_new(:compression, true)
566563

567564
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do
568565
{:ok,
@@ -607,7 +604,10 @@ defmodule Mongo do
607604
]
608605
|> filter_nils()
609606

610-
opts = Keyword.drop(opts, ~w(max_time projection sort collation)a)
607+
opts =
608+
opts
609+
|> Keyword.drop(~w(max_time projection sort collation)a)
610+
|> Keyword.put_new(:compression, true)
611611

612612
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts), do: {:ok, doc["value"]}
613613
end
@@ -626,7 +626,10 @@ defmodule Mongo do
626626
]
627627
|> filter_nils()
628628

629-
opts = Keyword.drop(opts, ~w(limit skip hint collation)a)
629+
opts =
630+
opts
631+
|> Keyword.drop(~w(limit skip hint collation)a)
632+
|> Keyword.put_new(:compression, true)
630633

631634
with {:ok, doc} <- issue_command(topology_pid, cmd, :read, opts) do
632635
{:ok, trunc(doc["n"])}
@@ -721,7 +724,10 @@ defmodule Mongo do
721724
]
722725
|> filter_nils()
723726

724-
opts = Keyword.drop(opts, ~w(max_time)a)
727+
opts =
728+
opts
729+
|> Keyword.drop(~w(max_time)a)
730+
|> Keyword.put_new(:compression, true)
725731

726732
with {:ok, doc} <- issue_command(topology_pid, cmd, :read, opts), do: {:ok, doc["values"]}
727733
end
@@ -783,7 +789,10 @@ defmodule Mongo do
783789

784790
drop = ~w(limit hint single_batch read_concern max min collation return_key show_record_id tailable no_cursor_timeout await_data projection comment skip sort)a
785791

786-
opts = Keyword.drop(opts, drop)
792+
opts =
793+
opts
794+
|> Keyword.drop(drop)
795+
|> Keyword.put_new(:compression, true)
787796

788797
try do
789798
get_stream(topology_pid, cmd, opts)
@@ -858,7 +867,7 @@ defmodule Mongo do
858867
]
859868
|> filter_nils()
860869

861-
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do
870+
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, Keyword.put_new(opts, :compression, true)) do
862871
case doc do
863872
%{"writeErrors" => _} ->
864873
{:error, %Mongo.WriteError{n: doc["n"], ok: doc["ok"], write_errors: doc["writeErrors"]}}
@@ -913,7 +922,7 @@ defmodule Mongo do
913922
]
914923
|> filter_nils()
915924

916-
case issue_command(topology_pid, cmd, :write, opts) do
925+
case issue_command(topology_pid, cmd, :write, Keyword.put_new(opts, :compression, true)) do
917926
{:ok, %{"writeErrors" => _} = doc} ->
918927
{:error, %Mongo.WriteError{n: doc["n"], ok: doc["ok"], write_errors: doc["writeErrors"]}}
919928

@@ -989,7 +998,7 @@ defmodule Mongo do
989998
]
990999
|> filter_nils()
9911000

992-
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do
1001+
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, Keyword.put_new(opts, :compression, true)) do
9931002
case doc do
9941003
%{"writeErrors" => _} ->
9951004
{:error, %Mongo.WriteError{n: doc["n"], ok: doc["ok"], write_errors: doc["writeErrors"]}}
@@ -1171,7 +1180,7 @@ defmodule Mongo do
11711180
]
11721181
|> filter_nils()
11731182

1174-
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do
1183+
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, Keyword.put_new(opts, :compression, true)) do
11751184
case doc do
11761185
%{"writeErrors" => write_errors} ->
11771186
{:error, %Mongo.WriteError{n: doc["n"], ok: doc["ok"], write_errors: write_errors}}
@@ -1243,7 +1252,7 @@ defmodule Mongo do
12431252
]
12441253
|> filter_nils()
12451254

1246-
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, opts) do
1255+
with {:ok, doc} <- issue_command(topology_pid, cmd, :write, Keyword.put_new(opts, :compression, true)) do
12471256
case doc do
12481257
%{"writeErrors" => _} ->
12491258
{:error, %Mongo.WriteError{n: doc["n"], ok: doc["ok"], write_errors: doc["writeErrors"]}}
@@ -1300,7 +1309,7 @@ defmodule Mongo do
13001309
Mongo.limits(top)
13011310
13021311
{:ok, %{
1303-
compression: nil,
1312+
compression: [],
13041313
logical_session_timeout: 30,
13051314
max_bson_object_size: 16777216,
13061315
max_message_size_bytes: 48000000,
@@ -1381,7 +1390,7 @@ defmodule Mongo do
13811390
]
13821391
|> filter_nils()
13831392

1384-
with {:ok, _doc} <- issue_command(topology_pid, cmd, :write, opts) do
1393+
with {:ok, _doc} <- issue_command(topology_pid, cmd, :write, Keyword.put_new(opts, :compression, true)) do
13851394
:ok
13861395
end
13871396
end

lib/mongo/binary_utils.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ defmodule Mongo.BinaryUtils do
2121
quote do: signed - little - 8
2222
end
2323

24+
defmacro uint8 do
25+
quote do: unsigned - little - 8
26+
end
27+
2428
defmacro float64 do
2529
quote do: float - little - 64
2630
end

lib/mongo/compressor.ex

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
defmodule Mongo.Compressor do
2+
@moduledoc false
3+
4+
@zlib_compressor_id 2
5+
if Code.ensure_loaded?(:ezstd) do
6+
@zstd_compressor_id 3
7+
end
8+
9+
def compress(binary, :zlib) do
10+
{@zlib_compressor_id, :zlib.compress(binary)}
11+
end
12+
13+
if Code.ensure_loaded?(:ezstd) do
14+
def compress(binary, :zstd) when is_binary(binary) do
15+
{@zstd_compressor_id, :ezstd.compress(binary)}
16+
end
17+
18+
def compress(iodata, :zstd) when is_list(iodata) do
19+
{@zstd_compressor_id,
20+
iodata
21+
|> IO.iodata_to_binary()
22+
|> :ezstd.compress()}
23+
end
24+
end
25+
26+
def uncompress(binary, @zlib_compressor_id) do
27+
:zlib.uncompress(binary)
28+
end
29+
30+
if Code.ensure_loaded?(:ezstd) do
31+
def uncompress(binary, @zstd_compressor_id) do
32+
:ezstd.decompress(binary)
33+
end
34+
end
35+
36+
def uncompress(binary, :zlib) do
37+
:zlib.uncompress(binary)
38+
end
39+
40+
if Code.ensure_loaded?(:ezstd) do
41+
def uncompress(binary, :zstd) do
42+
:ezstd.decompress(binary)
43+
end
44+
end
45+
end

lib/mongo/messages.ex

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ defmodule Mongo.Messages do
1414
import Record
1515
import Mongo.BinaryUtils
1616

17+
alias Mongo.Compressor
18+
1719
@op_reply 1
1820
@op_query 2004
1921
@op_msg_code 2013
22+
@op_compressed 2012
2023

2124
@query_flags [
2225
tailable_cursor: 0x2,
@@ -46,6 +49,7 @@ defmodule Mongo.Messages do
4649
defrecord :payload, [:doc, :sequence]
4750
defrecord :section, [:payload_type, :payload]
4851
defrecord :op_msg, [:flags, :sections]
52+
defrecord :op_msg_compressed, [:flags, :sections, :compressor]
4953

5054
@decoder_module Application.compile_env(:mongodb_driver, :decoder, BSON.Decoder)
5155

@@ -84,7 +88,10 @@ defmodule Mongo.Messages do
8488
@op_msg_code ->
8589
{:ok, response_to, decode_msg(response), rest}
8690

87-
_ ->
91+
@op_compressed ->
92+
decode_compression(response_to, binary)
93+
94+
_error ->
8895
:error
8996
end
9097
end
@@ -137,6 +144,22 @@ defmodule Mongo.Messages do
137144
end
138145
end
139146

147+
defp decode_compression(response_to, binary) do
148+
<<original_opcode::int32(), uncompressed_size::int32(), compressor_id::uint8(), compressed::binary>> = binary
149+
<<response::binary(uncompressed_size), rest::binary>> = Compressor.uncompress(compressed, compressor_id)
150+
151+
case original_opcode do
152+
@op_reply ->
153+
{:ok, response_to, decode_reply(response), rest}
154+
155+
@op_msg_code ->
156+
{:ok, response_to, decode_msg(response), rest}
157+
158+
_error ->
159+
:error
160+
end
161+
end
162+
140163
defp cstring(binary) do
141164
split(binary, [])
142165
end
@@ -161,6 +184,15 @@ defmodule Mongo.Messages do
161184
[encode_header(header) | iodata]
162185
end
163186

187+
def encode(request_id, op_msg_compressed(compressor: compressor) = op) do
188+
payload = encode_op(op)
189+
uncompressed_size = IO.iodata_length(payload)
190+
{compressor_id, compressed_payload} = Compressor.compress(payload, compressor)
191+
iodata = [<<@op_msg_code::int32()>>, <<uncompressed_size::int32()>>, <<compressor_id::uint8()>> | compressed_payload]
192+
header = msg_header(length: IO.iodata_length(iodata) + @header_size, request_id: request_id, response_to: 0, op_code: @op_compressed)
193+
[encode_header(header) | iodata]
194+
end
195+
164196
defp encode_header(msg_header(length: length, request_id: request_id, response_to: response_to, op_code: op_code)) do
165197
<<length::int32(), request_id::int32(), response_to::int32(), op_code::int32()>>
166198
end
@@ -169,6 +201,10 @@ defmodule Mongo.Messages do
169201
[<<blit_flags(:query, flags)::int32()>>, coll, <<0x00, num_skip::int32(), num_return::int32()>>, BSON.Encoder.document(query), select]
170202
end
171203

204+
defp encode_op(op_msg_compressed(flags: flags, sections: sections)) do
205+
[<<blit_flags(:msg, flags)::int32()>> | encode_sections(sections)]
206+
end
207+
172208
defp encode_op(op_msg(flags: flags, sections: sections)) do
173209
[<<blit_flags(:msg, flags)::int32()>> | encode_sections(sections)]
174210
end

lib/mongo/mongo_db_connection.ex

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ defmodule Mongo.MongoDBConnection do
213213
end
214214

215215
defp hand_shake(opts, state) do
216-
cmd = handshake_command(state, client(opts[:appname] || "elixir-driver"))
216+
cmd = handshake_command(state, client(opts[:appname] || "elixir-driver"), Keyword.get(opts, :compressors, []))
217217

218218
case Utils.command(-1, cmd, state) do
219219
{:ok, _flags, %{"ok" => ok, "maxWireVersion" => version} = response} when ok == 1 ->
@@ -342,14 +342,22 @@ defmodule Mongo.MongoDBConnection do
342342
defp send_command({:command, cmd}, opts, %{use_op_msg: true} = state) do
343343
{command_name, data} = provide_cmd_data(cmd)
344344
db = opts[:database] || state.database
345+
compressor = opts[:compressor]
345346
cmd = cmd ++ ["$db": db]
346347
flags = Keyword.get(opts, :flags, 0x0)
347348

348349
# MongoDB 3.6 only allows certain command arguments to be provided this way. These are:
349350
op =
350351
case pulling_out?(cmd, :documents) || pulling_out?(cmd, :updates) || pulling_out?(cmd, :deletes) do
351-
nil -> op_msg(flags: flags, sections: [section(payload_type: 0, payload: payload(doc: cmd))])
352-
key -> pulling_out(cmd, flags, key)
352+
nil ->
353+
if compressor != nil do
354+
op_msg_compressed(flags: flags, sections: [section(payload_type: 0, payload: payload(doc: cmd))], compressor: compressor)
355+
else
356+
op_msg(flags: flags, sections: [section(payload_type: 0, payload: payload(doc: cmd))])
357+
end
358+
359+
key ->
360+
pulling_out(cmd, flags, key, compressor)
353361
end
354362

355363
# overwrite temporary timeout by timeout option
@@ -427,14 +435,20 @@ defmodule Mongo.MongoDBConnection do
427435
end
428436
end
429437

430-
defp pulling_out(cmd, flags, key) when is_atom(key) do
438+
defp pulling_out(cmd, flags, key, compressor) when is_atom(key) do
431439
docs = Keyword.get(cmd, key)
432440
cmd = Keyword.delete(cmd, key)
433441

434442
payload_0 = section(payload_type: 0, payload: payload(doc: cmd))
435443
payload_1 = section(payload_type: 1, payload: payload(sequence: sequence(identifier: to_string(key), docs: docs)))
436444

437-
op_msg(flags: flags, sections: [payload_0, payload_1])
445+
case compressor != nil do
446+
false ->
447+
op_msg(flags: flags, sections: [payload_0, payload_1])
448+
449+
true ->
450+
op_msg_compressed(flags: flags, sections: [payload_0, payload_1], compressor: compressor)
451+
end
438452
end
439453

440454
defp flags(flags) do
@@ -444,12 +458,12 @@ defmodule Mongo.MongoDBConnection do
444458
end)
445459
end
446460

447-
defp handshake_command(%{stable_api: nil}, client) do
448-
[ismaster: 1, helloOk: true, client: client]
461+
defp handshake_command(%{stable_api: nil}, client, compression) do
462+
[ismaster: 1, helloOk: true, client: client, compression: compression]
449463
end
450464

451-
defp handshake_command(%{stable_api: stable_api}, client) do
452-
[client: client]
465+
defp handshake_command(%{stable_api: stable_api}, client, compression) do
466+
[client: client, compression: compression]
453467
|> StableVersion.merge_stable_api(stable_api)
454468
|> Keyword.put(:hello, 1)
455469
end

0 commit comments

Comments
 (0)