Skip to content

coenttb/swift-resource-pool

Repository files navigation

swift-resource-pool

Swift 5.9+ Platforms License Latest Release

CI Status Performance

A production-ready, actor-based resource pool for Swift
Thread-safe pooling with FIFO fairness, automatic resource management, and zero thundering herd

Overview

swift-resource-pool provides a generic, high-performance resource pooling solution built with Swift's actor model. It eliminates the thundering herd problem through direct resource handoff, ensures fairness with FIFO ordering, and offers comprehensive metrics for production monitoring.

import ResourcePool

// Define your poolable resource
actor DatabaseConnection: PoolableResource {
    struct Config: Sendable {
        let host: String
        let port: Int
    }
    
    static func create(config: Config) async throws -> DatabaseConnection {
        // Create and return connection
        return DatabaseConnection()
    }
    
    func validate() async -> Bool {
        // Check if connection is still alive
        return true
    }
    
    func reset() async throws {
        // Reset connection state for reuse
    }
}

// Create a pool with 10 connections
let pool = try await ResourcePool<DatabaseConnection>(
    capacity: 10,
    resourceConfig: .init(host: "localhost", port: 5432),
    warmup: true
)

// Use resources safely with automatic cleanup
let results = try await pool.withResource { connection in
    try await connection.query("SELECT * FROM users")
}

Why swift-resource-pool?

🎯 Zero Thundering Herd

  • Direct handoff: Resources go directly to exactly ONE waiting task
  • O(1) wakeup: No broadcast storms when resources become available
  • Proven efficiency: 90-95% handoff rate under sustained load
  • Scales to 200+ waiters: No performance degradation

⚖️ Fairness Guarantees

  • FIFO queue: Waiters served in arrival order
  • No starvation: Every task gets its turn
  • Predictable behavior: Consistent wait times across requests
  • Cache locality: LIFO resource selection for hot caches

🛡️ Production Ready

  • Cancellation safe: Guaranteed cleanup even on task cancellation
  • Actor isolated: Thread-safe without locks
  • Comprehensive metrics: Track utilization, timeouts, handoffs
  • Memory efficient: Bounded cache with automatic eviction
  • Battle tested: 45 test scenarios, 33+ consecutive successful runs

⚡ High Performance

  • Lazy creation: Resources created on-demand up to capacity
  • Optional warmup: Pre-create resources for instant availability
  • Validation & reset: Automatic resource cleanup between uses
  • Efficient timeouts: Per-waiter deadlines without polling

Quick Start

Installation

Add swift-resource-pool to your Swift package:

dependencies: [
    .package(url: "https://github.com/coenttb/swift-resource-pool", from: "0.1.0")
]

For Xcode projects, add the package URL: https://github.com/coenttb/swift-resource-pool

Your First Resource Pool

import ResourcePool

// Simple mock resource for demonstration
actor SimpleResource: PoolableResource {
    struct Config: Sendable {
        let name: String
    }
    
    let id = UUID()
    private var useCount = 0
    
    static func create(config: Config) async throws -> SimpleResource {
        return SimpleResource()
    }
    
    func validate() async -> Bool {
        return true
    }
    
    func reset() async throws {
        // Reset any state
    }
    
    func use() {
        useCount += 1
    }
}

// Create pool
let pool = try await ResourcePool<SimpleResource>(
    capacity: 5,
    resourceConfig: .init(name: "my-resource"),
    warmup: true
)

// Use resource with automatic cleanup
let result = try await pool.withResource(timeout: .seconds(10)) { resource in
    await resource.use()
    return "Success"
}

Core Concepts

🏗️ Poolable Resources

Resources must conform to PoolableResource protocol:

public protocol PoolableResource: Sendable, AnyObject {
    associatedtype Config: Sendable
    
    /// Create a new resource instance
    static func create(config: Config) async throws -> Self
    
    /// Check if the resource is still valid and can be reused
    func validate() async -> Bool
    
    /// Reset the resource to a clean state for reuse
    func reset() async throws
}

Important: Resources MUST be reference types (classes or actors) because the pool tracks them using ObjectIdentifier.

🔄 Resource Lifecycle

// Pool manages complete lifecycle:
// 1. Create (lazy or warmup)
let resource = try await factory.create()

// 2. Acquire (immediate or wait)
let resource = try await pool.withResource { resource in
    // 3. Use safely
    try await resource.performWork()
    // 4. Validate after use
    let isValid = await resource.validate()
    // 5. Reset for reuse
    try await resource.reset()
    // 6. Return to pool or discard
}

📊 Pool Statistics

Monitor pool behavior in real-time:

let stats = await pool.statistics
print("Available: \(stats.available)")
print("Leased: \(stats.leased)")
print("Utilization: \(stats.utilization * 100)%")
print("Queue depth: \(stats.waitQueueDepth)")
print("Backpressure: \(stats.hasBackpressure)")

let metrics = await pool.metrics
print("Total acquisitions: \(metrics.totalAcquisitions)")
print("Timeouts: \(metrics.timeouts)")
print("Handoff rate: \(metrics.handoffRate * 100)%")
print("Avg wait time: \(metrics.averageWaitTime?.formatted() ?? "N/A")")

Real-World Examples

🗄️ Database Connection Pool

import PostgresNIO

actor PostgresConnection: PoolableResource {
    struct Config: Sendable {
        let host: String
        let port: Int
        let database: String
        let user: String
        let password: String
    }
    
    private var connection: PostgresConnection?
    
    static func create(config: Config) async throws -> PostgresConnection {
        let conn = try await PostgresConnection.connect(
            host: config.host,
            port: config.port,
            username: config.user,
            password: config.password,
            database: config.database
        )
        return PostgresConnection(connection: conn)
    }
    
    func validate() async -> Bool {
        guard let connection else { return false }
        return !connection.isClosed
    }
    
    func reset() async throws {
        // Rollback any uncommitted transactions
        try await connection?.query("ROLLBACK")
    }
    
    func query<T>(_ sql: String) async throws -> [T] {
        // Execute query
    }
}

// Create pool
let dbPool = try await ResourcePool<PostgresConnection>(
    capacity: 20,
    resourceConfig: .init(
        host: "localhost",
        port: 5432,
        database: "myapp",
        user: "postgres",
        password: "password"
    )
)

// Use in your application
struct UserRepository {
    let pool: ResourcePool<PostgresConnection>
    
    func fetchUser(id: UUID) async throws -> User? {
        try await pool.withResource(timeout: .seconds(5)) { conn in
            try await conn.query("SELECT * FROM users WHERE id = \(id)")
        }
    }
}

🌐 HTTP Client Pool

import Foundation

actor HTTPClient: PoolableResource {
    struct Config: Sendable {
        let timeout: TimeInterval
    }
    
    private let session: URLSession
    
    static func create(config: Config) async throws -> HTTPClient {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = config.timeout
        return HTTPClient(session: URLSession(configuration: configuration))
    }
    
    func validate() async -> Bool {
        return true
    }
    
    func reset() async throws {
        session.invalidateAndCancel()
    }
    
    func fetch(url: URL) async throws -> Data {
        let (data, _) = try await session.data(from: url)
        return data
    }
}

// Create pool for API clients
let httpPool = try await ResourcePool<HTTPClient>(
    capacity: 10,
    resourceConfig: .init(timeout: 30)
)

// Make requests with automatic pooling
let data = try await httpPool.withResource { client in
    try await client.fetch(url: apiURL)
}

📄 WKWebView Pool (PDF Generation)

#if canImport(WebKit)
import WebKit

actor WebViewResource: PoolableResource {
    struct Config: Sendable {}
    
    private var webView: WKWebView!
    
    static func create(config: Config) async throws -> WebViewResource {
        return await WebViewResource()
    }
    
    @MainActor
    init() {
        self.webView = WKWebView()
    }
    
    func validate() async -> Bool {
        return webView != nil
    }
    
    func reset() async throws {
        await MainActor.run {
            webView.stopLoading()
            webView.loadHTMLString("", baseURL: nil)
        }
    }
    
    func renderPDF(html: String) async throws -> Data {
        // Render HTML to PDF
    }
}

// Pool for PDF generation
let pdfPool = try await ResourcePool<WebViewResource>(
    capacity: 3,
    resourceConfig: .init(),
    warmup: true
)

// Generate PDFs with pooled WebViews
let pdfData = try await pdfPool.withResource { webView in
    try await webView.renderPDF(html: invoiceHTML)
}
#endif

Performance Characteristics

Based on comprehensive test suite with 45 scenarios:

Throughput Benchmarks

Pool Capacity Concurrent Tasks Total Ops Throughput Avg Wait
2 10 200 277 ops/s 28.1ms
5 30 300 646 ops/s 36.8ms
10 50 500 1,267 ops/s 30.0ms
20 100 500 2,395 ops/s 30.1ms

Scalability Results

Concurrent Waiters Duration Ops/Sec Max Queue Handoff Rate
10 1.06s 9.4 0 0.0%
30 1.07s 28.1 20 66.7%
50 1.09s 45.9 40 80.0%
100 1.09s 92.1 90 90.0%
200 1.09s 183.8 190 95.0%

Key Insights:

  • ✅ Linear scalability up to 200 concurrent waiters
  • ✅ Zero timeouts even under extreme load (500 concurrent ops)
  • ✅ High handoff rate (90%+) indicates efficient queue management
  • ✅ Consistent throughput with minimal degradation

Latency Distribution (Under Contention)

Pool capacity: 5, Concurrent ops: 200

Percentile Latency
Min 15.9ms
P50 160.9ms
P90 165.7ms
P95 167.0ms
P99 167.9ms
Max 167.9ms
Avg 143.8ms

P99/Avg ratio: 1.2x (excellent tail latency)

API Reference

Initialization

init(
    capacity: Int,
    resourceConfig: Resource.Config,
    warmup: Bool = true
) async throws

Parameters:

  • capacity: Maximum resources to create (must be > 0)
  • resourceConfig: Configuration for creating resources
  • warmup: If true, pre-create all resources; if false, create lazily

Core Methods

// Use a resource with automatic cleanup
func withResource<T>(
    timeout: Duration = .seconds(30),
    _ operation: (Resource) async throws -> T
) async throws -> T

// Get current pool state
var statistics: Statistics { get async }

// Get production metrics
var metrics: Metrics { get async }

// Graceful shutdown
func drain(timeout: Duration = .seconds(30)) async throws

// Immediate shutdown
func close() async

Statistics

struct Statistics: Sendable, Equatable {
    let available: Int           // Resources ready for use
    let leased: Int             // Resources currently in use
    let capacity: Int           // Maximum pool capacity
    let waitQueueDepth: Int     // Tasks waiting for resources
    
    var inUse: Int              // Alias for leased
    var utilization: Double     // 0.0 to 1.0
    var hasBackpressure: Bool   // Queue depth > 0
}

Metrics

struct Metrics: Sendable {
    let currentStatistics: Statistics
    let totalAcquisitions: Int
    let timeouts: Int
    let validationFailures: Int
    let resetFailures: Int
    let creationFailures: Int
    let successfulReturns: Int
    let waitersQueued: Int
    let directHandoffs: Int
    
    var averageWaitTime: Duration?
    var handoffRate: Double
}

Advanced Usage

Handling Failures

// Resource that can fail validation
actor UnreliableResource: PoolableResource {
    struct Config: Sendable {}
    private var failureCount = 0
    
    func validate() async -> Bool {
        // Simulate intermittent failures
        failureCount += 1
        return failureCount % 5 != 0
    }
    
    // Pool automatically discards invalid resources
}

Custom Timeouts

// Short timeout for quick operations
let fastResult = try await pool.withResource(timeout: .seconds(1)) { resource in
    try await resource.quickOperation()
}

// Long timeout for expensive operations
let slowResult = try await pool.withResource(timeout: .seconds(60)) { resource in
    try await resource.expensiveOperation()
}

Monitoring and Observability

// Periodic monitoring
Task {
    while !Task.isCancelled {
        let stats = await pool.statistics
        let metrics = await pool.metrics
        
        logger.info("Pool stats", metadata: [
            "available": .string(String(stats.available)),
            "leased": .string(String(stats.leased)),
            "utilization": .string(String(format: "%.1f%%", stats.utilization * 100)),
            "queue_depth": .string(String(stats.waitQueueDepth)),
            "handoff_rate": .string(String(format: "%.1f%%", metrics.handoffRate * 100))
        ])
        
        try await Task.sleep(for: .seconds(30))
    }
}

Graceful Shutdown

// Application shutdown
func shutdown() async throws {
    do {
        // Wait for active operations to complete
        try await pool.drain(timeout: .seconds(30))
        logger.info("Pool drained successfully")
    } catch PoolError.drainTimeout {
        logger.warning("Pool drain timed out, some resources still leased")
        // Force close if needed
        await pool.close()
    }
}

Sharing Pools Globally

IMPORTANT: For system-limited resources (WebViews, database connections, file handles), creating multiple pool instances can lead to resource exhaustion. Use a global actor to ensure a single shared pool:

// ❌ BAD: Each operation creates its own pool
func generatePDF(html: String) async throws -> Data {
    let pool = try await ResourcePool<WKWebViewResource>(capacity: 8, ...)
    // Problem: 7 parallel calls = 56 WebViews trying to initialize!
    return try await pool.withResource { ... }
}

// ✅ GOOD: Single shared pool via global actor
@globalActor
public actor WebViewPoolActor {
    public static let shared = WebViewPoolActor()

    private var sharedPool: ResourcePool<WKWebViewResource>?

    public func getPool() async throws -> ResourcePool<WKWebViewResource> {
        if let existing = sharedPool {
            return existing
        }

        let pool = try await ResourcePool<WKWebViewResource>(
            capacity: 8,
            resourceConfig: .default,
            warmup: true
        )
        sharedPool = pool
        return pool
    }
}

// Usage: All callers share the same pool
func generatePDF(html: String) async throws -> Data {
    let pool = try await WebViewPoolActor.shared.getPool()
    return try await pool.withResource { webView in
        try await webView.renderPDF(html: html)
    }
}

Why this matters:

  • 7 parallel operations × 8 WebViews each = 56 WebViews (exhausts system)
  • 7 parallel operations sharing 1 pool of 8 = 8 WebViews (graceful queueing)
  • 56x improvement in test performance (76s → 1.4s)
  • Proper FIFO queueing ensures fairness
  • One warmup cost amortized across all users

When NOT to share:

  • Different resource configurations needed
  • Isolated testing scenarios
  • Short-lived, bounded workloads
  • Resources with incompatible lifecycles

Multiple Pools

Each ResourcePool is independent. When running multiple pools of different types, consider your total system resource budget:

// Each pool maxes out independently
let dbPool = ResourcePool<DatabaseConnection>(capacity: 10)    // ~100MB
let httpPool = ResourcePool<HTTPClient>(capacity: 20)          // ~100MB
let webViewPool = ResourcePool<WKWebView>(capacity: 3)         // ~600MB
// Total: ~800MB

Error Handling

public enum PoolError: Error, Sendable, Equatable {
    case timeout                    // Acquisition timeout expired
    case closed                     // Pool is closed
    case creationFailed(String)     // Resource creation failed
    case resetFailed(String)        // Resource reset failed
    case drainTimeout               // Drain timeout with leased resources
}

Requirements

  • Swift 5.9+ (Swift 6.0 language mode for strict concurrency)
  • Platforms:
    • macOS 14+
    • iOS 17+
    • tvOS 17+
    • watchOS 10+

Dependencies

This package has zero external dependencies - it only uses Foundation and Swift standard library.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Support

License

This project is licensed under the Apache License 2.0. See LICENSE for details.


Made with ❤️ by coenttb

About

A production-ready, actor-based resource pool for Swift with FIFO fairness and zero thundering herd

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages