Skip to content

Commit d17febb

Browse files
committed
fix: repair scheduler logic, fix timezone handling and refactor backup events
1 parent 606d767 commit d17febb

File tree

10 files changed

+93
-45
lines changed

10 files changed

+93
-45
lines changed

src/main/java/com/kcn/hikvisionmanager/controller/MainWebController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public String index(HttpSession httpSession, Principal principal, Model model) {
3232
String sessionId = httpSession.getId();
3333
String username = (principal != null) ? principal.getName() : "Guest";
3434

35-
log.info("\uD83D\uDE46\uD83C\uDFFB\u200D\uFE0F User {} accessed index page, sessionId: {}", username, sessionId);
35+
log.info("\uD83D\uDE46\uD83C\uDFFB\u200D\uFE0F User {} get successfully logged in", username);
3636

3737
model.addAttribute("sessionId", sessionId);
3838
model.addAttribute("username", username);
Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
package com.kcn.hikvisionmanager.events.model;
22

3-
import com.kcn.hikvisionmanager.entity.BackupConfigurationEntity;
43
import com.kcn.hikvisionmanager.events.DomainEvent;
5-
import lombok.extern.slf4j.Slf4j;
64

75
import java.time.LocalDateTime;
86

9-
@Slf4j
10-
public record BackupTriggerEvent(
11-
BackupConfigurationEntity configuration
12-
) implements DomainEvent {
13-
7+
public record BackupTriggerEvent(String configId) implements DomainEvent {
148
@Override
159
public LocalDateTime getOccurredAt() {
1610
return LocalDateTime.now();
1711
}
18-
}
12+
}

src/main/java/com/kcn/hikvisionmanager/mapper/BackupConfigMapper.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,25 +74,41 @@ private String generateIdIfNull(String id) {
7474
return (id == null || id.isBlank()) ? "backup-" + System.currentTimeMillis() : id;
7575
}
7676

77+
/**
78+
* Builds CRON expression from user-friendly scheduling fields.
79+
*/
7780
/**
7881
* Builds CRON expression from user-friendly scheduling fields.
7982
*/
8083
public String generateCron(BackupConfigDTO dto) {
81-
if (dto.getTime() == null || dto.getTime().isBlank()) {
84+
// Validation for standard types
85+
if (dto.getScheduleType() != ScheduleType.CUSTOM && (dto.getTime() == null || dto.getTime().isBlank())) {
8286
throw new IllegalArgumentException("Time must be provided for schedule generation");
8387
}
8488

89+
// Handle CUSTOM type
90+
if (dto.getScheduleType() == ScheduleType.CUSTOM) {
91+
// For future 'cronExpression' field in DTO
92+
// For now, return a safe default or specific logic instead of crashing.
93+
// return dto.getCronExpression() != null ? dto.getCronExpression() : "0 0 0 * * *";
94+
return "0 0 0 * * *"; // Placeholder to prevent crash
95+
}
96+
8597
String[] timeParts = dto.getTime().split(":");
8698
int hour = Integer.parseInt(timeParts[0]);
8799
int minute = Integer.parseInt(timeParts[1]);
88100

89101
return switch (dto.getScheduleType()) {
90102
case DAILY -> String.format("0 %d %d * * *", minute, hour);
91103
case WEEKLY -> {
92-
String day = (dto.getDayOfWeek() != null ? dto.getDayOfWeek().substring(0, 3).toUpperCase() : "MON");
104+
// Fix: Handle null dayOfWeek safely
105+
String dayInput = dto.getDayOfWeek();
106+
String day = (dayInput != null && dayInput.length() >= 3)
107+
? dayInput.substring(0, 3).toUpperCase()
108+
: "MON"; // Default to Monday if missing
93109
yield String.format("0 %d %d ? * %s", minute, hour, day);
94110
}
95-
case CUSTOM -> throw new IllegalArgumentException("Custom schedule must define CRON manually");
111+
default -> throw new IllegalArgumentException("Unknown schedule type");
96112
};
97113
}
98114

src/main/java/com/kcn/hikvisionmanager/mapper/BackupJobMapper.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.kcn.hikvisionmanager.entity.BackupJobEntity;
44
import com.kcn.hikvisionmanager.dto.BackupJobDTO;
5-
import com.kcn.hikvisionmanager.util.TimeUtils;
65
import org.springframework.stereotype.Component;
76

87
/**
@@ -22,12 +21,8 @@ public BackupJobDTO toDTO(BackupJobEntity entity) {
2221
.cameraId(entity.getConfiguration() != null
2322
? entity.getConfiguration().getCameraId()
2423
: null)
25-
.startedAt(entity.getStartedAt() != null
26-
? TimeUtils.cameraUtcToLocal(entity.getStartedAt())
27-
: null)
28-
.endTime(entity.getCompletedAt() != null
29-
? TimeUtils.cameraUtcToLocal(entity.getCompletedAt())
30-
: null)
24+
.startedAt(entity.getStartedAt())
25+
.endTime(entity.getCompletedAt())
3126
.totalFiles(entity.getTotalRecordings())
3227
.completedFiles(entity.getCompletedRecordings())
3328
.totalBytes(entity.getTotalSizeBytes() != null ? entity.getTotalSizeBytes() : 0)

src/main/java/com/kcn/hikvisionmanager/sheduler/BackupScheduler.java renamed to src/main/java/com/kcn/hikvisionmanager/scheduler/BackupScheduler.java

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.kcn.hikvisionmanager.sheduler;
1+
package com.kcn.hikvisionmanager.scheduler;
22

33
import com.kcn.hikvisionmanager.entity.BackupConfigurationEntity;
44
import com.kcn.hikvisionmanager.events.model.BackupTriggerEvent;
@@ -9,6 +9,7 @@
99
import org.springframework.scheduling.annotation.Scheduled;
1010
import org.springframework.scheduling.support.CronExpression;
1111
import org.springframework.stereotype.Component;
12+
import org.springframework.transaction.annotation.Transactional;
1213

1314
import java.time.LocalDateTime;
1415
import java.util.List;
@@ -22,14 +23,17 @@ public class BackupScheduler {
2223
private final EventPublisherHelper publisher;
2324

2425
@Scheduled(fixedRate = 60000)
26+
@Transactional
2527
public void checkAndExecuteBackups() {
2628
List<BackupConfigurationEntity> configs = configRepository.findByEnabled(true);
2729
LocalDateTime now = LocalDateTime.now();
2830

31+
log.debug("🔍 Checking schedule for {} enabled configurations", configs.size());
32+
2933
for (BackupConfigurationEntity config : configs) {
3034
try {
3135
if (shouldExecuteBackup(config, now)) {
32-
publisher.publish(new BackupTriggerEvent(config));
36+
publisher.publish(new BackupTriggerEvent(config.getId()));
3337
}
3438
} catch (Exception e) {
3539
log.error("❌ Error scheduling backup for {}: {}", config.getName(), e.getMessage(), e);
@@ -40,26 +44,38 @@ public void checkAndExecuteBackups() {
4044
private boolean shouldExecuteBackup(BackupConfigurationEntity config, LocalDateTime now) {
4145
try {
4246
CronExpression cron = CronExpression.parse(config.getCronExpression());
43-
if (config.getNextRunAt() != null && config.getNextRunAt().isAfter(now)) {
47+
48+
// 1. Initialization: If nextRunAt is missing (new config), calculate it and wait
49+
if (config.getNextRunAt() == null) {
50+
LocalDateTime nextRun = cron.next(now);
51+
config.setNextRunAt(nextRun);
52+
53+
configRepository.save(config);
54+
55+
log.debug("🗓️ Schedule initialized for: {}, next run at: {}", config.getName(), nextRun);
4456
return false;
4557
}
58+
59+
// 2. If the scheduled time is in the future -> do nothing
60+
if (config.getNextRunAt().isAfter(now)) {
61+
log.debug("⏳ schedule for {}, nothing to do yet. ", config.getName());
62+
return false;
63+
}
64+
65+
// 3. Time has come (or passed) -> EXECUTE
66+
67+
// Calculate the NEXT run time from NOW to prevent infinite loops or double execution
4668
LocalDateTime nextRun = cron.next(now);
47-
LocalDateTime previousNext = config.getNextRunAt();
69+
config.setNextRunAt(nextRun);
4870

49-
boolean due =
50-
previousNext == null ||
51-
(now.isAfter(previousNext.minusMinutes(1)) && now.isBefore(previousNext.plusMinutes(1)));
71+
configRepository.save(config);
5272

53-
if (due) {
54-
config.setNextRunAt(nextRun);
55-
configRepository.save(config);
56-
return true;
57-
}
58-
return false;
73+
log.info("⏰ Time for backup: {} (next run scheduled for: {})", config.getName(), nextRun);
74+
return true;
5975

6076
} catch (Exception e) {
61-
log.error("Invalid cron for {}: {}", config.getId(), e.getMessage());
77+
log.error("Invalid cron for {}: {}", config.getId(), e.getMessage());
6278
return false;
6379
}
6480
}
65-
}
81+
}

src/main/java/com/kcn/hikvisionmanager/service/backup/BackupEventHandler.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.kcn.hikvisionmanager.entity.BackupRecordingEntity;
44
import com.kcn.hikvisionmanager.domain.BackupRecordingStatus;
55
import com.kcn.hikvisionmanager.events.model.*;
6+
import com.kcn.hikvisionmanager.repository.BackupConfigurationRepository;
67
import com.kcn.hikvisionmanager.repository.BackupJobRepository;
78
import com.kcn.hikvisionmanager.repository.BackupRecordingRepository;
89
import lombok.RequiredArgsConstructor;
@@ -23,6 +24,7 @@ public class BackupEventHandler {
2324

2425
private final BackupRecordingRepository backupRecordingRepository;
2526
private final BackupJobRepository backupJobRepository;
27+
private final BackupConfigurationRepository backupConfigRepository;
2628
private final BackupExecutor backupExecutor;
2729

2830
// ===== EVENT LISTENERS (RECORDINGS) =====
@@ -107,6 +109,24 @@ public void onDownloadFailed(RecordingDownloadFailedEvent event) {
107109

108110
// ===== EVENT LISTENERS (BACKUPS) =====
109111

112+
/**
113+
* Handle scheduled backup trigger
114+
*/
115+
@Async
116+
@EventListener
117+
public void onBackupTriggered(BackupTriggerEvent event) {
118+
log.info("\uD83D\uDC49 Scheduled backup triggered for config ID: {}", event.configId());
119+
120+
try {
121+
var config = backupConfigRepository.findById(event.configId())
122+
.orElseThrow(() -> new IllegalStateException("Config not found: " + event.configId()));
123+
backupExecutor.executeBackup(config);
124+
125+
} catch (Exception e) {
126+
log.error("❌ Failed to execute scheduled backup: {}", e.getMessage(), e);
127+
}
128+
}
129+
110130
/**
111131
* Finalize backup when entire batch completes
112132
*/

src/main/java/com/kcn/hikvisionmanager/service/backup/BackupExecutor.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public String executeBackup(BackupConfigurationEntity config) {
8181
backupConfigurationRepository.save(config);
8282

8383
// Create backup job in DB
84+
8485
BackupJobEntity backupJob = createBackupJob(
8586
backupJobId, config.getId(), now, backupDateRange, backupDir);
8687

@@ -195,7 +196,7 @@ private BackupJobEntity createBackupJob(
195196
.build();
196197

197198
backupJobRepository.save(backupJob);
198-
log.info(" Backup job created in DB: {}", backupJobId);
199+
log.info("\uD83D\uDCBE Backup job saved in DB");
199200

200201
return backupJob;
201202
}
@@ -257,7 +258,7 @@ private void startBatchDownload(
257258
backupJob.getId()
258259
);
259260

260-
log.info("🚀 Batch download started: {} (files will be saved to {})", batchId, backupDir);
261+
log.info("🚀 Backup download started, files will be saved to {}", backupDir);
261262
}
262263

263264
/**
@@ -322,8 +323,8 @@ public void finalizeBackup(String backupJobId) {
322323
backupJob.setTotalSizeBytes(totalSize);
323324
backupJobRepository.save(backupJob);
324325

325-
log.info("🎉 Backup finalized: {} ({}/{} recordings, {} total)",
326-
backupJobId, completed, backupJob.getTotalRecordings(),
326+
log.info("🎉 Backup finalized: {}/{} recordings, {} total",
327+
completed, backupJob.getTotalRecordings(),
327328
formatBytes(totalSize));
328329

329330
// Apply retention policy

src/main/java/com/kcn/hikvisionmanager/service/backup/BackupService.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public String triggerBackup(String configId) {
5454
BackupConfigurationEntity config = configRepository.findById(configId)
5555
.orElseThrow(() -> new BackupNotFoundException("Backup configuration not found: " + configId));
5656

57-
log.info("🔔 Triggering manual backup for config {}", config.getId());
57+
log.info("🔔 Triggering manual backup for config: {}", config.getName());
5858
return backupExecutor.executeBackup(config);
5959
}
6060

@@ -90,10 +90,16 @@ public BackupConfigDTO updateBackupConfig(String id, BackupConfigDTO dto) {
9090
existing.setCameraId(dto.getCameraId());
9191
existing.setRetentionDays(dto.getRetentionDays());
9292
existing.setEnabled(dto.isEnabled());
93-
existing.setCronExpression(configMapper.generateCron(dto));
93+
String newCron = configMapper.generateCron(dto);
94+
// Only reset next run if cron changed or it was disabled/re-enabled
95+
if (!newCron.equals(existing.getCronExpression()) || (!existing.isEnabled() && dto.isEnabled())) {
96+
existing.setCronExpression(newCron);
97+
existing.setNextRunAt(null); // Force scheduler to recalculate immediately
98+
log.info("🔄 Schedule changed/reset for: {}", dto.getName());
99+
}
94100

95101
configRepository.save(existing);
96-
log.info("✏️ Updated backup configuration: {}", id);
102+
log.info("✏️ Updated backup configuration: {}", dto.getName());
97103

98104
return configMapper.toDTO(existing);
99105
}

src/main/java/com/kcn/hikvisionmanager/service/download/DownloadJobQueue.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ private void executeDownload(DownloadJob job) {
9696
job.setStartedAt(LocalDateTime.now());
9797
repository.save(job);
9898

99-
log.info("\uD83D\uDE80 [{}] Starting download: {} (Job: {}, Method: {})",
99+
log.info("\uD83D\uDE80 [{}] Starting download: {} (Method: {})",
100100
Thread.currentThread().getName(),
101101
job.getFileName(),
102-
job.getJobId(),
102+
//job.getJobId(),
103103
config.getMethod().toUpperCase());
104104

105105
if(job.isBackupJob())

src/main/java/com/kcn/hikvisionmanager/service/download/HttpProgressListener.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ public void onComplete(Path filePath) {
8080
long downloadTime = System.currentTimeMillis() - startTime;
8181
double averageSpeed = calculateAverageSpeedMbps(job.getDownloadedBytes(), downloadTime);
8282

83-
log.info("✅ HTTP Download completed: {} (Job: {}) in {}, Avg Speed: {} Mbps",
84-
job.getFileName(), job.getJobId(), formatDuration(downloadTime), averageSpeed);
83+
log.info("✅ HTTP Download completed: {} in {}, Avg Speed: {} Mbps",
84+
job.getFileName(), formatDuration(downloadTime), averageSpeed);
8585

8686
if (job.isBackupJob()) {
8787
publisher.publishDownloadCompleted(job.getRecordingId(), job.getBatchId(), job.getActualFileSizeBytes());

0 commit comments

Comments
 (0)