Просмотр исходного кода

Handle error packet as the initial packet from the server

Some versions of MariaDB (e.g. 10.1.21) may send an error packet as
the first packet, e.g. in case of 'Too many connections'. This seems
to be a bug in some versions as it is not permitted according to the
protocol documentation. Nevertheless, nothing prevents us from
handling an error packet as the first message from the server.

Fixes #88
Viktor Söderqvist 6 лет назад
Родитель
Сommit
ac69800652
2 измененных файлов с 53 добавлено и 10 удалено
  1. 17 10
      src/mysql_protocol.erl
  2. 36 0
      test/mysql_tests.erl

+ 17 - 10
src/mysql_protocol.erl

@@ -57,15 +57,19 @@ handshake(Username, Password, Database, SockModule0, SSLOpts, Socket0,
           SetFoundRows) ->
     SeqNum0 = 0,
     {ok, HandshakePacket, SeqNum1} = recv_packet(SockModule0, Socket0, SeqNum0),
-    Handshake = parse_handshake(HandshakePacket),
-    {ok, SockModule, Socket, SeqNum2}
-    = maybe_do_ssl_upgrade(SockModule0, Socket0, SeqNum1, Handshake,
-                           SSLOpts, Database, SetFoundRows),
-    Response = build_handshake_response(Handshake, Username, Password,
-                                        Database, SetFoundRows),
-    {ok, SeqNum3} = send_packet(SockModule, Socket, Response, SeqNum2),
-    handshake_finish_or_switch_auth(Handshake, Password, SockModule, Socket,
-                                    SeqNum3).
+    case parse_handshake(HandshakePacket) of
+        #handshake{} = Handshake ->
+            {ok, SockModule, Socket, SeqNum2} =
+                maybe_do_ssl_upgrade(SockModule0, Socket0, SeqNum1, Handshake,
+                                     SSLOpts, Database, SetFoundRows),
+            Response = build_handshake_response(Handshake, Username, Password,
+                                                Database, SetFoundRows),
+            {ok, SeqNum3} = send_packet(SockModule, Socket, Response, SeqNum2),
+            handshake_finish_or_switch_auth(Handshake, Password, SockModule,
+                                            Socket, SeqNum3);
+        #error{} = Error ->
+            Error
+    end.
 
 handshake_finish_or_switch_auth(Handshake = #handshake{status = Status}, Password,
                                 SockModule, Socket, SeqNum0) ->
@@ -204,7 +208,7 @@ fetch_execute_response(SockModule, Socket, Timeout) ->
 %% @doc Parses a handshake. This is the first thing that comes from the server
 %% when connecting. If an unsupported version or variant of the protocol is used
 %% an error is raised.
--spec parse_handshake(binary()) -> #handshake{}.
+-spec parse_handshake(binary()) -> #handshake{} | #error{}.
 parse_handshake(<<10, Rest/binary>>) ->
     %% Protocol version 10.
     {ServerVersion, Rest1} = nulterm_str(Rest),
@@ -241,6 +245,9 @@ parse_handshake(<<10, Rest/binary>>) ->
                status = StatusFlags,
                auth_plugin_data = AuthPluginData,
                auth_plugin_name = AuthPluginName1};
+parse_handshake(Packet = ?error_pattern) ->
+    %% 'Too many connections' in MariaDB 10.1.21
+    parse_error_packet(Packet);
 parse_handshake(<<Protocol:8, _/binary>>) when Protocol /= 10 ->
     error(unknown_protocol).
 

+ 36 - 0
test/mysql_tests.erl

@@ -56,6 +56,42 @@ failing_connect_test() ->
     end,
     process_flag(trap_exit, false).
 
+too_many_connections_test_() ->
+    {timeout, 30, fun too_many_connections/0}.
+
+too_many_connections() ->
+    process_flag(trap_exit, true),
+    try
+        Pids = too_many_connections(200),
+        %?debugFmt("Max connections seems to be ~p~n", [length(Pids)]),
+        %% Collect all the trapped exits.
+        receive {'EXIT', _, Reason} ->
+            ?assertMatch({1040, <<"08004">>, <<"Too many connections">>},
+                         Reason)
+        after 50 -> ok
+        end,
+        lists:foreach(fun (Pid) ->
+                              exit(Pid, normal),
+                              receive {'EXIT', _, normal} -> ok
+                              after 50 -> ok
+                              end
+                      end,
+                      Pids)
+    after
+        process_flag(trap_exit, false),
+        timer:sleep(1000)
+    end.
+
+too_many_connections(Max) when Max > 0 ->
+    Options = [{user, ?user}, {password, ?password}, {connect_timeout, 10000}],
+    case mysql:start_link(Options) of
+        {ok, Pid} ->
+            [Pid | too_many_connections(Max - 1)];
+        {error, E} ->
+            ?assertMatch({1040, <<"08004">>, <<"Too many connections">>}, E),
+            []
+    end.
+
 successful_connect_test() ->
     %% A connection with a registered name and execute initial queries and
     %% create prepared statements.