Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 28, 2025

  • Set up development environment and run existing tests
  • Create connection pooling architecture
    • Create PoolConfiguration class for pool settings
    • Create Connection class for individual connections
    • Create HostConnectionPool class for per-host pooling
    • Create ConnectionPool class for managing all pools
  • Create HTTP/2 support components
    • Create Http2Configuration class with separate curl/curl_multi options
    • Update Guzzle client configuration for HTTP/2
  • Create DNS caching
    • Create DnsCache class
  • Integrate connection pooling with existing ClientHandler
    • Create ManagesConnectionPool trait
    • Update ClientHandler to use new trait
    • Update ClientHandler interface with new methods
  • Add tests for new functionality
    • Add ConnectionPoolTest.php with 23 tests
    • Add ManagesConnectionPoolTest.php with 12 tests
  • Run code review and address feedback
    • Document static properties as shared global state
    • Always initialize DNS cache when pool is configured
    • Add PHPDoc explaining resolveHostname() null returns
    • Fix hostname encoding in NetworkException URL
    • Remove unconditional HTTP/2 from HostConnectionPool
    • Extract TCP keep-alive interval to constant
  • Run security scan (CodeQL)
  • All 360 tests pass with 1065 assertions
  • Fix code style issues (class element ordering, namespace imports)
Original prompt

This section details on the original issue you should resolve

<issue_title>Connection Pooling & HTTP/2 Support</issue_title>
<issue_description>## Summary
Add intelligent connection pooling and HTTP/2 support to dramatically improve performance for applications making multiple requests to the same hosts.

Motivation

Connection pooling and HTTP/2 support provide significant performance benefits:

  • Connection Reuse: Eliminate TCP handshake overhead (up to 100ms per connection)
  • HTTP/2 Multiplexing: Send multiple requests simultaneously over a single connection
  • Reduced Latency: Faster subsequent requests to the same host
  • Resource Efficiency: Lower memory and CPU usage
  • Server Push: Receive resources before requesting them (HTTP/2)
  • Header Compression: HPACK compression reduces overhead
  • Stream Prioritization: Critical requests get priority
  • Better Mobile Performance: Crucial for high-latency mobile connections

Currently, each Fetch PHP request creates a new connection, missing these optimization opportunities.

Proposed API

// Basic connection pooling
$client = fetch_client([
    'connection_pool' => [
        'enabled' => true,
        'max_connections' => 100,
        'max_per_host' => 6,
        'keep_alive_timeout' => 30,
        'connection_timeout' => 10,
    ]
]);

// HTTP/2 configuration
$client = fetch_client([
    'http2' => [
        'enabled' => true,
        'max_concurrent_streams' => 100,
        'window_size' => 65535,
        'header_table_size' => 4096,
        'enable_server_push' => true,
    ]
]);

// Advanced pooling configuration
$client = fetch_client([
    'connection_pool' => [
        'strategy' => 'least_connections', // round_robin, least_connections, random
        'health_check' => [
            'enabled' => true,
            'interval' => 30,
            'timeout' => 5,
            'endpoint' => '/health',
        ],
        'retry_failed_connections' => true,
        'connection_warmup' => true,
        'dns_cache_ttl' => 300,
    ]
]);

// Per-request connection options
$response = fetch('/api/data', [
    'connection' => [
        'reuse' => true,
        'priority' => 'high',
        'stream_weight' => 256,
    ]
]);

Implementation Details

Connection Pool Architecture

interface ConnectionPoolInterface
{
    public function getConnection(string $host, int $port, bool $ssl = false): ConnectionInterface;
    public function releaseConnection(ConnectionInterface $connection): void;
    public function closeConnection(ConnectionInterface $connection): void;
    public function getStats(): array;
    public function healthCheck(): array;
}

class ConnectionPool implements ConnectionPoolInterface
{
    private array $pools = []; // Pools by host:port
    private array $activeConnections = [];
    private ConnectionFactory $factory;
    private PoolConfiguration $config;
    
    public function __construct(
        ConnectionFactory $factory,
        PoolConfiguration $config
    ) {
        $this->factory = $factory;
        $this->config = $config;
    }
    
    public function getConnection(string $host, int $port, bool $ssl = false): ConnectionInterface
    {
        $key = $this->getPoolKey($host, $port, $ssl);
        
        if (!isset($this->pools[$key])) {
            $this->pools[$key] = new HostConnectionPool($host, $port, $ssl, $this->config);
        }
        
        $connection = $this->pools[$key]->borrowConnection();
        $this->activeConnections[spl_object_id($connection)] = $connection;
        
        return $connection;
    }
    
    public function releaseConnection(ConnectionInterface $connection): void
    {
        $id = spl_object_id($connection);
        
        if (isset($this->activeConnections[$id])) {
            unset($this->activeConnections[$id]);
            
            if ($connection->isReusable()) {
                $pool = $this->getPoolForConnection($connection);
                $pool->returnConnection($connection);
            } else {
                $connection->close();
            }
        }
    }
    
    public function getStats(): array
    {
        $stats = [
            'total_pools' => count($this->pools),
            'active_connections' => count($this->activeConnections),
            'pools' => [],
        ];
        
        foreach ($this->pools as $key => $pool) {
            $stats['pools'][$key] = $pool->getStats();
        }
        
        return $stats;
    }
}

class HostConnectionPool
{
    private \SplQueue $availableConnections;
    private array $connectionStats = [];
    private int $totalCreated = 0;
    private int $totalBorrowed = 0;
    private int $totalReturned = 0;
    
    public function __construct(
        private string $host,
        private int $port,
        private bool $ssl,
        private PoolConfiguration $config
    ) {
        $this->availableConnections = new \SplQueue();
        $this->warmup...

</details>

- Fixes Thavarshan/fetch-php#42

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

@netlify
Copy link

netlify bot commented Nov 28, 2025

Deploy Preview for fetch-php canceled.

Name Link
🔨 Latest commit 9c69a96
🔍 Latest deploy log https://app.netlify.com/projects/fetch-php/deploys/692af81a89ee03000899fd1c

@github-actions
Copy link
Contributor

github-actions bot commented Nov 28, 2025

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI and others added 3 commits November 28, 2025 17:32
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
…tion, remove error_log

Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Copilot AI changed the title [WIP] Add connection pooling and HTTP/2 support for performance enhancement Add Connection Pooling & HTTP/2 Support Nov 28, 2025
Copilot AI requested a review from Thavarshan November 28, 2025 17:41
@Thavarshan Thavarshan requested review from Copilot and removed request for Thavarshan November 28, 2025 17:43
@Thavarshan
Copy link
Owner

@copilot please fix all code style issues to make CI pass

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds connection pooling and HTTP/2 support to improve performance for applications making multiple requests. The implementation introduces a new Fetch\Pool namespace with six classes managing connection lifecycle, DNS caching, and HTTP/2 configuration. The feature is opt-in and integrates with the existing ClientHandler through a new trait.

  • Adds connection pooling to eliminate TCP handshake overhead and enable connection reuse
  • Implements HTTP/2 support with multiplexing and header compression
  • Includes DNS caching with configurable TTL to reduce lookup overhead

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/Fetch/Pool/PoolConfiguration.php Configuration class for connection pool settings with defaults and array conversion
src/Fetch/Pool/Http2Configuration.php HTTP/2 settings including concurrent streams, window size, and curl options
src/Fetch/Pool/Connection.php Individual connection lifecycle management with reusability checks
src/Fetch/Pool/HostConnectionPool.php Per-host connection pool with borrow/return semantics and warmup support
src/Fetch/Pool/ConnectionPool.php Multi-host pool manager with metrics tracking
src/Fetch/Pool/DnsCache.php DNS resolution caching with TTL and automatic pruning
src/Fetch/Concerns/ManagesConnectionPool.php Trait providing pool management methods to ClientHandler
src/Fetch/Interfaces/ClientHandler.php Interface extension with pool management method signatures
src/Fetch/Http/ClientHandler.php Integration of ManagesConnectionPool trait
tests/Unit/ManagesConnectionPoolTest.php Tests for pool management trait integration
tests/Unit/ConnectionPoolTest.php Comprehensive tests for pool classes
tests/Unit/ManagesDebugAndProfilingTest.php Removed unused import

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 180 to 182
throw new NetworkException(
"Failed to resolve hostname: {$hostname}",
new \GuzzleHttp\Psr7\Request('GET', "https://{$hostname}/")
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NetworkException constructor is being passed a Request object with the hostname in the URL. This may fail if the hostname contains special characters that are invalid in URLs. Consider using a more generic approach or validating/encoding the hostname before constructing the Request, or check if NetworkException actually requires a valid Request object.

Suggested change
throw new NetworkException(
"Failed to resolve hostname: {$hostname}",
new \GuzzleHttp\Psr7\Request('GET', "https://{$hostname}/")
// Encode hostname for use in URL (IDN and special chars)
$asciiHost = function_exists('idn_to_ascii') ? idn_to_ascii($hostname, 0, INTL_IDNA_VARIANT_UTS46) : $hostname;
$safeHost = rawurlencode($asciiHost);
throw new NetworkException(
"Failed to resolve hostname: {$hostname}",
new \GuzzleHttp\Psr7\Request('GET', "https://{$safeHost}/")

Copilot uses AI. Check for mistakes.
Comment on lines 145 to 146
// Enable HTTP/2 if available
'version' => 2.0,
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP/2 is being unconditionally enabled in the HostConnectionPool's HTTP client creation, but there's no check if HTTP/2 is actually configured or should be used. This could force HTTP/2 even when the user hasn't enabled it via the Http2Configuration. Consider checking if HTTP/2 is enabled before setting this option.

Copilot uses AI. Check for mistakes.
/**
* The connection pool instance.
*/
protected static ?ConnectionPool $connectionPool = null;
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using static properties for connection pool and DNS cache creates a shared state across all ClientHandler instances. This could lead to race conditions in multi-threaded environments (e.g., when using parallel processing or async operations) and makes it difficult to isolate connections per handler. Consider making these instance-level properties or adding proper synchronization mechanisms.

Copilot uses AI. Check for mistakes.
/**
* The DNS cache instance.
*/
protected static ?DnsCache $dnsCache = null;
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the connection pool, the static DNS cache creates shared state across all ClientHandler instances which could lead to race conditions and unexpected behavior when multiple handlers are used concurrently. Consider making this an instance property instead.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +86
$existingCurl = $this->options['curl'] ?? [];
// Use + operator to preserve integer keys (CURL constants)
// and give priority to existing options over defaults
$this->options['curl'] = $existingCurl + $curlOptions;
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states 'give priority to existing options over defaults', but with the + operator, if the same key exists in both arrays, the value from the left operand ($existingCurl) is kept and the right operand ($curlOptions) is ignored. This means HTTP/2 options from $curlOptions won't override existing curl options. If this is intentional, the comment is correct, but if HTTP/2 options should take precedence, use array_merge() or reverse the operands.

Copilot uses AI. Check for mistakes.
Comment on lines 148 to 153
'curl' => [
// Enable connection reuse
CURLOPT_TCP_KEEPALIVE => 1,
CURLOPT_TCP_KEEPIDLE => $this->config->getKeepAliveTimeout(),
CURLOPT_TCP_KEEPINTVL => 10,
],
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment 'Enable connection reuse' is somewhat misleading. These CURLOPT_TCP_KEEPALIVE options configure TCP keep-alive packets to detect dead connections, not connection reuse per se. Connection reuse is handled at a higher level by the pool. Consider clarifying the comment to 'Configure TCP keep-alive for connection health monitoring' or similar.

Copilot uses AI. Check for mistakes.
Comment on lines +250 to +261
protected function resolveHostname(string $hostname): ?string
{
if (self::$dnsCache === null) {
return null;
}

try {
return self::$dnsCache->resolveFirst($hostname);
} catch (\Throwable) {
return null;
}
}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The resolveHostname method silently catches and ignores all exceptions, returning null. Consider adding a PHPDoc comment explaining when null is returned (no DNS cache or DNS resolution failure) to help consumers of this method understand the behavior.

Copilot uses AI. Check for mistakes.
…ering

Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Nov 28, 2025

@copilot please fix all code style issues to make CI pass

Fixed in 4c03857. The issues were:

  1. Using \GuzzleHttp\Psr7\Request instead of proper use import in DnsCache.php
  2. Class element ordering in ManagesConnectionPool.php, DnsCache.php, and HostConnectionPool.php

All linting checks now pass (duster lint src).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 11 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +164 to +173
* Record connection latency for metrics.
*
* @param string $host The host
* @param int $port The port
* @param float $latency Latency in milliseconds
*/
public function recordLatency(string $host, int $port, float $latency): void
{
$this->metrics['total_latency'] += $latency;
}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recordLatency() method updates metrics but is never called anywhere in the codebase. For the connection pool metrics to be meaningful, this method should be integrated into the actual request/response flow to track latency.

Copilot uses AI. Check for mistakes.
/**
* Create a new connection.
*
* @return Connection The new connection
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number 10 used for TCP keep-alive interval without explanation. Consider extracting this to a named constant or configuration parameter for better maintainability.

Example:

public const DEFAULT_TCP_KEEPALIVE_INTERVAL = 10;

Copilot uses AI. Check for mistakes.
Thavarshan and others added 2 commits November 29, 2025 01:36
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Thavarshan and others added 3 commits November 29, 2025 01:37
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Thavarshan
Copy link
Owner

@copilot apply changes based on the comments in this thread

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Thavarshan and others added 3 commits November 29, 2025 01:39
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Nov 28, 2025

@copilot apply changes based on the comments in this thread

Applied changes from the review thread in commit 3d15f59:

  1. Static properties documentation: Added detailed PHPDoc explaining that $connectionPool and $dnsCache are shared across all handlers intentionally for connection reuse, with warnings about configuration changes affecting all handlers globally

  2. DNS cache initialization: Fixed to always initialize DNS cache when connection pool is configured, using the configuration's TTL (or default)

  3. Hostname encoding: Added rawurlencode() for hostnames in NetworkException URLs to handle IDN and special characters

  4. HTTP/2 version removed: Removed unconditional 'version' => 2.0 from HostConnectionPool - HTTP version is now controlled only via withHttp2() method

  5. TCP keep-alive constant: Extracted magic number 10 to DEFAULT_TCP_KEEPALIVE_INTERVAL constant with better documentation

  6. PHPDoc improvements: Enhanced resolveHostname() documentation explaining when null is returned

@Thavarshan Thavarshan marked this pull request as ready for review November 29, 2025 14:07
@Thavarshan Thavarshan merged commit 9c69a96 into main Nov 29, 2025
30 of 31 checks passed
@Thavarshan Thavarshan deleted the copilot/add-connection-pooling-http2-support branch November 29, 2025 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants