what is the standard way to design a high throughput spring based web serivce with Async in mind - spring-boot

I have a requirement to design spring-boot based webservice with #Async tags.
if the data return from server takes huge time how can i divide my webservice in multiple end points so that client does not have to wait for response from server.
i hv tried using three end points.
1. localhost:8080/start -> client will send request for data. (returs a uuid of requestnumber).
2. localhost:8080/checkprogress -> check for progress if data is ready at server.
3. localhost:8080/done/requestId -> return the data list
#ResponseBody
#GetMapping(value = "/start")
public ResponseEntity<String> start() {
String requestId = UUID.randomUUID().toString();
LOG.info("jobName" + requestId);
Job job = new Job("jobName" + requestId);
requestQueue.put(requestId.toString(), job);
service.submitWork(job);
return new ResponseEntity<String>(requestId, HttpStatus.ACCEPTED);
}
#ResponseBody
#RequestMapping(value = "/progress/{requestId}")
public ResponseEntity<String> fetchStatus(#PathVariable("requestId") String requestId) {
Job job = requestQueue.get(requestId);
if (job == null) {
return new ResponseEntity<String>("RequestId is either invalid or already served. requestId:" + requestId,
HttpStatus.BAD_REQUEST);
}
if (job.getState() == State.RUNNING || job.getState() == State.NEW)
return new ResponseEntity<String>(HttpStatus.NO_CONTENT);
if (job.getState() == State.DONE)
return new ResponseEntity<String>(HttpStatus.OK);
return null;
}
#ResponseBody
#RequestMapping(value = "/done/{requestId}")
public ResponseEntity<Object> done(#PathVariable("requestId") String requestId) {
LOG.info("removing requestId:" + requestId);
Job job = requestQueue.get(requestId);
if (job == null) {
return new ResponseEntity<Object>("RequestId is either invalid or already served. requestId:" + requestId,
HttpStatus.BAD_REQUEST);
}
// ResponseEntity<Object> response = new ResponseEntity<Object>(job.getList(),
// createHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE), HttpStatus.OK);
ResponseEntity<Object> response = new ResponseEntity<Object>(job.getListOfSecurities(),
createHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE), HttpStatus.OK);
requestQueue.remove(requestId);
return response;
}
is there a better way of doing above thing in a Standard way in spring-boot java8 application please?

Related

Handle Sharp In Controller And Get Id

There was a jsp application. I have just converted to spring boot application. I want to continue to use same links to handle company's information. Old urls are like /Dashboard.jsp#/company/10712. I have tried to handle company id but it didn't wook. How can I handle company id ?
#GetMapping("/Dashboard.jsp#/company/{id}")
public void try(#PathVariable String id) {
System.out.println(id);
}
I have also tried;
adding
server.tomcat.relaxed-path-chars=#
in application properties.
#RequestMapping(value = ERROR_PATH, produces = "text/html")
public Object errorHtml(HttpServletRequest request, HttpServletResponse response) {
if (response.getStatus() == HttpStatus.NOT_FOUND.value()) {
return new ModelAndView("redirect:" + StringUtils.getBaseUrl(request) + "/?page=error", HttpStatus.FOUND);
} else {
return new ModelAndView("redirect:" + StringUtils.getBaseUrl(request) + "/?page=error");
}
}
This function handle 404.
request.getAttribute("javax.servlet.forward.request_uri")
returns /esir/Dashboard.jsp. There is no # and others.

How to retry RestAPI connection if it fails for first time in restTemplate?

I am calling a third party rest API, some times it sends response with status code 500, 504.
I want to make a another hit to the API if it gives above status code.
My current logic of retry is:
public <T> ResponseEntity<T> sendGetRequest(String url,
Class<T> responseClazz,
HttpHeaders headers) {
ResponseEntity<T> response = null;
int count = 0;
int maxTries = 2;
while(true) {
try {
HttpEntity request = new HttpEntity(headers);
response = restTemplate.exchange(url, HttpMethod.GET, request, responseClazz);
if(response.getStatusCode() != HttpStatus.OK) {
log.error("null or Error response from server for ", url);
}
log.info("Response received {}", response.toString());
return response;
}catch (ResourceAccessException rae){
log.warn("retry count {} {}", count, rae);
if (++count == maxTries) throw new ServerErrorException("API timeout");
}
}
}
I have also used apache http where I use CloseableHttpClient to retry for status code 500 and 504.
I have also looks to the solution of spring-retry. Is there any other method to do this?
When calling HTTP request with RestTemplate, there are 2 main cases to retry:
Specific response HTTP statuses. For example:
503 Service Unavailable status can be retried.
404 Not Found can be proceeded without a retry attempt.
ResourceAccessException which can represent some IO exception received without getting the HTTP server response, like SocketException: Broken pipe.
For solution based on RestTemplate/HttpClient, while it exposes options to retry based on the HTTP response, combining it with IOException handling can be tricky.
Solution based on Spring RetryTemplate
#Bean(name = "restTemplateRetryTemplate")
public RetryTemplate restTemplateRetryTemplate() {
return createRestTemplateRetryTemplate();
}
private RetryTemplate createRestTemplateRetryTemplate(boolean retryOnServerErrors) {
Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>();
retryableExceptions.put(ResourceAccessException.class, true);
retryableExceptions.put(HttpServerErrorException.ServiceUnavailable.class, true);
retryableExceptions.put(HttpServerErrorException.BadGateway.class, true);
retryableExceptions.put(HttpServerErrorException.GatewayTimeout.class, true);
retryableExceptions.put(HttpClientErrorException.TooManyRequests.class, true);
return createRetryTemplate(retryableExceptions);
}
private RetryTemplate createRetryTemplate(Map<Class<? extends Throwable>, Boolean> retryableExceptions) {
RetryTemplate retryTemplate = new RetryTemplate();
ExponentialRandomBackOffPolicy exponentialRandomBackOffPolicy = new ExponentialRandomBackOffPolicy();
exponentialRandomBackOffPolicy.setInitialInterval(INITIAL_INTERVAL);
exponentialRandomBackOffPolicy.setMaxInterval(MAX_INTERVAL);
exponentialRandomBackOffPolicy.setMultiplier(MULTIPLIER);
retryTemplate.setBackOffPolicy(exponentialRandomBackOffPolicy);
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(MAX_ATTEMPTS, retryableExceptions));
// Optional, for additional logging on failures.
retryTemplate.registerListener(retryTemplateLogListener);
return retryTemplate;
}
Usage example
#Autowired
#Qualifier("restTemplateRetryTemplate")
private RetryTemplate retryTemplate;
...
String result = retryTemplate.execute(arg -> {
return longRestTemplate.getForObject(url, String.class);
});

How to read/modify form data that goes through Spring Cloud Gateway?

I am trying to validate and log form data that goes through Spring Cloud Gateway. I have tried a few methods and encounter a few problems and I could not read it properly. I have tried:
#Component
public class GatewayRequestFilter {
#Bean
public GlobalFilter apply() {
return (exchange, chain) -> {
MediaType contentType = exchange.getRequest().getHeaders().getContentType();
ModifyRequestBodyGatewayFilterFactory.Config modifyRequestConfig = new ModifyRequestBodyGatewayFilterFactory.Config();
/// Method 1
if (contentType.includes(MediaType.MULTIPART_FORM_DATA)) {
modifyRequestConfig.setRewriteFunction(String.class, String.class, (exchange1, originalRequestBody) -> {
validateAndAuditLog(exchange1, originalRequestBody);
return Mono.just(originalRequestBody);
});
}
/// Method 2
if (contentType.includes(MediaType.MULTIPART_FORM_DATA)) {
return exchange.getMultipartData().flatMap(originalRequestBody -> {
validateAndAuditLog(exchange1, originalRequestBody);
return chain.filter(exchange);
});
}
/// Method 3:
/// https://github.com/spring-cloud/spring-cloud-gateway/issues/1307#issuecomment-553910834
return new ModifyRequestBodyGatewayFilterFactory().apply(modifyRequestConfig).filter(exchange, chain);
};
}
}
For the 1st and 3rd method, if I set inClass as String.class then I can see data in some kind of http format. The problem is that I don't know how to parse it into hashMap or LinkedMultiValueMap to access each of value using key. Here is the output I get:
----------------------------162653831591335516327921
Content-Disposition: form-data; name="simple-text"
text
----------------------------162653831591335516327921
Content-Disposition: form-data; name="simple-file"; filename="simple-file"
Content-Type: application/octet-stream
Simple file
----------------------------162653831591335516327921--
If I change inClass as Object.class then there is another error:
{
"timestamp": "2020-04-03T02:37:57.096+0000",
"path": "/tc/test/test",
"status": 500,
"error": "Internal Server Error",
"message": "Content type 'multipart/form-data;boundary=--------------------------537619313111072161580699' not supported for bodyType=java.lang.Object",
"requestId": "0592497a-1"
}
For the 2nd method I can get data in LinkedMultiValueMap which is good because I can read each data using key value and I can also get uploaded files name, but the problem is that, it hang for 10s before pass the request to down stream.
Anyone has any idea what should I do to read or modify form data that goes through Spring Cloud Gateway?
Rewriting the answer with example.
Basic approach is defined here, though it needs lot of refinement to work for multi-part.
https://developpaper.com/question/how-to-modify-the-request-parameters-of-multipart-form-data-format-in-spring-cloud-gateway/
For any approach to work once you read the data, you need to set a modified request object to exchange downstream to be processed again. Setting the new multi-part object downstream is bit tricky because there is not a straightforward way to convert string->multi-part->string.
Here is a sample code based on the approach. Note that this for now works only if multi-part contains form fields and not file type fields, because in later case we are dealing with a stream, which can be embedded anywhere within the entire multi-part request, and it is not possible to modify such request without blocking calls, which the netty does not allow.
private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter((exchange, chain) -> {
ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
// get modified body from original body o
Mono<MultiValueMap<String, String>> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(o -> {
// create mock request to read body
SynchronossPartHttpMessageReader synchronossReader = new SynchronossPartHttpMessageReader();
MultipartHttpMessageReader reader = new MultipartHttpMessageReader(synchronossReader);
MockServerHttpRequest request = MockServerHttpRequest.post("").contentType(exchange.getRequest().getHeaders().getContentType()).body(o);
Mono<MultiValueMap<String, Part>> monoRequestParts = reader.readMono(MULTIPART_DATA_TYPE, request, Collections.emptyMap());
// modify parts
return monoRequestParts.flatMap(requestParts -> {
Map<String, List<String>> modifedBodyArray = requestParts.entrySet().stream().map(entry -> {
String key = entry.getKey();
LOGGER.info(key);
List<String> entries = entry.getValue().stream().map(part -> {
LOGGER.info("{}", part);
// read the input part
String input = ((FormFieldPart) part).value();
// return the modified input part
return new String(modifyRequest(config, exchange, key, input));
}).collect(Collectors.toList());
return new Map.Entry<String, List<String>>() {
#Override
public String getKey() {
return key;
}
#Override
public List<String> getValue() {
return entries;
}
#Override
public List<String> setValue(List<String> param1) {
return param1;
}
};
}).collect(Collectors.toMap(k -> k.getKey(), k -> k.getValue()));
return Mono.just(new LinkedMultiValueMap<String, String>(modifedBodyArray));
});
});
// insert the new modified body
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, new ParameterizedTypeReference<MultiValueMap<String, String>>() {});
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// the new content type will be computed by bodyInserter
// and then set in the request decorator
headers.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
ServerHttpRequest decorator = decorate(exchange, headers, outputMessage);
return chain.filter(exchange.mutate().request(decorator).build());
}));
}, RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER + 1);
}
// some of the helper methods
private String modifyRequest(Config config, ServerWebExchange exchange, String key, String input) {
// do your thing in here !!!
return input;
}
private ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
#Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(headers);
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
// TODO: this causes a 'HTTP/1.1 411 Length Required' // on httpbin.org
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
#Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
}

how to Throw an exception from an #Async annotated method

I have two modules, one calls the other from a rest template.
( admin calls notifServer)
the notifServer has a method annotated with #Async . I want to throw an exception in that method, but the admin gets the response too quickly and the exception method cannot be caught at admin.
I an new to spring and the #Async process. I've tried mapping the response body from the NotifServer to a CCompletableFuture.class .
But Still I get no error response.
This code is from admin
ResponseEntity response = fcmRestTemplate.exchange(nsUrl + "/fcm/admin/" + bulkFcmId, HttpMethod.POST,
HttpEntityUtils.getHttpEntity(moduleCode), CompletableFuture.class);
if (response.getStatusCode() != HttpStatus.CREATED && response.getStatusCode() != HttpStatus.ACCEPTED) {
String errorMessage = ErrorResourceUtil.getErrorMessage((HashMap) response.getBody(),"Unable to send fcm");
setStatusToFailedByBulkFcmId(bulkFcmId);
throw new ClientException(errorMessage);
}
now this is from NotifServer
JobExecution jobExecution = jobLauncher
.run(importJob, new JobParametersBuilder()
.addString("fullPathFileName", TMP_DIR)
.addString("batch_fcm_id", String.valueOf(id))
.addLong("time",System.currentTimeMillis())
.toJobParameters());
if(jobExecution.getStepExecutions().stream().map(StepExecution::getStatus).findFirst().get().equals(BatchStatus.ABANDONED)){
throw new ClientException("INVALID CSV");
This is annotated with #Async.
So is there a way for me to catch the client exception in the response body in the Admin?
EDIT
This is the API from notifServer
#ResponseStatus(HttpStatus.CREATED)
#PostMapping(value = "/admin/{bulkFcmId}")
public void pushFCMByAdmin(#PathVariable Long bulkFcmId) {
fcmService.sendFcmByAdmin(bulkFcmId, AuthUtil.getCurrentUser());
}
Then the sendFcmByAdmin has #Async annotation.
In below code can you provide the return type to be a business object rather than CompletableFuture.class.Since you are passing CompletableFuture.class as a parameter to the exchange it expects a response return value of the type CompletableFuture.class.
ResponseEntity response = fcmRestTemplate.exchange(nsUrl + "/fcm/admin/" + bulkFcmId, HttpMethod.POST,
HttpEntityUtils.getHttpEntity(moduleCode), CompletableFuture.class);
if (response.getStatusCode() != HttpStatus.CREATED && response.getStatusCode() != HttpStatus.ACCEPTED) {
String errorMessage = ErrorResourceUtil.getErrorMessage((HashMap) response.getBody(),"Unable to send fcm");
setStatusToFailedByBulkFcmId(bulkFcmId);
throw new ClientException(errorMessage);
}
Instead of passing Completable Future ,can you try creating it as follows:
Use an asynchronous method to make the rest template call in admin:
#Async
public CompletableFuture<List<BusinessObject>> getResponseAsynchronously(String value) {
String url = "https://restendpoint.eu/rest/v2/lang/" + value + "?fields=name";
BusinessObject[] response = restTemplate.getForObject(url, Country[].class);
return CompletableFuture.completedFuture(Arrays.asList(response));
}
then in the controller read the CompletableFuture like:
#GetMapping("")
public List<String> getAllDataFromRestCall() throws Throwable {
CompletableFuture<List<BusinessObject>> businessObjectsFuture = countryClient.getResponseAsynchronously("fr");
List<String> europeanFrenchSpeakingCountries;
try {
europeanFrenchSpeakingCountries = new ArrayList<>(businessObjectsFuture
.get()
.stream()
.map(Country::getName)
.collect(Collectors.toList()));
} catch (Throwable e) {
throw e.getCause();
}
return europeanFrenchSpeakingCountries;
}

HttpURLConnnection request failures on Android 6.0 (MarshMallow)

I am using Google's Volley library for my application project , that targets minimum api level 14.So, Volley library uses HttpURLConnection as the NetworkClient.
Therefore , there should not be any issue related to Removal of Apache HTTPClient. However, I have done the configuration required for 6.0 Sdk i.e compileSdkVersion 23, build-tool-version 23.0.1 and build:gradle:1.3.1' and even tried adding useLibrary 'org.apache.http.legacy'. Have updated the same for Volley library project in my application.
Recently ,I tried to run my app on Android 6.0 (MarshMallow), my project compiles and runs. But those requests that require authentication headers are failing on MarshMallow with:
BasicNetwork.performRequest: Unexpected response code 401 com.android.volley.AuthFailureError
However the same is running on all Api level below 23.
I have checked the headers many times.Strangely, those requests that do not require authentication are giving response with 200 OK.
Right now I am totally clueless what is breaking the requests, does anybody have any idea what has changed in new Version that HttpURLConnection request fails for only Api level 23? Is anybody else using Volley and facing similar issue?
Here is my CustomRequest Class
public class CustomRequest extends Request<Void> {
int id, cmd;
Map<String, String> params;
BaseModel model;
public CustomRequest(int method, int cmd, String url, Map<String, String> params, int id, BaseModel model) {
super(method, url, null);
this.id = id;
this.cmd = cmd;
this.params = params;
this.model = model;
if (method == Method.GET) {
setUrl(buildUrlForGetRequest(url));
}
Log.v("Volley", "Making request to: " + getUrl());
}
private String buildUrlForGetRequest(String url) {
if (params == null || params.size() == 0) return url;
StringBuilder newUrl = new StringBuilder(url);
Set<Entry<String, String>> paramPairs = params.entrySet();
Iterator<Entry<String, String>> iter = paramPairs.iterator();
newUrl.append("?");
while (iter.hasNext()) {
Entry<String, String> param = iter.next();
newUrl
.append(param.getKey())
.append("=")
.append(param.getValue());
if (iter.hasNext()) newUrl.append("&");
}
return newUrl.toString();
}
#Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<String, String>();
headers.put("X-Api-Version", Contract.API_VERSION);
headers.put("X-Client", "android");
String accessToken = APP.getInstance().getToken();
if (!accessToken.equals("")) {
headers.put("Authorization", "Bearer " + accessToken);
}
return headers;
}
#Override
protected Response<Void> parseNetworkResponse(NetworkResponse response) {
Exception ex;
try {
String jsonString = new String(
response.data, HttpHeaderParser.parseCharset(response.headers));
JsonNode json = new ObjectMapper().readTree(jsonString);
if (model != null) model.parse(id, json);
EventBus.getDefault().post(new EventResponse(cmd, true, null));
return Response.success(null, HttpHeaderParser.parseCacheHeaders(response)); //Doesn't return anything. BaseModel.parse() does all the storage work.
} catch (NoMoreDataException e) {
ex = e;
EventBus.getDefault().post(new NoMoreDataModel(cmd, e));
EventBus.getDefault().post(new EventResponse(cmd, false, null));
return Response.success(null, HttpHeaderParser.parseCacheHeaders(response));
} catch (Exception e) {
ex = e;
Log.e("CustomRequest", Log.getStackTraceString(e));
String message = APP.getInstance().getString(R.string.failedRequest);
if (!TextUtils.isEmpty(e.getMessage()))
message = e.getMessage();
EventBus.getDefault().post(new ErrorEventModel(cmd, message, e));
return Response.error(new ParseError(ex));
}
}
#Override
protected Map<String, String> getParams() throws AuthFailureError {
return params;
}
#Override
protected void deliverResponse(Void response) {
Log.v("Volley", "Delivering result: " + getUrl());
}
#Override
public void deliverError(VolleyError error) {
Log.e("CustomRequest", "Delivering error: Request=" + getUrl() + " | Error=" + error.toString());
String message = APP.getInstance().getString(R.string.failedRequest);
EventBus.getDefault().post(new ErrorEventModel(cmd, message, error));
}
}
Only difference I found between Api 23 and others is the HostNameVerifier.
For Api level 23 : com.android.okhttp.internal.tls.OkHostnameVerifier
For Api level <23 : javax.net.ssl.DefaultHostnameVerifier.
After checking the Server side of my application, found the reason behind the issue.
For Android 6.0(MarshMallow) the headers were becoming empty and this was not the case with other versions.
So the fix that worked for me:
Created and Added a new custom header X-Proxy-No-Redirect => 1 and passed along with other headers.
Theory behind it:
There is a API server to which we send request, then the API server redirects the request to corresponding site based on the oAuth token
While redirecting
For the redirection to happen, there are two ways to do that
1 - We just send a response back to the caller stating to redirect to a certain page. Then the caller(Networking library) takes care of redirection
2 - API server will itself makes the redirect request and get the response and then pass to caller
Earlier we were using the Method 1.
On Android 6.0 - The networking lib(Volley) doesn't seem to set all the headers while making the redirection request.
Once this new header is set, Method 2 will come into effective.
P.S This fix was applicable for my application project , maybe not for others.Just providing what was wrong and what helped me.

Resources