Skip to content

Commit dfd1f63

Browse files
Add handshake timeout (#60)
1 parent d77e3ee commit dfd1f63

File tree

7 files changed

+239
-14
lines changed

7 files changed

+239
-14
lines changed

include/signalrclient/signalr_client_config.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ namespace signalr
4545
SIGNALRCLIENT_API void __cdecl set_http_headers(const std::map<std::string, std::string>& http_headers);
4646
SIGNALRCLIENT_API void __cdecl set_scheduler(std::shared_ptr<scheduler> scheduler);
4747
SIGNALRCLIENT_API const std::shared_ptr<scheduler>& __cdecl get_scheduler() const noexcept;
48+
SIGNALRCLIENT_API void set_handshake_timeout(std::chrono::milliseconds);
49+
SIGNALRCLIENT_API std::chrono::milliseconds get_handshake_timeout() const noexcept;
4850

4951
private:
5052
#ifdef USE_CPPRESTSDK
@@ -53,5 +55,6 @@ namespace signalr
5355
#endif
5456
std::map<std::string, std::string> m_http_headers;
5557
std::shared_ptr<scheduler> m_scheduler;
58+
std::chrono::milliseconds m_handshake_timeout;
5659
};
5760
}

src/signalrclient/completion_event.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ namespace signalr
5050
m_future.get();
5151
}
5252

53+
bool is_set() const
54+
{
55+
return m_isSet;
56+
}
57+
5358
private:
5459
completion_event_impl() : m_isSet(false)
5560
{
@@ -91,6 +96,11 @@ namespace signalr
9196
{
9297
m_impl->get();
9398
}
99+
100+
bool is_set() const
101+
{
102+
return m_impl->is_set();
103+
}
94104
private:
95105
std::shared_ptr<completion_event_impl> m_impl;
96106
};

src/signalrclient/hub_connection_impl.cpp

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
#include "message_type.h"
1212
#include "handshake_protocol.h"
1313
#include "signalrclient/websocket_client.h"
14-
#include "messagepack_hub_protocol.h"
14+
#include "signalr_default_scheduler.h"
1515

1616
namespace signalr
1717
{
@@ -65,6 +65,7 @@ namespace signalr
6565
{
6666
// start may be waiting on the handshake response so we complete it here, this no-ops if already set
6767
connection->m_handshakeTask->set(std::make_exception_ptr(signalr_exception("connection closed while handshake was in progress.")));
68+
connection->m_disconnect_cts->cancel();
6869

6970
connection->m_callback_manager.clear("connection was stopped before invocation result was received");
7071

@@ -107,6 +108,7 @@ namespace signalr
107108

108109
m_connection->set_client_config(m_signalr_client_config);
109110
m_handshakeTask = std::make_shared<completion_event>();
111+
m_disconnect_cts = std::make_shared<cancellation_token>();
110112
m_handshakeReceived = false;
111113
std::weak_ptr<hub_connection_impl> weak_connection = shared_from_this();
112114
m_connection->start([weak_connection, callback](std::exception_ptr start_exception)
@@ -139,10 +141,13 @@ namespace signalr
139141
return;
140142
}
141143

142-
auto handshake_request = handshake::write_handshake(connection->m_protocol);
144+
std::shared_ptr<std::mutex> handshake_request_lock = std::make_shared<std::mutex>();
145+
std::shared_ptr<bool> handshake_request_done = std::make_shared<bool>();
143146

144-
connection->m_connection->send(handshake_request, connection->m_protocol->transfer_format(), [weak_connection, callback](std::exception_ptr exception)
147+
auto handle_handshake = [weak_connection, handshake_request_done, handshake_request_lock, callback](std::exception_ptr exception, bool fromSend)
145148
{
149+
assert(fromSend ? *handshake_request_done : true);
150+
146151
auto connection = weak_connection.lock();
147152
if (!connection)
148153
{
@@ -151,25 +156,103 @@ namespace signalr
151156
return;
152157
}
153158

154-
if (exception)
155159
{
156-
callback(exception);
157-
return;
160+
std::lock_guard<std::mutex> lock(*handshake_request_lock);
161+
// connection.send will be waiting on the handshake task which has been set by the caller already
162+
if (!fromSend && *handshake_request_done == true)
163+
{
164+
return;
165+
}
166+
*handshake_request_done = true;
158167
}
159168

160169
try
161170
{
162-
connection->m_handshakeTask->get();
163-
callback(nullptr);
171+
if (exception == nullptr)
172+
{
173+
connection->m_handshakeTask->get();
174+
callback(nullptr);
175+
}
164176
}
165177
catch (...)
166178
{
167-
auto handshake_exception = std::current_exception();
168-
connection->m_connection->stop([callback, handshake_exception](std::exception_ptr)
179+
exception = std::current_exception();
180+
}
181+
182+
if (exception != nullptr)
183+
{
184+
connection->m_connection->stop([callback, exception](std::exception_ptr)
185+
{
186+
callback(exception);
187+
}, exception);
188+
}
189+
};
190+
191+
auto handshake_request = handshake::write_handshake(connection->m_protocol);
192+
auto handshake_task = connection->m_handshakeTask;
193+
auto handshake_timeout = connection->m_signalr_client_config.get_handshake_timeout();
194+
195+
connection->m_disconnect_cts->register_callback([handle_handshake, handshake_request_lock, handshake_request_done]()
196+
{
197+
{
198+
std::lock_guard<std::mutex> lock(*handshake_request_lock);
199+
// no op after connection.send returned, m_handshakeTask should be set before m_disconnect_cts is canceled
200+
if (*handshake_request_done == true)
201+
{
202+
return;
203+
}
204+
}
205+
206+
// if the request isn't completed then no one is waiting on the handshake task
207+
// so we need to run the callback here instead of relying on connection.send completing
208+
// handshake_request_done is set in handle_handshake, don't set it here
209+
handle_handshake(nullptr, false);
210+
});
211+
212+
timer(connection->m_signalr_client_config.get_scheduler(),
213+
[handle_handshake, handshake_task, handshake_timeout, handshake_request_lock](std::chrono::milliseconds duration)
214+
{
215+
{
216+
std::lock_guard<std::mutex> lock(*handshake_request_lock);
217+
218+
// if the task is set then connection.send is either already waiting on the handshake or has completed,
219+
// or stop has been called and will be handling the callback
220+
if (handshake_task->is_set())
221+
{
222+
return true;
223+
}
224+
225+
if (duration < handshake_timeout)
226+
{
227+
return false;
228+
}
229+
}
230+
231+
auto exception = std::make_exception_ptr(signalr_exception("timed out waiting for the server to respond to the handshake message."));
232+
// unblocks connection.send if it's waiting on the task
233+
handshake_task->set(exception);
234+
235+
handle_handshake(exception, false);
236+
return true;
237+
});
238+
239+
connection->m_connection->send(handshake_request, connection->m_protocol->transfer_format(),
240+
[handle_handshake, handshake_request_done, handshake_request_lock](std::exception_ptr exception)
241+
{
242+
{
243+
std::lock_guard<std::mutex> lock(*handshake_request_lock);
244+
if (*handshake_request_done == true)
169245
{
170-
callback(handshake_exception);
171-
}, nullptr);
246+
// callback ran from timer or cancellation token, nothing to do here
247+
return;
248+
}
249+
250+
// indicates that the handshake timer doesn't need to call the callback, it just needs to set the timeout exception
251+
// handle_handshake will be waiting on the handshake completion (error or success) to call the callback
252+
*handshake_request_done = true;
172253
}
254+
255+
handle_handshake(exception, true);
173256
});
174257
});
175258
}
@@ -205,6 +288,8 @@ namespace signalr
205288
return;
206289
}
207290

291+
assert(connection->get_connection_state() == connection_state::disconnected);
292+
208293
std::vector<std::function<void(std::exception_ptr)>> callbacks;
209294

210295
{
@@ -251,6 +336,7 @@ namespace signalr
251336
if (found != obj.end())
252337
{
253338
m_handshakeTask->set(std::make_exception_ptr(signalr_exception(std::string("Received unexpected message while waiting for the handshake response."))));
339+
return;
254340
}
255341

256342
m_handshakeReceived = true;

src/signalrclient/hub_connection_impl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ namespace signalr
5858
bool m_handshakeReceived;
5959
std::shared_ptr<completion_event> m_handshakeTask;
6060
std::function<void(std::exception_ptr)> m_disconnected;
61+
std::shared_ptr<cancellation_token> m_disconnect_cts;
6162
signalr_client_config m_signalr_client_config;
6263
std::unique_ptr<hub_protocol> m_protocol;
6364

src/signalrclient/signalr_client_config.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ namespace signalr
4343
#endif
4444

4545
signalr_client_config::signalr_client_config()
46+
: m_handshake_timeout(std::chrono::seconds(15))
4647
{
4748
m_scheduler = std::make_shared<signalr_default_scheduler>();
4849
}
@@ -76,4 +77,19 @@ namespace signalr
7677
{
7778
return m_scheduler;
7879
}
80+
81+
void signalr_client_config::set_handshake_timeout(std::chrono::milliseconds timeout)
82+
{
83+
if (timeout <= std::chrono::seconds(0))
84+
{
85+
throw std::runtime_error("timeout must be greater than 0.");
86+
}
87+
88+
m_handshake_timeout = timeout;
89+
}
90+
91+
std::chrono::milliseconds signalr_client_config::get_handshake_timeout() const noexcept
92+
{
93+
return m_handshake_timeout;
94+
}
7995
}

src/signalrclient/signalr_default_scheduler.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,9 @@ namespace signalr
148148
// find the first callback that is ready to run, find a thread to run it and remove it from the list
149149
auto curr_time = std::chrono::steady_clock::now();
150150
auto it = callbacks.begin();
151-
auto found = false;
152151
while (it != callbacks.end())
153152
{
153+
auto found = false;
154154
if (it->second <= curr_time)
155155
{
156156
for (auto& thread : threads)

test/signalrclienttests/hub_connection_tests.cpp

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ TEST(start, start_fails_for_handshake_response_with_error)
210210
{
211211
auto websocket_client = create_test_websocket_client();
212212
auto hub_connection = create_hub_connection(websocket_client);
213+
std::exception_ptr exception;
214+
hub_connection.set_disconnected([&exception](std::exception_ptr ex)
215+
{
216+
exception = ex;
217+
});
213218

214219
auto mre = manual_reset_event<void>();
215220
hub_connection.start([&mre](std::exception_ptr exception)
@@ -232,6 +237,80 @@ TEST(start, start_fails_for_handshake_response_with_error)
232237
}
233238

234239
ASSERT_EQ(connection_state::disconnected, hub_connection.get_connection_state());
240+
241+
try
242+
{
243+
ASSERT_NE(nullptr, exception);
244+
std::rethrow_exception(exception);
245+
}
246+
catch (const std::exception& ex)
247+
{
248+
ASSERT_STREQ("Received an error during handshake: bad things", ex.what());
249+
}
250+
}
251+
252+
TEST(start, start_fails_if_non_handshake_message_received)
253+
{
254+
auto websocket_client = create_test_websocket_client();
255+
auto hub_connection = create_hub_connection(websocket_client);
256+
257+
auto mre = manual_reset_event<void>();
258+
hub_connection.start([&mre](std::exception_ptr exception)
259+
{
260+
mre.set(exception);
261+
});
262+
263+
ASSERT_FALSE(websocket_client->receive_loop_started.wait(5000));
264+
ASSERT_FALSE(websocket_client->handshake_sent.wait(5000));
265+
websocket_client->receive_message("{\"arguments\":[1,\"Foo\"],\"target\":\"Target\",\"type\":1}\x1e");
266+
267+
try
268+
{
269+
mre.get();
270+
ASSERT_TRUE(false);
271+
}
272+
catch (const std::exception& ex)
273+
{
274+
ASSERT_STREQ("Received unexpected message while waiting for the handshake response.", ex.what());
275+
}
276+
277+
ASSERT_EQ(connection_state::disconnected, hub_connection.get_connection_state());
278+
}
279+
280+
TEST(start, on_not_called_if_multiple_messages_received_before_handshake)
281+
{
282+
auto websocket_client = create_test_websocket_client();
283+
auto hub_connection = create_hub_connection(websocket_client);
284+
285+
bool on_called = false;
286+
hub_connection.on("Target", [&on_called](signalr::value)
287+
{
288+
on_called = true;
289+
});
290+
291+
auto mre = manual_reset_event<void>();
292+
hub_connection.start([&mre](std::exception_ptr exception)
293+
{
294+
mre.set(exception);
295+
});
296+
297+
ASSERT_FALSE(websocket_client->receive_loop_started.wait(5000));
298+
ASSERT_FALSE(websocket_client->handshake_sent.wait(5000));
299+
websocket_client->receive_message("{\"arguments\":[1,\"Foo\"],\"target\":\"Target\",\"type\":1}\x1e{\"arguments\":[1,\"Foo\"],\"target\":\"Target\",\"type\":1}\x1e");
300+
301+
try
302+
{
303+
mre.get();
304+
ASSERT_TRUE(false);
305+
}
306+
catch (const std::exception& ex)
307+
{
308+
ASSERT_STREQ("Received unexpected message while waiting for the handshake response.", ex.what());
309+
}
310+
311+
ASSERT_EQ(connection_state::disconnected, hub_connection.get_connection_state());
312+
313+
ASSERT_FALSE(on_called);
235314
}
236315

237316
TEST(start, start_fails_for_incomplete_handshake_response)
@@ -319,6 +398,36 @@ TEST(start, start_fails_if_stop_called_before_handshake_response)
319398
ASSERT_EQ(connection_state::disconnected, hub_connection.get_connection_state());
320399
}
321400

401+
TEST(start, start_fails_if_handshake_times_out)
402+
{
403+
auto websocket_client = create_test_websocket_client();
404+
auto hub_connection = create_hub_connection(websocket_client);
405+
auto config = signalr_client_config();
406+
config.set_handshake_timeout(std::chrono::seconds(1));
407+
hub_connection.set_client_config(config);
408+
409+
auto mre = manual_reset_event<void>();
410+
hub_connection.start([&mre](std::exception_ptr exception)
411+
{
412+
mre.set(exception);
413+
});
414+
415+
ASSERT_FALSE(websocket_client->receive_loop_started.wait(5000));
416+
ASSERT_FALSE(websocket_client->handshake_sent.wait(5000));
417+
418+
try
419+
{
420+
mre.get();
421+
ASSERT_TRUE(false);
422+
}
423+
catch (const std::exception& ex)
424+
{
425+
ASSERT_STREQ("timed out waiting for the server to respond to the handshake message.", ex.what());
426+
}
427+
428+
ASSERT_EQ(connection_state::disconnected, hub_connection.get_connection_state());
429+
}
430+
322431
TEST(start, propogates_exception_from_negotiate)
323432
{
324433
auto http_client = std::make_shared<test_http_client>([](const std::string& url, http_request) -> http_response
@@ -1624,7 +1733,7 @@ TEST(config, can_replace_scheduler)
16241733

16251734
mre.get();
16261735

1627-
ASSERT_EQ(6, scheduler->schedule_count);
1736+
ASSERT_EQ(7, scheduler->schedule_count);
16281737
}
16291738

16301739
class throw_hub_protocol : public hub_protocol

0 commit comments

Comments
 (0)