Fix infinitely waiting Alterations Workflow (#6561)
* Refactor TenantId string handling and nullability checks. Moved `StringExtensions` to a common module for reuse. Updated tenant-related logic to utilize null-safe string extensions, enhancing consistency and simplifying nullability handling across the codebase. * Set StrictMode to false by default in ObjectConverter Modified the default value of StrictMode to `false` to enable the original flexible behavior. Developers can opt into strict mode by explicitly setting it to `true`. This change aims to enhance backward compatibility and minimize unexpected strict conversions. * Add tenant ID retrieval to ElsaDbContextBase constructor Retrieve the current tenant ID if available using ITenantAccessor and assign it to the TenantId property. This ensures proper handling of multi-tenancy scenarios in the database context initialization. * Disable DbContext pooling, manual OTEL instrumentation, and strict mode. DbContext pooling is turned off to prevent potential issues with shared context instances. Manual OpenTelemetry instrumentation is disabled to rely on automatic instrumentation instead. Strict mode is also disabled to allow more flexibility in object conversion. * Fix infinitely waiting Alterations Workflow Replaced workflow dispatch logic with BookmarkQueue and StimulusHasher for triggering workflows. This fixes the issue where the Alterations workflow would signal completion while a later step awaits a completion bookmark. The Bookmark Queue now handles this.
This commit is contained in:
parent
286bdb22b2
commit
e2288d0b34
|
|
@ -86,7 +86,7 @@ using StackExchange.Redis;
|
|||
|
||||
// ReSharper disable RedundantAssignment
|
||||
const PersistenceProvider persistenceProvider = PersistenceProvider.EntityFrameworkCore;
|
||||
const bool useDbContextPooling = true;
|
||||
const bool useDbContextPooling = false;
|
||||
const bool useHangfire = false;
|
||||
const bool useQuartz = true;
|
||||
const bool useMassTransit = true;
|
||||
|
|
@ -105,9 +105,9 @@ const bool useTenantsFromConfiguration = true;
|
|||
const bool useSecrets = false;
|
||||
const bool disableVariableWrappers = false;
|
||||
const bool disableVariableCopying = false;
|
||||
const bool useManualOtelInstrumentation = true;
|
||||
const bool useManualOtelInstrumentation = false;
|
||||
|
||||
ObjectConverter.StrictMode = true; // Default.
|
||||
ObjectConverter.StrictMode = false;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var services = builder.Services;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
using Elsa.Alterations.Bookmarks;
|
||||
using Elsa.Alterations.Core.Notifications;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Helpers;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Contracts;
|
||||
using Elsa.Workflows.Runtime.Requests;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Alterations.Handlers;
|
||||
|
|
@ -13,7 +12,7 @@ namespace Elsa.Alterations.Handlers;
|
|||
/// Handles <see cref="AlterationPlanCompleted"/> notifications and triggers any workflows that are waiting for the plan to complete.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class AlterationPlanCompletedHandler(IWorkflowDispatcher workflowDispatcher) : INotificationHandler<AlterationPlanCompleted>
|
||||
public class AlterationPlanCompletedHandler(IBookmarkQueue bookmarkQueue, IStimulusHasher stimulusHasher) : INotificationHandler<AlterationPlanCompleted>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task HandleAsync(AlterationPlanCompleted notification, CancellationToken cancellationToken)
|
||||
|
|
@ -21,7 +20,12 @@ public class AlterationPlanCompletedHandler(IWorkflowDispatcher workflowDispatch
|
|||
// Trigger any workflow instances that are waiting for the plan to complete.
|
||||
var planId = notification.Plan.Id;
|
||||
var bookmarkPayload = new AlterationPlanCompletedPayload(planId);
|
||||
var triggerRequest = new DispatchTriggerWorkflowsRequest(ActivityTypeNameHelper.GenerateTypeName<Activities.AlterationPlanCompleted>(), bookmarkPayload);
|
||||
await workflowDispatcher.DispatchAsync(triggerRequest, cancellationToken);
|
||||
var activityTypeName = ActivityTypeNameHelper.GenerateTypeName<Activities.AlterationPlanCompleted>();
|
||||
var item = new NewBookmarkQueueItem
|
||||
{
|
||||
ActivityTypeName = activityTypeName,
|
||||
StimulusHash = stimulusHasher.Hash(activityTypeName, bookmarkPayload)
|
||||
};
|
||||
await bookmarkQueue.EnqueueAsync(item, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ public class AlterationPlanManager(IAlterationPlanStore planStore, IAlterationJo
|
|||
var jobFilter = new AlterationJobFilter
|
||||
{
|
||||
PlanId = planId,
|
||||
Statuses = new[] { AlterationJobStatus.Pending, AlterationJobStatus.Running }
|
||||
Statuses = [AlterationJobStatus.Pending, AlterationJobStatus.Running]
|
||||
};
|
||||
|
||||
return await jobStore.CountAsync(jobFilter, cancellationToken) == 0;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public class DefaultAlterationPlanScheduler : IAlterationPlanScheduler
|
|||
var workflowGraph = await _workflowDefinitionService.FindWorkflowGraphAsync(definitionId, VersionOptions.Published, cancellationToken);
|
||||
|
||||
if (workflowGraph == null)
|
||||
throw new Exception($"Workflow definition with ID '{definitionId}' not found");
|
||||
throw new($"Workflow definition with ID '{definitionId}' not found");
|
||||
|
||||
var serializedPlan = _jsonSerializer.Serialize(planParams);
|
||||
var request = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public class Tenant : Entity
|
|||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration.
|
||||
|
|
@ -22,7 +22,7 @@ public class Tenant : Entity
|
|||
|
||||
public static readonly Tenant Default = new()
|
||||
{
|
||||
Id = string.Empty,
|
||||
Id = null!,
|
||||
Name = "Default"
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Common.Multitenancy;
|
||||
|
|
@ -19,7 +20,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop
|
|||
public async Task<Tenant?> FindAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
|
||||
return dictionary.TryGetValue(id, out var tenant) ? tenant : null;
|
||||
return dictionary.TryGetValue(id.EmptyIfNull(), out var tenant) ? tenant : null;
|
||||
}
|
||||
|
||||
public async Task<Tenant?> FindAsync(TenantFilter filter, CancellationToken cancellationToken = default)
|
||||
|
|
@ -31,7 +32,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop
|
|||
public async Task<Tenant> GetAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
|
||||
return dictionary[id];
|
||||
return dictionary[id.EmptyIfNull()];
|
||||
}
|
||||
|
||||
public async Task<Tenant> GetAsync(TenantFilter filter, CancellationToken cancellationToken = default)
|
||||
|
|
@ -76,7 +77,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop
|
|||
var tenantsProvider = scope.ServiceProvider.GetRequiredService<ITenantsProvider>();
|
||||
var currentTenants = await GetTenantsDictionaryAsync(cancellationToken);
|
||||
var currentTenantIds = currentTenants.Keys;
|
||||
var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id);
|
||||
var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id.EmptyIfNull());
|
||||
var newTenantIds = newTenants.Keys;
|
||||
var removedTenantIds = currentTenantIds.Except(newTenantIds).ToArray();
|
||||
var addedTenantIds = newTenantIds.Except(currentTenantIds).ToArray();
|
||||
|
|
@ -129,21 +130,21 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop
|
|||
private async Task RegisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var scope = tenantScopeFactory.CreateScope(tenant);
|
||||
_tenantsDictionary![tenant.Id] = tenant;
|
||||
_tenantsDictionary![tenant.Id.EmptyIfNull()] = tenant;
|
||||
_tenantScopesDictionary![tenant] = scope;
|
||||
|
||||
using (tenantAccessor.PushContext(tenant))
|
||||
await tenantEvents.TenantActivatedAsync(new TenantActivatedEventArgs(tenant, scope, cancellationToken));
|
||||
await tenantEvents.TenantActivatedAsync(new(tenant, scope, cancellationToken));
|
||||
}
|
||||
|
||||
private async Task UnregisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_tenantScopesDictionary!.Remove(tenant, out var scope))
|
||||
{
|
||||
_tenantsDictionary!.Remove(tenant.Id, out _);
|
||||
_tenantsDictionary!.Remove(tenant.Id.EmptyIfNull(), out _);
|
||||
|
||||
using (tenantAccessor.PushContext(tenant))
|
||||
await tenantEvents.TenantDeactivatedAsync(new TenantDeactivatedEventArgs(tenant, scope, cancellationToken));
|
||||
await tenantEvents.TenantDeactivatedAsync(new(tenant, scope, cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ public class TenantFilter
|
|||
/// <summary>
|
||||
/// Gets or sets the tenant ID to filter for.
|
||||
/// </summary>
|
||||
public string? Id { get; set; }
|
||||
public string Id { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Applies the filter to the specified queryable.
|
||||
|
|
@ -20,7 +20,7 @@ public class TenantFilter
|
|||
/// <returns>The filtered queryable.</returns>
|
||||
public IQueryable<Tenant> Apply(IQueryable<Tenant> queryable)
|
||||
{
|
||||
if (Id != null) queryable = queryable.Where(x => x.Id == Id);
|
||||
queryable = queryable.Where(x => x.Id == Id);
|
||||
|
||||
return queryable;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Elsa.Common.Entities;
|
||||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -43,6 +45,12 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema
|
|||
|
||||
// ReSharper disable once VirtualMemberCallInConstructor
|
||||
Schema = !string.IsNullOrWhiteSpace(_elsaDbContextOptions?.SchemaName) ? _elsaDbContextOptions.SchemaName : ElsaSchema;
|
||||
|
||||
var tenantAccessor = serviceProvider.GetService<ITenantAccessor>();
|
||||
var tenantId = tenantAccessor?.Tenant?.Id;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tenantId))
|
||||
TenantId = tenantId.NullIfEmpty();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Common.Entities;
|
||||
using Elsa.Extensions;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace Elsa.EntityFrameworkCore.EntityHandlers;
|
||||
|
|
@ -12,7 +13,7 @@ public class ApplyTenantId : IEntitySavingHandler
|
|||
public ValueTask HandleAsync(ElsaDbContextBase dbContext, EntityEntry entry, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (entry.Entity is Entity entity)
|
||||
entity.TenantId = dbContext.TenantId;
|
||||
entity.TenantId = dbContext.TenantId.NullIfEmpty();
|
||||
|
||||
return default;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Linq.Expressions;
|
||||
using Elsa.Common.Entities;
|
||||
using Elsa.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
|
|
@ -17,8 +18,9 @@ public class SetTenantIdFilter : IEntityModelCreatingHandler
|
|||
if (!entityType.ClrType.IsAssignableTo(typeof(Entity)))
|
||||
return;
|
||||
|
||||
var tenantId = dbContext.TenantId.NullIfEmpty();
|
||||
var parameter = Expression.Parameter(entityType.ClrType);
|
||||
Expression<Func<Entity, bool>> filterExpr = entity => dbContext.TenantId == entity.TenantId;
|
||||
Expression<Func<Entity, bool>> filterExpr = entity => entity.TenantId == tenantId;
|
||||
var body = ReplacingExpressionVisitor.Replace(filterExpr.Parameters[0], parameter, filterExpr.Body);
|
||||
var lambdaExpression = Expression.Lambda(body, parameter);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions =
|
|||
/// </summary>
|
||||
public static class ObjectConverter
|
||||
{
|
||||
public static bool StrictMode = true; // Set to false to revert to original flexible behavior.
|
||||
public static bool StrictMode = false; // Set to true to opt into strict mode.
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert the source value into the destination type.
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ public class BookmarkQueueSignaler : IBookmarkQueueSignaler
|
|||
lock (_lock)
|
||||
{
|
||||
// Reset the TCS for the next wait
|
||||
_tcs = new TaskCompletionSource<object?>();
|
||||
_tcs = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue