Skip to content
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

feat: async task execution for cleanup - fix for #5408 #5412

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
@@ -0,0 +1,124 @@
/*
* Copyright 2016-2023 the original author or authors.
*
* 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
*
* https://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 org.springframework.cloud.dataflow.server.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.dataflow.core.DataFlowPropertyKeys;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.concurrent.ExecutorConfigurationSupport;

/**
* Used to configure the {@link ThreadPoolTaskExecutor} in {@link DataflowAsyncConfiguration}. For more information
* of the fields see {@link ThreadPoolTaskExecutor} and {@link ExecutorConfigurationSupport}
*
* @author Tobias Soloschenko
*/
@ConfigurationProperties(prefix = AsyncConfigurationProperties.ASYNC_PREFIX)
public class AsyncConfigurationProperties {

public static final String ASYNC_PREFIX = DataFlowPropertyKeys.PREFIX + "async";

private int corePoolSize = 1;

private int maxPoolSize = Integer.MAX_VALUE;

private int keepAliveSeconds = 60;

private int queueCapacity = Integer.MAX_VALUE;

private boolean allowCoreThreadTimeOut = false;

private boolean prestartAllCoreThreads = false;

private boolean waitForTasksToCompleteOnShutdown = false;

private long awaitTerminationMillis = 0L;

private String threadNamePrefix = "scdf-async-";

public int getQueueCapacity() {
return queueCapacity;
}

public void setCorePoolSize(int corePoolSize) {
this.corePoolSize = corePoolSize;
}

public int getMaxPoolSize() {
return maxPoolSize;
}

public void setMaxPoolSize(int maxPoolSize) {
this.maxPoolSize = maxPoolSize;
}

public int getKeepAliveSeconds() {
return keepAliveSeconds;
}

public void setKeepAliveSeconds(int keepAliveSeconds) {
this.keepAliveSeconds = keepAliveSeconds;
}

public void setQueueCapacity(int queueCapacity) {
this.queueCapacity = queueCapacity;
}

public int getCorePoolSize() {
return corePoolSize;
}

public boolean isAllowCoreThreadTimeOut() {
return allowCoreThreadTimeOut;
}

public void setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) {
this.allowCoreThreadTimeOut = allowCoreThreadTimeOut;
}

public boolean isPrestartAllCoreThreads() {
return prestartAllCoreThreads;
}

public void setPrestartAllCoreThreads(boolean prestartAllCoreThreads) {
this.prestartAllCoreThreads = prestartAllCoreThreads;
}

public boolean isWaitForTasksToCompleteOnShutdown() {
return waitForTasksToCompleteOnShutdown;
}

public void setWaitForTasksToCompleteOnShutdown(boolean waitForTasksToCompleteOnShutdown) {
this.waitForTasksToCompleteOnShutdown = waitForTasksToCompleteOnShutdown;
}

public long getAwaitTerminationMillis() {
return awaitTerminationMillis;
}

public void setAwaitTerminationMillis(long awaitTerminationMillis) {
this.awaitTerminationMillis = awaitTerminationMillis;
}

public String getThreadNamePrefix() {
return threadNamePrefix;
}

public void setThreadNamePrefix(String threadNamePrefix) {
this.threadNamePrefix = threadNamePrefix;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2016-2023 the original author or authors.
*
* 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
*
* https://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 org.springframework.cloud.dataflow.server.config;

import java.util.concurrent.Executor;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
* Class to override the executor at the application level. It also enables async executions for the Spring Cloud Data Flow Server.
*
* @author Tobias Soloschenko
*/
@Configuration(proxyBeanMethods = false)
@EnableAsync
@EnableConfigurationProperties(AsyncConfigurationProperties.class)
public class DataflowAsyncConfiguration implements AsyncConfigurer {

private final AsyncConfigurationProperties asyncConfigurationProperties;

public DataflowAsyncConfiguration(AsyncConfigurationProperties asyncConfigurationProperties) {
this.asyncConfigurationProperties = asyncConfigurationProperties;
}

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setQueueCapacity(asyncConfigurationProperties.getQueueCapacity());
threadPoolTaskExecutor.setCorePoolSize(asyncConfigurationProperties.getCorePoolSize());
threadPoolTaskExecutor.setMaxPoolSize(asyncConfigurationProperties.getMaxPoolSize());
threadPoolTaskExecutor.setKeepAliveSeconds(asyncConfigurationProperties.getKeepAliveSeconds());
threadPoolTaskExecutor.setAllowCoreThreadTimeOut(asyncConfigurationProperties.isAllowCoreThreadTimeOut());
threadPoolTaskExecutor.setPrestartAllCoreThreads(asyncConfigurationProperties.isPrestartAllCoreThreads());
threadPoolTaskExecutor.setAwaitTerminationMillis(asyncConfigurationProperties.getAwaitTerminationMillis());
threadPoolTaskExecutor.setThreadNamePrefix(asyncConfigurationProperties.getThreadNamePrefix());
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(asyncConfigurationProperties.isWaitForTasksToCompleteOnShutdown());
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import org.springframework.hateoas.server.ExposesResourceFor;
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport;
import org.springframework.http.HttpStatus;
import org.springframework.scheduling.annotation.Async;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down Expand Up @@ -265,6 +266,7 @@ public void cleanup(@PathVariable("id") Set<Long> ids,
*/
@RequestMapping(method = RequestMethod.DELETE)
@ResponseStatus(HttpStatus.OK)
@Async
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be an opt-in feature and here are a couple of options I can think of:

Option 1

Add another API (eg. cleanupAllAsync).

  • ❌ UI will not pick this up and you must use this via REST API
  • ➕ low risk of breaking other users
  • ➕ UI does not give the false appearance that all has been cleaned up instantly
  • ➕ can add CompletableFuture return value for better usage later from UI/callers etc..

Option 2

Guard the auto-config w/ enabled property - hence w/o @EnableAsync the @Async is invisible.

  • ❌ UI will give false appearance that all has been cleaned up instantly when async is enabled
  • ❌ moderate risk of breaking/confusing other users
  • ➕ for those users that opt-in, the UI does this async

I am in favor of the latter option. If you agree, please prototype/verify the absence of @EnableAsync does what I think it does.

Copy link
Contributor Author

@klopfdreh klopfdreh Sep 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t know why it should be an opt in feature as the UI says: „X task executions will be deleted“ - it doesn’t matter if the user can interact directly after pressing ok (in case of async) or has to wait till the backend processed the operation. (in case of sync)

In case this async option is used somewhere else you are right the api has to be designed to represent what is going on like „…will be…“

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hear you. That is a good point. My concern is

  1. w/o some sort of messaging in the UI that mentions that it will happen in the background I fear some users may start looking around and still see the executions and then try again, then run into an issue, then file bug report, etc..

  2. whether opt-in or opt-out, we need some sort of "light switch" for the feature in case we run into unforeseen issues in production.

Because it is late in the release cycle I am just being extra cautious. I do want to get this feature in, but I also am treading carefully.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

As you prefer we can delay this feature as with „clean task executions after n days“ you can run multiple cleanups and shrink down the count of present executions in multiple steps, so there is no need to hurry up with this anymore.😃

I am going to refactor and add the simplifications and we can think about a good way to implement this in a following release.

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be great and I would appreciate that. I know you put your hard work and effort into this so I wanted to match you w/ whatever effort I could to get this in. Moving it to post 2.11 is the smart decision. Thank you.

Copy link
Contributor Author

@klopfdreh klopfdreh Sep 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one little question regarding Option 2. The current refactoring includes the ConditionalOnProperty to check if the functionality is enabled so you have to opt in to use this feature.

From my understanding @EnableAsync has to be used with @Async as the annotation on the method picks up the thread pool executor provided with @EnableAsync - but I am not 100% sure.

Here are some information regarding async implementation: https://spring.io/guides/gs/async-method/ and https://stackoverflow.com/a/53357076

If this is the case I would suggest to still let @EnableAsync be on the Configuration, because it is not used when the opt flag in is not set.

Edit: I switched back the name of the bean and the prefix as this might be used for other async methods as well - not only for cleanup.

public void cleanupAll(
@RequestParam(defaultValue = "CLEANUP", name="action") TaskExecutionControllerDeleteAction[] actions,
@RequestParam(defaultValue = "false", name="completed") boolean completed,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.dataflow.server.config.DataFlowServerAutoConfiguration,\
org.springframework.cloud.dataflow.server.config.DataFlowControllerAutoConfiguration, \
org.springframework.cloud.dataflow.server.config.SpringDocAutoConfiguration
org.springframework.cloud.dataflow.server.config.SpringDocAutoConfiguration, \
org.springframework.cloud.dataflow.server.config.DataflowAsyncConfiguration
klopfdreh marked this conversation as resolved.
Show resolved Hide resolved

org.springframework.context.ApplicationContextInitializer=\
org.springframework.cloud.dataflow.common.flyway.FlywayVendorReplacingApplicationContextInitializer
Loading