diff --git a/build.gradle b/build.gradle index 2a634187e292..8821e224923d 100644 --- a/build.gradle +++ b/build.gradle @@ -217,7 +217,7 @@ dependencies { implementation "io.sentry:sentry-logback:${sentry_version}" implementation "io.sentry:sentry-spring-boot-starter-jakarta:${sentry_version}" - // NOTE: the following six dependencies use the newer versions explicitly to avoid other dependencies to use older versions + // NOTE: the following dependencies use the newer versions explicitly to avoid other dependencies to use older versions implementation "ch.qos.logback:logback-classic:${logback_version}" implementation "ch.qos.logback:logback-core:${logback_version}" diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/NotificationScheduleService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/NotificationScheduleService.java index fbd312a44bdd..560c5c7acfe7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/NotificationScheduleService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/NotificationScheduleService.java @@ -114,13 +114,13 @@ public void updateSchedulingForReleasedExercises(Exercise exercise) { private void scheduleNotificationForReleasedExercise(Exercise exercise) { try { checkSecurityUtils(); - scheduleService.scheduleTask(exercise, ExerciseLifecycle.RELEASE, () -> { + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.RELEASE, () -> { checkSecurityUtils(); Exercise foundCurrentVersionOfScheduledExercise = exerciseRepository.findByIdElseThrow(exercise.getId()); if (checkIfTimeIsCorrectForScheduledTask(foundCurrentVersionOfScheduledExercise.getReleaseDate())) { groupNotificationService.notifyAllGroupsAboutReleasedExercise(foundCurrentVersionOfScheduledExercise); } - }); + }, "notify about release exercise"); log.debug("Scheduled notify about started exercise after due date for exercise '{}' (#{}) for {}.", exercise.getTitle(), exercise.getId(), exercise.getReleaseDate()); } catch (Exception exception) { @@ -155,13 +155,13 @@ public void updateSchedulingForAssessedExercisesSubmissions(Exercise exercise) { private void scheduleNotificationForAssessedExercisesSubmissions(Exercise exercise) { try { checkSecurityUtils(); - scheduleService.scheduleTask(exercise, ExerciseLifecycle.ASSESSMENT_DUE, () -> { + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.ASSESSMENT_DUE, () -> { checkSecurityUtils(); Exercise foundCurrentVersionOfScheduledExercise = exerciseRepository.findByIdElseThrow(exercise.getId()); if (checkIfTimeIsCorrectForScheduledTask(foundCurrentVersionOfScheduledExercise.getAssessmentDueDate())) { singleUserNotificationService.notifyUsersAboutAssessedExerciseSubmission(foundCurrentVersionOfScheduledExercise); } - }); + }, "notify about assessed exercise submission"); log.debug("Scheduled notify about assessed exercise submission after assessment due date for exercise '{}' (#{}) at {}.", exercise.getTitle(), exercise.getId(), exercise.getAssessmentDueDate()); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/TaskSchedulingConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/TaskSchedulingConfiguration.java index e5f3b318b892..bb0fdac8fbf2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/TaskSchedulingConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/TaskSchedulingConfiguration.java @@ -18,11 +18,18 @@ public class TaskSchedulingConfiguration { private static final Logger log = LoggerFactory.getLogger(TaskSchedulingConfiguration.class); + /** + * Create a Task Scheduler with virtual threads and a pool size of 4. + * + * @return the Task Scheduler bean that can be injected into any service to schedule tasks + */ @Bean(name = "taskScheduler") public TaskScheduler taskScheduler() { log.debug("Creating Task Scheduler "); var scheduler = new ThreadPoolTaskScheduler(); + scheduler.setVirtualThreads(true); scheduler.setPoolSize(4); + scheduler.initialize(); return scheduler; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/ScheduleService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/ScheduleService.java index 69ad1cfd5181..f8baea63aea0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/ScheduleService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/ScheduleService.java @@ -1,19 +1,36 @@ package de.tum.cit.aet.artemis.core.service; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import static java.time.ZoneId.systemDefault; +import java.time.Duration; +import java.time.Instant; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.commons.lang3.tuple.Triple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.core.util.Tuple; @@ -36,34 +53,153 @@ public class ScheduleService { private final ParticipationLifecycleService participationLifecycleService; - private final ConcurrentMap, Set>> scheduledExerciseTasks = new ConcurrentHashMap<>(); + record ExerciseLifecycleKey(Long exerciseId, ExerciseLifecycle lifecycle) { + } + + record ParticipationLifecycleKey(Long exerciseId, Long participationId, ParticipationLifecycle lifecycle) { + } + + record ScheduledTaskName(ScheduledFuture future, String name) { + } + + public record ScheduledExerciseEvent(Long exerciseId, ExerciseLifecycle lifecycle, String name, ZonedDateTime scheduledTime, Future.State state) { + } + + private final ConcurrentMap> scheduledExerciseTasks = new ConcurrentHashMap<>(); // triple of exercise id, participation id, and lifecycle - private final ConcurrentMap, Set>> scheduledParticipationTasks = new ConcurrentHashMap<>(); + private final ConcurrentMap> scheduledParticipationTasks = new ConcurrentHashMap<>(); + + private final TaskScheduler taskScheduler; + + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss"); public ScheduleService(ExerciseLifecycleService exerciseLifecycleService, ParticipationLifecycleService participationLifecycleService) { this.exerciseLifecycleService = exerciseLifecycleService; this.participationLifecycleService = participationLifecycleService; + + // Initialize the TaskScheduler + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(1); + scheduler.setVirtualThreads(true); + scheduler.initialize(); + this.taskScheduler = scheduler; } - private void addScheduledTask(Exercise exercise, ExerciseLifecycle lifecycle, Set> futures) { - Tuple taskId = new Tuple<>(exercise.getId(), lifecycle); - scheduledExerciseTasks.put(taskId, futures); + /** + * Get all scheduled exercise events. + * + * @param pageable the pagination information + * @return a page of scheduled exercise events + */ + public Page findAllExerciseEvents(Pageable pageable) { + // Flatten the map into a list of ScheduledExerciseEvent + var allEvents = scheduledExerciseTasks.entrySet().stream().flatMap(entry -> entry.getValue().stream().map(task -> { + // Calculate the scheduled time from the future's delay + var scheduledTime = ZonedDateTime.now().plusSeconds(task.future().getDelay(TimeUnit.SECONDS)); + return new ScheduledExerciseEvent(entry.getKey().exerciseId(), entry.getKey().lifecycle(), task.name(), scheduledTime, task.future().state()); + })).sorted(Comparator.comparing(ScheduledExerciseEvent::scheduledTime)).toList(); + + // Apply pagination + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), allEvents.size()); + List paginatedEvents = start < allEvents.size() ? allEvents.subList(start, end) : new ArrayList<>(); + + return new PageImpl<>(paginatedEvents, pageable, allEvents.size()); } - private void removeScheduledTask(Long exerciseId, ExerciseLifecycle lifecycle) { - Tuple taskId = new Tuple<>(exerciseId, lifecycle); - scheduledExerciseTasks.remove(taskId); + /** + * Initializes and schedules periodic logging and cleanup tasks for scheduled exercises + * and participation tasks. This method is triggered automatically when the application + * is fully started. + * + *

+ * Every 15 seconds, this method: + *

    + *
  • Logs the total number of scheduled exercise and participation tasks.
  • + *
  • Iterates through scheduled exercise tasks and logs their details, including + * execution time and state. It removes tasks that are no longer running.
  • + *
  • Iterates through scheduled participation tasks and logs their details, + * including execution time and state. It removes tasks that are no longer running.
  • + *
  • Cleans up empty entries from both task maps to avoid memory leaks.
  • + *
+ * + *

+ * The scheduling mechanism ensures that outdated or completed tasks do not persist in + * memory unnecessarily while maintaining visibility into scheduled tasks. + */ + @EventListener(ApplicationReadyEvent.class) + public void startup() { + taskScheduler.scheduleAtFixedRate(() -> { + log.debug("Number of scheduled Exercise Tasks: {}", scheduledExerciseTasks.values().stream().mapToLong(Set::size).sum()); + + // if the map is not empty and there is at least still one future in the values map, log the tasks and remove the ones that are not running anymore + if (!scheduledExerciseTasks.isEmpty() && scheduledExerciseTasks.values().stream().anyMatch(set -> !set.isEmpty())) { + log.debug(" Scheduled Exercise Tasks:"); + scheduledExerciseTasks.forEach((key, taskNames) -> { + taskNames.removeIf(taskName -> { + long delay = taskName.future().getDelay(TimeUnit.SECONDS); + var state = taskName.future().state(); + Instant scheduledTime = Instant.now().plusSeconds(delay); + ZonedDateTime zonedScheduledTime = scheduledTime.atZone(systemDefault()); + String formattedTime = zonedScheduledTime.format(formatter); + log.debug(" Exercise: {}, Lifecycle: {}, Name: {}, Scheduled Run Time: {}, State: {}, Remaining Delay: {} s", key.exerciseId(), key.lifecycle(), + taskName.name(), formattedTime, state, delay); + return state != Future.State.RUNNING; + }); + }); + } + + // clean up empty entries in the map + scheduledExerciseTasks.entrySet().removeIf(entry -> entry.getValue().isEmpty()); + + log.debug("Number of scheduled Participation Tasks: {}", scheduledParticipationTasks.values().stream().mapToLong(Set::size).sum()); + + // if the map is not empty and there is at least still one future in the values map, log the tasks and remove the ones that are not running anymore + if (!scheduledParticipationTasks.isEmpty() && scheduledParticipationTasks.values().stream().anyMatch(set -> !set.isEmpty())) { + log.debug(" Scheduled Participation Tasks:"); + scheduledParticipationTasks.forEach((key, taskNames) -> { + taskNames.removeIf(taskName -> { + long delay = taskName.future().getDelay(TimeUnit.SECONDS); + var state = taskName.future().state(); + Instant scheduledTime = Instant.now().plusSeconds(delay); + ZonedDateTime zonedScheduledTime = scheduledTime.atZone(systemDefault()); + String formattedTime = zonedScheduledTime.format(formatter); + log.debug(" Exercise: {}, Participation: {}, Lifecycle: {}, Name: {}, Scheduled Run Time: {}, State: {}, Remaining Delay: {} s", key.exerciseId(), + key.participationId(), key.lifecycle(), taskName.name(), formattedTime, state, delay); + return state != Future.State.RUNNING; + }); + }); + } + + // clean up empty entries in the map + scheduledParticipationTasks.entrySet().removeIf(entry -> entry.getValue().isEmpty()); + + }, Duration.ofSeconds(15)); + } + + private void addScheduledExerciseTasks(Exercise exercise, ExerciseLifecycle lifecycle, Set> futures, String name) { + ExerciseLifecycleKey task = new ExerciseLifecycleKey(exercise.getId(), lifecycle); + scheduledExerciseTasks.put(task, convert(futures, name)); } - private void addScheduledTask(Participation participation, ParticipationLifecycle lifecycle, Set> futures) { - Triple taskId = Triple.of(participation.getExercise().getId(), participation.getId(), lifecycle); - scheduledParticipationTasks.put(taskId, futures); + private Set convert(Set> futures, String name) { + return futures.stream().map(future -> new ScheduledTaskName(future, name)).collect(Collectors.toSet()); } - private void removeScheduledTask(Long exerciseId, Long participationId, ParticipationLifecycle lifecycle) { - Triple taskId = Triple.of(exerciseId, participationId, lifecycle); - scheduledParticipationTasks.remove(taskId); + private void removeScheduledExerciseTask(Long exerciseId, ExerciseLifecycle lifecycle) { + ExerciseLifecycleKey task = new ExerciseLifecycleKey(exerciseId, lifecycle); + scheduledExerciseTasks.remove(task); + } + + private void addScheduledParticipationTask(Participation participation, ParticipationLifecycle lifecycle, Set> futures, String name) { + ParticipationLifecycleKey task = new ParticipationLifecycleKey(participation.getExercise().getId(), participation.getId(), lifecycle); + scheduledParticipationTasks.put(task, convert(futures, name)); + } + + private void removeScheduledParticipationTask(Long exerciseId, Long participationId, ParticipationLifecycle lifecycle) { + ParticipationLifecycleKey task = new ParticipationLifecycleKey(exerciseId, participationId, lifecycle); + scheduledParticipationTasks.remove(task); } /** @@ -72,13 +208,14 @@ private void removeScheduledTask(Long exerciseId, Long participationId, Particip * @param exercise Exercise * @param lifecycle ExerciseLifecycle * @param task Runnable task to be executed on the lifecycle hook + * @param name Name of the task */ - public void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Runnable task) { + public void scheduleExerciseTask(Exercise exercise, ExerciseLifecycle lifecycle, Runnable task, String name) { // check if already scheduled for exercise. if so, cancel. // no exercise should be scheduled more than once. cancelScheduledTaskForLifecycle(exercise.getId(), lifecycle); ScheduledFuture scheduledTask = exerciseLifecycleService.scheduleTask(exercise, lifecycle, task); - addScheduledTask(exercise, lifecycle, Set.of(scheduledTask)); + addScheduledExerciseTasks(exercise, lifecycle, new HashSet<>(List.of(scheduledTask)), name); } /** @@ -88,13 +225,30 @@ public void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Runnabl * @param batch QuizBatch * @param lifecycle ExerciseLifecycle * @param task Runnable task to be executed on the lifecycle hook + * @param name Name of the task */ - public void scheduleTask(QuizExercise exercise, QuizBatch batch, ExerciseLifecycle lifecycle, Runnable task) { + public void scheduleExerciseTask(QuizExercise exercise, QuizBatch batch, ExerciseLifecycle lifecycle, Runnable task, String name) { // check if already scheduled for exercise. if so, cancel. // no exercise should be scheduled more than once. cancelScheduledTaskForLifecycle(exercise.getId(), lifecycle); ScheduledFuture scheduledTask = exerciseLifecycleService.scheduleTask(exercise, batch, lifecycle, task); - addScheduledTask(exercise, lifecycle, Set.of(scheduledTask)); + addScheduledExerciseTasks(exercise, lifecycle, new HashSet<>(List.of(scheduledTask)), name); + } + + /** + * Schedule a set of tasks for the given Exercise for the provided ExerciseLifecycle at the given times. + * + * @param exercise Exercise + * @param lifecycle ExerciseLifecycle + * @param scheduledTask One runnable tasks to be executed at the associated ZonedDateTimes + * @param name Name of the task + */ + public void scheduleExerciseTask(Exercise exercise, ExerciseLifecycle lifecycle, Tuple scheduledTask, String name) { + // check if already scheduled for exercise. if so, cancel. + // no exercise should be scheduled more than once for each lifecycle + cancelScheduledTaskForLifecycle(exercise.getId(), lifecycle); + ScheduledFuture scheduledFuture = exerciseLifecycleService.scheduleTask(exercise, lifecycle, scheduledTask.second()); + addScheduledExerciseTasks(exercise, lifecycle, new HashSet<>(List.of(scheduledFuture)), name); } /** @@ -102,14 +256,15 @@ public void scheduleTask(QuizExercise exercise, QuizBatch batch, ExerciseLifecyc * * @param exercise Exercise * @param lifecycle ExerciseLifecycle - * @param tasks Runnable tasks to be executed at the associated ZonedDateTimes + * @param tasks Runnable tasks to be executed at the associated ZonedDateTimes, must be a mutable set + * @param name Name of the task */ - public void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Set> tasks) { + public void scheduleExerciseTask(Exercise exercise, ExerciseLifecycle lifecycle, Set> tasks, String name) { // check if already scheduled for exercise. if so, cancel. // no exercise should be scheduled more than once. cancelScheduledTaskForLifecycle(exercise.getId(), lifecycle); Set> scheduledTasks = exerciseLifecycleService.scheduleMultipleTasks(exercise, lifecycle, tasks); - addScheduledTask(exercise, lifecycle, scheduledTasks); + addScheduledExerciseTasks(exercise, lifecycle, scheduledTasks, name); } /** @@ -118,10 +273,12 @@ public void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Set addScheduledTask(participation, lifecycle, Set.of(scheduledTask))); + participationLifecycleService.scheduleTask(participation, lifecycle, task) + .ifPresent(scheduledTask -> addScheduledParticipationTask(participation, lifecycle, new HashSet<>(List.of(scheduledTask)), name)); } /** @@ -133,30 +290,21 @@ public void scheduleParticipationTask(Participation participation, Participation * @param lifecycle the lifecycle (e.g. release, due date) for which the schedule should be canceled */ public void cancelScheduledTaskForLifecycle(Long exerciseId, ExerciseLifecycle lifecycle) { - Tuple taskId = new Tuple<>(exerciseId, lifecycle); - Set> futures = scheduledExerciseTasks.get(taskId); - if (futures != null) { + var task = new ExerciseLifecycleKey(exerciseId, lifecycle); + var taskNames = scheduledExerciseTasks.get(task); + if (taskNames != null) { log.debug("Cancelling scheduled task {} for Exercise (#{}).", lifecycle, exerciseId); - futures.forEach(future -> future.cancel(true)); - removeScheduledTask(exerciseId, lifecycle); + taskNames.forEach(taskName -> taskName.future().cancel(true)); + removeScheduledExerciseTask(exerciseId, lifecycle); } ParticipationLifecycle.fromExerciseLifecycle(lifecycle).ifPresent(participationLifecycle -> { - final Stream participationIds = getScheduledParticipationIdsForExercise(exerciseId); + final Stream participationIds = scheduledParticipationTasks.keySet().stream().map(ParticipationLifecycleKey::exerciseId) + .filter(scheduledExerciseId -> Objects.equals(scheduledExerciseId, exerciseId)); participationIds.forEach(participationId -> cancelScheduledTaskForParticipationLifecycle(exerciseId, participationId, participationLifecycle)); }); } - /** - * Finds all individual participations that belong to the given exercise and are scheduled. - * - * @param exerciseId the participations belong to. - * @return a stream of the IDs of participations. - */ - private Stream getScheduledParticipationIdsForExercise(Long exerciseId) { - return scheduledParticipationTasks.keySet().stream().map(Triple::getLeft).filter(scheduledExerciseId -> Objects.equals(scheduledExerciseId, exerciseId)); - } - /** * Cancel possible schedules tasks for a provided participation. * @@ -165,12 +313,12 @@ private Stream getScheduledParticipationIdsForExercise(Long exerciseId) { * @param lifecycle the lifecycle (e.g. release, due date) for which the schedule should be canceled */ public void cancelScheduledTaskForParticipationLifecycle(Long exerciseId, Long participationId, ParticipationLifecycle lifecycle) { - Triple taskId = Triple.of(exerciseId, participationId, lifecycle); - Set> futures = scheduledParticipationTasks.get(taskId); - if (futures != null) { + var task = new ParticipationLifecycleKey(exerciseId, participationId, lifecycle); + Set taskNames = scheduledParticipationTasks.get(task); + if (taskNames != null) { log.debug("Cancelling scheduled task {} for Participation (#{}).", lifecycle, participationId); - futures.forEach(future -> future.cancel(true)); - removeScheduledTask(exerciseId, participationId, lifecycle); + taskNames.forEach(taskName -> taskName.future().cancel(true)); + removeScheduledParticipationTask(exerciseId, participationId, lifecycle); } } @@ -190,8 +338,8 @@ public void cancelAllScheduledParticipationTasks(Long exerciseId, Long participa * Cancels all futures tasks, only use this for testing purposes */ public void clearAllTasks() { - scheduledParticipationTasks.values().forEach(futures -> futures.forEach(future -> future.cancel(true))); - scheduledExerciseTasks.values().forEach(futures -> futures.forEach(future -> future.cancel(true))); + scheduledParticipationTasks.values().forEach(taskNames -> taskNames.forEach(taskName -> taskName.future().cancel(true))); + scheduledExerciseTasks.values().forEach(taskNames -> taskNames.forEach(taskName -> taskName.future().cancel(true))); scheduledParticipationTasks.clear(); scheduledExerciseTasks.clear(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/util/Tuple.java b/src/main/java/de/tum/cit/aet/artemis/core/util/Tuple.java index aac92784b327..ac23fbb1feac 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/util/Tuple.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/util/Tuple.java @@ -1,10 +1,12 @@ package de.tum.cit.aet.artemis.core.util; +import java.io.Serializable; + /** * Immutable tuple object. * - * @param first param. - * @param second param. + * @param first param. + * @param second param. */ -public record Tuple(X x, Y y) { +public record Tuple(F first, S second) implements Serializable { } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminScheduleResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminScheduleResource.java new file mode 100644 index 000000000000..0d697550f107 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminScheduleResource.java @@ -0,0 +1,49 @@ +package de.tum.cit.aet.artemis.core.web.admin; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import static tech.jhipster.web.util.PaginationUtil.generatePaginationHttpHeaders; + +import java.util.List; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAdmin; +import de.tum.cit.aet.artemis.core.service.ScheduleService; + +/** + * REST controller for getting the audit events. + */ +@Profile(PROFILE_CORE) +@EnforceAdmin +@RestController +@RequestMapping("api/admin/") +public class AdminScheduleResource { + + private final ScheduleService scheduleService; + + public AdminScheduleResource(ScheduleService scheduleService) { + this.scheduleService = scheduleService; + } + + /** + * GET /exercise-schedules : get a page of scheduled events. + * + * @param pageable the pagination information + * @return the ResponseEntity with status 200 (OK) and the list of ScheduledExerciseEvents in body + */ + @GetMapping("exercise-schedules") + public ResponseEntity> getAll(Pageable pageable) { + Page page = scheduleService.findAllExerciseEvents(pageable); + HttpHeaders headers = generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/ExerciseLifecycle.java b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/ExerciseLifecycle.java index 812b7a6534c7..fe7d3e1c1c32 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/ExerciseLifecycle.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/ExerciseLifecycle.java @@ -8,6 +8,19 @@ import de.tum.cit.aet.artemis.quiz.domain.QuizExercise; public enum ExerciseLifecycle implements IExerciseLifecycle { + SHORTLY_BEFORE_RELEASE { + + @Override + public ZonedDateTime getDateFromExercise(Exercise exercise) { + return exercise.getReleaseDate() != null ? exercise.getReleaseDate().minusSeconds(15) : null; + } + + @Override + public ZonedDateTime getDateFromQuizBatch(QuizBatch quizBatch, QuizExercise quizExercise) { + return getDateFromExercise(quizExercise); + } + }, + RELEASE { @Override diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseLifecycleService.java b/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseLifecycleService.java index 48387319740a..d8e929b28765 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseLifecycleService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/service/ExerciseLifecycleService.java @@ -47,7 +47,7 @@ public ExerciseLifecycleService(@Qualifier("taskScheduler") TaskScheduler schedu */ public ScheduledFuture scheduleTask(Exercise exercise, ZonedDateTime lifecycleDate, ExerciseLifecycle lifecycle, Runnable task) { final ScheduledFuture future = scheduler.schedule(task, lifecycleDate.toInstant()); - log.debug("Scheduled Task for Exercise \"{}\" (#{}) to trigger on {}.", exercise.getTitle(), exercise.getId(), lifecycle); + log.debug("Scheduled Task for Exercise \"{}\" (#{}) to trigger on {} - {}", exercise.getTitle(), exercise.getId(), lifecycle, lifecycleDate); return future; } @@ -101,7 +101,7 @@ public ScheduledFuture scheduleTask(QuizExercise exercise, QuizBatch batch, E public Set> scheduleMultipleTasks(Exercise exercise, ExerciseLifecycle lifecycle, Set> tasks) { final Set> futures = new HashSet<>(); for (var task : tasks) { - var future = scheduler.schedule(task.y(), task.x().toInstant()); + var future = scheduler.schedule(task.second(), task.first().toInstant()); futures.add(future); } log.debug("Scheduled {} Tasks for Exercise \"{}\" (#{}) to trigger on {}.", tasks.size(), exercise.getTitle(), exercise.getId(), lifecycle.toString()); diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java index 090af5a73bdd..01b49ce9e40d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java @@ -260,7 +260,7 @@ public ResponseEntity startParticipation(@PathVariable Long exerc // 2) create a scheduled lock operation (see ProgrammingExerciseScheduleService) // var task = programmingExerciseScheduleService.lockStudentRepository(participation); // 3) add the task to the schedule service - // scheduleService.scheduleTask(exercise, ExerciseLifecycle.DUE, task); + // scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.DUE, task); } // remove sensitive information before sending participation to the client diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseScheduleService.java b/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseScheduleService.java index c6a911757e45..8c800e5a3366 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseScheduleService.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseScheduleService.java @@ -142,7 +142,7 @@ private void scheduleCourseExercise(ModelingExercise exercise) { // For any course exercise that needsToBeScheduled (buildAndTestAfterDueDate and/or manual assessment) if (exercise.getDueDate() != null && ZonedDateTime.now().isBefore(exercise.getDueDate())) { - scheduleService.scheduleTask(exercise, ExerciseLifecycle.DUE, () -> buildModelingClusters(exercise).run()); + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.DUE, () -> buildModelingClusters(exercise).run(), "build modeling clusters after due date"); log.debug("Scheduled build modeling clusters after due date for Modeling Exercise '{}' (#{}) for {}.", exercise.getTitle(), exercise.getId(), exercise.getDueDate()); } else { @@ -160,7 +160,8 @@ private void scheduleExamExercise(ModelingExercise exercise) { if (ZonedDateTime.now().isBefore(examDateService.getLatestIndividualExamEndDateWithGracePeriod(exam))) { var buildDate = endDate.plusMinutes(EXAM_END_WAIT_TIME_FOR_COMPASS_MINUTES); exercise.setClusterBuildDate(buildDate); - scheduleService.scheduleTask(exercise, ExerciseLifecycle.BUILD_COMPASS_CLUSTERS_AFTER_EXAM, () -> buildModelingClusters(exercise).run()); + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.BUILD_COMPASS_CLUSTERS_AFTER_EXAM, () -> buildModelingClusters(exercise).run(), + "build modeling clusters after exam"); } log.debug("Scheduled Exam Modeling Exercise '{}' (#{}).", exercise.getTitle(), exercise.getId()); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java index 438b9d06b3bd..20a5ab1d6c67 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/GitService.java @@ -320,10 +320,10 @@ public Repository getOrCheckoutRepository(VcsRepositoryUri repoUri, boolean pull return getOrCheckoutRepository(repoUri, repoUri, localPath, pullOnGet, defaultBranch); } - public Repository getOrCheckoutRepositoryIntoTargetDirectory(VcsRepositoryUri repoUri, VcsRepositoryUri targetUrl, boolean pullOnGet) + public Repository getOrCheckoutRepositoryIntoTargetDirectory(VcsRepositoryUri repoUri, VcsRepositoryUri targetUri, boolean pullOnGet) throws GitAPIException, GitException, InvalidPathException { - Path localPath = getDefaultLocalPathOfRepo(targetUrl); - return getOrCheckoutRepository(repoUri, targetUrl, localPath, pullOnGet); + Path localPath = getDefaultLocalPathOfRepo(targetUri); + return getOrCheckoutRepository(repoUri, targetUri, localPath, pullOnGet); } public Repository getOrCheckoutRepository(VcsRepositoryUri repoUri, Path localPath, boolean pullOnGet) throws GitAPIException, GitException, InvalidPathException { @@ -1015,7 +1015,7 @@ public boolean accept(java.io.File directory, String fileName) { } /** - * Lists all files and directories within the given repository, excluding symbolic links. + * Returns all files and directories within the given repository in a map, excluding symbolic links. * This method performs a file scan and filters out symbolic links. * It supports bare and checked-out repositories. *

@@ -1026,7 +1026,7 @@ public boolean accept(java.io.File directory, String fileName) { * @return A {@link Map} where each key is a {@link File} object representing a file or directory, and each value is * the corresponding {@link FileType} (FILE or FOLDER). The map excludes symbolic links. */ - public Map listFilesAndFolders(Repository repo) { + public Map getFilesAndFolders(Repository repo) { FileAndDirectoryFilter filter = new FileAndDirectoryFilter(); Iterator itr = FileUtils.iterateFilesAndDirs(repo.getLocalPath().toFile(), filter, filter); @@ -1048,13 +1048,13 @@ public Map listFilesAndFolders(Repository repo) { } /** - * List all files in the repository. In an empty git repo, this method returns 0. + * List all files in the repository. In an empty git repo, this method returns en empty list. * * @param repo Local Repository Object. * @return Collection of File objects */ @NotNull - public Collection listFiles(Repository repo) { + public Collection getFiles(Repository repo) { // Check if list of files is already cached if (repo.getFiles() == null) { FileAndDirectoryFilter filter = new FileAndDirectoryFilter(); @@ -1083,7 +1083,7 @@ public Optional getFileByName(Repository repo, String filename) { // Makes sure the requested file is part of the scanned list of files. // Ensures that it is not possible to do bad things like filename="../../passwd" - for (File file : listFilesAndFolders(repo).keySet()) { + for (File file : getFilesAndFolders(repo).keySet()) { if (file.toString().equals(filename)) { return Optional.of(file); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/JavaTemplateUpgradeService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/JavaTemplateUpgradeService.java index 9e6a796307a0..c6be04cd1dc6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/JavaTemplateUpgradeService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/JavaTemplateUpgradeService.java @@ -103,7 +103,7 @@ private void upgradeTemplateFiles(ProgrammingExercise exercise, RepositoryType r String templatePomDir = repositoryType == RepositoryType.TESTS ? "test/maven/projectTemplate" : repositoryType.getName(); Resource[] templatePoms = getTemplateResources(exercise, templatePomDir + "/**/" + POM_FILE); Repository repository = gitService.getOrCheckoutRepository(exercise.getRepositoryURL(repositoryType), true); - List repositoryPoms = gitService.listFiles(repository).stream().filter(file -> Objects.equals(file.getName(), POM_FILE)).toList(); + List repositoryPoms = gitService.getFiles(repository).stream().filter(file -> Objects.equals(file.getName(), POM_FILE)).toList(); // Validate that template and repository have the same number of pom.xml files, otherwise no upgrade will take place if (templatePoms.length == 1 && repositoryPoms.size() == 1) { @@ -272,7 +272,7 @@ private Predicate isPlugin(String groupId, String artifactId) { } private Optional getFileByName(Repository repository, String filename) { - return gitService.listFilesAndFolders(repository).keySet().stream().filter(file -> Objects.equals(filename, file.getName())).findFirst(); + return gitService.getFilesAndFolders(repository).keySet().stream().filter(file -> Objects.equals(filename, file.getName())).findFirst(); } private Optional getFileByName(Resource[] resources, String filename) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java index ffbf6aa37f0a..2fa7a65c0880 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java @@ -324,7 +324,7 @@ private void removeAuxiliaryRepository(AuxiliaryRepository auxiliaryRepository) private void setupTemplateAndPush(final RepositoryResources repositoryResources, final String templateName, final ProgrammingExercise programmingExercise, final User user) throws IOException, GitAPIException { // Only copy template if repo is empty - if (!gitService.listFiles(repositoryResources.repository).isEmpty()) { + if (!gitService.getFiles(repositoryResources.repository).isEmpty()) { return; } @@ -358,7 +358,7 @@ private static Path getRepoAbsoluteLocalPath(final Repository repository) { */ private void setupTestTemplateAndPush(final RepositoryResources resources, final ProgrammingExercise programmingExercise, final User user) throws IOException, GitAPIException { // Only copy template if repo is empty - if (gitService.listFiles(resources.repository).isEmpty() + if (gitService.getFiles(resources.repository).isEmpty() && (programmingExercise.getProgrammingLanguage() == ProgrammingLanguage.JAVA || programmingExercise.getProgrammingLanguage() == ProgrammingLanguage.KOTLIN)) { setupJVMTestTemplateAndPush(resources, programmingExercise, user); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseScheduleService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseScheduleService.java index 16eebcaca693..a0d08476d978 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseScheduleService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseScheduleService.java @@ -6,8 +6,6 @@ import java.time.Instant; import java.time.ZonedDateTime; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; @@ -30,7 +28,6 @@ import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; import org.springframework.context.event.EventListener; -import org.springframework.core.env.Environment; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; @@ -42,6 +39,7 @@ import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; import de.tum.cit.aet.artemis.core.security.SecurityUtils; +import de.tum.cit.aet.artemis.core.service.ProfileService; import de.tum.cit.aet.artemis.core.service.ScheduleService; import de.tum.cit.aet.artemis.core.util.Tuple; import de.tum.cit.aet.artemis.exam.domain.Exam; @@ -60,7 +58,6 @@ import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseTestCaseRepository; -import tech.jhipster.config.JHipsterConstants; @Service @Profile(PROFILE_SCHEDULING) @@ -70,8 +67,6 @@ public class ProgrammingExerciseScheduleService implements IExerciseScheduleServ private final ScheduleService scheduleService; - private final Environment env; - private final ProgrammingExerciseRepository programmingExerciseRepository; private final ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository; @@ -102,12 +97,14 @@ public class ProgrammingExerciseScheduleService implements IExerciseScheduleServ private final TaskScheduler scheduler; + private final ProfileService profileService; + public ProgrammingExerciseScheduleService(ScheduleService scheduleService, ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository, ResultRepository resultRepository, ParticipationRepository participationRepository, - ProgrammingExerciseStudentParticipationRepository programmingExerciseParticipationRepository, Environment env, ProgrammingTriggerService programmingTriggerService, + ProgrammingExerciseStudentParticipationRepository programmingExerciseParticipationRepository, ProgrammingTriggerService programmingTriggerService, ProgrammingExerciseGradingService programmingExerciseGradingService, GroupNotificationService groupNotificationService, ExamDateService examDateService, ProgrammingExerciseParticipationService programmingExerciseParticipationService, ExerciseDateService exerciseDateService, ExamRepository examRepository, - StudentExamRepository studentExamRepository, GitService gitService, @Qualifier("taskScheduler") TaskScheduler scheduler) { + StudentExamRepository studentExamRepository, GitService gitService, @Qualifier("taskScheduler") TaskScheduler scheduler, ProfileService profileService) { this.scheduleService = scheduleService; this.programmingExerciseRepository = programmingExerciseRepository; this.programmingExerciseTestCaseRepository = programmingExerciseTestCaseRepository; @@ -122,9 +119,9 @@ public ProgrammingExerciseScheduleService(ScheduleService scheduleService, Progr this.examDateService = examDateService; this.programmingExerciseParticipationService = programmingExerciseParticipationService; this.programmingExerciseGradingService = programmingExerciseGradingService; - this.env = env; this.gitService = gitService; this.scheduler = scheduler; + this.profileService = profileService; } @EventListener(ApplicationReadyEvent.class) @@ -136,8 +133,7 @@ public void applicationReady() { @Override public void scheduleRunningExercisesOnStartup() { try { - Collection activeProfiles = Arrays.asList(env.getActiveProfiles()); - if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT)) { + if (profileService.isDevActive()) { // only execute this on production server, i.e. when the prod profile is active // NOTE: if you want to test this locally, please comment it out, but do not commit the changes return; @@ -299,29 +295,41 @@ private void scheduleCourseExercise(ProgrammingExercise exercise) { private void scheduleTemplateCommitCombination(ProgrammingExercise exercise) { if (exercise.getReleaseDate() != null) { - var scheduledRunnable = Set.of(new Tuple<>(exercise.getReleaseDate().minusSeconds(Constants.SECONDS_BEFORE_RELEASE_DATE_FOR_COMBINING_TEMPLATE_COMMITS), - combineTemplateCommitsForExercise(exercise))); - scheduleService.scheduleTask(exercise, ExerciseLifecycle.RELEASE, scheduledRunnable); - log.debug("Scheduled combining template commits before release date for Programming Exercise \"{}\" (#{}) for {}.", exercise.getTitle(), exercise.getId(), + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.SHORTLY_BEFORE_RELEASE, () -> combineTemplateCommitsForExercise(exercise).run(), + "combine template commits"); + log.info("Scheduled combining template commits before release date for programming exercise \"{}\" (#{}) for {}.", exercise.getTitle(), exercise.getId(), exercise.getReleaseDate()); } } private void scheduleDueDateLockAndScoreUpdate(ProgrammingExercise exercise) { final boolean updateScores = isScoreUpdateAfterDueDateNeeded(exercise); + final boolean isLocalVC = profileService.isLocalVcsActive(); - scheduleService.scheduleTask(exercise, ExerciseLifecycle.DUE, () -> { - lockStudentRepositoriesAndParticipationsRegularDueDate(exercise).run(); - if (updateScores) { - updateStudentScoresRegularDueDate(exercise).run(); + if (isLocalVC) { + if (!updateScores) { + // no scheduling is needed + return; } - }); + else { + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.DUE, () -> updateStudentScoresRegularDueDate(exercise).run(), "update student scores"); + } + } + else { + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.DUE, () -> { + lockStudentRepositoriesAndParticipationsRegularDueDate(exercise).run(); + if (updateScores) { + updateStudentScoresRegularDueDate(exercise).run(); + } + }, "lock student repositories and participations"); + } log.debug("Scheduled lock student repositories after due date for Programming Exercise '{}' (#{}) for {}.", exercise.getTitle(), exercise.getId(), exercise.getDueDate()); } private void scheduleBuildAndTestAfterDueDate(ProgrammingExercise exercise) { - scheduleService.scheduleTask(exercise, ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE, buildAndTestRunnableForExercise(exercise)); + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE, buildAndTestRunnableForExercise(exercise), + "build and test student submissions"); log.debug("Scheduled build and test for student submissions after due date for Programming Exercise '{}' (#{}) for {}.", exercise.getTitle(), exercise.getId(), exercise.getBuildAndTestStudentSubmissionsAfterDueDate()); } @@ -337,8 +345,7 @@ private void scheduleBuildAndTestAfterDueDate(ProgrammingExercise exercise) { private void scheduleParticipationTasks(final ProgrammingExercise exercise, final ZonedDateTime now) { final boolean isScoreUpdateNeeded = isScoreUpdateAfterDueDateNeeded(exercise); - final List participations = programmingExerciseParticipationRepository - .findWithSubmissionsAndTeamStudentsByExerciseId(exercise.getId()); + final var participations = programmingExerciseParticipationRepository.findWithSubmissionsAndTeamStudentsByExerciseId(exercise.getId()); for (final var participation : participations) { if (exercise.getDueDate() == null || participation.getIndividualDueDate() == null) { scheduleService.cancelAllScheduledParticipationTasks(exercise.getId(), participation.getId()); @@ -349,8 +356,8 @@ private void scheduleParticipationTasks(final ProgrammingExercise exercise, fina } } - private void scheduleParticipationWithIndividualDueDate(final ZonedDateTime now, final ProgrammingExercise exercise, - final ProgrammingExerciseStudentParticipation participation, boolean isScoreUpdateNeeded) { + private void scheduleParticipationWithIndividualDueDate(ZonedDateTime now, ProgrammingExercise exercise, ProgrammingExerciseStudentParticipation participation, + boolean isScoreUpdateNeeded) { final boolean isBeforeDueDate = now.isBefore(participation.getIndividualDueDate()); // Update scores on due date if (isBeforeDueDate) { @@ -372,15 +379,30 @@ private void scheduleParticipationWithIndividualDueDate(final ZonedDateTime now, } private void scheduleAfterDueDateForParticipation(ProgrammingExerciseStudentParticipation participation, boolean isScoreUpdateNeeded) { - scheduleService.scheduleParticipationTask(participation, ParticipationLifecycle.DUE, () -> { - lockStudentRepositoryAndParticipation(participation).run(); - + if (!profileService.isLocalVcsActive()) { if (isScoreUpdateNeeded) { - final List updatedResult = programmingExerciseGradingService.updateParticipationResults(participation); - resultRepository.saveAll(updatedResult); + scheduleService.scheduleParticipationTask(participation, ParticipationLifecycle.DUE, () -> { + lockStudentRepositoryAndParticipation(participation).run(); + final List updatedResult = programmingExerciseGradingService.updateParticipationResults(participation); + resultRepository.saveAll(updatedResult); + }, "lock student repository and update student scores"); + log.debug("Scheduled task to lock student repository and update student scores {} at the individual due date.", participation.getId()); } - }); - log.debug("Scheduled task to lock repository for participation {} at the individual due date.", participation.getId()); + else { + scheduleService.scheduleParticipationTask(participation, ParticipationLifecycle.DUE, () -> lockStudentRepositoryAndParticipation(participation).run(), + "lock student repository"); + log.debug("Scheduled task to lock repository for participation {} at the individual due date.", participation.getId()); + } + } + else { + if (isScoreUpdateNeeded) { + scheduleService.scheduleParticipationTask(participation, ParticipationLifecycle.DUE, () -> { + final List updatedResult = programmingExerciseGradingService.updateParticipationResults(participation); + resultRepository.saveAll(updatedResult); + }, "update student scores"); + } + log.debug("Scheduled task to update student scores {} at the individual due date.", participation.getId()); + } } private void scheduleBuildAndTestAfterDueDateForParticipation(ProgrammingExerciseStudentParticipation participation) { @@ -395,7 +417,7 @@ private void scheduleBuildAndTestAfterDueDateForParticipation(ProgrammingExercis log.error("Programming participation with id {} in exercise {} is no longer available in database for use in scheduled task.", participation.getId(), exercise.getId()); } - }); + }, "build and test student submission"); } private boolean isScoreUpdateAfterDueDateNeeded(ProgrammingExercise exercise) { @@ -421,24 +443,29 @@ private void scheduleExamExercise(ProgrammingExercise exercise) { if (now.isBefore(unlockDate)) { // Schedule unlocking of student repositories // Uses the custom exam unlock date rather than the of the exercise's lifecycle - scheduleService.scheduleTask(exercise, ExerciseLifecycle.RELEASE, Set.of(new Tuple<>(unlockDate, unlockAllStudentRepositoriesAndParticipations(exercise)))); + if (!profileService.isLocalVcsActive()) { + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.RELEASE, new Tuple<>(unlockDate, unlockAllStudentRepositoriesAndParticipations(exercise)), + "unlock student repositories"); + } } // DURING EXAM else if (now.isBefore(examDateService.getLatestIndividualExamEndDate(exam))) { // This is only a backup (e.g. a crash of this node and restart during the exam) // TODO: Christian Femers: this can lead to a weird edge case after the normal exam end date and before the last individual exam end date (in case of working time // extensions) - var scheduledRunnable = Set.of( - new Tuple<>(now.plusSeconds(Constants.SECONDS_AFTER_RELEASE_DATE_FOR_UNLOCKING_STUDENT_EXAM_REPOS), unlockAllStudentRepositoriesAndParticipations(exercise))); - scheduleService.scheduleTask(exercise, ExerciseLifecycle.RELEASE, scheduledRunnable); - + if (!profileService.isLocalVcsActive()) { + var scheduledRunnable = new Tuple<>(now.plusSeconds(Constants.SECONDS_AFTER_RELEASE_DATE_FOR_UNLOCKING_STUDENT_EXAM_REPOS), + unlockAllStudentRepositoriesAndParticipations(exercise)); + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.RELEASE, scheduledRunnable, "unlock student repositories"); + } // Re-schedule the locking of student repositories based on the individual working time rescheduleProgrammingExerciseDuringExamConduction(exercise); } // NOTHING TO DO AFTER EXAM if (exercise.getBuildAndTestStudentSubmissionsAfterDueDate() != null && now.isBefore(exercise.getBuildAndTestStudentSubmissionsAfterDueDate())) { - scheduleService.scheduleTask(exercise, ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE, buildAndTestRunnableForExercise(exercise)); + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE, () -> buildAndTestRunnableForExercise(exercise).run(), + "build and test student submissions"); } else { scheduleService.cancelScheduledTaskForLifecycle(exercise.getId(), ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE); @@ -449,6 +476,7 @@ else if (now.isBefore(examDateService.getLatestIndividualExamEndDate(exam))) { @NotNull private Runnable combineTemplateCommitsForExercise(ProgrammingExercise exercise) { return () -> { + log.debug("Start combine template commits for programming exercise {}.", exercise.getId()); SecurityUtils.setAuthorizationObject(); try { ProgrammingExercise programmingExerciseWithTemplateParticipation = programmingExerciseRepository @@ -457,7 +485,7 @@ private Runnable combineTemplateCommitsForExercise(ProgrammingExercise exercise) log.debug("Combined template repository commits of programming exercise {}.", programmingExerciseWithTemplateParticipation.getId()); } catch (GitAPIException e) { - log.error("Failed to communicate with GitAPI for combining template commits of exercise {}", exercise.getId(), e); + log.error("Failed to communicate with GitService for combining template commits of exercise {}", exercise.getId(), e); } }; } @@ -739,7 +767,7 @@ public Runnable runUnlockOperation(ProgrammingExercise exercise, Consumer> futureIndividualDueDates = individualDueDates.stream() - .filter(tuple -> tuple.x() != null && ZonedDateTime.now().isBefore(tuple.x())).collect(Collectors.toSet()); + .filter(tuple -> tuple.first() != null && ZonedDateTime.now().isBefore(tuple.first())).collect(Collectors.toSet()); scheduleIndividualRepositoryAndParticipationLockTasks(exercise, futureIndividualDueDates); }); } @@ -839,23 +867,25 @@ public Runnable unlockAllStudentParticipationsWithEarlierStartDateAndLaterDueDat */ private void scheduleIndividualRepositoryAndParticipationLockTasks(ProgrammingExercise exercise, Set> individualParticipationsWithDueDates) { + if (profileService.isLocalVcsActive()) { + // no scheduling needed + return; + } // 1. Group all participations by due date // TODO: use student exams for safety if some participations are not pre-generated - var participationsGroupedByDueDate = individualParticipationsWithDueDates.stream().filter(tuple -> tuple.x() != null) - .collect(Collectors.groupingBy(Tuple::x, Collectors.mapping(Tuple::y, Collectors.toSet()))); + var participationsGroupedByDueDate = individualParticipationsWithDueDates.stream().filter(tuple -> tuple.first() != null) + .collect(Collectors.groupingBy(Tuple::first, Collectors.mapping(Tuple::second, Collectors.toSet()))); // 2. Transform those groups into lock-repository tasks with times Set> tasks = participationsGroupedByDueDate.entrySet().stream().map(entry -> { // Check that this participation is planned to be locked and has still the same due date Predicate lockingCondition = participation -> entry.getValue().contains(participation) && entry.getKey().equals(exerciseDateService.getIndividualDueDate(exercise, participation)); - - var task = lockStudentRepositoriesAndParticipations(exercise, lockingCondition); - return new Tuple<>(entry.getKey(), task); + return new Tuple<>(entry.getKey(), lockStudentRepositoriesAndParticipations(exercise, lockingCondition)); }).collect(Collectors.toSet()); // 3. Schedule all tasks - scheduleService.scheduleTask(exercise, ExerciseLifecycle.DUE, tasks); + scheduleService.scheduleExerciseTask(exercise, ExerciseLifecycle.DUE, tasks, "lock student repositories and participations"); } /** @@ -893,6 +923,10 @@ private void rescheduleProgrammingExerciseDuringExamConduction(ProgrammingExerci * @param studentExamId the id of the student exam */ public void rescheduleStudentExamDuringConduction(Long studentExamId) { + if (profileService.isLocalVcsActive()) { + // no scheduling needed + return; + } StudentExam studentExam = studentExamRepository.findWithExercisesParticipationsSubmissionsById(studentExamId, false).orElseThrow(NoSuchElementException::new); // iterate over all programming exercises and its student participation in the student's exam @@ -910,7 +944,7 @@ public void rescheduleStudentExamDuringConduction(Long studentExamId) { // get the individual due date of the student's participation in the programming exercise ZonedDateTime dueDate = exerciseDateService.getIndividualDueDate(exercise, programmingParticipation); // schedule repository locks for each programming exercise - scheduleIndividualRepositoryAndParticipationLockTasks(exercise, Set.of(new Tuple<>(dueDate, programmingParticipation))); + scheduleIndividualRepositoryAndParticipationLockTasks(exercise, new HashSet<>(List.of(new Tuple<>(dueDate, programmingParticipation)))); }); } @@ -984,8 +1018,8 @@ private CompletableFuture> invokeO for (var future : futures) { future.whenComplete((participation, exception) -> { if (exception != null) { - log.error(String.format("'%s' failed for programming exercise with id %d for student repository with participation id %d", operationName, - programmingExercise.getId(), participation.getId()), exception); + log.error("'{}' failed for programming exercise with id {} for student repository with participation id {}", operationName, programmingExercise.getId(), + participation.getId(), exception); failedOperations.add(participation); } }); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java index 1ca095cac600..330873d2dfff 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/RepositoryService.java @@ -97,7 +97,7 @@ public ProgrammingExerciseParticipation getAsProgrammingExerciseParticipationOfE * @return a map of files with the information if they are a file or a folder. */ public Map getFiles(Repository repository) { - var iterator = gitService.listFilesAndFolders(repository).entrySet().iterator(); + var iterator = gitService.getFilesAndFolders(repository).entrySet().iterator(); Map fileList = new HashMap<>(); @@ -158,7 +158,7 @@ public Map getFilesContentAtCommit(ProgrammingExercise programmi * The map includes only those files that could successfully have their contents read; files that cause an IOException are logged but not included. */ public Map getFilesContentFromWorkingCopy(Repository repository) { - var files = gitService.listFilesAndFolders(repository).entrySet().stream().filter(entry -> entry.getValue() == FileType.FILE).map(Map.Entry::getKey).toList(); + var files = gitService.getFilesAndFolders(repository).entrySet().stream().filter(entry -> entry.getValue() == FileType.FILE).map(Map.Entry::getKey).toList(); Map fileListWithContent = new HashMap<>(); files.forEach(file -> { @@ -262,9 +262,9 @@ public byte[] getFile(Repository repository, String filename) throws IOException public Map getFilesWithInformationAboutChange(Repository repository, Repository templateRepository) { Map filesWithInformationAboutChange = new HashMap<>(); - var repoFiles = gitService.listFilesAndFolders(repository).entrySet().stream().filter(entry -> entry.getValue() == FileType.FILE).map(Map.Entry::getKey).toList(); + var repoFiles = gitService.getFilesAndFolders(repository).entrySet().stream().filter(entry -> entry.getValue() == FileType.FILE).map(Map.Entry::getKey).toList(); - Map templateRepoFiles = gitService.listFilesAndFolders(templateRepository).entrySet().stream().filter(entry -> entry.getValue() == FileType.FILE) + Map templateRepoFiles = gitService.getFilesAndFolders(templateRepository).entrySet().stream().filter(entry -> entry.getValue() == FileType.FILE) .collect(Collectors.toMap(entry -> entry.getKey().toString(), Map.Entry::getKey)); repoFiles.forEach(file -> { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/vcs/AbstractVersionControlService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/vcs/AbstractVersionControlService.java index 454814afc442..83f30c289159 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/vcs/AbstractVersionControlService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/vcs/AbstractVersionControlService.java @@ -120,7 +120,6 @@ public VcsRepositoryUri copyRepository(String sourceProjectKey, String sourceRep catch (GitAPIException | VersionControlException ex) { if (isReadFullyShortReadOfBlockException(ex)) { // NOTE: we ignore this particular error: it sometimes happens when pushing code that includes binary files, however the push operation typically worked correctly - // TODO: verify that the push operation actually worked correctly, e.g. by comparing the number of commits in the source and target repo log.warn("TransportException/EOFException with 'Short read of block' when copying repository {} to {}. Will ignore it", sourceRepoUri, targetRepoUri); return targetRepoUri; } diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizScheduleService.java b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizScheduleService.java index 7aa104280256..819467dd700a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizScheduleService.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizScheduleService.java @@ -89,7 +89,7 @@ public void scheduleQuizStart(QuizExercise quizExercise) { var quizBatch = quizExercise.getQuizBatches().stream().findAny(); if (quizBatch.isPresent() && quizBatch.get().getStartTime() != null) { if (quizBatch.get().getStartTime().isAfter(ZonedDateTime.now())) { - scheduleService.scheduleTask(quizExercise, quizBatch.get(), ExerciseLifecycle.START, () -> executeQuizStartNowTask(quizExercise.getId())); + scheduleService.scheduleExerciseTask(quizExercise, quizBatch.get(), ExerciseLifecycle.START, () -> executeQuizStartNowTask(quizExercise.getId()), "quiz start"); } } } @@ -104,8 +104,8 @@ public void scheduleQuizStart(QuizExercise quizExercise) { public void scheduleCalculateAllResults(QuizExercise quizExercise) { if (quizExercise.getDueDate() != null && !quizExercise.isQuizEnded()) { // we only schedule the task if the quiz is not over yet - scheduleService.scheduleTask(quizExercise, ExerciseLifecycle.DUE, - Set.of(new Tuple<>(quizExercise.getDueDate().plusSeconds(5), () -> quizSubmissionService.calculateAllResults(quizExercise.getId())))); + scheduleService.scheduleExerciseTask(quizExercise, ExerciseLifecycle.DUE, + new Tuple<>(quizExercise.getDueDate().plusSeconds(5), () -> quizSubmissionService.calculateAllResults(quizExercise.getId())), "calculate all quiz results"); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/service/TitleCacheEvictionServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/core/service/TitleCacheEvictionServiceTest.java index 9f969fb29328..7701d7f37e19 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/service/TitleCacheEvictionServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/service/TitleCacheEvictionServiceTest.java @@ -212,19 +212,19 @@ private void testCacheEvicted(String cacheName, Supplier> for (var modifier : entityModifiers) { var objInCache = idTitleSupplier.get(); - cache.put(objInCache.x(), objInCache.y()); - var cacheValueWrapper = cache.get(objInCache.x()); + cache.put(objInCache.first(), objInCache.second()); + var cacheValueWrapper = cache.get(objInCache.first()); assertThat(cacheValueWrapper).isNotNull(); - assertThat(cacheValueWrapper.get()).isEqualTo(objInCache.y()); + assertThat(cacheValueWrapper.get()).isEqualTo(objInCache.second()); boolean shouldEvict = modifier.get(); - cacheValueWrapper = cache.get(objInCache.x()); + cacheValueWrapper = cache.get(objInCache.first()); if (shouldEvict) { assertThat(cacheValueWrapper).isNull(); } else { assertThat(cacheValueWrapper).isNotNull(); - assertThat(cacheValueWrapper.get()).isEqualTo(objInCache.y()); + assertThat(cacheValueWrapper.get()).isEqualTo(objInCache.second()); } } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java index 41d8926aa61d..7347529dbd36 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java @@ -288,10 +288,10 @@ private static Stream getBranchCombinationsToTest() { } @Test - void testListFilesAndFolders() { + void testGetFilesAndFolders() { Repository localRepo = gitUtilService.getRepoByType(GitUtilService.REPOS.LOCAL); - var map = gitService.listFilesAndFolders(localRepo); + var map = gitService.getFilesAndFolders(localRepo); assertThat(map).hasSize(4).containsEntry(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE1), localRepo), FileType.FILE) .containsEntry(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE2), localRepo), FileType.FILE) @@ -300,10 +300,10 @@ void testListFilesAndFolders() { } @Test - void testListFiles() { + void testGetFiles() { Repository localRepo = gitUtilService.getRepoByType(GitUtilService.REPOS.LOCAL); - var fileList = gitService.listFiles(localRepo); + var fileList = gitService.getFiles(localRepo); assertThat(fileList).hasSize(3).contains(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE1), localRepo)) .contains(new File(gitUtilService.getFile(GitUtilService.REPOS.LOCAL, GitUtilService.FILES.FILE2), localRepo)) diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseParticipationIntegrationTest.java index 843cb7e0f378..73b1edd26296 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseParticipationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseParticipationIntegrationTest.java @@ -763,7 +763,7 @@ void getParticipationRepositoryFilesInstructorSuccess() throws Exception { var commitInfo2 = new CommitInfoDTO("hash2", "msg2", ZonedDateTime.of(2020, 1, 2, 0, 0, 0, 0, ZoneId.of("UTC")), "author2", "authorEmail2"); doReturn(List.of(commitInfo, commitInfo2)).when(gitService).getCommitInfos(participation.getVcsRepositoryUri()); doReturn(new Repository("ab", new VcsRepositoryUri("uri"))).when(gitService).checkoutRepositoryAtCommit(participation.getVcsRepositoryUri(), commitHash, true); - doReturn(Map.of()).when(gitService).listFilesAndFolders(any()); + doReturn(Map.of()).when(gitService).getFilesAndFolders(any()); doNothing().when(gitService).switchBackToDefaultBranchHead(any()); request.getMap("/api/programming-exercise-participations/" + participation.getId() + "/files-content/" + commitHash, HttpStatus.OK, String.class, String.class); @@ -792,7 +792,7 @@ void setup() throws GitAPIException, URISyntaxException, IOException { programmingExerciseIntegrationTestService.addAuxiliaryRepositoryToExercise(programmingExercise); COMMIT_HASH = "commitHash"; - doReturn(Map.of()).when(gitService).listFilesAndFolders(any()); + doReturn(Map.of()).when(gitService).getFilesAndFolders(any()); doNothing().when(gitService).switchBackToDefaultBranchHead(any()); doReturn(new Repository("ab", new VcsRepositoryUri("uri"))).when(gitService).checkoutRepositoryAtCommit(any(VcsRepositoryUri.class), any(String.class), any(Boolean.class)); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseScheduleServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseScheduleServiceTest.java index b386fb22d033..be133cc21e70 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseScheduleServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseScheduleServiceTest.java @@ -342,8 +342,8 @@ void scheduleIndividualDueDateBetweenDueDateAndBuildAndTestDate() throws Excepti instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(DELAY_MS * 2)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); + verify(scheduleService, timeout(DELAY_MS * 2)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any(), any()); + verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(), any()); // not yet locked on regular due date verify(programmingTriggerService, after(DELAY_MS * 2).never()).triggerInstructorBuildForExercise(programmingExercise.getId()); @@ -371,8 +371,9 @@ void scheduleIndividualDueDateAfterBuildAndTestDate() throws Exception { instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); // special scheduling for both lock and build and test - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any(), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(), + any()); } @Test @@ -392,11 +393,11 @@ void scheduleIndividualDueDateTestsAfterDueDateNoBuildAndTestDate() throws Excep instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any(), any()); + verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(), any()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); - verify(scheduleService, never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class), any()); + verify(scheduleService, never()).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class), any()); } @Test @@ -415,11 +416,11 @@ void cancelAllSchedulesOnRemovingExerciseDueDate() throws Exception { instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any(), any()); + verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(), any()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); - verify(scheduleService, never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class), any()); + verify(scheduleService, never()).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class), any()); // remove due date and schedule again programmingExercise.setDueDate(null); @@ -452,8 +453,8 @@ void cancelIndividualSchedulesOnRemovingIndividualDueDate() throws Exception { instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any(), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class), any()); // remove individual due date and schedule again participationIndividualDueDate.setIndividualDueDate(null); @@ -464,7 +465,7 @@ void cancelIndividualSchedulesOnRemovingIndividualDueDate() throws Exception { // called twice: first time when removing potential old schedules before scheduling, second time only cancelling verify(scheduleService, timeout(TIMEOUT_MS).times(2)).cancelScheduledTaskForParticipationLifecycle(programmingExercise.getId(), participationIndividualDueDate.getId(), ParticipationLifecycle.DUE); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any(), any()); } @Test @@ -480,8 +481,8 @@ void updateIndividualScheduleOnIndividualDueDateChange() throws Exception { instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any(), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class), any()); // change individual due date and schedule again participationIndividualDueDate.setIndividualDueDate(nowPlusMillis(DELAY_MS)); @@ -491,7 +492,7 @@ void updateIndividualScheduleOnIndividualDueDateChange() throws Exception { // scheduling called twice, each scheduling cancels potential old schedules verify(scheduleService, timeout(TIMEOUT_MS).times(2)).cancelScheduledTaskForParticipationLifecycle(programmingExercise.getId(), participationIndividualDueDate.getId(), ParticipationLifecycle.DUE); - verify(scheduleService, timeout(TIMEOUT_MS).times(2)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS).times(2)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any(), any()); } @Test @@ -510,22 +511,22 @@ void keepIndividualScheduleOnExerciseDueDateChange() throws Exception { instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any(), any()); + verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(), any()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); - verify(scheduleService, never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class), any()); + verify(scheduleService, never()).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class), any()); // change exercise due date and schedule again programmingExercise.setDueDate(nowPlusMillis(DELAY_MS)); programmingExercise = programmingExerciseRepository.saveAndFlush(programmingExercise); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(TIMEOUT_MS).times(2)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); + verify(scheduleService, timeout(TIMEOUT_MS).times(2)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any(), any()); + verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(), any()); - verify(scheduleService, timeout(TIMEOUT_MS).times(2)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); - verify(scheduleService, never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS).times(2)).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class), any()); + verify(scheduleService, never()).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class), any()); } @Test @@ -543,8 +544,8 @@ void shouldScheduleExerciseIfAnyIndividualDueDateInFuture() throws Exception { instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any(), any()); + verify(scheduleService, never()).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class), any()); } @Test @@ -561,7 +562,8 @@ void shouldCancelAllTasksIfSchedulingNoLongerNeeded() throws Exception { instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, after(SCHEDULER_TASK_TRIGGER_DELAY_MS).never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); + verify(scheduleService, after(SCHEDULER_TASK_TRIGGER_DELAY_MS).never()).scheduleExerciseTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class), + any()); verify(scheduleService, timeout(TIMEOUT_MS)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.RELEASE); verify(scheduleService, timeout(TIMEOUT_MS)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.DUE); verify(scheduleService, timeout(TIMEOUT_MS)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryIntegrationTest.java index 1fc69fb5eedb..455133ade3a3 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryIntegrationTest.java @@ -344,7 +344,7 @@ private String getCommitHash(Git repo) throws GitAPIException { void testGetFilesWithContent_shouldNotThrowException() throws Exception { Map mockedFiles = new HashMap<>(); mockedFiles.put(mock(de.tum.cit.aet.artemis.programming.domain.File.class), FileType.FILE); - doReturn(mockedFiles).when(gitService).listFilesAndFolders(any(Repository.class)); + doReturn(mockedFiles).when(gitService).getFilesAndFolders(any(Repository.class)); MockedStatic mockedFileUtils = mockStatic(FileUtils.class); mockedFileUtils.when(() -> FileUtils.readFileToString(any(File.class), eq(StandardCharsets.UTF_8))).thenThrow(IOException.class);