Skip to content
This repository was archived by the owner on Jul 30, 2024. It is now read-only.

Commit 5a85bee

Browse files
authored
Merge pull request #363 from NuGet/dev
[ReleasePrep][2018.03.08]FI of master into dev
2 parents 57315b9 + 417240f commit 5a85bee

38 files changed

Lines changed: 1638 additions & 773 deletions

build.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ trap {
2626
if (-not (Test-Path "$PSScriptRoot/build")) {
2727
New-Item -Path "$PSScriptRoot/build" -ItemType "directory"
2828
}
29+
30+
# Enable TLS 1.2 since GitHub requires it.
31+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
32+
2933
wget -UseBasicParsing -Uri "https://raw.githubusercontent.com/NuGet/ServerCommon/$BuildBranch/build/init.ps1" -OutFile "$PSScriptRoot/build/init.ps1"
3034
. "$PSScriptRoot/build/init.ps1" -BuildBranch "$BuildBranch"
3135

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
6+
namespace NuGet.Services.Validation.Orchestrator
7+
{
8+
public abstract class BaseValidator
9+
{
10+
public virtual Task CleanUpAsync(IValidationRequest request)
11+
{
12+
return Task.CompletedTask;
13+
}
14+
}
15+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Configuration;
7+
using System.Linq;
8+
using Microsoft.Extensions.Options;
9+
10+
namespace NuGet.Services.Validation.Orchestrator
11+
{
12+
/// <summary>
13+
/// Provides a methods for checking configuration validity
14+
/// </summary>
15+
public class ConfigurationValidator
16+
{
17+
private readonly IValidatorProvider _validatorProvider;
18+
private readonly ValidationConfiguration _configuration;
19+
20+
public ConfigurationValidator(
21+
IValidatorProvider validatorProvider,
22+
IOptionsSnapshot<ValidationConfiguration> optionsAccessor)
23+
{
24+
if (optionsAccessor == null)
25+
{
26+
throw new ArgumentNullException(nameof(optionsAccessor));
27+
}
28+
29+
_validatorProvider = validatorProvider ?? throw new ArgumentNullException(nameof(validatorProvider));
30+
_configuration = optionsAccessor.Value ?? throw new ArgumentException("Value property cannot be null", nameof(optionsAccessor));
31+
}
32+
33+
/// <summary>
34+
/// Checks if configuration object is valid
35+
/// </summary>
36+
public void Validate()
37+
{
38+
CheckValidationsNumber();
39+
40+
CheckPropertyValues();
41+
42+
CheckDuplicateValidations();
43+
44+
CheckUnknownPrerequisites();
45+
46+
CheckUnknownValidators();
47+
48+
CheckForCyclesAndParallelProcessors();
49+
50+
CheckForUnrunnableRequiredValidations();
51+
}
52+
53+
private void CheckValidationsNumber()
54+
{
55+
if (_configuration.Validations == null || !_configuration.Validations.Any())
56+
{
57+
throw new ConfigurationErrorsException("Must have at least one validation declared");
58+
}
59+
}
60+
61+
private void CheckPropertyValues()
62+
{
63+
foreach (var validationConfigurationItem in _configuration.Validations)
64+
{
65+
if (string.IsNullOrWhiteSpace(validationConfigurationItem.Name))
66+
{
67+
throw new ConfigurationErrorsException("Validation name cannot be empty");
68+
}
69+
70+
if (validationConfigurationItem.FailAfter == TimeSpan.Zero)
71+
{
72+
throw new ConfigurationErrorsException($"failAfter timeout must be set for validation {validationConfigurationItem.Name}");
73+
}
74+
}
75+
}
76+
77+
private void CheckDuplicateValidations()
78+
{
79+
var duplicateValidations = _configuration.Validations
80+
.Select(v => v.Name)
81+
.GroupBy(n => n)
82+
.Where(g => g.Count() > 1)
83+
.ToList();
84+
if (duplicateValidations.Any())
85+
{
86+
throw new ConfigurationErrorsException($"Duplicate validations: {string.Join(", ", duplicateValidations.Select(d => d.Key))}");
87+
}
88+
}
89+
90+
private void CheckUnknownPrerequisites()
91+
{
92+
var declaredValidations = new HashSet<string>(_configuration.Validations.Select(v => v.Name));
93+
var prerequisites = new HashSet<string>(_configuration.Validations.Select(v => v.RequiredValidations).SelectMany(p => p));
94+
prerequisites.ExceptWith(declaredValidations);
95+
if (prerequisites.Any())
96+
{
97+
throw new ConfigurationErrorsException($"Unknown validations set as prerequisites: {string.Join(", ", prerequisites)}");
98+
}
99+
}
100+
101+
private void CheckUnknownValidators()
102+
{
103+
foreach (var validatorItem in _configuration.Validations)
104+
{
105+
// This method will throw if the validator does not exist.
106+
var validatorType = _validatorProvider.GetValidatorType(validatorItem.Name);
107+
if (validatorType == null)
108+
{
109+
throw new ConfigurationErrorsException("Validator implementation not found for " + validatorItem.Name);
110+
}
111+
}
112+
}
113+
114+
private void CheckForUnrunnableRequiredValidations()
115+
{
116+
// checks for the case when validation that must run depends on a validation that
117+
// is configured not to run
118+
// we'll just walk up the dependency chain of each runnable validation and look for
119+
// not runnable validations
120+
121+
var validations = _configuration.Validations.ToDictionary(v => v.Name);
122+
var runnableValidations = _configuration.Validations.Where(v => v.ShouldStart);
123+
124+
foreach (var validation in runnableValidations)
125+
{
126+
var checkQueue = new Queue<string>(validation.RequiredValidations);
127+
while (checkQueue.Any())
128+
{
129+
var requiredValidationName = checkQueue.Dequeue();
130+
var requiredValidation = validations[requiredValidationName];
131+
if (!requiredValidation.ShouldStart)
132+
{
133+
throw new ConfigurationErrorsException($"Runnable validation {validation.Name} cannot be run because it requires non-runnable validation {requiredValidationName} to complete before it can be started.");
134+
}
135+
requiredValidation.RequiredValidations.ForEach(checkQueue.Enqueue);
136+
}
137+
}
138+
}
139+
140+
private void CheckForCyclesAndParallelProcessors()
141+
{
142+
var processorNames = _configuration
143+
.Validations
144+
.Select(x => x.Name)
145+
.Where(x => typeof(IProcessor).IsAssignableFrom(_validatorProvider.GetValidatorType(x)))
146+
.ToList();
147+
148+
TopologicalSort.Validate(_configuration.Validations, processorNames);
149+
}
150+
}
151+
}

src/NuGet.Services.Validation.Orchestrator/CoreMessageServiceConfiguration.cs renamed to src/NuGet.Services.Validation.Orchestrator/Configuration/CoreMessageServiceConfiguration.cs

File renamed without changes.

src/NuGet.Services.Validation.Orchestrator/EmailConfiguration.cs renamed to src/NuGet.Services.Validation.Orchestrator/Configuration/EmailConfiguration.cs

File renamed without changes.

src/NuGet.Services.Validation.Orchestrator/OrchestrationRunnerConfiguration.cs renamed to src/NuGet.Services.Validation.Orchestrator/Configuration/OrchestrationRunnerConfiguration.cs

File renamed without changes.

src/NuGet.Services.Validation.Orchestrator/SmtpConfiguration.cs renamed to src/NuGet.Services.Validation.Orchestrator/Configuration/SmtpConfiguration.cs

File renamed without changes.
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Configuration;
7+
using System.Linq;
8+
9+
namespace NuGet.Services.Validation.Orchestrator
10+
{
11+
public static class TopologicalSort
12+
{
13+
/// <summary>
14+
/// Processors cannot run in parallel with other processors or even with other validators. Suppose you have the
15+
/// following validator graph (where --> indicates a validator dependent)
16+
///
17+
/// ---> Validator B ---
18+
/// / \
19+
/// Validator A --- ---> Validator D
20+
/// \ /
21+
/// ---> Validator C ---
22+
///
23+
/// In this case, B and C cannot be processors. A and D can. Given a graph of validator dependencies and the
24+
/// list of validators that are processors, we use the following algorithm to determine whether it is possible
25+
/// for a processor to run in parallel with anything else.
26+
///
27+
/// 1. Enumerate all valid orderings using topological sort. In our example above, this would be:
28+
/// - A B C D
29+
/// - A C B D
30+
/// 2. For each processor, verify that the position in all orderings is the same.
31+
/// - Note that A is always first and D is always fourth.
32+
///
33+
/// This allows us to verify that a validator configuration is safe before orchestrator even starts accepting
34+
/// validation messages.
35+
/// </summary>
36+
/// <param name="validators">The validator configuration items.</param>
37+
/// <param name="cannotBeParallel">The names of validators that are also processors.</param>
38+
/// <exception cref="ConfigurationErrorsException">
39+
/// Thrown if a cycle or parallel processor is found
40+
/// </exception>
41+
public static void Validate(IReadOnlyList<ValidationConfigurationItem> validators, IReadOnlyList<string> cannotBeParallel)
42+
{
43+
var allOrders = EnumerateAll(validators);
44+
if (!allOrders.Any())
45+
{
46+
throw new ConfigurationErrorsException("No validation sequences were found. This indicates a cycle in the validation dependencies.");
47+
}
48+
49+
// A dictionary mapping the name of the validator to its index in the first topological sort result. All
50+
// other results must have their processors at the same indexes. If this is true, that means that no
51+
// validators or processors can run in parallel with any processor.
52+
var nameToExpectedIndex = allOrders[0]
53+
.Select((x, i) => new { Name = x, Index = i })
54+
.ToDictionary(x => x.Name, x => x.Index);
55+
56+
foreach (var order in allOrders.Skip(1))
57+
{
58+
foreach (var name in cannotBeParallel)
59+
{
60+
var index = nameToExpectedIndex[name];
61+
var otherName = order[index];
62+
if (otherName != name)
63+
{
64+
throw new ConfigurationErrorsException(
65+
$"The processor {name} could run in parallel with {otherName}. Processors must not run " +
66+
$"in parallel with any other validators.");
67+
}
68+
}
69+
}
70+
}
71+
72+
public static List<List<string>> EnumerateAll(IReadOnlyList<ValidationConfigurationItem> validators)
73+
{
74+
// Build the graph.
75+
var graph = validators.ToDictionary(x => x.Name, x => new ValidatorNode(x.Name));
76+
77+
// Invert the node relationship. Validators specify what they depend on. Nodes in a directed graph to be
78+
// explored using topological sort should specify what their dependents are.
79+
foreach (var validator in validators)
80+
{
81+
foreach (var dependencyName in validator.RequiredValidations)
82+
{
83+
graph[validator.Name].InDegree++;
84+
graph[dependencyName].DependentValidations.Add(validator.Name);
85+
}
86+
}
87+
88+
// Enumerate all combindations.
89+
var allResults = new List<List<string>>();
90+
AllTopologicalSort(graph, new List<string>(), allResults);
91+
92+
return allResults;
93+
}
94+
95+
/// <summary>
96+
/// Executes topological sort on the provided graph of validators. All possible results are enumerated and
97+
/// returned.
98+
/// </summary>
99+
/// <remarks>
100+
/// Source: https://www.geeksforgeeks.org/all-topological-sorts-of-a-directed-acyclic-graph/
101+
/// </remarks>
102+
private static void AllTopologicalSort(
103+
IReadOnlyDictionary<string, ValidatorNode> graph,
104+
List<string> currentResult,
105+
List<List<string>> allResults)
106+
{
107+
var done = false;
108+
109+
foreach (var node in graph.Values)
110+
{
111+
if (node.InDegree == 0 && !node.Visited)
112+
{
113+
foreach (var dependencyName in node.DependentValidations)
114+
{
115+
graph[dependencyName].InDegree--;
116+
}
117+
118+
currentResult.Add(node.Name);
119+
node.Visited = true;
120+
121+
// Recurse.
122+
AllTopologicalSort(graph, currentResult, allResults);
123+
124+
node.Visited = false;
125+
currentResult.RemoveAt(currentResult.Count - 1);
126+
127+
foreach (var dependencyName in node.DependentValidations)
128+
{
129+
graph[dependencyName].InDegree++;
130+
}
131+
132+
done = true;
133+
}
134+
}
135+
136+
if (!done && currentResult.Count == graph.Count)
137+
{
138+
// Append a copy of the running result.
139+
allResults.Add(currentResult.ToList());
140+
}
141+
}
142+
143+
private class ValidatorNode
144+
{
145+
public ValidatorNode(string name)
146+
{
147+
Name = name ?? throw new ArgumentNullException(nameof(name));
148+
}
149+
150+
public string Name { get; }
151+
public List<string> DependentValidations { get; } = new List<string>();
152+
public int InDegree { get; set; }
153+
public bool Visited { get; set; }
154+
}
155+
}
156+
}

src/NuGet.Services.Validation.Orchestrator/ValidationConfiguration.cs renamed to src/NuGet.Services.Validation.Orchestrator/Configuration/ValidationConfiguration.cs

File renamed without changes.

src/NuGet.Services.Validation.Orchestrator/ValidationConfigurationItem.cs renamed to src/NuGet.Services.Validation.Orchestrator/Configuration/ValidationConfigurationItem.cs

File renamed without changes.

0 commit comments

Comments
 (0)