Étiquette : Coding

  • Socle V004 – Workers

    Socle V004 – Workers

    05 – Workers

    Version : 4.0.1 Date : 2026-01-13

    1. Introduction

    Les Workers sont les composants de traitement du Socle V4. Chaque Worker implémente une tâche spécifique et son cycle de vie est géré par le MOP (Main Orchestrator Process).

    Caractéristiques

    • Interface unique : Tous les workers implémentent Worker
    • Cycle de vie géré : Le MOP orchestre start/stop/doWork
    • Priorités : Ordre de démarrage/arrêt configurable
    • Scheduling : Support cron et intervalle
    • Health check : Supervision intégrée

    2. Interface Worker

    package eu.lmvi.socle.worker;
    
    public interface Worker {
    
        /**
         * Nom unique du worker
         */
        String getName();
    
        /**
         * Initialisation (appelé une fois au démarrage)
         */
        void initialize();
    
        /**
         * Démarrage du worker
         */
        void start();
    
        /**
         * Traitement principal (appelé cycliquement)
         */
        void doWork();
    
        /**
         * Arrêt gracieux
         */
        void stop();
    
        /**
         * État de santé
         */
        boolean isHealthy();
    
        /**
         * Statistiques du worker
         */
        Map<String, Object> getStats();
    
        // === Priorités ===
    
        /**
         * Priorité au démarrage (plus petit = premier)
         */
        default int getStartPriority() {
            return 100;
        }
    
        /**
         * Priorité à l'arrêt (plus petit = premier)
         */
        default int getStopPriority() {
            return 100;
        }
    
        // === Scheduling ===
    
        /**
         * Expression cron (ou null si non schedulé)
         */
        default String getSchedule() {
            return null;
        }
    
        /**
         * Intervalle entre les cycles doWork() en ms
         * Valeurs recommandees : 5000 (5s), 10000 (10s), 30000 (30s)
         */
        default long getCycleIntervalMs() {
            return 5000; // 5 secondes par defaut
        }
    
        /**
         * Worker schedulé par cron ?
         */
        default boolean isScheduled() {
            return getSchedule() != null;
        }
    
        /**
         * Worker passif (ne fait rien dans doWork) ?
         */
        default boolean isPassive() {
            return false;
        }
    }
    

    3. Implémentation de base

    3.1 Worker simple

    package com.myapp.worker;
    
    import eu.lmvi.socle.worker.Worker;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    
    @Component
    public class SimpleWorker implements Worker {
    
        private static final Logger log = LoggerFactory.getLogger(SimpleWorker.class);
    
        private volatile boolean running = false;
        private long processedCount = 0;
    
        @Override
        public String getName() {
            return "simple-worker";
        }
    
        @Override
        public void initialize() {
            log.info("Initializing SimpleWorker");
            // Charger configuration, connexions, etc.
        }
    
        @Override
        public void start() {
            log.info("Starting SimpleWorker");
            running = true;
        }
    
        @Override
        public void doWork() {
            if (!running) return;
    
            try {
                // Traitement principal
                processedCount++;
                log.debug("Processing item #{}", processedCount);
            } catch (Exception e) {
                log.error("Error in doWork", e);
            }
        }
    
        @Override
        public void stop() {
            log.info("Stopping SimpleWorker");
            running = false;
        }
    
        @Override
        public boolean isHealthy() {
            return running;
        }
    
        @Override
        public Map<String, Object> getStats() {
            return Map.of(
                "running", running,
                "processedCount", processedCount
            );
        }
    }
    

    3.2 Worker avec priorité

    @Component
    public class DatabaseWorker implements Worker {
    
        @Override
        public String getName() {
            return "database-worker";
        }
    
        @Override
        public int getStartPriority() {
            return 10;  // Démarre en premier (connexion DB)
        }
    
        @Override
        public int getStopPriority() {
            return 90;  // S'arrête en dernier (flush des données)
        }
    
        // ... autres méthodes
    }
    

    3.3 Worker schedulé (cron)

    @Component
    public class ReportWorker implements Worker {
    
        @Override
        public String getName() {
            return "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...");
            // Génération du rapport
        }
    
        // ... autres méthodes
    }
    

    3.4 Worker passif (HTTP)

    @Component
    public class ApiWorker implements Worker {
    
        @Override
        public String getName() {
            return "api-worker";
        }
    
        @Override
        public boolean isPassive() {
            return true;  // Pas de doWork() cyclique
        }
    
        @Override
        public void doWork() {
            // Ne fait rien - le traitement est déclenché par HTTP
        }
    
        // Les requêtes HTTP déclenchent le traitement via endpoints REST
    }
    

    4. AbstractWorker

    Le Socle fournit une classe de base qui simplifie l’implémentation :

    package eu.lmvi.socle.worker;
    
    public abstract class AbstractWorker implements Worker {
    
        protected final Logger log = LoggerFactory.getLogger(getClass());
        protected volatile boolean running = false;
        protected volatile boolean healthy = true;
        protected final AtomicLong processedCount = new AtomicLong(0);
        protected final AtomicLong errorCount = new AtomicLong(0);
        protected Instant lastActivity;
    
        @Override
        public void initialize() {
            log.info("[{}] Initializing", getName());
            doInitialize();
        }
    
        @Override
        public void start() {
            log.info("[{}] Starting", getName());
            running = true;
            doStart();
        }
    
        @Override
        public void stop() {
            log.info("[{}] Stopping", getName());
            running = false;
            doStop();
        }
    
        @Override
        public void doWork() {
            if (!running) return;
    
            try {
                doProcess();
                lastActivity = Instant.now();
            } catch (Exception e) {
                errorCount.incrementAndGet();
                handleError(e);
            }
        }
    
        @Override
        public boolean isHealthy() {
            return running && healthy;
        }
    
        @Override
        public Map<String, Object> getStats() {
            return Map.of(
                "running", running,
                "healthy", healthy,
                "processedCount", processedCount.get(),
                "errorCount", errorCount.get(),
                "lastActivity", lastActivity != null ? lastActivity.toString() : "never"
            );
        }
    
        // === Méthodes à implémenter ===
    
        protected void doInitialize() {}
        protected void doStart() {}
        protected abstract void doProcess();
        protected void doStop() {}
    
        protected void handleError(Exception e) {
            log.error("[{}] Error in doWork", getName(), e);
        }
    
        protected void incrementProcessed() {
            processedCount.incrementAndGet();
        }
    }
    

    Utilisation

    @Component
    public class OrderWorker extends AbstractWorker {
    
        @Override
        public String getName() {
            return "order-worker";
        }
    
        @Override
        protected void doInitialize() {
            // Initialisation spécifique
        }
    
        @Override
        protected void doProcess() {
            // Traitement principal
            processOrders();
            incrementProcessed();
        }
    
        private void processOrders() {
            // ...
        }
    }
    

    5. Cycle de vie

    5.1 Séquence de démarrage

    1. MOP.start()
    2. Pour chaque worker (trié par startPriority ASC) :
       a. worker.initialize()
       b. worker.start()
       c. Enregistrement dans Supervisor
    3. Boucle principale :
       - Pour chaque worker non-schedulé, non-passif :
         - worker.doWork()
         - Sleep(worker.getCycleIntervalMs())
    

    5.2 Séquence d’arrêt

    1. Signal SIGTERM reçu
    2. MOP.gracefulShutdown()
    3. Pour chaque worker (trié par stopPriority ASC) :
       a. worker.stop()
       b. Attendre terminaison
    4. Cleanup final
    

    5.3 Diagramme

                     ┌──────────────┐
                     │   CREATED    │
                     └──────┬───────┘
                            │ initialize()
                            ▼
                     ┌──────────────┐
                     │ INITIALIZED  │
                     └──────┬───────┘
                            │ start()
                            ▼
              ┌─────────────────────────────┐
              │          RUNNING            │
              │                             │
              │  ┌──────────────────────┐   │
              │  │   doWork() loop      │   │
              │  │   (si non-passif)    │   │
              │  └──────────────────────┘   │
              │                             │
              └─────────────┬───────────────┘
                            │ stop()
                            ▼
                     ┌──────────────┐
                     │   STOPPED    │
                     └──────────────┘
    

    6. Communication entre Workers

    6.1 Via SharedDataRegistry

    @Component
    public class ProducerWorker extends AbstractWorker {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @Override
        protected void doProcess() {
            // Publier des données
            registry.put("orders.pending.count", pendingOrders.size());
            registry.incrementSequence("orders.total");
        }
    }
    
    @Component
    public class ConsumerWorker extends AbstractWorker {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @Override
        protected void doProcess() {
            // Lire les données
            int pending = registry.getInt("orders.pending.count").orElse(0);
            if (pending > 0) {
                processOrders();
            }
        }
    }
    

    6.2 Via KvBus

    @Component
    public class OrderWorker extends AbstractWorker {
    
        @Autowired
        private KvBus kvBus;
    
        @Override
        protected void doProcess() {
            // Stocker l'état partagé (même entre instances)
            kvBus.put("order:" + orderId, orderJson);
        }
    }
    

    6.3 Via Events

    @Component
    public class EventProducerWorker extends AbstractWorker {
    
        @Autowired
        private ApplicationEventPublisher eventPublisher;
    
        @Override
        protected void doProcess() {
            // Publier un événement Spring
            eventPublisher.publishEvent(new OrderCreatedEvent(order));
        }
    }
    
    @Component
    public class EventConsumerWorker extends AbstractWorker {
    
        @EventListener
        public void onOrderCreated(OrderCreatedEvent event) {
            // Réagir à l'événement
            processOrder(event.getOrder());
        }
    }
    

    7. Workers et TechDB (V4)

    7.1 Persistance d’état

    @Component
    public class KafkaConsumerWorker extends AbstractWorker {
    
        @Autowired
        private TechDbManager techDb;
    
        private long currentOffset;
    
        @Override
        protected void doInitialize() {
            // Restaurer l'offset au démarrage
            currentOffset = techDb.getOffset("kafka", "my-topic-0")
                .orElse(0L);
            log.info("Starting from offset: {}", currentOffset);
        }
    
        @Override
        protected void doProcess() {
            // Traiter les messages
            List<Message> messages = consume(currentOffset);
            for (Message msg : messages) {
                process(msg);
                currentOffset = msg.getOffset();
            }
    
            // Persister périodiquement
            if (currentOffset % 1000 == 0) {
                techDb.saveOffset("kafka", "my-topic-0", currentOffset, null);
            }
        }
    
        @Override
        protected void doStop() {
            // Sauvegarder l'offset final
            techDb.saveOffset("kafka", "my-topic-0", currentOffset, null);
        }
    }
    

    7.2 État du worker

    @Component
    public class BatchWorker extends AbstractWorker {
    
        @Autowired
        private TechDbManager techDb;
    
        @Override
        protected void doProcess() {
            // Mettre à jour l'état
            techDb.saveWorkerState(getName(), "PROCESSING",
                Map.of(
                    "currentBatch", currentBatchId,
                    "progress", progress
                ));
    
            // Traitement...
    
            techDb.saveWorkerState(getName(), "IDLE", Map.of());
        }
    }
    

    8. Patterns courants

    8.1 Worker avec retry

    @Component
    public class RetryableWorker extends AbstractWorker {
    
        @Autowired
        private RetryTemplate retryTemplate;
    
        @Override
        protected void doProcess() {
            retryTemplate.execute(context -> {
                processWithRetry();
                return null;
            });
        }
    }
    

    8.2 Worker avec circuit breaker

    @Component
    public class ResilientWorker extends AbstractWorker {
    
        @Autowired
        private CircuitBreakerTemplate circuitBreaker;
    
        @Override
        protected void doProcess() {
            circuitBreaker.execute("external-api", () -> {
                callExternalApi();
            });
        }
    }
    

    8.3 Worker batch

    @Component
    public class BatchWorker extends AbstractWorker {
    
        private final int BATCH_SIZE = 100;
    
        @Override
        protected void doProcess() {
            List<Item> batch = fetchBatch(BATCH_SIZE);
            if (batch.isEmpty()) {
                return;
            }
    
            for (Item item : batch) {
                processItem(item);
                incrementProcessed();
            }
        }
    }
    

    9. Tests

    9.1 Test unitaire

    @ExtendWith(MockitoExtension.class)
    class SimpleWorkerTest {
    
        @InjectMocks
        private SimpleWorker worker;
    
        @Test
        void shouldInitialize() {
            worker.initialize();
            // Assertions...
        }
    
        @Test
        void shouldProcessItems() {
            worker.initialize();
            worker.start();
    
            worker.doWork();
    
            Map<String, Object> stats = worker.getStats();
            assertEquals(1L, stats.get("processedCount"));
        }
    
        @Test
        void shouldStopGracefully() {
            worker.initialize();
            worker.start();
            worker.stop();
    
            assertFalse(worker.isHealthy());
        }
    }
    

    9.2 Test d’intégration

    @SpringBootTest
    class WorkerIntegrationTest {
    
        @Autowired
        private List<Worker> workers;
    
        @Test
        void allWorkersShouldBeHealthyAfterStart() {
            workers.forEach(Worker::initialize);
            workers.forEach(Worker::start);
    
            workers.forEach(w -> assertTrue(w.isHealthy(),
                "Worker " + w.getName() + " should be healthy"));
        }
    }
    

    10. Auto-Restart des Workers

    Le MOP surveille automatiquement la sante des workers et peut les redemarrer en cas d’echec.

    Fonctionnement

    1. Detection : Le MOP verifie worker.isHealthy() a chaque cycle de la boucle principale
    2. Compteur : Un compteur d’echecs consecutifs est maintenu pour chaque worker
    3. Seuil : Apres 3 echecs consecutifs (configurable), le worker est redemarre
    4. Restart : Sequence stop()initialize()start() → replanification doWork()
    5. Reset : Le compteur est remis a zero si le worker redevient healthy

    Logs associes

    [step:worker_auto_restart] Worker 'xxx' unhealthy 3 fois, tentative de redemarrage
    [step:worker_restarted] Worker 'xxx' redemarre avec succes
    [step:worker_restart_failed] Echec du redemarrage de 'xxx': <error>
    

    Implementation dans le Worker

    Pour beneficier de l’auto-restart, le worker doit :

    1. Retourner false dans isHealthy() en cas de probleme
    2. Supporter un cycle stop()initialize()start()
    3. Reinitialiser son etat interne dans initialize()
    @Override
    public boolean isHealthy() {
        // Retourner false declenche le compteur d'echecs
        return lastApiCallSuccessful && !hasConsecutiveErrors;
    }
    

    Workers exclus

    Les workers built-in du socle (http_worker, control_worker, etc.) ne sont pas soumis a l’auto-restart car ils sont geres par le Supervisor.

    11. Workers Event-Driven

    Les Event-Driven Workers sont des workers passifs declenches par des evenements externes (Kafka, queues, CDC, etc.).

    Classe de base

    package eu.lmvi.socle.worker.event;
    
    public abstract class AbstractEventDrivenWorker<T> implements Worker {
    
        // Concurrence configurable
        protected AbstractEventDrivenWorker(int concurrency) { ... }
    
        // Mode PASSIVE automatique
        @Override
        public final boolean isPassive() { return true; }
    
        @Override
        public final String getSchedule() { return "PASSIVE"; }
    
        // Methodes abstraites a implementer
        protected abstract T pollEvent() throws InterruptedException;
        protected abstract void processEvent(T event);
    
        // Hooks optionnels
        protected void onInitialize() { }
        protected void onStarted() { }
        protected void onStopping() { }
        protected void onStopped() { }
        protected void handleError(T event, Exception e, int workerId) { }
        public long getBacklog() { return 0; }
    }
    

    Exemple d’implementation

    @Component
    public class KafkaEventWorker extends AbstractEventDrivenWorker<ConsumerRecord<String, String>> {
    
        private final BlockingQueue<ConsumerRecord<String, String>> eventQueue = new LinkedBlockingQueue<>();
    
        public KafkaEventWorker() {
            super(4); // 4 threads concurrents
        }
    
        @Override
        public String getName() {
            return "kafka_event_worker";
        }
    
        @Override
        protected ConsumerRecord<String, String> pollEvent() throws InterruptedException {
            return eventQueue.poll(100, TimeUnit.MILLISECONDS);
        }
    
        @Override
        protected void processEvent(ConsumerRecord<String, String> event) {
            // Traitement de l'evenement
            log.info("Processing: {}", event.value());
        }
    
        @Override
        public long getBacklog() {
            return eventQueue.size();
        }
    }
    

    Metriques standardisees

    Les Event-Driven Workers exposent des metriques compatibles avec le StatusDashboard :

    Cle Type Description
    state String "running" ou "stopped"
    execution_count long Nombre d’evenements traites
    errors_count long Nombre d’erreurs
    last_execution String ISO-8601 du dernier traitement
    schedule String Toujours "PASSIVE"
    messages_processed long Alias de execution_count
    throughput_per_sec double Debit calcule
    backlog long Evenements en attente

    12. Convention des Stats Workers

    Pour une integration correcte avec le StatusDashboard et le WorkerActivityTracker, tous les workers doivent exposer des cles standardisees dans getStats().

    Cles obligatoires

    Cle Type Description
    state String "running" ou "stopped"
    execution_count long Nombre total d’executions
    errors_count long Nombre d’erreurs
    last_execution String/long Timestamp ISO-8601 ou epoch ms
    schedule String Mode: "PASSIVE", "INTERVAL", "CRON", ou expression cron

    Cles optionnelles

    Cle Type Description
    total_duration_ms long Duree totale cumulee
    avg_duration_ms double Duree moyenne par execution
    throughput_per_sec double Debit (ops/sec)
    messages_processed long Pour Event-Driven (alias execution_count)
    backlog long File d’attente

    Exemple d’implementation

    @Override
    public Map<String, Object> getStats() {
        Map<String, Object> stats = new HashMap<>();
    
        // Cles standardisees (obligatoires)
        stats.put("state", running ? "running" : "stopped");
        stats.put("execution_count", executionCount.get());
        stats.put("errors_count", errorCount.get());
        stats.put("last_execution", lastExecution != null
            ? lastExecution.toString()  // ISO-8601
            : null);
        stats.put("schedule", getSchedule() != null ? getSchedule() : "INTERVAL");
    
        // Cles optionnelles
        stats.put("total_duration_ms", totalDurationMs.get());
        stats.put("avg_duration_ms", executionCount.get() > 0
            ? (double) totalDurationMs.get() / executionCount.get()
            : 0.0);
    
        return stats;
    }
    

    13. JaninoWorker (Scripts Java Compiles)

    Le JaninoWorker permet d’executer du code Java compile dynamiquement a la volee. Contrairement au ScriptEngine (interprete), Janino compile le code en bytecode JVM pour des performances natives.

    13.1 Activation

    socle:
      janino:
        enabled: true
        scripts-path: ./repository/scripts/java
        reload-interval-ms: 300000  # 5 minutes
    

    13.2 Interfaces disponibles

    Les scripts doivent implementer une des interfaces :

    Interface Usage Methode
    Calculator<T> Calculs (frais, taxes) T calculate(Map<String, Object> context)
    Executable Execution generique Object execute(Map<String, Object> context)
    Validator Validations ValidationResult validate(Object input)

    13.3 Exemple de script

    // Fichier: repository/scripts/java/KrakenFeeCalculator.java
    
    import eu.lmvi.socle.janino.interfaces.Calculator;
    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.util.Map;
    
    public class KrakenFeeCalculator implements Calculator<BigDecimal> {
    
        private static final BigDecimal FEE_RATE = new BigDecimal("0.0026");
    
        @Override
        public BigDecimal calculate(Map<String, Object> context) {
            BigDecimal amount = (BigDecimal) context.get("amount");
            return amount.multiply(FEE_RATE).setScale(8, RoundingMode.HALF_UP);
        }
    }
    

    13.4 Utilisation dans un Worker

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

    13.5 Acces direct au moteur

    @Autowired
    private JaninoWorker janinoWorker;
    
    // Compiler un script a la volee
    janinoWorker.compileScript("MyScript", sourceCode);
    
    // Verifier si compile
    boolean ready = janinoWorker.isScriptCompiled("MyScript");
    
    // Forcer le rechargement
    janinoWorker.forceReload();
    
    // Acces au moteur
    JaninoEngine engine = janinoWorker.getEngine();
    

    13.6 Securite

    Janino bloque l’acces aux packages dangereux :

    • java.io – Acces fichiers
    • java.net – Acces reseau
    • java.lang.reflect – Reflection
    • sun.*, com.sun.* – Classes internes

    13.7 Comparaison avec ScriptEngine

    Critere ScriptEngine JaninoWorker
    Langages JavaScript, BeanShell Java pur
    Performance Interprete Compile (natif JVM)
    Typage Dynamique Statique
    Hot reload Non Oui (automatique)
    Use case Prototypage, scripts simples Calculs haute perf

    14. Bonnes pratiques

    DO

    • Implémenter stop() pour un arrêt gracieux
    • Utiliser isHealthy() pour signaler les problèmes
    • Logger les transitions d’état
    • Gérer les exceptions dans doWork()
    • Utiliser des priorités pour les dépendances

    DON’T

    • Ne pas bloquer indéfiniment dans doWork()
    • Ne pas ignorer les signaux d’arrêt
    • Ne pas oublier de décrémenter les ressources dans stop()
    • Ne pas utiliser de variables statiques pour l’état

    15. Références

  • Socle V004 – Status Dashboard

    Socle V004 – Status Dashboard

    27 – Status Dashboard

    Version : 4.0.0 Package : eu.lmvi.socle.worker.status

    Introduction

    Le StatusDashboardWorker est un Worker integre au Socle V004 qui expose un dashboard HTML de supervision sur un port dedie. Il permet de visualiser en temps reel l’etat de l’application et de tous les Workers.

    Caracteristiques

    • Automatique : Active par defaut, aucun code a ajouter
    • Port dedie : 9374 (configurable)
    • Dashboard HTML : Interface web avec rafraichissement AJAX partiel
    • API JSON : Endpoints REST pour integration
    • Metriques d’activite : Throughput, duree, charge relative
    • Animation visuelle : Mise en evidence des valeurs modifiees

    Acces au Dashboard

    Une fois l’application demarree, le dashboard est accessible sur :

    http://localhost:9374/
    

    Configuration

    application.yml

    socle:
      status_dashboard:
        # Activer/desactiver le dashboard (defaut: true)
        enabled: true
    
        # Port du serveur HTTP (defaut: 9374)
        port: 9374
    
        # Adresse de bind (vide = toutes les interfaces)
        bind_address: ""
    
        # Intervalle de rafraichissement HTML en secondes (defaut: 5)
        refresh_interval: 5
    
        # Fenetre de calcul des metriques en secondes (defaut: 60)
        metrics_window: 60
    
        # Limite de requetes par seconde (defaut: 10)
        max_requests_per_second: 10
    
        # Activer l'API JSON (defaut: true)
        api_enabled: true
    

    Variables d’environnement

    Variable Description Defaut
    STATUS_DASHBOARD_ENABLED Activer le dashboard true
    STATUS_DASHBOARD_PORT Port HTTP 9374
    STATUS_DASHBOARD_BIND Adresse de bind (vide)
    STATUS_DASHBOARD_REFRESH Refresh interval (sec) 5
    STATUS_DASHBOARD_METRICS_WINDOW Fenetre metriques (sec) 60
    STATUS_DASHBOARD_MAX_RPS Max requetes/sec 10
    STATUS_DASHBOARD_API_ENABLED Activer API JSON true

    Rafraichissement AJAX

    Le dashboard utilise JavaScript pour mettre a jour uniquement les valeurs qui changent, sans recharger la page entiere.

    Fonctionnement

    1. Chargement initial : La page HTML complete est servie
    2. Rafraichissement periodique : JavaScript appelle /api/status et /api/workers
    3. Mise a jour selective : Seuls les elements dont la valeur a change sont modifies
    4. Animation visuelle : Les valeurs modifiees sont brievement mises en surbrillance (effet cyan)

    Avantages

    • Pas de rechargement complet de la page
    • Experience utilisateur fluide
    • Reduction de la bande passante
    • Conservation de l’etat de scroll

    Configuration de l’intervalle

    L’intervalle de rafraichissement AJAX correspond a refresh_interval :

    socle:
      status_dashboard:
        refresh_interval: 3  # Rafraichissement toutes les 3 secondes
    

    Endpoints HTTP

    Dashboard HTML

    Endpoint Methode Description
    / GET Page HTML du dashboard
    /index.html GET Alias pour /

    Health Check

    Endpoint Methode Description
    /health GET Status UP/DOWN en JSON

    Exemple de reponse :

    {"status":"UP"}
    

    API JSON

    Endpoint Methode Description
    /api/status GET Status global de l’application
    /api/workers GET Liste de tous les workers avec metriques
    /api/workers/{name} GET Metriques d’un worker specifique

    Donnees affichees

    Section : Status Global

    Donnee Description
    MOP State Etat du MainOrchestratorProcess (RUNNING, DRAINING, etc.)
    Uptime Temps depuis le demarrage
    Workers Health Nombre de workers healthy / total
    Total Activity Throughput agrege (ops/sec)

    Section : Worker Activity

    Barres visuelles montrant la charge relative de chaque Worker :

    cdc_kafka_worker    ████████████████████░░░░  85%  [HOT]
    http_worker         ██████████████░░░░░░░░░░  58%
    rule_engine         ████████░░░░░░░░░░░░░░░░  32%
    control_worker      ███░░░░░░░░░░░░░░░░░░░░░  12%
    maintenance_worker  █░░░░░░░░░░░░░░░░░░░░░░░   2%  [IDLE]
    

    Tags :

    • [HOT] : Worker avec charge > 80%
    • [IDLE] : Worker inactif
    • [PASSIVE] : Worker event-driven sans activite

    Section : Workers Detail

    Tableau detaille avec :

    Colonne Description
    Name Nom du worker
    State Running / Stopped
    Health OK / FAIL
    Mode PASSIVE / CRON / INTERVAL
    Executions Nombre total d’executions doWork()
    Avg Duration Duree moyenne d’execution
    Throughput Operations par seconde
    Last Activity Temps depuis derniere activite
    Errors Nombre d’erreurs

    Exemples API JSON

    GET /api/status

    {
      "timestamp": "2026-01-12T17:34:56.789Z",
      "application": {
        "name": "my-app",
        "environment": "PROD",
        "version": "4.0.0"
      },
      "mop": {
        "state": "RUNNING",
        "uptime_ms": 9252000,
        "uptime_human": "2h 34m 12s"
      },
      "workers": {
        "total": 6,
        "healthy": 6,
        "running": 6
      },
      "activity": {
        "total_throughput": 847.3,
        "metrics_window_sec": 60
      }
    }
    

    GET /api/workers

    {
      "timestamp": "2026-01-12T17:34:56.789Z",
      "workers": [
        {
          "name": "cdc_kafka_worker",
          "state": "running",
          "healthy": true,
          "schedule": "PASSIVE",
          "metrics": {
            "execution_count": 12847,
            "total_duration_ms": 29548,
            "avg_duration_ms": 2.3,
            "last_execution": "2026-01-12T17:34:55.123Z",
            "throughput_per_sec": 721.4,
            "errors_count": 3,
            "messages_processed": 45230
          },
          "relative_load": 0.85
        }
      ]
    }
    

    Securite

    Bind localhost uniquement (production)

    Pour limiter l’acces au dashboard en production :

    socle:
      status_dashboard:
        bind_address: "127.0.0.1"
    

    Desactiver en production

    socle:
      status_dashboard:
        enabled: false
    

    Ou via variable d’environnement :

    export STATUS_DASHBOARD_ENABLED=false
    

    Architecture

    ┌─────────────────────────────────────────────────────────────┐
    │                  StatusDashboardWorker                      │
    │                                                             │
    │  ┌─────────────────────┐    ┌─────────────────────────┐    │
    │  │ WorkerActivityTracker│    │ DashboardHtmlRenderer   │    │
    │  │ (collecte metriques) │    │ (genere HTML)           │    │
    │  └──────────┬──────────┘    └────────────┬────────────┘    │
    │             │                            │                  │
    │             └──────────┬─────────────────┘                  │
    │                        │                                    │
    │              ┌─────────▼─────────┐                          │
    │              │   MiniHttpServer  │                          │
    │              │   (port 9374)     │                          │
    │              └─────────┬─────────┘                          │
    └────────────────────────┼────────────────────────────────────┘
                             │
                             ▼
                        Browser / curl
    

    Composants

    Composant Responsabilite
    StatusDashboardWorker Worker principal, orchestre le dashboard
    WorkerActivityTracker Collecte et agregation des metriques
    MiniHttpServer Serveur HTTP leger (ServerSocket)
    DashboardHtmlRenderer Generation du HTML avec CSS inline

    Integration avec Monitoring

    Le dashboard peut etre integre avec des outils de monitoring existants :

    Prometheus / Grafana

    Utilisez l’endpoint /api/status pour collecter les metriques :

    # prometheus.yml
    scrape_configs:
      - job_name: 'socle-status'
        metrics_path: /api/status
        static_configs:
          - targets: ['localhost:9374']
    

    Health Checks (Kubernetes)

    # deployment.yaml
    livenessProbe:
      httpGet:
        path: /health
        port: 9374
      initialDelaySeconds: 30
      periodSeconds: 10
    

    Convention des Stats Workers

    Pour que le dashboard affiche correctement les metriques, les workers doivent exposer des cles standardisees dans getStats().

    Cles requises par WorkerActivityTracker

    Cle Type Utilisation
    state String Affichage Running/Stopped
    execution_count long Colonne Executions
    errors_count long Colonne Errors
    last_execution String/long Colonne Last Activity
    schedule String Colonne Mode

    Cles optionnelles

    Cle Type Utilisation
    total_duration_ms long Calcul Avg Duration
    avg_duration_ms double Colonne Avg Duration (prioritaire)
    throughput_per_sec double Colonne Throughput
    messages_processed long Fallback pour execution_count

    Note : Les workers heritant de AbstractEventDrivenWorker exposent automatiquement ces cles depuis la version 4.0.1.

    Voir 05-WORKERS.md section 12 pour les details d’implementation.

    Troubleshooting

    Le dashboard ne demarre pas

    Cause possible : Port deja utilise

    Solution :

    socle:
      status_dashboard:
        port: 9375  # Changer le port
    

    Metriques a zero

    Cause possible : Les workers sont en mode PASSIVE et n’ont pas encore traite d’evenements

    Solution : Normal pour les workers event-driven. Les metriques apparaitront des que des evenements seront traites.

    Dashboard lent

    Cause possible : Trop de workers ou refresh trop frequent

    Solution :

    socle:
      status_dashboard:
        refresh_interval: 10  # Augmenter l'intervalle
    

    Voir aussi

    Socle V004 – Status Dashboard

  • Socle V004 – Workers

    Socle V004 – Workers

    05 – Workers

    Version : 4.0.1 Date : 2026-01-13

    1. Introduction

    Les Workers sont les composants de traitement du Socle V4. Chaque Worker implémente une tâche spécifique et son cycle de vie est géré par le MOP (Main Orchestrator Process).

    Caractéristiques

    • Interface unique : Tous les workers implémentent Worker
    • Cycle de vie géré : Le MOP orchestre start/stop/doWork
    • Priorités : Ordre de démarrage/arrêt configurable
    • Scheduling : Support cron et intervalle
    • Health check : Supervision intégrée

    2. Interface Worker

    package eu.lmvi.socle.worker;
    
    public interface Worker {
    
        /**
         * Nom unique du worker
         */
        String getName();
    
        /**
         * Initialisation (appelé une fois au démarrage)
         */
        void initialize();
    
        /**
         * Démarrage du worker
         */
        void start();
    
        /**
         * Traitement principal (appelé cycliquement)
         */
        void doWork();
    
        /**
         * Arrêt gracieux
         */
        void stop();
    
        /**
         * État de santé
         */
        boolean isHealthy();
    
        /**
         * Statistiques du worker
         */
        Map<String, Object> getStats();
    
        // === Priorités ===
    
        /**
         * Priorité au démarrage (plus petit = premier)
         */
        default int getStartPriority() {
            return 100;
        }
    
        /**
         * Priorité à l'arrêt (plus petit = premier)
         */
        default int getStopPriority() {
            return 100;
        }
    
        // === Scheduling ===
    
        /**
         * Expression cron (ou null si non schedulé)
         */
        default String getSchedule() {
            return null;
        }
    
        /**
         * Intervalle entre les cycles doWork() en ms
         * Valeurs recommandees : 5000 (5s), 10000 (10s), 30000 (30s)
         */
        default long getCycleIntervalMs() {
            return 5000; // 5 secondes par defaut
        }
    
        /**
         * Worker schedulé par cron ?
         */
        default boolean isScheduled() {
            return getSchedule() != null;
        }
    
        /**
         * Worker passif (ne fait rien dans doWork) ?
         */
        default boolean isPassive() {
            return false;
        }
    }
    

    3. Implémentation de base

    3.1 Worker simple

    package com.myapp.worker;
    
    import eu.lmvi.socle.worker.Worker;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    
    @Component
    public class SimpleWorker implements Worker {
    
        private static final Logger log = LoggerFactory.getLogger(SimpleWorker.class);
    
        private volatile boolean running = false;
        private long processedCount = 0;
    
        @Override
        public String getName() {
            return "simple-worker";
        }
    
        @Override
        public void initialize() {
            log.info("Initializing SimpleWorker");
            // Charger configuration, connexions, etc.
        }
    
        @Override
        public void start() {
            log.info("Starting SimpleWorker");
            running = true;
        }
    
        @Override
        public void doWork() {
            if (!running) return;
    
            try {
                // Traitement principal
                processedCount++;
                log.debug("Processing item #{}", processedCount);
            } catch (Exception e) {
                log.error("Error in doWork", e);
            }
        }
    
        @Override
        public void stop() {
            log.info("Stopping SimpleWorker");
            running = false;
        }
    
        @Override
        public boolean isHealthy() {
            return running;
        }
    
        @Override
        public Map<String, Object> getStats() {
            return Map.of(
                "running", running,
                "processedCount", processedCount
            );
        }
    }
    

    3.2 Worker avec priorité

    @Component
    public class DatabaseWorker implements Worker {
    
        @Override
        public String getName() {
            return "database-worker";
        }
    
        @Override
        public int getStartPriority() {
            return 10;  // Démarre en premier (connexion DB)
        }
    
        @Override
        public int getStopPriority() {
            return 90;  // S'arrête en dernier (flush des données)
        }
    
        // ... autres méthodes
    }
    

    3.3 Worker schedulé (cron)

    @Component
    public class ReportWorker implements Worker {
    
        @Override
        public String getName() {
            return "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...");
            // Génération du rapport
        }
    
        // ... autres méthodes
    }
    

    3.4 Worker passif (HTTP)

    @Component
    public class ApiWorker implements Worker {
    
        @Override
        public String getName() {
            return "api-worker";
        }
    
        @Override
        public boolean isPassive() {
            return true;  // Pas de doWork() cyclique
        }
    
        @Override
        public void doWork() {
            // Ne fait rien - le traitement est déclenché par HTTP
        }
    
        // Les requêtes HTTP déclenchent le traitement via endpoints REST
    }
    

    4. AbstractWorker

    Le Socle fournit une classe de base qui simplifie l’implémentation :

    package eu.lmvi.socle.worker;
    
    public abstract class AbstractWorker implements Worker {
    
        protected final Logger log = LoggerFactory.getLogger(getClass());
        protected volatile boolean running = false;
        protected volatile boolean healthy = true;
        protected final AtomicLong processedCount = new AtomicLong(0);
        protected final AtomicLong errorCount = new AtomicLong(0);
        protected Instant lastActivity;
    
        @Override
        public void initialize() {
            log.info("[{}] Initializing", getName());
            doInitialize();
        }
    
        @Override
        public void start() {
            log.info("[{}] Starting", getName());
            running = true;
            doStart();
        }
    
        @Override
        public void stop() {
            log.info("[{}] Stopping", getName());
            running = false;
            doStop();
        }
    
        @Override
        public void doWork() {
            if (!running) return;
    
            try {
                doProcess();
                lastActivity = Instant.now();
            } catch (Exception e) {
                errorCount.incrementAndGet();
                handleError(e);
            }
        }
    
        @Override
        public boolean isHealthy() {
            return running && healthy;
        }
    
        @Override
        public Map<String, Object> getStats() {
            return Map.of(
                "running", running,
                "healthy", healthy,
                "processedCount", processedCount.get(),
                "errorCount", errorCount.get(),
                "lastActivity", lastActivity != null ? lastActivity.toString() : "never"
            );
        }
    
        // === Méthodes à implémenter ===
    
        protected void doInitialize() {}
        protected void doStart() {}
        protected abstract void doProcess();
        protected void doStop() {}
    
        protected void handleError(Exception e) {
            log.error("[{}] Error in doWork", getName(), e);
        }
    
        protected void incrementProcessed() {
            processedCount.incrementAndGet();
        }
    }
    

    Utilisation

    @Component
    public class OrderWorker extends AbstractWorker {
    
        @Override
        public String getName() {
            return "order-worker";
        }
    
        @Override
        protected void doInitialize() {
            // Initialisation spécifique
        }
    
        @Override
        protected void doProcess() {
            // Traitement principal
            processOrders();
            incrementProcessed();
        }
    
        private void processOrders() {
            // ...
        }
    }
    

    5. Cycle de vie

    5.1 Séquence de démarrage

    1. MOP.start()
    2. Pour chaque worker (trié par startPriority ASC) :
       a. worker.initialize()
       b. worker.start()
       c. Enregistrement dans Supervisor
    3. Boucle principale :
       - Pour chaque worker non-schedulé, non-passif :
         - worker.doWork()
         - Sleep(worker.getCycleIntervalMs())
    

    5.2 Séquence d’arrêt

    1. Signal SIGTERM reçu
    2. MOP.gracefulShutdown()
    3. Pour chaque worker (trié par stopPriority ASC) :
       a. worker.stop()
       b. Attendre terminaison
    4. Cleanup final
    

    5.3 Diagramme

                     ┌──────────────┐
                     │   CREATED    │
                     └──────┬───────┘
                            │ initialize()
                            ▼
                     ┌──────────────┐
                     │ INITIALIZED  │
                     └──────┬───────┘
                            │ start()
                            ▼
              ┌─────────────────────────────┐
              │          RUNNING            │
              │                             │
              │  ┌──────────────────────┐   │
              │  │   doWork() loop      │   │
              │  │   (si non-passif)    │   │
              │  └──────────────────────┘   │
              │                             │
              └─────────────┬───────────────┘
                            │ stop()
                            ▼
                     ┌──────────────┐
                     │   STOPPED    │
                     └──────────────┘
    

    6. Communication entre Workers

    6.1 Via SharedDataRegistry

    @Component
    public class ProducerWorker extends AbstractWorker {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @Override
        protected void doProcess() {
            // Publier des données
            registry.put("orders.pending.count", pendingOrders.size());
            registry.incrementSequence("orders.total");
        }
    }
    
    @Component
    public class ConsumerWorker extends AbstractWorker {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @Override
        protected void doProcess() {
            // Lire les données
            int pending = registry.getInt("orders.pending.count").orElse(0);
            if (pending > 0) {
                processOrders();
            }
        }
    }
    

    6.2 Via KvBus

    @Component
    public class OrderWorker extends AbstractWorker {
    
        @Autowired
        private KvBus kvBus;
    
        @Override
        protected void doProcess() {
            // Stocker l'état partagé (même entre instances)
            kvBus.put("order:" + orderId, orderJson);
        }
    }
    

    6.3 Via Events

    @Component
    public class EventProducerWorker extends AbstractWorker {
    
        @Autowired
        private ApplicationEventPublisher eventPublisher;
    
        @Override
        protected void doProcess() {
            // Publier un événement Spring
            eventPublisher.publishEvent(new OrderCreatedEvent(order));
        }
    }
    
    @Component
    public class EventConsumerWorker extends AbstractWorker {
    
        @EventListener
        public void onOrderCreated(OrderCreatedEvent event) {
            // Réagir à l'événement
            processOrder(event.getOrder());
        }
    }
    

    7. Workers et TechDB (V4)

    7.1 Persistance d’état

    @Component
    public class KafkaConsumerWorker extends AbstractWorker {
    
        @Autowired
        private TechDbManager techDb;
    
        private long currentOffset;
    
        @Override
        protected void doInitialize() {
            // Restaurer l'offset au démarrage
            currentOffset = techDb.getOffset("kafka", "my-topic-0")
                .orElse(0L);
            log.info("Starting from offset: {}", currentOffset);
        }
    
        @Override
        protected void doProcess() {
            // Traiter les messages
            List<Message> messages = consume(currentOffset);
            for (Message msg : messages) {
                process(msg);
                currentOffset = msg.getOffset();
            }
    
            // Persister périodiquement
            if (currentOffset % 1000 == 0) {
                techDb.saveOffset("kafka", "my-topic-0", currentOffset, null);
            }
        }
    
        @Override
        protected void doStop() {
            // Sauvegarder l'offset final
            techDb.saveOffset("kafka", "my-topic-0", currentOffset, null);
        }
    }
    

    7.2 État du worker

    @Component
    public class BatchWorker extends AbstractWorker {
    
        @Autowired
        private TechDbManager techDb;
    
        @Override
        protected void doProcess() {
            // Mettre à jour l'état
            techDb.saveWorkerState(getName(), "PROCESSING",
                Map.of(
                    "currentBatch", currentBatchId,
                    "progress", progress
                ));
    
            // Traitement...
    
            techDb.saveWorkerState(getName(), "IDLE", Map.of());
        }
    }
    

    8. Patterns courants

    8.1 Worker avec retry

    @Component
    public class RetryableWorker extends AbstractWorker {
    
        @Autowired
        private RetryTemplate retryTemplate;
    
        @Override
        protected void doProcess() {
            retryTemplate.execute(context -> {
                processWithRetry();
                return null;
            });
        }
    }
    

    8.2 Worker avec circuit breaker

    @Component
    public class ResilientWorker extends AbstractWorker {
    
        @Autowired
        private CircuitBreakerTemplate circuitBreaker;
    
        @Override
        protected void doProcess() {
            circuitBreaker.execute("external-api", () -> {
                callExternalApi();
            });
        }
    }
    

    8.3 Worker batch

    @Component
    public class BatchWorker extends AbstractWorker {
    
        private final int BATCH_SIZE = 100;
    
        @Override
        protected void doProcess() {
            List<Item> batch = fetchBatch(BATCH_SIZE);
            if (batch.isEmpty()) {
                return;
            }
    
            for (Item item : batch) {
                processItem(item);
                incrementProcessed();
            }
        }
    }
    

    9. Tests

    9.1 Test unitaire

    @ExtendWith(MockitoExtension.class)
    class SimpleWorkerTest {
    
        @InjectMocks
        private SimpleWorker worker;
    
        @Test
        void shouldInitialize() {
            worker.initialize();
            // Assertions...
        }
    
        @Test
        void shouldProcessItems() {
            worker.initialize();
            worker.start();
    
            worker.doWork();
    
            Map<String, Object> stats = worker.getStats();
            assertEquals(1L, stats.get("processedCount"));
        }
    
        @Test
        void shouldStopGracefully() {
            worker.initialize();
            worker.start();
            worker.stop();
    
            assertFalse(worker.isHealthy());
        }
    }
    

    9.2 Test d’intégration

    @SpringBootTest
    class WorkerIntegrationTest {
    
        @Autowired
        private List<Worker> workers;
    
        @Test
        void allWorkersShouldBeHealthyAfterStart() {
            workers.forEach(Worker::initialize);
            workers.forEach(Worker::start);
    
            workers.forEach(w -> assertTrue(w.isHealthy(),
                "Worker " + w.getName() + " should be healthy"));
        }
    }
    

    10. Auto-Restart des Workers

    Le MOP surveille automatiquement la sante des workers et peut les redemarrer en cas d’echec.

    Fonctionnement

    1. Detection : Le MOP verifie worker.isHealthy() a chaque cycle de la boucle principale
    2. Compteur : Un compteur d’echecs consecutifs est maintenu pour chaque worker
    3. Seuil : Apres 3 echecs consecutifs (configurable), le worker est redemarre
    4. Restart : Sequence stop()initialize()start() → replanification doWork()
    5. Reset : Le compteur est remis a zero si le worker redevient healthy

    Logs associes

    [step:worker_auto_restart] Worker 'xxx' unhealthy 3 fois, tentative de redemarrage
    [step:worker_restarted] Worker 'xxx' redemarre avec succes
    [step:worker_restart_failed] Echec du redemarrage de 'xxx': <error>
    

    Implementation dans le Worker

    Pour beneficier de l’auto-restart, le worker doit :

    1. Retourner false dans isHealthy() en cas de probleme
    2. Supporter un cycle stop()initialize()start()
    3. Reinitialiser son etat interne dans initialize()
    @Override
    public boolean isHealthy() {
        // Retourner false declenche le compteur d'echecs
        return lastApiCallSuccessful && !hasConsecutiveErrors;
    }
    

    Workers exclus

    Les workers built-in du socle (http_worker, control_worker, etc.) ne sont pas soumis a l’auto-restart car ils sont geres par le Supervisor.

    11. Workers Event-Driven

    Les Event-Driven Workers sont des workers passifs declenches par des evenements externes (Kafka, queues, CDC, etc.).

    Classe de base

    package eu.lmvi.socle.worker.event;
    
    public abstract class AbstractEventDrivenWorker<T> implements Worker {
    
        // Concurrence configurable
        protected AbstractEventDrivenWorker(int concurrency) { ... }
    
        // Mode PASSIVE automatique
        @Override
        public final boolean isPassive() { return true; }
    
        @Override
        public final String getSchedule() { return "PASSIVE"; }
    
        // Methodes abstraites a implementer
        protected abstract T pollEvent() throws InterruptedException;
        protected abstract void processEvent(T event);
    
        // Hooks optionnels
        protected void onInitialize() { }
        protected void onStarted() { }
        protected void onStopping() { }
        protected void onStopped() { }
        protected void handleError(T event, Exception e, int workerId) { }
        public long getBacklog() { return 0; }
    }
    

    Exemple d’implementation

    @Component
    public class KafkaEventWorker extends AbstractEventDrivenWorker<ConsumerRecord<String, String>> {
    
        private final BlockingQueue<ConsumerRecord<String, String>> eventQueue = new LinkedBlockingQueue<>();
    
        public KafkaEventWorker() {
            super(4); // 4 threads concurrents
        }
    
        @Override
        public String getName() {
            return "kafka_event_worker";
        }
    
        @Override
        protected ConsumerRecord<String, String> pollEvent() throws InterruptedException {
            return eventQueue.poll(100, TimeUnit.MILLISECONDS);
        }
    
        @Override
        protected void processEvent(ConsumerRecord<String, String> event) {
            // Traitement de l'evenement
            log.info("Processing: {}", event.value());
        }
    
        @Override
        public long getBacklog() {
            return eventQueue.size();
        }
    }
    

    Metriques standardisees

    Les Event-Driven Workers exposent des metriques compatibles avec le StatusDashboard :

    Cle Type Description
    state String "running" ou "stopped"
    execution_count long Nombre d’evenements traites
    errors_count long Nombre d’erreurs
    last_execution String ISO-8601 du dernier traitement
    schedule String Toujours "PASSIVE"
    messages_processed long Alias de execution_count
    throughput_per_sec double Debit calcule
    backlog long Evenements en attente

    12. Convention des Stats Workers

    Pour une integration correcte avec le StatusDashboard et le WorkerActivityTracker, tous les workers doivent exposer des cles standardisees dans getStats().

    Cles obligatoires

    Cle Type Description
    state String "running" ou "stopped"
    execution_count long Nombre total d’executions
    errors_count long Nombre d’erreurs
    last_execution String/long Timestamp ISO-8601 ou epoch ms
    schedule String Mode: "PASSIVE", "INTERVAL", "CRON", ou expression cron

    Cles optionnelles

    Cle Type Description
    total_duration_ms long Duree totale cumulee
    avg_duration_ms double Duree moyenne par execution
    throughput_per_sec double Debit (ops/sec)
    messages_processed long Pour Event-Driven (alias execution_count)
    backlog long File d’attente

    Exemple d’implementation

    @Override
    public Map<String, Object> getStats() {
        Map<String, Object> stats = new HashMap<>();
    
        // Cles standardisees (obligatoires)
        stats.put("state", running ? "running" : "stopped");
        stats.put("execution_count", executionCount.get());
        stats.put("errors_count", errorCount.get());
        stats.put("last_execution", lastExecution != null
            ? lastExecution.toString()  // ISO-8601
            : null);
        stats.put("schedule", getSchedule() != null ? getSchedule() : "INTERVAL");
    
        // Cles optionnelles
        stats.put("total_duration_ms", totalDurationMs.get());
        stats.put("avg_duration_ms", executionCount.get() > 0
            ? (double) totalDurationMs.get() / executionCount.get()
            : 0.0);
    
        return stats;
    }
    

    13. JaninoWorker (Scripts Java Compiles)

    Le JaninoWorker permet d’executer du code Java compile dynamiquement a la volee. Contrairement au ScriptEngine (interprete), Janino compile le code en bytecode JVM pour des performances natives.

    13.1 Activation

    socle:
      janino:
        enabled: true
        scripts-path: ./repository/scripts/java
        reload-interval-ms: 300000  # 5 minutes
    

    13.2 Interfaces disponibles

    Les scripts doivent implementer une des interfaces :

    Interface Usage Methode
    Calculator<T> Calculs (frais, taxes) T calculate(Map<String, Object> context)
    Executable Execution generique Object execute(Map<String, Object> context)
    Validator Validations ValidationResult validate(Object input)

    13.3 Exemple de script

    // Fichier: repository/scripts/java/KrakenFeeCalculator.java
    
    import eu.lmvi.socle.janino.interfaces.Calculator;
    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.util.Map;
    
    public class KrakenFeeCalculator implements Calculator<BigDecimal> {
    
        private static final BigDecimal FEE_RATE = new BigDecimal("0.0026");
    
        @Override
        public BigDecimal calculate(Map<String, Object> context) {
            BigDecimal amount = (BigDecimal) context.get("amount");
            return amount.multiply(FEE_RATE).setScale(8, RoundingMode.HALF_UP);
        }
    }
    

    13.4 Utilisation dans un Worker

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

    13.5 Acces direct au moteur

    @Autowired
    private JaninoWorker janinoWorker;
    
    // Compiler un script a la volee
    janinoWorker.compileScript("MyScript", sourceCode);
    
    // Verifier si compile
    boolean ready = janinoWorker.isScriptCompiled("MyScript");
    
    // Forcer le rechargement
    janinoWorker.forceReload();
    
    // Acces au moteur
    JaninoEngine engine = janinoWorker.getEngine();
    

    13.6 Securite

    Janino bloque l’acces aux packages dangereux :

    • java.io – Acces fichiers
    • java.net – Acces reseau
    • java.lang.reflect – Reflection
    • sun.*, com.sun.* – Classes internes

    13.7 Comparaison avec ScriptEngine

    Critere ScriptEngine JaninoWorker
    Langages JavaScript, BeanShell Java pur
    Performance Interprete Compile (natif JVM)
    Typage Dynamique Statique
    Hot reload Non Oui (automatique)
    Use case Prototypage, scripts simples Calculs haute perf

    14. Bonnes pratiques

    DO

    • Implémenter stop() pour un arrêt gracieux
    • Utiliser isHealthy() pour signaler les problèmes
    • Logger les transitions d’état
    • Gérer les exceptions dans doWork()
    • Utiliser des priorités pour les dépendances

    DON’T

    • Ne pas bloquer indéfiniment dans doWork()
    • Ne pas ignorer les signaux d’arrêt
    • Ne pas oublier de décrémenter les ressources dans stop()
    • Ne pas utiliser de variables statiques pour l’état

    15. Références

  • 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 – Données Partagées

    Socle V004 – Données Partagées

    07 – SharedDataRegistry

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    SharedDataRegistry est un registre centralisé pour partager des données entre Workers au sein d’une même instance. Il fournit des opérations atomiques et un système de niveaux de santé.

    Différence avec KvBus

    Aspect SharedDataRegistry KvBus
    Scope Intra-instance Optionnel inter-instances (Redis)
    Performance Ultra rapide (mémoire) Variable (réseau si Redis)
    Types Fortement typés Strings/JSON
    Health levels Oui Non
    Callbacks Oui Non

    2. Interface SharedDataRegistry

    package eu.lmvi.socle.shared;
    
    public interface SharedDataRegistry {
    
        // === Key-Value basique ===
    
        void put(String key, Object value);
        void put(String key, Object value, HealthLevel level);
        Optional<Object> get(String key);
        <T> Optional<T> get(String key, Class<T> type);
        void delete(String key);
        boolean exists(String key);
    
        // === Typed getters ===
    
        Optional<String> getString(String key);
        Optional<Integer> getInt(String key);
        Optional<Long> getLong(String key);
        Optional<Double> getDouble(String key);
        Optional<Boolean> getBoolean(String key);
    
        // === Sequences (compteurs atomiques) ===
    
        void createSequence(String key, long initialValue, HealthLevel level);
        long incrementSequence(String key);
        long incrementSequence(String key, long delta);
        long getSequence(String key);
        void setSequence(String key, long value);
    
        // === Lists ===
    
        <T> void addToList(String key, T item);
        <T> List<T> getList(String key, Class<T> type);
        void clearList(String key);
    
        // === Maps ===
    
        <V> void putInMap(String key, String mapKey, V value);
        <V> Optional<V> getFromMap(String key, String mapKey, Class<V> type);
        <V> Map<String, V> getMap(String key, Class<V> type);
        void removeFromMap(String key, String mapKey);
    
        // === Health ===
    
        HealthLevel getHealthLevel(String key);
        Map<String, HealthLevel> getAllHealthLevels();
        List<String> getUnhealthyKeys();
    
        // === Callbacks ===
    
        void registerCallback(String key, Consumer<Object> callback);
        void unregisterCallback(String key);
    
        // === Introspection ===
    
        Set<String> keys();
        Set<String> keys(String pattern);
        Map<String, Object> getAll();
        int size();
        void clear();
    }
    

    3. Health Levels

    package eu.lmvi.socle.shared;
    
    public enum HealthLevel {
        /**
         * Informatif - pas d'impact sur la santé
         */
        INFO,
    
        /**
         * Normal - contribue à la santé normale
         */
        NORMAL,
    
        /**
         * Important - dégradation si problème
         */
        IMPORTANT,
    
        /**
         * Critique - unhealthy si problème
         */
        CRITICAL
    }
    

    Utilisation dans le Supervisor

    Le Supervisor consulte les HealthLevel pour déterminer l’état de santé global :

    • CRITICAL absent ou invalide → Instance UNHEALTHY
    • IMPORTANT absent ou invalide → Instance DEGRADED
    • NORMAL/INFO → Pas d’impact

    4. Implémentation

    package eu.lmvi.socle.shared;
    
    @Component
    public class InMemorySharedDataRegistry implements SharedDataRegistry {
    
        private final ConcurrentHashMap<String, Entry> store = new ConcurrentHashMap<>();
        private final ConcurrentHashMap<String, Consumer<Object>> callbacks = new ConcurrentHashMap<>();
    
        @Override
        public void put(String key, Object value) {
            put(key, value, HealthLevel.NORMAL);
        }
    
        @Override
        public void put(String key, Object value, HealthLevel level) {
            Entry previous = store.put(key, new Entry(value, level));
            notifyCallback(key, value);
        }
    
        @Override
        public Optional<Object> get(String key) {
            Entry entry = store.get(key);
            return entry != null ? Optional.of(entry.value) : Optional.empty();
        }
    
        @Override
        public <T> Optional<T> get(String key, Class<T> type) {
            return get(key).filter(type::isInstance).map(type::cast);
        }
    
        @Override
        public Optional<String> getString(String key) {
            return get(key).map(Object::toString);
        }
    
        @Override
        public Optional<Integer> getInt(String key) {
            return get(key, Number.class).map(Number::intValue);
        }
    
        @Override
        public Optional<Long> getLong(String key) {
            return get(key, Number.class).map(Number::longValue);
        }
    
        @Override
        public void createSequence(String key, long initialValue, HealthLevel level) {
            store.put(key, new Entry(new AtomicLong(initialValue), level));
        }
    
        @Override
        public long incrementSequence(String key) {
            return incrementSequence(key, 1);
        }
    
        @Override
        public long incrementSequence(String key, long delta) {
            Entry entry = store.get(key);
            if (entry == null || !(entry.value instanceof AtomicLong)) {
                throw new IllegalStateException("Sequence not found: " + key);
            }
            long newValue = ((AtomicLong) entry.value).addAndGet(delta);
            notifyCallback(key, newValue);
            return newValue;
        }
    
        @Override
        public long getSequence(String key) {
            Entry entry = store.get(key);
            if (entry == null || !(entry.value instanceof AtomicLong)) {
                throw new IllegalStateException("Sequence not found: " + key);
            }
            return ((AtomicLong) entry.value).get();
        }
    
        @Override
        public HealthLevel getHealthLevel(String key) {
            Entry entry = store.get(key);
            return entry != null ? entry.level : null;
        }
    
        @Override
        public List<String> getUnhealthyKeys() {
            return store.entrySet().stream()
                .filter(e -> e.getValue().level == HealthLevel.CRITICAL)
                .filter(e -> !isValueHealthy(e.getValue().value))
                .map(Map.Entry::getKey)
                .toList();
        }
    
        @Override
        public void registerCallback(String key, Consumer<Object> callback) {
            callbacks.put(key, callback);
        }
    
        private void notifyCallback(String key, Object value) {
            Consumer<Object> callback = callbacks.get(key);
            if (callback != null) {
                try {
                    callback.accept(value);
                } catch (Exception e) {
                    // Log but don't propagate
                }
            }
        }
    
        private boolean isValueHealthy(Object value) {
            if (value == null) return false;
            if (value instanceof Boolean b) return b;
            if (value instanceof Number n) return n.doubleValue() >= 0;
            return true;
        }
    
        private record Entry(Object value, HealthLevel level) {}
    }
    

    5. Utilisation

    5.1 Injection

    @Service
    public class MonService {
    
        @Autowired
        private SharedDataRegistry registry;
    
        public void process() {
            // ...
        }
    }
    

    5.2 Key-Value simple

    // Stocker
    registry.put("config.maxRetries", 3);
    registry.put("status.lastSync", Instant.now().toString());
    
    // Récupérer
    int maxRetries = registry.getInt("config.maxRetries").orElse(5);
    String lastSync = registry.getString("status.lastSync").orElse("never");
    

    5.3 Avec Health Level

    // Donnée critique - instance unhealthy si absente
    registry.put("database.connected", true, HealthLevel.CRITICAL);
    
    // Donnée importante - instance degraded si absente
    registry.put("cache.available", true, HealthLevel.IMPORTANT);
    
    // Donnée normale
    registry.put("stats.requestsTotal", 0, HealthLevel.NORMAL);
    
    // Donnée informative
    registry.put("info.startTime", Instant.now(), HealthLevel.INFO);
    

    5.4 Sequences (Compteurs)

    // Créer une séquence
    registry.createSequence("orders.processed", 0, HealthLevel.NORMAL);
    
    // Incrémenter
    long count = registry.incrementSequence("orders.processed");
    log.info("Processed order #{}", count);
    
    // Incrémenter avec delta
    long bytes = registry.incrementSequence("bytes.transferred", 1024);
    
    // Lire
    long total = registry.getSequence("orders.processed");
    

    5.5 Listes

    // Ajouter à une liste
    registry.addToList("errors.recent", new ErrorRecord("timeout", Instant.now()));
    registry.addToList("errors.recent", new ErrorRecord("connection", Instant.now()));
    
    // Lire la liste
    List<ErrorRecord> errors = registry.getList("errors.recent", ErrorRecord.class);
    
    // Vider
    registry.clearList("errors.recent");
    

    5.6 Maps

    // Stocker dans une map
    registry.putInMap("workers.status", "worker-1", "RUNNING");
    registry.putInMap("workers.status", "worker-2", "STOPPED");
    
    // Lire une entrée
    Optional<String> status = registry.getFromMap("workers.status", "worker-1", String.class);
    
    // Lire toute la map
    Map<String, String> allStatus = registry.getMap("workers.status", String.class);
    

    5.7 Callbacks

    // Enregistrer un callback
    registry.registerCallback("config.maxRetries", newValue -> {
        log.info("maxRetries changed to: {}", newValue);
        reconfigure((Integer) newValue);
    });
    
    // La modification déclenche le callback
    registry.put("config.maxRetries", 5);  // Callback appelé
    

    6. Patterns courants

    6.1 État de connexion

    @Component
    public class DatabaseWorker implements Worker {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @Override
        public void initialize() {
            registry.put("database.connected", false, HealthLevel.CRITICAL);
        }
    
        @Override
        public void start() {
            try {
                connect();
                registry.put("database.connected", true, HealthLevel.CRITICAL);
            } catch (Exception e) {
                registry.put("database.connected", false, HealthLevel.CRITICAL);
                throw e;
            }
        }
    
        @Override
        public void stop() {
            disconnect();
            registry.put("database.connected", false, HealthLevel.CRITICAL);
        }
    }
    

    6.2 Métriques temps réel

    @Component
    public class MetricsCollector {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @PostConstruct
        public void init() {
            registry.createSequence("metrics.requests.total", 0, HealthLevel.INFO);
            registry.createSequence("metrics.requests.errors", 0, HealthLevel.NORMAL);
            registry.createSequence("metrics.bytes.in", 0, HealthLevel.INFO);
            registry.createSequence("metrics.bytes.out", 0, HealthLevel.INFO);
        }
    
        public void recordRequest(long bytesIn, long bytesOut, boolean success) {
            registry.incrementSequence("metrics.requests.total");
            registry.incrementSequence("metrics.bytes.in", bytesIn);
            registry.incrementSequence("metrics.bytes.out", bytesOut);
    
            if (!success) {
                registry.incrementSequence("metrics.requests.errors");
            }
        }
    
        public Map<String, Object> getMetrics() {
            return Map.of(
                "requests.total", registry.getSequence("metrics.requests.total"),
                "requests.errors", registry.getSequence("metrics.requests.errors"),
                "bytes.in", registry.getSequence("metrics.bytes.in"),
                "bytes.out", registry.getSequence("metrics.bytes.out")
            );
        }
    }
    

    6.3 Circuit Breaker state

    @Component
    public class CircuitBreakerStateManager {
    
        @Autowired
        private SharedDataRegistry registry;
    
        public void updateState(String circuitName, CircuitState state) {
            String key = "circuit." + circuitName + ".state";
            HealthLevel level = state == CircuitState.OPEN
                ? HealthLevel.IMPORTANT
                : HealthLevel.NORMAL;
            registry.put(key, state.name(), level);
        }
    
        public CircuitState getState(String circuitName) {
            return registry.getString("circuit." + circuitName + ".state")
                .map(CircuitState::valueOf)
                .orElse(CircuitState.CLOSED);
        }
    }
    

    6.4 Progress tracking

    @Component
    public class BatchProcessor {
    
        @Autowired
        private SharedDataRegistry registry;
    
        public void processBatch(String batchId, List<Item> items) {
            registry.put("batch." + batchId + ".total", items.size());
            registry.createSequence("batch." + batchId + ".processed", 0, HealthLevel.NORMAL);
    
            for (Item item : items) {
                processItem(item);
                registry.incrementSequence("batch." + batchId + ".processed");
            }
    
            registry.put("batch." + batchId + ".status", "COMPLETED");
        }
    
        public double getProgress(String batchId) {
            int total = registry.getInt("batch." + batchId + ".total").orElse(0);
            if (total == 0) return 0;
    
            long processed = registry.getSequence("batch." + batchId + ".processed");
            return (double) processed / total * 100;
        }
    }
    

    7. Intégration avec Supervisor

    @Component
    public class HealthAggregator {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @Autowired
        private List<Worker> workers;
    
        public HealthStatus aggregateHealth() {
            // Check workers
            boolean allWorkersHealthy = workers.stream().allMatch(Worker::isHealthy);
    
            // Check critical registry entries
            List<String> unhealthyKeys = registry.getUnhealthyKeys();
            boolean hasCriticalFailure = !unhealthyKeys.isEmpty();
    
            if (!allWorkersHealthy || hasCriticalFailure) {
                return HealthStatus.UNHEALTHY;
            }
    
            // Check important entries
            Map<String, HealthLevel> levels = registry.getAllHealthLevels();
            boolean hasImportantFailure = levels.entrySet().stream()
                .filter(e -> e.getValue() == HealthLevel.IMPORTANT)
                .anyMatch(e -> !isHealthy(registry.get(e.getKey())));
    
            if (hasImportantFailure) {
                return HealthStatus.DEGRADED;
            }
    
            return HealthStatus.HEALTHY;
        }
    }
    

    8. Exposition API

    @RestController
    @RequestMapping("/admin/registry")
    public class SharedDataController {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @GetMapping
        public Map<String, Object> getAll() {
            return registry.getAll();
        }
    
        @GetMapping("/{key}")
        public ResponseEntity<?> get(@PathVariable String key) {
            return registry.get(key)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
        }
    
        @GetMapping("/health")
        public Map<String, HealthLevel> getHealthLevels() {
            return registry.getAllHealthLevels();
        }
    
        @GetMapping("/unhealthy")
        public List<String> getUnhealthyKeys() {
            return registry.getUnhealthyKeys();
        }
    }
    

    9. Bonnes pratiques

    Conventions de nommage

    <category>.<subcategory>.<name>
    
    Exemples:
    - database.connected
    - worker.kafka.status
    - metrics.requests.total
    - batch.order-123.progress
    - circuit.external-api.state
    

    DO

    • Utiliser des noms de clés cohérents et hiérarchiques
    • Définir le HealthLevel approprié pour chaque donnée
    • Utiliser les sequences pour les compteurs (thread-safe)
    • Nettoyer les données obsolètes

    DON’T

    • Ne pas stocker de données volumineuses (logs, payloads)
    • Ne pas utiliser pour le stockage persistant (utiliser TechDB)
    • Ne pas créer de nouvelles clés dynamiquement sans contrôle
    • Ne pas oublier que c’est per-instance (pas de sync multi-instances)

    10. Références

  • Socle V004 – TLS/HTTPS

    Socle V004 – TLS/HTTPS

    13 – TLS/HTTPS

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Configuration du TLS/HTTPS pour sécuriser les communications HTTP du Socle V4.

    2. Configuration Spring Boot

    2.1 application.yml

    server:
      port: ${HTTPS_PORT:8443}
      ssl:
        enabled: ${SSL_ENABLED:true}
        key-store: ${SSL_KEYSTORE:classpath:keystore.p12}
        key-store-password: ${SSL_KEYSTORE_PASSWORD:changeit}
        key-store-type: ${SSL_KEYSTORE_TYPE:PKCS12}
        key-alias: ${SSL_KEY_ALIAS:socle}
    

    2.2 Variables d’environnement

    Variable Description Défaut
    SSL_ENABLED Activer SSL false
    SSL_KEYSTORE Chemin du keystore classpath:keystore.p12
    SSL_KEYSTORE_PASSWORD Mot de passe keystore changeit
    SSL_KEYSTORE_TYPE Type de keystore PKCS12
    SSL_KEY_ALIAS Alias de la clé socle

    3. Génération des certificats

    3.1 Certificat auto-signé (développement)

    # Générer un keystore PKCS12 avec certificat auto-signé
    keytool -genkeypair \
      -alias socle \
      -keyalg RSA \
      -keysize 2048 \
      -storetype PKCS12 \
      -keystore keystore.p12 \
      -validity 365 \
      -dname "CN=localhost,OU=Dev,O=MyCompany,L=Paris,C=FR" \
      -storepass changeit \
      -keypass changeit
    
    # Exporter le certificat (pour les clients)
    keytool -exportcert \
      -alias socle \
      -keystore keystore.p12 \
      -storetype PKCS12 \
      -storepass changeit \
      -file socle.crt
    

    3.2 Avec Let’s Encrypt (production)

    # Obtenir le certificat
    certbot certonly --standalone -d myapp.example.com
    
    # Convertir en PKCS12
    openssl pkcs12 -export \
      -in /etc/letsencrypt/live/myapp.example.com/fullchain.pem \
      -inkey /etc/letsencrypt/live/myapp.example.com/privkey.pem \
      -out keystore.p12 \
      -name socle \
      -passout pass:changeit
    

    3.3 Avec CA interne

    # Générer CSR
    keytool -certreq \
      -alias socle \
      -keystore keystore.p12 \
      -file socle.csr \
      -storepass changeit
    
    # Après signature par la CA, importer le certificat
    keytool -importcert \
      -alias socle \
      -keystore keystore.p12 \
      -file signed-cert.crt \
      -storepass changeit
    
    # Importer la chaîne CA
    keytool -importcert \
      -alias ca-root \
      -keystore keystore.p12 \
      -file ca-root.crt \
      -storepass changeit
    

    4. Configuration avancée

    4.1 Mutual TLS (mTLS)

    server:
      ssl:
        enabled: true
        key-store: ${SSL_KEYSTORE:keystore.p12}
        key-store-password: ${SSL_KEYSTORE_PASSWORD}
        key-store-type: PKCS12
        # Trust store pour vérifier les clients
        trust-store: ${SSL_TRUSTSTORE:truststore.p12}
        trust-store-password: ${SSL_TRUSTSTORE_PASSWORD}
        trust-store-type: PKCS12
        # Exiger certificat client
        client-auth: ${SSL_CLIENT_AUTH:need}  # none, want, need
    

    4.2 Protocoles et Ciphers

    server:
      ssl:
        enabled-protocols: TLSv1.3,TLSv1.2
        ciphers:
          - TLS_AES_256_GCM_SHA384
          - TLS_AES_128_GCM_SHA256
          - TLS_CHACHA20_POLY1305_SHA256
          - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
          - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    

    5. HTTP + HTTPS (dual port)

    5.1 Configuration

    @Configuration
    public class TlsConfiguration {
    
        @Value("${server.http.port:8080}")
        private int httpPort;
    
        @Bean
        public ServletWebServerFactory servletContainer() {
            TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
            tomcat.addAdditionalTomcatConnectors(createHttpConnector());
            return tomcat;
        }
    
        private Connector createHttpConnector() {
            Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
            connector.setScheme("http");
            connector.setPort(httpPort);
            connector.setSecure(false);
            return connector;
        }
    }
    

    5.2 Redirection HTTP → HTTPS

    @Configuration
    public class HttpsRedirectConfiguration {
    
        @Bean
        public TomcatServletWebServerFactory servletContainer() {
            TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
                @Override
                protected void postProcessContext(Context context) {
                    SecurityConstraint securityConstraint = new SecurityConstraint();
                    securityConstraint.setUserConstraint("CONFIDENTIAL");
                    SecurityCollection collection = new SecurityCollection();
                    collection.addPattern("/*");
                    securityConstraint.addCollection(collection);
                    context.addConstraint(securityConstraint);
                }
            };
    
            tomcat.addAdditionalTomcatConnectors(httpToHttpsRedirectConnector());
            return tomcat;
        }
    
        private Connector httpToHttpsRedirectConnector() {
            Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
            connector.setScheme("http");
            connector.setPort(8080);
            connector.setSecure(false);
            connector.setRedirectPort(8443);
            return connector;
        }
    }
    

    6. Client HTTPS

    6.1 OkHttpClient avec TLS

    @Configuration
    public class HttpClientConfiguration {
    
        @Value("${ssl.truststore:#{null}}")
        private Resource trustStore;
    
        @Value("${ssl.truststore-password:changeit}")
        private String trustStorePassword;
    
        @Bean
        public OkHttpClient secureHttpClient() throws Exception {
            OkHttpClient.Builder builder = new OkHttpClient.Builder();
    
            if (trustStore != null && trustStore.exists()) {
                KeyStore ks = KeyStore.getInstance("PKCS12");
                try (InputStream is = trustStore.getInputStream()) {
                    ks.load(is, trustStorePassword.toCharArray());
                }
    
                TrustManagerFactory tmf = TrustManagerFactory.getInstance(
                    TrustManagerFactory.getDefaultAlgorithm());
                tmf.init(ks);
    
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
    
                builder.sslSocketFactory(sslContext.getSocketFactory(),
                    (X509TrustManager) tmf.getTrustManagers()[0]);
            }
    
            return builder
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .build();
        }
    }
    

    6.2 Bypass SSL pour développement (NON RECOMMANDÉ)

    // UNIQUEMENT POUR LE DÉVELOPPEMENT - NE PAS UTILISER EN PRODUCTION
    public OkHttpClient insecureClient() throws Exception {
        TrustManager[] trustAllCerts = new TrustManager[]{
            new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) {}
                public void checkServerTrusted(X509Certificate[] chain, String authType) {}
                public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
            }
        };
    
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustAllCerts, new SecureRandom());
    
        return new OkHttpClient.Builder()
            .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0])
            .hostnameVerifier((hostname, session) -> true)
            .build();
    }
    

    7. Docker avec TLS

    7.1 Dockerfile

    FROM eclipse-temurin:21-jre
    
    WORKDIR /app
    
    # Copier le certificat
    COPY keystore.p12 /app/certs/keystore.p12
    
    # Copier l'application
    COPY target/socle-v004-4.0.0.jar app.jar
    
    ENV SSL_ENABLED=true
    ENV SSL_KEYSTORE=/app/certs/keystore.p12
    
    EXPOSE 8443
    
    ENTRYPOINT ["java", "-jar", "app.jar"]
    

    7.2 docker-compose.yml

    version: '3.8'
    
    services:
      socle-app:
        image: socle-v4:latest
        environment:
          - SSL_ENABLED=true
          - SSL_KEYSTORE=/app/certs/keystore.p12
          - SSL_KEYSTORE_PASSWORD_FILE=/run/secrets/ssl_password
        ports:
          - "8443:8443"
        volumes:
          - ./certs:/app/certs:ro
        secrets:
          - ssl_password
    
    secrets:
      ssl_password:
        file: ./secrets/ssl_password.txt
    

    8. Kubernetes avec TLS

    8.1 Secret pour le certificat

    apiVersion: v1
    kind: Secret
    metadata:
      name: socle-tls
    type: kubernetes.io/tls
    data:
      tls.crt: <base64-encoded-cert>
      tls.key: <base64-encoded-key>
    

    8.2 Ingress avec TLS

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: socle-ingress
      annotations:
        nginx.ingress.kubernetes.io/ssl-redirect: "true"
    spec:
      tls:
        - hosts:
            - socle.example.com
          secretName: socle-tls
      rules:
        - host: socle.example.com
          http:
            paths:
              - path: /
                pathType: Prefix
                backend:
                  service:
                    name: socle-service
                    port:
                      number: 8080
    

    8.3 cert-manager

    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: socle-cert
    spec:
      secretName: socle-tls
      issuerRef:
        name: letsencrypt-prod
        kind: ClusterIssuer
      dnsNames:
        - socle.example.com
    

    9. Vérification

    9.1 Test avec curl

    # Test HTTPS
    curl -v https://localhost:8443/admin/health
    
    # Avec certificat client (mTLS)
    curl -v --cert client.crt --key client.key https://localhost:8443/admin/health
    
    # Ignorer la vérification (dev only)
    curl -vk https://localhost:8443/admin/health
    

    9.2 Test avec openssl

    # Vérifier le certificat du serveur
    openssl s_client -connect localhost:8443 -showcerts
    
    # Vérifier les protocoles supportés
    openssl s_client -connect localhost:8443 -tls1_3
    
    # Vérifier les ciphers
    openssl s_client -connect localhost:8443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384'
    

    10. Troubleshooting

    Erreur: PKIX path building failed

    Le certificat du serveur n’est pas trusté.

    # Importer le certificat dans le truststore Java
    keytool -importcert \
      -alias server-cert \
      -file server.crt \
      -keystore $JAVA_HOME/lib/security/cacerts \
      -storepass changeit
    

    Erreur: Handshake failure

    Incompatibilité de protocole ou cipher.

    # Vérifier les protocoles
    openssl s_client -connect host:port -tls1_2
    openssl s_client -connect host:port -tls1_3
    

    Erreur: Certificate expired

    Renouveler le certificat et recréer le keystore.

    11. Bonnes pratiques

    DO

    • Utiliser TLS 1.2 minimum, TLS 1.3 recommandé
    • Renouveler les certificats avant expiration
    • Utiliser des clés RSA 2048 bits minimum ou ECDSA 256 bits
    • Activer HSTS en production
    • Utiliser cert-manager en Kubernetes

    DON’T

    • Ne pas utiliser de certificats auto-signés en production
    • Ne pas désactiver la vérification des certificats
    • Ne pas stocker les mots de passe en clair
    • Ne pas utiliser TLS 1.0 ou 1.1 (dépréciés)

    12. Références

  • Socle V004 – Données Partagées

    Socle V004 – Données Partagées

    07 – SharedDataRegistry

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    SharedDataRegistry est un registre centralisé pour partager des données entre Workers au sein d’une même instance. Il fournit des opérations atomiques et un système de niveaux de santé.

    Différence avec KvBus

    Aspect SharedDataRegistry KvBus
    Scope Intra-instance Optionnel inter-instances (Redis)
    Performance Ultra rapide (mémoire) Variable (réseau si Redis)
    Types Fortement typés Strings/JSON
    Health levels Oui Non
    Callbacks Oui Non

    2. Interface SharedDataRegistry

    package eu.lmvi.socle.shared;
    
    public interface SharedDataRegistry {
    
        // === Key-Value basique ===
    
        void put(String key, Object value);
        void put(String key, Object value, HealthLevel level);
        Optional<Object> get(String key);
        <T> Optional<T> get(String key, Class<T> type);
        void delete(String key);
        boolean exists(String key);
    
        // === Typed getters ===
    
        Optional<String> getString(String key);
        Optional<Integer> getInt(String key);
        Optional<Long> getLong(String key);
        Optional<Double> getDouble(String key);
        Optional<Boolean> getBoolean(String key);
    
        // === Sequences (compteurs atomiques) ===
    
        void createSequence(String key, long initialValue, HealthLevel level);
        long incrementSequence(String key);
        long incrementSequence(String key, long delta);
        long getSequence(String key);
        void setSequence(String key, long value);
    
        // === Lists ===
    
        <T> void addToList(String key, T item);
        <T> List<T> getList(String key, Class<T> type);
        void clearList(String key);
    
        // === Maps ===
    
        <V> void putInMap(String key, String mapKey, V value);
        <V> Optional<V> getFromMap(String key, String mapKey, Class<V> type);
        <V> Map<String, V> getMap(String key, Class<V> type);
        void removeFromMap(String key, String mapKey);
    
        // === Health ===
    
        HealthLevel getHealthLevel(String key);
        Map<String, HealthLevel> getAllHealthLevels();
        List<String> getUnhealthyKeys();
    
        // === Callbacks ===
    
        void registerCallback(String key, Consumer<Object> callback);
        void unregisterCallback(String key);
    
        // === Introspection ===
    
        Set<String> keys();
        Set<String> keys(String pattern);
        Map<String, Object> getAll();
        int size();
        void clear();
    }
    

    3. Health Levels

    package eu.lmvi.socle.shared;
    
    public enum HealthLevel {
        /**
         * Informatif - pas d'impact sur la santé
         */
        INFO,
    
        /**
         * Normal - contribue à la santé normale
         */
        NORMAL,
    
        /**
         * Important - dégradation si problème
         */
        IMPORTANT,
    
        /**
         * Critique - unhealthy si problème
         */
        CRITICAL
    }
    

    Utilisation dans le Supervisor

    Le Supervisor consulte les HealthLevel pour déterminer l’état de santé global :

    • CRITICAL absent ou invalide → Instance UNHEALTHY
    • IMPORTANT absent ou invalide → Instance DEGRADED
    • NORMAL/INFO → Pas d’impact

    4. Implémentation

    package eu.lmvi.socle.shared;
    
    @Component
    public class InMemorySharedDataRegistry implements SharedDataRegistry {
    
        private final ConcurrentHashMap<String, Entry> store = new ConcurrentHashMap<>();
        private final ConcurrentHashMap<String, Consumer<Object>> callbacks = new ConcurrentHashMap<>();
    
        @Override
        public void put(String key, Object value) {
            put(key, value, HealthLevel.NORMAL);
        }
    
        @Override
        public void put(String key, Object value, HealthLevel level) {
            Entry previous = store.put(key, new Entry(value, level));
            notifyCallback(key, value);
        }
    
        @Override
        public Optional<Object> get(String key) {
            Entry entry = store.get(key);
            return entry != null ? Optional.of(entry.value) : Optional.empty();
        }
    
        @Override
        public <T> Optional<T> get(String key, Class<T> type) {
            return get(key).filter(type::isInstance).map(type::cast);
        }
    
        @Override
        public Optional<String> getString(String key) {
            return get(key).map(Object::toString);
        }
    
        @Override
        public Optional<Integer> getInt(String key) {
            return get(key, Number.class).map(Number::intValue);
        }
    
        @Override
        public Optional<Long> getLong(String key) {
            return get(key, Number.class).map(Number::longValue);
        }
    
        @Override
        public void createSequence(String key, long initialValue, HealthLevel level) {
            store.put(key, new Entry(new AtomicLong(initialValue), level));
        }
    
        @Override
        public long incrementSequence(String key) {
            return incrementSequence(key, 1);
        }
    
        @Override
        public long incrementSequence(String key, long delta) {
            Entry entry = store.get(key);
            if (entry == null || !(entry.value instanceof AtomicLong)) {
                throw new IllegalStateException("Sequence not found: " + key);
            }
            long newValue = ((AtomicLong) entry.value).addAndGet(delta);
            notifyCallback(key, newValue);
            return newValue;
        }
    
        @Override
        public long getSequence(String key) {
            Entry entry = store.get(key);
            if (entry == null || !(entry.value instanceof AtomicLong)) {
                throw new IllegalStateException("Sequence not found: " + key);
            }
            return ((AtomicLong) entry.value).get();
        }
    
        @Override
        public HealthLevel getHealthLevel(String key) {
            Entry entry = store.get(key);
            return entry != null ? entry.level : null;
        }
    
        @Override
        public List<String> getUnhealthyKeys() {
            return store.entrySet().stream()
                .filter(e -> e.getValue().level == HealthLevel.CRITICAL)
                .filter(e -> !isValueHealthy(e.getValue().value))
                .map(Map.Entry::getKey)
                .toList();
        }
    
        @Override
        public void registerCallback(String key, Consumer<Object> callback) {
            callbacks.put(key, callback);
        }
    
        private void notifyCallback(String key, Object value) {
            Consumer<Object> callback = callbacks.get(key);
            if (callback != null) {
                try {
                    callback.accept(value);
                } catch (Exception e) {
                    // Log but don't propagate
                }
            }
        }
    
        private boolean isValueHealthy(Object value) {
            if (value == null) return false;
            if (value instanceof Boolean b) return b;
            if (value instanceof Number n) return n.doubleValue() >= 0;
            return true;
        }
    
        private record Entry(Object value, HealthLevel level) {}
    }
    

    5. Utilisation

    5.1 Injection

    @Service
    public class MonService {
    
        @Autowired
        private SharedDataRegistry registry;
    
        public void process() {
            // ...
        }
    }
    

    5.2 Key-Value simple

    // Stocker
    registry.put("config.maxRetries", 3);
    registry.put("status.lastSync", Instant.now().toString());
    
    // Récupérer
    int maxRetries = registry.getInt("config.maxRetries").orElse(5);
    String lastSync = registry.getString("status.lastSync").orElse("never");
    

    5.3 Avec Health Level

    // Donnée critique - instance unhealthy si absente
    registry.put("database.connected", true, HealthLevel.CRITICAL);
    
    // Donnée importante - instance degraded si absente
    registry.put("cache.available", true, HealthLevel.IMPORTANT);
    
    // Donnée normale
    registry.put("stats.requestsTotal", 0, HealthLevel.NORMAL);
    
    // Donnée informative
    registry.put("info.startTime", Instant.now(), HealthLevel.INFO);
    

    5.4 Sequences (Compteurs)

    // Créer une séquence
    registry.createSequence("orders.processed", 0, HealthLevel.NORMAL);
    
    // Incrémenter
    long count = registry.incrementSequence("orders.processed");
    log.info("Processed order #{}", count);
    
    // Incrémenter avec delta
    long bytes = registry.incrementSequence("bytes.transferred", 1024);
    
    // Lire
    long total = registry.getSequence("orders.processed");
    

    5.5 Listes

    // Ajouter à une liste
    registry.addToList("errors.recent", new ErrorRecord("timeout", Instant.now()));
    registry.addToList("errors.recent", new ErrorRecord("connection", Instant.now()));
    
    // Lire la liste
    List<ErrorRecord> errors = registry.getList("errors.recent", ErrorRecord.class);
    
    // Vider
    registry.clearList("errors.recent");
    

    5.6 Maps

    // Stocker dans une map
    registry.putInMap("workers.status", "worker-1", "RUNNING");
    registry.putInMap("workers.status", "worker-2", "STOPPED");
    
    // Lire une entrée
    Optional<String> status = registry.getFromMap("workers.status", "worker-1", String.class);
    
    // Lire toute la map
    Map<String, String> allStatus = registry.getMap("workers.status", String.class);
    

    5.7 Callbacks

    // Enregistrer un callback
    registry.registerCallback("config.maxRetries", newValue -> {
        log.info("maxRetries changed to: {}", newValue);
        reconfigure((Integer) newValue);
    });
    
    // La modification déclenche le callback
    registry.put("config.maxRetries", 5);  // Callback appelé
    

    6. Patterns courants

    6.1 État de connexion

    @Component
    public class DatabaseWorker implements Worker {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @Override
        public void initialize() {
            registry.put("database.connected", false, HealthLevel.CRITICAL);
        }
    
        @Override
        public void start() {
            try {
                connect();
                registry.put("database.connected", true, HealthLevel.CRITICAL);
            } catch (Exception e) {
                registry.put("database.connected", false, HealthLevel.CRITICAL);
                throw e;
            }
        }
    
        @Override
        public void stop() {
            disconnect();
            registry.put("database.connected", false, HealthLevel.CRITICAL);
        }
    }
    

    6.2 Métriques temps réel

    @Component
    public class MetricsCollector {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @PostConstruct
        public void init() {
            registry.createSequence("metrics.requests.total", 0, HealthLevel.INFO);
            registry.createSequence("metrics.requests.errors", 0, HealthLevel.NORMAL);
            registry.createSequence("metrics.bytes.in", 0, HealthLevel.INFO);
            registry.createSequence("metrics.bytes.out", 0, HealthLevel.INFO);
        }
    
        public void recordRequest(long bytesIn, long bytesOut, boolean success) {
            registry.incrementSequence("metrics.requests.total");
            registry.incrementSequence("metrics.bytes.in", bytesIn);
            registry.incrementSequence("metrics.bytes.out", bytesOut);
    
            if (!success) {
                registry.incrementSequence("metrics.requests.errors");
            }
        }
    
        public Map<String, Object> getMetrics() {
            return Map.of(
                "requests.total", registry.getSequence("metrics.requests.total"),
                "requests.errors", registry.getSequence("metrics.requests.errors"),
                "bytes.in", registry.getSequence("metrics.bytes.in"),
                "bytes.out", registry.getSequence("metrics.bytes.out")
            );
        }
    }
    

    6.3 Circuit Breaker state

    @Component
    public class CircuitBreakerStateManager {
    
        @Autowired
        private SharedDataRegistry registry;
    
        public void updateState(String circuitName, CircuitState state) {
            String key = "circuit." + circuitName + ".state";
            HealthLevel level = state == CircuitState.OPEN
                ? HealthLevel.IMPORTANT
                : HealthLevel.NORMAL;
            registry.put(key, state.name(), level);
        }
    
        public CircuitState getState(String circuitName) {
            return registry.getString("circuit." + circuitName + ".state")
                .map(CircuitState::valueOf)
                .orElse(CircuitState.CLOSED);
        }
    }
    

    6.4 Progress tracking

    @Component
    public class BatchProcessor {
    
        @Autowired
        private SharedDataRegistry registry;
    
        public void processBatch(String batchId, List<Item> items) {
            registry.put("batch." + batchId + ".total", items.size());
            registry.createSequence("batch." + batchId + ".processed", 0, HealthLevel.NORMAL);
    
            for (Item item : items) {
                processItem(item);
                registry.incrementSequence("batch." + batchId + ".processed");
            }
    
            registry.put("batch." + batchId + ".status", "COMPLETED");
        }
    
        public double getProgress(String batchId) {
            int total = registry.getInt("batch." + batchId + ".total").orElse(0);
            if (total == 0) return 0;
    
            long processed = registry.getSequence("batch." + batchId + ".processed");
            return (double) processed / total * 100;
        }
    }
    

    7. Intégration avec Supervisor

    @Component
    public class HealthAggregator {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @Autowired
        private List<Worker> workers;
    
        public HealthStatus aggregateHealth() {
            // Check workers
            boolean allWorkersHealthy = workers.stream().allMatch(Worker::isHealthy);
    
            // Check critical registry entries
            List<String> unhealthyKeys = registry.getUnhealthyKeys();
            boolean hasCriticalFailure = !unhealthyKeys.isEmpty();
    
            if (!allWorkersHealthy || hasCriticalFailure) {
                return HealthStatus.UNHEALTHY;
            }
    
            // Check important entries
            Map<String, HealthLevel> levels = registry.getAllHealthLevels();
            boolean hasImportantFailure = levels.entrySet().stream()
                .filter(e -> e.getValue() == HealthLevel.IMPORTANT)
                .anyMatch(e -> !isHealthy(registry.get(e.getKey())));
    
            if (hasImportantFailure) {
                return HealthStatus.DEGRADED;
            }
    
            return HealthStatus.HEALTHY;
        }
    }
    

    8. Exposition API

    @RestController
    @RequestMapping("/admin/registry")
    public class SharedDataController {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @GetMapping
        public Map<String, Object> getAll() {
            return registry.getAll();
        }
    
        @GetMapping("/{key}")
        public ResponseEntity<?> get(@PathVariable String key) {
            return registry.get(key)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
        }
    
        @GetMapping("/health")
        public Map<String, HealthLevel> getHealthLevels() {
            return registry.getAllHealthLevels();
        }
    
        @GetMapping("/unhealthy")
        public List<String> getUnhealthyKeys() {
            return registry.getUnhealthyKeys();
        }
    }
    

    9. Bonnes pratiques

    Conventions de nommage

    <category>.<subcategory>.<name>
    
    Exemples:
    - database.connected
    - worker.kafka.status
    - metrics.requests.total
    - batch.order-123.progress
    - circuit.external-api.state
    

    DO

    • Utiliser des noms de clés cohérents et hiérarchiques
    • Définir le HealthLevel approprié pour chaque donnée
    • Utiliser les sequences pour les compteurs (thread-safe)
    • Nettoyer les données obsolètes

    DON’T

    • Ne pas stocker de données volumineuses (logs, payloads)
    • Ne pas utiliser pour le stockage persistant (utiliser TechDB)
    • Ne pas créer de nouvelles clés dynamiquement sans contrôle
    • Ne pas oublier que c’est per-instance (pas de sync multi-instances)

    10. Références

  • Socle V004 – TLS/HTTPS

    Socle V004 – TLS/HTTPS

    13 – TLS/HTTPS

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Configuration du TLS/HTTPS pour sécuriser les communications HTTP du Socle V4.

    2. Configuration Spring Boot

    2.1 application.yml

    server:
      port: ${HTTPS_PORT:8443}
      ssl:
        enabled: ${SSL_ENABLED:true}
        key-store: ${SSL_KEYSTORE:classpath:keystore.p12}
        key-store-password: ${SSL_KEYSTORE_PASSWORD:changeit}
        key-store-type: ${SSL_KEYSTORE_TYPE:PKCS12}
        key-alias: ${SSL_KEY_ALIAS:socle}
    

    2.2 Variables d’environnement

    Variable Description Défaut
    SSL_ENABLED Activer SSL false
    SSL_KEYSTORE Chemin du keystore classpath:keystore.p12
    SSL_KEYSTORE_PASSWORD Mot de passe keystore changeit
    SSL_KEYSTORE_TYPE Type de keystore PKCS12
    SSL_KEY_ALIAS Alias de la clé socle

    3. Génération des certificats

    3.1 Certificat auto-signé (développement)

    # Générer un keystore PKCS12 avec certificat auto-signé
    keytool -genkeypair \
      -alias socle \
      -keyalg RSA \
      -keysize 2048 \
      -storetype PKCS12 \
      -keystore keystore.p12 \
      -validity 365 \
      -dname "CN=localhost,OU=Dev,O=MyCompany,L=Paris,C=FR" \
      -storepass changeit \
      -keypass changeit
    
    # Exporter le certificat (pour les clients)
    keytool -exportcert \
      -alias socle \
      -keystore keystore.p12 \
      -storetype PKCS12 \
      -storepass changeit \
      -file socle.crt
    

    3.2 Avec Let’s Encrypt (production)

    # Obtenir le certificat
    certbot certonly --standalone -d myapp.example.com
    
    # Convertir en PKCS12
    openssl pkcs12 -export \
      -in /etc/letsencrypt/live/myapp.example.com/fullchain.pem \
      -inkey /etc/letsencrypt/live/myapp.example.com/privkey.pem \
      -out keystore.p12 \
      -name socle \
      -passout pass:changeit
    

    3.3 Avec CA interne

    # Générer CSR
    keytool -certreq \
      -alias socle \
      -keystore keystore.p12 \
      -file socle.csr \
      -storepass changeit
    
    # Après signature par la CA, importer le certificat
    keytool -importcert \
      -alias socle \
      -keystore keystore.p12 \
      -file signed-cert.crt \
      -storepass changeit
    
    # Importer la chaîne CA
    keytool -importcert \
      -alias ca-root \
      -keystore keystore.p12 \
      -file ca-root.crt \
      -storepass changeit
    

    4. Configuration avancée

    4.1 Mutual TLS (mTLS)

    server:
      ssl:
        enabled: true
        key-store: ${SSL_KEYSTORE:keystore.p12}
        key-store-password: ${SSL_KEYSTORE_PASSWORD}
        key-store-type: PKCS12
        # Trust store pour vérifier les clients
        trust-store: ${SSL_TRUSTSTORE:truststore.p12}
        trust-store-password: ${SSL_TRUSTSTORE_PASSWORD}
        trust-store-type: PKCS12
        # Exiger certificat client
        client-auth: ${SSL_CLIENT_AUTH:need}  # none, want, need
    

    4.2 Protocoles et Ciphers

    server:
      ssl:
        enabled-protocols: TLSv1.3,TLSv1.2
        ciphers:
          - TLS_AES_256_GCM_SHA384
          - TLS_AES_128_GCM_SHA256
          - TLS_CHACHA20_POLY1305_SHA256
          - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
          - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    

    5. HTTP + HTTPS (dual port)

    5.1 Configuration

    @Configuration
    public class TlsConfiguration {
    
        @Value("${server.http.port:8080}")
        private int httpPort;
    
        @Bean
        public ServletWebServerFactory servletContainer() {
            TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
            tomcat.addAdditionalTomcatConnectors(createHttpConnector());
            return tomcat;
        }
    
        private Connector createHttpConnector() {
            Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
            connector.setScheme("http");
            connector.setPort(httpPort);
            connector.setSecure(false);
            return connector;
        }
    }
    

    5.2 Redirection HTTP → HTTPS

    @Configuration
    public class HttpsRedirectConfiguration {
    
        @Bean
        public TomcatServletWebServerFactory servletContainer() {
            TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
                @Override
                protected void postProcessContext(Context context) {
                    SecurityConstraint securityConstraint = new SecurityConstraint();
                    securityConstraint.setUserConstraint("CONFIDENTIAL");
                    SecurityCollection collection = new SecurityCollection();
                    collection.addPattern("/*");
                    securityConstraint.addCollection(collection);
                    context.addConstraint(securityConstraint);
                }
            };
    
            tomcat.addAdditionalTomcatConnectors(httpToHttpsRedirectConnector());
            return tomcat;
        }
    
        private Connector httpToHttpsRedirectConnector() {
            Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
            connector.setScheme("http");
            connector.setPort(8080);
            connector.setSecure(false);
            connector.setRedirectPort(8443);
            return connector;
        }
    }
    

    6. Client HTTPS

    6.1 OkHttpClient avec TLS

    @Configuration
    public class HttpClientConfiguration {
    
        @Value("${ssl.truststore:#{null}}")
        private Resource trustStore;
    
        @Value("${ssl.truststore-password:changeit}")
        private String trustStorePassword;
    
        @Bean
        public OkHttpClient secureHttpClient() throws Exception {
            OkHttpClient.Builder builder = new OkHttpClient.Builder();
    
            if (trustStore != null && trustStore.exists()) {
                KeyStore ks = KeyStore.getInstance("PKCS12");
                try (InputStream is = trustStore.getInputStream()) {
                    ks.load(is, trustStorePassword.toCharArray());
                }
    
                TrustManagerFactory tmf = TrustManagerFactory.getInstance(
                    TrustManagerFactory.getDefaultAlgorithm());
                tmf.init(ks);
    
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
    
                builder.sslSocketFactory(sslContext.getSocketFactory(),
                    (X509TrustManager) tmf.getTrustManagers()[0]);
            }
    
            return builder
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .build();
        }
    }
    

    6.2 Bypass SSL pour développement (NON RECOMMANDÉ)

    // UNIQUEMENT POUR LE DÉVELOPPEMENT - NE PAS UTILISER EN PRODUCTION
    public OkHttpClient insecureClient() throws Exception {
        TrustManager[] trustAllCerts = new TrustManager[]{
            new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) {}
                public void checkServerTrusted(X509Certificate[] chain, String authType) {}
                public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
            }
        };
    
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustAllCerts, new SecureRandom());
    
        return new OkHttpClient.Builder()
            .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0])
            .hostnameVerifier((hostname, session) -> true)
            .build();
    }
    

    7. Docker avec TLS

    7.1 Dockerfile

    FROM eclipse-temurin:21-jre
    
    WORKDIR /app
    
    # Copier le certificat
    COPY keystore.p12 /app/certs/keystore.p12
    
    # Copier l'application
    COPY target/socle-v004-4.0.0.jar app.jar
    
    ENV SSL_ENABLED=true
    ENV SSL_KEYSTORE=/app/certs/keystore.p12
    
    EXPOSE 8443
    
    ENTRYPOINT ["java", "-jar", "app.jar"]
    

    7.2 docker-compose.yml

    version: '3.8'
    
    services:
      socle-app:
        image: socle-v4:latest
        environment:
          - SSL_ENABLED=true
          - SSL_KEYSTORE=/app/certs/keystore.p12
          - SSL_KEYSTORE_PASSWORD_FILE=/run/secrets/ssl_password
        ports:
          - "8443:8443"
        volumes:
          - ./certs:/app/certs:ro
        secrets:
          - ssl_password
    
    secrets:
      ssl_password:
        file: ./secrets/ssl_password.txt
    

    8. Kubernetes avec TLS

    8.1 Secret pour le certificat

    apiVersion: v1
    kind: Secret
    metadata:
      name: socle-tls
    type: kubernetes.io/tls
    data:
      tls.crt: <base64-encoded-cert>
      tls.key: <base64-encoded-key>
    

    8.2 Ingress avec TLS

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: socle-ingress
      annotations:
        nginx.ingress.kubernetes.io/ssl-redirect: "true"
    spec:
      tls:
        - hosts:
            - socle.example.com
          secretName: socle-tls
      rules:
        - host: socle.example.com
          http:
            paths:
              - path: /
                pathType: Prefix
                backend:
                  service:
                    name: socle-service
                    port:
                      number: 8080
    

    8.3 cert-manager

    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: socle-cert
    spec:
      secretName: socle-tls
      issuerRef:
        name: letsencrypt-prod
        kind: ClusterIssuer
      dnsNames:
        - socle.example.com
    

    9. Vérification

    9.1 Test avec curl

    # Test HTTPS
    curl -v https://localhost:8443/admin/health
    
    # Avec certificat client (mTLS)
    curl -v --cert client.crt --key client.key https://localhost:8443/admin/health
    
    # Ignorer la vérification (dev only)
    curl -vk https://localhost:8443/admin/health
    

    9.2 Test avec openssl

    # Vérifier le certificat du serveur
    openssl s_client -connect localhost:8443 -showcerts
    
    # Vérifier les protocoles supportés
    openssl s_client -connect localhost:8443 -tls1_3
    
    # Vérifier les ciphers
    openssl s_client -connect localhost:8443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384'
    

    10. Troubleshooting

    Erreur: PKIX path building failed

    Le certificat du serveur n’est pas trusté.

    # Importer le certificat dans le truststore Java
    keytool -importcert \
      -alias server-cert \
      -file server.crt \
      -keystore $JAVA_HOME/lib/security/cacerts \
      -storepass changeit
    

    Erreur: Handshake failure

    Incompatibilité de protocole ou cipher.

    # Vérifier les protocoles
    openssl s_client -connect host:port -tls1_2
    openssl s_client -connect host:port -tls1_3
    

    Erreur: Certificate expired

    Renouveler le certificat et recréer le keystore.

    11. Bonnes pratiques

    DO

    • Utiliser TLS 1.2 minimum, TLS 1.3 recommandé
    • Renouveler les certificats avant expiration
    • Utiliser des clés RSA 2048 bits minimum ou ECDSA 256 bits
    • Activer HSTS en production
    • Utiliser cert-manager en Kubernetes

    DON’T

    • Ne pas utiliser de certificats auto-signés en production
    • Ne pas désactiver la vérification des certificats
    • Ne pas stocker les mots de passe en clair
    • Ne pas utiliser TLS 1.0 ou 1.1 (dépréciés)

    12. Références

  • Socle V004 – Données Partagées

    Socle V004 – Données Partagées

    07 – SharedDataRegistry

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    SharedDataRegistry est un registre centralisé pour partager des données entre Workers au sein d’une même instance. Il fournit des opérations atomiques et un système de niveaux de santé.

    Différence avec KvBus

    Aspect SharedDataRegistry KvBus
    Scope Intra-instance Optionnel inter-instances (Redis)
    Performance Ultra rapide (mémoire) Variable (réseau si Redis)
    Types Fortement typés Strings/JSON
    Health levels Oui Non
    Callbacks Oui Non

    2. Interface SharedDataRegistry

    package eu.lmvi.socle.shared;
    
    public interface SharedDataRegistry {
    
        // === Key-Value basique ===
    
        void put(String key, Object value);
        void put(String key, Object value, HealthLevel level);
        Optional<Object> get(String key);
        <T> Optional<T> get(String key, Class<T> type);
        void delete(String key);
        boolean exists(String key);
    
        // === Typed getters ===
    
        Optional<String> getString(String key);
        Optional<Integer> getInt(String key);
        Optional<Long> getLong(String key);
        Optional<Double> getDouble(String key);
        Optional<Boolean> getBoolean(String key);
    
        // === Sequences (compteurs atomiques) ===
    
        void createSequence(String key, long initialValue, HealthLevel level);
        long incrementSequence(String key);
        long incrementSequence(String key, long delta);
        long getSequence(String key);
        void setSequence(String key, long value);
    
        // === Lists ===
    
        <T> void addToList(String key, T item);
        <T> List<T> getList(String key, Class<T> type);
        void clearList(String key);
    
        // === Maps ===
    
        <V> void putInMap(String key, String mapKey, V value);
        <V> Optional<V> getFromMap(String key, String mapKey, Class<V> type);
        <V> Map<String, V> getMap(String key, Class<V> type);
        void removeFromMap(String key, String mapKey);
    
        // === Health ===
    
        HealthLevel getHealthLevel(String key);
        Map<String, HealthLevel> getAllHealthLevels();
        List<String> getUnhealthyKeys();
    
        // === Callbacks ===
    
        void registerCallback(String key, Consumer<Object> callback);
        void unregisterCallback(String key);
    
        // === Introspection ===
    
        Set<String> keys();
        Set<String> keys(String pattern);
        Map<String, Object> getAll();
        int size();
        void clear();
    }
    

    3. Health Levels

    package eu.lmvi.socle.shared;
    
    public enum HealthLevel {
        /**
         * Informatif - pas d'impact sur la santé
         */
        INFO,
    
        /**
         * Normal - contribue à la santé normale
         */
        NORMAL,
    
        /**
         * Important - dégradation si problème
         */
        IMPORTANT,
    
        /**
         * Critique - unhealthy si problème
         */
        CRITICAL
    }
    

    Utilisation dans le Supervisor

    Le Supervisor consulte les HealthLevel pour déterminer l’état de santé global :

    • CRITICAL absent ou invalide → Instance UNHEALTHY
    • IMPORTANT absent ou invalide → Instance DEGRADED
    • NORMAL/INFO → Pas d’impact

    4. Implémentation

    package eu.lmvi.socle.shared;
    
    @Component
    public class InMemorySharedDataRegistry implements SharedDataRegistry {
    
        private final ConcurrentHashMap<String, Entry> store = new ConcurrentHashMap<>();
        private final ConcurrentHashMap<String, Consumer<Object>> callbacks = new ConcurrentHashMap<>();
    
        @Override
        public void put(String key, Object value) {
            put(key, value, HealthLevel.NORMAL);
        }
    
        @Override
        public void put(String key, Object value, HealthLevel level) {
            Entry previous = store.put(key, new Entry(value, level));
            notifyCallback(key, value);
        }
    
        @Override
        public Optional<Object> get(String key) {
            Entry entry = store.get(key);
            return entry != null ? Optional.of(entry.value) : Optional.empty();
        }
    
        @Override
        public <T> Optional<T> get(String key, Class<T> type) {
            return get(key).filter(type::isInstance).map(type::cast);
        }
    
        @Override
        public Optional<String> getString(String key) {
            return get(key).map(Object::toString);
        }
    
        @Override
        public Optional<Integer> getInt(String key) {
            return get(key, Number.class).map(Number::intValue);
        }
    
        @Override
        public Optional<Long> getLong(String key) {
            return get(key, Number.class).map(Number::longValue);
        }
    
        @Override
        public void createSequence(String key, long initialValue, HealthLevel level) {
            store.put(key, new Entry(new AtomicLong(initialValue), level));
        }
    
        @Override
        public long incrementSequence(String key) {
            return incrementSequence(key, 1);
        }
    
        @Override
        public long incrementSequence(String key, long delta) {
            Entry entry = store.get(key);
            if (entry == null || !(entry.value instanceof AtomicLong)) {
                throw new IllegalStateException("Sequence not found: " + key);
            }
            long newValue = ((AtomicLong) entry.value).addAndGet(delta);
            notifyCallback(key, newValue);
            return newValue;
        }
    
        @Override
        public long getSequence(String key) {
            Entry entry = store.get(key);
            if (entry == null || !(entry.value instanceof AtomicLong)) {
                throw new IllegalStateException("Sequence not found: " + key);
            }
            return ((AtomicLong) entry.value).get();
        }
    
        @Override
        public HealthLevel getHealthLevel(String key) {
            Entry entry = store.get(key);
            return entry != null ? entry.level : null;
        }
    
        @Override
        public List<String> getUnhealthyKeys() {
            return store.entrySet().stream()
                .filter(e -> e.getValue().level == HealthLevel.CRITICAL)
                .filter(e -> !isValueHealthy(e.getValue().value))
                .map(Map.Entry::getKey)
                .toList();
        }
    
        @Override
        public void registerCallback(String key, Consumer<Object> callback) {
            callbacks.put(key, callback);
        }
    
        private void notifyCallback(String key, Object value) {
            Consumer<Object> callback = callbacks.get(key);
            if (callback != null) {
                try {
                    callback.accept(value);
                } catch (Exception e) {
                    // Log but don't propagate
                }
            }
        }
    
        private boolean isValueHealthy(Object value) {
            if (value == null) return false;
            if (value instanceof Boolean b) return b;
            if (value instanceof Number n) return n.doubleValue() >= 0;
            return true;
        }
    
        private record Entry(Object value, HealthLevel level) {}
    }
    

    5. Utilisation

    5.1 Injection

    @Service
    public class MonService {
    
        @Autowired
        private SharedDataRegistry registry;
    
        public void process() {
            // ...
        }
    }
    

    5.2 Key-Value simple

    // Stocker
    registry.put("config.maxRetries", 3);
    registry.put("status.lastSync", Instant.now().toString());
    
    // Récupérer
    int maxRetries = registry.getInt("config.maxRetries").orElse(5);
    String lastSync = registry.getString("status.lastSync").orElse("never");
    

    5.3 Avec Health Level

    // Donnée critique - instance unhealthy si absente
    registry.put("database.connected", true, HealthLevel.CRITICAL);
    
    // Donnée importante - instance degraded si absente
    registry.put("cache.available", true, HealthLevel.IMPORTANT);
    
    // Donnée normale
    registry.put("stats.requestsTotal", 0, HealthLevel.NORMAL);
    
    // Donnée informative
    registry.put("info.startTime", Instant.now(), HealthLevel.INFO);
    

    5.4 Sequences (Compteurs)

    // Créer une séquence
    registry.createSequence("orders.processed", 0, HealthLevel.NORMAL);
    
    // Incrémenter
    long count = registry.incrementSequence("orders.processed");
    log.info("Processed order #{}", count);
    
    // Incrémenter avec delta
    long bytes = registry.incrementSequence("bytes.transferred", 1024);
    
    // Lire
    long total = registry.getSequence("orders.processed");
    

    5.5 Listes

    // Ajouter à une liste
    registry.addToList("errors.recent", new ErrorRecord("timeout", Instant.now()));
    registry.addToList("errors.recent", new ErrorRecord("connection", Instant.now()));
    
    // Lire la liste
    List<ErrorRecord> errors = registry.getList("errors.recent", ErrorRecord.class);
    
    // Vider
    registry.clearList("errors.recent");
    

    5.6 Maps

    // Stocker dans une map
    registry.putInMap("workers.status", "worker-1", "RUNNING");
    registry.putInMap("workers.status", "worker-2", "STOPPED");
    
    // Lire une entrée
    Optional<String> status = registry.getFromMap("workers.status", "worker-1", String.class);
    
    // Lire toute la map
    Map<String, String> allStatus = registry.getMap("workers.status", String.class);
    

    5.7 Callbacks

    // Enregistrer un callback
    registry.registerCallback("config.maxRetries", newValue -> {
        log.info("maxRetries changed to: {}", newValue);
        reconfigure((Integer) newValue);
    });
    
    // La modification déclenche le callback
    registry.put("config.maxRetries", 5);  // Callback appelé
    

    6. Patterns courants

    6.1 État de connexion

    @Component
    public class DatabaseWorker implements Worker {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @Override
        public void initialize() {
            registry.put("database.connected", false, HealthLevel.CRITICAL);
        }
    
        @Override
        public void start() {
            try {
                connect();
                registry.put("database.connected", true, HealthLevel.CRITICAL);
            } catch (Exception e) {
                registry.put("database.connected", false, HealthLevel.CRITICAL);
                throw e;
            }
        }
    
        @Override
        public void stop() {
            disconnect();
            registry.put("database.connected", false, HealthLevel.CRITICAL);
        }
    }
    

    6.2 Métriques temps réel

    @Component
    public class MetricsCollector {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @PostConstruct
        public void init() {
            registry.createSequence("metrics.requests.total", 0, HealthLevel.INFO);
            registry.createSequence("metrics.requests.errors", 0, HealthLevel.NORMAL);
            registry.createSequence("metrics.bytes.in", 0, HealthLevel.INFO);
            registry.createSequence("metrics.bytes.out", 0, HealthLevel.INFO);
        }
    
        public void recordRequest(long bytesIn, long bytesOut, boolean success) {
            registry.incrementSequence("metrics.requests.total");
            registry.incrementSequence("metrics.bytes.in", bytesIn);
            registry.incrementSequence("metrics.bytes.out", bytesOut);
    
            if (!success) {
                registry.incrementSequence("metrics.requests.errors");
            }
        }
    
        public Map<String, Object> getMetrics() {
            return Map.of(
                "requests.total", registry.getSequence("metrics.requests.total"),
                "requests.errors", registry.getSequence("metrics.requests.errors"),
                "bytes.in", registry.getSequence("metrics.bytes.in"),
                "bytes.out", registry.getSequence("metrics.bytes.out")
            );
        }
    }
    

    6.3 Circuit Breaker state

    @Component
    public class CircuitBreakerStateManager {
    
        @Autowired
        private SharedDataRegistry registry;
    
        public void updateState(String circuitName, CircuitState state) {
            String key = "circuit." + circuitName + ".state";
            HealthLevel level = state == CircuitState.OPEN
                ? HealthLevel.IMPORTANT
                : HealthLevel.NORMAL;
            registry.put(key, state.name(), level);
        }
    
        public CircuitState getState(String circuitName) {
            return registry.getString("circuit." + circuitName + ".state")
                .map(CircuitState::valueOf)
                .orElse(CircuitState.CLOSED);
        }
    }
    

    6.4 Progress tracking

    @Component
    public class BatchProcessor {
    
        @Autowired
        private SharedDataRegistry registry;
    
        public void processBatch(String batchId, List<Item> items) {
            registry.put("batch." + batchId + ".total", items.size());
            registry.createSequence("batch." + batchId + ".processed", 0, HealthLevel.NORMAL);
    
            for (Item item : items) {
                processItem(item);
                registry.incrementSequence("batch." + batchId + ".processed");
            }
    
            registry.put("batch." + batchId + ".status", "COMPLETED");
        }
    
        public double getProgress(String batchId) {
            int total = registry.getInt("batch." + batchId + ".total").orElse(0);
            if (total == 0) return 0;
    
            long processed = registry.getSequence("batch." + batchId + ".processed");
            return (double) processed / total * 100;
        }
    }
    

    7. Intégration avec Supervisor

    @Component
    public class HealthAggregator {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @Autowired
        private List<Worker> workers;
    
        public HealthStatus aggregateHealth() {
            // Check workers
            boolean allWorkersHealthy = workers.stream().allMatch(Worker::isHealthy);
    
            // Check critical registry entries
            List<String> unhealthyKeys = registry.getUnhealthyKeys();
            boolean hasCriticalFailure = !unhealthyKeys.isEmpty();
    
            if (!allWorkersHealthy || hasCriticalFailure) {
                return HealthStatus.UNHEALTHY;
            }
    
            // Check important entries
            Map<String, HealthLevel> levels = registry.getAllHealthLevels();
            boolean hasImportantFailure = levels.entrySet().stream()
                .filter(e -> e.getValue() == HealthLevel.IMPORTANT)
                .anyMatch(e -> !isHealthy(registry.get(e.getKey())));
    
            if (hasImportantFailure) {
                return HealthStatus.DEGRADED;
            }
    
            return HealthStatus.HEALTHY;
        }
    }
    

    8. Exposition API

    @RestController
    @RequestMapping("/admin/registry")
    public class SharedDataController {
    
        @Autowired
        private SharedDataRegistry registry;
    
        @GetMapping
        public Map<String, Object> getAll() {
            return registry.getAll();
        }
    
        @GetMapping("/{key}")
        public ResponseEntity<?> get(@PathVariable String key) {
            return registry.get(key)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
        }
    
        @GetMapping("/health")
        public Map<String, HealthLevel> getHealthLevels() {
            return registry.getAllHealthLevels();
        }
    
        @GetMapping("/unhealthy")
        public List<String> getUnhealthyKeys() {
            return registry.getUnhealthyKeys();
        }
    }
    

    9. Bonnes pratiques

    Conventions de nommage

    <category>.<subcategory>.<name>
    
    Exemples:
    - database.connected
    - worker.kafka.status
    - metrics.requests.total
    - batch.order-123.progress
    - circuit.external-api.state
    

    DO

    • Utiliser des noms de clés cohérents et hiérarchiques
    • Définir le HealthLevel approprié pour chaque donnée
    • Utiliser les sequences pour les compteurs (thread-safe)
    • Nettoyer les données obsolètes

    DON’T

    • Ne pas stocker de données volumineuses (logs, payloads)
    • Ne pas utiliser pour le stockage persistant (utiliser TechDB)
    • Ne pas créer de nouvelles clés dynamiquement sans contrôle
    • Ne pas oublier que c’est per-instance (pas de sync multi-instances)

    10. Références

  • Socle V004 – Status Dashboard

    Socle V004 – Status Dashboard

    27 – Status Dashboard

    Version : 4.0.0 Package : eu.lmvi.socle.worker.status

    Introduction

    Le StatusDashboardWorker est un Worker integre au Socle V004 qui expose un dashboard HTML de supervision sur un port dedie. Il permet de visualiser en temps reel l’etat de l’application et de tous les Workers.

    Caracteristiques

    • Automatique : Active par defaut, aucun code a ajouter
    • Port dedie : 9374 (configurable)
    • Dashboard HTML : Interface web avec rafraichissement AJAX partiel
    • API JSON : Endpoints REST pour integration
    • Metriques d’activite : Throughput, duree, charge relative
    • Animation visuelle : Mise en evidence des valeurs modifiees

    Acces au Dashboard

    Une fois l’application demarree, le dashboard est accessible sur :

    http://localhost:9374/
    

    Configuration

    application.yml

    socle:
      status_dashboard:
        # Activer/desactiver le dashboard (defaut: true)
        enabled: true
    
        # Port du serveur HTTP (defaut: 9374)
        port: 9374
    
        # Adresse de bind (vide = toutes les interfaces)
        bind_address: ""
    
        # Intervalle de rafraichissement HTML en secondes (defaut: 5)
        refresh_interval: 5
    
        # Fenetre de calcul des metriques en secondes (defaut: 60)
        metrics_window: 60
    
        # Limite de requetes par seconde (defaut: 10)
        max_requests_per_second: 10
    
        # Activer l'API JSON (defaut: true)
        api_enabled: true
    

    Variables d’environnement

    Variable Description Defaut
    STATUS_DASHBOARD_ENABLED Activer le dashboard true
    STATUS_DASHBOARD_PORT Port HTTP 9374
    STATUS_DASHBOARD_BIND Adresse de bind (vide)
    STATUS_DASHBOARD_REFRESH Refresh interval (sec) 5
    STATUS_DASHBOARD_METRICS_WINDOW Fenetre metriques (sec) 60
    STATUS_DASHBOARD_MAX_RPS Max requetes/sec 10
    STATUS_DASHBOARD_API_ENABLED Activer API JSON true

    Rafraichissement AJAX

    Le dashboard utilise JavaScript pour mettre a jour uniquement les valeurs qui changent, sans recharger la page entiere.

    Fonctionnement

    1. Chargement initial : La page HTML complete est servie
    2. Rafraichissement periodique : JavaScript appelle /api/status et /api/workers
    3. Mise a jour selective : Seuls les elements dont la valeur a change sont modifies
    4. Animation visuelle : Les valeurs modifiees sont brievement mises en surbrillance (effet cyan)

    Avantages

    • Pas de rechargement complet de la page
    • Experience utilisateur fluide
    • Reduction de la bande passante
    • Conservation de l’etat de scroll

    Configuration de l’intervalle

    L’intervalle de rafraichissement AJAX correspond a refresh_interval :

    socle:
      status_dashboard:
        refresh_interval: 3  # Rafraichissement toutes les 3 secondes
    

    Endpoints HTTP

    Dashboard HTML

    Endpoint Methode Description
    / GET Page HTML du dashboard
    /index.html GET Alias pour /

    Health Check

    Endpoint Methode Description
    /health GET Status UP/DOWN en JSON

    Exemple de reponse :

    {"status":"UP"}
    

    API JSON

    Endpoint Methode Description
    /api/status GET Status global de l’application
    /api/workers GET Liste de tous les workers avec metriques
    /api/workers/{name} GET Metriques d’un worker specifique

    Donnees affichees

    Section : Status Global

    Donnee Description
    MOP State Etat du MainOrchestratorProcess (RUNNING, DRAINING, etc.)
    Uptime Temps depuis le demarrage
    Workers Health Nombre de workers healthy / total
    Total Activity Throughput agrege (ops/sec)

    Section : Worker Activity

    Barres visuelles montrant la charge relative de chaque Worker :

    cdc_kafka_worker    ████████████████████░░░░  85%  [HOT]
    http_worker         ██████████████░░░░░░░░░░  58%
    rule_engine         ████████░░░░░░░░░░░░░░░░  32%
    control_worker      ███░░░░░░░░░░░░░░░░░░░░░  12%
    maintenance_worker  █░░░░░░░░░░░░░░░░░░░░░░░   2%  [IDLE]
    

    Tags :

    • [HOT] : Worker avec charge > 80%
    • [IDLE] : Worker inactif
    • [PASSIVE] : Worker event-driven sans activite

    Section : Workers Detail

    Tableau detaille avec :

    Colonne Description
    Name Nom du worker
    State Running / Stopped
    Health OK / FAIL
    Mode PASSIVE / CRON / INTERVAL
    Executions Nombre total d’executions doWork()
    Avg Duration Duree moyenne d’execution
    Throughput Operations par seconde
    Last Activity Temps depuis derniere activite
    Errors Nombre d’erreurs

    Exemples API JSON

    GET /api/status

    {
      "timestamp": "2026-01-12T17:34:56.789Z",
      "application": {
        "name": "my-app",
        "environment": "PROD",
        "version": "4.0.0"
      },
      "mop": {
        "state": "RUNNING",
        "uptime_ms": 9252000,
        "uptime_human": "2h 34m 12s"
      },
      "workers": {
        "total": 6,
        "healthy": 6,
        "running": 6
      },
      "activity": {
        "total_throughput": 847.3,
        "metrics_window_sec": 60
      }
    }
    

    GET /api/workers

    {
      "timestamp": "2026-01-12T17:34:56.789Z",
      "workers": [
        {
          "name": "cdc_kafka_worker",
          "state": "running",
          "healthy": true,
          "schedule": "PASSIVE",
          "metrics": {
            "execution_count": 12847,
            "total_duration_ms": 29548,
            "avg_duration_ms": 2.3,
            "last_execution": "2026-01-12T17:34:55.123Z",
            "throughput_per_sec": 721.4,
            "errors_count": 3,
            "messages_processed": 45230
          },
          "relative_load": 0.85
        }
      ]
    }
    

    Securite

    Bind localhost uniquement (production)

    Pour limiter l’acces au dashboard en production :

    socle:
      status_dashboard:
        bind_address: "127.0.0.1"
    

    Desactiver en production

    socle:
      status_dashboard:
        enabled: false
    

    Ou via variable d’environnement :

    export STATUS_DASHBOARD_ENABLED=false
    

    Architecture

    ┌─────────────────────────────────────────────────────────────┐
    │                  StatusDashboardWorker                      │
    │                                                             │
    │  ┌─────────────────────┐    ┌─────────────────────────┐    │
    │  │ WorkerActivityTracker│    │ DashboardHtmlRenderer   │    │
    │  │ (collecte metriques) │    │ (genere HTML)           │    │
    │  └──────────┬──────────┘    └────────────┬────────────┘    │
    │             │                            │                  │
    │             └──────────┬─────────────────┘                  │
    │                        │                                    │
    │              ┌─────────▼─────────┐                          │
    │              │   MiniHttpServer  │                          │
    │              │   (port 9374)     │                          │
    │              └─────────┬─────────┘                          │
    └────────────────────────┼────────────────────────────────────┘
                             │
                             ▼
                        Browser / curl
    

    Composants

    Composant Responsabilite
    StatusDashboardWorker Worker principal, orchestre le dashboard
    WorkerActivityTracker Collecte et agregation des metriques
    MiniHttpServer Serveur HTTP leger (ServerSocket)
    DashboardHtmlRenderer Generation du HTML avec CSS inline

    Integration avec Monitoring

    Le dashboard peut etre integre avec des outils de monitoring existants :

    Prometheus / Grafana

    Utilisez l’endpoint /api/status pour collecter les metriques :

    # prometheus.yml
    scrape_configs:
      - job_name: 'socle-status'
        metrics_path: /api/status
        static_configs:
          - targets: ['localhost:9374']
    

    Health Checks (Kubernetes)

    # deployment.yaml
    livenessProbe:
      httpGet:
        path: /health
        port: 9374
      initialDelaySeconds: 30
      periodSeconds: 10
    

    Convention des Stats Workers

    Pour que le dashboard affiche correctement les metriques, les workers doivent exposer des cles standardisees dans getStats().

    Cles requises par WorkerActivityTracker

    Cle Type Utilisation
    state String Affichage Running/Stopped
    execution_count long Colonne Executions
    errors_count long Colonne Errors
    last_execution String/long Colonne Last Activity
    schedule String Colonne Mode

    Cles optionnelles

    Cle Type Utilisation
    total_duration_ms long Calcul Avg Duration
    avg_duration_ms double Colonne Avg Duration (prioritaire)
    throughput_per_sec double Colonne Throughput
    messages_processed long Fallback pour execution_count

    Note : Les workers heritant de AbstractEventDrivenWorker exposent automatiquement ces cles depuis la version 4.0.1.

    Voir 05-WORKERS.md section 12 pour les details d’implementation.

    Troubleshooting

    Le dashboard ne demarre pas

    Cause possible : Port deja utilise

    Solution :

    socle:
      status_dashboard:
        port: 9375  # Changer le port
    

    Metriques a zero

    Cause possible : Les workers sont en mode PASSIVE et n’ont pas encore traite d’evenements

    Solution : Normal pour les workers event-driven. Les metriques apparaitront des que des evenements seront traites.

    Dashboard lent

    Cause possible : Trop de workers ou refresh trop frequent

    Solution :

    socle:
      status_dashboard:
        refresh_interval: 10  # Augmenter l'intervalle
    

    Voir aussi

    Socle V004 – Status Dashboard