Async REST API Using Spring Boot
Read in Vietnamese: Async REST API Using Spring Boot
Khi bạn có 1 hàm chạy async và bạn muốn check được hàm này đã chạy chưa, khi nào nó chạy xong để thực hiện 1 công việc khác
VD: Có 1 chức năng expport csv là 1 hàm chạy async, và có 1 request là nếu trong trường hợp data nhiều hàm export chạy quá 1p thì sẽ show 1 dialog thông báo export chưa chạy xong và sẽ gửi file csv qua email sau khi đã chạy xong
Còn ngược lại nếu hàm này chạy xong trước 1p thì sẽ thực hiện trả file csv trực tiếp về phía browser cho user download
ASYNC CONFIGURATION
@Configuration
@EnableAsync
public class AsyncConfig{
@Bean(name = "asyncTaskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("MyAsyncThread-");
executor.initialize();
return executor;
}
}
ASYNC SERVICE
@Autowired
private AsyncJobsManager asyncJobsManager;
@GetMapping(value = "/export-csv")
public ResponseEntity < JobsResponse > exportCSV() throws Exception {
long currentTime = System.currentTimeMillis();
String jobId = UUID.randomUUID().toString();
if (jobService.fetchJobById(jobId) != null) {
throw new Exeception("A job with same jobId already exists!");
}
CompletableFuture < JobsResponse > completableFuture = exportService.exportCSV(jobId, currentTime);
asyncJobsManager.putJob(jobId, completableFuture);
JobsResponse response = new JobsResponse(JobsStatus.SUBMITTED, jobId, currentTime);
return ResponseEntity.ok().body(response);
}
@GetMapping(path = "/votes/export-vote/check-status/{jobId}")
public ResponseEntity < JobsResponse > getVoteExportStatus(@PathVariable(name = "jobId") String jobId) throws Throwable {
log.debug("Rest request to check status of task export vote by jobId: {}", jobId);
return ResponseEntity.ok().body(voteService.checkExportVoteStatus(jobId));
}
@DeleteMapping(path = "/votes/export-vote/delete-jobs/{jobId}")
public ResponseEntity < JobsResponse > deleteJobExport(@PathVariable(name = "jobId") String jobId) {
log.debug("Rest request to delete job export vote by jobId: {}", jobId);
return ResponseEntity.ok().body(voteService.deleteJobExport(jobId));
}
@Async("asyncTaskExecutor")
public CompletableFuture<JobsResponse> exportCSV(String jobId, long startTime) throws Exception {
CompletableFuture<JobsResponse> task = new CompletableFuture<>();
Long fileId = this.prepareDataForExport(); // hàm này get data, tạo file csv lưu vào local serve và trả về id của file tương ứng được lưu trong bảng file
task.complete(new JobsResponse(JobsStatus.COMPLETE, jobId, fileId));
this.notificationEmailExportFinish(fileId, startTime, jobId); // send mail chứa file csv khi job chạy xong
return task;
}
private void notificationEmailExportFinish(Long fileId, long startTime, String jobId) {
if (fileId != null && jobId != null) {
Optional<FileDTO> file = fileService.findOne(fileId);
if (file.isPresent() && startTime < (System.currentTimeMillis() - Constants.EXPORT_WARNING_TIMES)) {
mailService.sendExportNotificationEmail(fileId);
voteService.deleteJobExport(jobId);
}
}
}
JobService
public JobsResponse checkExportVoteStatus(String jobId) throws Throwable {
log.debug("Start request to checkExportVoteStatus jobId: {}", jobId);
CompletableFuture < JobsResponse > jobStatus = fetchJobExportVoteById(jobId);
if (jobStatus == null) {
log.debug("checkExportVoteStatus jobId {} not exists", jobId);
throw new Exeception("checkExportVoteStatus, A job not exists");
}
if (!jobStatus.isDone()) {
return new JobsResponse(JobsStatus.IN_PROGRESS, jobId);
}
Throwable[] errors = new Throwable[1];
JobsResponse[] jobsResponses = new JobsResponse[1];
jobStatus.whenComplete((response, ex) - > {
if (ex != null) {
errors[0] = ex.getCause();
} else {
jobsResponses[0] = response;
}
});
if (errors[0] != null) {
log.error("checkExportVoteStatus, Job {} failed with exception: {}", jobId, errors[0]);
throw errors[0];
}
log.debug("End request to checkExportVoteStatus jobId: {}, response: {}", jobId, jobsResponses[0]);
return jobsResponses[0];
}
public CompletableFuture < JobsResponse > fetchJobExportById(String jobId) {
CompletableFuture < JobsResponse > completableFuture = (CompletableFuture < JobsResponse > ) asyncJobsManager
.getJob(jobId);
return completableFuture;
}
public JobsResponse deleteJobExport(String jobId) {
log.debug("Start request to deleteJobExport jobId: {}", jobId);
CompletableFuture < JobsResponse > jobStatus = fetchJobExportById(jobId);
if (jobStatus == null) {
log.debug("deleteJobExport jobId {} not exists", jobId);
throw new Exeception("deleteJobExport, A job not exists");
}
if (!jobStatus.isDone()) {
return new JobsResponse(JobsStatus.IN_PROGRESS, jobId);
}
jobStatus.whenComplete((response, ex) - > {
if (ex != null) {
log.error("deleteJobExport Job {} failed with exception: {}", jobId, ex);
}
asyncJobsManager.removeJob(jobId);
});
JobsResponse response = new JobsResponse(JobsStatus.DELETED, jobId);
log.debug("End request to deleteJobExport jobId: {}, response: {}", jobId, response);
return response;
}
Angular component UI
trong vòng 10s thì sẽ call api check status 1 lần nếu job chạy xong thì sẽ dùng call api và download file xuống, còn nếu chưa xong thì show dialog thông báo
export const EXPORT_WARNING_TIMES = 60 * 1000; // 60 seconds
export const EXPORT_FETCH_TIMES = 10 * 1000; // 10 seconds
exportVote() {
this.voteService.export().subscribe(res => {
if (res.body.requestStatus === 'SUBMITTED') {
let count = 0;
const myInterval = window.setInterval(() => {
count++;
this.fetchStatusExport(res.body.jobId).subscribe(data => {
if (data.body.requestStatus === 'COMPLETE') {
this.fileService.downloadExportCsv(data.body.fileId).subscribe();
window.clearInterval(myInterval);
// delete job export vote
this.voteService.deleteJobExportVote(data.body.jobId).subscribe();
this.turnOffSpinner(withTotal);
} else if (count === EXPORT_WARNING_TIMES / EXPORT_FETCH_TIMES) {
window.clearInterval(myInterval);
// show modal warning
const modalRef = this.modalService.open(ConfirmCommonDialogComponent, {
backdrop: 'static',
centered: true
});
modalRef.componentInstance.titleProperty = 'confirm.dialog.exportVotes.title';
modalRef.componentInstance.questionProperty = 'confirm.dialog.exportVotes.question';
modalRef.componentInstance.displayCancelButton = false;
this.turnOffSpinner(withTotal);
}
});
}, EXPORT_FETCH_TIMES);
this.exportInterVal.push({
myInterval
});
}
});
}