Resiliência da conexão inativa

Baixar o driver PHP

A resiliência de conexão permite que o driver Microsoft ODBC para SQL Server restaure uma conexão ociosa quebrada quando o estado do servidor e da sessão suporta a recuperação. As extensões PHP passam ConnectRetryCount e ConnectRetryInterval para o ODBC; elas não implementam o protocolo de reconexão. A documentação do ODBC lista suporte para SQL Server 2014 e versões posteriores, Banco de Dados SQL do Azure e SQL Database no Microsoft Fabric. Não infira suporte a Warehouse a partir do suporte a banco de dados SQL no Fabric.

Palavra-chave Valores Padrão Descrição
ConnectRetryCount Um inteiro entre 0 e 255 (inclusive) 1 O número máximo de tentativas de restabelecer uma conexão interrompida antes de desistir. Um valor de 0 desabilita a reconexão.
ConnectRetryInterval Um inteiro entre 1 e 60 (inclusive) 10 O número de segundos para esperar entre as tentativas. Ignorado se ConnectRetryCount for 0.

O tempo de espera da consulta da instrução SQL limita quanto tempo o ODBC pode gastar recuperando uma conexão ociosa durante a execução. Defina QueryTimeout nas opções de instrução SQLSRV ou PDO::SQLSRV_ATTR_QUERY_TIMEOUT para PDO_SQLSRV. LoginTimeout controla o estabelecimento de uma conexão; aumentá-la não estende o orçamento de recuperação da instrução.

Defina as mesmas palavras-chave de retry em um array de opções de conexão SQLSRV, como ["ConnectRetryCount" => 2, "ConnectRetryInterval" => 5], ou em um nome de fonte de dados PDO_SQLSRV (DSN), como ConnectRetryCount=2;ConnectRetryInterval=5. Essas configurações não reexecutam uma transação com falha nem tentam novamente a chamada de conexão inicial do aplicativo.

Para retentativas em nível de aplicação de erros transitórios que ocorrem enquanto uma consulta está sendo executada (que a resiliência de conexão ociosa não cobre), veja o padrão de retentativa no Microsoft Drivers for PHP for SQL Server.

Escolha a política de recuperação

A recuperação de conexão ociosa preserva uma sessão recuperável para que a aplicação possa continuar usando seu handle de conexão PHP. Não é uma instrução geral executar um comando novamente.

Failure A resiliência da conexão ociosa lida com isso? Ação de aplicativos
A conexão inicial não pode ser estabelecida No. Não há uma sessão estabelecida para restaurar. Inspecione o erro de conexão. Repita apenas uma falha temporária com uma política delimitada.
Uma conexão ociosa e recuperável é interrompida O ODBC pode tentar se recuperar quando a conexão é usada novamente. Permita um tempo limite da instrução suficiente para as tentativas de recuperação pretendidas; ainda assim, lide com uma falha final.
Falha de instrução durante execução Não presuma recuperação ou replay. Inspecione o erro e determine se a operação foi concluída. Tente nova leitura somente se repeti-la for aceitável para seu aplicativo.
Uma conexão é interrompida durante uma transação Não presuma que a transação pode continuar. Abandone a sessão quebrada e determine se a transação foi realizada antes de decidir por outra tentativa.
O cliente fecha explicitamente a conexão PHP No. Fechar um identificador é limpeza de aplicativo, não uma desconexão ociosa. Abra uma nova conexão se for necessário fazer mais trabalho.

Mantenha intervalos de recuperação e prazos de consulta consistentes. Por exemplo, os programas usam duas tentativas de reconexão com um intervalo de 5 segundos e um tempo de espera de instruções de 30 segundos. Tentativas de conexão também levam tempo; multiplicar a contagem pelo intervalo não garante a duração total da recuperação. Um timeout final ainda exige tratamento de erros.

Comentários

A resiliência da conexão só se aplica quando a conexão está ociosa. Uma falha durante uma transação não provoca uma reconexão. Os seguintes estados de sessão também impedem a reconexão:

  • Tabelas temporárias.
  • Cursores globais e locais.
  • Contexto de transação e bloqueios de transação em nível de sessão.
  • Bloqueios de aplicativo.
  • EXECUTE AS / REVERT Contexto de segurança.
  • Identificadores de automação de Vinculação e Incorporação de Objetos (OLE).
  • Identificadores de linguagem XML preparados.
  • Sinalizadores de rastreamento.

Exemplo

Cada programa abre duas conexões para uma instância de SQL Server de teste. A segunda conexão encerra a primeira sessão com KILL. Uma consulta subsequente na primeira conexão testa a recuperação de conexão ociosa do ODBC. A consulta usa linhas inline, então não é necessário configurar a tabela. Os programas liberam cada instrução antes de interromper a conexão; eles verificam os valores da consulta recuperada e o tempo de login do servidor alterado. Só um ID de sessão não é prova de recuperação porque o SQL Server pode reutilizá-lo.

Caution

Execute este exemplo apenas contra uma instância de teste. O login precisa de permissão para encerrar uma sessão com KILL. O código encerra apenas a sessão que ele acabou de abrir. Não substitua o ID de sessão de outro aplicativo.

Defina SQL_SERVER, SQL_DATABASE, SQL_USER, e SQL_PASSWORD no ambiente de processo. Configure um certificado de servidor confiável cujo nome corresponda a SQL_SERVER. Este exemplo usa autenticação SQL para uma instância de teste do SQL Server. Não é um exemplo de autenticação do Fabric ou de identidade gerenciada.

Armazene o nome real do banco de dados, nome de usuário e senha nas variáveis de ambiente. Cada programa escapa desses valores uma vez para o manuseio de conexão no estilo ODBC do driver PHP, dobrando as chaves de fechamento e envolvendo cada valor entre chaves. Ambas as conexões reutilizam os valores escapados.

SQLSRV

Salve o programa como recover-sqlsrv.php, e execute php recover-sqlsrv.php. O helper gera uma exceção em caso de falha do SQLSRV; ele não tenta novamente nenhuma instrução de aplicação.

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

Salve esse programa separado como recover-pdo.php, e execute php recover-pdo.php com o mesmo ambiente e permissões. Ele define a busca numérica explicitamente para que as asserções inteiras correspondam ao programa SQLSRV. PDO não tem chamada sqlsrv_errors(); falhas de operação lançam PDOException no modo de exceção.

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

Saída esperada de qualquer um dos programas quando o ODBC restaurar a conexão:

Recovered connection: 3 matching rows; login time changed.

O teste encerra uma sessão deliberadamente ociosa. Não testa uma partição de rede, failover de serviço, recuperação ativa de transações, replay de escrita ou limitação do Azure. Não interprete uma aprovação como prova de que cada operação de aplicativo interrompida é segura para repetição.

Diagnosticar uma recuperação fracassada

Sintoma Verificação
KILL é negado Use uma identidade de teste permitida ou peça ao administrador para realizar o teste controlado. Não substitua por outro ID de sessão.
A primeira consulta já falha Corrija a conexão, certificado, consulta e permissões antes de testar a recuperação.
A consulta após KILL falha Verifique ConnectRetryCount, o tempo limite da consulta, diagnósticos ODBC e se o estado da sessão é recuperável. Não oculte o erro com uma repetição incondicional.
Uma tabela temporária desaparece após abrir uma nova conexão Uma conexão substituta criada pelo aplicativo não é a sessão antiga. Redesenhe o trabalho dependente da sessão em vez de presumir a recriação automática do estado.
Uma gravação retorna um erro de conexão Estabeleça o resultado antes de repetir. O erro do cliente sozinho não prova que o servidor reverteu a operação.

Mantenha os diagnósticos em logs protegidos. O SQLSRV expõe uma coleção de erros que pode conter mais de um registro; O PDO fornece SQLSTATE e detalhes do driver através da exceção. Grave a operação e suas configurações de tempo limite sem registrar senhas ou tokens. Para mais informações, veja Atividade de registro.

Aplicabilidade do destino

ODBC e o destino SQL realizam a reconexão automática. PHP passa as configurações e reporta sucesso ou fracasso. Ele não recria objetos temporários, não restaura as variáveis do seu aplicativo nem reproduz uma transação.

Os programas controlados usam SQL Server e exigem permissão para encerrar sua própria sessão de teste por meio de uma segunda conexão. Banco de Dados SQL do Azure e SQL Database no Fabric têm suas próprias permissões de serviço e requisitos de autenticação. Não trate esses programas como testes drop-in para Instância Gerenciada de SQL do Azure, Azure Synapse Analytics ou Warehouse in Fabric, nem infira o suporte de recuperação deles pelo fato de que uma conexão comum é bem-sucedida. Verifique o suporte de recuperação do serviço selecionado antes de confiar nele. Warehouse e banco de dados SQL no Fabric continuam sendo alvos distintos.