diff --git a/CHANGELOG.md b/CHANGELOG.md index c99d8b4a4..bfd6e0b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,127 @@ ## Unreleased +### WolverineFx (core) + event store integrations + +- **A handler can take the store agnostic `JasperFx.Events.Documents` contracts as parameters.** + ([#3962](https://github.com/JasperFx/wolverine/pull/3962), closes + [#3956](https://github.com/JasperFx/wolverine/issues/3956)) `IDocumentSessionOperations`, + `IDocumentWriteOperations` and `IDocumentReadOperations` are the document side counterparts to the + `IEventOperations` contracts Wolverine already understood, and they are the only way store agnostic + source can take a session without naming a concrete store type. They now bind and commit on Marten, + Polecat and Fisher alike: + + ```csharp + // Valid against all three stores -- nothing Marten specific is named + public static void Handle(RecordNote command, IDocumentSessionOperations session) + => session.Store(new Note { Id = command.Id, Text = command.Text }); + ``` + + Two gaps had to close. Codegen matches a variable by its exact type, so nothing satisfied the + parameter from the `IDocumentSession` the chain had already created; and `CanApply` matched against + a fixed type list naming none of them, so `AutoApplyTransactions` skipped the chain and no + `SaveChangesAsync` postprocessor was attached. On a stock host the first gap surfaced as an outright + `UnResolvableVariableException`; with only the second remaining, the handler ran and its writes were + queued into the session's unit of work and **silently discarded**. + + All three contracts resolve from the chain's single `IDocumentSession`, including the read only one, + so a handler taking the read and write contracts together cannot end up with two sessions whose reads + miss its own pending writes. `IDocumentReadOperations` is deliberately not treated as evidence that a + chain writes anything, exactly as `IQuerySession` never has been. The contracts are also registered in + DI — the stores register only their own interfaces — delegating to `IDocumentSession`/`IQuerySession` + so a service located contract still gets the handler's outbox enrolled session. + + ⚠️ Importing the `JasperFx.Events.Documents` **namespace** makes `ToListAsync()` ambiguous between + `DocumentQueryableExtensions` and each store's own queryable extensions (`CS0121`). Alias the + individual contracts instead of importing the namespace. + +- **The persistence provider decides who owns a chain's transaction.** + ([#3957](https://github.com/JasperFx/wolverine/pull/3957), closes + [#3953](https://github.com/JasperFx/wolverine/issues/3953)) Ancillary store inference scanned + `chain.ServiceDependencies()` for ancillary marker types, and `ServiceDependencies` walks constructor + graphs **recursively** — so any dependency that merely *held* an ancillary store matched. A read only + `Directory(ISystemStore)` two hops down counted the same as an injected `DbContext`, and a tenant + Marten handler had its inbox and dead letters stolen by the wrong store. That inference was only ever + correct for EF Core, where the injected `DbContext` genuinely is the transaction owner; Marten, Polecat + and Fisher require `[MartenStore]`/`[Storage]`, which already populates `chain.AncillaryStoreType`. + There is a new default null `IPersistenceFrameProvider.TryDetermineTransactionOwnerType` for this, + implemented only by EF Core. + +### WolverineFx (core) + +- **Durability agents are no longer assigned to nodes that cannot run them.** + ([#3963](https://github.com/JasperFx/wolverine/pull/3963), closes + [#3954](https://github.com/JasperFx/wolverine/issues/3954)) A node started with + `Durability.DurabilityAgentEnabled = false` never registers the durability agent family, so it threw + `ArgumentOutOfRangeException: Unrecognized agent scheme 'wolverinedb'` the moment the leader handed it + one. The leader then re-issued the identical assignment every five minutes indefinitely, no durability + agent ran anywhere for that store, and `owner_id = 0` outgoing envelopes were never recovered. The + failure was silent in both directions: every queue table read zero while the backlog grew. + + Nodes now publish a marker capability when the family is actually registered, and the leader skips + nodes that have not. When no node in the cluster is capable, nothing is assigned and a warning names + the condition and the setting rather than leaving another quiet failure. + + Note this is a **per node** capability rather than the per agent matching the blue/green and group + affinity paths use. `AllNodesHaveSameCapabilities` returns true trivially both for a single node and + when every node is equally incapable — precisely the two configurations reported — and the durability + family's agent list grows at runtime as tenant databases are added, so per agent matching would strand + every later added tenant's agent. + +- **The idle reaper no longer permanently latches durable endpoints.** + ([#3958](https://github.com/JasperFx/wolverine/pull/3958), closes + [#3955](https://github.com/JasperFx/wolverine/issues/3955)) Two independent defects. + `Endpoint.AutoStartSendingAgent()` is `UsedInShardedTopology || Subscriptions.Any()`, so a durable + endpoint reached only via `EndpointFor(uri)` looked as disposable as an ephemeral reply queue and was + reaped. And `RabbitMqEndpoint.ResolveSender` was a permanent `_sender ??=` cache, so the rebuilt agent + wrapped a *disposed* sender and latched forever; `RemoveSendingAgentAsync` also left `Endpoint.Agent` + pointing at the disposed agent, which is transport agnostic. `SendingAgentIdleTimeout` had no test + coverage at all before this. + +### WolverineFx.RabbitMQ + +- **Opt in to waiting for prefetched messages when a listener drains.** + ([#3796](https://github.com/JasperFx/wolverine/pull/3796)) Contributed by + [@benjamin-alexander-simplisafe](https://github.com/benjamin-alexander-simplisafe). A listener can now + be configured to wait for already prefetched messages to be processed as it stops, rather than letting + the broker requeue them: + + ```csharp + opts.ListenToRabbitQueue("orders").DrainWaitForPrefetch(); + ``` + +- **The prefetch drain is safe for a non terminal stop.** + ([#3960](https://github.com/JasperFx/wolverine/pull/3960)) `RabbitMqListener.StopAsync` is not always + terminal — `RequeueContinuation` stops the listener inline from the handler pipeline and the background + `PauseAsync` stops the same consumer again. A JasperFx `BatchingChannel` tolerates every post completion + call silently, which means `PostAsync` after `Complete` **discards the envelope with no exception**, so a + delivery landing between the drain's `Complete()` and the dispose latch vanished and was redelivered. + The latch now happens before completing. + +- **A rejected settle quiesces the channel the broker has already closed.** + ([#3964](https://github.com/JasperFx/wolverine/pull/3964), addresses + [#3950](https://github.com/JasperFx/wolverine/issues/3950)) When the broker rejects a settle with + `PRECONDITION_FAILED - unknown delivery tag` it has already closed that channel, and Wolverine carried + on delivering into it. A channel torn down with deliveries still in flight is what makes RabbitMQ.Client + race itself — `SessionManager.Lookup` throws `KeyNotFoundException` for the channel number it just + removed and the client escalates that into a library initiated close of the **entire connection** + (`code=541`), taking down every listener and sender on it. Wolverine now cancels that channel's consumer + and rebuilds instead of feeding a channel known to be dead. + + This narrows the window and speeds recovery; it does **not** prevent the connection close, which is set + in motion before Wolverine can observe anything — the exception is raised on the client's own `MainLoop` + after the ack has gone out. The underlying defect is upstream in rabbitmq-dotnet-client, where + `SessionManager.Lookup` should `TryGetValue` and drop frames for a dead channel rather than throwing. + +- **Listener recovery after a mid flight connection death is now asserted.** + ([#3961](https://github.com/JasperFx/wolverine/pull/3961), covers + [#3950](https://github.com/JasperFx/wolverine/issues/3950)) That Wolverine recovers from the connection + close above was observed but never tested. A test now kills the broker connection through the management + API and asserts a message published *after* the kill still arrives. Two things it encodes: `/api/connections` + lags the TCP connect on RabbitMQ 4.x (empty at 4s, populated at 8s, on a healthy host passing traffic), and + the kill is scoped by a per test `ClientProvidedName` so it cannot take down a concurrently running test's + connections. + ### WolverineFx.Http.Fisher (new package) - **New `WolverineFx.Http.Fisher` package.** ([#3949](https://github.com/JasperFx/wolverine/pull/3949), diff --git a/Directory.Build.props b/Directory.Build.props index 4158380dd..7a9c4f05f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,7 +14,7 @@ true true enable - 6.28.1 + 6.28.2 $(PackageProjectUrl) true true