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-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-common/core/memory-resource/resource_allocator.h b/runtime-common/core/memory-resource/resource_allocator.h index eafe25610e..3c610e304b 100644 --- a/runtime-common/core/memory-resource/resource_allocator.h +++ b/runtime-common/core/memory-resource/resource_allocator.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include @@ -88,6 +89,9 @@ using vector = std::vector>; template using list = std::list>; +template +using forward_list = std::forward_list>; + template using string = std::basic_string, resource_allocator>; } // namespace stl diff --git a/runtime-common/core/std/containers.h b/runtime-common/core/std/containers.h index d0b2563973..4af86ce097 100644 --- a/runtime-common/core/std/containers.h +++ b/runtime-common/core/std/containers.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include #include @@ -41,6 +42,9 @@ using queue = std::queue>>; template class Allocator> using list = std::list>; +template class Allocator> +using forward_list = std::forward_list>; + template class Allocator> using vector = std::vector>; diff --git a/runtime-light/allocator/allocator.h b/runtime-light/allocator/allocator.h index bd625d9ebc..9ab181ca77 100644 --- a/runtime-light/allocator/allocator.h +++ b/runtime-light/allocator/allocator.h @@ -6,7 +6,13 @@ #include #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 +23,23 @@ auto make_unique_on_script_memory(Args&&... args) noexcept { } namespace kphp::memory { + +// 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 { + kphp::log::assertion(std::addressof(allocator.current_script_memory_resource()) == std::addressof(resource)); + std::ignore = 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/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/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 ecff9f47b1..c19c7438d9 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}/stdlib/confdata/confdata-storage.cpp + ${RUNTIME_LIGHT_DIR}/stdlib/confdata/confdata-keys.cpp + ${RUNTIME_LIGHT_DIR}/stdlib/confdata/predefined-wildcards.cpp) set(K2_CONFDATA_TL_SRC ${RUNTIME_LIGHT_DIR}/tl/tl-types.cpp @@ -18,18 +21,20 @@ 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_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}/") 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_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/component-state.cpp b/runtime-light/components/confdata/state/component-state.cpp index 335a56a1eb..225571c3aa 100644 --- a/runtime-light/components/confdata/state/component-state.cpp +++ b/runtime-light/components/confdata/state/component-state.cpp @@ -4,23 +4,99 @@ #include "runtime-light/components/confdata/state/component-state.h" +#include +#include +#include +#include #include +#include #include "runtime-light/k2-platform/k2-api.h" +#include "runtime-light/stdlib/confdata/predefined-wildcards.h" #include "runtime-light/stdlib/diagnostics/logs.h" +auto ComponentState::parse_confdata_memory_limit_arg(std::string_view value_view) noexcept -> void { + size_t parsed{}; + const auto [end, error]{std::from_chars(value_view.begin(), value_view.end(), parsed)}; + if (value_view.empty() || error != std::errc{} || end != value_view.end() || parsed == 0) [[unlikely]] { + kphp::log::error("{} must be a positive integer, got '{}'", CONFDATA_MEMORY_LIMIT_ARG, value_view); + } + m_confdata_memory_limit = parsed; +} + auto ComponentState::parse_confdata_proxy_actor_name_arg(std::string_view value_view) noexcept -> void { m_confdata_proxy_actor_name = value_view; } +auto ComponentState::parse_predefined_wildcards_arg(std::string_view value_view) noexcept -> void { + m_predefined_wildcards.clear(); + m_predefined_wildcards_storage.assign(value_view); + + const std::string_view storage_view{m_predefined_wildcards_storage}; + size_t line_number{1}; + size_t line_begin{}; + while (line_begin < storage_view.size()) { + const size_t line_end{storage_view.find('\n', line_begin)}; + const auto wildcard{storage_view.substr(line_begin, line_end - line_begin)}; + if (wildcard.empty()) [[unlikely]] { + kphp::log::error("{} contains an empty line: line -> {}", PREDEFINED_WILDCARDS_ARG, line_number); + } + if (wildcard.contains('\r')) [[unlikely]] { + kphp::log::error("{} contains a carriage return: line -> {}", PREDEFINED_WILDCARDS_ARG, line_number); + } + if (const auto validated{kphp::confdata::validate_predefined_wildcard(wildcard)}; !validated) [[unlikely]] { + kphp::log::error("{} contains an invalid wildcard: line -> {}, error -> {}", PREDEFINED_WILDCARDS_ARG, line_number, validated.error()); + } + m_predefined_wildcards.emplace_back(wildcard); + + if (line_end == std::string_view::npos) { + break; + } + line_begin = line_end + 1; + ++line_number; + } + if (!storage_view.empty() && storage_view.back() == '\n') [[unlikely]] { + kphp::log::error("{} contains an empty trailing line; use the YAML '|-' block style", PREDEFINED_WILDCARDS_ARG); + } + + std::ranges::sort(m_predefined_wildcards); + m_predefined_wildcards.erase(std::ranges::unique(m_predefined_wildcards).begin(), m_predefined_wildcards.end()); +} + +auto ComponentState::parse_initial_instance_memory_size_arg(std::string_view value_view) noexcept -> void { + size_t parsed{}; + const auto [end, error]{std::from_chars(value_view.begin(), value_view.end(), parsed)}; + if (value_view.empty() || error != std::errc{} || end != value_view.end() || parsed == 0) [[unlikely]] { + kphp::log::error("{} must be a positive integer, got '{}'", INITIAL_INSTANCE_MEMORY_SIZE_ARG, value_view); + } + m_initial_instance_memory_size = parsed; +} + +auto ComponentState::parse_min_instance_extra_memory_size_arg(std::string_view value_view) noexcept -> void { + size_t parsed{}; + const auto [end, error]{std::from_chars(value_view.begin(), value_view.end(), parsed)}; + if (value_view.empty() || error != std::errc{} || end != value_view.end() || parsed == 0) [[unlikely]] { + kphp::log::error("{} must be a positive integer, got '{}'", MIN_INSTANCE_EXTRA_MEMORY_SIZE_ARG, value_view); + } + m_min_instance_extra_memory_size = parsed; +} + auto ComponentState::parse_args() noexcept -> void { - for (auto i = 0; i < m_argc; ++i) { + for (auto i{0}; i < m_argc; ++i) { const auto [arg_key, arg_value]{k2::arg_fetch(i)}; const std::string_view key_view{arg_key.get(), std::strlen(arg_key.get())}; const std::string_view value_view{arg_value.get(), std::strlen(arg_value.get())}; - if (key_view == CONFDATA_PROXY_ACTOR_NAME_ARG) { + if (key_view == CONFDATA_MEMORY_LIMIT_ARG) { + parse_confdata_memory_limit_arg(value_view); + } else if (key_view == CONFDATA_PROXY_ACTOR_NAME_ARG) { parse_confdata_proxy_actor_name_arg(value_view); + } else if (key_view == PREDEFINED_WILDCARDS_ARG) { + parse_predefined_wildcards_arg(value_view); + } else if (key_view == INITIAL_INSTANCE_MEMORY_SIZE_ARG) { + parse_initial_instance_memory_size_arg(value_view); + } else if (key_view == MIN_INSTANCE_EXTRA_MEMORY_SIZE_ARG) { + parse_min_instance_extra_memory_size_arg(value_view); } else { kphp::log::error("unexpected argument: {}", key_view); } diff --git a/runtime-light/components/confdata/state/component-state.h b/runtime-light/components/confdata/state/component-state.h index 9e39724264..4ac3a06b8c 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,47 @@ 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; + 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&; 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_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 auto INIT_COMPONENT_ALLOCATOR_SIZE{static_cast(1024U * 1024U)}; // 1MiB + static constexpr std::string_view PREDEFINED_WILDCARDS_ARG{"predefined-wildcards"}; + static constexpr std::string_view INITIAL_INSTANCE_MEMORY_SIZE_ARG{"initial-instance-memory-size"}; + static constexpr std::string_view MIN_INSTANCE_EXTRA_MEMORY_SIZE_ARG{"min-instance-extra-memory-size"}; + static constexpr auto INIT_COMPONENT_ALLOCATOR_SIZE{static_cast(1024U * 1024U)}; // 1MiB + static constexpr auto DEFAULT_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 { 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/instance-state.cpp b/runtime-light/components/confdata/state/instance-state.cpp index 01510f1bb1..d012be19de 100644 --- a/runtime-light/components/confdata/state/instance-state.cpp +++ b/runtime-light/components/confdata/state/instance-state.cpp @@ -7,31 +7,182 @@ #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 +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]] { + return std::unexpected{ + confdata_piece_creation_error{.m_stage = creation_stage::memory_size, .m_code = static_cast(std::to_underlying(shared_memory_size.error()))}}; + } + + const auto shared_memory{k2::alloc_shared_memory(*shared_memory_size, kphp::confdata::storage::memory_alignment())}; + if (!shared_memory) [[unlikely]] { + return std::unexpected{confdata_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}; + } + 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 @@ -51,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 8bd4c66434..f58da11cbe 100644 --- a/runtime-light/components/confdata/state/instance-state.h +++ b/runtime-light/components/confdata/state/instance-state.h @@ -6,40 +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/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::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/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 a066930941..115170deea 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -129,6 +129,39 @@ 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 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]] { + return std::unexpected{error_code}; + } + return {}; +} + +inline std::expected, int32_t> get_shared_memory(std::string_view name) noexcept { + const void* pointer{}; + size_t size{}; + if (const auto error_code{k2_get_shared_memory(name.data(), name.size(), std::addressof(pointer), std::addressof(size))}; error_code != k2::errno_ok) + [[unlikely]] { + return std::unexpected{error_code}; + } + return std::span{static_cast(pointer), size}; +} + [[noreturn]] inline void exit(int32_t exit_code) noexcept { k2_exit(exit_code); } diff --git a/runtime-light/k2-platform/k2-header.h b/runtime-light/k2-platform/k2-header.h index 383a551f06..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-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-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-keys.cpp b/runtime-light/stdlib/confdata/confdata-keys.cpp new file mode 100644 index 0000000000..9957e01f7f --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-keys.cpp @@ -0,0 +1,66 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#include "runtime-light/stdlib/confdata/confdata-keys.h" + +#include +#include +#include +#include +#include + +#include "common/php-functions.h" + +namespace { + +/** + * @brief Normalizes a key remainder like a PHP array key: numeric strings become `int64_t`. + */ +auto normalize_remainder(std::string_view remainder) noexcept -> kphp::confdata::key_views::remainder_type { + int64_t remainder_as_int{0}; + if (!remainder.empty() && php_try_to_int(remainder.data(), remainder.size(), std::addressof(remainder_as_int))) { + return {remainder_as_int}; + } + return {remainder}; +} + +} // namespace + +namespace kphp::confdata { + +auto split_key(std::string_view key) noexcept -> std::expected { + if (key.size() > MAX_KEY_LENGTH) [[unlikely]] { + return std::unexpected{split_error::key_too_long}; + } + + const auto first_dot{key.find('.')}; + if (first_dot == std::string_view::npos) { + return key_views{section_kind::simple_key, key, key, key_views::remainder_type{}}; + } + const auto second_dot{key.find('.', first_dot + 1)}; + if (second_dot == std::string_view::npos) { + return key_views{section_kind::one_dot_wildcard, key, key.substr(0, first_dot + 1), normalize_remainder(key.substr(first_dot + 1))}; + } + return key_views{section_kind::two_dots_wildcard, key, key.substr(0, second_dot + 1), normalize_remainder(key.substr(second_dot + 1))}; +} + +auto split_key(std::string_view key, const predefined_wildcards& wildcards) noexcept -> std::expected { + // if the key has a predefined wildcard prefix, use the shortest matching one as the section + if (const auto opt_wildcard{wildcards.shortest_matching_wildcard(key)}; opt_wildcard.has_value()) { + return split_key_with_predefined_wildcard(key, opt_wildcard->size()); + } + return split_key(key); +} + +auto split_key_with_predefined_wildcard(std::string_view key, size_t wildcard_len) noexcept -> std::expected { + if (key.size() > MAX_KEY_LENGTH) [[unlikely]] { + return std::unexpected{split_error::key_too_long}; + } + if (wildcard_len == 0 || wildcard_len > key.size()) [[unlikely]] { + return std::unexpected{split_error::invalid_predefined_wildcard_length}; + } + return key_views{section_kind::predefined_wildcard, key, key.substr(0, wildcard_len), normalize_remainder(key.substr(wildcard_len))}; +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/confdata-keys.h b/runtime-light/stdlib/confdata/confdata-keys.h new file mode 100644 index 0000000000..e12d70ae0d --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-keys.h @@ -0,0 +1,195 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "common/mixin/not_copyable.h" +#include "common/wrappers/overloaded.h" +#include "runtime-common/core/runtime-core.h" +#include "runtime-light/stdlib/confdata/predefined-wildcards.h" +#include "runtime-light/stdlib/confdata/wildcard-kind.h" + +// A port of runtime/confdata-keys.h (minus the blacklist) shared by the confdata component and the kphp client. +// The client never includes this header directly; it's an implementation detail of the confdata sample reader/writer. +// +// Confdata keys are stored denormalized in two levels: a key is split into a `section` +// (the top-level storage key) and a `remainder` (the key inside the section's array): +// - "key" -> section "key" (section_kind::simple_key) +// - "a.b..." -> section "a.", remainder "b..." (section_kind::one_dot_wildcard) +// - "a.b.c..." -> section "a.b.", remainder "c..." (section_kind::two_dots_wildcard) +// - "predefined..." -> section = the matching predefined wildcard (section_kind::predefined_wildcard) +namespace kphp::confdata { + +/** + * @brief Classifies `section`; a would-be predefined wildcard that is not configured is reported + * as `section_kind::simple_key`. + */ +inline auto classify_section(std::string_view section, const predefined_wildcards& wildcards) noexcept -> section_kind { + const auto kind{classify_wildcard_form(section)}; + return kind != section_kind::predefined_wildcard || wildcards.contains(section) ? kind : section_kind::simple_key; +} + +enum class split_error : uint8_t { key_too_long, invalid_predefined_wildcard_length, not_a_two_dots_key }; + +/** + * @brief The decomposition of a confdata key as zero-copy views into the key. + * Instances are produced only by the `split_key*` factories, so every `key_views` + * is guaranteed to satisfy the protocol length bound (`int16_t`). + */ +struct key_views { + // the remainder of a key: absent for simple keys, int-normalized like a PHP array key otherwise + using remainder_type = std::variant; + +private: + section_kind m_section_kind; + std::string_view m_raw_key; + std::string_view m_section; + remainder_type m_remainder; + + key_views(section_kind section_kind, std::string_view raw_key, std::string_view section, remainder_type remainder) noexcept; + + friend auto split_key(std::string_view key) noexcept -> std::expected; + friend auto split_key(std::string_view key, const predefined_wildcards& wildcards) noexcept -> std::expected; + friend auto split_key_with_predefined_wildcard(std::string_view key, size_t wildcard_len) noexcept -> std::expected; + +public: + key_views() = delete; + + auto kind() const noexcept -> section_kind; + auto raw_key() const noexcept -> std::string_view; + auto section() const noexcept -> std::string_view; + auto remainder() const noexcept -> const remainder_type&; + + /** + * @return The one-dot duplicate of a two-dot key (`a.b.c...` -> section `a.`, remainder `b.c...`), + * or `split_error::not_a_two_dots_key`. + */ + auto reinterpret_two_dots_as_one_dot() const noexcept -> std::expected; +}; + +inline key_views::key_views(section_kind section_kind, std::string_view raw_key, std::string_view section, remainder_type remainder) noexcept + : m_section_kind{section_kind}, + m_raw_key{raw_key}, + m_section{section}, + m_remainder{remainder} {} + +inline auto key_views::kind() const noexcept -> section_kind { + return m_section_kind; +} + +inline auto key_views::raw_key() const noexcept -> std::string_view { + return m_raw_key; +} + +inline auto key_views::section() const noexcept -> std::string_view { + return m_section; +} + +inline auto key_views::remainder() const noexcept -> const remainder_type& { + return m_remainder; +} + +inline auto key_views::reinterpret_two_dots_as_one_dot() const noexcept -> std::expected { + if (m_section_kind != section_kind::two_dots_wildcard) { + return std::unexpected{split_error::not_a_two_dots_key}; + } + // a two-dot key always contains a dot, and the remainder after the first dot always contains another one, + // so the remainder is never numeric and needs no int-normalization + const auto first_dot{m_raw_key.find('.')}; + const auto remainder{m_raw_key.substr(first_dot + 1)}; + return key_views{section_kind::one_dot_wildcard, m_raw_key, m_raw_key.substr(0, first_dot + 1), remainder_type{remainder}}; +} + +// ================================================================================================ + +/** + * @brief Splits `key` into the section (up to the first/second dot) and the int-normalized remainder. + */ +auto split_key(std::string_view key) noexcept -> std::expected; + +/** + * @brief Splits `key` using the shortest matching predefined wildcard as the section, if any. + */ +auto split_key(std::string_view key, const predefined_wildcards& wildcards) noexcept -> std::expected; + +/** + * @brief Splits `key` using the explicitly given predefined wildcard length as the section. + */ +auto split_key_with_predefined_wildcard(std::string_view key, size_t wildcard_len) noexcept -> std::expected; + +// ================================================================================================ + +/** + * @brief Materializes validated key views into runtime handles (`string`/`mixed`) for storage lookups, + * allocation-free: the handles are placement-constructed into the internal stack buffers. + * Immovable, since the handles point into the object's own buffers. + */ +class key_handles : vk::not_copyable { // NOLINT(*member-init) + // Buffers precede the handles so that the handles are destroyed before the storage they refer to. + alignas(std::max_align_t) std::array::max() + 1> m_section_buffer; + alignas(std::max_align_t) std::array::max() + 1> m_remainder_buffer; + + string m_section; + mixed m_remainder; + +public: + explicit key_handles(const key_views& views) noexcept; // NOLINT(*member-init) + + auto section() const noexcept -> const string&; + + auto remainder() const noexcept -> const mixed&; + + /** + * @return A heap copy of the section; the internal section aliases the stack buffer so it must not escape the handles object. + */ + auto make_section_copy() const noexcept -> string; + + /** + * @return A heap copy of the remainder; the internal remainder aliases the stack buffer so it must not escape the handles object. + */ + auto make_remainder_copy() const noexcept -> mixed; +}; + +inline key_handles::key_handles(const key_views& views) noexcept { // NOLINT(*member-init) + m_section = views.section().empty() ? string{} + : string::make_const_string_on_memory(views.section().data(), static_cast(views.section().size()), + m_section_buffer.data(), m_section_buffer.size()); + m_remainder = std::visit(overloaded{ + [](std::monostate) noexcept -> mixed { return mixed{}; }, + [](int64_t remainder) noexcept -> mixed { return mixed{remainder}; }, + [this](std::string_view remainder) noexcept -> mixed { + return remainder.empty() + ? mixed{string{}} + : mixed{string::make_const_string_on_memory(remainder.data(), static_cast(remainder.size()), + m_remainder_buffer.data(), m_remainder_buffer.size())}; + }, + }, + views.remainder()); +} + +inline auto key_handles::section() const noexcept -> const string& { + return m_section; +} + +inline auto key_handles::remainder() const noexcept -> const mixed& { + return m_remainder; +} + +inline auto key_handles::make_section_copy() const noexcept -> string { + return m_section.copy_and_make_not_shared(); +} + +inline auto key_handles::make_remainder_copy() const noexcept -> mixed { + return m_remainder.is_string() ? mixed{m_remainder.as_string().copy_and_make_not_shared()} : m_remainder; +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/confdata-reader-lease.h b/runtime-light/stdlib/confdata/confdata-reader-lease.h new file mode 100644 index 0000000000..a67f0108e2 --- /dev/null +++ b/runtime-light/stdlib/confdata/confdata-reader-lease.h @@ -0,0 +1,75 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "runtime-light/stdlib/confdata/confdata-storage.h" + +namespace kphp::confdata { + +/** Fixed-size handshake sent when the component grants a reader sample lease. */ +class reader_lease final { + static constexpr uint64_t MAGIC{0x4b32'4344'4c45'4153}; // "K2CDLEAS" + static constexpr uint32_t VERSION{1}; + static constexpr size_t MAX_SHARED_MEMORY_NAME_SIZE{128}; + + /** Identifies this wire layout and rejects unrelated stream payloads. */ + uint64_t m_magic{MAGIC}; + /** Allows the handshake layout to evolve independently of storage layout. */ + uint32_t m_version{VERSION}; + /** Selects the immutable confdata generation pinned by the component. */ + storage::sample_id m_sample_id{storage::INVALID_SAMPLE_ID}; + /** Number of meaningful bytes in `m_shared_memory_name`. */ + uint32_t m_shared_memory_name_size{}; + /** Name passed to `k2_get_shared_memory`; it is not null-terminated. */ + std::array m_shared_memory_name{}; + +public: + reader_lease() noexcept = default; + + static auto create(std::string_view shared_memory_name, storage::sample_id sample_id) noexcept -> std::optional; + + auto is_valid() const noexcept -> bool; + auto sample_id() const noexcept -> storage::sample_id; + auto shared_memory_name() const noexcept -> std::string_view; +}; + +inline auto reader_lease::create(std::string_view shared_memory_name, storage::sample_id sample_id) noexcept -> std::optional { + if (shared_memory_name.empty() || shared_memory_name.size() > MAX_SHARED_MEMORY_NAME_SIZE || shared_memory_name.contains('\0') || + !storage::is_valid_sample_id(sample_id)) [[unlikely]] { + return std::nullopt; + } + + reader_lease lease{}; + lease.m_sample_id = sample_id; + lease.m_shared_memory_name_size = static_cast(shared_memory_name.size()); + std::ranges::copy(shared_memory_name, lease.m_shared_memory_name.begin()); + return lease; +} + +inline auto reader_lease::is_valid() const noexcept -> bool { + return m_magic == MAGIC && m_version == VERSION && storage::is_valid_sample_id(m_sample_id) && m_shared_memory_name_size != 0 && + m_shared_memory_name_size <= m_shared_memory_name.size() && !shared_memory_name().contains('\0'); +} + +inline auto reader_lease::sample_id() const noexcept -> storage::sample_id { + return m_sample_id; +} + +inline auto reader_lease::shared_memory_name() const noexcept -> std::string_view { + const auto size{std::min(static_cast(m_shared_memory_name_size), m_shared_memory_name.size())}; + return {m_shared_memory_name.data(), size}; +} + +static_assert(std::is_trivially_copyable_v); + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/confdata-state.cpp b/runtime-light/stdlib/confdata/confdata-state.cpp new file mode 100644 index 0000000000..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/predefined-wildcards.cpp b/runtime-light/stdlib/confdata/predefined-wildcards.cpp new file mode 100644 index 0000000000..5a2ad73ea6 --- /dev/null +++ b/runtime-light/stdlib/confdata/predefined-wildcards.cpp @@ -0,0 +1,100 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#include "runtime-light/stdlib/confdata/predefined-wildcards.h" + +#include +#include +#include +#include +#include +#include + +namespace kphp::confdata { + +auto predefined_wildcards::shortest_matching_wildcard(std::string_view key) const noexcept -> std::optional { + const auto candidates{find_matching_candidates(key)}; + for (const auto& wildcard : candidates.wildcards) { + if (candidates.key_tail.starts_with(wildcard.substr(m_shortest_wildcard_size))) { + return wildcard; + } + } + return std::nullopt; +} + +auto predefined_wildcards::is_top_level_wildcard(std::string_view wildcard) const noexcept -> bool { + const auto shortest{shortest_matching_wildcard(wildcard)}; + return shortest.has_value() && *shortest == wildcard; +} + +auto predefined_wildcards::has_matching_wildcard(std::string_view key) const noexcept -> bool { + return shortest_matching_wildcard(key).has_value(); +} + +auto predefined_wildcards::initialize(std::span wildcards) noexcept -> std::expected { + if (m_initialized) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::already_initialized}; + } + + std::string_view previous{}; + bool first{true}; + for (const auto& wildcard : wildcards) { + if (const auto validated{validate_predefined_wildcard(wildcard)}; !validated) [[unlikely]] { + return std::unexpected{validated.error()}; + } + if (!first && previous >= wildcard) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::non_canonical_wildcards}; + } + previous = wildcard; + first = false; + } + + m_wildcards.reserve(wildcards.size()); + for (const auto& wildcard : wildcards) { + const auto [it, inserted]{m_wildcards.emplace(wildcard, wildcard_string::allocator_type{m_resource})}; + if (!inserted) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::internal}; + } + + const std::string_view stored{*it}; + if (m_shortest_wildcard_size == 0 || stored.size() < m_shortest_wildcard_size) { + m_shortest_wildcard_size = stored.size(); + } + } + + m_groups.reserve(m_wildcards.size()); + for (const auto& wildcard_string : m_wildcards) { + const std::string_view wildcard{wildcard_string}; + const auto group_it{m_groups.try_emplace(wildcard.substr(0, m_shortest_wildcard_size), wildcard_group::allocator_type{m_resource}).first}; + auto& group{group_it->second}; + group.emplace_back(wildcard); + } + + for (auto& [_, group] : m_groups) { + std::ranges::sort(group); + for (size_t i{}; i < group.size(); ++i) { + size_t matches{}; + for (size_t j{}; j <= i; ++j) { + matches += static_cast(group[i].starts_with(group[j])); + } + m_max_matches_per_key = std::max(m_max_matches_per_key, matches); + } + } + + m_initialized = true; + return {}; +} + +auto predefined_wildcards::find_matching_candidates(std::string_view key) const noexcept -> matching_candidates { + if (m_groups.empty() || key.size() < m_shortest_wildcard_size) { + return {}; + } + const auto group_it{m_groups.find(key.substr(0, m_shortest_wildcard_size))}; + if (group_it == m_groups.end()) { + return {}; + } + return {.wildcards = group_it->second, .key_tail = key.substr(m_shortest_wildcard_size)}; +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/confdata/predefined-wildcards.h b/runtime-light/stdlib/confdata/predefined-wildcards.h new file mode 100644 index 0000000000..1b38ba403b --- /dev/null +++ b/runtime-light/stdlib/confdata/predefined-wildcards.h @@ -0,0 +1,191 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/mixin/not_copyable.h" +#include "runtime-common/core/memory-resource/resource_allocator.h" +#include "runtime-common/core/memory-resource/unsynchronized_pool_resource.h" +#include "runtime-light/stdlib/confdata/wildcard-kind.h" + +namespace kphp::confdata { + +inline constexpr auto MAX_KEY_LENGTH{static_cast(std::numeric_limits::max())}; + +enum class predefined_wildcards_error : uint8_t { + empty_wildcard, + wildcard_too_long, + reserved_wildcard, + non_canonical_wildcards, + already_initialized, + internal, +}; + +inline auto validate_predefined_wildcard(std::string_view wildcard) noexcept -> std::expected { + if (wildcard.empty()) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::empty_wildcard}; + } + if (wildcard.size() > MAX_KEY_LENGTH) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::wildcard_too_long}; + } + if (classify_wildcard_form(wildcard) != section_kind::predefined_wildcard) [[unlikely]] { + return std::unexpected{predefined_wildcards_error::reserved_wildcard}; + } + return {}; +} + +class storage; + +/** + * An immutable index of configured predefined wildcards. + * + * The owning strings and both lookup indexes retain the storage resource in + * their allocators, so their allocation domain does not depend on whichever + * script resource happens to be installed by the caller. + */ +class predefined_wildcards final : private vk::not_copyable { + using resource_type = memory_resource::unsynchronized_pool_resource; + using wildcard_string = memory_resource::stl::string; + + struct transparent_string_hash final { + using is_transparent = void; + + auto operator()(std::string_view value) const noexcept -> size_t { + return std::hash{}(value); + } + }; + + struct transparent_string_equal final { + using is_transparent = void; + + auto operator()(std::string_view lhs, std::string_view rhs) const noexcept -> bool { + return lhs == rhs; + } + }; + + using wildcard_set = memory_resource::stl::unordered_set; + using wildcard_group = memory_resource::stl::vector; + using wildcard_groups = + memory_resource::stl::unordered_map; + + /** Candidates from one shortest-prefix group and the unmatched part of the queried key. */ + struct matching_candidates final { + std::span wildcards; + std::string_view key_tail; + }; + + resource_type& m_resource; + // Owns each complete wildcard exactly once. References remain stable across + // unordered-set rehashes and the set is never mutated after initialization. + wildcard_set m_wildcards; + // Maps a shortest-length prefix to sorted views into `m_wildcards`. + wildcard_groups m_groups; + size_t m_shortest_wildcard_size{}; + size_t m_max_matches_per_key{}; + bool m_initialized{}; + +public: + explicit predefined_wildcards(resource_type& resource) noexcept; + + /** + * Invokes `f(wildcard)` for every configured wildcard that is a prefix of + * `key`, in ascending length order. + * + * @return True if at least one wildcard matched. + */ + template F> + auto for_each_matching_wildcard(std::string_view key, const F& f) const noexcept -> bool; + + /** @return The shortest configured wildcard that is a prefix of `key`, if any. */ + auto shortest_matching_wildcard(std::string_view key) const noexcept -> std::optional; + + /** @return The exact maximum number of configured wildcards that can match one key. */ + auto max_matches_per_key() const noexcept -> size_t; + + /** @return True if `wildcard` is configured. */ + auto contains(std::string_view wildcard) const noexcept -> bool; + + /** @return True if `wildcard` is configured and has no shorter configured wildcard prefix. */ + auto is_top_level_wildcard(std::string_view wildcard) const noexcept -> bool; + + /** @return True if at least one configured wildcard is a prefix of `key`. */ + auto has_matching_wildcard(std::string_view key) const noexcept -> bool; + +private: + /** Initializes the index from sorted, unique wildcards under the storage resource. */ + auto initialize(std::span wildcards) noexcept -> std::expected; + + auto find_matching_candidates(std::string_view key) const noexcept -> matching_candidates; + + friend class storage; +}; + +inline predefined_wildcards::predefined_wildcards(resource_type& resource) noexcept + : m_resource{resource}, + m_wildcards{wildcard_set::allocator_type{resource}}, + m_groups{wildcard_groups::allocator_type{resource}} {} + +template F> +auto predefined_wildcards::for_each_matching_wildcard(std::string_view key, const F& f) const noexcept -> bool { + const auto candidates{find_matching_candidates(key)}; + bool matched{}; + for (const auto& wildcard : candidates.wildcards) { + const auto wildcard_tail{wildcard.substr(m_shortest_wildcard_size)}; + if (candidates.key_tail.starts_with(wildcard_tail)) { + std::invoke(f, wildcard); + matched = true; + } + } + return matched; +} + +inline auto predefined_wildcards::max_matches_per_key() const noexcept -> size_t { + return m_max_matches_per_key; +} + +inline auto predefined_wildcards::contains(std::string_view wildcard) const noexcept -> bool { + return m_wildcards.contains(wildcard); +} + +} // namespace kphp::confdata + +template<> +struct std::formatter { + template + constexpr auto parse(ParseContext& ctx) const noexcept { + return ctx.begin(); + } + + template + auto format(kphp::confdata::predefined_wildcards_error error, FmtContext& ctx) const noexcept { + using kphp::confdata::predefined_wildcards_error; + + switch (error) { + case predefined_wildcards_error::empty_wildcard: + return std::format_to(ctx.out(), "empty wildcard"); + case predefined_wildcards_error::wildcard_too_long: + return std::format_to(ctx.out(), "wildcard is longer than the confdata key protocol limit"); + case predefined_wildcards_error::reserved_wildcard: + return std::format_to(ctx.out(), "wildcard uses the implicit one-dot or two-dot form"); + case predefined_wildcards_error::non_canonical_wildcards: + return std::format_to(ctx.out(), "wildcards are not sorted and unique"); + case predefined_wildcards_error::already_initialized: + return std::format_to(ctx.out(), "wildcards are already initialized"); + case predefined_wildcards_error::internal: + return std::format_to(ctx.out(), "unexpected internal error"); + } + return std::format_to(ctx.out(), "unknown wildcard error"); + } +}; diff --git a/runtime-light/stdlib/confdata/wildcard-kind.h b/runtime-light/stdlib/confdata/wildcard-kind.h new file mode 100644 index 0000000000..bfe24d5e8e --- /dev/null +++ b/runtime-light/stdlib/confdata/wildcard-kind.h @@ -0,0 +1,41 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include + +namespace kphp::confdata { + +enum class section_kind : uint8_t { simple_key, one_dot_wildcard, two_dots_wildcard, predefined_wildcard }; + +/** + * @brief Classifies the syntactic form of a wildcard section. + * + * Only trailing-dot forms with exactly one or two dots are implicit sections. All other forms are + * predefined-wildcard candidates and still require validation and presence in the configured index. + */ +inline auto classify_wildcard_form(std::string_view wildcard) noexcept -> section_kind { + size_t dots{}; + if (!wildcard.empty() && wildcard.back() == '.') { + for (const char c : wildcard) { + dots += static_cast(c == '.'); + if (dots > 2) { + break; + } + } + } + switch (dots) { + case 1: + return section_kind::one_dot_wildcard; + case 2: + return section_kind::two_dots_wildcard; + default: + return section_kind::predefined_wildcard; + } +} + +} // namespace kphp::confdata diff --git a/runtime-light/stdlib/stdlib.cmake b/runtime-light/stdlib/stdlib.cmake index 8776da4dc4..81fc965542 100644 --- a/runtime-light/stdlib/stdlib.cmake +++ b/runtime-light/stdlib/stdlib.cmake @@ -1,7 +1,11 @@ prepend( RUNTIME_LIGHT_STDLIB_SRC stdlib/ + confdata/confdata-state.cpp + confdata/confdata-storage.cpp + confdata/confdata-keys.cpp confdata/confdata-functions.cpp + confdata/predefined-wildcards.cpp crypto/crypto-functions.cpp diagnostics/backtrace.cpp diagnostics/php-assert.cpp diff --git a/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..b26cc04507 --- /dev/null +++ b/tests/cpp/runtime-light/allocator/script-memory-resource-test.cpp @@ -0,0 +1,580 @@ +// 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 +#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/k2-platform/k2-api.h" +#include "runtime-light/stdlib/confdata/confdata-storage.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__) + +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())}; + 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); +} + +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(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}; + + 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); + + 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{}; + + 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); + + 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 + +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_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(); + 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 new file mode 100644 index 0000000000..73e2f333dc --- /dev/null +++ b/tests/cpp/runtime-light/confdata/predefined-wildcards-test.cpp @@ -0,0 +1,543 @@ +// 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 +#include +#include + +#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 { + +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]] { + 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__) + +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()); + } + + ~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 { + std::vector result{}; + wildcards.for_each_matching_wildcard(key, [&result](std::string_view wildcard) { result.emplace_back(wildcard); }); + 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); + 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 { + 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()); + CHECK(!wildcards.has_matching_wildcard(key)); + } + CHECK(wildcards.max_matches_per_key() == 0); + 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); + } +} + +auto test_predefined_wildcards_matrix() -> void { + 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()); + } + 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"); + 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")); +} + +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& 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 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_noncanonical_wildcards() -> void { + const std::vector noncanonical{"b", "a"}; + 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 { + 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); + } +} + +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 { + // 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(".")}, + {"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)}; + CHECK(!oversized.has_value()); + 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_shared_storage_index(); + test_noncanonical_wildcards(); + 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(); + 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 new file mode 100644 index 0000000000..6876d66e85 --- /dev/null +++ b/tests/cpp/runtime-light/runtime-light-tests.cmake @@ -0,0 +1,37 @@ +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 + ${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) + +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) + +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 + ${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/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}) +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) 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)