Go Connection Pool Implementation
Databases are a critical part of many applications. Establishing a new database connection can be a time-consuming and resource-intensive operation. Connection pooling is a common technique to mitigate this overhead by maintaining a set of open database connections that can be reused by the application. This challenge asks you to implement a basic connection pool in Go.
Problem Description
You need to create a ConnectionPool struct in Go that manages a pool of database connections. The pool should be able to:
- Initialize: Create a specified number of initial connections when the pool is created.
- Acquire: Provide an existing, available connection to a client requesting one. If no connections are available, the client should wait until one becomes free.
- Release: Return a connection back to the pool, making it available for other clients.
- Close: Gracefully shut down all connections in the pool.
Key Requirements:
- The
ConnectionPoolshould be thread-safe, allowing multiple goroutines to acquire and release connections concurrently. - The pool should have a configurable maximum number of connections.
- When acquiring a connection, if all connections are in use, the requesting goroutine should block until a connection is released.
- The pool should handle the scenario where a connection might be returned in an invalid state (though for this simplified challenge, we'll assume connections are always valid upon release unless explicitly closed).
Expected Behavior:
- A client calls
Acquire()to get a connection. - If an idle connection is available, it's returned immediately.
- If no idle connection is available, the client's goroutine blocks.
- When another client calls
Release()with a connection, that connection becomes available. - If a blocked goroutine is waiting, it receives the released connection.
- The
Close()method should close all underlying connections and prevent furtherAcquireorReleaseoperations.
Edge Cases:
- Acquiring a connection when the pool is full and all connections are in use.
- Releasing a connection when the pool is already at its maximum capacity (this should be handled gracefully, perhaps by discarding the connection or logging a warning).
- Closing an already closed pool.
Examples
Example 1: Basic Acquisition and Release
// Imagine a mock connection type with a simple ID and a Close method
type MockConnection struct {
ID int
// ... other fields for a real connection
}
func (mc *MockConnection) Close() error {
fmt.Printf("Closing connection %d\n", mc.ID)
return nil
}
// Mock database driver
type MockDriver struct{}
func (md *MockDriver) Connect() (*MockConnection, error) {
// Simulate connection creation delay
time.Sleep(10 * time.Millisecond)
// Return a new mock connection
return &MockConnection{ID: rand.Intn(1000)}, nil
}
// --- In main or a test function ---
// Create a pool with max 3 connections, starting with 2
pool, err := NewConnectionPool(&MockDriver{}, 3, 2)
if err != nil { /* handle error */ }
defer pool.Close()
// Acquire a connection
conn1, err := pool.Acquire()
if err != nil { /* handle error */ }
fmt.Printf("Acquired connection: %+v\n", conn1.(*MockConnection).ID)
// Release the connection
pool.Release(conn1)
fmt.Println("Released connection.")
// Acquire another connection (might get the same one if available)
conn2, err := pool.Acquire()
if err != nil { /* handle error */ }
fmt.Printf("Acquired connection: %+v\n", conn2.(*MockConnection).ID)
pool.Release(conn2)
fmt.Println("Released connection.")
Expected Output (connection IDs will vary):
Acquired connection: 123
Released connection.
Acquired connection: 123
Released connection.
Example 2: Blocking on Acquisition
// Using MockConnection and MockDriver from Example 1
// Create a pool with max 2 connections, starting with 2
pool, err := NewConnectionPool(&MockDriver{}, 2, 2)
if err != nil { /* handle error */ }
defer pool.Close()
// Acquire all connections
conn1, _ := pool.Acquire()
fmt.Printf("Goroutine 1 acquired connection %d\n", conn1.(*MockConnection).ID)
conn2, _ := pool.Acquire()
fmt.Printf("Goroutine 1 acquired connection %d\n", conn2.(*MockConnection).ID)
// Try to acquire a third connection - this should block
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Goroutine 2 trying to acquire connection...")
conn3, err := pool.Acquire()
if err != nil { /* handle error */ }
fmt.Printf("Goroutine 2 acquired connection %d\n", conn3.(*MockConnection).ID)
pool.Release(conn3)
fmt.Println("Goroutine 2 released connection.")
}()
// Give Goroutine 2 a moment to start and block
time.Sleep(50 * time.Millisecond)
fmt.Println("Goroutine 1 releasing conn1...")
pool.Release(conn1)
fmt.Println("Goroutine 1 released conn1.")
wg.Wait() // Wait for Goroutine 2 to finish
fmt.Println("All operations completed.")
Expected Output (connection IDs will vary):
Goroutine 1 acquired connection 456
Goroutine 1 acquired connection 789
Goroutine 2 trying to acquire connection...
Goroutine 1 releasing conn1...
Goroutine 1 released conn1.
Goroutine 2 acquired connection 456
Goroutine 2 released connection.
All operations completed.
Example 3: Closing the Pool
// Using MockConnection and MockDriver from Example 1
// Create a pool with max 2 connections, starting with 2
pool, err := NewConnectionPool(&MockDriver{}, 2, 2)
if err != nil { /* handle error */ }
// Acquire a connection
conn, _ := pool.Acquire()
fmt.Printf("Acquired connection %d\n", conn.(*MockConnection).ID)
// Close the pool
pool.Close()
fmt.Println("Pool closed.")
// Try to acquire again (should return an error)
_, err = pool.Acquire()
if err != nil {
fmt.Printf("Error acquiring after close: %v\n", err)
}
// Release a connection after pool close (should be handled gracefully)
pool.Release(conn)
fmt.Println("Attempted to release after close.")
Expected Output (connection IDs will vary):
Acquired connection 101
Closing connection 101
Pool closed.
Error acquiring after close: connection pool is closed
Attempted to release after close.
Constraints
- The
ConnectionPoolshould be initialized with amaxConnectionsinteger greater than 0. - The
initialConnectionsinteger should be less than or equal tomaxConnectionsand greater than or equal to 0. - The
Driverinterface will have aConnect()method that returns a genericinterface{}representing a connection and anerror. The pool should not inspect the connection type directly, but rather store and return it as aninterface{}. - The underlying
Connect()operation might take a small but non-zero amount of time. - Concurrency: The pool must safely handle at least 100 concurrent
AcquireandReleaseoperations without data races or deadlocks.
Notes
- You'll need to define an interface for your database driver that includes a
Connect()method. - Consider using Go's built-in concurrency primitives like
sync.Mutex,sync.WaitGroup, and channels for managing the pool's state and goroutine synchronization. - Think about how to represent available connections and connections currently in use.
- The
Close()method should ensure all created connections are properly closed. - For this challenge, you don't need to implement a real database driver. You can use a mock driver as shown in the examples.