Auteur/autrice : jmh

  • Socle V004 – H2 et TechDB

    Socle V004 – H2 et TechDB

    21 – H2 TechDB (Nouveaute V4)

    Version : 4.0.2 Date : 2026-01-15

    1. Introduction

    La H2 TechDB est une base de donnees embarquee introduite dans le Socle V4 pour stocker l’etat technique de maniere persistante.

    Pourquoi H2 ?

    Critere H2 Nitrite (ancien)
    Embarque
    ARM/AMD64 ⚠️ Problemes
    UI debug ✅ H2 Console
    JSON SQL ✅ JSON_VALUE
    Stabilite ⚠️ v4 instable

    Nouveautes V4.0.1

    • Standard de tables x_ : Nouvelle structure avec champs techniques standardises
    • H2 Console sur port 9376 : Interface web dediee pour explorer la base
    • TechDbReaderWorker : Worker passif de lecture des donnees
    • TechDbPurgeWorker : Purge automatique des donnees obsoletes

    Nouveautes V4.0.2

    • API SQL REST : Nouveau endpoint /techdb/query pour requetes SQL via HTTP
    • Authentification Basic Auth : Securisation de l’acces API
    • Rate Limiting : Protection contre les abus (60 req/min/IP par defaut)
    • Mode Readonly : Protection contre les modifications accidentelles

    2. Cas d’usage

    La TechDB stocke :

    • Offsets/sequences : Position dans Kafka, NATS, DB2
    • Etat local des workers : Statut, derniere execution
    • Evenements techniques : Logs importants
    • Fallback logs : Logs non envoyes (LogForwarder)
    • Cle-valeur : Donnees generiques avec TTL

    3. Configuration

    3.1 application.yml

    socle:
      techdb:
        enabled: ${TECHDB_ENABLED:true}
        url: jdbc:h2:file:${TECHDB_PATH:./data/socle-techdb};MODE=PostgreSQL;DB_CLOSE_DELAY=-1;AUTO_SERVER=TRUE
        username: ${TECHDB_USERNAME:LMVI}
        password: ${TECHDB_PASSWORD:LMVI-SOCLEV004}
    
        # H2 Console Web (port 9376)
        console:
          enabled: ${TECHDB_CONSOLE_ENABLED:true}
          port: ${TECHDB_CONSOLE_PORT:9376}
          bind_address: ${TECHDB_CONSOLE_BIND:}
          allow_remote: ${TECHDB_CONSOLE_ALLOW_REMOTE:false}
    
        # Purge automatique des donnees anciennes
        purge:
          enabled: ${TECHDB_PURGE_ENABLED:true}
          schedule: ${TECHDB_PURGE_SCHEDULE:0 0 3 * * ?}  # 3h du matin
          events_retention_days: ${TECHDB_PURGE_EVENTS_DAYS:7}
          logs_retention_days: ${TECHDB_PURGE_LOGS_DAYS:3}
    

    3.2 Variables d’environnement

    Variable Description Defaut
    TECHDB_ENABLED Activer TechDB true
    TECHDB_PATH Chemin fichier H2 ./data/socle-techdb
    TECHDB_USERNAME Nom d’utilisateur LMVI
    TECHDB_PASSWORD Mot de passe LMVI-SOCLEV004
    TECHDB_CONSOLE_ENABLED Activer console web true
    TECHDB_CONSOLE_PORT Port console 9376
    TECHDB_CONSOLE_ALLOW_REMOTE Acces distant false
    TECHDB_PURGE_ENABLED Activer purge auto true
    TECHDB_PURGE_EVENTS_DAYS Retention events 7
    TECHDB_PURGE_LOGS_DAYS Retention logs 3

    3.3 Personnalisation des identifiants par environnement

    Les identifiants par defaut (LMVI / LMVI-SOCLEV004) conviennent pour le developpement et les tests. Pour la production, il est recommande de personnaliser via variables d’environnement.

    Docker Compose :

    services:
      mon-app:
        environment:
          - TECHDB_USERNAME=MonUserProd
          - TECHDB_PASSWORD=MonMotDePasseSecurise123!
    

    Java direct :

    export TECHDB_USERNAME=MonUserProd
    export TECHDB_PASSWORD=MonMotDePasseSecurise123!
    java -jar mon-application.jar
    

    Kubernetes :

    env:
      - name: TECHDB_USERNAME
        valueFrom:
          secretKeyRef:
            name: techdb-credentials
            key: username
      - name: TECHDB_PASSWORD
        valueFrom:
          secretKeyRef:
            name: techdb-credentials
            key: password
    

    Note : Si vous changez les identifiants sur une base existante, vous devez supprimer le fichier socle-techdb.mv.db et laisser l’application recreer la base.

    4. Schéma de base

    Les tables sont créées automatiquement au démarrage :

    -- Offsets / séquences
    CREATE TABLE IF NOT EXISTS socle_offsets (
        id IDENTITY PRIMARY KEY,
        source_name VARCHAR(200) NOT NULL,
        partition_key VARCHAR(200) NOT NULL,
        last_sequence BIGINT NOT NULL,
        last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        extra JSON,
        UNIQUE(source_name, partition_key)
    );
    
    -- État local des workers
    CREATE TABLE IF NOT EXISTS socle_worker_state (
        id IDENTITY PRIMARY KEY,
        worker_id VARCHAR(200) NOT NULL UNIQUE,
        status VARCHAR(20) NOT NULL,
        last_heartbeat TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        metadata JSON
    );
    
    -- Événements techniques
    CREATE TABLE IF NOT EXISTS socle_events (
        id IDENTITY PRIMARY KEY,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        type VARCHAR(100) NOT NULL,
        payload JSON
    );
    
    -- Fallback logs (LogForwarder)
    CREATE TABLE IF NOT EXISTS socle_log_fallback (
        id IDENTITY PRIMARY KEY,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        log_entry JSON NOT NULL,
        retry_count INT DEFAULT 0
    );
    

    5. Interface TechDbManager

    package eu.lmvi.socle.techdb;
    
    @Component
    public class TechDbManager {
    
        // ===== Lifecycle =====
    
        /**
         * Initialise la base H2 et crée les tables
         */
        public void initialize();
    
        /**
         * Ferme proprement la connexion
         */
        public void close();
    
        /**
         * Vérifie la santé de la base
         */
        public boolean isHealthy();
    
        // ===== Offsets =====
    
        /**
         * Sauvegarde un offset
         */
        public void saveOffset(String sourceName, String partitionKey,
                               long sequence, Map<String, Object> extra);
    
        /**
         * Récupère un offset
         */
        public OptionalLong getOffset(String sourceName, String partitionKey);
    
        /**
         * Liste tous les offsets d'une source
         */
        public List<OffsetRecord> getOffsets(String sourceName);
    
        // ===== Worker State =====
    
        /**
         * Sauvegarde l'état d'un worker
         */
        public void saveWorkerState(String workerId, String status,
                                    Map<String, Object> metadata);
    
        /**
         * Récupère l'état d'un worker
         */
        public Optional<WorkerState> getWorkerState(String workerId);
    
        // ===== Events =====
    
        /**
         * Enregistre un événement technique
         */
        public void logEvent(String type, Map<String, Object> payload);
    
        /**
         * Récupère les événements
         */
        public List<TechEvent> getEvents(String type, Instant since, int limit);
    
        // ===== Generic KV =====
    
        public void put(String key, String value);
        public Optional<String> get(String key);
        public void delete(String key);
    }
    

    6. Utilisation

    6.1 Injection

    @Service
    public class MonWorker implements Worker {
    
        @Autowired
        private TechDbManager techDb;
    
        @Override
        public void doWork() {
            // Récupérer le dernier offset
            OptionalLong lastOffset = techDb.getOffset("kafka", "my-topic-0");
            long startFrom = lastOffset.orElse(0L);
    
            // Traiter les messages...
            long newOffset = processMessages(startFrom);
    
            // Sauvegarder le nouvel offset
            techDb.saveOffset("kafka", "my-topic-0", newOffset,
                Map.of("processed", true));
        }
    }
    

    6.2 Gestion des offsets

    // Sauvegarder
    techDb.saveOffset("nats", "events.orders", 123456789L,
        Map.of("consumer", "order-processor"));
    
    // Récupérer
    OptionalLong offset = techDb.getOffset("nats", "events.orders");
    if (offset.isPresent()) {
        log.info("Dernier offset: {}", offset.getAsLong());
    }
    
    // Lister tous les offsets NATS
    List<OffsetRecord> offsets = techDb.getOffsets("nats");
    offsets.forEach(o -> log.info("{}: {}", o.partitionKey(), o.lastSequence()));
    

    6.3 État des workers

    // Sauvegarder l'état
    techDb.saveWorkerState("kafka-consumer-001", "RUNNING",
        Map.of(
            "lastProcessed", Instant.now(),
            "messagesPerMinute", 523
        ));
    
    // Récupérer l'état
    Optional<WorkerState> state = techDb.getWorkerState("kafka-consumer-001");
    state.ifPresent(s -> {
        log.info("Worker {} - Status: {}", s.workerId(), s.status());
    });
    

    6.4 Événements techniques

    // Logger un événement
    techDb.logEvent("PIPELINE_ERROR", Map.of(
        "pipeline", "order-processing",
        "error", "Connection timeout",
        "messageId", "msg-123"
    ));
    
    // Récupérer les événements récents
    List<TechEvent> errors = techDb.getEvents(
        "PIPELINE_ERROR",
        Instant.now().minus(1, ChronoUnit.HOURS),
        100
    );
    

    7. H2 Console Web (Port 9376)

    Le Socle V4 expose une console H2 dediee sur le port 9376 via le TechDbConsoleWorker.

    Acces

    http://localhost:9376
    

    Informations de connexion

    Champ Valeur
    JDBC URL jdbc:h2:./data/socle-techdb (local) ou jdbc:h2:/app/data/socle-techdb (Docker)
    User LMVI (defaut, personnalisable via TECHDB_USERNAME)
    Password LMVI-SOCLEV004 (defaut, personnalisable via TECHDB_PASSWORD)
    Driver org.h2.Driver

    Configuration

    socle:
      techdb:
        console:
          enabled: true       # Activer/desactiver
          port: 9376          # Port dedie
          bind_address: ""    # Vide = toutes interfaces
          allow_remote: false # Securite: localhost only
    

    Requetes utiles

    -- Voir tous les offsets
    SELECT * FROM techdb_offsets ORDER BY x_dateCreated DESC;
    
    -- Offsets Kafka uniquement
    SELECT * FROM techdb_offsets WHERE topic LIKE 'kafka%';
    
    -- Etat des workers
    SELECT worker_name, state, last_run_at, error_count FROM techdb_worker_state;
    
    -- Evenements non traites
    SELECT * FROM techdb_events WHERE processed = FALSE ORDER BY x_dateCreated;
    
    -- Logs non envoyes
    SELECT COUNT(*) as pending FROM techdb_log_buffer WHERE forwarded = FALSE;
    
    -- Cles KV avec expiration
    SELECT kv_key, value_type, expires_at FROM techdb_kv WHERE expires_at IS NOT NULL;
    

    8. API SQL REST (Nouveaute V4.0.2)

    Le Socle V4 expose une API REST permettant d’executer des requetes SQL sur la TechDB. Cette API est distincte de la console H2 et offre un acces programmatique securise.

    8.1 Configuration

    socle:
      techdb:
        sql_api:
          enabled: ${TECHDB_SQL_API_ENABLED:false}
          auth:
            user: ${TECHDB_API_USER:admin}
            password: ${TECHDB_API_PASSWORD:}
          security:
            readonly: ${TECHDB_SQL_API_READONLY:true}
            blocked_tables: ${TECHDB_SQL_API_BLOCKED_TABLES:}
          limits:
            max_rows: ${TECHDB_SQL_API_MAX_ROWS:1000}
            timeout_seconds: ${TECHDB_SQL_API_TIMEOUT:30}
            rate_limit_per_minute: ${TECHDB_SQL_API_RATE_LIMIT:60}
          logging:
            log_queries: ${TECHDB_SQL_API_LOG_QUERIES:true}
    

    8.2 Variables d’environnement

    Variable Description Defaut
    TECHDB_SQL_API_ENABLED Activer l’API SQL false
    TECHDB_API_USER Utilisateur Basic Auth admin
    TECHDB_API_PASSWORD Mot de passe (vide = pas d’auth) « 
    TECHDB_SQL_API_READONLY Mode lecture seule true
    TECHDB_SQL_API_BLOCKED_TABLES Tables interdites (CSV) « 
    TECHDB_SQL_API_MAX_ROWS Limite de lignes 1000
    TECHDB_SQL_API_TIMEOUT Timeout requetes (sec) 30
    TECHDB_SQL_API_RATE_LIMIT Requetes/minute/IP 60

    8.3 Endpoints

    Methode Path Description
    POST /techdb/query Executer une requete SQL
    GET /techdb/tables Liste des tables
    GET /techdb/tables/{name} Details d’une table
    GET /techdb/stats Statistiques DB

    8.4 Authentification

    L’API utilise Basic Auth. Si un mot de passe est configure, toutes les requetes doivent inclure l’en-tete:

    Authorization: Basic base64(user:password)
    

    Exemple avec curl:

    # Sans authentification (si TECHDB_API_PASSWORD vide)
    curl http://localhost:8080/techdb/tables
    
    # Avec authentification
    curl -u admin:monmotdepasse http://localhost:8080/techdb/query \
      -H "Content-Type: application/json" \
      -d '{"sql": "SELECT * FROM techdb_kv LIMIT 10"}'
    

    8.5 Executer une requete SQL

    Requete:

    curl -X POST http://localhost:8080/techdb/query \
      -H "Content-Type: application/json" \
      -u admin:secret \
      -d '{
        "sql": "SELECT * FROM techdb_events WHERE event_type = ?",
        "params": ["WORKER_START"],
        "maxRows": 100
      }'
    

    Reponse succes:

    {
      "success": true,
      "timestamp": 1705312800000,
      "executionTimeMs": 12,
      "rowCount": 15,
      "columns": ["X_ID", "X_DATECREATED", "EVENT_TYPE", "SOURCE", "DATAS"],
      "rows": [
        {"X_ID": 1, "X_DATECREATED": "2025-01-15T10:00:00", "EVENT_TYPE": "WORKER_START", ...},
        ...
      ],
      "truncated": false
    }
    

    Reponse erreur:

    {
      "success": false,
      "timestamp": 1705312800000,
      "error": {
        "code": "SQL_SYNTAX_ERROR",
        "message": "Syntax error in SQL statement"
      }
    }
    

    8.6 Codes d’erreur

    Code HTTP Status Description
    SQL_SYNTAX_ERROR 400 Erreur de syntaxe SQL
    DDL_NOT_ALLOWED 403 Operation DDL interdite
    READONLY_VIOLATION 403 Non-SELECT en mode readonly
    TABLE_BLOCKED 403 Table bloquee par config
    QUERY_TIMEOUT 408 Timeout depasse
    UNAUTHORIZED 401 Authentification requise
    RATE_LIMITED 429 Limite requetes depassee
    TECHDB_DISABLED 503 TechDB non disponible

    8.7 Securite

    Operations toujours interdites:

    • DDL: DROP, ALTER, CREATE, TRUNCATE, GRANT, REVOKE
    • Commandes dangereuses: SHUTDOWN, SCRIPT, BACKUP, RESTORE

    Mode readonly (defaut):

    • Seuls les SELECT sont autorises
    • INSERT, UPDATE, DELETE interdits

    Tables bloquees:

    socle:
      techdb:
        sql_api:
          security:
            blocked_tables: techdb_log_buffer,techdb_kv  # CSV
    

    8.8 Rate Limiting

    L’API applique un rate limiting par IP (sliding window par minute).

    Par defaut: 60 requetes/minute/IP

    Depassement = HTTP 429 Too Many Requests

    8.9 Metriques Prometheus

    techdb_sql_api_queries_total{status="success|error|timeout"}
    techdb_sql_api_queries_duration_seconds
    techdb_sql_api_auth_failures_total
    techdb_sql_api_rate_limited_total
    

    8.10 Exemples pratiques

    Lister les tables:

    curl http://localhost:8080/techdb/tables
    

    Details d’une table:

    curl http://localhost:8080/techdb/tables/TECHDB_EVENTS
    

    Statistiques DB:

    curl http://localhost:8080/techdb/stats
    

    Requete avec parametres:

    curl -X POST http://localhost:8080/techdb/query \
      -H "Content-Type: application/json" \
      -d '{
        "sql": "SELECT worker_name, state, error_count FROM techdb_worker_state WHERE state = ?",
        "params": ["RUNNING"]
      }'
    

    9. Workers TechDB

    Le Socle V4 inclut 3 workers dedies a la gestion de TechDB:

    9.1 TechDbReaderWorker

    Worker PASSIVE exposant des methodes de lecture.

    @Autowired
    private TechDbReaderWorker reader;
    
    // Recuperer tous les offsets
    List<Map<String, Object>> offsets = reader.getAllOffsets();
    
    // Recuperer les evenements recents
    List<Map<String, Object>> events = reader.getRecentEvents(100);
    
    // Executer une requete personnalisee
    List<Map<String, Object>> results = reader.executeCustomQuery(
        "SELECT * FROM techdb_worker_state WHERE state = 'RUNNING'"
    );
    

    9.2 TechDbPurgeWorker

    Worker CRON qui purge automatiquement les donnees obsoletes.

    socle:
      techdb:
        purge:
          enabled: true
          schedule: "0 0 3 * * ?"  # 3h du matin
          events_retention_days: 7
          logs_retention_days: 3
    

    Donnees purgees :

    • Evenements traites > 7 jours
    • Logs forwardes > 3 jours
    • Cles KV expirees

    9.3 TechDbConsoleWorker

    Worker PASSIVE qui demarre la console H2 sur le port 9376.

    @Autowired
    private TechDbConsoleWorker console;
    
    // Verifier si la console est accessible
    boolean running = console.isConsoleRunning();
    
    // Obtenir les infos de connexion
    Map<String, String> info = console.getConnectionInfo();
    

    10. Fonctions JSON H2

    H2 2.x supporte les fonctions JSON SQL standard :

    -- Extraction de valeur
    SELECT JSON_VALUE('{"name":"John","age":30}', '$.name');
    -- Résultat: John
    
    -- Extraction d'objet
    SELECT JSON_QUERY('{"data":{"items":[1,2,3]}}', '$.data');
    -- Résultat: {"items":[1,2,3]}
    
    -- Test d'existence
    SELECT JSON_EXISTS('{"name":"John"}', '$.name');
    -- Résultat: TRUE
    
    -- Construction JSON
    SELECT JSON_OBJECT('name': 'John', 'age': 30);
    -- Résultat: {"name":"John","age":30}
    
    -- Filtrage sur JSON
    SELECT * FROM socle_events
    WHERE JSON_VALUE(payload, '$.severity') = 'ERROR';
    

    11. Integration avec SharedDataRegistry

    TechDB complète SharedDataRegistry :

    Aspect SharedDataRegistry TechDbManager
    Scope Runtime (mémoire) Persistant (fichier)
    Survie restart Non Oui
    Performance Ultra rapide Rapide
    Usage Métriques live Offsets, état

    Exemple de synergie

    @Service
    public class MonService {
    
        @Autowired
        private TechDbManager techDb;
    
        @Autowired
        private SharedDataRegistry sharedData;
    
        public void initialize() {
            // Restaurer l'offset depuis TechDB
            OptionalLong persisted = techDb.getOffset("kafka", "topic-0");
    
            // Créer le compteur en mémoire
            sharedData.createSequence("kafka.offset.topic-0",
                persisted.orElse(0L),
                HealthLevel.CRITICAL);
        }
    
        public void onMessage(long offset) {
            // Mettre à jour en mémoire (rapide)
            sharedData.setSequence("kafka.offset.topic-0", offset);
    
            // Persister périodiquement (moins fréquent)
            if (offset % 1000 == 0) {
                techDb.saveOffset("kafka", "topic-0", offset, null);
            }
        }
    }
    

    12. Bonnes pratiques

    DO

    • ✅ Utiliser TechDB pour les données qui doivent survivre au restart
    • ✅ Persister les offsets périodiquement (pas à chaque message)
    • ✅ Utiliser JSON pour les métadonnées flexibles
    • ✅ Activer H2 Console uniquement en dev

    DON’T

    • ❌ Stocker des données volumineuses (utiliser PostgreSQL)
    • ❌ Faire des requêtes complexes en boucle doWork()
    • ❌ Activer H2 Console en production
    • ❌ Utiliser pour du cache haute fréquence (utiliser KvBus)

    13. Troubleshooting

    Base corrompue

    # Supprimer et recréer
    rm -rf ./data/socle-techdb.*
    # Redémarrer l'application
    

    Fichier verrouillé

    Database may be already in use: "locked by another process"
    

    Solution : Arrêter l’autre instance ou utiliser AUTO_SERVER=TRUE dans l’URL.

    Console H2 inaccessible

    1. Vérifier socle.techdb.console.enabled: true
    2. Vérifier que l’application tourne
    3. Essayer avec le chemin complet du fichier

    14. References

    Socle V004 – TechDB H2

  • Socle V004 – KV-Bus

    Socle V004 – KV-Bus

    06 – KvBus (Key-Value Bus)

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    KvBus est une abstraction de stockage clé-valeur avec deux implémentations :

    • in_memory : HashMap pour le développement local
    • redis : Redis pour la production multi-instances

    Caractéristiques

    • Interface unifiée
    • TTL (Time-To-Live) configurable
    • Opérations atomiques
    • Support JSON pour les objets complexes
    • Patterns pub/sub (Redis uniquement)

    2. Configuration

    2.1 application.yml

    socle:
      kvbus:
        mode: ${KVBUS_MODE:in_memory}
        redis:
          host: ${REDIS_HOST:localhost}
          port: ${REDIS_PORT:6379}
          password: ${REDIS_PASSWORD:}
          database: ${REDIS_DATABASE:0}
          prefix: ${REDIS_PREFIX:socle}
          connect-timeout-ms: ${REDIS_CONNECT_TIMEOUT:5000}
          read-timeout-ms: ${REDIS_READ_TIMEOUT:5000}
          pool:
            max-total: ${REDIS_POOL_MAX:16}
            max-idle: ${REDIS_POOL_MAX_IDLE:8}
            min-idle: ${REDIS_POOL_MIN_IDLE:2}
    

    2.2 Variables d’environnement

    Variable Description Défaut
    KVBUS_MODE Mode (in_memory/redis) in_memory
    REDIS_HOST Hôte Redis localhost
    REDIS_PORT Port Redis 6379
    REDIS_PASSWORD Mot de passe
    REDIS_DATABASE Database number 0
    REDIS_PREFIX Préfixe des clés socle

    3. Interface KvBus

    package eu.lmvi.socle.kv;
    
    public interface KvBus {
    
        // === CRUD basique ===
    
        /**
         * Stocke une valeur
         */
        void put(String key, String value);
    
        /**
         * Stocke une valeur avec TTL
         */
        void put(String key, String value, Duration ttl);
    
        /**
         * Récupère une valeur
         */
        Optional<String> get(String key);
    
        /**
         * Supprime une clé
         */
        void delete(String key);
    
        /**
         * Vérifie l'existence d'une clé
         */
        boolean exists(String key);
    
        // === TTL ===
    
        /**
         * Définit le TTL d'une clé existante
         */
        void setTtl(String key, Duration ttl);
    
        /**
         * Récupère le TTL restant
         */
        Optional<Duration> getTtl(String key);
    
        // === Opérations atomiques ===
    
        /**
         * Incrémente une valeur numérique
         */
        long increment(String key);
    
        /**
         * Incrémente avec delta
         */
        long increment(String key, long delta);
    
        /**
         * Set if not exists
         */
        boolean putIfAbsent(String key, String value);
    
        /**
         * Set if not exists avec TTL
         */
        boolean putIfAbsent(String key, String value, Duration ttl);
    
        // === Opérations en lot ===
    
        /**
         * Récupère plusieurs clés
         */
        Map<String, String> getAll(Collection<String> keys);
    
        /**
         * Stocke plusieurs valeurs
         */
        void putAll(Map<String, String> entries);
    
        /**
         * Supprime plusieurs clés
         */
        void deleteAll(Collection<String> keys);
    
        // === Pattern matching ===
    
        /**
         * Liste les clés correspondant à un pattern
         */
        Set<String> keys(String pattern);
    
        // === JSON helpers ===
    
        /**
         * Stocke un objet en JSON
         */
        <T> void putJson(String key, T object);
    
        /**
         * Récupère un objet depuis JSON
         */
        <T> Optional<T> getJson(String key, Class<T> type);
    
        // === Lifecycle ===
    
        /**
         * Vérifie la santé de la connexion
         */
        boolean isHealthy();
    
        /**
         * Ferme les connexions
         */
        void close();
    }
    

    4. Implémentation InMemoryKvBus

    package eu.lmvi.socle.kv;
    
    @Component
    @ConditionalOnProperty(name = "socle.kvbus.mode", havingValue = "in_memory", matchIfMissing = true)
    public class InMemoryKvBus implements KvBus {
    
        private final ConcurrentHashMap<String, Entry> store = new ConcurrentHashMap<>();
        private final ScheduledExecutorService cleaner;
    
        public InMemoryKvBus() {
            // Nettoyage des entrées expirées toutes les minutes
            cleaner = Executors.newSingleThreadScheduledExecutor();
            cleaner.scheduleAtFixedRate(this::cleanExpired, 1, 1, TimeUnit.MINUTES);
        }
    
        @Override
        public void put(String key, String value) {
            store.put(key, new Entry(value, null));
        }
    
        @Override
        public void put(String key, String value, Duration ttl) {
            Instant expiry = Instant.now().plus(ttl);
            store.put(key, new Entry(value, expiry));
        }
    
        @Override
        public Optional<String> get(String key) {
            Entry entry = store.get(key);
            if (entry == null) return Optional.empty();
            if (entry.isExpired()) {
                store.remove(key);
                return Optional.empty();
            }
            return Optional.of(entry.value);
        }
    
        @Override
        public void delete(String key) {
            store.remove(key);
        }
    
        @Override
        public boolean exists(String key) {
            return get(key).isPresent();
        }
    
        @Override
        public long increment(String key) {
            return increment(key, 1);
        }
    
        @Override
        public long increment(String key, long delta) {
            Entry entry = store.compute(key, (k, v) -> {
                long current = (v == null) ? 0 : Long.parseLong(v.value);
                return new Entry(String.valueOf(current + delta), v != null ? v.expiry : null);
            });
            return Long.parseLong(entry.value);
        }
    
        @Override
        public boolean putIfAbsent(String key, String value) {
            return store.putIfAbsent(key, new Entry(value, null)) == null;
        }
    
        @Override
        public Set<String> keys(String pattern) {
            String regex = pattern.replace("*", ".*");
            return store.keySet().stream()
                .filter(k -> k.matches(regex))
                .collect(Collectors.toSet());
        }
    
        @Override
        public boolean isHealthy() {
            return true;
        }
    
        @Override
        public void close() {
            cleaner.shutdown();
            store.clear();
        }
    
        private void cleanExpired() {
            store.entrySet().removeIf(e -> e.getValue().isExpired());
        }
    
        private record Entry(String value, Instant expiry) {
            boolean isExpired() {
                return expiry != null && Instant.now().isAfter(expiry);
            }
        }
    }
    

    5. Implémentation RedisKvBus

    package eu.lmvi.socle.kv;
    
    @Component
    @ConditionalOnProperty(name = "socle.kvbus.mode", havingValue = "redis")
    public class RedisKvBus implements KvBus {
    
        private static final Logger log = LoggerFactory.getLogger(RedisKvBus.class);
    
        private final JedisPool jedisPool;
        private final String prefix;
        private final ObjectMapper objectMapper;
    
        public RedisKvBus(SocleConfiguration config) {
            JedisPoolConfig poolConfig = new JedisPoolConfig();
            poolConfig.setMaxTotal(config.getKvbus().getRedis().getPool().getMaxTotal());
            poolConfig.setMaxIdle(config.getKvbus().getRedis().getPool().getMaxIdle());
            poolConfig.setMinIdle(config.getKvbus().getRedis().getPool().getMinIdle());
    
            this.jedisPool = new JedisPool(
                poolConfig,
                config.getKvbus().getRedis().getHost(),
                config.getKvbus().getRedis().getPort(),
                config.getKvbus().getRedis().getConnectTimeoutMs(),
                config.getKvbus().getRedis().getPassword(),
                config.getKvbus().getRedis().getDatabase()
            );
    
            this.prefix = config.getKvbus().getRedis().getPrefix() + ":";
            this.objectMapper = new ObjectMapper();
    
            log.info("RedisKvBus initialized: {}:{}",
                config.getKvbus().getRedis().getHost(),
                config.getKvbus().getRedis().getPort());
        }
    
        private String prefixedKey(String key) {
            return prefix + key;
        }
    
        @Override
        public void put(String key, String value) {
            try (Jedis jedis = jedisPool.getResource()) {
                jedis.set(prefixedKey(key), value);
            }
        }
    
        @Override
        public void put(String key, String value, Duration ttl) {
            try (Jedis jedis = jedisPool.getResource()) {
                jedis.setex(prefixedKey(key), ttl.toSeconds(), value);
            }
        }
    
        @Override
        public Optional<String> get(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                return Optional.ofNullable(jedis.get(prefixedKey(key)));
            }
        }
    
        @Override
        public void delete(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                jedis.del(prefixedKey(key));
            }
        }
    
        @Override
        public boolean exists(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.exists(prefixedKey(key));
            }
        }
    
        @Override
        public long increment(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.incr(prefixedKey(key));
            }
        }
    
        @Override
        public long increment(String key, long delta) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.incrBy(prefixedKey(key), delta);
            }
        }
    
        @Override
        public boolean putIfAbsent(String key, String value) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.setnx(prefixedKey(key), value) == 1;
            }
        }
    
        @Override
        public boolean putIfAbsent(String key, String value, Duration ttl) {
            try (Jedis jedis = jedisPool.getResource()) {
                String result = jedis.set(prefixedKey(key), value,
                    SetParams.setParams().nx().ex(ttl.toSeconds()));
                return "OK".equals(result);
            }
        }
    
        @Override
        public Set<String> keys(String pattern) {
            try (Jedis jedis = jedisPool.getResource()) {
                Set<String> rawKeys = jedis.keys(prefixedKey(pattern));
                return rawKeys.stream()
                    .map(k -> k.substring(prefix.length()))
                    .collect(Collectors.toSet());
            }
        }
    
        @Override
        public <T> void putJson(String key, T object) {
            try {
                String json = objectMapper.writeValueAsString(object);
                put(key, json);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("Failed to serialize object", e);
            }
        }
    
        @Override
        public <T> Optional<T> getJson(String key, Class<T> type) {
            return get(key).map(json -> {
                try {
                    return objectMapper.readValue(json, type);
                } catch (JsonProcessingException e) {
                    throw new RuntimeException("Failed to deserialize object", e);
                }
            });
        }
    
        @Override
        public boolean isHealthy() {
            try (Jedis jedis = jedisPool.getResource()) {
                return "PONG".equals(jedis.ping());
            } catch (Exception e) {
                return false;
            }
        }
    
        @Override
        public void close() {
            jedisPool.close();
        }
    }
    

    6. Utilisation

    6.1 Injection

    @Service
    public class MonService {
    
        @Autowired
        private KvBus kvBus;
    
        public void process() {
            // Utiliser kvBus...
        }
    }
    

    6.2 CRUD basique

    // Stocker
    kvBus.put("user:123:name", "John");
    kvBus.put("session:abc", "data", Duration.ofHours(1));
    
    // Récupérer
    Optional<String> name = kvBus.get("user:123:name");
    name.ifPresent(n -> log.info("Name: {}", n));
    
    // Vérifier
    if (kvBus.exists("user:123:name")) {
        // ...
    }
    
    // Supprimer
    kvBus.delete("user:123:name");
    

    6.3 JSON

    // Stocker un objet
    Order order = new Order("123", "PENDING", List.of("item1", "item2"));
    kvBus.putJson("order:123", order);
    
    // Récupérer un objet
    Optional<Order> retrieved = kvBus.getJson("order:123", Order.class);
    

    6.4 Compteurs atomiques

    // Incrémenter
    long newValue = kvBus.increment("stats:requests:total");
    long newValue2 = kvBus.increment("stats:bytes:total", 1024);
    
    // Compteur avec reset quotidien
    String dailyKey = "stats:requests:" + LocalDate.now();
    kvBus.increment(dailyKey);
    kvBus.setTtl(dailyKey, Duration.ofDays(1));
    

    6.5 Lock distribué (Redis)

    public boolean tryLock(String resource, Duration timeout) {
        String lockKey = "lock:" + resource;
        return kvBus.putIfAbsent(lockKey, "locked", timeout);
    }
    
    public void unlock(String resource) {
        kvBus.delete("lock:" + resource);
    }
    
    // Utilisation
    if (tryLock("order-processing", Duration.ofMinutes(5))) {
        try {
            processOrders();
        } finally {
            unlock("order-processing");
        }
    }
    

    6.6 Cache avec TTL

    public Order getOrder(String orderId) {
        String cacheKey = "cache:order:" + orderId;
    
        // Check cache
        Optional<Order> cached = kvBus.getJson(cacheKey, Order.class);
        if (cached.isPresent()) {
            return cached.get();
        }
    
        // Load from DB
        Order order = orderRepository.findById(orderId);
    
        // Cache for 5 minutes
        kvBus.putJson(cacheKey, order);
        kvBus.setTtl(cacheKey, Duration.ofMinutes(5));
    
        return order;
    }
    

    7. Patterns avancés

    7.1 Rate limiting

    public boolean isRateLimited(String userId, int maxRequests, Duration window) {
        String key = "ratelimit:" + userId + ":" + Instant.now().truncatedTo(ChronoUnit.MINUTES);
    
        long count = kvBus.increment(key);
        if (count == 1) {
            kvBus.setTtl(key, window);
        }
    
        return count > maxRequests;
    }
    

    7.2 Session management

    public void createSession(String sessionId, User user) {
        kvBus.putJson("session:" + sessionId, user);
        kvBus.setTtl("session:" + sessionId, Duration.ofHours(24));
    }
    
    public Optional<User> getSession(String sessionId) {
        return kvBus.getJson("session:" + sessionId, User.class);
    }
    
    public void refreshSession(String sessionId) {
        kvBus.setTtl("session:" + sessionId, Duration.ofHours(24));
    }
    
    public void destroySession(String sessionId) {
        kvBus.delete("session:" + sessionId);
    }
    

    7.3 Feature flags

    public boolean isFeatureEnabled(String feature) {
        return kvBus.get("feature:" + feature)
            .map(Boolean::parseBoolean)
            .orElse(false);
    }
    
    public void setFeatureFlag(String feature, boolean enabled) {
        kvBus.put("feature:" + feature, String.valueOf(enabled));
    }
    

    8. KvBus vs TechDB (V4)

    Aspect KvBus TechDB (H2)
    Cas d’usage Cache, sessions, locks Offsets, état persistant
    Survie restart Non (in_memory) / Oui (Redis) Oui (fichier)
    Multi-instances Non (in_memory) / Oui (Redis) Non (par instance)
    Performance Ultra rapide Rapide
    Requêtes Clé simple SQL, JSON

    Règle de choix

    • KvBus : Données temporaires, cache, sessions, compteurs temps réel
    • TechDB : Offsets, checkpoints, état qui doit survivre au restart

    9. Monitoring

    9.1 Métriques

    socle_kvbus_operations_total{operation="get"}
    socle_kvbus_operations_total{operation="put"}
    socle_kvbus_operations_total{operation="delete"}
    socle_kvbus_latency_seconds{operation="get"}
    socle_kvbus_keys_count
    

    9.2 Health Check

    @Component
    public class KvBusHealthIndicator implements HealthIndicator {
    
        @Autowired
        private KvBus kvBus;
    
        @Override
        public Health health() {
            if (kvBus.isHealthy()) {
                return Health.up().build();
            }
            return Health.down().withDetail("error", "KvBus not responding").build();
        }
    }
    

    10. Bonnes pratiques

    DO

    • Utiliser des préfixes de clés cohérents (user:, session:, cache:)
    • Toujours définir un TTL pour les caches
    • Utiliser putIfAbsent pour les locks
    • Préférer Redis en production multi-instances

    DON’T

    • Ne pas stocker de données volumineuses (> 1MB)
    • Ne pas utiliser keys("*") en production (scan)
    • Ne pas oublier de fermer les connexions
    • Ne pas utiliser in_memory pour les données critiques en prod

    11. Références

  • Socle V004 – Kubernetes

    Socle V004 – Kubernetes

    16 – Kubernetes

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Guide de déploiement du Socle V4 sur Kubernetes.

    2. Image Docker

    2.1 Dockerfile

    FROM eclipse-temurin:21-jre-alpine
    
    LABEL maintainer="your-team@company.com"
    LABEL version="4.0.0"
    
    WORKDIR /app
    
    # Non-root user
    RUN addgroup -S socle && adduser -S socle -G socle
    USER socle
    
    # Copy application
    COPY --chown=socle:socle target/socle-v004-4.0.0.jar app.jar
    
    # Health check
    HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
      CMD wget -qO- http://localhost:8080/admin/health/live || exit 1
    
    # Default environment
    ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
    
    EXPOSE 8080
    
    ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
    

    2.2 Build et Push

    # Build
    docker build -t gcr.io/my-project/socle-v4:4.0.0 .
    
    # Push
    docker push gcr.io/my-project/socle-v4:4.0.0
    

    3. Manifests Kubernetes

    3.1 Namespace

    apiVersion: v1
    kind: Namespace
    metadata:
      name: socle
      labels:
        name: socle
    

    3.2 ConfigMap

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: socle-config
      namespace: socle
    data:
      APP_NAME: "socle-v4"
      ENV_NAME: "PROD"
      REGION: "europe-west1"
      HTTP_PORT: "8080"
      KVBUS_MODE: "redis"
      REDIS_HOST: "redis-master.redis.svc.cluster.local"
      TECHDB_ENABLED: "true"
      LOG_FORWARDER_ENABLED: "true"
      LOG_TRANSPORT_MODE: "http"
      SCHEDULER_ENABLED: "true"
      ADMIN_ENABLED: "true"
      ADMIN_AUTH_ENABLED: "true"
    

    3.3 Secret

    apiVersion: v1
    kind: Secret
    metadata:
      name: socle-secrets
      namespace: socle
    type: Opaque
    stringData:
      REDIS_PASSWORD: "your-redis-password"
      ADMIN_PASSWORD: "your-admin-password"
      API_KEY: "your-api-key"
      TECHDB_PASSWORD: "your-techdb-password"
    

    3.4 Deployment

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: socle-v4
      namespace: socle
      labels:
        app: socle-v4
        version: "4.0.0"
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: socle-v4
      strategy:
        type: RollingUpdate
        rollingUpdate:
          maxSurge: 1
          maxUnavailable: 0
      template:
        metadata:
          labels:
            app: socle-v4
            version: "4.0.0"
          annotations:
            prometheus.io/scrape: "true"
            prometheus.io/path: "/actuator/prometheus"
            prometheus.io/port: "8080"
        spec:
          serviceAccountName: socle-sa
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            fsGroup: 1000
          containers:
            - name: socle
              image: gcr.io/my-project/socle-v4:4.0.0
              imagePullPolicy: Always
              ports:
                - name: http
                  containerPort: 8080
                  protocol: TCP
              envFrom:
                - configMapRef:
                    name: socle-config
                - secretRef:
                    name: socle-secrets
              env:
                - name: POD_NAME
                  valueFrom:
                    fieldRef:
                      fieldPath: metadata.name
                - name: POD_NAMESPACE
                  valueFrom:
                    fieldRef:
                      fieldPath: metadata.namespace
                - name: EXEC_ID
                  value: "$(POD_NAME)"
              resources:
                requests:
                  cpu: "250m"
                  memory: "512Mi"
                limits:
                  cpu: "1000m"
                  memory: "1Gi"
              livenessProbe:
                httpGet:
                  path: /admin/health/live
                  port: 8080
                initialDelaySeconds: 30
                periodSeconds: 10
                timeoutSeconds: 5
                failureThreshold: 3
              readinessProbe:
                httpGet:
                  path: /admin/health/ready
                  port: 8080
                initialDelaySeconds: 10
                periodSeconds: 5
                timeoutSeconds: 3
                failureThreshold: 3
              volumeMounts:
                - name: data
                  mountPath: /app/data
                - name: logs
                  mountPath: /app/logs
          volumes:
            - name: data
              emptyDir: {}
            - name: logs
              emptyDir: {}
          affinity:
            podAntiAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
                - weight: 100
                  podAffinityTerm:
                    labelSelector:
                      matchLabels:
                        app: socle-v4
                    topologyKey: kubernetes.io/hostname
    

    3.5 Service

    apiVersion: v1
    kind: Service
    metadata:
      name: socle-v4
      namespace: socle
      labels:
        app: socle-v4
    spec:
      type: ClusterIP
      ports:
        - name: http
          port: 80
          targetPort: 8080
          protocol: TCP
      selector:
        app: socle-v4
    

    3.6 Ingress

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: socle-v4
      namespace: socle
      annotations:
        kubernetes.io/ingress.class: nginx
        nginx.ingress.kubernetes.io/ssl-redirect: "true"
        cert-manager.io/cluster-issuer: letsencrypt-prod
    spec:
      tls:
        - hosts:
            - socle.example.com
          secretName: socle-tls
      rules:
        - host: socle.example.com
          http:
            paths:
              - path: /
                pathType: Prefix
                backend:
                  service:
                    name: socle-v4
                    port:
                      number: 80
    

    3.7 HorizontalPodAutoscaler

    apiVersion: autoscaling/v2
    kind: HorizontalPodAutoscaler
    metadata:
      name: socle-v4
      namespace: socle
    spec:
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: socle-v4
      minReplicas: 2
      maxReplicas: 10
      metrics:
        - type: Resource
          resource:
            name: cpu
            target:
              type: Utilization
              averageUtilization: 70
        - type: Resource
          resource:
            name: memory
            target:
              type: Utilization
              averageUtilization: 80
    

    3.8 PodDisruptionBudget

    apiVersion: policy/v1
    kind: PodDisruptionBudget
    metadata:
      name: socle-v4
      namespace: socle
    spec:
      minAvailable: 1
      selector:
        matchLabels:
          app: socle-v4
    

    3.9 ServiceAccount

    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: socle-sa
      namespace: socle
    

    4. Persistence avec PVC

    4.1 PersistentVolumeClaim

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: socle-data
      namespace: socle
    spec:
      accessModes:
        - ReadWriteOnce
      storageClassName: standard
      resources:
        requests:
          storage: 10Gi
    

    4.2 Deployment avec PVC

    # Dans le Deployment
    spec:
      template:
        spec:
          containers:
            - name: socle
              volumeMounts:
                - name: data
                  mountPath: /app/data
          volumes:
            - name: data
              persistentVolumeClaim:
                claimName: socle-data
    

    5. Network Policies

    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: socle-network-policy
      namespace: socle
    spec:
      podSelector:
        matchLabels:
          app: socle-v4
      policyTypes:
        - Ingress
        - Egress
      ingress:
        # Allow from ingress controller
        - from:
            - namespaceSelector:
                matchLabels:
                  name: ingress-nginx
          ports:
            - port: 8080
        # Allow from Prometheus
        - from:
            - namespaceSelector:
                matchLabels:
                  name: monitoring
          ports:
            - port: 8080
      egress:
        # Allow to Redis
        - to:
            - namespaceSelector:
                matchLabels:
                  name: redis
          ports:
            - port: 6379
        # Allow to DNS
        - to:
            - namespaceSelector: {}
              podSelector:
                matchLabels:
                  k8s-app: kube-dns
          ports:
            - port: 53
              protocol: UDP
    

    6. Helm Chart

    6.1 Chart.yaml

    apiVersion: v2
    name: socle-v4
    description: Socle V4 Framework
    version: 4.0.0
    appVersion: "4.0.0"
    

    6.2 values.yaml

    replicaCount: 2
    
    image:
      repository: gcr.io/my-project/socle-v4
      tag: "4.0.0"
      pullPolicy: Always
    
    service:
      type: ClusterIP
      port: 80
    
    ingress:
      enabled: true
      className: nginx
      hosts:
        - host: socle.example.com
          paths:
            - path: /
              pathType: Prefix
      tls:
        - secretName: socle-tls
          hosts:
            - socle.example.com
    
    resources:
      requests:
        cpu: 250m
        memory: 512Mi
      limits:
        cpu: 1000m
        memory: 1Gi
    
    autoscaling:
      enabled: true
      minReplicas: 2
      maxReplicas: 10
      targetCPUUtilizationPercentage: 70
    
    config:
      APP_NAME: socle-v4
      ENV_NAME: PROD
      KVBUS_MODE: redis
    
    secrets:
      REDIS_PASSWORD: ""
      ADMIN_PASSWORD: ""
      API_KEY: ""
    

    6.3 Installation

    # Install
    helm install socle-v4 ./socle-v4-chart -n socle --create-namespace -f values-prod.yaml
    
    # Upgrade
    helm upgrade socle-v4 ./socle-v4-chart -n socle -f values-prod.yaml
    
    # Uninstall
    helm uninstall socle-v4 -n socle
    

    7. Observability

    7.1 ServiceMonitor (Prometheus Operator)

    apiVersion: monitoring.coreos.com/v1
    kind: ServiceMonitor
    metadata:
      name: socle-v4
      namespace: socle
      labels:
        release: prometheus
    spec:
      selector:
        matchLabels:
          app: socle-v4
      endpoints:
        - port: http
          path: /actuator/prometheus
          interval: 15s
    

    7.2 PrometheusRule

    apiVersion: monitoring.coreos.com/v1
    kind: PrometheusRule
    metadata:
      name: socle-v4-alerts
      namespace: socle
    spec:
      groups:
        - name: socle-v4
          rules:
            - alert: SocleHighErrorRate
              expr: rate(socle_errors_total[5m]) > 0.1
              for: 5m
              labels:
                severity: warning
              annotations:
                summary: High error rate
    

    8. Déploiement Multi-région

    8.1 Structure

    clusters/
    ├── europe-west1/
    │   ├── kustomization.yaml
    │   └── config-patch.yaml
    ├── us-central1/
    │   ├── kustomization.yaml
    │   └── config-patch.yaml
    └── base/
        ├── kustomization.yaml
        ├── deployment.yaml
        ├── service.yaml
        └── configmap.yaml
    

    8.2 Kustomize overlay

    # clusters/europe-west1/kustomization.yaml
    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    bases:
      - ../../base
    patchesStrategicMerge:
      - config-patch.yaml
    configMapGenerator:
      - name: socle-config
        behavior: merge
        literals:
          - REGION=europe-west1
    

    9. Troubleshooting

    Commandes utiles

    # Logs
    kubectl logs -f deployment/socle-v4 -n socle
    
    # Describe pod
    kubectl describe pod -l app=socle-v4 -n socle
    
    # Port forward
    kubectl port-forward svc/socle-v4 8080:80 -n socle
    
    # Exec into pod
    kubectl exec -it deployment/socle-v4 -n socle -- sh
    
    # Check events
    kubectl get events -n socle --sort-by='.lastTimestamp'
    

    10. Références

  • Socle V004 – Scheduler

    Socle V004 – Scheduler

    12 – Scheduler

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Le Scheduler permet d’exécuter des Workers selon des expressions cron ou à intervalles réguliers.

    Caractéristiques

    • Support des expressions cron standard
    • Exécution à intervalle fixe
    • Gestion du chevauchement
    • Intégration avec le MOP

    2. Configuration

    2.1 application.yml

    socle:
      scheduler:
        enabled: ${SCHEDULER_ENABLED:true}
        thread-pool-size: ${SCHEDULER_POOL_SIZE:4}
        default-timezone: ${SCHEDULER_TIMEZONE:Europe/Paris}
    

    2.2 Variables d’environnement

    Variable Description Défaut
    SCHEDULER_ENABLED Activer le scheduler true
    SCHEDULER_POOL_SIZE Taille du thread pool 4
    SCHEDULER_TIMEZONE Timezone par défaut Europe/Paris

    3. Types de scheduling

    3.1 Cron

    Expressions cron standard (6 champs) :

    ┌───────────── seconde (0-59)
    │ ┌───────────── minute (0-59)
    │ │ ┌───────────── heure (0-23)
    │ │ │ ┌───────────── jour du mois (1-31)
    │ │ │ │ ┌───────────── mois (1-12)
    │ │ │ │ │ ┌───────────── jour de la semaine (0-6, 0=dimanche)
    │ │ │ │ │ │
    * * * * * *
    

    Exemples :

    • 0 0 6 * * ? : Tous les jours à 6h00
    • 0 */15 * * * ? : Toutes les 15 minutes
    • 0 0 0 1 * ? : Premier jour de chaque mois à minuit
    • 0 30 8 ? * MON-FRI : 8h30 du lundi au vendredi

    3.2 Intervalle fixe

    Exécution périodique simple :

    @Override
    public long getCycleIntervalMs() {
        return 60000;  // Toutes les minutes
    }
    

    4. Worker schedulé

    4.1 Avec expression cron

    @Component
    public class DailyReportWorker implements Worker {
    
        private static final Logger log = LoggerFactory.getLogger(DailyReportWorker.class);
    
        @Override
        public String getName() {
            return "daily-report-worker";
        }
    
        @Override
        public String getSchedule() {
            return "0 0 6 * * ?";  // Tous les jours à 6h
        }
    
        @Override
        public boolean isScheduled() {
            return true;
        }
    
        @Override
        public void doWork() {
            log.info("Generating daily report...");
            generateReport();
        }
    
        private void generateReport() {
            // Génération du rapport
        }
    
        // Autres méthodes Worker...
    }
    

    4.2 Avec intervalle

    @Component
    public class HealthCheckWorker implements Worker {
    
        @Override
        public String getName() {
            return "health-check-worker";
        }
    
        @Override
        public String getSchedule() {
            return null;  // Pas de cron
        }
    
        @Override
        public boolean isScheduled() {
            return false;  // Pas schedulé par cron
        }
    
        @Override
        public long getCycleIntervalMs() {
            return 30000;  // Toutes les 30 secondes
        }
    
        @Override
        public void doWork() {
            checkHealth();
        }
    }
    

    5. Interface Scheduler

    package eu.lmvi.socle.scheduler;
    
    public interface Scheduler {
    
        /**
         * Planifie un job cron
         */
        void scheduleCron(String jobId, String cronExpression, Runnable task);
    
        /**
         * Planifie un job à intervalle fixe
         */
        void scheduleInterval(String jobId, long intervalMs, Runnable task);
    
        /**
         * Planifie un job à intervalle fixe avec délai initial
         */
        void scheduleInterval(String jobId, long initialDelayMs, long intervalMs, Runnable task);
    
        /**
         * Planifie un job one-shot
         */
        void scheduleOnce(String jobId, long delayMs, Runnable task);
    
        /**
         * Annule un job
         */
        void cancel(String jobId);
    
        /**
         * Vérifie si un job est planifié
         */
        boolean isScheduled(String jobId);
    
        /**
         * Liste les jobs planifiés
         */
        List<ScheduledJob> getScheduledJobs();
    
        /**
         * Démarre le scheduler
         */
        void start();
    
        /**
         * Arrête le scheduler
         */
        void stop();
    }
    

    6. Implémentation

    package eu.lmvi.socle.scheduler;
    
    @Component
    public class DefaultScheduler implements Scheduler {
    
        private static final Logger log = LoggerFactory.getLogger(DefaultScheduler.class);
    
        private final ScheduledExecutorService executor;
        private final ConcurrentHashMap<String, ScheduledFuture<?>> jobs = new ConcurrentHashMap<>();
        private final ConcurrentHashMap<String, ScheduledJob> jobInfo = new ConcurrentHashMap<>();
        private final ZoneId timezone;
    
        public DefaultScheduler(SocleConfiguration config) {
            int poolSize = config.getScheduler().getThreadPoolSize();
            this.executor = Executors.newScheduledThreadPool(poolSize,
                r -> new Thread(r, "scheduler-" + System.currentTimeMillis()));
            this.timezone = ZoneId.of(config.getScheduler().getDefaultTimezone());
        }
    
        @Override
        public void scheduleCron(String jobId, String cronExpression, Runnable task) {
            CronExpression cron = CronExpression.parse(cronExpression);
    
            Runnable scheduledTask = () -> {
                log.debug("Executing cron job: {}", jobId);
                try {
                    task.run();
                } catch (Exception e) {
                    log.error("Error executing job {}: {}", jobId, e.getMessage(), e);
                }
                // Replanifier la prochaine exécution
                scheduleNextCronExecution(jobId, cron, task);
            };
    
            scheduleNextCronExecution(jobId, cron, task);
    
            jobInfo.put(jobId, new ScheduledJob(jobId, "cron", cronExpression, null, Instant.now()));
            log.info("Scheduled cron job: {} with expression: {}", jobId, cronExpression);
        }
    
        private void scheduleNextCronExecution(String jobId, CronExpression cron, Runnable task) {
            ZonedDateTime now = ZonedDateTime.now(timezone);
            ZonedDateTime next = cron.next(now);
    
            if (next != null) {
                long delayMs = Duration.between(now, next).toMillis();
    
                ScheduledFuture<?> future = executor.schedule(() -> {
                    task.run();
                    scheduleNextCronExecution(jobId, cron, task);
                }, delayMs, TimeUnit.MILLISECONDS);
    
                jobs.put(jobId, future);
            }
        }
    
        @Override
        public void scheduleInterval(String jobId, long intervalMs, Runnable task) {
            scheduleInterval(jobId, 0, intervalMs, task);
        }
    
        @Override
        public void scheduleInterval(String jobId, long initialDelayMs, long intervalMs, Runnable task) {
            Runnable wrappedTask = () -> {
                log.debug("Executing interval job: {}", jobId);
                try {
                    task.run();
                } catch (Exception e) {
                    log.error("Error executing job {}: {}", jobId, e.getMessage(), e);
                }
            };
    
            ScheduledFuture<?> future = executor.scheduleAtFixedRate(
                wrappedTask, initialDelayMs, intervalMs, TimeUnit.MILLISECONDS);
    
            jobs.put(jobId, future);
            jobInfo.put(jobId, new ScheduledJob(jobId, "interval", null, intervalMs, Instant.now()));
    
            log.info("Scheduled interval job: {} every {}ms", jobId, intervalMs);
        }
    
        @Override
        public void scheduleOnce(String jobId, long delayMs, Runnable task) {
            ScheduledFuture<?> future = executor.schedule(() -> {
                log.debug("Executing one-shot job: {}", jobId);
                try {
                    task.run();
                } finally {
                    jobs.remove(jobId);
                    jobInfo.remove(jobId);
                }
            }, delayMs, TimeUnit.MILLISECONDS);
    
            jobs.put(jobId, future);
            jobInfo.put(jobId, new ScheduledJob(jobId, "once", null, delayMs, Instant.now()));
    
            log.info("Scheduled one-shot job: {} in {}ms", jobId, delayMs);
        }
    
        @Override
        public void cancel(String jobId) {
            ScheduledFuture<?> future = jobs.remove(jobId);
            if (future != null) {
                future.cancel(false);
                jobInfo.remove(jobId);
                log.info("Cancelled job: {}", jobId);
            }
        }
    
        @Override
        public boolean isScheduled(String jobId) {
            return jobs.containsKey(jobId);
        }
    
        @Override
        public List<ScheduledJob> getScheduledJobs() {
            return new ArrayList<>(jobInfo.values());
        }
    
        @Override
        public void start() {
            log.info("Scheduler started");
        }
    
        @Override
        public void stop() {
            log.info("Stopping scheduler...");
            jobs.values().forEach(f -> f.cancel(false));
            jobs.clear();
            executor.shutdown();
            try {
                if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
                Thread.currentThread().interrupt();
            }
            log.info("Scheduler stopped");
        }
    }
    

    7. Gestion du chevauchement

    7.1 Éviter le chevauchement

    @Component
    public class LongRunningWorker implements Worker {
    
        private final AtomicBoolean running = new AtomicBoolean(false);
    
        @Override
        public void doWork() {
            // Éviter l'exécution concurrente
            if (!running.compareAndSet(false, true)) {
                log.warn("Previous execution still running, skipping");
                return;
            }
    
            try {
                doLongWork();
            } finally {
                running.set(false);
            }
        }
    
        private void doLongWork() {
            // Traitement long
        }
    }
    

    7.2 Avec verrou distribué (multi-instances)

    @Component
    public class DistributedScheduledWorker implements Worker {
    
        @Autowired
        private KvBus kvBus;
    
        @Override
        public void doWork() {
            String lockKey = "lock:job:" + getName();
    
            // Tenter d'acquérir le lock
            if (!kvBus.putIfAbsent(lockKey, "locked", Duration.ofMinutes(10))) {
                log.debug("Job already running on another instance");
                return;
            }
    
            try {
                executeJob();
            } finally {
                kvBus.delete(lockKey);
            }
        }
    }
    

    8. Intégration MOP

    Le MOP intègre automatiquement les workers schedulés :

    // Dans MainOrchestratorProcess
    private void startScheduledWorkers() {
        for (Worker worker : workers) {
            if (worker.isScheduled() && worker.getSchedule() != null) {
                scheduler.scheduleCron(
                    "worker:" + worker.getName(),
                    worker.getSchedule(),
                    () -> {
                        if (worker.isHealthy()) {
                            worker.doWork();
                        }
                    }
                );
            }
        }
    }
    

    9. API Admin

    @RestController
    @RequestMapping("/admin/scheduler")
    public class SchedulerController {
    
        @Autowired
        private Scheduler scheduler;
    
        @GetMapping("/jobs")
        public List<ScheduledJob> listJobs() {
            return scheduler.getScheduledJobs();
        }
    
        @PostMapping("/jobs/{jobId}/cancel")
        public ResponseEntity<Void> cancelJob(@PathVariable String jobId) {
            if (scheduler.isScheduled(jobId)) {
                scheduler.cancel(jobId);
                return ResponseEntity.ok().build();
            }
            return ResponseEntity.notFound().build();
        }
    
        @PostMapping("/jobs/{jobId}/trigger")
        public ResponseEntity<Void> triggerJob(@PathVariable String jobId) {
            // Exécution immédiate one-shot
            scheduler.scheduleOnce(jobId + "-manual-" + System.currentTimeMillis(), 0, () -> {
                // Trouver et exécuter le worker correspondant
            });
            return ResponseEntity.accepted().build();
        }
    }
    

    10. Expressions Cron communes

    Expression Description
    0 0 * * * ? Toutes les heures
    0 */15 * * * ? Toutes les 15 minutes
    0 0 6 * * ? Tous les jours à 6h
    0 0 0 * * ? Tous les jours à minuit
    0 0 0 * * SUN Tous les dimanches à minuit
    0 0 0 1 * ? Premier jour du mois
    0 0 8 ? * MON-FRI 8h en semaine
    0 0 */2 * * ? Toutes les 2 heures

    11. Bonnes pratiques

    DO

    • Utiliser des noms de jobs uniques et descriptifs
    • Gérer le chevauchement pour les jobs longs
    • Utiliser des locks distribués en multi-instances
    • Logger le début et la fin des jobs
    • Monitorer l’exécution des jobs

    DON’T

    • Ne pas planifier des jobs trop fréquents sans nécessité
    • Ne pas ignorer les erreurs dans les jobs
    • Ne pas créer trop de threads
    • Ne pas bloquer indéfiniment dans un job

    12. Références

  • Socle V004 – Migration V3 vers V4

    Socle V004 – Migration V3 vers V4

    25 – Guide de Migration V3 → V4

    Version : 4.0.0 Date : 2025-12-09

    1. Résumé des changements

    1.1 Ce qui change

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

    1.2 Ce qui ne change PAS

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

    2. Checklist de migration

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

    3. Phase 1 : Préparation

    3.1 Backup

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

    3.2 Version cible

    <version>4.0.0</version>
    

    4. Phase 2 : Dépendances Maven

    4.1 Modifications pom.xml

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

    4.2 Vérifier les exclusions

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

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

    5. Phase 3 : Configuration

    5.1 Créer log4j2.xml

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

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

    5.2 Créer log4j2.component.properties

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

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

    5.3 Supprimer logback-spring.xml

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

    5.4 Mettre à jour application.yml

    Ajouter les nouvelles configurations V4 :

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

    6. Phase 4 : Code (optionnel)

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

    6.1 Si vous voulez utiliser TechDB

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

    6.2 Si vous voulez utiliser AuthClient

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

    6.3 Si vous voulez utiliser WorkerRegistry

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

    7. Phase 5 : Tests

    7.1 Compilation

    mvn clean compile
    

    Erreurs possibles :

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

    7.2 Tests unitaires

    mvn test
    

    7.3 Vérification logs

    mvn spring-boot:run
    

    Vérifier :

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

    7.4 H2 Console

    Si H2_CONSOLE_ENABLED=true :

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

    8. Phase 6 : Déploiement

    8.1 Variables d’environnement (minimum)

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

    8.2 Docker

    Mettre à jour le Dockerfile si nécessaire :

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

    8.3 Kubernetes

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

    9. Mapping des configurations

    9.1 Logback → Log4j2

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

    9.2 Pattern identique

    Le pattern de log reste identique :

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

    10. Rollback

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

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

    11. FAQ

    Q: Dois-je modifier mon code de logging ?

    Non. Le code reste identique :

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

    Q: Les MDC fonctionnent-ils ?

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

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

    Oui. Toutes les fonctionnalités V4 sont optionnelles :

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

    Q: Performance : V4 est-il plus rapide ?

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

    12. Références

  • 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 – 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 – Worker Registry

    Socle V004 – Worker Registry

    24 – Client Worker Registry (Nouveauté V4)

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Le WorkerRegistryClient permet aux applications Socle V4 de s’auto-enregistrer auprès d’un Registry central pour la supervision.

    Bénéfices

    • Visibilité : Savoir quels workers sont actifs par région
    • Supervision : Détecter les workers « LOST » (heartbeat manquant)
    • Diagnostique : Informations version, capabilities, charge

    2. Architecture

    ┌─────────────────┐                    ┌─────────────────┐
    │   Application   │                    │  Registry       │
    │   Socle V4      │                    │  (central)      │
    └────────┬────────┘                    └────────┬────────┘
             │                                      │
             │  1. POST /workers/register           │
             │     (au démarrage)                   │
             │─────────────────────────────────────►│
             │                                      │
             │  2. POST /workers/heartbeat          │
             │     (toutes les 30s)                 │
             │─────────────────────────────────────►│
             │                                      │
             │  3. DELETE /workers/{id}             │
             │     (à l'arrêt)                      │
             │─────────────────────────────────────►│
             │                                      │
    
                        ┌─────────────────┐
                        │  Metabase /     │
                        │  Grafana        │◄────── Consultation
                        └─────────────────┘
    

    3. Configuration

    3.1 application.yml

    socle:
      worker-registry:
        enabled: ${WORKER_REGISTRY_ENABLED:false}
        server-url: ${WORKER_REGISTRY_URL:https://registry.lmvi.org}
        heartbeat-interval-ms: ${REGISTRY_HEARTBEAT_INTERVAL_MS:30000}
        connect-timeout-ms: 10000
        read-timeout-ms: 30000
    

    3.2 Variables d’environnement

    Variable Description Défaut
    WORKER_REGISTRY_ENABLED Activer le registry false
    WORKER_REGISTRY_URL URL du Registry
    REGISTRY_HEARTBEAT_INTERVAL_MS Intervalle heartbeat (ms) 30000

    4. Interface WorkerRegistryClient

    package eu.lmvi.socle.client.registry;
    
    /**
     * Client Registry pour auto-enregistrement des Workers
     */
    public interface WorkerRegistryClient {
    
        /**
         * Enregistrement initial au démarrage
         * @param registration Informations du worker
         * @throws RegistryException si échec
         */
        void register(WorkerRegistration registration) throws RegistryException;
    
        /**
         * Heartbeat périodique
         * @param heartbeat État courant du worker
         * @throws RegistryException si échec
         */
        void heartbeat(WorkerHeartbeat heartbeat) throws RegistryException;
    
        /**
         * Désenregistrement à l'arrêt
         * @param workerId ID du worker
         * @throws RegistryException si échec
         */
        void unregister(String workerId) throws RegistryException;
    
        /**
         * Vérifie si le worker est enregistré
         */
        boolean isRegistered();
    }
    

    5. DTOs

    5.1 WorkerRegistration

    package eu.lmvi.socle.client.registry;
    
    /**
     * Informations d'enregistrement d'un worker
     */
    public record WorkerRegistration(
        String workerId,          // Identifiant unique (ex: "AGENT-DB2-MTQ-001")
        String workerType,        // Type de worker (ex: "journal-reader")
        String region,            // Région (ex: "MTQ", "GUA", "REU")
        String host,              // Hostname
        String version,           // Version de l'application
        List<String> capabilities,// Capacités (ex: ["db2-cdc", "nats-publisher"])
        Map<String, Object> extra // Métadonnées additionnelles
    ) {
        public static WorkerRegistration of(SocleConfiguration config) {
            return new WorkerRegistration(
                config.getExec_id(),
                config.getApp_name(),
                config.getRegion(),
                getHostname(),
                config.getVersion(),
                List.of(),
                Map.of()
            );
        }
    
        private static String getHostname() {
            try {
                return InetAddress.getLocalHost().getHostName();
            } catch (UnknownHostException e) {
                return "unknown";
            }
        }
    }
    

    5.2 WorkerHeartbeat

    package eu.lmvi.socle.client.registry;
    
    /**
     * Heartbeat périodique d'un worker
     */
    public record WorkerHeartbeat(
        String workerId,           // ID du worker
        String status,             // RUNNING, STOPPING, ERROR
        Map<String, Object> load   // Métriques de charge
    ) {
        public static WorkerHeartbeat running(String workerId, Map<String, Object> load) {
            return new WorkerHeartbeat(workerId, "RUNNING", load);
        }
    
        public static WorkerHeartbeat stopping(String workerId) {
            return new WorkerHeartbeat(workerId, "STOPPING", Map.of());
        }
    
        public static WorkerHeartbeat error(String workerId, String errorMessage) {
            return new WorkerHeartbeat(workerId, "ERROR", Map.of("error", errorMessage));
        }
    }
    

    6. Implémentation

    package eu.lmvi.socle.client.registry;
    
    @Component
    @ConditionalOnProperty(name = "socle.worker-registry.enabled", havingValue = "true")
    public class HttpWorkerRegistryClient implements WorkerRegistryClient {
    
        private static final Logger log = LoggerFactory.getLogger(HttpWorkerRegistryClient.class);
    
        private final SocleConfiguration config;
        private final SocleAuthClient authClient;
        private final OkHttpClient httpClient;
        private final ObjectMapper objectMapper;
    
        private volatile boolean registered = false;
        private volatile String currentWorkerId;
    
        public HttpWorkerRegistryClient(
                SocleConfiguration config,
                @Autowired(required = false) SocleAuthClient authClient) {
            this.config = config;
            this.authClient = authClient;
            this.objectMapper = new ObjectMapper();
    
            this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(config.getRegistryConnectTimeoutMs(), TimeUnit.MILLISECONDS)
                .readTimeout(config.getRegistryReadTimeoutMs(), TimeUnit.MILLISECONDS)
                .build();
        }
    
        @Override
        public void register(WorkerRegistration registration) throws RegistryException {
            log.info("Registering worker: {} ({})", registration.workerId(), registration.workerType());
    
            try {
                String json = objectMapper.writeValueAsString(registration);
    
                Request.Builder requestBuilder = new Request.Builder()
                    .url(config.getRegistryServerUrl() + "/api/v1/workers/register")
                    .post(RequestBody.create(json, MediaType.parse("application/json")));
    
                // Add auth if available
                if (authClient != null && authClient.isAuthenticated()) {
                    requestBuilder.header("Authorization", "Bearer " + authClient.getValidAccessToken());
                }
    
                try (Response response = httpClient.newCall(requestBuilder.build()).execute()) {
                    if (!response.isSuccessful()) {
                        throw new RegistryException("Registration failed: " + response.code());
                    }
    
                    registered = true;
                    currentWorkerId = registration.workerId();
                    log.info("Worker registered successfully: {}", registration.workerId());
                }
            } catch (IOException e) {
                throw new RegistryException("Registration failed", e);
            }
        }
    
        @Override
        public void heartbeat(WorkerHeartbeat heartbeat) throws RegistryException {
            if (!registered) {
                log.warn("Cannot send heartbeat, worker not registered");
                return;
            }
    
            log.debug("Sending heartbeat: {} - {}", heartbeat.workerId(), heartbeat.status());
    
            try {
                String json = objectMapper.writeValueAsString(heartbeat);
    
                Request.Builder requestBuilder = new Request.Builder()
                    .url(config.getRegistryServerUrl() + "/api/v1/workers/heartbeat")
                    .post(RequestBody.create(json, MediaType.parse("application/json")));
    
                if (authClient != null && authClient.isAuthenticated()) {
                    requestBuilder.header("Authorization", "Bearer " + authClient.getValidAccessToken());
                }
    
                try (Response response = httpClient.newCall(requestBuilder.build()).execute()) {
                    if (!response.isSuccessful()) {
                        log.warn("Heartbeat failed: {}", response.code());
                        // Don't throw - heartbeat failure is not critical
                    }
                }
            } catch (IOException e) {
                log.warn("Heartbeat failed: {}", e.getMessage());
                // Don't throw - heartbeat failure is not critical
            }
        }
    
        @Override
        public void unregister(String workerId) throws RegistryException {
            if (!registered) {
                return;
            }
    
            log.info("Unregistering worker: {}", workerId);
    
            try {
                Request.Builder requestBuilder = new Request.Builder()
                    .url(config.getRegistryServerUrl() + "/api/v1/workers/" + workerId)
                    .delete();
    
                if (authClient != null && authClient.isAuthenticated()) {
                    requestBuilder.header("Authorization", "Bearer " + authClient.getValidAccessToken());
                }
    
                try (Response response = httpClient.newCall(requestBuilder.build()).execute()) {
                    // Ignore response - best effort
                    registered = false;
                    currentWorkerId = null;
                    log.info("Worker unregistered: {}", workerId);
                }
            } catch (IOException e) {
                log.warn("Unregister failed: {}", e.getMessage());
                // Don't throw - unregister failure is not critical
            }
        }
    
        @Override
        public boolean isRegistered() {
            return registered;
        }
    }
    

    7. Intégration avec MOP

    7.1 Enregistrement au démarrage

    // Dans MainOrchestratorProcess.start()
    if (registryClient != null && config.isWorkerRegistryEnabled()) {
        log.info("[step:registry_register] Enregistrement au Worker Registry");
        try {
            WorkerRegistration registration = new WorkerRegistration(
                config.getExec_id(),
                config.getApp_name(),
                config.getRegion(),
                InetAddress.getLocalHost().getHostName(),
                config.getVersion(),
                getWorkerCapabilities(),
                Map.of(
                    "startTime", Instant.now(),
                    "javaVersion", System.getProperty("java.version")
                )
            );
            registryClient.register(registration);
        } catch (RegistryException e) {
            log.warn("Registry registration failed, continuing", e);
        }
    }
    

    7.2 Heartbeat périodique

    // Dans la boucle principale ou via ScheduledExecutorService
    private void sendRegistryHeartbeat() {
        if (registryClient == null || !registryClient.isRegistered()) {
            return;
        }
    
        try {
            WorkerHeartbeat heartbeat = new WorkerHeartbeat(
                config.getExec_id(),
                "RUNNING",
                Map.of(
                    "uptime", getUptime(),
                    "workersCount", workers.size(),
                    "healthyWorkers", countHealthyWorkers(),
                    "memoryUsedMb", getMemoryUsedMb()
                )
            );
            registryClient.heartbeat(heartbeat);
        } catch (RegistryException e) {
            log.debug("Heartbeat failed: {}", e.getMessage());
        }
    }
    

    7.3 Désenregistrement à l’arrêt

    // Dans MainOrchestratorProcess.gracefulShutdown()
    if (registryClient != null && registryClient.isRegistered()) {
        log.info("[step:registry_unregister] Désenregistrement du Registry");
        try {
            registryClient.unregister(config.getExec_id());
        } catch (RegistryException e) {
            log.warn("Unregister failed", e);
        }
    }
    

    8. Exemple de données

    8.1 Registration

    {
      "workerId": "AGENT-DB2-MTQ-001",
      "workerType": "journal-reader",
      "region": "MTQ",
      "host": "mtq-nuc-01",
      "version": "4.0.0",
      "capabilities": ["db2-cdc", "nats-publisher"],
      "extra": {
        "journal": "QUSR0023",
        "library": "IMA001FDMQ",
        "startTime": "2025-12-09T10:00:00Z",
        "javaVersion": "21.0.1"
      }
    }
    

    8.2 Heartbeat

    {
      "workerId": "AGENT-DB2-MTQ-001",
      "status": "RUNNING",
      "load": {
        "uptime": 3600,
        "messagesPerMinute": 523,
        "lastSequence": 123456789,
        "memoryUsedMb": 256,
        "cpuPercent": 15
      }
    }
    

    9. Côté serveur (Registry central)

    9.1 Table worker_registry

    CREATE TABLE worker_registry (
        id BIGSERIAL PRIMARY KEY,
        worker_id VARCHAR(200) NOT NULL UNIQUE,
        worker_type VARCHAR(100) NOT NULL,
        region VARCHAR(50),
        host VARCHAR(200),
        version VARCHAR(50),
        status VARCHAR(20) DEFAULT 'UNKNOWN',
        registered_at TIMESTAMPTZ DEFAULT NOW(),
        last_heartbeat TIMESTAMPTZ,
        capabilities JSONB,
        extra JSONB,
        load JSONB
    );
    
    CREATE INDEX idx_worker_registry_region ON worker_registry(region);
    CREATE INDEX idx_worker_registry_type ON worker_registry(worker_type);
    CREATE INDEX idx_worker_registry_status ON worker_registry(status);
    

    9.2 Détection des workers LOST

    -- Workers sans heartbeat depuis plus de 2 minutes
    UPDATE worker_registry
    SET status = 'LOST'
    WHERE status = 'RUNNING'
      AND last_heartbeat < NOW() - INTERVAL '2 minutes';
    

    9.3 Dashboard Metabase/Grafana

    -- Workers actifs par région
    SELECT region, COUNT(*) as count
    FROM worker_registry
    WHERE status = 'RUNNING'
    GROUP BY region;
    
    -- Workers LOST
    SELECT worker_id, region, last_heartbeat
    FROM worker_registry
    WHERE status = 'LOST';
    

    10. Bonnes pratiques

    DO

    • ✅ Enregistrer au démarrage, désenregistrer à l’arrêt
    • ✅ Heartbeat régulier (30s recommandé)
    • ✅ Inclure des métriques utiles dans le heartbeat
    • ✅ Gérer gracieusement les échecs (non bloquant)

    DON’T

    • ❌ Heartbeat trop fréquent (< 10s)
    • ❌ Bloquer sur les erreurs registry
    • ❌ Stocker des données sensibles dans extra

    11. Troubleshooting

    Worker non visible dans le dashboard

    1. Vérifier WORKER_REGISTRY_ENABLED=true
    2. Vérifier WORKER_REGISTRY_URL
    3. Vérifier les logs : « Worker registered successfully »

    Worker marqué LOST

    1. Vérifier que l’application tourne
    2. Vérifier la connectivité réseau
    3. Vérifier les logs heartbeat

    Erreur 401

    1. Vérifier que AUTH_ENABLED=true
    2. Vérifier API_KEY
    3. Vérifier que l’auth fonctionne

    12. Références

  • Socle V004 – Standard TechDB

    Socle V004 – Standard TechDB

    28 – Standard de Creation de Table H2

    Version : 4.0.1 Date : 2026-01-13 Package : eu.lmvi.socle.techdb

    Introduction

    Ce document definit le standard de creation des tables dans la base technique H2 du Socle V004. Ce standard garantit la coherence, la tracabilite et la compatibilite PostgreSQL/H2.

    Structure de Base

    Toutes les tables TechDB doivent suivre cette structure:

    Champ Type Description
    x_id BIGINT GENERATED BY DEFAULT AS IDENTITY Identifiant technique auto-genere
    x_dateCreated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP Date creation
    x_dateChanged TIMESTAMP WITH TIME ZONE Date modification (NULL a l’insert, MAJ par appli)
    x_sub VARCHAR(255) Sujet/categorie
    x_partition VARCHAR(30) Partition logique
    x_comment CLOB Commentaires/historiques JSON texte
    [champs metier] Champs specifiques a la table
    datas CLOB Donnees metier JSON texte (toujours en fin)

    Regles

    Identite

    • Utiliser GENERATED BY DEFAULT AS IDENTITY, pas AUTO_INCREMENT (MySQL)
    • Pas de sequence explicite
    • La colonne x_id est toujours la cle primaire
    -- Correct (SQL standard / H2 / PostgreSQL)
    x_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
    
    -- Incorrect (MySQL uniquement)
    id BIGINT AUTO_INCREMENT PRIMARY KEY
    

    Timestamps

    • x_dateCreated: Toujours NOT NULL DEFAULT CURRENT_TIMESTAMP
    • x_dateChanged: NULL a l’insertion, mis a jour par l’application
    • Utiliser TIMESTAMP WITH TIME ZONE pour la compatibilite
    x_dateCreated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    x_dateChanged TIMESTAMP WITH TIME ZONE,
    

    Triggers

    • Aucun trigger dans H2
    • L’audit, l’historisation et la mise a jour de x_dateChanged sont geres par l’application

    Cle Primaire

    • Si une cle existante est presente (ex: worker_name), la conserver comme UNIQUE
    • x_id reste la PK technique
    x_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    worker_name VARCHAR(255) NOT NULL UNIQUE,
    

    Conventions de Nommage

    Element Convention Exemple
    Champs techniques Prefixe x_ x_id, x_dateCreated
    Cles etrangeres id_<table_cible> id_user, id_order
    Champ id existant Renommer en x_id
    Donnees JSON datas en derniere position

    Permissions

    • Droits geres au niveau utilisateur H2
    • Proprietaire = utilisateur createur (socle par defaut)

    Contraintes H2 vs PostgreSQL

    PostgreSQL H2
    JSONB CLOB
    Triggers natifs Logique applicative
    Validation JSON DB Validation applicative
    SERIAL GENERATED BY DEFAULT AS IDENTITY
    Index GIN sur JSON Non supporte

    Exemple DDL Complet

    CREATE TABLE IF NOT EXISTS techdb_example (
        -- Champs techniques (standard)
        x_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
        x_dateCreated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
        x_dateChanged TIMESTAMP WITH TIME ZONE,
        x_sub VARCHAR(255),
        x_partition VARCHAR(30),
        x_comment CLOB,
    
        -- Champs metier specifiques
        example_key VARCHAR(255) NOT NULL UNIQUE,
        status VARCHAR(50) NOT NULL,
        counter INT DEFAULT 0,
        last_activity TIMESTAMP WITH TIME ZONE,
    
        -- Donnees JSON (toujours en dernier)
        datas CLOB
    );
    
    -- Index recommandes
    CREATE INDEX IF NOT EXISTS idx_example_key ON techdb_example(example_key);
    CREATE INDEX IF NOT EXISTS idx_example_status ON techdb_example(status);
    CREATE INDEX IF NOT EXISTS idx_example_created ON techdb_example(x_dateCreated);
    

    Tables TechDB du Socle

    Le Socle V004 definit 5 tables techniques:

    techdb_offsets

    Stockage des offsets de consommation (Kafka, NATS, etc.)

    CREATE TABLE IF NOT EXISTS techdb_offsets (
        x_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
        x_dateCreated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
        x_dateChanged TIMESTAMP WITH TIME ZONE,
        x_sub VARCHAR(255),
        x_partition VARCHAR(30),
        x_comment CLOB,
        offset_key VARCHAR(255) NOT NULL UNIQUE,
        topic VARCHAR(255) NOT NULL,
        partition_id INT DEFAULT 0,
        offset_value BIGINT NOT NULL,
        consumer_group VARCHAR(255),
        datas CLOB
    );
    

    techdb_worker_state

    Etat persistant des Workers

    CREATE TABLE IF NOT EXISTS techdb_worker_state (
        x_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
        x_dateCreated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
        x_dateChanged TIMESTAMP WITH TIME ZONE,
        x_sub VARCHAR(255),
        x_partition VARCHAR(30),
        x_comment CLOB,
        worker_name VARCHAR(255) NOT NULL UNIQUE,
        state VARCHAR(50) NOT NULL,
        last_run_at TIMESTAMP WITH TIME ZONE,
        next_run_at TIMESTAMP WITH TIME ZONE,
        error_count INT DEFAULT 0,
        last_error CLOB,
        datas CLOB
    );
    

    techdb_events

    Evenements techniques

    CREATE TABLE IF NOT EXISTS techdb_events (
        x_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
        x_dateCreated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
        x_dateChanged TIMESTAMP WITH TIME ZONE,
        x_sub VARCHAR(255),
        x_partition VARCHAR(30),
        x_comment CLOB,
        event_type VARCHAR(100) NOT NULL,
        source VARCHAR(255) NOT NULL,
        processed BOOLEAN DEFAULT FALSE,
        processed_at TIMESTAMP WITH TIME ZONE,
        datas CLOB
    );
    

    techdb_log_buffer

    Buffer de logs pour LogForwarder

    CREATE TABLE IF NOT EXISTS techdb_log_buffer (
        x_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
        x_dateCreated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
        x_dateChanged TIMESTAMP WITH TIME ZONE,
        x_sub VARCHAR(255),
        x_partition VARCHAR(30),
        x_comment CLOB,
        log_level VARCHAR(20) NOT NULL,
        logger_name VARCHAR(255),
        message CLOB NOT NULL,
        thread_name VARCHAR(255),
        log_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
        forwarded BOOLEAN DEFAULT FALSE,
        forwarded_at TIMESTAMP WITH TIME ZONE,
        datas CLOB
    );
    

    techdb_kv

    Stockage cle-valeur generique

    CREATE TABLE IF NOT EXISTS techdb_kv (
        x_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
        x_dateCreated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
        x_dateChanged TIMESTAMP WITH TIME ZONE,
        x_sub VARCHAR(255),
        x_partition VARCHAR(30),
        x_comment CLOB,
        kv_key VARCHAR(512) NOT NULL UNIQUE,
        value_type VARCHAR(50) DEFAULT 'string',
        expires_at TIMESTAMP WITH TIME ZONE,
        datas CLOB
    );
    

    Bonnes Pratiques

    DO

    • Toujours utiliser le format standard x_ pour les champs techniques
    • Mettre datas en derniere colonne
    • Creer des index sur les colonnes frequemment requetees
    • Utiliser TIMESTAMP WITH TIME ZONE pour tous les timestamps
    • Documenter le contenu JSON attendu dans datas

    DON’T

    • Ne pas utiliser AUTO_INCREMENT (syntaxe MySQL)
    • Ne pas creer de triggers (gestion applicative)
    • Ne pas stocker de BLOBs volumineux (utiliser stockage externe)
    • Ne pas utiliser JSONB (non supporte par H2)
    • Ne pas omettre les index sur les colonnes de recherche

    Migration depuis l’ancien format

    Si vous avez des tables existantes avec l’ancien format:

    -- 1. Sauvegarder les donnees
    CREATE TABLE techdb_events_backup AS SELECT * FROM techdb_events;
    
    -- 2. Supprimer l'ancienne table
    DROP TABLE techdb_events;
    
    -- 3. Recreer avec le nouveau format
    -- (voir DDL ci-dessus)
    
    -- 4. Migrer les donnees
    INSERT INTO techdb_events (event_type, source, processed, processed_at, datas)
    SELECT event_type, source, processed, processed_at, payload
    FROM techdb_events_backup;
    
    -- 5. Supprimer la sauvegarde
    DROP TABLE techdb_events_backup;
    

    Voir aussi

    H2 = dev/tests/outillage – Structure compatible PostgreSQL/H2

    Socle V004 – Standard TechDB

  • Socle V004 – Configuration

    Socle V004 – Configuration

    04 – Configuration

    Version : 4.0.0 Date : 2025-01-25

    1. Introduction

    Le Socle V4 utilise une configuration centralisée via SocleConfiguration qui charge les paramètres depuis application.yml et les variables d’environnement.

    Priorité de configuration

    1. Variables d'environnement (priorité maximale)
    2. application.yml
    3. Valeurs par défaut dans le code
    

    2. Fichier application.yml

    2.1 Configuration minimale

    socle:
      app_name: ${APP_NAME:my-app}
      env_name: ${ENV_NAME:DEV}
      exec_id: ${EXEC_ID:${socle.app_name}-${random.uuid}}
      region: ${REGION:local}
      version: ${APP_VERSION:4.0.0}
    
    spring:
      application:
        name: ${socle.app_name}
    
    server:
      port: ${HTTP_PORT:8080}
    
    logging:
      config: classpath:log4j2.xml
    

    2.2 Configuration complète

    socle:
      # === Identification ===
      app_name: ${APP_NAME:socle-v4}
      env_name: ${ENV_NAME:DEV}
      exec_id: ${EXEC_ID:${socle.app_name}-${random.uuid}}
      region: ${REGION:local}
      version: ${APP_VERSION:4.0.0}
    
      # === HTTP Server ===
      http:
        enabled: ${HTTP_ENABLED:true}
        port: ${HTTP_PORT:8080}
        context-path: ${CONTEXT_PATH:/}
    
      # === KvBus ===
      kvbus:
        mode: ${KVBUS_MODE:in_memory}
        redis:
          host: ${REDIS_HOST:localhost}
          port: ${REDIS_PORT:6379}
          password: ${REDIS_PASSWORD:}
          database: ${REDIS_DATABASE:0}
          prefix: ${REDIS_PREFIX:socle}
    
      # === Supervisor ===
      supervisor:
        heartbeat-interval-ms: ${SUPERVISOR_HEARTBEAT_MS:10000}
        unhealthy-threshold: ${SUPERVISOR_UNHEALTHY_THRESHOLD:3}
        check-interval-ms: ${SUPERVISOR_CHECK_INTERVAL_MS:5000}
        stale-timeout-ms: ${SUPERVISOR_STALE_TIMEOUT_MS:60000}
    
      # === StatusDashboard (V4) ===
      status_dashboard:
        enabled: ${STATUS_DASHBOARD_ENABLED:true}
        port: ${STATUS_DASHBOARD_PORT:9374}
        refresh_interval: ${STATUS_DASHBOARD_REFRESH:5}
    
      # === Scheduler ===
      scheduler:
        enabled: ${SCHEDULER_ENABLED:true}
        thread-pool-size: ${SCHEDULER_POOL_SIZE:4}
    
      # === Admin API ===
      admin:
        enabled: ${ADMIN_ENABLED:true}
        auth:
          enabled: ${ADMIN_AUTH_ENABLED:false}
          username: ${ADMIN_USERNAME:admin}
          password: ${ADMIN_PASSWORD:admin}
    
      # === TechDB (V4) ===
      techdb:
        enabled: ${TECHDB_ENABLED:true}
        url: jdbc:h2:file:${TECHDB_PATH:./data/socle-techdb};MODE=PostgreSQL;DB_CLOSE_DELAY=-1
        username: socle
        password: ${TECHDB_PASSWORD:socle}
        console:
          enabled: ${H2_CONSOLE_ENABLED:false}
          path: /h2-console
    
      # === Logging (V4) ===
      logging:
        forwarder:
          enabled: ${LOG_FORWARDER_ENABLED:false}
          transport-mode: ${LOG_TRANSPORT_MODE:http}
          log-hub-url: ${LOG_HUB_URL:}
          nats-url: ${NATS_URL:}
          batch-size: ${LOG_BATCH_SIZE:100}
          flush-interval-ms: ${LOG_FLUSH_INTERVAL_MS:5000}
    
      # === Auth Client (V4) ===
      auth:
        enabled: ${AUTH_ENABLED:false}
        server-url: ${AUTH_SERVER_URL:}
        source-name: ${SOURCE_NAME:${socle.app_name}}
        api-key: ${API_KEY:}
    
      # === Worker Registry (V4) ===
      worker-registry:
        enabled: ${WORKER_REGISTRY_ENABLED:false}
        server-url: ${WORKER_REGISTRY_URL:}
        heartbeat-interval-ms: ${REGISTRY_HEARTBEAT_MS:30000}
    
    spring:
      application:
        name: ${socle.app_name}
    
    server:
      port: ${socle.http.port}
    
    logging:
      config: classpath:log4j2.xml
    

    3. Variables d’environnement

    3.1 Variables essentielles

    Variable Description Défaut Obligatoire
    APP_NAME Nom de l’application socle-v4 Recommandé
    ENV_NAME Environnement (DEV/STAGING/PROD) DEV Recommandé
    REGION Région géographique local Recommandé
    HTTP_PORT Port HTTP 8080 Non

    3.2 Variables KvBus

    Variable Description Défaut
    KVBUS_MODE Mode (in_memory/redis) in_memory
    REDIS_HOST Hôte Redis localhost
    REDIS_PORT Port Redis 6379
    REDIS_PASSWORD Mot de passe Redis
    REDIS_DATABASE Database Redis 0
    REDIS_PREFIX Préfixe des clés socle

    3.3 Variables Supervisor et Dashboard

    Variable Description Défaut
    SUPERVISOR_HEARTBEAT_MS Intervalle heartbeat attendu 10000 (10s)
    SUPERVISOR_UNHEALTHY_THRESHOLD Heartbeats manqués avant UNHEALTHY 3
    SUPERVISOR_CHECK_INTERVAL_MS Fréquence de vérification 5000 (5s)
    SUPERVISOR_STALE_TIMEOUT_MS Timeout avant STALE 60000 (1min)
    STATUS_DASHBOARD_ENABLED Activer le dashboard true
    STATUS_DASHBOARD_PORT Port du dashboard 9374
    STATUS_DASHBOARD_REFRESH Rafraîchissement (secondes) 5

    3.4 Variables V4

    Variable Description Défaut
    TECHDB_ENABLED Activer H2 TechDB true
    TECHDB_PATH Chemin fichier H2 ./data/socle-techdb
    H2_CONSOLE_ENABLED Console H2 web false
    LOG_FORWARDER_ENABLED Activer LogForwarder false
    LOG_TRANSPORT_MODE Mode transport logs http
    AUTH_ENABLED Activer auth JWT false
    WORKER_REGISTRY_ENABLED Activer registry false

    4. Classe SocleConfiguration

    package eu.lmvi.socle.config;
    
    @Configuration
    @ConfigurationProperties(prefix = "socle")
    public class SocleConfiguration {
    
        // === Identification ===
        private String app_name = "socle-v4";
        private String env_name = "DEV";
        private String exec_id;
        private String region = "local";
        private String version = "4.0.0";
    
        // === HTTP ===
        private HttpConfig http = new HttpConfig();
    
        // === KvBus ===
        private KvBusConfig kvbus = new KvBusConfig();
    
        // === Supervisor ===
        private SupervisorConfig supervisor = new SupervisorConfig();
    
        // === TechDB (V4) ===
        private TechDbConfig techdb = new TechDbConfig();
    
        // === Logging (V4) ===
        private LoggingConfig logging = new LoggingConfig();
    
        // === Auth (V4) ===
        private AuthConfig auth = new AuthConfig();
    
        // === Worker Registry (V4) ===
        private WorkerRegistryConfig workerRegistry = new WorkerRegistryConfig();
    
        // Getters / Setters...
    
        @PostConstruct
        public void init() {
            if (exec_id == null || exec_id.isEmpty()) {
                exec_id = app_name + "-" + UUID.randomUUID().toString().substring(0, 8);
            }
        }
    }
    

    4.1 Sous-configurations

    public static class HttpConfig {
        private boolean enabled = true;
        private int port = 8080;
        private String contextPath = "/";
    }
    
    public static class KvBusConfig {
        private String mode = "in_memory";
        private RedisConfig redis = new RedisConfig();
    }
    
    public static class TechDbConfig {
        private boolean enabled = true;
        private String url = "jdbc:h2:file:./data/socle-techdb";
        private String username = "socle";
        private String password = "socle";
        private ConsoleConfig console = new ConsoleConfig();
    }
    
    public static class LoggingConfig {
        private ForwarderConfig forwarder = new ForwarderConfig();
    }
    
    public static class AuthConfig {
        private boolean enabled = false;
        private String serverUrl;
        private String sourceName;
        private String apiKey;
    }
    
    public static class WorkerRegistryConfig {
        private boolean enabled = false;
        private String serverUrl;
        private long heartbeatIntervalMs = 30000;
    }
    

    5. Accès à la configuration

    5.1 Injection

    @Service
    public class MonService {
    
        @Autowired
        private SocleConfiguration config;
    
        public void doSomething() {
            String appName = config.getApp_name();
            String region = config.getRegion();
            boolean techDbEnabled = config.getTechdb().isEnabled();
        }
    }
    

    5.2 Dans un Worker

    public class MonWorker implements Worker {
    
        private final SocleConfiguration config;
    
        public MonWorker(SocleConfiguration config) {
            this.config = config;
        }
    
        @Override
        public void doWork() {
            log.info("Running in region: {}", config.getRegion());
        }
    }
    

    6. Profils Spring

    6.1 Activation

    # Via variable d'environnement
    export SPRING_PROFILES_ACTIVE=prod
    
    # Via ligne de commande
    java -jar app.jar --spring.profiles.active=prod
    

    6.2 Fichiers par profil

    src/main/resources/
    ├── application.yml           # Configuration de base
    ├── application-dev.yml       # Overrides DEV
    ├── application-staging.yml   # Overrides STAGING
    └── application-prod.yml      # Overrides PROD
    

    6.3 Exemple application-prod.yml

    socle:
      env_name: PROD
      techdb:
        console:
          enabled: false
      logging:
        forwarder:
          enabled: true
      auth:
        enabled: true
      worker-registry:
        enabled: true
    
    logging:
      config: classpath:log4j2-prod.xml
    

    7. Configuration Docker

    7.1 Dockerfile

    FROM eclipse-temurin:21-jre
    
    WORKDIR /app
    
    COPY target/socle-v004-4.0.0.jar app.jar
    
    # Variables par défaut (peuvent être overridées)
    ENV APP_NAME=socle-v4
    ENV ENV_NAME=PROD
    ENV HTTP_PORT=8080
    ENV TECHDB_PATH=/app/data/techdb
    
    EXPOSE 8080
    
    ENTRYPOINT ["java", "-jar", "app.jar"]
    

    7.2 docker-compose.yml

    version: '3.8'
    
    services:
      socle-app:
        image: socle-v4:latest
        environment:
          - APP_NAME=my-service
          - ENV_NAME=PROD
          - REGION=MTQ
          - KVBUS_MODE=redis
          - REDIS_HOST=redis
          - LOG_FORWARDER_ENABLED=true
          - LOG_HUB_URL=http://loghub:8080/api/ingest-logs
        ports:
          - "8080:8080"
        volumes:
          - ./data:/app/data
        depends_on:
          - redis
    
      redis:
        image: redis:7-alpine
        ports:
          - "6379:6379"
    

    8. Configuration Kubernetes

    8.1 ConfigMap

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: socle-config
    data:
      APP_NAME: "my-service"
      ENV_NAME: "PROD"
      REGION: "MTQ"
      KVBUS_MODE: "redis"
      LOG_FORWARDER_ENABLED: "true"
    

    8.2 Secret

    apiVersion: v1
    kind: Secret
    metadata:
      name: socle-secrets
    type: Opaque
    stringData:
      REDIS_PASSWORD: "secret-password"
      API_KEY: "my-api-key"
      TECHDB_PASSWORD: "techdb-password"
    

    8.3 Deployment

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: socle-app
    spec:
      replicas: 2
      template:
        spec:
          containers:
            - name: socle
              image: socle-v4:latest
              envFrom:
                - configMapRef:
                    name: socle-config
                - secretRef:
                    name: socle-secrets
              ports:
                - containerPort: 8080
    

    9. Validation de configuration

    9.1 Validation au démarrage

    @Component
    public class ConfigurationValidator implements ApplicationListener<ApplicationReadyEvent> {
    
        @Autowired
        private SocleConfiguration config;
    
        @Override
        public void onApplicationEvent(ApplicationReadyEvent event) {
            validateRequired();
            validateConsistency();
        }
    
        private void validateRequired() {
            if (config.getApp_name() == null || config.getApp_name().isEmpty()) {
                throw new IllegalStateException("APP_NAME is required");
            }
        }
    
        private void validateConsistency() {
            if (config.getAuth().isEnabled() && config.getAuth().getApiKey() == null) {
                throw new IllegalStateException("API_KEY required when AUTH_ENABLED=true");
            }
        }
    }
    

    9.2 Endpoint de configuration

    GET /admin/config
    

    Retourne la configuration actuelle (sans les secrets).

    10. Bonnes pratiques

    DO

    • Utiliser les variables d’environnement pour les valeurs spécifiques à l’environnement
    • Définir des valeurs par défaut sensées
    • Utiliser des profils Spring pour les environnements
    • Valider la configuration au démarrage
    • Ne jamais committer de secrets

    DON’T

    • Ne pas hardcoder de valeurs dans le code
    • Ne pas mettre de mots de passe dans application.yml
    • Ne pas utiliser de valeurs par défaut dangereuses en prod

    11. Références