Skip to content

Commit

Permalink
Fixing Specialization bug in List API
Browse files Browse the repository at this point in the history
  • Loading branch information
mathewc committed Nov 15, 2018
1 parent 4f6c0e9 commit 4a2ee70
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 26 deletions.
44 changes: 26 additions & 18 deletions src/WebJobs.Script.WebHost/Management/WebFunctionsManager.cs
Expand Up @@ -29,14 +29,14 @@ public class WebFunctionsManager : IWebFunctionsManager
private const string DurableTaskStorageConnectionName = "azureStorageConnectionStringName";
private const string DurableTask = "durableTask";

private readonly ScriptJobHostOptions _hostOptions;
private readonly IOptionsMonitor<ScriptApplicationHostOptions> _applicationHostOptions;
private readonly ILogger _logger;
private readonly HttpClient _client;
private readonly IEnumerable<WorkerConfig> _workerConfigs;

public WebFunctionsManager(IOptions<ScriptApplicationHostOptions> applicationHostOptions, IOptions<LanguageWorkerOptions> languageWorkerOptions, ILoggerFactory loggerFactory, HttpClient client)
public WebFunctionsManager(IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, IOptions<LanguageWorkerOptions> languageWorkerOptions, ILoggerFactory loggerFactory, HttpClient client)
{
_hostOptions = applicationHostOptions.Value.ToHostOptions();
_applicationHostOptions = applicationHostOptions;
_logger = loggerFactory?.CreateLogger(ScriptConstants.LogCategoryHostGeneral);
_client = client;
_workerConfigs = languageWorkerOptions.Value.WorkerConfigs;
Expand All @@ -50,8 +50,10 @@ public WebFunctionsManager(IOptions<ScriptApplicationHostOptions> applicationHos
/// <returns>collection of FunctionMetadataResponse</returns>
public async Task<IEnumerable<FunctionMetadataResponse>> GetFunctionsMetadata(HttpRequest request, bool includeProxies)
{
string routePrefix = await GetRoutePrefix(_hostOptions.RootScriptPath);
var tasks = GetFunctionsMetadata(includeProxies).Select(p => p.ToFunctionMetadataResponse(request, _hostOptions, routePrefix));
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();

string routePrefix = await GetRoutePrefix(hostOptions.RootScriptPath);
var tasks = GetFunctionsMetadata(includeProxies).Select(p => p.ToFunctionMetadataResponse(request, hostOptions, routePrefix));
return await tasks.WhenAll();
}

Expand All @@ -66,8 +68,9 @@ public async Task<IEnumerable<FunctionMetadataResponse>> GetFunctionsMetadata(Ht
/// <returns>(success, configChanged, functionMetadataResult)</returns>
public async Task<(bool, bool, FunctionMetadataResponse)> CreateOrUpdate(string name, FunctionMetadataResponse functionMetadata, HttpRequest request)
{
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
var configChanged = false;
var functionDir = Path.Combine(_hostOptions.RootScriptPath, name);
var functionDir = Path.Combine(hostOptions.RootScriptPath, name);

// Make sure the function folder exists
if (!FileUtility.DirectoryExists(functionDir))
Expand All @@ -79,7 +82,7 @@ public async Task<(bool, bool, FunctionMetadataResponse)> CreateOrUpdate(string

string newConfig = null;
string configPath = Path.Combine(functionDir, ScriptConstants.FunctionMetadataFileName);
string dataFilePath = FunctionMetadataExtensions.GetTestDataFilePath(name, _hostOptions);
string dataFilePath = FunctionMetadataExtensions.GetTestDataFilePath(name, hostOptions);

// If files are included, write them out
if (functionMetadata?.Files != null)
Expand Down Expand Up @@ -138,11 +141,12 @@ public async Task<(bool, FunctionMetadataResponse)> TryGetFunction(string name,
{
// TODO: DI (FACAVAL) Follow up with ahmels - Since loading of function metadata is no longer tied to the script host, we
// should be able to inject an IFunctionMetadataManager here and bypass this step.
var functionMetadata = FunctionMetadataManager.ReadFunctionMetadata(Path.Combine(_hostOptions.RootScriptPath, name), null, _workerConfigs, new Dictionary<string, ICollection<string>>(), fileSystem: FileUtility.Instance);
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
var functionMetadata = FunctionMetadataManager.ReadFunctionMetadata(Path.Combine(hostOptions.RootScriptPath, name), null, _workerConfigs, new Dictionary<string, ICollection<string>>(), fileSystem: FileUtility.Instance);
if (functionMetadata != null)
{
string routePrefix = await GetRoutePrefix(_hostOptions.RootScriptPath);
return (true, await functionMetadata.ToFunctionMetadataResponse(request, _hostOptions, routePrefix));
string routePrefix = await GetRoutePrefix(hostOptions.RootScriptPath);
return (true, await functionMetadata.ToFunctionMetadataResponse(request, hostOptions, routePrefix));
}
else
{
Expand All @@ -159,7 +163,8 @@ public async Task<(bool, FunctionMetadataResponse)> TryGetFunction(string name,
{
try
{
var functionPath = function.GetFunctionPath(_hostOptions);
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
var functionPath = function.GetFunctionPath(hostOptions);
if (!string.IsNullOrEmpty(functionPath))
{
FileUtility.DeleteDirectoryContentsSafe(functionPath);
Expand All @@ -180,9 +185,10 @@ public async Task<(bool, FunctionMetadataResponse)> TryGetFunction(string name,
/// <returns>(success, error)</returns>
public async Task<(bool success, string error)> TrySyncTriggers()
{
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
var durableTaskConfig = await ReadDurableTaskConfig();
var functionsTriggers = (await GetFunctionsMetadata()
.Select(f => f.ToFunctionTrigger(_hostOptions))
.Select(f => f.ToFunctionTrigger(hostOptions))
.WhenAll())
.Where(t => t != null)
.Select(t =>
Expand All @@ -206,7 +212,7 @@ public async Task<(bool success, string error)> TrySyncTriggers()
return t;
});

if (FileUtility.FileExists(Path.Combine(_hostOptions.RootScriptPath, ScriptConstants.ProxyMetadataFileName)))
if (FileUtility.FileExists(Path.Combine(hostOptions.RootScriptPath, ScriptConstants.ProxyMetadataFileName)))
{
// This is because we still need to scale function apps that are proxies only
functionsTriggers = functionsTriggers.Append(new JObject(new { type = "routingTrigger" }));
Expand Down Expand Up @@ -252,14 +258,14 @@ private async Task<(bool, string)> InternalSyncTriggers(IEnumerable<JObject> tri

internal IEnumerable<FunctionMetadata> GetFunctionsMetadata(bool includeProxies = false)
{
// get functions metadata
var functionDirectories = FileUtility.EnumerateDirectories(_hostOptions.RootScriptPath);
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
var functionDirectories = FileUtility.EnumerateDirectories(hostOptions.RootScriptPath);
IEnumerable<FunctionMetadata> functionsMetadata = FunctionMetadataManager.ReadFunctionsMetadata(functionDirectories, null, _workerConfigs, _logger, fileSystem: FileUtility.Instance);

if (includeProxies)
{
// get proxies metadata
var values = ProxyMetadataManager.ReadProxyMetadata(_hostOptions.RootScriptPath, _logger);
var values = ProxyMetadataManager.ReadProxyMetadata(hostOptions.RootScriptPath, _logger);
var proxyFunctionsMetadata = values.Item1;
if (proxyFunctionsMetadata?.Count > 0)
{
Expand All @@ -272,7 +278,8 @@ internal IEnumerable<FunctionMetadata> GetFunctionsMetadata(bool includeProxies

private async Task<Dictionary<string, string>> ReadDurableTaskConfig()
{
string hostJsonPath = Path.Combine(_hostOptions.RootScriptPath, ScriptConstants.HostMetadataFileName);
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
string hostJsonPath = Path.Combine(hostOptions.RootScriptPath, ScriptConstants.HostMetadataFileName);
var config = new Dictionary<string, string>();
if (FileUtility.FileExists(hostJsonPath))
{
Expand Down Expand Up @@ -316,7 +323,8 @@ internal IEnumerable<FunctionMetadata> GetFunctionsMetadata(bool includeProxies

private void DeleteFunctionArtifacts(FunctionMetadataResponse function)
{
var testDataPath = function.GetFunctionTestDataFilePath(_hostOptions);
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
var testDataPath = function.GetFunctionTestDataFilePath(hostOptions);

if (!string.IsNullOrEmpty(testDataPath))
{
Expand Down
29 changes: 29 additions & 0 deletions test/WebJobs.Script.Tests.Integration/Host/StandbyManagerTests.cs
Expand Up @@ -133,6 +133,13 @@ public async Task StandbyMode_EndToEnd_LinuxContainer()

await InitializeTestHostAsync("Linux", environment);

// verify only the Warmup function is present
// generally when in placeholder mode, the list API won't be called
// but we're doing this for regression testing
var functions = await ListFunctions();
Assert.Equal(1, functions.Length);
Assert.Equal("WarmUp", functions[0]);

await VerifyWarmupSucceeds();
await VerifyWarmupSucceeds(restart: true);

Expand Down Expand Up @@ -161,6 +168,12 @@ public async Task StandbyMode_EndToEnd_LinuxContainer()
Assert.False(environment.IsPlaceholderModeEnabled());
Assert.True(environment.IsContainerReady());

// verify that after specialization the correct
// app content is returned
functions = await ListFunctions();
Assert.Equal(1, functions.Length);
Assert.Equal("HttpTrigger", functions[0]);

// verify warmup function no longer there
request = new HttpRequestMessage(HttpMethod.Get, "api/warmup");
response = await _httpClient.SendAsync(request);
Expand Down Expand Up @@ -324,6 +337,22 @@ private async Task VerifyWarmupSucceeds(bool restart = false)
Assert.Equal("WarmUp complete.", responseBody);
}

private async Task<string[]> ListFunctions()
{
var secretManager = _httpServer.Host.Services.GetService<ISecretManagerProvider>().Current;
var keys = await secretManager.GetHostSecretsAsync();
string key = keys.MasterKey;

string uri = $"admin/functions?code={key}";
var request = new HttpRequestMessage(HttpMethod.Get, uri);

var response = await _httpClient.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
string responseBody = await response.Content.ReadAsStringAsync();
var functions = JArray.Parse(responseBody);
return functions.Select(p => (string)p.SelectToken("name")).ToArray();
}

private static async Task CreateContentZip(string contentRoot, string zipPath, params string[] copyDirs)
{
var contentTemp = Path.Combine(contentRoot, @"ZipContent");
Expand Down
Expand Up @@ -15,7 +15,7 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.Extensions
public class HttpRequestExtensionsTest
{
[Fact]
public void IsAntaresInternalRequest_ReturnsExpectedResult()
public void IsAppServiceInternalRequest_ReturnsExpectedResult()
{
// not running under Azure
var request = HttpTestHelpers.CreateHttpRequest("GET", "http://foobar");
Expand Down
22 changes: 15 additions & 7 deletions test/WebJobs.Script.Tests/Managment/WebFunctionsManagerTests.cs
Expand Up @@ -47,12 +47,16 @@ public async Task VerifyDurableTaskHubNameIsAdded()
const string expectedSyncTriggersPayload = "[{\"authLevel\":\"anonymous\",\"type\":\"httpTrigger\",\"direction\":\"in\",\"name\":\"req\",\"functionName\":\"function1\"}," +
"{\"name\":\"myQueueItem\",\"type\":\"orchestrationTrigger\",\"direction\":\"in\",\"queueName\":\"myqueue-items\",\"connection\":\"DurableStorage\",\"functionName\":\"function2\",\"taskHubName\":\"TestHubValue\"}," +
"{\"name\":\"myQueueItem\",\"type\":\"activityTrigger\",\"direction\":\"in\",\"queueName\":\"myqueue-items\",\"connection\":\"DurableStorage\",\"functionName\":\"function3\",\"taskHubName\":\"TestHubValue\"}]";
var settings = CreateWebSettings();
var fileSystem = CreateFileSystem(settings.ScriptPath);
var options = CreateApplicationHostOptions();
var fileSystem = CreateFileSystem(options.ScriptPath);
var loggerFactory = MockNullLogerFactory.CreateLoggerFactory();
var contentBuilder = new StringBuilder();
var httpClient = CreateHttpClient(contentBuilder);
var webManager = new WebFunctionsManager(new OptionsWrapper<ScriptApplicationHostOptions>(settings), new OptionsWrapper<LanguageWorkerOptions>(CreateLanguageWorkerConfigSettings()), loggerFactory, httpClient);
var factory = new TestOptionsFactory<ScriptApplicationHostOptions>(options);
var tokenSource = new TestChangeTokenSource();
var changeTokens = new[] { tokenSource };
var optionsMonitor = new OptionsMonitor<ScriptApplicationHostOptions>(factory, changeTokens, factory);
var webManager = new WebFunctionsManager(optionsMonitor, new OptionsWrapper<LanguageWorkerOptions>(CreateLanguageWorkerConfigSettings()), loggerFactory, httpClient);

FileUtility.Instance = fileSystem;

Expand Down Expand Up @@ -91,12 +95,16 @@ public static void ReadFunctionsMetadataSucceeds()
{
string functionsPath = Path.Combine(Environment.CurrentDirectory, @"..\..\..\..\..\sample");
// Setup
var settings = CreateWebSettings();
var fileSystem = CreateFileSystem(settings.ScriptPath);
var options = CreateApplicationHostOptions();
var fileSystem = CreateFileSystem(options.ScriptPath);
var loggerFactory = MockNullLogerFactory.CreateLoggerFactory();
var contentBuilder = new StringBuilder();
var httpClient = CreateHttpClient(contentBuilder);
var webManager = new WebFunctionsManager(new OptionsWrapper<ScriptApplicationHostOptions>(settings), new OptionsWrapper<LanguageWorkerOptions>(CreateLanguageWorkerConfigSettings()), loggerFactory, httpClient);
var factory = new TestOptionsFactory<ScriptApplicationHostOptions>(options);
var tokenSource = new TestChangeTokenSource();
var changeTokens = new[] { tokenSource };
var optionsMonitor = new OptionsMonitor<ScriptApplicationHostOptions>(factory, changeTokens, factory);
var webManager = new WebFunctionsManager(optionsMonitor, new OptionsWrapper<LanguageWorkerOptions>(CreateLanguageWorkerConfigSettings()), loggerFactory, httpClient);

FileUtility.Instance = fileSystem;
IEnumerable<FunctionMetadata> metadata = webManager.GetFunctionsMetadata();
Expand Down Expand Up @@ -131,7 +139,7 @@ private static HttpClient CreateHttpClient(StringBuilder writeContent)
return new HttpClient(new MockHttpHandler(writeContent));
}

private static ScriptApplicationHostOptions CreateWebSettings()
private static ScriptApplicationHostOptions CreateApplicationHostOptions()
{
return new ScriptApplicationHostOptions
{
Expand Down

0 comments on commit 4a2ee70

Please sign in to comment.