From 73a2bab283bb4634af6e7ecbe67ddbe46d0ab9ce Mon Sep 17 00:00:00 2001 From: Mas Kubo Date: Wed, 29 Apr 2026 15:53:35 -0700 Subject: [PATCH 1/3] refactor: remove PreExecCheck leaky abstraction and fix sync-over-async MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ValkeyBatch.PreExecCheck() was a virtual hook that existed solely to support transaction preconditions. Batches have no concept of preconditions, so this was transaction-specific logic leaking into the base class. Additionally, ValkeyTransaction.PreExecCheck() called ExecuteImpl().GetAwaiter().GetResult() — the last remaining sync-over-async call in the codebase — which risks deadlocks in environments with a synchronization context (e.g. ASP.NET). Changes: - Remove virtual PreExecCheck() from ValkeyBatch and the if (PreExecCheck()) guard from ExecuteImpl(). ExecuteImpl() now unconditionally executes the batch, which is the correct behaviour for ValkeyBatch (conditions are a transaction concern only). - Move condition evaluation into ValkeyTransaction.ExecuteAsync() directly, using await instead of .GetAwaiter().GetResult(). - Short-circuit early (set _tcs result to null and return false) as soon as any condition fails, avoiding unnecessary work. - Rename the inner batch variable from b to conditionBatch for clarity. Fixes #266 Signed-off-by: currantw --- sources/Valkey.Glide/Abstract/ValkeyBatch.cs | 25 +++++---------- .../Abstract/ValkeyTransaction.cs | 31 +++++++++++-------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/sources/Valkey.Glide/Abstract/ValkeyBatch.cs b/sources/Valkey.Glide/Abstract/ValkeyBatch.cs index 3e7f496b..fa4e5efc 100644 --- a/sources/Valkey.Glide/Abstract/ValkeyBatch.cs +++ b/sources/Valkey.Glide/Abstract/ValkeyBatch.cs @@ -30,8 +30,6 @@ internal override async Task Command(Cmd command, Route? route = : (T)batchResult[idx]!; } - protected virtual bool PreExecCheck() => true; - protected async Task ExecuteImpl() { if (_tcs.Task.Status == TaskStatus.RanToCompletion) @@ -39,23 +37,16 @@ protected async Task ExecuteImpl() // a batch is already executed return; } - if (PreExecCheck()) - { - if (_commands.Count == 0) - { - _tcs.SetResult([]); - return; - } - Batch b = new(_isAtomic); - b.Commands.AddRange(_commands); - - object?[]? res = await _client.Batch(b, false); - _tcs.SetResult(res); - } - else + if (_commands.Count == 0) { - _tcs.SetResult(null); + _tcs.SetResult([]); + return; } + Batch b = new(_isAtomic); + b.Commands.AddRange(_commands); + + object?[]? res = await _client.Batch(b, false); + _tcs.SetResult(res); } public void Execute() => ExecuteImpl().GetAwaiter().GetResult(); diff --git a/sources/Valkey.Glide/Abstract/ValkeyTransaction.cs b/sources/Valkey.Glide/Abstract/ValkeyTransaction.cs index 271b919d..4b9e5414 100644 --- a/sources/Valkey.Glide/Abstract/ValkeyTransaction.cs +++ b/sources/Valkey.Glide/Abstract/ValkeyTransaction.cs @@ -27,28 +27,33 @@ public bool Execute(CommandFlags flags = CommandFlags.None) public async Task ExecuteAsync(CommandFlags flags = CommandFlags.None) { GuardClauses.ThrowIfCommandFlags(flags); - await ExecuteImpl(); - return _tcs.Task.Result is not null; - } - protected override bool PreExecCheck() - { - bool allConditionsPassed = true; + // Evaluate all conditions asynchronously before submitting the transaction. + // This replaces the former sync-over-async PreExecCheck() override in the + // base class, keeping transaction-specific logic here rather than leaking + // it into ValkeyBatch. foreach (ConditionResult condition in _conditions) { - // We can't access internals of batch, but we can create a transaction, "downcast" it to batch and patch it - ValkeyTransaction b = new(_client) + // Execute the condition commands in a non-atomic batch to check the + // current state of the watched keys. + ValkeyTransaction conditionBatch = new(_client) { _isAtomic = false }; - b._commands.AddRange(condition.Condition.CreateCommands()); - b.ExecuteImpl().GetAwaiter().GetResult(); - condition.WasSatisfied = condition.Condition.Validate(b._tcs.Task.Result); + conditionBatch._commands.AddRange(condition.Condition.CreateCommands()); + await conditionBatch.ExecuteImpl(); + + condition.WasSatisfied = condition.Condition.Validate(conditionBatch._tcs.Task.Result); if (!condition.WasSatisfied) { - allConditionsPassed = false; + // At least one condition failed — cancel the transaction without + // submitting the MULTI/EXEC block. + _tcs.SetResult(null); + return false; } } - return allConditionsPassed; + + await ExecuteImpl(); + return _tcs.Task.Result is not null; } } From c6935603d8817b81e4abe90366bd64e215dc39de Mon Sep 17 00:00:00 2001 From: Taylor Curran Date: Wed, 13 May 2026 17:24:04 -0700 Subject: [PATCH 2/3] Cherry-pick `release-1.1` CHANGELOG into `main` (#403) Signed-off-by: currantw --- CHANGELOG.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16b0d2d1..8bf2bd8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,27 @@ # Changelog +All notable changes to the Valkey GLIDE C# client will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) + ## 1.1.0 -### Changes +### Added + +- Valkey JSON (JSON.*) command support for clients and batches (#358) +- Valkey Search (FT.*) command support for clients (#225) +- Client-side caching with TTL-based expiration, LRU/LFU eviction policies, and cache metrics API (#330) +- Compression support for CustomCommand with incompatible command detection and improved error messages (#348) + +### Security -- Add client-side caching support with TTL-based expiration, LRU/LFU eviction policies, and cache metrics (#330) +- Remove credential leakage vectors from FFI debug output (#371) -## 0.10.0 +## 1.0.0 -### Changes +### Added -- Add support for Windows CI and testing with WSL (#184) -- Add StackExchange.Redis compatible pub/sub API (#202) -- Add transparent compression support with Zstd and LZ4 backends (#213) -- Add Valkey Search command support (#225) +- StackExchange.Redis compatible pub/sub API (#202) +- Transparent compression support with Zstd and LZ4 backends (#213) +- Windows CI and testing with WSL (#184) From a05c6881fae989731f09d5fe48cf931e62367b11 Mon Sep 17 00:00:00 2001 From: Taylor Curran Date: Wed, 13 May 2026 17:24:30 -0700 Subject: [PATCH 3/3] refactor(cd): Test package locally before publishing to NuGet (#402) Signed-off-by: currantw --- .github/workflows/cd.yml | 61 ++++++---------------------- .github/workflows/relist-package.yml | 36 ---------------- 2 files changed, 12 insertions(+), 85 deletions(-) delete mode 100644 .github/workflows/relist-package.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 5e79d4f0..9b71f949 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -43,7 +43,7 @@ jobs: working-directory: .github/json_matrices # Select all VM runners (non-container) — each produces a unique native binary for the NuGet package. run: | - PLATFORM_MATRIX=$(jq -c '[.[] | select(has("IMAGE") | not)]' < os-matrix.json) + PLATFORM_MATRIX=$(jq -c '[.[] | select(has("IMAGE") | not)]' < os-matrix.json) echo "PLATFORM_MATRIX=$PLATFORM_MATRIX" | tee -a "$GITHUB_OUTPUT" set-release-version: @@ -172,10 +172,20 @@ jobs: if-no-files-found: error path: sources/Valkey.Glide/bin/Release/Valkey.Glide.*nupkg + # Test the package locally before publishing to nuget.org. + test: + needs: [build-package-to-publish, set-release-version] + uses: ./.github/workflows/test.yml + with: + profile: full + package-version: ${{ needs.set-release-version.outputs.RELEASE_VERSION }} + package-artifact-name: package + secrets: inherit + publish: environment: Release if: ${{ (inputs.nuget_publish == true || github.event_name == 'push') && github.repository_owner == 'valkey-io' }} - needs: [build-package-to-publish, set-release-version] + needs: [test, set-release-version] runs-on: ubuntu-latest env: RELEASE_VERSION: ${{ needs.set-release-version.outputs.RELEASE_VERSION }} @@ -191,50 +201,3 @@ jobs: dotnet nuget push "Valkey.Glide.$RELEASE_VERSION.nupkg" \ --api-key ${{ secrets.NUGET_API_KEY }} \ --source https://api.nuget.org/v3/index.json - - - name: Wait for package to be ready for download - run: | - dotnet new console --name temp-check --output . --framework net8.0 - TIMEOUT=600 # 10 minutes - INTERVAL=10 # 10 seconds - ELAPSED=0 - - while [ "$ELAPSED" -lt "$TIMEOUT" ]; do - echo "Checking availability... (${ELAPSED}s elapsed)" - if dotnet add package Valkey.Glide --version "$RELEASE_VERSION" ; then - echo "Package is now available!" - exit 0 - fi - sleep "$INTERVAL" - ELAPSED=$((ELAPSED + INTERVAL)) - dotnet nuget locals all --clear - done - - echo "Timeout: Package is not available after ${TIMEOUT}s" - exit 1 - - # Test the published package as users would install it from nuget.org. - # On dry-run (publish skipped), tests use the locally-built .nupkg artifact instead. - # If tests fail after a real publish, remove-package-if-validation-fails unlists the package. - test: - needs: [publish, set-release-version] - uses: ./.github/workflows/test.yml - with: - profile: full - package-version: ${{ needs.set-release-version.outputs.RELEASE_VERSION }} - package-artifact-name: ${{ needs.publish.result == 'skipped' && 'package' || '' }} - secrets: inherit - - remove-package-if-validation-fails: - needs: [publish, test, set-release-version] - if: ${{ !cancelled() }} - runs-on: ubuntu-latest - env: - RELEASE_VERSION: ${{ needs.set-release-version.outputs.RELEASE_VERSION }} - steps: - - name: Remove package from NuGet due to test failures - if: ${{ (needs.test.result == 'failure' && needs.publish.result == 'success') || needs.publish.result == 'failure' }} - run: | - dotnet nuget delete Valkey.Glide "$RELEASE_VERSION" \ - --api-key ${{ secrets.NUGET_API_KEY }} \ - --source https://api.nuget.org/v3/index.json --non-interactive diff --git a/.github/workflows/relist-package.yml b/.github/workflows/relist-package.yml deleted file mode 100644 index bbfcc7a2..00000000 --- a/.github/workflows/relist-package.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Re-list NuGet Package - -on: - workflow_dispatch: - inputs: - version: - description: "Package version to re-list" - required: true - -permissions: - contents: read - -jobs: - relist: - environment: Release - runs-on: ubuntu-latest - steps: - - name: Re-list Valkey.Glide ${{ inputs.version }} - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - PACKAGE_VERSION: ${{ inputs.version }} - run: | - echo "Re-listing Valkey.Glide version $PACKAGE_VERSION..." - HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ - -H "X-NuGet-ApiKey: $NUGET_API_KEY" \ - -H "Content-Length: 0" \ - "https://www.nuget.org/api/v2/package/Valkey.Glide/$PACKAGE_VERSION") - - echo "Response status: $HTTP_STATUS" - - if [ "$HTTP_STATUS" -eq 200 ]; then - echo "✅ Successfully re-listed Valkey.Glide $PACKAGE_VERSION" - else - echo "❌ Failed to re-list. HTTP status: $HTTP_STATUS" - exit 1 - fi