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
@@ -0,0 +1,39 @@
using System.Linq;
using MartenTests.Distribution.Support;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Wolverine.Marten.Distribution;
using Wolverine.Runtime.Agents;
using Xunit.Abstractions;

namespace MartenTests.Distribution;

// Regression for #3124: IEventSubscriptionAgentFamily.FindAgentUriAsync must resolve the agent URI
// for a REGISTERED projection/subscription shard regardless of whether its agent is currently
// running, so operators can pause/restart/rebuild a stopped or not-yet-started projection.
public class find_agent_uri_for_registered_projection(ITestOutputHelper output) : SingleTenantContext(output)
{
[Fact]
public async Task resolves_registered_shard_without_a_running_agent()
{
var family = theOriginalHost.Services.GetServices<IAgentFamily>()
.OfType<EventSubscriptionAgentFamily>().Single();

// "Trip:All" is the JasperFx ShardName.Identity for the registered TripProjection.
var uri = await family.FindAgentUriAsync("Trip:All", null);

uri.ShouldNotBeNull();
uri!.AbsoluteUri.ShouldBe("event-subscriptions://marten/main/localhost.postgres/trip/all");
}

[Fact]
public async Task returns_null_for_an_unregistered_shard()
{
var family = theOriginalHost.Services.GetServices<IAgentFamily>()
.OfType<EventSubscriptionAgentFamily>().Single();

var uri = await family.FindAgentUriAsync("DoesNotExist:All", null);

uri.ShouldBeNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ public static Uri UriFor(EventStoreIdentity storeIdentity, DatabaseId databaseId
}

/// <summary>
/// Resolve the live agent <see cref="Uri" /> for a projection/subscription shard identified by its
/// Resolve the agent <see cref="Uri" /> for a projection/subscription shard identified by its
/// <paramref name="shardIdentity" /> (the JasperFx <c>ShardName.Identity</c>, e.g. <c>"Trip:All"</c>)
/// and optional <paramref name="tenantId" />, across every store this family manages.
///
/// <para>Tooling such as CritterWatch uses this instead of composing agent URIs by hand — the URI
/// grammar lives here, not in the consumer. Returns <c>null</c> when no matching live agent is
/// <para>Resolution consults the <em>registered</em> subscription set, so it is independent of the
/// shard's current run state — a paused, stopped, crashed, or not-yet-started projection still
/// resolves (this is what makes restart/rebuild of a non-running projection possible; see GH-3124).
/// Tooling such as CritterWatch uses this instead of composing agent URIs by hand — the URI grammar
/// lives here, not in the consumer. Returns <c>null</c> when no matching registered shard is
/// found.</para>
///
/// <para>Handles store-global shards and single-database per-tenant partitioning (where the tenant
Expand Down
16 changes: 10 additions & 6 deletions src/Wolverine/Runtime/Agents/IEventSubscriptionAgentFamily.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@ namespace Wolverine.Runtime.Agents;

/// <summary>
/// Store-agnostic abstraction over the event-store agent family (implemented by Wolverine.Marten and
/// Wolverine.Polecat). Lets monitoring/admin tooling (CritterWatch) resolve the REAL live agent URI
/// for a projection/subscription shard without coupling to a specific event-store provider and
/// without hand-composing the agent URI — the URI grammar stays inside the provider that owns it.
/// Wolverine.Polecat). Lets monitoring/admin tooling (CritterWatch) resolve the REAL agent URI for a
/// projection/subscription shard without coupling to a specific event-store provider and without
/// hand-composing the agent URI — the URI grammar stays inside the provider that owns it.
/// </summary>
public interface IEventSubscriptionAgentFamily
{
/// <summary>
/// Resolve the live agent <see cref="Uri" /> for the shard identified by
/// Resolve the agent <see cref="Uri" /> for the shard identified by
/// <paramref name="shardIdentity" /> (the JasperFx <c>ShardName.Identity</c>, e.g. "Trip:All")
/// and optional <paramref name="tenantId" />. Returns null when no matching live agent is found
/// in this family. CritterWatch resolves every registered family and uses the first non-null hit.
/// and optional <paramref name="tenantId" />. Resolution is based on the <em>registered</em>
/// subscription set, so it succeeds regardless of whether the shard's agent is currently running —
/// a paused, stopped, crashed, or not-yet-started projection still resolves, which is what lets an
/// operator restart or rebuild a non-running projection (see GH-3124). Returns null when no matching
/// registered shard is found in this family. CritterWatch resolves every registered family and uses
/// the first non-null hit.
/// </summary>
ValueTask<Uri?> FindAgentUriAsync(string shardIdentity, string? tenantId, CancellationToken token = default);
}
Loading