Błąd połączonego serwera nie został wykryty przez TRY-CATCH

14

Konfiguruję zadanie, aby przejrzeć listę połączonych serwerów i wykonać określone zapytanie dla każdego z nich. Próbuję wykonać kwerendę w bloku TRY-CATCH, więc jeśli występuje problem z jednym konkretnym serwerem, mogę go zarejestrować, a następnie kontynuować z innymi serwerami.

Zapytanie, które wykonuję wewnątrz pętli, wygląda mniej więcej tak:

BEGIN TRY
    SELECT *
    FROM OPENQUERY([server1], 'SELECT 1 AS c;');
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH;

PRINT 'We got past the Catch block!';

Jeśli wystąpi problem z połączeniem z serwerem, kod po prostu zawiedzie natychmiast i nie zostanie przesłany do CATCHbloku. Jeśli serwer łączy się, ale w rzeczywistym zapytaniu wystąpił błąd, np. Podziel przez zero, jest to wychwytywane zgodnie z oczekiwaniami CATCHbloku.

Na przykład utworzyłem serwer połączony z nazwą, o której wiem, że nie istnieje. Wykonując powyższe otrzymuję:

OLE DB provider "SQLNCLI" for linked server "nonserver" returned message 
    "Login timeout expired".
OLE DB provider "SQLNCLI" for linked server "nonserver" returned message 
    "An error has occurred while establishing a connection to the server. 
    When connecting to SQL Server 2005, this failure may be caused by the 
    fact that under the default settings SQL Server does not allow remote
    connections.".
Msg 53, Level 16, State 1, Line 0
Named Pipes Provider: Could not open a connection to SQL Server [53].

Przeczytałem BOL TRY-CATCHi wiem, że nie będzie on wychwytywał błędów poziomu 20+, które przerywają połączenie, ale wydaje się, że tak nie jest (tylko poziom 16).

Czy ktoś wie, dlaczego te błędy nie są wychwytywane poprawnie?

JamesLean
źródło

Odpowiedzi:

11

Jedną z rzeczy, których możesz spróbować, jest użycie sp_testlinkedserver. Można także wydać OPENQUERYużycie dynamicznego SQL (jak słusznie wskazał Max), aby odroczyć parser sprawdzający poprawność nazwy serwera do czasu wykonania.

BEGIN TRY
    EXEC sp_testlinkedserver N'server1';

    EXEC sp_executesql N'SELECT * FROM OPENQUERY([server1], 
      ''SELECT 1 AS c;'');';
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH;

PRINT 'We got past the Catch block!';

Chociaż bez tego działa to równie dobrze sp_testlinkedserver, ta procedura może być nadal przydatna w zapobieganiu próbowaniu całej wiązki kodu na tym serwerze ...


Ponadto, ponieważ jeśli się sp_testlinkedservernie powiedzie, faktycznie się nie powiedzie w czasie kompilacji, możesz to odłożyć i nadal przechwytywać, używając również dynamicznego SQL:

BEGIN TRY
  EXEC master.sys.sp_executesql N'EXEC sp_testlinkedserver N''server1'';';
  ...
END TRY
Aaron Bertrand
źródło
6

Próbowałeś czegoś takiego?

BEGIN TRY
    DECLARE @cmd nvarchar(max);
    SET @cmd = 'SELECT * FROM OPENQUERY([server1], ''SELECT 1 AS c;'');';
    EXEC sp_executesql @cmd;
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH;

Zgodnie z poniższymi komentarzami działa to, ponieważ błąd nie jest już generowany w czasie kompilacji. Błąd występuje teraz w czasie wykonywania, w procedurze przechowywanej sp_executesql.

Max Vernon
źródło
Dzięki Max. Tak, myślałem o dynamicznym SQL (i powyższe rzeczywiście działa poprawnie). Jestem zainteresowany DLACZEGO błąd nie został wykryty?
JamesLean
@AaronBertrand Jeśli dodasz prosty PRINT 'Start';na samym początku skryptu, zostanie on wydrukowany w danych wyjściowych, nawet jeśli połączenie nie powiedzie się i skrypt zakończy działanie z błędem. Oznaczałoby to błąd w czasie wykonywania , prawda? Chyba że źle to zrozumiem?
JamesLean
Gah, kiedy dodałem PRINT, wciąż miałem sp_testlinkedserverwezwanie w skrypcie. W rzeczywistości nie jest drukowany przy użyciu mojego oryginalnego (nieudanego) skryptu. Wygląda więc na to, że jest to błąd czasu kompilacji i dlatego nie zostaje złapany.
JamesLean
@JamesLean zbyt zabawny, kiedy poszedłem do repro, aby potwierdzić to, co sugerujesz, skomentowałem sp_testlinkedserverpołączenie, ale zostawiłem SELECTjako dynamiczny SQL. PRINTNie wystąpić, jeśli odwołać się do nazwy serwera bezpośrednio, tak jak sugerował wcześniej, BEGIN TRYnigdy nie zostanie wprowadzony, ponieważ błąd jest podniesiona w pierwszej kolejności.
Aaron Bertrand
4

Po zbadaniu wygląda na to, że ten błąd nie został wyłapany, ponieważ jest to błąd czasu kompilacji, a nie błąd czasu wykonywania. Aby to zademonstrować, spróbuj wykonać następujące czynności:

PRINT 'Before TRY';

BEGIN TRY
    SELECT 1/0;

    SELECT *
    FROM OPENQUERY([nonserver], 'SELECT 1 AS c;');
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER(), ERROR_MESSAGE();
END CATCH;

Instrukcja początkowa PRINTnie otrzymuje danych wyjściowych, ani błąd dzielenia przez zero nie jest wykonywany / wychwytywany. Nieistniejący serwer powoduje natychmiastową awarię skryptu.

JamesLean
źródło
3

Ostatnio miałem podobny problem, w którym wywołałem zdalną procedurę z poziomu TRY-CATCH, a procedura nie powiodła się z powodu próby wstawienia duplikatu klucza (błąd czasu wykonania poziomu 16). Blok CATCH nie został wywołany. Znalazłem przyczynę w tym artykule: https://technet.microsoft.com/en-us/library/ms191515(v=sql.105).aspx

Rozwiązaniem jest ustawienie XACT_ABORT ON w procedurze wywoływania przed wywołaniem procedury zdalnej. Gdy XACT_ABORT jest włączony, CATCH jest wywoływany zgodnie z oczekiwaniami. Musisz pamiętać, że ustawienie XACT_ABORT jest propagowane do procedury zdalnej, co może wpłynąć na jego zachowanie.

RM Buda
źródło
0
ALTER PROCEDURE dbo.LinkedServer_Status 
    @linked_server nvarchar(128),
    @exists bit OUT,
    @connected bit OUT,
    @server_datetime datetime OUT
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @server_id int;
    SELECT @server_id = server_id from sys.servers where name = @linked_server;
    IF (@@ROWCOUNT = 0)
        SELECT @exists = 0, @connected = 0, @server_datetime = null;
    ELSE BEGIN
        SELECT @exists = 1;
        BEGIN TRY
            DECLARE @TBL TABLE(server_datetime DateTime);
            DECLARE @SQL nVarChar(2048); -- MUST BE nVarChar
            SELECT @SQL =
                'SELECT server_datetime FROM OPENQUERY(['+RTRIM(@linked_server)+'], ''SELECT GETDATE() server_datetime'')'; 
            INSERT @TBL EXEC sp_executesql @SQL;
            SELECT TOP 1 @connected = 1, @server_datetime = server_datetime FROM @TBL;
        END TRY
        BEGIN CATCH
            SELECT @connected = 0, @server_datetime = null;
            SELECT ERROR_MESSAGE();
        END CATCH
    END;
END

-- now use stored procedure

SET NOCOUNT ON;

DECLARE
    @linked_server nvarchar(128),
    @exists bit,
    @connected bit,
    @server_datetime datetime

SELECT @linked_server = 'FRICKE BMS';

exec dbo.LinkedServer_Status
    @linked_server, 
    @exists OUT, 
    @connected OUT, 
    @server_datetime OUT;

IF (@exists = 0)
    PRINT 'Linked Server "' + @linked_server + '" DOES NOT Exist';
ELSE BEGIN
    PRINT 'Linked Server "' + @linked_server + '" Exists';
    IF (@connected = 0)
        PRINT 'Linked Server "' + @linked_server + '" NOT Connected';
    ELSE
        PRINT 'Linked Server "' + @linked_server + '" IS Connected; Server DateTime: '+convert(varchar(25), @server_datetime, 120) 
END;
Jeffrey W Lott
źródło
1
Cześć, przede wszystkim witamy na stronie. W tym miejscu lubimy trochę wyjaśnienia na temat tego, jak to działa, a nie ścianę kodu bez dodatkowych informacji. Ale dzięki za odpowiedź.
Tom V - spróbuj topanswers.xyz