Skip to content

fix: catch exception if en_US.utf8-locale missing when parsing datetime headers #251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
namespace launchdarkly::events {

template <typename Clock>

static std::optional<typename Clock::time_point> ParseDateHeader(
std::string const& datetime) {
std::string const& datetime,
std::locale const& locale) {
// The following comments may not be entirely accurate.
// TODO: There must be a better way.

std::tm gmt_tm = {};

std::istringstream string_stream(datetime);
string_stream.imbue(std::locale("en_US.utf-8"));
string_stream.imbue(locale);
string_stream >> std::get_time(&gmt_tm, "%a, %d %b %Y %H:%M:%S GMT");
if (string_stream.fail()) {
return std::nullopt;
Expand Down
22 changes: 14 additions & 8 deletions libs/internal/include/launchdarkly/events/request_worker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ enum class State {
PermanentlyFailed = 4,
};

std::ostream& operator<<(std::ostream& out, State const& s);
std::ostream& operator<<(std::ostream& out, State const& state);

enum class Action {
/* No action necessary. */
Expand All @@ -39,7 +39,7 @@ enum class Action {
NotifyPermanentFailure = 4,
};

std::ostream& operator<<(std::ostream& out, Action const& s);
std::ostream& operator<<(std::ostream& out, Action const& state);

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

/**
* Returns true if the worker is available for delivery.
*/
bool Available() const;
[[nodiscard]] bool Available() const;

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

requester_.Request(
batch_->Request(), [this, handler](network::HttpResult result) {
OnDeliveryAttempt(std::move(result), std::move(handler));
});
requester_.Request(batch_->Request(),
[this, handler](network::HttpResult const& result) {
OnDeliveryAttempt(result, std::move(handler));
});
return result.get();
}

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

/* The en_US locale is used to parse the Date header from HTTP responses.
* On some platforms, this may not be available hence the optional. */
std::optional<std::locale> date_header_locale_;

Logger& logger_;

void OnDeliveryAttempt(network::HttpResult request, ResultCallback cb);
void OnDeliveryAttempt(network::HttpResult const& request,
ResultCallback cb);
};

} // namespace launchdarkly::events
2 changes: 0 additions & 2 deletions libs/internal/include/launchdarkly/events/worker_pool.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ namespace launchdarkly::events {
*/
class WorkerPool {
public:
using ServerTimeCallback =
std::function<void(std::chrono::system_clock::time_point)>;
/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(unused)

* Constructs a new WorkerPool.
* @param io The executor used for all workers.
Expand Down
12 changes: 9 additions & 3 deletions libs/internal/src/events/request_worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ namespace launchdarkly::events {
RequestWorker::RequestWorker(boost::asio::any_io_executor io,
std::chrono::milliseconds retry_after,
std::size_t id,
std::optional<std::locale> date_header_locale,
Logger& logger)
: timer_(io),
: timer_(std::move(io)),
retry_delay_(retry_after),
state_(State::Idle),
requester_(timer_.get_executor()),
batch_(std::nullopt),
tag_("flush-worker[" + std::to_string(id) + "]: "),
date_header_locale_(std::move(date_header_locale)),
logger_(logger) {}

bool RequestWorker::Available() const {
Expand Down Expand Up @@ -47,7 +49,7 @@ static bool IsSuccess(network::HttpResult const& result) {
http::status_class::successful;
}

void RequestWorker::OnDeliveryAttempt(network::HttpResult result,
void RequestWorker::OnDeliveryAttempt(network::HttpResult const& result,
ResultCallback callback) {
auto [next_state, action] = NextState(state_, result);

Expand Down Expand Up @@ -81,11 +83,15 @@ void RequestWorker::OnDeliveryAttempt(network::HttpResult result,
batch_.reset();
break;
case Action::ParseDateAndReset: {
if (!date_header_locale_) {
batch_.reset();
break;
}
auto headers = result.Headers();
if (auto date = headers.find("Date"); date != headers.end()) {
if (auto server_time =
ParseDateHeader<std::chrono::system_clock>(
date->second)) {
date->second, *date_header_locale_)) {
callback(batch_->Count(), *server_time);
}
}
Expand Down
25 changes: 24 additions & 1 deletion libs/internal/src/events/worker_pool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,37 @@

namespace launchdarkly::events {

std::optional<std::locale> GetLocale(std::string const& locale,
std::string const& tag,
Logger& logger) {
try {
return std::locale(locale);
} catch (std::runtime_error) {
LD_LOG(logger, LogLevel::kWarn)
<< tag << " couldn't load " << locale
<< " locale. If debug events are enabled, they may be emitted for "
"longer than expected";
return std::nullopt;
}
}

WorkerPool::WorkerPool(boost::asio::any_io_executor io,
std::size_t pool_size,
std::chrono::milliseconds delivery_retry_delay,
Logger& logger)
: io_(io), workers_() {
// The en_US.utf-8 locale is used whenever a date is parsed from the HTTP
// headers returned by the event-delivery endpoints. If the locale is
// unavailable, then the workers will skip the parsing step.
//
// This may result in debug events being emitted for longer than expected
// if the host's time is way out of sync.
std::optional<std::locale> date_header_locale =
GetLocale("en_US.utf-8", "event-processor", logger);

for (std::size_t i = 0; i < pool_size; i++) {
workers_.emplace_back(std::make_unique<RequestWorker>(
io_, delivery_retry_delay, i, logger));
io_, delivery_retry_delay, i, date_header_locale, logger));
}
}

Expand Down
21 changes: 14 additions & 7 deletions libs/internal/tests/event_processor_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,15 @@ TEST(WorkerPool, PoolReturnsNullptrWhenNoWorkerAvaialable) {
ioc_thread.join();
}

class EventProcessorTests : public ::testing::Test {
public:
EventProcessorTests() : locale("en_US.utf-8") {}
std::locale locale;
};

// This test is a temporary test that exists only to ensure the event processor
// compiles; it should be replaced by more robust tests (and contract tests.)
TEST(EventProcessorTests, ProcessorCompiles) {
TEST_F(EventProcessorTests, ProcessorCompiles) {
using namespace launchdarkly;

Logger logger{
Expand Down Expand Up @@ -95,33 +101,34 @@ TEST(EventProcessorTests, ProcessorCompiles) {
ioc_thread.join();
}

TEST(EventProcessorTests, ParseValidDateHeader) {
TEST_F(EventProcessorTests, ParseValidDateHeader) {
using namespace launchdarkly;

using Clock = std::chrono::system_clock;
auto date = events::ParseDateHeader<Clock>("Wed, 21 Oct 2015 07:28:00 GMT");
auto date =
events::ParseDateHeader<Clock>("Wed, 21 Oct 2015 07:28:00 GMT", locale);

ASSERT_TRUE(date);

ASSERT_EQ(date->time_since_epoch(),
std::chrono::microseconds(1445412480000000));
}

TEST(EventProcessorTests, ParseInvalidDateHeader) {
TEST_F(EventProcessorTests, ParseInvalidDateHeader) {
using namespace launchdarkly;

auto not_a_date = events::ParseDateHeader<std::chrono::system_clock>(
"this is definitely not a date");
"this is definitely not a date", locale);

ASSERT_FALSE(not_a_date);

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

ASSERT_FALSE(not_gmt);

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

ASSERT_FALSE(missing_year);
}