From d532d9505a318b71a50fb26276fe983ac842db18 Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 1 Apr 2024 20:40:03 +0200 Subject: [PATCH 1/7] Sets up autoclient. --- AutoClient/AutoClient.csproj | 16 ++++ AutoClient/Codex.cs | 112 ++++++++++++++++++++++++ AutoClient/Configuration.cs | 45 ++++++++++ AutoClient/Program.cs | 57 ++++++++++++ AutoClient/Runner.cs | 135 +++++++++++++++++++++++++++++ Framework/FileUtils/FileManager.cs | 21 +++-- cs-codex-dist-testing.sln | 7 ++ 7 files changed, 388 insertions(+), 5 deletions(-) create mode 100644 AutoClient/AutoClient.csproj create mode 100644 AutoClient/Codex.cs create mode 100644 AutoClient/Configuration.cs create mode 100644 AutoClient/Program.cs create mode 100644 AutoClient/Runner.cs diff --git a/AutoClient/AutoClient.csproj b/AutoClient/AutoClient.csproj new file mode 100644 index 00000000..fbbe4500 --- /dev/null +++ b/AutoClient/AutoClient.csproj @@ -0,0 +1,16 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + diff --git a/AutoClient/Codex.cs b/AutoClient/Codex.cs new file mode 100644 index 00000000..f667c276 --- /dev/null +++ b/AutoClient/Codex.cs @@ -0,0 +1,112 @@ +using CodexOpenApi; +using CodexPlugin; +using Core; +using Utils; +using DebugInfo = CodexPlugin.DebugInfo; + +namespace AutoClient +{ + public class Codex + { + private readonly IPluginTools tools; + private readonly Address address; + private readonly Mapper mapper = new Mapper(); + + /// + /// This class was largely copied from CodexAccess in CodexPlugin. + /// Should really be generalized so CodexPlugin supports talking to custom Codex instances. + /// + public Codex(IPluginTools tools, Address address) + { + this.tools = tools; + this.address = address; + } + + public DebugInfo GetDebugInfo() + { + return mapper.Map(OnCodex(api => api.GetDebugInfoAsync())); + } + + public DebugPeer GetDebugPeer(string peerId) + { + // Cannot use openAPI: debug/peer endpoint is not specified there. + var endpoint = GetEndpoint(); + var str = endpoint.HttpGetString($"debug/peer/{peerId}"); + + if (str.ToLowerInvariant() == "unable to find peer!") + { + return new DebugPeer + { + IsPeerFound = false + }; + } + + var result = endpoint.Deserialize(str); + result.IsPeerFound = true; + return result; + } + + public void ConnectToPeer(string peerId, string[] peerMultiAddresses) + { + OnCodex(api => + { + Time.Wait(api.ConnectPeerAsync(peerId, peerMultiAddresses)); + return Task.FromResult(string.Empty); + }); + } + + public string UploadFile(FileStream fileStream) + { + return OnCodex(api => api.UploadAsync(fileStream)); + } + + public Stream DownloadFile(string contentId) + { + var fileResponse = OnCodex(api => api.DownloadNetworkAsync(contentId)); + if (fileResponse.StatusCode != 200) throw new Exception("Download failed with StatusCode: " + fileResponse.StatusCode); + return fileResponse.Stream; + } + + public LocalDatasetList LocalFiles() + { + return mapper.Map(OnCodex(api => api.ListDataAsync())); + } + + public StorageAvailability SalesAvailability(StorageAvailability request) + { + var body = mapper.Map(request); + var read = OnCodex(api => api.OfferStorageAsync(body)); + return mapper.Map(read); + } + + public string RequestStorage(StoragePurchaseRequest request) + { + var body = mapper.Map(request); + return OnCodex(api => api.CreateStorageRequestAsync(request.ContentId.Id, body)); + } + + public StoragePurchase GetPurchaseStatus(string purchaseId) + { + return mapper.Map(OnCodex(api => api.GetPurchaseAsync(purchaseId))); + } + + private T OnCodex(Func> action) + { + var result = tools.CreateHttp() + .OnClient(client => + { + var api = new CodexApi(client); + api.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1"; + return Time.Wait(action(api)); + }); + return result; + } + + private IEndpoint GetEndpoint() + { + return tools + .CreateHttp() + .CreateEndpoint(address, "/api/codex/v1/"); + } + } +} diff --git a/AutoClient/Configuration.cs b/AutoClient/Configuration.cs new file mode 100644 index 00000000..4be2a268 --- /dev/null +++ b/AutoClient/Configuration.cs @@ -0,0 +1,45 @@ +using ArgsUniform; + +namespace AutoClient +{ + public class Configuration + { + [Uniform("codex-host", "ch", "CODEXHOST", false, "Codex Host address. (default localhost)")] + public string CodexHost { get; set; } = "localhost"; + + [Uniform("codex-port", "cp", "CODEXPORT", false, "port number of Codex API. (8080 by default)")] + public int CodexPort { get; set; } = 8080; + + [Uniform("datapath", "dp", "DATAPATH", false, "Root path where all data files will be saved.")] + public string DataPath { get; set; } = "datapath"; + + [Uniform("contract-duration", "cd", "CONTRACTDURATION", false, "contract duration in minutes. (default 30)")] + public int ContractDurationMinutes { get; set; } = 30; + + [Uniform("contract-expiry", "ce", "CONTRACTEXPIRY", false, "contract expiry in minutes. (default 15)")] + public int ContractExpiryMinutes { get; set; } = 15; + + [Uniform("dataset-size", "ds", "DATASETSIZE", false, "Total dataset size in bytes. (default 10MB).")] + public int DatasetSizeBytes { get; set; } = 10 * 1024 * 1024; + + [Uniform("num-hosts", "nh", "NUMHOSTS", false, "Number of hosts for contract. (default 5)")] + public int NumHosts { get; set; } = 5; + + [Uniform("num-hosts-tolerance", "nt", "NUMTOL", false, "Number of host tolerance for contract. (default 2)")] + public int HostTolerance { get; set; } = 2; + + [Uniform("price","p", "PRICE", false, "Price of contract. (default 10)")] + public int Price { get; set; } = 10; + + [Uniform("collateral", "c", "COLLATERAL", false, "Required collateral. (default 1)")] + public int RequiredCollateral { get; set; } = 1; + + public string LogPath + { + get + { + return Path.Combine(DataPath, "logs"); + } + } + } +} diff --git a/AutoClient/Program.cs b/AutoClient/Program.cs new file mode 100644 index 00000000..1358586b --- /dev/null +++ b/AutoClient/Program.cs @@ -0,0 +1,57 @@ +using ArgsUniform; +using AutoClient; +using CodexPlugin; +using Core; +using Logging; +using static Org.BouncyCastle.Math.EC.ECCurve; + +public static class Program +{ + public static void Main(string[] args) + { + var cts = new CancellationTokenSource(); + var cancellationToken = cts.Token; + Console.CancelKeyPress += (sender, args) => cts.Cancel(); + + var uniformArgs = new ArgsUniform(PrintHelp, args); + var config = uniformArgs.Parse(true); + + var log = new LogSplitter( + new FileLog(Path.Combine(config.LogPath, "autoclient")), + new ConsoleLog() + ); + + var address = new Utils.Address( + host: config.CodexHost, + port: config.CodexPort + ); + + log.Log($"Start. Address: {address}"); + + var tools = CreateTools(log, config); + var fileManager = tools.GetFileManager(); + var codex = new Codex(tools, address); + + var runner = new Runner(log, codex, fileManager, cancellationToken, config); + runner.Run(); + + log.Log("Done."); + } + + private static void PrintHelp() + { + Console.WriteLine("Generates fake data and creates Codex storage contracts for it."); + } + + private static IPluginTools CreateTools(ILog log, Configuration config) + { + var configuration = new KubernetesWorkflow.Configuration( + null, + operationTimeout: TimeSpan.FromMinutes(10), + retryDelay: TimeSpan.FromSeconds(10), + kubernetesNamespace: "notUsed!#"); + + var result = new EntryPoint(log, configuration, config.DataPath, new DefaultTimeSet()); + return result.Tools; + } +} \ No newline at end of file diff --git a/AutoClient/Runner.cs b/AutoClient/Runner.cs new file mode 100644 index 00000000..f58c683c --- /dev/null +++ b/AutoClient/Runner.cs @@ -0,0 +1,135 @@ +using CodexContractsPlugin; +using CodexPlugin; +using FileUtils; +using Logging; +using Utils; + +namespace AutoClient +{ + public class Runner + { + private readonly ILog log; + private readonly Codex codex; + private readonly IFileManager fileManager; + private readonly CancellationToken ct; + private readonly Configuration config; + + public Runner(ILog log, Codex codex, IFileManager fileManager, CancellationToken ct, Configuration config) + { + this.log = log; + this.codex = codex; + this.fileManager = fileManager; + this.ct = ct; + this.config = config; + } + + public void Run() + { + while (!ct.IsCancellationRequested) + { + log.Log("New run!"); + + try + { + fileManager.ScopedFiles(() => + { + DoRun(); + }); + + log.Log("Run succcessful."); + } + catch (Exception ex) + { + log.Error("Exception during run: " + ex); + } + + FixedShortDelay(); + } + } + + private void DoRun() + { + var file = CreateFile(); + var cid = UploadFile(file); + var pid = RequestStorage(cid); + WaitUntilStarted(pid); + } + + private TrackedFile CreateFile() + { + return fileManager.GenerateFile(new ByteSize(Convert.ToInt64(config.DatasetSizeBytes))); + } + + private ContentId UploadFile(TrackedFile file) + { + // Copied from CodexNode :/ + using var fileStream = File.OpenRead(file.Filename); + + var logMessage = $"Uploading file {file.Describe()}..."; + log.Log(logMessage); + var response = codex.UploadFile(fileStream); + + if (string.IsNullOrEmpty(response)) FrameworkAssert.Fail("Received empty response."); + if (response.StartsWith("Unable to store block")) FrameworkAssert.Fail("Node failed to store block."); + + log.Log($"Uploaded file. Received contentId: '{response}'."); + return new ContentId(response); + } + + private string RequestStorage(ContentId cid) + { + var request = new StoragePurchaseRequest(cid) + { + PricePerSlotPerSecond = config.Price.TestTokens(), + RequiredCollateral = config.RequiredCollateral.TestTokens(), + MinRequiredNumberOfNodes = Convert.ToUInt32(config.NumHosts), + NodeFailureTolerance = Convert.ToUInt32(config.HostTolerance), + Duration = TimeSpan.FromMinutes(config.ContractDurationMinutes), + Expiry = TimeSpan.FromMinutes(config.ContractExpiryMinutes) + }; + log.Log($"Requesting storage: {request}"); + return codex.RequestStorage(request); + } + + private void WaitUntilStarted(string pid) + { + log.Log("Waiting till contract is started, or expired..."); + try + { + while (true) + { + FixedShortDelay(); + var status = codex.GetPurchaseStatus(pid); + if (status != null) + { + if (!string.IsNullOrEmpty(status.Error)) log.Log("Contract errored: " + status.Error); + var state = status.State.ToLowerInvariant(); + if (state.Contains("pending") || state.Contains("submitted")) + { + FixedShortDelay(); + } + else + { + log.Log("Wait finished with contract status: " + state); + } + } + } + } + catch (Exception ex) + { + log.Log($"Wait failed with exception: {ex}. Assume contract will expire: Wait expiry time."); + ExpiryTimeDelay(); + } + } + + private void ExpiryTimeDelay() + { + Thread.Sleep(config.ContractExpiryMinutes * 60 * 1000); + } + + private void FixedShortDelay() + { + Thread.Sleep(15 * 1000); + } + } +} diff --git a/Framework/FileUtils/FileManager.cs b/Framework/FileUtils/FileManager.cs index 10ecf241..4a283b14 100644 --- a/Framework/FileUtils/FileManager.cs +++ b/Framework/FileUtils/FileManager.cs @@ -57,16 +57,27 @@ namespace FileUtils public void ScopedFiles(Action action) { PushFileSet(); - action(); - PopFileSet(); + try + { + action(); + } + finally + { + PopFileSet(); + } } public T ScopedFiles(Func action) { PushFileSet(); - var result = action(); - PopFileSet(); - return result; + try + { + return action(); + } + finally + { + PopFileSet(); + } } private void PushFileSet() diff --git a/cs-codex-dist-testing.sln b/cs-codex-dist-testing.sln index ada68933..809f9713 100644 --- a/cs-codex-dist-testing.sln +++ b/cs-codex-dist-testing.sln @@ -66,6 +66,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoClient", "AutoClient\AutoClient.csproj", "{8B8BF2B9-5855-4C92-A5DA-D13D778B7934}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -172,6 +174,10 @@ Global {88C212E9-308A-46A4-BAAD-468E8EBD8EDF}.Debug|Any CPU.Build.0 = Debug|Any CPU {88C212E9-308A-46A4-BAAD-468E8EBD8EDF}.Release|Any CPU.ActiveCfg = Release|Any CPU {88C212E9-308A-46A4-BAAD-468E8EBD8EDF}.Release|Any CPU.Build.0 = Release|Any CPU + {8B8BF2B9-5855-4C92-A5DA-D13D778B7934}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B8BF2B9-5855-4C92-A5DA-D13D778B7934}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B8BF2B9-5855-4C92-A5DA-D13D778B7934}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B8BF2B9-5855-4C92-A5DA-D13D778B7934}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -202,6 +208,7 @@ Global {F730DA73-1C92-4107-BCFB-D33759DAB0C3} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7} {B07820C4-309F-4454-BCC1-1D4902C9C67B} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7} {88C212E9-308A-46A4-BAAD-468E8EBD8EDF} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124} + {8B8BF2B9-5855-4C92-A5DA-D13D778B7934} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C} From 7f972bac85e54f71d4eb609843a7f3850d0bc136 Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 1 Apr 2024 20:47:56 +0200 Subject: [PATCH 2/7] Mvoes autoclient --- .../AutoClient}/AutoClient.csproj | 6 +++--- {AutoClient => Tools/AutoClient}/Codex.cs | 0 .../AutoClient}/Configuration.cs | 0 {AutoClient => Tools/AutoClient}/Program.cs | 17 +++++++++++++++++ {AutoClient => Tools/AutoClient}/Runner.cs | 0 cs-codex-dist-testing.sln | 12 ++++++------ 6 files changed, 26 insertions(+), 9 deletions(-) rename {AutoClient => Tools/AutoClient}/AutoClient.csproj (52%) rename {AutoClient => Tools/AutoClient}/Codex.cs (100%) rename {AutoClient => Tools/AutoClient}/Configuration.cs (100%) rename {AutoClient => Tools/AutoClient}/Program.cs (79%) rename {AutoClient => Tools/AutoClient}/Runner.cs (100%) diff --git a/AutoClient/AutoClient.csproj b/Tools/AutoClient/AutoClient.csproj similarity index 52% rename from AutoClient/AutoClient.csproj rename to Tools/AutoClient/AutoClient.csproj index fbbe4500..9340dc97 100644 --- a/AutoClient/AutoClient.csproj +++ b/Tools/AutoClient/AutoClient.csproj @@ -8,9 +8,9 @@ - - - + + + diff --git a/AutoClient/Codex.cs b/Tools/AutoClient/Codex.cs similarity index 100% rename from AutoClient/Codex.cs rename to Tools/AutoClient/Codex.cs diff --git a/AutoClient/Configuration.cs b/Tools/AutoClient/Configuration.cs similarity index 100% rename from AutoClient/Configuration.cs rename to Tools/AutoClient/Configuration.cs diff --git a/AutoClient/Program.cs b/Tools/AutoClient/Program.cs similarity index 79% rename from AutoClient/Program.cs rename to Tools/AutoClient/Program.cs index 1358586b..ebaefb9c 100644 --- a/AutoClient/Program.cs +++ b/Tools/AutoClient/Program.cs @@ -32,12 +32,29 @@ public static class Program var fileManager = tools.GetFileManager(); var codex = new Codex(tools, address); + CheckCodex(codex, log); + var runner = new Runner(log, codex, fileManager, cancellationToken, config); runner.Run(); log.Log("Done."); } + private static void CheckCodex(Codex codex, ILog log) + { + log.Log("Checking Codex..."); + try + { + var info = codex.GetDebugInfo(); + if (string.IsNullOrEmpty(info.Id)) throw new Exception("Failed to fetch Codex node id"); + } + catch (Exception ex) + { + log.Log($"Codex not OK: {ex}"); + throw; + } + } + private static void PrintHelp() { Console.WriteLine("Generates fake data and creates Codex storage contracts for it."); diff --git a/AutoClient/Runner.cs b/Tools/AutoClient/Runner.cs similarity index 100% rename from AutoClient/Runner.cs rename to Tools/AutoClient/Runner.cs diff --git a/cs-codex-dist-testing.sln b/cs-codex-dist-testing.sln index 809f9713..eb7de476 100644 --- a/cs-codex-dist-testing.sln +++ b/cs-codex-dist-testing.sln @@ -66,7 +66,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoClient", "AutoClient\AutoClient.csproj", "{8B8BF2B9-5855-4C92-A5DA-D13D778B7934}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoClient", "Tools\AutoClient\AutoClient.csproj", "{73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -174,10 +174,10 @@ Global {88C212E9-308A-46A4-BAAD-468E8EBD8EDF}.Debug|Any CPU.Build.0 = Debug|Any CPU {88C212E9-308A-46A4-BAAD-468E8EBD8EDF}.Release|Any CPU.ActiveCfg = Release|Any CPU {88C212E9-308A-46A4-BAAD-468E8EBD8EDF}.Release|Any CPU.Build.0 = Release|Any CPU - {8B8BF2B9-5855-4C92-A5DA-D13D778B7934}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8B8BF2B9-5855-4C92-A5DA-D13D778B7934}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B8BF2B9-5855-4C92-A5DA-D13D778B7934}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8B8BF2B9-5855-4C92-A5DA-D13D778B7934}.Release|Any CPU.Build.0 = Release|Any CPU + {73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -208,7 +208,7 @@ Global {F730DA73-1C92-4107-BCFB-D33759DAB0C3} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7} {B07820C4-309F-4454-BCC1-1D4902C9C67B} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7} {88C212E9-308A-46A4-BAAD-468E8EBD8EDF} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124} - {8B8BF2B9-5855-4C92-A5DA-D13D778B7934} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} + {73599F9C-98BB-4C6A-9D7D-7C50FBF2993B} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C} From 40393c3a5b69b6216a21063992084b246762d5f8 Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 1 Apr 2024 21:09:24 +0200 Subject: [PATCH 3/7] Using raw version of purchase-status call. API doesn't line up --- .gitignore | 3 ++- Tools/AutoClient/Codex.cs | 6 ++++++ Tools/AutoClient/Configuration.cs | 4 ++-- Tools/AutoClient/Runner.cs | 26 +++++++++++++++++++------- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index c134feec..488beaf6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vs obj bin -.vscode \ No newline at end of file +.vscode +Tools/AutoClient/datapath diff --git a/Tools/AutoClient/Codex.cs b/Tools/AutoClient/Codex.cs index f667c276..8ecdf366 100644 --- a/Tools/AutoClient/Codex.cs +++ b/Tools/AutoClient/Codex.cs @@ -90,6 +90,12 @@ namespace AutoClient return mapper.Map(OnCodex(api => api.GetPurchaseAsync(purchaseId))); } + public string GetPurchaseStatusRaw(string purchaseId) + { + var endpoint = GetEndpoint(); + return endpoint.HttpGetString($"storage/purchases/{purchaseId}"); + } + private T OnCodex(Func> action) { var result = tools.CreateHttp() diff --git a/Tools/AutoClient/Configuration.cs b/Tools/AutoClient/Configuration.cs index 4be2a268..8f409b48 100644 --- a/Tools/AutoClient/Configuration.cs +++ b/Tools/AutoClient/Configuration.cs @@ -4,8 +4,8 @@ namespace AutoClient { public class Configuration { - [Uniform("codex-host", "ch", "CODEXHOST", false, "Codex Host address. (default localhost)")] - public string CodexHost { get; set; } = "localhost"; + [Uniform("codex-host", "ch", "CODEXHOST", false, "Codex Host address. (default 'http://localhost')")] + public string CodexHost { get; set; } = "http://localhost"; [Uniform("codex-port", "cp", "CODEXPORT", false, "port number of Codex API. (8080 by default)")] public int CodexPort { get; set; } = 8080; diff --git a/Tools/AutoClient/Runner.cs b/Tools/AutoClient/Runner.cs index f58c683c..8b236807 100644 --- a/Tools/AutoClient/Runner.cs +++ b/Tools/AutoClient/Runner.cs @@ -87,7 +87,7 @@ namespace AutoClient Duration = TimeSpan.FromMinutes(config.ContractDurationMinutes), Expiry = TimeSpan.FromMinutes(config.ContractExpiryMinutes) }; - log.Log($"Requesting storage: {request}"); + request.Log(log); return codex.RequestStorage(request); } @@ -96,21 +96,33 @@ namespace AutoClient log.Log("Waiting till contract is started, or expired..."); try { + var emptyResponseTolerance = 10; while (true) { FixedShortDelay(); - var status = codex.GetPurchaseStatus(pid); - if (status != null) + var status = codex.GetPurchaseStatusRaw(pid).ToLowerInvariant(); + log.Log($"Status response: '{status}'"); + if (string.IsNullOrEmpty(status)) { - if (!string.IsNullOrEmpty(status.Error)) log.Log("Contract errored: " + status.Error); - var state = status.State.ToLowerInvariant(); - if (state.Contains("pending") || state.Contains("submitted")) + emptyResponseTolerance--; + if (emptyResponseTolerance == 0) + { + log.Log("Received 10 empty responses to '/storage/purchases/'. Applying expiry delay, then carrying on."); + ExpiryTimeDelay(); + return; + } + FixedShortDelay(); + } + else + { + if (status.Contains("pending") || status.Contains("submitted")) { FixedShortDelay(); } else { - log.Log("Wait finished with contract status: " + state); + log.Log("Wait finished."); + return; } } } From 362040bd3c69f176b5bb17d2af435fef2d8c62ea Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 1 Apr 2024 21:29:02 +0200 Subject: [PATCH 4/7] Sets up docker image and CI --- .github/workflows/docker-autoclient.yml | 26 +++++++++++++++++++++++++ Tools/AutoClient/docker/Dockerfile | 24 +++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .github/workflows/docker-autoclient.yml create mode 100644 Tools/AutoClient/docker/Dockerfile diff --git a/.github/workflows/docker-autoclient.yml b/.github/workflows/docker-autoclient.yml new file mode 100644 index 00000000..41d31018 --- /dev/null +++ b/.github/workflows/docker-autoclient.yml @@ -0,0 +1,26 @@ +name: Docker - AutoClient + +on: + push: + branches: + - master + tags: + - 'v*.*.*' + paths: + - 'Tools/AutoClient/**' + - '!Tools/AutoClient/docker/docker-compose.yaml' + - 'Framework/**' + - 'ProjectPlugins/**' + - .github/workflows/docker-autoclient.yml + - .github/workflows/docker-reusable.yml + workflow_dispatch: + +jobs: + build-and-push: + name: Build and Push + uses: ./.github/workflows/docker-reusable.yml + with: + docker_file: Tools/AutoClient/docker/Dockerfile + docker_repo: codexstorage/codex-autoclient + secrets: inherit + diff --git a/Tools/AutoClient/docker/Dockerfile b/Tools/AutoClient/docker/Dockerfile new file mode 100644 index 00000000..a1436c24 --- /dev/null +++ b/Tools/AutoClient/docker/Dockerfile @@ -0,0 +1,24 @@ +# Variables +ARG BUILDER=mcr.microsoft.com/dotnet/sdk:7.0 +ARG IMAGE=${BUILDER} +ARG APP_HOME=/app + +# Build +FROM ${IMAGE} AS builder +ARG APP_HOME + +WORKDIR ${APP_HOME} +COPY ./Tools/AutoClient ./Tools/AutoClient +COPY ./Framework ./Framework +COPY ./ProjectPlugins ./ProjectPlugins +RUN dotnet restore Tools/AutoClient +RUN dotnet publish Tools/AutoClient -c Release -o out + +# Create +FROM ${IMAGE} +ARG APP_HOME +ENV APP_HOME=${APP_HOME} + +WORKDIR ${APP_HOME} +COPY --from=builder ${APP_HOME}/out . +CMD dotnet ${APP_HOME}/AutoClient.dll From 4c75cebcd6b0f15364ed8082a5362dcdcdbbb1c5 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 27 Jun 2024 15:38:13 +0200 Subject: [PATCH 5/7] Implements autoclient with image generator --- Tools/AutoClient/Codex.cs | 118 ------------------------ Tools/AutoClient/Configuration.cs | 3 - Tools/AutoClient/ImageGenerator.cs | 17 ++++ Tools/AutoClient/Program.cs | 24 ++--- Tools/AutoClient/Runner.cs | 141 +++++++++++++++++++---------- 5 files changed, 121 insertions(+), 182 deletions(-) delete mode 100644 Tools/AutoClient/Codex.cs create mode 100644 Tools/AutoClient/ImageGenerator.cs diff --git a/Tools/AutoClient/Codex.cs b/Tools/AutoClient/Codex.cs deleted file mode 100644 index 8ecdf366..00000000 --- a/Tools/AutoClient/Codex.cs +++ /dev/null @@ -1,118 +0,0 @@ -using CodexOpenApi; -using CodexPlugin; -using Core; -using Utils; -using DebugInfo = CodexPlugin.DebugInfo; - -namespace AutoClient -{ - public class Codex - { - private readonly IPluginTools tools; - private readonly Address address; - private readonly Mapper mapper = new Mapper(); - - /// - /// This class was largely copied from CodexAccess in CodexPlugin. - /// Should really be generalized so CodexPlugin supports talking to custom Codex instances. - /// - public Codex(IPluginTools tools, Address address) - { - this.tools = tools; - this.address = address; - } - - public DebugInfo GetDebugInfo() - { - return mapper.Map(OnCodex(api => api.GetDebugInfoAsync())); - } - - public DebugPeer GetDebugPeer(string peerId) - { - // Cannot use openAPI: debug/peer endpoint is not specified there. - var endpoint = GetEndpoint(); - var str = endpoint.HttpGetString($"debug/peer/{peerId}"); - - if (str.ToLowerInvariant() == "unable to find peer!") - { - return new DebugPeer - { - IsPeerFound = false - }; - } - - var result = endpoint.Deserialize(str); - result.IsPeerFound = true; - return result; - } - - public void ConnectToPeer(string peerId, string[] peerMultiAddresses) - { - OnCodex(api => - { - Time.Wait(api.ConnectPeerAsync(peerId, peerMultiAddresses)); - return Task.FromResult(string.Empty); - }); - } - - public string UploadFile(FileStream fileStream) - { - return OnCodex(api => api.UploadAsync(fileStream)); - } - - public Stream DownloadFile(string contentId) - { - var fileResponse = OnCodex(api => api.DownloadNetworkAsync(contentId)); - if (fileResponse.StatusCode != 200) throw new Exception("Download failed with StatusCode: " + fileResponse.StatusCode); - return fileResponse.Stream; - } - - public LocalDatasetList LocalFiles() - { - return mapper.Map(OnCodex(api => api.ListDataAsync())); - } - - public StorageAvailability SalesAvailability(StorageAvailability request) - { - var body = mapper.Map(request); - var read = OnCodex(api => api.OfferStorageAsync(body)); - return mapper.Map(read); - } - - public string RequestStorage(StoragePurchaseRequest request) - { - var body = mapper.Map(request); - return OnCodex(api => api.CreateStorageRequestAsync(request.ContentId.Id, body)); - } - - public StoragePurchase GetPurchaseStatus(string purchaseId) - { - return mapper.Map(OnCodex(api => api.GetPurchaseAsync(purchaseId))); - } - - public string GetPurchaseStatusRaw(string purchaseId) - { - var endpoint = GetEndpoint(); - return endpoint.HttpGetString($"storage/purchases/{purchaseId}"); - } - - private T OnCodex(Func> action) - { - var result = tools.CreateHttp() - .OnClient(client => - { - var api = new CodexApi(client); - api.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1"; - return Time.Wait(action(api)); - }); - return result; - } - - private IEndpoint GetEndpoint() - { - return tools - .CreateHttp() - .CreateEndpoint(address, "/api/codex/v1/"); - } - } -} diff --git a/Tools/AutoClient/Configuration.cs b/Tools/AutoClient/Configuration.cs index 8f409b48..75aca2e2 100644 --- a/Tools/AutoClient/Configuration.cs +++ b/Tools/AutoClient/Configuration.cs @@ -19,9 +19,6 @@ namespace AutoClient [Uniform("contract-expiry", "ce", "CONTRACTEXPIRY", false, "contract expiry in minutes. (default 15)")] public int ContractExpiryMinutes { get; set; } = 15; - [Uniform("dataset-size", "ds", "DATASETSIZE", false, "Total dataset size in bytes. (default 10MB).")] - public int DatasetSizeBytes { get; set; } = 10 * 1024 * 1024; - [Uniform("num-hosts", "nh", "NUMHOSTS", false, "Number of hosts for contract. (default 5)")] public int NumHosts { get; set; } = 5; diff --git a/Tools/AutoClient/ImageGenerator.cs b/Tools/AutoClient/ImageGenerator.cs new file mode 100644 index 00000000..eeab0d32 --- /dev/null +++ b/Tools/AutoClient/ImageGenerator.cs @@ -0,0 +1,17 @@ +namespace AutoClient +{ + public class ImageGenerator + { + public async Task GenerateImage() + { + var httpClient = new HttpClient(); + var thing = await httpClient.GetStreamAsync("https://picsum.photos/3840/2160"); + + var filename = $"{Guid.NewGuid().ToString().ToLowerInvariant()}.jpg"; + using var file = File.OpenWrite(filename); + await thing.CopyToAsync(file); + + return filename; + } + } +} diff --git a/Tools/AutoClient/Program.cs b/Tools/AutoClient/Program.cs index ebaefb9c..ff2ad394 100644 --- a/Tools/AutoClient/Program.cs +++ b/Tools/AutoClient/Program.cs @@ -1,14 +1,14 @@ using ArgsUniform; using AutoClient; -using CodexPlugin; +using CodexOpenApi; using Core; using Logging; -using static Org.BouncyCastle.Math.EC.ECCurve; public static class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { + var cts = new CancellationTokenSource(); var cancellationToken = cts.Token; Console.CancelKeyPress += (sender, args) => cts.Cancel(); @@ -28,24 +28,26 @@ public static class Program log.Log($"Start. Address: {address}"); - var tools = CreateTools(log, config); - var fileManager = tools.GetFileManager(); - var codex = new Codex(tools, address); + var imgGenerator = new ImageGenerator(); - CheckCodex(codex, log); + var client = new HttpClient(); + var codex = new CodexApi(client); + codex.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1"; - var runner = new Runner(log, codex, fileManager, cancellationToken, config); - runner.Run(); + await CheckCodex(codex, log); + + var runner = new Runner(log, client, address, codex, cancellationToken, config, imgGenerator); + await runner.Run(); log.Log("Done."); } - private static void CheckCodex(Codex codex, ILog log) + private static async Task CheckCodex(CodexApi codex, ILog log) { log.Log("Checking Codex..."); try { - var info = codex.GetDebugInfo(); + var info = await codex.GetDebugInfoAsync(); if (string.IsNullOrEmpty(info.Id)) throw new Exception("Failed to fetch Codex node id"); } catch (Exception ex) diff --git a/Tools/AutoClient/Runner.cs b/Tools/AutoClient/Runner.cs index 8b236807..c57f95f0 100644 --- a/Tools/AutoClient/Runner.cs +++ b/Tools/AutoClient/Runner.cs @@ -1,7 +1,7 @@ -using CodexContractsPlugin; +using CodexOpenApi; using CodexPlugin; -using FileUtils; using Logging; +using Newtonsoft.Json; using Utils; namespace AutoClient @@ -9,21 +9,25 @@ namespace AutoClient public class Runner { private readonly ILog log; - private readonly Codex codex; - private readonly IFileManager fileManager; + private readonly HttpClient client; + private readonly Address address; + private readonly CodexApi codex; private readonly CancellationToken ct; private readonly Configuration config; + private readonly ImageGenerator generator; - public Runner(ILog log, Codex codex, IFileManager fileManager, CancellationToken ct, Configuration config) + public Runner(ILog log, HttpClient client, Address address, CodexApi codex, CancellationToken ct, Configuration config, ImageGenerator generator) { this.log = log; + this.client = client; + this.address = address; this.codex = codex; - this.fileManager = fileManager; this.ct = ct; this.config = config; + this.generator = generator; } - public void Run() + public async Task Run() { while (!ct.IsCancellationRequested) { @@ -31,10 +35,7 @@ namespace AutoClient try { - fileManager.ScopedFiles(() => - { - DoRun(); - }); + await DoRun(); log.Log("Run succcessful."); } @@ -42,32 +43,32 @@ namespace AutoClient { log.Error("Exception during run: " + ex); } - - FixedShortDelay(); + + await FixedShortDelay(); } } - private void DoRun() + private async Task DoRun() { - var file = CreateFile(); - var cid = UploadFile(file); - var pid = RequestStorage(cid); - WaitUntilStarted(pid); + var file = await CreateFile(); + var cid = await UploadFile(file); + var pid = await RequestStorage(cid); + await WaitUntilStarted(pid); } - private TrackedFile CreateFile() + private async Task CreateFile() { - return fileManager.GenerateFile(new ByteSize(Convert.ToInt64(config.DatasetSizeBytes))); + return await generator.GenerateImage(); } - private ContentId UploadFile(TrackedFile file) + private async Task UploadFile(string filename) { // Copied from CodexNode :/ - using var fileStream = File.OpenRead(file.Filename); + using var fileStream = File.OpenRead(filename); - var logMessage = $"Uploading file {file.Describe()}..."; + var logMessage = $"Uploading file {filename}..."; log.Log(logMessage); - var response = codex.UploadFile(fileStream); + var response = await codex.UploadAsync(fileStream, ct); if (string.IsNullOrEmpty(response)) FrameworkAssert.Fail("Received empty response."); if (response.StartsWith("Unable to store block")) FrameworkAssert.Fail("Node failed to store block."); @@ -76,22 +77,44 @@ namespace AutoClient return new ContentId(response); } - private string RequestStorage(ContentId cid) + private async Task RequestStorage(ContentId cid) { - var request = new StoragePurchaseRequest(cid) + log.Log("Requesting storage for " + cid.Id); + var result = await codex.CreateStorageRequestAsync(cid.Id, new StorageRequestCreation() { - PricePerSlotPerSecond = config.Price.TestTokens(), - RequiredCollateral = config.RequiredCollateral.TestTokens(), - MinRequiredNumberOfNodes = Convert.ToUInt32(config.NumHosts), - NodeFailureTolerance = Convert.ToUInt32(config.HostTolerance), - Duration = TimeSpan.FromMinutes(config.ContractDurationMinutes), - Expiry = TimeSpan.FromMinutes(config.ContractExpiryMinutes) - }; - request.Log(log); - return codex.RequestStorage(request); + Collateral = config.RequiredCollateral.ToString(), + Duration = (config.ContractDurationMinutes * 60).ToString(), + Expiry = (config.ContractExpiryMinutes * 60).ToString(), + Nodes = config.NumHosts, + Reward = config.Price.ToString(), + ProofProbability = "15", + Tolerance = config.HostTolerance + }, ct); + + log.Log("Response: " + result); + + return result; } - private void WaitUntilStarted(string pid) + private async Task GetPurchaseState(string pid) + { + try + { + // openapi still don't match code. + var str = await client.GetStringAsync($"{address.Host}:{address.Port}/api/codex/v1/storage/purchases/{pid}"); + if (string.IsNullOrEmpty(str)) return null; + var sp = JsonConvert.DeserializeObject(str)!; + log.Log($"Purchase {pid} is {sp.State}"); + if (!string.IsNullOrEmpty(sp.Error)) log.Log($"Purchase {pid} error is {sp.Error}"); + return sp.State; + } + catch (Exception ex) + { + return null; + } + } + + private async Task WaitUntilStarted(string pid) { log.Log("Waiting till contract is started, or expired..."); try @@ -99,30 +122,43 @@ namespace AutoClient var emptyResponseTolerance = 10; while (true) { - FixedShortDelay(); - var status = codex.GetPurchaseStatusRaw(pid).ToLowerInvariant(); - log.Log($"Status response: '{status}'"); + await FixedShortDelay(); + var status = await GetPurchaseState(pid); if (string.IsNullOrEmpty(status)) { emptyResponseTolerance--; if (emptyResponseTolerance == 0) { - log.Log("Received 10 empty responses to '/storage/purchases/'. Applying expiry delay, then carrying on."); - ExpiryTimeDelay(); + log.Log("Received 10 empty responses. Applying expiry delay, then carrying on."); + await ExpiryTimeDelay(); return; } - FixedShortDelay(); + await FixedShortDelay(); } else { if (status.Contains("pending") || status.Contains("submitted")) { - FixedShortDelay(); + await FixedShortDelay(); + } + else if (status.Contains("started")) + { + log.Log("Started."); + await FixedDurationDelay(); + } + else if (status.Contains("finished")) + { + log.Log("Purchase finished."); + return; + } + else if (status.Contains("error")) + { + await FixedShortDelay(); + return; } else { - log.Log("Wait finished."); - return; + await FixedShortDelay(); } } } @@ -130,18 +166,23 @@ namespace AutoClient catch (Exception ex) { log.Log($"Wait failed with exception: {ex}. Assume contract will expire: Wait expiry time."); - ExpiryTimeDelay(); + await ExpiryTimeDelay(); } } - private void ExpiryTimeDelay() + private async Task FixedDurationDelay() { - Thread.Sleep(config.ContractExpiryMinutes * 60 * 1000); + await Task.Delay(config.ContractDurationMinutes * 60 * 1000); } - private void FixedShortDelay() + private async Task ExpiryTimeDelay() { - Thread.Sleep(15 * 1000); + await Task.Delay(config.ContractExpiryMinutes * 60 * 1000); + } + + private async Task FixedShortDelay() + { + await Task.Delay(15 * 1000); } } } From c57dc4daa1cdd0462cd115d716231c12230e5dd3 Mon Sep 17 00:00:00 2001 From: benbierens Date: Fri, 28 Jun 2024 08:47:09 +0200 Subject: [PATCH 6/7] concurrent purchases --- Tools/AutoClient/Configuration.cs | 3 + Tools/AutoClient/Program.cs | 34 +++++---- Tools/AutoClient/{Runner.cs => Purchaser.cs} | 80 +++++++------------- 3 files changed, 51 insertions(+), 66 deletions(-) rename Tools/AutoClient/{Runner.cs => Purchaser.cs} (69%) diff --git a/Tools/AutoClient/Configuration.cs b/Tools/AutoClient/Configuration.cs index 75aca2e2..c5e04e7f 100644 --- a/Tools/AutoClient/Configuration.cs +++ b/Tools/AutoClient/Configuration.cs @@ -13,6 +13,9 @@ namespace AutoClient [Uniform("datapath", "dp", "DATAPATH", false, "Root path where all data files will be saved.")] public string DataPath { get; set; } = "datapath"; + [Uniform("purchases", "np", "PURCHASES", false, "Number of concurrent purchases.")] + public int NumConcurrentPurchases { get; set; } = 1; + [Uniform("contract-duration", "cd", "CONTRACTDURATION", false, "contract duration in minutes. (default 30)")] public int ContractDurationMinutes { get; set; } = 30; diff --git a/Tools/AutoClient/Program.cs b/Tools/AutoClient/Program.cs index ff2ad394..5eed206c 100644 --- a/Tools/AutoClient/Program.cs +++ b/Tools/AutoClient/Program.cs @@ -8,7 +8,6 @@ public static class Program { public static async Task Main(string[] args) { - var cts = new CancellationTokenSource(); var cancellationToken = cts.Token; Console.CancelKeyPress += (sender, args) => cts.Cancel(); @@ -16,6 +15,11 @@ public static class Program var uniformArgs = new ArgsUniform(PrintHelp, args); var config = uniformArgs.Parse(true); + if (config.NumConcurrentPurchases < 1) + { + throw new Exception("Number of concurrent purchases must be > 0"); + } + var log = new LogSplitter( new FileLog(Path.Combine(config.LogPath, "autoclient")), new ConsoleLog() @@ -36,8 +40,20 @@ public static class Program await CheckCodex(codex, log); - var runner = new Runner(log, client, address, codex, cancellationToken, config, imgGenerator); - await runner.Run(); + var purchasers = new List(); + for (var i = 0; i < config.NumConcurrentPurchases; i++) + { + purchasers.Add( + new Purchaser(new LogPrefixer(log, $"({i}) "), client, address, codex, cancellationToken, config, imgGenerator) + ); + } + + var delayPerPurchaser = TimeSpan.FromMinutes(config.ContractDurationMinutes) / config.NumConcurrentPurchases; + foreach (var purchaser in purchasers) + { + purchaser.Start(); + await Task.Delay(delayPerPurchaser); + } log.Log("Done."); } @@ -61,16 +77,4 @@ public static class Program { Console.WriteLine("Generates fake data and creates Codex storage contracts for it."); } - - private static IPluginTools CreateTools(ILog log, Configuration config) - { - var configuration = new KubernetesWorkflow.Configuration( - null, - operationTimeout: TimeSpan.FromMinutes(10), - retryDelay: TimeSpan.FromSeconds(10), - kubernetesNamespace: "notUsed!#"); - - var result = new EntryPoint(log, configuration, config.DataPath, new DefaultTimeSet()); - return result.Tools; - } } \ No newline at end of file diff --git a/Tools/AutoClient/Runner.cs b/Tools/AutoClient/Purchaser.cs similarity index 69% rename from Tools/AutoClient/Runner.cs rename to Tools/AutoClient/Purchaser.cs index c57f95f0..3d03258d 100644 --- a/Tools/AutoClient/Runner.cs +++ b/Tools/AutoClient/Purchaser.cs @@ -6,7 +6,7 @@ using Utils; namespace AutoClient { - public class Runner + public class Purchaser { private readonly ILog log; private readonly HttpClient client; @@ -16,7 +16,7 @@ namespace AutoClient private readonly Configuration config; private readonly ImageGenerator generator; - public Runner(ILog log, HttpClient client, Address address, CodexApi codex, CancellationToken ct, Configuration config, ImageGenerator generator) + public Purchaser(ILog log, HttpClient client, Address address, CodexApi codex, CancellationToken ct, Configuration config, ImageGenerator generator) { this.log = log; this.client = client; @@ -27,33 +27,25 @@ namespace AutoClient this.generator = generator; } - public async Task Run() + public void Start() + { + Task.Run(Worker); + } + + private async Task Worker() { while (!ct.IsCancellationRequested) { - log.Log("New run!"); - - try - { - await DoRun(); - - log.Log("Run succcessful."); - } - catch (Exception ex) - { - log.Error("Exception during run: " + ex); - } - - await FixedShortDelay(); + var pid = await StartNewPurchase(); + await WaitTillFinished(pid); } } - private async Task DoRun() + private async Task StartNewPurchase() { var file = await CreateFile(); var cid = await UploadFile(file); - var pid = await RequestStorage(cid); - await WaitUntilStarted(pid); + return await RequestStorage(cid); } private async Task CreateFile() @@ -66,8 +58,7 @@ namespace AutoClient // Copied from CodexNode :/ using var fileStream = File.OpenRead(filename); - var logMessage = $"Uploading file {filename}..."; - log.Log(logMessage); + log.Log($"Uploading file {filename}..."); var response = await codex.UploadAsync(fileStream, ct); if (string.IsNullOrEmpty(response)) FrameworkAssert.Fail("Received empty response."); @@ -91,7 +82,7 @@ namespace AutoClient Tolerance = config.HostTolerance }, ct); - log.Log("Response: " + result); + log.Log("Purchase ID: " + result); return result; } @@ -108,59 +99,46 @@ namespace AutoClient if (!string.IsNullOrEmpty(sp.Error)) log.Log($"Purchase {pid} error is {sp.Error}"); return sp.State; } - catch (Exception ex) + catch { return null; } } - private async Task WaitUntilStarted(string pid) + private async Task WaitTillFinished(string pid) { - log.Log("Waiting till contract is started, or expired..."); + log.Log("Waiting..."); try { var emptyResponseTolerance = 10; while (true) { - await FixedShortDelay(); - var status = await GetPurchaseState(pid); + var status = (await GetPurchaseState(pid))?.ToLowerInvariant(); if (string.IsNullOrEmpty(status)) { emptyResponseTolerance--; if (emptyResponseTolerance == 0) { - log.Log("Received 10 empty responses. Applying expiry delay, then carrying on."); + log.Log("Received 10 empty responses. Stop tracking this purchase."); await ExpiryTimeDelay(); return; } - await FixedShortDelay(); } else { - if (status.Contains("pending") || status.Contains("submitted")) + if (status.Contains("cancel") || + status.Contains("error") || + status.Contains("finished")) { - await FixedShortDelay(); + return; } - else if (status.Contains("started")) + if (status.Contains("started")) { - log.Log("Started."); await FixedDurationDelay(); } - else if (status.Contains("finished")) - { - log.Log("Purchase finished."); - return; - } - else if (status.Contains("error")) - { - await FixedShortDelay(); - return; - } - else - { - await FixedShortDelay(); - } } + + await FixedShortDelay(); } } catch (Exception ex) @@ -172,17 +150,17 @@ namespace AutoClient private async Task FixedDurationDelay() { - await Task.Delay(config.ContractDurationMinutes * 60 * 1000); + await Task.Delay(config.ContractDurationMinutes * 60 * 1000, ct); } private async Task ExpiryTimeDelay() { - await Task.Delay(config.ContractExpiryMinutes * 60 * 1000); + await Task.Delay(config.ContractExpiryMinutes * 60 * 1000, ct); } private async Task FixedShortDelay() { - await Task.Delay(15 * 1000); + await Task.Delay(15 * 1000, ct); } } } From 7eed6160eb992cc1b149746ee9d7ce5f5efdc553 Mon Sep 17 00:00:00 2001 From: benbierens Date: Fri, 28 Jun 2024 09:16:12 +0200 Subject: [PATCH 7/7] working as intended --- Tools/AutoClient/Configuration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/AutoClient/Configuration.cs b/Tools/AutoClient/Configuration.cs index c5e04e7f..a3808829 100644 --- a/Tools/AutoClient/Configuration.cs +++ b/Tools/AutoClient/Configuration.cs @@ -14,7 +14,7 @@ namespace AutoClient public string DataPath { get; set; } = "datapath"; [Uniform("purchases", "np", "PURCHASES", false, "Number of concurrent purchases.")] - public int NumConcurrentPurchases { get; set; } = 1; + public int NumConcurrentPurchases { get; set; } = 10; [Uniform("contract-duration", "cd", "CONTRACTDURATION", false, "contract duration in minutes. (default 30)")] public int ContractDurationMinutes { get; set; } = 30;