Resiliência da conexão inativa

Descarregar o driver PHP

A resiliência da ligação permite que o Microsoft ODBC Driver for SQL Server restaure uma ligação inativa avariada quando o servidor e o estado da sessão suportam a recuperação. As extensões PHP passam ConnectRetryCount e ConnectRetryInterval para ODBC; não implementam o protocolo de reconexão. A documentação do ODBC lista suporte para SQL Server 2014 e versões posteriores, Base de Dados SQL do Azure e SQL Database no Microsoft Fabric. Não infiras suporte a Warehouse a partir do suporte para base de dados SQL no Fabric.

Keyword Valores Predefinido Description
ConnectRetryCount Inteiro entre 0 e 255 (inclusive) 1 O número máximo de tentativas para restabelecer uma ligação quebrada antes de desistir. Um valor de 0 desativa a reconexão.
ConnectRetryInterval Inteiro entre 1 e 60 (inclusive) 10 O número de segundos de espera entre tentativas. Ignorado se ConnectRetryCount estiver 0.

O tempo de espera da consulta da instrução limita quanto tempo o ODBC pode gastar a recuperar uma ligação inativa 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 ligação; aumentá-la não prolonga a margem de recuperação da instrução.

Defina as mesmas palavras-chave de retry num array de opções de ligação SQLSRV, como ["ConnectRetryCount" => 2, "ConnectRetryInterval" => 5], ou num nome de fonte de dados PDO_SQLSRV (DSN), como ConnectRetryCount=2;ConnectRetryInterval=5. Estas definições não repetem uma transação falhada nem tentam novamente a chamada inicial de ligação da aplicação.

Para retentativas ao nível da aplicação de erros transitórios que ocorrem enquanto uma consulta está a ser executada (que a resiliência da ligação inativa não cobre), veja o padrão de retentativa nos Microsoft Drivers for PHP for SQL Server.

Escolha a política de recuperação

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

Failure A resiliência da ligação inativa trata disso? Ação da aplicação
A ligação inicial não pode ser estabelecida No. Não existe uma sessão estabelecida para restaurar. Inspecione o erro de ligação. Repita apenas em caso de falha transitória com uma política limitada.
Uma ligação inativa e recuperável quebra-se O ODBC pode tentar recuperar a ligação quando a ligação for usada novamente. Permitir tempo limite da instrução suficiente para as tentativas de recuperação pretendidas; ainda tratar de uma falha final.
Uma instrução falhou durante a execução Não assumas recuperação ou reexecução. Inspecione o erro e determine se a operação foi concluída. Repita uma operação de leitura apenas se repeti-la for aceitável para a sua aplicação.
Uma ligação é interrompida durante uma transação Não presuma que a transação pode continuar. Abandone a sessão interrompida e determine se a transação foi realizada antes de decidir por outra tentativa.
O cliente fecha explicitamente a sua ligação ao PHP No. Fechar um handle é limpeza da aplicação, não uma desligação por inatividade. Abra uma nova conexão se for preciso mais trabalho.

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

Observações

A resiliência da ligação só se aplica quando a ligação está inativa. Uma falha durante uma transação não desencadeia a 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 ao nível da sessão.
  • Bloqueia aplicações.
  • EXECUTE AS / REVERT Contexto de segurança.
  • Identificadores de automação de Ligação e Incorporação de Objetos (OLE).
  • Identificadores de Linguagem de Marcação Extensível (XML) preparados.
  • Sinalizadores de rastreio.

Example

Cada programa abre duas ligações a uma instância de SQL Server de teste. A segunda conexão termina a primeira sessão com KILL. Uma consulta subsequente na primeira ligação testa a recuperação da ligação ociosa do ODBC. A consulta utiliza linhas inline, pelo que não é necessária configuração de tabelas. Os programas libertam cada instrução antes de interromper a ligação e verificam tanto os valores da consulta recuperada como a hora de início de sessão alterada do servidor. Um ID de sessão por si só 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 terminar uma sessão com o KILL. O código encerra apenas a sessão que acabou de abrir. Não substitua o ID de sessão de outra aplicação.

Defina SQL_SERVER, SQL_DATABASE, SQL_USER, e SQL_PASSWORD no ambiente de processo. Configure um certificado de servidor de confiança cujo nome corresponda a SQL_SERVER. Este exemplo utiliza autenticação SQL para uma instância de teste do SQL Server. Não é um exemplo de autenticação do Fabric nem de identidade gerida.

Armazene o nome real da base de dados, nome de utilizador e palavra-passe nas variáveis de ambiente. Cada programa escapa a estes valores uma vez para o tratamento de ligação ao estilo ODBC do driver PHP, duplicando as chaves de fecho e envolvendo cada valor entre chavetas. Ambas as ligações reutilizam os valores escapados.

SQLSRV

Guarde o programa como recover-sqlsrv.php, e execute php recover-sqlsrv.php. O helper gera uma exceção em caso de falha do SQLSRV; não tenta novamente nenhuma instrução da 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

Guarde este programa separado como recover-pdo.php, e execute php recover-pdo.php com o mesmo ambiente e permissões. Define explicitamente a busca numérica para que as asserções inteiras correspondam ao programa SQLSRV. O 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 ligação:

Recovered connection: 3 matching rows; login time changed.

O teste termina uma sessão intencionalmente deixada inativa. Não testa partição de rede, failover de serviço, recuperação de transações ativas, replay de escrita ou limitação do Azure. Não interprete uma aprovação como prova de que todas as operações de aplicação interrompidas são seguras para repetir.

Diagnosticar uma recuperação falhada

Symptom Cheque
KILL foi negado Use uma identidade de teste permitida ou peça ao administrador que realize o teste controlado. Não substitua por outro ID de sessão.
A primeira consulta já falha Corrige a ligação, o certificado, a consulta e as permissões antes de testar a recuperação.
A consulta após KILL falha Verifique ConnectRetryCount, o timeout da consulta, os diagnósticos ODBC e se o estado da sessão é recuperável. Não escondas o erro com uma nova tentativa incondicional.
Uma tabela temporária desaparece após abrir uma nova ligação Uma ligação de substituição criada pela aplicação não é a sessão antiga. Redesenhe o trabalho dependente da sessão em vez de assumir a recriação automática do estado.
Uma operação de escrita devolve um erro de ligação Determine o seu resultado antes de tentar novamente. O erro do cliente por si só não prova que o servidor o reverteu.

Mantém os diagnósticos em registos protegidos. O SQLSRV expõe uma coleção de erros que pode conter mais do que um registo; O PDO fornece SQLSTATE e detalhes do driver através da exceção. Grave a operação e as definições de timeout sem registar palavras-passe ou tokens. Para mais informações, consulte Atividade de registo.

Aplicabilidade de destino

O ODBC e o destino SQL realizam a reconexão automática. O PHP passa nas definições e reporta sucesso ou falha. Não recria objetos temporários, não restaura as variáveis da sua aplicação nem reproduz uma transação.

Os programas controlados utilizam o SQL Server e necessitam de permissão para terminar a sua própria sessão de teste através de uma segunda ligação. Base de Dados SQL do Azure e SQL Database no Fabric têm as suas próprias permissões de serviço e requisitos de autenticação. Não trate esses programas como testes drop-in para Azure SQL Managed Instance, Azure Synapse Analytics ou Warehouse no Fabric, nem infira o suporte de recuperação deles pelo facto de uma ligação normal ter sucesso. Verifique o suporte de recuperação do serviço selecionado antes de confiar nele. Warehouse e base de dados SQL no Fabric continuam a ser alvos distintos.