Skip to content

Commit 91dfef8

Browse files
authored
add support for the new hello handshake, resolves #140 (#143)
1 parent 50726d5 commit 91dfef8

File tree

11 files changed

+174
-84
lines changed

11 files changed

+174
-84
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## 0.9.2 (2022-09-07)
8+
## 0.9.2 (2022-09-xx)
99
* Bugfix
1010
* fix a crash in the streaming hello monitor, if the server sends more than one response at once
11+
* add support for the new hello handshake
1112

1213
## 0.9.1 (2022-05-27)
1314
* Bugfix

lib/mongo.ex

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,18 @@ defmodule Mongo do
15231523
end
15241524
end
15251525

1526+
def exec_hello(conn, opts) do
1527+
with {:ok, _cmd, response} <- DBConnection.execute(conn, %Query{action: {:exec_hello, []}}, [], defaults(opts)) do
1528+
check_for_error(response, [hello: 1], opts)
1529+
end
1530+
end
1531+
1532+
def exec_hello(conn, cmd, opts) do
1533+
with {:ok, _cmd, response} <- DBConnection.execute(conn, %Query{action: {:exec_hello, cmd}}, [], defaults(opts)) do
1534+
check_for_error(response, cmd, opts)
1535+
end
1536+
end
1537+
15261538
def exec_more_to_come(conn, opts) do
15271539
with {:ok, _cmd, response} <- DBConnection.execute(conn, %Query{action: :command}, [:more_to_come], defaults(opts)) do
15281540
check_for_error(response, [:more_to_come], opts)
@@ -1745,7 +1757,7 @@ defmodule Mongo do
17451757
end
17461758

17471759
defp do_log(cmd, duration, opts) do
1748-
case Keyword.has_key?(cmd, :isMaster) || Keyword.has_key?(cmd, :more_to_come) do
1760+
case Keyword.has_key?(cmd, :isMaster) || Keyword.has_key?(cmd, :more_to_come) || Keyword.has_key?(cmd, :hello) do
17491761
true ->
17501762
:ok
17511763

lib/mongo/auth/scram.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ defmodule Mongo.Auth.SCRAM do
127127
# It calls isMaster with saslSupportedMechs option to ask for the selected user which mechanism is supported
128128
#
129129
defp select_digest(database, username, state) do
130-
with {:ok, _flags, doc} <- Utils.command(-2, [isMaster: 1, saslSupportedMechs: database <> "." <> username], state) do
130+
### todo
131+
with {:ok, _flags, doc} <- Utils.command(-2, [hello: 1, saslSupportedMechs: database <> "." <> username], state) do
131132
select_digest(doc)
132133
end
133134
end

lib/mongo/mongo_db_connection.ex

Lines changed: 121 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ defmodule Mongo.MongoDBConnection do
1212
alias Mongo.Events.CommandStartedEvent
1313
alias Mongo.MongoDBConnection.Utils
1414
alias Mongo.Events.MoreToComeEvent
15+
alias Mongo.StableVersion
1516

1617
@timeout 5_000
1718
@find_one_flags ~w(slave_ok exhaust partial)a
1819
@write_concern ~w(w j wtimeout)a
19-
@insecure_cmds [:authenticate, :saslStart, :saslContinue, :getnonce, :createUser, :updateUser, :copydbgetnonce, :copydbsaslstart, :copydb, :isMaster, :ismaster]
20+
@insecure_cmds [:authenticate, :saslStart, :saslContinue, :getnonce, :createUser, :updateUser, :copydbgetnonce, :copydbsaslstart, :copydb, :isMaster, :ismaster, :hello]
2021

2122
@impl true
2223
def connect(opts) do
@@ -34,12 +35,74 @@ defmodule Mongo.MongoDBConnection do
3435
auth_mechanism: opts[:auth_mechanism] || nil,
3536
connection_type: Keyword.fetch!(opts, :connection_type),
3637
topology_pid: Keyword.fetch!(opts, :topology_pid),
38+
stable_api: Keyword.get(opts, :stable_api),
39+
use_op_msg: Keyword.get(opts, :stable_api) != nil,
40+
hello_ok: Keyword.get(opts, :stable_api) != nil,
3741
ssl: opts[:ssl] || opts[:tls] || false
3842
}
3943

4044
connect(opts, state)
4145
end
4246

47+
@impl true
48+
def disconnect(_error, %{connection: {mod, socket}, connection_type: type, topology_pid: pid, host: host}) do
49+
GenServer.cast(pid, {:disconnect, type, host})
50+
mod.close(socket)
51+
:ok
52+
end
53+
54+
@impl true
55+
def checkout(state), do: {:ok, state}
56+
@impl true
57+
def handle_begin(_opts, state), do: {:ok, nil, state}
58+
@impl true
59+
def handle_close(_query, _opts, state), do: {:ok, nil, state}
60+
@impl true
61+
def handle_commit(_opts, state), do: {:ok, nil, state}
62+
@impl true
63+
def handle_deallocate(_query, _cursor, _opts, state), do: {:ok, nil, state}
64+
@impl true
65+
def handle_declare(query, _params, _opts, state), do: {:ok, query, nil, state}
66+
@impl true
67+
def handle_fetch(_query, _cursor, _opts, state), do: {:halt, nil, state}
68+
@impl true
69+
def handle_prepare(query, _opts, state), do: {:ok, query, state}
70+
@impl true
71+
def handle_rollback(_opts, state), do: {:ok, nil, state}
72+
@impl true
73+
def handle_status(_opts, state), do: {:idle, state}
74+
75+
@impl true
76+
def ping(%{connection_type: :client} = state) do
77+
cmd = [ping: 1]
78+
79+
case Utils.command(-1, cmd, state) do
80+
{:ok, _flags, %{"ok" => ok}} when ok == 1 ->
81+
{:ok, state}
82+
83+
{:ok, _flags, %{"ok" => ok, "errmsg" => msg, "code" => code}} when ok == 0 ->
84+
err = Mongo.Error.exception(message: msg, code: code)
85+
{:disconnect, err, state}
86+
87+
{:disconnect, _, _} = error ->
88+
error
89+
end
90+
end
91+
92+
@impl true
93+
def ping(state) do
94+
{:ok, state}
95+
end
96+
97+
@impl true
98+
def handle_execute(%Mongo.Query{action: action} = query, params, opts, original_state) do
99+
tmp_state = %{original_state | database: Keyword.get(opts, :database, original_state.database)}
100+
101+
with {:ok, reply, tmp_state} <- execute_action(action, params, opts, tmp_state) do
102+
{:ok, query, reply, Map.put(tmp_state, :database, original_state.database)}
103+
end
104+
end
105+
43106
defp connect(opts, state) do
44107
result =
45108
with {:ok, state} <- tcp_connect(opts, state),
@@ -123,12 +186,12 @@ defmodule Mongo.MongoDBConnection do
123186
end
124187
end
125188

126-
defp post_hello_command(state, client) do
127-
cmd = [ismaster: 1, client: client]
189+
defp hand_shake(opts, state) do
190+
cmd = handshake_command(state, client(opts[:appname] || "elixir-driver"))
128191

129192
case Utils.command(-1, cmd, state) do
130-
{:ok, _flags, %{"ok" => ok, "maxWireVersion" => version}} when ok == 1 ->
131-
{:ok, %{state | wire_version: version}}
193+
{:ok, _flags, %{"ok" => ok, "maxWireVersion" => version} = response} when ok == 1 ->
194+
{:ok, %{state | wire_version: version, use_op_msg: version >= 6, hello_ok: Map.get(response, "helloOk", false)}}
132195

133196
{:ok, _flags, %{"ok" => ok}} when ok == 1 ->
134197
{:ok, %{state | wire_version: 0}}
@@ -142,11 +205,7 @@ defmodule Mongo.MongoDBConnection do
142205
end
143206
end
144207

145-
defp hand_shake(opts, state) do
146-
post_hello_command(state, driver(opts[:appname] || "My killer app"))
147-
end
148-
149-
defp driver(appname) do
208+
defp client(app_name) do
150209
driver_version =
151210
case :application.get_key(:mongodb_driver, :vsn) do
152211
{:ok, version} -> to_string(version)
@@ -167,7 +226,7 @@ defmodule Mongo.MongoDBConnection do
167226

168227
%{
169228
client: %{
170-
application: %{name: appname}
229+
application: %{name: app_name}
171230
},
172231
driver: %{
173232
name: "mongodb_driver",
@@ -195,76 +254,50 @@ defmodule Mongo.MongoDBConnection do
195254
defp pretty_name("apple"), do: "Mac OS X"
196255
defp pretty_name(name), do: name
197256

198-
@impl true
199-
def disconnect(_error, %{connection: {mod, socket}, connection_type: type, topology_pid: pid, host: host}) do
200-
GenServer.cast(pid, {:disconnect, type, host})
201-
mod.close(socket)
202-
:ok
257+
defp provide_cmd_data([{command_name, _} | _] = cmd) do
258+
case Enum.member?(@insecure_cmds, command_name) do
259+
true -> {command_name, %{}}
260+
false -> {command_name, cmd}
261+
end
203262
end
204263

205-
@impl true
206-
def checkout(state), do: {:ok, state}
207-
@impl true
208-
def handle_begin(_opts, state), do: {:ok, nil, state}
209-
@impl true
210-
def handle_close(_query, _opts, state), do: {:ok, nil, state}
211-
@impl true
212-
def handle_commit(_opts, state), do: {:ok, nil, state}
213-
@impl true
214-
def handle_deallocate(_query, _cursor, _opts, state), do: {:ok, nil, state}
215-
@impl true
216-
def handle_declare(query, _params, _opts, state), do: {:ok, query, nil, state}
217-
@impl true
218-
def handle_fetch(_query, _cursor, _opts, state), do: {:halt, nil, state}
219-
@impl true
220-
def handle_prepare(query, _opts, state), do: {:ok, query, state}
221-
@impl true
222-
def handle_rollback(_opts, state), do: {:ok, nil, state}
223-
@impl true
224-
def handle_status(_opts, state), do: {:idle, state}
225-
226-
@impl true
227-
def ping(%{connection_type: :client} = state) do
228-
cmd = [ping: 1]
229-
230-
case Utils.command(-1, cmd, state) do
231-
{:ok, _flags, %{"ok" => ok}} when ok == 1 ->
232-
{:ok, state}
264+
##
265+
# Executes a hello or the legacy hello command
266+
##
267+
defp execute_action({:exec_hello, cmd}, _params, opts, %{use_op_msg: true} = state) do
268+
db = opts[:database] || state.database
269+
timeout = Keyword.get(opts, :timeout, state.timeout)
270+
flags = Keyword.get(opts, :flags, 0x0)
233271

234-
{:ok, _flags, %{"ok" => ok, "errmsg" => msg, "code" => code}} when ok == 0 ->
235-
err = Mongo.Error.exception(message: msg, code: code)
236-
{:disconnect, err, state}
272+
cmd = hello_command(cmd, state) ++ ["$db": db]
237273

238-
{:disconnect, _, _} = error ->
239-
error
240-
end
241-
end
274+
event = %CommandStartedEvent{
275+
command: :hello,
276+
command_name: :hello,
277+
database_name: db,
278+
request_id: state.request_id,
279+
operation_id: opts[:operation_id],
280+
connection_id: self()
281+
}
242282

243-
@impl true
244-
def ping(state) do
245-
{:ok, state}
246-
end
283+
Events.notify(event, :commands)
247284

248-
@impl true
249-
def handle_execute(%Mongo.Query{action: action} = query, params, opts, original_state) do
250-
tmp_state = %{original_state | database: Keyword.get(opts, :database, original_state.database)}
285+
op = op_msg(flags: flags, sections: [section(payload_type: 0, payload: payload(doc: cmd))])
251286

252-
with {:ok, reply, tmp_state} <- execute_action(action, params, opts, tmp_state) do
253-
{:ok, query, reply, Map.put(tmp_state, :database, original_state.database)}
254-
end
255-
end
287+
case :timer.tc(fn -> Utils.post_request(op, state.request_id, %{state | timeout: timeout}) end) do
288+
{duration, {:ok, flags, doc}} ->
289+
state = %{state | request_id: state.request_id + 1}
290+
{:ok, {doc, event, flags, duration}, state}
256291

257-
defp provide_cmd_data([{command_name, _} | _] = cmd) do
258-
case Enum.member?(@insecure_cmds, command_name) do
259-
true -> {command_name, %{}}
260-
false -> {command_name, cmd}
292+
{_duration, error} ->
293+
error
261294
end
262295
end
263296

264297
##
265298
# Executes a more to come command
266299
##
267-
defp execute_action(:command, [:more_to_come], opts, %{wire_version: version} = state) when version >= 6 do
300+
defp execute_action(:command, [:more_to_come], opts, %{use_op_msg: true} = state) do
268301
event = %MoreToComeEvent{command: :more_to_come, command_name: opts[:command_name] || :more_to_come}
269302

270303
timeout = Keyword.get(opts, :timeout, state.timeout)
@@ -280,7 +313,7 @@ defmodule Mongo.MongoDBConnection do
280313
end
281314
end
282315

283-
defp execute_action(:command, [cmd], opts, %{wire_version: version} = state) when version >= 6 do
316+
defp execute_action(:command, [cmd], opts, %{use_op_msg: true} = state) do
284317
{command_name, data} = provide_cmd_data(cmd)
285318
db = opts[:database] || state.database
286319
cmd = cmd ++ ["$db": db]
@@ -371,4 +404,24 @@ defmodule Mongo.MongoDBConnection do
371404
{_flag, false}, acc -> acc
372405
end)
373406
end
407+
408+
defp handshake_command(%{stable_api: nil}, client) do
409+
[ismaster: 1, helloOk: 1, client: client]
410+
end
411+
412+
defp handshake_command(%{stable_api: stable_api}, client) do
413+
StableVersion.merge_stable_api([hello: 1, client: client], stable_api)
414+
end
415+
416+
defp hello_command(cmd, %{hello_ok: false}) do
417+
cmd
418+
|> Keyword.put(:ismaster, 1)
419+
|> Keyword.put(:helloOk, 1)
420+
end
421+
422+
defp hello_command(cmd, %{hello_ok: true, stable_api: stable_api}) do
423+
cmd
424+
|> Keyword.put(:hello, 1)
425+
|> StableVersion.merge_stable_api(stable_api)
426+
end
374427
end

lib/mongo/monitor.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ defmodule Mongo.Monitor do
221221
# Streaming mode: calls hello command and updated the round trip time for the command.
222222
##
223223
defp get_server_description(%{connection_pid: conn_pid, round_trip_time: last_rtt, mode: :streaming_mode, opts: opts}) do
224-
{rtt, response} = :timer.tc(fn -> Mongo.exec_command(conn_pid, [isMaster: 1], opts) end)
224+
{rtt, response} = :timer.tc(fn -> Mongo.exec_hello(conn_pid, opts) end)
225225

226226
case response do
227227
{:ok, {_flags, _hello_doc}} ->
@@ -236,7 +236,7 @@ defmodule Mongo.Monitor do
236236
# Polling mode: updating the server description and the round trip time together
237237
##
238238
defp get_server_description(%{connection_pid: conn_pid, address: address, round_trip_time: last_rtt, opts: opts}) do
239-
{rtt, response} = :timer.tc(fn -> Mongo.exec_command(conn_pid, [isMaster: 1], opts) end)
239+
{rtt, response} = :timer.tc(fn -> Mongo.exec_hello(conn_pid, opts) end)
240240

241241
case response do
242242
{:ok, {_flags, hello_doc}} ->

lib/mongo/query.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule Mongo.Query do
22
@moduledoc """
33
This is the query implementation for the Query Protocol
44
5-
  Encoding and decoding does not take place at this point, but is directly performed
5+
Encoding and decoding does not take place at this point, but is directly performed
66
into the functions of Mongo.MongoDBConnection.Utils.
77
"""
88
defstruct action: nil

lib/mongo/server_description.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ defmodule Mongo.ServerDescription do
151151
defp determine_server_type(%{"setName" => set_name} = is_master_reply) when set_name != nil do
152152
case is_master_reply do
153153
%{"ismaster" => true} -> :rs_primary
154+
%{"isWritablePrimary" => true} -> :rs_primary
154155
%{"secondary" => true} -> :rs_secondary
155156
%{"arbiterOnly" => true} -> :rs_arbiter
156157
_ -> :rs_other

lib/mongo/stable_version.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
defmodule Mongo.StableVersion do
2+
@moduledoc false
3+
4+
defmodule ServerAPI do
5+
@moduledoc false
6+
7+
defstruct version: "1",
8+
strict: false,
9+
deprecation_errors: false
10+
end
11+
12+
def merge_stable_api(command, %{version: version, strict: strict, deprecation_errors: deprecation_errors}) do
13+
command
14+
|> Keyword.put(:apiVersion, version)
15+
|> Keyword.put(:apiStrict, strict)
16+
|> Keyword.put(:apiDeprecationErrors, deprecation_errors)
17+
end
18+
19+
def merge_stable_api(command, _other) do
20+
command
21+
end
22+
end

0 commit comments

Comments
 (0)