public async Task RetriesOfFixUpAndFailedPackageIdsCanSucceedWithinRetries() { var itemsA = new[] { new CatalogCommitItem( uri: new Uri("https://example/0"), commitId: null, commitTimeStamp: new DateTime(2018, 1, 1), types: null, typeUris: new List <Uri> { Schema.DataTypes.PackageDetails }, packageIdentity: new PackageIdentity("NuGet.Versioning", NuGetVersion.Parse("1.0.0"))), }; var itemsB = new List <CatalogCommitItem> { new CatalogCommitItem( uri: new Uri("https://example/1"), commitId: null, commitTimeStamp: new DateTime(2019, 1, 1), types: null, typeUris: new List <Uri> { Schema.DataTypes.PackageDetails }, packageIdentity: new PackageIdentity("NuGet.Versioning", NuGetVersion.Parse("1.0.0"))), }; var otherException = new InvalidOperationException("Not so fast, buddy."); _batchPusher .SetupSequence(x => x.TryFinishAsync()) .ReturnsAsync(new BatchPusherResult(new[] { "NuGet.Versioning" })) .ThrowsAsync(new InvalidOperationException()) .ReturnsAsync(new BatchPusherResult()); _fixUpEvaluator .Setup(x => x.TryFixUpAsync( It.IsAny <IReadOnlyList <CatalogCommitItem> >(), It.IsAny <ConcurrentBag <IdAndValue <IndexActions> > >(), It.IsAny <InvalidOperationException>())) .ReturnsAsync(() => DocumentFixUp.IsApplicable(new List <CatalogCommitItem>(itemsB))); await _target.OnProcessBatchAsync(itemsA); _fixUpEvaluator.Verify( x => x.TryFixUpAsync( It.IsAny <IReadOnlyList <CatalogCommitItem> >(), It.IsAny <ConcurrentBag <IdAndValue <IndexActions> > >(), It.IsAny <InvalidOperationException>()), Times.Once); _batchPusher.Verify(x => x.TryFinishAsync(), Times.Exactly(3)); _catalogClient.Verify(x => x.GetPackageDetailsLeafAsync("https://example/0"), Times.Exactly(2)); _catalogClient.Verify(x => x.GetPackageDetailsLeafAsync("https://example/1"), Times.Once); }
public BaseFacts(ITestOutputHelper output) { _catalogClient = new Mock <ICatalogClient>(); _catalogIndexActionBuilder = new Mock <ICatalogIndexActionBuilder>(); _batchPusher = new Mock <IBatchPusher>(); _fixUpEvaluator = new Mock <IDocumentFixUpEvaluator>(); _utilityOptions = new Mock <IOptionsSnapshot <CommitCollectorConfiguration> >(); _collectorOptions = new Mock <IOptionsSnapshot <Catalog2AzureSearchConfiguration> >(); _utilityConfig = new CommitCollectorConfiguration(); _collectorConfig = new Catalog2AzureSearchConfiguration(); _telemetryService = new Mock <IAzureSearchTelemetryService>(); _v3TelemetryService = new Mock <IV3TelemetryService>(); _logger = output.GetLogger <AzureSearchCollectorLogic>(); _utilityLogger = output.GetLogger <CommitCollectorUtility>(); _batchPusher.SetReturnsDefault(Task.FromResult(new BatchPusherResult())); _utilityOptions.Setup(x => x.Value).Returns(() => _utilityConfig); _utilityConfig.MaxConcurrentCatalogLeafDownloads = 1; _collectorOptions.Setup(x => x.Value).Returns(() => _collectorConfig); _collectorConfig.MaxConcurrentBatches = 1; _fixUpEvaluator .Setup(x => x.TryFixUpAsync( It.IsAny <IReadOnlyList <CatalogCommitItem> >(), It.IsAny <ConcurrentBag <IdAndValue <IndexActions> > >(), It.IsAny <InvalidOperationException>())) .ReturnsAsync(() => DocumentFixUp.IsNotApplicable()); _utility = new CommitCollectorUtility( _catalogClient.Object, _v3TelemetryService.Object, _utilityOptions.Object, _utilityLogger); _target = new AzureSearchCollectorLogic( _catalogIndexActionBuilder.Object, () => _batchPusher.Object, _fixUpEvaluator.Object, _utility, _collectorOptions.Object, _telemetryService.Object, _logger); }
public async Task ThrowsOriginalExceptionIfFixFailsAgain() { var items = new[] { new CatalogCommitItem( uri: new Uri("https://example/0"), commitId: null, commitTimeStamp: new DateTime(2018, 1, 1), types: null, typeUris: new List <Uri> { Schema.DataTypes.PackageDetails }, packageIdentity: new PackageIdentity("NuGet.Versioning", NuGetVersion.Parse("1.0.0"))), }; var otherException = new InvalidOperationException("Not so fast, buddy."); _batchPusher .Setup(x => x.FinishAsync()) .ThrowsAsync(otherException); _fixUpEvaluator .Setup(x => x.TryFixUpAsync( It.IsAny <IReadOnlyList <CatalogCommitItem> >(), It.IsAny <ConcurrentBag <IdAndValue <IndexActions> > >(), It.IsAny <InvalidOperationException>())) .ReturnsAsync(() => DocumentFixUp.IsApplicable(new List <CatalogCommitItem>())); var ex = await Assert.ThrowsAsync <InvalidOperationException>( () => _target.OnProcessBatchAsync(items)); Assert.Same(otherException, ex); _fixUpEvaluator.Verify( x => x.TryFixUpAsync( It.IsAny <IReadOnlyList <CatalogCommitItem> >(), It.IsAny <ConcurrentBag <IdAndValue <IndexActions> > >(), It.IsAny <InvalidOperationException>()), Times.Once); _batchPusher.Verify(x => x.FinishAsync(), Times.Exactly(2)); }
public async Task <DocumentFixUp> TryFixUpAsync( IReadOnlyList <CatalogCommitItem> itemList, ConcurrentBag <IdAndValue <IndexActions> > allIndexActions, InvalidOperationException exception) { var innerEx = exception.InnerException as IndexBatchException; if (innerEx == null || innerEx.IndexingResults == null) { return(DocumentFixUp.IsNotApplicable()); } // There may have been a Case of the Missing Document! We have confirmed with the Azure Search team that // this is a bug on the Azure Search side. To mitigate the issue, we replace any Merge operation that // failed with 404 with a MergeOrUpload with the full metadata so that we can replace that missing document. // // 1. The first step is to find all of the document keys that failed with a 404 Not Found error. var notFoundKeys = new HashSet <string>(innerEx .IndexingResults .Where(x => x.StatusCode == (int)HttpStatusCode.NotFound) .Select(x => x.Key)); if (!notFoundKeys.Any()) { return(DocumentFixUp.IsNotApplicable()); } _logger.LogWarning("{Count} document action(s) failed with 404 Not Found.", notFoundKeys.Count); // 2. Find all of the package IDs that were affected, only considering Merge operations against the Search // index. We ignore the the hijack index for now because we have only ever seen the problem in the Search // index. var failedIds = new HashSet <string>(); foreach (var pair in allIndexActions.OrderBy(x => x.Id, StringComparer.OrdinalIgnoreCase)) { var failedMerges = pair .Value .Search .Where(a => a.ActionType == IndexActionType.Merge) .Where(a => notFoundKeys.Contains(a.Document.Key)); if (failedMerges.Any() && failedIds.Add(pair.Id)) { _logger.LogWarning("Package {PackageId} had a Merge operation fail with 404 Not Found.", pair.Id); } } if (!failedIds.Any()) { _logger.LogInformation("No failed Merge operations against the Search index were found."); return(DocumentFixUp.IsNotApplicable()); } // 3. For each affected package ID, get the version list to determine the latest version per search filter // so we can find the the catalog entry for the version. var identityToItems = itemList.GroupBy(x => x.PackageIdentity).ToDictionary(x => x.Key, x => x.ToList()); foreach (var packageId in failedIds) { var accessConditionAndData = await _versionListClient.ReadAsync(packageId); var versionLists = new VersionLists(accessConditionAndData.Result); var latestVersions = DocumentUtilities .AllSearchFilters .Select(sf => versionLists.GetLatestVersionInfoOrNull(sf)) .Where(lvi => lvi != null) .Select(lvi => (IReadOnlyList <NuGetVersion>) new List <NuGetVersion> { lvi.ParsedVersion }) .ToList(); var leaves = await _leafFetcher.GetLatestLeavesAsync(packageId, latestVersions); // We ignore unavailable (deleted) versions for now. We have never had a delete cause this problem. It's // only ever been discovered when a new version is being added or updated. // // For each package details leaf found, create a catalog commit item and add it to the set of items we // will process. This will force the metadata to be updated on each of the latest versions. Since this // is the latest metadata, replace any older leaves that may be associated with that package version. foreach (var pair in leaves.Available) { var identity = new PackageIdentity(packageId, pair.Key); var leaf = pair.Value; if (identityToItems.TryGetValue(identity, out var existing)) { if (existing.Count == 1 && existing[0].Uri.AbsoluteUri == leaf.Url) { _logger.LogInformation( "For {PackageId} {PackageVersion}, metadata will remain the same.", identity.Id, identity.Version.ToNormalizedString(), leaf.Url, existing.Count); continue; } else { _logger.LogInformation( "For {PackageId} {PackageVersion}, metadata from {Url} will be used instead of {Count} catalog commit items.", identity.Id, identity.Version.ToNormalizedString(), leaf.Url, existing.Count); } } else { _logger.LogInformation( "For {PackageId} {PackageVersion}, metadata from {Url} will be used.", identity.Id, identity.Version.ToNormalizedString(), leaf.Url); } identityToItems[identity] = new List <CatalogCommitItem> { new CatalogCommitItem( new Uri(leaf.Url), leaf.CommitId, leaf.CommitTimestamp.UtcDateTime, new string[0], new[] { Schema.DataTypes.PackageDetails }, identity), }; } } _logger.LogInformation("The catalog commit item list has been modified to fix up the missing document(s)."); var newItemList = identityToItems.SelectMany(x => x.Value).ToList(); return(DocumentFixUp.IsApplicable(newItemList)); }