Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
name: Integration tests (${{ inputs.java-version }}, ${{ inputs.kube-version }}, ${{ inputs.http-client }})
runs-on: ubuntu-latest
continue-on-error: ${{ inputs.experimental }}
timeout-minutes: 40
timeout-minutes: 120
steps:
- uses: actions/checkout@v7
with:
Expand Down
36 changes: 25 additions & 11 deletions docs/content/en/blog/news/read-after-write-consistency.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> contex
}
```

In addition to that, the framework will automatically filter events for your own updates,
so they don't trigger the reconciliation again.
In addition to that, the framework will provide facilities to filter out
events for own updates so they don't trigger the reconciliation again.

{{% alert color=success %}}
**This should significantly simplify controller development, and will make reconciliation
Expand Down Expand Up @@ -180,12 +180,6 @@ From this point the idea of the algorithm is very simple:
the one in the TRC. If yes, evict the resource from the TRC.
3. When the controller reads a resource from cache, it checks the TRC first, then falls back to the Informer's cache.

The actual filtering of events for our own writes is more nuanced than a simple
"evict on RV ≥ TRC version" rule — it is driven by a per-resource state machine
that tracks in-flight writes and the events received around them. See
[Filtering events for our own updates](#filtering-events-for-our-own-updates) below.


```mermaid
sequenceDiagram
box rgba(50,108,229,0.1)
Expand Down Expand Up @@ -226,10 +220,30 @@ sequenceDiagram
When we update a resource, the informer will eventually propagate an event that would trigger a reconciliation.
In most cases, however, this is not desirable. Since we already have the up-to-date resource at that point,
we want to be notified only when the change originates outside our reconciler.
Therefore, in addition to caching the resource, we filter out events caused by our own updates.
Therefore, in addition to caching the resource, provide utilities to so you can optimize the update/patch
operations to filter out those events.

See [ResourceOperations](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperations.java#L49)
for details.

```java
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context) {

ConfigMap managedConfigMap = prepareConfigMap(webPage);

// resource operation in this case will resource only if
// it does not match the actual, and will filter our the related event
context.resourceOperations().serverSideApply(managedConfigMap);

// UpdateControl.patchStatus would only cache the resource to filter out events too
// you have to use resourceOperations.
context.resourceOperations().serverSideApplyPrimaryStatus(alterStatusObject(webPage));
return UpdateControl.noUpdate();
}
```

Note that the implementation of this is relatively complex: while performing the update, we record all the
events received in the meantime and decide whether to propagate them further once the update request completes.
Note that the implementation of this is relatively complex and has some caveats:
while performing the update, we record all the events received in the meantime and decide whether to propagate them further once the update request completes.

This way, we significantly reduce the number of reconciliations, making the whole process much more efficient.

Expand Down
120 changes: 120 additions & 0 deletions docs/content/en/blog/releases/v5-5-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
title: Version 5.5 Released!
date: 2026-07-14
author: >-
[Attila Mészáros](https://github.com/csviri)
---

We're pleased to announce the release of Java Operator SDK v5.5.0! This minor version reworks how
the framework filters the events caused by a controller's own writes, making it more correct and
more efficient, and exposes a richer, matcher-aware `ResourceOperations` API. There are **no
breaking API changes**, but there is one behavioral change around `UpdateControl` — see the
migration notes.

## Key Features

### Matcher-based updates in `ResourceOperations`

`ResourceOperations` (available from the reconciliation `Context` via `context.resourceOperations()`)
now offers a complete, consistent family of update/patch/create methods for every write strategy —
**server-side apply, update (PUT), JSON Patch (RFC 6902) and JSON Merge Patch (RFC 7386)** — each
available for the whole resource and the `status` subresource, and with dedicated `primary`
variants.

Every method comes in two flavors: a default one, and one taking an `Options` argument that controls
how the resulting own event is handled:

```java
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context) {
makeStatusChanges(webPage);
// updates the status, filters the own event, and skips the write entirely if nothing changed
context.resourceOperations().serverSideApplyPrimaryStatus(webPage);
return UpdateControl.noUpdate();
}
```

By default these operations **match the desired state against the actual (cached) state before
writing**: if they already match, the write is skipped; otherwise the write is performed and its own
event is filtered. This is the most efficient option — it covers full event filtering *and* avoids a
request to the Kubernetes API server when nothing changed. Default matchers are provided for every
operation type; when one does not fit, supply your own via `Options.matchAndFilter(matcher)`.

`Options` exposes the available strategies:

- `matchAndFilter(...)` / `matchAndFilterWithDefaultMatcher(...)` — match, then write-and-filter only
if needed (the default).
- `filterIfOptimisticLocking()` — filter the own event only when the write uses optimistic locking,
otherwise just cache the response.
- `cacheOnly()` — only cache the response (read-cache-after-write consistency), no filtering.
- `forceFilterEvents()` — always filter (mostly internal usage).

> **Correctness note**: filtering an own event is only safe if the framework can tell an own write
> apart from a concurrent third-party write. This requires **either** a matcher **or** optimistic
> locking; otherwise a concurrent external update inside the filter window may be missed until the
> next resync.

### More correct own-event filtering (no-op update edge case)

The read-cache-after-write own-event filter has been reworked to fix an edge case where a legitimate
external change could be filtered out together with a controller's own **no-op** update — for
example, when the spec was changed externally while the controller patched its status and that status
patch turned out to be a no-op. The new matcher-based operations avoid issuing such no-op writes in
the first place, and the filtering itself is now correct in these concurrent scenarios.

## Additional Improvements

- **`GenericKubernetesResourceMatcher.matchStatus(...)`**: a status-only counterpart to `match(...)`
that compares just the `/status` subtree.
- **New `Matcher` SPI**: `io.javaoperatorsdk.operator.api.reconciler.matcher.Matcher` lets you plug a
custom matching strategy into `Options.matchAndFilter(...)`.
- Extensive integration tests covering every `ResourceOperations` update/patch operation (primary,
status, and secondary resources) against a real cluster.

## Migration Notes

There are **no breaking API changes**; existing code compiles and runs unchanged. However:

### `UpdateControl` no longer filters own events by default

Returning an `UpdateControl` (or `ErrorStatusUpdateControl`) still updates the resource and keeps the
cache read-after-write consistent, but it **no longer filters the resulting own event by default** —
so the write may cause an additional (idempotent) reconciliation. This change fixes correctness edge
cases where an event that should have propagated was previously swallowed.

If you relied on the previous filtering, perform the update through `ResourceOperations` and return
`UpdateControl.noUpdate()`:

```java
// before (v5.4)
resource.setStatus(new MyStatus().setReady(true));
return UpdateControl.patchStatus(resource);

// after (v5.5) — filter the own event explicitly
resource.setStatus(new MyStatus().setReady(true));
context.resourceOperations().serverSideApplyPrimaryStatus(resource);
return UpdateControl.noUpdate();
```

See the [migration guide](/docs/migration/v5-5-migration) for details.

## Getting Started

```xml
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework</artifactId>
<version>5.5.0</version>
</dependency>
```

## All Changes

See the [comparison view](https://github.com/operator-framework/java-operator-sdk/compare/v5.4.0...v5.5.0)
for the full list of changes.

## Feedback

Please report issues or suggest improvements on our
[GitHub repository](https://github.com/operator-framework/java-operator-sdk/issues).

Happy operator building! 🚀
45 changes: 41 additions & 4 deletions docs/content/en/docs/documentation/reconciler.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,37 @@ supports stronger guarantees, both for primary and secondary resources. If this
they would otherwise look like own echoes, since the relist may have
hidden events.

#### Requesting event filtering and its correctness requirements

Own-event filtering is only safe if the framework can tell an own write apart from a concurrent
third-party write. This requires **either**:

- a *matcher* to be provided (so a filtered event can be confirmed to reflect the desired state we
just wrote), **or**
- the update to be done using *optimistic locking* (a resource version set on the written resource),
so a conflicting concurrent change is rejected by the API server rather than silently swallowed.

`ResourceOperations` methods accept an `Options` argument to select the behavior; each maps to a
`Mode`:

- `Options.matchAndFilter(matcher)` / `Options.matchAndFilterWithDefaultMatcher(updateType)` — compare
the desired state to the actual (cached) state; if they already match, skip the write entirely,
otherwise write and filter the own event. This is the **default** for the `ResourceOperations`
update/patch methods, and generally the most efficient option: it filters the own event *and*
avoids a request to the API server when nothing changed. Default matchers are provided for every
operation type, but they are heuristics — a workflow relying on them should be tested against the
concrete resources it manages, or a custom matcher supplied.
- `Options.filterIfOptimisticLocking()` — filter the own event only when the write uses optimistic
locking, otherwise just cache the response.
- `Options.cacheOnly()` — only cache the response (read-cache-after-write consistency), no own-event
filtering.
- `Options.forceFilterEvents()` — always filter, regardless of optimistic locking. Only safe when
correctness is otherwise guaranteed (mostly for internal usage); a concurrent external update in
the filter window may otherwise be missed until the next resync.

See the [`ResourceOperations`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperations.java)
and `ResourceOperations.Options` documentation for details.


In order to benefit from these stronger guarantees, use [`ResourceOperations`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperations.java)
from the context of the reconciliation:
Expand All @@ -208,20 +239,26 @@ public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> contex
var upToDateResource = context.getSecondaryResource(ConfigMap.class);

makeStatusChanges(webPage);

// built in update methods by default use this feature
// patches the status and caches the response (does not filter the own event by default, see below)
return UpdateControl.patchStatus(webPage);
}
```

`UpdateControl` and `ErrorStatusUpdateControl` by default use this functionality, but you can also update your primary resource at any time during the reconciliation using `ResourceOperations`:
{{% alert title="UpdateControl and event filtering" %}}
Since v5.5, returning an `UpdateControl` (or `ErrorStatusUpdateControl`) updates the resource and
keeps the cache read-after-write consistent, but by default it **no longer filters the own event** —
the resulting update may cause an additional (idempotent) reconciliation. To also filter the own
event, perform the update through `ResourceOperations` instead and return `UpdateControl.noUpdate()`.
{{% /alert %}}

You can update your primary resource at any time during the reconciliation using `ResourceOperations`:

```java

public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context) {

makeStatusChanges(webPage);
// this is equivalent to UpdateControl.patchStatus(webpage)
// updates the status, filters the own event and skips the write if nothing changed
context.resourceOperations().serverSideApplyPrimaryStatus(webPage);
return UpdateControl.noUpdate();
}
Expand Down
69 changes: 69 additions & 0 deletions docs/content/en/docs/migration/v5-5-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: Migrating from v5.4 to v5.5
description: Migrating from v5.4 to v5.5
---

## No breaking API changes

v5.5 does **not** contain breaking API changes: existing code compiles and continues to work without
modification. There is, however, one **behavioral** change around own-event filtering that you should
be aware of (see below).

## `UpdateControl` no longer filters own events by default

In previous versions, updating the primary resource (or its status) by returning an `UpdateControl`
from the reconciler would filter out the event caused by that own update, so the write did not
trigger an additional reconciliation. Unfortunately, in some edge cases this could lead an
incorrect behavior, thus filtering out events which should be propagated.

More precisely in case where for example the spec part of the primary resource was updated, while
controller patched the status, but that patch status was a no-op operation. Note that
these no-op operations are causing issue, which should not be done in first place.
From 5.5 we provide methods in `ResourceOperations` which allow only operations which are
correct.

From v5.5, `UpdateControl` **no longer filters these own events by default**. Returning an
`UpdateControl` still updates the resource and keeps the cache read-after-write consistent, but the
resulting update event is now delivered like any other event, which may cause an additional
reconciliation.

This is safe (reconciliations are expected to be idempotent), but if you relied on the previous
filtering — for example to avoid an extra reconciliation after a status update — use
[`ResourceOperations`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperations.java)
directly, which does filter own events.

### Using `ResourceOperations` instead

`ResourceOperations` is available from the reconciliation `Context` via
`context.resourceOperations()`. Instead of returning an `UpdateControl`, perform the update through
it and return `UpdateControl.noUpdate()`:

```java
// before (v5.4): the own status update event was filtered
@Override
public UpdateControl<MyResource> reconcile(MyResource resource, Context<MyResource> context) {
resource.setStatus(new MyStatus().setReady(true));
return UpdateControl.patchStatus(resource);
}
```

```java
// after (v5.5): filter the own event explicitly via ResourceOperations
@Override
public UpdateControl<MyResource> reconcile(MyResource resource, Context<MyResource> context) {
resource.setStatus(new MyStatus().setReady(true));
context.resourceOperations().serverSideApplyPrimaryStatus(resource);
return UpdateControl.noUpdate();
}
```

`ResourceOperations` covers every update/patch strategy (server-side apply, update, JSON Patch, JSON
Merge Patch) for both the whole resource and the status subresource, as well as their primary
variants. By default these operations match the desired state against the actual (cached) state
before writing and filter the own event, so they only issue a request to the Kubernetes API server
when something actually changed.

> **Note**: Safe own-event filtering requires either a matcher (used by default) or the update to be
> done with optimistic locking. See the `ResourceOperations` and `ResourceOperations.Options`
> documentation for the available modes, the correctness requirements, and the caveats of the
> default matchers.
Loading
Loading