Skip to content

Commit 2e43f2c

Browse files
committed
Merge branch 'main' into mattmcl4475/addScaleTest
2 parents 7bb49c9 + 5073c6a commit 2e43f2c

39 files changed

+850
-420
lines changed

src/Tes.ApiClients.Tests/TerraWsmApiClientTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public void SetUp()
3232
cache.Setup(c => c.CreateEntry(It.IsAny<object>())).Returns(new Mock<Microsoft.Extensions.Caching.Memory.ICacheEntry>().Object);
3333
cacheAndRetryBuilder.SetupGet(c => c.AppCache).Returns(cache.Object);
3434
cacheAndRetryHandler = new(TestServices.RetryHandlersHelpers.GetCachingAsyncRetryPolicyMock(cacheAndRetryBuilder, c => c.DefaultRetryHttpResponseMessagePolicyBuilder()));
35-
azureEnvironmentConfig = ExpensiveObjectTestUtility.AzureCloudConfig.AzureEnvironmentConfig;
35+
azureEnvironmentConfig = ExpensiveObjectTestUtility.AzureCloudConfig.AzureEnvironmentConfig!;
3636

3737
terraWsmApiClient = new TerraWsmApiClient(TerraApiStubData.WsmApiHost, tokenCredential.Object,
3838
cacheAndRetryBuilder.Object, azureEnvironmentConfig, NullLogger<TerraWsmApiClient>.Instance);

src/Tes.SDK/TesClient.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ private HttpRequestMessage GetRequest(HttpMethod method, string urlPath, string?
8080
private async Task<HttpResponseMessage> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
8181
{
8282
SetAuthorizationHeader(request);
83-
var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken);
83+
var response = await _httpClient.SendAsync(request, cancellationToken);
8484

8585
if (!response.IsSuccessStatusCode)
8686
{

src/Tes/Extensions/TesTaskExtensions.cs

+53-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.Linq;
67
using Tes.Models;
8+
using Tes.TaskSubmitters;
79

810
namespace Tes.Extensions
911
{
@@ -12,6 +14,51 @@ namespace Tes.Extensions
1214
/// </summary>
1315
public static class TesTaskExtensions
1416
{
17+
/// <summary>
18+
/// Reports if task was submitted by Cromwell.
19+
/// </summary>
20+
/// <param name="tesTask"><see cref="TesTask"/>.</param>
21+
/// <returns><see cref="true"/> if task is from Cromwell, false otherwise.</returns>
22+
public static bool IsCromwell(this TesTask tesTask)
23+
{
24+
return tesTask.TaskSubmitter is CromwellTaskSubmitter;
25+
}
26+
27+
/// <summary>
28+
/// Gets Cromwell task metadata
29+
/// </summary>
30+
/// <param name="tesTask"><see cref="TesTask"/>.</param>
31+
/// <returns><see cref="CromwellTaskSubmitter"/>.</returns>
32+
public static CromwellTaskSubmitter GetCromwellMetadata(this TesTask tesTask)
33+
{
34+
return tesTask.TaskSubmitter as CromwellTaskSubmitter;
35+
}
36+
37+
/// <summary>
38+
/// Visits each value in an enumeration with an action.
39+
/// </summary>
40+
/// <typeparam name="T">Type of enumerated items.</typeparam>
41+
/// <param name="values">Enumeration on which to visit each item.</param>
42+
/// <param name="action">Action to invoke with each item.</param>
43+
public static void ForEach<T>(this IEnumerable<T> values, Action<T> action)
44+
{
45+
foreach (var value in values)
46+
{
47+
action(value);
48+
}
49+
}
50+
51+
/// <summary>
52+
/// Adds a range of items to an <see cref="IList{T}"/>.
53+
/// </summary>
54+
/// <typeparam name="T">Type of enumerated items.</typeparam>
55+
/// <param name="list">List to add <paramref name="items"/> to.</param>
56+
/// <param name="items">Items to add to <paramref name="list"/>.</param>
57+
public static void AddRange<T>(this IList<T> list, IEnumerable<T> items)
58+
{
59+
items.ForEach(list.Add);
60+
}
61+
1562
/// <summary>
1663
/// Writes to <see cref="TesTask"/> system log.
1764
/// </summary>
@@ -22,7 +69,7 @@ public static void AddToSystemLog(this TesTask tesTask, IEnumerable<string> logE
2269
if (logEntries is not null && logEntries.Any(e => !string.IsNullOrEmpty(e)))
2370
{
2471
var tesTaskLog = tesTask.GetOrAddTesTaskLog();
25-
tesTaskLog.SystemLogs ??= new();
72+
tesTaskLog.SystemLogs ??= [];
2673
tesTaskLog.SystemLogs.AddRange(logEntries);
2774
}
2875
}
@@ -36,7 +83,7 @@ public static void AddToSystemLog(this TesTask tesTask, IEnumerable<string> logE
3683
public static void SetFailureReason(this TesTask tesTask, string failureReason, params string[] additionalSystemLogItems)
3784
{
3885
tesTask.GetOrAddTesTaskLog().FailureReason = failureReason;
39-
tesTask.AddToSystemLog(new[] { failureReason });
86+
tesTask.AddToSystemLog([failureReason]);
4087
tesTask.AddToSystemLog(additionalSystemLogItems.Where(i => !string.IsNullOrEmpty(i)));
4188
}
4289

@@ -57,7 +104,7 @@ public static void SetFailureReason(this TesTask tesTask, TesException tesExcept
57104
public static void SetWarning(this TesTask tesTask, string warning, params string[] additionalSystemLogItems)
58105
{
59106
tesTask.GetOrAddTesTaskLog().Warning = warning;
60-
tesTask.AddToSystemLog(new[] { warning });
107+
tesTask.AddToSystemLog([warning]);
61108
tesTask.AddToSystemLog(additionalSystemLogItems.Where(i => !string.IsNullOrEmpty(i)));
62109
}
63110

@@ -70,7 +117,7 @@ public static TesTaskLog GetOrAddTesTaskLog(this TesTask tesTask)
70117
{
71118
if (tesTask.Logs is null || !tesTask.Logs.Any())
72119
{
73-
tesTask.Logs = new() { new() };
120+
tesTask.Logs = [new()];
74121
}
75122

76123
return tesTask.Logs.Last();
@@ -83,7 +130,7 @@ public static TesTaskLog GetOrAddTesTaskLog(this TesTask tesTask)
83130
/// <returns>Last <see cref="TesTaskLog"/></returns>
84131
public static TesTaskLog AddTesTaskLog(this TesTask tesTask)
85132
{
86-
tesTask.Logs ??= new();
133+
tesTask.Logs ??= [];
87134
tesTask.Logs.Add(new());
88135

89136
return tesTask.Logs.Last();
@@ -114,7 +161,7 @@ public static TesExecutorLog GetOrAddExecutorLog(this TesTaskLog tesTaskLog)
114161
{
115162
if (tesTaskLog.Logs is null || !tesTaskLog.Logs.Any())
116163
{
117-
tesTaskLog.Logs = new() { new() };
164+
tesTaskLog.Logs = [new()];
118165
}
119166

120167
return tesTaskLog.Logs.Last();

src/Tes/Models/TesTask.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public TesTask()
116116
/// <returns>Valid TES task ID</returns>
117117
public string CreateId()
118118
{
119-
var tesTaskIdPrefix = WorkflowId is not null && Guid.TryParse(WorkflowId, out _) ? $"{WorkflowId.Substring(0, 8)}_" : string.Empty;
119+
var tesTaskIdPrefix = WorkflowId is not null && Guid.TryParse(WorkflowId, out _) ? $"{WorkflowId[..8]}_" : string.Empty;
120120
return $"{tesTaskIdPrefix}{Guid.NewGuid():N}";
121121
}
122122

@@ -125,7 +125,7 @@ public string CreateId()
125125
/// </summary>
126126
/// <param name="id">TesTask ID</param>
127127
/// <returns>True if valid, false if not</returns>
128-
/// <remarks>Letter, digit, _, -, length 32, 36, 41. Supports GUID for backwards compatibility.</remarks>
128+
/// <remarks>Letter, digit, _, -, length 32, 36, 41. Supports dashed GUID for backwards compatibility.</remarks>
129129
public static bool IsValidId(string id)
130130
{
131131
return (id.Length == 32 || id.Length == 36 || id.Length == 41) && !id.Any(c => !(char.IsLetterOrDigit(c) || c == '_' || c == '-'));

src/Tes/Models/TesTaskExtended.cs

+16-11
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,20 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Runtime.Serialization;
8-
using System.Text.RegularExpressions;
98
using Tes.Repository;
9+
using Tes.TaskSubmitters;
1010

1111
namespace Tes.Models
1212
{
1313
public partial class TesTask : RepositoryItem<TesTask>
1414
{
15-
private static readonly Regex CromwellTaskInstanceNameRegex = new("(.*):[^:]*:[^:]*");
16-
private static readonly Regex CromwellShardRegex = new(".*:([^:]*):[^:]*");
17-
private static readonly Regex CromwellAttemptRegex = new(".*:([^:]*)");
18-
public static readonly List<TesState> ActiveStates = new List<TesState> {
15+
public static readonly List<TesState> ActiveStates =
16+
[
1917
TesState.QUEUEDEnum,
2018
TesState.RUNNINGEnum,
2119
TesState.PAUSEDEnum,
22-
TesState.INITIALIZINGEnum};
20+
TesState.INITIALIZINGEnum
21+
];
2322

2423
/// <summary>
2524
/// Number of retries attempted
@@ -43,8 +42,14 @@ public partial class TesTask : RepositoryItem<TesTask>
4342
/// <summary>
4443
/// Top-most parent workflow ID from the workflow engine
4544
/// </summary>
46-
[DataMember(Name = "workflow_id")]
47-
public string WorkflowId { get; set; }
45+
[IgnoreDataMember]
46+
public string WorkflowId => TaskSubmitter?.WorkflowId;
47+
48+
/// <summary>
49+
/// Workflow engine task metadata
50+
/// </summary>
51+
[DataMember(Name = "task_submitter")]
52+
public TaskSubmitter TaskSubmitter { get; set; }
4853

4954
/// <summary>
5055
/// Assigned Azure Batch PoolId
@@ -74,19 +79,19 @@ public partial class TesTask : RepositoryItem<TesTask>
7479
/// Cromwell task description without shard and attempt numbers
7580
/// </summary>
7681
[IgnoreDataMember]
77-
public string CromwellTaskInstanceName => this.Description == null ? null : CromwellTaskInstanceNameRegex.Match(this.Description).Groups[1].Value;
82+
public string CromwellTaskInstanceName => (TaskSubmitter as CromwellTaskSubmitter)?.CromwellTaskInstanceName;
7883

7984
/// <summary>
8085
/// Cromwell shard number
8186
/// </summary>
8287
[IgnoreDataMember]
83-
public int? CromwellShard => this.Description == null ? null : (int.TryParse(CromwellShardRegex.Match(this.Description).Groups[1].Value, out var result) ? result : null);
88+
public int? CromwellShard => (TaskSubmitter as CromwellTaskSubmitter)?.CromwellShard;
8489

8590
/// <summary>
8691
/// Cromwell attempt number
8792
/// </summary>
8893
[IgnoreDataMember]
89-
public int? CromwellAttempt => this.Description == null ? null : (int.TryParse(CromwellAttemptRegex.Match(this.Description).Groups[1].Value, out var result) ? result : null);
94+
public int? CromwellAttempt => (TaskSubmitter as CromwellTaskSubmitter)?.CromwellAttempt;
9095

9196
public bool IsActiveState()
9297
{

src/Tes/Models/TesTaskLog.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public TesTaskLog()
3535
/// </summary>
3636
/// <value>Logs for each executor</value>
3737
[DataMember(Name = "logs")]
38-
public List<TesExecutorLog> Logs { get; set; } = new List<TesExecutorLog>();
38+
public List<TesExecutorLog> Logs { get; set; } = [];
3939

4040
/// <summary>
4141
/// Arbitrary logging metadata included by the implementation.
@@ -63,14 +63,14 @@ public TesTaskLog()
6363
/// </summary>
6464
/// <value>Information about all output files. Directory outputs are flattened into separate items.</value>
6565
[DataMember(Name = "outputs")]
66-
public List<TesOutputFileLog> Outputs { get; set; } = new List<TesOutputFileLog>();
66+
public List<TesOutputFileLog> Outputs { get; set; } = [];
6767

6868
/// <summary>
6969
/// System logs are any logs the system decides are relevant, which are not tied directly to an Executor process. Content is implementation specific: format, size, etc. System logs may be collected here to provide convenient access. For example, the system may include the name of the host where the task is executing, an error message that caused a SYSTEM_ERROR state (e.g. disk is full), etc. System logs are only included in the FULL task view.
7070
/// </summary>
7171
/// <value>System logs are any logs the system decides are relevant, which are not tied directly to an Executor process. Content is implementation specific: format, size, etc. System logs may be collected here to provide convenient access. For example, the system may include the name of the host where the task is executing, an error message that caused a SYSTEM_ERROR state (e.g. disk is full), etc. System logs are only included in the FULL task view.</value>
7272
[DataMember(Name = "system_logs")]
73-
public List<string> SystemLogs { get; set; } = new List<string>();
73+
public List<string> SystemLogs { get; set; } = [];
7474

7575
/// <summary>
7676
/// Returns the string presentation of the object

src/Tes/Repository/PostgreSqlCachingRepository.cs

+21-15
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public abstract class PostgreSqlCachingRepository<T> : IDisposable where T : cla
2828
.Handle<Npgsql.NpgsqlException>(e => e.IsTransient)
2929
.WaitAndRetryAsync(10, i => TimeSpan.FromSeconds(Math.Pow(2, i)));
3030

31-
private readonly Channel<(T, WriteAction, TaskCompletionSource<T>)> itemsToWrite = Channel.CreateUnbounded<(T, WriteAction, TaskCompletionSource<T>)>();
31+
private record struct WriteItem(T DbItem, WriteAction Action, TaskCompletionSource<T> TaskSource);
32+
private readonly Channel<WriteItem> itemsToWrite = Channel.CreateUnbounded<WriteItem>();
3233
private readonly ConcurrentDictionary<T, object> updatingItems = new(); // Collection of all pending updates to be written, to faciliate detection of simultaneous parallel updates.
3334
private readonly CancellationTokenSource writerWorkerCancellationTokenSource = new();
3435
private readonly Task writerWorkerTask;
@@ -45,8 +46,8 @@ protected enum WriteAction { Add, Update, Delete }
4546
/// Constructor
4647
/// </summary>
4748
/// <param name="hostApplicationLifetime">Used for requesting termination of the current application if the writer task unexpectedly exits.</param>
48-
/// <param name="logger"></param>
49-
/// <param name="cache"></param>
49+
/// <param name="logger">Logging interface.</param>
50+
/// <param name="cache">Memory cache for fast access to active items.</param>
5051
/// <exception cref="System.Diagnostics.UnreachableException"></exception>
5152
protected PostgreSqlCachingRepository(Microsoft.Extensions.Hosting.IHostApplicationLifetime hostApplicationLifetime, ILogger logger = default, ICache<T> cache = default)
5253
{
@@ -61,17 +62,16 @@ protected PostgreSqlCachingRepository(Microsoft.Extensions.Hosting.IHostApplicat
6162

6263
if (task.Status == TaskStatus.Faulted)
6364
{
64-
Console.WriteLine($"Repository WriterWorkerAsync failed unexpectedly with: {task.Exception.Message}.");
6565
Logger.LogCritical(task.Exception, "Repository WriterWorkerAsync failed unexpectedly with: {ErrorMessage}.", task.Exception.Message);
66+
Console.WriteLine($"Repository WriterWorkerAsync failed unexpectedly with: {task.Exception.Message}.");
6667
}
6768

68-
const string errMessage = "Repository WriterWorkerAsync unexpectedly completed. The TES application will now be stopped.";
69+
const string errMessage = "Repository WriterWorkerAsync unexpectedly completed. The service will now be stopped.";
6970
Logger.LogCritical(errMessage);
7071
Console.WriteLine(errMessage);
7172

7273
await Task.Delay(TimeSpan.FromSeconds(40)); // Give the logger time to flush; default flush is 30s
7374
hostApplicationLifetime?.StopApplication();
74-
return;
7575
}, TaskContinuationOptions.NotOnCanceled)
7676
.ContinueWith(task => Logger.LogInformation("The repository WriterWorkerAsync ended normally"), TaskContinuationOptions.OnlyOnCanceled);
7777
}
@@ -148,7 +148,7 @@ protected Task<T> AddUpdateOrRemoveItemInDbAsync(T item, WriteAction action, Can
148148
}
149149
}
150150

151-
if (!itemsToWrite.Writer.TryWrite((item, action, source)))
151+
if (!itemsToWrite.Writer.TryWrite(new(item, action, source)))
152152
{
153153
throw new InvalidOperationException("Failed to TryWrite to _itemsToWrite channel.");
154154
}
@@ -172,7 +172,7 @@ Task<T> RemoveUpdatingItem(Task<T> task)
172172
/// </summary>
173173
private async Task WriterWorkerAsync(CancellationToken cancellationToken)
174174
{
175-
var list = new List<(T, WriteAction, TaskCompletionSource<T>)>();
175+
var list = new List<WriteItem>();
176176

177177
await foreach (var itemToWrite in itemsToWrite.Reader.ReadAllAsync(cancellationToken))
178178
{
@@ -191,7 +191,7 @@ private async Task WriterWorkerAsync(CancellationToken cancellationToken)
191191
// If cancellation is requested, do not write any more items
192192
}
193193

194-
private async ValueTask WriteItemsAsync(IList<(T DbItem, WriteAction Action, TaskCompletionSource<T> TaskSource)> dbItems, CancellationToken cancellationToken)
194+
private async ValueTask WriteItemsAsync(IList<WriteItem> dbItems, CancellationToken cancellationToken)
195195
{
196196
cancellationToken.ThrowIfCancellationRequested();
197197

@@ -208,19 +208,25 @@ private async ValueTask WriteItemsAsync(IList<(T DbItem, WriteAction Action, Tas
208208
dbContext.UpdateRange(dbItems.Where(e => WriteAction.Update.Equals(e.Action)).Select(e => e.DbItem));
209209
dbContext.RemoveRange(dbItems.Where(e => WriteAction.Delete.Equals(e.Action)).Select(e => e.DbItem));
210210
await asyncPolicy.ExecuteAsync(dbContext.SaveChangesAsync, cancellationToken);
211+
var action = ActionOnSuccess();
212+
OperateOnAll(dbItems, action);
211213
}
212214
catch (Exception ex)
213215
{
214216
// It doesn't matter which item the failure was for, we will fail all items in this round.
215217
// TODO: are there exceptions Postgre will send us that will tell us which item(s) failed or alternately succeeded?
216-
FailAll(dbItems.Select(e => e.TaskSource), ex);
217-
return;
218+
var action = ActionOnFailure(ex);
219+
OperateOnAll(dbItems, action);
218220
}
219221

220-
_ = Parallel.ForEach(dbItems, e => e.TaskSource.TrySetResult(e.DbItem));
222+
static void OperateOnAll(IEnumerable<WriteItem> sources, Action<WriteItem> action)
223+
=> _ = Parallel.ForEach(sources, e => action(e));
224+
225+
static Action<WriteItem> ActionOnFailure(Exception ex) =>
226+
e => _ = e.TaskSource.TrySetException(new AggregateException(Enumerable.Empty<Exception>().Append(ex)));
221227

222-
static void FailAll(IEnumerable<TaskCompletionSource<T>> sources, Exception ex)
223-
=> _ = Parallel.ForEach(sources, s => s.TrySetException(new AggregateException(Enumerable.Empty<Exception>().Append(ex))));
228+
static Action<WriteItem> ActionOnSuccess() =>
229+
e => _ = e.TaskSource.TrySetResult(e.DbItem);
224230
}
225231

226232
protected virtual void Dispose(bool disposing)
@@ -233,7 +239,7 @@ protected virtual void Dispose(bool disposing)
233239

234240
try
235241
{
236-
writerWorkerTask.Wait();
242+
writerWorkerTask.GetAwaiter().GetResult();
237243
}
238244
catch (AggregateException aex) when (aex?.InnerException is TaskCanceledException ex && writerWorkerCancellationTokenSource.Token == ex.CancellationToken)
239245
{ } // Expected return from Wait().

src/Tes/Repository/TesDbContext.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public TesDbContext()
2121

2222
public TesDbContext(NpgsqlDataSource dataSource, Action<NpgsqlDbContextOptionsBuilder> contextOptionsBuilder = default)
2323
{
24-
ArgumentNullException.ThrowIfNull(dataSource, nameof(dataSource));
24+
ArgumentNullException.ThrowIfNull(dataSource);
2525
DataSource = dataSource;
2626
ContextOptionsBuilder = contextOptionsBuilder;
2727
}

0 commit comments

Comments
 (0)