diff --git a/backend/src/main/java/com/storycove/service/AsyncBackupExecutor.java b/backend/src/main/java/com/storycove/service/AsyncBackupExecutor.java new file mode 100644 index 0000000..d8c0f10 --- /dev/null +++ b/backend/src/main/java/com/storycove/service/AsyncBackupExecutor.java @@ -0,0 +1,125 @@ +package com.storycove.service; + +import com.storycove.entity.BackupJob; +import com.storycove.repository.BackupJobRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.UUID; + +/** + * Separate service for async backup execution. + * This is needed because @Async doesn't work when called from within the same class. + */ +@Service +public class AsyncBackupExecutor { + + private static final Logger logger = LoggerFactory.getLogger(AsyncBackupExecutor.class); + + @Value("${storycove.upload.dir:/app/images}") + private String uploadDir; + + @Autowired + private BackupJobRepository backupJobRepository; + + @Autowired + private DatabaseManagementService databaseManagementService; + + @Autowired + private LibraryService libraryService; + + /** + * Execute backup asynchronously. + * This method MUST be in a separate service class for @Async to work properly. + */ + @Async + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void executeBackupAsync(UUID jobId) { + logger.info("Async executor starting for job {}", jobId); + + Optional jobOpt = backupJobRepository.findById(jobId); + if (jobOpt.isEmpty()) { + logger.error("Backup job not found: {}", jobId); + return; + } + + BackupJob job = jobOpt.get(); + job.setStatus(BackupJob.BackupStatus.IN_PROGRESS); + job.setStartedAt(LocalDateTime.now()); + job.setProgressPercent(0); + backupJobRepository.save(job); + + try { + logger.info("Starting backup job {} for library {}", job.getId(), job.getLibraryId()); + + // Switch to the correct library + if (!job.getLibraryId().equals(libraryService.getCurrentLibraryId())) { + libraryService.switchToLibraryAfterAuthentication(job.getLibraryId()); + } + + // Create backup file + Path backupDir = Paths.get(uploadDir, "backups", job.getLibraryId()); + Files.createDirectories(backupDir); + + String filename = String.format("backup_%s_%s.%s", + job.getId().toString(), + LocalDateTime.now().toString().replaceAll(":", "-"), + job.getType() == BackupJob.BackupType.COMPLETE ? "zip" : "sql"); + + Path backupFile = backupDir.resolve(filename); + + job.setProgressPercent(10); + backupJobRepository.save(job); + + // Create the backup + Resource backupResource; + if (job.getType() == BackupJob.BackupType.COMPLETE) { + backupResource = databaseManagementService.createCompleteBackup(); + } else { + backupResource = databaseManagementService.createBackup(); + } + + job.setProgressPercent(80); + backupJobRepository.save(job); + + // Copy resource to permanent file + try (var inputStream = backupResource.getInputStream(); + var outputStream = Files.newOutputStream(backupFile)) { + inputStream.transferTo(outputStream); + } + + job.setProgressPercent(95); + backupJobRepository.save(job); + + // Set file info + job.setFilePath(backupFile.toString()); + job.setFileSizeBytes(Files.size(backupFile)); + job.setStatus(BackupJob.BackupStatus.COMPLETED); + job.setCompletedAt(LocalDateTime.now()); + job.setProgressPercent(100); + + logger.info("Backup job {} completed successfully. File size: {} bytes", + job.getId(), job.getFileSizeBytes()); + + } catch (Exception e) { + logger.error("Backup job {} failed", job.getId(), e); + job.setStatus(BackupJob.BackupStatus.FAILED); + job.setErrorMessage(e.getMessage()); + job.setCompletedAt(LocalDateTime.now()); + } finally { + backupJobRepository.save(job); + } + } +} diff --git a/backend/src/main/java/com/storycove/service/AsyncBackupService.java b/backend/src/main/java/com/storycove/service/AsyncBackupService.java index 84d0baa..19cbae7 100644 --- a/backend/src/main/java/com/storycove/service/AsyncBackupService.java +++ b/backend/src/main/java/com/storycove/service/AsyncBackupService.java @@ -8,7 +8,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; -import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,7 +16,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.sql.SQLException; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -35,108 +33,29 @@ public class AsyncBackupService { private BackupJobRepository backupJobRepository; @Autowired - private DatabaseManagementService databaseManagementService; - - @Autowired - private LibraryService libraryService; + private AsyncBackupExecutor asyncBackupExecutor; /** - * Start a backup job asynchronously + * Start a backup job asynchronously. + * This method returns immediately after creating the job record. */ @Transactional public BackupJob startBackupJob(String libraryId, BackupJob.BackupType type) { + logger.info("Creating backup job for library: {}, type: {}", libraryId, type); + BackupJob job = new BackupJob(libraryId, type); job = backupJobRepository.save(job); - // Force flush to ensure job is committed to DB before async execution - backupJobRepository.flush(); + logger.info("Backup job created with ID: {}. Starting async execution...", job.getId()); - // Start backup in background (async method will run in separate thread after this transaction commits) - executeBackupAsync(job.getId()); + // Start backup in background using separate service (ensures @Async works properly) + asyncBackupExecutor.executeBackupAsync(job.getId()); + + logger.info("Async backup execution triggered for job: {}", job.getId()); return job; } - /** - * Execute backup asynchronously - */ - @Async - @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW) - public void executeBackupAsync(UUID jobId) { - Optional jobOpt = backupJobRepository.findById(jobId); - if (jobOpt.isEmpty()) { - logger.error("Backup job not found: {}", jobId); - return; - } - - BackupJob job = jobOpt.get(); - job.setStatus(BackupJob.BackupStatus.IN_PROGRESS); - job.setStartedAt(LocalDateTime.now()); - job.setProgressPercent(0); - backupJobRepository.save(job); - - try { - logger.info("Starting backup job {} for library {}", job.getId(), job.getLibraryId()); - - // Switch to the correct library - if (!job.getLibraryId().equals(libraryService.getCurrentLibraryId())) { - libraryService.switchToLibraryAfterAuthentication(job.getLibraryId()); - } - - // Create backup file - Path backupDir = Paths.get(uploadDir, "backups", job.getLibraryId()); - Files.createDirectories(backupDir); - - String filename = String.format("backup_%s_%s.%s", - job.getId().toString(), - LocalDateTime.now().toString().replaceAll(":", "-"), - job.getType() == BackupJob.BackupType.COMPLETE ? "zip" : "sql"); - - Path backupFile = backupDir.resolve(filename); - - job.setProgressPercent(10); - backupJobRepository.save(job); - - // Create the backup - Resource backupResource; - if (job.getType() == BackupJob.BackupType.COMPLETE) { - backupResource = databaseManagementService.createCompleteBackup(); - } else { - backupResource = databaseManagementService.createBackup(); - } - - job.setProgressPercent(80); - backupJobRepository.save(job); - - // Copy resource to permanent file - try (var inputStream = backupResource.getInputStream(); - var outputStream = Files.newOutputStream(backupFile)) { - inputStream.transferTo(outputStream); - } - - job.setProgressPercent(95); - backupJobRepository.save(job); - - // Set file info - job.setFilePath(backupFile.toString()); - job.setFileSizeBytes(Files.size(backupFile)); - job.setStatus(BackupJob.BackupStatus.COMPLETED); - job.setCompletedAt(LocalDateTime.now()); - job.setProgressPercent(100); - - logger.info("Backup job {} completed successfully. File size: {} bytes", - job.getId(), job.getFileSizeBytes()); - - } catch (Exception e) { - logger.error("Backup job {} failed", job.getId(), e); - job.setStatus(BackupJob.BackupStatus.FAILED); - job.setErrorMessage(e.getMessage()); - job.setCompletedAt(LocalDateTime.now()); - } finally { - backupJobRepository.save(job); - } - } - /** * Get backup job status */