lucasalexander / AlexanderDevelopment.ProcessRunner

This is an open source Dynamics CRM solution for scheduling and executing recurring workflows
http://alexanderdevelopment.net/
Apache License 2.0
24 stars 8 forks source link

Recurring Process Triggering Work Flow More Than Once #1

Open 2Smooth opened 6 years ago

2Smooth commented 6 years ago

A Recurring Process I setup to trigger a workflow on a daily basis is often triggering the workflow more than once. Basically, the Recurring Process finds all the contacts whose Next Home Anniversary is today. It triggers a workflow called ABPR: Home Anniversary that adds some other logic, calls a Child Workflow that sends the email and then updates Next Home Anniversary to next year. I have occasionally seen more than one email sent to the same Contact. I updated to your latest version and ran some tests with 2 test Contacts. Looking at the Process Sessions for ABPR: Home Anniversary, in the latest run it was triggered 3 times for each Contact. Attached is the screen shot of the Process Sessions. I will change the logic in ABPR: Home Anniversary to update the Next Home Anniversary prior to calling the child workflow. I hope that will minimize the impact on my clients but doesn't solve the root problem. This is a great solution that is very useful. I hope there is a correction that can be made. error - recurring process triggers multiple times

lucasalexander commented 6 years ago

Did you by any chance manually change the next run date on your recurring process record after you initially created it? That can result in duplicate executions being triggered. Also, can you verify you have just a single recurring process record that calls your main workflow?

Finally, can you uncheck the option to automatically delete completed workflow jobs on the "Recurring workflow runner" workflow and see how many process sessions it fires every day?

2Smooth commented 6 years ago

Yes, I did change the next run date several times in the process of testing it. Is there a way to delete the duplicate executions? Yes, I only have a single Recurring Process calling the main workflow. I already have unchecked to automatically delete workflows. That was the screen shot I posted. These emails go to clients so I stopped sending them out and set up a couple test Contacts with my email address. It was set to send out daily and since I didn’t want to wait that long, I just kept changing the next run date and time. The last run gave me 14 duplicate executions. Sounds like I have been making it worse. My questions are:

  1. Is there a way to delete the duplicate executions? I suppose I could delete and recreate the Recurring Process.
  2. How do you change the run time without creating a duplicate execution? Thank you

Paul

From: Lucas Alexander notifications@github.com Sent: March 25, 2018 5:07 PM To: lucasalexander/AlexanderDevelopment.ProcessRunner AlexanderDevelopment.ProcessRunner@noreply.github.com Cc: Paul Talbot talbot@bizmax.ca; Author author@noreply.github.com Subject: Re: [lucasalexander/AlexanderDevelopment.ProcessRunner] Recurring Process Triggering Work Flow More Than Once (#1)

Did you by any chance manually change the next run date on your recurring process record after you initially created it? That can result in duplicate executions being triggered. Also, can you verify you have just a single recurring process record that calls your main workflow?

Finally, can you uncheck the option to automatically delete completed workflow jobs on the "Recurring workflow runner" workflow and see how many process sessions it fires every day?

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHubhttps://github.com/lucasalexander/AlexanderDevelopment.ProcessRunner/issues/1#issuecomment-376011349, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AaiqhCq3T-ES1aTkt8KiXYuu0Ek44Kh-ks5tiCMOgaJpZM4S5yvn.

lucasalexander commented 6 years ago

On the completed workflows, can you show the process sessions associated with the recurring process record? That's where you should see duplicate waiting workflows if there are any.

  1. If there are duplicate waiting workflows, you should be able to just delete them.
  2. Right now I don't think there's a way to prevent duplicates from spawning on a manual change, so I would just advise checking for duplicates after any manual changes.
2Smooth commented 6 years ago

Lucas,

That is straight forward. Thanks for your reply.

Paul

From: Lucas Alexander notifications@github.com Sent: March 26, 2018 10:15 AM To: lucasalexander/AlexanderDevelopment.ProcessRunner AlexanderDevelopment.ProcessRunner@noreply.github.com Cc: Paul Talbot talbot@bizmax.ca; Author author@noreply.github.com Subject: Re: [lucasalexander/AlexanderDevelopment.ProcessRunner] Recurring Process Triggering Work Flow More Than Once (#1)

On the completed workflows, can you show the process sessions associated with the recurring process record? That's where you should see duplicate waiting workflows if there are any.

  1. If there are duplicate waiting workflows, you should be able to just delete them.
  2. Right now I don't think there's a way to prevent duplicates from spawning on a manual change, so I would just advise checking for duplicates after any manual changes.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHubhttps://github.com/lucasalexander/AlexanderDevelopment.ProcessRunner/issues/1#issuecomment-376222997, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AaiqhFyvukTyqoPfMDg97tBV_RXNq2NIks5tiRP6gaJpZM4S5yvn.

donschia commented 5 years ago

Lucas, Great project! I have a suggestion that prevents duplicate triggering. As you've noted, your approach will always retrigger the main workflow and queue up a new instance every time you change the underlying Recurring process record.

My solution was to insert a CancelWaitingWorkflowInstances custom workflow step at the top of the workflow that stops any duplicate waiting workflow instances. The custom step ensures you only have one waiting workflow with the indicated name running at any given time against each record. I pasted in a copy in here if you are interested in incorporating it. But it is super useful as a separate Workflow step because you can use it in lots of other places. The only trick to to remember that the workflow name is important.

I borrowed directly from this post with a few tweaks to make allow testing and running outside of workflow: https://community.dynamics.com/crm/b/mscrmshop/archive/2012/10/29/workflow-assembly-to-cancel-waiting-workflows-part-1

Simply insert the custom step at the top of your "Workflow processes runner" workflow: image

Thanks again Lucas! Don Schiavone


/ CancelWaitingWorkflowInstances Custom Workflow Step /

using System; using System.Globalization; using System.Activities; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Workflow; using Microsoft.Xrm.Sdk.Query;

namespace Schiavone.Workflow.Utilities { public class CancelWaitingWorkflowInstances : WorkFlowActivityBase { protected override void Execute(CodeActivityContext context) { if (context == null) { throw new ArgumentNullException("serviceProvider"); }

        // Construct the Local plug-in context.
        LocalWorkflowContext localcontext = new LocalWorkflowContext(context);
        localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Entered {0}.Execute()", this.DisplayName));
        ExecuteCRMWorkFlowActivity(context, localcontext);
    }
    public override void ExecuteCRMWorkFlowActivity(CodeActivityContext executionContext, LocalWorkflowContext context)
    {
        //Get the tracing service
        ITracingService tracingService = executionContext.GetExtension<ITracingService>();

        //Get the context
        IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
        IOrganizationService service = serviceFactory.CreateOrganizationService(context.WorkflowExecutionContext.UserId);

        Guid workflowExecutionContextOperationId = context.WorkflowExecutionContext.PrimaryEntityId;
        //get workflow name from the input property
        string sWorkflowName = workflowName.Get<string>(executionContext);

        Guid relatedEntityId = context.WorkflowExecutionContext.PrimaryEntityId;
        bool bCancelThisInstanceToo = cancelThisInstanceToo.Get<bool>(executionContext);
        //Get a service context to use linq queries and ...
        //var ServiceContext = new OrganizationServiceContext(service);
        // var ServiceContext = context.OrganizationService;

        tracingService.Trace(string.Format("workflowExecutionContextOperationId={0}, sWorkflowName={1}, relatedEntityId={2}, bCancelThisInstanceToo={3} ",
            workflowExecutionContextOperationId, sWorkflowName, relatedEntityId, bCancelThisInstanceToo));

        int count = DoCancelWaitingWorkflowInstances(workflowExecutionContextOperationId, sWorkflowName, relatedEntityId, bCancelThisInstanceToo, service);

        tracingService.Trace(string.Format("numberOfCancelledWorkflows={0}", numberOfCancelledWorkflows));
        numberOfCancelledWorkflows.Set(executionContext, count);

        // Throw an exception so we can see the Trace (debugging only)
        var shouldForceException = forceException.Get(executionContext);
        if (shouldForceException) throw new InvalidPluginExecutionException();

    }
    public static int DoCancelWaitingWorkflowInstances(Guid workflowExecutionContextOperationId, string sWorkflowName,
     Guid relatedEntityId, bool bCancelThisInstanceToo, IOrganizationService service)
    {
        Guid primaryEntityId = relatedEntityId;

        // http://mscrmshop.blogspot.com/2012/10/workflow-assembly-to-cancel-waiting.html
        int iCancelledCount = 0;
        try
        {
            QueryExpression query = new QueryExpression("asyncoperation");

            // get columns
            ColumnSet cols = new ColumnSet(new string[] { "asyncoperationid", "statecode" });

            // conditions
            // name of the workflow -input property
            ConditionExpression c1 = new ConditionExpression("name", ConditionOperator.Equal, sWorkflowName);
            // entity id
            ConditionExpression c2 = new ConditionExpression("regardingobjectid", ConditionOperator.Equal, primaryEntityId);
            // statecode of waiting
            ConditionExpression c3 = new ConditionExpression("statecode", ConditionOperator.Equal, 1);

            //create the filter
            FilterExpression filter = new FilterExpression();
            filter.FilterOperator = LogicalOperator.And;
            filter.AddCondition(c1);
            filter.AddCondition(c2);
            filter.AddCondition(c3);

            query.ColumnSet = cols;
            query.Criteria.AddFilter(filter);

            //get the collection of results
            EntityCollection colResults = service.RetrieveMultiple(query);

            foreach (Entity async in colResults.Entities)
            {
                Entity e = async;
                // If we want to preserve this particular workflow instance, then skip the part where we cancel it
                if (!bCancelThisInstanceToo && (e.Id == workflowExecutionContextOperationId))
                    continue;

                //change the status of the system job
                e["statecode"] = new OptionSetValue(3); //cancelled

                //update the object             
                service.Update(e);
                iCancelledCount++;
            }

        }//end try
        // Catch any service fault exceptions that Microsoft Dynamics CRM throws.
        catch (Exception ex)
        {
            // You can handle an exception here or pass it back to the calling method.
            throw;
        }

        return iCancelledCount;
    }

    [Input("Workflow Name")]
    [Default("workflow name")]
    public InArgument<string> workflowName { get; set; }

    [Input("Cancel this workflow instance too?")]
    [Default("false")]
    public InArgument<Boolean> cancelThisInstanceToo { get; set; }

    [Input("Force Exception? (To View Tracing)")]
    [Default("false")]
    public InArgument<Boolean> forceException { get; set; }

    [Output("Number of cancelled workflows")]
    public OutArgument<int> numberOfCancelledWorkflows { get; set; }

}

}