Проблема в асинхронных вызовах API из SQLCLR - PullRequest
0 голосов
/ 01 января 2019

Вкратце, мне нужно асинхронно уведомлять службу веб-API от SQL Server, когда и в какой-либо таблице происходят изменения.

Для достижения вышеизложенного я создал хранимую процедуру SQLCLR, которая содержитасинхронный вызов API для уведомления службы.Хранимая процедура SQLCLR вызывается с помощью триггера по мере того, как происходит вставка в таблицу с именем Table1.Основной проблемой здесь является то, что API должен читать данные из той же таблицы (Table1).

Если я использую HttpWebRequest.GetResponse(), который является синхронной версией, вся операция блокируется из-за неявной блокировкитриггера вставки.Чтобы избежать этого, я использовал метод HttpWebRequest.GetResponseAsync(), который вызывает API и не ждет ответа.Таким образом, он запускает запрос API, и управление программой перемещается, поэтому транзакция триггера не удерживает никаких блокировок на table1, и API смог прочитать данные из table1.

Теперь яЯ должен реализовать механизм уведомления об ошибках, когда и когда возникают сбои (например, невозможно подключиться к удаленному серверу), и мне нужно отправить электронное письмо команде администраторовЯ написал логику составления почты внутри блока catch().Если я перейду к описанному выше методу HttpWebRequest.GetResponseAsync().Result, вся операция становится синхронной, и она блокирует всю операцию.

Если я использую реализацию методов BeginGetResponse() и EndGetResponse(), предложенную в документах Microsoft, и запускаю SQLCLRхранимая процедура, SQL Server зависает без какой-либо информации, почему?Что я здесь не так делаю?Почему метод RespCallback() не выполняется?

Совместное использование фрагментов кода SQLCLR, приведенных ниже.

public class RequestState
{
    // This class stores the State of the request.
    // const int BUFFER_SIZE = 1024;
    // public StringBuilder requestData;
    // public byte[] BufferRead;
    public HttpWebRequest request;
    public HttpWebResponse response;
    // public Stream streamResponse;

    public RequestState()
    {
        // BufferRead = new byte[BUFFER_SIZE];
        // requestData = new StringBuilder("");
        request = null;
        // streamResponse = null;
    }
}

public partial class StoredProcedures
{
    private static SqlString _mailServer = null;
    private static SqlString _port = null;
    private static SqlString _fromAddress = null;
    private static SqlString _toAddress = null;
    private static SqlString _mailAcctUserName = null;
    private static SqlString _decryptedPassword = null;
    private static SqlString _subject = null;

    private static string _mailContent = null;
    private static int _portNo = 0;

    public static ManualResetEvent allDone = new ManualResetEvent(false);
    const int DefaultTimeout = 20000; // 50 seconds timeout

#region TimeOutCallBack
/// <summary>
/// Abort the request if the timer fires.
/// </summary>
/// <param name="state">request state</param>
/// <param name="timedOut">timeout status</param>
private static void TimeoutCallback(object state, bool timedOut)
{
if (timedOut)
{
HttpWebRequest request = state as HttpWebRequest;
if (request != null)
{
request.Abort();
SendNotifyErrorEmail(null, "The request got timedOut!,please check the API");
}
}
}
#endregion

#region APINotification
[SqlProcedure]
public static void Notify(SqlString weburl, SqlString username, SqlString password, SqlString connectionLimit, SqlString mailServer, SqlString port, SqlString fromAddress
, SqlString toAddress, SqlString mailAcctUserName, SqlString mailAcctPassword, SqlString subject)
{
_mailServer = mailServer;
_port = port;
_fromAddress = fromAddress;
_toAddress = toAddress;
_mailAcctUserName = mailAcctUserName;
_decryptedPassword = mailAcctPassword;
_subject = subject;

if (!(weburl.IsNull && username.IsNull && password.IsNull && connectionLimit.IsNull))
{
var url = Convert.ToString(weburl);
var uname = Convert.ToString(username);
var pass = Convert.ToString(password);
var connLimit = Convert.ToString(connectionLimit);
int conLimit = Convert.ToInt32(connLimit);
try
{
if (!(string.IsNullOrEmpty(url) && string.IsNullOrEmpty(uname) && string.IsNullOrEmpty(pass) && conLimit > 0))
{
SqlContext.Pipe.Send("Entered inside the notify method");

HttpWebRequest httpWebRequest = WebRequest.Create(url) as HttpWebRequest;
string encoded = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(uname + ":" + pass));
httpWebRequest.Headers.Add("Authorization", "Basic " + encoded);
httpWebRequest.Method = "POST";
httpWebRequest.ContentLength = 0;
httpWebRequest.ServicePoint.ConnectionLimit = conLimit;

// Create an instance of the RequestState and assign the previous myHttpWebRequest
// object to its request field. 
RequestState requestState = new RequestState();
requestState.request = httpWebRequest;

SqlContext.Pipe.Send("before sending the notification");
//Start the asynchronous request.
IAsyncResult result =
(IAsyncResult)httpWebRequest.BeginGetResponse(new AsyncCallback(RespCallback), requestState);
SqlContext.Pipe.Send("after BeginGetResponse");

// this line implements the timeout, if there is a timeout, the callback fires and the request becomes aborted
ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, new WaitOrTimerCallback(TimeoutCallback), requestState, DefaultTimeout, true);
//SqlContext.Pipe.Send("after RegisterWaitForSingleObject");

// The response came in the allowed time. The work processing will happen in the 
// callback function.
allDone.WaitOne();
//SqlContext.Pipe.Send("after allDone.WaitOne();");

// Release the HttpWebResponse resource.
requestState.response.Close();
SqlContext.Pipe.Send("after requestState.response.Close()");
}
}
catch (Exception exception)
{
SqlContext.Pipe.Send(" Main Exception");
SqlContext.Pipe.Send(exception.Message.ToString());
//TODO: log the details in a error table
SendNotifyErrorEmail(exception, null);
}
}
}
#endregion

#region ResposnseCallBack
/// <summary>
/// asynchronous Httpresponse callback
/// </summary>
/// <param name="asynchronousResult"></param>
private static void RespCallback(IAsyncResult asynchronousResult)
{
try
{
SqlContext.Pipe.Send("Entering the respcallback");
// State of request is asynchronous.
RequestState httpRequestState = (RequestState)asynchronousResult.AsyncState;
HttpWebRequest currentHttpWebRequest = httpRequestState.request;
httpRequestState.response = (HttpWebResponse)currentHttpWebRequest.EndGetResponse(asynchronousResult);
SqlContext.Pipe.Send("exiting the respcallBack");
}
catch (Exception ex)
{
SqlContext.Pipe.Send("exception in the respcallBack");
SendNotifyErrorEmail(ex, null);
}
allDone.Set();
}
#endregion
}

Один из альтернативных подходов, описанных выше, - это использование SQL Server Service Broker с механизмом организации очереди, которыйпоможет нам реализовать асинхронные триггеры.Но есть ли у нас какое-либо решение для вышеуказанной ситуации?Я делаю что-то не так с точки зрения подхода?Пожалуйста, ведите меня.

Ответы [ 2 ]

0 голосов
/ 02 января 2019

Есть несколько вещей, которые выделяются как возможные проблемы:

  1. Не будет ли вызов на allDone.WaitOne(); блокироваться до тех пор, пока ответ не будет получен, отрицая необходимость / использование всего этогоАсинхронный материал?
  2. Даже если это работает, вы тестируете это за один сеанс?У вас есть несколько статических переменных (на уровне класса), таких как public static ManualResetEvent allDone, которые являются общими для всех сеансов.SQLCLR использует общий домен приложений (домены приложений для каждой базы данных / для владельца сборки).Следовательно, несколько сеансов перезаписывают значения друг друга этих общих статических переменных.Это очень опасно (следовательно, почему не -только статические переменные разрешены только в UNSAFE Сборках).Эта модель работает, только если вы можете гарантировать одного звонящего в любой момент.

Помимо каких-либо технических особенностей SQLCLR, я не уверен, что это хорошая модель, даже если вам удастся обойти эту особенностьПроблема.

Лучшей, более безопасной моделью было бы:

  1. создать таблицу очередей для регистрации этих изменений.обычно вам нужны только ключевые столбцы и временная метка (DATETIME или DATETIME2, а не тип данных TIMESTAMP).
  2. имеет журнал триггера текущего времени и строки, измененные в таблицу очередей
  3. создать хранимую процедуру, которая берет элементы из очереди, начиная с самых старых записей, обрабатывает их (что, безусловно, может вызывать вашу хранимую процедуру SQLCLR для вызова веб-сервера, но не нужно, чтобы она была асинхронной)поэтому удалите все это и установите сборку обратно на EXTERNAL_ACCESS, так как вам не нужно / хотите UNSAFE).

    Сделайте это в транзакции, чтобы записи не были полностью удалены из таблицы очереди в случае сбоя «обработки».Иногда помогает использование предложения OUTPUT с DELETE для резервирования строк, над которыми вы работаете, в локальную временную таблицу.

    Обрабатывать несколько записей одновременно, даже если вызов хранимой процедуры SQLCLR необходимо выполнять для каждой строки.

  4. создать задание агента SQL Server для выполнения сохраненнойпроцедура каждую минуту (или меньше в зависимости от необходимости)

Незначительные проблемы:

  1. шаблон копирования входных параметров в статические переменные (например, _mailServer = mailServer;) в лучшем случае не имеет смыслаи подвержен ошибкам независимо от того, что не является потокобезопасным.помните, что статические переменные являются общими для всех сеансов, поэтому любые параллельные сеансы будут перезаписывать предыдущие значения, что гарантирует условия гонки.Пожалуйста, удалите все переменные с именами, начинающимися с подчеркивания.
  2. шаблон использования Convert.To... также не нужен и незначительный удар по производительности.Все типы Sql* имеют свойство Value, которое возвращает ожидаемый тип .NET.Следовательно, вам нужно только: string url = weburl.Value;
  3. нет необходимости использовать неправильные типы данных, которые требуют преобразования позже.Это означает, что вместо использования SqlString для connectionLimit используйте SqlInt32, и тогда вы можете просто сделать int connLimit = connectionLimit.Value;
  4. Возможно, вам не нужно выполнять защиту вручную (то есть httpWebRequest.Headers.Add("Authorization", "Basic " + encoded);),Я почти уверен, что вы можете просто создать новый NetworkCredential с использованием uname и pass и назначить его для запроса.
0 голосов
/ 02 января 2019

Привет (и с Новым годом):)!

Пара вещей:

  • Если можете, держитесь подальше от триггеров!Они могут вызвать много неожиданных побочных эффектов, особенно если вы используете их для бизнес-логики.Я имею в виду, что они хороши для целей одитинга, но кроме этого я стараюсь избегать их, как чумы.

  • Итак, если вы не используете триггеры, какВы знаете, когда что-то вставлено?Что ж, я надеюсь, что ваши вставки происходят через хранимые процедуры и не являются прямыми вставками в таблицы.Если вы используете хранимые процедуры, вы можете иметь процедуру, которая выполняет вашу логику, и эта процедура вызывается из процедуры, которая выполняет вставку.

Вернуться к вашему вопросу.На самом деле у меня нет ответа на этот вопрос, но я бы не стал использовать SQLCLR в этом случае (sidenote: я БОЛЬШОЙ сторонник SQLCLR, и я реализовал много процессов SQLCLR, которые делают что-то похожее навы делаете, поэтому я не говорю это, потому что я не люблю SQLCLR).

В вашем случае, я бы посмотрел либо использовать Уведомления об изменениях , либо, как вы упоминаете в своем посте- Сервисный Брокер.Имейте в виду, что с SSB вы можете столкнуться с проблемами производительности (защелки, блокировки и т. Д.), Если ваша система очень нестабильна (+2000 ткс / сек).По крайней мере, это то, что мы испытали.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...