Skip to content

Commit 1efd34a

Browse files
committed
Improve logic for async RPA approval processing
1 parent b2e8248 commit 1efd34a

10 files changed

Lines changed: 147 additions & 44 deletions

File tree

pom.xml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
<sonar.jacoco.reportPath>${coverage.reports.dir}/jacoco-ut.exec</sonar.jacoco.reportPath>
3030
<sonar.jacoco.itReportPath>${coverage.reports.dir}/jacoco-it.exec</sonar.jacoco.itReportPath>
3131
<sonar.host.url>http://oardev3.nist.gov:9000/sonarqube</sonar.host.url>
32+
<surefireArgLine></surefireArgLine>
33+
<failsafeArgLine></failsafeArgLine>
3234
</properties>
3335

3436
<dependencyManagement>
@@ -279,15 +281,16 @@
279281
<artifactId>maven-surefire-plugin</artifactId>
280282

281283
<configuration>
282-
<argLine>${surefireArgLine}</argLine>
283-
<argLine>-Djava.io.tmpdir=${basedir}/target/tmp</argLine>
284+
<!-- Mockito inline needs instrumentation; preload Byte Buddy instead of relying on JDK self-attach. -->
285+
<argLine>${surefireArgLine} -javaagent:${settings.localRepository}/net/bytebuddy/byte-buddy-agent/${byte-buddy.version}/byte-buddy-agent-${byte-buddy.version}.jar -Djava.io.tmpdir=${project.build.directory}</argLine>
284286
<includes>
285287
<include>**/*Test.java</include>
286288
</includes>
287289
<systemPropertyVariables>
288290
<basedir>${project.basedir}</basedir>
289291
</systemPropertyVariables>
290-
<forkCount>0</forkCount>
292+
<forkCount>1</forkCount>
293+
<reuseForks>true</reuseForks>
291294
<systemPropertyVariables>
292295
<conf.path>${user.home}/spm</conf.path>
293296
<project.test.resourceDirectory>${project.basedir}/src/test/resources</project.test.resourceDirectory>

src/main/java/gov/nist/oar/distrib/cachemgr/CacheExpiryCheck.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public CacheExpiryCheck(StorageInventoryDB inventoryDB) {
2121
/**
2222
* Checks if a cache object is expired and removes it from the cache if it is.
2323
* The method uses the {@code expires} metadata field to determine the expiration status.
24-
* The expiration time is calculated based on the {@code LastModified} time plus the {@code expires} duration.
24+
* The expiration time is stored as an absolute epoch millisecond value.
2525
* If the current time is past the calculated expiry time, the object is removed from the inventory database.
2626
*
2727
* @param co The cache object to check for expiration.
@@ -36,17 +36,11 @@ public void check(CacheObject co) throws IntegrityException, StorageVolumeExcept
3636
}
3737

3838
if (co.hasMetadatum("expires")) {
39-
long expiresDuration = co.getMetadatumLong("expires", -1L);
40-
if (expiresDuration == -1L) {
39+
long expiryTime = co.getMetadatumLong("expires", -1L);
40+
if (expiryTime == -1L) {
4141
throw new IntegrityException("Invalid 'expires' metadata value");
4242
}
4343

44-
long lastModified = co.getLastModified();
45-
if (lastModified == -1L) {
46-
throw new IntegrityException("CacheObject 'lastModified' time not available");
47-
}
48-
49-
long expiryTime = lastModified + expiresDuration;
5044
long currentTime = Instant.now().toEpochMilli();
5145

5246
// Check if the object is expired

src/main/java/gov/nist/oar/distrib/service/rpa/EmailInfoProvider.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import java.time.ZonedDateTime;
99
import java.time.format.DateTimeFormatter;
10+
import java.time.temporal.ChronoUnit;
1011
import java.util.AbstractMap;
1112
import java.util.List;
1213
import java.util.Locale;
@@ -21,7 +22,6 @@
2122
public class EmailInfoProvider {
2223

2324
private static final String DATE_PATTERN = "EEEE, MM/dd/yyyy 'at' hh:mm a z";
24-
private static final int EXPIRATION_DAYS = 14;
2525
private final RPAConfiguration rpaConfiguration;
2626

2727
/**
@@ -208,7 +208,7 @@ private Map<String, String> getNamedPlaceholders(Record record, String downloadU
208208
*/
209209
private String getExpirationDate() {
210210
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_PATTERN, Locale.ENGLISH);
211-
ZonedDateTime date = ZonedDateTime.now().plusDays(EXPIRATION_DAYS);
211+
ZonedDateTime date = ZonedDateTime.now().plus(rpaConfiguration.getExpiresAfterMillis(), ChronoUnit.MILLIS);
212212
return formatter.format(date);
213213
}
214214
}

src/main/java/gov/nist/oar/distrib/service/rpa/HttpURLConnectionRPARequestHandlerService.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ public RecordUpdateResult updateRecord(String recordId, String status, String sm
713713

714714
// Return result for async processing
715715
// Note: Email notifications and caching are handled asynchronously
716+
record.getUserInfo().setApprovalStatus(approvalStatus);
716717
return new RecordUpdateResult(recordStatus, record, datasetId);
717718
}
718719

@@ -755,6 +756,7 @@ private void handleApprovalPostProcessing(Record record, String datasetId)
755756

756757
if (randomId == null) {
757758
LOGGER.error("Caching process returned a null randomId for dataset: {}", datasetId);
759+
patchFailureStatus(record);
758760
this.recordResponseHandler.onFailure(record);
759761
return;
760762
}
@@ -763,14 +765,23 @@ private void handleApprovalPostProcessing(Record record, String datasetId)
763765
String currentStatus = record.getUserInfo().getApprovalStatus();
764766
String finalStatus = updateApprovalStatusWithRandomId(currentStatus, randomId);
765767
patchRecordStatus(record.getId(), finalStatus);
768+
record.getUserInfo().setApprovalStatus(finalStatus);
766769

767770
// Send success email with download link
768771
this.recordResponseHandler.onRecordUpdateApproved(record, randomId);
769772
LOGGER.info("Approval post-processing completed for record: {}", record.getId());
770773

771774
} catch (Exception e) {
772775
LOGGER.error("Approval post-processing failed for record {}: {}", record.getId(), e.getMessage(), e);
773-
// Send failure notification to user
776+
if (randomId != null) {
777+
try {
778+
this.rpaDatasetCacher.uncache(randomId);
779+
} catch (Exception uncacheException) {
780+
LOGGER.error("Failed to uncache dataset after approval failure (random ID {}): {}",
781+
randomId, uncacheException.getMessage(), uncacheException);
782+
}
783+
}
784+
patchFailureStatus(record);
774785
this.recordResponseHandler.onFailure(record);
775786
}
776787
}
@@ -799,14 +810,25 @@ private void handleDeclinePostProcessing(Record record) {
799810
LOGGER.info("Decline post-processing completed for record: {}", record.getId());
800811
}
801812

813+
private void patchFailureStatus(Record record) {
814+
String failureStatus = updateApprovalStatusPrefix(record.getUserInfo().getApprovalStatus(), "ApprovalFailed");
815+
try {
816+
patchRecordStatus(record.getId(), failureStatus);
817+
record.getUserInfo().setApprovalStatus(failureStatus);
818+
} catch (RequestProcessingException e) {
819+
LOGGER.error("Failed to update approval failure status for record {}: {}",
820+
record.getId(), e.getMessage(), e);
821+
}
822+
}
823+
802824
/**
803825
* Updates the approval status string to include the random ID.
804826
* Converts "ApprovalPending_timestamp_smeId" to "Approved_timestamp_smeId_randomId"
805827
*/
806828
private String updateApprovalStatusWithRandomId(String currentStatus, String randomId) {
807829
// Replace "ApprovalPending" with "Approved" and append randomId
808830
if (currentStatus != null && currentStatus.startsWith("ApprovalPending_")) {
809-
return currentStatus.replace("ApprovalPending_", "Approved_") + "_" + randomId;
831+
return updateApprovalStatusPrefix(currentStatus, "Approved") + "_" + randomId;
810832
}
811833
// Fallback: if already "Approved_", just append randomId
812834
if (currentStatus != null && currentStatus.startsWith("Approved_") && !currentStatus.contains("_" + randomId)) {
@@ -815,6 +837,13 @@ private String updateApprovalStatusWithRandomId(String currentStatus, String ran
815837
return currentStatus;
816838
}
817839

840+
private String updateApprovalStatusPrefix(String currentStatus, String newPrefix) {
841+
if (currentStatus != null && currentStatus.contains("_")) {
842+
return newPrefix + currentStatus.substring(currentStatus.indexOf("_"));
843+
}
844+
return newPrefix + "_" + Instant.now().toString();
845+
}
846+
818847
/**
819848
* Sends a PATCH request to update the record status in Salesforce.
820849
*/

src/main/java/gov/nist/oar/distrib/service/rpa/RecordResponseHandlerImpl.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,12 @@ public void onRecordUpdateApproved(Record record, String randomId) throws Invali
158158
*/
159159
@Override
160160
public void onRecordUpdateDeclined(Record record) throws InvalidRequestException, RequestProcessingException {
161-
LOGGER.debug("User was declined by SME");
161+
LOGGER.debug("User was declined by SME. Sending decline notification...");
162+
if (this.emailSender.sendDeclinedEmailToEndUser(record)) {
163+
LOGGER.debug("Decline notification email sent successfully (RecordID=" + record.getId() + ")");
164+
} else {
165+
throw new RequestProcessingException("Failed to send decline notification email to end user");
166+
}
162167
}
163168

164169
/**

src/main/java/gov/nist/oar/distrib/web/RPAAsyncExecutor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public void handleAfterRecordCreationAsync(RecordWrapper wrapper, UserInfoWrappe
3737
try {
3838
handler.handleAfterRecordCreation(wrapper, input, code);
3939
} catch (RequestProcessingException e) {
40-
LOGGER.error("Async post-processing failed for record ID {}: {}", wrapper.getRecord().getId(),
40+
String recordId = wrapper != null && wrapper.getRecord() != null ? wrapper.getRecord().getId() : "unknown";
41+
LOGGER.error("Async post-processing failed for record ID {}: {}", recordId,
4142
e.getMessage(), e);
4243
}
4344
}

src/main/java/gov/nist/oar/distrib/web/RPAConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public class RPAConfiguration {
6161
private Map<String, BlacklistConfig> blacklists = new HashMap<>();
6262

6363
@JsonProperty("expiresAfterMillis")
64-
long expiresAfterMillis = 0L;
64+
long expiresAfterMillis = 1209600000L;
6565

6666
@JsonProperty("supportEmail")
6767
private String supportEmail;

src/test/java/gov/nist/oar/distrib/cachemgr/CacheExpiryCheckTest.java

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package gov.nist.oar.distrib.cachemgr;
22

3-
import static org.junit.jupiter.api.Assertions.assertThrows;
43
import static org.mockito.ArgumentMatchers.anyString;
54
import static org.mockito.Mockito.never;
65
import static org.mockito.Mockito.verify;
@@ -32,8 +31,8 @@ public void setUp() {
3231

3332
/**
3433
* Test to verify that {@link CacheExpiryCheck} correctly identifies and processes an expired cache object.
35-
* An object is considered expired based on the `expires` metadata, which defines the duration after which
36-
* an object should be considered expired from the time of its last modification. This test ensures that an object
34+
* An object is considered expired based on the `expires` metadata, which defines the absolute epoch
35+
* millisecond time when the object should expire. This test ensures that an object
3736
* past its expiration is appropriately removed from the inventory database.
3837
*
3938
* @throws Exception to handle any exceptions thrown during the test execution
@@ -43,8 +42,7 @@ public void testExpiredObjectRemoval() throws Exception {
4342
// Setup an expired cache object
4443
cacheObject.volume = mockVolume;
4544
when(cacheObject.hasMetadatum("expires")).thenReturn(true);
46-
when(cacheObject.getMetadatumLong("expires", -1L)).thenReturn(1000L); // Expires in 1 second
47-
when(cacheObject.getLastModified()).thenReturn(Instant.now().minusSeconds(10).toEpochMilli());
45+
when(cacheObject.getMetadatumLong("expires", -1L)).thenReturn(Instant.now().minusSeconds(1).toEpochMilli());
4846
when(cacheObject.volume.remove(cacheObject.name)).thenReturn(true);
4947

5048
expiryCheck.check(cacheObject);
@@ -55,7 +53,7 @@ public void testExpiredObjectRemoval() throws Exception {
5553

5654
/**
5755
* Test to ensure that {@link CacheExpiryCheck} does not flag a cache object as expired if the current time has not
58-
* exceeded its `expires` duration since its last modification. This test verifies that no removal action is taken
56+
* exceeded its absolute `expires` time. This test verifies that no removal action is taken
5957
* for such non-expired objects.
6058
*
6159
* @throws Exception to handle any exceptions thrown during the test execution
@@ -66,9 +64,7 @@ public void testNonExpiredObject() throws Exception {
6664
cacheObject.name = "nonExpiredObject";
6765
cacheObject.volname = "testVolume";
6866
when(cacheObject.hasMetadatum("expires")).thenReturn(true);
69-
when(cacheObject.getMetadatumLong("expires", -1L)).thenReturn(14 * 24 * 60 * 60 * 1000L); // 14 days in milliseconds
70-
long lastModified = System.currentTimeMillis() - (7 * 24 * 60 * 60 * 1000L); // 7 days ago, within expiry period
71-
when(cacheObject.getLastModified()).thenReturn(lastModified);
67+
when(cacheObject.getMetadatumLong("expires", -1L)).thenReturn(System.currentTimeMillis() + (7 * 24 * 60 * 60 * 1000L));
7268

7369
// Perform the check
7470
expiryCheck.check(cacheObject);
@@ -106,7 +102,6 @@ public void testNonExpiredObject_NoRemoval() throws Exception {
106102
// Setup a non-expired cache object
107103
when(cacheObject.hasMetadatum("expires")).thenReturn(true);
108104
when(cacheObject.getMetadatumLong("expires", -1L)).thenReturn(System.currentTimeMillis() + 10000L); // Expires in the future
109-
when(cacheObject.getLastModified()).thenReturn(System.currentTimeMillis());
110105

111106
expiryCheck.check(cacheObject);
112107

@@ -115,22 +110,20 @@ public void testNonExpiredObject_NoRemoval() throws Exception {
115110
}
116111

117112
/**
118-
* Tests that an {@link IntegrityException} is thrown when a cache object has the {@code expires} metadata
119-
* but lacks a valid {@code lastModified} time.
113+
* Tests that lastModified is not required because {@code expires} is stored as an absolute timestamp.
120114
*
121115
* @throws Exception to handle any exceptions thrown during the test execution
122116
*/
123117
@Test
124-
public void testObjectWithExpiresButNoLastModified_ThrowsException() throws Exception {
118+
public void testObjectWithExpiresButNoLastModified_UsesAbsoluteExpires() throws Exception {
125119
cacheObject.name = "objectWithNoLastModified";
126120
cacheObject.volname = "testVolume";
127121
when(cacheObject.hasMetadatum("expires")).thenReturn(true);
128-
when(cacheObject.getMetadatumLong("expires", -1L)).thenReturn(1000L); // Expires in 1 second
129-
when(cacheObject.getLastModified()).thenReturn(-1L); // Last modified not available
122+
when(cacheObject.getMetadatumLong("expires", -1L)).thenReturn(System.currentTimeMillis() + 10000L);
130123

131-
assertThrows(IntegrityException.class, () -> {
132-
expiryCheck.check(cacheObject);
133-
});
124+
expiryCheck.check(cacheObject);
125+
126+
verify(mockInventoryDB, never()).removeObject(anyString(), anyString());
134127
}
135128

136129
/**

0 commit comments

Comments
 (0)