├── ext.ini ├── utils ├── remove_optional_from_proto.py ├── get_licenses_from_ort.py └── patch_proto_and_rust.py ├── .github ├── codeql │ └── codeql-config.yml ├── workflows │ ├── apt-packages.txt │ ├── lint-yaml.yml │ ├── git-secrets-scan.yml │ ├── install-rust-and-protoc │ │ └── action.yml │ ├── update-glide-version │ │ └── action.yml │ ├── install-zig │ │ └── action.yml │ ├── test-benchmark │ │ └── action.yml │ ├── stale.yml │ ├── codeql.yml │ ├── semgrep.yml │ ├── start-self-hosted-runner │ │ └── action.yml │ ├── lint-rust │ │ └── action.yml │ ├── ort-sweeper.yml │ ├── setup-php-extension │ │ └── action.yml │ ├── install-shared-dependencies │ │ └── action.yml │ ├── setup-valkey-glide │ │ └── action.yml │ ├── run-ort-tools │ │ └── action.yml │ ├── create-test-matrices │ │ └── action.yml │ ├── install-engine │ │ └── action.yml │ ├── run-php-tests │ │ └── action.yml │ └── build-php-wrapper │ │ └── action.yml ├── json_matrices │ ├── supported-languages-versions.json │ ├── engine-matrix.json │ └── build-matrix.json ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── task.yml │ ├── feature-request.yml │ ├── inquiry.yml │ ├── flaky-ci-test-issue.yml │ └── bug-report.yml ├── dependabot.yml └── DEVELOPER.md ├── .gitmodules ├── aboutcode.template ├── php_valkey_glide.h ├── php_build.sh ├── SECURITY.md ├── tests ├── run_test ├── start_valkey_with_replicas.sh ├── create-valkey-cluster.sh ├── run_test_s.php ├── run_test_s_cluster.php ├── ValkeyGlideClusterBatchTest.php └── TestValkeyGlide.php ├── CHANGELOG.md ├── phpstan.neon ├── MetricsConfig.php ├── examples ├── utils │ ├── start_valkey_with_replicas.sh │ └── create-valkey-cluster.sh ├── README.md ├── otel_example.php └── basic │ └── standalone_client.php ├── NOTICE ├── .clang-format ├── .vscode ├── tasks.json ├── launch.json └── settings.json ├── cluster_scan_cursor.stub.php ├── TracesConfig.php ├── valkey_glide_otel.h ├── MetricsConfigBuilder.php ├── cluster_scan_cursor.h ├── lint.sh ├── composer.json ├── OpenTelemetryConfig.php ├── config.h ├── phpcs.xml ├── TracesConfigBuilder.php ├── .gitignore ├── OpenTelemetryConfigBuilder.php ├── CONTRIBUTING.md ├── .ort.yml ├── logger.stub.php ├── command_response.h ├── cluster_scan_cursor.c ├── package.xml └── AGENTS.md /ext.ini: -------------------------------------------------------------------------------- 1 | extension=modules/valkey_glide.so 2 | 3 | -------------------------------------------------------------------------------- /utils/remove_optional_from_proto.py: -------------------------------------------------------------------------------- 1 | patch_proto_and_rust.py -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | paths-ignore: 2 | - "examples/**" 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "valkey-glide"] 2 | path = valkey-glide 3 | url = https://github.com/valkey-io/valkey-glide.git 4 | -------------------------------------------------------------------------------- /.github/workflows/apt-packages.txt: -------------------------------------------------------------------------------- 1 | build-essential 2 | php-dev 3 | protobuf-compiler 4 | libprotobuf-c-dev 5 | libprotobuf-c1 6 | pkg-config 7 | libssl-dev 8 | valgrind 9 | -------------------------------------------------------------------------------- /.github/json_matrices/supported-languages-versions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "language": "php", 4 | "versions": ["8.2", "8.3"], 5 | "always-run-versions": ["8.2", "8.3"] 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /aboutcode.template: -------------------------------------------------------------------------------- 1 | {% for license in licenses_list %} 2 | {%for about_object in abouts %} 3 | {% if loop.last %} 4 | {{license.text}} 5 | {% endif %} 6 | {% endfor %} 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /php_valkey_glide.h: -------------------------------------------------------------------------------- 1 | #ifndef PHP_VALKEY_GLIDE_H 2 | #define PHP_VALKEY_GLIDE_H 3 | 4 | #include "php.h" 5 | 6 | extern zend_module_entry valkey_glide_module_entry; 7 | #define phpext_valkey_glide_ptr &valkey_glide_module_entry 8 | 9 | #define PHP_VALKEY_GLIDE_VERSION "0.10.0" 10 | 11 | #endif /* PHP_VALKEY_GLIDE_H */ 12 | -------------------------------------------------------------------------------- /.github/json_matrices/engine-matrix.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "valkey", 4 | "version": "9.0", 5 | "run": "always" 6 | }, 7 | { 8 | "type": "valkey", 9 | "version": "8.1" 10 | }, 11 | { 12 | "type": "valkey", 13 | "version": "8.0" 14 | }, 15 | { 16 | "type": "valkey", 17 | "version": "7.2" 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /php_build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | git submodule update --init --recursive 4 | 5 | python3 utils/remove_optional_from_proto.py 6 | cd valkey-glide/ffi 7 | cargo build --release 8 | cd ../../ 9 | 10 | protoc --proto_path=./valkey-glide/glide-core/src/protobuf --php_out=./tests/generated ./valkey-glide/glide-core/src/protobuf/connection_request.proto 11 | 12 | phpize 13 | ./configure --enable-valkey-glide 14 | make -j4 build-modules-pre 15 | make install -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a Vulnerability 2 | 3 | If you believe you've discovered a security vulnerability, please contact the Valkey team at security@lists.valkey.io. 4 | Please *DO NOT* create an issue. 5 | We follow a responsible disclosure procedure, so depending on the severity of the issue we may notify Valkey vendors about the issue before releasing it publicly. 6 | If you would like to be added to our list of vendors, please reach out to the Valkey team at valkey-glide@lists.valkey.io. 7 | -------------------------------------------------------------------------------- /tests/run_test: -------------------------------------------------------------------------------- 1 | php -n -d extension=../modules/valkey_glide.so TestValkeyGlide.php --class valkeyglideclusterfeatures 2 | php TestValkeyGlide.php --class valkeyglideclientfeatures 3 | php TestValkeyGlide.php --class valkeyglide 4 | php TestValkeyGlide.php --class valkeyglidecluster 5 | 6 | # Run TLS-specific tests 7 | echo "Running TLS standalone tests" 8 | php TestValkeyGlide.php --class valkeyglideclientfeatures --tls --port 6400 9 | 10 | # Run mock connection constructor tests 11 | echo "Running ConnectionRequest tests" 12 | php TestValkeyGlide.php --class connectionrequest 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.10.0 2 | 3 | #### Changes 4 | 5 | * PHP: Add refresh topology configuration 6 | * PHP: Add Multi-Database Support for Cluster Mode Valkey 9.0 - Added `database_id` parameter to `ValkeyGlideCluster` constructor and support for SELECT, COPY, and MOVE commands in cluster mode. The COPY command can now specify a `database_id` parameter for cross-database operations. This feature requires Valkey 9.0+ with `cluster-databases > 1` configuration. 7 | 8 | #### Documentation 9 | 10 | * PHP: Updated README with multi-database cluster examples and inline documentation for database selection requirements and best practices 11 | 12 | ## 0.9.0 13 | -------------------------------------------------------------------------------- /.github/workflows/lint-yaml.yml: -------------------------------------------------------------------------------- 1 | name: lint-yaml 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - release-* 11 | paths: 12 | - ".github/**/*.yml" 13 | - ".github/**/*.yaml" 14 | pull_request: 15 | paths: 16 | - ".github/**/*.yml" 17 | - ".github/**/*.yaml" 18 | workflow_dispatch: 19 | 20 | jobs: 21 | lint: 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 10 24 | 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v4 28 | 29 | - name: Run Prettier on YAML files 30 | run: | 31 | npx prettier --check .github/ 32 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - tests 5 | - examples 6 | excludePaths: 7 | - tests/generated 8 | - tests/Connection_request 9 | - tests/GPBMetadata 10 | - vendor 11 | - autom4te.cache 12 | - build 13 | - .libs 14 | ignoreErrors: 15 | - '#Call to an undefined method#' 16 | - '#Access to an undefined property#' 17 | - '#Function [a-zA-Z0-9_]+\(\) should return [a-zA-Z0-9_|\\\]+but returns [a-zA-Z0-9_|\\\]+#' 18 | checkMissingIterableValueType: false 19 | checkGenericClassInNonGenericObjectType: false 20 | reportUnmatchedIgnoredErrors: false 21 | tmpDir: %rootDir%/tmp/phpstan 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Issue link 10 | 11 | This Pull Request is linked to issue (URL): [REPLACE ME] 12 | 13 | ### Checklist 14 | 15 | Before submitting the PR make sure the following are checked: 16 | 17 | - [ ] This Pull Request is related to one issue. 18 | - [ ] Commit message has a detailed description of what changed and why. 19 | - [ ] Tests are added or updated. 20 | - [ ] CHANGELOG.md and documentation files are updated. 21 | - [ ] Destination branch is correct - main or release 22 | - [ ] Create merge commit if merging release branch into main, squash otherwise. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.yml: -------------------------------------------------------------------------------- 1 | name: Task 2 | description: Create a task to manage work 3 | title: "[Task]" 4 | labels: ["task"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | ## Task Description 12 | 13 | - type: textarea 14 | attributes: 15 | label: Description 16 | description: Describe the task in detail 17 | placeholder: Describe the task... 18 | 19 | - type: textarea 20 | attributes: 21 | label: Checklist 22 | description: Add items to be completed 23 | value: | 24 | 1. 25 | 2. 26 | ... 27 | 28 | - type: textarea 29 | attributes: 30 | label: Additional Notes 31 | description: Add any additional notes or comments 32 | placeholder: Any additional notes... 33 | -------------------------------------------------------------------------------- /.github/workflows/git-secrets-scan.yml: -------------------------------------------------------------------------------- 1 | name: Git Secrets Scan 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: git-secrets-scan-${{ github.head_ref || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | scan: 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 10 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v5 22 | 23 | - name: Install git-secrets 24 | run: | 25 | git clone --depth 1 --branch 1.3.0 https://github.com/awslabs/git-secrets.git 26 | cd git-secrets 27 | sudo make install 28 | 29 | - name: Configure git-secrets 30 | run: git secrets --register-aws 31 | 32 | - name: Run git-secrets 33 | run: git secrets --scan 34 | -------------------------------------------------------------------------------- /.github/workflows/install-rust-and-protoc/action.yml: -------------------------------------------------------------------------------- 1 | name: Install Rust tool chain and protoc 2 | 3 | inputs: 4 | target: 5 | description: "Specified target for rust toolchain, ex. x86_64-apple-darwin" 6 | type: string 7 | required: false 8 | default: "x86_64-unknown-linux-gnu" 9 | options: 10 | - x86_64-unknown-linux-gnu 11 | - aarch64-unknown-linux-gnu 12 | - x86_64-apple-darwin 13 | - aarch64-apple-darwin 14 | github-token: 15 | description: "GitHub token" 16 | type: string 17 | required: true 18 | 19 | runs: 20 | using: "composite" 21 | steps: 22 | - name: Install Rust toolchain 23 | uses: dtolnay/rust-toolchain@stable 24 | with: 25 | targets: ${{ inputs.target }} 26 | 27 | - name: Install protoc (protobuf) 28 | uses: arduino/setup-protoc@v3 29 | with: 30 | version: "25.1" 31 | repo-token: ${{ inputs.github-token }} 32 | -------------------------------------------------------------------------------- /.github/workflows/update-glide-version/action.yml: -------------------------------------------------------------------------------- 1 | name: Update GLIDE version in the config.toml file 2 | 3 | inputs: 4 | folder_path: 5 | description: "The folder path of the config.toml file" 6 | required: true 7 | type: string 8 | named_os: 9 | description: "The name of the current operating system" 10 | required: false 11 | default: "linux" 12 | type: string 13 | options: 14 | - linux 15 | - darwin 16 | runs: 17 | using: "composite" 18 | steps: 19 | - name: Update package version in config.toml 20 | working-directory: ${{ inputs.folder_path }} 21 | shell: bash 22 | env: 23 | NAMED_OS: ${{ inputs.named_os }} 24 | run: | 25 | SED_FOR_MACOS=`if [[ "$NAMED_OS" == "darwin" ]]; then echo "''"; fi` 26 | sed -i $SED_FOR_MACOS "s|unknown|${{ env.RELEASE_VERSION }}|g" ./config.toml 27 | # log the config.toml file 28 | cat ./config.toml 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for Composer dependencies 4 | - package-ecosystem: "composer" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "monday" 9 | time: "09:00" 10 | open-pull-requests-limit: 10 11 | reviewers: 12 | - "valkey-io/php-maintainers" 13 | assignees: 14 | - "valkey-io/php-maintainers" 15 | commit-message: 16 | prefix: "deps" 17 | prefix-development: "deps-dev" 18 | include: "scope" 19 | 20 | # Enable version updates for GitHub Actions 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | schedule: 24 | interval: "weekly" 25 | day: "monday" 26 | time: "09:00" 27 | open-pull-requests-limit: 5 28 | reviewers: 29 | - "valkey-io/php-maintainers" 30 | assignees: 31 | - "valkey-io/php-maintainers" 32 | commit-message: 33 | prefix: "ci" 34 | include: "scope" 35 | -------------------------------------------------------------------------------- /MetricsConfig.php: -------------------------------------------------------------------------------- 1 | endpoint = $builder->getEndpoint(); 20 | } 21 | 22 | /** 23 | * Creates a new MetricsConfig builder. 24 | * 25 | * @return MetricsConfigBuilder A new builder instance. 26 | */ 27 | public static function builder(): MetricsConfigBuilder 28 | { 29 | return new MetricsConfigBuilder(); 30 | } 31 | 32 | /** 33 | * Gets the endpoint. 34 | * 35 | * @return string The metrics endpoint URL. 36 | */ 37 | public function getEndpoint(): string 38 | { 39 | return $this->endpoint; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/utils/start_valkey_with_replicas.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | BASE_DIR="$(pwd)/valkey_data" 6 | 7 | # Create data directories with full path 8 | for port in 6379 6380 6381; do 9 | mkdir -p "$BASE_DIR/$port" 10 | done 11 | 12 | # Start primary (6379) 13 | valkey-server --port 6379 \ 14 | --dir "$BASE_DIR/6379" \ 15 | --daemonize yes \ 16 | --logfile "$BASE_DIR/6379/valkey.log" 17 | 18 | # Start replicas (6380, 6381) 19 | valkey-server --port 6380 \ 20 | --dir "$BASE_DIR/6380" \ 21 | --daemonize yes \ 22 | --logfile "$BASE_DIR/6380/valkey.log" 23 | 24 | valkey-server --port 6381 \ 25 | --dir "$BASE_DIR/6381" \ 26 | --daemonize yes \ 27 | --logfile "$BASE_DIR/6381/valkey.log" 28 | 29 | # Wait a moment for servers to start 30 | sleep 2 31 | 32 | # Make 6380 and 6381 replicas of 6379 33 | valkey-cli -p 6380 REPLICAOF 127.0.0.1 6379 34 | valkey-cli -p 6381 REPLICAOF 127.0.0.1 6379 35 | 36 | echo "✅ Valkey setup complete:" 37 | echo "- Primary: 127.0.0.1:6379" 38 | echo "- Replica: 127.0.0.1:6380" 39 | echo "- Replica: 127.0.0.1:6381" 40 | 41 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Valkey General Language Independent Driver for the Enterprise (GLIDE), is an open-source Valkey client library. Valkey GLIDE is one of the official client libraries for Valkey, and it supports all Valkey commands. Valkey GLIDE supports Valkey 7.2 and above, and Redis open-source 6.2, 7.0 and 7.2. Application programmers use Valkey GLIDE to safely and reliably connect their applications to Valkey- and Redis OSS- compatible services. Valkey GLIDE is designed for reliability, optimized performance, and high-availability, for Valkey and Redis OSS based applications. It is sponsored and supported by AWS, and is pre-configured with best practices learned from over a decade of operating Redis OSS-compatible services used by hundreds of thousands of customers. To help ensure consistency in application development and operations, Valkey GLIDE is implemented using a core driver framework, written in Rust, with language specific extensions. This design ensures consistency in features across languages and reduces overall complexity. 2 | 3 | Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | redis-rs code copyright 2022 by redis-rs contributors. 5 | -------------------------------------------------------------------------------- /.github/workflows/install-zig/action.yml: -------------------------------------------------------------------------------- 1 | name: Install zig compiler and ziglang 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Cache Cargo for zig tools 7 | uses: actions/cache@v4 8 | with: 9 | path: | 10 | ~/.cargo/registry/index/ 11 | ~/.cargo/registry/cache/ 12 | ~/.cargo/git/db/ 13 | ~/.cargo/bin/cargo-zigbuild 14 | ~/.cargo/bin/cbindgen 15 | key: cargo-zig-${{ runner.os }}-cargo-zigbuild-v0.20.1-cbindgen 16 | restore-keys: | 17 | cargo-zig-${{ runner.os }}- 18 | 19 | - name: Install zig 20 | shell: bash 21 | run: | 22 | if [[ `cat /etc/os-release | grep "Amazon Linux"` ]]; then 23 | yum install -y python3-pip 24 | else 25 | sudo apt install -y python3-pip 26 | fi 27 | pip3 install ziglang 28 | 29 | # Only install cargo-zigbuild if not cached 30 | if ! command -v cargo-zigbuild &> /dev/null; then 31 | echo "Installing cargo-zigbuild..." 32 | cargo install --locked cargo-zigbuild 33 | else 34 | echo "cargo-zigbuild already installed (cached)" 35 | fi 36 | -------------------------------------------------------------------------------- /.github/workflows/test-benchmark/action.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark test 2 | 3 | inputs: 4 | language-flag: 5 | description: "flag that tells the benchmark to run a certain language" 6 | required: true 7 | type: string 8 | 9 | runs: 10 | using: "composite" 11 | 12 | steps: 13 | # SH ephemeral runner is a VM which created anew every time and it doesn't have some required software 14 | - name: Install Node on self-hosted runner 15 | if: ${{ runner.environment == 'self-hosted' }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 23 19 | 20 | - name: Install extra software on self-hosted runner 21 | if: ${{ runner.environment == 'self-hosted' }} 22 | shell: bash 23 | run: | 24 | sudo apt-get update 25 | sudo apt-get install -y npm 26 | 27 | - shell: bash 28 | # Disable RDB snapshots to avoid configuration errors 29 | run: redis-server --save "" --daemonize "yes" 30 | 31 | - shell: bash 32 | working-directory: ./benchmarks 33 | env: 34 | LANGUAGE_FLAG: ${{ inputs.language-flag }} 35 | run: ./install_and_test.sh -no-tls -minimal -only-glide -data 1 -tasks 10 $LANGUAGE_FLAG 36 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | IndentWidth: 4 3 | TabWidth: 4 4 | UseTab: Never 5 | ColumnLimit: 100 6 | AllowShortFunctionsOnASingleLine: None 7 | AlwaysBreakBeforeMultilineStrings: true 8 | BreakBeforeBraces: Attach 9 | AlignConsecutiveAssignments: Consecutive 10 | AlignConsecutiveDeclarations: Consecutive 11 | AlignTrailingComments: true 12 | AllowShortBlocksOnASingleLine: Never 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortIfStatementsOnASingleLine: Never 15 | AllowShortLoopsOnASingleLine: false 16 | BinPackArguments: false 17 | BinPackParameters: false 18 | BreakAfterJavaFieldAnnotations: false 19 | BreakBeforeBinaryOperators: None 20 | BreakBeforeTernaryOperators: true 21 | BreakConstructorInitializersBeforeComma: false 22 | BreakStringLiterals: true 23 | Cpp11BracedListStyle: true 24 | DerivePointerAlignment: false 25 | IndentCaseLabels: true 26 | IndentWrappedFunctionNames: false 27 | MaxEmptyLinesToKeep: 2 28 | PointerAlignment: Left 29 | SpaceAfterCStyleCast: true 30 | SpaceBeforeAssignmentOperators: true 31 | SpaceBeforeParens: ControlStatements 32 | SpaceInEmptyParentheses: false 33 | SpacesInAngles: false 34 | SpacesInCStyleCastParentheses: false 35 | SpacesInParentheses: false 36 | SpacesInSquareBrackets: false 37 | Language: Cpp 38 | Standard: c++03 39 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/csharp/tests/tests.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/csharp/tests/tests.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/csharp/tests/tests.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /examples/utils/create-valkey-cluster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR="$HOME/valkey-cluster" 4 | PORTS=(7001 7002 7003 7004 7005 7006) 5 | VALKEY_BIN=$(which valkey-server) 6 | CLI_BIN=$(which valkey-cli) 7 | 8 | if [ -z "$VALKEY_BIN" ] || [ -z "$CLI_BIN" ]; then 9 | echo "valkey-server or valkey-cli not found in PATH" 10 | exit 1 11 | fi 12 | 13 | # 1. Clean previous setup 14 | echo "Cleaning up old cluster data..." 15 | rm -rf "$BASE_DIR" 16 | mkdir -p "$BASE_DIR" 17 | 18 | # 2. Create config and data folders 19 | for port in "${PORTS[@]}"; do 20 | NODE_DIR="$BASE_DIR/$port" 21 | mkdir -p "$NODE_DIR" 22 | 23 | cat > "$NODE_DIR/valkey.conf" <endpoint = $builder->getEndpoint(); 21 | $this->samplePercentage = $builder->getSamplePercentage(); 22 | } 23 | 24 | /** 25 | * Creates a new TracesConfig builder. 26 | * 27 | * @return TracesConfigBuilder A new builder instance. 28 | */ 29 | public static function builder(): TracesConfigBuilder 30 | { 31 | return new TracesConfigBuilder(); 32 | } 33 | 34 | /** 35 | * Gets the endpoint. 36 | * 37 | * @return string The traces endpoint URL. 38 | */ 39 | public function getEndpoint(): string 40 | { 41 | return $this->endpoint; 42 | } 43 | 44 | /** 45 | * Gets the sample percentage. 46 | * 47 | * @return int The sample percentage (0-100). 48 | */ 49 | public function getSamplePercentage(): int 50 | { 51 | return $this->samplePercentage; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "35 00 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | if: github.repository_owner == 'valkey-io' 12 | permissions: 13 | issues: write 14 | pull-requests: write 15 | steps: 16 | - name: Close Stale Issues 17 | uses: actions/stale@v9.1.0 18 | with: 19 | repo-token: ${{ github.token }} 20 | stale-issue-message: "This issue is inactive for 90 days, hence marked as stale, if the issue is still relevant please perform some activity" 21 | stale-pr-message: "This PR is inactive for 60 days, hence marked as stale, if the PR is still relevant please perform some activity" 22 | close-issue-message: "Since issue was marked as stale and no activity perfomed for 14 days after it was mark, this issue is closed automatically" 23 | close-pr-message: "Since PR was marked as stale and no activity perfomed for 10 days after it was mark, this PR is closed automatically" 24 | days-before-stale: 90 25 | days-before-pr-stale: 60 26 | days-before-close: 14 27 | days-before-pr-close: 10 28 | exempt-issue-labels: "bug,Users Pain,Epic,User issue,Unatriaged user issue" 29 | exempt-all-milestones: true 30 | operations-per-run: 50 31 | labels-to-add-when-unstale: "Unstaled" 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | - "v.?[0-9]+.[0-9]+.[0-9]+" 8 | - "v.?[0-9]+.[0-9]+" 9 | - "v?[0-9]+.[0-9]+.[0-9]+" 10 | - "v?[0-9]+.[0-9]+" 11 | - release-* 12 | pull_request: 13 | branches: 14 | - "main" 15 | - "v.?[0-9]+.[0-9]+.[0-9]+" 16 | - "v.?[0-9]+.[0-9]+" 17 | - "v?[0-9]+.[0-9]+.[0-9]+" 18 | - "v?[0-9]+.[0-9]+" 19 | - release-* 20 | schedule: 21 | - cron: "37 18 * * 6" 22 | workflow_dispatch: 23 | 24 | jobs: 25 | # Run CodeQL analysis for each language 26 | analyze: 27 | name: Analyze (${{ matrix.language }}) 28 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 29 | permissions: 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | include: 36 | - language: actions 37 | build-mode: none 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | - name: Initialize CodeQL 44 | uses: github/codeql-action/init@v3 45 | with: 46 | languages: ${{ matrix.language }} 47 | build-mode: ${{ matrix.build-mode }} 48 | config-file: .github/codeql/codeql-config.yml 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v3 52 | with: 53 | category: "/language:${{matrix.language}}" 54 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/csharp/tests/bin/Debug/net6.0/tests.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/csharp/tests", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": "C# benchmark Launch (console)", 22 | "type": "coreclr", 23 | "request": "launch", 24 | "preLaunchTask": "build", 25 | "program": "${workspaceFolder}/benchmarks/csharp/bin/Debug/net8.0/csharp_benchmark.dll", 26 | "args": [], 27 | "cwd": "${workspaceFolder}/benchmarks/csharp", 28 | "console": "internalConsole", 29 | "stopAtEntry": true 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /valkey_glide_otel.h: -------------------------------------------------------------------------------- 1 | /* 2 | +----------------------------------------------------------------------+ 3 | | Copyright (c) 2023-2025 The PHP Group | 4 | +----------------------------------------------------------------------+ 5 | | This source file is subject to version 3.01 of the PHP license, | 6 | | that is bundled with this package in the file LICENSE, and is | 7 | | available through the world-wide-web at the following url: | 8 | | http://www.php.net/license/3_01.txt | 9 | | If you did not receive a copy of the PHP license and are unable to | 10 | | obtain it through the world-wide-web, please send a note to | 11 | | license@php.net so we can mail you a copy immediately. | 12 | +----------------------------------------------------------------------+ 13 | */ 14 | 15 | #ifndef VALKEY_GLIDE_OTEL_H 16 | #define VALKEY_GLIDE_OTEL_H 17 | 18 | #include "include/glide_bindings.h" 19 | #include "php.h" 20 | 21 | /* Global OTEL configuration */ 22 | extern struct OpenTelemetryConfig* g_otel_config; 23 | 24 | /* Function declarations */ 25 | int valkey_glide_otel_init(zval* config_obj); 26 | void valkey_glide_otel_set_sample_percentage(uint32_t percentage); 27 | int valkey_glide_otel_get_sample_percentage(uint32_t* percentage); 28 | uint64_t valkey_glide_create_span(enum RequestType request_type); 29 | void valkey_glide_drop_span(uint64_t span_ptr); 30 | 31 | #endif /* VALKEY_GLIDE_OTEL_H */ 32 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | # Scan changed files in PRs (diff-aware scanning): 8 | pull_request: {} 9 | # Scan on-demand through GitHub Actions interface: 10 | workflow_dispatch: 11 | inputs: 12 | branch: 13 | description: "The branch to run against the semgrep tool" 14 | required: true 15 | push: 16 | branches: 17 | - main 18 | - release-* 19 | - v* 20 | # Schedule the CI job (this method uses cron syntax): 21 | schedule: 22 | - cron: "0 8 * * *" # Sets Semgrep to scan every day at 08:00 UTC. 23 | 24 | jobs: 25 | semgrep: 26 | # User definable name of this GitHub Actions job. 27 | name: semgrep/ci 28 | # If you are self-hosting, change the following `runs-on` value: 29 | runs-on: ubuntu-latest 30 | 31 | container: 32 | # A Docker image with Semgrep installed. Pinned to specific version to avoid Docker Hub rate limiting. 33 | image: semgrep/semgrep:1.45.0 34 | 35 | # Skip any PR created by dependabot to avoid permission issues: 36 | if: (github.actor != 'dependabot[bot]' && github.repository_owner == 'valkey-io') 37 | 38 | steps: 39 | # Fetch project source with GitHub Actions Checkout. 40 | - uses: actions/checkout@v4 41 | # Run the "semgrep ci" command on the command line of the docker image. 42 | - run: semgrep ci --config auto --no-suppress-errors --exclude-rule generic.secrets.security.detected-private-key.detected-private-key 43 | -------------------------------------------------------------------------------- /tests/start_valkey_with_replicas.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | BASE_DIR="$(pwd)/valkey_data" 6 | 7 | # Create data directories with full path 8 | for port in 6379 6380 6381; do 9 | mkdir -p "$BASE_DIR/$port" 10 | done 11 | 12 | # Start primary (6379) 13 | valkey-server --port 6379 \ 14 | --dir "$BASE_DIR/6379" \ 15 | --daemonize yes \ 16 | --logfile "$BASE_DIR/6379/valkey.log" \ 17 | --enable-debug-command yes 18 | 19 | # Start replicas (6380, 6381) 20 | valkey-server --port 6380 \ 21 | --dir "$BASE_DIR/6380" \ 22 | --daemonize yes \ 23 | --logfile "$BASE_DIR/6380/valkey.log" \ 24 | --enable-debug-command yes 25 | 26 | 27 | valkey-server --port 6381 \ 28 | --dir "$BASE_DIR/6381" \ 29 | --daemonize yes \ 30 | --logfile "$BASE_DIR/6381/valkey.log" \ 31 | --enable-debug-command yes 32 | 33 | # Handle TLS setup with graceful failure 34 | echo "Setting up TLS standalone server..." 35 | if ../valkey-glide/utils/cluster_manager.py --tls start --prefix tls-standalone -p 6400 -r 0; then 36 | echo "✅ TLS standalone server started on port 6400" 37 | else 38 | echo "⚠️ WARNING: TLS standalone setup failed (port 6400 may be in use), continuing without TLS..." 39 | fi 40 | 41 | # Wait a moment for servers to start 42 | sleep 2 43 | 44 | # Make 6380 and 6381 replicas of 6379 45 | valkey-cli -p 6380 REPLICAOF 127.0.0.1 6379 46 | valkey-cli -p 6381 REPLICAOF 127.0.0.1 6379 47 | 48 | echo "✅ Valkey setup complete:" 49 | echo "- Primary: 127.0.0.1:6379" 50 | echo "- Replica: 127.0.0.1:6380" 51 | echo "- Replica: 127.0.0.1:6381" 52 | -------------------------------------------------------------------------------- /MetricsConfigBuilder.php: -------------------------------------------------------------------------------- 1 | endpoint = $endpoint; 26 | return $this; 27 | } 28 | 29 | /** 30 | * Gets the endpoint. 31 | * 32 | * @return string The metrics endpoint URL. 33 | */ 34 | public function getEndpoint(): string 35 | { 36 | if ($this->endpoint === null) { 37 | throw new ValkeyGlideException("Metrics endpoint is required when metrics config is provided"); 38 | } 39 | return $this->endpoint; 40 | } 41 | 42 | /** 43 | * Builds the MetricsConfig. 44 | * 45 | * @return MetricsConfig The immutable metrics configuration. 46 | */ 47 | public function build(): MetricsConfig 48 | { 49 | if ($this->endpoint === null) { 50 | throw new ValkeyGlideException("Metrics endpoint is required when metrics config is provided"); 51 | } 52 | 53 | return new MetricsConfig($this); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cluster_scan_cursor.h: -------------------------------------------------------------------------------- 1 | #ifndef CLUSTER_SCAN_CURSOR_H 2 | #define CLUSTER_SCAN_CURSOR_H 3 | 4 | #include "common.h" 5 | #include "php.h" 6 | 7 | /* ClusterScanCursor object structure */ 8 | typedef struct { 9 | char* cursor_id; /* The cursor ID string */ 10 | char* next_cursor_id; /* The cursor ID string */ 11 | zend_object std; /* Standard PHP object */ 12 | } cluster_scan_cursor_object; 13 | 14 | /* Class entry and handlers */ 15 | extern zend_class_entry* cluster_scan_cursor_ce; 16 | extern zend_object_handlers cluster_scan_cursor_object_handlers; 17 | 18 | /* Object creation and destruction */ 19 | zend_object* create_cluster_scan_cursor_object(zend_class_entry* ce); 20 | void free_cluster_scan_cursor_object(zend_object* object); 21 | 22 | /* Class methods */ 23 | PHP_METHOD(ClusterScanCursor, __construct); 24 | PHP_METHOD(ClusterScanCursor, __destruct); 25 | PHP_METHOD(ClusterScanCursor, getCursor); 26 | PHP_METHOD(ClusterScanCursor, getNextCursor); 27 | PHP_METHOD(ClusterScanCursor, isFinished); 28 | 29 | /* Helper macros */ 30 | #define CLUSTER_SCAN_CURSOR_GET_OBJECT(obj) \ 31 | VALKEY_GLIDE_PHP_GET_OBJECT(cluster_scan_cursor_object, obj) 32 | #define CLUSTER_SCAN_CURSOR_ZVAL_GET_OBJECT(zv) \ 33 | VALKEY_GLIDE_PHP_ZVAL_GET_OBJECT(cluster_scan_cursor_object, zv) 34 | 35 | /* FFI function declaration */ 36 | extern void remove_cluster_scan_cursor(const char* cursor_id); 37 | 38 | /* Class registration function */ 39 | void register_cluster_scan_cursor_class(void); 40 | 41 | /* Getter function for class entry */ 42 | zend_class_entry* get_cluster_scan_cursor_ce(void); 43 | 44 | #endif /* CLUSTER_SCAN_CURSOR_H */ 45 | -------------------------------------------------------------------------------- /lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Running PHP linters..." 5 | 6 | # Check if we're in the php directory 7 | if [ ! -f "config.m4" ]; then 8 | echo "Error: This script must be run from the php directory" 9 | exit 1 10 | fi 11 | 12 | # Install composer dependencies if composer.json exists and vendor doesn't 13 | if [ -f "composer.json" ] && [ ! -d "vendor" ]; then 14 | echo "Installing composer dependencies..." 15 | if command -v composer &> /dev/null; then 16 | composer install --dev --no-progress --quiet 17 | else 18 | echo "Warning: composer not found, some PHP linting tools may not be available" 19 | fi 20 | fi 21 | 22 | echo "1. Running PHP CodeSniffer..." 23 | if command -v phpcs &> /dev/null || [ -f "vendor/bin/phpcs" ]; then 24 | if [ -f "vendor/bin/phpcs" ]; then 25 | ./vendor/bin/phpcs --standard=phpcs.xml --colors 26 | else 27 | phpcs --standard=phpcs.xml --colors 28 | fi 29 | echo "✓ PHP CodeSniffer passed" 30 | else 31 | echo "Warning: phpcs not found, skipping PHP coding standards check" 32 | fi 33 | 34 | echo "2. Checking C code formatting..." 35 | if command -v clang-format &> /dev/null; then 36 | # Find C files but exclude protobuf generated files 37 | if find . -name "*.c" -o -name "*.h" | grep -v "\.pb-c\." | grep -q .; then 38 | find . -name "*.c" -o -name "*.h" | grep -v "\.pb-c\." | xargs clang-format --dry-run --Werror 39 | echo "✓ C code formatting check passed" 40 | else 41 | echo "No C files found to check" 42 | fi 43 | else 44 | echo "Warning: clang-format not found, skipping C code formatting check" 45 | fi 46 | 47 | 48 | echo "" 49 | echo "🎉 All linting checks passed!" 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valkey-io/valkey-glide-php", 3 | "description": "General Language Independent Driver for the Enterprise (GLIDE) for Valkey", 4 | "type": "php-ext", 5 | "license": "Apache-2.0", 6 | "provide": { 7 | "ext-valkey_glide": "*" 8 | }, 9 | "php-ext": { 10 | "extension-name": "valkey_glide" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Valkey GLIDE contributors", 15 | "homepage": "https://github.com/valkey-io/valkey-glide-php" 16 | } 17 | ], 18 | "homepage": "https://github.com/valkey-io/valkey-glide-php", 19 | "require": { 20 | "php": ">=8.1" 21 | }, 22 | "require-dev": { 23 | "squizlabs/php_codesniffer": "^3.7", 24 | "phpstan/phpstan": "^1.10", 25 | "google/protobuf": "^v4.32.0", 26 | "protobuf-php/protobuf-plugin": "^0.1.3", 27 | "symfony/polyfill-ctype": "^1.32.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "ValkeyGlide\\": "src/", 32 | "ValkeyGlide\\OpenTelemetry\\": "./" 33 | } 34 | }, 35 | "scripts": { 36 | "lint": [ 37 | "phpcs --standard=phpcs.xml" 38 | ], 39 | "lint:fix": [ 40 | "phpcbf --standard=phpcs.xml" 41 | ], 42 | "analyze": [ 43 | "phpstan analyze" 44 | ], 45 | "test": [ 46 | "php run-tests.php tests/" 47 | ], 48 | "check": [ 49 | "@lint", 50 | "@analyze" 51 | ] 52 | }, 53 | "config": { 54 | "sort-packages": true, 55 | "allow-plugins": { 56 | "dealerdirect/phpcodesniffer-composer-installer": true 57 | }, 58 | "audit": { 59 | "ignore": ["PKSA-wws7-mr54-jsny"] 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/start-self-hosted-runner/action.yml: -------------------------------------------------------------------------------- 1 | name: Start self hosted EC2 runner 2 | 3 | inputs: 4 | aws-region: 5 | description: AWS Region, e.g. us-east-1 6 | required: true 7 | role-to-assume: 8 | description: AWS Role for which to fetch credentials. 9 | required: true 10 | ec2-instance-id: 11 | description: AWS EC2 instance ID for the self hosted runner 12 | required: true 13 | 14 | runs: 15 | using: "composite" 16 | steps: 17 | - name: Configure AWS Credentials 18 | uses: aws-actions/configure-aws-credentials@v4 19 | with: 20 | role-to-assume: ${{ inputs.role-to-assume }} 21 | aws-region: ${{ inputs.aws-region }} 22 | 23 | - name: Start EC2 self hosted runner 24 | shell: bash 25 | env: 26 | EC2_INSTANCE_ID: ${{ inputs.ec2-instance-id }} 27 | run: | 28 | sudo snap refresh 29 | sudo snap install aws-cli --classic 30 | command_id=$(aws ssm send-command --instance-ids "$EC2_INSTANCE_ID" --document-name StartGithubSelfHostedRunner --query Command.CommandId --output text) 31 | 32 | while [[ "$invoke_status" != "Success" && "$invoke_status" != "Failed" ]]; do 33 | invoke_status=$(aws ssm list-command-invocations --command-id $command_id --query 'CommandInvocations[0].Status' --output text) 34 | echo "Current Status: $invoke_status" 35 | if [[ "$invoke_status" != "Success" && "$invoke_status" != "Failed" ]]; then 36 | echo "Waiting for command to complete... Command ID: $command_id" 37 | sleep 10 38 | retry_counter=$((retry_counter+1)) 39 | fi 40 | if (( retry_counter >= 100 )); then 41 | echo "Reached maximum retries, status is: $invoke_status. Exiting..." 42 | exit 1 43 | fi 44 | done 45 | 46 | echo "Final Command Status: $invoke_status" 47 | -------------------------------------------------------------------------------- /.github/workflows/lint-rust/action.yml: -------------------------------------------------------------------------------- 1 | name: Lint rust 2 | 3 | inputs: 4 | cargo-toml-folder: 5 | description: "folder that contains the target Cargo.toml file" 6 | required: true 7 | type: string 8 | github-token: 9 | description: "github token" 10 | required: false 11 | type: string 12 | 13 | runs: 14 | using: "composite" 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install Rust toolchain and protoc 19 | uses: ./.github/workflows/install-rust-and-protoc 20 | with: 21 | github-token: ${{ inputs.github-token }} 22 | 23 | - uses: actions/cache@v4 24 | with: 25 | path: ${{ inputs.cargo-toml-folder }}/target 26 | # this action should be used on the most common runner only 27 | key: x86_64-unknown-linux-gnu-${{ inputs.cargo-toml-folder }} 28 | 29 | - run: cargo fmt --all -- --check 30 | working-directory: ${{ inputs.cargo-toml-folder }} 31 | shell: bash 32 | 33 | - run: cargo clippy --all-features --all-targets -- -D warnings 34 | working-directory: ${{ inputs.cargo-toml-folder }} 35 | shell: bash 36 | 37 | # We run clippy without features 38 | - run: cargo clippy --all-targets -- -D warnings 39 | working-directory: ${{ inputs.cargo-toml-folder }} 40 | shell: bash 41 | 42 | - run: | 43 | cargo update 44 | if [ -f "$HOME/.cargo/bin/cargo-deny" ]; then 45 | echo "cargo-deny already installed (cached)" 46 | else 47 | cargo install --locked cargo-deny 48 | fi 49 | cargo deny check --config ${GITHUB_WORKSPACE}/deny.toml 50 | working-directory: ${{ inputs.cargo-toml-folder }} 51 | shell: bash 52 | 53 | - name: doc 54 | shell: bash 55 | working-directory: ${{ inputs.cargo-toml-folder }} 56 | run: cargo doc --no-deps --document-private-items 57 | env: 58 | RUSTDOCFLAGS: -Dwarnings 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Request 3 | description: Suggest an idea for this project 4 | title: "(topic): (short issue description)" 5 | labels: [feature-request, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the feature 12 | description: A clear and concise description of the feature you are proposing. 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: use-case 17 | attributes: 18 | label: Use Case 19 | description: | 20 | Why do you need this feature? 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: solution 25 | attributes: 26 | label: Proposed Solution 27 | description: | 28 | Suggest how to implement the addition or change. Please include prototype/workaround/sketch/reference implementation. 29 | validations: 30 | required: false 31 | - type: textarea 32 | id: other 33 | attributes: 34 | label: Other Information 35 | description: | 36 | Any alternative solutions or features you considered, a more detailed explanation, stack traces, related issues, links for context, etc. 37 | validations: 38 | required: false 39 | - type: checkboxes 40 | id: ack 41 | attributes: 42 | label: Acknowledgements 43 | options: 44 | - label: I may be able to implement this feature request 45 | required: false 46 | - label: This feature might incur a breaking change 47 | required: false 48 | - type: input 49 | id: client-version 50 | attributes: 51 | label: Client version used 52 | validations: 53 | required: true 54 | - type: input 55 | id: environment 56 | attributes: 57 | label: Environment details (OS name and version, etc.) 58 | validations: 59 | required: true 60 | -------------------------------------------------------------------------------- /OpenTelemetryConfig.php: -------------------------------------------------------------------------------- 1 | traces = $builder->getTraces(); 22 | $this->metrics = $builder->getMetrics(); 23 | $this->flushIntervalMs = $builder->getFlushIntervalMs(); 24 | } 25 | 26 | /** 27 | * Creates a new OpenTelemetryConfig builder. 28 | * 29 | * @return OpenTelemetryConfigBuilder A new builder instance. 30 | */ 31 | public static function builder(): OpenTelemetryConfigBuilder 32 | { 33 | return new OpenTelemetryConfigBuilder(); 34 | } 35 | 36 | /** 37 | * Gets the traces configuration. 38 | * 39 | * @return TracesConfig|null The traces configuration, or null if not configured. 40 | */ 41 | public function getTraces(): ?TracesConfig 42 | { 43 | return $this->traces; 44 | } 45 | 46 | /** 47 | * Gets the metrics configuration. 48 | * 49 | * @return MetricsConfig|null The metrics configuration, or null if not configured. 50 | */ 51 | public function getMetrics(): ?MetricsConfig 52 | { 53 | return $this->metrics; 54 | } 55 | 56 | /** 57 | * Gets the flush interval in milliseconds. 58 | * 59 | * @return int|null The flush interval in milliseconds. 60 | */ 61 | public function getFlushIntervalMs(): ?int 62 | { 63 | return $this->flushIntervalMs; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Valkey GLIDE PHP Examples 2 | 3 | This directory contains comprehensive examples demonstrating how to use the Valkey GLIDE PHP client effectively. 4 | 5 | ## Prerequisites 6 | 7 | Before running these examples, ensure you have: 8 | 9 | 1. **Valkey GLIDE PHP extension installed** - See the main README.md for installation instructions 10 | 2. **Valkey server running** - Use the provided Docker setup or install locally 11 | 3. **PHP 8.1+** with the extension loaded 12 | 13 | ## Quick Start 14 | 15 | 1. **Start Valkey servers using Docker:** 16 | ```bash 17 | cd examples/utils 18 | ./create-valkey-cluster.sh 19 | ./start_valkey_with_replicas.sh 20 | ``` 21 | 22 | 2. **Run a basic example:** 23 | ```bash 24 | php basic/standalone_client.php 25 | ``` 26 | 27 | ## Directory Structure 28 | 29 | ### 📚 Basic Examples (`basic/`) 30 | - **`standalone_client.php`** - Basic standalone server connection 31 | - **`cluster_client.php`** - Basic cluster connection setup 32 | - **`configuration.php`** - Client configuration options 33 | 34 | ### 🎯 Real-world Patterns (`patterns/`) 35 | - **`caching.php`** - Web application caching patterns 36 | 37 | 38 | 39 | ## Running Examples 40 | 41 | ### Individual Examples 42 | ```bash 43 | # Run specific example 44 | php basic/standalone_client.php 45 | 46 | ``` 47 | 48 | ## Contributing 49 | 50 | When adding new examples: 51 | 52 | 1. Include comprehensive error handling 53 | 2. Add clear documentation and comments 54 | 3. Follow PSR-12 coding standards 55 | 4. Test with both standalone and cluster modes 56 | 5. Include performance considerations 57 | 58 | ## Support 59 | 60 | - **Documentation**: See main README.md and DEVELOPER.md 61 | - **Issues**: Report on GitHub 62 | - **Community**: Join the Valkey Slack workspace 63 | 64 | --- 65 | 66 | **Note**: These examples are for demonstration purposes. In production, implement additional security, monitoring, and error handling as needed. 67 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/inquiry.yml: -------------------------------------------------------------------------------- 1 | name: Inquiry 2 | description: Use this template for asking questions 3 | title: "[Inquiry] " 4 | labels: ["Inquiry"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | ## Question 12 | 13 | - type: textarea 14 | id: question-description 15 | attributes: 16 | label: Inquiry 17 | description: Describe your inquiry in detail 18 | placeholder: Describe your inquiry... 19 | 20 | - type: markdown 21 | attributes: 22 | value: | 23 | ## Language and Version 24 | 25 | - type: input 26 | id: language 27 | attributes: 28 | label: Language 29 | description: Optional - Specify the programming language 30 | placeholder: e.g., Python, Java 31 | 32 | - type: input 33 | id: language-version 34 | attributes: 35 | label: Language Version 36 | description: Optional - Specify the version of the language 37 | placeholder: e.g., 3.8, 11 38 | 39 | - type: markdown 40 | attributes: 41 | value: | 42 | ## Engine Version 43 | 44 | - type: input 45 | id: engine-version 46 | attributes: 47 | label: Engine Version 48 | description: Optional - Specify the engine version 49 | placeholder: e.g., ValKey 8.0.1, Redis-OSS 6.2.14 50 | 51 | - type: markdown 52 | attributes: 53 | value: | 54 | ## Operating System 55 | 56 | - type: input 57 | id: os 58 | attributes: 59 | label: Operating System 60 | description: Optional - Specify the operating system 61 | placeholder: e.g., MacOs 14, Ubuntu 20.04 62 | 63 | - type: markdown 64 | attributes: 65 | value: | 66 | ## Additional Technical Information 67 | 68 | - type: textarea 69 | id: additional-info 70 | attributes: 71 | label: Additional Technical Information 72 | description: Optional - Provide any additional technical information 73 | placeholder: Additional context or details... 74 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | /* config.h. Generated from config.h.in by configure. */ 2 | /* config.h.in. Generated from configure.ac by autoheader. */ 3 | 4 | /* Define to 1 if the PHP extension 'valkey_glide' is built as a dynamic 5 | module. */ 6 | #define COMPILE_DL_VALKEY_GLIDE 1 7 | 8 | /* Define to 1 if you have the header file. */ 9 | #define HAVE_DLFCN_H 1 10 | 11 | /* Define to 1 if you have the header file. */ 12 | #define HAVE_INTTYPES_H 1 13 | 14 | /* Define to 1 if you have the header file. */ 15 | #define HAVE_STDINT_H 1 16 | 17 | /* Define to 1 if you have the header file. */ 18 | #define HAVE_STDIO_H 1 19 | 20 | /* Define to 1 if you have the header file. */ 21 | #define HAVE_STDLIB_H 1 22 | 23 | /* Define to 1 if you have the header file. */ 24 | #define HAVE_STRINGS_H 1 25 | 26 | /* Define to 1 if you have the header file. */ 27 | #define HAVE_STRING_H 1 28 | 29 | /* Define to 1 if you have the header file. */ 30 | #define HAVE_SYS_STAT_H 1 31 | 32 | /* Define to 1 if you have the header file. */ 33 | #define HAVE_SYS_TYPES_H 1 34 | 35 | /* Define to 1 if you have the header file. */ 36 | #define HAVE_UNISTD_H 1 37 | 38 | /* Define to the address where bug reports for this package should be sent. */ 39 | /* #undef PACKAGE_BUGREPORT */ 40 | 41 | /* Define to the full name of this package. */ 42 | /* #undef PACKAGE_NAME */ 43 | 44 | /* Define to the full name and version of this package. */ 45 | /* #undef PACKAGE_STRING */ 46 | 47 | /* Define to the one symbol short name of this package. */ 48 | /* #undef PACKAGE_TARNAME */ 49 | 50 | /* Define to the home page for this package. */ 51 | /* #undef PACKAGE_URL */ 52 | 53 | /* Define to the version of this package. */ 54 | /* #undef PACKAGE_VERSION */ 55 | 56 | /* Define to 1 if all of the C89 standard headers exist (not just the ones 57 | required in a freestanding environment). This macro is provided for 58 | backward compatibility; new code need not use it. */ 59 | #define STDC_HEADERS 1 60 | 61 | /* Define if AddressSanitizer is enabled */ 62 | /* #undef VALKEY_GLIDE_ASAN_ENABLED */ 63 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PHP CodeSniffer configuration for Valkey GLIDE PHP 4 | 5 | 6 | tests/ 7 | 8 | 9 | */vendor/* 10 | *.pb-c.c 11 | *.pb-c.h 12 | */autom4te.cache/* 13 | */build/* 14 | */.libs/* 15 | */tests/Connection_request/* 16 | */tests/GPBMetadata/* 17 | */valkey-glide/* 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | */tests/* 37 | 38 | 39 | 40 | tests/* 41 | * 42 | 43 | 44 | 45 | *.stub.php 46 | 47 | examples/* 48 | 49 | run-tests.php 50 | 51 | tests/* 52 | 53 | 54 | 55 | *.stub.php 56 | 57 | 58 | -------------------------------------------------------------------------------- /.github/workflows/ort-sweeper.yml: -------------------------------------------------------------------------------- 1 | name: ORT - Trigger periodic checks for relevant branches 2 | 3 | permissions: 4 | contents: read 5 | actions: write 6 | 7 | on: 8 | schedule: 9 | - cron: "0 0 * * *" # Runs daily at 00:00 UTC 10 | 11 | jobs: 12 | trigger-ort-check: 13 | runs-on: ubuntu-latest 14 | if: github.repository_owner == 'valkey-io' 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Fetch relevant branches 21 | id: get-branches 22 | run: | 23 | # Get all branches matching 'release-*' and include 'main' 24 | branches=$(git ls-remote --heads origin | awk -F'/' '/refs\/heads\/release-/ {printf $NF" "}') 25 | branches="main $branches" 26 | echo "::set-output name=branches::$branches" 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Trigger ORT Check workflows 31 | uses: actions/github-script@v6 32 | with: 33 | script: | 34 | const branches = "${{ steps.get-branches.outputs.branches }}".split(" "); 35 | const workflowFile = "ort.yml"; 36 | 37 | const triggerWorkflow = async (branch) => { 38 | try { 39 | console.log(`Triggering workflow for branch: ${branch}`); 40 | await github.rest.actions.createWorkflowDispatch({ 41 | owner: context.repo.owner, 42 | repo: context.repo.repo, 43 | workflow_id: workflowFile, 44 | ref: branch, // The branch where workflow_dispatch is triggered 45 | inputs: { 46 | branch_name: branch 47 | } 48 | }); 49 | console.log(`Successfully triggered workflow for branch: ${branch}`); 50 | } catch (error) { 51 | core.setFailed(error.message); 52 | } 53 | }; 54 | 55 | // Fire all workflow dispatch requests concurrently 56 | const promises = branches 57 | .filter(branch => branch) // Skip empty branches 58 | .map(branch => triggerWorkflow(branch)); 59 | 60 | await Promise.allSettled(promises); 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /TracesConfigBuilder.php: -------------------------------------------------------------------------------- 1 | endpoint = $endpoint; 27 | return $this; 28 | } 29 | 30 | /** 31 | * Sets the sample percentage. 32 | * 33 | * @param int $samplePercentage The sample percentage (0-100). 34 | * @return self This builder instance for method chaining. 35 | */ 36 | public function samplePercentage(int $samplePercentage): self 37 | { 38 | if ($samplePercentage < 0 || $samplePercentage > 100) { 39 | throw new ValkeyGlideException("Sample percentage must be between 0 and 100"); 40 | } 41 | $this->samplePercentage = $samplePercentage; 42 | return $this; 43 | } 44 | 45 | /** 46 | * Gets the endpoint. 47 | * 48 | * @return string The traces endpoint URL. 49 | */ 50 | public function getEndpoint(): string 51 | { 52 | if ($this->endpoint === null) { 53 | throw new ValkeyGlideException("Traces endpoint is required when traces config is provided"); 54 | } 55 | return $this->endpoint; 56 | } 57 | 58 | /** 59 | * Gets the sample percentage. 60 | * 61 | * @return int The sample percentage (0-100). 62 | */ 63 | public function getSamplePercentage(): int 64 | { 65 | return $this->samplePercentage; 66 | } 67 | 68 | /** 69 | * Builds the TracesConfig. 70 | * 71 | * @return TracesConfig The immutable traces configuration. 72 | */ 73 | public function build(): TracesConfig 74 | { 75 | if ($this->endpoint === null) { 76 | throw new ValkeyGlideException("Traces endpoint is required when traces config is provided"); 77 | } 78 | 79 | return new TracesConfig($this); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.github/json_matrices/build-matrix.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "OS": "ubuntu", 4 | "NAMED_OS": "linux", 5 | "RUNNER": "ubuntu-24.04", 6 | "ARCH": "x64", 7 | "TARGET": "x86_64-unknown-linux-gnu", 8 | "PACKAGE_MANAGERS": ["pypi", "npm", "maven", "pkg_go_dev"], 9 | "run": "always", 10 | "languages": ["python", "node", "java", "go", "dotnet", "rust", "php"] 11 | }, 12 | { 13 | "OS": "ubuntu", 14 | "NAMED_OS": "linux", 15 | "RUNNER": ["self-hosted", "Linux", "ARM64", "ephemeral"], 16 | "CD_RUNNER": "ubuntu-24.04-arm", 17 | "ARCH": "arm64", 18 | "TARGET": "aarch64-unknown-linux-gnu", 19 | "PACKAGE_MANAGERS": ["pypi", "npm", "maven", "pkg_go_dev"], 20 | "languages": ["python", "node", "java", "go", "dotnet", "php"] 21 | }, 22 | { 23 | "OS": "macos", 24 | "NAMED_OS": "darwin", 25 | "RUNNER": "macos-15", 26 | "ARCH": "arm64", 27 | "TARGET": "aarch64-apple-darwin", 28 | "PACKAGE_MANAGERS": ["pypi", "npm", "maven", "pkg_go_dev"], 29 | "languages": ["python", "node", "java", "go", "dotnet", "php"] 30 | }, 31 | { 32 | "OS": "macos", 33 | "NAMED_OS": "darwin", 34 | "RUNNER": "macos-13", 35 | "CD_RUNNER": "macos-15", 36 | "ARCH": "x64", 37 | "TARGET": "x86_64-apple-darwin", 38 | "PACKAGE_MANAGERS": ["pypi", "npm", "maven", "pkg_go_dev"], 39 | "languages": [], 40 | "comment": "Tests for all clients (CI) are skipped for that platform due to instability, but CD pipelines still run on it. See PR #3482 for details." 41 | }, 42 | { 43 | "OS": "ubuntu", 44 | "NAMED_OS": "linux", 45 | "ARCH": "arm64", 46 | "TARGET": "aarch64-unknown-linux-musl", 47 | "RUNNER": ["self-hosted", "Linux", "ARM64", "ephemeral"], 48 | "CD_RUNNER": "ubuntu-24.04-arm", 49 | "IMAGE": "node:lts-alpine", 50 | "CONTAINER_OPTIONS": "--user root --privileged --rm", 51 | "PACKAGE_MANAGERS": ["npm"], 52 | "languages": [] 53 | }, 54 | { 55 | "OS": "ubuntu", 56 | "NAMED_OS": "linux", 57 | "ARCH": "x64", 58 | "TARGET": "x86_64-unknown-linux-musl", 59 | "RUNNER": "ubuntu-latest", 60 | "IMAGE": "node:lts-alpine", 61 | "CONTAINER_OPTIONS": "--user root --privileged", 62 | "PACKAGE_MANAGERS": ["npm"], 63 | "languages": ["node"] 64 | }, 65 | { 66 | "OS": "amazon-linux", 67 | "NAMED_OS": "linux", 68 | "RUNNER": "ubuntu-latest", 69 | "ARCH": "x64", 70 | "TARGET": "x86_64-unknown-linux-gnu", 71 | "IMAGE": "amazonlinux:latest", 72 | "PACKAGE_MANAGERS": [], 73 | "languages": ["python", "node", "java", "go", "dotnet"] 74 | } 75 | ] 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | 4 | debug/ 5 | target/ 6 | 7 | # Git stuff 8 | .worktrees 9 | 10 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 11 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 12 | Cargo.lock 13 | 14 | # These are backup files generated by rustfmt 15 | **/*.rs.bk 16 | 17 | # MSVC Windows builds of rustc generate these, which store debugging information 18 | *.pdb 19 | dump.rdb 20 | .env 21 | 22 | # Log files 23 | logs/ 24 | *.log 25 | *.log.* 26 | python/.env* 27 | benchmarks/results 28 | 29 | # IDE generaged files 30 | .vs 31 | .vscode 32 | .idea 33 | 34 | # MacOS metadata 35 | .DS_Store 36 | 37 | # lock files 38 | 39 | yarn.lock 40 | package-lock.json 41 | 42 | node_modules/ 43 | 44 | install_and_test_cp.sh 45 | 46 | glide-logs/ 47 | logger-rs.linux-x64-gnu.node 48 | utils/clusters/ 49 | utils/tls_crts/ 50 | utils/TestUtils.js 51 | .build/ 52 | .project 53 | 54 | # OSS Review Toolkit (ORT) files 55 | **/ort*/** 56 | **/ort_results/** 57 | 58 | # java compiled files. 59 | *.class 60 | 61 | # generaged files (e.g. protobuf) 62 | generated/ 63 | protoc-*.zip 64 | 65 | # docs 66 | docs/markdown/node/** 67 | docs/site/** 68 | copilot.yml 69 | memory-bank/ 70 | CLAUDE* 71 | #cpp files 72 | cpp/benchmarks/build/ 73 | cpp/build/ 74 | cpp/examples/build/ 75 | cpp/include/glide/connection_request.pb.h 76 | 77 | cpp/include/glide/glide_base.h 78 | cpp/src/connection_request.pb.cc 79 | 80 | **/.claude/settings.local.json 81 | copilot-instructions.md 82 | **/.yarn 83 | build/** 84 | coverage_html/** 85 | src/** 86 | include/** 87 | lib/** 88 | .libs/** 89 | tests/.libs/** 90 | *.dep 91 | *.lo 92 | *.loT 93 | *.o 94 | valkey_glide.la 95 | valkey_glide_arginfo.h 96 | valkey_glide_cluster_arginfo.h 97 | valkey_glide_cluster_legacy_arginfo.h 98 | valkey_glide_legacy_arginfo.h 99 | logger_arginfo.h 100 | logger_legacy_arginfo.h 101 | run-tests.php 102 | modules/valkey_glide.so 103 | libtool 104 | cluster_scan_cursor_legacy_arginfo.h 105 | cluster_scan_cursor_arginfo.h 106 | tests/valkey_data/*/valkey.log 107 | composer.lock 108 | configure 109 | configure.ac 110 | config.h.in 111 | config.log 112 | config.nice 113 | config.status 114 | Makefile 115 | Makefile.fragments 116 | Makefile.objects 117 | autom4te.cache 118 | config.h.in~ 119 | configure~ 120 | tests/Connection_request/** 121 | tests/GPBMetadata/** 122 | tests/client_constructor_mock_arginfo.h 123 | tests/client_constructor_mock_legacy_arginfo.h 124 | vendor/** 125 | # Dynamically generated submodule commit info 126 | .submodule-commits 127 | -------------------------------------------------------------------------------- /.github/workflows/setup-php-extension/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup PHP Extension Build Environment 2 | description: Common setup steps for building PHP extension 3 | 4 | inputs: 5 | php-version: 6 | description: PHP version to use 7 | required: true 8 | default: "8.1" 9 | install-composer-deps: 10 | description: Whether to install composer dependencies 11 | required: false 12 | default: "true" 13 | github-token: 14 | description: GitHub token for API access 15 | required: true 16 | engine-version: 17 | description: Engine version to install 18 | required: true 19 | 20 | runs: 21 | using: composite 22 | steps: 23 | - name: Install shared dependencies 24 | uses: ./.github/workflows/install-shared-dependencies 25 | with: 26 | target: x86_64-unknown-linux-gnu 27 | github-token: ${{ inputs.github-token }} 28 | engine-version: ${{ inputs.engine-version }} 29 | 30 | - name: Setup PHP 31 | uses: shivammathur/setup-php@v2 32 | with: 33 | php-version: ${{ inputs.php-version }} 34 | extensions: none 35 | tools: pie, jq 36 | 37 | - name: Install system dependencies 38 | shell: bash 39 | env: 40 | PHP_VERSION: ${{ inputs.php-version }} 41 | run: | 42 | sudo apt-get update 43 | sudo apt-get install -y \ 44 | "php${PHP_VERSION}-dev" \ 45 | "php${PHP_VERSION}-curl" \ 46 | "php${PHP_VERSION}-xml" \ 47 | build-essential \ 48 | autoconf \ 49 | automake \ 50 | libtool \ 51 | pkg-config \ 52 | libssl-dev \ 53 | clang-format \ 54 | protobuf-c-compiler \ 55 | libprotobuf-c-dev \ 56 | libprotobuf-c1 57 | 58 | - name: Install Rust and protoc 59 | uses: ./.github/workflows/install-rust-and-protoc 60 | with: 61 | github-token: ${{ inputs.github-token }} 62 | 63 | - name: Install cbindgen 64 | shell: bash 65 | run: | 66 | if [ -f "$HOME/.cargo/bin/cbindgen" ]; then 67 | echo "cbindgen already installed (cached)" 68 | else 69 | cargo install cbindgen 70 | fi 71 | 72 | - name: Install Composer dependencies 73 | if: inputs.install-composer-deps == 'true' 74 | shell: bash 75 | run: composer install --ignore-platform-reqs 76 | 77 | - name: Generate test protobuf PHP classes 78 | if: inputs.install-composer-deps == 'true' 79 | shell: bash 80 | run: | 81 | protoc --proto_path=./valkey-glide/glide-core/src/protobuf --php_out=./tests/ ./valkey-glide/glide-core/src/protobuf/connection_request.proto 82 | echo "tests directory contents:" 83 | cd tests 84 | ls -la 85 | -------------------------------------------------------------------------------- /.github/workflows/install-shared-dependencies/action.yml: -------------------------------------------------------------------------------- 1 | name: Install shared software dependencies 2 | 3 | inputs: 4 | os: 5 | description: "The current operating system" 6 | required: true 7 | type: string 8 | options: 9 | - amazon-linux 10 | - macos 11 | - ubuntu 12 | target: 13 | description: "Specified target for rust toolchain, ex. x86_64-apple-darwin" 14 | type: string 15 | required: false 16 | defalt: "x86_64-unknown-linux-gnu" 17 | options: 18 | - x86_64-unknown-linux-gnu 19 | - aarch64-unknown-linux-gnu 20 | - x86_64-apple-darwin 21 | - aarch64-apple-darwin 22 | - aarch64-unknown-linux-musl 23 | - x86_64-unknown-linux-musl 24 | engine-version: 25 | description: "Engine version to install" 26 | required: false 27 | type: string 28 | github-token: 29 | description: "GITHUB_TOKEN, GitHub App installation access token" 30 | required: true 31 | type: string 32 | 33 | runs: 34 | using: "composite" 35 | steps: 36 | - name: Install software dependencies for macOS 37 | shell: bash 38 | if: "${{ inputs.os == 'macos' }}" 39 | run: | 40 | brew update 41 | brew install openssl coreutils 42 | 43 | - name: Install software dependencies for Ubuntu GNU 44 | shell: bash 45 | if: "${{ inputs.os == 'ubuntu' && !contains(inputs.target, 'musl')}}" 46 | run: | 47 | sudo apt update -y 48 | sudo apt install -y git gcc pkg-config openssl libssl-dev 49 | 50 | - name: Install software dependencies for MUSL 51 | shell: bash 52 | if: "${{ contains(inputs.target, 'musl') }}" 53 | run: | 54 | apk update 55 | wget -O - https://sh.rustup.rs | sh -s -- -y 56 | source "$HOME/.cargo/env" 57 | apk add protobuf-dev musl-dev make gcc envsubst openssl libressl-dev 58 | 59 | - name: Install software dependencies for Amazon-Linux 60 | shell: bash 61 | if: "${{ inputs.os == 'amazon-linux' }}" 62 | run: | 63 | yum install -y gcc pkgconfig openssl openssl-devel which curl gettext libasan tar --allowerasing 64 | 65 | - name: Install Rust toolchain and protoc 66 | if: "${{ !contains(inputs.target, 'musl') }}" 67 | uses: ./.github/workflows/install-rust-and-protoc 68 | with: 69 | target: ${{ inputs.target }} 70 | github-token: ${{ inputs.github-token }} 71 | 72 | - name: Install engine 73 | if: "${{ inputs.engine-version }}" 74 | uses: ./.github/workflows/install-engine 75 | with: 76 | engine-version: ${{ inputs.engine-version }} 77 | target: ${{ inputs.target }} 78 | 79 | - name: Install zig 80 | if: ${{ contains(inputs.target, 'linux-gnu') }} 81 | uses: ./.github/workflows/install-zig 82 | -------------------------------------------------------------------------------- /OpenTelemetryConfigBuilder.php: -------------------------------------------------------------------------------- 1 | traces = $traces; 25 | return $this; 26 | } 27 | 28 | /** 29 | * Sets the metrics configuration. 30 | * 31 | * @param MetricsConfig|null $metrics The metrics configuration, or null to disable metrics. 32 | * @return self This builder instance for method chaining. 33 | */ 34 | public function metrics(?MetricsConfig $metrics): self 35 | { 36 | $this->metrics = $metrics; 37 | return $this; 38 | } 39 | 40 | /** 41 | * Sets the flush interval in milliseconds. 42 | * 43 | * @param int $flushIntervalMs The flush interval in milliseconds (must be positive). 44 | * @return self This builder instance for method chaining. 45 | */ 46 | public function flushIntervalMs(int $flushIntervalMs): self 47 | { 48 | if ($flushIntervalMs <= 0) { 49 | throw new ValkeyGlideException("Flush interval must be a positive integer"); 50 | } 51 | $this->flushIntervalMs = $flushIntervalMs; 52 | return $this; 53 | } 54 | 55 | /** 56 | * Gets the traces configuration. 57 | * 58 | * @return TracesConfig|null The traces configuration, or null if not configured. 59 | */ 60 | public function getTraces(): ?TracesConfig 61 | { 62 | return $this->traces; 63 | } 64 | 65 | /** 66 | * Gets the metrics configuration. 67 | * 68 | * @return MetricsConfig|null The metrics configuration, or null if not configured. 69 | */ 70 | public function getMetrics(): ?MetricsConfig 71 | { 72 | return $this->metrics; 73 | } 74 | 75 | /** 76 | * Gets the flush interval in milliseconds. 77 | * 78 | * @return int The flush interval in milliseconds. 79 | */ 80 | public function getFlushIntervalMs(): int 81 | { 82 | return $this->flushIntervalMs; 83 | } 84 | 85 | /** 86 | * Builds the OpenTelemetryConfig. 87 | * 88 | * @return OpenTelemetryConfig The immutable OpenTelemetry configuration. 89 | */ 90 | public function build(): OpenTelemetryConfig 91 | { 92 | if ($this->traces === null && $this->metrics === null) { 93 | throw new ValkeyGlideException("At least one of traces or metrics must be configured"); 94 | } 95 | 96 | return new OpenTelemetryConfig($this); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/create-valkey-cluster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR="$HOME/valkey-cluster" 4 | PORTS=(7001 7002 7003 7004 7005 7006) 5 | VALKEY_BIN=$(which valkey-server) 6 | CLI_BIN=$(which valkey-cli) 7 | 8 | if [ -z "$VALKEY_BIN" ] || [ -z "$CLI_BIN" ]; then 9 | echo "valkey-server or valkey-cli not found in PATH" 10 | exit 1 11 | fi 12 | 13 | # Check Valkey version for multi-database support 14 | VALKEY_VERSION=$("$VALKEY_BIN" --version | grep -o 'v=[0-9]\+\.[0-9]\+' | cut -d'=' -f2) 15 | MAJOR_VERSION=$(echo "$VALKEY_VERSION" | cut -d'.' -f1) 16 | 17 | echo "Detected Valkey version: $VALKEY_VERSION" 18 | 19 | # 1. Clean previous setup 20 | echo "Cleaning up old cluster data..." 21 | rm -rf "$BASE_DIR" 22 | mkdir -p "$BASE_DIR" 23 | 24 | # 2. Create config and data folders 25 | for port in "${PORTS[@]}"; do 26 | NODE_DIR="$BASE_DIR/$port" 27 | mkdir -p "$NODE_DIR" 28 | 29 | cat > "$NODE_DIR/valkey.conf" <> "$NODE_DIR/valkey.conf" 45 | echo "Added multi-database support for Valkey $VALKEY_VERSION" 46 | fi 47 | done 48 | 49 | # 3. Start each node 50 | echo "Starting Valkey nodes..." 51 | for port in "${PORTS[@]}"; do 52 | "$VALKEY_BIN" "$BASE_DIR/$port/valkey.conf" & 53 | sleep 0.2 54 | done 55 | 56 | sleep 2 57 | 58 | # 4. Create the cluster 59 | echo "Creating cluster..." 60 | "$CLI_BIN" --cluster create \ 61 | 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 \ 62 | 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 \ 63 | --cluster-replicas 1 \ 64 | --cluster-yes 65 | 66 | # 5. Use cluster_manager.py to create the TLS cluster 67 | echo "Setting up TLS cluster..." 68 | if ../valkey-glide/utils/cluster_manager.py --tls start --prefix tls-cluster --cluster-mode -p 8001 8002 8003 8004 8005 8006; then 69 | echo "✅ TLS cluster started on ports 8001-8006" 70 | else 71 | echo "⚠️ WARNING: TLS cluster setup failed (ports 8001-8006 may be in use), continuing without TLS cluster..." 72 | fi 73 | 74 | # 6. Test multi-database support if Valkey 9+ 75 | if [ "$MAJOR_VERSION" -ge 9 ]; then 76 | echo "Testing multi-database support in cluster..." 77 | if "$CLI_BIN" -c -h 127.0.0.1 -p 7001 SELECT 1 >/dev/null 2>&1; then 78 | echo "✅ Multi-database support confirmed (SELECT command works)" 79 | else 80 | echo "⚠️ Multi-database support not working (SELECT command failed)" 81 | fi 82 | fi 83 | 84 | echo "Cluster setup complete!" 85 | 86 | # 6. Use cluster_manager.py to create cluster with auth 87 | echo "Setting up auth cluster..." 88 | if ../valkey-glide/utils/cluster_manager.py --auth dummy_password start --prefix auth-cluster --cluster-mode -p 5001 5002 5003 5004 5005 5006; then 89 | echo "✅ Auth cluster started on ports 5001-5006" 90 | else 91 | echo "⚠️ WARNING: Auth cluster setup failed (ports 5001-5006 may be in use), continuing without auth cluster..." 92 | fi 93 | -------------------------------------------------------------------------------- /examples/otel_example.php: -------------------------------------------------------------------------------- 1 | traces( 19 | TracesConfig::builder() 20 | ->endpoint("file://{$tmpDir}/valkey_glide_traces.json") 21 | ->samplePercentage(10) 22 | ->build() 23 | ) 24 | ->metrics( 25 | MetricsConfig::builder() 26 | ->endpoint("file://{$tmpDir}/valkey_glide_metrics.json") 27 | ->build() 28 | ) 29 | ->flushIntervalMs(5000) 30 | ->build(); 31 | 32 | // Create ValkeyGlide client with configuration 33 | $client = new ValkeyGlide( 34 | addresses: [ 35 | ['host' => 'localhost', 'port' => 6379] 36 | ], 37 | use_tls: false, 38 | credentials: null, 39 | read_from: ValkeyGlide::READ_FROM_PRIMARY, 40 | request_timeout: null, 41 | reconnect_strategy: null, 42 | database_id: 0, 43 | client_name: 'otel-class-example-client', 44 | client_az: null, 45 | advanced_config: [ 46 | 'connection_timeout' => 5000, 47 | 'otel' => $otelConfig // Class-based configuration 48 | ] 49 | ); 50 | 51 | echo "ValkeyGlide client created with OpenTelemetry support" . PHP_EOL; 52 | echo "- Sample percentage: 10%" . PHP_EOL; 53 | echo "- Flush interval: 5000ms" . PHP_EOL; 54 | echo "- Traces endpoint: file://{$tmpDir}/valkey_glide_traces.json" . PHP_EOL; 55 | echo "- Metrics endpoint: file://{$tmpDir}/valkey_glide_metrics.json" . PHP_EOL . PHP_EOL; 56 | 57 | // Perform some operations that will be traced 58 | $client->set('otel:class:test:key1', 'value1'); 59 | echo "SET operation completed" . PHP_EOL; 60 | 61 | $value = $client->get('otel:class:test:key1'); 62 | echo "GET operation completed: $value" . PHP_EOL; 63 | 64 | $client->set('otel:class:test:key2', 'value2'); 65 | $client->set('otel:class:test:key3', 'value3'); 66 | 67 | // Batch operations will also be traced 68 | $results = $client->mget(['otel:class:test:key1', 'otel:class:test:key2', 'otel:class:test:key3']); 69 | echo "MGET operation completed: " . json_encode($results) . PHP_EOL; 70 | 71 | // Cleanup 72 | $client->del(['otel:class:test:key1', 'otel:class:test:key2', 'otel:class:test:key3']); 73 | echo "Cleanup completed" . PHP_EOL; 74 | 75 | $client->close(); 76 | echo "Client closed" . PHP_EOL . PHP_EOL; 77 | 78 | } catch (Exception $e) { 79 | echo "Error: " . $e->getMessage() . PHP_EOL; 80 | exit(1); 81 | } 82 | 83 | echo PHP_EOL . "OpenTelemetry example completed successfully!" . PHP_EOL; 84 | echo "Class-based configuration demonstrated." . PHP_EOL; 85 | echo "Check the following files for telemetry data:" . PHP_EOL; 86 | echo "- {$tmpDir}/valkey_glide_traces.json (traces)" . PHP_EOL; 87 | echo "- {$tmpDir}/valkey_glide_metrics.json (metrics)" . PHP_EOL; 88 | ?> 89 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. 16 | 17 | ## Contributing via Pull Requests 18 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 19 | 20 | 1. You are working against the latest source on the *main* branch. 21 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 22 | 3. You open an issue to discuss any significant changes before starting the work - we would hate for your time to be wasted. 23 | 24 | To send us a pull request, please: 25 | 26 | 1. Fork the repository. 27 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 28 | 3. Ensure local tests pass. 29 | 4. Commit to your fork using clear commit messages, merge or squash commits as necessary. 30 | 5. Send us a pull request, answering any default questions in the pull request interface. 31 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 32 | 33 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 34 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 35 | 36 | 37 | ## Finding contributions to work on 38 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 39 | 40 | ## Developer Guides 41 | - [Java](./java/DEVELOPER.md) 42 | - [Node](./node/DEVELOPER.md) 43 | - [Python](./python/DEVELOPER.md) 44 | 45 | ## Code of Conduct 46 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 47 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 48 | opensource-codeofconduct@amazon.com with any additional questions or comments. 49 | 50 | 51 | ## Security issue notifications 52 | See [SECURITY.md](./SECURITY.md) 53 | 54 | 55 | ## Licensing 56 | 57 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 58 | 59 | ## Community Support and Feedback 60 | 61 | We encourage you to join our community to support, share feedback, and ask questions. You can approach us for anything on our Valkey Slack: [Join Valkey Slack](https://join.slack.com/t/valkey-oss-developer/shared_invite/zt-2nxs51chx-EB9hu9Qdch3GMfRcztTSkQ). 62 | -------------------------------------------------------------------------------- /.ort.yml: -------------------------------------------------------------------------------- 1 | # ORT configuration file for valkey-glide-php project 2 | # This file configures the OSS Review Toolkit to properly analyze only the PHP dependencies 3 | # and exclude build tools, test files, and other non-relevant directories. 4 | 5 | analyzer: 6 | skip_excluded: true 7 | 8 | excludes: 9 | scopes: 10 | - pattern: "//indirect" 11 | reason: "DEV_DEPENDENCY_OF" 12 | comment: "Indirect dependencies used for development only." 13 | - pattern: "github.com/stretchr/testify|github.com/google/uuid" 14 | reason: "TEST_DEPENDENCY_OF" 15 | comment: "Testing framework not included in production code." 16 | - pattern: "require-dev" 17 | reason: "DEV_DEPENDENCY_OF" 18 | comment: "Packages for development only." 19 | paths: 20 | # ORT tool directory - exclude the entire ORT installation 21 | - pattern: "ort/**" 22 | reason: "BUILD_TOOL_OF" 23 | comment: "ORT tool directory with test files, not part of PHP project" 24 | 25 | # ORT test files and synthetic projects 26 | - pattern: "**/funTest/**" 27 | reason: "BUILD_TOOL_OF" 28 | comment: "ORT functional test files should be excluded" 29 | 30 | - pattern: "**/src/funTest/**" 31 | reason: "BUILD_TOOL_OF" 32 | comment: "ORT functional test source files should be excluded" 33 | 34 | - pattern: "ort/plugins/package-managers/composer/src/funTest/**" 35 | reason: "BUILD_TOOL_OF" 36 | comment: "ORT Composer test fixtures should be excluded" 37 | 38 | - pattern: "**/synthetic/**" 39 | reason: "BUILD_TOOL_OF" 40 | comment: "ORT synthetic test projects should be excluded" 41 | 42 | # Submodule exclusions 43 | - pattern: "valkey-glide/**" 44 | reason: "DOCUMENTATION_OF" 45 | comment: "Submodule not part of PHP project" 46 | 47 | # Language-specific exclusions 48 | - pattern: "**/cpp/**" 49 | reason: "BUILD_TOOL_OF" 50 | comment: "C++ directories should not be analyzed in PHP context" 51 | 52 | - pattern: "**/Cargo.toml" 53 | reason: "BUILD_TOOL_OF" 54 | comment: "Rust manifests not relevant for PHP analysis" 55 | 56 | # Build artifacts and cache directories 57 | - pattern: "build/**" 58 | reason: "BUILD_TOOL_OF" 59 | comment: "Build artifacts" 60 | 61 | - pattern: "modules/**" 62 | reason: "BUILD_TOOL_OF" 63 | comment: "Compiled PHP modules" 64 | 65 | - pattern: "autom4te.cache/**" 66 | reason: "BUILD_TOOL_OF" 67 | comment: "Autotools cache" 68 | 69 | # Compiled files 70 | - pattern: "**/*.o" 71 | reason: "BUILD_TOOL_OF" 72 | comment: "Object files" 73 | 74 | - pattern: "**/*.lo" 75 | reason: "BUILD_TOOL_OF" 76 | comment: "Libtool object files" 77 | 78 | - pattern: "**/*.la" 79 | reason: "BUILD_TOOL_OF" 80 | comment: "Libtool archive files" 81 | 82 | - pattern: "**/*.dep" 83 | reason: "BUILD_TOOL_OF" 84 | comment: "Dependency files" 85 | 86 | # Additional common exclusions for robustness 87 | - pattern: ".git/**" 88 | reason: "BUILD_TOOL_OF" 89 | comment: "Git repository metadata" 90 | 91 | - pattern: ".github/**" 92 | reason: "BUILD_TOOL_OF" 93 | comment: "GitHub workflow and configuration files" 94 | 95 | - pattern: "vendor/**" 96 | reason: "BUILD_TOOL_OF" 97 | comment: "PHP vendor directory managed by Composer" 98 | 99 | - pattern: "**/node_modules/**" 100 | reason: "BUILD_TOOL_OF" 101 | comment: "Node.js dependencies" 102 | 103 | - pattern: "**/.gradle/**" 104 | reason: "BUILD_TOOL_OF" 105 | comment: "Gradle cache directories" 106 | 107 | - pattern: "**/target/**" 108 | reason: "BUILD_TOOL_OF" 109 | comment: "Build target directories" 110 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/flaky-ci-test-issue.yml: -------------------------------------------------------------------------------- 1 | name: Flaky CI Test Issue 2 | description: Report a flaky test in the CI pipeline 3 | title: "[Flaky Test] " 4 | labels: ["bug", "flaky-test"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | ## Description of the Flaky Test 12 | 13 | - type: input 14 | id: test-name 15 | attributes: 16 | label: Test Name 17 | description: Name of the test that is flaky 18 | placeholder: e.g., test_example 19 | 20 | - type: input 21 | id: test-location 22 | attributes: 23 | label: Test Location 24 | description: File and line number or test suite 25 | placeholder: e.g., test_suite.py line 42 26 | 27 | - type: input 28 | id: failure-permlink 29 | attributes: 30 | label: Failure Permlink 31 | description: Permlink to the failure line in the test run 32 | placeholder: e.g., https://ci.example.com/build/123 33 | 34 | - type: input 35 | id: frequency 36 | attributes: 37 | label: Frequency 38 | description: How often does the test fail? 39 | placeholder: e.g., 1 in 10 runs 40 | 41 | - type: markdown 42 | attributes: 43 | value: | 44 | ## Steps to Reproduce 45 | 46 | - type: textarea 47 | id: steps-to-reproduce 48 | attributes: 49 | label: Steps to Reproduce 50 | description: List the steps required to reproduce the flaky test 51 | placeholder: 1. Step 1 52 | 2. Step 2 53 | 3. Step 3 54 | 55 | - type: markdown 56 | attributes: 57 | value: | 58 | ## Additional Context 59 | 60 | - type: input 61 | id: system-information 62 | attributes: 63 | label: System Information 64 | description: Operating system, CI environment, etc. 65 | placeholder: e.g., Ubuntu 20.04, GitHub Actions 66 | 67 | - type: input 68 | id: language-and-version 69 | attributes: 70 | label: Language and Version 71 | description: Programming language and its version 72 | placeholder: e.g., Python 3.8 73 | 74 | - type: input 75 | id: engine-version 76 | attributes: 77 | label: Engine Version 78 | description: Engine version used 79 | placeholder: e.g., v6.2 80 | 81 | - type: textarea 82 | id: logs 83 | attributes: 84 | label: Logs 85 | description: Include any relevant logs or error messages 86 | placeholder: Paste logs here... 87 | 88 | - type: textarea 89 | id: screenshots 90 | attributes: 91 | label: Screenshots 92 | description: If applicable, add screenshots to help explain the issue 93 | placeholder: Paste screenshots here... 94 | 95 | - type: input 96 | id: glide-version 97 | attributes: 98 | label: Glide Version 99 | description: Glide version used 100 | placeholder: e.g., 1.2.3 101 | 102 | - type: markdown 103 | attributes: 104 | value: | 105 | ## Expected Behavior 106 | 107 | - type: textarea 108 | id: expected-behavior 109 | attributes: 110 | label: Expected Behavior 111 | description: Describe what you expected to happen 112 | placeholder: Describe the expected behavior... 113 | 114 | - type: markdown 115 | attributes: 116 | value: | 117 | ## Actual Behavior 118 | 119 | - type: textarea 120 | id: actual-behavior 121 | attributes: 122 | label: Actual Behavior 123 | description: Describe what actually happened 124 | placeholder: Describe the actual behavior... 125 | 126 | - type: markdown 127 | attributes: 128 | value: | 129 | ## Possible Fixes 130 | 131 | - type: textarea 132 | id: possible-fixes 133 | attributes: 134 | label: Possible Fixes 135 | description: If you have any insight into what might be causing the flakiness, mention it here 136 | placeholder: Describe possible fixes... 137 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": true, 3 | "editor.tabSize": 4, 4 | "editor.codeActionsOnSave": { 5 | "source.organizeImports": "always" 6 | }, 7 | "rust-analyzer.procMacro.ignored": { 8 | "napi-derive": [ 9 | "napi" 10 | ] 11 | }, 12 | "editor.formatOnSave": true, 13 | "files.insertFinalNewline": true, 14 | "files.trimFinalNewlines": true, 15 | "python.formatting.provider": "black", 16 | "rust-analyzer.linkedProjects": [ 17 | "glide-core/Cargo.toml", 18 | "python/Cargo.toml", 19 | "node/rust-client/Cargo.toml", 20 | "logger_core/Cargo.toml", 21 | "csharp/rust/Cargo.toml", 22 | "glide-core/redis-rs/Cargo.toml", 23 | "benchmarks/rust/Cargo.toml", 24 | "java/Cargo.toml", 25 | "ffi/Cargo.toml", 26 | ], 27 | "rust-analyzer.runnableEnv": { 28 | "REDISRS_SERVER_TYPE": "tcp" 29 | }, 30 | "python.testing.pytestArgs": [ 31 | "python" 32 | ], 33 | "python.testing.unittestEnabled": false, 34 | "python.testing.pytestEnabled": true, 35 | "[github-actions-workflow]": { 36 | "editor.defaultFormatter": "esbenp.prettier-vscode" 37 | }, 38 | "yaml.schemas": { 39 | "https://json.schemastore.org/github-workflow.json": [ 40 | "*.github/workflows/*.yaml", 41 | "*.github/workflows/*.yml", 42 | ], 43 | "https://json.schemastore.org/github-action.json": [ 44 | "action.yaml", 45 | "action.yml", 46 | ] 47 | }, 48 | "[yaml]": { 49 | "editor.tabSize": 4, 50 | "editor.defaultFormatter": "redhat.vscode-yaml" 51 | }, 52 | "python.linting.flake8Enabled": false, 53 | "python.linting.enabled": true, 54 | "python.linting.flake8Args": [ 55 | "--extend-ignore=E203", 56 | "--max-line-length=127" 57 | ], 58 | "python.formatting.blackArgs": [ 59 | "--target-version", 60 | "py36" 61 | ], 62 | "isort.args": [ 63 | "--profile", 64 | "black" 65 | ], 66 | "rust-analyzer.cargo.features": "all", 67 | "rust-analyzer.procMacro.enable": true, 68 | "rust-analyzer.cargo.buildScripts.enable": true, 69 | "dotnet.defaultSolution": "csharp/csharp.sln", 70 | "java.compile.nullAnalysis.mode": "automatic", 71 | "java.configuration.updateBuildConfiguration": "interactive", 72 | "files.associations": { 73 | "**/frontmatter.json": "jsonc", 74 | "**/.frontmatter/config/*.json": "jsonc", 75 | "iosfwd": "cpp", 76 | "__node_handle": "c", 77 | "valkey_glide_cluster_arginfo.h": "c", 78 | "type_traits": "c", 79 | "bitset": "c", 80 | "map": "c", 81 | "unordered_map": "c", 82 | "__bit_reference": "c", 83 | "chrono": "c", 84 | "algorithm": "c", 85 | "format": "c", 86 | "deque": "c", 87 | "limits": "c", 88 | "locale": "c", 89 | "optional": "c", 90 | "ratio": "c", 91 | "vector": "c", 92 | "atomic": "c", 93 | "cstddef": "c", 94 | "__memory": "c", 95 | "random": "c", 96 | "system_error": "c", 97 | "tuple": "c", 98 | "charconv": "c", 99 | "array": "c", 100 | "functional": "c", 101 | "utility": "c", 102 | "valkey_glide_commands_common.h": "c", 103 | "common.h": "c", 104 | "command_response.h": "c", 105 | "valkey_glide_hash_common.h": "c", 106 | "php_var.h": "c", 107 | "valkey_glide_z_common.h": "c", 108 | "string": "c" 109 | }, 110 | "php.validate.enable": true, 111 | "php.validate.executablePath": "/usr/bin/php", 112 | "phpcs.enable": true, 113 | "phpcs.executablePath": "/usr/local/bin/phpcs", 114 | "phpcs.standard": "PSR12", 115 | "php-cs-fixer.executablePath": "/usr/local/bin/php-cs-fixer", 116 | "php-cs-fixer.onsave": true 117 | } 118 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | description: Report a bug 4 | title: "(topic): (short issue description)" 5 | labels: [bug, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the bug 12 | description: What is the problem? A clear and concise description of the bug. 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | id: expected 18 | attributes: 19 | label: Expected Behavior 20 | description: | 21 | What did you expect to happen? 22 | validations: 23 | required: true 24 | 25 | - type: textarea 26 | id: current 27 | attributes: 28 | label: Current Behavior 29 | description: | 30 | What actually happened? 31 | 32 | Please include full errors, uncaught exceptions, stack traces, and relevant logs. 33 | If service responses are relevant, please include wire logs. 34 | validations: 35 | required: true 36 | 37 | - type: textarea 38 | id: reproduction 39 | attributes: 40 | label: Reproduction Steps 41 | description: | 42 | Provide a self-contained, concise snippet of code that can be used to reproduce the issue. 43 | For more complex issues provide a repo with the smallest sample that reproduces the bug. 44 | 45 | Avoid including business logic or unrelated code, it makes diagnosis more difficult. 46 | The code sample should be an SSCCE. See http://sscce.org/ for details. In short, please provide a code sample that we can copy/paste, run and reproduce. 47 | validations: 48 | required: true 49 | 50 | - type: textarea 51 | id: solution 52 | attributes: 53 | label: Possible Solution 54 | description: | 55 | Suggest a fix/reason for the bug 56 | validations: 57 | required: false 58 | 59 | - type: textarea 60 | id: context 61 | attributes: 62 | label: Additional Information/Context 63 | description: | 64 | Anything else that might be relevant for troubleshooting this bug. Providing context helps us come up with a solution that is most useful in the real world. 65 | validations: 66 | required: false 67 | 68 | - type: input 69 | id: client-version 70 | attributes: 71 | label: Client version used 72 | validations: 73 | required: true 74 | 75 | - type: input 76 | id: engine-version 77 | attributes: 78 | label: Engine type and version 79 | description: E.g. Valkey 7.0 80 | validations: 81 | required: true 82 | 83 | - type: input 84 | id: operating-system 85 | attributes: 86 | label: OS 87 | validations: 88 | required: true 89 | 90 | - type: dropdown 91 | id: language 92 | attributes: 93 | label: Language 94 | multiple: true 95 | options: 96 | - TypeScript 97 | - Python 98 | - Java 99 | - Rust 100 | - Go 101 | - .Net 102 | validations: 103 | required: true 104 | 105 | - type: input 106 | id: language-version 107 | attributes: 108 | label: Language Version 109 | description: E.g. TypeScript (5.2.2) | Python (3.9) 110 | validations: 111 | required: true 112 | 113 | - type: textarea 114 | id: cluster-info 115 | attributes: 116 | label: Cluster information 117 | description: | 118 | Cluster information, cluster topology, number of shards, number of replicas, used data types. 119 | validations: 120 | required: false 121 | 122 | - type: textarea 123 | id: logs 124 | attributes: 125 | label: Logs 126 | description: | 127 | Client and/or server logs. 128 | validations: 129 | required: false 130 | 131 | - type: textarea 132 | id: other 133 | attributes: 134 | label: Other information 135 | description: | 136 | e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. associated pull-request, stackoverflow, etc 137 | validations: 138 | required: false 139 | -------------------------------------------------------------------------------- /.github/workflows/setup-valkey-glide/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup ValkeyGlide with Caching 2 | description: Common setup for ValkeyGlide builds with aggressive caching 3 | inputs: 4 | php-version: 5 | description: PHP version to use 6 | required: true 7 | engine-version: 8 | description: Engine version 9 | required: true 10 | github-token: 11 | description: GitHub token for API access 12 | required: true 13 | 14 | runs: 15 | using: composite 16 | steps: 17 | - name: Debug setup-valkey-glide inputs 18 | shell: bash 19 | env: 20 | PHP_VERSION: ${{ inputs.php-version }} 21 | ENGINE_VERSION: ${{ inputs.engine-version }} 22 | run: | 23 | echo "=== Setup ValkeyGlide Debug ===" 24 | echo "php-version: $PHP_VERSION" 25 | echo "engine-version: $ENGINE_VERSION" 26 | echo "github-token: [REDACTED]" 27 | 28 | - name: Cache APT packages 29 | uses: actions/cache@v4 30 | with: 31 | path: | 32 | /var/cache/apt/archives 33 | /var/lib/apt/lists 34 | key: apt-${{ runner.os }}-${{ hashFiles('.github/workflows/apt-packages.txt') }} 35 | restore-keys: | 36 | apt-${{ runner.os }}- 37 | 38 | - name: Install system dependencies (cached) 39 | shell: bash 40 | env: 41 | PHP_VERSION: ${{ inputs.php-version }} 42 | run: | 43 | # Skip update if cache hit and recent 44 | if [ ! -f "$HOME/.apt-cache-updated" ] || [ $(find "$HOME/.apt-cache-updated" -mmin +60 2>/dev/null) ]; then 45 | echo "Updating APT cache..." 46 | 47 | # Add PHP repository for older PHP versions on Ubuntu 24.04 48 | if [ "$PHP_VERSION" != "8.3" ]; then 49 | echo "Adding PHP repository for PHP $PHP_VERSION..." 50 | sudo add-apt-repository ppa:ondrej/php -y 51 | fi 52 | 53 | sudo apt-get update 54 | touch "$HOME/.apt-cache-updated" 55 | else 56 | echo "Using cached APT packages" 57 | fi 58 | 59 | sudo apt-get install -y --no-install-recommends \ 60 | build-essential \ 61 | "php${PHP_VERSION}-dev" \ 62 | protobuf-compiler \ 63 | protobuf-c-compiler \ 64 | libprotobuf-c-dev \ 65 | libprotobuf-c1 \ 66 | pkg-config \ 67 | libssl-dev 68 | 69 | - name: Cache Cargo dependencies 70 | uses: actions/cache@v4 71 | with: 72 | path: | 73 | ~/.cargo/registry/index/ 74 | ~/.cargo/registry/cache/ 75 | ~/.cargo/git/db/ 76 | valkey-glide/ffi/target/ 77 | valkey-glide/glide-core/target/ 78 | key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} 79 | restore-keys: | 80 | cargo-${{ runner.os }}- 81 | 82 | - name: Configure Cargo for speed 83 | shell: bash 84 | run: | 85 | mkdir -p ~/.cargo 86 | cat >> ~/.cargo/config.toml << EOF 87 | [registries.crates-io] 88 | protocol = "sparse" 89 | 90 | [build] 91 | jobs = 4 92 | 93 | [profile.release] 94 | codegen-units = 1 95 | lto = true 96 | EOF 97 | 98 | - name: Install Rust and protoc 99 | uses: ./.github/workflows/install-rust-and-protoc 100 | with: 101 | github-token: ${{ inputs.github-token }} 102 | 103 | - name: Install Valkey engine 104 | uses: ./.github/workflows/install-engine 105 | with: 106 | engine-version: ${{ inputs.engine-version }} 107 | target: x86_64-unknown-linux-gnu 108 | 109 | - name: Install zig 110 | uses: ./.github/workflows/install-zig 111 | 112 | - name: Install cbindgen (cached) 113 | shell: bash 114 | run: | 115 | # Check if cbindgen binary exists in cargo bin directory 116 | if [ -f "$HOME/.cargo/bin/cbindgen" ]; then 117 | echo "cbindgen already installed (cached)" 118 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 119 | else 120 | echo "Installing cbindgen..." 121 | cargo install cbindgen 122 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 123 | fi 124 | -------------------------------------------------------------------------------- /logger.stub.php: -------------------------------------------------------------------------------- 1 | > "$GITHUB_OUTPUT" 32 | echo "LOCAL_PATH=`basename $FOLDER`" >> "$GITHUB_OUTPUT" 33 | 34 | echo "Relative path calculated: $RELATIVE_PATH" 35 | echo "Local path: `basename $FOLDER`" 36 | 37 | # Check if source folder exists and list contents 38 | echo "Checking source folder: $FOLDER" 39 | if [ -d "$FOLDER" ]; then 40 | echo "Source folder exists" 41 | echo "Contents of source folder:" 42 | ls -la "$FOLDER" 43 | else 44 | echo "ERROR: Source folder does not exist!" 45 | exit 1 46 | fi 47 | 48 | # Create output directory 49 | echo "Creating output directory: $FOLDER/ort_results" 50 | mkdir -p "$FOLDER/ort_results" 51 | 52 | # Verify output directory was created 53 | if [ -d "$FOLDER/ort_results" ]; then 54 | echo "Output directory created successfully" 55 | else 56 | echo "ERROR: Failed to create output directory" 57 | exit 1 58 | fi 59 | 60 | # Run analyzer 61 | echo "Running ORT analyzer..." 62 | set -e # Exit on any error 63 | # Set ORT config directory and copy the specific config file if needed 64 | export ORT_CONFIG_DIR=~/.ort/config 65 | if [ "$CONFIG_FILE" != "config.yml" ]; then 66 | echo "Using custom config file: $CONFIG_FILE" 67 | cp ~/.ort/config/$CONFIG_FILE ~/.ort/config/config.yml 68 | else 69 | echo "Using default config file: config.yml" 70 | fi 71 | echo "Final config file content:" 72 | cat ~/.ort/config/config.yml 73 | echo "Running ORT command: ./gradlew cli:run --args=\"--info analyze -i $FOLDER -o $FOLDER/ort_results -f JSON\"" 74 | ./gradlew cli:run --args="--info analyze -i $FOLDER -o $FOLDER/ort_results -f JSON" 2>&1 | tee ort_analyzer.log || { 75 | echo "ORT analyzer failed. Last 50 lines of output:" 76 | tail -50 ort_analyzer.log 77 | echo "First 50 lines of output:" 78 | head -50 ort_analyzer.log 79 | exit 1 80 | } 81 | ANALYZER_EXIT_CODE=$? 82 | echo "Analyzer exit code: $ANALYZER_EXIT_CODE" 83 | 84 | # Check if analyzer produced output 85 | echo "Checking analyzer output..." 86 | if [ -f "$FOLDER/ort_results/analyzer-result.json" ]; then 87 | echo "Analyzer result file exists" 88 | echo "Size: $(stat -c%s "$FOLDER/ort_results/analyzer-result.json") bytes" 89 | else 90 | echo "ERROR: Analyzer result file not found!" 91 | echo "Contents of output directory:" 92 | ls -la "$FOLDER/ort_results/" || echo "Output directory is empty or doesn't exist" 93 | exit 1 94 | fi 95 | 96 | # Run reporter 97 | echo "Running ORT reporter..." 98 | ./gradlew cli:run --args="--info report -i $FOLDER/ort_results/analyzer-result.json -o $FOLDER/ort_results/ -f PlainTextTemplate" 99 | REPORTER_EXIT_CODE=$? 100 | echo "Reporter exit code: $REPORTER_EXIT_CODE" 101 | 102 | # List final output 103 | echo "Final contents of output directory:" 104 | ls -la "$FOLDER/ort_results/" 105 | 106 | # Verify from workspace root 107 | echo "Verifying files from workspace root:" 108 | cd "$GITHUB_WORKSPACE" 109 | ls -la "$RELATIVE_PATH/ort_results/" || echo "No files found at $RELATIVE_PATH/ort_results/" 110 | 111 | - name: Upload ORT results 112 | if: always() 113 | continue-on-error: true 114 | uses: actions/upload-artifact@v4 115 | with: 116 | name: ort_results-${{ steps.ort.outputs.LOCAL_PATH }} 117 | path: | 118 | ${{ steps.ort.outputs.RELATIVE_PATH }}/ort_results/** 119 | -------------------------------------------------------------------------------- /command_response.h: -------------------------------------------------------------------------------- 1 | /* 2 | +----------------------------------------------------------------------+ 3 | | Copyright (c) 2023-2025 The PHP Group | 4 | +----------------------------------------------------------------------+ 5 | | This source file is subject to version 3.01 of the PHP license, | 6 | | that is bundled with this package in the file LICENSE, and is | 7 | | available through the world-wide-web at the following url: | 8 | | http://www.php.net/license/3_01.txt | 9 | | If you did not receive a copy of the PHP license and are unable to | 10 | | obtain it through the world-wide-web, please send a note to | 11 | | license@php.net so we can mail you a copy immediately. | 12 | +----------------------------------------------------------------------+ 13 | */ 14 | 15 | #ifndef COMMAND_RESPONSE_H 16 | #define COMMAND_RESPONSE_H 17 | 18 | #include 19 | #include 20 | #include 21 | 22 | #include "common.h" 23 | #include "include/glide_bindings.h" 24 | #include "php.h" 25 | #include "zend.h" 26 | #include "zend_API.h" 27 | 28 | enum CommandResponseToZvalFlags { 29 | COMMAND_RESPONSE_NOT_ASSOSIATIVE = 0, 30 | COMMAND_RESPONSE_ASSOSIATIVE_ARRAY_MAP = 1, // Use associative array format for Map elements 31 | COMMAND_RESPONSE_ARRAY_ASSOCIATIVE = 2, // Use associative array format for stream entries 32 | COMMAND_RESPONSE_SCAN_ASSOSIATIVE_ARRAY = 3, 33 | COMMAND_RESPONSE_ASSOSIATIVE_ARRAY_MAP_FUNCTION = 34 | 4 // Use associative array format for FUNCTION command responses 35 | }; 36 | /* 37 | * Execute a command and handle common error checking 38 | * Returns NULL if there was an error, otherwise returns the CommandResult 39 | * The caller is responsible for freeing the CommandResult using free_command_result() 40 | */ 41 | CommandResult* execute_command(const void* glide_client, 42 | enum RequestType command_type, 43 | unsigned long arg_count, 44 | const uintptr_t* args, 45 | const unsigned long* args_len); 46 | 47 | CommandResult* execute_command_with_route(const void* glide_client, 48 | enum RequestType command_type, 49 | unsigned long arg_count, 50 | const uintptr_t* args, 51 | const unsigned long* args_len, 52 | zval* arg_route); 53 | 54 | 55 | /* 56 | * Handle a string response 57 | * Returns 1 on success, 0 if the key doesn't exist, -1 on error 58 | * The output and output_len parameters are set to the string value and length 59 | * The caller is responsible for freeing the output string using efree() 60 | * This function frees the CommandResult 61 | */ 62 | int handle_string_response(CommandResult* result, char** output, size_t* output_len); 63 | 64 | /* 65 | * Handle a map response 66 | * Returns 1 on success, 0 if null, -1 on error 67 | * The output parameter is set to a PHP associative array 68 | * This function frees the CommandResult 69 | */ 70 | int handle_map_response(CommandResult* result, zval* output); 71 | 72 | /* 73 | * Helper function to convert a CommandResponse to a PHP value 74 | * Returns 1 on success, 0 if null, -1 on error 75 | * The output parameter is set to the PHP value 76 | * use_associative_array: 77 | * - 0: regular array processing 78 | * - 1: convert Map elements to associative array format (for ZMPOP/sorted sets) 79 | */ 80 | int command_response_to_zval(CommandResponse* response, 81 | zval* output, 82 | int use_associative_array, 83 | bool use_false_if_null); 84 | 85 | /* 86 | * Helper function to convert a long value to a string 87 | * Returns a newly allocated string or NULL on error 88 | * The caller is responsible for freeing the string using efree() 89 | */ 90 | char* long_to_string(long value, size_t* len); 91 | 92 | /* 93 | * Helper function to convert a double value to a string 94 | * Returns a newly allocated string or NULL on error 95 | * The caller is responsible for freeing the string using efree() 96 | */ 97 | char* double_to_string(double value, size_t* len); 98 | 99 | /* 100 | * Helper function to convert a CommandResponse to a PHP stream format 101 | * This is specifically for XRANGE/XREVRANGE commands that return stream entries 102 | * Returns 1 on success, 0 if null, -1 on error 103 | * The output parameter is set to a PHP associative array with stream IDs as keys 104 | */ 105 | int command_response_to_stream_zval(CommandResponse* response, zval* output); 106 | 107 | /* Utility functions */ 108 | /** 109 | * Safe zval to string conversion with memory management 110 | * Returns allocated string that must be freed, or NULL on error 111 | * Sets need_free to 1 if returned string must be freed 112 | */ 113 | 114 | char* zval_to_string_safe(zval* z, size_t* len, int* need_free); 115 | void free_allocated_strings(char** strings, int count); 116 | 117 | #endif /* COMMAND_RESPONSE_H */ 118 | -------------------------------------------------------------------------------- /.github/workflows/create-test-matrices/action.yml: -------------------------------------------------------------------------------- 1 | inputs: 2 | language-name: 3 | description: "Language name" 4 | required: true 5 | type: choice 6 | options: 7 | - rust 8 | - java 9 | - node 10 | - python 11 | - go 12 | - C# 13 | run-full-matrix: 14 | description: "Run the full matrix" 15 | required: true 16 | type: boolean 17 | containers: 18 | description: "Run in containers" 19 | required: true 20 | default: false 21 | type: boolean 22 | 23 | outputs: 24 | engine-matrix-output: 25 | description: "Engine matrix" 26 | value: ${{ steps.load-engine-matrix.outputs.engine-matrix }} 27 | host-matrix-output: 28 | description: "Host matrix" 29 | value: ${{ steps.load-host-matrix.outputs.host-matrix }} 30 | version-matrix-output: 31 | description: "Version matrix" 32 | value: ${{ steps.create-lang-version-matrix.outputs.version-matrix }} 33 | 34 | runs: 35 | using: "composite" 36 | steps: 37 | - name: "Setup Environment Variables" 38 | shell: bash 39 | env: 40 | EVENT_NAME: ${{ github.event_name }} 41 | RUN_FULL_MATRIX: ${{ inputs.run-full-matrix }} 42 | CONTAINERS: ${{ inputs.containers }} 43 | LANGUAGE_NAME: ${{ inputs.language-name }} 44 | run: | 45 | echo "EVENT_NAME=$EVENT_NAME" >> $GITHUB_ENV 46 | echo "RUN_FULL_MATRIX=$RUN_FULL_MATRIX" >> $GITHUB_ENV 47 | echo "CONTAINERS=$CONTAINERS" >> $GITHUB_ENV 48 | echo "LANGUAGE_NAME=$LANGUAGE_NAME" >> $GITHUB_ENV 49 | 50 | - name: Load engine matrix 51 | id: load-engine-matrix 52 | shell: bash 53 | run: | 54 | set -o pipefail 55 | echo 'Select server engines to run tests against' 56 | if [[ "$EVENT_NAME" == "pull_request" || "$EVENT_NAME" == "push" || "$RUN_FULL_MATRIX" == "false" ]]; then 57 | echo 'Pick engines marked as `"run": "always"` only - on PR, push or manually triggered job which does not require full matrix' 58 | jq -c '[.[] | select(.run == "always")]' < .github/json_matrices/engine-matrix.json | awk '{ printf "engine-matrix=%s\n", $0 }' | tee -a $GITHUB_OUTPUT 59 | else 60 | echo 'Pick all engines - on cron (schedule) or if manually triggered job requires a full matrix' 61 | jq -c . < .github/json_matrices/engine-matrix.json | awk '{ printf "engine-matrix=%s\n", $0 }' | tee -a $GITHUB_OUTPUT 62 | fi 63 | 64 | - name: Load host matrix 65 | id: load-host-matrix 66 | shell: bash 67 | run: | 68 | set -o pipefail 69 | [[ "$CONTAINERS" == "true" ]] && CONDITION=".IMAGE?" || CONDITION=".IMAGE == null" 70 | echo 'Select runners (VMs) to run tests on' 71 | if [[ "$EVENT_NAME" == "pull_request" || "$EVENT_NAME" == "push" || "$RUN_FULL_MATRIX" == "false" ]]; then 72 | echo 'Pick runners marked as '"run": "always"' only - on PR, push or manually triggered job which does not require full matrix' 73 | jq -c '[.[] | select(.run == "always")]' < .github/json_matrices/build-matrix.json | awk '{ printf "host-matrix=%s\n", $0 }' | tee -a $GITHUB_OUTPUT 74 | else 75 | echo 'Pick all runners assigned for the chosen client (language) - on cron (schedule) or if manually triggered job requires a full matrix' 76 | jq -c "[.[] | select(.languages? and any(.languages[] == \"$LANGUAGE_NAME\"; .) and $CONDITION)]" < .github/json_matrices/build-matrix.json | awk '{ printf "host-matrix=%s\n", $0 }' | tee -a $GITHUB_OUTPUT 77 | fi 78 | 79 | - name: Create language version matrix 80 | id: create-lang-version-matrix 81 | shell: bash 82 | run: | 83 | set -o pipefail 84 | echo 'Select language (framework/SDK) versions to run tests on' 85 | if [[ "$EVENT_NAME" == "pull_request" || "$EVENT_NAME" == "push" || "$RUN_FULL_MATRIX" == "false" ]]; then 86 | echo 'Pick language versions listed in 'always-run-versions' only - on PR, push or manually triggered job which does not require full matrix' 87 | jq -c "[.[] | select(.language == \"$LANGUAGE_NAME\") | .[\"always-run-versions\"]][0] // []" < .github/json_matrices/supported-languages-versions.json | awk '{ printf "version-matrix=%s\n", $0 }' | tee -a $GITHUB_OUTPUT 88 | else 89 | echo 'Pick language versions listed in 'versions' - on cron (schedule) or if manually triggered job requires a full matrix' 90 | jq -c "[.[] | select(.language == \"$LANGUAGE_NAME\") | .versions][0]" < .github/json_matrices/supported-languages-versions.json | awk '{ printf "version-matrix=%s\n", $0 }' | tee -a $GITHUB_OUTPUT 91 | fi 92 | 93 | - name: Validate no empty/incorrect matrices 94 | shell: bash 95 | run: | 96 | ENGINES=`jq length <<< '${{ steps.load-engine-matrix.outputs.engine-matrix }}'` 97 | if [[ $ENGINES == 0 ]]; then 98 | echo "Engine matrix is empty!" 99 | exit 1 100 | fi 101 | HOSTS=`jq length <<< '${{ steps.load-host-matrix.outputs.host-matrix }}'` 102 | if [[ $HOSTS == 0 ]]; then 103 | echo "Host matrix is empty!" 104 | exit 1 105 | fi 106 | LANGS=`jq length <<< '${{ steps.create-lang-version-matrix.outputs.version-matrix }}'` 107 | if [[ $LANGS == 0 && $LANGUAGE_NAME != "rust" ]]; then 108 | echo "Language matrix is empty!" 109 | exit 1 110 | fi 111 | -------------------------------------------------------------------------------- /.github/workflows/install-engine/action.yml: -------------------------------------------------------------------------------- 1 | name: Install Engine 2 | 3 | inputs: 4 | engine-version: 5 | description: "Engine version to install" 6 | required: true 7 | type: string 8 | target: 9 | description: "Specified target toolchain, ex. x86_64-unknown-linux-gnu" 10 | type: string 11 | required: true 12 | options: 13 | - x86_64-unknown-linux-gnu 14 | - aarch64-unknown-linux-gnu 15 | - x86_64-apple-darwin 16 | - aarch64-apple-darwin 17 | - aarch64-unknown-linux-musl 18 | - x86_64-unknown-linux-musl 19 | 20 | env: 21 | CARGO_TERM_COLOR: always 22 | 23 | runs: 24 | using: "composite" 25 | 26 | steps: 27 | - name: "Setup Environment Variables" 28 | shell: bash 29 | env: 30 | ENGINE_VERSION: ${{ inputs.engine-version }} 31 | run: | 32 | echo "ENGINE_VERSION=$ENGINE_VERSION" >> $GITHUB_ENV 33 | 34 | - name: Prepare Valkey sources 35 | shell: bash 36 | env: 37 | TARGET: ${{ inputs.target }} 38 | run: | 39 | echo "Cloning and checking out Valkey $ENGINE_VERSION" 40 | if [[ -d valkey ]]; then 41 | echo "Removing existing valkey directory..." 42 | rm -fR valkey 43 | fi 44 | git clone https://github.com/valkey-io/valkey.git 45 | cd valkey/src 46 | git checkout $ENGINE_VERSION 47 | SOURCES_VERSION=`grep ' VALKEY_VERSION ' version.h | grep -o -E '[0-9]+\.[0-9]+(\.[0-9]+)?'` || : 48 | if [[ -z $SOURCES_VERSION ]]; then 49 | SOURCES_VERSION=`grep ' REDIS_VERSION ' version.h | grep -o -E '[0-9]+\.[0-9]+(\.[0-9]+)?'` 50 | fi 51 | echo "SOURCES_VERSION=$SOURCES_VERSION" >> $GITHUB_ENV 52 | 53 | # Get git commit SHA for unique cache key 54 | GIT_SHA=$(git rev-parse HEAD) 55 | echo "GIT_SHA=$GIT_SHA" >> $GITHUB_ENV 56 | 57 | # Debug output 58 | echo "=== Engine Installation Debug ===" 59 | echo "ENGINE_VERSION (input): $ENGINE_VERSION" 60 | echo "SOURCES_VERSION (detected): $SOURCES_VERSION" 61 | echo "GIT_SHA: $GIT_SHA" 62 | echo "Cache key will be: valkey-$SOURCES_VERSION-$GIT_SHA-$TARGET" 63 | 64 | - uses: actions/cache@v4 65 | id: cache-valkey 66 | with: 67 | path: | 68 | valkey/src/valkey-server 69 | valkey/src/valkey-cli 70 | key: valkey-${{ env.SOURCES_VERSION }}-${{ env.GIT_SHA }}-${{ inputs.target }} 71 | 72 | # Manual install, because makefile target rebuilds everything, we want to avoid that 73 | - name: Install cached engine 74 | if: ${{ steps.cache-valkey.outputs.cache-hit == 'true' }} 75 | shell: bash 76 | run: | 77 | cd valkey/src 78 | if command -v sudo &> /dev/null 79 | then 80 | echo "sudo command exists" 81 | SUDO=sudo 82 | fi 83 | $SUDO cp valkey-server valkey-cli redis-server redis-cli /usr/local/bin/ 2>/dev/null || : 84 | if [[ ! -e /usr/local/bin/redis-server ]]; then 85 | $SUDO cp valkey-server /usr/local/bin/redis-server 86 | fi 87 | echo 'export PATH=/usr/local/bin:$PATH' >>~/.bash_profile 88 | echo "/usr/local/bin" >> $GITHUB_PATH 89 | 90 | # Print installed server version 91 | echo "=== Installed Valkey/Redis server version (from cache) ===" 92 | redis-server -v || valkey-server -v || echo "Could not determine server version" 93 | 94 | - name: Build and install engine 95 | if: ${{ steps.cache-valkey.outputs.cache-hit != 'true' }} 96 | shell: bash 97 | run: | 98 | cd valkey 99 | make BUILD_TLS=yes -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo "4") 100 | if command -v sudo &> /dev/null 101 | then 102 | echo "sudo command exists" 103 | sudo make install 104 | else 105 | echo "sudo command does not exist" 106 | make install 107 | fi 108 | echo 'export PATH=/usr/local/bin:$PATH' >>~/.bash_profile 109 | echo "/usr/local/bin" >> $GITHUB_PATH 110 | 111 | # Print installed server version 112 | echo "=== Installed Valkey/Redis server version ===" 113 | redis-server -v || valkey-server -v || echo "Could not determine server version" 114 | 115 | # TODO: This seems redundant to me. Is it necessary? Do we check that the Python we install is the correct version? 116 | # Why here and not elsewhere? All Git git repos were created equal 117 | - name: Verify Valkey installation and symlinks 118 | if: ${{ !contains(inputs.engine-version, '-rc') }} 119 | shell: bash 120 | env: 121 | ENGINE_VERSION: ${{ inputs.engine-version }} 122 | run: | 123 | # In Valkey releases, the engine is built with symlinks from valkey-server and valkey-cli 124 | # to redis-server and redis-cli. This step ensures that the engine is properly installed 125 | # with the expected version and that Valkey symlinks are correctly created. 126 | # This verification runs for both cached and freshly built installations. 127 | INSTALLED_VER=$(redis-server -v) || INSTALLED_VER=$(valkey-server -v) 128 | if [[ $INSTALLED_VER != *"${ENGINE_VERSION}"* ]]; then 129 | echo "Wrong version has been installed. Expected: $ENGINE_VERSION, Installed: $INSTALLED_VER" 130 | exit 1 131 | else 132 | echo "Successfully installed the server: $INSTALLED_VER" 133 | fi 134 | -------------------------------------------------------------------------------- /cluster_scan_cursor.c: -------------------------------------------------------------------------------- 1 | /* 2 | +----------------------------------------------------------------------+ 3 | | ValkeyGlide ClusterScanCursor Implementation | 4 | +----------------------------------------------------------------------+ 5 | | Copyright (c) 2023-2025 The PHP Group | 6 | +----------------------------------------------------------------------+ 7 | | This source file is subject to version 3.01 of the PHP license, | 8 | | that is bundled with this package in the file LICENSE, and is | 9 | | available through the world-wide-web at the following url: | 10 | | http://www.php.net/license/3_01.txt | 11 | +----------------------------------------------------------------------+ 12 | */ 13 | 14 | #include "cluster_scan_cursor.h" 15 | 16 | #include 17 | 18 | #include 19 | 20 | #include "cluster_scan_cursor_arginfo.h" 21 | 22 | /* Global variables */ 23 | zend_class_entry* cluster_scan_cursor_ce; 24 | zend_object_handlers cluster_scan_cursor_object_handlers; 25 | 26 | /* Constants */ 27 | static const char* FINISHED_SCAN_CURSOR = "finished"; 28 | 29 | /* Object creation and destruction */ 30 | zend_object* create_cluster_scan_cursor_object(zend_class_entry* ce) { 31 | cluster_scan_cursor_object* cursor_obj = 32 | ecalloc(1, sizeof(cluster_scan_cursor_object) + zend_object_properties_size(ce)); 33 | 34 | zend_object_std_init(&cursor_obj->std, ce); 35 | object_properties_init(&cursor_obj->std, ce); 36 | 37 | cursor_obj->cursor_id = NULL; 38 | 39 | memcpy(&cluster_scan_cursor_object_handlers, 40 | zend_get_std_object_handlers(), 41 | sizeof(cluster_scan_cursor_object_handlers)); 42 | cluster_scan_cursor_object_handlers.offset = XtOffsetOf(cluster_scan_cursor_object, std); 43 | cluster_scan_cursor_object_handlers.free_obj = free_cluster_scan_cursor_object; 44 | cursor_obj->std.handlers = &cluster_scan_cursor_object_handlers; 45 | 46 | return &cursor_obj->std; 47 | } 48 | 49 | void free_cluster_scan_cursor_object(zend_object* object) { 50 | cluster_scan_cursor_object* cursor_obj = CLUSTER_SCAN_CURSOR_GET_OBJECT(object); 51 | 52 | 53 | /* Call FFI function to clean up Rust-side cursor */ 54 | remove_cluster_scan_cursor(cursor_obj->cursor_id); 55 | 56 | /* Free cursor string */ 57 | if (cursor_obj->cursor_id) { 58 | efree(cursor_obj->cursor_id); 59 | cursor_obj->cursor_id = NULL; 60 | } 61 | 62 | /* Free cursor string */ 63 | if (cursor_obj->next_cursor_id) { 64 | efree(cursor_obj->next_cursor_id); 65 | cursor_obj->next_cursor_id = NULL; 66 | } 67 | 68 | /* Clean up the standard object */ 69 | zend_object_std_dtor(&cursor_obj->std); 70 | } 71 | 72 | /* Class methods implementation */ 73 | 74 | /** 75 | * Constructor: new ClusterScanCursor($cursorId = "0") 76 | */ 77 | PHP_METHOD(ClusterScanCursor, __construct) { 78 | char* cursor_id = NULL; 79 | size_t cursor_id_len = 0; 80 | cluster_scan_cursor_object* cursor_obj; 81 | 82 | /* Parse optional cursor ID parameter - allow NULL */ 83 | ZEND_PARSE_PARAMETERS_START(0, 1) 84 | Z_PARAM_OPTIONAL 85 | Z_PARAM_STRING_OR_NULL(cursor_id, cursor_id_len) 86 | ZEND_PARSE_PARAMETERS_END(); 87 | 88 | cursor_obj = CLUSTER_SCAN_CURSOR_ZVAL_GET_OBJECT(getThis()); 89 | 90 | /* Set cursor ID - default to "0" if not provided or NULL */ 91 | if (cursor_id && cursor_id_len > 0) { 92 | cursor_obj->cursor_id = estrndup(cursor_id, cursor_id_len); 93 | } else { 94 | cursor_obj->cursor_id = estrdup("0"); 95 | } 96 | cursor_obj->next_cursor_id = NULL; /* Initialize next_cursor_id to NULL */ 97 | } 98 | 99 | /** 100 | * Destructor 101 | */ 102 | PHP_METHOD(ClusterScanCursor, __destruct) { 103 | /* Cleanup is handled in free_cluster_scan_cursor_object */ 104 | } 105 | 106 | /** 107 | * getCursor(): Returns the cursor ID string 108 | */ 109 | PHP_METHOD(ClusterScanCursor, getCursor) { 110 | cluster_scan_cursor_object* cursor_obj; 111 | 112 | if (zend_parse_parameters_none() == FAILURE) { 113 | RETURN_FALSE; 114 | } 115 | 116 | cursor_obj = CLUSTER_SCAN_CURSOR_ZVAL_GET_OBJECT(getThis()); 117 | 118 | if (cursor_obj->cursor_id) { 119 | RETURN_STRING(cursor_obj->cursor_id); 120 | } 121 | 122 | RETURN_STRING("0"); 123 | } 124 | 125 | PHP_METHOD(ClusterScanCursor, getNextCursor) { 126 | cluster_scan_cursor_object* cursor_obj; 127 | 128 | if (zend_parse_parameters_none() == FAILURE) { 129 | RETURN_FALSE; 130 | } 131 | 132 | cursor_obj = CLUSTER_SCAN_CURSOR_ZVAL_GET_OBJECT(getThis()); 133 | 134 | if (cursor_obj->next_cursor_id) { 135 | RETURN_STRING(cursor_obj->next_cursor_id); 136 | } 137 | 138 | RETURN_STRING("0"); 139 | } 140 | /** 141 | * isFinished(): Checks if cursor equals "finished" or "0" 142 | */ 143 | PHP_METHOD(ClusterScanCursor, isFinished) { 144 | cluster_scan_cursor_object* cursor_obj; 145 | 146 | if (zend_parse_parameters_none() == FAILURE) { 147 | RETURN_FALSE; 148 | } 149 | 150 | cursor_obj = CLUSTER_SCAN_CURSOR_ZVAL_GET_OBJECT(getThis()); 151 | 152 | if (cursor_obj->cursor_id && (strcmp(cursor_obj->cursor_id, FINISHED_SCAN_CURSOR) == 0 || 153 | strcmp(cursor_obj->cursor_id, "0") == 0)) { 154 | RETURN_TRUE; 155 | } 156 | 157 | RETURN_FALSE; 158 | } 159 | 160 | /* Class registration function using generated arginfo */ 161 | void register_cluster_scan_cursor_class(void) { 162 | /* Use the generated registration function */ 163 | cluster_scan_cursor_ce = register_class_ClusterScanCursor(); 164 | cluster_scan_cursor_ce->create_object = create_cluster_scan_cursor_object; 165 | } 166 | 167 | /* Getter function for the class entry */ 168 | zend_class_entry* get_cluster_scan_cursor_ce(void) { 169 | return cluster_scan_cursor_ce; 170 | } 171 | -------------------------------------------------------------------------------- /examples/basic/standalone_client.php: -------------------------------------------------------------------------------- 1 | $host, 'port' => $port] 33 | ]; 34 | 35 | echo "📡 Connecting to Valkey server at {$host}:{$port}\n"; 36 | 37 | $client = null; 38 | try { 39 | // Create Valkey GLIDE client 40 | $client = new ValkeyGlide( 41 | $addresses, // Server addresses 42 | $use_tls, // Use TLS 43 | $password ? ['password' => $password] : null, // Credentials 44 | 0, // Read from PRIMARY (0) 45 | 5000 // Request timeout (5 seconds) 46 | ); 47 | 48 | echo "✅ Connected successfully!\n\n"; 49 | 50 | // Test connection with PING 51 | echo "🏓 Testing connection...\n"; 52 | $pong = $client->ping(); 53 | echo "PING response: {$pong}\n\n"; 54 | 55 | // Basic string operations 56 | echo "📝 String Operations:\n"; 57 | echo "--------------------\n"; 58 | 59 | // SET operation 60 | $key = 'example:greeting'; 61 | $value = 'Hello, Valkey GLIDE!'; 62 | $setResult = $client->set($key, $value); 63 | echo "SET {$key} = '{$value}' -> {$setResult}\n"; 64 | 65 | // GET operation 66 | $getValue = $client->get($key); 67 | echo "GET {$key} -> '{$getValue}'\n"; 68 | 69 | // SET with expiration (EX = seconds) 70 | $tempKey = 'example:temp'; 71 | $client->set($tempKey, 'This will expire', ['EX' => 10]); 72 | echo "SET {$tempKey} with 10s expiration\n"; 73 | 74 | // Check TTL 75 | $ttl = $client->ttl($tempKey); 76 | echo "TTL {$tempKey} -> {$ttl} seconds\n\n"; 77 | 78 | // Numeric operations 79 | echo "🔢 Numeric Operations:\n"; 80 | echo "---------------------\n"; 81 | 82 | $counterKey = 'example:counter'; 83 | 84 | // Initialize counter 85 | $client->set($counterKey, '0'); 86 | echo "Initialized counter to 0\n"; 87 | 88 | // Increment operations 89 | for ($i = 1; $i <= 5; $i++) { 90 | $newValue = $client->incr($counterKey); 91 | echo "INCR {$counterKey} -> {$newValue}\n"; 92 | } 93 | 94 | // Increment by specific amount 95 | $newValue = $client->incrby($counterKey, 10); 96 | echo "INCRBY {$counterKey} 10 -> {$newValue}\n"; 97 | 98 | // Decrement 99 | $newValue = $client->decr($counterKey); 100 | echo "DECR {$counterKey} -> {$newValue}\n\n"; 101 | 102 | // Key operations 103 | echo "🔑 Key Operations:\n"; 104 | echo "-----------------\n"; 105 | 106 | // Check if key exists 107 | $exists = $client->exists([$key]); 108 | echo "EXISTS {$key} -> " . ($exists ? 'true' : 'false') . "\n"; 109 | 110 | // Get key type 111 | $type = $client->type($key); 112 | echo "TYPE {$key} -> {$type}\n"; 113 | 114 | // Set expiration 115 | $client->expire($key, 3600); // 1 hour 116 | $ttl = $client->ttl($key); 117 | echo "Set expiration on {$key}, TTL -> {$ttl} seconds\n"; 118 | 119 | // Remove expiration 120 | $client->persist($key); 121 | $ttl = $client->ttl($key); 122 | echo "Removed expiration from {$key}, TTL -> " . ($ttl == -1 ? 'no expiration' : "{$ttl} seconds") . "\n\n"; 123 | 124 | // Multiple key operations 125 | echo "🎯 Multiple Key Operations:\n"; 126 | echo "---------------------------\n"; 127 | 128 | // MSET - set multiple keys 129 | $keyValues = [ 130 | 'example:key1' => 'value1', 131 | 'example:key2' => 'value2', 132 | 'example:key3' => 'value3' 133 | ]; 134 | 135 | $client->mset($keyValues); 136 | echo "MSET: Set " . count($keyValues) . " keys\n"; 137 | 138 | // MGET - get multiple keys 139 | $keys = array_keys($keyValues); 140 | $values = $client->mget($keys); 141 | echo "MGET results:\n"; 142 | foreach ($keys as $index => $key) { 143 | $value = $values[$index] ?? 'null'; 144 | echo " {$key} -> '{$value}'\n"; 145 | } 146 | echo "\n"; 147 | 148 | // Server information 149 | echo "ℹ️ Server Information:\n"; 150 | echo "----------------------\n"; 151 | 152 | // Get server info 153 | $info = $client->info(); 154 | echo " redis_version -> '{$info["redis_version"]}'\n"; 155 | echo " valkey_version -> '{$info["valkey_version"]}'\n"; 156 | echo " connected_clients -> '{$info["connected_clients"]}'\n"; 157 | echo " used_memory_human -> '{$info["used_memory_human"]}'\n"; 158 | 159 | // Database size 160 | $dbsize = $client->dbsize(); 161 | echo " Database size: {$dbsize} keys\n\n"; 162 | 163 | // Cleanup 164 | echo "🧹 Cleanup:\n"; 165 | echo "----------\n"; 166 | 167 | $keysToDelete = array_merge($keys, [$counterKey, $tempKey]); 168 | $deletedCount = $client->del($keysToDelete); 169 | echo "Deleted {$deletedCount} keys\n"; 170 | 171 | echo "\n✅ Example completed successfully!\n"; 172 | } catch (Exception $e) { 173 | echo "❌ Error: " . $e->getMessage() . "\n"; 174 | echo "Error details: " . $e->getTraceAsString() . "\n"; 175 | exit(1); 176 | } finally { 177 | // Always close the connection 178 | if ($client) { 179 | $client->close(); 180 | echo "🔌 Connection closed.\n"; 181 | } 182 | } 183 | 184 | echo "\n📚 Next Steps:\n"; 185 | echo "- Try the cluster client example: php basic/cluster_client.php\n"; 186 | echo "- Explore configuration options: php basic/configuration.php\n"; 187 | echo "- Check out data structure examples in the data_structures/ directory\n"; 188 | -------------------------------------------------------------------------------- /.github/DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # CI/CD Workflow Guide 2 | 3 | ### Overview 4 | 5 | Our CI/CD pipeline tests and builds our project across multiple languages, versions, and environments. This guide outlines the key components and processes of our workflow. 6 | 7 | ### Workflow Triggers 8 | 9 | - Pull requests 10 | - Pushes to `main` or release branches (PR merges) 11 | - Scheduled runs (daily) - starts CI pipelines for all clients 12 | - Manual trigger (`workflow_dispatch`) - a developer can start a client's pipeline or the scheduled one to run all pipelines on demand 13 | 14 | Job triggers 15 | 16 | ### Test coverage 17 | 18 | There are two levels of testing: the basic one and full (_aka_ `full-matrix`). 19 | Basic amount of testing is executed on every open and merged PR. The full set of tests is executed by the scheduled job. 20 | A developer can select the level when starting a job, either scheduled or client's pipeline. 21 | 22 | Matrices 23 | 24 | ### Language-Specific Workflows 25 | 26 | Each language has its own workflow file with similar structure but language-specific steps, for example python.yml for Python, or java.yml for Java. 27 | 28 | ### Shared Components 29 | 30 | #### Matrix Files 31 | 32 | While workflows are language-specific, the matrix files are shared across all workflows. 33 | Workflows are starting by loading the matrix files from the `.github/json_matrices` directory. 34 | 35 | - `engine-matrix.json`: Defines the versions of the engine to test against. 36 | - `build-matrix.json`: Defines the host environments for testing. 37 | - `supported-languages-versions.json`: Defines the supported versions of languages. 38 | 39 | All matrices have a `run` like field which specifies if the configuration should be tested on every workflow run. 40 | This allows for flexible control over which configurations are tested in different scenarios, optimizing CI/CD performance and resource usage. 41 | 42 | #### Engine Matrix (engine-matrix.json) 43 | 44 | Defines the versions of Valkey engine to test against: 45 | 46 | ```json 47 | [ 48 | { "type": "valkey", "version": "7.2.5", "run": "always" } 49 | // ... other configurations 50 | ] 51 | ``` 52 | 53 | - `type`: The type of engine (e.g., Valkey, Redis). 54 | - `version`: Specifies the engine version that the workflow should checkout. 55 | For example, "7.2.5" represents a release tag, while "7.0" denotes a branch name. The workflow should use this parameter to checkout the specific release version or branch to build the engine with the appropriate version. 56 | - `run`: Specifies if the engine version should be tested on every workflow. 57 | 58 | #### Build Matrix (build-matrix.json) 59 | 60 | Defines the host environments for testing: 61 | 62 | ```json 63 | [ 64 | { 65 | "OS": "ubuntu", 66 | "RUNNER": "ubuntu-latest", 67 | "TARGET": "x86_64-unknown-linux-gnu", 68 | "run": ["always", "python", "node", "java"] 69 | } 70 | // ... other configurations 71 | ] 72 | ``` 73 | 74 | - `OS`: The operating system of the host. 75 | - `RUNNER`: The GitHub runner to use. 76 | - `TARGET`: The target environment as defined in Rust. To see a list of available targets, run `rustup target list`. 77 | - `run`: Specifies which language workflows should use this host configuration. The value `always` indicates that the configuration should be used for every workflow trigger. 78 | 79 | #### Supported Languages Version (supported-languages-version.json) 80 | 81 | Defines the supported versions of languages: 82 | 83 | ```json 84 | [ 85 | { 86 | "language": "java", 87 | "versions": ["11", "17"], 88 | "always-run-versions": ["17"] 89 | } 90 | // ... other configurations 91 | ] 92 | ``` 93 | 94 | - `language`: The language for which the version is supported. 95 | - `versions`: The full versions supported of the language which will test against scheduled. 96 | - `always-run-versions`: The versions that will always be tested, regardless of the workflow trigger. 97 | 98 | #### Triggering Workflows 99 | 100 | - Push to `main` by merging a PR or create a new pull request to run workflows automatically. 101 | - Use `workflow_dispatch` for manual triggers, accepting a boolean configuration parameter to run all configurations. 102 | - Scheduled runs are triggered daily to ensure regular testing of all configurations. 103 | 104 | ### Mutual vs. Language-Specific Components 105 | 106 | #### Mutual 107 | 108 | - Matrix files - `.github/json_matrices/` 109 | - Shared dependencies installation - `.github/workflows/install-shared-dependencies/action.yml` 110 | - Rust linters - `.github/workflows/lint-rust/action.yml` 111 | 112 | #### Language-Specific 113 | 114 | - Package manager commands 115 | - Testing frameworks 116 | - Build processes 117 | 118 | ### Customizing Workflows 119 | 120 | Modify `.yml` files to adjust language-specific steps. 121 | Update matrix files to change tested versions or environments. 122 | Adjust cron schedules in workflow files for different timing of scheduled runs. 123 | 124 | ### Workflow Matrices 125 | 126 | We use dynamic matrices for our CI/CD workflows, which are created using the `create-test-matrices` action. This action is defined in `.github/workflows/create-test-matrices/action.yml`. 127 | 128 | #### How it works 129 | 130 | 1. The action is called with a `language-name` input and `dispatch-run-full-matrix` input. 131 | 2. It reads the `engine-matrix.json`, `build-matrix.json`, and `supported-languages-version.json` files. 132 | 3. It filters the matrices based on the inputs and the event type. 133 | 4. It generates three matrices: 134 | - Engine matrix: Defines the types and versions of the engine to test against, for example Valkey 7.2.5. 135 | - Host matrix: Defines the host platforms to run the tests on, for example Ubuntu on ARM64. 136 | - Language-version matrix: Defines the supported versions of languages, for example python 3.9. 137 | 138 | #### Outputs 139 | 140 | - `engine-matrix-output`: The generated engine matrix. 141 | - `host-matrix-output`: The generated host matrix. 142 | - `language-version-matrix-output`: The generated language version matrix. 143 | 144 | This dynamic matrix generation allows for flexible and efficient CI/CD workflows, adapting the test configurations based on the type of change and the specific language being tested. 145 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | valkey_glide 4 | pecl.php.net 5 | High-performance PHP client for Valkey 6 | Valkey GLIDE is a high-performance client library for Valkey, built with Rust core and PHP FFI bindings. It provides better performance, type safety, and cluster support with automatic failover. 7 | 8 | Note: After installation, add "extension=valkey_glide" to your php.ini file. 9 | 10 | Valkey GLIDE Team 11 | valkeyglide 12 | noreply@valkey.io 13 | yes 14 | 15 | 2025-11-27 16 | 17 | 18 | 0.10.0 19 | 0.10.0 20 | 21 | 22 | beta 23 | beta 24 | 25 | Apache-2.0 26 | 27 | Release 0.10.0 of Valkey GLIDE PHP extension. 28 | 29 | New Features: 30 | - OpenTelemetry support for observability 31 | - Logging capabilities 32 | - IAM authentication support for ElastiCache and MemoryDB 33 | - Refresh topology configuration 34 | - Multi-Database Support for Cluster Mode (Valkey 9.0+) 35 | - Password API updates 36 | 37 | Requirements: 38 | - PHP 8.1 or higher 39 | - Rust toolchain for compilation 40 | - protobuf-c library 41 | - Git (for submodule handling during build) 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 8.1.0 108 | 109 | 110 | 1.10.0 111 | 112 | 113 | 114 | valkey_glide 115 | 116 | 117 | 118 | 119 | 120 | 2025-11-27 121 | 122 | 123 | 0.10.0 124 | 0.10.0 125 | 126 | 127 | beta 128 | beta 129 | 130 | Apache-2.0 131 | 132 | Release 0.10.0 with new features: 133 | - OpenTelemetry support for observability 134 | - Logging capabilities 135 | - IAM authentication support for ElastiCache and MemoryDB 136 | - Refresh topology configuration 137 | - Multi-Database Support for Cluster Mode (Valkey 9.0+) 138 | - Password API updates 139 | 140 | 141 | 142 | 2025-09-18 143 | 144 | 145 | 0.9.0 146 | 0.9.0 147 | 148 | 149 | beta 150 | beta 151 | 152 | Apache-2.0 153 | 154 | Initial PECL release with core functionality: 155 | - Basic Valkey command support 156 | - Cluster mode support 157 | - High-performance Rust core 158 | - Cross-platform compatibility 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /utils/get_licenses_from_ort.py: -------------------------------------------------------------------------------- 1 | # Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 2 | 3 | import json 4 | import os 5 | from typing import List, Optional, Set 6 | 7 | """ 8 | This script should be used after all specific langauge folders were scanned by the analyzer of the OSS review tool (ORT). 9 | The analyzer tool reports to analyzer-result.json files, which the script expect to be found under the /ort_results path. 10 | The script outputs a set of licenses identified by the analyzer. GLIDE maintainers should review the returned list to ensure that all licenses are approved. 11 | """ 12 | 13 | # TODO: Modify to use logic operations instead of including AND and OR in strings 14 | APPROVED_LICENSES = [ 15 | "(Apache-2.0 OR MIT) AND Unicode-DFS-2016", 16 | "Unicode-3.0", 17 | "(Apache-2.0 OR MIT) AND Unicode-3.0", 18 | "0BSD OR Apache-2.0 OR MIT", 19 | "Apache-2.0", 20 | "Apache-2.0 AND (Apache-2.0 OR BSD-2-Clause)", 21 | "Apache-2.0 AND (Apache-2.0 OR BSD-3-Clause)", 22 | "Apache-2.0 AND MIT", 23 | "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT", 24 | "Apache-2.0 OR BSD-2-Clause OR MIT", 25 | "Apache-2.0 OR BSL-1.0", 26 | "Apache-2.0 OR ISC OR MIT", 27 | "Apache-2.0 OR MIT", 28 | "Apache-2.0 OR MIT OR Zlib", 29 | "Apache-2.0 WITH LLVM-exception", 30 | "BSD License", 31 | "BSD-2-Clause", 32 | "BSD-2-Clause OR Apache-2.0", 33 | "BSD-3-Clause", 34 | "BSD-3-Clause OR Apache-2.0", 35 | "ISC", 36 | "MIT", 37 | "MPL-2.0", 38 | "Zlib", 39 | "MIT OR Unlicense", 40 | "PSF-2.0", 41 | "Unicode-3.0", 42 | "Unicode-DFS-2016", 43 | "Zlib", 44 | "BSD-3-Clause AND MIT", 45 | "Apache-2.0 OR LGPL-2.1-or-later OR MIT", 46 | "Apache-2.0 AND ISC", 47 | "Apache-2.0 AND (Apache-2.0 OR MIT) AND MIT", 48 | "(Apache-2.0 OR ISC) AND ISC", 49 | "(Apache-2.0 OR ISC) AND ISC AND OpenSSL", 50 | "CDLA-Permissive-2.0" 51 | ] 52 | 53 | # Packages with non-pre-approved licenses that received manual approval. 54 | APPROVED_PACKAGES = [ 55 | "PyPI::pathspec:0.12.1", 56 | "PyPI::certifi:2023.11.17", 57 | "Crate::ring:0.17.8", 58 | "Maven:org.json:json:20231013", 59 | ] 60 | SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) 61 | 62 | 63 | class OrtResults: 64 | def __init__(self, name: str, ort_results_folder: str) -> None: 65 | """ 66 | Args: 67 | name (str): the language name. 68 | ort_results_folder (str): The relative path to the ort results folder from the root of the valkey-glide directory. 69 | """ 70 | folder_path = f"{SCRIPT_PATH}/{ort_results_folder}" 71 | self.analyzer_result_file = f"{folder_path}/analyzer-result.json" 72 | self.notice_file = f"{folder_path}/NOTICE_DEFAULT" 73 | self.name = name 74 | 75 | 76 | class PackageLicense: 77 | def __init__( 78 | self, package_name: str, language: str, license: Optional[str] = None 79 | ) -> None: 80 | self.package_name = package_name 81 | self.language = language 82 | self.license = license 83 | 84 | def __str__(self): 85 | str_msg = f"Package_name: {self.package_name}, Language: {self.language}" 86 | if license: 87 | str_msg += f", License: {self.license}" 88 | return str_msg 89 | 90 | 91 | ort_results_per_lang = [ 92 | OrtResults("Rust", "../valkey-glide/glide-core/ort_results"), 93 | OrtResults("PHP", "../ort_results"), 94 | ] 95 | 96 | all_licenses_set: Set = set() 97 | unknown_licenses: List[PackageLicense] = [] 98 | final_packages: List[PackageLicense] = [] 99 | skipped_packages: List[PackageLicense] = [] 100 | 101 | for ort_result in ort_results_per_lang: 102 | with open(ort_result.analyzer_result_file, "r") as ort_results, open( 103 | ort_result.notice_file, "r" 104 | ) as notice_file: 105 | json_file = json.load(ort_results) 106 | notice_file_text = notice_file.read() 107 | for package in json_file["analyzer"]["result"]["packages"]: 108 | package_name = package["id"].split(":")[2] 109 | if package_name not in notice_file_text: 110 | # skip packages not in the final report 111 | skipped_packages.append(PackageLicense(package["id"], ort_result.name)) 112 | continue 113 | try: 114 | for license in package["declared_licenses_processed"].values(): 115 | if isinstance(license, list) or isinstance(license, dict): 116 | final_licenses = ( 117 | list(license.values()) 118 | if isinstance(license, dict) 119 | else license 120 | ) 121 | else: 122 | final_licenses = [license] 123 | for license in final_licenses: 124 | package_license = PackageLicense( 125 | package["id"], ort_result.name, license 126 | ) 127 | if ( 128 | license not in APPROVED_LICENSES 129 | and package["id"] not in APPROVED_PACKAGES 130 | ): 131 | unknown_licenses.append(package_license) 132 | else: 133 | final_packages.append(package_license) 134 | all_licenses_set.add(license) 135 | except Exception: 136 | print( 137 | f"Received error for package {package} used by {ort_result.name}\n Found license={license}" 138 | ) 139 | raise 140 | 141 | package_list_file_path = f"{SCRIPT_PATH}/final_package_list.txt" 142 | with open(package_list_file_path, mode="wt", encoding="utf-8") as f: 143 | f.writelines(f"{package}\n" for package in final_packages) 144 | 145 | skipped_list_file_path = f"{SCRIPT_PATH}/skipped_package_list.txt" 146 | with open(skipped_list_file_path, mode="wt", encoding="utf-8") as f: 147 | f.writelines(f"{package}\n" for package in skipped_packages) 148 | 149 | unapproved_list_file_path = f"{SCRIPT_PATH}/unapproved_package_list.txt" 150 | with open(unapproved_list_file_path, mode="wt", encoding="utf-8") as f: 151 | f.writelines(f"{package}\n" for package in unknown_licenses) 152 | 153 | print("\n\n#### Found Licenses #####\n") 154 | all_licenses_set = set(sorted(all_licenses_set)) 155 | for license in all_licenses_set: 156 | print(f"{license}") 157 | 158 | print("\n\n#### unknown / Not Pre-Approved Licenses #####\n") 159 | for package in unknown_licenses: 160 | print(str(package)) 161 | -------------------------------------------------------------------------------- /.github/workflows/run-php-tests/action.yml: -------------------------------------------------------------------------------- 1 | name: Run PHP Extension Tests 2 | description: Common steps for running PHP extension tests with Valkey servers 3 | 4 | inputs: 5 | extension-path: 6 | description: Path to the built extension (.so file) 7 | required: true 8 | php-version: 9 | description: PHP version to use for testing 10 | required: true 11 | default: "8.1" 12 | 13 | runs: 14 | using: composite 15 | steps: 16 | - name: Start Valkey servers 17 | working-directory: tests 18 | shell: bash 19 | run: | 20 | set -e # Exit on any error 21 | 22 | # Ensure /usr/local/bin is in PATH (where valkey-server is installed) 23 | export PATH="/usr/local/bin:$PATH" 24 | 25 | echo "=== DEBUG: Current directory and files ===" 26 | pwd 27 | ls -la 28 | 29 | echo "=== DEBUG: Environment ===" 30 | echo "PATH=$PATH" 31 | 32 | echo "=== DEBUG: Which commands are being used ===" 33 | which valkey-server || echo "valkey-server not found in PATH" 34 | which redis-server || echo "redis-server not found in PATH" 35 | 36 | echo "=== DEBUG: Version of found commands ===" 37 | valkey-server --version 2>/dev/null || echo "valkey-server --version failed" 38 | redis-server --version 2>/dev/null || echo "redis-server --version failed" 39 | 40 | echo "=== DEBUG: Checking for installed binaries in /usr/local/bin ===" 41 | ls -la /usr/local/bin/ | grep -E "(valkey|redis)" || echo "No valkey/redis binaries in /usr/local/bin" 42 | 43 | echo "=== DEBUG: Checking script permissions ===" 44 | ls -la start_valkey_with_replicas.sh create-valkey-cluster.sh 45 | 46 | # Make scripts executable 47 | chmod +x start_valkey_with_replicas.sh create-valkey-cluster.sh 48 | 49 | echo "=== DEBUG: After chmod ===" 50 | ls -la start_valkey_with_replicas.sh create-valkey-cluster.sh 51 | 52 | # Check if valkey-server is available 53 | echo "=== DEBUG: Checking for valkey-server ===" 54 | which valkey-server || echo "valkey-server not found in PATH" 55 | 56 | # Check alternative locations and names 57 | echo "=== DEBUG: Checking alternative binary names ===" 58 | which redis-server || echo "redis-server not found in PATH" 59 | which valkey-cli || echo "valkey-cli not found in PATH" 60 | which redis-cli || echo "redis-cli not found in PATH" 61 | 62 | # Start standalone servers (6379, 6380, 6381) 63 | echo "=== Starting standalone Valkey servers ===" 64 | ./start_valkey_with_replicas.sh 65 | 66 | # Wait a moment for servers to start 67 | echo "=== Waiting for standalone servers to start ===" 68 | sleep 3 69 | 70 | # Check if servers are running 71 | echo "=== Checking standalone server status ===" 72 | ps aux | grep -E "(valkey|redis)-server" | grep -v grep || echo "No valkey/redis-server processes found" 73 | 74 | # Start cluster servers (7001-7006) 75 | echo "=== Starting Valkey cluster ===" 76 | 77 | echo "=== DEBUG: Checking valkey binaries before cluster setup ===" 78 | VALKEY_SERVER_PATH=$(which valkey-server) 79 | VALKEY_CLI_PATH=$(which valkey-cli) 80 | echo "valkey-server path: $VALKEY_SERVER_PATH" 81 | echo "valkey-cli path: $VALKEY_CLI_PATH" 82 | echo "valkey-server version: $($VALKEY_SERVER_PATH --version)" 83 | echo "valkey-cli version: $($VALKEY_CLI_PATH --version)" 84 | 85 | ./create-valkey-cluster.sh 86 | 87 | # Final server status check 88 | echo "=== Final server status check ===" 89 | ps aux | grep -E "(valkey|redis)-server" | grep -v grep || echo "No valkey/redis-server processes found" 90 | echo "=== Valkey servers startup completed ===" 91 | 92 | - name: Run PHP extension tests 93 | shell: bash 94 | env: 95 | EXTENSION_PATH: ${{ inputs.extension-path }} 96 | run: | 97 | # Debug: Check if extension is loaded and classes exist 98 | echo "=== Extension Debug Info ===" 99 | echo "Extension path: $EXTENSION_PATH" 100 | 101 | # Check if this is a file path or extension name 102 | if [[ -f "$EXTENSION_PATH" ]]; then 103 | echo "Loading extension from file: $EXTENSION_PATH" 104 | EXTENSION_ARG="extension=$EXTENSION_PATH" 105 | else 106 | echo "Loading extension by name: $EXTENSION_PATH" 107 | EXTENSION_ARG="extension=$EXTENSION_PATH" 108 | fi 109 | 110 | php -n -d "$EXTENSION_ARG" -r " 111 | echo 'Extension loaded: ' . (extension_loaded('valkey_glide') ? 'YES' : 'NO') . PHP_EOL; 112 | echo 'Extension version: ' . phpversion('valkey_glide') . PHP_EOL; 113 | echo 'Extension functions: ' . PHP_EOL; 114 | foreach (get_extension_funcs('valkey_glide') as \$func) { 115 | echo ' - ' . \$func . PHP_EOL; 116 | } 117 | echo 'All declared classes: ' . PHP_EOL; 118 | foreach (get_declared_classes() as \$class) { 119 | if (strpos(\$class, 'Valkey') !== false || strpos(\$class, 'Client') !== false || strpos(\$class, 'Mock') !== false) { 120 | echo ' - ' . \$class . PHP_EOL; 121 | } 122 | } 123 | echo 'ClientConstructorMock exists: ' . (class_exists('ClientConstructorMock') ? 'YES' : 'NO') . PHP_EOL; 124 | if (class_exists('ClientConstructorMock')) { 125 | echo 'ClientConstructorMock methods: ' . PHP_EOL; 126 | foreach (get_class_methods('ClientConstructorMock') as \$method) { 127 | echo ' - ' . \$method . PHP_EOL; 128 | } 129 | } 130 | " 131 | 132 | # Run tests using extension loading 133 | php -n -d "$EXTENSION_ARG" tests/TestValkeyGlide.php 134 | 135 | - name: Run integration tests 136 | shell: bash 137 | run: | 138 | # All tests are now run through the main TestValkeyGlide.php runner 139 | echo "All 4 test classes are already included in the main test runner" 140 | echo "TestValkeyGlide.php now runs all classes by default:" 141 | echo "- ValkeyGlide_Test (standalone)" 142 | echo "- ValkeyGlide_Cluster_Test (cluster)" 143 | echo "- ValkeyGlide_Features_Test (standalone features)" 144 | echo "- ValkeyGlide_Cluster_Features_Test (cluster features)" 145 | 146 | - name: Stop Valkey servers 147 | if: always() 148 | shell: bash 149 | run: | 150 | # Stop all Valkey server processes 151 | echo "Stopping Valkey servers..." 152 | pkill valkey-server || true 153 | # Clean up data directories 154 | rm -rf $HOME/valkey-cluster || true 155 | rm -rf tests/valkey_data || true 156 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # AGENTS: PHP Client Context for Agentic Tools 2 | 3 | This file provides AI agents and developers with the minimum but sufficient context to work productively with the Valkey GLIDE PHP client. It covers build commands, testing, contribution requirements, and essential guardrails specific to the PHP implementation. 4 | 5 | ## Repository Overview 6 | 7 | This is the PHP client binding for Valkey GLIDE, providing a sync client implementations. The PHP client is intended to have the same programming interface as PHPRedis, differing only when constructing instances of the client (standalone and cluster). It is implemented as a Zend extension. 8 | 9 | **Primary Languages:** PHP, C (Zend Framework) 10 | **Build System:** Standard automake-based system for Zend extensions. phpize, configure, make, make install. 11 | **Architecture:** PHP API stubs implemented using the Zend Framework in C. Implementations delegate communication 12 | to the GLIDE Core written in Rust using C FFI headers. 13 | 14 | **Key Components:** 15 | - `./` - PHP API stubs (*.php.stub files), 16 | - `src/` - Mock classes for testing. 17 | - `tests/` - Test suite (primarily integration tests) 18 | - `config.m4` and `Makefile.frag` - Templates for configure and Makefile 19 | - `package.xml` - File manifest for PECL packages 20 | - `composer.json` - Manifest for PIE packages 21 | - `valkey-glide/` - submodule reference to the valkey-glide repo for the GLIDE core. 22 | 23 | ## Architecture Quick Facts 24 | 25 | **Core Implementation:** C wrappers around glide-core Rust library exposed through PHP's Zend framework. 26 | **Client Types:** ValkeyGlide and ValkeyGlideCluster represent standalone and cluster clients respectively. 27 | **API Styles:** Blocking API, matching PHPRedis. 28 | **Communication:** Direct FFI (sync) 29 | 30 | **Supported Platforms:** 31 | - Linux: Ubuntu 20+, Amazon Linux 2/2023 (x86_64, aarch64) 32 | - macOS: 13.7+ (x86_64), 14.7+ (aarch64) 33 | - **Note:** Alpine Linux/MUSL not supported 34 | 35 | **PHP Versions:** 8.1, 8.2 36 | 37 | **Packages:** `valkey-io/valkey-glide-php` 38 | 39 | ## Connection function implementation flow 40 | * Client constructors are implemented with the by implementing `__construct` PHP method for a class using the 41 | PHP_METHOD macro. See `valkey_glide.c` and `valkey_glide_cluster.c` 42 | * The design of the clusters basically marshal PHP function arguments to C structs defined locally, then those get 43 | written to protobufs messages defined by the FFI layer. Then create_client() is called in the FFI layer with the 44 | protobuf message. 45 | * The C structures representing the connection configuration are `valkey_glide_client_configuration_t` and 46 | `valkey_glide_cluster_client_configuration_t`. Shared fields are in `valkey_glide_base_client_configuration_t`. 47 | * The method which converts the configuration to protobuf is `create_connection_request()` defined in 48 | `valkey_glide_core_commands.c`. 49 | 50 | ## Command Implementation Flow 51 | * Define a method in `valkey_glide.stub.php` and/or `valkey_glide_cluster.stub.php` depending on if it should be 52 | available in standalone or cluster. 53 | * Write a helper function to execute the command through the FFI layer. See `execute_get_command` for an example. 54 | These methods should work generically on a `ValkeyGlide` or `ValkeyGlideCluster`. The `valkey_glide_object` pointer represents either. 55 | * Define a macro that invokes the PHP_METHOD macro, taking in a class name. Have that macro call the helper above. See `GET_METHOD_IMPL`. 56 | * Invoke this macro in a .c file. `valkey_z_php_methods.c` for standalone and `valkey_glide_cluster.c` for cluster. 57 | 58 | ## File naming scheme 59 | * Files are named `valkey_glide__commands.c`, possibly with a numerical suffix. 60 | * Code to be shared across a command category go in `valkey_glide__common.c`. 61 | 62 | ## Test Framework 63 | Tests are written using a proprietary test framework rather than an existing framework 64 | such as PHPUnit. The base class for test suites is TestSuite and it is in tests/TestSuite.php. This framework doesn't integrate into IDEs such as PhpStorm and doesn't provide a way to run individual test methods or by pattern so switching to a well-known framework may be beneficial. 65 | 66 | ## Build and Test Rules (Agents) 67 | 68 | ### Standard autoconf build (used internally by PIE and PECL installers) 69 | ```bash 70 | # Build commands 71 | phpize 72 | ./configure 73 | make 74 | 75 | # Testing 76 | make test 77 | 78 | ## Contribution Requirements 79 | 80 | ### Developer Certificate of Origin (DCO) Signoff REQUIRED 81 | 82 | All commits must include a `Signed-off-by` line: 83 | 84 | ```bash 85 | # Add signoff to new commits 86 | git commit -s -m "feat(python): add new command implementation" 87 | 88 | # Configure automatic signoff 89 | git config --global format.signOff true 90 | 91 | # Add signoff to existing commit 92 | git commit --amend --signoff --no-edit 93 | 94 | # Add signoff to multiple commits 95 | git rebase -i HEAD~n --signoff 96 | ``` 97 | 98 | ### Conventional Commits 99 | 100 | Use conventional commit format: 101 | 102 | ``` 103 | (): 104 | 105 | [optional body] 106 | ``` 107 | 108 | **Example:** `feat(python): implement async cluster scan with routing options` 109 | 110 | ### Code Quality Requirements 111 | 112 | **C Linters:** 113 | Run clang-format on any modified .c and .h files. 114 | 115 | **PHP Linters:** 116 | To be added. 117 | 118 | ## Guardrails & Policies 119 | 120 | ### Generated Outputs (Never Commit) 121 | See the .gitignore 122 | 123 | ## Quality Gates (Agent Checklist) 124 | 125 | - [ ] Build passes: `phpize, ./configure, make` succeeds 126 | - [ ] All tests pass: `make install` succeeds 127 | - [ ] Linting passes: `clang-format` does not report warnings on changed .c or .h files 128 | - [ ] No generated outputs committed (check `.gitignore`) 129 | - [ ] DCO signoff present: `git log --format="%B" -n 1 | grep "Signed-off-by"` 130 | - [ ] Conventional commit format used 131 | - [ ] Documentation follows Google Style format 132 | - [ ] Shared logic properly isolated in `glide-shared/` 133 | 134 | ## Quick Facts for Reasoners 135 | 136 | **Packages:** `valkey-glide` (async), `valkey-glide-sync` (sync) on PyPI 137 | **API Styles:** Async (asyncio/trio), Sync (blocking) 138 | **Client Types:** GlideClient (standalone), GlideClusterClient (cluster) for both async/sync 139 | **Key Features:** Dual client architecture, shared logic, multi-async framework support 140 | **Testing:** pytest with async backend selection, shared test suite 141 | **Platforms:** Linux (Ubuntu, AL2/AL2023), macOS (Intel/Apple Silicon) 142 | **Dependencies:** Python 3.9+, Rust toolchain, protobuf compiler 143 | 144 | ## If You Need More 145 | 146 | - **Getting Started:** [README.md](./README.md) 147 | - **Development Setup:** [DEVELOPER.md](./DEVELOPER.md) 148 | - **Examples:** [../examples/](../examples/) 149 | - **Test Suites:** [tests/](./tests/) directory 150 | -------------------------------------------------------------------------------- /tests/run_test_s.php: -------------------------------------------------------------------------------- 1 | 0) { 257 | echo "\nFailed tests:\n"; 258 | foreach ($failedTests as $test) { 259 | echo "- {$test}\n"; 260 | } 261 | } 262 | 263 | if ($skipped > 0) { 264 | echo "\nSkipped tests:\n"; 265 | foreach ($skippedTests as $test) { 266 | echo "- {$test}\n"; 267 | } 268 | } 269 | 270 | echo "========================\n"; 271 | -------------------------------------------------------------------------------- /tests/run_test_s_cluster.php: -------------------------------------------------------------------------------- 1 | 0) { 259 | echo "\nFailed tests:\n"; 260 | foreach ($failedTests as $test) { 261 | echo "- {$test}\n"; 262 | } 263 | } 264 | 265 | if ($skipped > 0) { 266 | echo "\nSkipped tests:\n"; 267 | foreach ($skippedTests as $test) { 268 | echo "- {$test}\n"; 269 | } 270 | } 271 | 272 | echo "========================\n"; 273 | -------------------------------------------------------------------------------- /.github/workflows/build-php-wrapper/action.yml: -------------------------------------------------------------------------------- 1 | name: Build PHP wrapper 2 | description: Build PHP wrapper 3 | inputs: 4 | os: 5 | description: Target OS 6 | required: true 7 | target: 8 | description: Target 9 | required: true 10 | php-version: 11 | description: PHP version 12 | required: true 13 | github-token: 14 | description: GitHub token 15 | required: true 16 | engine-version: 17 | description: Engine version 18 | required: true 19 | 20 | runs: 21 | using: composite 22 | steps: 23 | - name: Setup PHP 24 | uses: shivammathur/setup-php@v2 25 | with: 26 | php-version: ${{ inputs.php-version }} 27 | extensions: tokenizer, json, ctype, iconv, mbstring 28 | tools: none 29 | env: 30 | runner: self-hosted 31 | 32 | - name: Install system dependencies (Ubuntu) 33 | if: ${{ inputs.os == 'ubuntu' }} 34 | shell: bash 35 | env: 36 | PHP_VER: ${{ inputs.php-version }} 37 | run: | 38 | sudo apt-get update 39 | sudo apt-get install -y \ 40 | php$PHP_VER-dev \ 41 | php$PHP_VER-cli \ 42 | build-essential \ 43 | autoconf \ 44 | automake \ 45 | libtool \ 46 | pkg-config \ 47 | libssl-dev \ 48 | php-bcmath \ 49 | unzip \ 50 | clang-format \ 51 | cppcheck \ 52 | libprotobuf-c-dev \ 53 | libprotobuf-c1 54 | 55 | - name: Install system dependencies (macOS) 56 | if: ${{ inputs.os == 'macos' }} 57 | shell: bash 58 | run: | 59 | brew install autoconf automake libtool pkg-config protobuf@3 openssl clang-format 60 | 61 | - name: Install protobuf compiler (Ubuntu) 62 | if: ${{ inputs.os == 'ubuntu' }} 63 | uses: arduino/setup-protoc@v3 64 | with: 65 | version: "25.1" 66 | repo-token: ${{ inputs.github-token }} 67 | 68 | - name: Install protobuf-c-compiler (Ubuntu) 69 | if: ${{ inputs.os == 'ubuntu' }} 70 | shell: bash 71 | run: | 72 | # Install protobuf-c-compiler for PHP extension builds 73 | sudo apt-get install -y protobuf-c-compiler 74 | echo "protoc-c version: $(protoc-c --version)" 75 | 76 | - name: Install protobuf compiler (macOS) 77 | if: ${{ inputs.os == 'macos' }} 78 | shell: bash 79 | run: | 80 | echo 'export PATH="/opt/homebrew/opt/protobuf@3/bin:$PATH"' >> $GITHUB_ENV 81 | # Install protobuf-c 82 | brew install protobuf-c 83 | 84 | - name: Install shared software dependencies 85 | uses: ./.github/workflows/install-shared-dependencies 86 | with: 87 | os: ${{ inputs.os }} 88 | target: ${{ inputs.target }} 89 | github-token: ${{ inputs.github-token }} 90 | engine-version: ${{ inputs.engine-version }} 91 | 92 | - name: Remove optional fields from proto files 93 | shell: bash 94 | run: | 95 | echo "=== Removing optional fields from proto files ===" 96 | python3 utils/remove_optional_from_proto.py 97 | echo "=== Proto file cleanup completed ===" 98 | 99 | - name: Install Rust and Cargo 100 | uses: ./.github/workflows/install-rust-and-protoc 101 | with: 102 | github-token: ${{ inputs.github-token }} 103 | 104 | - name: Install cbindgen 105 | shell: bash 106 | run: | 107 | if [ -f "$HOME/.cargo/bin/cbindgen" ]; then 108 | echo "cbindgen already installed (cached)" 109 | else 110 | cargo install cbindgen 111 | fi 112 | 113 | - name: Install ziglang (Ubuntu) 114 | if: ${{ inputs.os == 'ubuntu' }} 115 | shell: bash 116 | run: | 117 | pip3 install ziglang 118 | if [ -f "$HOME/.cargo/bin/cargo-zigbuild" ]; then 119 | echo "cargo-zigbuild already installed (cached)" 120 | else 121 | cargo install --locked cargo-zigbuild 122 | fi 123 | 124 | - name: Setup sccache 125 | uses: mozilla-actions/sccache-action@v0.0.4 126 | 127 | - name: Setup Rust cache 128 | uses: Swatinem/rust-cache@v2 129 | with: 130 | workspaces: | 131 | valkey-glide/ffi 132 | cache-on-failure: true 133 | 134 | - name: Setup Cargo environment for caching 135 | shell: bash 136 | run: | 137 | # Ensure Cargo uses the cached registry and git databases 138 | mkdir -p ~/.cargo 139 | echo "Cargo home: $CARGO_HOME" 140 | echo "Registry cache: $(ls -la ~/.cargo/registry/ 2>/dev/null || echo 'Not found')" 141 | echo "Git cache: $(ls -la ~/.cargo/git/ 2>/dev/null || echo 'Not found')" 142 | 143 | - name: Debug FFI library location 144 | shell: bash 145 | env: 146 | TARGET: ${{ inputs.target }} 147 | working-directory: valkey-glide/ffi 148 | run: | 149 | echo "=== FFI Target Directory Structure ===" 150 | find target -name "libglide_ffi.a" -type f 2>/dev/null || echo "No libglide_ffi.a found" 151 | echo "=== Target directory contents ===" 152 | ls -la target/ || true 153 | if [ -d "target/$TARGET" ]; then 154 | echo "=== Target-specific directory ===" 155 | ls -la "target/$TARGET/" || true 156 | if [ -d "target/$TARGET/release" ]; then 157 | echo "=== Target release directory ===" 158 | ls -la "target/$TARGET/release/" || true 159 | fi 160 | fi 161 | 162 | - name: Initialize PHP extension build system 163 | shell: bash 164 | run: | 165 | phpize 166 | 167 | - name: Configure PHP extension 168 | shell: bash 169 | run: | 170 | ./configure --enable-valkey-glide 171 | 172 | - name: Generate protobuf and bindings 173 | shell: bash 174 | run: | 175 | make include/glide_bindings.h 176 | 177 | - name: Debug PHP extension linking 178 | shell: bash 179 | run: | 180 | echo "=== Checking FFI library paths ===" 181 | echo "Platform: $(uname)" 182 | echo "Checking for target-specific paths:" 183 | ls -la valkey-glide/ffi/target/*/release/libglide_ffi.a 2>/dev/null || echo "No target-specific paths found" 184 | echo "Checking for generic release path:" 185 | ls -la valkey-glide/ffi/target/release/libglide_ffi.a 2>/dev/null || echo "No generic release path found" 186 | echo "=== Makefile.frag VALKEY_GLIDE_SHARED_LIBADD setting ===" 187 | grep "VALKEY_GLIDE_SHARED_LIBADD" Makefile.frag || echo "VALKEY_GLIDE_SHARED_LIBADD not found" 188 | 189 | - name: Build PHP extension 190 | shell: bash 191 | run: | 192 | make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo "4") 193 | 194 | - name: Verify extension build 195 | shell: bash 196 | run: | 197 | # Check if the extension was built successfully 198 | if [ -f "modules/valkey_glide.so" ]; then 199 | echo "✓ Extension built successfully: modules/valkey_glide.so" 200 | ls -la modules/valkey_glide.so 201 | else 202 | echo "✗ Extension build failed - modules/valkey_glide.so not found" 203 | exit 1 204 | fi 205 | 206 | # Test loading the extension directly 207 | php -n -d extension=modules/valkey_glide.so -m | grep valkey_glide && echo "✓ Extension loads correctly" || echo "✗ Extension failed to load" 208 | -------------------------------------------------------------------------------- /tests/ValkeyGlideClusterBatchTest.php: -------------------------------------------------------------------------------- 1 | valkey_glide = $this->newInstance(); 25 | $info = $this->valkey_glide->info("randomNode"); 26 | $this->version = $info['valkey_version'] ?? $info['redis_version'] ?? '0.0.0'; 27 | 28 | $this->is_valkey = $this->detectValkey($info); 29 | 30 | // Log server type and version for debugging 31 | $server_type = $this->is_valkey ? 'Valkey' : 'Redis'; 32 | echo "Connected to $server_type cluster batch server version: {$this->version}\n"; 33 | } 34 | 35 | /* Override newInstance as we want a ValkeyGlideCluster object */ 36 | protected function newInstance() 37 | { 38 | try { 39 | return new ValkeyGlideCluster( 40 | [['host' => '127.0.0.1', 'port' => 7001]], // addresses array format 41 | false, // use_tls 42 | $this->getAuth(), // credentials 43 | ValkeyGlide::READ_FROM_PRIMARY, // read_from 44 | ); 45 | } catch (Exception $ex) { 46 | TestSuite::errorMessage("Fatal error: %s\n", $ex->getMessage()); 47 | //TestSuite::errorMessage("Seeds: %s\n", implode(' ', self::$seeds)); 48 | TestSuite::errorMessage("Seed source: %s\n", self::$seed_source); 49 | exit(1); 50 | } 51 | } 52 | 53 | public function testServerOperationsBatch() 54 | { 55 | } 56 | 57 | public function testInfoOperationsBatch() 58 | { 59 | 60 | // Execute INFO, CLIENT ID, CLIENT GETNAME in multi/exec batch 61 | $results = $this->valkey_glide->multi() 62 | ->client('id') 63 | // ->client('setname', 'phpredis_unit_tests')//TODO return once setname is supported 64 | ->client('getname') 65 | ->client('list') 66 | ->exec(); 67 | 68 | // Verify transaction results 69 | $this->assertIsArray($results); 70 | $this->assertCount(3, $results); 71 | $this->assertIsInt($results[0]); // CLIENT ID result (integer) 72 | // CLIENT GETNAME might return null if no name is set 73 | $this->assertEquals('valkey-glide-php', $results[1]); // CLIENT SETNAME result 74 | 75 | $this->assertGT(0, $results[0]); // Client ID should be positive 76 | } 77 | 78 | public function testDatabaseOperationsBatch() 79 | { 80 | $key1 = '{xxx}batch_db_' . uniqid(); 81 | 82 | // Execute SELECT, DBSIZE, TYPE in multi/exec batch 83 | $results = $this->valkey_glide->multi() 84 | ->set('{xxx}x', 'y') 85 | ->set($key1, 'test_value') 86 | ->type($key1) 87 | ->exec(); 88 | 89 | // Verify transaction results 90 | $this->assertIsArray($results); 91 | $this->assertCount(3, $results); 92 | $this->assertEquals(ValkeyGlide::VALKEY_GLIDE_STRING, $results[2]); // TYPE result 93 | 94 | 95 | // Cleanup 96 | $this->valkey_glide->del($key1); 97 | } 98 | 99 | public function testAdvancedKeyOperationsBatch() 100 | { 101 | 102 | $key1 = '{xyz}batch_adv_1_' . uniqid(); 103 | $key2 = '{xyz}batch_adv_2_' . uniqid(); 104 | $key3 = '{xyz}batch_adv_3_' . uniqid(); 105 | 106 | // Setup test data 107 | $this->valkey_glide->set($key1, 'test_value'); 108 | 109 | // Execute UNLINK, TOUCH, RANDOMKEY in multi/exec batch 110 | $results = $this->valkey_glide->multi() 111 | ->unlink($key1) 112 | ->touch($key2, $key3) // Touch non-existing keys 113 | ->exec(); 114 | 115 | // Verify transaction results 116 | $this->assertIsArray($results); 117 | $this->assertCount(2, $results); 118 | $this->assertEquals(1, $results[0]); // UNLINK result (1 key removed) 119 | $this->assertEquals(0, $results[1]); // TOUCH result (0 keys touched - they don't exist) 120 | // RANDOMKEY result can be any existing key or null 121 | 122 | // Verify server-side effects 123 | $this->assertEquals(0, $this->valkey_glide->exists($key1)); // Key unlinked 124 | 125 | // No cleanup needed as keys were deleted/don't exist 126 | } 127 | 128 | public function testConfigOperationsBatch() 129 | { 130 | // Config operations are not supported in batch and cluster mode 131 | } 132 | 133 | public function testFlushOperationsBatch() 134 | { 135 | // Flush operations are not supported in batch and cluster mode 136 | } 137 | 138 | public function testWaitBatch() 139 | { 140 | // WAIT is not supported in batch and cluster mode 141 | } 142 | 143 | 144 | public function testScanOperationsBatch() 145 | { 146 | $key1 = '{scantest}batch_scan_set_' . uniqid(); 147 | $key2 = '{scantest}batch_scan_hash_' . uniqid(); 148 | 149 | // Setup test data 150 | $this->valkey_glide->del($key1, $key2); 151 | $this->valkey_glide->sadd($key1, 'member1', 'member2', 'member3'); 152 | $this->valkey_glide->hset($key2, 'field1', 'value1', 'field2', 'value2'); 153 | 154 | // Execute SSCAN, HSCAN in multi/exec batch 155 | $sscan_it = null; 156 | $hscan_it = null; 157 | 158 | $results = $this->valkey_glide->multi() 159 | ->sscan($key1, $sscan_it) 160 | ->hscan($key2, $hscan_it) 161 | ->exec(); 162 | 163 | // Verify transaction results 164 | $this->assertIsArray($results); 165 | $this->assertCount(2, $results); 166 | $this->assertIsArray($results[0]); // SSCAN result [cursor, members] 167 | $sscan_it = null; 168 | $this->assertEquals($results[0], $this->valkey_glide->sscan($key1, $sscan_it)); 169 | $this->assertIsArray($results[1]); // HSCAN result [cursor, fields_values] 170 | $hscan_it = null; 171 | $this->assertEquals($results[1], $this->valkey_glide->hscan($key2, $hscan_it)); 172 | // Verify server-side effects (scan operations don't modify data) 173 | $this->assertEquals(3, $this->valkey_glide->scard($key1)); 174 | $this->assertEquals(2, $this->valkey_glide->hlen($key2)); 175 | 176 | // Cleanup 177 | $this->valkey_glide->del($key1, $key2); 178 | } 179 | 180 | public function testMoveBatch() 181 | { 182 | // MOVE is not supported in cluster mode 183 | } 184 | 185 | public function testFunctionManagementBatch() 186 | { 187 | 188 | // FUNCTION management is not supported in batch and cluster mode 189 | } 190 | 191 | public function testFunctionDumpRestoreBatch() 192 | { 193 | // FUNCTION DUMP and RESTORE are not supported in batch and cluster mode 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /utils/patch_proto_and_rust.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | from pathlib import Path 5 | 6 | def log_message(message, level="INFO"): 7 | """Simple logging function""" 8 | print(f"[{level}] {message}") 9 | 10 | def create_backup(filepath): 11 | """Create a backup of the file before modifying""" 12 | backup_path = f"{filepath}.backup" 13 | if not os.path.exists(backup_path): 14 | try: 15 | with open(filepath, 'r') as src, open(backup_path, 'w') as dst: 16 | dst.write(src.read()) 17 | log_message(f"Created backup: {backup_path}") 18 | except Exception as e: 19 | log_message(f"Warning: Could not create backup for {filepath}: {e}", "WARN") 20 | 21 | def remove_optional_from_proto(directory): 22 | """Remove 'optional' keyword from protobuf files""" 23 | proto_file_pattern = re.compile(r'.*\.proto$') 24 | files_modified = 0 25 | 26 | log_message(f"Scanning protobuf files in: {directory}") 27 | 28 | for root, _, files in os.walk(directory): 29 | for file in files: 30 | if proto_file_pattern.match(file): 31 | filepath = os.path.join(root, file) 32 | 33 | try: 34 | with open(filepath, 'r') as f: 35 | content = f.read() 36 | 37 | # Check if file needs modification 38 | if 'optional ' in content: 39 | create_backup(filepath) 40 | new_content = re.sub(r'\boptional\s+', '', content) 41 | 42 | with open(filepath, 'w') as f: 43 | f.write(new_content) 44 | 45 | log_message(f"Updated protobuf: {filepath}") 46 | files_modified += 1 47 | else: 48 | log_message(f"No changes needed: {filepath}") 49 | 50 | except Exception as e: 51 | log_message(f"Error processing {filepath}: {e}", "ERROR") 52 | return False 53 | 54 | log_message(f"Modified {files_modified} protobuf files") 55 | return True 56 | 57 | def patch_rust_types_rs(rust_types_file): 58 | """Patch the Rust types.rs file to fix jitter_percent and refresh_interval_seconds type mismatches""" 59 | 60 | if not os.path.exists(rust_types_file): 61 | log_message(f"Rust types file not found: {rust_types_file}", "ERROR") 62 | return False 63 | 64 | log_message(f"Patching Rust file: {rust_types_file}") 65 | 66 | try: 67 | with open(rust_types_file, 'r') as f: 68 | content = f.read() 69 | 70 | # Check if the file needs patching 71 | jitter_pattern = r'jitter_percent:\s*strategy\.jitter_percent,' 72 | jitter_replacement = 'jitter_percent: Some(strategy.jitter_percent),' 73 | 74 | refresh_pattern = r'refresh_interval_seconds,\s*\n\s*}' 75 | refresh_replacement = 'refresh_interval_seconds: Some(refresh_interval_seconds),\n }' 76 | 77 | needs_patching = False 78 | new_content = content 79 | 80 | # Apply jitter_percent patch 81 | if re.search(jitter_pattern, content): 82 | create_backup(rust_types_file) 83 | new_content = re.sub(jitter_pattern, jitter_replacement, new_content) 84 | needs_patching = True 85 | log_message("Applied jitter_percent patch") 86 | elif 'Some(strategy.jitter_percent)' in content: 87 | log_message("jitter_percent already patched") 88 | 89 | # Apply refresh_interval_seconds patch 90 | if re.search(refresh_pattern, new_content): 91 | if not needs_patching: 92 | create_backup(rust_types_file) 93 | new_content = re.sub(refresh_pattern, refresh_replacement, new_content) 94 | needs_patching = True 95 | log_message("Applied refresh_interval_seconds patch") 96 | elif 'refresh_interval_seconds: Some(refresh_interval_seconds)' in new_content: 97 | log_message("refresh_interval_seconds already patched") 98 | 99 | if needs_patching: 100 | with open(rust_types_file, 'w') as f: 101 | f.write(new_content) 102 | log_message(f"Successfully patched Rust types file: {rust_types_file}") 103 | return True 104 | else: 105 | log_message(f"File already patched: {rust_types_file}") 106 | return True 107 | 108 | except Exception as e: 109 | log_message(f"Error patching {rust_types_file}: {e}", "ERROR") 110 | return False 111 | 112 | def verify_rust_patch(rust_types_file): 113 | """Verify that the Rust patch was applied correctly""" 114 | try: 115 | with open(rust_types_file, 'r') as f: 116 | content = f.read() 117 | 118 | # Check for the fixed patterns 119 | jitter_fixed = 'Some(strategy.jitter_percent)' in content 120 | refresh_fixed = 'refresh_interval_seconds: Some(refresh_interval_seconds)' in content 121 | 122 | if jitter_fixed and refresh_fixed: 123 | log_message("Rust patch verification: SUCCESS") 124 | return True 125 | else: 126 | missing = [] 127 | if not jitter_fixed: 128 | missing.append("jitter_percent fix") 129 | if not refresh_fixed: 130 | missing.append("refresh_interval_seconds fix") 131 | log_message(f"Rust patch verification: FAILED - Missing: {', '.join(missing)}", "ERROR") 132 | return False 133 | 134 | except Exception as e: 135 | log_message(f"Error verifying patch: {e}", "ERROR") 136 | return False 137 | 138 | def main(): 139 | """Main function to run all patching operations for protobuf and Rust files""" 140 | log_message("Starting build-time patching process for protobuf and Rust files") 141 | 142 | # Base directory (relative to script location) 143 | base_dir = Path(__file__).parent.parent 144 | 145 | # Define paths 146 | protobuf_directory = base_dir / "valkey-glide" / "glide-core" / "src" / "protobuf" 147 | rust_types_file = base_dir / "valkey-glide" / "glide-core" / "src" / "client" / "types.rs" 148 | 149 | success = True 150 | 151 | # Step 1: Remove optional from protobuf files 152 | log_message("=== Phase 1: Patching Protobuf Files ===") 153 | if protobuf_directory.exists(): 154 | if not remove_optional_from_proto(str(protobuf_directory)): 155 | success = False 156 | else: 157 | log_message(f"Protobuf directory not found: {protobuf_directory}", "ERROR") 158 | success = False 159 | 160 | # Step 2: Patch Rust jitter_percent issue 161 | log_message("=== Phase 2: Patching Rust Code ===") 162 | if rust_types_file.exists(): 163 | if not patch_rust_types_rs(str(rust_types_file)): 164 | success = False 165 | else: 166 | # Verify the patch 167 | if not verify_rust_patch(str(rust_types_file)): 168 | success = False 169 | else: 170 | log_message(f"Rust types file not found: {rust_types_file}", "ERROR") 171 | success = False 172 | 173 | # Summary 174 | if success: 175 | log_message("=== Build-time patching for protobuf and Rust files completed successfully ===") 176 | return 0 177 | else: 178 | log_message("=== Build-time patching for protobuf and Rust files completed with errors ===", "ERROR") 179 | return 1 180 | 181 | # Usage 182 | if __name__ == "__main__": 183 | sys.exit(main()) 184 | -------------------------------------------------------------------------------- /tests/TestValkeyGlide.php: -------------------------------------------------------------------------------- 1 | ". 47 | * 48 | * THIS SOFTWARE IS PROVIDED BY THE PHP DEVELOPMENT TEAM ``AS IS'' AND 49 | * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 50 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 51 | * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE PHP 52 | * DEVELOPMENT TEAM OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 53 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 54 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 55 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 56 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 57 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 58 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 59 | * OF THE POSSIBILITY OF SUCH DAMAGE. 60 | * 61 | * -------------------------------------------------------------------- 62 | * 63 | * This software consists of voluntary contributions made by many 64 | * individuals on behalf of the PHP Group. 65 | * 66 | * The PHP Group can be contacted via Email at group@php.net. 67 | * 68 | * For more information on the PHP Group and the PHP project, 69 | * please see . 70 | * 71 | * PHP includes the Zend Engine, freely available at 72 | * . 73 | */ 74 | 75 | if (file_exists(__DIR__ . '/../vendor/autoload.php')) { 76 | require_once __DIR__ . '/../vendor/autoload.php'; 77 | } 78 | 79 | require_once __DIR__ . "/TestSuite.php"; 80 | require_once __DIR__ . "/ConnectionRequestTest.php"; 81 | require_once __DIR__ . "/ValkeyGlideBaseTest.php"; 82 | require_once __DIR__ . "/ValkeyGlideClusterBaseTest.php"; 83 | require_once __DIR__ . "/ValkeyGlideTest.php"; 84 | require_once __DIR__ . "/ValkeyGlideClusterTest.php"; 85 | require_once __DIR__ . "/ValkeyGlideFeaturesTest.php"; 86 | require_once __DIR__ . "/ValkeyGlideClusterFeaturesTest.php"; 87 | require_once __DIR__ . "/ValkeyGlideBatchTest.php"; 88 | require_once __DIR__ . "/ValkeyGlideClusterBatchTest.php"; 89 | require_once __DIR__ . "/UpdateConnectionPasswordTest.php"; 90 | echo "Loading ValkeyGlide tests...\n"; 91 | function getClassArray($classes) 92 | { 93 | $result = []; 94 | 95 | if (! is_array($classes)) { 96 | $classes = [$classes]; 97 | } 98 | 99 | foreach ($classes as $class) { 100 | $result = array_merge($result, explode(',', $class)); 101 | } 102 | 103 | return array_unique( 104 | array_map(function ($v) { 105 | return strtolower($v); 106 | }, 107 | $result) 108 | ); 109 | } 110 | 111 | function getTestClass($class) 112 | { 113 | $valid_classes = [ 114 | 'connectionrequest' => 'ConnectionRequestTest', 115 | 'valkeyglide' => 'ValkeyGlideTest', 116 | 'valkeyglidecluster' => 'ValkeyGlideClusterTest', 117 | 'valkeyglideclientfeatures' => 'ValkeyGlideFeaturesTest', 118 | 'valkeyglideclusterfeatures' => 'ValkeyGlideClusterFeaturesTest', 119 | 'valkeyglideclientbatch' => 'ValkeyGlideBatchTest', 120 | 'valkeyglideclusterbatch' => 'ValkeyGlideClusterBatchTest', 121 | 'updateconnectionpassword' => 'UpdateConnectionPasswordTest' 122 | ]; 123 | 124 | /* Return early if the class is one of our built-in ones */ 125 | if (isset($valid_classes[$class])) { 126 | return $valid_classes[$class]; 127 | } 128 | 129 | /* Try to load it */ 130 | return TestSuite::loadTestClass($class); 131 | } 132 | 133 | function raHosts($host, $ports) 134 | { 135 | if (! is_array($ports)) { 136 | $ports = [6379, 6380, 6381, 6382]; 137 | } 138 | 139 | return array_map(function ($port) use ($host) { 140 | return sprintf("%s:%d", $host, $port); 141 | }, $ports); 142 | } 143 | echo "Running ValkeyGlide tests...\n"; 144 | /* Make sure errors go to stdout and are shown */ 145 | error_reporting(E_ALL); 146 | ini_set('display_errors', '1'); 147 | 148 | /* Grab options */ 149 | $opt = getopt('', ['host:', 'port:', 'class:', 'test:', 'nocolors', 'user:', 'auth:', 'tls']); 150 | 151 | /* The test class(es) we want to run */ 152 | $classes = getClassArray($opt['class'] ?? 'connectionrequest,valkeyglide,valkeyglidecluster,valkeyglideclientfeatures,valkeyglideclusterfeatures,valkeyglideclientbatch,valkeyglideclusterbatch,updateconnectionpassword'); 153 | 154 | $colorize = !isset($opt['nocolors']); 155 | 156 | /* Get our test filter if provided one */ 157 | $filter = $opt['test'] ?? null; 158 | 159 | /* Grab override host/port if it was passed */ 160 | $host = $opt['host'] ?? '127.0.0.1'; 161 | $port = $opt['port'] ?? 6379; 162 | 163 | /* Get optional username and auth (password) */ 164 | $user = $opt['user'] ?? null; 165 | $auth = $opt['auth'] ?? null; 166 | 167 | /* Check if TLS should be enabled. */ 168 | $tls = isset($opt['tls']); 169 | if (isset($opt['tls'])) { 170 | echo TestSuite::makeBold("Assuming TLS connection for client constructor feature tests.\n"); 171 | } 172 | 173 | if ($user && $auth) { 174 | $auth = [$user, $auth]; 175 | } elseif ($user && ! $auth) { 176 | echo TestSuite::makeWarning("User passed without a password!\n"); 177 | } 178 | 179 | /* Toggle colorization in our TestSuite class */ 180 | TestSuite::flagColorization($colorize); 181 | 182 | /* Let the user know this can take a bit of time */ 183 | echo "Note: these tests might take up to a minute. Don't worry :-)\n"; 184 | echo "Using PHP version " . PHP_VERSION . " (" . (PHP_INT_SIZE * 8) . " bits)\n"; 185 | 186 | foreach ($classes as $class) { 187 | $class = getTestClass($class); 188 | 189 | /* Depending on the classes being tested, run our tests on it */ 190 | echo "Testing class "; 191 | 192 | echo TestSuite::makeBold($class) . "\n"; 193 | 194 | if (TestSuite::run("$class", $filter, $host, $port, $auth, $tls)) { 195 | exit(1); 196 | } 197 | } 198 | 199 | /* Success */ 200 | exit(0); 201 | --------------------------------------------------------------------------------