Resistencia de conexión inactiva

Descargar controlador PHP

La resiliencia de la conexión permite al controlador ODBC de Microsoft para SQL Server restaurar una conexión inactiva rota cuando el servidor y el estado de la sesión soportan la recuperación. Las extensiones PHP pasan ConnectRetryCount y ConnectRetryInterval a ODBC; no implementan el protocolo de reconexión. La documentación de ODBC indica soporte para SQL Server 2014 y versiones posteriores, Azure SQL Database y SQL Database en Microsoft Fabric. No infieras soporte para Warehouse a partir del soporte para base de datos SQL en Fabric.

Palabra clave Valores Valor predeterminado Descripción
ConnectRetryCount Un entero comprendido entre 0 y 255, ambos incluidos 1 El número máximo de intentos de restablecer una conexión interrumpida antes de abandonar. Un valor de 0 desactiva la reconexión.
ConnectRetryInterval Un entero comprendido entre 1 y 60, ambos incluidos 10 El número de segundos para esperar entre intentos. Se omite si ConnectRetryCount es 0.

El tiempo de espera de consulta de la instrucción limita cuánto tiempo puede ODBC pasar recuperando una conexión inactiva durante la ejecución. Configura QueryTimeout en opciones de sentencias SQLSRV o PDO::SQLSRV_ATTR_QUERY_TIMEOUT para PDO_SQLSRV. LoginTimeout controla el establecimiento de una conexión; aumentarla no extiende el presupuesto de recuperación de la sentencia.

Establece las mismas palabras clave de retry en un array de opciones de conexión SQLSRV, como ["ConnectRetryCount" => 2, "ConnectRetryInterval" => 5], o en un nombre de fuente de datos PDO_SQLSRV (DSN), como ConnectRetryCount=2;ConnectRetryInterval=5. Estas configuraciones no vuelven a ejecutar una transacción fallida ni intentan de nuevo la llamada inicial de conexión de la aplicación.

Para reintentos a nivel de aplicación de errores transitorios que ocurren mientras se ejecuta una consulta (que la resiliencia de conexión inactiva no cubre), consulta el patrón de reintentos en Microsoft Drivers for PHP for SQL Server.

Elige la política de recuperación

La recuperación de conexión inactiva preserva una sesión recuperable para que la aplicación pueda seguir usando su handle de conexión PHP. No es una instrucción general para ejecutar un comando de nuevo.

Failure ¿La resiliencia de la conexión inactiva lo gestiona? Acción de la aplicación
La conexión inicial no se puede establecer N. º No hay una sesión establecida que restaurar. Inspecciona el error en la conexión. Reintenta solo ante un fallo transitorio con una política acotada.
Interrupción de una conexión inactiva y recuperable ODBC puede intentar la recuperación cuando se vuelva a usar la conexión. Permitir suficiente tiempo de espera de la instrucción para los intentos de recuperación previstos; seguir gestionando un fallo final.
Una instrucción falla durante la ejecución No asumas recuperación ni reprocesamiento. Inspecciona el error y determina si la operación se ha completado. Vuelve a intentar una lectura solo si repetirla es aceptable para tu aplicación.
Una conexión se rompe durante una transacción No asumas que la transacción pueda continuar. Abandona la sesión rota y determina si la transacción se ha confirmado antes de decidir otro intento.
El cliente cierra explícitamente su conexión PHP N. º Cerrar un descriptor forma parte de la limpieza de la aplicación, no de una desconexión por inactividad. Abre una nueva conexión si es necesario realizar más trabajo.

Mantén los intervalos de recuperación y los tiempos límite de consulta coherentes. Por ejemplo, los programas utilizan dos intentos de reconexión con un intervalo de 5 segundos y un tiempo de espera de sentencia de 30 segundos. Los intentos de conexión también llevan tiempo; multiplicar el recuento por intervalo no garantiza la duración total de la recuperación. Un tiempo de espera final sigue requiriendo manejo de errores.

Observaciones

La resiliencia de la conexión solo se aplica cuando la conexión está inactiva. Un fallo durante una transacción no provoca la reconexión. Los siguientes estados de sesión también impiden la reconexión:

  • Tablas temporales.
  • Cursores globales y locales.
  • Contexto de transacción y bloqueos de transacciones a nivel de sesión.
  • La aplicación se bloquea.
  • EXECUTE AS / REVERT Contexto de seguridad.
  • Identificadores de automatización OLE.
  • Controladores preparados para el Lenguaje de Marcado Extensible (XML).
  • Indicadores de seguimiento.

Ejemplo

Cada programa abre dos conexiones a una instancia de SQL Server de prueba. La segunda conexión finaliza la primera sesión con KILL. Una consulta posterior en la primera conexión activa la recuperación de la conexión inactiva de ODBC. La consulta utiliza filas insertadas, por lo que no se requiere configuración de tablas. Los programas liberan cada sentencia antes de romper la conexión y verifican tanto los valores de la consulta recuperada como una hora de inicio de sesión del servidor modificada. Un ID de sesión por sí solo no es prueba de recuperación porque SQL Server puede reutilizarlo.

Caution

Ejecuta este ejemplo solo contra una instancia de prueba. La cuenta necesita permiso para terminar una sesión con KILL. El código solo termina la sesión que acaba de abrir. No sustituyas el ID de sesión de otra aplicación.

Establece SQL_SERVER, SQL_DATABASE, SQL_USER y SQL_PASSWORD en el entorno del proceso. Configura un certificado de servidor de confianza cuyo nombre coincida con SQL_SERVER. Este ejemplo utiliza autenticación SQL para una instancia de prueba de SQL Server. No es un ejemplo de autenticación de identidad gestionada ni de Fabric.

Almacena el nombre real de la base de datos, nombre de usuario y contraseña en las variables de entorno. Cada programa escapa estos valores una vez para el manejo de conexiones tipo ODBC del controlador PHP, duplicando las llaves de cierre y encerrando cada valor entre llaves. Ambas conexiones reutilizan los valores escapados.

SQLSRV

Guarda el programa como recover-sqlsrv.php, y ejecuta php recover-sqlsrv.php. El ayudante genera una excepción si SQLSRV falla; no vuelve a intentar ninguna instrucción de aplicación.

<?php
function requireSqlsrv($result)
{
    if ($result === false) {
        throw new RuntimeException(print_r(sqlsrv_errors(), true));
    }
    return $result;
}

$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $value = getenv($name);
    if ($value === false || $value === '') {
        throw new RuntimeException("Set $name before running this example.");
    }
    $config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$connectionInfo = [
    'Database' => $config['SQL_DATABASE'],
    'UID' => $config['SQL_USER'],
    'PWD' => $config['SQL_PASSWORD'],
    'Encrypt' => true,
    'TrustServerCertificate' => false,
    'LoginTimeout' => 30,
    'ConnectRetryCount' => 2,
    'ConnectRetryInterval' => 5,
];
$conn = requireSqlsrv(sqlsrv_connect($config['SQL_SERVER'], $connectionInfo));
$breaker = null;
try {
    $breaker = requireSqlsrv(sqlsrv_connect($config['SQL_SERVER'], $connectionInfo));
    $sessionQuery = "SELECT @@SPID AS SessionId, "
        . "CONVERT(varchar(33), login_time, 126) AS LoginTime "
        . "FROM sys.dm_exec_sessions WHERE session_id = @@SPID";
    $idStatement = requireSqlsrv(sqlsrv_query($conn, $sessionQuery));
    $before = requireSqlsrv(sqlsrv_fetch_array($idStatement, SQLSRV_FETCH_ASSOC));
    sqlsrv_free_stmt($idStatement);
    if (!is_array($before) || !is_int($before['SessionId']) || $before['SessionId'] <= 0) {
        throw new RuntimeException('Expected the current session identity.');
    }
    $spid = $before['SessionId'];

    $query = 'SELECT id FROM (VALUES (1), (2), (3)) AS Demo(id) ORDER BY id';
    $options = ['Scrollable' => SQLSRV_CURSOR_CLIENT_BUFFERED, 'QueryTimeout' => 30];
    $stmt = requireSqlsrv(sqlsrv_query($conn, $query, [], $options));
    if (requireSqlsrv(sqlsrv_num_rows($stmt)) !== 3) {
        throw new RuntimeException('Expected three rows before the disconnect.');
    }
    sqlsrv_free_stmt($stmt);

    // KILL doesn't accept a parameter marker; $spid is a checked server-returned integer.
    $kill = requireSqlsrv(sqlsrv_query($breaker, 'KILL ' . $spid));
    sqlsrv_free_stmt($kill);
    // Allow the simulated disconnect to reach the client before the next query.
    sleep(1);

    $stmt = requireSqlsrv(sqlsrv_query($conn, $query, [], $options));
    $ids = [];
    while (($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) !== null) {
        $row = requireSqlsrv($row);
        $ids[] = $row['id'];
    }
    if ($ids !== [1, 2, 3]) {
        throw new RuntimeException('Unexpected recovered rows or PHP types.');
    }
    sqlsrv_free_stmt($stmt);

    $idStatement = requireSqlsrv(sqlsrv_query($conn, $sessionQuery));
    $after = requireSqlsrv(sqlsrv_fetch_array($idStatement, SQLSRV_FETCH_ASSOC));
    sqlsrv_free_stmt($idStatement);
    if (!is_array($after) || $after['LoginTime'] === $before['LoginTime']) {
        throw new RuntimeException('Expected a new physical session.');
    }
    echo "Recovered connection: 3 matching rows; login time changed.\n";
} finally {
    if (is_resource($breaker)) {
        sqlsrv_close($breaker);
    }
    sqlsrv_close($conn);
}

PDO_SQLSRV

Guarda este programa separado como recover-pdo.php, y ejecuta php recover-pdo.php con el mismo entorno y permisos. Establece la obtención numérica explícitamente para que las aserciones de enteros coincidan con el programa SQLSRV. PDO no tiene ninguna llamada sqlsrv_errors(); los fallos de operación generan PDOException en modo excepción.

<?php
$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $value = getenv($name);
    if ($value === false || $value === '') {
        throw new RuntimeException("Set $name before running this example.");
    }
    $config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$server = $config['SQL_SERVER'];
$database = $config['SQL_DATABASE'];
$dsn = "sqlsrv:Server=$server;Database=$database;Encrypt=true;"
    . "TrustServerCertificate=false;LoginTimeout=30;"
    . "ConnectRetryCount=2;ConnectRetryInterval=5";
$attributes = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
    PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 30,
];
$pdo = new PDO($dsn, $config['SQL_USER'], $config['SQL_PASSWORD'], $attributes);
$breaker = null;
$stmt = null;
try {
    $breaker = new PDO($dsn, $config['SQL_USER'], $config['SQL_PASSWORD'], $attributes);
    $sessionQuery = "SELECT @@SPID AS SessionId, "
        . "CONVERT(varchar(33), login_time, 126) AS LoginTime "
        . "FROM sys.dm_exec_sessions WHERE session_id = @@SPID";
    $stmt = $pdo->query($sessionQuery);
    $before = $stmt->fetch(PDO::FETCH_ASSOC);
    $stmt = null;
    if (!is_array($before) || !is_int($before['SessionId']) || $before['SessionId'] <= 0) {
        throw new RuntimeException('Expected the current session identity.');
    }
    $spid = $before['SessionId'];
    $query = 'SELECT id FROM (VALUES (1), (2), (3)) AS Demo(id) ORDER BY id';
    $stmt = $pdo->query($query);
    if ($stmt->fetchAll(PDO::FETCH_COLUMN) !== [1, 2, 3]) {
        throw new RuntimeException('Unexpected initial rows or PHP types.');
    }
    $stmt = null;

    // KILL requires a checked integer, not a parameter marker.
    $stmt = $breaker->query('KILL ' . $spid);
    $stmt = null;
    sleep(1);

    $stmt = $pdo->query($query);
    if ($stmt->fetchAll(PDO::FETCH_COLUMN) !== [1, 2, 3]) {
        throw new RuntimeException('Unexpected recovered rows or PHP types.');
    }
    $stmt = null;
    $stmt = $pdo->query($sessionQuery);
    $after = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!is_array($after) || $after['LoginTime'] === $before['LoginTime']) {
        throw new RuntimeException('Expected a new physical session.');
    }
    echo "Recovered connection: 3 matching rows; login time changed.\n";
} finally {
    $stmt = null;
    $breaker = null;
    $pdo = null;
}

Salida esperada de cualquiera de los programas cuando ODBC restablece la conexión:

Recovered connection: 3 matching rows; login time changed.

La comprobación finaliza una sesión inactiva intencionadamente. No prueba una partición de red, conmutación por error de servicio, recuperación activa de transacciones, reproducción de escritura ni limitación de Azure. No interpretes una aprobación como prueba de que cada operación de la aplicación interrumpida es segura de repetir.

Diagnosticar una recuperación fallida

Síntoma Check
KILL está denegado Utiliza una identidad de prueba permitida o pide al administrador que realice la prueba controlada. No sustituyas por otro ID de sesión.
La primera consulta ya falla Arregla la conexión, el certificado, la consulta y los permisos antes de probar la recuperación.
La consulta posterior a KILL falla Comprueba ConnectRetryCount, el tiempo de espera de la consulta, los diagnósticos ODBC y si el estado de la sesión es recuperable. No ocultes el error con un reintento incondicional.
Una tabla temporal desaparece tras abrir una nueva conexión Una conexión de reemplazo creada por la aplicación no es la sesión antigua. Rediseñar el trabajo dependiente de la sesión en lugar de asumir recreación automática del estado.
Una operación de escritura devuelve un error de conexión Determina el resultado antes de volver a intentarlo. El error del cliente por sí solo no demuestra que el servidor haya deshecho la operación.

Mantén los diagnósticos en registros protegidos. SQLSRV expone una colección de errores que puede contener más de un registro; PDO suministra SQLSTATE y detalles del controlador a través de la excepción. Graba la operación y sus ajustes de tiempo de espera sin registrar contraseñas ni tokens. Para más información, consulte Actividad de registro.

Aplicabilidad de destino

ODBC y el destino SQL realizan la reconexión automática. PHP pasa los ajustes y reporta éxito o fracaso. No recrea objetos temporales, no restaura las variables de tu aplicación ni repite una transacción.

Los programas controlados utilizan SQL Server y requieren permiso para terminar su propia sesión de prueba a través de una segunda conexión. Azure SQL Database y SQL Database en Fabric tienen sus propios permisos de servicio y requisitos de autenticación. No trates esos programas como pruebas directas para Azure SQL Managed Instance, Azure Synapse Analytics o Warehouse en Fabric, ni infieras su soporte de recuperación a partir de que una conexión ordinaria tiene éxito. Consulta el soporte de recuperación del servicio seleccionado antes de confiar en él. Warehouse y base de datos SQL en Fabric siguen siendo objetivos distintos.