c# – 来自SQLCLR的异步API调用中的问题

简而言之,我需要在特定表中发生更改时异步通知SQL Server的Web API服务.

为了实现上述目标,我创建了一个SQLCLR存储过程,其中包含异步API调用以通知服务.当存在名为Table1的表中的插入时,通过触发器调用SQLCLR存储过程.这里的主要挑战是API必须从同一个表中读取数据(表1).

如果我使用HttpWebRequest.GetResponse()这是同步版本,由于插入触发器的隐式锁定,整个操作将被锁定.为了避免这种情况,我使用了HttpWebRequest.GetResponseAsync()方法来调用API并且不等待响应.因此它触发API请求并且程序控件继续运行,因此触发器事务不会在table1上保持任何锁定,并且API能够从table1读取数据.

现在我必须实现一个错误通知机制,当出现故障时(比如无法连接到远程服务器),我需要向管理团队发送一封电子邮件.我在catch()块中编写了邮件组合逻辑.如果我继续使用上面的HttpWebRequest.GetResponseAsync().Result方法,整个操作将变为同步并锁定整个操作.

如果我使用Microsoft文档中建议的BeginGetResponse()和EndGetResponse()方法实现并运行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,它具有排队机制,可以帮助我们实现异步触发器.但是我们对上述情况有什么解决方案吗?从方法角度来看,我做错了吗?请指导我.

最佳答案 有几件事情可能会成为可能的问题:

>不会调用allDone.WaitOne();阻止,直到收到响应,否定所有这些异步的东西的需要/使用?
>即使这确实有效,您是否在一次会话中对此进行测试?您有几个静态成员(类级别)变量,例如public static ManualResetEvent allDone,它们在所有会话中共享. SQLCLR使用共享应用程序域(应用程序域是每个数据库/每个程序集所有者).因此,多个会话将覆盖这些共享静态变量的彼此的值.这是非常危险的(因此,为什么非只读静态变量只允许在UNSAFE程序集中使用).此模型仅在您可以在任何给定时刻保证单个呼叫者时才有效.

除了任何SQLCLR技术细节之外,即使您确实设法克服了这个特定问题,我也不确定这是一个好模型.

一个更好,更安全的模型是:

>创建一个队列表来记录这些更改.您通常只需要键列和时间戳(DATETIME或DATETIME2,而不是TIMESTAMP数据类型).
>让触发器记录当前时间和修改到队列表中的行
>创建一个存储过程,从队列中获取项目,从最旧的记录开始,处理它们(它肯定可以调用您的SQLCLR存储过程来进行Web服务器调用,但不需要它是异步的,所以删除那些东西并将程序集重新设置为EXTERNAL_ACCESS,因为您不需要/想要UNSAFE).

在事务中执行此操作,以便在“处理”失败时不会从队列表中完全删除记录.有时使用带有DELETE的OUTPUT子句将您正在处理的行保留到本地临时表中会有所帮助.

即使调用SQLCLR存储过程需要按行进行,也一次处理多个记录.
>创建一个SQL Server代理程序作业,每分钟执行一次存储过程(或根据需要减少)

小问题:

>将输入参数复制到静态变量(例如_mailServer = mailServer;)的模式充其量是没有意义的,并且由于不是线程安全的而且容易出错.请记住,静态变量在所有会话中共享,因此任何并发会话都将覆盖以前的值,从而确保竞争条件.请删除名称以下划线开头的所有变量.
>使用Convert.To …的模式也是不必要的,并且对性能有轻微影响.所有Sql *类型都具有Value属性,该属性返回预期的.NET类型.因此,您只需要:string url = weburl.Value;
>不需要使用以后需要转换的不正确数据类型.意思是,而不是使用SqlString for connectionLimit,而是使用SqlInt32然后你可以简单地做int connLimit = connectionLimit.Value;
>您可能不需要手动执行安全性(即httpWebRequest.Headers.Add(“授权”,“基本”编码);).我很确定你可以使用uname创建一个新的NetworkCredential并传递并将其分配给请求.

点赞