FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index mod_harbour mod_harbour.v2.1 plugin for mysql keep-alive
Posts: 129
Joined: Mon Oct 17, 2005 03:03 AM
mod_harbour.v2.1 plugin for mysql keep-alive
Posted: Wed Feb 25, 2026 03:21 AM

** Provides a MySQL keep-alive connection pool, allowing .prg applications
** to reuse the same MySQL connection across multiple HTTP requests,
** eliminating the overhead of reconnecting on every request
** (connection setup typically takes 1050ms).
** This significantly improves database throughput under high concurrency.



mh_mysql_pool-e.prg

/*
** mh_mysql_pool-e.prg -- MySQL Keep-Alive Connection Pool for mod_harbour.v2.1
** (c) WenSheng, 2026
** MIT License
**
** ════════════════════════════════════════════
** Design Goals
** ════════════════════════════════════════════
**   Provides a MySQL keep-alive connection pool, allowing .prg applications
**   to reuse the same MySQL connection across multiple HTTP requests,
**   eliminating the overhead of reconnecting on every request
**   (connection setup typically takes 10~50ms).
**   This significantly improves database throughput under high concurrency.
**
** ════════════════════════════════════════════
** Thread Safety
** ════════════════════════════════════════════
**   The pool uses STATIC variables (shared across all threads) protected
**   by an hb_mutex lock.
**   All access to s_hPool MUST be wrapped between hb_mutexLock / hb_mutexUnlock.
**
**   [IMPORTANT] BEGIN SEQUENCE / RECOVER USING are used together to ensure:
**   1. Even if an exception occurs mid-operation, hb_mutexUnlock() is always
**      called, preventing a permanent mutex lock (deadlock).
**   2. Exception error messages are stored in THREAD STATIC ts_cLastError
**      and can be retrieved after the function returns via mhDB_LastError().
**
** ════════════════════════════════════════════
** Pool Data Structure (s_hPool)
** ════════════════════════════════════════════
**   s_hPool is a Hash table structured as follows:
**
**   s_hPool {
**     'host:port:user:dbname' => aPool [       <- sub-pool array (shared by same DB+user)
**       aEntry { oConn, lBusy, cPass, tLastUsed, tBorrowedAt, nTimeout },
**       aEntry { ... },
**       ...
**     ],
**     'host:port:user:db2'    => aPool [ ... ],
**     ...
**   }
**
**   Each aEntry array fields:
**   aEntry[1] = oConn       : TMySQLServer object (the actual MySQL connection)
**   aEntry[2] = lBusy       : .T. = checked out (in use),  .F. = idle (available)
**   aEntry[3] = cPass       : Stored password, used for automatic reconnection
**   aEntry[4] = tLastUsed   : Timestamp of last mhDB_Free() call
**   aEntry[5] = tBorrowedAt : Timestamp of last mhDB_Connect() checkout
**   aEntry[6] = nTimeout    : Maximum borrow duration in seconds (0 = unlimited)
**
** ════════════════════════════════════════════
** Connection Types (nTimeout)
** ════════════════════════════════════════════
**   nTimeout > 0 : Regular short-lived connection (HTTP request).
**                  If not returned within N seconds, it is considered a leak
**                  (caller forgot mhDB_Free()). It will be force-returned
**                  before the next checkout, preventing pool exhaustion.
**
**   nTimeout = 0 : Long-lived connection (SSE / Server-Sent Events / long-polling).
**                  Never auto-force-returned. The caller is responsible for
**                  calling mhDB_Free() explicitly.
**                  Long-lived and short-lived connections are managed in
**                  separate sub-pools and do NOT interfere with each other.
**
** ════════════════════════════════════════════
** Public API Overview
** ════════════════════════════════════════════
**   mhDB_Connect( cHost, cUser, cPass, nPort, cDBName, nTimeout ) -> oConn
**     Borrow a connection from the pool. Creates a new one if none is idle.
**
**   mhDB_Free( oConn )
**     Return a connection to the pool without closing it. MUST be called
**     after use β€” failing to call this will permanently mark the connection
**     as checked out.
**
**   mhDB_ForceClean( [cHost], [cUser], [cDBName], [nPort] ) -> nCount
**     Force-mark all checked-out connections as idle, including nTimeout=0
**     long-lived connections. No params = clean ALL sub-pools.
**     Returns the number of connections force-returned.
**
**   mhDB_ClosePool( cHost, cUser, nPort, cDBName )
**     Close and remove a specific sub-pool. TMySQLServer objects are set to
**     NIL and released by the Harbour garbage collector.
**
**   mhDB_CloseAll()
**     Close and remove all sub-pools. Typically called before server shutdown.
**
**   mhDB_PoolStatus() -> cHTML
**     Returns an HTML table showing the full pool status, for use in
**     debug/admin pages.
**
**   mhDB_LastError() -> cError
**     Returns the last error message for the current thread
**     (THREAD STATIC β€” each thread has its own independent copy).
**
** ════════════════════════════════════════════
** Quick Usage Example
** ════════════════════════════════════════════
**   oConn := mhDB_Connect( '127.0.0.1', 'root', 'pass', 3306, 'mydb' )
**   IF oConn == NIL
**      ?? 'Connection failed: ' + mhDB_LastError()
**   ELSE
**      oConn:SetChar( 'utf8' )  // Set UTF-8 charset (required for CJK content)
**      oConn:lWeb := .T.        // Web mode: FieldGet() strings have no trailing spaces
**      oRS := oConn:Query( 'SELECT * FROM users' )
**      // ... process data ...
**      oRS:End()                // Release ResultSet
**      mhDB_Free( oConn )       // ⚠️ Always return the connection!
**   ENDIF
*/

#include "hbclass.ch"
#include "hbthread.ch"

// ────────────────────────────────────────────────────────────────
// Global static variables (shared across all threads, protected by Mutex)
// ────────────────────────────────────────────────────────────────
STATIC s_hPool  := NIL   // Connection pool Hash (see data structure above)
STATIC s_oMutex := NIL   // Mutex lock β€” prevents concurrent modification of s_hPool

// ────────────────────────────────────────────────────────────────
// Per-thread last error message
// THREAD STATIC: each thread has its own independent copy
// ────────────────────────────────────────────────────────────────
THREAD STATIC ts_cLastError := ''

// ════════════════════════════════════════════════════════════════
// Public API: mhDB_LastError()
// Returns the last error message for the current thread.
// Returns: error string (empty string if no error)
// ════════════════════════════════════════════════════════════════
FUNCTION mhDB_LastError()
RETURN ts_cLastError

// ────────────────────────────────────────────────────────────────
// Internal: mhDB__Init()
// Lazy initialization β€” creates the Mutex and empty pool on first call.
// Avoids unnecessary initialization overhead at startup.
// ────────────────────────────────────────────────────────────────
STATIC PROCEDURE mhDB__Init()
    IF s_oMutex == NIL
    s_oMutex := hb_mutexCreate()  // Create the mutex lock
    ENDIF
    IF s_hPool == NIL
    s_hPool := { => }             // Create an empty Hash as the pool container
    ENDIF
RETURN

// ────────────────────────────────────────────────────────────────
// Internal: mhDB__Key()
// Generates a unique string key from connection parameters,
// used as the Hash key in s_hPool.
// Format: 'host:port:user:dbname'
// Connections with the same key share the same sub-pool.
// ────────────────────────────────────────────────────────────────
STATIC FUNCTION mhDB__Key( cHost, cUser, nPort, cDBName )
RETURN cHost + ':' + hb_ntos( nPort ) + ':' + cUser + ':' + cDBName

// ────────────────────────────────────────────────────────────────
// Internal: mhDB__NewConn()
// Creates a brand new TMySQLServer connection and selects the database.
// Returns NIL on failure; error details are written to ts_cLastError.
//
// Process:
//   1. TMySQLServer():New() establishes the TCP connection
//   2. oConn:NetErr() checks for connection failure
//   3. oConn:SelectDB() switches to the target database
//   4. oConn:Error() verifies the switch succeeded
// ────────────────────────────────────────────────────────────────
STATIC FUNCTION mhDB__NewConn( cHost, cUser, cPass, nPort, cDBName )
    LOCAL oConn := NIL
    LOCAL oError
    BEGIN SEQUENCE
    // Establish the MySQL TCP connection (no DB selected yet)
    oConn := TMySQLServer():New( cHost, cUser, cPass, nPort )
    IF oConn:NetErr()
    // Network connection failed (host unreachable, bad credentials, etc.)
    ts_cLastError := 'TMySQLServer:New() failed to connect!'
    oConn := NIL
    BREAK   // Exit SEQUENCE, continue after END SEQUENCE
    ENDIF
    IF ! Empty( cDBName )
    // Switch to the target database
    oConn:SelectDB( cDBName )
    IF ! Empty( oConn:Error() )
    // Database does not exist, or account lacks privileges
    ts_cLastError := 'SelectDB(' + cDBName + ') failed: ' + oConn:Error()
    oConn := NIL
    BREAK
    ENDIF
    ENDIF
    RECOVER USING oError
    // Catch unexpected exceptions (e.g. MySQL library crash)
    IF Empty( ts_cLastError )
    ts_cLastError := 'Exception during connection: ' + IF( ValType( oError ) == 'O', GetErrorInfo( oError ), '' )
    ENDIF
    oConn := NIL
    END SEQUENCE
RETURN oConn

// ────────────────────────────────────────────────────────────────
// Internal: mhDB__CleanupStale()
// Scans the sub-pool and force-returns connections that have exceeded
// their borrow timeout.
//
// [NOTE] Only processes connections with nTimeout > 0.
//        nTimeout=0 (long-lived) connections are NOT touched here.
// [NOTE] Must be called AFTER hb_mutexLock (caller holds the lock).
//
// Design purpose:
//   If an HTTP request has a bug and forgets to call mhDB_Free(),
//   the connection stays marked as checked-out permanently.
//   Over time, available connections dwindle and every request
//   must create a new one, defeating the purpose of the pool.
//   This function is called automatically inside mhDB_Connect()
//   to prevent pool exhaustion.
// ────────────────────────────────────────────────────────────────
STATIC PROCEDURE mhDB__CleanupStale( aPool )
    LOCAL aEntry, i, nElapsed
    FOR i := 1 TO Len( aPool )
    aEntry := aPool[ i ]
    IF aEntry[ 2 ] .AND. aEntry[ 6 ] > 0  // checked out AND has a timeout (not long-lived)
    // Calculate elapsed seconds: DateTime subtraction gives days; multiply by 86400
    nElapsed := ( hb_DateTime() - aEntry[ 5 ] ) * 86400
    IF nElapsed > aEntry[ 6 ]
    // Timed out! Force-return (caller has a bug β€” forgot mhDB_Free)
    aEntry[ 2 ] := .F.           // lBusy -> idle
    aEntry[ 4 ] := hb_DateTime() // update tLastUsed
    aEntry[ 5 ] := hb_DateTime() // reset tBorrowedAt
    ENDIF
    ENDIF
    NEXT i
RETURN

// ════════════════════════════════════════════════════════════════
// Public API: mhDB_ForceClean()
// Force-marks all checked-out connections as idle,
// including nTimeout=0 long-lived connections.
//
// When to use:
//   - An SSE connection died abnormally without calling mhDB_Free()
//   - Before server maintenance to ensure all connections are reclaimed
//   - Admin interface to manually reset the pool state
//
// Parameters (all optional):
//   cHost   : MySQL host (omit to clean ALL sub-pools)
//   cUser   : Login account
//   cDBName : Database name
//   nPort   : Port number (default 3306)
//
// Returns: number of connections force-returned
// ════════════════════════════════════════════════════════════════
FUNCTION mhDB_ForceClean( cHost, cUser, cDBName, nPort )
    LOCAL cKey, aPool, aEntry, i, nCount
    LOCAL lTargetAll, oError

// If cHost is not passed (NIL), clean all sub-pools
lTargetAll := ( cHost == NIL )

mhDB__Init()

ts_cLastError := ''
nCount := 0

hb_mutexLock( s_oMutex )

BEGIN SEQUENCE

IF lTargetAll
// Clean all sub-pools
FOR EACH aPool IN s_hPool
FOR i := 1 TO Len( aPool )
aEntry := aPool[ i ]
IF aEntry[ 2 ]  // lBusy = checked out
aEntry[ 2 ] := .F.           // force-mark as idle
aEntry[ 4 ] := hb_DateTime() // update tLastUsed
aEntry[ 5 ] := hb_DateTime() // reset tBorrowedAt
nCount++
ENDIF
NEXT i
NEXT
ELSE
// Clean only the specified sub-pool
hb_default( @nPort, 3306 )
cKey := mhDB__Key( cHost, cUser, nPort, cDBName )
IF hb_HHasKey( s_hPool, cKey )
aPool := s_hPool[ cKey ]
FOR i := 1 TO Len( aPool )
aEntry := aPool[ i ]
IF aEntry[ 2 ]  // lBusy = checked out
aEntry[ 2 ] := .F.
aEntry[ 4 ] := hb_DateTime()
aEntry[ 5 ] := hb_DateTime()
nCount++
ENDIF
NEXT i
ENDIF
ENDIF

RECOVER USING oError
// Catch exception β€” ensures Mutex can be unlocked below
IF Empty( ts_cLastError )
ts_cLastError := 'mhDB_ForceClean() exception: ' + IF( ValType( oError ) == 'O', GetErrorInfo( oError ), '' )
ENDIF

END SEQUENCE

hb_mutexUnlock( s_oMutex )  // Always executed, even on error

RETURN nCount

// ════════════════════════════════════════════════════════════════
// Public API: mhDB_Connect()
// Borrow a MySQL connection from the pool.
//
// Checkout process:
//   1. Ensure pool and mutex are initialized (lazy init)
//   2. Scan the sub-pool for stale (timed-out) checked-out connections
//      and force-return them (CleanupStale)
//   3. Search the sub-pool for an idle connection of the matching type:
//      a. Ping() succeeds -> check it out directly
//      b. Ping() fails    -> attempt reconnect;
//                           remove from pool if reconnect also fails
//      c. Type matching: nTimeout=0 only borrows from nTimeout=0 entries,
//         and vice versa β€” the two types never mix
//   4. No idle connection available -> create a new one, add to pool, check out
//
// Parameters:
//   cHost    : MySQL host IP or hostname (default '127.0.0.1')
//   cUser    : Login account (default 'root')
//   cPass    : Login password
//   nPort    : MySQL port (default 3306)
//   cDBName  : Database name to use
//   nTimeout : Max borrow duration in seconds (default 30; 0 = unlimited for SSE)
//
// Returns:
//   Success -> TMySQLServer object (MUST call mhDB_Free() when done)
//   Failure -> NIL (call mhDB_LastError() for details)
// ════════════════════════════════════════════════════════════════
FUNCTION mhDB_Connect( cHost, cUser, cPass, nPort, cDBName, nTimeout )
    LOCAL cKey, aPool, oConn, aEntry, i, lFound
    LOCAL oError

// Apply defaults for any omitted parameters
hb_default( @cHost,    '127.0.0.1' )
hb_default( @cUser,    'root' )
hb_default( @cPass,    '' )
hb_default( @nPort,    3306 )
hb_default( @cDBName,  '' )
hb_default( @nTimeout, 30 )   // Default 30-second timeout

mhDB__Init()

ts_cLastError := ''                                 // Clear previous error
cKey  := mhDB__Key( cHost, cUser, nPort, cDBName ) // Build the sub-pool Hash key
oConn := NIL

hb_mutexLock( s_oMutex )  // Lock β€” protect s_hPool access

BEGIN SEQUENCE

// Create the sub-pool array if this key has never been seen before
IF ! hb_HHasKey( s_hPool, cKey )
s_hPool[ cKey ] := {}
ENDIF
aPool := s_hPool[ cKey ]  // Get a reference to the sub-pool array

// Before borrowing, sweep the pool for timed-out connections
// (only handles nTimeout > 0 regular connections)
mhDB__CleanupStale( aPool )

// Search for an idle connection of the matching timeout type
lFound := .F.
FOR i := 1 TO Len( aPool )
aEntry := aPool[ i ]

// Condition 1: lBusy = .F. (idle)
// Condition 2: type match (both long-lived OR both regular β€” never mixed)
IF ! aEntry[ 2 ] .AND. ( ( nTimeout == 0 ) == ( aEntry[ 6 ] == 0 ) )
IF aEntry[ 1 ]:Ping() == 0  // 0 = Ping succeeded, connection still alive
// Check out this idle connection
aEntry[ 2 ] := .T.            // Mark as checked out
aEntry[ 5 ] := hb_DateTime()  // Record checkout timestamp
aEntry[ 6 ] := nTimeout       // Update timeout setting
oConn  := aEntry[ 1 ]
lFound := .T.
EXIT  // Found one β€” stop searching
ELSE
// Ping failed β€” connection dropped; attempt reconnect using stored password
oConn := mhDB__NewConn( cHost, cUser, aEntry[ 3 ], nPort, cDBName )
IF oConn != NIL
// Reconnect succeeded β€” update pool entry and check out
aEntry[ 1 ] := oConn
aEntry[ 2 ] := .T.
aEntry[ 5 ] := hb_DateTime()
aEntry[ 6 ] := nTimeout
lFound := .T.
EXIT
ELSE
// Reconnect also failed β€” remove this dead entry from the pool
hb_ADel( aPool, i, .T. )  // .T. = shrink array
i--  // Adjust index to avoid skipping the next entry
ENDIF
ENDIF
ENDIF
NEXT i

// No idle connection available β€” create a brand-new connection
IF ! lFound
oConn := mhDB__NewConn( cHost, cUser, cPass, nPort, cDBName )
IF oConn != NIL
// Add to pool: { connection, checked-out, password, returnedAt, borrowedAt, timeout }
AAdd( aPool, { oConn, .T., cPass, hb_DateTime(), hb_DateTime(), nTimeout } )
ELSE
// New connection also failed; ts_cLastError was set inside mhDB__NewConn
BREAK  // Exit SEQUENCE, enter RECOVER
ENDIF
ENDIF

RECOVER USING oError
// Catch any unexpected exception; ensures Mutex will be unlocked below
IF Empty( ts_cLastError )
ts_cLastError := 'mhDB_Connect() exception: ' + IF( ValType( oError ) == 'O', GetErrorInfo( oError ), '' )
ENDIF
oConn := NIL

END SEQUENCE

hb_mutexUnlock( s_oMutex )  // Always executed, even on error β€” prevents deadlock

RETURN oConn

// ════════════════════════════════════════════════════════════════
// Public API: mhDB_Free()
// Return a borrowed connection back to the pool (without closing it),
// marking it as idle and available for the next checkout.
//
// ⚠️ IMPORTANT: Always call this after you are done using a connection!
//    If omitted, the connection stays marked as checked-out permanently.
//    It will only be force-returned when nTimeout expires (regular connections),
//    or never (for nTimeout=0 long-lived connections).
//
// Parameter:
//   oConn : The TMySQLServer object returned by mhDB_Connect()
// ════════════════════════════════════════════════════════════════
PROCEDURE mhDB_Free( oConn )
    LOCAL aPool, aEntry, i
    LOCAL oError

// Ignore NIL or non-object values silently
IF ValType( oConn ) != 'O' .OR. oConn == NIL
RETURN
ENDIF

mhDB__Init()

hb_mutexLock( s_oMutex )

BEGIN SEQUENCE

// Search all sub-pools for this specific connection object
FOR EACH aPool IN s_hPool
FOR i := 1 TO Len( aPool )
aEntry := aPool[ i ]
IF aEntry[ 1 ] == oConn  // Compare by object reference (same TMySQLServer instance)
aEntry[ 2 ] := .F.           // Mark as idle (available for re-use)
aEntry[ 4 ] := hb_DateTime() // Record return timestamp
EXIT  // Found it β€” exit the inner loop
ENDIF
NEXT i
NEXT

RECOVER USING oError
IF Empty( ts_cLastError )
ts_cLastError := 'mhDB_Free() exception: ' + IF( ValType( oError ) == 'O', GetErrorInfo( oError ), '' )
ENDIF

END SEQUENCE

hb_mutexUnlock( s_oMutex )

RETURN

// ════════════════════════════════════════════════════════════════
// Public API: mhDB_ClosePool()
// Close and remove all connections in a specific sub-pool.
// TMySQLServer objects are set to NIL; Harbour GC releases the resources.
// Call this when a particular database is no longer needed.
//
// Parameters:
//   cHost   : MySQL host
//   cUser   : Login account
//   nPort   : Port number (default 3306)
//   cDBName : Database name
// ════════════════════════════════════════════════════════════════
PROCEDURE mhDB_ClosePool( cHost, cUser, nPort, cDBName )
    LOCAL cKey, aPool, aEntry, i
    LOCAL oError

hb_default( @nPort, 3306 )

mhDB__Init()

cKey := mhDB__Key( cHost, cUser, nPort, cDBName )

hb_mutexLock( s_oMutex )

BEGIN SEQUENCE

IF hb_HHasKey( s_hPool, cKey )
aPool := s_hPool[ cKey ]
FOR i := 1 TO Len( aPool )
aEntry := aPool[ i ]
aEntry[ 1 ] := NIL  // Clear object reference β€” GC will close the MySQL connection
NEXT i
hb_HDel( s_hPool, cKey )  // Remove this sub-pool from the Hash
ENDIF

RECOVER USING oError
IF Empty( ts_cLastError )
ts_cLastError := 'mhDB_ClosePool() exception: ' + IF( ValType( oError ) == 'O', GetErrorInfo( oError ), '' )
ENDIF

END SEQUENCE

hb_mutexUnlock( s_oMutex )

RETURN

// ════════════════════════════════════════════════════════════════
// Public API: mhDB_CloseAll()
// Close and remove ALL sub-pools and connections.
// Typically called before Apache module unload or server shutdown
// to ensure all MySQL connections are properly released.
// ════════════════════════════════════════════════════════════════
PROCEDURE mhDB_CloseAll()
    LOCAL aPool, aEntry, i
    LOCAL oError

mhDB__Init()

hb_mutexLock( s_oMutex )

BEGIN SEQUENCE

// Iterate all sub-pools and release each connection object
FOR EACH aPool IN s_hPool
FOR i := 1 TO Len( aPool )
aEntry := aPool[ i ]
aEntry[ 1 ] := NIL  // Let GC release the MySQL connection
NEXT i
NEXT
s_hPool := { => }  // Reset the entire pool to an empty Hash

RECOVER USING oError
IF Empty( ts_cLastError )
ts_cLastError := 'mhDB_CloseAll() exception: ' + IF( ValType( oError ) == 'O', GetErrorInfo( oError ), '' )
ENDIF

END SEQUENCE

hb_mutexUnlock( s_oMutex )

RETURN

// ════════════════════════════════════════════════════════════════
// Public API: mhDB_PoolStatus()
// Returns the full pool status as an HTML table, for debug/admin pages.
//
// Table columns:
//   Connection Key : host:port:user:dbname (sub-pool identifier)
//   Total          : Total number of connections in this sub-pool
//   In Use         : Currently checked-out connections
//   Idle           : Available connections ready to borrow
//   Type           : 'Long-lived' (nTimeout=0) or 'Ns' (N = timeout seconds)
//   Elapsed        : Seconds since last checkout (shows 'Returned' if idle)
//   Borrowed At    : Timestamp of last checkout
//   Last Returned  : Timestamp of last mhDB_Free()
//
// Returns: complete HTML string (<table>...</table>)
// ════════════════════════════════════════════════════════════════
FUNCTION mhDB_PoolStatus()
    LOCAL cHTML
    LOCAL aPool, aEntry, i
    LOCAL nTotal, nBusy, nElapsed
    LOCAL oError

mhDB__Init()

hb_mutexLock( s_oMutex )

// Build the table header (8 columns)
cHTML  := '<table border="1" cellpadding="4">'
cHTML  += '<tr>'
cHTML  += '<th>Connection Key</th>'
cHTML  += '<th>Total</th><th>In Use</th><th>Idle</th>'
cHTML  += '<th>Type</th><th>Elapsed(s)</th><th>Borrowed At</th><th>Last Returned</th>'
cHTML  += '</tr>'

BEGIN SEQUENCE

FOR EACH aPool IN s_hPool

// Count checked-out connections in this sub-pool
nTotal := Len( aPool )
nBusy  := 0
FOR i := 1 TO nTotal
aEntry := aPool[ i ]
IF aEntry[ 2 ]  // lBusy
nBusy++
ENDIF
NEXT i

// Output the sub-pool summary row
// Key column uses rowspan to span the summary row + all detail rows
cHTML += '<tr>'
cHTML += '<td rowspan="' + hb_ntos( nTotal + 1 ) + '">' + aPool:__enumKey() + '</td>'
cHTML += '<td>' + hb_ntos( nTotal )         + '</td>'  // Total
cHTML += '<td>' + hb_ntos( nBusy )          + '</td>'  // In Use
cHTML += '<td>' + hb_ntos( nTotal - nBusy ) + '</td>'  // Idle
cHTML += '<td colspan="4"></td>'  // Placeholder for Type/Elapsed/Borrowed/Returned columns
cHTML += '</tr>'

// Output detail rows for each individual connection
FOR i := 1 TO nTotal
aEntry   := aPool[ i ]
// Elapsed seconds (only meaningful when checked out; -1 when idle)
nElapsed := IF( aEntry[ 2 ], Int( ( hb_DateTime() - aEntry[ 5 ] ) * 86400 ), -1 )

cHTML += '<tr>'
// Status column (spans Total/In Use/Idle positions)
cHTML += '<td colspan="3">' + IF( aEntry[ 2 ], 'In Use', 'Idle' ) + '</td>'
// Connection type
cHTML += '<td>' + IF( aEntry[ 6 ] == 0, 'Long-lived', hb_ntos( aEntry[ 6 ] ) + 's' ) + '</td>'
// Elapsed borrow time (or 'Returned' if idle)
cHTML += '<td>' + IF( nElapsed >= 0, hb_ntos( nElapsed ) + 's', 'Returned' ) + '</td>'
// Last checkout timestamp (shown even when idle, to track last use)
cHTML += '<td>' + IF( ! Empty( aEntry[ 5 ] ), hb_TToC( aEntry[ 5 ] ), '-' ) + '</td>'
// Last return timestamp
cHTML += '<td>' + hb_TToC( aEntry[ 4 ] ) + '</td>'
cHTML += '</tr>'
NEXT i

NEXT

RECOVER USING oError
IF Empty( ts_cLastError )
ts_cLastError := 'mhDB_PoolStatus() exception: ' + IF( ValType( oError ) == 'O', GetErrorInfo( oError ), '' )
ENDIF
cHTML += '<tr><td colspan="8">Error: ' + ts_cLastError + '</td></tr>'

END SEQUENCE

cHTML += '</table>'

hb_mutexUnlock( s_oMutex )

RETURN cHTML

mh_mysql_pool_example-e.prg

<?prg
/*
** mh_mysql_pool_example-e.prg -- Usage Example for mh_mysql_pool
**
** Prerequisites:
**   mh_mysql_pool.prg must be compiled into libmhapache.dll (or .so).
**   Add the following line to libmhapache.hbp:
**     source/mh_mysql_pool.prg
**
** This example demonstrates:
**   1. Borrowing a connection from the pool (mhDB_Connect)
**   2. Setting UTF-8 charset (SetChar)
**   3. Enabling Web mode (lWeb)
**   4. Executing a Query and rendering results as HTML
**   5. Returning the connection to the pool (mhDB_Free)
**   6. Connecting to multiple different databases simultaneously
**   7. Viewing the pool status (mhDB_PoolStatus)
**
** Key Rules:
**   - Every borrowed connection MUST be returned via mhDB_Free() when done.
**   - Every ResultSet (oRS) MUST be released via oRS:End() after use.
**   - When using multiple databases, each connection object
**     (oConn / oConnA / oConnB ...) is completely independent.
**     SetChar, lWeb, Query, and mhDB_Free must all use the SAME object β€”
**     never mix them up.
*/

// ================================================================
// All LOCAL variable declarations must appear at the top of the
// program (Harbour language requirement)
// ================================================================
LOCAL oConn, oRS    // ss_aaa database connection and query result
LOCAL oConnA        // ss_bbb database connection
LOCAL oConnB        // ss_ccc database connection
LOCAL oConnC        // ss_ddd database connection (demo: 60s timeout)

AP_SetContentType( "text/html; charset=UTF-8" )

// ================================================================
// Example 1: ss_aaa database (Long-lived connection, nTimeout=0)
// ================================================================
// mhDB_Connect parameters: cHost, cUser, cPass, nPort, cDBName, nTimeout
// nTimeout=0 means long-lived β€” will NOT be auto-force-returned on timeout
?? '<h3>ss_aaa Database (Long-lived connection, nTimeout=0)</h3>'

oConn := mhDB_Connect( '127.0.0.1', 'user', 'password', 3306, 'ss_aaa', 0 )
IF oConn == NIL
// Connection failed β€” use mhDB_LastError() to get the reason
?? '<p style="color:red"> ss_aaa connection failed: ' + mhDB_LastError() + '</p>'
ELSE
?? '<p style="color:green"> ss_aaa connected successfully!</p>'
IF oConn:SetChar( 'utf8' ) != 0
// SetChar failed β€” MySQL does not support utf8 (uncommon)
?? 'Charset error!!<br>' + mhDB_LastError()
ELSE
oConn:lWeb := .T.  // Web mode: FieldGet() strings have no trailing spaces

// Verify the active database (for debugging)
oRS := oConn:Query( "SELECT DATABASE() AS d" )
IF oRS != NIL .AND. oRS:RecCount() > 0
?? '<p>Current DB = <b>' + hb_valToStr( oRS:FieldGet( 'd' ) ) + '</b></p>'
oRS:End()  // Always call End() to release the ResultSet
ENDIF

// Run the main query
oRS := oConn:Query( "SELECT CUTID, CT_NUM, CT_DAM FROM cut LIMIT 10" )
IF oRS != NIL .AND. oRS:RecCount() > 0
?? '<table border="1" cellpadding="4">'
?? '<tr><th>ID</th><th>Name</th><th>Tel</th></tr>'
oRS:GoTop()  // Move to the first record
DO WHILE ! oRS:EOF()
?? '<tr>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CUTID'  ) ) + '</td>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CT_NUM' ) ) + '</td>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CT_DAM' ) ) + '</td>'
?? '</tr>'
oRS:Skip()  // Advance to the next record
ENDDO
?? '</table>'
oRS:End()  // Release the ResultSet
ELSE
?? '<p>No records found.</p>'
ENDIF
ENDIF
mhDB_Free( oConn )  // IMPORTANT: Always return the connection to the pool!
ENDIF

// ================================================================
// Example 2: ss_bbb database (Default 30-second timeout)
// ================================================================
// When nTimeout is omitted, it defaults to 30 seconds
?? '<h3>ss_bbb Database (Regular connection, nTimeout=30s)</h3>'

oConnA := mhDB_Connect( '127.0.0.1', 'user', 'password', 3306, 'ss_bbb' )
IF oConnA == NIL
?? '<p style="color:red"> ss_bbb connection failed: ' + mhDB_LastError() + '</p>'
ELSE
?? '<p style="color:green"> ss_bbb connected successfully!</p>'
IF oConnA:SetChar( 'utf8' ) != 0  // Use oConnA here β€” NOT oConn!
?? 'Charset error!!<br>' + mhDB_LastError()
ELSE
oConnA:lWeb := .T.

// Verify the active database
oRS := oConnA:Query( "SELECT DATABASE() AS d" )
IF oRS != NIL .AND. oRS:RecCount() > 0
?? '<p>Current DB = <b>' + hb_valToStr( oRS:FieldGet( 'd' ) ) + '</b></p>'
oRS:End()
ENDIF

// Run the query (ss_bbb uses table name "cut", different from ss_aaa)
oRS := oConnA:Query( "SELECT CUTID, CT_NUM, CT_DAM FROM cut LIMIT 10" )
IF oRS != NIL .AND. oRS:RecCount() > 0
?? '<table border="1" cellpadding="4">'
?? '<tr><th>ID</th><th>Name</th><th>Tel</th></tr>'
oRS:GoTop()
DO WHILE ! oRS:EOF()
?? '<tr>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CUTID'  ) ) + '</td>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CT_NUM' ) ) + '</td>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CT_DAM' ) ) + '</td>'
?? '</tr>'
oRS:Skip()
ENDDO
?? '</table>'
oRS:End()
ELSE
// Display MySQL error for debugging (e.g. table does not exist)
?? '<p>No records found. | oConnA:Error() = ' + oConnA:Error() + '</p>'
ENDIF
ENDIF
mhDB_Free( oConnA )  // oConnA was borrowed, so return oConnA
ENDIF

// ================================================================
// Example 3: ss_ccc database (Default 30-second timeout)
// ================================================================
?? '<h3>ss_ccc Database (Regular connection, nTimeout=30s)</h3>'

oConnB := mhDB_Connect( '127.0.0.1', 'user', 'password', 3306, 'ss_ccc' )
IF oConnB == NIL
?? '<p style="color:red"> ss_ccc connection failed: ' + mhDB_LastError() + '</p>'
ELSE
?? '<p style="color:green"> ss_ccc connected successfully!</p>'
IF oConnB:SetChar( 'utf8' ) != 0  //  Use oConnB β€” NOT oConn or oConnA!
?? 'Charset error!!<br>' + mhDB_LastError()
ELSE
oConnB:lWeb := .T.

// Verify the active database
oRS := oConnB:Query( "SELECT DATABASE() AS d" )
IF oRS != NIL .AND. oRS:RecCount() > 0
?? '<p>Current DB = <b>' + hb_valToStr( oRS:FieldGet( 'd' ) ) + '</b></p>'
oRS:End()
ENDIF

oRS := oConnB:Query( "SELECT CUTID, CT_NUM, CT_DAM FROM cut LIMIT 10" )
IF oRS != NIL .AND. oRS:RecCount() > 0
?? '<table border="1" cellpadding="4">'
?? '<tr><th>ID</th><th>Name</th><th>Tel</th></tr>'
oRS:GoTop()
DO WHILE ! oRS:EOF()
?? '<tr>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CUTID'  ) ) + '</td>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CT_NUM' ) ) + '</td>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CT_DAM' ) ) + '</td>'
?? '</tr>'
oRS:Skip()
ENDDO
?? '</table>'
oRS:End()
ELSE
?? '<p>No records found. | oConnB:Error() = ' + oConnB:Error() + '</p>'
ENDIF
ENDIF
mhDB_Free( oConnB )
ENDIF

// ================================================================
// Example 4: ss_ddd database (Custom 60-second timeout)
// ================================================================
// nTimeout=60 means the connection will be force-returned if not
// returned within 60 seconds (on the next checkout attempt)
?? '<h3>ss_ddd Database (Custom nTimeout=60s)</h3>'

oConnC := mhDB_Connect( '127.0.0.1', 'user', 'password', 3306, 'ss_ddd', 60 )
IF oConnC == NIL
?? '<p style="color:red"> ss_ddd connection failed: ' + mhDB_LastError() + '</p>'
ELSE
?? '<p style="color:green"> ss_ddd connected successfully!</p>'
IF oConnC:SetChar( 'utf8' ) != 0
?? 'Charset error!!<br>' + mhDB_LastError()
ELSE
oConnC:lWeb := .T.

// Verify the active database
oRS := oConnC:Query( "SELECT DATABASE() AS d" )
IF oRS != NIL .AND. oRS:RecCount() > 0
?? '<p>Current DB = <b>' + hb_valToStr( oRS:FieldGet( 'd' ) ) + '</b></p>'
oRS:End()
ENDIF

oRS := oConnC:Query( "SELECT CUTID, CT_NUM, CT_DAM FROM cut LIMIT 10" )
IF oRS != NIL .AND. oRS:RecCount() > 0
?? '<table border="1" cellpadding="4">'
?? '<tr><th>ID</th><th>Name</th><th>Tel</th></tr>'
oRS:GoTop()
DO WHILE ! oRS:EOF()
?? '<tr>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CUTID'  ) ) + '</td>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CT_NUM' ) ) + '</td>'
?? '<td>' + hb_valToStr( oRS:FieldGet( 'CT_DAM' ) ) + '</td>'
?? '</tr>'
oRS:Skip()
ENDDO
?? '</table>'
oRS:End()
ELSE
// Error message must use oConnC, not oConnB
?? '<p>No records found. | oConnC:Error() = ' + oConnC:Error() + '</p>'
ENDIF
ENDIF
mhDB_Free( oConnC )
ENDIF

// ================================================================
// Pool Status Report (for debugging / admin)
// ================================================================
// mhDB_PoolStatus() returns an HTML table showing:
//   - Connection Key (host:port:user:dbname)
//   - Total / In Use / Idle counts per sub-pool
//   - Per-connection details: type, elapsed borrow time,
//     checkout timestamp, last return timestamp
?? '<h3>Connection Pool Status</h3>'
?? mhDB_PoolStatus()
?>

ε¦‚ζžœθͺθ­˜δΈ­ζ–‡ε­—ηš„οΌŒε―δ»₯εƒθ€ƒδΈ‹ι’ηΆ²ε€οΌš
https://www4.zzz.com.tw/phpBB3/viewtopic.php?f=2&t=371

line ID: ssbbstw

WeChat ID: ssbbstw
Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Re: mod_harbour.v2.1 plugin for mysql keep-alive
Posted: Wed Feb 25, 2026 11:54 AM

Good morning...

In a recurring call to the endpoint, where is the pool stored and how is it retrieved to handle multiple HTTP requests?

Is this strategy similar to this one in Node.js?

https://www.codegenes.net/blog/safely-keeping-mysql-connections-alive/

Jonsson Russi

Continue the discussion