Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 105 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,125 +1,152 @@
# Netpool - Go TCP Connection Pool
# Netpool - Lock-Free Go TCP Connection Pool

## Description
[![Go Reference](https://pkg.go.dev/badge/github.com/yudhasubki/netpool.svg)](https://pkg.go.dev/github.com/yudhasubki/netpool)

Netpool is a lightweight and efficient TCP connection pool library for Golang. It provides a simple way to manage and reuse TCP connections, reducing the overhead of establishing new connections for each request and improving performance in high-concurrency scenarios.
## 🚀 Performance

Netpool is the Go connection pool. It uses a lock-free channel design with zero-allocation pass-by-value optimization.

| Library | ns/op | Throughput | Memory | Allocations | Features |
|---------|-------|-----------|--------|-------------|----------|
| **netpool (Basic)** | **42 ns** | **23.8M ops/sec** | **0 B** | **0 allocs** | Maximum Speed (No Idle/Health) |
| **netpool (Standard)**| **118 ns** | **8.4M ops/sec** | **0 B** | **0 allocs** | IdleTimeout, HealthCheck |
| fatih/pool | 124 ns | 8.0M ops/sec | 64 B | 1 alloc | No HealthCheck |
| silenceper/pool | 303 ns | 3.3M ops/sec | 48 B | 1 alloc | IdleTimeout, HealthCheck |

## Features
- TCP connection pooling for efficient connection reuse
- Configurable maximum connection limit per host
- Automatic connection reaping to remove idle connections
- Graceful handling of connection errors and reconnecting
- Customizable connection dialer for flexible connection establishment
Thread-safe operations for concurrent use

- **Lock-free** - Uses channels and atomics only
- **Zero allocation** - No memory allocations on Get/Put
- **Idle Timeout** - Automatically closes stale connections
- **Health Check** - Validates connections before use
- **Thread-safe** - Safe for concurrent use

## Installation
```

```bash
go get github.com/yudhasubki/netpool
```

## Quick Start
## Quick Start

### Standard Pool (Recommended)
Best balance of features and performance (118 ns/op).

### Basic Usage
```go
package main

import (
"log"
"net"
"time"

"github.com/yudhasubki/netpool"
)

func main() {
// Create a pool
pool, err := netpool.New(func() (net.Conn, error) {
return net.Dial("tcp", "localhost:6379")
},
netpool.WithMinPool(5),
netpool.WithMaxPool(20),
)
if err != nil {
log.Fatal(err)
}
defer pool.Close()

conn, err := pool.Get()
if err != nil {
log.Fatal(err)
}
defer conn.Close() // Automatically returns to pool

conn.Write([]byte("PING\r\n"))
return net.Dial("tcp", "localhost:6379")
}, netpool.Config{
MaxPool: 100,
MinPool: 10,
MaxIdleTime: 30 * time.Second, // Optional
HealthCheck: func(conn net.Conn) error { // Optional
return nil
},
})

conn, _ := pool.Get()
defer pool.Put(conn)
}
```

### With Idle Timeout
### Basic Pool (Maximum Performance)
For use cases needing absolute raw speed (~40 ns/op).
**Note:** Does NOT support `MaxIdleTime` or `HealthCheck`.

```go
pool, err := netpool.New(func() (net.Conn, error) {
// Use NewBasic() instead of New()
pool, err := netpool.NewBasic(func() (net.Conn, error) {
return net.Dial("tcp", "localhost:6379")
},
netpool.WithMinPool(2),
netpool.WithMaxPool(10),
netpool.WithMaxIdleTime(30*time.Second),
)
}, netpool.Config{
MaxPool: 100,
MinPool: 10,
})

conn, _ := pool.Get()
defer pool.Put(conn)
```

### Monitoring Pool Statistics
## API

### Creating a Pool

```go
stats := pool.Stats()
fmt.Printf("Active: %d, Idle: %d, InUse: %d\n",
stats.Active, stats.Idle, stats.InUse)
pool, err := netpool.New(factory, netpool.Config{
MaxPool: 100, // Maximum connections
MinPool: 10, // Minimum idle connections
DialTimeout: 5 * time.Second, // Connection creation timeout
MaxIdleTime: 30 * time.Second, // Close connections idle too long
HealthCheck: pingFunc, // Validate connection on Get()
})
```

## How it works
### Connection Lifecycle
- Creation: Connections are created using the provided factory function
- Dial Hooks: Optional hooks run for connection initialization
- Pool Storage: Idle connections are stored in a FIFO queue
- Health Check: Connections are validated before being returned (if configured)
- Idle Timeout: Unused connections are automatically cleaned up (if configured)
- Auto-Return: Connections automatically return to pool on Close()

### Auto-Return Connection Wrapper
Connections returned by Get() are automatically wrapped in a pooledConn that returns the connection to the pool when Close() is called. This eliminates the need for manual Put() calls:
### Getting a Connection

```go
// Old way (manual Put)
conn, _ := pool.Get()
defer pool.Put(conn, nil)
// Simple get (blocks if pool is full)
conn, err := pool.Get()

// New way (auto-return)
conn, _ := pool.Get()
defer conn.Close() // Automatically returns to pool
// Get with context (supports cancellation/timeout)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := pool.GetWithContext(ctx)
```

If a connection encounters an error and should not be reused, use MarkUnusable():
### Returning a Connection

```go
conn, _ := pool.Get()
// Return healthy connection
pool.Put(conn)

_, err := conn.Write(data)
if err != nil {
if pc, ok := conn.(interface{ MarkUnusable() error }); ok {
pc.MarkUnusable()
}
return err
}
// Return with error (connection will be closed)
pool.PutWithError(conn, err)
```

### Pool Statistics

```go
stats := pool.Stats()
fmt.Printf("Active: %d, Idle: %d, InUse: %d\n",
stats.Active, stats.Idle, stats.InUse)
```

## How It Works

Netpool uses a **Pass-by-Value** channel design for maximum efficiency:

conn.Close()
1. **Wait-Free Path**: `Get()` and `Put()` operations use Go channels with value copying.
2. **Zero Allocation**: Connection wrappers (`idleConn`) are passed by value (copying ~40 bytes), eliminating `sync.Pool` overhead and heap allocations.
3. **Atomic State**: Pool size is tracked with `atomic.Int32` for contention-free reads.

This design beats standard `sync.Pool` or mutex-based implementations by reducing memory pressure and CPU cycles.

## Running Benchmarks

```bash
# Run comparison with other libraries
go test -bench=BenchmarkComparison -benchmem ./...
```

### Thread Safety
netpool uses fine-grained locking to minimize contention:
## Credits

This project is inspired by the design and implementation of:

Pool operations are protected by a mutex
Condition variables handle blocking when pool is exhausted
Connection health checks run without holding the main lock
- [fatih/pool](https://github.com/fatih/pool)
- [silenceper/pool](https://github.com/silenceper/pool)

## Contributing
Contributions are welcome! If you find a bug or have a suggestion for improvement, please open an issue or submit a pull request. Make sure to follow the existing coding style and write tests for any new functionality.
We thank the authors for their contributions to the Go ecosystem.

## License
This project is licensed under the MIT License. See the LICENSE file for details.

## Acknowledgments
NetPool is inspired by various connection pool implementations in the Golang ecosystem. We would like to thank the authors of those projects for their contributions and ideas.
MIT License - see [LICENSE](LICENSE) file.
117 changes: 117 additions & 0 deletions benchmark_comparison_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package netpool

import (
"net"
"testing"

fatihpool "github.com/fatih/pool"
silencerpool "github.com/silenceper/pool"
)

func BenchmarkComparisonNetpool(b *testing.B) {
listener, addr := createTestServer(b)
defer listener.Close()

pool, _ := New(func() (net.Conn, error) {
return net.Dial("tcp", addr)
}, Config{
MaxPool: 100,
MinPool: 50,
})
defer pool.Close()

b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
conn, err := pool.Get()
if err != nil {
b.Fatal(err)
}
pool.Put(conn)
}
})
}

func BenchmarkComparisonFatihPool(b *testing.B) {
listener, addr := createTestServer(b)
defer listener.Close()

factory := func() (net.Conn, error) {
return net.Dial("tcp", addr)
}

pool, err := fatihpool.NewChannelPool(50, 100, factory)
if err != nil {
b.Fatalf("failed to create pool: %v", err)
}
defer pool.Close()

b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
conn, err := pool.Get()
if err != nil {
b.Fatal(err)
}
conn.Close()
}
})
}

func BenchmarkComparisonSilencerPool(b *testing.B) {
listener, addr := createTestServer(b)
defer listener.Close()

poolConfig := &silencerpool.Config{
InitialCap: 10,
MaxIdle: 50,
MaxCap: 100,
Factory: func() (interface{}, error) {
return net.Dial("tcp", addr)
},
Close: func(v interface{}) error {
return v.(net.Conn).Close()
},
}

pool, err := silencerpool.NewChannelPool(poolConfig)
if err != nil {
b.Fatalf("failed to create pool: %v", err)
}
defer pool.Release()

b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
conn, err := pool.Get()
if err != nil {
b.Fatal(err)
}
pool.Put(conn)
}
})
}

func BenchmarkComparisonBasicPool(b *testing.B) {
listener, addr := createTestServer(b)
defer listener.Close()

pool, _ := NewBasic(func() (net.Conn, error) {
return net.Dial("tcp", addr)
}, Config{
MaxPool: 100,
MinPool: 50,
})
defer pool.Close()

b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
conn, err := pool.Get()
if err != nil {
b.Fatal(err)
}
pool.Put(conn)
}
})
}
Loading
Loading