diff --git a/core/src/main/java/se/su/dsv/scipro/mail/PrintingMailer.java b/core/src/main/java/se/su/dsv/scipro/mail/PrintingMailer.java index a5c7f597be..179bffc02c 100644 --- a/core/src/main/java/se/su/dsv/scipro/mail/PrintingMailer.java +++ b/core/src/main/java/se/su/dsv/scipro/mail/PrintingMailer.java @@ -6,7 +6,7 @@ import se.su.dsv.scipro.file.FileDescription; import java.util.Arrays; import java.util.UUID; -class PrintingMailer implements Mailer { +public class PrintingMailer implements Mailer { @Override public MailResult mail(final String fromName, final String fromEmail, final String[] recipients, final String subject, final String message, final FileDescription attachment) { return new MailResult() { diff --git a/core/src/main/java/se/su/dsv/scipro/match/AllowAllIdeaCreationJudge.java b/core/src/main/java/se/su/dsv/scipro/match/AllowAllIdeaCreationJudge.java index f5e0035fa5..6a91588ff1 100644 --- a/core/src/main/java/se/su/dsv/scipro/match/AllowAllIdeaCreationJudge.java +++ b/core/src/main/java/se/su/dsv/scipro/match/AllowAllIdeaCreationJudge.java @@ -1,6 +1,6 @@ package se.su.dsv.scipro.match; -class AllowAllIdeaCreationJudge implements IdeaCreationJudge { +public class AllowAllIdeaCreationJudge implements IdeaCreationJudge { @Override public Decision ruling(Idea idea) { return Decision.allowed(); diff --git a/core/src/main/java/se/su/dsv/scipro/workerthreads/AbstractWorker.java b/core/src/main/java/se/su/dsv/scipro/workerthreads/AbstractWorker.java index 352c674419..5c721b2ffe 100755 --- a/core/src/main/java/se/su/dsv/scipro/workerthreads/AbstractWorker.java +++ b/core/src/main/java/se/su/dsv/scipro/workerthreads/AbstractWorker.java @@ -22,8 +22,7 @@ public abstract class AbstractWorker implements Worker { private WorkerData wd = null; private Date lastSuccessfulRun = new Date(0); private boolean successfulWorker = true; - private Provider<EntityManager> emProvider; - private EntityTransaction transaction; + private WorkerTransactionManager txManager; /** * Subclasses must be annotated with @Component or similar annotation in order for autowiring of dependencies to work @@ -37,16 +36,13 @@ public abstract class AbstractWorker implements Worker { } @Inject - public void setTxManager(Provider<EntityManager> em) { - this.emProvider = em; + public void setTxManager(WorkerTransactionManager txManager) { + this.txManager = txManager; } @Override public void run() { - EntityManager em = emProvider.get(); try { - transaction = em.getTransaction(); - wd = workerDataService.getWorkerDataByName(getName()); if (wd != null) { lastSuccessfulRun = wd.getLastSuccessfulRun(); @@ -72,9 +68,7 @@ public abstract class AbstractWorker implements Worker { setSuccessfulWorker(false); } finally { - if (transaction.isActive() && em.isOpen()) { - transaction.rollback(); - } + txManager.rollbackIfActive(); } // in case a job crashes or clears the cache (so the entity is detached) @@ -87,9 +81,7 @@ public abstract class AbstractWorker implements Worker { saveWorkerData(); } finally { - if (transaction.isActive() && em.isOpen()) { - transaction.rollback(); - } + txManager.rollbackIfActive(); } } @@ -139,15 +131,15 @@ public abstract class AbstractWorker implements Worker { protected abstract void doWork(); protected void beginTransaction() { - transaction.begin(); + txManager.begin(); } protected void commitTransaction() { - transaction.commit(); + txManager.commit(); } protected void rollbackTransaction() { - transaction.rollback(); + txManager.rollback(); } } diff --git a/core/src/main/java/se/su/dsv/scipro/workerthreads/WorkerModule.java b/core/src/main/java/se/su/dsv/scipro/workerthreads/WorkerModule.java index 370d486d83..db17d05aec 100644 --- a/core/src/main/java/se/su/dsv/scipro/workerthreads/WorkerModule.java +++ b/core/src/main/java/se/su/dsv/scipro/workerthreads/WorkerModule.java @@ -15,9 +15,9 @@ public class WorkerModule extends AbstractModule { @Override protected void configure() { - Multibinder.newSetBinder(binder(), Lifecycle.class).addBinding().to(SchedulerImpl.class); + //Multibinder.newSetBinder(binder(), Lifecycle.class).addBinding().to(SchedulerImpl.class); bind(ScheduledExecutorService.class).toInstance(Executors.newScheduledThreadPool(NUMBER_OF_WORKER_THREADS)); - bind(Scheduler.class).to(SchedulerImpl.class).in(Scopes.SINGLETON); + //bind(Scheduler.class).to(SchedulerImpl.class).in(Scopes.SINGLETON); bind(TemporaryWorkerScheduler.class).asEagerSingleton(); bind(WorkerDataService.class).to(WorkerDataServiceImpl.class); bind(ThesisUploadDeadlineWorker.class); diff --git a/core/src/main/java/se/su/dsv/scipro/workerthreads/WorkerTransactionManager.java b/core/src/main/java/se/su/dsv/scipro/workerthreads/WorkerTransactionManager.java new file mode 100644 index 0000000000..1ff2b458d4 --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/workerthreads/WorkerTransactionManager.java @@ -0,0 +1,20 @@ +package se.su.dsv.scipro.workerthreads; + +public interface WorkerTransactionManager { + void rollbackIfActive(); + + /** + * @throws IllegalStateException if a transaction is already active + */ + void begin(); + + /** + * @throws IllegalStateException if a transaction is not active + */ + void commit(); + + /** + * @throws IllegalStateException if a transaction is not active + */ + void rollback(); +} diff --git a/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/GradingCompletedMilestoneActivatorTest.java b/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/GradingCompletedMilestoneActivatorTest.java index 9c766ce817..f51c23592a 100644 --- a/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/GradingCompletedMilestoneActivatorTest.java +++ b/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/GradingCompletedMilestoneActivatorTest.java @@ -1,10 +1,6 @@ package se.su.dsv.scipro.integration.daisy.workers; import com.google.common.eventbus.EventBus; -import com.google.inject.persist.UnitOfWork; -import com.google.inject.util.Providers; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityTransaction; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,6 +19,7 @@ import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.UserService; import se.su.dsv.scipro.workerthreads.WorkerData; import se.su.dsv.scipro.workerthreads.WorkerDataService; +import se.su.dsv.scipro.workerthreads.WorkerTransactionManager; import java.util.*; @@ -47,21 +44,16 @@ public class GradingCompletedMilestoneActivatorTest { @Mock private EventBus eventBus; @Mock - private UnitOfWork unitOfWork; - @Mock - private EntityManager entityManager; - @Mock - private EntityTransaction entityTransaction; + private WorkerTransactionManager workerTransactionManager; @Mock private WorkerDataService workerDataService; @BeforeEach public void setup() { gradingCompletedMilestoneActivator = new GradingCompletedMilestoneActivator(projectService, daisyAPI, eventBus, userService); - gradingCompletedMilestoneActivator.setTxManager(Providers.of(entityManager)); + gradingCompletedMilestoneActivator.setTxManager(workerTransactionManager); gradingCompletedMilestoneActivator.setWorkerDataService(workerDataService); when(workerDataService.save(any(WorkerData.class))).thenReturn(new WorkerData()); - when(entityManager.getTransaction()).thenReturn(entityTransaction); project.setId(3493L); project.setIdentifier(234); diff --git a/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/ProjectExporterTest.java b/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/ProjectExporterTest.java index d2f53aca29..dfd523ad4f 100644 --- a/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/ProjectExporterTest.java +++ b/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/ProjectExporterTest.java @@ -1,10 +1,6 @@ package se.su.dsv.scipro.integration.daisy.workers; -import com.google.inject.persist.UnitOfWork; -import com.google.inject.util.Providers; import com.querydsl.core.types.Predicate; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityTransaction; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,6 +22,7 @@ import se.su.dsv.scipro.system.Unit; import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.workerthreads.WorkerData; import se.su.dsv.scipro.workerthreads.WorkerDataService; +import se.su.dsv.scipro.workerthreads.WorkerTransactionManager; import java.time.LocalDate; import java.util.*; @@ -45,10 +42,8 @@ public class ProjectExporterTest { private @Mock ExporterFacade exporterFacade; private @Mock DaisyAPI daisyAPI; private @Mock ExternalExporter externalExporter; - private @Mock EntityManager entityManager; - private @Mock EntityTransaction entityTransaction; + private @Mock WorkerTransactionManager workerTransactionManager; private @Mock WorkerDataService workerDataService; - private @Mock UnitOfWork unitOfWork; private @Mock FinalSeminarService finalSeminarService; private @Mock FinalThesisService finalThesisService; @@ -61,10 +56,9 @@ public class ProjectExporterTest { @BeforeEach public void setUp() { projectExporter = new ProjectExporter(projectRepo, exporterFacade, daisyAPI, externalExporter, finalSeminarService, finalThesisService); - projectExporter.setTxManager(Providers.of(entityManager)); + projectExporter.setTxManager(workerTransactionManager); projectExporter.setWorkerDataService(workerDataService); when(workerDataService.save(any(WorkerData.class))).thenReturn(new WorkerData()); - when(entityManager.getTransaction()).thenReturn(entityTransaction); Unit unit = new Unit(); unit.setIdentifier(239478); diff --git a/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/ProjectFinalizerTest.java b/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/ProjectFinalizerTest.java index 52b7094897..048d2165e7 100644 --- a/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/ProjectFinalizerTest.java +++ b/daisy-integration/src/test/java/se/su/dsv/scipro/integration/daisy/workers/ProjectFinalizerTest.java @@ -1,7 +1,5 @@ package se.su.dsv.scipro.integration.daisy.workers; -import com.google.inject.persist.UnitOfWork; -import com.google.inject.util.Providers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,8 +16,8 @@ import se.su.dsv.scipro.project.QProject; import se.su.dsv.scipro.workerthreads.WorkerData; import se.su.dsv.scipro.workerthreads.WorkerDataService; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityTransaction; +import se.su.dsv.scipro.workerthreads.WorkerTransactionManager; + import java.util.Collections; import java.util.List; import java.util.Optional; @@ -38,9 +36,7 @@ public class ProjectFinalizerTest { private @Mock DaisyAPI daisyAPI; private @Mock ProjectService projectService; - private @Mock UnitOfWork unitOfWork; - private @Mock EntityManager entityManager; - private @Mock EntityTransaction entityTransaction; + private @Mock WorkerTransactionManager workerTransactionManager; private @Mock WorkerDataService workerDataService; private @Mock ThesisApprovedHistoryService thesisApprovedHistoryService; @@ -48,10 +44,9 @@ public class ProjectFinalizerTest { @BeforeEach public void setup() { projectFinalizer = new ProjectFinalizer(projectService, daisyAPI, thesisApprovedHistoryService); - projectFinalizer.setTxManager(Providers.of(entityManager)); + projectFinalizer.setTxManager(workerTransactionManager); projectFinalizer.setWorkerDataService(workerDataService); when(workerDataService.save(any(WorkerData.class))).thenReturn(new WorkerData()); - when(entityManager.getTransaction()).thenReturn(entityTransaction); project.setId(3493L); project.setIdentifier(234); diff --git a/war/src/main/java/se/su/dsv/scipro/war/CoreConfig.java b/war/src/main/java/se/su/dsv/scipro/war/CoreConfig.java index 360419ac35..12860dcade 100644 --- a/war/src/main/java/se/su/dsv/scipro/war/CoreConfig.java +++ b/war/src/main/java/se/su/dsv/scipro/war/CoreConfig.java @@ -21,8 +21,6 @@ import se.su.dsv.scipro.sukat.LDAP; import se.su.dsv.scipro.sukat.Sukat; import java.time.Clock; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; @Configuration @ComponentScan( @@ -34,13 +32,6 @@ import java.util.concurrent.ScheduledExecutorService; ".*GradingServiceImpl" })) public class CoreConfig { - private static final int NUMBER_OF_WORKER_THREADS = 4; - - @Bean - public ScheduledExecutorService scheduledExecutorService() { - return Executors.newScheduledThreadPool(NUMBER_OF_WORKER_THREADS); - } - @Bean public EventBus eventBus() { return new EventBus(); diff --git a/war/src/main/java/se/su/dsv/scipro/war/MailConfig.java b/war/src/main/java/se/su/dsv/scipro/war/MailConfig.java new file mode 100644 index 0000000000..35b7a0ff64 --- /dev/null +++ b/war/src/main/java/se/su/dsv/scipro/war/MailConfig.java @@ -0,0 +1,45 @@ +package se.su.dsv.scipro.war; + +import jakarta.mail.Session; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import se.su.dsv.scipro.data.facade.MailFacade; +import se.su.dsv.scipro.file.FileService; +import se.su.dsv.scipro.generalsystemsettings.GeneralSystemSettingsService; +import se.su.dsv.scipro.mail.Mail; +import se.su.dsv.scipro.mail.Mailer; +import se.su.dsv.scipro.mail.PrintingMailer; +import se.su.dsv.scipro.mail.RedirectingMailer; +import se.su.dsv.scipro.profiles.CurrentProfile; + +import java.util.Properties; + +@Configuration +public class MailConfig { + @Bean + public MailFacade mailFacade() { + return new MailFacade(); + } + + @Bean + public Mailer mailer( + CurrentProfile currentProfile, + Session session, + FileService fileDescriptionService) + { + return switch (currentProfile.getCurrentProfile()) { + case DEV -> new PrintingMailer(); + case PROD -> new Mail(session, fileDescriptionService); + case TEST -> new RedirectingMailer(session, "scipro-mailtest@dsv.su.se", fileDescriptionService); + }; + } + + @Bean + public Session session(GeneralSystemSettingsService generalSystemSettings) { + String smtpHost = generalSystemSettings.getGeneralSystemSettingsInstance().getSmtpServer(); + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", smtpHost); + properties.setProperty("mail.smtp.sendpartial", Boolean.toString(true)); + return Session.getDefaultInstance(properties); + } +} diff --git a/war/src/main/java/se/su/dsv/scipro/war/Main.java b/war/src/main/java/se/su/dsv/scipro/war/Main.java index 3f629e21ab..ca339abb8b 100644 --- a/war/src/main/java/se/su/dsv/scipro/war/Main.java +++ b/war/src/main/java/se/su/dsv/scipro/war/Main.java @@ -37,7 +37,7 @@ import java.util.Set; @SpringBootApplication @EntityScan("se.su.dsv.scipro") -@Import({CoreConfig.class, ApiConfig.class}) +@Import({CoreConfig.class, ApiConfig.class, WorkerConfig.class, MailConfig.class}) public class Main extends SpringBootServletInitializer implements ServletContainerInitializer { @Override public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException { diff --git a/war/src/main/java/se/su/dsv/scipro/war/SpringManagedWorkerTransactions.java b/war/src/main/java/se/su/dsv/scipro/war/SpringManagedWorkerTransactions.java new file mode 100644 index 0000000000..ae66a50e36 --- /dev/null +++ b/war/src/main/java/se/su/dsv/scipro/war/SpringManagedWorkerTransactions.java @@ -0,0 +1,50 @@ +package se.su.dsv.scipro.war; + +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import se.su.dsv.scipro.workerthreads.WorkerTransactionManager; + +public class SpringManagedWorkerTransactions implements WorkerTransactionManager { + private final PlatformTransactionManager platformTransactionManager; + + private TransactionStatus activeTransaction; + + public SpringManagedWorkerTransactions(PlatformTransactionManager platformTransactionManager) { + this.platformTransactionManager = platformTransactionManager; + } + + @Override + public void rollbackIfActive() { + if (this.activeTransaction != null) { + platformTransactionManager.rollback(this.activeTransaction); + this.activeTransaction = null; + } + } + + @Override + public void begin() { + if (this.activeTransaction != null) { + throw new IllegalStateException("A transaction is already active"); + } + this.activeTransaction = platformTransactionManager.getTransaction(TransactionDefinition.withDefaults()); + } + + @Override + public void commit() { + if (this.activeTransaction == null) { + throw new IllegalStateException("A transaction is not active"); + } + platformTransactionManager.commit(this.activeTransaction); + this.activeTransaction = null; + } + + @Override + public void rollback() { + if (this.activeTransaction == null) { + throw new IllegalStateException("A transaction is not active"); + } + platformTransactionManager.rollback(this.activeTransaction); + this.activeTransaction = null; + } +} diff --git a/war/src/main/java/se/su/dsv/scipro/war/WorkerConfig.java b/war/src/main/java/se/su/dsv/scipro/war/WorkerConfig.java new file mode 100644 index 0000000000..400fc754a7 --- /dev/null +++ b/war/src/main/java/se/su/dsv/scipro/war/WorkerConfig.java @@ -0,0 +1,94 @@ +package se.su.dsv.scipro.war; + +import jakarta.inject.Provider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Scope; +import org.springframework.transaction.PlatformTransactionManager; +import se.su.dsv.scipro.file.FileService; +import se.su.dsv.scipro.mail.MailEventWorker; +import se.su.dsv.scipro.match.AllowAllIdeaCreationJudge; +import se.su.dsv.scipro.match.IdeaCreationJudge; +import se.su.dsv.scipro.plagiarism.PlagiarismRequestRepository; +import se.su.dsv.scipro.plagiarism.PlagiarismSubmitter; +import se.su.dsv.scipro.plagiarism.urkund.StatusPollingWorker; +import se.su.dsv.scipro.plagiarism.urkund.UrkundService; +import se.su.dsv.scipro.plagiarism.urkund.UrkundSettings; +import se.su.dsv.scipro.plagiarism.urkund.UrkundSettingsRepository; +import se.su.dsv.scipro.projectpartner.RemoveFulfilledPartnerAdsWorker; +import se.su.dsv.scipro.reviewing.ReviewerDecisionReminderWorker; +import se.su.dsv.scipro.workerthreads.GradeFinalSeminarParticipantReminderWorker; +import se.su.dsv.scipro.workerthreads.IdeaExportWorker; +import se.su.dsv.scipro.workerthreads.ManualMatchRemindWorker; +import se.su.dsv.scipro.workerthreads.NotificationCompilationWorker; +import se.su.dsv.scipro.workerthreads.Scheduler; +import se.su.dsv.scipro.workerthreads.TemporaryWorkerScheduler; +import se.su.dsv.scipro.workerthreads.ThesisUploadDeadlineWorker; +import se.su.dsv.scipro.workerthreads.ThesisUploadReminderWorker; +import se.su.dsv.scipro.workerthreads.WorkerTransactionManager; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@Configuration +@ComponentScan( + basePackages = "se.su.dsv.scipro", + includeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Worker$"), + excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*[Dd]aisy.*") +) +public class WorkerConfig { + private static final int NUMBER_OF_WORKER_THREADS = 4; + + @Bean + public ScheduledExecutorService scheduledExecutorService() { + return Executors.newScheduledThreadPool(NUMBER_OF_WORKER_THREADS); + } + + @Bean + public TemporaryWorkerScheduler temporaryWorkerScheduler( + Scheduler scheduler, + Provider<MailEventWorker> mailEventWorker, + Provider<NotificationCompilationWorker> notificationCompilationWorker, + Provider<IdeaExportWorker> ideaExportWorker, + Provider<ThesisUploadReminderWorker> thesisUploadReminderWorker, + Provider<ThesisUploadDeadlineWorker> thesisUploadDeadlineWorker, + Provider<ManualMatchRemindWorker> manualMatchRemindWorkerProvider, + Provider<ReviewerDecisionReminderWorker> reviewerDecisionReminderWorker, + Provider<PlagiarismSubmitter> plagiarismSubmitter, + Provider<StatusPollingWorker> urkundPoller, + Provider<RemoveFulfilledPartnerAdsWorker> removeFulfilledPartnerAds, + Provider<GradeFinalSeminarParticipantReminderWorker> gradeFinalSeminarParticipantReminderWorkerProvider) + { + return new TemporaryWorkerScheduler(scheduler, mailEventWorker, notificationCompilationWorker, ideaExportWorker, thesisUploadReminderWorker, thesisUploadDeadlineWorker, manualMatchRemindWorkerProvider, reviewerDecisionReminderWorker, plagiarismSubmitter, urkundPoller, removeFulfilledPartnerAds, gradeFinalSeminarParticipantReminderWorkerProvider); + } + + @Bean + @ConditionalOnMissingBean(IdeaCreationJudge.class) + public IdeaCreationJudge ideaCreationJudge() { + return new AllowAllIdeaCreationJudge(); + } + + @Bean + public PlagiarismSubmitter plagiarismSubmitter( + Provider<UrkundSettings> urkundSettings, + PlagiarismRequestRepository plagiarismRequestRepository, + UrkundService urkundService, + FileService fileService) + { + return new PlagiarismSubmitter(urkundSettings, plagiarismRequestRepository, urkundService, fileService); + } + + @Bean + public UrkundSettings urkundSettings(UrkundSettingsRepository urkundSettingsRepository) { + return urkundSettingsRepository.getSettings(); + } + + @Bean + @Scope("prototype") + public WorkerTransactionManager workerTransactionManager(PlatformTransactionManager platformTransactionManager) { + return new SpringManagedWorkerTransactions(platformTransactionManager); + } +} diff --git a/core/src/main/java/se/su/dsv/scipro/workerthreads/SchedulerImpl.java b/war/src/main/java/se/su/dsv/scipro/workerthreads/SchedulerImpl.java similarity index 59% rename from core/src/main/java/se/su/dsv/scipro/workerthreads/SchedulerImpl.java rename to war/src/main/java/se/su/dsv/scipro/workerthreads/SchedulerImpl.java index d8ecf35252..8cd979ede0 100755 --- a/core/src/main/java/se/su/dsv/scipro/workerthreads/SchedulerImpl.java +++ b/war/src/main/java/se/su/dsv/scipro/workerthreads/SchedulerImpl.java @@ -1,12 +1,24 @@ package se.su.dsv.scipro.workerthreads; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter; +import org.springframework.transaction.support.TransactionSynchronizationManager; import se.su.dsv.scipro.system.Lifecycle; import jakarta.inject.Inject; import jakarta.inject.Provider; import jakarta.inject.Singleton; + +import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -19,13 +31,15 @@ public class SchedulerImpl implements Lifecycle, Scheduler { private static final Logger LOGGER = LoggerFactory.getLogger(SchedulerImpl.class); private final ScheduledExecutorService scheduledExecutorService; + private final EntityManagerFactory emf; private final Set<Task> tasks = new TreeSet<>(new Task.ByDescriptionComparator()); private final Set<Task> runningWorkers = Collections.synchronizedSet(new HashSet<>()); @Inject - public SchedulerImpl(ScheduledExecutorService scheduledExecutorService) { + public SchedulerImpl(ScheduledExecutorService scheduledExecutorService, EntityManagerFactory emf) { this.scheduledExecutorService = scheduledExecutorService; + this.emf = emf; } @Override @@ -43,6 +57,14 @@ public class SchedulerImpl implements Lifecycle, Scheduler { private Runnable tracked(final Task task) { return () -> { try { + // Since we're not in a web request when running a scheduled job, Spring's OpenEntityManagerInView + // filter is not active. Therefore, we need to manually bind a new EntityManager to the current thread + // to be shared by all DAOs. If we do not, we will get problems with detached entity passed to persist + // since every read and write from the database will be in a new EntityManager. + EntityManager em = emf.createEntityManager(); + EntityManagerHolder emHolder = new EntityManagerHolder(em); + TransactionSynchronizationManager.bindResource(emf, emHolder); + runningWorkers.add(task); task.execute(); } @@ -51,6 +73,11 @@ public class SchedulerImpl implements Lifecycle, Scheduler { } finally { runningWorkers.remove(task); + + // Clean up the shared EntityManager + EntityManagerHolder emHolder = (EntityManagerHolder) + TransactionSynchronizationManager.unbindResource(emf); + EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); } }; } diff --git a/core/src/main/java/se/su/dsv/scipro/workerthreads/TaskScheduling.java b/war/src/main/java/se/su/dsv/scipro/workerthreads/TaskScheduling.java similarity index 100% rename from core/src/main/java/se/su/dsv/scipro/workerthreads/TaskScheduling.java rename to war/src/main/java/se/su/dsv/scipro/workerthreads/TaskScheduling.java diff --git a/core/src/test/java/se/su/dsv/scipro/workerthreads/SchedulerImplTest.java b/war/src/test/java/se/su/dsv/scipro/workerthreads/SchedulerImplTest.java similarity index 100% rename from core/src/test/java/se/su/dsv/scipro/workerthreads/SchedulerImplTest.java rename to war/src/test/java/se/su/dsv/scipro/workerthreads/SchedulerImplTest.java