Pull request #616: RED-6310
Merge in RED/persistence-service from RED-6310 to master * commit '45bd8e600328ce85c8457525b605da2c16dd70c8': RED-6310: Moved code to create user-preferences to a separate class so that the calling code can handle a persistence exception RED-6310: Corrected services so that they use the user id instead of wrongly using the entity id RED-6310: Moved code for multithreaded tests to a helper class RED-6310: Removed not needed code from test RED-6310: Added setup of tenant id to fix the service test RED-6310: Added tests to check if concurrent access to notification-preferences works RED-6310: Refomatted sql query for readability
This commit is contained in:
commit
dcc83474e6
@ -10,6 +10,8 @@ import java.util.stream.Collectors;
|
||||
import javax.transaction.Transactional;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.iqser.red.service.persistence.management.v1.processor.entity.notification.NotificationPreferencesEntity;
|
||||
@ -18,15 +20,20 @@ import com.iqser.red.service.persistence.management.v1.processor.service.persist
|
||||
import com.iqser.red.service.persistence.service.v1.api.model.notification.NotificationPreferences;
|
||||
import com.iqser.red.service.persistence.service.v1.api.model.notification.NotificationType;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class NotificationPreferencesPersistenceService {
|
||||
|
||||
private final NotificationPreferencesRepository notificationPreferencesRepository;
|
||||
NotificationPreferencesRepository notificationPreferencesRepository;
|
||||
|
||||
private final NotificationRepository notificationRepository;
|
||||
NonThreadSafeNotificationPreferencesRepositoryWrapper notificationPreferencesRepositoryWrapper;
|
||||
|
||||
NotificationRepository notificationRepository;
|
||||
|
||||
|
||||
@Transactional
|
||||
@ -60,31 +67,28 @@ public class NotificationPreferencesPersistenceService {
|
||||
@Transactional
|
||||
public void deleteNotificationPreferences(String userId) {
|
||||
|
||||
notificationPreferencesRepository.deleteById(userId);
|
||||
notificationPreferencesRepository.deleteByUserId(userId);
|
||||
}
|
||||
|
||||
|
||||
@Transactional
|
||||
// This method intentionally does not have a @Transactional annotation, since it needs to handle an underlying transaction exception.
|
||||
public NotificationPreferencesEntity getOrCreateNotificationPreferences(String userId) {
|
||||
|
||||
return notificationPreferencesRepository.findById(userId).orElseGet(() -> {
|
||||
|
||||
var notificationPreference = new NotificationPreferencesEntity();
|
||||
notificationPreference.setUserId(userId);
|
||||
notificationPreference.setEmailNotificationsEnabled(false);
|
||||
notificationPreference.setInAppNotificationsEnabled(true);
|
||||
notificationPreference.setInAppNotifications(Arrays.stream(NotificationType.values()).map(Enum::name).collect(Collectors.toList()));
|
||||
return notificationPreferencesRepository.save(notificationPreference);
|
||||
});
|
||||
try {
|
||||
// The method called here will fail if it is called concurrently (more than 1 thread), since it will always try to create
|
||||
// the desired entity. But the exception only means, that the entity has been created by another thread.
|
||||
// In that case we can just fetch the data from the db.
|
||||
return notificationPreferencesRepositoryWrapper.getOrCreateNotificationPreferences(userId);
|
||||
} catch (DataIntegrityViolationException ex) {
|
||||
return notificationPreferencesRepository.getByUserId(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Transactional
|
||||
public void initializePreferencesIfNotExists(String userId) {
|
||||
|
||||
if (!notificationPreferencesRepository.existsByUserId(userId)) {
|
||||
getOrCreateNotificationPreferences(userId);
|
||||
}
|
||||
getOrCreateNotificationPreferences(userId);
|
||||
}
|
||||
|
||||
|
||||
@ -93,4 +97,29 @@ public class NotificationPreferencesPersistenceService {
|
||||
return notificationPreferencesRepository.findAll();
|
||||
}
|
||||
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
private static class NonThreadSafeNotificationPreferencesRepositoryWrapper {
|
||||
|
||||
NotificationPreferencesRepository notificationPreferencesRepository;
|
||||
|
||||
|
||||
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||
public NotificationPreferencesEntity getOrCreateNotificationPreferences(String userId) {
|
||||
|
||||
return notificationPreferencesRepository.findByUserId(userId).orElseGet(() -> {
|
||||
|
||||
var notificationPreference = new NotificationPreferencesEntity();
|
||||
notificationPreference.setUserId(userId);
|
||||
notificationPreference.setEmailNotificationsEnabled(false);
|
||||
notificationPreference.setInAppNotificationsEnabled(true);
|
||||
notificationPreference.setInAppNotifications(Arrays.stream(NotificationType.values()).map(Enum::name).collect(Collectors.toList()));
|
||||
return notificationPreferencesRepository.save(notificationPreference);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,11 +1,19 @@
|
||||
package com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.iqser.red.service.persistence.management.v1.processor.entity.notification.NotificationPreferencesEntity;
|
||||
|
||||
public interface NotificationPreferencesRepository extends JpaRepository<NotificationPreferencesEntity, String> {
|
||||
|
||||
boolean existsByUserId(String userId);
|
||||
Optional<NotificationPreferencesEntity> findByUserId(String userId);
|
||||
|
||||
|
||||
NotificationPreferencesEntity getByUserId(String userId);
|
||||
|
||||
|
||||
void deleteByUserId(String userId);
|
||||
|
||||
}
|
||||
|
||||
@ -15,11 +15,19 @@ public interface NotificationRepository extends JpaRepository<NotificationEntity
|
||||
int hasInAppNotificationForUser(String userId, OffsetDateTime since);
|
||||
|
||||
|
||||
@Query("select n from NotificationEntity n where " + " (exists (select e from NotificationPreferencesEntity e where e.userId = :userId) and n.notificationType in ( select apn from NotificationPreferencesEntity p join p.inAppNotifications apn where p.userId = :userId and p.inAppNotificationsEnabled = true )) " + " and n.softDeleted is null and n.userId = :userId order by n.creationDate desc")
|
||||
@Query("""
|
||||
select n from NotificationEntity n where
|
||||
(exists (select e from NotificationPreferencesEntity e where e.userId = :userId) and n.notificationType in
|
||||
(select apn from NotificationPreferencesEntity p join p.inAppNotifications apn where p.userId = :userId and p.inAppNotificationsEnabled = true))
|
||||
and n.softDeleted is null and n.userId = :userId order by n.creationDate desc""")
|
||||
List<NotificationEntity> findAppNotificationsForUser(String userId);
|
||||
|
||||
|
||||
@Query("select n from NotificationEntity n where " + " (exists (select e from NotificationPreferencesEntity e where e.userId = :userId) and n.notificationType in ( select apn from NotificationPreferencesEntity p join p.inAppNotifications apn where p.userId = :userId and p.inAppNotificationsEnabled = true )) " + " and n.seenDate is null and n.softDeleted is null and n.userId = :userId order by n.creationDate desc ")
|
||||
@Query("""
|
||||
select n from NotificationEntity n where
|
||||
(exists (select e from NotificationPreferencesEntity e where e.userId = :userId) and n.notificationType in
|
||||
(select apn from NotificationPreferencesEntity p join p.inAppNotifications apn where p.userId = :userId and p.inAppNotificationsEnabled = true))
|
||||
and n.seenDate is null and n.softDeleted is null and n.userId = :userId order by n.creationDate desc""")
|
||||
List<NotificationEntity> findUnseenNotificationsForUser(String userId);
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
package com.iqser.red.service.peristence.v1.server.integration.tests;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.iqser.red.service.peristence.v1.server.integration.utils.AbstractPersistenceServerServiceTest;
|
||||
import com.iqser.red.service.peristence.v1.server.integration.utils.MultithreadedTestRunner;
|
||||
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.NotificationPreferencesPersistenceService;
|
||||
import com.iqser.red.service.persistence.management.v1.processor.utils.multitenancy.TenantContext;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
public class NotificationPreferencesServiceTest extends AbstractPersistenceServerServiceTest {
|
||||
|
||||
@Autowired
|
||||
NotificationPreferencesPersistenceService notificationPreferencesPersistenceService;
|
||||
|
||||
final MultithreadedTestRunner multithreadedTestRunner = new MultithreadedTestRunner(2, 1000);
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
|
||||
TenantContext.setTenantId("redaction");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
public void testNotificationPreferencesConcurrent() {
|
||||
|
||||
final String userId = "1";
|
||||
Runnable test = () -> notificationPreferencesPersistenceService.getOrCreateNotificationPreferences(userId);
|
||||
Runnable afterTest = () -> notificationPreferencesPersistenceService.deleteNotificationPreferences(userId);
|
||||
var exceptions = multithreadedTestRunner.runMutlithreadedCollectingExceptions(test, afterTest);
|
||||
|
||||
for (Exception ex : exceptions) {
|
||||
log.error("Exception during notification creation", ex);
|
||||
}
|
||||
|
||||
assertThat(exceptions).isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
@ -12,11 +12,17 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.iqser.red.service.peristence.v1.server.integration.client.NotificationClient;
|
||||
import com.iqser.red.service.peristence.v1.server.integration.client.NotificationPreferencesClient;
|
||||
import com.iqser.red.service.peristence.v1.server.integration.utils.AbstractPersistenceServerServiceTest;
|
||||
import com.iqser.red.service.peristence.v1.server.integration.utils.MultithreadedTestRunner;
|
||||
import com.iqser.red.service.persistence.service.v1.api.model.audit.AddNotificationRequest;
|
||||
import com.iqser.red.service.persistence.service.v1.api.model.common.JSONPrimitive;
|
||||
import com.iqser.red.service.persistence.service.v1.api.model.notification.Notification;
|
||||
import com.iqser.red.service.persistence.service.v1.api.model.notification.NotificationType;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
|
||||
@Slf4j
|
||||
public class NotificationTest extends AbstractPersistenceServerServiceTest {
|
||||
|
||||
@Autowired
|
||||
@ -25,6 +31,8 @@ public class NotificationTest extends AbstractPersistenceServerServiceTest {
|
||||
@Autowired
|
||||
private NotificationPreferencesClient notificationPreferencesClient;
|
||||
|
||||
private final MultithreadedTestRunner multithreadedTestRunner = new MultithreadedTestRunner(2, 1000);
|
||||
|
||||
|
||||
@Test
|
||||
public void testNotificationPreferences() {
|
||||
@ -106,4 +114,38 @@ public class NotificationTest extends AbstractPersistenceServerServiceTest {
|
||||
return currentNotifications.iterator().next();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
public void testNotificationPreferencesConcurrent() {
|
||||
|
||||
final String userId = "1";
|
||||
Runnable test = () -> notificationPreferencesClient.getNotificationPreferences(userId);
|
||||
Runnable afterTest = () -> notificationPreferencesClient.deleteNotificationPreferences(userId);
|
||||
var exceptions = multithreadedTestRunner.runMutlithreadedCollectingExceptions(test, afterTest);
|
||||
|
||||
for (Exception ex : exceptions) {
|
||||
log.error("Exception during notification creation", ex);
|
||||
}
|
||||
|
||||
assertThat(exceptions).isEmpty();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
public void testNotificationsConcurrent() {
|
||||
|
||||
final String userId = "1";
|
||||
Runnable test = () -> notificationClient.getNotifications(userId, false);
|
||||
Runnable afterTest = () -> notificationPreferencesClient.deleteNotificationPreferences(userId);
|
||||
var exceptions = multithreadedTestRunner.runMutlithreadedCollectingExceptions(test, afterTest);
|
||||
|
||||
for (Exception ex : exceptions) {
|
||||
log.error("Exception during notification creation", ex);
|
||||
}
|
||||
|
||||
assertThat(exceptions).isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
package com.iqser.red.service.peristence.v1.server.integration.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class MultithreadedTestRunner {
|
||||
|
||||
int numberOfThreads;
|
||||
int numberOfExecutions;
|
||||
|
||||
|
||||
public List<Exception> runMutlithreadedCollectingExceptions(boolean stopOnFirstRunWithExceptions, Runnable test, Runnable afterTest) {
|
||||
|
||||
List<Exception> allExceptions = new ArrayList<>();
|
||||
|
||||
for (int execution = 1; execution <= numberOfExecutions; execution++) {
|
||||
var threads = new ArrayList<Thread>(numberOfThreads);
|
||||
var exceptions = Collections.synchronizedList(new ArrayList<Exception>());
|
||||
|
||||
for (int threadNumber = 1; threadNumber <= numberOfThreads; threadNumber++) {
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
test.run();
|
||||
} catch (Exception e) {
|
||||
exceptions.add(e);
|
||||
}
|
||||
});
|
||||
|
||||
threads.add(t);
|
||||
}
|
||||
|
||||
for (Thread t : threads) {
|
||||
t.start();
|
||||
}
|
||||
|
||||
for (Thread t : threads) {
|
||||
try {
|
||||
t.join();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
afterTest.run();
|
||||
|
||||
if (stopOnFirstRunWithExceptions) {
|
||||
if (!exceptions.isEmpty()) {
|
||||
return exceptions;
|
||||
}
|
||||
} else {
|
||||
allExceptions.addAll(exceptions);
|
||||
}
|
||||
}
|
||||
|
||||
return allExceptions;
|
||||
}
|
||||
|
||||
|
||||
public List<Exception> runMutlithreadedCollectingExceptions(Runnable test, Runnable afterTest) {
|
||||
|
||||
return runMutlithreadedCollectingExceptions(true, test, afterTest);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user