Upgrading WebJobs.Host.Storage to v12 Azure Storage sdk (#2764)
Changes in this PR: - Upgrading WebJobs.Host.Storage to use v12 Azure storage SDK - New major version of WebJobs.Host.Storage - New public types for IDistributedLockManager implementation, IAzureBlobStorageProvider - Removing Track1 Microsoft.Azure.WebJobs.Extensions.Storage from solution and build files (code remains in repo) - Migrating E2E and Functional Tests to use the Track2 Microsoft.Azure.WebJobs.Extensions.Storage packages
This commit is contained in:
Родитель
8c90eb9bf2
Коммит
b6d5b52da5
|
@ -20,7 +20,6 @@ $projects =
|
|||
"src\Microsoft.Azure.WebJobs.Host\WebJobs.Host.Sources.csproj",
|
||||
"src\Microsoft.Azure.WebJobs.Logging\WebJobs.Logging.csproj",
|
||||
"src\Microsoft.Azure.WebJobs.Logging.ApplicationInsights\WebJobs.Logging.ApplicationInsights.csproj",
|
||||
"src\Microsoft.Azure.WebJobs.Extensions.Storage\WebJobs.Extensions.Storage.csproj",
|
||||
"src\Microsoft.Azure.WebJobs.Host.Storage\WebJobs.Host.Storage.csproj",
|
||||
"test\Microsoft.Azure.WebJobs.Host.TestCommon\WebJobs.Host.TestCommon.csproj"
|
||||
|
||||
|
|
15
WebJobs.sln
15
WebJobs.sln
|
@ -35,14 +35,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{3B089351
|
|||
EndProject
|
||||
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "WebJobs.Shared", "src\Microsoft.Azure.WebJobs.Shared\WebJobs.Shared.shproj", "{ADD036F5-2170-4B05-9E0A-C2ED0A08A929}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebJobs.Extensions.Storage", "src\Microsoft.Azure.WebJobs.Extensions.Storage\WebJobs.Extensions.Storage.csproj", "{A9733406-267C-4A53-AB07-D3A834E22153}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebJobs.Host.Storage", "src\Microsoft.Azure.WebJobs.Host.Storage\WebJobs.Host.Storage.csproj", "{DED33098-FE99-436C-96CC-B59A30BEF027}"
|
||||
EndProject
|
||||
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "WebJobs.Shared.Storage", "src\Microsoft.Azure.WebJobs.Shared.Storage\WebJobs.Shared.Storage.shproj", "{6BED7F8A-A199-4D9D-85D1-6856EE3292C6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebJobs.Extensions.Storage.UnitTests", "test\Microsoft.Azure.Webjobs.Extensions.Storage.UnitTests\WebJobs.Extensions.Storage.UnitTests.csproj", "{0CC5741F-ACDA-4DB8-9C17-074E8896F244}"
|
||||
EndProject
|
||||
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "WebJobs.Protocols", "src\Microsoft.Azure.WebJobs.Protocols\WebJobs.Protocols.shproj", "{6FCD0852-6019-4CD5-9B7E-0DE021A72BD7}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FakeAzureStorage", "test\FakeStorage\FakeAzureStorage.csproj", "{337B79EB-A3CB-4CE0-A7F2-DD5E638AC882}"
|
||||
|
@ -59,8 +55,6 @@ Global
|
|||
src\Microsoft.Azure.WebJobs.Shared\WebJobs.Shared.projitems*{39554abe-d7b7-45ff-8d7c-56432abcde3d}*SharedItemsImports = 5
|
||||
src\Microsoft.Azure.WebJobs.Shared.Storage\Microsoft.Azure.WebJobs.Shared.Storage.projitems*{6bed7f8a-a199-4d9d-85d1-6856ee3292c6}*SharedItemsImports = 13
|
||||
src\Microsoft.Azure.WebJobs.Protocols\Microsoft.Azure.WebJobs.Protocols.projitems*{6fcd0852-6019-4cd5-9b7e-0de021a72bd7}*SharedItemsImports = 13
|
||||
src\Microsoft.Azure.WebJobs.Shared.Storage\Microsoft.Azure.WebJobs.Shared.Storage.projitems*{a9733406-267c-4a53-ab07-d3a834e22153}*SharedItemsImports = 5
|
||||
src\Microsoft.Azure.WebJobs.Shared\WebJobs.Shared.projitems*{a9733406-267c-4a53-ab07-d3a834e22153}*SharedItemsImports = 5
|
||||
src\Microsoft.Azure.WebJobs.Shared\WebJobs.Shared.projitems*{add036f5-2170-4b05-9e0a-c2ed0a08a929}*SharedItemsImports = 13
|
||||
src\Microsoft.Azure.WebJobs.Shared.Storage\Microsoft.Azure.WebJobs.Shared.Storage.projitems*{ded33098-fe99-436c-96cc-b59a30bef027}*SharedItemsImports = 5
|
||||
src\Microsoft.Azure.WebJobs.Shared\WebJobs.Shared.projitems*{ded33098-fe99-436c-96cc-b59a30bef027}*SharedItemsImports = 5
|
||||
|
@ -110,18 +104,10 @@ Global
|
|||
{340AB554-5482-4B3D-B65F-46DFF5AF1684}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{340AB554-5482-4B3D-B65F-46DFF5AF1684}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{340AB554-5482-4B3D-B65F-46DFF5AF1684}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A9733406-267C-4A53-AB07-D3A834E22153}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9733406-267C-4A53-AB07-D3A834E22153}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9733406-267C-4A53-AB07-D3A834E22153}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A9733406-267C-4A53-AB07-D3A834E22153}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DED33098-FE99-436C-96CC-B59A30BEF027}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DED33098-FE99-436C-96CC-B59A30BEF027}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DED33098-FE99-436C-96CC-B59A30BEF027}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DED33098-FE99-436C-96CC-B59A30BEF027}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0CC5741F-ACDA-4DB8-9C17-074E8896F244}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0CC5741F-ACDA-4DB8-9C17-074E8896F244}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0CC5741F-ACDA-4DB8-9C17-074E8896F244}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0CC5741F-ACDA-4DB8-9C17-074E8896F244}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{337B79EB-A3CB-4CE0-A7F2-DD5E638AC882}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{337B79EB-A3CB-4CE0-A7F2-DD5E638AC882}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{337B79EB-A3CB-4CE0-A7F2-DD5E638AC882}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
@ -145,7 +131,6 @@ Global
|
|||
{C6B834AB-7B6A-47AE-A7C3-C102B0C861FF} = {639967B0-0544-4C52-94AC-9A3D25E33256}
|
||||
{C8EAAE01-E8CF-4131-9D4B-F0FDF00DA4BE} = {639967B0-0544-4C52-94AC-9A3D25E33256}
|
||||
{340AB554-5482-4B3D-B65F-46DFF5AF1684} = {639967B0-0544-4C52-94AC-9A3D25E33256}
|
||||
{0CC5741F-ACDA-4DB8-9C17-074E8896F244} = {639967B0-0544-4C52-94AC-9A3D25E33256}
|
||||
{337B79EB-A3CB-4CE0-A7F2-DD5E638AC882} = {639967B0-0544-4C52-94AC-9A3D25E33256}
|
||||
{C5E1A8E8-711F-4377-A8BD-7DB58E6C580D} = {639967B0-0544-4C52-94AC-9A3D25E33256}
|
||||
{11702A4B-8402-4082-BE38-4F0C2CBBF61D} = {C5E1A8E8-711F-4377-A8BD-7DB58E6C580D}
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
<PropertyGroup>
|
||||
<!-- Packages can have independent versions and only increment when released -->
|
||||
<Version>3.0.31$(VersionSuffix)</Version>
|
||||
<ExtensionsStorageVersion>4.0.5$(VersionSuffix)</ExtensionsStorageVersion>
|
||||
<HostStorageVersion>4.0.4$(VersionSuffix)</HostStorageVersion>
|
||||
<HostStorageVersion>5.0.0-beta.1$(VersionSuffix)</HostStorageVersion>
|
||||
<LoggingVersion>4.0.2$(VersionSuffix)</LoggingVersion>
|
||||
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
|
|
|
@ -4,8 +4,7 @@ $projects =
|
|||
"test\Microsoft.Azure.WebJobs.Host.UnitTests",
|
||||
"test\Microsoft.Azure.WebJobs.Host.FunctionalTests",
|
||||
"test\Microsoft.Azure.WebJobs.Logging.FunctionalTests",
|
||||
"test\Microsoft.Azure.WebJobs.Host.EndToEndTests",
|
||||
"test\Microsoft.Azure.Webjobs.Extensions.Storage.UnitTests"
|
||||
"test\Microsoft.Azure.WebJobs.Host.EndToEndTests"
|
||||
|
||||
|
||||
foreach ($project in $projects)
|
||||
|
|
|
@ -18,7 +18,8 @@ namespace SampleHost
|
|||
.ConfigureWebJobs(b =>
|
||||
{
|
||||
b.AddAzureStorageCoreServices()
|
||||
.AddAzureStorage()
|
||||
.AddAzureStorageBlobs()
|
||||
.AddAzureStorageQueues()
|
||||
.AddServiceBus()
|
||||
.AddEventHubs();
|
||||
})
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.EventHubs" Version="3.0.6" />
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.ServiceBus" Version="3.1.1" />
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="5.0.0-beta.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.1.1" />
|
||||
|
@ -22,7 +23,6 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Extensions.Storage\WebJobs.Extensions.Storage.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Host.Storage\WebJobs.Host.Storage.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Host\WebJobs.Host.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Logging.ApplicationInsights\WebJobs.Logging.ApplicationInsights.csproj" />
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Azure.Storage.Blobs;
|
||||
using Microsoft.Extensions.Azure;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles instantiating Azure storage clients from an <see cref="IConfiguration"/> source.
|
||||
/// Certain configuration settings are used to instantiate the clients. These include settings
|
||||
/// necessary to construct the Azure service URIs and settings to specify credential related
|
||||
/// information (i.e. clientId, tenantId, etc. where applicable).
|
||||
/// <see cref="Microsoft.Extensions.Azure.ClientFactory"/> is where a bulk of the
|
||||
/// <see cref="IConfiguration"/> source is read.
|
||||
/// This implementation adds extra configuration by using <see cref="StorageServiceUriOptions"/> to bind to
|
||||
/// a particular <see cref="IConfigurationSection"/>.
|
||||
/// </summary>
|
||||
internal class AzureStorageProvider : IAzureBlobStorageProvider
|
||||
{
|
||||
private readonly BlobServiceClientProvider _blobServiceClientProvider;
|
||||
private readonly ILogger<AzureStorageProvider> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IOptionsMonitor<JobHostInternalStorageOptions> _storageOptions;
|
||||
|
||||
public AzureStorageProvider(IConfiguration configuration, IOptionsMonitor<JobHostInternalStorageOptions> options, ILogger<AzureStorageProvider> logger, AzureComponentFactory componentFactory, AzureEventSourceLogForwarder logForwarder)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_storageOptions = options;
|
||||
_logger = logger;
|
||||
|
||||
_blobServiceClientProvider = new BlobServiceClientProvider(componentFactory, logForwarder);
|
||||
}
|
||||
|
||||
public virtual bool TryCreateHostingBlobContainerClient(out BlobContainerClient blobContainerClient)
|
||||
{
|
||||
if (_storageOptions?.CurrentValue.InternalSasBlobContainer != null)
|
||||
{
|
||||
blobContainerClient = new BlobContainerClient(new Uri(_storageOptions.CurrentValue.InternalSasBlobContainer));
|
||||
_logger.LogDebug($"Using storage account {blobContainerClient.AccountName} and container {blobContainerClient.Name} for hosting BlobContainerClient.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryCreateBlobServiceClientFromConnection(ConnectionStringNames.Storage, out BlobServiceClient blobServiceClient))
|
||||
{
|
||||
_logger.LogDebug($"Could not create BlobContainerClient using Connection: {ConnectionStringNames.Storage}");
|
||||
blobContainerClient = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
blobContainerClient = blobServiceClient.GetBlobContainerClient(HostContainerNames.Hosts);
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual bool TryCreateBlobServiceClientFromConnection(string connection, out BlobServiceClient client)
|
||||
{
|
||||
var connectionToUse = connection ?? ConnectionStringNames.Storage;
|
||||
|
||||
try
|
||||
{
|
||||
client = _blobServiceClientProvider.Create(connectionToUse, _configuration);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogDebug($"Could not create BlobServiceClient. Exception: {e}");
|
||||
client = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Azure.Core;
|
||||
using Azure.Storage.Blobs;
|
||||
using Microsoft.Extensions.Azure;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// Pass-through class to create a <see cref="BlobServiceClient"/> using <see cref="AzureComponentFactory"/>.
|
||||
/// This class uses additional configuration settings for a specified connection in addition to those in <see cref="AzureComponentFactory"/>.
|
||||
/// To support scenarios where a storage connection (i.e. AzureWebJobsStorage) needs to reference multiple serviceUris (blob and queue),
|
||||
/// properties in <see cref="StorageServiceUriOptions"/> are supported to create the Azure blob service URI.
|
||||
/// </summary>
|
||||
internal class BlobServiceClientProvider : StorageClientProvider<BlobServiceClient, BlobClientOptions>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlobServiceClientProvider"/> class that uses the registered Azure services to create a BlobServiceClient.
|
||||
/// </summary>
|
||||
/// <param name="componentFactory">The Azure factory responsible for creating clients. <see cref="AzureComponentFactory"/></param>
|
||||
/// <param name="logForwarder">Log forwarder that forwards events to ILogger. <see cref="AzureEventSourceLogForwarder"/></param>
|
||||
public BlobServiceClientProvider(AzureComponentFactory componentFactory, AzureEventSourceLogForwarder logForwarder)
|
||||
: base(componentFactory, logForwarder) { }
|
||||
|
||||
/// <summary>
|
||||
/// Provides logic to create a <see cref="BlobServiceClient"/> from an <see cref="IConfiguration"/> source. This class
|
||||
/// supports using configuration settings (AccountName and BlobServiceUri) in <see cref="StorageServiceUriOptions"/> in
|
||||
/// addition to those in <see cref="AzureComponentFactory"/>.
|
||||
/// If no serviceUri can be constructed using the additional settings, this method falls back to the logic in
|
||||
/// <see cref="AzureComponentFactory"/>.
|
||||
/// </summary>
|
||||
/// <param name="configuration">Configuration to retrieve settings from.</param>
|
||||
/// <param name="tokenCredential">Credential for the client.</param>
|
||||
/// <param name="options">Options to configure the client.</param>
|
||||
/// <returns>An instance of <see cref="BlobServiceClient"/></returns>
|
||||
protected override BlobServiceClient CreateClient(IConfiguration configuration, TokenCredential tokenCredential, BlobClientOptions options)
|
||||
{
|
||||
// If connection string is present, it will be honored first; bypass creating a serviceUri
|
||||
if (!IsConnectionStringPresent(configuration))
|
||||
{
|
||||
var serviceUri = configuration.Get<StorageServiceUriOptions>().GetBlobServiceUri();
|
||||
if (serviceUri != null)
|
||||
{
|
||||
return new BlobServiceClient(serviceUri, tokenCredential, options);
|
||||
}
|
||||
}
|
||||
|
||||
return base.CreateClient(configuration, tokenCredential, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An options class that constructs a storage service URI from a different properties.
|
||||
/// These properties are specific to WebJobs, as there may be other relevant properties used downstream
|
||||
/// to create storage clients.
|
||||
/// <seealso cref="Microsoft.Extensions.Azure.ClientFactory" />
|
||||
/// A storage service URI can be built using just the account name along with default
|
||||
/// parameters for Scheme and Endpoint Suffix.
|
||||
/// </summary>
|
||||
internal class StorageServiceUriOptions
|
||||
{
|
||||
private const string DefaultScheme = "https";
|
||||
private const string DefaultEndpointSuffix = "core.windows.net";
|
||||
|
||||
/// <summary>
|
||||
/// The resource URI for blob storage. If this property is given explicitly, it will be
|
||||
/// honored over the AccountName property.
|
||||
/// </summary>
|
||||
public string BlobServiceUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the storage account.
|
||||
/// </summary>
|
||||
public string AccountName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the blob service URI from the properties in this class.
|
||||
/// First checks if BlobServiceUri is specified. If not, the AccountName is used
|
||||
/// to construct a blob service URI with https scheme and core.windows.net endpoint suffix.
|
||||
/// </summary>
|
||||
/// <returns>Service URI to Azure blob storage</returns>
|
||||
public Uri GetBlobServiceUri()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(BlobServiceUri))
|
||||
{
|
||||
return new Uri(BlobServiceUri);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(AccountName))
|
||||
{
|
||||
var uri = string.Format(CultureInfo.InvariantCulture, "{0}://{1}.blob.{2}", DefaultScheme, AccountName, DefaultEndpointSuffix);
|
||||
return new Uri(uri);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,15 +4,14 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Azure;
|
||||
using Azure.Storage.Blobs;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Scale;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Azure.WebJobs.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
@ -20,17 +19,16 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
{
|
||||
internal class BlobStorageConcurrencyStatusRepository : IConcurrencyStatusRepository
|
||||
{
|
||||
private const string HostContainerName = "azure-webjobs-hosts";
|
||||
private readonly IHostIdProvider _hostIdProvider;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger _logger;
|
||||
private CloudBlobContainer? _blobContainer;
|
||||
private readonly IAzureBlobStorageProvider _blobStorageProvider;
|
||||
private BlobContainerClient? _blobContainerClient;
|
||||
|
||||
public BlobStorageConcurrencyStatusRepository(IConfiguration configuration, IHostIdProvider hostIdProvider, ILoggerFactory loggerFactory)
|
||||
public BlobStorageConcurrencyStatusRepository(IHostIdProvider hostIdProvider, ILoggerFactory loggerFactory, IAzureBlobStorageProvider azureStorageProvider)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_hostIdProvider = hostIdProvider;
|
||||
_logger = loggerFactory.CreateLogger(LogCategories.Concurrency);
|
||||
_blobStorageProvider = azureStorageProvider;
|
||||
}
|
||||
|
||||
public async Task<HostConcurrencySnapshot?> ReadAsync(CancellationToken cancellationToken)
|
||||
|
@ -39,11 +37,12 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
|
||||
try
|
||||
{
|
||||
CloudBlobContainer? container = await GetContainerAsync(cancellationToken);
|
||||
if (container != null)
|
||||
BlobContainerClient? containerClient = await GetContainerClientAsync(cancellationToken);
|
||||
if (containerClient != null)
|
||||
{
|
||||
CloudBlockBlob blob = container.GetBlockBlobReference(blobPath);
|
||||
string content = await blob.DownloadTextAsync(cancellationToken);
|
||||
BlobClient blobClient = containerClient.GetBlobClient(blobPath);
|
||||
string content = await blobClient.DownloadTextAsync(cancellationToken: cancellationToken);
|
||||
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
var result = JsonConvert.DeserializeObject<HostConcurrencySnapshot>(content);
|
||||
|
@ -51,8 +50,9 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
}
|
||||
}
|
||||
}
|
||||
catch (StorageException stex) when (stex.RequestInformation?.HttpStatusCode == 404)
|
||||
catch (RequestFailedException exception) when (exception.Status == 404)
|
||||
{
|
||||
// we haven't recorded a status yet
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
@ -70,16 +70,12 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
|
||||
try
|
||||
{
|
||||
CloudBlobContainer? container = await GetContainerAsync(cancellationToken);
|
||||
if (container != null)
|
||||
BlobContainerClient? containerClient = await GetContainerClientAsync(cancellationToken);
|
||||
if (containerClient != null)
|
||||
{
|
||||
CloudBlockBlob blob = container.GetBlockBlobReference(blobPath);
|
||||
|
||||
using (StreamWriter writer = new StreamWriter(await blob.OpenWriteAsync(cancellationToken)))
|
||||
{
|
||||
var content = JsonConvert.SerializeObject(snapshot);
|
||||
await writer.WriteAsync(content);
|
||||
}
|
||||
BlobClient blobClient = containerClient.GetBlobClient(blobPath);
|
||||
var content = JsonConvert.SerializeObject(snapshot);
|
||||
await blobClient.UploadTextAsync(content, overwrite: true, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
@ -89,21 +85,14 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
}
|
||||
}
|
||||
|
||||
internal async Task<CloudBlobContainer?> GetContainerAsync(CancellationToken cancellationToken)
|
||||
internal async Task<BlobContainerClient?> GetContainerClientAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_blobContainer == null)
|
||||
if (_blobContainerClient == null && _blobStorageProvider.TryCreateHostingBlobContainerClient(out _blobContainerClient))
|
||||
{
|
||||
string storageConnectionString = _configuration.GetWebJobsConnectionString(ConnectionStringNames.Storage);
|
||||
if (!string.IsNullOrEmpty(storageConnectionString) && CloudStorageAccount.TryParse(storageConnectionString, out CloudStorageAccount account))
|
||||
{
|
||||
var client = account.CreateCloudBlobClient();
|
||||
_blobContainer = client.GetContainerReference(HostContainerName);
|
||||
|
||||
await _blobContainer.CreateIfNotExistsAsync(cancellationToken);
|
||||
}
|
||||
await _blobContainerClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
return _blobContainer;
|
||||
return _blobContainerClient;
|
||||
}
|
||||
|
||||
internal async Task<string> GetBlobPathAsync(CancellationToken cancellationToken)
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using Azure.Storage.Blobs;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for creating Azure blob storage clients, ensuring all necessary configuration is applied.
|
||||
/// Implementations are responsible instantiating these clients and using desired options, credentials, or service URIs.
|
||||
/// </summary>
|
||||
public interface IAzureBlobStorageProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to create a client for the hosting container used for internal storage.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="BlobContainerClient"/> for the hosting container.</returns>
|
||||
bool TryCreateHostingBlobContainerClient(out BlobContainerClient blobContainerClient);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to create the <see cref="BlobServiceClient"/> from the specified connection.
|
||||
/// </summary>
|
||||
/// <param name="connection">connection name to use.</param>
|
||||
/// <param name="client"><see cref="BlobServiceClient"/> to instantiate.</param>
|
||||
/// <returns>Whether the attempt was successful.</returns>
|
||||
bool TryCreateBlobServiceClientFromConnection(string connection, out BlobServiceClient client);
|
||||
}
|
||||
}
|
|
@ -1,37 +1,8 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs
|
||||
{
|
||||
/// <summary>
|
||||
/// Config object for providing a container for distributed lock manager.
|
||||
/// This is hydrated from a <see cref="JobHostInternalStorageOptions"/>
|
||||
/// </summary>
|
||||
public class DistributedLockManagerContainerProvider
|
||||
{
|
||||
public DistributedLockManagerContainerProvider() { }
|
||||
public DistributedLockManagerContainerProvider(IOptions<JobHostInternalStorageOptions> x )
|
||||
{
|
||||
var sasBlobContainer = x.Value.InternalSasBlobContainer;
|
||||
if (sasBlobContainer != null)
|
||||
{
|
||||
var uri = new Uri(sasBlobContainer);
|
||||
this.InternalContainer = new CloudBlobContainer(uri);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A SAS to a Blob Container. This allows services to create blob leases and do distributed locking.
|
||||
/// If this is set, <see cref="JobHostConfiguration.StorageConnectionString"/> and
|
||||
/// <see cref="JobHostConfiguration.DashboardConnectionString"/> can be set to null and the runtime will use the container.
|
||||
/// </summary>
|
||||
public CloudBlobContainer InternalContainer { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The storage configuration that the JobHost needs for its own operations (independent of binding)
|
||||
/// For example, this can support <see cref="SingletonAttribute"/>, blob leases, timers, etc.
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Azure.WebJobs.Host.Bindings;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
using Microsoft.Azure.WebJobs.Host.Timers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using WebJobs.Host.Storage.Logging;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.Loggers
|
||||
{
|
||||
// Wrap facilities for logging a function's output.
|
||||
// This means capturing console out, redirecting to a TraceWriter that is available at a blob.
|
||||
// Handle incremental updates to get real-time updates for long running functions.
|
||||
internal sealed class BlobFunctionOutputDefinition : IFunctionOutputDefinition
|
||||
{
|
||||
private readonly CloudBlobClient _client;
|
||||
private readonly LocalBlobDescriptor _outputBlob;
|
||||
private readonly LocalBlobDescriptor _parameterLogBlob;
|
||||
|
||||
public BlobFunctionOutputDefinition(CloudBlobClient client, LocalBlobDescriptor outputBlob, LocalBlobDescriptor parameterLogBlob)
|
||||
{
|
||||
_client = client;
|
||||
_outputBlob = outputBlob;
|
||||
_parameterLogBlob = parameterLogBlob;
|
||||
}
|
||||
|
||||
public LocalBlobDescriptor OutputBlob
|
||||
{
|
||||
get { return _outputBlob; }
|
||||
}
|
||||
|
||||
public LocalBlobDescriptor ParameterLogBlob
|
||||
{
|
||||
get { return _parameterLogBlob; }
|
||||
}
|
||||
|
||||
public IFunctionOutput CreateOutput()
|
||||
{
|
||||
var blob = GetBlockBlobReference(_outputBlob);
|
||||
return UpdateOutputLogCommand.Create(blob);
|
||||
}
|
||||
|
||||
public IRecurrentCommand CreateParameterLogUpdateCommand(IReadOnlyDictionary<string, IWatcher> watches, ILogger logger)
|
||||
{
|
||||
return new UpdateParameterLogCommand(watches, GetBlockBlobReference(_parameterLogBlob), logger);
|
||||
}
|
||||
|
||||
private CloudBlockBlob GetBlockBlobReference(LocalBlobDescriptor descriptor)
|
||||
{
|
||||
var container = _client.GetContainerReference(descriptor.ContainerName);
|
||||
return container.GetBlockBlobReference(descriptor.BlobName);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Extensions.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
|
||||
namespace WebJobs.Host.Storage.Logging
|
||||
{
|
||||
internal class BlobFunctionOutputLogger : IFunctionOutputLogger
|
||||
{
|
||||
private readonly CloudBlobDirectory _outputLogDirectory;
|
||||
|
||||
public BlobFunctionOutputLogger(CloudBlobClient client)
|
||||
: this(client.GetContainerReference(HostContainerNames.Hosts).GetDirectoryReference(HostDirectoryNames.OutputLogs))
|
||||
{
|
||||
}
|
||||
|
||||
private BlobFunctionOutputLogger(CloudBlobDirectory outputLogDirectory)
|
||||
{
|
||||
_outputLogDirectory = outputLogDirectory;
|
||||
}
|
||||
|
||||
public async Task<IFunctionOutputDefinition> CreateAsync(IFunctionInstance instance, CancellationToken cancellationToken)
|
||||
{
|
||||
await _outputLogDirectory.Container.CreateIfNotExistsAsync(cancellationToken);
|
||||
|
||||
string namePrefix = instance.Id.ToString("N");
|
||||
LocalBlobDescriptor outputBlob = CreateDescriptor(_outputLogDirectory, namePrefix + ".txt");
|
||||
LocalBlobDescriptor parameterLogBlob = CreateDescriptor(_outputLogDirectory, namePrefix + ".params.txt");
|
||||
|
||||
return new BlobFunctionOutputDefinition(_outputLogDirectory.ServiceClient, outputBlob, parameterLogBlob);
|
||||
}
|
||||
|
||||
private static LocalBlobDescriptor CreateDescriptor(CloudBlobDirectory directory, string name)
|
||||
{
|
||||
var blob = directory.SafeGetBlockBlobReference(name);
|
||||
|
||||
return new LocalBlobDescriptor
|
||||
{
|
||||
ContainerName = blob.Container.Name,
|
||||
BlobName = blob.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Host;
|
||||
using Microsoft.Azure.WebJobs.Host.Dispatch;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Indexers;
|
||||
using Microsoft.Azure.WebJobs.Host.Listeners;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
using Microsoft.Azure.WebJobs.Host.Queues.Listeners;
|
||||
using Microsoft.Azure.WebJobs.Host.Timers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace WebJobs.Host.Storage.Logging
|
||||
{
|
||||
// $$$
|
||||
// Wires up V1 dashboard logging
|
||||
internal class DashboardLoggingSetup : IDashboardLoggingSetup
|
||||
{
|
||||
private readonly IWebJobsExceptionHandler _exceptionHandler;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IFunctionInstanceLogger _functionInstanceLogger;
|
||||
private readonly IFunctionExecutor _functionExecutor;
|
||||
private readonly SharedQueueHandler _sharedQueueHandler;
|
||||
private readonly ILoadBalancerQueue _queueFactory;
|
||||
private readonly StorageAccountOptions _storageAccountOptions;
|
||||
|
||||
public DashboardLoggingSetup(
|
||||
StorageAccountOptions storageAccountOptions,
|
||||
IWebJobsExceptionHandler exceptionHandler,
|
||||
ILoggerFactory loggerFactory,
|
||||
IFunctionInstanceLogger functionInstanceLogger,
|
||||
IFunctionExecutor functionExecutor,
|
||||
SharedQueueHandler sharedQueueHandler,
|
||||
ILoadBalancerQueue queueFactory
|
||||
)
|
||||
{
|
||||
_storageAccountOptions = storageAccountOptions;
|
||||
_exceptionHandler = exceptionHandler;
|
||||
_loggerFactory = loggerFactory;
|
||||
_functionInstanceLogger = functionInstanceLogger;
|
||||
_functionExecutor = functionExecutor;
|
||||
_sharedQueueHandler = sharedQueueHandler;
|
||||
_queueFactory = queueFactory;
|
||||
}
|
||||
|
||||
public bool Setup(
|
||||
IFunctionIndex functions,
|
||||
IListenerFactory functionsListenerFactory,
|
||||
out IFunctionExecutor hostCallExecutor,
|
||||
out IListener listener,
|
||||
out HostOutputMessage hostOutputMessage,
|
||||
string hostId,
|
||||
CancellationToken shutdownToken)
|
||||
{
|
||||
string sharedQueueName = HostQueueNames.GetHostQueueName(hostId);
|
||||
var sharedQueue = sharedQueueName;
|
||||
|
||||
IListenerFactory sharedQueueListenerFactory = new HostMessageListenerFactory(_queueFactory, sharedQueue,
|
||||
_exceptionHandler, _loggerFactory, functions,
|
||||
_functionInstanceLogger, _functionExecutor);
|
||||
|
||||
Guid hostInstanceId = Guid.NewGuid();
|
||||
string instanceQueueName = HostQueueNames.GetHostQueueName(hostInstanceId.ToString("N"));
|
||||
var instanceQueue = instanceQueueName;
|
||||
IListenerFactory instanceQueueListenerFactory = new HostMessageListenerFactory(_queueFactory, instanceQueue,
|
||||
_exceptionHandler, _loggerFactory, functions,
|
||||
_functionInstanceLogger, _functionExecutor);
|
||||
|
||||
HeartbeatDescriptor heartbeatDescriptor = new HeartbeatDescriptor
|
||||
{
|
||||
SharedContainerName = HostContainerNames.Hosts,
|
||||
SharedDirectoryName = HostDirectoryNames.Heartbeats + "/" + hostId,
|
||||
InstanceBlobName = hostInstanceId.ToString("N"),
|
||||
ExpirationInSeconds = (int)HeartbeatIntervals.ExpirationInterval.TotalSeconds
|
||||
};
|
||||
|
||||
var dashboardAccount = _storageAccountOptions.GetDashboardStorageAccount();
|
||||
DelegatingHandler delegatingHandler = _storageAccountOptions.DelegatingHandlerProvider?.Create();
|
||||
|
||||
var blob = new CloudBlobClient(dashboardAccount.BlobStorageUri, dashboardAccount.Credentials, delegatingHandler)
|
||||
.GetContainerReference(heartbeatDescriptor.SharedContainerName)
|
||||
.GetBlockBlobReference(heartbeatDescriptor.SharedDirectoryName + "/" + heartbeatDescriptor.InstanceBlobName);
|
||||
IRecurrentCommand heartbeatCommand = new UpdateHostHeartbeatCommand(new HeartbeatCommand(blob));
|
||||
|
||||
IEnumerable<MethodInfo> indexedMethods = functions.ReadAllMethods();
|
||||
Assembly hostAssembly = JobHostContextFactory.GetHostAssembly(indexedMethods);
|
||||
string displayName = hostAssembly != null ? AssemblyNameCache.GetName(hostAssembly).Name : "Unknown";
|
||||
|
||||
hostOutputMessage = new JobHostContextFactory.DataOnlyHostOutputMessage
|
||||
{
|
||||
HostInstanceId = hostInstanceId,
|
||||
HostDisplayName = displayName,
|
||||
SharedQueueName = sharedQueueName,
|
||||
InstanceQueueName = instanceQueueName,
|
||||
Heartbeat = heartbeatDescriptor,
|
||||
WebJobRunIdentifier = WebJobRunIdentifier.Current
|
||||
};
|
||||
|
||||
hostCallExecutor = JobHostContextFactory.CreateHostCallExecutor(instanceQueueListenerFactory, heartbeatCommand,
|
||||
_exceptionHandler, shutdownToken, _functionExecutor);
|
||||
IListenerFactory hostListenerFactory = new CompositeListenerFactory(functionsListenerFactory,
|
||||
sharedQueueListenerFactory, instanceQueueListenerFactory);
|
||||
listener = JobHostContextFactory.CreateHostListener(hostListenerFactory, _sharedQueueHandler, heartbeatCommand, _exceptionHandler, shutdownToken);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace WebJobs.Host.Storage.Logging
|
||||
{
|
||||
internal class DefaultLoggerProvider : IHostInstanceLoggerProvider, IFunctionInstanceLoggerProvider, IFunctionOutputLoggerProvider
|
||||
{
|
||||
private bool _loggersSet;
|
||||
private IHostInstanceLogger _hostInstanceLogger;
|
||||
private IFunctionInstanceLogger _functionInstanceLogger;
|
||||
private IFunctionOutputLogger _functionOutputLogger;
|
||||
private ILoggerFactory _loggerFactory;
|
||||
private StorageAccountOptions _storageAccountOptions;
|
||||
|
||||
public DefaultLoggerProvider(StorageAccountOptions storageAccountOptions, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_storageAccountOptions = storageAccountOptions;
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
async Task<IHostInstanceLogger> IHostInstanceLoggerProvider.GetAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureLoggersAsync(cancellationToken);
|
||||
return _hostInstanceLogger;
|
||||
}
|
||||
|
||||
async Task<IFunctionInstanceLogger> IFunctionInstanceLoggerProvider.GetAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureLoggersAsync(cancellationToken);
|
||||
return _functionInstanceLogger;
|
||||
}
|
||||
|
||||
async Task<IFunctionOutputLogger> IFunctionOutputLoggerProvider.GetAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureLoggersAsync(cancellationToken);
|
||||
return _functionOutputLogger;
|
||||
}
|
||||
|
||||
private void EnsureLoggers()
|
||||
{
|
||||
if (_loggersSet)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IFunctionInstanceLogger functionLogger = new FunctionInstanceLogger(_loggerFactory);
|
||||
|
||||
if (_storageAccountOptions.Dashboard != null)
|
||||
{
|
||||
var dashboardAccount = _storageAccountOptions.GetDashboardStorageAccount();
|
||||
DelegatingHandler delegatingHandler = _storageAccountOptions.DelegatingHandlerProvider?.Create();
|
||||
|
||||
// Create logging against a live Azure account.
|
||||
var dashboardBlobClient = new CloudBlobClient(dashboardAccount.BlobStorageUri, dashboardAccount.Credentials, delegatingHandler);
|
||||
IPersistentQueueWriter<PersistentQueueMessage> queueWriter = new PersistentQueueWriter<PersistentQueueMessage>(dashboardBlobClient);
|
||||
PersistentQueueLogger queueLogger = new PersistentQueueLogger(queueWriter);
|
||||
_hostInstanceLogger = queueLogger;
|
||||
_functionInstanceLogger = new CompositeFunctionInstanceLogger(queueLogger, functionLogger);
|
||||
_functionOutputLogger = new BlobFunctionOutputLogger(dashboardBlobClient);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No auxillary logging. Logging interfaces are nops or in-memory.
|
||||
_hostInstanceLogger = new NullHostInstanceLogger();
|
||||
_functionInstanceLogger = functionLogger;
|
||||
_functionOutputLogger = new NullFunctionOutputLogger();
|
||||
}
|
||||
|
||||
_loggersSet = true;
|
||||
}
|
||||
private Task EnsureLoggersAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(() => EnsureLoggers(), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace WebJobs.Host.Storage.Logging
|
||||
{
|
||||
internal sealed class LoggerProviderFactory
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly bool _hasFastTableHook;
|
||||
private readonly Lazy<object> _loggerProvider;
|
||||
private readonly StorageAccountOptions _storageAccountOptions;
|
||||
|
||||
public LoggerProviderFactory(
|
||||
IOptions<StorageAccountOptions> storageAccountOptions,
|
||||
ILoggerFactory loggerFactory,
|
||||
IEventCollectorFactory fastLoggerFactory = null)
|
||||
{
|
||||
_storageAccountOptions = storageAccountOptions.Value;
|
||||
_loggerFactory = loggerFactory;
|
||||
_hasFastTableHook = fastLoggerFactory != null;
|
||||
|
||||
_loggerProvider = new Lazy<object>(CreateLoggerProvider);
|
||||
}
|
||||
|
||||
private object CreateLoggerProvider()
|
||||
{
|
||||
// $$$ if this is null, we should have registered different DI components
|
||||
bool noDashboardStorage = _storageAccountOptions.Dashboard == null;
|
||||
|
||||
if (_hasFastTableHook && noDashboardStorage)
|
||||
{
|
||||
return new FastTableLoggerProvider(_loggerFactory);
|
||||
}
|
||||
|
||||
return new DefaultLoggerProvider(_storageAccountOptions, _loggerFactory);
|
||||
}
|
||||
|
||||
public T GetLoggerProvider<T>() where T : class => _loggerProvider.Value as T;
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
|
||||
namespace WebJobs.Host.Storage.Logging
|
||||
{
|
||||
internal class PersistentQueueLogger : IHostInstanceLogger, IFunctionInstanceLogger
|
||||
{
|
||||
private readonly IPersistentQueueWriter<PersistentQueueMessage> _queueWriter;
|
||||
|
||||
public PersistentQueueLogger(IPersistentQueueWriter<PersistentQueueMessage> queueWriter)
|
||||
{
|
||||
if (queueWriter == null)
|
||||
{
|
||||
throw new ArgumentNullException("queueWriter");
|
||||
}
|
||||
|
||||
_queueWriter = queueWriter;
|
||||
}
|
||||
|
||||
public Task LogHostStartedAsync(HostStartedMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
return _queueWriter.EnqueueAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string> LogFunctionStartedAsync(FunctionStartedMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
return _queueWriter.EnqueueAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
public Task LogFunctionCompletedAsync(FunctionCompletedMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
throw new ArgumentNullException("message");
|
||||
}
|
||||
|
||||
return _queueWriter.EnqueueAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
public Task DeleteLogFunctionStartedAsync(string startedMessageId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (String.IsNullOrEmpty(startedMessageId))
|
||||
{
|
||||
throw new ArgumentNullException("startedMessageId");
|
||||
}
|
||||
|
||||
return _queueWriter.DeleteAsync(startedMessageId, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
using Microsoft.Azure.WebJobs.Host.Timers;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.Executors
|
||||
{
|
||||
internal class UpdateHostHeartbeatCommand : IRecurrentCommand
|
||||
{
|
||||
private readonly IHeartbeatCommand _heartbeatCommand;
|
||||
|
||||
public UpdateHostHeartbeatCommand(IHeartbeatCommand heartbeatCommand)
|
||||
{
|
||||
if (heartbeatCommand == null)
|
||||
{
|
||||
throw new ArgumentNullException("heartbeatCommand");
|
||||
}
|
||||
|
||||
_heartbeatCommand = heartbeatCommand;
|
||||
}
|
||||
|
||||
public async Task<bool> TryExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _heartbeatCommand.BeatAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
catch (StorageException exception)
|
||||
{
|
||||
if (exception.IsServerSideError())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,219 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Azure.WebJobs.Host.Timers;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
|
||||
namespace WebJobs.Host.Storage.Logging
|
||||
{
|
||||
// Flush on a timer so that we get updated output.
|
||||
// Flush will come on a different thread, so we need to have thread-safe
|
||||
// access between the Reader (ToString) and the Writers (which are happening as our
|
||||
// caller uses the textWriter that we return).
|
||||
internal sealed class UpdateOutputLogCommand : IRecurrentCommand, IDisposable, IFunctionOutput
|
||||
{
|
||||
// Contents for what's written. Owned by the timer thread.
|
||||
private readonly StringWriter _innerWriter;
|
||||
private readonly CloudBlockBlob _outputBlob;
|
||||
private readonly Func<string, CancellationToken, Task> _uploadCommand;
|
||||
private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
|
||||
|
||||
// Thread-safe access to _innerWriter so that user threads can write to it.
|
||||
private readonly TextWriter _synchronizedWriter;
|
||||
private object _writerSyncLock = new object();
|
||||
private bool _disposed;
|
||||
private string _existingContent = null;
|
||||
|
||||
private UpdateOutputLogCommand(CloudBlockBlob outputBlob, Func<string, CancellationToken, Task> uploadCommand)
|
||||
{
|
||||
_outputBlob = outputBlob;
|
||||
_innerWriter = new StringWriter(CultureInfo.InvariantCulture);
|
||||
_synchronizedWriter = TextWriter.Synchronized(_innerWriter);
|
||||
_uploadCommand = uploadCommand;
|
||||
}
|
||||
|
||||
public IRecurrentCommand UpdateCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public TextWriter Output
|
||||
{
|
||||
get
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _synchronizedWriter;
|
||||
}
|
||||
}
|
||||
|
||||
public static UpdateOutputLogCommand Create(CloudBlockBlob outputBlob)
|
||||
{
|
||||
return Create(outputBlob, (contents, innerToken) => UploadTextAsync(outputBlob, contents, innerToken));
|
||||
}
|
||||
|
||||
public static UpdateOutputLogCommand Create(CloudBlockBlob outputBlob, Func<string, CancellationToken, Task> uploadCommand)
|
||||
{
|
||||
if (outputBlob == null)
|
||||
{
|
||||
throw new ArgumentNullException("outputBlob");
|
||||
}
|
||||
else if (uploadCommand == null)
|
||||
{
|
||||
throw new ArgumentNullException("uploadCommand");
|
||||
}
|
||||
|
||||
return new UpdateOutputLogCommand(outputBlob, uploadCommand);
|
||||
}
|
||||
|
||||
public async Task<bool> TryExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await UpdateOutputBlob(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_innerWriter.Dispose();
|
||||
_synchronizedWriter.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveAndCloseAsync(FunctionInstanceLogEntry item, CancellationToken cancellationToken)
|
||||
{
|
||||
await UpdateOutputBlob(cancellationToken, flushAndClose: true);
|
||||
}
|
||||
|
||||
private async Task<string> GetExistingContent(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_existingContent == null)
|
||||
{
|
||||
_existingContent = await ReadBlobAsync(_outputBlob, cancellationToken);
|
||||
if (_existingContent != null)
|
||||
{
|
||||
// This can happen if the function was running previously and the
|
||||
// node crashed. Save previous output, could be useful for diagnostics.
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
stringWriter.WriteLine("Previous execution information:");
|
||||
stringWriter.WriteLine(_existingContent);
|
||||
|
||||
var lastTime = await GetBlobModifiedUtcTimeAsync(_outputBlob, cancellationToken);
|
||||
if (lastTime.HasValue)
|
||||
{
|
||||
var delta = DateTime.UtcNow - lastTime.Value;
|
||||
stringWriter.WriteLine("... Last write at {0}, {1} ago", lastTime, delta);
|
||||
}
|
||||
|
||||
stringWriter.WriteLine("========================");
|
||||
}
|
||||
else
|
||||
{
|
||||
_existingContent = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
return _existingContent;
|
||||
}
|
||||
|
||||
private async Task UpdateOutputBlob(CancellationToken cancellationToken, bool flushAndClose = false)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
string snapshot;
|
||||
lock (_writerSyncLock)
|
||||
{
|
||||
if (flushAndClose)
|
||||
{
|
||||
_synchronizedWriter.Flush();
|
||||
}
|
||||
|
||||
// Explicitly specify the length. Without this, any writes occurring
|
||||
// during the call to ToString() could throw.
|
||||
StringBuilder innerBuilder = _innerWriter.GetStringBuilder();
|
||||
snapshot = innerBuilder.ToString(0, innerBuilder.Length);
|
||||
|
||||
if (flushAndClose)
|
||||
{
|
||||
_synchronizedWriter.Close();
|
||||
_innerWriter.Close();
|
||||
}
|
||||
}
|
||||
|
||||
// when we write the output blob, ensure that we always include
|
||||
// any preexisting contents
|
||||
string existingText = await GetExistingContent(cancellationToken);
|
||||
StringBuilder sb = new StringBuilder(existingText);
|
||||
sb.Append(snapshot);
|
||||
snapshot = sb.ToString();
|
||||
|
||||
// Make sure we can only do one of these at a time to prevent Md5Mismatch errors
|
||||
await _semaphoreSlim.WaitAsync();
|
||||
try
|
||||
{
|
||||
await _uploadCommand.Invoke(snapshot, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static Task UploadTextAsync(CloudBlockBlob outputBlob, string contents, CancellationToken cancellationToken)
|
||||
{
|
||||
return outputBlob.UploadTextAsync(contents, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<DateTime?> GetBlobModifiedUtcTimeAsync(ICloudBlob blob, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await blob.ExistsAsync())
|
||||
{
|
||||
return null; // no blob, no time.
|
||||
}
|
||||
|
||||
var lastModified = blob.Properties.LastModified;
|
||||
return lastModified.HasValue ? (DateTime?)lastModified.Value.UtcDateTime : null;
|
||||
}
|
||||
|
||||
[DebuggerNonUserCode]
|
||||
private static async Task<string> ReadBlobAsync(ICloudBlob blob, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Beware! Blob.DownloadText does not strip the BOM!
|
||||
using (var stream = await blob.OpenReadAsync(cancellationToken))
|
||||
using (StreamReader sr = new StreamReader(stream, detectEncodingFromByteOrderMarks: true))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
string data = await sr.ReadToEndAsync();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Host.Bindings;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
using Microsoft.Azure.WebJobs.Host.Timers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace WebJobs.Host.Storage.Logging
|
||||
{
|
||||
internal sealed class UpdateParameterLogCommand : IRecurrentCommand
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, IWatcher> _watches;
|
||||
private readonly CloudBlockBlob _parameterLogBlob;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private string _lastContent;
|
||||
|
||||
public UpdateParameterLogCommand(IReadOnlyDictionary<string, IWatcher> watches, CloudBlockBlob parameterLogBlob, ILogger logger)
|
||||
{
|
||||
if (parameterLogBlob == null)
|
||||
{
|
||||
throw new ArgumentNullException("parameterLogBlob");
|
||||
}
|
||||
else if (watches == null)
|
||||
{
|
||||
throw new ArgumentNullException("watches");
|
||||
}
|
||||
|
||||
_parameterLogBlob = parameterLogBlob;
|
||||
_logger = logger;
|
||||
_watches = watches;
|
||||
}
|
||||
|
||||
public static void AddLogs(IReadOnlyDictionary<string, IWatcher> watches,
|
||||
IDictionary<string, ParameterLog> collector)
|
||||
{
|
||||
foreach (KeyValuePair<string, IWatcher> item in watches)
|
||||
{
|
||||
IWatcher watch = item.Value;
|
||||
|
||||
if (watch == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ParameterLog status = watch.GetStatus();
|
||||
|
||||
if (status == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
collector.Add(item.Key, status);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> TryExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Dictionary<string, ParameterLog> logs = new Dictionary<string, ParameterLog>();
|
||||
AddLogs(_watches, logs);
|
||||
string content = JsonConvert.SerializeObject(logs, JsonSerialization.Settings);
|
||||
|
||||
try
|
||||
{
|
||||
if (_lastContent == content)
|
||||
{
|
||||
// If it hasn't change, then don't re upload stale content.
|
||||
return true;
|
||||
}
|
||||
|
||||
_lastContent = content;
|
||||
await _parameterLogBlob.UploadTextAsync(content, cancellationToken: cancellationToken);
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Not fatal if we can't update parameter status.
|
||||
// But at least log what happened for diagnostics in case it's an infrastructure bug.
|
||||
string msg = "---- Parameter status update failed ----";
|
||||
_logger?.LogWarning(0, e, msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +1,12 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
|
||||
namespace Microsoft.Extensions.Hosting
|
||||
{
|
||||
public static class RuntimeStorageWebJobsBuilderExtensions
|
||||
{
|
||||
// WebJobs v1 Classic logging. Needed for dashboard.
|
||||
[Obsolete("Dashboard is being deprecated. Use AppInsights.")]
|
||||
public static IWebJobsBuilder AddDashboardLogging(this IWebJobsBuilder builder)
|
||||
{
|
||||
builder.Services.AddDashboardLogging();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
// Make the Runtime itself use storage for its internal operations.
|
||||
// Uses v1 app settings, via a LegacyConfigSetup object.
|
||||
public static IWebJobsBuilder AddAzureStorageCoreServices(this IWebJobsBuilder builder)
|
||||
|
|
|
@ -5,37 +5,38 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Extensions.Storage;
|
||||
using Azure;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Azure.Storage.Blobs.Specialized;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Azure.WebJobs.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a CloudBlob lease-based implementation of the <see cref="IDistributedLockManager"/> service for singleton locking.
|
||||
/// This class can be overridden to control where the container comes from.
|
||||
/// The default derived implementation is <see cref="CloudBlobContainerDistributedLockManager"/> which is container based.
|
||||
/// Hosts can provide a derived implementation to leverage the accountName and allow different hosts to share.
|
||||
/// Provides a BlobClient lease-based implementation of the <see cref="IDistributedLockManager"/> service for singleton locking.
|
||||
/// </summary>
|
||||
public abstract class StorageBaseDistributedLockManager : IDistributedLockManager
|
||||
internal class BlobLeaseDistributedLockManager : IDistributedLockManager
|
||||
{
|
||||
internal const string FunctionInstanceMetadataKey = "FunctionInstance";
|
||||
|
||||
// Convention for container name to use.
|
||||
public const string DefaultContainerName = HostContainerNames.Hosts;
|
||||
|
||||
private readonly ConcurrentDictionary<string, CloudBlobDirectory> _lockDirectoryMap = new ConcurrentDictionary<string, CloudBlobDirectory>(StringComparer.OrdinalIgnoreCase);
|
||||
internal const string SingletonLocks = "locks";
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IAzureBlobStorageProvider _blobStorageProvider;
|
||||
private readonly ConcurrentDictionary<string, BlobContainerClient> _lockBlobContainerClientMap = new ConcurrentDictionary<string, BlobContainerClient>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public StorageBaseDistributedLockManager(
|
||||
ILoggerFactory loggerFactory) // Take an ILoggerFactory since that's a DI component.
|
||||
public BlobLeaseDistributedLockManager(
|
||||
ILoggerFactory loggerFactory,
|
||||
IAzureBlobStorageProvider azureStorageProvider) // Take an ILoggerFactory since that's a DI component.
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger(LogCategories.Singleton);
|
||||
_blobStorageProvider = azureStorageProvider;
|
||||
}
|
||||
|
||||
public Task<bool> RenewAsync(IDistributedLock lockHandle, CancellationToken cancellationToken)
|
||||
|
@ -47,27 +48,26 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
public async Task ReleaseLockAsync(IDistributedLock lockHandle, CancellationToken cancellationToken)
|
||||
{
|
||||
SingletonLockHandle singletonLockHandle = (SingletonLockHandle)lockHandle;
|
||||
await ReleaseLeaseAsync(singletonLockHandle.Blob, singletonLockHandle.LeaseId, cancellationToken);
|
||||
await ReleaseLeaseAsync(singletonLockHandle.BlobLeaseClient, singletonLockHandle.LeaseId, cancellationToken);
|
||||
}
|
||||
|
||||
public async virtual Task<string> GetLockOwnerAsync(string account, string lockId, CancellationToken cancellationToken)
|
||||
{
|
||||
var lockDirectory = GetLockDirectory(account);
|
||||
var lockBlob = lockDirectory.SafeGetBlockBlobReference(lockId);
|
||||
var lockBlob = this.GetContainerClient(account).GetBlobClient(GetLockPath(lockId));
|
||||
|
||||
await ReadLeaseBlobMetadata(lockBlob, cancellationToken);
|
||||
var blobProperties = await ReadLeaseBlobMetadata(lockBlob, cancellationToken);
|
||||
|
||||
// if the lease is Available, then there is no current owner
|
||||
// (any existing owner value is the last owner that held the lease)
|
||||
if (lockBlob.Properties.LeaseState == LeaseState.Available &&
|
||||
lockBlob.Properties.LeaseStatus == LeaseStatus.Unlocked)
|
||||
if (blobProperties != null &&
|
||||
blobProperties.LeaseState == LeaseState.Available &&
|
||||
blobProperties.LeaseStatus == LeaseStatus.Unlocked)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string owner = string.Empty;
|
||||
lockBlob.Metadata.TryGetValue(FunctionInstanceMetadataKey, out owner);
|
||||
|
||||
string owner = default;
|
||||
blobProperties?.Metadata.TryGetValue(FunctionInstanceMetadataKey, out owner);
|
||||
return owner;
|
||||
}
|
||||
|
||||
|
@ -79,8 +79,7 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
TimeSpan lockPeriod,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var lockDirectory = GetLockDirectory(account);
|
||||
var lockBlob = lockDirectory.SafeGetBlockBlobReference(lockId);
|
||||
var lockBlob = this.GetContainerClient(account).GetBlobClient(GetLockPath(lockId));
|
||||
string leaseId = await TryAcquireLeaseAsync(lockBlob, lockPeriod, proposedLeaseId, cancellationToken);
|
||||
|
||||
if (string.IsNullOrEmpty(leaseId))
|
||||
|
@ -93,34 +92,74 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
await WriteLeaseBlobMetadata(lockBlob, leaseId, lockOwnerId, cancellationToken);
|
||||
}
|
||||
|
||||
SingletonLockHandle lockHandle = new SingletonLockHandle(leaseId, lockId, lockBlob, lockPeriod);
|
||||
SingletonLockHandle lockHandle = new SingletonLockHandle(leaseId, lockId, this.GetBlobLeaseClient(lockBlob, leaseId), lockPeriod);
|
||||
|
||||
return lockHandle;
|
||||
}
|
||||
|
||||
protected abstract CloudBlobContainer GetContainer(string accountName);
|
||||
|
||||
internal CloudBlobDirectory GetLockDirectory(string accountName)
|
||||
protected virtual BlobContainerClient GetContainerClient(string connectionName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(accountName))
|
||||
if (string.IsNullOrEmpty(connectionName))
|
||||
{
|
||||
accountName = string.Empty; // must be non-null for a dictionary key
|
||||
// Dictionary lookup needs non-null string
|
||||
connectionName = string.Empty;
|
||||
}
|
||||
|
||||
CloudBlobDirectory storageDirectory = null;
|
||||
if (!_lockDirectoryMap.TryGetValue(accountName, out storageDirectory))
|
||||
// First check the cache if we have a BlobContainerClient for this connection
|
||||
if (_lockBlobContainerClientMap.TryGetValue(connectionName, out BlobContainerClient blobContainerClient))
|
||||
{
|
||||
var container = this.GetContainer(accountName);
|
||||
|
||||
storageDirectory = container.GetDirectoryReference(HostDirectoryNames.SingletonLocks);
|
||||
_lockDirectoryMap[accountName] = storageDirectory;
|
||||
return blobContainerClient;
|
||||
}
|
||||
|
||||
return storageDirectory;
|
||||
BlobContainerClient containerClient = CreateBlobContainerClient(connectionName);
|
||||
_lockBlobContainerClientMap[connectionName] = containerClient;
|
||||
return containerClient;
|
||||
}
|
||||
|
||||
private static async Task<string> TryAcquireLeaseAsync(
|
||||
CloudBlockBlob blob,
|
||||
// Helper method to retrieve a new BlobContainerClient with ability to override the default storage account
|
||||
private BlobContainerClient CreateBlobContainerClient(string connectionName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(connectionName))
|
||||
{
|
||||
if (_blobStorageProvider.TryCreateBlobServiceClientFromConnection(connectionName, out BlobServiceClient client))
|
||||
{
|
||||
return client.GetBlobContainerClient(HostContainerNames.Hosts);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Could not create BlobContainerClient for {typeof(BlobLeaseDistributedLockManager).Name} with Connection: {connectionName}.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_blobStorageProvider.TryCreateHostingBlobContainerClient(out BlobContainerClient blobContainerClient))
|
||||
{
|
||||
throw new InvalidOperationException($"Could not create BlobContainerClient for {typeof(BlobLeaseDistributedLockManager).Name}.");
|
||||
}
|
||||
|
||||
return blobContainerClient;
|
||||
}
|
||||
}
|
||||
|
||||
internal string GetLockPath(string lockId)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}/{1}", SingletonLocks, lockId);
|
||||
}
|
||||
|
||||
// Allows the extension method to be mocked for testing
|
||||
protected virtual BlobLeaseClient GetBlobLeaseClient(BlobClient blobClient, string proposedLeaseId)
|
||||
{
|
||||
return blobClient.GetBlobLeaseClient(proposedLeaseId);
|
||||
}
|
||||
|
||||
// Allows the extension method to be mocked for testing
|
||||
protected virtual BlobContainerClient GetParentBlobContainerClient(BlobClient blobClient)
|
||||
{
|
||||
return blobClient.GetParentBlobContainerClient();
|
||||
}
|
||||
|
||||
private async Task<string> TryAcquireLeaseAsync(
|
||||
BlobClient blobClient,
|
||||
TimeSpan leasePeriod,
|
||||
string proposedLeaseId,
|
||||
CancellationToken cancellationToken)
|
||||
|
@ -130,24 +169,18 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
{
|
||||
// Optimistically try to acquire the lease. The blob may not yet
|
||||
// exist. If it doesn't we handle the 404, create it, and retry below
|
||||
return await blob.AcquireLeaseAsync(leasePeriod, proposedLeaseId, accessCondition: null, options: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
var leaseResponse = await GetBlobLeaseClient(blobClient, proposedLeaseId).AcquireAsync(leasePeriod, cancellationToken: cancellationToken);
|
||||
return leaseResponse.Value.LeaseId;
|
||||
}
|
||||
catch (StorageException exception)
|
||||
catch (RequestFailedException exception)
|
||||
{
|
||||
if (exception.RequestInformation != null)
|
||||
if (exception.Status == 409)
|
||||
{
|
||||
if (exception.RequestInformation.HttpStatusCode == 409)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if (exception.RequestInformation.HttpStatusCode == 404)
|
||||
{
|
||||
blobDoesNotExist = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
else if (exception.Status == 404)
|
||||
{
|
||||
blobDoesNotExist = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -157,16 +190,16 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
|
||||
if (blobDoesNotExist)
|
||||
{
|
||||
await TryCreateAsync(blob, cancellationToken);
|
||||
await TryCreateAsync(blobClient, cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
return await blob.AcquireLeaseAsync(leasePeriod, null, accessCondition: null, options: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
var leaseResponse = await GetBlobLeaseClient(blobClient, proposedLeaseId).AcquireAsync(leasePeriod, cancellationToken: cancellationToken);
|
||||
return leaseResponse.Value.LeaseId;
|
||||
}
|
||||
catch (StorageException exception)
|
||||
catch (RequestFailedException exception)
|
||||
{
|
||||
if (exception.RequestInformation != null &&
|
||||
exception.RequestInformation.HttpStatusCode == 409)
|
||||
if (exception.Status == 409)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
@ -180,33 +213,22 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
return null;
|
||||
}
|
||||
|
||||
private static async Task ReleaseLeaseAsync(CloudBlockBlob blob, string leaseId, CancellationToken cancellationToken)
|
||||
private static async Task ReleaseLeaseAsync(BlobLeaseClient blobLeaseClient, string leaseId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Note that this call returns without throwing if the lease is expired. See the table at:
|
||||
// http://msdn.microsoft.com/en-us/library/azure/ee691972.aspx
|
||||
await blob.ReleaseLeaseAsync(
|
||||
accessCondition: new AccessCondition { LeaseId = leaseId },
|
||||
options: null,
|
||||
operationContext: null,
|
||||
cancellationToken: cancellationToken);
|
||||
await blobLeaseClient.ReleaseAsync(cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (StorageException exception)
|
||||
catch (RequestFailedException exception)
|
||||
{
|
||||
if (exception.RequestInformation != null)
|
||||
if (exception.Status == 404 ||
|
||||
exception.Status == 409)
|
||||
{
|
||||
if (exception.RequestInformation.HttpStatusCode == 404 ||
|
||||
exception.RequestInformation.HttpStatusCode == 409)
|
||||
{
|
||||
// if the blob no longer exists, or there is another lease
|
||||
// now active, there is nothing for us to release so we can
|
||||
// ignore
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
// if the blob no longer exists, or there is another lease
|
||||
// now active, there is nothing for us to release so we can
|
||||
// ignore
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -215,33 +237,29 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> TryCreateAsync(CloudBlockBlob blob, CancellationToken cancellationToken)
|
||||
private async Task<bool> TryCreateAsync(BlobClient blobClient, CancellationToken cancellationToken)
|
||||
{
|
||||
bool isContainerNotFoundException = false;
|
||||
|
||||
try
|
||||
{
|
||||
await blob.UploadTextAsync(string.Empty, cancellationToken: cancellationToken);
|
||||
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty)))
|
||||
{
|
||||
await blobClient.UploadAsync(stream, cancellationToken: cancellationToken);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (StorageException exception)
|
||||
catch (RequestFailedException exception)
|
||||
{
|
||||
if (exception.RequestInformation != null)
|
||||
if (exception.Status == 404)
|
||||
{
|
||||
if (exception.RequestInformation.HttpStatusCode == 404)
|
||||
{
|
||||
isContainerNotFoundException = true;
|
||||
}
|
||||
else if (exception.RequestInformation.HttpStatusCode == 409 ||
|
||||
exception.RequestInformation.HttpStatusCode == 412)
|
||||
{
|
||||
// The blob already exists, or is leased by someone else
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
isContainerNotFoundException = true;
|
||||
}
|
||||
else if (exception.Status == 409 ||
|
||||
exception.Status == 412)
|
||||
{
|
||||
// The blob already exists, or is leased by someone else
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -251,26 +269,29 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
|
||||
Debug.Assert(isContainerNotFoundException);
|
||||
|
||||
var container = blob.Container;
|
||||
var container = GetParentBlobContainerClient(blobClient);
|
||||
try
|
||||
{
|
||||
await container.CreateIfNotExistsAsync(cancellationToken);
|
||||
await container.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (StorageException exc)
|
||||
when (exc.RequestInformation.HttpStatusCode == 409 && string.Compare("ContainerBeingDeleted", exc.RequestInformation.ExtendedErrorInformation?.ErrorCode) == 0)
|
||||
catch (RequestFailedException exception)
|
||||
when (exception.Status == 409 && string.Compare("ContainerBeingDeleted", exception.ErrorCode) == 0)
|
||||
{
|
||||
throw new StorageException("The host container is pending deletion and currently inaccessible.");
|
||||
throw new RequestFailedException("The host container is pending deletion and currently inaccessible.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await blob.UploadTextAsync(string.Empty, cancellationToken: cancellationToken);
|
||||
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty)))
|
||||
{
|
||||
await blobClient.UploadAsync(stream, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (StorageException exception)
|
||||
catch (RequestFailedException exception)
|
||||
{
|
||||
if (exception.RequestInformation != null &&
|
||||
(exception.RequestInformation.HttpStatusCode == 409 || exception.RequestInformation.HttpStatusCode == 412))
|
||||
if (exception.Status == 409 || exception.Status == 412)
|
||||
{
|
||||
// The blob already exists, or is leased by someone else
|
||||
return false;
|
||||
|
@ -282,29 +303,32 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
}
|
||||
}
|
||||
|
||||
private static async Task WriteLeaseBlobMetadata(CloudBlockBlob blob, string leaseId, string functionInstanceId, CancellationToken cancellationToken)
|
||||
private static async Task WriteLeaseBlobMetadata(BlobClient blobClient, string leaseId, string functionInstanceId, CancellationToken cancellationToken)
|
||||
{
|
||||
blob.Metadata.Add(FunctionInstanceMetadataKey, functionInstanceId);
|
||||
|
||||
await blob.SetMetadataAsync(
|
||||
accessCondition: new AccessCondition { LeaseId = leaseId },
|
||||
options: null,
|
||||
operationContext: null,
|
||||
cancellationToken: cancellationToken);
|
||||
var blobProperties = await ReadLeaseBlobMetadata(blobClient, cancellationToken);
|
||||
if (blobProperties != null)
|
||||
{
|
||||
blobProperties.Metadata[FunctionInstanceMetadataKey] = functionInstanceId;
|
||||
await blobClient.SetMetadataAsync(
|
||||
blobProperties.Metadata,
|
||||
new BlobRequestConditions { LeaseId = leaseId },
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReadLeaseBlobMetadata(CloudBlockBlob blob, CancellationToken cancellationToken)
|
||||
private static async Task<BlobProperties> ReadLeaseBlobMetadata(BlobClient blobClient, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await blob.FetchAttributesAsync(accessCondition: null, options: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
var propertiesResponse = await blobClient.GetPropertiesAsync(cancellationToken: cancellationToken);
|
||||
return propertiesResponse.Value;
|
||||
}
|
||||
catch (StorageException exception)
|
||||
catch (RequestFailedException exception)
|
||||
{
|
||||
if (exception.RequestInformation != null &&
|
||||
exception.RequestInformation.HttpStatusCode == 404)
|
||||
if (exception.Status == 404)
|
||||
{
|
||||
// the blob no longer exists
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -324,37 +348,36 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
{
|
||||
}
|
||||
|
||||
public SingletonLockHandle(string leaseId, string lockId, CloudBlockBlob blob, TimeSpan leasePeriod)
|
||||
public SingletonLockHandle(string leaseId, string lockId, BlobLeaseClient blobLeaseClient, TimeSpan leasePeriod)
|
||||
{
|
||||
this.LeaseId = leaseId;
|
||||
this.LockId = lockId;
|
||||
this._leasePeriod = leasePeriod;
|
||||
this.Blob = blob;
|
||||
this.BlobLeaseClient = blobLeaseClient;
|
||||
}
|
||||
|
||||
public string LeaseId { get; internal set; }
|
||||
|
||||
public string LockId { get; internal set; }
|
||||
public CloudBlockBlob Blob { get; internal set; }
|
||||
|
||||
public BlobLeaseClient BlobLeaseClient { get; internal set; }
|
||||
|
||||
public async Task<bool> RenewAsync(ILogger logger, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
AccessCondition condition = new AccessCondition
|
||||
{
|
||||
LeaseId = this.LeaseId
|
||||
};
|
||||
DateTimeOffset requestStart = DateTimeOffset.UtcNow;
|
||||
await this.Blob.RenewLeaseAsync(condition, null, null, cancellationToken);
|
||||
await this.BlobLeaseClient.RenewAsync(cancellationToken: cancellationToken);
|
||||
_lastRenewal = DateTime.UtcNow;
|
||||
_lastRenewalLatency = _lastRenewal - requestStart;
|
||||
|
||||
// The next execution should occur after a normal delay.
|
||||
return true;
|
||||
}
|
||||
catch (StorageException exception)
|
||||
catch (RequestFailedException exception)
|
||||
{
|
||||
if (exception.IsServerSideError())
|
||||
// indicates server-side error
|
||||
if (exception.Status >= 500 && exception.Status < 600)
|
||||
{
|
||||
string msg = string.Format(CultureInfo.InvariantCulture, "Singleton lock renewal failed for blob '{0}' with error code {1}.",
|
||||
this.LockId, FormatErrorCode(exception));
|
||||
|
@ -380,17 +403,11 @@ namespace Microsoft.Azure.WebJobs.Host
|
|||
}
|
||||
}
|
||||
|
||||
private static string FormatErrorCode(StorageException exception)
|
||||
private static string FormatErrorCode(RequestFailedException exception)
|
||||
{
|
||||
int statusCode;
|
||||
if (!exception.TryGetStatusCode(out statusCode))
|
||||
{
|
||||
return "''";
|
||||
}
|
||||
string message = exception.Status.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
string message = statusCode.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
string errorCode = exception.GetErrorCode();
|
||||
string errorCode = exception.ErrorCode;
|
||||
|
||||
if (errorCode != null)
|
||||
{
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host
|
||||
{
|
||||
/// <summary>
|
||||
/// A Singleton manager service that uses a blob lease within the given container.
|
||||
/// Caller decides whether the container could come from a SAS connection string or a full storage account.
|
||||
/// Hosts can set the <see cref="IDistributedLockManager"/> service to completely replace this.
|
||||
/// </summary>
|
||||
public class CloudBlobContainerDistributedLockManager : StorageBaseDistributedLockManager
|
||||
{
|
||||
private readonly CloudBlobContainer _container;
|
||||
|
||||
public CloudBlobContainerDistributedLockManager(
|
||||
CloudBlobContainer container,
|
||||
ILoggerFactory logger) : base(logger)
|
||||
{
|
||||
_container = container;
|
||||
}
|
||||
|
||||
protected override CloudBlobContainer GetContainer(string accountName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(accountName))
|
||||
{
|
||||
throw new InvalidOperationException("Must replace singleton lease manager to support multiple accounts");
|
||||
}
|
||||
return _container;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs
|
||||
{
|
||||
|
||||
// $$$ Validate these? And what are their capabilities?
|
||||
internal class StorageAccountOptionsSetup : IConfigureOptions<StorageAccountOptions>
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IDelegatingHandlerProvider _delegatingHandlerProvider;
|
||||
|
||||
public StorageAccountOptionsSetup(IConfiguration configuration, IDelegatingHandlerProvider delegatingHandlerProvider)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_delegatingHandlerProvider = delegatingHandlerProvider;
|
||||
}
|
||||
|
||||
public void Configure(StorageAccountOptions options)
|
||||
{
|
||||
if (options.Dashboard == null)
|
||||
{
|
||||
options.Dashboard = _configuration.GetWebJobsConnectionString(ConnectionStringNames.Dashboard);
|
||||
}
|
||||
if (options.Storage == null)
|
||||
{
|
||||
options.Storage = _configuration.GetWebJobsConnectionString(ConnectionStringNames.Storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class StorageAccountOptions
|
||||
{
|
||||
// Property names here must match existing names.
|
||||
public string Dashboard { get; set; }
|
||||
public string Storage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The DelegatingHandlerProvider to be used when creating storage clients.
|
||||
/// </summary>
|
||||
public IDelegatingHandlerProvider DelegatingHandlerProvider { get; set; }
|
||||
|
||||
public CloudStorageAccount GetDashboardStorageAccount()
|
||||
{
|
||||
CloudStorageAccount account;
|
||||
CloudStorageAccount.TryParse(this.Dashboard, out account);
|
||||
return account;
|
||||
}
|
||||
|
||||
public CloudStorageAccount GetStorageAccount()
|
||||
{
|
||||
CloudStorageAccount account;
|
||||
CloudStorageAccount.TryParse(this.Storage, out account);
|
||||
return account;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Azure.Core;
|
||||
using Microsoft.Extensions.Azure;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// Passthrough-class to create Azure storage service clients using using <see cref="AzureComponentFactory"/>.
|
||||
/// If the connection is not specified, it uses a default account.
|
||||
/// </summary>
|
||||
internal abstract class StorageClientProvider<TClient, TClientOptions> where TClientOptions : ClientOptions
|
||||
{
|
||||
private readonly AzureComponentFactory _componentFactory;
|
||||
private readonly AzureEventSourceLogForwarder _logForwarder;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StorageClientProvider{TClient, TClientOptions}"/> class that uses the registered Azure services.
|
||||
/// </summary>
|
||||
/// <param name="componentFactory">The Azure factory responsible for creating clients. <see cref="AzureComponentFactory"/>.</param>
|
||||
/// <param name="logForwarder">Log forwarder that forwards events to ILogger. <see cref="AzureEventSourceLogForwarder"/>.</param>
|
||||
public StorageClientProvider(AzureComponentFactory componentFactory, AzureEventSourceLogForwarder logForwarder)
|
||||
{
|
||||
_componentFactory = componentFactory;
|
||||
_logForwarder = logForwarder;
|
||||
|
||||
_logForwarder.Start();
|
||||
}
|
||||
|
||||
public virtual TClient Create(string name, INameResolver resolver, IConfiguration configuration)
|
||||
{
|
||||
var resolvedName = resolver.ResolveWholeString(name);
|
||||
return this.Create(resolvedName, configuration);
|
||||
}
|
||||
|
||||
public virtual TClient Create(string name, IConfiguration configuration)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
name = ConnectionStringNames.Storage; // default
|
||||
}
|
||||
|
||||
IConfigurationSection connectionSection = configuration?.GetWebJobsConnectionSection(name);
|
||||
if (connectionSection == null || !connectionSection.Exists())
|
||||
{
|
||||
// Not found
|
||||
throw new InvalidOperationException($"Storage account connection string '{IConfigurationExtensions.GetPrefixedConnectionStringName(name)}' does not exist. Make sure that it is a defined App Setting.");
|
||||
}
|
||||
|
||||
var credential = _componentFactory.CreateTokenCredential(connectionSection);
|
||||
var options = CreateClientOptions(connectionSection);
|
||||
return CreateClient(connectionSection, credential, options);
|
||||
}
|
||||
|
||||
protected virtual TClient CreateClient(IConfiguration configuration, TokenCredential tokenCredential, TClientOptions options)
|
||||
{
|
||||
return (TClient)_componentFactory.CreateClient(typeof(TClient), configuration, tokenCredential, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the specified <see cref="IConfiguration"/> object has a value. This is assumed to be a connection string.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The <see cref="IConfiguration"/> to retrieve the value from.</param>
|
||||
/// <returns>true if this <see cref="IConfiguration"/> object is a connection string; false otherwise.</returns>
|
||||
protected bool IsConnectionStringPresent(IConfiguration configuration)
|
||||
{
|
||||
return configuration is IConfigurationSection section && section.Value != null;
|
||||
}
|
||||
|
||||
private TClientOptions CreateClientOptions(IConfiguration configuration)
|
||||
{
|
||||
var clientOptions = (TClientOptions)_componentFactory.CreateClientOptions(typeof(TClientOptions), null, configuration);
|
||||
return clientOptions;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,89 +2,60 @@
|
|||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Azure.Storage.Blobs;
|
||||
using Microsoft.Azure.WebJobs;
|
||||
using Microsoft.Azure.WebJobs.Host;
|
||||
using Microsoft.Azure.WebJobs.Host.Config;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Azure.WebJobs.Host.Scale;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Extensions.Azure;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using WebJobs.Host.Storage.Logging;
|
||||
|
||||
namespace Microsoft.Extensions.Hosting
|
||||
{
|
||||
public static class StorageServiceCollectionExtensions
|
||||
{
|
||||
[Obsolete("Dashboard is being deprecated. Use AppInsights.")]
|
||||
public static IServiceCollection AddDashboardLogging(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<LoggerProviderFactory>();
|
||||
|
||||
services.TryAddSingleton<IFunctionOutputLoggerProvider>(p => p.GetRequiredService<LoggerProviderFactory>().GetLoggerProvider<IFunctionOutputLoggerProvider>());
|
||||
services.TryAddSingleton<IFunctionOutputLogger>(p => p.GetRequiredService<IFunctionOutputLoggerProvider>().GetAsync(CancellationToken.None).GetAwaiter().GetResult());
|
||||
|
||||
services.TryAddSingleton<IFunctionInstanceLoggerProvider>(p => p.GetRequiredService<LoggerProviderFactory>().GetLoggerProvider<IFunctionInstanceLoggerProvider>());
|
||||
services.TryAddSingleton<IFunctionInstanceLogger>(p => p.GetRequiredService<IFunctionInstanceLoggerProvider>().GetAsync(CancellationToken.None).GetAwaiter().GetResult());
|
||||
|
||||
services.TryAddSingleton<IHostInstanceLoggerProvider>(p => p.GetRequiredService<LoggerProviderFactory>().GetLoggerProvider<IHostInstanceLoggerProvider>());
|
||||
services.TryAddSingleton<IHostInstanceLogger>(p => p.GetRequiredService<IHostInstanceLoggerProvider>().GetAsync(CancellationToken.None).GetAwaiter().GetResult());
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static void AddAzureStorageCoreServices(this IServiceCollection services)
|
||||
{
|
||||
// Replace existing runtime services with storage-backed implementations.
|
||||
// Add runtime services that depend on storage.
|
||||
services.AddSingleton<IDistributedLockManager>(provider => Create(provider));
|
||||
|
||||
// Used specifically for the CloudBlobContainerDistributedLockManager implementation
|
||||
services.TryAddSingleton<DistributedLockManagerContainerProvider>();
|
||||
|
||||
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<StorageAccountOptions>, StorageAccountOptionsSetup>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<JobHostInternalStorageOptions>, CoreWebJobsOptionsSetup<JobHostInternalStorageOptions>>());
|
||||
|
||||
services.TryAddSingleton<IDelegatingHandlerProvider, DefaultDelegatingHandlerProvider>();
|
||||
|
||||
// Adds necessary Azure services to create clients
|
||||
services.AddAzureClientsCore();
|
||||
|
||||
services.TryAddSingleton<IAzureBlobStorageProvider, AzureStorageProvider>();
|
||||
|
||||
services.AddSingleton<IConcurrencyStatusRepository, BlobStorageConcurrencyStatusRepository>();
|
||||
}
|
||||
|
||||
// This is only called if the host didn't already provide an implementation
|
||||
private static IDistributedLockManager Create(IServiceProvider provider)
|
||||
{
|
||||
// $$$ get rid of LegacyConfig
|
||||
var opts = provider.GetRequiredService<IOptions<StorageAccountOptions>>();
|
||||
|
||||
var blobStorageProvider = provider.GetRequiredService<IAzureBlobStorageProvider>();
|
||||
var loggerFactory = provider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger<IDistributedLockManager>();
|
||||
|
||||
var sas = provider.GetService<DistributedLockManagerContainerProvider>();
|
||||
|
||||
CloudBlobContainer container;
|
||||
|
||||
if (sas != null && sas.InternalContainer != null)
|
||||
IDistributedLockManager lockManager;
|
||||
if (blobStorageProvider.TryCreateHostingBlobContainerClient(out _))
|
||||
{
|
||||
container = sas.InternalContainer;
|
||||
lockManager = new BlobLeaseDistributedLockManager(loggerFactory, blobStorageProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
var config = opts.Value;
|
||||
CloudStorageAccount account = config.GetStorageAccount();
|
||||
if (account == null)
|
||||
{
|
||||
return new InMemoryDistributedLockManager();
|
||||
}
|
||||
|
||||
var blobClient = new CloudBlobClient(account.BlobStorageUri, account.Credentials, config.DelegatingHandlerProvider?.Create());
|
||||
container = blobClient.GetContainerReference(HostContainerNames.Hosts);
|
||||
// If there is an error getting the container client,
|
||||
// register an InMemoryDistributedLockManager.
|
||||
lockManager = new InMemoryDistributedLockManager();
|
||||
}
|
||||
|
||||
var lockManager = new CloudBlobContainerDistributedLockManager(container, loggerFactory);
|
||||
logger.LogDebug($"Using {lockManager.GetType().Name}");
|
||||
return lockManager;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,25 +2,12 @@
|
|||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Microsoft.Azure.WebJobs.Host;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Extensions.Storage
|
||||
{
|
||||
static class Utility
|
||||
{
|
||||
// CloudBlobDirectory has a private ctor, so we can't actually override it.
|
||||
// This overload is unit-testable
|
||||
internal static CloudBlockBlob SafeGetBlockBlobReference(this CloudBlobDirectory dir, string blobName)
|
||||
{
|
||||
var container = dir.Container;
|
||||
var prefix = dir.Prefix; // already ends in /
|
||||
var blob = container.GetBlockBlobReference(prefix + blobName);
|
||||
return blob;
|
||||
}
|
||||
|
||||
internal static int GetProcessorCount()
|
||||
{
|
||||
int processorCount = 1;
|
||||
|
|
|
@ -24,15 +24,10 @@
|
|||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarningsAsErrors />
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Microsoft.Azure.WebJobs.Protocols\HeartbeatCommand.cs" Link="Logging\HeartbeatCommand.cs" />
|
||||
<Compile Include="..\Microsoft.Azure.WebJobs.Protocols\PersistentQueueReader.cs" Link="Logging\PersistentQueueReader.cs" />
|
||||
<Compile Include="..\Microsoft.Azure.WebJobs.Protocols\PersistentQueueWriter.cs" Link="Logging\PersistentQueueWriter.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Storage.Blob" Version="11.1.7" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.9.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.1.1" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
|
|
@ -93,5 +93,43 @@ namespace Microsoft.Extensions.Configuration
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks for an <see cref="IConfigurationSection"/> corresponding to the connectionName parameter.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The <see cref="IConfiguration"/> instance to use.</param>
|
||||
/// <param name="connectionName">Name of the section to search for.</param>
|
||||
/// <returns></returns>
|
||||
public static IConfigurationSection GetWebJobsConnectionSection(this IConfiguration configuration, string connectionName)
|
||||
{
|
||||
// first try prefixing
|
||||
string prefixedConnectionStringName = GetPrefixedConnectionStringName(connectionName);
|
||||
IConfigurationSection section = GetConnectionStringOrSettingSection(configuration, prefixedConnectionStringName);
|
||||
|
||||
if (!section.Exists())
|
||||
{
|
||||
// next try a direct unprefixed lookup
|
||||
section = GetConnectionStringOrSettingSection(configuration, connectionName);
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks for a connection string by first checking the ConnectionStrings section, and then the root.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
/// <param name="connectionName">The connection string key.</param>
|
||||
/// <returns>Configuration section corresponding to the connectionName</returns>
|
||||
private static IConfigurationSection GetConnectionStringOrSettingSection(this IConfiguration configuration, string connectionName)
|
||||
{
|
||||
var connectionStringSection = configuration?.GetSection("ConnectionStrings").GetSection(connectionName);
|
||||
|
||||
if (connectionStringSection.Exists())
|
||||
{
|
||||
return connectionStringSection;
|
||||
}
|
||||
return configuration?.GetSection(connectionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,73 +1,38 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Microsoft.Azure.Storage.Shared.Protocol;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Specialized;
|
||||
|
||||
internal static class BlobStorageExtensions
|
||||
{
|
||||
public static Task SetServicePropertiesAsync(this CloudBlobClient sdk, ServiceProperties properties, CancellationToken cancellationToken)
|
||||
public static async Task UploadTextAsync(this BlobClient blobClient, string content, bool overwrite = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return sdk.SetServicePropertiesAsync(properties, requestOptions: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content)))
|
||||
{
|
||||
await blobClient.UploadAsync(stream, overwrite: overwrite, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<ServiceProperties> GetServicePropertiesAsync(this CloudBlobClient sdk, CancellationToken cancellationToken)
|
||||
public static async Task UploadTextAsync(this BlockBlobClient blockBlobClient, string content, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return sdk.GetServicePropertiesAsync(cancellationToken);
|
||||
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content)))
|
||||
{
|
||||
await blockBlobClient.UploadAsync(stream, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<CloudBlobStream> OpenWriteAsync(this CloudBlockBlob sdk, CancellationToken cancellationToken)
|
||||
public static async Task<string> DownloadTextAsync(this BlobClient blobClient, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return sdk.OpenWriteAsync(accessCondition: null, options: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public static Task<string> DownloadTextAsync(this CloudBlockBlob sdk, CancellationToken cancellationToken)
|
||||
{
|
||||
return sdk.DownloadTextAsync(encoding: null, accessCondition: null, options: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public static Task UploadTextAsync(this CloudBlockBlob sdk, string content, Encoding encoding = null, AccessCondition accessCondition = null,
|
||||
BlobRequestOptions options = null, OperationContext operationContext = null,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return sdk.UploadTextAsync(content, encoding, accessCondition, options, operationContext,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public static Task DeleteAsync(this CloudBlockBlob sdk, CancellationToken cancellationToken)
|
||||
{
|
||||
return sdk.DeleteAsync(DeleteSnapshotsOption.None, accessCondition: null, options: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public static Task CreateIfNotExistsAsync(this CloudBlobContainer sdk, CancellationToken cancellationToken)
|
||||
{
|
||||
return sdk.CreateIfNotExistsAsync(BlobContainerPublicAccessType.Off, options: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public static Task<Stream> OpenReadAsync(this ICloudBlob sdk, CancellationToken cancellationToken)
|
||||
{
|
||||
return sdk.OpenReadAsync(accessCondition: null, options: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public static Task<string> AcquireLeaseAsync(this CloudBlockBlob sdk, TimeSpan? leaseTime, string proposedLeaseId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return sdk.AcquireLeaseAsync(leaseTime, proposedLeaseId, accessCondition: null, options: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public static Task FetchAttributesAsync(this ICloudBlob sdk, CancellationToken cancellationToken)
|
||||
{
|
||||
return sdk.FetchAttributesAsync(accessCondition: null, options: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public static Task<ICloudBlob> GetBlobReferenceFromServerAsync(this CloudBlobContainer sdk, string blobName, CancellationToken cancellationToken)
|
||||
{
|
||||
return sdk.GetBlobReferenceFromServerAsync(blobName, accessCondition: null, options: null, operationContext: null, cancellationToken: cancellationToken);
|
||||
var downloadResponse = await blobClient.DownloadAsync(cancellationToken: cancellationToken);
|
||||
using (StreamReader reader = new StreamReader(downloadResponse.Value.Content, true))
|
||||
{
|
||||
string content = reader.ReadToEnd();
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@
|
|||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)BlobStorageExtensions.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)StorageExceptionsExtensions.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)CommonUtility.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)WebJobsStorageDelegatingHandler.cs" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -1,818 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Storage;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs
|
||||
{
|
||||
/// <summary>Provides extension methods for the <see cref="StorageException"/> class.</summary>
|
||||
internal static class StorageExceptionExtensions
|
||||
{
|
||||
public static bool IsServerSideError(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int statusCode = result.HttpStatusCode;
|
||||
return statusCode >= 500 && statusCode < 600;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 400 Bad Request error with the error code PopReceiptMismatch.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 400 Bad Request error with the error code
|
||||
/// PopReceiptMismatch; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsBadRequestPopReceiptMismatch(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 400)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "PopReceiptMismatch";
|
||||
}
|
||||
|
||||
/// <summary>Determines whether the exception is due to a 409 Conflict error.</summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 409 Conflict error; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsConflict(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return result.HttpStatusCode == 409;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 409 Conflict error with the error code BlobAlreadyExists.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 409 Conflict error with the error code
|
||||
/// BlobAlreadyExists; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsConflictBlobAlreadyExists(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 409)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "BlobAlreadyExists";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 409 Conflict error with the error code LeaseAlreadyPresent.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 409 Conflict error with the error code
|
||||
/// LeaseAlreadyPresent; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsConflictLeaseAlreadyPresent(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 409)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "LeaseAlreadyPresent";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 409 Conflict error with the error code
|
||||
/// LeaseIdMismatchWithLeaseOperation.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 409 Conflict error with the error code
|
||||
/// LeaseIdMismatchWithLeaseOperation; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsConflictLeaseIdMismatchWithLeaseOperation(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 409)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "LeaseIdMismatchWithLeaseOperation";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 409 Conflict error with the error code QueueBeingDeleted.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 409 Conflict error with the error code
|
||||
/// QueueBeingDeleted; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsConflictQueueBeingDeleted(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 409)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "QueueBeingDeleted";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 409 Conflict error with the error code QueueBeingDeleted or
|
||||
/// QueueDisabled.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 409 Conflict error with the error code QueueBeingDeleted
|
||||
/// or QueueDisabled; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsConflictQueueBeingDeletedOrDisabled(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 409)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "QueueBeingDeleted";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 409 Conflict error with the error code QueueDisabled.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 409 Conflict error with the error code QueueDisabled;
|
||||
/// otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsConflictQueueDisabled(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 409)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "QueueDisabled";
|
||||
}
|
||||
|
||||
/// <summary>Determines whether the exception is due to a 404 Not Found error.</summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 404 Not Found error; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsNotFound(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return result.HttpStatusCode == 404;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 404 Not Found error with the error code BlobNotFound.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 404 Not Found error with the error code BlobNotFound;
|
||||
/// otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsNotFoundBlobNotFound(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 404)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "BlobNotFound";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 404 Not Found error with the error code ContainerNotFound.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 404 Not Found error with the error code
|
||||
/// ContainerNotFound; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsNotFoundContainerNotFound(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 404)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "ContainerNotFound";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 404 Not Found error with the error code BlobNotFound or
|
||||
/// ContainerNotFound.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 404 Not Found error with the error code BlobNotFound or
|
||||
/// ContainerNotFound; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsNotFoundBlobOrContainerNotFound(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 404)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string errorCode = extendedInformation.ErrorCode;
|
||||
return errorCode == "BlobNotFound" || errorCode == "ContainerNotFound";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 404 Not Found error with the error code MessageNotFound or
|
||||
/// QueueNotFound.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 404 Not Found error with the error code MessageNotFound
|
||||
/// or QueueNotFound; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsNotFoundMessageOrQueueNotFound(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 404)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string errorCode = extendedInformation.ErrorCode;
|
||||
return errorCode == "MessageNotFound" || errorCode == "QueueNotFound";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 404 Not Found error with the error code MessageNotFound.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 404 Not Found error with the error code MessageNotFound;
|
||||
/// otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsNotFoundMessageNotFound(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 404)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "MessageNotFound";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 404 Not Found error with the error code QueueNotFound.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 404 Not Found error with the error code QueueNotFound;
|
||||
/// otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsNotFoundQueueNotFound(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 404)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "QueueNotFound";
|
||||
}
|
||||
|
||||
/// <summary>Determines whether the exception occurred despite a 200 OK response.</summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception occurred despite a 200 OK response; otherwise
|
||||
/// <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsOk(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return result.HttpStatusCode == 200;
|
||||
}
|
||||
|
||||
/// <summary>Determines whether the exception is due to a 412 Precondition Failed error.</summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 412 Precondition Failed error; otherwise
|
||||
/// <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsPreconditionFailed(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return result.HttpStatusCode == 412;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 412 Precondition Failed error with the error code
|
||||
/// ConditionNotMet.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 412 Precondition Failed error with the error code
|
||||
/// ConditionNotMet; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsPreconditionFailedConditionNotMet(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 412)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "ConditionNotMet";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 412 Precondition Failed error with the error code
|
||||
/// LeaseIdMissing.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 412 Precondition Failed error with the error code
|
||||
/// LeaseIdMissing; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsPreconditionFailedLeaseIdMissing(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 412)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "LeaseIdMissing";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a 412 Precondition Failed error with the error code LeaseLost.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the exception is due to a 412 Precondition Failed error with the error code
|
||||
/// LeaseLost; otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool IsPreconditionFailedLeaseLost(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.HttpStatusCode != 412)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode == "LeaseLost";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is due to a task cancellation.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns><see langword="true"/> if the inner exception is a <see cref="TaskCanceledException"/>. Otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsTaskCanceled(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
return exception.InnerException is TaskCanceledException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the status code from the storage exception, or null.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <param name="statusCode">When this method returns, contains the status code.</param>
|
||||
/// <returns>Returns true if there was a status code; otherwise, false.</returns>
|
||||
public static bool TryGetStatusCode(this StorageException exception, out int statusCode)
|
||||
{
|
||||
statusCode = 0;
|
||||
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
statusCode = result.HttpStatusCode;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the error code from storage exception, or null.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>The error code, or null.</returns>
|
||||
public static string GetErrorCode(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
RequestResult result = exception.RequestInformation;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
StorageExtendedErrorInformation extendedInformation = result.ExtendedErrorInformation;
|
||||
|
||||
if (extendedInformation == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return extendedInformation.ErrorCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a custom detailed error message for a StorageException. This is a workaround for bad error messages
|
||||
/// returned by Azure Storage.
|
||||
/// </summary>
|
||||
/// <param name="exception">The storage exception.</param>
|
||||
/// <returns>The error message.</returns>
|
||||
public static string GetDetailedErrorMessage(this StorageException exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
string message = exception.Message;
|
||||
|
||||
if (exception.RequestInformation != null)
|
||||
{
|
||||
message += $" (HTTP status code {exception.RequestInformation.HttpStatusCode.ToString()}: "
|
||||
+ $"{exception.RequestInformation.ExtendedErrorInformation?.ErrorCode}. "
|
||||
+ $"{exception.RequestInformation.ExtendedErrorInformation?.ErrorMessage})";
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,8 +11,4 @@
|
|||
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Extensions.Storage\WebJobs.Extensions.Storage.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
using System;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.Azure.Storage.Queue;
|
||||
using Microsoft.Azure.WebJobs.Host.Queues;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Benchmarks
|
||||
{
|
||||
[MemoryDiagnoser]
|
||||
public class WebjobsExtensionsStorage
|
||||
{
|
||||
readonly CloudQueueMessage _minimal;
|
||||
readonly CloudQueueMessage _moderate;
|
||||
|
||||
public WebjobsExtensionsStorage()
|
||||
{
|
||||
var expected = Guid.NewGuid();
|
||||
var minimal = new JObject
|
||||
{
|
||||
{"$AzureWebJobsParentId", new JValue(expected.ToString())}
|
||||
};
|
||||
|
||||
_minimal = new CloudQueueMessage(minimal.ToString());
|
||||
|
||||
var moderate = new JObject
|
||||
{
|
||||
{"$AzureWebJobsParentId", new JValue(expected.ToString())},
|
||||
{"BlobName", new JValue("some/path/to/blob")},
|
||||
{"Date", new JValue(DateTime.Now)},
|
||||
{"Number", new JValue(1234324)},
|
||||
};
|
||||
|
||||
_moderate = new CloudQueueMessage(moderate.ToString());
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public Guid? QueueCausalityManager_GetOwner_Minimal()
|
||||
{
|
||||
return QueueCausalityManager.GetOwner(_minimal);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public Guid? QueueCausalityManager_GetOwner_Moderate()
|
||||
{
|
||||
return QueueCausalityManager.GetOwner(_moderate);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1256,12 +1256,12 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
|
||||
public void StopListening()
|
||||
{
|
||||
_applicationInsightsListener.Stop();
|
||||
_tcs?.Cancel(false);
|
||||
if (_listenTask != null && !_listenTask.IsCompleted)
|
||||
{
|
||||
_listenTask.Wait();
|
||||
}
|
||||
_applicationInsightsListener.Stop();
|
||||
|
||||
_tcs?.Dispose();
|
||||
_listenTask = null;
|
||||
|
|
|
@ -7,22 +7,22 @@ using System.Linq;
|
|||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Queues;
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Microsoft.ApplicationInsights.Extensibility;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Azure.WebJobs.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Microsoft.Azure.Storage.Queue;
|
||||
using Xunit;
|
||||
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
||||
{
|
||||
public class HttpDependencyCollectionTests : IDisposable
|
||||
public class HttpDependencyCollectionTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestArtifactPrefix = "e2etestsai";
|
||||
private const string OutputQueueNamePattern = TestArtifactPrefix + "out%rnd%";
|
||||
|
@ -41,10 +41,10 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
private string _outputQueueName;
|
||||
private string _triggerQueueName;
|
||||
|
||||
private CloudBlobClient _blobClient;
|
||||
private RandomNameResolver _resolver;
|
||||
private CloudQueueClient _queueClient;
|
||||
private CloudQueue _triggerQueue;
|
||||
private BlobServiceClient _blobServiceClient;
|
||||
private QueueServiceClient _queueServiceClient;
|
||||
private QueueClient _triggerQueueClient;
|
||||
|
||||
[Fact]
|
||||
public async Task BindingsAreNotReportedWhenFiltered()
|
||||
|
@ -65,6 +65,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
Assert.Empty(_channel.Telemetries.OfType<DependencyTelemetry>().ToList());
|
||||
}
|
||||
|
||||
// TODO: Analyze Track2 changes for differences in this test from Track1
|
||||
[Fact]
|
||||
public async Task BlobBindingsAreReported()
|
||||
{
|
||||
|
@ -81,15 +82,20 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
}
|
||||
|
||||
RequestTelemetry request = _channel.Telemetries.OfType<RequestTelemetry>().Single(r => r.Name == nameof(BlobInputAndOutputBindings));
|
||||
|
||||
var leafDependencies = new List<DependencyTelemetry>();
|
||||
GetLeafDependenciesOfRequest(_channel.Telemetries.OfType<DependencyTelemetry>(), request.Id, leafDependencies);
|
||||
|
||||
List<DependencyTelemetry> dependencies = _channel.Telemetries.OfType<DependencyTelemetry>().ToList();
|
||||
|
||||
List<DependencyTelemetry> inDependencies = dependencies
|
||||
var containerDependencies = dependencies.Where(d => d.Properties.ContainsKey("Container")).ToList();
|
||||
List<DependencyTelemetry> inDependencies = containerDependencies
|
||||
.Where(d => d.Properties.Single(p => p.Key == "Container").Value == _inputContainerName).ToList();
|
||||
List<DependencyTelemetry> outDependencies = dependencies
|
||||
List<DependencyTelemetry> outDependencies = containerDependencies
|
||||
.Where(d => d.Properties.Single(p => p.Key == "Container").Value == _outputContainerName).ToList();
|
||||
|
||||
// only bindings calls are reported
|
||||
Assert.Equal(dependencies.Count, inDependencies.Count + outDependencies.Count);
|
||||
Assert.Equal(containerDependencies.Count, inDependencies.Count + outDependencies.Count);
|
||||
|
||||
foreach (var inputDep in inDependencies)
|
||||
{
|
||||
|
@ -99,7 +105,10 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
"in",
|
||||
nameof(BlobInputAndOutputBindings),
|
||||
request.Context.Operation.Id,
|
||||
request.Id);
|
||||
inputDep.Context.Operation.ParentId); // ParentId won't be the parentId of the RequestTelemetry since it is Dependency Tree
|
||||
|
||||
// Check that the ParentId for inputDep can be traced back to RequestTelemtry
|
||||
Assert.Contains(leafDependencies, d => d.Id == inputDep.Id);
|
||||
}
|
||||
|
||||
foreach (var outputDep in outDependencies)
|
||||
|
@ -110,16 +119,38 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
"out",
|
||||
nameof(BlobInputAndOutputBindings),
|
||||
request.Context.Operation.Id,
|
||||
request.Id);
|
||||
outputDep.Context.Operation.ParentId); // ParentId won't be the parentId of the RequestTelemetry since it is Dependency Tree
|
||||
|
||||
// Check that the ParentId for outputDep can be traced back to RequestTelemtry
|
||||
Assert.Contains(leafDependencies, d => d.Id == outputDep.Id);
|
||||
}
|
||||
|
||||
|
||||
// PUT container, HEAD blob, PUT lease, PUT content
|
||||
Assert.True(outDependencies.Select(d => d.Name).Distinct().Count() <= 4);
|
||||
// since there could be failures and retries, we should expect more
|
||||
Assert.True(outDependencies.Count <= 4);
|
||||
|
||||
// HEAD blob, GET blob
|
||||
Assert.True(inDependencies.Count >= 2);
|
||||
Assert.True(inDependencies.Select(d => d.Name).Distinct().Count() >= 2);
|
||||
|
||||
// NEED TO REEXAMINE WITH TRACK2 EXTENSIONS
|
||||
// since there could be failures and retries, we should expect more
|
||||
//Assert.True(outDependencies.Count <= 4);
|
||||
//Assert.True(inDependencies.Count >= 2);
|
||||
}
|
||||
|
||||
private static void GetLeafDependenciesOfRequest(IEnumerable<DependencyTelemetry> dependencies, string currId, List<DependencyTelemetry> leafDependencies)
|
||||
{
|
||||
var children = dependencies.Where(d => d.Context.Operation.ParentId == currId);
|
||||
if (!children.Any())
|
||||
{
|
||||
leafDependencies.Add(dependencies.Single(d => d.Id == currId));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var dependency in children)
|
||||
{
|
||||
GetLeafDependenciesOfRequest(dependencies, dependency.Id, leafDependencies);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -182,6 +213,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
Assert.Single(_channel.Telemetries.OfType<DependencyTelemetry>());
|
||||
}
|
||||
|
||||
// TODO: Analyze Track2 changes for differences in this test from Track1
|
||||
[Fact]
|
||||
public async Task BlobTriggerCallsAreReported()
|
||||
{
|
||||
|
@ -193,11 +225,10 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
await Task.Delay(3000);
|
||||
Assert.Empty(_channel.Telemetries.OfType<DependencyTelemetry>());
|
||||
|
||||
CloudBlobContainer container = _blobClient.GetContainerReference(_triggerContainerName);
|
||||
await container.CreateIfNotExistsAsync();
|
||||
|
||||
CloudBlockBlob blob = container.GetBlockBlobReference("triggerBlob");
|
||||
await blob.UploadTextAsync("TestData");
|
||||
var containerClient = _blobServiceClient.GetBlobContainerClient(_triggerContainerName);
|
||||
await containerClient.CreateIfNotExistsAsync();
|
||||
var blobClient = containerClient.GetBlobClient("triggerBlob");
|
||||
blobClient.UploadTextAsync("TestData", overwrite: true).Wait();
|
||||
|
||||
_functionWaitHandle.WaitOne();
|
||||
// let host run for a while to write output queue message
|
||||
|
@ -207,15 +238,20 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
}
|
||||
|
||||
RequestTelemetry request = _channel.Telemetries.OfType<RequestTelemetry>().Single();
|
||||
|
||||
var leafDependencies = new List<DependencyTelemetry>();
|
||||
GetLeafDependenciesOfRequest(_channel.Telemetries.OfType<DependencyTelemetry>(), request.Id, leafDependencies);
|
||||
|
||||
List<DependencyTelemetry> dependencies = _channel.Telemetries.OfType<DependencyTelemetry>().ToList();
|
||||
|
||||
List<DependencyTelemetry> blobDependencies = dependencies.Where(d => d.Type == "Azure blob").ToList();
|
||||
List<DependencyTelemetry> queueDependencies = dependencies.Where(d => d.Type == "Azure queue").ToList();
|
||||
var azureDependencies = dependencies.Where(d => d.Type.StartsWith("Azure")).ToList();
|
||||
List<DependencyTelemetry> blobDependencies = azureDependencies.Where(d => d.Type == "Azure blob").ToList();
|
||||
List<DependencyTelemetry> queueDependencies = azureDependencies.Where(d => d.Type == "Azure queue").ToList();
|
||||
|
||||
Assert.True(dependencies.Count >= 3);
|
||||
Assert.True(azureDependencies.Count >= 3);
|
||||
|
||||
// only bindings calls are reported
|
||||
Assert.Equal(dependencies.Count, blobDependencies.Count + queueDependencies.Count);
|
||||
Assert.Equal(azureDependencies.Count, blobDependencies.Count + queueDependencies.Count);
|
||||
|
||||
// one HEAD, one GET to download blob
|
||||
Assert.True(blobDependencies.Count >= 2);
|
||||
|
@ -231,7 +267,10 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
"triggerBlob",
|
||||
nameof(BlobTrigger),
|
||||
request.Context.Operation.Id,
|
||||
request.Id);
|
||||
inputDep.Context.Operation.ParentId); // ParentId won't be the parentId of the RequestTelemetry since it is Dependency Tree
|
||||
|
||||
// Check that the ParentId for outputDep can be traced back to RequestTelemtry
|
||||
Assert.Contains(leafDependencies, d => d.Id == inputDep.Id);
|
||||
}
|
||||
|
||||
ValidateQueueDependency(
|
||||
|
@ -239,9 +278,13 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
_outputQueueName,
|
||||
nameof(BlobTrigger),
|
||||
request.Context.Operation.Id,
|
||||
request.Id);
|
||||
queueDependencies.First().Context.Operation.ParentId); // ParentId won't be the parentId of the RequestTelemetry since it is Dependency Tree
|
||||
|
||||
// Check that the ParentId for outputDep can be traced back to RequestTelemtry
|
||||
Assert.Contains(leafDependencies, d => d.Id == queueDependencies.First().Id);
|
||||
}
|
||||
|
||||
// TODO: Analyze Track2 changes for differences in this test from Track1
|
||||
[Fact]
|
||||
public async Task QueueTriggerCallsAreReported()
|
||||
{
|
||||
|
@ -253,7 +296,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
await Task.Delay(3000);
|
||||
Assert.Empty(_channel.Telemetries.OfType<DependencyTelemetry>());
|
||||
|
||||
await _triggerQueue.AddMessageAsync(new CloudQueueMessage("TestData"));
|
||||
await _triggerQueueClient.SendMessageAsync("TestData");
|
||||
|
||||
_functionWaitHandle.WaitOne();
|
||||
// let host run for a while to write output queue message
|
||||
|
@ -263,16 +306,21 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
}
|
||||
|
||||
RequestTelemetry request = _channel.Telemetries.OfType<RequestTelemetry>().Single();
|
||||
|
||||
var leafDependencies = new List<DependencyTelemetry>();
|
||||
GetLeafDependenciesOfRequest(_channel.Telemetries.OfType<DependencyTelemetry>(), request.Id, leafDependencies);
|
||||
|
||||
List<DependencyTelemetry> dependencies = _channel.Telemetries.OfType<DependencyTelemetry>().ToList();
|
||||
|
||||
List<DependencyTelemetry> bindingsDependencies = dependencies
|
||||
var containerDependencies = dependencies.Where(d => d.Properties.ContainsKey("Container")).ToList();
|
||||
List<DependencyTelemetry> bindingsDependencies = containerDependencies
|
||||
.Where(d => d.Properties.Single(p => p.Key == LogConstants.CategoryNameKey).Value == LogCategories.Bindings).ToList();
|
||||
|
||||
// only bindings calls are reported, queue read happens before that and is not reported
|
||||
Assert.True(dependencies.Count >= 4);
|
||||
Assert.True(containerDependencies.Count >= 4);
|
||||
|
||||
// PUT container, HEAD blob, PUT lease, PUT content
|
||||
Assert.Equal(dependencies.Count, bindingsDependencies.Count);
|
||||
Assert.Equal(containerDependencies.Count, bindingsDependencies.Count);
|
||||
|
||||
foreach (var outputDep in bindingsDependencies)
|
||||
{
|
||||
|
@ -282,7 +330,10 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
"out1",
|
||||
nameof(QueueTrigger),
|
||||
request.Context.Operation.Id,
|
||||
request.Id);
|
||||
outputDep.Context.Operation.ParentId); // ParentId won't be the parentId of the RequestTelemetry since it is Dependency Tree
|
||||
|
||||
// Check that the ParentId for outputDep can be traced back to RequestTelemtry
|
||||
Assert.Contains(leafDependencies, d => d.Id == outputDep.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -372,7 +423,10 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<HttpDependencyCollectionTests>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
// Necessary for Blob/Queue bindings
|
||||
b.AddAzureStorageBlobs();
|
||||
// Track2 Queues Extensions expects Base64 encoded message; Track2 Queues package no longer encodes by default (Track1 used Base64 default)
|
||||
b.AddAzureStorageQueues(o => o.MessageEncoding = QueueMessageEncoding.None);
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
|
@ -392,10 +446,10 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
TelemetryConfiguration telemteryConfiguration = host.Services.GetService<TelemetryConfiguration>();
|
||||
telemteryConfiguration.TelemetryChannel = _channel;
|
||||
|
||||
StorageAccountProvider provider = host.Services.GetService<StorageAccountProvider>();
|
||||
CloudStorageAccount storageAccount = provider.GetHost().SdkObject;
|
||||
_blobClient = storageAccount.CreateCloudBlobClient();
|
||||
_queueClient = storageAccount.CreateCloudQueueClient();
|
||||
var configuration = host.Services.GetService<IConfiguration>();
|
||||
|
||||
_blobServiceClient = TestHelpers.GetTestBlobServiceClient();
|
||||
_queueServiceClient = TestHelpers.GetTestQueueServiceClient();
|
||||
|
||||
_inputContainerName = _resolver.ResolveInString(InputContainerNamePattern);
|
||||
_outputContainerName = _resolver.ResolveInString(OutputContainerNamePattern);
|
||||
|
@ -403,27 +457,53 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
_outputQueueName = _resolver.ResolveInString(OutputQueueNamePattern);
|
||||
_triggerQueueName = _resolver.ResolveInString(TriggerQueueNamePattern);
|
||||
|
||||
CloudBlobContainer inContainer = _blobClient.GetContainerReference(_inputContainerName);
|
||||
CloudBlobContainer outContainer = _blobClient.GetContainerReference(_outputContainerName);
|
||||
CloudBlobContainer triggerContainer = _blobClient.GetContainerReference(_triggerContainerName);
|
||||
CloudQueue outputQueue = _queueClient.GetQueueReference(_outputQueueName);
|
||||
_triggerQueue = _queueClient.GetQueueReference(_triggerQueueName);
|
||||
var inContainer = _blobServiceClient.GetBlobContainerClient(_inputContainerName);
|
||||
var outContainer = _blobServiceClient.GetBlobContainerClient(_outputContainerName);
|
||||
var triggerContainer = _blobServiceClient.GetBlobContainerClient(_triggerContainerName);
|
||||
var outputQueue = _queueServiceClient.GetQueueClient(_outputQueueName);
|
||||
_triggerQueueClient = _queueServiceClient.GetQueueClient(_triggerQueueName);
|
||||
|
||||
inContainer.CreateIfNotExistsAsync().Wait();
|
||||
outContainer.CreateIfNotExistsAsync().Wait();
|
||||
triggerContainer.CreateIfNotExistsAsync().Wait();
|
||||
outputQueue.CreateIfNotExistsAsync().Wait();
|
||||
_triggerQueue.CreateIfNotExistsAsync().Wait();
|
||||
inContainer.CreateIfNotExists();
|
||||
outContainer.CreateIfNotExists();
|
||||
triggerContainer.CreateIfNotExists();
|
||||
outputQueue.CreateIfNotExists();
|
||||
_triggerQueueClient.CreateIfNotExists();
|
||||
|
||||
CloudBlockBlob inBlob = inContainer.GetBlockBlobReference("in");
|
||||
inBlob.UploadTextAsync("TestData").Wait();
|
||||
var blobClient = inContainer.GetBlobClient("in");
|
||||
blobClient.UploadTextAsync("TestData", overwrite: true).Wait();
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
_channel?.Dispose();
|
||||
await CleanBlobsAsync();
|
||||
await CleanQueuesAsync();
|
||||
}
|
||||
|
||||
private async Task CleanBlobsAsync()
|
||||
{
|
||||
if (_blobServiceClient != null)
|
||||
{
|
||||
await foreach (var testBlob in _blobServiceClient.GetBlobContainersAsync(prefix: TestArtifactPrefix))
|
||||
{
|
||||
await _blobServiceClient.DeleteBlobContainerAsync(testBlob.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanQueuesAsync()
|
||||
{
|
||||
if (_queueServiceClient != null)
|
||||
{
|
||||
await foreach (var testQueue in _queueServiceClient.GetQueuesAsync(prefix: TestArtifactPrefix))
|
||||
{
|
||||
await _queueServiceClient.DeleteQueueAsync(testQueue.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -407,7 +407,6 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.ApplicationInsights
|
|||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<ServiceBusRequestAndDependencyCollectionTests>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
b.AddServiceBus(o =>
|
||||
{
|
||||
// We'll complete these ourselves as we don't
|
||||
|
|
|
@ -4,18 +4,15 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Azure.Storage.Queues;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Queue;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
||||
{
|
||||
public class AsyncCancellationEndToEndTests : IDisposable
|
||||
public class AsyncCancellationEndToEndTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestArtifactPrefix = "asynccancele2e";
|
||||
private const string QueueName = TestArtifactPrefix + "%rnd%";
|
||||
|
@ -27,7 +24,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
private static EventWaitHandle _functionCompleted;
|
||||
private static bool _tokenCancelled;
|
||||
|
||||
private readonly CloudStorageAccount _storageAccount;
|
||||
private readonly QueueServiceClient _queueServiceClient;
|
||||
private readonly RandomNameResolver _resolver;
|
||||
private readonly IHost _host;
|
||||
|
||||
|
@ -38,7 +35,9 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
_host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<AsyncCancellationEndToEndTests>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
// Necessary for Blob/Queue bindings
|
||||
b.AddAzureStorageBlobs();
|
||||
b.AddAzureStorageQueues();
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
|
@ -46,8 +45,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
})
|
||||
.Build();
|
||||
|
||||
var accountProvider = _host.Services.GetService<StorageAccountProvider>();
|
||||
_storageAccount = accountProvider.GetHost().SdkObject;
|
||||
_queueServiceClient = TestHelpers.GetTestQueueServiceClient();
|
||||
|
||||
_invokeInFunction = () => { };
|
||||
_tokenCancelled = false;
|
||||
|
@ -55,17 +53,23 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
_functionCompleted = new ManualResetEvent(initialState: false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
_functionStarted.Dispose();
|
||||
_functionCompleted.Dispose();
|
||||
|
||||
if (_storageAccount != null)
|
||||
await CleanQueuesAsync();
|
||||
}
|
||||
|
||||
private async Task CleanQueuesAsync()
|
||||
{
|
||||
if (_queueServiceClient != null)
|
||||
{
|
||||
CloudQueueClient queueClient = _storageAccount.CreateCloudQueueClient();
|
||||
foreach (var testQueue in queueClient.ListQueuesSegmentedAsync(TestArtifactPrefix, null).Result.Results)
|
||||
await foreach (var testQueue in _queueServiceClient.GetQueuesAsync(prefix: TestArtifactPrefix))
|
||||
{
|
||||
testQueue.DeleteAsync().Wait();
|
||||
await _queueServiceClient.DeleteQueueAsync(testQueue.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,18 +11,21 @@ using System.Text;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Azure.Storage.Queues;
|
||||
using Azure.Storage.Queues.Models;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Azure.WebJobs.Host.Queues;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Azure.WebJobs.Host.Timers;
|
||||
using Microsoft.Azure.WebJobs.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Microsoft.Azure.Storage.Queue;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
||||
|
@ -43,7 +46,8 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
|
||||
private const string TriggerDetailsMessageStart = "Trigger Details:";
|
||||
|
||||
private static CloudStorageAccount _storageAccount;
|
||||
private static QueueServiceClient _queueServiceClient;
|
||||
private static BlobServiceClient _blobServiceClient;
|
||||
|
||||
private static RandomNameResolver _resolver;
|
||||
private readonly IHostBuilder _hostBuilder;
|
||||
|
@ -52,7 +56,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
private static string _finalBlobContent;
|
||||
private static TimeSpan _timeoutJobDelay;
|
||||
|
||||
private readonly CloudQueue _testQueue;
|
||||
private readonly QueueClient _testQueueClient;
|
||||
private readonly TestFixture _fixture;
|
||||
|
||||
public AsyncChainEndToEndTests(TestFixture fixture)
|
||||
|
@ -60,10 +64,14 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
_fixture = fixture;
|
||||
_resolver = new RandomNameResolver();
|
||||
|
||||
int processorCount = Extensions.Storage.Utility.GetProcessorCount();
|
||||
|
||||
_hostBuilder = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<AsyncChainEndToEndTests>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
// Necessary for Blob/Queue bindings
|
||||
b.AddAzureStorageQueues();
|
||||
b.AddAzureStorageBlobs();
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
|
@ -71,6 +79,8 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
services.Configure<QueuesOptions>(o =>
|
||||
{
|
||||
o.MaxPollingInterval = TimeSpan.FromSeconds(2);
|
||||
o.BatchSize = 16;
|
||||
o.NewBatchThreshold = 8 * processorCount; // workaround for static isDynamicSku and ProcessorCount variables in T2 extensions
|
||||
});
|
||||
services.Configure<FunctionResultAggregatorOptions>(o =>
|
||||
{
|
||||
|
@ -78,17 +88,20 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
});
|
||||
});
|
||||
|
||||
_storageAccount = fixture.StorageAccount;
|
||||
_blobServiceClient = fixture.BlobServiceClient;
|
||||
_queueServiceClient = fixture.QueueServiceClient;
|
||||
|
||||
// This is how long the Timeout test functions will wait for cancellation.
|
||||
_timeoutJobDelay = TimeSpan.FromSeconds(30);
|
||||
|
||||
CloudQueueClient queueClient = _storageAccount.CreateCloudQueueClient();
|
||||
string queueName = _resolver.ResolveInString(TestQueueName);
|
||||
_testQueue = queueClient.GetQueueReference(queueName);
|
||||
if (!_testQueue.CreateIfNotExistsAsync().Result)
|
||||
_testQueueClient = _queueServiceClient.GetQueueClient(queueName);
|
||||
|
||||
var response = _testQueueClient.CreateIfNotExistsAsync().Result;
|
||||
if (response == null)
|
||||
{
|
||||
_testQueue.ClearAsync().Wait();
|
||||
// Queue already exists, need to clear it
|
||||
_testQueueClient.ClearMessagesAsync().Wait();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,6 +118,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
|
||||
string[] loggerOutputLines = loggerProvider.GetAllLogMessages()
|
||||
.Where(p => p.FormattedMessage != null)
|
||||
.Where(p => p.Category != "Azure.Core")
|
||||
.SelectMany(p => p.FormattedMessage.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries))
|
||||
.OrderBy(p => p)
|
||||
.ToArray();
|
||||
|
@ -146,13 +160,14 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
"User TraceWriter log 1",
|
||||
"User TraceWriter log 2",
|
||||
"Starting JobHost",
|
||||
"Stopped the listener 'Microsoft.Azure.WebJobs.Host.Listeners.CompositeListener' for function 'BlobToBlobAsync'",
|
||||
"Stopped the listener 'Microsoft.Azure.WebJobs.Host.Queues.Listeners.QueueListener' for function 'QueueToBlobAsync'",
|
||||
"Stopped the listener 'Microsoft.Azure.WebJobs.Host.Queues.Listeners.QueueListener' for function 'QueueToQueueAsync'",
|
||||
"Stopped the listener 'Microsoft.Azure.WebJobs.Extensions.Storage.Common.Listeners.CompositeListener' for function 'BlobToBlobAsync'",
|
||||
"Stopped the listener 'Microsoft.Azure.WebJobs.Extensions.Storage.Common.Listeners.QueueListener' for function 'QueueToBlobAsync'",
|
||||
"Stopped the listener 'Microsoft.Azure.WebJobs.Extensions.Storage.Common.Listeners.QueueListener' for function 'QueueToQueueAsync'",
|
||||
"Stopping JobHost",
|
||||
"Stopping the listener 'Microsoft.Azure.WebJobs.Host.Listeners.CompositeListener' for function 'BlobToBlobAsync'",
|
||||
"Stopping the listener 'Microsoft.Azure.WebJobs.Host.Queues.Listeners.QueueListener' for function 'QueueToBlobAsync'",
|
||||
"Stopping the listener 'Microsoft.Azure.WebJobs.Host.Queues.Listeners.QueueListener' for function 'QueueToQueueAsync'",
|
||||
"Stopping the listener 'Microsoft.Azure.WebJobs.Extensions.Storage.Common.Listeners.CompositeListener' for function 'BlobToBlobAsync'",
|
||||
"Stopping the listener 'Microsoft.Azure.WebJobs.Extensions.Storage.Common.Listeners.QueueListener' for function 'QueueToBlobAsync'",
|
||||
"Stopping the listener 'Microsoft.Azure.WebJobs.Extensions.Storage.Common.Listeners.QueueListener' for function 'QueueToQueueAsync'",
|
||||
"registered http endpoint", // logged in Microsoft.Azure.WebJobs.Extensions.Blobs.BlobsExtensionConfigProvider
|
||||
"QueuesOptions",
|
||||
"{",
|
||||
" \"BatchSize\": 16",
|
||||
|
@ -160,6 +175,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
" \"MaxPollingInterval\": \"00:00:02\",",
|
||||
string.Format(" \"NewBatchThreshold\": {0},", 8 * processorCount),
|
||||
" \"VisibilityTimeout\": \"00:00:00\"",
|
||||
" \"MessageEncoding\": \"Base64\"",
|
||||
"}",
|
||||
"LoggerFilterOptions",
|
||||
"{",
|
||||
|
@ -181,7 +197,16 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
"}",
|
||||
"BlobsOptions",
|
||||
"{",
|
||||
" \"CentralizedPoisonQueue\": false",
|
||||
string.Format(" \"MaxDegreeOfParallelism\": {0}", 8 * processorCount),
|
||||
"}",
|
||||
"QueuesOptions", // This QueuesOptions are an internal type within Microsoft.Azure.WebJobs.Extensions.Storage.Blobs
|
||||
"{",
|
||||
" \"BatchSize\": 16",
|
||||
string.Format(" \"NewBatchThreshold\": {0},", 8 * processorCount),
|
||||
" \"MaxPollingInterval\": \"00:01:00\",",
|
||||
" \"MaxDequeueCount\": 5,",
|
||||
" \"VisibilityTimeout\": \"00:00:00\"",
|
||||
" \"MessageEncoding\": \"Base64\"",
|
||||
"}",
|
||||
"SingletonOptions",
|
||||
"{",
|
||||
|
@ -206,7 +231,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
string[] triggerDetailsLoggerOutput = loggerOutputLines
|
||||
.Where(m => m.StartsWith(TriggerDetailsMessageStart)).ToArray();
|
||||
|
||||
string expectedPattern = "Trigger Details: MessageId: (.*), DequeueCount: [0-9]+, InsertionTime: (.*)";
|
||||
string expectedPattern = "Trigger Details: MessageId: (.*), DequeueCount: [0-9]+, InsertedOn: (.*)";
|
||||
|
||||
foreach (string msg in triggerDetailsLoggerOutput)
|
||||
{
|
||||
|
@ -220,7 +245,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
{
|
||||
using (_functionCompletedEvent = new ManualResetEvent(initialState: false))
|
||||
{
|
||||
CustomQueueProcessorFactory queueProcessorFactory = new CustomQueueProcessorFactory();
|
||||
TestQueueProcessorFactory queueProcessorFactory = new TestQueueProcessorFactory();
|
||||
|
||||
_hostBuilder.ConfigureServices(services =>
|
||||
{
|
||||
|
@ -229,10 +254,10 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
|
||||
await AsyncChainEndToEndInternal(_hostBuilder);
|
||||
|
||||
Assert.Equal(2, queueProcessorFactory.CustomQueueProcessors.Count);
|
||||
Assert.True(queueProcessorFactory.CustomQueueProcessors.All(p => p.Context.Queue.Name.StartsWith("asynce2eq")));
|
||||
Assert.True(queueProcessorFactory.CustomQueueProcessors.Sum(p => p.BeginProcessingCount) >= 2);
|
||||
Assert.True(queueProcessorFactory.CustomQueueProcessors.Sum(p => p.CompleteProcessingCount) >= 2);
|
||||
Assert.Equal(2, queueProcessorFactory.TestQueueProcessors.Count);
|
||||
Assert.True(queueProcessorFactory.TestQueueProcessors.All(p => p.Context.Queue.Name.StartsWith("asynce2eq")));
|
||||
Assert.True(queueProcessorFactory.TestQueueProcessors.Sum(p => p.BeginProcessingCount) >= 2);
|
||||
Assert.True(queueProcessorFactory.TestQueueProcessors.Sum(p => p.CompleteProcessingCount) >= 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -243,8 +268,8 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
{
|
||||
TestLoggerProvider loggerProvider = await AsyncChainEndToEndInternal(_hostBuilder);
|
||||
|
||||
// Validate Logger
|
||||
bool hasError = loggerProvider.GetAllLogMessages().Where(p => p.FormattedMessage != null && p.FormattedMessage.Contains("Error")).Any();
|
||||
// Validate Logger
|
||||
bool hasError = loggerProvider.GetAllLogMessages().Where(p => p.FormattedMessage != null && p.Category != "Azure.Core" && p.FormattedMessage.Contains("Error")).Any();
|
||||
Assert.False(hasError);
|
||||
|
||||
IEnumerable<string> userLogMessages = loggerProvider.GetAllLogMessages()
|
||||
|
@ -410,13 +435,12 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
IHost host = _hostBuilder.Build();
|
||||
JobHost jobHost = host.GetJobHost();
|
||||
|
||||
var blobClient = _fixture.StorageAccount.CreateCloudBlobClient();
|
||||
var container = blobClient.GetContainerReference("test-output");
|
||||
if (await container.ExistsAsync())
|
||||
var blobContainerClient = _blobServiceClient.GetBlobContainerClient("test-output");
|
||||
if (await blobContainerClient.ExistsAsync())
|
||||
{
|
||||
foreach (CloudBlockBlob blob in (await container.ListBlobsSegmentedAsync(null)).Results)
|
||||
await foreach (var blob in blobContainerClient.GetBlobsAsync())
|
||||
{
|
||||
await blob.DeleteAsync();
|
||||
await blobContainerClient.DeleteBlobIfExistsAsync(blob.Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -428,12 +452,24 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
await jobHost.CallAsync(methodInfo, arguments);
|
||||
|
||||
// We expect 3 separate blobs to have been written
|
||||
var blobs = (await container.ListBlobsSegmentedAsync(null)).Results.Cast<CloudBlockBlob>().ToArray();
|
||||
Assert.Equal(3, blobs.Length);
|
||||
foreach (var blob in blobs)
|
||||
int blobCount = 0;
|
||||
await foreach (var blob in blobContainerClient.GetBlobsAsync())
|
||||
{
|
||||
string content = await blob.DownloadTextAsync();
|
||||
string content = await blobContainerClient.GetBlobClient(blob.Name).DownloadTextAsync();
|
||||
Assert.Equal("Test Value", content.Trim(new char[] { '\uFEFF', '\u200B' }));
|
||||
|
||||
blobCount++;
|
||||
}
|
||||
|
||||
Assert.Equal(3, blobCount);
|
||||
|
||||
// Test Cleanup
|
||||
if (await blobContainerClient.ExistsAsync())
|
||||
{
|
||||
await foreach (var blob in blobContainerClient.GetBlobsAsync())
|
||||
{
|
||||
await blobContainerClient.DeleteBlobIfExistsAsync(blob.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -643,15 +679,10 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
CancellationToken token,
|
||||
TraceWriter trace)
|
||||
{
|
||||
CloudBlobClient blobClient = _storageAccount.CreateCloudBlobClient();
|
||||
CloudBlobContainer container = blobClient.GetContainerReference(_resolver.ResolveInString(ContainerName));
|
||||
CloudBlockBlob blob = container.GetBlockBlobReference(NonWebJobsBlobName);
|
||||
|
||||
string blobContent = await blob.DownloadTextAsync();
|
||||
|
||||
var content = await _blobServiceClient.GetBlobContainerClient(_resolver.ResolveInString(ContainerName)).GetBlobClient(NonWebJobsBlobName).DownloadTextAsync(cancellationToken: token);
|
||||
trace.Info("User TraceWriter log 1");
|
||||
|
||||
await output.AddAsync(blobContent + message);
|
||||
await output.AddAsync(content + message);
|
||||
}
|
||||
|
||||
public static async Task QueueToBlobAsync(
|
||||
|
@ -742,55 +773,44 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
userMessageCallback: () => $"Function did not complete in {waitTimeout} ms. Current time: {DateTime.UtcNow.ToString("HH:mm:ss.fff")}{Environment.NewLine}{loggerProvider.GetLogString()}");
|
||||
}
|
||||
|
||||
private class CustomQueueProcessorFactory : IQueueProcessorFactory
|
||||
private class TestQueueProcessorFactory : IQueueProcessorFactory
|
||||
{
|
||||
public List<CustomQueueProcessor> CustomQueueProcessors = new List<CustomQueueProcessor>();
|
||||
public List<TestQueueProcessor> TestQueueProcessors = new List<TestQueueProcessor>();
|
||||
|
||||
public QueueProcessor Create(QueueProcessorFactoryContext context)
|
||||
public QueueProcessor Create(QueueProcessorOptions context)
|
||||
{
|
||||
// demonstrates how the Queue.ServiceClient options can be configured
|
||||
context.Queue.ServiceClient.DefaultRequestOptions.ServerTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
// demonstrates how queue options can be customized
|
||||
context.Queue.EncodeMessage = true;
|
||||
|
||||
// demonstrates how batch processing behavior and other knobs
|
||||
// can be customized
|
||||
context.BatchSize = 30;
|
||||
context.NewBatchThreshold = 100;
|
||||
context.MaxPollingInterval = TimeSpan.FromSeconds(15);
|
||||
|
||||
CustomQueueProcessor processor = new CustomQueueProcessor(context);
|
||||
CustomQueueProcessors.Add(processor);
|
||||
return processor;
|
||||
var queueProcessor = new TestQueueProcessor(context);
|
||||
TestQueueProcessors.Add(queueProcessor);
|
||||
return queueProcessor;
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomQueueProcessor : QueueProcessor
|
||||
private class TestQueueProcessor : QueueProcessor
|
||||
{
|
||||
public int BeginProcessingCount = 0;
|
||||
public int CompleteProcessingCount = 0;
|
||||
|
||||
public CustomQueueProcessor(QueueProcessorFactoryContext context) : base(context)
|
||||
public TestQueueProcessor(QueueProcessorOptions context)
|
||||
: base(context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public QueueProcessorFactoryContext Context { get; private set; }
|
||||
internal QueueProcessorOptions Context { get; private set; }
|
||||
|
||||
public override Task<bool> BeginProcessingMessageAsync(CloudQueueMessage message, CancellationToken cancellationToken)
|
||||
protected override Task<bool> BeginProcessingMessageAsync(QueueMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
BeginProcessingCount++;
|
||||
return base.BeginProcessingMessageAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
public override Task CompleteProcessingMessageAsync(CloudQueueMessage message, FunctionResult result, CancellationToken cancellationToken)
|
||||
protected override Task CompleteProcessingMessageAsync(QueueMessage message, FunctionResult result, CancellationToken cancellationToken)
|
||||
{
|
||||
CompleteProcessingCount++;
|
||||
return base.CompleteProcessingMessageAsync(message, result, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async Task ReleaseMessageAsync(CloudQueueMessage message, FunctionResult result, TimeSpan visibilityTimeout, CancellationToken cancellationToken)
|
||||
protected override async Task ReleaseMessageAsync(QueueMessage message, FunctionResult result, TimeSpan visibilityTimeout, CancellationToken cancellationToken)
|
||||
{
|
||||
// demonstrates how visibility timeout for failed messages can be customized
|
||||
// the logic here could implement exponential backoff, etc.
|
||||
|
@ -800,8 +820,11 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
}
|
||||
}
|
||||
|
||||
public class TestFixture : IDisposable
|
||||
public class TestFixture : IAsyncLifetime
|
||||
{
|
||||
public readonly BlobServiceClient BlobServiceClient;
|
||||
public readonly QueueServiceClient QueueServiceClient;
|
||||
|
||||
public TestFixture()
|
||||
{
|
||||
// We know the tests are using the default storage provider, so pull that out
|
||||
|
@ -809,32 +832,43 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<TestFixture>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
// Necessary for Blob/Queue bindings
|
||||
b.AddAzureStorageBlobs();
|
||||
b.AddAzureStorageQueues();
|
||||
})
|
||||
.Build();
|
||||
|
||||
var provider = host.Services.GetService<StorageAccountProvider>();
|
||||
StorageAccount = provider.GetHost().SdkObject;
|
||||
BlobServiceClient = TestHelpers.GetTestBlobServiceClient();
|
||||
QueueServiceClient = TestHelpers.GetTestQueueServiceClient();
|
||||
}
|
||||
|
||||
public CloudStorageAccount StorageAccount
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
await CleanBlobsAsync();
|
||||
await CleanQueuesAsync();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
private async Task CleanBlobsAsync()
|
||||
{
|
||||
CloudBlobClient blobClient = StorageAccount.CreateCloudBlobClient();
|
||||
foreach (var testContainer in blobClient.ListContainersSegmentedAsync(TestArtifactsPrefix, null).Result.Results)
|
||||
if (BlobServiceClient != null)
|
||||
{
|
||||
testContainer.DeleteAsync().Wait();
|
||||
await foreach (var testBlob in BlobServiceClient.GetBlobContainersAsync(prefix: TestArtifactsPrefix))
|
||||
{
|
||||
await BlobServiceClient.DeleteBlobContainerAsync(testBlob.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CloudQueueClient queueClient = StorageAccount.CreateCloudQueueClient();
|
||||
foreach (var testQueue in queueClient.ListQueuesSegmentedAsync(TestArtifactsPrefix, null).Result.Results)
|
||||
private async Task CleanQueuesAsync()
|
||||
{
|
||||
if (QueueServiceClient != null)
|
||||
{
|
||||
testQueue.DeleteAsync().Wait();
|
||||
await foreach (var testQueue in QueueServiceClient.GetQueuesAsync(prefix: TestArtifactsPrefix))
|
||||
{
|
||||
await QueueServiceClient.DeleteQueueAsync(testQueue.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -860,7 +894,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
public Task OnUnhandledExceptionAsync(ExceptionDispatchInfo exceptionInfo)
|
||||
{
|
||||
// TODO: FACAVAL - Validate this, tests are stepping over each other.
|
||||
if (!(exceptionInfo.SourceException is StorageException storageException &&
|
||||
if (!(exceptionInfo.SourceException is RequestFailedException storageException &&
|
||||
storageException?.InnerException is TaskCanceledException))
|
||||
{
|
||||
UnhandledExceptionInfos.Add(exceptionInfo);
|
||||
|
|
|
@ -1,583 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Cosmos.Table;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Microsoft.Azure.Storage.Queue;
|
||||
using Microsoft.Azure.WebJobs.Host.Queues;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using CloudStorageAccount = Microsoft.Azure.Storage.CloudStorageAccount;
|
||||
using TableStorageAccount = Microsoft.Azure.Cosmos.Table.CloudStorageAccount;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Various E2E tests that use only the public surface and the real Azure storage
|
||||
/// </summary>
|
||||
public class AzureStorageEndToEndTests : IClassFixture<AzureStorageEndToEndTests.TestFixture>
|
||||
{
|
||||
private const string TestArtifactsPrefix = "e2etest";
|
||||
private const string ContainerName = TestArtifactsPrefix + "container%rnd%";
|
||||
private const string BlobName = "testblob";
|
||||
|
||||
private const string TableName = TestArtifactsPrefix + "table%rnd%";
|
||||
|
||||
private const string HostStartQueueName = TestArtifactsPrefix + "startqueue%rnd%";
|
||||
private const string TestQueueName = TestArtifactsPrefix + "queue%rnd%";
|
||||
private const string TestQueueNameEtag = TestArtifactsPrefix + "etag2equeue%rnd%";
|
||||
private const string DoneQueueName = TestArtifactsPrefix + "donequeue%rnd%";
|
||||
|
||||
private const string BadMessageQueue1 = TestArtifactsPrefix + "-badmessage1-%rnd%";
|
||||
private const string BadMessageQueue2 = TestArtifactsPrefix + "-badmessage2-%rnd%";
|
||||
|
||||
private static int _badMessage1Calls;
|
||||
private static int _badMessage2Calls;
|
||||
|
||||
private static EventWaitHandle _startWaitHandle;
|
||||
private static EventWaitHandle _functionChainWaitHandle;
|
||||
private CloudStorageAccount _storageAccount;
|
||||
private TableStorageAccount _tableStorageAccount;
|
||||
private RandomNameResolver _resolver;
|
||||
private static object testResult;
|
||||
|
||||
private static string _lastMessageId;
|
||||
private static string _lastMessagePopReceipt;
|
||||
|
||||
public AzureStorageEndToEndTests(TestFixture fixture)
|
||||
{
|
||||
_storageAccount = fixture.StorageAccount;
|
||||
_tableStorageAccount = fixture.TableStorageAccount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to syncronize the application start and blob creation
|
||||
/// </summary>
|
||||
public static void NotifyStart(
|
||||
[QueueTrigger(HostStartQueueName)] string input)
|
||||
{
|
||||
_startWaitHandle.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Covers:
|
||||
/// - blob binding to custom object
|
||||
/// - blob trigger
|
||||
/// - queue writing
|
||||
/// - blob name pattern binding
|
||||
/// </summary>
|
||||
public static void BlobToQueue(
|
||||
[BlobTrigger(ContainerName + @"/{name}")] string input,
|
||||
string name,
|
||||
[Queue(TestQueueNameEtag)] out CustomObject output)
|
||||
{
|
||||
// TODO: Use CustomObject as param when POCO blob supported:
|
||||
// https://github.com/Azure/azure-webjobs-sdk/issues/995
|
||||
var inputObject = JsonConvert.DeserializeObject<CustomObject>(input);
|
||||
|
||||
CustomObject result = new CustomObject()
|
||||
{
|
||||
Text = inputObject.Text + " " + name,
|
||||
Number = inputObject.Number + 1
|
||||
};
|
||||
|
||||
output = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Covers:
|
||||
/// - queue binding to custom object
|
||||
/// - queue trigger
|
||||
/// - table writing
|
||||
/// </summary>
|
||||
public static void QueueToICollectorAndQueue(
|
||||
[QueueTrigger(TestQueueNameEtag)] CustomObject e2equeue,
|
||||
[Table(TableName)] ICollector<ITableEntity> table,
|
||||
[Queue(TestQueueName)] out CustomObject output)
|
||||
{
|
||||
const string tableKeys = "testETag";
|
||||
|
||||
DynamicTableEntity result = new DynamicTableEntity
|
||||
{
|
||||
PartitionKey = tableKeys,
|
||||
RowKey = tableKeys,
|
||||
Properties = new Dictionary<string, EntityProperty>()
|
||||
{
|
||||
{ "Text", new EntityProperty("before") },
|
||||
{ "Number", new EntityProperty("1") }
|
||||
}
|
||||
};
|
||||
|
||||
table.Add(result);
|
||||
|
||||
result.Properties["Text"] = new EntityProperty("after");
|
||||
result.ETag = "*";
|
||||
table.Add(result);
|
||||
|
||||
output = e2equeue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Covers:
|
||||
/// - queue binding to custom object
|
||||
/// - queue trigger
|
||||
/// - table writing
|
||||
/// </summary>
|
||||
public static void QueueToTable(
|
||||
[QueueTrigger(TestQueueName)] CustomObject e2equeue,
|
||||
[Table(TableName)] CloudTable table,
|
||||
[Queue(DoneQueueName)] out string e2edone)
|
||||
{
|
||||
const string tableKeys = "test";
|
||||
|
||||
CustomTableEntity result = new CustomTableEntity
|
||||
{
|
||||
PartitionKey = tableKeys,
|
||||
RowKey = tableKeys,
|
||||
Text = e2equeue.Text + " " + "QueueToTable",
|
||||
Number = e2equeue.Number + 1
|
||||
};
|
||||
|
||||
table.ExecuteAsync(TableOperation.InsertOrReplace(result)).Wait();
|
||||
|
||||
// Write a queue message to signal the scenario completion
|
||||
e2edone = "done";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the completion of the scenario
|
||||
/// </summary>
|
||||
public static void NotifyCompletion(
|
||||
[QueueTrigger(DoneQueueName)] string e2edone)
|
||||
{
|
||||
_functionChainWaitHandle.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We'll insert a bad message. It should get here okay. It will
|
||||
/// then pass it on to the next trigger.
|
||||
/// </summary>
|
||||
public static void BadMessage_CloudQueueMessage(
|
||||
[QueueTrigger(BadMessageQueue1)] CloudQueueMessage badMessageIn,
|
||||
[Queue(BadMessageQueue2)] out CloudQueueMessage badMessageOut,
|
||||
TraceWriter log)
|
||||
{
|
||||
_badMessage1Calls++;
|
||||
badMessageOut = badMessageIn;
|
||||
}
|
||||
|
||||
public static void BadMessage_String(
|
||||
[QueueTrigger(BadMessageQueue2)] string message,
|
||||
TraceWriter log)
|
||||
{
|
||||
_badMessage2Calls++;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void TableWithFilter(
|
||||
[QueueTrigger("test")] Person person,
|
||||
[Table(TableName, Filter = "(Age gt {Age}) and (Location eq '{Location}')")] JArray results)
|
||||
{
|
||||
testResult = results;
|
||||
}
|
||||
|
||||
// Uncomment the Fact attribute to run
|
||||
// [Fact(Timeout = 20 * 60 * 1000)]
|
||||
public async Task AzureStorageEndToEndSlow()
|
||||
{
|
||||
await EndToEndTest(uploadBlobBeforeHostStart: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AzureStorageEndToEndFast()
|
||||
{
|
||||
await EndToEndTest(uploadBlobBeforeHostStart: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TableFilterTest()
|
||||
{
|
||||
// Reinitialize the name resolver to avoid conflicts
|
||||
_resolver = new RandomNameResolver();
|
||||
|
||||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<AzureStorageEndToEndTests>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<INameResolver>(_resolver);
|
||||
})
|
||||
.Build();
|
||||
|
||||
// write test entities
|
||||
string testTableName = _resolver.ResolveInString(TableName);
|
||||
CloudTableClient tableClient = _tableStorageAccount.CreateCloudTableClient();
|
||||
CloudTable table = tableClient.GetTableReference(testTableName);
|
||||
await table.CreateIfNotExistsAsync();
|
||||
var operation = new TableBatchOperation();
|
||||
operation.Insert(new Person
|
||||
{
|
||||
PartitionKey = "1",
|
||||
RowKey = "1",
|
||||
Name = "Lary",
|
||||
Age = 20,
|
||||
Location = "Seattle"
|
||||
});
|
||||
operation.Insert(new Person
|
||||
{
|
||||
PartitionKey = "1",
|
||||
RowKey = "2",
|
||||
Name = "Moe",
|
||||
Age = 35,
|
||||
Location = "Seattle"
|
||||
});
|
||||
operation.Insert(new Person
|
||||
{
|
||||
PartitionKey = "1",
|
||||
RowKey = "3",
|
||||
Name = "Curly",
|
||||
Age = 45,
|
||||
Location = "Texas"
|
||||
});
|
||||
operation.Insert(new Person
|
||||
{
|
||||
PartitionKey = "1",
|
||||
RowKey = "4",
|
||||
Name = "Bill",
|
||||
Age = 28,
|
||||
Location = "Tam O'Shanter"
|
||||
});
|
||||
|
||||
await table.ExecuteBatchAsync(operation);
|
||||
|
||||
JobHost jobHost = host.GetJobHost();
|
||||
var methodInfo = GetType().GetMethod(nameof(TableWithFilter));
|
||||
var input = new Person { Age = 25, Location = "Seattle" };
|
||||
string json = JsonConvert.SerializeObject(input);
|
||||
var arguments = new { person = json };
|
||||
await jobHost.CallAsync(methodInfo, arguments);
|
||||
|
||||
// wait for test results to appear
|
||||
await TestHelpers.Await(() => testResult != null);
|
||||
|
||||
JArray results = (JArray)testResult;
|
||||
Assert.Single(results);
|
||||
|
||||
input = new Person { Age = 25, Location = "Tam O'Shanter" };
|
||||
json = JsonConvert.SerializeObject(input);
|
||||
arguments = new { person = json };
|
||||
await jobHost.CallAsync(methodInfo, arguments);
|
||||
await TestHelpers.Await(() => testResult != null);
|
||||
results = (JArray)testResult;
|
||||
Assert.Single(results);
|
||||
Assert.Equal("Bill", (string)results[0]["Name"]);
|
||||
}
|
||||
|
||||
private async Task EndToEndTest(bool uploadBlobBeforeHostStart)
|
||||
{
|
||||
// Reinitialize the name resolver to avoid conflicts
|
||||
_resolver = new RandomNameResolver();
|
||||
|
||||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<AzureStorageEndToEndTests>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<INameResolver>(_resolver);
|
||||
})
|
||||
.Build();
|
||||
|
||||
if (uploadBlobBeforeHostStart)
|
||||
{
|
||||
// The function will be triggered fast because the blob is already there
|
||||
await UploadTestObject();
|
||||
}
|
||||
|
||||
// The jobs host is started
|
||||
JobHost jobHost = host.GetJobHost();
|
||||
|
||||
_functionChainWaitHandle = new ManualResetEvent(initialState: false);
|
||||
|
||||
await host.StartAsync();
|
||||
|
||||
if (!uploadBlobBeforeHostStart)
|
||||
{
|
||||
await WaitForTestFunctionsToStart();
|
||||
await UploadTestObject();
|
||||
}
|
||||
|
||||
var waitTime = TimeSpan.FromSeconds(15);
|
||||
bool signaled = _functionChainWaitHandle.WaitOne(waitTime);
|
||||
|
||||
// Stop the host and wait for it to finish
|
||||
await host.StopAsync();
|
||||
|
||||
Assert.True(signaled, $"[{DateTime.UtcNow.ToString("HH:mm:ss.fff")}] Function chain did not complete in {waitTime}. Logs:{Environment.NewLine}{host.GetTestLoggerProvider().GetLogString()}");
|
||||
|
||||
// Verify
|
||||
await VerifyTableResultsAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BadQueueMessageE2ETests()
|
||||
{
|
||||
// This test ensures that the host does not crash on a bad message (it previously did)
|
||||
// Insert a bad message into a queue that should:
|
||||
// - trigger BadMessage_CloudQueueMessage, which will put it into a second queue that will
|
||||
// - trigger BadMessage_String, which should fail
|
||||
// - BadMessage_String should fail repeatedly until it is moved to the poison queue
|
||||
// The test will watch that poison queue to know when to complete
|
||||
|
||||
// Reinitialize the name resolver to avoid conflicts
|
||||
_resolver = new RandomNameResolver();
|
||||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<AzureStorageEndToEndTests>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
// use a custom processor so we can grab the Id and PopReceipt
|
||||
services.AddSingleton<IQueueProcessorFactory>(new TestQueueProcessorFactory());
|
||||
services.AddSingleton<INameResolver>(_resolver);
|
||||
})
|
||||
.Build();
|
||||
|
||||
TestLoggerProvider loggerProvider = host.GetTestLoggerProvider();
|
||||
|
||||
// The jobs host is started
|
||||
host.Start();
|
||||
|
||||
// Construct a bad message:
|
||||
// - use a GUID as the content, which is not a valid base64 string
|
||||
// - pass 'true', to indicate that it is a base64 string
|
||||
string messageContent = Guid.NewGuid().ToString();
|
||||
var message = new CloudQueueMessage(messageContent, true);
|
||||
|
||||
var queueClient = _storageAccount.CreateCloudQueueClient();
|
||||
var queue = queueClient.GetQueueReference(_resolver.ResolveInString(BadMessageQueue1));
|
||||
await queue.CreateIfNotExistsAsync();
|
||||
await queue.ClearAsync();
|
||||
|
||||
// the poison queue will end up off of the second queue
|
||||
var poisonQueue = queueClient.GetQueueReference(_resolver.ResolveInString(BadMessageQueue2) + "-poison");
|
||||
await poisonQueue.DeleteIfExistsAsync();
|
||||
|
||||
await queue.AddMessageAsync(message);
|
||||
|
||||
CloudQueueMessage poisonMessage = null;
|
||||
await TestHelpers.Await(async () =>
|
||||
{
|
||||
bool done = false;
|
||||
if (await poisonQueue.ExistsAsync())
|
||||
{
|
||||
poisonMessage = await poisonQueue.GetMessageAsync();
|
||||
done = poisonMessage != null;
|
||||
|
||||
if (done)
|
||||
{
|
||||
// Sleep briefly, then make sure the other message has been deleted.
|
||||
// If so, trying to delete it again will throw an error.
|
||||
Thread.Sleep(1000);
|
||||
|
||||
// The message is in the second queue
|
||||
var queue2 = queueClient.GetQueueReference(_resolver.ResolveInString(BadMessageQueue2));
|
||||
|
||||
Azure.Storage.StorageException ex = await Assert.ThrowsAsync<Azure.Storage.StorageException>(
|
||||
() => queue2.DeleteMessageAsync(_lastMessageId, _lastMessagePopReceipt));
|
||||
Assert.Equal("MessageNotFound", ex.RequestInformation.ExtendedErrorInformation.ErrorCode);
|
||||
}
|
||||
}
|
||||
var logs = loggerProvider.GetAllLogMessages();
|
||||
return done;
|
||||
});
|
||||
|
||||
await host.StopAsync();
|
||||
|
||||
// find the raw string to compare it to the original
|
||||
Assert.NotNull(poisonMessage);
|
||||
var propInfo = typeof(CloudQueueMessage).GetProperty("RawString", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
string rawString = propInfo.GetValue(poisonMessage) as string;
|
||||
Assert.Equal(messageContent, rawString);
|
||||
|
||||
// Make sure the functions were called correctly
|
||||
Assert.Equal(1, _badMessage1Calls);
|
||||
Assert.Equal(0, _badMessage2Calls);
|
||||
|
||||
// Validate Logger
|
||||
var loggerErrors = loggerProvider.GetAllLogMessages().Where(l => l.Level == Microsoft.Extensions.Logging.LogLevel.Error);
|
||||
Assert.True(loggerErrors.All(t => t.Exception.InnerException.InnerException is FormatException));
|
||||
}
|
||||
|
||||
private async Task UploadTestObject()
|
||||
{
|
||||
string testContainerName = _resolver.ResolveInString(ContainerName);
|
||||
|
||||
CloudBlobContainer container = _storageAccount.CreateCloudBlobClient().GetContainerReference(testContainerName);
|
||||
await container.CreateIfNotExistsAsync();
|
||||
|
||||
// The test blob
|
||||
CloudBlockBlob testBlob = container.GetBlockBlobReference(BlobName);
|
||||
CustomObject testObject = new CustomObject()
|
||||
{
|
||||
Text = "Test",
|
||||
Number = 42
|
||||
};
|
||||
|
||||
await testBlob.UploadTextAsync(JsonConvert.SerializeObject(testObject));
|
||||
}
|
||||
|
||||
private async Task WaitForTestFunctionsToStart()
|
||||
{
|
||||
_startWaitHandle = new ManualResetEvent(initialState: false);
|
||||
|
||||
string startQueueName = _resolver.ResolveInString(HostStartQueueName);
|
||||
|
||||
CloudQueueClient queueClient = _storageAccount.CreateCloudQueueClient();
|
||||
CloudQueue queue = queueClient.GetQueueReference(startQueueName);
|
||||
await queue.CreateIfNotExistsAsync();
|
||||
await queue.AddMessageAsync(new CloudQueueMessage(String.Empty));
|
||||
|
||||
_startWaitHandle.WaitOne(30000);
|
||||
}
|
||||
|
||||
private async Task VerifyTableResultsAsync()
|
||||
{
|
||||
string testTableName = _resolver.ResolveInString(TableName);
|
||||
|
||||
CloudTableClient tableClient = _tableStorageAccount.CreateCloudTableClient();
|
||||
CloudTable table = tableClient.GetTableReference(testTableName);
|
||||
|
||||
Assert.True(await table.ExistsAsync(), "Result table not found");
|
||||
|
||||
TableQuery query = new TableQuery()
|
||||
.Where(TableQuery.CombineFilters(
|
||||
TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "test"),
|
||||
TableOperators.And,
|
||||
TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, "test")))
|
||||
.Take(1);
|
||||
DynamicTableEntity result = (await table.ExecuteQuerySegmentedAsync(query, null)).FirstOrDefault();
|
||||
|
||||
// Ensure expected row found
|
||||
Assert.NotNull(result);
|
||||
|
||||
Assert.Equal("Test testblob QueueToTable", result.Properties["Text"].StringValue);
|
||||
Assert.Equal(44, result.Properties["Number"].Int32Value);
|
||||
|
||||
query = new TableQuery()
|
||||
.Where(TableQuery.CombineFilters(
|
||||
TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "testETag"),
|
||||
TableOperators.And,
|
||||
TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, "testETag")))
|
||||
.Take(1);
|
||||
result = (await table.ExecuteQuerySegmentedAsync(query, null)).FirstOrDefault();
|
||||
|
||||
// Ensure expected row found
|
||||
Assert.NotNull(result);
|
||||
|
||||
Assert.Equal("after", result.Properties["Text"].StringValue);
|
||||
}
|
||||
|
||||
private class CustomTableEntity : TableEntity
|
||||
{
|
||||
public string Text { get; set; }
|
||||
|
||||
public int Number { get; set; }
|
||||
}
|
||||
|
||||
public class Person : TableEntity
|
||||
{
|
||||
public int Age { get; set; }
|
||||
public string Location { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private class TestQueueProcessorFactory : IQueueProcessorFactory
|
||||
{
|
||||
public QueueProcessor Create(QueueProcessorFactoryContext context)
|
||||
{
|
||||
return new TestQueueProcessor(context);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestQueueProcessor : QueueProcessor
|
||||
{
|
||||
public TestQueueProcessor(QueueProcessorFactoryContext context)
|
||||
: base(context)
|
||||
{
|
||||
}
|
||||
|
||||
public override Task<bool> BeginProcessingMessageAsync(CloudQueueMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
_lastMessageId = message.Id;
|
||||
_lastMessagePopReceipt = message.PopReceipt;
|
||||
|
||||
return base.BeginProcessingMessageAsync(message, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestFixture : IDisposable
|
||||
{
|
||||
public TestFixture()
|
||||
{
|
||||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<TestFixture>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
})
|
||||
.Build();
|
||||
|
||||
var provider = host.Services.GetService<StorageAccountProvider>();
|
||||
StorageAccount = provider.GetHost().SdkObject;
|
||||
TableStorageAccount = provider.GetHost().TableSdkObject;
|
||||
}
|
||||
|
||||
public CloudStorageAccount StorageAccount
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public TableStorageAccount TableStorageAccount
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CloudBlobClient blobClient = StorageAccount.CreateCloudBlobClient();
|
||||
foreach (var testContainer in blobClient.ListContainersSegmentedAsync(TestArtifactsPrefix, null).Result.Results)
|
||||
{
|
||||
testContainer.DeleteAsync().Wait();
|
||||
}
|
||||
|
||||
CloudQueueClient queueClient = StorageAccount.CreateCloudQueueClient();
|
||||
foreach (var testQueue in queueClient.ListQueuesSegmentedAsync(TestArtifactsPrefix, null).Result.Results)
|
||||
{
|
||||
testQueue.DeleteAsync().Wait();
|
||||
}
|
||||
|
||||
CloudTableClient tableClient = TableStorageAccount.CreateCloudTableClient();
|
||||
foreach (var testTable in tableClient.ListTablesSegmentedAsync(TestArtifactsPrefix, null).Result.Results)
|
||||
{
|
||||
testTable.DeleteAsync().Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,846 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
||||
{
|
||||
public class BlobBindingEndToEndTests : IClassFixture<BlobBindingEndToEndTests.TestFixture>
|
||||
{
|
||||
private const string TestArtifactPrefix = "e2etestbindings";
|
||||
private const string ContainerName = TestArtifactPrefix + "-%rnd%";
|
||||
private const string OutputContainerName = TestArtifactPrefix + "-out%rnd%";
|
||||
private const string PageBlobContainerName = TestArtifactPrefix + "pageblobs-%rnd%";
|
||||
private const string AppendBlobContainerName = TestArtifactPrefix + "appendblobs-%rnd%";
|
||||
private const string HierarchicalBlobContainerName = TestArtifactPrefix + "subblobs-%rnd%";
|
||||
private const string TestData = "TestData";
|
||||
private readonly TestFixture _fixture;
|
||||
private static int _numBlobsRead;
|
||||
|
||||
public BlobBindingEndToEndTests(TestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_numBlobsRead = 0;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToCloudBlobContainer()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("CloudBlobContainerBinding"));
|
||||
|
||||
Assert.Equal(6, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToCloudBlobDirectory()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("CloudBlobDirectoryBinding"));
|
||||
|
||||
Assert.Equal(3, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToCloudBlobContainer_WithModelBinding()
|
||||
{
|
||||
TestPoco poco = new TestPoco
|
||||
{
|
||||
A = _fixture.NameResolver.ResolveWholeString(ContainerName)
|
||||
};
|
||||
string json = JsonConvert.SerializeObject(poco);
|
||||
var arguments = new { poco = json };
|
||||
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("CloudBlobContainerBinding_WithModelBinding"), arguments);
|
||||
|
||||
Assert.Equal(6, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToCloudBlockBlob_WithUrlBinding()
|
||||
{
|
||||
// get url for the test blob
|
||||
CloudBlockBlob blob = _fixture.BlobContainer.GetBlockBlobReference("blob1");
|
||||
TestPoco poco = new TestPoco
|
||||
{
|
||||
A = blob.Uri.ToString()
|
||||
};
|
||||
string json = JsonConvert.SerializeObject(poco);
|
||||
var arguments = new { poco = json };
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("CloudBlockBlobBinding_WithUrlBinding"), arguments);
|
||||
|
||||
Assert.Equal(1, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToCloudBlob_WithModelBinding_Fail()
|
||||
{
|
||||
TestPoco poco = new TestPoco
|
||||
{
|
||||
A = _fixture.NameResolver.ResolveWholeString(ContainerName)
|
||||
};
|
||||
string json = JsonConvert.SerializeObject(poco);
|
||||
var arguments = new { poco = json };
|
||||
var ex = await Assert.ThrowsAsync<FunctionInvocationException>(() =>
|
||||
_fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("CloudBlockBlobBinding_WithUrlBinding"), arguments));
|
||||
// CloudBlockBlobBinding_WithUrlBinding is suppose to bind to a blob
|
||||
Assert.Equal($"Invalid blob path specified : '{poco.A}'. Blob identifiers must be in the format 'container/blob'.", ex.InnerException.InnerException.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToCloudBlobContainer_WithUrlBinding_Fail()
|
||||
{
|
||||
// get url for the test blob
|
||||
CloudBlockBlob blob = _fixture.BlobContainer.GetBlockBlobReference("blob1");
|
||||
TestPoco poco = new TestPoco
|
||||
{
|
||||
A = blob.Uri.ToString()
|
||||
};
|
||||
string json = JsonConvert.SerializeObject(poco);
|
||||
var arguments = new { poco = json };
|
||||
var ex = await Assert.ThrowsAsync<FunctionInvocationException>(() =>
|
||||
_fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("CloudBlobContainerBinding_WithModelBinding"), arguments));
|
||||
// CloudBlobContainerBinding_WithModelBinding is suppose to bind to a container
|
||||
Assert.IsType<FormatException>(ex.InnerException.InnerException);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToIEnumerableCloudBlockBlob_WithPrefixFilter()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableCloudBlockBlobBinding_WithPrefixFilter"));
|
||||
|
||||
Assert.Equal(3, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToIEnumerableCloudBlockBlob_WithPrefixFilter_NoMatchingBlobs()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableCloudBlockBlobBinding_WithPrefixFilter_NoMatchingBlobs"));
|
||||
|
||||
Assert.Equal(0, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToIEnumerableCloudBlockBlob_WithPrefixFilter_HierarchicalBlobs()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableCloudBlockBlobBinding_WithPrefixFilter_HierarchicalBlobs"));
|
||||
|
||||
Assert.Equal(2, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToIEnumerableCloudBlockBlob_WithPrefixFilter_HierarchicalBlobs_UsesFlatBlobListing()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableCloudBlockBlobBinding_WithPrefixFilter_HierarchicalBlobs_UsesFlatBlobListing"));
|
||||
|
||||
Assert.Equal(3, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToIEnumerableCloudBlockBlob_WithModelBinding()
|
||||
{
|
||||
TestPoco poco = new TestPoco
|
||||
{
|
||||
A = _fixture.NameResolver.ResolveWholeString(ContainerName),
|
||||
B = "bl"
|
||||
};
|
||||
string json = JsonConvert.SerializeObject(poco);
|
||||
var arguments = new { poco = json };
|
||||
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableCloudBlockBlobBinding_WithModelBinding"), arguments);
|
||||
|
||||
Assert.Equal(3, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToIEnumerableCloudPageBlob()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableCloudPageBlobBinding"));
|
||||
|
||||
Assert.Equal(2, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToIEnumerableCloudAppendBlob()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableCloudAppendBlobBinding"));
|
||||
|
||||
Assert.Equal(3, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToIEnumerableString()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableStringBinding"));
|
||||
|
||||
Assert.Equal(6, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToIEnumerableStream()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableStreamBinding"));
|
||||
|
||||
Assert.Equal(6, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToIEnumerableTextReader()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableTextReaderBinding"));
|
||||
|
||||
Assert.Equal(6, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToIEnumerableICloudBlob()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("IEnumerableICloudBlobBinding"));
|
||||
|
||||
Assert.Equal(6, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToByteArray()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("ByteArrayBinding"));
|
||||
|
||||
Assert.Equal(1, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("StringBinding_Block")]
|
||||
[InlineData("StringBinding_Page")]
|
||||
[InlineData("StringBinding_Append")]
|
||||
public async Task BindToString(string functionName)
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod(functionName));
|
||||
|
||||
Assert.Equal(1, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("StreamBindingReadable_Block")]
|
||||
[InlineData("StreamBindingReadable_Page")]
|
||||
[InlineData("StreamBindingReadable_Append")]
|
||||
public async Task BindToStream(string functionName)
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod(functionName));
|
||||
|
||||
Assert.Equal(1, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToOutString()
|
||||
{
|
||||
CloudBlockBlob blob = _fixture.BlobContainer.GetBlockBlobReference("overwrite");
|
||||
Assert.Equal(TestData, await blob.DownloadTextAsync());
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("OutStringBinding_Block"));
|
||||
string text = null;
|
||||
using (var reader = new StreamReader(await blob.OpenReadAsync()))
|
||||
{
|
||||
text = reader.ReadToEnd();
|
||||
}
|
||||
Assert.Equal("overwritten", text);
|
||||
await blob.UploadTextAsync(TestData);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("OutStringBinding_Page")]
|
||||
[InlineData("OutStringBinding_Append")]
|
||||
public async Task BindToOutString_Fails(string functionName)
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<FunctionInvocationException>(() =>
|
||||
_fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod(functionName)));
|
||||
|
||||
var innerEx = ex.InnerException.InnerException;
|
||||
Assert.Equal("Cannot bind to page or append blobs using 'out string', 'TextWriter', or writable 'Stream' parameters.", innerEx.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToTextWriter()
|
||||
{
|
||||
CloudBlockBlob blob = _fixture.BlobContainer.GetBlockBlobReference("overwrite");
|
||||
Assert.Equal(TestData, await blob.DownloadTextAsync());
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("OutStringBinding_Block"));
|
||||
|
||||
string text = null;
|
||||
using (var reader = new StreamReader(await blob.OpenReadAsync()))
|
||||
{
|
||||
text = reader.ReadToEnd();
|
||||
}
|
||||
Assert.Equal("overwritten", text);
|
||||
await blob.UploadTextAsync(TestData);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("TextWriterBinding_Page")]
|
||||
[InlineData("TextWriterBinding_Append")]
|
||||
public async Task BindToTextWriter_Fails(string functionName)
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<FunctionInvocationException>(() =>
|
||||
_fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod(functionName)));
|
||||
|
||||
var innerEx = ex.InnerException.InnerException;
|
||||
Assert.Equal("Cannot bind to page or append blobs using 'out string', 'TextWriter', or writable 'Stream' parameters.", innerEx.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToByteArray_Output()
|
||||
{
|
||||
// if the function sets the output binding to null, no blob
|
||||
// should be written
|
||||
var arguments = new { input = "null" };
|
||||
var method = typeof(BlobBindingEndToEndTests).GetMethod("ByteArrayOutputBinding");
|
||||
await _fixture.JobHost.CallAsync(method, arguments);
|
||||
|
||||
CloudBlockBlob blob = _fixture.OutputBlobContainer.GetBlockBlobReference("blob1");
|
||||
Assert.False(await blob.ExistsAsync());
|
||||
|
||||
// if the function sets a value, the blob should be written
|
||||
arguments = new { input = TestData };
|
||||
await _fixture.JobHost.CallAsync(method, arguments);
|
||||
|
||||
Assert.True(await blob.ExistsAsync());
|
||||
string result = await blob.DownloadTextAsync();
|
||||
Assert.Equal(TestData, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindToByteArray_Trigger()
|
||||
{
|
||||
var arguments = new { blob = string.Format("{0}/{1}", _fixture.NameResolver.ResolveWholeString(ContainerName), "blob1") };
|
||||
|
||||
await _fixture.JobHost.CallAsync(typeof(BlobBindingEndToEndTests).GetMethod("ByteArrayTriggerBinding"), arguments);
|
||||
|
||||
Assert.Equal(1, _numBlobsRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BlobTriggerSingletonListener_LockIsHeld()
|
||||
{
|
||||
await _fixture.VerifyLockState("WebJobs.Internal.Blobs.Listener", LeaseState.Leased, LeaseStatus.Locked);
|
||||
}
|
||||
|
||||
// This function just exists to force initialization of the
|
||||
// blob listener pipeline
|
||||
public static void TestBlobTrigger([BlobTrigger("test/test")] string blob)
|
||||
{
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void CloudBlobContainerBinding(
|
||||
[Blob(ContainerName)] CloudBlobContainer container)
|
||||
{
|
||||
var blobs = container.ListBlobsSegmentedAsync(null).Result;
|
||||
foreach (CloudBlockBlob blob in blobs.Results)
|
||||
{
|
||||
string content = blob.DownloadTextAsync().Result;
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
_numBlobsRead = blobs.Results.Count();
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static async Task CloudBlobDirectoryBinding(
|
||||
[Blob(HierarchicalBlobContainerName + "/sub")] CloudBlobDirectory directory)
|
||||
{
|
||||
var directoryItems = await directory.ListBlobsSegmentedAsync(null);
|
||||
|
||||
var blobs = directoryItems.Results.OfType<CloudBlockBlob>();
|
||||
foreach (CloudBlockBlob blob in blobs)
|
||||
{
|
||||
string content = blob.DownloadTextAsync().Result;
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
_numBlobsRead += blobs.Count();
|
||||
|
||||
CloudBlobDirectory subDirectory = directoryItems.Results.OfType<CloudBlobDirectory>().Single();
|
||||
CloudBlockBlob subBlob = (await subDirectory.ListBlobsSegmentedAsync(null)).Results.Cast<CloudBlockBlob>().Single();
|
||||
Assert.Equal(TestData, await subBlob.DownloadTextAsync());
|
||||
_numBlobsRead += 1;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void CloudBlobContainerBinding_WithModelBinding(
|
||||
[QueueTrigger("testqueue")] TestPoco poco,
|
||||
[Blob("{A}")] CloudBlobContainer container)
|
||||
{
|
||||
CloudBlobContainerBinding(container);
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void CloudBlockBlobBinding_WithUrlBinding(
|
||||
[QueueTrigger("testqueue")] TestPoco poco,
|
||||
[Blob("{A}")] string blob)
|
||||
{
|
||||
Assert.Equal(TestData, blob);
|
||||
_numBlobsRead = 1;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static async Task IEnumerableCloudBlockBlobBinding_WithPrefixFilter(
|
||||
[Blob(ContainerName + "/blo")] IEnumerable<CloudBlockBlob> blobs)
|
||||
{
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
string content = await blob.DownloadTextAsync();
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
_numBlobsRead = blobs.Count();
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void IEnumerableCloudBlockBlobBinding_WithPrefixFilter_NoMatchingBlobs(
|
||||
[Blob(ContainerName + "/dne")] IEnumerable<CloudBlockBlob> blobs)
|
||||
{
|
||||
_numBlobsRead = blobs.Count();
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static async Task IEnumerableCloudBlockBlobBinding_WithPrefixFilter_HierarchicalBlobs(
|
||||
[Blob(HierarchicalBlobContainerName + "/sub/bl")] IEnumerable<CloudBlockBlob> blobs)
|
||||
{
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
string content = await blob.DownloadTextAsync();
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
_numBlobsRead = blobs.Count();
|
||||
}
|
||||
|
||||
// Ensure that a flat blob listing is used, meaning if a route prefix covers
|
||||
// sub directries, blobs within those sub directories are returned. Users can bind
|
||||
// to CloudBlobDirectory if they want to operate on directories.
|
||||
[NoAutomaticTrigger]
|
||||
public static async Task IEnumerableCloudBlockBlobBinding_WithPrefixFilter_HierarchicalBlobs_UsesFlatBlobListing(
|
||||
[Blob(HierarchicalBlobContainerName + "/sub")] IEnumerable<CloudBlockBlob> blobs)
|
||||
{
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
string content = await blob.DownloadTextAsync();
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
_numBlobsRead = blobs.Count();
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static async Task IEnumerableCloudBlockBlobBinding_WithModelBinding(
|
||||
[QueueTrigger("testqueue")] TestPoco poco,
|
||||
[Blob("{A}/{B}ob")] IEnumerable<CloudBlockBlob> blobs)
|
||||
{
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
string content = await blob.DownloadTextAsync();
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
_numBlobsRead = blobs.Count();
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static async Task IEnumerableCloudPageBlobBinding(
|
||||
[Blob(PageBlobContainerName)] IEnumerable<CloudPageBlob> blobs)
|
||||
{
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
byte[] bytes = new byte[512];
|
||||
int byteCount = await blob.DownloadToByteArrayAsync(bytes, 0);
|
||||
string content = Encoding.UTF8.GetString(bytes, 0, byteCount);
|
||||
Assert.True(content.StartsWith(TestData));
|
||||
}
|
||||
_numBlobsRead = blobs.Count();
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static async Task IEnumerableCloudAppendBlobBinding(
|
||||
[Blob(AppendBlobContainerName)] IEnumerable<CloudAppendBlob> blobs)
|
||||
{
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
string content = await blob.DownloadTextAsync();
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
_numBlobsRead = blobs.Count();
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void IEnumerableStringBinding(
|
||||
[Blob(ContainerName)] IEnumerable<string> blobs)
|
||||
{
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
Assert.Equal(TestData, blob);
|
||||
}
|
||||
_numBlobsRead = blobs.Count();
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void IEnumerableStreamBinding(
|
||||
[Blob(ContainerName)] IEnumerable<Stream> blobs)
|
||||
{
|
||||
foreach (var blobStream in blobs)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(blobStream))
|
||||
{
|
||||
string content = reader.ReadToEnd();
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
}
|
||||
_numBlobsRead = blobs.Count();
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void IEnumerableTextReaderBinding(
|
||||
[Blob(ContainerName)] IEnumerable<TextReader> blobs)
|
||||
{
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
string content = blob.ReadToEnd();
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
_numBlobsRead = blobs.Count();
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static async Task IEnumerableICloudBlobBinding(
|
||||
[Blob(ContainerName)] IEnumerable<ICloudBlob> blobs)
|
||||
{
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
Stream stream = await blob.OpenReadAsync(null, null, null);
|
||||
using (StreamReader reader = new StreamReader(stream))
|
||||
{
|
||||
string content = reader.ReadToEnd();
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
}
|
||||
_numBlobsRead = blobs.Count();
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void StringBinding_Block(
|
||||
[Blob(ContainerName + "/blob1")] string blob)
|
||||
{
|
||||
Assert.Equal(TestData, blob);
|
||||
_numBlobsRead = 1;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void StringBinding_Page(
|
||||
[Blob(PageBlobContainerName + "/blob1")] string blob)
|
||||
{
|
||||
blob = blob.Trim('\0');
|
||||
Assert.Equal(TestData, blob);
|
||||
_numBlobsRead = 1;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void StringBinding_Append(
|
||||
[Blob(AppendBlobContainerName + "/blob1")] string blob)
|
||||
{
|
||||
Assert.Equal(TestData, blob);
|
||||
_numBlobsRead = 1;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void OutStringBinding_Block(
|
||||
[Blob(ContainerName + "/overwrite")] out string blob)
|
||||
{
|
||||
blob = "overwritten";
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void OutStringBinding_Page(
|
||||
[Blob(PageBlobContainerName + "/blob1")] out string blob)
|
||||
{
|
||||
// this wil fail before getting this far
|
||||
blob = TestData;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void OutStringBinding_Append(
|
||||
[Blob(AppendBlobContainerName + "/blob1")] out string blob)
|
||||
{
|
||||
// this will fail before getting this far
|
||||
blob = TestData;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void TextWriterBinding_Block(
|
||||
[Blob(ContainerName + "/overwrite")] TextWriter blob)
|
||||
{
|
||||
// this will fail
|
||||
blob.Write("overwritten");
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void TextWriterBinding_Page(
|
||||
[Blob(PageBlobContainerName + "/blob1")] TextWriter blob)
|
||||
{
|
||||
// this will fail
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void TextWriterBinding_Append(
|
||||
[Blob(AppendBlobContainerName + "/blob1")] TextWriter blob)
|
||||
{
|
||||
// this will fail
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void StreamBindingReadable_Block(
|
||||
[Blob(ContainerName + "/blob1", FileAccess.Read)] Stream blobStream)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(blobStream))
|
||||
{
|
||||
string content = reader.ReadToEnd();
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
_numBlobsRead = 1;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void StreamBindingReadable_Page(
|
||||
[Blob(ContainerName + "/blob1", FileAccess.Read)] Stream blobStream)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(blobStream))
|
||||
{
|
||||
string content = reader.ReadToEnd();
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
_numBlobsRead = 1;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void StreamBindingReadable_Append(
|
||||
[Blob(ContainerName + "/blob1", FileAccess.Read)] Stream blobStream)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(blobStream))
|
||||
{
|
||||
string content = reader.ReadToEnd();
|
||||
Assert.Equal(TestData, content);
|
||||
}
|
||||
_numBlobsRead = 1;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void ByteArrayBinding(
|
||||
[Blob(ContainerName + "/blob1")] byte[] blob)
|
||||
{
|
||||
string result = Encoding.UTF8.GetString(blob);
|
||||
Assert.Equal(TestData, result);
|
||||
_numBlobsRead = 1;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void ByteArrayBinding_Page(
|
||||
[Blob(PageBlobContainerName + "/blob1")] byte[] blob)
|
||||
{
|
||||
string result = Encoding.UTF8.GetString(blob);
|
||||
Assert.Equal(TestData, result);
|
||||
_numBlobsRead = 1;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void ByteArrayBinding_Append(
|
||||
[Blob(AppendBlobContainerName + "/blob1")] byte[] blob)
|
||||
{
|
||||
string result = Encoding.UTF8.GetString(blob);
|
||||
Assert.Equal(TestData, result);
|
||||
_numBlobsRead = 1;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void ByteArrayOutputBinding(string input,
|
||||
[Blob(OutputContainerName + "/blob1")] out byte[] output)
|
||||
{
|
||||
if (input == "null")
|
||||
{
|
||||
output = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
output = Encoding.UTF8.GetBytes(input);
|
||||
}
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void ByteArrayTriggerBinding(
|
||||
[BlobTrigger(ContainerName)] byte[] blob)
|
||||
{
|
||||
string result = Encoding.UTF8.GetString(blob);
|
||||
Assert.Equal(TestData, result);
|
||||
_numBlobsRead = 1;
|
||||
}
|
||||
|
||||
public class TestFixture : IAsyncLifetime
|
||||
{
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
RandomNameResolver nameResolver = new RandomNameResolver();
|
||||
|
||||
Host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<BlobBindingEndToEndTests>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
RuntimeStorageWebJobsBuilderExtensions.AddAzureStorageCoreServices(b);
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<INameResolver>(nameResolver);
|
||||
})
|
||||
.Build();
|
||||
|
||||
|
||||
JobHost = Host.GetJobHost();
|
||||
|
||||
var provider = Host.Services.GetService<StorageAccountProvider>();
|
||||
StorageAccount = provider.GetHost().SdkObject;
|
||||
CloudBlobClient blobClient = StorageAccount.CreateCloudBlobClient();
|
||||
|
||||
BlobContainer = blobClient.GetContainerReference(nameResolver.ResolveInString(ContainerName));
|
||||
Assert.False(await BlobContainer.ExistsAsync());
|
||||
await BlobContainer.CreateAsync();
|
||||
|
||||
OutputBlobContainer = blobClient.GetContainerReference(nameResolver.ResolveInString(OutputContainerName));
|
||||
|
||||
CloudBlobContainer pageBlobContainer = blobClient.GetContainerReference(nameResolver.ResolveInString(PageBlobContainerName));
|
||||
Assert.False(await pageBlobContainer.ExistsAsync());
|
||||
await pageBlobContainer.CreateAsync();
|
||||
|
||||
CloudBlobContainer hierarchicalBlobContainer = blobClient.GetContainerReference(nameResolver.ResolveInString(HierarchicalBlobContainerName));
|
||||
Assert.False(await hierarchicalBlobContainer.ExistsAsync());
|
||||
await hierarchicalBlobContainer.CreateAsync();
|
||||
|
||||
CloudBlobContainer appendBlobContainer = blobClient.GetContainerReference(nameResolver.ResolveInString(AppendBlobContainerName));
|
||||
Assert.False(await appendBlobContainer.ExistsAsync());
|
||||
await appendBlobContainer.CreateAsync();
|
||||
|
||||
await Host.StartAsync();
|
||||
|
||||
// upload some test blobs
|
||||
CloudBlockBlob blob = BlobContainer.GetBlockBlobReference("blob1");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
blob = BlobContainer.GetBlockBlobReference("blob2");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
blob = BlobContainer.GetBlockBlobReference("blob3");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
blob = BlobContainer.GetBlockBlobReference("file1");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
blob = BlobContainer.GetBlockBlobReference("file2");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
blob = BlobContainer.GetBlockBlobReference("overwrite");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
|
||||
// add a couple hierarchical blob paths
|
||||
blob = hierarchicalBlobContainer.GetBlockBlobReference("sub/blob1");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
blob = hierarchicalBlobContainer.GetBlockBlobReference("sub/blob2");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
blob = hierarchicalBlobContainer.GetBlockBlobReference("sub/sub/blob3");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
blob = hierarchicalBlobContainer.GetBlockBlobReference("blob4");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
|
||||
byte[] bytes = new byte[512];
|
||||
byte[] testBytes = Encoding.UTF8.GetBytes(TestData);
|
||||
for (int i = 0; i < testBytes.Length; i++)
|
||||
{
|
||||
bytes[i] = testBytes[i];
|
||||
}
|
||||
CloudPageBlob pageBlob = pageBlobContainer.GetPageBlobReference("blob1");
|
||||
await pageBlob.UploadFromByteArrayAsync(bytes, 0, bytes.Length);
|
||||
pageBlob = pageBlobContainer.GetPageBlobReference("blob2");
|
||||
await pageBlob.UploadFromByteArrayAsync(bytes, 0, bytes.Length);
|
||||
|
||||
CloudAppendBlob appendBlob = appendBlobContainer.GetAppendBlobReference("blob1");
|
||||
await appendBlob.UploadTextAsync(TestData);
|
||||
appendBlob = appendBlobContainer.GetAppendBlobReference("blob2");
|
||||
await appendBlob.UploadTextAsync(TestData);
|
||||
appendBlob = appendBlobContainer.GetAppendBlobReference("blob3");
|
||||
await appendBlob.UploadTextAsync(TestData);
|
||||
}
|
||||
|
||||
public IHost Host
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public JobHost JobHost
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public INameResolver NameResolver => Host.Services.GetService<INameResolver>();
|
||||
|
||||
public string HostId => Host.Services.GetService<IHostIdProvider>().GetHostIdAsync(CancellationToken.None).Result;
|
||||
|
||||
public CloudStorageAccount StorageAccount
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public CloudBlobContainer BlobContainer
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public CloudBlobContainer OutputBlobContainer
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public async Task VerifyLockState(string lockId, LeaseState state, LeaseStatus status)
|
||||
{
|
||||
CloudBlobClient blobClient = StorageAccount.CreateCloudBlobClient();
|
||||
var container = blobClient.GetContainerReference("azure-webjobs-hosts");
|
||||
string blobName = string.Format("locks/{0}/{1}", HostId, lockId);
|
||||
var lockBlob = container.GetBlockBlobReference(blobName);
|
||||
|
||||
Assert.True(await lockBlob.ExistsAsync());
|
||||
await lockBlob.FetchAttributesAsync();
|
||||
|
||||
Assert.Equal(state, lockBlob.Properties.LeaseState);
|
||||
Assert.Equal(status, lockBlob.Properties.LeaseStatus);
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await Host.StopAsync();
|
||||
|
||||
// $$$ reenalbe this
|
||||
VerifyLockState("WebJobs.Internal.Blobs.Listener", LeaseState.Available, LeaseStatus.Unlocked).Wait();
|
||||
|
||||
CloudBlobClient blobClient = StorageAccount.CreateCloudBlobClient();
|
||||
foreach (var testContainer in (await blobClient.ListContainersSegmentedAsync(TestArtifactPrefix, null)).Results)
|
||||
{
|
||||
await testContainer.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TestPoco
|
||||
{
|
||||
public string A { get; set; }
|
||||
|
||||
public string B { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,12 +3,12 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Azure.Storage.Blobs;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Scale;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -46,18 +46,17 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
_loggerProvider = new TestLoggerProvider();
|
||||
_loggerFactory.AddProvider(_loggerProvider);
|
||||
|
||||
IConfiguration configuration = new ConfigurationBuilder().AddEnvironmentVariables().Build();
|
||||
_mockHostIdProvider = new Mock<IHostIdProvider>(MockBehavior.Strict);
|
||||
_mockHostIdProvider.Setup(p => p.GetHostIdAsync(CancellationToken.None)).ReturnsAsync(TestHostId);
|
||||
|
||||
_repository = new BlobStorageConcurrencyStatusRepository(configuration, _mockHostIdProvider.Object, _loggerFactory);
|
||||
_repository = new BlobStorageConcurrencyStatusRepository(_mockHostIdProvider.Object, _loggerFactory, TestHelpers.GetTestAzureBlobStorageProvider());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetContainerAsync_ReturnsExpectedContainer()
|
||||
public async Task GetContainerClientAsync_ReturnsExpectedContainer()
|
||||
{
|
||||
CloudBlobContainer container = await _repository.GetContainerAsync(CancellationToken.None);
|
||||
Assert.Equal(HostContainerNames.Hosts, container.Name);
|
||||
BlobContainerClient blobContainerClient = await _repository.GetContainerClientAsync(CancellationToken.None);
|
||||
Assert.Equal(HostContainerNames.Hosts, blobContainerClient.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -74,33 +73,41 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
await DeleteTestBlobsAsync();
|
||||
|
||||
var path = await _repository.GetBlobPathAsync(CancellationToken.None);
|
||||
CloudBlobContainer container = await _repository.GetContainerAsync(CancellationToken.None);
|
||||
CloudBlockBlob blob = container.GetBlockBlobReference(path);
|
||||
bool exists = await blob.ExistsAsync();
|
||||
BlobContainerClient blobContainerClient = await _repository.GetContainerClientAsync(CancellationToken.None);
|
||||
BlobClient blobClient = blobContainerClient.GetBlobClient(path);
|
||||
bool exists = await blobClient.ExistsAsync();
|
||||
Assert.False(exists);
|
||||
|
||||
await _repository.WriteAsync(_testSnapshot, CancellationToken.None);
|
||||
|
||||
exists = await blob.ExistsAsync();
|
||||
exists = await blobClient.ExistsAsync();
|
||||
Assert.True(exists);
|
||||
|
||||
var content = await blob.DownloadTextAsync();
|
||||
string content = await blobClient.DownloadTextAsync();
|
||||
|
||||
var result = JsonConvert.DeserializeObject<HostConcurrencySnapshot>(content);
|
||||
|
||||
Assert.True(_testSnapshot.Equals(result));
|
||||
|
||||
// upload again and ensure the existing blob is replaced
|
||||
_testSnapshot.NumberOfCores += 2;
|
||||
await _repository.WriteAsync(_testSnapshot, CancellationToken.None);
|
||||
content = await blobClient.DownloadTextAsync();
|
||||
result = JsonConvert.DeserializeObject<HostConcurrencySnapshot>(content);
|
||||
Assert.Equal(_testSnapshot.NumberOfCores, result.NumberOfCores);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsyncAsync_ReadsExpectedBlob()
|
||||
public async Task ReadAsync_ReadsExpectedBlob()
|
||||
{
|
||||
await DeleteTestBlobsAsync();
|
||||
|
||||
string path = await _repository.GetBlobPathAsync(CancellationToken.None);
|
||||
CloudBlobContainer container = await _repository.GetContainerAsync(CancellationToken.None);
|
||||
CloudBlockBlob blob = container.GetBlockBlobReference(path);
|
||||
BlobContainerClient blobContainerClient = await _repository.GetContainerClientAsync(CancellationToken.None);
|
||||
BlobClient blobClient = blobContainerClient.GetBlobClient(path);
|
||||
|
||||
string content = JsonConvert.SerializeObject(_testSnapshot);
|
||||
await blob.UploadTextAsync(content);
|
||||
await blobClient.UploadTextAsync(content, overwrite: true);
|
||||
|
||||
var result = await _repository.ReadAsync(CancellationToken.None);
|
||||
|
||||
|
@ -108,7 +115,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsyncAsync_NoSnapshot_ReturnsNull()
|
||||
public async Task ReadAsync_NoSnapshot_ReturnsNull()
|
||||
{
|
||||
await DeleteTestBlobsAsync();
|
||||
|
||||
|
@ -121,9 +128,12 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
public async Task NoStorageConnection_HandledGracefully()
|
||||
{
|
||||
IConfiguration configuration = new ConfigurationBuilder().Build();
|
||||
var localRepository = new BlobStorageConcurrencyStatusRepository(configuration, _mockHostIdProvider.Object, _loggerFactory);
|
||||
var mockBlobStorageProvider = new Mock<IAzureBlobStorageProvider>(MockBehavior.Strict);
|
||||
BlobContainerClient blobContainerClient = null;
|
||||
mockBlobStorageProvider.Setup(p => p.TryCreateHostingBlobContainerClient(out blobContainerClient)).Returns(false);
|
||||
var localRepository = new BlobStorageConcurrencyStatusRepository(_mockHostIdProvider.Object, _loggerFactory, mockBlobStorageProvider.Object);
|
||||
|
||||
var container = await localRepository.GetContainerAsync(CancellationToken.None);
|
||||
var container = await localRepository.GetContainerClientAsync(CancellationToken.None);
|
||||
Assert.Null(container);
|
||||
|
||||
await localRepository.WriteAsync(new HostConcurrencySnapshot(), CancellationToken.None);
|
||||
|
@ -134,12 +144,13 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
|
||||
private async Task DeleteTestBlobsAsync()
|
||||
{
|
||||
CloudBlobContainer container = await _repository.GetContainerAsync(CancellationToken.None);
|
||||
var blobs = container.ListBlobs($"concurrency/{TestHostId}", useFlatBlobListing: true);
|
||||
foreach (var blob in blobs.Cast<CloudBlockBlob>())
|
||||
BlobContainerClient blobContainerClient = await _repository.GetContainerClientAsync(CancellationToken.None);
|
||||
var blobItems = blobContainerClient.GetBlobsByHierarchyAsync(prefix: $"concurrency/{TestHostId}");
|
||||
await foreach (var blob in blobItems)
|
||||
{
|
||||
await blob.DeleteAsync();
|
||||
BlobClient blobClient = blobContainerClient.GetBlobClient(blob.Blob.Name);
|
||||
await blobClient.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,304 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
||||
{
|
||||
public class BlobTriggerEndToEndTests : IDisposable
|
||||
{
|
||||
private const string TestArtifactPrefix = "e2etests";
|
||||
|
||||
private const string SingleTriggerContainerName = TestArtifactPrefix + "singletrigger-%rnd%";
|
||||
private const string PoisonTestContainerName = TestArtifactPrefix + "poison-%rnd%";
|
||||
private const string TestBlobName = "test";
|
||||
|
||||
private const string BlobChainContainerName = TestArtifactPrefix + "blobchain-%rnd%";
|
||||
private const string BlobChainTriggerBlobName = "blob";
|
||||
private const string BlobChainTriggerBlobPath = BlobChainContainerName + "/" + BlobChainTriggerBlobName;
|
||||
private const string BlobChainCommittedQueueName = "committed";
|
||||
private const string BlobChainIntermediateBlobPath = BlobChainContainerName + "/" + "blob.middle";
|
||||
private const string BlobChainOutputBlobName = "blob.out";
|
||||
private const string BlobChainOutputBlobPath = BlobChainContainerName + "/" + BlobChainOutputBlobName;
|
||||
|
||||
private readonly CloudBlobContainer _testContainer;
|
||||
private readonly CloudStorageAccount _storageAccount;
|
||||
private readonly RandomNameResolver _nameResolver;
|
||||
|
||||
private static object _syncLock = new object();
|
||||
|
||||
public BlobTriggerEndToEndTests()
|
||||
{
|
||||
_nameResolver = new RandomNameResolver();
|
||||
|
||||
// pull from a default host
|
||||
var host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
})
|
||||
.Build();
|
||||
var provider = host.Services.GetService<StorageAccountProvider>();
|
||||
_storageAccount = provider.GetHost().SdkObject;
|
||||
CloudBlobClient blobClient = _storageAccount.CreateCloudBlobClient();
|
||||
_testContainer = blobClient.GetContainerReference(_nameResolver.ResolveInString(SingleTriggerContainerName));
|
||||
Assert.False(_testContainer.ExistsAsync().Result);
|
||||
_testContainer.CreateAsync().Wait();
|
||||
}
|
||||
|
||||
public IHostBuilder NewBuilder<TProgram>(TProgram program, Action<IWebJobsBuilder> configure = null)
|
||||
{
|
||||
var activator = new FakeActivator();
|
||||
activator.Add(program);
|
||||
|
||||
return new HostBuilder()
|
||||
.ConfigureDefaultTestHost<TProgram>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
configure?.Invoke(b);
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<IJobActivator>(activator);
|
||||
services.AddSingleton<INameResolver>(_nameResolver);
|
||||
});
|
||||
}
|
||||
|
||||
public class Poison_Program
|
||||
{
|
||||
public List<string> _poisonBlobMessages = new List<string>();
|
||||
|
||||
public void BlobProcessorPrimary(
|
||||
[BlobTrigger(PoisonTestContainerName + "/{name}")] string input)
|
||||
{
|
||||
// throw to generate a poison blob message
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
// process the poison queue for the primary storage account
|
||||
public void PoisonBlobQueueProcessorPrimary(
|
||||
[QueueTrigger("webjobs-blobtrigger-poison")] JObject message)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
string blobName = (string)message["BlobName"];
|
||||
_poisonBlobMessages.Add(blobName);
|
||||
}
|
||||
}
|
||||
|
||||
public void BlobProcessorSecondary(
|
||||
[StorageAccount("SecondaryStorage")]
|
||||
[BlobTrigger(PoisonTestContainerName + "/{name}")] string input)
|
||||
{
|
||||
// throw to generate a poison blob message
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
// process the poison queue for the secondary storage account
|
||||
public void PoisonBlobQueueProcessorSecondary(
|
||||
[StorageAccount("SecondaryStorage")]
|
||||
[QueueTrigger("webjobs-blobtrigger-poison")] JObject message)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
string blobName = (string)message["BlobName"];
|
||||
_poisonBlobMessages.Add(blobName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class BlobGetsProcessedOnlyOnce_SingleHost_Program
|
||||
{
|
||||
public int _timesProcessed;
|
||||
public ManualResetEvent _completedEvent;
|
||||
|
||||
public void SingleBlobTrigger(
|
||||
[BlobTrigger(SingleTriggerContainerName + "/{name}")] string sleepTimeInSeconds)
|
||||
{
|
||||
Interlocked.Increment(ref _timesProcessed);
|
||||
|
||||
int sleepTime = int.Parse(sleepTimeInSeconds) * 1000;
|
||||
Thread.Sleep(sleepTime);
|
||||
|
||||
_completedEvent.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public class BlobChainTest_Program
|
||||
{
|
||||
public ManualResetEvent _completedEvent;
|
||||
|
||||
public void BlobChainStepOne(
|
||||
[BlobTrigger(BlobChainTriggerBlobPath)] TextReader input,
|
||||
[Blob(BlobChainIntermediateBlobPath)] TextWriter output)
|
||||
{
|
||||
string content = input.ReadToEnd();
|
||||
output.Write(content);
|
||||
}
|
||||
|
||||
public void BlobChainStepTwo(
|
||||
[BlobTrigger(BlobChainIntermediateBlobPath)] TextReader input,
|
||||
[Blob(BlobChainOutputBlobPath)] TextWriter output,
|
||||
[Queue(BlobChainCommittedQueueName)] out string committed)
|
||||
{
|
||||
string content = input.ReadToEnd();
|
||||
output.Write("*" + content + "*");
|
||||
committed = String.Empty;
|
||||
}
|
||||
|
||||
public void BlobChainStepThree([QueueTrigger(BlobChainCommittedQueueName)] string ignore)
|
||||
{
|
||||
_completedEvent.Set();
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("AzureWebJobsSecondaryStorage")]
|
||||
[InlineData("AzureWebJobsStorage")]
|
||||
public async Task PoisonMessage_CreatedInCorrectStorageAccount(string storageAccountSetting)
|
||||
{
|
||||
var storageAccount = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable(storageAccountSetting));
|
||||
var blobClient = storageAccount.CreateCloudBlobClient();
|
||||
var containerName = _nameResolver.ResolveInString(PoisonTestContainerName);
|
||||
var container = blobClient.GetContainerReference(containerName);
|
||||
await container.CreateAsync();
|
||||
|
||||
var blobName = Guid.NewGuid().ToString();
|
||||
CloudBlockBlob blob = container.GetBlockBlobReference(blobName);
|
||||
await blob.UploadTextAsync("0");
|
||||
|
||||
var prog = new Poison_Program();
|
||||
var host = NewBuilder(prog).Build();
|
||||
|
||||
using (host)
|
||||
{
|
||||
host.Start();
|
||||
|
||||
// wait for the poison message to be handled
|
||||
await TestHelpers.Await(() =>
|
||||
{
|
||||
return prog._poisonBlobMessages.Contains(blobName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BlobGetsProcessedOnlyOnce_SingleHost()
|
||||
{
|
||||
CloudBlockBlob blob = _testContainer.GetBlockBlobReference(TestBlobName);
|
||||
await blob.UploadTextAsync("0");
|
||||
|
||||
int timeToProcess;
|
||||
|
||||
var prog = new BlobGetsProcessedOnlyOnce_SingleHost_Program();
|
||||
|
||||
// make sure they both have the same id
|
||||
var host = NewBuilder(prog, builder => builder.UseHostId(Guid.NewGuid().ToString("N")))
|
||||
.Build();
|
||||
|
||||
// Process the blob first
|
||||
using (prog._completedEvent = new ManualResetEvent(initialState: false))
|
||||
using (host)
|
||||
{
|
||||
DateTime startTime = DateTime.Now;
|
||||
|
||||
host.Start();
|
||||
Assert.True(prog._completedEvent.WaitOne(TimeSpan.FromSeconds(60)));
|
||||
|
||||
timeToProcess = (int)(DateTime.Now - startTime).TotalMilliseconds;
|
||||
|
||||
Assert.Equal(1, prog._timesProcessed);
|
||||
|
||||
string[] loggerOutputLines = host.GetTestLoggerProvider().GetAllLogMessages()
|
||||
.Where(p => p.FormattedMessage != null)
|
||||
.SelectMany(p => p.FormattedMessage.Split(Environment.NewLine, StringSplitOptions.None))
|
||||
.ToArray();
|
||||
|
||||
var executions = loggerOutputLines.Where(p => p.Contains("Executing"));
|
||||
Assert.Single(executions);
|
||||
Assert.StartsWith(string.Format("Executing 'BlobGetsProcessedOnlyOnce_SingleHost_Program.SingleBlobTrigger' (Reason='New blob detected: {0}/{1}', Id=", blob.Container.Name, blob.Name), executions.Single());
|
||||
|
||||
await host.StopAsync();
|
||||
|
||||
|
||||
// Can't restart
|
||||
Assert.Throws<InvalidOperationException>(() => host.Start());
|
||||
}
|
||||
|
||||
Assert.Equal(1, prog._timesProcessed);
|
||||
} // host
|
||||
|
||||
[Fact]
|
||||
public async Task BlobChainTest()
|
||||
{
|
||||
// write the initial trigger blob to start the chain
|
||||
var blobClient = _storageAccount.CreateCloudBlobClient();
|
||||
var container = blobClient.GetContainerReference(_nameResolver.ResolveInString(BlobChainContainerName));
|
||||
await container.CreateIfNotExistsAsync();
|
||||
CloudBlockBlob blob = container.GetBlockBlobReference(BlobChainTriggerBlobName);
|
||||
await blob.UploadTextAsync("0");
|
||||
|
||||
var prog = new BlobChainTest_Program();
|
||||
var host = NewBuilder(prog).Build();
|
||||
|
||||
using (prog._completedEvent = new ManualResetEvent(initialState: false))
|
||||
using (host)
|
||||
{
|
||||
host.Start();
|
||||
Assert.True(prog._completedEvent.WaitOne(TimeSpan.FromSeconds(60)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BlobGetsProcessedOnlyOnce_MultipleHosts()
|
||||
{
|
||||
await _testContainer
|
||||
.GetBlockBlobReference(TestBlobName)
|
||||
.UploadTextAsync("10");
|
||||
|
||||
var prog = new BlobGetsProcessedOnlyOnce_SingleHost_Program();
|
||||
|
||||
|
||||
string hostId = Guid.NewGuid().ToString("N");
|
||||
var host1 = NewBuilder(prog, builder=>builder.UseHostId(hostId))
|
||||
.Build();
|
||||
var host2 = NewBuilder(prog, builder => builder.UseHostId(hostId))
|
||||
.Build();
|
||||
|
||||
using (prog._completedEvent = new ManualResetEvent(initialState: false))
|
||||
using (host1)
|
||||
using (host2)
|
||||
{
|
||||
host1.Start();
|
||||
host2.Start();
|
||||
|
||||
Assert.True(prog._completedEvent.WaitOne(TimeSpan.FromSeconds(60)));
|
||||
}
|
||||
|
||||
Assert.Equal(1, prog._timesProcessed);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CloudBlobClient blobClient = _storageAccount.CreateCloudBlobClient();
|
||||
foreach (var testContainer in blobClient.ListContainersSegmentedAsync(TestArtifactPrefix, null).Result.Results)
|
||||
{
|
||||
testContainer.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,10 +8,8 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Azure.WebJobs.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Azure.Storage.Queue;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
@ -28,9 +26,6 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
private static ITestOutputHelper _output;
|
||||
private static Stopwatch _stopwatch = new Stopwatch();
|
||||
|
||||
private CloudQueue _sharedQueue;
|
||||
private CloudQueue _poisonQueue;
|
||||
|
||||
// Each test should set this up; it will be used during cleanup.
|
||||
private IHost _host;
|
||||
|
||||
|
@ -75,11 +70,12 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
{
|
||||
// each test will have a unique hostId so that consecutive test run will not be affected by clean up code
|
||||
b.UseHostId(Guid.NewGuid().ToString("N"))
|
||||
.AddAzureStorage()
|
||||
// Necessary for Queue bindings
|
||||
.AddAzureStorageQueues()
|
||||
.AddExtension<DispatchQueueTestConfig>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
|
||||
{
|
||||
_funcInvocation = new ConcurrentStringSet();
|
||||
|
||||
|
@ -107,11 +103,12 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
{
|
||||
// each test will have a unique hostId so that consecutive test run will not be affected by clean up code
|
||||
b.UseHostId(Guid.NewGuid().ToString("N"))
|
||||
.AddAzureStorage()
|
||||
// Necessary for Queue bindings
|
||||
.AddAzureStorageQueues()
|
||||
.AddExtension<DispatchQueueTestConfig>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
|
||||
{
|
||||
_funcInvocation = new ConcurrentStringSet();
|
||||
|
||||
|
@ -136,11 +133,12 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
{
|
||||
// each test will have a different hostId
|
||||
// and therefore a different sharedQueue and poisonQueue
|
||||
CloudQueueClient client = _host.GetStorageAccount().CreateCloudQueueClient();
|
||||
_sharedQueue = client.GetQueueReference("azure-webjobs-shared-" + _host.Services.GetService<IHostIdProvider>().GetHostIdAsync(CancellationToken.None).Result);
|
||||
_poisonQueue = client.GetQueueReference("azure-webjobs-poison-" + _host.Services.GetService<IHostIdProvider>().GetHostIdAsync(CancellationToken.None).Result);
|
||||
_sharedQueue.DeleteIfExistsAsync().Wait();
|
||||
_poisonQueue.DeleteIfExistsAsync().Wait();
|
||||
var queueServiceClient = TestHelpers.GetTestQueueServiceClient();
|
||||
var sharedQueue = queueServiceClient?.GetQueueClient("azure-webjobs-shared-" + _host.Services.GetService<IHostIdProvider>().GetHostIdAsync(CancellationToken.None).Result);
|
||||
var poisonQueue = queueServiceClient?.GetQueueClient("azure-webjobs-poison-" + _host.Services.GetService<IHostIdProvider>().GetHostIdAsync(CancellationToken.None).Result);
|
||||
|
||||
sharedQueue?.DeleteIfExistsAsync().Wait();
|
||||
poisonQueue?.DeleteIfExistsAsync().Wait();
|
||||
|
||||
_host.Dispose();
|
||||
}
|
||||
|
|
|
@ -279,10 +279,9 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
.ConfigureDefaultTestHost<TProg>(b =>
|
||||
{
|
||||
b.UseHostId(TestHostId)
|
||||
.AddAzureStorage()
|
||||
.AddExtension<TestTriggerAttributeBindingProvider>();
|
||||
|
||||
RuntimeStorageWebJobsBuilderExtensions.AddAzureStorageCoreServices(b);
|
||||
b.AddAzureStorageCoreServices();
|
||||
|
||||
b.Services.AddOptions<ConcurrencyOptions>().Configure(options =>
|
||||
{
|
||||
|
@ -298,9 +297,6 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
}
|
||||
});
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
})
|
||||
.ConfigureLogging((context, b) =>
|
||||
{
|
||||
b.SetMinimumLevel(LogLevel.Information);
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Sas;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Moq;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
||||
|
@ -31,26 +31,16 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
}
|
||||
|
||||
// Create a real Blob Container Sas URI
|
||||
var account1 = CloudStorageAccount.Parse(acs);
|
||||
var client = account1.CreateCloudBlobClient();
|
||||
var container = client.GetContainerReference(containerName);
|
||||
await container.CreateIfNotExistsAsync(); // this will throw if acs is bad
|
||||
var blobServiceClient = new BlobServiceClient(acs);
|
||||
var containerClient = blobServiceClient.GetBlobContainerClient(containerName);
|
||||
await containerClient.CreateIfNotExistsAsync(); // this will throw if acs is bad;
|
||||
var fakeSasUri = containerClient.GenerateSasUri(BlobContainerSasPermissions.Read | BlobContainerSasPermissions.Write | BlobContainerSasPermissions.List, DateTime.UtcNow.AddDays(10));
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var sig = container.GetSharedAccessSignature(new SharedAccessBlobPolicy
|
||||
{
|
||||
Permissions = SharedAccessBlobPermissions.Read | SharedAccessBlobPermissions.Write | SharedAccessBlobPermissions.List,
|
||||
SharedAccessStartTime = now.AddDays(-10),
|
||||
SharedAccessExpiryTime = now.AddDays(10)
|
||||
});
|
||||
|
||||
var fakeSasUri = container.Uri + sig;
|
||||
var prog = new BasicProg();
|
||||
|
||||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost(prog, b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
RuntimeStorageWebJobsBuilderExtensions.AddAzureStorageCoreServices(b);
|
||||
})
|
||||
.ConfigureAppConfiguration(config =>
|
||||
|
@ -58,16 +48,19 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
// Set env to the SAS container and clear out all other storage.
|
||||
config.AddInMemoryCollection(new Dictionary<string, string>()
|
||||
{
|
||||
{ "AzureWebJobs:InternalSasBlobContainer", fakeSasUri },
|
||||
{ "AzureWebJobs:InternalSasBlobContainer", fakeSasUri.ToString() },
|
||||
{ "AzureWebJobsStorage", null },
|
||||
{ "AzureWebJobsDashboard", null }
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
var internalOptions = host.Services.GetService<DistributedLockManagerContainerProvider>();
|
||||
var internalOptions = host.Services.GetService<IOptions<JobHostInternalStorageOptions>>();
|
||||
Assert.NotNull(internalOptions);
|
||||
Assert.Equal(container.Name, internalOptions.InternalContainer.Name);
|
||||
Assert.Equal(fakeSasUri.ToString(), internalOptions.Value.InternalSasBlobContainer);
|
||||
|
||||
Assert.True(host.Services.GetService<IAzureBlobStorageProvider>().TryCreateHostingBlobContainerClient(out BlobContainerClient actualContainer));
|
||||
Assert.Equal(containerClient.Name, actualContainer.Name);
|
||||
|
||||
await host.GetJobHost().CallAsync(nameof(BasicProg.Foo));
|
||||
|
||||
|
|
|
@ -1,385 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Microsoft.Azure.Storage.Queue;
|
||||
using Microsoft.Azure.Cosmos.Table;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using CloudStorageAccount = Microsoft.Azure.Storage.CloudStorageAccount;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
||||
{
|
||||
public class MultipleStorageAccountsEndToEndTests : IClassFixture<MultipleStorageAccountsEndToEndTests.TestFixture>
|
||||
{
|
||||
private const string TestArtifactPrefix = "e2etestmultiaccount";
|
||||
private const string Input = TestArtifactPrefix + "-input-%rnd%";
|
||||
private const string Output = TestArtifactPrefix + "-output-%rnd%";
|
||||
private const string InputTableName = TestArtifactPrefix + "tableinput%rnd%";
|
||||
private const string OutputTableName = TestArtifactPrefix + "tableinput%rnd%";
|
||||
private const string TestData = "TestData";
|
||||
private const string Secondary = "SecondaryStorage";
|
||||
private static CloudStorageAccount primaryAccountResult;
|
||||
private static CloudStorageAccount secondaryAccountResult;
|
||||
private readonly TestFixture _fixture;
|
||||
|
||||
public MultipleStorageAccountsEndToEndTests(TestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BlobToBlob_DifferentAccounts_PrimaryToSecondary_Succeeds()
|
||||
{
|
||||
CloudBlockBlob resultBlob = null;
|
||||
|
||||
await TestHelpers.Await(async () =>
|
||||
{
|
||||
resultBlob = (CloudBlockBlob)(await _fixture.OutputContainer2.ListBlobsSegmentedAsync("blob1", null)).Results.SingleOrDefault();
|
||||
return resultBlob != null;
|
||||
});
|
||||
|
||||
string data = await resultBlob.DownloadTextAsync();
|
||||
Assert.Equal("blob1", resultBlob.Name);
|
||||
Assert.Equal(TestData, data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BlobToBlob_DifferentAccounts_SecondaryToPrimary_Succeeds()
|
||||
{
|
||||
CloudBlockBlob resultBlob = null;
|
||||
|
||||
await TestHelpers.Await(async () =>
|
||||
{
|
||||
resultBlob = (CloudBlockBlob)(await _fixture.OutputContainer1.ListBlobsSegmentedAsync(null)).Results.SingleOrDefault();
|
||||
return resultBlob != null;
|
||||
});
|
||||
|
||||
string data = await resultBlob.DownloadTextAsync();
|
||||
Assert.Equal("blob2", resultBlob.Name);
|
||||
Assert.Equal(TestData, data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueueToQueue_DifferentAccounts_PrimaryToSecondary_Succeeds()
|
||||
{
|
||||
CloudQueueMessage resultMessage = null;
|
||||
|
||||
await TestHelpers.Await(async () =>
|
||||
{
|
||||
resultMessage = await _fixture.OutputQueue2.GetMessageAsync();
|
||||
return resultMessage != null;
|
||||
});
|
||||
|
||||
Assert.Equal(TestData, resultMessage.AsString);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("QueueToBlob_DifferentAccounts_PrimaryToSecondary_NameResolver")]
|
||||
[InlineData("QueueToBlob_DifferentAccounts_PrimaryToSecondary_FullSettingName")]
|
||||
public async Task QueueToBlob_DifferentAccounts_PrimaryToSecondary_NameResolver_Succeeds(string methodName)
|
||||
{
|
||||
var method = typeof(MultipleStorageAccountsEndToEndTests).GetMethod(methodName);
|
||||
string name = Guid.NewGuid().ToString();
|
||||
JObject jObject = new JObject
|
||||
{
|
||||
{ "Name", name },
|
||||
{ "Value", TestData }
|
||||
};
|
||||
await _fixture.JobHost.CallAsync(method, new { input = jObject.ToString() });
|
||||
|
||||
var blobReference = await _fixture.OutputContainer2.GetBlobReferenceFromServerAsync(name);
|
||||
await TestHelpers.Await(() => blobReference.ExistsAsync());
|
||||
|
||||
string data;
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
await blobReference.DownloadToStreamAsync(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
using (var reader = new StreamReader(memoryStream))
|
||||
{
|
||||
data = reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
Assert.Equal(TestData, data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueueToQueue_DifferentAccounts_SecondaryToPrimary_Succeeds()
|
||||
{
|
||||
CloudQueueMessage resultMessage = null;
|
||||
|
||||
await TestHelpers.Await(async () =>
|
||||
{
|
||||
resultMessage = await _fixture.OutputQueue1.GetMessageAsync();
|
||||
return resultMessage != null;
|
||||
});
|
||||
|
||||
Assert.Equal(TestData, resultMessage.AsString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Table_PrimaryAndSecondary_Succeeds()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(MultipleStorageAccountsEndToEndTests).GetMethod("Table_PrimaryAndSecondary"));
|
||||
|
||||
TestTableEntity entity1 = null;
|
||||
TestTableEntity entity2 = null;
|
||||
await TestHelpers.Await(async () =>
|
||||
{
|
||||
TableResult result = await _fixture.OutputTable1.ExecuteAsync(TableOperation.Retrieve<TestTableEntity>("test", "test"));
|
||||
if (result != null)
|
||||
{
|
||||
entity1 = (TestTableEntity)result.Result;
|
||||
}
|
||||
|
||||
result = await _fixture.OutputTable2.ExecuteAsync(TableOperation.Retrieve<TestTableEntity>("test", "test"));
|
||||
if (result != null)
|
||||
{
|
||||
entity2 = (TestTableEntity)result.Result;
|
||||
}
|
||||
|
||||
return entity1 != null && entity2 != null;
|
||||
});
|
||||
|
||||
Assert.Equal(TestData, entity1.Text);
|
||||
Assert.Equal(TestData, entity2.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CloudStorageAccount_PrimaryAndSecondary_Succeeds()
|
||||
{
|
||||
await _fixture.JobHost.CallAsync(typeof(MultipleStorageAccountsEndToEndTests).GetMethod(nameof(MultipleStorageAccountsEndToEndTests.BindToCloudStorageAccount)));
|
||||
|
||||
Assert.Equal(_fixture.Account1.SdkObject.Credentials.AccountName, primaryAccountResult.Credentials.AccountName);
|
||||
Assert.Equal(_fixture.Account2.SdkObject.Credentials.AccountName, secondaryAccountResult.Credentials.AccountName);
|
||||
}
|
||||
|
||||
public static void BlobToBlob_DifferentAccounts_PrimaryToSecondary(
|
||||
[BlobTrigger(Input + "/{name}")] string input,
|
||||
[Blob(Output + "/{name}", Connection = Secondary)] out string output)
|
||||
{
|
||||
output = input;
|
||||
}
|
||||
|
||||
public static void BlobToBlob_DifferentAccounts_SecondaryToPrimary(
|
||||
[BlobTrigger(Input + "/{name}", Connection = Secondary)] string input,
|
||||
[Blob(Output + "/{name}")] out string output)
|
||||
{
|
||||
output = input;
|
||||
}
|
||||
|
||||
|
||||
public static void QueueToQueue_DifferentAccounts_PrimaryToSecondary(
|
||||
[QueueTrigger(Input)] string input,
|
||||
[Queue(Output, Connection = Secondary)] out string output)
|
||||
{
|
||||
output = input;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void QueueToBlob_DifferentAccounts_PrimaryToSecondary_NameResolver(
|
||||
[QueueTrigger("test")] Message input,
|
||||
[Blob(Output + "/{Name}", Connection = "%test_account%")] out string output)
|
||||
{
|
||||
output = input.Value;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void QueueToBlob_DifferentAccounts_PrimaryToSecondary_FullSettingName(
|
||||
[QueueTrigger("test")] Message input,
|
||||
[Blob(Output + "/{Name}", Connection = "AzureWebJobsSecondaryStorage")] out string output)
|
||||
{
|
||||
output = input.Value;
|
||||
}
|
||||
|
||||
public static void QueueToQueue_DifferentAccounts_SecondaryToPrimary(
|
||||
[QueueTrigger(Input, Connection = Secondary)] string input,
|
||||
[Queue(Output)] out string output)
|
||||
{
|
||||
output = input;
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public async static Task Table_PrimaryAndSecondary(
|
||||
[Table(OutputTableName)] CloudTable primaryOutput,
|
||||
[Table(OutputTableName, Connection = Secondary)] CloudTable secondaryOutput)
|
||||
{
|
||||
TestTableEntity entity = new TestTableEntity
|
||||
{
|
||||
PartitionKey = "test",
|
||||
RowKey = "test",
|
||||
Text = TestData
|
||||
};
|
||||
await primaryOutput.ExecuteAsync(TableOperation.InsertOrReplace(entity));
|
||||
await secondaryOutput.ExecuteAsync(TableOperation.InsertOrReplace(entity));
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
public static void BindToCloudStorageAccount(
|
||||
CloudStorageAccount primary,
|
||||
[StorageAccount(Secondary)] CloudStorageAccount secondary)
|
||||
{
|
||||
primaryAccountResult = primary;
|
||||
secondaryAccountResult = secondary;
|
||||
}
|
||||
|
||||
private class TestNameResolver : RandomNameResolver
|
||||
{
|
||||
public override string Resolve(string name)
|
||||
{
|
||||
if (name == "test_account")
|
||||
{
|
||||
return "SecondaryStorage";
|
||||
}
|
||||
return base.Resolve(name);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestFixture : IAsyncLifetime
|
||||
{
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
RandomNameResolver nameResolver = new TestNameResolver();
|
||||
|
||||
Host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<MultipleStorageAccountsEndToEndTests>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<INameResolver>(nameResolver);
|
||||
})
|
||||
.Build();
|
||||
|
||||
Account1 = Host.GetStorageAccount();
|
||||
var config = Host.Services.GetService<IConfiguration>();
|
||||
string secondaryConnectionString = config[$"AzureWebJobs{Secondary}"];
|
||||
Account2 = StorageAccount.NewFromConnectionString(secondaryConnectionString);
|
||||
|
||||
await CleanContainersAsync();
|
||||
|
||||
CloudBlobClient blobClient1 = Account1.CreateCloudBlobClient();
|
||||
string inputName = nameResolver.ResolveInString(Input);
|
||||
CloudBlobContainer inputContainer1 = blobClient1.GetContainerReference(inputName);
|
||||
await inputContainer1.CreateIfNotExistsAsync();
|
||||
string outputName = nameResolver.ResolveWholeString(Output);
|
||||
OutputContainer1 = blobClient1.GetContainerReference(outputName);
|
||||
await OutputContainer1.CreateIfNotExistsAsync();
|
||||
|
||||
CloudBlobClient blobClient2 = Account2.CreateCloudBlobClient();
|
||||
CloudBlobContainer inputContainer2 = blobClient2.GetContainerReference(inputName);
|
||||
await inputContainer2.CreateIfNotExistsAsync();
|
||||
OutputContainer2 = blobClient2.GetContainerReference(outputName);
|
||||
await OutputContainer2.CreateIfNotExistsAsync();
|
||||
|
||||
CloudQueueClient queueClient1 = Account1.CreateCloudQueueClient();
|
||||
CloudQueue inputQueue1 = queueClient1.GetQueueReference(inputName);
|
||||
await inputQueue1.CreateIfNotExistsAsync();
|
||||
OutputQueue1 = queueClient1.GetQueueReference(outputName);
|
||||
await OutputQueue1.CreateIfNotExistsAsync();
|
||||
|
||||
CloudQueueClient queueClient2 = Account2.CreateCloudQueueClient();
|
||||
CloudQueue inputQueue2 = queueClient2.GetQueueReference(inputName);
|
||||
await inputQueue2.CreateIfNotExistsAsync();
|
||||
OutputQueue2 = queueClient2.GetQueueReference(outputName);
|
||||
await OutputQueue2.CreateIfNotExistsAsync();
|
||||
|
||||
CloudTableClient tableClient1 = Account1.CreateCloudTableClient();
|
||||
string outputTableName = nameResolver.ResolveWholeString(OutputTableName);
|
||||
OutputTable1 = tableClient1.GetTableReference(outputTableName);
|
||||
OutputTable2 = Account2.CreateCloudTableClient().GetTableReference(outputTableName);
|
||||
|
||||
// upload some test blobs to the input containers of both storage accounts
|
||||
CloudBlockBlob blob = inputContainer1.GetBlockBlobReference("blob1");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
blob = inputContainer2.GetBlockBlobReference("blob2");
|
||||
await blob.UploadTextAsync(TestData);
|
||||
|
||||
// upload some test queue messages to the input queues of both storage accounts
|
||||
await inputQueue1.AddMessageAsync(new CloudQueueMessage(TestData));
|
||||
await inputQueue2.AddMessageAsync(new CloudQueueMessage(TestData));
|
||||
|
||||
Host.Start();
|
||||
}
|
||||
|
||||
public JobHost JobHost => Host.GetJobHost();
|
||||
|
||||
public IHost Host
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public StorageAccount Account1 { get; private set; }
|
||||
public StorageAccount Account2 { get; private set; }
|
||||
|
||||
public CloudBlobContainer OutputContainer1 { get; private set; }
|
||||
|
||||
public CloudBlobContainer OutputContainer2 { get; private set; }
|
||||
|
||||
public CloudQueue OutputQueue1 { get; private set; }
|
||||
|
||||
public CloudQueue OutputQueue2 { get; private set; }
|
||||
|
||||
public CloudTable OutputTable1 { get; private set; }
|
||||
|
||||
public CloudTable OutputTable2 { get; private set; }
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await Host.StopAsync();
|
||||
|
||||
await CleanContainersAsync();
|
||||
}
|
||||
|
||||
private async Task CleanContainersAsync()
|
||||
{
|
||||
await Clean(Account1);
|
||||
await Clean(Account2);
|
||||
}
|
||||
}
|
||||
|
||||
private async static Task Clean(StorageAccount account)
|
||||
{
|
||||
CloudBlobClient blobClient = account.CreateCloudBlobClient();
|
||||
foreach (var testContainer in (await blobClient.ListContainersSegmentedAsync(TestArtifactPrefix, null)).Results)
|
||||
{
|
||||
await testContainer.DeleteAsync();
|
||||
}
|
||||
|
||||
CloudTableClient tableClient = account.CreateCloudTableClient();
|
||||
foreach (var table in await tableClient.ListTablesSegmentedAsync(TestArtifactPrefix, null))
|
||||
{
|
||||
await table.DeleteAsync();
|
||||
}
|
||||
|
||||
CloudQueueClient queueClient = account.CreateCloudQueueClient();
|
||||
foreach (var queue in (await queueClient.ListQueuesSegmentedAsync(TestArtifactPrefix, null)).Results)
|
||||
{
|
||||
await queue.DeleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestTableEntity : TableEntity
|
||||
{
|
||||
public string Text { get; set; }
|
||||
}
|
||||
|
||||
public class Message
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,21 +2,18 @@
|
|||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Azure.Storage.Queues;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Azure.Storage.Queue;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
||||
{
|
||||
public class ParallelExecutionTests : IDisposable
|
||||
public class ParallelExecutionTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestArtifactPrefix = "e2etestparallelqueue";
|
||||
private const string TestQueueName = TestArtifactPrefix + "-%rnd%";
|
||||
|
@ -30,7 +27,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
private static int _maxSimultaneouslyRunningFunctions;
|
||||
|
||||
private static ManualResetEvent _allMessagesProcessed;
|
||||
private CloudQueueClient _queueClient;
|
||||
private QueueServiceClient _queueServiceClient;
|
||||
|
||||
public static async Task ParallelQueueTrigger([QueueTrigger(TestQueueName)] int sleepTimeInSeconds)
|
||||
{
|
||||
|
@ -81,32 +78,35 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
}
|
||||
else
|
||||
{
|
||||
Environment.SetEnvironmentVariable(Constants.AzureWebsiteSku, "dynamic");
|
||||
Environment.SetEnvironmentVariable(Constants.AzureWebsiteSku, "Dynamic");
|
||||
}
|
||||
|
||||
RandomNameResolver nameResolver = new RandomNameResolver();
|
||||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<ParallelExecutionTests>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
b.AddAzureStorageQueues(o =>
|
||||
{
|
||||
o.BatchSize = batchSize;
|
||||
o.MessageEncoding = QueueMessageEncoding.None;
|
||||
o.NewBatchThreshold = isDynamicSku ? (batchSize / 2) : (batchSize / 2) * processorCount; // To get around static isDynamicSku variable in T2 storage extensions
|
||||
});
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<INameResolver>(nameResolver);
|
||||
services.Configure<QueuesOptions>(o => o.BatchSize = batchSize);
|
||||
})
|
||||
.ConfigureAppConfiguration(c => c.AddEnvironmentVariables())
|
||||
.Build();
|
||||
|
||||
StorageAccount storageAccount = host.GetStorageAccount();
|
||||
_queueClient = storageAccount.CreateCloudQueueClient();
|
||||
CloudQueue queue = _queueClient.GetQueueReference(nameResolver.ResolveInString(TestQueueName));
|
||||
|
||||
await queue.CreateIfNotExistsAsync();
|
||||
_queueServiceClient = TestHelpers.GetTestQueueServiceClient();
|
||||
var queueClient = _queueServiceClient.GetQueueClient(nameResolver.ResolveInString(TestQueueName));
|
||||
await queueClient.CreateIfNotExistsAsync();
|
||||
|
||||
for (int i = 0; i < _numberOfQueueMessages; i++)
|
||||
{
|
||||
int sleepTimeInSeconds = i % 2 == 0 ? 5 : 1;
|
||||
await queue.AddMessageAsync(new CloudQueueMessage(sleepTimeInSeconds.ToString()));
|
||||
await queueClient.SendMessageAsync(sleepTimeInSeconds.ToString());
|
||||
}
|
||||
|
||||
using (_allMessagesProcessed = new ManualResetEvent(initialState: false))
|
||||
|
@ -130,15 +130,22 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
if (_queueClient != null)
|
||||
await CleanQueuesAsync();
|
||||
}
|
||||
|
||||
private async Task CleanQueuesAsync()
|
||||
{
|
||||
if (_queueServiceClient != null)
|
||||
{
|
||||
foreach (var testQueue in _queueClient.ListQueuesSegmentedAsync(TestArtifactPrefix, null).Result.Results)
|
||||
await foreach (var testQueue in _queueServiceClient.GetQueuesAsync(prefix: TestArtifactPrefix))
|
||||
{
|
||||
testQueue.DeleteAsync().Wait();
|
||||
await _queueServiceClient.DeleteQueueAsync(testQueue.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,13 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Azure.Storage.Queues;
|
||||
using Microsoft.Azure.WebJobs.Description;
|
||||
using Microsoft.Azure.WebJobs.Host.Bindings;
|
||||
using Microsoft.Azure.WebJobs.Host.Config;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Listeners;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
|
@ -19,9 +22,6 @@ using Microsoft.Extensions.Configuration;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Microsoft.Azure.Storage.Queue;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
|
@ -38,8 +38,9 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
private Random _rand = new Random(314159);
|
||||
|
||||
private static RandomNameResolver _resolver = new TestNameResolver();
|
||||
private static CloudBlobDirectory _lockDirectory;
|
||||
private static CloudBlobDirectory _secondaryLockDirectory;
|
||||
|
||||
private static BlobContainerClient _lockDirectoryContainerClient;
|
||||
private static BlobContainerClient _secondaryLockDirectoryContainerClient;
|
||||
|
||||
public SingletonEndToEndTests()
|
||||
{
|
||||
|
@ -254,13 +255,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
[Fact]
|
||||
public async Task SingletonFunction_StorageAccountOverride()
|
||||
{
|
||||
IHost host = CreateTestJobHost<TestJobs1>(1, (hostBuilder) =>
|
||||
{
|
||||
hostBuilder.ConfigureServices((services) =>
|
||||
{
|
||||
services.AddSingleton<IDistributedLockManager, CustomLockManager>();
|
||||
});
|
||||
});
|
||||
IHost host = CreateTestJobHost<TestJobs1>(1);
|
||||
await host.StartAsync();
|
||||
|
||||
MethodInfo method = typeof(TestJobs1).GetMethod(nameof(TestJobs1.SingletonJob_StorageAccountOverride));
|
||||
|
@ -272,25 +267,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
|
||||
// make sure the lease blob was only created in the secondary account
|
||||
await VerifyLeaseDoesNotExistAsync(method, SingletonScope.Function, null);
|
||||
await VerifyLeaseState(method, SingletonScope.Function, null, LeaseState.Available, LeaseStatus.Unlocked, directory: _secondaryLockDirectory);
|
||||
}
|
||||
|
||||
// Allow a host to override container resolution.
|
||||
class CustomLockManager : StorageBaseDistributedLockManager
|
||||
{
|
||||
private readonly StorageAccountProvider _storage;
|
||||
|
||||
public CustomLockManager(ILoggerFactory logger, StorageAccountProvider storage) : base(logger)
|
||||
{
|
||||
_storage = storage;
|
||||
}
|
||||
protected override CloudBlobContainer GetContainer(string accountName)
|
||||
{
|
||||
var account = _storage.Get(accountName);
|
||||
var client = account.CreateCloudBlobClient();
|
||||
var container = client.GetContainerReference(StorageBaseDistributedLockManager.DefaultContainerName);
|
||||
return container;
|
||||
}
|
||||
await VerifyLeaseState(method, SingletonScope.Function, null, LeaseState.Available, LeaseStatus.Unlocked, directory: _secondaryLockDirectoryContainerClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -338,24 +315,25 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
host.Dispose();
|
||||
}
|
||||
|
||||
internal async static Task VerifyLeaseState(MethodInfo method, SingletonScope scope, string scopeId, LeaseState leaseState, LeaseStatus leaseStatus, CloudBlobDirectory directory = null)
|
||||
internal async static Task VerifyLeaseState(MethodInfo method, SingletonScope scope, string scopeId, LeaseState leaseState, LeaseStatus leaseStatus, BlobContainerClient directory = null)
|
||||
{
|
||||
string lockId = FormatLockId(method, scope, scopeId);
|
||||
|
||||
CloudBlobDirectory lockDirectory = directory ?? _lockDirectory;
|
||||
CloudBlockBlob lockBlob = lockDirectory.GetBlockBlobReference(lockId);
|
||||
await lockBlob.FetchAttributesAsync();
|
||||
Assert.Equal(leaseState, lockBlob.Properties.LeaseState);
|
||||
Assert.Equal(leaseStatus, lockBlob.Properties.LeaseStatus);
|
||||
BlobContainerClient lockDirectory = directory ?? _lockDirectoryContainerClient;
|
||||
BlobClient blobClient = lockDirectory.GetBlobClient(lockId);
|
||||
|
||||
var properties = await blobClient.GetPropertiesAsync();
|
||||
Assert.Equal(leaseState, properties.Value.LeaseState);
|
||||
Assert.Equal(leaseStatus, properties.Value.LeaseStatus);
|
||||
}
|
||||
|
||||
internal static async Task VerifyLeaseDoesNotExistAsync(MethodInfo method, SingletonScope scope, string scopeId, CloudBlobDirectory directory = null)
|
||||
internal static async Task VerifyLeaseDoesNotExistAsync(MethodInfo method, SingletonScope scope, string scopeId, BlobContainerClient directory = null)
|
||||
{
|
||||
string lockId = FormatLockId(method, scope, scopeId);
|
||||
|
||||
CloudBlobDirectory lockDirectory = directory ?? _lockDirectory;
|
||||
CloudBlockBlob lockBlob = lockDirectory.GetBlockBlobReference(lockId);
|
||||
Assert.False(await lockBlob.ExistsAsync());
|
||||
BlobContainerClient lockDirectory = directory ?? _lockDirectoryContainerClient;
|
||||
BlobClient blobClient = lockDirectory.GetBlobClient(lockId);
|
||||
Assert.False(await blobClient.ExistsAsync());
|
||||
}
|
||||
|
||||
private static string FormatLockId(MethodInfo method, SingletonScope scope, string scopeId)
|
||||
|
@ -375,7 +353,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
lockId += scopeId;
|
||||
}
|
||||
|
||||
lockId = string.Format("{0}/{1}", TestHostId, lockId);
|
||||
lockId = string.Format("locks/{0}/{1}", TestHostId, lockId);
|
||||
|
||||
return lockId;
|
||||
}
|
||||
|
@ -402,7 +380,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
SingletonScope.Function,
|
||||
null,
|
||||
LeaseState.Leased, LeaseStatus.Locked,
|
||||
_secondaryLockDirectory);
|
||||
_secondaryLockDirectoryContainerClient);
|
||||
|
||||
await Task.Delay(50);
|
||||
}
|
||||
|
@ -636,10 +614,14 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
var hostBuilder = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<TProg>(b =>
|
||||
{
|
||||
b.UseHostId(TestHostId)
|
||||
.AddAzureStorage()
|
||||
.AddExtension<TestTriggerAttributeBindingProvider>();
|
||||
RuntimeStorageWebJobsBuilderExtensions.AddAzureStorageCoreServices(b);
|
||||
b.UseHostId(TestHostId);
|
||||
|
||||
// For Queue and Blob Triggers
|
||||
b.AddAzureStorageBlobs();
|
||||
b.AddAzureStorageQueues();
|
||||
|
||||
b.AddAzureStorageCoreServices();
|
||||
b.AddExtension<TestTriggerAttributeBindingProvider>();
|
||||
})
|
||||
.ConfigureTestLogger()
|
||||
.ConfigureServices(services =>
|
||||
|
@ -807,82 +789,73 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
|||
}
|
||||
}
|
||||
|
||||
private class TestFixture : IDisposable
|
||||
private class TestFixture : IAsyncLifetime
|
||||
{
|
||||
private Lazy<StorageAccount> _storageAccountLazy = new Lazy<StorageAccount>(GetStorageAccount);
|
||||
public BlobServiceClient BlobServiceClient;
|
||||
public BlobServiceClient SecondaryBlobServiceClient;
|
||||
public QueueServiceClient QueueServiceClient;
|
||||
|
||||
public StorageAccount StorageAccount => _storageAccountLazy.Value;
|
||||
|
||||
public TestFixture()
|
||||
{
|
||||
CloudBlobClient blobClient = StorageAccount.CreateCloudBlobClient();
|
||||
_lockDirectory = blobClient.GetContainerReference("azure-webjobs-hosts").GetDirectoryReference("locks");
|
||||
|
||||
string secondaryConnectionString = GetConfigurationString($"AzureWebJobs{Secondary}");
|
||||
var secondaryStorageAccount = CloudStorageAccount.Parse(secondaryConnectionString);
|
||||
blobClient = secondaryStorageAccount.CreateCloudBlobClient();
|
||||
_secondaryLockDirectory = blobClient.GetContainerReference("azure-webjobs-hosts").GetDirectoryReference("locks");
|
||||
}
|
||||
|
||||
private static StorageAccount GetStorageAccount()
|
||||
{
|
||||
// Create a default host since we know that's where the account
|
||||
// is coming from
|
||||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
b.AddAzureStorageCoreServices();
|
||||
})
|
||||
.Build();
|
||||
|
||||
return host.GetStorageAccount();
|
||||
BlobServiceClient = TestHelpers.GetTestBlobServiceClient();
|
||||
QueueServiceClient = TestHelpers.GetTestQueueServiceClient();
|
||||
|
||||
_lockDirectoryContainerClient = BlobServiceClient.GetBlobContainerClient(HostContainerNames.Hosts);
|
||||
|
||||
SecondaryBlobServiceClient = TestHelpers.GetTestBlobServiceClient($"AzureWebJobs{Secondary}");
|
||||
_secondaryLockDirectoryContainerClient = SecondaryBlobServiceClient.GetBlobContainerClient(HostContainerNames.Hosts);
|
||||
}
|
||||
|
||||
private static string GetConfigurationString(string key)
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
// Create a default host since we know that's where the account
|
||||
// is coming from
|
||||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
|
||||
await CleanBlobsAsync(BlobServiceClient);
|
||||
await CleanBlobsAsync(SecondaryBlobServiceClient);
|
||||
await CleanQueuesAsync();
|
||||
}
|
||||
|
||||
private async Task CleanBlobsAsync(BlobServiceClient blobServiceClient)
|
||||
{
|
||||
if (blobServiceClient != null)
|
||||
{
|
||||
var containerClient = blobServiceClient.GetBlobContainerClient(HostContainerNames.Hosts);
|
||||
if (await containerClient.ExistsAsync())
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
})
|
||||
.Build();
|
||||
|
||||
return host.Services.GetService<IConfiguration>()[key];
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (StorageAccount != null)
|
||||
{
|
||||
Clean().Wait();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Clean()
|
||||
{
|
||||
CloudQueueClient queueClient = StorageAccount.CreateCloudQueueClient();
|
||||
var queuesResult = await queueClient.ListQueuesSegmentedAsync(TestArtifactsPrefix, null);
|
||||
foreach (var queue in queuesResult.Results)
|
||||
{
|
||||
await queue.DeleteAsync();
|
||||
}
|
||||
|
||||
CloudBlobClient blobClient = StorageAccount.CreateCloudBlobClient();
|
||||
CloudBlobContainer hostContainer = blobClient.GetContainerReference("azure-webjobs-hosts");
|
||||
var blobs = await hostContainer.ListBlobsSegmentedAsync(string.Format("locks/{0}", TestHostId), useFlatBlobListing: true, blobListingDetails: BlobListingDetails.None,
|
||||
maxResults: null, currentToken: null, options: null, operationContext: null);
|
||||
foreach (CloudBlockBlob lockBlob in blobs.Results)
|
||||
{
|
||||
try
|
||||
{
|
||||
await lockBlob.DeleteAsync();
|
||||
await foreach (var testBlob in containerClient.GetBlobsAsync(prefix: string.Format("locks/{0}", TestHostId)))
|
||||
{
|
||||
try
|
||||
{
|
||||
await containerClient.DeleteBlobAsync(testBlob.Name);
|
||||
}
|
||||
catch (RequestFailedException)
|
||||
{
|
||||
// best effort - might fail if there is an active
|
||||
// lease on the blob
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (StorageException)
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanQueuesAsync()
|
||||
{
|
||||
if (QueueServiceClient != null)
|
||||
{
|
||||
await foreach (var testQueue in QueueServiceClient.GetQueuesAsync(prefix: TestArtifactsPrefix))
|
||||
{
|
||||
// best effort - might fail if there is an active
|
||||
// lease on the blob
|
||||
await QueueServiceClient.DeleteQueueAsync(testQueue.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.EndToEndTests
|
||||
{
|
||||
static class TestExtensions
|
||||
{
|
||||
public static StorageAccount GetStorageAccount(this IHost host)
|
||||
{
|
||||
var provider = host.Services.GetRequiredService<StorageAccountProvider>(); // $$$ ok?
|
||||
return provider.GetHost();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -34,10 +34,10 @@
|
|||
<PackageReference Include="Moq" Version="4.7.145" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="5.0.0-beta.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Extensions.Storage\WebJobs.Extensions.Storage.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Host.Storage\WebJobs.Host.Storage.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Host\WebJobs.Host.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Logging.ApplicationInsights\WebJobs.Logging.ApplicationInsights.csproj" />
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.Storage.Blobs;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
||||
{
|
||||
public class AzureStorageProviderTests
|
||||
{
|
||||
private const string StorageConnection = "AzureWebJobsStorage";
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionStringSectionUsed()
|
||||
{
|
||||
var testConfiguration = new ConfigurationBuilder()
|
||||
.AddEnvironmentVariables()
|
||||
.AddTestSettings()
|
||||
.Build();
|
||||
var testData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "ConnectionStrings:AzureWebJobsStorage", testConfiguration.GetWebJobsConnectionString(StorageConnection) },
|
||||
{ "AzureWebJobsStorage", "" }
|
||||
};
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(testData)
|
||||
.Build();
|
||||
|
||||
var azureStorageProvider = GetAzureStorageProvider(configuration);
|
||||
Assert.True(azureStorageProvider.TryCreateHostingBlobContainerClient(out BlobContainerClient container));
|
||||
await VerifyContainerClientAvailable(container);
|
||||
|
||||
Assert.True(azureStorageProvider.TryCreateBlobServiceClientFromConnection(ConnectionStringNames.Storage, out BlobServiceClient blobServiceClient));
|
||||
await VerifyBlobServiceClientAvailable(blobServiceClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoConnectionThrowsException()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.Build();
|
||||
|
||||
var azureStorageProvider = GetAzureStorageProvider(configuration);
|
||||
Assert.False(azureStorageProvider.TryCreateHostingBlobContainerClient(out _));
|
||||
Assert.False(azureStorageProvider.TryCreateBlobServiceClientFromConnection(ConnectionStringNames.Storage, out _));
|
||||
}
|
||||
|
||||
private async Task VerifyBlobServiceClientAvailable(BlobServiceClient client)
|
||||
{
|
||||
try
|
||||
{
|
||||
var propertiesResponse = await client.GetPropertiesAsync();
|
||||
Assert.True(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Assert.False(true, $"Could not establish connection to BlobService. {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task VerifyContainerClientAvailable(BlobContainerClient client)
|
||||
{
|
||||
try
|
||||
{
|
||||
var propertiesResponse = await client.GetPropertiesAsync();
|
||||
Assert.True(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Assert.False(true, $"Could not establish connection to BlobService. {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private static IAzureBlobStorageProvider GetAzureStorageProvider(IConfiguration configuration, JobHostInternalStorageOptions storageOptions = null)
|
||||
{
|
||||
IHost tempHost = new HostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
// Override configuration
|
||||
services.AddSingleton(configuration);
|
||||
services.AddAzureStorageCoreServices();
|
||||
|
||||
if (storageOptions != null)
|
||||
{
|
||||
services.AddTransient<IOptions<JobHostInternalStorageOptions>>(s => new OptionsWrapper<JobHostInternalStorageOptions>(storageOptions));
|
||||
}
|
||||
}).Build();
|
||||
|
||||
var azureStorageProvider = tempHost.Services.GetRequiredService<IAzureBlobStorageProvider>();
|
||||
return azureStorageProvider;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.Storage.Blobs;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.Azure;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests whether the StorageClientProvider can properly create a client and send a request
|
||||
/// </summary>
|
||||
public class BlobServiceClientProviderTests
|
||||
{
|
||||
private const string StorageConnection = "AzureWebJobsStorage";
|
||||
|
||||
private readonly BlobServiceClientProvider _blobServiceClientProvider;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public BlobServiceClientProviderTests()
|
||||
{
|
||||
_configuration = new ConfigurationBuilder()
|
||||
.AddEnvironmentVariables()
|
||||
.AddTestSettings()
|
||||
.Build();
|
||||
|
||||
IHost tempHost = new HostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddAzureStorageCoreServices();
|
||||
}).Build();
|
||||
|
||||
var componentFactory = tempHost.Services.GetRequiredService<AzureComponentFactory>();
|
||||
var logForwarder = tempHost.Services.GetRequiredService<AzureEventSourceLogForwarder>();
|
||||
_blobServiceClientProvider = new BlobServiceClientProvider(componentFactory, logForwarder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_ReturnsValidClient()
|
||||
{
|
||||
var client = _blobServiceClientProvider.Create(StorageConnection, _configuration);
|
||||
Assert.NotNull(client);
|
||||
await VerifyServiceAvailable(client);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_NameResolverReturnsValidClient()
|
||||
{
|
||||
var resolver = new DefaultNameResolver(_configuration);
|
||||
|
||||
var client = _blobServiceClientProvider.Create(StorageConnection, resolver, _configuration);
|
||||
await VerifyServiceAvailable(client);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_ConnectionStringSectionChecked()
|
||||
{
|
||||
var testData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "ConnectionStrings:AzureWebJobsStorage", Environment.GetEnvironmentVariable(StorageConnection) },
|
||||
{ "AzureWebJobsStorage", "" }
|
||||
};
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(testData)
|
||||
.AddTestSettings()
|
||||
.Build();
|
||||
|
||||
var client = _blobServiceClientProvider.Create(StorageConnection, configuration);
|
||||
await VerifyServiceAvailable(client);
|
||||
}
|
||||
|
||||
|
||||
private async Task VerifyServiceAvailable(BlobServiceClient client)
|
||||
{
|
||||
try
|
||||
{
|
||||
var propertiesResponse = await client.GetPropertiesAsync();
|
||||
Assert.True(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Assert.False(true, $"Could not establish connection to BlobService. {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,20 +2,12 @@
|
|||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Host.Bindings;
|
||||
using Microsoft.Azure.WebJobs.Host.Indexers;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Azure.WebJobs.Host.Triggers;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.UnitTests.Bindings.Data
|
||||
|
@ -41,7 +33,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Bindings.Data
|
|||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
Assert.NotNull(method); // Guard
|
||||
|
||||
FunctionIndexer indexer = FunctionIndexerFactory.Create(CloudStorageAccount.DevelopmentStorageAccount);
|
||||
FunctionIndexer indexer = FunctionIndexerFactory.Create();
|
||||
IFunctionIndexCollector stubIndex = new Mock<IFunctionIndexCollector>().Object;
|
||||
|
||||
// Act & Assert
|
||||
|
|
|
@ -31,12 +31,12 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
_jobActivator = new DefaultJobActivator(serviceProvider);
|
||||
}
|
||||
|
||||
public void Call(StorageAccount account, Type programType, MethodInfo method,
|
||||
public void Call(Type programType, MethodInfo method,
|
||||
IDictionary<string, object> arguments, params Type[] cloudBlobStreamBinderTypes)
|
||||
{
|
||||
// Arrange
|
||||
TaskCompletionSource<object> backgroundTaskSource = new TaskCompletionSource<object>();
|
||||
IHost host = CreateConfigurationForManualCompletion<object>(account,
|
||||
IHost host = CreateConfigurationForManualCompletion<object>(
|
||||
programType, backgroundTaskSource, cloudBlobStreamBinderTypes: cloudBlobStreamBinderTypes);
|
||||
Task backgroundTask = backgroundTaskSource.Task;
|
||||
|
||||
|
@ -71,12 +71,12 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
}
|
||||
|
||||
// Stops running the host as soon as the program marks the task as completed.
|
||||
public TResult Call<TResult>(StorageAccount account, Type programType, MethodInfo method,
|
||||
public TResult Call<TResult>(Type programType, MethodInfo method,
|
||||
IDictionary<string, object> arguments, Action<TaskCompletionSource<TResult>> setTaskSource)
|
||||
{
|
||||
// Arrange
|
||||
TaskCompletionSource<TResult> taskSource = new TaskCompletionSource<TResult>();
|
||||
var serviceProvider = CreateConfigurationForManualCompletion<TResult>(account, programType,
|
||||
var serviceProvider = CreateConfigurationForManualCompletion<TResult>(programType,
|
||||
taskSource);
|
||||
Task<TResult> task = taskSource.Task;
|
||||
setTaskSource.Invoke(taskSource);
|
||||
|
@ -117,12 +117,12 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
}
|
||||
|
||||
// Stops running the host as soon as the program marks the task as completed.
|
||||
public Exception CallFailure(StorageAccount account, Type programType, MethodInfo method,
|
||||
public Exception CallFailure(Type programType, MethodInfo method,
|
||||
IDictionary<string, object> arguments)
|
||||
{
|
||||
// Arrange
|
||||
TaskCompletionSource<object> backgroundTaskSource = new TaskCompletionSource<object>();
|
||||
var host = CreateConfigurationForCallFailure(account, programType,
|
||||
var host = CreateConfigurationForCallFailure(programType,
|
||||
backgroundTaskSource);
|
||||
Task backgroundTask = backgroundTaskSource.Task;
|
||||
|
||||
|
@ -147,76 +147,70 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
public IHost CreateConfigurationForCallFailure(StorageAccount storageAccount,
|
||||
Type programType, TaskCompletionSource<object> taskSource)
|
||||
public IHost CreateConfigurationForCallFailure(Type programType, TaskCompletionSource<object> taskSource)
|
||||
{
|
||||
return CreateConfiguration<object>(storageAccount, programType, taskSource, new NullFunctionInstanceLogger());
|
||||
return CreateConfiguration<object>(programType, taskSource, new NullFunctionInstanceLogger());
|
||||
}
|
||||
|
||||
private IHost CreateConfigurationForInstanceFailure(StorageAccount storageAccount,
|
||||
Type programType, TaskCompletionSource<Exception> taskSource)
|
||||
private IHost CreateConfigurationForInstanceFailure(Type programType, TaskCompletionSource<Exception> taskSource)
|
||||
{
|
||||
return CreateConfiguration<Exception>(storageAccount, programType, taskSource,
|
||||
return CreateConfiguration<Exception>(programType, taskSource,
|
||||
new ExpectInstanceFailureTaskFunctionInstanceLogger(taskSource));
|
||||
}
|
||||
|
||||
public IHost CreateConfigurationForInstanceSuccess(StorageAccount storageAccount,
|
||||
public IHost CreateConfigurationForInstanceSuccess(
|
||||
Type programType, TaskCompletionSource<object> taskSource, IExtensionRegistry extensions = null)
|
||||
{
|
||||
return CreateConfiguration<object>(storageAccount, programType, taskSource,
|
||||
return CreateConfiguration<object>(programType, taskSource,
|
||||
new ExpectInstanceSuccessTaskFunctionInstanceLogger(taskSource), extensions);
|
||||
}
|
||||
|
||||
public IHost CreateConfigurationForManualCompletion<TResult>(StorageAccount storageAccount,
|
||||
public IHost CreateConfigurationForManualCompletion<TResult>(
|
||||
Type programType, TaskCompletionSource<TResult> taskSource, IExtensionRegistry extensions = null, params Type[] cloudBlobStreamBinderTypes)
|
||||
{
|
||||
IEnumerable<string> ignoreFailureFunctionIds = null;
|
||||
return CreateConfigurationForManualCompletion<TResult>(storageAccount, programType, taskSource,
|
||||
return CreateConfigurationForManualCompletion<TResult>(programType, taskSource,
|
||||
ignoreFailureFunctionIds, extensions, cloudBlobStreamBinderTypes);
|
||||
}
|
||||
|
||||
private IHost CreateConfigurationForManualCompletion<TResult>(
|
||||
StorageAccount storageAccount, Type programType, TaskCompletionSource<TResult> taskSource,
|
||||
Type programType, TaskCompletionSource<TResult> taskSource,
|
||||
IEnumerable<string> ignoreFailureFunctions, IExtensionRegistry extensions = null, params Type[] cloudBlobStreamBinderTypes)
|
||||
{
|
||||
return CreateConfigurationForManualCompletion<TResult>(storageAccount, programType,
|
||||
return CreateConfigurationForManualCompletion<TResult>(programType,
|
||||
_jobActivator, taskSource, ignoreFailureFunctions, extensions);
|
||||
}
|
||||
|
||||
private static IHost CreateConfigurationForManualCompletion<TResult>(
|
||||
StorageAccount storageAccount, Type programType, IJobActivator activator,
|
||||
Type programType, IJobActivator activator,
|
||||
TaskCompletionSource<TResult> taskSource, IEnumerable<string> ignoreFailureFunctions, IExtensionRegistry extensions = null)
|
||||
{
|
||||
return CreateConfiguration<TResult>(storageAccount, programType, activator, taskSource,
|
||||
return CreateConfiguration<TResult>(programType, activator, taskSource,
|
||||
new ExpectManualCompletionFunctionInstanceLogger<TResult>(taskSource, false, ignoreFailureFunctions), extensions);
|
||||
}
|
||||
|
||||
private IHost CreateConfiguration<TResult>(StorageAccount storageAccount, Type programType, TaskCompletionSource<TResult> taskSource,
|
||||
private IHost CreateConfiguration<TResult>(Type programType, TaskCompletionSource<TResult> taskSource,
|
||||
IFunctionInstanceLogger functionInstanceLogger, IExtensionRegistry extensions = null)
|
||||
{
|
||||
return CreateConfiguration<TResult>(storageAccount, programType,
|
||||
return CreateConfiguration<TResult>(programType,
|
||||
_jobActivator, taskSource, functionInstanceLogger, extensions);
|
||||
}
|
||||
|
||||
private static IHost CreateConfiguration<TResult>(StorageAccount storageAccount, Type programType,
|
||||
private static IHost CreateConfiguration<TResult>(Type programType,
|
||||
IJobActivator activator, TaskCompletionSource<TResult> taskSource,
|
||||
IFunctionInstanceLogger functionInstanceLogger, IExtensionRegistry extensions = null)
|
||||
{
|
||||
StorageAccountProvider storageAccountProvider = null; // new FakeStorageAccountProvider(storageAccount); $$$
|
||||
|
||||
IWebJobsExceptionHandler exceptionHandler = new TaskBackgroundExceptionHandler<TResult>(taskSource);
|
||||
|
||||
return new HostBuilder()
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.UseHostId(Guid.NewGuid().ToString("N"))
|
||||
.AddAzureStorage();
|
||||
b.UseHostId(Guid.NewGuid().ToString("N"));
|
||||
}, programType)
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
// services.AddSingleton<IOptionsFactory<JobHostQueuesOptions>, FakeQueuesOptionsFactory>(); $$$ ???
|
||||
|
||||
services.AddSingletonIfNotNull(storageAccountProvider);
|
||||
services.AddSingletonIfNotNull(activator);
|
||||
services.AddSingletonIfNotNull(exceptionHandler);
|
||||
services.AddSingletonIfNotNull(extensions);
|
||||
|
@ -229,43 +223,42 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
}
|
||||
|
||||
// Stops running the host as soon as the first function logs completion.
|
||||
public void RunTrigger(StorageAccount account, Type programType, IExtensionRegistry extensions = null)
|
||||
public void RunTrigger(Type programType, IExtensionRegistry extensions = null)
|
||||
{
|
||||
// Arrange
|
||||
TaskCompletionSource<object> taskSource = new TaskCompletionSource<object>();
|
||||
var serviceProvider = CreateConfigurationForInstanceSuccess(account, programType, taskSource, extensions);
|
||||
var serviceProvider = CreateConfigurationForInstanceSuccess(programType, taskSource, extensions);
|
||||
|
||||
// Act & Assert
|
||||
RunTrigger<object>(serviceProvider, taskSource.Task);
|
||||
}
|
||||
|
||||
// Stops running the host as soon as the program marks the task as completed.
|
||||
public TResult RunTrigger<TResult>(StorageAccount account, Type programType,
|
||||
Action<TaskCompletionSource<TResult>> setTaskSource)
|
||||
public TResult RunTrigger<TResult>(Type programType, Action<TaskCompletionSource<TResult>> setTaskSource)
|
||||
{
|
||||
return RunTrigger<TResult>(account, programType, setTaskSource, _jobActivator,
|
||||
return RunTrigger<TResult>(programType, setTaskSource, _jobActivator,
|
||||
ignoreFailureFunctions: null);
|
||||
}
|
||||
|
||||
public TResult RunTrigger<TResult>(StorageAccount account, Type programType,
|
||||
public TResult RunTrigger<TResult>(Type programType,
|
||||
IJobActivator activator, Action<TaskCompletionSource<TResult>> setTaskSource)
|
||||
{
|
||||
return RunTrigger<TResult>(account, programType, setTaskSource, activator, ignoreFailureFunctions: null);
|
||||
return RunTrigger<TResult>(programType, setTaskSource, activator, ignoreFailureFunctions: null);
|
||||
}
|
||||
|
||||
public TResult RunTrigger<TResult>(StorageAccount account, Type programType,
|
||||
public TResult RunTrigger<TResult>(Type programType,
|
||||
Action<TaskCompletionSource<TResult>> setTaskSource, IEnumerable<string> ignoreFailureFunctions)
|
||||
{
|
||||
return RunTrigger<TResult>(account, programType, setTaskSource, _jobActivator, ignoreFailureFunctions);
|
||||
return RunTrigger<TResult>(programType, setTaskSource, _jobActivator, ignoreFailureFunctions);
|
||||
}
|
||||
|
||||
public TResult RunTrigger<TResult>(StorageAccount account, Type programType,
|
||||
public TResult RunTrigger<TResult>(Type programType,
|
||||
Action<TaskCompletionSource<TResult>> setTaskSource, IJobActivator activator,
|
||||
IEnumerable<string> ignoreFailureFunctions)
|
||||
{
|
||||
// Arrange
|
||||
TaskCompletionSource<TResult> taskSource = new TaskCompletionSource<TResult>();
|
||||
var serviceProvider = CreateConfigurationForManualCompletion<TResult>(account, programType,
|
||||
var serviceProvider = CreateConfigurationForManualCompletion<TResult>(programType,
|
||||
activator, taskSource, ignoreFailureFunctions);
|
||||
Task<TResult> task = taskSource.Task;
|
||||
setTaskSource.Invoke(taskSource);
|
||||
|
@ -316,11 +309,11 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
}
|
||||
|
||||
// Stops running the host as soon as the first function logs completion.
|
||||
public Exception RunTriggerFailure(StorageAccount account, Type programType)
|
||||
public Exception RunTriggerFailure(Type programType)
|
||||
{
|
||||
// Arrange
|
||||
TaskCompletionSource<Exception> taskSource = new TaskCompletionSource<Exception>();
|
||||
IHost host = CreateConfigurationForInstanceFailure(account, programType, taskSource);
|
||||
IHost host = CreateConfigurationForInstanceFailure(programType, taskSource);
|
||||
|
||||
// The task for failed function invocation (should complete successfully with a non-null exception).
|
||||
Task<Exception> task = taskSource.Task;
|
||||
|
@ -349,12 +342,12 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
}
|
||||
|
||||
// Stops running the host as soon as the program marks the task as completed.
|
||||
public Exception RunTriggerFailure<TResult>(StorageAccount account, Type programType,
|
||||
public Exception RunTriggerFailure<TResult>(Type programType,
|
||||
Action<TaskCompletionSource<TResult>> setTaskSource)
|
||||
{
|
||||
// Arrange
|
||||
TaskCompletionSource<Exception> failureTaskSource = new TaskCompletionSource<Exception>();
|
||||
IHost host = CreateConfigurationForInstanceFailure(account, programType, failureTaskSource);
|
||||
IHost host = CreateConfigurationForInstanceFailure(programType, failureTaskSource);
|
||||
TaskCompletionSource<TResult> successTaskSource = new TaskCompletionSource<TResult>();
|
||||
|
||||
// The task for failed function invocation (should complete successfully with an exception).
|
||||
|
@ -403,4 +396,4 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -147,10 +147,7 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
const int N = 5;
|
||||
|
||||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<ILoggerFunctions>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
})
|
||||
.ConfigureDefaultTestHost<ILoggerFunctions>()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<IAsyncCollector<FunctionInstanceLogEntry>>(mockAggregator.Object);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.Bindings;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Indexers;
|
||||
|
@ -16,33 +15,22 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
{
|
||||
internal static class FunctionIndexerFactory
|
||||
{
|
||||
public class FakeStorageAccountProvider : StorageAccountProvider
|
||||
{
|
||||
public FakeStorageAccountProvider()
|
||||
: base(null)
|
||||
{
|
||||
}
|
||||
|
||||
public override StorageAccount Get(string name)
|
||||
{
|
||||
return StorageAccount.New(CloudStorageAccount.DevelopmentStorageAccount);
|
||||
}
|
||||
}
|
||||
|
||||
public static FunctionIndexer Create(CloudStorageAccount account = null, INameResolver nameResolver = null,
|
||||
public static FunctionIndexer Create(INameResolver nameResolver = null,
|
||||
IExtensionRegistry extensionRegistry = null, ILoggerFactory loggerFactory = null)
|
||||
{
|
||||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.UseHostId("testhost")
|
||||
.AddAzureStorage()
|
||||
.AddServiceBus();
|
||||
b.UseHostId("testhost");
|
||||
|
||||
// Needed for Blob/Queue triggers and bindings
|
||||
b.AddAzureStorageBlobs();
|
||||
b.AddAzureStorageQueues();
|
||||
|
||||
b.AddServiceBus();
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<StorageAccountProvider>(new FakeStorageAccountProvider());
|
||||
|
||||
if (nameResolver != null)
|
||||
{
|
||||
services.AddSingleton<INameResolver>(nameResolver);
|
||||
|
|
|
@ -43,8 +43,6 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
|||
}
|
||||
}
|
||||
|
||||
private static void BadTableName([Table(@"#")] IDictionary<Tuple<string, string>, object> t) { }
|
||||
|
||||
private static void MultipleQueueParams([QueueTrigger("p123")] int p123, [QueueTrigger("p234")] int p234) { }
|
||||
|
||||
private static void QueueNestedIEnumerable([Queue("myoutputqueue")] ICollection<IEnumerable<Payload>> myoutputqueue) { }
|
||||
|
|
|
@ -9,11 +9,8 @@ using System.Threading;
|
|||
using Microsoft.Azure.WebJobs.Host.Indexers;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Cosmos.Table;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using CloudStorageAccount = Microsoft.Azure.Storage.CloudStorageAccount;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
||||
{
|
||||
|
@ -25,8 +22,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
|||
MethodInfo method = typeof(FunctionIndexerIntegrationTests).GetMethod(methodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
Assert.NotNull(method);
|
||||
|
||||
FunctionIndexer indexer = FunctionIndexerFactory.Create(CloudStorageAccount.DevelopmentStorageAccount,
|
||||
nameResolver);
|
||||
FunctionIndexer indexer = FunctionIndexerFactory.Create(nameResolver);
|
||||
|
||||
Tuple<FunctionDescriptor, IFunctionDefinition> indexEntry = null;
|
||||
Mock<IFunctionIndexCollector> indexMock = new Mock<IFunctionIndexCollector>(MockBehavior.Strict);
|
||||
|
@ -61,7 +57,6 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
|||
Assert.NotNull(descriptor);
|
||||
var parameters = descriptor.Parameters;
|
||||
Assert.Single(parameters);
|
||||
Assert.IsType<BlobParameterDescriptor>(parameters.First());
|
||||
}
|
||||
|
||||
private static void NameResolver([Blob(@"input/%name%")] TextReader inputs) { }
|
||||
|
@ -79,10 +74,6 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
|||
Assert.Single(parameters);
|
||||
ParameterDescriptor firstParameter = parameters.First();
|
||||
Assert.Equal("inputs", firstParameter.Name);
|
||||
Assert.IsType<BlobParameterDescriptor>(firstParameter);
|
||||
BlobParameterDescriptor blobParameter = (BlobParameterDescriptor)firstParameter;
|
||||
Assert.Equal(@"input", blobParameter.ContainerName);
|
||||
Assert.Equal(@"VALUE", blobParameter.BlobName);
|
||||
}
|
||||
|
||||
public static void AutoTrigger1([BlobTrigger(@"daas-test-input/{name}.csv")] TextReader inputs) { }
|
||||
|
@ -95,7 +86,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
|||
Assert.NotNull(func);
|
||||
var parameters = func.Parameters;
|
||||
Assert.Single(parameters);
|
||||
Assert.IsType<BlobTriggerParameterDescriptor>(parameters.First());
|
||||
Assert.Equal("BlobTriggerParameterDescriptor", parameters.First().GetType().Name);
|
||||
}
|
||||
|
||||
[NoAutomaticTrigger]
|
||||
|
@ -132,25 +123,6 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
|||
Assert.Null(entry);
|
||||
}
|
||||
|
||||
private static void Table([Table("TableName")] CloudTable table) { }
|
||||
|
||||
[Fact]
|
||||
public void TestTable()
|
||||
{
|
||||
FunctionDescriptor func = IndexMethod("Table").Item1;
|
||||
|
||||
Assert.NotNull(func);
|
||||
var parameters = func.Parameters;
|
||||
Assert.Single(parameters);
|
||||
|
||||
ParameterDescriptor firstParameter = parameters.First();
|
||||
Assert.NotNull(firstParameter);
|
||||
Assert.Equal("table", firstParameter.Name);
|
||||
Assert.IsType<TableParameterDescriptor>(firstParameter);
|
||||
TableParameterDescriptor typedTableParameter = (TableParameterDescriptor)firstParameter;
|
||||
Assert.Equal("TableName", typedTableParameter.TableName);
|
||||
}
|
||||
|
||||
public static void QueueTrigger([QueueTrigger("inputQueue")] int queueValue) { }
|
||||
|
||||
[Fact]
|
||||
|
@ -163,9 +135,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
|||
Assert.Single(parameters);
|
||||
|
||||
ParameterDescriptor firstParameter = parameters.First();
|
||||
Assert.IsType<QueueTriggerParameterDescriptor>(firstParameter);
|
||||
QueueTriggerParameterDescriptor queueParameter = (QueueTriggerParameterDescriptor)firstParameter;
|
||||
Assert.Equal("inputqueue", queueParameter.QueueName); // queue name gets normalized.
|
||||
Assert.Equal("QueueTriggerParameterDescriptor", firstParameter.GetType().Name);
|
||||
Assert.Equal("queueValue", firstParameter.Name); // parameter name does not.
|
||||
}
|
||||
|
||||
|
@ -185,8 +155,6 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
|||
Assert.Single(parameters);
|
||||
|
||||
ParameterDescriptor firstParameter = parameters.First();
|
||||
QueueParameterDescriptor queueParameter = (QueueParameterDescriptor)firstParameter;
|
||||
Assert.Equal("inputqueue", queueParameter.QueueName); // queue name gets normalized.
|
||||
Assert.Equal("inputQueue", firstParameter.Name); // parameter name does not.
|
||||
}
|
||||
|
||||
|
@ -213,9 +181,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
|||
|
||||
ParameterDescriptor firstParameter = parameters.ElementAt(0);
|
||||
Assert.Equal("input", firstParameter.Name);
|
||||
Assert.IsType<BlobTriggerParameterDescriptor>(firstParameter);
|
||||
BlobTriggerParameterDescriptor blobParameter = (BlobTriggerParameterDescriptor)firstParameter;
|
||||
Assert.Equal("container", blobParameter.ContainerName);
|
||||
Assert.Equal("BlobTriggerParameterDescriptor", firstParameter.GetType().Name);
|
||||
|
||||
ParameterDescriptor secondParameter = parameters.ElementAt(1);
|
||||
Assert.Equal("unbound", secondParameter.Name);
|
||||
|
@ -237,9 +203,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
|||
|
||||
ParameterDescriptor firstParameter = parameters.ElementAt(0);
|
||||
Assert.Equal("input", firstParameter.Name);
|
||||
Assert.IsType<BlobTriggerParameterDescriptor>(firstParameter);
|
||||
BlobTriggerParameterDescriptor blobParameter = (BlobTriggerParameterDescriptor)firstParameter;
|
||||
Assert.Equal("container", blobParameter.ContainerName);
|
||||
Assert.Equal("BlobTriggerParameterDescriptor", firstParameter.GetType().Name);
|
||||
|
||||
ParameterDescriptor secondParameter = parameters.ElementAt(1);
|
||||
Assert.Equal("bound", secondParameter.Name);
|
||||
|
|
|
@ -9,7 +9,6 @@ using System.Reflection;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.ServiceBus.Core;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.WebJobs.Description;
|
||||
using Microsoft.Azure.WebJobs.Host.Bindings;
|
||||
using Microsoft.Azure.WebJobs.Host.Indexers;
|
||||
|
@ -589,7 +588,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers
|
|||
BindingFlags.Static | BindingFlags.Public);
|
||||
Assert.NotNull(method); // Guard
|
||||
|
||||
FunctionIndexer indexer = FunctionIndexerFactory.Create(CloudStorageAccount.DevelopmentStorageAccount);
|
||||
FunctionIndexer indexer = FunctionIndexerFactory.Create();
|
||||
var indexCollector = new TestIndexCollector();
|
||||
|
||||
// Act & Assert
|
||||
|
|
|
@ -140,7 +140,8 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
b.AddAzureStorageBlobs();
|
||||
b.AddAzureStorageQueues();
|
||||
})
|
||||
.Build();
|
||||
|
||||
|
@ -194,48 +195,15 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
|
||||
var queueTriggerAttr = GetAttr<QueueTriggerAttribute>(metadataProvider, new { QueueName = "q1" });
|
||||
Assert.Equal("q1", queueTriggerAttr.QueueName);
|
||||
|
||||
// Table
|
||||
var tableAttr = GetAttr<TableAttribute>(metadataProvider, new { TableName = "t1" });
|
||||
Assert.Equal("t1", tableAttr.TableName);
|
||||
|
||||
tableAttr = GetAttr<TableAttribute>(metadataProvider, new { TableName = "t1", partitionKey = "pk", Filter = "f1" });
|
||||
Assert.Equal("t1", tableAttr.TableName);
|
||||
Assert.Equal("pk", tableAttr.PartitionKey);
|
||||
Assert.Equal(null, tableAttr.RowKey);
|
||||
Assert.Equal("f1", tableAttr.Filter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultTypeForTable()
|
||||
{
|
||||
var host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
})
|
||||
.Build();
|
||||
|
||||
var metadataProvider = host.CreateMetadataProvider();
|
||||
|
||||
var t1 = metadataProvider.GetDefaultType(new TableAttribute("table1"), FileAccess.Read, null);
|
||||
Assert.Equal(typeof(JArray), t1);
|
||||
|
||||
var t2 = metadataProvider.GetDefaultType(new TableAttribute("table1", "pk", "rk"), FileAccess.Read, null);
|
||||
Assert.Equal(typeof(JObject), t2);
|
||||
|
||||
var t3 = metadataProvider.GetDefaultType(new TableAttribute("table1"), FileAccess.Write, null);
|
||||
Assert.Equal(typeof(IAsyncCollector<JObject>), t3);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void DefaultTypeForQueue()
|
||||
{
|
||||
var host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
b.AddAzureStorageQueues();
|
||||
})
|
||||
.Build();
|
||||
|
||||
|
|
|
@ -8,8 +8,9 @@ using System.Reflection;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Azure.Storage.Blobs;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
@ -29,12 +30,12 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
.UseEnvironment("Development")
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.AddAzureStorage()
|
||||
.AddAzureStorageCoreServices();
|
||||
b.AddAzureStorageQueues();
|
||||
b.AddAzureStorageCoreServices();
|
||||
});
|
||||
|
||||
IHost host = hostBuilder.Build();
|
||||
var config = host.Services.GetService<DistributedLockManagerContainerProvider>();
|
||||
var config = host.Services.GetService<IOptions<JobHostInternalStorageOptions>>();
|
||||
|
||||
var hostingEnvironment = host.Services.GetService<IHostingEnvironment>();
|
||||
Assert.True(hostingEnvironment.IsDevelopment());
|
||||
|
@ -53,12 +54,12 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
.UseEnvironment("Production")
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.AddAzureStorage()
|
||||
.AddAzureStorageCoreServices();
|
||||
b.AddAzureStorageQueues();
|
||||
b.AddAzureStorageCoreServices();
|
||||
});
|
||||
|
||||
IHost host = hostBuilder.Build();
|
||||
var config = host.Services.GetService<DistributedLockManagerContainerProvider>();
|
||||
var config = host.Services.GetService<IOptions<JobHostInternalStorageOptions>>();
|
||||
|
||||
var hostingEnvironment = host.Services.GetService<IHostingEnvironment>();
|
||||
Assert.False(hostingEnvironment.IsDevelopment());
|
||||
|
@ -78,8 +79,8 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
.UseEnvironment("Development")
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.AddAzureStorage()
|
||||
.AddAzureStorageCoreServices();
|
||||
b.AddAzureStorageQueues();
|
||||
b.AddAzureStorageCoreServices();
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
|
@ -95,7 +96,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
});
|
||||
|
||||
IHost host = hostBuilder.Build();
|
||||
var config = host.Services.GetService<DistributedLockManagerContainerProvider>();
|
||||
var config = host.Services.GetService<IOptions<JobHostInternalStorageOptions>>();
|
||||
|
||||
var hostingEnvironment = host.Services.GetService<IHostingEnvironment>();
|
||||
Assert.True(hostingEnvironment.IsDevelopment());
|
||||
|
@ -148,8 +149,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
var hostBuilder = new HostBuilder()
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.AddAzureStorage()
|
||||
.AddAzureStorageCoreServices();
|
||||
b.AddAzureStorageCoreServices();
|
||||
})
|
||||
.ConfigureAppConfiguration(c =>
|
||||
{
|
||||
|
@ -158,11 +158,14 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
|
||||
IHost host = hostBuilder.Build();
|
||||
|
||||
var config = host.Services.GetService<DistributedLockManagerContainerProvider>();
|
||||
var config = host.Services.GetService<IOptions<JobHostInternalStorageOptions>>();
|
||||
var blobStorageProvider = host.Services.GetService<IAzureBlobStorageProvider>();
|
||||
|
||||
var container = config.InternalContainer;
|
||||
var container = config.Value.InternalSasBlobContainer;
|
||||
Assert.NotNull(container);
|
||||
Assert.Equal(container.Name, "myContainer"); // specified in sas.
|
||||
|
||||
Assert.True(blobStorageProvider.TryCreateHostingBlobContainerClient(out BlobContainerClient containerClient));
|
||||
Assert.Equal("myContainer", containerClient.Name); // specified in sas.
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,8 +176,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
var hostBuilder = new HostBuilder()
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.AddAzureStorage()
|
||||
.AddAzureStorageCoreServices();
|
||||
b.AddAzureStorageCoreServices();
|
||||
})
|
||||
.ConfigureAppConfiguration(c =>
|
||||
{
|
||||
|
@ -184,19 +186,19 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
hostBuilder.ConfigureServices((ctx, services) =>
|
||||
{
|
||||
var uri2 = new Uri("https://contoso.blob.core.windows.net/myContainer2?signature=foo");
|
||||
services.AddSingleton<DistributedLockManagerContainerProvider>(new DistributedLockManagerContainerProvider()
|
||||
{
|
||||
InternalContainer = new CloudBlobContainer(uri2)
|
||||
});
|
||||
services.Configure<JobHostInternalStorageOptions>(o => o.InternalSasBlobContainer = uri2.ToString());
|
||||
});
|
||||
|
||||
IHost host = hostBuilder.Build();
|
||||
|
||||
var config = host.Services.GetService<DistributedLockManagerContainerProvider>();
|
||||
var config = host.Services.GetService<IOptions<JobHostInternalStorageOptions>>();
|
||||
var blobStorageProvider = host.Services.GetService<IAzureBlobStorageProvider>();
|
||||
|
||||
var container = config.InternalContainer;
|
||||
var container = config.Value.InternalSasBlobContainer;
|
||||
Assert.NotNull(container);
|
||||
Assert.Equal(container.Name, "myContainer2"); // specified in sas.
|
||||
|
||||
Assert.True(blobStorageProvider.TryCreateHostingBlobContainerClient(out BlobContainerClient containerClient));
|
||||
Assert.Equal("myContainer2", containerClient.Name); // specified in sas.
|
||||
}
|
||||
|
||||
// Verify that JobHostConfig pulls a Sas container from appsettings.
|
||||
|
@ -208,8 +210,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
var hostBuilder = new HostBuilder()
|
||||
.ConfigureDefaultTestHost(b =>
|
||||
{
|
||||
b.AddAzureStorage()
|
||||
.AddAzureStorageCoreServices();
|
||||
b.AddAzureStorageCoreServices();
|
||||
})
|
||||
.ConfigureAppConfiguration(c =>
|
||||
{
|
||||
|
@ -221,11 +222,14 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
|
||||
IHost host = hostBuilder.Build();
|
||||
|
||||
var config = host.Services.GetService<DistributedLockManagerContainerProvider>();
|
||||
var config = host.Services.GetService<IOptions<JobHostInternalStorageOptions>>();
|
||||
var blobStorageProvider = host.Services.GetService<IAzureBlobStorageProvider>();
|
||||
|
||||
var container = config.InternalContainer;
|
||||
var container = config.Value.InternalSasBlobContainer;
|
||||
Assert.NotNull(container);
|
||||
Assert.Equal(container.Name, "myContainer3"); // specified in sas.
|
||||
|
||||
Assert.True(blobStorageProvider.TryCreateHostingBlobContainerClient(out BlobContainerClient containerClient));
|
||||
Assert.Equal("myContainer3", containerClient.Name); // specified in sas.
|
||||
}
|
||||
|
||||
// Test that we can explicitly disable storage and call through a function
|
||||
|
@ -304,4 +308,4 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,8 @@ using System.Reflection;
|
|||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Microsoft.Azure.Storage.Queue;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Queues.Models;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Indexers;
|
||||
using Microsoft.Azure.WebJobs.Host.Listeners;
|
||||
|
@ -374,7 +373,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
[Fact]
|
||||
public async Task CallAsync_WithDictionary()
|
||||
{
|
||||
var host = JobHostFactory.Create<ProgramSimple>(null);
|
||||
var host = JobHostFactory.Create<ProgramSimple>();
|
||||
|
||||
var value = "abc";
|
||||
ProgramSimple._value = null;
|
||||
|
@ -389,7 +388,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
[InlineData("test")]
|
||||
public async Task CallAsync_WithObject(string methodName)
|
||||
{
|
||||
var host = JobHostFactory.Create<ProgramSimple>(null);
|
||||
var host = JobHostFactory.Create<ProgramSimple>();
|
||||
|
||||
var x = "abc";
|
||||
ProgramSimple._value = null;
|
||||
|
@ -404,7 +403,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
{
|
||||
// Arrange
|
||||
ProgramWithCancellationToken.Cleanup();
|
||||
var host = JobHostFactory.Create<ProgramWithCancellationToken>(null);
|
||||
var host = JobHostFactory.Create<ProgramWithCancellationToken>();
|
||||
|
||||
using (CancellationTokenSource source = new CancellationTokenSource())
|
||||
{
|
||||
|
@ -429,7 +428,7 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
string expectedStackTrace = expectedExceptionInfo.SourceException.StackTrace;
|
||||
ThrowingProgram.ExceptionInfo = expectedExceptionInfo;
|
||||
|
||||
var host = JobHostFactory.Create<ThrowingProgram>(null);
|
||||
var host = JobHostFactory.Create<ThrowingProgram>();
|
||||
MethodInfo methodInfo = typeof(ThrowingProgram).GetMethod("Throw");
|
||||
|
||||
// Act & Assert
|
||||
|
@ -445,179 +444,6 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BlobTrigger_ProvidesBlobTriggerBindingData()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
var host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<BlobTriggerBindingDataProgram>(c =>
|
||||
{
|
||||
c.AddAzureStorage();
|
||||
})
|
||||
.Build()
|
||||
.GetJobHost();
|
||||
|
||||
using (host)
|
||||
{
|
||||
CloudStorageAccount account = CloudStorageAccount.DevelopmentStorageAccount;
|
||||
|
||||
MethodInfo methodInfo = typeof(BlobTriggerBindingDataProgram).GetMethod(nameof(BlobTriggerBindingDataProgram.OnBlob));
|
||||
string containerName = "a";
|
||||
string blobName = "b";
|
||||
string expectedPath = containerName + "/" + blobName;
|
||||
CloudBlobContainer container = account.CreateCloudBlobClient().GetContainerReference(containerName);
|
||||
ICloudBlob blob = container.GetBlockBlobReference(blobName);
|
||||
|
||||
// Act
|
||||
await host.CallAsync(methodInfo, new { blob = blob });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedPath, BlobTriggerBindingDataProgram.BlobTrigger);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
BlobTriggerBindingDataProgram.BlobTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueueTrigger_ProvidesQueueTriggerBindingData()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
var host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<QueueTriggerBindingDataProgram>(c =>
|
||||
{
|
||||
c.AddAzureStorage();
|
||||
})
|
||||
.Build()
|
||||
.GetJobHost();
|
||||
|
||||
using (host)
|
||||
{
|
||||
MethodInfo methodInfo = typeof(QueueTriggerBindingDataProgram).GetMethod(nameof(QueueTriggerBindingDataProgram.OnQueue));
|
||||
string expectedMessage = "a";
|
||||
|
||||
// Act
|
||||
await host.CallAsync(methodInfo, new { message = expectedMessage });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedMessage, QueueTriggerBindingDataProgram.QueueTrigger);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
QueueTriggerBindingDataProgram.QueueTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueueTrigger_WithTextualByteArrayMessage_ProvidesQueueTriggerBindingData()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
var host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<QueueTriggerBindingDataProgram>(c =>
|
||||
{
|
||||
c.AddAzureStorage();
|
||||
})
|
||||
.Build()
|
||||
.GetJobHost();
|
||||
|
||||
using (host)
|
||||
{
|
||||
MethodInfo methodInfo = typeof(QueueTriggerBindingDataProgram).GetMethod(nameof(QueueTriggerBindingDataProgram.OnQueue));
|
||||
string expectedMessage = "abc";
|
||||
CloudQueueMessage message = new CloudQueueMessage(expectedMessage);
|
||||
Assert.Equal(expectedMessage, message.AsString); // Guard
|
||||
|
||||
// Act
|
||||
await host.CallAsync(methodInfo, new { message });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedMessage, QueueTriggerBindingDataProgram.QueueTrigger);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
QueueTriggerBindingDataProgram.QueueTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueueTrigger_WithNonTextualByteArrayMessageUsingQueueTriggerBindingData_Throws()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
var host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<QueueTriggerBindingDataProgram>(c =>
|
||||
{
|
||||
c.AddAzureStorage();
|
||||
})
|
||||
.Build()
|
||||
.GetJobHost();
|
||||
|
||||
using (host)
|
||||
{
|
||||
MethodInfo methodInfo = typeof(QueueTriggerBindingDataProgram).GetMethod(nameof(QueueTriggerBindingDataProgram.OnQueue));
|
||||
byte[] contents = new byte[] { 0x00, 0xFF }; // Not valid UTF-8
|
||||
CloudQueueMessage message = new CloudQueueMessage(contents);
|
||||
|
||||
// Act & Assert
|
||||
FunctionInvocationException exception = await Assert.ThrowsAsync<FunctionInvocationException>(
|
||||
() => host.CallAsync(methodInfo, new { message = message }));
|
||||
// This exeption shape/message could be better, but it's meets a minimum acceptibility threshold.
|
||||
Assert.Equal("Exception binding parameter 'queueTrigger'", exception.InnerException.Message);
|
||||
Exception innerException = exception.InnerException.InnerException;
|
||||
Assert.IsType<InvalidOperationException>(innerException);
|
||||
Assert.Equal("Binding data does not contain expected value 'queueTrigger'.", innerException.Message);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
QueueTriggerBindingDataProgram.QueueTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueueTrigger_WithNonTextualByteArrayMessageNotUsingQueueTriggerBindingData_DoesNotThrow()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
var host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<QueueTriggerBindingDataProgram>(c =>
|
||||
{
|
||||
c.AddAzureStorage();
|
||||
})
|
||||
.Build()
|
||||
.GetJobHost();
|
||||
|
||||
using (host)
|
||||
{
|
||||
MethodInfo methodInfo = typeof(QueueTriggerBindingDataProgram).GetMethod(nameof(QueueTriggerBindingDataProgram.ProcessQueueAsBytes));
|
||||
byte[] expectedBytes = new byte[] { 0x00, 0xFF }; // Not valid UTF-8
|
||||
CloudQueueMessage message = new CloudQueueMessage(expectedBytes);
|
||||
|
||||
// Act
|
||||
await host.CallAsync(methodInfo, new { message = message });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(QueueTriggerBindingDataProgram.Bytes, expectedBytes);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
QueueTriggerBindingDataProgram.QueueTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "secretsrequired")]
|
||||
public async Task IndexingExceptions_CanBeHandledByLogger()
|
||||
|
@ -632,7 +458,8 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
var builder = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<BindingErrorsProgram>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
b.AddAzureStorageBlobs();
|
||||
b.AddAzureStorageQueues();
|
||||
})
|
||||
.ConfigureLogging(logging =>
|
||||
{
|
||||
|
@ -649,27 +476,21 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
Assert.True(fex.Handled);
|
||||
Assert.Equal("BindingErrorsProgram.Invalid", fex.MethodName);
|
||||
|
||||
// verify that the binding error was logged
|
||||
Assert.Equal(11, errorLogger.GetLogMessages().Count);
|
||||
|
||||
// Skip validating the initial 'Starting JobHost' message and the OptionsFormatters
|
||||
|
||||
LogMessage logMessage = errorLogger.GetLogMessages()[7];
|
||||
Assert.Equal("Error indexing method 'BindingErrorsProgram.Invalid'", logMessage.FormattedMessage);
|
||||
Assert.Same(fex, logMessage.Exception);
|
||||
Assert.Equal("Invalid container name: invalid$=+1", logMessage.Exception.InnerException.Message);
|
||||
|
||||
logMessage = errorLogger.GetLogMessages()[8];
|
||||
Assert.Equal("Function 'BindingErrorsProgram.Invalid' failed indexing and will be disabled.", logMessage.FormattedMessage);
|
||||
// verify that the binding error was logged
|
||||
var bindingErrorLog = errorLogger.GetLogMessages().Single(l => l.FormattedMessage.Equals("Error indexing method 'BindingErrorsProgram.Invalid'"));
|
||||
Assert.NotNull(bindingErrorLog);
|
||||
Assert.Same(fex, bindingErrorLog.Exception);
|
||||
|
||||
var disabledLog = errorLogger.GetLogMessages().Single(l => l.FormattedMessage.Equals("Function 'BindingErrorsProgram.Invalid' failed indexing and will be disabled."));
|
||||
Assert.NotNull(disabledLog);
|
||||
|
||||
// verify that the valid function was still indexed
|
||||
logMessage = errorLogger.GetLogMessages()[9];
|
||||
Assert.True(logMessage.FormattedMessage.Contains("Found the following functions"));
|
||||
Assert.True(logMessage.FormattedMessage.Contains("BindingErrorsProgram.Valid"));
|
||||
Assert.Contains(errorLogger.GetLogMessages(), p => p.FormattedMessage.Contains("Found the following functions") && p.FormattedMessage.Contains("BindingErrorsProgram.Valid"));
|
||||
|
||||
// verify that the job host was started successfully
|
||||
logMessage = errorLogger.GetLogMessages()[10];
|
||||
Assert.Equal("Job host started", logMessage.FormattedMessage);
|
||||
Assert.Contains(errorLogger.GetLogMessages(), p => p.FormattedMessage.Equals("Job host started"));
|
||||
|
||||
await host.StopAsync();
|
||||
}
|
||||
|
@ -744,32 +565,6 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
}
|
||||
}
|
||||
|
||||
private class BlobTriggerBindingDataProgram
|
||||
{
|
||||
public static string BlobTrigger { get; set; }
|
||||
|
||||
public static void OnBlob([BlobTrigger("ignore/{name}")] ICloudBlob blob, string blobTrigger)
|
||||
{
|
||||
BlobTrigger = blobTrigger;
|
||||
}
|
||||
}
|
||||
|
||||
private class QueueTriggerBindingDataProgram
|
||||
{
|
||||
public static string QueueTrigger { get; set; }
|
||||
public static byte[] Bytes { get; set; }
|
||||
|
||||
public static void OnQueue([QueueTrigger("ignore")] CloudQueueMessage message, string queueTrigger)
|
||||
{
|
||||
QueueTrigger = queueTrigger;
|
||||
}
|
||||
|
||||
public static void ProcessQueueAsBytes([QueueTrigger("ignore")] byte[] message)
|
||||
{
|
||||
Bytes = message;
|
||||
}
|
||||
}
|
||||
|
||||
private class BindingErrorsProgram
|
||||
{
|
||||
// Invalid function
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.UnitTests.Protocols
|
||||
{
|
||||
public class FunctionStartedMessageExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void FormatReason_QueueTriggeredFunction_ReturnsExpectedReason()
|
||||
{
|
||||
FunctionStartedMessage message = new FunctionStartedMessage();
|
||||
|
||||
QueueTriggerParameterDescriptor triggerParameterDescriptor = new QueueTriggerParameterDescriptor
|
||||
{
|
||||
Name = "paramName",
|
||||
QueueName = "testqueue"
|
||||
};
|
||||
FunctionDescriptor function = new FunctionDescriptor
|
||||
{
|
||||
Parameters = new ParameterDescriptor[] { triggerParameterDescriptor },
|
||||
TriggerParameterDescriptor = triggerParameterDescriptor
|
||||
};
|
||||
message.Function = function;
|
||||
message.Reason = ExecutionReason.AutomaticTrigger;
|
||||
|
||||
string result = message.FormatReason();
|
||||
Assert.Equal("New queue message detected on 'testqueue'.", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatReason_BlobTriggeredFunction_ReturnsExpectedReason()
|
||||
{
|
||||
FunctionStartedMessage message = new FunctionStartedMessage();
|
||||
message.Reason = ExecutionReason.AutomaticTrigger;
|
||||
|
||||
BlobTriggerParameterDescriptor triggerParameterDescriptor = new BlobTriggerParameterDescriptor
|
||||
{
|
||||
Name = "paramName"
|
||||
};
|
||||
FunctionDescriptor function = new FunctionDescriptor()
|
||||
{
|
||||
Parameters = new ParameterDescriptor[] { triggerParameterDescriptor },
|
||||
TriggerParameterDescriptor = triggerParameterDescriptor
|
||||
};
|
||||
message.Function = function;
|
||||
message.Arguments = new Dictionary<string, string>() { { "paramName", "blob/path" } };
|
||||
|
||||
string result = message.FormatReason();
|
||||
Assert.Equal("New blob detected: blob/path", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatReason_ReasonDetailsAlreadySet_ReturnsExpectedReason()
|
||||
{
|
||||
FunctionStartedMessage message = new FunctionStartedMessage();
|
||||
message.ReasonDetails = "The trigger fired!";
|
||||
|
||||
string result = message.FormatReason();
|
||||
Assert.Equal("The trigger fired!", result);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using WebJobs.Host.Storage.Logging;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.UnitTests.Loggers
|
||||
{
|
||||
internal static class UpdateOutputLogCommandExtensions
|
||||
{
|
||||
public static bool TryExecute(this UpdateOutputLogCommand command)
|
||||
{
|
||||
if (command == null)
|
||||
{
|
||||
throw new ArgumentNullException("command");
|
||||
}
|
||||
|
||||
return command.TryExecuteAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static void SaveAndClose(this UpdateOutputLogCommand command)
|
||||
{
|
||||
if (command == null)
|
||||
{
|
||||
throw new ArgumentNullException("command");
|
||||
}
|
||||
|
||||
command.SaveAndCloseAsync(null, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Moq;
|
||||
using WebJobs.Host.Storage.Logging;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.UnitTests.Loggers
|
||||
{
|
||||
public class UpdateOutputLogCommandTests
|
||||
{
|
||||
private CloudBlockBlob GetTestBlob()
|
||||
{
|
||||
// Need a fake blob that we can read from, see UpdateOutputLogCommand.ReadBlobAsync
|
||||
var account = new FakeStorage.FakeAccount();
|
||||
var blobClient = account.CreateCloudBlobClient();
|
||||
var blob = blobClient.GetContainerReference("container").GetBlockBlobReference("blob");
|
||||
return blob;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestIncrementalWriter()
|
||||
{
|
||||
string content = null;
|
||||
Func<string, CancellationToken, Task> fp = (x, _) =>
|
||||
{
|
||||
content = x;
|
||||
return Task.FromResult(0);
|
||||
};
|
||||
|
||||
|
||||
var blob = GetTestBlob();
|
||||
|
||||
UpdateOutputLogCommand writer = UpdateOutputLogCommand.Create(
|
||||
blob, fp);
|
||||
|
||||
var tw = writer.Output;
|
||||
tw.Write("1");
|
||||
|
||||
// Ensure content not yet written
|
||||
Assert.Equal(null, content);
|
||||
|
||||
writer.TryExecute();
|
||||
|
||||
Assert.Equal("1", content);
|
||||
|
||||
tw.Write("2");
|
||||
writer.TryExecute();
|
||||
|
||||
Assert.Equal("12", content);
|
||||
|
||||
tw.Write("3");
|
||||
writer.SaveAndClose();
|
||||
Assert.Equal("123", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestMultipleThreads()
|
||||
{
|
||||
// This validates a bug where flushing from one thread while writing from another
|
||||
// would cause an exception.
|
||||
|
||||
var blob = GetTestBlob();
|
||||
|
||||
string content = null;
|
||||
Func<string, CancellationToken, Task> fp = (x, _) => { content = x; return Task.FromResult(0); };
|
||||
UpdateOutputLogCommand writer = UpdateOutputLogCommand.Create(blob, fp);
|
||||
|
||||
var tw = writer.Output;
|
||||
bool writeDone = false;
|
||||
|
||||
// Start a Task to flush
|
||||
Task flushTask = Task.Run(() =>
|
||||
{
|
||||
while (!writeDone)
|
||||
{
|
||||
writer.TryExecute();
|
||||
}
|
||||
});
|
||||
|
||||
// Start a Task to write
|
||||
Task writeTask = Task.Run(() =>
|
||||
{
|
||||
for (int i = 0; i < 10000000; i++)
|
||||
{
|
||||
tw.WriteLine(string.Empty);
|
||||
}
|
||||
writeDone = true;
|
||||
});
|
||||
|
||||
await flushTask;
|
||||
await writeTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,9 +7,11 @@ using System.Linq;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Azure;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Specialized;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Azure.WebJobs.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
@ -91,7 +93,7 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
{
|
||||
var host = CreateHost();
|
||||
|
||||
string connectionString = GetStorageConnectionString(host);
|
||||
var containerClient = GetHostingBlobContainerClient(host);
|
||||
string hostId = GetHostId(host);
|
||||
|
||||
using (host)
|
||||
|
@ -104,7 +106,7 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
await host.StopAsync();
|
||||
}
|
||||
|
||||
await ClearLeaseBlob(connectionString, hostId);
|
||||
await ClearLeaseBlob(containerClient, hostId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -112,13 +114,13 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
{
|
||||
var host = CreateHost();
|
||||
|
||||
string connectionString = GetStorageConnectionString(host);
|
||||
var containerClient = GetHostingBlobContainerClient(host);
|
||||
string hostId = GetHostId(host);
|
||||
|
||||
ICloudBlob blob = await GetLockBlobAsync(connectionString, hostId);
|
||||
BlobClient blobClient = await GetLockBlobAsync(containerClient, hostId);
|
||||
|
||||
// Acquire a lease on the host lock blob
|
||||
string leaseId = await blob.AcquireLeaseAsync(TimeSpan.FromMinutes(1));
|
||||
string leaseId = (await blobClient.GetBlobLeaseClient().AcquireAsync(TimeSpan.FromMinutes(1))).Value.LeaseId;
|
||||
|
||||
using (host)
|
||||
{
|
||||
|
@ -130,7 +132,7 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
Assert.False(primaryState.IsPrimary);
|
||||
|
||||
// Now release it, and we should reclaim it.
|
||||
await blob.ReleaseLeaseAsync(new AccessCondition { LeaseId = leaseId });
|
||||
await blobClient.GetBlobLeaseClient(leaseId).ReleaseAsync();
|
||||
|
||||
await TestHelpers.Await(() => primaryState.IsPrimary,
|
||||
userMessageCallback: () => $"{nameof(IPrimaryHostStateProvider.IsPrimary)} was not correctly set to 'true' when lease was acquired.");
|
||||
|
@ -138,7 +140,7 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
await host.StopAsync();
|
||||
}
|
||||
|
||||
await ClearLeaseBlob(connectionString, hostId);
|
||||
await ClearLeaseBlob(containerClient, hostId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -146,12 +148,12 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
{
|
||||
var host = CreateHost();
|
||||
|
||||
string connectionString = GetStorageConnectionString(host);
|
||||
var containerClient = GetHostingBlobContainerClient(host);
|
||||
string hostId = GetHostId(host);
|
||||
|
||||
using (host)
|
||||
{
|
||||
ICloudBlob blob = await GetLockBlobAsync(connectionString, hostId);
|
||||
BlobClient blobClient = await GetLockBlobAsync(containerClient, hostId);
|
||||
var primaryState = host.Services.GetService<IPrimaryHostStateProvider>();
|
||||
var manager = host.Services.GetServices<IHostedService>().OfType<PrimaryHostCoordinator>().Single();
|
||||
var lockManager = host.Services.GetService<IDistributedLockManager>();
|
||||
|
@ -165,7 +167,7 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
|
||||
// Release the manager's lease and acquire one with a different id
|
||||
await lockManager.ReleaseLockAsync(manager.LockHandle, CancellationToken.None);
|
||||
tempLeaseId = await blob.AcquireLeaseAsync(TimeSpan.FromSeconds(30), Guid.NewGuid().ToString());
|
||||
tempLeaseId = (await blobClient.GetBlobLeaseClient(Guid.NewGuid().ToString()).AcquireAsync(TimeSpan.FromSeconds(30))).Value.LeaseId;
|
||||
|
||||
await TestHelpers.Await(() => !primaryState.IsPrimary,
|
||||
userMessageCallback: () => $"{nameof(IPrimaryHostStateProvider.IsPrimary)} was not correctly set to 'false' when lease lost.");
|
||||
|
@ -174,14 +176,14 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
{
|
||||
if (tempLeaseId != null)
|
||||
{
|
||||
await blob.ReleaseLeaseAsync(new AccessCondition { LeaseId = tempLeaseId });
|
||||
await blobClient.GetBlobLeaseClient(tempLeaseId).ReleaseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
await host.StopAsync();
|
||||
}
|
||||
|
||||
await ClearLeaseBlob(connectionString, hostId);
|
||||
await ClearLeaseBlob(containerClient, hostId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -189,7 +191,7 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
{
|
||||
var host = CreateHost();
|
||||
|
||||
string connectionString = GetStorageConnectionString(host);
|
||||
var containerClient = GetHostingBlobContainerClient(host);
|
||||
string hostId = GetHostId(host);
|
||||
var primaryHostCoordinator = host.Services.GetServices<IHostedService>().OfType<PrimaryHostCoordinator>().Single();
|
||||
|
||||
|
@ -206,23 +208,23 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
// Container disposal is a fire-and-forget so this service disposal could be delayed. This will force it.
|
||||
primaryHostCoordinator.Dispose();
|
||||
|
||||
ICloudBlob blob = await GetLockBlobAsync(connectionString, hostId);
|
||||
BlobClient blobClient = await GetLockBlobAsync(containerClient, hostId);
|
||||
|
||||
string leaseId = null;
|
||||
try
|
||||
{
|
||||
// Acquire a lease on the host lock blob
|
||||
leaseId = await blob.AcquireLeaseAsync(TimeSpan.FromSeconds(15));
|
||||
leaseId = (await blobClient.GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15))).Value.LeaseId;
|
||||
|
||||
await blob.ReleaseLeaseAsync(new AccessCondition { LeaseId = leaseId });
|
||||
await blobClient.GetBlobLeaseClient(leaseId).ReleaseAsync();
|
||||
}
|
||||
catch (StorageException exc) when (exc.RequestInformation.HttpStatusCode == 409)
|
||||
catch (RequestFailedException exc) when (exc.Status == 409)
|
||||
{
|
||||
}
|
||||
|
||||
Assert.False(string.IsNullOrEmpty(leaseId), "Failed to acquire a blob lease. The lease was not properly released.");
|
||||
|
||||
await ClearLeaseBlob(connectionString, hostId);
|
||||
await ClearLeaseBlob(containerClient, hostId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -238,7 +240,6 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
s.AddSingleton<IDistributedLockManager>(_ => blobMock.Object);
|
||||
});
|
||||
|
||||
string connectionString = GetStorageConnectionString(host);
|
||||
string hostId = GetHostId(host);
|
||||
string instanceId = Microsoft.Azure.WebJobs.Utility.GetInstanceId();
|
||||
|
||||
|
@ -268,7 +269,7 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
.Returns(() => Task.FromResult<IDistributedLock>(new FakeLock()));
|
||||
|
||||
blobMock.Setup(b => b.RenewAsync(It.IsAny<IDistributedLock>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(() => Task.FromException<bool>(new StorageException(new RequestResult { HttpStatusCode = 409 }, "test", null)))
|
||||
.Returns(() => Task.FromException<bool>(new RequestFailedException(409, "test")))
|
||||
.Callback(() => renewResetEvent.Set());
|
||||
|
||||
var host = CreateHost(s =>
|
||||
|
@ -309,13 +310,13 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
s.AddSingleton<IHostIdProvider>(_ => new FixedHostIdProvider(hostId1));
|
||||
});
|
||||
|
||||
Assert.True(host1.Services.GetRequiredService<IAzureBlobStorageProvider>().TryCreateHostingBlobContainerClient(out BlobContainerClient containerClient1));
|
||||
|
||||
var host2 = CreateHost(s =>
|
||||
{
|
||||
s.AddSingleton<IHostIdProvider>(_ => new FixedHostIdProvider(hostId2));
|
||||
});
|
||||
|
||||
string host1ConnectionString = GetStorageConnectionString(host1);
|
||||
string host2ConnectionString = GetStorageConnectionString(host2);
|
||||
Assert.True(host2.Services.GetRequiredService<IAzureBlobStorageProvider>().TryCreateHostingBlobContainerClient(out BlobContainerClient containerClient2));
|
||||
|
||||
using (host1)
|
||||
using (host2)
|
||||
|
@ -335,41 +336,36 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
await host2.StopAsync();
|
||||
}
|
||||
|
||||
await Task.WhenAll(ClearLeaseBlob(host1ConnectionString, hostId1), ClearLeaseBlob(host2ConnectionString, hostId2));
|
||||
await Task.WhenAll(ClearLeaseBlob(containerClient1, hostId1), ClearLeaseBlob(containerClient2, hostId2));
|
||||
}
|
||||
|
||||
private static async Task<ICloudBlob> GetLockBlobAsync(string accountConnectionString, string hostId)
|
||||
private static async Task<BlobClient> GetLockBlobAsync(BlobContainerClient containerClient, string hostId)
|
||||
{
|
||||
CloudStorageAccount account = CloudStorageAccount.Parse(accountConnectionString);
|
||||
CloudBlobClient client = account.CreateCloudBlobClient();
|
||||
await containerClient.CreateIfNotExistsAsync();
|
||||
|
||||
var container = client.GetContainerReference(HostContainerNames.Hosts);
|
||||
|
||||
await container.CreateIfNotExistsAsync();
|
||||
|
||||
// the StorageDistributedLockManager puts things under the /locks path by default
|
||||
CloudBlockBlob blob = container.GetBlockBlobReference("locks/" + PrimaryHostCoordinator.GetBlobName(hostId));
|
||||
if (!await blob.ExistsAsync())
|
||||
// The BlobLeaseDistributedLockManager puts things under the /locks path by default
|
||||
var blobClient = containerClient.GetBlobClient("locks/" + PrimaryHostCoordinator.GetBlobName(hostId));
|
||||
if (!await blobClient.ExistsAsync())
|
||||
{
|
||||
await blob.UploadFromStreamAsync(new MemoryStream());
|
||||
await blobClient.UploadTextAsync("", overwrite: true);
|
||||
}
|
||||
|
||||
return blob;
|
||||
return blobClient;
|
||||
}
|
||||
|
||||
private async Task ClearLeaseBlob(string connectionString, string hostId)
|
||||
private async Task ClearLeaseBlob(BlobContainerClient containerClient, string hostId)
|
||||
{
|
||||
ICloudBlob blob = await GetLockBlobAsync(connectionString, hostId);
|
||||
BlobClient blobClient = await GetLockBlobAsync(containerClient, hostId);
|
||||
|
||||
try
|
||||
{
|
||||
await blob.BreakLeaseAsync(TimeSpan.Zero);
|
||||
await blobClient.GetBlobLeaseClient().BreakAsync(TimeSpan.Zero);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
await blob.DeleteIfExistsAsync();
|
||||
await blobClient.DeleteIfExistsAsync();
|
||||
}
|
||||
|
||||
private class FakeLock : IDistributedLock
|
||||
|
@ -394,16 +390,17 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
private static string GetStorageConnectionString(IHost host)
|
||||
{
|
||||
return host.Services.GetService<IConfiguration>().GetWebJobsConnectionString("Storage");
|
||||
}
|
||||
|
||||
public static string GetHostId(IHost host)
|
||||
{
|
||||
return host.Services.GetService<IHostIdProvider>().GetHostIdAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static BlobContainerClient GetHostingBlobContainerClient(IHost host)
|
||||
{
|
||||
host.Services.GetRequiredService<IAzureBlobStorageProvider>().TryCreateHostingBlobContainerClient(out BlobContainerClient containerClient);
|
||||
return containerClient;
|
||||
}
|
||||
|
||||
public class Program
|
||||
{
|
||||
[NoAutomaticTrigger]
|
||||
|
|
|
@ -21,12 +21,10 @@ namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
|||
|
||||
var expected = new[]
|
||||
{
|
||||
"CloudBlobContainerDistributedLockManager",
|
||||
"DistributedLockManagerContainerProvider",
|
||||
"JobHostInternalStorageOptions",
|
||||
"RuntimeStorageWebJobsBuilderExtensions",
|
||||
"StorageBaseDistributedLockManager",
|
||||
"StorageServiceCollectionExtensions"
|
||||
"StorageServiceCollectionExtensions",
|
||||
"IAzureBlobStorageProvider"
|
||||
};
|
||||
|
||||
TestHelpers.AssertPublicTypes(expected, assembly);
|
||||
|
|
|
@ -12,7 +12,7 @@ using Microsoft.Extensions.Configuration;
|
|||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using SingletonLockHandle = Microsoft.Azure.WebJobs.Host.StorageBaseDistributedLockManager.SingletonLockHandle;
|
||||
using SingletonLockHandle = Microsoft.Azure.WebJobs.Host.BlobLeaseDistributedLockManager.SingletonLockHandle;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
||||
{
|
||||
|
|
|
@ -5,7 +5,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using SingletonLockHandle = Microsoft.Azure.WebJobs.Host.StorageBaseDistributedLockManager.SingletonLockHandle;
|
||||
using SingletonLockHandle = Microsoft.Azure.WebJobs.Host.BlobLeaseDistributedLockManager.SingletonLockHandle;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
||||
{
|
||||
|
|
|
@ -0,0 +1,372 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Azure.Storage.Blobs.Specialized;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Azure.WebJobs.Host.Timers;
|
||||
using Microsoft.Azure.WebJobs.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
||||
{
|
||||
public class SingletonManagerStorageIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestArtifactContainerPrefix = "e2e-singletonmanagertests";
|
||||
private const string TestArtifactContainerName = TestArtifactContainerPrefix + "-%rnd%";
|
||||
|
||||
private const string TestHostId = "testhost";
|
||||
private const string TestLockId = "testid";
|
||||
private const string TestInstanceId = "testinstance";
|
||||
|
||||
private BlobLeaseDistributedLockManager _core;
|
||||
private SingletonManager _singletonManager;
|
||||
private SingletonOptions _singletonConfig;
|
||||
|
||||
private Mock<IWebJobsExceptionHandler> _mockExceptionDispatcher;
|
||||
|
||||
private readonly BlobContainerClient _testContainerClient;
|
||||
|
||||
private TestLoggerProvider _loggerProvider;
|
||||
private TestNameResolver _nameResolver;
|
||||
|
||||
internal class TestBlobLeaseDistributedLockManager : BlobLeaseDistributedLockManager
|
||||
{
|
||||
// To set up testing
|
||||
public BlobContainerClient ContainerClient;
|
||||
|
||||
public TestBlobLeaseDistributedLockManager(ILoggerFactory logger, IAzureBlobStorageProvider blobStorageProvider) : base(logger, blobStorageProvider) { }
|
||||
|
||||
protected override BlobContainerClient GetContainerClient(string accountName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(accountName))
|
||||
{
|
||||
throw new InvalidOperationException("This test does not support multiple accounts.");
|
||||
}
|
||||
|
||||
return ContainerClient;
|
||||
}
|
||||
|
||||
// Testing the base method to verify we can use multiple storage accounts
|
||||
internal BlobContainerClient BaseGetContainerClient(string accountName)
|
||||
{
|
||||
return base.GetContainerClient(accountName);
|
||||
}
|
||||
}
|
||||
|
||||
// All dependencies of SingletonManager is mocked except Blob storage
|
||||
public SingletonManagerStorageIntegrationTests()
|
||||
{
|
||||
ILoggerFactory loggerFactory = new LoggerFactory();
|
||||
_loggerProvider = new TestLoggerProvider();
|
||||
loggerFactory.AddProvider(_loggerProvider);
|
||||
|
||||
var testBlobLeaseDistributedLockManager = new TestBlobLeaseDistributedLockManager(loggerFactory, null);
|
||||
var blobServiceClient = TestHelpers.GetTestBlobServiceClient();
|
||||
|
||||
_testContainerClient = blobServiceClient.GetBlobContainerClient(new RandomNameResolver().ResolveInString(TestArtifactContainerName));
|
||||
testBlobLeaseDistributedLockManager.ContainerClient = _testContainerClient;
|
||||
_core = testBlobLeaseDistributedLockManager;
|
||||
|
||||
_mockExceptionDispatcher = new Mock<IWebJobsExceptionHandler>(MockBehavior.Strict);
|
||||
|
||||
_singletonConfig = new SingletonOptions();
|
||||
|
||||
// use reflection to bypass the normal validations (so tests can run fast)
|
||||
TestHelpers.SetField(_singletonConfig, "_lockAcquisitionPollingInterval", TimeSpan.FromMilliseconds(500));
|
||||
TestHelpers.SetField(_singletonConfig, "_lockPeriod", TimeSpan.FromSeconds(15));
|
||||
_singletonConfig.LockAcquisitionTimeout = TimeSpan.FromSeconds(16);
|
||||
|
||||
_nameResolver = new TestNameResolver();
|
||||
|
||||
_singletonManager = new SingletonManager(_core, new OptionsWrapper<SingletonOptions>(_singletonConfig), _mockExceptionDispatcher.Object, loggerFactory, new FixedHostIdProvider(TestHostId), _nameResolver);
|
||||
|
||||
_singletonManager.MinimumLeaseRenewalInterval = TimeSpan.FromMilliseconds(250);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _testContainerClient.DeleteIfExistsAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryLockAsync_CreatesBlob_WhenItDoesNotExist()
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
|
||||
// Create the container as this is only testing that the blob is created
|
||||
await _testContainerClient.CreateIfNotExistsAsync();
|
||||
|
||||
await foreach (var _ in _testContainerClient.GetBlobsAsync())
|
||||
{
|
||||
Assert.False(true, "Blob already exists. This shouldn't happen.");
|
||||
}
|
||||
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
RenewableLockHandle lockHandle = await _singletonManager.TryLockAsync(TestLockId, TestInstanceId, attribute, cancellationToken);
|
||||
var innerHandle = lockHandle.GetInnerHandle();
|
||||
|
||||
Assert.Equal(TestLockId, innerHandle.LockId);
|
||||
|
||||
// Shouldn't be able to lease this blob
|
||||
var exception = await Assert.ThrowsAnyAsync<RequestFailedException>(async () => await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15)));
|
||||
Assert.Equal(409, exception.Status); // Conflict
|
||||
|
||||
// Checks Instance Id in metadata
|
||||
var metadata = (await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetPropertiesAsync()).Value.Metadata;
|
||||
Assert.Single(metadata.Keys);
|
||||
Assert.Equal(TestInstanceId, metadata[BlobLeaseDistributedLockManager.FunctionInstanceMetadataKey]);
|
||||
Assert.NotNull(lockHandle);
|
||||
Assert.NotNull(lockHandle.LeaseRenewalTimer);
|
||||
|
||||
// Verify the leaseId in the innerHandle is the actual active LeaseId. Otherwise, ChangeAsync will fail.
|
||||
await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient(innerHandle.LeaseId).ChangeAsync(Guid.NewGuid().ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryLockAsync_CreatesBlobContainer_WhenItDoesNotExist()
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
|
||||
Assert.False(_testContainerClient.Exists());
|
||||
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
RenewableLockHandle lockHandle = await _singletonManager.TryLockAsync(TestLockId, TestInstanceId, attribute, cancellationToken);
|
||||
var innerHandle = lockHandle.GetInnerHandle();
|
||||
|
||||
Assert.Equal(TestLockId, innerHandle.LockId);
|
||||
|
||||
// Shouldn't be able to lease this blob
|
||||
var exception = await Assert.ThrowsAnyAsync<RequestFailedException>(async () => await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15)));
|
||||
Assert.Equal(409, exception.Status); // Conflict
|
||||
|
||||
// Checks Instance Id in metadata
|
||||
var metadata = (await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetPropertiesAsync()).Value.Metadata;
|
||||
Assert.Single(metadata.Keys);
|
||||
Assert.Equal(TestInstanceId, metadata[BlobLeaseDistributedLockManager.FunctionInstanceMetadataKey]);
|
||||
Assert.NotNull(lockHandle);
|
||||
Assert.NotNull(lockHandle.LeaseRenewalTimer);
|
||||
|
||||
// Verify the leaseId in the innerHandle is the actual active LeaseId. Otherwise, ChangeAsync will fail.
|
||||
await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient(innerHandle.LeaseId).ChangeAsync(Guid.NewGuid().ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryLockAsync_CreatesBlobLease_WithAutoRenewal()
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
var lockHandle = await _singletonManager.TryLockAsync(TestLockId, TestInstanceId, attribute, cancellationToken);
|
||||
var innerHandle = lockHandle.GetInnerHandle();
|
||||
|
||||
Assert.Equal(TestLockId, innerHandle.LockId);
|
||||
|
||||
// Shouldn't be able to lease this blob yet
|
||||
var exception = await Assert.ThrowsAnyAsync<RequestFailedException>(async () => await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15)));
|
||||
Assert.Equal(409, exception.Status); // Conflict
|
||||
|
||||
// Wait for enough time that we expect some lease renewals to occur
|
||||
var duration = _singletonConfig.LockPeriod.TotalMilliseconds + TimeSpan.FromSeconds(1).TotalMilliseconds;
|
||||
await Task.Delay((int)duration);
|
||||
|
||||
// Still shouldn't be able to lease this blob yet
|
||||
exception = await Assert.ThrowsAnyAsync<RequestFailedException>(async () => await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15)));
|
||||
Assert.Equal(409, exception.Status); // Conflict
|
||||
|
||||
// Now release the lock and verify no more renewals
|
||||
await _singletonManager.ReleaseLockAsync(lockHandle, cancellationToken);
|
||||
|
||||
// Should be able to lease this blob now in the test context
|
||||
await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15));
|
||||
|
||||
// verify the logger
|
||||
TestLogger logger = _loggerProvider.CreatedLoggers.Single() as TestLogger;
|
||||
Assert.Equal(LogCategories.Singleton, logger.Category);
|
||||
var messages = logger.GetLogMessages();
|
||||
Assert.Equal(2, messages.Count);
|
||||
Assert.NotNull(messages.Single(m => m.Level == Microsoft.Extensions.Logging.LogLevel.Debug && m.FormattedMessage == "Singleton lock acquired (testid)"));
|
||||
Assert.NotNull(messages.Single(m => m.Level == Microsoft.Extensions.Logging.LogLevel.Debug && m.FormattedMessage == "Singleton lock released (testid)"));
|
||||
|
||||
Assert.NotNull(lockHandle);
|
||||
Assert.NotNull(lockHandle.LeaseRenewalTimer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryLockAsync_WithContention_PollsForLease()
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
|
||||
// Setup test so that the blob exists
|
||||
await _testContainerClient.CreateIfNotExistsAsync();
|
||||
if (!_testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).Exists())
|
||||
{
|
||||
await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).UploadTextAsync("", overwrite: true);
|
||||
}
|
||||
|
||||
// Lease this blob in the test context to test polling behavior of the SingletonManager
|
||||
await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15));
|
||||
|
||||
// SingletonManager will be trying to poll for the lock and eventually get it after 15 seconds
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
var lockHandle = await _singletonManager.TryLockAsync(TestLockId, TestInstanceId, attribute, cancellationToken);
|
||||
var innerHandle = lockHandle.GetInnerHandle();
|
||||
Assert.Equal(TestLockId, innerHandle.LockId);
|
||||
|
||||
// Shouldn't be able to lease this blob since the SingletonManager has the lease
|
||||
var exception = await Assert.ThrowsAnyAsync<RequestFailedException>(async () => await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15)));
|
||||
Assert.Equal(409, exception.Status); // Conflict
|
||||
|
||||
// Checks Instance Id in metadata
|
||||
var metadata = (await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetPropertiesAsync()).Value.Metadata;
|
||||
Assert.Single(metadata.Keys);
|
||||
Assert.Equal(TestInstanceId, metadata[BlobLeaseDistributedLockManager.FunctionInstanceMetadataKey]);
|
||||
Assert.NotNull(lockHandle);
|
||||
Assert.NotNull(lockHandle.LeaseRenewalTimer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryLockAsync_WithContention_NoRetry_DoesNotPollForLease()
|
||||
{
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
|
||||
// Setup test so that the blob exists
|
||||
await _testContainerClient.CreateIfNotExistsAsync();
|
||||
if (!_testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).Exists())
|
||||
{
|
||||
await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).UploadTextAsync("", overwrite: true);
|
||||
}
|
||||
|
||||
// Lease this blob in the test context
|
||||
var lease = await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15));
|
||||
|
||||
// SingletonManager will not poll for the lease; test should end quickly
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
var lockHandle = await _singletonManager.TryLockAsync(TestLockId, TestInstanceId, attribute, cancellationToken, retry: false);
|
||||
|
||||
Assert.Null(lockHandle);
|
||||
});
|
||||
|
||||
if (task.Wait(1500))
|
||||
{
|
||||
await task;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException("Test ran too long. The SingletonManager is likely retrying to get lease");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LockAsync_WithContention_AcquisitionTimeoutExpires_Throws()
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
|
||||
// Setup test so that the blob exists
|
||||
await _testContainerClient.CreateIfNotExistsAsync();
|
||||
if (!_testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).Exists())
|
||||
{
|
||||
await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).UploadTextAsync("", overwrite: true);
|
||||
}
|
||||
|
||||
// Lease this blob in the test context
|
||||
var lease = await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15));
|
||||
|
||||
// Failure to acquire lock is expected with a custom error message
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
attribute.LockAcquisitionTimeout = 1;
|
||||
TimeoutException exception = await Assert.ThrowsAsync<TimeoutException>(async () => await _singletonManager.LockAsync(TestLockId, TestInstanceId, attribute, cancellationToken));
|
||||
Assert.Equal("Unable to acquire singleton lock blob lease for blob 'testid' (timeout of 0:00:01 exceeded).", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReleaseLockAsync_StopsRenewalTimerAndReleasesLease()
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
var lockHandle = await _singletonManager.TryLockAsync(TestLockId, TestInstanceId, attribute, cancellationToken);
|
||||
var innerHandle = lockHandle.GetInnerHandle();
|
||||
|
||||
await _singletonManager.ReleaseLockAsync(lockHandle, cancellationToken);
|
||||
|
||||
// Timer should've been stopped
|
||||
await Assert.ThrowsAnyAsync<InvalidOperationException>(async () => await lockHandle.LeaseRenewalTimer.StopAsync(cancellationToken));
|
||||
|
||||
// Verify lease has been released by SingletonManager
|
||||
var lease = await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLockOwnerAsync_LeaseLocked_ReturnsOwner()
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
|
||||
Assert.False(_testContainerClient.Exists());
|
||||
|
||||
// No owner yet and no blob/container
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
string lockOwner = await _singletonManager.GetLockOwnerAsync(attribute, TestLockId, cancellationToken);
|
||||
Assert.Equal(null, lockOwner);
|
||||
|
||||
await _testContainerClient.CreateIfNotExistsAsync();
|
||||
if (!_testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).Exists())
|
||||
{
|
||||
await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).UploadTextAsync("", overwrite: true);
|
||||
}
|
||||
|
||||
var properties = (await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetPropertiesAsync()).Value;
|
||||
Assert.Equal(LeaseState.Available, properties.LeaseState);
|
||||
Assert.Equal(LeaseStatus.Unlocked, properties.LeaseStatus);
|
||||
|
||||
// No owner but blob/container exists
|
||||
lockOwner = await _singletonManager.GetLockOwnerAsync(attribute, TestLockId, cancellationToken);
|
||||
Assert.Equal(null, lockOwner);
|
||||
|
||||
// Get a lease
|
||||
var lockHandle = await _singletonManager.TryLockAsync(TestLockId, TestInstanceId, attribute, cancellationToken);
|
||||
|
||||
// There should be an owner
|
||||
lockOwner = await _singletonManager.GetLockOwnerAsync(attribute, TestLockId, cancellationToken);
|
||||
Assert.Equal(TestInstanceId, lockOwner);
|
||||
|
||||
// Shouldn't be able to lease this blob
|
||||
await Assert.ThrowsAnyAsync<Exception>(async () => await _testContainerClient.GetBlobClient(string.Format("locks/{0}", TestLockId)).GetBlobLeaseClient().AcquireAsync(TimeSpan.FromSeconds(15)));
|
||||
}
|
||||
|
||||
private class TestNameResolver : INameResolver
|
||||
{
|
||||
public TestNameResolver()
|
||||
{
|
||||
Names = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public Dictionary<string, string> Names { get; private set; }
|
||||
|
||||
public string Resolve(string name)
|
||||
{
|
||||
if (Names.TryGetValue(name, out string value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
throw new NotSupportedException(string.Format("Cannot resolve name: '{0}'", name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,16 +3,19 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FakeStorage;
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.Storage.Blob;
|
||||
using Azure;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Azure.Storage.Blobs.Specialized;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.Indexers;
|
||||
using Microsoft.Azure.WebJobs.Host.Protocols;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Azure.WebJobs.Host.Timers;
|
||||
using Microsoft.Azure.WebJobs.Logging;
|
||||
|
@ -21,7 +24,7 @@ using Microsoft.Extensions.Logging;
|
|||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using SingletonLockHandle = Microsoft.Azure.WebJobs.Host.StorageBaseDistributedLockManager.SingletonLockHandle;
|
||||
using SingletonLockHandle = Microsoft.Azure.WebJobs.Host.BlobLeaseDistributedLockManager.SingletonLockHandle;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
||||
{
|
||||
|
@ -54,46 +57,118 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
|||
|
||||
private readonly IConfiguration _configuration = new ConfigurationBuilder().Build();
|
||||
|
||||
private StorageBaseDistributedLockManager _core;
|
||||
private BlobLeaseDistributedLockManager _core;
|
||||
private SingletonManager _singletonManager;
|
||||
private SingletonOptions _singletonConfig;
|
||||
|
||||
private CloudBlobDirectory _mockBlobDirectory;
|
||||
private CloudBlobDirectory _mockSecondaryBlobDirectory;
|
||||
|
||||
internal FakeAccount _account1 = new FakeAccount();
|
||||
internal FakeAccount _account2 = new FakeAccount();
|
||||
private MockAzureBlobStorageAccount _account = new MockAzureBlobStorageAccount(ConnectionStringNames.Storage);
|
||||
|
||||
private Mock<IWebJobsExceptionHandler> _mockExceptionDispatcher;
|
||||
private Mock<CloudBlockBlob> _mockStorageBlob;
|
||||
private TestLoggerProvider _loggerProvider;
|
||||
private readonly Dictionary<string, string> _mockBlobMetadata;
|
||||
private TestNameResolver _nameResolver;
|
||||
|
||||
private class FakeLeaseProvider : StorageBaseDistributedLockManager
|
||||
private class FakeLeaseProvider : BlobLeaseDistributedLockManager
|
||||
{
|
||||
internal FakeAccount _account1 = new FakeAccount();
|
||||
internal FakeAccount _account2 = new FakeAccount();
|
||||
private readonly MockAzureBlobStorageAccount _account;
|
||||
|
||||
public FakeLeaseProvider(ILoggerFactory logger) : base(logger) { }
|
||||
|
||||
protected override CloudBlobContainer GetContainer(string accountName)
|
||||
public FakeLeaseProvider(ILoggerFactory logger,
|
||||
IAzureBlobStorageProvider azureBlobStorageProvider,
|
||||
MockAzureBlobStorageAccount account)
|
||||
: base(logger, azureBlobStorageProvider)
|
||||
{
|
||||
FakeAccount account;
|
||||
if (string.IsNullOrEmpty(accountName) || accountName == ConnectionStringNames.Storage)
|
||||
_account = account;
|
||||
}
|
||||
|
||||
protected override BlobContainerClient GetContainerClient(string accountName)
|
||||
{
|
||||
return _account.BlobContainerClient.Object;
|
||||
}
|
||||
|
||||
protected override BlobLeaseClient GetBlobLeaseClient(BlobClient blobClient, string proposedLeaseId)
|
||||
{
|
||||
return _account.BlobLeaseClient.Object;
|
||||
}
|
||||
|
||||
protected override BlobContainerClient GetParentBlobContainerClient(BlobClient blobClient)
|
||||
{
|
||||
return _account.BlobContainerClient.Object;
|
||||
}
|
||||
|
||||
// Testing the base method to verify we can use multiple storage accounts
|
||||
internal BlobContainerClient BaseGetContainerClient(string accountName)
|
||||
{
|
||||
return base.GetContainerClient(accountName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the mocking layer for Azure Blob storage objects. This private class extensively relies on reflection to construct and edit
|
||||
/// classes in <see cref="Azure.Storage.Blobs.Models"/> since they have internal constructors and no setters.
|
||||
/// Relationship is BlobContainerClient <-- has --> BlobClient <-- has --> BlobLeaseClient.
|
||||
/// The tests in <see cref="SingletonManagerTests"/> use this class as a utility.
|
||||
/// </summary>
|
||||
private class MockAzureBlobStorageAccount
|
||||
{
|
||||
public MockAzureBlobStorageAccount(string accountName)
|
||||
{
|
||||
AccountName = accountName;
|
||||
|
||||
BlobClient = new Mock<BlobClient>(MockBehavior.Strict);
|
||||
BlobContainerClient = new Mock<BlobContainerClient>(MockBehavior.Strict);
|
||||
BlobContainerClient.Setup(b => b.GetBlobClient(It.IsAny<string>())).Returns(BlobClient.Object);
|
||||
|
||||
BlobLeaseClient = new Mock<BlobLeaseClient>(MockBehavior.Strict);
|
||||
}
|
||||
|
||||
public string AccountName { get; private set; }
|
||||
|
||||
public Mock<BlobContainerClient> BlobContainerClient { get; private set; }
|
||||
|
||||
public Mock<BlobClient> BlobClient { get; private set; }
|
||||
|
||||
public Mock<BlobLeaseClient> BlobLeaseClient { get; private set; }
|
||||
|
||||
public IDictionary<string, string> LastSetMetadata { get; set; }
|
||||
|
||||
public Response<ReleasedObjectInfo> CreateReleasedObjectInfoResponse()
|
||||
{
|
||||
var blobInfo = CreateInternalObjectHelper<BlobInfo>();
|
||||
ConstructorInfo c = typeof(ReleasedObjectInfo).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[1] { typeof(BlobInfo) }, null);
|
||||
var releasedObjectInfo = (ReleasedObjectInfo)c.Invoke(BindingFlags.NonPublic, null, new object[1] { blobInfo }, null);
|
||||
return Response.FromValue(releasedObjectInfo, default);
|
||||
}
|
||||
|
||||
public Response<T> CreateInternalAzureBlobResponse<T>(Dictionary<string, object> fields = null)
|
||||
{
|
||||
T obj = CreateInternalObjectHelper<T>();
|
||||
if (fields != null)
|
||||
{
|
||||
account = _account1;
|
||||
var allFields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
var f = allFields.Where(f => f.Name.Contains(field.Key)).SingleOrDefault();
|
||||
f.SetValue(obj, field.Value);
|
||||
}
|
||||
}
|
||||
else if (accountName == Secondary)
|
||||
|
||||
return Response.FromValue(obj, default);
|
||||
}
|
||||
|
||||
private static T CreateInternalObjectHelper<T>()
|
||||
{
|
||||
ConstructorInfo c = typeof(T).GetConstructor(new Type[0]) ?? typeof(T).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[0], null);
|
||||
return (T)c.Invoke(BindingFlags.NonPublic, null, null, null);
|
||||
}
|
||||
|
||||
public static Dictionary<string, object> AddObjectField(Dictionary<string, object> fields, string fieldName, object fieldValue)
|
||||
{
|
||||
fields = fields ?? new Dictionary<string, object>();
|
||||
if (!fields.ContainsKey(fieldName))
|
||||
{
|
||||
account = _account2;
|
||||
fields[fieldName] = fieldValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unknown account: " + accountName);
|
||||
}
|
||||
var container = account.CreateCloudBlobClient().GetContainerReference("azure-webjobs-hosts");
|
||||
return container;
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,20 +180,10 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
|||
|
||||
var logger = loggerFactory?.CreateLogger(LogCategories.Singleton);
|
||||
|
||||
|
||||
var leaseProvider = new FakeLeaseProvider(loggerFactory);
|
||||
_mockBlobDirectory = leaseProvider._account1.CreateCloudBlobClient().GetContainerReference(HostContainerNames.Hosts).GetDirectoryReference(HostDirectoryNames.SingletonLocks);
|
||||
_mockSecondaryBlobDirectory = leaseProvider._account2.CreateCloudBlobClient().GetContainerReference(HostContainerNames.Hosts).GetDirectoryReference(HostDirectoryNames.SingletonLocks);
|
||||
var leaseProvider = new FakeLeaseProvider(loggerFactory, null, _account);
|
||||
|
||||
_mockExceptionDispatcher = new Mock<IWebJobsExceptionHandler>(MockBehavior.Strict);
|
||||
|
||||
_mockStorageBlob = new Mock<CloudBlockBlob>(MockBehavior.Strict,
|
||||
new Uri("https://fakeaccount.blob.core.windows.net/" + HostContainerNames.Hosts + "/" + HostDirectoryNames.SingletonLocks + "/" + TestLockId));
|
||||
|
||||
_mockBlobMetadata = new Dictionary<string, string>();
|
||||
leaseProvider._account1.SetBlob(HostContainerNames.Hosts, HostDirectoryNames.SingletonLocks + "/" + TestLockId, _mockStorageBlob.Object);
|
||||
|
||||
|
||||
_singletonConfig = new SingletonOptions();
|
||||
|
||||
// use reflection to bypass the normal validations (so tests can run fast)
|
||||
|
@ -127,90 +192,136 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
|||
_singletonConfig.LockAcquisitionTimeout = TimeSpan.FromMilliseconds(200);
|
||||
|
||||
_nameResolver = new TestNameResolver();
|
||||
|
||||
_core = leaseProvider;
|
||||
|
||||
_singletonManager = new SingletonManager(_core, new OptionsWrapper<SingletonOptions>(_singletonConfig), _mockExceptionDispatcher.Object, loggerFactory, new FixedHostIdProvider(TestHostId), _nameResolver);
|
||||
|
||||
_singletonManager.MinimumLeaseRenewalInterval = TimeSpan.FromMilliseconds(250);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLockDirectory_HandlesMultipleAccounts()
|
||||
public void GetLockPath()
|
||||
{
|
||||
var directory = _core.GetLockDirectory(ConnectionStringNames.Storage);
|
||||
Assert.Equal(_mockBlobDirectory.Uri, directory.Uri);
|
||||
var path = _core.GetLockPath(ConnectionStringNames.Storage);
|
||||
var expectedPath = string.Format("{0}/{1}", HostDirectoryNames.SingletonLocks, ConnectionStringNames.Storage);
|
||||
Assert.Equal(expectedPath, path);
|
||||
}
|
||||
|
||||
directory = _core.GetLockDirectory(null);
|
||||
Assert.Equal(_mockBlobDirectory.Uri, directory.Uri);
|
||||
// This test should not make any calls to storage. We are just creating clients.
|
||||
[Fact]
|
||||
public void GetContainerClient_SupportsMultipleAccounts()
|
||||
{
|
||||
var blobStorageProvider = TestHelpers.GetTestAzureBlobStorageProvider();
|
||||
var blobLockManager = new FakeLeaseProvider(new LoggerFactory(), blobStorageProvider, null);
|
||||
|
||||
directory = _core.GetLockDirectory(Secondary);
|
||||
Assert.Equal(_mockSecondaryBlobDirectory.Uri, directory.Uri);
|
||||
// Testing primary account
|
||||
blobStorageProvider.TryCreateBlobServiceClientFromConnection(ConnectionStringNames.Storage, out BlobServiceClient blobServiceClient);
|
||||
var accountContainer = blobLockManager.BaseGetContainerClient(ConnectionStringNames.Storage);
|
||||
Assert.Equal(blobServiceClient.AccountName, accountContainer.AccountName);
|
||||
|
||||
// Testing primary account by providing null accountName argument
|
||||
blobStorageProvider.TryCreateBlobServiceClientFromConnection(ConnectionStringNames.Storage, out blobServiceClient);
|
||||
accountContainer = blobLockManager.BaseGetContainerClient(null);
|
||||
Assert.Equal(blobServiceClient.AccountName, accountContainer.AccountName);
|
||||
|
||||
// Testing secondary account
|
||||
blobStorageProvider.TryCreateBlobServiceClientFromConnection(Secondary, out blobServiceClient);
|
||||
accountContainer = blobLockManager.BaseGetContainerClient(Secondary);
|
||||
Assert.Equal(blobServiceClient.AccountName, accountContainer.AccountName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryLockAsync_CreatesBlob_WhenItDoesNotExist()
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
RequestResult storageResult = new RequestResult
|
||||
{
|
||||
HttpStatusCode = 404
|
||||
};
|
||||
StorageException storageException = new StorageException(storageResult, null, null);
|
||||
RequestFailedException storageException = new RequestFailedException(404, "Not Found Test Error");
|
||||
|
||||
var mockAccount = _account;
|
||||
|
||||
int count = 0;
|
||||
|
||||
MockAcquireLeaseAsync(null, () =>
|
||||
MockAcquireLeaseAsync(mockAccount, null, () =>
|
||||
{
|
||||
if (count++ == 0)
|
||||
{
|
||||
throw storageException;
|
||||
}
|
||||
return TestLeaseId;
|
||||
|
||||
var fields = new Dictionary<string, object>()
|
||||
{
|
||||
{ "LeaseId", TestLeaseId },
|
||||
};
|
||||
return mockAccount.CreateInternalAzureBlobResponse<BlobLease>(fields);
|
||||
});
|
||||
|
||||
_mockStorageBlob.Setup(p => p.UploadTextAsync(string.Empty, cancellationToken)).Returns(Task.FromResult(true));
|
||||
//_mockStorageBlob.SetupGet(p => p.Metadata).Returns(_mockBlobMetadata);
|
||||
_mockStorageBlob.Setup(p => p.SetMetadataAsync(It.Is<AccessCondition>(q => q.LeaseId == TestLeaseId), null, null, cancellationToken)).Returns(Task.FromResult(true));
|
||||
_mockStorageBlob.Setup(p => p.ReleaseLeaseAsync(It.Is<AccessCondition>(q => q.LeaseId == TestLeaseId), null, null, cancellationToken)).Returns(Task.FromResult(true));
|
||||
// First call to create the blob should fail because container does not exist
|
||||
int uploadAsyncCallCount = 0;
|
||||
mockAccount.BlobContainerClient.Setup(p => p.CreateIfNotExistsAsync(It.IsAny<PublicAccessType>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<BlobContainerEncryptionScopeOptions>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobContainerInfo>()));
|
||||
mockAccount.BlobClient.Setup(p => p.UploadAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(() =>
|
||||
{
|
||||
if (uploadAsyncCallCount++ == 0)
|
||||
{
|
||||
throw storageException;
|
||||
}
|
||||
return Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobContentInfo>());
|
||||
});
|
||||
mockAccount.BlobClient.Setup(p => p.GetPropertiesAsync(It.IsAny<BlobRequestConditions>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobProperties>()));
|
||||
mockAccount.BlobClient.Setup(p => p.SetMetadataAsync(It.IsAny<IDictionary<string, string>>(), It.Is<BlobRequestConditions>(q => q.LeaseId == TestLeaseId), It.IsAny<CancellationToken>()))
|
||||
.Callback<IDictionary<string, string>, BlobRequestConditions, CancellationToken>((metadata, conditions, cancellationToken) => mockAccount.LastSetMetadata = metadata)
|
||||
.Returns(Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobInfo>()));
|
||||
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
RenewableLockHandle lockHandle = await _singletonManager.TryLockAsync(TestLockId, TestInstanceId, attribute, cancellationToken);
|
||||
var innerHandle = lockHandle.GetInnerHandle();
|
||||
|
||||
Assert.Equal(_mockStorageBlob.Object, innerHandle.Blob);
|
||||
Assert.Equal(mockAccount.BlobLeaseClient.Object, innerHandle.BlobLeaseClient);
|
||||
Assert.Equal(TestLeaseId, innerHandle.LeaseId);
|
||||
Assert.Equal(1, _mockStorageBlob.Object.Metadata.Keys.Count);
|
||||
Assert.Equal(_mockStorageBlob.Object.Metadata[StorageBaseDistributedLockManager.FunctionInstanceMetadataKey], TestInstanceId);
|
||||
Assert.Equal(1, mockAccount.LastSetMetadata.Keys.Count);
|
||||
Assert.Equal(mockAccount.LastSetMetadata[BlobLeaseDistributedLockManager.FunctionInstanceMetadataKey], TestInstanceId);
|
||||
|
||||
mockAccount.BlobContainerClient.VerifyAll();
|
||||
mockAccount.BlobClient.VerifyAll();
|
||||
mockAccount.BlobLeaseClient.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryLockAsync_CreatesBlobLease_WithAutoRenewal()
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
//_mockStorageBlob.SetupGet(p => p.Metadata).Returns(_mockBlobMetadata);
|
||||
|
||||
MockAcquireLeaseAsync(null, () => TestLeaseId);
|
||||
var mockAccount = _account;
|
||||
MockAcquireLeaseAsync(mockAccount, null, () =>
|
||||
{
|
||||
var fields = new Dictionary<string, object>()
|
||||
{
|
||||
{ "LeaseId", TestLeaseId },
|
||||
};
|
||||
return mockAccount.CreateInternalAzureBlobResponse<BlobLease>(fields);
|
||||
});
|
||||
|
||||
_mockStorageBlob.Setup(p => p.SetMetadataAsync(It.Is<AccessCondition>(q => q.LeaseId == TestLeaseId), null, null, cancellationToken)).Returns(Task.FromResult(true));
|
||||
_mockStorageBlob.Setup(p => p.ReleaseLeaseAsync(It.Is<AccessCondition>(q => q.LeaseId == TestLeaseId), null, null, cancellationToken)).Returns(Task.FromResult(true));
|
||||
mockAccount.BlobClient.Setup(p => p.GetPropertiesAsync(It.IsAny<BlobRequestConditions>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobProperties>()));
|
||||
mockAccount.BlobClient.Setup(p => p.SetMetadataAsync(It.IsAny<IDictionary<string, string>>(), It.Is<BlobRequestConditions>(q => q.LeaseId == TestLeaseId), It.IsAny<CancellationToken>()))
|
||||
.Callback<IDictionary<string, string>, BlobRequestConditions, CancellationToken>((metadata, conditions, cancellationToken) => mockAccount.LastSetMetadata = metadata)
|
||||
.Returns(Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobInfo>()));
|
||||
mockAccount.BlobLeaseClient.Setup(p => p.ReleaseAsync(It.IsAny<RequestConditions>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockAccount.CreateReleasedObjectInfoResponse()));
|
||||
|
||||
int renewCount = 0;
|
||||
_mockStorageBlob.Setup(p => p.RenewLeaseAsync(It.Is<AccessCondition>(q => q.LeaseId == TestLeaseId), null, null, It.IsAny<CancellationToken>()))
|
||||
.Callback<AccessCondition, BlobRequestOptions, OperationContext, CancellationToken>(
|
||||
(mockAccessCondition, mockOptions, mockContext, mockCancellationToken) =>
|
||||
|
||||
mockAccount.BlobLeaseClient.Setup(p => p.RenewAsync(It.IsAny<RequestConditions>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<RequestConditions, CancellationToken>((requestCondition, cancellationToken) =>
|
||||
{
|
||||
renewCount++;
|
||||
}).Returns(Task.FromResult(true));
|
||||
})
|
||||
.Returns(Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobLease>()));
|
||||
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
var lockHandle = await _singletonManager.TryLockAsync(TestLockId, TestInstanceId, attribute, cancellationToken);
|
||||
var innerHandle = lockHandle.GetInnerHandle();
|
||||
|
||||
Assert.Equal(_mockStorageBlob.Object, innerHandle.Blob);
|
||||
Assert.Equal(mockAccount.BlobLeaseClient.Object, innerHandle.BlobLeaseClient);
|
||||
Assert.Equal(TestLeaseId, innerHandle.LeaseId);
|
||||
Assert.Equal(1, _mockStorageBlob.Object.Metadata.Keys.Count);
|
||||
Assert.Equal(_mockStorageBlob.Object.Metadata[StorageBaseDistributedLockManager.FunctionInstanceMetadataKey], TestInstanceId);
|
||||
Assert.Equal(1, mockAccount.LastSetMetadata.Keys.Count);
|
||||
Assert.Equal(mockAccount.LastSetMetadata[BlobLeaseDistributedLockManager.FunctionInstanceMetadataKey], TestInstanceId);
|
||||
|
||||
// wait for enough time that we expect some lease renewals to occur
|
||||
int duration = 2000;
|
||||
|
@ -235,23 +346,37 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
|||
|
||||
Assert.Equal(0, renewCount);
|
||||
|
||||
_mockStorageBlob.VerifyAll();
|
||||
mockAccount.BlobClient.VerifyAll();
|
||||
mockAccount.BlobLeaseClient.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryLockAsync_WithContention_PollsForLease()
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
// _mockStorageBlob.SetupGet(p => p.Metadata).Returns(_mockBlobMetadata);
|
||||
_mockStorageBlob.Setup(p => p.SetMetadataAsync(It.Is<AccessCondition>(q => q.LeaseId == TestLeaseId), null, null, cancellationToken)).Returns(Task.FromResult(true));
|
||||
|
||||
var mockAccount = _account;
|
||||
mockAccount.BlobClient.Setup(p => p.GetPropertiesAsync(It.IsAny<BlobRequestConditions>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobProperties>()));
|
||||
mockAccount.BlobClient.Setup(p => p.SetMetadataAsync(It.IsAny<IDictionary<string, string>>(), It.Is<BlobRequestConditions>(q => q.LeaseId == TestLeaseId), It.IsAny<CancellationToken>()))
|
||||
.Callback<IDictionary<string, string>, BlobRequestConditions, CancellationToken>((metadata, conditions, cancellationToken) => mockAccount.LastSetMetadata = metadata)
|
||||
.Returns(Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobInfo>()));
|
||||
|
||||
int numRetries = 3;
|
||||
int count = 0;
|
||||
|
||||
MockAcquireLeaseAsync(null, () =>
|
||||
MockAcquireLeaseAsync(mockAccount, null, () =>
|
||||
{
|
||||
count++;
|
||||
return count > numRetries ? TestLeaseId : null;
|
||||
if (count > numRetries)
|
||||
{
|
||||
var fields = new Dictionary<string, object>()
|
||||
{
|
||||
{ "LeaseId", TestLeaseId },
|
||||
};
|
||||
return mockAccount.CreateInternalAzureBlobResponse<BlobLease>(fields);
|
||||
}
|
||||
|
||||
throw new RequestFailedException(409, "Failed to get lease in test");
|
||||
});
|
||||
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
|
@ -263,7 +388,8 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
|||
Assert.Equal(numRetries, count - 1);
|
||||
Assert.NotNull(lockHandle.LeaseRenewalTimer);
|
||||
|
||||
_mockStorageBlob.VerifyAll();
|
||||
mockAccount.BlobClient.VerifyAll();
|
||||
mockAccount.BlobLeaseClient.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -272,11 +398,11 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
|||
CancellationToken cancellationToken = new CancellationToken();
|
||||
|
||||
int count = 0;
|
||||
|
||||
MockAcquireLeaseAsync(null, () =>
|
||||
var mockAccount = _account;
|
||||
MockAcquireLeaseAsync(mockAccount, null, () =>
|
||||
{
|
||||
count++;
|
||||
return null;
|
||||
throw new RequestFailedException(409, "Failed to get lease in test");
|
||||
});
|
||||
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
|
@ -285,38 +411,21 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
|||
Assert.Null(lockHandle);
|
||||
Assert.Equal(1, count);
|
||||
|
||||
_mockStorageBlob.VerifyAll();
|
||||
mockAccount.BlobClient.VerifyAll();
|
||||
mockAccount.BlobLeaseClient.VerifyAll();
|
||||
}
|
||||
|
||||
// Helper to setup mock since the signatures are very complex
|
||||
private void MockAcquireLeaseAsync(Action fpAction, Func<string> returns)
|
||||
private void MockAcquireLeaseAsync(MockAzureBlobStorageAccount mockAccount, Action fpAction, Func<Response<BlobLease>> returns)
|
||||
{
|
||||
_mockStorageBlob.Setup(
|
||||
p => p.AcquireLeaseAsync(_singletonConfig.LockPeriod, null, It.IsAny<AccessCondition>(), It.IsAny<BlobRequestOptions>(), It.IsAny<OperationContext>(), It.IsAny<CancellationToken>())
|
||||
)
|
||||
.Callback<TimeSpan?, string, AccessCondition, BlobRequestOptions, OperationContext, CancellationToken>(
|
||||
(mockPeriod, mockLeaseId, accessCondition, blobRequest, opCtx, cancelToken) =>
|
||||
mockAccount.BlobLeaseClient.Setup(p => p.AcquireAsync(_singletonConfig.LockPeriod, It.IsAny<RequestConditions>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<TimeSpan, RequestConditions, CancellationToken>((timeSpan, requestConditions, cancellationToken) =>
|
||||
{
|
||||
fpAction?.Invoke();
|
||||
}).Returns(() =>
|
||||
{
|
||||
var retResult = returns();
|
||||
return Task.FromResult<string>(retResult);
|
||||
});
|
||||
}
|
||||
|
||||
private void MockFetchAttributesAsync(Action fpAction)
|
||||
{
|
||||
_mockStorageBlob.Setup(
|
||||
p => p.FetchAttributesAsync(It.IsAny<AccessCondition>(), It.IsAny<BlobRequestOptions>(), It.IsAny<OperationContext>(), It.IsAny<CancellationToken>())
|
||||
)
|
||||
.Callback<AccessCondition, BlobRequestOptions, OperationContext, CancellationToken>(
|
||||
(accessCondition, blobRequest, opCtx, cancelToken) =>
|
||||
{
|
||||
fpAction?.Invoke();
|
||||
}).Returns(() =>
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
return Task.FromResult(retResult);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -326,11 +435,11 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
|||
CancellationToken cancellationToken = new CancellationToken();
|
||||
|
||||
int count = 0;
|
||||
|
||||
MockAcquireLeaseAsync(() =>
|
||||
var mockAccount = _account;
|
||||
MockAcquireLeaseAsync(mockAccount, () =>
|
||||
{
|
||||
++count;
|
||||
}, () => null);
|
||||
}, () => throw new RequestFailedException(409, "Failed to get lease in test"));
|
||||
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
TimeoutException exception = await Assert.ThrowsAsync<TimeoutException>(async () => await _singletonManager.LockAsync(TestLockId, TestInstanceId, attribute, cancellationToken));
|
||||
|
@ -339,23 +448,25 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
|||
Assert.Equal(expectedRetryCount, count - 1);
|
||||
Assert.Equal("Unable to acquire singleton lock blob lease for blob 'testid' (timeout of 0:00:00.2 exceeded).", exception.Message);
|
||||
|
||||
_mockStorageBlob.VerifyAll();
|
||||
mockAccount.BlobClient.VerifyAll();
|
||||
mockAccount.BlobLeaseClient.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReleaseLockAsync_StopsRenewalTimerAndReleasesLease()
|
||||
{
|
||||
CancellationToken cancellationToken = new CancellationToken();
|
||||
var mockAccount = _account;
|
||||
|
||||
Mock<ITaskSeriesTimer> mockRenewalTimer = new Mock<ITaskSeriesTimer>(MockBehavior.Strict);
|
||||
mockRenewalTimer.Setup(p => p.StopAsync(cancellationToken)).Returns(Task.FromResult(true));
|
||||
|
||||
_mockStorageBlob.Setup(p => p.ReleaseLeaseAsync(It.Is<AccessCondition>(q => q.LeaseId == TestLeaseId), null, null, cancellationToken)).Returns(Task.FromResult(true));
|
||||
mockAccount.BlobLeaseClient.Setup(p => p.ReleaseAsync(It.IsAny<RequestConditions>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockAccount.CreateReleasedObjectInfoResponse()));
|
||||
|
||||
var handle = new RenewableLockHandle(
|
||||
new SingletonLockHandle
|
||||
{
|
||||
Blob = _mockStorageBlob.Object,
|
||||
BlobLeaseClient = mockAccount.BlobLeaseClient.Object,
|
||||
LeaseId = TestLeaseId
|
||||
},
|
||||
mockRenewalTimer.Object
|
||||
|
@ -369,34 +480,55 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Singleton
|
|||
[Fact]
|
||||
public async Task GetLockOwnerAsync_LeaseLocked_ReturnsOwner()
|
||||
{
|
||||
MockFetchAttributesAsync(null);
|
||||
var mockAccount = _account;
|
||||
|
||||
var fields = new Dictionary<string, object>()
|
||||
{
|
||||
{ "LeaseState", LeaseState.Leased },
|
||||
};
|
||||
mockAccount.BlobClient.Setup(p => p.GetPropertiesAsync(It.IsAny<BlobRequestConditions>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobProperties>(fields)));
|
||||
|
||||
_mockStorageBlob.Object.Properties.SetLeaseState(LeaseState.Leased);
|
||||
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
string lockOwner = await _singletonManager.GetLockOwnerAsync(attribute, TestLockId, CancellationToken.None);
|
||||
Assert.Equal(null, lockOwner);
|
||||
|
||||
_mockStorageBlob.Object.Metadata.Add(StorageBaseDistributedLockManager.FunctionInstanceMetadataKey, TestLockId);
|
||||
IDictionary<string, string> metadata = new Dictionary<string, string>()
|
||||
{
|
||||
{ BlobLeaseDistributedLockManager.FunctionInstanceMetadataKey, TestLockId }
|
||||
};
|
||||
fields = new Dictionary<string, object>()
|
||||
{
|
||||
{ "LeaseState", LeaseState.Leased },
|
||||
{ "Metadata", metadata }
|
||||
};
|
||||
mockAccount.BlobClient.Setup(p => p.GetPropertiesAsync(It.IsAny<BlobRequestConditions>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobProperties>(fields)));
|
||||
|
||||
lockOwner = await _singletonManager.GetLockOwnerAsync(attribute, TestLockId, CancellationToken.None);
|
||||
Assert.Equal(TestLockId, lockOwner);
|
||||
|
||||
_mockStorageBlob.VerifyAll();
|
||||
mockAccount.BlobClient.VerifyAll();
|
||||
mockAccount.BlobLeaseClient.VerifyAll();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLockOwnerAsync_LeaseAvailable_ReturnsNull()
|
||||
{
|
||||
MockFetchAttributesAsync(null);
|
||||
var mockAccount = _account;
|
||||
|
||||
_mockStorageBlob.Object.Properties.SetLeaseState(LeaseState.Available);
|
||||
_mockStorageBlob.Object.Properties.SetLeaseStatus(LeaseStatus.Unlocked);
|
||||
var fields = new Dictionary<string, object>()
|
||||
{
|
||||
{ "LeaseState", LeaseState.Available },
|
||||
{ "LeaseStatus", LeaseStatus.Unlocked },
|
||||
};
|
||||
mockAccount.BlobClient.Setup(p => p.GetPropertiesAsync(It.IsAny<BlobRequestConditions>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockAccount.CreateInternalAzureBlobResponse<BlobProperties>(fields)));
|
||||
|
||||
SingletonAttribute attribute = new SingletonAttribute();
|
||||
string lockOwner = await _singletonManager.GetLockOwnerAsync(attribute, TestLockId, CancellationToken.None);
|
||||
Assert.Equal(null, lockOwner);
|
||||
|
||||
_mockStorageBlob.VerifyAll();
|
||||
mockAccount.BlobClient.VerifyAll();
|
||||
mockAccount.BlobLeaseClient.VerifyAll();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Xunit;
|
||||
using static Microsoft.Azure.WebJobs.Host.Storage.BlobServiceClientProvider;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
||||
{
|
||||
public class StorageServiceUriOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetBlobServiceUri_BindsProperly()
|
||||
{
|
||||
// Value and children in the section
|
||||
var configValues = new Dictionary<string, string>
|
||||
{
|
||||
{ "SomeSection:BLOBServiceUri", "https://account1.blob.core.windows.net" },
|
||||
{ "SomeSection:ACCountName", "account2" },
|
||||
};
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
var options = config.GetWebJobsConnectionSection("SomeSection").Get<StorageServiceUriOptions>();
|
||||
Assert.Equal("https://account1.blob.core.windows.net", options.BlobServiceUri);
|
||||
Assert.Equal("account2", options.AccountName);
|
||||
Assert.Equal("https://account1.blob.core.windows.net", options.GetBlobServiceUri().ToString().TrimEnd('/'));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBlobServiceUri_ThrowsOnBadUri()
|
||||
{
|
||||
// Value and children in the section
|
||||
var configValues = new Dictionary<string, string>
|
||||
{
|
||||
{ "SomeSection:BLOBServiceUri", "account1.blob.core.windows.net" },
|
||||
};
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
var options = config.GetWebJobsConnectionSection("SomeSection").Get<StorageServiceUriOptions>();
|
||||
Assert.Equal("account1.blob.core.windows.net", options.BlobServiceUri);
|
||||
Assert.Throws<UriFormatException>(() => options.GetBlobServiceUri().ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBlobServiceUri_UsesAccountNameWithDefaults()
|
||||
{
|
||||
// Value and children in the section
|
||||
var configValues = new Dictionary<string, string>
|
||||
{
|
||||
{ "SomeSection:accountName", "account1" },
|
||||
};
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
var options = config.GetWebJobsConnectionSection("SomeSection").Get<StorageServiceUriOptions>();
|
||||
Assert.Equal("account1", options.AccountName);
|
||||
Assert.Equal("https://account1.blob.core.windows.net", options.GetBlobServiceUri().ToString().TrimEnd('/'));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Azure.WebJobs.Host.FunctionalTests.TestDoubles;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Azure.WebJobs.Host.TestCommon;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
|
||||
{
|
||||
static class Utility
|
||||
{
|
||||
public static IHostBuilder ConfigureDefaultTestHost<TProgram>(this IHostBuilder builder, StorageAccount account)
|
||||
{
|
||||
return builder.ConfigureDefaultTestHost<TProgram>(b =>
|
||||
{
|
||||
b.AddAzureStorage();
|
||||
})
|
||||
.ConfigureServices(services => services.AddFakeStorageAccountProvider(account));
|
||||
}
|
||||
|
||||
public static IHostBuilder ConfigureFakeStorageAccount(this IHostBuilder builder)
|
||||
{
|
||||
return builder.ConfigureServices(services =>
|
||||
{
|
||||
services.AddFakeStorageAccountProvider();
|
||||
});
|
||||
}
|
||||
|
||||
public static IServiceCollection AddFakeStorageAccountProvider(this IServiceCollection services)
|
||||
{
|
||||
// return services.AddFakeStorageAccountProvider(new XFakeStorageAccount()); $$$
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static IServiceCollection AddNullLoggerProviders(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddSingleton<IFunctionOutputLoggerProvider, NullFunctionOutputLoggerProvider>()
|
||||
.AddSingleton<IFunctionInstanceLoggerProvider, NullFunctionInstanceLoggerProvider>();
|
||||
}
|
||||
|
||||
public static IServiceCollection AddFakeStorageAccountProvider(this IServiceCollection services, StorageAccount account)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
/*
|
||||
if (account is XFakeStorageAccount)
|
||||
{
|
||||
services.AddNullLoggerProviders();
|
||||
}
|
||||
|
||||
return services.AddSingleton<XStorageAccountProvider>(new FakeStorageAccountProvider(account));
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,17 +28,16 @@
|
|||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.7.145" />
|
||||
<PackageReference Include="Moq" Version="4.16.0" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="5.0.0-beta.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Extensions.Storage\WebJobs.Extensions.Storage.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Host.Storage\WebJobs.Host.Storage.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Host\WebJobs.Host.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Logging.ApplicationInsights\WebJobs.Logging.ApplicationInsights.csproj" />
|
||||
<ProjectReference Include="..\FakeStorage\FakeAzureStorage.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Azure.WebJobs.Host.TestCommon\WebJobs.Host.TestCommon.csproj" />
|
||||
<ProjectReference Include="..\TestProjects\FSharpFunctions\FSharpFunctions.fsproj" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Storage.Queue;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.TestCommon.AzureSdk
|
||||
{
|
||||
public static class CloudQueueClientExtensions
|
||||
{
|
||||
public async static Task CreateQueueOrClearIfExists(this CloudQueueClient queueClient, string queueName)
|
||||
{
|
||||
CloudQueue queue = queueClient.GetQueueReference(queueName);
|
||||
|
||||
bool wasCreatedNow = await queue.CreateIfNotExistsAsync();
|
||||
if (!wasCreatedNow)
|
||||
{
|
||||
await queue.ClearAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async static Task DeleteQueueIfExists(this CloudQueueClient queueClient, string queueName)
|
||||
{
|
||||
CloudQueue queue = queueClient.GetQueueReference(queueName);
|
||||
|
||||
if (await queue.ExistsAsync())
|
||||
{
|
||||
await queue.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Azure.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.Executors;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.TestCommon
|
||||
{
|
||||
|
@ -13,20 +11,10 @@ namespace Microsoft.Azure.WebJobs.Host.TestCommon
|
|||
{
|
||||
public static JobHost<TProgram> Create<TProgram>()
|
||||
{
|
||||
return Create<TProgram>(CloudStorageAccount.DevelopmentStorageAccount, maxDequeueCount: 5);
|
||||
return Create<TProgram>(maxDequeueCount: 5);
|
||||
}
|
||||
|
||||
public static JobHost<TProgram> Create<TProgram>(int maxDequeueCount)
|
||||
{
|
||||
return Create<TProgram>(CloudStorageAccount.DevelopmentStorageAccount, maxDequeueCount);
|
||||
}
|
||||
|
||||
public static JobHost<TProgram> Create<TProgram>(CloudStorageAccount storageAccount)
|
||||
{
|
||||
return Create<TProgram>(storageAccount, maxDequeueCount: 5);
|
||||
}
|
||||
|
||||
public static JobHost<TProgram> Create<TProgram>(CloudStorageAccount storageAccount, int maxDequeueCount)
|
||||
{
|
||||
IHost host = new HostBuilder()
|
||||
.ConfigureDefaultTestHost<TProgram>()
|
||||
|
@ -43,7 +31,7 @@ namespace Microsoft.Azure.WebJobs.Host.TestCommon
|
|||
})
|
||||
.Build();
|
||||
|
||||
return host.GetJobHost<TProgram>();
|
||||
return host.GetJobHost<TProgram>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using Azure.Storage.Queues;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Extensions.Azure;
|
||||
|
||||
namespace Microsoft.Azure.WebJobs.Host.TestCommon
|
||||
{
|
||||
/// <summary>
|
||||
/// Provider to create QueueServiceClient objects. This is used only for tests and is intended to only honor connection strings.
|
||||
/// </summary>
|
||||
internal class QueueServiceClientProvider : StorageClientProvider<QueueServiceClient, QueueClientOptions>
|
||||
{
|
||||
public QueueServiceClientProvider(AzureComponentFactory componentFactory, AzureEventSourceLogForwarder logForwarder)
|
||||
: base(componentFactory, logForwarder) { }
|
||||
}
|
||||
}
|
|
@ -8,11 +8,15 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Queues;
|
||||
using Microsoft.Azure.WebJobs.Host.Config;
|
||||
using Microsoft.Azure.WebJobs.Host.FunctionalTests.TestDoubles;
|
||||
using Microsoft.Azure.WebJobs.Host.Indexers;
|
||||
using Microsoft.Azure.WebJobs.Host.Loggers;
|
||||
using Microsoft.Azure.WebJobs.Host.Storage;
|
||||
using Microsoft.Azure.WebJobs.Host.Timers;
|
||||
using Microsoft.Extensions.Azure;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
@ -350,6 +354,30 @@ namespace Microsoft.Azure.WebJobs.Host.TestCommon
|
|||
FieldInfo elapsedFieldInfo = typeof(Stopwatch).GetField("_elapsed", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
elapsedFieldInfo.SetValue(sw, elapsed.Ticks);
|
||||
}
|
||||
|
||||
public static BlobServiceClient GetTestBlobServiceClient(string connection = "Storage")
|
||||
{
|
||||
var host = new HostBuilder().ConfigureDefaultTestHost(builder => builder.AddAzureStorageCoreServices()).Build();
|
||||
var componentFactory = host.Services.GetRequiredService<AzureComponentFactory>();
|
||||
var logForwarder = host.Services.GetRequiredService<AzureEventSourceLogForwarder>();
|
||||
var provider = new BlobServiceClientProvider(componentFactory, logForwarder);
|
||||
return provider.Create(connection, host.Services.GetRequiredService<IConfiguration>());
|
||||
}
|
||||
|
||||
public static QueueServiceClient GetTestQueueServiceClient(string connection = "Storage")
|
||||
{
|
||||
var host = new HostBuilder().ConfigureDefaultTestHost(builder => builder.AddAzureStorageCoreServices()).Build();
|
||||
var componentFactory = host.Services.GetRequiredService<AzureComponentFactory>();
|
||||
var logForwarder = host.Services.GetRequiredService<AzureEventSourceLogForwarder>();
|
||||
var provider = new QueueServiceClientProvider(componentFactory, logForwarder);
|
||||
return provider.Create(connection, host.Services.GetRequiredService<IConfiguration>());
|
||||
}
|
||||
|
||||
public static IAzureBlobStorageProvider GetTestAzureBlobStorageProvider()
|
||||
{
|
||||
var host = new HostBuilder().ConfigureDefaultTestHost(builder => builder.AddAzureStorageCoreServices()).Build();
|
||||
return host.Services.GetRequiredService<IAzureBlobStorageProvider>();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestProgram
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Storage.Blob" Version="11.1.7" />
|
||||
<PackageReference Include="Microsoft.Azure.Storage.Queue" Version="11.1.7" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.9.0" />
|
||||
<PackageReference Include="Azure.Storage.Queues" Version="12.7.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -34,6 +34,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Host.Storage\WebJobs.Host.Storage.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Azure.WebJobs.Host\WebJobs.Host.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Xunit;
|
||||
|
||||
|
@ -41,5 +42,38 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
|
|||
Environment.SetEnvironmentVariable(settingName, null);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetWebJobsConnectionSection_ReturnsExpected()
|
||||
{
|
||||
// Value and children in the section
|
||||
var configValues = new Dictionary<string, string>
|
||||
{
|
||||
{ "AzureWebJobsStorage", "connectionString" },
|
||||
{ "AzureWebJobsStorage:subsection", "test1" },
|
||||
};
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
var section = config.GetWebJobsConnectionSection(ConnectionStringNames.Storage);
|
||||
Assert.True(section.Exists());
|
||||
Assert.False(string.IsNullOrEmpty(section.Value));
|
||||
Assert.Equal("test1", section["subsection"]);
|
||||
|
||||
// No value, just children
|
||||
configValues = new Dictionary<string, string>
|
||||
{
|
||||
{ "AzureWebJobsStorage:subsection", "test2" },
|
||||
};
|
||||
config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configValues)
|
||||
.Build();
|
||||
|
||||
section = config.GetWebJobsConnectionSection(ConnectionStringNames.Storage);
|
||||
Assert.True(section.Exists());
|
||||
Assert.True(string.IsNullOrEmpty(section.Value));
|
||||
Assert.Equal("test2", section["subsection"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче