diff --git a/src/main/java/life/mosu/mosuserver/application/exam/cache/AtomicExamQuotaDecrementOperator.java b/src/main/java/life/mosu/mosuserver/application/exam/cache/AtomicExamQuotaDecrementOperator.java index 85679e97..b246598c 100644 --- a/src/main/java/life/mosu/mosuserver/application/exam/cache/AtomicExamQuotaDecrementOperator.java +++ b/src/main/java/life/mosu/mosuserver/application/exam/cache/AtomicExamQuotaDecrementOperator.java @@ -1,6 +1,8 @@ package life.mosu.mosuserver.application.exam.cache; import java.util.List; +import java.util.Map; +import java.util.Objects; import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; import life.mosu.mosuserver.infra.persistence.redis.operator.VoidCacheAtomicOperator; @@ -15,12 +17,17 @@ public class AtomicExamQuotaDecrementOperator implements VoidCacheAtomicOperator private final RedisTemplate redisTemplate; private final DefaultRedisScript decrementScript; + public AtomicExamQuotaDecrementOperator( RedisTemplate redisTemplate, - @Qualifier("decrementExamQuotaScript") DefaultRedisScript decrementScript + + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + @Qualifier("examLuaScripts") + Map> examLuaScripts ) { this.redisTemplate = redisTemplate; - this.decrementScript = decrementScript; + this.decrementScript = Objects.requireNonNull(examLuaScripts.get("decrementQuota"), + "Redis script 'decrementQuota' not found"); } @Override diff --git a/src/main/java/life/mosu/mosuserver/application/exam/cache/AtomicExamQuotaIncrementOperator.java b/src/main/java/life/mosu/mosuserver/application/exam/cache/AtomicExamQuotaIncrementOperator.java index ef7657b2..bfd4d235 100644 --- a/src/main/java/life/mosu/mosuserver/application/exam/cache/AtomicExamQuotaIncrementOperator.java +++ b/src/main/java/life/mosu/mosuserver/application/exam/cache/AtomicExamQuotaIncrementOperator.java @@ -1,26 +1,34 @@ package life.mosu.mosuserver.application.exam.cache; import java.util.List; +import java.util.Map; +import java.util.Objects; import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; import life.mosu.mosuserver.infra.persistence.redis.operator.VoidCacheAtomicOperator; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; @Component +@Slf4j public class AtomicExamQuotaIncrementOperator implements VoidCacheAtomicOperator { private final RedisTemplate redisTemplate; - private final DefaultRedisScript decrementScript; + private final DefaultRedisScript incrementScript; public AtomicExamQuotaIncrementOperator( RedisTemplate redisTemplate, - @Qualifier("incrementExamQuotaScript") DefaultRedisScript decrementScript + + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + @Qualifier("examLuaScripts") + Map> examLuaScripts ) { this.redisTemplate = redisTemplate; - this.decrementScript = decrementScript; + this.incrementScript = Objects.requireNonNull(examLuaScripts.get("incrementQuota"), + "Redis script 'incrementQuota' not found"); } @Override @@ -36,7 +44,7 @@ public String getActionName() { @Override public void execute(String key) { try { - Long result = redisTemplate.execute(decrementScript, List.of( + Long result = redisTemplate.execute(incrementScript, List.of( ExamQuotaPrefix.CURRENT_APPLICATIONS.with(key), ExamQuotaPrefix.MAX_CAPACITY.with(key) )); diff --git a/src/main/java/life/mosu/mosuserver/application/exam/cache/ExamQuotaCacheManager.java b/src/main/java/life/mosu/mosuserver/application/exam/cache/ExamQuotaCacheManager.java index a0056cc3..9f8b3ed9 100644 --- a/src/main/java/life/mosu/mosuserver/application/exam/cache/ExamQuotaCacheManager.java +++ b/src/main/java/life/mosu/mosuserver/application/exam/cache/ExamQuotaCacheManager.java @@ -14,6 +14,7 @@ import life.mosu.mosuserver.infra.persistence.redis.operator.VoidCacheAtomicOperator; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -29,7 +30,9 @@ public ExamQuotaCacheManager( CacheWriter cacheWriter, CacheReader cacheReader, - @Qualifier("examCacheAtomicOperatorMap") + @Lazy + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + @Qualifier("examQuotaCacheAtomicOperatorMap") Map> cacheAtomicOperatorMap, ExamJpaRepository examJpaRepository ) { diff --git a/src/main/java/life/mosu/mosuserver/global/config/ExamQuotaAtomicOperationConfig.java b/src/main/java/life/mosu/mosuserver/global/config/ExamQuotaAtomicOperationConfig.java deleted file mode 100644 index e7da0024..00000000 --- a/src/main/java/life/mosu/mosuserver/global/config/ExamQuotaAtomicOperationConfig.java +++ /dev/null @@ -1,66 +0,0 @@ -package life.mosu.mosuserver.global.config; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import life.mosu.mosuserver.application.exam.cache.AtomicExamQuotaDecrementOperator; -import life.mosu.mosuserver.application.exam.cache.AtomicExamQuotaIncrementOperator; -import life.mosu.mosuserver.infra.persistence.redis.operator.CacheAtomicOperator; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.Resource; -import org.springframework.data.redis.core.script.DefaultRedisScript; - -@Configuration -public class ExamQuotaAtomicOperationConfig { - - @Value("classpath:scripts/decrement_exam_quota.lua") - private Resource decrementScript; - - @Value("classpath:scripts/increment_exam_quota.lua") - private Resource incrementScript; - - @Bean - @Qualifier("decrementExamQuotaScript") - public DefaultRedisScript decrementExamQuotaScript() { - DefaultRedisScript script = new DefaultRedisScript<>(); - script.setResultType(Long.class); - try { - String lua = new String(decrementScript.getInputStream().readAllBytes(), - StandardCharsets.UTF_8); - script.setScriptText(lua); - } catch (IOException e) { - throw new RuntimeException("Failed to load decrement_exam_quota.lua", e); - } - return script; - } - - @Bean - @Qualifier("incrementExamQuotaScript") - public DefaultRedisScript incrementExamQuotaScript() { - DefaultRedisScript script = new DefaultRedisScript<>(); - script.setResultType(Long.class); - try { - String lua = new String(incrementScript.getInputStream().readAllBytes(), - StandardCharsets.UTF_8); - script.setScriptText(lua); - } catch (IOException e) { - throw new RuntimeException("Failed to load increment_exam_quota.lua", e); - } - return script; - } - - @Bean - @Qualifier("examCacheAtomicOperatorMap") - public Map> examCacheAtomicOperatorMap( - AtomicExamQuotaIncrementOperator incrementOp, - AtomicExamQuotaDecrementOperator decrementOp - ) { - return Map.of( - incrementOp.getActionName(), incrementOp, - decrementOp.getActionName(), decrementOp - ); - } -} diff --git a/src/main/java/life/mosu/mosuserver/infra/config/AtomicOperatorAutoRegistrar.java b/src/main/java/life/mosu/mosuserver/infra/config/AtomicOperatorAutoRegistrar.java new file mode 100644 index 00000000..a1524f00 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/config/AtomicOperatorAutoRegistrar.java @@ -0,0 +1,50 @@ +package life.mosu.mosuserver.infra.config; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import life.mosu.mosuserver.infra.persistence.redis.operator.CacheAtomicOperator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Slf4j +public class AtomicOperatorAutoRegistrar implements SmartInitializingSingleton { + + private final ConfigurableListableBeanFactory beanFactory; + + public AtomicOperatorAutoRegistrar(ConfigurableListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public void afterSingletonsInstantiated() { + Map allOperators = beanFactory.getBeansOfType( + CacheAtomicOperator.class); + + Map> grouped = allOperators.values().stream() + .collect(Collectors.groupingBy(CacheAtomicOperator::getName)); + + DefaultListableBeanFactory registry = (DefaultListableBeanFactory) beanFactory; + for (Map.Entry> entry : grouped.entrySet()) { + String domain = entry.getKey(); + List operators = entry.getValue(); + + Map mapValue = operators.stream() + .collect(Collectors.toMap(CacheAtomicOperator::getActionName, + Function.identity())); + + String beanName = domain + "CacheAtomicOperatorMap"; + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); + beanDefinition.setBeanClass(Map.class); + beanDefinition.setInstanceSupplier(() -> mapValue); + + registry.registerBeanDefinition(beanName, beanDefinition); + } + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/persistence/redis/support/LuaScriptsFunctionalRegistrar.java b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/support/LuaScriptsFunctionalRegistrar.java new file mode 100644 index 00000000..b80a4bdb --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/persistence/redis/support/LuaScriptsFunctionalRegistrar.java @@ -0,0 +1,90 @@ +package life.mosu.mosuserver.infra.persistence.redis.support; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.data.redis.core.script.DefaultRedisScript; + +/** + * FQCN + */ +@Slf4j +public class LuaScriptsFunctionalRegistrar implements + ApplicationContextInitializer { + + private static final String SCRIPT_BASE_PATH = "classpath:scripts/"; + private static final String SCRIPT_PATH_PREFIX = "/scripts/"; + + @Override + public void initialize(GenericApplicationContext context) { + try { + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources(SCRIPT_BASE_PATH + "**/*.lua"); + + Map>> domainScriptsMap = new HashMap<>(); + + for (Resource resource : resources) { + String path = resource.getURL().getPath(); + int idx = path.lastIndexOf(SCRIPT_PATH_PREFIX); + if (idx < 0) { + continue; + } + + String relativePath = path.substring(idx + SCRIPT_PATH_PREFIX.length()); + String[] parts = relativePath.split("/"); + if (parts.length < 2) { + continue; + } + + String domain = parts[0]; + String filename = parts[1]; + String scriptName = toCamelCase(filename.replace(".lua", "")); + + DefaultRedisScript script = new DefaultRedisScript<>(); + script.setResultType(Long.class); + + try (InputStream is = resource.getInputStream()) { + String lua = new String(is.readAllBytes(), StandardCharsets.UTF_8); + script.setScriptText(lua); + } + + domainScriptsMap + .computeIfAbsent(domain, k -> new HashMap<>()) + .put(scriptName, script); + } + + for (Map.Entry>> entry : domainScriptsMap.entrySet()) { + String beanName = entry.getKey() + "LuaScripts"; + log.info("Registering Lua scripts for domain: {}", beanName); + Map> scripts = entry.getValue(); + + String keys = String.join(", ", scripts.keySet()); + log.info("Lua script keys: [{}]", keys); + + context.registerBean(beanName, Map.class, () -> scripts); + } + } catch (Exception e) { + throw new RuntimeException("Failed to load Lua scripts", e); + } + } + + private String toCamelCase(String snakeCase) { + StringBuilder result = new StringBuilder(); + boolean toUpper = false; + for (char c : snakeCase.toCharArray()) { + if (c == '_') { + toUpper = true; + } else { + result.append(toUpper ? Character.toUpperCase(c) : c); + toUpper = false; + } + } + return result.toString(); + } +} diff --git a/src/main/resources/META-INF/.gitkeep b/src/main/resources/META-INF/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..f508cd52 --- /dev/null +++ b/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.context.ApplicationContextInitializer=\ +life.mosu.mosuserver.infra.persistence.redis.support.LuaScriptsFunctionalRegistrar \ No newline at end of file diff --git a/src/main/resources/scripts/.gitkeep b/src/main/resources/scripts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/scripts/decrement_exam_quota.lua b/src/main/resources/scripts/exam/decrement_quota.lua similarity index 100% rename from src/main/resources/scripts/decrement_exam_quota.lua rename to src/main/resources/scripts/exam/decrement_quota.lua diff --git a/src/main/resources/scripts/increment_exam_quota.lua b/src/main/resources/scripts/exam/increment_quota.lua similarity index 100% rename from src/main/resources/scripts/increment_exam_quota.lua rename to src/main/resources/scripts/exam/increment_quota.lua