This is an automated email from the ASF dual-hosted git repository. liuhongyu pushed a commit to branch feat/refactor in repository https://gitbox.apache.org/repos/asf/shenyu.git
commit ffd7cedd4552af74bb43491decf3dd94aa525205 Author: liuhy <[email protected]> AuthorDate: Thu Jan 8 16:20:43 2026 +0800 feat: implement enhanced caching and routing mechanisms with adaptive strategies --- .../src/main/resources/application-enhanced.yml | 225 +++++++++++++++ .../plugin/base/EnhancedAbstractShenyuPlugin.java | 227 +++++++++++++++ .../base/cache/DefaultSmartCacheManager.java | 310 +++++++++++++++++++++ .../plugin/base/cache/HybridCacheInstance.java | 106 +++++++ .../shenyu/plugin/base/cache/SmartCacheConfig.java | 189 +++++++++++++ .../plugin/base/cache/SmartCacheManager.java | 126 +++++++++ .../plugin/base/cache/TrieCacheInstance.java | 86 ++++++ .../base/config/PluginBaseAutoConfiguration.java | 104 +++++++ .../base/context/PluginExecutionContext.java | 174 ++++++++++++ .../shenyu/plugin/base/metrics/MetricsHelper.java | 98 +++++++ .../plugin/base/metrics/NoOpMetricsHelper.java | 42 +++ .../plugin/base/route/DefaultRouteResolver.java | 199 +++++++++++++ .../shenyu/plugin/base/route/RouteResolver.java | 66 +++++ .../shenyu/plugin/base/route/RouteResult.java | 95 +++++++ .../src/main/resources/META-INF/spring.factories | 2 + .../base/context/PluginExecutionContextTest.java | 166 +++++++++++ .../base/route/DefaultRouteResolverTest.java | 226 +++++++++++++++ 17 files changed, 2441 insertions(+) diff --git a/shenyu-bootstrap/src/main/resources/application-enhanced.yml b/shenyu-bootstrap/src/main/resources/application-enhanced.yml new file mode 100644 index 0000000000..f69a74016b --- /dev/null +++ b/shenyu-bootstrap/src/main/resources/application-enhanced.yml @@ -0,0 +1,225 @@ +# ShenYu Enhanced Configuration - Phase 1 Optimized +# This configuration enables the performance improvements from the enhanced architecture + +server: + port: 9195 + address: 0.0.0.0 + compression: + enabled: true + min-response-size: 1MB + +spring: + main: + allow-bean-definition-overriding: true + application: + name: shenyu-bootstrap + codec: + # Increased for better performance with large requests + max-in-memory-size: 8MB # Enhanced from 2MB + cloud: + discovery: + enabled: false + +# Enhanced ShenYu Configuration +shenyu: + # Core system configuration + core: + namespace: ${SHENYU_NAMESPACE:649330b6-c2d7-4edc-be8e-8a54df9eb385} + mode: ${SHENYU_MODE:standalone} # standalone, cluster + + # Enhanced performance configuration + performance: + # Smart caching with adaptive strategies + cache: + enabled: true + strategy: adaptive # adaptive, lru_only, trie_only, hybrid + + # Selector cache configuration + selector: + strategy: adaptive + initialCapacity: 10000 + maximumSize: 65536 + expireAfterWrite: 30m + expireAfterAccess: 10m + + # Rule cache configuration + rule: + strategy: adaptive + initialCapacity: 20000 + maximumSize: 131072 + expireAfterWrite: 30m + expireAfterAccess: 10m + + # Plugin metadata cache + plugin: + strategy: lru_only + initialCapacity: 1000 + maximumSize: 10000 + expireAfterWrite: 60m + + # Adaptive behavior configuration + adaptive: + switchThreshold: 1000 + hitRatioThreshold: 0.8 + evaluationInterval: 5m + autoOptimization: true + + # Netty performance tuning + netty: + enabled: true + bossThreads: 1 + workerThreads: ${SHENYU_WORKER_THREADS:8} # Enhanced: configurable via env + maxConnections: 10000 + backlog: 1024 + + # Enhanced thread pool configuration + threadPool: + enabled: true # Enhanced: enabled by default + coreSize: ${SHENYU_CORE_THREADS:200} + maxSize: ${SHENYU_MAX_THREADS:2000} + queueCapacity: ${SHENYU_QUEUE_CAPACITY:10000} + keepAliveTime: 60s + threadNamePrefix: "shenyu-exec-" + + # Enhanced observability configuration + observability: + # Comprehensive metrics collection + metrics: + enabled: true # Enhanced: enabled by default + provider: prometheus + endpoint: /actuator/prometheus + collectInterval: 15s + + # Detailed metric categories + categories: + plugin_execution: true + cache_performance: true + route_matching: true + error_tracking: true + jvm_metrics: true + + # Custom labels for better monitoring + labels: + application: ${spring.application.name} + instance: ${HOSTNAME:${random.value}} + version: ${shenyu.version:2.7.1-enhanced} + environment: ${SHENYU_ENV:development} + + # Enhanced logging configuration + logging: + enabled: true + level: ${SHENYU_LOG_LEVEL:INFO} + + # Structured logging patterns + patterns: + request: "[%thread] %X{traceId:-N/A} %X{pluginName:-system} - %msg%n" + plugin: "[%thread] %X{traceId:-N/A} [%X{pluginName}] - %msg%n" + cache: "[%thread] %X{traceId:-N/A} [CACHE-%X{cacheType}] - %msg%n" + + # Log sampling for high-traffic scenarios + sampling: + enabled: true + rate: 0.1 # Log 10% of requests + + # Distributed tracing + tracing: + enabled: ${SHENYU_TRACING_ENABLED:false} + provider: ${SHENYU_TRACING_PROVIDER:zipkin} # zipkin, jaeger, otel + samplingProbability: 0.1 + + # Enhanced health checking + health: + enabled: true + paths: + - /actuator/health + - /health_check + + # Deep health checks + checks: + cache: true + database: true + upstream: true + memory: true + + # Health check thresholds + thresholds: + memoryUsagePercent: 90 + cacheHitRatio: 0.5 + errorRate: 0.05 + + # Enhanced upstream checking + upstreamCheck: + enabled: true # Enhanced: enabled by default + poolSize: ${SHENYU_UPSTREAM_POOL_SIZE:20} # Enhanced: increased pool size + timeout: ${SHENYU_UPSTREAM_TIMEOUT:3000} + healthyThreshold: 2 # Enhanced: stricter health requirements + unhealthyThreshold: 3 + interval: ${SHENYU_UPSTREAM_INTERVAL:5000} + circuitBreakerEnabled: true # Enhanced: circuit breaker support + + # Smart plugin management + plugins: + # Intelligent defaults based on usage patterns + defaults: + globalPlugin: { enabled: true, order: 0 } + signPlugin: { enabled: true, order: 10 } + wafPlugin: { enabled: true, order: 20 } + rateLimiterPlugin: { enabled: true, order: 30 } + dividePlugin: { enabled: true, order: 100 } + springCloudPlugin: { enabled: false, order: 200 } + dubboPlugin: { enabled: false, order: 300 } + responsePlugin: { enabled: true, order: 1000 } + + # Plugin auto-discovery and loading + discovery: + enabled: true + scanPackages: + - "org.apache.shenyu.plugin" + - "com.custom.shenyu.plugin" + + # Legacy compatibility (for migration period) + legacy: + selectorMatchCache: + cache: + enabled: true # Bridged to new cache system + initialCapacity: 10000 + maximumSize: 10000 + ruleMatchCache: + cache: + enabled: true # Bridged to new cache system + initialCapacity: 10000 + maximumSize: 65536 + +# Spring Boot Actuator configuration for monitoring +management: + endpoints: + web: + exposure: + include: + - 'health' + - 'info' + - 'metrics' + - 'prometheus' + - 'caches' + - 'threaddump' + - 'heapdump' + endpoint: + health: + show-details: always + show-components: always + metrics: + enabled: true + prometheus: + enabled: true + +# Enhanced logging configuration +logging: + level: + root: ${SHENYU_LOG_LEVEL:INFO} + org.apache.shenyu: INFO + org.apache.shenyu.plugin.base.cache: DEBUG # Enhanced cache logging + org.apache.shenyu.plugin.base.metrics: DEBUG # Enhanced metrics logging + org.apache.shenyu.plugin.base.route: DEBUG # Enhanced routing logging + pattern: + console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %X{traceId:-N/A} - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} %X{traceId:-N/A} %X{pluginName:-system} - %msg%n" \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/EnhancedAbstractShenyuPlugin.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/EnhancedAbstractShenyuPlugin.java new file mode 100644 index 0000000000..f98e0a3d97 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/EnhancedAbstractShenyuPlugin.java @@ -0,0 +1,227 @@ +package org.apache.shenyu.plugin.base; + +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.plugin.api.ShenyuPlugin; +import org.apache.shenyu.plugin.api.ShenyuPluginChain; +import org.apache.shenyu.plugin.base.cache.BaseDataCache; +import org.apache.shenyu.plugin.base.context.PluginExecutionContext; +import org.apache.shenyu.plugin.base.metrics.MetricsHelper; +import org.apache.shenyu.plugin.base.route.RouteResolver; +import org.apache.shenyu.plugin.base.route.RouteResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.Objects; + +/** + * Enhanced AbstractShenyuPlugin with separated responsibilities. + * + * Key improvements: + * 1. Simplified execute() method using template method pattern + * 2. Delegated routing logic to RouteResolver + * 3. Built-in metrics collection + * 4. Cleaner error handling + * 5. Lazy initialization of dependencies + * + * Backward compatibility: This class maintains the same public interface + * as the original AbstractShenyuPlugin, ensuring existing plugins work without changes. + */ +public abstract class EnhancedAbstractShenyuPlugin implements ShenyuPlugin { + + private static final Logger LOG = LoggerFactory.getLogger(EnhancedAbstractShenyuPlugin.class); + + // Lazy-initialized dependencies + private volatile RouteResolver routeResolver; + private volatile MetricsHelper metricsHelper; + private volatile boolean initialized = false; + + /** + * Template method for plugin execution. + * + * This method implements the main execution flow: + * 1. Check plugin enabled status + * 2. Create execution context + * 3. Resolve routing (selector + rule) + * 4. Execute plugin logic + * 5. Record metrics and handle errors + */ + @Override + public final Mono<Void> execute(final ServerWebExchange exchange, final ShenyuPluginChain chain) { + // Lazy initialization on first use + if (!initialized) { + initializeDependencies(); + } + + final String pluginName = named(); + + return Mono.just(pluginName) + .flatMap(this::checkPluginEnabled) + .filter(enabled -> enabled) + .flatMap(enabled -> createExecutionContext(exchange)) + .flatMap(context -> executeWithContext(context, chain)) + .switchIfEmpty(chain.execute(exchange)) // Plugin disabled or not applicable + .doOnSuccess(v -> metricsHelper.recordSuccess(pluginName)) + .doOnError(error -> { + metricsHelper.recordError(pluginName, error); + LOG.error("Plugin [{}] execution failed", pluginName, error); + }) + .onErrorResume(error -> handlePluginError(exchange, chain, error)); + } + + /** + * Core plugin execution logic with context. + */ + private Mono<Void> executeWithContext(PluginExecutionContext context, ShenyuPluginChain chain) { + final String pluginName = named(); + final long startTime = System.nanoTime(); + + return routeResolver.resolveRoute(context, pluginName) + .flatMap(routeResult -> { + if (!routeResult.isMatched()) { + return handleNoMatch(context, chain); + } + + // Record metrics for successful route resolution + metricsHelper.recordRouteMatch(pluginName, routeResult.getMatchType()); + + // Execute plugin business logic + return executePluginLogic(context, chain, routeResult); + }) + .doFinally(signal -> { + long duration = System.nanoTime() - startTime; + metricsHelper.recordExecutionTime(pluginName, duration); + }) + .contextWrite(ctx -> ctx.put("pluginName", pluginName)); + } + + /** + * Execute the actual plugin business logic. + */ + private Mono<Void> executePluginLogic( + PluginExecutionContext context, + ShenyuPluginChain chain, + RouteResult routeResult) { + + SelectorData selector = routeResult.getSelector().orElse(null); + RuleData rule = routeResult.getRule().orElse(createDefaultRule(selector)); + + // Call the abstract method that subclasses implement + return Mono.fromRunnable(() -> printExecutionLog(selector, rule, named())) + .then(doExecute(context.getExchange(), chain, selector, rule)); + } + + /** + * Abstract method that subclasses must implement. + * This maintains backward compatibility with the original interface. + */ + protected abstract Mono<Void> doExecute( + ServerWebExchange exchange, + ShenyuPluginChain chain, + SelectorData selector, + RuleData rule); + + // === Hook Methods for Customization === + + /** + * Handle scenarios where no route is matched. + * Subclasses can override for custom behavior. + */ + protected Mono<Void> handleNoMatch(PluginExecutionContext context, ShenyuPluginChain chain) { + return chain.execute(context.getExchange()); + } + + /** + * Handle plugin execution errors. + * Subclasses can override for custom error handling. + */ + protected Mono<Void> handlePluginError( + ServerWebExchange exchange, + ShenyuPluginChain chain, + Throwable error) { + // Default: continue chain execution on error + return chain.execute(exchange); + } + + /** + * Create execution context from exchange. + * Subclasses can override to add custom context data. + */ + protected Mono<PluginExecutionContext> createExecutionContext(ServerWebExchange exchange) { + return Mono.just(PluginExecutionContext.fromExchange(exchange)); + } + + // === Helper Methods === + + /** + * Check if plugin is enabled. + */ + private Mono<Boolean> checkPluginEnabled(String pluginName) { + return Mono.fromCallable(() -> { + PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName); + return Objects.nonNull(pluginData) && pluginData.getEnabled(); + }); + } + + /** + * Create default rule when selector doesn't continue to rules. + */ + private RuleData createDefaultRule(SelectorData selector) { + if (selector == null) { + return null; + } + + RuleData rule = new RuleData(); + rule.setSelectorId(selector.getId()); + rule.setPluginName(selector.getPluginName()); + rule.setId("default_rule"); + rule.setName("Default Rule"); + return rule; + } + + /** + * Print execution log if logging is enabled. + */ + private void printExecutionLog(SelectorData selector, RuleData rule, String pluginName) { + if (selector != null && selector.getLogged()) { + LOG.info("{} selector success match, selector name: {}", pluginName, selector.getName()); + } + if (rule != null && rule.getLoged()) { + LOG.info("{} rule success match, rule name: {}", pluginName, rule.getName()); + } + } + + /** + * Lazy initialization of dependencies to avoid startup overhead. + */ + private synchronized void initializeDependencies() { + if (initialized) { + return; + } + + try { + // Initialize RouteResolver + this.routeResolver = getSpringBean(RouteResolver.class); + + // Initialize MetricsHelper + this.metricsHelper = getSpringBean(MetricsHelper.class); + + initialized = true; + LOG.debug("Dependencies initialized for plugin: {}", named()); + + } catch (Exception e) { + LOG.error("Failed to initialize dependencies for plugin: {}", named(), e); + throw new IllegalStateException("Plugin initialization failed", e); + } + } + + /** + * Get Spring bean - subclasses can override for different IoC strategies. + */ + protected <T> T getSpringBean(Class<T> clazz) { + return org.apache.shenyu.plugin.api.utils.SpringBeanUtils.getInstance().getBean(clazz); + } +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/DefaultSmartCacheManager.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/DefaultSmartCacheManager.java new file mode 100644 index 0000000000..edf60fc1f7 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/DefaultSmartCacheManager.java @@ -0,0 +1,310 @@ +package org.apache.shenyu.plugin.base.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.stats.CacheStats; +import org.apache.shenyu.common.config.ShenyuConfig; +import org.apache.shenyu.plugin.base.trie.ShenyuTrie; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Default implementation of SmartCacheManager with adaptive strategies. + * + * Supports multiple caching strategies: + * 1. LRU_ONLY - Pure LRU cache using Caffeine + * 2. TRIE_ONLY - Pure Trie-based cache for path matching + * 3. HYBRID - Combination of LRU (L1) + Trie (L2) + * 4. ADAPTIVE - Automatically switches between strategies based on performance + */ +@Component +public class DefaultSmartCacheManager implements SmartCacheManager { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultSmartCacheManager.class); + + private final ConcurrentHashMap<String, CacheInstance<?>> caches = new ConcurrentHashMap<>(); + private final SmartCacheConfig config; + private final AtomicLong totalRequests = new AtomicLong(0); + + public DefaultSmartCacheManager(ShenyuConfig shenyuConfig) { + this.config = SmartCacheConfig.fromShenyuConfig(shenyuConfig); + initializePredefinedCaches(); + } + + @Override + @SuppressWarnings("unchecked") + public <K, V> Mono<V> getOrLoad(String cacheType, K key, Mono<V> loader) { + totalRequests.incrementAndGet(); + + return Mono.fromCallable(() -> { + CacheInstance<V> cache = (CacheInstance<V>) getOrCreateCache(cacheType); + return cache.getOrLoad(key, loader); + }).flatMap(mono -> mono); + } + + @Override + @SuppressWarnings("unchecked") + public <K, V> Mono<Void> put(String cacheType, K key, V value, Duration ttl) { + return Mono.fromRunnable(() -> { + CacheInstance<V> cache = (CacheInstance<V>) getOrCreateCache(cacheType); + cache.put(key, value, ttl); + }); + } + + @Override + public <K> Mono<Void> invalidate(String cacheType, K key) { + return Mono.fromRunnable(() -> { + CacheInstance<?> cache = getOrCreateCache(cacheType); + cache.invalidate(key); + }); + } + + @Override + public Mono<Void> clear(String cacheType) { + return Mono.fromRunnable(() -> { + CacheInstance<?> cache = caches.get(cacheType); + if (cache != null) { + cache.clear(); + } + }); + } + + @Override + public SmartCacheManager.CacheStats getStats(String cacheType) { + CacheInstance<?> cache = caches.get(cacheType); + return cache != null ? cache.getStats() : createEmptyStats(); + } + + @Override + public Mono<Void> adaptStrategy(String cacheType) { + return Mono.fromRunnable(() -> { + CacheInstance<?> cache = caches.get(cacheType); + if (cache instanceof AdaptiveCacheInstance) { + ((AdaptiveCacheInstance<?>) cache).adaptStrategy(); + } + }); + } + + /** + * Get or create cache instance for the given type. + */ + private CacheInstance<?> getOrCreateCache(String cacheType) { + return caches.computeIfAbsent(cacheType, type -> { + SmartCacheConfig.CacheConfig typeConfig = config.getCacheConfig(type); + return createCacheInstance(type, typeConfig); + }); + } + + /** + * Create appropriate cache instance based on configuration. + */ + private CacheInstance<?> createCacheInstance(String cacheType, SmartCacheConfig.CacheConfig cacheConfig) { + switch (cacheConfig.getStrategy()) { + case LRU_ONLY: + return new LruCacheInstance<>(cacheConfig); + case TRIE_ONLY: + LOG.info("Creating Trie cache instance (Phase 1 basic implementation)"); + return new LruCacheInstance<>(cacheConfig); // Use LRU for now + case HYBRID: + LOG.info("Creating Hybrid cache instance (Phase 1 basic implementation)"); + return new LruCacheInstance<>(cacheConfig); // Use LRU for now + case ADAPTIVE: + return new AdaptiveCacheInstance<>(cacheConfig); + default: + LOG.warn("Unknown cache strategy: {}, falling back to LRU", cacheConfig.getStrategy()); + return new LruCacheInstance<>(cacheConfig); + } + } + + /** + * Initialize predefined cache instances. + */ + private void initializePredefinedCaches() { + // Pre-create common cache types + getOrCreateCache(CacheTypes.SELECTOR); + getOrCreateCache(CacheTypes.RULE); + getOrCreateCache(CacheTypes.PLUGIN); + + LOG.info("Smart cache manager initialized with strategy: {}", config.getDefaultStrategy()); + } + + private SmartCacheManager.CacheStats createEmptyStats() { + return new SmartCacheManager.CacheStats(0, 0, 0, 0, "NONE"); + } + + /** + * Base interface for cache instances. + */ + private interface CacheInstance<V> { + <K> Mono<V> getOrLoad(K key, Mono<V> loader); + <K> void put(K key, V value, Duration ttl); + <K> void invalidate(K key); + void clear(); + SmartCacheManager.CacheStats getStats(); + } + + /** + * LRU-only cache implementation using Caffeine. + */ + private static class LruCacheInstance<V> implements CacheInstance<V> { + private final Cache<Object, V> cache; + private final SmartCacheConfig.CacheConfig config; + + public LruCacheInstance(SmartCacheConfig.CacheConfig config) { + this.config = config; + this.cache = Caffeine.newBuilder() + .initialCapacity(config.getInitialCapacity()) + .maximumSize(config.getMaximumSize()) + .expireAfterWrite(config.getExpireAfterWrite()) + .expireAfterAccess(config.getExpireAfterAccess()) + .recordStats() + .build(); + } + + @Override + public <K> Mono<V> getOrLoad(K key, Mono<V> loader) { + V cachedValue = cache.getIfPresent(key); + if (cachedValue != null) { + return Mono.just(cachedValue); + } + + return loader.doOnNext(value -> { + if (value != null) { + cache.put(key, value); + } + }); + } + + @Override + public <K> void put(K key, V value, Duration ttl) { + cache.put(key, value); + } + + @Override + public <K> void invalidate(K key) { + cache.invalidate(key); + } + + @Override + public void clear() { + cache.invalidateAll(); + } + + @Override + public SmartCacheManager.CacheStats getStats() { + com.github.benmanes.caffeine.cache.stats.CacheStats caffeineStats = cache.stats(); + return new SmartCacheManager.CacheStats( + caffeineStats.hitCount(), + caffeineStats.missCount(), + (long) caffeineStats.averageLoadPenalty(), + caffeineStats.evictionCount(), + "LRU_ONLY" + ); + } + } + + /** + * Adaptive cache implementation that switches strategies based on performance. + */ + private static class AdaptiveCacheInstance<V> implements CacheInstance<V> { + private volatile CacheInstance<V> activeCache; + private final SmartCacheConfig.CacheConfig config; + private final AtomicLong requestCount = new AtomicLong(0); + private final AtomicLong lastAdaptation = new AtomicLong(System.currentTimeMillis()); + + public AdaptiveCacheInstance(SmartCacheConfig.CacheConfig config) { + this.config = config; + this.activeCache = new LruCacheInstance<>(config); // Start with LRU + } + + @Override + public <K> Mono<V> getOrLoad(K key, Mono<V> loader) { + long count = requestCount.incrementAndGet(); + + // Check if adaptation is needed + if (shouldAdapt(count)) { + adaptStrategy(); + } + + return activeCache.getOrLoad(key, loader); + } + + @Override + public <K> void put(K key, V value, Duration ttl) { + activeCache.put(key, value, ttl); + } + + @Override + public <K> void invalidate(K key) { + activeCache.invalidate(key); + } + + @Override + public void clear() { + activeCache.clear(); + } + + @Override + public SmartCacheManager.CacheStats getStats() { + return activeCache.getStats(); + } + + /** + * Adapt caching strategy based on current performance metrics. + */ + public void adaptStrategy() { + SmartCacheManager.CacheStats currentStats = getStats(); + + // Decision logic for strategy adaptation + if (currentStats.getHitRatio() < config.getAdaptiveConfig().getHitRatioThreshold()) { + if (requestCount.get() > config.getAdaptiveConfig().getSwitchThreshold()) { + // Switch to Trie for better path matching + switchToStrategy("TRIE"); + } else { + // Switch to Hybrid for balanced performance + switchToStrategy("HYBRID"); + } + } + + lastAdaptation.set(System.currentTimeMillis()); + } + + private boolean shouldAdapt(long count) { + return count % 1000 == 0 && // Check every 1000 requests + System.currentTimeMillis() - lastAdaptation.get() > 60_000; // At most once per minute + } + + private void switchToStrategy(String strategy) { + // TODO: Implement Trie and Hybrid cache instances + // For now, just log the intention but keep using LRU + LOG.info("Strategy adaptation requested to: {}, but implementation pending. Keeping LRU.", strategy); + + // When Trie and Hybrid are implemented, uncomment: + /* + CacheInstance<V> newCache; + switch (strategy) { + case "TRIE": + newCache = new TrieCacheInstance<>(config); + break; + case "HYBRID": + newCache = new HybridCacheInstance<>(config); + break; + default: + return; // No change + } + // TODO: Migrate existing cache entries + this.activeCache = newCache; + LOG.info("Cache strategy adapted to: {}", strategy); + */ + } + } + + // Additional cache implementations would be defined here... + // TrieCacheInstance, HybridCacheInstance, etc. +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/HybridCacheInstance.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/HybridCacheInstance.java new file mode 100644 index 0000000000..51e9553b45 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/HybridCacheInstance.java @@ -0,0 +1,106 @@ +package org.apache.shenyu.plugin.base.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +/** + * Hybrid cache implementation combining LRU (L1) and Trie (L2) caching. + * + * This implementation provides two-level caching: + * - L1: Fast LRU cache for frequently accessed items + * - L2: Trie-based cache for path pattern matching + * + * TODO: Full implementation planned for Phase 2 + * Current status: L1 cache operational, L2 planned + */ +public class HybridCacheInstance<V> { + + private final SmartCacheConfig.CacheConfig config; + private final Cache<Object, V> l1Cache; // Fast LRU cache + private final TrieCacheInstance<V> l2Cache; // Trie-based cache + + public HybridCacheInstance(SmartCacheConfig.CacheConfig config) { + this.config = config; + + // Initialize L1 cache (LRU) + this.l1Cache = Caffeine.newBuilder() + .initialCapacity(config.getInitialCapacity() / 10) // L1 is 10% of total + .maximumSize(config.getMaximumSize() / 10) + .expireAfterWrite(config.getExpireAfterWrite()) + .expireAfterAccess(config.getExpireAfterAccess()) + .recordStats() + .build(); + + // Initialize L2 cache (Trie) + this.l2Cache = new TrieCacheInstance<>(config); + } + + /** + * Get value from cache with L1/L2 lookup. + * + * Lookup order: + * 1. Check L1 cache (fast) + * 2. Check L2 cache (slower but more intelligent) + * 3. Load from source + */ + public <K> Mono<V> getOrLoad(K key, Mono<V> loader) { + // Try L1 cache first + V l1Value = l1Cache.getIfPresent(key); + if (l1Value != null) { + return Mono.just(l1Value); + } + + // Try L2 cache + return l2Cache.getOrLoad(key, loader) + .doOnNext(value -> { + if (value != null) { + // Promote to L1 cache on access + l1Cache.put(key, value); + } + }); + } + + /** + * Put value into both cache levels. + */ + public <K> void put(K key, V value, Duration ttl) { + l1Cache.put(key, value); + l2Cache.put(key, value, ttl); + } + + /** + * Invalidate from both cache levels. + */ + public <K> void invalidate(K key) { + l1Cache.invalidate(key); + l2Cache.invalidate(key); + } + + /** + * Clear both cache levels. + */ + public void clear() { + l1Cache.invalidateAll(); + l2Cache.clear(); + } + + /** + * Get combined cache statistics. + */ + public SmartCacheManager.CacheStats getStats() { + com.github.benmanes.caffeine.cache.stats.CacheStats l1Stats = l1Cache.stats(); + SmartCacheManager.CacheStats l2Stats = l2Cache.getStats(); + + // Combine statistics from both levels + return new SmartCacheManager.CacheStats( + l1Stats.hitCount() + l2Stats.getHitCount(), + l1Stats.missCount() + l2Stats.getMissCount(), + (long) l1Stats.averageLoadPenalty(), + l1Stats.evictionCount() + l2Stats.getEvictionCount(), + "HYBRID" + ); + } +} diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/SmartCacheConfig.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/SmartCacheConfig.java new file mode 100644 index 0000000000..1cb2c0d5e2 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/SmartCacheConfig.java @@ -0,0 +1,189 @@ +package org.apache.shenyu.plugin.base.cache; + +import org.apache.shenyu.common.config.ShenyuConfig; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +/** + * Smart cache configuration with adaptive strategies. + */ +public class SmartCacheConfig { + + private CacheStrategy defaultStrategy = CacheStrategy.ADAPTIVE; + private Map<String, CacheConfig> cacheConfigs = new HashMap<>(); + private AdaptiveConfig adaptiveConfig = new AdaptiveConfig(); + + /** + * Create configuration from existing ShenyuConfig. + */ + public static SmartCacheConfig fromShenyuConfig(ShenyuConfig shenyuConfig) { + SmartCacheConfig config = new SmartCacheConfig(); + + // Extract selector cache config + if (shenyuConfig.getSelectorMatchCache() != null) { + CacheConfig selectorConfig = CacheConfig.fromLegacyConfig( + shenyuConfig.getSelectorMatchCache(), CacheStrategy.ADAPTIVE); + config.cacheConfigs.put("selector_cache", selectorConfig); + } + + // Extract rule cache config + if (shenyuConfig.getRuleMatchCache() != null) { + CacheConfig ruleConfig = CacheConfig.fromLegacyRuleConfig( + shenyuConfig.getRuleMatchCache(), CacheStrategy.ADAPTIVE); + config.cacheConfigs.put("rule_cache", ruleConfig); + } + + return config; + } + + /** + * Get cache configuration for specific type. + */ + public CacheConfig getCacheConfig(String cacheType) { + return cacheConfigs.getOrDefault(cacheType, createDefaultConfig()); + } + + private CacheConfig createDefaultConfig() { + return CacheConfig.builder() + .strategy(defaultStrategy) + .initialCapacity(10000) + .maximumSize(65536) + .expireAfterWrite(Duration.ofMinutes(30)) + .expireAfterAccess(Duration.ofMinutes(10)) + .build(); + } + + // Getters and setters + public CacheStrategy getDefaultStrategy() { return defaultStrategy; } + public AdaptiveConfig getAdaptiveConfig() { return adaptiveConfig; } + + /** + * Cache strategy enumeration. + */ + public enum CacheStrategy { + LRU_ONLY, // Pure LRU cache + TRIE_ONLY, // Pure Trie cache for path matching + HYBRID, // L1 (LRU) + L2 (Trie) + ADAPTIVE // Automatically adapt based on performance + } + + /** + * Individual cache configuration. + */ + public static class CacheConfig { + private CacheStrategy strategy; + private int initialCapacity; + private long maximumSize; + private Duration expireAfterWrite; + private Duration expireAfterAccess; + private AdaptiveConfig adaptiveConfig; + + public static CacheConfig fromLegacyConfig( + ShenyuConfig.SelectorMatchCache legacyConfig, + CacheStrategy defaultStrategy) { + + return CacheConfig.builder() + .strategy(defaultStrategy) + .initialCapacity(legacyConfig.getCache().getInitialCapacity()) + .maximumSize(legacyConfig.getCache().getMaximumSize()) + .expireAfterWrite(Duration.ofMinutes(30)) + .expireAfterAccess(Duration.ofMinutes(10)) + .build(); + } + + public static CacheConfig fromLegacyRuleConfig( + ShenyuConfig.RuleMatchCache legacyConfig, + CacheStrategy defaultStrategy) { + + return CacheConfig.builder() + .strategy(defaultStrategy) + .initialCapacity(legacyConfig.getCache().getInitialCapacity()) + .maximumSize(legacyConfig.getCache().getMaximumSize()) + .expireAfterWrite(Duration.ofMinutes(30)) + .expireAfterAccess(Duration.ofMinutes(10)) + .build(); + } + + public static Builder builder() { + return new Builder(); + } + + // Builder pattern + public static class Builder { + private CacheStrategy strategy = CacheStrategy.ADAPTIVE; + private int initialCapacity = 10000; + private long maximumSize = 65536; + private Duration expireAfterWrite = Duration.ofMinutes(30); + private Duration expireAfterAccess = Duration.ofMinutes(10); + + public Builder strategy(CacheStrategy strategy) { + this.strategy = strategy; + return this; + } + + public Builder initialCapacity(int initialCapacity) { + this.initialCapacity = initialCapacity; + return this; + } + + public Builder maximumSize(long maximumSize) { + this.maximumSize = maximumSize; + return this; + } + + public Builder expireAfterWrite(Duration expireAfterWrite) { + this.expireAfterWrite = expireAfterWrite; + return this; + } + + public Builder expireAfterAccess(Duration expireAfterAccess) { + this.expireAfterAccess = expireAfterAccess; + return this; + } + + public CacheConfig build() { + CacheConfig config = new CacheConfig(); + config.strategy = this.strategy; + config.initialCapacity = this.initialCapacity; + config.maximumSize = this.maximumSize; + config.expireAfterWrite = this.expireAfterWrite; + config.expireAfterAccess = this.expireAfterAccess; + config.adaptiveConfig = new AdaptiveConfig(); + return config; + } + } + + // Getters + public CacheStrategy getStrategy() { return strategy; } + public int getInitialCapacity() { return initialCapacity; } + public long getMaximumSize() { return maximumSize; } + public Duration getExpireAfterWrite() { return expireAfterWrite; } + public Duration getExpireAfterAccess() { return expireAfterAccess; } + public AdaptiveConfig getAdaptiveConfig() { return adaptiveConfig; } + } + + /** + * Configuration for adaptive caching behavior. + */ + public static class AdaptiveConfig { + private int switchThreshold = 1000; // Switch to Trie after 1000 entries + private double hitRatioThreshold = 0.8; // Switch if hit ratio < 80% + private Duration evaluationInterval = Duration.ofMinutes(5); // Evaluate every 5 minutes + private boolean autoOptimization = true; // Enable automatic optimization + + // Getters and setters + public int getSwitchThreshold() { return switchThreshold; } + public void setSwitchThreshold(int switchThreshold) { this.switchThreshold = switchThreshold; } + + public double getHitRatioThreshold() { return hitRatioThreshold; } + public void setHitRatioThreshold(double hitRatioThreshold) { this.hitRatioThreshold = hitRatioThreshold; } + + public Duration getEvaluationInterval() { return evaluationInterval; } + public void setEvaluationInterval(Duration evaluationInterval) { this.evaluationInterval = evaluationInterval; } + + public boolean isAutoOptimization() { return autoOptimization; } + public void setAutoOptimization(boolean autoOptimization) { this.autoOptimization = autoOptimization; } + } +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/SmartCacheManager.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/SmartCacheManager.java new file mode 100644 index 0000000000..fcc74bfe50 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/SmartCacheManager.java @@ -0,0 +1,126 @@ +package org.apache.shenyu.plugin.base.cache; + +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Optional; + +/** + * Smart cache manager with adaptive strategies. + * + * This interface provides a unified caching abstraction that can automatically + * adapt between different caching strategies based on runtime characteristics. + * + * Key features: + * 1. Adaptive strategy selection (LRU, Trie, Hybrid) + * 2. Performance-based strategy switching + * 3. Unified interface for different cache types + * 4. Built-in metrics and monitoring + */ +public interface SmartCacheManager { + + /** + * Get value from cache with adaptive strategy. + * + * @param key cache key + * @param loader fallback loader if cache miss + * @param <K> key type + * @param <V> value type + * @return cached or loaded value + */ + <K, V> Mono<V> getOrLoad(String cacheType, K key, Mono<V> loader); + + /** + * Put value into cache. + * + * @param cacheType cache type identifier + * @param key cache key + * @param value cache value + * @param ttl time to live + * @param <K> key type + * @param <V> value type + * @return completion signal + */ + <K, V> Mono<Void> put(String cacheType, K key, V value, Duration ttl); + + /** + * Invalidate cache entries. + * + * @param cacheType cache type identifier + * @param key cache key + * @param <K> key type + * @return completion signal + */ + <K> Mono<Void> invalidate(String cacheType, K key); + + /** + * Clear all cache entries for a specific type. + * + * @param cacheType cache type identifier + * @return completion signal + */ + Mono<Void> clear(String cacheType); + + /** + * Get cache statistics for monitoring. + * + * @param cacheType cache type identifier + * @return cache statistics + */ + CacheStats getStats(String cacheType); + + /** + * Manually trigger strategy adaptation based on current performance. + * + * @param cacheType cache type identifier + * @return completion signal + */ + Mono<Void> adaptStrategy(String cacheType); + + /** + * Cache type constants. + */ + interface CacheTypes { + String SELECTOR = "selector_cache"; + String RULE = "rule_cache"; + String PLUGIN = "plugin_cache"; + String UPSTREAM = "upstream_cache"; + } + + /** + * Cache statistics for monitoring and adaptation. + */ + class CacheStats { + private final long hitCount; + private final long missCount; + private final long loadTime; + private final long evictionCount; + private final String strategy; + private final double hitRatio; + + public CacheStats(long hitCount, long missCount, long loadTime, + long evictionCount, String strategy) { + this.hitCount = hitCount; + this.missCount = missCount; + this.loadTime = loadTime; + this.evictionCount = evictionCount; + this.strategy = strategy; + this.hitRatio = hitCount + missCount > 0 ? + (double) hitCount / (hitCount + missCount) : 0.0; + } + + // Getters + public long getHitCount() { return hitCount; } + public long getMissCount() { return missCount; } + public long getLoadTime() { return loadTime; } + public long getEvictionCount() { return evictionCount; } + public String getStrategy() { return strategy; } + public double getHitRatio() { return hitRatio; } + + @Override + public String toString() { + return String.format("CacheStats{hits=%d, misses=%d, hitRatio=%.2f%%, strategy=%s}", + hitCount, missCount, hitRatio * 100, strategy); + } + } +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/TrieCacheInstance.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/TrieCacheInstance.java new file mode 100644 index 0000000000..aa415eb35e --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/TrieCacheInstance.java @@ -0,0 +1,86 @@ +package org.apache.shenyu.plugin.base.cache; + +import org.apache.shenyu.plugin.base.trie.ShenyuTrie; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Trie-based cache implementation for efficient path matching. + * + * This cache uses a Trie (prefix tree) data structure for fast path-based lookups, + * particularly useful for URL pattern matching in selectors and rules. + * + * TODO: Full implementation planned for Phase 2 + * Current status: Basic implementation with fallback to map-based storage + */ +public class TrieCacheInstance<V> { + + private final SmartCacheConfig.CacheConfig config; + private final ShenyuTrie trie; + private final ConcurrentHashMap<Object, V> fallbackMap; + + public TrieCacheInstance(SmartCacheConfig.CacheConfig config) { + this.config = config; + this.trie = new ShenyuTrie(config.getMaximumSize(), "antPathMatch"); + this.fallbackMap = new ConcurrentHashMap<>(); + } + + /** + * Get value from cache or load from provider. + * + * @param key cache key + * @param loader fallback loader + * @return cached or loaded value + */ + public <K> Mono<V> getOrLoad(K key, Mono<V> loader) { + // TODO: Implement Trie-based lookup for path keys + // For now, use fallback map + V cachedValue = fallbackMap.get(key); + if (cachedValue != null) { + return Mono.just(cachedValue); + } + + return loader.doOnNext(value -> { + if (value != null) { + fallbackMap.put(key, value); + // TODO: Insert into Trie if key is a path string + } + }); + } + + /** + * Put value into cache. + */ + public <K> void put(K key, V value, Duration ttl) { + fallbackMap.put(key, value); + // TODO: Insert into Trie structure + } + + /** + * Invalidate cache entry. + */ + public <K> void invalidate(K key) { + fallbackMap.remove(key); + // TODO: Remove from Trie structure + } + + /** + * Clear all cache entries. + */ + public void clear() { + fallbackMap.clear(); + // TODO: Clear Trie structure + } + + /** + * Get cache statistics. + */ + public SmartCacheManager.CacheStats getStats() { + // TODO: Implement proper statistics collection + return new SmartCacheManager.CacheStats( + 0, 0, 0, 0, "TRIE_ONLY" + ); + } +} diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/config/PluginBaseAutoConfiguration.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/config/PluginBaseAutoConfiguration.java new file mode 100644 index 0000000000..bc27f431c7 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/config/PluginBaseAutoConfiguration.java @@ -0,0 +1,104 @@ +package org.apache.shenyu.plugin.base.config; + +import org.apache.shenyu.common.config.ShenyuConfig; +import org.apache.shenyu.plugin.base.cache.DefaultSmartCacheManager; +import org.apache.shenyu.plugin.base.cache.SmartCacheManager; +import org.apache.shenyu.plugin.base.metrics.MetricsHelper; +import org.apache.shenyu.plugin.base.metrics.NoOpMetricsHelper; +import org.apache.shenyu.plugin.base.route.DefaultRouteResolver; +import org.apache.shenyu.plugin.base.route.RouteResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for enhanced plugin base components. + * + * This configuration registers all required beans for the + * enhanced plugin architecture. + */ +@Configuration +public class PluginBaseAutoConfiguration { + + private static final Logger LOG = LoggerFactory.getLogger(PluginBaseAutoConfiguration.class); + + /** + * Create SmartCacheManager bean. + * + * @param shenyuConfig ShenYu configuration + * @return SmartCacheManager instance + */ + @Bean + public SmartCacheManager smartCacheManager(ShenyuConfig shenyuConfig) { + LOG.info("Initializing SmartCacheManager with enhanced caching"); + return new DefaultSmartCacheManager(shenyuConfig); + } + + /** + * Create RouteResolver bean. + * + * @param cacheManager SmartCacheManager instance + * @return RouteResolver instance + */ + @Bean + public RouteResolver routeResolver(SmartCacheManager cacheManager) { + LOG.info("Initializing DefaultRouteResolver with SmartCacheManager"); + return new DefaultRouteResolver(cacheManager); + } + + /** + * Create MetricsHelper bean (no-op implementation by default). + * + * Override this bean to provide custom metrics implementation. + * + * @return MetricsHelper instance + */ + @Bean + public MetricsHelper metricsHelper() { + LOG.info("Initializing NoOpMetricsHelper (override to enable metrics)"); + return new NoOpMetricsHelper(); + } + + /** + * Log cache configuration on startup. + */ + @Bean + public CacheConfigLogger cacheConfigLogger(ShenyuConfig shenyuConfig) { + return new CacheConfigLogger(shenyuConfig); + } + + /** + * Logger for cache configuration details. + */ + static class CacheConfigLogger { + + private static final Logger LOG = LoggerFactory.getLogger(CacheConfigLogger.class); + + public CacheConfigLogger(ShenyuConfig shenyuConfig) { + logCacheConfiguration(shenyuConfig); + } + + private void logCacheConfiguration(ShenyuConfig shenyuConfig) { + LOG.info("=== Enhanced Plugin Base Configuration ==="); + + if (shenyuConfig.getSelectorMatchCache() != null) { + LOG.info("Selector Cache:"); + LOG.info(" - Initial Capacity: {}", + shenyuConfig.getSelectorMatchCache().getCache().getInitialCapacity()); + LOG.info(" - Maximum Size: {}", + shenyuConfig.getSelectorMatchCache().getCache().getMaximumSize()); + } + + if (shenyuConfig.getRuleMatchCache() != null) { + LOG.info("Rule Cache:"); + LOG.info(" - Initial Capacity: {}", + shenyuConfig.getRuleMatchCache().getCache().getInitialCapacity()); + LOG.info(" - Maximum Size: {}", + shenyuConfig.getRuleMatchCache().getCache().getMaximumSize()); + } + + LOG.info("=========================================="); + } + } +} diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/context/PluginExecutionContext.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/context/PluginExecutionContext.java new file mode 100644 index 0000000000..417a159b41 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/context/PluginExecutionContext.java @@ -0,0 +1,174 @@ +package org.apache.shenyu.plugin.base.context; + +import org.springframework.web.server.ServerWebExchange; + +import java.net.URI; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Plugin execution context containing all information needed for plugin execution. + * + * This class encapsulates request information and execution state to avoid + * repeated parsing and provide a clean interface for plugins. + */ +public final class PluginExecutionContext { + + private final ServerWebExchange exchange; + private final String requestPath; + private final String method; + private final Map<String, String> headers; + private final Map<String, Object> attributes; + private final long startTime; + + private PluginExecutionContext(Builder builder) { + this.exchange = builder.exchange; + this.requestPath = builder.requestPath; + this.method = builder.method; + this.headers = Map.copyOf(builder.headers); + this.attributes = new ConcurrentHashMap<>(builder.attributes); + this.startTime = System.currentTimeMillis(); + } + + /** + * Create context from ServerWebExchange. + */ + public static PluginExecutionContext fromExchange(ServerWebExchange exchange) { + return builder() + .exchange(exchange) + .requestPath(extractPath(exchange)) + .method(exchange.getRequest().getMethod().name()) + .headers(extractHeaders(exchange)) + .build(); + } + + /** + * Get the original ServerWebExchange. + */ + public ServerWebExchange getExchange() { + return exchange; + } + + /** + * Get the request path (URI raw path). + */ + public String getRequestPath() { + return requestPath; + } + + /** + * Get HTTP method. + */ + public String getMethod() { + return method; + } + + /** + * Get request headers. + */ + public Map<String, String> getHeaders() { + return headers; + } + + /** + * Get a specific header value. + */ + public Optional<String> getHeader(String name) { + return Optional.ofNullable(headers.get(name.toLowerCase())); + } + + /** + * Get context attributes. + */ + public Map<String, Object> getAttributes() { + return attributes; + } + + /** + * Set context attribute. + */ + public void setAttribute(String key, Object value) { + attributes.put(key, value); + } + + /** + * Get context attribute. + */ + @SuppressWarnings("unchecked") + public <T> Optional<T> getAttribute(String key) { + return Optional.ofNullable((T) attributes.get(key)); + } + + /** + * Get execution start time. + */ + public long getStartTime() { + return startTime; + } + + /** + * Calculate elapsed time in milliseconds. + */ + public long getElapsedTime() { + return System.currentTimeMillis() - startTime; + } + + // Helper methods + private static String extractPath(ServerWebExchange exchange) { + URI uri = exchange.getRequest().getURI(); + return uri.getRawPath(); + } + + private static Map<String, String> extractHeaders(ServerWebExchange exchange) { + Map<String, String> headerMap = new ConcurrentHashMap<>(); + exchange.getRequest().getHeaders().forEach((name, values) -> { + if (!values.isEmpty()) { + headerMap.put(name.toLowerCase(), values.get(0)); + } + }); + return headerMap; + } + + // Builder pattern + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private ServerWebExchange exchange; + private String requestPath; + private String method; + private Map<String, String> headers = new ConcurrentHashMap<>(); + private Map<String, Object> attributes = new ConcurrentHashMap<>(); + + public Builder exchange(ServerWebExchange exchange) { + this.exchange = exchange; + return this; + } + + public Builder requestPath(String requestPath) { + this.requestPath = requestPath; + return this; + } + + public Builder method(String method) { + this.method = method; + return this; + } + + public Builder headers(Map<String, String> headers) { + this.headers.putAll(headers); + return this; + } + + public Builder attribute(String key, Object value) { + this.attributes.put(key, value); + return this; + } + + public PluginExecutionContext build() { + return new PluginExecutionContext(this); + } + } +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/metrics/MetricsHelper.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/metrics/MetricsHelper.java new file mode 100644 index 0000000000..738d4b53ec --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/metrics/MetricsHelper.java @@ -0,0 +1,98 @@ +package org.apache.shenyu.plugin.base.metrics; + +import org.apache.shenyu.plugin.base.route.RouteResult; + +/** + * Metrics helper for collecting plugin execution statistics. + * + * This component provides a unified interface for collecting performance + * and operational metrics from plugin executions. + */ +public interface MetricsHelper { + + /** + * Record plugin execution time. + * + * @param pluginName name of the plugin + * @param durationNanos execution time in nanoseconds + */ + void recordExecutionTime(String pluginName, long durationNanos); + + /** + * Record successful plugin execution. + * + * @param pluginName name of the plugin + */ + void recordSuccess(String pluginName); + + /** + * Record plugin execution error. + * + * @param pluginName name of the plugin + * @param error the error that occurred + */ + void recordError(String pluginName, Throwable error); + + /** + * Record route matching result. + * + * @param pluginName name of the plugin + * @param matchType type of match (cache hit, trie match, etc.) + */ + void recordRouteMatch(String pluginName, RouteResult.MatchType matchType); + + /** + * Record cache operation metrics. + * + * @param cacheType type of cache (selector, rule, etc.) + * @param operation operation type (hit, miss, put, etc.) + */ + void recordCacheOperation(String cacheType, String operation); + + /** + * Get current metrics snapshot for a plugin. + * + * @param pluginName name of the plugin + * @return metrics snapshot + */ + PluginMetrics getPluginMetrics(String pluginName); + + /** + * Plugin metrics data structure. + */ + class PluginMetrics { + private final String pluginName; + private final long totalExecutions; + private final long successfulExecutions; + private final long failedExecutions; + private final double averageExecutionTime; + private final double successRate; + + public PluginMetrics(String pluginName, long totalExecutions, + long successfulExecutions, long failedExecutions, + double averageExecutionTime) { + this.pluginName = pluginName; + this.totalExecutions = totalExecutions; + this.successfulExecutions = successfulExecutions; + this.failedExecutions = failedExecutions; + this.averageExecutionTime = averageExecutionTime; + this.successRate = totalExecutions > 0 ? + (double) successfulExecutions / totalExecutions : 0.0; + } + + // Getters + public String getPluginName() { return pluginName; } + public long getTotalExecutions() { return totalExecutions; } + public long getSuccessfulExecutions() { return successfulExecutions; } + public long getFailedExecutions() { return failedExecutions; } + public double getAverageExecutionTime() { return averageExecutionTime; } + public double getSuccessRate() { return successRate; } + + @Override + public String toString() { + return String.format("PluginMetrics{plugin=%s, executions=%d, " + + "successRate=%.2f%%, avgTime=%.2fms}", + pluginName, totalExecutions, successRate * 100, averageExecutionTime / 1_000_000.0); + } + } +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/metrics/NoOpMetricsHelper.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/metrics/NoOpMetricsHelper.java new file mode 100644 index 0000000000..d083c67aac --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/metrics/NoOpMetricsHelper.java @@ -0,0 +1,42 @@ +package org.apache.shenyu.plugin.base.metrics; + +import org.apache.shenyu.plugin.base.route.RouteResult; + +/** + * No-op implementation of MetricsHelper. + * + * This implementation is used when metrics collection is disabled. + * All methods are no-ops and have minimal overhead. + */ +public class NoOpMetricsHelper implements MetricsHelper { + + @Override + public void recordExecutionTime(String pluginName, long durationNanos) { + // No-op + } + + @Override + public void recordSuccess(String pluginName) { + // No-op + } + + @Override + public void recordError(String pluginName, Throwable error) { + // No-op + } + + @Override + public void recordRouteMatch(String pluginName, RouteResult.MatchType matchType) { + // No-op + } + + @Override + public void recordCacheOperation(String cacheType, String operation) { + // No-op + } + + @Override + public PluginMetrics getPluginMetrics(String pluginName) { + return new PluginMetrics(pluginName, 0, 0, 0, 0.0); + } +} diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/route/DefaultRouteResolver.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/route/DefaultRouteResolver.java new file mode 100644 index 0000000000..06e67792e3 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/route/DefaultRouteResolver.java @@ -0,0 +1,199 @@ +package org.apache.shenyu.plugin.base.route; + +import org.apache.shenyu.common.dto.ConditionData; +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.common.enums.SelectorTypeEnum; +import org.apache.shenyu.plugin.base.cache.BaseDataCache; +import org.apache.shenyu.plugin.base.cache.SmartCacheManager; +import org.apache.shenyu.plugin.base.condition.strategy.MatchStrategyFactory; +import org.apache.shenyu.plugin.base.context.PluginExecutionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Default implementation of RouteResolver. + * + * This implementation provides caching-aware route resolution with support for: + * - Smart caching of selector and rule matching results + * - Conditional matching based on request attributes + * - Full compatibility with existing ShenYu matching logic + */ +@Component +public class DefaultRouteResolver implements RouteResolver { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultRouteResolver.class); + + private final SmartCacheManager cacheManager; + + public DefaultRouteResolver(SmartCacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + @Override + public Mono<Optional<SelectorData>> matchSelector( + PluginExecutionContext context, + List<SelectorData> selectors) { + + if (selectors == null || selectors.isEmpty()) { + return Mono.just(Optional.empty()); + } + + // Try cache first + String cacheKey = buildSelectorCacheKey(context); + + return cacheManager.getOrLoad( + SmartCacheManager.CacheTypes.SELECTOR, + cacheKey, + Mono.fromCallable(() -> performSelectorMatch(context, selectors)) + ).map(Optional::ofNullable); + } + + @Override + public Mono<Optional<RuleData>> matchRule( + PluginExecutionContext context, + SelectorData selector, + List<RuleData> rules) { + + if (selector == null || rules == null || rules.isEmpty()) { + return Mono.just(Optional.empty()); + } + + // Try cache first + String cacheKey = buildRuleCacheKey(context, selector); + + return cacheManager.getOrLoad( + SmartCacheManager.CacheTypes.RULE, + cacheKey, + Mono.fromCallable(() -> performRuleMatch(context, rules)) + ).map(Optional::ofNullable); + } + + @Override + public Mono<Boolean> isPluginApplicable(PluginExecutionContext context, String pluginName) { + return Mono.fromCallable(() -> { + PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName); + return Objects.nonNull(pluginData) && pluginData.getEnabled(); + }); + } + + @Override + public Mono<RouteResult> resolveRoute(PluginExecutionContext context, String pluginName) { + return isPluginApplicable(context, pluginName) + .filter(applicable -> applicable) + .flatMap(applicable -> { + // Get selectors for this plugin + List<SelectorData> selectors = BaseDataCache.getInstance() + .obtainSelectorData(pluginName); + + return matchSelector(context, selectors) + .flatMap(selectorOpt -> { + if (!selectorOpt.isPresent()) { + return Mono.just(RouteResult.noMatch()); + } + + SelectorData selector = selectorOpt.get(); + + // Check if selector continues to rules + if (!selector.getContinued()) { + return Mono.just(RouteResult.selectorOnly( + selector, RouteResult.MatchType.CACHE_HIT)); + } + + // Get and match rules + List<RuleData> rules = BaseDataCache.getInstance() + .obtainRuleData(selector.getId()); + + return matchRule(context, selector, rules) + .map(ruleOpt -> { + if (ruleOpt.isPresent()) { + return RouteResult.success( + selector, + ruleOpt.get(), + RouteResult.MatchType.CACHE_HIT + ); + } + return RouteResult.selectorOnly( + selector, RouteResult.MatchType.CACHE_HIT); + }); + }); + }) + .defaultIfEmpty(RouteResult.noMatch()); + } + + /** + * Perform actual selector matching logic. + */ + private SelectorData performSelectorMatch(PluginExecutionContext context, List<SelectorData> selectors) { + for (SelectorData selector : selectors) { + if (selector.getEnabled() && matchSelector(context, selector)) { + LOG.debug("Matched selector: {}", selector.getName()); + return selector; + } + } + return null; + } + + /** + * Perform actual rule matching logic. + */ + private RuleData performRuleMatch(PluginExecutionContext context, List<RuleData> rules) { + for (RuleData rule : rules) { + if (rule.getEnabled() && matchRule(context, rule)) { + LOG.debug("Matched rule: {}", rule.getName()); + return rule; + } + } + return null; + } + + /** + * Match selector against context. + */ + private boolean matchSelector(PluginExecutionContext context, SelectorData selector) { + if (selector.getConditionList() == null || selector.getConditionList().isEmpty()) { + return true; + } + return MatchStrategyFactory.match( + selector.getMatchMode(), + selector.getConditionList(), + context.getExchange() + ); + } + + /** + * Match rule against context. + */ + private boolean matchRule(PluginExecutionContext context, RuleData rule) { + if (rule.getConditionDataList() == null || rule.getConditionDataList().isEmpty()) { + return true; + } + return MatchStrategyFactory.match( + rule.getMatchMode(), + rule.getConditionDataList(), + context.getExchange() + ); + } + + /** + * Build cache key for selector matching. + */ + private String buildSelectorCacheKey(PluginExecutionContext context) { + return context.getRequestPath() + ":" + context.getMethod(); + } + + /** + * Build cache key for rule matching. + */ + private String buildRuleCacheKey(PluginExecutionContext context, SelectorData selector) { + return selector.getId() + ":" + context.getRequestPath() + ":" + context.getMethod(); + } +} diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/route/RouteResolver.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/route/RouteResolver.java new file mode 100644 index 0000000000..064b35330a --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/route/RouteResolver.java @@ -0,0 +1,66 @@ +package org.apache.shenyu.plugin.base.route; + +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.plugin.base.context.PluginExecutionContext; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Optional; + +/** + * Route resolver interface for selector and rule matching. + * + * This component is responsible for all routing logic that was previously + * embedded in AbstractShenyuPlugin.execute() method. + */ +public interface RouteResolver { + + /** + * Match selector for the given context. + * + * @param context plugin execution context containing request information + * @param selectors available selectors for the plugin + * @return matched selector wrapped in Optional + */ + Mono<Optional<SelectorData>> matchSelector( + PluginExecutionContext context, + List<SelectorData> selectors); + + /** + * Match rule for the given context and selector. + * + * @param context plugin execution context + * @param selector matched selector + * @param rules available rules for the selector + * @return matched rule wrapped in Optional + */ + Mono<Optional<RuleData>> matchRule( + PluginExecutionContext context, + SelectorData selector, + List<RuleData> rules); + + /** + * Check if the plugin is applicable for the current request. + * + * @param context plugin execution context + * @param pluginName name of the plugin + * @return true if plugin should be executed + */ + Mono<Boolean> isPluginApplicable( + PluginExecutionContext context, + String pluginName); + + /** + * Resolve complete routing information for plugin execution. + * + * This method combines selector and rule matching logic. + * + * @param context plugin execution context + * @param pluginName name of the plugin + * @return routing result containing selector and rule data + */ + Mono<RouteResult> resolveRoute( + PluginExecutionContext context, + String pluginName); +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/route/RouteResult.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/route/RouteResult.java new file mode 100644 index 0000000000..11d6fcbe55 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/route/RouteResult.java @@ -0,0 +1,95 @@ +package org.apache.shenyu.plugin.base.route; + +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; + +import java.util.Objects; +import java.util.Optional; + +/** + * Route resolution result containing matched selector and rule data. + */ +public final class RouteResult { + + private final SelectorData selector; + private final RuleData rule; + private final boolean matched; + private final MatchType matchType; + + private RouteResult(SelectorData selector, RuleData rule, boolean matched, MatchType matchType) { + this.selector = selector; + this.rule = rule; + this.matched = matched; + this.matchType = matchType; + } + + /** + * Create a successful route result. + */ + public static RouteResult success(SelectorData selector, RuleData rule, MatchType matchType) { + return new RouteResult( + Objects.requireNonNull(selector, "Selector cannot be null"), + Objects.requireNonNull(rule, "Rule cannot be null"), + true, + matchType); + } + + /** + * Create a no-match route result. + */ + public static RouteResult noMatch() { + return new RouteResult(null, null, false, MatchType.NONE); + } + + /** + * Create a selector-only match result (for continued = false scenarios). + */ + public static RouteResult selectorOnly(SelectorData selector, MatchType matchType) { + return new RouteResult( + Objects.requireNonNull(selector, "Selector cannot be null"), + null, + true, + matchType); + } + + // Getters + public Optional<SelectorData> getSelector() { + return Optional.ofNullable(selector); + } + + public Optional<RuleData> getRule() { + return Optional.ofNullable(rule); + } + + public boolean isMatched() { + return matched; + } + + public MatchType getMatchType() { + return matchType; + } + + public boolean hasRule() { + return rule != null; + } + + public boolean hasSelector() { + return selector != null; + } + + /** + * Match type indicating how the route was resolved. + */ + public enum MatchType { + CACHE_HIT, // Matched from L1 cache + TRIE_MATCH, // Matched from L2 Trie cache + DEFAULT_MATCH, // Matched using default strategy + NONE // No match found + } + + @Override + public String toString() { + return String.format("RouteResult{matched=%s, matchType=%s, hasSelector=%s, hasRule=%s}", + matched, matchType, hasSelector(), hasRule()); + } +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/resources/META-INF/spring.factories b/shenyu-plugin/shenyu-plugin-base/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..f25f9e2fc2 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.shenyu.plugin.base.config.PluginBaseAutoConfiguration diff --git a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/context/PluginExecutionContextTest.java b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/context/PluginExecutionContextTest.java new file mode 100644 index 0000000000..4094dd068f --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/context/PluginExecutionContextTest.java @@ -0,0 +1,166 @@ +package org.apache.shenyu.plugin.base.context; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; + +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Test for PluginExecutionContext. + */ +@ExtendWith(MockitoExtension.class) +class PluginExecutionContextTest { + + @Mock + private ServerWebExchange exchange; + + @Mock + private ServerHttpRequest request; + + @BeforeEach + void setUp() { + when(exchange.getRequest()).thenReturn(request); + } + + @Test + void testFromExchange() { + // Given + URI uri = URI.create("http://localhost:8080/api/users"); + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + headers.add("Authorization", "Bearer token123"); + + when(request.getURI()).thenReturn(uri); + when(request.getMethod()).thenReturn(HttpMethod.POST); + when(request.getHeaders()).thenReturn(headers); + + // When + PluginExecutionContext context = PluginExecutionContext.fromExchange(exchange); + + // Then + assertNotNull(context); + assertEquals("/api/users", context.getRequestPath()); + assertEquals("POST", context.getMethod()); + assertEquals(exchange, context.getExchange()); + } + + @Test + void testGetHeader() { + // Given + URI uri = URI.create("http://localhost:8080/api/users"); + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + headers.add("X-Custom-Header", "custom-value"); + + when(request.getURI()).thenReturn(uri); + when(request.getMethod()).thenReturn(HttpMethod.GET); + when(request.getHeaders()).thenReturn(headers); + + PluginExecutionContext context = PluginExecutionContext.fromExchange(exchange); + + // When & Then + assertTrue(context.getHeader("content-type").isPresent()); + assertEquals("application/json", context.getHeader("content-type").get()); + + assertTrue(context.getHeader("x-custom-header").isPresent()); + assertEquals("custom-value", context.getHeader("x-custom-header").get()); + + assertFalse(context.getHeader("non-existent").isPresent()); + } + + @Test + void testAttributes() { + // Given + URI uri = URI.create("http://localhost:8080/test"); + when(request.getURI()).thenReturn(uri); + when(request.getMethod()).thenReturn(HttpMethod.GET); + when(request.getHeaders()).thenReturn(new HttpHeaders()); + + PluginExecutionContext context = PluginExecutionContext.fromExchange(exchange); + + // When + context.setAttribute("key1", "value1"); + context.setAttribute("key2", 123); + + // Then + assertTrue(context.getAttribute("key1").isPresent()); + assertEquals("value1", context.getAttribute("key1").get()); + + assertTrue(context.getAttribute("key2").isPresent()); + assertEquals(123, context.getAttribute("key2").get()); + + assertFalse(context.getAttribute("non-existent").isPresent()); + } + + @Test + void testElapsedTime() throws InterruptedException { + // Given + URI uri = URI.create("http://localhost:8080/test"); + when(request.getURI()).thenReturn(uri); + when(request.getMethod()).thenReturn(HttpMethod.GET); + when(request.getHeaders()).thenReturn(new HttpHeaders()); + + PluginExecutionContext context = PluginExecutionContext.fromExchange(exchange); + + // When + Thread.sleep(10); // Wait a bit + long elapsed = context.getElapsedTime(); + + // Then + assertTrue(elapsed >= 10, "Elapsed time should be at least 10ms"); + assertTrue(context.getStartTime() > 0); + } + + @Test + void testBuilder() { + // Given + URI uri = URI.create("http://localhost:8080/api/test"); + when(request.getURI()).thenReturn(uri); + when(request.getMethod()).thenReturn(HttpMethod.PUT); + when(request.getHeaders()).thenReturn(new HttpHeaders()); + + // When + PluginExecutionContext context = PluginExecutionContext.builder() + .exchange(exchange) + .requestPath("/api/test") + .method("PUT") + .attribute("custom", "value") + .build(); + + // Then + assertNotNull(context); + assertEquals("/api/test", context.getRequestPath()); + assertEquals("PUT", context.getMethod()); + assertTrue(context.getAttribute("custom").isPresent()); + assertEquals("value", context.getAttribute("custom").get()); + } + + @Test + void testHeadersAreCaseInsensitive() { + // Given + URI uri = URI.create("http://localhost:8080/test"); + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + + when(request.getURI()).thenReturn(uri); + when(request.getMethod()).thenReturn(HttpMethod.GET); + when(request.getHeaders()).thenReturn(headers); + + PluginExecutionContext context = PluginExecutionContext.fromExchange(exchange); + + // When & Then - headers should be case-insensitive + assertTrue(context.getHeader("content-type").isPresent()); + assertTrue(context.getHeader("Content-Type").isPresent()); + assertTrue(context.getHeader("CONTENT-TYPE").isPresent()); + } +} diff --git a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/route/DefaultRouteResolverTest.java b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/route/DefaultRouteResolverTest.java new file mode 100644 index 0000000000..98ec0a2de3 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/route/DefaultRouteResolverTest.java @@ -0,0 +1,226 @@ +package org.apache.shenyu.plugin.base.route; + +import org.apache.shenyu.common.dto.ConditionData; +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.plugin.base.cache.BaseDataCache; +import org.apache.shenyu.plugin.base.cache.SmartCacheManager; +import org.apache.shenyu.plugin.base.context.PluginExecutionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Test for DefaultRouteResolver. + */ +@ExtendWith(MockitoExtension.class) +class DefaultRouteResolverTest { + + @Mock + private SmartCacheManager cacheManager; + + @Mock + private ServerWebExchange exchange; + + @Mock + private ServerHttpRequest request; + + private DefaultRouteResolver routeResolver; + + @BeforeEach + void setUp() { + routeResolver = new DefaultRouteResolver(cacheManager); + + // Setup mock exchange + when(exchange.getRequest()).thenReturn(request); + when(request.getURI()).thenReturn(URI.create("http://localhost:8080/api/users")); + when(request.getMethod()).thenReturn(HttpMethod.GET); + } + + @Test + void testMatchSelectorFromCache() { + // Given + PluginExecutionContext context = PluginExecutionContext.fromExchange(exchange); + SelectorData cachedSelector = createTestSelector("test-selector", true); + + when(cacheManager.getOrLoad( + eq(SmartCacheManager.CacheTypes.SELECTOR), + anyString(), + any(Mono.class) + )).thenReturn(Mono.just(cachedSelector)); + + // When + Mono<Optional<SelectorData>> result = routeResolver.matchSelector( + context, Collections.singletonList(cachedSelector)); + + // Then + StepVerifier.create(result) + .assertNext(opt -> { + assertTrue(opt.isPresent()); + assertEquals("test-selector", opt.get().getName()); + }) + .verifyComplete(); + } + + @Test + void testMatchSelectorNoMatch() { + // Given + PluginExecutionContext context = PluginExecutionContext.fromExchange(exchange); + + when(cacheManager.getOrLoad( + eq(SmartCacheManager.CacheTypes.SELECTOR), + anyString(), + any(Mono.class) + )).thenReturn(Mono.empty()); + + // When + Mono<Optional<SelectorData>> result = routeResolver.matchSelector( + context, Collections.emptyList()); + + // Then + StepVerifier.create(result) + .assertNext(opt -> assertFalse(opt.isPresent())) + .verifyComplete(); + } + + @Test + void testMatchRuleFromCache() { + // Given + PluginExecutionContext context = PluginExecutionContext.fromExchange(exchange); + SelectorData selector = createTestSelector("test-selector", true); + RuleData cachedRule = createTestRule("test-rule", true); + + when(cacheManager.getOrLoad( + eq(SmartCacheManager.CacheTypes.RULE), + anyString(), + any(Mono.class) + )).thenReturn(Mono.just(cachedRule)); + + // When + Mono<Optional<RuleData>> result = routeResolver.matchRule( + context, selector, Collections.singletonList(cachedRule)); + + // Then + StepVerifier.create(result) + .assertNext(opt -> { + assertTrue(opt.isPresent()); + assertEquals("test-rule", opt.get().getName()); + }) + .verifyComplete(); + } + + @Test + void testResolveRouteSuccess() { + // Given + PluginExecutionContext context = PluginExecutionContext.fromExchange(exchange); + String pluginName = "test-plugin"; + + SelectorData selector = createTestSelector("test-selector", true); + selector.setContinued(true); + RuleData rule = createTestRule("test-rule", true); + + when(cacheManager.getOrLoad( + eq(SmartCacheManager.CacheTypes.SELECTOR), + anyString(), + any(Mono.class) + )).thenReturn(Mono.just(selector)); + + when(cacheManager.getOrLoad( + eq(SmartCacheManager.CacheTypes.RULE), + anyString(), + any(Mono.class) + )).thenReturn(Mono.just(rule)); + + // When + Mono<RouteResult> result = routeResolver.resolveRoute(context, pluginName); + + // Then + StepVerifier.create(result) + .assertNext(routeResult -> { + assertTrue(routeResult.isMatched()); + assertTrue(routeResult.hasSelector()); + assertTrue(routeResult.hasRule()); + }) + .verifyComplete(); + } + + @Test + void testResolveRouteNoMatch() { + // Given + PluginExecutionContext context = PluginExecutionContext.fromExchange(exchange); + String pluginName = "test-plugin"; + + when(cacheManager.getOrLoad( + eq(SmartCacheManager.CacheTypes.SELECTOR), + anyString(), + any(Mono.class) + )).thenReturn(Mono.empty()); + + // When + Mono<RouteResult> result = routeResolver.resolveRoute(context, pluginName); + + // Then + StepVerifier.create(result) + .assertNext(routeResult -> { + assertFalse(routeResult.isMatched()); + assertEquals(RouteResult.MatchType.NONE, routeResult.getMatchType()); + }) + .verifyComplete(); + } + + @Test + void testIsPluginApplicable() { + // Given + PluginExecutionContext context = PluginExecutionContext.fromExchange(exchange); + String pluginName = "test-plugin"; + + // When & Then + Mono<Boolean> result = routeResolver.isPluginApplicable(context, pluginName); + + StepVerifier.create(result) + .expectNext(false) // BaseDataCache will return null by default + .verifyComplete(); + } + + // Helper methods + private SelectorData createTestSelector(String name, boolean enabled) { + SelectorData selector = new SelectorData(); + selector.setId("selector-id-1"); + selector.setName(name); + selector.setEnabled(enabled); + selector.setPluginName("test-plugin"); + selector.setMatchMode(0); + selector.setConditionList(new ArrayList<>()); + selector.setContinued(false); + return selector; + } + + private RuleData createTestRule(String name, boolean enabled) { + RuleData rule = new RuleData(); + rule.setId("rule-id-1"); + rule.setName(name); + rule.setEnabled(enabled); + rule.setPluginName("test-plugin"); + rule.setMatchMode(0); + rule.setConditionDataList(new ArrayList<>()); + return rule; + } +}
