Repository navigation
Conversation
Test Results 4 files 4 suites 1m 28s ⏱️ Results for commit e69cd65. ♻️ This comment has been updated with latest results. |
smolnar82
left a comment
There was a problem hiding this comment.
I found 2 blockers, and the following efficiency issues:
1. every descriptor is parsed twice per polling cycle
File: PollingConfigurationAnalyzer.java (getReferencedServiceTypes() / clusterReferencesExist())
clusterReferencesExist() and getReferencedServiceTypes() each independently loop over ts.getDescriptors() and re-parse every descriptor file via SimpleDescriptorFactory.parse() (around lines 423 and 602). With N descriptors on disk, each polling cycle now parses every file twice per cluster — doubling filesystem reads + JSON parsing — and duplicates near-identical IOException handling.
Fix direction: merge the two into a single pass over ts.getDescriptors() that returns both "does any descriptor reference this (address, cluster)" and "the set of referenced service names", so each descriptor is parsed once.
2. CORE_SETTINGS is re-fetched from CM once per relevant service type
File: PollingConfigurationAnalyzer.java (getCoreSettingsConfig())
getCoreSettingsConfig() issues a fresh readServices(clusterName, summary) plus a readServiceConfig() round-trip every time getCurrentServiceConfiguration() runs — i.e. once per distinct relevant service type per polling cycle. Cluster discovery, by contrast, computes CORE_SETTINGS once for the whole cluster (coreSettingsConfig(...)).
Failure path: a polling cycle with events for three service types (e.g. HDFS, HIVE_ON_TEZ, YARN) makes three separate readServices() + readServiceConfig() calls just to re-derive the same CORE_SETTINGS value, adding avoidable CM API load on every interval.
Fix direction: compute CORE_SETTINGS once per polling pass and reuse it across all services handled in that pass (cache it for the duration of the cycle).
3.: role-type exclusion config is cached for the analyzer's lifetime while service-type exclusion is read fresh
File: PollingConfigurationAnalyzer.java (isExcludedRoleType() / isExcludedServiceType())
isExcludedRoleType() lazily builds and then permanently caches its TypeNameFilter (roleTypeFilter) from gatewayConfig on first use, whereas the adjacent isExcludedServiceType() re-reads gatewayConfig.getClouderaManagerServiceDiscoveryExcludedServiceTypes() fresh on every call.
Failure path: an operator updates gateway.cloudera.manager.service.discovery.excluded.role.types on a running gateway (without restarting the CM configuration monitor thread). isExcludedServiceType honors an equivalent excluded-service-types change immediately, but isExcludedRoleType keeps using the stale filter built at first invocation and silently ignores the update.
Fix direction: pick one policy for both. Either read both config values fresh per call, or resolve both once at construction — but don't split the behavior between the two sibling checks introduced in this PR.
| roleConfigs.put(role, configList); | ||
| } | ||
| currentConfig = new ServiceConfigurationModel(svcConfig, roleConfigs); | ||
| final ApiService apiService = new ApiService().name(service).type(serviceType); |
There was a problem hiding this comment.
To reuse the discovery workflow, getCurrentServiceConfiguration() reconstructs the ApiService / ApiRole / config objects it feeds into ServiceModelFactory.generateServiceModels(). But this reconstruction doesn't populate the same fields that real discovery (ClouderaManagerServiceDiscovery.discoverService) hands to generators. Any ServiceModelGenerator that dereferences a field the reconstruction leaves null throws an NPE inside generateService(). This is not an edge case — it hits the majority of generators:
role.getHostRef().getHostname()— used by 34 of 51 generators (OozieServiceModelGenerator,SolrServiceModelGenerator, HBase, Impala, and most others). NPEs if the reconstructed role'shostRef(or its hostname) isn't populated exactly as discovery populates it.service.getClusterRef().getClusterName()— used byYarnUIServiceModelGeneratorandJobHistoryUIServiceModelGenerator. NPEs because the syntheticApiServicesets onlyname/type, noclusterRef.- Any other field a given generator's
handles()/generateService()reads that the reconstruction omits.
Failure path & blast radius: a start/restart/scale event for almost any discoverable service (YARN, Oozie, Solr, HBase, …) → hasConfigChanged → getCurrentServiceConfiguration → ServiceModelFactory.generateServiceModels → the generator's generateService() → NPE. The NPE is not an ApiException, so the method's own try/catch doesn't catch it; it propagates to monitorClusterConfigurationChanges' outer catch(Exception), aborting the entire polling cycle for all clusters. Because the triggering event is never marked processed, the same NPE recurs every polling interval — PCA is effectively dead for that gateway.
Root cause / fix direction: the reconstruction in getCurrentServiceConfiguration() diverges from how ClouderaManagerServiceDiscovery builds these objects. Rather than hand-rebuilding ApiService / ApiRole from readServiceConfig, PCA should obtain the model inputs through the same code path discovery uses (the shared component introduced here), so every field a generator may read is populated identically. A unit test that runs each registered generator against PCA-reconstructed inputs would have caught this and would guard against regressions as new generators land.
There was a problem hiding this comment.
Fixed:
- 8c06e4d: fixes the clusterRef NPE by passing the real ApiService from the cluster's service list.
- 0561951: adds the test that runs every registered generator against monitor-shaped inputs.
- dd30643: adds the RuntimeException guard so a failing generator no longer ends the whole polling cycle, and tightens its test.
For hostRef, the shared ServiceModelFactory from 7cc335a already covered it.
| } else { | ||
| // Had a prior config but produces no model now: became invalid / was removed -> re-discover so the | ||
| // service is dropped from the affected topologies (and the scoped-replace merge clears its baseline). | ||
| log.serviceDisabled(re.getService()); | ||
| configHasChanged = true; |
There was a problem hiding this comment.
This new branch treats getCurrentServiceConfiguration() == null as "the service is now invalid / disabled" and forces re-discovery. But that method also returns null on any ApiException - network blip, auth failure, transient 5xx:
} catch (ApiException e) {
log.clouderaManagerConfigurationAPIError(e);
}
return currentConfig; // still null on API errorSo a null return is ambiguous: it can mean either "config genuinely produced no model" or "couldn't reach CM". hasConfigChanged's else branch (prior config present, currentConfig == null) can't tell them apart and unconditionally sets configHasChanged = true, logs serviceDisabled, and triggers a full re-discovery.
Failure path: CM API has a transient outage while computing the current config for a service that has a valid prior baseline → getCurrentServiceConfiguration catches the ApiException and returns null → hasConfigChanged forces re-discovery → repeats every polling cycle until CM recovers.
Regression: previously an API-error null was a harmless no-op (configHasChanged stayed false). This PR turns a transient CM error into a repeating full-cluster rediscovery storm — the same failure class KNOX-2900 set out to eliminate.
Fix direction: distinguish "no model produced" from "CM unreachable". Options: let the ApiException propagate (or rethrow) instead of collapsing it to null, or return a tri-state / Optional so the caller can skip the change decision on API errors and only treat a genuinely empty-but-successful result as "service invalid".
There was a problem hiding this comment.
Fixed by
- 4b0c342 returns a three-state result so an unreachable CM no longer forces a rediscovery
- 37f71bf adds the tests.
One limitation, which also existed on master: after an error the event is still marked processed,
so a real change that coincides with an outage isn't retried until another event arrives. It is not a regression.
I'd create a follow-up ticket for that.
c45a83c to
5da56dd
Compare
# Conflicts: # gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ServiceModelGeneratorsHolder.java # gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ServiceModelGeneratorsHolderTest.java
…figurationAnalyzer getReferencedServiceTypes and clusterReferencesExist swallowed descriptor parse/read errors silently and only caught IOException. A malformed or unreadable descriptor could silently stop rediscovery or discard a live cluster's cached configuration. Both now catch broadly and log the offending file. getReferencedServiceTypes skips the unreadable descriptor and keeps filtering on the descriptors it could read (an unparseable descriptor can't be regenerated until it is fixed, so it can't be the target of a useful rediscovery now). clusterReferencesExist instead assumes a reference remains when a descriptor can't be read, so an unreadable descriptor never causes the monitored cluster's baseline to be discarded.
…de when determining referenced services, since SimpleDescriptorHandler never regenerates them (using the gateway-site override list, not the descriptor's own read-only field). Both now catch broadly and log the offending file. getReferencedServiceTypes skips the unreadable descriptor and keeps filtering on the descriptors it could read (an unparseable descriptor can't be regenerated until it is fixed, so it can't be the target of a useful rediscovery now). clusterReferencesExist instead assumes a reference remains when a descriptor can't be read, so an unreadable descriptor never causes the monitored cluster's baseline to be discarded.
…iService from the existing readServices(...,"summary") call (also fixes the Solr displayName null).
…ation (withModel/noModel/error) so a transient CM error no longer triggers unnecessary re-discovery each polling cycle.
…e honored isExcludedRoleType cached its TypeNameFilter for the analyzer's lifetime, so an update to gateway.cloudera.manager.service.discovery.excluded.role.types on a running gateway was silently ignored - unlike the sibling isExcludedServiceType, which reads its config fresh. Drop the cached roleTypeFilter and build the filter per call (getAllRoleTypes() is already an immutable cached set; scale events are rare). Add a test that flips the config between two scale-event evaluations and asserts the update is honored.
clusterReferencesExist() and getReferencedServiceTypes() each looped ts.getDescriptors() and re-parsed every descriptor file, so an event-bearing cycle parsed every file twice per cluster. Merge them into a single analyzeDescriptorReferences() pass returning both whether the cluster is still referenced and the referenced CM service types. Preserve the two deliberate asymmetries: an unreadable or read-only-override descriptor still counts as a reference (keep monitoring) but contributes no referenced service names. Merge the two per-descriptor error log messages into one accordingly.
getCurrentServiceConfiguration re-fetched the cluster service list (readServices "summary") and the CORE_SETTINGS config on every call - once per relevant service type per cycle - though both are cluster-wide and identical for every service examined that cycle. Cache them per cluster in a map cleared at the start of each polling pass, so they are fetched at most once per cluster per cycle and reused (mirroring how cluster discovery computes CORE_SETTINGS once). The cache is populated lazily inside getCurrentServiceConfiguration, so scale-only cycles make no extra CM calls and no test seam changes are needed. Add testClusterModelInputsFetchedOncePerClusterAcrossServices, which drives the real getCurrentServiceConfiguration for two services in one cluster through a counting API client and asserts the service list is read exactly once.
The tests started the analyzer on a polling thread and blocked on awaitTermination(10s); since run() loops until stop() (called only after await), every test waited the full 10s, making the suite take ~320s. Extract run()'s per-iteration body into a package-visible runMonitoringCycle() (readiness gate + monitorClusterConfigurationChanges; gatewayStatusOk becomes a sticky instance field) and have the outcome tests call it directly instead of spawning a thread. Registering events before the cycle also removes the previous add-after-start race. No production behavior change. Add one latch-based test that still exercises the real polling thread: an event arriving after start is picked up on a subsequent poll, and stop() terminates the loop.
…event relevance log The DEBUG line for activation events printed only the raw inputs of the relevance decision. Since descriptor-reference and excluded-service-type filtering was added, a successful Start/Restart can be ignored because the service type is excluded or no descriptor eligible for re-discovery references it (e.g. a service used only by a topology listed in gateway.read.only.override.topologies), and the line could not tell these apart. Reformat the message as labeled key=value pairs and add the two filter results, excludedServiceType and referencedByDescriptor. Compute each filter once in isStartEvent and use it for both the decision and the log. The relevance decision itself is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… relevance log Like the activation event line, the scale event line printed only the raw inputs of its relevance decision. A role created/deleted event can be ignored because the service type is excluded, no descriptor eligible for re-discovery references it, or the role type is excluded from discovery, and the line could not tell these apart. Reformat the message as labeled key=value pairs and add the three filter results, excludedServiceType, referencedByDescriptor and excludedRoleType. Compute each filter once in isScaleEvent and use it for both the decision and the log. The relevance decision itself is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ctivation After "Analyzing current <service> configuration for changes..." the analyzer logged nothing when the comparison found no difference, so a restart that needs no re-discovery looked the same as a stalled or failed analysis. Add a DEBUG message for that outcome, alongside the existing INFO messages for changed properties and the skip messages for invalid and unreachable services. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
When the TopologyService is unavailable, the referenced service types were passed around as null. Make DescriptorReferences hold that state explicitly instead. Behavior is unchanged. Also add a test for the unavailable TopologyService case. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… cycle Cover the CORE_SETTINGS fetch, and check that the cached data is refreshed on each new polling cycle. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Add tests showing that changing the excluded role types or excluded service types on a running gateway takes effect on the next polling cycle. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tion fails Service model generators are pluggable, so one of them may throw an unexpected exception. Treat that like a Cloudera Manager failure: log it and skip the change decision for that service instead of ending the whole polling cycle. Name the service in the related log messages, and make the all-generators test fail on any unexpected exception. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
About the 3 issues above:
|
KNOX-2900 - Re-enable service-based discovery filter
What changes were proposed in this pull request?
PollingConfigurationAnalyzer changes:
How was this patch tested?
Unit tests, manual tests on a dev cluster.
Integration Tests