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

azjezz / psl / 23094245055

14 Mar 2026 06:56PM UTC coverage: 98.499% (-0.06%) from 98.558%
23094245055

push

github

web-flow
bc(io): `Psl\IO\CloseHandleInterface` now requires an `isClosed(): bool` method. (#624)

2 of 6 new or added lines in 5 files covered. (33.33%)

2 existing lines in 1 file now uncovered.

9775 of 9924 relevant lines covered (98.5%)

35.06 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace Psl\TCP;
6

7
use Psl\Async\CancellationTokenInterface;
8
use Psl\Async\NullCancellationToken;
9
use Psl\DateTime\Duration;
10
use Revolt\EventLoop;
11

12
use function array_filter;
13
use function array_pop;
14
use function array_values;
15
use function is_resource;
16
use function spl_object_id;
17

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

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

46
    private readonly Duration $idleTimeout;
47

48
    private bool $closed = false;
49

50
    public function __construct(
51
        private readonly ConnectorInterface $connector = new Connector(),
52
        null|Duration $idleTimeout = null,
53
    ) {
54
        $this->idleTimeout = $idleTimeout ?? Duration::seconds(10);
11✔
55
    }
56

57
    public function checkout(
58
        string $host,
59
        int $port,
60
        CancellationTokenInterface $cancellation = new NullCancellationToken(),
61
    ): StreamInterface {
62
        $key = "{$host}:{$port}";
10✔
63

64
        // Try to reuse an idle connection
65
        while (isset($this->idle[$key]) && $this->idle[$key] !== []) {
10✔
66
            $entry = array_pop($this->idle[$key]);
3✔
67
            [$stream, $timerId] = $entry;
3✔
68
            EventLoop::cancel($timerId);
3✔
69

70
            if ($this->idle[$key] === []) {
3✔
71
                unset($this->idle[$key]);
3✔
72
            }
73

74
            // Verify connection is still valid
75
            $resource = $stream->getStream();
3✔
76
            if ($resource !== null && is_resource($resource)) {
3✔
77
                $this->checkedOut[spl_object_id($stream)] = $key;
1✔
78
                return $stream;
1✔
79
            }
80

81
            // Connection is dead, close and try next
82
            $stream->close();
2✔
83
        }
84

85
        // No idle connection available, create a new one
86
        $stream = $this->connector->connect($host, $port, $cancellation);
10✔
87
        $this->checkedOut[spl_object_id($stream)] = $key;
10✔
88

89
        return $stream;
10✔
90
    }
91

92
    public function checkin(StreamInterface $stream): void
93
    {
94
        $id = spl_object_id($stream);
7✔
95
        $key = $this->checkedOut[$id] ?? null;
7✔
96
        if ($key === null) {
7✔
97
            return;
1✔
98
        }
99

100
        unset($this->checkedOut[$id]);
6✔
101

102
        // Verify connection is still valid before pooling
103
        $resource = $stream->getStream();
6✔
104
        if ($resource === null || !is_resource($resource)) {
6✔
105
            $stream->close();
1✔
106
            return;
1✔
107
        }
108

109
        // Start idle timer
110
        $timerId = EventLoop::delay($this->idleTimeout->getTotalSeconds(), function () use ($stream, $key): void {
5✔
111
            $this->removeFromIdle($stream, $key);
1✔
112
            $stream->close();
1✔
113
        });
5✔
114

115
        $this->idle[$key] ??= [];
5✔
116
        $this->idle[$key][] = [$stream, $timerId];
5✔
117
    }
118

119
    public function clear(StreamInterface $stream): void
120
    {
121
        $id = spl_object_id($stream);
4✔
122

123
        // Remove from checked-out tracking
124
        $key = $this->checkedOut[$id] ?? null;
4✔
125
        if ($key !== null) {
4✔
126
            unset($this->checkedOut[$id]);
3✔
127
        }
128

129
        // Remove from idle pool
130
        if ($key !== null) {
4✔
131
            $this->removeFromIdle($stream, $key);
3✔
132
        }
133

134
        $stream->close();
4✔
135
    }
136

137
    public function isClosed(): bool
138
    {
NEW
139
        return $this->closed;
×
140
    }
141

142
    public function close(): void
143
    {
144
        $this->closed = true;
11✔
145

146
        foreach ($this->idle as $entries) {
11✔
147
            foreach ($entries as [$stream, $timerId]) {
1✔
148
                EventLoop::cancel($timerId);
1✔
149
                $stream->close();
1✔
150
            }
151
        }
152

153
        $this->idle = [];
11✔
154
    }
155

156
    private function removeFromIdle(StreamInterface $stream, string $key): void
157
    {
158
        if (!isset($this->idle[$key])) {
4✔
159
            return;
3✔
160
        }
161

162
        $targetId = spl_object_id($stream);
1✔
163
        $this->idle[$key] = array_values(array_filter($this->idle[$key], static function (array $entry) use (
1✔
164
            $targetId,
1✔
165
        ): bool {
1✔
166
            [$s, $timerId] = $entry;
1✔
167
            if (spl_object_id($s) === $targetId) {
1✔
168
                EventLoop::cancel($timerId);
1✔
169
                return false;
1✔
170
            }
171

172
            return true;
×
173
        }));
1✔
174

175
        if ($this->idle[$key] === []) {
1✔
176
            unset($this->idle[$key]);
1✔
177
        }
178
    }
179
}
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