Catégorie : Blog

  • Socle V004 – Plan de Documentation

    Socle V004 – Plan de Documentation

    Plan de Documentation – Socle V4

    Version : 4.0.0 Date : 2025-01-25

    Structure de la documentation

    # Document Description Statut
    00 PLAN-DOCUMENTATION Ce document – Index de la documentation Done
    01 INTRODUCTION Présentation du Socle V4 et philosophie Done
    02 ARCHITECTURE Architecture technique et composants Done
    03 QUICKSTART Guide de démarrage rapide (5 min) Done
    04 CONFIGURATION Référence complète de configuration Done
    05 WORKERS Guide des Workers et lifecycle Done
    06 KV-BUS Guide du Key-Value Bus Done
    07 SHARED-DATA Guide du SharedDataRegistry Done
    08 SUPERVISOR Supervision et heartbeats Done
    09 PIPELINE Pipeline Engine V1 et V2 (Queue/Claim/Ack, DLQ) Done
    10 SECURITY Sécurité, Auth, Rate Limiting Done
    11 RESILIENCE Circuit Breaker et Retry Done
    12 SCHEDULER Scheduling cron et interval Done
    13 TLS-HTTPS Configuration TLS/HTTPS Done
    14 ADMIN-API API REST d’administration Done
    15 METRICS Métriques et Prometheus Done
    16 KUBERNETES Déploiement Kubernetes Done
    17 HOWTO Guides pratiques Done
    18 TROUBLESHOOTING Résolution de problèmes Done
    19 EXEMPLES Exemples de code Done
    20 PLUGINS Système de plugins Done
    21 H2-TECHDB Base technique H2 (V4) Done
    22 LOG4J2-LOGFORWARDER Log4j2 et LogForwarder (V4) Done
    23 AUTH-CLIENT Client authentification JWT (V4) Done
    24 WORKER-REGISTRY Client Worker Registry (V4) Done
    25 MIGRATION-V3-V4 Guide de migration V3 → V4 Done
    26 GRAALVM-JAVASCRIPT GraalVM CE et GraalJS pour scripts JS Done
    27 STATUS-DASHBOARD Dashboard HTML de supervision (port 9374) Done
    29 JANINO Scripts Java compiles dynamiquement Done
    30 EVENTBUS-WORKERS Workers event-driven Done
    31 GRPC-INTER-SOCLES Communication gRPC entre Socles Done

    Nouveautés V4

    Les documents 21 à 30 sont spécifiques au Socle V4 :

    • 21-H2-TECHDB : Base embarquée H2 pour état technique
    • 22-LOG4J2-LOGFORWARDER : Migration Logback → Log4j2 + centralisation logs
    • 23-AUTH-CLIENT : Client JWT pour services centraux
    • 24-WORKER-REGISTRY : Auto-enregistrement des workers
    • 25-MIGRATION-V3-V4 : Guide de migration depuis V3
    • 26-GRAALVM-JAVASCRIPT : GraalVM CE 21 et GraalJS pour exécution JavaScript
    • 27-STATUS-DASHBOARD : Dashboard HTML de supervision temps réel sur port 9374
    • 29-JANINO : Compilation dynamique de scripts Java
    • 30-EVENTBUS-WORKERS : Workers orientés événements
    • 31-GRPC-INTER-SOCLES : Communication gRPC bidirectionnelle entre Socles

    Guide Méthodologique

    Un guide méthodologique complet est disponible pour aider les développeurs à implémenter leurs solutions :

    Ce document répond à la question : « Je dois implémenter X, comment je fais ? »

    Conventions

    • Tous les fichiers sont en Markdown
    • Les exemples de code sont en Java 21
    • Les configurations sont en YAML
    • Les commandes sont pour Linux/macOS (adaptables Windows)
  • Socle V004 – API Administration

    Socle V004 – API Administration

    14 – Admin API

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    L’Admin API expose des endpoints REST pour l’administration et le monitoring du Socle V4.

    Endpoints principaux

    Endpoint Description
    /admin/health État de santé
    /admin/workers État des workers
    /admin/config Configuration
    /admin/registry SharedDataRegistry
    /admin/metrics Métriques

    2. Configuration

    2.1 application.yml

    socle:
      admin:
        enabled: ${ADMIN_ENABLED:true}
        path-prefix: ${ADMIN_PATH_PREFIX:/admin}
        auth:
          enabled: ${ADMIN_AUTH_ENABLED:false}
          username: ${ADMIN_USERNAME:admin}
          password: ${ADMIN_PASSWORD:}
    

    2.2 Variables d’environnement

    Variable Description Défaut
    ADMIN_ENABLED Activer l’API admin true
    ADMIN_PATH_PREFIX Préfixe des endpoints /admin
    ADMIN_AUTH_ENABLED Activer l’authentification false
    ADMIN_USERNAME Utilisateur admin admin
    ADMIN_PASSWORD Mot de passe admin

    3. Endpoints Health

    3.1 GET /admin/health

    État de santé global de l’application.

    Réponse :

    {
      "status": "HEALTHY",
      "timestamp": "2025-12-09T10:30:00Z",
      "uptime": "2h 30m 15s",
      "unhealthyWorkers": [],
      "components": {
        "database": "UP",
        "redis": "UP",
        "techdb": "UP"
      }
    }
    

    Codes HTTP :

    • 200 : HEALTHY
    • 503 : UNHEALTHY ou DEGRADED

    3.2 GET /admin/health/live

    Liveness probe pour Kubernetes.

    {
      "status": "UP"
    }
    

    3.3 GET /admin/health/ready

    Readiness probe pour Kubernetes.

    {
      "status": "READY",
      "checks": {
        "workers": "OK",
        "database": "OK"
      }
    }
    

    4. Endpoints Workers

    4.1 GET /admin/workers

    Liste tous les workers et leur état.

    {
      "workers": [
        {
          "name": "kafka-consumer",
          "status": "RUNNING",
          "lastHeartbeat": "2025-12-09T10:29:55Z",
          "healthy": true,
          "stats": {
            "processed": 12345,
            "errors": 2
          }
        },
        {
          "name": "order-processor",
          "status": "RUNNING",
          "lastHeartbeat": "2025-12-09T10:29:58Z",
          "healthy": true,
          "stats": {
            "ordersProcessed": 567
          }
        }
      ],
      "total": 2,
      "healthy": 2,
      "unhealthy": 0
    }
    

    4.2 GET /admin/workers/{name}

    Détails d’un worker spécifique.

    {
      "name": "kafka-consumer",
      "status": "RUNNING",
      "healthy": true,
      "startPriority": 10,
      "stopPriority": 90,
      "scheduled": false,
      "passive": false,
      "cycleIntervalMs": 1000,
      "lastHeartbeat": "2025-12-09T10:29:55Z",
      "stats": {
        "processed": 12345,
        "errors": 2,
        "lastOffset": 98765
      }
    }
    

    4.3 POST /admin/workers/{name}/stop

    Arrête un worker spécifique.

    curl -X POST http://localhost:8080/admin/workers/kafka-consumer/stop
    

    4.4 POST /admin/workers/{name}/start

    Redémarre un worker arrêté.

    curl -X POST http://localhost:8080/admin/workers/kafka-consumer/start
    

    5. Endpoints Configuration

    5.1 GET /admin/config

    Configuration actuelle (sans secrets).

    {
      "appName": "socle-v4",
      "envName": "PROD",
      "region": "MTQ",
      "version": "4.0.0",
      "execId": "socle-v4-abc123",
      "kvbus": {
        "mode": "redis"
      },
      "techdb": {
        "enabled": true
      },
      "logging": {
        "forwarder": {
          "enabled": true,
          "transportMode": "http"
        }
      }
    }
    

    5.2 GET /admin/config/env

    Variables d’environnement (filtrées).

    {
      "APP_NAME": "socle-v4",
      "ENV_NAME": "PROD",
      "REGION": "MTQ",
      "HTTP_PORT": "8080",
      "KVBUS_MODE": "redis"
    }
    

    6. Endpoints Registry

    6.1 GET /admin/registry

    Contenu du SharedDataRegistry.

    {
      "database.connected": true,
      "metrics.requests.total": 12345,
      "metrics.errors.total": 23,
      "batch.current.id": "batch-001",
      "worker.kafka.offset": 98765
    }
    

    6.2 GET /admin/registry/{key}

    Valeur d’une clé spécifique.

    {
      "key": "metrics.requests.total",
      "value": 12345,
      "healthLevel": "NORMAL"
    }
    

    6.3 GET /admin/registry/health

    Clés avec leur niveau de santé.

    {
      "database.connected": "CRITICAL",
      "cache.available": "IMPORTANT",
      "metrics.requests.total": "INFO"
    }
    

    6.4 GET /admin/registry/unhealthy

    Clés en état unhealthy.

    [
      {
        "key": "external.api.available",
        "value": false,
        "healthLevel": "CRITICAL"
      }
    ]
    

    7. Endpoints TechDB (V4)

    7.1 GET /admin/techdb/offsets

    Tous les offsets stockés.

    {
      "offsets": [
        {
          "sourceName": "kafka",
          "partitionKey": "orders-topic-0",
          "lastSequence": 123456,
          "lastUpdated": "2025-12-09T10:30:00Z"
        },
        {
          "sourceName": "nats",
          "partitionKey": "events.orders",
          "lastSequence": 789012,
          "lastUpdated": "2025-12-09T10:29:55Z"
        }
      ]
    }
    

    7.2 GET /admin/techdb/workers

    État des workers persisté.

    {
      "workers": [
        {
          "workerId": "kafka-consumer-001",
          "status": "RUNNING",
          "lastHeartbeat": "2025-12-09T10:30:00Z",
          "metadata": {
            "messagesPerMinute": 523
          }
        }
      ]
    }
    

    7.3 GET /admin/techdb/events

    Événements techniques récents.

    {
      "events": [
        {
          "id": 123,
          "createdAt": "2025-12-09T10:25:00Z",
          "type": "PIPELINE_ERROR",
          "payload": {
            "pipeline": "order-processing",
            "error": "Connection timeout"
          }
        }
      ]
    }
    

    8. Endpoints Resilience

    8.1 GET /admin/resilience/circuits

    État des circuit breakers.

    {
      "circuits": {
        "payment-gateway": "CLOSED",
        "inventory-api": "HALF_OPEN",
        "notification-service": "OPEN"
      }
    }
    

    8.2 POST /admin/resilience/circuits/{name}/reset

    Reset un circuit breaker.

    curl -X POST http://localhost:8080/admin/resilience/circuits/notification-service/reset
    

    9. Endpoints Scheduler

    9.1 GET /admin/scheduler/jobs

    Jobs schedulés.

    {
      "jobs": [
        {
          "jobId": "worker:daily-report",
          "type": "cron",
          "schedule": "0 0 6 * * ?",
          "scheduledAt": "2025-12-09T06:00:00Z"
        },
        {
          "jobId": "worker:health-check",
          "type": "interval",
          "intervalMs": 30000,
          "scheduledAt": "2025-12-09T10:00:00Z"
        }
      ]
    }
    

    9.2 POST /admin/scheduler/jobs/{jobId}/trigger

    Déclenche un job immédiatement.

    curl -X POST http://localhost:8080/admin/scheduler/jobs/worker:daily-report/trigger
    

    10. Endpoints LogForwarder (V4)

    10.1 GET /admin/logforwarder/status

    État du LogForwarder.

    {
      "enabled": true,
      "transportMode": "http",
      "queueSize": 23,
      "queueCapacity": 10000,
      "fallbackCount": 0,
      "lastFlush": "2025-12-09T10:29:55Z"
    }
    

    10.2 POST /admin/logforwarder/flush

    Force le flush des logs.

    curl -X POST http://localhost:8080/admin/logforwarder/flush
    

    10.3 POST /admin/logforwarder/replay

    Rejoue les logs en fallback.

    curl -X POST http://localhost:8080/admin/logforwarder/replay
    

    11. Implémentation

    package eu.lmvi.socle.admin;
    
    @RestController
    @RequestMapping("${socle.admin.path-prefix:/admin}")
    public class AdminRestApi {
    
        @Autowired private Supervisor supervisor;
        @Autowired private SharedDataRegistry registry;
        @Autowired private SocleConfiguration config;
        @Autowired(required = false) private TechDbManager techDb;
        @Autowired(required = false) private Scheduler scheduler;
    
        // === Health ===
    
        @GetMapping("/health")
        public ResponseEntity<HealthResponse> health() {
            HealthStatus status = supervisor.getGlobalHealth();
            HttpStatus httpStatus = status == HealthStatus.HEALTHY
                ? HttpStatus.OK
                : HttpStatus.SERVICE_UNAVAILABLE;
    
            return ResponseEntity.status(httpStatus).body(new HealthResponse(
                status,
                Instant.now(),
                getUptime(),
                supervisor.getUnhealthyWorkers()
            ));
        }
    
        @GetMapping("/health/live")
        public ResponseEntity<Map<String, String>> live() {
            return ResponseEntity.ok(Map.of("status", "UP"));
        }
    
        @GetMapping("/health/ready")
        public ResponseEntity<Map<String, Object>> ready() {
            HealthStatus status = supervisor.getGlobalHealth();
            if (status != HealthStatus.HEALTHY) {
                return ResponseEntity.status(503).body(Map.of(
                    "status", "NOT_READY",
                    "unhealthy", supervisor.getUnhealthyWorkers()
                ));
            }
            return ResponseEntity.ok(Map.of("status", "READY"));
        }
    
        // === Workers ===
    
        @GetMapping("/workers")
        public Map<String, Object> workers() {
            Map<String, WorkerState> states = supervisor.getAllWorkerStates();
            return Map.of(
                "workers", states.values(),
                "total", states.size(),
                "healthy", states.values().stream().filter(WorkerState::isHealthy).count(),
                "unhealthy", states.values().stream().filter(s -> !s.isHealthy()).count()
            );
        }
    
        @GetMapping("/workers/{name}")
        public ResponseEntity<WorkerState> worker(@PathVariable String name) {
            WorkerState state = supervisor.getWorkerState(name);
            return state != null
                ? ResponseEntity.ok(state)
                : ResponseEntity.notFound().build();
        }
    
        // === Config ===
    
        @GetMapping("/config")
        public Map<String, Object> config() {
            return Map.of(
                "appName", config.getApp_name(),
                "envName", config.getEnv_name(),
                "region", config.getRegion(),
                "version", config.getVersion(),
                "execId", config.getExec_id()
            );
        }
    
        // === Registry ===
    
        @GetMapping("/registry")
        public Map<String, Object> registry() {
            return registry.getAll();
        }
    
        @GetMapping("/registry/{key}")
        public ResponseEntity<Map<String, Object>> registryKey(@PathVariable String key) {
            return registry.get(key)
                .map(v -> ResponseEntity.ok(Map.of(
                    "key", key,
                    "value", v,
                    "healthLevel", registry.getHealthLevel(key)
                )))
                .orElse(ResponseEntity.notFound().build());
        }
    
        // ... autres endpoints
    }
    

    12. Sécurité

    12.1 Authentification Basic

    # Avec authentification
    curl -u admin:secret http://localhost:8080/admin/workers
    
    # En-tête Authorization
    curl -H "Authorization: Basic YWRtaW46c2VjcmV0" http://localhost:8080/admin/workers
    

    12.2 Endpoints publics

    Les endpoints suivants sont accessibles sans authentification :

    • /admin/health
    • /admin/health/live
    • /admin/health/ready

    13. Bonnes pratiques

    DO

    • Activer l’authentification en production
    • Utiliser HTTPS pour l’API admin
    • Limiter l’accès réseau à l’API admin
    • Monitorer les accès à l’API admin

    DON’T

    • Ne pas exposer l’API admin publiquement
    • Ne pas désactiver l’authentification en production
    • Ne pas logger les credentials

    14. Références

  • Socle V004 – Plan de Documentation

    Socle V004 – Plan de Documentation

    Plan de Documentation – Socle V4

    Version : 4.0.0 Date : 2025-01-25

    Structure de la documentation

    # Document Description Statut
    00 PLAN-DOCUMENTATION Ce document – Index de la documentation Done
    01 INTRODUCTION Présentation du Socle V4 et philosophie Done
    02 ARCHITECTURE Architecture technique et composants Done
    03 QUICKSTART Guide de démarrage rapide (5 min) Done
    04 CONFIGURATION Référence complète de configuration Done
    05 WORKERS Guide des Workers et lifecycle Done
    06 KV-BUS Guide du Key-Value Bus Done
    07 SHARED-DATA Guide du SharedDataRegistry Done
    08 SUPERVISOR Supervision et heartbeats Done
    09 PIPELINE Pipeline Engine V1 et V2 (Queue/Claim/Ack, DLQ) Done
    10 SECURITY Sécurité, Auth, Rate Limiting Done
    11 RESILIENCE Circuit Breaker et Retry Done
    12 SCHEDULER Scheduling cron et interval Done
    13 TLS-HTTPS Configuration TLS/HTTPS Done
    14 ADMIN-API API REST d’administration Done
    15 METRICS Métriques et Prometheus Done
    16 KUBERNETES Déploiement Kubernetes Done
    17 HOWTO Guides pratiques Done
    18 TROUBLESHOOTING Résolution de problèmes Done
    19 EXEMPLES Exemples de code Done
    20 PLUGINS Système de plugins Done
    21 H2-TECHDB Base technique H2 (V4) Done
    22 LOG4J2-LOGFORWARDER Log4j2 et LogForwarder (V4) Done
    23 AUTH-CLIENT Client authentification JWT (V4) Done
    24 WORKER-REGISTRY Client Worker Registry (V4) Done
    25 MIGRATION-V3-V4 Guide de migration V3 → V4 Done
    26 GRAALVM-JAVASCRIPT GraalVM CE et GraalJS pour scripts JS Done
    27 STATUS-DASHBOARD Dashboard HTML de supervision (port 9374) Done
    29 JANINO Scripts Java compiles dynamiquement Done
    30 EVENTBUS-WORKERS Workers event-driven Done
    31 GRPC-INTER-SOCLES Communication gRPC entre Socles Done

    Nouveautés V4

    Les documents 21 à 30 sont spécifiques au Socle V4 :

    • 21-H2-TECHDB : Base embarquée H2 pour état technique
    • 22-LOG4J2-LOGFORWARDER : Migration Logback → Log4j2 + centralisation logs
    • 23-AUTH-CLIENT : Client JWT pour services centraux
    • 24-WORKER-REGISTRY : Auto-enregistrement des workers
    • 25-MIGRATION-V3-V4 : Guide de migration depuis V3
    • 26-GRAALVM-JAVASCRIPT : GraalVM CE 21 et GraalJS pour exécution JavaScript
    • 27-STATUS-DASHBOARD : Dashboard HTML de supervision temps réel sur port 9374
    • 29-JANINO : Compilation dynamique de scripts Java
    • 30-EVENTBUS-WORKERS : Workers orientés événements
    • 31-GRPC-INTER-SOCLES : Communication gRPC bidirectionnelle entre Socles

    Guide Méthodologique

    Un guide méthodologique complet est disponible pour aider les développeurs à implémenter leurs solutions :

    Ce document répond à la question : « Je dois implémenter X, comment je fais ? »

    Conventions

    • Tous les fichiers sont en Markdown
    • Les exemples de code sont en Java 21
    • Les configurations sont en YAML
    • Les commandes sont pour Linux/macOS (adaptables Windows)
  • Socle V004 – API Administration

    Socle V004 – API Administration

    14 – Admin API

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    L’Admin API expose des endpoints REST pour l’administration et le monitoring du Socle V4.

    Endpoints principaux

    Endpoint Description
    /admin/health État de santé
    /admin/workers État des workers
    /admin/config Configuration
    /admin/registry SharedDataRegistry
    /admin/metrics Métriques

    2. Configuration

    2.1 application.yml

    socle:
      admin:
        enabled: ${ADMIN_ENABLED:true}
        path-prefix: ${ADMIN_PATH_PREFIX:/admin}
        auth:
          enabled: ${ADMIN_AUTH_ENABLED:false}
          username: ${ADMIN_USERNAME:admin}
          password: ${ADMIN_PASSWORD:}
    

    2.2 Variables d’environnement

    Variable Description Défaut
    ADMIN_ENABLED Activer l’API admin true
    ADMIN_PATH_PREFIX Préfixe des endpoints /admin
    ADMIN_AUTH_ENABLED Activer l’authentification false
    ADMIN_USERNAME Utilisateur admin admin
    ADMIN_PASSWORD Mot de passe admin

    3. Endpoints Health

    3.1 GET /admin/health

    État de santé global de l’application.

    Réponse :

    {
      "status": "HEALTHY",
      "timestamp": "2025-12-09T10:30:00Z",
      "uptime": "2h 30m 15s",
      "unhealthyWorkers": [],
      "components": {
        "database": "UP",
        "redis": "UP",
        "techdb": "UP"
      }
    }
    

    Codes HTTP :

    • 200 : HEALTHY
    • 503 : UNHEALTHY ou DEGRADED

    3.2 GET /admin/health/live

    Liveness probe pour Kubernetes.

    {
      "status": "UP"
    }
    

    3.3 GET /admin/health/ready

    Readiness probe pour Kubernetes.

    {
      "status": "READY",
      "checks": {
        "workers": "OK",
        "database": "OK"
      }
    }
    

    4. Endpoints Workers

    4.1 GET /admin/workers

    Liste tous les workers et leur état.

    {
      "workers": [
        {
          "name": "kafka-consumer",
          "status": "RUNNING",
          "lastHeartbeat": "2025-12-09T10:29:55Z",
          "healthy": true,
          "stats": {
            "processed": 12345,
            "errors": 2
          }
        },
        {
          "name": "order-processor",
          "status": "RUNNING",
          "lastHeartbeat": "2025-12-09T10:29:58Z",
          "healthy": true,
          "stats": {
            "ordersProcessed": 567
          }
        }
      ],
      "total": 2,
      "healthy": 2,
      "unhealthy": 0
    }
    

    4.2 GET /admin/workers/{name}

    Détails d’un worker spécifique.

    {
      "name": "kafka-consumer",
      "status": "RUNNING",
      "healthy": true,
      "startPriority": 10,
      "stopPriority": 90,
      "scheduled": false,
      "passive": false,
      "cycleIntervalMs": 1000,
      "lastHeartbeat": "2025-12-09T10:29:55Z",
      "stats": {
        "processed": 12345,
        "errors": 2,
        "lastOffset": 98765
      }
    }
    

    4.3 POST /admin/workers/{name}/stop

    Arrête un worker spécifique.

    curl -X POST http://localhost:8080/admin/workers/kafka-consumer/stop
    

    4.4 POST /admin/workers/{name}/start

    Redémarre un worker arrêté.

    curl -X POST http://localhost:8080/admin/workers/kafka-consumer/start
    

    5. Endpoints Configuration

    5.1 GET /admin/config

    Configuration actuelle (sans secrets).

    {
      "appName": "socle-v4",
      "envName": "PROD",
      "region": "MTQ",
      "version": "4.0.0",
      "execId": "socle-v4-abc123",
      "kvbus": {
        "mode": "redis"
      },
      "techdb": {
        "enabled": true
      },
      "logging": {
        "forwarder": {
          "enabled": true,
          "transportMode": "http"
        }
      }
    }
    

    5.2 GET /admin/config/env

    Variables d’environnement (filtrées).

    {
      "APP_NAME": "socle-v4",
      "ENV_NAME": "PROD",
      "REGION": "MTQ",
      "HTTP_PORT": "8080",
      "KVBUS_MODE": "redis"
    }
    

    6. Endpoints Registry

    6.1 GET /admin/registry

    Contenu du SharedDataRegistry.

    {
      "database.connected": true,
      "metrics.requests.total": 12345,
      "metrics.errors.total": 23,
      "batch.current.id": "batch-001",
      "worker.kafka.offset": 98765
    }
    

    6.2 GET /admin/registry/{key}

    Valeur d’une clé spécifique.

    {
      "key": "metrics.requests.total",
      "value": 12345,
      "healthLevel": "NORMAL"
    }
    

    6.3 GET /admin/registry/health

    Clés avec leur niveau de santé.

    {
      "database.connected": "CRITICAL",
      "cache.available": "IMPORTANT",
      "metrics.requests.total": "INFO"
    }
    

    6.4 GET /admin/registry/unhealthy

    Clés en état unhealthy.

    [
      {
        "key": "external.api.available",
        "value": false,
        "healthLevel": "CRITICAL"
      }
    ]
    

    7. Endpoints TechDB (V4)

    7.1 GET /admin/techdb/offsets

    Tous les offsets stockés.

    {
      "offsets": [
        {
          "sourceName": "kafka",
          "partitionKey": "orders-topic-0",
          "lastSequence": 123456,
          "lastUpdated": "2025-12-09T10:30:00Z"
        },
        {
          "sourceName": "nats",
          "partitionKey": "events.orders",
          "lastSequence": 789012,
          "lastUpdated": "2025-12-09T10:29:55Z"
        }
      ]
    }
    

    7.2 GET /admin/techdb/workers

    État des workers persisté.

    {
      "workers": [
        {
          "workerId": "kafka-consumer-001",
          "status": "RUNNING",
          "lastHeartbeat": "2025-12-09T10:30:00Z",
          "metadata": {
            "messagesPerMinute": 523
          }
        }
      ]
    }
    

    7.3 GET /admin/techdb/events

    Événements techniques récents.

    {
      "events": [
        {
          "id": 123,
          "createdAt": "2025-12-09T10:25:00Z",
          "type": "PIPELINE_ERROR",
          "payload": {
            "pipeline": "order-processing",
            "error": "Connection timeout"
          }
        }
      ]
    }
    

    8. Endpoints Resilience

    8.1 GET /admin/resilience/circuits

    État des circuit breakers.

    {
      "circuits": {
        "payment-gateway": "CLOSED",
        "inventory-api": "HALF_OPEN",
        "notification-service": "OPEN"
      }
    }
    

    8.2 POST /admin/resilience/circuits/{name}/reset

    Reset un circuit breaker.

    curl -X POST http://localhost:8080/admin/resilience/circuits/notification-service/reset
    

    9. Endpoints Scheduler

    9.1 GET /admin/scheduler/jobs

    Jobs schedulés.

    {
      "jobs": [
        {
          "jobId": "worker:daily-report",
          "type": "cron",
          "schedule": "0 0 6 * * ?",
          "scheduledAt": "2025-12-09T06:00:00Z"
        },
        {
          "jobId": "worker:health-check",
          "type": "interval",
          "intervalMs": 30000,
          "scheduledAt": "2025-12-09T10:00:00Z"
        }
      ]
    }
    

    9.2 POST /admin/scheduler/jobs/{jobId}/trigger

    Déclenche un job immédiatement.

    curl -X POST http://localhost:8080/admin/scheduler/jobs/worker:daily-report/trigger
    

    10. Endpoints LogForwarder (V4)

    10.1 GET /admin/logforwarder/status

    État du LogForwarder.

    {
      "enabled": true,
      "transportMode": "http",
      "queueSize": 23,
      "queueCapacity": 10000,
      "fallbackCount": 0,
      "lastFlush": "2025-12-09T10:29:55Z"
    }
    

    10.2 POST /admin/logforwarder/flush

    Force le flush des logs.

    curl -X POST http://localhost:8080/admin/logforwarder/flush
    

    10.3 POST /admin/logforwarder/replay

    Rejoue les logs en fallback.

    curl -X POST http://localhost:8080/admin/logforwarder/replay
    

    11. Implémentation

    package eu.lmvi.socle.admin;
    
    @RestController
    @RequestMapping("${socle.admin.path-prefix:/admin}")
    public class AdminRestApi {
    
        @Autowired private Supervisor supervisor;
        @Autowired private SharedDataRegistry registry;
        @Autowired private SocleConfiguration config;
        @Autowired(required = false) private TechDbManager techDb;
        @Autowired(required = false) private Scheduler scheduler;
    
        // === Health ===
    
        @GetMapping("/health")
        public ResponseEntity<HealthResponse> health() {
            HealthStatus status = supervisor.getGlobalHealth();
            HttpStatus httpStatus = status == HealthStatus.HEALTHY
                ? HttpStatus.OK
                : HttpStatus.SERVICE_UNAVAILABLE;
    
            return ResponseEntity.status(httpStatus).body(new HealthResponse(
                status,
                Instant.now(),
                getUptime(),
                supervisor.getUnhealthyWorkers()
            ));
        }
    
        @GetMapping("/health/live")
        public ResponseEntity<Map<String, String>> live() {
            return ResponseEntity.ok(Map.of("status", "UP"));
        }
    
        @GetMapping("/health/ready")
        public ResponseEntity<Map<String, Object>> ready() {
            HealthStatus status = supervisor.getGlobalHealth();
            if (status != HealthStatus.HEALTHY) {
                return ResponseEntity.status(503).body(Map.of(
                    "status", "NOT_READY",
                    "unhealthy", supervisor.getUnhealthyWorkers()
                ));
            }
            return ResponseEntity.ok(Map.of("status", "READY"));
        }
    
        // === Workers ===
    
        @GetMapping("/workers")
        public Map<String, Object> workers() {
            Map<String, WorkerState> states = supervisor.getAllWorkerStates();
            return Map.of(
                "workers", states.values(),
                "total", states.size(),
                "healthy", states.values().stream().filter(WorkerState::isHealthy).count(),
                "unhealthy", states.values().stream().filter(s -> !s.isHealthy()).count()
            );
        }
    
        @GetMapping("/workers/{name}")
        public ResponseEntity<WorkerState> worker(@PathVariable String name) {
            WorkerState state = supervisor.getWorkerState(name);
            return state != null
                ? ResponseEntity.ok(state)
                : ResponseEntity.notFound().build();
        }
    
        // === Config ===
    
        @GetMapping("/config")
        public Map<String, Object> config() {
            return Map.of(
                "appName", config.getApp_name(),
                "envName", config.getEnv_name(),
                "region", config.getRegion(),
                "version", config.getVersion(),
                "execId", config.getExec_id()
            );
        }
    
        // === Registry ===
    
        @GetMapping("/registry")
        public Map<String, Object> registry() {
            return registry.getAll();
        }
    
        @GetMapping("/registry/{key}")
        public ResponseEntity<Map<String, Object>> registryKey(@PathVariable String key) {
            return registry.get(key)
                .map(v -> ResponseEntity.ok(Map.of(
                    "key", key,
                    "value", v,
                    "healthLevel", registry.getHealthLevel(key)
                )))
                .orElse(ResponseEntity.notFound().build());
        }
    
        // ... autres endpoints
    }
    

    12. Sécurité

    12.1 Authentification Basic

    # Avec authentification
    curl -u admin:secret http://localhost:8080/admin/workers
    
    # En-tête Authorization
    curl -H "Authorization: Basic YWRtaW46c2VjcmV0" http://localhost:8080/admin/workers
    

    12.2 Endpoints publics

    Les endpoints suivants sont accessibles sans authentification :

    • /admin/health
    • /admin/health/live
    • /admin/health/ready

    13. Bonnes pratiques

    DO

    • Activer l’authentification en production
    • Utiliser HTTPS pour l’API admin
    • Limiter l’accès réseau à l’API admin
    • Monitorer les accès à l’API admin

    DON’T

    • Ne pas exposer l’API admin publiquement
    • Ne pas désactiver l’authentification en production
    • Ne pas logger les credentials

    14. Références

  • Socle V004 – Sécurité

    Socle V004 – Sécurité

    10 – Security

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Le Socle V4 intègre plusieurs mécanismes de sécurité :

    • Authentification Admin API (Basic Auth)
    • Client JWT pour services externes (nouveauté V4)
    • Filtrage des endpoints sensibles
    • Gestion sécurisée des secrets

    2. Authentification Admin API

    2.1 Configuration

    socle:
      admin:
        enabled: true
        auth:
          enabled: ${ADMIN_AUTH_ENABLED:false}
          username: ${ADMIN_USERNAME:admin}
          password: ${ADMIN_PASSWORD:}
    

    2.2 AdminAuthFilter

    package eu.lmvi.socle.security;
    
    @Component
    @Order(1)
    public class AdminAuthFilter implements Filter {
    
        private final SocleConfiguration config;
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
    
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;
    
            // Skip si auth désactivée
            if (!config.getAdmin().getAuth().isEnabled()) {
                chain.doFilter(request, response);
                return;
            }
    
            // Skip les endpoints publics
            String path = httpRequest.getRequestURI();
            if (isPublicEndpoint(path)) {
                chain.doFilter(request, response);
                return;
            }
    
            // Vérifier l'authentification pour /admin/*
            if (path.startsWith("/admin")) {
                String authHeader = httpRequest.getHeader("Authorization");
    
                if (!isValidAuth(authHeader)) {
                    httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    httpResponse.setHeader("WWW-Authenticate", "Basic realm=\"Admin API\"");
                    return;
                }
            }
    
            chain.doFilter(request, response);
        }
    
        private boolean isPublicEndpoint(String path) {
            return path.equals("/admin/health") || path.equals("/admin/health/live");
        }
    
        private boolean isValidAuth(String authHeader) {
            if (authHeader == null || !authHeader.startsWith("Basic ")) {
                return false;
            }
    
            String base64Credentials = authHeader.substring("Basic ".length());
            String credentials = new String(Base64.getDecoder().decode(base64Credentials));
            String[] parts = credentials.split(":", 2);
    
            if (parts.length != 2) {
                return false;
            }
    
            return parts[0].equals(config.getAdmin().getAuth().getUsername())
                && parts[1].equals(config.getAdmin().getAuth().getPassword());
        }
    }
    

    2.3 Utilisation

    # Sans auth
    curl http://localhost:8080/admin/health
    
    # Avec auth
    curl -u admin:secret http://localhost:8080/admin/workers
    

    3. JWT Auth Client (Nouveauté V4)

    3.1 Principe

    Le SocleAuthClient permet aux applications Socle de s’authentifier auprès de services centraux (LogHub, Registry, etc.).

    Application Socle  ──────►  Auth Server  ──────►  Services sécurisés
          │                          │                      │
          │  1. Login (API Key)      │                      │
          │─────────────────────────►│                      │
          │                          │                      │
          │  2. JWT tokens           │                      │
          │◄─────────────────────────│                      │
          │                          │                      │
          │  3. Requêtes avec Bearer │                      │
          │──────────────────────────────────────────────────►
    

    3.2 Configuration

    socle:
      auth:
        enabled: ${AUTH_ENABLED:false}
        server-url: ${AUTH_SERVER_URL:https://auth.mycompany.com}
        source-name: ${SOURCE_NAME:${socle.app_name}}
        api-key: ${API_KEY:}
        access-token-buffer-seconds: 60
    

    3.3 Utilisation

    @Service
    public class SecuredApiClient {
    
        @Autowired(required = false)
        private SocleAuthClient authClient;
    
        public void callSecuredApi() {
            if (authClient == null || !authClient.isAuthenticated()) {
                throw new SecurityException("Authentication required");
            }
    
            String token = authClient.getValidAccessToken();
    
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.mycompany.com/data"))
                .header("Authorization", "Bearer " + token)
                .build();
    
            // ...
        }
    }
    

    Voir 23-AUTH-CLIENT pour la documentation complète.

    4. Gestion des secrets

    4.1 Variables d’environnement

    # JAMAIS dans le code ou les fichiers committes
    export ADMIN_PASSWORD="super-secret-password"
    export API_KEY="my-api-key"
    export REDIS_PASSWORD="redis-password"
    export TECHDB_PASSWORD="techdb-password"
    

    4.2 Docker Secrets

    # docker-compose.yml
    services:
      app:
        image: socle-v4:latest
        secrets:
          - admin_password
          - api_key
        environment:
          - ADMIN_PASSWORD_FILE=/run/secrets/admin_password
          - API_KEY_FILE=/run/secrets/api_key
    
    secrets:
      admin_password:
        file: ./secrets/admin_password.txt
      api_key:
        file: ./secrets/api_key.txt
    

    4.3 Kubernetes Secrets

    apiVersion: v1
    kind: Secret
    metadata:
      name: socle-secrets
    type: Opaque
    stringData:
      ADMIN_PASSWORD: "super-secret"
      API_KEY: "my-api-key"
    ---
    apiVersion: apps/v1
    kind: Deployment
    spec:
      template:
        spec:
          containers:
            - name: app
              envFrom:
                - secretRef:
                    name: socle-secrets
    

    4.4 Configuration sécurisée

    @Configuration
    public class SecretsConfiguration {
    
        @Value("${ADMIN_PASSWORD:#{null}}")
        private String adminPassword;
    
        @Value("${ADMIN_PASSWORD_FILE:#{null}}")
        private String adminPasswordFile;
    
        @PostConstruct
        public void loadSecrets() {
            // Charger depuis fichier si spécifié
            if (adminPasswordFile != null) {
                try {
                    adminPassword = Files.readString(Path.of(adminPasswordFile)).trim();
                } catch (IOException e) {
                    throw new RuntimeException("Failed to load secret from file", e);
                }
            }
        }
    
        public String getAdminPassword() {
            return adminPassword;
        }
    }
    

    5. CORS Configuration

    @Configuration
    public class CorsConfiguration implements WebMvcConfigurer {
    
        @Value("${socle.security.cors.allowed-origins:*}")
        private String allowedOrigins;
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/api/**")
                .allowedOrigins(allowedOrigins.split(","))
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .exposedHeaders("X-Total-Count")
                .allowCredentials(true)
                .maxAge(3600);
        }
    }
    

    6. Rate Limiting

    6.1 Implémentation simple

    @Component
    public class RateLimitFilter implements Filter {
    
        private final KvBus kvBus;
        private final int maxRequests = 100;
        private final Duration window = Duration.ofMinutes(1);
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
    
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;
    
            String clientId = getClientId(httpRequest);
            String key = "ratelimit:" + clientId + ":" + Instant.now().truncatedTo(ChronoUnit.MINUTES);
    
            long count = kvBus.increment(key);
            if (count == 1) {
                kvBus.setTtl(key, window);
            }
    
            if (count > maxRequests) {
                httpResponse.setStatus(429);
                httpResponse.setHeader("Retry-After", "60");
                httpResponse.getWriter().write("Rate limit exceeded");
                return;
            }
    
            httpResponse.setHeader("X-RateLimit-Limit", String.valueOf(maxRequests));
            httpResponse.setHeader("X-RateLimit-Remaining", String.valueOf(maxRequests - count));
    
            chain.doFilter(request, response);
        }
    
        private String getClientId(HttpServletRequest request) {
            String apiKey = request.getHeader("X-API-Key");
            if (apiKey != null) {
                return apiKey;
            }
            return request.getRemoteAddr();
        }
    }
    

    7. Input Validation

    7.1 Validation des DTOs

    public record CreateOrderRequest(
        @NotNull @Size(min = 1, max = 100)
        String customerId,
    
        @NotEmpty
        List<@Valid OrderItem> items,
    
        @Email
        String notificationEmail
    ) {}
    
    public record OrderItem(
        @NotNull @Size(min = 1, max = 50)
        String productId,
    
        @Min(1) @Max(1000)
        int quantity
    ) {}
    

    7.2 Controller avec validation

    @RestController
    @RequestMapping("/api/orders")
    public class OrderController {
    
        @PostMapping
        public ResponseEntity<Order> createOrder(@Valid @RequestBody CreateOrderRequest request) {
            // request est validé automatiquement
            return ResponseEntity.ok(orderService.create(request));
        }
    }
    

    7.3 Sanitization

    public class InputSanitizer {
    
        private static final Pattern SAFE_STRING = Pattern.compile("^[a-zA-Z0-9-_]+$");
    
        public static String sanitizeId(String input) {
            if (input == null) return null;
            if (!SAFE_STRING.matcher(input).matches()) {
                throw new IllegalArgumentException("Invalid ID format");
            }
            return input;
        }
    
        public static String sanitizeForLog(String input) {
            if (input == null) return null;
            return input.replaceAll("[\n\r\t]", "_");
        }
    }
    

    8. Logging sécurisé

    8.1 Ne pas logger les secrets

    // MAUVAIS
    log.info("Connecting with password: {}", password);
    log.info("API Key: {}", apiKey);
    log.info("Request: {}", requestWithSensitiveData);
    
    // BON
    log.info("Connecting to database");
    log.info("Using API Key: {}...", apiKey.substring(0, 4));
    log.info("Request received for user: {}", sanitizeForLog(userId));
    

    8.2 Pattern pour masquer les données sensibles

    public class SecureLogger {
    
        private static final Set<String> SENSITIVE_FIELDS = Set.of(
            "password", "apiKey", "token", "secret", "credential"
        );
    
        public static String maskSensitiveData(Map<String, Object> data) {
            Map<String, Object> masked = new HashMap<>();
            for (Map.Entry<String, Object> entry : data.entrySet()) {
                if (SENSITIVE_FIELDS.contains(entry.getKey().toLowerCase())) {
                    masked.put(entry.getKey(), "***MASKED***");
                } else {
                    masked.put(entry.getKey(), entry.getValue());
                }
            }
            return masked.toString();
        }
    }
    

    9. Headers de sécurité

    @Component
    public class SecurityHeadersFilter implements Filter {
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
    
            HttpServletResponse httpResponse = (HttpServletResponse) response;
    
            // Prevent clickjacking
            httpResponse.setHeader("X-Frame-Options", "DENY");
    
            // XSS protection
            httpResponse.setHeader("X-Content-Type-Options", "nosniff");
            httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
    
            // HSTS (si HTTPS)
            httpResponse.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
    
            // Content Security Policy
            httpResponse.setHeader("Content-Security-Policy", "default-src 'self'");
    
            chain.doFilter(request, response);
        }
    }
    

    10. Audit logging

    @Aspect
    @Component
    public class AuditAspect {
    
        private static final Logger auditLog = LoggerFactory.getLogger("AUDIT");
    
        @Around("@annotation(Audited)")
        public Object audit(ProceedingJoinPoint joinPoint) throws Throwable {
            String method = joinPoint.getSignature().getName();
            String user = getCurrentUser();
            Instant start = Instant.now();
    
            try {
                Object result = joinPoint.proceed();
                auditLog.info("SUCCESS | user={} | method={} | duration={}ms",
                    user, method, Duration.between(start, Instant.now()).toMillis());
                return result;
            } catch (Exception e) {
                auditLog.warn("FAILURE | user={} | method={} | error={}",
                    user, method, e.getMessage());
                throw e;
            }
        }
    
        private String getCurrentUser() {
            // Récupérer l'utilisateur du contexte
            return "system";
        }
    }
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Audited {}
    

    11. Checklist de sécurité

    Configuration

    • [ ] ADMIN_AUTH_ENABLED=true en production
    • [ ] Mots de passe forts et uniques
    • [ ] Secrets via variables d’environnement ou secret manager
    • [ ] HTTPS activé
    • [ ] CORS configuré correctement

    Code

    • [ ] Validation de tous les inputs
    • [ ] Pas de secrets dans les logs
    • [ ] Headers de sécurité activés
    • [ ] Rate limiting en place
    • [ ] Audit logging activé

    Infrastructure

    • [ ] Firewall configuré
    • [ ] Ports non nécessaires fermés
    • [ ] H2 Console désactivée en production
    • [ ] Accès admin restreint

    12. 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 – 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