Résilience des connexions inactives

Télécharger le pilote PHP

La résilience de la connexion permet au pilote Microsoft ODBC pour SQL Server de restaurer une connexion inactive défectueuse lorsque le serveur et l’état de la session supportent la récupération. Les extensions PHP passent ConnectRetryCount et ConnectRetryInterval à ODBC ; elles n’implémentent pas le protocole de reconnexion. La documentation ODBC liste la prise en charge de SQL Server 2014 et versions ultérieures, Azure SQL Database, et SQL Database dans Microsoft Fabric. N'inférez pas le support de l'Warehouse à partir du support de la base de données SQL dans Fabric.

Mot clé Valeurs Par défaut Description
ConnectRetryCount Entier compris entre 0 et 255 (inclus) 1 Nombre maximal de tentatives de rétablissement d’une connexion interrompue avant abandon. Une valeur de 0 désactive la reconnexion.
ConnectRetryInterval Entier compris entre 1 et 60 (inclus) 10 Le nombre de secondes d’attente entre les tentatives. Ignoré si ConnectRetryCount c’est 0.

Le délai d’attente de la requête limite la durée que ODBC peut consacrer à la récupération d’une connexion inactive pendant l’exécution. Définissez QueryTimeout dans les options d’instructions SQLSRV ou PDO::SQLSRV_ATTR_QUERY_TIMEOUT pour PDO_SQLSRV. LoginTimeout contrôle l’établissement d’une connexion ; son augmentation n’augmente pas le budget de récupération de l’instruction.

Définissez les mêmes mots-clés de réessai dans un tableau d’options de connexion SQLSRV, comme ["ConnectRetryCount" => 2, "ConnectRetryInterval" => 5], ou dans un nom de source de données PDO_SQLSRV (DSN), comme ConnectRetryCount=2;ConnectRetryInterval=5. Ces paramètres ne rejouent pas une transaction ratée ni ne réessaient l’appel de connexion initial de l’application.

Pour la nouvelle tentative au niveau de l'application des erreurs transitoires qui surviennent pendant l'exécution d'une requête (ce que la résilience de la connexion inactive ne couvre pas), voir le schéma de nouvelle tentative dans Microsoft Drivers for PHP for SQL Server.

Choisissez la stratégie de récupération

La récupération de connexion inactive préserve une session récupérable afin que l’application puisse continuer à utiliser son handle de connexion PHP. Ce n’est pas une instruction générale pour exécuter une commande à nouveau.

Failure La résilience des connexions inactives s’en occupe-t-elle ? Action de l’application
La connexion initiale ne peut pas être établie No. Il n’y a pas de session établie à restaurer. Inspectez l’erreur de connexion. Réessayez uniquement en cas d’échec transitoire avec une stratégie limitée.
Une connexion inactive et récupérable se rompt ODBC peut tenter la récupération lorsque la connexion est réutilisée. Prévoir un délai d’expiration de l’instruction suffisant pour les tentatives de récupération prévues ; gérer malgré tout une défaillance finale.
Une instruction échoue lors de l’exécution Ne supposez pas que c’est une récupération ou une réexécution. Inspectez l’erreur et déterminez si l’opération s’est terminée. Réessayez une opération de lecture seulement si le fait de la répéter est acceptable pour votre application.
Une connexion se rompt pendant une transaction Ne supposez pas que la transaction peut continuer. Abandonnez la session défectueuse et déterminez si la transaction a été réalisée avant de décider d’une nouvelle tentative.
Le client ferme explicitement sa connexion PHP No. Fermer un descripteur est un nettoyage d’application, pas une déconnexion pour cause d’inactivité. Ouvrez une nouvelle connexion si davantage d’opérations sont nécessaires.

Gardez les intervalles de récupération et les délais de requête cohérents. Par exemple, les programmes utilisent deux tentatives de reconnexion avec un intervalle de 5 secondes et un délai d’attente d’instruction de 30 secondes. Les tentatives de connexion prennent aussi du temps ; multiplier le nombre de tentatives par l’intervalle ne garantit pas une durée totale de récupération. Un dernier délai d’expiration nécessite toujours une gestion des erreurs.

Remarques

La résilience de la connexion ne s’applique que lorsque la connexion est inactive. Une défaillance lors d’une transaction ne déclenche pas la reconnexion. Les états de session suivants empêchent également la reconnexion :

  • Des tables temporaires.
  • Curseurs globaux et locaux.
  • Contexte de transaction et verrous de transaction au niveau de la session.
  • Verrouiller l’application
  • EXECUTE AS / REVERT Contexte de sécurité.
  • Handles d’automatisation de la liaison et de l’incorporation d’objets (OLE).
  • Descripteurs XML préparés.
  • Drapeaux de trace.

Exemple

Chaque programme ouvre deux connexions vers une instance de SQL Server de test. La seconde connexion termine la première session avec KILL. Une requête ultérieure sur la première connexion met à l’épreuve le mécanisme de récupération des connexions inactives d’ODBC. La requête utilise des lignes intégrées, donc aucune configuration de table n’est requise. Les programmes libèrent chaque instruction avant de rompre la connexion et vérifient à la fois les valeurs de la requête récupérée et une heure de connexion au serveur modifiée. Un identifiant de session seul ne prouve pas la récupération car SQL Server peut le réutiliser.

Avertissement

Exécutez cet exemple uniquement sur une instance de test. La connexion nécessite une autorisation pour terminer une session avec KILL. Le code ne termine que la session qu’il vient d’ouvrir. Ne remplacez pas l’ID de session d’une autre application.

Définissez SQL_SERVER, SQL_DATABASE, SQL_USER et SQL_PASSWORD dans l’environnement du processus. Configurez un certificat serveur de confiance dont le nom correspond à SQL_SERVER. Cet exemple utilise l’authentification SQL pour une instance de test de SQL Server. Ce n'est pas un exemple d'authentification Fabric ni d'authentification par identité gérée.

Stockez le nom réel de la base de données, le nom d’utilisateur et le mot de passe dans les variables d’environnement. Chaque programme échappe à ces valeurs une fois pour la gestion de connexion de type ODBC du pilote PHP en doublant les accolades fermantes et en encadrant chaque valeur entre accolades. Les deux connexions réutilisent les valeurs échappées.

SQLSRV

Enregistrez le programme sous le nom de recover-sqlsrv.php et exécutez php recover-sqlsrv.php. La fonction utilitaire génère une exception en cas d’échec SQLSRV ; elle ne réessaie aucune instruction de l’application.

<?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

Enregistrez ce programme séparé sous recover-pdo.php, et exécutez php recover-pdo.php avec le même environnement et les mêmes permissions. Il définit explicitement la récupération numérique pour que les assertions entières correspondent au programme SQLSRV. PDO n’a pas d’appel sqlsrv_errors() ; en cas d’échec d’une opération, PDOException est levée en mode exception.

<?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;
}

Sortie attendue de l’un ou l’autre programme lorsque ODBC rétablit la connexion :

Recovered connection: 3 matching rows; login time changed.

Le test met fin à une session volontairement inactive. Il ne teste pas de partition réseau, de basculement de service, de récupération active des transactions, de relecture d'écriture ou de limitation côté Azure. N’interprétez pas une réussite comme preuve que chaque opération d’application interrompue est sûre à répéter.

Diagnostiquer un échec de restauration

Symptôme Cocher
KILL Accès refusé Utilisez une identité de test autorisée ou demandez à l’administrateur d’effectuer le test contrôlé. Ne remplacez pas par un autre ID de session.
La première requête échoue déjà Corrigez la connexion, le certificat, la requête et les autorisations avant de tester la récupération.
La requête après KILL échoue Vérifiez ConnectRetryCount, le délai d’attente de la requête, les diagnostics ODBC, et si l’état de la session est récupérable. Ne masquez pas l’erreur avec une nouvelle tentative inconditionnelle.
Une table temporaire disparaît après l’ouverture d’une nouvelle connexion Une connexion de remplacement créée par une application n’est pas l’ancienne session. Repensez le travail dépendant de la session plutôt que de supposer une reconstitution automatique de l’état.
Une opération d’écriture renvoie une erreur de connexion Déterminez son résultat avant de réessayer. L’erreur du client à elle seule ne prouve pas que le serveur l’a annulée.

Gardez les diagnostics dans des journaux protégés. SQLSRV expose une collection d’erreurs pouvant contenir plus d’un enregistrement ; PDO fournit SQLSTATE et détails du pilote via l’exception. Enregistrez l’opération et ses paramètres d’expiration sans enregistrer de mots de passe ni de jetons. Pour plus d’informations, voir Activité de journalisation.

Applicabilité cible

ODBC et la cible SQL effectuent la reconnexion automatique. PHP passe les paramètres et rapporte le succès ou l’échec. Il ne recrée pas les objets temporaires, ne restaure pas les variables de votre application, ni ne rejoue une transaction.

Les programmes contrôlés utilisent SQL Server et nécessitent la permission de terminer leur propre session de test via une seconde connexion. Azure SQL Database et SQL Database dans Fabric ont leurs propres permissions de service et exigences d’authentification. Ne considérez pas ces programmes comme des tests de remplacement pour Azure SQL Managed Instance, Azure Synapse Analytics ou Warehouse in Fabric, ni ne déduisez leur support de récupération du fait qu'une connexion ordinaire réussit. Vérifiez le support de récupération du service sélectionné avant de vous y fier. L’entrepôt et la base de données SQL dans Fabric restent des cibles distinctes.