diff --git a/src/main/java/com/codzilla/backend/Authentication/config/SecurityConfig.java b/src/main/java/com/codzilla/backend/Authentication/config/SecurityConfig.java index 5c1353d..4bf1654 100644 --- a/src/main/java/com/codzilla/backend/Authentication/config/SecurityConfig.java +++ b/src/main/java/com/codzilla/backend/Authentication/config/SecurityConfig.java @@ -3,6 +3,7 @@ import com.codzilla.backend.Authentication.AdminAccessDeniedHandler; import com.codzilla.backend.Authentication.HttpStatusEntryPoint; import com.codzilla.backend.Authentication.JWTRequestFilter.JWTRequestFilter; +import jakarta.servlet.DispatcherType; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -49,7 +50,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, JWTRequestFilter filte .accessDeniedHandler(adminAccessDeniedHandler) ) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth.requestMatchers(WHITELIST).permitAll().anyRequest().authenticated()) + .authorizeHttpRequests(auth -> auth.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll().requestMatchers(WHITELIST).permitAll().anyRequest().authenticated()) .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class).build(); } diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/Submission.java b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/Submission.java index 377f9be..0885f78 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/Submission.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/Submission.java @@ -3,25 +3,29 @@ import jakarta.persistence.*; import lombok.Data; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; @Entity @Table(name = "submissions") @Data public class Submission { - - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private int retryCount = 0; private Long problemId; - private Long userId; + + @JdbcTypeCode(SqlTypes.UUID) + private UUID userId; @Column(columnDefinition = "TEXT") private String sourceCode; diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionController.java b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionController.java new file mode 100644 index 0000000..d73133a --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionController.java @@ -0,0 +1,97 @@ +package com.codzilla.backend.controller.Sandbox.submission; + +import com.codzilla.backend.User.User; +import com.codzilla.backend.User.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/my-submissions") +@RequiredArgsConstructor +public class SubmissionController { + + private final SubmissionRepository submissionRepository; + + private final Map>>>> waiters = new ConcurrentHashMap<>(); + private final UserRepository userRepository; + + @GetMapping + public DeferredResult>> getUserSubmissions( + @AuthenticationPrincipal User user, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime lastUpdate) { + + DeferredResult>> output = new DeferredResult<>(20000L); + + if (user == null) { + output.setResult(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + return output; + } + + User fullUser = userRepository.findByEmail(user.getEmail()) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + UUID userId = fullUser.getId(); + + output.onTimeout(() -> output.setResult(ResponseEntity.status(HttpStatus.NOT_MODIFIED).build())); + + Optional latestSub = submissionRepository.findFirstByUserIdOrderByUpdatedAtDesc(userId); + boolean hasUpdates = lastUpdate == null || + (latestSub.isPresent() && latestSub.get().getUpdatedAt().isAfter(lastUpdate)); + + if (hasUpdates) { + output.setResult(ResponseEntity.ok(fetchUserSubmissions(userId))); + return output; + } + + waiters.computeIfAbsent(userId, k -> new ConcurrentLinkedQueue<>()).add(output); + + output.onCompletion(() -> { + ConcurrentLinkedQueue>>> queue = waiters.get(userId); + if (queue != null) { + queue.remove(output); + } + }); + + return output; + } + + @EventListener + public void onSubmissionUpdated(SubmissionUpdatedEvent event) { + ConcurrentLinkedQueue>>> queue = waiters.get(event.userId()); + + if (queue != null && !queue.isEmpty()) { + List freshData = fetchUserSubmissions(event.userId()); + + DeferredResult>> result; + while ((result = queue.poll()) != null) { + result.setResult(ResponseEntity.ok(freshData)); + } + } + } + + private List fetchUserSubmissions(UUID userId) { + return submissionRepository + .findAllByUserIdOrderByCreatedAtDesc(userId) + .stream() + .map(SubmissionResponseDTO::fromEntity) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionPollingService.java b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionPollingService.java index dc51dd1..f3a6555 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionPollingService.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionPollingService.java @@ -6,8 +6,14 @@ import com.codzilla.backend.controller.Sandbox.problem.ProblemRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; + +import com.codzilla.backend.controller.Sandbox.submission.SubmissionUpdatedEvent; + +import org.springframework.context.ApplicationEventPublisher; + import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; @@ -22,9 +28,17 @@ public class SubmissionPollingService { final private PolygonProblemService polygonProblemService; final private ProblemRepository problemRepository; + private final ApplicationEventPublisher eventPublisher; + + @Transactional(readOnly = true) + public List getPendingSubmissions() { + return submissionRepository.findAllByStatus(Submission.Status.IN_QUEUE); + } + @Scheduled(fixedDelay = 2000) public void pollStatuses() { - List pending = submissionRepository.findAllByStatus(Submission.Status.IN_QUEUE); + List pending = getPendingSubmissions(); + for (Submission sub : pending) { var response = judge0Client.getSubmissionStatus(sub.getJudge0Token()); if (response != null && response.getStatus() != null && response.getStatus().getId() > 2) { @@ -51,7 +65,6 @@ private void updateSubmissionStatus(Submission sub, Judge0Client.SubmissionRespo log.info("Submission {} finished with verdict: {}", sub.getId(), description); - - + eventPublisher.publishEvent(new SubmissionUpdatedEvent(sub.getUserId())); } } \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionRepository.java b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionRepository.java index e9cce27..f168331 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionRepository.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionRepository.java @@ -1,11 +1,16 @@ package com.codzilla.backend.controller.Sandbox.submission; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository public interface SubmissionRepository extends JpaRepository { List findAllByStatus(Submission.Status status); + + List findAllByUserIdOrderByCreatedAtDesc(UUID userId); + Optional findFirstByUserIdOrderByUpdatedAtDesc(UUID userId); } diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionResponseDTO.java b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionResponseDTO.java new file mode 100644 index 0000000..b974a4e --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionResponseDTO.java @@ -0,0 +1,23 @@ +package com.codzilla.backend.controller.Sandbox.submission; + +import java.time.LocalDateTime; + +public record SubmissionResponseDTO( + Long id, + Long problemId, + Integer languageId, + String status, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public static SubmissionResponseDTO fromEntity(Submission submission) { + return new SubmissionResponseDTO( + submission.getId(), + submission.getProblemId(), + submission.getLanguageId(), + submission.getStatus().name(), + submission.getCreatedAt(), + submission.getUpdatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionUpdatedEvent.java b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionUpdatedEvent.java new file mode 100644 index 0000000..3938746 --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionUpdatedEvent.java @@ -0,0 +1,5 @@ +package com.codzilla.backend.controller.Sandbox.submission; + +import java.util.UUID; + +public record SubmissionUpdatedEvent(UUID userId) {} \ No newline at end of file