JsonEncoder: lock olock conditionally & flush output regularly

This commit is contained in:
Yonas Habteab 2025-07-03 18:02:30 +02:00
parent 398b5e3193
commit dad4c0889f
2 changed files with 71 additions and 18 deletions

View File

@ -23,7 +23,7 @@ JsonEncoder::JsonEncoder(std::basic_ostream<char>& stream, bool prettify)
} }
JsonEncoder::JsonEncoder(nlohmann::detail::output_adapter_t<char> w, bool prettify) JsonEncoder::JsonEncoder(nlohmann::detail::output_adapter_t<char> w, bool prettify)
: m_Pretty(prettify), m_Writer(std::move(w)) : m_IsAsyncWriter{dynamic_cast<AsyncJsonWriter*>(w.get()) != nullptr}, m_Pretty(prettify), m_Writer(std::move(w))
{ {
} }
@ -35,9 +35,18 @@ JsonEncoder::JsonEncoder(nlohmann::detail::output_adapter_t<char> w, bool pretti
* If prettifying is enabled, the JSON output will be formatted with indentation and newlines for better * If prettifying is enabled, the JSON output will be formatted with indentation and newlines for better
* readability, and the final JSON will also be terminated by a newline character. * readability, and the final JSON will also be terminated by a newline character.
* *
* @note If the used output adapter performs asynchronous I/O operations (it's derived from @c AsyncJsonWriter),
* please provide a @c boost::asio::yield_context object to allow the encoder to flush the output stream in a
* safe manner. The encoder will try to regularly give the output stream a chance to flush its data when it is
* safe to do so, but for this to work, there must be a valid yield context provided. Otherwise, the encoder
* will not attempt to flush the output stream at all, which may lead to huge memory consumption when encoding
* large JSON objects or arrays.
*
* @param value The value to be JSON serialized. * @param value The value to be JSON serialized.
* @param yc The optional yield context for asynchronous operations. If provided, it allows the encoder
* to flush the output stream safely when it has not acquired any object lock on the parent containers.
*/ */
void JsonEncoder::Encode(const Value& value) void JsonEncoder::Encode(const Value& value, boost::asio::yield_context* yc)
{ {
switch (value.GetType()) { switch (value.GetType()) {
case ValueEmpty: case ValueEmpty:
@ -57,14 +66,14 @@ void JsonEncoder::Encode(const Value& value)
const auto& type = obj->GetReflectionType(); const auto& type = obj->GetReflectionType();
if (type == Namespace::TypeInstance) { if (type == Namespace::TypeInstance) {
static constexpr auto extractor = [](const NamespaceValue& v) -> const Value& { return v.Val; }; static constexpr auto extractor = [](const NamespaceValue& v) -> const Value& { return v.Val; };
EncodeObject(static_pointer_cast<Namespace>(obj), extractor); EncodeObject(static_pointer_cast<Namespace>(obj), extractor, yc);
} else if (type == Dictionary::TypeInstance) { } else if (type == Dictionary::TypeInstance) {
static constexpr auto extractor = [](const Value& v) -> const Value& { return v; }; static constexpr auto extractor = [](const Value& v) -> const Value& { return v; };
EncodeObject(static_pointer_cast<Dictionary>(obj), extractor); EncodeObject(static_pointer_cast<Dictionary>(obj), extractor, yc);
} else if (type == Array::TypeInstance) { } else if (type == Array::TypeInstance) {
EncodeArray(static_pointer_cast<Array>(obj)); EncodeArray(static_pointer_cast<Array>(obj), yc);
} else if (auto gen(dynamic_pointer_cast<ValueGenerator>(obj)); gen) { } else if (auto gen(dynamic_pointer_cast<ValueGenerator>(obj)); gen) {
EncodeValueGenerator(gen); EncodeValueGenerator(gen, yc);
} else { } else {
// Some other non-serializable object type! // Some other non-serializable object type!
EncodeNlohmannJson(Utility::ValidateUTF8(obj->ToString())); EncodeNlohmannJson(Utility::ValidateUTF8(obj->ToString()));
@ -86,16 +95,23 @@ void JsonEncoder::Encode(const Value& value)
* Encodes an Array object into JSON and writes it to the output stream. * Encodes an Array object into JSON and writes it to the output stream.
* *
* @param array The Array object to be serialized into JSON. * @param array The Array object to be serialized into JSON.
* @param yc The optional yield context for asynchronous operations. If provided, it allows the encoder
* to flush the output stream safely when it has not acquired any object lock.
*/ */
void JsonEncoder::EncodeArray(const Array::Ptr& array) void JsonEncoder::EncodeArray(const Array::Ptr& array, boost::asio::yield_context* yc)
{ {
BeginContainer('['); BeginContainer('[');
ObjectLock olock(array); auto olock = array->LockIfRequired();
if (olock) {
yc = nullptr; // We've acquired an object lock, never allow asynchronous operations.
}
bool isEmpty = true; bool isEmpty = true;
for (const auto& item : array) { for (const auto& item : array) {
WriteSeparatorAndIndentStrIfNeeded(!isEmpty); WriteSeparatorAndIndentStrIfNeeded(!isEmpty);
isEmpty = false; isEmpty = false;
Encode(item); Encode(item, yc);
FlushIfSafe(yc);
} }
EndContainer(']', isEmpty); EndContainer(']', isEmpty);
} }
@ -106,15 +122,18 @@ void JsonEncoder::EncodeArray(const Array::Ptr& array)
* This will iterate through the generator, encoding each value it produces until it is exhausted. * This will iterate through the generator, encoding each value it produces until it is exhausted.
* *
* @param generator The ValueGenerator object to be serialized into JSON. * @param generator The ValueGenerator object to be serialized into JSON.
* @param yc The optional yield context for asynchronous operations. If provided, it allows the encoder
* to flush the output stream safely when it has not acquired any object lock on the parent containers.
*/ */
void JsonEncoder::EncodeValueGenerator(const ValueGenerator::Ptr& generator) void JsonEncoder::EncodeValueGenerator(const ValueGenerator::Ptr& generator, boost::asio::yield_context* yc)
{ {
BeginContainer('['); BeginContainer('[');
bool isEmpty = true; bool isEmpty = true;
while (auto result = generator->Next()) { while (auto result = generator->Next()) {
WriteSeparatorAndIndentStrIfNeeded(!isEmpty); WriteSeparatorAndIndentStrIfNeeded(!isEmpty);
isEmpty = false; isEmpty = false;
Encode(*result); Encode(*result, yc);
FlushIfSafe(yc);
} }
EndContainer(']', isEmpty); EndContainer(']', isEmpty);
} }
@ -127,15 +146,21 @@ void JsonEncoder::EncodeValueGenerator(const ValueGenerator::Ptr& generator)
* *
* @param container The container to JSON serialize. * @param container The container to JSON serialize.
* @param extractor The value extractor function used to extract values from the container's iterator. * @param extractor The value extractor function used to extract values from the container's iterator.
* @param yc The optional yield context for asynchronous operations. It will only be set when the encoder
* has not acquired any object lock on the parent containers, allowing safe asynchronous operations.
*/ */
template<typename Iterable, typename ValExtractor> template<typename Iterable, typename ValExtractor>
void JsonEncoder::EncodeObject(const Iterable& container, const ValExtractor& extractor) void JsonEncoder::EncodeObject(const Iterable& container, const ValExtractor& extractor, boost::asio::yield_context* yc)
{ {
static_assert(std::is_same_v<Iterable, Namespace::Ptr> || std::is_same_v<Iterable, Dictionary::Ptr>, static_assert(std::is_same_v<Iterable, Namespace::Ptr> || std::is_same_v<Iterable, Dictionary::Ptr>,
"Container must be a Namespace or Dictionary"); "Container must be a Namespace or Dictionary");
BeginContainer('{'); BeginContainer('{');
ObjectLock olock(container); auto olock = container->LockIfRequired();
if (olock) {
yc = nullptr; // We've acquired an object lock, never allow asynchronous operations.
}
bool isEmpty = true; bool isEmpty = true;
for (const auto& [key, val] : container) { for (const auto& [key, val] : container) {
WriteSeparatorAndIndentStrIfNeeded(!isEmpty); WriteSeparatorAndIndentStrIfNeeded(!isEmpty);
@ -143,7 +168,9 @@ void JsonEncoder::EncodeObject(const Iterable& container, const ValExtractor& ex
EncodeNlohmannJson(Utility::ValidateUTF8(key)); EncodeNlohmannJson(Utility::ValidateUTF8(key));
Write(m_Pretty ? ": " : ":"); Write(m_Pretty ? ": " : ":");
Encode(extractor(val));
Encode(extractor(val), yc);
FlushIfSafe(yc);
} }
EndContainer('}', isEmpty); EndContainer('}', isEmpty);
} }
@ -193,6 +220,29 @@ void JsonEncoder::EncodeNumber(double value) const
EncodeNlohmannJson(value); EncodeNlohmannJson(value);
} }
/**
* Flushes the output stream if it is safe to do so.
*
* Safe flushing means that it only performs the flush operation if the @c JsonEncoder has not acquired
* any object lock so far. This is to ensure that the stream can safely perform asynchronous operations
* without risking undefined behaviour due to coroutines being suspended while the stream is being flushed.
*
* When the @c yc parameter is provided, it indicates that it's safe to perform asynchronous operations,
* and the function will attempt to flush if the writer is an instance of @c AsyncJsonWriter.
*
* @param yc The yield context to use for asynchronous operations.
*/
void JsonEncoder::FlushIfSafe(boost::asio::yield_context* yc) const
{
if (yc && m_IsAsyncWriter) {
// The m_IsAsyncWriter flag is a constant, and it will never change, so we can safely static
// cast the m_Writer to AsyncJsonWriter without any additional checks as it is guaranteed
// to be an instance of AsyncJsonWriter when m_IsAsyncWriter is true.
auto ajw(static_cast<AsyncJsonWriter*>(m_Writer.get()));
ajw->Flush(*yc);
}
}
/** /**
* Writes a string to the underlying output stream. * Writes a string to the underlying output stream.
* *

View File

@ -73,18 +73,20 @@ public:
explicit JsonEncoder(std::basic_ostream<char>& stream, bool prettify = false); explicit JsonEncoder(std::basic_ostream<char>& stream, bool prettify = false);
explicit JsonEncoder(nlohmann::detail::output_adapter_t<char> w, bool prettify = false); explicit JsonEncoder(nlohmann::detail::output_adapter_t<char> w, bool prettify = false);
void Encode(const Value& value); void Encode(const Value& value, boost::asio::yield_context* yc = nullptr);
private: private:
void EncodeArray(const Array::Ptr& array); void EncodeArray(const Array::Ptr& array, boost::asio::yield_context* yc);
void EncodeValueGenerator(const ValueGenerator::Ptr& generator); void EncodeValueGenerator(const ValueGenerator::Ptr& generator, boost::asio::yield_context* yc);
template<typename Iterable, typename ValExtractor> template<typename Iterable, typename ValExtractor>
void EncodeObject(const Iterable& container, const ValExtractor& extractor); void EncodeObject(const Iterable& container, const ValExtractor& extractor, boost::asio::yield_context* yc);
void EncodeNlohmannJson(const nlohmann::json& json) const; void EncodeNlohmannJson(const nlohmann::json& json) const;
void EncodeNumber(double value) const; void EncodeNumber(double value) const;
void FlushIfSafe(boost::asio::yield_context* yc) const;
void Write(const std::string_view& sv) const; void Write(const std::string_view& sv) const;
void BeginContainer(char openChar); void BeginContainer(char openChar);
void EndContainer(char closeChar, bool isContainerEmpty = false); void EndContainer(char closeChar, bool isContainerEmpty = false);
@ -93,6 +95,7 @@ private:
// The number of spaces to use for indentation in prettified JSON. // The number of spaces to use for indentation in prettified JSON.
static constexpr uint8_t m_IndentSize = 4; static constexpr uint8_t m_IndentSize = 4;
const bool m_IsAsyncWriter; // Whether the writer is an instance of AsyncJsonWriter.
bool m_Pretty; // Whether to pretty-print the JSON output. bool m_Pretty; // Whether to pretty-print the JSON output.
unsigned m_Indent{0}; // The current indentation level for pretty-printing. unsigned m_Indent{0}; // The current indentation level for pretty-printing.
/** /**