并发 Acumatica API 上下文导致 GetSchema() 出错

Concurrent Acumatica API contexts cause errors in GetSchema()

我们有一个 Web 服务,它接受来自外部站点的请求,然后使用 Acumatica API 根据请求添加或更新客户记录。当我们一次收到一个请求时,这工作正常。问题是外部站点将他们的请求分批处理,然后同时发送多个请求。这意味着我们最终会同时收到两个或更多请求 运行ning,这意味着同时发生多个登录和多个上下文。这最终几乎总是在许多 GetSchema() 调用之一中产生模糊的 "Object reference not set to an instance of an object" 。我还看到了一些 Lock Violation 错误,例如:"Error #147: Another process has added 'CSAnswers' record. Your changes will be lost."

我在下面创建了一个测试用例,它可以通过向发出所有 API 调用的同一个网页发出 3 个异步 Web 请求来复制这种情况。另一个问题是,当我 运行 它一段时间没有 运行 后,它似乎总是会产生错误。如果我 运行 它立即再次出现,那么它通常会成功。这让我觉得可能在后续调用中缓存了一些东西,所以它 运行 更快,然后 运行 不会进入自身???我不知道,我尝试添加一些延迟,看看是否会在随后的 运行 秒中更频繁地发生这种情况,但事实并非如此。

有谁知道 Acumatica API 是否绝对不支持异步/同步上下文?我只看到了 ,但不确定是不是一回事。

代码是 VB 中的两个 ASP.Net 页。 Default.aspx 只是用于创建对 CreateReservation.aspx 的单个和同时调用的一些按钮。

我们使用的是 Acumatica 版本 4.20.2063 和 IIS 8.5,我认为管道是 .net 4.0 集成的。谢谢!

Default.aspx.vb:

<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="Default.aspx.vb" Inherits="AcumaticaTesting._Default" Async="true" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
    <asp:Label runat="server" ID="lblMessage" ForeColor="Red"></asp:Label>
    <br />
<asp:Button runat="server" ID="btnStartOne" Text="Run One" OnClick="btnStartOne_Click" />
    <br />
<asp:Button runat="server" ID="btnStartAsynch" Text="Run Three (Asynchronous)" OnClick="btnStartAsynch_Click" />
</div>
</form>
</body>
</html>

Default.aspx.vb(相关方法)

Protected m_webRequest1 As WebClient
Protected m_webRequest2 As WebClient
Protected m_webRequest3 As WebClient
Protected m_webAddress As String = "http://localhost:61343/CreateReservation.aspx"

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

End Sub

Protected Sub btnStartOne_Click(sender As Object, e As EventArgs)
    Dim uri As Uri = New Uri(m_webAddress)

    m_webRequest1 = New WebClient()
    AddHandler m_webRequest1.OpenReadCompleted, AddressOf OpenReadCallback
    m_webRequest1.OpenReadAsync(uri)
End Sub

Protected Sub btnStartAsynch_Click(sender As Object, e As EventArgs)
    Dim uri As Uri = New Uri(m_webAddress)

    m_webRequest1 = New WebClient()
    AddHandler m_webRequest1.OpenReadCompleted, AddressOf OpenReadCallback
    m_webRequest1.OpenReadAsync(Uri)
    Threading.Thread.Sleep(CInt(Int(50))) ' milliseconds

    m_webRequest2 = New WebClient()
    AddHandler m_webRequest2.OpenReadCompleted, AddressOf OpenReadCallback
    m_webRequest2.OpenReadAsync(uri)
    Threading.Thread.Sleep(CInt(Int(50))) ' milliseconds

    m_webRequest3 = New WebClient()
    AddHandler m_webRequest3.OpenReadCompleted, AddressOf OpenReadCallback
    m_webRequest3.OpenReadAsync(uri)
End Sub

' THIS IS JUST A CALLBACK FOR THE ASYNCHRONOUS CALLS, ALL IT DOES IS SET A STATUS MESSAGE
Protected Sub OpenReadCallback(sender As Object, e As OpenReadCompletedEventArgs)
    Dim reply As Stream = Nothing
    Dim s As StreamReader = Nothing

    Try

        reply = CType(e.Result, Stream)
        s = New StreamReader(reply)
        Console.WriteLine(s.ReadToEnd())
    Finally

        If Not s Is Nothing Then

            s.Close()
        End If

        If Not reply Is Nothing Then

            reply.Close()
        End If
    End Try
    lblMessage.Text = "Received result"
End Sub

CreateReservation.aspx(相关方法)

' HELPER METHOD
Protected Function CreateValue(screenField As AcumaticaAPI.Field, newVal As String) As Value
    Return CreateValue(screenField, newVal, False)
End Function

' HELPER METHOD
Protected Function CreateValue(screenField As AcumaticaAPI.Field, newVal As String, addCommit As Boolean) As Value
    Dim theValue As Value = New Value()
    theValue.LinkedCommand = screenField
    theValue.Value = newVal

    If addCommit Then
        theValue.Commit = True
    End If

    Return theValue
End Function

' PAGE_LOAD MAKES ALL THE ACTUAL API CALLS
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
    Dim delayAmt As Integer = 4000

    ' Initialize the random-number generator.
    Randomize()
    Dim nameEnd As Integer = Now.Millisecond
    Dim fullName As String = "Doe, John" + nameEnd.ToString()

    ' STEP 1: Login
    Dim context1 As AcumaticaAPI.Screen
    context1 = New AcumaticaAPI.Screen
    context1.CookieContainer = New System.Net.CookieContainer()
    context1.AllowAutoRedirect = True
    context1.EnableDecompression = True
    context1.Timeout = 1000000
    context1.Url = ACUMATICA_URL
    Dim login1 As LoginResult = context1.Login(ACUMATICA_USER, ACUMATICA_PWD)

    ' STEP 2 : See if customer exists
    Dim CR303000 As CR303000Content = context1.CR303000GetSchema()
    context1.CR303000Clear()

    Dim nameFilter As Filter = New Filter()
    nameFilter.Field = CR303000.AccountSummary.BusinessAccountName
    nameFilter.Condition = FilterCondition.Equals
    nameFilter.Value = fullName

    Dim searchfilters() As Filter = {nameFilter}
    Dim searchCommands() As Command = {CR303000.AccountSummary.BusinessAccount, CR303000.DetailsMainContact.Phone1, CR303000.DetailsMainContact.Phone2}
    Dim searchResult As String()() = context1.CR303000Export(searchCommands, searchfilters, 0, False, False)

    ' STEP 3 CREATE CUSTOMER
    Dim AR303000 As AR303000Content = context1.AR303000GetSchema()
    context1.AR303000Clear()

    ' create customer with just name for now
    Dim nameVal As Value = CreateValue(AR303000.CustomerSummary.CustomerName, fullName)

    ' other fields required for Customer
    Dim classVal As Value = CreateValue(AR303000.GeneralInfoFinancialSettings.CustomerClass, "DEFAULT")
    Dim statementCycleVal As Value = CreateValue(AR303000.GeneralInfoFinancialSettings.StatementCycleID, "ENDOFMONTH")
    Dim statementTypeVal As Value = CreateValue(AR303000.BillingSettingsPrintAndEmailSettings.StatementType, "Open Item")
    Dim cashDiscountAccountVal As Value = CreateValue(AR303000.GLAccountsCashDiscountAccount.CashDiscountAccount, "10103")
    Dim creditVerificationVal As Value = CreateValue(AR303000.GeneralInfoCreditVerificationRulesCreditVerification.CreditVerification, "Disabled")

    ' execute insert with just name and required fields
    Dim insertCommands As Command() = {nameVal, classVal, statementCycleVal, statementTypeVal, cashDiscountAccountVal, creditVerificationVal, AR303000.Actions.Save}
    Dim insertResult As AR303000Content() = context1.AR303000Submit(insertCommands)

    ' STEP 4 : Find the newly created Customer record
    Dim CR303000_2 As CR303000Content = context1.CR303000GetSchema()
    context1.CR303000Clear()

    Dim nameFilter_2 As Filter = New Filter()
    nameFilter_2.Field = CR303000_2.AccountSummary.BusinessAccountName
    nameFilter_2.Condition = FilterCondition.Equals
    nameFilter_2.Value = fullName

    Dim searchfilters_2() As Filter = {nameFilter_2}
    Dim searchCommands_2() As Command = {CR303000_2.AccountSummary.BusinessAccount, CR303000_2.DetailsMainContact.Phone1, CR303000_2.DetailsMainContact.Phone2}
    Dim searchResult_2 As String()() = context1.CR303000Export(searchCommands_2, searchfilters_2, 0, False, False)
    Dim newCustomerID As String = searchResult_2(0)(0)

    ' STEP 5 : Add Business Acct fields
    Dim CR303000_3 As CR303000Content = context1.CR303000GetSchema()
    context1.CR303000Clear()

    ' create key field
    Dim baKeyVal As Value = CreateValue(CR303000_3.AccountSummary.BusinessAccount, newCustomerID.ToString())
    Dim baClassIDVal As Value = CreateValue(CR303000_3.DetailsCRM.ClassID, "DEFAULT")

    ' create custom fields to update at same time
    Dim passwordName As Value = CreateValue(CR303000_3.Attributes.Attribute, CUST_ATTRIBUTE_ID_PASSWORD)
    Dim passwordVal As Value = CreateValue(CR303000_3.Attributes.Value, "-------", True)
    Dim secretQuestionName As Value = CreateValue(CR303000_3.Attributes.Attribute, CUST_ATTRIBUTE_ID_SECRET_QUESTION)
    Dim secretQuestionVal As Value = CreateValue(CR303000_3.Attributes.Value, "QQQQQQ", True)
    Dim secretAnswerName As Value = CreateValue(CR303000_3.Attributes.Attribute, CUST_ATTRIBUTE_ID_SECRET_ANSWER)
    Dim secretAnswerVal As Value = CreateValue(CR303000_3.Attributes.Value, "AAAAAAA", True)

    ' execute update
    Dim updateBACommands As Command() = {baKeyVal, baClassIDVal, passwordName, passwordVal, secretQuestionName, secretQuestionVal, secretAnswerName, secretAnswerVal, CR303000_3.Actions.Save}
    Dim updateBAResult As CR303000Content() = context1.CR303000Submit(updateBACommands)
End Sub

我一直得到的完整异常是这个,但它可能发生在不同的 GetSchema() 调用中:

System.Web.Services.Protocols.SoapException was unhandled by user code
Actor=""
HResult=-2146233087
Lang=""
Message=System.Web.Services.Protocols.SoapException: Server was unable to process request. ---> System.NullReferenceException: Object reference not set to an instance of an object.
at PX.Api.ScreenUtils.GetScreenInfo(String screenId, Boolean appendDescriptors)
at PX.Api.ScreenUtils.GetScreenInfoWithServiceCommands(Boolean appendDescriptors, String screenID)
at PX.Api.Services.ScreenService.Get(String id, SchemaMode mode)
at PX.Api.Soap.Screen.ScreenGeneric.GetSchema(String screenID)
--- End of inner exception stack trace ---
Node=""
Role=""
Source=System.Web.Services
StackTrace:
   at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
   at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
   at AcumaticaTesting.AcumaticaAPI.Screen.AR303000GetSchema() in C:\Users\Eric\Documents\Visual Studio 2013\Projects\AcumaticaTesting\AcumaticaTesting\Web References\AcumaticaAPI\Reference.vb:line 671
   at AcumaticaTesting.CreateReservation.Page_Load(Object sender, EventArgs e) in C:\Users\Eric\Documents\Visual Studio 2013\Projects\AcumaticaTesting\AcumaticaTesting\CreateReservation.aspx.vb:line 59
   at System.Web.UI.Control.OnLoad(EventArgs e)
   at System.Web.UI.Control.LoadRecursive()
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
InnerException: 

这是一个已知错误,已在版本 5.20.1227 中修复。

原因是系统在构建屏幕模式时会出现竞争条件。此架构仅在应用程序池已被回收时构建,这就是为什么您主要在系统闲置一段时间后遇到它。手动回收应用程序池应该会强制发生这种行为。