From dad4c0889f753781b64d67466bdef0d2c1f1e00d Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Thu, 3 Jul 2025 18:02:30 +0200 Subject: [PATCH] JsonEncoder: lock olock conditionally & flush output regularly --- lib/base/json.cpp | 78 ++++++++++++++++++++++++++++++++++++++--------- lib/base/json.hpp | 11 ++++--- 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/lib/base/json.cpp b/lib/base/json.cpp index 74bc9f63a..e2749bbde 100644 --- a/lib/base/json.cpp +++ b/lib/base/json.cpp @@ -23,7 +23,7 @@ JsonEncoder::JsonEncoder(std::basic_ostream& stream, bool prettify) } JsonEncoder::JsonEncoder(nlohmann::detail::output_adapter_t w, bool prettify) - : m_Pretty(prettify), m_Writer(std::move(w)) + : m_IsAsyncWriter{dynamic_cast(w.get()) != nullptr}, m_Pretty(prettify), m_Writer(std::move(w)) { } @@ -35,9 +35,18 @@ JsonEncoder::JsonEncoder(nlohmann::detail::output_adapter_t w, bool pretti * 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. * + * @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 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()) { case ValueEmpty: @@ -57,14 +66,14 @@ void JsonEncoder::Encode(const Value& value) const auto& type = obj->GetReflectionType(); if (type == Namespace::TypeInstance) { static constexpr auto extractor = [](const NamespaceValue& v) -> const Value& { return v.Val; }; - EncodeObject(static_pointer_cast(obj), extractor); + EncodeObject(static_pointer_cast(obj), extractor, yc); } else if (type == Dictionary::TypeInstance) { static constexpr auto extractor = [](const Value& v) -> const Value& { return v; }; - EncodeObject(static_pointer_cast(obj), extractor); + EncodeObject(static_pointer_cast(obj), extractor, yc); } else if (type == Array::TypeInstance) { - EncodeArray(static_pointer_cast(obj)); + EncodeArray(static_pointer_cast(obj), yc); } else if (auto gen(dynamic_pointer_cast(obj)); gen) { - EncodeValueGenerator(gen); + EncodeValueGenerator(gen, yc); } else { // Some other non-serializable object type! 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. * * @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('['); - ObjectLock olock(array); + auto olock = array->LockIfRequired(); + if (olock) { + yc = nullptr; // We've acquired an object lock, never allow asynchronous operations. + } + bool isEmpty = true; for (const auto& item : array) { WriteSeparatorAndIndentStrIfNeeded(!isEmpty); isEmpty = false; - Encode(item); + Encode(item, yc); + FlushIfSafe(yc); } 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. * * @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('['); bool isEmpty = true; while (auto result = generator->Next()) { WriteSeparatorAndIndentStrIfNeeded(!isEmpty); isEmpty = false; - Encode(*result); + Encode(*result, yc); + FlushIfSafe(yc); } EndContainer(']', isEmpty); } @@ -127,15 +146,21 @@ void JsonEncoder::EncodeValueGenerator(const ValueGenerator::Ptr& generator) * * @param container The container to JSON serialize. * @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 -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 || std::is_same_v, "Container must be a Namespace or Dictionary"); 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; for (const auto& [key, val] : container) { WriteSeparatorAndIndentStrIfNeeded(!isEmpty); @@ -143,7 +168,9 @@ void JsonEncoder::EncodeObject(const Iterable& container, const ValExtractor& ex EncodeNlohmannJson(Utility::ValidateUTF8(key)); Write(m_Pretty ? ": " : ":"); - Encode(extractor(val)); + + Encode(extractor(val), yc); + FlushIfSafe(yc); } EndContainer('}', isEmpty); } @@ -193,6 +220,29 @@ void JsonEncoder::EncodeNumber(double value) const 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(m_Writer.get())); + ajw->Flush(*yc); + } +} + /** * Writes a string to the underlying output stream. * diff --git a/lib/base/json.hpp b/lib/base/json.hpp index 63828316c..464a926f5 100644 --- a/lib/base/json.hpp +++ b/lib/base/json.hpp @@ -73,18 +73,20 @@ public: explicit JsonEncoder(std::basic_ostream& stream, bool prettify = false); explicit JsonEncoder(nlohmann::detail::output_adapter_t w, bool prettify = false); - void Encode(const Value& value); + void Encode(const Value& value, boost::asio::yield_context* yc = nullptr); private: - void EncodeArray(const Array::Ptr& array); - void EncodeValueGenerator(const ValueGenerator::Ptr& generator); + void EncodeArray(const Array::Ptr& array, boost::asio::yield_context* yc); + void EncodeValueGenerator(const ValueGenerator::Ptr& generator, boost::asio::yield_context* yc); template - 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 EncodeNumber(double value) const; + void FlushIfSafe(boost::asio::yield_context* yc) const; + void Write(const std::string_view& sv) const; void BeginContainer(char openChar); void EndContainer(char closeChar, bool isContainerEmpty = false); @@ -93,6 +95,7 @@ private: // The number of spaces to use for indentation in prettified JSON. 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. unsigned m_Indent{0}; // The current indentation level for pretty-printing. /**