Skip to content

Commit a71eafe

Browse files
committed
docs
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent e8ede1a commit a71eafe

File tree

4 files changed

+129
-16
lines changed

4 files changed

+129
-16
lines changed

docs/content/en/docs/documentation/reconciler.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,121 @@ You can specify the name of the finalizer to use for your `Reconciler` using the
169169
annotation. If you do not specify a finalizer name, one will be automatically generated for you.
170170

171171
From v5, by default, the finalizer is added using Server Side Apply. See also `UpdateControl` in docs.
172+
173+
### Making sure primary is up to date for the next reconciliation
174+
175+
When you implement a reconciler as a final step (but maybe also multiple times during one reconciliation), you
176+
usually update the status subresource with the information that was available during the reconciliation.
177+
Sometimes this is referred to as the last observed state.
178+
When the resource is updated, the framework does not cache the resource directly from the response of the update.
179+
Instead, the underlying informer eventually receives an event with the updated resource and caches the resource.
180+
Therefore, it can happen that on next reconciliation the primary resource is not up-to-date regarding your updated (note that other event sources
181+
can trigger the reconciliation meanwhile). This is not usually a problem, since the status is not used as an input,
182+
the reconciliation runs again, and the status us updated again. The caches are eventually consistent.
183+
184+
However, there are cases when you would like to store some state in the status, typically generated
185+
IDs of external resources.
186+
See related topic in Kubernetes docs: [Representing Allocated Values](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#representing-allocated-values).
187+
In this case, it is reasonable to expect to have the state always available for the next reconciliation,
188+
to avoid generating the resource again and other race conditions.
189+
190+
Therefore,
191+
the framework provides facilities
192+
to cover these use cases withing [`PrimaryUpdateAndCacheUtils`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java#L16).
193+
These utility methods come in two flavors:
194+
195+
#### Using internal cache
196+
197+
In almost all cases for this purpose, you can use internal caches:
198+
199+
```java
200+
@Override
201+
public UpdateControl<StatusPatchCacheCustomResource> reconcile(
202+
StatusPatchCacheCustomResource resource, Context<StatusPatchCacheCustomResource> context) {
203+
204+
// omitted logic
205+
206+
// update with SSA requires a fresh copy
207+
var freshCopy = createFreshCopy(primary);
208+
209+
freshCopy.getStatus().setValue(statusWithState());
210+
211+
var updatedResource = PrimaryUpdateAndCacheUtils.ssaPatchAndCacheStatus(resource, freshCopy, context);
212+
213+
return UpdateControl.noUpdate();
214+
}
215+
```
216+
217+
In the background `PrimaryUpdateAndCacheUtils.ssaPatchAndCacheStatusWith` puts the result of the update into an internal
218+
cache of the event source of primary resource, and will make sure that the next reconciliation will contain the most
219+
recent version of the resource. Note that it is not necessarily the version from the update response, it can be newer
220+
since other parties can do additional updates meanwhile, but if not explicitly modified, it will contain the up-to-date
221+
status.
222+
223+
See related integration test [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/internal).
224+
225+
This approach works with the default configuration of the framework and should be good to go in most of the cases.
226+
Without going further into the details, this won't work if `ConfigurtionService.parseResourceVersionsForEventFilteringAndCaching`
227+
is set to `false` (more precisely there are some edge cases when it won't work). For that case framework provides the following solution:
228+
229+
#### Using `PrimaryResourceCache` cache
230+
231+
As an alternative, you can use an explicit caching approach that the framework supports:
232+
233+
```java
234+
235+
// We on purpose don't use the provided predicate to show what a custom one could look like.
236+
private final PrimaryResourceCache<StatusPatchPrimaryCacheCustomResource> cache =
237+
new PrimaryResourceCache<>(
238+
(statusPatchCacheCustomResourcePair, statusPatchCacheCustomResource) ->
239+
statusPatchCacheCustomResource.getStatus().getValue()
240+
>= statusPatchCacheCustomResourcePair.afterUpdate().getStatus().getValue());
241+
242+
@Override
243+
public UpdateControl<StatusPatchPrimaryCacheCustomResource> reconcile(
244+
StatusPatchPrimaryCacheCustomResource primary,
245+
Context<StatusPatchPrimaryCacheCustomResource> context) {
246+
247+
//
248+
primary = cache.getFreshResource(primary);
249+
250+
// omitted logic
251+
252+
var freshCopy = createFreshCopy(primary);
253+
254+
freshCopy.getStatus().setValue(statusWithState());
255+
256+
var updated =
257+
PrimaryUpdateAndCacheUtils.ssaPatchAndCacheStatus(primary, freshCopy, context, cache);
258+
259+
return UpdateControl.noUpdate();
260+
}
261+
262+
@Override
263+
public DeleteControl cleanup(
264+
StatusPatchPrimaryCacheCustomResource resource,
265+
Context<StatusPatchPrimaryCacheCustomResource> context)
266+
throws Exception {
267+
// cleanup the cache on resource deletion
268+
cache.cleanup(resource);
269+
return DeleteControl.defaultDelete();
270+
}
271+
272+
```
273+
274+
[`PrimaryResourceCache`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/support/PrimaryResourceCache.java)
275+
is designed for this purpose.
276+
As shown in the example above, it is up to you to provide a predicate to determine if the resource is more recent than the one available.
277+
In other words, when to evict the resource from the cache. Typically, as show in the [integration test](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/primarycache)
278+
you can have a counter in status to check on that.
279+
280+
Since all of this happens explicitly, you cannot use it for now with managed dependent resources and workflows.
281+
282+
#### Additional remarks
283+
284+
As shown in the integration tests, there is no optimistic locking used when updating the
285+
[resource](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/internal/StatusPatchCacheReconciler.java#L41)
286+
(in other works `metadata.resourceVersion` is set to `null`).
287+
This is desired since you don't want the patch to fail on update.
288+
289+
In addition, you can configure retry for in fabric8 client.

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ private PrimaryUpdateAndCacheUtils() {}
2828
* @return updated resource
2929
* @param <P> primary resource type
3030
*/
31-
public static <P extends HasMetadata> P updateAndCacheStatusWith(P primary, Context<P> context) {
32-
return patchAndCacheStatusWith(primary, context, (p, c) -> c.resource(primary).updateStatus());
31+
public static <P extends HasMetadata> P updateAndCacheStatus(P primary, Context<P> context) {
32+
return patchAndCacheStatus(primary, context, (p, c) -> c.resource(primary).updateStatus());
3333
}
3434

3535
/**
@@ -41,8 +41,8 @@ public static <P extends HasMetadata> P updateAndCacheStatusWith(P primary, Cont
4141
* @return updated resource
4242
* @param <P> primary resource type
4343
*/
44-
public static <P extends HasMetadata> P patchAndCacheStatusWith(P primary, Context<P> context) {
45-
return patchAndCacheStatusWith(primary, context, (p, c) -> c.resource(primary).patchStatus());
44+
public static <P extends HasMetadata> P patchAndCacheStatus(P primary, Context<P> context) {
45+
return patchAndCacheStatus(primary, context, (p, c) -> c.resource(primary).patchStatus());
4646
}
4747

4848
/**
@@ -54,9 +54,9 @@ public static <P extends HasMetadata> P patchAndCacheStatusWith(P primary, Conte
5454
* @return updated resource
5555
* @param <P> primary resource type
5656
*/
57-
public static <P extends HasMetadata> P editAndCacheStatusWith(
57+
public static <P extends HasMetadata> P editAndCacheStatus(
5858
P primary, Context<P> context, UnaryOperator<P> operation) {
59-
return patchAndCacheStatusWith(
59+
return patchAndCacheStatus(
6060
primary, context, (p, c) -> c.resource(primary).editStatus(operation));
6161
}
6262

@@ -70,7 +70,7 @@ public static <P extends HasMetadata> P editAndCacheStatusWith(
7070
* @return the updated resource.
7171
* @param <P> primary resource type
7272
*/
73-
public static <P extends HasMetadata> P patchAndCacheStatusWith(
73+
public static <P extends HasMetadata> P patchAndCacheStatus(
7474
P primary, Context<P> context, BiFunction<P, KubernetesClient, P> patch) {
7575
var updatedResource = patch.apply(primary, context.getClient());
7676
context
@@ -90,7 +90,7 @@ public static <P extends HasMetadata> P patchAndCacheStatusWith(
9090
* @return the updated resource.
9191
* @param <P> primary resource type
9292
*/
93-
public static <P extends HasMetadata> P ssaPatchAndCacheStatusWith(
93+
public static <P extends HasMetadata> P ssaPatchAndCacheStatus(
9494
P primary, P freshResourceWithStatus, Context<P> context) {
9595
var res =
9696
context

operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/internal/StatusPatchCacheReconciler.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ public class StatusPatchCacheReconciler implements Reconciler<StatusPatchCacheCu
2020

2121
@Override
2222
public UpdateControl<StatusPatchCacheCustomResource> reconcile(
23-
StatusPatchCacheCustomResource resource, Context<StatusPatchCacheCustomResource> context)
24-
throws InterruptedException {
23+
StatusPatchCacheCustomResource resource, Context<StatusPatchCacheCustomResource> context) {
2524

2625
if (resource.getStatus() != null && resource.getStatus().getValue() != latestValue) {
2726
errorPresent = true;
@@ -40,8 +39,7 @@ public UpdateControl<StatusPatchCacheCustomResource> reconcile(
4039
.setValue(resource.getStatus() == null ? 1 : resource.getStatus().getValue() + 1);
4140

4241
resource.getMetadata().setResourceVersion(null);
43-
var updated =
44-
PrimaryUpdateAndCacheUtils.ssaPatchAndCacheStatusWith(resource, freshCopy, context);
42+
var updated = PrimaryUpdateAndCacheUtils.ssaPatchAndCacheStatus(resource, freshCopy, context);
4543
latestValue = updated.getStatus().getValue();
4644

4745
return UpdateControl.noUpdate();

operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/primarycache/StatusPatchPrimaryCacheReconciler.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ public class StatusPatchPrimaryCacheReconciler
3333
@Override
3434
public UpdateControl<StatusPatchPrimaryCacheCustomResource> reconcile(
3535
StatusPatchPrimaryCacheCustomResource primary,
36-
Context<StatusPatchPrimaryCacheCustomResource> context)
37-
throws InterruptedException {
36+
Context<StatusPatchPrimaryCacheCustomResource> context) {
3837

3938
primary = cache.getFreshResource(primary);
4039

@@ -48,8 +47,6 @@ public UpdateControl<StatusPatchPrimaryCacheCustomResource> reconcile(
4847
}
4948

5049
var freshCopy = createFreshCopy(primary);
51-
// setting the resource version
52-
freshCopy.getMetadata().setResourceVersion(primary.getMetadata().getResourceVersion());
5350
freshCopy
5451
.getStatus()
5552
.setValue(primary.getStatus() == null ? 1 : primary.getStatus().getValue() + 1);

0 commit comments

Comments
 (0)