From 45b809e78a1db3d676d2d6d48d51d4fe6883186e Mon Sep 17 00:00:00 2001 From: HARPER Jon Date: Wed, 6 May 2026 23:30:18 +0200 Subject: [PATCH 1/7] docker compose wars --- docker-compose/multiwar/.env | 6 + .../multiwar/docker-compose.multiwar.yml | 234 ++++++++++++++++++ docker-compose/multiwar/docker-compose.yml | 3 + docker-compose/multiwar/itools-config.yml | 46 ++++ docker-compose/multiwar/prepare.sh | 47 ++++ docker-compose/multiwar/server.xml | 38 +++ .../shared-config/common-application.yml | 56 +++++ 7 files changed, 430 insertions(+) create mode 100644 docker-compose/multiwar/.env create mode 100644 docker-compose/multiwar/docker-compose.multiwar.yml create mode 100644 docker-compose/multiwar/docker-compose.yml create mode 100644 docker-compose/multiwar/itools-config.yml create mode 100755 docker-compose/multiwar/prepare.sh create mode 100644 docker-compose/multiwar/server.xml create mode 100644 docker-compose/multiwar/shared-config/common-application.yml diff --git a/docker-compose/multiwar/.env b/docker-compose/multiwar/.env new file mode 100644 index 00000000..2633c881 --- /dev/null +++ b/docker-compose/multiwar/.env @@ -0,0 +1,6 @@ +SHOULD_INIT_GEO_DATA=true +SHOULD_INIT_LINES_CATALOG=true + +COMPOSE_PROJECT_NAME=gridstudy + +TOMCAT_PORT=12345 diff --git a/docker-compose/multiwar/docker-compose.multiwar.yml b/docker-compose/multiwar/docker-compose.multiwar.yml new file mode 100644 index 00000000..abd3376d --- /dev/null +++ b/docker-compose/multiwar/docker-compose.multiwar.yml @@ -0,0 +1,234 @@ +services: + + # =========================================================================== + # SINGLE TOMCAT — all WAR-deployed microservices + # =========================================================================== + tomcat: + image: tomcat:10.1-jdk21 + ports: + - "${TOMCAT_PORT:-12345}:8080" + volumes: + - ./server.xml:/usr/local/tomcat/conf/server.xml:Z + - ./webapps:/usr/local/tomcat/webapps:Z + - ./config:/config:Z + - ./itools-config.yml:/root/.itools/config.yml:Z + environment: + - CATALINA_OPTS=-Dspring.profiles.active=default -Xmx2g + depends_on: + - postgres + - rabbitmq + - elasticsearch + - s3-storage + entrypoint: ["/bin/bash", "-c", "until curl -sf http://elasticsearch:9200/_cluster/health; do echo 'Waiting for elasticsearch...'; sleep 3; done && catalina.sh run"] + restart: unless-stopped + memswap_limit: 6g + deploy: + resources: + limits: + memory: 6g + + # =========================================================================== + # NON-WAR SERVICES (WebFlux / notification servers) + # =========================================================================== + config-server: + image: gridsuite/config-server:latest + ports: + - 5025:80 + volumes: + - ../../k8s/resources/common/config/config-server-application.yml:/config/specific/application.yml:Z + - ./shared-config/common-application.yml:/config/common/application.yml:Z + restart: unless-stopped + environment: + - JAVA_TOOL_OPTIONS=-Xmx96m + command: --server.port=80 --spring.config.additional-location=/config/ + sysctls: + - net.ipv4.ip_unprivileged_port_start=0 + memswap_limit: 384m + deploy: + resources: + limits: + memory: 384m + + config-notification-server: + image: gridsuite/config-notification-server:latest + ports: + - 5024:80 + volumes: + - ../../k8s/resources/common/config/config-notification-server-application.yml:/config/specific/application.yml:Z + - ./shared-config/common-application.yml:/config/common/application.yml:Z + restart: unless-stopped + environment: + - JAVA_TOOL_OPTIONS=-Xmx96m + command: --server.port=80 --spring.config.additional-location=/config/ + sysctls: + - net.ipv4.ip_unprivileged_port_start=0 + memswap_limit: 384m + deploy: + resources: + limits: + memory: 384m + + study-notification-server: + image: gridsuite/study-notification-server:latest + ports: + - 5009:80 + volumes: + - ../../k8s/resources/study/config/study-notification-server-application.yml:/config/specific/application.yml:Z + - ./shared-config/common-application.yml:/config/common/application.yml:Z + restart: unless-stopped + environment: + - JAVA_TOOL_OPTIONS=-Xmx96m + command: --server.port=80 --spring.config.additional-location=/config/ + sysctls: + - net.ipv4.ip_unprivileged_port_start=0 + memswap_limit: 384m + deploy: + resources: + limits: + memory: 384m + + directory-notification-server: + image: gridsuite/directory-notification-server:latest + ports: + - 5004:80 + volumes: + - ../../k8s/resources/study/config/directory-notification-server-application.yml:/config/specific/application.yml:Z + - ./shared-config/common-application.yml:/config/common/application.yml:Z + restart: unless-stopped + environment: + - JAVA_TOOL_OPTIONS=-Xmx96m + command: --server.port=80 --spring.config.additional-location=/config/ + sysctls: + - net.ipv4.ip_unprivileged_port_start=0 + memswap_limit: 384m + deploy: + resources: + limits: + memory: 384m + + gateway: + image: gridsuite/gateway:latest + ports: + - 9000:80 + volumes: + - ../../k8s/resources/common/config/gateway-application.yml:/config/specific/application.yml:Z + - ./shared-config/common-application.yml:/config/common/application.yml:Z + - ../allowed-issuers.yml:/config/allowed-issuers.yml:Z + restart: unless-stopped + environment: + - JAVA_TOOL_OPTIONS=-Xmx96m + command: --server.port=80 --spring.config.additional-location=/config/ + sysctls: + - net.ipv4.ip_unprivileged_port_start=0 + memswap_limit: 384m + deploy: + resources: + limits: + memory: 384m + + # =========================================================================== + # FRONTEND APPS + # =========================================================================== + gridstudy-app: + image: gridsuite/gridstudy-app:latest + ports: + - 84:8080 + volumes: + - ../study/gridstudy-app-idpSettings.json:/opt/bitnami/apache/htdocs/gridstudy/idpSettings.json:Z + - ../env.json:/opt/bitnami/apache/htdocs/gridstudy/env.json:Z + memswap_limit: 128m + deploy: + resources: + limits: + memory: 128m + restart: unless-stopped + + gridexplore-app: + image: gridsuite/gridexplore-app:latest + ports: + - 80:8080 + volumes: + - ../study/gridexplore-app-idpSettings.json:/opt/bitnami/apache/htdocs/gridexplore/idpSettings.json:Z + - ../env.json:/opt/bitnami/apache/htdocs/gridexplore/env.json:Z + memswap_limit: 128m + deploy: + resources: + limits: + memory: 128m + restart: unless-stopped + + gridadmin-app: + image: gridsuite/gridadmin-app:latest + ports: + - 82:8080 + volumes: + - ../study/gridadmin-app-idpSettings.json:/opt/bitnami/apache/htdocs/gridadmin/idpSettings.json:Z + - ../env.json:/opt/bitnami/apache/htdocs/gridadmin/env.json:Z + memswap_limit: 128m + deploy: + resources: + limits: + memory: 128m + restart: unless-stopped + + # =========================================================================== + # MOCK & METADATA + # =========================================================================== + mock-user-service: + image: gridsuite/oidc-mock-server + ports: + - 9090:9090 + environment: + - PORT=9090 + - ISSUER_HOST=172.17.0.1:9090 + - USERS_PROFILE=UTILISATEURS|ADMIN|ADMIN_EXPLORE + - CLIENT_COUNT=8 + - CLIENT_ID=gridexplore-client + - CLIENT_REDIRECT_URI=http://localhost:80/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI=http://localhost:80/logout-callback + - CLIENT_SILENT_REDIRECT_URI=http://localhost:80/silent-renew-callback + - CLIENT_ID_2=gridadmin-client + - CLIENT_REDIRECT_URI_2=http://localhost:82/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_2=http://localhost:82/logout-callback + - CLIENT_SILENT_REDIRECT_URI_2=http://localhost:82/silent-renew-callback + - CLIENT_ID_3=griddyna-client + - CLIENT_REDIRECT_URI_3=http://localhost:83/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_3=http://localhost:83/logout-callback + - CLIENT_SILENT_REDIRECT_URI_3=http://localhost:83/silent-renew-callback + - CLIENT_ID_4=gridstudy-client + - CLIENT_REDIRECT_URI_4=http://localhost:84/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_4=http://localhost:84/logout-callback + - CLIENT_SILENT_REDIRECT_URI_4=http://localhost:84/silent-renew-callback + - CLIENT_ID_5=gridexplore-local + - CLIENT_REDIRECT_URI_5=http://localhost:3000/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_5=http://localhost:3000/logout-callback + - CLIENT_SILENT_REDIRECT_URI_5=http://localhost:3000/silent-renew-callback + - CLIENT_ID_6=gridadmin-local + - CLIENT_REDIRECT_URI_6=http://localhost:3002/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_6=http://localhost:3002/logout-callback + - CLIENT_SILENT_REDIRECT_URI_6=http://localhost:3002/silent-renew-callback + - CLIENT_ID_7=griddyna-local + - CLIENT_REDIRECT_URI_7=http://localhost:3003/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_7=http://localhost:3003/logout-callback + - CLIENT_SILENT_REDIRECT_URI_7=http://localhost:3003/silent-renew-callback + - CLIENT_ID_8=gridstudy-local + - CLIENT_REDIRECT_URI_8=http://localhost:3004/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_8=http://localhost:3004/logout-callback + - CLIENT_SILENT_REDIRECT_URI_8=http://localhost:3004/silent-renew-callback + restart: unless-stopped + + apps-metadata-server: + image: bitnami/apache:2.4.55-debian-11-r3@sha256:bbe50190eb3bbf3be6f61318004480b3230846bfd52dec9286bd1862254c1719 + ports: + - 8070:8080 + volumes: + - ../apps-metadata.json:/opt/bitnami/apache/htdocs/apps-metadata.json:Z + - ../../k8s/resources/common/config/apps-metadata-base-voltages.json:/opt/bitnami/apache/htdocs/apps-metadata-base-voltages.json:Z + - ../version.json:/opt/bitnami/apache/htdocs/version.json:Z + - ../gridapps-metadata-httpd.conf:/opt/bitnami/apache/conf/bitnami/bitnami.conf:Z + memswap_limit: 128m + deploy: + resources: + limits: + memory: 128m + restart: unless-stopped diff --git a/docker-compose/multiwar/docker-compose.yml b/docker-compose/multiwar/docker-compose.yml new file mode 100644 index 00000000..ceb04af5 --- /dev/null +++ b/docker-compose/multiwar/docker-compose.yml @@ -0,0 +1,3 @@ +include: + - ../technical/docker-compose.technical.yml + - docker-compose.multiwar.yml diff --git a/docker-compose/multiwar/itools-config.yml b/docker-compose/multiwar/itools-config.yml new file mode 100644 index 00000000..450d1e26 --- /dev/null +++ b/docker-compose/multiwar/itools-config.yml @@ -0,0 +1,46 @@ +# Merged itools config for all WAR-deployed servers in Tomcat. +# Mounted at /home/powsybl/.itools/config.yml + +hades2: + homeDir: /hades2 + debug: false + +dynaflow: + homeDir: /dynaflow-launcher + debug: false + +dynawo: + homeDir: /dynaflow-launcher + debug: false + +dynawo-algorithms: + homeDir: /dynaflow-launcher + debug: false + +network: + default-impl-name: NetworkStore + +dynaflow-default-parameters: + mergeLoads: false + +computation-local: + available-core: 4 + +load-flow-default-parameters: + voltageInitMode: DC_VALUES + phaseShifterRegulationOn: true + twtSplitShuntAdmittance: true + dcUseTransformerRatio: false + countriesToBalance: + - FR + +open-loadflow-default-parameters: + writeReferenceTerminals: false + slackDistributionFailureBehavior: FAIL + +open-security-analysis-default-parameters: + threadCount: 2 + +import-export-parameters-default-value: + iidm.import.cgmes.cgm-with-subnetworks: false + ucte.import.create-areas: false diff --git a/docker-compose/multiwar/prepare.sh b/docker-compose/multiwar/prepare.sh new file mode 100755 index 00000000..172071c2 --- /dev/null +++ b/docker-compose/multiwar/prepare.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# +# prepare.sh — Build WAR files and populate webapps/ for Docker Tomcat. +# +# Usage: +# ./prepare.sh [--skip-build] +# +# Prerequisites: +# - Java 17+ and Maven 3.8+ installed +# - Server submodules checked out +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WAR_DEPLOY="$(cd "$SCRIPT_DIR" && realpath ../../../../tools/war-deploy/war-deploy.sh)" +WEBAPPS_DIR="$SCRIPT_DIR/webapps" + +SKIP_BUILD=false +[[ "${1:-}" == "--skip-build" ]] && SKIP_BUILD=true + +if ! $SKIP_BUILD; then + echo "=== Building WARs (this may take a while)... ===" + "$WAR_DEPLOY" --build --docker +fi + +echo "=== Copying WARs to webapps/ ===" +rm -rf "$WEBAPPS_DIR" +mkdir -p "$WEBAPPS_DIR" + +WRAPPERS_DIR="$(dirname "$WAR_DEPLOY")/war-wrappers" +for war in "$WRAPPERS_DIR"/*/target/*.war; do + [[ -f "$war" ]] || continue + cp "$war" "$WEBAPPS_DIR/" + echo " $(basename "$war")" +done + +echo "=== Copying per-WAR config overrides ===" +rm -rf "$SCRIPT_DIR/config" +if [[ -d "$WRAPPERS_DIR/config" ]]; then + cp -r "$WRAPPERS_DIR/config" "$SCRIPT_DIR/config" + echo " $(ls "$SCRIPT_DIR/config" | wc -l) server configs copied" +fi + +COUNT=$(ls -1 "$WEBAPPS_DIR"/*.war 2>/dev/null | wc -l) +echo "=== Done: $COUNT WARs copied to webapps/ ===" +echo "" +echo "Now run: docker compose up" diff --git a/docker-compose/multiwar/server.xml b/docker-compose/multiwar/server.xml new file mode 100644 index 00000000..4685fff4 --- /dev/null +++ b/docker-compose/multiwar/server.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docker-compose/multiwar/shared-config/common-application.yml b/docker-compose/multiwar/shared-config/common-application.yml new file mode 100644 index 00000000..c8a0189c --- /dev/null +++ b/docker-compose/multiwar/shared-config/common-application.yml @@ -0,0 +1,56 @@ +# Common application config for non-WAR services in multiwar deployment. +# Points service base-uris to the single Tomcat instance. + +powsybl: + services: + case-server: + base-uri: http://tomcat:8080/case-server/ + network-store-server: + base-uri: http://tomcat:8080/network-store-server/ + network-conversion-server: + base-uri: http://tomcat:8080/network-conversion-server/ + single-line-diagram-server: + base-uri: http://tomcat:8080/single-line-diagram-server/ + +gridsuite: + services: + directory-server: + base-uri: http://tomcat:8080/directory-server/ + explore-server: + base-uri: http://tomcat:8080/explore-server/ + study-server: + base-uri: http://tomcat:8080/study-server/ + actions-server: + base-uri: http://tomcat:8080/actions-server/ + filter-server: + base-uri: http://tomcat:8080/filter-server/ + user-admin-server: + base-uri: http://tomcat:8080/user-admin-server/ + config-server: + base-uri: http://config-server:80 + report-server: + base-uri: http://tomcat:8080/report-server/ + network-modification-server: + base-uri: http://tomcat:8080/network-modification-server/ + geo-data-server: + base-uri: http://tomcat:8080/geo-data-server/ + network-map-server: + base-uri: http://tomcat:8080/network-map-server/ + loadflow-server: + base-uri: http://tomcat:8080/loadflow-server/ + security-analysis-server: + base-uri: http://tomcat:8080/security-analysis-server/ + sensitivity-analysis-server: + base-uri: http://tomcat:8080/sensitivity-analysis-server/ + shortcircuit-server: + base-uri: http://tomcat:8080/shortcircuit-server/ + voltage-init-server: + base-uri: http://tomcat:8080/voltage-init-server/ + dynamic-simulation-server: + base-uri: http://tomcat:8080/dynamic-simulation-server/ + timeseries-server: + base-uri: http://tomcat:8080/timeseries-server/ + study-config-server: + base-uri: http://tomcat:8080/study-config-server/ + user-identity-server: + base-uri: http://tomcat:8080/user-identity-oidc-replication-server/ From 92830d43191cbc6ef868015086a6afd971086ad3 Mon Sep 17 00:00:00 2001 From: HARPER Jon Date: Thu, 7 May 2026 00:00:36 +0200 Subject: [PATCH 2/7] comments --- .../multiwar/docker-compose.multiwar.yml | 14 +++++++++++- docker-compose/multiwar/itools-config.yml | 22 ++++++++++++++++++- docker-compose/multiwar/server.xml | 6 +++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docker-compose/multiwar/docker-compose.multiwar.yml b/docker-compose/multiwar/docker-compose.multiwar.yml index abd3376d..78506430 100644 --- a/docker-compose/multiwar/docker-compose.multiwar.yml +++ b/docker-compose/multiwar/docker-compose.multiwar.yml @@ -1,24 +1,36 @@ services: # =========================================================================== - # SINGLE TOMCAT — all WAR-deployed microservices + # SINGLE TOMCAT — all WAR-deployed microservices (33 webmvc servers) # =========================================================================== tomcat: + # JDK 21 required: servers are compiled with Java 21 (class file version 65) image: tomcat:10.1-jdk21 ports: - "${TOMCAT_PORT:-12345}:8080" volumes: + # Custom server.xml: sets Host startStopThreads="10" for parallel WAR deployment - ./server.xml:/usr/local/tomcat/conf/server.xml:Z - ./webapps:/usr/local/tomcat/webapps:Z + # Per-WAR config overrides (base-uri rewrites) loaded via spring.config.additional-location + # in each WAR's SpringBootServletInitializer. Only contains service base-uri mappings. - ./config:/config:Z + # Merged powsybl itools config (loadflow params, writeReferenceTerminals: false, etc.) - ./itools-config.yml:/root/.itools/config.yml:Z environment: + # spring.profiles.active=default: disables "local" profile so application-local.yml + # files embedded in server JARs are NOT loaded. Infrastructure defaults (rabbitmq, + # postgres, elasticsearch hosts) come from powsybl-ws-commons.jar application.yaml + # which already has the correct docker service names. - CATALINA_OPTS=-Dspring.profiles.active=default -Xmx2g depends_on: - postgres - rabbitmq - elasticsearch - s3-storage + # Wait for elasticsearch before starting Tomcat: WARs that depend on elasticsearch + # (study-server, directory-server, case-server, network-conversion-server) fail + # immediately if ES is not reachable at startup (no built-in retry). entrypoint: ["/bin/bash", "-c", "until curl -sf http://elasticsearch:9200/_cluster/health; do echo 'Waiting for elasticsearch...'; sleep 3; done && catalina.sh run"] restart: unless-stopped memswap_limit: 6g diff --git a/docker-compose/multiwar/itools-config.yml b/docker-compose/multiwar/itools-config.yml index 450d1e26..ff2716b4 100644 --- a/docker-compose/multiwar/itools-config.yml +++ b/docker-compose/multiwar/itools-config.yml @@ -1,5 +1,25 @@ # Merged itools config for all WAR-deployed servers in Tomcat. -# Mounted at /home/powsybl/.itools/config.yml +# Mounted at /root/.itools/config.yml (Tomcat container runs as root). +# +# In k8s, each server has its own config.yml. Here we merge them all because +# all 33 WARs share a single JVM. Key settings: +# - writeReferenceTerminals: false — workaround for ReferenceTerminals extension +# not implemented in network-store (causes InvalidClassException without it) +# - network.default-impl-name: NetworkStore — forces iidm to use network-store +# instead of in-memory impl (which is on classpath due to cvg-extension) +# - computation-local.available-core: 4 — limits concurrent computation processes +# +# Source files merged: +# k8s/resources/common/config/loadflow-server-config.yml +# k8s/resources/common/config/network-conversion-server-config.yml +# k8s/resources/study/config/network-modification-server-config.yml +# k8s/resources/study/config/dynawo-itools-config.yml +# k8s/resources/study/config/voltage-init-server-config.yml +# k8s/resources/study/config/sensitivity-analysis-server-config.yml +# k8s/resources/study/config/security-analysis-server-config.yml +# k8s/resources/study/config/shortcircuit-server-config.yml +# k8s/resources/monitor/config/monitor-lf-worker-server-config.yml +# k8s/resources/monitor/config/monitor-sa-worker-server-config.yml hades2: homeDir: /hades2 diff --git a/docker-compose/multiwar/server.xml b/docker-compose/multiwar/server.xml index 4685fff4..957a5a26 100644 --- a/docker-compose/multiwar/server.xml +++ b/docker-compose/multiwar/server.xml @@ -1,4 +1,10 @@ + From 3c7f8f68784945f4bb791cccbed9c5c383f6ce68 Mon Sep 17 00:00:00 2001 From: HARPER Jon Date: Thu, 7 May 2026 00:46:20 +0200 Subject: [PATCH 3/7] cleanup --- .../multiwar/docker-compose.multiwar.yml | 10 +++- docker-compose/multiwar/prepare.sh | 47 ------------------- .../shared-config/common-application.yml | 2 +- 3 files changed, 9 insertions(+), 50 deletions(-) delete mode 100755 docker-compose/multiwar/prepare.sh diff --git a/docker-compose/multiwar/docker-compose.multiwar.yml b/docker-compose/multiwar/docker-compose.multiwar.yml index 78506430..e08012c5 100644 --- a/docker-compose/multiwar/docker-compose.multiwar.yml +++ b/docker-compose/multiwar/docker-compose.multiwar.yml @@ -33,14 +33,20 @@ services: # immediately if ES is not reachable at startup (no built-in retry). entrypoint: ["/bin/bash", "-c", "until curl -sf http://elasticsearch:9200/_cluster/health; do echo 'Waiting for elasticsearch...'; sleep 3; done && catalina.sh run"] restart: unless-stopped - memswap_limit: 6g + memswap_limit: 5g deploy: resources: limits: - memory: 6g + memory: 5g + # Future: move shared JARs (Spring, Hibernate, powsybl-*) to Tomcat lib/ so + # they are loaded once by the common classloader, reducing Metaspace usage. + # Current setup: each WAR bundles ~200MB of dependencies, many identical across + # WARs. Shared classloading could cut memory by 30-50%. # =========================================================================== # NON-WAR SERVICES (WebFlux / notification servers) + # Future: avoid redeclaring these services here — find a way to reuse the + # definitions from docker-compose.base.yml directly. # =========================================================================== config-server: image: gridsuite/config-server:latest diff --git a/docker-compose/multiwar/prepare.sh b/docker-compose/multiwar/prepare.sh deleted file mode 100755 index 172071c2..00000000 --- a/docker-compose/multiwar/prepare.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -# -# prepare.sh — Build WAR files and populate webapps/ for Docker Tomcat. -# -# Usage: -# ./prepare.sh [--skip-build] -# -# Prerequisites: -# - Java 17+ and Maven 3.8+ installed -# - Server submodules checked out -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WAR_DEPLOY="$(cd "$SCRIPT_DIR" && realpath ../../../../tools/war-deploy/war-deploy.sh)" -WEBAPPS_DIR="$SCRIPT_DIR/webapps" - -SKIP_BUILD=false -[[ "${1:-}" == "--skip-build" ]] && SKIP_BUILD=true - -if ! $SKIP_BUILD; then - echo "=== Building WARs (this may take a while)... ===" - "$WAR_DEPLOY" --build --docker -fi - -echo "=== Copying WARs to webapps/ ===" -rm -rf "$WEBAPPS_DIR" -mkdir -p "$WEBAPPS_DIR" - -WRAPPERS_DIR="$(dirname "$WAR_DEPLOY")/war-wrappers" -for war in "$WRAPPERS_DIR"/*/target/*.war; do - [[ -f "$war" ]] || continue - cp "$war" "$WEBAPPS_DIR/" - echo " $(basename "$war")" -done - -echo "=== Copying per-WAR config overrides ===" -rm -rf "$SCRIPT_DIR/config" -if [[ -d "$WRAPPERS_DIR/config" ]]; then - cp -r "$WRAPPERS_DIR/config" "$SCRIPT_DIR/config" - echo " $(ls "$SCRIPT_DIR/config" | wc -l) server configs copied" -fi - -COUNT=$(ls -1 "$WEBAPPS_DIR"/*.war 2>/dev/null | wc -l) -echo "=== Done: $COUNT WARs copied to webapps/ ===" -echo "" -echo "Now run: docker compose up" diff --git a/docker-compose/multiwar/shared-config/common-application.yml b/docker-compose/multiwar/shared-config/common-application.yml index c8a0189c..45cb98c0 100644 --- a/docker-compose/multiwar/shared-config/common-application.yml +++ b/docker-compose/multiwar/shared-config/common-application.yml @@ -53,4 +53,4 @@ gridsuite: study-config-server: base-uri: http://tomcat:8080/study-config-server/ user-identity-server: - base-uri: http://tomcat:8080/user-identity-oidc-replication-server/ + base-uri: http://tomcat:8080/user-identity-server/ From 03a99b47bff9cb3f971147363b1ccc6d477291e6 Mon Sep 17 00:00:00 2001 From: HARPER Jon Date: Thu, 7 May 2026 01:46:46 +0200 Subject: [PATCH 4/7] refactor --- docker-compose/multiwar/README.md | 58 ++ .../multiwar/docker-compose.multiwar.yml | 14 +- .../common-application.yml | 0 docker-compose/multiwar/wars.sh | 516 ++++++++++++++++++ 4 files changed, 581 insertions(+), 7 deletions(-) create mode 100644 docker-compose/multiwar/README.md rename docker-compose/multiwar/{shared-config => nonwars-to-tomcat-config}/common-application.yml (100%) create mode 100755 docker-compose/multiwar/wars.sh diff --git a/docker-compose/multiwar/README.md b/docker-compose/multiwar/README.md new file mode 100644 index 00000000..895e5eaa --- /dev/null +++ b/docker-compose/multiwar/README.md @@ -0,0 +1,58 @@ +# WAR Deploy Tool + +Generated with little supervision by opus4.6. + +Builds GridSuite microservices as WAR files for docker-compose deployment, **without modifying any original source files**. + +## How it works + +Instead of patching server sources, this tool generates **wrapper Maven projects** that: +1. Depend on the original server JAR (classes artifact) +2. Add a generated `SpringBootServletInitializer` class that delegates to the original Application class +3. Package everything as a WAR with a clean context-path name + +``` +Original server (unmodified) Wrapper project (generated) +┌─────────────────────────┐ ┌─────────────────────────┐ +│ report-server/ │ │ war-wrappers/report-server/ +│ pom.xml (jar) │◄─────────│ pom.xml (war) │ +│ ReportApplication.java│ │ WarInitializer.java │ +└─────────────────────────┘ └─────────────────────────┘ + │ │ + ▼ ▼ + gridsuite-report-server.jar report-server.war +``` + +## Quick Start + +```bash +# Build all WARs and prepare for docker-compose +./wars.sh + +# Start the stack +docker compose up +``` + +## Enabling/disabling servers + +Edit the `MANIFEST` array in `war-deploy.sh`. Comment lines with `#` to disable: + +```bash +"actions-server|actions-server||..." # enabled +#"timeseries-server|timeseries-server||..." # disabled +``` + +## Prerequisites + +- Java 21+ +- Maven 3.8+ +- Server submodules checked out and buildable in the layout of Gridsuite Aggregator + +## Excluded (Boot WebFlux — incompatible(??) with WAR) + +- config-server +- config-notification-server +- directory-notification-server +- merge-notification-server +- study-notification-server +- gateway diff --git a/docker-compose/multiwar/docker-compose.multiwar.yml b/docker-compose/multiwar/docker-compose.multiwar.yml index e08012c5..8f9e1ab8 100644 --- a/docker-compose/multiwar/docker-compose.multiwar.yml +++ b/docker-compose/multiwar/docker-compose.multiwar.yml @@ -11,10 +11,10 @@ services: volumes: # Custom server.xml: sets Host startStopThreads="10" for parallel WAR deployment - ./server.xml:/usr/local/tomcat/conf/server.xml:Z - - ./webapps:/usr/local/tomcat/webapps:Z + - ./gen/wars:/usr/local/tomcat/webapps:Z # Per-WAR config overrides (base-uri rewrites) loaded via spring.config.additional-location # in each WAR's SpringBootServletInitializer. Only contains service base-uri mappings. - - ./config:/config:Z + - ./gen/externalized-war-configs:/config:Z # Merged powsybl itools config (loadflow params, writeReferenceTerminals: false, etc.) - ./itools-config.yml:/root/.itools/config.yml:Z environment: @@ -54,7 +54,7 @@ services: - 5025:80 volumes: - ../../k8s/resources/common/config/config-server-application.yml:/config/specific/application.yml:Z - - ./shared-config/common-application.yml:/config/common/application.yml:Z + - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z restart: unless-stopped environment: - JAVA_TOOL_OPTIONS=-Xmx96m @@ -73,7 +73,7 @@ services: - 5024:80 volumes: - ../../k8s/resources/common/config/config-notification-server-application.yml:/config/specific/application.yml:Z - - ./shared-config/common-application.yml:/config/common/application.yml:Z + - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z restart: unless-stopped environment: - JAVA_TOOL_OPTIONS=-Xmx96m @@ -92,7 +92,7 @@ services: - 5009:80 volumes: - ../../k8s/resources/study/config/study-notification-server-application.yml:/config/specific/application.yml:Z - - ./shared-config/common-application.yml:/config/common/application.yml:Z + - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z restart: unless-stopped environment: - JAVA_TOOL_OPTIONS=-Xmx96m @@ -111,7 +111,7 @@ services: - 5004:80 volumes: - ../../k8s/resources/study/config/directory-notification-server-application.yml:/config/specific/application.yml:Z - - ./shared-config/common-application.yml:/config/common/application.yml:Z + - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z restart: unless-stopped environment: - JAVA_TOOL_OPTIONS=-Xmx96m @@ -130,7 +130,7 @@ services: - 9000:80 volumes: - ../../k8s/resources/common/config/gateway-application.yml:/config/specific/application.yml:Z - - ./shared-config/common-application.yml:/config/common/application.yml:Z + - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z - ../allowed-issuers.yml:/config/allowed-issuers.yml:Z restart: unless-stopped environment: diff --git a/docker-compose/multiwar/shared-config/common-application.yml b/docker-compose/multiwar/nonwars-to-tomcat-config/common-application.yml similarity index 100% rename from docker-compose/multiwar/shared-config/common-application.yml rename to docker-compose/multiwar/nonwars-to-tomcat-config/common-application.yml diff --git a/docker-compose/multiwar/wars.sh b/docker-compose/multiwar/wars.sh new file mode 100755 index 00000000..6c2088ca --- /dev/null +++ b/docker-compose/multiwar/wars.sh @@ -0,0 +1,516 @@ +#!/usr/bin/env bash +# +# war-deploy.sh — Build GridSuite microservices as WAR files for docker-compose. +# Zero modifications to original server source files. +# +# Word of Caution, the whole script with comments (except this sentence) +# has been generated by opus4.6 without much look at the details. +# +# Usage: +# ./war-deploy.sh [OPTIONS] +# +# Options: +# --only SRV Only process this server (repeatable) +# --help Show this help +# +# Strategy ("wrapper POM" approach): +# Instead of modifying original Spring Boot server sources to produce WAR files, +# we generate lightweight wrapper Maven projects that: +# 1. Depend on the original server's classes JAR (available because powsybl-parent-ws +# configures spring-boot-maven-plugin with exec, so +# `mvn install` installs both the executable fat JAR and a plain classes JAR) +# 2. Provide a SpringBootServletInitializer subclass (required for WAR deployment) +# 3. Set spring.config.additional-location for per-WAR config overrides +# 4. Package as WAR with = desired context path +# +# This keeps all server submodules UNMODIFIED while producing deployable WARs. +# +# Future improvements: +# - Move shared JARs (Spring, Hibernate, powsybl-commons, etc.) into Tomcat's +# lib/ directory so they are loaded once by the common classloader. This would +# significantly reduce Metaspace usage (each WAR currently loads its own copy +# of every dependency class). +# - Reduce duplication in config override generation: many servers share the same +# service references (e.g. network-store, report-server). A template or merge +# system could replace per-server YAML generation. +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +AGGREGATOR_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +SERVERS_DIR="$AGGREGATOR_ROOT/backend/servers" +WRAPPERS_DIR="$SCRIPT_DIR/gen/war-wrappers" +WEBAPPS_DIR="$SCRIPT_DIR/gen/wars" +CONFIG_DIR="$SCRIPT_DIR/gen/externalized-war-configs" + +# Base URL used inside docker network (container-to-container) +TOMCAT_BASE_URL="http://tomcat:8080" + +# Defaults +ONLY_SERVERS=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --only) ONLY_SERVERS+=("$2"); shift 2 ;; + --help) sed -n '2,/^$/p' "$0" | sed 's/^# \?//'; exit 0 ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +log() { echo -e "\033[1;34m[WAR]\033[0m $*"; } +err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } + +# =========================================================================== +# SERVER MANIFEST +# Format: context_name|server_folder|pom_subpath|app_class_fqn +# +# context_name: WAR filename / Tomcat context path +# server_folder: folder under backend/servers/ +# pom_subpath: submodule path within server_folder (empty = root) +# app_class_fqn: fully qualified Application class name +# Lines starting with # are disabled. Uncomment to include in the build. +# =========================================================================== +MANIFEST=( +"actions-server|actions-server||org.gridsuite.actions.server.ActionsApplication" +#"balances-adjustment-server|balances-adjustment-server||org.gridsuite.balances.adjustment.server.BalancesAdjustmentApplication" +#"case-import-server|case-import-server||org.gridsuite.caseimport.server.CaseImportApplication" +#"case-validation-server|case-validation-server||org.gridsuite.casevalidation.server.CaseValidationApplication" +#"cgmes-boundary-server|cgmes-boundary-server||org.gridsuite.cgmes.boundary.server.CgmesBoundaryApplication" +#"cgmes-gl-server|cgmes-gl-server||org.gridsuite.cgmes.gl.server.CgmesGlApplication" +"directory-server|directory-server||org.gridsuite.directory.server.DirectoryApplication" +#"dynamic-mapping-server|dynamic-mapping-server||org.gridsuite.mapping.server.MappingApplication" +#"dynamic-margin-calculation-server|dynamic-margin-calculation-server||org.gridsuite.dynamicmargincalculation.server.DynamicMarginCalculationApplication" +#"dynamic-security-analysis-server|dynamic-security-analysis-server||org.gridsuite.dynamicsecurityanalysis.server.DynamicSecurityAnalysisApplication" +#"dynamic-simulation-server|dynamic-simulation-server||org.gridsuite.ds.server.DynamicSimulationApplication" +"explore-server|explore-server||org.gridsuite.explore.server.ExploreApplication" +"filter-server|filter-server||org.gridsuite.filter.server.FilterApplication" +"geo-data-server|geo-data-server||org.gridsuite.geodata.server.GeoDataApplication" +"loadflow-server|loadflow-server||org.gridsuite.loadflow.server.LoadFlowApplication" +#"merge-orchestrator-server|merge-orchestrator-server||org.gridsuite.merge.orchestrator.server.MergeOrchestratorApplication" +"network-map-server|network-map-server||org.gridsuite.network.map.NetworkMapApplication" +"network-modification-server|network-modification-server||org.gridsuite.modification.server.NetworkModificationApplication" +"network-store-server|powsybl-network-store-server|network-store-server|com.powsybl.network.store.server.NetworkStoreApplication" +#"odre-server|odre-server||org.gridsuite.odre.server.OdreApplication" +"case-server|powsybl-case-server||com.powsybl.caseserver.CaseApplication" +"network-conversion-server|powsybl-network-conversion-server||com.powsybl.network.conversion.server.NetworkConversionApplication" +"single-line-diagram-server|powsybl-single-line-diagram-server||com.powsybl.sld.server.SingleLineDiagramApplication" +"report-server|report-server||org.gridsuite.report.server.ReportApplication" +"security-analysis-server|security-analysis-server||org.gridsuite.securityanalysis.server.SecurityAnalysisApplication" +"sensitivity-analysis-server|sensitivity-analysis-server||org.gridsuite.sensitivityanalysis.server.SensitivityAnalysisApplication" +"shortcircuit-server|shortcircuit-server||org.gridsuite.shortcircuit.server.ShortCircuitApplication" +"study-config-server|study-config-server||org.gridsuite.studyconfig.server.StudyConfigApplication" +"study-server|study-server||org.gridsuite.study.server.StudyApplication" +#"timeseries-server|timeseries-server||org.gridsuite.timeseries.server.TimeSeriesApplication" +"user-admin-server|user-admin-server||org.gridsuite.useradmin.server.UserAdminApplication" +"user-identity-server|user-identity-oidc-replication-server||org.gridsuite.useridentity.oidcreplication.server.UserIdentityOidcReplicationApplication" +"voltage-init-server|voltage-init-server||org.gridsuite.voltageinit.server.VoltageInitApplication" +) + +# Filter out commented entries (lines starting with #) +_filtered=() +for _e in "${MANIFEST[@]}"; do + [[ "$_e" =~ ^# ]] || _filtered+=("$_e") +done +MANIFEST=("${_filtered[@]}") +unset _filtered _e + +should_process() { + local ctx="$1" + if [[ ${#ONLY_SERVERS[@]} -gt 0 ]]; then + for s in "${ONLY_SERVERS[@]}"; do + [[ "$s" == "$ctx" ]] && return 0 + done + return 1 + fi + return 0 +} + +# Resolve a service key to its base-uri. +# Trailing slash is required: clients use UriComponentsBuilder.fromUriString(baseUri) +# which concatenates paths like "v1/networks/..." directly after the base URI. +# Without trailing slash: "http://tomcat:8080/network-store-serverv1/..." (broken) +# With trailing slash: "http://tomcat:8080/network-store-server/v1/..." (correct) +resolve_base_uri() { + local svc_key="$1" + echo "${TOMCAT_BASE_URL}/${svc_key}/" +} + +# Generate per-WAR config override with base-uri values pointing to Tomcat. +# Since we disable the "local" profile, we only need base-uri overrides in application.yml. +# Reads the original application-local file to discover which services are referenced, +# then generates a clean application.yml with only the base-uri section. +generate_local_config_override() { + local ctx="$1" + local server_folder="$2" + local pom_subpath="$3" + local wrapper_dir="$4" + + local module_dir="$SERVERS_DIR/$server_folder" + [[ -n "$pom_subpath" ]] && module_dir="$module_dir/$pom_subpath" + + # Find the original application-local file to discover service references + local orig_file="" + for ext in yaml yml; do + if [[ -f "$module_dir/src/main/resources/application-local.$ext" ]]; then + orig_file="$module_dir/src/main/resources/application-local.$ext" + break + fi + done + [[ -z "$orig_file" ]] && return 0 + + # Detect which format gridsuite: services: uses (list-style vs map-style) + local uses_list_style=false + if grep -qE '^\s+-\s*(name:|$)' "$orig_file" 2>/dev/null; then + uses_list_style=true + fi + + # Extract service keys referenced in the file + local powsybl_services=() gridsuite_services=() + local current_section="" + + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ^powsybl: ]]; then + current_section="powsybl" + elif [[ "$line" =~ ^gridsuite: ]]; then + current_section="gridsuite" + elif [[ ! "$line" =~ ^[[:space:]] && ! "$line" =~ ^# && -n "$line" ]]; then + current_section="other" + fi + + # Map-style key: " server-name:" + if [[ "$line" =~ ^[[:space:]]{4}([a-z][a-z0-9-]+):$ ]]; then + local key="${BASH_REMATCH[1]}" + if [[ "$current_section" == "powsybl" ]]; then + powsybl_services+=("$key") + elif [[ "$current_section" == "gridsuite" ]]; then + gridsuite_services+=("$key") + fi + fi + # List-style: "- name: xxx" or " name: xxx" + if [[ "$line" =~ ^[[:space:]]+-[[:space:]]*name:[[:space:]]*(.+) ]]; then + local key="${BASH_REMATCH[1]}" + key="${key%%[[:space:]]}" + [[ "$current_section" == "gridsuite" ]] && gridsuite_services+=("$key") + elif [[ "$line" =~ ^[[:space:]]+name:[[:space:]]*(.+) && "$current_section" == "gridsuite" ]]; then + local key="${BASH_REMATCH[1]}" + key="${key%%[[:space:]]}" + gridsuite_services+=("$key") + fi + done < "$orig_file" + + # If no services referenced, no config needed + if [[ ${#powsybl_services[@]} -eq 0 && ${#gridsuite_services[@]} -eq 0 ]]; then + return 0 + fi + + local out_file="$CONFIG_DIR/$ctx/application.yml" + mkdir -p "$CONFIG_DIR/$ctx" + + local output="" + + # Generate powsybl: services: section (always map-style) + if [[ ${#powsybl_services[@]} -gt 0 ]]; then + output+="powsybl:"$'\n' + output+=" services:"$'\n' + for svc in "${powsybl_services[@]}"; do + local uri + uri=$(resolve_base_uri "$svc") + output+=" ${svc}:"$'\n' + output+=" base-uri: ${uri}"$'\n' + done + fi + + # Generate gridsuite: services: section + if [[ ${#gridsuite_services[@]} -gt 0 ]]; then + [[ -n "$output" ]] && output+=$'\n' + output+="gridsuite:"$'\n' + output+=" services:"$'\n' + if $uses_list_style; then + for svc in "${gridsuite_services[@]}"; do + local uri + uri=$(resolve_base_uri "$svc") + output+=" -"$'\n' + output+=" name: ${svc}"$'\n' + output+=" base-uri: ${uri}"$'\n' + done + else + for svc in "${gridsuite_services[@]}"; do + local uri + uri=$(resolve_base_uri "$svc") + output+=" ${svc}:"$'\n' + output+=" base-uri: ${uri}"$'\n' + done + fi + fi + + echo -n "$output" > "$out_file" +} + +# =========================================================================== +# PHASE 1: GENERATE WRAPPER PROJECTS +# =========================================================================== +generate_wrappers() { + log "=== Generating wrapper projects ===" + rm -rf "$WRAPPERS_DIR" + mkdir -p "$WRAPPERS_DIR" + + # Detect gridsuite-dependencies BOM version + local first_entry="${MANIFEST[0]}" + IFS='|' read -r _ctx first_folder first_sub _ <<< "$first_entry" + local first_dir="$SERVERS_DIR/$first_folder" + [[ -n "$first_sub" ]] && first_dir="$first_dir/$first_sub" + local gridsuite_deps_version + gridsuite_deps_version=$(grep -m1 "gridsuite-dependencies.version" "$first_dir/pom.xml" 2>/dev/null | sed 's/.*>\(.*\)<.*/\1/' || true) + [[ -z "$gridsuite_deps_version" ]] && gridsuite_deps_version="50.0.0" + log " gridsuite-dependencies version: $gridsuite_deps_version" + + local modules="" + + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + + local module_dir="$SERVERS_DIR/$server_folder" + [[ -n "$pom_subpath" ]] && module_dir="$module_dir/$pom_subpath" + + # Get groupId, artifactId, version from the server's pom + local group_id artifact_id version parent_group parent_version + + parent_group=$(sed -n '//,/<\/parent>/p' "$module_dir/pom.xml" | grep '' | sed 's/.*//;s/<.*//' | tr -d ' ') + parent_version=$(sed -n '//,/<\/parent>/p' "$module_dir/pom.xml" | grep '' | sed 's/.*//;s/<.*//' | tr -d ' ') + artifact_id=$(sed -n '/<\/parent>/,$ p' "$module_dir/pom.xml" | grep -m1 '' | sed 's/.*//;s/<.*//' | tr -d ' ') + version=$(sed -n '/<\/parent>/,/<\(build\|dependencies\|dependencyManagement\|properties\)>/ p' "$module_dir/pom.xml" | grep -m1 '' | sed 's/.*//;s/<.*//' | tr -d ' ' || true) + group_id=$(sed -n '/<\/parent>/,/<\(build\|dependencies\|dependencyManagement\|properties\)>/ p' "$module_dir/pom.xml" | grep -m1 '' | sed 's/.*//;s/<.*//' | tr -d ' ' || true) + + # Fall back to parent values if not specified at project level + [[ -z "$group_id" ]] && group_id="$parent_group" + [[ -z "$version" ]] && version="$parent_version" + + local wrapper_dir="$WRAPPERS_DIR/$ctx" + mkdir -p "$wrapper_dir/src/main/java/org/gridsuite/war" + + # Generate WarInitializer.java + # Sets spring.config.additional-location to load per-WAR base-uri overrides from + # an external directory (/config//application.yml). "optional:" prefix means + # servers with no service references (e.g. report-server) won't fail if the + # directory doesn't exist. This is the Spring Boot guaranteed way to override + # config — additional-location properties always win over classpath configs. + local simple_class="${app_class##*.}" + local initializer_class="${simple_class/Application/WarInitializer}" + cat > "$wrapper_dir/src/main/java/org/gridsuite/war/${initializer_class}.java" << JAVA +package org.gridsuite.war; + +import ${app_class}; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +public class ${initializer_class} extends SpringBootServletInitializer { + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(${simple_class}.class) + .properties("spring.config.additional-location=optional:file:/config/${ctx}/"); + } +} +JAVA + + # Generate wrapper pom.xml + local extra_deps="" + # antlr4 version override: these servers depend on powsybl-open-loadflow which + # pulls in graphviz-builder → antlr4:4.5.1. But Spring Data JPA's HqlLexer was + # compiled with antlr4 4.13.0 (ATN version 4). The old antlr4-4.5.1.jar contains + # ATNDeserializer that only understands ATN version 3, causing: + # InvalidClassException: org.antlr.v4.runtime.atn.ATN; Could not deserialize ATN with version 4 + # Fix: force antlr4 (the full tool jar, which includes the runtime) to 4.13.0. + case "$ctx" in + loadflow-server|security-analysis-server|sensitivity-analysis-server|network-modification-server) + extra_deps=' + + org.antlr + antlr4 + 4.13.0 + ' + ;; + esac + + cat > "$wrapper_dir/pom.xml" << POM + + + 4.0.0 + + + org.gridsuite.war + war-wrappers + 1.0.0 + + + ${ctx}-war + war + + + + ${group_id} + ${artifact_id} + ${version} + ${extra_deps} + + + + ${ctx} + + +POM + + modules="$modules $ctx\n" + log " ${ctx}-war -> $module_dir" + done + + # Generate reactor parent pom + cat > "$WRAPPERS_DIR/pom.xml" << POM + + + 4.0.0 + + org.gridsuite.war + war-wrappers + 1.0.0 + pom + + + 21 + 21 + + + + + + + org.gridsuite + gridsuite-dependencies + ${gridsuite_deps_version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + + +$(echo -e "$modules") + +POM + + # Generate per-WAR config overrides directly into gen/externalized-war-configs/ + rm -rf "$CONFIG_DIR" + log "=== Generating externalized WAR additional-configs ===" + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + local wrapper_dir="$WRAPPERS_DIR/$ctx" + generate_local_config_override "$ctx" "$server_folder" "$pom_subpath" "$wrapper_dir" + done + ls -1 "$CONFIG_DIR"/*/* 2>/dev/null || true + + +} + +# =========================================================================== +# PHASE 2: BUILD ALL (SINGLE REACTOR) +# Builds ALL server source modules + wrapper projects in one Maven invocation. +# Server source modules are added to the reactor so that their classes JARs +# are available in the local repo for wrapper dependency resolution. +# -T2.0C = 2 threads per CPU core for parallel module builds. +# =========================================================================== +build_wars() { + # Prefer mvnd (Maven Daemon) for faster builds; fall back to mvn -q + local mvn_cmd + if command -v mvnd &>/dev/null; then + mvn_cmd="mvnd" + log "=== Building all with mvnd ===" + else + mvn_cmd="mvn -T2.0C -q" + log "=== Building all with mvn (mvnd not found, using -T2.0C -q) ===" + fi + + # Add server source modules to reactor POM for a single build + local server_modules="" + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + + local rel_path="../../../../../../backend/servers/$server_folder" + [[ -n "$pom_subpath" ]] && rel_path="$rel_path/$pom_subpath" + server_modules+=" ${rel_path}\n" + done + + # Inject server modules into reactor POM (before wrapper modules) + local wrapper_pom="$WRAPPERS_DIR/pom.xml" + sed -i "s| | \n \n${server_modules} |" "$wrapper_pom" + + log "Running: $mvn_cmd package -DskipTests -f $WRAPPERS_DIR/pom.xml" + (cd "$WRAPPERS_DIR" && $mvn_cmd package -DskipTests) || { + err "Reactor build failed" + return 1 + } + + # Verify + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + if [[ ! -f "$WRAPPERS_DIR/$ctx/target/${ctx}.war" ]]; then + err "Missing: $WRAPPERS_DIR/$ctx/target/${ctx}.war" + fi + done +} + +# =========================================================================== +# PHASE 3: COPY WARS TO DOCKER-COMPOSE MOUNT DIR +# =========================================================================== +deploy_to_compose_dirs() { + log "=== Copying WARs to gen/wars/ ===" + rm -rf "$WEBAPPS_DIR" + mkdir -p "$WEBAPPS_DIR" + + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + + local war="$WRAPPERS_DIR/$ctx/target/${ctx}.war" + if [[ -f "$war" ]]; then + cp "$war" "$WEBAPPS_DIR/" + else + err "Missing: $war" + fi + done + + ls -1 "$WEBAPPS_DIR"/*.war 2>/dev/null + echo "" + echo "Folders gen/wars/ and gen/externalized-war-configs/ ready for \$ docker compose up" +} + +# =========================================================================== +# MAIN +# =========================================================================== +generate_wrappers +build_wars +deploy_to_compose_dirs From 8f81015970267512f36f79f2cbe43cdc0e45dff7 Mon Sep 17 00:00:00 2001 From: HARPER Jon Date: Thu, 7 May 2026 01:58:43 +0200 Subject: [PATCH 5/7] improve --- docker-compose/multiwar/wars.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose/multiwar/wars.sh b/docker-compose/multiwar/wars.sh index 6c2088ca..3a5f26cf 100755 --- a/docker-compose/multiwar/wars.sh +++ b/docker-compose/multiwar/wars.sh @@ -429,7 +429,7 @@ POM local wrapper_dir="$WRAPPERS_DIR/$ctx" generate_local_config_override "$ctx" "$server_folder" "$pom_subpath" "$wrapper_dir" done - ls -1 "$CONFIG_DIR"/*/* 2>/dev/null || true + (cd "$SCRIPT_DIR" && find gen/externalized-war-configs -type f 2>/dev/null) || true } @@ -488,7 +488,7 @@ build_wars() { # =========================================================================== deploy_to_compose_dirs() { log "=== Copying WARs to gen/wars/ ===" - rm -rf "$WEBAPPS_DIR" + rm -f "$WEBAPPS_DIR"/*.war 2>/dev/null || true mkdir -p "$WEBAPPS_DIR" for entry in "${MANIFEST[@]}"; do @@ -503,7 +503,7 @@ deploy_to_compose_dirs() { fi done - ls -1 "$WEBAPPS_DIR"/*.war 2>/dev/null + (cd "$SCRIPT_DIR" && find gen/wars -maxdepth 1 -name '*.war' -type f 2>/dev/null) || true echo "" echo "Folders gen/wars/ and gen/externalized-war-configs/ ready for \$ docker compose up" } From 47327056bc000f78709e7081976f6d2aaee494d2 Mon Sep 17 00:00:00 2001 From: HARPER Jon Date: Fri, 8 May 2026 23:07:08 +0200 Subject: [PATCH 6/7] shared spring --- .../multiwar/SHARED_CLASSLOADING_ISSUES.txt | 87 ++++ .../multiwar/docker-compose.multiwar.yml | 13 +- docker-compose/multiwar/server.xml | 8 +- docker-compose/multiwar/wars.sh | 467 ++++++++++++++---- 4 files changed, 469 insertions(+), 106 deletions(-) create mode 100644 docker-compose/multiwar/SHARED_CLASSLOADING_ISSUES.txt diff --git a/docker-compose/multiwar/SHARED_CLASSLOADING_ISSUES.txt b/docker-compose/multiwar/SHARED_CLASSLOADING_ISSUES.txt new file mode 100644 index 00000000..45794247 --- /dev/null +++ b/docker-compose/multiwar/SHARED_CLASSLOADING_ISSUES.txt @@ -0,0 +1,87 @@ +Shared Classloading Issues — WAR Deployment on Single Tomcat +============================================================= + +All 21 Spring Boot microservices deployed as WARs on one Tomcat instance, +with all dependency JARs in Tomcat's shared lib/ (common classloader). +Each WAR contains only its own server classes JAR. + +ISSUE 1: Bean definition conflicts +─────────────────────────────────── +Symptom: BeanDefinitionOverrideException — duplicate bean names from + @Configuration classes in gridsuite-computation, gridsuite-network-modification + JARs now visible to ALL webapps via common classloader. +Fix: -Dspring.main.allow-bean-definition-overriding=true in CATALINA_OPTS. + +ISSUE 2: DataSource auto-configuration on non-DB servers +──────────────────────────────────────────────────────── +Symptom: "Could not resolve placeholder 'powsybl-ws.database.name'" on + explore-server, network-conversion-server, network-map-server. +Cause: powsybl-ws-commons JAR in shared lib has application.yaml referencing + DB properties. With HikariCP/spring-jdbc on common classpath, + DataSourceAutoConfiguration activates for all webapps. +Fix: Auto-detect non-DB servers (no JPA/JDBC/Liquibase deps) and add + spring.autoconfigure.exclude entries to their externalized configs: + DataSourceAutoConfiguration, HibernateJpaAutoConfiguration, + LiquibaseAutoConfiguration, DataSourceTransactionManagerAutoConfiguration. + +ISSUE 3: Spring Security activating on all servers +────────────────────────────────────────────────── +Symptom: All servers return "Please sign in" login page. +Cause: Only explore-server depends on spring-security, but security JARs + in shared lib activate SecurityAutoConfiguration for all webapps. +Fix: Auto-detect non-security servers (no spring-boot-starter-security or + spring-security-config in deps) and add exclusions: + SecurityAutoConfiguration, UserDetailsServiceAutoConfiguration, + ManagementWebSecurityAutoConfiguration. + +ISSUE 4: Logback ConcurrentModificationException during parallel deployment +─────────────────────────────────────────────────────────────────────────── +Symptom: ConcurrentModificationException in ContextBase.getCopyOfPropertyMap + during startup. +Cause: startStopThreads="10" in server.xml → 10 WARs deploy simultaneously, + all sharing the same Logback LoggerContext from common classloader. + Race condition in property map access. +Fix: Set startStopThreads="1" in server.xml for sequential deployment. + Shared lib preloads all classes, so sequential is fast enough. + +ISSUE 5: ServiceLoader "not a subtype" errors +────────────────────────────────────────────── +Symptom: ServiceConfigurationError: ActivePowerControlLoader not a subtype + of ExtensionLoader. +Cause: Version mismatch between shared lib (e.g. 1.40.0) and per-WAR + override JARs (1.41.0). Different classloaders load the same + interface → different type identity → ServiceLoader rejects impl. +Fix: Removed per-WAR version overrides entirely. All servers use the + highest version from shared lib (sort -k5Vr). No override JARs in + WEB-INF/lib/ — only the server's own classes JAR. + +ISSUE 6: antlr4 tool JAR conflicts with antlr4-runtime +─────────────────────────────────────────────────────── +Symptom: ATN version mismatch errors from Hibernate HQL parser. +Cause: org.antlr:antlr4:4.5.1 (parser generator tool) bundles old runtime + classes that conflict with antlr4-runtime:4.13.0 used by Hibernate. +Fix: Exclude org.antlr:antlr4: (tool JAR) from shared lib via grep filter. + antlr4-runtime (the actual dependency) is kept. + +ISSUE 7: ResourceBundle not found for reports.properties +──────────────────────────────────────────────────────── +Symptom: MissingResourceException: Can't find bundle for base name + com.powsybl.network.conversion.server.reports. +Cause: MultiBundleMessageTemplateProvider (in powsybl-commons, common CL) + calls ResourceBundle.getBundle() without explicit classloader → + uses caller's CL (common CL) → can't see .properties files inside + each WAR's server JAR in WEB-INF/lib/. +Fix: Extract reports*.properties from all 9 server source trees, package + into a single resource-bundles.jar in shared lib. All bundle paths + are unique (different packages), so no conflicts. + +ISSUE 8: Maven build failures +───────────────────────────── +8a) Thread contention: -T2.0C (40 threads on 20 cores) caused Maven lock + contention. Fixed: -T4. +8b) Missing artifact com.bea.xml:jsr173-ri:1.0 (transitive of stax-utils). + Fixed: wildcard on all shared-deps POM entries since + _shared.txt is already the full transitive closure. +8c) Empty WARs: packagingExcludes=WEB-INF/lib/*.jar removed everything. + maven-war-plugin doesn't support ! negation in packagingExcludes. + Fixed: switched to packagingIncludes (whitelist approach). diff --git a/docker-compose/multiwar/docker-compose.multiwar.yml b/docker-compose/multiwar/docker-compose.multiwar.yml index 8f9e1ab8..5ebfc745 100644 --- a/docker-compose/multiwar/docker-compose.multiwar.yml +++ b/docker-compose/multiwar/docker-compose.multiwar.yml @@ -12,6 +12,10 @@ services: # Custom server.xml: sets Host startStopThreads="10" for parallel WAR deployment - ./server.xml:/usr/local/tomcat/conf/server.xml:Z - ./gen/wars:/usr/local/tomcat/webapps:Z + # Shared JARs: common dependencies across all WARs, loaded once by Tomcat's + # common classloader. Reduces Metaspace usage by avoiding duplicate class loading. + # Shared JARs copied to Tomcat lib/ at startup + - ./gen/shared-lib:/shared-lib:Z # Per-WAR config overrides (base-uri rewrites) loaded via spring.config.additional-location # in each WAR's SpringBootServletInitializer. Only contains service base-uri mappings. - ./gen/externalized-war-configs:/config:Z @@ -22,16 +26,19 @@ services: # files embedded in server JARs are NOT loaded. Infrastructure defaults (rabbitmq, # postgres, elasticsearch hosts) come from powsybl-ws-commons.jar application.yaml # which already has the correct docker service names. - - CATALINA_OPTS=-Dspring.profiles.active=default -Xmx2g + # allow-bean-definition-overriding: required because shared-lib JARs place + # @Configuration classes (e.g. RabbitConsumerAutoConfiguration) on the common + # classloader, visible to all webapps. Without this, bean name collisions between + # shared-lib configs and per-WAR configs cause startup failures. + - CATALINA_OPTS=-Dspring.profiles.active=default -Dspring.main.allow-bean-definition-overriding=true -Xmx2g depends_on: - postgres - rabbitmq - elasticsearch - - s3-storage # Wait for elasticsearch before starting Tomcat: WARs that depend on elasticsearch # (study-server, directory-server, case-server, network-conversion-server) fail # immediately if ES is not reachable at startup (no built-in retry). - entrypoint: ["/bin/bash", "-c", "until curl -sf http://elasticsearch:9200/_cluster/health; do echo 'Waiting for elasticsearch...'; sleep 3; done && catalina.sh run"] + entrypoint: ["/bin/bash", "-c", "cp /shared-lib/*.jar /usr/local/tomcat/lib/ 2>/dev/null; until curl -sf http://elasticsearch:9200/_cluster/health; do echo 'Waiting for elasticsearch...'; sleep 3; done && catalina.sh run"] restart: unless-stopped memswap_limit: 5g deploy: diff --git a/docker-compose/multiwar/server.xml b/docker-compose/multiwar/server.xml index 957a5a26..48994362 100644 --- a/docker-compose/multiwar/server.xml +++ b/docker-compose/multiwar/server.xml @@ -1,9 +1,9 @@ @@ -34,7 +34,7 @@ + startStopThreads="1"> diff --git a/docker-compose/multiwar/wars.sh b/docker-compose/multiwar/wars.sh index 3a5f26cf..658b7a83 100755 --- a/docker-compose/multiwar/wars.sh +++ b/docker-compose/multiwar/wars.sh @@ -25,23 +25,23 @@ # # This keeps all server submodules UNMODIFIED while producing deployable WARs. # -# Future improvements: -# - Move shared JARs (Spring, Hibernate, powsybl-commons, etc.) into Tomcat's -# lib/ directory so they are loaded once by the common classloader. This would -# significantly reduce Metaspace usage (each WAR currently loads its own copy -# of every dependency class). -# - Reduce duplication in config override generation: many servers share the same -# service references (e.g. network-store, report-server). A template or merge -# system could replace per-server YAML generation. +# Shared classloading: all deps (highest version across servers) are placed +# in Tomcat's lib/ via gen/shared-lib/. WARs use packagingIncludes to keep +# only the server's own classes JAR. No per-WAR version overrides — all +# servers use the same shared version to avoid ServiceLoader/SPI classloader +# conflicts. # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" AGGREGATOR_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" SERVERS_DIR="$AGGREGATOR_ROOT/backend/servers" +SERVERS_REACTOR_DIR="$SCRIPT_DIR/gen/servers-reactor" +SHARED_DEPS_DIR="$SCRIPT_DIR/gen/shared-deps" WRAPPERS_DIR="$SCRIPT_DIR/gen/war-wrappers" WEBAPPS_DIR="$SCRIPT_DIR/gen/wars" CONFIG_DIR="$SCRIPT_DIR/gen/externalized-war-configs" +SHARED_LIB_DIR="$SCRIPT_DIR/gen/shared-lib" # Base URL used inside docker network (container-to-container) TOMCAT_BASE_URL="http://tomcat:8080" @@ -247,6 +247,264 @@ generate_local_config_override() { echo -n "$output" > "$out_file" } +# --------------------------------------------------------------------------- +# Add auto-configuration exclusions for servers affected by shared-lib. +# With shared-lib, ALL Spring Boot auto-configuration trigger classes are on +# the common classpath, so auto-configs activate for ALL webapps — even servers +# that don't use a given feature. This function detects which features each +# server actually uses and adds exclusions for the rest. +# --------------------------------------------------------------------------- +add_shared_lib_autoconfig_exclusions() { + local datasource_exclusions=( + "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration" + "org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration" + "org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration" + "org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration" + ) + local security_exclusions=( + "org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration" + "org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration" + "org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration" + ) + + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + + local deps_file="${DEPS_DIR}/${ctx}.parsed" + [[ -f "$deps_file" ]] || continue + + local needs_exclusions=false + local exclusions_to_add=() + + # Check if server uses a database + if ! grep -q 'spring-boot-starter-data-jpa\|spring-data-r2dbc\|spring-boot-starter-jdbc\|liquibase' "$deps_file"; then + needs_exclusions=true + exclusions_to_add+=("${datasource_exclusions[@]}") + fi + + # Check if server uses Spring Security + if ! grep -q 'spring-boot-starter-security\|spring-security-config' "$deps_file"; then + needs_exclusions=true + exclusions_to_add+=("${security_exclusions[@]}") + fi + + $needs_exclusions || continue + + local module_dir="$SERVERS_DIR/$server_folder" + [[ -n "$pom_subpath" ]] && module_dir="$module_dir/$pom_subpath" + + # Find the server's config/application.yaml from source to extract existing exclusions + local config_yaml="$module_dir/src/main/resources/config/application.yaml" + [[ ! -f "$config_yaml" ]] && config_yaml="$module_dir/src/main/resources/config/application.yml" + + # Collect existing exclusions from the server's config/application.yaml + local all_exclusions=() + if [[ -f "$config_yaml" ]]; then + while IFS= read -r line; do + local cls + cls=$(echo "$line" | sed -n 's/^[[:space:]]*-[[:space:]]*//p') + [[ -n "$cls" ]] && all_exclusions+=("$cls") + done < <(sed -n '/autoconfigure:/,/^[^ ]/p' "$config_yaml" | \ + sed -n '/exclude:/,/^[[:space:]]*[^-[:space:]]/p' | \ + grep '^[[:space:]]*-') + fi + + # Add new exclusions (dedup) + for excl in "${exclusions_to_add[@]}"; do + local found=false + for existing in "${all_exclusions[@]}"; do + [[ "$existing" == "$excl" ]] && found=true && break + done + $found || all_exclusions+=("$excl") + done + + # Write/update externalized config + local out_file="$CONFIG_DIR/$ctx/application.yml" + mkdir -p "$CONFIG_DIR/$ctx" + + # Build the exclusion YAML block + local excl_yaml="spring:"$'\n'" autoconfigure:"$'\n'" exclude:"$'\n' + for excl in "${all_exclusions[@]}"; do + excl_yaml+=" - ${excl}"$'\n' + done + + if [[ -f "$out_file" ]]; then + # Prepend to existing config (spring: block must come first) + local existing + existing=$(cat "$out_file") + echo -n "${excl_yaml} +${existing}" > "$out_file" + else + echo -n "$excl_yaml" > "$out_file" + fi + + log " $ctx: added ${#exclusions_to_add[@]} auto-config exclusions" + done +} + +# =========================================================================== +# PHASE 0: FIND COMMON DEPENDENCIES ACROSS ALL SERVERS +# Generates a "servers-reactor" POM that references all original server modules, +# then runs a SINGLE `mvn dependency:list` on that reactor. Each module writes +# its resolved deps to its own target/deps.txt. For each groupId:artifactId, +# the most common version is selected. A "shared-deps" POM is generated with +# all these as compile deps (used later by dependency:copy-dependencies to +# populate Tomcat's lib/). Per-server version overrides are recorded for +# use in packagingExcludes negations. +# =========================================================================== +find_common_deps() { + log "=== Generating servers-reactor POM ===" + rm -rf "$SERVERS_REACTOR_DIR" + mkdir -p "$SERVERS_REACTOR_DIR" + + # Build the list pointing to original server sources + local server_modules="" + local server_count=0 + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + + local rel_path="../../../../../../backend/servers/$server_folder" + [[ -n "$pom_subpath" ]] && rel_path="$rel_path/$pom_subpath" + server_modules+=" ${rel_path}\n" + server_count=$((server_count + 1)) + done + + cat > "$SERVERS_REACTOR_DIR/pom.xml" << POM + + + 4.0.0 + + org.gridsuite.war + servers-reactor + 1.0.0 + pom + + +$(echo -e "$server_modules") + +POM + + # Single Maven invocation: resolve deps for all server modules at once. + # Each module writes target/deps.txt relative to its own basedir. + log "=== Resolving dependencies ($server_count servers, single mvn invocation) ===" + (cd "$SERVERS_REACTOR_DIR" && mvn dependency:list \ + -DincludeScope=runtime \ + -DoutputAbsoluteArtifactFilename=false \ + -DoutputFile=target/deps.txt \ + -q) || { + err "Failed to resolve dependencies" + return 1 + } + + # Parse each module's target/deps.txt + log "=== Analyzing dependencies across $server_count servers ===" + local deps_dir="$SCRIPT_DIR/gen/deps" + rm -rf "$deps_dir" + mkdir -p "$deps_dir" + + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + + local module_dir="$SERVERS_DIR/$server_folder" + [[ -n "$pom_subpath" ]] && module_dir="$module_dir/$pom_subpath" + local deps_file="$module_dir/target/deps.txt" + + if [[ ! -f "$deps_file" ]]; then + err " No deps.txt for $ctx" + continue + fi + + # Parse: extract groupId:artifactId:type:classifier:version + # dependency:list format is g:a:t:v:scope (5 fields) or g:a:t:classifier:v:scope (6 fields) + # Normalize to g:a:type:classifier:version (classifier empty if none) + grep -oP '^\s+\S+' "$deps_file" | sed 's/^[[:space:]]*//' | \ + awk -F: 'NF==5 {print $1":"$2":"$3"::"$4} NF>=6 {print $1":"$2":"$3":"$4":"$5}' | \ + sort -u > "$deps_dir/${ctx}.parsed" + done + + # For each groupId:artifactId:type:classifier, find the most common version. + log "=== Selecting most common version per artifact ===" + + # Collect all deps, count each full coordinate (with version) + cat "$deps_dir"/*.parsed | sort | uniq -c | sort -rn > "$deps_dir/_full_counts.txt" + + # For each g:a:t:classifier key, pick the HIGHEST version. + # Using the highest version avoids ServiceLoader/SPI classloader conflicts + # that occur when a WAR overrides a shared-lib JAR with a different version. + # Output: g:a:type:classifier:version (the "winning" version for shared-lib) + awk '{$1=$1; count=$1; $1=""; sub(/^ /,""); print $0}' "$deps_dir/_full_counts.txt" | \ + sort -t: -k1,4 -k5Vr | \ + awk -F: '{key=$1":"$2":"$3":"$4} !seen[key]++ {print}' | \ + grep ':jar:' | \ + grep -v 'javax\.servlet\|jakarta\.servlet\|tomcat-embed\|spring-boot-starter-tomcat\|org\.antlr:antlr4:' \ + > "$deps_dir/_shared.txt" || true + + local shared_count + shared_count=$(wc -l < "$deps_dir/_shared.txt") + log " $shared_count artifacts selected for shared-lib (highest version each)" + + # Build a lookup file: key -> version (for detecting per-server overrides) + awk -F: '{print $1":"$2":"$3":"$4"="$5}' "$deps_dir/_shared.txt" > "$deps_dir/_shared_lookup.txt" + + # Note: no per-server version overrides. With shared classloading, all servers + # must use the same version from shared-lib. Version overrides (keeping a + # different version in WEB-INF/lib) cause ServiceLoader/SPI classloader conflicts + # because the interface from the parent classloader != the interface from the + # child classloader. + + # Generate shared-deps POM: lists all common deps as compile dependencies. + # Used by dependency:copy-dependencies to populate Tomcat's lib/. + log "=== Generating shared-deps POM ===" + rm -rf "$SHARED_DEPS_DIR" + mkdir -p "$SHARED_DEPS_DIR" + + # Since _shared.txt is already the full transitive closure from the servers, + # exclude all transitives to avoid pulling in artifacts not in Maven Central + # (e.g. com.bea.xml:jsr173-ri via stax-utils). + local shared_deps_xml="" + while IFS=: read -r g a t classifier v; do + shared_deps_xml+=" + + ${g} + ${a} + ${v}" + if [[ -n "$classifier" ]]; then + shared_deps_xml+=" + ${classifier}" + fi + shared_deps_xml+=" + ** + " + done < "$deps_dir/_shared.txt" + + cat > "$SHARED_DEPS_DIR/pom.xml" << POM + + + 4.0.0 + + org.gridsuite.war + shared-deps + 1.0.0 + pom + All shared dependencies (most common version). Used to collect JARs for Tomcat lib/. + + ${shared_deps_xml} + + +POM + + # Export for use by generate_wrappers + SHARED_DEPS_FILE="$deps_dir/_shared.txt" + DEPS_DIR="$deps_dir" +} + # =========================================================================== # PHASE 1: GENERATE WRAPPER PROJECTS # =========================================================================== @@ -255,16 +513,6 @@ generate_wrappers() { rm -rf "$WRAPPERS_DIR" mkdir -p "$WRAPPERS_DIR" - # Detect gridsuite-dependencies BOM version - local first_entry="${MANIFEST[0]}" - IFS='|' read -r _ctx first_folder first_sub _ <<< "$first_entry" - local first_dir="$SERVERS_DIR/$first_folder" - [[ -n "$first_sub" ]] && first_dir="$first_dir/$first_sub" - local gridsuite_deps_version - gridsuite_deps_version=$(grep -m1 "gridsuite-dependencies.version" "$first_dir/pom.xml" 2>/dev/null | sed 's/.*>\(.*\)<.*/\1/' || true) - [[ -z "$gridsuite_deps_version" ]] && gridsuite_deps_version="50.0.0" - log " gridsuite-dependencies version: $gridsuite_deps_version" - local modules="" for entry in "${MANIFEST[@]}"; do @@ -287,15 +535,16 @@ generate_wrappers() { [[ -z "$group_id" ]] && group_id="$parent_group" [[ -z "$version" ]] && version="$parent_version" + # Detect spring-boot version from this server's resolved deps + local spring_boot_version + spring_boot_version=$(grep 'org\.springframework\.boot:spring-boot:jar:' \ + "${DEPS_DIR}/${ctx}.parsed" | awk -F: '{print $5}' | head -1) + [[ -z "$spring_boot_version" ]] && spring_boot_version="3.5.11" + local wrapper_dir="$WRAPPERS_DIR/$ctx" mkdir -p "$wrapper_dir/src/main/java/org/gridsuite/war" # Generate WarInitializer.java - # Sets spring.config.additional-location to load per-WAR base-uri overrides from - # an external directory (/config//application.yml). "optional:" prefix means - # servers with no service references (e.g. report-server) won't fail if the - # directory doesn't exist. This is the Spring Boot guaranteed way to override - # config — additional-location properties always win over classpath configs. local simple_class="${app_class##*.}" local initializer_class="${simple_class/Application/WarInitializer}" cat > "$wrapper_dir/src/main/java/org/gridsuite/war/${initializer_class}.java" << JAVA @@ -314,24 +563,11 @@ public class ${initializer_class} extends SpringBootServletInitializer { } JAVA - # Generate wrapper pom.xml - local extra_deps="" - # antlr4 version override: these servers depend on powsybl-open-loadflow which - # pulls in graphviz-builder → antlr4:4.5.1. But Spring Data JPA's HqlLexer was - # compiled with antlr4 4.13.0 (ATN version 4). The old antlr4-4.5.1.jar contains - # ATNDeserializer that only understands ATN version 3, causing: - # InvalidClassException: org.antlr.v4.runtime.atn.ATN; Could not deserialize ATN with version 4 - # Fix: force antlr4 (the full tool jar, which includes the runtime) to 4.13.0. - case "$ctx" in - loadflow-server|security-analysis-server|sensitivity-analysis-server|network-modification-server) - extra_deps=' - - org.antlr - antlr4 - 4.13.0 - ' - ;; - esac + # Build packagingIncludes: only keep the server's own classes JAR. + # Everything else (shared deps) is excluded from the WAR. + # Non-JAR content (WEB-INF/classes, META-INF) is always included. + local server_jar_name="${artifact_id}-${version}.jar" + local packaging_includes="META-INF/**,WEB-INF/classes/**,WEB-INF/lib/${server_jar_name}" cat > "$wrapper_dir/pom.xml" << POM @@ -354,11 +590,28 @@ JAVA ${group_id} ${artifact_id} ${version} - ${extra_deps} + + + + org.springframework.boot + spring-boot + ${spring_boot_version} + ${ctx} + + + org.apache.maven.plugins + maven-war-plugin + + ${packaging_includes} + + + POM @@ -367,7 +620,7 @@ POM log " ${ctx}-war -> $module_dir" done - # Generate reactor parent pom + # Generate reactor parent pom (minimal: just modules + compiler settings) cat > "$WRAPPERS_DIR/pom.xml" << POM 21 - - - - - org.gridsuite - gridsuite-dependencies - ${gridsuite_deps_version} - pom - import - - - - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-tomcat - provided - - - + + ../servers-reactor + $(echo -e "$modules") POM @@ -429,45 +655,27 @@ POM local wrapper_dir="$WRAPPERS_DIR/$ctx" generate_local_config_override "$ctx" "$server_folder" "$pom_subpath" "$wrapper_dir" done + add_shared_lib_autoconfig_exclusions (cd "$SCRIPT_DIR" && find gen/externalized-war-configs -type f 2>/dev/null) || true - - } # =========================================================================== # PHASE 2: BUILD ALL (SINGLE REACTOR) -# Builds ALL server source modules + wrapper projects in one Maven invocation. -# Server source modules are added to the reactor so that their classes JARs -# are available in the local repo for wrapper dependency resolution. -# -T2.0C = 2 threads per CPU core for parallel module builds. +# war-wrappers/pom.xml includes servers-reactor as a module, so a single +# `mvn package` builds server JARs first (reactor ordering), then WAR wrappers. # =========================================================================== build_wars() { # Prefer mvnd (Maven Daemon) for faster builds; fall back to mvn -q local mvn_cmd if command -v mvnd &>/dev/null; then mvn_cmd="mvnd" - log "=== Building all with mvnd ===" + log "=== Building with mvnd ===" else - mvn_cmd="mvn -T2.0C -q" - log "=== Building all with mvn (mvnd not found, using -T2.0C -q) ===" + mvn_cmd="mvn -T4 -q" + log "=== Building with mvn (mvnd not found, using -T4 -q) ===" fi - # Add server source modules to reactor POM for a single build - local server_modules="" - for entry in "${MANIFEST[@]}"; do - IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" - should_process "$ctx" || continue - - local rel_path="../../../../../../backend/servers/$server_folder" - [[ -n "$pom_subpath" ]] && rel_path="$rel_path/$pom_subpath" - server_modules+=" ${rel_path}\n" - done - - # Inject server modules into reactor POM (before wrapper modules) - local wrapper_pom="$WRAPPERS_DIR/pom.xml" - sed -i "s| | \n \n${server_modules} |" "$wrapper_pom" - - log "Running: $mvn_cmd package -DskipTests -f $WRAPPERS_DIR/pom.xml" + log " Running: $mvn_cmd package -DskipTests -f $WRAPPERS_DIR/pom.xml" (cd "$WRAPPERS_DIR" && $mvn_cmd package -DskipTests) || { err "Reactor build failed" return 1 @@ -505,12 +713,73 @@ deploy_to_compose_dirs() { (cd "$SCRIPT_DIR" && find gen/wars -maxdepth 1 -name '*.war' -type f 2>/dev/null) || true echo "" - echo "Folders gen/wars/ and gen/externalized-war-configs/ ready for \$ docker compose up" + echo "Folders gen/wars/, gen/externalized-war-configs/ and gen/shared-lib/ ready for \$ docker compose up" +} + +# =========================================================================== +# PHASE 4: COLLECT SHARED JARS FOR TOMCAT LIB +# Uses dependency:copy-dependencies on the shared-deps POM to download all +# common JARs into gen/shared-lib/. These are the JARs excluded from WARs +# by packagingExcludes and must be in Tomcat's classpath. +# =========================================================================== +collect_shared_libs() { + log "=== Collecting shared JARs to gen/shared-lib/ ===" + rm -rf "$SHARED_LIB_DIR" + mkdir -p "$SHARED_LIB_DIR" + + (cd "$SHARED_DEPS_DIR" && mvn dependency:copy-dependencies \ + -DoutputDirectory="$SHARED_LIB_DIR" \ + -q) || { + err "Failed to collect shared JARs" + return 1 + } + + # Extract resource bundles (.properties) from each server's classes JAR + # and package them into a single resource-bundles.jar in shared-lib. + # This is needed because MultiBundleMessageTemplateProvider (in powsybl-commons, + # loaded by the common classloader) calls ResourceBundle.getBundle() without + # specifying a classloader, so it uses the caller's CL (common CL) which + # can't see .properties files inside each WAR's WEB-INF/lib/ server JAR. + local bundle_tmp="$SCRIPT_DIR/gen/_resource-bundles" + rm -rf "$bundle_tmp" + mkdir -p "$bundle_tmp" + local found_bundles=false + + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + + local server_dir="$SERVERS_DIR/$server_folder" + # Find reports*.properties in the server source + local res_dir="$server_dir/src/main/resources" + if [[ -d "$res_dir" ]]; then + while IFS= read -r propfile; do + local rel="${propfile#$res_dir/}" + mkdir -p "$bundle_tmp/$(dirname "$rel")" + cp "$propfile" "$bundle_tmp/$rel" + found_bundles=true + done < <(find "$res_dir" -name 'reports*.properties' -type f 2>/dev/null) + fi + done + + if [[ "$found_bundles" == "true" ]]; then + (cd "$bundle_tmp" && jar cf "$SHARED_LIB_DIR/resource-bundles.jar" .) + local bundle_count + bundle_count=$(find "$bundle_tmp" -name '*.properties' -type f | wc -l) + log " Packaged $bundle_count resource bundle files into resource-bundles.jar" + fi + rm -rf "$bundle_tmp" + + local count + count=$(find "$SHARED_LIB_DIR" -name '*.jar' -type f | wc -l) + log " $count JARs collected in gen/shared-lib/" } # =========================================================================== # MAIN # =========================================================================== +find_common_deps generate_wrappers build_wars +collect_shared_libs deploy_to_compose_dirs From 1b13abb9cbf88e4b1235f170933f214f7c60be6d Mon Sep 17 00:00:00 2001 From: HARPER Jon Date: Mon, 11 May 2026 10:16:44 +0200 Subject: [PATCH 7/7] more vibes --- .../multiwar/SHARED_CLASSLOADING_ISSUES.txt | 58 ++++++++++++++++++- .../multiwar/docker-compose.multiwar.yml | 6 +- docker-compose/multiwar/wars.sh | 26 +++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/docker-compose/multiwar/SHARED_CLASSLOADING_ISSUES.txt b/docker-compose/multiwar/SHARED_CLASSLOADING_ISSUES.txt index 45794247..19ae7ab6 100644 --- a/docker-compose/multiwar/SHARED_CLASSLOADING_ISSUES.txt +++ b/docker-compose/multiwar/SHARED_CLASSLOADING_ISSUES.txt @@ -75,7 +75,63 @@ Fix: Extract reports*.properties from all 9 server source trees, package into a single resource-bundles.jar in shared lib. All bundle paths are unique (different packages), so no conflicts. -ISSUE 8: Maven build failures +ISSUE 8: Elasticsearch thread pool explosion (441 threads) +────────────────────────────────────────────────────────── +Symptom: 706 threads total; 441 are elasticsearch-rest-client I/O threads. + 21 ES client pools × 21 threads each (one per CPU core). +Cause: ElasticsearchRestClientAutoConfiguration activates for ALL 21 webapps + because ES client JARs are on the common classpath. Only 5 servers + actually use Elasticsearch (case, directory, network-conversion, + network-modification, study). +Fix: Auto-detect non-ES servers (no elasticsearch/elastic-clients in deps) + and add exclusions: ElasticsearchRestClientAutoConfiguration, + ElasticsearchClientAutoConfiguration, + ReactiveElasticsearchClientAutoConfiguration, + ElasticsearchDataAutoConfiguration, + ElasticsearchRepositoriesAutoConfiguration. + Result: 706 → 456 threads (−250). + +ISSUE 9: OpenTelemetry tracing auto-configuration failure +───────────────────────────────────────────────────────── +Symptom: APPLICATION FAILED TO START — OpenTelemetryTracingAutoConfiguration + required a bean of type 'io.opentelemetry.api.OpenTelemetry'. +Cause: OTel JARs on common classpath activate tracing autoconfigs for all + webapps. Each webapp also creates BatchSpanProcessor and + BatchLogRecordProcessor threads (42 total) needlessly. +Fix: Exclude for all servers (none configure OTel tracing): + OpenTelemetryAutoConfiguration (×2 in different packages), + OpenTelemetryTracingAutoConfiguration, + OtlpAutoConfiguration, OtlpTracingAutoConfiguration, + OpenTelemetryLoggingAutoConfiguration. + +MEMORY USAGE REPORT (21 webapps, shared classloading) +====================================================== +Container: 2.5g limit, no swap | JVM: -Xmx1g, G1GC + +Docker stats: 2.24 GiB / 2.5 GiB (89%) — 488 threads (PIDs) + +NMT committed breakdown (1,619 MB total): + Java Heap 1,020 MB (Xmx 1g, ~300MB used at idle, ~1g under load) + Metaspace 193 MB (43K classes across 21 webapps) + GC (G1) 92 MB (card tables, remembered sets) + Code cache 85 MB (JIT compiled code) + Symbol table 71 MB (interned strings, 582K entries) + Threads 51 MB (488 threads × ~1MB stack) + Class metadata 34 MB (class pointers, vtables) + NMT overhead 14 MB + Other (CDS, etc.) 59 MB + +RSS vs NMT gap (~600 MB): + NMT committed = 1,619 MB, docker reports 2,236 MB. + Difference is thread stack RSS, glibc malloc arenas, mmap'd JARs, + and kernel page table overhead not tracked by NMT. + +Comparison vs 21 separate containers: + Each server standalone uses ~300-500 MB (avg ~400 MB). + 21 × 400 MB = 8.4 GB total. + Shared classloading: 2.24 GB = ~73% memory reduction. + +ISSUE 10: Maven build failures ───────────────────────────── 8a) Thread contention: -T2.0C (40 threads on 20 cores) caused Maven lock contention. Fixed: -T4. diff --git a/docker-compose/multiwar/docker-compose.multiwar.yml b/docker-compose/multiwar/docker-compose.multiwar.yml index 5ebfc745..12183660 100644 --- a/docker-compose/multiwar/docker-compose.multiwar.yml +++ b/docker-compose/multiwar/docker-compose.multiwar.yml @@ -30,7 +30,7 @@ services: # @Configuration classes (e.g. RabbitConsumerAutoConfiguration) on the common # classloader, visible to all webapps. Without this, bean name collisions between # shared-lib configs and per-WAR configs cause startup failures. - - CATALINA_OPTS=-Dspring.profiles.active=default -Dspring.main.allow-bean-definition-overriding=true -Xmx2g + - CATALINA_OPTS=-Dspring.profiles.active=default -Dspring.main.allow-bean-definition-overriding=true -Xmx1g -XX:NativeMemoryTracking=summary depends_on: - postgres - rabbitmq @@ -40,11 +40,11 @@ services: # immediately if ES is not reachable at startup (no built-in retry). entrypoint: ["/bin/bash", "-c", "cp /shared-lib/*.jar /usr/local/tomcat/lib/ 2>/dev/null; until curl -sf http://elasticsearch:9200/_cluster/health; do echo 'Waiting for elasticsearch...'; sleep 3; done && catalina.sh run"] restart: unless-stopped - memswap_limit: 5g + memswap_limit: 2560m deploy: resources: limits: - memory: 5g + memory: 2560m # Future: move shared JARs (Spring, Hibernate, powsybl-*) to Tomcat lib/ so # they are loaded once by the common classloader, reducing Metaspace usage. # Current setup: each WAR bundles ~200MB of dependencies, many identical across diff --git a/docker-compose/multiwar/wars.sh b/docker-compose/multiwar/wars.sh index 658b7a83..2b9c5918 100755 --- a/docker-compose/multiwar/wars.sh +++ b/docker-compose/multiwar/wars.sh @@ -266,6 +266,21 @@ add_shared_lib_autoconfig_exclusions() { "org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration" "org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration" ) + local elasticsearch_exclusions=( + "org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration" + "org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchClientAutoConfiguration" + "org.springframework.boot.autoconfigure.elasticsearch.ReactiveElasticsearchClientAutoConfiguration" + "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration" + "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration" + ) + local otel_exclusions=( + "org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration" + "org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration" + "org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryTracingAutoConfiguration" + "org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration" + "org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingAutoConfiguration" + "org.springframework.boot.actuate.autoconfigure.logging.OpenTelemetryLoggingAutoConfiguration" + ) for entry in "${MANIFEST[@]}"; do IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" @@ -289,6 +304,17 @@ add_shared_lib_autoconfig_exclusions() { exclusions_to_add+=("${security_exclusions[@]}") fi + # Check if server uses Elasticsearch + if ! grep -q 'elasticsearch\|elastic-clients' "$deps_file"; then + needs_exclusions=true + exclusions_to_add+=("${elasticsearch_exclusions[@]}") + fi + + # Check if server uses OpenTelemetry tracing (all get it transitively but none configure it) + # Every webapp creates BatchSpanProcessor + BatchLogRecordProcessor threads unnecessarily + needs_exclusions=true + exclusions_to_add+=("${otel_exclusions[@]}") + $needs_exclusions || continue local module_dir="$SERVERS_DIR/$server_folder"