Skip to content

feat: silent exception handling in managed workflows #2363

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 2 commits into from
Apr 29, 2024
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 @@ -178,6 +178,12 @@ public List<DependentResourceSpec> getDependentResourceSpecs() {
public boolean isExplicitInvocation() {
return workflowAnnotation.explicitInvocation();
}

@Override
public boolean handleExceptionsInReconciler() {
return workflowAnnotation.handleExceptionsInReconciler();
}

};
config.setWorkflowSpec(workflowSpec);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ public interface WorkflowSpec {
List<DependentResourceSpec> getDependentResourceSpecs();

boolean isExplicitInvocation();

boolean handleExceptionsInReconciler();
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package io.javaoperatorsdk.operator.api.reconciler;

import java.lang.annotation.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
Expand All @@ -13,10 +17,25 @@
Dependent[] dependents();

/**
* If true, managed workflow should be explicitly invoked within the reconciler implementation. If
* false workflow is invoked just before the {@link Reconciler#reconcile(HasMetadata, Context)}
* method.
* If {@code true}, the managed workflow should be explicitly invoked within the reconciler
* implementation. If {@code false}, the workflow is invoked just before the
* {@link Reconciler#reconcile(HasMetadata, Context)} method.
*/
boolean explicitInvocation() default false;

/**
* If {@code true} and exceptions are thrown during the workflow's execution, the reconciler won't
* throw an {@link io.javaoperatorsdk.operator.AggregatedOperatorException} at the end of the
* execution as would normally be the case. Instead, it will proceed to its
* {@link Reconciler#reconcile(HasMetadata, Context)} method as if no error occurred. It is then
* up to the developer to decide how to proceed by retrieving the errored dependents (and their
* associated exception) via
* {@link io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowResult#erroredDependents},
* the workflow result itself being accessed from
* {@link Context#managedWorkflowAndDependentResourceContext()}. If {@code false}, an exception
* will be automatically thrown at the end of the workflow execution, presenting an aggregated
* view of what happened.
*/
boolean handleExceptionsInReconciler() default false;

}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public class Controller<P extends HasMetadata>
private final boolean isCleaner;
private final Metrics metrics;
private final Workflow<P> managedWorkflow;
private final boolean explicitWorkflowInvocation;

private final GroupVersionKind associatedGVK;
private final EventProcessor<P> eventProcessor;
Expand All @@ -94,6 +95,9 @@ public Controller(Reconciler<P> reconciler,

final var managed = configurationService.getWorkflowFactory().workflowFor(configuration);
managedWorkflow = managed.resolve(kubernetesClient, configuration);
explicitWorkflowInvocation =
configuration.getWorkflowSpec().map(WorkflowSpec::isExplicitInvocation)
.orElse(false);

eventSourceManager = new EventSourceManager<>(this);
eventProcessor = new EventProcessor<>(eventSourceManager, configurationService);
Expand Down Expand Up @@ -145,7 +149,7 @@ public Map<String, Object> metadata() {
public UpdateControl<P> execute() throws Exception {
initContextIfNeeded(resource, context);
configuration.getWorkflowSpec().ifPresent(ws -> {
if (!isWorkflowExplicitInvocation()) {
if (!explicitWorkflowInvocation) {
reconcileManagedWorkflow(resource, context);
}
});
Expand Down Expand Up @@ -191,7 +195,7 @@ public DeleteControl execute() {

// The cleanup is called also when explicit invocation is true, but the cleaner is not
// implemented
if (!isCleaner || !isWorkflowExplicitInvocation()) {
if (!isCleaner || !explicitWorkflowInvocation) {
workflowCleanupResult = cleanupManagedWorkflow(resource, context);
}

Expand Down Expand Up @@ -449,7 +453,6 @@ public void reconcileManagedWorkflow(P primary, Context<P> context) {
((DefaultManagedWorkflowAndDependentResourceContext) context
.managedWorkflowAndDependentResourceContext())
.setWorkflowExecutionResult(res);
res.throwAggregateExceptionIfErrorsPresent();
}
}

Expand All @@ -459,15 +462,14 @@ public WorkflowCleanupResult cleanupManagedWorkflow(P resource, Context<P> conte
((DefaultManagedWorkflowAndDependentResourceContext) context
.managedWorkflowAndDependentResourceContext())
.setWorkflowCleanupResult(workflowCleanupResult);
workflowCleanupResult.throwAggregateExceptionIfErrorsPresent();

return workflowCleanupResult;
} else {
return null;
}
}

public boolean isWorkflowExplicitInvocation() {
return configuration.getWorkflowSpec().map(WorkflowSpec::isExplicitInvocation)
.orElse(false);
return explicitWorkflowInvocation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware;

import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_VALUE_SET;
import static io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow.THROW_EXCEPTION_AUTOMATICALLY_DEFAULT;

@SuppressWarnings("rawtypes")
public class DefaultManagedWorkflow<P extends HasMetadata> implements ManagedWorkflow<P> {
Expand Down Expand Up @@ -96,7 +95,8 @@ public Workflow<P> resolve(KubernetesClient client,
final var top =
topLevelResources.stream().map(alreadyResolved::get).collect(Collectors.toSet());
return new DefaultWorkflow<>(alreadyResolved, bottom, top,
THROW_EXCEPTION_AUTOMATICALLY_DEFAULT, hasCleaner);
configuration.getWorkflowSpec().map(w -> !w.handleExceptionsInReconciler()).orElseThrow(),
hasCleaner);
}

@SuppressWarnings({"rawtypes", "unchecked"})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.javaoperatorsdk.operator.processing.dependent.workflow;

import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
Expand All @@ -10,11 +11,10 @@
@SuppressWarnings("rawtypes")
class WorkflowResult {

private static final String NUMBER_DELIMITER = "_";
private final Map<DependentResource, Exception> erroredDependents;

WorkflowResult(Map<DependentResource, Exception> erroredDependents) {
this.erroredDependents = erroredDependents;
this.erroredDependents = erroredDependents != null ? erroredDependents : Collections.emptyMap();
}

public Map<DependentResource, Exception> getErroredDependents() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public List<DependentResourceSpec> getDependentResourceSpecs() {
public boolean isExplicitInvocation() {
return false;
}

@Override
public boolean handleExceptionsInReconciler() {
return false;
}
};
when(configuration.getWorkflowSpec()).thenReturn(Optional.of(ws));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.javaoperatorsdk.operator;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
import io.javaoperatorsdk.operator.sample.workflowsilentexceptionhandling.HandleWorkflowExceptionsInReconcilerCustomResource;
import io.javaoperatorsdk.operator.sample.workflowsilentexceptionhandling.HandleWorkflowExceptionsInReconcilerReconciler;

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

public class WorkflowSilentExceptionHandlingIT {

@RegisterExtension
LocallyRunOperatorExtension extension =
LocallyRunOperatorExtension.builder()
.withReconciler(HandleWorkflowExceptionsInReconcilerReconciler.class)
.build();

@Test
void handleExceptionsInReconciler() {
extension.create(testResource());
var reconciler =
extension.getReconcilerOfType(HandleWorkflowExceptionsInReconcilerReconciler.class);

await().untilAsserted(() -> {
assertThat(reconciler.isErrorsFoundInReconcilerResult()).isTrue();
});

extension.delete(testResource());

await().untilAsserted(() -> {
assertThat(reconciler.isErrorsFoundInCleanupResult()).isTrue();
});
}

HandleWorkflowExceptionsInReconcilerCustomResource testResource() {
var res = new HandleWorkflowExceptionsInReconcilerCustomResource();
res.setMetadata(new ObjectMetaBuilder()
.withName("test1")
.build());
return res;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.javaoperatorsdk.operator.sample.workflowsilentexceptionhandling;


import io.fabric8.kubernetes.api.model.ConfigMap;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDNoGCKubernetesDependentResource;

public class ConfigMapDependent extends
CRUDNoGCKubernetesDependentResource<ConfigMap, HandleWorkflowExceptionsInReconcilerCustomResource> {

public ConfigMapDependent() {
super(ConfigMap.class);
}

@Override
public ReconcileResult<ConfigMap> reconcile(
HandleWorkflowExceptionsInReconcilerCustomResource primary,
Context<HandleWorkflowExceptionsInReconcilerCustomResource> context) {
throw new RuntimeException("Exception thrown on purpose");
}

@Override
public void delete(HandleWorkflowExceptionsInReconcilerCustomResource primary,
Context<HandleWorkflowExceptionsInReconcilerCustomResource> context) {
throw new RuntimeException("Exception thrown on purpose");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.javaoperatorsdk.operator.sample.workflowsilentexceptionhandling;

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("hweir")
public class HandleWorkflowExceptionsInReconcilerCustomResource
extends CustomResource<Void, Void>
implements Namespaced {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.javaoperatorsdk.operator.sample.workflowsilentexceptionhandling;

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;
import io.javaoperatorsdk.operator.api.reconciler.Workflow;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;

@Workflow(handleExceptionsInReconciler = true,
dependents = @Dependent(type = ConfigMapDependent.class))
@ControllerConfiguration
public class HandleWorkflowExceptionsInReconcilerReconciler
implements Reconciler<HandleWorkflowExceptionsInReconcilerCustomResource>,
Cleaner<HandleWorkflowExceptionsInReconcilerCustomResource> {

private volatile boolean errorsFoundInReconcilerResult = false;
private volatile boolean errorsFoundInCleanupResult = false;

@Override
public UpdateControl<HandleWorkflowExceptionsInReconcilerCustomResource> reconcile(
HandleWorkflowExceptionsInReconcilerCustomResource resource,
Context<HandleWorkflowExceptionsInReconcilerCustomResource> context) {

errorsFoundInReconcilerResult = context.managedWorkflowAndDependentResourceContext()
.getWorkflowReconcileResult().erroredDependentsExist();


return UpdateControl.noUpdate();
}

@Override
public DeleteControl cleanup(HandleWorkflowExceptionsInReconcilerCustomResource resource,
Context<HandleWorkflowExceptionsInReconcilerCustomResource> context) {

errorsFoundInCleanupResult = context.managedWorkflowAndDependentResourceContext()
.getWorkflowCleanupResult().erroredDependentsExist();
return DeleteControl.defaultDelete();
}

public boolean isErrorsFoundInReconcilerResult() {
return errorsFoundInReconcilerResult;
}

public boolean isErrorsFoundInCleanupResult() {
return errorsFoundInCleanupResult;
}
}
Loading