Skip to content

Commit 53557e0

Browse files
authored
Refactor curl native impl (#5743)
* Optimize the implementation of curl coroutines to support KeepAlive, reduce memory allocation, and align the logic of Multi::exec() with that of Multi::select(). Refer: GH-5474 & GH-5101 * Copy handle->sockets to a new unordered_map to avoid memory errors caused by deleting elements during iteration. * fix --filter=[unit] * fix 2 --filter=[unit]
1 parent b5129b0 commit 53557e0

File tree

10 files changed

+215
-108
lines changed

10 files changed

+215
-108
lines changed

ext-src/php_swoole_curl.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,15 @@ struct HandleSocket {
5353
struct Handle {
5454
CURL *cp;
5555
Multi *multi;
56+
// This is only for the swoole_curl_easy_perform function, and it has a one-to-one relationship with the curl
57+
// handle. It must be destroyed when the curl handle is released.
58+
Multi *easy_multi;
5659
std::unordered_map<int, HandleSocket *> sockets;
5760

5861
Handle(CURL *_cp) {
5962
cp = _cp;
6063
multi = nullptr;
64+
easy_multi = nullptr;
6165
}
6266

6367
HandleSocket *create_socket(curl_socket_t sockfd);
@@ -79,17 +83,17 @@ class Multi {
7983
long timeout_ms_ = 0;
8084
Coroutine *co = nullptr;
8185
int running_handles_ = 0;
82-
int last_sockfd;
8386
int event_count_ = 0;
8487
bool defer_callback = false;
85-
std::unique_ptr<Selector> selector;
88+
Selector selector;
8689

8790
CURLcode read_info();
8891

8992
HandleSocket *create_socket(Handle *handle, curl_socket_t sockfd);
9093

9194
void set_event(CURL *cp, void *socket_ptr, curl_socket_t sockfd, int action);
9295
void del_event(CURL *cp, void *socket_ptr, curl_socket_t sockfd);
96+
void selector_finish();
9397

9498
void add_timer(long timeout_ms) {
9599
if (timer && swoole_timer_is_available()) {
@@ -139,10 +143,6 @@ class Multi {
139143
return running_handles_;
140144
}
141145

142-
void set_selector(Selector *_selector) {
143-
selector.reset(_selector);
144-
}
145-
146146
CURLMcode add_handle(Handle *handle);
147147
CURLMcode remove_handle(Handle *handle);
148148

ext-src/swoole_curl.cc

Lines changed: 55 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ void destroy_handle(CURL *cp) {
4747
}
4848
auto handle = iter->second;
4949
handle_buckets.erase(iter);
50+
51+
if (handle->easy_multi) {
52+
delete handle->easy_multi;
53+
}
54+
5055
delete handle;
5156
swoole_trace_log(SW_TRACE_CO_CURL, SW_ECHO_RED " handle=%p, curl=%p", "[DESTROY]", handle, cp);
5257
}
@@ -191,10 +196,6 @@ void Multi::set_event(CURL *cp, void *socket_ptr, curl_socket_t sockfd, int acti
191196
}
192197

193198
CURLMcode Multi::add_handle(Handle *handle) {
194-
if (handle == nullptr) {
195-
php_swoole_fatal_error(E_WARNING, "The given handle is not initialized in coroutine");
196-
return CURLM_INTERNAL_ERROR;
197-
}
198199
auto retval = curl_multi_add_handle(multi_handle_, handle->cp);
199200
if (retval == CURLM_OK) {
200201
handle->multi = this;
@@ -242,21 +243,8 @@ CURLcode Multi::exec(Handle *handle) {
242243
break;
243244
}
244245

245-
int sockfd = last_sockfd;
246-
int bitmask = 0;
247-
if (sockfd >= 0) {
248-
auto it = handle->sockets.find(sockfd);
249-
if (it != handle->sockets.end()) {
250-
curl_socket = it->second;
251-
bitmask = curl_socket->event_bitmask;
252-
if (!curl_socket->socket->removed && swoole_event_del(curl_socket->socket) == SW_OK) {
253-
event_count_--;
254-
}
255-
}
256-
}
257-
del_timer();
246+
selector_finish();
258247

259-
curl_multi_socket_action(multi_handle_, sockfd, bitmask, &running_handles_);
260248
swoole_trace_log(SW_TRACE_CO_CURL,
261249
"curl_multi_socket_action: handle=%p, sockfd=%d, bitmask=%d, running_handles_=%d",
262250
handle,
@@ -267,38 +255,17 @@ CURLcode Multi::exec(Handle *handle) {
267255
break;
268256
}
269257
set_timer();
270-
if (sockfd >= 0) {
271-
auto it = handle->sockets.find(sockfd);
272-
if (it != handle->sockets.end()) {
273-
curl_socket = it->second;
274-
if (curl_socket->socket && curl_socket->socket->removed) {
275-
if (swoole_event_add(curl_socket->socket, get_event(curl_socket->action)) == SW_OK) {
276-
event_count_++;
277-
}
278-
}
279-
}
280-
}
258+
}
281259

282-
if (!timer) {
283-
bool removed = true;
284-
for (auto it = handle->sockets.begin(); it != handle->sockets.end();) {
285-
curl_socket = it->second;
286-
if (curl_socket->socket) {
287-
if (curl_socket->socket->removed) {
288-
it = handle->sockets.erase(it);
289-
delete curl_socket;
290-
continue;
291-
} else {
292-
removed = false;
293-
}
294-
}
295-
++it;
296-
}
297-
if (removed) {
298-
break;
260+
for (auto it : handle->sockets) {
261+
curl_socket = it.second;
262+
if (curl_socket->socket && !curl_socket->socket->removed) {
263+
if (swoole_event_del(curl_socket->socket) == SW_OK) {
264+
event_count_--;
299265
}
300266
}
301267
}
268+
del_timer();
302269

303270
CURLcode retval = read_info();
304271
remove_handle(handle);
@@ -347,6 +314,33 @@ int Multi::handle_timeout(CURLM *mh, long timeout_ms, void *userp) {
347314
return 0;
348315
}
349316

317+
void Multi::selector_finish() {
318+
del_timer();
319+
320+
if (selector.timer_callback) {
321+
selector.timer_callback = false;
322+
curl_multi_socket_action(multi_handle_, CURL_SOCKET_TIMEOUT, 0, &running_handles_);
323+
swoole_trace_log(SW_TRACE_CO_CURL, "socket_action[timer], running_handles=%d", running_handles_);
324+
}
325+
326+
for (auto handle : selector.active_handles) {
327+
/**
328+
* In `curl_multi_socket_action`, `Handle::destroy_socket()` may be invoked,
329+
* which will remove entries from the `unordered_map`.
330+
* In C++, removing elements during iteration can render the iterator invalid; hence,
331+
* it's necessary to copy `handle->sockets` into a new `unordered_map`.
332+
*/
333+
auto sockets = handle->sockets;
334+
for (auto it : sockets) {
335+
HandleSocket *handle_socket = it.second;
336+
curl_multi_socket_action(
337+
multi_handle_, handle_socket->event_fd, handle_socket->event_bitmask, &running_handles_);
338+
swoole_trace_log(SW_TRACE_CO_CURL, "socket_action[socket], running_handles=%d", running_handles_);
339+
}
340+
}
341+
selector.active_handles.clear();
342+
}
343+
350344
long Multi::select(php_curlm *mh, double timeout) {
351345
if (zend_llist_count(&mh->easyh) == 0) {
352346
return 0;
@@ -399,7 +393,7 @@ long Multi::select(php_curlm *mh, double timeout) {
399393

400394
swoole_trace_log(SW_TRACE_CO_CURL, "yield timeout, count=%lu", zend_llist_count(&mh->easyh));
401395

402-
auto count = selector->active_handles.size();
396+
auto count = selector.active_handles.size();
403397

404398
for (zend_llist_element *element = mh->easyh.head; element; element = element->next) {
405399
zval *z_ch = (zval *) element->data;
@@ -419,44 +413,17 @@ long Multi::select(php_curlm *mh, double timeout) {
419413
}
420414
}
421415
}
422-
del_timer();
423416

424-
if (selector->timer_callback) {
425-
selector->timer_callback = false;
426-
curl_multi_socket_action(multi_handle_, CURL_SOCKET_TIMEOUT, 0, &running_handles_);
427-
swoole_trace_log(SW_TRACE_CO_CURL, "socket_action[timer], running_handles=%d", running_handles_);
428-
}
429-
430-
for (auto iter = selector->active_handles.begin(); iter != selector->active_handles.end(); iter++) {
431-
Handle *handle = *iter;
432-
if (handle) {
433-
for (auto it = handle->sockets.begin(); it != handle->sockets.end();) {
434-
HandleSocket *handle_socket = it->second;
435-
it++;
436-
curl_multi_socket_action(
437-
multi_handle_, handle_socket->event_fd, handle_socket->event_bitmask, &running_handles_);
438-
swoole_trace_log(SW_TRACE_CO_CURL, "socket_action[socket], running_handles=%d", running_handles_);
439-
}
440-
}
441-
}
442-
443-
selector->active_handles.clear();
417+
selector_finish();
444418

445419
return count;
446420
}
447421

448422
void Multi::callback(Handle *handle, int event_bitmask, int sockfd) {
449423
swoole_trace_log(
450424
SW_TRACE_CO_CURL, "handle=%p, event_bitmask=%d, co=%p, sockfd=%d", handle, event_bitmask, co, sockfd);
451-
if (handle) {
452-
last_sockfd = sockfd;
453-
} else {
454-
last_sockfd = -1;
455-
}
456-
if (selector.get()) {
457-
if (!handle) {
458-
selector->timer_callback = true;
459-
}
425+
if (!handle) {
426+
selector.timer_callback = true;
460427
}
461428
if (!co) {
462429
if (handle) {
@@ -470,8 +437,8 @@ void Multi::callback(Handle *handle, int event_bitmask, int sockfd) {
470437
}
471438
return;
472439
}
473-
if (selector.get() && handle) {
474-
selector->active_handles.insert(handle);
440+
if (handle) {
441+
selector.active_handles.insert(handle);
475442
}
476443
if (defer_callback) {
477444
return;
@@ -490,10 +457,11 @@ void Multi::callback(Handle *handle, int event_bitmask, int sockfd) {
490457
} // namespace swoole
491458

492459
CURLcode swoole_curl_easy_perform(CURL *cp) {
493-
Multi *multi = new Multi();
494-
CURLcode error = multi->exec(swoole::curl::get_handle(cp));
495-
delete multi;
496-
return error;
460+
auto handle = swoole::curl::get_handle(cp);
461+
if (!handle->easy_multi) {
462+
handle->easy_multi = new Multi();
463+
}
464+
return handle->easy_multi->exec(handle);
497465
}
498466

499467
php_curl *swoole_curl_get_handle(zval *zid, bool exclusive, bool required) {
@@ -503,6 +471,10 @@ php_curl *swoole_curl_get_handle(zval *zid, bool exclusive, bool required) {
503471
}
504472
if (exclusive && swoole_coroutine_is_in()) {
505473
auto handle = swoole::curl::get_handle(ch->cp);
474+
if (required && !handle) {
475+
php_swoole_fatal_error(E_WARNING, "The given handle is not initialized in coroutine");
476+
return nullptr;
477+
}
506478
if (handle && handle->multi && handle->multi->check_bound_co() == nullptr) {
507479
return nullptr;
508480
}

tests/swoole_curl/keepalive.phpt

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
--TEST--
2+
swoole_curl: keepalive
3+
--SKIPIF--
4+
<?php
5+
require __DIR__ . '/../include/skipif.inc';
6+
?>
7+
--FILE--
8+
<?php
9+
require __DIR__ . '/../include/bootstrap.php';
10+
11+
use Swoole\Http\Request;
12+
use Swoole\Http\Response;
13+
use Swoole\Runtime;
14+
15+
use function Swoole\Coroutine\run;
16+
$pm = new SwooleTest\ProcessManager;
17+
18+
$pm->parentFunc = function () use ($pm) {
19+
Runtime::enableCoroutine(SWOOLE_HOOK_NATIVE_CURL);
20+
run(function () use ($pm) {
21+
$ch = curl_init();
22+
$code = uniqid('swoole_');
23+
24+
$execFn = function () use ($ch, $code, $pm) {
25+
$url = "http://127.0.0.1:" . $pm->getFreePort() . "/?code=" . urlencode($code);
26+
curl_setopt($ch, CURLOPT_URL, $url);
27+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
28+
curl_setopt($ch, CURLOPT_HEADER, 0);
29+
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $strHeader) {
30+
return strlen($strHeader);
31+
});
32+
$output = curl_exec($ch);
33+
$info = curl_getinfo($ch);
34+
Assert::eq($output, "Hello World\n" . $code);
35+
if ($output === false) {
36+
exit("CURL Error:" . curl_error($ch));
37+
}
38+
return $info;
39+
};
40+
41+
echo "co 1 exec\n";
42+
$info1 = $execFn();
43+
44+
Co::sleep(0.1);
45+
46+
echo "co 2 exec\n";
47+
$info2 = $execFn();
48+
49+
Assert::eq($info1['local_port'], $info2['local_port']);
50+
51+
curl_close($ch);
52+
});
53+
$pm->kill();
54+
echo "Done\n";
55+
};
56+
$pm->childFunc = function () use ($pm) {
57+
$http = new Swoole\Http\Server("127.0.0.1", $pm->getFreePort(), SWOOLE_PROCESS);
58+
$http->set(['worker_num' => 2, 'log_file' => '/dev/null']);
59+
60+
$http->on("start", function ($server) use ($pm) {
61+
$pm->wakeup();
62+
});
63+
64+
$http->on("request", function (Request $request, Response $response) {
65+
$response->end("Hello World\n" . $request->get['code']);
66+
});
67+
$http->start();
68+
};
69+
$pm->childFirst();
70+
$pm->run();
71+
?>
72+
--EXPECTF--
73+
co 1 exec
74+
co 2 exec
75+
Done
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
swoole_curl/multi: add handle after easy exec
3+
--SKIPIF--
4+
<?php require __DIR__ . '/../../include/skipif.inc'; ?>
5+
<?php
6+
if (!extension_loaded("curl")) exit("skip curl extension not loaded");
7+
?>
8+
--FILE--
9+
<?php
10+
require __DIR__ . '/../../include/bootstrap.php';
11+
use Swoole\Runtime;
12+
13+
use function Swoole\Coroutine\run;
14+
15+
Runtime::enableCoroutine(SWOOLE_HOOK_NATIVE_CURL);
16+
17+
use SwooleTest\CurlManager;
18+
19+
$cm = new CurlManager();
20+
$cm->run(function ($host) {
21+
$ch1 = curl_init();
22+
curl_setopt($ch1, CURLOPT_URL, "{$host}/get.php?test=get");
23+
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, 1);
24+
$rs = curl_exec($ch1);
25+
Assert::eq($rs, "Hello World!\nHello World!");
26+
27+
$mh1 = curl_multi_init();
28+
Assert::eq(curl_multi_add_handle($mh1, $ch1), 0);
29+
});
30+
?>
31+
--EXPECT--

0 commit comments

Comments
 (0)