Skip to content

Commit 8f8ff17

Browse files
authored
Add CancellationToken to ActionExtensions- Debouce API (#4643)
1 parent aedab0b commit 8f8ff17

File tree

11 files changed

+160
-89
lines changed

11 files changed

+160
-89
lines changed

sample/CSharp/HttpTrigger/run.csx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public static IActionResult Run(HttpRequest req, TraceWriter log)
88

99
if (req.Query.TryGetValue("name", out StringValues value))
1010
{
11-
return new OkObjectResult($"Hello, {value.ToString()}");
11+
return new OkObjectResult($"Hello {value.ToString()}");
1212
}
1313

1414
return new BadRequestObjectResult("Please pass a name on the query string or in the request body");

src/WebJobs.Script.WebHost/FileMonitoringService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public FileMonitoringService(IOptions<ScriptJobHostOptions> scriptOptions, ILogg
5353
_restart = _restart.Debounce(500);
5454

5555
_shutdown = Shutdown;
56-
_shutdown = _shutdown.Debounce(500);
56+
_shutdown = _shutdown.Debounce(milliseconds: 500);
5757
}
5858

5959
public Task StartAsync(CancellationToken cancellationToken)

src/WebJobs.Script/Extensions/ActionExtensions.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.Azure.WebJobs.Script
99
{
1010
public static class ActionExtensions
1111
{
12-
public static Action<T> Debounce<T>(this Action<T> func, int milliseconds = 300)
12+
public static Action<T> Debounce<T>(this Action<T> func, CancellationToken cancellationToken = default, int milliseconds = 300)
1313
{
1414
var last = 0;
1515

@@ -24,17 +24,20 @@ public static Action<T> Debounce<T>(this Action<T> func, int milliseconds = 300)
2424
// Only proceeed with the operation if there have been no
2525
// more events within the specified time window (i.e. there
2626
// is a quiet period)
27-
func(arg);
27+
if (!cancellationToken.IsCancellationRequested)
28+
{
29+
func(arg);
30+
}
2831
}
2932
t.Dispose();
3033
});
3134
};
3235
}
3336

34-
public static Action Debounce(this Action targetAction, int milliseconds = 300)
37+
public static Action Debounce(this Action targetAction, CancellationToken cancellationToken = default, int milliseconds = 300)
3538
{
3639
Action<object> action = _ => targetAction();
37-
action = action.Debounce(milliseconds);
40+
action = action.Debounce(cancellationToken, milliseconds);
3841

3942
return () => action(null);
4043
}

src/WebJobs.Script/Rpc/FunctionRegistration/FunctionDispatcher.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Reactive.Concurrency;
99
using System.Reactive.Linq;
10+
using System.Threading;
1011
using System.Threading.Tasks;
1112
using System.Threading.Tasks.Dataflow;
1213
using Microsoft.Azure.WebJobs.Script.Description;
@@ -45,6 +46,7 @@ internal class FunctionDispatcher : IFunctionDispatcher
4546
private Action _shutdownStandbyWorkerChannels;
4647
private IEnumerable<FunctionMetadata> _functions;
4748
private ConcurrentBag<Exception> _languageWorkerErrors = new ConcurrentBag<Exception>();
49+
private CancellationTokenSource _processStartCancellationToken = new CancellationTokenSource();
4850

4951
public FunctionDispatcher(IOptions<ScriptJobHostOptions> scriptHostOptions,
5052
IMetricsLogger metricsLogger,
@@ -87,7 +89,7 @@ public FunctionDispatcher(IOptions<ScriptJobHostOptions> scriptHostOptions,
8789
.Subscribe(AddOrUpdateWorkerChannels);
8890

8991
_shutdownStandbyWorkerChannels = ShutdownWebhostLanguageWorkerChannels;
90-
_shutdownStandbyWorkerChannels = _shutdownStandbyWorkerChannels.Debounce(5000);
92+
_shutdownStandbyWorkerChannels = _shutdownStandbyWorkerChannels.Debounce(milliseconds: 5000);
9193
}
9294

9395
public FunctionDispatcherState State { get; private set; }
@@ -131,7 +133,7 @@ private void StartWorkerProcesses(int startIndex, Action startAction)
131133
{
132134
for (var count = startIndex; count < _maxProcessCount; count++)
133135
{
134-
startAction = startAction.Debounce(count * _debounceSeconds * 1000);
136+
startAction = startAction.Debounce(_processStartCancellationToken.Token, count * _debounceSeconds * 1000);
135137
startAction();
136138
}
137139
}
@@ -280,6 +282,8 @@ protected virtual void Dispose(bool disposing)
280282
{
281283
_workerErrorSubscription.Dispose();
282284
_rpcChannelReadySubscriptions.Dispose();
285+
_processStartCancellationToken.Cancel();
286+
_processStartCancellationToken.Dispose();
283287
_jobHostLanguageWorkerChannelManager.DisposeAndRemoveChannels();
284288
_disposed = true;
285289
}

src/WebJobs.Script/Rpc/WebHostLanguageWorkerChannelManager.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Linq;
88
using System.Reactive.Linq;
9+
using System.Threading;
910
using System.Threading.Tasks;
1011
using Microsoft.Azure.WebJobs.Script.Eventing;
1112
using Microsoft.Extensions.Logging;
@@ -37,7 +38,7 @@ public WebHostLanguageWorkerChannelManager(IScriptEventManager eventManager, IEn
3738
_applicationHostOptions = applicationHostOptions;
3839

3940
_shutdownStandbyWorkerChannels = ScheduleShutdownStandbyChannels;
40-
_shutdownStandbyWorkerChannels = _shutdownStandbyWorkerChannels.Debounce(5000);
41+
_shutdownStandbyWorkerChannels = _shutdownStandbyWorkerChannels.Debounce(milliseconds: 5000);
4142
}
4243

4344
public Task<ILanguageWorkerChannel> InitializeChannelAsync(string runtime)

test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/EndToEndTestFixture.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ public abstract class EndToEndTestFixture : IAsyncLifetime
3131
private readonly string _rootPath;
3232
private string _copiedRootPath;
3333
private string _functionsWorkerRuntime;
34+
private int _workerProcessCount;
3435

35-
protected EndToEndTestFixture(string rootPath, string testId, string functionsWorkerRuntime)
36+
protected EndToEndTestFixture(string rootPath, string testId, string functionsWorkerRuntime, int workerProcessesCount = 1)
3637
{
3738
FixtureId = testId;
3839

3940
_rootPath = rootPath;
4041
_functionsWorkerRuntime = functionsWorkerRuntime;
42+
_workerProcessCount = workerProcessesCount;
43+
4144
}
4245

4346
public CloudBlobContainer TestInputContainer { get; private set; }
@@ -91,6 +94,7 @@ public async Task InitializeAsync()
9194
if (!string.IsNullOrEmpty(_functionsWorkerRuntime))
9295
{
9396
Environment.SetEnvironmentVariable(LanguageWorkerConstants.FunctionWorkerRuntimeSettingName, _functionsWorkerRuntime);
97+
Environment.SetEnvironmentVariable(LanguageWorkerConstants.FunctionsWorkerProcessCountSettingName, _workerProcessCount.ToString());
9498
}
9599

96100
FunctionsSyncManagerMock = new Mock<IFunctionsSyncManager>(MockBehavior.Strict);
@@ -214,6 +218,7 @@ public virtual Task DisposeAsync()
214218
}
215219
}
216220
Environment.SetEnvironmentVariable(LanguageWorkerConstants.FunctionWorkerRuntimeSettingName, string.Empty);
221+
Environment.SetEnvironmentVariable(LanguageWorkerConstants.FunctionsWorkerProcessCountSettingName, string.Empty);
217222
return Task.CompletedTask;
218223
}
219224

test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SamplesEndToEndTests_CSharp.cs

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ public async Task SetHostState_Offline_Succeeds()
283283
var hostStatus = response.Content.ReadAsAsync<HostStatus>();
284284

285285
// verify functions can be invoked
286-
await InvokeAndValidateHttpTrigger(functionName);
286+
await SamplesTestHelpers.InvokeAndValidateHttpTrigger(_fixture, functionName);
287287

288288
// verify function status is ok
289289
response = await GetFunctionStatusAsync(functionName);
@@ -310,7 +310,7 @@ await TestHelpers.RunWithTimeoutAsync(async () =>
310310
Assert.Null(functionStatus.Errors);
311311

312312
// verify that when offline function requests return 503
313-
response = await InvokeHttpTrigger(functionName);
313+
response = await SamplesTestHelpers.InvokeHttpTrigger(_fixture, functionName);
314314
await VerifyOfflineResponse(response);
315315

316316
// verify that the root returns 503 immediately when offline
@@ -331,7 +331,7 @@ await TestHelpers.RunWithTimeoutAsync(async () =>
331331
await _fixture.InitializeAsync();
332332

333333
// verify functions can be invoked
334-
await InvokeAndValidateHttpTrigger(functionName);
334+
await SamplesTestHelpers.InvokeAndValidateHttpTrigger(_fixture, functionName);
335335

336336
// verify the same thing via admin api
337337
response = await AdminInvokeFunction(functionName);
@@ -535,26 +535,6 @@ public async Task HttpTrigger_Poco_Get_Succeeds()
535535
}
536536
}
537537

538-
private async Task InvokeAndValidateHttpTrigger(string functionName)
539-
{
540-
string functionKey = await _fixture.Host.GetFunctionSecretAsync($"{functionName}");
541-
string uri = $"api/{functionName}?code={functionKey}&name=Mathew";
542-
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
543-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
544-
545-
HttpResponseMessage response = await _fixture.Host.HttpClient.SendAsync(request);
546-
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
547-
string body = await response.Content.ReadAsStringAsync();
548-
Assert.Equal("text/plain", response.Content.Headers.ContentType.MediaType);
549-
Assert.Equal("Hello, Mathew", body);
550-
551-
// verify request also succeeds with master key
552-
string masterKey = await _fixture.Host.GetMasterKeyAsync();
553-
uri = $"api/{functionName}?code={masterKey}&name=Mathew";
554-
request = new HttpRequestMessage(HttpMethod.Get, uri);
555-
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
556-
}
557-
558538
// invoke a function via the admin invoke api
559539
private async Task<HttpResponseMessage> AdminInvokeFunction(string functionName, string input = null)
560540
{
@@ -570,16 +550,6 @@ private async Task<HttpResponseMessage> AdminInvokeFunction(string functionName,
570550
return await _fixture.Host.HttpClient.SendAsync(request);
571551
}
572552

573-
private async Task<HttpResponseMessage> InvokeHttpTrigger(string functionName)
574-
{
575-
string functionKey = await _fixture.Host.GetFunctionSecretAsync($"{functionName}");
576-
string uri = $"api/{functionName}?code={functionKey}&name=Mathew";
577-
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
578-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
579-
580-
return await _fixture.Host.HttpClient.SendAsync(request);
581-
}
582-
583553
[Fact]
584554
public async Task HttpTrigger_DuplicateQueryParams_Succeeds()
585555
{
@@ -599,7 +569,7 @@ public async Task HttpTrigger_DuplicateQueryParams_Succeeds()
599569
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
600570
string body = await response.Content.ReadAsStringAsync();
601571
Assert.Equal("text/plain", response.Content.Headers.ContentType.MediaType);
602-
Assert.Equal("Hello, Mathew,Amy", body);
572+
Assert.Equal("Hello Mathew,Amy", body);
603573
}
604574
}
605575

test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SamplesEndToEndTests_Java.cs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public SamplesEndToEndTests_Java(TestFixture fixture)
3434
[Fact]
3535
public async Task HttpTrigger_Java_Get_Succeeds()
3636
{
37-
await InvokeHttpTrigger("HttpTrigger");
37+
await SamplesTestHelpers.InvokeHttpTrigger(_fixture, "HttpTrigger");
3838
}
3939

4040
[Fact]
@@ -52,17 +52,6 @@ public async Task JavaProcess_Same_AfterHostRestart()
5252
Assert.Equal(0, result.Count());
5353
}
5454

55-
private async Task InvokeHttpTrigger(string functionName)
56-
{
57-
string functionKey = await _fixture.Host.GetFunctionSecretAsync($"{functionName}");
58-
string uri = $"api/{functionName}?code={functionKey}&name=Mathew";
59-
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
60-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
61-
62-
var response = await _fixture.Host.HttpClient.SendAsync(request);
63-
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
64-
}
65-
6655
public class TestFixture : EndToEndTestFixture
6756
{
6857
static TestFixture()

test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SamplesEndToEndTests_Node.cs

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -252,43 +252,12 @@ public async Task ManualTrigger_Invoke_Succeeds()
252252
[Fact]
253253
public async Task HttpTrigger_Get_Succeeds()
254254
{
255-
await InvokeAndValidateHttpTrigger("HttpTrigger");
256-
}
257-
258-
private async Task InvokeAndValidateHttpTrigger(string functionName)
259-
{
260-
string functionKey = await _fixture.Host.GetFunctionSecretAsync($"{functionName}");
261-
string uri = $"api/{functionName}?code={functionKey}&name=Mathew";
262-
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
263-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
264-
265-
HttpResponseMessage response = await _fixture.Host.HttpClient.SendAsync(request);
266-
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
267-
string body = await response.Content.ReadAsStringAsync();
268-
Assert.Equal("text/plain", response.Content.Headers.ContentType.MediaType);
269-
Assert.Equal("Hello Mathew", body);
270-
271-
// verify request also succeeds with master key
272-
string masterKey = await _fixture.Host.GetMasterKeyAsync();
273-
uri = $"api/{functionName}?code={masterKey}&name=Mathew";
274-
request = new HttpRequestMessage(HttpMethod.Get, uri);
275-
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
276-
}
277-
278-
private async Task<HttpResponseMessage> InvokeHttpTrigger(string functionName)
279-
{
280-
string functionKey = await _fixture.Host.GetFunctionSecretAsync($"{functionName}");
281-
string uri = $"api/{functionName}?code={functionKey}&name=Mathew";
282-
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
283-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
284-
285-
return await _fixture.Host.HttpClient.SendAsync(request);
255+
await SamplesTestHelpers.InvokeAndValidateHttpTrigger(_fixture, "HttpTrigger");
286256
}
287257

288258
[Fact]
289259
public async Task HttpTrigger_DuplicateQueryParams_Succeeds()
290260
{
291-
292261
string functionKey = await _fixture.Host.GetFunctionSecretAsync("httptrigger");
293262
string uri = $"api/httptrigger?code={functionKey}&name=Mathew&name=Amy";
294263
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
@@ -436,6 +405,8 @@ public async Task NodeProcess_Different_AfterHostRestart()
436405
await _fixture.Host.RestartAsync(CancellationToken.None);
437406

438407
await HttpTrigger_Get_Succeeds();
408+
// wait for orphaned jobhost instance to be disposed
409+
await Task.Delay(TimeSpan.FromSeconds(5));
439410
IEnumerable<int> nodeProcessesAfter = Process.GetProcessesByName("node").Select(p => p.Id);
440411
// Verify number of node processes before and after restart are the same.
441412
Assert.Equal(nodeProcessesBefore.Count(), nodeProcessesAfter.Count());
@@ -447,7 +418,6 @@ public async Task NodeProcess_Different_AfterHostRestart()
447418
[Fact]
448419
public async Task HttpTrigger_Disabled_SucceedsWithAdminKey()
449420
{
450-
451421
// first try with function key only - expect 404
452422
string functionKey = await _fixture.Host.GetFunctionSecretAsync("HttpTrigger-Disabled");
453423
string uri = $"api/httptrigger-disabled?code={functionKey}";

0 commit comments

Comments
 (0)