Skip to content

Commit

Permalink
feat(core): Delegate task/stage lookup to TaskResolver and `StageRe…
Browse files Browse the repository at this point in the history
…solver` respectively (#2868)

The `*Resolver` implementations will be able to look at both raw
implementation classes _as well as_ any specified aliases.

This supports (currently unsupported!) use cases that would be made
easier if a `Task` or `StageDefinitionBuilder` could be renamed.
  • Loading branch information
ajordens authored Apr 29, 2019
1 parent e8f06af commit 761b4e0
Show file tree
Hide file tree
Showing 28 changed files with 616 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2019 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.orca;

import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder;

import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static java.lang.String.format;

/**
* {@code StageResolver} allows for {@code StageDefinitionBuilder} retrieval via bean name or alias.
* <p>
* Aliases represent the previous bean names that a {@code StageDefinitionBuilder} registered as.
*/
public class StageResolver {
private final Map<String, StageDefinitionBuilder> stageDefinitionBuilderByAlias = new HashMap<>();

public StageResolver(Collection<StageDefinitionBuilder> stageDefinitionBuilders) {
for (StageDefinitionBuilder stageDefinitionBuilder : stageDefinitionBuilders) {
stageDefinitionBuilderByAlias.put(stageDefinitionBuilder.getType(), stageDefinitionBuilder);
for (String alias : stageDefinitionBuilder.aliases()) {
if (stageDefinitionBuilderByAlias.containsKey(alias)) {
throw new DuplicateStageAliasException(
format(
"Duplicate stage alias detected (alias: %s, previous: %s, current: %s)",
alias,
stageDefinitionBuilderByAlias.get(alias).getClass().getCanonicalName(),
stageDefinitionBuilder.getClass().getCanonicalName()
)
);
}

stageDefinitionBuilderByAlias.put(alias, stageDefinitionBuilder);
}
}
}

/**
* Fetch a {@code StageDefinitionBuilder} by {@param type} or {@param typeAlias}.
*
* @param type StageDefinitionBuilder type
* @param typeAlias StageDefinitionBuilder alias (optional)
* @return the StageDefinitionBuilder matching {@param type} or {@param typeAlias}
* @throws NoSuchStageDefinitionBuilderException if StageDefinitionBuilder does not exist
*/
@Nonnull
public StageDefinitionBuilder getStageDefinitionBuilder(@Nonnull String type, String typeAlias) {
StageDefinitionBuilder stageDefinitionBuilder = stageDefinitionBuilderByAlias.getOrDefault(
type, stageDefinitionBuilderByAlias.get(typeAlias)
);

if (stageDefinitionBuilder == null) {
throw new NoSuchStageDefinitionBuilderException(type, stageDefinitionBuilderByAlias.keySet());
}

return stageDefinitionBuilder;
}

class DuplicateStageAliasException extends IllegalStateException {
DuplicateStageAliasException(String message) {
super(message);
}
}

class NoSuchStageDefinitionBuilderException extends IllegalArgumentException {
NoSuchStageDefinitionBuilderException(String type, Collection<String> knownTypes) {
super(
format(
"No StageDefinitionBuilder implementation for %s found (knownTypes: %s)",
type,
String.join(",", knownTypes)
)
);
}
}
}
21 changes: 21 additions & 0 deletions orca-core/src/main/java/com/netflix/spinnaker/orca/Task.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,32 @@
import com.netflix.spinnaker.orca.pipeline.model.Stage;

import javax.annotation.Nonnull;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;

public interface Task {
@Nonnull TaskResult execute(@Nonnull Stage stage);

default void onTimeout(@Nonnull Stage stage) {}

default void onCancel(@Nonnull Stage stage) {}

default Collection<String> aliases() {
if (getClass().isAnnotationPresent(Aliases.class)) {
return Arrays.asList(getClass().getAnnotation(Aliases.class).value());
}

return Collections.emptyList();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Aliases {
String[] value() default {};
}
}
120 changes: 120 additions & 0 deletions orca-core/src/main/java/com/netflix/spinnaker/orca/TaskResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2019 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.orca;

import com.google.common.annotations.VisibleForTesting;

import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static java.lang.String.format;

/**
* {@code TaskResolver} allows for {@code Task} retrieval via class name or alias.
* <p>
* Aliases represent the previous class names of a {@code Task}.
*/
public class TaskResolver {
private final Map<String, Task> taskByAlias = new HashMap<>();

private final boolean allowFallback;

@VisibleForTesting
public TaskResolver(Collection<Task> tasks) {
this(tasks, true);
}

/**
* @param tasks Task implementations
* @param allowFallback Fallback to {@code Class.forName()} if a task cannot be located by name or alias
*/
public TaskResolver(Collection<Task> tasks, boolean allowFallback) {
for (Task task : tasks) {
taskByAlias.put(task.getClass().getCanonicalName(), task);
for (String alias : task.aliases()) {
if (taskByAlias.containsKey(alias)) {
throw new DuplicateTaskAliasException(
String.format(
"Duplicate task alias detected (alias: %s, previous: %s, current: %s)",
alias,
taskByAlias.get(alias).getClass().getCanonicalName(),
task.getClass().getCanonicalName()
)
);
}

taskByAlias.put(alias, task);
}
}

this.allowFallback = allowFallback;
}

/**
* Fetch a {@code Task} by {@param taskTypeIdentifier}.
*
* @param taskTypeIdentifier Task identifier (class name or alias)
* @return the Task matching {@param taskTypeIdentifier}
* @throws NoSuchTaskException if Task does not exist
*/
@Nonnull
public Task getTask(@Nonnull String taskTypeIdentifier) {
Task task = taskByAlias.get(taskTypeIdentifier);

if (task == null) {
throw new NoSuchTaskException(taskTypeIdentifier);
}

return task;
}

/**
* @param taskTypeIdentifier Task identifier (class name or alias)
* @return Task Class
* @throws NoSuchTaskException if task does not exist
*/
@Nonnull
public Class<? extends Task> getTaskClass(@Nonnull String taskTypeIdentifier) {
try {
return getTask(taskTypeIdentifier).getClass();
} catch (IllegalArgumentException e) {
if (!allowFallback) {
throw e;
}

try {
return (Class<? extends Task>) Class.forName(taskTypeIdentifier);
} catch (ClassNotFoundException ex) {
throw e;
}
}
}

class DuplicateTaskAliasException extends IllegalStateException {
DuplicateTaskAliasException(String message) {
super(message);
}
}

class NoSuchTaskException extends IllegalArgumentException {
NoSuchTaskException(String taskTypeIdentifier) {
super("No task found for '" + taskTypeIdentifier + "'");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.spectator.api.Registry;
import com.netflix.spinnaker.kork.core.RetrySupport;
import com.netflix.spinnaker.orca.StageResolver;
import com.netflix.spinnaker.orca.Task;
import com.netflix.spinnaker.orca.TaskResolver;
import com.netflix.spinnaker.orca.commands.ForceExecutionCancellationCommand;
import com.netflix.spinnaker.orca.events.ExecutionEvent;
import com.netflix.spinnaker.orca.events.ExecutionListenerAdapter;
Expand Down Expand Up @@ -145,8 +148,8 @@ public ApplicationListener<ExecutionEvent> onCompleteMetricExecutionListenerAdap

@Bean
@ConditionalOnMissingBean(StageDefinitionBuilderFactory.class)
public StageDefinitionBuilderFactory stageDefinitionBuilderFactory(Collection<StageDefinitionBuilder> stageDefinitionBuilders) {
return new DefaultStageDefinitionBuilderFactory(stageDefinitionBuilders);
public StageDefinitionBuilderFactory stageDefinitionBuilderFactory(StageResolver stageResolver) {
return new DefaultStageDefinitionBuilderFactory(stageResolver);
}

@Bean
Expand Down Expand Up @@ -181,6 +184,16 @@ public TaskScheduler taskScheduler() {
return scheduler;
}

@Bean
public TaskResolver taskResolver(Collection<Task> tasks) {
return new TaskResolver(tasks, true);
}

@Bean
public StageResolver stageResolver(Collection<StageDefinitionBuilder> stageDefinitionBuilders) {
return new StageResolver(stageDefinitionBuilders);
}

@Bean(name = EVENT_LISTENER_FACTORY_BEAN_NAME)
public EventListenerFactory eventListenerFactory() {
return new InspectableEventListenerFactory();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,20 @@

package com.netflix.spinnaker.orca.pipeline;

import static java.util.stream.Collectors.toList;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import javax.annotation.Nonnull;
import com.netflix.spinnaker.orca.pipeline.ExecutionRunner.NoSuchStageDefinitionBuilder;

import com.netflix.spinnaker.orca.StageResolver;
import com.netflix.spinnaker.orca.pipeline.model.Stage;

public class DefaultStageDefinitionBuilderFactory implements StageDefinitionBuilderFactory {
private final Collection<StageDefinitionBuilder> stageDefinitionBuilders;

public DefaultStageDefinitionBuilderFactory(Collection<StageDefinitionBuilder> stageDefinitionBuilders) {
this.stageDefinitionBuilders = stageDefinitionBuilders;
}
private final StageResolver stageResolver;

public DefaultStageDefinitionBuilderFactory(StageDefinitionBuilder... stageDefinitionBuilders) {
this(Arrays.asList(stageDefinitionBuilders));
public DefaultStageDefinitionBuilderFactory(StageResolver stageResolver) {
this.stageResolver = stageResolver;
}

@Override
public @Nonnull StageDefinitionBuilder builderFor(
@Nonnull Stage stage) throws NoSuchStageDefinitionBuilder {
return stageDefinitionBuilders
.stream()
.filter((it) -> it.getType().equals(stage.getType()) || it.getType().equals(stage.getContext().get("alias")))
.findFirst()
.orElseThrow(() -> {
List<String> knownTypes = stageDefinitionBuilders.stream().map(it -> it.getType()).sorted().collect(toList());
return new NoSuchStageDefinitionBuilder(stage.getType(), knownTypes);
});
public @Nonnull StageDefinitionBuilder builderFor(@Nonnull Stage stage) {
return stageResolver.getStageDefinitionBuilder(stage.getType(), (String) stage.getContext().get("alias"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,4 @@ default void cancel(
@Nonnull String user, @Nullable String reason) throws Exception {
throw new UnsupportedOperationException();
}

class NoSuchStageDefinitionBuilder extends RuntimeException {
public NoSuchStageDefinitionBuilder(String type, Collection<String> knownTypes) {
super(format("No StageDefinitionBuilder implementation for %s found. %s", type,
knownTypes == null || knownTypes.size() == 0 ? "There are no known stage types." : format(" Known stage types: %s", String.join(",", knownTypes))));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.netflix.spinnaker.orca.pipeline;

import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService;
import com.netflix.spinnaker.orca.Task;
import com.netflix.spinnaker.orca.pipeline.TaskNode.TaskGraph;
import com.netflix.spinnaker.orca.pipeline.graph.StageGraphBuilder;
import com.netflix.spinnaker.orca.pipeline.model.Execution;
Expand All @@ -25,6 +26,13 @@

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -186,4 +194,18 @@ default boolean isForceCacheRefreshEnabled(DynamicConfigService dynamicConfigSer
return true;
}
}

default Collection<String> aliases() {
if (getClass().isAnnotationPresent(Aliases.class)) {
return Arrays.asList(getClass().getAnnotation(Aliases.class).value());
}

return Collections.emptyList();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Aliases {
String[] value() default {};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,9 @@
package com.netflix.spinnaker.orca.pipeline;

import javax.annotation.Nonnull;
import com.netflix.spinnaker.orca.pipeline.ExecutionRunner.NoSuchStageDefinitionBuilder;
import com.netflix.spinnaker.orca.pipeline.model.Stage;

@FunctionalInterface
public interface StageDefinitionBuilderFactory {

@Nonnull StageDefinitionBuilder builderFor(
@Nonnull Stage stage) throws NoSuchStageDefinitionBuilder;

@Nonnull StageDefinitionBuilder builderFor(@Nonnull Stage stage);
}
Loading

0 comments on commit 761b4e0

Please sign in to comment.