diff --git a/CMakeLists.txt b/CMakeLists.txt index ab6f18f00e..8e599fecd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,7 +83,7 @@ endif() add_custom_target(kphp ALL DEPENDS ${OBJS_DIR}/php_lib_version.sha256) if (COMPILE_RUNTIME_LIGHT) - add_dependencies(kphp kphp2cpp kphp-light-runtime-pic) + add_dependencies(kphp kphp2cpp kphp-light-runtime-pic kphp-confdata-pic) else () add_dependencies(kphp kphp2cpp kphp-full-runtime-no-pic kphp-full-runtime-pic) endif () @@ -95,6 +95,10 @@ install_symlink(${VK_INSTALL_DIR}/bin/kphp2cpp ${CMAKE_INSTALL_PREFIX}/bin/kphp # k2 specific if(COMPILE_RUNTIME_LIGHT) + install(TARGETS kphp-confdata-pic + LIBRARY DESTINATION ${INSTALL_KPHP_SOURCE}/objs + COMPONENT KPHP) + install(FILES ${OBJS_DIR}/libk2kphp-rt.a COMPONENT KPHP DESTINATION ${INSTALL_KPHP_SOURCE}/objs) diff --git a/builtin-functions/kphp-light/stdlib/confdata-functions.txt b/builtin-functions/kphp-light/stdlib/confdata-functions.txt index 1e86ee0e82..89238c2450 100644 --- a/builtin-functions/kphp-light/stdlib/confdata-functions.txt +++ b/builtin-functions/kphp-light/stdlib/confdata-functions.txt @@ -2,11 +2,8 @@ function is_confdata_loaded(): bool; -/** @kphp-extern-func-info interruptible */ function confdata_get_value($key ::: string): mixed; -/** @kphp-extern-func-info interruptible */ function confdata_get_values_by_any_wildcard($wildcard ::: string): mixed[]; -/** @kphp-extern-func-info interruptible */ function confdata_get_values_by_predefined_wildcard($wildcard ::: string): mixed[]; diff --git a/compiler/compiler-settings.cpp b/compiler/compiler-settings.cpp index 2eab8d9f83..9107d281ab 100644 --- a/compiler/compiler-settings.cpp +++ b/compiler/compiler-settings.cpp @@ -340,6 +340,8 @@ void CompilerSettings::init() { ss << " -I" << kphp_src_path.get() + "objs/include "; if (is_k2_mode) { + // Generated code and its precompiled header must use the light runtime declarations. + ss << " -DRUNTIME_LIGHT"; // for now k2-component must be compiled with clang and statically linked libc++ ss << " -stdlib=libc++"; if (!dynamic_incremental_linkage.get()) { diff --git a/runtime-common/core/allocator/pool-allocator.h b/runtime-common/core/allocator/pool-allocator.h index a92b7cd476..fe85ebd2c1 100644 --- a/runtime-common/core/allocator/pool-allocator.h +++ b/runtime-common/core/allocator/pool-allocator.h @@ -12,17 +12,25 @@ namespace kphp::memory { struct pool_allocator : private vk::not_copyable { + struct external_memory {}; + private: + enum class memory_mode { + owned_growable, + external_fixed, // Never grows or releases the externally owned backing buffer. + }; + memory_resource::unsynchronized_pool_resource memory_resource; + memory_mode m_memory_mode{memory_mode::owned_growable}; size_t m_min_extra_mem_size{0}; auto request_extra_memory(size_t requested_size) noexcept -> void; public: - pool_allocator() = default; pool_allocator(size_t script_mem_size, size_t min_extra_mem_size, size_t oom_handling_mem_size) noexcept; + // Borrows the buffer without growing it or releasing it in free(). + pool_allocator(external_memory, void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept; - auto init(void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept -> void; auto free() noexcept -> void; auto alloc_script_memory(size_t size) noexcept -> void*; diff --git a/runtime-common/core/allocator/runtime-allocator.h b/runtime-common/core/allocator/runtime-allocator.h index 46b948b130..72da66f458 100644 --- a/runtime-common/core/allocator/runtime-allocator.h +++ b/runtime-common/core/allocator/runtime-allocator.h @@ -1,24 +1,37 @@ -// Compiler for PHP (aka KPHP) -// Copyright (c) 2024 LLC «V Kontakte» -// Distributed under the GPL v3 License, see LICENSE.notice.txt +// Compiler for PHP (aka KPHP) +// Copyright (c) 2024 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt #pragma once #include +#ifdef RUNTIME_LIGHT +#include +#include +#include +#include +#include "common/containers/final_action.h" #include "runtime-common/core/allocator/pool-allocator.h" +#endif struct RuntimeAllocator final { +#ifdef RUNTIME_LIGHT private: kphp::memory::pool_allocator m_allocator; + std::reference_wrapper m_allocator_ref{m_allocator}; +#endif public: static auto get() noexcept -> RuntimeAllocator&; - RuntimeAllocator() = default; +#ifdef RUNTIME_LIGHT RuntimeAllocator(size_t script_mem_size, size_t min_extra_mem_size, size_t oom_handling_mem_size) noexcept; - +#else + RuntimeAllocator() = default; auto init(void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept -> void; +#endif + auto free() noexcept -> void; auto alloc_script_memory(size_t size) noexcept -> void*; @@ -26,7 +39,20 @@ struct RuntimeAllocator final { auto realloc_script_memory(void* mem, size_t new_size, size_t old_size) noexcept -> void*; auto free_script_memory(void* mem, size_t size) noexcept -> void; +#ifdef RUNTIME_LIGHT auto get_memory_resource() noexcept -> memory_resource::unsynchronized_pool_resource& { - return m_allocator.get_memory_resource(); + return m_allocator_ref.get().get_memory_resource(); + } + + // The callback must run synchronously without yielding. Objects allocated by it + // may outlive the scope, but later operations that allocate or deallocate their + // memory must install the same allocator. The replacement must outlive the callback. + template && std::is_same_v, void>, int32_t> = 0> + auto with_allocator(kphp::memory::pool_allocator& replacement, callback_type&& callback) noexcept -> void { + const auto previous_allocator{std::exchange(m_allocator_ref, std::ref(replacement))}; + const auto restore_allocator{vk::finally([this, previous_allocator]() noexcept { m_allocator_ref = previous_allocator; })}; + std::invoke(std::forward(callback)); } +#endif }; diff --git a/runtime-common/core/memory-resource/resource_allocator.h b/runtime-common/core/memory-resource/resource_allocator.h index eafe25610e..3c610e304b 100644 --- a/runtime-common/core/memory-resource/resource_allocator.h +++ b/runtime-common/core/memory-resource/resource_allocator.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include @@ -88,6 +89,9 @@ using vector = std::vector>; template using list = std::list>; +template +using forward_list = std::forward_list>; + template using string = std::basic_string, resource_allocator>; } // namespace stl diff --git a/runtime-common/core/std/containers.h b/runtime-common/core/std/containers.h index d0b2563973..4af86ce097 100644 --- a/runtime-common/core/std/containers.h +++ b/runtime-common/core/std/containers.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include #include @@ -41,6 +42,9 @@ using queue = std::queue>>; template class Allocator> using list = std::list>; +template class Allocator> +using forward_list = std::forward_list>; + template class Allocator> using vector = std::vector>; diff --git a/runtime-light/allocator/allocator.h b/runtime-light/allocator/allocator.h index bd625d9ebc..14c7921fa0 100644 --- a/runtime-light/allocator/allocator.h +++ b/runtime-light/allocator/allocator.h @@ -5,7 +5,8 @@ #pragma once #include -#include +#include +#include #include "runtime-common/core/allocator/script-allocator-managed.h" #include "runtime-light/allocator/allocator-state.h" @@ -17,6 +18,7 @@ auto make_unique_on_script_memory(Args&&... args) noexcept { } namespace kphp::memory { + struct libc_alloc_guard final { libc_alloc_guard() noexcept { AllocatorState::get_mutable().enable_libc_alloc(); diff --git a/runtime-light/allocator/pool-allocator.cpp b/runtime-light/allocator/pool-allocator.cpp index f75fa2fc76..c487c29799 100644 --- a/runtime-light/allocator/pool-allocator.cpp +++ b/runtime-light/allocator/pool-allocator.cpp @@ -16,25 +16,22 @@ namespace kphp::memory { pool_allocator::pool_allocator(size_t script_mem_size, size_t min_extra_mem_size, size_t oom_handling_mem_size) noexcept : m_min_extra_mem_size(min_extra_mem_size) { - // kphp::log::debug("create pool allocator -> {:p}: script memory -> {}, oom handling size -> {}", reinterpret_cast(this), script_mem_size, - // oom_handling_mem_size); void* buffer{kphp::memory::platform::alloc(script_mem_size)}; - kphp::log::assertion(buffer != nullptr); - memory_resource.init(buffer, script_mem_size, oom_handling_mem_size); } -auto pool_allocator::init(void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept -> void { +pool_allocator::pool_allocator(external_memory /*unused*/, void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept + : m_memory_mode{memory_mode::external_fixed} { kphp::log::assertion(buffer != nullptr); - - // kphp::log::debug("init pool allocator -> {:p}: buffer -> {:p}, script memory -> {}, oom handling size -> {}", reinterpret_cast(this), buffer, - // script_mem_size, oom_handling_mem_size); memory_resource.init(buffer, script_mem_size, oom_handling_mem_size); } auto pool_allocator::free() noexcept -> void { - // kphp::log::debug("free pool allocator -> {:p}", reinterpret_cast(this)); + if (m_memory_mode == memory_mode::external_fixed) { + return; + } + auto* extra_memory{memory_resource.get_extra_memory_head()}; while (extra_memory->get_pool_payload_size() != 0) { auto* extra_memory_to_release{extra_memory}; @@ -94,6 +91,9 @@ auto pool_allocator::free_script_memory(void* mem, size_t size) noexcept -> void } auto pool_allocator::request_extra_memory(size_t requested_size) noexcept -> void { + // Fixed pools must fail on exhaustion instead of allocating outside their buffer. + kphp::log::assertion(m_memory_mode == memory_mode::owned_growable); + // Extra mem size have to be greater than max chunk block const auto min_size{std::max(m_min_extra_mem_size, memory_resource::unsynchronized_pool_resource::MAX_CHUNK_BLOCK_SIZE)}; @@ -103,12 +103,8 @@ auto pool_allocator::request_extra_memory(size_t requested_size) noexcept -> voi // The smallest power of two that is not smaller than `extra_mem_size` extra_mem_size = std::bit_ceil(extra_mem_size); - // kphp::log::debug("requested extra memory pool with size {} bytes, will be allocated {} bytes", requested_size, extra_mem_size); - auto* extra_mem{kphp::memory::platform::alloc(extra_mem_size)}; - kphp::log::assertion(extra_mem != nullptr); - memory_resource.add_extra_memory(new (extra_mem) memory_resource::extra_memory_pool{extra_mem_size}); } diff --git a/runtime-light/allocator/runtime-light-allocator.cpp b/runtime-light/allocator/runtime-light-allocator.cpp index 0bcfc019d5..9c7aac979b 100644 --- a/runtime-light/allocator/runtime-light-allocator.cpp +++ b/runtime-light/allocator/runtime-light-allocator.cpp @@ -3,7 +3,6 @@ // Distributed under the GPL v3 License, see LICENSE.notice.txt #include "runtime-common/core/allocator/runtime-allocator.h" - #include "runtime-light/allocator/allocator-state.h" auto RuntimeAllocator::get() noexcept -> RuntimeAllocator& { @@ -13,26 +12,22 @@ auto RuntimeAllocator::get() noexcept -> RuntimeAllocator& { RuntimeAllocator::RuntimeAllocator(size_t script_mem_size, size_t min_extra_mem_size, size_t oom_handling_mem_size) noexcept : m_allocator{script_mem_size, min_extra_mem_size, oom_handling_mem_size} {} -auto RuntimeAllocator::init(void* buffer, size_t script_mem_size, size_t oom_handling_mem_size) noexcept -> void { - m_allocator.init(buffer, script_mem_size, oom_handling_mem_size); -} - auto RuntimeAllocator::free() noexcept -> void { m_allocator.free(); } auto RuntimeAllocator::alloc_script_memory(size_t size) noexcept -> void* { - return m_allocator.alloc_script_memory(size); + return m_allocator_ref.get().alloc_script_memory(size); } auto RuntimeAllocator::calloc_script_memory(size_t size) noexcept -> void* { - return m_allocator.calloc_script_memory(size); + return m_allocator_ref.get().calloc_script_memory(size); } auto RuntimeAllocator::realloc_script_memory(void* mem, size_t new_size, size_t old_size) noexcept -> void* { - return m_allocator.realloc_script_memory(mem, new_size, old_size); + return m_allocator_ref.get().realloc_script_memory(mem, new_size, old_size); } auto RuntimeAllocator::free_script_memory(void* mem, size_t size) noexcept -> void { - m_allocator.free_script_memory(mem, size); + m_allocator_ref.get().free_script_memory(mem, size); } diff --git a/runtime-light/components/confdata/confdata-component.cpp b/runtime-light/components/confdata/confdata-component.cpp index 4f7761cd05..ae3c1f08a0 100644 --- a/runtime-light/components/confdata/confdata-component.cpp +++ b/runtime-light/components/confdata/confdata-component.cpp @@ -85,12 +85,12 @@ VISIBILITY_DEFAULT k2::PollStatus k2_poll() { } VISIBILITY_DEFAULT const ImageInfo* k2_describe() { - static constexpr std::array extra_info{ImageInfo::KeyValuePair{.key = "compiler_version", .value = K2_CONFDATA_COMPILER_VERSION}}; - static constexpr ImageInfo image_info{.image_name = kphp::confdata::COMPONENT_NAME.data(), + static constexpr std::array extra_info{ImageInfo::KeyValuePair{.key = "compiler_version", .value = KPHP_CONFDATA_COMPILER_VERSION}}; + static constexpr ImageInfo image_info{.image_name = kphp::confdata::IMAGE_NAME.data(), .is_oneshot = 0, - .build_timestamp = K2_CONFDATA_BUILD_TIMESTAMP, + .build_timestamp = KPHP_CONFDATA_BUILD_TIMESTAMP, .header_h_version = K2_PLATFORM_HEADER_H_VERSION, - .version = "0.0.1", + .version = "1.0.0", .extra_info_size = extra_info.size(), .extra_info = extra_info.data()}; return std::addressof(image_info); diff --git a/runtime-light/components/confdata/confdata-proxy/sync-functions.h b/runtime-light/components/confdata/confdata-proxy/sync-functions.h index d6e0d49bb7..0b906cab88 100644 --- a/runtime-light/components/confdata/confdata-proxy/sync-functions.h +++ b/runtime-light/components/confdata/confdata-proxy/sync-functions.h @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -34,17 +36,26 @@ struct pagination { bool m_has_synced{}; }; -enum class subscribe_error : uint8_t { transport, old_offset, malformed_response, not_synced }; +using encoded_snapshot_page = kphp::stl::vector; + +struct snapshot final { + pagination m_pagination; + kphp::stl::vector m_pages; +}; + +enum class subscribe_error : uint8_t { transport, old_offset, malformed_response, not_synced, batch_rejected }; namespace details { // Performs a single confdata.subscribe round-trip. // On success, invokes `event_handler(events)` once with the batch of received events and updates `to` pagination. +// If the handler returns false, the batch is rejected with `batch_rejected` and `to` is left unchanged so it can be requested again. // The batch is a view into the response buffer and is only valid for the duration of the call; empty batches are not delivered. // An empty event value means that the key has been deleted. -template> event_handler_type> -auto subscribe(std::string_view confdata_proxy_actor, kphp::confdata::pagination& to, - const event_handler_type& event_handler) noexcept -> kphp::coro::task> { +template> event_handler_type> +auto subscribe(std::string_view confdata_proxy_actor, kphp::confdata::pagination& to, const event_handler_type& event_handler, + std::optional> retained_response = {}) noexcept + -> kphp::coro::task> { // subscribe is a longpoll method, so the timeout must cover the time confdata-proxy may hold the request open static constexpr auto SUBSCRIBE_TIMEOUT{std::chrono::milliseconds{45'000}}; @@ -66,63 +77,99 @@ auto subscribe(std::string_view confdata_proxy_actor, kphp::confdata::pagination // client-side timeout must outlive the server-side longpoll (SUBSCRIBE_TIMEOUT); 10x is a safe margin auto expected_query{kphp::rpc::query::send(confdata_proxy_actor, SUBSCRIBE_TIMEOUT * 10, tls.view(), k2::RpcKind::TL_RPC)}; if (!expected_query) [[unlikely]] { - kphp::log::warning("confdata: failed to send subscribe request: {}", expected_query.error()); + kphp::log::warning("failed to send subscribe request: {}", expected_query.error()); co_return std::unexpected{kphp::confdata::subscribe_error::transport}; } - kphp::stl::vector response_buffer{}; + encoded_snapshot_page response_buffer{}; auto expected_response{co_await kphp::rpc::query::response(std::move(*expected_query), [&response_buffer](size_t size) noexcept -> std::span { response_buffer.resize(size); return {response_buffer.data(), response_buffer.size()}; })}; if (!expected_response) [[unlikely]] { - kphp::log::warning("confdata: failed to fetch subscribe response: {}", expected_response.error()); + kphp::log::warning("failed to fetch subscribe response: {}", expected_response.error()); co_return std::unexpected{kphp::confdata::subscribe_error::transport}; } tl::fetcher tlf{*expected_response}; tl::confdata::SubscribeResponse response{}; if (!response.fetch(tlf)) [[unlikely]] { - kphp::log::warning("confdata: failed to parse subscribe response"); + kphp::log::warning("failed to parse subscribe response"); co_return std::unexpected{kphp::confdata::subscribe_error::malformed_response}; } - co_return std::visit( - overloaded{ - [&event_handler, &to](const tl::confdata::subscribeResponseOk& response) noexcept -> std::expected { - if (const auto& events{response.events}; events.size() != 0) { - std::invoke(event_handler, std::span{events.value}); - } - - to.m_page = response.new_page.value; - to.m_offset = response.new_offset.value; - to.m_has_synced = response.new_has_synced.value; - return {}; - }, - [](const tl::confdata::subscribeResponseOldOffsetError& /* unused */) noexcept -> std::expected { - return std::unexpected{kphp::confdata::subscribe_error::old_offset}; - }, - }, - response.value); + auto handled{ + std::visit(overloaded{ + [&event_handler, &to](const tl::confdata::subscribeResponseOk& response) noexcept -> std::expected { + if (const auto& events{response.events}; events.size() != 0) { + if (!std::invoke(event_handler, std::span{events.value})) { + return std::unexpected{kphp::confdata::subscribe_error::batch_rejected}; + } + } + + to.m_page = response.new_page.value; + to.m_offset = response.new_offset.value; + to.m_has_synced = response.new_has_synced.value; + return {}; + }, + [](const tl::confdata::subscribeResponseOldOffsetError& /* unused */) noexcept -> std::expected { + return std::unexpected{kphp::confdata::subscribe_error::old_offset}; + }, + }, + response.value)}; + if (handled && retained_response.has_value()) { + retained_response->get() = std::move(response_buffer); + } + co_return std::move(handled); } } // namespace details -// Paginates through a consistent snapshot of all subscribed keys until it has been fully synced. -// Returns the final pagination that should be passed to `update`. -// -// `event_handler` is invoked once per round-trip with a batch of events; the batch is only valid -// for the duration of the call and must be copied if it needs to be retained. -template> event_handler_type> +// Fetches one consistent snapshot while retaining the encoded response pages. +// The handler can collect metadata from the initial parse; replay() +// reparses the same bytes later without another network synchronization. +template> event_handler_type> auto sync(std::string_view confdata_proxy_actor, - event_handler_type event_handler) noexcept -> kphp::coro::task> { - kphp::confdata::pagination p{}; - for (; !p.m_has_synced;) { - if (auto expected{co_await details::subscribe(confdata_proxy_actor, p, event_handler)}; !expected) [[unlikely]] { + event_handler_type event_handler) noexcept -> kphp::coro::task> { + snapshot snapshot{}; + for (; !snapshot.m_pagination.m_has_synced;) { + snapshot.m_pages.emplace_back(); + if (auto expected{co_await details::subscribe(confdata_proxy_actor, snapshot.m_pagination, event_handler, snapshot.m_pages.back())}; !expected) + [[unlikely]] { co_return std::unexpected{expected.error()}; } } - co_return std::move(p); + co_return std::move(snapshot); +} + +// Applies a previously fetched snapshot and releases encoded pages as soon as +// their parsed event descriptors have been consumed. Processed pages stay +// consumed if a later page fails, so the snapshot must be discarded after any call. +template> event_handler_type> +auto replay(snapshot& snapshot, const event_handler_type& event_handler) noexcept -> std::expected { + for (auto& encoded_page : snapshot.m_pages) { + { + tl::fetcher tlf{std::span{encoded_page.data(), encoded_page.size()}}; + tl::confdata::SubscribeResponse response{}; + if (!response.fetch(tlf)) [[unlikely]] { + return std::unexpected{kphp::confdata::subscribe_error::malformed_response}; + } + + if (std::holds_alternative(response.value)) { + return std::unexpected{kphp::confdata::subscribe_error::old_offset}; + } + + const auto& response_ok{std::get(response.value)}; + if (const auto& events{response_ok.events}; + events.size() != 0 && !std::invoke(event_handler, std::span{events.value})) { + return std::unexpected{kphp::confdata::subscribe_error::batch_rejected}; + } + } + // Make the processed page's memory reusable before parsing the next page. + encoded_snapshot_page{}.swap(encoded_page); + } + snapshot.m_pages.clear(); + return {}; } // Longpoll loop: invokes `event_handler` for each event as it arrives, throttled to at most one batch per second: @@ -132,7 +179,7 @@ auto sync(std::string_view confdata_proxy_actor, // // `event_handler` is invoked once per round-trip with a batch of events; the batch is only valid // for the duration of the call and must be copied if it needs to be retained. -template> event_handler_type> +template> event_handler_type> auto update(std::string_view confdata_proxy_actor, kphp::confdata::pagination& from, event_handler_type event_handler) noexcept -> kphp::coro::task> { // limits the update rate to at most one batch per interval diff --git a/runtime-light/components/confdata/confdata.cmake b/runtime-light/components/confdata/confdata.cmake index ecff9f47b1..f5f022e99c 100644 --- a/runtime-light/components/confdata/confdata.cmake +++ b/runtime-light/components/confdata/confdata.cmake @@ -1,50 +1,57 @@ -# k2-confdata image: a standalone C++ component that shares the common +# kphp-confdata image: a standalone C++ component that shares the common # runtime-light machinery but provides its own entry points and bindings -set(K2_CONFDATA_COMPONENT_SRC +set(KPHP_CONFDATA_COMPONENT_SRC ${RUNTIME_LIGHT_DIR}/components/confdata/confdata-component.cpp ${RUNTIME_LIGHT_DIR}/components/confdata/bindings/bindings.cpp ${RUNTIME_LIGHT_DIR}/components/confdata/state/component-state.cpp - ${RUNTIME_LIGHT_DIR}/components/confdata/state/instance-state.cpp) + ${RUNTIME_LIGHT_DIR}/components/confdata/state/instance-state.cpp + ${RUNTIME_LIGHT_DIR}/stdlib/confdata/confdata-storage.cpp + ${RUNTIME_LIGHT_DIR}/stdlib/confdata/confdata-keys.cpp + ${RUNTIME_LIGHT_DIR}/stdlib/confdata/predefined-wildcards.cpp) -set(K2_CONFDATA_TL_SRC +set(KPHP_CONFDATA_TL_SRC ${RUNTIME_LIGHT_DIR}/tl/tl-types.cpp ${RUNTIME_LIGHT_DIR}/tl/tl-functions.cpp) -set(K2_CONFDATA_ALLOCATOR_SRC - ${RUNTIME_LIGHT_DIR}/allocator/runtime-light-allocator.cpp +set(KPHP_CONFDATA_ALLOCATOR_SRC ${RUNTIME_LIGHT_ALLOCATOR_SRC}) +list(TRANSFORM KPHP_CONFDATA_ALLOCATOR_SRC PREPEND "${RUNTIME_LIGHT_DIR}/") +list(APPEND KPHP_CONFDATA_ALLOCATOR_SRC ${RUNTIME_LIGHT_DIR}/memory-resource-impl/monotonic-light-buffer-resource.cpp) -set(K2_CONFDATA_DIAGNOSTICS_SRC +set(KPHP_CONFDATA_DIAGNOSTICS_SRC ${RUNTIME_LIGHT_DIR}/stdlib/diagnostics/backtrace.cpp ${RUNTIME_LIGHT_DIR}/stdlib/diagnostics/php-assert.cpp) -set(K2_CONFDATA_MEMORY_RESOURCE_SRC - ${RUNTIME_COMMON_DIR}/core/memory-resource/unsynchronized_pool_resource.cpp - ${RUNTIME_COMMON_DIR}/core/memory-resource/monotonic_buffer_resource.cpp - ${RUNTIME_COMMON_DIR}/core/memory-resource/details/memory_chunk_tree.cpp - ${RUNTIME_COMMON_DIR}/core/memory-resource/details/memory_ordered_chunk_list.cpp) - -set(K2_CONFDATA_SRC - ${K2_CONFDATA_COMPONENT_SRC} - ${K2_CONFDATA_TL_SRC} - ${K2_CONFDATA_ALLOCATOR_SRC} - ${K2_CONFDATA_DIAGNOSTICS_SRC} - ${K2_CONFDATA_MEMORY_RESOURCE_SRC} +set(KPHP_CONFDATA_SERIALIZATION_SRC + ${RUNTIME_COMMON_DIR}/stdlib/serialization/json-functions.cpp + ${RUNTIME_COMMON_DIR}/stdlib/serialization/serialize-functions.cpp) + +set(KPHP_CONFDATA_RUNTIME_CORE_SRC ${CORE_SRC}) +list(TRANSFORM KPHP_CONFDATA_RUNTIME_CORE_SRC PREPEND "${RUNTIME_COMMON_DIR}/") + +set(KPHP_CONFDATA_SRC + ${KPHP_CONFDATA_COMPONENT_SRC} + ${KPHP_CONFDATA_TL_SRC} + ${KPHP_CONFDATA_ALLOCATOR_SRC} + ${KPHP_CONFDATA_DIAGNOSTICS_SRC} + ${KPHP_CONFDATA_SERIALIZATION_SRC} + ${KPHP_CONFDATA_RUNTIME_CORE_SRC} # link the alloc-wrapper objects directly (not as an archive) so that # __wrap_* definitions are always present regardless of link order $) -vk_add_library_pic(k2-confdata-pic SHARED ${K2_CONFDATA_SRC}) -set_target_properties(k2-confdata-pic PROPERTIES PREFIX "" OUTPUT_NAME "k2-confdata" LIBRARY_OUTPUT_DIRECTORY ${OBJS_DIR}) -target_compile_options(k2-confdata-pic PUBLIC ${RUNTIME_LIGHT_COMPILE_FLAGS}) +vk_add_library_pic(kphp-confdata-pic SHARED ${KPHP_CONFDATA_SRC}) +set_target_properties(kphp-confdata-pic PROPERTIES PREFIX "" OUTPUT_NAME "kphp-confdata" LIBRARY_OUTPUT_DIRECTORY ${OBJS_DIR}) +target_compile_options(kphp-confdata-pic PUBLIC ${RUNTIME_LIGHT_COMPILE_FLAGS}) +target_link_libraries(kphp-confdata-pic PRIVATE vk::pic::light-common) # reuse the common link flags; cmake drives the link through the compiler, # so bare ld options need the -Wl, prefix -set(K2_CONFDATA_LINK_FLAGS ${RUNTIME_LIGHT_LINK_FLAGS}) +set(KPHP_CONFDATA_LINK_FLAGS ${RUNTIME_LIGHT_LINK_FLAGS}) if(NOT APPLE) - list(TRANSFORM K2_CONFDATA_LINK_FLAGS REPLACE "^--" "-Wl,--") + list(TRANSFORM KPHP_CONFDATA_LINK_FLAGS REPLACE "^--" "-Wl,--") endif() -target_link_options(k2-confdata-pic PUBLIC ${K2_CONFDATA_LINK_FLAGS}) +target_link_options(kphp-confdata-pic PUBLIC ${KPHP_CONFDATA_LINK_FLAGS}) -string(TIMESTAMP K2_CONFDATA_BUILD_TIMESTAMP "%s" UTC) -target_compile_definitions(k2-confdata-pic PRIVATE K2_CONFDATA_BUILD_TIMESTAMP=${K2_CONFDATA_BUILD_TIMESTAMP}ULL - K2_CONFDATA_COMPILER_VERSION="${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}") +string(TIMESTAMP KPHP_CONFDATA_BUILD_TIMESTAMP "%s" UTC) +target_compile_definitions(kphp-confdata-pic PRIVATE KPHP_CONFDATA_BUILD_TIMESTAMP=${KPHP_CONFDATA_BUILD_TIMESTAMP}ULL + KPHP_CONFDATA_COMPILER_VERSION="${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}") diff --git a/runtime-light/components/confdata/state/component-state.cpp b/runtime-light/components/confdata/state/component-state.cpp index 335a56a1eb..36fef5523d 100644 --- a/runtime-light/components/confdata/state/component-state.cpp +++ b/runtime-light/components/confdata/state/component-state.cpp @@ -4,23 +4,110 @@ #include "runtime-light/components/confdata/state/component-state.h" +#include +#include +#include +#include #include +#include #include "runtime-light/k2-platform/k2-api.h" +#include "runtime-light/stdlib/confdata/predefined-wildcards.h" #include "runtime-light/stdlib/diagnostics/logs.h" +auto ComponentState::parse_confdata_memory_limit_arg(std::string_view value_view) noexcept -> void { + size_t parsed{}; + const auto [end, error]{std::from_chars(value_view.begin(), value_view.end(), parsed)}; + if (value_view.empty() || error != std::errc{} || end != value_view.end() || parsed == 0) [[unlikely]] { + kphp::log::error("{} must be a positive integer, got '{}'", CONFDATA_MEMORY_LIMIT_ARG, value_view); + } + m_confdata_memory_limit = parsed; +} + +auto ComponentState::parse_confdata_oom_handling_size_arg(std::string_view value_view) noexcept -> void { + size_t parsed{}; + const auto [end, error]{std::from_chars(value_view.begin(), value_view.end(), parsed)}; + if (value_view.empty() || error != std::errc{} || end != value_view.end() || parsed == 0) [[unlikely]] { + kphp::log::error("{} must be a positive integer, got '{}'", CONFDATA_OOM_HANDLING_SIZE_ARG, value_view); + } + m_confdata_oom_handling_size = parsed; +} + auto ComponentState::parse_confdata_proxy_actor_name_arg(std::string_view value_view) noexcept -> void { m_confdata_proxy_actor_name = value_view; } +auto ComponentState::parse_predefined_wildcards_arg(std::string_view value_view) noexcept -> void { + m_predefined_wildcards.clear(); + m_predefined_wildcards_storage.assign(value_view); + + const std::string_view storage_view{m_predefined_wildcards_storage}; + size_t line_number{1}; + size_t line_begin{}; + while (line_begin < storage_view.size()) { + const size_t line_end{storage_view.find('\n', line_begin)}; + const auto wildcard{storage_view.substr(line_begin, line_end - line_begin)}; + if (wildcard.empty()) [[unlikely]] { + kphp::log::error("{} contains an empty line: line -> {}", PREDEFINED_WILDCARDS_ARG, line_number); + } + if (wildcard.contains('\r')) [[unlikely]] { + kphp::log::error("{} contains a carriage return: line -> {}", PREDEFINED_WILDCARDS_ARG, line_number); + } + if (const auto validated{kphp::confdata::validate_predefined_wildcard(wildcard)}; !validated) [[unlikely]] { + kphp::log::error("{} contains an invalid wildcard: line -> {}, error -> {}", PREDEFINED_WILDCARDS_ARG, line_number, validated.error()); + } + m_predefined_wildcards.emplace_back(wildcard); + + if (line_end == std::string_view::npos) { + break; + } + line_begin = line_end + 1; + ++line_number; + } + if (!storage_view.empty() && storage_view.back() == '\n') [[unlikely]] { + kphp::log::error("{} contains an empty trailing line; use the YAML '|-' block style", PREDEFINED_WILDCARDS_ARG); + } + + std::ranges::sort(m_predefined_wildcards); + m_predefined_wildcards.erase(std::ranges::unique(m_predefined_wildcards).begin(), m_predefined_wildcards.end()); +} + +auto ComponentState::parse_initial_instance_memory_size_arg(std::string_view value_view) noexcept -> void { + size_t parsed{}; + const auto [end, error]{std::from_chars(value_view.begin(), value_view.end(), parsed)}; + if (value_view.empty() || error != std::errc{} || end != value_view.end() || parsed == 0) [[unlikely]] { + kphp::log::error("{} must be a positive integer, got '{}'", INITIAL_INSTANCE_MEMORY_SIZE_ARG, value_view); + } + m_initial_instance_memory_size = parsed; +} + +auto ComponentState::parse_min_instance_extra_memory_size_arg(std::string_view value_view) noexcept -> void { + size_t parsed{}; + const auto [end, error]{std::from_chars(value_view.begin(), value_view.end(), parsed)}; + if (value_view.empty() || error != std::errc{} || end != value_view.end() || parsed == 0) [[unlikely]] { + kphp::log::error("{} must be a positive integer, got '{}'", MIN_INSTANCE_EXTRA_MEMORY_SIZE_ARG, value_view); + } + m_min_instance_extra_memory_size = parsed; +} + auto ComponentState::parse_args() noexcept -> void { - for (auto i = 0; i < m_argc; ++i) { + for (auto i{0}; i < m_argc; ++i) { const auto [arg_key, arg_value]{k2::arg_fetch(i)}; const std::string_view key_view{arg_key.get(), std::strlen(arg_key.get())}; const std::string_view value_view{arg_value.get(), std::strlen(arg_value.get())}; - if (key_view == CONFDATA_PROXY_ACTOR_NAME_ARG) { + if (key_view == CONFDATA_MEMORY_LIMIT_ARG) { + parse_confdata_memory_limit_arg(value_view); + } else if (key_view == CONFDATA_OOM_HANDLING_SIZE_ARG) { + parse_confdata_oom_handling_size_arg(value_view); + } else if (key_view == CONFDATA_PROXY_ACTOR_NAME_ARG) { parse_confdata_proxy_actor_name_arg(value_view); + } else if (key_view == PREDEFINED_WILDCARDS_ARG) { + parse_predefined_wildcards_arg(value_view); + } else if (key_view == INITIAL_INSTANCE_MEMORY_SIZE_ARG) { + parse_initial_instance_memory_size_arg(value_view); + } else if (key_view == MIN_INSTANCE_EXTRA_MEMORY_SIZE_ARG) { + parse_min_instance_extra_memory_size_arg(value_view); } else { kphp::log::error("unexpected argument: {}", key_view); } diff --git a/runtime-light/components/confdata/state/component-state.h b/runtime-light/components/confdata/state/component-state.h index c111f8ee8d..3f7d920ac5 100644 --- a/runtime-light/components/confdata/state/component-state.h +++ b/runtime-light/components/confdata/state/component-state.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include "common/mixin/not_copyable.h" @@ -15,29 +16,69 @@ #include "runtime-light/stdlib/diagnostics/logs.h" struct ComponentState final : private vk::not_copyable { + // === MEMBERS ================================================================================== +private: + static constexpr std::string_view CONFDATA_MEMORY_LIMIT_ARG{"confdata-memory-limit"}; + static constexpr std::string_view CONFDATA_OOM_HANDLING_SIZE_ARG{"confdata-oom-handling-size"}; + static constexpr std::string_view CONFDATA_PROXY_ACTOR_NAME_ARG{"confdata-proxy-actor-name"}; + static constexpr std::string_view PREDEFINED_WILDCARDS_ARG{"predefined-wildcards"}; + static constexpr std::string_view INITIAL_INSTANCE_MEMORY_SIZE_ARG{"initial-instance-memory-size"}; + static constexpr std::string_view MIN_INSTANCE_EXTRA_MEMORY_SIZE_ARG{"min-instance-extra-memory-size"}; + static constexpr auto INIT_COMPONENT_ALLOCATOR_SIZE{static_cast(1024U * 1024U)}; // 1MiB + static constexpr auto DEFAULT_MIN_COMPONENT_EXTRA_MEMORY_POOL_SIZE = static_cast(1024U * 1024U); // 1MiB + static constexpr auto DEFAULT_INIT_INSTANCE_ALLOCATOR_SIZE{static_cast(64U * 1024U * 1024U)}; // 64MiB + static constexpr auto DEFAULT_MIN_INSTANCE_EXTRA_MEMORY_SIZE{64U * 1024U * 1024U}; // 64MiB + /** Leaves the same five-percent runway between legacy kPHP's hard OOM threshold and its memory limit. */ + static constexpr size_t DEFAULT_OOM_HANDLING_SIZE_DIVISOR{20}; + +public: AllocatorState m_allocator_state{INIT_COMPONENT_ALLOCATOR_SIZE, DEFAULT_MIN_COMPONENT_EXTRA_MEMORY_POOL_SIZE, 0}; - kphp::stl::string m_confdata_proxy_actor_name; private: const uint32_t m_argc{k2::args_count()}; + // Owns the immutable multiline argument referenced by m_predefined_wildcards. + kphp::stl::string m_predefined_wildcards_storage; public: + /** Total allocator payload available in each shared-memory piece. */ + size_t m_confdata_memory_limit{}; + /** Final allocatable part of the payload reserved for completing one in-flight operation safely. */ + size_t m_confdata_oom_handling_size{}; + kphp::stl::string m_confdata_proxy_actor_name; + kphp::stl::vector m_predefined_wildcards; + size_t m_initial_instance_memory_size{DEFAULT_INIT_INSTANCE_ALLOCATOR_SIZE}; + size_t m_min_instance_extra_memory_size{DEFAULT_MIN_INSTANCE_EXTRA_MEMORY_SIZE}; + + // === METHODS ================================================================================== ComponentState() noexcept; static auto get() noexcept -> const ComponentState&; static auto get_mutable() noexcept -> ComponentState&; private: + auto parse_confdata_memory_limit_arg(std::string_view) noexcept -> void; + auto parse_confdata_oom_handling_size_arg(std::string_view) noexcept -> void; auto parse_confdata_proxy_actor_name_arg(std::string_view) noexcept -> void; + auto parse_predefined_wildcards_arg(std::string_view) noexcept -> void; + auto parse_initial_instance_memory_size_arg(std::string_view) noexcept -> void; + auto parse_min_instance_extra_memory_size_arg(std::string_view) noexcept -> void; auto parse_args() noexcept -> void; - - static constexpr std::string_view CONFDATA_PROXY_ACTOR_NAME_ARG{"confdata-proxy-actor-name"}; - static constexpr auto INIT_COMPONENT_ALLOCATOR_SIZE{static_cast(1024U * 1024U)}; // 1MiB - static constexpr auto DEFAULT_MIN_COMPONENT_EXTRA_MEMORY_POOL_SIZE = static_cast(1024U * 1024U); // 1MiB }; inline ComponentState::ComponentState() noexcept { parse_args(); + if (m_confdata_memory_limit == 0) { + kphp::log::error("{} argument is required and must be a positive number", CONFDATA_MEMORY_LIMIT_ARG); + } + if (m_confdata_oom_handling_size == 0) { + m_confdata_oom_handling_size = m_confdata_memory_limit / DEFAULT_OOM_HANDLING_SIZE_DIVISOR; + if (m_confdata_oom_handling_size == 0) { + m_confdata_oom_handling_size = 1; + } + } + if (m_confdata_oom_handling_size >= m_confdata_memory_limit) { + kphp::log::error("{} must be smaller than {}", CONFDATA_OOM_HANDLING_SIZE_ARG, CONFDATA_MEMORY_LIMIT_ARG); + } if (m_confdata_proxy_actor_name.empty()) { kphp::log::error("{} argument is required", CONFDATA_PROXY_ACTOR_NAME_ARG); } diff --git a/runtime-light/components/confdata/state/instance-state.cpp b/runtime-light/components/confdata/state/instance-state.cpp index 01510f1bb1..6d7875844d 100644 --- a/runtime-light/components/confdata/state/instance-state.cpp +++ b/runtime-light/components/confdata/state/instance-state.cpp @@ -7,31 +7,210 @@ #include #include #include +#include +#include #include #include #include #include +#include "runtime-common/stdlib/serialization/json-functions.h" +#include "runtime-common/stdlib/serialization/serialize-functions.h" #include "runtime-light/components/confdata/confdata-proxy/sync-functions.h" #include "runtime-light/components/confdata/confdata-proxy/tl.h" #include "runtime-light/components/confdata/state/component-state.h" +#include "runtime-light/coroutine/event.h" #include "runtime-light/coroutine/task.h" #include "runtime-light/coroutine/when-all.h" +#include "runtime-light/stdlib/confdata/confdata-constants.h" +#include "runtime-light/stdlib/confdata/confdata-reader-lease.h" +#include "runtime-light/stdlib/confdata/confdata-storage.h" #include "runtime-light/stdlib/diagnostics/logs.h" +#include "runtime-light/streams/connection.h" #include "runtime-light/streams/stream.h" namespace { -auto sync_handler(std::span events) noexcept -> void { - kphp::log::info("got {} events on sync", events.size()); +constexpr auto CONFDATA_RETRY_INTERVAL{std::chrono::seconds{1}}; + +// Event decoding stays component-local: only the writer sees serialization +// flags, while readers consume the already-decoded shared `mixed` values. +auto decode_value(const tl::confdata::keyValuePair& event) noexcept -> mixed { + if (event.is_php_serialized.value && event.is_json_serialized.value) [[unlikely]] { + kphp::log::warning("confdata value has both php_serialized and json_serialized flags set: key -> {}", event.key.value); + return {}; + } + if (event.is_php_serialized.value) { + return unserialize_raw(event.value.value.data(), static_cast(event.value.value.size())); + } else if (event.is_json_serialized.value) { + return json_decode(event.value.value).value_or(mixed{}); + } + return string{event.value.value.data(), static_cast(event.value.value.size())}; } -auto update_handler(std::span events) noexcept -> void { - kphp::log::info("got {} events on update", events.size()); +auto report_reached_oom_threshold(const kphp::confdata::storage& storage) noexcept -> bool { + if (!storage.is_oom_threshold_reached()) { + return false; + } + const auto usage{storage.memory_usage()}; + kphp::log::warning("confdata shared-memory OOM threshold reached: used -> {}, threshold -> {}, capacity -> {}", usage.m_used, usage.m_oom_threshold, + usage.m_capacity); + return true; } } // namespace +template<> +struct std::formatter { + template + constexpr auto parse(parse_context_type& ctx) const noexcept { + return ctx.begin(); + } + + template + auto format(const InstanceState::confdata_sync_error& error, format_context_type& ctx) const noexcept { + using stage = InstanceState::confdata_sync_error::stage; + + std::string_view stage_name{"unknown"}; + switch (error.m_stage) { + case stage::memory_size: + stage_name = "memory size calculation"; + break; + case stage::shared_memory_allocation: + stage_name = "shared memory allocation"; + break; + case stage::storage_initialization: + stage_name = "storage initialization"; + break; + case stage::wildcard_initialization: + stage_name = "predefined wildcard initialization"; + break; + case stage::oom_threshold: + stage_name = "OOM threshold check"; + break; + case stage::synchronization: + stage_name = "clean synchronization"; + break; + case stage::shared_memory_publication: + stage_name = "shared memory publication"; + break; + } + return std::format_to(ctx.out(), "{}: error -> {}", stage_name, error.m_code); + } +}; + +class InstanceState::reader_session final { + /** Owner that removes a retired piece after this reader disconnects. */ + InstanceState& m_instance_state; + /** Stable iterator to the registry node containing this session's sample. */ + confdata_piece_list::iterator m_piece_it; + /** Ring sample pinned in the piece referenced by `m_piece_it`. */ + kphp::confdata::storage::sample_id m_sample_id; + +public: + reader_session(InstanceState& instance_state, confdata_piece_list::iterator piece_it) noexcept + : m_instance_state{instance_state}, + m_piece_it{piece_it}, + m_sample_id{m_piece_it->acquire_active_sample()} {} + + ~reader_session() { + m_instance_state.release_reader(m_piece_it, m_sample_id); + } + + reader_session(const reader_session&) = delete; + reader_session(reader_session&&) = delete; + auto operator=(const reader_session&) -> reader_session& = delete; + auto operator=(reader_session&&) -> reader_session& = delete; + + auto sample_id() const noexcept -> kphp::confdata::storage::sample_id { + return m_sample_id; + } +}; + +InstanceState::confdata_piece::confdata_piece(const creation_token& /* token */, void* memory) noexcept + : m_memory{memory} {} + +InstanceState::confdata_piece::~confdata_piece() { + if (m_storage.is_initialized()) { + m_storage.close(); + } + // TODO: + // if (const auto released{k2::free_shared_memory(m_memory)}; !released) [[unlikely]] { + // kphp::log::warning("failed to free confdata shared memory: error -> {}", released.error()); + // } +} + +auto InstanceState::confdata_piece::create(confdata_piece_list& owner, size_t memory_limit, size_t oom_handling_size, + std::span predefined_wildcards) noexcept + -> std::expected { + kphp::log::assertion(owner.empty()); + + const auto shared_memory_size{kphp::confdata::storage::memory_size(memory_limit)}; + if (!shared_memory_size) [[unlikely]] { + return std::unexpected{confdata_sync_error{.m_stage = confdata_sync_error::stage::memory_size, + .m_code = static_cast(std::to_underlying(shared_memory_size.error()))}}; + } + + const auto shared_memory{k2::alloc_shared_memory(*shared_memory_size, kphp::confdata::storage::memory_alignment())}; + if (!shared_memory) [[unlikely]] { + return std::unexpected{confdata_sync_error{.m_stage = confdata_sync_error::stage::shared_memory_allocation, .m_code = shared_memory.error()}}; + } + + const auto piece_it{owner.emplace(owner.end(), creation_token{}, *shared_memory)}; + if (const auto initialized{piece_it->m_storage.init({static_cast(*shared_memory), *shared_memory_size}, oom_handling_size)}; !initialized) + [[unlikely]] { + const confdata_sync_error error{.m_stage = confdata_sync_error::stage::storage_initialization, + .m_code = static_cast(std::to_underlying(initialized.error()))}; + owner.erase(piece_it); + return std::unexpected{error}; + } + if (const auto initialized{piece_it->m_storage.initialize_wildcards(predefined_wildcards)}; !initialized) [[unlikely]] { + const confdata_sync_error error{.m_stage = confdata_sync_error::stage::wildcard_initialization, + .m_code = static_cast(std::to_underlying(initialized.error()))}; + owner.erase(piece_it); + return std::unexpected{error}; + } + if (piece_it->m_storage.is_oom_threshold_reached()) [[unlikely]] { + const confdata_sync_error error{.m_stage = confdata_sync_error::stage::oom_threshold, .m_code = k2::errno_enomem}; + owner.erase(piece_it); + return std::unexpected{error}; + } + return piece_it; +} + +auto InstanceState::confdata_piece::storage() noexcept -> kphp::confdata::storage& { + return m_storage; +} + +auto InstanceState::confdata_piece::acquire_active_sample() noexcept -> kphp::confdata::storage::sample_id { + ++m_readers; + return m_storage.acquire_active_sample(); +} + +auto InstanceState::confdata_piece::release_sample(kphp::confdata::storage::sample_id sample_id) noexcept -> void { + kphp::log::assertion(m_readers != 0); + m_storage.release_sample(sample_id); + --m_readers; +} + +auto InstanceState::confdata_piece::has_readers() const noexcept -> bool { + return m_readers != 0; +} + +auto InstanceState::release_reader(confdata_piece_list::iterator piece_it, kphp::confdata::storage::sample_id sample_id) noexcept -> void { + piece_it->release_sample(sample_id); + erase_if_retired_and_unused(piece_it); +} + +auto InstanceState::erase_if_retired_and_unused(confdata_piece_list::iterator piece_it) noexcept -> void { + kphp::log::assertion(!m_confdata_pieces.empty()); + kphp::log::assertion(piece_it != m_confdata_pieces.end()); + if (piece_it == std::prev(m_confdata_pieces.end()) || piece_it->has_readers()) { + return; + } + m_confdata_pieces.erase(piece_it); +} + auto InstanceState::init() noexcept -> void { auto main_task{run()}; // initialize async stack @@ -49,53 +228,202 @@ auto InstanceState::run() noexcept -> kphp::coro::task<> { auto InstanceState::accept_loop() noexcept -> kphp::coro::task<> { for (;;) { - auto opt_stream{co_await kphp::component::stream::accept()}; - if (!opt_stream.has_value()) [[unlikely]] { - kphp::log::warning("failed to accept a stream"); + auto stream{co_await kphp::component::stream::accept()}; + if (!stream.has_value()) [[unlikely]] { continue; } - auto request_stream{std::move(*opt_stream)}; - kphp::log::info("accepted a stream: descriptor -> {}", request_stream.descriptor()); - // dummy implementation: drain the request and close - if (auto expected{co_await request_stream.read_all([](std::span) noexcept {})}; !expected) [[unlikely]] { - kphp::log::warning("failed to read a request: error -> {}", expected.error()); + kphp::log::debug("accepted a stream: descriptor -> {}", stream->descriptor()); + if (!m_io_scheduler.start(serve_reader_lease(std::move(*stream)))) [[unlikely]] { + kphp::log::warning("failed to serve a confdata reader lease"); } } } +auto InstanceState::serve_reader_lease(kphp::component::stream reader_stream) noexcept -> kphp::coro::task<> { + auto connection{kphp::component::connection::from_stream(std::move(reader_stream))}; + if (!connection) [[unlikely]] { + co_return kphp::log::warning("failed to create a confdata reader connection: error -> {}", connection.error()); + } + + if (m_confdata_pieces.empty()) [[unlikely]] { + co_return kphp::log::warning("can't serve a confdata reader lease: can't find confdata piece"); + } + + reader_session session{*this, std::prev(m_confdata_pieces.end())}; + kphp::log::debug("issuing reader lease: sample -> {}, sections -> {}", session.sample_id(), + m_confdata_pieces.back().storage().values(session.sample_id()).size()); + const auto lease{kphp::confdata::reader_lease::create(kphp::confdata::SHARED_MEMORY_NAME, session.sample_id())}; + // The fixed shared-memory name and internally acquired sample ID must be valid. + // Failure here is an internal invariant violation, not a recoverable client error. + kphp::log::assertion(lease.has_value()); + if (const auto written{co_await connection->get_stream().write_all(std::as_bytes(std::span{std::addressof(*lease), 1}))}; !written) [[unlikely]] { + co_return kphp::log::warning("failed to write a confdata reader lease: error -> {}", written.error()); + } + + kphp::coro::event reader_disconnected{}; + if (const auto registered{connection->register_abort_handler([&reader_disconnected] noexcept { reader_disconnected.set(); })}; !registered) [[unlikely]] { + co_return kphp::log::warning("failed to watch a confdata reader connection: error -> {}", registered.error()); + } + co_await reader_disconnected; +} + +auto InstanceState::perform_sync(std::string_view confdata_proxy_actor) noexcept -> kphp::coro::task> { + kphp::log::debug("starting sync: actor -> {}", confdata_proxy_actor); + // kPHP scans the encoded snapshot before materializing it so wildcard arrays + // can reserve sufficient capacity before insertion. Retain the paginated + // proxy responses and replay those same bytes after collecting size hints. + kphp::confdata::storage::sync_size_hints size_hints{}; + auto snapshot{co_await kphp::confdata::sync(confdata_proxy_actor, [&size_hints](std::span events) noexcept { + for (const auto& event : events) { + if (!event.inner.value.value.empty()) { + size_hints.add(event.inner.key.value); + } + } + return true; + })}; + if (!snapshot) [[unlikely]] { + co_return std::unexpected{ + confdata_sync_error{.m_stage = confdata_sync_error::stage::synchronization, .m_code = static_cast(std::to_underlying(snapshot.error()))}}; + } + + size_hints.finish(); + size_t encoded_bytes{}; + for (const auto& page : snapshot->m_pages) { + encoded_bytes += page.size(); + } + kphp::log::debug("snapshot fetched: pages -> {}, encoded bytes -> {}, offset -> {}", snapshot->m_pages.size(), encoded_bytes, + snapshot->m_pagination.m_offset); + // A separate one-node list owns the unpublished piece and later permits a + // zero-allocation transfer into the registry. + confdata_piece_list pending_piece{}; + const auto created_piece{confdata_piece::create(pending_piece, m_component_state.m_confdata_memory_limit, m_component_state.m_confdata_oom_handling_size, + m_component_state.m_predefined_wildcards)}; + if (!created_piece) [[unlikely]] { + co_return std::unexpected{created_piece.error()}; + } + auto& piece{**created_piece}; + + auto sync_editor{piece.storage().start_sync(size_hints)}; + const auto replay_result{ + kphp::confdata::replay(*snapshot, [this, &storage = piece.storage(), &sync_editor](std::span events) noexcept { + return apply_batched_events(storage, sync_editor, events); + })}; + if (!replay_result) [[unlikely]] { + sync_editor.cancel(); + co_return std::unexpected{ + confdata_sync_error{.m_stage = confdata_sync_error::stage::synchronization, .m_code = static_cast(std::to_underlying(replay_result.error()))}}; + } + if (report_reached_oom_threshold(piece.storage())) [[unlikely]] { + sync_editor.cancel(); + co_return std::unexpected{confdata_sync_error{.m_stage = confdata_sync_error::stage::oom_threshold, .m_code = k2::errno_enomem}}; + } + + sync_editor.commit(); + const auto diagnostic_sample{piece.storage().acquire_active_sample()}; + const auto diagnostic_sections{piece.storage().values(diagnostic_sample).size()}; + piece.storage().release_sample(diagnostic_sample); + kphp::log::debug("snapshot committed: sample -> {}, sections -> {}, used bytes -> {}", diagnostic_sample, diagnostic_sections, + piece.storage().memory_usage().m_used); + // Existing readers keep their mapped allocation; future lookups of the + // stable name resolve to this newly published piece. + if (const auto published{k2::publish_shared_memory(kphp::confdata::SHARED_MEMORY_NAME, piece.storage().memory().data(), 0, true, true)}; !published) + [[unlikely]] { + co_return std::unexpected{ + confdata_sync_error{.m_stage = confdata_sync_error::stage::shared_memory_publication, .m_code = static_cast(published.error())}}; + } + // Only successfully synchronized and published pieces enter the registry, + // so its last element is always the current piece. + const auto retired_piece_it{m_confdata_pieces.empty() ? m_confdata_pieces.end() : std::prev(m_confdata_pieces.end())}; + m_confdata_pieces.splice(m_confdata_pieces.end(), pending_piece); + if (retired_piece_it != m_confdata_pieces.end()) { + erase_if_retired_and_unused(retired_piece_it); + } + m_pagination = std::move(snapshot->m_pagination); + kphp::log::info("shared memory published: name -> {}, sample -> {}, sections -> {}, offset -> {}", kphp::confdata::SHARED_MEMORY_NAME, diagnostic_sample, + diagnostic_sections, m_pagination.m_offset); + co_return std::expected{}; +} + auto InstanceState::service_loop() noexcept -> kphp::coro::task<> { - static constexpr auto CONFDATA_RETRY_INTERVAL{std::chrono::seconds{1}}; const std::string_view confdata_proxy_actor{ComponentState::get().m_confdata_proxy_actor_name}; for (;;) { if (!m_pagination.m_has_synced) { - auto sync{co_await kphp::confdata::sync(confdata_proxy_actor, sync_handler)}; - if (!sync) [[unlikely]] { - kphp::log::warning("confdata sync failed: error -> {}, retrying", std::to_underlying(std::move(sync).error())); + const auto sync_result{co_await perform_sync(confdata_proxy_actor)}; + if (!sync_result) [[unlikely]] { + kphp::log::warning("failed to prepare a synchronized confdata shared-memory piece: {}; retrying", sync_result.error()); co_await m_io_scheduler.schedule(CONFDATA_RETRY_INTERVAL); continue; } - m_pagination = *std::move(sync); m_warmup_status = InstanceState::warmup_status::done; } - auto update{co_await kphp::confdata::update(confdata_proxy_actor, m_pagination, update_handler)}; + auto update{co_await kphp::confdata::update( + confdata_proxy_actor, m_pagination, [this](std::span events) noexcept { return apply_incremental_events(events); })}; // update returns only on error; m_pagination was advanced in place up to the last applied batch kphp::log::assertion(!update.has_value()); switch (update.error()) { case kphp::confdata::subscribe_error::old_offset: case kphp::confdata::subscribe_error::not_synced: // local version is too old: clean re-sync required - kphp::log::warning("confdata update failed: error -> {}, resyncing", std::to_underlying(std::move(update).error())); + kphp::log::warning("confdata update failed: error -> {}, resyncing", std::to_underlying(update.error())); m_pagination = {}; break; case kphp::confdata::subscribe_error::transport: case kphp::confdata::subscribe_error::malformed_response: // pagination is still valid; the longpoll resumes from the last applied position - kphp::log::warning("confdata update failed: error -> {}, retrying", std::to_underlying(std::move(update).error())); + kphp::log::warning("confdata update failed: error -> {}, retrying", std::to_underlying(update.error())); + break; + case kphp::confdata::subscribe_error::batch_rejected: + // The rejected batch did not advance pagination or alter the active + // sample. Reset pagination so the next iteration takes the clean-sync + // path while the current piece keeps serving its readers. + kphp::log::warning("the current confdata shared-memory piece can't accept an update; resyncing"); + m_pagination = {}; break; } co_await m_io_scheduler.schedule(CONFDATA_RETRY_INTERVAL); } } + +auto InstanceState::apply_batched_events(kphp::confdata::storage& storage, kphp::confdata::storage::editor& editor, + std::span events) noexcept -> bool { + if (report_reached_oom_threshold(storage)) [[unlikely]] { + return false; + } + for (const auto& event : events) { + if (event.inner.key.value.size() > kphp::confdata::MAX_KEY_LENGTH) [[unlikely]] { + kphp::log::warning("confdata event key is too long and was ignored: size -> {}", event.inner.key.value.size()); + continue; + } + if (event.inner.value.value.empty()) { + static_cast(editor.erase(event.inner.key.value)); + } else { + static_cast(editor.upsert(event.inner.key.value, [&event] noexcept { return decode_value(event.inner); })); + } + if (report_reached_oom_threshold(storage)) [[unlikely]] { + return false; + } + } + return true; +} + +auto InstanceState::apply_incremental_events(std::span events) noexcept -> bool { + kphp::log::assertion(!m_confdata_pieces.empty()); + auto& storage{m_confdata_pieces.back().storage()}; + if (report_reached_oom_threshold(storage)) [[unlikely]] { + return false; + } + auto editor{storage.start_update()}; + if (!editor) [[unlikely]] { + return false; + } + if (!apply_batched_events(storage, *editor, events)) [[unlikely]] { + return false; + } + if (editor->changed()) { + editor->commit(); + } + return true; +} diff --git a/runtime-light/components/confdata/state/instance-state.h b/runtime-light/components/confdata/state/instance-state.h index 1f1c4373fa..31e7794c73 100644 --- a/runtime-light/components/confdata/state/instance-state.h +++ b/runtime-light/components/confdata/state/instance-state.h @@ -6,43 +6,122 @@ #include #include +#include +#include +#include #include "common/mixin/not_copyable.h" +#include "runtime-common/core/allocator/script-allocator.h" +#include "runtime-common/core/std/containers.h" #include "runtime-light/allocator/allocator-state.h" #include "runtime-light/components/confdata/confdata-proxy/sync-functions.h" +#include "runtime-light/components/confdata/state/component-state.h" #include "runtime-light/coroutine/coroutine-state.h" #include "runtime-light/coroutine/io-scheduler.h" #include "runtime-light/coroutine/task.h" #include "runtime-light/k2-platform/k2-api.h" +#include "runtime-light/stdlib/confdata/confdata-storage.h" #include "runtime-light/stdlib/diagnostics/contextual-tags.h" +#include "runtime-light/streams/stream.h" struct InstanceState final : vk::not_copyable { + // === TYPES ==================================================================================== enum class warmup_status : uint8_t { pending, done }; - AllocatorState m_allocator_state{INIT_INSTANCE_ALLOCATOR_SIZE, DEFAULT_MIN_INSTANCE_EXTRA_MEMORY_POOL_SIZE, 0}; + struct confdata_sync_error final { + enum class stage : uint8_t { + memory_size, + shared_memory_allocation, + storage_initialization, + wildcard_initialization, + oom_threshold, + synchronization, + shared_memory_publication + }; + /** Step that failed while preparing and publishing the replacement piece. */ + stage m_stage; + /** Error code produced by that step's underlying API. */ + int32_t m_code; + }; + +private: + class confdata_piece; + using confdata_piece_list = kphp::stl::list; + + class confdata_piece final { + class creation_token final { + friend class confdata_piece; + + creation_token() noexcept = default; + }; + + /** K2 allocation owned and eventually released wholesale by this piece. */ + [[maybe_unused]] void* m_memory{}; // TODO: remove maybe_unused + /** Number of reader sessions that still refer to this piece. */ + size_t m_readers{}; + /** Non-owning writer view over the allocation. */ + kphp::confdata::storage m_storage; + + public: + /** Public for allocator-aware container construction; only `create()` can provide the token. */ + confdata_piece(const creation_token& /* token */, void* memory) noexcept; + ~confdata_piece(); + + confdata_piece(const confdata_piece&) = delete; + confdata_piece(confdata_piece&&) = delete; + auto operator=(const confdata_piece&) -> confdata_piece& = delete; + auto operator=(confdata_piece&&) -> confdata_piece& = delete; + + static auto create(confdata_piece_list& owner, size_t memory_limit, size_t oom_handling_size, + std::span predefined_wildcards) noexcept -> std::expected; + + auto storage() noexcept -> kphp::confdata::storage&; + auto acquire_active_sample() noexcept -> kphp::confdata::storage::sample_id; + auto release_sample(kphp::confdata::storage::sample_id sample_id) noexcept -> void; + auto has_readers() const noexcept -> bool; + }; + + class reader_session; + + // === MEMBERS ================================================================================== + const ComponentState& m_component_state{ComponentState::get()}; + +public: + AllocatorState m_allocator_state{m_component_state.m_initial_instance_memory_size, m_component_state.m_min_instance_extra_memory_size, 0}; warmup_status m_warmup_status{warmup_status::pending}; kphp::confdata::pagination m_pagination{}; - kphp::log::contextual_tags m_instance_tags; +private: + /** Owns retired pieces still used by readers followed by the current piece. */ + confdata_piece_list m_confdata_pieces; +public: + kphp::log::contextual_tags m_instance_tags{}; kphp::coro::instance_state m_coroutine_instance_state{INIT_INSTANCE_COROUTINE_ALLOCATOR_SIZE, DEFAULT_MIN_INSTANCE_EXTRA_COROUTINE_MEMORY_POOL_SIZE, 0}; kphp::coro::io_scheduler m_io_scheduler{m_coroutine_instance_state}; + // === METHODS ================================================================================== InstanceState() noexcept = default; static auto get() noexcept -> InstanceState&; auto init() noexcept -> void; private: - static constexpr auto INIT_INSTANCE_ALLOCATOR_SIZE = static_cast(16U * 1024U * 1024U); // 16MiB - static constexpr auto DEFAULT_MIN_INSTANCE_EXTRA_MEMORY_POOL_SIZE = static_cast(1024U * 1024U); // 1MiB - static constexpr auto INIT_INSTANCE_COROUTINE_ALLOCATOR_SIZE = static_cast(2U * 1024U * 1024U); // 2MiB - static constexpr auto DEFAULT_MIN_INSTANCE_EXTRA_COROUTINE_MEMORY_POOL_SIZE = static_cast(512U * 1024U); // 0.5MiB + static constexpr auto INIT_INSTANCE_COROUTINE_ALLOCATOR_SIZE = static_cast(16U * 1024U * 1024U); // 16MiB + static constexpr auto DEFAULT_MIN_INSTANCE_EXTRA_COROUTINE_MEMORY_POOL_SIZE = static_cast(1U * 1024U * 1024U); // 1MiB + auto release_reader(confdata_piece_list::iterator piece_it, kphp::confdata::storage::sample_id sample_id) noexcept -> void; + auto erase_if_retired_and_unused(confdata_piece_list::iterator piece_it) noexcept -> void; auto run() noexcept -> kphp::coro::task<>; auto accept_loop() noexcept -> kphp::coro::task<>; + auto serve_reader_lease(kphp::component::stream reader_stream) noexcept -> kphp::coro::task<>; + auto service_loop() noexcept -> kphp::coro::task<>; + auto perform_sync(std::string_view confdata_proxy_actor) noexcept -> kphp::coro::task>; + auto apply_batched_events(kphp::confdata::storage& storage, kphp::confdata::storage::editor& editor, + std::span events) noexcept -> bool; + auto apply_incremental_events(std::span events) noexcept -> bool; }; inline auto InstanceState::get() noexcept -> InstanceState& { diff --git a/runtime-light/components/kphp/bindings/bindings.cpp b/runtime-light/components/kphp/bindings/bindings.cpp index 5f1b40b704..5d55a00d2d 100644 --- a/runtime-light/components/kphp/bindings/bindings.cpp +++ b/runtime-light/components/kphp/bindings/bindings.cpp @@ -65,6 +65,14 @@ auto contextual_tags::try_get() noexcept -> std::optional instance_state& { + return InstanceState::get().confdata_instance_state; +} + +} // namespace kphp::confdata + auto AllocatorState::get() noexcept -> const AllocatorState& { if (const auto* instance_state_ptr{k2::instance_state()}; instance_state_ptr != nullptr) [[likely]] { return instance_state_ptr->instance_allocator_state; @@ -125,10 +133,6 @@ auto RpcServerInstanceState::get() noexcept -> RpcServerInstanceState& { return InstanceState::get().rpc_server_instance_state; } -auto ConfdataInstanceState::get() noexcept -> ConfdataInstanceState& { - return InstanceState::get().confdata_instance_state; -} - auto CurlInstanceState::get() noexcept -> CurlInstanceState& { return InstanceState::get().curl_instance_state; } diff --git a/runtime-light/components/kphp/state/instance-state.cpp b/runtime-light/components/kphp/state/instance-state.cpp index 7b332b13ce..0698f22329 100644 --- a/runtime-light/components/kphp/state/instance-state.cpp +++ b/runtime-light/components/kphp/state/instance-state.cpp @@ -25,6 +25,7 @@ #include "runtime-light/server/http/init-functions.h" #include "runtime-light/server/rpc/init-functions.h" #include "runtime-light/stdlib/component/component-api.h" +#include "runtime-light/stdlib/confdata/confdata-constants.h" #include "runtime-light/stdlib/diagnostics/logs.h" #include "runtime-light/stdlib/fork/fork-functions.h" #include "runtime-light/stdlib/fork/fork-state.h" @@ -149,6 +150,12 @@ kphp::coro::task<> InstanceState::run_instance_prologue() noexcept { superglobals.v$d$PHP_SAPI = string{sapi_name.data(), sapi_name.size()}; } + if (k2::component_access(kphp::confdata::COMPONENT_LINK_ALIAS) == k2::errno_ok) { // TODO: we want to do it during either component state init or warmup + co_await confdata_instance_state.init(); + } else { + kphp::log::info("confdata initialization skipped: component link '{}' is unavailable", kphp::confdata::COMPONENT_LINK_ALIAS); + } + if constexpr (kind == image_kind::cli || kind == image_kind::server) { // TODO set these headers in CLI and HTTP modes only static constexpr std::string_view DEFAULT_SERVER_NAME{"nginx/0.3.33"}; @@ -224,4 +231,5 @@ kphp::coro::task<> InstanceState::run_instance_epilogue() noexcept { web_state.session_is_finished = true; web_state.session.reset(); } + confdata_instance_state.release(); } diff --git a/runtime-light/components/kphp/state/instance-state.h b/runtime-light/components/kphp/state/instance-state.h index 92beeab5bf..960cf6b113 100644 --- a/runtime-light/components/kphp/state/instance-state.h +++ b/runtime-light/components/kphp/state/instance-state.h @@ -64,8 +64,7 @@ struct InstanceState final : vk::not_copyable { // It's important to use `{}` instead of `= default` here. // In the second case clang++ zeroes the whole structure. // It drastically ruins performance. Be careful! - InstanceState() noexcept - : component_state{ComponentState::get()} { + InstanceState() noexcept { kml_instance_state.init(component_state.kml_component_state.max_buffer_size()); } @@ -87,7 +86,7 @@ struct InstanceState final : vk::not_copyable { return instance_kind_; } - const ComponentState& component_state; + const ComponentState& component_state{ComponentState::get()}; AllocatorState instance_allocator_state{component_state.initial_instance_memory_size, component_state.min_instance_extra_memory_size, 0}; @@ -111,7 +110,7 @@ struct InstanceState final : vk::not_copyable { JobWorkerClientInstanceState job_worker_client_instance_state; JobWorkerServerInstanceState job_worker_server_instance_state; InstanceCacheInstanceState instance_cache_instance_state; - ConfdataInstanceState confdata_instance_state; + kphp::confdata::instance_state confdata_instance_state; TimeInstanceState time_instance_state; MathInstanceState math_instance_state; diff --git a/runtime-light/k2-platform/k2-api.h b/runtime-light/k2-platform/k2-api.h index a066930941..e5a369e9e0 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -129,6 +129,32 @@ inline void free_checked(void* ptr, size_t size, size_t align) noexcept { k2_free_checked(ptr, size, align); } +inline std::expected alloc_shared_memory(size_t size, size_t align) noexcept { + void* pointer{}; + if (const auto error_code{k2_alloc_shared_memory(size, align, std::addressof(pointer))}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return pointer; +} + +inline std::expected publish_shared_memory(std::string_view name, const void* memory, uint64_t ttl, bool as_mut, bool ignore_if_exist) noexcept { + if (const auto error_code{k2_publish_shared_memory(name.data(), name.size(), memory, ttl, as_mut, ignore_if_exist)}; error_code != k2::errno_ok) + [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + +inline std::expected, int32_t> get_shared_memory(std::string_view name) noexcept { + const void* pointer{}; + size_t size{}; + if (const auto error_code{k2_get_shared_memory(name.data(), name.size(), std::addressof(pointer), std::addressof(size))}; error_code != k2::errno_ok) + [[unlikely]] { + return std::unexpected{error_code}; + } + return std::span{static_cast(pointer), size}; +} + [[noreturn]] inline void exit(int32_t exit_code) noexcept { k2_exit(exit_code); } diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index 383a551f06..b3e0b0d69a 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -200,7 +200,7 @@ void k2_free_checked(void* ptr, size_t size, size_t align); /** * Shared memory provides a mechanism for instances to share data. * To use it, first allocate memory with `k2_alloc_shared_memory`, then publish - * it with a unique name using `k2_publish_shared_memory`. Other instances can + * it with a name using `k2_publish_shared_memory`. Other instances can * then retrieve the memory by name with `k2_get_shared_memory`. * * Lifecycle: @@ -211,7 +211,8 @@ void k2_free_checked(void* ptr, size_t size, size_t align); * - Calling `k2_publish_shared_memory` sets the reference count to one * - Calling `k2_get_shared_memory` increments the reference count * - Reference count is decremented automatically when instance finishes - * - No explicit release function is needed + * - The publishing instance releases an allocation explicitly with + * `k2_free_shared_memory` once it no longer needs to keep it published */ /** @@ -237,8 +238,9 @@ int32_t k2_alloc_shared_memory(size_t size, size_t align, void** pointer); /** * Publishes shared memory with a name and TTL, making it discoverable by other instances. * - * @param `name` Name to associate with the memory region. Must be unique. - * Should be valid UTF-8 and not contain null bytes. + * @param `name` Name to associate with the memory region. Should be valid + * UTF-8 and not contain null bytes. A live name can be reused + * only when `ignore_if_exist` is true. * @param `name_len` Length of the name in bytes. Must be greater than 0. * @param `memory` Pointer to memory previously allocated via `k2_alloc_shared_memory`. * @param `ttl` Time-to-live in milliseconds. Memory becomes eligible for diff --git a/runtime-light/runtime-light.cmake b/runtime-light/runtime-light.cmake index 736fa8cec2..c16371b353 100644 --- a/runtime-light/runtime-light.cmake +++ b/runtime-light/runtime-light.cmake @@ -2,7 +2,7 @@ include(${THIRD_PARTY_DIR}/pcre2-cmake/pcre2.cmake) # ================================================================================================= -set(RUNTIME_LIGHT_COMPILE_FLAGS -stdlib=libc++ -fcoro-aligned-allocation ${RUNTIME_LIGHT_VISIBILITY}) +set(RUNTIME_LIGHT_COMPILE_FLAGS -DRUNTIME_LIGHT -stdlib=libc++ -fcoro-aligned-allocation ${RUNTIME_LIGHT_VISIBILITY}) set(RUNTIME_LIGHT_PLATFORM_SPECIFIC_LINK_FLAGS) if(APPLE) diff --git a/runtime-light/stdlib/confdata/confdata-constants.h b/runtime-light/stdlib/confdata/confdata-constants.h index d2f3d2b5a4..1adb66c3e7 100644 --- a/runtime-light/stdlib/confdata/confdata-constants.h +++ b/runtime-light/stdlib/confdata/confdata-constants.h @@ -8,6 +8,13 @@ namespace kphp::confdata { -inline constexpr std::string_view COMPONENT_NAME = "confdata"; // TODO: it may actually have an alias specified in linking config +inline constexpr std::string_view IMAGE_NAME{"kphp-confdata"}; + +// K2 resolves component streams by the link alias from the caller's linking +// config, not by the target image or component name. KPHP images that use +// confdata must therefore expose the confdata component under this alias. +inline constexpr std::string_view COMPONENT_LINK_ALIAS{"kphp-confdata"}; + +inline constexpr std::string_view SHARED_MEMORY_NAME{"#kphp-confdata"}; } // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/confdata-functions.cpp b/runtime-light/stdlib/confdata/confdata-functions.cpp index b3e6c154e6..de940cd3b7 100644 --- a/runtime-light/stdlib/confdata/confdata-functions.cpp +++ b/runtime-light/stdlib/confdata/confdata-functions.cpp @@ -4,130 +4,163 @@ #include "runtime-light/stdlib/confdata/confdata-functions.h" -#include #include -#include #include -#include -#include "runtime-common/core/allocator/script-allocator.h" #include "runtime-common/core/runtime-core.h" -#include "runtime-common/core/std/containers.h" -#include "runtime-common/stdlib/serialization/json-functions.h" -#include "runtime-common/stdlib/serialization/serialize-functions.h" -#include "runtime-light/coroutine/task.h" -#include "runtime-light/k2-platform/k2-api.h" -#include "runtime-light/stdlib/component/component-api.h" -#include "runtime-light/stdlib/confdata/confdata-constants.h" +#include "runtime-light/stdlib/confdata/confdata-keys.h" #include "runtime-light/stdlib/confdata/confdata-state.h" #include "runtime-light/stdlib/diagnostics/logs.h" -#include "runtime-light/stdlib/fork/fork-functions.h" -#include "runtime-light/streams/read-ext.h" -#include "runtime-light/streams/stream.h" -#include "runtime-light/tl/tl-core.h" -#include "runtime-light/tl/tl-functions.h" -#include "runtime-light/tl/tl-types.h" namespace { -mixed extract_confdata_value(const tl::confdataValue& confdata_value) noexcept { - if (confdata_value.is_php_serialized.value && confdata_value.is_json_serialized.value) [[unlikely]] { // check that we don't have both flags set - kphp::log::warning("confdata value has both php_serialized and json_serialized flags set"); - return {}; +auto verify_confdata_parameter(std::string_view parameter) noexcept -> bool { + if (!kphp::confdata::instance_state::get().is_initialized()) [[unlikely]] { + kphp::log::warning("confdata is not initialized"); + return false; + } + if (parameter.size() > kphp::confdata::MAX_KEY_LENGTH) [[unlikely]] { + kphp::log::warning("confdata key is too long {}", parameter); + return false; } - if (confdata_value.is_php_serialized.value) { - return unserialize_raw(confdata_value.value.value.data(), static_cast(confdata_value.value.value.size())); - } else if (confdata_value.is_json_serialized.value) { - return json_decode(confdata_value.value.value).value_or(mixed{}); - } else { - return string{confdata_value.value.value.data(), static_cast(confdata_value.value.value.size())}; + if (parameter.empty()) [[unlikely]] { + kphp::log::warning("confdata does not support empty keys"); + return false; } + return true; } } // namespace -kphp::coro::task f$confdata_get_value(string key) noexcept { - if (key.empty()) [[unlikely]] { - kphp::log::warning("empty key is not supported"); - co_return mixed{}; - } - - auto& confdata_key_cache{ConfdataInstanceState::get().key_cache()}; - if (auto it{confdata_key_cache.find(key)}; it != confdata_key_cache.end()) { - co_return it->second; +auto f$confdata_get_value(const string& key) noexcept -> mixed { + const std::string_view key_view{key.c_str(), key.size()}; + if (!verify_confdata_parameter(key_view)) [[unlikely]] { + return {}; } - tl::ConfdataGet confdata_get{.key = {.value = {key.c_str(), key.size()}}}; - tl::storer tls{confdata_get.footprint()}; - confdata_get.store(tls); + const auto& confdata_st{kphp::confdata::instance_state::get()}; + const auto views{kphp::confdata::split_key(key_view, confdata_st.wildcards())}; + kphp::log::assertion(views.has_value()); + const kphp::confdata::key_handles handles{*views}; - auto expected_stream{kphp::component::stream::open(kphp::confdata::COMPONENT_NAME, k2::stream_kind::component)}; - if (!expected_stream) [[unlikely]] { - co_return mixed{}; + const auto& values{confdata_st.values()}; + const auto section_it{values.find(handles.section())}; + if (section_it == values.end()) { + return {}; } - - auto stream{*std::move(expected_stream)}; - kphp::stl::vector response{}; - if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response)))) [[unlikely]] { - co_return mixed{}; + if (views->kind() == kphp::confdata::section_kind::simple_key) { + return section_it->second; } - tl::fetcher tlf{response}; - tl::Maybe maybe_confdata_value{}; - kphp::log::assertion(maybe_confdata_value.fetch(tlf)); - - if (!maybe_confdata_value.opt_value) { // no such key - co_return mixed{}; + kphp::log::assertion(section_it->second.is_array()); + if (const auto* value{section_it->second.as_array().find_value(handles.remainder())}; value != nullptr) { + return *value; } - - auto value{extract_confdata_value(*maybe_confdata_value.opt_value)}; // the key exists - confdata_key_cache.emplace(std::move(key), value); - co_return std::move(value); + return {}; } -kphp::coro::task> f$confdata_get_values_by_any_wildcard(string wildcard) noexcept { - static constexpr size_t CONFDATA_GET_WILDCARD_INIT_BUFFER_CAPACITY = 1 << 20; +auto f$confdata_get_values_by_any_wildcard(const string& wildcard) noexcept -> array { + const std::string_view wildcard_view{wildcard.c_str(), wildcard.size()}; + if (!verify_confdata_parameter(wildcard_view)) [[unlikely]] { + return {}; + } - if (wildcard.empty()) [[unlikely]] { - kphp::log::warning("empty wildcard is not supported"); - co_return array{}; + const auto& confdata_st{kphp::confdata::instance_state::get()}; + const auto& predefined_wildcards{confdata_st.wildcards()}; + const auto views{kphp::confdata::split_key(wildcard_view, predefined_wildcards)}; + kphp::log::assertion(views.has_value()); + const kphp::confdata::key_handles handles{*views}; + const auto& values{confdata_st.values()}; + + if (views->kind() != kphp::confdata::section_kind::simple_key) { + const auto section_it{values.find(handles.section())}; + if (section_it == values.end()) { + return {}; + } + + kphp::log::assertion(section_it->second.is_array()); + const auto& entries{section_it->second.as_array()}; + if (handles.remainder().is_string() && handles.remainder().as_string().empty()) { + return entries; + } + + array result{}; + const string remainder_prefix{handles.remainder().to_string()}; + const std::string_view remainder_prefix_view{remainder_prefix.c_str(), remainder_prefix.size()}; + for (const auto& entry : entries) { + const string entry_key{entry.get_key().to_string()}; + const std::string_view entry_key_view{entry_key.c_str(), entry_key.size()}; + if (entry_key_view.starts_with(remainder_prefix_view)) { + const auto suffix{entry_key_view.substr(remainder_prefix_view.size())}; + result.set_value(string{suffix.data(), static_cast(suffix.size())}, entry.get_value()); + } + } + return result; } - auto& confdata_wildcard_cache{ConfdataInstanceState::get().wildcard_cache()}; - if (auto it{confdata_wildcard_cache.find(wildcard)}; it != confdata_wildcard_cache.end()) { - co_return it->second; + array result{}; + const auto merge_entries{[&result, wildcard_view](kphp::confdata::storage::map_type::const_iterator section_it) noexcept { + const std::string_view section_view{section_it->first.c_str(), section_it->first.size()}; + const auto suffix_view{section_view.substr(wildcard_view.size())}; + const string section_suffix{suffix_view.data(), static_cast(suffix_view.size())}; + kphp::log::assertion(section_it->second.is_array()); + const auto& entries{section_it->second.as_array()}; + const auto inserting_size{entries.size() + result.size()}; + result.reserve(inserting_size.size, inserting_size.is_vector); + for (const auto& entry : entries) { + result.set_value(string{section_suffix}.append(entry.get_key()), entry.get_value()); + } + }}; + + for (auto section_it{values.lower_bound(handles.section())}; section_it != values.end(); ++section_it) { + const std::string_view section_view{section_it->first.c_str(), section_it->first.size()}; + if (!section_view.starts_with(wildcard_view)) { + break; + } + switch (kphp::confdata::classify_section(section_view, predefined_wildcards)) { + case kphp::confdata::section_kind::simple_key: { + const auto suffix{section_view.substr(wildcard_view.size())}; + result.set_value(string{suffix.data(), static_cast(suffix.size())}, section_it->second); + break; + } + case kphp::confdata::section_kind::predefined_wildcard: + if (!section_view.contains('.') && predefined_wildcards.is_top_level_wildcard(section_view)) { + merge_entries(section_it); + } + break; + case kphp::confdata::section_kind::one_dot_wildcard: + if (!predefined_wildcards.has_matching_wildcard(section_view)) { + merge_entries(section_it); + } + break; + case kphp::confdata::section_kind::two_dots_wildcard: + break; + } } + return result; +} +auto f$confdata_get_values_by_predefined_wildcard(const string& wildcard) noexcept -> array { const std::string_view wildcard_view{wildcard.c_str(), wildcard.size()}; - - const tl::ConfdataGetWildcard confdata_get_wildcard{.wildcard = {.value = wildcard_view}}; - tl::storer tls{confdata_get_wildcard.footprint()}; - confdata_get_wildcard.store(tls); - - auto expected_stream{kphp::component::stream::open(kphp::confdata::COMPONENT_NAME, k2::stream_kind::component)}; - if (!expected_stream) [[unlikely]] { - co_return array{}; + if (!verify_confdata_parameter(wildcard_view)) [[unlikely]] { + return {}; } - auto stream{*std::move(expected_stream)}; - kphp::stl::vector response{}; - response.reserve(CONFDATA_GET_WILDCARD_INIT_BUFFER_CAPACITY); - if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response)))) [[unlikely]] { - co_return array{}; + const auto& confdata_st{kphp::confdata::instance_state::get()}; + if (kphp::confdata::classify_section(wildcard_view, confdata_st.wildcards()) == kphp::confdata::section_kind::simple_key) [[unlikely]] { + kphp::log::warning("trying to get elements by non-predefined wildcard '{}'", wildcard_view); + return {}; } - tl::fetcher tlf{response}; - tl::Dictionary dict_confdata_value{}; - kphp::log::assertion(dict_confdata_value.fetch(tlf)); - - array result{array_size{static_cast(dict_confdata_value.size()), false}}; - std::ranges::for_each(dict_confdata_value, [&result, wildcard_size = wildcard_view.size()](const auto& dict_field) noexcept { - kphp::log::assertion(dict_field.key.value.size() >= wildcard_size); + const auto views{kphp::confdata::split_key_with_predefined_wildcard(wildcard_view, wildcard_view.size())}; + kphp::log::assertion(views.has_value()); + const kphp::confdata::key_handles handles{*views}; + const auto& values{confdata_st.values()}; + const auto elements_it{values.find(handles.section())}; + if (elements_it == values.end()) { + return {}; + } - const std::string_view key_without_wildcard_prefix{dict_field.key.value.substr(wildcard_size)}; - result.set_value(string{key_without_wildcard_prefix.data(), static_cast(key_without_wildcard_prefix.size())}, - extract_confdata_value(dict_field.value)); - }); - confdata_wildcard_cache.emplace(std::move(wildcard), result); - co_return std::move(result); + kphp::log::assertion(elements_it->second.is_array()); + return elements_it->second.as_array(); } diff --git a/runtime-light/stdlib/confdata/confdata-functions.h b/runtime-light/stdlib/confdata/confdata-functions.h index 1e4dba4682..6c1e8404cd 100644 --- a/runtime-light/stdlib/confdata/confdata-functions.h +++ b/runtime-light/stdlib/confdata/confdata-functions.h @@ -4,21 +4,15 @@ #pragma once -#include - #include "runtime-common/core/runtime-core.h" -#include "runtime-light/coroutine/task.h" -#include "runtime-light/k2-platform/k2-api.h" -#include "runtime-light/stdlib/confdata/confdata-constants.h" +#include "runtime-light/stdlib/confdata/confdata-state.h" inline bool f$is_confdata_loaded() noexcept { - return k2::component_access(kphp::confdata::COMPONENT_NAME) == k2::errno_ok; + return kphp::confdata::instance_state::get().is_initialized(); } -kphp::coro::task f$confdata_get_value(string key) noexcept; +auto f$confdata_get_value(const string& key) noexcept -> mixed; -kphp::coro::task> f$confdata_get_values_by_any_wildcard(string wildcard) noexcept; +auto f$confdata_get_values_by_any_wildcard(const string& wildcard) noexcept -> array; -inline kphp::coro::task> f$confdata_get_values_by_predefined_wildcard(string wildcard) noexcept { - co_return co_await f$confdata_get_values_by_any_wildcard(std::move(wildcard)); -} +auto f$confdata_get_values_by_predefined_wildcard(const string& wildcard) noexcept -> array; diff --git a/runtime-light/stdlib/confdata/confdata-keys.cpp b/runtime-light/stdlib/confdata/confdata-keys.cpp new file mode 100644 index 0000000000..9957e01f7f --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-keys.cpp @@ -0,0 +1,66 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#include "runtime-light/stdlib/confdata/confdata-keys.h" + +#include +#include +#include +#include +#include + +#include "common/php-functions.h" + +namespace { + +/** + * @brief Normalizes a key remainder like a PHP array key: numeric strings become `int64_t`. + */ +auto normalize_remainder(std::string_view remainder) noexcept -> kphp::confdata::key_views::remainder_type { + int64_t remainder_as_int{0}; + if (!remainder.empty() && php_try_to_int(remainder.data(), remainder.size(), std::addressof(remainder_as_int))) { + return {remainder_as_int}; + } + return {remainder}; +} + +} // namespace + +namespace kphp::confdata { + +auto split_key(std::string_view key) noexcept -> std::expected { + if (key.size() > MAX_KEY_LENGTH) [[unlikely]] { + return std::unexpected{split_error::key_too_long}; + } + + const auto first_dot{key.find('.')}; + if (first_dot == std::string_view::npos) { + return key_views{section_kind::simple_key, key, key, key_views::remainder_type{}}; + } + const auto second_dot{key.find('.', first_dot + 1)}; + if (second_dot == std::string_view::npos) { + return key_views{section_kind::one_dot_wildcard, key, key.substr(0, first_dot + 1), normalize_remainder(key.substr(first_dot + 1))}; + } + return key_views{section_kind::two_dots_wildcard, key, key.substr(0, second_dot + 1), normalize_remainder(key.substr(second_dot + 1))}; +} + +auto split_key(std::string_view key, const predefined_wildcards& wildcards) noexcept -> std::expected { + // if the key has a predefined wildcard prefix, use the shortest matching one as the section + if (const auto opt_wildcard{wildcards.shortest_matching_wildcard(key)}; opt_wildcard.has_value()) { + return split_key_with_predefined_wildcard(key, opt_wildcard->size()); + } + return split_key(key); +} + +auto split_key_with_predefined_wildcard(std::string_view key, size_t wildcard_len) noexcept -> std::expected { + if (key.size() > MAX_KEY_LENGTH) [[unlikely]] { + return std::unexpected{split_error::key_too_long}; + } + if (wildcard_len == 0 || wildcard_len > key.size()) [[unlikely]] { + return std::unexpected{split_error::invalid_predefined_wildcard_length}; + } + return key_views{section_kind::predefined_wildcard, key, key.substr(0, wildcard_len), normalize_remainder(key.substr(wildcard_len))}; +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/confdata-keys.h b/runtime-light/stdlib/confdata/confdata-keys.h new file mode 100644 index 0000000000..e12d70ae0d --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-keys.h @@ -0,0 +1,195 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "common/mixin/not_copyable.h" +#include "common/wrappers/overloaded.h" +#include "runtime-common/core/runtime-core.h" +#include "runtime-light/stdlib/confdata/predefined-wildcards.h" +#include "runtime-light/stdlib/confdata/wildcard-kind.h" + +// A port of runtime/confdata-keys.h (minus the blacklist) shared by the confdata component and the kphp client. +// The client never includes this header directly; it's an implementation detail of the confdata sample reader/writer. +// +// Confdata keys are stored denormalized in two levels: a key is split into a `section` +// (the top-level storage key) and a `remainder` (the key inside the section's array): +// - "key" -> section "key" (section_kind::simple_key) +// - "a.b..." -> section "a.", remainder "b..." (section_kind::one_dot_wildcard) +// - "a.b.c..." -> section "a.b.", remainder "c..." (section_kind::two_dots_wildcard) +// - "predefined..." -> section = the matching predefined wildcard (section_kind::predefined_wildcard) +namespace kphp::confdata { + +/** + * @brief Classifies `section`; a would-be predefined wildcard that is not configured is reported + * as `section_kind::simple_key`. + */ +inline auto classify_section(std::string_view section, const predefined_wildcards& wildcards) noexcept -> section_kind { + const auto kind{classify_wildcard_form(section)}; + return kind != section_kind::predefined_wildcard || wildcards.contains(section) ? kind : section_kind::simple_key; +} + +enum class split_error : uint8_t { key_too_long, invalid_predefined_wildcard_length, not_a_two_dots_key }; + +/** + * @brief The decomposition of a confdata key as zero-copy views into the key. + * Instances are produced only by the `split_key*` factories, so every `key_views` + * is guaranteed to satisfy the protocol length bound (`int16_t`). + */ +struct key_views { + // the remainder of a key: absent for simple keys, int-normalized like a PHP array key otherwise + using remainder_type = std::variant; + +private: + section_kind m_section_kind; + std::string_view m_raw_key; + std::string_view m_section; + remainder_type m_remainder; + + key_views(section_kind section_kind, std::string_view raw_key, std::string_view section, remainder_type remainder) noexcept; + + friend auto split_key(std::string_view key) noexcept -> std::expected; + friend auto split_key(std::string_view key, const predefined_wildcards& wildcards) noexcept -> std::expected; + friend auto split_key_with_predefined_wildcard(std::string_view key, size_t wildcard_len) noexcept -> std::expected; + +public: + key_views() = delete; + + auto kind() const noexcept -> section_kind; + auto raw_key() const noexcept -> std::string_view; + auto section() const noexcept -> std::string_view; + auto remainder() const noexcept -> const remainder_type&; + + /** + * @return The one-dot duplicate of a two-dot key (`a.b.c...` -> section `a.`, remainder `b.c...`), + * or `split_error::not_a_two_dots_key`. + */ + auto reinterpret_two_dots_as_one_dot() const noexcept -> std::expected; +}; + +inline key_views::key_views(section_kind section_kind, std::string_view raw_key, std::string_view section, remainder_type remainder) noexcept + : m_section_kind{section_kind}, + m_raw_key{raw_key}, + m_section{section}, + m_remainder{remainder} {} + +inline auto key_views::kind() const noexcept -> section_kind { + return m_section_kind; +} + +inline auto key_views::raw_key() const noexcept -> std::string_view { + return m_raw_key; +} + +inline auto key_views::section() const noexcept -> std::string_view { + return m_section; +} + +inline auto key_views::remainder() const noexcept -> const remainder_type& { + return m_remainder; +} + +inline auto key_views::reinterpret_two_dots_as_one_dot() const noexcept -> std::expected { + if (m_section_kind != section_kind::two_dots_wildcard) { + return std::unexpected{split_error::not_a_two_dots_key}; + } + // a two-dot key always contains a dot, and the remainder after the first dot always contains another one, + // so the remainder is never numeric and needs no int-normalization + const auto first_dot{m_raw_key.find('.')}; + const auto remainder{m_raw_key.substr(first_dot + 1)}; + return key_views{section_kind::one_dot_wildcard, m_raw_key, m_raw_key.substr(0, first_dot + 1), remainder_type{remainder}}; +} + +// ================================================================================================ + +/** + * @brief Splits `key` into the section (up to the first/second dot) and the int-normalized remainder. + */ +auto split_key(std::string_view key) noexcept -> std::expected; + +/** + * @brief Splits `key` using the shortest matching predefined wildcard as the section, if any. + */ +auto split_key(std::string_view key, const predefined_wildcards& wildcards) noexcept -> std::expected; + +/** + * @brief Splits `key` using the explicitly given predefined wildcard length as the section. + */ +auto split_key_with_predefined_wildcard(std::string_view key, size_t wildcard_len) noexcept -> std::expected; + +// ================================================================================================ + +/** + * @brief Materializes validated key views into runtime handles (`string`/`mixed`) for storage lookups, + * allocation-free: the handles are placement-constructed into the internal stack buffers. + * Immovable, since the handles point into the object's own buffers. + */ +class key_handles : vk::not_copyable { // NOLINT(*member-init) + // Buffers precede the handles so that the handles are destroyed before the storage they refer to. + alignas(std::max_align_t) std::array::max() + 1> m_section_buffer; + alignas(std::max_align_t) std::array::max() + 1> m_remainder_buffer; + + string m_section; + mixed m_remainder; + +public: + explicit key_handles(const key_views& views) noexcept; // NOLINT(*member-init) + + auto section() const noexcept -> const string&; + + auto remainder() const noexcept -> const mixed&; + + /** + * @return A heap copy of the section; the internal section aliases the stack buffer so it must not escape the handles object. + */ + auto make_section_copy() const noexcept -> string; + + /** + * @return A heap copy of the remainder; the internal remainder aliases the stack buffer so it must not escape the handles object. + */ + auto make_remainder_copy() const noexcept -> mixed; +}; + +inline key_handles::key_handles(const key_views& views) noexcept { // NOLINT(*member-init) + m_section = views.section().empty() ? string{} + : string::make_const_string_on_memory(views.section().data(), static_cast(views.section().size()), + m_section_buffer.data(), m_section_buffer.size()); + m_remainder = std::visit(overloaded{ + [](std::monostate) noexcept -> mixed { return mixed{}; }, + [](int64_t remainder) noexcept -> mixed { return mixed{remainder}; }, + [this](std::string_view remainder) noexcept -> mixed { + return remainder.empty() + ? mixed{string{}} + : mixed{string::make_const_string_on_memory(remainder.data(), static_cast(remainder.size()), + m_remainder_buffer.data(), m_remainder_buffer.size())}; + }, + }, + views.remainder()); +} + +inline auto key_handles::section() const noexcept -> const string& { + return m_section; +} + +inline auto key_handles::remainder() const noexcept -> const mixed& { + return m_remainder; +} + +inline auto key_handles::make_section_copy() const noexcept -> string { + return m_section.copy_and_make_not_shared(); +} + +inline auto key_handles::make_remainder_copy() const noexcept -> mixed { + return m_remainder.is_string() ? mixed{m_remainder.as_string().copy_and_make_not_shared()} : m_remainder; +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/confdata-reader-lease.h b/runtime-light/stdlib/confdata/confdata-reader-lease.h new file mode 100644 index 0000000000..a67f0108e2 --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-reader-lease.h @@ -0,0 +1,75 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "runtime-light/stdlib/confdata/confdata-storage.h" + +namespace kphp::confdata { + +/** Fixed-size handshake sent when the component grants a reader sample lease. */ +class reader_lease final { + static constexpr uint64_t MAGIC{0x4b32'4344'4c45'4153}; // "K2CDLEAS" + static constexpr uint32_t VERSION{1}; + static constexpr size_t MAX_SHARED_MEMORY_NAME_SIZE{128}; + + /** Identifies this wire layout and rejects unrelated stream payloads. */ + uint64_t m_magic{MAGIC}; + /** Allows the handshake layout to evolve independently of storage layout. */ + uint32_t m_version{VERSION}; + /** Selects the immutable confdata generation pinned by the component. */ + storage::sample_id m_sample_id{storage::INVALID_SAMPLE_ID}; + /** Number of meaningful bytes in `m_shared_memory_name`. */ + uint32_t m_shared_memory_name_size{}; + /** Name passed to `k2_get_shared_memory`; it is not null-terminated. */ + std::array m_shared_memory_name{}; + +public: + reader_lease() noexcept = default; + + static auto create(std::string_view shared_memory_name, storage::sample_id sample_id) noexcept -> std::optional; + + auto is_valid() const noexcept -> bool; + auto sample_id() const noexcept -> storage::sample_id; + auto shared_memory_name() const noexcept -> std::string_view; +}; + +inline auto reader_lease::create(std::string_view shared_memory_name, storage::sample_id sample_id) noexcept -> std::optional { + if (shared_memory_name.empty() || shared_memory_name.size() > MAX_SHARED_MEMORY_NAME_SIZE || shared_memory_name.contains('\0') || + !storage::is_valid_sample_id(sample_id)) [[unlikely]] { + return std::nullopt; + } + + reader_lease lease{}; + lease.m_sample_id = sample_id; + lease.m_shared_memory_name_size = static_cast(shared_memory_name.size()); + std::ranges::copy(shared_memory_name, lease.m_shared_memory_name.begin()); + return lease; +} + +inline auto reader_lease::is_valid() const noexcept -> bool { + return m_magic == MAGIC && m_version == VERSION && storage::is_valid_sample_id(m_sample_id) && m_shared_memory_name_size != 0 && + m_shared_memory_name_size <= m_shared_memory_name.size() && !shared_memory_name().contains('\0'); +} + +inline auto reader_lease::sample_id() const noexcept -> storage::sample_id { + return m_sample_id; +} + +inline auto reader_lease::shared_memory_name() const noexcept -> std::string_view { + const auto size{std::min(static_cast(m_shared_memory_name_size), m_shared_memory_name.size())}; + return {m_shared_memory_name.data(), size}; +} + +static_assert(std::is_trivially_copyable_v); + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/confdata-state.cpp b/runtime-light/stdlib/confdata/confdata-state.cpp new file mode 100644 index 0000000000..090b6e511c --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-state.cpp @@ -0,0 +1,47 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#include "runtime-light/stdlib/confdata/confdata-state.h" + +#include +#include +#include + +#include "runtime-light/k2-platform/k2-api.h" +#include "runtime-light/stdlib/confdata/confdata-constants.h" +#include "runtime-light/stdlib/confdata/confdata-reader-lease.h" +#include "runtime-light/stdlib/diagnostics/logs.h" + +namespace kphp::confdata { + +auto instance_state::init() noexcept -> kphp::coro::task<> { + kphp::log::assertion(!is_initialized()); + kphp::log::assertion(!m_reader_lease.has_value()); + + auto lease_stream{kphp::component::stream::open(kphp::confdata::COMPONENT_LINK_ALIAS, k2::stream_kind::component)}; + if (!lease_stream) { + co_return kphp::log::warning("failed to open reader lease stream: error -> {}", lease_stream.error()); + } + + kphp::confdata::reader_lease lease{}; + const auto read{co_await lease_stream->read(std::as_writable_bytes(std::span{std::addressof(lease), 1}))}; + if (!read || *read != sizeof(lease) || !lease.is_valid()) [[unlikely]] { + co_return kphp::log::warning("failed to acquire a valid confdata reader lease"); + } + + const auto shared_memory{k2::get_shared_memory(lease.shared_memory_name())}; + if (!shared_memory) { + co_return kphp::log::warning("failed to get confdata shared memory: error -> {}", shared_memory.error()); + } + if (const auto opened{m_storage.open(*shared_memory)}; !opened) [[unlikely]] { + co_return kphp::log::warning("failed to open confdata shared memory: error -> {}", std::to_underlying(opened.error())); + } + + m_sample_id = lease.sample_id(); + m_reader_lease.emplace(std::move(*lease_stream)); + kphp::log::debug("confdata reader attached: name -> {}, sample -> {}, sections -> {}, mapped bytes -> {}", lease.shared_memory_name(), m_sample_id, + m_storage.values(m_sample_id).size(), shared_memory->size()); +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/confdata-state.h b/runtime-light/stdlib/confdata/confdata-state.h index 58c23776c3..f1b22e766c 100644 --- a/runtime-light/stdlib/confdata/confdata-state.h +++ b/runtime-light/stdlib/confdata/confdata-state.h @@ -5,28 +5,57 @@ #pragma once #include +#include #include "common/mixin/not_copyable.h" -#include "runtime-common/core/allocator/script-allocator.h" -#include "runtime-common/core/runtime-core.h" -#include "runtime-common/core/std/containers.h" +#include "runtime-light/coroutine/task.h" +#include "runtime-light/stdlib/confdata/confdata-storage.h" +#include "runtime-light/stdlib/confdata/predefined-wildcards.h" +#include "runtime-light/stdlib/diagnostics/logs.h" +#include "runtime-light/streams/stream.h" -class ConfdataInstanceState final : private vk::not_copyable { - using hasher_type = decltype([](const string& s) noexcept { return static_cast(s.hash()); }); +namespace kphp::confdata { - kphp::stl::unordered_map m_key_cache; - kphp::stl::unordered_map, kphp::memory::script_allocator, hasher_type> m_wildcard_cache; +class instance_state final : private vk::not_copyable { + kphp::confdata::storage m_storage{}; + std::optional m_reader_lease; + kphp::confdata::storage::sample_id m_sample_id{kphp::confdata::storage::INVALID_SAMPLE_ID}; public: - ConfdataInstanceState() noexcept = default; + instance_state() noexcept = default; - auto& key_cache() noexcept { - return m_key_cache; - } - - auto& wildcard_cache() noexcept { - return m_wildcard_cache; - } + auto init() noexcept -> kphp::coro::task<>; + auto release() noexcept -> void; + auto is_initialized() const noexcept -> bool; + auto values() const noexcept -> const kphp::confdata::storage::map_type&; + auto wildcards() const noexcept -> const kphp::confdata::predefined_wildcards&; - static ConfdataInstanceState& get() noexcept; + static auto get() noexcept -> instance_state&; }; + +inline auto instance_state::release() noexcept -> void { + if (!is_initialized()) { + return; + } + m_storage.close(); + m_sample_id = kphp::confdata::storage::INVALID_SAMPLE_ID; + // Closing the stream is the release signal; the component owns the reader + // count and also observes this close when K2 terminates an instance abruptly. + m_reader_lease.reset(); +} + +inline auto instance_state::is_initialized() const noexcept -> bool { + return m_sample_id != kphp::confdata::storage::INVALID_SAMPLE_ID; +} + +inline auto instance_state::values() const noexcept -> const kphp::confdata::storage::map_type& { + kphp::log::assertion(is_initialized()); + return m_storage.values(m_sample_id); +} + +inline auto instance_state::wildcards() const noexcept -> const kphp::confdata::predefined_wildcards& { + kphp::log::assertion(is_initialized()); + return m_storage.wildcards(); +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/confdata-storage.cpp b/runtime-light/stdlib/confdata/confdata-storage.cpp new file mode 100644 index 0000000000..d52a9cb324 --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-storage.cpp @@ -0,0 +1,721 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#include "runtime-light/stdlib/confdata/confdata-storage.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/php-functions.h" +#include "runtime-light/stdlib/confdata/confdata-keys.h" +#include "runtime-light/stdlib/diagnostics/logs.h" + +namespace { + +constexpr uint64_t STORAGE_MAGIC{0x4b32'434f'4e46'4441}; // "K2CONFDA" +constexpr uint32_t STORAGE_VERSION{1}; + +struct storage_layout final { + size_t m_pool_offset{}; + size_t m_total_size{}; +}; + +constexpr auto checked_add(size_t lhs, size_t rhs) noexcept -> std::expected { + if (lhs > std::numeric_limits::max() - rhs) [[unlikely]] { + return std::unexpected{kphp::confdata::storage_error::size_overflow}; + } + return lhs + rhs; +} + +constexpr auto checked_align_up(size_t size) noexcept -> std::expected { + constexpr auto alignment{kphp::confdata::storage::memory_alignment()}; + static_assert(std::has_single_bit(alignment)); + + const auto with_padding{checked_add(size, alignment - 1)}; + if (!with_padding) [[unlikely]] { + return std::unexpected{with_padding.error()}; + } + return *with_padding & ~(alignment - 1); +} + +constexpr auto calculate_layout(size_t header_size, size_t memory_limit) noexcept -> std::expected { + const auto pool_offset{checked_align_up(header_size)}; + if (!pool_offset) [[unlikely]] { + return std::unexpected{pool_offset.error()}; + } + const auto total_size{checked_add(*pool_offset, memory_limit)}; + if (!total_size) [[unlikely]] { + return std::unexpected{total_size.error()}; + } + return storage_layout{.m_pool_offset = *pool_offset, .m_total_size = *total_size}; +} + +auto is_same_allocation(const mixed& lhs, const mixed& rhs) noexcept -> bool { + if (lhs.get_type() != rhs.get_type()) { + return false; + } + if (lhs.is_string()) { + return lhs.as_string().c_str() == rhs.as_string().c_str(); + } + if (lhs.is_array()) { + return lhs.as_array().is_equal_inner_pointer(rhs.as_array()); + } + return false; +} + +auto mark_string_as_confdata(string& value) noexcept -> void { + kphp::log::assertion(!value.is_reference_counter(ExtraRefCnt::for_instance_cache)); + if (!value.is_reference_counter(ExtraRefCnt::for_confdata) && !value.is_reference_counter(ExtraRefCnt::for_global_const)) { + value.set_reference_counter_to(ExtraRefCnt::for_confdata); + } +} + +auto mark_value_as_confdata(mixed& value) noexcept -> void { + kphp::log::assertion(!value.is_reference_counter(ExtraRefCnt::for_instance_cache)); + if (value.is_reference_counter(ExtraRefCnt::for_global_const) || value.is_reference_counter(ExtraRefCnt::for_confdata)) { + return; + } + if (value.is_string()) { + mark_string_as_confdata(value.as_string()); + return; + } + if (!value.is_array()) { + return; + } + + auto& array{value.as_array()}; + array.set_reference_counter_to(ExtraRefCnt::for_confdata); + for (auto it{array.begin_no_mutate()}, last{array.end_no_mutate()}; it != last; ++it) { + if (it.is_string_key()) { + mark_string_as_confdata(it.get_string_key()); + } + mark_value_as_confdata(it.get_value()); + } +} + +auto recursively_destroy_value(mixed& value) noexcept -> void { + if (value.is_reference_counter(ExtraRefCnt::for_global_const)) { + return; + } + if (value.is_array()) { + auto& array{value.as_array()}; + for (auto it{array.begin_no_mutate()}, last{array.end_no_mutate()}; it != last; ++it) { + if (it.is_string_key() && !it.get_string_key().is_reference_counter(ExtraRefCnt::for_global_const)) { + it.get_string_key().force_destroy(ExtraRefCnt::for_confdata); + } + recursively_destroy_value(it.get_value()); + } + } else if (!value.is_string()) { + return; + } + value.force_destroy(ExtraRefCnt::for_confdata); +} + +} // namespace + +namespace kphp::confdata { + +struct storage::shared_state final { + /** Identifies a K2 confdata piece rather than unrelated shared memory. */ + uint64_t m_magic{STORAGE_MAGIC}; + /** Rejects pieces created for a different in-memory layout. */ + uint32_t m_version{STORAGE_VERSION}; + /** Logical initialized size; K2 may expose larger page-aligned capacity. */ + size_t m_total_size{}; + /** Byte offset at which allocator-managed payload memory begins. */ + size_t m_pool_offset{}; + /** Allocator shared by wildcard indexes, sample maps, and PHP values. */ + kphp::memory::pool_allocator m_allocator; + /** Immutable wildcard index shared by every sample in this piece. */ + predefined_wildcards m_wildcards; + /** Thirty immutable generations, matching legacy KPHP's backpressure bound. */ + std::array m_samples; + +private: + template + static auto make_samples_impl(resource_type& resource, std::index_sequence /*unused*/) noexcept -> std::array { + static_assert(sizeof...(indexes) == SAMPLE_COUNT); + return {((void)indexes, sample{resource})...}; + } + + static auto make_samples(resource_type& resource) noexcept -> std::array { + return make_samples_impl(resource, std::make_index_sequence{}); + } + +public: + explicit shared_state(std::span pool_memory) noexcept + : m_allocator{kphp::memory::pool_allocator::external_memory{}, pool_memory.data(), pool_memory.size(), 0}, + m_wildcards{m_allocator.get_memory_resource()}, + m_samples{make_samples(m_allocator.get_memory_resource())} {} +}; + +storage::retired_allocations::retired_allocations(resource_type& resource) noexcept + : m_detached_allocations{retired_list::allocator_type{resource}}, + m_owned_values{retired_list::allocator_type{resource}} {} + +auto storage::retired_allocations::empty() const noexcept -> bool { + return m_detached_allocations.empty() && m_owned_values.empty(); +} + +auto storage::retired_allocations::swap(retired_allocations& other) noexcept -> void { + m_detached_allocations.swap(other.m_detached_allocations); + m_owned_values.swap(other.m_owned_values); +} + +storage::sample::sample(resource_type& resource) noexcept + : m_values{map_type::allocator_type{resource}}, + m_retired_allocations{resource} {} + +auto storage::sync_size_hints::add_section(std::string_view section) noexcept -> void { + if (auto hint_it{m_hints.find(section)}; hint_it != m_hints.end()) { + ++hint_it->second; + return; + } + m_hints.emplace(hint_string{section.data(), section.size()}, 1); +} + +auto storage::sync_size_hints::add(std::string_view key) noexcept -> void { + kphp::log::assertion(!m_finished); + + const auto views{split_key(key)}; + if (!views) [[unlikely]] { + return; + } + + switch (views->kind()) { + case section_kind::simple_key: + case section_kind::predefined_wildcard: + return; + case section_kind::one_dot_wildcard: + return add_section(views->section()); + case section_kind::two_dots_wildcard: { + const auto one_dot_views{views->reinterpret_two_dots_as_one_dot()}; + kphp::log::assertion(one_dot_views.has_value()); + add_section(one_dot_views->section()); + return add_section(views->section()); + } + } +} + +auto storage::sync_size_hints::finish() noexcept -> void { + kphp::log::assertion(!m_finished); + for (auto hint_it{m_hints.begin()}; hint_it != m_hints.end();) { + if (hint_it->second <= 1) { + hint_it = m_hints.erase(hint_it); + } else { + ++hint_it; + } + } + m_finished = true; +} + +auto storage::sync_size_hints::section_size(std::string_view section) const noexcept -> size_t { + kphp::log::assertion(m_finished); + const auto hint_it{m_hints.find(section)}; + return hint_it != m_hints.end() ? hint_it->second : 0; +} + +storage::editor::editor(storage& owner, sample_id destination, bool copy_active_sample, sync_size_hints_ref size_hints) noexcept + : m_owner{std::addressof(owner)}, + m_destination{destination}, + m_sync_size_hints{size_hints}, + m_values{map_type::allocator_type{owner.resource()}}, + m_retired_allocations{owner.resource()} { + if (copy_active_sample) { + owner.with_storage_allocator([this, &owner] noexcept { m_values = owner.m_state->m_samples[owner.m_active_sample].m_values; }); + } +} + +storage::editor::editor(editor&& other) noexcept + : m_owner{std::exchange(other.m_owner, nullptr)}, + m_destination{std::exchange(other.m_destination, INVALID_SAMPLE_ID)}, + m_sync_size_hints{std::exchange(other.m_sync_size_hints, std::nullopt)}, + m_values{std::move(other.m_values)}, + m_retired_allocations{std::move(other.m_retired_allocations)}, + m_last_retired_value{std::move(other.m_last_retired_value)}, + m_changed{other.m_changed} {} + +storage::editor::~editor() { + cancel(); +} + +auto storage::editor::erase(std::string_view key) noexcept -> bool { + kphp::log::assertion(m_owner != nullptr); + bool erased{}; + m_owner->with_storage_allocator([this, key, &erased] noexcept { + erased = apply_erase(key); + m_last_retired_value.clear(); + }); + m_changed = erased || m_changed; + return erased; +} + +auto storage::editor::changed() const noexcept -> bool { + return m_changed; +} + +auto storage::editor::commit() noexcept -> void { + kphp::log::assertion(m_owner != nullptr); + m_owner->commit(*this); +} + +auto storage::editor::cancel() noexcept -> void { + if (m_owner != nullptr) { + m_owner->cancel(*this); + } +} + +auto storage::editor::apply_upsert(std::string_view key, const mixed& value) noexcept -> bool { + const auto implicit_views{split_key(key)}; + if (!implicit_views) [[unlikely]] { + return false; + } + + bool changed{}; + const bool has_predefined_wildcard{ + m_owner->m_state->m_wildcards.for_each_matching_wildcard(key, [this, key, &value, &changed](std::string_view wildcard) noexcept { + const auto views{split_key_with_predefined_wildcard(key, wildcard.size())}; + kphp::log::assertion(views.has_value()); + changed = upsert_one(*views, value) || changed; + })}; + + if (!has_predefined_wildcard || implicit_views->kind() != section_kind::simple_key) { + changed = upsert_one(*implicit_views, value) || changed; + if (implicit_views->kind() == section_kind::two_dots_wildcard) { + const auto one_dot_views{implicit_views->reinterpret_two_dots_as_one_dot()}; + kphp::log::assertion(one_dot_views.has_value()); + changed = upsert_one(*one_dot_views, value) || changed; + } + } + return changed; +} + +auto storage::editor::apply_erase(std::string_view key) noexcept -> bool { + const auto implicit_views{split_key(key)}; + if (!implicit_views) [[unlikely]] { + return false; + } + + bool erased{}; + const bool has_predefined_wildcard{m_owner->m_state->m_wildcards.for_each_matching_wildcard(key, [this, key, &erased](std::string_view wildcard) noexcept { + const auto views{split_key_with_predefined_wildcard(key, wildcard.size())}; + kphp::log::assertion(views.has_value()); + erased = erase_one(*views) || erased; + })}; + + if (!has_predefined_wildcard || implicit_views->kind() != section_kind::simple_key) { + erased = erase_one(*implicit_views) || erased; + if (implicit_views->kind() == section_kind::two_dots_wildcard) { + const auto one_dot_views{implicit_views->reinterpret_two_dots_as_one_dot()}; + kphp::log::assertion(one_dot_views.has_value()); + erased = erase_one(*one_dot_views) || erased; + } + } + return erased; +} + +auto storage::editor::upsert_one(const key_views& views, const mixed& value) noexcept -> bool { + key_handles handles{views}; + auto section_it{m_values.find(handles.section())}; + + if (section_it == m_values.end()) { + if (views.kind() == section_kind::simple_key) { + m_values.emplace(handles.make_section_copy(), value); + } else { + array entries{}; + if (m_sync_size_hints.has_value()) { + const auto size_hint{m_sync_size_hints->get().section_size(views.section())}; + if (size_hint != 0) { + kphp::log::assertion(size_hint <= static_cast(std::numeric_limits::max())); + entries = array{array_size{static_cast(size_hint), false}}; + } + } + entries.set_value(handles.make_remainder_copy(), value); + m_values.emplace(handles.make_section_copy(), mixed{std::move(entries)}); + } + return true; + } + + if (views.kind() == section_kind::simple_key) { + if (equals(section_it->second, value)) { + return false; + } + retire_value(section_it->second); + section_it->second = value; + return true; + } + + kphp::log::assertion(section_it->second.is_array()); + auto& entries{section_it->second.as_array()}; + const auto* previous{entries.find_value(handles.remainder())}; + if (previous != nullptr && equals(*previous, value)) { + return false; + } + + retire_for_shallow_destruction(mixed{entries}); + if (previous == nullptr) { + entries.set_value(handles.make_remainder_copy(), value); + } else { + retire_value(*previous); + entries.mutate_if_shared(); + auto entry_it{entries.find_no_mutate(handles.remainder())}; + kphp::log::assertion(entry_it != entries.end()); + entry_it.get_value() = value; + } + return true; +} + +auto storage::editor::erase_one(const key_views& views) noexcept -> bool { + key_handles handles{views}; + auto section_it{m_values.find(handles.section())}; + if (section_it == m_values.end()) { + return false; + } + + if (views.kind() == section_kind::simple_key) { + retire_value(section_it->second); + retire_for_shallow_destruction(mixed{section_it->first}); + m_values.erase(section_it); + return true; + } + + kphp::log::assertion(section_it->second.is_array()); + auto& entries{section_it->second.as_array()}; + if (!entries.has_key(handles.remainder())) { + return false; + } + + retire_for_shallow_destruction(mixed{entries}); + entries.mutate_if_shared(); + auto entry_it{entries.find_no_mutate(handles.remainder())}; + kphp::log::assertion(entry_it != entries.end()); + if (entry_it.is_string_key()) { + retire_for_recursive_destruction(mixed{entry_it.get_string_key()}); + } + retire_value(entry_it.get_value()); + entries.unset(handles.remainder()); + + if (entries.empty()) { + retire_for_shallow_destruction(mixed{section_it->first}); + m_values.erase(section_it); + } + return true; +} + +auto storage::editor::retire_value(const mixed& value) noexcept -> void { + if ((!value.is_string() && !value.is_array()) || + (!value.is_reference_counter(ExtraRefCnt::for_confdata) && !value.is_reference_counter(ExtraRefCnt::for_global_const))) { + return; + } + if (!m_last_retired_value.is_null()) { + kphp::log::assertion(is_same_allocation(m_last_retired_value, value)); + return; + } + retire_for_recursive_destruction(value); + m_last_retired_value = value; +} + +auto storage::editor::retire_for_shallow_destruction(const mixed& value) noexcept -> void { + if ((value.is_string() || value.is_array()) && value.is_reference_counter(ExtraRefCnt::for_confdata)) { + m_retired_allocations.m_detached_allocations.emplace_front(value); + } +} + +auto storage::editor::retire_for_recursive_destruction(const mixed& value) noexcept -> void { + if ((value.is_string() || value.is_array()) && value.is_reference_counter(ExtraRefCnt::for_confdata)) { + m_retired_allocations.m_owned_values.emplace_front(value); + } +} + +auto storage::memory_size(size_t memory_limit) noexcept -> std::expected { + static_assert(alignof(shared_state) <= memory_alignment()); + if (memory_limit == 0) [[unlikely]] { + return std::unexpected{storage_error::insufficient_buffer}; + } + const auto layout{calculate_layout(sizeof(shared_state), memory_limit)}; + if (!layout) [[unlikely]] { + return std::unexpected{layout.error()}; + } + if (memory_limit > memory_resource::memory_buffer_limit()) [[unlikely]] { + return std::unexpected{storage_error::memory_limit_exceeded}; + } + return layout->m_total_size; +} + +auto storage::is_valid_sample_id(sample_id id) noexcept -> bool { + return id < SAMPLE_COUNT; +} + +auto storage::init(std::span memory, size_t oom_handling_size) noexcept -> std::expected { + kphp::log::assertion(!is_initialized()); + if (reinterpret_cast(memory.data()) % alignof(shared_state) != 0) [[unlikely]] { + return std::unexpected{storage_error::misaligned_buffer}; + } + const auto layout{calculate_layout(sizeof(shared_state), 1)}; + if (!layout) [[unlikely]] { + return std::unexpected{layout.error()}; + } + if (memory.size() < layout->m_total_size) [[unlikely]] { + return std::unexpected{storage_error::insufficient_buffer}; + } + + const auto pool_memory{memory.subspan(layout->m_pool_offset)}; + if (pool_memory.size() > memory_resource::memory_buffer_limit()) [[unlikely]] { + return std::unexpected{storage_error::memory_limit_exceeded}; + } + if (oom_handling_size >= pool_memory.size()) [[unlikely]] { + return std::unexpected{storage_error::invalid_oom_handling_size}; + } + + m_memory = memory; + m_state = std::construct_at(reinterpret_cast(m_memory.data()), pool_memory); + m_active_sample = 0; + m_state->m_total_size = memory.size(); + m_state->m_pool_offset = layout->m_pool_offset; + m_oom_threshold = pool_memory.size() - oom_handling_size; + return {}; +} + +auto storage::open(std::span memory) noexcept -> std::expected { + kphp::log::assertion(!is_initialized()); + if (reinterpret_cast(memory.data()) % alignof(shared_state) != 0) [[unlikely]] { + return std::unexpected{storage_error::misaligned_buffer}; + } + if (memory.size() <= sizeof(shared_state)) [[unlikely]] { + return std::unexpected{storage_error::insufficient_buffer}; + } + + const auto* state{std::launder(reinterpret_cast(memory.data()))}; + const auto layout{calculate_layout(sizeof(shared_state), 1)}; + if (!layout || state->m_magic != STORAGE_MAGIC || state->m_version != STORAGE_VERSION || state->m_total_size > memory.size() || + state->m_pool_offset != layout->m_pool_offset || state->m_pool_offset >= state->m_total_size) [[unlikely]] { + return std::unexpected{storage_error::invalid_storage}; + } + + // K2 may report page-aligned physical capacity. Only this logical prefix + // was initialized by the writer and belongs to the storage. + m_memory = {const_cast(memory.data()), state->m_total_size}; + m_state = const_cast(state); + m_has_committed_sample = true; + return {}; +} + +auto storage::close() noexcept -> void { + kphp::log::assertion(is_initialized()); + // Reader-side views never start updates and therefore leave this writer-only + // invariant false. For a writer, it prevents detaching while an editor lives. + kphp::log::assertion(!m_update_in_progress); + m_state = nullptr; + m_memory = {}; + m_active_sample = INVALID_SAMPLE_ID; + m_has_committed_sample = false; + m_oom_threshold = 0; +} + +auto storage::initialize_wildcards(std::span wildcards) noexcept -> std::expected { + kphp::log::assertion(is_initialized()); + kphp::log::assertion(!m_has_committed_sample); + std::expected result{}; + with_storage_allocator([this, wildcards, &result] noexcept { result = m_state->m_wildcards.initialize(wildcards); }); + return result; +} + +auto storage::memory_usage() const noexcept -> storage_memory_usage { + kphp::log::assertion(is_initialized()); + kphp::log::assertion(m_oom_threshold != 0); + const auto& memory_stats{m_state->m_allocator.get_memory_resource().get_memory_stats()}; + return {.m_used = memory_stats.real_memory_used, .m_oom_threshold = m_oom_threshold, .m_capacity = memory_stats.memory_limit}; +} + +auto storage::is_oom_threshold_reached() const noexcept -> bool { + const auto usage{memory_usage()}; + return usage.m_used >= usage.m_oom_threshold; +} + +auto storage::wildcards() const noexcept -> const predefined_wildcards& { + kphp::log::assertion(is_initialized()); + return m_state->m_wildcards; +} + +auto storage::acquire_active_sample() noexcept -> sample_id { + kphp::log::assertion(is_initialized()); + kphp::log::assertion(is_valid_sample_id(m_active_sample)); + const auto id{m_active_sample}; + ++m_state->m_samples[id].m_readers; + return id; +} + +auto storage::release_sample(sample_id id) noexcept -> void { + kphp::log::assertion(is_initialized()); + kphp::log::assertion(is_valid_sample_id(id)); + auto& readers{m_state->m_samples[id].m_readers}; + kphp::log::assertion(readers != 0); + --readers; +} + +auto storage::values(sample_id id) const noexcept -> const map_type& { + kphp::log::assertion(is_initialized()); + kphp::log::assertion(is_valid_sample_id(id)); + return m_state->m_samples[id].m_values; +} + +auto storage::start_sync() noexcept -> editor { + kphp::log::assertion(is_initialized()); + kphp::log::assertion(is_valid_sample_id(m_active_sample)); + kphp::log::assertion(!m_has_committed_sample); + auto update{begin_update(false)}; + kphp::log::assertion(update.has_value()); + return std::move(*update); +} + +auto storage::start_sync(const sync_size_hints& size_hints) noexcept -> editor { + kphp::log::assertion(size_hints.m_finished); + kphp::log::assertion(is_initialized()); + kphp::log::assertion(is_valid_sample_id(m_active_sample)); + kphp::log::assertion(!m_has_committed_sample); + auto update{begin_update(false, std::cref(size_hints))}; + kphp::log::assertion(update.has_value()); + return std::move(*update); +} + +auto storage::start_update() noexcept -> std::optional { + kphp::log::assertion(is_initialized()); + kphp::log::assertion(is_valid_sample_id(m_active_sample)); + kphp::log::assertion(m_has_committed_sample); + return begin_update(true); +} + +auto storage::allocator() noexcept -> kphp::memory::pool_allocator& { + kphp::log::assertion(is_initialized()); + return m_state->m_allocator; +} + +auto storage::resource() noexcept -> resource_type& { + return allocator().get_memory_resource(); +} + +auto storage::begin_update(bool copy_active_sample, sync_size_hints_ref size_hints) noexcept -> std::optional { + kphp::log::assertion(!m_update_in_progress); + kphp::log::assertion(is_valid_sample_id(m_active_sample)); + reclaim_retired_samples(); + + if (copy_active_sample) { + constexpr auto max_node_size{map_type::allocator_type::max_value_type_size()}; + const auto active_size{m_state->m_samples[m_active_sample].m_values.size()}; + if (active_size > std::numeric_limits::max() / max_node_size) [[unlikely]] { + return std::nullopt; + } + const auto required_size{active_size * max_node_size}; + const auto usage{memory_usage()}; + if (usage.m_used >= usage.m_oom_threshold || required_size >= usage.m_oom_threshold - usage.m_used || !resource().is_enough_memory_for(required_size)) + [[unlikely]] { + return std::nullopt; + } + } + + const auto destination{static_cast((m_active_sample + 1) % SAMPLE_COUNT)}; + const auto& sample{m_state->m_samples[destination]}; + if (sample.m_readers != 0 || sample.m_retired) { + return std::nullopt; + } + + kphp::log::assertion(sample.m_values.empty()); + kphp::log::assertion(sample.m_retired_allocations.empty()); + m_update_in_progress = true; + return editor{*this, destination, copy_active_sample, size_hints}; +} + +auto storage::commit(editor& update) noexcept -> void { + kphp::log::assertion(m_update_in_progress); + kphp::log::assertion(update.m_owner == this); + kphp::log::assertion(is_valid_sample_id(update.m_destination)); + kphp::log::assertion(update.m_last_retired_value.is_null()); + + with_storage_allocator([this, &update] noexcept { + for (auto& [section, value] : update.m_values) { + // The map key is const, but a copied handle updates the shared string header. + string mutable_section{section}; + mark_string_as_confdata(mutable_section); + mark_value_as_confdata(value); + } + + auto& destination{m_state->m_samples[update.m_destination]}; + kphp::log::assertion(destination.m_readers == 0); + kphp::log::assertion(!destination.m_retired); + kphp::log::assertion(destination.m_values.empty()); + kphp::log::assertion(destination.m_retired_allocations.empty()); + destination.m_values = std::move(update.m_values); + + auto& previous{m_state->m_samples[m_active_sample]}; + kphp::log::assertion(previous.m_retired_allocations.empty()); + previous.m_retired_allocations.swap(update.m_retired_allocations); + previous.m_retired = true; + m_active_sample = update.m_destination; + }); + + update.m_owner = nullptr; + update.m_destination = INVALID_SAMPLE_ID; + m_update_in_progress = false; + m_has_committed_sample = true; +} + +auto storage::cancel(editor& update) noexcept -> void { + kphp::log::assertion(m_update_in_progress); + kphp::log::assertion(update.m_owner == this); + with_storage_allocator([&update] noexcept { + update.m_last_retired_value.clear(); + update.m_values.clear(); + update.m_retired_allocations.m_detached_allocations.clear(); + update.m_retired_allocations.m_owned_values.clear(); + }); + update.m_owner = nullptr; + update.m_destination = INVALID_SAMPLE_ID; + m_update_in_progress = false; +} + +auto storage::reclaim_retired_samples() noexcept -> void { + kphp::log::assertion(is_valid_sample_id(m_active_sample)); + with_storage_allocator([this] noexcept { + const auto active{m_active_sample}; + for (auto id{static_cast((active + 1) % SAMPLE_COUNT)}; id != active; id = static_cast((id + 1) % SAMPLE_COUNT)) { + const auto& sample{m_state->m_samples[id]}; + if (!sample.m_retired) { + continue; + } + // Neighboring generations can share payloads. Stop at the oldest pinned + // sample rather than reclaiming a newer sample out of order. + if (sample.m_readers != 0) { + break; + } + reclaim_sample(id); + } + }); +} + +auto storage::reclaim_sample(sample_id id) noexcept -> void { + auto& sample{m_state->m_samples[id]}; + sample.m_values.clear(); + + // Destroy detached parents first. Their element destructors are no-ops for + // `for_confdata` handles, after which recursively owned roots are safe. + while (!sample.m_retired_allocations.m_detached_allocations.empty()) { + sample.m_retired_allocations.m_detached_allocations.front().force_destroy(ExtraRefCnt::for_confdata); + sample.m_retired_allocations.m_detached_allocations.pop_front(); + } + while (!sample.m_retired_allocations.m_owned_values.empty()) { + recursively_destroy_value(sample.m_retired_allocations.m_owned_values.front()); + sample.m_retired_allocations.m_owned_values.pop_front(); + } + sample.m_retired = false; +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/confdata-storage.h b/runtime-light/stdlib/confdata/confdata-storage.h new file mode 100644 index 0000000000..21fae4c198 --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-storage.h @@ -0,0 +1,305 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/mixin/not_copyable.h" +#include "runtime-common/core/allocator/pool-allocator.h" +#include "runtime-common/core/allocator/runtime-allocator.h" +#include "runtime-common/core/allocator/script-allocator.h" +#include "runtime-common/core/memory-resource/resource_allocator.h" +#include "runtime-common/core/memory-resource/unsynchronized_pool_resource.h" +#include "runtime-common/core/runtime-core.h" +#include "runtime-common/core/std/containers.h" +#include "runtime-light/stdlib/confdata/predefined-wildcards.h" +#include "runtime-light/stdlib/diagnostics/logs.h" + +namespace kphp::confdata { + +enum class storage_error : uint8_t { misaligned_buffer, insufficient_buffer, size_overflow, memory_limit_exceeded, invalid_oom_handling_size, invalid_storage }; + +struct storage_memory_usage final { + /** Allocator frontier, including fragmentation that cannot be reused directly. */ + size_t m_used{}; + /** Usage at which the writer must rotate to a freshly synchronized piece. */ + size_t m_oom_threshold{}; + /** Complete allocator capacity, including OOM-handling headroom. */ + size_t m_capacity{}; +}; + +struct key_views; + +/** + * A non-owning view of one confdata shared-memory piece. + * + * A writer initializes the piece, builds its initial sync sample, and + * then publishes incremental samples through a 30-slot ring. Readers open the + * same piece and address the immutable sample named by their reader lease. + * + * The shared-memory allocation owns every object reachable through this view. + * Releasing the whole piece therefore requires no object-by-object teardown. + */ +class storage final : private vk::not_copyable { + // === TYPES ===================================================================================== + using resource_type = memory_resource::unsynchronized_pool_resource; + using retired_list = memory_resource::stl::forward_list; + +public: + using map_type = memory_resource::stl::map; + /** Opaque sample token exchanged by the confdata component's reader-lease protocol. */ + using sample_id = uint32_t; + + class sync_size_hints; + class editor; + +private: + using sync_size_hints_ref = std::optional>; + + struct retired_allocations final { + /** Detached arrays and keys whose child allocations remain owned elsewhere. */ + retired_list m_detached_allocations; + /** Logical values and nested keys that this generation owned recursively. */ + retired_list m_owned_values; + + explicit retired_allocations(resource_type& resource) noexcept; + + auto empty() const noexcept -> bool; + auto swap(retired_allocations& other) noexcept -> void; + }; + + struct sample final { + /** Number of connected readers whose lease names this sample. */ + size_t m_readers{}; + /** Whether this sample was superseded and awaits ordered reclamation. */ + bool m_retired{}; + /** Immutable confdata representation visible to readers of this sample. */ + map_type m_values; + /** Allocations detached while the following sample was being constructed. */ + retired_allocations m_retired_allocations; + + explicit sample(resource_type& resource) noexcept; + }; + + struct shared_state; + + friend class editor; + + // === MEMBERS ================================================================================== +public: + static constexpr size_t SAMPLE_COUNT{30}; + static constexpr sample_id INVALID_SAMPLE_ID{std::numeric_limits::max()}; + +private: + // Common local view state used in both writer and reader roles. + /** Header constructed at the beginning of the shared-memory piece. */ + shared_state* m_state{}; + /** Complete logical extent of the shared-memory piece. */ + std::span m_memory; + + // Writer-local state. A reader-side view leaves these members at their defaults. + /** Writer-local current sample; readers receive their sample ID in the lease. */ + sample_id m_active_sample{INVALID_SAMPLE_ID}; + /** Enforces the single-writer, single-unpublished-update invariant. */ + bool m_update_in_progress{}; + /** Distinguishes a fresh sync piece from an incrementally updated one. */ + bool m_has_committed_sample{}; + /** Writer-local usage boundary that leaves the configured recovery headroom. */ + size_t m_oom_threshold{}; + + // === METHODS ================================================================================== +public: + // Common layout and view API. + static constexpr auto memory_alignment() noexcept -> size_t; + static auto memory_size(size_t memory_limit) noexcept -> std::expected; + static auto is_valid_sample_id(sample_id id) noexcept -> bool; + + auto is_initialized() const noexcept -> bool; + /** Detaches this local view without modifying the shared-memory piece. */ + auto close() noexcept -> void; + auto wildcards() const noexcept -> const predefined_wildcards&; + + // Writer-side API. + /** + * Constructs a new writer-side storage in `memory`. + * + * `oom_handling_size` remains allocatable so an in-flight operation can + * finish safely, but reaching that final part of the pool asks the writer to + * discard the unpublished update and rotate to a new piece. + */ + auto init(std::span memory, size_t oom_handling_size = 0) noexcept -> std::expected; + /** Builds the immutable wildcard index owned by this shared-memory piece. */ + auto initialize_wildcards(std::span wildcards) noexcept -> std::expected; + auto memory() const noexcept -> std::span; + auto memory_usage() const noexcept -> storage_memory_usage; + auto is_oom_threshold_reached() const noexcept -> bool; + /** Pins the current sample for a newly connected reader. */ + auto acquire_active_sample() noexcept -> sample_id; + /** Releases the sample when that reader disconnects. */ + auto release_sample(sample_id id) noexcept -> void; + /** Starts the initial empty sample used by a sync on a fresh piece. */ + auto start_sync() noexcept -> editor; + /** Starts a sync whose wildcard arrays are created using a completed snapshot sizing pass. */ + auto start_sync(const sync_size_hints& size_hints) noexcept -> editor; + /** Starts an incremental update by copying the current immutable map. */ + auto start_update() noexcept -> std::optional; + + // Reader-side API. + /** Opens an initialized reader-side storage. */ + auto open(std::span memory) noexcept -> std::expected; + auto values(sample_id id) const noexcept -> const map_type&; + +private: + template + requires std::same_as, void> && std::is_nothrow_invocable_v + auto with_storage_allocator(callback_type&& callback) noexcept -> void; + + auto allocator() noexcept -> kphp::memory::pool_allocator&; + auto resource() noexcept -> resource_type&; + auto begin_update(bool copy_active_sample, sync_size_hints_ref size_hints = {}) noexcept -> std::optional; + auto commit(editor& update) noexcept -> void; + auto cancel(editor& update) noexcept -> void; + auto reclaim_retired_samples() noexcept -> void; + auto reclaim_sample(sample_id id) noexcept -> void; +}; + +inline constexpr auto storage::memory_alignment() noexcept -> size_t { + return alignof(std::max_align_t); +} + +template +requires std::same_as, void> && std::is_nothrow_invocable_v +auto storage::with_storage_allocator(callback_type&& callback) noexcept -> void { + RuntimeAllocator::get().with_allocator(allocator(), std::forward(callback)); +} + +inline auto storage::is_initialized() const noexcept -> bool { + return m_state != nullptr; +} + +inline auto storage::memory() const noexcept -> std::span { + return m_memory; +} + +/** + * Compact wildcard-section capacity bounds collected during a count-only snapshot pass. + * + * Snapshot events may arrive in any order, so counts are accumulated in an ordered + * map. The map owns one key per wildcard section rather than one key per event, and + * its transparent comparator avoids allocating temporary strings during lookups. + * Repeated upserts can overestimate a final cardinality, but never cause replay to + * outgrow the reserved capacity. + */ +class storage::sync_size_hints final : private vk::not_copyable { + using hint_string = kphp::stl::string; + + struct transparent_comparator final { + using is_transparent = void; + + auto operator()(std::string_view lhs, std::string_view rhs) const noexcept -> bool { + return lhs < rhs; + } + }; + + using hint_map = kphp::stl::map; + + hint_map m_hints; + bool m_finished{}; + + auto add_section(std::string_view section) noexcept -> void; + auto section_size(std::string_view section) const noexcept -> size_t; + + friend class storage; + friend class editor; + +public: + /** Accounts one non-deleted snapshot key. */ + auto add(std::string_view key) noexcept -> void; + /** Completes the pass and prepares hints for lookups by a sync editor. */ + auto finish() noexcept -> void; +}; + +/** + * The unpublished working copy of the next sample. + * + * Its map and retirement lists allocate from the owning shared-memory piece, + * while the editor object itself remains local to the writer. Destruction + * rolls the update back unless `commit()` has published it. + */ +class storage::editor final { + friend class storage; + + /** Nullable non-owning owner; null after this editor is moved, committed, or cancelled. */ + storage* m_owner{}; + /** Ring slot reserved for this unpublished working copy. */ + sample_id m_destination{INVALID_SAMPLE_ID}; + /** Optional instance-local sizing metadata that outlives this clean-sync editor. */ + sync_size_hints_ref m_sync_size_hints; + /** Complete map that will become the destination sample at commit. */ + map_type m_values; + /** Allocations removed from the current sample while building this map. */ + retired_allocations m_retired_allocations; + /** Deduplicates the same logical value stored in multiple wildcard sections. */ + mixed m_last_retired_value; + bool m_changed{}; + + editor(storage& owner, sample_id destination, bool copy_active_sample, sync_size_hints_ref size_hints) noexcept; + + auto apply_upsert(std::string_view key, const mixed& value) noexcept -> bool; + auto apply_erase(std::string_view key) noexcept -> bool; + auto upsert_one(const key_views& views, const mixed& value) noexcept -> bool; + auto erase_one(const key_views& views) noexcept -> bool; + auto retire_value(const mixed& value) noexcept -> void; + auto retire_for_shallow_destruction(const mixed& value) noexcept -> void; + auto retire_for_recursive_destruction(const mixed& value) noexcept -> void; + +public: + editor(editor&& other) noexcept; + editor(const editor&) = delete; + auto operator=(const editor& other) -> editor& = delete; + auto operator=(editor&& other) -> editor& = delete; + ~editor(); + + /** + * Constructs `value_factory()` under this piece's shared allocator and + * applies the resulting value to every denormalized representation of `key`. + */ + template + requires std::same_as, mixed> && std::is_nothrow_invocable_v + auto upsert(std::string_view key, value_factory_type&& value_factory) noexcept -> bool; + /** Applies one deletion to every denormalized representation of `key`. */ + auto erase(std::string_view key) noexcept -> bool; + auto changed() const noexcept -> bool; + /** Atomically publishes this working copy as the active sample. */ + auto commit() noexcept -> void; + /** Discards this working copy. Calling this more than once is harmless. */ + auto cancel() noexcept -> void; +}; + +template +requires std::same_as, mixed> && std::is_nothrow_invocable_v +auto storage::editor::upsert(std::string_view key, value_factory_type&& value_factory) noexcept -> bool { + kphp::log::assertion(m_owner != nullptr); + bool changed{}; + m_owner->with_storage_allocator([this, key, &value_factory, &changed] noexcept { + const mixed value{std::invoke(std::forward(value_factory))}; + changed = apply_upsert(key, value); + m_last_retired_value.clear(); + }); + m_changed = changed || m_changed; + return changed; +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/predefined-wildcards.cpp b/runtime-light/stdlib/confdata/predefined-wildcards.cpp new file mode 100644 index 0000000000..5a2ad73ea6 --- /dev/null +++ b/runtime-light/stdlib/confdata/predefined-wildcards.cpp @@ -0,0 +1,100 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#include "runtime-light/stdlib/confdata/predefined-wildcards.h" + +#include +#include +#include +#include +#include +#include + +namespace kphp::confdata { + +auto predefined_wildcards::shortest_matching_wildcard(std::string_view key) const noexcept -> std::optional { + const auto candidates{find_matching_candidates(key)}; + for (const auto& wildcard : candidates.wildcards) { + if (candidates.key_tail.starts_with(wildcard.substr(m_shortest_wildcard_size))) { + return wildcard; + } + } + return std::nullopt; +} + +auto predefined_wildcards::is_top_level_wildcard(std::string_view wildcard) const noexcept -> bool { + const auto shortest{shortest_matching_wildcard(wildcard)}; + return shortest.has_value() && *shortest == wildcard; +} + +auto predefined_wildcards::has_matching_wildcard(std::string_view key) const noexcept -> bool { + return shortest_matching_wildcard(key).has_value(); +} + +auto predefined_wildcards::initialize(std::span wildcards) noexcept -> std::expected { + if (m_initialized) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::already_initialized}; + } + + std::string_view previous{}; + bool first{true}; + for (const auto& wildcard : wildcards) { + if (const auto validated{validate_predefined_wildcard(wildcard)}; !validated) [[unlikely]] { + return std::unexpected{validated.error()}; + } + if (!first && previous >= wildcard) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::non_canonical_wildcards}; + } + previous = wildcard; + first = false; + } + + m_wildcards.reserve(wildcards.size()); + for (const auto& wildcard : wildcards) { + const auto [it, inserted]{m_wildcards.emplace(wildcard, wildcard_string::allocator_type{m_resource})}; + if (!inserted) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::internal}; + } + + const std::string_view stored{*it}; + if (m_shortest_wildcard_size == 0 || stored.size() < m_shortest_wildcard_size) { + m_shortest_wildcard_size = stored.size(); + } + } + + m_groups.reserve(m_wildcards.size()); + for (const auto& wildcard_string : m_wildcards) { + const std::string_view wildcard{wildcard_string}; + const auto group_it{m_groups.try_emplace(wildcard.substr(0, m_shortest_wildcard_size), wildcard_group::allocator_type{m_resource}).first}; + auto& group{group_it->second}; + group.emplace_back(wildcard); + } + + for (auto& [_, group] : m_groups) { + std::ranges::sort(group); + for (size_t i{}; i < group.size(); ++i) { + size_t matches{}; + for (size_t j{}; j <= i; ++j) { + matches += static_cast(group[i].starts_with(group[j])); + } + m_max_matches_per_key = std::max(m_max_matches_per_key, matches); + } + } + + m_initialized = true; + return {}; +} + +auto predefined_wildcards::find_matching_candidates(std::string_view key) const noexcept -> matching_candidates { + if (m_groups.empty() || key.size() < m_shortest_wildcard_size) { + return {}; + } + const auto group_it{m_groups.find(key.substr(0, m_shortest_wildcard_size))}; + if (group_it == m_groups.end()) { + return {}; + } + return {.wildcards = group_it->second, .key_tail = key.substr(m_shortest_wildcard_size)}; +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/predefined-wildcards.h b/runtime-light/stdlib/confdata/predefined-wildcards.h new file mode 100644 index 0000000000..1b38ba403b --- /dev/null +++ b/runtime-light/stdlib/confdata/predefined-wildcards.h @@ -0,0 +1,191 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/mixin/not_copyable.h" +#include "runtime-common/core/memory-resource/resource_allocator.h" +#include "runtime-common/core/memory-resource/unsynchronized_pool_resource.h" +#include "runtime-light/stdlib/confdata/wildcard-kind.h" + +namespace kphp::confdata { + +inline constexpr auto MAX_KEY_LENGTH{static_cast(std::numeric_limits::max())}; + +enum class predefined_wildcards_error : uint8_t { + empty_wildcard, + wildcard_too_long, + reserved_wildcard, + non_canonical_wildcards, + already_initialized, + internal, +}; + +inline auto validate_predefined_wildcard(std::string_view wildcard) noexcept -> std::expected { + if (wildcard.empty()) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::empty_wildcard}; + } + if (wildcard.size() > MAX_KEY_LENGTH) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::wildcard_too_long}; + } + if (classify_wildcard_form(wildcard) != section_kind::predefined_wildcard) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::reserved_wildcard}; + } + return {}; +} + +class storage; + +/** + * An immutable index of configured predefined wildcards. + * + * The owning strings and both lookup indexes retain the storage resource in + * their allocators, so their allocation domain does not depend on whichever + * script resource happens to be installed by the caller. + */ +class predefined_wildcards final : private vk::not_copyable { + using resource_type = memory_resource::unsynchronized_pool_resource; + using wildcard_string = memory_resource::stl::string; + + struct transparent_string_hash final { + using is_transparent = void; + + auto operator()(std::string_view value) const noexcept -> size_t { + return std::hash{}(value); + } + }; + + struct transparent_string_equal final { + using is_transparent = void; + + auto operator()(std::string_view lhs, std::string_view rhs) const noexcept -> bool { + return lhs == rhs; + } + }; + + using wildcard_set = memory_resource::stl::unordered_set; + using wildcard_group = memory_resource::stl::vector; + using wildcard_groups = + memory_resource::stl::unordered_map; + + /** Candidates from one shortest-prefix group and the unmatched part of the queried key. */ + struct matching_candidates final { + std::span wildcards; + std::string_view key_tail; + }; + + resource_type& m_resource; + // Owns each complete wildcard exactly once. References remain stable across + // unordered-set rehashes and the set is never mutated after initialization. + wildcard_set m_wildcards; + // Maps a shortest-length prefix to sorted views into `m_wildcards`. + wildcard_groups m_groups; + size_t m_shortest_wildcard_size{}; + size_t m_max_matches_per_key{}; + bool m_initialized{}; + +public: + explicit predefined_wildcards(resource_type& resource) noexcept; + + /** + * Invokes `f(wildcard)` for every configured wildcard that is a prefix of + * `key`, in ascending length order. + * + * @return True if at least one wildcard matched. + */ + template F> + auto for_each_matching_wildcard(std::string_view key, const F& f) const noexcept -> bool; + + /** @return The shortest configured wildcard that is a prefix of `key`, if any. */ + auto shortest_matching_wildcard(std::string_view key) const noexcept -> std::optional; + + /** @return The exact maximum number of configured wildcards that can match one key. */ + auto max_matches_per_key() const noexcept -> size_t; + + /** @return True if `wildcard` is configured. */ + auto contains(std::string_view wildcard) const noexcept -> bool; + + /** @return True if `wildcard` is configured and has no shorter configured wildcard prefix. */ + auto is_top_level_wildcard(std::string_view wildcard) const noexcept -> bool; + + /** @return True if at least one configured wildcard is a prefix of `key`. */ + auto has_matching_wildcard(std::string_view key) const noexcept -> bool; + +private: + /** Initializes the index from sorted, unique wildcards under the storage resource. */ + auto initialize(std::span wildcards) noexcept -> std::expected; + + auto find_matching_candidates(std::string_view key) const noexcept -> matching_candidates; + + friend class storage; +}; + +inline predefined_wildcards::predefined_wildcards(resource_type& resource) noexcept + : m_resource{resource}, + m_wildcards{wildcard_set::allocator_type{resource}}, + m_groups{wildcard_groups::allocator_type{resource}} {} + +template F> +auto predefined_wildcards::for_each_matching_wildcard(std::string_view key, const F& f) const noexcept -> bool { + const auto candidates{find_matching_candidates(key)}; + bool matched{}; + for (const auto& wildcard : candidates.wildcards) { + const auto wildcard_tail{wildcard.substr(m_shortest_wildcard_size)}; + if (candidates.key_tail.starts_with(wildcard_tail)) { + std::invoke(f, wildcard); + matched = true; + } + } + return matched; +} + +inline auto predefined_wildcards::max_matches_per_key() const noexcept -> size_t { + return m_max_matches_per_key; +} + +inline auto predefined_wildcards::contains(std::string_view wildcard) const noexcept -> bool { + return m_wildcards.contains(wildcard); +} + +} // namespace kphp::confdata + +template<> +struct std::formatter { + template + constexpr auto parse(ParseContext& ctx) const noexcept { + return ctx.begin(); + } + + template + auto format(kphp::confdata::predefined_wildcards_error error, FmtContext& ctx) const noexcept { + using kphp::confdata::predefined_wildcards_error; + + switch (error) { + case predefined_wildcards_error::empty_wildcard: + return std::format_to(ctx.out(), "empty wildcard"); + case predefined_wildcards_error::wildcard_too_long: + return std::format_to(ctx.out(), "wildcard is longer than the confdata key protocol limit"); + case predefined_wildcards_error::reserved_wildcard: + return std::format_to(ctx.out(), "wildcard uses the implicit one-dot or two-dot form"); + case predefined_wildcards_error::non_canonical_wildcards: + return std::format_to(ctx.out(), "wildcards are not sorted and unique"); + case predefined_wildcards_error::already_initialized: + return std::format_to(ctx.out(), "wildcards are already initialized"); + case predefined_wildcards_error::internal: + return std::format_to(ctx.out(), "unexpected internal error"); + } + return std::format_to(ctx.out(), "unknown wildcard error"); + } +}; diff --git a/runtime-light/stdlib/confdata/wildcard-kind.h b/runtime-light/stdlib/confdata/wildcard-kind.h new file mode 100644 index 0000000000..bfe24d5e8e --- /dev/null +++ b/runtime-light/stdlib/confdata/wildcard-kind.h @@ -0,0 +1,41 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include + +namespace kphp::confdata { + +enum class section_kind : uint8_t { simple_key, one_dot_wildcard, two_dots_wildcard, predefined_wildcard }; + +/** + * @brief Classifies the syntactic form of a wildcard section. + * + * Only trailing-dot forms with exactly one or two dots are implicit sections. All other forms are + * predefined-wildcard candidates and still require validation and presence in the configured index. + */ +inline auto classify_wildcard_form(std::string_view wildcard) noexcept -> section_kind { + size_t dots{}; + if (!wildcard.empty() && wildcard.back() == '.') { + for (const char c : wildcard) { + dots += static_cast(c == '.'); + if (dots > 2) { + break; + } + } + } + switch (dots) { + case 1: + return section_kind::one_dot_wildcard; + case 2: + return section_kind::two_dots_wildcard; + default: + return section_kind::predefined_wildcard; + } +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/stdlib.cmake b/runtime-light/stdlib/stdlib.cmake index 8776da4dc4..81fc965542 100644 --- a/runtime-light/stdlib/stdlib.cmake +++ b/runtime-light/stdlib/stdlib.cmake @@ -1,7 +1,11 @@ prepend( RUNTIME_LIGHT_STDLIB_SRC stdlib/ + confdata/confdata-state.cpp + confdata/confdata-storage.cpp + confdata/confdata-keys.cpp confdata/confdata-functions.cpp + confdata/predefined-wildcards.cpp crypto/crypto-functions.cpp diagnostics/backtrace.cpp diagnostics/php-assert.cpp diff --git a/runtime/runtime-builtin-stats.h b/runtime/runtime-builtin-stats.h index e8a0306a38..a5d351b10e 100644 --- a/runtime/runtime-builtin-stats.h +++ b/runtime/runtime-builtin-stats.h @@ -12,6 +12,7 @@ #include "runtime-common/core/allocator/script-allocator.h" #include "runtime-common/core/std/containers.h" +#include "runtime-common/core/utils/kphp-assert-core.h" template<> struct std::hash> { diff --git a/tests/tests.cmake b/tests/tests.cmake index 498205ea3a..d8b4e52e71 100644 --- a/tests/tests.cmake +++ b/tests/tests.cmake @@ -18,8 +18,7 @@ if(KPHP_TESTS) include(common/common-tests.cmake) include(net/net-tests.cmake) include(tests/cpp/compiler/compiler-tests.cmake) - if (COMPILE_RUNTIME_LIGHT) - else () + if (NOT COMPILE_RUNTIME_LIGHT) include(tests/cpp/runtime/runtime-tests.cmake) include(tests/cpp/server/server-tests.cmake) endif ()