|
2 | 2 |
|
3 | 3 | import static java.util.function.Predicate.not; |
4 | 4 |
|
| 5 | +import java.time.Duration; |
5 | 6 | import java.time.Instant; |
| 7 | +import java.time.temporal.ChronoUnit; |
6 | 8 | import java.util.ArrayList; |
7 | 9 | import java.util.List; |
8 | 10 | import java.util.Objects; |
|
23 | 25 | import life.qbic.projectmanagement.domain.model.project.ProjectObjective; |
24 | 26 | import life.qbic.projectmanagement.domain.model.project.ProjectTitle; |
25 | 27 | import life.qbic.projectmanagement.domain.repository.ProjectRepository; |
| 28 | +import org.hibernate.dialect.lock.OptimisticEntityLockException; |
26 | 29 | import org.springframework.beans.factory.annotation.Autowired; |
27 | 30 | import org.springframework.security.access.prepost.PreAuthorize; |
28 | 31 | import org.springframework.security.core.Authentication; |
@@ -204,7 +207,54 @@ public void removeFunding(ProjectId projectId) { |
204 | 207 | projectRepository.update(project); |
205 | 208 | } |
206 | 209 |
|
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); |
209 | 248 | } |
| 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 | + |
210 | 260 | } |
0 commit comments