Skip to content
9 changes: 9 additions & 0 deletions docs/content/en/blog/news/primary-cache-for-next-recon.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ author: >-
[Attila Mészáros](https://github.com/csviri) and [Chris Laprun](https://github.com/metacosm)
---

{{% alert title="Deprecated" %}}

Read-cache-after-write consistency feature replaces this functionality. (since version 5.3.0)

> It provides this functionality also for secondary resources and optimistic locking
is not required anymore. See [details here](./../../docs/documentation/reconciler.md#read-cache-after-write-consistency-and-event-filtering).
{{% /alert %}}


We recently released v5.1 of Java Operator SDK (JOSDK). One of the highlights of this release is related to a topic of
so-called
[allocated values](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#representing-allocated-values
Expand Down
113 changes: 100 additions & 13 deletions docs/content/en/docs/documentation/reconciler.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,94 @@ annotation. If you do not specify a finalizer name, one will be automatically ge

From v5, by default, the finalizer is added using Server Side Apply. See also `UpdateControl` in docs.

### Making sure the primary resource is up to date for the next reconciliation
### Read-cache-after-write consistency and event filtering

It is an inherent issue with Informers that their caches are eventually consistent even
with the updates to Kubernetes API done from the controller. From version 5.3.0 the framework
supports stronger guarantees, both for primary and secondary resources. If this feature is used:

1. Reading from the cache after our update — even within the same reconciliation — returns a fresh resource.
"Fresh" means at least the version of the resource that is in the response from our update,
or a more recent version if some other party updated the resource after our update.
2. Filtering events for our updates. If the controller updates a resource
an event is produced by the Kubernetes API and propagated to Informer, what would normally trigger a next
reconciliation. However, this is not ideal, since we already have that up-to-date resource
in the cache in the current reconciliation, so in general it is not desirable to reconcile that again.
This feature also makes sure that the reconciliation is not triggered from the event from our writes.


In order to have these 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:

```java

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

ConfigMap managedConfigMap = prepareConfigMap(webPage);
// filtering and caching update
context.resourceOperations().serverSideApply(managedConfigMap);

// fresh resource instantly available from our update in the cache
var upToDateResource = context.getSecondaryResource(ConfigMap.class);

makeStatusChanges(webPage);

// built in update methods by default use this feature
return UpdateControl.patchStatus(webPage);
}
```

### Built-in patches

`UpdateControl` and `ErrorStatusUpdateControl` by default uses this functionality.

Mainly to cover migration path for the cases when somebody expected events for their update
these controls now contain a new method: `UpdateControl.reschedule()`, that instantly propagates
an event to reschedule the reconciliation.

### Allocated values

If you want to store some state - like generated IDs - in `.status` sub-resource, you
can do it safely with this feature. Since it is guaranteed that you will see the update
in the next reconciliation.

Note that this is not just with `UpdateControl` you can also do update any time on primary resource
with `resourceOperations`:

```java

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

makeStatusChanges(webPage);
// this is equivalent to UpdateControl.patchStatus(webpage)
context.resourceOperations().serverSideApplyPrimaryStatus(webPage);
return UpdateControl.noUpdate();
}
```

### Caveats

- This feature is implemented on top of fabric8 client informers using additional caches in `InformerEventSource`,
so it is safe to use `context.getSecondaryResources(..)` or `InformerEventSource.get(ResourceID)`
methods. However won't work with `InformerEventSource.list(..)` method, since it directly reads
the underlying informer cache.


### Notes
- Talk about this feature in this [talk](https://www.youtube.com/watch?v=HrwHh5Yh6AM&t=1387s).
- [Umbrella issue](https://github.com/operator-framework/java-operator-sdk/issues/2944) on our GitHub.
- We were able to implement this feature since Kubernetes introduces guideline to compare
resource versions. See the details [here](https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/5504-comparable-resource-version).

### Making sure the primary resource is up to date for the next reconciliation (deprecated)

{{% alert title="Deprecated" %}}

Read-cache-after-write consistency feature replaces this functionality.

> It provides this functionality also for secondary resources and optimistic locking
is not required anymore. See details above.
{{% /alert %}}

It is typical to want to update the status subresource with the information that is available during the reconciliation.
This is sometimes referred to as the last observed state. When the primary resource is updated, though, the framework
Expand Down Expand Up @@ -263,30 +350,30 @@ See also [sample](https://github.com/operator-framework/java-operator-sdk/blob/m
### Expectations

Expectations are a pattern to ensure that, during reconciliation, your secondary resources are in a certain state.
For a more detailed explanation see [this blogpost](https://ahmet.im/blog/controller-pitfalls/#expectations-pattern).
You can find framework support for this pattern in [`io.javaoperatorsdk.operator.processing.expectation`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/expectation/)
package. See also related [integration test](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/expectation/ExpectationReconciler.java).
Note that this feature is marked as `@Experimental`, since based on feedback the API might be improved / changed, but we intend
to support it, later also might be integrated to Dependent Resources and/or Workflows.
For a more detailed explanation, see [this blogpost](https://ahmet.im/blog/controller-pitfalls/#expectations-pattern).
You can find framework support for this pattern in the [`io.javaoperatorsdk.operator.processing.expectation`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/expectation/)
package. See also the related [integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/expectation/onallevent/ExpectationReconciler.java).
Note that this feature is marked as `@Experimental`: based on feedback the API may be improved or changed, but we intend
to keep supporting it and may later integrate it into Dependent Resources and/or Workflows.

The idea is the nutshell, is that you can track your expectations in the expectation manager in the reconciler
which has an API that covers the common use cases.
The idea, in a nutshell, is that you can track your expectations in the expectation manager within the reconciler,
which provides an API covering the common use cases.

The following sample is the simplified version of the integration test that implements the logic that creates a
deployment and sets status message if there are the target three replicas ready:
The following is a simplified version of the integration test that implements the logic to create a
deployment and set a status message once the target three replicas are ready:

```java
public class ExpectationReconciler implements Reconciler<ExpectationCustomResource> {

// some code is omitted

private final ExpectationManager<ExpectationCustomResource> expectationManager =
new ExpectationManager<>();

@Override
public UpdateControl<ExpectationCustomResource> reconcile(
ExpectationCustomResource primary, Context<ExpectationCustomResource> context) {
// exiting asap if there is an expectation that is not timed out neither fulfilled yet
// exit early if there is an expectation that has not yet timed out or been fulfilled
if (expectationManager.ongoingExpectationPresent(primary, context)) {
return UpdateControl.noUpdate();
}
Expand All @@ -299,7 +386,7 @@ public class ExpectationReconciler implements Reconciler<ExpectationCustomResour
return UpdateControl.noUpdate();
} else {
// Checks the expectation and removes it once it is fulfilled.
// In your logic you might add a next expectation based on your workflow.
// In your logic, you might add a next expectation based on your workflow.
// Expectations have a name, so you can easily distinguish multiple expectations.
var res = expectationManager.checkExpectation("deploymentReadyExpectation", primary, context);
if (res.isFulfilled()) {
Expand Down
6 changes: 6 additions & 0 deletions docs/content/en/docs/documentation/working-with-es-caches.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,9 @@ With this index in place, you can retrieve the target resources very efficiently
.collect(Collectors.toSet()))
.withNamespacesInheritedFromController().build(), context);
```

## Read-cache-after-write consistency and event filtering

From version 5.3.0 we provide stronger consistency guarantees and
other features on top of basic informers, see [this section](reconciler.md#read-cache-after-write-consistency-and-event-filtering)
for details.