Skip to content

Commit ffb488e

Browse files
committed
Merge branch 'master' into zombie_queues
# Conflicts: # src/ServiceControl.Audit.Persistence.Tests/AuditCountingTests.cs
2 parents 4bb78af + 4bbd920 commit ffb488e

68 files changed

Lines changed: 977 additions & 493 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/coding-and-design-guidelines.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ There are a few things that are still registered using convention. Note that the
4040
Additionally, because NServiceBus does a type-scan at startup it will automatically register any implementations of `Feature` and `IHandleMessage<>`. We have chosen to leave this alone as we would be fighting with NServiceBus in order to turn this off.
4141

4242

43+
## Prefer explicit persistence operations
44+
45+
Use a direct data-store method when all inputs for a persistence operation fit in one method call. The method should own and dispose its EF Core scope/context or RavenDB session, accept a `CancellationToken`, and commit before returning. Returned entities are detached snapshots; callers must not be required to mutate tracked entities as an implicit persistence command.
46+
47+
Atomic operations that can encounter concurrency conflicts should document their provider guarantees and translate expected provider exceptions into explicit domain outcomes. For example, a unique-key or optimistic-concurrency conflict should not escape when contention is part of the operation's normal contract.
48+
49+
Reserve a specialized unit of work for cases where a caller genuinely composes several writes into one atomic batch. New unit-of-work APIs should consistently provide:
50+
51+
- an `I...UnitOfWorkFactory`;
52+
- a `StartNew` factory method;
53+
- a `Complete(CancellationToken)` commit method;
54+
- `IAsyncDisposable` lifetime ownership;
55+
- explicit operation-recording methods rather than mutation of tracked return values;
56+
- documented commit, abandon, repeated-completion, and concurrency behavior.
57+
58+
Do not introduce generic `IDataSessionManager`-style abstractions or persistence managers with hidden call-order protocols. During review, prefer one explicit store operation unless caller-composed atomicity requires a unit of work.
59+
4360
## Avoid property injection
4461

4562
Although the Autofac container can be configured to allow property injection, we prefer to avoid it. There is no way to specify property injection using the Microsoft DI abstractions, and the default `IServiceProvider` implementation does not support it. Where possible, use constructor injection instead.

‎src/Directory.Packages.props‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(RuntimeFrameworkVersion)" />
2323
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="$(RuntimeFrameworkVersion)" />
2424
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(RuntimeFrameworkVersion)" />
25+
<PackageVersion Include="Microsoft.Data.SqlClient.Extensions.Azure" Version="7.0.2" />
2526
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="$(RuntimeFrameworkVersion)" />
2627
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="$(RuntimeFrameworkVersion)" />
2728
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="$(RuntimeFrameworkVersion)" />
@@ -77,10 +78,10 @@
7778
<PackageVersion Include="PropertyChanging.Fody" Version="1.31.0" />
7879
<PackageVersion Include="PublicApiGenerator" Version="11.5.4" />
7980
<PackageVersion Include="RavenDB.Embedded" Version="6.2.17" />
80-
<PackageVersion Include="Testcontainers.Azurite" Version="4.13.0" />
81-
<PackageVersion Include="Testcontainers.LocalStack" Version="4.13.0" />
82-
<PackageVersion Include="Testcontainers.MsSql" Version="4.13.0" />
83-
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.13.0" />
81+
<PackageVersion Include="Testcontainers.Azurite" Version="4.14.0" />
82+
<PackageVersion Include="Testcontainers.LocalStack" Version="4.14.0" />
83+
<PackageVersion Include="Testcontainers.MsSql" Version="4.14.0" />
84+
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.14.0" />
8485
<PackageVersion Include="ReactiveUI.WPF" Version="22.3.1" />
8586
<PackageVersion Include="Seq.Extensions.Logging" Version="9.0.0" />
8687
<PackageVersion Include="ServiceControl.Contracts" Version="5.1.0" />

‎src/Particular.LicensingComponent.UnitTests/.editorconfig‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,4 @@
22

33
# Justification: Test project
44
dotnet_diagnostic.CA2007.severity = none
5-
6-
# Cancellation analyzer debt. These fire because Particular.Analyzers is no longer pinned to 0.9.0.
7-
# They are scheduled work, not accepted exceptions: remove a line once this project has no
8-
# violations of that rule left, and never add a rule back to this list.
95
dotnet_diagnostic.PS0018.severity = none

‎src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Particular.LicensingComponent.UnitTests;
22

33
using System;
4+
using System.Collections.Concurrent;
45
using System.Collections.Generic;
56
using System.Collections.ObjectModel;
67
using System.Linq;
@@ -13,6 +14,7 @@ namespace Particular.LicensingComponent.UnitTests;
1314
using Infrastructure;
1415
using MonitoringThroughput;
1516
using NUnit.Framework;
17+
using Persistence;
1618
using ServiceControl.Transports.BrokerThroughput;
1719

1820
[TestFixture]
@@ -159,6 +161,110 @@ public async Task Should_return_error_monitoring_connection_and_diagnostics_if_n
159161
Approver.Verify(connectionSettingsResult.Diagnostics);
160162
}
161163

164+
[Test]
165+
public async Task Should_record_throughput_for_every_endpoint_in_the_message()
166+
{
167+
// Arrange
168+
var dataStore = new YieldingLicensingDataStore();
169+
var monitoringService = new MonitoringService(dataStore);
170+
171+
string[] endpointNames = [.. Enumerable.Range(1, 20).Select(i => $"Endpoint{i}")];
172+
var message = new RecordEndpointThroughputData
173+
{
174+
StartDateTime = DateTime.UtcNow.AddMinutes(-5),
175+
EndDateTime = DateTime.UtcNow,
176+
EndpointThroughputData = [.. endpointNames.Select(name => new EndpointThroughputData { Name = name, Throughput = 15 })]
177+
};
178+
179+
// Act
180+
byte[] messageBytes = JsonSerializer.SerializeToUtf8Bytes(message);
181+
await monitoringService.RecordMonitoringThroughput(messageBytes);
182+
183+
// Assert
184+
string[] endpointsWithoutThroughput = [.. endpointNames.Where(name => !dataStore.RecordedThroughput.ContainsKey(name))];
185+
string[] unsavedEndpoints = [.. endpointNames.Where(name => !dataStore.SavedEndpoints.ContainsKey(name))];
186+
187+
using (Assert.EnterMultipleScope())
188+
{
189+
Assert.That(unsavedEndpoints, Is.Empty,
190+
$"{unsavedEndpoints.Length} of {endpointNames.Length} endpoints were not saved when RecordMonitoringThroughput returned: {string.Join(", ", unsavedEndpoints)}");
191+
Assert.That(endpointsWithoutThroughput, Is.Empty,
192+
$"{endpointsWithoutThroughput.Length} of {endpointNames.Length} endpoints had no throughput recorded when RecordMonitoringThroughput returned: {string.Join(", ", endpointsWithoutThroughput)}");
193+
}
194+
195+
Assert.That(dataStore.RecordedThroughput.Values, Is.All.EqualTo(15L), "Expected a throughput of 15 for every endpoint");
196+
}
197+
198+
// Records only after suspending, so any write the service starts without awaiting is still
199+
// outstanding when RecordMonitoringThroughput returns.
200+
class YieldingLicensingDataStore : ILicensingDataStore
201+
{
202+
public ConcurrentDictionary<string, Endpoint> SavedEndpoints { get; } = new();
203+
204+
public ConcurrentDictionary<string, long> RecordedThroughput { get; } = new();
205+
206+
public async Task<Endpoint> GetEndpoint(EndpointIdentifier id, CancellationToken cancellationToken = default)
207+
{
208+
await Task.Yield();
209+
return SavedEndpoints.TryGetValue(id.Name, out Endpoint endpoint) ? endpoint : null;
210+
}
211+
212+
public async Task SaveEndpoint(Endpoint endpoint, CancellationToken cancellationToken = default)
213+
{
214+
await Task.Yield();
215+
SavedEndpoints[endpoint.Id.Name] = endpoint;
216+
}
217+
218+
public async Task RecordEndpointThroughput(string endpointName, ThroughputSource throughputSource,
219+
IList<EndpointDailyThroughput> throughput, CancellationToken cancellationToken = default)
220+
{
221+
await Task.Yield();
222+
RecordedThroughput[endpointName] = throughput.Sum(t => t.MessageCount);
223+
}
224+
225+
public Task<IEnumerable<Endpoint>> GetAllEndpoints(bool includePlatformEndpoints, CancellationToken cancellationToken = default) =>
226+
throw new NotSupportedException();
227+
228+
public Task<IEnumerable<(EndpointIdentifier Id, Endpoint Endpoint)>> GetEndpoints(IList<EndpointIdentifier> endpointIds, CancellationToken cancellationToken = default) =>
229+
throw new NotSupportedException();
230+
231+
public Task<IDictionary<string, IEnumerable<ThroughputData>>> GetEndpointThroughputByQueueName(IList<string> queueNames, CancellationToken cancellationToken = default) =>
232+
throw new NotSupportedException();
233+
234+
public Task UpdateUserIndicatorOnEndpoints(List<UpdateUserIndicator> userIndicatorUpdates, CancellationToken cancellationToken = default) =>
235+
throw new NotSupportedException();
236+
237+
public Task<bool> IsThereThroughputForLastXDays(int days, CancellationToken cancellationToken = default) =>
238+
throw new NotSupportedException();
239+
240+
public Task<bool> IsThereThroughputForLastXDaysForSource(int days, ThroughputSource throughputSource, bool includeToday, CancellationToken cancellationToken = default) =>
241+
throw new NotSupportedException();
242+
243+
public Task<BrokerMetadata> GetBrokerMetadata(CancellationToken cancellationToken = default) =>
244+
throw new NotSupportedException();
245+
246+
public Task SaveBrokerMetadata(BrokerMetadata brokerMetadata, CancellationToken cancellationToken = default) =>
247+
throw new NotSupportedException();
248+
249+
public Task<AuditServiceMetadata> GetAuditServiceMetadata(CancellationToken cancellationToken = default) =>
250+
throw new NotSupportedException();
251+
252+
public Task SaveAuditServiceMetadata(AuditServiceMetadata auditServiceMetadata, CancellationToken cancellationToken = default) =>
253+
throw new NotSupportedException();
254+
255+
public Task<List<string>> GetReportMasks(CancellationToken cancellationToken = default) =>
256+
throw new NotSupportedException();
257+
258+
public Task SaveReportMasks(List<string> reportMasks, CancellationToken cancellationToken = default) =>
259+
throw new NotSupportedException();
260+
261+
public Task<LicensedEndpointDetails> GetLicensedEndpointDetails(CancellationToken cancellationToken = default) =>
262+
throw new NotSupportedException();
263+
264+
public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default) =>
265+
throw new NotSupportedException();
266+
}
267+
162268
class BrokerThroughputQuery_WithSanitization : IBrokerThroughputQuery
163269
{
164270
public Dictionary<string, string> Data => throw new NotImplementedException();

‎src/Particular.LicensingComponent/MonitoringThroughput/MonitoringService.cs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ public async Task RecordMonitoringThroughput(byte[] throughputMessage, Cancellat
1818
message = await JsonSerializer.DeserializeAsync<RecordEndpointThroughputData>(stream, cancellationToken: cancellationToken);
1919
}
2020

21-
if (message != null && message.EndpointThroughputData != null)
21+
if (message is not null && message.EndpointThroughputData is { } endpointThroughputData)
2222
{
23-
Debug.WriteLine($"Throughput data from {message.StartDateTime:yyyy-MM-dd HH:mm} to {message.EndDateTime:yyyy-MM-dd HH:mm} for {message.EndpointThroughputData?.Length} endpoint(s)");
23+
Debug.WriteLine($"Throughput data from {message.StartDateTime:yyyy-MM-dd HH:mm} to {message.EndDateTime:yyyy-MM-dd HH:mm} for {endpointThroughputData.Length} endpoint(s)");
2424

25-
message.EndpointThroughputData?.ToList().ForEach(async e =>
25+
foreach (var e in endpointThroughputData)
2626
{
2727
var endpoint = await dataStore.GetEndpoint(e.Name, ThroughputSource.Monitoring, cancellationToken);
2828
if (endpoint == null)
@@ -41,7 +41,7 @@ public async Task RecordMonitoringThroughput(byte[] throughputMessage, Cancellat
4141

4242
await dataStore.RecordEndpointThroughput(e.Name, ThroughputSource.Monitoring, [endpointThroughput], cancellationToken);
4343
}
44-
});
44+
}
4545
}
4646
}
4747

‎src/ServiceControl.AcceptanceTesting/.editorconfig‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,3 @@
22

33
# Justification: Usage is from test projects
44
dotnet_diagnostic.CA2007.severity = none
5-
6-
# These types sit on NServiceBus.AcceptanceTesting's extension points, which drive them without a
7-
# CancellationToken: IEndpointSetupTemplate, IComponentBehavior/ComponentRunner, and the scenario
8-
# Done/When callbacks whose delegate shapes the framework fixes. A token added here could only ever
9-
# be CancellationToken.None. Everything else in this project takes and forwards one.
10-
[{EndpointTemplates/*.cs,InfrastructureConfig/*.cs,ScenarioWithEndpointBehaviorExtensions.cs,Sequence.cs,DispatchRawMessages.cs}]
11-
dotnet_diagnostic.PS0013.severity = none
12-
dotnet_diagnostic.PS0018.severity = none

‎src/ServiceControl.AcceptanceTesting/DispatchRawMessages.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
using NServiceBus.Features;
1010
using NServiceBus.Transport;
1111

12+
#pragma warning disable PS0013
13+
#pragma warning disable PS0018
1214
public abstract class DispatchRawMessages<TContext> : Feature
1315
where TContext : ScenarioContext
1416
{
@@ -79,4 +81,6 @@ protected override Task OnStop(IMessageSession session, CancellationToken cancel
7981
ScenarioContext scenarioContext;
8082
}
8183
}
84+
#pragma warning restore PS0018
85+
#pragma warning restore PS0013
8286
}

‎src/ServiceControl.AcceptanceTesting/EndpointTemplates/DefaultServerBase.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ protected DefaultServerBase() : this(new ConfigureEndpointLearningTransport())
1616
{
1717
}
1818

19+
#pragma warning disable PS0013
20+
#pragma warning disable PS0018
1921
public virtual async Task<EndpointConfiguration> GetConfiguration(RunDescriptor runDescriptor, EndpointCustomizationConfiguration endpointCustomizations, Func<EndpointConfiguration, Task> configurationBuilderCustomization)
22+
#pragma warning restore PS0018
23+
#pragma warning restore PS0013
2024
{
2125
var endpointConfiguration = new EndpointConfiguration(endpointCustomizations.EndpointName);
2226

‎src/ServiceControl.AcceptanceTesting/EndpointTemplates/IConfigureEndpointTestExecution.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
/// Provide a mechanism in acceptance tests for transports and persistences
99
/// to configure an endpoint for a test and then clean up afterwards.
1010
/// </summary>
11+
#pragma warning disable PS0018
1112
public interface IConfigureEndpointTestExecution
1213
{
1314
/// <summary>
@@ -32,4 +33,5 @@ public interface IConfigureEndpointTestExecution
3233
/// <returns>An async Task.</returns>
3334
Task Cleanup();
3435
}
36+
#pragma warning restore PS0018
3537
}

‎src/ServiceControl.AcceptanceTesting/InfrastructureConfig/ConfigureEndpointInMemoryPersistence.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using NServiceBus;
66
using NServiceBus.AcceptanceTesting.Support;
77

8+
#pragma warning disable PS0018
89
public class ConfigureEndpointInMemoryPersistence : IConfigureEndpointTestExecution
910
{
1011
public Task Configure(string endpointName, EndpointConfiguration configuration, RunSettings settings, PublisherMetadata publisherMetadata)
@@ -17,4 +18,5 @@ public Task Cleanup() =>
1718
// Nothing required for in-memory persistence
1819
Task.CompletedTask;
1920
}
21+
#pragma warning restore PS0018
2022
}

0 commit comments

Comments
 (0)