Skip to content

Commit 62fef82

Browse files
committed
[k2] implement ignore_user_abort for HTTP server mode
1 parent dbdba43 commit 62fef82

6 files changed

Lines changed: 203 additions & 10 deletions

File tree

builtin-functions/kphp-light/stdlib/server-functions.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ function kphp_get_runtime_config() ::: mixed;
1010

1111
function register_shutdown_function (callable():void $callback) ::: void;
1212

13+
/** @kphp-extern-func-info interruptible */
14+
function ignore_user_abort ($enable ::: ?bool = null) ::: int;
15+
1316
// === URL ========================================================================================
1417

1518
define('PHP_URL_SCHEME', 0);
@@ -125,8 +128,6 @@ define('LC_MESSAGES', 5);
125128

126129
function debug_backtrace() ::: string[][];
127130

128-
/** @kphp-extern-func-info stub generation-required */
129-
function ignore_user_abort ($enable ::: ?bool = null) ::: int;
130131
/** @kphp-extern-func-info stub generation-required */
131132
function flush() ::: void;
132133

runtime-light/server/http/http-server-state.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
#include <locale>
1010
#include <optional>
1111
#include <string_view>
12+
#include <utility>
1213

1314
#include "common/mixin/not_copyable.h"
1415
#include "runtime-common/core/allocator/script-allocator.h"
1516
#include "runtime-common/core/runtime-core.h"
1617
#include "runtime-common/core/std/containers.h"
1718
#include "runtime-light/coroutine/task.h"
1819
#include "runtime-light/streams/stream.h"
20+
#include "runtime-light/streams/watcher.h"
1921

2022
namespace kphp::http {
2123

@@ -55,7 +57,9 @@ struct HttpServerInstanceState final : private vk::not_copyable {
5557
static constexpr auto ENCODING_GZIP = static_cast<uint32_t>(1U << 0U);
5658
static constexpr auto ENCODING_DEFLATE = static_cast<uint32_t>(1U << 1U);
5759

58-
std::optional<kphp::component::stream> request_stream;
60+
std::optional<kphp::component::stream> opt_connection;
61+
std::optional<kphp::component::watcher> opt_user_abort_watcher;
62+
uint32_t ignore_user_abort_level{};
5963

6064
std::optional<string> opt_raw_post_data;
6165

runtime-light/server/http/init-functions.cpp

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,18 @@
2424
#include "runtime-common/core/std/containers.h"
2525
#include "runtime-common/stdlib/server/url-functions.h"
2626
#include "runtime-light/core/globals/php-script-globals.h"
27+
#include "runtime-light/coroutine/task.h"
2728
#include "runtime-light/k2-platform/k2-api.h"
2829
#include "runtime-light/server/http/http-server-state.h"
2930
#include "runtime-light/state/instance-state.h"
3031
#include "runtime-light/stdlib/component/component-api.h"
3132
#include "runtime-light/stdlib/diagnostics/logs.h"
3233
#include "runtime-light/stdlib/output/output-state.h"
3334
#include "runtime-light/stdlib/server/http-functions.h"
35+
#include "runtime-light/stdlib/system/system-functions.h"
3436
#include "runtime-light/stdlib/zlib/zlib-functions.h"
3537
#include "runtime-light/streams/stream.h"
38+
#include "runtime-light/streams/watcher.h"
3639
#include "runtime-light/tl/tl-core.h"
3740
#include "runtime-light/tl/tl-functions.h"
3841
#include "runtime-light/tl/tl-types.h"
@@ -241,7 +244,29 @@ void init_server(kphp::component::stream&& request_stream, kphp::stl::vector<std
241244
auto& superglobals{InstanceState::get().php_script_mutable_globals_singleton.get_superglobals()};
242245
auto& server{superglobals.v$_SERVER};
243246
auto& http_server_instance_st{HttpServerInstanceState::get()};
244-
http_server_instance_st.request_stream = std::move(request_stream);
247+
248+
{
249+
http_server_instance_st.opt_connection.emplace(std::move(request_stream));
250+
251+
auto expected_user_abort_watcher{kphp::component::watcher::create(http_server_instance_st.opt_connection->descriptor())};
252+
if (!expected_user_abort_watcher) [[unlikely]] {
253+
kphp::log::error("failed to create user abort watcher: error code -> {}", expected_user_abort_watcher.error());
254+
}
255+
256+
static constexpr auto user_abort_watcher{[] noexcept -> kphp::coro::task<> {
257+
auto& http_server_instance_st{HttpServerInstanceState::get()};
258+
http_server_instance_st.opt_connection.reset();
259+
http_server_instance_st.opt_user_abort_watcher.reset();
260+
if (http_server_instance_st.ignore_user_abort_level == 0) {
261+
co_await kphp::system::exit(1);
262+
}
263+
}};
264+
265+
http_server_instance_st.opt_user_abort_watcher.emplace(*std::move(expected_user_abort_watcher));
266+
if (auto expected{http_server_instance_st.opt_user_abort_watcher->watch(user_abort_watcher)}; !expected) [[unlikely]] {
267+
kphp::log::error("failed to setup user abort watcher: error code -> {}", expected.error());
268+
}
269+
}
245270

246271
// determine HTTP method
247272
if (invoke_http.method.value == GET_METHOD) {
@@ -379,6 +404,7 @@ void init_server(kphp::component::stream&& request_stream, kphp::stl::vector<std
379404

380405
kphp::coro::task<> finalize_server() noexcept {
381406
auto& http_server_instance_st{HttpServerInstanceState::get()};
407+
http_server_instance_st.opt_user_abort_watcher.reset();
382408

383409
string response_body{};
384410
tl::HttpResponse http_response{};
@@ -419,15 +445,18 @@ kphp::coro::task<> finalize_server() noexcept {
419445
[[fallthrough]];
420446
}
421447
case kphp::http::response_state::sending_body: {
448+
if (!http_server_instance_st.opt_connection) [[unlikely]] {
449+
if (http_server_instance_st.ignore_user_abort_level > 0) {
450+
co_return kphp::log::warning("HTTP connection closed");
451+
}
452+
kphp::log::error("HTTP connection closed");
453+
}
454+
422455
tl::storer tls{http_response.footprint()};
423456
http_response.store(tls);
424457

425-
if (!http_server_instance_st.request_stream.has_value()) [[unlikely]] {
426-
kphp::log::error("can't send HTTP response since there is no available stream");
427-
}
428-
auto& request_stream{*http_server_instance_st.request_stream};
429-
if (auto expected{co_await kphp::component::send_response(request_stream, tls.view())}; !expected) [[unlikely]] {
430-
kphp::log::error("can't write HTTP response: stream -> {}, error code -> {}", request_stream.descriptor(), expected.error());
458+
if (auto expected{co_await kphp::component::send_response(*http_server_instance_st.opt_connection, tls.view())}; !expected) [[unlikely]] {
459+
kphp::log::error("can't write HTTP response: error code -> {}", expected.error());
431460
}
432461
http_server_instance_st.response_state = kphp::http::response_state::completed;
433462
[[fallthrough]];

runtime-light/state/instance-state.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include <cstddef>
88
#include <cstdint>
9+
#include <optional>
910

1011
#include "common/mixin/not_copyable.h"
1112
#include "runtime-common/core/runtime-core.h"
@@ -41,6 +42,7 @@
4142
#include "runtime-light/stdlib/system/system-state.h"
4243
#include "runtime-light/stdlib/time/time-state.h"
4344
#include "runtime-light/stdlib/web-transfer-lib/web-state.h"
45+
#include "runtime-light/streams/watcher.h"
4446

4547
/**
4648
* Supported kinds of KPHP images:
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Compiler for PHP (aka KPHP)
2+
// Copyright (c) 2026 LLC «V Kontakte»
3+
// Distributed under the GPL v3 License, see LICENSE.notice.txt
4+
5+
#pragma once
6+
7+
#include <cstdint>
8+
9+
#include "runtime-common/core/runtime-core.h"
10+
#include "runtime-light/coroutine/task.h"
11+
#include "runtime-light/server/http/http-server-state.h"
12+
#include "runtime-light/state/instance-state.h"
13+
#include "runtime-light/stdlib/diagnostics/logs.h"
14+
#include "runtime-light/stdlib/system/system-functions.h"
15+
16+
inline auto f$ignore_user_abort(Optional<bool> enable) noexcept -> kphp::coro::task<int64_t> {
17+
if (InstanceState::get().instance_kind() != instance_kind::http_server) {
18+
kphp::log::warning("called stub f$ignore_user_abort");
19+
co_return 0;
20+
}
21+
22+
auto& http_server_instance_st{HttpServerInstanceState::get()};
23+
if (enable.is_null()) {
24+
co_return http_server_instance_st.ignore_user_abort_level;
25+
} else if (enable.val()) {
26+
co_return http_server_instance_st.ignore_user_abort_level++;
27+
} else {
28+
const auto prev{http_server_instance_st.ignore_user_abort_level > 0 ? http_server_instance_st.ignore_user_abort_level-- : 0};
29+
30+
if (http_server_instance_st.ignore_user_abort_level == 0 && !http_server_instance_st.opt_connection) {
31+
co_await kphp::system::exit(1);
32+
}
33+
co_return prev;
34+
}
35+
}

runtime-light/streams/watcher.h

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Compiler for PHP (aka KPHP)
2+
// Copyright (c) 2026 LLC «V Kontakte»
3+
// Distributed under the GPL v3 License, see LICENSE.notice.txt
4+
5+
#pragma once
6+
7+
#include <concepts>
8+
#include <cstddef>
9+
#include <cstdint>
10+
#include <expected>
11+
#include <functional>
12+
#include <memory>
13+
#include <optional>
14+
#include <utility>
15+
#include <variant>
16+
17+
#include "runtime-light/coroutine/event.h"
18+
#include "runtime-light/coroutine/io-scheduler.h"
19+
#include "runtime-light/coroutine/task.h"
20+
#include "runtime-light/coroutine/type-traits.h"
21+
#include "runtime-light/coroutine/when-any.h"
22+
#include "runtime-light/k2-platform/k2-api.h"
23+
24+
namespace kphp::component {
25+
26+
class watcher {
27+
k2::descriptor m_descriptor{k2::INVALID_PLATFORM_DESCRIPTOR};
28+
// TODO it should watch for specific poll_op
29+
std::optional<kphp::coro::event> m_unwatch_event;
30+
31+
explicit watcher(k2::descriptor descriptor) noexcept
32+
: m_descriptor(descriptor) {}
33+
34+
public:
35+
watcher(watcher&& other) noexcept
36+
: m_descriptor(std::exchange(other.m_descriptor, k2::INVALID_PLATFORM_DESCRIPTOR)),
37+
m_unwatch_event(std::exchange(other.m_unwatch_event, {})) {}
38+
39+
watcher& operator=(watcher&& other) noexcept {
40+
if (this != std::addressof(other)) {
41+
m_descriptor = std::exchange(other.m_descriptor, k2::INVALID_PLATFORM_DESCRIPTOR);
42+
m_unwatch_event = std::exchange(other.m_unwatch_event, {});
43+
}
44+
return *this;
45+
}
46+
47+
watcher() = delete;
48+
watcher(const watcher&) = delete;
49+
watcher& operator=(const watcher&) = delete;
50+
51+
~watcher() {
52+
unwatch();
53+
}
54+
55+
static auto create(k2::descriptor descriptor) noexcept -> std::expected<watcher, int32_t>;
56+
57+
template<std::invocable on_event_handler_type>
58+
auto watch(on_event_handler_type&& f) noexcept -> std::expected<void, int32_t>;
59+
60+
auto unwatch() noexcept -> void;
61+
};
62+
63+
inline auto watcher::create(k2::descriptor descriptor) noexcept -> std::expected<watcher, int32_t> {
64+
if (descriptor == k2::INVALID_PLATFORM_DESCRIPTOR) {
65+
return std::unexpected{k2::errno_einval};
66+
}
67+
return watcher{descriptor};
68+
}
69+
70+
template<std::invocable on_event_handler_type>
71+
auto watcher::watch(on_event_handler_type&& f) noexcept -> std::expected<void, int32_t> {
72+
if (m_unwatch_event) { // already watching
73+
return std::unexpected{k2::errno_ealready};
74+
}
75+
76+
k2::StreamStatus stream_status{};
77+
k2::stream_status(m_descriptor, std::addressof(stream_status));
78+
if (stream_status.libc_errno != k2::errno_ok) [[unlikely]] {
79+
return std::unexpected{stream_status.libc_errno};
80+
}
81+
82+
static constexpr auto watcher{[](k2::descriptor descriptor, kphp::coro::event& unwatch_event, on_event_handler_type f) noexcept -> kphp::coro::task<> {
83+
static constexpr auto unwatch_awaiter{[](kphp::coro::event& unwatch_event) noexcept -> kphp::coro::task<> { co_await unwatch_event; }};
84+
static constexpr auto update_awaiter{[](k2::descriptor descriptor) noexcept -> kphp::coro::task<std::monostate> {
85+
k2::StreamStatus stream_status{};
86+
auto& io_scheduler{kphp::coro::io_scheduler::get()};
87+
for (;;) {
88+
k2::stream_status(descriptor, std::addressof(stream_status));
89+
if (stream_status.write_status == k2::IOStatus::IOClosed) {
90+
co_return std::monostate{};
91+
}
92+
93+
using namespace std::chrono_literals;
94+
co_await io_scheduler.schedule(150ms);
95+
}
96+
}};
97+
98+
if (std::holds_alternative<std::monostate>(co_await kphp::coro::when_any(update_awaiter(descriptor), unwatch_awaiter(unwatch_event)))) {
99+
if constexpr (kphp::coro::is_async_function_v<on_event_handler_type>) {
100+
co_await std::invoke(std::move(f));
101+
} else {
102+
std::invoke(std::move(f));
103+
}
104+
}
105+
}};
106+
107+
if (!kphp::coro::io_scheduler::get().start(watcher(m_descriptor, m_unwatch_event.emplace(), std::forward<on_event_handler_type>(f)))) {
108+
m_unwatch_event.reset();
109+
return std::unexpected{k2::errno_ebusy};
110+
}
111+
return {};
112+
}
113+
114+
inline auto watcher::unwatch() noexcept -> void {
115+
if (!m_unwatch_event) {
116+
return;
117+
}
118+
m_unwatch_event->set();
119+
m_unwatch_event.reset();
120+
}
121+
122+
} // namespace kphp::component

0 commit comments

Comments
 (0)