Skip to content

fix: retry finalizer removal on http 422 #2776

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 30, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,13 @@ public P conflictRetryingPatch(
} catch (KubernetesClientException e) {
log.trace("Exception during patch for resource: {}", resource);
retryIndex++;
// only retry on conflict (HTTP 409), otherwise fail
if (e.getCode() != 409) {
// only retry on conflict (409) and unprocessable content (422) which
// can happen if JSON Patch is not a valid request since there was
// a concurrent request which already removed another finalizer:
// List element removal from a list is by index in JSON Patch
// so if addressing a second finalizer but first is meanwhile removed
// it is a wrong request.
if (e.getCode() != 409 && e.getCode() != 422) {
throw e;
}
if (retryIndex >= MAX_UPDATE_RETRY) {
Expand All @@ -392,6 +397,11 @@ public P conflictRetryingPatch(
+ ") retry attempts to patch resource: "
+ ResourceID.fromResource(resource));
}
log.debug(
"Retrying patch for resource name: {}, namespace: {}; HTTP code: {}",
resource.getMetadata().getName(),
resource.getMetadata().getNamespace(),
e.getCode());
resource =
customResourceFacade.getResource(
resource.getMetadata().getNamespace(), resource.getMetadata().getName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.javaoperatorsdk.operator.baseapi.concurrentfinalizerremoval;

import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.ShortNames;
import io.fabric8.kubernetes.model.annotation.Version;

@Group("sample.javaoperatorsdk")
@Version("v1")
@ShortNames("cfr")
public class ConcurrentFinalizerRemovalCustomResource
extends CustomResource<ConcurrentFinalizerRemovalSpec, Void> implements Namespaced {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.javaoperatorsdk.operator.baseapi.concurrentfinalizerremoval;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
import io.javaoperatorsdk.operator.processing.retry.GenericRetry;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

class ConcurrentFinalizerRemovalIT {

private static final Logger log = LoggerFactory.getLogger(ConcurrentFinalizerRemovalIT.class);
public static final String TEST_RESOURCE_NAME = "test";

@RegisterExtension
LocallyRunOperatorExtension extension =
LocallyRunOperatorExtension.builder()
// should work without a retry, thus not retry the whole reconciliation but to retry
// finalizer removal only.
.withReconciler(
new ConcurrentFinalizerRemovalReconciler1(),
o ->
o.withRetry(GenericRetry.noRetry()).withFinalizer("reconciler1.sample/finalizer"))
.withReconciler(
new ConcurrentFinalizerRemovalReconciler2(),
o ->
o.withRetry(GenericRetry.noRetry()).withFinalizer("reconciler2.sample/finalizer"))
.build();

@Test
void concurrentFinalizerRemoval() {
for (int i = 0; i < 10; i++) {
var resource = extension.create(createResource());
await()
.untilAsserted(
() -> {
var res =
extension.get(
ConcurrentFinalizerRemovalCustomResource.class, TEST_RESOURCE_NAME);
assertThat(res.getMetadata().getFinalizers()).hasSize(2);
});
resource.getMetadata().setResourceVersion(null);
extension.delete(resource);

await()
.untilAsserted(
() -> {
var res =
extension.get(
ConcurrentFinalizerRemovalCustomResource.class, TEST_RESOURCE_NAME);
assertThat(res).isNull();
});
}
}

public ConcurrentFinalizerRemovalCustomResource createResource() {
ConcurrentFinalizerRemovalCustomResource res = new ConcurrentFinalizerRemovalCustomResource();
res.setMetadata(new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).build());
res.setSpec(new ConcurrentFinalizerRemovalSpec());
res.getSpec().setNumber(0);
return res;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.javaoperatorsdk.operator.baseapi.concurrentfinalizerremoval;

import io.javaoperatorsdk.operator.api.reconciler.Cleaner;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;

@ControllerConfiguration
public class ConcurrentFinalizerRemovalReconciler1
implements Reconciler<ConcurrentFinalizerRemovalCustomResource>,
Cleaner<ConcurrentFinalizerRemovalCustomResource> {

@Override
public UpdateControl<ConcurrentFinalizerRemovalCustomResource> reconcile(
ConcurrentFinalizerRemovalCustomResource resource,
Context<ConcurrentFinalizerRemovalCustomResource> context) {
return UpdateControl.noUpdate();
}

@Override
public DeleteControl cleanup(
ConcurrentFinalizerRemovalCustomResource resource,
Context<ConcurrentFinalizerRemovalCustomResource> context)
throws Exception {
return DeleteControl.defaultDelete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.javaoperatorsdk.operator.baseapi.concurrentfinalizerremoval;

import io.javaoperatorsdk.operator.api.reconciler.Cleaner;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;

@ControllerConfiguration
public class ConcurrentFinalizerRemovalReconciler2
implements Reconciler<ConcurrentFinalizerRemovalCustomResource>,
Cleaner<ConcurrentFinalizerRemovalCustomResource> {

@Override
public UpdateControl<ConcurrentFinalizerRemovalCustomResource> reconcile(
ConcurrentFinalizerRemovalCustomResource resource,
Context<ConcurrentFinalizerRemovalCustomResource> context) {
return UpdateControl.noUpdate();
}

@Override
public DeleteControl cleanup(
ConcurrentFinalizerRemovalCustomResource resource,
Context<ConcurrentFinalizerRemovalCustomResource> context)
throws Exception {
return DeleteControl.defaultDelete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.javaoperatorsdk.operator.baseapi.concurrentfinalizerremoval;

public class ConcurrentFinalizerRemovalSpec {

private int number;

public int getNumber() {
return number;
}

public ConcurrentFinalizerRemovalSpec setNumber(int number) {
this.number = number;
return this;
}
}