Skip to content

[WIP] Performance tests initial structure - #3575

Draft
csviri wants to merge 25 commits into
operator-framework:nextfrom
csviri:performance-tests
Draft

[WIP] Performance tests initial structure#3575
csviri wants to merge 25 commits into
operator-framework:nextfrom
csviri:performance-tests

Conversation

@csviri

@csviri csviri commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

csviri and others added 25 commits July 22, 2026 15:59
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
…us (operator-framework#3480)

Add integration tests that showcases handling explicit state in status. Both for simple managed/secondary resources and using a dependent resource.


Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* feat: pool informers so controllers and event sources can share them

Every InformerEventSource used to create its own SharedIndexInformer, so an
operator whose controllers all watch the same secondary type - ConfigMap and
Secret being the usual suspects - opened one watch connection and kept one cache
per controller for the very same resources.

Informers are now handed out by an InformerPool obtained from the
ConfigurationService, keyed by an InformerClassifier. Event sources whose
classifiers are equal are backed by one informer; the pool reference counts its
users and stops the informer once the last one releases it. The classifier is
made up of everything that decides what an informer watches and how: the
KubernetesClient instance (compared by identity, since two clients for the same
API server may still differ in credentials, impersonation or TLS material), the
resource class or the group/version/kind for generic resources, the namespace,
the label, field and shard selectors, and the item store.

Two components are deliberately not part of that identity. The informerListLimit
is excluded, so event sources that disagree only on it still share an informer,
keeping the limit of whichever one created it and logging a warning. Indexers are
excluded because they can be added to a running informer: they are registered
under a name qualified with the controller and event source that added them, so
index names stay private to an event source while callers keep using their own
names, and they are removed again when that event source releases the informer.

Two strategies ship: DefaultInformerPool shares as described and is the default,
NonSharingInformerPool creates a dedicated informer per event source for anyone
wanting to opt out. Either is selected with
ConfigurationServiceOverrider#withInformerPool, and a custom strategy extends
AbstractInformerPool, which already creates the informers from a classifier,
starts them and waits for their caches to sync, leaving the subclass only the
question of whether and when an informer is shared.

Consequently informer creation and startup moved out of InformerWrapper and
InformerManager into the pool, InformerManager acquires and releases informers
instead of owning them, and it removes its own event handler and indexers from an
informer that keeps running for others. An event source registered dynamically
against an already running shared informer needs no special handling: the client
replays the cache contents to a newly added handler.

Also in support of the above: ConfigurationService#informerPool, an
InformerEventSource constructor that no longer needs an EventSourceContext (the
one taking it is deprecated), the resource group/version/kind on
InformerConfiguration, equality and toString on FieldSelector, and equality of
GroupVersionKindPlural made consistent with its hashCode so that an unspecified
plural no longer splits informers.

The pooling itself is production ready; the configuration API around it is marked
experimental and may still change. Covered by unit tests for the pools, the
classifier, the wrapper and the manager, and by integration tests for sharing,
dynamic registration and de-registration that each run against both strategies.


Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
…-framework#3511)

Bumps [org.apache.maven.plugins:maven-jar-plugin](https://github.com/apache/maven-jar-plugin) from 3.5.0 to 3.5.1.
- [Release notes](https://github.com/apache/maven-jar-plugin/releases)
- [Commits](apache/maven-jar-plugin@maven-jar-plugin-3.5.0...maven-jar-plugin-3.5.1)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-jar-plugin
  dependency-version: 3.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…-framework#3510)

Bumps `openrewrite.version` from 8.87.5 to 8.87.7.

Updates `org.openrewrite:rewrite-java` from 8.87.5 to 8.87.7
- [Release notes](https://github.com/openrewrite/rewrite/releases)
- [Commits](openrewrite/rewrite@v8.87.5...v8.87.7)

Updates `org.openrewrite:rewrite-maven` from 8.87.5 to 8.87.7
- [Release notes](https://github.com/openrewrite/rewrite/releases)
- [Commits](openrewrite/rewrite@v8.87.5...v8.87.7)

Updates `org.openrewrite:rewrite-test` from 8.87.5 to 8.87.7
- [Release notes](https://github.com/openrewrite/rewrite/releases)
- [Commits](openrewrite/rewrite@v8.87.5...v8.87.7)

Updates `org.openrewrite:rewrite-java-17` from 8.87.5 to 8.87.7
- [Release notes](https://github.com/openrewrite/rewrite/releases)
- [Commits](openrewrite/rewrite@v8.87.5...v8.87.7)

---
updated-dependencies:
- dependency-name: org.openrewrite:rewrite-java
  dependency-version: 8.87.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.openrewrite:rewrite-maven
  dependency-version: 8.87.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.openrewrite:rewrite-test
  dependency-version: 8.87.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: org.openrewrite:rewrite-java-17
  dependency-version: 8.87.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…8.0 to 3.9.0 (operator-framework#3515)

Signed-off-by: dependabot[bot] <support@github.com>
…erator-framework#3514)

Fabric8's no-context patch() fetches the resource from the server and
computes an RFC 6902 JSON Patch diff, which removes the fields omitted
from a partial resource (including the whole spec). Pass an explicit
JSON_MERGE PatchContext so the non-status merge patch operations really
send application/merge-patch+json.
…ator E2E test (operator-framework#3513)

Adds a Maven-compiled Kotlin test source set to operator-framework-core
and a smoke test verifying that a checked (non-RuntimeException) Exception
thrown from a Kotlin DependentResource is properly caught and reported by
the workflow executor, so that retries are triggered as expected.

Kotlin does not have checked exceptions, so Kotlin code can throw a
checked Exception from an overridden method without declaring it, even
though the Java DependentResource#reconcile signature does not declare
`throws Exception`. Before operator-framework#2965 this exception would not have been
caught by NodeExecutor, since it only handled RuntimeException, silently
swallowing the error and preventing retries.

Closes operator-framework#2967

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: close class brace in Kotlin test; add Kotlin sample operator E2E

Fixes a missing closing brace left in KotlinCheckedExceptionDependentResourceTest.kt.

Adds a new sample-operators/kotlin-operator module: a minimalist Kotlin
operator (ConfigMapCopyReconciler) that copies a value from a custom
resource's spec into a ConfigMap, with a dual-mode (local/remote) E2E
test mirroring the other samples. This verifies deserialization via the
fabric8 client and the full reconciliation runtime when both the CR and
reconciler are implemented in Kotlin, addressing the maintainer's request
for broader coverage than the existing unit-level smoke test.


Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This needed to be fixed after rebase.

Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
)

This is expected for a pool to set. Event if the underlying implementation would set it, this is the correct way to handle it.

Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
…work#3547)

AbstractWorkflowExecutor called Workflow#getDependentResourcesByName purely to
read its size. That allocates a HashMap and walks every node to collect the
dependent resources, then discards the map. The executor is constructed on
every reconcile and cleanup of a workflow-based reconciler, so use the existing
Workflow#size instead.
…perator-framework#3550)

ResourceOperations#desiredForJsonPatch takes an Options argument that it never
reads - it only clones the actual resource and applies the operator. All six
jsonPatch call sites thread the value in for nothing, which suggests the method
still honours the option. Remove the parameter.
…ator-framework#3544)

No behavior change; each site is replaced by a helper that already exists.

- PrimaryUpdateAndCacheUtils#compareResourceVersions (and its private
  validateResourceVersion) duplicated the entire algorithm of
  ReconcilerUtilsInternal#validateAndCompareResourceVersions: the length-first
  compare, the empty check, the leading-zero check and the same exception
  messages. It had no production caller, so the copy could silently drift from
  the version all production paths use. Delegate instead.
- addFinalizerWithSSA builds its bare SSA skeleton with
  HasMetadata#initNameAndNamespaceFrom, which ResourceOperations already uses
  for the same purpose and which is Namespaced-aware.
- AbstractInformerPool formats the informer identifier with
  ReconcilerUtilsInternal#getResourceTypeNameWithVersion instead of
  concatenating the resource name and version by hand.
- EventFilterWindow uses ExtendedResourceEvent#getResourceVersion, which had
  no callers even though it is exactly the expression used here.
- LocallyRunOperatorExtension instantiates reconcilers with Utils#instantiate,
  so it also supports non-public no-arg constructors and reports the failing
  class instead of wrapping in a bare RuntimeException.
…perator-framework#3543)

The event count is increased at the beginning of the reconciliation, thus
waiting for it released the test into the middle of a reconciliation that
was still about to remove the finalizer. The subsequent update then raced
with that removal and failed with a conflict.

Wait for the finalizer removal to actually land, and retry the (optimistically
locked) update with a fresh read on conflict.
…rk#3536)

Add an opt-in, experimental detectApiVersionChange option on
@KubernetesDependent that records the API version the operator applies
in the javaoperatorsdk.io/last-applied-api-version annotation. The
regular matcher then detects a mismatch when that marker differs from
(or is missing relative to) the currently applied API version, causing
a one-time update without triggering repeated reconciliations once the
resource is up-to-date. Disabled by default, so existing behavior and
matching are unaffected unless explicitly enabled.

Guard against a null or immutable annotations map (e.g. Map.of(...))
on the desired resource when detectApiVersionChange is enabled, since
writing the last-applied-api-version marker (and the pre-existing
previous-annotation bookkeeping that runs alongside it) requires a
mutable map. Also rename a misleadingly-named test helper and replace
a no-op assertion on a primitive boolean with a concrete expectation.

Addresses Copilot review feedback on PR operator-framework#3536.

Add DetectApiVersionChangeIT covering the end-to-end scenario: a
ConfigMap dependent resource configured with detectApiVersionChange
is marked with the current API version on creation (without
triggering an update), and a stale marker annotation left on the
actual resource (simulating an older operator/CRD version) is
detected and corrected with exactly one update, after which no
further reconciliation loop occurs.

Signed-off-by: hej090224 <fc49854985@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ator-framework#3548)

InformerManager is generic over C extends Informable, but getTargetClient
type-tested for InformerEventSourceConfiguration to find out whether a specific
(e.g. remote cluster) client was configured. Informable has two implementors
and only one could answer the question, so a third configuration type wanting
its own client would be ignored silently rather than failing to compile.

Move the default getKubernetesClient() up from InformerEventSourceConfiguration
to Informable and let InformerManager ask the configuration directly. The
default still returns Optional.empty(), so existing implementations are
unaffected. As a side effect the ConfigurationService client is now only
created when no specific client is configured, instead of being created and
then discarded.
…rk#3546)

- GenericKubernetesResourceMatcher allocated the path-prefix lists
  (List.of(SPEC), List.of(METADATA), the labels/annotations pair, List.of(STATUS))
  once per JSON-diff node while matching, and nodeIsChildOf built a stream per
  call. Both run for every node of every match, so hoist the lists to constants
  and use an indexed loop.
- SSABasedGenericKubernetesResourceMatcher#sanitizeState nested the StatefulSet
  volume-claim-template handling four levels deep inside the type ladder;
  extract it into sanitizeVolumeClaimTemplates so the ladder reads as one
  dispatch per resource kind.
- addresses deprecations
- addresses late PR comments for Informer Pools: operator-framework#3325

Signed-off-by: Attila Mészáros <a_meszaros@apple.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…tor-framework#3549)

markEventReceived and unMarkEventReceived took a boolean that only ever selects
which IllegalStateException guards apply, and every EventProcessor call site
supplied it by re-reading the same controller configuration value. It can never
differ between calls for a given processor, but nothing enforced that: a call
site passing the wrong value would silently change which state transitions are
legal, and the state machine could not be read without also reading its callers.

Decide it once at the edge: ResourceStateManager takes the flag at construction
(EventProcessor already knows it there) and passes it to each ResourceState,
which keeps it as a final field. Both types are package-private, so this is
self-contained.
…k#3545)

- Mappers#fromMetadata resolved the primary GroupVersionKind on every
  secondary event, although it only depends on the primary type. Hoist it out
  of the lambda and compare the encoded string before falling back to parsing
  the annotation value.
- Mappers.SecondaryToPrimaryFromDefaultAnnotation built a whole new mapper on
  every invocation; hold a single delegate instead. The primaryResourceType
  field becomes unused and is dropped.
- InformerEventSource#start walked the entire informer cache to seed the
  primary-to-secondary index even when that index is the no-op implementation
  (i.e. whenever a primaryToSecondaryMapper is configured), which is pure
  startup latency proportional to the number of cached secondaries.
- ExternalResourceCachingEventSource#getSecondaryResources looked the primary
  up in the cache a second time although the value was already in a local.
- PerResourcePollingEventSource#getAndCacheResource derived the same
  ResourceID twice in adjacent statements.
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Split performance-tests into submodules and record the results of every run
so they can be compared across commits.

- jmh: the existing benchmarks and in-process throughput tests
- e2e: reconciliation throughput of an operator against a real cluster
- reporting: writes the measurements of both as JSON

Results are keyed by commit, ordered by commit timestamp and indexed in
index.json. The workflow runs on main and next, merges the results of all
jobs and commits them to the performance-test-results branch.

Also fixes PrimaryToSecondaryIndexBenchmark, which did not compile against
the current onAddOrUpdate signature, and excludes the JMH generated classes
from spotbugs so that the module passes verify.
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 25, 2026
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@csviri csviri changed the title [WIP] Performance Tests [WIP] Performance tests initial structure Aug 25, 2026
@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Aug 28, 2026
@openshift-ci

openshift-ci Bot commented Aug 28, 2026

Copy link
Copy Markdown

PR needs rebase.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants