This repository has been archived by the owner on Jul 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #363 from NuGet/dev
[ReleasePrep][2018.03.08]FI of master into dev
- Loading branch information
Showing
38 changed files
with
1,638 additions
and
773 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
src/NuGet.Services.Validation.Orchestrator/BaseValidator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Threading.Tasks; | ||
|
||
namespace NuGet.Services.Validation.Orchestrator | ||
{ | ||
public abstract class BaseValidator | ||
{ | ||
public virtual Task CleanUpAsync(IValidationRequest request) | ||
{ | ||
return Task.CompletedTask; | ||
} | ||
} | ||
} |
151 changes: 151 additions & 0 deletions
151
src/NuGet.Services.Validation.Orchestrator/Configuration/ConfigurationValidator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Configuration; | ||
using System.Linq; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace NuGet.Services.Validation.Orchestrator | ||
{ | ||
/// <summary> | ||
/// Provides a methods for checking configuration validity | ||
/// </summary> | ||
public class ConfigurationValidator | ||
{ | ||
private readonly IValidatorProvider _validatorProvider; | ||
private readonly ValidationConfiguration _configuration; | ||
|
||
public ConfigurationValidator( | ||
IValidatorProvider validatorProvider, | ||
IOptionsSnapshot<ValidationConfiguration> optionsAccessor) | ||
{ | ||
if (optionsAccessor == null) | ||
{ | ||
throw new ArgumentNullException(nameof(optionsAccessor)); | ||
} | ||
|
||
_validatorProvider = validatorProvider ?? throw new ArgumentNullException(nameof(validatorProvider)); | ||
_configuration = optionsAccessor.Value ?? throw new ArgumentException("Value property cannot be null", nameof(optionsAccessor)); | ||
} | ||
|
||
/// <summary> | ||
/// Checks if configuration object is valid | ||
/// </summary> | ||
public void Validate() | ||
{ | ||
CheckValidationsNumber(); | ||
|
||
CheckPropertyValues(); | ||
|
||
CheckDuplicateValidations(); | ||
|
||
CheckUnknownPrerequisites(); | ||
|
||
CheckUnknownValidators(); | ||
|
||
CheckForCyclesAndParallelProcessors(); | ||
|
||
CheckForUnrunnableRequiredValidations(); | ||
} | ||
|
||
private void CheckValidationsNumber() | ||
{ | ||
if (_configuration.Validations == null || !_configuration.Validations.Any()) | ||
{ | ||
throw new ConfigurationErrorsException("Must have at least one validation declared"); | ||
} | ||
} | ||
|
||
private void CheckPropertyValues() | ||
{ | ||
foreach (var validationConfigurationItem in _configuration.Validations) | ||
{ | ||
if (string.IsNullOrWhiteSpace(validationConfigurationItem.Name)) | ||
{ | ||
throw new ConfigurationErrorsException("Validation name cannot be empty"); | ||
} | ||
|
||
if (validationConfigurationItem.FailAfter == TimeSpan.Zero) | ||
{ | ||
throw new ConfigurationErrorsException($"failAfter timeout must be set for validation {validationConfigurationItem.Name}"); | ||
} | ||
} | ||
} | ||
|
||
private void CheckDuplicateValidations() | ||
{ | ||
var duplicateValidations = _configuration.Validations | ||
.Select(v => v.Name) | ||
.GroupBy(n => n) | ||
.Where(g => g.Count() > 1) | ||
.ToList(); | ||
if (duplicateValidations.Any()) | ||
{ | ||
throw new ConfigurationErrorsException($"Duplicate validations: {string.Join(", ", duplicateValidations.Select(d => d.Key))}"); | ||
} | ||
} | ||
|
||
private void CheckUnknownPrerequisites() | ||
{ | ||
var declaredValidations = new HashSet<string>(_configuration.Validations.Select(v => v.Name)); | ||
var prerequisites = new HashSet<string>(_configuration.Validations.Select(v => v.RequiredValidations).SelectMany(p => p)); | ||
prerequisites.ExceptWith(declaredValidations); | ||
if (prerequisites.Any()) | ||
{ | ||
throw new ConfigurationErrorsException($"Unknown validations set as prerequisites: {string.Join(", ", prerequisites)}"); | ||
} | ||
} | ||
|
||
private void CheckUnknownValidators() | ||
{ | ||
foreach (var validatorItem in _configuration.Validations) | ||
{ | ||
// This method will throw if the validator does not exist. | ||
var validatorType = _validatorProvider.GetValidatorType(validatorItem.Name); | ||
if (validatorType == null) | ||
{ | ||
throw new ConfigurationErrorsException("Validator implementation not found for " + validatorItem.Name); | ||
} | ||
} | ||
} | ||
|
||
private void CheckForUnrunnableRequiredValidations() | ||
{ | ||
// checks for the case when validation that must run depends on a validation that | ||
// is configured not to run | ||
// we'll just walk up the dependency chain of each runnable validation and look for | ||
// not runnable validations | ||
|
||
var validations = _configuration.Validations.ToDictionary(v => v.Name); | ||
var runnableValidations = _configuration.Validations.Where(v => v.ShouldStart); | ||
|
||
foreach (var validation in runnableValidations) | ||
{ | ||
var checkQueue = new Queue<string>(validation.RequiredValidations); | ||
while (checkQueue.Any()) | ||
{ | ||
var requiredValidationName = checkQueue.Dequeue(); | ||
var requiredValidation = validations[requiredValidationName]; | ||
if (!requiredValidation.ShouldStart) | ||
{ | ||
throw new ConfigurationErrorsException($"Runnable validation {validation.Name} cannot be run because it requires non-runnable validation {requiredValidationName} to complete before it can be started."); | ||
} | ||
requiredValidation.RequiredValidations.ForEach(checkQueue.Enqueue); | ||
} | ||
} | ||
} | ||
|
||
private void CheckForCyclesAndParallelProcessors() | ||
{ | ||
var processorNames = _configuration | ||
.Validations | ||
.Select(x => x.Name) | ||
.Where(x => typeof(IProcessor).IsAssignableFrom(_validatorProvider.GetValidatorType(x))) | ||
.ToList(); | ||
|
||
TopologicalSort.Validate(_configuration.Validations, processorNames); | ||
} | ||
} | ||
} |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
156 changes: 156 additions & 0 deletions
156
src/NuGet.Services.Validation.Orchestrator/Configuration/TopologicalSort.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Configuration; | ||
using System.Linq; | ||
|
||
namespace NuGet.Services.Validation.Orchestrator | ||
{ | ||
public static class TopologicalSort | ||
{ | ||
/// <summary> | ||
/// Processors cannot run in parallel with other processors or even with other validators. Suppose you have the | ||
/// following validator graph (where --> indicates a validator dependent) | ||
/// | ||
/// ---> Validator B --- | ||
/// / \ | ||
/// Validator A --- ---> Validator D | ||
/// \ / | ||
/// ---> Validator C --- | ||
/// | ||
/// In this case, B and C cannot be processors. A and D can. Given a graph of validator dependencies and the | ||
/// list of validators that are processors, we use the following algorithm to determine whether it is possible | ||
/// for a processor to run in parallel with anything else. | ||
/// | ||
/// 1. Enumerate all valid orderings using topological sort. In our example above, this would be: | ||
/// - A B C D | ||
/// - A C B D | ||
/// 2. For each processor, verify that the position in all orderings is the same. | ||
/// - Note that A is always first and D is always fourth. | ||
/// | ||
/// This allows us to verify that a validator configuration is safe before orchestrator even starts accepting | ||
/// validation messages. | ||
/// </summary> | ||
/// <param name="validators">The validator configuration items.</param> | ||
/// <param name="cannotBeParallel">The names of validators that are also processors.</param> | ||
/// <exception cref="ConfigurationErrorsException"> | ||
/// Thrown if a cycle or parallel processor is found | ||
/// </exception> | ||
public static void Validate(IReadOnlyList<ValidationConfigurationItem> validators, IReadOnlyList<string> cannotBeParallel) | ||
{ | ||
var allOrders = EnumerateAll(validators); | ||
if (!allOrders.Any()) | ||
{ | ||
throw new ConfigurationErrorsException("No validation sequences were found. This indicates a cycle in the validation dependencies."); | ||
} | ||
|
||
// A dictionary mapping the name of the validator to its index in the first topological sort result. All | ||
// other results must have their processors at the same indexes. If this is true, that means that no | ||
// validators or processors can run in parallel with any processor. | ||
var nameToExpectedIndex = allOrders[0] | ||
.Select((x, i) => new { Name = x, Index = i }) | ||
.ToDictionary(x => x.Name, x => x.Index); | ||
|
||
foreach (var order in allOrders.Skip(1)) | ||
{ | ||
foreach (var name in cannotBeParallel) | ||
{ | ||
var index = nameToExpectedIndex[name]; | ||
var otherName = order[index]; | ||
if (otherName != name) | ||
{ | ||
throw new ConfigurationErrorsException( | ||
$"The processor {name} could run in parallel with {otherName}. Processors must not run " + | ||
$"in parallel with any other validators."); | ||
} | ||
} | ||
} | ||
} | ||
|
||
public static List<List<string>> EnumerateAll(IReadOnlyList<ValidationConfigurationItem> validators) | ||
{ | ||
// Build the graph. | ||
var graph = validators.ToDictionary(x => x.Name, x => new ValidatorNode(x.Name)); | ||
|
||
// Invert the node relationship. Validators specify what they depend on. Nodes in a directed graph to be | ||
// explored using topological sort should specify what their dependents are. | ||
foreach (var validator in validators) | ||
{ | ||
foreach (var dependencyName in validator.RequiredValidations) | ||
{ | ||
graph[validator.Name].InDegree++; | ||
graph[dependencyName].DependentValidations.Add(validator.Name); | ||
} | ||
} | ||
|
||
// Enumerate all combindations. | ||
var allResults = new List<List<string>>(); | ||
AllTopologicalSort(graph, new List<string>(), allResults); | ||
|
||
return allResults; | ||
} | ||
|
||
/// <summary> | ||
/// Executes topological sort on the provided graph of validators. All possible results are enumerated and | ||
/// returned. | ||
/// </summary> | ||
/// <remarks> | ||
/// Source: https://www.geeksforgeeks.org/all-topological-sorts-of-a-directed-acyclic-graph/ | ||
/// </remarks> | ||
private static void AllTopologicalSort( | ||
IReadOnlyDictionary<string, ValidatorNode> graph, | ||
List<string> currentResult, | ||
List<List<string>> allResults) | ||
{ | ||
var done = false; | ||
|
||
foreach (var node in graph.Values) | ||
{ | ||
if (node.InDegree == 0 && !node.Visited) | ||
{ | ||
foreach (var dependencyName in node.DependentValidations) | ||
{ | ||
graph[dependencyName].InDegree--; | ||
} | ||
|
||
currentResult.Add(node.Name); | ||
node.Visited = true; | ||
|
||
// Recurse. | ||
AllTopologicalSort(graph, currentResult, allResults); | ||
|
||
node.Visited = false; | ||
currentResult.RemoveAt(currentResult.Count - 1); | ||
|
||
foreach (var dependencyName in node.DependentValidations) | ||
{ | ||
graph[dependencyName].InDegree++; | ||
} | ||
|
||
done = true; | ||
} | ||
} | ||
|
||
if (!done && currentResult.Count == graph.Count) | ||
{ | ||
// Append a copy of the running result. | ||
allResults.Add(currentResult.ToList()); | ||
} | ||
} | ||
|
||
private class ValidatorNode | ||
{ | ||
public ValidatorNode(string name) | ||
{ | ||
Name = name ?? throw new ArgumentNullException(nameof(name)); | ||
} | ||
|
||
public string Name { get; } | ||
public List<string> DependentValidations { get; } = new List<string>(); | ||
public int InDegree { get; set; } | ||
public bool Visited { get; set; } | ||
} | ||
} | ||
} |
File renamed without changes.
File renamed without changes.
File renamed without changes.
Oops, something went wrong.