elsa-workflows / elsa-workflows/elsa-core
[BUG] Race condition in Bookmark Management
- Dominant language
- C#
- Stars
- 7.9k
- Forks
- 1.5k
- Avg merge
- 15h 22m
- Merged PRs (30d)
- 114
Description
## Description
An activity creates a bookmark to be suspended and later resumed.
The activity then publishes an event.
A service waits for incoming events that are potential responses to previously published events.
If an event arrives too early, the service will not find the bookmark that has already been created.
As a result, the activity is never resumed.
## Steps to Reproduce
1. **Detailed Steps**:
1. The activity creates a bookmark (line 97).
2. The activity asynchronously publishes an event (line 98).
3. The activity is suspended to be resumed later.
4. The workflow service receives the published event (line 135).
5. The workflow service retrieves all bookmarks (line 137 ff.).
6. The workflow service filters the list of bookmarks for the bookmark created by the activity (line 141).
7. The workflow service creates a DispatchWorkflowInstanceRequest for each filtered bookmark (line 142).
8. The workflow service dispatches all instances of DispatchWorkflowInstanceRequest.
If the workflow service retrieves all bookmarks immediately after receiving the incoming event, no bookmarks are found.
If the workflow service calls ```Thread.Sleep(2000);``` (line 136) after receiving the incoming event, the previously created bookmark is found.
2. **Code Snippets**:
```csharp
namespace WebApplication8;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using Elsa.Workflows.Runtime.Requests;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Concurrent;
using System.Threading;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddElsa(elsa =>
{
// Configure Management layer to use EF Core.
elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore());
// Configure Runtime layer to use EF Core.
elsa.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore());
// Default Identity features for authentication/authorization.
elsa.UseIdentity(identity =>
{
identity.TokenOptions = options => options.SigningKey = "sufficiently-large-secret-signing-key"; // This key needs to be at least 256 bits long.
identity.UseAdminUserProvider();
});
// Configure ASP.NET authentication/authorization.
elsa.UseDefaultAuthentication(auth => auth.UseAdminApiKey());
// Expose Elsa API endpoints.
elsa.UseWorkflowsApi();
// Setup a SignalR hub for real-time updates from the server.
elsa.UseRealTimeWorkflows();
// Enable HTTP activities.
elsa.UseHttp();
// Register custom activities from the application, if any.
elsa.AddActivitiesFrom();
});
// Configure CORS to allow designer app hosted on a different origin to invoke the APIs.
builder.Services.AddCors(cors => cors
.AddDefaultPolicy(policy => policy
.AllowAnyOrigin() // For demo purposes only. Use a specific origin instead.
.AllowAnyHeader()
.AllowAnyMethod()
.WithExposedHeaders("x-elsa-workflow-instance-id"))); // Required for Elsa Studio in order to support running workflows from the designer. Alternatively, you can use the `*` wildcard to expose all headers.
// Add Health Checks.
builder.Services.AddHealthChecks();
var queue = new BlockingCollection();
builder.Services.AddSingleton(new Publish(queue.Add));
builder.Services.AddSingleton(new Receive(queue.Take));
builder.Services.AddHostedService();
// Build the web application.
var app = builder.Build();
// Configure web application's middleware pipeline.
app.UseCors();
app.UseRouting(); // Required for SignalR.
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi(); // Use Elsa API endpoints.
app.UseWorkflows(); // Use Elsa middleware to handle HTTP requests mapped to HTTP Endpoint activities.
app.UseWorkflowsSignalRHubs(); // Optional SignalR integration. Elsa Studio uses SignalR to receive real-time updates from the server.
app.Run();
}
public record Publish(Action Action);
public record Receive(Func Func);
public record BookmarkPayload(Guid guid);
public sealed class MyActivity : Activity
{
protected override async ValueTask ExecuteAsync(
ActivityExecutionContext context)
{
var publish = context.GetRequiredService();
var guid = Guid.NewGuid();
var payload = new BookmarkPayload(guid);
context.CreateBookmark(payload, ResumeAsync);
publish.Action(guid);
await Task.Run(() => { });
}
private async ValueTask ResumeAsync(
ActivityExecutionContext context) =>
await context.CompleteActivityAsync();
}
public sealed class WorkflowService(
IServiceScopeFactory factory,
Receive receive) : BackgroundService
{
protected override Task ExecuteAsync(
CancellationToken stoppingToken)
{
var serviceProvider = factory.CreateScope().ServiceProvider;
var bookmarkStore = serviceProvider.GetRequiredService();
var workflowDispatcher = serviceProvider.GetRequiredService();
var result = new Task(() => Execute(
receive.Func,
bookmarkStore,
workflowDispatcher,
stoppingToken));
result.Start();
return result;
}
private static async void Execute(
Func receive,
IBookmarkStore bookmarkStore,
IWorkflowDispatcher workflowDispatcher,
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var guid = receive();
// Thread.Sleep(2000);
var allBookmarks = await bookmarkStore.FindManyAsync(
new BookmarkFilter(),
stoppingToken);
var requests = allBookmarks
.Where(b => b.Payload is BookmarkPayload)
.Select(MakeDispatchWorkflowInstanceRequest);
foreach (var request in requests)
{
await workflowDispatcher.DispatchAsync(request, stoppingToken);
}
}
}
private static DispatchWorkflowInstanceRequest MakeDispatchWorkflowInstanceRequest(
StoredBookmark bookmark) =>
new DispatchWorkflowInstanceRequest(bookmark.WorkflowInstanceId)
{
BookmarkId = bookmark.BookmarkId,
};
}
}
```
3. **Attachments**:
- **Workflow JSON**:
[workflow.json](https://github.com/user-attachments/files/15816024/workflow.json)
- **Sample Project**:
[Program.zip](https://github.com/user-attachments/files/15816051/Program.zip)
4. **Reproduction Rate**: 100 %
5. **Video/Screenshots**: see below
6. **Additional Configuration**: none
## Expected Behavior
A bookmark can be found as soon as it has been created.
## Actual Behavior
Whether a bookmark is found is time-dependent (race condition).
## Screenshots

## Environment
- **Elsa Package Version**: 3.2.0-rc1
- **Operating System**: Microsoft Windows 10 Enterprise, 10.0.19045 Build 19045
- **Browser and Version**: Microsoft Edge for Business, 125.0.2535.92
## Log Output
none
## Troubleshooting Attempts
```csharp
Thread.Sleep(2000);
```
## Additional Context
none
## Related Issues
none
Contributor guide
Assessment
This issue has not been assessed yet.