Skip to content

Commit eb2a8f0

Browse files
authored
fix: catch exception if en_US.utf8-locale missing when parsing datetime headers (#251)
The event delivery subsystem inspects the HTTP headers on the event endpoints whenever it posts a batch of events. It parses out the date header, storing it for later use. When emitting debug events, it is used to ensure that events stop emitting even if the host's time is off - as a form of time synchronization. The parsing code is quite ugly, and also wrong - it assumed that `en_US.utf-8` would always be available. This commit hoists the locale loading routine out of the individual flush-workers and into the worker pool. If it can't load the locale, it emits a single `warn` log at startup explaining the effect.
1 parent 69ac1c7 commit eb2a8f0

File tree

6 files changed

+66
-23
lines changed

6 files changed

+66
-23
lines changed

libs/internal/include/launchdarkly/events/parse_date_header.hpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88
namespace launchdarkly::events {
99

1010
template <typename Clock>
11+
1112
static std::optional<typename Clock::time_point> ParseDateHeader(
12-
std::string const& datetime) {
13+
std::string const& datetime,
14+
std::locale const& locale) {
1315
// The following comments may not be entirely accurate.
1416
// TODO: There must be a better way.
1517

1618
std::tm gmt_tm = {};
19+
1720
std::istringstream string_stream(datetime);
18-
string_stream.imbue(std::locale("en_US.utf-8"));
21+
string_stream.imbue(locale);
1922
string_stream >> std::get_time(&gmt_tm, "%a, %d %b %Y %H:%M:%S GMT");
2023
if (string_stream.fail()) {
2124
return std::nullopt;

libs/internal/include/launchdarkly/events/request_worker.hpp

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ enum class State {
2424
PermanentlyFailed = 4,
2525
};
2626

27-
std::ostream& operator<<(std::ostream& out, State const& s);
27+
std::ostream& operator<<(std::ostream& out, State const& state);
2828

2929
enum class Action {
3030
/* No action necessary. */
@@ -39,7 +39,7 @@ enum class Action {
3939
NotifyPermanentFailure = 4,
4040
};
4141

42-
std::ostream& operator<<(std::ostream& out, Action const& s);
42+
std::ostream& operator<<(std::ostream& out, Action const& state);
4343

4444
/**
4545
* Computes the next (state, action) pair from an existing state and an HTTP
@@ -99,12 +99,13 @@ class RequestWorker {
9999
RequestWorker(boost::asio::any_io_executor io,
100100
std::chrono::milliseconds retry_after,
101101
std::size_t id,
102+
std::optional<std::locale> date_header_locale,
102103
Logger& logger);
103104

104105
/**
105106
* Returns true if the worker is available for delivery.
106107
*/
107-
bool Available() const;
108+
[[nodiscard]] bool Available() const;
108109

109110
/**
110111
* Passes an EventBatch to the worker for delivery. The delivery may be
@@ -135,10 +136,10 @@ class RequestWorker {
135136
<< batch_->Target() << " with payload: "
136137
<< batch_->Request().Body().value_or("(no body)");
137138

138-
requester_.Request(
139-
batch_->Request(), [this, handler](network::HttpResult result) {
140-
OnDeliveryAttempt(std::move(result), std::move(handler));
141-
});
139+
requester_.Request(batch_->Request(),
140+
[this, handler](network::HttpResult const& result) {
141+
OnDeliveryAttempt(result, std::move(handler));
142+
});
142143
return result.get();
143144
}
144145

@@ -163,9 +164,14 @@ class RequestWorker {
163164
/* Tag used in logs. */
164165
std::string tag_;
165166

167+
/* The en_US locale is used to parse the Date header from HTTP responses.
168+
* On some platforms, this may not be available hence the optional. */
169+
std::optional<std::locale> date_header_locale_;
170+
166171
Logger& logger_;
167172

168-
void OnDeliveryAttempt(network::HttpResult request, ResultCallback cb);
173+
void OnDeliveryAttempt(network::HttpResult const& request,
174+
ResultCallback cb);
169175
};
170176

171177
} // namespace launchdarkly::events

libs/internal/include/launchdarkly/events/worker_pool.hpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ namespace launchdarkly::events {
2424
*/
2525
class WorkerPool {
2626
public:
27-
using ServerTimeCallback =
28-
std::function<void(std::chrono::system_clock::time_point)>;
2927
/**
3028
* Constructs a new WorkerPool.
3129
* @param io The executor used for all workers.

libs/internal/src/events/request_worker.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ namespace launchdarkly::events {
66
RequestWorker::RequestWorker(boost::asio::any_io_executor io,
77
std::chrono::milliseconds retry_after,
88
std::size_t id,
9+
std::optional<std::locale> date_header_locale,
910
Logger& logger)
10-
: timer_(io),
11+
: timer_(std::move(io)),
1112
retry_delay_(retry_after),
1213
state_(State::Idle),
1314
requester_(timer_.get_executor()),
1415
batch_(std::nullopt),
1516
tag_("flush-worker[" + std::to_string(id) + "]: "),
17+
date_header_locale_(std::move(date_header_locale)),
1618
logger_(logger) {}
1719

1820
bool RequestWorker::Available() const {
@@ -47,7 +49,7 @@ static bool IsSuccess(network::HttpResult const& result) {
4749
http::status_class::successful;
4850
}
4951

50-
void RequestWorker::OnDeliveryAttempt(network::HttpResult result,
52+
void RequestWorker::OnDeliveryAttempt(network::HttpResult const& result,
5153
ResultCallback callback) {
5254
auto [next_state, action] = NextState(state_, result);
5355

@@ -81,11 +83,15 @@ void RequestWorker::OnDeliveryAttempt(network::HttpResult result,
8183
batch_.reset();
8284
break;
8385
case Action::ParseDateAndReset: {
86+
if (!date_header_locale_) {
87+
batch_.reset();
88+
break;
89+
}
8490
auto headers = result.Headers();
8591
if (auto date = headers.find("Date"); date != headers.end()) {
8692
if (auto server_time =
8793
ParseDateHeader<std::chrono::system_clock>(
88-
date->second)) {
94+
date->second, *date_header_locale_)) {
8995
callback(batch_->Count(), *server_time);
9096
}
9197
}

libs/internal/src/events/worker_pool.cpp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,37 @@
55

66
namespace launchdarkly::events {
77

8+
std::optional<std::locale> GetLocale(std::string const& locale,
9+
std::string const& tag,
10+
Logger& logger) {
11+
try {
12+
return std::locale(locale);
13+
} catch (std::runtime_error) {
14+
LD_LOG(logger, LogLevel::kWarn)
15+
<< tag << " couldn't load " << locale
16+
<< " locale. If debug events are enabled, they may be emitted for "
17+
"longer than expected";
18+
return std::nullopt;
19+
}
20+
}
21+
822
WorkerPool::WorkerPool(boost::asio::any_io_executor io,
923
std::size_t pool_size,
1024
std::chrono::milliseconds delivery_retry_delay,
1125
Logger& logger)
1226
: io_(io), workers_() {
27+
// The en_US.utf-8 locale is used whenever a date is parsed from the HTTP
28+
// headers returned by the event-delivery endpoints. If the locale is
29+
// unavailable, then the workers will skip the parsing step.
30+
//
31+
// This may result in debug events being emitted for longer than expected
32+
// if the host's time is way out of sync.
33+
std::optional<std::locale> date_header_locale =
34+
GetLocale("en_US.utf-8", "event-processor", logger);
35+
1336
for (std::size_t i = 0; i < pool_size; i++) {
1437
workers_.emplace_back(std::make_unique<RequestWorker>(
15-
io_, delivery_retry_delay, i, logger));
38+
io_, delivery_retry_delay, i, date_header_locale, logger));
1639
}
1740
}
1841

libs/internal/tests/event_processor_test.cpp

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,15 @@ TEST(WorkerPool, PoolReturnsNullptrWhenNoWorkerAvaialable) {
5959
ioc_thread.join();
6060
}
6161

62+
class EventProcessorTests : public ::testing::Test {
63+
public:
64+
EventProcessorTests() : locale("en_US.utf-8") {}
65+
std::locale locale;
66+
};
67+
6268
// This test is a temporary test that exists only to ensure the event processor
6369
// compiles; it should be replaced by more robust tests (and contract tests.)
64-
TEST(EventProcessorTests, ProcessorCompiles) {
70+
TEST_F(EventProcessorTests, ProcessorCompiles) {
6571
using namespace launchdarkly;
6672

6773
Logger logger{
@@ -95,33 +101,34 @@ TEST(EventProcessorTests, ProcessorCompiles) {
95101
ioc_thread.join();
96102
}
97103

98-
TEST(EventProcessorTests, ParseValidDateHeader) {
104+
TEST_F(EventProcessorTests, ParseValidDateHeader) {
99105
using namespace launchdarkly;
100106

101107
using Clock = std::chrono::system_clock;
102-
auto date = events::ParseDateHeader<Clock>("Wed, 21 Oct 2015 07:28:00 GMT");
108+
auto date =
109+
events::ParseDateHeader<Clock>("Wed, 21 Oct 2015 07:28:00 GMT", locale);
103110

104111
ASSERT_TRUE(date);
105112

106113
ASSERT_EQ(date->time_since_epoch(),
107114
std::chrono::microseconds(1445412480000000));
108115
}
109116

110-
TEST(EventProcessorTests, ParseInvalidDateHeader) {
117+
TEST_F(EventProcessorTests, ParseInvalidDateHeader) {
111118
using namespace launchdarkly;
112119

113120
auto not_a_date = events::ParseDateHeader<std::chrono::system_clock>(
114-
"this is definitely not a date");
121+
"this is definitely not a date", locale);
115122

116123
ASSERT_FALSE(not_a_date);
117124

118125
auto not_gmt = events::ParseDateHeader<std::chrono::system_clock>(
119-
"Wed, 21 Oct 2015 07:28:00 PST");
126+
"Wed, 21 Oct 2015 07:28:00 PST", locale);
120127

121128
ASSERT_FALSE(not_gmt);
122129

123130
auto missing_year = events::ParseDateHeader<std::chrono::system_clock>(
124-
"Wed, 21 Oct 07:28:00 GMT");
131+
"Wed, 21 Oct 07:28:00 GMT", locale);
125132

126133
ASSERT_FALSE(missing_year);
127134
}

0 commit comments

Comments
 (0)