Compare commits
270
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ed3caafe | ||
|
|
daad6468c6 | ||
|
|
3d347a936d | ||
|
|
87bda475df | ||
|
|
44dfa39737 | ||
|
|
d38f6da26f | ||
|
|
5667cfc054 | ||
|
|
ecd0e70261 | ||
|
|
5bbb95f1ff | ||
|
|
5aa3edbda2 | ||
|
|
9bdebb963b | ||
|
|
c690598868 | ||
|
|
ba59ac91d7 | ||
|
|
5170699ae4 | ||
|
|
09d2f418eb | ||
|
|
cc9d04acd7 | ||
|
|
b5c8c55c72 | ||
|
|
38b987144e | ||
|
|
8136bf1c92 | ||
|
|
5ddec99114 | ||
|
|
58cebd43ce | ||
|
|
11a015e0b6 | ||
|
|
745b7e43b4 | ||
|
|
a72866c725 | ||
|
|
2ec7f26387 | ||
|
|
72865e7f96 | ||
|
|
6a0d2830d6 | ||
|
|
b3710f26ae | ||
|
|
f6cd9db408 | ||
|
|
7a3a2b558b | ||
|
|
53aad5cb37 | ||
|
|
fd65e1f022 | ||
|
|
d16b8cb011 | ||
|
|
37611bdc66 | ||
|
|
7352cdf3fe | ||
|
|
8e084d6ca0 | ||
|
|
017c71a6e8 | ||
|
|
488fbb383b | ||
|
|
cfd635146b | ||
|
|
7d737a5534 | ||
|
|
0d21246b06 | ||
|
|
ec99f5f8aa | ||
|
|
9d9f65c5a3 | ||
|
|
87271f4f37 | ||
|
|
9bebc23f6a | ||
|
|
682c267b10 | ||
|
|
4de35b579f | ||
|
|
fb4d520ba4 | ||
|
|
1f72d4f37d | ||
|
|
ecada92dc5 | ||
|
|
410d62849a | ||
|
|
927ccaa119 | ||
|
|
d58d65b751 | ||
|
|
802b18e990 | ||
|
|
8064051f2f | ||
|
|
485b2387e4 | ||
|
|
ae4d782566 | ||
|
|
8f16699bab | ||
|
|
5ba919a638 | ||
|
|
af7aeb2bba | ||
|
|
1f11c5d6b5 | ||
|
|
8694bfe9be | ||
|
|
637b5a4736 | ||
|
|
e0cc46260b | ||
|
|
c1ad97d26f | ||
|
|
1e8f8106b0 | ||
|
|
926e323569 | ||
|
|
8d32758918 | ||
|
|
7eed6160eb | ||
|
|
c57dc4daa1 | ||
|
|
4c75cebcd6 | ||
|
|
a820788c7d | ||
|
|
52a02abd3f | ||
|
|
a92455b2a5 | ||
|
|
933d5e7d4d | ||
|
|
ba43fd90c6 | ||
|
|
01ee514c73 | ||
|
|
1eb30329c6 | ||
|
|
0ef55abdf4 | ||
|
|
8341807d92 | ||
|
|
c2712daccb | ||
|
|
8033da1176 | ||
|
|
5afab577a7 | ||
|
|
4be9b9df9a | ||
|
|
a905f0ce53 | ||
|
|
0c4d3be912 | ||
|
|
2b7ba61543 | ||
|
|
f9c7e18985 | ||
|
|
32e7028029 | ||
|
|
cc3eddf02d | ||
|
|
d73ad6db15 | ||
|
|
5280f0732c | ||
|
|
5ca135646a | ||
|
|
e1da38b6e8 | ||
|
|
f7c45d17d7 | ||
|
|
cb4cdfe69a | ||
|
|
bed57dd35b | ||
|
|
c38a2242ba | ||
|
|
d3488dc907 | ||
|
|
1fa7787d3b | ||
|
|
54f053cfcc | ||
|
|
7b5e802efb | ||
|
|
390b9de54a | ||
|
|
4fc9835f43 | ||
|
|
9a46e20b4a | ||
|
|
74ca512f92 | ||
|
|
1c856f7615 | ||
|
|
cc2513bd2f | ||
|
|
22527a5d93 | ||
|
|
25663b59a0 | ||
|
|
aa416d50b3 | ||
|
|
16bf5ce5ab | ||
|
|
52ae671234 | ||
|
|
5ffff1ed07 | ||
|
|
18a02b1717 | ||
|
|
3a61fc89c6 | ||
|
|
38c2d1749a | ||
|
|
9900db8d25 | ||
|
|
e53a644718 | ||
|
|
62b56e198b | ||
|
|
f4d1dae478 | ||
|
|
bf75c25152 | ||
|
|
b888999b61 | ||
|
|
e9555cf99e | ||
|
|
11b866986d | ||
|
|
67fc2183b7 | ||
|
|
02baa72c1e | ||
|
|
383102b988 | ||
|
|
3da36725e3 | ||
|
|
ac7d323201 | ||
|
|
a846d51c0c | ||
|
|
f5da80dc9c | ||
|
|
f94a67adb4 | ||
|
|
f1d453251c | ||
|
|
12dc7efd5b | ||
|
|
117a30bb82 | ||
|
|
ccc6c815e4 | ||
|
|
ba9e4b098f | ||
|
|
69aa3a998f | ||
|
|
00fd2cebf9 | ||
|
|
684a99027b | ||
|
|
5143361dcd | ||
|
|
6c956c1a64 | ||
|
|
a749f82ca5 | ||
|
|
6e9ea47b7d | ||
|
|
b1818400ca | ||
|
|
f7bdafbdc5 | ||
|
|
a236544ee9 | ||
|
|
e082f8a31c | ||
|
|
fa1b560a91 | ||
|
|
d6f7e225be | ||
|
|
22cf82b99b | ||
|
|
ad2181db0b | ||
|
|
3525d13e69 | ||
|
|
8847de116d | ||
|
|
7d9dcb263d | ||
|
|
b3771cce32 | ||
|
|
9717591224 | ||
|
|
64ae7c8efe | ||
|
|
d8d7605ce5 | ||
|
|
6995cbfb23 | ||
|
|
c6a757d6fb | ||
|
|
c01f9dbb21 | ||
|
|
0ec43a9325 | ||
|
|
266c661958 | ||
|
|
8ea4c4ee37 | ||
|
|
725dfc23a6 | ||
|
|
39ff757b39 | ||
|
|
e187bfc941 | ||
|
|
dd36929a81 | ||
|
|
2902f6baab | ||
|
|
f38f7861a0 | ||
|
|
84a68521b0 | ||
|
|
ce7ba47a32 | ||
|
|
a20e9a30cf | ||
|
|
52cec0e9f3 | ||
|
|
9a6866cf8e | ||
|
|
15d7e6483e | ||
|
|
e1c710a093 | ||
|
|
0c700ded9d | ||
|
|
50b7e2300d | ||
|
|
a6379d02f1 | ||
|
|
6545f3469f | ||
|
|
58f7f9384a | ||
|
|
7ec9934751 | ||
|
|
c4c3f61a23 | ||
|
|
c856f404e3 | ||
|
|
eed989cbf5 | ||
|
|
630dc2814a | ||
|
|
570b174a00 | ||
|
|
700fc0ea40 | ||
|
|
23ebd4166b | ||
|
|
3683044bf7 | ||
|
|
fb10906816 | ||
|
|
86074dab6a | ||
|
|
015d8da21d | ||
|
|
d847c4f3ec | ||
|
|
a2e4869403 | ||
|
|
5ffe34bb83 | ||
|
|
e3b16fd742 | ||
|
|
80261959e7 | ||
|
|
7db9360ba4 | ||
|
|
899d775873 | ||
|
|
581cc80d5d | ||
|
|
24cf6c70b8 | ||
|
|
ae25b58610 | ||
|
|
2cf5a26934 | ||
|
|
5ed78da30b | ||
|
|
3fb1b212b6 | ||
|
|
ca7258ef28 | ||
|
|
1dbb749732 | ||
|
|
525fdcf4a7 | ||
|
|
40208373e4 | ||
|
|
4a0885cf2d | ||
|
|
0224f17733 | ||
|
|
b7ab2f994e | ||
|
|
0182ce134f | ||
|
|
69666d3fee | ||
|
|
605004286a | ||
|
|
bbc975141f | ||
|
|
0b2dcef57e | ||
|
|
9f7e95c515 | ||
|
|
8db84bfe7d | ||
|
|
9c8151bdb7 | ||
|
|
43aaed225d | ||
|
|
b805c5f004 | ||
|
|
29344451d6 | ||
|
|
5511f8ed32 | ||
|
|
362040bd3c | ||
|
|
40393c3a5b | ||
|
|
7f972bac85 | ||
|
|
d532d9505a | ||
|
|
4cd22f3719 | ||
|
|
f5291517c1 | ||
|
|
ceed913143 | ||
|
|
3086aa78d5 | ||
|
|
6c4b9345cb | ||
|
|
2ab84e2a61 | ||
|
|
48e7f98956 | ||
|
|
d7c7d47a61 | ||
|
|
be268ace42 | ||
|
|
a6b0b16909 | ||
|
|
8c64352acf | ||
|
|
fd38cc336b | ||
|
|
f5429b9c14 | ||
|
|
1344a6e3d2 | ||
|
|
9a2e889ef9 | ||
|
|
a58c9aba59 | ||
|
|
0263cc4eff | ||
|
|
b40215dd36 | ||
|
|
4e978bd5b5 | ||
|
|
0a8083d4d5 | ||
|
|
376fd60974 | ||
|
|
8bc63c1fdb | ||
|
|
4f461e4cb3 | ||
|
|
b25c747522 | ||
|
|
01d6b8f227 | ||
|
|
330552563c | ||
|
|
581cf2a813 | ||
|
|
6597728e5c | ||
|
|
0a4c4c60c5 | ||
|
|
8c8b1748ee | ||
|
|
a6fce1084d | ||
|
|
23b7bbd548 | ||
|
|
42d3c5cd2c | ||
|
|
1bf693938c | ||
|
|
da4101a042 | ||
|
|
3c210f96fc | ||
|
|
dd5baeda46 | ||
|
|
7d33c1c113 |
@@ -0,0 +1,30 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
!**/.gitignore
|
||||
!.git/HEAD
|
||||
!.git/config
|
||||
!.git/packed-refs
|
||||
!.git/refs/heads/**
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Docker - KeyMaker
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths:
|
||||
- 'Tools/KeyMaker/**'
|
||||
- 'Framework/**'
|
||||
- 'ProjectPlugins/**'
|
||||
- .github/workflows/docker-KeyMaker.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/KeyMaker/docker/Dockerfile
|
||||
docker_repo: codexstorage/codex-keymaker
|
||||
secrets: inherit
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Docker - MarketInsights API
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths:
|
||||
- 'Tools/MarketInsights/**'
|
||||
- 'Framework/**'
|
||||
- 'ProjectPlugins/**'
|
||||
- .github/workflows/docker-marketinsights.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/MarketInsights/Dockerfile
|
||||
docker_repo: codexstorage/codex-marketinsights
|
||||
secrets: inherit
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
.vs
|
||||
obj
|
||||
bin
|
||||
.vscode
|
||||
.vscode
|
||||
Tools/AutoClient/datapath
|
||||
|
||||
@@ -4,9 +4,8 @@ namespace ArgsUniform
|
||||
{
|
||||
public class ArgsUniform<T>
|
||||
{
|
||||
private readonly Assigner<T> assigner;
|
||||
private readonly Action printAppInfo;
|
||||
private readonly object? defaultsProvider;
|
||||
private readonly IEnv.IEnv env;
|
||||
private readonly string[] args;
|
||||
private const int cliStart = 8;
|
||||
private const int shortStart = 38;
|
||||
@@ -31,9 +30,9 @@ namespace ArgsUniform
|
||||
public ArgsUniform(Action printAppInfo, object defaultsProvider, IEnv.IEnv env, params string[] args)
|
||||
{
|
||||
this.printAppInfo = printAppInfo;
|
||||
this.defaultsProvider = defaultsProvider;
|
||||
this.env = env;
|
||||
this.args = args;
|
||||
|
||||
assigner = new Assigner<T>(env, args, defaultsProvider);
|
||||
}
|
||||
|
||||
public T Parse(bool printResult = false)
|
||||
@@ -42,7 +41,7 @@ namespace ArgsUniform
|
||||
{
|
||||
printAppInfo();
|
||||
PrintHelp();
|
||||
throw new Exception();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
var result = Activator.CreateInstance<T>();
|
||||
@@ -53,18 +52,16 @@ namespace ArgsUniform
|
||||
var attr = uniformProperty.GetCustomAttribute<UniformAttribute>();
|
||||
if (attr != null)
|
||||
{
|
||||
if (!UniformAssign(result, attr, uniformProperty) && attr.Required)
|
||||
if (!assigner.UniformAssign(result, attr, uniformProperty) && attr.Required)
|
||||
{
|
||||
{
|
||||
missingRequired.Add(uniformProperty);
|
||||
}
|
||||
missingRequired.Add(uniformProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingRequired.Any())
|
||||
{
|
||||
PrintResults(result, uniformProperties);
|
||||
PrintResults(printResult,result, uniformProperties);
|
||||
Print("");
|
||||
foreach (var missing in missingRequired)
|
||||
{
|
||||
@@ -75,37 +72,39 @@ namespace ArgsUniform
|
||||
}
|
||||
|
||||
PrintHelp();
|
||||
throw new ArgumentException("Unable to assemble all required arguments");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
if (printResult)
|
||||
{
|
||||
PrintResults(result, uniformProperties);
|
||||
}
|
||||
PrintResults(printResult, result, uniformProperties);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void PrintResults(T result, PropertyInfo[] uniformProperties)
|
||||
{
|
||||
Print("");
|
||||
foreach (var p in uniformProperties)
|
||||
{
|
||||
Print($"\t{p.Name} = {p.GetValue(result)}");
|
||||
}
|
||||
Print("");
|
||||
}
|
||||
|
||||
public void PrintHelp()
|
||||
{
|
||||
Print("");
|
||||
PrintAligned("CLI option:", "(short)", "Environment variable:", "Description");
|
||||
var attrs = typeof(T).GetProperties().Where(m => m.GetCustomAttributes(typeof(UniformAttribute), false).Length == 1).Select(p => p.GetCustomAttribute<UniformAttribute>()).Where(a => a != null).ToArray();
|
||||
foreach (var attr in attrs)
|
||||
PrintAligned("CLI option:", "(short)", "Environment variable:", "Description", "(default)");
|
||||
var props = typeof(T).GetProperties().Where(m => m.GetCustomAttributes(typeof(UniformAttribute), false).Length == 1).ToArray();
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var a = attr!;
|
||||
var optional = !a.Required ? " *" : "";
|
||||
PrintAligned($"--{a.Arg}=...", $"({a.ArgShort})", a.EnvVar, a.Description + optional);
|
||||
var a = prop.GetCustomAttribute<UniformAttribute>();
|
||||
if (a != null)
|
||||
{
|
||||
var optional = !a.Required ? " (optional)" : "";
|
||||
var def = assigner.DescribeDefaultFor(prop);
|
||||
PrintAligned($"--{a.Arg}=...", $"({a.ArgShort})", a.EnvVar, a.Description + optional, $"({def})");
|
||||
}
|
||||
}
|
||||
Print("");
|
||||
}
|
||||
|
||||
private void PrintResults(bool printResult, T result, PropertyInfo[] uniformProperties)
|
||||
{
|
||||
if (!printResult) return;
|
||||
Print("");
|
||||
foreach (var p in uniformProperties)
|
||||
{
|
||||
Print($"\t{p.Name} = {p.GetValue(result)}");
|
||||
}
|
||||
Print("");
|
||||
}
|
||||
@@ -115,7 +114,7 @@ namespace ArgsUniform
|
||||
Console.WriteLine(msg);
|
||||
}
|
||||
|
||||
private void PrintAligned(string cli, string s, string env, string desc)
|
||||
private void PrintAligned(string cli, string s, string env, string desc, string def)
|
||||
{
|
||||
Console.CursorLeft = cliStart;
|
||||
Console.Write(cli);
|
||||
@@ -124,132 +123,8 @@ namespace ArgsUniform
|
||||
Console.CursorLeft = envStart;
|
||||
Console.Write(env);
|
||||
Console.CursorLeft = descStart;
|
||||
Console.Write(desc + Environment.NewLine);
|
||||
}
|
||||
|
||||
private object GetDefaultValue(Type t)
|
||||
{
|
||||
if (t.IsValueType) return Activator.CreateInstance(t)!;
|
||||
return null!;
|
||||
}
|
||||
|
||||
private bool UniformAssign(T result, UniformAttribute attr, PropertyInfo uniformProperty)
|
||||
{
|
||||
if (AssignFromArgsIfAble(result, attr, uniformProperty)) return true;
|
||||
if (AssignFromEnvVarIfAble(result, attr, uniformProperty)) return true;
|
||||
if (AssignFromDefaultsIfAble(result, uniformProperty)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool AssignFromDefaultsIfAble(T result, PropertyInfo uniformProperty)
|
||||
{
|
||||
var currentValue = uniformProperty.GetValue(result);
|
||||
var isEmptryString = (currentValue as string) == string.Empty;
|
||||
if (currentValue != GetDefaultValue(uniformProperty.PropertyType) && !isEmptryString) return true;
|
||||
|
||||
if (defaultsProvider == null) return false;
|
||||
|
||||
var defaultProperty = defaultsProvider.GetType().GetProperties().SingleOrDefault(p => p.Name == uniformProperty.Name);
|
||||
if (defaultProperty == null) return false;
|
||||
|
||||
var value = defaultProperty.GetValue(defaultsProvider);
|
||||
if (value != null)
|
||||
{
|
||||
return Assign(result, uniformProperty, value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool AssignFromEnvVarIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
|
||||
{
|
||||
var e = env.GetEnvVarOrDefault(attr.EnvVar, string.Empty);
|
||||
if (!string.IsNullOrEmpty(e))
|
||||
{
|
||||
return Assign(result, uniformProperty, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool AssignFromArgsIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
|
||||
{
|
||||
var fromArg = GetFromArgs(attr.Arg);
|
||||
if (fromArg != null)
|
||||
{
|
||||
return Assign(result, uniformProperty, fromArg);
|
||||
}
|
||||
var fromShort = GetFromArgs(attr.ArgShort);
|
||||
if (fromShort != null)
|
||||
{
|
||||
return Assign(result, uniformProperty, fromShort);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool Assign(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
if (uniformProperty.PropertyType == value.GetType())
|
||||
{
|
||||
uniformProperty.SetValue(result, value);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uniformProperty.PropertyType == typeof(string) || uniformProperty.PropertyType == typeof(int))
|
||||
{
|
||||
uniformProperty.SetValue(result, Convert.ChangeType(value, uniformProperty.PropertyType));
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uniformProperty.PropertyType == typeof(int?)) return AssignOptionalInt(result, uniformProperty, value);
|
||||
if (uniformProperty.PropertyType.IsEnum) return AssignEnum(result, uniformProperty, value);
|
||||
if (uniformProperty.PropertyType == typeof(bool)) return AssignBool(result, uniformProperty, value);
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AssignEnum(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
var s = value.ToString();
|
||||
if (Enum.TryParse(uniformProperty.PropertyType, s, out var e))
|
||||
{
|
||||
uniformProperty.SetValue(result, e);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AssignOptionalInt(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
if (int.TryParse(value.ToString(), out int i))
|
||||
{
|
||||
uniformProperty.SetValue(result, i);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AssignBool(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
var s = value.ToString();
|
||||
if (s == "1" || (s != null && s.ToLowerInvariant() == "true"))
|
||||
{
|
||||
uniformProperty.SetValue(result, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private string? GetFromArgs(string key)
|
||||
{
|
||||
var argKey = $"--{key}=";
|
||||
var arg = args.FirstOrDefault(a => a.StartsWith(argKey));
|
||||
if (arg != null)
|
||||
{
|
||||
return arg.Substring(argKey.Length);
|
||||
}
|
||||
return null;
|
||||
Console.Write(desc + " ");
|
||||
Console.Write(def + Environment.NewLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ArgsUniform
|
||||
{
|
||||
public class Assigner<T>
|
||||
{
|
||||
private readonly IEnv.IEnv env;
|
||||
private readonly string[] args;
|
||||
private readonly object? defaultsProvider;
|
||||
|
||||
public Assigner(IEnv.IEnv env, string[] args, object? defaultsProvider)
|
||||
{
|
||||
this.env = env;
|
||||
this.args = args;
|
||||
this.defaultsProvider = defaultsProvider;
|
||||
}
|
||||
|
||||
public bool UniformAssign(T result, UniformAttribute attr, PropertyInfo uniformProperty)
|
||||
{
|
||||
if (AssignFromArgsIfAble(result, attr, uniformProperty)) return true;
|
||||
if (AssignFromEnvVarIfAble(result, attr, uniformProperty)) return true;
|
||||
if (AssignFromDefaultsIfAble(result, uniformProperty)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public string DescribeDefaultFor(PropertyInfo property)
|
||||
{
|
||||
var obj = Activator.CreateInstance<T>();
|
||||
var defaultValue = GetDefaultValue(obj, property);
|
||||
if (defaultValue == null) return "";
|
||||
if (defaultValue is string str)
|
||||
{
|
||||
return "\"" + str + "\"";
|
||||
}
|
||||
return defaultValue.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private object? GetDefaultValue(T result, PropertyInfo uniformProperty)
|
||||
{
|
||||
// Get value from object's static initializer if it's there.
|
||||
var currentValue = uniformProperty.GetValue(result);
|
||||
if (currentValue != null) return currentValue;
|
||||
|
||||
// Get value from defaults-provider object if it's there.
|
||||
if (defaultsProvider == null) return null;
|
||||
var defaultProperty = defaultsProvider.GetType().GetProperties().SingleOrDefault(p => p.Name == uniformProperty.Name);
|
||||
if (defaultProperty == null) return null;
|
||||
return defaultProperty.GetValue(defaultsProvider);
|
||||
}
|
||||
|
||||
private bool AssignFromDefaultsIfAble(T result, PropertyInfo uniformProperty)
|
||||
{
|
||||
var defaultValue = GetDefaultValue(result, uniformProperty);
|
||||
var isEmptryString = (defaultValue as string) == string.Empty;
|
||||
if (defaultValue != null && defaultValue != GetDefaultValueForType(uniformProperty.PropertyType) && !isEmptryString)
|
||||
{
|
||||
return Assign(result, uniformProperty, defaultValue);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool AssignFromEnvVarIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
|
||||
{
|
||||
var e = env.GetEnvVarOrDefault(attr.EnvVar, string.Empty);
|
||||
if (!string.IsNullOrEmpty(e))
|
||||
{
|
||||
return Assign(result, uniformProperty, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool AssignFromArgsIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
|
||||
{
|
||||
var fromArg = GetFromArgs(attr.Arg);
|
||||
if (fromArg != null)
|
||||
{
|
||||
return Assign(result, uniformProperty, fromArg);
|
||||
}
|
||||
var fromShort = GetFromArgs(attr.ArgShort);
|
||||
if (fromShort != null)
|
||||
{
|
||||
return Assign(result, uniformProperty, fromShort);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool Assign(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
if (uniformProperty.PropertyType == value.GetType())
|
||||
{
|
||||
uniformProperty.SetValue(result, value);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uniformProperty.PropertyType == typeof(string) || uniformProperty.PropertyType == typeof(int))
|
||||
{
|
||||
uniformProperty.SetValue(result, Convert.ChangeType(value, uniformProperty.PropertyType));
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uniformProperty.PropertyType == typeof(int?)) return AssignOptionalInt(result, uniformProperty, value);
|
||||
if (uniformProperty.PropertyType.IsEnum) return AssignEnum(result, uniformProperty, value);
|
||||
if (uniformProperty.PropertyType == typeof(bool)) return AssignBool(result, uniformProperty, value);
|
||||
if (uniformProperty.PropertyType == typeof(ulong)) return AssignUlong(result, uniformProperty, value);
|
||||
if (uniformProperty.PropertyType == typeof(BigInteger)) return AssignBigInt(result, uniformProperty, value);
|
||||
|
||||
throw new NotSupportedException(
|
||||
$"Unsupported property type '${uniformProperty.PropertyType}' " +
|
||||
$"for property '${uniformProperty.Name}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AssignEnum(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
var s = value.ToString();
|
||||
if (Enum.TryParse(uniformProperty.PropertyType, s, out var e))
|
||||
{
|
||||
uniformProperty.SetValue(result, e);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AssignOptionalInt(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
if (int.TryParse(value.ToString(), CultureInfo.InvariantCulture, out int i))
|
||||
{
|
||||
uniformProperty.SetValue(result, i);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool AssignUlong(T? result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
if (ulong.TryParse(value.ToString(), CultureInfo.InvariantCulture, out ulong i))
|
||||
{
|
||||
uniformProperty.SetValue(result, i);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool AssignBigInt(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
if (BigInteger.TryParse(value.ToString(), CultureInfo.InvariantCulture, out BigInteger i))
|
||||
{
|
||||
uniformProperty.SetValue(result, i);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AssignBool(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
var s = value.ToString();
|
||||
if (s == "1" || (s != null && s.ToLowerInvariant() == "true"))
|
||||
{
|
||||
uniformProperty.SetValue(result, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private string? GetFromArgs(string key)
|
||||
{
|
||||
var argKey = $"--{key}=";
|
||||
var arg = args.FirstOrDefault(a => a.StartsWith(argKey));
|
||||
if (arg != null)
|
||||
{
|
||||
return arg.Substring(argKey.Length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object GetDefaultValueForType(Type t)
|
||||
{
|
||||
if (t.IsValueType) return Activator.CreateInstance(t)!;
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,11 +30,7 @@ namespace Core
|
||||
public IDownloadedLog DownloadLog(RunningContainer container, int? tailLines = null)
|
||||
{
|
||||
var workflow = entryPoint.Tools.CreateWorkflow();
|
||||
var file = entryPoint.Tools.GetLog().CreateSubfile();
|
||||
entryPoint.Tools.GetLog().Log($"Downloading container log for '{container.Name}' to file '{file.FullFilename}'...");
|
||||
var logHandler = new LogDownloadHandler(container.Name, file);
|
||||
workflow.DownloadContainerLog(container, logHandler, tailLines);
|
||||
return logHandler.DownloadLog();
|
||||
return workflow.DownloadContainerLog(container, tailLines);
|
||||
}
|
||||
|
||||
public string ExecuteContainerCommand(IHasContainer containerSource, string command, params string[] args)
|
||||
|
||||
@@ -38,10 +38,14 @@ namespace Core
|
||||
return new CoreInterface(this);
|
||||
}
|
||||
|
||||
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles)
|
||||
/// <summary>
|
||||
/// Deletes kubernetes and tracked file resources.
|
||||
/// when `waitTillDone` is true, this function will block until resources are deleted.
|
||||
/// </summary>
|
||||
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone)
|
||||
{
|
||||
manager.DecommissionPlugins(deleteKubernetesResources, deleteTrackedFiles);
|
||||
Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles);
|
||||
manager.DecommissionPlugins(deleteKubernetesResources, deleteTrackedFiles, waitTillDone);
|
||||
Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles, waitTillDone);
|
||||
}
|
||||
|
||||
internal T GetPlugin<T>() where T : IProjectPlugin
|
||||
|
||||
+27
-8
@@ -7,23 +7,27 @@ namespace Core
|
||||
{
|
||||
T OnClient<T>(Func<HttpClient, T> action);
|
||||
T OnClient<T>(Func<HttpClient, T> action, string description);
|
||||
T OnClient<T>(Func<HttpClient, T> action, Retry retry);
|
||||
IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null);
|
||||
}
|
||||
|
||||
internal class Http : IHttp
|
||||
{
|
||||
private static readonly object httpLock = new object();
|
||||
private static object lockLock = new object();
|
||||
private static readonly Dictionary<string, object> httpLocks = new Dictionary<string, object>();
|
||||
private readonly ILog log;
|
||||
private readonly ITimeSet timeSet;
|
||||
private readonly Action<HttpClient> onClientCreated;
|
||||
private readonly string id;
|
||||
|
||||
internal Http(ILog log, ITimeSet timeSet)
|
||||
: this(log, timeSet, DoNothing)
|
||||
internal Http(string id, ILog log, ITimeSet timeSet)
|
||||
: this(id, log, timeSet, DoNothing)
|
||||
{
|
||||
}
|
||||
|
||||
internal Http(ILog log, ITimeSet timeSet, Action<HttpClient> onClientCreated)
|
||||
internal Http(string id, ILog log, ITimeSet timeSet, Action<HttpClient> onClientCreated)
|
||||
{
|
||||
this.id = id;
|
||||
this.log = log;
|
||||
this.timeSet = timeSet;
|
||||
this.onClientCreated = onClientCreated;
|
||||
@@ -35,13 +39,19 @@ namespace Core
|
||||
}
|
||||
|
||||
public T OnClient<T>(Func<HttpClient, T> action, string description)
|
||||
{
|
||||
var retry = new Retry(description, timeSet.HttpRetryTimeout(), timeSet.HttpCallRetryDelay(), f => { });
|
||||
return OnClient(action, retry);
|
||||
}
|
||||
|
||||
public T OnClient<T>(Func<HttpClient, T> action, Retry retry)
|
||||
{
|
||||
var client = GetClient();
|
||||
|
||||
return LockRetry(() =>
|
||||
{
|
||||
return action(client);
|
||||
}, description);
|
||||
}, retry);
|
||||
}
|
||||
|
||||
public IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null)
|
||||
@@ -51,15 +61,24 @@ namespace Core
|
||||
|
||||
private string GetDescription()
|
||||
{
|
||||
// todo: check this:
|
||||
return DebugStack.GetCallerName(skipFrames: 2);
|
||||
}
|
||||
|
||||
private T LockRetry<T>(Func<T> operation, string description)
|
||||
private T LockRetry<T>(Func<T> operation, Retry retry)
|
||||
{
|
||||
var httpLock = GetLock();
|
||||
lock (httpLock)
|
||||
{
|
||||
return Time.Retry(operation, timeSet.HttpMaxNumberOfRetries(), timeSet.HttpCallRetryDelay(), description);
|
||||
return retry.Run(operation);
|
||||
}
|
||||
}
|
||||
|
||||
private object GetLock()
|
||||
{
|
||||
lock (lockLock) // I had to.
|
||||
{
|
||||
if (!httpLocks.ContainsKey(id)) httpLocks.Add(id, new object());
|
||||
return httpLocks[id];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
internal class LogDownloadHandler : LogHandler, ILogHandler
|
||||
{
|
||||
private readonly LogFile log;
|
||||
|
||||
internal LogDownloadHandler(string description, LogFile log)
|
||||
{
|
||||
this.log = log;
|
||||
|
||||
log.Write($"{description} -->> {log.FullFilename}");
|
||||
log.WriteRaw(description);
|
||||
}
|
||||
|
||||
internal IDownloadedLog DownloadLog()
|
||||
{
|
||||
return new DownloadedLog(log);
|
||||
}
|
||||
|
||||
protected override void ProcessLine(string line)
|
||||
{
|
||||
log.WriteRaw(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,12 +34,12 @@
|
||||
return metadata;
|
||||
}
|
||||
|
||||
internal void DecommissionPlugins(bool deleteKubernetesResources, bool deleteTrackedFiles)
|
||||
internal void DecommissionPlugins(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone)
|
||||
{
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
pair.Plugin.Decommission();
|
||||
pair.Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles);
|
||||
pair.Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles, waitTillDone);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,13 @@ namespace Core
|
||||
{
|
||||
public interface IPluginTools : IWorkflowTool, ILogTool, IHttpFactoryTool, IFileTool
|
||||
{
|
||||
void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles);
|
||||
ITimeSet TimeSet { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Deletes kubernetes and tracked file resources.
|
||||
/// when `waitTillDone` is true, this function will block until resources are deleted.
|
||||
/// </summary>
|
||||
void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone);
|
||||
}
|
||||
|
||||
public interface IWorkflowTool
|
||||
@@ -21,9 +27,9 @@ namespace Core
|
||||
|
||||
public interface IHttpFactoryTool
|
||||
{
|
||||
IHttp CreateHttp(Action<HttpClient> onClientCreated);
|
||||
IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet timeSet);
|
||||
IHttp CreateHttp();
|
||||
IHttp CreateHttp(string id, Action<HttpClient> onClientCreated);
|
||||
IHttp CreateHttp(string id, Action<HttpClient> onClientCreated, ITimeSet timeSet);
|
||||
IHttp CreateHttp(string id);
|
||||
}
|
||||
|
||||
public interface IFileTool
|
||||
@@ -33,7 +39,6 @@ namespace Core
|
||||
|
||||
internal class PluginTools : IPluginTools
|
||||
{
|
||||
private readonly ITimeSet timeSet;
|
||||
private readonly WorkflowCreator workflowCreator;
|
||||
private readonly IFileManager fileManager;
|
||||
private readonly LogPrefixer log;
|
||||
@@ -42,28 +47,30 @@ namespace Core
|
||||
{
|
||||
this.log = new LogPrefixer(log);
|
||||
this.workflowCreator = workflowCreator;
|
||||
this.timeSet = timeSet;
|
||||
TimeSet = timeSet;
|
||||
fileManager = new FileManager(log, fileManagerRootFolder);
|
||||
}
|
||||
|
||||
public ITimeSet TimeSet { get; }
|
||||
|
||||
public void ApplyLogPrefix(string prefix)
|
||||
{
|
||||
log.Prefix = prefix;
|
||||
}
|
||||
|
||||
public IHttp CreateHttp(Action<HttpClient> onClientCreated)
|
||||
public IHttp CreateHttp(string id, Action<HttpClient> onClientCreated)
|
||||
{
|
||||
return CreateHttp(onClientCreated, timeSet);
|
||||
return CreateHttp(id, onClientCreated, TimeSet);
|
||||
}
|
||||
|
||||
public IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet ts)
|
||||
public IHttp CreateHttp(string id, Action<HttpClient> onClientCreated, ITimeSet ts)
|
||||
{
|
||||
return new Http(log, ts, onClientCreated);
|
||||
return new Http(id, log, ts, onClientCreated);
|
||||
}
|
||||
|
||||
public IHttp CreateHttp()
|
||||
public IHttp CreateHttp(string id)
|
||||
{
|
||||
return new Http(log, timeSet);
|
||||
return new Http(id, log, TimeSet);
|
||||
}
|
||||
|
||||
public IStartupWorkflow CreateWorkflow(string? namespaceOverride = null)
|
||||
@@ -71,9 +78,9 @@ namespace Core
|
||||
return workflowCreator.CreateWorkflow(namespaceOverride);
|
||||
}
|
||||
|
||||
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles)
|
||||
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone)
|
||||
{
|
||||
if (deleteKubernetesResources) CreateWorkflow().DeleteNamespace();
|
||||
if (deleteKubernetesResources) CreateWorkflow().DeleteNamespace(waitTillDone);
|
||||
if (deleteTrackedFiles) fileManager.DeleteAllFiles();
|
||||
}
|
||||
|
||||
|
||||
+34
-13
@@ -2,10 +2,31 @@
|
||||
{
|
||||
public interface ITimeSet
|
||||
{
|
||||
/// <summary>
|
||||
/// Timeout for a single HTTP call.
|
||||
/// </summary>
|
||||
TimeSpan HttpCallTimeout();
|
||||
int HttpMaxNumberOfRetries();
|
||||
|
||||
/// <summary>
|
||||
/// Maximum total time to attempt to make a successful HTTP call to a service.
|
||||
/// When HTTP calls time out during this timespan, retries will be made.
|
||||
/// </summary>
|
||||
TimeSpan HttpRetryTimeout();
|
||||
|
||||
/// <summary>
|
||||
/// After a failed HTTP call, wait this long before trying again.
|
||||
/// </summary>
|
||||
TimeSpan HttpCallRetryDelay();
|
||||
TimeSpan WaitForK8sServiceDelay();
|
||||
|
||||
/// <summary>
|
||||
/// After a failed K8s operation, wait this long before trying again.
|
||||
/// </summary>
|
||||
TimeSpan K8sOperationRetryDelay();
|
||||
|
||||
/// <summary>
|
||||
/// Maximum total time to attempt to perform a successful k8s operation.
|
||||
/// If k8s operations fail during this timespan, retries will be made.
|
||||
/// </summary>
|
||||
TimeSpan K8sOperationTimeout();
|
||||
}
|
||||
|
||||
@@ -13,12 +34,12 @@
|
||||
{
|
||||
public TimeSpan HttpCallTimeout()
|
||||
{
|
||||
return TimeSpan.FromMinutes(3);
|
||||
return TimeSpan.FromMinutes(2);
|
||||
}
|
||||
|
||||
public int HttpMaxNumberOfRetries()
|
||||
public TimeSpan HttpRetryTimeout()
|
||||
{
|
||||
return 3;
|
||||
return TimeSpan.FromMinutes(5);
|
||||
}
|
||||
|
||||
public TimeSpan HttpCallRetryDelay()
|
||||
@@ -26,7 +47,7 @@
|
||||
return TimeSpan.FromSeconds(1);
|
||||
}
|
||||
|
||||
public TimeSpan WaitForK8sServiceDelay()
|
||||
public TimeSpan K8sOperationRetryDelay()
|
||||
{
|
||||
return TimeSpan.FromSeconds(10);
|
||||
}
|
||||
@@ -41,27 +62,27 @@
|
||||
{
|
||||
public TimeSpan HttpCallTimeout()
|
||||
{
|
||||
return TimeSpan.FromHours(2);
|
||||
return TimeSpan.FromMinutes(30);
|
||||
}
|
||||
|
||||
public int HttpMaxNumberOfRetries()
|
||||
public TimeSpan HttpRetryTimeout()
|
||||
{
|
||||
return 1;
|
||||
return TimeSpan.FromHours(2.2);
|
||||
}
|
||||
|
||||
public TimeSpan HttpCallRetryDelay()
|
||||
{
|
||||
return TimeSpan.FromSeconds(2);
|
||||
return TimeSpan.FromSeconds(20);
|
||||
}
|
||||
|
||||
public TimeSpan WaitForK8sServiceDelay()
|
||||
public TimeSpan K8sOperationRetryDelay()
|
||||
{
|
||||
return TimeSpan.FromSeconds(10);
|
||||
return TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
public TimeSpan K8sOperationTimeout()
|
||||
{
|
||||
return TimeSpan.FromMinutes(15);
|
||||
return TimeSpan.FromHours(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ namespace DiscordRewards
|
||||
public enum CheckType
|
||||
{
|
||||
Uninitialized,
|
||||
FilledSlot,
|
||||
FinishedSlot,
|
||||
PostedContract,
|
||||
StartedContract,
|
||||
HostFilledSlot,
|
||||
HostFinishedSlot,
|
||||
ClientPostedContract,
|
||||
ClientStartedContract,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
public class GiveRewardsCommand
|
||||
{
|
||||
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
|
||||
public string[] EventsOverview { get; set; } = Array.Empty<string>();
|
||||
|
||||
public bool HasAny()
|
||||
{
|
||||
return Rewards.Any() || EventsOverview.Any();
|
||||
}
|
||||
}
|
||||
|
||||
public class RewardUsersCommand
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
using Utils;
|
||||
|
||||
namespace DiscordRewards
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class RewardRepo
|
||||
{
|
||||
private static string Tag => RewardConfig.UsernameTag;
|
||||
|
||||
public RewardConfig[] Rewards { get; } = new RewardConfig[]
|
||||
{
|
||||
// Filled any slot
|
||||
new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FilledSlot
|
||||
}),
|
||||
public RewardConfig[] Rewards { get; } = new RewardConfig[0];
|
||||
|
||||
// Finished any slot
|
||||
new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FinishedSlot
|
||||
}),
|
||||
// Example configuration, from test server:
|
||||
//{
|
||||
// // Filled any slot
|
||||
// new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig
|
||||
// {
|
||||
// Type = CheckType.HostFilledSlot
|
||||
// }),
|
||||
|
||||
// Finished a sizable slot
|
||||
new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FinishedSlot,
|
||||
MinSlotSize = 1.GB(),
|
||||
MinDuration = TimeSpan.FromHours(24.0),
|
||||
}),
|
||||
// // Finished any slot
|
||||
// new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
|
||||
// {
|
||||
// Type = CheckType.HostFinishedSlot
|
||||
// }),
|
||||
|
||||
// Posted any contract
|
||||
new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.PostedContract
|
||||
}),
|
||||
// // Finished a sizable slot
|
||||
// new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig
|
||||
// {
|
||||
// Type = CheckType.HostFinishedSlot,
|
||||
// MinSlotSize = 10.MB(),
|
||||
// MinDuration = TimeSpan.FromMinutes(5.0),
|
||||
// }),
|
||||
|
||||
// Started any contract
|
||||
new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.StartedContract
|
||||
}),
|
||||
// // Posted any contract
|
||||
// new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
|
||||
// {
|
||||
// Type = CheckType.ClientPostedContract
|
||||
// }),
|
||||
|
||||
// Started a sizable contract
|
||||
new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FinishedSlot,
|
||||
MinNumberOfHosts = 4,
|
||||
MinSlotSize = 1.GB(),
|
||||
MinDuration = TimeSpan.FromHours(24.0),
|
||||
})
|
||||
};
|
||||
// // Started any contract
|
||||
// new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig
|
||||
// {
|
||||
// Type = CheckType.ClientStartedContract
|
||||
// }),
|
||||
|
||||
// // Started a sizable contract
|
||||
// new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig
|
||||
// {
|
||||
// Type = CheckType.ClientStartedContract,
|
||||
// MinNumberOfHosts = 4,
|
||||
// MinSlotSize = 10.MB(),
|
||||
// MinDuration = TimeSpan.FromMinutes(5.0),
|
||||
// })
|
||||
//};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,27 +7,35 @@ namespace FileUtils
|
||||
{
|
||||
TrackedFile CreateEmptyFile(string label = "");
|
||||
TrackedFile GenerateFile(ByteSize size, string label = "");
|
||||
TrackedFile GenerateFile(Action<IGenerateOption> options, string label = "");
|
||||
void DeleteAllFiles();
|
||||
void ScopedFiles(Action action);
|
||||
T ScopedFiles<T>(Func<T> action);
|
||||
}
|
||||
|
||||
public interface IGenerateOption
|
||||
{
|
||||
IGenerateOption Random(ByteSize size);
|
||||
IGenerateOption StringRepeat(string str, ByteSize size);
|
||||
IGenerateOption StringRepeat(string str, int times);
|
||||
IGenerateOption ByteRepeat(byte[] bytes, ByteSize size);
|
||||
IGenerateOption ByteRepeat(byte[] bytes, int times);
|
||||
}
|
||||
|
||||
public class FileManager : IFileManager
|
||||
{
|
||||
public const int ChunkSize = 1024 * 1024 * 100;
|
||||
private static NumberSource folderNumberSource = new NumberSource(0);
|
||||
private readonly Random random = new Random();
|
||||
private static readonly NumberSource folderNumberSource = new NumberSource(0);
|
||||
private readonly ILog log;
|
||||
private readonly string rootFolder;
|
||||
private readonly string folder;
|
||||
private readonly List<List<TrackedFile>> fileSetStack = new List<List<TrackedFile>>();
|
||||
|
||||
public const int ChunkSize = 1024 * 1024 * 100;
|
||||
|
||||
public FileManager(ILog log, string rootFolder)
|
||||
{
|
||||
folder = Path.Combine(rootFolder, folderNumberSource.GetNextNumber().ToString("D5"));
|
||||
|
||||
this.log = log;
|
||||
this.rootFolder = rootFolder;
|
||||
}
|
||||
|
||||
public TrackedFile CreateEmptyFile(string label = "")
|
||||
@@ -41,10 +49,15 @@ namespace FileUtils
|
||||
return result;
|
||||
}
|
||||
|
||||
public TrackedFile GenerateFile(ByteSize size, string label)
|
||||
public TrackedFile GenerateFile(ByteSize size, string label = "")
|
||||
{
|
||||
return GenerateFile(o => o.Random(size), label);
|
||||
}
|
||||
|
||||
public TrackedFile GenerateFile(Action<IGenerateOption> options, string label = "")
|
||||
{
|
||||
var sw = Stopwatch.Begin(log);
|
||||
var result = GenerateRandomFile(size, label);
|
||||
var result = RunGenerators(options, label);
|
||||
sw.End($"Generated file {result.Describe()}.");
|
||||
return result;
|
||||
}
|
||||
@@ -57,16 +70,27 @@ namespace FileUtils
|
||||
public void ScopedFiles(Action action)
|
||||
{
|
||||
PushFileSet();
|
||||
action();
|
||||
PopFileSet();
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
PopFileSet();
|
||||
}
|
||||
}
|
||||
|
||||
public T ScopedFiles<T>(Func<T> action)
|
||||
{
|
||||
PushFileSet();
|
||||
var result = action();
|
||||
PopFileSet();
|
||||
return result;
|
||||
try
|
||||
{
|
||||
return action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
PopFileSet();
|
||||
}
|
||||
}
|
||||
|
||||
private void PushFileSet()
|
||||
@@ -89,26 +113,35 @@ namespace FileUtils
|
||||
if (!Directory.GetFiles(folder).Any()) DeleteDirectory();
|
||||
}
|
||||
|
||||
private TrackedFile GenerateRandomFile(ByteSize size, string label)
|
||||
private TrackedFile RunGenerators(Action<IGenerateOption> options, string label)
|
||||
{
|
||||
var result = CreateEmptyFile(label);
|
||||
CheckSpaceAvailable(result, size);
|
||||
var generators = GetGenerators(options);
|
||||
CheckSpaceAvailable(result, generators.GetRequiredSpace());
|
||||
|
||||
GenerateFileBytes(result, size);
|
||||
using var stream = new FileStream(result.Filename, FileMode.Append);
|
||||
generators.Run(stream);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CheckSpaceAvailable(TrackedFile testFile, ByteSize size)
|
||||
private GeneratorCollection GetGenerators(Action<IGenerateOption> options)
|
||||
{
|
||||
var result = new GeneratorCollection();
|
||||
options(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CheckSpaceAvailable(TrackedFile testFile, long requiredSize)
|
||||
{
|
||||
var file = new FileInfo(testFile.Filename);
|
||||
var drive = new DriveInfo(file.Directory!.Root.FullName);
|
||||
|
||||
var spaceAvailable = drive.TotalFreeSpace;
|
||||
|
||||
if (spaceAvailable < size.SizeInBytes)
|
||||
if (spaceAvailable < requiredSize)
|
||||
{
|
||||
var msg = $"Not enough disk space. " +
|
||||
$"{Formatter.FormatByteSize(size.SizeInBytes)} required. " +
|
||||
$"{Formatter.FormatByteSize(requiredSize)} required. " +
|
||||
$"{Formatter.FormatByteSize(spaceAvailable)} available.";
|
||||
|
||||
log.Log(msg);
|
||||
@@ -116,34 +149,6 @@ namespace FileUtils
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateFileBytes(TrackedFile result, ByteSize size)
|
||||
{
|
||||
long bytesLeft = size.SizeInBytes;
|
||||
int chunkSize = ChunkSize;
|
||||
while (bytesLeft > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var length = Math.Min(bytesLeft, chunkSize);
|
||||
AppendRandomBytesToFile(result, length);
|
||||
bytesLeft -= length;
|
||||
}
|
||||
catch
|
||||
{
|
||||
chunkSize = chunkSize / 2;
|
||||
if (chunkSize < 1024) throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendRandomBytesToFile(TrackedFile result, long length)
|
||||
{
|
||||
var bytes = new byte[length];
|
||||
random.NextBytes(bytes);
|
||||
using var stream = new FileStream(result.Filename, FileMode.Append);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
private void EnsureDirectory()
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
using System.Text;
|
||||
using Utils;
|
||||
|
||||
namespace FileUtils
|
||||
{
|
||||
public class GeneratorCollection : IGenerateOption
|
||||
{
|
||||
private readonly List<IGenerator> generators = new List<IGenerator>();
|
||||
|
||||
public IGenerateOption ByteRepeat(byte[] bytes, ByteSize size)
|
||||
{
|
||||
var times = size.SizeInBytes / bytes.Length;
|
||||
generators.Add(new ByteRepeater(bytes, times));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IGenerateOption ByteRepeat(byte[] bytes, int times)
|
||||
{
|
||||
generators.Add(new ByteRepeater(bytes, times));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IGenerateOption Random(ByteSize size)
|
||||
{
|
||||
generators.Add(new RandomGenerator(size));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IGenerateOption StringRepeat(string str, ByteSize size)
|
||||
{
|
||||
var times = size.SizeInBytes / str.Length;
|
||||
generators.Add(new StringRepeater(str, times));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IGenerateOption StringRepeat(string str, int times)
|
||||
{
|
||||
generators.Add(new StringRepeater(str, times));
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Run(FileStream file)
|
||||
{
|
||||
foreach (var generator in generators)
|
||||
{
|
||||
generator.Generate(file);
|
||||
}
|
||||
}
|
||||
|
||||
public long GetRequiredSpace()
|
||||
{
|
||||
return generators.Sum(g => g.GetRequiredSpace());
|
||||
}
|
||||
}
|
||||
|
||||
public interface IGenerator
|
||||
{
|
||||
void Generate(FileStream file);
|
||||
long GetRequiredSpace();
|
||||
}
|
||||
|
||||
public class ByteRepeater : IGenerator
|
||||
{
|
||||
private readonly byte[] bytes;
|
||||
private readonly long times;
|
||||
|
||||
public ByteRepeater(byte[] bytes, long times)
|
||||
{
|
||||
this.bytes = bytes;
|
||||
this.times = times;
|
||||
}
|
||||
|
||||
public void Generate(FileStream file)
|
||||
{
|
||||
for (var i = 0; i < times; i++)
|
||||
{
|
||||
file.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
}
|
||||
|
||||
public long GetRequiredSpace()
|
||||
{
|
||||
return bytes.Length * times;
|
||||
}
|
||||
}
|
||||
|
||||
public class StringRepeater : IGenerator
|
||||
{
|
||||
private readonly string str;
|
||||
private readonly long times;
|
||||
|
||||
public StringRepeater(string str, long times)
|
||||
{
|
||||
this.str = str;
|
||||
this.times = times;
|
||||
}
|
||||
|
||||
public void Generate(FileStream file)
|
||||
{
|
||||
using var writer = new StreamWriter(file);
|
||||
for (var i = 0; i < times; i++)
|
||||
{
|
||||
writer.Write(str);
|
||||
}
|
||||
}
|
||||
|
||||
public long GetRequiredSpace()
|
||||
{
|
||||
return Encoding.ASCII.GetBytes(str).Length * times;
|
||||
}
|
||||
}
|
||||
|
||||
public class RandomGenerator : IGenerator
|
||||
{
|
||||
private readonly Random random = new Random();
|
||||
private readonly ByteSize size;
|
||||
|
||||
public RandomGenerator(ByteSize size)
|
||||
{
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public void Generate(FileStream file)
|
||||
{
|
||||
var bytesLeft = size.SizeInBytes;
|
||||
while (bytesLeft > 0)
|
||||
{
|
||||
var size = Math.Min(bytesLeft, FileManager.ChunkSize);
|
||||
var bytes = new byte[size];
|
||||
random.NextBytes(bytes);
|
||||
file.Write(bytes, 0, bytes.Length);
|
||||
bytesLeft -= size;
|
||||
}
|
||||
}
|
||||
|
||||
public long GetRequiredSpace()
|
||||
{
|
||||
return size.SizeInBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ namespace FileUtils
|
||||
|
||||
if (readExpected == 0 && readActual == 0)
|
||||
{
|
||||
log.Log($"OK: '{Describe()}' is equal to '{actual.Describe()}'.");
|
||||
log.Log($"OK: {Describe()} is equal to {actual.Describe()}.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
using Utils;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public static class ByteSizeExtensions
|
||||
{
|
||||
public static string ToSuffixNotation(this ByteSize b)
|
||||
{
|
||||
long x = 1024;
|
||||
var map = new Dictionary<long, string>
|
||||
{
|
||||
{ Pow(x, 4), "Ti" },
|
||||
{ Pow(x, 3), "Gi" },
|
||||
{ Pow(x, 2), "Mi" },
|
||||
{ (x), "Ki" },
|
||||
};
|
||||
|
||||
var bytes = b.SizeInBytes;
|
||||
foreach (var pair in map)
|
||||
{
|
||||
if (bytes > pair.Key)
|
||||
{
|
||||
double bytesD = bytes;
|
||||
double divD = pair.Key;
|
||||
double numD = Math.Ceiling(bytesD / divD);
|
||||
var v = Convert.ToInt64(numD);
|
||||
return $"{v}{pair.Value}";
|
||||
}
|
||||
}
|
||||
|
||||
return $"{bytes}";
|
||||
}
|
||||
|
||||
private static long Pow(long x, int v)
|
||||
{
|
||||
long result = 1;
|
||||
for (var i = 0; i < v; i++) result *= x;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ namespace KubernetesWorkflow
|
||||
private readonly string podName;
|
||||
private readonly string recipeName;
|
||||
private readonly string k8sNamespace;
|
||||
private ILogHandler? logHandler;
|
||||
private CancellationTokenSource cts;
|
||||
private Task? worker;
|
||||
private Exception? workerException;
|
||||
@@ -27,11 +26,10 @@ namespace KubernetesWorkflow
|
||||
cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
public void Start(ILogHandler logHandler)
|
||||
public void Start()
|
||||
{
|
||||
if (worker != null) throw new InvalidOperationException();
|
||||
|
||||
this.logHandler = logHandler;
|
||||
cts = new CancellationTokenSource();
|
||||
worker = Task.Run(Worker);
|
||||
}
|
||||
@@ -50,7 +48,9 @@ namespace KubernetesWorkflow
|
||||
public bool HasContainerCrashed()
|
||||
{
|
||||
using var client = new Kubernetes(config);
|
||||
return HasContainerBeenRestarted(client);
|
||||
var result = HasContainerBeenRestarted(client);
|
||||
if (result) DownloadCrashedContainerLogs(client);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void Worker()
|
||||
@@ -83,14 +83,16 @@ namespace KubernetesWorkflow
|
||||
private bool HasContainerBeenRestarted(Kubernetes client)
|
||||
{
|
||||
var podInfo = client.ReadNamespacedPod(podName, k8sNamespace);
|
||||
return podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0);
|
||||
var result = podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0);
|
||||
if (result) log.Log("Pod crash detected for " + containerName);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void DownloadCrashedContainerLogs(Kubernetes client)
|
||||
{
|
||||
log.Log("Pod crash detected for " + containerName);
|
||||
using var stream = client.ReadNamespacedPodLog(podName, k8sNamespace, recipeName, previous: true);
|
||||
logHandler!.Log(stream);
|
||||
var handler = new WriteToFileLogHandler(log, "Crash detected for " + containerName);
|
||||
handler.Log(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
using Logging;
|
||||
|
||||
namespace Core
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public interface IDownloadedLog
|
||||
{
|
||||
string ContainerName { get; }
|
||||
|
||||
void IterateLines(Action<string> action, params string[] thatContain);
|
||||
string[] GetLinesContaining(string expectedString);
|
||||
string[] FindLinesThatContain(params string[] tags);
|
||||
string GetFilepath();
|
||||
void DeleteFile();
|
||||
}
|
||||
|
||||
@@ -13,9 +17,28 @@ namespace Core
|
||||
{
|
||||
private readonly LogFile logFile;
|
||||
|
||||
internal DownloadedLog(LogFile logFile)
|
||||
internal DownloadedLog(WriteToFileLogHandler logHandler, string containerName)
|
||||
{
|
||||
this.logFile = logFile;
|
||||
logFile = logHandler.LogFile;
|
||||
ContainerName = containerName;
|
||||
}
|
||||
|
||||
public string ContainerName { get; }
|
||||
|
||||
public void IterateLines(Action<string> action, params string[] thatContain)
|
||||
{
|
||||
using var file = File.OpenRead(logFile.FullFilename);
|
||||
using var streamReader = new StreamReader(file);
|
||||
|
||||
var line = streamReader.ReadLine();
|
||||
while (line != null)
|
||||
{
|
||||
if (thatContain.All(line.Contains))
|
||||
{
|
||||
action(line);
|
||||
}
|
||||
line = streamReader.ReadLine();
|
||||
}
|
||||
}
|
||||
|
||||
public string[] GetLinesContaining(string expectedString)
|
||||
@@ -57,6 +80,11 @@ namespace Core
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
public string GetFilepath()
|
||||
{
|
||||
return logFile.FullFilename;
|
||||
}
|
||||
|
||||
public void DeleteFile()
|
||||
{
|
||||
File.Delete(logFile.FullFilename);
|
||||
@@ -16,6 +16,7 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
var config = GetConfig();
|
||||
UpdateHostAddress(config);
|
||||
config.SkipTlsVerify = true; // Required for operation on Wings cluster.
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@ namespace KubernetesWorkflow
|
||||
return new StartResult(cluster, containerRecipes, deployment, internalService, externalService);
|
||||
}
|
||||
|
||||
public void WaitUntilOnline(RunningContainer container)
|
||||
{
|
||||
WaitUntilDeploymentOnline(container);
|
||||
}
|
||||
|
||||
public PodInfo GetPodInfo(RunningDeployment deployment)
|
||||
{
|
||||
var pod = GetPodForDeployment(deployment);
|
||||
@@ -59,14 +64,14 @@ namespace KubernetesWorkflow
|
||||
if (waitTillStopped) WaitUntilPodsForDeploymentAreOffline(startResult.Deployment);
|
||||
}
|
||||
|
||||
public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines)
|
||||
public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines, bool? previous)
|
||||
{
|
||||
log.Debug();
|
||||
|
||||
var podName = GetPodName(container);
|
||||
var recipeName = container.Recipe.Name;
|
||||
|
||||
using var stream = client.Run(c => c.ReadNamespacedPodLog(podName, K8sNamespace, recipeName, tailLines: tailLines));
|
||||
using var stream = client.Run(c => c.ReadNamespacedPodLog(podName, K8sNamespace, recipeName, tailLines: tailLines, previous: previous));
|
||||
logHandler.Log(stream);
|
||||
}
|
||||
|
||||
@@ -110,7 +115,7 @@ namespace KubernetesWorkflow
|
||||
});
|
||||
}
|
||||
|
||||
public void DeleteAllNamespacesStartingWith(string prefix)
|
||||
public void DeleteAllNamespacesStartingWith(string prefix, bool wait)
|
||||
{
|
||||
log.Debug();
|
||||
|
||||
@@ -119,25 +124,28 @@ namespace KubernetesWorkflow
|
||||
|
||||
foreach (var ns in namespaces)
|
||||
{
|
||||
DeleteNamespace(ns);
|
||||
DeleteNamespace(ns, wait);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteNamespace()
|
||||
public void DeleteNamespace(bool wait)
|
||||
{
|
||||
log.Debug();
|
||||
if (IsNamespaceOnline(K8sNamespace))
|
||||
{
|
||||
client.Run(c => c.DeleteNamespace(K8sNamespace, null, null, gracePeriodSeconds: 0));
|
||||
|
||||
if (wait) WaitUntilNamespaceDeleted(K8sNamespace);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteNamespace(string ns)
|
||||
public void DeleteNamespace(string ns, bool wait)
|
||||
{
|
||||
log.Debug();
|
||||
if (IsNamespaceOnline(ns))
|
||||
{
|
||||
client.Run(c => c.DeleteNamespace(ns, null, null, gracePeriodSeconds: 0));
|
||||
if (wait) WaitUntilNamespaceDeleted(ns);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +380,6 @@ namespace KubernetesWorkflow
|
||||
};
|
||||
|
||||
client.Run(c => c.CreateNamespacedDeployment(deploymentSpec, K8sNamespace));
|
||||
WaitUntilDeploymentOnline(deploymentSpec.Metadata.Name);
|
||||
|
||||
var name = deploymentSpec.Metadata.Name;
|
||||
return new RunningDeployment(name, podLabel);
|
||||
@@ -528,7 +535,7 @@ namespace KubernetesWorkflow
|
||||
}
|
||||
if (set.Memory.SizeInBytes != 0)
|
||||
{
|
||||
result.Add("memory", new ResourceQuantity(set.Memory.ToSuffixNotation()));
|
||||
result.Add("memory", new ResourceQuantity(set.Memory.SizeInBytes.ToString()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -701,14 +708,14 @@ namespace KubernetesWorkflow
|
||||
|
||||
private string GetPodName(RunningContainer container)
|
||||
{
|
||||
return GetPodForDeployment(container.RunningContainers.StartResult.Deployment).Metadata.Name;
|
||||
return GetPodForDeployment(container.RunningPod.StartResult.Deployment).Metadata.Name;
|
||||
}
|
||||
|
||||
private V1Pod GetPodForDeployment(RunningDeployment deployment)
|
||||
{
|
||||
return Time.Retry(() => GetPodForDeplomentInternal(deployment),
|
||||
// We will wait up to 1 minute, k8s might be moving pods around.
|
||||
maxRetries: 6,
|
||||
maxTimeout: TimeSpan.FromMinutes(1),
|
||||
retryTime: TimeSpan.FromSeconds(10),
|
||||
description: "Find pod by label for deployment.");
|
||||
}
|
||||
@@ -864,16 +871,45 @@ namespace KubernetesWorkflow
|
||||
|
||||
private void WaitUntilNamespaceCreated()
|
||||
{
|
||||
WaitUntil(() => IsNamespaceOnline(K8sNamespace));
|
||||
WaitUntil(() => IsNamespaceOnline(K8sNamespace), nameof(WaitUntilNamespaceCreated));
|
||||
}
|
||||
|
||||
private void WaitUntilDeploymentOnline(string deploymentName)
|
||||
private void WaitUntilNamespaceDeleted(string @namespace)
|
||||
{
|
||||
WaitUntil(() => !IsNamespaceOnline(@namespace), nameof(WaitUntilNamespaceDeleted));
|
||||
}
|
||||
|
||||
private void WaitUntilDeploymentOnline(RunningContainer container)
|
||||
{
|
||||
WaitUntil(() =>
|
||||
{
|
||||
var deployment = client.Run(c => c.ReadNamespacedDeployment(deploymentName, K8sNamespace));
|
||||
CheckForCrash(container);
|
||||
|
||||
var deployment = client.Run(c => c.ReadNamespacedDeployment(container.Recipe.Name, K8sNamespace));
|
||||
return deployment?.Status.AvailableReplicas != null && deployment.Status.AvailableReplicas > 0;
|
||||
});
|
||||
}, nameof(WaitUntilDeploymentOnline));
|
||||
}
|
||||
|
||||
private void CheckForCrash(RunningContainer container)
|
||||
{
|
||||
var deploymentName = container.Recipe.Name;
|
||||
var podName = GetPodName(container);
|
||||
|
||||
var podInfo = client.Run(c => c.ReadNamespacedPod(podName, K8sNamespace));
|
||||
if (podInfo == null) return;
|
||||
if (podInfo.Status == null) return;
|
||||
if (podInfo.Status.ContainerStatuses == null) return;
|
||||
|
||||
var result = podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0);
|
||||
if (result)
|
||||
{
|
||||
var msg = $"Pod crash detected for deployment {deploymentName} (pod:{podName})";
|
||||
log.Error(msg);
|
||||
|
||||
DownloadPodLog(container, new WriteToFileLogHandler(log, msg), tailLines: null, previous: true);
|
||||
|
||||
throw new Exception(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void WaitUntilDeploymentOffline(string deploymentName)
|
||||
@@ -883,7 +919,7 @@ namespace KubernetesWorkflow
|
||||
var deployments = client.Run(c => c.ListNamespacedDeployment(K8sNamespace));
|
||||
var deployment = deployments.Items.SingleOrDefault(d => d.Metadata.Name == deploymentName);
|
||||
return deployment == null || deployment.Status.AvailableReplicas == 0;
|
||||
});
|
||||
}, nameof(WaitUntilDeploymentOffline));
|
||||
}
|
||||
|
||||
private void WaitUntilPodsForDeploymentAreOffline(RunningDeployment deployment)
|
||||
@@ -892,19 +928,19 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
var pods = FindPodsByLabel(deployment.PodLabel);
|
||||
return !pods.Any();
|
||||
});
|
||||
}, nameof(WaitUntilPodsForDeploymentAreOffline));
|
||||
}
|
||||
|
||||
private void WaitUntil(Func<bool> predicate)
|
||||
private void WaitUntil(Func<bool> predicate, string msg)
|
||||
{
|
||||
var sw = Stopwatch.Begin(log, true);
|
||||
try
|
||||
{
|
||||
Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.K8sOperationRetryDelay());
|
||||
Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.K8sOperationRetryDelay(), msg);
|
||||
}
|
||||
finally
|
||||
{
|
||||
sw.End("", 1);
|
||||
sw.End(msg, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,18 +5,18 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
public interface IK8sHooks
|
||||
{
|
||||
void OnContainersStarted(RunningContainers runningContainers);
|
||||
void OnContainersStopped(RunningContainers runningContainers);
|
||||
void OnContainersStarted(RunningPod runningPod);
|
||||
void OnContainersStopped(RunningPod runningPod);
|
||||
void OnContainerRecipeCreated(ContainerRecipe recipe);
|
||||
}
|
||||
|
||||
public class DoNothingK8sHooks : IK8sHooks
|
||||
{
|
||||
public void OnContainersStarted(RunningContainers runningContainers)
|
||||
public void OnContainersStarted(RunningPod runningPod)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnContainersStopped(RunningContainers runningContainers)
|
||||
public void OnContainersStopped(RunningPod runningPod)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace KubernetesWorkflow
|
||||
using Logging;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public interface ILogHandler
|
||||
{
|
||||
@@ -20,4 +22,25 @@
|
||||
|
||||
protected abstract void ProcessLine(string line);
|
||||
}
|
||||
|
||||
public class WriteToFileLogHandler : LogHandler, ILogHandler
|
||||
{
|
||||
public WriteToFileLogHandler(ILog sourceLog, string description)
|
||||
{
|
||||
LogFile = sourceLog.CreateSubfile();
|
||||
|
||||
var msg = $"{description} -->> {LogFile.FullFilename}";
|
||||
sourceLog.Log(msg);
|
||||
|
||||
LogFile.Write(msg);
|
||||
LogFile.WriteRaw(description);
|
||||
}
|
||||
|
||||
public LogFile LogFile { get; }
|
||||
|
||||
protected override void ProcessLine(string line)
|
||||
{
|
||||
LogFile.WriteRaw(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
@@ -21,14 +22,13 @@ namespace KubernetesWorkflow.Recipe
|
||||
var typeName = GetTypeName(typeof(T));
|
||||
var userData = Additionals.SingleOrDefault(a => a.Type == typeName);
|
||||
if (userData == null) return default;
|
||||
var jobject = (JObject)userData.UserData;
|
||||
return jobject.ToObject<T>();
|
||||
return JsonConvert.DeserializeObject<T>(userData.UserData);
|
||||
}
|
||||
|
||||
private static Additional ConvertToAdditional(object userData)
|
||||
{
|
||||
var typeName = GetTypeName(userData.GetType());
|
||||
return new Additional(typeName, userData);
|
||||
return new Additional(typeName, JsonConvert.SerializeObject(userData));
|
||||
}
|
||||
|
||||
private static string GetTypeName(Type type)
|
||||
@@ -41,13 +41,13 @@ namespace KubernetesWorkflow.Recipe
|
||||
|
||||
public class Additional
|
||||
{
|
||||
public Additional(string type, object userData)
|
||||
public Additional(string type, string userData)
|
||||
{
|
||||
Type = type;
|
||||
UserData = userData;
|
||||
}
|
||||
|
||||
public string Type { get; }
|
||||
public object UserData { get; }
|
||||
public string UserData { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
{
|
||||
public class ContainerRecipe
|
||||
{
|
||||
public ContainerRecipe(int number, string? nameOverride, string image, ContainerResources resources, SchedulingAffinity schedulingAffinity, CommandOverride commandOverride, bool setCriticalPriority, Port[] exposedPorts, Port[] internalPorts, EnvVar[] envVars, PodLabels podLabels, PodAnnotations podAnnotations, VolumeMount[] volumes, ContainerAdditionals additionals)
|
||||
public ContainerRecipe(DateTime recipeCreatedUtc, int number, string? nameOverride, string image, ContainerResources resources, SchedulingAffinity schedulingAffinity, CommandOverride commandOverride, bool setCriticalPriority, Port[] exposedPorts, Port[] internalPorts, EnvVar[] envVars, PodLabels podLabels, PodAnnotations podAnnotations, VolumeMount[] volumes, ContainerAdditionals additionals)
|
||||
{
|
||||
RecipeCreatedUtc = recipeCreatedUtc;
|
||||
Number = number;
|
||||
NameOverride = nameOverride;
|
||||
Image = image;
|
||||
@@ -31,6 +32,7 @@
|
||||
if (exposedPorts.Any(p => string.IsNullOrEmpty(p.Tag))) throw new Exception("Port tags are required for all exposed ports.");
|
||||
}
|
||||
|
||||
public DateTime RecipeCreatedUtc { get; }
|
||||
public string Name { get; }
|
||||
public int Number { get; }
|
||||
public string? NameOverride { get; }
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace KubernetesWorkflow.Recipe
|
||||
|
||||
Initialize(config);
|
||||
|
||||
var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources, schedulingAffinity, commandOverride, setCriticalPriority,
|
||||
var recipe = new ContainerRecipe(DateTime.UtcNow, containerNumber, config.NameOverride, Image, resources, schedulingAffinity, commandOverride, setCriticalPriority,
|
||||
exposedPorts.ToArray(),
|
||||
internalPorts.ToArray(),
|
||||
envVars.ToArray(),
|
||||
@@ -105,7 +105,7 @@ namespace KubernetesWorkflow.Recipe
|
||||
|
||||
protected void AddVolume(string name, string mountPath, string? subPath = null, string? secret = null, string? hostPath = null)
|
||||
{
|
||||
var size = 10.MB().ToSuffixNotation();
|
||||
var size = 10.MB().SizeInBytes.ToString();
|
||||
volumeMounts.Add(new VolumeMount(name, mountPath, subPath, size, secret, hostPath));
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace KubernetesWorkflow.Recipe
|
||||
volumeMounts.Add(new VolumeMount(
|
||||
$"autovolume-{Guid.NewGuid().ToString().ToLowerInvariant()}",
|
||||
mountPath,
|
||||
resourceQuantity: volumeSize.ToSuffixNotation()));
|
||||
resourceQuantity: volumeSize.SizeInBytes.ToString()));
|
||||
}
|
||||
|
||||
protected void Additional(object userData)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using k8s;
|
||||
using k8s.Models;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
|
||||
@@ -9,16 +9,17 @@ namespace KubernetesWorkflow
|
||||
public interface IStartupWorkflow
|
||||
{
|
||||
IKnownLocations GetAvailableLocations();
|
||||
RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
|
||||
RunningContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
|
||||
FutureContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
|
||||
FutureContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
|
||||
PodInfo GetPodInfo(RunningContainer container);
|
||||
PodInfo GetPodInfo(RunningContainers containers);
|
||||
PodInfo GetPodInfo(RunningPod pod);
|
||||
CrashWatcher CreateCrashWatcher(RunningContainer container);
|
||||
void Stop(RunningContainers containers, bool waitTillStopped);
|
||||
void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null);
|
||||
void Stop(RunningPod pod, bool waitTillStopped);
|
||||
void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null, bool? previous = null);
|
||||
IDownloadedLog DownloadContainerLog(RunningContainer container, int? tailLines = null, bool? previous = null);
|
||||
string ExecuteCommand(RunningContainer container, string command, params string[] args);
|
||||
void DeleteNamespace();
|
||||
void DeleteNamespacesStartingWith(string namespacePrefix);
|
||||
void DeleteNamespace(bool wait);
|
||||
void DeleteNamespacesStartingWith(string namespacePrefix, bool wait);
|
||||
}
|
||||
|
||||
public class StartupWorkflow : IStartupWorkflow
|
||||
@@ -45,12 +46,12 @@ namespace KubernetesWorkflow
|
||||
return locationProvider.GetAvailableLocations();
|
||||
}
|
||||
|
||||
public RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
|
||||
public FutureContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
|
||||
{
|
||||
return Start(numberOfContainers, KnownLocations.UnspecifiedLocation, recipeFactory, startupConfig);
|
||||
}
|
||||
|
||||
public RunningContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
|
||||
public FutureContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
|
||||
{
|
||||
return K8s(controller =>
|
||||
{
|
||||
@@ -60,25 +61,36 @@ namespace KubernetesWorkflow
|
||||
var startResult = controller.BringOnline(recipes, location);
|
||||
var containers = CreateContainers(startResult, recipes, startupConfig);
|
||||
|
||||
var rc = new RunningContainers(startupConfig, startResult, containers);
|
||||
var rc = new RunningPod(Guid.NewGuid().ToString(), startupConfig, startResult, containers);
|
||||
cluster.Configuration.Hooks.OnContainersStarted(rc);
|
||||
|
||||
if (startResult.ExternalService != null)
|
||||
{
|
||||
componentFactory.Update(controller);
|
||||
}
|
||||
return rc;
|
||||
return new FutureContainers(rc, this);
|
||||
});
|
||||
}
|
||||
|
||||
public void WaitUntilOnline(RunningPod rc)
|
||||
{
|
||||
K8s(controller =>
|
||||
{
|
||||
foreach (var c in rc.Containers)
|
||||
{
|
||||
controller.WaitUntilOnline(c);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public PodInfo GetPodInfo(RunningContainer container)
|
||||
{
|
||||
return K8s(c => c.GetPodInfo(container.RunningContainers.StartResult.Deployment));
|
||||
return K8s(c => c.GetPodInfo(container.RunningPod.StartResult.Deployment));
|
||||
}
|
||||
|
||||
public PodInfo GetPodInfo(RunningContainers containers)
|
||||
public PodInfo GetPodInfo(RunningPod pod)
|
||||
{
|
||||
return K8s(c => c.GetPodInfo(containers.StartResult.Deployment));
|
||||
return K8s(c => c.GetPodInfo(pod.StartResult.Deployment));
|
||||
}
|
||||
|
||||
public CrashWatcher CreateCrashWatcher(RunningContainer container)
|
||||
@@ -86,21 +98,43 @@ namespace KubernetesWorkflow
|
||||
return K8s(c => c.CreateCrashWatcher(container));
|
||||
}
|
||||
|
||||
public void Stop(RunningContainers runningContainers, bool waitTillStopped)
|
||||
public void Stop(RunningPod runningPod, bool waitTillStopped)
|
||||
{
|
||||
if (runningPod.IsStopped) return;
|
||||
foreach (var c in runningPod.Containers)
|
||||
{
|
||||
c.StopLog = DownloadContainerLog(c);
|
||||
}
|
||||
runningPod.IsStopped = true;
|
||||
|
||||
K8s(controller =>
|
||||
{
|
||||
controller.Stop(runningPod.StartResult, waitTillStopped);
|
||||
});
|
||||
|
||||
cluster.Configuration.Hooks.OnContainersStopped(runningPod);
|
||||
}
|
||||
|
||||
public void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null, bool? previous = null)
|
||||
{
|
||||
K8s(controller =>
|
||||
{
|
||||
controller.Stop(runningContainers.StartResult, waitTillStopped);
|
||||
cluster.Configuration.Hooks.OnContainersStopped(runningContainers);
|
||||
controller.DownloadPodLog(container, logHandler, tailLines, previous);
|
||||
});
|
||||
}
|
||||
|
||||
public void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null)
|
||||
public IDownloadedLog DownloadContainerLog(RunningContainer container, int? tailLines = null, bool? previous = null)
|
||||
{
|
||||
var msg = $"Downloading container log for '{container.Name}'";
|
||||
log.Log(msg);
|
||||
var logHandler = new WriteToFileLogHandler(log, msg);
|
||||
|
||||
K8s(controller =>
|
||||
{
|
||||
controller.DownloadPodLog(container, logHandler, tailLines);
|
||||
controller.DownloadPodLog(container, logHandler, tailLines, previous);
|
||||
});
|
||||
|
||||
return new DownloadedLog(logHandler, container.Name);
|
||||
}
|
||||
|
||||
public string ExecuteCommand(RunningContainer container, string command, params string[] args)
|
||||
@@ -111,19 +145,19 @@ namespace KubernetesWorkflow
|
||||
});
|
||||
}
|
||||
|
||||
public void DeleteNamespace()
|
||||
public void DeleteNamespace(bool wait)
|
||||
{
|
||||
K8s(controller =>
|
||||
{
|
||||
controller.DeleteNamespace();
|
||||
controller.DeleteNamespace(wait);
|
||||
});
|
||||
}
|
||||
|
||||
public void DeleteNamespacesStartingWith(string namespacePrefix)
|
||||
public void DeleteNamespacesStartingWith(string namespacePrefix, bool wait)
|
||||
{
|
||||
K8s(controller =>
|
||||
{
|
||||
controller.DeleteAllNamespacesStartingWith(namespacePrefix);
|
||||
controller.DeleteAllNamespacesStartingWith(namespacePrefix, wait);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -136,7 +170,7 @@ namespace KubernetesWorkflow
|
||||
var addresses = CreateContainerAddresses(startResult, r);
|
||||
log.Debug($"{r}={name} -> container addresses: {string.Join(Environment.NewLine, addresses.Select(a => a.ToString()))}");
|
||||
|
||||
return new RunningContainer(name, r, addresses);
|
||||
return new RunningContainer(Guid.NewGuid().ToString(), name, r, addresses);
|
||||
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class FutureContainers
|
||||
{
|
||||
private readonly RunningPod runningPod;
|
||||
private readonly StartupWorkflow workflow;
|
||||
|
||||
public FutureContainers(RunningPod runningPod, StartupWorkflow workflow)
|
||||
{
|
||||
this.runningPod = runningPod;
|
||||
this.workflow = workflow;
|
||||
}
|
||||
|
||||
public RunningPod WaitForOnline()
|
||||
{
|
||||
workflow.WaitUntilOnline(runningPod);
|
||||
return runningPod;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,19 +7,22 @@ namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class RunningContainer
|
||||
{
|
||||
public RunningContainer(string name, ContainerRecipe recipe, ContainerAddress[] addresses)
|
||||
public RunningContainer(string id, string name, ContainerRecipe recipe, ContainerAddress[] addresses)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Recipe = recipe;
|
||||
Addresses = addresses;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public string Name { get; }
|
||||
public ContainerRecipe Recipe { get; }
|
||||
public ContainerAddress[] Addresses { get; }
|
||||
public IDownloadedLog? StopLog { get; internal set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public RunningContainers RunningContainers { get; internal set; } = null!;
|
||||
public RunningPod RunningPod { get; internal set; } = null!;
|
||||
|
||||
public Address GetAddress(ILog log, string portTag)
|
||||
{
|
||||
@@ -50,5 +53,21 @@ namespace KubernetesWorkflow.Types
|
||||
}
|
||||
throw new Exception("Running location not known.");
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is RunningContainer container &&
|
||||
Id == container.Id;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class RunningContainers
|
||||
{
|
||||
public RunningContainers(StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
|
||||
{
|
||||
StartupConfig = startupConfig;
|
||||
StartResult = startResult;
|
||||
Containers = containers;
|
||||
|
||||
foreach (var c in containers) c.RunningContainers = this;
|
||||
}
|
||||
|
||||
public StartupConfig StartupConfig { get; }
|
||||
public StartResult StartResult { get; }
|
||||
public RunningContainer[] Containers { get; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Name
|
||||
{
|
||||
get { return $"{Containers.Length}x '{Containers.First().Name}'"; }
|
||||
}
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
return string.Join(",", Containers.Select(c => c.Name));
|
||||
}
|
||||
}
|
||||
|
||||
public static class RunningContainersExtensions
|
||||
{
|
||||
public static RunningContainer[] Containers(this RunningContainers[] runningContainers)
|
||||
{
|
||||
return runningContainers.SelectMany(c => c.Containers).ToArray();
|
||||
}
|
||||
|
||||
public static string Describe(this RunningContainers[] runningContainers)
|
||||
{
|
||||
return string.Join(",", runningContainers.Select(c => c.Describe()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class RunningPod
|
||||
{
|
||||
public RunningPod(string id, StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
|
||||
{
|
||||
Id = id;
|
||||
StartupConfig = startupConfig;
|
||||
StartResult = startResult;
|
||||
Containers = containers;
|
||||
|
||||
foreach (var c in containers) c.RunningPod = this;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public StartupConfig StartupConfig { get; }
|
||||
public StartResult StartResult { get; }
|
||||
public RunningContainer[] Containers { get; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Name
|
||||
{
|
||||
get { return $"'{string.Join("&", Containers.Select(c => c.Name).ToArray())}'"; }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsStopped { get; internal set; }
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
return string.Join(",", Containers.Select(c => c.Name));
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is RunningPod pod &&
|
||||
Id == pod.Id;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Id);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (IsStopped) return Name + " (*)";
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RunningContainersExtensions
|
||||
{
|
||||
public static string Describe(this RunningPod[] runningContainers)
|
||||
{
|
||||
return string.Join(",", runningContainers.Select(c => c.Describe()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ namespace Logging
|
||||
return new LogFile($"{GetFullName()}_{GetSubfileNumber()}", ext);
|
||||
}
|
||||
|
||||
private string ApplyReplacements(string str)
|
||||
protected string ApplyReplacements(string str)
|
||||
{
|
||||
if (IsDebug) return str;
|
||||
foreach (var replacement in replacements)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
public override void Log(string message)
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
Console.WriteLine(ApplyReplacements(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,5 +35,7 @@
|
||||
if (!entries.TryGetValue(number, out BlockTimeEntry? value)) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
public int Size { get { return entries.Count; } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,21 @@ namespace NethereumWorkflow.BlockUtils
|
||||
bounds = new BlockchainBounds(cache, web3);
|
||||
}
|
||||
|
||||
public BlockTimeEntry Get(ulong blockNumber)
|
||||
{
|
||||
bounds.Initialize();
|
||||
var b = cache.Get(blockNumber);
|
||||
if (b != null) return b;
|
||||
return GetBlock(blockNumber);
|
||||
}
|
||||
|
||||
public ulong? GetHighestBlockNumberBefore(DateTime moment)
|
||||
{
|
||||
bounds.Initialize();
|
||||
if (moment <= bounds.Genesis.Utc) return null;
|
||||
if (moment >= bounds.Current.Utc) return bounds.Current.BlockNumber;
|
||||
|
||||
return Search(bounds.Genesis, bounds.Current, moment, HighestBeforeSelector);
|
||||
return Log(() => Search(bounds.Genesis, bounds.Current, moment, HighestBeforeSelector));
|
||||
}
|
||||
|
||||
public ulong? GetLowestBlockNumberAfter(DateTime moment)
|
||||
@@ -33,7 +41,16 @@ namespace NethereumWorkflow.BlockUtils
|
||||
if (moment >= bounds.Current.Utc) return null;
|
||||
if (moment <= bounds.Genesis.Utc) return bounds.Genesis.BlockNumber;
|
||||
|
||||
return Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector);
|
||||
return Log(()=> Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector)); ;
|
||||
}
|
||||
|
||||
private ulong Log(Func<ulong> operation)
|
||||
{
|
||||
var sw = Stopwatch.Begin(log, nameof(BlockTimeFinder), true);
|
||||
var result = operation();
|
||||
sw.End($"(Bounds: [{bounds.Genesis.BlockNumber}-{bounds.Current.BlockNumber}] Cache: {cache.Size})");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ulong Search(BlockTimeEntry lower, BlockTimeEntry upper, DateTime target, Func<DateTime, BlockTimeEntry, bool> isWhatIwant)
|
||||
@@ -70,7 +87,7 @@ namespace NethereumWorkflow.BlockUtils
|
||||
{
|
||||
var next = GetBlock(entry.BlockNumber + 1);
|
||||
return
|
||||
entry.Utc < target &&
|
||||
entry.Utc <= target &&
|
||||
next.Utc > target;
|
||||
}
|
||||
|
||||
@@ -78,7 +95,7 @@ namespace NethereumWorkflow.BlockUtils
|
||||
{
|
||||
var previous = GetBlock(entry.BlockNumber - 1);
|
||||
return
|
||||
entry.Utc > target &&
|
||||
entry.Utc >= target &&
|
||||
previous.Utc < target;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,26 +89,9 @@ namespace NethereumWorkflow
|
||||
}
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new()
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, BlockInterval blockRange) where TEvent : IEventDTO, new()
|
||||
{
|
||||
var wrapper = new Web3Wrapper(web3, log);
|
||||
var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log);
|
||||
|
||||
var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From);
|
||||
var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To);
|
||||
|
||||
if (!fromBlock.HasValue)
|
||||
{
|
||||
log.Error("Failed to find lowest block for time range: " + timeRange);
|
||||
throw new Exception("Failed");
|
||||
}
|
||||
if (!toBlock.HasValue)
|
||||
{
|
||||
log.Error("Failed to find highest block for time range: " + timeRange);
|
||||
throw new Exception("Failed");
|
||||
}
|
||||
|
||||
return GetEvents<TEvent>(address, fromBlock.Value, toBlock.Value);
|
||||
return GetEvents<TEvent>(address, blockRange.From, blockRange.To);
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
|
||||
@@ -119,5 +102,32 @@ namespace NethereumWorkflow
|
||||
var blockFilter = Time.Wait(eventHandler.CreateFilterBlockRangeAsync(from, to));
|
||||
return Time.Wait(eventHandler.GetAllChangesAsync(blockFilter));
|
||||
}
|
||||
|
||||
public BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange)
|
||||
{
|
||||
var wrapper = new Web3Wrapper(web3, log);
|
||||
var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log);
|
||||
|
||||
var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From);
|
||||
var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To);
|
||||
|
||||
if (fromBlock == null || toBlock == null)
|
||||
{
|
||||
throw new Exception("Failed to convert time range to block range.");
|
||||
}
|
||||
|
||||
return new BlockInterval(
|
||||
timeRange: timeRange,
|
||||
from: fromBlock.Value,
|
||||
to: toBlock.Value
|
||||
);
|
||||
}
|
||||
|
||||
public BlockTimeEntry GetBlockForNumber(ulong number)
|
||||
{
|
||||
var wrapper = new Web3Wrapper(web3, log);
|
||||
var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log);
|
||||
return blockTimeFinder.Get(number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
public class ActionQueue
|
||||
{
|
||||
// Using ConcurrentQueue<> here would make this process slower.
|
||||
private readonly object queueLock = new object();
|
||||
private readonly AutoResetEvent signal = new AutoResetEvent(false);
|
||||
private List<Action> queue = new List<Action>();
|
||||
private Task queueWorker = null!;
|
||||
private bool stopping = false;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
queueWorker = Task.Run(QueueWorker);
|
||||
}
|
||||
|
||||
public int Count { get; private set; }
|
||||
|
||||
public void StopAndJoin()
|
||||
{
|
||||
stopping = true;
|
||||
queueWorker.Wait();
|
||||
if (queue.Count > 0) throw new Exception("not all acions handled");
|
||||
queueWorker.Dispose();
|
||||
}
|
||||
|
||||
public void Add(Action action)
|
||||
{
|
||||
if (stopping) throw new Exception("queue stopping");
|
||||
|
||||
lock (queueLock)
|
||||
{
|
||||
queue.Add(action);
|
||||
Count = queue.Count;
|
||||
}
|
||||
signal.Set();
|
||||
}
|
||||
|
||||
private void QueueWorker()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
signal.WaitOne(10);
|
||||
|
||||
List<Action> work = null!;
|
||||
lock (queueLock)
|
||||
{
|
||||
work = queue;
|
||||
queue = new List<Action>();
|
||||
Count = 0;
|
||||
}
|
||||
if (stopping && !work.Any()) return;
|
||||
|
||||
foreach (var action in work)
|
||||
{
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using Logging;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
public class BucketSet
|
||||
{
|
||||
private const int numberOfActiveBuckets = 10;
|
||||
private readonly ILog log;
|
||||
private readonly string workingDir;
|
||||
private readonly object _bucketLock = new object();
|
||||
private readonly List<EventBucketWriter> fullBuckets = new List<EventBucketWriter>();
|
||||
private readonly List<EventBucketWriter> activeBuckets = new List<EventBucketWriter>();
|
||||
private readonly ActionQueue queue = new ActionQueue();
|
||||
private int activeBucketIndex = 0;
|
||||
private bool closed = false;
|
||||
private string internalErrors = string.Empty;
|
||||
|
||||
public BucketSet(ILog log, string workingDir)
|
||||
{
|
||||
this.log = log;
|
||||
this.workingDir = workingDir;
|
||||
|
||||
for (var i = 0; i < numberOfActiveBuckets;i++)
|
||||
{
|
||||
AddNewBucket();
|
||||
}
|
||||
|
||||
queue.Start();
|
||||
}
|
||||
|
||||
public void Add(DateTime utc, object payload)
|
||||
{
|
||||
if (closed) throw new Exception("Buckets already closed!");
|
||||
queue.Add(() => AddInternal(utc, payload));
|
||||
|
||||
if (queue.Count > 1000)
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
public IFinalizedBucket[] FinalizeBuckets()
|
||||
{
|
||||
closed = true;
|
||||
queue.StopAndJoin();
|
||||
|
||||
if (IsEmpty()) throw new Exception("No entries have been added.");
|
||||
if (!string.IsNullOrEmpty(internalErrors)) throw new Exception(internalErrors);
|
||||
|
||||
var buckets = fullBuckets.Concat(activeBuckets).ToArray();
|
||||
log.Debug($"Finalizing {buckets.Length} buckets...");
|
||||
|
||||
var finalized = new ConcurrentBag<IFinalizedBucket>();
|
||||
var tasks = Parallel.ForEach(buckets, b => finalized.Add(b.FinalizeBucket()));
|
||||
if (!tasks.IsCompleted) throw new Exception("Failed to finalize buckets: " + tasks);
|
||||
|
||||
return finalized.ToArray();
|
||||
}
|
||||
|
||||
private bool IsEmpty()
|
||||
{
|
||||
return fullBuckets.All(b => b.Count == 0) && activeBuckets.All(b => b.Count == 0);
|
||||
}
|
||||
|
||||
private void AddInternal(DateTime utc, object payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_bucketLock)
|
||||
{
|
||||
var current = activeBuckets[activeBucketIndex];
|
||||
current.Add(utc, payload);
|
||||
activeBucketIndex = (activeBucketIndex + 1) % numberOfActiveBuckets;
|
||||
|
||||
if (current.IsFull)
|
||||
{
|
||||
log.Debug("Bucket is full. New bucket...");
|
||||
fullBuckets.Add(current);
|
||||
activeBuckets.Remove(current);
|
||||
AddNewBucket();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
internalErrors += ex.ToString();
|
||||
log.Error(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private static int bucketSizeIndex = 0;
|
||||
private static int[] bucketSizes = new[]
|
||||
{
|
||||
10000,
|
||||
15000,
|
||||
20000,
|
||||
};
|
||||
|
||||
private void AddNewBucket()
|
||||
{
|
||||
lock (_bucketLock)
|
||||
{
|
||||
var size = bucketSizes[bucketSizeIndex];
|
||||
bucketSizeIndex = (bucketSizeIndex + 1) % bucketSizes.Length;
|
||||
activeBuckets.Add(new EventBucketWriter(log, Path.Combine(workingDir, Guid.NewGuid().ToString()), size));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
public interface IFinalizedBucket
|
||||
{
|
||||
bool IsEmpty { get; }
|
||||
DateTime? SeeTopUtc();
|
||||
BucketTop? TakeTop();
|
||||
}
|
||||
|
||||
public class BucketTop
|
||||
{
|
||||
public BucketTop(DateTime utc, OverwatchEvent[] events)
|
||||
{
|
||||
Utc = utc;
|
||||
Events = events;
|
||||
}
|
||||
|
||||
public DateTime Utc { get; }
|
||||
public OverwatchEvent[] Events { get; }
|
||||
}
|
||||
|
||||
public class EventBucketReader : IFinalizedBucket
|
||||
{
|
||||
private readonly string bucketFile;
|
||||
private readonly ConcurrentQueue<BucketTop> topQueue = new ConcurrentQueue<BucketTop>();
|
||||
private readonly AutoResetEvent itemDequeued = new AutoResetEvent(false);
|
||||
private bool stopping;
|
||||
|
||||
public EventBucketReader(ILog log, string bucketFile)
|
||||
{
|
||||
this.bucketFile = bucketFile;
|
||||
if (!File.Exists(bucketFile)) throw new Exception("Doesn't exist: " + bucketFile);
|
||||
|
||||
log.Debug("Read Bucket open: " + bucketFile);
|
||||
|
||||
Task.Run(ReadBucket);
|
||||
}
|
||||
|
||||
public bool IsEmpty { get; private set; }
|
||||
|
||||
public DateTime? SeeTopUtc()
|
||||
{
|
||||
if (IsEmpty) return null;
|
||||
while (true)
|
||||
{
|
||||
UpdateIsEmpty();
|
||||
if (IsEmpty) return null;
|
||||
if (topQueue.TryPeek(out BucketTop? top))
|
||||
{
|
||||
return top.Utc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BucketTop? TakeTop()
|
||||
{
|
||||
if (IsEmpty) return null;
|
||||
|
||||
while (true)
|
||||
{
|
||||
UpdateIsEmpty();
|
||||
if (IsEmpty) return null;
|
||||
if (topQueue.TryDequeue(out BucketTop? top))
|
||||
{
|
||||
itemDequeued.Set();
|
||||
return top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadBucket()
|
||||
{
|
||||
using var file = File.OpenRead(bucketFile);
|
||||
using var reader = new StreamReader(file);
|
||||
|
||||
while (true)
|
||||
{
|
||||
while (topQueue.Count < 5)
|
||||
{
|
||||
var top = CreateNewTop(reader);
|
||||
if (top != null)
|
||||
{
|
||||
topQueue.Enqueue(top);
|
||||
}
|
||||
else
|
||||
{
|
||||
stopping = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
itemDequeued.Reset();
|
||||
itemDequeued.WaitOne();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateIsEmpty()
|
||||
{
|
||||
var empty = stopping && topQueue.IsEmpty;
|
||||
if (!IsEmpty && empty)
|
||||
{
|
||||
File.Delete(bucketFile);
|
||||
IsEmpty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private EventBucketEntry? nextEntry = null;
|
||||
private BucketTop? CreateNewTop(StreamReader reader)
|
||||
{
|
||||
if (nextEntry == null)
|
||||
{
|
||||
nextEntry = ReadEntry(reader);
|
||||
if (nextEntry == null) return null;
|
||||
}
|
||||
|
||||
var topEntry = nextEntry;
|
||||
var entries = new List<EventBucketEntry>
|
||||
{
|
||||
topEntry
|
||||
};
|
||||
|
||||
nextEntry = ReadEntry(reader);
|
||||
while (nextEntry != null && nextEntry.Utc == topEntry.Utc)
|
||||
{
|
||||
entries.Add(nextEntry);
|
||||
nextEntry = ReadEntry(reader);
|
||||
}
|
||||
|
||||
return new BucketTop(topEntry.Utc, entries.Select(e => e.Event).ToArray());
|
||||
}
|
||||
|
||||
private EventBucketEntry? ReadEntry(StreamReader reader)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (string.IsNullOrEmpty(line)) return null;
|
||||
return JsonConvert.DeserializeObject<EventBucketEntry>(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
public class EventBucketWriter
|
||||
{
|
||||
private const int MaxBuffer = 1000;
|
||||
|
||||
private readonly object _lock = new object();
|
||||
private bool closed = false;
|
||||
private readonly ILog log;
|
||||
private readonly string bucketFile;
|
||||
private readonly int maxCount;
|
||||
private readonly List<EventBucketEntry> buffer = new List<EventBucketEntry>();
|
||||
|
||||
public EventBucketWriter(ILog log, string bucketFile, int maxCount)
|
||||
{
|
||||
this.log = log;
|
||||
this.bucketFile = bucketFile;
|
||||
this.maxCount = maxCount;
|
||||
if (File.Exists(bucketFile)) throw new Exception("Already exists");
|
||||
|
||||
log.Debug("Write Bucket open: " + bucketFile);
|
||||
}
|
||||
|
||||
public int Count { get; private set; }
|
||||
public bool IsFull { get; private set; }
|
||||
|
||||
public void Add(DateTime utc, object payload)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (closed) throw new Exception("Already closed");
|
||||
AddToBuffer(utc, payload);
|
||||
BufferToFile(emptyBuffer: false);
|
||||
}
|
||||
}
|
||||
|
||||
public IFinalizedBucket FinalizeBucket()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
closed = true;
|
||||
BufferToFile(emptyBuffer: true);
|
||||
SortFileByTimestamps();
|
||||
}
|
||||
log.Debug($"Finalized bucket with {Count} entries");
|
||||
return new EventBucketReader(log, bucketFile);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"EventBucket: " + Count;
|
||||
}
|
||||
|
||||
private void AddToBuffer(DateTime utc, object payload)
|
||||
{
|
||||
var typeName = payload.GetType().FullName;
|
||||
if (string.IsNullOrEmpty(typeName)) throw new Exception("Empty typename for payload");
|
||||
if (utc == default) throw new Exception("DateTimeUtc not set");
|
||||
|
||||
var entry = new EventBucketEntry
|
||||
{
|
||||
Utc = utc,
|
||||
Event = new OverwatchEvent
|
||||
{
|
||||
Type = typeName,
|
||||
Payload = Json.Serialize(payload)
|
||||
}
|
||||
};
|
||||
|
||||
Count++;
|
||||
IsFull = Count > maxCount;
|
||||
|
||||
buffer.Add(entry);
|
||||
}
|
||||
|
||||
private void BufferToFile(bool emptyBuffer)
|
||||
{
|
||||
if (emptyBuffer || buffer.Count > MaxBuffer)
|
||||
{
|
||||
using var file = File.Open(bucketFile, FileMode.Append);
|
||||
using var writer = new StreamWriter(file);
|
||||
foreach (var entry in buffer)
|
||||
{
|
||||
writer.WriteLine(Json.Serialize(entry));
|
||||
}
|
||||
log.Debug($"Bucket wrote {buffer.Count} entries to file.");
|
||||
buffer.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void SortFileByTimestamps()
|
||||
{
|
||||
var lines = File.ReadAllLines(bucketFile);
|
||||
var entries = lines.Select(Json.Deserialize<EventBucketEntry>)
|
||||
.Cast<EventBucketEntry>()
|
||||
.OrderBy(e => e.Utc)
|
||||
.ToArray();
|
||||
|
||||
File.Delete(bucketFile);
|
||||
File.WriteAllLines(bucketFile, entries.Select(e => Json.Serialize(e)));
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class EventBucketEntry
|
||||
{
|
||||
public DateTime Utc { get; set; }
|
||||
public OverwatchEvent Event { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Globalization;
|
||||
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
public static class Json
|
||||
{
|
||||
private static JsonSerializerSettings settings = new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.None,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Culture = CultureInfo.InvariantCulture,
|
||||
DateFormatHandling = DateFormatHandling.IsoDateFormat,
|
||||
FloatFormatHandling = FloatFormatHandling.Symbol
|
||||
};
|
||||
|
||||
public static string Serialize(object obj, Formatting formatting = Formatting.None)
|
||||
{
|
||||
return JsonConvert.SerializeObject(obj, formatting, settings);
|
||||
}
|
||||
|
||||
public static T Deserialize<T>(string json)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(json)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
[Serializable]
|
||||
public class OverwatchTranscript
|
||||
{
|
||||
public OverwatchHeader Header { get; set; } = new();
|
||||
public OverwatchMomentReference[] MomentReferences { get; set; } = Array.Empty<OverwatchMomentReference>();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class OverwatchMomentReference
|
||||
{
|
||||
public string MomentsFile { get; set; } = string.Empty;
|
||||
public int NumberOfMoments { get; set; }
|
||||
public int NumberOfEvents { get; set; }
|
||||
public DateTime EarliestUtc { get; set; }
|
||||
public DateTime LatestUtc { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class OverwatchHeader
|
||||
{
|
||||
public OverwatchCommonHeader Common { get; set; } = new();
|
||||
public OverwatchHeaderEntry[] Entries { get; set; } = Array.Empty<OverwatchHeaderEntry>();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class OverwatchCommonHeader
|
||||
{
|
||||
public long NumberOfMoments { get; set; }
|
||||
public long NumberOfEvents { get; set; }
|
||||
public DateTime EarliestUtc { get; set; }
|
||||
public DateTime LatestUtc { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class OverwatchHeaderEntry
|
||||
{
|
||||
public string Key { get; set; } = string.Empty;
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class OverwatchMoment
|
||||
{
|
||||
public DateTime Utc { get; set; }
|
||||
public OverwatchEvent[] Events { get; set; } = Array.Empty<OverwatchEvent>();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class OverwatchEvent
|
||||
{
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string Payload { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
public class MomentReader
|
||||
{
|
||||
private readonly OverwatchTranscript model;
|
||||
private readonly string workingDir;
|
||||
private int referenceIndex = 0;
|
||||
private int momentsRead = 0;
|
||||
private OpenReference currentRef;
|
||||
|
||||
public MomentReader(OverwatchTranscript model, string workingDir)
|
||||
{
|
||||
this.model = model;
|
||||
this.workingDir = workingDir;
|
||||
|
||||
currentRef = CreateOpenReference();
|
||||
}
|
||||
|
||||
public OverwatchMoment? Next()
|
||||
{
|
||||
if (referenceIndex >= model.MomentReferences.Length) return null;
|
||||
|
||||
var moment = currentRef.ReadNext();
|
||||
if (moment == null)
|
||||
{
|
||||
Close();
|
||||
|
||||
// This reference file ran out.
|
||||
// The number of moments read should match exactly the number of moments
|
||||
// describe in the reference. If not, error:
|
||||
var expected = model.MomentReferences[referenceIndex].NumberOfMoments;
|
||||
if (momentsRead != expected)
|
||||
{
|
||||
throw new Exception("Number of moments read from referenced file does not match number of moments value in model. " +
|
||||
$"Reads: { momentsRead} - model.MomentReferences[{referenceIndex}].NumberOfMoment: {expected}");
|
||||
}
|
||||
|
||||
referenceIndex++;
|
||||
if (referenceIndex < model.MomentReferences.Length)
|
||||
{
|
||||
// Proceed to next reference file.
|
||||
currentRef = CreateOpenReference();
|
||||
momentsRead = 0;
|
||||
return Next();
|
||||
}
|
||||
else
|
||||
{
|
||||
// That was the last one.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
momentsRead++;
|
||||
return moment;
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (currentRef != null)
|
||||
{
|
||||
currentRef.Close();
|
||||
currentRef = null!;
|
||||
}
|
||||
}
|
||||
|
||||
private OpenReference CreateOpenReference()
|
||||
{
|
||||
var filepath = Path.Combine(workingDir, model.MomentReferences[referenceIndex].MomentsFile);
|
||||
return new OpenReference(filepath);
|
||||
}
|
||||
|
||||
private class OpenReference
|
||||
{
|
||||
private readonly FileStream file;
|
||||
private readonly StreamReader reader;
|
||||
|
||||
public OpenReference(string filePath)
|
||||
{
|
||||
file = File.OpenRead(filePath);
|
||||
reader = new StreamReader(file);
|
||||
}
|
||||
|
||||
public OverwatchMoment? ReadNext()
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (string.IsNullOrEmpty(line)) return null;
|
||||
return JsonConvert.DeserializeObject<OverwatchMoment>(line);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
reader.Close();
|
||||
file.Close();
|
||||
|
||||
reader.Dispose();
|
||||
file.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
public class MomentReferenceBuilder
|
||||
{
|
||||
private const int MaxMomentsPerReference = 10000;
|
||||
private readonly ILog log;
|
||||
private readonly string workingDir;
|
||||
|
||||
public MomentReferenceBuilder(ILog log, string workingDir)
|
||||
{
|
||||
this.log = log;
|
||||
this.workingDir = workingDir;
|
||||
}
|
||||
|
||||
public OverwatchMomentReference[] Build(IFinalizedBucket[] finalizedBuckets)
|
||||
{
|
||||
var result = new List<OverwatchMomentReference>();
|
||||
var currentBuilder = new Builder(log, workingDir);
|
||||
|
||||
var buckets = finalizedBuckets.ToList();
|
||||
log.Debug($"Building references for {buckets.Count} buckets.");
|
||||
while (buckets.Any())
|
||||
{
|
||||
buckets.RemoveAll(b => b.IsEmpty);
|
||||
if (!buckets.Any()) break;
|
||||
|
||||
var earliestUtc = GetEarliestUtc(buckets);
|
||||
if (earliestUtc == null) continue;
|
||||
|
||||
var tops = CollectAllTopsForUtc(earliestUtc.Value, buckets);
|
||||
var moment = ConvertTopsToMoment(tops);
|
||||
currentBuilder.Add(moment);
|
||||
if (currentBuilder.NumberOfMoments == MaxMomentsPerReference)
|
||||
{
|
||||
result.Add(currentBuilder.Build());
|
||||
currentBuilder = new Builder(log, workingDir);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentBuilder.NumberOfMoments > 0)
|
||||
{
|
||||
result.Add(currentBuilder.Build());
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private OverwatchMoment ConvertTopsToMoment(List<BucketTop> tops)
|
||||
{
|
||||
var discintUtc = tops.Select(e => e.Utc).Distinct().ToArray();
|
||||
if (discintUtc.Length != 1) throw new Exception("UTC mixing in moment construction.");
|
||||
|
||||
return new OverwatchMoment
|
||||
{
|
||||
Utc = tops[0].Utc,
|
||||
Events = tops.SelectMany(e => e.Events).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private List<BucketTop> CollectAllTopsForUtc(DateTime earliestUtc, List<IFinalizedBucket> buckets)
|
||||
{
|
||||
var result = new List<BucketTop>();
|
||||
|
||||
foreach (var bucket in buckets)
|
||||
{
|
||||
if (bucket.IsEmpty) continue;
|
||||
|
||||
var utc = bucket.SeeTopUtc();
|
||||
if (utc == null) continue;
|
||||
|
||||
if (utc.Value == earliestUtc)
|
||||
{
|
||||
var top = bucket.TakeTop();
|
||||
if (top == null) throw new Exception("top was null after top utc was not");
|
||||
result.Add(top);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private DateTime? GetEarliestUtc(List<IFinalizedBucket> buckets)
|
||||
{
|
||||
var earliest = DateTime.MaxValue;
|
||||
foreach (var bucket in buckets)
|
||||
{
|
||||
var utc = bucket.SeeTopUtc();
|
||||
if (utc == null) return null;
|
||||
|
||||
if (utc.Value < earliest) earliest = utc.Value;
|
||||
}
|
||||
return earliest;
|
||||
}
|
||||
|
||||
public class Builder
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly string workingDir;
|
||||
private OverwatchMomentReference reference;
|
||||
private readonly ActionQueue queue = new ActionQueue();
|
||||
|
||||
public Builder(ILog log, string workingDir)
|
||||
{
|
||||
reference = new OverwatchMomentReference
|
||||
{
|
||||
MomentsFile = Guid.NewGuid().ToString(),
|
||||
EarliestUtc = DateTime.MaxValue,
|
||||
LatestUtc = DateTime.MinValue,
|
||||
NumberOfEvents = 0,
|
||||
NumberOfMoments = 0,
|
||||
};
|
||||
this.log = log;
|
||||
this.workingDir = workingDir;
|
||||
queue.Start();
|
||||
}
|
||||
|
||||
public int NumberOfMoments => reference.NumberOfMoments;
|
||||
|
||||
public void Add(OverwatchMoment moment)
|
||||
{
|
||||
if (moment.Utc < reference.EarliestUtc) reference.EarliestUtc = moment.Utc;
|
||||
if (moment.Utc > reference.LatestUtc) reference.LatestUtc = moment.Utc;
|
||||
reference.NumberOfMoments++;
|
||||
reference.NumberOfEvents += moment.Events.Length;
|
||||
|
||||
var filePath = Path.Combine(workingDir, reference.MomentsFile);
|
||||
|
||||
queue.Add(() =>
|
||||
{
|
||||
File.AppendAllLines(filePath, new[]
|
||||
{
|
||||
Json.Serialize(moment)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public OverwatchMomentReference Build()
|
||||
{
|
||||
queue.StopAndJoin();
|
||||
|
||||
log.Debug($"Created reference with {reference.NumberOfMoments} moments and {reference.NumberOfEvents} events...");
|
||||
var result = reference;
|
||||
reference = null!;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Logging\Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Logging;
|
||||
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
public static class Transcript
|
||||
{
|
||||
public static ITranscriptWriter NewWriter(ILog log)
|
||||
{
|
||||
log = new LogPrefixer(log, "(TranscriptWriter) ");
|
||||
return new TranscriptWriter(log, NewWorkDir());
|
||||
}
|
||||
|
||||
public static ITranscriptReader NewReader(string transcriptFile)
|
||||
{
|
||||
return new TranscriptReader(NewWorkDir(), transcriptFile);
|
||||
}
|
||||
|
||||
private static string NewWorkDir()
|
||||
{
|
||||
return Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
public static class TranscriptConstants
|
||||
{
|
||||
public const string TranscriptFilename = "transcript.json";
|
||||
public const string ArtifactFolderName = "artifacts";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
public interface ITranscriptReader
|
||||
{
|
||||
OverwatchCommonHeader Header { get; }
|
||||
T GetHeader<T>(string key);
|
||||
void AddMomentHandler(Action<ActivateMoment> handler);
|
||||
void AddEventHandler<T>(Action<ActivateEvent<T>> handler);
|
||||
bool Next();
|
||||
void Close();
|
||||
}
|
||||
|
||||
public class TranscriptReader : ITranscriptReader
|
||||
{
|
||||
private readonly object handlersLock = new object();
|
||||
private readonly string transcriptFile;
|
||||
private readonly string artifactsFolder;
|
||||
private readonly List<Action<ActivateMoment>> momentHandlers = new List<Action<ActivateMoment>>();
|
||||
private readonly Dictionary<string, List<Action<ActivateMoment, string>>> eventHandlers = new Dictionary<string, List<Action<ActivateMoment, string>>>();
|
||||
private readonly string workingDir;
|
||||
private readonly OverwatchTranscript model;
|
||||
private bool closed;
|
||||
private long momentCounter;
|
||||
private readonly ConcurrentQueue<OverwatchMoment> queue = new ConcurrentQueue<OverwatchMoment>();
|
||||
private readonly Task queueFiller;
|
||||
|
||||
public TranscriptReader(string workingDir, string inputFilename)
|
||||
{
|
||||
closed = false;
|
||||
this.workingDir = workingDir;
|
||||
transcriptFile = Path.Combine(workingDir, TranscriptConstants.TranscriptFilename);
|
||||
artifactsFolder = Path.Combine(workingDir, TranscriptConstants.ArtifactFolderName);
|
||||
|
||||
if (!Directory.Exists(workingDir)) Directory.CreateDirectory(workingDir);
|
||||
if (File.Exists(transcriptFile) || Directory.Exists(artifactsFolder)) throw new Exception("workingdir not clean");
|
||||
|
||||
model = LoadModel(inputFilename);
|
||||
|
||||
queueFiller = Task.Run(() => FillQueue(model, workingDir));
|
||||
}
|
||||
|
||||
public OverwatchCommonHeader Header
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckClosed();
|
||||
return model.Header.Common;
|
||||
}
|
||||
}
|
||||
|
||||
public T GetHeader<T>(string key)
|
||||
{
|
||||
CheckClosed();
|
||||
var value = model.Header.Entries.First(e => e.Key == key).Value;
|
||||
return JsonConvert.DeserializeObject<T>(value)!;
|
||||
}
|
||||
|
||||
public void AddMomentHandler(Action<ActivateMoment> handler)
|
||||
{
|
||||
CheckClosed();
|
||||
lock (handlersLock)
|
||||
{
|
||||
momentHandlers.Add(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEventHandler<T>(Action<ActivateEvent<T>> handler)
|
||||
{
|
||||
CheckClosed();
|
||||
|
||||
var typeName = typeof(T).FullName;
|
||||
if (string.IsNullOrEmpty(typeName)) throw new Exception("Empty typename for payload");
|
||||
|
||||
lock (handlersLock)
|
||||
{
|
||||
if (eventHandlers.ContainsKey(typeName))
|
||||
{
|
||||
eventHandlers[typeName].Add(CreateEventAction(handler));
|
||||
}
|
||||
else
|
||||
{
|
||||
eventHandlers.Add(typeName, new List<Action<ActivateMoment, string>>
|
||||
{
|
||||
CreateEventAction(handler)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly object nextLock = new object();
|
||||
private OverwatchMoment? moment = null;
|
||||
private OverwatchMoment? next = null;
|
||||
|
||||
public bool Next()
|
||||
{
|
||||
CheckClosed();
|
||||
|
||||
OverwatchMoment? m = null;
|
||||
TimeSpan? duration = null;
|
||||
lock (nextLock)
|
||||
{
|
||||
if (next == null)
|
||||
{
|
||||
if (!queue.TryDequeue(out moment)) return false;
|
||||
queue.TryDequeue(out next);
|
||||
}
|
||||
else
|
||||
{
|
||||
moment = next;
|
||||
next = null;
|
||||
queue.TryDequeue(out next);
|
||||
}
|
||||
|
||||
m = moment;
|
||||
duration = GetMomentDuration();
|
||||
}
|
||||
|
||||
ActivateMoment(moment, duration);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
CheckClosed();
|
||||
closed = true;
|
||||
|
||||
queueFiller.Wait();
|
||||
|
||||
Directory.Delete(workingDir, true);
|
||||
}
|
||||
|
||||
private Action<ActivateMoment, string> CreateEventAction<T>(Action<ActivateEvent<T>> handler)
|
||||
{
|
||||
return (m, s) =>
|
||||
{
|
||||
handler(new ActivateEvent<T>(m, JsonConvert.DeserializeObject<T>(s)!));
|
||||
};
|
||||
}
|
||||
|
||||
private void FillQueue(OverwatchTranscript model, string workingDir)
|
||||
{
|
||||
var reader = new MomentReader(model, workingDir);
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (closed)
|
||||
{
|
||||
reader.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
while (queue.Count < 10)
|
||||
{
|
||||
var moment = reader.Next();
|
||||
if (moment == null)
|
||||
{
|
||||
reader.Close();
|
||||
return;
|
||||
}
|
||||
queue.Enqueue(moment);
|
||||
}
|
||||
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
private TimeSpan? GetMomentDuration()
|
||||
{
|
||||
if (moment == null) return null;
|
||||
if (next == null) return null;
|
||||
|
||||
return next.Utc - moment.Utc;
|
||||
}
|
||||
|
||||
private void ActivateMoment(OverwatchMoment moment, TimeSpan? duration)
|
||||
{
|
||||
var m = new ActivateMoment(moment.Utc, duration, momentCounter);
|
||||
|
||||
lock (handlersLock)
|
||||
{
|
||||
ActivateMomentHandlers(m);
|
||||
|
||||
foreach (var @event in moment.Events)
|
||||
{
|
||||
ActivateEventHandlers(m, @event);
|
||||
}
|
||||
}
|
||||
|
||||
momentCounter++;
|
||||
}
|
||||
|
||||
private void ActivateMomentHandlers(ActivateMoment m)
|
||||
{
|
||||
foreach (var handler in momentHandlers)
|
||||
{
|
||||
handler(m);
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateEventHandlers(ActivateMoment m, OverwatchEvent @event)
|
||||
{
|
||||
if (!eventHandlers.ContainsKey(@event.Type)) return;
|
||||
var handlers = eventHandlers[@event.Type];
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
handler(m, @event.Payload);
|
||||
}
|
||||
}
|
||||
|
||||
private OverwatchTranscript LoadModel(string inputFilename)
|
||||
{
|
||||
ZipFile.ExtractToDirectory(inputFilename, workingDir);
|
||||
|
||||
if (!File.Exists(transcriptFile))
|
||||
{
|
||||
closed = true;
|
||||
throw new Exception("Is not a transcript file. Unzipped to: " + workingDir);
|
||||
}
|
||||
|
||||
return JsonConvert.DeserializeObject<OverwatchTranscript>(File.ReadAllText(transcriptFile))!;
|
||||
}
|
||||
|
||||
private void CheckClosed()
|
||||
{
|
||||
if (closed) throw new Exception("Transcript has already been closed.");
|
||||
}
|
||||
}
|
||||
|
||||
public class ActivateMoment
|
||||
{
|
||||
public ActivateMoment(DateTime utc, TimeSpan? duration, long index)
|
||||
{
|
||||
Utc = utc;
|
||||
Duration = duration;
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public DateTime Utc { get; }
|
||||
public TimeSpan? Duration { get; }
|
||||
public long Index { get; }
|
||||
}
|
||||
|
||||
public class ActivateEvent<T>
|
||||
{
|
||||
public ActivateEvent(ActivateMoment moment, T payload)
|
||||
{
|
||||
Moment = moment;
|
||||
Payload = payload;
|
||||
}
|
||||
|
||||
public ActivateMoment Moment { get; }
|
||||
public T Payload { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace OverwatchTranscript
|
||||
{
|
||||
public interface ITranscriptWriter
|
||||
{
|
||||
void AddHeader(string key, object value);
|
||||
void Add(DateTime utc, object payload);
|
||||
void IncludeArtifact(string filePath);
|
||||
void Write(string outputFilename);
|
||||
}
|
||||
|
||||
public class TranscriptWriter : ITranscriptWriter
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
private readonly MomentReferenceBuilder builder;
|
||||
private readonly string transcriptFile;
|
||||
private readonly string artifactsFolder;
|
||||
private readonly Dictionary<string, string> header = new Dictionary<string, string>();
|
||||
private readonly BucketSet bucketSet;
|
||||
private readonly ILog log;
|
||||
private readonly string workingDir;
|
||||
private bool closed;
|
||||
|
||||
public TranscriptWriter(ILog log, string workingDir)
|
||||
{
|
||||
closed = false;
|
||||
this.log = log;
|
||||
this.workingDir = workingDir;
|
||||
bucketSet = new BucketSet(log, workingDir);
|
||||
builder = new MomentReferenceBuilder(log, workingDir);
|
||||
transcriptFile = Path.Combine(workingDir, TranscriptConstants.TranscriptFilename);
|
||||
artifactsFolder = Path.Combine(workingDir, TranscriptConstants.ArtifactFolderName);
|
||||
|
||||
if (!Directory.Exists(workingDir)) Directory.CreateDirectory(workingDir);
|
||||
if (File.Exists(transcriptFile) || Directory.Exists(artifactsFolder)) throw new Exception("workingdir not clean");
|
||||
}
|
||||
|
||||
public void Add(DateTime utc, object payload)
|
||||
{
|
||||
CheckClosed();
|
||||
bucketSet.Add(utc, payload);
|
||||
}
|
||||
|
||||
public void AddHeader(string key, object value)
|
||||
{
|
||||
CheckClosed();
|
||||
lock (_lock)
|
||||
{
|
||||
header.Add(key, Json.Serialize(value));
|
||||
}
|
||||
}
|
||||
|
||||
public void IncludeArtifact(string filePath)
|
||||
{
|
||||
CheckClosed();
|
||||
if (!File.Exists(filePath)) throw new Exception("File not found: " + filePath);
|
||||
if (!Directory.Exists(artifactsFolder)) Directory.CreateDirectory(artifactsFolder);
|
||||
var name = Path.GetFileName(filePath);
|
||||
File.Copy(filePath, Path.Combine(artifactsFolder, name), overwrite: false);
|
||||
}
|
||||
|
||||
public void Write(string outputFilename)
|
||||
{
|
||||
CheckClosed();
|
||||
closed = true;
|
||||
|
||||
var momentReferences = builder.Build(bucketSet.FinalizeBuckets());
|
||||
var model = CreateModel(momentReferences);
|
||||
|
||||
File.WriteAllText(transcriptFile, Json.Serialize(model, Formatting.Indented));
|
||||
|
||||
ZipFile.CreateFromDirectory(workingDir, outputFilename);
|
||||
log.Debug($"Transcript written to {outputFilename}");
|
||||
log.Debug($"Common header: {Json.Serialize(model.Header.Common, Formatting.Indented)}");
|
||||
|
||||
Directory.Delete(workingDir, true);
|
||||
log.Debug($"Workdir {workingDir} deleted");
|
||||
}
|
||||
|
||||
private OverwatchTranscript CreateModel(OverwatchMomentReference[] momentReferences)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var model = new OverwatchTranscript
|
||||
{
|
||||
Header = new OverwatchHeader
|
||||
{
|
||||
Common = CreateCommonHeader(momentReferences),
|
||||
Entries = header.Select(h =>
|
||||
{
|
||||
return new OverwatchHeaderEntry
|
||||
{
|
||||
Key = h.Key,
|
||||
Value = h.Value
|
||||
};
|
||||
}).ToArray()
|
||||
},
|
||||
MomentReferences = momentReferences
|
||||
};
|
||||
|
||||
header.Clear();
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
private OverwatchCommonHeader CreateCommonHeader(OverwatchMomentReference[] momentReferences)
|
||||
{
|
||||
var moments = momentReferences.Sum(m => m.NumberOfMoments);
|
||||
var events = momentReferences.Sum(m => m.NumberOfEvents);
|
||||
var earliest = momentReferences.Min(m => m.EarliestUtc);
|
||||
var latest = momentReferences.Max(m => m.LatestUtc);
|
||||
|
||||
return new OverwatchCommonHeader
|
||||
{
|
||||
NumberOfMoments = moments,
|
||||
NumberOfEvents = events,
|
||||
EarliestUtc = earliest,
|
||||
LatestUtc = latest
|
||||
};
|
||||
}
|
||||
|
||||
private void CheckClosed()
|
||||
{
|
||||
if (closed) throw new Exception("Transcript has already been written. Cannot modify or write again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace Utils
|
||||
{
|
||||
public class BlockInterval
|
||||
{
|
||||
public BlockInterval(TimeRange timeRange, ulong from, ulong to)
|
||||
{
|
||||
if (from < to)
|
||||
{
|
||||
From = from;
|
||||
To = to;
|
||||
}
|
||||
else
|
||||
{
|
||||
From = to;
|
||||
To = from;
|
||||
}
|
||||
TimeRange = timeRange;
|
||||
}
|
||||
|
||||
public ulong From { get; }
|
||||
public ulong To { get; }
|
||||
public TimeRange TimeRange { get; }
|
||||
public ulong NumberOfBlocks => To - From;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{From} - {To}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
public long SizeInBytes { get; }
|
||||
|
||||
|
||||
public long ToMB()
|
||||
{
|
||||
return SizeInBytes / (1024 * 1024);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Utils
|
||||
using System.Globalization;
|
||||
|
||||
namespace Utils
|
||||
{
|
||||
public static class Formatter
|
||||
{
|
||||
@@ -10,7 +12,7 @@
|
||||
|
||||
var sizeOrder = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
|
||||
var digit = Math.Round(bytes / Math.Pow(1024, sizeOrder), 1);
|
||||
return digit.ToString() + sizeSuffixes[sizeOrder];
|
||||
return digit.ToString(CultureInfo.InvariantCulture) + sizeSuffixes[sizeOrder];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
public class NumberSource
|
||||
{
|
||||
private readonly object @lock = new object();
|
||||
private int number;
|
||||
|
||||
public NumberSource(int start)
|
||||
@@ -11,8 +12,12 @@
|
||||
|
||||
public int GetNextNumber()
|
||||
{
|
||||
var n = number;
|
||||
number++;
|
||||
var n = -1;
|
||||
lock (@lock)
|
||||
{
|
||||
n = number;
|
||||
number++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,16 @@
|
||||
remainingItems.RemoveAt(i);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static T[] Shuffled<T>(T[] items)
|
||||
{
|
||||
var result = new List<T>();
|
||||
var source = items.ToList();
|
||||
while (source.Any())
|
||||
{
|
||||
result.Add(RandomUtils.PickOneRandom(source));
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
namespace Utils
|
||||
{
|
||||
public class Retry
|
||||
{
|
||||
private readonly string description;
|
||||
private readonly TimeSpan maxTimeout;
|
||||
private readonly TimeSpan sleepAfterFail;
|
||||
private readonly Action<Failure> onFail;
|
||||
|
||||
public Retry(string description, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action<Failure> onFail)
|
||||
{
|
||||
this.description = description;
|
||||
this.maxTimeout = maxTimeout;
|
||||
this.sleepAfterFail = sleepAfterFail;
|
||||
this.onFail = onFail;
|
||||
}
|
||||
|
||||
public void Run(Action task)
|
||||
{
|
||||
var run = new RetryRun(description, task, maxTimeout, sleepAfterFail, onFail);
|
||||
run.Run();
|
||||
}
|
||||
|
||||
public T Run<T>(Func<T> task)
|
||||
{
|
||||
T? result = default;
|
||||
|
||||
var run = new RetryRun(description, () =>
|
||||
{
|
||||
result = task();
|
||||
}, maxTimeout, sleepAfterFail, onFail);
|
||||
run.Run();
|
||||
|
||||
return result!;
|
||||
}
|
||||
|
||||
private class RetryRun
|
||||
{
|
||||
private readonly string description;
|
||||
private readonly Action task;
|
||||
private readonly TimeSpan maxTimeout;
|
||||
private readonly TimeSpan sleepAfterFail;
|
||||
private readonly Action<Failure> onFail;
|
||||
private readonly DateTime start = DateTime.UtcNow;
|
||||
private readonly List<Failure> failures = new List<Failure>();
|
||||
private int tryNumber;
|
||||
private DateTime tryStart;
|
||||
|
||||
public RetryRun(string description, Action task, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action<Failure> onFail)
|
||||
{
|
||||
this.description = description;
|
||||
this.task = task;
|
||||
this.maxTimeout = maxTimeout;
|
||||
this.sleepAfterFail = sleepAfterFail;
|
||||
this.onFail = onFail;
|
||||
|
||||
tryNumber = 0;
|
||||
tryStart = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
CheckMaximums();
|
||||
|
||||
tryNumber++;
|
||||
tryStart = DateTime.UtcNow;
|
||||
try
|
||||
{
|
||||
task();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var failure = CaptureFailure(ex);
|
||||
onFail(failure);
|
||||
Time.Sleep(sleepAfterFail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Failure CaptureFailure(Exception ex)
|
||||
{
|
||||
var f = new Failure(ex, DateTime.UtcNow - tryStart, tryNumber);
|
||||
failures.Add(f);
|
||||
return f;
|
||||
}
|
||||
|
||||
private void CheckMaximums()
|
||||
{
|
||||
if (Duration() > maxTimeout) Fail();
|
||||
}
|
||||
|
||||
private void Fail()
|
||||
{
|
||||
throw new TimeoutException($"Retry '{description}' timed out after {tryNumber} tries over {Time.FormatDuration(Duration())}: {GetFailureReport}",
|
||||
new AggregateException(failures.Select(f => f.Exception)));
|
||||
}
|
||||
|
||||
private string GetFailureReport()
|
||||
{
|
||||
return Environment.NewLine + string.Join(Environment.NewLine, failures.Select(f => f.Describe()));
|
||||
}
|
||||
|
||||
private TimeSpan Duration()
|
||||
{
|
||||
return DateTime.UtcNow - start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Failure
|
||||
{
|
||||
public Failure(Exception exception, TimeSpan duration, int tryNumber)
|
||||
{
|
||||
Exception = exception;
|
||||
Duration = duration;
|
||||
TryNumber = tryNumber;
|
||||
}
|
||||
|
||||
public Exception Exception { get; }
|
||||
public TimeSpan Duration { get; }
|
||||
public int TryNumber { get; }
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
return $"Try {TryNumber} failed after {Time.FormatDuration(Duration)} with exception '{Exception}'";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Utils
|
||||
{
|
||||
public static class RollingAverage
|
||||
{
|
||||
/// <param name="currentAverage">Value of average before new value is added.</param>
|
||||
/// <param name="newNumberOfValues">Number of values in average after new value is added.</param>
|
||||
/// <param name="newValue">New value to be added.</param>
|
||||
/// <returns>New average value.</returns>
|
||||
/// <exception cref="Exception">newNumberOfValues must be 1 or greater.</exception>
|
||||
public static float GetNewAverage(float currentAverage, int newNumberOfValues, float newValue)
|
||||
{
|
||||
if (newNumberOfValues < 1) throw new Exception("Should be at least 1 value.");
|
||||
|
||||
float n = newNumberOfValues;
|
||||
var originalValue = currentAverage;
|
||||
var originalValueWeight = ((n - 1.0f) / n);
|
||||
var newValueWeight = (1.0f / n);
|
||||
return GetWeightedAverage(originalValue, originalValueWeight, newValue, newValueWeight);
|
||||
}
|
||||
|
||||
public static float GetWeightedAverage(float value1, float weight1, float value2, float weight2)
|
||||
{
|
||||
float totalWeight = weight1 + weight2;
|
||||
if (totalWeight == 0.0f) return 0.0f;
|
||||
return ((value1 * weight1) + (value2 * weight2)) / totalWeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Utils
|
||||
{
|
||||
public static class Str
|
||||
{
|
||||
public static string Between(string input, string open, string close)
|
||||
{
|
||||
var openIndex = input.IndexOf(open) + open.Length;
|
||||
var closeIndex = input.LastIndexOf(close);
|
||||
|
||||
return input.Substring(openIndex, closeIndex - openIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-58
@@ -18,6 +18,12 @@
|
||||
task.Wait();
|
||||
}
|
||||
|
||||
public static string FormatDuration(TimeSpan? d)
|
||||
{
|
||||
if (d == null) return "[NULL]";
|
||||
return FormatDuration(d.Value);
|
||||
}
|
||||
|
||||
public static string FormatDuration(TimeSpan d)
|
||||
{
|
||||
var result = "";
|
||||
@@ -57,100 +63,70 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void WaitUntil(Func<bool> predicate)
|
||||
public static void WaitUntil(Func<bool> predicate, string msg)
|
||||
{
|
||||
WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1));
|
||||
WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1), msg);
|
||||
}
|
||||
|
||||
public static void WaitUntil(Func<bool> predicate, TimeSpan timeout, TimeSpan retryDelay)
|
||||
public static void WaitUntil(Func<bool> predicate, TimeSpan timeout, TimeSpan retryDelay, string msg)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
var tries = 1;
|
||||
var state = predicate();
|
||||
while (!state)
|
||||
{
|
||||
if (DateTime.UtcNow - start > timeout)
|
||||
var duration = DateTime.UtcNow - start;
|
||||
if (duration > timeout)
|
||||
{
|
||||
throw new TimeoutException("Operation timed out.");
|
||||
throw new TimeoutException($"Operation timed out after {tries} tries over (total) {FormatDuration(duration)}. '{msg}'");
|
||||
}
|
||||
|
||||
Sleep(retryDelay);
|
||||
state = predicate();
|
||||
tries++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Retry(Action action, string description)
|
||||
{
|
||||
Retry(action, 1, description);
|
||||
Retry(action, TimeSpan.FromSeconds(30), description);
|
||||
}
|
||||
|
||||
public static T Retry<T>(Func<T> action, string description)
|
||||
{
|
||||
return Retry(action, 1, description);
|
||||
return Retry(action, TimeSpan.FromSeconds(30), description);
|
||||
}
|
||||
|
||||
public static void Retry(Action action, int maxRetries, string description)
|
||||
public static void Retry(Action action, TimeSpan maxTimeout, string description)
|
||||
{
|
||||
Retry(action, maxRetries, TimeSpan.FromSeconds(5), description);
|
||||
Retry(action, maxTimeout, TimeSpan.FromSeconds(5), description);
|
||||
}
|
||||
|
||||
public static T Retry<T>(Func<T> action, int maxRetries, string description)
|
||||
public static T Retry<T>(Func<T> action, TimeSpan maxTimeout, string description)
|
||||
{
|
||||
return Retry(action, maxRetries, TimeSpan.FromSeconds(5), description);
|
||||
return Retry(action, maxTimeout, TimeSpan.FromSeconds(5), description);
|
||||
}
|
||||
|
||||
public static void Retry(Action action, int maxRetries, TimeSpan retryTime, string description)
|
||||
public static void Retry(Action action, TimeSpan maxTimeout, TimeSpan retryTime, string description)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
var retries = 0;
|
||||
var exceptions = new List<Exception>();
|
||||
while (true)
|
||||
{
|
||||
if (retries > maxRetries)
|
||||
{
|
||||
var duration = DateTime.UtcNow - start;
|
||||
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
retries++;
|
||||
}
|
||||
|
||||
Sleep(retryTime);
|
||||
}
|
||||
Retry(action, maxTimeout, retryTime, description, f => { });
|
||||
}
|
||||
|
||||
public static T Retry<T>(Func<T> action, int maxRetries, TimeSpan retryTime, string description)
|
||||
public static T Retry<T>(Func<T> action, TimeSpan maxTimeout, TimeSpan retryTime, string description)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
var retries = 0;
|
||||
var exceptions = new List<Exception>();
|
||||
while (true)
|
||||
{
|
||||
if (retries > maxRetries)
|
||||
{
|
||||
var duration = DateTime.UtcNow - start;
|
||||
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
|
||||
}
|
||||
return Retry(action, maxTimeout, retryTime, description, f => { });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
retries++;
|
||||
}
|
||||
public static void Retry(Action action, TimeSpan maxTimeout, TimeSpan retryTime, string description, Action<Failure> onFail)
|
||||
{
|
||||
var r = new Retry(description, maxTimeout, retryTime, onFail);
|
||||
r.Run(action);
|
||||
}
|
||||
|
||||
Sleep(retryTime);
|
||||
}
|
||||
public static T Retry<T>(Func<T> action, TimeSpan maxTimeout, TimeSpan retryTime, string description, Action<Failure> onFail)
|
||||
{
|
||||
var r = new Retry(description, maxTimeout, retryTime, onFail);
|
||||
return r.Run(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using Utils;
|
||||
|
||||
namespace CodexContractsPlugin.ChainMonitor
|
||||
{
|
||||
public class ChainEvents
|
||||
{
|
||||
private ChainEvents(
|
||||
BlockInterval blockInterval,
|
||||
Request[] requests,
|
||||
RequestFulfilledEventDTO[] fulfilled,
|
||||
RequestCancelledEventDTO[] cancelled,
|
||||
RequestFailedEventDTO[] failed,
|
||||
SlotFilledEventDTO[] slotFilled,
|
||||
SlotFreedEventDTO[] slotFreed
|
||||
)
|
||||
{
|
||||
BlockInterval = blockInterval;
|
||||
Requests = requests;
|
||||
Fulfilled = fulfilled;
|
||||
Cancelled = cancelled;
|
||||
Failed = failed;
|
||||
SlotFilled = slotFilled;
|
||||
SlotFreed = slotFreed;
|
||||
}
|
||||
|
||||
public BlockInterval BlockInterval { get; }
|
||||
public Request[] Requests { get; }
|
||||
public RequestFulfilledEventDTO[] Fulfilled { get; }
|
||||
public RequestCancelledEventDTO[] Cancelled { get; }
|
||||
public RequestFailedEventDTO[] Failed { get; }
|
||||
public SlotFilledEventDTO[] SlotFilled { get; }
|
||||
public SlotFreedEventDTO[] SlotFreed { get; }
|
||||
|
||||
public IHasBlock[] All
|
||||
{
|
||||
get
|
||||
{
|
||||
var all = new List<IHasBlock>();
|
||||
all.AddRange(Requests);
|
||||
all.AddRange(Fulfilled);
|
||||
all.AddRange(Cancelled);
|
||||
all.AddRange(Failed);
|
||||
all.AddRange(SlotFilled);
|
||||
all.AddRange(SlotFreed);
|
||||
return all.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static ChainEvents FromBlockInterval(ICodexContracts contracts, BlockInterval blockInterval)
|
||||
{
|
||||
return FromContractEvents(contracts.GetEvents(blockInterval));
|
||||
}
|
||||
|
||||
public static ChainEvents FromTimeRange(ICodexContracts contracts, TimeRange timeRange)
|
||||
{
|
||||
return FromContractEvents(contracts.GetEvents(timeRange));
|
||||
}
|
||||
|
||||
public static ChainEvents FromContractEvents(ICodexContractsEvents events)
|
||||
{
|
||||
return new ChainEvents(
|
||||
events.BlockInterval,
|
||||
events.GetStorageRequests(),
|
||||
events.GetRequestFulfilledEvents(),
|
||||
events.GetRequestCancelledEvents(),
|
||||
events.GetRequestFailedEvents(),
|
||||
events.GetSlotFilledEvents(),
|
||||
events.GetSlotFreedEvents()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
using NethereumWorkflow.BlockUtils;
|
||||
using System.Numerics;
|
||||
using Utils;
|
||||
|
||||
namespace CodexContractsPlugin.ChainMonitor
|
||||
{
|
||||
public interface IChainStateChangeHandler
|
||||
{
|
||||
void OnNewRequest(RequestEvent requestEvent);
|
||||
void OnRequestFinished(RequestEvent requestEvent);
|
||||
void OnRequestFulfilled(RequestEvent requestEvent);
|
||||
void OnRequestCancelled(RequestEvent requestEvent);
|
||||
void OnRequestFailed(RequestEvent requestEvent);
|
||||
void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex);
|
||||
void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex);
|
||||
}
|
||||
|
||||
public class RequestEvent
|
||||
{
|
||||
public RequestEvent(BlockTimeEntry block, IChainStateRequest request)
|
||||
{
|
||||
Block = block;
|
||||
Request = request;
|
||||
}
|
||||
|
||||
public BlockTimeEntry Block { get; }
|
||||
public IChainStateRequest Request { get; }
|
||||
}
|
||||
|
||||
public class ChainState
|
||||
{
|
||||
private readonly List<ChainStateRequest> requests = new List<ChainStateRequest>();
|
||||
private readonly ILog log;
|
||||
private readonly ICodexContracts contracts;
|
||||
private readonly IChainStateChangeHandler handler;
|
||||
|
||||
public ChainState(ILog log, ICodexContracts contracts, IChainStateChangeHandler changeHandler, DateTime startUtc)
|
||||
{
|
||||
this.log = new LogPrefixer(log, "(ChainState) ");
|
||||
this.contracts = contracts;
|
||||
handler = changeHandler;
|
||||
TotalSpan = new TimeRange(startUtc, startUtc);
|
||||
}
|
||||
|
||||
public TimeRange TotalSpan { get; private set; }
|
||||
public IChainStateRequest[] Requests => requests.ToArray();
|
||||
|
||||
public void Update()
|
||||
{
|
||||
Update(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
public void Update(DateTime toUtc)
|
||||
{
|
||||
var span = new TimeRange(TotalSpan.To, toUtc);
|
||||
var events = ChainEvents.FromTimeRange(contracts, span);
|
||||
Apply(events);
|
||||
|
||||
TotalSpan = new TimeRange(TotalSpan.From, span.To);
|
||||
}
|
||||
|
||||
private void Apply(ChainEvents events)
|
||||
{
|
||||
if (events.BlockInterval.TimeRange.From < TotalSpan.From)
|
||||
throw new Exception("Attempt to update ChainState with set of events from before its current record.");
|
||||
|
||||
log.Log($"ChainState updating: {events.BlockInterval}");
|
||||
|
||||
// Run through each block and apply the events to the state in order.
|
||||
var span = events.BlockInterval.TimeRange.Duration;
|
||||
var numBlocks = events.BlockInterval.NumberOfBlocks;
|
||||
var spanPerBlock = span / numBlocks;
|
||||
|
||||
var eventUtc = events.BlockInterval.TimeRange.From;
|
||||
for (var b = events.BlockInterval.From; b <= events.BlockInterval.To; b++)
|
||||
{
|
||||
var blockEvents = events.All.Where(e => e.Block.BlockNumber == b).ToArray();
|
||||
ApplyEvents(b, blockEvents, eventUtc);
|
||||
|
||||
eventUtc += spanPerBlock;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEvents(ulong blockNumber, IHasBlock[] blockEvents, DateTime eventsUtc)
|
||||
{
|
||||
foreach (var e in blockEvents)
|
||||
{
|
||||
dynamic d = e;
|
||||
ApplyEvent(d);
|
||||
}
|
||||
|
||||
ApplyTimeImplicitEvents(blockNumber, eventsUtc);
|
||||
}
|
||||
|
||||
private void ApplyEvent(Request request)
|
||||
{
|
||||
if (requests.Any(r => Equal(r.Request.RequestId, request.RequestId)))
|
||||
throw new Exception("Received NewRequest event for id that already exists.");
|
||||
|
||||
var newRequest = new ChainStateRequest(log, request, RequestState.New);
|
||||
requests.Add(newRequest);
|
||||
|
||||
handler.OnNewRequest(new RequestEvent(request.Block, newRequest));
|
||||
}
|
||||
|
||||
private void ApplyEvent(RequestFulfilledEventDTO @event)
|
||||
{
|
||||
var r = FindRequest(@event.RequestId);
|
||||
if (r == null) return;
|
||||
r.UpdateState(@event.Block.BlockNumber, RequestState.Started);
|
||||
handler.OnRequestFulfilled(new RequestEvent(@event.Block, r));
|
||||
}
|
||||
|
||||
private void ApplyEvent(RequestCancelledEventDTO @event)
|
||||
{
|
||||
var r = FindRequest(@event.RequestId);
|
||||
if (r == null) return;
|
||||
r.UpdateState(@event.Block.BlockNumber, RequestState.Cancelled);
|
||||
handler.OnRequestCancelled(new RequestEvent(@event.Block, r));
|
||||
}
|
||||
|
||||
private void ApplyEvent(RequestFailedEventDTO @event)
|
||||
{
|
||||
var r = FindRequest(@event.RequestId);
|
||||
if (r == null) return;
|
||||
r.UpdateState(@event.Block.BlockNumber, RequestState.Failed);
|
||||
handler.OnRequestFailed(new RequestEvent(@event.Block, r));
|
||||
}
|
||||
|
||||
private void ApplyEvent(SlotFilledEventDTO @event)
|
||||
{
|
||||
var r = FindRequest(@event.RequestId);
|
||||
if (r == null) return;
|
||||
r.Hosts.Add(@event.Host, (int)@event.SlotIndex);
|
||||
r.Log($"[{@event.Block.BlockNumber}] SlotFilled (host:'{@event.Host}', slotIndex:{@event.SlotIndex})");
|
||||
handler.OnSlotFilled(new RequestEvent(@event.Block, r), @event.Host, @event.SlotIndex);
|
||||
}
|
||||
|
||||
private void ApplyEvent(SlotFreedEventDTO @event)
|
||||
{
|
||||
var r = FindRequest(@event.RequestId);
|
||||
if (r == null) return;
|
||||
r.Hosts.RemoveHost((int)@event.SlotIndex);
|
||||
r.Log($"[{@event.Block.BlockNumber}] SlotFreed (slotIndex:{@event.SlotIndex})");
|
||||
handler.OnSlotFreed(new RequestEvent(@event.Block, r), @event.SlotIndex);
|
||||
}
|
||||
|
||||
private void ApplyTimeImplicitEvents(ulong blockNumber, DateTime eventsUtc)
|
||||
{
|
||||
foreach (var r in requests)
|
||||
{
|
||||
if (r.State == RequestState.Started
|
||||
&& r.FinishedUtc < eventsUtc)
|
||||
{
|
||||
r.UpdateState(blockNumber, RequestState.Finished);
|
||||
handler.OnRequestFinished(new RequestEvent(new BlockTimeEntry(blockNumber, eventsUtc), r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ChainStateRequest? FindRequest(byte[] requestId)
|
||||
{
|
||||
var r = requests.SingleOrDefault(r => Equal(r.Request.RequestId, requestId));
|
||||
if (r == null) log.Log("Unable to find request by ID!");
|
||||
return r;
|
||||
}
|
||||
|
||||
private bool Equal(byte[] a, byte[] b)
|
||||
{
|
||||
return a.SequenceEqual(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using GethPlugin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CodexContractsPlugin.ChainMonitor
|
||||
{
|
||||
public class ChainStateChangeHandlerMux : IChainStateChangeHandler
|
||||
{
|
||||
public ChainStateChangeHandlerMux(params IChainStateChangeHandler[] handlers)
|
||||
{
|
||||
Handlers = handlers.ToList();
|
||||
}
|
||||
|
||||
public List<IChainStateChangeHandler> Handlers { get; } = new List<IChainStateChangeHandler>();
|
||||
|
||||
public void OnNewRequest(RequestEvent requestEvent)
|
||||
{
|
||||
foreach (var handler in Handlers) handler.OnNewRequest(requestEvent);
|
||||
}
|
||||
|
||||
public void OnRequestCancelled(RequestEvent requestEvent)
|
||||
{
|
||||
foreach (var handler in Handlers) handler.OnRequestCancelled(requestEvent);
|
||||
}
|
||||
|
||||
public void OnRequestFailed(RequestEvent requestEvent)
|
||||
{
|
||||
foreach (var handler in Handlers) handler.OnRequestFailed(requestEvent);
|
||||
}
|
||||
|
||||
public void OnRequestFinished(RequestEvent requestEvent)
|
||||
{
|
||||
foreach (var handler in Handlers) handler.OnRequestFinished(requestEvent);
|
||||
}
|
||||
|
||||
public void OnRequestFulfilled(RequestEvent requestEvent)
|
||||
{
|
||||
foreach (var handler in Handlers) handler.OnRequestFulfilled(requestEvent);
|
||||
}
|
||||
|
||||
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
|
||||
{
|
||||
foreach (var handler in Handlers) handler.OnSlotFilled(requestEvent, host, slotIndex);
|
||||
}
|
||||
|
||||
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
|
||||
{
|
||||
foreach (var handler in Handlers) handler.OnSlotFreed(requestEvent, slotIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
|
||||
namespace CodexContractsPlugin.ChainMonitor
|
||||
{
|
||||
public interface IChainStateRequest
|
||||
{
|
||||
Request Request { get; }
|
||||
RequestState State { get; }
|
||||
DateTime ExpiryUtc { get; }
|
||||
DateTime FinishedUtc { get; }
|
||||
EthAddress Client { get; }
|
||||
RequestHosts Hosts { get; }
|
||||
}
|
||||
|
||||
public class ChainStateRequest : IChainStateRequest
|
||||
{
|
||||
private readonly ILog log;
|
||||
|
||||
public ChainStateRequest(ILog log, Request request, RequestState state)
|
||||
{
|
||||
this.log = log;
|
||||
Request = request;
|
||||
State = state;
|
||||
|
||||
ExpiryUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Expiry);
|
||||
FinishedUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Ask.Duration);
|
||||
|
||||
Log($"[{request.Block.BlockNumber}] Created as {State}.");
|
||||
|
||||
Client = new EthAddress(request.Client);
|
||||
Hosts = new RequestHosts();
|
||||
}
|
||||
|
||||
public Request Request { get; }
|
||||
public RequestState State { get; private set; }
|
||||
public DateTime ExpiryUtc { get; }
|
||||
public DateTime FinishedUtc { get; }
|
||||
public EthAddress Client { get; }
|
||||
public RequestHosts Hosts { get; }
|
||||
|
||||
public void UpdateState(ulong blockNumber, RequestState newState)
|
||||
{
|
||||
Log($"[{blockNumber}] Transit: {State} -> {newState}");
|
||||
State = newState;
|
||||
}
|
||||
|
||||
public void Log(string msg)
|
||||
{
|
||||
log.Log($"Request '{Request.Id}': {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
public class RequestHosts
|
||||
{
|
||||
private readonly Dictionary<int, EthAddress> hosts = new Dictionary<int, EthAddress>();
|
||||
|
||||
public void Add(EthAddress host, int index)
|
||||
{
|
||||
hosts.Add(index, host);
|
||||
}
|
||||
|
||||
public void RemoveHost(int index)
|
||||
{
|
||||
hosts.Remove(index);
|
||||
}
|
||||
|
||||
public EthAddress? GetHost(int index)
|
||||
{
|
||||
if (!hosts.ContainsKey(index)) return null;
|
||||
return hosts[index];
|
||||
}
|
||||
|
||||
public EthAddress[] GetHosts()
|
||||
{
|
||||
return hosts.Values.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using GethPlugin;
|
||||
using System.Numerics;
|
||||
|
||||
namespace CodexContractsPlugin.ChainMonitor
|
||||
{
|
||||
public class DoNothingChainEventHandler : IChainStateChangeHandler
|
||||
{
|
||||
public void OnNewRequest(RequestEvent requestEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnRequestCancelled(RequestEvent requestEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnRequestFailed(RequestEvent requestEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnRequestFinished(RequestEvent requestEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnRequestFulfilled(RequestEvent requestEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,10 @@
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
using Nethereum.ABI;
|
||||
using Nethereum.Hex.HexTypes;
|
||||
using Nethereum.Util;
|
||||
using NethereumWorkflow;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Utils;
|
||||
|
||||
namespace CodexContractsPlugin
|
||||
@@ -19,15 +20,13 @@ namespace CodexContractsPlugin
|
||||
TestToken GetTestTokenBalance(IHasEthAddress owner);
|
||||
TestToken GetTestTokenBalance(EthAddress ethAddress);
|
||||
|
||||
Request[] GetStorageRequests(TimeRange timeRange);
|
||||
ICodexContractsEvents GetEvents(TimeRange timeRange);
|
||||
ICodexContractsEvents GetEvents(BlockInterval blockInterval);
|
||||
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
|
||||
RequestState GetRequestState(Request request);
|
||||
RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange);
|
||||
RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange);
|
||||
SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange);
|
||||
SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange);
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum RequestState
|
||||
{
|
||||
New,
|
||||
@@ -63,7 +62,7 @@ namespace CodexContractsPlugin
|
||||
|
||||
public string MintTestTokens(EthAddress ethAddress, TestToken testTokens)
|
||||
{
|
||||
return StartInteraction().MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress);
|
||||
return StartInteraction().MintTestTokens(ethAddress, testTokens.TstWei, Deployment.TokenAddress);
|
||||
}
|
||||
|
||||
public TestToken GetTestTokenBalance(IHasEthAddress owner)
|
||||
@@ -74,68 +73,17 @@ namespace CodexContractsPlugin
|
||||
public TestToken GetTestTokenBalance(EthAddress ethAddress)
|
||||
{
|
||||
var balance = StartInteraction().GetBalance(Deployment.TokenAddress, ethAddress.Address);
|
||||
return balance.TestTokens();
|
||||
return balance.TstWei();
|
||||
}
|
||||
|
||||
public Request[] GetStorageRequests(TimeRange timeRange)
|
||||
public ICodexContractsEvents GetEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
var i = StartInteraction();
|
||||
return events
|
||||
.Select(e =>
|
||||
{
|
||||
var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId);
|
||||
var result = requestEvent.ReturnValue1;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
result.RequestId = e.Event.RequestId;
|
||||
return result;
|
||||
})
|
||||
.ToArray();
|
||||
return GetEvents(gethNode.ConvertTimeRangeToBlockRange(timeRange));
|
||||
}
|
||||
|
||||
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange)
|
||||
public ICodexContractsEvents GetEvents(BlockInterval blockInterval)
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestCancelledEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<SlotFilledEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<SlotFreedEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
return result;
|
||||
}).ToArray();
|
||||
return new CodexContractsEvents(log, gethNode, Deployment, blockInterval);
|
||||
}
|
||||
|
||||
public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex)
|
||||
@@ -166,12 +114,6 @@ namespace CodexContractsPlugin
|
||||
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func);
|
||||
}
|
||||
|
||||
private EthAddress GetEthAddressFromTransaction(string transactionHash)
|
||||
{
|
||||
var transaction = gethNode.GetTransaction(transactionHash);
|
||||
return new EthAddress(transaction.From);
|
||||
}
|
||||
|
||||
private ContractInteractions StartInteraction()
|
||||
{
|
||||
return new ContractInteractions(log, gethNode);
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
using Nethereum.Hex.HexTypes;
|
||||
using NethereumWorkflow.BlockUtils;
|
||||
using Utils;
|
||||
|
||||
namespace CodexContractsPlugin
|
||||
{
|
||||
public interface ICodexContractsEvents
|
||||
{
|
||||
BlockInterval BlockInterval { get; }
|
||||
Request[] GetStorageRequests();
|
||||
RequestFulfilledEventDTO[] GetRequestFulfilledEvents();
|
||||
RequestCancelledEventDTO[] GetRequestCancelledEvents();
|
||||
RequestFailedEventDTO[] GetRequestFailedEvents();
|
||||
SlotFilledEventDTO[] GetSlotFilledEvents();
|
||||
SlotFreedEventDTO[] GetSlotFreedEvents();
|
||||
}
|
||||
|
||||
public class CodexContractsEvents : ICodexContractsEvents
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly IGethNode gethNode;
|
||||
private readonly CodexContractsDeployment deployment;
|
||||
|
||||
public CodexContractsEvents(ILog log, IGethNode gethNode, CodexContractsDeployment deployment, BlockInterval blockInterval)
|
||||
{
|
||||
this.log = log;
|
||||
this.gethNode = gethNode;
|
||||
this.deployment = deployment;
|
||||
BlockInterval = blockInterval;
|
||||
}
|
||||
|
||||
public BlockInterval BlockInterval { get; }
|
||||
|
||||
public Request[] GetStorageRequests()
|
||||
{
|
||||
var events = gethNode.GetEvents<StorageRequestedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
|
||||
var i = new ContractInteractions(log, gethNode);
|
||||
return events
|
||||
.Select(e =>
|
||||
{
|
||||
var requestEvent = i.GetRequest(deployment.MarketplaceAddress, e.Event.RequestId);
|
||||
var result = requestEvent.ReturnValue1;
|
||||
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
|
||||
result.RequestId = e.Event.RequestId;
|
||||
return result;
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents()
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public RequestCancelledEventDTO[] GetRequestCancelledEvents()
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestCancelledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public RequestFailedEventDTO[] GetRequestFailedEvents()
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestFailedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public SlotFilledEventDTO[] GetSlotFilledEvents()
|
||||
{
|
||||
var events = gethNode.GetEvents<SlotFilledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
|
||||
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public SlotFreedEventDTO[] GetSlotFreedEvents()
|
||||
{
|
||||
var events = gethNode.GetEvents<SlotFreedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private BlockTimeEntry GetBlock(ulong number)
|
||||
{
|
||||
return gethNode.GetBlockForNumber(number);
|
||||
}
|
||||
|
||||
private EthAddress GetEthAddressFromTransaction(string transactionHash)
|
||||
{
|
||||
var transaction = gethNode.GetTransaction(transactionHash);
|
||||
return new EthAddress(transaction.From);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ namespace CodexContractsPlugin
|
||||
var startupConfig = CreateStartupConfig(gethNode);
|
||||
startupConfig.NameOverride = "codex-contracts";
|
||||
|
||||
var containers = workflow.Start(1, new CodexContractsContainerRecipe(), startupConfig);
|
||||
var containers = workflow.Start(1, new CodexContractsContainerRecipe(), startupConfig).WaitForOnline();
|
||||
if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Codex contracts container to be created. Test infra failure.");
|
||||
var container = containers.Containers[0];
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace CodexContractsPlugin
|
||||
var logHandler = new ContractsReadyLogHandler(tools.GetLog());
|
||||
workflow.DownloadContainerLog(container, logHandler, 100);
|
||||
return logHandler.Found;
|
||||
});
|
||||
}, nameof(DeployContract));
|
||||
Log("Contracts deployed. Extracting addresses...");
|
||||
|
||||
var extractor = new ContractsContainerInfoExtractor(tools.GetLog(), workflow, container);
|
||||
@@ -71,7 +71,7 @@ namespace CodexContractsPlugin
|
||||
|
||||
Log("Extract completed. Checking sync...");
|
||||
|
||||
Time.WaitUntil(() => interaction.IsSynced(marketplaceAddress, abi));
|
||||
Time.WaitUntil(() => interaction.IsSynced(marketplaceAddress, abi), nameof(DeployContract));
|
||||
|
||||
Log("Synced. Codex SmartContracts deployed.");
|
||||
|
||||
@@ -83,9 +83,9 @@ namespace CodexContractsPlugin
|
||||
tools.GetLog().Log(msg);
|
||||
}
|
||||
|
||||
private void WaitUntil(Func<bool> predicate)
|
||||
private void WaitUntil(Func<bool> predicate, string msg)
|
||||
{
|
||||
Time.WaitUntil(predicate, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(2));
|
||||
Time.WaitUntil(predicate, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(2), msg);
|
||||
}
|
||||
|
||||
private StartupConfig CreateStartupConfig(IGethNode gethNode)
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace CodexContractsPlugin
|
||||
}
|
||||
}
|
||||
|
||||
public string MintTestTokens(EthAddress address, decimal amount, string tokenAddress)
|
||||
public string MintTestTokens(EthAddress address, BigInteger amount, string tokenAddress)
|
||||
{
|
||||
log.Debug($"{amount} -> {address} (token: {tokenAddress})");
|
||||
return MintTokens(address.Address, amount, tokenAddress);
|
||||
@@ -85,7 +85,7 @@ namespace CodexContractsPlugin
|
||||
}
|
||||
}
|
||||
|
||||
private string MintTokens(string account, decimal amount, string tokenAddress)
|
||||
private string MintTokens(string account, BigInteger amount, string tokenAddress)
|
||||
{
|
||||
log.Debug($"({tokenAddress}) {amount} --> {account}");
|
||||
if (string.IsNullOrEmpty(account)) throw new ArgumentException("Invalid arguments for MintTestTokens");
|
||||
@@ -93,7 +93,7 @@ namespace CodexContractsPlugin
|
||||
var function = new MintTokensFunction
|
||||
{
|
||||
Holder = account,
|
||||
Amount = amount.ToBig()
|
||||
Amount = amount
|
||||
};
|
||||
|
||||
return gethNode.SendTransaction(tokenAddress, function);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using KubernetesWorkflow;
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
@@ -53,7 +54,18 @@ namespace CodexContractsPlugin
|
||||
|
||||
var artifact = JObject.Parse(json);
|
||||
var abi = artifact["abi"];
|
||||
return abi!.ToString(Formatting.None);
|
||||
var byteCode = artifact["bytecode"];
|
||||
var abiResult = abi!.ToString(Formatting.None);
|
||||
var byteCodeResult = byteCode!.ToString(Formatting.None);
|
||||
|
||||
if (byteCodeResult
|
||||
.ToLowerInvariant()
|
||||
.Replace("\"", "") != MarketplaceDeploymentBase.BYTECODE.ToLowerInvariant())
|
||||
{
|
||||
throw new Exception("BYTECODE in CodexContractsPlugin does not match BYTECODE deployed by container. Update Marketplace.cs generated code?");
|
||||
}
|
||||
|
||||
return abiResult;
|
||||
}
|
||||
|
||||
private static string Retry(Func<string> fetch)
|
||||
|
||||
@@ -1,35 +1,62 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
using GethPlugin;
|
||||
using NethereumWorkflow.BlockUtils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CodexContractsPlugin.Marketplace
|
||||
{
|
||||
public partial class Request : RequestBase
|
||||
public interface IHasBlock
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
BlockTimeEntry Block { get; set; }
|
||||
}
|
||||
|
||||
public partial class Request : RequestBase, IHasBlock
|
||||
{
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
public byte[] RequestId { get; set; }
|
||||
|
||||
public EthAddress ClientAddress { get { return new EthAddress(Client); } }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Id
|
||||
{
|
||||
get
|
||||
{
|
||||
return BitConverter.ToString(RequestId).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class RequestFulfilledEventDTO
|
||||
public partial class RequestFulfilledEventDTO : IHasBlock
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestCancelledEventDTO
|
||||
public partial class RequestCancelledEventDTO : IHasBlock
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFilledEventDTO
|
||||
public partial class RequestFailedEventDTO : IHasBlock
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFilledEventDTO : IHasBlock
|
||||
{
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
public EthAddress Host { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFreedEventDTO
|
||||
public partial class SlotFreedEventDTO : IHasBlock
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1,14 @@
|
||||
This code was generated using the Nethereum code generator, here: http://playground.nethereum.com
|
||||
|
||||
1. Go to site -> Abi Code Gen.
|
||||
1. Contract name = "Marketplace".
|
||||
1. In container, get "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json".
|
||||
1. Save only ABI section as new JSON. (top-level is a json array.)
|
||||
1. From original JSON get byte code.
|
||||
1. Put ABI JSON and byte code into site.
|
||||
1. Generate.
|
||||
1. From site generated code, copy `public partial class MarketplaceDeployment` and everything after it. (be considerate of namespace brackets!)
|
||||
1. In Marketplace/Marketplace.cs, replace content of 'namespace CodexContractsPlugin.Marketplace'.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,45 +1,102 @@
|
||||
namespace CodexContractsPlugin
|
||||
using System.Numerics;
|
||||
|
||||
namespace CodexContractsPlugin
|
||||
{
|
||||
public class TestToken : IComparable<TestToken>
|
||||
{
|
||||
public TestToken(decimal amount)
|
||||
public static BigInteger WeiFactor = new BigInteger(1000000000000000000);
|
||||
|
||||
public TestToken(BigInteger tstWei)
|
||||
{
|
||||
Amount = amount;
|
||||
TstWei = tstWei;
|
||||
Tst = tstWei / WeiFactor;
|
||||
}
|
||||
|
||||
public decimal Amount { get; }
|
||||
public BigInteger TstWei { get; }
|
||||
public BigInteger Tst { get; }
|
||||
|
||||
public int CompareTo(TestToken? other)
|
||||
{
|
||||
return Amount.CompareTo(other!.Amount);
|
||||
return TstWei.CompareTo(other!.TstWei);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is TestToken token && Amount == token.Amount;
|
||||
return obj is TestToken token && TstWei == token.TstWei;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Amount);
|
||||
return HashCode.Combine(TstWei);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Amount} TestTokens";
|
||||
var weiOnly = TstWei % WeiFactor;
|
||||
|
||||
var tokens = new List<string>();
|
||||
if (Tst > 0) tokens.Add($"{Tst} TST");
|
||||
if (weiOnly > 0) tokens.Add($"{weiOnly} TSTWEI");
|
||||
|
||||
return string.Join(" + ", tokens);
|
||||
}
|
||||
|
||||
public static TestToken operator +(TestToken a, TestToken b)
|
||||
{
|
||||
return new TestToken(a.TstWei + b.TstWei);
|
||||
}
|
||||
|
||||
public static bool operator <(TestToken a, TestToken b)
|
||||
{
|
||||
return a.TstWei < b.TstWei;
|
||||
}
|
||||
|
||||
public static bool operator >(TestToken a, TestToken b)
|
||||
{
|
||||
return a.TstWei > b.TstWei;
|
||||
}
|
||||
|
||||
public static bool operator ==(TestToken a, TestToken b)
|
||||
{
|
||||
return a.TstWei == b.TstWei;
|
||||
}
|
||||
|
||||
public static bool operator !=(TestToken a, TestToken b)
|
||||
{
|
||||
return a.TstWei != b.TstWei;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TokensIntExtensions
|
||||
public static class TestTokensExtensions
|
||||
{
|
||||
public static TestToken TestTokens(this int i)
|
||||
public static TestToken TstWei(this int i)
|
||||
{
|
||||
return TestTokens(Convert.ToDecimal(i));
|
||||
return TstWei(Convert.ToDecimal(i));
|
||||
}
|
||||
|
||||
public static TestToken TestTokens(this decimal i)
|
||||
public static TestToken TstWei(this decimal i)
|
||||
{
|
||||
return new TestToken(new BigInteger(i));
|
||||
}
|
||||
|
||||
public static TestToken TstWei(this BigInteger i)
|
||||
{
|
||||
return new TestToken(i);
|
||||
}
|
||||
|
||||
public static TestToken Tst(this int i)
|
||||
{
|
||||
return Tst(Convert.ToDecimal(i));
|
||||
}
|
||||
|
||||
public static TestToken Tst(this decimal i)
|
||||
{
|
||||
return new TestToken(new BigInteger(i) * TestToken.WeiFactor);
|
||||
}
|
||||
|
||||
public static TestToken Tst(this BigInteger i)
|
||||
{
|
||||
return new TestToken(i * TestToken.WeiFactor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Utils;
|
||||
|
||||
namespace CodexDiscordBotPlugin
|
||||
{
|
||||
public class CodexDiscordBotPlugin : IProjectPlugin, IHasLogPrefix, IHasMetadata
|
||||
{
|
||||
private const string ExpectedStartupMessage = "Debug option is set. Discord connection disabled!";
|
||||
private readonly IPluginTools tools;
|
||||
|
||||
public CodexDiscordBotPlugin(IPluginTools tools)
|
||||
@@ -29,18 +31,76 @@ namespace CodexDiscordBotPlugin
|
||||
{
|
||||
}
|
||||
|
||||
public RunningContainers Deploy(DiscordBotStartupConfig config)
|
||||
public RunningPod Deploy(DiscordBotStartupConfig config)
|
||||
{
|
||||
var workflow = tools.CreateWorkflow();
|
||||
return StartContainer(workflow, config);
|
||||
}
|
||||
|
||||
private RunningContainers StartContainer(IStartupWorkflow workflow, DiscordBotStartupConfig config)
|
||||
public RunningPod DeployRewarder(RewarderBotStartupConfig config)
|
||||
{
|
||||
var workflow = tools.CreateWorkflow();
|
||||
return StartRewarderContainer(workflow, config);
|
||||
}
|
||||
|
||||
private RunningPod StartContainer(IStartupWorkflow workflow, DiscordBotStartupConfig config)
|
||||
{
|
||||
var startupConfig = new StartupConfig();
|
||||
startupConfig.NameOverride = config.Name;
|
||||
startupConfig.Add(config);
|
||||
return workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig);
|
||||
var pod = workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline();
|
||||
WaitForStartupMessage(workflow, pod);
|
||||
workflow.CreateCrashWatcher(pod.Containers.Single()).Start();
|
||||
return pod;
|
||||
}
|
||||
|
||||
private RunningPod StartRewarderContainer(IStartupWorkflow workflow, RewarderBotStartupConfig config)
|
||||
{
|
||||
var startupConfig = new StartupConfig();
|
||||
startupConfig.NameOverride = config.Name;
|
||||
startupConfig.Add(config);
|
||||
var pod = workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline();
|
||||
workflow.CreateCrashWatcher(pod.Containers.Single()).Start();
|
||||
return pod;
|
||||
}
|
||||
|
||||
private void WaitForStartupMessage(IStartupWorkflow workflow, RunningPod pod)
|
||||
{
|
||||
var finder = new LogLineFinder(ExpectedStartupMessage, workflow);
|
||||
Time.WaitUntil(() =>
|
||||
{
|
||||
finder.FindLine(pod);
|
||||
return finder.Found;
|
||||
}, nameof(WaitForStartupMessage));
|
||||
}
|
||||
|
||||
public class LogLineFinder : LogHandler
|
||||
{
|
||||
private readonly string message;
|
||||
private readonly IStartupWorkflow workflow;
|
||||
|
||||
public LogLineFinder(string message, IStartupWorkflow workflow)
|
||||
{
|
||||
this.message = message;
|
||||
this.workflow = workflow;
|
||||
}
|
||||
|
||||
public void FindLine(RunningPod pod)
|
||||
{
|
||||
Found = false;
|
||||
foreach (var c in pod.Containers)
|
||||
{
|
||||
workflow.DownloadContainerLog(c, this);
|
||||
if (Found) return;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Found { get; private set; }
|
||||
|
||||
protected override void ProcessLine(string line)
|
||||
{
|
||||
if (!Found && line.Contains(message)) Found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,16 @@ namespace CodexDiscordBotPlugin
|
||||
{
|
||||
public static class CoreInterfaceExtensions
|
||||
{
|
||||
public static RunningContainers DeployCodexDiscordBot(this CoreInterface ci, DiscordBotStartupConfig config)
|
||||
public static RunningPod DeployCodexDiscordBot(this CoreInterface ci, DiscordBotStartupConfig config)
|
||||
{
|
||||
return Plugin(ci).Deploy(config);
|
||||
}
|
||||
|
||||
public static RunningPod DeployRewarderBot(this CoreInterface ci, RewarderBotStartupConfig config)
|
||||
{
|
||||
return Plugin(ci).DeployRewarder(config);
|
||||
}
|
||||
|
||||
private static CodexDiscordBotPlugin Plugin(CoreInterface ci)
|
||||
{
|
||||
return ci.GetPlugin<CodexDiscordBotPlugin>();
|
||||
|
||||
@@ -7,7 +7,9 @@ namespace CodexDiscordBotPlugin
|
||||
public class DiscordBotContainerRecipe : ContainerRecipeFactory
|
||||
{
|
||||
public override string AppName => "discordbot-bibliotech";
|
||||
public override string Image => "thatbenbierens/codex-discordbot:initial";
|
||||
public override string Image => "codexstorage/codex-discordbot:sha-8033da1";
|
||||
|
||||
public static string RewardsPort = "bot_rewards_port";
|
||||
|
||||
protected override void Initialize(StartupConfig startupConfig)
|
||||
{
|
||||
@@ -19,6 +21,7 @@ namespace CodexDiscordBotPlugin
|
||||
AddEnvVar("SERVERNAME", config.ServerName);
|
||||
AddEnvVar("ADMINROLE", config.AdminRoleName);
|
||||
AddEnvVar("ADMINCHANNELNAME", config.AdminChannelName);
|
||||
AddEnvVar("REWARDSCHANNELNAME", config.RewardChannelName);
|
||||
AddEnvVar("KUBECONFIG", "/opt/kubeconfig.yaml");
|
||||
AddEnvVar("KUBENAMESPACE", config.KubeNamespace);
|
||||
|
||||
@@ -30,13 +33,15 @@ namespace CodexDiscordBotPlugin
|
||||
AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress);
|
||||
AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi);
|
||||
|
||||
AddEnvVar("NODISCORD", "1");
|
||||
|
||||
AddInternalPortAndVar("REWARDAPIPORT", RewardsPort);
|
||||
|
||||
if (!string.IsNullOrEmpty(config.DataPath))
|
||||
{
|
||||
AddEnvVar("DATAPATH", config.DataPath);
|
||||
AddVolume(config.DataPath, 1.GB());
|
||||
}
|
||||
|
||||
AddVolume(name: "kubeconfig", mountPath: "/opt/kubeconfig.yaml", subPath: "kubeconfig.yaml", secret: "discordbot-sa-kubeconfig");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
public class DiscordBotStartupConfig
|
||||
{
|
||||
public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName, string kubeNamespace, DiscordBotGethInfo gethInfo)
|
||||
public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName, string kubeNamespace, DiscordBotGethInfo gethInfo, string rewardChannelName)
|
||||
{
|
||||
Name = name;
|
||||
Token = token;
|
||||
@@ -11,6 +11,7 @@
|
||||
AdminChannelName = adminChannelName;
|
||||
KubeNamespace = kubeNamespace;
|
||||
GethInfo = gethInfo;
|
||||
RewardChannelName = rewardChannelName;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
@@ -18,11 +19,34 @@
|
||||
public string ServerName { get; }
|
||||
public string AdminRoleName { get; }
|
||||
public string AdminChannelName { get; }
|
||||
public string RewardChannelName { get; }
|
||||
public string KubeNamespace { get; }
|
||||
public DiscordBotGethInfo GethInfo { get; }
|
||||
public string? DataPath { get; set; }
|
||||
}
|
||||
|
||||
public class RewarderBotStartupConfig
|
||||
{
|
||||
public RewarderBotStartupConfig(string name, string discordBotHost, int discordBotPort, int intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath)
|
||||
{
|
||||
Name = name;
|
||||
DiscordBotHost = discordBotHost;
|
||||
DiscordBotPort = discordBotPort;
|
||||
IntervalMinutes = intervalMinutes;
|
||||
HistoryStartUtc = historyStartUtc;
|
||||
GethInfo = gethInfo;
|
||||
DataPath = dataPath;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string DiscordBotHost { get; }
|
||||
public int DiscordBotPort { get; }
|
||||
public int IntervalMinutes { get; }
|
||||
public DateTime HistoryStartUtc { get; }
|
||||
public DiscordBotGethInfo GethInfo { get; }
|
||||
public string? DataPath { get; set; }
|
||||
}
|
||||
|
||||
public class DiscordBotGethInfo
|
||||
{
|
||||
public DiscordBotGethInfo(string host, int port, string privKey, string marketplaceAddress, string tokenAddress, string abi)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using KubernetesWorkflow;
|
||||
using Utils;
|
||||
|
||||
namespace CodexDiscordBotPlugin
|
||||
{
|
||||
public class RewarderBotContainerRecipe : ContainerRecipeFactory
|
||||
{
|
||||
public override string AppName => "discordbot-rewarder";
|
||||
public override string Image => "codexstorage/codex-rewarderbot:sha-8033da1";
|
||||
|
||||
protected override void Initialize(StartupConfig startupConfig)
|
||||
{
|
||||
var config = startupConfig.Get<RewarderBotStartupConfig>();
|
||||
|
||||
SetSchedulingAffinity(notIn: "false");
|
||||
|
||||
AddEnvVar("DISCORDBOTHOST", config.DiscordBotHost);
|
||||
AddEnvVar("DISCORDBOTPORT", config.DiscordBotPort.ToString());
|
||||
AddEnvVar("INTERVALMINUTES", config.IntervalMinutes.ToString());
|
||||
var offset = new DateTimeOffset(config.HistoryStartUtc);
|
||||
AddEnvVar("CHECKHISTORY", offset.ToUnixTimeSeconds().ToString());
|
||||
|
||||
var gethInfo = config.GethInfo;
|
||||
AddEnvVar("GETH_HOST", gethInfo.Host);
|
||||
AddEnvVar("GETH_HTTP_PORT", gethInfo.Port.ToString());
|
||||
AddEnvVar("GETH_PRIVATE_KEY", gethInfo.PrivKey);
|
||||
AddEnvVar("CODEXCONTRACTS_MARKETPLACEADDRESS", gethInfo.MarketplaceAddress);
|
||||
AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress);
|
||||
AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi);
|
||||
|
||||
if (!string.IsNullOrEmpty(config.DataPath))
|
||||
{
|
||||
AddEnvVar("DATAPATH", config.DataPath);
|
||||
AddVolume(config.DataPath, 1.GB());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace CodexPlugin
|
||||
public class ApiChecker
|
||||
{
|
||||
// <INSERT-OPENAPI-YAML-HASH>
|
||||
private const string OpenApiYamlHash = "DC-90-1B-63-76-1B-92-01-05-65-33-DA-17-C2-34-83-E1-2E-6C-A9-04-4D-68-ED-96-43-F5-E5-6A-00-0F-5F";
|
||||
private const string OpenApiYamlHash = "67-76-AB-FC-54-4F-EB-81-F5-E4-F8-27-DF-82-92-41-63-A5-EA-1B-17-14-0C-BE-20-9C-B3-DF-CE-E4-AA-38";
|
||||
private const string OpenApiFilePath = "/codex/openapi.yaml";
|
||||
private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK";
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace CodexPlugin
|
||||
if (string.IsNullOrEmpty(OpenApiYamlHash)) throw new Exception("OpenAPI yaml hash was not inserted by pre-build trigger.");
|
||||
}
|
||||
|
||||
public void CheckCompatibility(RunningContainers[] containers)
|
||||
public void CheckCompatibility(RunningPod[] containers)
|
||||
{
|
||||
if (checkPassed) return;
|
||||
|
||||
|
||||
@@ -2,27 +2,29 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Utils;
|
||||
|
||||
namespace CodexPlugin
|
||||
{
|
||||
public class CodexAccess : ILogHandler
|
||||
public class CodexAccess
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly IPluginTools tools;
|
||||
private readonly Mapper mapper = new Mapper();
|
||||
private bool hasContainerCrashed;
|
||||
|
||||
public CodexAccess(IPluginTools tools, RunningContainer container, CrashWatcher crashWatcher)
|
||||
public CodexAccess(IPluginTools tools, RunningPod container, CrashWatcher crashWatcher)
|
||||
{
|
||||
this.tools = tools;
|
||||
log = tools.GetLog();
|
||||
Container = container;
|
||||
CrashWatcher = crashWatcher;
|
||||
hasContainerCrashed = false;
|
||||
|
||||
CrashWatcher.Start(this);
|
||||
CrashWatcher.Start();
|
||||
}
|
||||
|
||||
public RunningContainer Container { get; }
|
||||
public RunningPod Container { get; }
|
||||
public CrashWatcher CrashWatcher { get; }
|
||||
|
||||
public DebugInfo GetDebugInfo()
|
||||
@@ -33,20 +35,23 @@ namespace CodexPlugin
|
||||
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 CrashCheck(() =>
|
||||
{
|
||||
return new DebugPeer
|
||||
{
|
||||
IsPeerFound = false
|
||||
};
|
||||
}
|
||||
var endpoint = GetEndpoint();
|
||||
var str = endpoint.HttpGetString($"debug/peer/{peerId}");
|
||||
|
||||
var result = endpoint.Deserialize<DebugPeer>(str);
|
||||
result.IsPeerFound = true;
|
||||
return result;
|
||||
if (str.ToLowerInvariant() == "unable to find peer!")
|
||||
{
|
||||
return new DebugPeer
|
||||
{
|
||||
IsPeerFound = false
|
||||
};
|
||||
}
|
||||
|
||||
var result = endpoint.Deserialize<DebugPeer>(str);
|
||||
result.IsPeerFound = true;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public void ConnectToPeer(string peerId, string[] peerMultiAddresses)
|
||||
@@ -58,14 +63,19 @@ namespace CodexPlugin
|
||||
});
|
||||
}
|
||||
|
||||
public string UploadFile(FileStream fileStream)
|
||||
public string UploadFile(FileStream fileStream, Action<Failure> onFailure)
|
||||
{
|
||||
return OnCodex(api => api.UploadAsync(fileStream));
|
||||
return OnCodex(
|
||||
api => api.UploadAsync(fileStream),
|
||||
CreateRetryConfig(nameof(UploadFile), onFailure));
|
||||
}
|
||||
|
||||
public Stream DownloadFile(string contentId)
|
||||
public Stream DownloadFile(string contentId, Action<Failure> onFailure)
|
||||
{
|
||||
var fileResponse = OnCodex(api => api.DownloadNetworkAsync(contentId));
|
||||
var fileResponse = OnCodex(
|
||||
api => api.DownloadNetworkAsync(contentId),
|
||||
CreateRetryConfig(nameof(DownloadFile), onFailure));
|
||||
|
||||
if (fileResponse.StatusCode != 200) throw new Exception("Download failed with StatusCode: " + fileResponse.StatusCode);
|
||||
return fileResponse.Stream;
|
||||
}
|
||||
@@ -88,9 +98,27 @@ namespace CodexPlugin
|
||||
return OnCodex<string>(api => api.CreateStorageRequestAsync(request.ContentId.Id, body));
|
||||
}
|
||||
|
||||
public CodexSpace Space()
|
||||
{
|
||||
var space = OnCodex<Space>(api => api.SpaceAsync());
|
||||
return mapper.Map(space);
|
||||
}
|
||||
|
||||
public StoragePurchase GetPurchaseStatus(string purchaseId)
|
||||
{
|
||||
return mapper.Map(OnCodex(api => api.GetPurchaseAsync(purchaseId)));
|
||||
return CrashCheck(() =>
|
||||
{
|
||||
var endpoint = GetEndpoint();
|
||||
return Time.Retry(() =>
|
||||
{
|
||||
var str = endpoint.HttpGetString($"storage/purchases/{purchaseId}");
|
||||
if (string.IsNullOrEmpty(str)) throw new Exception("Empty response.");
|
||||
return JsonConvert.DeserializeObject<StoragePurchase>(str)!;
|
||||
}, nameof(GetPurchaseStatus));
|
||||
});
|
||||
|
||||
// TODO: current getpurchase api does not line up with its openapi spec.
|
||||
// return mapper.Map(OnCodex(api => api.GetPurchaseAsync(purchaseId)));
|
||||
}
|
||||
|
||||
public string GetName()
|
||||
@@ -104,53 +132,127 @@ namespace CodexPlugin
|
||||
return workflow.GetPodInfo(Container);
|
||||
}
|
||||
|
||||
public void DeleteRepoFolder()
|
||||
{
|
||||
try
|
||||
{
|
||||
var containerNumber = Container.Containers.First().Recipe.Number;
|
||||
var dataDir = $"datadir{containerNumber}";
|
||||
var workflow = tools.CreateWorkflow();
|
||||
workflow.ExecuteCommand(Container.Containers.First(), "rm", "-Rfv", $"/codex/{dataDir}/repo");
|
||||
Log("Deleted repo folder.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log("Unable to delete repo folder: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private T OnCodex<T>(Func<CodexApi, Task<T>> action)
|
||||
{
|
||||
var address = GetAddress();
|
||||
var result = tools.CreateHttp(CheckContainerCrashed)
|
||||
.OnClient(client =>
|
||||
{
|
||||
var api = new CodexApi(client);
|
||||
api.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1";
|
||||
return Time.Wait(action(api));
|
||||
});
|
||||
var result = tools.CreateHttp(GetHttpId(), CheckContainerCrashed).OnClient(client => CallCodex(client, action));
|
||||
return result;
|
||||
}
|
||||
|
||||
private T OnCodex<T>(Func<CodexApi, Task<T>> action, Retry retry)
|
||||
{
|
||||
var result = tools.CreateHttp(GetHttpId(), CheckContainerCrashed).OnClient(client => CallCodex(client, action), retry);
|
||||
return result;
|
||||
}
|
||||
|
||||
private T CallCodex<T>(HttpClient client, Func<CodexApi, Task<T>> action)
|
||||
{
|
||||
var address = GetAddress();
|
||||
var api = new CodexApi(client);
|
||||
api.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1";
|
||||
return CrashCheck(() => Time.Wait(action(api)));
|
||||
}
|
||||
|
||||
private T CrashCheck<T>(Func<T> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
return action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
CrashWatcher.HasContainerCrashed();
|
||||
}
|
||||
}
|
||||
|
||||
private IEndpoint GetEndpoint()
|
||||
{
|
||||
return tools
|
||||
.CreateHttp(CheckContainerCrashed)
|
||||
.CreateHttp(GetHttpId(), CheckContainerCrashed)
|
||||
.CreateEndpoint(GetAddress(), "/api/codex/v1/", Container.Name);
|
||||
}
|
||||
|
||||
private Address GetAddress()
|
||||
{
|
||||
return Container.GetAddress(tools.GetLog(), CodexContainerRecipe.ApiPortTag);
|
||||
return Container.Containers.Single().GetAddress(log, CodexContainerRecipe.ApiPortTag);
|
||||
}
|
||||
|
||||
private string GetHttpId()
|
||||
{
|
||||
return GetAddress().ToString();
|
||||
}
|
||||
|
||||
private void CheckContainerCrashed(HttpClient client)
|
||||
{
|
||||
if (hasContainerCrashed) throw new Exception("Container has crashed.");
|
||||
if (CrashWatcher.HasContainerCrashed()) throw new Exception($"Container {GetName()} has crashed.");
|
||||
}
|
||||
|
||||
public void Log(Stream crashLog)
|
||||
private Retry CreateRetryConfig(string description, Action<Failure> onFailure)
|
||||
{
|
||||
var log = tools.GetLog();
|
||||
var file = log.CreateSubfile();
|
||||
log.Log($"Container {Container.Name} has crashed. Downloading crash log to '{file.FullFilename}'...");
|
||||
file.Write($"Container Crash Log for {Container.Name}.");
|
||||
var timeSet = tools.TimeSet;
|
||||
|
||||
using var reader = new StreamReader(crashLog);
|
||||
var line = reader.ReadLine();
|
||||
while (line != null)
|
||||
return new Retry(description, timeSet.HttpRetryTimeout(), timeSet.HttpCallRetryDelay(), failure =>
|
||||
{
|
||||
file.Write(line);
|
||||
line = reader.ReadLine();
|
||||
onFailure(failure);
|
||||
Investigate(failure, timeSet);
|
||||
});
|
||||
}
|
||||
|
||||
private void Investigate(Failure failure, ITimeSet timeSet)
|
||||
{
|
||||
Log($"Retry {failure.TryNumber} took {Time.FormatDuration(failure.Duration)} and failed with '{failure.Exception}'. " +
|
||||
$"(HTTP timeout = {Time.FormatDuration(timeSet.HttpCallTimeout())}) " +
|
||||
$"Checking if node responds to debug/info...");
|
||||
|
||||
try
|
||||
{
|
||||
var debugInfo = GetDebugInfo();
|
||||
if (string.IsNullOrEmpty(debugInfo.Spr))
|
||||
{
|
||||
Log("Did not get value debug/info response.");
|
||||
Throw(failure);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("Got valid response from debug/info.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log("Got exception from debug/info call: " + ex);
|
||||
Throw(failure);
|
||||
}
|
||||
|
||||
log.Log("Crash log successfully downloaded.");
|
||||
hasContainerCrashed = true;
|
||||
if (failure.Duration < timeSet.HttpCallTimeout())
|
||||
{
|
||||
Log("Retry failed within HTTP timeout duration.");
|
||||
Throw(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void Throw(Failure failure)
|
||||
{
|
||||
throw failure.Exception;
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
{
|
||||
log.Log($"{GetName()} {msg}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace CodexPlugin
|
||||
{
|
||||
public class CodexContainerRecipe : ContainerRecipeFactory
|
||||
{
|
||||
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-cd280d4-dist-tests";
|
||||
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-64b82de-dist-tests";
|
||||
|
||||
public const string ApiPortTag = "codex_api_port";
|
||||
public const string ListenPortTag = "codex_listen_port";
|
||||
public const string MetricsPortTag = "codex_metrics_port";
|
||||
@@ -108,8 +109,9 @@ namespace CodexPlugin
|
||||
|
||||
// Custom scripting in the Codex test image will write this variable to a private-key file,
|
||||
// and pass the correct filename to Codex.
|
||||
AddEnvVar("PRIV_KEY", marketplaceSetup.EthAccount.PrivateKey);
|
||||
Additional(marketplaceSetup.EthAccount);
|
||||
var account = marketplaceSetup.EthAccountSetup.GetNew();
|
||||
AddEnvVar("PRIV_KEY", account.PrivateKey);
|
||||
Additional(account);
|
||||
|
||||
SetCommandOverride(marketplaceSetup);
|
||||
if (marketplaceSetup.IsValidator)
|
||||
@@ -118,7 +120,7 @@ namespace CodexPlugin
|
||||
}
|
||||
}
|
||||
|
||||
if(!string.IsNullOrEmpty(config.NameOverride))
|
||||
if (!string.IsNullOrEmpty(config.NameOverride))
|
||||
{
|
||||
AddEnvVar("CODEX_NODENAME", config.NameOverride);
|
||||
}
|
||||
@@ -158,7 +160,7 @@ namespace CodexPlugin
|
||||
|
||||
private ByteSize GetVolumeCapacity(CodexStartupConfig config)
|
||||
{
|
||||
if (config.StorageQuota != null) return config.StorageQuota;
|
||||
if (config.StorageQuota != null) return config.StorageQuota.Multiply(1.2);
|
||||
// Default Codex quota: 8 Gb, using +20% to be safe.
|
||||
return 8.GB().Multiply(1.2);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ namespace CodexPlugin
|
||||
public class CodexDeployment
|
||||
{
|
||||
public CodexDeployment(CodexInstance[] codexInstances, GethDeployment gethDeployment,
|
||||
CodexContractsDeployment codexContractsDeployment, RunningContainers? prometheusContainer,
|
||||
RunningContainers? discordBotContainer, DeploymentMetadata metadata,
|
||||
CodexContractsDeployment codexContractsDeployment, RunningPod? prometheusContainer,
|
||||
RunningPod? discordBotContainer, DeploymentMetadata metadata,
|
||||
String id)
|
||||
{
|
||||
Id = id;
|
||||
@@ -24,20 +24,20 @@ namespace CodexPlugin
|
||||
public CodexInstance[] CodexInstances { get; }
|
||||
public GethDeployment GethDeployment { get; }
|
||||
public CodexContractsDeployment CodexContractsDeployment { get; }
|
||||
public RunningContainers? PrometheusContainer { get; }
|
||||
public RunningContainers? DiscordBotContainer { get; }
|
||||
public RunningPod? PrometheusContainer { get; }
|
||||
public RunningPod? DiscordBotContainer { get; }
|
||||
public DeploymentMetadata Metadata { get; }
|
||||
}
|
||||
|
||||
public class CodexInstance
|
||||
{
|
||||
public CodexInstance(RunningContainers containers, DebugInfo info)
|
||||
public CodexInstance(RunningPod pod, DebugInfo info)
|
||||
{
|
||||
Containers = containers;
|
||||
Pod = pod;
|
||||
Info = info;
|
||||
}
|
||||
|
||||
public RunningContainers Containers { get; }
|
||||
public RunningPod Pod { get; }
|
||||
public DebugInfo Info { get; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace CodexPlugin
|
||||
{
|
||||
public class CodexLogLine
|
||||
{
|
||||
public static CodexLogLine? Parse(string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(line) ||
|
||||
line.Length < 34 ||
|
||||
line[3] != ' ' ||
|
||||
line[33] != ' ') return null;
|
||||
|
||||
line = line.Replace(Environment.NewLine, string.Empty);
|
||||
|
||||
var level = line.Substring(0, 3);
|
||||
var dtLine = line.Substring(4, 23);
|
||||
|
||||
var firstEqualSign = line.IndexOf('=');
|
||||
var msgStart = 34;
|
||||
var msgEnd = line.Substring(0, firstEqualSign).LastIndexOf(' ');
|
||||
var msg = line.Substring(msgStart, msgEnd - msgStart).Trim();
|
||||
var attrsLine = line.Substring(msgEnd);
|
||||
|
||||
var attrs = SplitAttrs(attrsLine);
|
||||
|
||||
var format = "yyyy-MM-dd HH:mm:ss.fff";
|
||||
var dt = DateTime.ParseExact(dtLine, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToUniversalTime();
|
||||
|
||||
return new CodexLogLine()
|
||||
{
|
||||
LogLevel = level,
|
||||
TimestampUtc = dt,
|
||||
Message = msg,
|
||||
Attributes = attrs
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string LogLevel { get; set; } = string.Empty;
|
||||
public DateTime TimestampUtc { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> Attributes { get; private set; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// After too much time spent cursing at regexes, here's what I got:
|
||||
/// Parses input string into 'key=value' pair, considerate of quoted (") values.
|
||||
/// </summary>
|
||||
private static Dictionary<string, string> SplitAttrs(string input)
|
||||
{
|
||||
input += " ";
|
||||
var result = new Dictionary<string, string>();
|
||||
|
||||
var key = string.Empty;
|
||||
var value = string.Empty;
|
||||
var mode = 1;
|
||||
var inQuote = false;
|
||||
|
||||
foreach (var c in input)
|
||||
{
|
||||
if (mode == 1)
|
||||
{
|
||||
if (c == '=') mode = 2;
|
||||
else if (c == ' ')
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) continue;
|
||||
else
|
||||
{
|
||||
result.Add(key, string.Empty);
|
||||
key = string.Empty;
|
||||
value = string.Empty;
|
||||
}
|
||||
}
|
||||
else key += c;
|
||||
}
|
||||
else if (mode == 2)
|
||||
{
|
||||
if (c == ' ' && !inQuote)
|
||||
{
|
||||
result.Add(key, value);
|
||||
key = string.Empty;
|
||||
value = string.Empty;
|
||||
mode = 1;
|
||||
}
|
||||
else if (c == '\"')
|
||||
{
|
||||
inQuote = !inQuote;
|
||||
}
|
||||
else
|
||||
{
|
||||
value += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Core;
|
||||
using CodexPlugin.Hooks;
|
||||
using Core;
|
||||
using FileUtils;
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
@@ -12,39 +13,69 @@ namespace CodexPlugin
|
||||
public interface ICodexNode : IHasContainer, IHasMetricsScrapeTarget, IHasEthAddress
|
||||
{
|
||||
string GetName();
|
||||
string GetPeerId();
|
||||
DebugInfo GetDebugInfo();
|
||||
DebugPeer GetDebugPeer(string peerId);
|
||||
ContentId UploadFile(TrackedFile file);
|
||||
ContentId UploadFile(TrackedFile file, Action<Failure> onFailure);
|
||||
TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "");
|
||||
TrackedFile? DownloadContent(ContentId contentId, Action<Failure> onFailure, string fileLabel = "");
|
||||
LocalDatasetList LocalFiles();
|
||||
CodexSpace Space();
|
||||
void ConnectToPeer(ICodexNode node);
|
||||
DebugInfoVersion Version { get; }
|
||||
IMarketplaceAccess Marketplace { get; }
|
||||
CrashWatcher CrashWatcher { get; }
|
||||
PodInfo GetPodInfo();
|
||||
ITransferSpeeds TransferSpeeds { get; }
|
||||
EthAccount EthAccount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Warning! The node is not usable after this.
|
||||
/// TODO: Replace with delete-blocks debug call once available in Codex.
|
||||
/// </summary>
|
||||
void DeleteRepoFolder();
|
||||
void Stop(bool waitTillStopped);
|
||||
}
|
||||
|
||||
public class CodexNode : ICodexNode
|
||||
{
|
||||
private const string UploadFailedMessage = "Unable to store block";
|
||||
private readonly ILog log;
|
||||
private readonly IPluginTools tools;
|
||||
private readonly EthAddress? ethAddress;
|
||||
private readonly ICodexNodeHooks hooks;
|
||||
private readonly EthAccount? ethAccount;
|
||||
private readonly TransferSpeeds transferSpeeds;
|
||||
private string peerId = string.Empty;
|
||||
private string nodeId = string.Empty;
|
||||
|
||||
public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, EthAddress? ethAddress)
|
||||
public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, ICodexNodeHooks hooks, EthAccount? ethAccount)
|
||||
{
|
||||
this.tools = tools;
|
||||
this.ethAddress = ethAddress;
|
||||
this.ethAccount = ethAccount;
|
||||
CodexAccess = codexAccess;
|
||||
Group = group;
|
||||
Marketplace = marketplaceAccess;
|
||||
this.hooks = hooks;
|
||||
Version = new DebugInfoVersion();
|
||||
transferSpeeds = new TransferSpeeds();
|
||||
|
||||
log = new LogPrefixer(tools.GetLog(), $"{GetName()} ");
|
||||
}
|
||||
|
||||
public RunningContainer Container { get { return CodexAccess.Container; } }
|
||||
public void Awake()
|
||||
{
|
||||
hooks.OnNodeStarting(Container.Recipe.RecipeCreatedUtc, Container.Recipe.Image, ethAccount);
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
hooks.OnNodeStarted(peerId, nodeId);
|
||||
}
|
||||
|
||||
public RunningPod Pod { get { return CodexAccess.Container; } }
|
||||
|
||||
public RunningContainer Container { get { return Pod.Containers.Single(); } }
|
||||
public CodexAccess CodexAccess { get; }
|
||||
public CrashWatcher CrashWatcher { get => CodexAccess.CrashWatcher; }
|
||||
public CodexNodeGroup Group { get; }
|
||||
@@ -56,7 +87,7 @@ namespace CodexPlugin
|
||||
{
|
||||
get
|
||||
{
|
||||
return new MetricsScrapeTarget(CodexAccess.Container, CodexContainerRecipe.MetricsPortTag);
|
||||
return new MetricsScrapeTarget(CodexAccess.Container.Containers.First(), CodexContainerRecipe.MetricsPortTag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,21 +95,35 @@ namespace CodexPlugin
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ethAddress == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it.");
|
||||
return ethAddress;
|
||||
EnsureMarketplace();
|
||||
return ethAccount!.EthAddress;
|
||||
}
|
||||
}
|
||||
|
||||
public EthAccount EthAccount
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureMarketplace();
|
||||
return ethAccount!;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetName()
|
||||
{
|
||||
return CodexAccess.Container.Name;
|
||||
return Container.Name;
|
||||
}
|
||||
|
||||
public string GetPeerId()
|
||||
{
|
||||
return peerId;
|
||||
}
|
||||
|
||||
public DebugInfo GetDebugInfo()
|
||||
{
|
||||
var debugInfo = CodexAccess.GetDebugInfo();
|
||||
var known = string.Join(",", debugInfo.Table.Nodes.Select(n => n.PeerId));
|
||||
Log($"Got DebugInfo with id: '{debugInfo.Id}'. This node knows: {known}");
|
||||
Log($"Got DebugInfo with id: {debugInfo.Id}. This node knows: [{known}]");
|
||||
return debugInfo;
|
||||
}
|
||||
|
||||
@@ -88,34 +133,55 @@ namespace CodexPlugin
|
||||
}
|
||||
|
||||
public ContentId UploadFile(TrackedFile file)
|
||||
{
|
||||
return UploadFile(file, DoNothing);
|
||||
}
|
||||
|
||||
public ContentId UploadFile(TrackedFile file, Action<Failure> onFailure)
|
||||
{
|
||||
using var fileStream = File.OpenRead(file.Filename);
|
||||
var uniqueId = Guid.NewGuid().ToString();
|
||||
var size = file.GetFilesize();
|
||||
|
||||
hooks.OnFileUploading(uniqueId, size);
|
||||
|
||||
var logMessage = $"Uploading file {file.Describe()}...";
|
||||
Log(logMessage);
|
||||
var measurement = Stopwatch.Measure(tools.GetLog(), logMessage, () =>
|
||||
var measurement = Stopwatch.Measure(log, logMessage, () =>
|
||||
{
|
||||
return CodexAccess.UploadFile(fileStream);
|
||||
return CodexAccess.UploadFile(fileStream, onFailure);
|
||||
});
|
||||
|
||||
var response = measurement.Value;
|
||||
transferSpeeds.AddUploadSample(file.GetFilesize(), measurement.Duration);
|
||||
transferSpeeds.AddUploadSample(size, measurement.Duration);
|
||||
|
||||
if (string.IsNullOrEmpty(response)) FrameworkAssert.Fail("Received empty response.");
|
||||
if (response.StartsWith(UploadFailedMessage)) FrameworkAssert.Fail("Node failed to store block.");
|
||||
|
||||
Log($"Uploaded file. Received contentId: '{response}'.");
|
||||
return new ContentId(response);
|
||||
Log($"Uploaded file {file.Describe()}. Received contentId: '{response}'.");
|
||||
|
||||
var cid = new ContentId(response);
|
||||
hooks.OnFileUploaded(uniqueId, size, cid);
|
||||
return cid;
|
||||
}
|
||||
|
||||
public TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "")
|
||||
{
|
||||
var logMessage = $"Downloading for contentId: '{contentId.Id}'...";
|
||||
Log(logMessage);
|
||||
return DownloadContent(contentId, DoNothing, fileLabel);
|
||||
}
|
||||
|
||||
public TrackedFile? DownloadContent(ContentId contentId, Action<Failure> onFailure, string fileLabel = "")
|
||||
{
|
||||
var file = tools.GetFileManager().CreateEmptyFile(fileLabel);
|
||||
var measurement = Stopwatch.Measure(tools.GetLog(), logMessage, () => DownloadToFile(contentId.Id, file));
|
||||
transferSpeeds.AddDownloadSample(file.GetFilesize(), measurement);
|
||||
Log($"Downloaded file {file.Describe()} to '{file.Filename}'.");
|
||||
hooks.OnFileDownloading(contentId);
|
||||
Log($"Downloading '{contentId}'...");
|
||||
|
||||
var logMessage = $"Downloaded '{contentId}' to '{file.Filename}'";
|
||||
var measurement = Stopwatch.Measure(log, logMessage, () => DownloadToFile(contentId.Id, file, onFailure));
|
||||
|
||||
var size = file.GetFilesize();
|
||||
transferSpeeds.AddDownloadSample(size, measurement);
|
||||
hooks.OnFileDownloaded(size, contentId);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -124,6 +190,11 @@ namespace CodexPlugin
|
||||
return CodexAccess.LocalFiles();
|
||||
}
|
||||
|
||||
public CodexSpace Space()
|
||||
{
|
||||
return CodexAccess.Space();
|
||||
}
|
||||
|
||||
public void ConnectToPeer(ICodexNode node)
|
||||
{
|
||||
var peer = (CodexNode)node;
|
||||
@@ -140,19 +211,25 @@ namespace CodexPlugin
|
||||
return CodexAccess.GetPodInfo();
|
||||
}
|
||||
|
||||
public void DeleteRepoFolder()
|
||||
{
|
||||
CodexAccess.DeleteRepoFolder();
|
||||
}
|
||||
|
||||
public void Stop(bool waitTillStopped)
|
||||
{
|
||||
if (Group.Count() > 1) throw new InvalidOperationException("Codex-nodes that are part of a group cannot be " +
|
||||
"individually shut down. Use 'BringOffline()' on the group object to stop the group. This method is only " +
|
||||
"available for codex-nodes in groups of 1.");
|
||||
Log("Stopping...");
|
||||
hooks.OnNodeStopping();
|
||||
|
||||
Group.BringOffline(waitTillStopped);
|
||||
CrashWatcher.Stop();
|
||||
Group.Stop(this, waitTillStopped);
|
||||
}
|
||||
|
||||
public void EnsureOnlineGetVersionResponse()
|
||||
{
|
||||
var debugInfo = Time.Retry(CodexAccess.GetDebugInfo, "ensure online");
|
||||
var nodePeerId = debugInfo.Id;
|
||||
peerId = debugInfo.Id;
|
||||
nodeId = debugInfo.Table.LocalNode.NodeId;
|
||||
var nodeName = CodexAccess.Container.Name;
|
||||
|
||||
if (!debugInfo.Version.IsValid())
|
||||
@@ -160,9 +237,10 @@ namespace CodexPlugin
|
||||
throw new Exception($"Invalid version information received from Codex node {GetName()}: {debugInfo.Version}");
|
||||
}
|
||||
|
||||
var log = tools.GetLog();
|
||||
log.AddStringReplace(nodePeerId, nodeName);
|
||||
log.AddStringReplace(peerId, nodeName);
|
||||
log.AddStringReplace(CodexUtils.ToShortId(peerId), nodeName);
|
||||
log.AddStringReplace(debugInfo.Table.LocalNode.NodeId, nodeName);
|
||||
log.AddStringReplace(CodexUtils.ToShortId(debugInfo.Table.LocalNode.NodeId), nodeName);
|
||||
Version = debugInfo.Version;
|
||||
}
|
||||
|
||||
@@ -171,19 +249,19 @@ namespace CodexPlugin
|
||||
// The peer we want to connect is in a different pod.
|
||||
// We must replace the default IP with the pod IP in the multiAddress.
|
||||
var workflow = tools.CreateWorkflow();
|
||||
var podInfo = workflow.GetPodInfo(peer.Container);
|
||||
var podInfo = workflow.GetPodInfo(peer.Pod);
|
||||
|
||||
return peerInfo.Addrs.Select(a => a
|
||||
.Replace("0.0.0.0", podInfo.Ip))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private void DownloadToFile(string contentId, TrackedFile file)
|
||||
private void DownloadToFile(string contentId, TrackedFile file, Action<Failure> onFailure)
|
||||
{
|
||||
using var fileStream = File.OpenWrite(file.Filename);
|
||||
try
|
||||
{
|
||||
using var downloadStream = CodexAccess.DownloadFile(contentId);
|
||||
using var downloadStream = CodexAccess.DownloadFile(contentId, onFailure);
|
||||
downloadStream.CopyTo(fileStream);
|
||||
}
|
||||
catch
|
||||
@@ -193,9 +271,18 @@ namespace CodexPlugin
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureMarketplace()
|
||||
{
|
||||
if (ethAccount == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it.");
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
{
|
||||
tools.GetLog().Log($"{GetName()}: {msg}");
|
||||
log.Log(msg);
|
||||
}
|
||||
|
||||
private void DoNothing(Failure failure)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Core;
|
||||
using CodexPlugin.Hooks;
|
||||
using Core;
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
@@ -14,30 +15,34 @@ namespace CodexPlugin
|
||||
public class CodexNodeFactory : ICodexNodeFactory
|
||||
{
|
||||
private readonly IPluginTools tools;
|
||||
private readonly CodexHooksFactory codexHooksFactory;
|
||||
|
||||
public CodexNodeFactory(IPluginTools tools)
|
||||
public CodexNodeFactory(IPluginTools tools, CodexHooksFactory codexHooksFactory)
|
||||
{
|
||||
this.tools = tools;
|
||||
this.codexHooksFactory = codexHooksFactory;
|
||||
}
|
||||
|
||||
public CodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group)
|
||||
{
|
||||
var ethAddress = GetEthAddress(access);
|
||||
var marketplaceAccess = GetMarketplaceAccess(access, ethAddress);
|
||||
return new CodexNode(tools, access, group, marketplaceAccess, ethAddress);
|
||||
var ethAccount = GetEthAccount(access);
|
||||
var hooks = codexHooksFactory.CreateHooks(access.Container.Name);
|
||||
|
||||
var marketplaceAccess = GetMarketplaceAccess(access, ethAccount, hooks);
|
||||
return new CodexNode(tools, access, group, marketplaceAccess, hooks, ethAccount);
|
||||
}
|
||||
|
||||
private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAddress? ethAddress)
|
||||
private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAccount? ethAccount, ICodexNodeHooks hooks)
|
||||
{
|
||||
if (ethAddress == null) return new MarketplaceUnavailable();
|
||||
return new MarketplaceAccess(tools.GetLog(), codexAccess);
|
||||
if (ethAccount == null) return new MarketplaceUnavailable();
|
||||
return new MarketplaceAccess(tools.GetLog(), codexAccess, hooks);
|
||||
}
|
||||
|
||||
private EthAddress? GetEthAddress(CodexAccess access)
|
||||
private EthAccount? GetEthAccount(CodexAccess access)
|
||||
{
|
||||
var ethAccount = access.Container.Recipe.Additionals.Get<EthAccount>();
|
||||
var ethAccount = access.Container.Containers.Single().Recipe.Additionals.Get<EthAccount>();
|
||||
if (ethAccount == null) return null;
|
||||
return ethAccount.EthAddress;
|
||||
return ethAccount;
|
||||
}
|
||||
|
||||
public CrashWatcher CreateCrashWatcher(RunningContainer c)
|
||||
|
||||
@@ -15,11 +15,11 @@ namespace CodexPlugin
|
||||
{
|
||||
private readonly CodexStarter starter;
|
||||
|
||||
public CodexNodeGroup(CodexStarter starter, IPluginTools tools, RunningContainers[] containers, ICodexNodeFactory codexNodeFactory)
|
||||
public CodexNodeGroup(CodexStarter starter, IPluginTools tools, RunningPod[] containers, ICodexNodeFactory codexNodeFactory)
|
||||
{
|
||||
this.starter = starter;
|
||||
Containers = containers;
|
||||
Nodes = containers.Containers().Select(c => CreateOnlineCodexNode(c, tools, codexNodeFactory)).ToArray();
|
||||
Nodes = containers.Select(c => CreateOnlineCodexNode(c, tools, codexNodeFactory)).ToArray();
|
||||
Version = new DebugInfoVersion();
|
||||
}
|
||||
|
||||
@@ -39,7 +39,14 @@ namespace CodexPlugin
|
||||
Containers = null!;
|
||||
}
|
||||
|
||||
public RunningContainers[] Containers { get; private set; }
|
||||
public void Stop(CodexNode node, bool waitTillStopped)
|
||||
{
|
||||
starter.Stop(node.Pod, waitTillStopped);
|
||||
Nodes = Nodes.Where(n => n != node).ToArray();
|
||||
Containers = Containers.Where(c => c != node.Pod).ToArray();
|
||||
}
|
||||
|
||||
public RunningPod[] Containers { get; private set; }
|
||||
public CodexNode[] Nodes { get; private set; }
|
||||
public DebugInfoVersion Version { get; private set; }
|
||||
public IMetricsScrapeTarget[] ScrapeTargets => Nodes.Select(n => n.MetricsScrapeTarget).ToArray();
|
||||
@@ -72,13 +79,16 @@ namespace CodexPlugin
|
||||
}
|
||||
|
||||
Version = first;
|
||||
foreach (var node in Nodes) node.Initialize();
|
||||
}
|
||||
|
||||
private CodexNode CreateOnlineCodexNode(RunningContainer c, IPluginTools tools, ICodexNodeFactory factory)
|
||||
private CodexNode CreateOnlineCodexNode(RunningPod c, IPluginTools tools, ICodexNodeFactory factory)
|
||||
{
|
||||
var watcher = factory.CreateCrashWatcher(c);
|
||||
var watcher = factory.CreateCrashWatcher(c.Containers.Single());
|
||||
var access = new CodexAccess(tools, c, watcher);
|
||||
return factory.CreateOnlineCodexNode(access, this);
|
||||
var node = factory.CreateOnlineCodexNode(access, this);
|
||||
node.Awake();
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using CodexPlugin.Hooks;
|
||||
using Core;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
@@ -32,13 +33,13 @@ namespace CodexPlugin
|
||||
{
|
||||
}
|
||||
|
||||
public RunningContainers[] DeployCodexNodes(int numberOfNodes, Action<ICodexSetup> setup)
|
||||
public RunningPod[] DeployCodexNodes(int numberOfNodes, Action<ICodexSetup> setup)
|
||||
{
|
||||
var codexSetup = GetSetup(numberOfNodes, setup);
|
||||
return codexStarter.BringOnline(codexSetup);
|
||||
}
|
||||
|
||||
public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningContainers[] containers)
|
||||
public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningPod[] containers)
|
||||
{
|
||||
containers = containers.Select(c => SerializeGate.Gate(c)).ToArray();
|
||||
return codexStarter.WrapCodexContainers(coreInterface, containers);
|
||||
@@ -57,6 +58,11 @@ namespace CodexPlugin
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCodexHooksProvider(ICodexHooksProvider hooksProvider)
|
||||
{
|
||||
codexStarter.HooksFactory.Provider = hooksProvider;
|
||||
}
|
||||
|
||||
private CodexSetup GetSetup(int numberOfNodes, Action<ICodexSetup> setup)
|
||||
{
|
||||
var codexSetup = new CodexSetup(numberOfNodes);
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Framework\Core\Core.csproj" />
|
||||
<ProjectReference Include="..\..\Framework\KubernetesWorkflow\KubernetesWorkflow.csproj" />
|
||||
<ProjectReference Include="..\..\Framework\OverwatchTranscript\OverwatchTranscript.csproj" />
|
||||
<ProjectReference Include="..\CodexContractsPlugin\CodexContractsPlugin.csproj" />
|
||||
<ProjectReference Include="..\GethPlugin\GethPlugin.csproj" />
|
||||
<ProjectReference Include="..\MetricsPlugin\MetricsPlugin.csproj" />
|
||||
|
||||
@@ -50,7 +50,9 @@ namespace CodexPlugin
|
||||
|
||||
public CodexLogLevel DiscV5 { get; set; }
|
||||
public CodexLogLevel Libp2p { get; set; }
|
||||
public CodexLogLevel ContractClock { get; set; } = CodexLogLevel.Warn;
|
||||
public CodexLogLevel? BlockExchange { get; }
|
||||
public CodexLogLevel JsonSerialize { get; set; } = CodexLogLevel.Warn;
|
||||
}
|
||||
|
||||
public class CodexSetup : CodexStartupConfig, ICodexSetup
|
||||
@@ -166,8 +168,8 @@ namespace CodexPlugin
|
||||
public bool IsStorageNode { get; private set; }
|
||||
public bool IsValidator { get; private set; }
|
||||
public Ether InitialEth { get; private set; } = 0.Eth();
|
||||
public TestToken InitialTestTokens { get; private set; } = 0.TestTokens();
|
||||
public EthAccount EthAccount { get; private set; } = EthAccount.GenerateNew();
|
||||
public TestToken InitialTestTokens { get; private set; } = 0.Tst();
|
||||
public EthAccountSetup EthAccountSetup { get; } = new EthAccountSetup();
|
||||
|
||||
public IMarketplaceSetup AsStorageNode()
|
||||
{
|
||||
@@ -183,7 +185,7 @@ namespace CodexPlugin
|
||||
|
||||
public IMarketplaceSetup WithAccount(EthAccount account)
|
||||
{
|
||||
EthAccount = account;
|
||||
EthAccountSetup.Pin(account);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -199,10 +201,42 @@ namespace CodexPlugin
|
||||
var result = "[(clientNode)"; // When marketplace is enabled, being a clientNode is implicit.
|
||||
result += IsStorageNode ? "(storageNode)" : "()";
|
||||
result += IsValidator ? "(validator)" : "() ";
|
||||
result += $"Address: '{EthAccount.EthAddress}' ";
|
||||
result += $"InitialEth/TT({InitialEth.Eth}/{InitialTestTokens.Amount})";
|
||||
result += $"Pinned address: '{EthAccountSetup}' ";
|
||||
result += $"{InitialEth} / {InitialTestTokens}";
|
||||
result += "] ";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class EthAccountSetup
|
||||
{
|
||||
private readonly List<EthAccount> accounts = new List<EthAccount>();
|
||||
private bool pinned = false;
|
||||
|
||||
public void Pin(EthAccount account)
|
||||
{
|
||||
accounts.Add(account);
|
||||
pinned = true;
|
||||
}
|
||||
|
||||
public EthAccount GetNew()
|
||||
{
|
||||
if (pinned) return accounts.Last();
|
||||
|
||||
var a = EthAccount.GenerateNew();
|
||||
accounts.Add(a);
|
||||
return a;
|
||||
}
|
||||
|
||||
public EthAccount[] GetAll()
|
||||
{
|
||||
return accounts.ToArray();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (!accounts.Any()) return "NoEthAccounts";
|
||||
return string.Join(",", accounts.Select(a => a.ToString()).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Core;
|
||||
using CodexPlugin.Hooks;
|
||||
using Core;
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
@@ -19,7 +21,9 @@ namespace CodexPlugin
|
||||
apiChecker = new ApiChecker(pluginTools);
|
||||
}
|
||||
|
||||
public RunningContainers[] BringOnline(CodexSetup codexSetup)
|
||||
public CodexHooksFactory HooksFactory { get; } = new CodexHooksFactory();
|
||||
|
||||
public RunningPod[] BringOnline(CodexSetup codexSetup)
|
||||
{
|
||||
LogSeparator();
|
||||
Log($"Starting {codexSetup.Describe()}...");
|
||||
@@ -33,17 +37,18 @@ namespace CodexPlugin
|
||||
foreach (var rc in containers)
|
||||
{
|
||||
var podInfo = GetPodInfo(rc);
|
||||
var podInfos = string.Join(", ", rc.Containers.Select(c => $"Container: '{c.Name}' runs at '{podInfo.K8SNodeName}'={podInfo.Ip}"));
|
||||
Log($"Started {codexSetup.NumberOfNodes} nodes of image '{containers.Containers().First().Recipe.Image}'. ({podInfos})");
|
||||
var podInfos = string.Join(", ", rc.Containers.Select(c => $"Container: '{c.Name}' PodLabel: '{c.RunningPod.StartResult.Deployment.PodLabel}' runs at '{podInfo.K8SNodeName}'={podInfo.Ip}"));
|
||||
Log($"Started node with image '{containers.First().Containers.First().Recipe.Image}'. ({podInfos})");
|
||||
LogEthAddress(rc);
|
||||
}
|
||||
LogSeparator();
|
||||
|
||||
return containers;
|
||||
}
|
||||
|
||||
public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningContainers[] containers)
|
||||
public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningPod[] containers)
|
||||
{
|
||||
var codexNodeFactory = new CodexNodeFactory(pluginTools);
|
||||
var codexNodeFactory = new CodexNodeFactory(pluginTools, HooksFactory);
|
||||
|
||||
var group = CreateCodexGroup(coreInterface, containers, codexNodeFactory);
|
||||
|
||||
@@ -65,6 +70,14 @@ namespace CodexPlugin
|
||||
Log("Stopped.");
|
||||
}
|
||||
|
||||
public void Stop(RunningPod pod, bool waitTillStopped)
|
||||
{
|
||||
Log($"Stopping node...");
|
||||
var workflow = pluginTools.CreateWorkflow();
|
||||
workflow.Stop(pod, waitTillStopped);
|
||||
Log("Stopped.");
|
||||
}
|
||||
|
||||
public string GetCodexId()
|
||||
{
|
||||
if (versionResponse != null) return versionResponse.Version;
|
||||
@@ -85,24 +98,27 @@ namespace CodexPlugin
|
||||
return startupConfig;
|
||||
}
|
||||
|
||||
private RunningContainers[] StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, ILocation location)
|
||||
private RunningPod[] StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, ILocation location)
|
||||
{
|
||||
var result = new List<RunningContainers>();
|
||||
var futureContainers = new List<FutureContainers>();
|
||||
for (var i = 0; i < numberOfNodes; i++)
|
||||
{
|
||||
var workflow = pluginTools.CreateWorkflow();
|
||||
result.Add(workflow.Start(1, location, recipe, startupConfig));
|
||||
futureContainers.Add(workflow.Start(1, location, recipe, startupConfig));
|
||||
}
|
||||
return result.ToArray();
|
||||
|
||||
return futureContainers
|
||||
.Select(f => f.WaitForOnline())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private PodInfo GetPodInfo(RunningContainers rc)
|
||||
private PodInfo GetPodInfo(RunningPod rc)
|
||||
{
|
||||
var workflow = pluginTools.CreateWorkflow();
|
||||
return workflow.GetPodInfo(rc);
|
||||
}
|
||||
|
||||
private CodexNodeGroup CreateCodexGroup(CoreInterface coreInterface, RunningContainers[] runningContainers, CodexNodeFactory codexNodeFactory)
|
||||
private CodexNodeGroup CreateCodexGroup(CoreInterface coreInterface, RunningPod[] runningContainers, CodexNodeFactory codexNodeFactory)
|
||||
{
|
||||
var group = new CodexNodeGroup(this, pluginTools, runningContainers, codexNodeFactory);
|
||||
|
||||
@@ -119,10 +135,10 @@ namespace CodexPlugin
|
||||
return group;
|
||||
}
|
||||
|
||||
private void CodexNodesNotOnline(CoreInterface coreInterface, RunningContainers[] runningContainers)
|
||||
private void CodexNodesNotOnline(CoreInterface coreInterface, RunningPod[] runningContainers)
|
||||
{
|
||||
Log("Codex nodes failed to start");
|
||||
foreach (var container in runningContainers.Containers()) coreInterface.DownloadLog(container);
|
||||
foreach (var container in runningContainers.First().Containers) coreInterface.DownloadLog(container);
|
||||
}
|
||||
|
||||
private void LogSeparator()
|
||||
@@ -130,6 +146,13 @@ namespace CodexPlugin
|
||||
Log("----------------------------------------------------------------------------");
|
||||
}
|
||||
|
||||
private void LogEthAddress(RunningPod rc)
|
||||
{
|
||||
var account = rc.Containers.First().Recipe.Additionals.Get<EthAccount>();
|
||||
if (account == null) return;
|
||||
Log($"{rc.Name} = {account}");
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
pluginTools.GetLog().Log(message);
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace CodexPlugin
|
||||
public string? NameOverride { get; set; }
|
||||
public ILocation Location { get; set; } = KnownLocations.UnspecifiedLocation;
|
||||
public CodexLogLevel LogLevel { get; set; }
|
||||
public CodexLogCustomTopics? CustomTopics { get; set; } = new CodexLogCustomTopics(CodexLogLevel.Warn, CodexLogLevel.Warn);
|
||||
public CodexLogCustomTopics? CustomTopics { get; set; } = new CodexLogCustomTopics(CodexLogLevel.Info, CodexLogLevel.Warn);
|
||||
public ByteSize? StorageQuota { get; set; }
|
||||
public bool MetricsEnabled { get; set; }
|
||||
public MarketplaceInitialConfig? MarketplaceConfig { get; set; }
|
||||
@@ -50,10 +50,10 @@ namespace CodexPlugin
|
||||
"secure",
|
||||
"chronosstream",
|
||||
"connection",
|
||||
"connmanager",
|
||||
// Removed: "connmanager", is used for transcript peer-dropped event.
|
||||
"websock",
|
||||
"ws-session",
|
||||
"dialer",
|
||||
// Removed: "dialer", is used for transcript successful-dial event.
|
||||
"muxedupgrade",
|
||||
"upgrade",
|
||||
"identify"
|
||||
@@ -73,12 +73,18 @@ namespace CodexPlugin
|
||||
"contracts",
|
||||
"clock"
|
||||
};
|
||||
var jsonSerializeTopics = new[]
|
||||
{
|
||||
"serde",
|
||||
"json",
|
||||
"serialization"
|
||||
};
|
||||
|
||||
level = $"{level};" +
|
||||
$"{CustomTopics.DiscV5.ToString()!.ToLowerInvariant()}:{string.Join(",", discV5Topics)};" +
|
||||
$"{CustomTopics.Libp2p.ToString()!.ToLowerInvariant()}:{string.Join(",", libp2pTopics)};" +
|
||||
// Contract clock is always set to warn. It logs a trace every second.
|
||||
$"{CodexLogLevel.Warn.ToString().ToLowerInvariant()}:{string.Join(",", contractClockTopics)}";
|
||||
$"{CustomTopics.ContractClock.ToString().ToLowerInvariant()}:{string.Join(",", contractClockTopics)};" +
|
||||
$"{CustomTopics.JsonSerialize.ToString().ToLowerInvariant()}:{string.Join(",", jsonSerializeTopics)}";
|
||||
|
||||
if (CustomTopics.BlockExchange != null)
|
||||
{
|
||||
|
||||
@@ -95,6 +95,11 @@ namespace CodexPlugin
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Id;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ContentId id && Id == id.Id;
|
||||
@@ -105,4 +110,18 @@ namespace CodexPlugin
|
||||
return HashCode.Combine(Id);
|
||||
}
|
||||
}
|
||||
|
||||
public class CodexSpace
|
||||
{
|
||||
public long TotalBlocks { get; set; }
|
||||
public long QuotaMaxBytes { get; set; }
|
||||
public long QuotaUsedBytes { get; set; }
|
||||
public long QuotaReservedBytes { get; set; }
|
||||
public long FreeBytes => QuotaMaxBytes - (QuotaUsedBytes + QuotaReservedBytes);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonConvert.SerializeObject(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace CodexPlugin
|
||||
{
|
||||
public static class CodexUtils
|
||||
{
|
||||
public static string ToShortId(string id)
|
||||
{
|
||||
if (id.Length > 10)
|
||||
{
|
||||
return $"{id[..3]}*{id[^6..]}";
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
// after update of codex-dht, shortID should be consistent!
|
||||
public static string ToNodeIdShortId(string id)
|
||||
{
|
||||
if (id.Length > 10)
|
||||
{
|
||||
return $"{id[..2]}*{id[^6..]}";
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
using Core;
|
||||
using CodexPlugin.Hooks;
|
||||
using Core;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace CodexPlugin
|
||||
{
|
||||
public static class CoreInterfaceExtensions
|
||||
{
|
||||
public static RunningContainers[] DeployCodexNodes(this CoreInterface ci, int number, Action<ICodexSetup> setup)
|
||||
public static RunningPod[] DeployCodexNodes(this CoreInterface ci, int number, Action<ICodexSetup> setup)
|
||||
{
|
||||
return Plugin(ci).DeployCodexNodes(number, setup);
|
||||
}
|
||||
|
||||
public static ICodexNodeGroup WrapCodexContainers(this CoreInterface ci, RunningContainers[] containers)
|
||||
public static ICodexNodeGroup WrapCodexContainers(this CoreInterface ci, RunningPod[] containers)
|
||||
{
|
||||
return Plugin(ci).WrapCodexContainers(ci, containers);
|
||||
}
|
||||
@@ -38,6 +39,11 @@ namespace CodexPlugin
|
||||
return ci.StartCodexNodes(number, s => { });
|
||||
}
|
||||
|
||||
public static void SetCodexHooksProvider(this CoreInterface ci, ICodexHooksProvider hooksProvider)
|
||||
{
|
||||
Plugin(ci).SetCodexHooksProvider(hooksProvider);
|
||||
}
|
||||
|
||||
private static CodexPlugin Plugin(CoreInterface ci)
|
||||
{
|
||||
return ci.GetPlugin<CodexPlugin>();
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using GethPlugin;
|
||||
using Utils;
|
||||
|
||||
namespace CodexPlugin.Hooks
|
||||
{
|
||||
public interface ICodexHooksProvider
|
||||
{
|
||||
ICodexNodeHooks CreateHooks(string nodeName);
|
||||
}
|
||||
|
||||
public class CodexHooksFactory
|
||||
{
|
||||
public ICodexHooksProvider Provider { get; set; } = new DoNothingHooksProvider();
|
||||
|
||||
public ICodexNodeHooks CreateHooks(string nodeName)
|
||||
{
|
||||
return Provider.CreateHooks(nodeName);
|
||||
}
|
||||
}
|
||||
|
||||
public class DoNothingHooksProvider : ICodexHooksProvider
|
||||
{
|
||||
public ICodexNodeHooks CreateHooks(string nodeName)
|
||||
{
|
||||
return new DoNothingCodexHooks();
|
||||
}
|
||||
}
|
||||
|
||||
public class DoNothingCodexHooks : ICodexNodeHooks
|
||||
{
|
||||
public void OnFileDownloaded(ByteSize size, ContentId cid)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnFileDownloading(ContentId cid)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnFileUploaded(string uid, ByteSize size, ContentId cid)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnFileUploading(string uid, ByteSize size)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnNodeStarted(string peerId, string nodeId)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnNodeStarting(DateTime startUtc, string image, EthAccount? ethAccount)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnNodeStopping()
|
||||
{
|
||||
}
|
||||
|
||||
public void OnStorageAvailabilityCreated(StorageAvailability response)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnStorageContractSubmitted(StoragePurchaseContract storagePurchaseContract)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnStorageContractUpdated(StoragePurchase purchaseStatus)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using GethPlugin;
|
||||
using Utils;
|
||||
|
||||
namespace CodexPlugin.Hooks
|
||||
{
|
||||
public interface ICodexNodeHooks
|
||||
{
|
||||
void OnNodeStarting(DateTime startUtc, string image, EthAccount? ethAccount);
|
||||
void OnNodeStarted(string peerId, string nodeId);
|
||||
void OnNodeStopping();
|
||||
void OnFileUploading(string uid, ByteSize size);
|
||||
void OnFileUploaded(string uid, ByteSize size, ContentId cid);
|
||||
void OnFileDownloading(ContentId cid);
|
||||
void OnFileDownloaded(ByteSize size, ContentId cid);
|
||||
void OnStorageContractSubmitted(StoragePurchaseContract storagePurchaseContract);
|
||||
void OnStorageContractUpdated(StoragePurchase purchaseStatus);
|
||||
void OnStorageAvailabilityCreated(StorageAvailability response);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user