An audit of the codebase turned up 15 correctness bugs plus one cosmetic nit.
Each is fixed by its own PR, linked below; the reasoning, impact and (where
present) the regression test live in the individual commit messages.
SpotBugs (effort=Max, threshold=Medium) reports zero findings across all
modules, so none of these are reachable by the existing static analysis.
Confirmed with regression tests
Each of these ships a test that fails without the fix and passes with it.
| Area |
Bug |
ExternalResourceCachingEventSource |
Configuring only a generic filter throws NPE on add/delete/update — the branch guard is || but the body dereferences the specific filter. Affects all polling and inbound event sources. |
ExternalResourceCachingEventSource |
handleRecentResourceUpdate NPEs when the primary is cached but the secondary id is not. |
AbstractDependentResource / bulk |
A BulkDependentResource that is not a Creator/Updater still gets create/update attempted, failing with ClassCastException. The creatable()/updatable() hooks added for this were never wired up. |
LinearRateLimiter |
isLimited returns time elapsed instead of time remaining, so rate-limited resources are rescheduled almost immediately (burst of timer events) and then wait roughly a full extra period. |
MicrometerMetricsV2 |
processing.started.latency is registered against a boxed Long that nothing holds a strong reference to, so the gauge always reports NaN. |
PollingEventSource |
The timer is a non-daemon Timer created in a final field, so it keeps the JVM alive and the event source cannot be restarted (IllegalStateException: Timer already cancelled). |
ExternalResourceCachingEventSource |
Cache reads are unsynchronized while every mutator is synchronized, and getSecondaryResources looks the primary up twice (TOCTOU NPE). |
EventProcessor |
When the primary is gone from cache at execution time the submission is never completed: the resource stays flagged as under processing (never reconciled again) and the in-flight metrics gauges drift. |
ExecutorServiceManager |
The scheduled executor is never shut down; on interrupt the helper pool leaks and started stays true, leaving a "started" operator whose executors are all terminated. |
Fix only
Covered by integration tests only, or needing clock/HTTP-layer control that the
unit tests are not set up for. Each commit says which.
| Area |
Bug |
KubernetesResourceFetcher |
Optional.orElse eagerly evaluates the cluster-scoped fallback, so every namespaced bounded-cache refresh issues a second, wrong-scope API request. |
AbstractExternalDependentResource |
selectTargetSecondaryResource calls desired() directly instead of getOrComputeDesired(), breaking the once-per-reconciliation contract. |
PrimaryUpdateAndCacheUtils |
pollLocalCache builds its deadline from LocalTime, which wraps at midnight — a 10s window each day where the poll times out instantly. |
ResourceOperations |
addFinalizer's precondition NPEs if the resource is deleted mid-retry; removeFinalizer already guards for this. |
Controller |
The constructor's null-Metrics guard is bypassed on the last line, which calls the getter again. |
EventSourceManager |
Cosmetic: a log message has one placeholder too many, rendering a literal {}. (Originally reported as a lost stack trace — that was wrong, SLF4J extracts a trailing Throwable regardless of placeholder count.) |
| assorted |
ManagedInformerEventSource.get can throw on a null last-sync resource version or an unwatched namespace; ExpectationResult.name() throws a bare NPE; an unused parameter and a misleading comment in ResourceOperations. |
Notes
- The branches are based on a commit slightly behind
main and may need a rebase.
- The PRs are opened as drafts; they reference this issue with "Part of", not a
closing keyword, so merging one will not close this issue.
Pull requests
An audit of the codebase turned up 15 correctness bugs plus one cosmetic nit.
Each is fixed by its own PR, linked below; the reasoning, impact and (where
present) the regression test live in the individual commit messages.
SpotBugs (
effort=Max,threshold=Medium) reports zero findings across allmodules, so none of these are reachable by the existing static analysis.
Confirmed with regression tests
Each of these ships a test that fails without the fix and passes with it.
ExternalResourceCachingEventSource||but the body dereferences the specific filter. Affects all polling and inbound event sources.ExternalResourceCachingEventSourcehandleRecentResourceUpdateNPEs when the primary is cached but the secondary id is not.AbstractDependentResource/ bulkBulkDependentResourcethat is not aCreator/Updaterstill gets create/update attempted, failing withClassCastException. Thecreatable()/updatable()hooks added for this were never wired up.LinearRateLimiterisLimitedreturns time elapsed instead of time remaining, so rate-limited resources are rescheduled almost immediately (burst of timer events) and then wait roughly a full extra period.MicrometerMetricsV2processing.started.latencyis registered against a boxedLongthat nothing holds a strong reference to, so the gauge always reportsNaN.PollingEventSourceTimercreated in a final field, so it keeps the JVM alive and the event source cannot be restarted (IllegalStateException: Timer already cancelled).ExternalResourceCachingEventSourcesynchronized, andgetSecondaryResourceslooks the primary up twice (TOCTOU NPE).EventProcessorExecutorServiceManagerstartedstaystrue, leaving a "started" operator whose executors are all terminated.Fix only
Covered by integration tests only, or needing clock/HTTP-layer control that the
unit tests are not set up for. Each commit says which.
KubernetesResourceFetcherOptional.orElseeagerly evaluates the cluster-scoped fallback, so every namespaced bounded-cache refresh issues a second, wrong-scope API request.AbstractExternalDependentResourceselectTargetSecondaryResourcecallsdesired()directly instead ofgetOrComputeDesired(), breaking the once-per-reconciliation contract.PrimaryUpdateAndCacheUtilspollLocalCachebuilds its deadline fromLocalTime, which wraps at midnight — a 10s window each day where the poll times out instantly.ResourceOperationsaddFinalizer's precondition NPEs if the resource is deleted mid-retry;removeFinalizeralready guards for this.ControllerMetricsguard is bypassed on the last line, which calls the getter again.EventSourceManager{}. (Originally reported as a lost stack trace — that was wrong, SLF4J extracts a trailingThrowableregardless of placeholder count.)ManagedInformerEventSource.getcan throw on a null last-sync resource version or an unwatched namespace;ExpectationResult.name()throws a bare NPE; an unused parameter and a misleading comment inResourceOperations.Notes
mainand may need a rebase.closing keyword, so merging one will not close this issue.
Pull requests