package com.nanri.aiimage.config; import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.modules.task.service.TaskDistributedLockService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @Configuration @RequiredArgsConstructor public class TaskOperationLockConfig implements WebMvcConfigurer { private static final String LOCK_ATTRIBUTE = TaskOperationLockConfig.class.getName() + ".LOCK"; private final TaskDistributedLockService taskDistributedLockService; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new TaskOperationLockInterceptor(taskDistributedLockService)) .addPathPatterns("/api/**"); } public static boolean releaseRequestLock(HttpServletRequest request) { if (request == null) { return false; } Object lockHandle = request.getAttribute(LOCK_ATTRIBUTE); if (lockHandle instanceof TaskDistributedLockService.LockHandle handle) { request.removeAttribute(LOCK_ATTRIBUTE); handle.close(); return true; } return false; } @Slf4j private static final class TaskOperationLockInterceptor implements HandlerInterceptor { private static final Pattern TASK_PATH = Pattern.compile(".*/api/([^/]+)/tasks/(\\d+)(?:/.*)?$"); private static final Pattern TASK_RESULT_PATH = Pattern.compile(".*/api/([^/]+)/tasks/(\\d+)/result/?$"); private static final Set MUTATING_METHODS = Set.of("POST", "PUT", "PATCH", "DELETE"); private static final long RESULT_SUBMIT_WAIT_MILLIS = 30 * 1000L; private static final long DELETE_WAIT_MILLIS = 60 * 1000L; private final TaskDistributedLockService taskDistributedLockService; private TaskOperationLockInterceptor(TaskDistributedLockService taskDistributedLockService) { this.taskDistributedLockService = taskDistributedLockService; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!MUTATING_METHODS.contains(request.getMethod())) { return true; } String uri = request.getRequestURI(); if ("POST".equals(request.getMethod()) && TASK_RESULT_PATH.matcher(uri == null ? "" : uri).matches()) { return true; } Matcher matcher = TASK_PATH.matcher(uri == null ? "" : uri); if (!matcher.matches()) { return true; } String moduleType = matcher.group(1).trim().toUpperCase(Locale.ROOT).replace('-', '_'); Long taskId = Long.valueOf(matcher.group(2)); long waitMillis = resolveWaitMillis(request.getMethod(), uri); TaskDistributedLockService.LockHandle lockHandle = taskDistributedLockService.acquire(moduleType, taskId, waitMillis); if (lockHandle == null) { log.info("[task-operation-lock] rejected busy task operation method={} uri={} moduleType={} taskId={} waitMillis={}", request.getMethod(), uri, moduleType, taskId, waitMillis); throw new BusinessException(40902, "TASK_LOCK_BUSY"); } request.setAttribute(LOCK_ATTRIBUTE, lockHandle); return true; } private long resolveWaitMillis(String method, String uri) { if ("POST".equals(method) && TASK_RESULT_PATH.matcher(uri == null ? "" : uri).matches()) { return RESULT_SUBMIT_WAIT_MILLIS; } if ("DELETE".equals(method)) { return DELETE_WAIT_MILLIS; } return TaskDistributedLockService.DEFAULT_WAIT_MILLIS; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { releaseRequestLock(request); } } }