Skip to content

Commit eb03cae

Browse files
authored
Fixed the keymanager performance issue (#540)
Signed-off-by: Dhanendra Sahu <dhanendra.tech@gmail.com>
1 parent cfd9622 commit eb03cae

5 files changed

Lines changed: 188 additions & 74 deletions

File tree

kernel/kernel-keymanager-service/src/main/java/io/mosip/kernel/cryptomanager/util/CryptomanagerUtils.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ public class CryptomanagerUtils {
6464

6565
private static ObjectMapper mapper = JsonMapper.builder().addModule(new AfterburnerModule()).build();
6666

67+
// Single shared instance seeded once at JVM startup. SecureRandom.nextBytes()
68+
// is thread-safe (synchronized internally). Static so it survives @RefreshScope
69+
// bean recreation and avoids re-seeding overhead (and potential entropy
70+
// starvation) at 150 new instances/sec under load.
71+
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
72+
6773
/** The Constant UTC_DATETIME_PATTERN. */
6874
private static final String UTC_DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
6975

@@ -235,8 +241,7 @@ public byte[] concatCertThumbprint(byte[] certThumbprint, byte[] encryptedKey){
235241

236242
public byte[] generateRandomBytes(int size) {
237243
byte[] randomBytes = new byte[size];
238-
SecureRandom secureRandom = new SecureRandom();
239-
secureRandom.nextBytes(randomBytes);
244+
SECURE_RANDOM.nextBytes(randomBytes);
240245
return randomBytes;
241246
}
242247

kernel/kernel-keymanager-service/src/main/java/io/mosip/kernel/keymanagerservice/config/ReqResFilter.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import jakarta.servlet.http.HttpServletResponse;
1313

1414
import org.springframework.web.util.ContentCachingRequestWrapper;
15-
import org.springframework.web.util.ContentCachingResponseWrapper;
1615

1716
/**
1817
* This class is for input logging of all parameters in HTTP requests
@@ -32,18 +31,17 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
3231
throws IOException, ServletException {
3332
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
3433
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
35-
ContentCachingRequestWrapper requestWrapper = null;
36-
ContentCachingResponseWrapper responseWrapper = null;
37-
3834
// Default processing for url ends with .stream
3935
if (httpServletRequest.getRequestURI().endsWith(".stream")) {
4036
chain.doFilter(request, response);
4137
return;
4238
}
43-
requestWrapper = new ContentCachingRequestWrapper(httpServletRequest);
44-
responseWrapper = new ContentCachingResponseWrapper(httpServletResponse);
45-
chain.doFilter(requestWrapper, responseWrapper);
46-
responseWrapper.copyBodyToResponse();
39+
// Cache only the first 4096 bytes — sufficient for JSON metadata (id, version)
40+
// without buffering the full encrypted payload in memory at high RPS.
41+
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpServletRequest, 4096);
42+
// Pass the actual response directly; response body buffering is not required
43+
// since ResponseBodyAdviceConfig only reads request metadata, not the response.
44+
chain.doFilter(requestWrapper, httpServletResponse);
4745

4846
}
4947

kernel/kernel-keymanager-service/src/main/java/io/mosip/kernel/keymanagerservice/config/ResponseBodyAdviceConfig.java

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.mosip.kernel.keymanagerservice.config;
22

3+
import jakarta.annotation.PostConstruct;
34
import jakarta.servlet.http.HttpServletRequest;
45
import jakarta.servlet.http.HttpServletRequestWrapper;
56

@@ -14,14 +15,14 @@
1415
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
1516
import org.springframework.web.util.ContentCachingRequestWrapper;
1617

18+
import com.fasterxml.jackson.core.JsonParser;
19+
import com.fasterxml.jackson.core.JsonToken;
1720
import com.fasterxml.jackson.databind.ObjectMapper;
1821
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
1922

20-
import io.mosip.kernel.core.http.RequestWrapper;
2123
import io.mosip.kernel.core.http.ResponseFilter;
2224
import io.mosip.kernel.core.http.ResponseWrapper;
2325
import io.mosip.kernel.core.logger.spi.Logger;
24-
import io.mosip.kernel.core.util.EmptyCheckUtils;
2526

2627
/**
2728
* @author Bal Vikash Sharma
@@ -36,6 +37,14 @@ public class ResponseBodyAdviceConfig implements ResponseBodyAdvice<ResponseWrap
3637
@Autowired
3738
private ObjectMapper objectMapper;
3839

40+
@PostConstruct
41+
public void init() {
42+
// Register JavaTimeModule once at startup on the shared singleton ObjectMapper.
43+
// Registering per-request (150 RPS) creates object churn and mutates shared
44+
// state concurrently — both corrected here.
45+
objectMapper.registerModule(new JavaTimeModule());
46+
}
47+
3948
/*
4049
* (non-Javadoc)
4150
*
@@ -63,27 +72,21 @@ public ResponseWrapper<?> beforeBodyWrite(ResponseWrapper<?> body, MethodParamet
6372
MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
6473
ServerHttpRequest request, ServerHttpResponse response) {
6574

66-
RequestWrapper<?> requestWrapper = null;
67-
String requestBody = null;
68-
6975
try {
7076
HttpServletRequest httpServletRequest = ((ServletServerHttpRequest) request).getServletRequest();
7177

78+
byte[] cachedBody = null;
7279
if (httpServletRequest instanceof ContentCachingRequestWrapper) {
73-
requestBody = new String(((ContentCachingRequestWrapper) httpServletRequest).getContentAsByteArray());
80+
cachedBody = ((ContentCachingRequestWrapper) httpServletRequest).getContentAsByteArray();
7481
} else if (httpServletRequest instanceof HttpServletRequestWrapper
7582
&& ((HttpServletRequestWrapper) httpServletRequest)
76-
.getRequest() instanceof ContentCachingRequestWrapper) {
77-
requestBody = new String(
78-
((ContentCachingRequestWrapper) ((HttpServletRequestWrapper) httpServletRequest).getRequest())
79-
.getContentAsByteArray());
83+
.getRequest() instanceof ContentCachingRequestWrapper) {
84+
cachedBody = ((ContentCachingRequestWrapper) ((HttpServletRequestWrapper) httpServletRequest).getRequest())
85+
.getContentAsByteArray();
8086
}
8187

82-
objectMapper.registerModule(new JavaTimeModule());
83-
if (!EmptyCheckUtils.isNullEmpty(requestBody)) {
84-
requestWrapper = objectMapper.readValue(requestBody, RequestWrapper.class);
85-
body.setId(requestWrapper.getId());
86-
body.setVersion(requestWrapper.getVersion());
88+
if (cachedBody != null && cachedBody.length > 0) {
89+
extractAndSetIdVersion(body, cachedBody);
8790
}
8891
body.setErrors(null);
8992
return body;
@@ -93,4 +96,42 @@ public ResponseWrapper<?> beforeBodyWrite(ResponseWrapper<?> body, MethodParamet
9396
return body;
9497
}
9598

96-
}
99+
/**
100+
* Extracts only the top-level "id" and "version" fields from the cached
101+
* (possibly truncated) request body using a streaming JSON parser.
102+
*
103+
* ReqResFilter caps the cached body at 4096 bytes. A full ObjectMapper.readValue()
104+
* fails with JsonEOFException when the encrypted "data" field spans the truncation
105+
* boundary (column 4097). The streaming parser reads token-by-token and stops as
106+
* soon as both fields are found — which happens within the first ~100 bytes since
107+
* "id" and "version" are always the first two fields in the MOSIP RequestWrapper
108+
* JSON envelope — long before any truncation can occur.
109+
*/
110+
private void extractAndSetIdVersion(ResponseWrapper<?> body, byte[] cachedBody) {
111+
try (JsonParser parser = objectMapper.getFactory().createParser(cachedBody)) {
112+
String id = null;
113+
String version = null;
114+
JsonToken token;
115+
while ((token = parser.nextToken()) != null) {
116+
if (id != null && version != null) break;
117+
if (token == JsonToken.FIELD_NAME) {
118+
String fieldName = parser.getCurrentName();
119+
token = parser.nextToken();
120+
if ("id".equals(fieldName) && token == JsonToken.VALUE_STRING) {
121+
id = parser.getText();
122+
} else if ("version".equals(fieldName) && token == JsonToken.VALUE_STRING) {
123+
version = parser.getText();
124+
} else if (token == JsonToken.START_OBJECT || token == JsonToken.START_ARRAY) {
125+
if (id != null && version != null) break;
126+
parser.skipChildren();
127+
}
128+
}
129+
}
130+
if (id != null) body.setId(id);
131+
if (version != null) body.setVersion(version);
132+
} catch (Exception e) {
133+
mosipLogger.debug("", "", "", "Could not extract id/version from cached request body: " + e.getMessage());
134+
}
135+
}
136+
137+
}

kernel/kernel-keymanager-service/src/main/java/io/mosip/kernel/keymanagerservice/exception/KeymanagerExceptionHandler.java

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/*
2-
*
3-
*
4-
*
5-
*
2+
*
3+
*
4+
*
5+
*
66
*/
77
package io.mosip.kernel.keymanagerservice.exception;
88

@@ -12,9 +12,11 @@
1212
import java.time.format.DateTimeParseException;
1313
import java.util.List;
1414

15+
import jakarta.annotation.PostConstruct;
1516
import jakarta.servlet.http.HttpServletRequest;
1617

17-
import com.fasterxml.jackson.databind.JsonNode;
18+
import com.fasterxml.jackson.core.JsonParser;
19+
import com.fasterxml.jackson.core.JsonToken;
1820
import com.fasterxml.jackson.databind.ObjectMapper;
1921
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
2022
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
@@ -44,7 +46,6 @@
4446
import io.mosip.kernel.core.signatureutil.exception.ParseResponseException;
4547
import io.mosip.kernel.core.signatureutil.exception.SignatureUtilClientException;
4648
import io.mosip.kernel.core.signatureutil.exception.SignatureUtilException;
47-
import io.mosip.kernel.core.util.EmptyCheckUtils;
4849
import io.mosip.kernel.cryptomanager.constant.CryptomanagerErrorCode;
4950
import io.mosip.kernel.cryptomanager.exception.CryptoManagerSerivceException;
5051
import io.mosip.kernel.keymanagerservice.constant.KeymanagerConstant;
@@ -72,9 +73,17 @@ public class KeymanagerExceptionHandler {
7273
@Autowired
7374
private ObjectMapper objectMapper;
7475

76+
@PostConstruct
77+
public void init() {
78+
// Register JavaTimeModule once at startup. The original code called
79+
// registerModule() inside setErrors() which runs on every exception path,
80+
// mutating the shared ObjectMapper bean concurrently and creating garbage.
81+
objectMapper.registerModule(new JavaTimeModule());
82+
}
83+
7584
@ExceptionHandler(NullDataException.class)
7685
public ResponseEntity<ResponseWrapper<ServiceError>> nullDataException(HttpServletRequest httpServletRequest,
77-
final NullDataException e) throws IOException {
86+
final NullDataException e) throws IOException {
7887
ExceptionUtils.logRootCause(e);
7988
return new ResponseEntity<>(
8089
getErrorResponse(httpServletRequest, e.getErrorCode(), e.getErrorText(), HttpStatus.OK), HttpStatus.OK);
@@ -367,17 +376,44 @@ private ResponseWrapper<ServiceError> getErrorResponse(HttpServletRequest httpSe
367376
private ResponseWrapper<ServiceError> setErrors(HttpServletRequest httpServletRequest) throws IOException {
368377
ResponseWrapper<ServiceError> responseWrapper = new ResponseWrapper<>();
369378
responseWrapper.setResponsetime(LocalDateTime.now(ZoneId.of("UTC")));
370-
String requestBody = null;
379+
380+
byte[] cachedBody = null;
371381
if (httpServletRequest instanceof ContentCachingRequestWrapper) {
372-
requestBody = new String(((ContentCachingRequestWrapper) httpServletRequest).getContentAsByteArray());
382+
cachedBody = ((ContentCachingRequestWrapper) httpServletRequest).getContentAsByteArray();
373383
}
374-
if (EmptyCheckUtils.isNullEmpty(requestBody)) {
384+
if (cachedBody == null || cachedBody.length == 0) {
375385
return responseWrapper;
376386
}
377-
objectMapper.registerModule(new JavaTimeModule());
378-
JsonNode reqNode = objectMapper.readTree(requestBody);
379-
responseWrapper.setId(reqNode.path("id").asText());
380-
responseWrapper.setVersion(reqNode.path("version").asText());
387+
388+
// Use a streaming JSON parser to extract only the top-level "id" and "version"
389+
// fields. The cached body is capped at 4096 bytes (ReqResFilter) so a full
390+
// readTree() fails with JsonEOFException when the encrypted "data" field
391+
// crosses the truncation boundary. The streaming parser stops as soon as both
392+
// fields are found — within the first ~100 bytes — before any truncation.
393+
try (JsonParser parser = objectMapper.getFactory().createParser(cachedBody)) {
394+
String id = null;
395+
String version = null;
396+
JsonToken token;
397+
while ((token = parser.nextToken()) != null) {
398+
if (id != null && version != null) break;
399+
if (token == JsonToken.FIELD_NAME) {
400+
String fieldName = parser.getCurrentName();
401+
token = parser.nextToken();
402+
if ("id".equals(fieldName) && token == JsonToken.VALUE_STRING) {
403+
id = parser.getText();
404+
} else if ("version".equals(fieldName) && token == JsonToken.VALUE_STRING) {
405+
version = parser.getText();
406+
} else if (token == JsonToken.START_OBJECT || token == JsonToken.START_ARRAY) {
407+
if (id != null && version != null) break;
408+
parser.skipChildren();
409+
}
410+
}
411+
}
412+
if (id != null) responseWrapper.setId(id);
413+
if (version != null) responseWrapper.setVersion(version);
414+
} catch (Exception e) {
415+
// Best-effort: truncated or malformed body won't affect error response structure
416+
}
381417
return responseWrapper;
382418
}
383419

0 commit comments

Comments
 (0)