Skip to content

Commit

Permalink
fix(case): use 429 status when case creation limit reached (#3182)
Browse files Browse the repository at this point in the history
  • Loading branch information
rbioteau authored Oct 2, 2024
1 parent ed949c8 commit 6bcb65f
Show file tree
Hide file tree
Showing 21 changed files with 162 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
**/
package org.bonitasoft.engine.bpm.process;

import lombok.Getter;
import org.bonitasoft.engine.exception.ExecutionException;

/**
Expand All @@ -26,6 +27,8 @@
public class ProcessExecutionException extends ExecutionException {

private static final long serialVersionUID = 4412292065541283593L;
@Getter
private long retryAfter = -1L;

/**
* Constructs a new exception with the specified detail cause.
Expand All @@ -39,6 +42,11 @@ public ProcessExecutionException(Throwable cause) {
super(cause);
}

public ProcessExecutionException(Throwable cause, long retryAfter) {
super(cause);
this.retryAfter = retryAfter;
}

/**
* Constructs a new exception with the specified detail message.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ public List<SDataInstance> getDataInstances(final long containerId, final String
} catch (SProcessDefinitionNotFoundException | SFlowNodeNotFoundException | SFlowNodeReadException
| SBonitaReadException e) {
throw new SDataInstanceException(
String.format("An error occured while retrieving transient data for container %s with type %s",
String.format("An error occurred while retrieving transient data for container %s with type %s",
containerId, containerType),
e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ void update(final long processDefinitionId, final String parameterName, final St
* @param parameters
* parameters to merge
* @throws SBonitaReadException
* error thrown if an error occured while retrieving the process definition
* error thrown if an error occurred while retrieving the process definition
* @throws SObjectModificationException
* error thrown if an error occured while updating the parameter value
* error thrown if an error occurred while updating the parameter value
*/
void merge(long processDefinitionId, Map<String, String> parameters)
throws SBonitaReadException, SObjectModificationException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@
import org.bonitasoft.engine.core.process.instance.model.SProcessInstance;
import org.bonitasoft.engine.exception.BonitaRuntimeException;
import org.bonitasoft.engine.exception.RetrieveException;
import org.bonitasoft.engine.execution.*;
import org.bonitasoft.engine.execution.Filter;
import org.bonitasoft.engine.execution.FlowNodeNameFilter;
import org.bonitasoft.engine.execution.FlowNodeSelector;
import org.bonitasoft.engine.execution.ProcessExecutor;
import org.bonitasoft.engine.execution.StartFlowNodeFilter;
import org.bonitasoft.engine.identity.IdentityService;
import org.bonitasoft.engine.identity.model.SUser;
import org.bonitasoft.engine.operation.Operation;
Expand Down Expand Up @@ -110,6 +114,12 @@ public ProcessInstance start()
throw new RetrieveException(e);
} catch (final SProcessDefinitionException e) {
throw new ProcessActivationException(e);
} catch (final SProcessInstanceCreationException e) {
if (e.getRetryAfter() != -1L) {
throw new ProcessExecutionException(e, e.getRetryAfter());
} else {
throw new ProcessExecutionException(e);
}
} catch (final SBonitaException e) {
throw new ProcessExecutionException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,14 @@ public void verify(SProcessInstance processInstance) throws SProcessInstanceCrea
final long processStartDate = processInstance.getStartDate();
cleanupOldValues(processStartDate - PERIOD_IN_MILLIS);
log.debug("Found {} cases already started in the last {} days", counters.size(), PERIOD_IN_DAYS);

if (counters.size() >= LIMIT) {
final String nextValidTime = getStringRepresentation(getNextResetTimestamp(counters));
var nextResetTimestamp = getNextResetTimestamp(counters);
final String nextValidTime = getStringRepresentation(nextResetTimestamp);
throw new SProcessInstanceCreationException(
format("Process start limit (%s cases during last %s days) reached. You are not allowed to start a new process until %s.",
LIMIT, PERIOD_IN_DAYS, nextValidTime));
LIMIT, PERIOD_IN_DAYS, nextValidTime),
nextResetTimestamp);
}
try {
synchronized (counters) {
Expand All @@ -112,7 +115,7 @@ public void verify(SProcessInstance processInstance) throws SProcessInstanceCrea
} catch (IOException | SPlatformNotFoundException | SPlatformUpdateException e) {
log.trace(e.getMessage(), e);
throw new SProcessInstanceCreationException(
format("Unable to start the process instance %s", processInstance.getId()));
format("Unable to start the process instance %s", processInstance.getId()), e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
**/
package org.bonitasoft.engine.core.process.instance.api.exceptions;

import lombok.Getter;
import org.bonitasoft.engine.commons.exceptions.SBonitaException;
import org.bonitasoft.engine.core.process.definition.model.SProcessDefinition;

Expand All @@ -23,21 +24,26 @@ public class SProcessInstanceCreationException extends SBonitaException {

private static final long serialVersionUID = 7581906795549409593L;

@Getter
private long retryAfter = -1L;

public SProcessInstanceCreationException(final Throwable cause) {
super(cause);
}

public SProcessInstanceCreationException(final String message, final SBonitaException e) {
super(message, e);
public SProcessInstanceCreationException(final String message, final Throwable cause) {
super(message, cause);
}

/**
* @param string
*/
public SProcessInstanceCreationException(final String message) {
super(message);
}

public SProcessInstanceCreationException(final String message, final long retryAfter) {
super(message);
this.retryAfter = retryAfter;
}

/**
* @param sDefinition
* The process definition to add on context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ protected void doGet(final HttpServletRequest request, final HttpServletResponse
LOGGER.error(e.getMessage(), e);
}
try {
out.write("An exception occured. Please contact an administrator".getBytes());
out.write("An exception occurred. Please contact an administrator".getBytes());
} catch (final IOException e1) {
throw new ServletException(e1);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ protected void doGet(final HttpServletRequest request, final HttpServletResponse
if (LOGGER.isErrorEnabled()) {
LOGGER.error("Error while trying to get the error page.", e);
}
output.println("An Error occured.");
output.println("An Error occurred.");
}
}
writeFormatedResponse(output, errorCode, contextPath);
Expand Down Expand Up @@ -124,7 +124,7 @@ protected void writeFormatedResponse(PrintWriter output, String errorCode, Strin
if (LOGGER.isErrorEnabled()) {
LOGGER.error("Error while trying to display the error page.", e);
}
output.println("An Error occured.");
output.println("An Error occurred.");
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import java.io.FileNotFoundException;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;

import com.fasterxml.jackson.databind.node.JsonNodeFactory;
Expand All @@ -29,6 +30,8 @@
import org.bonitasoft.engine.bpm.process.ProcessExecutionException;
import org.bonitasoft.web.rest.server.api.resource.CommonResource;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIException;
import org.restlet.Response;
import org.restlet.data.Status;
import org.restlet.resource.Post;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -83,7 +86,17 @@ public String instantiateProcess(final Map<String, Serializable> inputs)
} catch (ProcessExecutionException e) {
String errorMessage = "Unable to start the process with ID " + processDefinitionId;
if (LOGGER.isErrorEnabled()) {
LOGGER.error(errorMessage + " Error: " + e.getMessage());
LOGGER.error("{}. Caused by: {}", errorMessage, e.getMessage());
}
if (e.getRetryAfter() != -1L) {
// Return a 429 status code with Retry-After header to indicate the client
// that he should retry later in case of case creation limit reached
Response response = getResponse();
response.setRetryAfter(new Date(e.getRetryAfter()));
var status = new Status(Status.CLIENT_ERROR_TOO_MANY_REQUESTS, "Case creation limit reached.",
errorMessage);
response.setStatus(status);
return null;
}
//Avoid throwing original exception that may contain sensitive information unwanted in the HTTP response
throw new ProcessExecutionException(errorMessage + " (consult the logs for more information).");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.bonitasoft.engine.search.SearchOptionsBuilder;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APINotFoundException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APITooManyRequestException;
import org.bonitasoft.web.toolkit.client.common.i18n.T_;
import org.bonitasoft.web.toolkit.client.common.texttemplate.Arg;

Expand Down Expand Up @@ -70,8 +71,14 @@ public ProcessInstance start(final long userId, final long processId, final Map<
new T_("Can't start process, process %processId% is not enabled", new Arg("processId", processId)),
e);
} catch (final ProcessExecutionException e) {
if (e.getRetryAfter() != -1L) {
throw new APITooManyRequestException(
new T_("Error occurred when starting process %processId%. Case creation limit reached.",
new Arg("processId", processId)),
e.getRetryAfter());
}
throw new APIException(
new T_("Error occured when starting process %processId%", new Arg("processId", processId)), e);
new T_("Error occurred when starting process %processId%", new Arg("processId", processId)), e);
} catch (final UserNotFoundException e) {
throw new APIException(
new T_("Can't start process %processId%, user %userId% not found", new Arg("processId", processId),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright (C) 2024 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* This library is free software; you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Foundation
* version 2.1 of the License.
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
* Floor, Boston, MA 02110-1301, USA.
**/
package org.bonitasoft.web.toolkit.client.common.exception.api;

import lombok.Getter;
import org.bonitasoft.web.toolkit.client.common.i18n.T_;

public class APITooManyRequestException extends APIException {

private static final long serialVersionUID = 1820639344042666872L;
@Getter
private long retryAfter = -1L;

public APITooManyRequestException(final T_ message, long retryAfter) {
super(message);
this.retryAfter = retryAfter;
setStatusCode(429);
}

@Override
protected String defaultMessage() {
return getApi() + "#" + getResource() + ": Case creation limit reached.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@

import java.io.IOException;
import java.io.PrintWriter;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
Expand All @@ -26,7 +31,14 @@
import org.bonitasoft.console.common.server.utils.LocaleUtils;
import org.bonitasoft.web.rest.server.framework.json.JSonSimpleDeserializer;
import org.bonitasoft.web.toolkit.client.common.CommonDateFormater;
import org.bonitasoft.web.toolkit.client.common.exception.api.*;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIForbiddenException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIIncorrectIdException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIItemIdMalformedException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIItemNotFoundException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APIMethodNotAllowedException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APINotFoundException;
import org.bonitasoft.web.toolkit.client.common.exception.api.APITooManyRequestException;
import org.bonitasoft.web.toolkit.client.common.i18n.AbstractI18n.LOCALE;
import org.bonitasoft.web.toolkit.client.common.json.JSonItemReader;
import org.bonitasoft.web.toolkit.client.common.json.JSonSerializer;
Expand All @@ -45,6 +57,10 @@ public abstract class ToolkitHttpServlet extends HttpServlet {

private static final long serialVersionUID = -8470006030459575773L;

public static final DateTimeFormatter RFC1123_DATE_TIME_FORMATTER = DateTimeFormatter
.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US)
.withZone(ZoneId.of("GMT"));

/**
* Console logger
*/
Expand Down Expand Up @@ -104,8 +120,8 @@ protected final void outputException(final Throwable e, final HttpServletRequest

try {
final PrintWriter output = resp.getWriter();
if (e instanceof APIException) {
setLocalization((APIException) e, LocaleUtils.getUserLocaleAsString(req));
if (e instanceof APIException apiException) {
setLocalization(apiException, LocaleUtils.getUserLocaleAsString(req));
}

output.print(e == null ? "" : JSonSerializer.serialize(e));
Expand All @@ -115,6 +131,14 @@ protected final void outputException(final Throwable e, final HttpServletRequest
}
}

protected final void outputException(final Throwable e, final HttpServletRequest req,
final HttpServletResponse resp, final int httpStatusCode, Map<String, String> headers) {
for (var header : headers.entrySet()) {
resp.addHeader(header.getKey(), header.getValue());
}
outputException(e, req, resp, httpStatusCode);
}

/**
* Output an exception in JSon. Expect the status code to be already set
*
Expand Down Expand Up @@ -161,12 +185,7 @@ protected void catchAllExceptions(final Throwable exception, final HttpServletRe
LOGGER.info(exception.getMessage(), exception);
}
outputException(exception, req, resp, HttpServletResponse.SC_METHOD_NOT_ALLOWED);
} else if (exception instanceof APINotFoundException) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info(exception.getMessage(), exception);
}
outputException(exception, req, resp, HttpServletResponse.SC_NOT_FOUND);
} else if (exception instanceof ServiceNotFoundException) {
} else if (exception instanceof APINotFoundException || exception instanceof ServiceNotFoundException) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info(exception.getMessage(), exception);
}
Expand Down Expand Up @@ -194,6 +213,13 @@ protected void catchAllExceptions(final Throwable exception, final HttpServletRe
LOGGER.debug(exception.getMessage(), exception);
}
outputException(exception, req, resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} else if (exception instanceof APITooManyRequestException ex) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error(exception.getMessage(), exception);
}
var headers = Map.of("Retry-After",
RFC1123_DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(ex.getRetryAfter())));
outputException(exception, req, resp, ex.getStatusCode(), headers);
} else {
if (LOGGER.isErrorEnabled()) {
LOGGER.error(exception.getMessage(), exception);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ msgstr "Una aplicación es un entorno personalizado para un perfil de usuario es
msgid "An error has occurred. For more information, check the log file."
msgstr "Ha ocurrido un error. Para más información, compruebe el archivo de registro."

msgid "An error occured during categories update"
msgid "An error occurred during categories update"
msgstr "Se produjo un error durante la actualización de las categorías"

msgid "An error occurred when deploying the BDM. Consult the logs for more information."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ msgstr "Une application est un environnement adapté à un profil particulier, d
msgid "An error has occurred. For more information, check the log file."
msgstr "Une erreur s'est produite. Pour plus d'informations, consultez les logs."

msgid "An error occured during categories update"
msgid "An error occurred during categories update"
msgstr "Une erreur s'est produite à la mise à jour des catégories"

msgid "An error occurred when deploying the BDM. Consult the logs for more information."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ msgstr "アプリケーションは、特定のユーザー プロファイル
msgid "An error has occurred. For more information, check the log file."
msgstr "エラーが発生しました。 詳細についてはログファイルを確認してください。"

msgid "An error occured during categories update"
msgid "An error occurred during categories update"
msgstr "カテゴリの更新中にエラーが発生しました"

msgid "An error occurred when deploying the BDM. Consult the logs for more information."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ msgstr "Uma aplicação é um ambiente personalizado para um perfil específico,
msgid "An error has occurred. For more information, check the log file."
msgstr "Ocorreu um erro. Para mais informações, verifique o arquivo de log."

msgid "An error occured during categories update"
msgid "An error occurred during categories update"
msgstr "Ocorreu um erro durante a atualização de categorias"

msgid "An error occurred when deploying the BDM. Consult the logs for more information."
Expand Down
Loading

0 comments on commit 6bcb65f

Please sign in to comment.