Skip to content

Commit 675c5bf

Browse files
committed
Don't do empty background sync triggers (#5436)
1 parent 2491c42 commit 675c5bf

File tree

4 files changed

+84
-31
lines changed

4 files changed

+84
-31
lines changed

src/WebJobs.Script.WebHost/FunctionsSyncService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ private async void OnSyncTimerTick(object state)
7474
if (!cancellationToken.IsCancellationRequested && ShouldSyncTriggers)
7575
{
7676
_logger.LogDebug("Initiating background SyncTriggers operation");
77-
await _functionsSyncManager.TrySyncTriggersAsync(checkHash: true);
77+
await _functionsSyncManager.TrySyncTriggersAsync(isBackgroundSync: true);
7878
}
7979
}
8080
catch (Exception exc) when (!exc.IsFatal())

src/WebJobs.Script.WebHost/Management/FunctionsSyncManager.cs

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ internal bool ArmCacheEnabled
7979
}
8080
}
8181

82-
public async Task<SyncTriggersResult> TrySyncTriggersAsync(bool checkHash = false)
82+
public async Task<SyncTriggersResult> TrySyncTriggersAsync(bool isBackgroundSync = false)
8383
{
8484
var result = new SyncTriggersResult
8585
{
@@ -99,27 +99,36 @@ public async Task<SyncTriggersResult> TrySyncTriggersAsync(bool checkHash = fals
9999
await _syncSemaphore.WaitAsync();
100100

101101
var hashBlob = await GetHashBlobAsync();
102-
if (checkHash && hashBlob == null)
102+
if (isBackgroundSync && hashBlob == null)
103103
{
104-
// short circuit before doing any work in cases where
105-
// we're asked to check/update hash but don't have
104+
// short circuit before doing any work in background sync
105+
// cases where we need to check/update hash but don't have
106106
// storage access
107107
return result;
108108
}
109109

110-
string json = await GetSyncTriggersPayload();
110+
var payload = await GetSyncTriggersPayload();
111+
if (isBackgroundSync && payload.Count == 0)
112+
{
113+
// We don't do background sync for empty triggers.
114+
// We've seen error cases where a site temporarily gets into a situation
115+
// where it's site content is empty. Doing the empty sync can cause the app
116+
// to go idle when it shouldn't.
117+
_logger.LogDebug("No functions found. Skipping Sync operation.");
118+
return result;
119+
}
111120

112121
bool shouldSyncTriggers = true;
113122
string newHash = null;
114-
if (checkHash)
123+
if (isBackgroundSync)
115124
{
116-
newHash = await CheckHashAsync(hashBlob, json);
125+
newHash = await CheckHashAsync(hashBlob, payload.Content);
117126
shouldSyncTriggers = newHash != null;
118127
}
119128

120129
if (shouldSyncTriggers)
121130
{
122-
var (success, error) = await SetTriggersAsync(json);
131+
var (success, error) = await SetTriggersAsync(payload.Content);
123132
if (success && newHash != null)
124133
{
125134
await UpdateHashAsync(hashBlob, newHash);
@@ -254,19 +263,24 @@ internal async Task<CloudBlockBlob> GetHashBlobAsync()
254263
return _hashBlob;
255264
}
256265

257-
public async Task<string> GetSyncTriggersPayload()
266+
public async Task<SyncTriggersPayload> GetSyncTriggersPayload()
258267
{
259268
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
260269
var functionsMetadata = _functionMetadataProvider.GetFunctionMetadata();
261270

262271
// trigger information used by the ScaleController
263272
var triggers = await GetFunctionTriggers(functionsMetadata, hostOptions);
264273
var triggersArray = new JArray(triggers);
274+
int count = triggersArray.Count;
265275

266276
if (!ArmCacheEnabled)
267277
{
268278
// extended format is disabled - just return triggers
269-
return JsonConvert.SerializeObject(triggersArray);
279+
return new SyncTriggersPayload
280+
{
281+
Content = JsonConvert.SerializeObject(triggersArray),
282+
Count = count
283+
};
270284
}
271285

272286
// Add triggers to the payload
@@ -326,10 +340,18 @@ public async Task<string> GetSyncTriggersPayload()
326340
// limit. If we're over limit, revert to the minimal triggers
327341
// format.
328342
_logger.LogWarning($"SyncTriggers payload of length '{json.Length}' exceeds max length of '{ScriptConstants.MaxTriggersStringLength}'. Reverting to minimal format.");
329-
return JsonConvert.SerializeObject(triggersArray);
343+
return new SyncTriggersPayload
344+
{
345+
Content = JsonConvert.SerializeObject(triggersArray),
346+
Count = count
347+
};
330348
}
331349

332-
return json;
350+
return new SyncTriggersPayload
351+
{
352+
Content = json,
353+
Count = count
354+
};
333355
}
334356

335357
internal async Task<IEnumerable<JObject>> GetFunctionTriggers(IEnumerable<FunctionMetadata> functionsMetadata, ScriptJobHostOptions hostOptions)
@@ -586,6 +608,13 @@ public void Dispose()
586608
_syncSemaphore.Dispose();
587609
}
588610

611+
public class SyncTriggersPayload
612+
{
613+
public string Content { get; set; }
614+
615+
public int Count { get; set; }
616+
}
617+
589618
private class DurableConfig
590619
{
591620
public string HubName { get; set; }

src/WebJobs.Script.WebHost/Management/IFunctionsSyncManager.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ public interface IFunctionsSyncManager
1010
/// <summary>
1111
/// Sync function triggers with Antares infrastructure.
1212
/// </summary>
13-
/// <param name="checkHash">Indicates whether the last sync hash should be checked
14-
/// to conditonally perform the sync.</param>
13+
/// <param name="isBackgroundSync">Indicates whether this is a background sync operation.</param>
1514
/// <returns>The <see cref="SyncTriggersResult"/> for the request.</returns>
16-
Task<SyncTriggersResult> TrySyncTriggersAsync(bool checkHash = false);
15+
Task<SyncTriggersResult> TrySyncTriggersAsync(bool isBackgroundSync = false);
1716
}
1817
}

test/WebJobs.Script.Tests.Integration/Management/FunctionsSyncManagerTests.cs

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections;
65
using System.Collections.Generic;
76
using System.IO;
87
using System.IO.Abstractions;
@@ -13,9 +12,7 @@
1312
using System.Threading;
1413
using System.Threading.Tasks;
1514
using Microsoft.Azure.WebJobs.Host.Executors;
16-
using Microsoft.Azure.WebJobs.Script.BindingExtensions;
1715
using Microsoft.Azure.WebJobs.Script.Config;
18-
using Microsoft.Azure.WebJobs.Script.Models;
1916
using Microsoft.Azure.WebJobs.Script.WebHost;
2017
using Microsoft.Azure.WebJobs.Script.WebHost.Management;
2118
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
@@ -26,7 +23,6 @@
2623
using Moq;
2724
using Newtonsoft.Json;
2825
using Newtonsoft.Json.Linq;
29-
using NuGet.Packaging.Signing;
3026
using Xunit;
3127

3228
namespace Microsoft.Azure.WebJobs.Script.Tests.Managment
@@ -48,6 +44,7 @@ public class FunctionsSyncManagerTests : IDisposable
4844
private readonly Mock<IEnvironment> _mockEnvironment;
4945
private readonly HostNameProvider _hostNameProvider;
5046
private string _function1;
47+
private bool _emptyContent;
5148

5249
public FunctionsSyncManagerTests()
5350
{
@@ -284,14 +281,32 @@ private void VerifyResultWithCacheOff()
284281
}
285282

286283
[Fact]
287-
public async Task TrySyncTriggers_CheckHash_PostsExpectedContent()
284+
public async Task TrySyncTriggers_BackgroundSync_DoesNotPostsEmptyContent()
285+
{
286+
_emptyContent = true;
287+
288+
using (var env = new TestScopedEnvironmentVariable(_vars))
289+
{
290+
var syncResult = await _functionsSyncManager.TrySyncTriggersAsync(isBackgroundSync: true);
291+
Assert.Equal(0, _mockHttpHandler.RequestCount);
292+
Assert.Equal(0, _contentBuilder.Length);
293+
Assert.True(syncResult.Success);
294+
Assert.Null(syncResult.Error);
295+
296+
var logs = _loggerProvider.GetAllLogMessages().Select(p => p.FormattedMessage).ToArray();
297+
Assert.Equal("No functions found. Skipping Sync operation.", logs.Single());
298+
}
299+
}
300+
301+
[Fact]
302+
public async Task TrySyncTriggers_BackgroundSync_PostsExpectedContent()
288303
{
289304
using (var env = new TestScopedEnvironmentVariable(_vars))
290305
{
291306
var hashBlob = await _functionsSyncManager.GetHashBlobAsync();
292307
await hashBlob.DeleteIfExistsAsync();
293308

294-
var syncResult = await _functionsSyncManager.TrySyncTriggersAsync(checkHash: true);
309+
var syncResult = await _functionsSyncManager.TrySyncTriggersAsync(isBackgroundSync: true);
295310
Assert.True(syncResult.Success);
296311
Assert.Null(syncResult.Error);
297312
Assert.Equal(1, _mockHttpHandler.RequestCount);
@@ -318,7 +333,7 @@ public async Task TrySyncTriggers_CheckHash_PostsExpectedContent()
318333
_loggerProvider.ClearAllLogMessages();
319334
ResetMockFileSystem();
320335
_mockHttpHandler.Reset();
321-
syncResult = await _functionsSyncManager.TrySyncTriggersAsync(checkHash: true);
336+
syncResult = await _functionsSyncManager.TrySyncTriggersAsync(isBackgroundSync: true);
322337
Assert.Equal(0, _mockHttpHandler.RequestCount);
323338
Assert.Equal(0, _contentBuilder.Length);
324339
Assert.True(syncResult.Success);
@@ -331,23 +346,23 @@ public async Task TrySyncTriggers_CheckHash_PostsExpectedContent()
331346
// simulate a function change resulting in a new hash value
332347
ResetMockFileSystem("{}");
333348
_mockHttpHandler.Reset();
334-
syncResult = await _functionsSyncManager.TrySyncTriggersAsync(checkHash: true);
349+
syncResult = await _functionsSyncManager.TrySyncTriggersAsync(isBackgroundSync: true);
335350
Assert.Equal(1, _mockHttpHandler.RequestCount);
336351
Assert.True(syncResult.Success);
337352
Assert.Null(syncResult.Error);
338353
}
339354
}
340355

341356
[Fact]
342-
public async Task TrySyncTriggers_CheckHash_SetTriggersFailure_HashNotUpdated()
357+
public async Task TrySyncTriggers_BackgroundSync_SetTriggersFailure_HashNotUpdated()
343358
{
344359
using (var env = new TestScopedEnvironmentVariable(_vars))
345360
{
346361
var hashBlob = await _functionsSyncManager.GetHashBlobAsync();
347362
await hashBlob.DeleteIfExistsAsync();
348363

349364
_mockHttpHandler.MockStatusCode = HttpStatusCode.InternalServerError;
350-
var syncResult = await _functionsSyncManager.TrySyncTriggersAsync(checkHash: true);
365+
var syncResult = await _functionsSyncManager.TrySyncTriggersAsync(isBackgroundSync: true);
351366
Assert.False(syncResult.Success);
352367
string expectedErrorMessage = "SyncTriggers call failed (StatusCode=InternalServerError).";
353368
Assert.Equal(expectedErrorMessage, syncResult.Error);
@@ -674,12 +689,22 @@ private IFileSystem CreateFileSystem(ScriptApplicationHostOptions hostOptions, s
674689
dirBase.Setup(d => d.Exists(rootPath)).Returns(true);
675690
dirBase.Setup(d => d.Exists(Path.Combine(rootPath, "bin"))).Returns(true);
676691
dirBase.Setup(d => d.EnumerateDirectories(rootPath))
677-
.Returns(new[]
692+
.Returns(() =>
678693
{
679-
Path.Combine(rootPath, "bin"),
680-
Path.Combine(rootPath, "function1"),
681-
Path.Combine(rootPath, "function2"),
682-
Path.Combine(rootPath, "function3")
694+
if (_emptyContent)
695+
{
696+
return new string[0];
697+
}
698+
else
699+
{
700+
return new[]
701+
{
702+
Path.Combine(rootPath, "bin"),
703+
Path.Combine(rootPath, "function1"),
704+
Path.Combine(rootPath, "function2"),
705+
Path.Combine(rootPath, "function3")
706+
};
707+
}
683708
});
684709

685710
_function1 = @"{

0 commit comments

Comments
 (0)