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
4 changes: 2 additions & 2 deletions docs/guide/codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ As of Wolverine 5.0, you now have the ability to better control the usage of the
code generation to potentially avoid unwanted usage:

<!-- snippet: sample_configuring_ServiceLocationPolicy -->
<a id='snippet-sample_configuring_ServiceLocationPolicy'></a>
<a id='snippet-sample_configuring_servicelocationpolicy'></a>
```cs
var builder = Host.CreateApplicationBuilder();
builder.UseWolverine(opts =>
Expand All @@ -225,7 +225,7 @@ builder.UseWolverine(opts =>
opts.ServiceLocationPolicy = ServiceLocationPolicy.NotAllowed;
});
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/DocumentationSamples/ServiceLocationUsage.cs#L11-L33' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_ServiceLocationPolicy' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/DocumentationSamples/ServiceLocationUsage.cs#L11-L33' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_servicelocationpolicy' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

::: note
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/durability/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ var host = await Host.CreateDefaultBuilder()
})
.StartAsync();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/SqlServerTests/Persistence/SqlServerMessageStore_with_IdAndDestination_Identity.cs#L28-L40' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_message_identity_to_use_id_and_destination' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/SqlServerTests/Persistence/SqlServerMessageStore_with_IdAndDestination_Identity.cs#L34-L46' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_message_identity_to_use_id_and_destination' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

This might be an important setting for [modular monolith architectures](/tutorials/modular-monolith).
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/durability/ravendb.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public static class RecordTeamHandler
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/RavenDbTests/transactional_middleware.cs#L47-L59' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_ravendb_side_effects' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/RavenDbTests/transactional_middleware.cs#L50-L62' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_ravendb_side_effects' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## System Control Queues
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ internal class DisableExternalTransports : IWolverineExtension
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Wolverine/HostBuilderExtensions.cs#L387-L397' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_disableexternaltransports' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Wolverine/HostBuilderExtensions.cs#L421-L431' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_disableexternaltransports' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

And that extension is just added to the application's IoC container at test bootstrapping time like this:
Expand All @@ -123,7 +123,7 @@ public static IServiceCollection DisableAllExternalWolverineTransports(this ISer
return services;
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Wolverine/HostBuilderExtensions.cs#L364-L372' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_extension_method_to_disable_external_transports' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Wolverine/HostBuilderExtensions.cs#L398-L406' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_extension_method_to_disable_external_transports' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

In usage, the `IWolverineExtension` objects added to the IoC container are applied *after* the inner configuration
Expand Down Expand Up @@ -282,7 +282,7 @@ that issue, or just want a faster start up time, you can disable the automatic e
using var host = await Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Discovery.DisableConventionalDiscovery();
opts.DisableConventionalDiscovery();
}, ExtensionDiscovery.ManualOnly)

.StartAsync();
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/http/transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ using var host = await Host.CreateDefaultBuilder()

::: tip
This functionality if very new, and you may want to reach out through Discord for any questions.
:::
:::
2 changes: 1 addition & 1 deletion docs/guide/messaging/transports/signalr.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ The actual JSON serialization in the SignalR transport is isolated from the rest
JsonOptions = new(JsonSerializerOptions.Web) { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
JsonOptions.Converters.Add(new JsonStringEnumConverter());
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/SignalR/Wolverine.SignalR/Internals/SignalRTransport.cs#L26-L31' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_signalr_default_json_configuration' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/SignalR/Wolverine.SignalR/Internals/SignalRTransport.cs#L27-L32' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_signalr_default_json_configuration' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

But of course, if you needed to override the JSON serialization for whatever reason, you can just push in a
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/idempotency.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var host = await Host.CreateDefaultBuilder()
})
.StartAsync();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/SqlServerTests/Persistence/SqlServerMessageStore_with_IdAndDestination_Identity.cs#L28-L40' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_message_identity_to_use_id_and_destination' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Persistence/SqlServerTests/Persistence/SqlServerMessageStore_with_IdAndDestination_Identity.cs#L34-L46' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_message_identity_to_use_id_and_destination' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Means that the uniqueness is the message id + the endpoint destination, which Wolverine stores as a `Uri` string in the
Expand Down
71 changes: 71 additions & 0 deletions docs/tutorials/leader-election.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,78 @@ To wrap this up, I’m trying to guess at the questions you might have and see i
* **Can Wolverine switch over the leadership role?** Yes, and that should be relatively quick. Plus Wolverine would keep trying to start a leader election if none is found. But yet, it’s an imperfect world where things can go wrong and there will 100% be the ability to either kickstart or assign the leader role from the forthcoming CritterWatch user interface.
* **How does the leadership election work?** Crudely and relatively effectively. All of the storage mechanics today have some kind of sequential node number assignment for all newly persisted nodes. In a kind of simplified “Bully Algorithm,” Wolverine will always try to send “try assume leadership” messages to the node with the lowest sequential node number which will always be the longest running node. When a node does try to take leadership, it uses whatever kind of global, advisory lock function the current persistence uses to get sole access to write the leader node assignment to itself, but will back out if the current node detects from storage that the leadership is already running on another active node.

## Singular Agent <Badge type="tip" text="5.14" />

::: info
`SingularAgent` is trying to assign itself to the "first" node that is not the leader, but will
choose the leader if there is only one node. `SingularAgent` will not reassign the itself to other
nodes as long as it is running anywhere. If you need more sophisticated assignment logic, you will need
to write a custom `IAgentFamily` and register that in your DI container.
:::

What if all you really want is a single `IAgent` for some kind of background process, and that agent should only
ever be running on one single node? Wolverine has the `SingularAgent` base class just for that scenario. See this
sample from our tests:

<!-- snippet: sample_SimpleSingularAgent -->
<a id='snippet-sample_simplesingularagent'></a>
```cs
using JasperFx.Core;
using Wolverine.Runtime.Agents;

namespace Wolverine.ComplianceTests;

public class SimpleSingularAgent : SingularAgent
{
private CancellationTokenSource _cancellation = new();
private Timer _timer;

// The scheme argument is meant to be descriptive and
// your agent will have the Uri {scheme}:// in all diagnostics
// and node assignment storage
public SimpleSingularAgent() : base("simple")
{

}

// This template method should be used to start up your background service
protected override Task startAsync(CancellationToken cancellationToken)
{
_cancellation = new();
_timer = new Timer(execute, null, 1.Seconds(), 5.Seconds());
return Task.CompletedTask;
}

private void execute(object? state)
{
// Do something...
}

// This template method should be used to cleanly stop up your background service
protected override Task stopAsync(CancellationToken cancellationToken)
{
_timer.SafeDispose();
return Task.CompletedTask;
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Testing/Wolverine.ComplianceTests/SimpleSingularAgent.cs#L1-L42' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_simplesingularagent' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

To add that to your Wolverine system, we've added this convenience method:

<!-- snippet: sample_register_singular_agent -->
<a id='snippet-sample_register_singular_agent'></a>
```cs
// Little extension method helper on IServiceCollection to register your
// SingularAgent
opts.Services.AddSingularAgent<SimpleSingularAgent>();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Testing/Wolverine.ComplianceTests/LeadershipElectionCompliance.cs#L80-L86' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_register_singular_agent' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

In the end, what you need is an `IAgentFamily` that can assign a singular `IAgent` to one and only
one node within your system. `SingularAgent` just makes that a little bit simpler.



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ public async Task DisposeAsync()
}
}

public IHost FindHostRunning(Uri agentUri)
{
if (_originalHost.RunningAgents().Contains(agentUri)) return _originalHost;

return _hosts.FirstOrDefault(x => x.RunningAgents().Contains(agentUri));
}

public IHost OriginalHost => _originalHost;

protected abstract void configureNode(WolverineOptions options);
Expand All @@ -69,6 +76,14 @@ protected async Task<IHost> startHostAsync()
opts.Services.AddSingleton<ILoggerProvider>(new OutputLoggerProvider(_output));

opts.Services.AddResourceSetupOnStartup();

#region sample_register_singular_agent

// Little extension method helper on IServiceCollection to register your
// SingularAgent
opts.Services.AddSingularAgent<SimpleSingularAgent>();

#endregion
}).StartAsync();

new XUnitEventObserver(host, _output);
Expand Down Expand Up @@ -114,6 +129,30 @@ await _originalHost.WaitUntilAssignmentsChangeTo(w =>

/***** NEW TESTS END HERE **********************************************/

[Fact]
public async Task singular_agent_is_only_running_on_one()
{
var host2 = await startHostAsync();
var host3 = await startHostAsync();
var host4 = await startHostAsync();

await _originalHost.WaitUntilAssumesLeadershipAsync(5.Seconds());

await shutdownHostAsync(_originalHost);

var uri = new Uri("simple://");

var cancellation = new CancellationTokenSource();
cancellation.CancelAfter(15.Seconds());
while (!cancellation.IsCancellationRequested && !_hosts.Any(x => x.RunningAgents().Contains(uri)))
{
await Task.Delay(250.Milliseconds());
}

_hosts.SelectMany(x => x.RunningAgents()).Where(x => x == uri)
.Count().ShouldBe(1);
}

[Fact]
public async Task leader_switchover_between_nodes()
{
Expand Down
42 changes: 42 additions & 0 deletions src/Testing/Wolverine.ComplianceTests/SimpleSingularAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#region sample_SimpleSingularAgent

using JasperFx.Core;
using Wolverine.Runtime.Agents;

namespace Wolverine.ComplianceTests;

public class SimpleSingularAgent : SingularAgent
{
private CancellationTokenSource _cancellation = new();
private Timer _timer;

// The scheme argument is meant to be descriptive and
// your agent will have the Uri {scheme}:// in all diagnostics
// and node assignment storage
public SimpleSingularAgent() : base("simple")
{

}

// This template method should be used to start up your background service
protected override Task startAsync(CancellationToken cancellationToken)
{
_cancellation = new();
_timer = new Timer(execute, null, 1.Seconds(), 5.Seconds());
return Task.CompletedTask;
}

private void execute(object? state)
{
// Do something...
}

// This template method should be used to cleanly stop up your background service
protected override Task stopAsync(CancellationToken cancellationToken)
{
_timer.SafeDispose();
return Task.CompletedTask;
}
}

#endregion
13 changes: 13 additions & 0 deletions src/Wolverine/HostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Wolverine.Persistence.Durability;
using Wolverine.Persistence.Sagas;
using Wolverine.Runtime;
using Wolverine.Runtime.Agents;
using Wolverine.Runtime.Handlers;

namespace Wolverine;
Expand Down Expand Up @@ -374,6 +375,18 @@ public static Task ApplyAsyncWolverineExtensions(this IServiceProvider services)
return services.GetRequiredService<IWolverineRuntime>().As<WolverineRuntime>().ApplyAsyncExtensions();
}

/// <summary>
/// Registers a SingularAgent type to this Wolverine system
/// </summary>
/// <param name="services"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static IServiceCollection AddSingularAgent<T>(this IServiceCollection services) where T : SingularAgent
{
services.AddSingleton<IAgentFamily, T>();
return services;
}

/// <summary>
/// Disable all Wolverine messaging outside the current process. This is almost entirely
/// meant to enable integration testing scenarios where you only mean to execute messages
Expand Down
81 changes: 81 additions & 0 deletions src/Wolverine/Runtime/Agents/SingularAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using JasperFx;
using Microsoft.Extensions.Hosting;

namespace Wolverine.Runtime.Agents;

/// <summary>
/// Base class for a Wolverine agent that should run on only one
/// node within your system
/// </summary>
public abstract class SingularAgent : IAgent, IAgentFamily
{
/// <summary>
/// Base constructor for SingularAgent classes
/// </summary>
/// <param name="scheme">Descriptive name for Wolverine. Will be the scheme or protocol name for the Agent Uri</param>
public SingularAgent(string scheme)
{
Scheme = scheme;
Uri = new Uri($"{Scheme}://");
}

async Task IHostedService.StartAsync(CancellationToken cancellationToken)
{
await startAsync(cancellationToken);
Status = AgentStatus.Running;
}

/// <summary>
/// Start processing within your system
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected abstract Task startAsync(CancellationToken cancellationToken);

async Task IHostedService.StopAsync(CancellationToken cancellationToken)
{
await stopAsync(cancellationToken);
Status = AgentStatus.Stopped;
}

/// <summary>
/// Stop processing within your system
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected abstract Task stopAsync(CancellationToken cancellationToken);

public Uri Uri { get; }
public AgentStatus Status { get; protected set; } = AgentStatus.Stopped;


public string Scheme { get; }

ValueTask<IReadOnlyList<Uri>> IAgentFamily.AllKnownAgentsAsync()
{
return new ValueTask<IReadOnlyList<Uri>>([Uri]);
}

ValueTask<IAgent> IAgentFamily.BuildAgentAsync(Uri uri, IWolverineRuntime wolverineRuntime)
{
return new ValueTask<IAgent>(this);
}

ValueTask<IReadOnlyList<Uri>> IAgentFamily.SupportedAgentsAsync()
{
return new ValueTask<IReadOnlyList<Uri>>([Uri]);
}

ValueTask IAgentFamily.EvaluateAssignmentsAsync(AssignmentGrid assignments)
{
var agent = assignments.AgentFor(Uri);
if (agent.IsPaused) return new ValueTask();

if (agent.AssignedNode != null) return new ValueTask();

var node = assignments.Nodes.FirstOrDefault(x => !x.IsLeader) ?? assignments.Nodes.FirstOrDefault();
node?.Assign(agent);

return new ValueTask();
}
}
Loading