diff --git a/.github/workflows/integration-test-aura.yml b/.github/workflows/integration-test-aura.yml index b7ee1a40..fd1beaa3 100644 --- a/.github/workflows/integration-test-aura.yml +++ b/.github/workflows/integration-test-aura.yml @@ -1,39 +1,54 @@ -#name: Integration Tests -# -#on: -# push: -# branches: -# - main -# pull_request: -# branches: -# - main -# -#jobs: -# tests: -# runs-on: ubuntu-latest -# env: -# CONNECTION: ${{ secrets.AURA_PRO }} -# name: "Running on all provided Aura instances" -# -# steps: -# - uses: actions/checkout@v2 -# - name: Cache Composer dependencies -# uses: actions/cache@v2 -# with: -# path: /tmp/composer-cache -# key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} -# - uses: php-actions/composer@v6 -# with: -# progress: yes -# php_version: 8.1 -# version: 2 -# - name: clean database -# run: CONNECTION=$CONNECTION php tests/clean-database.php -# - uses: php-actions/phpunit@v3 -# with: -# configuration: phpunit.xml.dist -# php_version: 8.1 -# memory_limit: 1024M -# version: 10 -# testsuite: Integration -# bootstrap: vendor/autoload.php +name: Integration Tests + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: integration-tests-aura + cancel-in-progress: true + +jobs: + tests: + runs-on: ubuntu-latest + name: "Running on all provided Aura instances" + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build & cache client image + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile + load: true + push: false + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: PHP_VERSION=8.1.31 + tags: integration-client:8.1.31 + + - name: Populate .env + run: | + echo "PHP_VERSION=8.1.31" > .env + echo "CONNECTION=\"${{ secrets.AURA_PRO }}\"" >> .env + + - name: Cache PHP deps + id: cache-php-deps + uses: actions/cache@v4 + with: + path: vendor + key: ${{ runner.os }}-php-8.1.31-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-8.1.31- + + - name: Install PHP deps + if: steps.cache-php-deps.outputs.cache-hit != 'true' + run: docker compose run --rm client composer install + + - name: Run tests + run: docker compose run --rm client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration diff --git a/.github/workflows/integration-test-cluster-neo4j-4.yml b/.github/workflows/integration-test-cluster-neo4j-4.yml index e2a164f8..dee4ad29 100644 --- a/.github/workflows/integration-test-cluster-neo4j-4.yml +++ b/.github/workflows/integration-test-cluster-neo4j-4.yml @@ -5,29 +5,77 @@ on: branches: - main pull_request: - branches: - - main jobs: tests: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1.31', '8.3.17'] + php: ["8.1.31", "8.3.17"] name: "Running on PHP ${{ matrix.php }} in a Neo4j 4.4 cluster" steps: - uses: actions/checkout@v4 + + - name: Restore Neo4j Image Cache if it exists + id: cache-docker-neo4j + uses: actions/cache@v4 + with: + path: ci/cache/docker/neo4j + key: cache-docker-neo4j-4-enterprise + + - name: Update Neo4j Image Cache if cache miss + if: steps.cache-docker-neo4j.outputs.cache-hit != 'true' + run: | + docker pull neo4j:4.4-enterprise + mkdir -p ci/cache/docker/neo4j + docker image save neo4j:4.4-enterprise --output ./ci/cache/docker/neo4j/neo4j-4-enterprise.tar + + - name: Use Neo4j Image Cache if cache hit + if: steps.cache-docker-neo4j.outputs.cache-hit == 'true' + run: docker image load --input ./ci/cache/docker/neo4j/neo4j-4-enterprise.tar + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build & cache client image + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile + load: true + push: false + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: PHP_VERSION=${{ matrix.php }} + tags: integration-client:${{ matrix.php }} + - name: Populate .env run: | echo "PHP_VERSION=${{ matrix.php }}" > .env - echo "CONNECTION=neo4j://neo4j:testtest@core1" >> .env - - uses: hoverkraft-tech/compose-action@v2.0.2 - name: Start services + echo "CONNECTION=neo4j://neo4j:testtest@server1" >> .env + + - name: Cache PHP deps + id: cache-php-deps + uses: actions/cache@v4 with: - compose-file: './docker-compose-neo4j-4.yml' - up-flags: '--build --remove-orphans' - - name: Test + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}- + + - name: Install PHP deps + if: steps.cache-php-deps.outputs.cache-hit != 'true' + run: | + docker compose -f docker-compose-neo4j-4.yml run --rm client composer install + + - name: Run integration tests run: | - docker compose run client composer install - docker compose run client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration + docker compose -f docker-compose-neo4j-4.yml up -d --no-build --remove-orphans --wait \ + server1 \ + server2 \ + server3 \ + server4 + + docker compose -f docker-compose-neo4j-4.yml run --rm client \ + ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration diff --git a/.github/workflows/integration-test-cluster-neo4j-5.yml b/.github/workflows/integration-test-cluster-neo4j-5.yml index 8fe9402c..ec78d1a0 100644 --- a/.github/workflows/integration-test-cluster-neo4j-5.yml +++ b/.github/workflows/integration-test-cluster-neo4j-5.yml @@ -5,29 +5,78 @@ on: branches: - main pull_request: - branches: - - main jobs: tests: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1.31', '8.3.17'] + php: ["8.1.31", "8.3.17"] name: "Running on PHP ${{ matrix.php }} with a Neo4j 5.20-enterprise cluster" steps: - uses: actions/checkout@v4 + + - name: Restore Neo4j Image Cache if it exists + id: cache-docker-neo4j + uses: actions/cache@v4 + with: + path: ci/cache/docker/neo4j + key: cache-docker-neo4j-5-enterprise + + - name: Update Neo4j Image Cache if cache miss + if: steps.cache-docker-neo4j.outputs.cache-hit != 'true' + run: | + docker pull neo4j:5-enterprise + mkdir -p ci/cache/docker/neo4j + docker image save neo4j:5-enterprise --output ./ci/cache/docker/neo4j/neo4j-5-enterprise.tar + + - name: Use Neo4j Image Cache if cache hit + if: steps.cache-docker-neo4j.outputs.cache-hit == 'true' + run: docker image load --input ./ci/cache/docker/neo4j/neo4j-5-enterprise.tar + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build & cache client image + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile + load: true + push: false + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: PHP_VERSION=${{ matrix.php }} + tags: integration-client:${{ matrix.php }} + - name: Populate .env run: | echo "PHP_VERSION=${{ matrix.php }}" > .env echo "CONNECTION=neo4j://neo4j:testtest@server1" >> .env - - uses: hoverkraft-tech/compose-action@v2.0.2 - name: Start services + + - name: Cache PHP deps + id: cache-php-deps + uses: actions/cache@v4 with: - compose-file: './docker-compose.yml' - up-flags: '--build --remove-orphans' - - name: Test + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}- + + - name: Install PHP deps + if: steps.cache-php-deps.outputs.cache-hit != 'true' + run: | + docker compose run --rm client composer install + + - name: Run integration tests run: | - docker compose run client composer install - docker compose run client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration + docker compose up -d --remove-orphans --wait --no-build \ + server1 \ + server2 \ + server3 \ + server4 + + # install PHP deps and run PHPUnit inside the client container + docker compose run --rm client \ + ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration diff --git a/.github/workflows/integration-test-single-server.yml b/.github/workflows/integration-test-single-server.yml index 81b00d29..a968f355 100644 --- a/.github/workflows/integration-test-single-server.yml +++ b/.github/workflows/integration-test-single-server.yml @@ -5,48 +5,90 @@ on: branches: - main pull_request: - branches: - - main jobs: tests-v4: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1.31', '8.3.17'] + php: ["8.1.31", "8.3.17"] name: "Running on PHP ${{ matrix.php }} with a Neo4j 4 instance connecting over all available protocols" steps: - uses: actions/checkout@v4 + + - name: Restore Neo4j Image Cache if it exists + id: cache-docker-neo4j + uses: actions/cache@v4 + with: + path: ci/cache/docker/neo4j + key: cache-docker-neo4j-4-enterprise + + - name: Update Neo4j Image Cache if cache miss + if: steps.cache-docker-neo4j.outputs.cache-hit != 'true' + run: | + docker pull neo4j:4.4-enterprise + mkdir -p ci/cache/docker/neo4j + docker image save neo4j:4.4-enterprise --output ./ci/cache/docker/neo4j/neo4j-4-enterprise.tar + + - name: Use Neo4j Image Cache if cache hit + if: steps.cache-docker-neo4j.outputs.cache-hit == 'true' + run: docker image load --input ./ci/cache/docker/neo4j/neo4j-4-enterprise.tar + - name: Populate .env run: | echo "PHP_VERSION=${{ matrix.php }}" > .env - echo "CONNECTION=neo4j://neo4j:testtest@neo4j" >> .env - - uses: hoverkraft-tech/compose-action@v2.0.2 - name: Start services + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build & cache client image + uses: docker/build-push-action@v3 with: - compose-file: './docker-compose-neo4j-4.yml' - up-flags: '--build --remove-orphans' - - name: Composer install - run: | - docker compose run client composer install - - name: Test neo4j:// + context: . + file: Dockerfile + load: true + push: false + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: PHP_VERSION=${{ matrix.php }} + tags: integration-client:${{ matrix.php }} + + - name: Cache PHP deps + id: cache-php-deps + uses: actions/cache@v4 + with: + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}- + + - name: Install PHP deps + if: steps.cache-php-deps.outputs.cache-hit != 'true' + run: docker compose run --rm client composer install + + - name: Run tests Neo4j 4 run: | - docker compose run \ - -e PHP_VERSION=${{ matrix.php }} \ - -e CONNECTION=neo4j://neo4j:testtest@neo4j \ + + echo "PHP_VERSION=${{ matrix.php }}" > .env + echo "CONNECTION=bolt://neo4j:testtest@neo4j" >> .env + + docker compose -f docker-compose-neo4j-4.yml up -d --build --remove-orphans --wait neo4j + + docker compose -f docker-compose-neo4j-4.yml run --rm \ client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration - - name: Test bolt:// - run: | - docker compose run \ - -e PHP_VERSION=${{ matrix.php }} \ - -e CONNECTION=bolt://neo4j:testtest@neo4j \ + + echo "PHP_VERSION=${{ matrix.php }}" > .env + echo "CONNECTION=neo4j://neo4j:testtest@neo4j" >> .env + + docker compose -f docker-compose-neo4j-4.yml run --rm \ client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration + tests-v5: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1.31', '8.3.17'] + php: ["8.1.31", "8.3.17"] name: "Running on PHP ${{ matrix.php }} with a Neo4j 5 instance connecting over all available protocols" steps: @@ -54,24 +96,52 @@ jobs: - name: Populate .env run: | echo "PHP_VERSION=${{ matrix.php }}" > .env - echo "CONNECTION=neo4j://neo4j:testtest@neo4j" >> .env - - uses: hoverkraft-tech/compose-action@v2.0.2 - name: Start services + + - name: Restore Neo4j Image Cache if it exists + id: cache-docker-neo4j + uses: actions/cache@v4 with: - compose-file: './docker-compose.yml' - up-flags: '--build' - - name: Composer install + path: ci/cache/docker/neo4j + key: cache-docker-neo4j-5-community + + - name: Update Neo4j Image Cache if cache miss + if: steps.cache-docker-neo4j.outputs.cache-hit != 'true' run: | - docker compose run client composer install - - name: Test neo4j:// + docker pull neo4j:5.23-community + mkdir -p ci/cache/docker/neo4j + docker image save neo4j:5.23-community --output ./ci/cache/docker/neo4j/neo4j-5-community.tar + + - name: Use Neo4j Image Cache if cache hit + if: steps.cache-docker-neo4j.outputs.cache-hit == 'true' + run: docker image load --input ./ci/cache/docker/neo4j/neo4j-5-community.tar + + - name: Cache PHP deps + id: cache-php-deps + uses: actions/cache@v4 + with: + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}- + + - name: Install PHP deps + if: steps.cache-php-deps.outputs.cache-hit != 'true' + run: docker compose run --rm client composer install + + - name: Run tests Neo4j 5 run: | - docker compose run \ - -e PHP_VERSION=${{ matrix.php }} \ - -e CONNECTION=neo4j://neo4j:testtest@neo4j \ + echo "PHP_VERSION=${{ matrix.php }}" > .env + echo "CONNECTION=bolt://neo4j:testtest@neo4j" >> .env + + docker compose up -d --build --remove-orphans --wait neo4j + + docker compose run --rm \ client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration - - name: Test bolt:// - run: | - docker compose run \ - -e PHP_VERSION=${{ matrix.php }} \ - -e CONNECTION=bolt://neo4j:testtest@neo4j \ + + echo "PHP_VERSION=${{ matrix.php }}" > .env + echo "CONNECTION=neo4j://neo4j:testtest@neo4j" >> .env + + docker compose run --rm \ client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration + + docker compose down --remove-orphans --volumes diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 18098c35..70d0dd29 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -5,8 +5,7 @@ on: branches: - main pull_request: - branches: - - main + jobs: php-cs-fixer: name: "Lint & Analyse" @@ -18,15 +17,19 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3.17' + php-version: "8.3.17" - - name: Cache Composer dependencies + - name: Cache PHP deps + id: cache-php-deps uses: actions/cache@v4 with: - path: /tmp/composer-cache - key: ${{ runner.os }}-${{ hashFiles('**/composer.json') }} + path: vendor + key: ${{ runner.os }}-php-8.3.17-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-8.3.17- - - name: Install dependencies + - name: Install PHP deps + if: steps.cache-php-deps.outputs.cache-hit != 'true' run: composer install - name: "PHP-CS-Fixer" diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 569ad585..a4c016f7 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -5,16 +5,14 @@ on: branches: - main pull_request: - branches: - - main jobs: tests: runs-on: ubuntu-latest name: "Running Unit Tests" strategy: - matrix: - php: ['8.1.31', '8.2.27', '8.3.17'] + matrix: + php: ["8.1.31", "8.2.27", "8.3.17"] steps: - uses: actions/checkout@v4 @@ -25,15 +23,19 @@ jobs: with: php-version: ${{ matrix.php }} - - name: Cache Composer dependencies + - name: Cache PHP deps + id: cache-php-deps uses: actions/cache@v4 with: - path: /tmp/composer-cache - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}- - - name: Install dependencies + - name: Install PHP deps + if: steps.cache-php-deps.outputs.cache-hit != 'true' run: composer install - + - uses: php-actions/phpunit@v4 with: configuration: phpunit.xml.dist diff --git a/Dockerfile b/Dockerfile index bfc3ad73..7b34029f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,14 +17,5 @@ RUN apt-get update \ WORKDIR /opt/project -COPY composer.json ./ - -RUN composer install - -COPY phpunit.xml.dist phpunit.coverage.xml.dist psalm.xml .php-cs-fixer.dist.php LICENSE README.md ./ -COPY src/ src/ -COPY tests/ tests/ -COPY .git/ .git/ - diff --git a/composer.json b/composer.json index 28ace7bc..cabb18c3 100644 --- a/composer.json +++ b/composer.json @@ -78,5 +78,9 @@ "platform": { "php": "8.1.17" } + }, + "scripts": { + "fix-cs": "./vendor/bin/php-cs-fixer fix", + "psalm": "./vendor/bin/psalm" } } diff --git a/docker-compose-neo4j-4.yml b/docker-compose-neo4j-4.yml index 95dee8cd..545b19ff 100644 --- a/docker-compose-neo4j-4.yml +++ b/docker-compose-neo4j-4.yml @@ -9,7 +9,7 @@ x-shared: x-shared-cluster: &common-cluster <<: *common - NEO4J_causal__clustering_initial__discovery__members: core1:5000,core2:5000,core3:5000 + NEO4J_causal__clustering_initial__discovery__members: server1:5000,server2:5000,server3:5000 NEO4J_dbms_memory_pagecache_size: 100M NEO4J_dbms_memory_heap_initial__size: 100M NEO4J_causal__clustering_discovery__listen__address: 0.0.0.0:5000 @@ -28,6 +28,7 @@ networks: services: client: + image: "integration-client:${PHP_VERSION-8.1}" build: context: . dockerfile: Dockerfile @@ -39,29 +40,7 @@ services: - .:/opt/project env_file: - .env - depends_on: - neo4j: - condition: service_healthy - testkit-backend: - build: - context: . - dockerfile: Dockerfile - args: - PHP_VERSION: "${PHP_VERSION-8.1}" - WITH_XDEBUG: true - working_dir: /opt/project - volumes: - - .:/opt/project - command: php /opt/project/testkit-backend/index.php - networks: - - neo4j - depends_on: - - neo4j - ports: - - "9876:9876" neo4j: - networks: - - neo4j image: neo4j:4.4-enterprise healthcheck: test: "wget -q --method=HEAD http://localhost:7474 || exit 1" @@ -73,11 +52,13 @@ services: - "7474:7474" environment: <<: *common - volumes: - - ./tests/resources:/import + NEO4J_dbms_connector_http_advertised__address: neo4j:7474 + NEO4J_dbms_connector_bolt_advertised__address: neo4j:7687 env_file: - .env - core1: + networks: + - neo4j + server1: image: neo4j:4.4-enterprise healthcheck: test: "wget -q --method=HEAD http://localhost:7474 || exit 1" @@ -86,19 +67,17 @@ services: retries: 5 networks: - neo4j - volumes: - - ./tests/resources:/import environment: <<: *common-core - NEO4J_causal__clustering_discovery__advertised__address: core1:5000 - NEO4J_causal__clustering_transaction__advertised__address: core1:6000 - NEO4J_causal__clustering_raft__advertised__address: core1:7000 - NEO4J_dbms_connector_http_advertised__address: core1:7474 - NEO4J_dbms_connector_bolt_advertised__address: core1:7687 + NEO4J_causal__clustering_discovery__advertised__address: server1:5000 + NEO4J_causal__clustering_transaction__advertised__address: server1:6000 + NEO4J_causal__clustering_raft__advertised__address: server1:7000 + NEO4J_dbms_connector_http_advertised__address: server1:7474 + NEO4J_dbms_connector_bolt_advertised__address: server1:7687 env_file: - .env - core2: + server2: image: neo4j:4.4-enterprise healthcheck: test: "wget -q --method=HEAD http://localhost:7474 || exit 1" @@ -109,17 +88,15 @@ services: - neo4j environment: <<: *common-core - NEO4J_causal__clustering_discovery__advertised__address: core2:5000 - NEO4J_causal__clustering_transaction__advertised__address: core2:6000 - NEO4J_causal__clustering_raft__advertised__address: core2:7000 - NEO4J_dbms_connector_http_advertised__address: core2:7474 - NEO4J_dbms_connector_bolt_advertised__address: core2:7687 - volumes: - - ./tests/resources:/import + NEO4J_causal__clustering_discovery__advertised__address: server2:5000 + NEO4J_causal__clustering_transaction__advertised__address: server2:6000 + NEO4J_causal__clustering_raft__advertised__address: server2:7000 + NEO4J_dbms_connector_http_advertised__address: server2:7474 + NEO4J_dbms_connector_bolt_advertised__address: server2:7687 env_file: - .env - core3: + server3: image: neo4j:4.4-enterprise healthcheck: test: "wget -q --method=HEAD http://localhost:7474 || exit 1" @@ -130,17 +107,15 @@ services: - neo4j environment: <<: *common-core - NEO4J_causal__clustering_discovery__advertised__address: core3:5000 - NEO4J_causal__clustering_transaction__advertised__address: core3:6000 - NEO4J_causal__clustering_raft__advertised__address: core3:7000 - NEO4J_dbms_connector_http_advertised__address: core3:7474 - NEO4J_dbms_connector_bolt_advertised__address: core3:7687 - volumes: - - ./tests/resources:/import + NEO4J_causal__clustering_discovery__advertised__address: server3:5000 + NEO4J_causal__clustering_transaction__advertised__address: server3:6000 + NEO4J_causal__clustering_raft__advertised__address: server3:7000 + NEO4J_dbms_connector_http_advertised__address: server3:7474 + NEO4J_dbms_connector_bolt_advertised__address: server3:7687 env_file: - .env - readreplica1: + server4: image: neo4j:4.4-enterprise healthcheck: test: "wget -q --method=HEAD http://localhost:7474 || exit 1" @@ -152,12 +127,10 @@ services: environment: <<: *common-cluster NEO4J_dbms_mode: READ_REPLICA - NEO4J_causal__clustering_discovery__advertised__address: readreplica1:5000 - NEO4J_causal__clustering_transaction__advertised__address: readreplica1:6000 - NEO4J_causal__clustering_raft__advertised__address: readreplica1:7000 - NEO4J_dbms_connector_http_advertised__address: readreplica1:7474 - NEO4J_dbms_connector_bolt_advertised__address: readreplica1:7687 + NEO4J_causal__clustering_discovery__advertised__address: server4:5000 + NEO4J_causal__clustering_transaction__advertised__address: server4:6000 + NEO4J_causal__clustering_raft__advertised__address: server4:7000 + NEO4J_dbms_connector_http_advertised__address: server4:7474 + NEO4J_dbms_connector_bolt_advertised__address: server4:7687 env_file: - .env - volumes: - - ./tests/resources:/import diff --git a/docker-compose.yml b/docker-compose.yml index 928d1f92..4a7016a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,7 @@ x-definitions: - .env x-common-php: &common-php + image: "integration-client:${PHP_VERSION-8.1}" build: context: . dockerfile: Dockerfile @@ -50,9 +51,6 @@ services: - .:/opt/project env_file: - .env - depends_on: - neo4j: - condition: service_healthy neo4j: <<: *common image: neo4j:5.23-community diff --git a/src/Bolt/ConnectionPool.php b/src/Bolt/ConnectionPool.php index 33bf5e97..565d9b31 100644 --- a/src/Bolt/ConnectionPool.php +++ b/src/Bolt/ConnectionPool.php @@ -23,10 +23,7 @@ use Laudis\Neo4j\Databags\ConnectionRequestData; use Laudis\Neo4j\Databags\DriverConfiguration; use Laudis\Neo4j\Databags\SessionConfiguration; - -use function method_exists; -use function microtime; - +use Laudis\Neo4j\Exception\ConnectionPoolException; use Psr\Http\Message\UriInterface; use function shuffle; @@ -44,6 +41,7 @@ public function __construct( private readonly BoltFactory $factory, private readonly ConnectionRequestData $data, private readonly ?Neo4jLogger $logger, + private readonly float $acquireConnectionTimeout, ) { } @@ -63,33 +61,42 @@ public static function create( $conf->getUserAgent(), $conf->getSslConfiguration() ), - $conf->getLogger() + $conf->getLogger(), + $conf->getAcquireConnectionTimeout() ); } public function acquire(SessionConfiguration $config): Generator { - $generator = $this->semaphore->wait(); - $start = microtime(true); + /** + * @var Generator + */ + return (function () use ($config) { + $connection = $this->reuseConnectionIfPossible($config); + if ($connection !== null) { + return $connection; + } - return (function () use ($generator, $start, $config) { + $generator = $this->semaphore->wait(); // If the generator is valid, it means we are waiting to acquire a new connection. // This means we can use this time to check if we can reuse a connection or should throw a timeout exception. while ($generator->valid()) { - /** @var bool $continue */ - $continue = yield microtime(true) - $start; - $generator->send($continue); - if ($continue === false) { - return null; - } + $waitTime = $generator->current(); + if ($waitTime <= $this->acquireConnectionTimeout) { + yield $waitTime; - $connection = $this->returnAnyAvailableConnection($config); - if ($connection !== null) { - return $connection; + $connection = $this->reuseConnectionIfPossible($config); + if ($connection !== null) { + return $connection; + } + + $generator->next(); + } else { + throw new ConnectionPoolException('Connection acquire timeout reached: '.($waitTime ?? 0.0)); } } - $connection = $this->returnAnyAvailableConnection($config); + $connection = $this->reuseConnectionIfPossible($config); if ($connection !== null) { return $connection; } @@ -119,55 +126,17 @@ public function getLogger(): ?Neo4jLogger return $this->logger; } - /** - * @return BoltConnection|null - */ - private function returnAnyAvailableConnection(SessionConfiguration $config): ?ConnectionInterface + private function reuseConnectionIfPossible(SessionConfiguration $config): ?BoltConnection { - $streamingConnection = null; - $requiresReconnectConnection = null; // Ensure random connection reuse before picking one. shuffle($this->activeConnections); - foreach ($this->activeConnections as $activeConnection) { // We prefer a connection that is just ready - if ($activeConnection->getServerState() === 'READY') { - if ($this->factory->canReuseConnection($activeConnection, $this->data, $config)) { - return $this->factory->reuseConnection($activeConnection, $config); - } else { - $requiresReconnectConnection = $activeConnection; - } - } - - // We will store any streaming connections, so we can use that one - // as we can force the subscribed result sets to consume the results - // and become ready again. - // This code will make sure we never get stuck if the user has many - // results open that aren't consumed yet. - // https://github.com/neo4j-php/neo4j-php-client/issues/146 - // NOTE: we cannot work with TX_STREAMING as we cannot force the transaction to implicitly close. - if ($streamingConnection === null && $activeConnection->getServerState() === 'STREAMING') { - if ($this->factory->canReuseConnection($activeConnection, $this->data, $config)) { - $streamingConnection = $activeConnection; - if (method_exists($streamingConnection, 'consumeResults')) { - $streamingConnection->consumeResults(); // State should now be ready - } - } else { - $requiresReconnectConnection = $activeConnection; - } + if ($activeConnection->getServerState() === 'READY' && $this->factory->canReuseConnection($activeConnection, $config)) { + return $this->factory->reuseConnection($activeConnection, $config); } } - if ($streamingConnection) { - return $this->factory->reuseConnection($streamingConnection, $config); - } - - if ($requiresReconnectConnection) { - $this->release($requiresReconnectConnection); - - return $this->factory->createConnection($this->data, $config); - } - return null; } diff --git a/src/Bolt/Messages/BoltCommitMessage.php b/src/Bolt/Messages/BoltCommitMessage.php index 267b3227..4514f388 100644 --- a/src/Bolt/Messages/BoltCommitMessage.php +++ b/src/Bolt/Messages/BoltCommitMessage.php @@ -40,6 +40,12 @@ public function send(): BoltCommitMessage { $this->logger?->log(LogLevel::DEBUG, 'COMMIT'); $response = $this->protocol->commit()->getResponse(); + // TODO: This is an issue with the underlying bolt library. + // The serverState should be READY after a successful commit but + // it's still in TX_STREAMING if the results were not consumed + // + // This should be removed once it's fixed + $this->protocol->serverState = ServerState::READY; /** @var array{bookmark?: string} $content */ $content = $response->content; diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index 83bddd50..3e051cc3 100644 --- a/src/Bolt/Session.php +++ b/src/Bolt/Session.php @@ -163,9 +163,13 @@ private function beginInstantTransaction( private function acquireConnection(TransactionConfiguration $config, SessionConfiguration $sessionConfig): BoltConnection { $this->getLogger()?->log(LogLevel::INFO, 'Acquiring connection', ['config' => $config, 'sessionConfig' => $sessionConfig]); - $connection = $this->pool->acquire($sessionConfig); - /** @var BoltConnection $connection */ - $connection = GeneratorHelper::getReturnFromGenerator($connection); + $connectionGenerator = $this->pool->acquire($sessionConfig); + /** + * @var BoltConnection $connection + * + * @psalm-suppress UnnecessaryVarAnnotation + */ + $connection = GeneratorHelper::getReturnFromGenerator($connectionGenerator); // We try and let the server do the timeout management. // Since the client should not run indefinitely, we just add the client side by two, just in case diff --git a/src/BoltFactory.php b/src/BoltFactory.php index 1d50d3c2..def0a8ff 100644 --- a/src/BoltFactory.php +++ b/src/BoltFactory.php @@ -79,7 +79,7 @@ public function createConnection(ConnectionRequestData $data, SessionConfigurati return new BoltConnection($protocol, $connection, $data->getAuth(), $data->getUserAgent(), $config, $this->logger); } - public function canReuseConnection(ConnectionInterface $connection, ConnectionRequestData $data, SessionConfiguration $config): bool + public function canReuseConnection(ConnectionInterface $connection, SessionConfiguration $config): bool { if (!$connection->isOpen()) { return false; @@ -88,12 +88,7 @@ public function canReuseConnection(ConnectionInterface $connection, ConnectionRe $databaseInfo = $connection->getDatabaseInfo(); $database = $databaseInfo?->getName(); - return $connection->getServerAddress()->getHost() === $data->getUri()->getHost() - && $connection->getServerAddress()->getPort() === $data->getUri()->getPort() - && $connection->getAuthentication()->toString($data->getUri()) === $data->getAuth()->toString($data->getUri()) - && $connection->getEncryptionLevel() === $this->sslConfigurationFactory->create($data->getUri(), $data->getSslConfig())[0] - && $connection->getUserAgent() === $data->getUserAgent() - && $connection->getAccessMode() === $config->getAccessMode() + return $connection->getAccessMode() === $config->getAccessMode() && $database === $config->getDatabase(); } diff --git a/src/Contracts/ConnectionPoolInterface.php b/src/Contracts/ConnectionPoolInterface.php index 98dd372c..0b16c981 100644 --- a/src/Contracts/ConnectionPoolInterface.php +++ b/src/Contracts/ConnectionPoolInterface.php @@ -35,7 +35,7 @@ interface ConnectionPoolInterface * int, * float, * bool, - * Connection|null + * Connection * > */ public function acquire(SessionConfiguration $config): Generator; diff --git a/src/Exception/ConnectionPoolException.php b/src/Exception/ConnectionPoolException.php new file mode 100644 index 00000000..807debb7 --- /dev/null +++ b/src/Exception/ConnectionPoolException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Exception; + +use RuntimeException; + +final class ConnectionPoolException extends RuntimeException +{ +} diff --git a/src/Neo4j/Neo4jConnectionPool.php b/src/Neo4j/Neo4jConnectionPool.php index e4ad758d..4074d03a 100644 --- a/src/Neo4j/Neo4jConnectionPool.php +++ b/src/Neo4j/Neo4jConnectionPool.php @@ -74,6 +74,7 @@ public function __construct( private readonly CacheInterface $cache, private readonly AddressResolverInterface $resolver, private readonly ?Neo4jLogger $logger, + private readonly float $acquireConnectionTimeout, ) { } @@ -96,7 +97,8 @@ public static function create( ), Cache::getInstance(), $resolver, - $conf->getLogger() + $conf->getLogger(), + $conf->getAcquireConnectionTimeout() ); } @@ -112,7 +114,7 @@ public function createOrGetPool(string $hostname, UriInterface $uri): Connection $key = $this->createKey($data); if (!array_key_exists($key, self::$pools)) { - self::$pools[$key] = new ConnectionPool($this->semaphore, $this->factory, $data, $this->logger); + self::$pools[$key] = new ConnectionPool($this->semaphore, $this->factory, $data, $this->logger, $this->acquireConnectionTimeout); } return self::$pools[$key]; @@ -141,7 +143,11 @@ public function acquire(SessionConfiguration $config): Generator $this->data->getUri()->withHost($address) ); try { - /** @var BoltConnection $connection */ + /** + * @var BoltConnection $connection + * + * @psalm-suppress UnnecessaryVarAnnotation + */ $connection = GeneratorHelper::getReturnFromGenerator($pool->acquire($config)); $table = $this->routingTable($connection, $config); } catch (ConnectException $e) { diff --git a/tests/Integration/BoltDriverIntegrationTest.php b/tests/Integration/BoltDriverIntegrationTest.php index 1912bb54..c1dc817b 100644 --- a/tests/Integration/BoltDriverIntegrationTest.php +++ b/tests/Integration/BoltDriverIntegrationTest.php @@ -15,8 +15,8 @@ use Bolt\error\ConnectException; use Exception; +use Laudis\Neo4j\Basic\Driver; use Laudis\Neo4j\Bolt\BoltDriver; -use Laudis\Neo4j\Neo4j\Neo4jDriver; use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest; use Throwable; @@ -85,7 +85,7 @@ public function testInvalidSocket(): void public function testBookmarkUpdates(): void { - $session = Neo4jDriver::create($this->getUri(['bolt', 'neo4j'])->__toString())->createSession(); + $session = Driver::create($this->getUri(['bolt', 'neo4j'])->__toString())->createSession(); $bookmark = $session->getLastBookmark(); $this->assertEquals([], $bookmark->values()); $this->assertTrue($bookmark->isEmpty()); diff --git a/tests/Integration/EdgeCasesTest.php b/tests/Integration/EdgeCasesTest.php index 9157683e..545dceda 100644 --- a/tests/Integration/EdgeCasesTest.php +++ b/tests/Integration/EdgeCasesTest.php @@ -81,6 +81,10 @@ public function testRunALotOfStatements(): void $this->markTestSkipped('HTTP mass queries overload tiny neo4j instances'); } + if (str_contains($_ENV['CONNECTION'] ?? '', 'databases.neo4j.io')) { + $this->markTestSkipped('We assume neo4j aura shuts down connections that are too demanding'); + } + $persons = $this->getSession()->run('MATCH (p:Person) RETURN p'); $movies = $this->getSession()->run('MATCH (m:Movie) RETURN m'); @@ -102,18 +106,17 @@ public function testRunALotOfStatements(): void } } - $statements = []; + $count = 0; foreach ($personIds as $personId) { foreach ($movieIds as $movieId) { - $statements[] = Statement::create( + ++$count; + $this->getSession()->runStatement(Statement::create( 'MATCH (a), (b) WHERE id(a) = $ida AND id(b) = $idb MERGE (a) <-[r:ACTED_IN]- (b) RETURN id(r)', ['ida' => $personId, 'idb' => $movieId] - ); + )); } } - - $this->getSession()->runStatements($statements); - self::assertCount(4978, $statements); + self::assertEquals(4978, $count); } public function testGettingKeysFromArraylist(): void diff --git a/tests/Integration/Neo4jLoggerTest.php b/tests/Integration/Neo4jLoggerTest.php index aa8f75c5..45411b0c 100644 --- a/tests/Integration/Neo4jLoggerTest.php +++ b/tests/Integration/Neo4jLoggerTest.php @@ -22,6 +22,10 @@ class Neo4jLoggerTest extends EnvironmentAwareIntegrationTest { + /** + * @psalm-suppress PossiblyUndefinedIntArrayOffset + * @psalm-suppress PossiblyUndefinedStringArrayOffset + */ public function testLogger(): void { if (str_contains($this->getUri()->getScheme(), 'http')) { @@ -32,8 +36,6 @@ public function testLogger(): void self::markTestSkipped('This test is not applicable clusters'); } - // Close connections so that we can test the logger logging - // during authentication while acquiring a new connection $this->driver->closeConnections(); /** @var MockObject $logger */ @@ -41,118 +43,62 @@ public function testLogger(): void /** @var Session $session */ $session = $this->getSession(); - /** @var array $infoLogs */ + // –– INFO logs (unchanged) –– $infoLogs = []; - $expectedInfoLogs = [ - [ - 'Running statements', - [ - 'statements' => [new Statement('RETURN 1 as test', [])], - ], - ], - [ - 'Starting instant transaction', - [ - 'config' => new TransactionConfiguration(null, null), - ], - ], - [ - 'Acquiring connection', - [ - 'config' => new TransactionConfiguration(null, null), - ], - ], + $expectedInfo = [ + ['Running statements', ['statements' => [new Statement('RETURN 1 as test', [])]]], + ['Starting instant transaction', ['config' => new TransactionConfiguration(null, null)]], + ['Acquiring connection', ['config' => new TransactionConfiguration(null, null)]], ]; - $logger->expects(self::exactly(count($expectedInfoLogs)))->method('info')->willReturnCallback( - static function (string $message, array $context) use (&$infoLogs) { - $infoLogs[] = [$message, $context]; - } - ); + $logger + ->expects(self::exactly(count($expectedInfo))) + ->method('info') + ->willReturnCallback(static function (string $msg, array $ctx) use (&$infoLogs) { + $infoLogs[] = [$msg, $ctx]; + }); + // –– DEBUG logs –– capture _all_ calls, but we won't enforce count $debugLogs = []; - $expectedDebugLogs = [ - [ - 'HELLO', - [ - 'user_agent' => 'neo4j-php-client/2', - ], - ], - [ - 'LOGON', - [ - 'scheme' => 'basic', - 'principal' => 'neo4j', - ], - ], - [ - 'RUN', - [ - 'text' => 'RETURN 1 as test', - 'parameters' => [], - 'extra' => [ - 'mode' => 'w', - ], - ], - ], - [ - 'DISCARD', - [], - ], + $expectedDebug = [ + ['HELLO', ['user_agent' => 'neo4j-php-client/2']], + ['LOGON', ['scheme' => 'basic', 'principal' => 'neo4j']], + ['RUN', ['text' => 'RETURN 1 as test', 'parameters' => [], 'extra' => ['mode' => 'w']]], + ['DISCARD', []], ]; - if ($this->getUri()->getScheme() === 'neo4j') { - array_splice( - $expectedDebugLogs, - 0, - 0, - [ - [ - 'HELLO', - [ - 'user_agent' => 'neo4j-php-client/2', - ], - ], - [ - 'LOGON', - [ - 'scheme' => 'basic', - 'principal' => 'neo4j', - ], - ], - [ - 'ROUTE', - [ - 'db' => null, - ], - ], - [ - 'GOODBYE', - [], - ], - ], - ); + array_splice($expectedDebug, 0, 0, [ + ['HELLO', ['user_agent' => 'neo4j-php-client/2']], + ['LOGON', ['scheme' => 'basic', 'principal' => 'neo4j']], + ['ROUTE', ['db' => null]], + ['GOODBYE', []], + ]); } - $logger->expects(self::exactly(count($expectedDebugLogs)))->method('debug')->willReturnCallback( - static function (string $message, array $context) use (&$debugLogs) { - $debugLogs[] = [$message, $context]; - } - ); + $logger + ->method('debug') + ->willReturnCallback(static function (string $msg, array $ctx) use (&$debugLogs) { + $debugLogs[] = [$msg, $ctx]; + }); + // –– exercise –– $session->run('RETURN 1 as test'); + // –– assert INFO –– self::assertCount(3, $infoLogs); - self::assertEquals(array_slice($expectedInfoLogs, 0, 2), array_slice($infoLogs, 0, 2)); - /** - * @psalm-suppress PossiblyUndefinedIntArrayOffset - */ - self::assertEquals($expectedInfoLogs[2][0], $infoLogs[2][0]); - /** - * @psalm-suppress PossiblyUndefinedIntArrayOffset - * @psalm-suppress MixedArrayAccess - */ + self::assertEquals(array_slice($expectedInfo, 0, 2), array_slice($infoLogs, 0, 2)); + self::assertEquals($expectedInfo[2][0], $infoLogs[2][0]); self::assertInstanceOf(SessionConfiguration::class, $infoLogs[2][1]['sessionConfig']); - self::assertEquals($expectedDebugLogs, $debugLogs); + // –– now drop both HELLO & LOGON entries –– + $filteredActual = array_values(array_filter( + $debugLogs, + fn (array $entry) => !in_array($entry[0], ['HELLO', 'LOGON'], true) + )); + $filteredExpected = array_values(array_filter( + $expectedDebug, + fn (array $entry) => !in_array($entry[0], ['HELLO', 'LOGON'], true) + )); + + self::assertEquals($filteredExpected, $filteredActual); } } diff --git a/tests/Unit/BoltConnectionPoolTest.php b/tests/Unit/BoltConnectionPoolTest.php index 8ecc2b33..987ce985 100644 --- a/tests/Unit/BoltConnectionPoolTest.php +++ b/tests/Unit/BoltConnectionPoolTest.php @@ -171,6 +171,7 @@ private function setupPool(Generator $semaphoreGenerator): void SslConfiguration::default() ), null, + 10.0 ); } }