Skip to content
This repository was archived by the owner on Mar 12, 2021. It is now read-only.

Commit 146bd82

Browse files
moozzykmoozzyk
authored andcommitted
ExceptionToTheRule - incorrect exception thrown if stop() called before init message recieved and /connect timeouts
If the user called `stop()` before the init message (the response to the /connect request) was received and the init message was not received within the `TransportConnectTimeout` we would have thrown an exception saying "transport timed out when trying to connect". This is unexpected since the user had already called `.stop()` on the connection. The issue was that in the thread that was terminating the `start()` request in case of connect timeout we did not check if the connection hadn't been stopped. After the change, if the connection had been stopped we just complete the `connect_request_tce` and let the continuation to take care of the rest (which results in throwing pplx::task_canceled exception since the `start()` was cancelled by invoking the `stop()` function). Bonus: removing the test that tests that the transport is being stopped if the instance goes out of scope without first `disconnect()`ing first. This test is flakey and there is no good way of making it more reliable. The `websocket_transport` class is meant to only be used internally and we always `disconnect()` the transport when the connection is being stopped.
1 parent 40c0fba commit 146bd82

3 files changed

Lines changed: 61 additions & 45 deletions

File tree

src/signalrclient/connection_impl.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,23 @@ namespace signalr
225225
auto transport = connection->m_transport_factory->create_transport(
226226
transport_type::websockets, connection->m_logger, connection->m_headers, process_response_callback, error_callback);
227227

228-
pplx::create_task([negotiation_response, connect_request_tce]()
228+
auto& disconnect_cts = m_disconnect_cts;
229+
pplx::create_task([negotiation_response, connect_request_tce, disconnect_cts, weak_connection]()
229230
{
230231
std::this_thread::sleep_for(std::chrono::milliseconds(negotiation_response.transport_connect_timeout));
231-
connect_request_tce.set_exception(std::runtime_error("transport timed out when trying to connect"));
232+
233+
// if the disconnect_cts is cancelled it means that the connection has been stopped or went out of scope in
234+
// which case we should not throw due to timeout. Instead we need to set the tce prevent the task that is
235+
// using this tce from hanging indifinitely. (This will eventually result in throwing the pplx::task_canceled
236+
// exception to the user since this is what we do in the start() function if disconnect_cts is tripped).
237+
if (disconnect_cts.get_token().is_canceled())
238+
{
239+
connect_request_tce.set();
240+
}
241+
else
242+
{
243+
connect_request_tce.set_exception(std::runtime_error("transport timed out when trying to connect"));
244+
}
232245
});
233246

234247
return connection->send_connect_request(transport, negotiation_response.connection_token, connect_request_tce)

test/signalrclienttests/connection_impl_tests.cpp

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,52 @@ TEST(connection_impl_stop, stop_cancels_ongoing_start_request)
878878
ASSERT_EQ(_XPLATSTR("[state change] connecting -> disconnected\n"), remove_date_from_log_entry(log_entries[4]));
879879
}
880880

881+
TEST(connection_impl_stop, ongoing_start_request_cancelled_if_connection_stopped_before_init_message_received)
882+
{
883+
auto web_request_factory = std::make_unique<test_web_request_factory>([](const web::uri& url)
884+
{
885+
auto response_body =
886+
url.path() == _XPLATSTR("/negotiate")
887+
? _XPLATSTR("{\"Url\":\"/signalr\", \"ConnectionToken\" : \"A==\", \"ConnectionId\" : \"f7707523-307d-4cba-9abf-3eef701241e8\", ")
888+
_XPLATSTR("\"DisconnectTimeout\" : 0.5, \"ConnectionTimeout\" : 110.0, \"TryWebSockets\" : true, ")
889+
_XPLATSTR("\"ProtocolVersion\" : \"1.4\", \"TransportConnectTimeout\" : 0.1, \"LongPollDelay\" : 0.0}")
890+
: _XPLATSTR("");
891+
892+
return std::unique_ptr<web_request>(new web_request_stub((unsigned short)200, _XPLATSTR("OK"), response_body));
893+
});
894+
895+
auto websocket_client = create_test_websocket_client(/*receive function*/ []()
896+
{
897+
return pplx::task_from_result<std::string>("{}");
898+
});
899+
900+
auto writer = std::shared_ptr<log_writer>{std::make_shared<memory_log_writer>()};
901+
auto connection = connection_impl::create(create_uri(), _XPLATSTR(""), trace_level::all, writer,
902+
std::move(web_request_factory), std::make_unique<test_transport_factory>(websocket_client));
903+
904+
auto start_task = connection->start();
905+
connection->stop().get();
906+
907+
start_task.then([](pplx::task<void> t)
908+
{
909+
try
910+
{
911+
t.get();
912+
ASSERT_TRUE(false); // exception expected but not thrown
913+
}
914+
catch (const pplx::task_canceled &)
915+
{ }
916+
}).get();
917+
918+
auto log_entries = std::dynamic_pointer_cast<memory_log_writer>(writer)->get_log_entries();
919+
ASSERT_EQ(5, log_entries.size());
920+
ASSERT_EQ(_XPLATSTR("[state change] disconnected -> connecting\n"), remove_date_from_log_entry(log_entries[0]));
921+
ASSERT_EQ(_XPLATSTR("[info ] stopping connection\n"), remove_date_from_log_entry(log_entries[1]));
922+
ASSERT_EQ(_XPLATSTR("[info ] acquired lock in shutdown()\n"), remove_date_from_log_entry(log_entries[2]));
923+
ASSERT_EQ(_XPLATSTR("[info ] starting the connection has been cancelled.\n"), remove_date_from_log_entry(log_entries[3]));
924+
ASSERT_EQ(_XPLATSTR("[state change] connecting -> disconnected\n"), remove_date_from_log_entry(log_entries[4]));
925+
}
926+
881927
TEST(connection_impl_stop, stop_ignores_exceptions_from_abort_requests)
882928
{
883929
auto writer = std::shared_ptr<log_writer>{std::make_shared<memory_log_writer>()};
@@ -1190,25 +1236,6 @@ TEST(connection_impl_reconnect, reconnect_works_if_connection_dropped_during_aft
11901236
{
11911237
auto connection_dropped_event = std::make_shared<pplx::event>();
11921238

1193-
auto web_request_factory = std::make_unique<test_web_request_factory>([&connection_dropped_event](const web::uri& url)
1194-
{
1195-
if (url.path() == _XPLATSTR("/start"))
1196-
{
1197-
connection_dropped_event->wait();
1198-
}
1199-
1200-
auto response_body =
1201-
url.path() == _XPLATSTR("/negotiate")
1202-
? _XPLATSTR("{\"Url\":\"/signalr\", \"ConnectionToken\" : \"A==\", \"ConnectionId\" : \"f7707523-307d-4cba-9abf-3eef701241e8\", ")
1203-
_XPLATSTR("\"DisconnectTimeout\" : 0.5, \"ConnectionTimeout\" : 110.0, \"TryWebSockets\" : true, ")
1204-
_XPLATSTR("\"ProtocolVersion\" : \"1.4\", \"TransportConnectTimeout\" : 5.0, \"LongPollDelay\" : 0.0}")
1205-
: url.path() == _XPLATSTR("/start")
1206-
? _XPLATSTR("{\"Response\":\"started\" }")
1207-
: _XPLATSTR("");
1208-
1209-
return std::unique_ptr<web_request>(new web_request_stub((unsigned short)200, _XPLATSTR("OK"), response_body));
1210-
});
1211-
12121239
int call_number = -1;
12131240
auto websocket_client = create_test_websocket_client(
12141241
/* receive function */ [call_number, connection_dropped_event]() mutable

test/signalrclienttests/websocket_transport_tests.cpp

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -128,30 +128,6 @@ TEST(websocket_transport_connect, can_connect_after_disconnecting)
128128
// shouldn't throw or crash
129129
}
130130

131-
TEST(websocket_transport_connect, transport_destroyed_even_if_disconnect_not_called)
132-
{
133-
auto client = std::make_shared<test_websocket_client>();
134-
{
135-
auto ws_transport = websocket_transport::create([&](){ return client; }, logger(std::make_shared<trace_log_writer>(), trace_level::none),
136-
[](const utility::string_t&){}, [](const std::exception&){});
137-
138-
ws_transport->connect(_XPLATSTR("ws://fakeuri.org")).get();
139-
}
140-
141-
// The receive loop may still run even if the transport goes out of scope above. Since the receive loop may hold
142-
// on to the websocket_transport instance by using a shared_ptr the actual instance may not be released even though
143-
// the ws_transport variable goes out of scope. Therefore we need to wait for the receive loop to finish but we
144-
// don't have any indicator when it happens since it is running on a separate and unsychnronized thread.
145-
for (int wait_time_ms = 10; wait_time_ms < 5000 && client.use_count() > 1; wait_time_ms <<= 1)
146-
{
147-
pplx::wait(wait_time_ms);
148-
}
149-
150-
// the idea is that if the transport is not destroyed it will hold a reference
151-
// to the client and therefore the `use_count()` will be greater than 1
152-
ASSERT_EQ(1, client.use_count());
153-
}
154-
155131
TEST(websocket_transport_send, send_creates_and_sends_websocket_messages)
156132
{
157133
bool send_called = false;

0 commit comments

Comments
 (0)