* Avoid null endpoint DTO metadata in tests * Enforce console logs hub read permission * Remove unused console logs hub import * Support mapped endpoint metadata in auth tests * Reduce console log capture throughput impact * Address Copilot console logs review * Refactor task scheduling to support tenant-level background work and enhance logging functionality. * Introduce ConsoleStreamHook for stdout/stderr tee and enhance logging validation. Adjust test cases and startup warnings for distributed lock provider usage. * Refactor console logging pipeline with capture optimization and new ConsoleLogsHost; update tests accordingly. * Add Ansi SGR parser for console logs and associated unit tests * Remove ANSI color renderings and parsers; integrate ConsoleLogScopeAccessor for improved logging context with workflow instance ID support. * Address console logs code quality feedback * Address PR review feedback * Preserve console logs extension points * Stabilize console logs host lifecycle * Address final automated review comments * Tighten console log capture shutdown * Address console log review feedback * Address follow-up review feedback * Cover final review feedback * Avoid recursive console provider initialization * Guard console host lease shutdown * Preserve console log scope and provider lifetime * Correlate console log scope fallback * Tighten console scope correlation * Expose host services during provider construction * Redact ANSI-normalized console lines * Remove `ConsoleCaptureTee` and related services and tests * Use pipeline contributors for console log context * Update CShells package versions to 0.0.24-preview.132 * Filter live console logs by workflow instance * Enhance console logging with activity execution metadata and extend test coverage. * Address console logs stream consumption comment * Add diagnostics OpenTelemetry backend * Introduce dedicated workflow JSON type registry and hardening This change addresses GitHub issue #7541 by establishing a separate type registry (`IWorkflowJsonTypeRegistry`) for workflow JSON serialization. This decouples workflow type resolution from expression type aliases, enforcing a strict trust boundary. Key aspects: - New workflow JSON emits preferred aliases for registered types. - Existing persisted workflows can be loaded via registered legacy names. - Unknown, abstract, interface, open generic, or inappropriate collection types are rejected during deserialization, enhancing security. - Public APIs (e.g., incident strategies) now expose consistent workflow JSON type identifiers. This ensures secure, predictable, and backward-compatible handling of types within workflow definitions and payloads. * Remove unused project references and streamline console log endpoint * Move serialization type aliases to Elsa.Common * Update serialization integration fixtures for aliases * Stabilize missing rate limiter policy test
234 lines
10 KiB
C#
234 lines
10 KiB
C#
using System.Runtime.CompilerServices;
|
|
using Elsa.Common.DistributedHosting;
|
|
using Elsa.Expressions.Contracts;
|
|
using Elsa.Extensions;
|
|
using Elsa.Mediator.Contracts;
|
|
using Elsa.Workflows.Activities;
|
|
using Elsa.Workflows.Helpers;
|
|
using Elsa.Workflows.Management;
|
|
using Elsa.Workflows.Management.Entities;
|
|
using Elsa.Workflows.Runtime.Comparers;
|
|
using Elsa.Workflows.Runtime.Entities;
|
|
using Elsa.Workflows.Runtime.Filters;
|
|
using Elsa.Workflows.Runtime.Notifications;
|
|
using Medallion.Threading;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Open.Linq.AsyncExtensions;
|
|
using Elsa.Common.Serialization;
|
|
|
|
namespace Elsa.Workflows.Runtime;
|
|
|
|
/// <inheritdoc />
|
|
public class TriggerIndexer : ITriggerIndexer
|
|
{
|
|
private readonly IActivityVisitor _activityVisitor;
|
|
private readonly IWorkflowDefinitionService _workflowDefinitionService;
|
|
private readonly IExpressionEvaluator _expressionEvaluator;
|
|
private readonly IIdentityGenerator _identityGenerator;
|
|
private readonly ITriggerStore _triggerStore;
|
|
private readonly IActivityRegistry _activityRegistry;
|
|
private readonly INotificationSender _notificationSender;
|
|
private readonly IServiceProvider _serviceProvider;
|
|
private readonly IStimulusHasher _hasher;
|
|
private readonly IDistributedLockProvider _distributedLockProvider;
|
|
private readonly WorkflowTriggerEqualityComparer _triggerEqualityComparer;
|
|
private readonly DistributedLockingOptions _lockingOptions;
|
|
private readonly ILogger _logger;
|
|
|
|
/// <summary>
|
|
/// Constructor.
|
|
/// </summary>
|
|
public TriggerIndexer(
|
|
IActivityVisitor activityVisitor,
|
|
IWorkflowDefinitionService workflowDefinitionService,
|
|
IExpressionEvaluator expressionEvaluator,
|
|
IIdentityGenerator identityGenerator,
|
|
ITriggerStore triggerStore,
|
|
IActivityRegistry activityRegistry,
|
|
INotificationSender notificationSender,
|
|
IServiceProvider serviceProvider,
|
|
IStimulusHasher hasher,
|
|
IDistributedLockProvider distributedLockProvider,
|
|
ISerializationTypeRegistry workflowJsonTypeRegistry,
|
|
IOptions<DistributedLockingOptions> lockingOptions,
|
|
ILogger<TriggerIndexer> logger)
|
|
{
|
|
_activityVisitor = activityVisitor;
|
|
_expressionEvaluator = expressionEvaluator;
|
|
_identityGenerator = identityGenerator;
|
|
_triggerStore = triggerStore;
|
|
_activityRegistry = activityRegistry;
|
|
_notificationSender = notificationSender;
|
|
_serviceProvider = serviceProvider;
|
|
_hasher = hasher;
|
|
_distributedLockProvider = distributedLockProvider;
|
|
_triggerEqualityComparer = new WorkflowTriggerEqualityComparer(workflowJsonTypeRegistry);
|
|
_lockingOptions = lockingOptions.Value;
|
|
_logger = logger;
|
|
_workflowDefinitionService = workflowDefinitionService;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task DeleteTriggersAsync(TriggerFilter filter, CancellationToken cancellationToken = default)
|
|
{
|
|
var triggers = (await _triggerStore.FindManyAsync(filter, cancellationToken)).ToList();
|
|
var workflowDefinitionVersionIds = triggers.Select(x => x.WorkflowDefinitionVersionId).Distinct().ToList();
|
|
|
|
foreach (var workflowDefinitionVersionId in workflowDefinitionVersionIds)
|
|
{
|
|
try
|
|
{
|
|
var workflowGraph = await _workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionVersionId, cancellationToken);
|
|
|
|
if (workflowGraph == null)
|
|
continue;
|
|
|
|
await DeleteTriggersAsync(workflowGraph.Workflow, cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to load workflow graph for workflow definition version {WorkflowDefinitionVersionId}. Skipping trigger deletion for this workflow.", workflowDefinitionVersionId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IndexedWorkflowTriggers> IndexTriggersAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
|
|
{
|
|
var workflowGraph = await _workflowDefinitionService.MaterializeWorkflowAsync(definition, cancellationToken);
|
|
return await IndexTriggersAsync(workflowGraph.Workflow, cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IndexedWorkflowTriggers> IndexTriggersAsync(Workflow workflow, CancellationToken cancellationToken = default)
|
|
{
|
|
// Use distributed lock to prevent concurrent trigger indexing race conditions
|
|
var lockResource = $"trigger-indexer:{workflow.Identity.DefinitionId}";
|
|
await using (await _distributedLockProvider.AcquireLockAsync(lockResource, _lockingOptions.LockAcquisitionTimeout, cancellationToken))
|
|
{
|
|
return await IndexTriggersInternalAsync(workflow, cancellationToken);
|
|
}
|
|
}
|
|
|
|
private async Task<IndexedWorkflowTriggers> IndexTriggersInternalAsync(Workflow workflow, CancellationToken cancellationToken)
|
|
{
|
|
// Get current triggers
|
|
var currentTriggers = await GetCurrentTriggersAsync(workflow.Identity.DefinitionId, cancellationToken).ToList();
|
|
|
|
// Collect new triggers **if the workflow is published**.
|
|
var newTriggers = workflow.Publication.IsPublished
|
|
? await GetTriggersInternalAsync(workflow, cancellationToken).ToListAsync(cancellationToken)
|
|
: new(0);
|
|
|
|
// Diff triggers.
|
|
var diff = Diff.For(currentTriggers, newTriggers, _triggerEqualityComparer);
|
|
|
|
// Replace triggers for the specified workflow.
|
|
await _triggerStore.ReplaceAsync(diff.Removed, diff.Added, cancellationToken);
|
|
|
|
var indexedWorkflow = new IndexedWorkflowTriggers(workflow, diff.Added, diff.Removed, diff.Unchanged);
|
|
|
|
// Publish event.
|
|
await _notificationSender.SendAsync(new WorkflowTriggersIndexed(indexedWorkflow), cancellationToken);
|
|
return indexedWorkflow;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IEnumerable<StoredTrigger>> GetTriggersAsync(Workflow workflow, CancellationToken cancellationToken)
|
|
{
|
|
return await GetTriggersInternalAsync(workflow, cancellationToken).ToListAsync(cancellationToken);
|
|
}
|
|
|
|
private async Task DeleteTriggersAsync(Workflow workflow, CancellationToken cancellationToken = default)
|
|
{
|
|
var emptyTriggerList = new List<StoredTrigger>(0);
|
|
var currentTriggers = await GetCurrentTriggersAsync(workflow.Identity.DefinitionId, cancellationToken).ToList();
|
|
var diff = Diff.For(currentTriggers, emptyTriggerList, _triggerEqualityComparer);
|
|
await _triggerStore.ReplaceAsync(diff.Removed, diff.Added, cancellationToken);
|
|
var indexedWorkflow = new IndexedWorkflowTriggers(workflow, emptyTriggerList, currentTriggers, emptyTriggerList);
|
|
await _notificationSender.SendAsync(new WorkflowTriggersIndexed(indexedWorkflow), cancellationToken);
|
|
}
|
|
|
|
private async Task<IEnumerable<StoredTrigger>> GetCurrentTriggersAsync(string workflowDefinitionId, CancellationToken cancellationToken)
|
|
{
|
|
var filter = new TriggerFilter
|
|
{
|
|
WorkflowDefinitionId = workflowDefinitionId
|
|
};
|
|
return await _triggerStore.FindManyAsync(filter, cancellationToken);
|
|
}
|
|
|
|
private async IAsyncEnumerable<StoredTrigger> GetTriggersInternalAsync(Workflow workflow, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
{
|
|
var context = new WorkflowIndexingContext(workflow, cancellationToken);
|
|
var nodes = await _activityVisitor.VisitAsync(workflow.Root, cancellationToken);
|
|
|
|
// Get a list of trigger activities that are configured as "startable".
|
|
var triggerActivities = nodes
|
|
.Flatten()
|
|
.Where(x => x.Activity.GetCanStartWorkflow() && x.Activity is ITrigger)
|
|
.Select(x => x.Activity)
|
|
.Cast<ITrigger>()
|
|
.ToList();
|
|
|
|
// For each trigger activity, create a trigger.
|
|
foreach (var triggerActivity in triggerActivities)
|
|
{
|
|
var triggers = await CreateWorkflowTriggersAsync(context, triggerActivity);
|
|
|
|
foreach (var trigger in triggers)
|
|
yield return trigger;
|
|
}
|
|
}
|
|
|
|
private async Task<ICollection<StoredTrigger>> CreateWorkflowTriggersAsync(WorkflowIndexingContext context, ITrigger trigger)
|
|
{
|
|
var workflow = context.Workflow;
|
|
var cancellationToken = context.CancellationToken;
|
|
var activityTypeName = trigger.Type;
|
|
var triggerDescriptor = _activityRegistry.Find(activityTypeName, trigger.Version);
|
|
|
|
if (triggerDescriptor == null)
|
|
{
|
|
_logger.LogWarning("Could not find activity descriptor for activity type {ActivityType}", activityTypeName);
|
|
return new List<StoredTrigger>(0);
|
|
}
|
|
|
|
var expressionExecutionContext = await trigger.CreateExpressionExecutionContextAsync(triggerDescriptor, _serviceProvider, context, _expressionEvaluator, _logger);
|
|
var triggerIndexingContext = new TriggerIndexingContext(context, expressionExecutionContext, trigger, cancellationToken);
|
|
var triggerData = await TryGetTriggerDataAsync(trigger, triggerIndexingContext);
|
|
var triggerName = triggerIndexingContext.TriggerName;
|
|
|
|
// If no trigger payloads were returned, create a null payload.
|
|
if (!triggerData.Any()) triggerData.Add(null!);
|
|
|
|
var triggers = triggerData.Select(payload => new StoredTrigger
|
|
{
|
|
Id = _identityGenerator.GenerateId(),
|
|
WorkflowDefinitionId = workflow.Identity.DefinitionId,
|
|
WorkflowDefinitionVersionId = workflow.Identity.Id,
|
|
Name = triggerName,
|
|
ActivityId = trigger.Id,
|
|
Hash = _hasher.Hash(triggerName, payload),
|
|
Payload = payload
|
|
});
|
|
|
|
return triggers.ToList();
|
|
}
|
|
|
|
private async Task<List<object>> TryGetTriggerDataAsync(ITrigger trigger, TriggerIndexingContext context)
|
|
{
|
|
try
|
|
{
|
|
return (await trigger.GetTriggerPayloadsAsync(context)).ToList();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.LogWarning(e, "Failed to get trigger data for activity {ActivityId}", trigger.Id);
|
|
}
|
|
|
|
return new(0);
|
|
}
|
|
}
|