Repository navigation
[3.10] Resolve Quartz jobs inside the trigger's tenant scope - #9125
sfmskywalker wants to merge 1 commit into
Conversation
* fix(scheduling): resolve Quartz jobs inside the trigger tenant scope Quartz constructed jobs before any tenant was pushed, so constructor dependencies bound to the default tenant. A tenant-aware job factory now reads TenantId from the trigger (or job) data, pushes that tenant into a DI scope, and only then resolves the job. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * fix(scheduling): keep tenant around Quartz Execute and restore job ctors Address CR R1: derive TenantAwareJobFactory from MicrosoftDependencyInjectionJobFactory, wrap the inner IJob so the tenant is pushed again in Execute, throw from NewJob only when the tenant is missing, defer lookup failures to IQuartzJobRetryScheduler, restore the public job constructors for 3.10.1 binary compatibility, and register the factory only on the Quartz feature. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * fix(scheduling): fall back to per-job tenant push without the factory Address CR R2: when TenantAwareJobFactory is not registered, each workflow and background job looks up TenantId and pushes the tenant for Execute. Lookup exceptions stay on the existing retry path. TenantLookupFailedJob deletes durable ExecuteBackgroundActivityJob instead of retrying, and rethrows with ExceptionDispatchInfo. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * fix(scheduling): register tenant job factory on the scheduler path Replace the per-job Execute tenant lookup with module-path factory registration and a fail-loud guard. Quartz's default DI factory is treated as replaceable; a host custom IJobFactory is kept. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> (cherry picked from commit 0cceb5a)
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
|
@elsa-workflows-bot review |
[Medium impact] The reviewed changes have no outstanding issues requiring changes before merging. SummaryMoves tenant setup ahead of Quartz job construction so job dependencies see the scheduled tenant.
Intentional behavior noted by sfmskywalker: missing tenants place triggers in ERROR; host factories are preserved, with the tenant guard rejecting jobs that lack the scheduled tenant rather than silently using default-tenant stores; background activity lookup failures delete the job and rethrow rather than retry. No blocking issues were found in the reviewed changes. The PR is safe to merge. Reviews (1) · Last reviewed commit: "Resolve Quartz jobs inside the trigger's..." · Reviewed by Greptile |
There was a problem hiding this comment.
Elsa 3 Code Review: APPROVE + HIGH @ 2878c3a
Code Review, Round 1/6
This is a clean pick of #9121 (main 0cceb5ac) onto release/3.10.0 for 3.10.1. #9121 was approved in Round 3 at 6ce7b8cf, and this PR carries exactly that change: no extra edits, no conflict resolution. Approved for 3.10.1; per the PR body it merges only after the 3.10.0 tag.
Verified
- Same change as main.
git patch-id --stableis identical for0cceb5acand2878c3a9(2c7a817f).git range-diffshows only the added(cherry picked from commit 0cceb5ac...)trailer in the message. Both touch the same 23 files (+1110 / -124). - Same starting point. The 23 touched files have identical content on
release/3.10.0(775cb601) and on main just before the #9121 merge (f295d8ba), and identical content on this head and on0cceb5ac. So the picked files equal main after #9121, byte for byte. - Build and tests on this head.
Elsa.Scheduling.Quartzbuilds for net8.0, net9.0 and net10.0 with no errors.Elsa.Scheduling.Quartz.UnitTests: 182/182 pass.Elsa.Scheduling.Quartz.ComponentTests: 7/7 pass. - Quartz version.
release/3.10.0pinsQuartz.Extensions.HostingandQuartz.Serialization.Jsonat 3.15.1, the same as main, so theAddQuartz/UseJobFactory<T>descriptor behaviour thatJobFactoryRegistrationrelies on, and theMicrosoftDependencyInjectionJobFactory.InstantiateJoboverride, are the same here. CShells is 0.0.28 on both. - Feature and shell setup. The set of files that call
AddQuartz, register or replace anIJobFactory, or callTryUseTenantAwareJobFactoryis the same on this head and on main:QuartzFeature,QuartzSchedulerFeatureandQuartzBackgroundActivitySchedulerFeature, their shell counterparts, and the unchanged EF Core store features. Underextensions/src/schedulingthe only difference from main is one package reference inElsa.Scheduling.Quartz.EFCore.SqlServer.csproj, tracked separately and not touched here. - No public API break.
JobExecutionExtensionsisinternal, so droppingGetTenantAsyncaffects no consumers.TenantAwareJobFactory(internal sealed) andJobFactoryRegistration(internal static) are internal. TheRunWorkflowJob,ResumeWorkflowJobandExecuteBackgroundActivityJobconstructors are unchanged,tenantFinderincluded, so they stay binary compatible. - PR body. It no longer tells hosts to use the internal factory, and it states that a later
UseJobFactory<T>()orUseMicrosoftDependencyInjectionJobFactory()call replaces the tenant-aware factory. Both match the code.
Non-blocking (carried over from #9121, relevant for a patch release)
- 3.10.1 release note. On a multi-tenant host whose
IJobFactoryis notTenantAwareJobFactory, tenant jobs now fail loudly instead of quietly running under the default tenant. That covers a host's own factory, andUseJobFactory<T>()orUseMicrosoftDependencyInjectionJobFactory()called after Elsa's Quartz features. One-shot resume and background jobs that fail this way are not retried. This is intended, but it is a behaviour change in a patch, so please add a line to the 3.10.1 notes. - Follow-ups (main first, then pick if wanted): a bounded timeout or cache for the synchronous tenant lookup in
NewJob, one sharedTenantIdreader for the factory and the guard, guard tests that assert no retry and noDeleteJobfor the workflow jobs, and the CS9107 capture warning inTenantAwareJobFactory.
Threads
There are no review threads on this PR. CodeRabbit skips non-default bases. Greptile's summary (5/5) raises no issues.
CI
All checks on this head are green: ubuntu-latest, prove, import-rehearsal-tests, retained-testing-helper, select-tests, GitGuardian, solution grouping and Greptile. run-affected-tests was skipped.
— Code Review (Elsa 3), via Elsa Review
Cherry-pick of #9121 (main SHA 0cceb5a).
Fixes #9120 for 3.10.1.
This PR merges only after the 3.10.0 tag.
Purpose
Timers and cron triggers only ran against the default tenant when tenants have their own databases. Quartz resolved job constructor dependencies before any tenant was pushed, so stores and DbContexts bound to the default tenant.
Scope
Description
Problem
RunWorkflowJob,ResumeWorkflowJobandExecuteBackgroundActivityJobused to push the tenant at the start ofExecute. That is too late:IWorkflowStarter, stores and the DbContexts behind them are resolved by the job factory first. An Execute-time lookup (the previous fallback) is the same too-late push and silently reintroduces #9120.Solution
TenantAwareJobFactoryderives from Quartz 3.15.1'sMicrosoftDependencyInjectionJobFactoryand is registered once per container byJobFactoryRegistration.TryUseTenantAwareJobFactory:QuartzFeature,QuartzSchedulerFeature(UseQuartzScheduler()), andQuartzBackgroundActivitySchedulerFeature(same module path), plus the matching shell features.IJobFactoryis present.AddQuartzdefault isTryAddSingleton(typeof(IJobFactory), typeof(MicrosoftDependencyInjectionJobFactory)). That default is treated as replaceable and does not count as "another factory".IJobFactory(typed, instance, or factory delegate) is kept. Detection works when features run before or afterAddQuartz, but a laterUseJobFactory<T>()orUseMicrosoftDependencyInjectionJobFactory()call replaces the tenant-aware factory.The factory:
TenantIdfrom the triggerJobDataMap, falling back to the jobJobDataMap.NewJob.NewJob. Return a job that defers the failure intoExecute. Workflow jobs useIQuartzJobRetryScheduler; durableExecuteBackgroundActivityJobdeletes itself and rethrows viaExceptionDispatchInfo.IJobwrapper that pushes the tenant again aroundExecute.TenantIdkeeps today's default-tenant behaviour.Jobs no longer look up or push a tenant. If trigger/job data has a
TenantIdbut the tenant in context duringExecuteis null or different, they log theTenantIdand job key and throw. They never fall through to default-tenant stores.Hosts that register their own
IJobFactorykeep it. A fail-loud guard then reports jobs that run without their tenant, instead of silently using the default tenant. Public constructors are unchanged.Tests
TenantIduses default-tenant services.Execute.UseQuartzScheduler()withoutUseQuartz(), real in-memory scheduler, tenant-a and tenant-b each see their own tenant at construction and inExecute.IJobFactoryis kept (not overridden); the guard then fires loudly.Local:
Checklist