异步服务多次处理实体

Async Service processes entity multiple times

我的异步服务有一个非常奇怪的行为。 故事是: 有一个插件,在 Lead Create 上触发。插件本身的目的是创建线索的自定义枚举。该插件从保留数字的自动编号实体中的字段中获取最后一个数字。然后插件将自动编号实体的编号字段递增 1,并将获得的编号分配给 Lead。

问题如下: 当我 运行 大量创建潜在客户时(编号的碰撞测试)例如400,自动编号计数器从 0 开始,当所有线索都被处理后,我的自动编号计数器以 ~770 的值结束,比估计的 400 多得多。

根据经验,我发现异步服务会多次处理相同的潜在客户。有的只有1次,有的是2-5次。

为什么会这样?

这是我的代码:

public void Execute(IServiceProvider serviceProvider)
{
    Entity target = ((Entity)context.InputParameters["Target"]);
    target["new_id"] = GetCurrentNumber(service, LEAD_AUTONUMBER);
    service.Update(target);
    return;
}

public int GetCurrentNumber(IOrganizationService service, Guid EntityType)
{
    lock (_locker)
    {
        Entity record = service.Retrieve("new_autonumbering", EntityType, new ColumnSet("new_nextnumber"));
        record["new_nextnumber"] = int.Parse(record["new_nextnumber"].ToString()) + 1;
        service.Update(record);

        return int.Parse(record["new_nextnumber"].ToString());
    }
}

更新 1: 首先,我的上下文工厂服务变量在 class 中声明,因此它们可以在一个实例中用于多个线程。

public class IdAssignerPlugin : IPlugin
{
    private static      IPluginExecutionContext context;
    private static      IOrganizationServiceFactory factory;
    private static      IOrganizationService service;

    public void Execute(IServiceProvider serviceProvider)
    {
        context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
        factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
        service = factory.CreateOrganizationService(null);
        [...]
    }
}

@HenkvanBoeijen 的评论之后,我意识到这是不安全的方式,所以我将所有声明都移到了 Execute() 方法中。

public class IdAssignerPlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));;
        IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));;
        IOrganizationService service = factory.CreateOrganizationService(null);;

        [...]
    }
}

但这并没有使我免于多次处理,尽管现在处理速度非常快。

更新 2: 在系统作业中,我还注意到在状态为 Retry Count = 0 的 11 次操作之后,其余操作具有 Retry Count = 1,在 16 次之后它是 Retry Count = 2,等等

(在此测试中,我以编程方式创建了 20 个潜在客户,在分配后,计数器显示 last number = 33,如果我总结所有 retry count 值,则结果为 33,这类似于 last number 在自动编号中)

我不能告诉你为什么它被多次处理,除非你没有从你的 IServiceProvider 获得你的上下文和你的服务,你做错了。

防止这种情况发生的一个简单方法是在您的插件首次启动时检查 SharedPluginVariable。如果存在则退出,如果不存在则添加shared插件变量。我默认为所有插件执行此操作,以防止使用触发自身的插件无限循环。

/// <summary>
/// Allows Plugin to trigger itself.  Delete Messge Types always return False 
/// since you can't delete something twice, all other message types return true 
/// if the execution key is found in the shared parameters.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
protected virtual bool PreventRecursiveCall(IExtendedPluginContext context)
{
    if (context.Event.Message == MessageType.Delete)
    {
        return false;
    }

    var sharedVariables = context.SharedVariables;
    var key = $"{context.PluginTypeName}|{context.Event.MessageName}|{context.Event.Stage}|{context.PrimaryEntityId}";
    if (context.GetFirstSharedVariable<int>(key) > 0)
    {
        return true;
    }

    sharedVariables.Add(key, 1);
    return false;
}

我发现了问题。 在对以下所有任务进行 11 次尝试后,CRM 一直显示插件错误(Generic SQL Error,没有任何附加信息,我想这可能是由过载引起的,某种 SQL Timeout Error)。

crm 的事件执行管道如下:

  1. Event happened.
  2. Event listener cathes event and sends it to the handler based on following parameters sync-async & pre-operation - post-operation (async - post-operation in my case)
  3. Then event goes into Async Queue Agent which decides when to execute the plugin.
  4. Async Queue Agent runs related to this event plugin.
  5. Plugin does his work and then returns 0 (for e.g.) when succeeded or 1 when failed.
  6. If 0, Async Queue Agent closes current pipeline with the status of Succeeded and sends notification to CRM core.

错误可能是在更新代码(第 5 步)实体内的 Autonumbering 之后出现的,但在完成状态为成功的任务(第 6 步)之前。

因此,由于此错误,CRM 运行 任务再次具有相同的 InputParameters。

我的 CRM 服务器不是很重载,所以我提出了以下解决方法:

我在整个 Execute() 方法上推断我的 lock() 语句,并将更新实体请求移动到方法的末尾。

一切顺利。缺点是这种方式将我的插件变回(几乎)旧的同步,但正如我所说,我的服务器并没有过载到无法承受这个问题。

我 post 由于历史原因我的代码:

public class IdAssignerPlugin : IPlugin
{
    public const string AUTONUMBERING_ENTITY = "new_autonumber";
    public static Guid LEAD_AUTONUMBER = 
new Guid("yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy");

    static readonly object _locker = new object();

    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider.GetService(
typeof(IPluginExecutionContext));
        var factory = (IOrganizationServiceFactory)serviceProvider.GetService(
typeof(IOrganizationServiceFactory));
        var service = factory.CreateOrganizationService(null);

        if (PreventRecursiveCall(context))
        return;

        lock (_locker)
        {
            if (context.InputParameters.Contains("Target") && 
context.InputParameters["Target"] is Entity)
            {
                Entity autoNumber;
                Entity target = ((Entity)context.InputParameters["Target"]);
                if (target.LogicalName.Equals("lead",
 StringComparison.InvariantCultureIgnoreCase))
                {
                    autoNumber = GetCurrentNumber(service, LEAD_AUTONUMBER);
                    target["new_id"] = autoNumber["new_nextnumber"];
                }
                service.Update(autoNumber);
                service.Update(target);
            }
        }
        return;
    }

    public int GetCurrentNumber(IOrganizationService service, Guid EntityType)
    {
        Entity record = 
service.Retrieve(AUTONUMBERING_ENTITY, EntityType, new ColumnSet("new_nextnumber"));
        record["new_nextnumber"] = int.Parse(record["new_nextnumber"].ToString()) + 1;
        return record;
    }

    protected virtual bool PreventRecursiveCall(IPluginExecutionContext context)
    {
        if (context.SharedVariables.Contains("Fired")) return true;
        context.SharedVariables.Add("Fired", 1);
        return false;
    }
}