diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/CHANGELOG.md b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/CHANGELOG.md index b7b30d605656..a89f0bb18f28 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/CHANGELOG.md +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/CHANGELOG.md @@ -7,6 +7,7 @@ ### Breaking Changes ### Bugs Fixed +- Fixed an issue where polling for new blobs could miss recently created or updated blobs when the List Blobs/Get Blobs operation spans multiple requests. If a blob is added or modified after LogScan starts and appears in a later segment of the listing, any blobs changed between the initial listing start time and the last modified timestamp of those later blobs were not flagged as new in the subsequent LogScan. - Bug fix ensuring that BlobTrigger log scan targets the correct storage account in multi-account scenarios. ### Other Changes diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ContainerScanInfo.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ContainerScanInfo.cs index a474cbc76c6b..8763e67bc61f 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ContainerScanInfo.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ContainerScanInfo.cs @@ -11,9 +11,11 @@ internal class ContainerScanInfo { public ICollection> Registrations { get; set; } - public DateTime LastSweepCycleLatestModified { get; set; } + public DateTimeOffset PollingStartTime { get; set; } - public DateTime CurrentSweepCycleLatestModified { get; set; } + public DateTimeOffset LastSweepCycleLatestModified { get; set; } + + public DateTimeOffset CurrentSweepCycleLatestModified { get; set; } public string ContinuationToken { get; set; } } diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/IBlobScanInfoManager.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/IBlobScanInfoManager.cs index 7876884bdf8e..547b7d8ce1e8 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/IBlobScanInfoManager.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/IBlobScanInfoManager.cs @@ -8,7 +8,7 @@ namespace Microsoft.Azure.WebJobs.Extensions.Storage.Blobs.Listeners { internal interface IBlobScanInfoManager { - Task LoadLatestScanAsync(string storageAccountName, string containerName); - Task UpdateLatestScanAsync(string storageAccountName, string containerName, DateTime scanInfo); + Task LoadLatestScanAsync(string storageAccountName, string containerName); + Task UpdateLatestScanAsync(string storageAccountName, string containerName, DateTimeOffset scanInfo); } } diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ScanBlobScanLogHybridPollingStrategy.Logger.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ScanBlobScanLogHybridPollingStrategy.Logger.cs index d1318bfa62f0..4d5ae525f0d9 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ScanBlobScanLogHybridPollingStrategy.Logger.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ScanBlobScanLogHybridPollingStrategy.Logger.cs @@ -26,10 +26,10 @@ private static class Logger LoggerMessage.Define(LogLevel.Debug, new EventId(3, nameof(ContainerDoesNotExist)), "Container '{containerName}' does not exist."); - public static void InitializedScanInfo(ILogger logger, string container, DateTime latestScanInfo) => + public static void InitializedScanInfo(ILogger logger, string container, DateTimeOffset latestScanInfo) => _initializedScanInfo(logger, container, latestScanInfo.ToString(Constants.DateTimeFormatString, CultureInfo.InvariantCulture), null); - public static void PollBlobContainer(ILogger logger, string container, DateTime latestScanInfo, string clientRequestId, int blobCount, long latencyInMilliseconds, bool hasContinuationToken) => + public static void PollBlobContainer(ILogger logger, string container, DateTimeOffset latestScanInfo, string clientRequestId, int blobCount, long latencyInMilliseconds, bool hasContinuationToken) => _pollBlobContainer(logger, latestScanInfo.ToString(Constants.DateTimeFormatString, CultureInfo.InvariantCulture), container, clientRequestId, blobCount, latencyInMilliseconds, hasContinuationToken, null); public static void ContainerDoesNotExist(ILogger logger, string container) => diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ScanBlobScanLogHybridPollingStrategy.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ScanBlobScanLogHybridPollingStrategy.cs index ed6099a35e0c..7754c1b79f78 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ScanBlobScanLogHybridPollingStrategy.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/ScanBlobScanLogHybridPollingStrategy.cs @@ -73,7 +73,7 @@ public async Task RegisterAsync(BlobServiceClient blobServiceClient, BlobContain if (!_scanInfo.TryGetValue(container, out ContainerScanInfo containerScanInfo)) { // First, try to load serialized scanInfo for this container. - DateTime? latestStoredScan = await _blobScanInfoManager.LoadLatestScanAsync(blobServiceClient.AccountName, container.Name).ConfigureAwait(false); + DateTimeOffset? latestStoredScan = await _blobScanInfoManager.LoadLatestScanAsync(blobServiceClient.AccountName, container.Name).ConfigureAwait(false); containerScanInfo = new ContainerScanInfo() { @@ -138,7 +138,7 @@ private async Task PollAndNotify(BlobContainerClient container, ContainerScanInf List failedNotifications, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - DateTime lastScan = containerScanInfo.LastSweepCycleLatestModified; + DateTimeOffset lastScan = containerScanInfo.LastSweepCycleLatestModified; // For tracking string clientRequestId = Guid.NewGuid().ToString(); @@ -154,7 +154,7 @@ private async Task PollAndNotify(BlobContainerClient container, ContainerScanInf // if the 'LatestModified' has changed, update it in the manager if (containerScanInfo.LastSweepCycleLatestModified > lastScan) { - DateTime latestScan = containerScanInfo.LastSweepCycleLatestModified; + DateTimeOffset latestScan = containerScanInfo.LastSweepCycleLatestModified; // It's possible that we had some blobs that we failed to move to the queue. We want to make sure // we continue to find these if the host needs to restart. @@ -217,9 +217,10 @@ public async Task> PollNewBlobsAsync( string continuationToken = containerScanInfo.ContinuationToken; Page page; - // if starting the cycle, reset the sweep time + // if starting the cycle, reset the sweep time and set start time if (continuationToken == null) { + containerScanInfo.PollingStartTime = DateTimeOffset.UtcNow; containerScanInfo.CurrentSweepCycleLatestModified = DateTime.MinValue; } @@ -266,14 +267,16 @@ public async Task> PollNewBlobsAsync( var properties = currentBlob.Properties; DateTime lastModifiedTimestamp = properties.LastModified.Value.UtcDateTime; - if (lastModifiedTimestamp > containerScanInfo.CurrentSweepCycleLatestModified) + if (lastModifiedTimestamp > containerScanInfo.CurrentSweepCycleLatestModified && + (continuationToken == null || lastModifiedTimestamp <= containerScanInfo.PollingStartTime)) { containerScanInfo.CurrentSweepCycleLatestModified = lastModifiedTimestamp; } // Blob timestamps are rounded to the nearest second, so make sure we continue to check // the previous timestamp to catch any blobs that came in slightly after our previous poll. - if (lastModifiedTimestamp >= containerScanInfo.LastSweepCycleLatestModified) + if (lastModifiedTimestamp >= containerScanInfo.LastSweepCycleLatestModified && + lastModifiedTimestamp <= containerScanInfo.PollingStartTime) { newBlobs.Add(container.GetBlobClient(currentBlob.Name)); } diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/StorageBlobScanInfoManager.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/StorageBlobScanInfoManager.cs index c9f6cda466eb..336c4012e9e6 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/StorageBlobScanInfoManager.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/src/Listeners/StorageBlobScanInfoManager.cs @@ -43,10 +43,10 @@ public StorageBlobScanInfoManager(string hostId, BlobServiceClient blobClient) _blobContainerClient = blobClient.GetBlobContainerClient(HostContainerNames.Hosts); } - public async Task LoadLatestScanAsync(string storageAccountName, string containerName) + public async Task LoadLatestScanAsync(string storageAccountName, string containerName) { var scanInfoBlob = GetScanInfoBlobReference(storageAccountName, containerName); - DateTime? latestScan = null; + DateTimeOffset? latestScan = null; try { string scanInfoLine = await scanInfoBlob.DownloadTextAsync(CancellationToken.None).ConfigureAwait(false); @@ -74,7 +74,7 @@ public StorageBlobScanInfoManager(string hostId, BlobServiceClient blobClient) } } - public async Task UpdateLatestScanAsync(string storageAccountName, string containerName, DateTime latestScan) + public async Task UpdateLatestScanAsync(string storageAccountName, string containerName, DateTimeOffset latestScan) { string scanInfoLine; ScanInfo scanInfo = new ScanInfo @@ -109,7 +109,7 @@ private BlockBlobClient GetScanInfoBlobReference(string storageAccountName, stri internal class ScanInfo { - public DateTime LatestScan { get; set; } + public DateTimeOffset LatestScan { get; set; } } } } diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/Listeners/ScanBlobScanLogHybridPollingStrategyTests.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/Listeners/ScanBlobScanLogHybridPollingStrategyTests.cs index 6d6d1ed76be3..f01e56634bc2 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/Listeners/ScanBlobScanLogHybridPollingStrategyTests.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/Listeners/ScanBlobScanLogHybridPollingStrategyTests.cs @@ -14,6 +14,7 @@ using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Azure.Storage.Blobs.Specialized; +using Microsoft.Azure.WebJobs.Extensions.Storage.Blobs.Tests; using Microsoft.Azure.WebJobs.Extensions.Storage.Common.Listeners; using Microsoft.Azure.WebJobs.Extensions.Storage.Common.Tests; using Microsoft.Azure.WebJobs.Host.Executors; @@ -306,7 +307,7 @@ public async Task RegisterAsync_InitializesWithScanInfoManager() // delay slightly so we guarantee a later timestamp await Task.Delay(10); - await scanInfoManager.UpdateLatestScanAsync(AccountName, ContainerName, DateTime.UtcNow); + await scanInfoManager.UpdateLatestScanAsync(AccountName, ContainerName, DateTimeOffset.UtcNow); await product.RegisterAsync(_blobClientMock.Object, container, executor, CancellationToken.None); // delay slightly so we guarantee a later timestamp @@ -422,7 +423,7 @@ public async Task ExecuteAsync_UpdatesScanInfo_WithEarliestFailure() int testScanBlobLimitPerPoll = 6; // we'll introduce multiple errors to make sure we take the earliest timestamp - DateTime earliestErrorTime = DateTime.UtcNow; + DateTimeOffset earliestErrorTime = DateTimeOffset.UtcNow.AddHours(-1); var container = _blobContainerMock.Object; @@ -455,13 +456,98 @@ public async Task ExecuteAsync_UpdatesScanInfo_WithEarliestFailure() RunExecuteWithMultiPollingInterval(expectedNames, product, executor, testScanBlobLimitPerPoll); - DateTime? storedTime = await testScanInfoManager.LoadLatestScanAsync(accountName, ContainerName); + DateTimeOffset? storedTime = await testScanInfoManager.LoadLatestScanAsync(accountName, ContainerName); Assert.True(storedTime < earliestErrorTime); Assert.AreEqual(1, testScanInfoManager.UpdateCounts[accountName][ContainerName]); _blobContainerMock.Verify(x => x.GetBlobsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } + [Test] + public async Task ExecuteAsync_PollNewBlobsAsync_ContinuationTokenUpdatedBlobs() + { + // Arrange + int testScanBlobLimitPerPoll = 3; + IBlobListenerStrategy product = new ScanBlobScanLogHybridPollingStrategy(new TestBlobScanInfoManager(), _exceptionHandler, NullLogger.Instance); + LambdaBlobTriggerExecutor executor = new LambdaBlobTriggerExecutor(); + typeof(ScanBlobScanLogHybridPollingStrategy) + .GetField("_scanBlobLimitPerPoll", BindingFlags.Instance | BindingFlags.NonPublic) + .SetValue(product, testScanBlobLimitPerPoll); + + // Setup container to have multiple GetBlobsAsync calls to simulate continuation tokens + Uri uri = new Uri("https://fakeaccount.blob.core.windows.net/fakecontainer2"); + Mock containerMock = new Mock(uri, null); + containerMock.Setup(x => x.Uri).Returns(uri); + containerMock.Setup(x => x.Name).Returns(ContainerName); + containerMock.Setup(x => x.AccountName).Returns(AccountName); + + // Create first page of blobs to list from + List blobItems = new List(); + List expectedNames = new List(); + for (int i = 0; i < 5; i++) + { + DateTimeOffset lastModified = DateTimeOffset.UtcNow.AddMinutes(-10 * i); + expectedNames.Add(CreateBlobAndUploadToContainer(containerMock, blobItems, lastModified: lastModified)); + } + // Create second page + List blobItemsPage2 = new List(); + for (int i = 0; i < 3; i++) + { + DateTimeOffset lastModified = DateTimeOffset.UtcNow.AddMinutes(-5 * i); + expectedNames.Add(CreateBlobAndUploadToContainer(containerMock, blobItemsPage2, lastModified: lastModified)); + } + + // Add at least one blob that has a LastModifiedTime that goes beyond the start time of polling + DateTimeOffset lastModifiedAfterStartPolling = DateTimeOffset.UtcNow.AddSeconds(5); + string blobNameWithLmtAfterStartedPolling = CreateBlobAndUploadToContainer(containerMock, blobItemsPage2, lastModified: lastModifiedAfterStartPolling); + + // Update all the blobs in the second listing, that way they get detected again in the second polling + List blobItemsUpdated = new List(); + List secondSetExpectedNames = new List(); + for (int i = 0; i < 4; i++) + { + // Create LastModified to be after the LMT of the blob that was beyond the start of the polling time to test if blobs created after that time will also be detected + secondSetExpectedNames.Add(CreateBlobAndUploadToContainer(containerMock, blobItemsUpdated, lastModified: lastModifiedAfterStartPolling.AddSeconds(-2))); + } + // Add blob with LMT after polling started to the second polling expected names. + secondSetExpectedNames.Add(blobNameWithLmtAfterStartedPolling); + + // Set up GetBlobsAsync to return pages with continuation token for each page, but not at the end of each polling + containerMock.SetupSequence(x => x.GetBlobsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + // First polling + .Returns(() => + { + return new TestAsyncPageableWithContinuationToken(blobItems, true); + }) + .Returns(() => + { + return new TestAsyncPageableWithContinuationToken(blobItemsPage2, false); + }) + // Second polling + .Returns(() => + { + return new TestAsyncPageableWithContinuationToken(blobItemsUpdated, true); + }) + .Returns(() => + { + return new TestAsyncPageableWithContinuationToken(blobItemsPage2, false); + }); + + // Register the container to initialize _scanInfo + await product.RegisterAsync(_blobClientMock.Object, containerMock.Object, executor, CancellationToken.None); + + // Act / Assert - First Polling + RunExecuteWithMultiPollingInterval(expectedNames, product, executor, blobItems.Count); + + // Wait 5 seconds to ensure that the blob with LMT after polling started is detected as a new blob. + await Task.Delay(TimeSpan.FromSeconds(5)); + + // Act / Assert - Second Polling + // We expect all the blobs we updated above to be detected and the blob that was created after the first polling started that wasn't detected + // to be now detected in this polling. + RunExecuteWithMultiPollingInterval(secondSetExpectedNames, product, executor, blobItemsUpdated.Count); + } + private void RunExecuterWithExpectedBlobsInternal(IDictionary blobNameMap, IBlobListenerStrategy product, LambdaBlobTriggerExecutor executor, int expectedCount) { if (blobNameMap.Count == 0) @@ -585,23 +671,23 @@ public Task ExecuteAsync(BlobTriggerExecutorContext value, Cance private class TestBlobScanInfoManager : IBlobScanInfoManager { - private IDictionary> _latestScans; + private IDictionary> _latestScans; public TestBlobScanInfoManager() { - _latestScans = new Dictionary>(); + _latestScans = new Dictionary>(); UpdateCounts = new Dictionary>(); } public IDictionary> UpdateCounts { get; private set; } - public Task LoadLatestScanAsync(string storageAccountName, string containerName) + public Task LoadLatestScanAsync(string storageAccountName, string containerName) { - DateTime? value = null; - IDictionary accounts; + DateTimeOffset? value = null; + IDictionary accounts; if (_latestScans.TryGetValue(storageAccountName, out accounts)) { - DateTime latestScan; + DateTimeOffset latestScan; if (accounts.TryGetValue(containerName, out latestScan)) { value = latestScan; @@ -611,20 +697,20 @@ public TestBlobScanInfoManager() return Task.FromResult(value); } - public Task UpdateLatestScanAsync(string storageAccountName, string containerName, DateTime latestScan) + public Task UpdateLatestScanAsync(string storageAccountName, string containerName, DateTimeOffset latestScan) { SetScanInfo(storageAccountName, containerName, latestScan); IncrementCount(storageAccountName, containerName); return Task.FromResult(0); } - public void SetScanInfo(string storageAccountName, string containerName, DateTime latestScan) + public void SetScanInfo(string storageAccountName, string containerName, DateTimeOffset latestScan) { - IDictionary containers; + IDictionary containers; if (!_latestScans.TryGetValue(storageAccountName, out containers)) { - _latestScans[storageAccountName] = new Dictionary(); + _latestScans[storageAccountName] = new Dictionary(); containers = _latestScans[storageAccountName]; } diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/Listeners/StorageBlobScanInfoManagerTests.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/Listeners/StorageBlobScanInfoManagerTests.cs index b6c36147b1cb..86248e2f970a 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/Listeners/StorageBlobScanInfoManagerTests.cs +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/Listeners/StorageBlobScanInfoManagerTests.cs @@ -62,7 +62,7 @@ public async Task LoadLatestScan_Returns_Timestamp() var container = blobServiceClient.GetBlobContainerClient(HostContainerNames.Hosts); await container.CreateIfNotExistsAsync(); - DateTime now = DateTime.UtcNow; + DateTimeOffset now = DateTimeOffset.UtcNow; var blob = GetBlockBlobReference(blobServiceClient, hostId, storageAccountName, containerName); await blob.UploadTextAsync(string.Format("{{ \"LatestScan\" : \"{0}\" }}", now.ToString("o"))); @@ -82,7 +82,7 @@ public async Task UpdateLatestScan_Inserts() var container = blobServiceClient.GetBlobContainerClient(HostContainerNames.Hosts); await container.CreateIfNotExistsAsync(); - DateTime now = DateTime.UtcNow; + DateTimeOffset now = DateTimeOffset.UtcNow; var manager = new StorageBlobScanInfoManager(hostId, blobServiceClient); @@ -90,7 +90,7 @@ public async Task UpdateLatestScan_Inserts() var scanInfo = GetBlockBlobReference(blobServiceClient, hostId, storageAccountName, containerName).DownloadText(); var scanInfoJson = JObject.Parse(scanInfo); - var storedTime = (DateTime)(scanInfoJson["LatestScan"]); + var storedTime = (DateTimeOffset)(scanInfoJson["LatestScan"]); Assert.AreEqual(now, storedTime); Assert.AreEqual(now, await manager.LoadLatestScanAsync(storageAccountName, containerName)); @@ -106,8 +106,8 @@ public async Task UpdateLatestScan_Updates() var container = blobServiceClient.GetBlobContainerClient(HostContainerNames.Hosts); await container.CreateIfNotExistsAsync(); - DateTime now = DateTime.UtcNow; - DateTime past = now.AddMinutes(-1); + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset past = now.AddMinutes(-1); var blob = GetBlockBlobReference(blobServiceClient, hostId, storageAccountName, containerName); await blob.UploadTextAsync(string.Format("{{ 'LatestScan' : '{0}' }}", past.ToString("o"))); @@ -118,7 +118,7 @@ public async Task UpdateLatestScan_Updates() var scanInfo = GetBlockBlobReference(blobServiceClient, hostId, storageAccountName, containerName).DownloadText(); var scanInfoJson = JObject.Parse(scanInfo); - var storedTime = (DateTime)scanInfoJson["LatestScan"]; + var storedTime = (DateTimeOffset)scanInfoJson["LatestScan"]; Assert.AreEqual(now, storedTime); Assert.AreEqual(now, await manager.LoadLatestScanAsync(storageAccountName, containerName)); diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/TestAsyncPageableWithContinuationToken.cs b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/TestAsyncPageableWithContinuationToken.cs new file mode 100644 index 000000000000..45cf020bfb33 --- /dev/null +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs/tests/TestAsyncPageableWithContinuationToken.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure; +using Azure.Storage.Blobs.Models; +using Moq; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.Storage.Blobs.Tests +{ + /// + /// This is based on the TestAsyncPageable in Common tests. It adds the ability to specify whether a continuation token is returned. + /// This will allow the strategy to determine whether there are more pages and continue listing. + /// It will NOT use the passed in continuation token to determine what to return; it always returns the page. + /// + public class TestAsyncPageableWithContinuationToken : AsyncPageable + { + private readonly List _page; + private readonly bool _returnsContinuationToken; + + public TestAsyncPageableWithContinuationToken(List page, bool returnsContinuationToken) + { + _page = page; + _returnsContinuationToken = returnsContinuationToken; + } + + public override async IAsyncEnumerable> AsPages(string continuationToken = null, int? pageSizeHint = null) + { + string mockContinuationToken = System.Guid.NewGuid().ToString(); + yield return Page.FromValues( + _page.AsReadOnly(), + _returnsContinuationToken ? mockContinuationToken : null, + Mock.Of()); + // Simulate async page boundary + await Task.Yield(); + } + } +} diff --git a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Scenario.Tests/tests/Microsoft.Azure.WebJobs.Extensions.Storage.Scenario.Tests.csproj b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Scenario.Tests/tests/Microsoft.Azure.WebJobs.Extensions.Storage.Scenario.Tests.csproj index 842bf92be72c..b7decbd728ea 100644 --- a/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Scenario.Tests/tests/Microsoft.Azure.WebJobs.Extensions.Storage.Scenario.Tests.csproj +++ b/sdk/storage/Microsoft.Azure.WebJobs.Extensions.Storage.Scenario.Tests/tests/Microsoft.Azure.WebJobs.Extensions.Storage.Scenario.Tests.csproj @@ -5,7 +5,6 @@ -