Skip to content

Stop one bad property getter from killing the whole capabilities snapshot (GH-3740) - #3741

Merged
jeremydmiller merged 1 commit into
mainfrom
gh/harden-capabilities-asb-hostname
Jul 30, 2026
Merged

Stop one bad property getter from killing the whole capabilities snapshot (GH-3740)#3741
jeremydmiller merged 1 commit into
mainfrom
gh/harden-capabilities-asb-hostname

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3740.

From a user error report: an Azure Service Bus service on 6.24.0 / JasperFx 2.36.2 could never hand its capabilities to CritterWatch.

System.Reflection.TargetInvocationException:
   at JasperFx.Descriptors.OptionsValue.Read
   at JasperFx.Descriptors.OptionsDescription.readProperties
   at Wolverine.Transports.TransportBase`1.TryBuildBrokerUsage
   at Wolverine.Configuration.Capabilities.ServiceCapabilities.readTransports
   at Wolverine.Configuration.Capabilities.ServiceCapabilities+<ReadFrom>d__74.MoveNext
   at Wolverine.CritterWatch.CritterWatchObserver+<processBatch>d__27.MoveNext
Inner exception System.NullReferenceException:
   at Wolverine.AzureServiceBus.AzureServiceBusTransport.get_HostName

HostName parsed the host out of ConnectionString!, so it threw for every credential-based connection (FullyQualifiedNamespace + TokenCredential / AzureNamedKeyCredential / AzureSasCredential) — the normal managed-identity setup, where there is no connection string at all. HostName is public, BrokerDescription reads every public property reflectively, and ServiceCapabilities.ReadFrom was all-or-nothing, so that one getter cost the service its whole snapshot. CritterWatch retries the same doomed read on every batch, so the service heartbeats forever and never completes a handshake — no endpoints, no message types, no stores.

What changed

Azure Service Bus

  • HostName returns the connection-string host when there is one, otherwise FullyQualifiedNamespace (which is the host name for credential-based connections), and null only when nothing is configured at all. This also fixes the IMassTransitInteropEndpoint.MassTransitUri() implementations for queues, topics and subscriptions, which interpolate it. The Endpoint= parse now splits on the first = only, since SharedAccessKey values carry base64 padding.
  • [IgnoreDescription] on BusClient / ManagementClient: reading those lazily builds a live Azure client as a side effect of describing configuration, and throws ArgumentNullException outright when nothing is configured yet. NATS and Pulsar already mark their live-runtime properties this way.

Pulsar

  • [IgnoreDescription] on this[Uri]. Reflection counts an indexer as a property and GetValue without index arguments throws TargetParameterCountException — enough to break the snapshot for every application using Pulsar.

ServiceCapabilities

  • ReadFrom is best effort per section, and per transport and per endpoint inside the two reflective sections: a failure is logged (with the section / protocol / endpoint Uri) and skipped instead of aborting everything. A capabilities snapshot is diagnostic data — losing one section beats losing all of it.
  • A cancelled token still propagates rather than being logged as a section failure, so shutdown doesn't grind through twelve sections and a dozen warnings.

Tests

  • CoreTests/Acceptance/service_capabilities_resilience_3740.cs — a transport with a throwing getter is skipped while a well-behaved one still lands, plus the cancellation case.
  • Wolverine.AzureServiceBus.Tests/broker_description_with_credentials_3740.csHostName for token-credential, connection-string, no-Endpoint-segment and nothing-configured setups, and that BrokerDescription builds cleanly for a credential-based transport.

Full CoreTests (2148) and the ASB unit tests (35) green locally.

Companion hardening in JasperFx, so a throwing getter degrades to a "could not be read" value instead of an exception: JasperFx/jasperfx#591 (JasperFx/jasperfx#590).

🤖 Generated with Claude Code

https://claude.ai/code/session_01RainvDHdde5JUAiGEPEgWw

…shot (GH-3740)

AzureServiceBusTransport.HostName parsed the host out of ConnectionString!,
so it threw a NullReferenceException for every credential-based connection
(FullyQualifiedNamespace + TokenCredential / NamedKeyCredential /
SasCredential) -- the normal managed-identity setup, where there is no
connection string at all. HostName is public, BrokerDescription reads every
public property reflectively, and ServiceCapabilities.ReadFrom was
all-or-nothing, so that single getter cost the service its entire
capabilities snapshot. CritterWatch retries the same doomed read on every
batch, so the service heartbeats forever and never completes a handshake.

- HostName returns the connection-string host when there is one, otherwise
  FullyQualifiedNamespace (which *is* the host name for credential-based
  connections), and null only when nothing is configured. Also fixes the
  MassTransit interop Uris, which interpolate it. The Endpoint parse now
  splits on the first '=' only, since SharedAccessKey values carry base64
  padding.
- [IgnoreDescription] on BusClient / ManagementClient: reading those
  lazily builds a LIVE Azure client as a side effect of describing
  configuration, and throws outright when nothing is configured. NATS and
  Pulsar already mark their live-runtime properties this way.
- [IgnoreDescription] on PulsarTransport's this[Uri] indexer -- reflection
  counts an indexer as a property and GetValue without index arguments
  throws TargetParameterCountException, which was enough to break the
  snapshot for every application using Pulsar.
- ServiceCapabilities.ReadFrom is now best effort per section, and per
  transport and per endpoint within the reflective sections: a failure is
  logged and skipped rather than aborting everything. Cancellation still
  propagates instead of being logged as a section failure.

Companion hardening in JasperFx (so a throwing getter degrades to a
"could not be read" value) is JasperFx/jasperfx#590.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RainvDHdde5JUAiGEPEgWw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Azure Service Bus: HostName NREs for credential-based connections, taking the whole ServiceCapabilities snapshot with it

1 participant