Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,25 @@ public async Task Should_ingest_without_an_endpoint_and_without_the_single_owner
}
}

[Test]
public async Task Should_default_the_concurrency_to_10_when_none_is_configured()
{
var settings = await CreateSettings();
settings.MaximumConcurrencyLevel = null;

var host = ErrorIngestionOnlyCommand.BuildHost(settings);

try
{
Assert.DoesNotThrow(() => host.Services.GetServices<IHostedService>().ToArray());
Assert.That(host.Services.GetRequiredService<TransportSettings>().MaxConcurrency, Is.EqualTo(10));
}
finally
{
await host.DisposeAsync();
}
}

static readonly string[] ExpectedHostedServices =
[
"GenericWebHostService", // health endpoint only, no ServiceControl API
Expand Down
52 changes: 52 additions & 0 deletions src/ServiceControl.UnitTests/Hosting/HostArgumentsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace ServiceControl.UnitTests.Hosting
{
using System;
using NUnit.Framework;
using Particular.ServiceControl.Hosting;
using ServiceControl.Hosting.Commands;

[TestFixture]
class HostArgumentsTests
{
[Test]
public void Setup_keeps_the_error_ingestion_only_flag_so_setup_can_refuse_it()
{
var arguments = new HostArguments(["--setup", "--error-ingestion-only"]);

Assert.Multiple(() =>
{
Assert.That(arguments.Command, Is.EqualTo(typeof(SetupCommand)));
Assert.That(arguments.ErrorIngestionOnly, Is.True);
});
}

[Test]
public void Error_ingestion_only_alone_runs_the_ingestion_only_host()
{
var arguments = new HostArguments(["--error-ingestion-only"]);

Assert.That(arguments.Command, Is.EqualTo(typeof(ErrorIngestionOnlyCommand)));
}

[TestCase("--maintenance", typeof(MaintenanceModeCommand))]
[TestCase("--import-failed-errors", typeof(ImportFailedErrorsCommand))]
public void Other_modes_still_win_over_error_ingestion_only(string mode, Type expected)
{
var arguments = new HostArguments([mode, "--error-ingestion-only"]);

Assert.That(arguments.Command, Is.EqualTo(expected));
}

[Test]
public void Setup_alone_is_not_error_ingestion_only()
{
var arguments = new HostArguments(["--setup"]);

Assert.Multiple(() =>
{
Assert.That(arguments.Command, Is.EqualTo(typeof(SetupCommand)));
Assert.That(arguments.ErrorIngestionOnly, Is.False);
});
}
}
}
22 changes: 22 additions & 0 deletions src/ServiceControl.UnitTests/Hosting/SetupCommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace ServiceControl.UnitTests.Hosting
{
using System;
using NUnit.Framework;
using Particular.ServiceControl.Hosting;
using ServiceControl.Hosting.Commands;

[TestFixture]
class SetupCommandTests
{
[Test]
public void Refuses_to_set_up_an_error_ingestion_only_worker()
{
var arguments = new HostArguments(["--setup", "--error-ingestion-only"]);

// No settings, so a missing guard fails on a null reference instead of provisioning whatever this machine configures.
var exception = Assert.ThrowsAsync<Exception>(() => new SetupCommand().Execute(arguments, settings: null));

Assert.That(exception.Message, Does.Contain("--error-ingestion-only runs no setup"));
}
}
}
3 changes: 3 additions & 0 deletions src/ServiceControl/HostApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ public static void AddServiceControl(this IHostApplicationBuilder hostBuilder, S
{
// Ingestion receives through its own transport infrastructure and forwards through
// that same infrastructure's dispatcher, so the endpoint is not hosted at all.
// A hosted endpoint gets this default from CustomizePrimaryEndpoint, which this branch skips.
transportSettings.MaxConcurrency ??= 10;

var machineName = NServiceBus.Support.RuntimeEnvironment.MachineName;
services.AddSingleton(new HostInformation(
DeterministicGuid.MakeId(machineName, settings.InstanceName),
Expand Down
7 changes: 7 additions & 0 deletions src/ServiceControl/Hosting/Commands/SetupCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace ServiceControl.Hosting.Commands
{
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -18,6 +19,12 @@ class SetupCommand : AbstractCommand
{
public override async Task Execute(HostArguments args, Settings settings, CancellationToken cancellationToken = default)
{
if (args.ErrorIngestionOnly)
{
throw new Exception(
"--error-ingestion-only runs no setup: the queues and database belong to the normal instance, which provisions them. Start this worker with --error-ingestion-only alone, without --setup or --setup-and-run.");
}

var hostBuilder = Host.CreateApplicationBuilder();
hostBuilder.AddServiceControlInstallers(settings);

Expand Down
1 change: 1 addition & 0 deletions src/ServiceControl/Hosting/Help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Runs a host that only drains the error queue into the configured database, so se
share the ingestion load. Requires SQL Server or PostgreSQL storage, and requires that the database
has already been provisioned by a normal instance. Exactly one normal instance must still be running:
it owns the retry pipeline, the retention sweep, integration event dispatch and heartbeat monitoring.
This mode runs no setup, so combining it with --setup or --setup-and-run is refused.

Message bodies must be stored somewhere every host can read, so this mode should not be combined with
file system body storage unless the path is a shared mount.
Expand Down
12 changes: 8 additions & 4 deletions src/ServiceControl/Hosting/HostArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,15 @@ public HostArguments(string[] args)
{
"error-ingestion-only",
"Run only error ingestion, for scaling out ingestion across several processes",
s => Command = typeof(ErrorIngestionOnlyCommand)
s => ErrorIngestionOnly = true
}
};

try
{
// Parsed before setup returns, so setup can refuse to provision an ingestion-only worker.
errorIngestionOnlyOptions.Parse(args);

externalInstallerOptions.Parse(args);

if (Command == typeof(SetupCommand))
Expand All @@ -85,10 +88,9 @@ public HostArguments(string[] args)
return;
}

errorIngestionOnlyOptions.Parse(args);

if (Command == typeof(ErrorIngestionOnlyCommand))
if (ErrorIngestionOnly)
{
Command = typeof(ErrorIngestionOnlyCommand);
return;
}

Expand All @@ -107,6 +109,8 @@ public HostArguments(string[] args)

public bool SkipQueueCreation { get; private set; }

public bool ErrorIngestionOnly { get; private set; }

public void PrintUsage()
{
var helpText = string.Empty;
Expand Down
Loading