Skip to content

Commit 4caf4d0

Browse files
committed
docs
Signed-off-by: Attila Mészáros <[email protected]>
1 parent e966d57 commit 4caf4d0

File tree

6 files changed

+143
-74
lines changed

6 files changed

+143
-74
lines changed

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

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -175,20 +175,23 @@ From v5, by default, the finalizer is added using Server Side Apply. See also `U
175175
It is typical to want to update the status subresource with the information that is available during the reconciliation.
176176
This is sometimes referred to as the last observed state. When the primary resource is updated, though, the framework
177177
does not cache the resource directly, relying instead on the propagation of the update to the underlying informer's
178-
cache. It can, therefore, happen that, if other events trigger other reconciliations before the informer cache gets
178+
cache. It can, therefore, happen that, if other events trigger other reconciliations, before the informer cache gets
179179
updated, your reconciler does not see the latest version of the primary resource. While this might not typically be a
180180
problem in most cases, as caches eventually become consistent, depending on your reconciliation logic, you might still
181-
require the latest status version possible, for example if the status subresource is used as a communication mechanism,
182-
see [Representing Allocated Values](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#representing-allocated-values)
181+
require the latest status version possible, for example, if the status subresource is used to store allocated values.
182+
See [Representing Allocated Values](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#representing-allocated-values)
183183
from the Kubernetes docs for more details.
184184

185185
The framework provides utilities to help with these use cases with
186186
[`PrimaryUpdateAndCacheUtils`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java).
187-
These utility methods come in two flavors:
187+
These utility methods come in multiple flavors:
188188

189189
#### Using internal cache
190190

191-
In almost all cases for this purpose, you can use internal caches:
191+
In almost all cases for this purpose, you can use internal caches in combination with update methods that use
192+
optimistic locking (end with *WithLock(...)). If the update method fails on optimistic locking, it will retry
193+
using a fresh resource from the server as base for modification. Again, this is the default option and will probably
194+
work for you.
192195

193196
```java
194197
@Override
@@ -201,27 +204,32 @@ public UpdateControl<StatusPatchCacheCustomResource> reconcile(
201204
var freshCopy = createFreshCopy(primary);
202205
freshCopy.getStatus().setValue(statusWithState());
203206

204-
var updatedResource = PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResource(resource, freshCopy, context);
207+
var updatedResource = PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResourceWithLock(resource, freshCopy, context);
205208

206209
return UpdateControl.noUpdate();
207210
}
208211
```
209212

210-
In the background `PrimaryUpdateAndCacheUtils.ssaPatchAndCacheStatus` puts the result of the update into an internal
211-
cache and will make sure that the next reconciliation will contain the most recent version of the resource. Note that it
212-
is not necessarily the version of the resource you got as response from the update, it can be newer since other parties
213+
After the update `PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResourceWithLock` puts the result of the update into an internal
214+
cache and will the framework will make sure that the next reconciliation will contain the most recent version of the resource.
215+
Note that it is not necessarily the version of the resource you got as response from the update, it can be newer since other parties
213216
can do additional updates meanwhile, but if not explicitly modified, it will contain the up-to-date status.
214217

215-
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).
218+
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/internalwithlock).
216219

217220
This approach works with the default configuration of the framework and should be good to go in most of the cases.
218-
Without going further into the details, this won't work if `ConfigurationService.parseResourceVersionsForEventFilteringAndCaching`
219-
is set to `false` (more precisely there are some edge cases when it won't work). For that case framework provides the following solution:
221+
222+
Without going further into the details, a bit more experimental way we provide overloaded methods without optimistic locking,
223+
to use those you have to set `ConfigurationService.parseResourceVersionsForEventFilteringAndCaching`
224+
to `true`. This in practice would mean that request won't fail on optimistic locking, but requires bending a bit
225+
the rules regarding Kubernetes API contract. This might be needed only if you have multiple resources frequently
226+
writing the resource.
220227

221228
#### Fallback approach: using `PrimaryResourceCache` cache
222229

223-
As an alternative, for very rare cases when `ConfigurationService.parseResourceVersionsForEventFilteringAndCaching`
224-
needs to be set to `false` you can use an explicit caching approach:
230+
For the sake of completeness, we also provide a more explicit approach to manage the cache yourself.
231+
This approach has the advantage that you don't have to do neither optimistic locking nor
232+
setting the `parseResourceVersionsForEventFilteringAndCaching` to `true`:
225233

226234
```java
227235

@@ -277,9 +285,7 @@ their associated primary resource from the underlying informer event source cach
277285

278286
#### Additional remarks
279287

280-
As shown in the integration tests, there is no optimistic locking used when updating the
288+
As shown in the last two cases, there is no optimistic locking used when updating the
281289
[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)
282-
(in other words `metadata.resourceVersion` is set to `null`). This is desired since you don't want the patch to fail on
283-
update.
284-
285-
In addition, you can configure the [Fabric8 client retry](https://github.com/fabric8io/kubernetes-client?tab=readme-ov-file#configuring-the-client).
290+
(in other words `metadata.resourceVersion` is set to `null`). This has nice property the request will be successful.
291+
However, it might be desirable to configure retry on [Fabric8 client](https://github.com/fabric8io/kubernetes-client?tab=readme-ov-file#configuring-the-client).

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

Lines changed: 101 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@
2222
* various options, where all of them have pros and cons.
2323
*
2424
* <ul>
25-
* <li>Retryable updates with optimistic locking (*withLock) - you can use this approach out of
26-
* the box, it updates the resource using optimistic locking and caches the resource. If the
27-
* update fails it reads the primary resource and applies the modifications again and retries
28-
* the update. After successful update it caches the resource for next reconciliation. The
29-
* disadvantage of this method is that theoretically it could fail the max attempt retry. Note
30-
* that optimistic locking is essential to have the caching work in general.
25+
* <li>(Preferred) Retryable updates with optimistic locking (*withLock) - you can use this
26+
* approach out of the box, it updates the resource using optimistic locking and caches the
27+
* resource. If the update fails it reads the primary resource and applies the modifications
28+
* again and retries the update. After successful update it caches the resource for next
29+
* reconciliation. The disadvantage of this method is that theoretically it could fail the max
30+
* attempt retry. Note that optimistic locking is essential to have the caching work in
31+
* general.
3132
* <li>Caching without optimistic locking but with parsing the resource version - to use this you
3233
* have to set {@link ConfigurationService#parseResourceVersionsForEventFilteringAndCaching()}
3334
* to true. The update won't fail on optimistic locking so there is much higher chance to
@@ -48,6 +49,65 @@ private PrimaryUpdateAndCacheUtils() {}
4849

4950
private static final Logger log = LoggerFactory.getLogger(PrimaryUpdateAndCacheUtils.class);
5051

52+
/**
53+
* Updates the status with optimistic locking and caches the result for next reconciliation. For
54+
* details see {@link #updateAndCacheResourceWithLock}.
55+
*/
56+
public static <P extends HasMetadata> P updateStatusAndCacheResourceWithLock(
57+
P primary, Context<P> context, UnaryOperator<P> modificationFunction) {
58+
return updateAndCacheResourceWithLock(
59+
primary,
60+
context,
61+
modificationFunction,
62+
r -> context.getClient().resource(r).updateStatus());
63+
}
64+
65+
/**
66+
* Patches the status using JSON Merge Patch with optimistic locking and caches the result for
67+
* next reconciliation. For details see {@link #updateAndCacheResourceWithLock}.
68+
*/
69+
public static <P extends HasMetadata> P patchStatusAndCacheResourceWithLock(
70+
P primary, Context<P> context, UnaryOperator<P> modificationFunction) {
71+
return updateAndCacheResourceWithLock(
72+
primary, context, modificationFunction, r -> context.getClient().resource(r).patchStatus());
73+
}
74+
75+
/**
76+
* Patches the status using JSON Patch with optimistic locking and caches the result for next
77+
* reconciliation. For details see {@link #updateAndCacheResourceWithLock}.
78+
*/
79+
public static <P extends HasMetadata> P editStatusAndCacheResourceWithLock(
80+
P primary, Context<P> context, UnaryOperator<P> modificationFunction) {
81+
return updateAndCacheResourceWithLock(
82+
primary,
83+
context,
84+
UnaryOperator.identity(),
85+
r -> context.getClient().resource(r).editStatus(modificationFunction));
86+
}
87+
88+
/**
89+
* Patches the status using Server Side Apply with optimistic locking and caches the result for
90+
* next reconciliation. For details see {@link #updateAndCacheResourceWithLock}.
91+
*/
92+
public static <P extends HasMetadata> P ssaPatchStatusAndCacheResourceWithLock(
93+
P primary, P freshResourceWithStatus, Context<P> context) {
94+
return updateAndCacheResourceWithLock(
95+
primary,
96+
context,
97+
r -> freshResourceWithStatus,
98+
r ->
99+
context
100+
.getClient()
101+
.resource(r)
102+
.subresource("status")
103+
.patch(
104+
new PatchContext.Builder()
105+
.withForce(true)
106+
.withFieldManager(context.getControllerConfiguration().fieldManager())
107+
.withPatchType(PatchType.SERVER_SIDE_APPLY)
108+
.build()));
109+
}
110+
51111
/**
52112
* Updates status and makes sure that the up-to-date primary resource will be present during the
53113
* next reconciliation. Using update (PUT) method.
@@ -64,15 +124,6 @@ public static <P extends HasMetadata> P updateStatusAndCacheResource(
64124
primary, context, () -> context.getClient().resource(primary).updateStatus());
65125
}
66126

67-
public static <P extends HasMetadata> P updateStatusAndCacheResourceWithLock(
68-
P primary, Context<P> context, UnaryOperator<P> modificationFunction) {
69-
return updateAndCacheResourceWithLock(
70-
primary,
71-
context,
72-
modificationFunction,
73-
r -> context.getClient().resource(r).updateStatus());
74-
}
75-
76127
/**
77128
* Patches status with and makes sure that the up-to-date primary resource will be present during
78129
* the next reconciliation. Using JSON Merge patch.
@@ -89,12 +140,6 @@ public static <P extends HasMetadata> P patchStatusAndCacheResource(
89140
primary, context, () -> context.getClient().resource(primary).patchStatus());
90141
}
91142

92-
public static <P extends HasMetadata> P patchStatusAndCacheResourceWithLock(
93-
P primary, Context<P> context, UnaryOperator<P> modificationFunction) {
94-
return updateAndCacheResourceWithLock(
95-
primary, context, modificationFunction, r -> context.getClient().resource(r).patchStatus());
96-
}
97-
98143
/**
99144
* Patches status and makes sure that the up-to-date primary resource will be present during the
100145
* next reconciliation. Using JSON Patch.
@@ -116,15 +161,6 @@ public static <P extends HasMetadata> P editStatusAndCacheResource(
116161
primary, context, () -> context.getClient().resource(primary).editStatus(operation));
117162
}
118163

119-
public static <P extends HasMetadata> P editStatusAndCacheResourceWithLock(
120-
P primary, Context<P> context, UnaryOperator<P> modificationFunction) {
121-
return updateAndCacheResourceWithLock(
122-
primary,
123-
context,
124-
UnaryOperator.identity(),
125-
r -> context.getClient().resource(r).editStatus(modificationFunction));
126-
}
127-
128164
/**
129165
* Patches the resource with supplied method and makes sure that the up-to-date primary resource
130166
* will be present during the next reconciliation.
@@ -174,25 +210,6 @@ public static <P extends HasMetadata> P ssaPatchStatusAndCacheResource(
174210
.build()));
175211
}
176212

177-
public static <P extends HasMetadata> P ssaPatchStatusAndCacheResourceWithLock(
178-
P primary, P freshResourceWithStatus, Context<P> context) {
179-
return updateAndCacheResourceWithLock(
180-
primary,
181-
context,
182-
r -> freshResourceWithStatus,
183-
r ->
184-
context
185-
.getClient()
186-
.resource(r)
187-
.subresource("status")
188-
.patch(
189-
new PatchContext.Builder()
190-
.withForce(true)
191-
.withFieldManager(context.getControllerConfiguration().fieldManager())
192-
.withPatchType(PatchType.SERVER_SIDE_APPLY)
193-
.build()));
194-
}
195-
196213
/**
197214
* Patches the resource status and caches the response in provided {@link PrimaryResourceCache}.
198215
* Uses Server Side Apply.
@@ -309,6 +326,23 @@ private static <P extends HasMetadata> void checkResourceVersionNotPresentAndPar
309326
}
310327
}
311328

329+
/**
330+
* Modifies the primary using modificationFunction, then uses the modified resource for the
331+
* request to update with provided update method. But before the update operation sets the
332+
* resourceVersion to the modified resource from the primary resource, so there is always
333+
* optimistic locking happening. If the request fails on optimistic update, we read the resource
334+
* again from the K8S API server and retry the whole process. In short, we make sure we always
335+
* update the resource with optimistic locking, after we cache the resource in internal cache.
336+
* Without further going into the details, the optimistic locking is needed so we can reliably
337+
* handle the caching.
338+
*
339+
* @param primary original resource to update
340+
* @param context of reconciliation
341+
* @param modificationFunction modifications to make on primary
342+
* @param updateMethod the update method implementation
343+
* @return updated resource
344+
* @param <P> primary type
345+
*/
312346
public static <P extends HasMetadata> P updateAndCacheResourceWithLock(
313347
P primary,
314348
Context<P> context,
@@ -318,6 +352,24 @@ public static <P extends HasMetadata> P updateAndCacheResourceWithLock(
318352
primary, context, modificationFunction, updateMethod, DEFAULT_MAX_RETRY);
319353
}
320354

355+
/**
356+
* Modifies the primary using modificationFunction, then uses the modified resource for the
357+
* request to update with provided update method. But before the update operation sets the
358+
* resourceVersion to the modified resource from the primary resource, so there is always
359+
* optimistic locking happening. If the request fails on optimistic update, we read the resource
360+
* again from the K8S API server and retry the whole process. In short, we make sure we always
361+
* update the resource with optimistic locking, after we cache the resource in internal cache.
362+
* Without further going into the details, the optimistic locking is needed so we can reliably
363+
* handle the caching.
364+
*
365+
* @param primary original resource to update
366+
* @param context of reconciliation
367+
* @param modificationFunction modifications to make on primary
368+
* @param updateMethod the update method implementation
369+
* @param maxRetry - maximum number of retries of conflicts
370+
* @return updated resource
371+
* @param <P> primary type
372+
*/
321373
@SuppressWarnings("unchecked")
322374
public static <P extends HasMetadata> P updateAndCacheResourceWithLock(
323375
P primary,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ public UpdateControl<StatusPatchCacheCustomResource> reconcile(
3131
+ resource.getStatus().getValue());
3232
}
3333

34+
// test also resource update happening meanwhile reconciliation
35+
resource.getSpec().setCounter(resource.getSpec().getCounter() + 1);
36+
context.getClient().resource(resource).update();
37+
3438
var freshCopy = createFreshCopy(resource);
3539

3640
freshCopy

operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/internalwithlock/StatusPatchCacheWithLockReconciler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ public UpdateControl<StatusPatchCacheWithLockCustomResource> reconcile(
3333
+ resource.getStatus().getValue());
3434
}
3535

36+
// test also resource update happening meanwhile reconciliation
37+
resource.getSpec().setCounter(resource.getSpec().getCounter() + 1);
38+
context.getClient().resource(resource).update();
39+
3640
var freshCopy = createFreshCopy(resource);
3741

3842
freshCopy

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public UpdateControl<StatusPatchPrimaryCacheCustomResource> reconcile(
4646
+ primary.getStatus().getValue());
4747
}
4848

49+
// test also resource update happening meanwhile reconciliation
50+
primary.getSpec().setCounter(primary.getSpec().getCounter() + 1);
51+
context.getClient().resource(primary).update();
52+
4953
var freshCopy = createFreshCopy(primary);
5054
freshCopy
5155
.getStatus()

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
public class StatusPatchPrimaryCacheSpec {
44

5-
private boolean messageInStatus = true;
5+
private int counter = 0;
66

7-
public boolean isMessageInStatus() {
8-
return messageInStatus;
7+
public int getCounter() {
8+
return counter;
99
}
1010

11-
public StatusPatchPrimaryCacheSpec setMessageInStatus(boolean messageInStatus) {
12-
this.messageInStatus = messageInStatus;
13-
return this;
11+
public void setCounter(int counter) {
12+
this.counter = counter;
1413
}
1514
}

0 commit comments

Comments
 (0)