From 3e46d1562706238b0d3b439abbea3cbcb0a6f99b Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 20 Mar 2026 12:02:08 +0300 Subject: [PATCH 01/20] [k2] add two errno codes to k2-api --- runtime-light/k2-platform/k2-api.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime-light/k2-platform/k2-api.h b/runtime-light/k2-platform/k2-api.h index 9b0c5f4874..656eb95bae 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -46,6 +46,8 @@ inline constexpr int32_t errno_eshutdown = ESHUTDOWN; inline constexpr int32_t errno_ecanceled = ECANCELED; inline constexpr int32_t errno_erange = ERANGE; inline constexpr int32_t errno_enoent = ENOENT; +inline constexpr int32_t errno_eopnotsupp = EOPNOTSUPP; +inline constexpr int32_t errno_einprogress = EINPROGRESS; using descriptor = uint64_t; inline constexpr k2::descriptor INVALID_PLATFORM_DESCRIPTOR = 0; From ac0e80fc02ef42124b1c73cda120cc6ffb7f9dcb Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 20 Mar 2026 12:03:17 +0300 Subject: [PATCH 02/20] [k2] move exit functions to stdlib/system/ --- runtime-light/stdlib/exit/exit-functions.h | 34 ----------------- .../stdlib/system/system-functions.h | 38 +++++++++++++++---- 2 files changed, 31 insertions(+), 41 deletions(-) delete mode 100644 runtime-light/stdlib/exit/exit-functions.h diff --git a/runtime-light/stdlib/exit/exit-functions.h b/runtime-light/stdlib/exit/exit-functions.h deleted file mode 100644 index bece1a20b1..0000000000 --- a/runtime-light/stdlib/exit/exit-functions.h +++ /dev/null @@ -1,34 +0,0 @@ -// Compiler for PHP (aka KPHP) -// Copyright (c) 2024 LLC «V Kontakte» -// Distributed under the GPL v3 License, see LICENSE.notice.txt - -#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/state/instance-state.h" -#include "runtime-light/stdlib/fork/fork-functions.h" -#include "runtime-light/stdlib/output/output-state.h" - -inline kphp::coro::task<> f$exit(mixed v = 0) noexcept { // TODO: make it synchronous - auto& instance_st{InstanceState::get()}; - - int64_t exit_code{}; - if (v.is_string()) { - OutputInstanceState::get().output_buffers.current_buffer().get() << v; - } else if (v.is_int()) { - int64_t v_code{v.to_int()}; - exit_code = v_code >= 0 && v_code <= 254 ? v_code : 1; // valid PHP exit codes: [0..254] - } else { - exit_code = 1; - } - co_await kphp::forks::id_managed(instance_st.run_instance_epilogue()); - k2::exit(static_cast(exit_code)); -} - -inline kphp::coro::task<> f$die(mixed v = 0) noexcept { - co_await f$exit(std::move(v)); -} diff --git a/runtime-light/stdlib/system/system-functions.h b/runtime-light/stdlib/system/system-functions.h index a3c4affc62..2bc407c5fc 100644 --- a/runtime-light/stdlib/system/system-functions.h +++ b/runtime-light/stdlib/system/system-functions.h @@ -32,25 +32,32 @@ #include "runtime-light/k2-platform/k2-api.h" #include "runtime-light/state/component-state.h" #include "runtime-light/state/image-state.h" +#include "runtime-light/state/instance-state.h" #include "runtime-light/stdlib/diagnostics/contextual-tags.h" #include "runtime-light/stdlib/diagnostics/logs.h" #include "runtime-light/stdlib/fork/fork-functions.h" +#include "runtime-light/stdlib/output/output-state.h" #include "runtime-light/stdlib/system/system-state.h" namespace kphp::posix::impl { -constexpr std::string_view NAME_PWUID_KEY = "name"; -constexpr std::string_view PASSWD_PWUID_KEY = "passwd"; -constexpr std::string_view UID_PWUID_KEY = "uid"; -constexpr std::string_view GID_PWUID_KEY = "gid"; -constexpr std::string_view GECOS_PWUID_KEY = "gecos"; -constexpr std::string_view DIR_PWUID_KEY = "dir"; -constexpr std::string_view SHELL_PWUID_KEY = "shell"; +inline constexpr std::string_view NAME_PWUID_KEY = "name"; +inline constexpr std::string_view PASSWD_PWUID_KEY = "passwd"; +inline constexpr std::string_view UID_PWUID_KEY = "uid"; +inline constexpr std::string_view GID_PWUID_KEY = "gid"; +inline constexpr std::string_view GECOS_PWUID_KEY = "gecos"; +inline constexpr std::string_view DIR_PWUID_KEY = "dir"; +inline constexpr std::string_view SHELL_PWUID_KEY = "shell"; } // namespace kphp::posix::impl namespace kphp::system { +inline auto exit(int32_t exit_code) noexcept -> kphp::coro::task<> { + co_await InstanceState::get().run_instance_epilogue(); + k2::exit(exit_code); +} + template> output_handler_type = std::identity> auto exec(std::string_view cmd, const output_handler_type& output_handler = {}) noexcept -> std::expected { static constexpr std::string_view program{"sh"}; @@ -78,6 +85,23 @@ auto exec(std::string_view cmd, const output_handler_type& output_handler = {}) } // namespace kphp::system +inline kphp::coro::task<> f$exit(mixed v = 0) noexcept { // TODO: make it synchronous + int64_t exit_code{}; + if (v.is_string()) { + OutputInstanceState::get().output_buffers.current_buffer().get() << v; + } else if (v.is_int()) { + int64_t v_code{v.to_int()}; + exit_code = v_code >= 0 && v_code <= 254 ? v_code : 1; // valid PHP exit codes: [0..254] + } else { + exit_code = 1; + } + co_await kphp::forks::id_managed(kphp::system::exit(static_cast(exit_code))); +} + +inline kphp::coro::task<> f$die(mixed v = 0) noexcept { + co_await f$exit(std::move(v)); +} + template bool f$register_kphp_on_oom_callback(F&& /*callback*/) { kphp::log::warning("called stub register_kphp_on_oom_callback"); From b9bf474637d6011ea10a3be467ec8a176e29ef0d Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 20 Mar 2026 12:05:07 +0300 Subject: [PATCH 03/20] [k2] rename ScriptAllocatorManaged -> kphp::memory::script_allocator_managed --- .../core/allocator/script-allocator-managed.h | 10 +++++++--- .../core/class-instance/refcountable-php-classes.h | 4 ++-- runtime-light/allocator/allocator.h | 2 +- runtime-light/stdlib/rpc/rpc-tl-func-base.h | 2 +- runtime-light/stdlib/rpc/rpc-tl-request.h | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/runtime-common/core/allocator/script-allocator-managed.h b/runtime-common/core/allocator/script-allocator-managed.h index 707e7c051b..679a152894 100644 --- a/runtime-common/core/allocator/script-allocator-managed.h +++ b/runtime-common/core/allocator/script-allocator-managed.h @@ -8,13 +8,15 @@ #include "runtime-common/core/allocator/runtime-allocator.h" -class ScriptAllocatorManaged { +namespace kphp::memory { + +class script_allocator_managed { public: static void* operator new(size_t size) noexcept { return RuntimeAllocator::get().alloc_script_memory(size); } - static void* operator new(size_t, void* ptr) noexcept { + static void* operator new(size_t /*unused*/, void* ptr) noexcept { return ptr; } @@ -27,5 +29,7 @@ class ScriptAllocatorManaged { static void operator delete[](void* ptr) = delete; protected: - ~ScriptAllocatorManaged() = default; + ~script_allocator_managed() = default; }; + +} // namespace kphp::memory diff --git a/runtime-common/core/class-instance/refcountable-php-classes.h b/runtime-common/core/class-instance/refcountable-php-classes.h index e046200889..c3cea08249 100644 --- a/runtime-common/core/class-instance/refcountable-php-classes.h +++ b/runtime-common/core/class-instance/refcountable-php-classes.h @@ -9,7 +9,7 @@ #include "runtime-common/core/allocator/script-allocator-managed.h" -class abstract_refcountable_php_interface : public ScriptAllocatorManaged { +class abstract_refcountable_php_interface : public kphp::memory::script_allocator_managed { public: abstract_refcountable_php_interface() __attribute__((always_inline)) = default; virtual ~abstract_refcountable_php_interface() noexcept __attribute__((always_inline)) = default; @@ -98,7 +98,7 @@ class refcountable_polymorphic_php_classes_virt<> : public virtual abstract_refc }; template -class refcountable_php_classes : public ScriptAllocatorManaged { +class refcountable_php_classes : public kphp::memory::script_allocator_managed { public: void add_ref() noexcept { if (refcnt < ExtraRefCnt::for_global_const) { diff --git a/runtime-light/allocator/allocator.h b/runtime-light/allocator/allocator.h index cd418849a8..bd625d9ebc 100644 --- a/runtime-light/allocator/allocator.h +++ b/runtime-light/allocator/allocator.h @@ -10,7 +10,7 @@ #include "runtime-common/core/allocator/script-allocator-managed.h" #include "runtime-light/allocator/allocator-state.h" -template T, typename... Args> +template T, typename... Args> requires std::constructible_from auto make_unique_on_script_memory(Args&&... args) noexcept { return std::make_unique(std::forward(args)...); diff --git a/runtime-light/stdlib/rpc/rpc-tl-func-base.h b/runtime-light/stdlib/rpc/rpc-tl-func-base.h index 14568b4ac8..98d9262e37 100644 --- a/runtime-light/stdlib/rpc/rpc-tl-func-base.h +++ b/runtime-light/stdlib/rpc/rpc-tl-func-base.h @@ -9,7 +9,7 @@ #include "runtime-light/stdlib/diagnostics/logs.h" #include "runtime-light/stdlib/rpc/rpc-tl-function.h" -struct tl_func_base : ScriptAllocatorManaged { +struct tl_func_base : kphp::memory::script_allocator_managed { virtual mixed fetch() = 0; virtual class_instance typed_fetch() { diff --git a/runtime-light/stdlib/rpc/rpc-tl-request.h b/runtime-light/stdlib/rpc/rpc-tl-request.h index a83e572de9..7dd50bcf0d 100644 --- a/runtime-light/stdlib/rpc/rpc-tl-request.h +++ b/runtime-light/stdlib/rpc/rpc-tl-request.h @@ -11,7 +11,7 @@ #include "runtime-light/stdlib/rpc/rpc-tl-func-base.h" #include "runtime-light/stdlib/rpc/rpc-tl-function.h" -class RpcRequestResult : public ScriptAllocatorManaged { +class RpcRequestResult : public kphp::memory::script_allocator_managed { public: const bool is_typed{}; From 7c94510ff939f70138667ace30ed1c95e10fbee1 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 20 Mar 2026 12:06:31 +0300 Subject: [PATCH 04/20] [k2] add kphp::forks::scoped_id_managed to ease fork id management in some cases --- runtime-light/stdlib/fork/fork-functions.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/runtime-light/stdlib/fork/fork-functions.h b/runtime-light/stdlib/fork/fork-functions.h index b22ec2176a..01eb3658c3 100644 --- a/runtime-light/stdlib/fork/fork-functions.h +++ b/runtime-light/stdlib/fork/fork-functions.h @@ -26,6 +26,23 @@ namespace kphp::forks { +class scoped_id_managed { + ForkInstanceState& state{ForkInstanceState::get()}; + int64_t fork_id{state.current_id}; + +public: + scoped_id_managed() noexcept = default; + + ~scoped_id_managed() { + state.current_id = fork_id; + } + + scoped_id_managed(const scoped_id_managed&) = delete; + scoped_id_managed(scoped_id_managed&&) = delete; + scoped_id_managed& operator=(const scoped_id_managed&) = delete; + scoped_id_managed& operator=(scoped_id_managed&&) = delete; +}; + template auto id_managed(awaitable_type awaitable) noexcept -> kphp::coro::task::awaiter_return_type> { auto& fork_instance_st{ForkInstanceState::get()}; From 6c28747d5258d382ae6f6cf60f0d5b52f2d5f908 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 20 Mar 2026 12:17:13 +0300 Subject: [PATCH 05/20] [k2] make kphp::coro::event really movable and not copyable --- runtime-light/coroutine/event.h | 49 +++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/runtime-light/coroutine/event.h b/runtime-light/coroutine/event.h index 8e428b72ce..441f6c79e6 100644 --- a/runtime-light/coroutine/event.h +++ b/runtime-light/coroutine/event.h @@ -6,8 +6,11 @@ #include #include +#include #include +#include "common/mixin/not_copyable.h" +#include "runtime-common/core/allocator/script-allocator-managed.h" #include "runtime-light/coroutine/async-stack.h" #include "runtime-light/coroutine/coroutine-state.h" #include "runtime-light/stdlib/diagnostics/logs.h" @@ -15,10 +18,14 @@ namespace kphp::coro { class event { - // 1) nullptr => not set - // 2) awaiter* => linked list of awaiters waiting for the event to trigger - // 3) this => The event is triggered and all awaiters are resumed - void* m_state{}; + struct state_handle : kphp::memory::script_allocator_managed, vk::not_copyable { + // 1) nullptr => not set + // 2) awaiter* => linked list of awaiters waiting for the event to trigger + // 3) this => The event is triggered and all awaiters are resumed + void* m_state{}; + }; + + std::unique_ptr m_handle; struct awaiter { event& m_event; @@ -55,6 +62,26 @@ class event { }; public: + event() noexcept { + m_handle = std::make_unique(); + kphp::log::assertion(m_handle != nullptr); + } + + event(event&& other) noexcept + : m_handle(std::move(other.m_handle)) {} + + event& operator=(event&& other) noexcept { + if (this != std::addressof(other)) { + m_handle = std::move(other.m_handle); + } + return *this; + } + + ~event() = default; + + event(const event&) = delete; + event& operator=(const event&) = delete; + auto set() noexcept -> void; auto unset() noexcept -> void; @@ -72,7 +99,7 @@ inline auto event::awaiter::cancel_awaiter() noexcept -> void { m_prev->m_next = m_next; } else { // we are the head of the awaiters list, so we need to update the head - m_event.m_state = m_next; + m_event.m_handle->m_state = m_next; } m_next = nullptr; m_prev = nullptr; @@ -90,7 +117,7 @@ auto event::awaiter::await_suspend(std::coroutine_handle aw m_suspended = true; m_awaiting_coroutine = awaiting_coroutine; - m_next = static_cast(m_event.m_state); + m_next = static_cast(m_event.m_handle->m_state); // ensure that the event isn't triggered kphp::log::assertion(reinterpret_cast(m_next) != std::addressof(m_event)); @@ -98,7 +125,7 @@ auto event::awaiter::await_suspend(std::coroutine_handle aw if (m_next != nullptr) { m_next->m_prev = this; } - m_event.m_state = this; + m_event.m_handle->m_state = this; } inline auto event::awaiter::await_resume() noexcept -> void { @@ -110,7 +137,7 @@ inline auto event::awaiter::await_resume() noexcept -> void { } inline auto event::set() noexcept -> void { - void* prev_value{std::exchange(m_state, this)}; + void* prev_value{std::exchange(m_handle->m_state, this)}; if (prev_value == this || prev_value == nullptr) [[unlikely]] { return; } @@ -123,13 +150,13 @@ inline auto event::set() noexcept -> void { } inline auto event::unset() noexcept -> void { - if (m_state == this) { - m_state = nullptr; + if (m_handle->m_state == this) { + m_handle->m_state = nullptr; } } inline auto event::is_set() const noexcept -> bool { - return m_state == this; + return m_handle->m_state == this; } inline auto event::operator co_await() noexcept { From 2024b0c720240977820da90eac0b2e37c95a9110 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 20 Mar 2026 15:08:36 +0300 Subject: [PATCH 06/20] [k2] fix how kphp::coro::when_any works with void --- runtime-light/coroutine/detail/when-any.h | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/runtime-light/coroutine/detail/when-any.h b/runtime-light/coroutine/detail/when-any.h index f124f5928e..2d7df28e37 100644 --- a/runtime-light/coroutine/detail/when-any.h +++ b/runtime-light/coroutine/detail/when-any.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include @@ -249,12 +250,17 @@ class when_any_task { }; struct when_any_task_promise_void : public when_any_task_promise_common { - auto yield_value() const noexcept { + private: + std::optional m_result; + + public: + auto yield_value(kphp::coro::void_value&& return_value) noexcept { + m_result.emplace(return_value); return when_any_task_promise_common::final_suspend(); } - constexpr auto result() const noexcept -> std::optional { - return std::optional{std::in_place}; + auto result() noexcept -> std::optional { + return m_result; } constexpr auto return_void() const noexcept -> void {} @@ -296,6 +302,7 @@ template auto make_when_any_task(awaitable_type awaitable) noexcept -> when_any_task::awaiter_return_type> { if constexpr (std::is_void_v::awaiter_return_type>) { co_await std::move(awaitable); + co_yield kphp::coro::void_value{}; } else { co_yield co_await std::move(awaitable); } From 482810f5cb3232b9beeae1778a8b16537a1fe829 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 20 Mar 2026 20:51:53 +0300 Subject: [PATCH 07/20] [k2] make kphp::coro::event safe to move --- runtime-light/coroutine/event.h | 66 +++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/runtime-light/coroutine/event.h b/runtime-light/coroutine/event.h index 441f6c79e6..39fd6d6974 100644 --- a/runtime-light/coroutine/event.h +++ b/runtime-light/coroutine/event.h @@ -18,18 +18,22 @@ namespace kphp::coro { class event { - struct state_handle : kphp::memory::script_allocator_managed, vk::not_copyable { + struct event_controller : kphp::memory::script_allocator_managed, vk::not_copyable { // 1) nullptr => not set // 2) awaiter* => linked list of awaiters waiting for the event to trigger // 3) this => The event is triggered and all awaiters are resumed void* m_state{}; + + auto set() noexcept -> void; + auto unset() noexcept -> void; + auto is_set() const noexcept -> bool; }; - std::unique_ptr m_handle; + std::unique_ptr m_controller; struct awaiter { - event& m_event; bool m_suspended{}; + event_controller& m_controller; std::coroutine_handle<> m_awaiting_coroutine; kphp::coro::async_stack_root& m_async_stack_root; kphp::coro::async_stack_frame* m_caller_async_stack_frame{}; @@ -37,8 +41,8 @@ class event { awaiter* m_next{}; awaiter* m_prev{}; - explicit awaiter(event& event) noexcept - : m_event(event), + explicit awaiter(event_controller& event_controller) noexcept + : m_controller(event_controller), m_async_stack_root(CoroutineInstanceState::get().coroutine_stack_root) {} awaiter(const awaiter&) = delete; @@ -62,17 +66,17 @@ class event { }; public: - event() noexcept { - m_handle = std::make_unique(); - kphp::log::assertion(m_handle != nullptr); + event() noexcept + : m_controller(std::make_unique()) { + kphp::log::assertion(m_controller != nullptr); } event(event&& other) noexcept - : m_handle(std::move(other.m_handle)) {} + : m_controller(std::move(other.m_controller)) {} event& operator=(event&& other) noexcept { if (this != std::addressof(other)) { - m_handle = std::move(other.m_handle); + m_controller = std::move(other.m_controller); } return *this; } @@ -83,9 +87,7 @@ class event { event& operator=(const event&) = delete; auto set() noexcept -> void; - auto unset() noexcept -> void; - auto is_set() const noexcept -> bool; auto operator co_await() noexcept; @@ -99,14 +101,14 @@ inline auto event::awaiter::cancel_awaiter() noexcept -> void { m_prev->m_next = m_next; } else { // we are the head of the awaiters list, so we need to update the head - m_event.m_handle->m_state = m_next; + m_controller.m_state = m_next; } m_next = nullptr; m_prev = nullptr; } inline auto event::awaiter::await_ready() const noexcept -> bool { - return m_event.is_set(); + return m_controller.is_set(); } template caller_promise_type> @@ -117,15 +119,15 @@ auto event::awaiter::await_suspend(std::coroutine_handle aw m_suspended = true; m_awaiting_coroutine = awaiting_coroutine; - m_next = static_cast(m_event.m_handle->m_state); + m_next = static_cast(m_controller.m_state); // ensure that the event isn't triggered - kphp::log::assertion(reinterpret_cast(m_next) != std::addressof(m_event)); + kphp::log::assertion(reinterpret_cast(m_next) != std::addressof(m_controller)); if (m_next != nullptr) { m_next->m_prev = this; } - m_event.m_handle->m_state = this; + m_controller.m_state = this; } inline auto event::awaiter::await_resume() noexcept -> void { @@ -136,8 +138,8 @@ inline auto event::awaiter::await_resume() noexcept -> void { } } -inline auto event::set() noexcept -> void { - void* prev_value{std::exchange(m_handle->m_state, this)}; +inline auto event::event_controller::set() noexcept -> void { + void* prev_value{std::exchange(m_state, this)}; if (prev_value == this || prev_value == nullptr) [[unlikely]] { return; } @@ -149,18 +151,34 @@ inline auto event::set() noexcept -> void { } } -inline auto event::unset() noexcept -> void { - if (m_handle->m_state == this) { - m_handle->m_state = nullptr; +inline auto event::event_controller::unset() noexcept -> void { + if (m_state == this) { + m_state = nullptr; } } +inline auto event::event_controller::is_set() const noexcept -> bool { + return m_state == this; +} + +inline auto event::set() noexcept -> void { + kphp::log::assertion(m_controller != nullptr); + m_controller->set(); +} + +inline auto event::unset() noexcept -> void { + kphp::log::assertion(m_controller != nullptr); + m_controller->unset(); +} + inline auto event::is_set() const noexcept -> bool { - return m_handle->m_state == this; + kphp::log::assertion(m_controller != nullptr); + return m_controller->is_set(); } inline auto event::operator co_await() noexcept { - return event::awaiter{*this}; + kphp::log::assertion(m_controller != nullptr); + return event::awaiter{*this->m_controller}; } } // namespace kphp::coro From 210fe27beafbf8c777dce394a629a5593e4e3591 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 20 Mar 2026 20:52:11 +0300 Subject: [PATCH 08/20] [k2] add more errno codes to k2-api --- runtime-light/k2-platform/k2-api.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime-light/k2-platform/k2-api.h b/runtime-light/k2-platform/k2-api.h index 656eb95bae..1bbf21333b 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -36,6 +36,7 @@ inline constexpr size_t DEFAULT_MEMORY_ALIGN = 16; } // namespace k2_impl_ inline constexpr int32_t errno_ok = 0; +inline constexpr int32_t errno_ebusy = EBUSY; inline constexpr int32_t errno_enodev = ENODEV; inline constexpr int32_t errno_einval = EINVAL; inline constexpr int32_t errno_enodata = ENODATA; @@ -47,6 +48,7 @@ inline constexpr int32_t errno_ecanceled = ECANCELED; inline constexpr int32_t errno_erange = ERANGE; inline constexpr int32_t errno_enoent = ENOENT; inline constexpr int32_t errno_eopnotsupp = EOPNOTSUPP; +inline constexpr int32_t errno_ealready = EALREADY; inline constexpr int32_t errno_einprogress = EINPROGRESS; using descriptor = uint64_t; From 90125cc78257d0f0210c0edf3478060407e1815a Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 20 Mar 2026 20:53:14 +0300 Subject: [PATCH 09/20] [k2] implement ignore_user_abort for HTTP server mode --- .../kphp-light/stdlib/server-functions.txt | 5 +- runtime-light/server/http/http-server-state.h | 6 +- runtime-light/server/http/init-functions.cpp | 43 +++++- runtime-light/state/instance-state.h | 2 + .../stdlib/server/common-functions.h | 35 +++++ runtime-light/streams/watcher.h | 122 ++++++++++++++++++ 6 files changed, 203 insertions(+), 10 deletions(-) create mode 100644 runtime-light/stdlib/server/common-functions.h create mode 100644 runtime-light/streams/watcher.h diff --git a/builtin-functions/kphp-light/stdlib/server-functions.txt b/builtin-functions/kphp-light/stdlib/server-functions.txt index 0e46d2a141..91996c6b1e 100644 --- a/builtin-functions/kphp-light/stdlib/server-functions.txt +++ b/builtin-functions/kphp-light/stdlib/server-functions.txt @@ -10,6 +10,9 @@ function kphp_get_runtime_config() ::: mixed; function register_shutdown_function (callable():void $callback) ::: void; +/** @kphp-extern-func-info interruptible */ +function ignore_user_abort ($enable ::: ?bool = null) ::: int; + // === URL ======================================================================================== define('PHP_URL_SCHEME', 0); @@ -125,8 +128,6 @@ define('LC_MESSAGES', 5); function debug_backtrace() ::: string[][]; -/** @kphp-extern-func-info stub generation-required */ -function ignore_user_abort ($enable ::: ?bool = null) ::: int; /** @kphp-extern-func-info stub generation-required */ function flush() ::: void; diff --git a/runtime-light/server/http/http-server-state.h b/runtime-light/server/http/http-server-state.h index 814b5614b8..f6f6b17da5 100644 --- a/runtime-light/server/http/http-server-state.h +++ b/runtime-light/server/http/http-server-state.h @@ -9,6 +9,7 @@ #include #include #include +#include #include "common/mixin/not_copyable.h" #include "runtime-common/core/allocator/script-allocator.h" @@ -16,6 +17,7 @@ #include "runtime-common/core/std/containers.h" #include "runtime-light/coroutine/task.h" #include "runtime-light/streams/stream.h" +#include "runtime-light/streams/watcher.h" namespace kphp::http { @@ -57,7 +59,9 @@ struct HttpServerInstanceState final : private vk::not_copyable { static constexpr auto ENCODING_GZIP = static_cast(1U << 0U); static constexpr auto ENCODING_DEFLATE = static_cast(1U << 1U); - std::optional request_stream; + std::optional opt_connection; + std::optional opt_user_abort_watcher; + uint32_t ignore_user_abort_level{}; std::optional opt_raw_post_data; diff --git a/runtime-light/server/http/init-functions.cpp b/runtime-light/server/http/init-functions.cpp index 5ce9a19343..5ae39571bc 100644 --- a/runtime-light/server/http/init-functions.cpp +++ b/runtime-light/server/http/init-functions.cpp @@ -24,6 +24,7 @@ #include "runtime-common/core/std/containers.h" #include "runtime-common/stdlib/server/url-functions.h" #include "runtime-light/core/globals/php-script-globals.h" +#include "runtime-light/coroutine/task.h" #include "runtime-light/k2-platform/k2-api.h" #include "runtime-light/server/http/http-server-state.h" #include "runtime-light/server/http/multipart/multipart.h" @@ -32,8 +33,10 @@ #include "runtime-light/stdlib/diagnostics/logs.h" #include "runtime-light/stdlib/output/output-state.h" #include "runtime-light/stdlib/server/http-functions.h" +#include "runtime-light/stdlib/system/system-functions.h" #include "runtime-light/stdlib/zlib/zlib-functions.h" #include "runtime-light/streams/stream.h" +#include "runtime-light/streams/watcher.h" #include "runtime-light/tl/tl-core.h" #include "runtime-light/tl/tl-functions.h" #include "runtime-light/tl/tl-types.h" @@ -241,7 +244,29 @@ void init_server(kphp::component::stream&& request_stream, kphp::stl::vectordescriptor())}; + if (!expected_user_abort_watcher) [[unlikely]] { + kphp::log::error("failed to create user abort watcher: error code -> {}", expected_user_abort_watcher.error()); + } + + static constexpr auto user_abort_watcher{[] noexcept -> kphp::coro::task<> { + auto& http_server_instance_st{HttpServerInstanceState::get()}; + http_server_instance_st.opt_connection.reset(); + http_server_instance_st.opt_user_abort_watcher.reset(); + if (http_server_instance_st.ignore_user_abort_level == 0) { + co_await kphp::system::exit(1); + } + }}; + + http_server_instance_st.opt_user_abort_watcher.emplace(*std::move(expected_user_abort_watcher)); + if (auto expected{http_server_instance_st.opt_user_abort_watcher->watch(user_abort_watcher)}; !expected) [[unlikely]] { + kphp::log::error("failed to setup user abort watcher: error code -> {}", expected.error()); + } + } // determine HTTP method if (invoke_http.method.value == GET_METHOD) { @@ -379,6 +404,7 @@ void init_server(kphp::component::stream&& request_stream, kphp::stl::vector finalize_server() noexcept { auto& http_server_instance_st{HttpServerInstanceState::get()}; + http_server_instance_st.opt_user_abort_watcher.reset(); string response_body{}; tl::HttpResponse http_response{}; @@ -419,15 +445,18 @@ kphp::coro::task<> finalize_server() noexcept { [[fallthrough]]; } case kphp::http::response_state::sending_body: { + if (!http_server_instance_st.opt_connection) [[unlikely]] { + if (http_server_instance_st.ignore_user_abort_level > 0) { + co_return kphp::log::warning("HTTP connection closed"); + } + kphp::log::error("HTTP connection closed"); + } + tl::storer tls{http_response.footprint()}; http_response.store(tls); - if (!http_server_instance_st.request_stream.has_value()) [[unlikely]] { - kphp::log::error("can't send HTTP response since there is no available stream"); - } - auto& request_stream{*http_server_instance_st.request_stream}; - if (auto expected{co_await kphp::component::send_response(request_stream, tls.view())}; !expected) [[unlikely]] { - kphp::log::error("can't write HTTP response: stream -> {}, error code -> {}", request_stream.descriptor(), expected.error()); + if (auto expected{co_await kphp::component::send_response(*http_server_instance_st.opt_connection, tls.view())}; !expected) [[unlikely]] { + kphp::log::error("can't write HTTP response: error code -> {}", expected.error()); } http_server_instance_st.response_state = kphp::http::response_state::completed; [[fallthrough]]; diff --git a/runtime-light/state/instance-state.h b/runtime-light/state/instance-state.h index 1c91367c30..dc09063f32 100644 --- a/runtime-light/state/instance-state.h +++ b/runtime-light/state/instance-state.h @@ -6,6 +6,7 @@ #include #include +#include #include "common/mixin/not_copyable.h" #include "runtime-common/core/runtime-core.h" @@ -41,6 +42,7 @@ #include "runtime-light/stdlib/system/system-state.h" #include "runtime-light/stdlib/time/time-state.h" #include "runtime-light/stdlib/web-transfer-lib/web-state.h" +#include "runtime-light/streams/watcher.h" /** * Supported kinds of KPHP images: diff --git a/runtime-light/stdlib/server/common-functions.h b/runtime-light/stdlib/server/common-functions.h new file mode 100644 index 0000000000..a587b2fc17 --- /dev/null +++ b/runtime-light/stdlib/server/common-functions.h @@ -0,0 +1,35 @@ +// 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 "runtime-common/core/runtime-core.h" +#include "runtime-light/coroutine/task.h" +#include "runtime-light/server/http/http-server-state.h" +#include "runtime-light/state/instance-state.h" +#include "runtime-light/stdlib/diagnostics/logs.h" +#include "runtime-light/stdlib/system/system-functions.h" + +inline auto f$ignore_user_abort(Optional enable) noexcept -> kphp::coro::task { + if (InstanceState::get().instance_kind() != instance_kind::http_server) { + kphp::log::warning("called stub f$ignore_user_abort"); + co_return 0; + } + + auto& http_server_instance_st{HttpServerInstanceState::get()}; + if (enable.is_null()) { + co_return http_server_instance_st.ignore_user_abort_level; + } else if (enable.val()) { + co_return http_server_instance_st.ignore_user_abort_level++; + } else { + const auto prev{http_server_instance_st.ignore_user_abort_level > 0 ? http_server_instance_st.ignore_user_abort_level-- : 0}; + + if (http_server_instance_st.ignore_user_abort_level == 0 && !http_server_instance_st.opt_connection) { + co_await kphp::system::exit(1); + } + co_return prev; + } +} diff --git a/runtime-light/streams/watcher.h b/runtime-light/streams/watcher.h new file mode 100644 index 0000000000..e5d7278d69 --- /dev/null +++ b/runtime-light/streams/watcher.h @@ -0,0 +1,122 @@ +// 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 "runtime-light/coroutine/event.h" +#include "runtime-light/coroutine/io-scheduler.h" +#include "runtime-light/coroutine/task.h" +#include "runtime-light/coroutine/type-traits.h" +#include "runtime-light/coroutine/when-any.h" +#include "runtime-light/k2-platform/k2-api.h" + +namespace kphp::component { + +class watcher { + k2::descriptor m_descriptor{k2::INVALID_PLATFORM_DESCRIPTOR}; + // TODO it should watch for specific poll_op + std::optional m_unwatch_event; + + explicit watcher(k2::descriptor descriptor) noexcept + : m_descriptor(descriptor) {} + +public: + watcher(watcher&& other) noexcept + : m_descriptor(std::exchange(other.m_descriptor, k2::INVALID_PLATFORM_DESCRIPTOR)), + m_unwatch_event(std::exchange(other.m_unwatch_event, {})) {} + + watcher& operator=(watcher&& other) noexcept { + if (this != std::addressof(other)) { + m_descriptor = std::exchange(other.m_descriptor, k2::INVALID_PLATFORM_DESCRIPTOR); + m_unwatch_event = std::exchange(other.m_unwatch_event, {}); + } + return *this; + } + + watcher() = delete; + watcher(const watcher&) = delete; + watcher& operator=(const watcher&) = delete; + + ~watcher() { + unwatch(); + } + + static auto create(k2::descriptor descriptor) noexcept -> std::expected; + + template + auto watch(on_event_handler_type&& f) noexcept -> std::expected; + + auto unwatch() noexcept -> void; +}; + +inline auto watcher::create(k2::descriptor descriptor) noexcept -> std::expected { + if (descriptor == k2::INVALID_PLATFORM_DESCRIPTOR) { + return std::unexpected{k2::errno_einval}; + } + return watcher{descriptor}; +} + +template +auto watcher::watch(on_event_handler_type&& f) noexcept -> std::expected { + if (m_unwatch_event) { // already watching + return std::unexpected{k2::errno_ealready}; + } + + k2::StreamStatus stream_status{}; + k2::stream_status(m_descriptor, std::addressof(stream_status)); + if (stream_status.libc_errno != k2::errno_ok) [[unlikely]] { + return std::unexpected{stream_status.libc_errno}; + } + + static constexpr auto watcher{[](k2::descriptor descriptor, kphp::coro::event& unwatch_event, on_event_handler_type f) noexcept -> kphp::coro::task<> { + static constexpr auto unwatch_awaiter{[](kphp::coro::event& unwatch_event) noexcept -> kphp::coro::task<> { co_await unwatch_event; }}; + static constexpr auto update_awaiter{[](k2::descriptor descriptor) noexcept -> kphp::coro::task { + k2::StreamStatus stream_status{}; + auto& io_scheduler{kphp::coro::io_scheduler::get()}; + for (;;) { + k2::stream_status(descriptor, std::addressof(stream_status)); + if (stream_status.write_status == k2::IOStatus::IOClosed) { + co_return std::monostate{}; + } + + using namespace std::chrono_literals; + co_await io_scheduler.schedule(150ms); + } + }}; + + if (std::holds_alternative(co_await kphp::coro::when_any(update_awaiter(descriptor), unwatch_awaiter(unwatch_event)))) { + if constexpr (kphp::coro::is_async_function_v) { + co_await std::invoke(std::move(f)); + } else { + std::invoke(std::move(f)); + } + } + }}; + + if (!kphp::coro::io_scheduler::get().start(watcher(m_descriptor, m_unwatch_event.emplace(), std::forward(f)))) { + m_unwatch_event.reset(); + return std::unexpected{k2::errno_ebusy}; + } + return {}; +} + +inline auto watcher::unwatch() noexcept -> void { + if (!m_unwatch_event) { + return; + } + m_unwatch_event->set(); + m_unwatch_event.reset(); +} + +} // namespace kphp::component From 2a50eb891fd2825631c723fd18735c0244e700dd Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Sat, 21 Mar 2026 15:41:24 +0300 Subject: [PATCH 10/20] [k2] add kphp::component::connection --- runtime-light/streams/connection.h | 101 +++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 runtime-light/streams/connection.h diff --git a/runtime-light/streams/connection.h b/runtime-light/streams/connection.h new file mode 100644 index 0000000000..a3a5eff107 --- /dev/null +++ b/runtime-light/streams/connection.h @@ -0,0 +1,101 @@ +// 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 "runtime-light/k2-platform/k2-api.h" +#include "runtime-light/streams/stream.h" +#include "runtime-light/streams/watcher.h" + +namespace kphp::component { + +class connection { + kphp::component::stream m_stream; + kphp::component::watcher m_watcher; + uint32_t m_ignore_abort_level{}; + + connection(kphp::component::stream&& stream, kphp::component::watcher&& watcher) noexcept + : m_stream(std::move(stream)), + m_watcher(std::move(watcher)) {} + +public: + connection() = delete; + connection(const connection&) = delete; + auto operator=(const connection&) = delete; + + connection(connection&&) noexcept = default; + auto operator=(connection&&) noexcept -> connection& = default; + ~connection() = default; + + static auto from_stream(kphp::component::stream&& stream) noexcept -> std::expected; + + auto get_stream() noexcept -> kphp::component::stream&; + + auto increase_ignore_abort_level() noexcept -> void; + auto decrease_ignore_abort_level() noexcept -> void; + auto get_ignore_abort_level() const noexcept -> uint32_t; + + auto is_aborted() const noexcept -> bool; + + template + auto register_abort_handler(on_abort_handler_type&& h) noexcept -> std::expected; + auto unregister_abort_handler() noexcept -> void; +}; + +// ================================================================================================ + +inline auto connection::from_stream(kphp::component::stream&& stream) noexcept -> std::expected { + k2::StreamStatus stream_status{}; + k2::stream_status(stream.descriptor(), std::addressof(stream_status)); + if (stream_status.libc_errno != k2::errno_ok || stream_status.write_status == k2::IOStatus::IOClosed) [[unlikely]] { + return std::unexpected{stream_status.libc_errno != k2::errno_ok ? stream_status.libc_errno : k2::errno_eshutdown}; + } + + auto expected_watcher{kphp::component::watcher::create(stream.descriptor())}; + if (!expected_watcher) [[unlikely]] { + return std::unexpected{expected_watcher.error()}; + } + + auto watcher{*std::move(expected_watcher)}; + // watcher.watch(std::forward(h)); + return connection{std::move(stream), std::move(watcher)}; +} + +inline auto connection::get_stream() noexcept -> kphp::component::stream& { + return m_stream; +} + +inline auto connection::increase_ignore_abort_level() noexcept -> void { + ++m_ignore_abort_level; +} + +inline auto connection::decrease_ignore_abort_level() noexcept -> void { + if (m_ignore_abort_level > 0) { + --m_ignore_abort_level; + } +} + +inline auto connection::get_ignore_abort_level() const noexcept -> uint32_t { + return m_ignore_abort_level; +} + +inline auto connection::is_aborted() const noexcept -> bool { + return m_stream.status().write_status == k2::IOStatus::IOClosed; +} + +template +auto connection::register_abort_handler(on_abort_handler_type&& h) noexcept -> std::expected { + return m_watcher.watch(std::forward(h)); +} + +inline auto connection::unregister_abort_handler() noexcept -> void { + m_watcher.unwatch(); +} +} // namespace kphp::component From 6777efc71d89d2c8572fdc8ef71e45813b77602b Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Sat, 21 Mar 2026 15:41:48 +0300 Subject: [PATCH 11/20] [k2] make kphp::component::watcher safer --- runtime-light/streams/watcher.h | 59 ++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/runtime-light/streams/watcher.h b/runtime-light/streams/watcher.h index e5d7278d69..800e5259ad 100644 --- a/runtime-light/streams/watcher.h +++ b/runtime-light/streams/watcher.h @@ -14,32 +14,50 @@ #include #include +#include "common/containers/final_action.h" +#include "runtime-common/core/class-instance/refcountable-php-classes.h" +#include "runtime-common/core/runtime-core.h" #include "runtime-light/coroutine/event.h" #include "runtime-light/coroutine/io-scheduler.h" #include "runtime-light/coroutine/task.h" #include "runtime-light/coroutine/type-traits.h" #include "runtime-light/coroutine/when-any.h" #include "runtime-light/k2-platform/k2-api.h" +#include "runtime-light/stdlib/diagnostics/logs.h" namespace kphp::component { class watcher { + struct shared_state : refcountable_php_classes { + std::optional m_unwatch_event; + + shared_state() noexcept = default; + shared_state(shared_state&&) noexcept = default; + shared_state& operator=(shared_state&&) noexcept = default; + ~shared_state() = default; + + shared_state(const shared_state&) = delete; + shared_state operator=(const shared_state&) = delete; + }; + k2::descriptor m_descriptor{k2::INVALID_PLATFORM_DESCRIPTOR}; - // TODO it should watch for specific poll_op - std::optional m_unwatch_event; + class_instance m_shared_state; explicit watcher(k2::descriptor descriptor) noexcept - : m_descriptor(descriptor) {} + : m_descriptor(descriptor) { + kphp::log::assertion(!m_shared_state.alloc().is_null()); + } public: watcher(watcher&& other) noexcept : m_descriptor(std::exchange(other.m_descriptor, k2::INVALID_PLATFORM_DESCRIPTOR)), - m_unwatch_event(std::exchange(other.m_unwatch_event, {})) {} + m_shared_state(std::move(other.m_shared_state)) {} watcher& operator=(watcher&& other) noexcept { if (this != std::addressof(other)) { + unwatch(); m_descriptor = std::exchange(other.m_descriptor, k2::INVALID_PLATFORM_DESCRIPTOR); - m_unwatch_event = std::exchange(other.m_unwatch_event, {}); + m_shared_state = std::move(other.m_shared_state); } return *this; } @@ -69,7 +87,10 @@ inline auto watcher::create(k2::descriptor descriptor) noexcept -> std::expected template auto watcher::watch(on_event_handler_type&& f) noexcept -> std::expected { - if (m_unwatch_event) { // already watching + if (m_shared_state.is_null()) [[unlikely]] { + return std::unexpected{k2::errno_eshutdown}; + } + if (m_shared_state.get()->m_unwatch_event.has_value()) { // already watching return std::unexpected{k2::errno_ealready}; } @@ -79,12 +100,16 @@ auto watcher::watch(on_event_handler_type&& f) noexcept -> std::expected kphp::coro::task<> { - static constexpr auto unwatch_awaiter{[](kphp::coro::event& unwatch_event) noexcept -> kphp::coro::task<> { co_await unwatch_event; }}; - static constexpr auto update_awaiter{[](k2::descriptor descriptor) noexcept -> kphp::coro::task { + static constexpr auto watcher{[](k2::descriptor descriptor, class_instance state, on_event_handler_type f) noexcept -> kphp::coro::task<> { + kphp::log::assertion(!state.is_null() && state.get()->m_unwatch_event.has_value()); + const auto finalizer{vk::finally([state] noexcept { state.get()->m_unwatch_event.reset(); })}; + + static constexpr auto unwatch_awaiter{[](class_instance state) noexcept -> kphp::coro::task<> { co_await *state.get()->m_unwatch_event; }}; + + static constexpr auto descriptor_awaiter{[](k2::descriptor descriptor) noexcept -> kphp::coro::task { k2::StreamStatus stream_status{}; auto& io_scheduler{kphp::coro::io_scheduler::get()}; - for (;;) { + for (;;) { // TODO it should watch for specific poll_op k2::stream_status(descriptor, std::addressof(stream_status)); if (stream_status.write_status == k2::IOStatus::IOClosed) { co_return std::monostate{}; @@ -95,7 +120,8 @@ auto watcher::watch(on_event_handler_type&& f) noexcept -> std::expected(co_await kphp::coro::when_any(update_awaiter(descriptor), unwatch_awaiter(unwatch_event)))) { + const auto v{co_await kphp::coro::when_any(descriptor_awaiter(descriptor), unwatch_awaiter(std::move(state)))}; + if (std::holds_alternative(v)) { if constexpr (kphp::coro::is_async_function_v) { co_await std::invoke(std::move(f)); } else { @@ -104,19 +130,20 @@ auto watcher::watch(on_event_handler_type&& f) noexcept -> std::expected(f)))) { - m_unwatch_event.reset(); + m_shared_state.get()->m_unwatch_event.emplace(); + if (!kphp::coro::io_scheduler::get().spawn(watcher(m_descriptor, m_shared_state, std::forward(f)))) { + m_shared_state.get()->m_unwatch_event.reset(); return std::unexpected{k2::errno_ebusy}; } return {}; } inline auto watcher::unwatch() noexcept -> void { - if (!m_unwatch_event) { + if (m_shared_state.is_null() || !m_shared_state.get()->m_unwatch_event.has_value()) { return; } - m_unwatch_event->set(); - m_unwatch_event.reset(); + m_shared_state.get()->m_unwatch_event->set(); + m_shared_state.get()->m_unwatch_event.reset(); } } // namespace kphp::component From 40454ffa8c4c4502295b2571ffe3d7c2a38c3906 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Sat, 21 Mar 2026 15:42:15 +0300 Subject: [PATCH 12/20] [k2] add kphp::component::stream::status method --- runtime-light/streams/stream.h | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/runtime-light/streams/stream.h b/runtime-light/streams/stream.h index 4a556688b5..43b81b9e41 100644 --- a/runtime-light/streams/stream.h +++ b/runtime-light/streams/stream.h @@ -59,6 +59,7 @@ class stream { auto descriptor() const noexcept -> k2::descriptor; auto reset(k2::descriptor descriptor) noexcept -> void; + auto status() const noexcept -> k2::StreamStatus; auto read(std::span buf) const noexcept -> kphp::coro::task>; template> F> @@ -107,6 +108,10 @@ auto stream::accept(duration_type timeout) noexcept -> kphp::coro::task k2::descriptor { + return m_descriptor; +} + inline auto stream::reset(k2::descriptor descriptor) noexcept -> void { if (descriptor == m_descriptor) [[unlikely]] { return; @@ -114,8 +119,10 @@ inline auto stream::reset(k2::descriptor descriptor) noexcept -> void { k2::free_descriptor(std::exchange(m_descriptor, descriptor)); } -inline auto stream::descriptor() const noexcept -> k2::descriptor { - return m_descriptor; +inline auto stream::status() const noexcept -> k2::StreamStatus { + k2::StreamStatus stream_status{}; + k2::stream_status(descriptor(), std::addressof(stream_status)); + return stream_status; } inline auto stream::read(std::span buf) const noexcept -> kphp::coro::task> { From ab391a6f12b30e0fe561de821486c14a40fd4e3b Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Sat, 21 Mar 2026 15:43:26 +0300 Subject: [PATCH 13/20] [k2] reimplement ignore_user_abort with kphp::component::connection --- runtime-light/server/http/http-server-state.h | 7 +--- runtime-light/server/http/init-functions.cpp | 38 ++++++++----------- .../stdlib/server/common-functions.h | 11 ++++-- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/runtime-light/server/http/http-server-state.h b/runtime-light/server/http/http-server-state.h index f6f6b17da5..13d0d1fa94 100644 --- a/runtime-light/server/http/http-server-state.h +++ b/runtime-light/server/http/http-server-state.h @@ -16,8 +16,7 @@ #include "runtime-common/core/runtime-core.h" #include "runtime-common/core/std/containers.h" #include "runtime-light/coroutine/task.h" -#include "runtime-light/streams/stream.h" -#include "runtime-light/streams/watcher.h" +#include "runtime-light/streams/connection.h" namespace kphp::http { @@ -59,9 +58,7 @@ struct HttpServerInstanceState final : private vk::not_copyable { static constexpr auto ENCODING_GZIP = static_cast(1U << 0U); static constexpr auto ENCODING_DEFLATE = static_cast(1U << 1U); - std::optional opt_connection; - std::optional opt_user_abort_watcher; - uint32_t ignore_user_abort_level{}; + std::optional connection; std::optional opt_raw_post_data; diff --git a/runtime-light/server/http/init-functions.cpp b/runtime-light/server/http/init-functions.cpp index 5ae39571bc..c7056124cf 100644 --- a/runtime-light/server/http/init-functions.cpp +++ b/runtime-light/server/http/init-functions.cpp @@ -35,8 +35,8 @@ #include "runtime-light/stdlib/server/http-functions.h" #include "runtime-light/stdlib/system/system-functions.h" #include "runtime-light/stdlib/zlib/zlib-functions.h" +#include "runtime-light/streams/connection.h" #include "runtime-light/streams/stream.h" -#include "runtime-light/streams/watcher.h" #include "runtime-light/tl/tl-core.h" #include "runtime-light/tl/tl-functions.h" #include "runtime-light/tl/tl-types.h" @@ -246,25 +246,22 @@ void init_server(kphp::component::stream&& request_stream, kphp::stl::vectordescriptor())}; - if (!expected_user_abort_watcher) [[unlikely]] { - kphp::log::error("failed to create user abort watcher: error code -> {}", expected_user_abort_watcher.error()); + auto expected_connection{kphp::component::connection::from_stream(std::move(request_stream))}; + if (!expected_connection) [[unlikely]] { + kphp::log::error("failed to create HTTP connection: error code -> {}", expected_connection.error()); } - static constexpr auto user_abort_watcher{[] noexcept -> kphp::coro::task<> { + static constexpr auto user_abort_handler{[] noexcept -> kphp::coro::task<> { auto& http_server_instance_st{HttpServerInstanceState::get()}; - http_server_instance_st.opt_connection.reset(); - http_server_instance_st.opt_user_abort_watcher.reset(); - if (http_server_instance_st.ignore_user_abort_level == 0) { + kphp::log::assertion(http_server_instance_st.connection.has_value()); + if (http_server_instance_st.connection->get_ignore_abort_level() == 0) { co_await kphp::system::exit(1); } }}; - http_server_instance_st.opt_user_abort_watcher.emplace(*std::move(expected_user_abort_watcher)); - if (auto expected{http_server_instance_st.opt_user_abort_watcher->watch(user_abort_watcher)}; !expected) [[unlikely]] { - kphp::log::error("failed to setup user abort watcher: error code -> {}", expected.error()); + http_server_instance_st.connection = *std::move(expected_connection); + if (auto expected{http_server_instance_st.connection->register_abort_handler(user_abort_handler)}; !expected) [[unlikely]] { + kphp::log::error("failed to register user abort handler: error code -> {}", expected.error()); } } @@ -404,7 +401,11 @@ void init_server(kphp::component::stream&& request_stream, kphp::stl::vector finalize_server() noexcept { auto& http_server_instance_st{HttpServerInstanceState::get()}; - http_server_instance_st.opt_user_abort_watcher.reset(); + kphp::log::assertion(http_server_instance_st.connection.has_value()); + http_server_instance_st.connection->unregister_abort_handler(); + if (http_server_instance_st.connection->is_aborted()) { + co_return kphp::log::info("HTTP connection closed"); + } string response_body{}; tl::HttpResponse http_response{}; @@ -445,17 +446,10 @@ kphp::coro::task<> finalize_server() noexcept { [[fallthrough]]; } case kphp::http::response_state::sending_body: { - if (!http_server_instance_st.opt_connection) [[unlikely]] { - if (http_server_instance_st.ignore_user_abort_level > 0) { - co_return kphp::log::warning("HTTP connection closed"); - } - kphp::log::error("HTTP connection closed"); - } - tl::storer tls{http_response.footprint()}; http_response.store(tls); - if (auto expected{co_await kphp::component::send_response(*http_server_instance_st.opt_connection, tls.view())}; !expected) [[unlikely]] { + if (auto expected{co_await kphp::component::send_response(http_server_instance_st.connection->get_stream(), tls.view())}; !expected) [[unlikely]] { kphp::log::error("can't write HTTP response: error code -> {}", expected.error()); } http_server_instance_st.response_state = kphp::http::response_state::completed; diff --git a/runtime-light/stdlib/server/common-functions.h b/runtime-light/stdlib/server/common-functions.h index a587b2fc17..3bb93c76cc 100644 --- a/runtime-light/stdlib/server/common-functions.h +++ b/runtime-light/stdlib/server/common-functions.h @@ -20,14 +20,17 @@ inline auto f$ignore_user_abort(Optional enable) noexcept -> kphp::coro::t } auto& http_server_instance_st{HttpServerInstanceState::get()}; + kphp::log::assertion(http_server_instance_st.connection.has_value()); if (enable.is_null()) { - co_return http_server_instance_st.ignore_user_abort_level; + co_return http_server_instance_st.connection->get_ignore_abort_level(); } else if (enable.val()) { - co_return http_server_instance_st.ignore_user_abort_level++; + http_server_instance_st.connection->increase_ignore_abort_level(); + co_return http_server_instance_st.connection->get_ignore_abort_level(); } else { - const auto prev{http_server_instance_st.ignore_user_abort_level > 0 ? http_server_instance_st.ignore_user_abort_level-- : 0}; + const auto prev{http_server_instance_st.connection->get_ignore_abort_level()}; + http_server_instance_st.connection->decrease_ignore_abort_level(); - if (http_server_instance_st.ignore_user_abort_level == 0 && !http_server_instance_st.opt_connection) { + if (http_server_instance_st.connection->get_ignore_abort_level() == 0 && http_server_instance_st.connection->is_aborted()) { co_await kphp::system::exit(1); } co_return prev; From a23d1316b2ca7103f7eb5b32471f92da25b1cbe1 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Sat, 21 Mar 2026 16:31:53 +0300 Subject: [PATCH 14/20] fix code style --- runtime-light/streams/connection.h | 2 +- runtime-light/streams/watcher.h | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/runtime-light/streams/connection.h b/runtime-light/streams/connection.h index a3a5eff107..00962824c9 100644 --- a/runtime-light/streams/connection.h +++ b/runtime-light/streams/connection.h @@ -64,7 +64,6 @@ inline auto connection::from_stream(kphp::component::stream&& stream) noexcept - } auto watcher{*std::move(expected_watcher)}; - // watcher.watch(std::forward(h)); return connection{std::move(stream), std::move(watcher)}; } @@ -98,4 +97,5 @@ auto connection::register_abort_handler(on_abort_handler_type&& h) noexcept -> s inline auto connection::unregister_abort_handler() noexcept -> void { m_watcher.unwatch(); } + } // namespace kphp::component diff --git a/runtime-light/streams/watcher.h b/runtime-light/streams/watcher.h index 800e5259ad..c9dd64ebfb 100644 --- a/runtime-light/streams/watcher.h +++ b/runtime-light/streams/watcher.h @@ -73,11 +73,13 @@ class watcher { static auto create(k2::descriptor descriptor) noexcept -> std::expected; template - auto watch(on_event_handler_type&& f) noexcept -> std::expected; + auto watch(on_event_handler_type&& h) noexcept -> std::expected; auto unwatch() noexcept -> void; }; +// ================================================================================================ + inline auto watcher::create(k2::descriptor descriptor) noexcept -> std::expected { if (descriptor == k2::INVALID_PLATFORM_DESCRIPTOR) { return std::unexpected{k2::errno_einval}; @@ -86,7 +88,7 @@ inline auto watcher::create(k2::descriptor descriptor) noexcept -> std::expected } template -auto watcher::watch(on_event_handler_type&& f) noexcept -> std::expected { +auto watcher::watch(on_event_handler_type&& h) noexcept -> std::expected { if (m_shared_state.is_null()) [[unlikely]] { return std::unexpected{k2::errno_eshutdown}; } @@ -100,7 +102,7 @@ auto watcher::watch(on_event_handler_type&& f) noexcept -> std::expected state, on_event_handler_type f) noexcept -> kphp::coro::task<> { + static constexpr auto watcher{[](k2::descriptor descriptor, class_instance state, on_event_handler_type h) noexcept -> kphp::coro::task<> { kphp::log::assertion(!state.is_null() && state.get()->m_unwatch_event.has_value()); const auto finalizer{vk::finally([state] noexcept { state.get()->m_unwatch_event.reset(); })}; @@ -123,15 +125,15 @@ auto watcher::watch(on_event_handler_type&& f) noexcept -> std::expected(v)) { if constexpr (kphp::coro::is_async_function_v) { - co_await std::invoke(std::move(f)); + co_await std::invoke(std::move(h)); } else { - std::invoke(std::move(f)); + std::invoke(std::move(h)); } } }}; m_shared_state.get()->m_unwatch_event.emplace(); - if (!kphp::coro::io_scheduler::get().spawn(watcher(m_descriptor, m_shared_state, std::forward(f)))) { + if (!kphp::coro::io_scheduler::get().spawn(watcher(m_descriptor, m_shared_state, std::forward(h)))) { m_shared_state.get()->m_unwatch_event.reset(); return std::unexpected{k2::errno_ebusy}; } From 6cf724ebce2542f41716a267468596e5910682da Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Sat, 21 Mar 2026 18:19:53 +0300 Subject: [PATCH 15/20] add id_managed to ignore_user_abort --- runtime-light/stdlib/server/common-functions.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runtime-light/stdlib/server/common-functions.h b/runtime-light/stdlib/server/common-functions.h index 3bb93c76cc..8ff83a9c2b 100644 --- a/runtime-light/stdlib/server/common-functions.h +++ b/runtime-light/stdlib/server/common-functions.h @@ -11,6 +11,7 @@ #include "runtime-light/server/http/http-server-state.h" #include "runtime-light/state/instance-state.h" #include "runtime-light/stdlib/diagnostics/logs.h" +#include "runtime-light/stdlib/fork/fork-functions.h" #include "runtime-light/stdlib/system/system-functions.h" inline auto f$ignore_user_abort(Optional enable) noexcept -> kphp::coro::task { @@ -31,7 +32,7 @@ inline auto f$ignore_user_abort(Optional enable) noexcept -> kphp::coro::t http_server_instance_st.connection->decrease_ignore_abort_level(); if (http_server_instance_st.connection->get_ignore_abort_level() == 0 && http_server_instance_st.connection->is_aborted()) { - co_await kphp::system::exit(1); + co_await kphp::forks::id_managed(kphp::system::exit(1)); } co_return prev; } From 14881a9fb8257584737ab618f612e862ba9e9fc4 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Sat, 21 Mar 2026 22:17:26 +0300 Subject: [PATCH 16/20] get rid of watcher. inline its functionality into connection --- runtime-light/state/instance-state.h | 2 - runtime-light/streams/connection.h | 118 +++++++++++++++++---- runtime-light/streams/watcher.h | 151 --------------------------- 3 files changed, 97 insertions(+), 174 deletions(-) delete mode 100644 runtime-light/streams/watcher.h diff --git a/runtime-light/state/instance-state.h b/runtime-light/state/instance-state.h index dc09063f32..1c91367c30 100644 --- a/runtime-light/state/instance-state.h +++ b/runtime-light/state/instance-state.h @@ -6,7 +6,6 @@ #include #include -#include #include "common/mixin/not_copyable.h" #include "runtime-common/core/runtime-core.h" @@ -42,7 +41,6 @@ #include "runtime-light/stdlib/system/system-state.h" #include "runtime-light/stdlib/time/time-state.h" #include "runtime-light/stdlib/web-transfer-lib/web-state.h" -#include "runtime-light/streams/watcher.h" /** * Supported kinds of KPHP images: diff --git a/runtime-light/streams/connection.h b/runtime-light/streams/connection.h index 00962824c9..1463e9be2e 100644 --- a/runtime-light/streams/connection.h +++ b/runtime-light/streams/connection.h @@ -10,20 +10,32 @@ #include #include +#include "runtime-common/core/runtime-core.h" +#include "runtime-light/coroutine/event.h" #include "runtime-light/k2-platform/k2-api.h" +#include "runtime-light/stdlib/diagnostics/logs.h" #include "runtime-light/streams/stream.h" -#include "runtime-light/streams/watcher.h" namespace kphp::component { class connection { + struct shared_state : refcountable_php_classes { + std::optional m_unwatch_event; + + shared_state() noexcept = default; + shared_state(shared_state&&) noexcept = default; + shared_state& operator=(shared_state&&) noexcept = default; + ~shared_state() = default; + + shared_state(const shared_state&) = delete; + shared_state operator=(const shared_state&) = delete; + }; + + class_instance m_shared_state; kphp::component::stream m_stream; - kphp::component::watcher m_watcher; uint32_t m_ignore_abort_level{}; - connection(kphp::component::stream&& stream, kphp::component::watcher&& watcher) noexcept - : m_stream(std::move(stream)), - m_watcher(std::move(watcher)) {} + explicit connection(kphp::component::stream&& stream) noexcept; public: connection() = delete; @@ -31,8 +43,11 @@ class connection { auto operator=(const connection&) = delete; connection(connection&&) noexcept = default; - auto operator=(connection&&) noexcept -> connection& = default; - ~connection() = default; + auto operator=(connection&& other) noexcept -> connection&; + + ~connection() { + unregister_abort_handler(); + } static auto from_stream(kphp::component::stream&& stream) noexcept -> std::expected; @@ -41,7 +56,6 @@ class connection { auto increase_ignore_abort_level() noexcept -> void; auto decrease_ignore_abort_level() noexcept -> void; auto get_ignore_abort_level() const noexcept -> uint32_t; - auto is_aborted() const noexcept -> bool; template @@ -51,20 +65,26 @@ class connection { // ================================================================================================ -inline auto connection::from_stream(kphp::component::stream&& stream) noexcept -> std::expected { - k2::StreamStatus stream_status{}; - k2::stream_status(stream.descriptor(), std::addressof(stream_status)); - if (stream_status.libc_errno != k2::errno_ok || stream_status.write_status == k2::IOStatus::IOClosed) [[unlikely]] { - return std::unexpected{stream_status.libc_errno != k2::errno_ok ? stream_status.libc_errno : k2::errno_eshutdown}; - } +inline connection::connection(kphp::component::stream&& stream) noexcept + : m_stream(std::move(stream)) { + kphp::log::assertion(!m_shared_state.alloc().is_null()); +} - auto expected_watcher{kphp::component::watcher::create(stream.descriptor())}; - if (!expected_watcher) [[unlikely]] { - return std::unexpected{expected_watcher.error()}; +inline auto connection::operator=(connection&& other) noexcept -> connection& { + if (this != std::addressof(other)) { + unregister_abort_handler(); + m_shared_state = std::move(other.m_shared_state); + m_stream = std::move(other.m_stream); + m_ignore_abort_level = other.m_ignore_abort_level; } + return *this; +} - auto watcher{*std::move(expected_watcher)}; - return connection{std::move(stream), std::move(watcher)}; +inline auto connection::from_stream(kphp::component::stream&& stream) noexcept -> std::expected { + if (const auto status{stream.status()}; status.libc_errno != k2::errno_ok) [[unlikely]] { + return std::unexpected{status.libc_errno}; + } + return connection{std::move(stream)}; } inline auto connection::get_stream() noexcept -> kphp::component::stream& { @@ -91,11 +111,67 @@ inline auto connection::is_aborted() const noexcept -> bool { template auto connection::register_abort_handler(on_abort_handler_type&& h) noexcept -> std::expected { - return m_watcher.watch(std::forward(h)); + if (m_shared_state.is_null()) [[unlikely]] { + return std::unexpected{k2::errno_eshutdown}; + } + if (m_shared_state.get()->m_unwatch_event.has_value()) [[unlikely]] { // already registered + return std::unexpected{k2::errno_ealready}; + } + + if (const auto status{m_stream.status()}; status.libc_errno != k2::errno_ok || status.write_status == k2::IOStatus::IOClosed) [[unlikely]] { + return std::unexpected{status.libc_errno != k2::errno_ok ? status.libc_errno : k2::errno_ecanceled}; + } + + static constexpr auto watcher{[](k2::descriptor descriptor, class_instance state, on_abort_handler_type h) noexcept -> kphp::coro::task<> { + static constexpr auto unwatch_awaiter{[](class_instance state) noexcept -> kphp::coro::task<> { + kphp::log::assertion(state.get()->m_unwatch_event.has_value()); + co_await *state.get()->m_unwatch_event; + }}; + + static constexpr auto descriptor_awaiter{[](k2::descriptor descriptor) noexcept -> kphp::coro::task { + k2::StreamStatus stream_status{}; + auto& io_scheduler{kphp::coro::io_scheduler::get()}; + for (;;) { // FIXME it should actually use scheduler.poll + k2::stream_status(descriptor, std::addressof(stream_status)); + if (stream_status.write_status == k2::IOStatus::IOClosed) { + co_return std::monostate{}; + } + + using namespace std::chrono_literals; + co_await io_scheduler.schedule(150ms); + } + }}; + + kphp::log::assertion(!state.is_null()); + if (!state.get()->m_unwatch_event.has_value()) { // already unregistered + co_return; + } + + const auto finalizer{vk::finally([state] noexcept { state.get()->m_unwatch_event.reset(); })}; + const auto v{co_await kphp::coro::when_any(unwatch_awaiter(std::move(state)), descriptor_awaiter(descriptor))}; + if (std::holds_alternative(v)) { + if constexpr (kphp::coro::is_async_function_v) { + co_await std::invoke(std::move(h)); + } else { + std::invoke(std::move(h)); + } + } + }}; + + m_shared_state.get()->m_unwatch_event.emplace(); + if (!kphp::coro::io_scheduler::get().spawn(watcher(m_stream.descriptor(), m_shared_state, std::forward(h)))) [[unlikely]] { + m_shared_state.get()->m_unwatch_event.reset(); + return std::unexpected{k2::errno_ebusy}; + } + return {}; } inline auto connection::unregister_abort_handler() noexcept -> void { - m_watcher.unwatch(); + if (m_shared_state.is_null() || !m_shared_state.get()->m_unwatch_event.has_value()) { + return; + } + m_shared_state.get()->m_unwatch_event->set(); + m_shared_state.get()->m_unwatch_event.reset(); } } // namespace kphp::component diff --git a/runtime-light/streams/watcher.h b/runtime-light/streams/watcher.h deleted file mode 100644 index c9dd64ebfb..0000000000 --- a/runtime-light/streams/watcher.h +++ /dev/null @@ -1,151 +0,0 @@ -// Compiler for PHP (aka KPHP) -// Copyright (c) 2026 LLC «V Kontakte» -// Distributed under the GPL v3 License, see LICENSE.notice.txt - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "common/containers/final_action.h" -#include "runtime-common/core/class-instance/refcountable-php-classes.h" -#include "runtime-common/core/runtime-core.h" -#include "runtime-light/coroutine/event.h" -#include "runtime-light/coroutine/io-scheduler.h" -#include "runtime-light/coroutine/task.h" -#include "runtime-light/coroutine/type-traits.h" -#include "runtime-light/coroutine/when-any.h" -#include "runtime-light/k2-platform/k2-api.h" -#include "runtime-light/stdlib/diagnostics/logs.h" - -namespace kphp::component { - -class watcher { - struct shared_state : refcountable_php_classes { - std::optional m_unwatch_event; - - shared_state() noexcept = default; - shared_state(shared_state&&) noexcept = default; - shared_state& operator=(shared_state&&) noexcept = default; - ~shared_state() = default; - - shared_state(const shared_state&) = delete; - shared_state operator=(const shared_state&) = delete; - }; - - k2::descriptor m_descriptor{k2::INVALID_PLATFORM_DESCRIPTOR}; - class_instance m_shared_state; - - explicit watcher(k2::descriptor descriptor) noexcept - : m_descriptor(descriptor) { - kphp::log::assertion(!m_shared_state.alloc().is_null()); - } - -public: - watcher(watcher&& other) noexcept - : m_descriptor(std::exchange(other.m_descriptor, k2::INVALID_PLATFORM_DESCRIPTOR)), - m_shared_state(std::move(other.m_shared_state)) {} - - watcher& operator=(watcher&& other) noexcept { - if (this != std::addressof(other)) { - unwatch(); - m_descriptor = std::exchange(other.m_descriptor, k2::INVALID_PLATFORM_DESCRIPTOR); - m_shared_state = std::move(other.m_shared_state); - } - return *this; - } - - watcher() = delete; - watcher(const watcher&) = delete; - watcher& operator=(const watcher&) = delete; - - ~watcher() { - unwatch(); - } - - static auto create(k2::descriptor descriptor) noexcept -> std::expected; - - template - auto watch(on_event_handler_type&& h) noexcept -> std::expected; - - auto unwatch() noexcept -> void; -}; - -// ================================================================================================ - -inline auto watcher::create(k2::descriptor descriptor) noexcept -> std::expected { - if (descriptor == k2::INVALID_PLATFORM_DESCRIPTOR) { - return std::unexpected{k2::errno_einval}; - } - return watcher{descriptor}; -} - -template -auto watcher::watch(on_event_handler_type&& h) noexcept -> std::expected { - if (m_shared_state.is_null()) [[unlikely]] { - return std::unexpected{k2::errno_eshutdown}; - } - if (m_shared_state.get()->m_unwatch_event.has_value()) { // already watching - return std::unexpected{k2::errno_ealready}; - } - - k2::StreamStatus stream_status{}; - k2::stream_status(m_descriptor, std::addressof(stream_status)); - if (stream_status.libc_errno != k2::errno_ok) [[unlikely]] { - return std::unexpected{stream_status.libc_errno}; - } - - static constexpr auto watcher{[](k2::descriptor descriptor, class_instance state, on_event_handler_type h) noexcept -> kphp::coro::task<> { - kphp::log::assertion(!state.is_null() && state.get()->m_unwatch_event.has_value()); - const auto finalizer{vk::finally([state] noexcept { state.get()->m_unwatch_event.reset(); })}; - - static constexpr auto unwatch_awaiter{[](class_instance state) noexcept -> kphp::coro::task<> { co_await *state.get()->m_unwatch_event; }}; - - static constexpr auto descriptor_awaiter{[](k2::descriptor descriptor) noexcept -> kphp::coro::task { - k2::StreamStatus stream_status{}; - auto& io_scheduler{kphp::coro::io_scheduler::get()}; - for (;;) { // TODO it should watch for specific poll_op - k2::stream_status(descriptor, std::addressof(stream_status)); - if (stream_status.write_status == k2::IOStatus::IOClosed) { - co_return std::monostate{}; - } - - using namespace std::chrono_literals; - co_await io_scheduler.schedule(150ms); - } - }}; - - const auto v{co_await kphp::coro::when_any(descriptor_awaiter(descriptor), unwatch_awaiter(std::move(state)))}; - if (std::holds_alternative(v)) { - if constexpr (kphp::coro::is_async_function_v) { - co_await std::invoke(std::move(h)); - } else { - std::invoke(std::move(h)); - } - } - }}; - - m_shared_state.get()->m_unwatch_event.emplace(); - if (!kphp::coro::io_scheduler::get().spawn(watcher(m_descriptor, m_shared_state, std::forward(h)))) { - m_shared_state.get()->m_unwatch_event.reset(); - return std::unexpected{k2::errno_ebusy}; - } - return {}; -} - -inline auto watcher::unwatch() noexcept -> void { - if (m_shared_state.is_null() || !m_shared_state.get()->m_unwatch_event.has_value()) { - return; - } - m_shared_state.get()->m_unwatch_event->set(); - m_shared_state.get()->m_unwatch_event.reset(); -} - -} // namespace kphp::component From 2e764df054119c618bc2eb5c7148a4a7a5637d0c Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Sat, 28 Mar 2026 14:48:58 +0300 Subject: [PATCH 17/20] integrate multipart form-data cleanup into ignore user abort --- runtime-light/server/http/init-functions.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/runtime-light/server/http/init-functions.cpp b/runtime-light/server/http/init-functions.cpp index c7056124cf..21d14756c1 100644 --- a/runtime-light/server/http/init-functions.cpp +++ b/runtime-light/server/http/init-functions.cpp @@ -19,6 +19,7 @@ #include #include "common/algorithms/string-algorithms.h" +#include "common/containers/final_action.h" #include "runtime-common/core/allocator/script-allocator.h" #include "runtime-common/core/runtime-core.h" #include "runtime-common/core/std/containers.h" @@ -403,6 +404,17 @@ kphp::coro::task<> finalize_server() noexcept { auto& http_server_instance_st{HttpServerInstanceState::get()}; kphp::log::assertion(http_server_instance_st.connection.has_value()); http_server_instance_st.connection->unregister_abort_handler(); + + const auto finalizer{vk::finally([&http_server_instance_st] noexcept { + // TODO pay attention when adding flush + std::ranges::for_each(http_server_instance_st.multipart_temporary_files, [](const auto& multipart_filename) noexcept { + if (const auto expected{k2::unlink(multipart_filename)}; !expected) { + kphp::log::warning("failed to unlink multipart temporary file: error code -> {}", expected.error()); + } + }); + http_server_instance_st.multipart_temporary_files.clear(); + })}; + if (http_server_instance_st.connection->is_aborted()) { co_return kphp::log::info("HTTP connection closed"); } @@ -456,10 +468,6 @@ kphp::coro::task<> finalize_server() noexcept { [[fallthrough]]; } case kphp::http::response_state::completed: - for (const auto& temporary_file : http_server_instance_st.multipart_temporary_files) { - std::ignore = k2::unlink(temporary_file); - } - http_server_instance_st.multipart_temporary_files.clear(); co_return; } } From 16595330ad6997306ddebc5d0c7993f337adbfd1 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Sat, 28 Mar 2026 14:51:43 +0300 Subject: [PATCH 18/20] throw error on unsupported ingore_user_abort call --- runtime-light/stdlib/server/common-functions.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/runtime-light/stdlib/server/common-functions.h b/runtime-light/stdlib/server/common-functions.h index 8ff83a9c2b..e5716cab42 100644 --- a/runtime-light/stdlib/server/common-functions.h +++ b/runtime-light/stdlib/server/common-functions.h @@ -16,8 +16,7 @@ inline auto f$ignore_user_abort(Optional enable) noexcept -> kphp::coro::task { if (InstanceState::get().instance_kind() != instance_kind::http_server) { - kphp::log::warning("called stub f$ignore_user_abort"); - co_return 0; + kphp::log::error("called stub f$ignore_user_abort"); } auto& http_server_instance_st{HttpServerInstanceState::get()}; From d461691916f606b169eedd8fb72f29635a48bd26 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Sat, 28 Mar 2026 15:07:20 +0300 Subject: [PATCH 19/20] minor change --- runtime-light/state/instance-state.h | 3 +- runtime-light/stdlib/rpc/rpc-api.cpp | 2 +- tests/python/tests/http_server/php/index.php | 12 ++--- .../http_server/test_ignore_user_abort.py | 45 ++++++++++++++++++- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/runtime-light/state/instance-state.h b/runtime-light/state/instance-state.h index 1c91367c30..ddec40a59d 100644 --- a/runtime-light/state/instance-state.h +++ b/runtime-light/state/instance-state.h @@ -89,9 +89,10 @@ struct InstanceState final : vk::not_copyable { } AllocatorState instance_allocator_state{INIT_INSTANCE_ALLOCATOR_SIZE, 0}; - kphp::log::contextual_tags instance_tags; + kphp::log::contextual_tags instance_tags; kphp::coro::io_scheduler io_scheduler; + CoroutineInstanceState coroutine_instance_state; ForkInstanceState fork_instance_state; WaitQueueInstanceState wait_queue_instance_state; diff --git a/runtime-light/stdlib/rpc/rpc-api.cpp b/runtime-light/stdlib/rpc/rpc-api.cpp index 6ac41e3272..d881fa3c11 100644 --- a/runtime-light/stdlib/rpc/rpc-api.cpp +++ b/runtime-light/stdlib/rpc/rpc-api.cpp @@ -345,7 +345,7 @@ kphp::coro::task send_request(std::string_view actor, std }}; static constexpr auto ignore_answer_awaiter_coroutine{ - [](kphp::component::stream stream, std::chrono::milliseconds timeout) noexcept -> kphp::coro::shared_task { + [](kphp::component::stream stream, std::chrono::milliseconds timeout) noexcept -> kphp::coro::shared_task<> { auto fetch_task{kphp::component::fetch_response(stream, [](std::span) noexcept {})}; std::ignore = co_await kphp::coro::io_scheduler::get().schedule(std::move(fetch_task), timeout); }}; diff --git a/tests/python/tests/http_server/php/index.php b/tests/python/tests/http_server/php/index.php index 125d31dbe4..ddbf2477e0 100644 --- a/tests/python/tests/http_server/php/index.php +++ b/tests/python/tests/http_server/php/index.php @@ -29,7 +29,7 @@ function rpc_fetch_responses(array $query_ids): array { * @kphp-required */ function shutdown_function() { - fwrite(STDERR, "shutdown_function was called\n"); + fwrite(fopen("php://stderr", "w"), "shutdown_function was called\n"); } function assert($flag) { @@ -56,7 +56,7 @@ public function work(string $output) { foreach ($responses as $resp) { assert($resp); } - fwrite(STDERR, $output); + fwrite(fopen("php://stderr", "w"), $output); } } @@ -319,6 +319,8 @@ public function work(string $output) { default: echo "ERROR"; return; } + + $stderr = fopen("php://stderr", "w"); switch($_GET["level"]) { case "no_ignore": $worker->work($msg); @@ -326,14 +328,14 @@ public function work(string $output) { case "ignore": ignore_user_abort(true); $worker->work($msg); - fwrite(STDERR, "test_ignore_user_abort/finish_ignore_" . $_GET["type"] . "\n"); + fwrite($stderr, "test_ignore_user_abort/finish_ignore_" . $_GET["type"] . "\n"); ignore_user_abort(false); break; case "multi_ignore": ignore_user_abort(true); $worker->work($msg); $worker->work($msg); - fwrite(STDERR, "test_ignore_user_abort/finish_multi_ignore_" . $_GET["type"] . "\n"); + fwrite($stderr, "test_ignore_user_abort/finish_multi_ignore_" . $_GET["type"] . "\n"); ignore_user_abort(false); break; case "nested_ignore": @@ -341,7 +343,7 @@ public function work(string $output) { ignore_user_abort(true); $worker->work($msg); ignore_user_abort(false); - fwrite(STDERR, "test_ignore_user_abort/finish_nested_ignore_" . $_GET["type"] . "\n"); + fwrite($stderr, "test_ignore_user_abort/finish_nested_ignore_" . $_GET["type"] . "\n"); ignore_user_abort(false); default: echo "ERROR"; return; diff --git a/tests/python/tests/http_server/test_ignore_user_abort.py b/tests/python/tests/http_server/test_ignore_user_abort.py index 02070720b3..fe7bbe2e6e 100644 --- a/tests/python/tests/http_server/test_ignore_user_abort.py +++ b/tests/python/tests/http_server/test_ignore_user_abort.py @@ -16,7 +16,7 @@ def _send_request(self, uri="/", timeout=0.05): pass """ - Changing the name leads to different tests run order and for some reason it helps to get rid of ASAN warning. + Changing the name leads to different tests run order and for some reason it helps to get rid of ASAN warning. As we decided that the previous ASAN warning was false-positive, this kind of fix might be acceptable for us. Old name was - "test_user_abort_rpc_work" """ @@ -88,3 +88,46 @@ def test_idempotence_ignore_user_abort(self): self.web_server.assert_log(["test_ignore_user_abort/finish_resumable_work_" + "nested_ignore"], timeout=5) self.web_server.assert_log(["test_ignore_user_abort/finish_nested_ignore_" + "resumable"], timeout=5) self.web_server.assert_log(["shutdown_function was called"], timeout=5) + +@pytest.mark.kphp_skip +class TestIgnoreUserAbortK2(WebServerAutoTestCase): + + def _send_request(self, uri="/", timeout=0.05): + try: + self.web_server.http_request(uri=uri, timeout=timeout) + except Exception: + pass + + def test_user_abort_resumable_work(self): + self._send_request(uri='/test_ignore_user_abort?type=resumable&level=no_ignore') + self.web_server.assert_log(['(.*)hyper\\:\\:Error\\(IncompleteMessage\\)(.*)'], timeout=10) + error = False + try: + self.web_server.assert_log(["test_ignore_user_abort/finish_resumable_work_" + "no_ignore"], timeout=10) + except Exception: + error = True + self.assertTrue(error) + + def test_ignore_user_abort_resumable_work(self): + self._send_request(uri='/test_ignore_user_abort?type=resumable&level=ignore') + self.web_server.assert_log(['(.*)hyper\\:\\:Error\\(IncompleteMessage\\)(.*)'], timeout=10) + self.web_server.assert_log(['(.*)HTTP connection closed(.*)'], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_resumable_work_" + "ignore"], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_ignore_" + "resumable"], timeout=10) + self.web_server.assert_log(["shutdown_function was called"], timeout=10) + + def test_nested_ignore_user_abort_resumable_work(self): + self._send_request(uri='/test_ignore_user_abort?type=resumable&level=nested_ignore') + self.web_server.assert_log(['(.*)hyper\\:\\:Error\\(IncompleteMessage\\)(.*)'], timeout=10) + self.web_server.assert_log(['(.*)HTTP connection closed(.*)'], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_resumable_work_" + "nested_ignore"], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_nested_ignore_" + "resumable"], timeout=10) + self.web_server.assert_log(["shutdown_function was called"], timeout=10) + + def test_multi_ignore_user_abort_resumable_work(self): + self._send_request(uri='/test_ignore_user_abort?type=resumable&level=multi_ignore') + self.web_server.assert_log(['(.*)hyper\\:\\:Error\\(IncompleteMessage\\)(.*)'], timeout=10) + self.web_server.assert_log(['(.*)HTTP connection closed(.*)'], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_resumable_work_" + "multi_ignore"], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_multi_ignore_" + "resumable"], timeout=10) + self.web_server.assert_log(["shutdown_function was called"], timeout=10) From 4ad992f377591941012a543a448e05221e2d1575 Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Sat, 28 Mar 2026 15:39:09 +0300 Subject: [PATCH 20/20] add missing include --- runtime-light/streams/connection.h | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime-light/streams/connection.h b/runtime-light/streams/connection.h index 1463e9be2e..1cdddc49ab 100644 --- a/runtime-light/streams/connection.h +++ b/runtime-light/streams/connection.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "runtime-common/core/runtime-core.h"