Skip to content

Commit 7d20b8e

Browse files
committed
Fixing issue causing deadlocks in AuthorizationLevelAttribute
1 parent f119988 commit 7d20b8e

File tree

6 files changed

+155
-26
lines changed

6 files changed

+155
-26
lines changed

src/WebJobs.Script.WebHost/Filters/AuthorizationLevelAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ internal static async Task<AuthorizationLevel> GetAuthorizationLevelAsync(HttpRe
8888
// if there is a function specific key specified try to match against that
8989
if (functionName != null)
9090
{
91-
IDictionary<string, string> functionSecrets = secretManager.GetFunctionSecretsAsync(functionName).GetAwaiter().GetResult();
91+
IDictionary<string, string> functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName);
9292
if (functionSecrets != null &&
9393
functionSecrets.Values.Any(s => Key.SecretValueEquals(keyValue, s)))
9494
{

test/WebJobs.Script.Tests.Integration/FunctionGeneratorEndToEndTests.cs

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Microsoft.Azure.WebJobs.Script.Config;
1717
using Microsoft.Azure.WebJobs.Script.Description;
1818
using Microsoft.Azure.WebJobs.Script.WebHost;
19+
using Microsoft.WebJobs.Script.Tests;
1920
using Xunit;
2021

2122
namespace Microsoft.Azure.WebJobs.Script.Tests
@@ -160,30 +161,5 @@ await TestHelpers.Await(() =>
160161
}
161162
}
162163
}
163-
164-
private sealed class SingleThreadSynchronizationContext : SynchronizationContext
165-
{
166-
private readonly ConcurrentQueue<Tuple<SendOrPostCallback, object>> _workItems =
167-
new ConcurrentQueue<Tuple<SendOrPostCallback, object>>();
168-
169-
public override void Post(SendOrPostCallback d, object state)
170-
{
171-
_workItems.Enqueue(new Tuple<SendOrPostCallback, object>(d, state));
172-
}
173-
174-
public override void Send(SendOrPostCallback d, object state)
175-
{
176-
throw new NotSupportedException();
177-
}
178-
179-
public void Run()
180-
{
181-
Tuple<SendOrPostCallback, object> item;
182-
while (_workItems.TryDequeue(out item))
183-
{
184-
item.Item1(item.Item2);
185-
}
186-
}
187-
}
188164
}
189165
}

test/WebJobs.Script.Tests.Integration/SamplesEndToEndTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
using System.Net.Http.Headers;
1111
using System.Reflection;
1212
using System.Text;
13+
using System.Threading;
1314
using System.Threading.Tasks;
1415
using System.Web.Http;
16+
using System.Web.Http.Hosting;
1517
using System.Xml.Linq;
1618
using Microsoft.Azure.WebJobs.Host;
1719
using Microsoft.Azure.WebJobs.Script.Config;
@@ -20,13 +22,15 @@
2022
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
2123
using Microsoft.ServiceBus;
2224
using Microsoft.ServiceBus.Messaging;
25+
using Microsoft.WebJobs.Script.Tests;
2326
using Microsoft.WindowsAzure.Storage;
2427
using Microsoft.WindowsAzure.Storage.Blob;
2528
using Microsoft.WindowsAzure.Storage.Queue;
2629
using Microsoft.WindowsAzure.Storage.Table;
2730
using Newtonsoft.Json;
2831
using Newtonsoft.Json.Linq;
2932
using Xunit;
33+
using static Microsoft.Azure.WebJobs.Script.Tests.FunctionGeneratorTests;
3034

3135
namespace Microsoft.Azure.WebJobs.Script.Tests
3236
{
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Collections.Generic;
7+
using System.Text;
8+
using System.Threading;
9+
10+
namespace Microsoft.WebJobs.Script.Tests
11+
{
12+
internal sealed class SingleThreadSynchronizationContext : SynchronizationContext
13+
{
14+
private readonly ConcurrentQueue<Tuple<SendOrPostCallback, object>> _workItems =
15+
new ConcurrentQueue<Tuple<SendOrPostCallback, object>>();
16+
private readonly bool _runOnEmptyQueue;
17+
private bool _stopped;
18+
19+
public SingleThreadSynchronizationContext(bool runOnEmptyQueue = false)
20+
{
21+
_runOnEmptyQueue = runOnEmptyQueue;
22+
}
23+
24+
public override void Post(SendOrPostCallback d, object state)
25+
{
26+
_workItems.Enqueue(new Tuple<SendOrPostCallback, object>(d, state));
27+
}
28+
29+
public override void Send(SendOrPostCallback d, object state)
30+
{
31+
throw new NotSupportedException();
32+
}
33+
34+
public void Stop()
35+
{
36+
_stopped = true;
37+
}
38+
39+
public void Run()
40+
{
41+
Tuple<SendOrPostCallback, object> item;
42+
while (!_stopped && (_workItems.TryDequeue(out item) || _runOnEmptyQueue))
43+
{
44+
if (item != null)
45+
{
46+
item.Item1(item.Item2);
47+
}
48+
}
49+
}
50+
}
51+
}

test/WebJobs.Script.Tests.Shared/WebJobs.Script.Tests.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<Import_RootNamespace>Microsoft.WebJobs.Script.Tests</Import_RootNamespace>
1010
</PropertyGroup>
1111
<ItemGroup>
12+
<Compile Include="$(MSBuildThisFileDirectory)SingleThreadedSynchronizationContext.cs" />
1213
<Compile Include="$(MSBuildThisFileDirectory)TempDirectory.cs" />
1314
<Compile Include="$(MSBuildThisFileDirectory)TestHelpers.cs" />
1415
<Compile Include="$(MSBuildThisFileDirectory)TestInvoker.cs" />

test/WebJobs.Script.Tests/Filters/AuthorizationLevelAttributeTests.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using System.Web.Http.Dependencies;
1313
using Microsoft.Azure.WebJobs.Script.WebHost;
1414
using Microsoft.Azure.WebJobs.Script.WebHost.Filters;
15+
using Microsoft.WebJobs.Script.Tests;
1516
using Moq;
1617
using Xunit;
1718

@@ -155,6 +156,102 @@ public async Task GetAuthorizationLevel_ValidKeyHeader_FunctionKey_ReturnsFuncti
155156
Assert.Equal(AuthorizationLevel.Function, level);
156157
}
157158

159+
[Fact]
160+
public void GetAuthorizationLevelAsync_CalledWithSingleThreadedContext_ResolvingSecrets_DoesNotDeadlock()
161+
{
162+
// Test resolving secrets
163+
var secretManager = CreateSecretManager(
164+
async () =>
165+
{
166+
await Task.Delay(500);
167+
return new HostSecretsInfo();
168+
},
169+
async () =>
170+
{
171+
await Task.Delay(500);
172+
return new Dictionary<string, string> { { "default", TestFunctionKeyValue1 } };
173+
});
174+
175+
bool methodReturned = GetAuthorizationLevelAsync_CalledWithSingleThreadedContext_DoesNotDeadlock(secretManager);
176+
177+
Assert.True(methodReturned, $"{nameof(AuthorizationLevelAttribute.GetAuthorizationLevelAsync)} resolving secrets did not return.");
178+
}
179+
180+
[Fact]
181+
public void GetAuthorizationLevelAsync_CalledWithSingleThreadedContext_AndCachedHostSecrets_DoesNotDeadlock()
182+
{
183+
var secretManager = CreateSecretManager(() => Task.FromResult(new HostSecretsInfo()), async () =>
184+
{
185+
await Task.Delay(500);
186+
return new Dictionary<string, string> { { "default", TestFunctionKeyValue1 } };
187+
});
188+
189+
var methodReturned = GetAuthorizationLevelAsync_CalledWithSingleThreadedContext_DoesNotDeadlock(secretManager);
190+
191+
Assert.True(methodReturned, $"{nameof(AuthorizationLevelAttribute.GetAuthorizationLevelAsync)} with cached host secrets did not return.");
192+
}
193+
194+
[Fact]
195+
public void GetAuthorizationLevelAsync_CalledWithSingleThreadedContext_AndCachedSecrets_DoesNotDeadlock()
196+
{
197+
var secretManager = CreateSecretManager(() => Task.FromResult(new HostSecretsInfo()), () => Task.FromResult(new Dictionary<string, string> { { "default", TestFunctionKeyValue1 } }));
198+
199+
var methodReturned = GetAuthorizationLevelAsync_CalledWithSingleThreadedContext_DoesNotDeadlock(secretManager);
200+
201+
Assert.True(methodReturned, $"{nameof(AuthorizationLevelAttribute.GetAuthorizationLevelAsync)} with cached secrets did not return.");
202+
}
203+
204+
private ISecretManager CreateSecretManager(Func<Task<HostSecretsInfo>> hostSecrets, Func<Task<Dictionary<string, string>>> functionSecrets)
205+
{
206+
var secretManagerMock = new Mock<ISecretManager>();
207+
208+
secretManagerMock.Setup(m => m.GetHostSecretsAsync())
209+
.Returns(() => Task.FromResult(new HostSecretsInfo()));
210+
211+
secretManagerMock.Setup(m => m.GetFunctionSecretsAsync(It.IsAny<string>(), false))
212+
.Returns(async () =>
213+
{
214+
await Task.Delay(500);
215+
return new Dictionary<string, string> { { "default", TestFunctionKeyValue1 } };
216+
});
217+
218+
return secretManagerMock.Object;
219+
}
220+
221+
private bool GetAuthorizationLevelAsync_CalledWithSingleThreadedContext_DoesNotDeadlock(ISecretManager secretManager)
222+
{
223+
var resetEvent = new ManualResetEvent(false);
224+
225+
var thread = new Thread(() =>
226+
{
227+
HttpRequestMessage request = new HttpRequestMessage();
228+
request = new HttpRequestMessage();
229+
request.Headers.Add(AuthorizationLevelAttribute.FunctionsKeyHeaderName, TestFunctionKeyValue1);
230+
231+
var context = new SingleThreadSynchronizationContext(true);
232+
SynchronizationContext.SetSynchronizationContext(context);
233+
234+
AuthorizationLevelAttribute.GetAuthorizationLevelAsync(request, secretManager, functionName: "TestFunction")
235+
.ContinueWith(t =>
236+
{
237+
context.Stop();
238+
239+
resetEvent.Set();
240+
});
241+
242+
context.Run();
243+
});
244+
245+
thread.IsBackground = true;
246+
thread.Start();
247+
248+
bool eventSignaled = resetEvent.WaitOne(TimeSpan.FromSeconds(5));
249+
250+
thread.Abort();
251+
252+
return eventSignaled;
253+
}
254+
158255
[Fact]
159256
public async Task GetAuthorizationLevel_InvalidKeyHeader_ReturnsAnonymous()
160257
{

0 commit comments

Comments
 (0)