Skip to content

Commit 0285591

Browse files
authored
Ensure that no callbacks are added to the ShellScope during termination (OrchardCMS#17945)
1 parent 9605014 commit 0285591

File tree

2 files changed

+74
-33
lines changed

2 files changed

+74
-33
lines changed

src/OrchardCore/OrchardCore.Abstractions/Shell/Scope/ShellScope.cs

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@ public sealed class ShellScope : IServiceScope, IAsyncDisposable
2020
private List<Func<ShellScope, Task>> _deferredTasks;
2121
private List<Func<ShellScope, Exception, Task>> _exceptionHandlers;
2222

23-
private bool _serviceScopeOnly;
24-
private bool _shellTerminated;
25-
private bool _terminated;
26-
private bool _disposed;
23+
private ShellScopeStates _state;
2724

2825
/// <summary>
2926
/// Initializes a <see cref="ShellScope"/> from a given parent <see cref="Builders.ShellContext"/>.
@@ -255,7 +252,7 @@ public void StartAsyncFlow()
255252
/// </summary>
256253
public Task UsingServiceScopeAsync(Func<ShellScope, Task> execute)
257254
{
258-
_serviceScopeOnly = true;
255+
_state |= ShellScopeStates.ServiceScopeOnly;
259256
return UsingAsync(execute);
260257
}
261258

@@ -325,7 +322,7 @@ internal async Task ActivateShellInternalAsync()
325322
return;
326323
}
327324

328-
if (_serviceScopeOnly)
325+
if (_state.HasFlag(ShellScopeStates.ServiceScopeOnly))
329326
{
330327
return;
331328
}
@@ -376,6 +373,8 @@ internal async Task ActivateShellInternalAsync()
376373
/// If true, the delegate is added to the end of the invocation list; otherwise, it is added to the beginning.</param>
377374
internal void BeforeDispose(Func<ShellScope, Task> callback, bool last)
378375
{
376+
ThrowIfTerminating();
377+
379378
var list = _beforeDispose ??= [];
380379

381380
if (last)
@@ -391,17 +390,32 @@ internal void BeforeDispose(Func<ShellScope, Task> callback, bool last)
391390
/// <summary>
392391
/// Adds a Signal (if not already present) to be sent just after 'BeforeDisposeAsync()'.
393392
/// </summary>
394-
internal void DeferredSignal(string key) => (_deferredSignals ??= []).Add(key);
393+
internal void DeferredSignal(string key)
394+
{
395+
ThrowIfTerminating();
396+
397+
(_deferredSignals ??= []).Add(key);
398+
}
395399

396400
/// <summary>
397401
/// Adds a Task to be executed in a new scope after 'BeforeDisposeAsync()'.
398402
/// </summary>
399-
internal void DeferredTask(Func<ShellScope, Task> task) => (_deferredTasks ??= []).Add(task);
403+
internal void DeferredTask(Func<ShellScope, Task> task)
404+
{
405+
ThrowIfTerminating();
406+
407+
(_deferredTasks ??= []).Add(task);
408+
}
400409

401410
/// <summary>
402411
/// Adds an handler to be invoked if an exception is thrown while executing in this shell scope.
403412
/// </summary>
404-
internal void ExceptionHandler(Func<ShellScope, Exception, Task> callback) => (_exceptionHandlers ??= []).Add(callback);
413+
internal void ExceptionHandler(Func<ShellScope, Exception, Task> callback)
414+
{
415+
ThrowIfTerminating();
416+
417+
(_exceptionHandlers ??= []).Add(callback);
418+
}
405419

406420
/// <summary>
407421
/// Registers a delegate to be invoked before the current shell scope will be disposed.
@@ -453,7 +467,7 @@ internal async Task BeforeDisposeAsync()
453467
}
454468
}
455469

456-
if (_serviceScopeOnly)
470+
if (_state.HasFlag(ShellScopeStates.ServiceScopeOnly))
457471
{
458472
return;
459473
}
@@ -516,12 +530,14 @@ await scope.UsingAsync(async scope =>
516530
/// </summary>
517531
internal async Task TerminateShellInternalAsync()
518532
{
519-
if (_serviceScopeOnly)
533+
_state |= ShellScopeStates.IsTerminating;
534+
535+
if (_state.HasFlag(ShellScopeStates.ServiceScopeOnly))
520536
{
521537
return;
522538
}
523539

524-
_terminated = true;
540+
_state |= ShellScopeStates.Terminated;
525541

526542
// If the shell context is released and in its last shell scope, according to the ref counter value,
527543
// the terminate event handlers are called, and the shell will be disposed at the end of this scope.
@@ -552,7 +568,7 @@ internal async Task TerminateShellInternalAsync()
552568
return;
553569
}
554570

555-
_shellTerminated = true;
571+
_state |= ShellScopeStates.ShellTerminated;
556572

557573
var tenantEvents = _serviceScope.ServiceProvider.GetServices<IModularTenantEvents>();
558574
foreach (var tenantEvent in tenantEvents)
@@ -569,16 +585,16 @@ internal async Task TerminateShellInternalAsync()
569585

570586
public void Dispose()
571587
{
572-
if (_disposed)
588+
if (_state.HasFlag(ShellScopeStates.IsDisposed))
573589
{
574590
return;
575591
}
576592

577-
_disposed = true;
593+
_state |= ShellScopeStates.IsDisposed;
578594

579595
_serviceScope.Dispose();
580596

581-
if (_shellTerminated)
597+
if (_state.HasFlag(ShellScopeStates.ShellTerminated))
582598
{
583599
ShellContext.Dispose();
584600
}
@@ -588,16 +604,16 @@ public void Dispose()
588604

589605
public async ValueTask DisposeAsync()
590606
{
591-
if (_disposed)
607+
if (_state.HasFlag(ShellScopeStates.IsDisposed))
592608
{
593609
return;
594610
}
595611

596-
_disposed = true;
612+
_state |= ShellScopeStates.IsDisposed;
597613

598614
await _serviceScope.DisposeAsync();
599615

600-
if (_shellTerminated)
616+
if (_state.HasFlag(ShellScopeStates.ShellTerminated))
601617
{
602618
await ShellContext.DisposeAsync();
603619
}
@@ -607,7 +623,7 @@ public async ValueTask DisposeAsync()
607623

608624
private void Terminate()
609625
{
610-
if (!_terminated)
626+
if (!_state.HasFlag(ShellScopeStates.Terminated))
611627
{
612628
// Keep the counter clean if not yet decremented.
613629
Interlocked.Decrement(ref ShellContext._refCount);
@@ -621,8 +637,27 @@ private void Terminate()
621637
}
622638
}
623639

640+
private void ThrowIfTerminating()
641+
{
642+
if (_state.HasFlag(ShellScopeStates.IsTerminating))
643+
{
644+
throw new InvalidOperationException(
645+
$"Cannot perform this operation because the shell scope for tenant '{ShellContext.Settings.Name}' is already terminating.");
646+
}
647+
}
648+
624649
private sealed class ShellScopeHolder
625650
{
626651
public ShellScope Scope;
627652
}
653+
654+
[Flags]
655+
private enum ShellScopeStates : byte
656+
{
657+
ServiceScopeOnly = 1,
658+
ShellTerminated = 2,
659+
IsTerminating = 4,
660+
Terminated = 8,
661+
IsDisposed = 16,
662+
}
628663
}

src/OrchardCore/OrchardCore.Data.YesSql/OrchardCoreBuilderExtensions.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -151,19 +151,25 @@ public static OrchardCoreBuilder AddDataAccess(this OrchardCoreBuilder builder)
151151

152152
session.RegisterIndexes(scopedServices.ToArray());
153153

154-
ShellScope.Current
155-
.RegisterBeforeDispose(scope =>
156-
{
157-
return scope.ServiceProvider
158-
.GetRequiredService<IDocumentStore>()
159-
.CommitAsync();
160-
})
161-
.AddExceptionHandler((scope, e) =>
162-
{
163-
return scope.ServiceProvider
164-
.GetRequiredService<IDocumentStore>()
165-
.CancelAsync();
166-
});
154+
// Register automated document store commit and rollback when the ISession is used
155+
// on the DI scope of the shell. All other scopes will not be automatically committed.
156+
var shellScope = ShellScope.Current;
157+
if (sp == shellScope?.ServiceProvider)
158+
{
159+
shellScope
160+
.RegisterBeforeDispose(scope =>
161+
{
162+
return scope.ServiceProvider
163+
.GetRequiredService<IDocumentStore>()
164+
.CommitAsync();
165+
})
166+
.AddExceptionHandler((scope, e) =>
167+
{
168+
return scope.ServiceProvider
169+
.GetRequiredService<IDocumentStore>()
170+
.CancelAsync();
171+
});
172+
}
167173

168174
return session;
169175
});

0 commit comments

Comments
 (0)