Click or drag to resize

Continue as new

A common pattern is to deploy a workflow business instance for important business entities and have these run continuously there after. For example, you could implement a customer engagement workflow and then execute an instance for every customer. The workflow could wake up every so often, query the status of customer and then send marketing emails or TEXT messages as appropriate.

C#
public class CustomerInfo
{
    public long         Id { get; set; }
    public string       Email { get; set; }
    public DateTime     SignupTimeUtc { get; set; }
    public bool         WelcomeSent { get; set; }
    public DateTime?    LastMarketingPingUtc { get; set; }
}

[ActivityInterface(TaskList = "my-tasks")]
public interface ICustomerActivities : IActivity
{
    [ActivityMethod(Name = "get-customer-info")]
    Task<CustomerInfo> GetCustomerInfo(long id);

    [ActivityMethod(Name = "save-customer")]
    Task UpdateCustomerInfo(CustomerInfo customer);

    [ActivityMethod(Name = "send-email")]
    Task SendEmail(string email, string message);
}

[Activity]
public class CustomerActivities : ActivityBase, ICustomerActivities
{
    public async Task<CustomerInfo> GetCustomerInfo(long id)
    {
        // Pretend that we're getting this from a database.

        return new CustomerInfo()
        {
            Id                   = id,
            Email                = "jeff@my-company.com",
            SignupTimeUtc        = new DateTime(2019, 11, 18, 11, 4,0 , DateTimeKind.Utc),
            WelcomeSent          = false,
            LastMarketingPingUtc = null
        };
    }

    public async Task UpdateCustomerInfo(CustomerInfo customer)
    {
        // Pretend that we're persisting the customer to a database.
    }

    public async Task SendEmail(string email, string message)
    {
        // Pretend that we're sending an email here.
    }
}

[WorkflowInterface(TaskList = "my-tasks")]
public interface IEngagementWorkflow : IWorkflow
{
    [WorkflowMethod]
    Task RunAsync(long customerId);
}

[Workflow(AutoRegister = true)]
public class EngagementWorkflow : WorkflowBase, IEngagementWorkflow
{
    public async Task RunAsync(long customerId)
    {
        var stub = Workflow.NewActivityStub<ICustomerActivities>();

        while (true)
        {
            var customer = await stub.GetCustomerInfo(customerId);
            var utcNow   = await Workflow.UtcNowAsync();

            if (!customer.WelcomeSent)
            {
                await stub.SendEmail(customer.Email, "Welcome to our amazing service!");
                customer.WelcomeSent = true;
                await stub.UpdateCustomerInfo(customer);
            }
            else if (!customer.LastMarketingPingUtc.HasValue || 
                     customer.LastMarketingPingUtc.Value - utcNow >= TimeSpan.FromDays(7))
            {
                await stub.SendEmail(customer.Email, "Weekly email bugging you to buy something!");
                customer.LastMarketingPingUtc = utcNow;
                await stub.UpdateCustomerInfo(customer);
            }

            await Workflow.SleepAsync(TimeSpan.FromMinutes(30));
        }
    }
}

This example defines an activity that can read/write a customer record and can send an email and then implements a simple customer engagement workflow that sends a welcome email to the customer when one hasn't been sent yet and then sends a marketing message every seven days. The workflow is coded as a loop that sleeps for 30 minutes between iterations.

Although this will work, looping like this is a bad practice. The problem is that Cadence records each of the activity calls as well as the sleep operation to the workflow history. This workflow is designed to run indefinitely which means the history is going to grow without bounds. Cadence can support histories with 100K+ items, but you really should avoid huge histories whenever possible because:

  • Eventually you'll reach the history limit and your workflow will stop working.

  • Rescheduled workflows will need to replay the history which could take a while to complete.

  • Large histories are going stress all parts of the system including Cadence server, Cassandra, and your workflow services.

Cadence provides a way around this called continue as new. The idea is to have the workflow indicate that should continue as a new run. Cadence responds by starting a new workflow run with the same workflow ID but with a new run ID. Here's an example:

C#
[Workflow(AutoRegister = true)]
public class EngagementWorkflow : WorkflowBase, IEngagementWorkflow
{
    public async Task RunAsync(long customerId)
    {
        var stub     = Workflow.NewActivityStub<ICustomerActivities>();
        var customer = await stub.GetCustomerInfo(customerId);
        var utcNow   = await Workflow.UtcNowAsync();

        if (!customer.WelcomeSent)
        {
            await stub.SendEmail(customer.Email, "Welcome to our amazing service!");
            customer.WelcomeSent = true;
            await stub.UpdateCustomerInfo(customer);
        }
        else if (!customer.LastMarketingPingUtc.HasValue ||
                 customer.LastMarketingPingUtc.Value - utcNow >= TimeSpan.FromDays(7))
        {
            await stub.SendEmail(customer.Email, "Weekly email bugging you to buy something!");
            customer.LastMarketingPingUtc = utcNow;
            await stub.UpdateCustomerInfo(customer);
        }

        await Workflow.SleepAsync(TimeSpan.FromMinutes(30));
        await Workflow.ContinueAsNewAsync(customerId);
    }
}

We've refactored the workflow by removing the loop and replacing it with a call to ContinueAsNewAsync, passing the original arguments to the new run. Cadence will start a new workflow run, replacing the loop and also restarting the workflow history.

See Also

Reference