Hone logo
Hone
Problems

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 ConnectionPool should 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:

  1. A client calls Acquire() to get a connection.
  2. If an idle connection is available, it's returned immediately.
  3. If no idle connection is available, the client's goroutine blocks.
  4. When another client calls Release() with a connection, that connection becomes available.
  5. If a blocked goroutine is waiting, it receives the released connection.
  6. The Close() method should close all underlying connections and prevent further Acquire or Release operations.

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 ConnectionPool should be initialized with a maxConnections integer greater than 0.
  • The initialConnections integer should be less than or equal to maxConnections and greater than or equal to 0.
  • The Driver interface will have a Connect() method that returns a generic interface{} representing a connection and an error. The pool should not inspect the connection type directly, but rather store and return it as an interface{}.
  • The underlying Connect() operation might take a small but non-zero amount of time.
  • Concurrency: The pool must safely handle at least 100 concurrent Acquire and Release operations 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.
Loading editor...
go