From d1031e5a27334504f0672e9669ca9cb649e13ece Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Thu, 20 Aug 2026 20:04:34 +0300 Subject: [PATCH 1/7] add K2 confdata key splitting and wildcard metadata --- runtime-common/core/std/containers.h | 4 + .../components/confdata/confdata.cmake | 5 +- .../confdata/state/component-state.cpp | 58 ++++- .../confdata/state/component-state.h | 15 +- .../state/predefined-wildcards-builder.cpp | 167 +++++++++++++ .../state/predefined-wildcards-builder.h | 28 +++ .../stdlib/confdata/confdata-keys.cpp | 66 +++++ runtime-light/stdlib/confdata/confdata-keys.h | 219 +++++++++++++++++ .../detail/predefined-wildcards-layout.h | 115 +++++++++ .../stdlib/confdata/predefined-wildcards.cpp | 228 ++++++++++++++++++ .../stdlib/confdata/predefined-wildcards.h | 147 +++++++++++ runtime-light/stdlib/stdlib.cmake | 2 + .../confdata/predefined-wildcards-test.cpp | 183 ++++++++++++++ .../runtime-light/runtime-light-tests.cmake | 11 + tests/tests.cmake | 1 + 15 files changed, 1245 insertions(+), 4 deletions(-) create mode 100644 runtime-light/components/confdata/state/predefined-wildcards-builder.cpp create mode 100644 runtime-light/components/confdata/state/predefined-wildcards-builder.h create mode 100644 runtime-light/stdlib/confdata/confdata-keys.cpp create mode 100644 runtime-light/stdlib/confdata/confdata-keys.h create mode 100644 runtime-light/stdlib/confdata/detail/predefined-wildcards-layout.h create mode 100644 runtime-light/stdlib/confdata/predefined-wildcards.cpp create mode 100644 runtime-light/stdlib/confdata/predefined-wildcards.h create mode 100644 tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp create mode 100644 tests/cpp/runtime-light/runtime-light-tests.cmake 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/components/confdata/confdata.cmake b/runtime-light/components/confdata/confdata.cmake index ecff9f47b1..7e181090e9 100644 --- a/runtime-light/components/confdata/confdata.cmake +++ b/runtime-light/components/confdata/confdata.cmake @@ -4,7 +4,10 @@ set(K2_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}/components/confdata/state/predefined-wildcards-builder.cpp + ${RUNTIME_LIGHT_DIR}/stdlib/confdata/confdata-keys.cpp + ${RUNTIME_LIGHT_DIR}/stdlib/confdata/predefined-wildcards.cpp) set(K2_CONFDATA_TL_SRC ${RUNTIME_LIGHT_DIR}/tl/tl-types.cpp diff --git a/runtime-light/components/confdata/state/component-state.cpp b/runtime-light/components/confdata/state/component-state.cpp index 335a56a1eb..4e47293b31 100644 --- a/runtime-light/components/confdata/state/component-state.cpp +++ b/runtime-light/components/confdata/state/component-state.cpp @@ -4,23 +4,77 @@ #include "runtime-light/components/confdata/state/component-state.h" +#include +#include +#include +#include #include +#include +#include "runtime-light/components/confdata/state/predefined-wildcards-builder.h" #include "runtime-light/k2-platform/k2-api.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_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_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_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 { 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 9e39724264..3ea0f49c4f 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" @@ -16,27 +17,39 @@ struct ComponentState final : private vk::not_copyable { AllocatorState m_allocator_state{INIT_COMPONENT_ALLOCATOR_SIZE, DEFAULT_MIN_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: + size_t m_confdata_memory_limit{}; + kphp::stl::string m_confdata_proxy_actor_name; + kphp::stl::vector m_predefined_wildcards; + 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_proxy_actor_name_arg(std::string_view) noexcept -> void; + auto parse_predefined_wildcards_arg(std::string_view) noexcept -> void; auto parse_args() noexcept -> void; + static constexpr std::string_view CONFDATA_MEMORY_LIMIT_ARG{"confdata-memory-limit"}; 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 auto INIT_COMPONENT_ALLOCATOR_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_proxy_actor_name.empty()) { kphp::log::error("{} argument is required", CONFDATA_PROXY_ACTOR_NAME_ARG); } diff --git a/runtime-light/components/confdata/state/predefined-wildcards-builder.cpp b/runtime-light/components/confdata/state/predefined-wildcards-builder.cpp new file mode 100644 index 0000000000..7adb327082 --- /dev/null +++ b/runtime-light/components/confdata/state/predefined-wildcards-builder.cpp @@ -0,0 +1,167 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#include "runtime-light/components/confdata/state/predefined-wildcards-builder.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "runtime-light/stdlib/confdata/detail/predefined-wildcards-layout.h" +#include "runtime-light/stdlib/confdata/predefined-wildcards.h" + +namespace { + +auto analyze_canonical_wildcards(std::span wildcards) noexcept + -> std::expected { + using kphp::confdata::predefined_wildcards_error; + using kphp::confdata::validate_predefined_wildcard; + + if (wildcards.size() > std::numeric_limits::max()) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::size_overflow}; + } + if (wildcards.empty()) { + return *kphp::confdata::detail::calculate_predefined_wildcards_metadata_layout(0, 0, 0, 0, 0); + } + + size_t strings_size{}; + size_t shortest_wildcard_size{std::numeric_limits::max()}; + for (size_t i{}; i < wildcards.size(); ++i) { + if (const auto validated{validate_predefined_wildcard(wildcards[i])}; !validated) [[unlikely]] { + return std::unexpected{validated.error()}; + } + if (i != 0 && wildcards[i - 1] >= wildcards[i]) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::non_canonical_wildcards}; + } + const auto new_strings_size{kphp::confdata::detail::checked_add(strings_size, wildcards[i].size())}; + if (!new_strings_size || *new_strings_size > std::numeric_limits::max()) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::size_overflow}; + } + strings_size = *new_strings_size; + shortest_wildcard_size = std::min(shortest_wildcard_size, wildcards[i].size()); + } + + size_t group_count{1}; + size_t max_matches_per_key{}; + size_t group_begin{}; + for (size_t i{}; i < wildcards.size(); ++i) { + const auto prefix{wildcards[i].substr(0, shortest_wildcard_size)}; + if (i != 0 && wildcards[i - 1].substr(0, shortest_wildcard_size) != prefix) { + ++group_count; + group_begin = i; + } + + size_t matches{1}; + for (size_t j{group_begin}; j < i; ++j) { + matches += wildcards[i].starts_with(wildcards[j]) ? 1 : 0; + } + max_matches_per_key = std::max(max_matches_per_key, matches); + } + + if (group_count > std::numeric_limits::max()) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::size_overflow}; + } + const auto layout{kphp::confdata::detail::calculate_predefined_wildcards_metadata_layout( + static_cast(wildcards.size()), static_cast(group_count), static_cast(strings_size), + static_cast(shortest_wildcard_size), static_cast(max_matches_per_key))}; + if (!layout) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::size_overflow}; + } + return *layout; +} + +auto header_from_layout(const kphp::confdata::detail::predefined_wildcards_metadata_layout& layout) noexcept + -> kphp::confdata::detail::predefined_wildcards_metadata_header { + return {.magic = kphp::confdata::detail::PREDEFINED_WILDCARDS_METADATA_MAGIC, + .version = kphp::confdata::detail::PREDEFINED_WILDCARDS_METADATA_VERSION, + .total_size = layout.total_size, + .entries_offset = layout.entries_offset, + .groups_offset = layout.groups_offset, + .strings_offset = layout.strings_offset, + .strings_size = layout.strings_size, + .wildcard_count = layout.wildcard_count, + .group_count = layout.group_count, + .shortest_wildcard_size = layout.shortest_wildcard_size, + .max_matches_per_key = layout.max_matches_per_key}; +} + +} // namespace + +namespace kphp::confdata { + +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}; + } + + size_t dots{}; + if (wildcard.back() == '.') { + for (const char c : wildcard) { + dots += (c == '.'); + if (dots > 2) { + break; + } + } + } + if (dots == 1 || dots == 2) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::reserved_wildcard}; + } + return {}; +} + +auto predefined_wildcards_metadata_size(std::span wildcards) noexcept -> std::expected { + const auto layout{analyze_canonical_wildcards(wildcards)}; + if (!layout) [[unlikely]] { + return std::unexpected{layout.error()}; + } + return layout->total_size; +} + +auto write_predefined_wildcards(std::span buffer, + std::span wildcards) noexcept -> std::expected { + if (!detail::is_predefined_wildcards_metadata_aligned(buffer.data())) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::misaligned_buffer}; + } + const auto layout{analyze_canonical_wildcards(wildcards)}; + if (!layout) [[unlikely]] { + return std::unexpected{layout.error()}; + } + if (buffer.size() < layout->total_size) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::insufficient_buffer}; + } + + detail::store(buffer.data(), 0, header_from_layout(*layout)); + size_t string_offset{layout->strings_offset}; + uint32_t group_index{}; + uint32_t group_begin{}; + for (uint32_t i{}; i < layout->wildcard_count; ++i) { + const auto wildcard{wildcards[i]}; + detail::store( + buffer.data(), layout->entries_offset + static_cast(i) * sizeof(detail::predefined_wildcard_entry), + detail::predefined_wildcard_entry{.string_offset = static_cast(string_offset), .string_size = static_cast(wildcard.size())}); + std::memcpy(buffer.data() + string_offset, wildcard.data(), wildcard.size()); + string_offset += wildcard.size(); + + const bool group_finished{i + 1 == layout->wildcard_count || + wildcard.substr(0, layout->shortest_wildcard_size) != wildcards[i + 1].substr(0, layout->shortest_wildcard_size)}; + if (group_finished) { + detail::store(buffer.data(), layout->groups_offset + static_cast(group_index) * sizeof(detail::predefined_wildcard_group), + detail::predefined_wildcard_group{.first_entry = group_begin, .entry_count = i - group_begin + 1}); + ++group_index; + group_begin = i + 1; + } + } + + return open_predefined_wildcards(buffer.first(layout->total_size)); +} + +} // namespace kphp::confdata diff --git a/runtime-light/components/confdata/state/predefined-wildcards-builder.h b/runtime-light/components/confdata/state/predefined-wildcards-builder.h new file mode 100644 index 0000000000..98e4306e51 --- /dev/null +++ b/runtime-light/components/confdata/state/predefined-wildcards-builder.h @@ -0,0 +1,28 @@ +// 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 "runtime-light/stdlib/confdata/predefined-wildcards.h" + +namespace kphp::confdata { + +auto validate_predefined_wildcard(std::string_view wildcard) noexcept -> std::expected; + +/** @return The number of bytes needed to encode sorted, unique `wildcards`. */ +auto predefined_wildcards_metadata_size(std::span wildcards) noexcept -> std::expected; + +/** + * @brief Writes sorted, unique `wildcards` into relocatable immutable metadata at the start of `buffer`. + * @return A read-only view over the written metadata. + */ +auto write_predefined_wildcards(std::span buffer, + std::span wildcards) noexcept -> std::expected; + +} // namespace kphp::confdata 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..8d69eb6c57 --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-keys.h @@ -0,0 +1,219 @@ +// 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" + +// 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 { + +enum class section_kind : uint8_t { simple_key, one_dot_wildcard, two_dots_wildcard, predefined_wildcard }; + +inline auto classify_wildcard(std::string_view wildcard) noexcept -> section_kind { + size_t dots{0}; + if (!wildcard.empty() && wildcard.back() == '.') { + for (const char c : wildcard) { + dots += (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; + } +} + +/** + * @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(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`). + */ +class key_views { +public: + // 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: + 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 and the raw key, + * 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 and the raw key, + * 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/detail/predefined-wildcards-layout.h b/runtime-light/stdlib/confdata/detail/predefined-wildcards-layout.h new file mode 100644 index 0000000000..559281fb60 --- /dev/null +++ b/runtime-light/stdlib/confdata/detail/predefined-wildcards-layout.h @@ -0,0 +1,115 @@ +// 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 "runtime-light/stdlib/confdata/predefined-wildcards.h" + +namespace kphp::confdata::detail { + +inline constexpr uint64_t PREDEFINED_WILDCARDS_METADATA_MAGIC{0x444c49574443324bULL}; // "K2CDWILD" in little-endian byte order +inline constexpr uint32_t PREDEFINED_WILDCARDS_METADATA_VERSION{1}; + +struct alignas(PREDEFINED_WILDCARDS_ALIGNMENT) predefined_wildcards_metadata_header { + uint64_t magic; + uint32_t version; + uint32_t total_size; + uint32_t entries_offset; + uint32_t groups_offset; + uint32_t strings_offset; + uint32_t strings_size; + uint32_t wildcard_count; + uint32_t group_count; + uint32_t shortest_wildcard_size; + uint32_t max_matches_per_key; +}; + +struct predefined_wildcard_entry { + uint32_t string_offset; + uint32_t string_size; +}; + +struct predefined_wildcard_group { + uint32_t first_entry; + uint32_t entry_count; +}; + +struct predefined_wildcards_metadata_layout { + uint32_t total_size; + uint32_t entries_offset; + uint32_t groups_offset; + uint32_t strings_offset; + uint32_t strings_size; + uint32_t wildcard_count; + uint32_t group_count; + uint32_t shortest_wildcard_size; + uint32_t max_matches_per_key; +}; + +template +auto load(const std::byte* data, size_t offset) noexcept -> T { + T value{}; + std::memcpy(std::addressof(value), data + offset, sizeof(value)); + return value; +} + +template +auto store(std::byte* data, size_t offset, const T& value) noexcept -> void { + std::memcpy(data + offset, std::addressof(value), sizeof(value)); +} + +inline auto checked_add(size_t lhs, size_t rhs) noexcept -> std::optional { + if (rhs > std::numeric_limits::max() - lhs) [[unlikely]] { + return std::nullopt; + } + return lhs + rhs; +} + +inline auto checked_mul(size_t lhs, size_t rhs) noexcept -> std::optional { + if (lhs != 0 && rhs > std::numeric_limits::max() / lhs) [[unlikely]] { + return std::nullopt; + } + return lhs * rhs; +} + +inline auto calculate_predefined_wildcards_metadata_layout(uint32_t wildcard_count, uint32_t group_count, uint32_t strings_size, + uint32_t shortest_wildcard_size, + uint32_t max_matches_per_key) noexcept -> std::optional { + const auto entries_size{checked_mul(wildcard_count, sizeof(predefined_wildcard_entry))}; + const auto groups_size{checked_mul(group_count, sizeof(predefined_wildcard_group))}; + if (!entries_size || !groups_size) [[unlikely]] { + return std::nullopt; + } + + const size_t entries_offset{sizeof(predefined_wildcards_metadata_header)}; + const auto groups_offset{checked_add(entries_offset, *entries_size)}; + const auto strings_offset{groups_offset.and_then([groups_size](size_t offset) noexcept { return checked_add(offset, *groups_size); })}; + const auto total_size{strings_offset.and_then([strings_size](size_t offset) noexcept { return checked_add(offset, strings_size); })}; + if (!groups_offset || !strings_offset || !total_size || *total_size > std::numeric_limits::max()) [[unlikely]] { + return std::nullopt; + } + + return predefined_wildcards_metadata_layout{.total_size = static_cast(*total_size), + .entries_offset = static_cast(entries_offset), + .groups_offset = static_cast(*groups_offset), + .strings_offset = static_cast(*strings_offset), + .strings_size = strings_size, + .wildcard_count = wildcard_count, + .group_count = group_count, + .shortest_wildcard_size = shortest_wildcard_size, + .max_matches_per_key = max_matches_per_key}; +} + +inline auto is_predefined_wildcards_metadata_aligned(const void* pointer) noexcept -> bool { + return reinterpret_cast(pointer) % PREDEFINED_WILDCARDS_ALIGNMENT == 0; +} + +} // namespace kphp::confdata::detail diff --git a/runtime-light/stdlib/confdata/predefined-wildcards.cpp b/runtime-light/stdlib/confdata/predefined-wildcards.cpp new file mode 100644 index 0000000000..85278fc10f --- /dev/null +++ b/runtime-light/stdlib/confdata/predefined-wildcards.cpp @@ -0,0 +1,228 @@ +// 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 +#include +#include + +#include "runtime-light/stdlib/confdata/detail/predefined-wildcards-layout.h" + +namespace { + +using metadata_header = kphp::confdata::detail::predefined_wildcards_metadata_header; +using wildcard_entry = kphp::confdata::detail::predefined_wildcard_entry; +using wildcard_group = kphp::confdata::detail::predefined_wildcard_group; + +auto is_valid_predefined_wildcard(std::string_view wildcard) noexcept -> bool { + if (wildcard.empty() || wildcard.size() > kphp::confdata::MAX_KEY_LENGTH) [[unlikely]] { + return false; + } + + size_t dots{}; + if (wildcard.back() == '.') { + for (const char c : wildcard) { + dots += (c == '.'); + if (dots > 2) { + break; + } + } + } + return dots != 1 && dots != 2; +} + +auto load_wildcard(const std::byte* data, const metadata_header& header, uint32_t index) noexcept -> std::string_view { + const auto entry{kphp::confdata::detail::load(data, header.entries_offset + static_cast(index) * sizeof(wildcard_entry))}; + return {reinterpret_cast(data + entry.string_offset), entry.string_size}; +} + +auto load_group(const std::byte* data, const metadata_header& header, uint32_t index) noexcept -> wildcard_group { + return kphp::confdata::detail::load(data, header.groups_offset + static_cast(index) * sizeof(wildcard_group)); +} + +auto has_valid_layout(size_t buffer_size, const metadata_header& header) noexcept -> bool { + // Recompute all offsets instead of trusting the serialized ones. This also + // proves that every subsequent fixed-size load fits in `header.total_size`. + const auto layout{kphp::confdata::detail::calculate_predefined_wildcards_metadata_layout(header.wildcard_count, header.group_count, header.strings_size, + header.shortest_wildcard_size, header.max_matches_per_key)}; + if (!layout || header.total_size != layout->total_size || header.entries_offset != layout->entries_offset || header.groups_offset != layout->groups_offset || + header.strings_offset != layout->strings_offset || header.total_size > buffer_size) [[unlikely]] { + return false; + } + + if (header.wildcard_count == 0) { + return header.group_count == 0 && header.strings_size == 0 && header.shortest_wildcard_size == 0 && header.max_matches_per_key == 0; + } + return header.group_count != 0 && header.shortest_wildcard_size != 0 && header.max_matches_per_key != 0; +} + +auto has_valid_entries(const std::byte* data, const metadata_header& header) noexcept -> bool { + size_t next_string_offset{header.strings_offset}; + size_t shortest_wildcard_size{std::numeric_limits::max()}; + std::string_view previous_wildcard{}; + + for (uint32_t i{}; i < header.wildcard_count; ++i) { + const auto entry{kphp::confdata::detail::load(data, header.entries_offset + static_cast(i) * sizeof(wildcard_entry))}; + const auto string_end{kphp::confdata::detail::checked_add(entry.string_offset, entry.string_size)}; + + // Strings are packed without gaps. Besides enforcing a canonical encoding, + // this prevents entries from overlapping or referring outside the blob. + if (entry.string_offset != next_string_offset || !string_end || *string_end > header.total_size) [[unlikely]] { + return false; + } + + const std::string_view wildcard{reinterpret_cast(data + entry.string_offset), entry.string_size}; + if (!is_valid_predefined_wildcard(wildcard) || (i != 0 && previous_wildcard >= wildcard)) [[unlikely]] { + return false; + } + next_string_offset = *string_end; + shortest_wildcard_size = std::min(shortest_wildcard_size, wildcard.size()); + previous_wildcard = wildcard; + } + + return next_string_offset == header.total_size && (header.wildcard_count == 0 || shortest_wildcard_size == header.shortest_wildcard_size); +} + +auto has_valid_groups(const std::byte* data, const metadata_header& header) noexcept -> bool { + uint32_t next_entry{}; + size_t max_matches_per_key{}; + std::string_view previous_prefix{}; + + for (uint32_t i{}; i < header.group_count; ++i) { + const auto group{load_group(data, header, i)}; + if (group.first_entry != next_entry || group.entry_count == 0 || group.entry_count > header.wildcard_count - group.first_entry) [[unlikely]] { + return false; + } + + // A group is exactly one run of wildcards sharing a prefix whose length is + // the shortest wildcard size. These prefixes form the lookup index. + const auto group_prefix{load_wildcard(data, header, group.first_entry).substr(0, header.shortest_wildcard_size)}; + if (i != 0 && previous_prefix >= group_prefix) [[unlikely]] { + return false; + } + + for (uint32_t j{}; j < group.entry_count; ++j) { + const auto wildcard{load_wildcard(data, header, group.first_entry + j)}; + if (wildcard.substr(0, header.shortest_wildcard_size) != group_prefix) [[unlikely]] { + return false; + } + + size_t matches{1}; + for (uint32_t k{}; k < j; ++k) { + matches += wildcard.starts_with(load_wildcard(data, header, group.first_entry + k)) ? 1 : 0; + } + max_matches_per_key = std::max(max_matches_per_key, matches); + } + next_entry += group.entry_count; + previous_prefix = group_prefix; + } + + return next_entry == header.wildcard_count && max_matches_per_key == header.max_matches_per_key; +} + +} // namespace + +namespace kphp::confdata { + +predefined_wildcards::predefined_wildcards(const std::byte* data, uint32_t entries_offset, uint32_t groups_offset, uint32_t wildcard_count, + uint32_t group_count, uint32_t shortest_wildcard_size, uint32_t max_matches_per_key) noexcept + : m_data{data}, + m_entries_offset{entries_offset}, + m_groups_offset{groups_offset}, + m_wildcard_count{wildcard_count}, + m_group_count{group_count}, + m_shortest_wildcard_size{shortest_wildcard_size}, + m_max_matches_per_key{max_matches_per_key} {} + +auto predefined_wildcards::wildcard_at(uint32_t index) const noexcept -> std::string_view { + const auto entry{ + detail::load(m_data, m_entries_offset + static_cast(index) * sizeof(detail::predefined_wildcard_entry))}; + return {reinterpret_cast(m_data + entry.string_offset), entry.string_size}; +} + +auto predefined_wildcards::matching_group(std::string_view key) const noexcept -> std::pair { + if (m_group_count == 0 || key.size() < m_shortest_wildcard_size) { + return {}; + } + + const auto key_prefix{key.substr(0, m_shortest_wildcard_size)}; + uint32_t first{}; + uint32_t last{m_group_count}; + while (first < last) { + const uint32_t middle{first + (last - first) / 2}; + const auto group{ + detail::load(m_data, m_groups_offset + static_cast(middle) * sizeof(detail::predefined_wildcard_group))}; + const auto group_prefix{wildcard_at(group.first_entry).substr(0, m_shortest_wildcard_size)}; + if (group_prefix < key_prefix) { + first = middle + 1; + } else { + last = middle; + } + } + if (first == m_group_count) { + return {}; + } + const auto group{ + detail::load(m_data, m_groups_offset + static_cast(first) * sizeof(detail::predefined_wildcard_group))}; + if (wildcard_at(group.first_entry).substr(0, m_shortest_wildcard_size) != key_prefix) { + return {}; + } + return {group.first_entry, group.entry_count}; +} + +auto predefined_wildcards::contains(std::string_view wildcard) const noexcept -> bool { + uint32_t first{}; + uint32_t last{m_wildcard_count}; + while (first < last) { + const uint32_t middle{first + (last - first) / 2}; + if (wildcard_at(middle) < wildcard) { + first = middle + 1; + } else { + last = middle; + } + } + return first != m_wildcard_count && wildcard_at(first) == wildcard; +} + +auto predefined_wildcards::is_top_level_wildcard(std::string_view wildcard) const noexcept -> bool { + if (!contains(wildcard)) { + return false; + } + size_t matches{}; + for_each_matching_wildcard(wildcard, [&matches](std::string_view /*unused*/) noexcept { ++matches; }); + return matches == 1; +} + +auto open_predefined_wildcards(std::span buffer) noexcept -> std::expected { + if (!detail::is_predefined_wildcards_metadata_aligned(buffer.data())) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::misaligned_buffer}; + } + if (buffer.size() < sizeof(detail::predefined_wildcards_metadata_header)) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::invalid_metadata}; + } + + const auto header{detail::load(buffer.data(), 0)}; + if (header.magic != detail::PREDEFINED_WILDCARDS_METADATA_MAGIC) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::invalid_metadata}; + } + if (header.version != detail::PREDEFINED_WILDCARDS_METADATA_VERSION) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::unsupported_version}; + } + if (!has_valid_layout(buffer.size(), header)) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::invalid_metadata}; + } + if (!has_valid_entries(buffer.data(), header) || !has_valid_groups(buffer.data(), header)) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::invalid_metadata}; + } + return predefined_wildcards{buffer.data(), header.entries_offset, header.groups_offset, header.wildcard_count, + header.group_count, header.shortest_wildcard_size, header.max_matches_per_key}; +} + +} // 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..55bd84e02a --- /dev/null +++ b/runtime-light/stdlib/confdata/predefined-wildcards.h @@ -0,0 +1,147 @@ +// 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 + +namespace kphp::confdata { + +inline constexpr auto MAX_KEY_LENGTH{static_cast(std::numeric_limits::max())}; +inline constexpr size_t PREDEFINED_WILDCARDS_ALIGNMENT{alignof(uint64_t)}; + +enum class predefined_wildcards_error : uint8_t { + empty_wildcard, + wildcard_too_long, + reserved_wildcard, + non_canonical_wildcards, + size_overflow, + insufficient_buffer, + misaligned_buffer, + invalid_metadata, + unsupported_version, +}; + +class predefined_wildcards final { +public: + predefined_wildcards() noexcept = default; + + /** + * @brief Invokes `f(wildcard)` for every configured wildcard that is a prefix of `key`. + * Matching wildcards are visited in ascending length order. + */ + template F> + auto for_each_matching_wildcard(std::string_view key, const F& f) const noexcept -> void; + + /** @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: + const std::byte* m_data{}; + uint32_t m_entries_offset{}; + uint32_t m_groups_offset{}; + uint32_t m_wildcard_count{}; + uint32_t m_group_count{}; + uint32_t m_shortest_wildcard_size{}; + uint32_t m_max_matches_per_key{}; + + predefined_wildcards(const std::byte* data, uint32_t entries_offset, uint32_t groups_offset, uint32_t wildcard_count, uint32_t group_count, + uint32_t shortest_wildcard_size, uint32_t max_matches_per_key) noexcept; + + auto wildcard_at(uint32_t index) const noexcept -> std::string_view; + auto matching_group(std::string_view key) const noexcept -> std::pair; + + friend auto open_predefined_wildcards(std::span) noexcept -> std::expected; +}; + +/** @brief Validates and opens relocatable immutable wildcard metadata. */ +auto open_predefined_wildcards(std::span buffer) noexcept -> std::expected; + +template F> +auto predefined_wildcards::for_each_matching_wildcard(std::string_view key, const F& f) const noexcept -> void { + const auto [first_entry, entry_count]{matching_group(key)}; + for (uint32_t i{0}; i < entry_count; ++i) { + const auto wildcard{wildcard_at(first_entry + i)}; + if (wildcard.size() <= key.size() && key.starts_with(wildcard)) { + std::invoke(f, wildcard); + } + } +} + +inline auto predefined_wildcards::shortest_matching_wildcard(std::string_view key) const noexcept -> std::optional { + std::optional result{}; + for_each_matching_wildcard(key, [&result](std::string_view wildcard) noexcept { + if (!result.has_value()) { + result = wildcard; + } + }); + return result; +} + +inline auto predefined_wildcards::max_matches_per_key() const noexcept -> size_t { + return m_max_matches_per_key; +} + +inline auto predefined_wildcards::has_matching_wildcard(std::string_view key) const noexcept -> bool { + return shortest_matching_wildcard(key).has_value(); +} + +} // 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::size_overflow: + return std::format_to(ctx.out(), "metadata size overflow"); + case predefined_wildcards_error::insufficient_buffer: + return std::format_to(ctx.out(), "insufficient metadata buffer"); + case predefined_wildcards_error::misaligned_buffer: + return std::format_to(ctx.out(), "misaligned metadata buffer"); + case predefined_wildcards_error::invalid_metadata: + return std::format_to(ctx.out(), "invalid wildcard metadata"); + case predefined_wildcards_error::unsupported_version: + return std::format_to(ctx.out(), "unsupported wildcard metadata version"); + } + return std::format_to(ctx.out(), "unknown wildcard metadata error"); + } +}; diff --git a/runtime-light/stdlib/stdlib.cmake b/runtime-light/stdlib/stdlib.cmake index 8776da4dc4..13a0f3e757 100644 --- a/runtime-light/stdlib/stdlib.cmake +++ b/runtime-light/stdlib/stdlib.cmake @@ -1,7 +1,9 @@ prepend( RUNTIME_LIGHT_STDLIB_SRC stdlib/ + 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/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp b/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp new file mode 100644 index 0000000000..db8adbadd8 --- /dev/null +++ b/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp @@ -0,0 +1,183 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "runtime-light/components/confdata/state/predefined-wildcards-builder.h" +#include "runtime-light/stdlib/confdata/confdata-keys.h" +#include "runtime-light/stdlib/confdata/predefined-wildcards.h" + +namespace { + +using kphp::confdata::predefined_wildcards; +using kphp::confdata::predefined_wildcards_error; + +auto check(bool condition, const char* expression, int line) noexcept -> void { + if (!condition) [[unlikely]] { + std::ignore = std::fprintf(stderr, "check failed at line %d: %s\n", line, expression); + std::abort(); + } +} + +#define CHECK(expression) check(static_cast(expression), #expression, __LINE__) + +auto make_metadata(std::vector wildcards, std::vector& storage) -> predefined_wildcards { + for (const auto wildcard : wildcards) { + CHECK(kphp::confdata::validate_predefined_wildcard(wildcard).has_value()); + } + std::ranges::sort(wildcards); + wildcards.erase(std::ranges::unique(wildcards).begin(), wildcards.end()); + const auto metadata_size{kphp::confdata::predefined_wildcards_metadata_size(wildcards)}; + CHECK(metadata_size.has_value()); + storage.resize(*metadata_size); + const auto metadata{kphp::confdata::write_predefined_wildcards(storage, wildcards)}; + CHECK(metadata.has_value()); + return *metadata; +} + +auto matching(const predefined_wildcards& wildcards, std::string_view key) -> std::vector { + std::vector result{}; + wildcards.for_each_matching_wildcard(key, [&result](std::string_view wildcard) { result.emplace_back(wildcard); }); + return result; +} + +auto test_validation_and_formatting() -> void { + CHECK(kphp::confdata::validate_predefined_wildcard("").error() == predefined_wildcards_error::empty_wildcard); + CHECK(kphp::confdata::validate_predefined_wildcard("foo.").error() == predefined_wildcards_error::reserved_wildcard); + CHECK(kphp::confdata::validate_predefined_wildcard("foo.bar.").error() == predefined_wildcards_error::reserved_wildcard); + CHECK(kphp::confdata::validate_predefined_wildcard("foo").has_value()); + CHECK(kphp::confdata::validate_predefined_wildcard("foo...").has_value()); + CHECK(std::format("{}", predefined_wildcards_error::empty_wildcard) == "empty wildcard"); +} + +auto test_empty_metadata() -> void { + std::vector storage{}; + const auto wildcards{make_metadata({}, storage)}; + + CHECK(wildcards.max_matches_per_key() == 0); + CHECK(!wildcards.has_matching_wildcard("anything")); + CHECK(!storage.empty()); +} + +auto test_indexed_prefix_groups() -> void { + std::vector storage{}; + const auto wildcards{make_metadata({"abc", "a", "c", "ab", "ca", "d"}, storage)}; + + CHECK(matching(wildcards, "abc.def") == (std::vector{"a", "ab", "abc"})); + CHECK(matching(wildcards, "cab") == (std::vector{"c", "ca"})); + CHECK(matching(wildcards, "d") == (std::vector{"d"})); + CHECK(matching(wildcards, "b").empty()); + + CHECK(wildcards.max_matches_per_key() == 3); + CHECK(wildcards.shortest_matching_wildcard("abc") == "a"); + CHECK(wildcards.contains("abc")); + CHECK(!wildcards.contains("ac")); + CHECK(wildcards.is_top_level_wildcard("a")); + CHECK(!wildcards.is_top_level_wildcard("ab")); + CHECK(!wildcards.is_top_level_wildcard("missing")); +} + +auto test_relocation() -> void { + std::vector original_storage{}; + const auto original{make_metadata({"foo", "foobar", "zip"}, original_storage)}; + std::vector relocated_storage{original_storage}; + + const auto relocated{kphp::confdata::open_predefined_wildcards(relocated_storage)}; + CHECK(relocated.has_value()); + CHECK(matching(*relocated, "foobar.value") == (std::vector{"foo", "foobar"})); + const auto shortest{relocated->shortest_matching_wildcard("foobar.value")}; + CHECK(shortest.has_value()); + const auto relocated_bytes{std::as_bytes(std::span{relocated_storage})}; + CHECK(reinterpret_cast(shortest->data()) >= relocated_bytes.data()); + CHECK(reinterpret_cast(shortest->data()) < relocated_bytes.data() + relocated_bytes.size()); + CHECK(original.max_matches_per_key() == relocated->max_matches_per_key()); +} + +auto test_invalid_metadata() -> void { + const std::vector noncanonical{"b", "a"}; + CHECK(kphp::confdata::predefined_wildcards_metadata_size(noncanonical).error() == predefined_wildcards_error::non_canonical_wildcards); + + std::vector storage{}; + static_cast(make_metadata({"abc"}, storage)); + storage.front() ^= std::byte{1}; + const auto corrupted{kphp::confdata::open_predefined_wildcards(storage)}; + CHECK(!corrupted.has_value()); + CHECK(corrupted.error() == predefined_wildcards_error::invalid_metadata); + + std::vector misaligned_storage(storage.size() + 1); + const auto misaligned{kphp::confdata::open_predefined_wildcards(std::span{misaligned_storage}.subspan(1))}; + CHECK(!misaligned.has_value()); + CHECK(misaligned.error() == predefined_wildcards_error::misaligned_buffer); +} + +auto test_key_splitting() -> void { + using kphp::confdata::section_kind; + + const auto simple{kphp::confdata::split_key("simple")}; + CHECK(simple.has_value()); + CHECK(simple->kind() == section_kind::simple_key); + CHECK(simple->section() == "simple"); + CHECK(std::holds_alternative(simple->remainder())); + + const auto one_dot_integer{kphp::confdata::split_key("foo.-15")}; + CHECK(one_dot_integer.has_value()); + CHECK(one_dot_integer->kind() == section_kind::one_dot_wildcard); + CHECK(one_dot_integer->section() == "foo."); + CHECK(std::get(one_dot_integer->remainder()) == -15); + + const auto one_dot_string{kphp::confdata::split_key("foo.012")}; + CHECK(one_dot_string.has_value()); + CHECK(std::get(one_dot_string->remainder()) == "012"); + + const auto two_dots{kphp::confdata::split_key("foo.bar.123")}; + CHECK(two_dots.has_value()); + CHECK(two_dots->kind() == section_kind::two_dots_wildcard); + CHECK(two_dots->section() == "foo.bar."); + CHECK(std::get(two_dots->remainder()) == 123); + const auto one_dot_reinterpretation{two_dots->reinterpret_two_dots_as_one_dot()}; + CHECK(one_dot_reinterpretation.has_value()); + CHECK(one_dot_reinterpretation->kind() == section_kind::one_dot_wildcard); + CHECK(one_dot_reinterpretation->section() == "foo."); + CHECK(std::get(one_dot_reinterpretation->remainder()) == "bar.123"); + + std::vector storage{}; + const auto wildcards{make_metadata({"abc"}, storage)}; + const auto predefined{kphp::confdata::split_key("abc123", wildcards)}; + CHECK(predefined.has_value()); + CHECK(predefined->kind() == section_kind::predefined_wildcard); + CHECK(predefined->section() == "abc"); + CHECK(std::get(predefined->remainder()) == 123); + + const auto invalid_wildcard_length{kphp::confdata::split_key_with_predefined_wildcard("abc", 0)}; + CHECK(!invalid_wildcard_length.has_value()); + CHECK(invalid_wildcard_length.error() == kphp::confdata::split_error::invalid_predefined_wildcard_length); + + std::string oversized_key{}; + oversized_key.assign(kphp::confdata::MAX_KEY_LENGTH + 1, 'x'); + const auto oversized{kphp::confdata::split_key(oversized_key)}; + CHECK(!oversized.has_value()); + CHECK(oversized.error() == kphp::confdata::split_error::key_too_long); +} + +} // namespace + +auto main() -> int { + test_validation_and_formatting(); + test_empty_metadata(); + test_indexed_prefix_groups(); + test_relocation(); + test_invalid_metadata(); + test_key_splitting(); + return 0; +} diff --git a/tests/cpp/runtime-light/runtime-light-tests.cmake b/tests/cpp/runtime-light/runtime-light-tests.cmake new file mode 100644 index 0000000000..2152a4c132 --- /dev/null +++ b/tests/cpp/runtime-light/runtime-light-tests.cmake @@ -0,0 +1,11 @@ +set(RUNTIME_LIGHT_CONFDATA_TEST_SOURCES + ${BASE_DIR}/runtime-light/components/confdata/state/predefined-wildcards-builder.cpp + ${BASE_DIR}/runtime-light/stdlib/confdata/confdata-keys.cpp + ${BASE_DIR}/runtime-light/stdlib/confdata/predefined-wildcards.cpp + ${BASE_DIR}/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp) + +add_executable(unittests-runtime-light-confdata ${RUNTIME_LIGHT_CONFDATA_TEST_SOURCES}) +target_compile_options(unittests-runtime-light-confdata PRIVATE ${RUNTIME_LIGHT_COMPILE_FLAGS}) +target_link_options(unittests-runtime-light-confdata PRIVATE -stdlib=libc++) +add_test(NAME unittests-runtime-light-confdata COMMAND unittests-runtime-light-confdata) +set_target_properties(unittests-runtime-light-confdata PROPERTIES FOLDER tests) diff --git a/tests/tests.cmake b/tests/tests.cmake index 498205ea3a..7ec6b2de36 100644 --- a/tests/tests.cmake +++ b/tests/tests.cmake @@ -19,6 +19,7 @@ if(KPHP_TESTS) include(net/net-tests.cmake) include(tests/cpp/compiler/compiler-tests.cmake) if (COMPILE_RUNTIME_LIGHT) + include(tests/cpp/runtime-light/runtime-light-tests.cmake) else () include(tests/cpp/runtime/runtime-tests.cmake) include(tests/cpp/server/server-tests.cmake) From 7969aa8b79d0e3e39a8f041cab5d5cf9e1c6470c Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Thu, 20 Aug 2026 22:18:39 +0300 Subject: [PATCH 2/7] port kPHP confdata test matrices to runtime-light --- .../confdata/predefined-wildcards-test.cpp | 374 +++++++++++++++--- 1 file changed, 327 insertions(+), 47 deletions(-) diff --git a/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp b/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp index db8adbadd8..70a77d9809 100644 --- a/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp +++ b/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp @@ -4,9 +4,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -22,6 +24,26 @@ namespace { using kphp::confdata::predefined_wildcards; using kphp::confdata::predefined_wildcards_error; +using kphp::confdata::section_kind; + +using remainder_type = kphp::confdata::key_views::remainder_type; + +struct key_sample { + std::string_view key; + section_kind kind; + std::string_view section; + remainder_type remainder; +}; + +struct section_sample { + std::string_view section; + section_kind kind; +}; + +struct two_dots_sample { + key_sample split; + key_sample reinterpreted; +}; auto check(bool condition, const char* expression, int line) noexcept -> void { if (!condition) [[unlikely]] { @@ -52,6 +74,50 @@ auto matching(const predefined_wildcards& wildcards, std::string_view key) -> st return result; } +auto no_remainder() noexcept -> remainder_type { + return std::monostate{}; +} + +auto string_remainder(std::string_view value) noexcept -> remainder_type { + return value; +} + +auto integer_remainder(int64_t value) noexcept -> remainder_type { + return value; +} + +auto check_key(const kphp::confdata::key_views& actual, const key_sample& expected, int line) noexcept -> void { + if (actual.raw_key() != expected.key || actual.kind() != expected.kind || actual.section() != expected.section || actual.remainder() != expected.remainder) + [[unlikely]] { + std::ignore = std::fprintf(stderr, "key check failed at line %d: key -> '%.*s'\n", line, static_cast(expected.key.size()), expected.key.data()); + std::abort(); + } +} + +#define CHECK_KEY(actual, expected) check_key(actual, expected, __LINE__) + +auto check_split(const key_sample& expected) -> void { + const auto actual{kphp::confdata::split_key(expected.key)}; + CHECK(actual.has_value()); + CHECK_KEY(*actual, expected); +} + +auto check_split(const key_sample& expected, const predefined_wildcards& wildcards) -> void { + const auto actual{kphp::confdata::split_key(expected.key, wildcards)}; + CHECK(actual.has_value()); + CHECK_KEY(*actual, expected); +} + +auto check_two_dots_split(const two_dots_sample& expected) -> void { + const auto split{kphp::confdata::split_key(expected.split.key)}; + CHECK(split.has_value()); + CHECK_KEY(*split, expected.split); + + const auto reinterpreted{split->reinterpret_two_dots_as_one_dot()}; + CHECK(reinterpreted.has_value()); + CHECK_KEY(*reinterpreted, expected.reinterpreted); +} + auto test_validation_and_formatting() -> void { CHECK(kphp::confdata::validate_predefined_wildcard("").error() == predefined_wildcards_error::empty_wildcard); CHECK(kphp::confdata::validate_predefined_wildcard("foo.").error() == predefined_wildcards_error::reserved_wildcard); @@ -61,30 +127,100 @@ auto test_validation_and_formatting() -> void { CHECK(std::format("{}", predefined_wildcards_error::empty_wildcard) == "empty wildcard"); } -auto test_empty_metadata() -> void { +auto test_empty_predefined_wildcards_matrix() -> void { std::vector storage{}; const auto wildcards{make_metadata({}, storage)}; + for (const std::string_view key : {"", ".", "..", "abc", "abc.", "abc.def", "abc.def.", "abc.def.ghi", "abc.def.ghi."}) { + CHECK(matching(wildcards, key).empty()); + CHECK(!wildcards.has_matching_wildcard(key)); + } CHECK(wildcards.max_matches_per_key() == 0); - CHECK(!wildcards.has_matching_wildcard("anything")); + for (const std::string_view wildcard : {"", ".", "abc", "abc.", "abc.def", "abc.def."}) { + CHECK(!wildcards.contains(wildcard)); + } + + const std::vector sections{ + {"", section_kind::simple_key}, + {"abc", section_kind::simple_key}, + {"abc.def", section_kind::simple_key}, + {"abc.def.ghi", section_kind::simple_key}, + {"abc.def.ghi.", section_kind::simple_key}, + {"abc.def.ghi.jkl", section_kind::simple_key}, + {".", section_kind::one_dot_wildcard}, + {"abc.", section_kind::one_dot_wildcard}, + {"..", section_kind::two_dots_wildcard}, + {"abc..", section_kind::two_dots_wildcard}, + {".abc.", section_kind::two_dots_wildcard}, + {"abc.def.", section_kind::two_dots_wildcard}, + }; + for (const auto& sample : sections) { + CHECK(kphp::confdata::classify_section(sample.section, wildcards) == sample.kind); + } CHECK(!storage.empty()); } -auto test_indexed_prefix_groups() -> void { +auto test_predefined_wildcards_matrix() -> void { std::vector storage{}; - const auto wildcards{make_metadata({"abc", "a", "c", "ab", "ca", "d"}, storage)}; + const auto wildcards{make_metadata({"a", "ab", "abc", "c"}, storage)}; - CHECK(matching(wildcards, "abc.def") == (std::vector{"a", "ab", "abc"})); - CHECK(matching(wildcards, "cab") == (std::vector{"c", "ca"})); - CHECK(matching(wildcards, "d") == (std::vector{"d"})); - CHECK(matching(wildcards, "b").empty()); + for (const std::string_view key : {"", ".", "..", "xyz", "xyz.abc"}) { + CHECK(matching(wildcards, key).empty()); + } + for (const std::string_view key : {"a", "acb", "a.bc", "axyz"}) { + CHECK(matching(wildcards, key) == (std::vector{"a"})); + } + for (const std::string_view key : {"ab", "abb", "ab.c", "abxyz"}) { + CHECK(matching(wildcards, key) == (std::vector{"a", "ab"})); + } + for (const std::string_view key : {"abc", "abc.def", "abcdef"}) { + CHECK(matching(wildcards, key) == (std::vector{"a", "ab", "abc"})); + } + for (const std::string_view key : {"c", "cab", "cccc", "c.axyz"}) { + CHECK(matching(wildcards, key) == (std::vector{"c"})); + } CHECK(wildcards.max_matches_per_key() == 3); CHECK(wildcards.shortest_matching_wildcard("abc") == "a"); - CHECK(wildcards.contains("abc")); - CHECK(!wildcards.contains("ac")); + for (const std::string_view wildcard : {"a", "ab", "abc", "c"}) { + CHECK(wildcards.contains(wildcard)); + } + for (const std::string_view wildcard : {"", ".", "a.", "abc.", "abc.def"}) { + CHECK(!wildcards.contains(wildcard)); + } + + const std::vector sections{ + {"", section_kind::simple_key}, + {"abc.def", section_kind::simple_key}, + {"abc.def.ghi", section_kind::simple_key}, + {"abc.def.ghi.", section_kind::simple_key}, + {"abc.def.ghi.jkl", section_kind::simple_key}, + {".", section_kind::one_dot_wildcard}, + {"abc.", section_kind::one_dot_wildcard}, + {"..", section_kind::two_dots_wildcard}, + {"abc..", section_kind::two_dots_wildcard}, + {".abc.", section_kind::two_dots_wildcard}, + {"abc.def.", section_kind::two_dots_wildcard}, + {"a", section_kind::predefined_wildcard}, + {"ab", section_kind::predefined_wildcard}, + {"abc", section_kind::predefined_wildcard}, + {"c", section_kind::predefined_wildcard}, + }; + for (const auto& sample : sections) { + CHECK(kphp::confdata::classify_section(sample.section, wildcards) == sample.kind); + } + + for (const std::string_view key : {"", ".", "b", "ba"}) { + CHECK(!wildcards.has_matching_wildcard(key)); + } + for (const std::string_view key : {"ab", "abc", "abc.", "abc.def", "abc.def.", "c.ab", "a.bc", "a", "c", "cxyz"}) { + CHECK(wildcards.has_matching_wildcard(key)); + } + CHECK(wildcards.is_top_level_wildcard("a")); CHECK(!wildcards.is_top_level_wildcard("ab")); + CHECK(!wildcards.is_top_level_wildcard("abc")); + CHECK(wildcards.is_top_level_wildcard("c")); CHECK(!wildcards.is_top_level_wildcard("missing")); } @@ -121,48 +257,183 @@ auto test_invalid_metadata() -> void { CHECK(misaligned.error() == predefined_wildcards_error::misaligned_buffer); } -auto test_key_splitting() -> void { - using kphp::confdata::section_kind; +auto test_zero_dots_key_matrix() -> void { + const std::vector samples{ + {"", section_kind::simple_key, "", no_remainder()}, + {"x", section_kind::simple_key, "x", no_remainder()}, + {"1", section_kind::simple_key, "1", no_remainder()}, + {"hello world!", section_kind::simple_key, "hello world!", no_remainder()}, + }; + for (const auto& sample : samples) { + check_split(sample); + } +} - const auto simple{kphp::confdata::split_key("simple")}; - CHECK(simple.has_value()); - CHECK(simple->kind() == section_kind::simple_key); - CHECK(simple->section() == "simple"); - CHECK(std::holds_alternative(simple->remainder())); - - const auto one_dot_integer{kphp::confdata::split_key("foo.-15")}; - CHECK(one_dot_integer.has_value()); - CHECK(one_dot_integer->kind() == section_kind::one_dot_wildcard); - CHECK(one_dot_integer->section() == "foo."); - CHECK(std::get(one_dot_integer->remainder()) == -15); - - const auto one_dot_string{kphp::confdata::split_key("foo.012")}; - CHECK(one_dot_string.has_value()); - CHECK(std::get(one_dot_string->remainder()) == "012"); - - const auto two_dots{kphp::confdata::split_key("foo.bar.123")}; - CHECK(two_dots.has_value()); - CHECK(two_dots->kind() == section_kind::two_dots_wildcard); - CHECK(two_dots->section() == "foo.bar."); - CHECK(std::get(two_dots->remainder()) == 123); - const auto one_dot_reinterpretation{two_dots->reinterpret_two_dots_as_one_dot()}; - CHECK(one_dot_reinterpretation.has_value()); - CHECK(one_dot_reinterpretation->kind() == section_kind::one_dot_wildcard); - CHECK(one_dot_reinterpretation->section() == "foo."); - CHECK(std::get(one_dot_reinterpretation->remainder()) == "bar.123"); +auto test_one_dot_empty_remainder_matrix() -> void { + const std::vector samples{ + {".", section_kind::one_dot_wildcard, ".", string_remainder("")}, + {"x.", section_kind::one_dot_wildcard, "x.", string_remainder("")}, + {"1.", section_kind::one_dot_wildcard, "1.", string_remainder("")}, + {"hello world!.", section_kind::one_dot_wildcard, "hello world!.", string_remainder("")}, + }; + for (const auto& sample : samples) { + check_split(sample); + } +} + +auto test_one_dot_string_remainder_matrix() -> void { + const std::vector samples{ + {".g", section_kind::one_dot_wildcard, ".", string_remainder("g")}, + {"x.gg", section_kind::one_dot_wildcard, "x.", string_remainder("gg")}, + {"1.two", section_kind::one_dot_wildcard, "1.", string_remainder("two")}, + {"hello .world!", section_kind::one_dot_wildcard, "hello .", string_remainder("world!")}, + {"big.9223372036854775808", section_kind::one_dot_wildcard, "big.", string_remainder("9223372036854775808")}, + {"small.-9223372036854775809", section_kind::one_dot_wildcard, "small.", string_remainder("-9223372036854775809")}, + {"bad_num0.-0", section_kind::one_dot_wildcard, "bad_num0.", string_remainder("-0")}, + {"bad_num1.1xx", section_kind::one_dot_wildcard, "bad_num1.", string_remainder("1xx")}, + {"bad_num2.012", section_kind::one_dot_wildcard, "bad_num2.", string_remainder("012")}, + }; + for (const auto& sample : samples) { + check_split(sample); + } +} + +auto test_one_dot_integer_remainder_matrix() -> void { + const std::vector samples{ + {".123", section_kind::one_dot_wildcard, ".", integer_remainder(123)}, + {"x.-15", section_kind::one_dot_wildcard, "x.", integer_remainder(-15)}, + {"x.48", section_kind::one_dot_wildcard, "x.", integer_remainder(48)}, + {"0.0", section_kind::one_dot_wildcard, "0.", integer_remainder(0)}, + {"max.9223372036854775807", section_kind::one_dot_wildcard, "max.", integer_remainder(std::numeric_limits::max())}, + {"min.-9223372036854775808", section_kind::one_dot_wildcard, "min.", integer_remainder(std::numeric_limits::min())}, + }; + for (const auto& sample : samples) { + check_split(sample); + } +} + +auto test_two_dots_empty_remainder_matrix() -> void { + const std::vector samples{ + {{"..", section_kind::two_dots_wildcard, "..", string_remainder("")}, {"..", section_kind::one_dot_wildcard, ".", string_remainder(".")}}, + {{".ab.", section_kind::two_dots_wildcard, ".ab.", string_remainder("")}, {".ab.", section_kind::one_dot_wildcard, ".", string_remainder("ab.")}}, + {{"x..", section_kind::two_dots_wildcard, "x..", string_remainder("")}, {"x..", section_kind::one_dot_wildcard, "x.", string_remainder(".")}}, + {{"x.y.", section_kind::two_dots_wildcard, "x.y.", string_remainder("")}, {"x.y.", section_kind::one_dot_wildcard, "x.", string_remainder("y.")}}, + {{"1..", section_kind::two_dots_wildcard, "1..", string_remainder("")}, {"1..", section_kind::one_dot_wildcard, "1.", string_remainder(".")}}, + {{"1.2.", section_kind::two_dots_wildcard, "1.2.", string_remainder("")}, {"1.2.", section_kind::one_dot_wildcard, "1.", string_remainder("2.")}}, + {{"hello world!..", section_kind::two_dots_wildcard, "hello world!..", string_remainder("")}, + {"hello world!..", section_kind::one_dot_wildcard, "hello world!.", string_remainder(".")}}, + {{"hello.world!.", section_kind::two_dots_wildcard, "hello.world!.", string_remainder("")}, + {"hello.world!.", section_kind::one_dot_wildcard, "hello.", string_remainder("world!.")}}, + }; + for (const auto& sample : samples) { + check_two_dots_split(sample); + } +} + +auto test_two_dots_string_remainder_matrix() -> void { + const std::vector samples{ + {{"..g", section_kind::two_dots_wildcard, "..", string_remainder("g")}, {"..g", section_kind::one_dot_wildcard, ".", string_remainder(".g")}}, + {{"x.g.g", section_kind::two_dots_wildcard, "x.g.", string_remainder("g")}, {"x.g.g", section_kind::one_dot_wildcard, "x.", string_remainder("g.g")}}, + {{"1.2.two", section_kind::two_dots_wildcard, "1.2.", string_remainder("two")}, + {"1.2.two", section_kind::one_dot_wildcard, "1.", string_remainder("2.two")}}, + {{"hello. .world!", section_kind::two_dots_wildcard, "hello. .", string_remainder("world!")}, + {"hello. .world!", section_kind::one_dot_wildcard, "hello.", string_remainder(" .world!")}}, + {{"big.num.9223372036854775808", section_kind::two_dots_wildcard, "big.num.", string_remainder("9223372036854775808")}, + {"big.num.9223372036854775808", section_kind::one_dot_wildcard, "big.", string_remainder("num.9223372036854775808")}}, + {{"small.num.-9223372036854775809", section_kind::two_dots_wildcard, "small.num.", string_remainder("-9223372036854775809")}, + {"small.num.-9223372036854775809", section_kind::one_dot_wildcard, "small.", string_remainder("num.-9223372036854775809")}}, + {{"bad_num.0.-0", section_kind::two_dots_wildcard, "bad_num.0.", string_remainder("-0")}, + {"bad_num.0.-0", section_kind::one_dot_wildcard, "bad_num.", string_remainder("0.-0")}}, + {{"bad_num.1.1xx", section_kind::two_dots_wildcard, "bad_num.1.", string_remainder("1xx")}, + {"bad_num.1.1xx", section_kind::one_dot_wildcard, "bad_num.", string_remainder("1.1xx")}}, + {{"bad_num.2.012", section_kind::two_dots_wildcard, "bad_num.2.", string_remainder("012")}, + {"bad_num.2.012", section_kind::one_dot_wildcard, "bad_num.", string_remainder("2.012")}}, + }; + for (const auto& sample : samples) { + check_two_dots_split(sample); + } +} +auto test_two_dots_integer_remainder_matrix() -> void { + const std::vector samples{ + {{"..123", section_kind::two_dots_wildcard, "..", integer_remainder(123)}, {"..123", section_kind::one_dot_wildcard, ".", string_remainder(".123")}}, + {{"x.y.-15", section_kind::two_dots_wildcard, "x.y.", integer_remainder(-15)}, + {"x.y.-15", section_kind::one_dot_wildcard, "x.", string_remainder("y.-15")}}, + {{"x..48", section_kind::two_dots_wildcard, "x..", integer_remainder(48)}, {"x..48", section_kind::one_dot_wildcard, "x.", string_remainder(".48")}}, + {{"0.-1.0", section_kind::two_dots_wildcard, "0.-1.", integer_remainder(0)}, {"0.-1.0", section_kind::one_dot_wildcard, "0.", string_remainder("-1.0")}}, + {{"max.n.2147483647", section_kind::two_dots_wildcard, "max.n.", integer_remainder(2147483647)}, + {"max.n.2147483647", section_kind::one_dot_wildcard, "max.", string_remainder("n.2147483647")}}, + {{"min.n.-2147483648", section_kind::two_dots_wildcard, "min.n.", integer_remainder(-2147483648LL)}, + {"min.n.-2147483648", section_kind::one_dot_wildcard, "min.", string_remainder("n.-2147483648")}}, + }; + for (const auto& sample : samples) { + check_two_dots_split(sample); + } +} + +auto test_explicit_predefined_wildcard_matrix() -> void { + struct sample { + size_t wildcard_size; + key_sample expected; + }; + const std::vector samples{ + {3, {"abcd", section_kind::predefined_wildcard, "abc", string_remainder("d")}}, + {3, {"123abc", section_kind::predefined_wildcard, "123", string_remainder("abc")}}, + {3, {"abc123", section_kind::predefined_wildcard, "abc", integer_remainder(123)}}, + {1, {"abc123", section_kind::predefined_wildcard, "a", string_remainder("bc123")}}, + }; + for (const auto& sample : samples) { + const auto actual{kphp::confdata::split_key_with_predefined_wildcard(sample.expected.key, sample.wildcard_size)}; + CHECK(actual.has_value()); + CHECK_KEY(*actual, sample.expected); + } +} + +auto test_automatic_predefined_wildcard_matrix() -> void { std::vector storage{}; - const auto wildcards{make_metadata({"abc"}, storage)}; - const auto predefined{kphp::confdata::split_key("abc123", wildcards)}; - CHECK(predefined.has_value()); - CHECK(predefined->kind() == section_kind::predefined_wildcard); - CHECK(predefined->section() == "abc"); - CHECK(std::get(predefined->remainder()) == 123); + // Implicit one-dot/two-dot sections are deliberately excluded from predefined metadata. + const auto wildcards{make_metadata({"abc", "abc.xyz", "cde", "cd"}, storage)}; + const std::vector samples{ + {"abc", section_kind::predefined_wildcard, "abc", string_remainder("")}, + {"abc.", section_kind::predefined_wildcard, "abc", string_remainder(".")}, + {"abcd", section_kind::predefined_wildcard, "abc", string_remainder("d")}, + {"abc123", section_kind::predefined_wildcard, "abc", integer_remainder(123)}, + {"abc.xyz", section_kind::predefined_wildcard, "abc", string_remainder(".xyz")}, + {"abc.xyz.uvz", section_kind::predefined_wildcard, "abc", string_remainder(".xyz.uvz")}, + {"cd", section_kind::predefined_wildcard, "cd", string_remainder("")}, + {"cdxxx", section_kind::predefined_wildcard, "cd", string_remainder("xxx")}, + {"cde.xyz", section_kind::predefined_wildcard, "cd", string_remainder("e.xyz")}, + {"a", section_kind::simple_key, "a", no_remainder()}, + {"ab", section_kind::simple_key, "ab", no_remainder()}, + {"hello world", section_kind::simple_key, "hello world", no_remainder()}, + {"foo", section_kind::simple_key, "foo", no_remainder()}, + {"foo.", section_kind::one_dot_wildcard, "foo.", string_remainder("")}, + {"foo.bar", section_kind::one_dot_wildcard, "foo.", string_remainder("bar")}, + {"hello.world", section_kind::one_dot_wildcard, "hello.", string_remainder("world")}, + {"foo.bar.", section_kind::two_dots_wildcard, "foo.bar.", string_remainder("")}, + {"foo.bar.baz", section_kind::two_dots_wildcard, "foo.bar.", string_remainder("baz")}, + {"hello.wo.ld", section_kind::two_dots_wildcard, "hello.wo.", string_remainder("ld")}, + }; + for (const auto& sample : samples) { + check_split(sample, wildcards); + } +} +auto test_key_splitting_errors() -> void { const auto invalid_wildcard_length{kphp::confdata::split_key_with_predefined_wildcard("abc", 0)}; CHECK(!invalid_wildcard_length.has_value()); CHECK(invalid_wildcard_length.error() == kphp::confdata::split_error::invalid_predefined_wildcard_length); + const auto excessive_wildcard_length{kphp::confdata::split_key_with_predefined_wildcard("abc", 4)}; + CHECK(!excessive_wildcard_length.has_value()); + CHECK(excessive_wildcard_length.error() == kphp::confdata::split_error::invalid_predefined_wildcard_length); + + const auto simple{kphp::confdata::split_key("simple")}; + CHECK(simple.has_value()); + const auto not_two_dots{simple->reinterpret_two_dots_as_one_dot()}; + CHECK(!not_two_dots.has_value()); + CHECK(not_two_dots.error() == kphp::confdata::split_error::not_a_two_dots_key); + std::string oversized_key{}; oversized_key.assign(kphp::confdata::MAX_KEY_LENGTH + 1, 'x'); const auto oversized{kphp::confdata::split_key(oversized_key)}; @@ -174,10 +445,19 @@ auto test_key_splitting() -> void { auto main() -> int { test_validation_and_formatting(); - test_empty_metadata(); - test_indexed_prefix_groups(); + test_empty_predefined_wildcards_matrix(); + test_predefined_wildcards_matrix(); test_relocation(); test_invalid_metadata(); - test_key_splitting(); + test_zero_dots_key_matrix(); + test_one_dot_empty_remainder_matrix(); + test_one_dot_string_remainder_matrix(); + test_one_dot_integer_remainder_matrix(); + test_two_dots_empty_remainder_matrix(); + test_two_dots_string_remainder_matrix(); + test_two_dots_integer_remainder_matrix(); + test_explicit_predefined_wildcard_matrix(); + test_automatic_predefined_wildcard_matrix(); + test_key_splitting_errors(); return 0; } From c67dc3783a55ff7dbc3809d5f81b06b8c6f10bd9 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 21 Aug 2026 15:11:25 +0300 Subject: [PATCH 3/7] add K2 script memory resource switching --- .../core/allocator/runtime-allocator.h | 12 +- runtime-light/allocator/allocator.h | 21 ++ .../allocator/runtime-light-allocator.cpp | 72 ++++--- .../allocator/script-memory-resource-test.cpp | 203 ++++++++++++++++++ .../runtime-light/runtime-light-tests.cmake | 15 ++ 5 files changed, 295 insertions(+), 28 deletions(-) create mode 100644 tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp diff --git a/runtime-common/core/allocator/runtime-allocator.h b/runtime-common/core/allocator/runtime-allocator.h index aac9d859d8..58c92fc22c 100644 --- a/runtime-common/core/allocator/runtime-allocator.h +++ b/runtime-common/core/allocator/runtime-allocator.h @@ -5,6 +5,8 @@ #pragma once #include +#include +#include #include "common/mixin/not_copyable.h" #include "runtime-common/core/memory-resource/unsynchronized_pool_resource.h" @@ -28,12 +30,18 @@ struct RuntimeAllocator final : vk::not_copyable { void* realloc_global_memory(void* mem, size_t new_size, size_t old_size) noexcept; void free_global_memory(void* mem, size_t size) noexcept; -private: - void request_extra_memory(size_t requested_size) noexcept; + [[nodiscard]] std::reference_wrapper + replace_script_memory_resource(memory_resource::unsynchronized_pool_resource& replacement) noexcept { + return std::exchange(m_script_memory_resource, std::ref(replacement)); + } + memory_resource::unsynchronized_pool_resource& current_script_memory_resource() const noexcept { + return m_script_memory_resource.get(); + } public: memory_resource::unsynchronized_pool_resource memory_resource; private: + std::reference_wrapper m_script_memory_resource{memory_resource}; size_t m_min_extra_mem_size{0}; }; diff --git a/runtime-light/allocator/allocator.h b/runtime-light/allocator/allocator.h index bd625d9ebc..9307b86f9c 100644 --- a/runtime-light/allocator/allocator.h +++ b/runtime-light/allocator/allocator.h @@ -6,7 +6,12 @@ #include #include +#include +#include +#include +#include +#include "common/containers/final_action.h" #include "runtime-common/core/allocator/script-allocator-managed.h" #include "runtime-light/allocator/allocator-state.h" @@ -17,6 +22,22 @@ auto make_unique_on_script_memory(Args&&... args) noexcept { } namespace kphp::memory { + +// All script-allocated objects created by the callback must be destroyed before +// it returns. Keeping the operation synchronous and non-throwing ensures that +// the replacement cannot accidentally survive a suspension or stack unwind. +template +requires std::same_as, void> && std::is_nothrow_invocable_v +void with_script_memory_resource(memory_resource::unsynchronized_pool_resource& resource, callback_type&& callback) noexcept { + auto& allocator{RuntimeAllocator::get()}; + const auto previous_resource{allocator.replace_script_memory_resource(resource)}; + const auto restore_resource{vk::finally([&allocator, &resource, previous_resource]() noexcept { + kphp::log::assertion(std::addressof(allocator.current_script_memory_resource()) == std::addressof(resource)); + static_cast(allocator.replace_script_memory_resource(previous_resource.get())); + })}; + std::invoke(std::forward(callback)); +} + struct libc_alloc_guard final { libc_alloc_guard() noexcept { AllocatorState::get_mutable().enable_libc_alloc(); diff --git a/runtime-light/allocator/runtime-light-allocator.cpp b/runtime-light/allocator/runtime-light-allocator.cpp index 8234c5ebb7..bab8ad058c 100644 --- a/runtime-light/allocator/runtime-light-allocator.cpp +++ b/runtime-light/allocator/runtime-light-allocator.cpp @@ -6,11 +6,41 @@ #include #include #include +#include #include "runtime-light/allocator/allocator-state.h" #include "runtime-light/k2-platform/k2-api.h" #include "runtime-light/stdlib/diagnostics/logs.h" +namespace { + +bool try_extend_default_memory_resource(RuntimeAllocator& allocator, memory_resource::unsynchronized_pool_resource& current_resource, size_t min_extra_mem_size, + size_t requested_size) noexcept { + // Only the allocator-owned resource can be extended with K2 memory. A + // temporarily installed resource is a fixed allocation domain and must + // not silently fall back to request-local memory when it is exhausted. + if (std::addressof(current_resource) != std::addressof(allocator.memory_resource)) { + return false; + } + + // Extra mem size have to be greater than max chunk block + const auto min_size{std::max(min_extra_mem_size, memory_resource::unsynchronized_pool_resource::MAX_CHUNK_BLOCK_SIZE)}; + + size_t extra_mem_size{std::max(min_size, requested_size)}; + // Take into account internal layout of `memory_resource::extra_memory_pool` + extra_mem_size += sizeof(memory_resource::extra_memory_pool); + // 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{allocator.alloc_global_memory(extra_mem_size)}; + allocator.memory_resource.add_extra_memory(new (extra_mem) memory_resource::extra_memory_pool{extra_mem_size}); + return true; +} + +} // namespace + RuntimeAllocator& RuntimeAllocator::get() noexcept { return AllocatorState::get_mutable().allocator; } @@ -43,10 +73,12 @@ void RuntimeAllocator::free() { void* RuntimeAllocator::alloc_script_memory(size_t size) noexcept { kphp::log::assertion(size != 0); - void* mem{memory_resource.allocate(size)}; + auto& current_resource{m_script_memory_resource.get()}; + void* mem{current_resource.allocate(size)}; + if (mem == nullptr && try_extend_default_memory_resource(*this, current_resource, m_min_extra_mem_size, size)) [[unlikely]] { + mem = current_resource.allocate(size); + } if (mem == nullptr) [[unlikely]] { - request_extra_memory(size); - mem = memory_resource.allocate(size); kphp::log::assertion(mem != nullptr); } return mem; @@ -54,10 +86,12 @@ void* RuntimeAllocator::alloc_script_memory(size_t size) noexcept { void* RuntimeAllocator::alloc0_script_memory(size_t size) noexcept { kphp::log::assertion(size != 0); - void* mem{memory_resource.allocate0(size)}; + auto& current_resource{m_script_memory_resource.get()}; + void* mem{current_resource.allocate0(size)}; + if (mem == nullptr && try_extend_default_memory_resource(*this, current_resource, m_min_extra_mem_size, size)) [[unlikely]] { + mem = current_resource.allocate0(size); + } if (mem == nullptr) [[unlikely]] { - request_extra_memory(size); - mem = memory_resource.allocate0(size); kphp::log::assertion(mem != nullptr); } return mem; @@ -65,10 +99,12 @@ void* RuntimeAllocator::alloc0_script_memory(size_t size) noexcept { void* RuntimeAllocator::realloc_script_memory(void* old_mem, size_t new_size, size_t old_size) noexcept { kphp::log::assertion(new_size > old_size); - void* new_mem{memory_resource.reallocate(old_mem, new_size, old_size)}; + auto& current_resource{m_script_memory_resource.get()}; + void* new_mem{current_resource.reallocate(old_mem, new_size, old_size)}; + if (new_mem == nullptr && try_extend_default_memory_resource(*this, current_resource, m_min_extra_mem_size, new_size * 2)) [[unlikely]] { + new_mem = current_resource.reallocate(old_mem, new_size, old_size); + } if (new_mem == nullptr) [[unlikely]] { - request_extra_memory(new_size * 2); - new_mem = memory_resource.reallocate(old_mem, new_size, old_size); kphp::log::assertion(new_mem != nullptr); } return new_mem; @@ -76,7 +112,7 @@ void* RuntimeAllocator::realloc_script_memory(void* old_mem, size_t new_size, si void RuntimeAllocator::free_script_memory(void* mem, size_t size) noexcept { kphp::log::assertion(size != 0); - memory_resource.deallocate(mem, size); + m_script_memory_resource.get().deallocate(mem, size); } void* RuntimeAllocator::alloc_global_memory(size_t size) noexcept { @@ -101,19 +137,3 @@ void* RuntimeAllocator::realloc_global_memory(void* old_mem, size_t new_size, si void RuntimeAllocator::free_global_memory(void* mem, size_t /*unused*/) noexcept { k2::free(mem); } - -void RuntimeAllocator::request_extra_memory(size_t requested_size) noexcept { - // 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)}; - - size_t extra_mem_size{std::max(min_size, requested_size)}; - // Take into account internal layout of `memory_resource::extra_memory_pool` - extra_mem_size += sizeof(memory_resource::extra_memory_pool); - // 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{alloc_global_memory(extra_mem_size)}; - memory_resource.add_extra_memory(new (extra_mem) memory_resource::extra_memory_pool{extra_mem_size}); -} diff --git a/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp b/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp new file mode 100644 index 0000000000..89f87b3630 --- /dev/null +++ b/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp @@ -0,0 +1,203 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "runtime-common/core/allocator/script-allocator.h" +#include "runtime-light/allocator/allocator-state.h" +#include "runtime-light/allocator/allocator.h" +#include "runtime-light/k2-platform/k2-api.h" + +namespace { + +constexpr auto DEFAULT_ALLOCATOR_SIZE{static_cast(1024U * 1024U)}; +constexpr auto TEST_RESOURCE_SIZE{static_cast(64U * 1024U)}; + +auto check(bool condition, const char* expression, int line) noexcept -> void { + if (!condition) [[unlikely]] { + static_cast(std::fprintf(stderr, "check failed at line %d: %s\n", line, expression)); + std::abort(); + } +} + +#define CHECK(expression) check(static_cast(expression), #expression, __LINE__) + +template +auto contains(const std::array& storage, const void* memory) noexcept -> bool { + const auto begin{reinterpret_cast(storage.data())}; + const auto end{begin + storage.size()}; + const auto address{reinterpret_cast(memory)}; + return begin <= address && address < end; +} + +struct noexcept_void_callback final { + auto operator()() const noexcept -> void {} +}; + +struct throwing_void_callback final { + auto operator()() const -> void {} +}; + +struct noexcept_value_callback final { + auto operator()() const noexcept -> int { + return 0; + } +}; + +template +concept valid_script_memory_callback = requires(memory_resource::unsynchronized_pool_resource& resource, callback_type callback) { + kphp::memory::with_script_memory_resource(resource, std::move(callback)); +}; + +static_assert(valid_script_memory_callback); +static_assert(!valid_script_memory_callback); +static_assert(!valid_script_memory_callback); + +auto test_routes_script_allocations_and_restores_default() noexcept -> void { + alignas(std::max_align_t) std::array shared_storage{}; + memory_resource::unsynchronized_pool_resource shared_resource{}; + shared_resource.init(shared_storage.data(), shared_storage.size()); + + auto& allocator{RuntimeAllocator::get()}; + auto& default_resource{allocator.memory_resource}; + const auto default_memory_before{default_resource.get_memory_stats().memory_used}; + + kphp::memory::with_script_memory_resource(shared_resource, [&]() noexcept { + CHECK(std::addressof(allocator.current_script_memory_resource()) == std::addressof(shared_resource)); + + kphp::memory::script_allocator script_allocator{}; + auto* memory{script_allocator.allocate(64)}; + CHECK(contains(shared_storage, memory)); + CHECK(shared_resource.get_memory_stats().memory_used != 0); + CHECK(default_resource.get_memory_stats().memory_used == default_memory_before); + script_allocator.deallocate(memory, 64); + }); + + CHECK(std::addressof(allocator.current_script_memory_resource()) == std::addressof(default_resource)); + CHECK(shared_resource.get_memory_stats().memory_used == 0); + CHECK(default_resource.get_memory_stats().memory_used == default_memory_before); +} + +auto test_nested_resources_restore_previous_target() noexcept -> void { + alignas(std::max_align_t) std::array outer_storage{}; + alignas(std::max_align_t) std::array inner_storage{}; + memory_resource::unsynchronized_pool_resource outer_resource{}; + memory_resource::unsynchronized_pool_resource inner_resource{}; + outer_resource.init(outer_storage.data(), outer_storage.size()); + inner_resource.init(inner_storage.data(), inner_storage.size()); + + auto& allocator{RuntimeAllocator::get()}; + kphp::memory::script_allocator script_allocator{}; + + kphp::memory::with_script_memory_resource(outer_resource, [&]() noexcept { + auto* outer_memory_before{script_allocator.allocate(32)}; + CHECK(contains(outer_storage, outer_memory_before)); + + kphp::memory::with_script_memory_resource(inner_resource, [&]() noexcept { + auto* inner_memory{script_allocator.allocate(32)}; + CHECK(contains(inner_storage, inner_memory)); + script_allocator.deallocate(inner_memory, 32); + }); + + CHECK(std::addressof(allocator.current_script_memory_resource()) == std::addressof(outer_resource)); + auto* outer_memory_after{script_allocator.allocate(32)}; + CHECK(contains(outer_storage, outer_memory_after)); + script_allocator.deallocate(outer_memory_after, 32); + script_allocator.deallocate(outer_memory_before, 32); + }); + + CHECK(std::addressof(allocator.current_script_memory_resource()) == std::addressof(allocator.memory_resource)); + CHECK(outer_resource.get_memory_stats().memory_used == 0); + CHECK(inner_resource.get_memory_stats().memory_used == 0); +} + +auto test_zeroing_and_reallocation_use_replacement_resource() noexcept -> void { + alignas(std::max_align_t) std::array shared_storage{}; + memory_resource::unsynchronized_pool_resource shared_resource{}; + shared_resource.init(shared_storage.data(), shared_storage.size()); + + auto& allocator{RuntimeAllocator::get()}; + kphp::memory::with_script_memory_resource(shared_resource, [&]() noexcept { + constexpr auto initial_size{static_cast(32)}; + constexpr auto expanded_size{static_cast(128)}; + + auto* zeroed{static_cast(allocator.alloc0_script_memory(initial_size))}; + CHECK(contains(shared_storage, zeroed)); + CHECK(std::ranges::all_of(std::span{zeroed, initial_size}, [](std::byte value) noexcept { return value == std::byte{}; })); + + std::ranges::fill(std::span{zeroed, initial_size}, std::byte{0x5A}); + auto* expanded{static_cast(allocator.realloc_script_memory(zeroed, expanded_size, initial_size))}; + CHECK(contains(shared_storage, expanded)); + CHECK(std::ranges::all_of(std::span{expanded, initial_size}, [](std::byte value) noexcept { return value == std::byte{0x5A}; })); + allocator.free_script_memory(expanded, expanded_size); + }); + + CHECK(shared_resource.get_memory_stats().memory_used == 0); +} + +auto test_default_resource_can_request_extra_memory() noexcept -> void { + auto& allocator{RuntimeAllocator::get()}; + constexpr auto allocation_size{DEFAULT_ALLOCATOR_SIZE * 2}; + + auto* memory{allocator.alloc_script_memory(allocation_size)}; + CHECK(memory != nullptr); + CHECK(allocator.memory_resource.get_extra_memory_head()->get_pool_payload_size() != 0); + allocator.free_script_memory(memory, allocation_size); +} + +} // namespace + +extern "C" void* k2_alloc(size_t size, size_t align) { + const auto actual_align{std::max(align, alignof(std::max_align_t))}; + const auto actual_size{(size + actual_align - 1) / actual_align * actual_align}; + return std::aligned_alloc(actual_align, actual_size); +} + +extern "C" void* k2_realloc(void* memory, size_t new_size) { + return std::realloc(memory, new_size); +} + +extern "C" void k2_free(void* memory) { + std::free(memory); +} + +extern "C" void k2_log(size_t /*level*/, size_t /*len*/, const char* /*msg*/, size_t /*kv_count*/, const LogKeyValuePair* /*kv_pairs*/) {} + +extern "C" void k2_exit(int32_t /*exit_code*/) { + std::abort(); +} + +void runtime_error(const char* /*unused*/, ...) {} + +[[noreturn]] void critical_error_handler() { + std::abort(); +} + +[[noreturn]] void php_assert__(const char* /*unused*/, const char* /*unused*/, int /*unused*/) { + std::abort(); +} + +auto AllocatorState::get() noexcept -> const AllocatorState& { + static AllocatorState allocator_state{DEFAULT_ALLOCATOR_SIZE, DEFAULT_ALLOCATOR_SIZE, 0}; + return allocator_state; +} + +auto main() -> int { + test_routes_script_allocations_and_restores_default(); + test_nested_resources_restore_previous_target(); + test_zeroing_and_reallocation_use_replacement_resource(); + test_default_resource_can_request_extra_memory(); + RuntimeAllocator::get().free(); + return 0; +} diff --git a/tests/cpp/runtime-light/runtime-light-tests.cmake b/tests/cpp/runtime-light/runtime-light-tests.cmake index 2152a4c132..663fadcb76 100644 --- a/tests/cpp/runtime-light/runtime-light-tests.cmake +++ b/tests/cpp/runtime-light/runtime-light-tests.cmake @@ -9,3 +9,18 @@ target_compile_options(unittests-runtime-light-confdata PRIVATE ${RUNTIME_LIGHT_ target_link_options(unittests-runtime-light-confdata PRIVATE -stdlib=libc++) add_test(NAME unittests-runtime-light-confdata COMMAND unittests-runtime-light-confdata) set_target_properties(unittests-runtime-light-confdata PROPERTIES FOLDER tests) + +set(RUNTIME_LIGHT_ALLOCATOR_TEST_SOURCES + ${BASE_DIR}/runtime-common/core/memory-resource/details/memory_chunk_tree.cpp + ${BASE_DIR}/runtime-common/core/memory-resource/details/memory_ordered_chunk_list.cpp + ${BASE_DIR}/runtime-common/core/memory-resource/monotonic_buffer_resource.cpp + ${BASE_DIR}/runtime-common/core/memory-resource/unsynchronized_pool_resource.cpp + ${BASE_DIR}/runtime-light/allocator/runtime-light-allocator.cpp + ${BASE_DIR}/runtime-light/memory-resource-impl/monotonic-light-buffer-resource.cpp + ${BASE_DIR}/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp) + +add_executable(unittests-runtime-light-allocator ${RUNTIME_LIGHT_ALLOCATOR_TEST_SOURCES}) +target_compile_options(unittests-runtime-light-allocator PRIVATE ${RUNTIME_LIGHT_COMPILE_FLAGS}) +target_link_options(unittests-runtime-light-allocator PRIVATE -stdlib=libc++) +add_test(NAME unittests-runtime-light-allocator COMMAND unittests-runtime-light-allocator) +set_target_properties(unittests-runtime-light-allocator PROPERTIES FOLDER tests) From 27b7db181e654b01c802b94b16c4c55690e1b750 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 21 Aug 2026 16:21:38 +0300 Subject: [PATCH 4/7] add shared memory confdata storage --- runtime-light/allocator/allocator.h | 9 +-- .../components/confdata/confdata.cmake | 10 ++- .../confdata/state/confdata-storage.cpp | 72 +++++++++++++++++++ .../confdata/state/confdata-storage.h | 65 +++++++++++++++++ .../confdata/state/instance-state.cpp | 13 ++++ .../confdata/state/instance-state.h | 2 + runtime-light/k2-platform/k2-api.h | 26 +++++++ .../allocator/script-memory-resource-test.cpp | 56 +++++++++++++-- .../runtime-light/runtime-light-tests.cmake | 9 +-- 9 files changed, 244 insertions(+), 18 deletions(-) create mode 100644 runtime-light/components/confdata/state/confdata-storage.cpp create mode 100644 runtime-light/components/confdata/state/confdata-storage.h diff --git a/runtime-light/allocator/allocator.h b/runtime-light/allocator/allocator.h index 9307b86f9c..db7e2b301c 100644 --- a/runtime-light/allocator/allocator.h +++ b/runtime-light/allocator/allocator.h @@ -23,15 +23,16 @@ auto make_unique_on_script_memory(Args&&... args) noexcept { namespace kphp::memory { -// All script-allocated objects created by the callback must be destroyed before -// it returns. Keeping the operation synchronous and non-throwing ensures that -// the replacement cannot accidentally survive a suspension or stack unwind. +// Objects allocated by the callback may outlive it, but every later operation +// that can allocate or deallocate their memory must install the same resource. +// Keeping the operation synchronous and non-throwing prevents the replacement +// itself from surviving a suspension or stack unwind. template requires std::same_as, void> && std::is_nothrow_invocable_v void with_script_memory_resource(memory_resource::unsynchronized_pool_resource& resource, callback_type&& callback) noexcept { auto& allocator{RuntimeAllocator::get()}; const auto previous_resource{allocator.replace_script_memory_resource(resource)}; - const auto restore_resource{vk::finally([&allocator, &resource, previous_resource]() noexcept { + const auto restore_resource{vk::finally([&allocator, &resource, previous_resource] noexcept { kphp::log::assertion(std::addressof(allocator.current_script_memory_resource()) == std::addressof(resource)); static_cast(allocator.replace_script_memory_resource(previous_resource.get())); })}; diff --git a/runtime-light/components/confdata/confdata.cmake b/runtime-light/components/confdata/confdata.cmake index 7e181090e9..da23952e40 100644 --- a/runtime-light/components/confdata/confdata.cmake +++ b/runtime-light/components/confdata/confdata.cmake @@ -4,6 +4,7 @@ set(K2_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/confdata-storage.cpp ${RUNTIME_LIGHT_DIR}/components/confdata/state/instance-state.cpp ${RUNTIME_LIGHT_DIR}/components/confdata/state/predefined-wildcards-builder.cpp ${RUNTIME_LIGHT_DIR}/stdlib/confdata/confdata-keys.cpp @@ -21,18 +22,15 @@ set(K2_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_RUNTIME_CORE_SRC ${CORE_SRC}) +list(TRANSFORM K2_CONFDATA_RUNTIME_CORE_SRC PREPEND "${RUNTIME_COMMON_DIR}/") 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} + ${K2_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 $) diff --git a/runtime-light/components/confdata/state/confdata-storage.cpp b/runtime-light/components/confdata/state/confdata-storage.cpp new file mode 100644 index 0000000000..625c7444e3 --- /dev/null +++ b/runtime-light/components/confdata/state/confdata-storage.cpp @@ -0,0 +1,72 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#include "runtime-light/components/confdata/state/confdata-storage.h" + +#include +#include +#include +#include +#include + +#include "runtime-light/stdlib/diagnostics/logs.h" + +namespace kphp::confdata { + +struct alignas(std::max_align_t) storage::shared_state final { + memory_resource::unsynchronized_pool_resource resource{}; + alignas(map_type) std::byte map_storage[sizeof(map_type)]{}; +}; + +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}; + } + if (memory_limit > std::numeric_limits::max() - sizeof(shared_state)) [[unlikely]] { + return std::unexpected{storage_error::size_overflow}; + } + return sizeof(shared_state) + memory_limit; +} + +auto storage::init(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}; + } + + m_memory = memory; + m_state = std::construct_at(reinterpret_cast(m_memory.data())); + auto pool_memory{memory.subspan(sizeof(shared_state))}; + m_state->resource.init(pool_memory.data(), pool_memory.size()); + kphp::memory::with_script_memory_resource(m_state->resource, [this] noexcept { std::construct_at(reinterpret_cast(m_state->map_storage)); }); + return {}; +} + +auto storage::destroy() noexcept -> void { + kphp::log::assertion(is_initialized()); + kphp::memory::with_script_memory_resource(resource(), [this] noexcept { std::destroy_at(std::addressof(mutable_values())); }); + std::destroy_at(m_state); + m_state = nullptr; + m_memory = {}; +} + +auto storage::values() const noexcept -> const map_type& { + kphp::log::assertion(is_initialized()); + return *std::launder(reinterpret_cast(m_state->map_storage)); +} + +auto storage::mutable_values() noexcept -> map_type& { + return const_cast(values()); +} + +auto storage::resource() noexcept -> memory_resource::unsynchronized_pool_resource& { + kphp::log::assertion(is_initialized()); + return m_state->resource; +} + +} // namespace kphp::confdata diff --git a/runtime-light/components/confdata/state/confdata-storage.h b/runtime-light/components/confdata/state/confdata-storage.h new file mode 100644 index 0000000000..d4b185ccd1 --- /dev/null +++ b/runtime-light/components/confdata/state/confdata-storage.h @@ -0,0 +1,65 @@ +// 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 "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/allocator/allocator.h" + +namespace kphp::confdata { + +enum class storage_error : uint8_t { misaligned_buffer, insufficient_buffer, size_overflow }; + +class storage final : private vk::not_copyable { +public: + using map_type = kphp::stl::map; + + static constexpr auto memory_alignment() noexcept -> size_t { + return alignof(std::max_align_t); + } + + static auto memory_size(size_t memory_limit) noexcept -> std::expected; + + auto init(std::span memory) noexcept -> std::expected; + auto destroy() noexcept -> void; + + auto is_initialized() const noexcept -> bool { + return m_state != nullptr; + } + + auto memory() const noexcept -> std::span { + return m_memory; + } + + auto values() const noexcept -> const map_type&; + + template callback_type> + requires std::same_as, void> && std::is_nothrow_invocable_v + auto mutate(callback_type&& callback) noexcept -> void { + kphp::memory::with_script_memory_resource(resource(), [&callback, this] noexcept { std::invoke(std::forward(callback), mutable_values()); }); + } + +private: + struct shared_state; + + auto mutable_values() noexcept -> map_type&; + auto resource() noexcept -> memory_resource::unsynchronized_pool_resource&; + + shared_state* m_state{}; + std::span m_memory; +}; + +} // namespace kphp::confdata diff --git a/runtime-light/components/confdata/state/instance-state.cpp b/runtime-light/components/confdata/state/instance-state.cpp index 01510f1bb1..a87fee9b45 100644 --- a/runtime-light/components/confdata/state/instance-state.cpp +++ b/runtime-light/components/confdata/state/instance-state.cpp @@ -33,6 +33,19 @@ auto update_handler(std::span events) noexcept } // namespace auto InstanceState::init() noexcept -> void { + const auto shared_memory_size{kphp::confdata::storage::memory_size(ComponentState::get().m_confdata_memory_limit)}; + if (!shared_memory_size) [[unlikely]] { + kphp::log::error("invalid confdata shared memory size: error -> {}", std::to_underlying(shared_memory_size.error())); + } + auto shared_memory{k2::alloc_shared_memory(*shared_memory_size, kphp::confdata::storage::memory_alignment())}; + if (!shared_memory) [[unlikely]] { + kphp::log::error("failed to allocate confdata shared memory: error -> {}", shared_memory.error()); + } + auto initialized_storage{m_confdata_storage.init({static_cast(*shared_memory), *shared_memory_size})}; + if (!initialized_storage) [[unlikely]] { + kphp::log::error("failed to initialize confdata shared memory: error -> {}", std::to_underlying(initialized_storage.error())); + } + auto main_task{run()}; // initialize async stack auto& main_task_async_stack_frame{main_task.get_handle().promise().get_async_stack_frame()}; diff --git a/runtime-light/components/confdata/state/instance-state.h b/runtime-light/components/confdata/state/instance-state.h index 8bd4c66434..d33ad8b1b4 100644 --- a/runtime-light/components/confdata/state/instance-state.h +++ b/runtime-light/components/confdata/state/instance-state.h @@ -10,6 +10,7 @@ #include "common/mixin/not_copyable.h" #include "runtime-light/allocator/allocator-state.h" #include "runtime-light/components/confdata/confdata-proxy/sync-functions.h" +#include "runtime-light/components/confdata/state/confdata-storage.h" #include "runtime-light/coroutine/coroutine-state.h" #include "runtime-light/coroutine/io-scheduler.h" #include "runtime-light/coroutine/task.h" @@ -23,6 +24,7 @@ struct InstanceState final : vk::not_copyable { warmup_status m_warmup_status{warmup_status::pending}; kphp::confdata::pagination m_pagination{}; + kphp::confdata::storage m_confdata_storage; kphp::log::contextual_tags m_instance_tags; 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/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp b/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp index 89f87b3630..1305ff653d 100644 --- a/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp +++ b/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ #include "runtime-common/core/allocator/script-allocator.h" #include "runtime-light/allocator/allocator-state.h" #include "runtime-light/allocator/allocator.h" +#include "runtime-light/components/confdata/state/confdata-storage.h" #include "runtime-light/k2-platform/k2-api.h" namespace { @@ -73,7 +75,7 @@ auto test_routes_script_allocations_and_restores_default() noexcept -> void { auto& default_resource{allocator.memory_resource}; const auto default_memory_before{default_resource.get_memory_stats().memory_used}; - kphp::memory::with_script_memory_resource(shared_resource, [&]() noexcept { + kphp::memory::with_script_memory_resource(shared_resource, [&] noexcept { CHECK(std::addressof(allocator.current_script_memory_resource()) == std::addressof(shared_resource)); kphp::memory::script_allocator script_allocator{}; @@ -100,11 +102,11 @@ auto test_nested_resources_restore_previous_target() noexcept -> void { auto& allocator{RuntimeAllocator::get()}; kphp::memory::script_allocator script_allocator{}; - kphp::memory::with_script_memory_resource(outer_resource, [&]() noexcept { + kphp::memory::with_script_memory_resource(outer_resource, [&] noexcept { auto* outer_memory_before{script_allocator.allocate(32)}; CHECK(contains(outer_storage, outer_memory_before)); - kphp::memory::with_script_memory_resource(inner_resource, [&]() noexcept { + kphp::memory::with_script_memory_resource(inner_resource, [&] noexcept { auto* inner_memory{script_allocator.allocate(32)}; CHECK(contains(inner_storage, inner_memory)); script_allocator.deallocate(inner_memory, 32); @@ -128,7 +130,7 @@ auto test_zeroing_and_reallocation_use_replacement_resource() noexcept -> void { shared_resource.init(shared_storage.data(), shared_storage.size()); auto& allocator{RuntimeAllocator::get()}; - kphp::memory::with_script_memory_resource(shared_resource, [&]() noexcept { + kphp::memory::with_script_memory_resource(shared_resource, [&] noexcept { constexpr auto initial_size{static_cast(32)}; constexpr auto expanded_size{static_cast(128)}; @@ -156,6 +158,50 @@ auto test_default_resource_can_request_extra_memory() noexcept -> void { allocator.free_script_memory(memory, allocation_size); } +auto test_confdata_storage_persists_values_between_mutations() noexcept -> void { + alignas(std::max_align_t) std::array shared_storage{}; + kphp::confdata::storage storage{}; + CHECK(storage.init(shared_storage).has_value()); + CHECK(storage.memory().data() == shared_storage.data()); + const auto default_memory_before{RuntimeAllocator::get().memory_resource.get_memory_stats().memory_used}; + + storage.mutate([&shared_storage](kphp::confdata::storage::map_type& values) noexcept { + CHECK(contains(shared_storage, std::addressof(values))); + values.emplace(string{"persistent-key"}, mixed{string{"persistent-value"}}); + const auto& [key, value]{*values.begin()}; + CHECK(contains(shared_storage, std::addressof(*values.begin()))); + CHECK(contains(shared_storage, key.c_str())); + CHECK(value.is_string()); + CHECK(contains(shared_storage, value.as_string().c_str())); + }); + + CHECK(storage.values().size() == 1); + CHECK(RuntimeAllocator::get().memory_resource.get_memory_stats().memory_used == default_memory_before); + storage.mutate([](kphp::confdata::storage::map_type& values) noexcept { values.clear(); }); + CHECK(storage.values().empty()); + storage.destroy(); + CHECK(!storage.is_initialized()); + CHECK(storage.memory().empty()); +} + +auto test_confdata_storage_rejects_invalid_memory() noexcept -> void { + alignas(std::max_align_t) std::array memory{}; + + kphp::confdata::storage misaligned_storage{}; + const auto misaligned{misaligned_storage.init(std::span{memory}.subspan(1))}; + CHECK(!misaligned.has_value()); + CHECK(misaligned.error() == kphp::confdata::storage_error::misaligned_buffer); + + kphp::confdata::storage undersized_storage{}; + const auto undersized{undersized_storage.init(std::span{memory}.first(1))}; + CHECK(!undersized.has_value()); + CHECK(undersized.error() == kphp::confdata::storage_error::insufficient_buffer); + + const auto overflow{kphp::confdata::storage::memory_size(std::numeric_limits::max())}; + CHECK(!overflow.has_value()); + CHECK(overflow.error() == kphp::confdata::storage_error::size_overflow); +} + } // namespace extern "C" void* k2_alloc(size_t size, size_t align) { @@ -198,6 +244,8 @@ auto main() -> int { test_nested_resources_restore_previous_target(); test_zeroing_and_reallocation_use_replacement_resource(); test_default_resource_can_request_extra_memory(); + test_confdata_storage_persists_values_between_mutations(); + test_confdata_storage_rejects_invalid_memory(); RuntimeAllocator::get().free(); return 0; } diff --git a/tests/cpp/runtime-light/runtime-light-tests.cmake b/tests/cpp/runtime-light/runtime-light-tests.cmake index 663fadcb76..db8f750402 100644 --- a/tests/cpp/runtime-light/runtime-light-tests.cmake +++ b/tests/cpp/runtime-light/runtime-light-tests.cmake @@ -10,12 +10,13 @@ target_link_options(unittests-runtime-light-confdata PRIVATE -stdlib=libc++) add_test(NAME unittests-runtime-light-confdata COMMAND unittests-runtime-light-confdata) set_target_properties(unittests-runtime-light-confdata PROPERTIES FOLDER tests) +set(RUNTIME_LIGHT_ALLOCATOR_TEST_RUNTIME_CORE_SOURCES ${CORE_SRC}) +list(TRANSFORM RUNTIME_LIGHT_ALLOCATOR_TEST_RUNTIME_CORE_SOURCES PREPEND "${RUNTIME_COMMON_DIR}/") + set(RUNTIME_LIGHT_ALLOCATOR_TEST_SOURCES - ${BASE_DIR}/runtime-common/core/memory-resource/details/memory_chunk_tree.cpp - ${BASE_DIR}/runtime-common/core/memory-resource/details/memory_ordered_chunk_list.cpp - ${BASE_DIR}/runtime-common/core/memory-resource/monotonic_buffer_resource.cpp - ${BASE_DIR}/runtime-common/core/memory-resource/unsynchronized_pool_resource.cpp + ${RUNTIME_LIGHT_ALLOCATOR_TEST_RUNTIME_CORE_SOURCES} ${BASE_DIR}/runtime-light/allocator/runtime-light-allocator.cpp + ${BASE_DIR}/runtime-light/components/confdata/state/confdata-storage.cpp ${BASE_DIR}/runtime-light/memory-resource-impl/monotonic-light-buffer-resource.cpp ${BASE_DIR}/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp) From be0b24e22c4a0d696b1a896fec299a54fbe74fd9 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Thu, 27 Aug 2026 13:00:04 +0300 Subject: [PATCH 5/7] add memory_resource::stl::forward_list --- runtime-common/core/memory-resource/resource_allocator.h | 4 ++++ 1 file changed, 4 insertions(+) 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 From 75a7c16788c00d87c16863bf50f4c63707d41bce Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Thu, 27 Aug 2026 17:15:52 +0300 Subject: [PATCH 6/7] reviewed --- .../confdata/confdata-component.cpp | 2 +- .../confdata/state/component-state.cpp | 24 +- .../confdata/state/component-state.h | 10 +- .../stdlib/confdata/confdata-constants.h | 9 +- runtime-light/stdlib/confdata/confdata-keys.h | 38 +-- .../stdlib/confdata/confdata-reader-lease.h | 75 ++++++ .../stdlib/confdata/predefined-wildcards.cpp | 238 ++++-------------- .../stdlib/confdata/predefined-wildcards.h | 154 ++++++++---- runtime-light/stdlib/confdata/wildcard-kind.h | 41 +++ 9 files changed, 318 insertions(+), 273 deletions(-) create mode 100644 runtime-light/stdlib/confdata/confdata-reader-lease.h create mode 100644 runtime-light/stdlib/confdata/wildcard-kind.h diff --git a/runtime-light/components/confdata/confdata-component.cpp b/runtime-light/components/confdata/confdata-component.cpp index 4f7761cd05..367dbce7b6 100644 --- a/runtime-light/components/confdata/confdata-component.cpp +++ b/runtime-light/components/confdata/confdata-component.cpp @@ -86,7 +86,7 @@ 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 ImageInfo image_info{.image_name = kphp::confdata::IMAGE_NAME.data(), .is_oneshot = 0, .build_timestamp = K2_CONFDATA_BUILD_TIMESTAMP, .header_h_version = K2_PLATFORM_HEADER_H_VERSION, diff --git a/runtime-light/components/confdata/state/component-state.cpp b/runtime-light/components/confdata/state/component-state.cpp index 4e47293b31..225571c3aa 100644 --- a/runtime-light/components/confdata/state/component-state.cpp +++ b/runtime-light/components/confdata/state/component-state.cpp @@ -11,8 +11,8 @@ #include #include -#include "runtime-light/components/confdata/state/predefined-wildcards-builder.h" #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 { @@ -63,6 +63,24 @@ auto ComponentState::parse_predefined_wildcards_arg(std::string_view value_view) 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) { const auto [arg_key, arg_value]{k2::arg_fetch(i)}; @@ -75,6 +93,10 @@ auto ComponentState::parse_args() noexcept -> void { 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 3ea0f49c4f..4ac3a06b8c 100644 --- a/runtime-light/components/confdata/state/component-state.h +++ b/runtime-light/components/confdata/state/component-state.h @@ -27,6 +27,8 @@ struct ComponentState final : private vk::not_copyable { size_t m_confdata_memory_limit{}; 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}; ComponentState() noexcept; static auto get() noexcept -> const ComponentState&; @@ -36,12 +38,18 @@ struct ComponentState final : private vk::not_copyable { auto parse_confdata_memory_limit_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_MEMORY_LIMIT_ARG{"confdata-memory-limit"}; 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 auto INIT_COMPONENT_ALLOCATOR_SIZE{static_cast(1024U * 1024U)}; // 1MiB + 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_INIT_INSTANCE_ALLOCATOR_SIZE{static_cast(64U * 1024U * 1024U)}; // 64MiB + static constexpr auto DEFAULT_MIN_INSTANCE_EXTRA_MEMORY_SIZE{64U * 1024U * 1024U}; // 64MiB }; inline ComponentState::ComponentState() noexcept { diff --git a/runtime-light/stdlib/confdata/confdata-constants.h b/runtime-light/stdlib/confdata/confdata-constants.h index d2f3d2b5a4..1cfebe677b 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{"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{"confdata"}; + +inline constexpr std::string_view SHARED_MEMORY_NAME{"#kphp-confdata"}; } // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/confdata-keys.h b/runtime-light/stdlib/confdata/confdata-keys.h index 8d69eb6c57..e12d70ae0d 100644 --- a/runtime-light/stdlib/confdata/confdata-keys.h +++ b/runtime-light/stdlib/confdata/confdata-keys.h @@ -16,6 +16,7 @@ #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. @@ -28,39 +29,15 @@ // - "predefined..." -> section = the matching predefined wildcard (section_kind::predefined_wildcard) namespace kphp::confdata { -enum class section_kind : uint8_t { simple_key, one_dot_wildcard, two_dots_wildcard, predefined_wildcard }; - -inline auto classify_wildcard(std::string_view wildcard) noexcept -> section_kind { - size_t dots{0}; - if (!wildcard.empty() && wildcard.back() == '.') { - for (const char c : wildcard) { - dots += (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; - } -} - /** * @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(section)}; + 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 }; /** @@ -68,8 +45,7 @@ enum class split_error : uint8_t { key_too_long, invalid_predefined_wildcard_len * Instances are produced only by the `split_key*` factories, so every `key_views` * is guaranteed to satisfy the protocol length bound (`int16_t`). */ -class key_views { -public: +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; @@ -86,6 +62,8 @@ class key_views { 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; @@ -171,14 +149,12 @@ class key_handles : vk::not_copyable { // NOLINT(*member-init) auto remainder() const noexcept -> const mixed&; /** - * @return A heap copy of the section; the internal section aliases the stack buffer and the raw key, - * so it must not escape the handles object. + * @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 and the raw key, - * so it must not escape the handles object. + * @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; }; 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/predefined-wildcards.cpp b/runtime-light/stdlib/confdata/predefined-wildcards.cpp index 85278fc10f..5a2ad73ea6 100644 --- a/runtime-light/stdlib/confdata/predefined-wildcards.cpp +++ b/runtime-light/stdlib/confdata/predefined-wildcards.cpp @@ -6,223 +6,95 @@ #include #include -#include #include -#include +#include #include #include -#include -#include "runtime-light/stdlib/confdata/detail/predefined-wildcards-layout.h" - -namespace { - -using metadata_header = kphp::confdata::detail::predefined_wildcards_metadata_header; -using wildcard_entry = kphp::confdata::detail::predefined_wildcard_entry; -using wildcard_group = kphp::confdata::detail::predefined_wildcard_group; - -auto is_valid_predefined_wildcard(std::string_view wildcard) noexcept -> bool { - if (wildcard.empty() || wildcard.size() > kphp::confdata::MAX_KEY_LENGTH) [[unlikely]] { - return false; - } +namespace kphp::confdata { - size_t dots{}; - if (wildcard.back() == '.') { - for (const char c : wildcard) { - dots += (c == '.'); - if (dots > 2) { - break; - } +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 dots != 1 && dots != 2; + return std::nullopt; } -auto load_wildcard(const std::byte* data, const metadata_header& header, uint32_t index) noexcept -> std::string_view { - const auto entry{kphp::confdata::detail::load(data, header.entries_offset + static_cast(index) * sizeof(wildcard_entry))}; - return {reinterpret_cast(data + entry.string_offset), entry.string_size}; +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 load_group(const std::byte* data, const metadata_header& header, uint32_t index) noexcept -> wildcard_group { - return kphp::confdata::detail::load(data, header.groups_offset + static_cast(index) * sizeof(wildcard_group)); +auto predefined_wildcards::has_matching_wildcard(std::string_view key) const noexcept -> bool { + return shortest_matching_wildcard(key).has_value(); } -auto has_valid_layout(size_t buffer_size, const metadata_header& header) noexcept -> bool { - // Recompute all offsets instead of trusting the serialized ones. This also - // proves that every subsequent fixed-size load fits in `header.total_size`. - const auto layout{kphp::confdata::detail::calculate_predefined_wildcards_metadata_layout(header.wildcard_count, header.group_count, header.strings_size, - header.shortest_wildcard_size, header.max_matches_per_key)}; - if (!layout || header.total_size != layout->total_size || header.entries_offset != layout->entries_offset || header.groups_offset != layout->groups_offset || - header.strings_offset != layout->strings_offset || header.total_size > buffer_size) [[unlikely]] { - return false; - } - - if (header.wildcard_count == 0) { - return header.group_count == 0 && header.strings_size == 0 && header.shortest_wildcard_size == 0 && header.max_matches_per_key == 0; +auto predefined_wildcards::initialize(std::span wildcards) noexcept -> std::expected { + if (m_initialized) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::already_initialized}; } - return header.group_count != 0 && header.shortest_wildcard_size != 0 && header.max_matches_per_key != 0; -} - -auto has_valid_entries(const std::byte* data, const metadata_header& header) noexcept -> bool { - size_t next_string_offset{header.strings_offset}; - size_t shortest_wildcard_size{std::numeric_limits::max()}; - std::string_view previous_wildcard{}; - - for (uint32_t i{}; i < header.wildcard_count; ++i) { - const auto entry{kphp::confdata::detail::load(data, header.entries_offset + static_cast(i) * sizeof(wildcard_entry))}; - const auto string_end{kphp::confdata::detail::checked_add(entry.string_offset, entry.string_size)}; - // Strings are packed without gaps. Besides enforcing a canonical encoding, - // this prevents entries from overlapping or referring outside the blob. - if (entry.string_offset != next_string_offset || !string_end || *string_end > header.total_size) [[unlikely]] { - return false; + 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()}; } - - const std::string_view wildcard{reinterpret_cast(data + entry.string_offset), entry.string_size}; - if (!is_valid_predefined_wildcard(wildcard) || (i != 0 && previous_wildcard >= wildcard)) [[unlikely]] { - return false; + if (!first && previous >= wildcard) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::non_canonical_wildcards}; } - next_string_offset = *string_end; - shortest_wildcard_size = std::min(shortest_wildcard_size, wildcard.size()); - previous_wildcard = wildcard; + previous = wildcard; + first = false; } - return next_string_offset == header.total_size && (header.wildcard_count == 0 || shortest_wildcard_size == header.shortest_wildcard_size); -} - -auto has_valid_groups(const std::byte* data, const metadata_header& header) noexcept -> bool { - uint32_t next_entry{}; - size_t max_matches_per_key{}; - std::string_view previous_prefix{}; - - for (uint32_t i{}; i < header.group_count; ++i) { - const auto group{load_group(data, header, i)}; - if (group.first_entry != next_entry || group.entry_count == 0 || group.entry_count > header.wildcard_count - group.first_entry) [[unlikely]] { - return 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}; } - // A group is exactly one run of wildcards sharing a prefix whose length is - // the shortest wildcard size. These prefixes form the lookup index. - const auto group_prefix{load_wildcard(data, header, group.first_entry).substr(0, header.shortest_wildcard_size)}; - if (i != 0 && previous_prefix >= group_prefix) [[unlikely]] { - return false; + const std::string_view stored{*it}; + if (m_shortest_wildcard_size == 0 || stored.size() < m_shortest_wildcard_size) { + m_shortest_wildcard_size = stored.size(); } + } - for (uint32_t j{}; j < group.entry_count; ++j) { - const auto wildcard{load_wildcard(data, header, group.first_entry + j)}; - if (wildcard.substr(0, header.shortest_wildcard_size) != group_prefix) [[unlikely]] { - return false; - } + 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); + } - size_t matches{1}; - for (uint32_t k{}; k < j; ++k) { - matches += wildcard.starts_with(load_wildcard(data, header, group.first_entry + k)) ? 1 : 0; + 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])); } - max_matches_per_key = std::max(max_matches_per_key, matches); + m_max_matches_per_key = std::max(m_max_matches_per_key, matches); } - next_entry += group.entry_count; - previous_prefix = group_prefix; } - return next_entry == header.wildcard_count && max_matches_per_key == header.max_matches_per_key; + m_initialized = true; + return {}; } -} // namespace - -namespace kphp::confdata { - -predefined_wildcards::predefined_wildcards(const std::byte* data, uint32_t entries_offset, uint32_t groups_offset, uint32_t wildcard_count, - uint32_t group_count, uint32_t shortest_wildcard_size, uint32_t max_matches_per_key) noexcept - : m_data{data}, - m_entries_offset{entries_offset}, - m_groups_offset{groups_offset}, - m_wildcard_count{wildcard_count}, - m_group_count{group_count}, - m_shortest_wildcard_size{shortest_wildcard_size}, - m_max_matches_per_key{max_matches_per_key} {} - -auto predefined_wildcards::wildcard_at(uint32_t index) const noexcept -> std::string_view { - const auto entry{ - detail::load(m_data, m_entries_offset + static_cast(index) * sizeof(detail::predefined_wildcard_entry))}; - return {reinterpret_cast(m_data + entry.string_offset), entry.string_size}; -} - -auto predefined_wildcards::matching_group(std::string_view key) const noexcept -> std::pair { - if (m_group_count == 0 || key.size() < m_shortest_wildcard_size) { - return {}; - } - - const auto key_prefix{key.substr(0, m_shortest_wildcard_size)}; - uint32_t first{}; - uint32_t last{m_group_count}; - while (first < last) { - const uint32_t middle{first + (last - first) / 2}; - const auto group{ - detail::load(m_data, m_groups_offset + static_cast(middle) * sizeof(detail::predefined_wildcard_group))}; - const auto group_prefix{wildcard_at(group.first_entry).substr(0, m_shortest_wildcard_size)}; - if (group_prefix < key_prefix) { - first = middle + 1; - } else { - last = middle; - } - } - if (first == m_group_count) { +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{ - detail::load(m_data, m_groups_offset + static_cast(first) * sizeof(detail::predefined_wildcard_group))}; - if (wildcard_at(group.first_entry).substr(0, m_shortest_wildcard_size) != key_prefix) { + const auto group_it{m_groups.find(key.substr(0, m_shortest_wildcard_size))}; + if (group_it == m_groups.end()) { return {}; } - return {group.first_entry, group.entry_count}; -} - -auto predefined_wildcards::contains(std::string_view wildcard) const noexcept -> bool { - uint32_t first{}; - uint32_t last{m_wildcard_count}; - while (first < last) { - const uint32_t middle{first + (last - first) / 2}; - if (wildcard_at(middle) < wildcard) { - first = middle + 1; - } else { - last = middle; - } - } - return first != m_wildcard_count && wildcard_at(first) == wildcard; -} - -auto predefined_wildcards::is_top_level_wildcard(std::string_view wildcard) const noexcept -> bool { - if (!contains(wildcard)) { - return false; - } - size_t matches{}; - for_each_matching_wildcard(wildcard, [&matches](std::string_view /*unused*/) noexcept { ++matches; }); - return matches == 1; -} - -auto open_predefined_wildcards(std::span buffer) noexcept -> std::expected { - if (!detail::is_predefined_wildcards_metadata_aligned(buffer.data())) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::misaligned_buffer}; - } - if (buffer.size() < sizeof(detail::predefined_wildcards_metadata_header)) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::invalid_metadata}; - } - - const auto header{detail::load(buffer.data(), 0)}; - if (header.magic != detail::PREDEFINED_WILDCARDS_METADATA_MAGIC) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::invalid_metadata}; - } - if (header.version != detail::PREDEFINED_WILDCARDS_METADATA_VERSION) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::unsupported_version}; - } - if (!has_valid_layout(buffer.size(), header)) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::invalid_metadata}; - } - if (!has_valid_entries(buffer.data(), header) || !has_valid_groups(buffer.data(), header)) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::invalid_metadata}; - } - return predefined_wildcards{buffer.data(), header.entries_offset, header.groups_offset, header.wildcard_count, - header.group_count, header.shortest_wildcard_size, header.max_matches_per_key}; + 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 index 55bd84e02a..1b38ba403b 100644 --- a/runtime-light/stdlib/confdata/predefined-wildcards.h +++ b/runtime-light/stdlib/confdata/predefined-wildcards.h @@ -14,35 +14,99 @@ #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())}; -inline constexpr size_t PREDEFINED_WILDCARDS_ALIGNMENT{alignof(uint64_t)}; enum class predefined_wildcards_error : uint8_t { empty_wildcard, wildcard_too_long, reserved_wildcard, non_canonical_wildcards, - size_overflow, - insufficient_buffer, - misaligned_buffer, - invalid_metadata, - unsupported_version, + already_initialized, + internal, }; -class predefined_wildcards final { +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: - predefined_wildcards() noexcept = default; + explicit predefined_wildcards(resource_type& resource) noexcept; /** - * @brief Invokes `f(wildcard)` for every configured wildcard that is a prefix of `key`. - * Matching wildcards are visited in ascending length order. + * 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 -> void; + 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; @@ -60,53 +124,39 @@ class predefined_wildcards final { auto has_matching_wildcard(std::string_view key) const noexcept -> bool; private: - const std::byte* m_data{}; - uint32_t m_entries_offset{}; - uint32_t m_groups_offset{}; - uint32_t m_wildcard_count{}; - uint32_t m_group_count{}; - uint32_t m_shortest_wildcard_size{}; - uint32_t m_max_matches_per_key{}; + /** Initializes the index from sorted, unique wildcards under the storage resource. */ + auto initialize(std::span wildcards) noexcept -> std::expected; - predefined_wildcards(const std::byte* data, uint32_t entries_offset, uint32_t groups_offset, uint32_t wildcard_count, uint32_t group_count, - uint32_t shortest_wildcard_size, uint32_t max_matches_per_key) noexcept; + auto find_matching_candidates(std::string_view key) const noexcept -> matching_candidates; - auto wildcard_at(uint32_t index) const noexcept -> std::string_view; - auto matching_group(std::string_view key) const noexcept -> std::pair; - - friend auto open_predefined_wildcards(std::span) noexcept -> std::expected; + friend class storage; }; -/** @brief Validates and opens relocatable immutable wildcard metadata. */ -auto open_predefined_wildcards(std::span buffer) noexcept -> std::expected; +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 -> void { - const auto [first_entry, entry_count]{matching_group(key)}; - for (uint32_t i{0}; i < entry_count; ++i) { - const auto wildcard{wildcard_at(first_entry + i)}; - if (wildcard.size() <= key.size() && key.starts_with(wildcard)) { +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; } } -} - -inline auto predefined_wildcards::shortest_matching_wildcard(std::string_view key) const noexcept -> std::optional { - std::optional result{}; - for_each_matching_wildcard(key, [&result](std::string_view wildcard) noexcept { - if (!result.has_value()) { - result = wildcard; - } - }); - return result; + return matched; } inline auto predefined_wildcards::max_matches_per_key() const noexcept -> size_t { return m_max_matches_per_key; } -inline auto predefined_wildcards::has_matching_wildcard(std::string_view key) const noexcept -> bool { - return shortest_matching_wildcard(key).has_value(); +inline auto predefined_wildcards::contains(std::string_view wildcard) const noexcept -> bool { + return m_wildcards.contains(wildcard); } } // namespace kphp::confdata @@ -131,17 +181,11 @@ struct std::formatter { 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::size_overflow: - return std::format_to(ctx.out(), "metadata size overflow"); - case predefined_wildcards_error::insufficient_buffer: - return std::format_to(ctx.out(), "insufficient metadata buffer"); - case predefined_wildcards_error::misaligned_buffer: - return std::format_to(ctx.out(), "misaligned metadata buffer"); - case predefined_wildcards_error::invalid_metadata: - return std::format_to(ctx.out(), "invalid wildcard metadata"); - case predefined_wildcards_error::unsupported_version: - return std::format_to(ctx.out(), "unsupported wildcard metadata version"); + 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 metadata 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 From edfd3c141ada13e74a1f472254dd7c67ac8bb716 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 28 Aug 2026 17:39:28 +0300 Subject: [PATCH 7/7] tmp --- .../kphp-light/stdlib/confdata-functions.txt | 3 - runtime-light/allocator/allocator.h | 3 +- .../confdata/confdata-proxy/sync-functions.h | 13 +- .../components/confdata/confdata.cmake | 8 +- .../confdata/state/confdata-storage.cpp | 72 -- .../confdata/state/confdata-storage.h | 65 -- .../confdata/state/instance-state.cpp | 291 ++++++++- .../confdata/state/instance-state.h | 79 ++- .../state/predefined-wildcards-builder.cpp | 167 ----- .../state/predefined-wildcards-builder.h | 28 - .../components/kphp/state/instance-state.cpp | 2 + runtime-light/k2-platform/k2-api.h | 7 + runtime-light/k2-platform/k2-header.h | 28 +- .../stdlib/confdata/confdata-functions.cpp | 219 ++++--- .../stdlib/confdata/confdata-functions.h | 16 +- .../stdlib/confdata/confdata-state.cpp | 69 ++ .../stdlib/confdata/confdata-state.h | 31 +- .../stdlib/confdata/confdata-storage.cpp | 617 ++++++++++++++++++ .../stdlib/confdata/confdata-storage.h | 226 +++++++ .../detail/predefined-wildcards-layout.h | 115 ---- runtime-light/stdlib/stdlib.cmake | 2 + .../allocator/script-memory-resource-test.cpp | 365 ++++++++++- .../confdata/predefined-wildcards-test.cpp | 176 +++-- .../runtime-light/runtime-light-tests.cmake | 14 +- 24 files changed, 1934 insertions(+), 682 deletions(-) delete mode 100644 runtime-light/components/confdata/state/confdata-storage.cpp delete mode 100644 runtime-light/components/confdata/state/confdata-storage.h delete mode 100644 runtime-light/components/confdata/state/predefined-wildcards-builder.cpp delete mode 100644 runtime-light/components/confdata/state/predefined-wildcards-builder.h create mode 100644 runtime-light/stdlib/confdata/confdata-state.cpp create mode 100644 runtime-light/stdlib/confdata/confdata-storage.cpp create mode 100644 runtime-light/stdlib/confdata/confdata-storage.h delete mode 100644 runtime-light/stdlib/confdata/detail/predefined-wildcards-layout.h 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/runtime-light/allocator/allocator.h b/runtime-light/allocator/allocator.h index db7e2b301c..9ab181ca77 100644 --- a/runtime-light/allocator/allocator.h +++ b/runtime-light/allocator/allocator.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -34,7 +35,7 @@ void with_script_memory_resource(memory_resource::unsynchronized_pool_resource& const auto previous_resource{allocator.replace_script_memory_resource(resource)}; const auto restore_resource{vk::finally([&allocator, &resource, previous_resource] noexcept { kphp::log::assertion(std::addressof(allocator.current_script_memory_resource()) == std::addressof(resource)); - static_cast(allocator.replace_script_memory_resource(previous_resource.get())); + std::ignore = allocator.replace_script_memory_resource(previous_resource.get()); })}; std::invoke(std::forward(callback)); } diff --git a/runtime-light/components/confdata/confdata-proxy/sync-functions.h b/runtime-light/components/confdata/confdata-proxy/sync-functions.h index d6e0d49bb7..e6401aca02 100644 --- a/runtime-light/components/confdata/confdata-proxy/sync-functions.h +++ b/runtime-light/components/confdata/confdata-proxy/sync-functions.h @@ -34,15 +34,16 @@ struct pagination { bool m_has_synced{}; }; -enum class subscribe_error : uint8_t { transport, old_offset, malformed_response, not_synced }; +enum class subscribe_error : uint8_t { transport, old_offset, malformed_response, not_synced, storage_busy }; 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 `storage_busy` 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> +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> { // subscribe is a longpoll method, so the timeout must cover the time confdata-proxy may hold the request open @@ -91,7 +92,9 @@ auto subscribe(std::string_view confdata_proxy_actor, kphp::confdata::pagination 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}); + if (!std::invoke(event_handler, std::span{events.value})) { + return std::unexpected{kphp::confdata::subscribe_error::storage_busy}; + } } to.m_page = response.new_page.value; @@ -113,7 +116,7 @@ auto subscribe(std::string_view confdata_proxy_actor, kphp::confdata::pagination // // `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 sync(std::string_view confdata_proxy_actor, event_handler_type event_handler) noexcept -> kphp::coro::task> { kphp::confdata::pagination p{}; @@ -132,7 +135,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 da23952e40..c19c7438d9 100644 --- a/runtime-light/components/confdata/confdata.cmake +++ b/runtime-light/components/confdata/confdata.cmake @@ -4,9 +4,8 @@ set(K2_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/confdata-storage.cpp ${RUNTIME_LIGHT_DIR}/components/confdata/state/instance-state.cpp - ${RUNTIME_LIGHT_DIR}/components/confdata/state/predefined-wildcards-builder.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) @@ -22,6 +21,10 @@ set(K2_CONFDATA_DIAGNOSTICS_SRC ${RUNTIME_LIGHT_DIR}/stdlib/diagnostics/backtrace.cpp ${RUNTIME_LIGHT_DIR}/stdlib/diagnostics/php-assert.cpp) +set(K2_CONFDATA_SERIALIZATION_SRC + ${RUNTIME_COMMON_DIR}/stdlib/serialization/json-functions.cpp + ${RUNTIME_COMMON_DIR}/stdlib/serialization/serialize-functions.cpp) + set(K2_CONFDATA_RUNTIME_CORE_SRC ${CORE_SRC}) list(TRANSFORM K2_CONFDATA_RUNTIME_CORE_SRC PREPEND "${RUNTIME_COMMON_DIR}/") @@ -30,6 +33,7 @@ set(K2_CONFDATA_SRC ${K2_CONFDATA_TL_SRC} ${K2_CONFDATA_ALLOCATOR_SRC} ${K2_CONFDATA_DIAGNOSTICS_SRC} + ${K2_CONFDATA_SERIALIZATION_SRC} ${K2_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 diff --git a/runtime-light/components/confdata/state/confdata-storage.cpp b/runtime-light/components/confdata/state/confdata-storage.cpp deleted file mode 100644 index 625c7444e3..0000000000 --- a/runtime-light/components/confdata/state/confdata-storage.cpp +++ /dev/null @@ -1,72 +0,0 @@ -// Compiler for PHP (aka KPHP) -// Copyright (c) 2026 LLC «V Kontakte» -// Distributed under the GPL v3 License, see LICENSE.notice.txt - -#include "runtime-light/components/confdata/state/confdata-storage.h" - -#include -#include -#include -#include -#include - -#include "runtime-light/stdlib/diagnostics/logs.h" - -namespace kphp::confdata { - -struct alignas(std::max_align_t) storage::shared_state final { - memory_resource::unsynchronized_pool_resource resource{}; - alignas(map_type) std::byte map_storage[sizeof(map_type)]{}; -}; - -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}; - } - if (memory_limit > std::numeric_limits::max() - sizeof(shared_state)) [[unlikely]] { - return std::unexpected{storage_error::size_overflow}; - } - return sizeof(shared_state) + memory_limit; -} - -auto storage::init(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}; - } - - m_memory = memory; - m_state = std::construct_at(reinterpret_cast(m_memory.data())); - auto pool_memory{memory.subspan(sizeof(shared_state))}; - m_state->resource.init(pool_memory.data(), pool_memory.size()); - kphp::memory::with_script_memory_resource(m_state->resource, [this] noexcept { std::construct_at(reinterpret_cast(m_state->map_storage)); }); - return {}; -} - -auto storage::destroy() noexcept -> void { - kphp::log::assertion(is_initialized()); - kphp::memory::with_script_memory_resource(resource(), [this] noexcept { std::destroy_at(std::addressof(mutable_values())); }); - std::destroy_at(m_state); - m_state = nullptr; - m_memory = {}; -} - -auto storage::values() const noexcept -> const map_type& { - kphp::log::assertion(is_initialized()); - return *std::launder(reinterpret_cast(m_state->map_storage)); -} - -auto storage::mutable_values() noexcept -> map_type& { - return const_cast(values()); -} - -auto storage::resource() noexcept -> memory_resource::unsynchronized_pool_resource& { - kphp::log::assertion(is_initialized()); - return m_state->resource; -} - -} // namespace kphp::confdata diff --git a/runtime-light/components/confdata/state/confdata-storage.h b/runtime-light/components/confdata/state/confdata-storage.h deleted file mode 100644 index d4b185ccd1..0000000000 --- a/runtime-light/components/confdata/state/confdata-storage.h +++ /dev/null @@ -1,65 +0,0 @@ -// 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 "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/allocator/allocator.h" - -namespace kphp::confdata { - -enum class storage_error : uint8_t { misaligned_buffer, insufficient_buffer, size_overflow }; - -class storage final : private vk::not_copyable { -public: - using map_type = kphp::stl::map; - - static constexpr auto memory_alignment() noexcept -> size_t { - return alignof(std::max_align_t); - } - - static auto memory_size(size_t memory_limit) noexcept -> std::expected; - - auto init(std::span memory) noexcept -> std::expected; - auto destroy() noexcept -> void; - - auto is_initialized() const noexcept -> bool { - return m_state != nullptr; - } - - auto memory() const noexcept -> std::span { - return m_memory; - } - - auto values() const noexcept -> const map_type&; - - template callback_type> - requires std::same_as, void> && std::is_nothrow_invocable_v - auto mutate(callback_type&& callback) noexcept -> void { - kphp::memory::with_script_memory_resource(resource(), [&callback, this] noexcept { std::invoke(std::forward(callback), mutable_values()); }); - } - -private: - struct shared_state; - - auto mutable_values() noexcept -> map_type&; - auto resource() noexcept -> memory_resource::unsynchronized_pool_resource&; - - shared_state* m_state{}; - std::span m_memory; -}; - -} // namespace kphp::confdata diff --git a/runtime-light/components/confdata/state/instance-state.cpp b/runtime-light/components/confdata/state/instance-state.cpp index a87fee9b45..d012be19de 100644 --- a/runtime-light/components/confdata/state/instance-state.cpp +++ b/runtime-light/components/confdata/state/instance-state.cpp @@ -7,45 +7,183 @@ #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}}; -auto update_handler(std::span events) noexcept -> void { - kphp::log::info("got {} events on update", events.size()); +// 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())}; } } // namespace -auto InstanceState::init() noexcept -> void { - const auto shared_memory_size{kphp::confdata::storage::memory_size(ComponentState::get().m_confdata_memory_limit)}; +template<> +struct std::formatter { + template + constexpr auto parse(parse_context_type& ctx) const noexcept { + return ctx.begin(); + } + + template + auto format(const InstanceState::confdata_piece_creation_error& error, format_context_type& ctx) const noexcept { + using stage = InstanceState::confdata_piece_creation_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; + } + 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 (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, std::span predefined_wildcards) noexcept + -> std::expected { + using creation_stage = confdata_piece_creation_error::stage; + + kphp::log::assertion(owner.empty()); + + const auto shared_memory_size{kphp::confdata::storage::memory_size(memory_limit)}; if (!shared_memory_size) [[unlikely]] { - kphp::log::error("invalid confdata shared memory size: error -> {}", std::to_underlying(shared_memory_size.error())); + return std::unexpected{ + confdata_piece_creation_error{.m_stage = creation_stage::memory_size, .m_code = static_cast(std::to_underlying(shared_memory_size.error()))}}; } - auto shared_memory{k2::alloc_shared_memory(*shared_memory_size, kphp::confdata::storage::memory_alignment())}; + + const auto shared_memory{k2::alloc_shared_memory(*shared_memory_size, kphp::confdata::storage::memory_alignment())}; if (!shared_memory) [[unlikely]] { - kphp::log::error("failed to allocate confdata shared memory: error -> {}", shared_memory.error()); + return std::unexpected{confdata_piece_creation_error{.m_stage = creation_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})}; !initialized) [[unlikely]] { + const confdata_piece_creation_error error{.m_stage = creation_stage::storage_initialization, + .m_code = static_cast(std::to_underlying(initialized.error()))}; + owner.erase(piece_it); + return std::unexpected{error}; } - auto initialized_storage{m_confdata_storage.init({static_cast(*shared_memory), *shared_memory_size})}; - if (!initialized_storage) [[unlikely]] { - kphp::log::error("failed to initialize confdata shared memory: error -> {}", std::to_underlying(initialized_storage.error())); + if (const auto initialized{piece_it->m_storage.initialize_wildcards(predefined_wildcards)}; !initialized) [[unlikely]] { + const confdata_piece_creation_error error{.m_stage = creation_stage::wildcard_initialization, + .m_code = static_cast(std::to_underlying(initialized.error()))}; + 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 auto& main_task_async_stack_frame{main_task.get_handle().promise().get_async_stack_frame()}; @@ -64,51 +202,150 @@ 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"); 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()); + auto stream{std::move(*opt_stream)}; + kphp::log::debug("accepted a stream: descriptor -> {}", stream.descriptor()); + if (!m_io_scheduler.spawn(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 expected_connection{kphp::component::connection::from_stream(std::move(reader_stream))}; + if (!expected_connection) [[unlikely]] { + co_return kphp::log::warning("failed to create a confdata reader connection: error -> {}", expected_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"); + } + + auto connection{*std::move(expected_connection)}; + reader_session session{*this, std::prev(m_confdata_pieces.end())}; + const auto lease{kphp::confdata::reader_lease::create(kphp::confdata::SHARED_MEMORY_NAME, session.sample_id())}; + 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]] { + kphp::log::warning("failed to write a confdata reader lease: error -> {}", written.error()); + co_return; + } + + 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_clean_sync(std::string_view confdata_proxy_actor) noexcept -> kphp::coro::task> { + // 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_predefined_wildcards)}; + if (!created_piece) [[unlikely]] { + co_return std::unexpected{created_piece.error()}; + } + auto& piece{**created_piece}; + + for (;;) { + auto clean_sync{piece.storage().start_clean_sync()}; + + auto sync{co_await kphp::confdata::sync(confdata_proxy_actor, [this, &editor = clean_sync](std::span events) noexcept { + return try_apply_events(editor, events); + })}; + if (!sync) [[unlikely]] { + kphp::log::warning("confdata sync failed: error -> {}, retrying", std::to_underlying(sync.error())); + clean_sync.cancel(); + co_await m_io_scheduler.schedule(CONFDATA_RETRY_INTERVAL); + continue; + } + + clean_sync.commit(); + // Existing readers keep their mapped allocation; future lookups of the + // stable name resolve to this newly published piece. + if (auto published{k2::publish_shared_memory(kphp::confdata::SHARED_MEMORY_NAME, piece.storage().memory().data(), 0, true, true)}; !published) + [[unlikely]] { + kphp::log::error("failed to publish confdata shared memory: error -> {}", 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(sync); + 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 clean_sync{co_await perform_clean_sync(confdata_proxy_actor)}; + if (!clean_sync) [[unlikely]] { + kphp::log::warning("failed to create a confdata shared-memory piece: {}; retrying", clean_sync.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: + case kphp::confdata::subscribe_error::storage_busy: // 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; } co_await m_io_scheduler.schedule(CONFDATA_RETRY_INTERVAL); } } + +auto InstanceState::try_apply_events(kphp::confdata::storage::editor& editor, std::span events) noexcept -> bool { + for (const auto& wrapped_event : events) { + const auto& event{wrapped_event.inner}; + if (event.key.value.size() > kphp::confdata::MAX_KEY_LENGTH) [[unlikely]] { + kphp::log::warning("confdata event key is too long and was ignored: size -> {}", event.key.value.size()); + continue; + } + if (event.value.value.empty()) { + static_cast(editor.erase(event.key.value)); + } else { + static_cast(editor.upsert(event.key.value, [&event] noexcept { return decode_value(event); })); + } + } + // Shared-storage OOM is fatal today. Keeping batch acceptance explicit lets + // a future recoverable allocator reject the batch without advancing pagination. + return true; +} + +auto InstanceState::apply_incremental_events(std::span events) noexcept -> bool { + kphp::log::assertion(!m_confdata_pieces.empty()); + auto editor{m_confdata_pieces.back().storage().start_update()}; + if (!editor) [[unlikely]] { + return false; + } + if (!try_apply_events(*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 d33ad8b1b4..f58da11cbe 100644 --- a/runtime-light/components/confdata/state/instance-state.h +++ b/runtime-light/components/confdata/state/instance-state.h @@ -6,42 +6,111 @@ #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/confdata-storage.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_EXTRA_MEMORY_POOL_SIZE, 0}; + struct confdata_piece_creation_error final { + enum class stage : uint8_t { memory_size, shared_memory_allocation, storage_initialization, wildcard_initialization }; + /** Step that failed while constructing the unpublished 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{}; + /** 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, 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::confdata::storage m_confdata_storage; - 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; 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 + 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_clean_sync(std::string_view confdata_proxy_actor) noexcept -> kphp::coro::task>; + auto try_apply_events(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/confdata/state/predefined-wildcards-builder.cpp b/runtime-light/components/confdata/state/predefined-wildcards-builder.cpp deleted file mode 100644 index 7adb327082..0000000000 --- a/runtime-light/components/confdata/state/predefined-wildcards-builder.cpp +++ /dev/null @@ -1,167 +0,0 @@ -// Compiler for PHP (aka KPHP) -// Copyright (c) 2026 LLC «V Kontakte» -// Distributed under the GPL v3 License, see LICENSE.notice.txt - -#include "runtime-light/components/confdata/state/predefined-wildcards-builder.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "runtime-light/stdlib/confdata/detail/predefined-wildcards-layout.h" -#include "runtime-light/stdlib/confdata/predefined-wildcards.h" - -namespace { - -auto analyze_canonical_wildcards(std::span wildcards) noexcept - -> std::expected { - using kphp::confdata::predefined_wildcards_error; - using kphp::confdata::validate_predefined_wildcard; - - if (wildcards.size() > std::numeric_limits::max()) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::size_overflow}; - } - if (wildcards.empty()) { - return *kphp::confdata::detail::calculate_predefined_wildcards_metadata_layout(0, 0, 0, 0, 0); - } - - size_t strings_size{}; - size_t shortest_wildcard_size{std::numeric_limits::max()}; - for (size_t i{}; i < wildcards.size(); ++i) { - if (const auto validated{validate_predefined_wildcard(wildcards[i])}; !validated) [[unlikely]] { - return std::unexpected{validated.error()}; - } - if (i != 0 && wildcards[i - 1] >= wildcards[i]) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::non_canonical_wildcards}; - } - const auto new_strings_size{kphp::confdata::detail::checked_add(strings_size, wildcards[i].size())}; - if (!new_strings_size || *new_strings_size > std::numeric_limits::max()) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::size_overflow}; - } - strings_size = *new_strings_size; - shortest_wildcard_size = std::min(shortest_wildcard_size, wildcards[i].size()); - } - - size_t group_count{1}; - size_t max_matches_per_key{}; - size_t group_begin{}; - for (size_t i{}; i < wildcards.size(); ++i) { - const auto prefix{wildcards[i].substr(0, shortest_wildcard_size)}; - if (i != 0 && wildcards[i - 1].substr(0, shortest_wildcard_size) != prefix) { - ++group_count; - group_begin = i; - } - - size_t matches{1}; - for (size_t j{group_begin}; j < i; ++j) { - matches += wildcards[i].starts_with(wildcards[j]) ? 1 : 0; - } - max_matches_per_key = std::max(max_matches_per_key, matches); - } - - if (group_count > std::numeric_limits::max()) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::size_overflow}; - } - const auto layout{kphp::confdata::detail::calculate_predefined_wildcards_metadata_layout( - static_cast(wildcards.size()), static_cast(group_count), static_cast(strings_size), - static_cast(shortest_wildcard_size), static_cast(max_matches_per_key))}; - if (!layout) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::size_overflow}; - } - return *layout; -} - -auto header_from_layout(const kphp::confdata::detail::predefined_wildcards_metadata_layout& layout) noexcept - -> kphp::confdata::detail::predefined_wildcards_metadata_header { - return {.magic = kphp::confdata::detail::PREDEFINED_WILDCARDS_METADATA_MAGIC, - .version = kphp::confdata::detail::PREDEFINED_WILDCARDS_METADATA_VERSION, - .total_size = layout.total_size, - .entries_offset = layout.entries_offset, - .groups_offset = layout.groups_offset, - .strings_offset = layout.strings_offset, - .strings_size = layout.strings_size, - .wildcard_count = layout.wildcard_count, - .group_count = layout.group_count, - .shortest_wildcard_size = layout.shortest_wildcard_size, - .max_matches_per_key = layout.max_matches_per_key}; -} - -} // namespace - -namespace kphp::confdata { - -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}; - } - - size_t dots{}; - if (wildcard.back() == '.') { - for (const char c : wildcard) { - dots += (c == '.'); - if (dots > 2) { - break; - } - } - } - if (dots == 1 || dots == 2) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::reserved_wildcard}; - } - return {}; -} - -auto predefined_wildcards_metadata_size(std::span wildcards) noexcept -> std::expected { - const auto layout{analyze_canonical_wildcards(wildcards)}; - if (!layout) [[unlikely]] { - return std::unexpected{layout.error()}; - } - return layout->total_size; -} - -auto write_predefined_wildcards(std::span buffer, - std::span wildcards) noexcept -> std::expected { - if (!detail::is_predefined_wildcards_metadata_aligned(buffer.data())) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::misaligned_buffer}; - } - const auto layout{analyze_canonical_wildcards(wildcards)}; - if (!layout) [[unlikely]] { - return std::unexpected{layout.error()}; - } - if (buffer.size() < layout->total_size) [[unlikely]] { - return std::unexpected{predefined_wildcards_error::insufficient_buffer}; - } - - detail::store(buffer.data(), 0, header_from_layout(*layout)); - size_t string_offset{layout->strings_offset}; - uint32_t group_index{}; - uint32_t group_begin{}; - for (uint32_t i{}; i < layout->wildcard_count; ++i) { - const auto wildcard{wildcards[i]}; - detail::store( - buffer.data(), layout->entries_offset + static_cast(i) * sizeof(detail::predefined_wildcard_entry), - detail::predefined_wildcard_entry{.string_offset = static_cast(string_offset), .string_size = static_cast(wildcard.size())}); - std::memcpy(buffer.data() + string_offset, wildcard.data(), wildcard.size()); - string_offset += wildcard.size(); - - const bool group_finished{i + 1 == layout->wildcard_count || - wildcard.substr(0, layout->shortest_wildcard_size) != wildcards[i + 1].substr(0, layout->shortest_wildcard_size)}; - if (group_finished) { - detail::store(buffer.data(), layout->groups_offset + static_cast(group_index) * sizeof(detail::predefined_wildcard_group), - detail::predefined_wildcard_group{.first_entry = group_begin, .entry_count = i - group_begin + 1}); - ++group_index; - group_begin = i + 1; - } - } - - return open_predefined_wildcards(buffer.first(layout->total_size)); -} - -} // namespace kphp::confdata diff --git a/runtime-light/components/confdata/state/predefined-wildcards-builder.h b/runtime-light/components/confdata/state/predefined-wildcards-builder.h deleted file mode 100644 index 98e4306e51..0000000000 --- a/runtime-light/components/confdata/state/predefined-wildcards-builder.h +++ /dev/null @@ -1,28 +0,0 @@ -// 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 "runtime-light/stdlib/confdata/predefined-wildcards.h" - -namespace kphp::confdata { - -auto validate_predefined_wildcard(std::string_view wildcard) noexcept -> std::expected; - -/** @return The number of bytes needed to encode sorted, unique `wildcards`. */ -auto predefined_wildcards_metadata_size(std::span wildcards) noexcept -> std::expected; - -/** - * @brief Writes sorted, unique `wildcards` into relocatable immutable metadata at the start of `buffer`. - * @return A read-only view over the written metadata. - */ -auto write_predefined_wildcards(std::span buffer, - std::span wildcards) noexcept -> std::expected; - -} // namespace kphp::confdata diff --git a/runtime-light/components/kphp/state/instance-state.cpp b/runtime-light/components/kphp/state/instance-state.cpp index cc68a2e9b6..75706aaea8 100644 --- a/runtime-light/components/kphp/state/instance-state.cpp +++ b/runtime-light/components/kphp/state/instance-state.cpp @@ -134,6 +134,7 @@ template kphp::coro::task<> InstanceState::run_instance_prologue() noexcept { static_assert(kind != image_kind::invalid); image_kind_ = kind; + co_await confdata_instance_state.init(); // common initialization { @@ -224,4 +225,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/k2-platform/k2-api.h b/runtime-light/k2-platform/k2-api.h index e5a369e9e0..115170deea 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -137,6 +137,13 @@ inline std::expected alloc_shared_memory(size_t size, size_t ali return pointer; } +inline std::expected free_shared_memory(void* memory) noexcept { + if (const auto error_code{k2_free_shared_memory(memory)}; error_code != k2::errno_ok) [[unlikely]] { + return std::unexpected{error_code}; + } + return {}; +} + 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]] { diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index 383a551f06..1d46b3ca8e 100644 --- a/runtime-light/k2-platform/k2-header.h +++ b/runtime-light/k2-platform/k2-header.h @@ -28,7 +28,7 @@ #include #endif -#define K2_PLATFORM_HEADER_H_VERSION 16 +#define K2_PLATFORM_HEADER_H_VERSION 17 // Always check that enum value is a valid value! @@ -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 */ /** @@ -234,11 +235,28 @@ void k2_free_checked(void* ptr, size_t size, size_t align); */ int32_t k2_alloc_shared_memory(size_t size, size_t align, void** pointer); +/** + * Releases a shared-memory allocation owned by the calling instance. + * Existing reader references remain valid until they are released; the + * allocation becomes reclaimable once no references remain. + * + * @param `memory` Pointer previously returned by `k2_alloc_shared_memory`. + * + * @return `0` on success. libc-like `errno` on error. + * + * Possible `errno`: + * `EINVAL` => `memory` is NULL. + * `ENOENT` => `memory` was not allocated by `k2_alloc_shared_memory`. + * `ENOSYS` => Shared memory subsystem is unavailable on this host. + */ +int32_t k2_free_shared_memory(void* memory); + /** * 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/stdlib/confdata/confdata-functions.cpp b/runtime-light/stdlib/confdata/confdata-functions.cpp index b3e6c154e6..46b8646210 100644 --- a/runtime-light/stdlib/confdata/confdata-functions.cpp +++ b/runtime-light/stdlib/confdata/confdata-functions.cpp @@ -4,130 +4,165 @@ #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(const string& parameter, const char* parameter_name) noexcept -> bool { + if (!ConfdataInstanceState::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("too long {}", parameter_name); + 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("empty {} is not supported", parameter_name); + return false; } + return true; } -} // namespace +auto string_view_of(const string& value) noexcept -> std::string_view { + return {value.c_str(), value.size()}; +} -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{}; - } +} // namespace - 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 { + if (!verify_confdata_parameter(key, "key")) [[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& state{ConfdataInstanceState::get()}; + const auto views{kphp::confdata::split_key(string_view_of(key), state.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{state.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; - - if (wildcard.empty()) [[unlikely]] { - kphp::log::warning("empty wildcard is not supported"); - co_return array{}; +auto f$confdata_get_values_by_any_wildcard(const string& wildcard) noexcept -> array { + if (!verify_confdata_parameter(wildcard, "wildcard")) [[unlikely]] { + return {}; } - 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; + const auto& state{ConfdataInstanceState::get()}; + const auto& predefined_wildcards{state.wildcards()}; + const auto views{kphp::confdata::split_key(string_view_of(wildcard), predefined_wildcards)}; + kphp::log::assertion(views.has_value()); + const kphp::confdata::key_handles handles{*views}; + const auto& values{state.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 auto remainder_prefix_view{string_view_of(remainder_prefix)}; + for (const auto& entry : entries) { + const string entry_key{entry.get_key().to_string()}; + const auto entry_key_view{string_view_of(entry_key)}; + 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; } - 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{}; + array result{}; + const auto wildcard_view{string_view_of(wildcard)}; + const auto merge_entries = [&result, wildcard_view](kphp::confdata::storage::map_type::const_iterator section_it) noexcept { + const auto section_view{string_view_of(section_it->first)}; + 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()); + } + }; + + auto section_it{values.lower_bound(handles.section())}; + while (section_it != values.end() && string_view_of(section_it->first).starts_with(wildcard_view)) { + const auto section_view{string_view_of(section_it->first)}; + 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; + } + ++section_it; } + return result; +} - 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{}; +auto f$confdata_get_values_by_predefined_wildcard(const string& wildcard) noexcept -> array { + if (!verify_confdata_parameter(wildcard, "wildcard")) [[unlikely]] { + return {}; } - tl::fetcher tlf{response}; - tl::Dictionary dict_confdata_value{}; - kphp::log::assertion(dict_confdata_value.fetch(tlf)); + const auto& state{ConfdataInstanceState::get()}; + const auto wildcard_view{string_view_of(wildcard)}; + if (kphp::confdata::classify_section(wildcard_view, state.wildcards()) == kphp::confdata::section_kind::simple_key) [[unlikely]] { + kphp::log::warning("trying to get elements by non-predefined wildcard '{}'", wildcard_view); + return {}; + } - 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{state.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..02446d7da0 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 ConfdataInstanceState::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-state.cpp b/runtime-light/stdlib/confdata/confdata-state.cpp new file mode 100644 index 0000000000..b65d9ab35b --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-state.cpp @@ -0,0 +1,69 @@ +// 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" + +auto ConfdataInstanceState::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::confdata::reader_lease lease{}; + const auto read{co_await lease_stream->read(std::as_writable_bytes(std::span{&lease, 1}))}; + if (!read || *read != sizeof(lease) || !lease.is_valid()) [[unlikely]] { + kphp::log::warning("failed to acquire a valid confdata reader lease"); + co_return; + } + + const auto shared_memory{k2::get_shared_memory(lease.shared_memory_name())}; + if (!shared_memory) { + kphp::log::warning("failed to get confdata shared memory: error -> {}", shared_memory.error()); + co_return; + } + if (const auto opened{m_storage.open(*shared_memory)}; !opened) [[unlikely]] { + kphp::log::warning("failed to open confdata shared memory: error -> {}", std::to_underlying(opened.error())); + co_return; + } + + m_sample_id = lease.sample_id(); + m_reader_lease.emplace(std::move(*lease_stream)); +} + +auto ConfdataInstanceState::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(); +} + +auto ConfdataInstanceState::is_initialized() const noexcept -> bool { + return m_sample_id != kphp::confdata::storage::INVALID_SAMPLE_ID; +} + +auto ConfdataInstanceState::values() const noexcept -> const kphp::confdata::storage::map_type& { + kphp::log::assertion(is_initialized()); + return m_storage.values(m_sample_id); +} + +auto ConfdataInstanceState::wildcards() const noexcept -> const kphp::confdata::predefined_wildcards& { + kphp::log::assertion(is_initialized()); + return m_storage.wildcards(); +} diff --git a/runtime-light/stdlib/confdata/confdata-state.h b/runtime-light/stdlib/confdata/confdata-state.h index 58c23776c3..b0bbef31f5 100644 --- a/runtime-light/stdlib/confdata/confdata-state.h +++ b/runtime-light/stdlib/confdata/confdata-state.h @@ -4,29 +4,28 @@ #pragma once -#include +#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/streams/stream.h" class ConfdataInstanceState final : private vk::not_copyable { - using hasher_type = decltype([](const string& s) noexcept { return static_cast(s.hash()); }); - - kphp::stl::unordered_map m_key_cache; - kphp::stl::unordered_map, kphp::memory::script_allocator, hasher_type> m_wildcard_cache; + 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; - 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 -> ConfdataInstanceState&; }; diff --git a/runtime-light/stdlib/confdata/confdata-storage.cpp b/runtime-light/stdlib/confdata/confdata-storage.cpp new file mode 100644 index 0000000000..7e152fd762 --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-storage.cpp @@ -0,0 +1,617 @@ +// 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. */ + resource_type m_resource{}; + /** 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: + shared_state() noexcept + : m_wildcards{m_resource}, + m_samples{make_samples(m_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} {} + +storage::editor::editor(storage& owner, sample_id destination, bool copy_active_sample) noexcept + : m_storage{std::addressof(owner)}, + m_destination{destination}, + m_values{map_type::allocator_type{owner.resource()}}, + m_retired_allocations{owner.resource()} { + if (copy_active_sample) { + owner.with_resource([this, &owner] noexcept { m_values = owner.m_state->m_samples[owner.m_active_sample].m_values; }); + } +} + +storage::editor::editor(editor&& other) noexcept + : m_storage{std::exchange(other.m_storage, nullptr)}, + m_destination{std::exchange(other.m_destination, INVALID_SAMPLE_ID)}, + 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_storage != nullptr); + bool erased{}; + m_storage->with_resource([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_storage != nullptr); + m_storage->commit(*this); +} + +auto storage::editor::cancel() noexcept -> void { + if (m_storage != nullptr) { + m_storage->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_storage->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_storage->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{}; + 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_detached_allocation(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_detached_allocation(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_detached_allocation(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_owned_value(mixed{entry_it.get_string_key()}); + } + retire_value(entry_it.get_value()); + entries.unset(handles.remainder()); + + if (entries.empty()) { + retire_detached_allocation(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_owned_value(value); + m_last_retired_value = value; +} + +auto storage::editor::retire_detached_allocation(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_owned_value(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()}; + } + 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) 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}; + } + + m_memory = memory; + m_state = std::construct_at(reinterpret_cast(m_memory.data())); + m_active_sample = 0; + m_state->m_total_size = memory.size(); + m_state->m_pool_offset = layout->m_pool_offset; + auto pool_memory{memory.subspan(m_state->m_pool_offset)}; + m_state->m_resource.init(pool_memory.data(), pool_memory.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()); + kphp::log::assertion(!m_update_in_progress); + m_state = nullptr; + m_memory = {}; + m_active_sample = INVALID_SAMPLE_ID; + m_has_committed_sample = false; +} + +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_resource([this, wildcards, &result] noexcept { result = m_state->m_wildcards.initialize(wildcards); }); + return result; +} + +auto storage::is_initialized() const noexcept -> bool { + return m_state != nullptr; +} + +auto storage::memory() const noexcept -> std::span { + return m_memory; +} + +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_clean_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_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::resource() noexcept -> resource_type& { + kphp::log::assertion(is_initialized()); + return m_state->m_resource; +} + +auto storage::begin_update(bool copy_active_sample) noexcept -> std::optional { + kphp::log::assertion(!m_update_in_progress); + kphp::log::assertion(is_valid_sample_id(m_active_sample)); + reclaim_retired_samples(); + + 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}; +} + +auto storage::commit(editor& update) noexcept -> void { + kphp::log::assertion(m_update_in_progress); + kphp::log::assertion(update.m_storage == this); + kphp::log::assertion(is_valid_sample_id(update.m_destination)); + kphp::log::assertion(update.m_last_retired_value.is_null()); + + with_resource([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_storage = 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_storage == this); + with_resource([&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_storage = 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_resource([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..f174cea9d5 --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-storage.h @@ -0,0 +1,226 @@ +// 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/memory-resource/resource_allocator.h" +#include "runtime-common/core/memory-resource/unsynchronized_pool_resource.h" +#include "runtime-common/core/runtime-core.h" +#include "runtime-light/allocator/allocator.h" +#include "runtime-light/stdlib/confdata/predefined-wildcards.h" + +namespace kphp::confdata { + +enum class storage_error : uint8_t { misaligned_buffer, insufficient_buffer, size_overflow, invalid_storage }; + +struct key_views; + +/** + * A non-owning view of one confdata shared-memory piece. + * + * A writer initializes the piece, builds its initial clean-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 ===================================================================================== +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 editor; + +private: + using resource_type = memory_resource::unsynchronized_pool_resource; + using retired_list = memory_resource::stl::forward_list; + + 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: + /** 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 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 clean-sync piece from an incrementally updated one. */ + bool m_has_committed_sample{}; + + // === METHODS ================================================================================== +public: + 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; + + /** Constructs a new writer-side storage in `memory`. */ + auto init(std::span memory) noexcept -> std::expected; + /** Opens an initialized reader-side storage. */ + auto open(std::span memory) noexcept -> std::expected; + /** Detaches this local view without modifying the shared-memory piece. */ + auto close() noexcept -> void; + + /** Builds the immutable wildcard index owned by this shared-memory piece. */ + auto initialize_wildcards(std::span wildcards) noexcept -> std::expected; + + auto is_initialized() const noexcept -> bool; + auto memory() const noexcept -> std::span; + auto wildcards() const noexcept -> const predefined_wildcards&; + + /** 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; + auto values(sample_id id) const noexcept -> const map_type&; + + /** Starts the initial empty sample used by a clean sync on a fresh piece. */ + auto start_clean_sync() noexcept -> editor; + /** Starts an incremental update by copying the current immutable map. */ + auto start_update() noexcept -> std::optional; + +private: + template + requires std::same_as, void> && std::is_nothrow_invocable_v + auto with_resource(callback_type&& callback) noexcept -> void; + + auto resource() noexcept -> resource_type&; + auto begin_update(bool copy_active_sample) 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_resource(callback_type&& callback) noexcept -> void { + kphp::memory::with_script_memory_resource(resource(), std::forward(callback)); +} + +/** + * 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; + + /** Non-null until this update is committed or cancelled. */ + storage* m_storage{}; + /** Ring slot reserved for this unpublished working copy. */ + sample_id m_destination{INVALID_SAMPLE_ID}; + /** 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) 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_detached_allocation(const mixed& value) noexcept -> void; + auto retire_owned_value(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_storage != nullptr); + bool changed{}; + m_storage->with_resource([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/detail/predefined-wildcards-layout.h b/runtime-light/stdlib/confdata/detail/predefined-wildcards-layout.h deleted file mode 100644 index 559281fb60..0000000000 --- a/runtime-light/stdlib/confdata/detail/predefined-wildcards-layout.h +++ /dev/null @@ -1,115 +0,0 @@ -// 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 "runtime-light/stdlib/confdata/predefined-wildcards.h" - -namespace kphp::confdata::detail { - -inline constexpr uint64_t PREDEFINED_WILDCARDS_METADATA_MAGIC{0x444c49574443324bULL}; // "K2CDWILD" in little-endian byte order -inline constexpr uint32_t PREDEFINED_WILDCARDS_METADATA_VERSION{1}; - -struct alignas(PREDEFINED_WILDCARDS_ALIGNMENT) predefined_wildcards_metadata_header { - uint64_t magic; - uint32_t version; - uint32_t total_size; - uint32_t entries_offset; - uint32_t groups_offset; - uint32_t strings_offset; - uint32_t strings_size; - uint32_t wildcard_count; - uint32_t group_count; - uint32_t shortest_wildcard_size; - uint32_t max_matches_per_key; -}; - -struct predefined_wildcard_entry { - uint32_t string_offset; - uint32_t string_size; -}; - -struct predefined_wildcard_group { - uint32_t first_entry; - uint32_t entry_count; -}; - -struct predefined_wildcards_metadata_layout { - uint32_t total_size; - uint32_t entries_offset; - uint32_t groups_offset; - uint32_t strings_offset; - uint32_t strings_size; - uint32_t wildcard_count; - uint32_t group_count; - uint32_t shortest_wildcard_size; - uint32_t max_matches_per_key; -}; - -template -auto load(const std::byte* data, size_t offset) noexcept -> T { - T value{}; - std::memcpy(std::addressof(value), data + offset, sizeof(value)); - return value; -} - -template -auto store(std::byte* data, size_t offset, const T& value) noexcept -> void { - std::memcpy(data + offset, std::addressof(value), sizeof(value)); -} - -inline auto checked_add(size_t lhs, size_t rhs) noexcept -> std::optional { - if (rhs > std::numeric_limits::max() - lhs) [[unlikely]] { - return std::nullopt; - } - return lhs + rhs; -} - -inline auto checked_mul(size_t lhs, size_t rhs) noexcept -> std::optional { - if (lhs != 0 && rhs > std::numeric_limits::max() / lhs) [[unlikely]] { - return std::nullopt; - } - return lhs * rhs; -} - -inline auto calculate_predefined_wildcards_metadata_layout(uint32_t wildcard_count, uint32_t group_count, uint32_t strings_size, - uint32_t shortest_wildcard_size, - uint32_t max_matches_per_key) noexcept -> std::optional { - const auto entries_size{checked_mul(wildcard_count, sizeof(predefined_wildcard_entry))}; - const auto groups_size{checked_mul(group_count, sizeof(predefined_wildcard_group))}; - if (!entries_size || !groups_size) [[unlikely]] { - return std::nullopt; - } - - const size_t entries_offset{sizeof(predefined_wildcards_metadata_header)}; - const auto groups_offset{checked_add(entries_offset, *entries_size)}; - const auto strings_offset{groups_offset.and_then([groups_size](size_t offset) noexcept { return checked_add(offset, *groups_size); })}; - const auto total_size{strings_offset.and_then([strings_size](size_t offset) noexcept { return checked_add(offset, strings_size); })}; - if (!groups_offset || !strings_offset || !total_size || *total_size > std::numeric_limits::max()) [[unlikely]] { - return std::nullopt; - } - - return predefined_wildcards_metadata_layout{.total_size = static_cast(*total_size), - .entries_offset = static_cast(entries_offset), - .groups_offset = static_cast(*groups_offset), - .strings_offset = static_cast(*strings_offset), - .strings_size = strings_size, - .wildcard_count = wildcard_count, - .group_count = group_count, - .shortest_wildcard_size = shortest_wildcard_size, - .max_matches_per_key = max_matches_per_key}; -} - -inline auto is_predefined_wildcards_metadata_aligned(const void* pointer) noexcept -> bool { - return reinterpret_cast(pointer) % PREDEFINED_WILDCARDS_ALIGNMENT == 0; -} - -} // namespace kphp::confdata::detail diff --git a/runtime-light/stdlib/stdlib.cmake b/runtime-light/stdlib/stdlib.cmake index 13a0f3e757..81fc965542 100644 --- a/runtime-light/stdlib/stdlib.cmake +++ b/runtime-light/stdlib/stdlib.cmake @@ -1,6 +1,8 @@ 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 diff --git a/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp b/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp index 1305ff653d..b26cc04507 100644 --- a/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp +++ b/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -12,14 +13,18 @@ #include #include #include +#include #include #include +#include #include "runtime-common/core/allocator/script-allocator.h" +#include "runtime-common/stdlib/serialization/json-functions.h" +#include "runtime-common/stdlib/serialization/serialize-functions.h" #include "runtime-light/allocator/allocator-state.h" #include "runtime-light/allocator/allocator.h" -#include "runtime-light/components/confdata/state/confdata-storage.h" #include "runtime-light/k2-platform/k2-api.h" +#include "runtime-light/stdlib/confdata/confdata-storage.h" namespace { @@ -35,6 +40,18 @@ auto check(bool condition, const char* expression, int line) noexcept -> void { #define CHECK(expression) check(static_cast(expression), #expression, __LINE__) +auto start_update(kphp::confdata::storage& storage) noexcept -> kphp::confdata::storage::editor { + auto editor{storage.start_update()}; + CHECK(editor.has_value()); + return std::move(*editor); +} + +template +requires std::same_as, mixed> && std::is_nothrow_invocable_v +auto upsert_shared(kphp::confdata::storage::editor& editor, std::string_view key, value_factory_type&& value_factory) noexcept -> bool { + return editor.upsert(key, std::forward(value_factory)); +} + template auto contains(const std::array& storage, const void* memory) noexcept -> bool { const auto begin{reinterpret_cast(storage.data())}; @@ -158,32 +175,328 @@ auto test_default_resource_can_request_extra_memory() noexcept -> void { allocator.free_script_memory(memory, allocation_size); } -auto test_confdata_storage_persists_values_between_mutations() noexcept -> void { +auto test_confdata_storage_keeps_pinned_samples_alive() noexcept -> void { alignas(std::max_align_t) std::array shared_storage{}; + const auto logical_storage{std::span{shared_storage}.first(shared_storage.size() - 13)}; kphp::confdata::storage storage{}; - CHECK(storage.init(shared_storage).has_value()); + CHECK(storage.init(logical_storage).has_value()); CHECK(storage.memory().data() == shared_storage.data()); + CHECK(storage.memory().size() == logical_storage.size()); const auto default_memory_before{RuntimeAllocator::get().memory_resource.get_memory_stats().memory_used}; - storage.mutate([&shared_storage](kphp::confdata::storage::map_type& values) noexcept { - CHECK(contains(shared_storage, std::addressof(values))); - values.emplace(string{"persistent-key"}, mixed{string{"persistent-value"}}); - const auto& [key, value]{*values.begin()}; - CHECK(contains(shared_storage, std::addressof(*values.begin()))); - CHECK(contains(shared_storage, key.c_str())); - CHECK(value.is_string()); - CHECK(contains(shared_storage, value.as_string().c_str())); - }); - - CHECK(storage.values().size() == 1); + auto initial_update{storage.start_clean_sync()}; + CHECK(upsert_shared(initial_update, "persistent-key", [] noexcept { return mixed{string{"persistent-value"}}; })); + initial_update.commit(); + + kphp::confdata::storage reader{}; + CHECK(reader.open(shared_storage).has_value()); + CHECK(reader.memory().size() == logical_storage.size()); + const auto old_sample{reader.acquire_active_sample()}; + CHECK(reader.values(old_sample).size() == 1); + const auto& [key, value]{*reader.values(old_sample).begin()}; + CHECK(contains(shared_storage, std::addressof(*reader.values(old_sample).begin()))); + CHECK(contains(shared_storage, key.c_str())); + CHECK(value.is_string()); + CHECK(contains(shared_storage, value.as_string().c_str())); CHECK(RuntimeAllocator::get().memory_resource.get_memory_stats().memory_used == default_memory_before); - storage.mutate([](kphp::confdata::storage::map_type& values) noexcept { values.clear(); }); - CHECK(storage.values().empty()); - storage.destroy(); + + auto deletion{start_update(storage)}; + CHECK(deletion.erase("persistent-key")); + deletion.commit(); + + const auto new_sample{reader.acquire_active_sample()}; + CHECK(reader.values(new_sample).empty()); + CHECK(reader.values(old_sample).size() == 1); + reader.release_sample(new_sample); + reader.release_sample(old_sample); + reader.close(); + storage.close(); CHECK(!storage.is_initialized()); CHECK(storage.memory().empty()); } +auto test_confdata_storage_backpressures_when_every_sample_is_pinned() noexcept -> void { + alignas(std::max_align_t) std::array shared_storage{}; + kphp::confdata::storage storage{}; + CHECK(storage.init(shared_storage).has_value()); + + std::vector pinned_samples; + auto clean_sync{storage.start_clean_sync()}; + clean_sync.commit(); + pinned_samples.emplace_back(storage.acquire_active_sample()); + while (auto update{storage.start_update()}) { + update->commit(); + pinned_samples.emplace_back(storage.acquire_active_sample()); + } + CHECK(!pinned_samples.empty()); + CHECK(!storage.start_update().has_value()); + + for (const auto sample : pinned_samples) { + storage.release_sample(sample); + } + auto update{start_update(storage)}; + update.cancel(); + storage.close(); +} + +auto test_confdata_storage_deletion_retires_garbage_with_the_old_sample() noexcept -> void { + alignas(std::max_align_t) std::array shared_storage{}; + kphp::confdata::storage storage{}; + CHECK(storage.init(shared_storage).has_value()); + + auto initial_update{storage.start_clean_sync()}; + CHECK(upsert_shared(initial_update, "a.b.c", [] noexcept { return mixed{string{"value"}}; })); + CHECK(upsert_shared(initial_update, "plain", [] noexcept { return mixed{string{"plain-value"}}; })); + initial_update.commit(); + + const auto old_sample{storage.acquire_active_sample()}; + CHECK(storage.values(old_sample).size() == 3); + + auto deletion{start_update(storage)}; + CHECK(deletion.erase("a.b.c")); + CHECK(deletion.erase("plain")); + deletion.commit(); + + const auto new_sample{storage.acquire_active_sample()}; + CHECK(storage.values(new_sample).empty()); + CHECK(storage.values(old_sample).size() == 3); + storage.release_sample(new_sample); + storage.release_sample(old_sample); + + // Starting another update reclaims the now-unpinned retired sample and its + // shallow/deep garbage before choosing an update target. + auto reclaim{start_update(storage)}; + reclaim.cancel(); + storage.close(); +} + +auto test_confdata_storage_reclaims_retired_samples_in_generation_order() noexcept -> void { + alignas(std::max_align_t) std::array shared_storage{}; + kphp::confdata::storage storage{}; + CHECK(storage.init(shared_storage).has_value()); + + auto initial_update{storage.start_clean_sync()}; + CHECK(upsert_shared(initial_update, "key", [] noexcept { return mixed{string{"value"}}; })); + initial_update.commit(); + const auto oldest_sample{storage.acquire_active_sample()}; + + auto copied_update{start_update(storage)}; + copied_update.commit(); + const auto newer_sample{storage.acquire_active_sample()}; + + auto deletion{start_update(storage)}; + CHECK(deletion.erase("key")); + deletion.commit(); + storage.release_sample(newer_sample); + + // The newer retired sample owns the deleted value's garbage, but it cannot + // be reclaimed before an older reader that still references that value. + auto blocked_reclamation{start_update(storage)}; + const auto old_value{storage.values(oldest_sample).find(string{"key"})}; + CHECK(old_value != storage.values(oldest_sample).end()); + CHECK(old_value->second.is_string()); + CHECK(old_value->second.as_string() == string{"value"}); + blocked_reclamation.cancel(); + + storage.release_sample(oldest_sample); + auto reclamation{start_update(storage)}; + reclamation.cancel(); + storage.close(); +} + +auto test_confdata_storage_preserves_global_constants() noexcept -> void { + static constexpr std::string_view GLOBAL_VALUE{"global"}; + alignas(std::max_align_t) std::array global_value_memory{}; + const string global_value{string::make_const_string_on_memory(GLOBAL_VALUE.data(), static_cast(GLOBAL_VALUE.size()), + global_value_memory.data(), global_value_memory.size())}; + + alignas(std::max_align_t) std::array shared_storage{}; + kphp::confdata::storage storage{}; + CHECK(storage.init(shared_storage).has_value()); + auto initial_update{storage.start_clean_sync()}; + CHECK(upsert_shared(initial_update, "key", [&global_value] noexcept { return mixed{global_value}; })); + initial_update.commit(); + + const auto old_sample{storage.acquire_active_sample()}; + const auto value_it{storage.values(old_sample).find(string{"key"})}; + CHECK(value_it != storage.values(old_sample).end()); + CHECK(value_it->second.is_reference_counter(ExtraRefCnt::for_global_const)); + + auto deletion{start_update(storage)}; + CHECK(deletion.erase("key")); + deletion.commit(); + storage.release_sample(old_sample); + auto reclamation{start_update(storage)}; + reclamation.cancel(); + storage.close(); +} + +auto test_confdata_storage_deletes_all_predefined_representations() noexcept -> void { + static constexpr std::array PREDEFINED_WILDCARDS{"pre", "prefix"}; + + alignas(std::max_align_t) std::array shared_storage{}; + kphp::confdata::storage storage{}; + CHECK(storage.init(shared_storage).has_value()); + CHECK(storage.initialize_wildcards(PREDEFINED_WILDCARDS).has_value()); + + auto initial_update{storage.start_clean_sync()}; + CHECK(upsert_shared(initial_update, "prefix.value", [] noexcept { return mixed{string{"value"}}; })); + initial_update.commit(); + + kphp::confdata::storage reader{}; + CHECK(reader.open(storage.memory()).has_value()); + CHECK(reader.wildcards().contains("pre")); + CHECK(reader.wildcards().contains("prefix")); + const auto old_sample{reader.acquire_active_sample()}; + CHECK(reader.values(old_sample).size() == 3); + const auto value_in = [&reader, old_sample](std::string_view section, std::string_view remainder) noexcept -> const mixed* { + const string section_key{section.data(), static_cast(section.size())}; + const auto section_it{reader.values(old_sample).find(section_key)}; + CHECK(section_it != reader.values(old_sample).end()); + CHECK(section_it->second.is_array()); + const string remainder_key{remainder.data(), static_cast(remainder.size())}; + return section_it->second.as_array().find_value(remainder_key); + }; + const auto* shortest_value{value_in("pre", "fix.value")}; + const auto* longest_value{value_in("prefix", ".value")}; + const auto* implicit_value{value_in("prefix.", "value")}; + CHECK(shortest_value != nullptr && longest_value != nullptr && implicit_value != nullptr); + CHECK(shortest_value->is_string() && shortest_value->as_string() == string{"value"}); + CHECK(shortest_value->as_string().c_str() == longest_value->as_string().c_str()); + CHECK(shortest_value->as_string().c_str() == implicit_value->as_string().c_str()); + + auto deletion{start_update(storage)}; + CHECK(deletion.erase("prefix.value")); + deletion.commit(); + + const auto new_sample{reader.acquire_active_sample()}; + CHECK(reader.values(new_sample).empty()); + CHECK(reader.values(old_sample).size() == 3); + reader.release_sample(new_sample); + reader.release_sample(old_sample); + reader.close(); + + auto reclamation{start_update(storage)}; + reclamation.cancel(); + storage.close(); +} + +auto test_confdata_storage_update_is_atomic() noexcept -> void { + static constexpr std::array PREDEFINED_WILDCARDS{"pre", "prefix"}; + alignas(std::max_align_t) std::array shared_storage{}; + kphp::confdata::storage storage{}; + CHECK(storage.init(shared_storage).has_value()); + CHECK(storage.initialize_wildcards(PREDEFINED_WILDCARDS).has_value()); + + auto initial_update{storage.start_clean_sync()}; + CHECK(upsert_shared(initial_update, "old-key", [] noexcept { return mixed{string{"old-value"}}; })); + CHECK(upsert_shared(initial_update, "prefix.value", [] noexcept -> mixed { + array decoded{}; + decoded.set_value(string{"nested"}, mixed{string{"shared-old-value"}}); + return mixed{std::move(decoded)}; + })); + initial_update.commit(); + + const auto old_sample{storage.acquire_active_sample()}; + CHECK(storage.values(old_sample).contains(string{"old-key"})); + + { + auto rolled_back{start_update(storage)}; + CHECK(rolled_back.erase("old-key")); + CHECK(rolled_back.erase("prefix.value")); + CHECK(upsert_shared(rolled_back, "new-key", [] noexcept -> mixed { + array decoded{}; + decoded.set_value(string{"field"}, mixed{42}); + return mixed{std::move(decoded)}; + })); + + const auto visible_sample{storage.acquire_active_sample()}; + CHECK(visible_sample == old_sample); + CHECK(storage.values(visible_sample).contains(string{"old-key"})); + CHECK(!storage.values(visible_sample).contains(string{"new-key"})); + storage.release_sample(visible_sample); + // The editor destructor rolls this unpublished update back. + } + + const auto after_rollback{storage.acquire_active_sample()}; + CHECK(after_rollback == old_sample); + storage.release_sample(after_rollback); + + auto update{start_update(storage)}; + CHECK(update.erase("old-key")); + CHECK(update.erase("prefix.value")); + CHECK(upsert_shared(update, "new-key", [] noexcept -> mixed { + array decoded{}; + decoded.set_value(string{"field"}, mixed{42}); + return mixed{std::move(decoded)}; + })); + CHECK(storage.values(old_sample).contains(string{"old-key"})); + update.commit(); + + const auto new_sample{storage.acquire_active_sample()}; + CHECK(!storage.values(new_sample).contains(string{"old-key"})); + const auto new_value{storage.values(new_sample).find(string{"new-key"})}; + CHECK(new_value != storage.values(new_sample).end()); + CHECK(new_value->second.is_array()); + const auto* field{new_value->second.as_array().find_value(string{"field"})}; + CHECK(field != nullptr && field->is_int() && field->as_int() == 42); + CHECK(storage.values(old_sample).contains(string{"old-key"})); + + storage.release_sample(new_sample); + storage.release_sample(old_sample); + auto reclamation{start_update(storage)}; + reclamation.cancel(); + storage.close(); +} + +auto test_confdata_storage_decoded_values() noexcept -> void { + alignas(std::max_align_t) std::array shared_storage{}; + kphp::confdata::storage storage{}; + CHECK(storage.init(shared_storage).has_value()); + + auto update{storage.start_clean_sync()}; + CHECK(upsert_shared(update, "plain", [] noexcept { return mixed{string{"plain-value"}}; })); + CHECK(upsert_shared(update, "json", [] noexcept -> mixed { + const auto json{json_decode(R"({"name":"k2","count":2,"nested":[true,null]})")}; + CHECK(json.has_value()); + return *json; + })); + CHECK(upsert_shared(update, "php", [] noexcept -> mixed { + static constexpr std::string_view PHP_SERIALIZED{R"(a:2:{s:4:"name";s:2:"k2";i:5;s:5:"value";})"}; + const mixed php{unserialize_raw(PHP_SERIALIZED.data(), static_cast(PHP_SERIALIZED.size()))}; + CHECK(php.is_array()); + return php; + })); + update.commit(); + + const auto sample{storage.acquire_active_sample()}; + const auto& values{storage.values(sample)}; + + const auto plain{values.find(string{"plain"})}; + CHECK(plain != values.end()); + CHECK(plain->second.is_string() && plain->second.as_string() == string{"plain-value"}); + CHECK(plain->second.is_reference_counter(ExtraRefCnt::for_confdata)); + + const auto json{values.find(string{"json"})}; + CHECK(json != values.end() && json->second.is_array()); + const auto* json_name{json->second.as_array().find_value(string{"name"})}; + const auto* json_count{json->second.as_array().find_value(string{"count"})}; + CHECK(json_name != nullptr && json_name->is_string() && json_name->as_string() == string{"k2"}); + CHECK(json_count != nullptr && json_count->is_int() && json_count->as_int() == 2); + CHECK(json->second.is_reference_counter(ExtraRefCnt::for_confdata)); + CHECK(json_name->is_reference_counter(ExtraRefCnt::for_confdata)); + + const auto php{values.find(string{"php"})}; + CHECK(php != values.end() && php->second.is_array()); + const auto* php_name{php->second.as_array().find_value(string{"name"})}; + const auto* php_value{php->second.as_array().find_value(mixed{5})}; + CHECK(php_name != nullptr && php_name->is_string() && php_name->as_string() == string{"k2"}); + CHECK(php_value != nullptr && php_value->is_string() && php_value->as_string() == string{"value"}); + + storage.release_sample(sample); + storage.close(); +} + auto test_confdata_storage_rejects_invalid_memory() noexcept -> void { alignas(std::max_align_t) std::array memory{}; @@ -200,6 +513,11 @@ auto test_confdata_storage_rejects_invalid_memory() noexcept -> void { const auto overflow{kphp::confdata::storage::memory_size(std::numeric_limits::max())}; CHECK(!overflow.has_value()); CHECK(overflow.error() == kphp::confdata::storage_error::size_overflow); + + kphp::confdata::storage invalid_storage{}; + const auto invalid{invalid_storage.open(memory)}; + CHECK(!invalid.has_value()); + CHECK(invalid.error() == kphp::confdata::storage_error::invalid_storage); } } // namespace @@ -226,6 +544,10 @@ extern "C" void k2_exit(int32_t /*exit_code*/) { void runtime_error(const char* /*unused*/, ...) {} +void php_warning(const char* /*unused*/, ...) {} + +void php_error(const char* /*unused*/, ...) {} + [[noreturn]] void critical_error_handler() { std::abort(); } @@ -244,7 +566,14 @@ auto main() -> int { test_nested_resources_restore_previous_target(); test_zeroing_and_reallocation_use_replacement_resource(); test_default_resource_can_request_extra_memory(); - test_confdata_storage_persists_values_between_mutations(); + test_confdata_storage_keeps_pinned_samples_alive(); + test_confdata_storage_backpressures_when_every_sample_is_pinned(); + test_confdata_storage_deletion_retires_garbage_with_the_old_sample(); + test_confdata_storage_reclaims_retired_samples_in_generation_order(); + test_confdata_storage_preserves_global_constants(); + test_confdata_storage_deletes_all_predefined_representations(); + test_confdata_storage_update_is_atomic(); + test_confdata_storage_decoded_values(); test_confdata_storage_rejects_invalid_memory(); RuntimeAllocator::get().free(); return 0; diff --git a/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp b/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp index 70a77d9809..73e2f333dc 100644 --- a/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp +++ b/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp @@ -3,6 +3,7 @@ // Distributed under the GPL v3 License, see LICENSE.notice.txt #include +#include #include #include #include @@ -16,8 +17,12 @@ #include #include -#include "runtime-light/components/confdata/state/predefined-wildcards-builder.h" +#include "runtime-common/core/allocator/runtime-allocator.h" +#include "runtime-light/allocator/allocator-state.h" +#include "runtime-light/k2-platform/k2-api.h" #include "runtime-light/stdlib/confdata/confdata-keys.h" +#include "runtime-light/stdlib/confdata/confdata-reader-lease.h" +#include "runtime-light/stdlib/confdata/confdata-storage.h" #include "runtime-light/stdlib/confdata/predefined-wildcards.h" namespace { @@ -54,18 +59,48 @@ auto check(bool condition, const char* expression, int line) noexcept -> void { #define CHECK(expression) check(static_cast(expression), #expression, __LINE__) -auto make_metadata(std::vector wildcards, std::vector& storage) -> predefined_wildcards { - for (const auto wildcard : wildcards) { - CHECK(kphp::confdata::validate_predefined_wildcard(wildcard).has_value()); +constexpr auto DEFAULT_ALLOCATOR_SIZE{static_cast(1024U * 1024U)}; +constexpr auto SHARED_STORAGE_SIZE{static_cast(256U * 1024U)}; + +class wildcard_fixture final { + alignas(std::max_align_t) std::array m_memory{}; + kphp::confdata::storage m_storage; + +public: + explicit wildcard_fixture(std::vector wildcards) noexcept { + for (const auto wildcard : wildcards) { + CHECK(kphp::confdata::validate_predefined_wildcard(wildcard).has_value()); + } + std::ranges::sort(wildcards); + wildcards.erase(std::ranges::unique(wildcards).begin(), wildcards.end()); + CHECK(m_storage.init(m_memory).has_value()); + CHECK(m_storage.initialize_wildcards(wildcards).has_value()); } - std::ranges::sort(wildcards); - wildcards.erase(std::ranges::unique(wildcards).begin(), wildcards.end()); - const auto metadata_size{kphp::confdata::predefined_wildcards_metadata_size(wildcards)}; - CHECK(metadata_size.has_value()); - storage.resize(*metadata_size); - const auto metadata{kphp::confdata::write_predefined_wildcards(storage, wildcards)}; - CHECK(metadata.has_value()); - return *metadata; + + ~wildcard_fixture() { + m_storage.close(); + } + + auto wildcards() const noexcept -> const predefined_wildcards& { + return m_storage.wildcards(); + } + + auto storage() const noexcept -> const kphp::confdata::storage& { + return m_storage; + } +}; + +auto make_wildcards(std::vector wildcards) -> wildcard_fixture { + return wildcard_fixture{std::move(wildcards)}; +} + +auto initialize_noncanonical(std::span wildcards) -> std::expected { + alignas(std::max_align_t) std::array memory{}; + kphp::confdata::storage storage{}; + CHECK(storage.init(memory).has_value()); + auto result{storage.initialize_wildcards(wildcards)}; + storage.close(); + return result; } auto matching(const predefined_wildcards& wildcards, std::string_view key) -> std::vector { @@ -123,13 +158,15 @@ auto test_validation_and_formatting() -> void { CHECK(kphp::confdata::validate_predefined_wildcard("foo.").error() == predefined_wildcards_error::reserved_wildcard); CHECK(kphp::confdata::validate_predefined_wildcard("foo.bar.").error() == predefined_wildcards_error::reserved_wildcard); CHECK(kphp::confdata::validate_predefined_wildcard("foo").has_value()); + CHECK(kphp::confdata::validate_predefined_wildcard("foo.bar").has_value()); + CHECK(kphp::confdata::validate_predefined_wildcard("foo.bar.baz.").has_value()); CHECK(kphp::confdata::validate_predefined_wildcard("foo...").has_value()); CHECK(std::format("{}", predefined_wildcards_error::empty_wildcard) == "empty wildcard"); } auto test_empty_predefined_wildcards_matrix() -> void { - std::vector storage{}; - const auto wildcards{make_metadata({}, storage)}; + const auto fixture{make_wildcards({})}; + const auto& wildcards{fixture.wildcards()}; for (const std::string_view key : {"", ".", "..", "abc", "abc.", "abc.def", "abc.def.", "abc.def.ghi", "abc.def.ghi."}) { CHECK(matching(wildcards, key).empty()); @@ -157,12 +194,11 @@ auto test_empty_predefined_wildcards_matrix() -> void { for (const auto& sample : sections) { CHECK(kphp::confdata::classify_section(sample.section, wildcards) == sample.kind); } - CHECK(!storage.empty()); } auto test_predefined_wildcards_matrix() -> void { - std::vector storage{}; - const auto wildcards{make_metadata({"a", "ab", "abc", "c"}, storage)}; + const auto fixture{make_wildcards({"a", "ab", "abc", "c"})}; + const auto& wildcards{fixture.wildcards()}; for (const std::string_view key : {"", ".", "..", "xyz", "xyz.abc"}) { CHECK(matching(wildcards, key).empty()); @@ -224,37 +260,27 @@ auto test_predefined_wildcards_matrix() -> void { CHECK(!wildcards.is_top_level_wildcard("missing")); } -auto test_relocation() -> void { - std::vector original_storage{}; - const auto original{make_metadata({"foo", "foobar", "zip"}, original_storage)}; - std::vector relocated_storage{original_storage}; +auto test_shared_storage_index() -> void { + const auto fixture{make_wildcards({"foo", "foobar", "zip"})}; + kphp::confdata::storage reader{}; + CHECK(reader.open(fixture.storage().memory()).has_value()); - const auto relocated{kphp::confdata::open_predefined_wildcards(relocated_storage)}; - CHECK(relocated.has_value()); - CHECK(matching(*relocated, "foobar.value") == (std::vector{"foo", "foobar"})); - const auto shortest{relocated->shortest_matching_wildcard("foobar.value")}; + const auto& wildcards{reader.wildcards()}; + CHECK(matching(wildcards, "foobar.value") == (std::vector{"foo", "foobar"})); + const auto shortest{wildcards.shortest_matching_wildcard("foobar.value")}; CHECK(shortest.has_value()); - const auto relocated_bytes{std::as_bytes(std::span{relocated_storage})}; - CHECK(reinterpret_cast(shortest->data()) >= relocated_bytes.data()); - CHECK(reinterpret_cast(shortest->data()) < relocated_bytes.data() + relocated_bytes.size()); - CHECK(original.max_matches_per_key() == relocated->max_matches_per_key()); + const auto shared_memory{reader.memory()}; + CHECK(reinterpret_cast(shortest->data()) >= shared_memory.data()); + CHECK(reinterpret_cast(shortest->data()) < shared_memory.data() + shared_memory.size()); + CHECK(fixture.wildcards().max_matches_per_key() == wildcards.max_matches_per_key()); + reader.close(); } -auto test_invalid_metadata() -> void { +auto test_noncanonical_wildcards() -> void { const std::vector noncanonical{"b", "a"}; - CHECK(kphp::confdata::predefined_wildcards_metadata_size(noncanonical).error() == predefined_wildcards_error::non_canonical_wildcards); - - std::vector storage{}; - static_cast(make_metadata({"abc"}, storage)); - storage.front() ^= std::byte{1}; - const auto corrupted{kphp::confdata::open_predefined_wildcards(storage)}; - CHECK(!corrupted.has_value()); - CHECK(corrupted.error() == predefined_wildcards_error::invalid_metadata); - - std::vector misaligned_storage(storage.size() + 1); - const auto misaligned{kphp::confdata::open_predefined_wildcards(std::span{misaligned_storage}.subspan(1))}; - CHECK(!misaligned.has_value()); - CHECK(misaligned.error() == predefined_wildcards_error::misaligned_buffer); + const auto result{initialize_noncanonical(noncanonical)}; + CHECK(!result.has_value()); + CHECK(result.error() == predefined_wildcards_error::non_canonical_wildcards); } auto test_zero_dots_key_matrix() -> void { @@ -390,9 +416,9 @@ auto test_explicit_predefined_wildcard_matrix() -> void { } auto test_automatic_predefined_wildcard_matrix() -> void { - std::vector storage{}; - // Implicit one-dot/two-dot sections are deliberately excluded from predefined metadata. - const auto wildcards{make_metadata({"abc", "abc.xyz", "cde", "cd"}, storage)}; + // Implicit one-dot/two-dot sections are deliberately excluded from the configured index. + const auto fixture{make_wildcards({"abc", "abc.xyz", "cde", "cd"})}; + const auto& wildcards{fixture.wildcards()}; const std::vector samples{ {"abc", section_kind::predefined_wildcard, "abc", string_remainder("")}, {"abc.", section_kind::predefined_wildcard, "abc", string_remainder(".")}, @@ -441,14 +467,66 @@ auto test_key_splitting_errors() -> void { CHECK(oversized.error() == kphp::confdata::split_error::key_too_long); } +auto test_reader_lease_handshake() -> void { + const kphp::confdata::reader_lease empty{}; + CHECK(!empty.is_valid()); + CHECK(!kphp::confdata::reader_lease::create("", 3).has_value()); + CHECK(!kphp::confdata::reader_lease::create("confdata", kphp::confdata::storage::INVALID_SAMPLE_ID).has_value()); + + const auto lease{kphp::confdata::reader_lease::create("kphp-confdata", 7)}; + CHECK(lease.has_value()); + CHECK(lease->is_valid()); + CHECK(lease->sample_id() == 7); + CHECK(lease->shared_memory_name() == "kphp-confdata"); +} + } // namespace +extern "C" void* k2_alloc(size_t size, size_t align) { + const auto actual_align{std::max(align, alignof(std::max_align_t))}; + const auto actual_size{(size + actual_align - 1) / actual_align * actual_align}; + return std::aligned_alloc(actual_align, actual_size); +} + +extern "C" void* k2_realloc(void* memory, size_t new_size) { + return std::realloc(memory, new_size); +} + +extern "C" void k2_free(void* memory) { + std::free(memory); +} + +extern "C" void k2_log(size_t /*level*/, size_t /*len*/, const char* /*msg*/, size_t /*kv_count*/, const LogKeyValuePair* /*kv_pairs*/) {} + +extern "C" void k2_exit(int32_t /*exit_code*/) { + std::abort(); +} + +void runtime_error(const char* /*unused*/, ...) {} + +void php_warning(const char* /*unused*/, ...) {} + +void php_error(const char* /*unused*/, ...) {} + +[[noreturn]] void critical_error_handler() { + std::abort(); +} + +[[noreturn]] void php_assert__(const char* /*unused*/, const char* /*unused*/, int /*unused*/) { + std::abort(); +} + +auto AllocatorState::get() noexcept -> const AllocatorState& { + static AllocatorState allocator_state{DEFAULT_ALLOCATOR_SIZE, DEFAULT_ALLOCATOR_SIZE, 0}; + return allocator_state; +} + auto main() -> int { test_validation_and_formatting(); test_empty_predefined_wildcards_matrix(); test_predefined_wildcards_matrix(); - test_relocation(); - test_invalid_metadata(); + test_shared_storage_index(); + test_noncanonical_wildcards(); test_zero_dots_key_matrix(); test_one_dot_empty_remainder_matrix(); test_one_dot_string_remainder_matrix(); @@ -459,5 +537,7 @@ auto main() -> int { test_explicit_predefined_wildcard_matrix(); test_automatic_predefined_wildcard_matrix(); test_key_splitting_errors(); + test_reader_lease_handshake(); + RuntimeAllocator::get().free(); return 0; } diff --git a/tests/cpp/runtime-light/runtime-light-tests.cmake b/tests/cpp/runtime-light/runtime-light-tests.cmake index db8f750402..6876d66e85 100644 --- a/tests/cpp/runtime-light/runtime-light-tests.cmake +++ b/tests/cpp/runtime-light/runtime-light-tests.cmake @@ -1,5 +1,11 @@ +set(RUNTIME_LIGHT_CONFDATA_TEST_RUNTIME_CORE_SOURCES ${CORE_SRC}) +list(TRANSFORM RUNTIME_LIGHT_CONFDATA_TEST_RUNTIME_CORE_SOURCES PREPEND "${RUNTIME_COMMON_DIR}/") + set(RUNTIME_LIGHT_CONFDATA_TEST_SOURCES - ${BASE_DIR}/runtime-light/components/confdata/state/predefined-wildcards-builder.cpp + ${RUNTIME_LIGHT_CONFDATA_TEST_RUNTIME_CORE_SOURCES} + ${BASE_DIR}/runtime-light/allocator/runtime-light-allocator.cpp + ${BASE_DIR}/runtime-light/memory-resource-impl/monotonic-light-buffer-resource.cpp + ${BASE_DIR}/runtime-light/stdlib/confdata/confdata-storage.cpp ${BASE_DIR}/runtime-light/stdlib/confdata/confdata-keys.cpp ${BASE_DIR}/runtime-light/stdlib/confdata/predefined-wildcards.cpp ${BASE_DIR}/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp) @@ -15,9 +21,13 @@ list(TRANSFORM RUNTIME_LIGHT_ALLOCATOR_TEST_RUNTIME_CORE_SOURCES PREPEND "${RUNT set(RUNTIME_LIGHT_ALLOCATOR_TEST_SOURCES ${RUNTIME_LIGHT_ALLOCATOR_TEST_RUNTIME_CORE_SOURCES} + ${BASE_DIR}/runtime-common/stdlib/serialization/json-functions.cpp + ${BASE_DIR}/runtime-common/stdlib/serialization/serialize-functions.cpp ${BASE_DIR}/runtime-light/allocator/runtime-light-allocator.cpp - ${BASE_DIR}/runtime-light/components/confdata/state/confdata-storage.cpp ${BASE_DIR}/runtime-light/memory-resource-impl/monotonic-light-buffer-resource.cpp + ${BASE_DIR}/runtime-light/stdlib/confdata/confdata-storage.cpp + ${BASE_DIR}/runtime-light/stdlib/confdata/confdata-keys.cpp + ${BASE_DIR}/runtime-light/stdlib/confdata/predefined-wildcards.cpp ${BASE_DIR}/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp) add_executable(unittests-runtime-light-allocator ${RUNTIME_LIGHT_ALLOCATOR_TEST_SOURCES})