Skip to content

Commit 62337d0

Browse files
committed
Max length check/handling for SyncTriggers
1 parent bc3c22e commit 62337d0

File tree

3 files changed

+56
-16
lines changed

3 files changed

+56
-16
lines changed

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ public async Task<SyncTriggersResult> TrySyncTriggersAsync(bool checkHash = fals
9797
return result;
9898
}
9999

100-
var payload = await GetSyncTriggersPayload();
101-
string json = JsonConvert.SerializeObject(payload);
100+
string json = await GetSyncTriggersPayload();
102101

103102
bool shouldSyncTriggers = true;
104103
string newHash = null;
@@ -245,7 +244,7 @@ internal async Task<CloudBlockBlob> GetHashBlobAsync()
245244
return _hashBlob;
246245
}
247246

248-
public async Task<JToken> GetSyncTriggersPayload()
247+
public async Task<string> GetSyncTriggersPayload()
249248
{
250249
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
251250
var functionsMetadata = WebFunctionsManager.GetFunctionsMetadata(hostOptions, _workerConfigs, _logger);
@@ -257,7 +256,7 @@ public async Task<JToken> GetSyncTriggersPayload()
257256
if (!ArmCacheEnabled)
258257
{
259258
// extended format is disabled - just return triggers
260-
return triggersArray;
259+
return JsonConvert.SerializeObject(triggersArray);
261260
}
262261

263262
// Add triggers to the payload
@@ -310,7 +309,17 @@ public async Task<JToken> GetSyncTriggersPayload()
310309
// like KeyVault when the feature comes online
311310
}
312311

313-
return result;
312+
string json = JsonConvert.SerializeObject(result);
313+
if (json.Length > ScriptConstants.MaxTriggersStringLength)
314+
{
315+
// The settriggers call to the FE enforces a max request size
316+
// limit. If we're over limit, revert to the minimal triggers
317+
// format.
318+
_logger.LogWarning($"SyncTriggers payload of length '{json.Length}' exceeds max length of '{ScriptConstants.MaxTriggersStringLength}'. Reverting to minimal format.");
319+
return JsonConvert.SerializeObject(triggersArray);
320+
}
321+
322+
return json;
314323
}
315324

316325
internal async Task<IEnumerable<JObject>> GetFunctionTriggers(IEnumerable<FunctionMetadata> functionsMetadata, ScriptJobHostOptions hostOptions)
@@ -420,9 +429,12 @@ internal HttpRequestMessage BuildSetTriggersRequest()
420429
if (ArmCacheEnabled)
421430
{
422431
// sanitize the content before logging
423-
var sanitizedContent = JObject.Parse(content);
424-
sanitizedContent.Remove("secrets");
425-
sanitizedContentString = sanitizedContent.ToString();
432+
var sanitizedContent = JToken.Parse(content);
433+
if (sanitizedContent.Type == JTokenType.Object)
434+
{
435+
((JObject)sanitizedContent).Remove("secrets");
436+
sanitizedContentString = sanitizedContent.ToString();
437+
}
426438
}
427439

428440
using (var request = BuildSetTriggersRequest())

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ public static class ScriptConstants
106106
public const int MaximumHostIdLength = 32;
107107
public const int DynamicSkuConnectionLimit = 50;
108108

109+
/// <summary>
110+
/// This constant is also defined in Antares, where the limit is ultimately enforced
111+
/// for settriggers calls. If we raise that limit there, we should raise here as well.
112+
/// </summary>
113+
public const int MaxTriggersStringLength = 102400;
114+
109115
public const string ExtensionsProjectFileName = "extensions.csproj";
110116
public const string MetadataGeneratorPackageId = "Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator";
111117
public const string MetadataGeneratorPackageVersion = "1.1.*";

test/WebJobs.Script.Tests/Managment/FunctionsSyncManagerTests.cs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class FunctionsSyncManagerTests : IDisposable
4040
private readonly Mock<IScriptWebHostEnvironment> _mockWebHostEnvironment;
4141
private readonly Mock<IEnvironment> _mockEnvironment;
4242
private readonly HostNameProvider _hostNameProvider;
43+
private string _function1;
4344

4445
public FunctionsSyncManagerTests()
4546
{
@@ -139,6 +140,27 @@ public async Task TrySyncTriggers_StandbyMode_ReturnsFalse()
139140
}
140141
}
141142

143+
[Fact]
144+
public async Task TrySyncTriggers_MaxSyncTriggersPayloadSize_Succeeds()
145+
{
146+
// create a dummy file that pushes us over size
147+
string maxString = new string('x', ScriptConstants.MaxTriggersStringLength + 1);
148+
_function1 = $"{{ bindings: [], test: '{maxString}'}}";
149+
150+
using (var env = new TestScopedEnvironmentVariable(_vars))
151+
{
152+
Assert.True(_functionsSyncManager.ArmCacheEnabled);
153+
154+
var result = await _functionsSyncManager.TrySyncTriggersAsync();
155+
Assert.True(result.Success);
156+
157+
string syncString = _contentBuilder.ToString();
158+
Assert.True(syncString.Length < ScriptConstants.MaxTriggersStringLength);
159+
var syncContent = JToken.Parse(syncString);
160+
Assert.Equal(JTokenType.Array, syncContent.Type);
161+
}
162+
}
163+
142164
[Fact]
143165
public async Task TrySyncTriggers_LocalEnvironment_ReturnsFalse()
144166
{
@@ -370,9 +392,9 @@ private static LanguageWorkerOptions CreateLanguageWorkerConfigSettings()
370392
};
371393
}
372394

373-
private static IFileSystem CreateFileSystem(ScriptApplicationHostOptions hostOptions, string hostJsonContent = null)
395+
private IFileSystem CreateFileSystem(ScriptApplicationHostOptions hostOptions, string hostJsonContent = null)
374396
{
375-
string rootPath = hostOptions.ScriptPath;
397+
var rootPath = hostOptions.ScriptPath;
376398
string testDataPath = hostOptions.TestDataPath;
377399

378400
var fullFileSystem = new FileSystem();
@@ -412,7 +434,7 @@ private static IFileSystem CreateFileSystem(ScriptApplicationHostOptions hostOpt
412434
Path.Combine(rootPath, "function3")
413435
});
414436

415-
var function1 = @"{
437+
_function1 = @"{
416438
""scriptFile"": ""main.py"",
417439
""disabled"": false,
418440
""bindings"": [
@@ -459,14 +481,14 @@ private static IFileSystem CreateFileSystem(ScriptApplicationHostOptions hostOpt
459481

460482
fileBase.Setup(f => f.Exists(Path.Combine(rootPath, @"function1\function.json"))).Returns(true);
461483
fileBase.Setup(f => f.Exists(Path.Combine(rootPath, @"function1\main.py"))).Returns(true);
462-
fileBase.Setup(f => f.ReadAllText(Path.Combine(rootPath, @"function1\function.json"))).Returns(function1);
484+
fileBase.Setup(f => f.ReadAllText(Path.Combine(rootPath, @"function1\function.json"))).Returns(_function1);
463485
fileBase.Setup(f => f.Open(Path.Combine(rootPath, @"function1\function.json"), It.IsAny<FileMode>(), It.IsAny<FileAccess>(), It.IsAny<FileShare>())).Returns(() =>
464486
{
465-
return new MemoryStream(Encoding.UTF8.GetBytes(function1));
487+
return new MemoryStream(Encoding.UTF8.GetBytes(_function1));
466488
});
467489
fileBase.Setup(f => f.Open(Path.Combine(testDataPath, "function1.dat"), It.IsAny<FileMode>(), It.IsAny<FileAccess>(), It.IsAny<FileShare>())).Returns(() =>
468490
{
469-
return new MemoryStream(Encoding.UTF8.GetBytes(function1));
491+
return new MemoryStream(Encoding.UTF8.GetBytes(_function1));
470492
});
471493

472494
fileBase.Setup(f => f.Exists(Path.Combine(rootPath, @"function2\function.json"))).Returns(true);
@@ -478,7 +500,7 @@ private static IFileSystem CreateFileSystem(ScriptApplicationHostOptions hostOpt
478500
});
479501
fileBase.Setup(f => f.Open(Path.Combine(testDataPath, "function2.dat"), It.IsAny<FileMode>(), It.IsAny<FileAccess>(), It.IsAny<FileShare>())).Returns(() =>
480502
{
481-
return new MemoryStream(Encoding.UTF8.GetBytes(function1));
503+
return new MemoryStream(Encoding.UTF8.GetBytes(_function1));
482504
});
483505

484506
fileBase.Setup(f => f.Exists(Path.Combine(rootPath, @"function3\function.json"))).Returns(true);
@@ -490,7 +512,7 @@ private static IFileSystem CreateFileSystem(ScriptApplicationHostOptions hostOpt
490512
});
491513
fileBase.Setup(f => f.Open(Path.Combine(testDataPath, "function3.dat"), It.IsAny<FileMode>(), It.IsAny<FileAccess>(), It.IsAny<FileShare>())).Returns(() =>
492514
{
493-
return new MemoryStream(Encoding.UTF8.GetBytes(function1));
515+
return new MemoryStream(Encoding.UTF8.GetBytes(_function1));
494516
});
495517

496518
return fileSystem.Object;

0 commit comments

Comments
 (0)