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

Commit 6ade966

Browse files
author
Scott Bommarito
authored
Status Aggregator - Monitor manual status changes from multiple storages (#537)
1 parent aead8dc commit 6ade966

33 files changed

Lines changed: 1395 additions & 68 deletions

src/NuGet.Jobs.Common/Configuration/JobArgumentNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ public static class JobArgumentNames
120120

121121
// Arguments specific to StatusAggregator
122122
public const string StatusStorageAccount = "StatusStorageAccount";
123+
public const string StatusStorageAccountSecondary = "StatusStorageAccountSecondary";
123124
public const string StatusContainerName = "StatusContainerName";
124125
public const string StatusTableName = "StatusTableName";
125126
public const string StatusEnvironment = "StatusEnvironment";

src/StatusAggregator/Cursor.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,35 +24,35 @@ public Cursor(
2424

2525
private readonly ILogger<Cursor> _logger;
2626

27-
public async Task<DateTime> Get()
27+
public async Task<DateTime> Get(string name)
2828
{
29-
using (_logger.Scope("Fetching cursor."))
29+
using (_logger.Scope("Fetching cursor with name {CursorName}.", name))
3030
{
31-
var cursor = await _table.Retrieve<CursorEntity>(
32-
CursorEntity.DefaultPartitionKey, CursorEntity.DefaultRowKey);
31+
var cursor = await _table.RetrieveAsync<CursorEntity>(
32+
CursorEntity.DefaultPartitionKey, name);
3333

3434
DateTime value;
3535
if (cursor == null)
3636
{
3737
// If we can't find a cursor, the job is likely uninitialized, so start at the beginning of time.
3838
value = DateTime.MinValue;
39-
_logger.LogInformation("Could not fetch cursor, reinitializing cursor at {Cursor}.", value);
39+
_logger.LogInformation("Could not fetch cursor, reinitializing cursor at {CursorValue}.", value);
4040
}
4141
else
4242
{
4343
value = cursor.Value;
44-
_logger.LogInformation("Fetched cursor with value {Cursor}.", value);
44+
_logger.LogInformation("Fetched cursor with value {CursorValue}.", value);
4545
}
4646

4747
return value;
4848
}
4949
}
5050

51-
public Task Set(DateTime value)
51+
public Task Set(string name, DateTime value)
5252
{
53-
using (_logger.Scope("Updating cursor to {Cursor}.", value))
53+
using (_logger.Scope("Updating cursor with name {CursorName} to {CursorValue}.", name, value))
5454
{
55-
var cursorEntity = new CursorEntity(value);
55+
var cursorEntity = new CursorEntity(name, value);
5656
return _table.InsertOrReplaceAsync(cursorEntity);
5757
}
5858
}

src/StatusAggregator/ICursor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace StatusAggregator
1111
/// </summary>
1212
public interface ICursor
1313
{
14-
Task<DateTime> Get();
15-
Task Set(DateTime value);
14+
Task<DateTime> Get(string name);
15+
Task Set(string name, DateTime value);
1616
}
1717
}

src/StatusAggregator/Job.cs

Lines changed: 99 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@
44
using System;
55
using System.Collections.Generic;
66
using System.ComponentModel.Design;
7+
using System.Linq;
78
using System.Security.Cryptography.X509Certificates;
89
using System.Threading.Tasks;
10+
using Autofac;
11+
using Autofac.Core;
12+
using Autofac.Extensions.DependencyInjection;
913
using Microsoft.Extensions.DependencyInjection;
1014
using Microsoft.WindowsAzure.Storage;
15+
using Microsoft.WindowsAzure.Storage.Blob;
1116
using Newtonsoft.Json.Linq;
1217
using NuGet.Jobs;
1318
using NuGet.Services.Incidents;
19+
using NuGet.Services.Status.Table.Manual;
20+
using StatusAggregator.Manual;
1421
using StatusAggregator.Parse;
1522
using StatusAggregator.Table;
1623

@@ -26,10 +33,14 @@ public override void Init(IServiceContainer serviceContainer, IDictionary<string
2633

2734
AddLogging(serviceCollection);
2835
AddConfiguration(serviceCollection, jobArgsDictionary);
29-
AddStorage(serviceCollection);
3036
AddServices(serviceCollection);
3137

32-
_serviceProvider = serviceCollection.BuildServiceProvider();
38+
var containerBuilder = new ContainerBuilder();
39+
containerBuilder.Populate(serviceCollection);
40+
41+
AddStorage(containerBuilder);
42+
43+
_serviceProvider = new AutofacServiceProvider(containerBuilder.Build());
3344
}
3445

3546
public override Task Run()
@@ -48,11 +59,23 @@ private static void AddServices(IServiceCollection serviceCollection)
4859
serviceCollection.AddTransient<IIncidentFactory, IncidentFactory>();
4960
AddParsing(serviceCollection);
5061
serviceCollection.AddTransient<IIncidentUpdater, IncidentUpdater>();
62+
AddManualStatusChangeHandling(serviceCollection);
5163
serviceCollection.AddTransient<IStatusUpdater, StatusUpdater>();
5264
serviceCollection.AddTransient<IStatusExporter, StatusExporter>();
5365
serviceCollection.AddTransient<StatusAggregator>();
5466
}
5567

68+
private static void AddManualStatusChangeHandling(IServiceCollection serviceCollection)
69+
{
70+
serviceCollection.AddTransient<IManualStatusChangeHandler<AddStatusEventManualChangeEntity>, AddStatusEventManualChangeHandler>();
71+
serviceCollection.AddTransient<IManualStatusChangeHandler<EditStatusEventManualChangeEntity>, EditStatusEventManualChangeHandler>();
72+
serviceCollection.AddTransient<IManualStatusChangeHandler<DeleteStatusEventManualChangeEntity>, DeleteStatusEventManualChangeHandler>();
73+
serviceCollection.AddTransient<IManualStatusChangeHandler<AddStatusMessageManualChangeEntity>, AddStatusMessageManualChangeHandler>();
74+
serviceCollection.AddTransient<IManualStatusChangeHandler<EditStatusMessageManualChangeEntity>, EditStatusMessageManualChangeHandler>();
75+
serviceCollection.AddTransient<IManualStatusChangeHandler<DeleteStatusMessageManualChangeEntity>, DeleteStatusMessageManualChangeHandler>();
76+
serviceCollection.AddTransient<IManualStatusChangeHandler, ManualStatusChangeHandler>();
77+
}
78+
5679
private static void AddParsing(IServiceCollection serviceCollection)
5780
{
5881
serviceCollection.AddTransient<IIncidentParsingFilter, SeverityFilter>();
@@ -66,31 +89,78 @@ private static void AddParsing(IServiceCollection serviceCollection)
6689
serviceCollection.AddTransient<IAggregateIncidentParser, AggregateIncidentParser>();
6790
}
6891

69-
private static void AddStorage(IServiceCollection serviceCollection)
92+
private const string StorageAccountNameParameter = "name";
93+
94+
private const string PrimaryStorageAccountName = "Primary";
95+
private const string SecondaryStorageAccountName = "Secondary";
96+
97+
private static void AddStorage(ContainerBuilder containerBuilder)
98+
{
99+
var statusStorageConnectionBuilders = new StatusStorageConnectionBuilder[]
100+
{
101+
new StatusStorageConnectionBuilder(PrimaryStorageAccountName, configuration => configuration.StorageAccount),
102+
new StatusStorageConnectionBuilder(SecondaryStorageAccountName, configuration => configuration.StorageAccountSecondary)
103+
};
104+
105+
// Add all storages to the container by name.
106+
foreach (var statusStorageConnectionBuilder in
107+
// Register the primary storage last, so it will be the default and will be used unless a specific storage is referenced.
108+
statusStorageConnectionBuilders.OrderBy(b => b.Name == PrimaryStorageAccountName))
109+
{
110+
var name = statusStorageConnectionBuilder.Name;
111+
112+
containerBuilder
113+
.Register(ctx => GetCloudStorageAccount(ctx, statusStorageConnectionBuilder))
114+
.As<CloudStorageAccount>()
115+
.Named<CloudStorageAccount>(name);
116+
117+
containerBuilder
118+
.Register(ctx =>
119+
{
120+
var storageAccount = ctx.ResolveNamed<CloudStorageAccount>(name);
121+
return GetTableWrapper(ctx, storageAccount);
122+
})
123+
.As<ITableWrapper>()
124+
.Named<ITableWrapper>(name);
125+
126+
containerBuilder
127+
.Register(ctx =>
128+
{
129+
var storageAccount = ctx.ResolveNamed<CloudStorageAccount>(name);
130+
return GetCloudBlobContainer(ctx, storageAccount);
131+
})
132+
.As<CloudBlobContainer>()
133+
.Named<CloudBlobContainer>(name);
134+
135+
// We need to listen to manual status change updates from each storage.
136+
containerBuilder
137+
.RegisterType<ManualStatusChangeUpdater>()
138+
.WithParameter(new NamedParameter(StorageAccountNameParameter, name))
139+
.WithParameter(new ResolvedParameter(
140+
(pi, ctx) => pi.ParameterType == typeof(ITableWrapper),
141+
(pi, ctx) => ctx.ResolveNamed<ITableWrapper>(name)))
142+
.As<IManualStatusChangeUpdater>()
143+
.Named<IManualStatusChangeUpdater>(name);
144+
}
145+
}
146+
147+
private static CloudStorageAccount GetCloudStorageAccount(IComponentContext ctx, StatusStorageConnectionBuilder statusStorageConnectionBuilder)
148+
{
149+
var configuration = ctx.Resolve<StatusAggregatorConfiguration>();
150+
return CloudStorageAccount.Parse(statusStorageConnectionBuilder.GetConnectionString(configuration));
151+
}
152+
153+
private static ITableWrapper GetTableWrapper(IComponentContext ctx, CloudStorageAccount storageAccount)
154+
{
155+
var configuration = ctx.Resolve<StatusAggregatorConfiguration>();
156+
return new TableWrapper(storageAccount, configuration.TableName);
157+
}
158+
159+
private static CloudBlobContainer GetCloudBlobContainer(IComponentContext ctx, CloudStorageAccount storageAccount)
70160
{
71-
serviceCollection.AddSingleton(
72-
serviceProvider =>
73-
{
74-
var configuration = serviceProvider.GetRequiredService<StatusAggregatorConfiguration>();
75-
return CloudStorageAccount.Parse(configuration.StorageAccount);
76-
});
77-
78-
serviceCollection.AddSingleton<ITableWrapper>(
79-
serviceProvider =>
80-
{
81-
var storageAccount = serviceProvider.GetRequiredService<CloudStorageAccount>();
82-
var configuration = serviceProvider.GetRequiredService<StatusAggregatorConfiguration>();
83-
return new TableWrapper(storageAccount, configuration.TableName);
84-
});
85-
86-
serviceCollection.AddSingleton(
87-
serviceProvider =>
88-
{
89-
var storageAccount = serviceProvider.GetRequiredService<CloudStorageAccount>();
90-
var blobClient = storageAccount.CreateCloudBlobClient();
91-
var configuration = serviceProvider.GetRequiredService<StatusAggregatorConfiguration>();
92-
return blobClient.GetContainerReference(configuration.ContainerName);
93-
});
161+
var blobClient = storageAccount.CreateCloudBlobClient();
162+
var configuration = ctx.Resolve<StatusAggregatorConfiguration>();
163+
return blobClient.GetContainerReference(configuration.ContainerName);
94164
}
95165

96166
private const int _defaultEventStartMessageDelayMinutes = 15;
@@ -101,8 +171,10 @@ private static void AddConfiguration(IServiceCollection serviceCollection, IDict
101171
{
102172
var configuration = new StatusAggregatorConfiguration()
103173
{
104-
StorageAccount =
174+
StorageAccount =
105175
JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.StatusStorageAccount),
176+
StorageAccountSecondary =
177+
JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.StatusStorageAccountSecondary),
106178
ContainerName =
107179
JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.StatusContainerName),
108180
TableName =

src/StatusAggregator/LogEvents.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ namespace StatusAggregator
55
public static class LogEvents
66
{
77
public static EventId RegexFailure = new EventId(400, "Failed to parse incident using Regex.");
8+
public static EventId ManualChangeFailure = new EventId(401, "Failed to apply a manual change.");
89
}
910
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using NuGet.Services.Status.Table;
5+
using NuGet.Services.Status.Table.Manual;
6+
using StatusAggregator.Table;
7+
using System;
8+
using System.Threading.Tasks;
9+
10+
namespace StatusAggregator.Manual
11+
{
12+
public class AddStatusEventManualChangeHandler : IManualStatusChangeHandler<AddStatusEventManualChangeEntity>
13+
{
14+
private readonly ITableWrapper _table;
15+
16+
public AddStatusEventManualChangeHandler(
17+
ITableWrapper table)
18+
{
19+
_table = table ?? throw new ArgumentNullException(nameof(table));
20+
}
21+
22+
public async Task Handle(AddStatusEventManualChangeEntity entity)
23+
{
24+
var time = entity.Timestamp.UtcDateTime;
25+
26+
var eventEntity = new EventEntity(
27+
entity.EventAffectedComponentPath,
28+
entity.EventAffectedComponentStatus,
29+
time,
30+
entity.EventIsActive ? (DateTime?)null : time);
31+
32+
var messageEntity = new MessageEntity(
33+
eventEntity,
34+
time,
35+
entity.MessageContents);
36+
37+
await _table.InsertAsync(messageEntity);
38+
await _table.InsertAsync(eventEntity);
39+
}
40+
}
41+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using NuGet.Services.Status.Table;
5+
using NuGet.Services.Status.Table.Manual;
6+
using StatusAggregator.Table;
7+
using System;
8+
using System.Threading.Tasks;
9+
10+
namespace StatusAggregator.Manual
11+
{
12+
public class AddStatusMessageManualChangeHandler : IManualStatusChangeHandler<AddStatusMessageManualChangeEntity>
13+
{
14+
private readonly ITableWrapper _table;
15+
16+
public AddStatusMessageManualChangeHandler(
17+
ITableWrapper table)
18+
{
19+
_table = table ?? throw new ArgumentNullException(nameof(table));
20+
}
21+
22+
public async Task Handle(AddStatusMessageManualChangeEntity entity)
23+
{
24+
var time = entity.Timestamp.UtcDateTime;
25+
26+
var eventRowKey = EventEntity.GetRowKey(entity.EventAffectedComponentPath, entity.EventStartTime);
27+
var messageEntity = new MessageEntity(eventRowKey, time, entity.MessageContents);
28+
29+
var eventEntity = await _table.RetrieveAsync<EventEntity>(EventEntity.DefaultPartitionKey, eventRowKey);
30+
if (eventEntity == null)
31+
{
32+
throw new ArgumentException("Cannot create a message for an event that does not exist.");
33+
}
34+
35+
await _table.InsertAsync(messageEntity);
36+
if (ManualStatusChangeUtility.UpdateEventIsActive(eventEntity, entity.EventIsActive, time))
37+
{
38+
await _table.ReplaceAsync(eventEntity);
39+
}
40+
}
41+
}
42+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using NuGet.Services.Status.Table;
5+
using NuGet.Services.Status.Table.Manual;
6+
using StatusAggregator.Table;
7+
using System;
8+
using System.Threading.Tasks;
9+
10+
namespace StatusAggregator.Manual
11+
{
12+
public class DeleteStatusEventManualChangeHandler : IManualStatusChangeHandler<DeleteStatusEventManualChangeEntity>
13+
{
14+
private readonly ITableWrapper _table;
15+
16+
public DeleteStatusEventManualChangeHandler(
17+
ITableWrapper table)
18+
{
19+
_table = table ?? throw new ArgumentNullException(nameof(table));
20+
}
21+
22+
public Task Handle(DeleteStatusEventManualChangeEntity entity)
23+
{
24+
var eventRowKey = EventEntity.GetRowKey(entity.EventAffectedComponentPath, entity.EventStartTime);
25+
return _table.DeleteAsync(EventEntity.DefaultPartitionKey, eventRowKey);
26+
}
27+
}
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using NuGet.Services.Status.Table;
5+
using NuGet.Services.Status.Table.Manual;
6+
using StatusAggregator.Table;
7+
using System;
8+
using System.Threading.Tasks;
9+
10+
namespace StatusAggregator.Manual
11+
{
12+
public class DeleteStatusMessageManualChangeHandler : IManualStatusChangeHandler<DeleteStatusMessageManualChangeEntity>
13+
{
14+
private readonly ITableWrapper _table;
15+
16+
public DeleteStatusMessageManualChangeHandler(
17+
ITableWrapper table)
18+
{
19+
_table = table ?? throw new ArgumentNullException(nameof(table));
20+
}
21+
22+
public Task Handle(DeleteStatusMessageManualChangeEntity entity)
23+
{
24+
var eventRowKey = EventEntity.GetRowKey(entity.EventAffectedComponentPath, entity.EventStartTime);
25+
var messageRowKey = MessageEntity.GetRowKey(eventRowKey, entity.MessageTimestamp);
26+
return _table.DeleteAsync(MessageEntity.DefaultPartitionKey, messageRowKey);
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)