Hone logo
Hone
Problems

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 struct representing a connection and a sync.Mutex to 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 NewConnectionPool function must accept an integer capacity which represents the maximum number of connections.
  • capacity will be a non-negative integer.
  • The Acquire method should block indefinitely if no connections are available until one is released.
  • The Release method 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 struct with 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.
Loading editor...
go