fix(concurrency): guard 20+ race conditions across getOrCreate, balance updates, and view permissions #1014
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build on Pull Request | |
| on: | |
| pull_request: | |
| branches: | |
| - "**" | |
| env: | |
| DOCKER_HUB_ORGANIZATION: ${{ vars.DOCKER_HUB_ORGANIZATION }} | |
| # --------------------------------------------------------------------------- | |
| # compile — compiles everything once, packages the JAR, uploads classes | |
| # test — 3-way matrix downloads compiled output and runs a shard of tests | |
| # | |
| # Wall-clock target: | |
| # compile ~10 min (parallel with setup of test shards) | |
| # tests ~8 min (3 shards in parallel after compile finishes) | |
| # total ~18 min (vs ~27 min single-job) | |
| # --------------------------------------------------------------------------- | |
| jobs: | |
| # -------------------------------------------------------------------------- | |
| # Job 1: compile | |
| # -------------------------------------------------------------------------- | |
| compile: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up JDK 11 | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: "11" | |
| distribution: "adopt" | |
| cache: maven # caches ~/.m2/repository keyed on pom.xml hash | |
| - name: Setup production props | |
| run: | | |
| cp obp-api/src/main/resources/props/sample.props.template \ | |
| obp-api/src/main/resources/props/production.default.props | |
| - name: Lint — test-isolation (no setPropsValues at class/feature body) | |
| run: python3 .github/scripts/check_test_isolation.py | |
| - name: Compile and install (skip test execution) | |
| run: | | |
| # -DskipTests — compile test sources but do NOT run them | |
| # Test classes must be in target/test-classes for the test shards. | |
| # `clean` for a guaranteed-correct build (see build_container.yml: a no-clean | |
| # Zinc incremental cache saved only ~17s — compile is dominated by Maven + | |
| # fat-jar packaging + install, not Scala compilation). | |
| MAVEN_OPTS="-Xmx3G -Xss2m -XX:MaxMetaspaceSize=1G" \ | |
| mvn clean install -T 4 -Pprod -DskipTests | |
| - name: Upload compiled output | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: compiled-output | |
| retention-days: 1 | |
| # Upload full target dirs — test shards download and run surefire:test | |
| # without recompiling (surefire:test goal bypasses compile lifecycle) | |
| path: | | |
| obp-api/target/ | |
| obp-commons/target/ | |
| - name: Save .jar artifact | |
| run: mkdir -p ./pull && cp obp-api/target/obp-api.jar ./pull/ | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ github.sha }} | |
| path: pull/ | |
| # -------------------------------------------------------------------------- | |
| # Job 2: test (4-way matrix) | |
| # | |
| # Shard assignment (based on actual clean-run timings): | |
| # Shard 1 ~258s v4_0_0(258) | |
| # Shard 2 ~267s v6_0_0(122) v5_0_0(42) v3_0_0(39) v2_1_0(35) v2_2_0(12) … | |
| # Shard 3 ~252s v1_2_1(137) ResourceDocs(67) berlin(34) util(12) … | |
| # Shard 4 ~232s v5_1_0(79) v3_1_0(65) http4sbridge(52) v7_0_0(45) … + catch-all | |
| # -------------------------------------------------------------------------- | |
| test: | |
| needs: compile | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - shard: 1 | |
| name: "v4 only (bottleneck pkg)" | |
| # ~258s — single largest package, kept on its own shard | |
| test_filter: >- | |
| code.api.v4_0_0 | |
| - shard: 2 | |
| name: "v1_2_1 only (largest unsplittable suite, isolated)" | |
| # API1_2_1Test is a single 6604-line suite (~333 scenarios, ~281s). Isolated | |
| # on its own shard: mixing in berlin/management/metrics made this the slowest | |
| # shard (314s); those moved to shards 7/8 to rebalance. | |
| test_filter: >- | |
| code.api.v1_2_1 | |
| - shard: 3 | |
| name: "v6 + v2_x" | |
| test_filter: >- | |
| code.api.v6_0_0 | |
| code.api.v2_1_0 | |
| code.api.v2_2_0 | |
| code.api.v2_0_0 | |
| - shard: 4 | |
| name: "v5_1 + v5_0 + v3_0" | |
| test_filter: >- | |
| code.api.v5_1_0 | |
| code.api.v5_0_0 | |
| code.api.v3_0_0 | |
| - shard: 5 | |
| name: "ResourceDocs + v3_1 + v1_4 + v1_3" | |
| test_filter: >- | |
| code.api.ResourceDocs1_4_0 | |
| code.api.v3_1_0 | |
| code.api.v1_4_0 | |
| code.api.v1_3_0 | |
| - shard: 6 | |
| name: "v7 + http4sbridge + UKOpenBanking" | |
| test_filter: >- | |
| code.api.v7_0_0 | |
| code.api.http4sbridge | |
| code.api.UKOpenBanking | |
| - shard: 7 | |
| name: "model + views + customer + util + small data + berlin" | |
| test_filter: >- | |
| code.model | |
| code.views | |
| code.customer | |
| code.usercustomerlinks | |
| code.api.util | |
| code.errormessages | |
| code.atms | |
| code.branches | |
| code.products | |
| code.crm | |
| code.accountHolder | |
| code.api.berlin | |
| - shard: 8 | |
| name: "connector + auth + login + mgmt + metrics + remaining (catch-all)" | |
| # catch-all shard: appends any test package not assigned to shards 1-7 | |
| # Root-level code.api tests use class-name prefix matching (lowercase classes). | |
| # NOTE: classes that sit DIRECTLY in package code.api must be listed here by | |
| # FQN-prefix — the catch-all marks the parent package code.api as "covered" once | |
| # any child (code.api.v4_0_0, …) is assigned, so it never appends code.api itself. | |
| test_filter: >- | |
| code.connector | |
| code.util | |
| code.api.Authentication | |
| code.api.dauthTest | |
| code.api.DirectLoginTest | |
| code.api.gateWayloginTest | |
| code.api.OBPRestHelperTest | |
| code.api.AliveCheckRoutesTest | |
| code.api.Http4sOpenIdConnect | |
| code.entitlement | |
| code.bankaccountcreation | |
| code.bankconnectors | |
| code.container | |
| code.management | |
| code.metrics | |
| services: | |
| redis: | |
| image: redis:7-alpine # pinned + small (~30MB) to cut redis docker-pull flakiness | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up JDK 11 | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: "11" | |
| distribution: "adopt" | |
| cache: maven | |
| - name: Download compiled output | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: compiled-output | |
| - name: Touch artifact files (prevent Zinc recompilation) | |
| run: | | |
| # actions/download-artifact preserves original compile-job timestamps. | |
| # actions/checkout gives source files the current (later) time. | |
| # Zinc sees sources newer than classes → full recompile (~215 s wasted). | |
| # Touching everything in target/ makes all artifact files appear | |
| # just-downloaded (current time) → newer than sources → Zinc skips. | |
| find obp-api/target obp-commons/target -type f -exec touch {} + 2>/dev/null || true | |
| echo "Touched $(find obp-api/target obp-commons/target -type f 2>/dev/null | wc -l) files" | |
| - name: Install local artifacts into Maven repo | |
| run: | | |
| # The compile runner's ~/.m2 is discarded after that job completes. | |
| # Install the two local multi-module artifacts so scalatest:test can | |
| # resolve com.tesobe:* without hitting remote repos. | |
| # | |
| # 1. Parent POM — obp-commons' pom.xml declares obp-parent as its | |
| # <parent>; Maven fetches it when reading transitive deps. | |
| mvn install:install-file \ | |
| -Dfile=pom.xml \ | |
| -DgroupId=com.tesobe \ | |
| -DartifactId=obp-parent \ | |
| -Dversion=1.10.1 \ | |
| -Dpackaging=pom \ | |
| -DgeneratePom=false | |
| # 2. obp-commons JAR with its full POM (lists compile deps inherited | |
| # by obp-api at test classpath resolution time). | |
| mvn install:install-file \ | |
| -Dfile=obp-commons/target/obp-commons-1.10.1.jar \ | |
| -DpomFile=obp-commons/pom.xml | |
| - name: Setup props | |
| run: | | |
| cp obp-api/src/main/resources/props/sample.props.template \ | |
| obp-api/src/main/resources/props/production.default.props | |
| echo connector=star > obp-api/src/main/resources/props/test.default.props | |
| echo starConnector_supported_types=mapped,internal >> obp-api/src/main/resources/props/test.default.props | |
| echo hostname=http://localhost:8016 >> obp-api/src/main/resources/props/test.default.props | |
| echo tests.port=8016 >> obp-api/src/main/resources/props/test.default.props | |
| echo End of minimum settings >> obp-api/src/main/resources/props/test.default.props | |
| echo payments_enabled=false >> obp-api/src/main/resources/props/test.default.props | |
| echo messageQueue.updateBankAccountsTransaction=false >> obp-api/src/main/resources/props/test.default.props | |
| echo messageQueue.createBankAccounts=false >> obp-api/src/main/resources/props/test.default.props | |
| echo allow_sandbox_account_creation=true >> obp-api/src/main/resources/props/test.default.props | |
| echo allow_sandbox_data_import=true >> obp-api/src/main/resources/props/test.default.props | |
| echo sandbox_data_import_secret=change_me >> obp-api/src/main/resources/props/test.default.props | |
| echo allow_account_deletion=true >> obp-api/src/main/resources/props/test.default.props | |
| echo allowed_internal_redirect_urls = /,/oauth/authorize >> obp-api/src/main/resources/props/test.default.props | |
| echo transactionRequests_enabled=true >> obp-api/src/main/resources/props/test.default.props | |
| echo transactionRequests_supported_types=SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,SIMPLE >> obp-api/src/main/resources/props/test.default.props | |
| echo SIMPLE_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props | |
| echo openredirects.hostname.whitlelist=http://127.0.0.1,http://localhost >> obp-api/src/main/resources/props/test.default.props | |
| echo remotedata.secret = foobarbaz >> obp-api/src/main/resources/props/test.default.props | |
| echo allow_public_views=true >> obp-api/src/main/resources/props/test.default.props | |
| echo SIMPLE_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props | |
| echo ACCOUNT_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props | |
| echo SEPA_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props | |
| echo FREE_FORM_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props | |
| echo COUNTERPARTY_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props | |
| echo SEPA_CREDIT_TRANSFERS_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props | |
| echo allow_oauth2_login=true >> obp-api/src/main/resources/props/test.default.props | |
| echo oauth2.jwk_set.url=https://www.googleapis.com/oauth2/v3/certs >> obp-api/src/main/resources/props/test.default.props | |
| echo ResetPasswordUrlEnabled=true >> obp-api/src/main/resources/props/test.default.props | |
| echo consents.allowed=true >> obp-api/src/main/resources/props/test.default.props | |
| echo hikari.maximumPoolSize=20 >> obp-api/src/main/resources/props/test.default.props | |
| echo write_metrics=false >> obp-api/src/main/resources/props/test.default.props | |
| # Log emails instead of opening a real SMTP socket: without this, | |
| # LocalMappedConnector.sendCustomerNotification's EMAIL branch calls | |
| # CommonsEmailWrapper.sendTextEmail which throws ConnectException because | |
| # there's no mail server in CI. That surfaces as 500 in any test that | |
| # hits an endpoint triggering the notification (v5 consent flows, etc.). | |
| echo mail.test.mode=true >> obp-api/src/main/resources/props/test.default.props | |
| # Permissions granted to runtime-compiled dynamic-endpoint code inside the security sandbox | |
| # (mirrors default.props / production.default.props). Required so dynamic resource-doc bodies | |
| # can do JSON extraction (reflection) and read OBP props (getenv); without it the sandbox | |
| # denies these and DynamicResourceDocTest's native-execution scenarios fail. | |
| echo 'dynamic_code_sandbox_permissions=[new java.net.NetPermission("specifyStreamHandler"), new java.lang.reflect.ReflectPermission("suppressAccessChecks"), new java.lang.RuntimePermission("getenv.*"), new java.util.PropertyPermission("cglib.useCache", "read"), new java.util.PropertyPermission("net.sf.cglib.test.stressHashCodes", "read"), new java.util.PropertyPermission("cglib.debugLocation", "read"), new java.lang.RuntimePermission("accessDeclaredMembers"), new java.lang.RuntimePermission("getClassLoader")]' >> obp-api/src/main/resources/props/test.default.props | |
| - name: Run tests — shard ${{ matrix.shard }} (${{ matrix.name }}) | |
| run: | | |
| # wildcardSuites requires comma-separated package prefixes (-w per entry). | |
| # The YAML >- scalar collapses newlines to spaces, so we convert here. | |
| FILTER=$(echo "${{ matrix.test_filter }}" | tr ' ' ',') | |
| # Shard 8 is the catch-all: append any test package not explicitly | |
| # assigned to shards 1–7, so new packages are never silently skipped. | |
| if [ "${{ matrix.shard }}" = "8" ]; then | |
| SHARD1="code.api.v4_0_0" | |
| SHARD2="code.api.v1_2_1" | |
| SHARD3="code.api.v6_0_0 code.api.v2_1_0 code.api.v2_2_0 code.api.v2_0_0" | |
| SHARD4="code.api.v5_1_0 code.api.v5_0_0 code.api.v3_0_0" | |
| SHARD5="code.api.ResourceDocs1_4_0 code.api.v3_1_0 code.api.v1_4_0 code.api.v1_3_0" | |
| SHARD6="code.api.v7_0_0 code.api.http4sbridge code.api.UKOpenBanking" | |
| SHARD7="code.model code.views code.customer code.usercustomerlinks \ | |
| code.api.util code.errormessages code.atms code.branches \ | |
| code.products code.crm code.accountHolder code.api.berlin" | |
| ASSIGNED="$SHARD1 $SHARD2 $SHARD3 $SHARD4 $SHARD5 $SHARD6 $SHARD7 ${{ matrix.test_filter }}" | |
| # Discover all packages that contain at least one .scala test file | |
| ALL_PKGS=$(find obp-api/src/test/scala obp-commons/src/test/scala \ | |
| -name "*.scala" 2>/dev/null \ | |
| | sed 's|.*/test/scala/||; s|/[^/]*\.scala$||; s|/|.|g' \ | |
| | sort -u) | |
| EXTRAS="" | |
| for pkg in $ALL_PKGS; do | |
| covered=false | |
| for prefix in $ASSIGNED; do | |
| if [[ "$pkg" == "$prefix" || "$pkg" == "$prefix."* || "$prefix" == "$pkg."* ]]; then | |
| covered=true; break | |
| fi | |
| done | |
| [ "$covered" = "false" ] && EXTRAS="$EXTRAS,$pkg" | |
| done | |
| [ -n "$EXTRAS" ] && echo "Catch-all extras added to shard ${{ matrix.shard }}:$EXTRAS" | |
| FILTER="${FILTER}${EXTRAS}" | |
| fi | |
| # `mvn process-resources scalatest:test` — run process-resources (copies the | |
| # dynamically-generated props from src/main/resources onto the classpath at | |
| # target/classes/props) then the scalatest:test goal. This SKIPS the | |
| # compile/testCompile lifecycle phases (~208s/shard: classes already come from | |
| # the compiled-output artifact + touch trick) while still placing | |
| # test.default.props on the classpath. (Plain goal-only `scalatest:test` skips | |
| # process-resources → Props init fails → 0 tests but BUILD SUCCESS = false green.) | |
| # -pl obp-commons,obp-api: obp-commons' own 5 util suites run on whichever | |
| # shard's filter matches com.openbankproject.* (the catch-all shard); on every | |
| # other shard the filter matches nothing in obp-commons → 0 tests there. | |
| MAVEN_OPTS="-Xmx3G -Xss2m -XX:MaxMetaspaceSize=1G" \ | |
| mvn process-resources scalatest:test -pl obp-commons,obp-api -DfailIfNoTests=false \ | |
| -DwildcardSuites="$FILTER" \ | |
| > maven-build-shard${{ matrix.shard }}.log 2>&1 | |
| - name: Report failing tests — shard ${{ matrix.shard }} | |
| if: always() | |
| run: | | |
| echo "Checking shard ${{ matrix.shard }} log for failing tests..." | |
| if [ ! -f maven-build-shard${{ matrix.shard }}.log ]; then | |
| echo "No build log found."; exit 0 | |
| fi | |
| echo "=== RECOMPILATION CHECK ===" | |
| if grep -c "Compiling " maven-build-shard${{ matrix.shard }}.log > /dev/null 2>&1; then | |
| echo "WARNING: Scala recompilation occurred on this shard:" | |
| grep "Compiling " maven-build-shard${{ matrix.shard }}.log | head -10 | |
| else | |
| echo "OK: no recompilation (Zinc used pre-compiled classes)" | |
| fi | |
| echo "" | |
| echo "=== BRIDGE / UNCAUGHT EXCEPTIONS ===" | |
| grep -n "\[BRIDGE\] Exception\|Uncaught exception in dispatch\|requestScopeProxy=" \ | |
| maven-build-shard${{ matrix.shard }}.log | head -200 || true | |
| echo "" | |
| echo "=== FAILING TEST SCENARIOS (with 30 lines context) ===" | |
| if grep -C 30 -n "\*\*\* FAILED \*\*\*" maven-build-shard${{ matrix.shard }}.log; then | |
| echo "Failing tests detected in shard ${{ matrix.shard }}." | |
| exit 1 | |
| else | |
| echo "No failing tests detected in shard ${{ matrix.shard }}." | |
| fi | |
| - name: Upload Maven build log — shard ${{ matrix.shard }} | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: maven-build-log-shard${{ matrix.shard }} | |
| if-no-files-found: ignore | |
| path: maven-build-shard${{ matrix.shard }}.log | |
| - name: Upload test reports — shard ${{ matrix.shard }} | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-reports-shard${{ matrix.shard }} | |
| if-no-files-found: ignore | |
| path: | | |
| obp-api/target/surefire-reports/** | |
| obp-commons/target/surefire-reports/** | |
| **/target/scalatest-reports/** | |
| **/target/site/surefire-report.html | |
| **/target/site/surefire-report/* | |
| # -------------------------------------------------------------------------- | |
| # Job 3: report — per-test speed (unit/pure vs integration, all http4s) | |
| # -------------------------------------------------------------------------- | |
| report: | |
| needs: test | |
| runs-on: ubuntu-latest | |
| if: always() | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download test reports — all shards | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: test-reports-shard* | |
| path: all-reports | |
| merge-multiple: true | |
| - name: Per-test speed — unit/pure vs integration (all http4s) | |
| run: python3 .github/scripts/test_speed_report.py all-reports |