Harden endpoint reachability resolution

This commit is contained in:
2026-08-06 15:58:18 +02:00
parent 82584473e8
commit ac51714a88
19 changed files with 556 additions and 152 deletions
@@ -44,7 +44,7 @@ namespace BizTalkCheckmkPulse
var store = new EndpointCatalogStore(
_options.EndpointCatalogPath,
_options.EndpointCatalogMaxBytes,
_options.EndpointMaxCount);
_options.EndpointCatalogMaxEntries);
EndpointCatalog catalog = null;
string readFailure = null;
try
@@ -76,7 +76,7 @@ namespace BizTalkCheckmkPulse
"Endpoint catalog synchronized. path=" + _options.EndpointCatalogPath
+ " active_candidates=" + catalog.ActiveCandidates
+ " configured=" + catalog.Entries.Count
+ " unsupported_external=" + catalog.UnsupportedCandidates);
+ " unresolved_candidates=" + catalog.UnresolvedCandidates);
}
catch (Exception ex)
{
@@ -104,19 +104,28 @@ namespace BizTalkCheckmkPulse
state.CatalogAvailable = true;
state.CatalogSynchronizedUtc = catalog.SynchronizedUtc;
state.Configured = catalog.Entries.Count;
state.UnsupportedActive = CountUnsupportedExternal(result.EndpointCandidates);
var activeEntries = ResolveActiveEndpoints(catalog, result.EndpointCandidates, state);
var activeTargetCount = activeEntries
.Select(TargetKey)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count();
if (activeTargetCount > _options.EndpointMaxCount)
{
state.Failure = "Eindeutige aktive Netzwerkziele=" + activeTargetCount
+ " ueberschreiten EndpointMaxCount=" + _options.EndpointMaxCount + ".";
return;
}
var activeKeys = new HashSet<string>(
result.EndpointCandidates.Where(x => x.Active).Select(x => x.Key),
StringComparer.OrdinalIgnoreCase);
var activeEntries = catalog.Entries
.Where(x => x.Enabled)
.Where(x => string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase)
|| activeKeys.Contains(x.Key))
.Take(_options.EndpointMaxCount)
.ToArray();
state.Active = activeEntries.Length;
state.SkippedInactive = catalog.Entries.Count(x => x.Enabled) - activeEntries.Length;
// Detailwarnungen nur beim Katalogabgleich schreiben; der Minutensnapshot
// enthaelt die aktuelle begrenzte Liste bereits und das Tageslog bleibt kompakt.
foreach (var issue in state.RefreshRequired
? state.ResolutionIssues.Take(20)
: Enumerable.Empty<string>())
{
_logger.Warning("Endpoint target unresolved. " + issue);
}
try
{
@@ -155,26 +164,26 @@ namespace BizTalkCheckmkPulse
var active = (candidates ?? Enumerable.Empty<EndpointCandidate>())
.Where(x => x != null && x.Active)
.ToArray();
var discovered = new List<EndpointCatalogEntry>();
var unsupported = 0;
foreach (var candidate in active)
{
EndpointCatalogEntry entry;
string reason;
if (EndpointAddressParser.TryCreate(candidate, out entry, out reason))
{
discovered.Add(entry);
}
else if (EndpointAddressParser.IsPotentialExternalEndpoint(candidate))
{
unsupported++;
}
}
var manual = existing == null
? new EndpointCatalogEntry[0]
: existing.Entries.Where(x => !x.AutoDiscovered).ToArray();
var manualKeys = new HashSet<string>(manual.Select(x => x.Key), StringComparer.OrdinalIgnoreCase);
var discovered = new List<EndpointCatalogEntry>();
var unresolved = 0;
foreach (var candidate in active)
{
var resolution = EndpointAddressParser.Analyze(candidate);
if (resolution.Status == EndpointResolutionStatus.Probeable)
{
discovered.Add(resolution.Entry);
}
else if (resolution.Status == EndpointResolutionStatus.Unresolved
&& !manualKeys.Contains(candidate.Key))
{
unresolved++;
}
}
var merged = manual
.Concat(discovered.Where(x => !manualKeys.Contains(x.Key)))
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
@@ -187,7 +196,8 @@ namespace BizTalkCheckmkPulse
EnvironmentName = existing == null ? string.Empty : existing.EnvironmentName,
SynchronizedUtc = synchronizedUtc.ToUniversalTime(),
ActiveCandidates = active.Length,
UnsupportedCandidates = unsupported
// Attributsname bleibt fuer die Rueckwaertskompatibilitaet des Katalogformats bestehen.
UnresolvedCandidates = unresolved
};
catalog.Entries.AddRange(merged);
return catalog;
@@ -200,29 +210,107 @@ namespace BizTalkCheckmkPulse
{
var catalog = SynchronizeCatalog(existing, candidates, synchronizedUtc);
catalog.EnvironmentName = _options.EnvironmentName ?? string.Empty;
if (catalog.Entries.Count > _options.EndpointMaxCount)
if (catalog.Entries.Count > _options.EndpointCatalogMaxEntries)
{
throw new InvalidDataException("Probeable endpoints exceed EndpointMaxCount.");
throw new InvalidDataException("Endpoint catalog exceeds EndpointCatalogMaxEntries.");
}
return catalog;
}
private static int CountUnsupportedExternal(IEnumerable<EndpointCandidate> candidates)
/// <summary>
/// Ermittelt aus aktuellen WMI-Kandidaten und manuellen Overrides die in diesem Lauf zu pruefenden Ziele.
/// Automatisch erkannte Katalogeintraege werden bewusst nicht als veraltete Laufzeitquelle verwendet.
/// </summary>
internal static EndpointCatalogEntry[] ResolveActiveEndpoints(
EndpointCatalog catalog,
IEnumerable<EndpointCandidate> candidates,
EndpointConnectivityState state)
{
var count = 0;
foreach (var candidate in candidates.Where(x => x.Active))
if (catalog == null)
{
EndpointCatalogEntry ignored;
string reason;
if (!EndpointAddressParser.TryCreate(candidate, out ignored, out reason)
&& EndpointAddressParser.IsPotentialExternalEndpoint(candidate))
throw new ArgumentNullException("catalog");
}
if (state == null)
{
throw new ArgumentNullException("state");
}
var activeCandidates = (candidates ?? Enumerable.Empty<EndpointCandidate>())
.Where(x => x != null && x.Active)
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First())
.ToArray();
var activeKeys = new HashSet<string>(activeCandidates.Select(x => x.Key), StringComparer.OrdinalIgnoreCase);
var manualOverrides = catalog.Entries
.Where(x => x.Enabled && !x.AutoDiscovered)
.Where(x => !string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase))
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(x => x.Key, x => x.First(), StringComparer.OrdinalIgnoreCase);
var selected = catalog.Entries
.Where(x => x.Enabled && string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var candidate in activeCandidates)
{
EndpointCatalogEntry manual;
if (manualOverrides.TryGetValue(candidate.Key, out manual))
{
count++;
selected.Add(manual);
state.ManualOverridesActive++;
continue;
}
var resolution = EndpointAddressParser.Analyze(candidate);
if (resolution.Status == EndpointResolutionStatus.Probeable)
{
// Immer das aktuelle WMI-Ziel pruefen; der Wochenkatalog darf keine alte Adresse erzwingen.
selected.Add(resolution.Entry);
}
else if (resolution.Status == EndpointResolutionStatus.ExpectedNonProbeable)
{
state.ExpectedNonProbeableActive++;
}
else
{
state.UnresolvedActive++;
state.ResolutionIssues.Add(DisplayResolutionIssue(candidate, resolution.Reason));
}
}
return count;
state.SkippedInactive = catalog.Entries.Count(x =>
x.Enabled
&& !string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase)
&& !activeKeys.Contains(x.Key));
return selected
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First())
.ToArray();
}
/// <summary>
/// Formatiert eine Aufloesungsluecke ohne Transportadresse, Pfad, Querystring oder Zugangsdaten.
/// </summary>
private static string DisplayResolutionIssue(EndpointCandidate candidate, string reason)
{
var artifact = string.IsNullOrWhiteSpace(candidate.ApplicationName)
|| string.Equals(candidate.ApplicationName, "(unknown)", StringComparison.OrdinalIgnoreCase)
? candidate.ArtifactName
: candidate.ApplicationName + "\\" + candidate.ArtifactName;
return "endpoint=" + CompactText(candidate.ArtifactType) + ":" + CompactText(artifact)
+ "[" + CompactText(candidate.TransportRole) + "]"
+ " adapter=" + CompactText(candidate.AdapterName)
+ " reason=" + CompactText(reason);
}
private static string CompactText(string value)
{
return (value ?? string.Empty)
.Replace('\r', ' ')
.Replace('\n', ' ')
.Replace('|', '/')
.Trim();
}
private async Task<IReadOnlyCollection<EndpointProbeResult>> ProbeAllAsync(EndpointCatalogEntry[] endpoints)