Go Database Connection Pool Implementation
Modern applications often interact with databases, and inefficiently managing database connections can lead to performance bottlenecks. A database connection pool is a crucial technique to address this by maintaining a set of open database connections that can be reused by applications, reducing the overhead of establishing new connections for each database operation.
Problem Description
Your task is to implement a generic database connection pool in Go. This pool should manage a set of connections to a hypothetical "database" (which you can simulate using a simple structure or channel). The pool should support the following key functionalities:
- Acquiring a connection: When a client needs to interact with the database, it should request a connection from the pool. If a connection is available, it should be returned immediately. If all connections are in use, the client should wait until a connection becomes available.
- Releasing a connection: Once a client has finished its database operation, it should return the connection to the pool, making it available for other clients.
- Pool capacity: The pool should have a configurable maximum number of connections.
- Connection simulation: You will need to simulate database connections. For this challenge, a simple
structrepresenting a connection and async.Mutexto protect its state will suffice. You don't need to implement actual database operations. - Concurrency safety: The pool must be thread-safe, meaning it can be accessed concurrently by multiple goroutines without data corruption or race conditions.
Examples
Example 1: Basic Acquisition and Release
// Assume a pool with capacity 2 is created
pool := NewConnectionPool(2)
// Goroutine 1 acquires a connection
conn1 := pool.Acquire()
fmt.Println("Goroutine 1 acquired connection")
// Goroutine 2 acquires a connection
conn2 := pool.Acquire()
fmt.Println("Goroutine 2 acquired connection")
// Goroutine 1 releases its connection
pool.Release(conn1)
fmt.Println("Goroutine 1 released connection")
// Goroutine 2 releases its connection
pool.Release(conn2)
fmt.Println("Goroutine 2 released connection")
Expected Output:
Goroutine 1 acquired connection
Goroutine 2 acquired connection
Goroutine 1 released connection
Goroutine 2 released connection
Example 2: Waiting for Connection Availability
// Assume a pool with capacity 1 is created
pool := NewConnectionPool(1)
// Goroutine 1 acquires the only connection
conn1 := pool.Acquire()
fmt.Println("Goroutine 1 acquired connection")
// Goroutine 2 attempts to acquire a connection, it should block
go func() {
conn2 := pool.Acquire()
fmt.Println("Goroutine 2 acquired connection")
pool.Release(conn2)
fmt.Println("Goroutine 2 released connection")
}()
// Give Goroutine 2 some time to start and block
time.Sleep(100 * time.Millisecond)
fmt.Println("Goroutine 1 releasing connection...")
pool.Release(conn1)
fmt.Println("Goroutine 1 released connection")
Expected Output:
Goroutine 1 acquired connection
Goroutine 1 releasing connection...
Goroutine 1 released connection
Goroutine 2 acquired connection
Goroutine 2 released connection
Example 3: Pool Exhaustion Scenario
// Assume a pool with capacity 0 is created (this might be an edge case to consider how to handle)
// For a typical pool, capacity should be > 0. Let's assume capacity 1 for demonstration.
pool := NewConnectionPool(1)
// Goroutine 1 acquires the connection
conn1 := pool.Acquire()
fmt.Println("Goroutine 1 acquired connection")
// Goroutine 2 attempts to acquire, and will block indefinitely if pool is exhausted
go func() {
fmt.Println("Goroutine 2 attempting to acquire...")
conn2 := pool.Acquire() // This will block
fmt.Println("Goroutine 2 acquired connection (should not happen in this scenario)")
pool.Release(conn2)
}()
// Goroutine 1 holds the connection for a short while
time.Sleep(200 * time.Millisecond)
fmt.Println("Goroutine 1 releasing connection...")
pool.Release(conn1)
fmt.Println("Goroutine 1 released connection")
// Give Goroutine 2 a chance to run IF a connection became available
time.Sleep(100 * time.Millisecond)
Expected Output:
Goroutine 1 acquired connection
Goroutine 2 attempting to acquire...
Goroutine 1 releasing connection...
Goroutine 1 released connection
// Goroutine 2 will acquire and release here if the sleep durations are right,
// but the key is that it WAITS. For this example, it's designed to show waiting.
// If Goroutine 2 successfully acquires, the output would continue with:
// Goroutine 2 acquired connection
// Goroutine 2 released connection
Constraints
- The
NewConnectionPoolfunction must accept an integercapacitywhich represents the maximum number of connections. capacitywill be a non-negative integer.- The
Acquiremethod should block indefinitely if no connections are available until one is released. - The
Releasemethod should safely add a connection back to the pool. - Implementations must be safe for concurrent access from multiple goroutines.
Notes
- You will need to define what a "connection" is. A simple
structwith an identifier might be sufficient for simulation. - Consider using Go's built-in concurrency primitives like
sync.Mutex,sync.Cond, or channels to manage the pool's state and synchronization. - Think about how to represent the available connections and the connections currently in use.
- The problem statement focuses on the management of connections, not the actual database interaction. You can simulate connection creation/destruction if you wish, but it's not strictly required.
- For this exercise, you don't need to implement connection validation or automatic reconnection if a connection breaks.