• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

azjezz / psl / 22519606807

28 Feb 2026 11:11AM UTC coverage: 97.532% (-1.2%) from 98.733%
22519606807

push

github

web-flow
feat(network): rewrite networking stack with TLS, UDP, SOCKS5, CIDR, and IO utilities (#585)

860 of 937 new or added lines in 31 files covered. (91.78%)

15 existing lines in 6 files now uncovered.

7470 of 7659 relevant lines covered (97.53%)

42.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.18
/src/Psl/TCP/SocketPool.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\TCP;
6

7
use Psl\DateTime\Duration;
8
use Revolt\EventLoop;
9

10
use function array_filter;
11
use function array_pop;
12
use function array_values;
13
use function is_resource;
14
use function spl_object_id;
15

16
/**
17
 * A connection pool that reuses idle TCP connections.
18
 *
19
 * When a connection is checked in, it becomes available for reuse. An idle timer
20
 * is started; if the connection is not checked out before the timer fires, it is
21
 * closed and removed from the pool.
22
 *
23
 * Usage:
24
 *   $pool = new SocketPool();
25
 *   $stream = $pool->checkout('example.com', 80);
26
 *   // ... use stream ...
27
 *   $pool->checkin($stream);  // return for reuse
28
 *   $stream2 = $pool->checkout('example.com', 80); // reuses the same connection
29
 */
30
final class SocketPool implements SocketPoolInterface
31
{
32
    /**
33
     * @var array<string, list<array{StreamInterface, string}>>
34
     *     Map of "host:port" => list of [stream, idle_timer_watcher_id]
35
     */
36
    private array $idle = [];
37

38
    /**
39
     * @var array<int, string>
40
     *     Map of spl_object_id => "host:port" key for checked-out streams
41
     */
42
    private array $checkedOut = [];
43

44
    private readonly Duration $idleTimeout;
45

46
    public function __construct(
47
        private readonly ConnectorInterface $connector = new Connector(),
48
        null|Duration $idleTimeout = null,
49
    ) {
50
        $this->idleTimeout = $idleTimeout ?? Duration::seconds(10);
6✔
51
    }
52

53
    public function checkout(string $host, int $port, null|Duration $timeout = null): StreamInterface
54
    {
55
        $key = "{$host}:{$port}";
5✔
56

57
        // Try to reuse an idle connection
58
        while (isset($this->idle[$key]) && $this->idle[$key] !== []) {
5✔
59
            $entry = array_pop($this->idle[$key]);
1✔
60
            [$stream, $timerId] = $entry;
1✔
61
            EventLoop::cancel($timerId);
1✔
62

63
            if ($this->idle[$key] === []) {
1✔
64
                unset($this->idle[$key]);
1✔
65
            }
66

67
            // Verify connection is still valid
68
            $resource = $stream->getStream();
1✔
69
            if ($resource !== null && is_resource($resource)) {
1✔
70
                $this->checkedOut[spl_object_id($stream)] = $key;
1✔
71
                return $stream;
1✔
72
            }
73

74
            // Connection is dead, close and try next
NEW
75
            $stream->close();
×
76
        }
77

78
        // No idle connection available, create a new one
79
        $stream = $this->connector->connect($host, $port, $timeout);
5✔
80
        $this->checkedOut[spl_object_id($stream)] = $key;
5✔
81

82
        return $stream;
5✔
83
    }
84

85
    public function checkin(StreamInterface $stream): void
86
    {
87
        $id = spl_object_id($stream);
3✔
88
        $key = $this->checkedOut[$id] ?? null;
3✔
89
        if ($key === null) {
3✔
90
            return;
1✔
91
        }
92

93
        unset($this->checkedOut[$id]);
2✔
94

95
        // Verify connection is still valid before pooling
96
        $resource = $stream->getStream();
2✔
97
        if ($resource === null || !is_resource($resource)) {
2✔
NEW
98
            $stream->close();
×
NEW
99
            return;
×
100
        }
101

102
        // Start idle timer
103
        $timerId = EventLoop::delay($this->idleTimeout->getTotalSeconds(), function () use ($stream, $key): void {
2✔
NEW
104
            $this->removeFromIdle($stream, $key);
×
NEW
105
            $stream->close();
×
106
        });
2✔
107

108
        $this->idle[$key] ??= [];
2✔
109
        $this->idle[$key][] = [$stream, $timerId];
2✔
110
    }
111

112
    public function clear(StreamInterface $stream): void
113
    {
114
        $id = spl_object_id($stream);
2✔
115

116
        // Remove from checked-out tracking
117
        $key = $this->checkedOut[$id] ?? null;
2✔
118
        if ($key !== null) {
2✔
119
            unset($this->checkedOut[$id]);
2✔
120
        }
121

122
        // Remove from idle pool
123
        if ($key !== null) {
2✔
124
            $this->removeFromIdle($stream, $key);
2✔
125
        }
126

127
        $stream->close();
2✔
128
    }
129

130
    public function close(): void
131
    {
132
        foreach ($this->idle as $entries) {
6✔
133
            foreach ($entries as [$stream, $timerId]) {
1✔
134
                EventLoop::cancel($timerId);
1✔
135
                $stream->close();
1✔
136
            }
137
        }
138

139
        $this->idle = [];
6✔
140
    }
141

142
    private function removeFromIdle(StreamInterface $stream, string $key): void
143
    {
144
        if (!isset($this->idle[$key])) {
2✔
145
            return;
2✔
146
        }
147

NEW
148
        $targetId = spl_object_id($stream);
×
NEW
149
        $this->idle[$key] = array_values(array_filter($this->idle[$key], static function (array $entry) use (
×
NEW
150
            $targetId,
×
NEW
151
        ): bool {
×
NEW
152
            [$s, $timerId] = $entry;
×
NEW
153
            if (spl_object_id($s) === $targetId) {
×
NEW
154
                EventLoop::cancel($timerId);
×
NEW
155
                return false;
×
156
            }
157

NEW
158
            return true;
×
NEW
159
        }));
×
160

NEW
161
        if ($this->idle[$key] === []) {
×
NEW
162
            unset($this->idle[$key]);
×
163
        }
164
    }
165
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc