Auteur/autrice : jmh

  • Socle V004 – Client Authentification

    Socle V004 – Client Authentification

    23 – Client Authentification JWT (Nouveauté V4)

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Le SocleAuthClient est un client d’authentification JWT intégré au Socle V4 pour communiquer avec les services centraux (LogHub, Registry, etc.).

    Pattern d’authentification

    ┌─────────────────┐                    ┌─────────────────┐
    │   Application   │                    │  Auth Server    │
    │   Socle V4      │                    │  (central)      │
    └────────┬────────┘                    └────────┬────────┘
             │                                      │
             │  1. POST /auth/login                 │
             │     {sourceName, apiKey}             │
             │─────────────────────────────────────►│
             │                                      │
             │  2. {accessToken, refreshToken}      │
             │◄─────────────────────────────────────│
             │                                      │
             │  3. Requêtes avec Bearer token       │
             │  Authorization: Bearer <accessToken> │
             │─────────────────────────────────────►│ Services
             │                                      │
             │  4. POST /auth/refresh (auto)        │
             │     {refreshToken}                   │
             │─────────────────────────────────────►│
             │                                      │
             │  5. {accessToken (new)}              │
             │◄─────────────────────────────────────│
    

    2. Configuration

    2.1 application.yml

    socle:
      auth:
        enabled: ${AUTH_ENABLED:false}
        server-url: ${AUTH_SERVER_URL:https://auth.lmvi.org}
        source-name: ${SOURCE_NAME:${socle.app_name}}
        api-key: ${API_KEY:}
        access-token-buffer-seconds: 60
        connect-timeout-ms: 10000
        read-timeout-ms: 30000
    

    2.2 Variables d’environnement

    Variable Description Défaut
    AUTH_ENABLED Activer l’authentification false
    AUTH_SERVER_URL URL du serveur d’auth
    SOURCE_NAME Identifiant du client ${APP_NAME}
    API_KEY Clé API (secret)

    3. Interface SocleAuthClient

    package eu.lmvi.socle.client.auth;
    
    /**
     * Client d'authentification Socle V4
     */
    public interface SocleAuthClient {
    
        /**
         * Login initial avec API Key
         * @return Tokens d'accès et de refresh
         * @throws AuthenticationException si échec
         */
        AuthTokens login() throws AuthenticationException;
    
        /**
         * Refresh du token d'accès
         * @param refreshToken Token de refresh
         * @return Nouveaux tokens
         * @throws AuthenticationException si échec
         */
        AuthTokens refresh(String refreshToken) throws AuthenticationException;
    
        /**
         * Obtenir un token d'accès valide (avec refresh auto si nécessaire)
         * @return Token d'accès valide
         * @throws AuthenticationException si échec
         */
        String getValidAccessToken() throws AuthenticationException;
    
        /**
         * Vérifie si le client est authentifié
         * @return true si un token valide existe
         */
        boolean isAuthenticated();
    
        /**
         * Invalide les tokens courants
         */
        void logout();
    }
    

    4. DTOs

    4.1 AuthTokens

    package eu.lmvi.socle.client.auth;
    
    public record AuthTokens(
        String accessToken,
        String refreshToken,
        Instant accessTokenExpiry,
        Instant refreshTokenExpiry
    ) {
        public boolean isAccessTokenExpired() {
            return Instant.now().isAfter(accessTokenExpiry);
        }
    
        public boolean isAccessTokenExpiringSoon(int bufferSeconds) {
            return Instant.now().plusSeconds(bufferSeconds).isAfter(accessTokenExpiry);
        }
    
        public boolean isRefreshTokenExpired() {
            return Instant.now().isAfter(refreshTokenExpiry);
        }
    }
    

    4.2 LoginRequest / LoginResponse

    // Request
    public record LoginRequest(
        String sourceName,
        String apiKey
    ) {}
    
    // Response
    public record LoginResponse(
        String accessToken,
        String refreshToken,
        int expiresIn,        // secondes
        int refreshExpiresIn  // secondes
    ) {}
    

    5. Implémentation AuthTokenManager

    package eu.lmvi.socle.client.auth;
    
    @Component
    @ConditionalOnProperty(name = "socle.auth.enabled", havingValue = "true")
    public class AuthTokenManager implements SocleAuthClient {
    
        private static final Logger log = LoggerFactory.getLogger(AuthTokenManager.class);
    
        private final SocleConfiguration config;
        private final OkHttpClient httpClient;
        private final ObjectMapper objectMapper;
    
        private volatile AuthTokens currentTokens;
        private final ReentrantLock refreshLock = new ReentrantLock();
    
        public AuthTokenManager(SocleConfiguration config) {
            this.config = config;
            this.objectMapper = new ObjectMapper();
            this.objectMapper.registerModule(new JavaTimeModule());
    
            this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(config.getAuthConnectTimeoutMs(), TimeUnit.MILLISECONDS)
                .readTimeout(config.getAuthReadTimeoutMs(), TimeUnit.MILLISECONDS)
                .build();
        }
    
        @Override
        public AuthTokens login() throws AuthenticationException {
            log.info("Login to auth server: {}", config.getAuthServerUrl());
    
            LoginRequest request = new LoginRequest(
                config.getSourceName(),
                config.getApiKey()
            );
    
            try {
                String json = objectMapper.writeValueAsString(request);
    
                Request httpRequest = new Request.Builder()
                    .url(config.getAuthServerUrl() + "/api/v1/auth/login")
                    .post(RequestBody.create(json, MediaType.parse("application/json")))
                    .build();
    
                try (Response response = httpClient.newCall(httpRequest).execute()) {
                    if (!response.isSuccessful()) {
                        throw new AuthenticationException("Login failed: " + response.code());
                    }
    
                    LoginResponse loginResponse = objectMapper.readValue(
                        response.body().string(),
                        LoginResponse.class
                    );
    
                    currentTokens = new AuthTokens(
                        loginResponse.accessToken(),
                        loginResponse.refreshToken(),
                        Instant.now().plusSeconds(loginResponse.expiresIn()),
                        Instant.now().plusSeconds(loginResponse.refreshExpiresIn())
                    );
    
                    log.info("Login successful, token expires in {} seconds", loginResponse.expiresIn());
                    return currentTokens;
                }
            } catch (IOException e) {
                throw new AuthenticationException("Login failed", e);
            }
        }
    
        @Override
        public AuthTokens refresh(String refreshToken) throws AuthenticationException {
            log.debug("Refreshing access token");
    
            RefreshRequest request = new RefreshRequest(refreshToken);
    
            try {
                String json = objectMapper.writeValueAsString(request);
    
                Request httpRequest = new Request.Builder()
                    .url(config.getAuthServerUrl() + "/api/v1/auth/refresh")
                    .post(RequestBody.create(json, MediaType.parse("application/json")))
                    .build();
    
                try (Response response = httpClient.newCall(httpRequest).execute()) {
                    if (!response.isSuccessful()) {
                        // Refresh failed, need to re-login
                        log.warn("Refresh failed, attempting re-login");
                        return login();
                    }
    
                    RefreshResponse refreshResponse = objectMapper.readValue(
                        response.body().string(),
                        RefreshResponse.class
                    );
    
                    currentTokens = new AuthTokens(
                        refreshResponse.accessToken(),
                        currentTokens.refreshToken(),  // Keep same refresh token
                        Instant.now().plusSeconds(refreshResponse.expiresIn()),
                        currentTokens.refreshTokenExpiry()
                    );
    
                    log.debug("Token refreshed, new expiry in {} seconds", refreshResponse.expiresIn());
                    return currentTokens;
                }
            } catch (IOException e) {
                throw new AuthenticationException("Refresh failed", e);
            }
        }
    
        @Override
        public String getValidAccessToken() throws AuthenticationException {
            // First time - login
            if (currentTokens == null) {
                login();
                return currentTokens.accessToken();
            }
    
            // Refresh token expired - need full re-login
            if (currentTokens.isRefreshTokenExpired()) {
                log.info("Refresh token expired, re-login required");
                login();
                return currentTokens.accessToken();
            }
    
            // Access token expiring soon - refresh
            int bufferSeconds = config.getAccessTokenBufferSeconds();
            if (currentTokens.isAccessTokenExpiringSoon(bufferSeconds)) {
                refreshLock.lock();
                try {
                    // Double-check after acquiring lock
                    if (currentTokens.isAccessTokenExpiringSoon(bufferSeconds)) {
                        refresh(currentTokens.refreshToken());
                    }
                } finally {
                    refreshLock.unlock();
                }
            }
    
            return currentTokens.accessToken();
        }
    
        @Override
        public boolean isAuthenticated() {
            return currentTokens != null && !currentTokens.isAccessTokenExpired();
        }
    
        @Override
        public void logout() {
            currentTokens = null;
            log.info("Logged out");
        }
    }
    

    6. Utilisation

    6.1 Injection

    @Service
    public class MonService {
    
        @Autowired(required = false)
        private SocleAuthClient authClient;
    
        public void callSecuredApi() throws Exception {
            if (authClient == null || !authClient.isAuthenticated()) {
                throw new IllegalStateException("Auth not configured");
            }
    
            String token = authClient.getValidAccessToken();
    
            // Utiliser le token
            Request request = new Request.Builder()
                .url("https://api.mycompany.com/secured")
                .header("Authorization", "Bearer " + token)
                .build();
    
            // ...
        }
    }
    

    6.2 Intégration avec LogForwarder

    // Dans HttpLogTransport
    public class HttpLogTransport implements LogTransport {
    
        private final SocleAuthClient authClient;
    
        @Override
        public void send(List<LogEntry> entries) throws Exception {
            String token = authClient.getValidAccessToken();
    
            Request request = new Request.Builder()
                .url(logHubUrl)
                .header("Authorization", "Bearer " + token)
                .post(RequestBody.create(toJson(entries), JSON))
                .build();
    
            // ...
        }
    }
    

    6.3 Intégration avec MOP

    // Dans MainOrchestratorProcess.start()
    if (authClient != null && config.isAuthEnabled()) {
        log.info("[step:auth] Login auprès du serveur d'auth");
        try {
            authClient.login();
        } catch (AuthenticationException e) {
            log.error("Auth failed, continuing without auth", e);
        }
    }
    

    7. Gestion des erreurs

    7.1 AuthenticationException

    public class AuthenticationException extends Exception {
        public AuthenticationException(String message) {
            super(message);
        }
    
        public AuthenticationException(String message, Throwable cause) {
            super(message, cause);
        }
    }
    

    7.2 Retry automatique

    Le AuthTokenManager gère automatiquement :

    • Le refresh avant expiration
    • Le re-login si le refresh échoue
    • Le re-login si le refresh token expire

    7.3 Fallback sans auth

    if (authClient == null) {
        log.warn("Auth not configured, proceeding without authentication");
        // Continuer sans auth (pour dev local)
    }
    

    8. Sécurité

    8.1 Stockage de l’API Key

    # NE PAS mettre dans le code
    # Utiliser des variables d'environnement
    export API_KEY="xxx-secret-key"
    
    # Ou un gestionnaire de secrets
    # Kubernetes Secret, AWS Secrets Manager, etc.
    

    8.2 Tokens en mémoire

    Les tokens sont stockés en mémoire uniquement :

    • Jamais persistés sur disque
    • Invalidés au restart
    • Thread-safe

    8.3 HTTPS obligatoire

    socle:
      auth:
        server-url: https://auth.mycompany.com  # HTTPS obligatoire
    

    9. Monitoring

    9.1 Métriques

    // Exposées via /metrics
    socle_auth_login_total          # Nombre de logins
    socle_auth_login_errors_total   # Erreurs de login
    socle_auth_refresh_total        # Nombre de refresh
    socle_auth_token_expiry_seconds # Temps avant expiration
    

    9.2 Logs

    INFO  - Login to auth server: https://auth.lmvi.org
    INFO  - Login successful, token expires in 900 seconds
    DEBUG - Refreshing access token
    DEBUG - Token refreshed, new expiry in 900 seconds
    WARN  - Refresh failed, attempting re-login
    

    10. Troubleshooting

    Connection refused

    AuthenticationException: Login failed: Connection refused
    

    Vérifier :

    • AUTH_SERVER_URL est correct
    • Le serveur d’auth est accessible
    • Les ports sont ouverts

    Invalid API Key

    AuthenticationException: Login failed: 401
    

    Vérifier :

    • API_KEY est correct
    • SOURCE_NAME est enregistré côté serveur

    Token expired

    Si les tokens expirent trop vite :

    • Vérifier l’horloge système (NTP)
    • Augmenter access-token-buffer-seconds

    11. Références

  • Socle V004 – KV-Bus

    Socle V004 – KV-Bus

    06 – KvBus (Key-Value Bus)

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

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

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

    Caractéristiques

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

    2. Configuration

    2.1 application.yml

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

    2.2 Variables d’environnement

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

    3. Interface KvBus

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

    4. Implémentation InMemoryKvBus

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

    5. Implémentation RedisKvBus

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

    6. Utilisation

    6.1 Injection

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

    6.2 CRUD basique

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

    6.3 JSON

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

    6.4 Compteurs atomiques

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

    6.5 Lock distribué (Redis)

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

    6.6 Cache avec TTL

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

    7. Patterns avancés

    7.1 Rate limiting

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

    7.2 Session management

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

    7.3 Feature flags

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

    8. KvBus vs TechDB (V4)

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

    Règle de choix

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

    9. Monitoring

    9.1 Métriques

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

    9.2 Health Check

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

    10. Bonnes pratiques

    DO

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

    DON’T

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

    11. Références

  • Socle V004 – 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 – 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 – 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 – Introduction

    Socle V004 – Introduction

    01 – Introduction au Socle V4

    Version : 4.0.0 Date : 2025-01-25

    1. Qu’est-ce que le Socle V4 ?

    Le Socle V4 est un framework Java de grade production construit sur Spring Boot 3.2.1 qui implémente le pattern MOP (Main Orchestrator Process). Il fournit une base solide pour construire des applications d’entreprise robustes et observables.

    Évolution depuis V3

    Le Socle V4 conserve et étend l’architecture V3 en ajoutant :

    Nouveauté V4 Description
    H2 TechDB Base embarquée pour état technique (remplace Nitrite)
    Log4j2 Framework logging haute performance (remplace Logback)
    LogForwarder Centralisation des logs vers LogHub (HTTP/NATS)
    SocleAuthClient Client authentification JWT
    WorkerRegistryClient Auto-enregistrement des workers
    StatusDashboard Dashboard HTML de supervision temps réel (port 9374)
    Pipeline V2 Pipeline asynchrone avec garantie at-least-once (Queue/Claim/Ack)

    2. Philosophie « MOP Pilote Tout »

    Le Main Orchestrator Process est le cœur du framework :

    ┌─────────────────────────────────────────────────────────────────┐
    │                           MOP                                    │
    │  - Orchestre tous les Workers                                   │
    │  - Gère le lifecycle (start/stop)                               │
    │  - Appelle doWork() automatiquement                             │
    │  - Garantit le shutdown gracieux                                │
    └─────────────────────────────────────────────────────────────────┘
                                  │
             ┌────────────────────┼────────────────────┐
             ▼                    ▼                    ▼
        ┌─────────┐         ┌─────────┐         ┌─────────┐
        │ Worker  │         │ Worker  │         │  HTTP   │
        │ Métier  │         │ Métier  │         │ Worker  │
        └─────────┘         └─────────┘         └─────────┘
    

    Principes clés

    1. Orchestration centralisée : Le MOP contrôle tout le lifecycle
    2. Démarrage ordonné : Workers par priorité (petit → grand), HTTP en dernier
    3. Arrêt gracieux : HTTP d’abord (drain), puis Workers
    4. Scheduling automatique : doWork() appelé selon cron ou interval

    3. Les 4 principes fondamentaux V4

    3.1 Portabilité

    • Fonctionne sur ARM/AMD64, Linux/macOS
    • Aucune dépendance serveur externe obligatoire
    • Base H2 embarquée pour l’état technique

    3.2 Sécurité

    • Aucun port entrant sur les NUC/agents
    • Communication sortante uniquement (HTTP/NATS)
    • Authentification JWT pour les services centraux

    3.3 Observabilité

    • Logs centralisés via LogForwarder
    • Corrélation par correlationId / execId
    • Suivi des workers via Registry

    3.4 Standardisation

    • Même authentification partout
    • Même format de logs
    • Même enregistrement des workers

    4. Stack technique

    Composant Version Usage
    Java 21 LTS Runtime
    Spring Boot 3.2.1 Framework
    Log4j2 2.22.1 Logging (nouveau V4)
    LMAX Disruptor 4.0.0 AsyncLoggers
    H2 2.2.x Base technique embarquée (nouveau V4)
    Kafka 3.6.0 Messaging
    NATS 2.17.0 Messaging
    Redisson 3.24.3 Redis client
    OkHttp 4.12.0 HTTP client
    Micrometer 1.12.0 Metrics

    5. Composants du Socle

    Composants V3 (conservés)

    Package Description
    mop Main Orchestrator Process
    worker Interface Worker
    config SocleConfiguration
    kv KvBus (in_memory / Redis)
    shared SharedDataRegistry
    supervisor Supervision heartbeats
    http HttpWorker, TomcatManager
    admin AdminRestApi
    metrics SocleMetrics
    pipeline PipelineEngine
    resilience CircuitBreaker, Retry
    scheduler WorkerScheduler
    security AdminAuthFilter, RateLimit

    Nouveaux composants V4

    Package Description
    techdb H2 TechDB Manager
    logging Log4j2 + LogForwarder
    client/auth SocleAuthClient
    client/registry WorkerRegistryClient

    6. Cas d’usage

    Le Socle V4 est idéal pour :

    • Agents de collecte (DB2 Journal Reader, CDC)
    • Services de synchronisation (ODH-sync)
    • Proxies et bridges (Kafka Proxy)
    • Workers de traitement (ETL, pipelines)
    • Services multi-région (MTQ, GUA, REU, etc.)

    7. Prérequis

    Développement

    • JDK 21+
    • Maven 3.9+
    • IDE (IntelliJ IDEA recommandé)

    Production

    • JRE 21+
    • Docker (optionnel)
    • Accès NATS ou HTTP pour LogForwarder (optionnel)

    8. Premiers pas

    # Cloner le projet
    git clone <repo>/socle-v004.git
    
    # Build
    cd socle-v004
    mvn clean package -DskipTests
    
    # Run
    java -jar target/socle-v004-4.0.0.jar
    
    # Vérifier
    curl http://localhost:8080/health
    

    9. Documentation

    Document Description
    02-ARCHITECTURE Architecture détaillée
    03-QUICKSTART Guide de démarrage
    08-SUPERVISOR Supervision et heartbeats
    09-PIPELINE Pipeline V1 et V2
    21-H2-TECHDB Base H2 (V4)
    22-LOG4J2-LOGFORWARDER Logging V4
    25-MIGRATION-V3-V4 Migration
    27-STATUS-DASHBOARD Dashboard supervision
    GUIDE-METHODOLOGIQUE Bonnes pratiques

    10. Support

    • Issues : GitHub Issues
    • Documentation : Ce dossier docs/Help/
    • Exemples : 20-EXEMPLES
  • Socle V004 – Exemples de Code

    Socle V004 – Exemples de Code

    19 – Exemples

    Version : 4.0.0 Date : 2025-12-09

    1. Application minimale

    1.1 pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
             https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.2.1</version>
        </parent>
    
        <groupId>com.example</groupId>
        <artifactId>my-socle-app</artifactId>
        <version>1.0.0</version>
    
        <properties>
            <java.version>21</java.version>
        </properties>
    
        <dependencies>
            <!-- Socle V4 -->
            <dependency>
                <groupId>eu.lmvi</groupId>
                <artifactId>socle-v004</artifactId>
                <version>4.0.0</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
    

    1.2 Application.java

    package com.example;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.ComponentScan;
    
    @SpringBootApplication
    @ComponentScan(basePackages = {"com.example", "eu.lmvi.socle"})
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    

    1.3 application.yml

    socle:
      app_name: my-app
      env_name: ${ENV_NAME:DEV}
      region: ${REGION:local}
    
    server:
      port: ${HTTP_PORT:8080}
    
    logging:
      config: classpath:log4j2.xml
    

    1.4 Worker simple

    package com.example.worker;
    
    import eu.lmvi.socle.worker.Worker;
    import org.springframework.stereotype.Component;
    
    @Component
    public class HelloWorker implements Worker {
    
        @Override
        public String getName() {
            return "hello-worker";
        }
    
        @Override
        public void initialize() {
            System.out.println("Hello Worker initialized");
        }
    
        @Override
        public void start() {
            System.out.println("Hello Worker started");
        }
    
        @Override
        public void doWork() {
            System.out.println("Hello from worker!");
        }
    
        @Override
        public void stop() {
            System.out.println("Hello Worker stopped");
        }
    
        @Override
        public boolean isHealthy() {
            return true;
        }
    
        @Override
        public Map<String, Object> getStats() {
            return Map.of("status", "running");
        }
    }
    

    2. Worker Kafka Consumer

    package com.example.worker;
    
    import eu.lmvi.socle.worker.AbstractWorker;
    import eu.lmvi.socle.techdb.TechDbManager;
    import org.apache.kafka.clients.consumer.*;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    @Component
    public class KafkaConsumerWorker extends AbstractWorker {
    
        @Autowired
        private TechDbManager techDb;
    
        private KafkaConsumer<String, String> consumer;
        private String topic = "my-topic";
        private long lastOffset = 0;
    
        @Override
        public String getName() {
            return "kafka-consumer";
        }
    
        @Override
        public int getStartPriority() {
            return 10;
        }
    
        @Override
        protected void doInitialize() {
            // Restaurer l'offset
            lastOffset = techDb.getOffset("kafka", topic + "-0").orElse(0L);
            log.info("Starting from offset: {}", lastOffset);
    
            // Créer le consumer
            Properties props = new Properties();
            props.put("bootstrap.servers", "localhost:9092");
            props.put("group.id", "my-group");
            props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
            props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
            props.put("enable.auto.commit", "false");
    
            consumer = new KafkaConsumer<>(props);
            consumer.subscribe(List.of(topic));
        }
    
        @Override
        protected void doProcess() {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
    
            for (ConsumerRecord<String, String> record : records) {
                processMessage(record);
                lastOffset = record.offset();
                incrementProcessed();
            }
    
            // Persister périodiquement
            if (processedCount.get() % 100 == 0) {
                techDb.saveOffset("kafka", topic + "-0", lastOffset, null);
            }
        }
    
        private void processMessage(ConsumerRecord<String, String> record) {
            log.debug("Processing: key={}, value={}", record.key(), record.value());
            // Traitement...
        }
    
        @Override
        protected void doStop() {
            // Sauvegarder l'offset final
            techDb.saveOffset("kafka", topic + "-0", lastOffset, null);
    
            if (consumer != null) {
                consumer.close();
            }
        }
    
        @Override
        public Map<String, Object> getStats() {
            Map<String, Object> stats = new HashMap<>(super.getStats());
            stats.put("lastOffset", lastOffset);
            return stats;
        }
    }
    

    3. Worker HTTP API

    package com.example.worker;
    
    import eu.lmvi.socle.worker.Worker;
    import eu.lmvi.socle.kv.KvBus;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.bind.annotation.*;
    
    @Component
    @RestController
    @RequestMapping("/api/orders")
    public class OrderApiWorker implements Worker {
    
        @Autowired
        private KvBus kvBus;
    
        @Autowired
        private OrderService orderService;
    
        private volatile boolean running = false;
    
        @Override
        public String getName() {
            return "order-api";
        }
    
        @Override
        public boolean isPassive() {
            return true;  // Pas de doWork cyclique
        }
    
        @Override
        public void initialize() {}
    
        @Override
        public void start() {
            running = true;
        }
    
        @Override
        public void doWork() {
            // Passif - traitement via endpoints REST
        }
    
        @Override
        public void stop() {
            running = false;
        }
    
        @Override
        public boolean isHealthy() {
            return running;
        }
    
        @Override
        public Map<String, Object> getStats() {
            return Map.of("running", running);
        }
    
        // === REST Endpoints ===
    
        @PostMapping
        public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
            Order order = orderService.create(request);
    
            // Cache l'order
            kvBus.putJson("order:" + order.getId(), order);
            kvBus.setTtl("order:" + order.getId(), Duration.ofHours(1));
    
            return ResponseEntity.status(HttpStatus.CREATED).body(order);
        }
    
        @GetMapping("/{id}")
        public ResponseEntity<Order> getOrder(@PathVariable String id) {
            // Vérifier le cache d'abord
            Optional<Order> cached = kvBus.getJson("order:" + id, Order.class);
            if (cached.isPresent()) {
                return ResponseEntity.ok(cached.get());
            }
    
            // Sinon charger depuis la DB
            return orderService.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
        }
    }
    

    4. Worker Schedulé (Cron)

    package com.example.worker;
    
    import eu.lmvi.socle.worker.AbstractWorker;
    import org.springframework.stereotype.Component;
    
    @Component
    public class DailyReportWorker extends AbstractWorker {
    
        @Override
        public String getName() {
            return "daily-report";
        }
    
        @Override
        public String getSchedule() {
            return "0 0 6 * * ?";  // Tous les jours à 6h
        }
    
        @Override
        public boolean isScheduled() {
            return true;
        }
    
        @Override
        protected void doProcess() {
            log.info("Generating daily report...");
    
            // Collecter les données
            ReportData data = collectReportData();
    
            // Générer le rapport
            Report report = generateReport(data);
    
            // Envoyer par email
            sendReportByEmail(report);
    
            log.info("Daily report sent successfully");
            incrementProcessed();
        }
    
        private ReportData collectReportData() {
            // ...
            return new ReportData();
        }
    
        private Report generateReport(ReportData data) {
            // ...
            return new Report();
        }
    
        private void sendReportByEmail(Report report) {
            // ...
        }
    }
    

    5. Pipeline de traitement

    package com.example.pipeline;
    
    import eu.lmvi.socle.pipeline.*;
    import org.springframework.stereotype.Component;
    
    @Component
    public class OrderProcessingPipeline {
    
        @Autowired
        private PipelineEngine engine;
    
        @Autowired
        private OrderValidator validator;
    
        @Autowired
        private OrderEnricher enricher;
    
        @Autowired
        private PaymentProcessor paymentProcessor;
    
        @Autowired
        private NotificationService notificationService;
    
        public PipelineResult<ProcessedOrder> process(Order order) {
            Pipeline<Order, ProcessedOrder> pipeline = PipelineBuilder
                .<Order, ProcessedOrder>create("order-processing")
                .addStep(new ValidationStep(validator))
                .addStep(new EnrichmentStep(enricher))
                .addStep(new PaymentStep(paymentProcessor))
                .addStep(new NotificationStep(notificationService))
                .build();
    
            return engine.execute(pipeline, order);
        }
    }
    
    // Étape de validation
    class ValidationStep implements PipelineStep<Order, ValidatedOrder> {
    
        private final OrderValidator validator;
    
        public ValidationStep(OrderValidator validator) {
            this.validator = validator;
        }
    
        @Override
        public String getName() {
            return "validation";
        }
    
        @Override
        public StepResult<ValidatedOrder> execute(Order input, PipelineContext context) {
            List<String> errors = validator.validate(input);
            if (!errors.isEmpty()) {
                return StepResult.failure(getName(),
                    new ValidationException(errors), Duration.ZERO, 1);
            }
            return StepResult.success(getName(), new ValidatedOrder(input), Duration.ZERO);
        }
    
        @Override
        public boolean isRetryable() {
            return false;
        }
    }
    
    // Étape de notification (optionnelle)
    class NotificationStep implements PipelineStep<ProcessedOrder, ProcessedOrder> {
    
        private final NotificationService notificationService;
    
        @Override
        public String getName() {
            return "notification";
        }
    
        @Override
        public StepResult<ProcessedOrder> execute(ProcessedOrder input, PipelineContext context) {
            try {
                notificationService.sendOrderConfirmation(input);
                return StepResult.success(getName(), input, Duration.ZERO);
            } catch (Exception e) {
                return StepResult.failure(getName(), e, Duration.ZERO, 1);
            }
        }
    
        @Override
        public boolean isOptional() {
            return true;  // Le pipeline continue même si la notif échoue
        }
    }
    

    6. Service avec résilience

    package com.example.service;
    
    import eu.lmvi.socle.resilience.*;
    import org.springframework.stereotype.Service;
    
    @Service
    public class ExternalApiService {
    
        @Autowired
        private RetryTemplate retryTemplate;
    
        @Autowired
        private CircuitBreakerRegistry cbRegistry;
    
        @Autowired
        private KvBus kvBus;
    
        private final OkHttpClient httpClient;
    
        public Data fetchData(String id) {
            // Circuit breaker + Retry + Cache fallback
            CircuitBreaker cb = cbRegistry.getOrCreate("external-api");
    
            return cb.executeWithFallback(
                () -> retryTemplate.execute(() -> doFetchData(id)),
                () -> getCachedData(id)
            );
        }
    
        private Data doFetchData(String id) throws IOException {
            Request request = new Request.Builder()
                .url("https://api.example.com/data/" + id)
                .build();
    
            try (Response response = httpClient.newCall(request).execute()) {
                if (!response.isSuccessful()) {
                    throw new IOException("API returned " + response.code());
                }
    
                Data data = parseResponse(response.body().string());
    
                // Mettre en cache
                kvBus.putJson("cache:data:" + id, data);
                kvBus.setTtl("cache:data:" + id, Duration.ofMinutes(5));
    
                return data;
            }
        }
    
        private Data getCachedData(String id) {
            return kvBus.getJson("cache:data:" + id, Data.class)
                .orElseThrow(() -> new RuntimeException("No cached data available"));
        }
    }
    

    7. Configuration multi-environnement

    application.yml (base)

    socle:
      app_name: ${APP_NAME:my-app}
      env_name: ${ENV_NAME:DEV}
      region: ${REGION:local}
    
    spring:
      profiles:
        active: ${PROFILE:dev}
    

    application-dev.yml

    socle:
      kvbus:
        mode: in_memory
      techdb:
        console:
          enabled: true
      admin:
        auth:
          enabled: false
    
    logging:
      level:
        eu.lmvi.socle: DEBUG
    

    application-prod.yml

    socle:
      kvbus:
        mode: redis
        redis:
          host: ${REDIS_HOST}
          password: ${REDIS_PASSWORD}
      techdb:
        console:
          enabled: false
      logging:
        forwarder:
          enabled: true
      auth:
        enabled: true
      admin:
        auth:
          enabled: true
    
    logging:
      level:
        eu.lmvi.socle: INFO
    

    8. Dockerfile complet

    # Build stage
    FROM eclipse-temurin:21-jdk-alpine AS build
    WORKDIR /app
    COPY pom.xml .
    COPY src ./src
    RUN apk add --no-cache maven && \
        mvn clean package -DskipTests
    
    # Runtime stage
    FROM eclipse-temurin:21-jre-alpine
    WORKDIR /app
    
    # Security
    RUN addgroup -S app && adduser -S app -G app
    USER app
    
    # Copy artifact
    COPY --from=build --chown=app:app /app/target/*.jar app.jar
    
    # Config
    ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom"
    
    # Health check
    HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
        CMD wget -qO- http://localhost:8080/admin/health/live || exit 1
    
    EXPOSE 8080
    
    ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
    

    9. Kubernetes deployment complet

    Voir 16-KUBERNETES pour l’exemple complet de déploiement K8s.

    10. Références

  • Socle V004 – Supervisor

    Socle V004 – Supervisor

    08 – Supervisor

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Le Supervisor est le composant de supervision du Socle V4. Il surveille l’état de santé des Workers via heartbeats et expose des métriques de santé.

    Fonctionnalités

    • Collecte des heartbeats des Workers
    • Détection des Workers défaillants
    • Agrégation de l’état de santé global
    • Exposition via API REST et métriques
    • Intégration avec SharedDataRegistry

    2. Architecture

    ┌─────────────────────────────────────────────────────────────┐
    │                        Supervisor                            │
    │                                                              │
    │   ┌──────────────────┐    ┌──────────────────┐              │
    │   │ HeartbeatCollector│    │ HealthAggregator │              │
    │   └────────┬─────────┘    └────────┬─────────┘              │
    │            │                       │                         │
    │            ▼                       ▼                         │
    │   ┌──────────────────┐    ┌──────────────────┐              │
    │   │ Worker States    │    │ Global Health    │              │
    │   │ (per worker)     │    │ (aggregated)     │              │
    │   └──────────────────┘    └──────────────────┘              │
    │                                                              │
    └──────────────────────────────────────────────────────────────┘
             │                           │
             ▼                           ▼
    ┌─────────────────┐         ┌─────────────────┐
    │ /admin/health   │         │ /admin/workers  │
    └─────────────────┘         └─────────────────┘
    

    3. Configuration

    3.1 application.yml

    socle:
      supervisor:
        heartbeat-interval-ms: ${SUPERVISOR_HEARTBEAT_MS:10000}
        unhealthy-threshold: ${SUPERVISOR_UNHEALTHY_THRESHOLD:3}
        check-interval-ms: ${SUPERVISOR_CHECK_INTERVAL_MS:5000}
        stale-timeout-ms: ${SUPERVISOR_STALE_TIMEOUT_MS:60000}
    

    3.2 Variables d’environnement

    Variable Description Défaut
    SUPERVISOR_HEARTBEAT_MS Intervalle heartbeat attendu 10000 (10s)
    SUPERVISOR_UNHEALTHY_THRESHOLD Heartbeats manqués avant UNHEALTHY 3
    SUPERVISOR_CHECK_INTERVAL_MS Intervalle de vérification 5000 (5s)
    SUPERVISOR_STALE_TIMEOUT_MS Timeout avant worker STALE 60000 (1min)

    4. Interface Supervisor

    package eu.lmvi.socle.supervisor;
    
    public interface Supervisor {
    
        /**
         * Enregistre un worker dans le supervisor
         */
        void registerWorker(Worker worker);
    
        /**
         * Désenregistre un worker
         */
        void unregisterWorker(String workerName);
    
        /**
         * Reçoit un heartbeat d'un worker
         */
        void heartbeat(String workerName);
    
        /**
         * Reçoit un heartbeat avec métriques
         */
        void heartbeat(String workerName, Map<String, Object> metrics);
    
        /**
         * Récupère l'état d'un worker
         */
        WorkerState getWorkerState(String workerName);
    
        /**
         * Récupère l'état de tous les workers
         */
        Map<String, WorkerState> getAllWorkerStates();
    
        /**
         * Vérifie si un worker est healthy
         */
        boolean isWorkerHealthy(String workerName);
    
        /**
         * Récupère l'état de santé global
         */
        HealthStatus getGlobalHealth();
    
        /**
         * Liste les workers unhealthy
         */
        List<String> getUnhealthyWorkers();
    
        /**
         * Démarre la supervision
         */
        void start();
    
        /**
         * Arrête la supervision
         */
        void stop();
    }
    

    5. États des Workers

    5.1 WorkerState

    package eu.lmvi.socle.supervisor;
    
    public record WorkerState(
        String workerName,
        WorkerStatus status,
        Instant lastHeartbeat,
        int missedHeartbeats,
        Map<String, Object> lastMetrics,
        Instant registeredAt
    ) {
        public boolean isHealthy() {
            return status == WorkerStatus.RUNNING;
        }
    
        public boolean isStale() {
            return status == WorkerStatus.STALE;
        }
    }
    

    5.2 WorkerStatus

    public enum WorkerStatus {
        /**
         * Worker enregistré mais pas encore démarré
         */
        REGISTERED,
    
        /**
         * Worker en cours d'exécution, heartbeats reçus
         */
        RUNNING,
    
        /**
         * Heartbeats manqués mais pas encore timeout
         */
        DEGRADED,
    
        /**
         * Trop de heartbeats manqués, worker considéré unhealthy
         */
        UNHEALTHY,
    
        /**
         * Aucun heartbeat depuis longtemps, worker potentiellement mort
         */
        STALE,
    
        /**
         * Worker arrêté proprement
         */
        STOPPED
    }
    

    5.3 Diagramme d’états

                        ┌────────────────┐
                        │   REGISTERED   │
                        └───────┬────────┘
                                │ first heartbeat
                                ▼
                        ┌────────────────┐
             ┌─────────│    RUNNING     │─────────┐
             │         └───────┬────────┘         │
             │                 │                  │
             │ heartbeat       │ missed           │ stop()
             │ received        │ heartbeat        │
             │                 ▼                  │
             │         ┌────────────────┐         │
             └────────►│   DEGRADED     │         │
                       └───────┬────────┘         │
                               │ threshold        │
                               │ exceeded         │
                               ▼                  │
                       ┌────────────────┐         │
                       │   UNHEALTHY    │         │
                       └───────┬────────┘         │
                               │ stale            │
                               │ timeout          │
                               ▼                  │
                       ┌────────────────┐         │
                       │     STALE      │         │
                       └────────────────┘         │
                                                  │
                       ┌────────────────┐         │
                       │    STOPPED     │◄────────┘
                       └────────────────┘
    

    6. Implémentation

    package eu.lmvi.socle.supervisor;
    
    @Component
    public class DefaultSupervisor implements Supervisor {
    
        private static final Logger log = LoggerFactory.getLogger(DefaultSupervisor.class);
    
        private final SocleConfiguration config;
        private final ConcurrentHashMap<String, WorkerStateInternal> workers = new ConcurrentHashMap<>();
        private final ScheduledExecutorService scheduler;
        private volatile boolean running = false;
    
        public DefaultSupervisor(SocleConfiguration config) {
            this.config = config;
            this.scheduler = Executors.newSingleThreadScheduledExecutor(
                r -> new Thread(r, "supervisor-checker"));
        }
    
        @Override
        public void registerWorker(Worker worker) {
            String name = worker.getName();
            workers.put(name, new WorkerStateInternal(
                name, WorkerStatus.REGISTERED, Instant.now(), 0, Map.of(), Instant.now()));
            log.info("Worker registered: {}", name);
        }
    
        @Override
        public void unregisterWorker(String workerName) {
            WorkerStateInternal state = workers.remove(workerName);
            if (state != null) {
                log.info("Worker unregistered: {}", workerName);
            }
        }
    
        @Override
        public void heartbeat(String workerName) {
            heartbeat(workerName, Map.of());
        }
    
        @Override
        public void heartbeat(String workerName, Map<String, Object> metrics) {
            workers.computeIfPresent(workerName, (name, state) -> {
                log.debug("Heartbeat received: {}", name);
                return new WorkerStateInternal(
                    name,
                    WorkerStatus.RUNNING,
                    Instant.now(),
                    0,
                    metrics,
                    state.registeredAt
                );
            });
        }
    
        @Override
        public WorkerState getWorkerState(String workerName) {
            WorkerStateInternal internal = workers.get(workerName);
            return internal != null ? internal.toPublic() : null;
        }
    
        @Override
        public Map<String, WorkerState> getAllWorkerStates() {
            return workers.entrySet().stream()
                .collect(Collectors.toMap(
                    Map.Entry::getKey,
                    e -> e.getValue().toPublic()
                ));
        }
    
        @Override
        public boolean isWorkerHealthy(String workerName) {
            WorkerStateInternal state = workers.get(workerName);
            return state != null && state.status == WorkerStatus.RUNNING;
        }
    
        @Override
        public HealthStatus getGlobalHealth() {
            if (workers.isEmpty()) {
                return HealthStatus.HEALTHY;
            }
    
            boolean hasUnhealthy = workers.values().stream()
                .anyMatch(s -> s.status == WorkerStatus.UNHEALTHY || s.status == WorkerStatus.STALE);
    
            if (hasUnhealthy) {
                return HealthStatus.UNHEALTHY;
            }
    
            boolean hasDegraded = workers.values().stream()
                .anyMatch(s -> s.status == WorkerStatus.DEGRADED);
    
            if (hasDegraded) {
                return HealthStatus.DEGRADED;
            }
    
            return HealthStatus.HEALTHY;
        }
    
        @Override
        public List<String> getUnhealthyWorkers() {
            return workers.entrySet().stream()
                .filter(e -> e.getValue().status == WorkerStatus.UNHEALTHY
                          || e.getValue().status == WorkerStatus.STALE)
                .map(Map.Entry::getKey)
                .toList();
        }
    
        @Override
        public void start() {
            running = true;
            long checkInterval = config.getSupervisor().getCheckIntervalMs();
            scheduler.scheduleAtFixedRate(
                this::checkWorkers,
                checkInterval,
                checkInterval,
                TimeUnit.MILLISECONDS
            );
            log.info("Supervisor started with check interval: {}ms", checkInterval);
        }
    
        @Override
        public void stop() {
            running = false;
            scheduler.shutdown();
            log.info("Supervisor stopped");
        }
    
        private void checkWorkers() {
            if (!running) return;
    
            long heartbeatInterval = config.getSupervisor().getHeartbeatIntervalMs();
            int unhealthyThreshold = config.getSupervisor().getUnhealthyThreshold();
            long staleTimeout = config.getSupervisor().getStaleTimeoutMs();
    
            Instant now = Instant.now();
    
            workers.replaceAll((name, state) -> {
                if (state.status == WorkerStatus.STOPPED) {
                    return state;
                }
    
                long msSinceLastHeartbeat = Duration.between(state.lastHeartbeat, now).toMillis();
    
                // Stale check
                if (msSinceLastHeartbeat > staleTimeout) {
                    if (state.status != WorkerStatus.STALE) {
                        log.warn("Worker STALE: {} (no heartbeat for {}ms)", name, msSinceLastHeartbeat);
                    }
                    return state.withStatus(WorkerStatus.STALE);
                }
    
                // Missed heartbeat check
                int expectedHeartbeats = (int) (msSinceLastHeartbeat / heartbeatInterval);
                if (expectedHeartbeats > 0) {
                    int newMissedCount = state.missedHeartbeats + 1;
    
                    if (newMissedCount >= unhealthyThreshold) {
                        if (state.status != WorkerStatus.UNHEALTHY) {
                            log.warn("Worker UNHEALTHY: {} (missed {} heartbeats)", name, newMissedCount);
                        }
                        return state.withStatus(WorkerStatus.UNHEALTHY).withMissedCount(newMissedCount);
                    } else {
                        if (state.status == WorkerStatus.RUNNING) {
                            log.info("Worker DEGRADED: {} (missed {} heartbeats)", name, newMissedCount);
                        }
                        return state.withStatus(WorkerStatus.DEGRADED).withMissedCount(newMissedCount);
                    }
                }
    
                return state;
            });
        }
    
        private record WorkerStateInternal(
            String workerName,
            WorkerStatus status,
            Instant lastHeartbeat,
            int missedHeartbeats,
            Map<String, Object> lastMetrics,
            Instant registeredAt
        ) {
            WorkerState toPublic() {
                return new WorkerState(workerName, status, lastHeartbeat, missedHeartbeats, lastMetrics, registeredAt);
            }
    
            WorkerStateInternal withStatus(WorkerStatus newStatus) {
                return new WorkerStateInternal(workerName, newStatus, lastHeartbeat, missedHeartbeats, lastMetrics, registeredAt);
            }
    
            WorkerStateInternal withMissedCount(int newCount) {
                return new WorkerStateInternal(workerName, status, lastHeartbeat, newCount, lastMetrics, registeredAt);
            }
        }
    }
    

    7. Heartbeat depuis les Workers

    7.1 Heartbeat manuel

    @Component
    public class MyWorker implements Worker {
    
        @Autowired
        private Supervisor supervisor;
    
        @Override
        public void doWork() {
            // Envoyer heartbeat avec métriques
            supervisor.heartbeat(getName(), Map.of(
                "processed", processedCount,
                "queueSize", queue.size()
            ));
    
            // Traitement...
        }
    }
    

    7.2 Heartbeat automatique via AbstractWorker

    public abstract class AbstractWorker implements Worker {
    
        @Autowired
        private Supervisor supervisor;
    
        @Override
        public final void doWork() {
            // Heartbeat automatique
            supervisor.heartbeat(getName(), getStats());
    
            // Appel au traitement réel
            doProcess();
        }
    
        protected abstract void doProcess();
    }
    

    7.3 Heartbeat via thread dédié

    @Component
    public class LongRunningWorker implements Worker {
    
        @Autowired
        private Supervisor supervisor;
    
        private ScheduledExecutorService heartbeatExecutor;
    
        @Override
        public void start() {
            heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
            heartbeatExecutor.scheduleAtFixedRate(
                () -> supervisor.heartbeat(getName()),
                0, 10, TimeUnit.SECONDS
            );
        }
    
        @Override
        public void stop() {
            if (heartbeatExecutor != null) {
                heartbeatExecutor.shutdown();
            }
        }
    
        @Override
        public void doWork() {
            // Long processing - heartbeat handled by separate thread
            processLongTask();
        }
    }
    

    8. API REST

    8.1 Endpoints

    @RestController
    @RequestMapping("/admin")
    public class SupervisorController {
    
        @Autowired
        private Supervisor supervisor;
    
        @GetMapping("/health")
        public ResponseEntity<HealthResponse> health() {
            HealthStatus status = supervisor.getGlobalHealth();
            return ResponseEntity
                .status(status == HealthStatus.HEALTHY ? 200 : 503)
                .body(new HealthResponse(status, supervisor.getUnhealthyWorkers()));
        }
    
        @GetMapping("/workers")
        public Map<String, WorkerState> workers() {
            return supervisor.getAllWorkerStates();
        }
    
        @GetMapping("/workers/{name}")
        public ResponseEntity<WorkerState> worker(@PathVariable String name) {
            WorkerState state = supervisor.getWorkerState(name);
            return state != null
                ? ResponseEntity.ok(state)
                : ResponseEntity.notFound().build();
        }
    }
    

    8.2 Réponses

    // GET /admin/health
    {
      "status": "HEALTHY",
      "unhealthyWorkers": []
    }
    
    // GET /admin/workers
    {
      "kafka-consumer": {
        "workerName": "kafka-consumer",
        "status": "RUNNING",
        "lastHeartbeat": "2025-12-09T10:30:00Z",
        "missedHeartbeats": 0,
        "lastMetrics": {
          "processed": 12345,
          "lag": 23
        },
        "registeredAt": "2025-12-09T10:00:00Z"
      },
      "order-processor": {
        "workerName": "order-processor",
        "status": "DEGRADED",
        "lastHeartbeat": "2025-12-09T10:29:45Z",
        "missedHeartbeats": 1,
        "lastMetrics": {},
        "registeredAt": "2025-12-09T10:00:00Z"
      }
    }
    

    9. Intégration Kubernetes

    9.1 Liveness Probe

    livenessProbe:
      httpGet:
        path: /admin/health
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 10
      failureThreshold: 3
    

    9.2 Readiness Probe

    readinessProbe:
      httpGet:
        path: /admin/health
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5
    

    9.3 Health Controller adapté

    @GetMapping("/health/live")
    public ResponseEntity<Void> live() {
        // Liveness: l'application répond
        return ResponseEntity.ok().build();
    }
    
    @GetMapping("/health/ready")
    public ResponseEntity<Void> ready() {
        // Readiness: tous les workers sont healthy
        HealthStatus status = supervisor.getGlobalHealth();
        return status == HealthStatus.HEALTHY
            ? ResponseEntity.ok().build()
            : ResponseEntity.status(503).build();
    }
    

    10. Métriques Prometheus

    @Component
    public class SupervisorMetrics {
    
        private final Supervisor supervisor;
        private final MeterRegistry registry;
    
        @PostConstruct
        public void registerMetrics() {
            Gauge.builder("socle_workers_total", supervisor,
                s -> s.getAllWorkerStates().size())
                .register(registry);
    
            Gauge.builder("socle_workers_healthy", supervisor,
                s -> s.getAllWorkerStates().values().stream()
                    .filter(WorkerState::isHealthy).count())
                .register(registry);
    
            Gauge.builder("socle_workers_unhealthy", supervisor,
                s -> s.getUnhealthyWorkers().size())
                .register(registry);
        }
    }
    

    11. Bonnes pratiques

    DO

    • Envoyer des heartbeats réguliers depuis tous les workers actifs
    • Inclure des métriques utiles dans les heartbeats
    • Configurer des timeouts adaptés à vos workers
    • Utiliser les probes Kubernetes pour la haute disponibilité

    DON’T

    • Ne pas oublier d’envoyer des heartbeats dans les workers long-running
    • Ne pas ignorer les états DEGRADED
    • Ne pas configurer des timeouts trop courts (faux positifs)
    • Ne pas bloquer l’envoi de heartbeat avec du traitement lourd

    12. Références

  • Socle V004 – gRPC Inter-Socles

    Socle V004 – gRPC Inter-Socles

    31 – Communication gRPC Inter-Socles

    Vue d’ensemble

    Le module gRPC permet aux instances Socle V4 de communiquer entre elles via streaming bidirectionnel. Il offre :

    • Sessions : Gestion de sessions multi-participants avec TTL
    • Streaming bidirectionnel : Communication temps reel entre Socles
    • Pipeline de traitement : Transformation des messages via Janino et JDM
    • Fan-out : Routage broadcast ou cible vers les participants
    • Pool de connexions : Connexions persistantes vers les peers

    Architecture

    ┌─────────────────────────────────────────────────────────────────┐
    │                         Socle A                                  │
    │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐  │
    │  │ SessionMgr  │  │  Pipeline   │  │    GrpcServerWorker     │  │
    │  │             │  │  Executor   │  │    (port 9400)          │  │
    │  └─────────────┘  └─────────────┘  └─────────────────────────┘  │
    │         │               │                      │                 │
    │         └───────────────┼──────────────────────┘                 │
    │                         │                                        │
    │              ┌──────────┴──────────┐                            │
    │              │   gRPC Services     │                            │
    │              │  - SocleComm        │                            │
    │              │  - SessionService   │                            │
    │              │  - DiscoveryService │                            │
    │              └──────────┬──────────┘                            │
    └─────────────────────────┼───────────────────────────────────────┘
                              │ gRPC/HTTP2
                              ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                         Socle B                                  │
    │              ┌──────────────────────┐                           │
    │              │  PeerConnectionPool  │                           │
    │              └──────────────────────┘                           │
    └─────────────────────────────────────────────────────────────────┘
    

    Configuration

    application.yml

    socle:
      grpc:
        # Activation du module
        enabled: ${GRPC_ENABLED:false}
    
        # Port du serveur gRPC
        port: ${GRPC_PORT:9400}
    
        # Identification du Socle
        socle-id: ${SOCLE_ID:${socle.app_name}}
        socle-version: ${socle.version}
    
        # Limites serveur
        max-inbound-message-size: ${GRPC_MAX_MESSAGE_SIZE:4194304}  # 4MB
        max-concurrent-calls-per-connection: ${GRPC_MAX_CONCURRENT_CALLS:100}
    
        # Sessions
        session:
          ttl-seconds: ${GRPC_SESSION_TTL:1800}           # 30 min
          max-participants: ${GRPC_MAX_PARTICIPANTS:100}
          persist-to-tech-db: ${GRPC_PERSIST_SESSIONS:true}
          cache-in-redis: ${GRPC_CACHE_REDIS:true}
    
        # Pipeline de traitement
        pipeline:
          enabled: ${GRPC_PIPELINE_ENABLED:true}
          config-cache-ttl-seconds: ${GRPC_PIPELINE_CACHE_TTL:300}
    
        # Connexions peer
        peer:
          max-channels-per-peer: ${GRPC_PEER_MAX_CHANNELS:4}
          connection-timeout-ms: ${GRPC_PEER_CONNECT_TIMEOUT:5000}
          idle-timeout-seconds: ${GRPC_PEER_IDLE_TIMEOUT:300}
          keep-alive-enabled: ${GRPC_PEER_KEEPALIVE:true}
          keep-alive-time-seconds: ${GRPC_PEER_KEEPALIVE_TIME:30}
          keep-alive-timeout-seconds: ${GRPC_PEER_KEEPALIVE_TIMEOUT:10}
    

    Variables d’environnement

    Variable Default Description
    GRPC_ENABLED false Active le module gRPC
    GRPC_PORT 9400 Port du serveur gRPC
    SOCLE_ID ${app_name} Identifiant unique du Socle
    GRPC_SESSION_TTL 1800 TTL des sessions en secondes
    GRPC_MAX_PARTICIPANTS 100 Max participants par session
    GRPC_PIPELINE_ENABLED true Active le pipeline de traitement

    Services gRPC

    DiscoveryService

    Service de decouverte et health check.

    service DiscoveryService {
        rpc GetCapabilities(CapabilitiesRequest) returns (CapabilitiesResponse);
        rpc Ping(PingRequest) returns (PingResponse);
    }
    

    Test avec grpcurl :

    # Ping
    grpcurl -plaintext localhost:9400 socle.DiscoveryService/Ping
    
    # Capabilities
    grpcurl -plaintext localhost:9400 socle.DiscoveryService/GetCapabilities
    

    SessionService

    Gestion du cycle de vie des sessions.

    service SessionService {
        rpc CreateSession(CreateSessionRequest) returns (SessionInfo);
        rpc JoinSession(JoinSessionRequest) returns (JoinSessionResponse);
        rpc LeaveSession(LeaveSessionRequest) returns (LeaveSessionResponse);
        rpc GetSession(GetSessionRequest) returns (SessionInfo);
        rpc CloseSession(CloseSessionRequest) returns (CloseSessionResponse);
    }
    

    Exemples :

    # Creer une session
    grpcurl -plaintext -d '{
      "session_type": "chat",
      "owner_id": "user1",
      "ttl_seconds": 3600
    }' localhost:9400 socle.SessionService/CreateSession
    
    # Joindre une session
    grpcurl -plaintext -d '{
      "session_id": "uuid-de-la-session",
      "participant_id": "user2",
      "display_name": "User 2"
    }' localhost:9400 socle.SessionService/JoinSession
    
    # Obtenir info session
    grpcurl -plaintext -d '{
      "session_id": "uuid-de-la-session"
    }' localhost:9400 socle.SessionService/GetSession
    

    SocleComm

    Streaming bidirectionnel pour l’echange de messages.

    service SocleComm {
        rpc Exchange(stream SessionMessage) returns (stream SessionMessage);
    }
    

    Format des messages :

    message SessionMessage {
        string session_id = 1;
        string sender_id = 2;
        MessageKind kind = 3;        // JOIN, LEAVE, DATA, REQUEST, RESPONSE, etc.
        repeated string target_ids = 4;  // Vide = broadcast
        string correlation_id = 5;
        int64 timestamp = 6;
        string payload = 7;          // JSON
        map<string, string> headers = 8;
    }
    

    Sessions

    Cycle de vie

    CREATE ──► ACTIVE ──► CLOSING ──► CLOSED
                  │
                  └──► EXPIRED (TTL depasse)
    

    Stockage

    Les sessions sont stockees dans :

    1. Cache memoire : Acces rapide, stream observers
    2. Redis (si KvBus en mode redis) : Partage entre instances
    3. TechDB : Audit et persistance

    Tables TechDB

    -- Sessions (audit)
    CREATE TABLE grpc_sessions (
        x_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
        x_dateCreated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
        x_dateChanged TIMESTAMP WITH TIME ZONE,
        x_sub VARCHAR(255),
        x_partition VARCHAR(30),
        x_comment CLOB,
        session_id VARCHAR(36) NOT NULL UNIQUE,
        session_type VARCHAR(100) NOT NULL,
        owner_id VARCHAR(100) NOT NULL,
        status VARCHAR(20) DEFAULT 'ACTIVE',
        datas CLOB
    );
    
    -- Configuration pipeline par type de session
    CREATE TABLE grpc_pipeline_configs (
        x_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
        x_dateCreated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
        x_dateChanged TIMESTAMP WITH TIME ZONE,
        x_sub VARCHAR(255),
        x_partition VARCHAR(30),
        x_comment CLOB,
        session_type VARCHAR(100) NOT NULL UNIQUE,
        janino_pre VARCHAR(255),
        jdm_rules VARCHAR(255),
        janino_post VARCHAR(255),
        default_targets VARCHAR(50) DEFAULT 'broadcast',
        enabled BOOLEAN DEFAULT TRUE,
        datas CLOB
    );
    

    Pipeline de traitement

    Le pipeline permet de transformer les messages et determiner leur routage.

    Etapes

    Message entrant
          │
          ▼
    ┌─────────────────┐
    │  1. Janino PRE  │  Transformation du payload
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │  2. JDM Rules   │  Determination des cibles (routing)
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │  3. Janino POST │  Transformation finale
    └────────┬────────┘
             │
             ▼
        Routage
       /        \
    Broadcast   Cible
    

    Configuration du pipeline

    Inserez dans grpc_pipeline_configs :

    INSERT INTO grpc_pipeline_configs
    (session_type, janino_pre, jdm_rules, janino_post, default_targets, enabled)
    VALUES
    ('chat', NULL, 'chat_routing', NULL, 'broadcast', TRUE);
    

    Exemple script Janino PRE

    // repository/scripts/java/grpc/chat_filter.java
    public class ChatFilter {
        public Object execute(Map<String, Object> context) {
            Map<String, Object> payload = (Map<String, Object>) context.get("payload");
            String senderId = (String) context.get("senderId");
    
            // Ajouter metadata
            payload.put("processed_at", System.currentTimeMillis());
            payload.put("sender", senderId);
    
            return payload;
        }
    }
    

    Exemple modele JDM pour routing

    {
      "name": "chat_routing",
      "nodes": [
        {
          "id": "input",
          "type": "inputNode",
          "content": {
            "fields": [
              {"field": "payload.target", "type": "string"},
              {"field": "payload.type", "type": "string"}
            ]
          }
        },
        {
          "id": "decision",
          "type": "decisionTableNode",
          "content": {
            "hitPolicy": "first",
            "inputs": [
              {"field": "payload.type"}
            ],
            "outputs": [
              {"field": "targets", "type": "string"},
              {"field": "broadcast", "type": "boolean"}
            ],
            "rules": [
              {"_input": ["private"], "_output": ["${payload.target}", false]},
              {"_input": ["broadcast"], "_output": ["", true]},
              {"_input": ["*"], "_output": ["", true]}
            ]
          }
        }
      ]
    }
    

    Connexions Peer

    Ajouter un peer programmatiquement

    @Autowired
    private PeerConnectionPool peerPool;
    
    public void connectToPeer() {
        // Ajouter et connecter
        boolean connected = peerPool.addPeer("socle-b", "192.168.1.100", 9400);
    
        if (connected) {
            // Ouvrir un stream
            peerPool.openStream("socle-b", message -> {
                // Handler pour messages entrants
                System.out.println("Received: " + message.getPayload());
            });
    
            // Envoyer un message
            SessionMessage msg = SessionMessage.newBuilder()
                .setSessionId("...")
                .setSenderId("local-participant")
                .setKind(MessageKind.DATA)
                .setPayload("{\"text\":\"Hello\"}")
                .build();
    
            peerPool.sendToPeer("socle-b", msg);
        }
    }
    

    Broadcast vers tous les peers

    SessionMessage msg = SessionMessage.newBuilder()
        .setSessionId(sessionId)
        .setSenderId(myId)
        .setKind(MessageKind.DATA)
        .setPayload(jsonPayload)
        .build();
    
    int sent = peerPool.broadcast(msg, null);  // null = inclure tous
    

    Worker gRPC

    Priorites

    Methode Valeur Description
    getStartPriority() 800 Demarre apres Janino (25) et JDM (30)
    getStopPriority() 10 S’arrete tot pour drain des connexions

    Statistiques

    @Autowired
    private GrpcServerWorker grpcWorker;
    
    Map<String, Object> stats = grpcWorker.getStats();
    // {
    //   "running": true,
    //   "port": 9400,
    //   "sessions": { "cached_sessions": 5, "active_streams": 12 },
    //   "messaging": { "messages_received": 1234, "messages_sent": 5678 },
    //   "peers": { "connected_peers": 2, "total_messages_sent": 100 }
    // }
    

    Types de sessions supportes

    Type Description
    chat Communication temps reel
    sync Synchronisation de donnees
    broadcast Diffusion un-vers-plusieurs
    pipeline Traitement en chaine
    custom Type personnalise

    Securite

    TLS (a venir)

    Le support TLS sera ajoute dans une version future :

    socle:
      grpc:
        tls:
          enabled: true
          cert-path: /path/to/server.crt
          key-path: /path/to/server.key
          ca-path: /path/to/ca.crt  # Pour mTLS
    

    Authentification

    L’authentification peut etre implementee via :

    • Metadata gRPC (tokens dans headers)
    • Intercepteurs personnalises
    • Integration avec le module Auth existant

    Monitoring

    Metriques exposees

    Les metriques sont disponibles via l’endpoint Prometheus /actuator/prometheus :

    • grpc_sessions_active : Nombre de sessions actives
    • grpc_messages_received_total : Total messages recus
    • grpc_messages_sent_total : Total messages envoyes
    • grpc_peers_connected : Nombre de peers connectes

    Logs

    [grpc_server] Started on port 9400
    [grpc_session] Session created: abc-123 (type=chat, owner=user1)
    [grpc_session] Participant joined: user2 -> abc-123 (total: 2)
    [grpc_comm] Message received: session=abc-123, sender=user1, kind=DATA
    [grpc_pipeline] Executed for session abc-123 (targets=broadcast)
    

    Troubleshooting

    Le serveur ne demarre pas

    1. Verifiez que le port 9400 n’est pas utilise
    2. Verifiez GRPC_ENABLED=true
    3. Consultez les logs pour les erreurs d’initialisation

    Sessions expirent trop vite

    Augmentez le TTL :

    socle:
      grpc:
        session:
          ttl-seconds: 7200  # 2 heures
    

    Messages non delivres

    1. Verifiez que le destinataire a un stream actif
    2. Verifiez les logs du pipeline pour les erreurs
    3. Verifiez la configuration du routing dans grpc_pipeline_configs

    Erreurs de connexion peer

    1. Verifiez la connectivite reseau
    2. Verifiez que le peer a gRPC active
    3. Augmentez le timeout de connexion :
    socle:
      grpc:
        peer:
          connection-timeout-ms: 10000
    

    Structure des fichiers

    src/main/java/eu/lmvi/socle/grpc/
    ├── GrpcConfiguration.java        # Configuration Spring
    ├── GrpcServerWorker.java         # Worker principal
    ├── session/
    │   ├── Session.java              # Record session
    │   ├── Participant.java          # Record participant
    │   ├── SessionStatus.java        # Enum ACTIVE/CLOSING/CLOSED/EXPIRED
    │   ├── SessionManager.java       # Gestion sessions + streams
    │   └── SessionRedisRepository.java  # Persistance Redis
    ├── pipeline/
    │   ├── PipelineConfig.java       # Config pipeline
    │   ├── PipelineResult.java       # Resultat pipeline
    │   └── PipelineExecutor.java     # Execution Janino + JDM
    ├── peer/
    │   ├── PeerConnection.java       # Connexion unique
    │   └── PeerConnectionPool.java   # Pool avec reconnexion
    └── service/
        ├── SocleCommService.java     # Streaming bidirectionnel
        ├── SessionServiceImpl.java   # CRUD sessions
        └── DiscoveryServiceImpl.java # Discovery + Ping
    
    src/main/proto/
    └── socle_comm.proto              # Definition Protocol Buffers
    
  • Socle V004 – Démarrage Rapide

    Socle V004 – Démarrage Rapide

    03 – Guide de Démarrage Rapide

    Version : 4.0.0 Date : 2025-01-25 Temps estimé : 5 minutes

    1. Prérequis

    • JDK 21+ installé
    • Maven 3.9+ installé
    • Git (optionnel)

    Vérification :

    java -version   # java version "21.x.x"
    mvn -version    # Maven 3.9.x
    

    2. Installation

    Option A : Cloner le projet

    git clone <repo>/socle-v004.git
    cd socle-v004
    

    Option B : Créer un nouveau projet

    mvn archetype:generate \
      -DgroupId=com.mycompany \
      -DartifactId=my-socle-app \
      -DarchetypeArtifactId=maven-archetype-quickstart \
      -DinteractiveMode=false
    
    cd my-socle-app
    # Ajouter la dépendance socle-v004 dans pom.xml
    

    3. Configuration minimale

    3.1 pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
             http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>eu.lmvi</groupId>
        <artifactId>socle-v004</artifactId>
        <version>4.0.0</version>
        <packaging>jar</packaging>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.2.1</version>
        </parent>
    
        <properties>
            <java.version>21</java.version>
        </properties>
    
        <dependencies>
            <!-- Spring Boot Web (sans Logback) -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-logging</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    
            <!-- Log4j2 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-log4j2</artifactId>
            </dependency>
    
            <!-- LMAX Disruptor (AsyncLoggers) -->
            <dependency>
                <groupId>com.lmax</groupId>
                <artifactId>disruptor</artifactId>
                <version>4.0.0</version>
            </dependency>
    
            <!-- H2 Database -->
            <dependency>
                <groupId>com.h2database</groupId>
                <artifactId>h2</artifactId>
                <version>2.2.224</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
    

    3.2 application.yml

    spring:
      application:
        name: my-socle-app
    
    socle:
      app_name: ${APP_NAME:my-socle-app}
      version: 4.0.0
      env_name: ${ENV_NAME:DEV}
      region: ${REGION:local}
    
      # HTTP
      http_port: ${HTTP_PORT:8080}
    
      # KV Bus
      kv_impl: in_memory
    
      # Supervisor
      supervisor_enabled: true
    
      # H2 TechDB
      techdb:
        enabled: true
        url: jdbc:h2:file:./data/techdb;MODE=PostgreSQL
        console:
          enabled: true
          path: /h2-console
    
    logging:
      config: classpath:log4j2.xml
    

    3.3 log4j2.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="WARN">
        <Appenders>
            <Console name="Console" target="SYSTEM_OUT">
                <PatternLayout pattern="%d{ISO8601} %-5level [%thread] %logger{36} - %msg%n"/>
            </Console>
        </Appenders>
        <Loggers>
            <Root level="INFO">
                <AppenderRef ref="Console"/>
            </Root>
        </Loggers>
    </Configuration>
    

    4. Créer un Worker simple

    package com.mycompany.worker;
    
    import eu.lmvi.socle.worker.Worker;
    import eu.lmvi.socle.supervisor.Supervisor;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.util.Map;
    
    @Component
    public class HelloWorker implements Worker {
    
        private static final Logger log = LoggerFactory.getLogger(HelloWorker.class);
    
        @Autowired
        private Supervisor supervisor;  // Injection du Supervisor
    
        private int counter = 0;
        private volatile boolean running = false;
    
        @Override
        public String getName() {
            return "hello-worker";
        }
    
        @Override
        public void initialize() {
            log.info("HelloWorker initializing...");
        }
    
        @Override
        public void start() {
            running = true;
            log.info("HelloWorker started!");
        }
    
        @Override
        public void doWork() {
            if (!running) return;  // Vérifier le flag d'arrêt
    
            try {
                counter++;
                log.info("Hello from worker! Count: {}", counter);
    
                // IMPORTANT: Envoyer un heartbeat avec métriques
                supervisor.heartbeat(getName(), getStats());
    
            } catch (Exception e) {
                log.error("Erreur dans doWork", e);
                // NE PAS propager l'exception
            }
        }
    
        @Override
        public void stop() {
            running = false;
            log.info("HelloWorker stopped. Final count: {}", counter);
        }
    
        @Override
        public boolean isHealthy() {
            return running;
        }
    
        @Override
        public Map<String, Object> getStats() {
            return Map.of(
                "counter", counter,
                "running", running
            );
        }
    
        @Override
        public long getCycleIntervalMs() {
            return 5000; // doWork() toutes les 5 secondes
        }
    }
    

    5. Build et Run

    # Build
    mvn clean package -DskipTests
    
    # Run
    java -jar target/socle-v004-4.0.0.jar
    
    # Ou avec variables d'environnement
    APP_NAME=my-app ENV_NAME=PROD java -jar target/socle-v004-4.0.0.jar
    

    6. Vérification

    Health check

    curl http://localhost:8080/health
    

    Réponse attendue :

    {
      "status": "UP",
      "app": "my-socle-app",
      "version": "4.0.0"
    }
    

    H2 Console (dev)

    Ouvrir : http://localhost:8080/h2-console

    • JDBC URL : jdbc:h2:file:./data/techdb
    • User : socle
    • Password : socle

    Logs

    Vous devriez voir :

    INFO  [main] MainOrchestratorProcess - MOP démarré
    INFO  [main] HelloWorker - HelloWorker started!
    INFO  [scheduler-1] HelloWorker - Hello from worker! Count: 1
    INFO  [scheduler-1] HelloWorker - Hello from worker! Count: 2
    

    StatusDashboard (supervision)

    Ouvrir : http://localhost:9374/

    Ce dashboard affiche en temps réel :

    • L’état de chaque worker (RUNNING, DEGRADED, UNHEALTHY)
    • Les métriques transmises par les heartbeats
    • L’historique des changements d’état

    7. Prochaines étapes

    Document Description
    04-CONFIGURATION Configuration complète
    05-WORKERS Guide des Workers
    08-SUPERVISOR Supervision et heartbeats
    21-H2-TECHDB Base H2 TechDB
    22-LOG4J2-LOGFORWARDER Logging centralisé
    27-STATUS-DASHBOARD Dashboard de supervision
    GUIDE-METHODOLOGIQUE Bonnes pratiques

    8. Troubleshooting

    Port déjà utilisé

    # Changer le port
    HTTP_PORT=9090 java -jar target/socle-v004-4.0.0.jar
    

    H2 Console inaccessible

    Vérifier que socle.techdb.console.enabled: true dans application.yml.

    Logs non visibles

    Vérifier que log4j2.xml existe dans src/main/resources/.