Catégorie : Blog

  • Socle V004 – Résilience

    Socle V004 – Résilience

    11 – Resilience (Circuit Breaker & Retry)

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Le Socle V4 intègre des patterns de résilience pour gérer les défaillances des systèmes externes :

    • Retry : Réessayer les opérations échouées
    • Circuit Breaker : Protéger contre les cascades de défaillances

    2. Retry Pattern

    2.1 Configuration

    socle:
      resilience:
        retry:
          max-attempts: ${RETRY_MAX_ATTEMPTS:3}
          initial-delay-ms: ${RETRY_INITIAL_DELAY_MS:1000}
          max-delay-ms: ${RETRY_MAX_DELAY_MS:30000}
          multiplier: ${RETRY_MULTIPLIER:2.0}
    

    2.2 Interface RetryTemplate

    package eu.lmvi.socle.resilience;
    
    public interface RetryTemplate {
    
        /**
         * Exécute une opération avec retry
         */
        <T> T execute(Supplier<T> operation) throws RetryExhaustedException;
    
        /**
         * Exécute une opération avec retry et fallback
         */
        <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback);
    
        /**
         * Exécute une opération void avec retry
         */
        void executeVoid(Runnable operation) throws RetryExhaustedException;
    }
    

    2.3 Implémentation

    package eu.lmvi.socle.resilience;
    
    @Component
    public class ExponentialBackoffRetryTemplate implements RetryTemplate {
    
        private static final Logger log = LoggerFactory.getLogger(ExponentialBackoffRetryTemplate.class);
    
        private final int maxAttempts;
        private final long initialDelayMs;
        private final long maxDelayMs;
        private final double multiplier;
    
        public ExponentialBackoffRetryTemplate(SocleConfiguration config) {
            this.maxAttempts = config.getResilience().getRetry().getMaxAttempts();
            this.initialDelayMs = config.getResilience().getRetry().getInitialDelayMs();
            this.maxDelayMs = config.getResilience().getRetry().getMaxDelayMs();
            this.multiplier = config.getResilience().getRetry().getMultiplier();
        }
    
        @Override
        public <T> T execute(Supplier<T> operation) throws RetryExhaustedException {
            Exception lastException = null;
            long delay = initialDelayMs;
    
            for (int attempt = 1; attempt <= maxAttempts; attempt++) {
                try {
                    return operation.get();
                } catch (Exception e) {
                    lastException = e;
                    log.warn("Attempt {}/{} failed: {}", attempt, maxAttempts, e.getMessage());
    
                    if (attempt < maxAttempts) {
                        sleep(delay);
                        delay = Math.min((long) (delay * multiplier), maxDelayMs);
                    }
                }
            }
    
            throw new RetryExhaustedException(
                "Operation failed after " + maxAttempts + " attempts",
                lastException
            );
        }
    
        @Override
        public <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback) {
            try {
                return execute(operation);
            } catch (RetryExhaustedException e) {
                log.warn("All retries exhausted, using fallback");
                return fallback.get();
            }
        }
    
        @Override
        public void executeVoid(Runnable operation) throws RetryExhaustedException {
            execute(() -> {
                operation.run();
                return null;
            });
        }
    
        private void sleep(long ms) {
            try {
                Thread.sleep(ms);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Retry interrupted", e);
            }
        }
    }
    

    2.4 Utilisation

    @Service
    public class ExternalApiService {
    
        @Autowired
        private RetryTemplate retryTemplate;
    
        public Data fetchData(String id) {
            return retryTemplate.execute(() -> {
                // Appel qui peut échouer
                return httpClient.get("/data/" + id);
            });
        }
    
        public Data fetchDataWithFallback(String id) {
            return retryTemplate.executeWithFallback(
                () -> httpClient.get("/data/" + id),
                () -> getCachedData(id)  // Fallback vers le cache
            );
        }
    }
    

    3. Circuit Breaker Pattern

    3.1 États

            succès
         ┌─────────────────┐
         │                 │
         ▼                 │
    ┌─────────┐      ┌─────┴─────┐      ┌─────────┐
    │  CLOSED │─────►│   OPEN    │─────►│HALF_OPEN│
    └────┬────┘      └───────────┘      └────┬────┘
         │  échecs        │ timeout          │
         │  threshold     │                  │
         │                │                  │
         └────────────────┴──────────────────┘
                  retour succès
    
    • CLOSED : Fonctionnement normal, les requêtes passent
    • OPEN : Circuit ouvert, les requêtes échouent immédiatement
    • HALF_OPEN : Test de reprise, quelques requêtes passent

    3.2 Configuration

    socle:
      resilience:
        circuit-breaker:
          failure-threshold: ${CB_FAILURE_THRESHOLD:5}
          success-threshold: ${CB_SUCCESS_THRESHOLD:3}
          timeout-ms: ${CB_TIMEOUT_MS:60000}
          half-open-requests: ${CB_HALF_OPEN_REQUESTS:3}
    

    3.3 Interface CircuitBreaker

    package eu.lmvi.socle.resilience;
    
    public interface CircuitBreaker {
    
        /**
         * Nom du circuit
         */
        String getName();
    
        /**
         * État actuel
         */
        CircuitState getState();
    
        /**
         * Exécute une opération protégée
         */
        <T> T execute(Supplier<T> operation) throws CircuitBreakerOpenException;
    
        /**
         * Exécute avec fallback
         */
        <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback);
    
        /**
         * Force l'ouverture
         */
        void forceOpen();
    
        /**
         * Force la fermeture
         */
        void forceClose();
    
        /**
         * Reset les compteurs
         */
        void reset();
    
        /**
         * Métriques
         */
        CircuitBreakerMetrics getMetrics();
    }
    
    public enum CircuitState {
        CLOSED,
        OPEN,
        HALF_OPEN
    }
    

    3.4 Implémentation

    package eu.lmvi.socle.resilience;
    
    public class DefaultCircuitBreaker implements CircuitBreaker {
    
        private static final Logger log = LoggerFactory.getLogger(DefaultCircuitBreaker.class);
    
        private final String name;
        private final int failureThreshold;
        private final int successThreshold;
        private final long timeoutMs;
        private final int halfOpenRequests;
    
        private volatile CircuitState state = CircuitState.CLOSED;
        private final AtomicInteger failureCount = new AtomicInteger(0);
        private final AtomicInteger successCount = new AtomicInteger(0);
        private final AtomicInteger halfOpenCount = new AtomicInteger(0);
        private volatile Instant lastFailureTime;
        private final ReentrantLock lock = new ReentrantLock();
    
        @Override
        public <T> T execute(Supplier<T> operation) throws CircuitBreakerOpenException {
            if (!allowRequest()) {
                throw new CircuitBreakerOpenException(name);
            }
    
            try {
                T result = operation.get();
                onSuccess();
                return result;
            } catch (Exception e) {
                onFailure();
                throw e;
            }
        }
    
        @Override
        public <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback) {
            try {
                return execute(operation);
            } catch (CircuitBreakerOpenException e) {
                log.debug("[{}] Circuit open, using fallback", name);
                return fallback.get();
            } catch (Exception e) {
                log.warn("[{}] Operation failed, using fallback", name, e);
                return fallback.get();
            }
        }
    
        private boolean allowRequest() {
            switch (state) {
                case CLOSED:
                    return true;
    
                case OPEN:
                    // Vérifier si le timeout est passé
                    if (lastFailureTime != null &&
                        Duration.between(lastFailureTime, Instant.now()).toMillis() > timeoutMs) {
                        transitionTo(CircuitState.HALF_OPEN);
                        return true;
                    }
                    return false;
    
                case HALF_OPEN:
                    // Limiter les requêtes en half-open
                    return halfOpenCount.incrementAndGet() <= halfOpenRequests;
    
                default:
                    return false;
            }
        }
    
        private void onSuccess() {
            lock.lock();
            try {
                switch (state) {
                    case CLOSED:
                        failureCount.set(0);
                        break;
    
                    case HALF_OPEN:
                        if (successCount.incrementAndGet() >= successThreshold) {
                            transitionTo(CircuitState.CLOSED);
                        }
                        break;
                }
            } finally {
                lock.unlock();
            }
        }
    
        private void onFailure() {
            lock.lock();
            try {
                lastFailureTime = Instant.now();
    
                switch (state) {
                    case CLOSED:
                        if (failureCount.incrementAndGet() >= failureThreshold) {
                            transitionTo(CircuitState.OPEN);
                        }
                        break;
    
                    case HALF_OPEN:
                        transitionTo(CircuitState.OPEN);
                        break;
                }
            } finally {
                lock.unlock();
            }
        }
    
        private void transitionTo(CircuitState newState) {
            if (state != newState) {
                log.info("[{}] Circuit state: {} -> {}", name, state, newState);
                state = newState;
                failureCount.set(0);
                successCount.set(0);
                halfOpenCount.set(0);
            }
        }
    
        @Override
        public CircuitState getState() {
            return state;
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        @Override
        public void forceOpen() {
            transitionTo(CircuitState.OPEN);
        }
    
        @Override
        public void forceClose() {
            transitionTo(CircuitState.CLOSED);
        }
    
        @Override
        public void reset() {
            lock.lock();
            try {
                state = CircuitState.CLOSED;
                failureCount.set(0);
                successCount.set(0);
                halfOpenCount.set(0);
                lastFailureTime = null;
            } finally {
                lock.unlock();
            }
        }
    }
    

    3.5 CircuitBreakerRegistry

    @Component
    public class CircuitBreakerRegistry {
    
        private final ConcurrentHashMap<String, CircuitBreaker> circuits = new ConcurrentHashMap<>();
        private final SocleConfiguration config;
    
        public CircuitBreaker getOrCreate(String name) {
            return circuits.computeIfAbsent(name, this::createCircuitBreaker);
        }
    
        public CircuitBreaker get(String name) {
            return circuits.get(name);
        }
    
        public Map<String, CircuitState> getAllStates() {
            return circuits.entrySet().stream()
                .collect(Collectors.toMap(
                    Map.Entry::getKey,
                    e -> e.getValue().getState()
                ));
        }
    
        private CircuitBreaker createCircuitBreaker(String name) {
            return new DefaultCircuitBreaker(
                name,
                config.getResilience().getCircuitBreaker().getFailureThreshold(),
                config.getResilience().getCircuitBreaker().getSuccessThreshold(),
                config.getResilience().getCircuitBreaker().getTimeoutMs(),
                config.getResilience().getCircuitBreaker().getHalfOpenRequests()
            );
        }
    }
    

    3.6 Utilisation

    @Service
    public class PaymentService {
    
        @Autowired
        private CircuitBreakerRegistry cbRegistry;
    
        public PaymentResult processPayment(Payment payment) {
            CircuitBreaker cb = cbRegistry.getOrCreate("payment-gateway");
    
            return cb.executeWithFallback(
                () -> paymentGateway.process(payment),
                () -> {
                    // Fallback: mettre en queue pour traitement ultérieur
                    paymentQueue.enqueue(payment);
                    return PaymentResult.pending("Queued for later processing");
                }
            );
        }
    }
    

    4. Combinaison Retry + Circuit Breaker

    @Service
    public class ResilientApiClient {
    
        @Autowired
        private RetryTemplate retryTemplate;
    
        @Autowired
        private CircuitBreakerRegistry cbRegistry;
    
        public Data fetchData(String endpoint) {
            CircuitBreaker cb = cbRegistry.getOrCreate("api-" + endpoint);
    
            return cb.executeWithFallback(
                () -> retryTemplate.execute(() -> httpClient.get(endpoint)),
                () -> getCachedData(endpoint)
            );
        }
    }
    

    Ordre d’exécution

    1. CircuitBreaker vérifie si le circuit est ouvert
       → Si OPEN: fallback immédiat
       → Si CLOSED/HALF_OPEN: continue
    
    2. RetryTemplate essaie l'opération
       → Retry avec backoff exponentiel
       → Si tous les retries échouent: exception
    
    3. CircuitBreaker compte l'échec
       → Si seuil atteint: passe en OPEN
    
    4. Fallback si échec
    

    5. Annotations (optionnel)

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Resilient {
        String circuitBreaker() default "";
        int maxRetries() default 3;
        long retryDelay() default 1000;
        Class<? extends Throwable>[] retryOn() default {Exception.class};
    }
    
    @Aspect
    @Component
    public class ResilienceAspect {
    
        @Around("@annotation(resilient)")
        public Object handleResilience(ProceedingJoinPoint pjp, Resilient resilient) throws Throwable {
            String cbName = resilient.circuitBreaker();
            if (cbName.isEmpty()) {
                cbName = pjp.getSignature().getDeclaringTypeName() + "." + pjp.getSignature().getName();
            }
    
            CircuitBreaker cb = cbRegistry.getOrCreate(cbName);
    
            return cb.execute(() -> {
                return retryTemplate.execute(() -> {
                    try {
                        return pjp.proceed();
                    } catch (Throwable t) {
                        throw new RuntimeException(t);
                    }
                });
            });
        }
    }
    

    Utilisation

    @Service
    public class UserService {
    
        @Resilient(circuitBreaker = "user-api", maxRetries = 5)
        public User getUser(String id) {
            return userApiClient.fetchUser(id);
        }
    }
    

    6. Bulkhead Pattern

    Le pattern Bulkhead limite le nombre d’appels concurrents pour éviter l’épuisement des ressources.

    @Component
    public class BulkheadRegistry {
    
        private final ConcurrentHashMap<String, Semaphore> bulkheads = new ConcurrentHashMap<>();
    
        public <T> T execute(String name, int maxConcurrent, Supplier<T> operation)
                throws BulkheadFullException {
            Semaphore semaphore = bulkheads.computeIfAbsent(name,
                k -> new Semaphore(maxConcurrent));
    
            if (!semaphore.tryAcquire()) {
                throw new BulkheadFullException(name);
            }
    
            try {
                return operation.get();
            } finally {
                semaphore.release();
            }
        }
    }
    
    // Utilisation
    @Service
    public class ApiService {
    
        @Autowired
        private BulkheadRegistry bulkheadRegistry;
    
        public Data callExternalApi() {
            return bulkheadRegistry.execute("external-api", 10, () -> {
                // Max 10 appels concurrents
                return httpClient.get("/api/data");
            });
        }
    }
    

    7. Timeout Pattern

    @Component
    public class TimeoutTemplate {
    
        private final ScheduledExecutorService scheduler =
            Executors.newScheduledThreadPool(4);
    
        public <T> T executeWithTimeout(Supplier<T> operation, Duration timeout)
                throws TimeoutException {
    
            CompletableFuture<T> future = CompletableFuture.supplyAsync(operation);
    
            try {
                return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
            } catch (java.util.concurrent.TimeoutException e) {
                future.cancel(true);
                throw new TimeoutException("Operation timed out after " + timeout);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
    

    8. API Admin

    @RestController
    @RequestMapping("/admin/resilience")
    public class ResilienceController {
    
        @Autowired
        private CircuitBreakerRegistry cbRegistry;
    
        @GetMapping("/circuits")
        public Map<String, CircuitState> getCircuits() {
            return cbRegistry.getAllStates();
        }
    
        @PostMapping("/circuits/{name}/reset")
        public void resetCircuit(@PathVariable String name) {
            CircuitBreaker cb = cbRegistry.get(name);
            if (cb != null) {
                cb.reset();
            }
        }
    
        @PostMapping("/circuits/{name}/open")
        public void openCircuit(@PathVariable String name) {
            CircuitBreaker cb = cbRegistry.get(name);
            if (cb != null) {
                cb.forceOpen();
            }
        }
    
        @PostMapping("/circuits/{name}/close")
        public void closeCircuit(@PathVariable String name) {
            CircuitBreaker cb = cbRegistry.get(name);
            if (cb != null) {
                cb.forceClose();
            }
        }
    }
    

    9. Métriques

    @Component
    public class ResilienceMetrics {
    
        private final MeterRegistry registry;
    
        public void recordRetry(String operation, int attempt, boolean success) {
            Counter.builder("socle_retry_attempts")
                .tag("operation", operation)
                .tag("attempt", String.valueOf(attempt))
                .tag("success", String.valueOf(success))
                .register(registry)
                .increment();
        }
    
        public void recordCircuitBreakerState(String name, CircuitState state) {
            Gauge.builder("socle_circuit_breaker_state", () ->
                state == CircuitState.CLOSED ? 0 :
                state == CircuitState.HALF_OPEN ? 1 : 2)
                .tag("name", name)
                .register(registry);
        }
    }
    

    10. Bonnes pratiques

    DO

    • Utiliser le circuit breaker pour les appels réseau externes
    • Configurer des timeouts appropriés
    • Toujours prévoir un fallback
    • Monitorer l’état des circuits
    • Logger les transitions d’état

    DON’T

    • Ne pas utiliser le retry pour les erreurs non récupérables (400, 401)
    • Ne pas configurer des seuils trop bas (faux positifs)
    • Ne pas oublier les timeouts (risque de blocage)
    • Ne pas ignorer les métriques

    11. Références

  • Socle V004 – GraalVM JavaScript

    Socle V004 – GraalVM JavaScript

    GraalVM et JavaScript (GraalJS)

    Version : 4.0.0 Date : 2025-12-13

    1. Vue d’ensemble

    Le Socle V004 supporte l’exécution de scripts JavaScript pour l’enrichissement et la transformation de données via GraalJS, le moteur JavaScript de GraalVM.

    Pourquoi GraalVM ?

    • Performance : Compilation JIT pour JavaScript, bien plus rapide que Nashorn
    • Compatibilité : Support ECMAScript moderne (ES2023+)
    • Interopérabilité : Accès bidirectionnel entre Java et JavaScript
    • Sécurité : Sandbox configurable pour isoler l’exécution

    2. Prérequis

    2.1 GraalVM CE 21.0.2

    Important : Pour les applications utilisant GraalJS, GraalVM CE 21.0.2 est requis au lieu d’OpenJDK standard.

    OpenJDK ne contient pas le runtime Truffle nécessaire à GraalJS. Utiliser OpenJDK avec GraalJS provoquera l’erreur :

    org.graalvm.polyglot.PolyglotException:
    The Truffle API cannot be used without GraalVM runtime.
    

    2.2 Installation de GraalVM

    # Télécharger GraalVM CE 21.0.2
    wget https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz
    
    # Extraire dans /opt
    sudo tar -xzf graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz -C /opt/
    
    # Créer un lien symbolique
    sudo ln -s /opt/graalvm-community-openjdk-21.0.2+13.1 /opt/graalvm
    
    # Vérifier l'installation
    /opt/graalvm/bin/java -version
    

    Sortie attendue :

    openjdk version "21.0.2" 2024-01-16
    OpenJDK Runtime Environment GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30)
    OpenJDK 64-Bit Server VM GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30, mixed mode, sharing)
    

    3. Configuration Maven

    3.1 Version GraalJS

    La version de GraalJS doit correspondre à la version du runtime GraalVM :

    GraalVM CE GraalJS Version
    21.0.2 (23.1) 23.1.0
    22.0.0 (24.0) 24.0.0

    3.2 Dépendances pom.xml

    <properties>
        <graaljs.version>23.1.0</graaljs.version>
    </properties>
    
    <dependencies>
        <!-- GraalVM Polyglot API -->
        <dependency>
            <groupId>org.graalvm.polyglot</groupId>
            <artifactId>polyglot</artifactId>
            <version>${graaljs.version}</version>
        </dependency>
    
        <!-- GraalJS Community (runtime) -->
        <dependency>
            <groupId>org.graalvm.polyglot</groupId>
            <artifactId>js-community</artifactId>
            <version>${graaljs.version}</version>
            <type>pom</type>
            <scope>runtime</scope>
        </dependency>
    
        <!-- Script Engine (optionnel, pour JSR-223) -->
        <dependency>
            <groupId>org.graalvm.js</groupId>
            <artifactId>js-scriptengine</artifactId>
            <version>${graaljs.version}</version>
        </dependency>
    </dependencies>
    

    4. Options JVM

    4.1 Option obligatoire pour uber-jars

    Pour les fat JARs (Spring Boot repackaged), cette option est obligatoire :

    -Dpolyglotimpl.DisableClassPathIsolation=true
    

    Sans cette option, vous obtiendrez :

    java.lang.NullPointerException: Cannot invoke "java.io.File.toPath()"
    because the return value of "com.oracle.truffle.polyglot.FileSystems$InternalFileSystemContext.collectClassPathJars()"
    

    4.2 Supprimer les avertissements

    Pour supprimer les avertissements « interpreter only » :

    -Dpolyglot.engine.WarnInterpreterOnly=false
    

    4.3 Configuration systemd complète

    [Service]
    Environment="JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC -Dpolyglotimpl.DisableClassPathIsolation=true"
    ExecStart=/opt/graalvm/bin/java $JAVA_OPTS -jar /opt/myapp/myapp.jar
    

    5. Utilisation de GraalJS

    5.1 Exemple basique

    import org.graalvm.polyglot.Context;
    import org.graalvm.polyglot.Value;
    
    public class JavaScriptExecutor {
    
        public String execute(String script, Map<String, Object> bindings) {
            try (Context context = Context.newBuilder("js")
                    .allowAllAccess(false)
                    .allowHostAccess(HostAccess.ALL)
                    .allowHostClassLookup(className -> false)
                    .option("engine.WarnInterpreterOnly", "false")
                    .build()) {
    
                Value jsBindings = context.getBindings("js");
                bindings.forEach(jsBindings::putMember);
    
                Value result = context.eval("js", script);
                return result.asString();
            }
        }
    }
    

    5.2 Options de sécurité

    Option Description
    allowAllAccess(false) Désactive tous les accès par défaut
    allowHostAccess(HostAccess.ALL) Permet l’accès aux objets Java passés
    allowHostClassLookup(className -> false) Empêche Java.type()
    allowIO(false) Désactive les accès fichiers
    allowNativeAccess(false) Désactive les appels natifs

    6. Performance et initialisation

    6.1 Temps d’initialisation

    La première exécution de GraalJS prend environ 10-15 secondes car :

    • Chargement du runtime Truffle
    • Compilation JIT du moteur JavaScript
    • Initialisation des structures internes

    Les exécutions suivantes sont très rapides (<100ms).

    6.2 Pré-initialisation au démarrage

    Pour éviter la latence sur le premier événement, vous pouvez pré-initialiser GraalVM :

    @Component
    public class GraalVMWarmup {
    
        private Engine sharedEngine;
    
        @PostConstruct
        public void init() {
            log.info("Pre-initializing GraalVM JavaScript engine...");
            long start = System.currentTimeMillis();
    
            this.sharedEngine = Engine.newBuilder()
                .option("engine.WarnInterpreterOnly", "false")
                .build();
    
            // Warm-up avec un script simple
            try (Context warmup = Context.newBuilder("js")
                    .engine(sharedEngine)
                    .allowAllAccess(false)
                    .allowHostAccess(HostAccess.ALL)
                    .build()) {
                warmup.eval("js", "var x = 1 + 1;");
            }
    
            log.info("GraalVM initialized in {} ms", System.currentTimeMillis() - start);
        }
    
        @PreDestroy
        public void cleanup() {
            if (sharedEngine != null) {
                sharedEngine.close();
            }
        }
    
        public Engine getSharedEngine() {
            return sharedEngine;
        }
    }
    

    6.3 Impact sur le démarrage

    Configuration Démarrage app Premier événement
    Sans pré-init ~40s ~12s
    Avec pré-init ~52s (+12s) <100ms

    7. Troubleshooting

    7.1 « Truffle API cannot be used without GraalVM runtime »

    Cause : Utilisation d’OpenJDK au lieu de GraalVM Solution : Installer et utiliser GraalVM CE 21.0.2

    7.2 « NullPointerException in collectClassPathJars »

    Cause : Classpath isolation dans un uber-jar Solution : Ajouter -Dpolyglotimpl.DisableClassPathIsolation=true

    7.3 « Version mismatch: truffle-api-24.0.0 vs GraalVM 23.1 »

    Cause : Version GraalJS incompatible avec le runtime GraalVM Solution : Aligner graaljs.version avec la version GraalVM installée

    7.4 Script timeout

    Cause : Initialisation GraalVM dépasse le timeout Solution : Augmenter le timeout à 30 secondes pour le premier appel, ou pré-initialiser

    8. Références

  • Socle V004 – Résilience

    Socle V004 – Résilience

    11 – Resilience (Circuit Breaker & Retry)

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Le Socle V4 intègre des patterns de résilience pour gérer les défaillances des systèmes externes :

    • Retry : Réessayer les opérations échouées
    • Circuit Breaker : Protéger contre les cascades de défaillances

    2. Retry Pattern

    2.1 Configuration

    socle:
      resilience:
        retry:
          max-attempts: ${RETRY_MAX_ATTEMPTS:3}
          initial-delay-ms: ${RETRY_INITIAL_DELAY_MS:1000}
          max-delay-ms: ${RETRY_MAX_DELAY_MS:30000}
          multiplier: ${RETRY_MULTIPLIER:2.0}
    

    2.2 Interface RetryTemplate

    package eu.lmvi.socle.resilience;
    
    public interface RetryTemplate {
    
        /**
         * Exécute une opération avec retry
         */
        <T> T execute(Supplier<T> operation) throws RetryExhaustedException;
    
        /**
         * Exécute une opération avec retry et fallback
         */
        <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback);
    
        /**
         * Exécute une opération void avec retry
         */
        void executeVoid(Runnable operation) throws RetryExhaustedException;
    }
    

    2.3 Implémentation

    package eu.lmvi.socle.resilience;
    
    @Component
    public class ExponentialBackoffRetryTemplate implements RetryTemplate {
    
        private static final Logger log = LoggerFactory.getLogger(ExponentialBackoffRetryTemplate.class);
    
        private final int maxAttempts;
        private final long initialDelayMs;
        private final long maxDelayMs;
        private final double multiplier;
    
        public ExponentialBackoffRetryTemplate(SocleConfiguration config) {
            this.maxAttempts = config.getResilience().getRetry().getMaxAttempts();
            this.initialDelayMs = config.getResilience().getRetry().getInitialDelayMs();
            this.maxDelayMs = config.getResilience().getRetry().getMaxDelayMs();
            this.multiplier = config.getResilience().getRetry().getMultiplier();
        }
    
        @Override
        public <T> T execute(Supplier<T> operation) throws RetryExhaustedException {
            Exception lastException = null;
            long delay = initialDelayMs;
    
            for (int attempt = 1; attempt <= maxAttempts; attempt++) {
                try {
                    return operation.get();
                } catch (Exception e) {
                    lastException = e;
                    log.warn("Attempt {}/{} failed: {}", attempt, maxAttempts, e.getMessage());
    
                    if (attempt < maxAttempts) {
                        sleep(delay);
                        delay = Math.min((long) (delay * multiplier), maxDelayMs);
                    }
                }
            }
    
            throw new RetryExhaustedException(
                "Operation failed after " + maxAttempts + " attempts",
                lastException
            );
        }
    
        @Override
        public <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback) {
            try {
                return execute(operation);
            } catch (RetryExhaustedException e) {
                log.warn("All retries exhausted, using fallback");
                return fallback.get();
            }
        }
    
        @Override
        public void executeVoid(Runnable operation) throws RetryExhaustedException {
            execute(() -> {
                operation.run();
                return null;
            });
        }
    
        private void sleep(long ms) {
            try {
                Thread.sleep(ms);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Retry interrupted", e);
            }
        }
    }
    

    2.4 Utilisation

    @Service
    public class ExternalApiService {
    
        @Autowired
        private RetryTemplate retryTemplate;
    
        public Data fetchData(String id) {
            return retryTemplate.execute(() -> {
                // Appel qui peut échouer
                return httpClient.get("/data/" + id);
            });
        }
    
        public Data fetchDataWithFallback(String id) {
            return retryTemplate.executeWithFallback(
                () -> httpClient.get("/data/" + id),
                () -> getCachedData(id)  // Fallback vers le cache
            );
        }
    }
    

    3. Circuit Breaker Pattern

    3.1 États

            succès
         ┌─────────────────┐
         │                 │
         ▼                 │
    ┌─────────┐      ┌─────┴─────┐      ┌─────────┐
    │  CLOSED │─────►│   OPEN    │─────►│HALF_OPEN│
    └────┬────┘      └───────────┘      └────┬────┘
         │  échecs        │ timeout          │
         │  threshold     │                  │
         │                │                  │
         └────────────────┴──────────────────┘
                  retour succès
    
    • CLOSED : Fonctionnement normal, les requêtes passent
    • OPEN : Circuit ouvert, les requêtes échouent immédiatement
    • HALF_OPEN : Test de reprise, quelques requêtes passent

    3.2 Configuration

    socle:
      resilience:
        circuit-breaker:
          failure-threshold: ${CB_FAILURE_THRESHOLD:5}
          success-threshold: ${CB_SUCCESS_THRESHOLD:3}
          timeout-ms: ${CB_TIMEOUT_MS:60000}
          half-open-requests: ${CB_HALF_OPEN_REQUESTS:3}
    

    3.3 Interface CircuitBreaker

    package eu.lmvi.socle.resilience;
    
    public interface CircuitBreaker {
    
        /**
         * Nom du circuit
         */
        String getName();
    
        /**
         * État actuel
         */
        CircuitState getState();
    
        /**
         * Exécute une opération protégée
         */
        <T> T execute(Supplier<T> operation) throws CircuitBreakerOpenException;
    
        /**
         * Exécute avec fallback
         */
        <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback);
    
        /**
         * Force l'ouverture
         */
        void forceOpen();
    
        /**
         * Force la fermeture
         */
        void forceClose();
    
        /**
         * Reset les compteurs
         */
        void reset();
    
        /**
         * Métriques
         */
        CircuitBreakerMetrics getMetrics();
    }
    
    public enum CircuitState {
        CLOSED,
        OPEN,
        HALF_OPEN
    }
    

    3.4 Implémentation

    package eu.lmvi.socle.resilience;
    
    public class DefaultCircuitBreaker implements CircuitBreaker {
    
        private static final Logger log = LoggerFactory.getLogger(DefaultCircuitBreaker.class);
    
        private final String name;
        private final int failureThreshold;
        private final int successThreshold;
        private final long timeoutMs;
        private final int halfOpenRequests;
    
        private volatile CircuitState state = CircuitState.CLOSED;
        private final AtomicInteger failureCount = new AtomicInteger(0);
        private final AtomicInteger successCount = new AtomicInteger(0);
        private final AtomicInteger halfOpenCount = new AtomicInteger(0);
        private volatile Instant lastFailureTime;
        private final ReentrantLock lock = new ReentrantLock();
    
        @Override
        public <T> T execute(Supplier<T> operation) throws CircuitBreakerOpenException {
            if (!allowRequest()) {
                throw new CircuitBreakerOpenException(name);
            }
    
            try {
                T result = operation.get();
                onSuccess();
                return result;
            } catch (Exception e) {
                onFailure();
                throw e;
            }
        }
    
        @Override
        public <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback) {
            try {
                return execute(operation);
            } catch (CircuitBreakerOpenException e) {
                log.debug("[{}] Circuit open, using fallback", name);
                return fallback.get();
            } catch (Exception e) {
                log.warn("[{}] Operation failed, using fallback", name, e);
                return fallback.get();
            }
        }
    
        private boolean allowRequest() {
            switch (state) {
                case CLOSED:
                    return true;
    
                case OPEN:
                    // Vérifier si le timeout est passé
                    if (lastFailureTime != null &&
                        Duration.between(lastFailureTime, Instant.now()).toMillis() > timeoutMs) {
                        transitionTo(CircuitState.HALF_OPEN);
                        return true;
                    }
                    return false;
    
                case HALF_OPEN:
                    // Limiter les requêtes en half-open
                    return halfOpenCount.incrementAndGet() <= halfOpenRequests;
    
                default:
                    return false;
            }
        }
    
        private void onSuccess() {
            lock.lock();
            try {
                switch (state) {
                    case CLOSED:
                        failureCount.set(0);
                        break;
    
                    case HALF_OPEN:
                        if (successCount.incrementAndGet() >= successThreshold) {
                            transitionTo(CircuitState.CLOSED);
                        }
                        break;
                }
            } finally {
                lock.unlock();
            }
        }
    
        private void onFailure() {
            lock.lock();
            try {
                lastFailureTime = Instant.now();
    
                switch (state) {
                    case CLOSED:
                        if (failureCount.incrementAndGet() >= failureThreshold) {
                            transitionTo(CircuitState.OPEN);
                        }
                        break;
    
                    case HALF_OPEN:
                        transitionTo(CircuitState.OPEN);
                        break;
                }
            } finally {
                lock.unlock();
            }
        }
    
        private void transitionTo(CircuitState newState) {
            if (state != newState) {
                log.info("[{}] Circuit state: {} -> {}", name, state, newState);
                state = newState;
                failureCount.set(0);
                successCount.set(0);
                halfOpenCount.set(0);
            }
        }
    
        @Override
        public CircuitState getState() {
            return state;
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        @Override
        public void forceOpen() {
            transitionTo(CircuitState.OPEN);
        }
    
        @Override
        public void forceClose() {
            transitionTo(CircuitState.CLOSED);
        }
    
        @Override
        public void reset() {
            lock.lock();
            try {
                state = CircuitState.CLOSED;
                failureCount.set(0);
                successCount.set(0);
                halfOpenCount.set(0);
                lastFailureTime = null;
            } finally {
                lock.unlock();
            }
        }
    }
    

    3.5 CircuitBreakerRegistry

    @Component
    public class CircuitBreakerRegistry {
    
        private final ConcurrentHashMap<String, CircuitBreaker> circuits = new ConcurrentHashMap<>();
        private final SocleConfiguration config;
    
        public CircuitBreaker getOrCreate(String name) {
            return circuits.computeIfAbsent(name, this::createCircuitBreaker);
        }
    
        public CircuitBreaker get(String name) {
            return circuits.get(name);
        }
    
        public Map<String, CircuitState> getAllStates() {
            return circuits.entrySet().stream()
                .collect(Collectors.toMap(
                    Map.Entry::getKey,
                    e -> e.getValue().getState()
                ));
        }
    
        private CircuitBreaker createCircuitBreaker(String name) {
            return new DefaultCircuitBreaker(
                name,
                config.getResilience().getCircuitBreaker().getFailureThreshold(),
                config.getResilience().getCircuitBreaker().getSuccessThreshold(),
                config.getResilience().getCircuitBreaker().getTimeoutMs(),
                config.getResilience().getCircuitBreaker().getHalfOpenRequests()
            );
        }
    }
    

    3.6 Utilisation

    @Service
    public class PaymentService {
    
        @Autowired
        private CircuitBreakerRegistry cbRegistry;
    
        public PaymentResult processPayment(Payment payment) {
            CircuitBreaker cb = cbRegistry.getOrCreate("payment-gateway");
    
            return cb.executeWithFallback(
                () -> paymentGateway.process(payment),
                () -> {
                    // Fallback: mettre en queue pour traitement ultérieur
                    paymentQueue.enqueue(payment);
                    return PaymentResult.pending("Queued for later processing");
                }
            );
        }
    }
    

    4. Combinaison Retry + Circuit Breaker

    @Service
    public class ResilientApiClient {
    
        @Autowired
        private RetryTemplate retryTemplate;
    
        @Autowired
        private CircuitBreakerRegistry cbRegistry;
    
        public Data fetchData(String endpoint) {
            CircuitBreaker cb = cbRegistry.getOrCreate("api-" + endpoint);
    
            return cb.executeWithFallback(
                () -> retryTemplate.execute(() -> httpClient.get(endpoint)),
                () -> getCachedData(endpoint)
            );
        }
    }
    

    Ordre d’exécution

    1. CircuitBreaker vérifie si le circuit est ouvert
       → Si OPEN: fallback immédiat
       → Si CLOSED/HALF_OPEN: continue
    
    2. RetryTemplate essaie l'opération
       → Retry avec backoff exponentiel
       → Si tous les retries échouent: exception
    
    3. CircuitBreaker compte l'échec
       → Si seuil atteint: passe en OPEN
    
    4. Fallback si échec
    

    5. Annotations (optionnel)

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Resilient {
        String circuitBreaker() default "";
        int maxRetries() default 3;
        long retryDelay() default 1000;
        Class<? extends Throwable>[] retryOn() default {Exception.class};
    }
    
    @Aspect
    @Component
    public class ResilienceAspect {
    
        @Around("@annotation(resilient)")
        public Object handleResilience(ProceedingJoinPoint pjp, Resilient resilient) throws Throwable {
            String cbName = resilient.circuitBreaker();
            if (cbName.isEmpty()) {
                cbName = pjp.getSignature().getDeclaringTypeName() + "." + pjp.getSignature().getName();
            }
    
            CircuitBreaker cb = cbRegistry.getOrCreate(cbName);
    
            return cb.execute(() -> {
                return retryTemplate.execute(() -> {
                    try {
                        return pjp.proceed();
                    } catch (Throwable t) {
                        throw new RuntimeException(t);
                    }
                });
            });
        }
    }
    

    Utilisation

    @Service
    public class UserService {
    
        @Resilient(circuitBreaker = "user-api", maxRetries = 5)
        public User getUser(String id) {
            return userApiClient.fetchUser(id);
        }
    }
    

    6. Bulkhead Pattern

    Le pattern Bulkhead limite le nombre d’appels concurrents pour éviter l’épuisement des ressources.

    @Component
    public class BulkheadRegistry {
    
        private final ConcurrentHashMap<String, Semaphore> bulkheads = new ConcurrentHashMap<>();
    
        public <T> T execute(String name, int maxConcurrent, Supplier<T> operation)
                throws BulkheadFullException {
            Semaphore semaphore = bulkheads.computeIfAbsent(name,
                k -> new Semaphore(maxConcurrent));
    
            if (!semaphore.tryAcquire()) {
                throw new BulkheadFullException(name);
            }
    
            try {
                return operation.get();
            } finally {
                semaphore.release();
            }
        }
    }
    
    // Utilisation
    @Service
    public class ApiService {
    
        @Autowired
        private BulkheadRegistry bulkheadRegistry;
    
        public Data callExternalApi() {
            return bulkheadRegistry.execute("external-api", 10, () -> {
                // Max 10 appels concurrents
                return httpClient.get("/api/data");
            });
        }
    }
    

    7. Timeout Pattern

    @Component
    public class TimeoutTemplate {
    
        private final ScheduledExecutorService scheduler =
            Executors.newScheduledThreadPool(4);
    
        public <T> T executeWithTimeout(Supplier<T> operation, Duration timeout)
                throws TimeoutException {
    
            CompletableFuture<T> future = CompletableFuture.supplyAsync(operation);
    
            try {
                return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
            } catch (java.util.concurrent.TimeoutException e) {
                future.cancel(true);
                throw new TimeoutException("Operation timed out after " + timeout);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
    

    8. API Admin

    @RestController
    @RequestMapping("/admin/resilience")
    public class ResilienceController {
    
        @Autowired
        private CircuitBreakerRegistry cbRegistry;
    
        @GetMapping("/circuits")
        public Map<String, CircuitState> getCircuits() {
            return cbRegistry.getAllStates();
        }
    
        @PostMapping("/circuits/{name}/reset")
        public void resetCircuit(@PathVariable String name) {
            CircuitBreaker cb = cbRegistry.get(name);
            if (cb != null) {
                cb.reset();
            }
        }
    
        @PostMapping("/circuits/{name}/open")
        public void openCircuit(@PathVariable String name) {
            CircuitBreaker cb = cbRegistry.get(name);
            if (cb != null) {
                cb.forceOpen();
            }
        }
    
        @PostMapping("/circuits/{name}/close")
        public void closeCircuit(@PathVariable String name) {
            CircuitBreaker cb = cbRegistry.get(name);
            if (cb != null) {
                cb.forceClose();
            }
        }
    }
    

    9. Métriques

    @Component
    public class ResilienceMetrics {
    
        private final MeterRegistry registry;
    
        public void recordRetry(String operation, int attempt, boolean success) {
            Counter.builder("socle_retry_attempts")
                .tag("operation", operation)
                .tag("attempt", String.valueOf(attempt))
                .tag("success", String.valueOf(success))
                .register(registry)
                .increment();
        }
    
        public void recordCircuitBreakerState(String name, CircuitState state) {
            Gauge.builder("socle_circuit_breaker_state", () ->
                state == CircuitState.CLOSED ? 0 :
                state == CircuitState.HALF_OPEN ? 1 : 2)
                .tag("name", name)
                .register(registry);
        }
    }
    

    10. Bonnes pratiques

    DO

    • Utiliser le circuit breaker pour les appels réseau externes
    • Configurer des timeouts appropriés
    • Toujours prévoir un fallback
    • Monitorer l’état des circuits
    • Logger les transitions d’état

    DON’T

    • Ne pas utiliser le retry pour les erreurs non récupérables (400, 401)
    • Ne pas configurer des seuils trop bas (faux positifs)
    • Ne pas oublier les timeouts (risque de blocage)
    • Ne pas ignorer les métriques

    11. Références

  • Socle V004 – GraalVM JavaScript

    Socle V004 – GraalVM JavaScript

    GraalVM et JavaScript (GraalJS)

    Version : 4.0.0 Date : 2025-12-13

    1. Vue d’ensemble

    Le Socle V004 supporte l’exécution de scripts JavaScript pour l’enrichissement et la transformation de données via GraalJS, le moteur JavaScript de GraalVM.

    Pourquoi GraalVM ?

    • Performance : Compilation JIT pour JavaScript, bien plus rapide que Nashorn
    • Compatibilité : Support ECMAScript moderne (ES2023+)
    • Interopérabilité : Accès bidirectionnel entre Java et JavaScript
    • Sécurité : Sandbox configurable pour isoler l’exécution

    2. Prérequis

    2.1 GraalVM CE 21.0.2

    Important : Pour les applications utilisant GraalJS, GraalVM CE 21.0.2 est requis au lieu d’OpenJDK standard.

    OpenJDK ne contient pas le runtime Truffle nécessaire à GraalJS. Utiliser OpenJDK avec GraalJS provoquera l’erreur :

    org.graalvm.polyglot.PolyglotException:
    The Truffle API cannot be used without GraalVM runtime.
    

    2.2 Installation de GraalVM

    # Télécharger GraalVM CE 21.0.2
    wget https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz
    
    # Extraire dans /opt
    sudo tar -xzf graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz -C /opt/
    
    # Créer un lien symbolique
    sudo ln -s /opt/graalvm-community-openjdk-21.0.2+13.1 /opt/graalvm
    
    # Vérifier l'installation
    /opt/graalvm/bin/java -version
    

    Sortie attendue :

    openjdk version "21.0.2" 2024-01-16
    OpenJDK Runtime Environment GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30)
    OpenJDK 64-Bit Server VM GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30, mixed mode, sharing)
    

    3. Configuration Maven

    3.1 Version GraalJS

    La version de GraalJS doit correspondre à la version du runtime GraalVM :

    GraalVM CE GraalJS Version
    21.0.2 (23.1) 23.1.0
    22.0.0 (24.0) 24.0.0

    3.2 Dépendances pom.xml

    <properties>
        <graaljs.version>23.1.0</graaljs.version>
    </properties>
    
    <dependencies>
        <!-- GraalVM Polyglot API -->
        <dependency>
            <groupId>org.graalvm.polyglot</groupId>
            <artifactId>polyglot</artifactId>
            <version>${graaljs.version}</version>
        </dependency>
    
        <!-- GraalJS Community (runtime) -->
        <dependency>
            <groupId>org.graalvm.polyglot</groupId>
            <artifactId>js-community</artifactId>
            <version>${graaljs.version}</version>
            <type>pom</type>
            <scope>runtime</scope>
        </dependency>
    
        <!-- Script Engine (optionnel, pour JSR-223) -->
        <dependency>
            <groupId>org.graalvm.js</groupId>
            <artifactId>js-scriptengine</artifactId>
            <version>${graaljs.version}</version>
        </dependency>
    </dependencies>
    

    4. Options JVM

    4.1 Option obligatoire pour uber-jars

    Pour les fat JARs (Spring Boot repackaged), cette option est obligatoire :

    -Dpolyglotimpl.DisableClassPathIsolation=true
    

    Sans cette option, vous obtiendrez :

    java.lang.NullPointerException: Cannot invoke "java.io.File.toPath()"
    because the return value of "com.oracle.truffle.polyglot.FileSystems$InternalFileSystemContext.collectClassPathJars()"
    

    4.2 Supprimer les avertissements

    Pour supprimer les avertissements « interpreter only » :

    -Dpolyglot.engine.WarnInterpreterOnly=false
    

    4.3 Configuration systemd complète

    [Service]
    Environment="JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC -Dpolyglotimpl.DisableClassPathIsolation=true"
    ExecStart=/opt/graalvm/bin/java $JAVA_OPTS -jar /opt/myapp/myapp.jar
    

    5. Utilisation de GraalJS

    5.1 Exemple basique

    import org.graalvm.polyglot.Context;
    import org.graalvm.polyglot.Value;
    
    public class JavaScriptExecutor {
    
        public String execute(String script, Map<String, Object> bindings) {
            try (Context context = Context.newBuilder("js")
                    .allowAllAccess(false)
                    .allowHostAccess(HostAccess.ALL)
                    .allowHostClassLookup(className -> false)
                    .option("engine.WarnInterpreterOnly", "false")
                    .build()) {
    
                Value jsBindings = context.getBindings("js");
                bindings.forEach(jsBindings::putMember);
    
                Value result = context.eval("js", script);
                return result.asString();
            }
        }
    }
    

    5.2 Options de sécurité

    Option Description
    allowAllAccess(false) Désactive tous les accès par défaut
    allowHostAccess(HostAccess.ALL) Permet l’accès aux objets Java passés
    allowHostClassLookup(className -> false) Empêche Java.type()
    allowIO(false) Désactive les accès fichiers
    allowNativeAccess(false) Désactive les appels natifs

    6. Performance et initialisation

    6.1 Temps d’initialisation

    La première exécution de GraalJS prend environ 10-15 secondes car :

    • Chargement du runtime Truffle
    • Compilation JIT du moteur JavaScript
    • Initialisation des structures internes

    Les exécutions suivantes sont très rapides (<100ms).

    6.2 Pré-initialisation au démarrage

    Pour éviter la latence sur le premier événement, vous pouvez pré-initialiser GraalVM :

    @Component
    public class GraalVMWarmup {
    
        private Engine sharedEngine;
    
        @PostConstruct
        public void init() {
            log.info("Pre-initializing GraalVM JavaScript engine...");
            long start = System.currentTimeMillis();
    
            this.sharedEngine = Engine.newBuilder()
                .option("engine.WarnInterpreterOnly", "false")
                .build();
    
            // Warm-up avec un script simple
            try (Context warmup = Context.newBuilder("js")
                    .engine(sharedEngine)
                    .allowAllAccess(false)
                    .allowHostAccess(HostAccess.ALL)
                    .build()) {
                warmup.eval("js", "var x = 1 + 1;");
            }
    
            log.info("GraalVM initialized in {} ms", System.currentTimeMillis() - start);
        }
    
        @PreDestroy
        public void cleanup() {
            if (sharedEngine != null) {
                sharedEngine.close();
            }
        }
    
        public Engine getSharedEngine() {
            return sharedEngine;
        }
    }
    

    6.3 Impact sur le démarrage

    Configuration Démarrage app Premier événement
    Sans pré-init ~40s ~12s
    Avec pré-init ~52s (+12s) <100ms

    7. Troubleshooting

    7.1 « Truffle API cannot be used without GraalVM runtime »

    Cause : Utilisation d’OpenJDK au lieu de GraalVM Solution : Installer et utiliser GraalVM CE 21.0.2

    7.2 « NullPointerException in collectClassPathJars »

    Cause : Classpath isolation dans un uber-jar Solution : Ajouter -Dpolyglotimpl.DisableClassPathIsolation=true

    7.3 « Version mismatch: truffle-api-24.0.0 vs GraalVM 23.1 »

    Cause : Version GraalJS incompatible avec le runtime GraalVM Solution : Aligner graaljs.version avec la version GraalVM installée

    7.4 Script timeout

    Cause : Initialisation GraalVM dépasse le timeout Solution : Augmenter le timeout à 30 secondes pour le premier appel, ou pré-initialiser

    8. Références

  • Socle V004 – Résilience

    Socle V004 – Résilience

    11 – Resilience (Circuit Breaker & Retry)

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Le Socle V4 intègre des patterns de résilience pour gérer les défaillances des systèmes externes :

    • Retry : Réessayer les opérations échouées
    • Circuit Breaker : Protéger contre les cascades de défaillances

    2. Retry Pattern

    2.1 Configuration

    socle:
      resilience:
        retry:
          max-attempts: ${RETRY_MAX_ATTEMPTS:3}
          initial-delay-ms: ${RETRY_INITIAL_DELAY_MS:1000}
          max-delay-ms: ${RETRY_MAX_DELAY_MS:30000}
          multiplier: ${RETRY_MULTIPLIER:2.0}
    

    2.2 Interface RetryTemplate

    package eu.lmvi.socle.resilience;
    
    public interface RetryTemplate {
    
        /**
         * Exécute une opération avec retry
         */
        <T> T execute(Supplier<T> operation) throws RetryExhaustedException;
    
        /**
         * Exécute une opération avec retry et fallback
         */
        <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback);
    
        /**
         * Exécute une opération void avec retry
         */
        void executeVoid(Runnable operation) throws RetryExhaustedException;
    }
    

    2.3 Implémentation

    package eu.lmvi.socle.resilience;
    
    @Component
    public class ExponentialBackoffRetryTemplate implements RetryTemplate {
    
        private static final Logger log = LoggerFactory.getLogger(ExponentialBackoffRetryTemplate.class);
    
        private final int maxAttempts;
        private final long initialDelayMs;
        private final long maxDelayMs;
        private final double multiplier;
    
        public ExponentialBackoffRetryTemplate(SocleConfiguration config) {
            this.maxAttempts = config.getResilience().getRetry().getMaxAttempts();
            this.initialDelayMs = config.getResilience().getRetry().getInitialDelayMs();
            this.maxDelayMs = config.getResilience().getRetry().getMaxDelayMs();
            this.multiplier = config.getResilience().getRetry().getMultiplier();
        }
    
        @Override
        public <T> T execute(Supplier<T> operation) throws RetryExhaustedException {
            Exception lastException = null;
            long delay = initialDelayMs;
    
            for (int attempt = 1; attempt <= maxAttempts; attempt++) {
                try {
                    return operation.get();
                } catch (Exception e) {
                    lastException = e;
                    log.warn("Attempt {}/{} failed: {}", attempt, maxAttempts, e.getMessage());
    
                    if (attempt < maxAttempts) {
                        sleep(delay);
                        delay = Math.min((long) (delay * multiplier), maxDelayMs);
                    }
                }
            }
    
            throw new RetryExhaustedException(
                "Operation failed after " + maxAttempts + " attempts",
                lastException
            );
        }
    
        @Override
        public <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback) {
            try {
                return execute(operation);
            } catch (RetryExhaustedException e) {
                log.warn("All retries exhausted, using fallback");
                return fallback.get();
            }
        }
    
        @Override
        public void executeVoid(Runnable operation) throws RetryExhaustedException {
            execute(() -> {
                operation.run();
                return null;
            });
        }
    
        private void sleep(long ms) {
            try {
                Thread.sleep(ms);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Retry interrupted", e);
            }
        }
    }
    

    2.4 Utilisation

    @Service
    public class ExternalApiService {
    
        @Autowired
        private RetryTemplate retryTemplate;
    
        public Data fetchData(String id) {
            return retryTemplate.execute(() -> {
                // Appel qui peut échouer
                return httpClient.get("/data/" + id);
            });
        }
    
        public Data fetchDataWithFallback(String id) {
            return retryTemplate.executeWithFallback(
                () -> httpClient.get("/data/" + id),
                () -> getCachedData(id)  // Fallback vers le cache
            );
        }
    }
    

    3. Circuit Breaker Pattern

    3.1 États

            succès
         ┌─────────────────┐
         │                 │
         ▼                 │
    ┌─────────┐      ┌─────┴─────┐      ┌─────────┐
    │  CLOSED │─────►│   OPEN    │─────►│HALF_OPEN│
    └────┬────┘      └───────────┘      └────┬────┘
         │  échecs        │ timeout          │
         │  threshold     │                  │
         │                │                  │
         └────────────────┴──────────────────┘
                  retour succès
    
    • CLOSED : Fonctionnement normal, les requêtes passent
    • OPEN : Circuit ouvert, les requêtes échouent immédiatement
    • HALF_OPEN : Test de reprise, quelques requêtes passent

    3.2 Configuration

    socle:
      resilience:
        circuit-breaker:
          failure-threshold: ${CB_FAILURE_THRESHOLD:5}
          success-threshold: ${CB_SUCCESS_THRESHOLD:3}
          timeout-ms: ${CB_TIMEOUT_MS:60000}
          half-open-requests: ${CB_HALF_OPEN_REQUESTS:3}
    

    3.3 Interface CircuitBreaker

    package eu.lmvi.socle.resilience;
    
    public interface CircuitBreaker {
    
        /**
         * Nom du circuit
         */
        String getName();
    
        /**
         * État actuel
         */
        CircuitState getState();
    
        /**
         * Exécute une opération protégée
         */
        <T> T execute(Supplier<T> operation) throws CircuitBreakerOpenException;
    
        /**
         * Exécute avec fallback
         */
        <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback);
    
        /**
         * Force l'ouverture
         */
        void forceOpen();
    
        /**
         * Force la fermeture
         */
        void forceClose();
    
        /**
         * Reset les compteurs
         */
        void reset();
    
        /**
         * Métriques
         */
        CircuitBreakerMetrics getMetrics();
    }
    
    public enum CircuitState {
        CLOSED,
        OPEN,
        HALF_OPEN
    }
    

    3.4 Implémentation

    package eu.lmvi.socle.resilience;
    
    public class DefaultCircuitBreaker implements CircuitBreaker {
    
        private static final Logger log = LoggerFactory.getLogger(DefaultCircuitBreaker.class);
    
        private final String name;
        private final int failureThreshold;
        private final int successThreshold;
        private final long timeoutMs;
        private final int halfOpenRequests;
    
        private volatile CircuitState state = CircuitState.CLOSED;
        private final AtomicInteger failureCount = new AtomicInteger(0);
        private final AtomicInteger successCount = new AtomicInteger(0);
        private final AtomicInteger halfOpenCount = new AtomicInteger(0);
        private volatile Instant lastFailureTime;
        private final ReentrantLock lock = new ReentrantLock();
    
        @Override
        public <T> T execute(Supplier<T> operation) throws CircuitBreakerOpenException {
            if (!allowRequest()) {
                throw new CircuitBreakerOpenException(name);
            }
    
            try {
                T result = operation.get();
                onSuccess();
                return result;
            } catch (Exception e) {
                onFailure();
                throw e;
            }
        }
    
        @Override
        public <T> T executeWithFallback(Supplier<T> operation, Supplier<T> fallback) {
            try {
                return execute(operation);
            } catch (CircuitBreakerOpenException e) {
                log.debug("[{}] Circuit open, using fallback", name);
                return fallback.get();
            } catch (Exception e) {
                log.warn("[{}] Operation failed, using fallback", name, e);
                return fallback.get();
            }
        }
    
        private boolean allowRequest() {
            switch (state) {
                case CLOSED:
                    return true;
    
                case OPEN:
                    // Vérifier si le timeout est passé
                    if (lastFailureTime != null &&
                        Duration.between(lastFailureTime, Instant.now()).toMillis() > timeoutMs) {
                        transitionTo(CircuitState.HALF_OPEN);
                        return true;
                    }
                    return false;
    
                case HALF_OPEN:
                    // Limiter les requêtes en half-open
                    return halfOpenCount.incrementAndGet() <= halfOpenRequests;
    
                default:
                    return false;
            }
        }
    
        private void onSuccess() {
            lock.lock();
            try {
                switch (state) {
                    case CLOSED:
                        failureCount.set(0);
                        break;
    
                    case HALF_OPEN:
                        if (successCount.incrementAndGet() >= successThreshold) {
                            transitionTo(CircuitState.CLOSED);
                        }
                        break;
                }
            } finally {
                lock.unlock();
            }
        }
    
        private void onFailure() {
            lock.lock();
            try {
                lastFailureTime = Instant.now();
    
                switch (state) {
                    case CLOSED:
                        if (failureCount.incrementAndGet() >= failureThreshold) {
                            transitionTo(CircuitState.OPEN);
                        }
                        break;
    
                    case HALF_OPEN:
                        transitionTo(CircuitState.OPEN);
                        break;
                }
            } finally {
                lock.unlock();
            }
        }
    
        private void transitionTo(CircuitState newState) {
            if (state != newState) {
                log.info("[{}] Circuit state: {} -> {}", name, state, newState);
                state = newState;
                failureCount.set(0);
                successCount.set(0);
                halfOpenCount.set(0);
            }
        }
    
        @Override
        public CircuitState getState() {
            return state;
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        @Override
        public void forceOpen() {
            transitionTo(CircuitState.OPEN);
        }
    
        @Override
        public void forceClose() {
            transitionTo(CircuitState.CLOSED);
        }
    
        @Override
        public void reset() {
            lock.lock();
            try {
                state = CircuitState.CLOSED;
                failureCount.set(0);
                successCount.set(0);
                halfOpenCount.set(0);
                lastFailureTime = null;
            } finally {
                lock.unlock();
            }
        }
    }
    

    3.5 CircuitBreakerRegistry

    @Component
    public class CircuitBreakerRegistry {
    
        private final ConcurrentHashMap<String, CircuitBreaker> circuits = new ConcurrentHashMap<>();
        private final SocleConfiguration config;
    
        public CircuitBreaker getOrCreate(String name) {
            return circuits.computeIfAbsent(name, this::createCircuitBreaker);
        }
    
        public CircuitBreaker get(String name) {
            return circuits.get(name);
        }
    
        public Map<String, CircuitState> getAllStates() {
            return circuits.entrySet().stream()
                .collect(Collectors.toMap(
                    Map.Entry::getKey,
                    e -> e.getValue().getState()
                ));
        }
    
        private CircuitBreaker createCircuitBreaker(String name) {
            return new DefaultCircuitBreaker(
                name,
                config.getResilience().getCircuitBreaker().getFailureThreshold(),
                config.getResilience().getCircuitBreaker().getSuccessThreshold(),
                config.getResilience().getCircuitBreaker().getTimeoutMs(),
                config.getResilience().getCircuitBreaker().getHalfOpenRequests()
            );
        }
    }
    

    3.6 Utilisation

    @Service
    public class PaymentService {
    
        @Autowired
        private CircuitBreakerRegistry cbRegistry;
    
        public PaymentResult processPayment(Payment payment) {
            CircuitBreaker cb = cbRegistry.getOrCreate("payment-gateway");
    
            return cb.executeWithFallback(
                () -> paymentGateway.process(payment),
                () -> {
                    // Fallback: mettre en queue pour traitement ultérieur
                    paymentQueue.enqueue(payment);
                    return PaymentResult.pending("Queued for later processing");
                }
            );
        }
    }
    

    4. Combinaison Retry + Circuit Breaker

    @Service
    public class ResilientApiClient {
    
        @Autowired
        private RetryTemplate retryTemplate;
    
        @Autowired
        private CircuitBreakerRegistry cbRegistry;
    
        public Data fetchData(String endpoint) {
            CircuitBreaker cb = cbRegistry.getOrCreate("api-" + endpoint);
    
            return cb.executeWithFallback(
                () -> retryTemplate.execute(() -> httpClient.get(endpoint)),
                () -> getCachedData(endpoint)
            );
        }
    }
    

    Ordre d’exécution

    1. CircuitBreaker vérifie si le circuit est ouvert
       → Si OPEN: fallback immédiat
       → Si CLOSED/HALF_OPEN: continue
    
    2. RetryTemplate essaie l'opération
       → Retry avec backoff exponentiel
       → Si tous les retries échouent: exception
    
    3. CircuitBreaker compte l'échec
       → Si seuil atteint: passe en OPEN
    
    4. Fallback si échec
    

    5. Annotations (optionnel)

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Resilient {
        String circuitBreaker() default "";
        int maxRetries() default 3;
        long retryDelay() default 1000;
        Class<? extends Throwable>[] retryOn() default {Exception.class};
    }
    
    @Aspect
    @Component
    public class ResilienceAspect {
    
        @Around("@annotation(resilient)")
        public Object handleResilience(ProceedingJoinPoint pjp, Resilient resilient) throws Throwable {
            String cbName = resilient.circuitBreaker();
            if (cbName.isEmpty()) {
                cbName = pjp.getSignature().getDeclaringTypeName() + "." + pjp.getSignature().getName();
            }
    
            CircuitBreaker cb = cbRegistry.getOrCreate(cbName);
    
            return cb.execute(() -> {
                return retryTemplate.execute(() -> {
                    try {
                        return pjp.proceed();
                    } catch (Throwable t) {
                        throw new RuntimeException(t);
                    }
                });
            });
        }
    }
    

    Utilisation

    @Service
    public class UserService {
    
        @Resilient(circuitBreaker = "user-api", maxRetries = 5)
        public User getUser(String id) {
            return userApiClient.fetchUser(id);
        }
    }
    

    6. Bulkhead Pattern

    Le pattern Bulkhead limite le nombre d’appels concurrents pour éviter l’épuisement des ressources.

    @Component
    public class BulkheadRegistry {
    
        private final ConcurrentHashMap<String, Semaphore> bulkheads = new ConcurrentHashMap<>();
    
        public <T> T execute(String name, int maxConcurrent, Supplier<T> operation)
                throws BulkheadFullException {
            Semaphore semaphore = bulkheads.computeIfAbsent(name,
                k -> new Semaphore(maxConcurrent));
    
            if (!semaphore.tryAcquire()) {
                throw new BulkheadFullException(name);
            }
    
            try {
                return operation.get();
            } finally {
                semaphore.release();
            }
        }
    }
    
    // Utilisation
    @Service
    public class ApiService {
    
        @Autowired
        private BulkheadRegistry bulkheadRegistry;
    
        public Data callExternalApi() {
            return bulkheadRegistry.execute("external-api", 10, () -> {
                // Max 10 appels concurrents
                return httpClient.get("/api/data");
            });
        }
    }
    

    7. Timeout Pattern

    @Component
    public class TimeoutTemplate {
    
        private final ScheduledExecutorService scheduler =
            Executors.newScheduledThreadPool(4);
    
        public <T> T executeWithTimeout(Supplier<T> operation, Duration timeout)
                throws TimeoutException {
    
            CompletableFuture<T> future = CompletableFuture.supplyAsync(operation);
    
            try {
                return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
            } catch (java.util.concurrent.TimeoutException e) {
                future.cancel(true);
                throw new TimeoutException("Operation timed out after " + timeout);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
    

    8. API Admin

    @RestController
    @RequestMapping("/admin/resilience")
    public class ResilienceController {
    
        @Autowired
        private CircuitBreakerRegistry cbRegistry;
    
        @GetMapping("/circuits")
        public Map<String, CircuitState> getCircuits() {
            return cbRegistry.getAllStates();
        }
    
        @PostMapping("/circuits/{name}/reset")
        public void resetCircuit(@PathVariable String name) {
            CircuitBreaker cb = cbRegistry.get(name);
            if (cb != null) {
                cb.reset();
            }
        }
    
        @PostMapping("/circuits/{name}/open")
        public void openCircuit(@PathVariable String name) {
            CircuitBreaker cb = cbRegistry.get(name);
            if (cb != null) {
                cb.forceOpen();
            }
        }
    
        @PostMapping("/circuits/{name}/close")
        public void closeCircuit(@PathVariable String name) {
            CircuitBreaker cb = cbRegistry.get(name);
            if (cb != null) {
                cb.forceClose();
            }
        }
    }
    

    9. Métriques

    @Component
    public class ResilienceMetrics {
    
        private final MeterRegistry registry;
    
        public void recordRetry(String operation, int attempt, boolean success) {
            Counter.builder("socle_retry_attempts")
                .tag("operation", operation)
                .tag("attempt", String.valueOf(attempt))
                .tag("success", String.valueOf(success))
                .register(registry)
                .increment();
        }
    
        public void recordCircuitBreakerState(String name, CircuitState state) {
            Gauge.builder("socle_circuit_breaker_state", () ->
                state == CircuitState.CLOSED ? 0 :
                state == CircuitState.HALF_OPEN ? 1 : 2)
                .tag("name", name)
                .register(registry);
        }
    }
    

    10. Bonnes pratiques

    DO

    • Utiliser le circuit breaker pour les appels réseau externes
    • Configurer des timeouts appropriés
    • Toujours prévoir un fallback
    • Monitorer l’état des circuits
    • Logger les transitions d’état

    DON’T

    • Ne pas utiliser le retry pour les erreurs non récupérables (400, 401)
    • Ne pas configurer des seuils trop bas (faux positifs)
    • Ne pas oublier les timeouts (risque de blocage)
    • Ne pas ignorer les métriques

    11. Références

  • Socle V004 – GraalVM JavaScript

    Socle V004 – GraalVM JavaScript

    GraalVM et JavaScript (GraalJS)

    Version : 4.0.0 Date : 2025-12-13

    1. Vue d’ensemble

    Le Socle V004 supporte l’exécution de scripts JavaScript pour l’enrichissement et la transformation de données via GraalJS, le moteur JavaScript de GraalVM.

    Pourquoi GraalVM ?

    • Performance : Compilation JIT pour JavaScript, bien plus rapide que Nashorn
    • Compatibilité : Support ECMAScript moderne (ES2023+)
    • Interopérabilité : Accès bidirectionnel entre Java et JavaScript
    • Sécurité : Sandbox configurable pour isoler l’exécution

    2. Prérequis

    2.1 GraalVM CE 21.0.2

    Important : Pour les applications utilisant GraalJS, GraalVM CE 21.0.2 est requis au lieu d’OpenJDK standard.

    OpenJDK ne contient pas le runtime Truffle nécessaire à GraalJS. Utiliser OpenJDK avec GraalJS provoquera l’erreur :

    org.graalvm.polyglot.PolyglotException:
    The Truffle API cannot be used without GraalVM runtime.
    

    2.2 Installation de GraalVM

    # Télécharger GraalVM CE 21.0.2
    wget https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz
    
    # Extraire dans /opt
    sudo tar -xzf graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz -C /opt/
    
    # Créer un lien symbolique
    sudo ln -s /opt/graalvm-community-openjdk-21.0.2+13.1 /opt/graalvm
    
    # Vérifier l'installation
    /opt/graalvm/bin/java -version
    

    Sortie attendue :

    openjdk version "21.0.2" 2024-01-16
    OpenJDK Runtime Environment GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30)
    OpenJDK 64-Bit Server VM GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30, mixed mode, sharing)
    

    3. Configuration Maven

    3.1 Version GraalJS

    La version de GraalJS doit correspondre à la version du runtime GraalVM :

    GraalVM CE GraalJS Version
    21.0.2 (23.1) 23.1.0
    22.0.0 (24.0) 24.0.0

    3.2 Dépendances pom.xml

    <properties>
        <graaljs.version>23.1.0</graaljs.version>
    </properties>
    
    <dependencies>
        <!-- GraalVM Polyglot API -->
        <dependency>
            <groupId>org.graalvm.polyglot</groupId>
            <artifactId>polyglot</artifactId>
            <version>${graaljs.version}</version>
        </dependency>
    
        <!-- GraalJS Community (runtime) -->
        <dependency>
            <groupId>org.graalvm.polyglot</groupId>
            <artifactId>js-community</artifactId>
            <version>${graaljs.version}</version>
            <type>pom</type>
            <scope>runtime</scope>
        </dependency>
    
        <!-- Script Engine (optionnel, pour JSR-223) -->
        <dependency>
            <groupId>org.graalvm.js</groupId>
            <artifactId>js-scriptengine</artifactId>
            <version>${graaljs.version}</version>
        </dependency>
    </dependencies>
    

    4. Options JVM

    4.1 Option obligatoire pour uber-jars

    Pour les fat JARs (Spring Boot repackaged), cette option est obligatoire :

    -Dpolyglotimpl.DisableClassPathIsolation=true
    

    Sans cette option, vous obtiendrez :

    java.lang.NullPointerException: Cannot invoke "java.io.File.toPath()"
    because the return value of "com.oracle.truffle.polyglot.FileSystems$InternalFileSystemContext.collectClassPathJars()"
    

    4.2 Supprimer les avertissements

    Pour supprimer les avertissements « interpreter only » :

    -Dpolyglot.engine.WarnInterpreterOnly=false
    

    4.3 Configuration systemd complète

    [Service]
    Environment="JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC -Dpolyglotimpl.DisableClassPathIsolation=true"
    ExecStart=/opt/graalvm/bin/java $JAVA_OPTS -jar /opt/myapp/myapp.jar
    

    5. Utilisation de GraalJS

    5.1 Exemple basique

    import org.graalvm.polyglot.Context;
    import org.graalvm.polyglot.Value;
    
    public class JavaScriptExecutor {
    
        public String execute(String script, Map<String, Object> bindings) {
            try (Context context = Context.newBuilder("js")
                    .allowAllAccess(false)
                    .allowHostAccess(HostAccess.ALL)
                    .allowHostClassLookup(className -> false)
                    .option("engine.WarnInterpreterOnly", "false")
                    .build()) {
    
                Value jsBindings = context.getBindings("js");
                bindings.forEach(jsBindings::putMember);
    
                Value result = context.eval("js", script);
                return result.asString();
            }
        }
    }
    

    5.2 Options de sécurité

    Option Description
    allowAllAccess(false) Désactive tous les accès par défaut
    allowHostAccess(HostAccess.ALL) Permet l’accès aux objets Java passés
    allowHostClassLookup(className -> false) Empêche Java.type()
    allowIO(false) Désactive les accès fichiers
    allowNativeAccess(false) Désactive les appels natifs

    6. Performance et initialisation

    6.1 Temps d’initialisation

    La première exécution de GraalJS prend environ 10-15 secondes car :

    • Chargement du runtime Truffle
    • Compilation JIT du moteur JavaScript
    • Initialisation des structures internes

    Les exécutions suivantes sont très rapides (<100ms).

    6.2 Pré-initialisation au démarrage

    Pour éviter la latence sur le premier événement, vous pouvez pré-initialiser GraalVM :

    @Component
    public class GraalVMWarmup {
    
        private Engine sharedEngine;
    
        @PostConstruct
        public void init() {
            log.info("Pre-initializing GraalVM JavaScript engine...");
            long start = System.currentTimeMillis();
    
            this.sharedEngine = Engine.newBuilder()
                .option("engine.WarnInterpreterOnly", "false")
                .build();
    
            // Warm-up avec un script simple
            try (Context warmup = Context.newBuilder("js")
                    .engine(sharedEngine)
                    .allowAllAccess(false)
                    .allowHostAccess(HostAccess.ALL)
                    .build()) {
                warmup.eval("js", "var x = 1 + 1;");
            }
    
            log.info("GraalVM initialized in {} ms", System.currentTimeMillis() - start);
        }
    
        @PreDestroy
        public void cleanup() {
            if (sharedEngine != null) {
                sharedEngine.close();
            }
        }
    
        public Engine getSharedEngine() {
            return sharedEngine;
        }
    }
    

    6.3 Impact sur le démarrage

    Configuration Démarrage app Premier événement
    Sans pré-init ~40s ~12s
    Avec pré-init ~52s (+12s) <100ms

    7. Troubleshooting

    7.1 « Truffle API cannot be used without GraalVM runtime »

    Cause : Utilisation d’OpenJDK au lieu de GraalVM Solution : Installer et utiliser GraalVM CE 21.0.2

    7.2 « NullPointerException in collectClassPathJars »

    Cause : Classpath isolation dans un uber-jar Solution : Ajouter -Dpolyglotimpl.DisableClassPathIsolation=true

    7.3 « Version mismatch: truffle-api-24.0.0 vs GraalVM 23.1 »

    Cause : Version GraalJS incompatible avec le runtime GraalVM Solution : Aligner graaljs.version avec la version GraalVM installée

    7.4 Script timeout

    Cause : Initialisation GraalVM dépasse le timeout Solution : Augmenter le timeout à 30 secondes pour le premier appel, ou pré-initialiser

    8. Références

  • Socle V004 – Janino

    Socle V004 – Janino

    29 – Janino (Compilateur Java Dynamique)

    Version : 4.0.0 Date : 2026-01-17

    1. Introduction

    Janino est un compilateur Java embarqué qui permet de compiler du code source Java en bytecode JVM à la volée. Contrairement au ScriptEngine existant (interprété), Janino offre des performances natives car le code est réellement compilé.

    Positionnement

    ┌─────────────────────────────────────────────────────────────────┐
    │                      Socle V004 Scripts                          │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  ┌─────────────────────┐       ┌─────────────────────┐          │
    │  │    ScriptEngine     │       │   JaninoEngine      │          │
    │  │    (existant)       │       │   (NOUVEAU)         │          │
    │  ├─────────────────────┤       ├─────────────────────┤          │
    │  │ - JavaScript        │       │ - Java pur          │          │
    │  │ - BeanShell         │       │ - Bytecode natif    │          │
    │  │ - Interprété        │       │ - Haute performance │          │
    │  │ - Typage dynamique  │       │ - Typage statique   │          │
    │  └─────────────────────┘       └─────────────────────┘          │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘
    

    Cas d’usage

    Situation Recommandation
    Calculs financiers (frais, taxes) Janino
    Validations métier complexes Janino
    Transformations haute performance Janino
    Scripts simples, prototypage ScriptEngine
    Formules configurables Janino

    2. Architecture

    2.1 Composants

    eu.lmvi.socle.janino/
    ├── JaninoEngine.java              # Moteur principal
    ├── JaninoWorker.java              # Worker de gestion
    ├── JaninoScript.java              # Script compilé
    ├── JaninoClassLoader.java         # ClassLoader sécurisé
    ├── JaninoConfiguration.java       # Configuration Spring
    ├── JaninoCompilationException.java # Exception
    └── interfaces/
        ├── Calculator.java            # Interface calculateur
        ├── Executable.java            # Interface exécutable
        ├── Validator.java             # Interface validateur
        └── ValidationResult.java      # Résultat validation
    

    2.2 Diagramme de classes

    ┌───────────────────────┐
    │   JaninoWorker        │  (implements Worker)
    │   @Component          │
    ├───────────────────────┤
    │ - janinoEngine        │
    │ - config              │
    │ - techDb              │
    │ - supervisor          │
    ├───────────────────────┤
    │ + execute()           │
    │ + compileScript()     │
    │ + forceReload()       │
    │ + getEngine()         │
    └───────────┬───────────┘
                │
                ▼
    ┌───────────────────────┐
    │   JaninoEngine        │
    │   @Component          │
    ├───────────────────────┤
    │ - scriptCache         │
    │ - classLoader         │
    ├───────────────────────┤
    │ + compile()           │
    │ + execute()           │
    │ + reload()            │
    │ + getStats()          │
    └───────────┬───────────┘
                │
                ▼
    ┌───────────────────────┐
    │   JaninoScript        │
    ├───────────────────────┤
    │ - name                │
    │ - compiledClass       │
    │ - executionCount      │
    │ - avgExecutionTimeNs  │
    └───────────────────────┘
    

    3. Configuration

    3.1 application.yml

    socle:
      janino:
        # Activer Janino (défaut: false)
        enabled: ${JANINO_ENABLED:false}
    
        # Répertoire des scripts Java
        scripts-path: ${JANINO_SCRIPTS_PATH:./repository/scripts/java}
    
        # Intervalle de rechargement (défaut: 5 minutes)
        reload-interval-ms: ${JANINO_RELOAD_INTERVAL:300000}
    
        # Nombre max de classes en cache
        max-cached-classes: ${JANINO_MAX_CACHED:100}
    
        # Sécurité
        security:
          # Packages bloqués dans les scripts
          blocked-packages:
            - java.io
            - java.net
            - java.lang.reflect
            - java.lang.invoke
            - sun.
            - com.sun.
    
          # Timeout d'exécution max
          max-execution-time-ms: ${JANINO_MAX_EXEC_TIME:5000}
    

    3.2 Variables d’environnement

    Variable Description Défaut
    JANINO_ENABLED Activer Janino false
    JANINO_SCRIPTS_PATH Répertoire scripts ./repository/scripts/java
    JANINO_RELOAD_INTERVAL Intervalle reload (ms) 300000
    JANINO_MAX_CACHED Max classes en cache 100
    JANINO_MAX_EXEC_TIME Timeout exécution (ms) 5000

    4. Interfaces de Scripts

    Les scripts Java doivent implémenter une des interfaces suivantes :

    4.1 Calculator

    Pour les calculs (frais, taxes, conversions).

    package eu.lmvi.socle.janino.interfaces;
    
    public interface Calculator<T> {
        T calculate(Map<String, Object> context);
    }
    

    Exemple :

    import eu.lmvi.socle.janino.interfaces.Calculator;
    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.util.Map;
    
    public class FeeCalculator implements Calculator<BigDecimal> {
    
        private static final BigDecimal FEE_RATE = new BigDecimal("0.0026");
    
        @Override
        public BigDecimal calculate(Map<String, Object> context) {
            BigDecimal amount = (BigDecimal) context.get("amount");
            String side = (String) context.get("side");
    
            BigDecimal rate = "MAKER".equals(side)
                ? new BigDecimal("0.0016")
                : FEE_RATE;
    
            return amount.multiply(rate).setScale(8, RoundingMode.HALF_UP);
        }
    }
    

    4.2 Executable

    Pour les exécutions génériques.

    package eu.lmvi.socle.janino.interfaces;
    
    public interface Executable {
        Object execute(Map<String, Object> context);
    }
    

    Exemple :

    import eu.lmvi.socle.janino.interfaces.Executable;
    import java.util.Map;
    
    public class OrderProcessor implements Executable {
    
        @Override
        public Object execute(Map<String, Object> context) {
            String orderId = (String) context.get("orderId");
            Double amount = (Double) context.get("amount");
    
            // Logique de traitement...
    
            return Map.of(
                "status", "PROCESSED",
                "orderId", orderId,
                "processedAmount", amount * 0.99
            );
        }
    }
    

    4.3 Validator

    Pour les validations métier.

    package eu.lmvi.socle.janino.interfaces;
    
    public interface Validator {
        ValidationResult validate(Object input);
    }
    

    Exemple :

    import eu.lmvi.socle.janino.interfaces.Validator;
    import eu.lmvi.socle.janino.interfaces.ValidationResult;
    import java.math.BigDecimal;
    
    public class OrderValidator implements Validator {
    
        private static final BigDecimal MIN_AMOUNT = new BigDecimal("10.00");
        private static final BigDecimal MAX_AMOUNT = new BigDecimal("100000.00");
    
        @Override
        public ValidationResult validate(Object input) {
            Order order = (Order) input;
            ValidationResult result = new ValidationResult();
    
            if (order.getAmount().compareTo(MIN_AMOUNT) < 0) {
                result.addError("AMOUNT_TOO_LOW",
                    "Amount must be >= " + MIN_AMOUNT);
            }
    
            if (order.getAmount().compareTo(MAX_AMOUNT) > 0) {
                result.addError("AMOUNT_TOO_HIGH",
                    "Amount must be <= " + MAX_AMOUNT);
            }
    
            if (order.getPair() == null || order.getPair().isEmpty()) {
                result.addError("INVALID_PAIR", "Trading pair is required");
            }
    
            return result;
        }
    }
    

    5. Utilisation

    5.1 Structure des scripts

    repository/scripts/java/
    ├── fees/
    │   ├── KrakenFeeCalculator.java
    │   ├── BinanceFeeCalculator.java
    │   └── CryptoComFeeCalculator.java
    ├── validators/
    │   ├── OrderValidator.java
    │   └── AmountValidator.java
    └── processors/
        ├── OrderProcessor.java
        └── TradeProcessor.java
    

    5.2 Injection dans un Worker

    @Component
    public class TradingWorker implements Worker {
    
        @Autowired
        private JaninoWorker janinoWorker;
    
        @Override
        public void doWork() {
            // Exécuter un calculateur
            Map<String, Object> context = Map.of(
                "amount", new BigDecimal("1000.00"),
                "side", "TAKER"
            );
    
            BigDecimal fee = janinoWorker.execute(
                "KrakenFeeCalculator",
                context,
                BigDecimal.class
            );
    
            log.info("Fee calculated: {}", fee);
        }
    }
    

    5.3 Compilation à la volée

    @Autowired
    private JaninoWorker janinoWorker;
    
    public void compileCustomScript() {
        String source = """
            import eu.lmvi.socle.janino.interfaces.Calculator;
            import java.math.BigDecimal;
            import java.util.Map;
    
            public class CustomCalculator implements Calculator<BigDecimal> {
                @Override
                public BigDecimal calculate(Map<String, Object> context) {
                    BigDecimal value = (BigDecimal) context.get("value");
                    return value.multiply(new BigDecimal("1.05"));
                }
            }
            """;
    
        janinoWorker.compileScript("CustomCalculator", source);
    
        // Exécuter
        BigDecimal result = janinoWorker.execute(
            "CustomCalculator",
            Map.of("value", new BigDecimal("100")),
            BigDecimal.class
        );
    }
    

    5.4 Accès direct au moteur

    @Autowired
    private JaninoWorker janinoWorker;
    
    public void advancedUsage() {
        JaninoEngine engine = janinoWorker.getEngine();
    
        // Vérifier si un script est compilé
        boolean ready = engine.isCompiled("FeeCalculator");
    
        // Liste des scripts compilés
        Set<String> scripts = engine.getCompiledScripts();
    
        // Statistiques détaillées
        Map<String, Map<String, Object>> stats = engine.getScriptStats();
    
        // Forcer le rechargement
        janinoWorker.forceReload();
    }
    

    6. Hot-Reload

    Le JaninoWorker surveille automatiquement les modifications des fichiers .java dans le répertoire configuré.

    Fonctionnement

    1. À chaque cycle (reload-interval-ms), le worker scanne le répertoire
    2. Pour chaque fichier modifié (timestamp changé), le script est recompilé
    3. Le nouveau bytecode remplace l’ancien dans le cache
    4. Les prochaines exécutions utilisent la nouvelle version

    Logs

    [exec:xxx][step:janino_reloaded] Reloaded script: FeeCalculator
    [exec:xxx][step:janino_reload_cycle] Reload cycle completed (5 scripts)
    

    Forcer le rechargement

    // Recharger tous les scripts
    janinoWorker.forceReload();
    

    7. Securite

    7.1 Validation des Imports (Source-Level)

    Le JaninoEngine valide les imports avant la compilation pour bloquer l’acces aux packages dangereux. Cette approche (validation au niveau du code source) est plus compatible avec les fat-jars Spring Boot que la precedente approche ClassLoader.

    Package bloque Raison
    java.io Acces fichiers
    java.net Acces reseau
    java.lang.reflect Reflection
    java.lang.invoke MethodHandles
    sun.* Classes internes
    com.sun.* Classes internes

    7.2 Fonctionnement

    Source Java → validateSourceSecurity() → Compilation Janino → Execution
                         ↓
                  Analyse des imports
                         ↓
                Blocage si package interdit
    

    Le moteur analyse les declarations import dans le code source et rejette le script si un package bloque est detecte.

    7.3 Tentative d’acces bloque

    // Ce script sera bloque AVANT compilation
    import java.io.File;  // BLOQUE!
    
    public class MaliciousScript implements Executable {
        @Override
        public Object execute(Map<String, Object> context) {
            File file = new File("/etc/passwd");
            return null;
        }
    }
    

    Erreur :

    JaninoCompilationException: Security violation in script 'MaliciousScript':
    Import of blocked package 'java.io' is not allowed. Blocked import: java.io.File
    

    7.4 Configuration personnalisee

    socle:
      janino:
        security:
          blocked-packages:
            - java.io
            - java.net
            - java.lang.reflect
            - java.lang.invoke
            - sun.
            - com.sun.
            - com.mycompany.internal  # Packages internes custom
    

    8. Métriques et Monitoring

    8.1 Stats du Worker

    Map<String, Object> stats = janinoWorker.getStats();
    
    // Résultat:
    {
        "name": "janino_worker",
        "running": true,
        "healthy": true,
        "scripts_path": "./repository/scripts/java",
        "reload_interval_ms": 300000,
        "reload_count": 15,
        "last_reload_at": "2026-01-17T10:30:00Z",
        "scripts_compiled": 5,
        "compilation_count": 8,
        "compilation_errors": 0,
        "execution_count": 1234,
        "execution_errors": 2
    }
    

    8.2 Stats par script

    Map<String, Map<String, Object>> scriptStats = janinoWorker.getEngine().getScriptStats();
    
    // Résultat:
    {
        "KrakenFeeCalculator": {
            "class": "KrakenFeeCalculator",
            "compiled_at": 1705487400000,
            "execution_count": 500,
            "avg_execution_us": 12
        },
        "BinanceFeeCalculator": {
            "class": "BinanceFeeCalculator",
            "compiled_at": 1705487400000,
            "execution_count": 300,
            "avg_execution_us": 8
        }
    }
    

    8.3 TechDB

    Le worker persiste ses stats dans la table janino_stats :

    SELECT * FROM janino_stats;
    
    -- worker_name | scripts_count | compilation_count | execution_count | reload_count | last_reload
    -- janino_worker | 5 | 8 | 1234 | 15 | 2026-01-17 10:30:00
    

    9. Comparaison avec ScriptEngine

    Critère ScriptEngine JaninoEngine
    Langages JavaScript, BeanShell Java pur
    Exécution Interprétée Compilée (bytecode)
    Performance ~1000x plus lent Native JVM
    Typage Dynamique Statique
    IDE Support Limité Complet (Java)
    Debug Difficile Standard Java
    Hot-reload Non Oui
    Sécurité Sandbox complexe ClassLoader isolation
    Courbe apprentissage Nouveau langage Java existant

    Quand utiliser quoi ?

    Situation Recommandation
    Calculs critiques (frais, taxes) JaninoEngine
    Validations complexes JaninoEngine
    Scripts simples, one-liners ScriptEngine
    Prototypage rapide ScriptEngine
    Logique métier complexe JaninoEngine
    Transformations JSON basiques ScriptEngine

    10. Dépannage

    10.1 Script non compilé

    Erreur :

    IllegalStateException: Script not compiled: MyScript
    

    Solution :

    • Vérifier que le fichier existe dans scripts-path
    • Vérifier l’extension .java
    • Consulter les logs pour les erreurs de compilation

    10.2 Erreur de compilation

    Erreur :

    JaninoCompilationException: Failed to compile script: MyScript
    

    Solution :

    • Vérifier la syntaxe Java
    • Vérifier les imports
    • Vérifier que la classe implémente une interface valide

    10.3 Classe non trouvée

    Erreur :

    Cannot find class name in source
    

    Solution :

    • Le source doit contenir public class NomDeLaClasse
    • Le nom de la classe doit correspondre au nom du fichier

    10.4 Package bloque

    Erreur :

    JaninoCompilationException: Security violation in script 'MyScript':
    Import of blocked package 'java.io' is not allowed
    

    Solution :

    • Utiliser uniquement les packages autorises
    • Si necessaire, modifier la config blocked-packages

    10.5 Cannot load simple types (Fat-Jar Spring Boot)

    Erreur :

    org.codehaus.commons.compiler.CompileException: Cannot load simple types
    

    Cause : Ce probleme survient dans les fat-jars Spring Boot car le classloader personnalise casse la resolution des modules Java 9+.

    Solution : Cette erreur a ete corrigee dans le socle V4. Le JaninoEngine utilise maintenant le context classloader directement :

    // JaninoEngine.java - ligne 117-121
    ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
    if (contextLoader == null) {
        contextLoader = getClass().getClassLoader();
    }
    compiler.setParentClassLoader(contextLoader);
    

    Si vous rencontrez encore cette erreur, verifiez que vous utilisez la derniere version du socle.

    10.6 Assignment conversion not possible from Object

    Erreur :

    Line 35, Column 41: Assignment conversion not possible from type "java.lang.Object" to type "java.math.BigDecimal"
    

    Cause : Janino ne supporte pas l’inference de type pour Map.get().

    Solution : Ajouter un cast explicite :

    // AVANT (erreur)
    BigDecimal rate = myMap.get(key);
    
    // APRES (correct)
    BigDecimal rate = (BigDecimal) myMap.get(key);
    

    10.7 Invalid escape sequence

    Erreur :

    Line 27, Column 35: Invalid escape sequence
    

    Cause : Les sequences d’echappement regex (\s, \d, \{) doivent etre double-echappees.

    Solution :

    // AVANT (erreur)
    Pattern p = Pattern.compile("\s+");
    
    // APRES (correct)
    Pattern p = Pattern.compile("\\s+");
    

    10.8 Invocation of static interface methods

    Erreur :

    Invocation of static interface methods only available for target version 8+
    

    Cause : Janino ne supporte pas Map.of(), Set.of(), List.of() (Java 9+).

    Solution : Utiliser des blocs static :

    // AVANT (erreur)
    private static final Map<String, String> DATA = Map.of("a", "b");
    
    // APRES (correct)
    private static final Map<String, String> DATA = new HashMap<>();
    static {
        DATA.put("a", "b");
    }
    

    11. Bonnes Pratiques

    DO

    • Implémenter une des interfaces (Calculator, Executable, Validator)
    • Utiliser des types explicites (pas de var)
    • Gérer les exceptions dans le script
    • Tester les scripts avant déploiement
    • Utiliser des noms de classes descriptifs

    DON’T

    • Ne pas utiliser java.io, java.net, java.lang.reflect
    • Ne pas stocker d’état entre les exécutions (stateless)
    • Ne pas faire d’opérations bloquantes longues
    • Ne pas utiliser de dépendances externes non disponibles

    12. Limitations Janino et Compatibilite

    Janino est un compilateur Java simplifie qui ne supporte pas toutes les fonctionnalites du langage Java moderne. Cette section documente les limitations et les solutions.

    12.1 Fonctionnalites Non Supportees

    Fonctionnalite Version Java Statut Janino
    Methodes generiques <T> Java 5+ NON SUPPORTE
    Map.of(), Set.of(), List.of() Java 9+ NON SUPPORTE
    switch expressions Java 14+ NON SUPPORTE
    var (inference de type) Java 10+ NON SUPPORTE
    Records Java 16+ NON SUPPORTE
    Pattern matching Java 16+ NON SUPPORTE
    Text blocks """ Java 15+ NON SUPPORTE

    12.2 Solutions et Contournements

    A. Pas de methodes generiques

    // INTERDIT - Ne compile pas
    private <T extends Number> T extractValue(String json, String key, Class<T> type) {
        // ...
    }
    
    // CORRECT - Methodes specifiques par type
    private Integer extractIntValue(String json, String key) {
        // implementation pour Integer
    }
    
    private Double extractDoubleValue(String json, String key) {
        // implementation pour Double
    }
    

    B. Pas de Map.of() / Set.of() / List.of()

    // INTERDIT - Ne compile pas
    private static final Map<String, BigDecimal> RATES = Map.of(
        "FR", new BigDecimal("0.015"),
        "DE", new BigDecimal("0.018")
    );
    
    // CORRECT - Bloc static avec HashMap
    private static final Map<String, BigDecimal> RATES = new HashMap<>();
    static {
        RATES.put("FR", new BigDecimal("0.015"));
        RATES.put("DE", new BigDecimal("0.018"));
    }
    
    // CORRECT - Pour Set
    private static final Set<String> COUNTRIES = new HashSet<>();
    static {
        COUNTRIES.add("FR");
        COUNTRIES.add("DE");
    }
    

    C. Cast explicite pour Map.get()

    // INTERDIT - "Assignment conversion not possible from Object to BigDecimal"
    BigDecimal rate = RATES.get(country);
    
    // CORRECT - Cast explicite
    BigDecimal rate = (BigDecimal) RATES.get(country);
    if (rate == null) rate = BigDecimal.ZERO;
    

    D. Pas de switch expressions

    // INTERDIT - Ne compile pas
    String result = switch (status) {
        case "A" -> "Active";
        case "I" -> "Inactive";
        default -> "Unknown";
    };
    
    // CORRECT - Switch statement classique
    String result;
    switch (status) {
        case "A": result = "Active"; break;
        case "I": result = "Inactive"; break;
        default: result = "Unknown";
    }
    

    E. Double echappement des regex

    // INTERDIT - "Invalid escape sequence"
    Pattern p = Pattern.compile("\s+");
    Pattern p2 = Pattern.compile("\{.*\}");
    
    // CORRECT - Double echappement
    Pattern p = Pattern.compile("\\s+");
    Pattern p2 = Pattern.compile("\\{.*\\}");
    

    12.3 Template de Script Compatible

    Voici un template de script qui fonctionne avec Janino :

    import eu.lmvi.socle.janino.interfaces.Calculator;
    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.util.Map;
    import java.util.HashMap;
    import java.util.Set;
    import java.util.HashSet;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    public class MyCalculator implements Calculator<BigDecimal> {
    
        // Collections statiques avec bloc static
        private static final Map<String, BigDecimal> RATES = new HashMap<>();
        private static final Set<String> VALID_CODES = new HashSet<>();
    
        static {
            RATES.put("A", new BigDecimal("0.10"));
            RATES.put("B", new BigDecimal("0.20"));
    
            VALID_CODES.add("X");
            VALID_CODES.add("Y");
        }
    
        @Override
        public BigDecimal calculate(Map<String, Object> context) {
            // Extraction avec cast explicite
            BigDecimal amount = extractAmount(context.get("amount"));
            String code = (String) context.get("code");
            if (code == null) code = "A";
    
            // Acces Map avec cast
            BigDecimal rate = (BigDecimal) RATES.get(code);
            if (rate == null) rate = new BigDecimal("0.15");
    
            return amount.multiply(rate).setScale(2, RoundingMode.HALF_UP);
        }
    
        // Methode d'extraction type-safe (pas de generiques)
        private BigDecimal extractAmount(Object value) {
            if (value == null) return BigDecimal.ZERO;
            if (value instanceof BigDecimal) return (BigDecimal) value;
            if (value instanceof Number) {
                return BigDecimal.valueOf(((Number) value).doubleValue());
            }
            try {
                return new BigDecimal(value.toString());
            } catch (NumberFormatException e) {
                return BigDecimal.ZERO;
            }
        }
    }
    

    12.4 Checklist de Compatibilite

    Avant de deployer un script Janino, verifiez :

    • [ ] Pas de <T> dans les signatures de methodes
    • [ ] Pas de Map.of(), Set.of(), List.of()
    • [ ] Pas de var pour les declarations
    • [ ] Pas de switch expressions (fleches ->)
    • [ ] Cast explicite (Type) pour tous les Map.get()
    • [ ] Double echappement \\ dans les regex
    • [ ] Pas de text blocks """
    • [ ] Imports explicites (pas de wildcards import java.util.*)

    13. Exemple Complet

    13.1 Script : TradingFeeCalculator.java

    import eu.lmvi.socle.janino.interfaces.Calculator;
    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.util.Map;
    import java.util.HashMap;
    
    /**
     * Calculateur de frais de trading multi-exchange.
     * Compatible Janino (pas de switch expressions, pas de Map.of)
     */
    public class TradingFeeCalculator implements Calculator<BigDecimal> {
    
        // Taux MAKER par exchange
        private static final Map<String, BigDecimal> MAKER_RATES = new HashMap<>();
        // Taux TAKER par exchange
        private static final Map<String, BigDecimal> TAKER_RATES = new HashMap<>();
    
        static {
            MAKER_RATES.put("KRAKEN", new BigDecimal("0.0016"));
            MAKER_RATES.put("BINANCE", new BigDecimal("0.0010"));
            MAKER_RATES.put("COINBASE", new BigDecimal("0.0040"));
    
            TAKER_RATES.put("KRAKEN", new BigDecimal("0.0026"));
            TAKER_RATES.put("BINANCE", new BigDecimal("0.0010"));
            TAKER_RATES.put("COINBASE", new BigDecimal("0.0060"));
        }
    
        private static final BigDecimal DEFAULT_RATE = new BigDecimal("0.0025");
    
        @Override
        public BigDecimal calculate(Map<String, Object> context) {
            String exchange = (String) context.get("exchange");
            BigDecimal amount = (BigDecimal) context.get("amount");
            String side = (String) context.get("side");
    
            if (exchange == null) exchange = "DEFAULT";
            if (side == null) side = "TAKER";
    
            BigDecimal feeRate = getFeeRate(exchange.toUpperCase(), side.toUpperCase());
    
            return amount.multiply(feeRate).setScale(8, RoundingMode.HALF_UP);
        }
    
        private BigDecimal getFeeRate(String exchange, String side) {
            boolean isMaker = "MAKER".equals(side);
    
            // Cast explicite requis par Janino
            BigDecimal rate;
            if (isMaker) {
                rate = (BigDecimal) MAKER_RATES.get(exchange);
            } else {
                rate = (BigDecimal) TAKER_RATES.get(exchange);
            }
    
            if (rate == null) {
                rate = DEFAULT_RATE;
            }
            return rate;
        }
    }
    

    13.2 Worker utilisant le script

    Note : Ce Worker est du code Java standard compile par javac/Maven, pas un script Janino. Il peut donc utiliser List.of(), Map.of() et les fonctionnalites Java modernes.

    @Component
    public class FeeWorker implements Worker {
    
        private static final Logger log = LoggerFactory.getLogger(FeeWorker.class);
    
        @Autowired
        private JaninoWorker janinoWorker;
    
        @Override
        public String getName() {
            return "fee-worker";
        }
    
        @Override
        public void doWork() {
            // Calculer les frais pour différents exchanges
            List<String> exchanges = List.of("KRAKEN", "BINANCE", "COINBASE");
            BigDecimal amount = new BigDecimal("10000.00");
    
            for (String exchange : exchanges) {
                Map<String, Object> context = Map.of(
                    "exchange", exchange,
                    "amount", amount,
                    "side", "TAKER"
                );
    
                BigDecimal fee = janinoWorker.execute(
                    "TradingFeeCalculator",
                    context,
                    BigDecimal.class
                );
    
                log.info("Fee for {} on amount {}: {}",
                    exchange, amount, fee);
            }
        }
    
        // ... autres méthodes Worker
    }
    

    14. References

  • Socle V004 – Migration V3 vers V4

    Socle V004 – Migration V3 vers V4

    25 – Guide de Migration V3 → V4

    Version : 4.0.0 Date : 2025-12-09

    1. Résumé des changements

    1.1 Ce qui change

    Aspect V3 V4
    Logging Logback Log4j2 + LogForwarder
    Persistance technique In-memory/Redis + H2 TechDB
    Auth AdminAuthFilter local + SocleAuthClient JWT
    Registry Supervisor local + WorkerRegistryClient

    1.2 Ce qui ne change PAS

    • Architecture MOP
    • Interface Worker
    • KvBus (in_memory / Redis)
    • SharedDataRegistry
    • Supervisor
    • HttpWorker
    • AdminRestApi
    • PipelineEngine
    • CircuitBreaker / Retry
    • Scheduler

    2. Checklist de migration

    □ Phase 1: Préparation
      □ Lire ce guide en entier
      □ Backup du projet V3
      □ Créer branche migration-v4
    
    □ Phase 2: Dépendances Maven
      □ Mettre à jour pom.xml
      □ Exclure Logback
      □ Ajouter Log4j2
      □ Ajouter H2
    
    □ Phase 3: Configuration
      □ Créer log4j2.xml
      □ Créer log4j2.component.properties
      □ Supprimer logback-spring.xml
      □ Mettre à jour application.yml
    
    □ Phase 4: Code (optionnel)
      □ Intégrer TechDbManager
      □ Intégrer SocleAuthClient
      □ Intégrer WorkerRegistryClient
    
    □ Phase 5: Tests
      □ Compiler
      □ Exécuter les tests
      □ Vérifier les logs
      □ Valider H2 Console
    
    □ Phase 6: Déploiement
      □ Variables d'environnement
      □ Test en staging
      □ Déploiement production
    

    3. Phase 1 : Préparation

    3.1 Backup

    # Backup du projet V3
    cp -r socle-v003 socle-v003-backup
    
    # Créer branche
    cd socle-v003
    git checkout -b migration-v4
    

    3.2 Version cible

    <version>4.0.0</version>
    

    4. Phase 2 : Dépendances Maven

    4.1 Modifications pom.xml

    <!-- AVANT (V3) -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
    </dependency>
    <dependency>
        <groupId>net.logstash.logback</groupId>
        <artifactId>logstash-logback-encoder</artifactId>
    </dependency>
    
    <!-- APRÈS (V4) -->
    <!-- Exclure Logback de Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!-- Log4j2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
    
    <!-- LMAX Disruptor -->
    <dependency>
        <groupId>com.lmax</groupId>
        <artifactId>disruptor</artifactId>
        <version>4.0.0</version>
    </dependency>
    
    <!-- H2 Database -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>2.2.224</version>
    </dependency>
    

    4.2 Vérifier les exclusions

    S’assurer que Logback est exclu de TOUTES les dépendances Spring Boot :

    mvn dependency:tree | grep logback
    # Ne doit rien retourner
    

    5. Phase 3 : Configuration

    5.1 Créer log4j2.xml

    Créer src/main/resources/log4j2.xml :

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="WARN" monitorInterval="30">
        <Properties>
            <Property name="LOG_DIR">${env:LOG_DIR:-./logs}</Property>
            <Property name="APP_NAME">${env:APP_NAME:-socle-v4}</Property>
        </Properties>
    
        <Appenders>
            <Console name="Console" target="SYSTEM_OUT">
                <PatternLayout pattern="%d{ISO8601} %-5level [%thread] %logger{36} - %msg%n"/>
            </Console>
    
            <RollingFile name="File"
                         fileName="${LOG_DIR}/${APP_NAME}.log"
                         filePattern="${LOG_DIR}/${APP_NAME}-%d{yyyy-MM-dd}-%i.log.gz">
                <PatternLayout pattern="%d{ISO8601} %-5level [%thread] %logger{36} - %msg%n"/>
                <Policies>
                    <TimeBasedTriggeringPolicy interval="1"/>
                    <SizeBasedTriggeringPolicy size="100MB"/>
                </Policies>
                <DefaultRolloverStrategy max="30"/>
            </RollingFile>
        </Appenders>
    
        <Loggers>
            <Logger name="eu.lmvi.socle" level="${env:LOG_LEVEL:-INFO}" additivity="false">
                <AppenderRef ref="Console"/>
                <AppenderRef ref="File"/>
            </Logger>
            <Logger name="org.springframework" level="WARN"/>
            <Root level="INFO">
                <AppenderRef ref="Console"/>
                <AppenderRef ref="File"/>
            </Root>
        </Loggers>
    </Configuration>
    

    5.2 Créer log4j2.component.properties

    Créer src/main/resources/log4j2.component.properties :

    Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
    AsyncLogger.RingBufferSize=262144
    AsyncLogger.WaitStrategy=Sleep
    log4j2.formatMsgNoLookups=true
    

    5.3 Supprimer logback-spring.xml

    rm src/main/resources/logback-spring.xml
    

    5.4 Mettre à jour application.yml

    Ajouter les nouvelles configurations V4 :

    # Ajouter à application.yml existant
    
    socle:
      # ... config V3 existante ...
    
      # NOUVEAU V4: H2 TechDB
      techdb:
        enabled: ${TECHDB_ENABLED:true}
        url: jdbc:h2:file:${TECHDB_PATH:./data/socle-techdb};MODE=PostgreSQL;DB_CLOSE_DELAY=-1
        username: socle
        password: ${TECHDB_PASSWORD:socle}
        console:
          enabled: ${H2_CONSOLE_ENABLED:false}
          path: /h2-console
    
      # NOUVEAU V4: LogForwarder (optionnel)
      logging:
        forwarder:
          enabled: ${LOG_FORWARDER_ENABLED:false}
          transport-mode: ${LOG_TRANSPORT_MODE:http}
          log-hub-url: ${LOG_HUB_URL:}
    
      # NOUVEAU V4: Auth Client (optionnel)
      auth:
        enabled: ${AUTH_ENABLED:false}
        server-url: ${AUTH_SERVER_URL:}
        api-key: ${API_KEY:}
    
      # NOUVEAU V4: Worker Registry (optionnel)
      worker-registry:
        enabled: ${WORKER_REGISTRY_ENABLED:false}
        server-url: ${WORKER_REGISTRY_URL:}
    
    # Logging
    logging:
      config: classpath:log4j2.xml
    

    6. Phase 4 : Code (optionnel)

    Les composants V4 sont optionnels et activés via configuration. Aucune modification de code n’est obligatoire.

    6.1 Si vous voulez utiliser TechDB

    @Autowired(required = false)
    private TechDbManager techDbManager;
    
    // Utilisation
    if (techDbManager != null) {
        techDbManager.saveOffset("kafka", "topic-0", offset, null);
    }
    

    6.2 Si vous voulez utiliser AuthClient

    @Autowired(required = false)
    private SocleAuthClient authClient;
    
    // Utilisation
    if (authClient != null && authClient.isAuthenticated()) {
        String token = authClient.getValidAccessToken();
    }
    

    6.3 Si vous voulez utiliser WorkerRegistry

    @Autowired(required = false)
    private WorkerRegistryClient registryClient;
    
    // L'intégration MOP est automatique si enabled
    

    7. Phase 5 : Tests

    7.1 Compilation

    mvn clean compile
    

    Erreurs possibles :

    • package ch.qos.logback does not exist → Logback pas complètement exclu
    • cannot find symbol: class Logger → Import SLF4J correct ?

    7.2 Tests unitaires

    mvn test
    

    7.3 Vérification logs

    mvn spring-boot:run
    

    Vérifier :

    • Logs apparaissent en console
    • Format correct
    • Pas d’erreur Log4j2

    7.4 H2 Console

    Si H2_CONSOLE_ENABLED=true :

    1. Ouvrir http://localhost:8080/h2-console
    2. JDBC URL: jdbc:h2:file:./data/socle-techdb
    3. Vérifier les tables créées

    8. Phase 6 : Déploiement

    8.1 Variables d’environnement (minimum)

    # Existantes V3 (inchangées)
    APP_NAME=my-app
    ENV_NAME=PROD
    HTTP_PORT=8080
    
    # Nouvelles V4 (optionnelles)
    TECHDB_ENABLED=true
    TECHDB_PATH=./data/techdb
    H2_CONSOLE_ENABLED=false
    LOG_FORWARDER_ENABLED=false
    AUTH_ENABLED=false
    WORKER_REGISTRY_ENABLED=false
    

    8.2 Docker

    Mettre à jour le Dockerfile si nécessaire :

    # Pas de changement requis si vous utilisez le JAR
    FROM eclipse-temurin:21-jre
    COPY target/socle-v004-4.0.0.jar app.jar
    ENTRYPOINT ["java", "-jar", "app.jar"]
    

    8.3 Kubernetes

    Mettre à jour les ConfigMaps/Secrets avec les nouvelles variables.

    9. Mapping des configurations

    9.1 Logback → Log4j2

    Logback Log4j2
    <appender class="ConsoleAppender"> <Console name="...">
    <appender class="RollingFileAppender"> <RollingFile name="...">
    <encoder><pattern> <PatternLayout pattern="...">
    <root level="INFO"> <Root level="INFO">
    <logger name="..." level="..."> <Logger name="..." level="...">

    9.2 Pattern identique

    Le pattern de log reste identique :

    %d{ISO8601} %-5level [%thread] %logger{36} - %msg%n
    

    10. Rollback

    En cas de problème, pour revenir à V3 :

    # Restaurer le pom.xml V3
    git checkout HEAD~1 -- pom.xml
    
    # Restaurer logback-spring.xml
    git checkout HEAD~1 -- src/main/resources/logback-spring.xml
    
    # Supprimer fichiers V4
    rm src/main/resources/log4j2.xml
    rm src/main/resources/log4j2.component.properties
    
    # Rebuild
    mvn clean package
    

    11. FAQ

    Q: Dois-je modifier mon code de logging ?

    Non. Le code reste identique :

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    private static final Logger log = LoggerFactory.getLogger(MyClass.class);
    log.info("Message");
    

    Q: Les MDC fonctionnent-ils ?

    Oui. MDC fonctionne de manière identique avec Log4j2.

    Q: Puis-je activer les fonctionnalités V4 progressivement ?

    Oui. Toutes les fonctionnalités V4 sont optionnelles :

    • TECHDB_ENABLED=false → Pas de H2
    • LOG_FORWARDER_ENABLED=false → Pas de LogForwarder
    • AUTH_ENABLED=false → Pas d’auth JWT
    • WORKER_REGISTRY_ENABLED=false → Pas de registry

    Q: Performance : V4 est-il plus rapide ?

    Oui pour le logging. Log4j2 avec AsyncLoggers (LMAX Disruptor) est 6-68x plus rapide que Logback en mode async.

    12. Références

  • Socle V004 – Architecture

    Socle V004 – Architecture

    02 – Architecture du Socle V4

    Version : 4.0.0 Date : 2025-01-25

    1. Vue d’ensemble

    ┌─────────────────────────────────────────────────────────────────┐
    │                         SOCLE V4                                │
    │                                                                 │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │                    MOP (inchangé)                        │   │
    │  │  - Orchestration Workers                                 │   │
    │  │  - Lifecycle management                                  │   │
    │  │  - Scheduling doWork()                                   │   │
    │  └─────────────────────────────────────────────────────────┘   │
    │                              │                                  │
    │  ┌───────────┬───────────┬───┴───────┬─────────────┐           │
    │  │           │           │           │             │           │
    │  ▼           ▼           ▼           ▼             ▼           │
    │ ┌─────┐  ┌───────┐  ┌────────┐  ┌─────────┐  ┌──────────┐     │
    │ │KvBus│  │Shared │  │Supervi-│  │ HTTP    │  │ Workers  │     │
    │ │     │  │Data   │  │sor     │  │ Worker  │  │ métier   │     │
    │ └─────┘  └───────┘  └────────┘  └─────────┘  └──────────┘     │
    │                                                                 │
    │  ════════════════════ NOUVEAUTÉS V4 ════════════════════════   │
    │                                                                 │
    │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐     │
    │  │ H2 TechDB   │  │ Log4j2 +    │  │ Clients centraux    │     │
    │  │ (embarqué)  │  │ LogForwarder│  │ - SocleAuthClient   │     │
    │  │             │  │             │  │ - WorkerRegistry    │     │
    │  └─────────────┘  └─────────────┘  └─────────────────────┘     │
    │                                                                 │
    │  ┌─────────────┐  ┌─────────────────────────────────────┐      │
    │  │ Status      │  │ Pipeline V2                          │      │
    │  │ Dashboard   │  │ (Queue/Claim/Ack, DLQ, at-least-once)│      │
    │  │ (port 9374) │  │                                      │      │
    │  └─────────────┘  └─────────────────────────────────────┘      │
    │                                                                 │
    └─────────────────────────────────────────────────────────────────┘
    

    2. Architecture en couches

    ┌─────────────────────────────────────────────────────────────────┐
    │                    COUCHE APPLICATION                           │
    │  Workers métier + Contrôleurs REST                              │
    ├─────────────────────────────────────────────────────────────────┤
    │                    COUCHE FRAMEWORK (SOCLE)                     │
    │  MOP + Core Components + Nouveautés V4                          │
    ├─────────────────────────────────────────────────────────────────┤
    │                    COUCHE INFRASTRUCTURE                        │
    │  Tomcat (HTTP), H2 (TechDB), Redis (KV), Kafka/NATS (Msg)      │
    └─────────────────────────────────────────────────────────────────┘
    

    3. Structure des packages

    eu.lmvi.socle/
    │
    │  ══════ COMPOSANTS V3 (conservés) ══════
    ├── mop/
    │   └── MainOrchestratorProcess.java    # Orchestrateur central
    ├── worker/
    │   └── Worker.java                     # Interface de base
    ├── config/
    │   └── SocleConfiguration.java         # Configuration centralisée
    ├── kv/
    │   ├── KvBus.java                      # Abstraction KV
    │   ├── KvImplementation.java           # Interface implémentation
    │   ├── InMemoryKvImplementation.java   # Implémentation mémoire
    │   └── RedisKvImplementation.java      # Implémentation Redis
    ├── shared/
    │   └── SharedDataRegistry.java         # Registre données partagées
    ├── supervisor/
    │   └── Supervisor.java                 # Monitoring heartbeats
    ├── http/
    │   ├── HttpWorker.java                 # Worker HTTP
    │   ├── TomcatManager.java              # Gestion Tomcat
    │   └── GracefulShutdownFilter.java     # Filtre drain
    ├── admin/
    │   └── AdminRestApi.java               # API REST admin
    ├── metrics/
    │   └── SocleMetrics.java               # Métriques
    ├── pipeline/
    │   └── PipelineEngine.java             # Traitement asynchrone
    ├── resilience/
    │   ├── CircuitBreaker.java             # Circuit Breaker
    │   └── RetryExecutor.java              # Retry avec backoff
    ├── scheduler/
    │   ├── WorkerScheduler.java            # Scheduler
    │   └── CronExpression.java             # Parser cron
    ├── security/
    │   ├── AdminAuthFilter.java            # Auth admin
    │   └── RateLimitFilter.java            # Rate limiting
    │
    │  ══════ NOUVEAUX COMPOSANTS V4 ══════
    ├── techdb/
    │   ├── TechDbManager.java              # Gestionnaire H2
    │   ├── TechDbConfig.java               # Configuration Spring
    │   └── TechDbRepository.java           # Repository
    ├── logging/
    │   ├── SocleLogForwarderAppender.java  # Appender Log4j2
    │   ├── LogTransport.java               # Interface transport
    │   ├── HttpLogTransport.java           # Transport HTTP
    │   ├── NatsLogTransport.java           # Transport NATS
    │   └── H2FallbackStorage.java          # Fallback H2
    └── client/
        ├── auth/
        │   ├── SocleAuthClient.java        # Interface auth
        │   ├── AuthTokenManager.java       # Gestion tokens
        │   └── AuthTokens.java             # DTO tokens
        └── registry/
            ├── WorkerRegistryClient.java   # Client registry
            ├── WorkerRegistration.java     # DTO registration
            └── WorkerHeartbeat.java        # DTO heartbeat
    

    4. Flux de démarrage

    1. Spring Boot initialise
    2. SocleConfiguration charge la config (.env + YAML)
    3. Spring crée les beans @Component
    4. MOP.start() appelé (ApplicationReadyEvent)
       │
       ├── 4.1 [V4] TechDbManager.initialize()
       │         └── Création tables H2
       │         └── Restauration offsets
       │
       ├── 4.2 [V4] SocleAuthClient.login()
       │         └── Obtention JWT
       │
       ├── 4.3 [V4] WorkerRegistryClient.register()
       │         └── Enregistrement au Registry
       │
       ├── 4.4 SharedDataRegistry.initialize()
       ├── 4.5 KvBus.initialize()
       ├── 4.6 Supervisor.start()
       ├── 4.7 Metrics.start()
       │
       ├── 4.8 Workers triés par START_PRIORITY
       │   └── Pour chaque worker:
       │       ├── worker.initialize()
       │       ├── worker.start()
       │       ├── Register avec Supervisor
       │       └── Schedule si isScheduled()
       │
       ├── 4.9 HttpWorker.start() [priorité 1000 = dernier]
       └── 4.10 Boucle principale doWork()
    

    5. Flux de shutdown

    1. Signal SIGTERM (ou /admin/shutdown)
    2. MOP.stop() appelé
    3. État → DRAINING
       │
       ├── 3.1 HttpWorker.startDraining()
       │         └── Refuse nouvelles connexions
       ├── 3.2 HttpWorker.awaitDrain(timeout)
       │         └── Attente requêtes en cours
       │
       ├── 3.3 Workers par STOP_PRIORITY (petit = premier)
       │   ├── HttpWorker.stop() [priorité 0]
       │   └── Autres workers [priorités 1-999]
       │
       ├── 3.4 [V4] WorkerRegistryClient.unregister()
       ├── 3.5 [V4] TechDbManager.close()
       │
       ├── 3.6 Supervisor.shutdown()
       ├── 3.7 SharedData.close()
       └── 3.8 État → STOPPED
    

    6. Dépendances entre composants

    ┌──────────────────────────────────────────────────────────────┐
    │                        MOP (orchestrateur)                    │
    │                              │                                │
    │         ┌────────────────────┼────────────────────┐          │
    │         │                    │                    │          │
    │         ▼                    ▼                    ▼          │
    │   ┌───────────┐       ┌───────────┐       ┌───────────┐     │
    │   │ TechDB    │◄──────│ Supervisor│───────►│ Workers   │     │
    │   │ Manager   │       │           │        │           │     │
    │   └─────┬─────┘       └─────┬─────┘        └─────┬─────┘     │
    │         │                   │                    │           │
    │         │                   ▼                    │           │
    │         │            ┌───────────┐               │           │
    │         │            │ Worker    │◄──────────────┘           │
    │         │            │ Registry  │                           │
    │         │            │ Client    │                           │
    │         │            └─────┬─────┘                           │
    │         │                  │                                 │
    │         ▼                  ▼                                 │
    │   ┌───────────┐     ┌───────────┐                           │
    │   │ H2        │     │ SocleAuth │                           │
    │   │ Database  │     │ Client    │                           │
    │   └───────────┘     └─────┬─────┘                           │
    │                           │                                  │
    │                           ▼                                  │
    │                    ┌─────────────┐                           │
    │                    │ LogForwarder│──► HTTP/NATS (sortant)   │
    │                    │ Appender    │                           │
    │                    └─────────────┘                           │
    └──────────────────────────────────────────────────────────────┘
    

    7. Architecture du Logging V4

    ┌─────────────────────────────────────────────────────────────┐
    │                      Application                             │
    │                                                              │
    │  Logger.info("message")                                      │
    │         │                                                    │
    │         ▼                                                    │
    │  ┌─────────────────────────────────────────────────────┐    │
    │  │              Log4j2 AsyncLoggers                     │    │
    │  │              (LMAX Disruptor)                        │    │
    │  └────────────────────┬────────────────────────────────┘    │
    │                       │                                      │
    │         ┌─────────────┼─────────────┐                       │
    │         ▼             ▼             ▼                       │
    │  ┌───────────┐ ┌───────────┐ ┌─────────────────────┐       │
    │  │  Console  │ │  File     │ │ SocleLogForwarder   │       │
    │  │  Appender │ │  Appender │ │ Appender            │       │
    │  └───────────┘ └───────────┘ └──────────┬──────────┘       │
    │                                         │                   │
    └─────────────────────────────────────────┼───────────────────┘
                                              │
                        ┌─────────────────────┴─────────────────────┐
                        │                                           │
                        ▼                                           ▼
                ┌──────────────┐                           ┌──────────────┐
                │ HTTP Transport│                           │ NATS Transport│
                │ → LogHub     │                           │ → JetStream  │
                └──────┬───────┘                           └──────┬───────┘
                       │                                          │
                       │  (si échec)                              │
                       ▼                                          │
                ┌──────────────┐                                  │
                │ H2 Fallback  │◄─────────────────────────────────┘
                │ Storage      │
                └──────────────┘
    

    8. Patterns de conception

    Pattern Composant Usage
    Orchestrator MOP Orchestre tout le lifecycle
    Observer KvBus pub/sub Communication inter-workers
    Strategy KvImplementation In-memory ou Redis
    Registry SharedDataRegistry État partagé
    Circuit Breaker CircuitBreaker Résilience pannes
    Factory Spring DI Création composants
    Builder Configuration Construction config

    9. Thread Safety

    Mécanisme Usage
    ConcurrentHashMap Maps partagées
    AtomicLong/Boolean Compteurs atomiques
    ReentrantReadWriteLock Opérations complexes
    BlockingQueue Queues thread-safe
    CompletableFuture Opérations async
    ScheduledExecutorService Scheduling
    LMAX Disruptor Ring buffer logging

    10. Points d’extension

    Créer un nouveau Worker

    @Component
    public class MonWorker implements Worker {
        // Implémenter l'interface Worker
    }
    

    Ajouter une implémentation KvBus

    public class MonKvImplementation implements KvImplementation {
        // Implémenter l'interface
    }
    

    Créer un Pipeline Stage

    public class MonStage implements PipelineStage<MonType> {
        // Implémenter le traitement
    }
    

    11. Références

  • Socle V004 – Migration V3 vers V4

    Socle V004 – Migration V3 vers V4

    25 – Guide de Migration V3 → V4

    Version : 4.0.0 Date : 2025-12-09

    1. Résumé des changements

    1.1 Ce qui change

    Aspect V3 V4
    Logging Logback Log4j2 + LogForwarder
    Persistance technique In-memory/Redis + H2 TechDB
    Auth AdminAuthFilter local + SocleAuthClient JWT
    Registry Supervisor local + WorkerRegistryClient

    1.2 Ce qui ne change PAS

    • Architecture MOP
    • Interface Worker
    • KvBus (in_memory / Redis)
    • SharedDataRegistry
    • Supervisor
    • HttpWorker
    • AdminRestApi
    • PipelineEngine
    • CircuitBreaker / Retry
    • Scheduler

    2. Checklist de migration

    □ Phase 1: Préparation
      □ Lire ce guide en entier
      □ Backup du projet V3
      □ Créer branche migration-v4
    
    □ Phase 2: Dépendances Maven
      □ Mettre à jour pom.xml
      □ Exclure Logback
      □ Ajouter Log4j2
      □ Ajouter H2
    
    □ Phase 3: Configuration
      □ Créer log4j2.xml
      □ Créer log4j2.component.properties
      □ Supprimer logback-spring.xml
      □ Mettre à jour application.yml
    
    □ Phase 4: Code (optionnel)
      □ Intégrer TechDbManager
      □ Intégrer SocleAuthClient
      □ Intégrer WorkerRegistryClient
    
    □ Phase 5: Tests
      □ Compiler
      □ Exécuter les tests
      □ Vérifier les logs
      □ Valider H2 Console
    
    □ Phase 6: Déploiement
      □ Variables d'environnement
      □ Test en staging
      □ Déploiement production
    

    3. Phase 1 : Préparation

    3.1 Backup

    # Backup du projet V3
    cp -r socle-v003 socle-v003-backup
    
    # Créer branche
    cd socle-v003
    git checkout -b migration-v4
    

    3.2 Version cible

    <version>4.0.0</version>
    

    4. Phase 2 : Dépendances Maven

    4.1 Modifications pom.xml

    <!-- AVANT (V3) -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
    </dependency>
    <dependency>
        <groupId>net.logstash.logback</groupId>
        <artifactId>logstash-logback-encoder</artifactId>
    </dependency>
    
    <!-- APRÈS (V4) -->
    <!-- Exclure Logback de Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!-- Log4j2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
    
    <!-- LMAX Disruptor -->
    <dependency>
        <groupId>com.lmax</groupId>
        <artifactId>disruptor</artifactId>
        <version>4.0.0</version>
    </dependency>
    
    <!-- H2 Database -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>2.2.224</version>
    </dependency>
    

    4.2 Vérifier les exclusions

    S’assurer que Logback est exclu de TOUTES les dépendances Spring Boot :

    mvn dependency:tree | grep logback
    # Ne doit rien retourner
    

    5. Phase 3 : Configuration

    5.1 Créer log4j2.xml

    Créer src/main/resources/log4j2.xml :

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="WARN" monitorInterval="30">
        <Properties>
            <Property name="LOG_DIR">${env:LOG_DIR:-./logs}</Property>
            <Property name="APP_NAME">${env:APP_NAME:-socle-v4}</Property>
        </Properties>
    
        <Appenders>
            <Console name="Console" target="SYSTEM_OUT">
                <PatternLayout pattern="%d{ISO8601} %-5level [%thread] %logger{36} - %msg%n"/>
            </Console>
    
            <RollingFile name="File"
                         fileName="${LOG_DIR}/${APP_NAME}.log"
                         filePattern="${LOG_DIR}/${APP_NAME}-%d{yyyy-MM-dd}-%i.log.gz">
                <PatternLayout pattern="%d{ISO8601} %-5level [%thread] %logger{36} - %msg%n"/>
                <Policies>
                    <TimeBasedTriggeringPolicy interval="1"/>
                    <SizeBasedTriggeringPolicy size="100MB"/>
                </Policies>
                <DefaultRolloverStrategy max="30"/>
            </RollingFile>
        </Appenders>
    
        <Loggers>
            <Logger name="eu.lmvi.socle" level="${env:LOG_LEVEL:-INFO}" additivity="false">
                <AppenderRef ref="Console"/>
                <AppenderRef ref="File"/>
            </Logger>
            <Logger name="org.springframework" level="WARN"/>
            <Root level="INFO">
                <AppenderRef ref="Console"/>
                <AppenderRef ref="File"/>
            </Root>
        </Loggers>
    </Configuration>
    

    5.2 Créer log4j2.component.properties

    Créer src/main/resources/log4j2.component.properties :

    Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
    AsyncLogger.RingBufferSize=262144
    AsyncLogger.WaitStrategy=Sleep
    log4j2.formatMsgNoLookups=true
    

    5.3 Supprimer logback-spring.xml

    rm src/main/resources/logback-spring.xml
    

    5.4 Mettre à jour application.yml

    Ajouter les nouvelles configurations V4 :

    # Ajouter à application.yml existant
    
    socle:
      # ... config V3 existante ...
    
      # NOUVEAU V4: H2 TechDB
      techdb:
        enabled: ${TECHDB_ENABLED:true}
        url: jdbc:h2:file:${TECHDB_PATH:./data/socle-techdb};MODE=PostgreSQL;DB_CLOSE_DELAY=-1
        username: socle
        password: ${TECHDB_PASSWORD:socle}
        console:
          enabled: ${H2_CONSOLE_ENABLED:false}
          path: /h2-console
    
      # NOUVEAU V4: LogForwarder (optionnel)
      logging:
        forwarder:
          enabled: ${LOG_FORWARDER_ENABLED:false}
          transport-mode: ${LOG_TRANSPORT_MODE:http}
          log-hub-url: ${LOG_HUB_URL:}
    
      # NOUVEAU V4: Auth Client (optionnel)
      auth:
        enabled: ${AUTH_ENABLED:false}
        server-url: ${AUTH_SERVER_URL:}
        api-key: ${API_KEY:}
    
      # NOUVEAU V4: Worker Registry (optionnel)
      worker-registry:
        enabled: ${WORKER_REGISTRY_ENABLED:false}
        server-url: ${WORKER_REGISTRY_URL:}
    
    # Logging
    logging:
      config: classpath:log4j2.xml
    

    6. Phase 4 : Code (optionnel)

    Les composants V4 sont optionnels et activés via configuration. Aucune modification de code n’est obligatoire.

    6.1 Si vous voulez utiliser TechDB

    @Autowired(required = false)
    private TechDbManager techDbManager;
    
    // Utilisation
    if (techDbManager != null) {
        techDbManager.saveOffset("kafka", "topic-0", offset, null);
    }
    

    6.2 Si vous voulez utiliser AuthClient

    @Autowired(required = false)
    private SocleAuthClient authClient;
    
    // Utilisation
    if (authClient != null && authClient.isAuthenticated()) {
        String token = authClient.getValidAccessToken();
    }
    

    6.3 Si vous voulez utiliser WorkerRegistry

    @Autowired(required = false)
    private WorkerRegistryClient registryClient;
    
    // L'intégration MOP est automatique si enabled
    

    7. Phase 5 : Tests

    7.1 Compilation

    mvn clean compile
    

    Erreurs possibles :

    • package ch.qos.logback does not exist → Logback pas complètement exclu
    • cannot find symbol: class Logger → Import SLF4J correct ?

    7.2 Tests unitaires

    mvn test
    

    7.3 Vérification logs

    mvn spring-boot:run
    

    Vérifier :

    • Logs apparaissent en console
    • Format correct
    • Pas d’erreur Log4j2

    7.4 H2 Console

    Si H2_CONSOLE_ENABLED=true :

    1. Ouvrir http://localhost:8080/h2-console
    2. JDBC URL: jdbc:h2:file:./data/socle-techdb
    3. Vérifier les tables créées

    8. Phase 6 : Déploiement

    8.1 Variables d’environnement (minimum)

    # Existantes V3 (inchangées)
    APP_NAME=my-app
    ENV_NAME=PROD
    HTTP_PORT=8080
    
    # Nouvelles V4 (optionnelles)
    TECHDB_ENABLED=true
    TECHDB_PATH=./data/techdb
    H2_CONSOLE_ENABLED=false
    LOG_FORWARDER_ENABLED=false
    AUTH_ENABLED=false
    WORKER_REGISTRY_ENABLED=false
    

    8.2 Docker

    Mettre à jour le Dockerfile si nécessaire :

    # Pas de changement requis si vous utilisez le JAR
    FROM eclipse-temurin:21-jre
    COPY target/socle-v004-4.0.0.jar app.jar
    ENTRYPOINT ["java", "-jar", "app.jar"]
    

    8.3 Kubernetes

    Mettre à jour les ConfigMaps/Secrets avec les nouvelles variables.

    9. Mapping des configurations

    9.1 Logback → Log4j2

    Logback Log4j2
    <appender class="ConsoleAppender"> <Console name="...">
    <appender class="RollingFileAppender"> <RollingFile name="...">
    <encoder><pattern> <PatternLayout pattern="...">
    <root level="INFO"> <Root level="INFO">
    <logger name="..." level="..."> <Logger name="..." level="...">

    9.2 Pattern identique

    Le pattern de log reste identique :

    %d{ISO8601} %-5level [%thread] %logger{36} - %msg%n
    

    10. Rollback

    En cas de problème, pour revenir à V3 :

    # Restaurer le pom.xml V3
    git checkout HEAD~1 -- pom.xml
    
    # Restaurer logback-spring.xml
    git checkout HEAD~1 -- src/main/resources/logback-spring.xml
    
    # Supprimer fichiers V4
    rm src/main/resources/log4j2.xml
    rm src/main/resources/log4j2.component.properties
    
    # Rebuild
    mvn clean package
    

    11. FAQ

    Q: Dois-je modifier mon code de logging ?

    Non. Le code reste identique :

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    private static final Logger log = LoggerFactory.getLogger(MyClass.class);
    log.info("Message");
    

    Q: Les MDC fonctionnent-ils ?

    Oui. MDC fonctionne de manière identique avec Log4j2.

    Q: Puis-je activer les fonctionnalités V4 progressivement ?

    Oui. Toutes les fonctionnalités V4 sont optionnelles :

    • TECHDB_ENABLED=false → Pas de H2
    • LOG_FORWARDER_ENABLED=false → Pas de LogForwarder
    • AUTH_ENABLED=false → Pas d’auth JWT
    • WORKER_REGISTRY_ENABLED=false → Pas de registry

    Q: Performance : V4 est-il plus rapide ?

    Oui pour le logging. Log4j2 avec AsyncLoggers (LMAX Disruptor) est 6-68x plus rapide que Logback en mode async.

    12. Références