Merge branch 'feature/continuous-peer-test'

This commit is contained in:
benbierens 2023-09-04 09:15:30 +02:00
commit a528c0111d
No known key found for this signature in database
GPG Key ID: FE44815D96D0A1AA
15 changed files with 131 additions and 82 deletions

View File

@ -28,6 +28,7 @@ namespace CodexNetDeployer
var workflowStartup = new StartupConfig(); var workflowStartup = new StartupConfig();
workflowStartup.Add(gethResult); workflowStartup.Add(gethResult);
workflowStartup.Add(CreateCodexStartupConfig(bootstrapSpr, i, validatorsLeft)); workflowStartup.Add(CreateCodexStartupConfig(bootstrapSpr, i, validatorsLeft));
workflowStartup.NameOverride = GetCodexContainerName(i);
var containers = workflow.Start(1, Location.Unspecified, new CodexContainerRecipe(), workflowStartup); var containers = workflow.Start(1, Location.Unspecified, new CodexContainerRecipe(), workflowStartup);
@ -75,6 +76,12 @@ namespace CodexNetDeployer
return null; return null;
} }
private string GetCodexContainerName(int i)
{
if (i == 0) return "BOOTSTRAP";
return "CODEX" + i;
}
private CodexStartupConfig CreateCodexStartupConfig(string bootstrapSpr, int i, int validatorsLeft) private CodexStartupConfig CreateCodexStartupConfig(string bootstrapSpr, int i, int validatorsLeft)
{ {
var codexStart = new CodexStartupConfig(config.CodexLogLevel); var codexStart = new CodexStartupConfig(config.CodexLogLevel);

View File

@ -22,8 +22,8 @@ namespace ContinuousTests
[Uniform("kube-config", "kc", "KUBECONFIG", true, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")] [Uniform("kube-config", "kc", "KUBECONFIG", true, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")]
public string KubeConfigFile { get; set; } = "null"; public string KubeConfigFile { get; set; } = "null";
[Uniform("stop", "s", "STOPONFAIL", false, "If true, runner will stop on first test failure and download all cluster container logs. False by default.")] [Uniform("stop", "s", "STOPONFAIL", false, "If greater than zero, runner will stop after this many test failures and download all cluster container logs. 0 by default.")]
public bool StopOnFailure { get; set; } = false; public int StopOnFailure { get; set; } = 0;
[Uniform("dl-logs", "dl", "DLLOGS", false, "If true, runner will periodically download and save/append container logs to the log path.")] [Uniform("dl-logs", "dl", "DLLOGS", false, "If true, runner will periodically download and save/append container logs to the log path.")]
public bool DownloadContainerLogs { get; set; } = false; public bool DownloadContainerLogs { get; set; } = false;

View File

@ -30,7 +30,7 @@ namespace ContinuousTests
ClearAllCustomNamespaces(allTests, overviewLog); ClearAllCustomNamespaces(allTests, overviewLog);
var testLoops = allTests.Select(t => new TestLoop(taskFactory, config, overviewLog, t.GetType(), t.RunTestEvery, cancelToken)).ToArray(); var testLoops = allTests.Select(t => new TestLoop(taskFactory, config, overviewLog, t.GetType(), t.RunTestEvery, startupChecker, cancelToken)).ToArray();
foreach (var testLoop in testLoops) foreach (var testLoop in testLoops)
{ {

View File

@ -23,8 +23,9 @@ namespace ContinuousTests
private readonly FixtureLog fixtureLog; private readonly FixtureLog fixtureLog;
private readonly string testName; private readonly string testName;
private readonly string dataFolder; private readonly string dataFolder;
private static int failureCount = 0;
public SingleTestRun(TaskFactory taskFactory, Configuration config, BaseLog overviewLog, TestHandle handle, CancellationToken cancelToken) public SingleTestRun(TaskFactory taskFactory, Configuration config, BaseLog overviewLog, TestHandle handle, StartupChecker startupChecker, CancellationToken cancelToken)
{ {
this.taskFactory = taskFactory; this.taskFactory = taskFactory;
this.config = config; this.config = config;
@ -33,8 +34,9 @@ namespace ContinuousTests
this.cancelToken = cancelToken; this.cancelToken = cancelToken;
testName = handle.Test.GetType().Name; testName = handle.Test.GetType().Name;
fixtureLog = new FixtureLog(new LogConfig(config.LogPath, true), DateTime.UtcNow, testName); fixtureLog = new FixtureLog(new LogConfig(config.LogPath, true), DateTime.UtcNow, testName);
ApplyLogReplacements(fixtureLog, startupChecker);
nodes = CreateRandomNodes(handle.Test.RequiredNumberOfNodes); nodes = CreateRandomNodes();
dataFolder = config.DataPath + "-" + Guid.NewGuid(); dataFolder = config.DataPath + "-" + Guid.NewGuid();
fileManager = new FileManager(fixtureLog, CreateFileManagerConfiguration()); fileManager = new FileManager(fixtureLog, CreateFileManagerConfiguration());
} }
@ -71,16 +73,26 @@ namespace ContinuousTests
fixtureLog.Error("Test run failed with exception: " + ex); fixtureLog.Error("Test run failed with exception: " + ex);
fixtureLog.MarkAsFailed(); fixtureLog.MarkAsFailed();
if (config.StopOnFailure) failureCount++;
if (config.StopOnFailure > 0)
{ {
OverviewLog("Configured to stop on first failure. Downloading cluster logs..."); OverviewLog($"Failures: {failureCount} / {config.StopOnFailure}");
DownloadClusterLogs(); if (failureCount >= config.StopOnFailure)
OverviewLog("Log download finished. Cancelling test runner..."); {
Cancellation.Cts.Cancel(); OverviewLog($"Configured to stop after {config.StopOnFailure} failures. Downloading cluster logs...");
DownloadClusterLogs();
OverviewLog("Log download finished. Cancelling test runner...");
Cancellation.Cts.Cancel();
}
} }
} }
} }
private void ApplyLogReplacements(FixtureLog fixtureLog, StartupChecker startupChecker)
{
foreach (var replacement in startupChecker.LogReplacements) fixtureLog.AddStringReplace(replacement.From, replacement.To);
}
private void RunTestMoments() private void RunTestMoments()
{ {
var earliestMoment = handle.GetEarliestMoment(); var earliestMoment = handle.GetEarliestMoment();
@ -112,7 +124,7 @@ namespace ContinuousTests
{ {
ThrowFailTest(); ThrowFailTest();
} }
OverviewLog(" > Test passed. " + FuturesInfo()); OverviewLog(" > Test passed.");
return; return;
} }
} }
@ -120,25 +132,19 @@ namespace ContinuousTests
private void ThrowFailTest() private void ThrowFailTest()
{ {
var ex = UnpackException(exceptions.First()); var exs = UnpackExceptions(exceptions);
Log(ex.ToString()); var exceptionsMessage = GetCombinedExceptionsMessage(exs);
OverviewLog($" > Test failed {FuturesInfo()}: " + ex.Message); Log(exceptionsMessage);
throw ex; OverviewLog($" > Test failed: " + exceptionsMessage);
} throw new Exception(exceptionsMessage);
private string FuturesInfo()
{
var containers = config.CodexDeployment.CodexContainers;
var nodes = codexNodeFactory.Create(config, containers, fixtureLog, handle.Test.TimeSet);
var f = nodes.Select(n => n.GetDebugFutures().ToString());
var msg = $"(Futures: [{string.Join(", ", f)}])";
return msg;
} }
private void DownloadClusterLogs() private void DownloadClusterLogs()
{ {
var k8sFactory = new K8sFactory(); var k8sFactory = new K8sFactory();
var lifecycle = k8sFactory.CreateTestLifecycle(config.KubeConfigFile, config.LogPath, "dataPath", config.CodexDeployment.Metadata.KubeNamespace, new DefaultTimeSet(), new NullLog()); var log = new NullLog();
log.FullFilename = Path.Combine(config.LogPath, "NODE");
var lifecycle = k8sFactory.CreateTestLifecycle(config.KubeConfigFile, config.LogPath, "dataPath", config.CodexDeployment.Metadata.KubeNamespace, new DefaultTimeSet(), log);
foreach (var container in config.CodexDeployment.CodexContainers) foreach (var container in config.CodexDeployment.CodexContainers)
{ {
@ -146,6 +152,16 @@ namespace ContinuousTests
} }
} }
private string GetCombinedExceptionsMessage(Exception[] exceptions)
{
return string.Join(Environment.NewLine, exceptions.Select(ex => ex.ToString()));
}
private Exception[] UnpackExceptions(List<Exception> exceptions)
{
return exceptions.Select(UnpackException).ToArray();
}
private Exception UnpackException(Exception exception) private Exception UnpackException(Exception exception)
{ {
if (exception is AggregateException a) if (exception is AggregateException a)
@ -196,19 +212,26 @@ namespace ContinuousTests
private void OverviewLog(string msg) private void OverviewLog(string msg)
{ {
Log(msg); Log(msg);
var containerNames = $"({string.Join(",", nodes.Select(n => n.Container.Name))})"; var containerNames = GetContainerNames();
overviewLog.Log($"{containerNames} {testName}: {msg}"); overviewLog.Log($"{containerNames} {testName}: {msg}");
} }
private CodexAccess[] CreateRandomNodes(int number) private string GetContainerNames()
{ {
var containers = SelectRandomContainers(number); if (handle.Test.RequiredNumberOfNodes == -1) return "(All Nodes)";
return $"({string.Join(",", nodes.Select(n => n.Container.Name))})";
}
private CodexAccess[] CreateRandomNodes()
{
var containers = SelectRandomContainers();
fixtureLog.Log("Selected nodes: " + string.Join(",", containers.Select(c => c.Name))); fixtureLog.Log("Selected nodes: " + string.Join(",", containers.Select(c => c.Name)));
return codexNodeFactory.Create(config, containers, fixtureLog, handle.Test.TimeSet); return codexNodeFactory.Create(config, containers, fixtureLog, handle.Test.TimeSet);
} }
private RunningContainer[] SelectRandomContainers(int number) private RunningContainer[] SelectRandomContainers()
{ {
var number = handle.Test.RequiredNumberOfNodes;
if (number == -1) return config.CodexDeployment.CodexContainers; if (number == -1) return config.CodexDeployment.CodexContainers;
var containers = config.CodexDeployment.CodexContainers.ToList(); var containers = config.CodexDeployment.CodexContainers.ToList();

View File

@ -15,6 +15,7 @@ namespace ContinuousTests
{ {
this.config = config; this.config = config;
this.cancelToken = cancelToken; this.cancelToken = cancelToken;
LogReplacements = new List<BaseLogStringReplacement>();
} }
public void Check() public void Check()
@ -28,6 +29,8 @@ namespace ContinuousTests
log.Log("All OK."); log.Log("All OK.");
} }
public List<BaseLogStringReplacement> LogReplacements { get; }
private void PreflightCheck(Configuration config) private void PreflightCheck(Configuration config)
{ {
var tests = testFactory.CreateTests(); var tests = testFactory.CreateTests();
@ -90,6 +93,7 @@ namespace ContinuousTests
if (info == null || string.IsNullOrEmpty(info.id)) return false; if (info == null || string.IsNullOrEmpty(info.id)) return false;
log.Log($"Codex version: '{info.codex.version}' revision: '{info.codex.revision}'"); log.Log($"Codex version: '{info.codex.version}' revision: '{info.codex.revision}'");
LogReplacements.Add(new BaseLogStringReplacement(info.id, n.GetName()));
} }
catch catch
{ {

View File

@ -9,16 +9,18 @@ namespace ContinuousTests
private readonly BaseLog overviewLog; private readonly BaseLog overviewLog;
private readonly Type testType; private readonly Type testType;
private readonly TimeSpan runsEvery; private readonly TimeSpan runsEvery;
private readonly StartupChecker startupChecker;
private readonly CancellationToken cancelToken; private readonly CancellationToken cancelToken;
private readonly EventWaitHandle runFinishedHandle = new EventWaitHandle(true, EventResetMode.ManualReset); private readonly EventWaitHandle runFinishedHandle = new EventWaitHandle(true, EventResetMode.ManualReset);
public TestLoop(TaskFactory taskFactory, Configuration config, BaseLog overviewLog, Type testType, TimeSpan runsEvery, CancellationToken cancelToken) public TestLoop(TaskFactory taskFactory, Configuration config, BaseLog overviewLog, Type testType, TimeSpan runsEvery, StartupChecker startupChecker, CancellationToken cancelToken)
{ {
this.taskFactory = taskFactory; this.taskFactory = taskFactory;
this.config = config; this.config = config;
this.overviewLog = overviewLog; this.overviewLog = overviewLog;
this.testType = testType; this.testType = testType;
this.runsEvery = runsEvery; this.runsEvery = runsEvery;
this.startupChecker = startupChecker;
this.cancelToken = cancelToken; this.cancelToken = cancelToken;
Name = testType.Name; Name = testType.Name;
} }
@ -58,7 +60,7 @@ namespace ContinuousTests
{ {
var test = (ContinuousTest)Activator.CreateInstance(testType)!; var test = (ContinuousTest)Activator.CreateInstance(testType)!;
var handle = new TestHandle(test); var handle = new TestHandle(test);
var run = new SingleTestRun(taskFactory, config, overviewLog, handle, cancelToken); var run = new SingleTestRun(taskFactory, config, overviewLog, handle, startupChecker, cancelToken);
runFinishedHandle.Reset(); runFinishedHandle.Reset();
run.Run(runFinishedHandle); run.Run(runFinishedHandle);

View File

@ -15,17 +15,7 @@ namespace ContinuousTests.Tests
[TestMoment(t: Zero)] [TestMoment(t: Zero)]
public void UploadTestFile() public void UploadTestFile()
{ {
var metadata = Configuration.CodexDeployment.Metadata; var filesize = 80.MB();
var maxQuotaUseMb = metadata.StorageQuotaMB / 2;
var safeTTL = Math.Max(metadata.BlockTTL, metadata.BlockMI) + 30;
var runsPerTtl = Convert.ToInt32(safeTTL / RunTestEvery.TotalSeconds);
var filesizePerUploadMb = Math.Min(80, maxQuotaUseMb / runsPerTtl);
// This filesize should keep the quota below 50% of the node's max.
var filesize = filesizePerUploadMb.MB();
double codexDefaultBlockSize = 31 * 64 * 33;
var numberOfBlocks = Convert.ToInt64(Math.Ceiling(filesize.SizeInBytes / codexDefaultBlockSize));
Assert.That(numberOfBlocks, Is.EqualTo(1282));
file = FileManager.GenerateTestFile(filesize); file = FileManager.GenerateTestFile(filesize);

View File

@ -1,4 +1,5 @@
using DistTestCore.Codex; using DistTestCore.Codex;
using DistTestCore.Helpers;
using NUnit.Framework; using NUnit.Framework;
namespace ContinuousTests.Tests namespace ContinuousTests.Tests
@ -10,10 +11,24 @@ namespace ContinuousTests.Tests
public override TestFailMode TestFailMode => TestFailMode.AlwaysRunAllMoments; public override TestFailMode TestFailMode => TestFailMode.AlwaysRunAllMoments;
[TestMoment(t: 0)] [TestMoment(t: 0)]
public void CheckConnectivity()
{
var checker = new PeerConnectionTestHelpers(Log);
checker.AssertFullyConnected(Nodes);
}
[TestMoment(t: 10)]
public void CheckRoutingTables() public void CheckRoutingTables()
{ {
var allIds = Nodes.Select(n => n.GetDebugInfo().table.localNode.nodeId).ToArray(); var allInfos = Nodes.Select(n =>
{
var info = n.GetDebugInfo();
Log.Log($"{n.GetName()} = {info.table.localNode.nodeId}");
Log.AddStringReplace(info.table.localNode.nodeId, n.GetName());
return info;
}).ToArray();
var allIds = allInfos.Select(i => i.table.localNode.nodeId).ToArray();
var errors = Nodes.Select(n => AreAllPresent(n, allIds)).Where(s => !string.IsNullOrEmpty(s)).ToArray(); var errors = Nodes.Select(n => AreAllPresent(n, allIds)).Where(s => !string.IsNullOrEmpty(s)).ToArray();
if (errors.Any()) if (errors.Any())
@ -30,7 +45,10 @@ namespace ContinuousTests.Tests
if (!expected.All(ex => known.Contains(ex))) if (!expected.All(ex => known.Contains(ex)))
{ {
return $"Not all of '{string.Join(",", expected)}' were present in routing table: '{string.Join(",", known)}'"; var nl = Environment.NewLine;
return $"{nl}At node '{info.table.localNode.nodeId}'{nl}" +
$"Not all of{nl}'{string.Join(",", expected)}'{nl}" +
$"were present in routing table:{nl}'{string.Join(",", known)}'";
} }
return string.Empty; return string.Empty;

View File

@ -2,5 +2,5 @@ dotnet run \
--kube-config=/opt/kubeconfig.yaml \ --kube-config=/opt/kubeconfig.yaml \
--codex-deployment=codex-deployment.json \ --codex-deployment=codex-deployment.json \
--keep=1 \ --keep=1 \
--stop=1 \ --stop=10 \
--dl-logs=1 --dl-logs=1

View File

@ -47,12 +47,6 @@ namespace DistTestCore.Codex
return result; return result;
} }
public int GetDebugFutures()
{
// Some Codex images support debug/futures to count the number of open futures.
return 0; // Http().HttpGetJson<CodexDebugFutures>("debug/futures").futures;
}
public CodexDebugThresholdBreaches GetDebugThresholdBreaches() public CodexDebugThresholdBreaches GetDebugThresholdBreaches()
{ {
return Http().HttpGetJson<CodexDebugThresholdBreaches>("debug/loop"); return Http().HttpGetJson<CodexDebugThresholdBreaches>("debug/loop");
@ -75,7 +69,7 @@ namespace DistTestCore.Codex
public string RequestStorage(CodexSalesRequestStorageRequest request, string contentId) public string RequestStorage(CodexSalesRequestStorageRequest request, string contentId)
{ {
return Http().HttpPostJson<CodexSalesRequestStorageRequest, string>($"storage/request/{contentId}", request); return Http().HttpPostJson($"storage/request/{contentId}", request);
} }
public CodexStoragePurchase GetPurchaseStatus(string purchaseId) public CodexStoragePurchase GetPurchaseStatus(string purchaseId)

View File

@ -1,7 +1,4 @@
using KubernetesWorkflow; using Newtonsoft.Json;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace DistTestCore.Codex namespace DistTestCore.Codex
{ {

View File

@ -55,25 +55,20 @@ namespace DistTestCore
public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body) public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body)
{ {
var response = HttpPostJson(route, body); var response = PostJson(route, body);
var json = Time.Wait(response.Content.ReadAsStringAsync()); var json = Time.Wait(response.Content.ReadAsStringAsync());
if(!response.IsSuccessStatusCode) { if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(json); throw new HttpRequestException(json);
} }
Log(GetUrl() + route, json); Log(GetUrl() + route, json);
return TryJsonDeserialize<TResponse>(json); return TryJsonDeserialize<TResponse>(json);
} }
public HttpResponseMessage HttpPostJson<TRequest>(string route, TRequest body) public string HttpPostJson<TRequest>(string route, TRequest body)
{ {
return Retry(() => var response = PostJson<TRequest>(route, body);
{ return Time.Wait(response.Content.ReadAsStringAsync());
using var client = GetClient();
var url = GetUrl() + route;
using var content = JsonContent.Create(body);
Log(url, JsonConvert.SerializeObject(body));
return Time.Wait(client.PostAsync(url, content));
}, $"HTTP-POST-JSON: {route}");
} }
public string HttpPostString(string route, string body) public string HttpPostString(string route, string body)
@ -122,7 +117,8 @@ namespace DistTestCore
public T TryJsonDeserialize<T>(string json) public T TryJsonDeserialize<T>(string json)
{ {
var errors = new List<string>(); var errors = new List<string>();
var deserialized = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings(){ var deserialized = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings()
{
Error = delegate(object? sender, Serialization.ErrorEventArgs args) Error = delegate(object? sender, Serialization.ErrorEventArgs args)
{ {
if (args.CurrentObject == args.ErrorContext.OriginalObject) if (args.CurrentObject == args.ErrorContext.OriginalObject)
@ -136,15 +132,29 @@ namespace DistTestCore
} }
} }
}); });
if (errors.Count() > 0) { if (errors.Count() > 0)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}' with exception(s): \n{string.Join("\n", errors)}"); throw new JsonSerializationException($"Failed to deserialize JSON '{json}' with exception(s): \n{string.Join("\n", errors)}");
} }
else if (deserialized == null) { else if (deserialized == null)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}': resulting deserialized object is null"); throw new JsonSerializationException($"Failed to deserialize JSON '{json}': resulting deserialized object is null");
} }
return deserialized; return deserialized;
} }
private HttpResponseMessage PostJson<TRequest>(string route, TRequest body)
{
return Retry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
using var content = JsonContent.Create(body);
Log(url, JsonConvert.SerializeObject(body));
return Time.Wait(client.PostAsync(url, content));
}, $"HTTP-POST-JSON: {route}");
}
private string GetUrl() private string GetUrl()
{ {
return $"{address.Host}:{address.Port}{baseUrl}"; return $"{address.Host}:{address.Port}{baseUrl}";

View File

@ -60,6 +60,7 @@ namespace Logging
public virtual void AddStringReplace(string from, string to) public virtual void AddStringReplace(string from, string to)
{ {
if (string.IsNullOrWhiteSpace(from)) return; if (string.IsNullOrWhiteSpace(from)) return;
if (replacements.Any(r => r.From == from)) return;
replacements.Add(new BaseLogStringReplacement(from, to)); replacements.Add(new BaseLogStringReplacement(from, to));
} }
@ -98,20 +99,20 @@ namespace Logging
public class BaseLogStringReplacement public class BaseLogStringReplacement
{ {
private readonly string from;
private readonly string to;
public BaseLogStringReplacement(string from, string to) public BaseLogStringReplacement(string from, string to)
{ {
this.from = from; From = from;
this.to = to; To = to;
if (string.IsNullOrEmpty(from) || string.IsNullOrEmpty(to) || from == to) throw new ArgumentException(); if (string.IsNullOrEmpty(from) || string.IsNullOrEmpty(to) || from == to) throw new ArgumentException();
} }
public string From { get; }
public string To { get; }
public string Apply(string msg) public string Apply(string msg)
{ {
return msg.Replace(from, to); return msg.Replace(From, To);
} }
} }
} }

View File

@ -6,9 +6,11 @@
{ {
} }
public string FullFilename { get; set; } = "NULL";
protected override string GetFullName() protected override string GetFullName()
{ {
return "NULL"; return FullFilename;
} }
public override void Log(string message) public override void Log(string message)

View File

@ -4,6 +4,7 @@ using Utils;
namespace Tests.BasicTests namespace Tests.BasicTests
{ {
[Ignore("Used for debugging continuous tests")]
[TestFixture] [TestFixture]
public class ContinuousSubstitute : AutoBootstrapDistTest public class ContinuousSubstitute : AutoBootstrapDistTest
{ {
@ -63,13 +64,13 @@ namespace Tests.BasicTests
while (DateTime.UtcNow < endTime) while (DateTime.UtcNow < endTime)
{ {
CreatePeerConnectionTestHelpers().AssertFullyConnected(GetAllOnlineCodexNodes()); CreatePeerConnectionTestHelpers().AssertFullyConnected(GetAllOnlineCodexNodes());
CheckRoutingTables(GetAllOnlineCodexNodes());
if (DateTime.UtcNow > checkTime) var node = RandomUtils.PickOneRandom(nodes.ToList());
{ var file = GenerateTestFile(50.MB());
CheckRoutingTables(GetAllOnlineCodexNodes()); node.UploadFile(file);
}
Thread.Sleep(5000); Thread.Sleep(20000);
} }
} }