Skip to content

Commit 92c9de3

Browse files
committed
Don't leak AsyncLocal values from synchronous background job methods
This behavior matches the behavior of async methods, where `AsyncLocal` values are allowed to flow into methods (will be useful for context-aware logging in the future), but not allowed to leak them outside of background job methods. Closes #2409, #906
1 parent a48c7e7 commit 92c9de3

File tree

3 files changed

+401
-17
lines changed

3 files changed

+401
-17
lines changed

Diff for: src/Hangfire.Core/Server/CoreBackgroundJobPerformer.cs

+45-15
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ private object InvokeMethod(PerformContext context, object instance, object[] ar
154154
private object InvokeOnTaskScheduler(PerformContext context, BackgroundJobMethod method, Func<object, Task> getTaskFunc)
155155
{
156156
var scheduledTask = Task.Factory.StartNew(
157-
InvokeSynchronously,
157+
InvokeOnTaskSchedulerInternal,
158158
method,
159159
CancellationToken.None,
160160
TaskCreationOptions.None,
@@ -166,6 +166,14 @@ private object InvokeOnTaskScheduler(PerformContext context, BackgroundJobMethod
166166
return getTaskFunc(result).GetTaskLikeResult(result, method.ReturnType);
167167
}
168168

169+
private static object InvokeOnTaskSchedulerInternal(object state)
170+
{
171+
// ExecutionContext is captured automatically when calling the Task.Factory.StartNew
172+
// method, so we don't need to capture it manually. Please see the comment for
173+
// synchronous method execution below for details.
174+
return ((BackgroundJobMethod)state).Invoke();
175+
}
176+
169177
private static object InvokeOnTaskPump(PerformContext context, BackgroundJobMethod method, Func<object, Task> getTaskFunc)
170178
{
171179
// Using SynchronizationContext here is the best default option, where workers
@@ -212,9 +220,40 @@ private static object InvokeOnTaskPump(PerformContext context, BackgroundJobMeth
212220
private static object InvokeSynchronously(object state)
213221
{
214222
var method = (BackgroundJobMethod) state;
223+
var executionContext = ExecutionContext.Capture();
224+
225+
if (executionContext != null)
226+
{
227+
// Asynchronous methods started with the TaskScheduler.StartNew method, capture the
228+
// current execution context by default and call the ExecutionContext.Run method on
229+
// a thread pool thread to pass the current AsyncLocal values there.
230+
//
231+
// As a side effect of this call, any updates to AsyncLocal values which happen inside
232+
// the background job method itself values aren't passed back to the calling thread
233+
// and end their lifetime as soon as method execution is finished.
234+
//
235+
// Synchronous methods don't need to capture the current execution context because the
236+
// thread is not changed. However, any updates to AsyncLocal values that happen inside
237+
// the background job method are passed back to the calling thread, expanding their
238+
// lifetime to the lifetime of the thread itself. This can result in memory leaks and
239+
// possibly affect future background job method executions in unexpected ways.
240+
//
241+
// To avoid this and to have the same behavior of AsyncLocal between synchronous and
242+
// asynchronous methods, we run synchronous ones in a captured execution context. The
243+
// ExecutionContext.Run method ensures that AsyncLocal values will be modified only in
244+
// the captured context and will not flow back to the parent context.
245+
ExecutionContext.Run(executionContext, InvokeSynchronouslyInternal, method);
246+
return method.Result;
247+
}
248+
215249
return method.Invoke();
216250
}
217251

252+
private static void InvokeSynchronouslyInternal(object state)
253+
{
254+
((BackgroundJobMethod)state).Invoke();
255+
}
256+
218257
private static object[] SubstituteArguments(PerformContext context)
219258
{
220259
if (context.BackgroundJob.Job == null)
@@ -240,24 +279,15 @@ private static object[] SubstituteArguments(PerformContext context)
240279
return result.ToArray();
241280
}
242281

243-
private sealed class BackgroundJobMethod
282+
private sealed class BackgroundJobMethod(MethodInfo methodInfo, object instance, object[] parameters)
244283
{
245-
private readonly MethodInfo _methodInfo;
246-
private readonly object _instance;
247-
private readonly object[] _parameters;
248-
249-
public BackgroundJobMethod(MethodInfo methodInfo, object instance, object[] parameters)
250-
{
251-
_methodInfo = methodInfo;
252-
_instance = instance;
253-
_parameters = parameters;
254-
}
255-
256-
public Type ReturnType => _methodInfo.ReturnType;
284+
public Type ReturnType => methodInfo.ReturnType;
285+
public object Result { get; private set; }
257286

258287
public object Invoke()
259288
{
260-
return _methodInfo.Invoke(_instance, _parameters);
289+
Result = methodInfo.Invoke(instance, parameters);
290+
return Result;
261291
}
262292
}
263293
}

Diff for: tests/Hangfire.Core.Tests/Server/BackgroundJobPerformerFacts.cs

+142-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Linq;
45
using System.Threading;
6+
using System.Threading.Tasks;
57
using Hangfire.Common;
68
using Hangfire.Server;
79
using Moq;
@@ -548,9 +550,95 @@ public void Run_ThrowsJobPerformanceException_InsteadOfOperationCanceled_Occurre
548550
Assert.IsType<OperationCanceledException>(exception.InnerException);
549551
}
550552

551-
private BackgroundJobPerformer CreatePerformer()
553+
#if !NET452
554+
[Theory]
555+
[MemberData(nameof(GetSchedulers))]
556+
public void Run_FlowsAsyncLocal_ThroughFilters_AndSynchronousBackgroundJobMethod(TaskScheduler scheduler)
552557
{
553-
return new BackgroundJobPerformer(_filterProvider.Object, _innerPerformer.Object);
558+
// Arrange
559+
var id = Guid.NewGuid();
560+
_filters.Add(new AsyncLocalFilter(id));
561+
_context.BackgroundJob.Job = Job.FromExpression(() => AsyncLocalSync());
562+
563+
var performer = CreatePerformer(CreateInnerPerformer(scheduler));
564+
565+
// Act
566+
var result = performer.Perform(_context.Object);
567+
568+
// Assert
569+
Assert.Equal(id, result);
570+
}
571+
572+
[Theory]
573+
[MemberData(nameof(GetSchedulers))]
574+
public void Run_FlowsAsyncLocal_ThroughFilters_AndSimpleAsynchronousBackgroundJobMethod(TaskScheduler scheduler)
575+
{
576+
// Arrange
577+
var id = Guid.NewGuid();
578+
_filters.Add(new AsyncLocalFilter(id));
579+
_context.BackgroundJob.Job = Job.FromExpression(() => AsyncLocalSimpleAsync());
580+
581+
var performer = CreatePerformer(CreateInnerPerformer(scheduler));
582+
583+
// Act
584+
var result = performer.Perform(_context.Object);
585+
586+
// Assert
587+
Assert.Equal(id, result);
588+
}
589+
590+
[Theory]
591+
[MemberData(nameof(GetSchedulers))]
592+
public void Run_FlowsAsyncLocal_ThroughFilters_AndAsyncAwaitAsynchronousBackgroundJobMethod(TaskScheduler scheduler)
593+
{
594+
// Arrange
595+
var id = Guid.NewGuid();
596+
_filters.Add(new AsyncLocalFilter(id));
597+
_context.BackgroundJob.Job = Job.FromExpression(() => AsyncLocalAsyncAwait());
598+
599+
var performer = CreatePerformer(CreateInnerPerformer(scheduler));
600+
601+
// Act
602+
var result = performer.Perform(_context.Object);
603+
604+
// Assert
605+
Assert.Equal(id, result);
606+
}
607+
608+
[Theory]
609+
[MemberData(nameof(GetSchedulers))]
610+
public void Run_FlowsAsyncLocal_ThroughFilters_AndAsyncAwaitContinuationAsynchronousBackgroundJobMethod(TaskScheduler scheduler)
611+
{
612+
// Arrange
613+
var id = Guid.NewGuid();
614+
_filters.Add(new AsyncLocalFilter(id));
615+
_context.BackgroundJob.Job = Job.FromExpression(() => AsyncLocalAsyncAwaitContinuation());
616+
617+
var performer = CreatePerformer(CreateInnerPerformer(scheduler));
618+
619+
// Act
620+
var result = performer.Perform(_context.Object);
621+
622+
// Assert
623+
Assert.Equal(id, result);
624+
}
625+
626+
private static CoreBackgroundJobPerformer CreateInnerPerformer(TaskScheduler taskScheduler)
627+
{
628+
return new CoreBackgroundJobPerformer(new JobActivator(), taskScheduler);
629+
}
630+
631+
public static IEnumerable<object[]> GetSchedulers()
632+
{
633+
yield return new object[] { null };
634+
yield return new object[] { TaskScheduler.Default };
635+
yield return new object[] { new Hangfire.Processing.BackgroundTaskScheduler(threadCount: 1) };
636+
}
637+
#endif
638+
639+
private BackgroundJobPerformer CreatePerformer(IBackgroundJobPerformer inner = null)
640+
{
641+
return new BackgroundJobPerformer(_filterProvider.Object, inner ?? _innerPerformer.Object);
554642
}
555643

556644
private Mock<T> CreateFilter<T>()
@@ -561,5 +649,57 @@ private Mock<T> CreateFilter<T>()
561649

562650
return filter;
563651
}
652+
653+
#if !NET452
654+
private static readonly AsyncLocal<Guid> Identifier = new AsyncLocal<Guid>();
655+
656+
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
657+
[SuppressMessage("ReSharper", "UnusedMethodReturnValue.Global")]
658+
public static Guid AsyncLocalSync()
659+
{
660+
return Identifier.Value;
661+
}
662+
663+
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
664+
public static Task<Guid> AsyncLocalSimpleAsync()
665+
{
666+
return Task.FromResult(Identifier.Value);
667+
}
668+
669+
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
670+
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
671+
public static async Task<Guid> AsyncLocalAsyncAwait()
672+
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
673+
{
674+
return Identifier.Value;
675+
}
676+
677+
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
678+
public static async Task<Guid> AsyncLocalAsyncAwaitContinuation()
679+
{
680+
await Task.Yield();
681+
return Identifier.Value;
682+
}
683+
684+
private sealed class AsyncLocalFilter : IServerFilter
685+
{
686+
private readonly Guid _identifier;
687+
688+
public AsyncLocalFilter(Guid identifier)
689+
{
690+
_identifier = identifier;
691+
}
692+
693+
public void OnPerforming(PerformingContext context)
694+
{
695+
Identifier.Value = _identifier;
696+
}
697+
698+
public void OnPerformed(PerformedContext context)
699+
{
700+
Assert.Equal(_identifier, Identifier.Value);
701+
}
702+
}
703+
#endif
564704
}
565705
}

0 commit comments

Comments
 (0)