Skip to content

Commit baaf2ed

Browse files
authored
Introduce eventual consistent project modification date update (#1239)
Handles optimistic locking exceptions in case of massive concurrent project resource access.
1 parent 2b24daa commit baaf2ed

File tree

3 files changed

+64
-16
lines changed

3 files changed

+64
-16
lines changed

project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/project/ProjectRepositoryImpl.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,11 @@ public Optional<Project> find(ProjectId projectId) {
9999
}
100100

101101
/**
102-
* Updates the lastModified time of the project. Does not check credentials, as the jobrunner
103-
* needs to call it.
104-
* <p>
105-
* <b>Use with care!</b>
106-
* @param projectId the id of the project to update
107-
* @param modifiedOn the Instant object denoting the time the project was updated
102+
* Saves a project to the repository.
103+
* @param project the project to save persistently
104+
* @since 1.11.1
108105
*/
109-
@Override
110-
public void unsafeUpdateLastModified(ProjectId projectId, Instant modifiedOn) {
111-
var project = projectRepo.findById(projectId).orElseThrow(ProjectNotFoundException::new);
112-
project.setLastModified(modifiedOn);
106+
public void save(Project project) {
113107
projectRepo.save(project);
114108
}
115109

project-management/src/main/java/life/qbic/projectmanagement/application/ProjectInformationService.java

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import static java.util.function.Predicate.not;
44

5+
import java.time.Duration;
56
import java.time.Instant;
7+
import java.time.temporal.ChronoUnit;
68
import java.util.ArrayList;
79
import java.util.List;
810
import java.util.Objects;
@@ -23,6 +25,7 @@
2325
import life.qbic.projectmanagement.domain.model.project.ProjectObjective;
2426
import life.qbic.projectmanagement.domain.model.project.ProjectTitle;
2527
import life.qbic.projectmanagement.domain.repository.ProjectRepository;
28+
import org.hibernate.dialect.lock.OptimisticEntityLockException;
2629
import org.springframework.beans.factory.annotation.Autowired;
2730
import org.springframework.security.access.prepost.PreAuthorize;
2831
import org.springframework.security.core.Authentication;
@@ -204,7 +207,54 @@ public void removeFunding(ProjectId projectId) {
204207
projectRepository.update(project);
205208
}
206209

207-
public void updateModifiedDate(ProjectId projectID, Instant modifiedOn) {
208-
projectRepository.unsafeUpdateLastModified(projectID, modifiedOn);
210+
public void updateModifiedDate(ProjectId projectID, Instant modifiedOn) throws ProjectNotFoundException {
211+
// The update might fail due to an optimistic locking exception (concurrent access of other processes)
212+
// To address this, the update is tried until it will eventually not throw the locking exception anymore.
213+
// This approach naively assumes, that the locked resource will be released eventually again by the other process.
214+
var attempt = 1;
215+
var project = projectRepository.find(projectID).orElseThrow(() -> new ProjectNotFoundException("Project with not found: %s".formatted(projectID)));
216+
while(true) {
217+
try {
218+
tryToUpdateModifiedDate(project, modifiedOn);
219+
return;
220+
} catch (OptimisticEntityLockException e) {
221+
log.debug("Optimistic lock exception occurred while updating modified date for project " + projectID);
222+
}
223+
try {
224+
Thread.sleep(calcBase2Duration(attempt));
225+
} catch (InterruptedException e) {
226+
log.error("Interrupted while updating modified date for project " + projectID);
227+
// We try one last time
228+
tryToUpdateModifiedDate(project, modifiedOn);
229+
Thread.currentThread().interrupt();
230+
}
231+
attempt++;
232+
}
233+
}
234+
/*
235+
Will only update the last modified date in case the passed instant is newer then the
236+
the already stored one in the project.
237+
*/
238+
private void tryToUpdateModifiedDate(Project project, Instant modifiedOn) throws OptimisticEntityLockException {
239+
if (project.getLastModified() == null) {
240+
project.setLastModified(modifiedOn);
241+
}
242+
if (project.getLastModified().isAfter(modifiedOn)) {
243+
// Nothing to do if the passed instant is before the already stored one
244+
return;
245+
}
246+
project.setLastModified(modifiedOn);
247+
projectRepository.save(project);
209248
}
249+
250+
private static Duration calcBase2Duration(int attempt) {
251+
return Duration.of((long) Math.pow(2.0, attempt) * 100, ChronoUnit.MILLIS);
252+
}
253+
254+
public class ProjectNotFoundException extends RuntimeException {
255+
public ProjectNotFoundException(String message) {
256+
super(message);
257+
}
258+
}
259+
210260
}

project-management/src/main/java/life/qbic/projectmanagement/domain/repository/ProjectRepository.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package life.qbic.projectmanagement.domain.repository;
22

3-
import java.time.Instant;
43
import java.util.Optional;
54
import life.qbic.projectmanagement.domain.model.project.Project;
65
import life.qbic.projectmanagement.domain.model.project.ProjectCode;
@@ -17,7 +16,7 @@
1716
public interface ProjectRepository {
1817

1918
/**
20-
* Saves a {@link Project} entity permanently.
19+
* Adds a {@link Project} entity permanently and sets access rights according to the logged in user.
2120
*
2221
* @param project the project to store
2322
* @since 1.0.0
@@ -32,6 +31,13 @@ public interface ProjectRepository {
3231
*/
3332
void update(Project project);
3433

34+
/**
35+
* Saves a project to the repository
36+
* @param project the project to save
37+
* @since 1.11.1
38+
*/
39+
void save(Project project);
40+
3541
/**
3642
* Searches for a project that contain the provided project code
3743
*
@@ -43,8 +49,6 @@ public interface ProjectRepository {
4349

4450
Optional<Project> find(ProjectId projectId);
4551

46-
void unsafeUpdateLastModified(ProjectId projectId, Instant modifiedOn);
47-
4852
/**
4953
* Is thrown if a project that should be created already exists, as denoted by the project id
5054
*/

0 commit comments

Comments
 (0)