Skip to main content

Stop bot execution if Business Process is stopped

Bots are executed on a separate Java Virtual Machine (JVM). As a result, a stop or pause action for a Business Process (BP) cleans up all queues for this BP and does not allow the Worker to start processing a new task from the main Worker queue. However, it doesn't kill the Worker already processing the task from the BP, and execution of the Bot Task continues till the very end, even if you stop or pause the BP.

This behavior can be a problem in the following cases:

  • It is a long-running Bot Task. A Worker cannot take on a new task until it finishes processing the task for the already stopped or paused BP.
  • A bot executes a critical action when the action is interrupted due to the Control Tower functionality paused or stopped.

The solution is to check the BP status directly from the bot and throw an exception to interrupt the bot execution if the BP status is paused or stopped. Add this logic to the following bot steps:

  • Before a critical transaction in the bot step, if you pause or stop the BP in production to interrupt the transaction.
  • For long-running Bot Tasks (five minutes and more), if it is critical to free the Worker fast for timely processing of other tasks. Define X, which is the average time you can allow the long-running Bot Task to run after the stop or pause BP action. Ideally, X must be in minutes, for example, 5 or 15 minutes.
  • The right location for the logic is as follows:
    • At the beginning of a loop cycle, if the total execution time is more than X minutes.
    • Before a long wait logic in a bot step.
    • In other places, outside loops, to add an interval between a status check in average X minutes during execution.

To add the logic, follow the steps below:

  1. Put class in your Bot Config Bundle project.

  2. Create an instance of the class in your Bot Task.

    • Recommended: use a binding and a Secrets Vault alias. The username and password are taken from provided Secrets Vault.
    • Use a binding, a username, and a password.
    • Use a binding only. The default Secrets Vault alias specified in the DEFAULT_SECRET_VAULT_ALIAS field is used.
  3. Insert if statements with the status that checks critical BP spots:

    if (bpStatus.isStoppedOrPaused()) {
    //Do something before throwing an exception (if needed)
    //to complete bot execution gracefully.
    //E.g. close some applications or connections,
    //remove temporary files and so on
    throw new RuntimeException("BP was stopped");
    }
info

You must have at least the View Business Processes permission to make an API call. For more details, refer to WorkFusion REST API | API security.

The available methods to check the BP status are as follows:

  • getStatus() returns a string representation of the BP status.
  • isRunning() returns true if the process is running.
  • isPaused() returns true if the process is paused.
  • isStoppedOrFinished() returns true if the process is stopped or finished.
  • isStoppedOrPaused() returns true if the process is stopped, finished, or paused.
See code for BP status check
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.freedomoss.crowdcontrol.webharvest.WebHarvestConstants;
import com.freedomoss.crowdcontrol.webharvest.WebHarvestTaskItem;
import com.freedomoss.crowdcontrol.webharvest.web.dto.SecureEntryDTO;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import groovy.lang.Binding;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.webharvest.runtime.Scraper;
import org.webharvest.utils.SystemUtilities;

import com.workfusion.rpa.core.BindingUtils;
import com.workfusion.rpa.core.plugin.PluginAdapterFactory;
import com.workfusion.rpa.core.plugin.security.SecretsVaultPluginAdapter;

@SuppressWarnings("deprecation")
public class BPStatus {

public static final String STATUS_BP_URL = "/api/v2/workfusion/task/";
public static final String LOGIN_URL = "/api/dologin";

//Secrets Vault with this alias
//and credentials with the "View Business Processes" permission
//must be created in CT
private static final String DEFAULT_SECRET_VAULT_ALIAS = "wf_api_user";

private static final String COMPLETED = "COMPLETED";
private static final String PAUSE = "PAUSE";
private static final String PROCESSING = "PROCESSING";
private static final String MACHINE_PROCESSING = "MACHINE_PROCESSING";

private final Binding binding;
private final String username;
private final String password;

public BPStatus(Binding binding) {
this(binding, DEFAULT_SECRET_VAULT_ALIAS);
}

public BPStatus(Binding binding, String secretVaultAlias) {
PluginAdapterFactory pluginAdapterFactory = getPluginAdaptersFactory(binding);
SecureEntryDTO credentials = getUserCredentials(secretVaultAlias, pluginAdapterFactory);
this.binding = binding;
this.username = credentials.getKey();
this.password = credentials.getValue();
}

public BPStatus(Binding binding, String username, String password) {
this.binding = binding;
this.username = username;
this.password = password;
}

/**
* @return true if BP is Running
*/
public boolean isRunning() throws IOException {
String status = getStatus();
return MACHINE_PROCESSING.equalsIgnoreCase(status)
|| PROCESSING.equalsIgnoreCase(status);
}

/**
* @return true if BP is Paused
*/
public boolean isPaused() throws IOException {
return PAUSE.equalsIgnoreCase(getStatus());
}

/**
* @return true if BP is Stopped or Finished
*/
public boolean isStoppedOrFinished() throws IOException {
return COMPLETED.equalsIgnoreCase(getStatus());
}

/**
* @return true if BP is Stopped, Finished, or Paused
*/
public boolean isStoppedOrPaused() throws IOException {
String status = getStatus();
return COMPLETED.equalsIgnoreCase(status)
|| PAUSE.equalsIgnoreCase(status);
}

/**
* @return status of current BP
*/
public String getStatus() throws IOException {
HttpClient client = HttpClients.createDefault();
String baseUrl = getRestPath(binding);
List<Header> headers = login(client, baseUrl, username, password);

String uuid = getBotUuid();

return getBusinessProcessStatus(uuid, client, headers, baseUrl);
}

private String getBotUuid() {
WebHarvestTaskItem taskItem = BindingUtils.getWebHarvestTaskItem(binding);
return taskItem != null && taskItem.getRun() != null ? taskItem.getRun().getUuid() : null;
}

private String getRestPath(Binding binding) {
String applicationRestPath = BindingUtils.getRestBasePath(binding);
if (applicationRestPath.startsWith("http://")
|| applicationRestPath.startsWith("https://")) {
return applicationRestPath;
}
//External path is not defined. Bot is running on local CT
return BindingUtils.getPropertyValue(binding, "internalApplicationHost")
+ BindingUtils.getPropertyValue(binding, "internalApplicationContextPath");
}

private SecureEntryDTO getUserCredentials(String alias, PluginAdapterFactory pluginAdapterFactory) {
SecretsVaultPluginAdapter secretsVaultPluginAdapter = pluginAdapterFactory.getPluginAdapter(PluginAdapterFactory.PluginsEnum.SECRETS_VAULT);
return secretsVaultPluginAdapter.getSecretsVault(alias).orElse(null);
}

private List<Header> login(HttpClient httpClient, String baseUrl,
String username, String password) throws IOException {
List<NameValuePair> nvp = new ArrayList<>();
nvp.add(new BasicNameValuePair("j_username", username));
nvp.add(new BasicNameValuePair("j_password", password));

List<Header> headers = new ArrayList<>();

HttpPost httpPost = new HttpPost(baseUrl + LOGIN_URL);
httpPost.setEntity(new UrlEncodedFormEntity(nvp));

HttpResponse response = httpClient.execute(httpPost);
String cookieHeader = response.getHeaders("Set-Cookie")[0].getValue();
headers.add(new BasicHeader("Cookie", cookieHeader));
String stringResponse = convertStreamToString(response.getEntity().getContent());
Map<String, String> mapResponse = getMapFromResponse(stringResponse);
String csrfToken = mapResponse.get("csrfToken");
String csrfHeader = mapResponse.get("csrfHeaderName");
headers.add(new BasicHeader(csrfHeader, csrfToken));
return headers;
}

private Map<String, String> getMapFromResponse(String response) {
Type mapType = new TypeToken<Map<String, String>>() {
}.getType();
return new Gson().fromJson(response, mapType);
}

private String getBusinessProcessStatus(String uuid, HttpClient httpClient,
List<Header> headers, String baseUrl) throws IOException {
HttpGet httpGet = new HttpGet(baseUrl + STATUS_BP_URL + uuid);
headers.forEach(httpGet::addHeader);

HttpResponse response = httpClient.execute(httpGet);
String stringResponse = convertStreamToString(response.getEntity().getContent());
Map<String, String> mapResponse = getMapFromResponse(stringResponse);
return mapResponse.get("status");
}

private String convertStreamToString(java.io.InputStream is) {
java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}

private PluginAdapterFactory getPluginAdaptersFactory(Binding binding) {
try {
final SystemUtilities sys = (SystemUtilities) binding.getVariable(WebHarvestConstants.SYS);
final Class<? extends SystemUtilities> systemUtilitiesClass = sys.getClass();
final Field scraperField = systemUtilitiesClass.getDeclaredField("scraper");
scraperField.setAccessible(true);
final Scraper scraperFromSys = (Scraper) scraperField.get(sys);
scraperField.setAccessible(false);
return new PluginAdapterFactory(scraperFromSys);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalStateException("Could not get Scraper for PluginAdapterFactory", e);
}
}
}

Alternatively, you can kill RPA or BEP Workers manually. Mind the following limitations of the approach:

  • You should have access to the RPA or BEP cluster.
  • Defining which BEP Worker or RPA node to stop can be tricky if the bot is executed in the RPA cluster but not in an individual fleet.