Inline filtering for vector sets #10767
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Garnet .NET CI | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: | |
| - main | |
| - dev | |
| pull_request: | |
| branches: | |
| - main | |
| - dev | |
| env: | |
| DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 | |
| DOTNET_NOLOGO: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| changes: | |
| name: Check for changes | |
| runs-on: ubuntu-latest # don't need matrix to test where the changes were made in code | |
| permissions: | |
| pull-requests: read | |
| contents: read | |
| outputs: | |
| tsavorite: ${{ steps.filter.outputs.tsavorite }} | |
| website: ${{ steps.filter.outputs.website }} | |
| garnet: ${{ steps.filter.outputs.garnet }} | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v6 | |
| - name: Apply filter | |
| uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d #v4 for security reasons have pinned tag (commit SHA) for 3rd party | |
| id: filter | |
| with: | |
| filters: | | |
| tsavorite: | |
| - 'libs/storage/Tsavorite/**' | |
| website: | |
| - 'website/**' | |
| garnet: | |
| - '!((*.md)|(website/**))' | |
| format-garnet: | |
| name: Format Garnet | |
| needs: changes | |
| runs-on: ubuntu-latest | |
| if: needs.changes.outputs.garnet == 'true' | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v6 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v5 | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.nuget/packages | |
| key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', 'Directory.Packages.props') }} | |
| restore-keys: nuget-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: dotnet restore Garnet.slnx | |
| - name: Check style format | |
| run: dotnet format Garnet.slnx --no-restore --verify-no-changes --verbosity diagnostic | |
| format-tsavorite: | |
| name: Format Tsavorite | |
| needs: changes | |
| runs-on: ubuntu-latest | |
| if: needs.changes.outputs.tsavorite == 'true' | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v6 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v5 | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.nuget/packages | |
| key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', 'Directory.Packages.props') }} | |
| restore-keys: nuget-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: dotnet restore libs/storage/Tsavorite/cs/Tsavorite.slnx | |
| - name: Check style format | |
| run: dotnet format libs/storage/Tsavorite/cs/Tsavorite.slnx --no-restore --verify-no-changes --verbosity diagnostic | |
| # Job to build Garnet code (once per os/configuration) | |
| build-garnet: | |
| name: Build Garnet | |
| needs: [changes] | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ ubuntu-latest, windows-latest ] | |
| configuration: [ 'Debug', 'Release' ] | |
| if: needs.changes.outputs.garnet == 'true' | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v6 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v5 | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.nuget/packages | |
| key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', 'Directory.Packages.props') }} | |
| restore-keys: nuget-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: dotnet restore | |
| - name: Build Garnet | |
| run: dotnet build --configuration ${{ matrix.configuration }} --no-restore | |
| # Job to test Garnet standalone code | |
| test-garnet-standalone: | |
| name: Garnet Standalone | |
| needs: [changes, build-garnet] | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ ubuntu-latest, windows-latest ] | |
| framework: [ 'net8.0' , 'net10.0'] | |
| configuration: [ 'Debug', 'Release' ] | |
| test: [ 'Garnet.test', 'Garnet.test.collections', 'Garnet.test.acl', 'Garnet.test.scripting', 'Garnet.test.complexstring', 'Garnet.test.vectorset', 'Garnet.test.rangeindex', 'Garnet.test.extensions' ] | |
| if: needs.changes.outputs.garnet == 'true' | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v6 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v5 | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.nuget/packages | |
| key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', 'Directory.Packages.props') }} | |
| restore-keys: nuget-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: dotnet restore | |
| - name: Run tests ${{ matrix.test }} | |
| run: dotnet test test/standalone/${{ matrix.test }} -f ${{ matrix.framework }} --configuration ${{ matrix.configuration }} --logger "console;verbosity=detailed" --logger trx --results-directory "GarnetTestResults-${{ matrix.os }}-${{ matrix.framework }}-${{ matrix.configuration }}-${{ matrix.test }}" -- NUnit.DisplayName=FullName | |
| timeout-minutes: 45 | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: dotnet-standalone-results-${{ matrix.os }}-${{ matrix.framework }}-${{ matrix.configuration }}-${{ matrix.test }} | |
| path: GarnetTestResults-${{ matrix.os }}-${{ matrix.framework }}-${{ matrix.configuration }}-${{ matrix.test }} | |
| if: ${{ always() }} | |
| # Job to test Garnet cluster code | |
| test-garnet-cluster: | |
| name: Garnet Cluster | |
| needs: [changes, build-garnet] | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ ubuntu-latest, windows-latest ] | |
| framework: [ 'net8.0' , 'net10.0'] | |
| configuration: [ 'Debug', 'Release' ] | |
| test: [ 'Garnet.test.cluster', 'Garnet.test.cluster.migrate', 'Garnet.test.cluster.migrate.rangeindex', 'Garnet.test.cluster.replication', 'Garnet.test.cluster.replication.tls', 'Garnet.test.cluster.replication.disklesssync', 'Garnet.test.cluster.replication.rangeindex', 'Garnet.test.cluster.vectorsets', 'Garnet.test.cluster.multilog' ] | |
| if: needs.changes.outputs.garnet == 'true' | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v6 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v5 | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.nuget/packages | |
| key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', 'Directory.Packages.props') }} | |
| restore-keys: nuget-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: dotnet restore | |
| - name: Run tests ${{ matrix.test }} | |
| run: dotnet test test/cluster/${{ matrix.test }} -f ${{ matrix.framework }} --configuration ${{ matrix.configuration }} --logger "console;verbosity=detailed" --logger trx --blame-crash --blame-crash-dump-type full --blame-hang-timeout 30m --blame-hang-dump-type full --results-directory "GarnetTestResults-${{ matrix.os }}-${{ matrix.framework }}-${{ matrix.configuration }}-${{ matrix.test }}" -- NUnit.DisplayName=FullName | |
| timeout-minutes: 45 | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: dotnet-cluster-results-${{ matrix.os }}-${{ matrix.framework }}-${{ matrix.configuration }}-${{ matrix.test }} | |
| path: GarnetTestResults-${{ matrix.os }}-${{ matrix.framework }}-${{ matrix.configuration }}-${{ matrix.test }} | |
| if: ${{ always() }} | |
| # Job to build Tsavorite code (once per os/configuration) | |
| build-tsavorite: | |
| name: Build Tsavorite | |
| needs: [changes] | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ ubuntu-latest, windows-latest ] | |
| configuration: [ 'Debug', 'Release' ] | |
| if: needs.changes.outputs.tsavorite == 'true' | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v6 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v5 | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.nuget/packages | |
| key: nuget-tsavorite-${{ runner.os }}-${{ hashFiles('libs/storage/Tsavorite/**/*.csproj', 'Directory.Packages.props') }} | |
| restore-keys: nuget-tsavorite-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: dotnet restore libs/storage/Tsavorite/cs/Tsavorite.slnx | |
| - name: Build Tsavorite | |
| run: dotnet build libs/storage/Tsavorite/cs/Tsavorite.slnx --configuration ${{ matrix.configuration }} --no-restore | |
| # Job to test Tsavorite code | |
| test-tsavorite: | |
| name: Tsavorite | |
| needs: [changes, build-tsavorite] | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ ubuntu-latest, windows-latest ] | |
| framework: [ 'net8.0', 'net10.0' ] | |
| configuration: [ 'Debug', 'Release' ] | |
| test: [ 'Tsavorite.test', 'Tsavorite.test.recordops', 'Tsavorite.test.session', 'Tsavorite.test.session.context', 'Tsavorite.test.hlog', 'Tsavorite.test.recovery' ] | |
| if: needs.changes.outputs.tsavorite == 'true' | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v6 | |
| - name: Set environment variable for Linux | |
| run: echo "RunAzureTests=yes" >> $GITHUB_ENV | |
| if: ${{ matrix.os == 'ubuntu-latest' && matrix.test != 'Tsavorite.test.recordops' && matrix.test != 'Tsavorite.test.session' && matrix.test != 'Tsavorite.test.session.context' && matrix.test != 'Tsavorite.test.recovery' }} | |
| - name: Set environment variable for Windows | |
| run: echo ("RunAzureTests=yes") >> $env:GITHUB_ENV | |
| if: ${{ matrix.os == 'windows-latest' && matrix.test != 'Tsavorite.test.recordops' && matrix.test != 'Tsavorite.test.session' && matrix.test != 'Tsavorite.test.session.context' && matrix.test != 'Tsavorite.test.recovery' }} | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v5 | |
| - name: Setup Node.js for Azurite | |
| if: ${{ matrix.test != 'Tsavorite.test.recordops' && matrix.test != 'Tsavorite.test.session' && matrix.test != 'Tsavorite.test.session.context' && matrix.test != 'Tsavorite.test.recovery' }} | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| - name: Cache Azurite | |
| if: ${{ matrix.test != 'Tsavorite.test.recordops' && matrix.test != 'Tsavorite.test.session' && matrix.test != 'Tsavorite.test.session.context' && matrix.test != 'Tsavorite.test.recovery' }} | |
| uses: actions/cache@v5 | |
| with: | |
| path: ${{ runner.os == 'Windows' && '%APPDATA%\npm-cache' || '~/.npm' }} | |
| key: azurite-${{ runner.os }} | |
| - name: Install and Run Azurite | |
| if: ${{ matrix.test != 'Tsavorite.test.recordops' && matrix.test != 'Tsavorite.test.session' && matrix.test != 'Tsavorite.test.session.context' && matrix.test != 'Tsavorite.test.recovery' }} | |
| shell: bash | |
| run: | | |
| npm install -g azurite | |
| azurite --skipApiVersionCheck & | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.nuget/packages | |
| key: nuget-tsavorite-${{ runner.os }}-${{ hashFiles('libs/storage/Tsavorite/**/*.csproj', 'Directory.Packages.props') }} | |
| restore-keys: nuget-tsavorite-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: dotnet restore libs/storage/Tsavorite/cs/Tsavorite.slnx | |
| - name: Resolve Tsavorite test directory | |
| shell: pwsh | |
| run: | | |
| $tsavoriteDirMap = @{ | |
| 'Tsavorite.test' = 'libs/storage/Tsavorite/cs/test' | |
| 'Tsavorite.test.recordops' = 'libs/storage/Tsavorite/cs/test/test.recordops' | |
| 'Tsavorite.test.session' = 'libs/storage/Tsavorite/cs/test/test.session' | |
| 'Tsavorite.test.session.context' = 'libs/storage/Tsavorite/cs/test/test.session.context' | |
| 'Tsavorite.test.hlog' = 'libs/storage/Tsavorite/cs/test/test.hlog' | |
| 'Tsavorite.test.recovery' = 'libs/storage/Tsavorite/cs/test/test.recovery' | |
| } | |
| $dir = $tsavoriteDirMap['${{ matrix.test }}'] | |
| echo "TSAVORITE_TEST_DIR=$dir" >> $env:GITHUB_ENV | |
| - name: Run tests ${{ matrix.test }} | |
| run: dotnet test ${{ env.TSAVORITE_TEST_DIR }} -f ${{ matrix.framework }} --configuration ${{ matrix.configuration }} --logger "console;verbosity=detailed" --logger trx --results-directory "TsavoriteTestResults-${{ matrix.os }}-${{ matrix.framework }}-${{ matrix.configuration }}-${{ matrix.test }}" -- NUnit.DisplayName=FullName | |
| timeout-minutes: 45 | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: dotnet-tsavorite-results-${{ matrix.os }}-${{ matrix.framework }}-${{ matrix.configuration }}-${{ matrix.test }} | |
| path: TsavoriteTestResults-${{ matrix.os }}-${{ matrix.framework }}-${{ matrix.configuration }}-${{ matrix.test }} | |
| if: ${{ always() }} | |
| build-website: | |
| name: Build Website | |
| needs: changes | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: website | |
| if: needs.changes.outputs.website == 'true' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: yarn | |
| cache-dependency-path: ./website/yarn.lock | |
| - name: Install dependencies | |
| run: yarn install --frozen-lockfile | |
| - name: Build website | |
| run: yarn build | |
| # Job to generate combined test summaries per category | |
| test-summary: | |
| name: Test Summary | |
| runs-on: ubuntu-latest | |
| needs: [changes, test-garnet-standalone, test-garnet-cluster, test-tsavorite] | |
| if: always() && (needs.test-garnet-standalone.result != 'skipped' || needs.test-garnet-cluster.result != 'skipped' || needs.test-tsavorite.result != 'skipped') | |
| steps: | |
| - name: Download standalone test results | |
| if: needs.test-garnet-standalone.result != 'skipped' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: dotnet-standalone-results-* | |
| path: results/standalone | |
| merge-multiple: true | |
| - name: Download cluster test results | |
| if: needs.test-garnet-cluster.result != 'skipped' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: dotnet-cluster-results-* | |
| path: results/cluster | |
| merge-multiple: true | |
| - name: Download Tsavorite test results | |
| if: needs.test-tsavorite.result != 'skipped' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: dotnet-tsavorite-results-* | |
| path: results/tsavorite | |
| merge-multiple: true | |
| - name: Generate summaries | |
| shell: pwsh | |
| run: | | |
| function Write-CategorySummary($category, $dir) { | |
| $trxFiles = Get-ChildItem -Path $dir -Filter "*.trx" -Recurse -ErrorAction SilentlyContinue | |
| if (-not $trxFiles -or $trxFiles.Count -eq 0) { return } | |
| $totalTests = 0; $passed = 0; $failed = 0; $skipped = 0 | |
| $durations = @() | |
| $failedTests = @() | |
| foreach ($trx in $trxFiles) { | |
| $xml = [System.Xml.XmlDocument]::new() | |
| try { $xml.Load($trx.FullName) } | |
| catch { | |
| Write-Warning "Skipping malformed TRX file: $($trx.FullName) - $_" | |
| continue | |
| } | |
| $ns = @{ t = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010" } | |
| $results = Select-Xml -Xml $xml -XPath "//t:UnitTestResult" -Namespace $ns | |
| foreach ($r in $results) { | |
| $totalTests++ | |
| $outcome = $r.Node.outcome | |
| if ($outcome -eq "Passed") { $passed++ } | |
| elseif ($outcome -eq "Failed") { | |
| $failed++ | |
| $dur = $r.Node.duration | |
| $durSec = 0 | |
| if ($dur) { try { $durSec = [TimeSpan]::Parse($dur).TotalSeconds } catch { } } | |
| $failedTests += @{ Name = $r.Node.testName; Seconds = $durSec } | |
| } | |
| else { $skipped++ } | |
| $dur = $r.Node.duration | |
| if ($dur) { | |
| try { $ts = [TimeSpan]::Parse($dur); $durations += @{ Name = $r.Node.testName; Seconds = $ts.TotalSeconds } } | |
| catch { } | |
| } | |
| } | |
| } | |
| # Compute total duration as sum of all individual test durations | |
| $totalSec = ($durations | ForEach-Object { $_.Seconds } | Measure-Object -Sum).Sum | |
| if ($totalSec -gt 0) { | |
| $totalDurStr = "{0}m {1:D2}s" -f [int][math]::Floor($totalSec / 60), [int]($totalSec % 60) | |
| } else { $totalDurStr = "N/A" } | |
| $buckets = @( | |
| @{ Label = "< 1s"; Min = 0; Max = 1 }, | |
| @{ Label = "1s - 5s"; Min = 1; Max = 5 }, | |
| @{ Label = "5s - 15s"; Min = 5; Max = 15 }, | |
| @{ Label = "15s - 30s"; Min = 15; Max = 30 }, | |
| @{ Label = "30s - 60s"; Min = 30; Max = 60 }, | |
| @{ Label = "> 60s"; Min = 60; Max = [double]::MaxValue } | |
| ) | |
| $counts = @(0,0,0,0,0,0) | |
| foreach ($d in $durations) { | |
| for ($i = 0; $i -lt $buckets.Count; $i++) { | |
| if ($d.Seconds -ge $buckets[$i].Min -and $d.Seconds -lt $buckets[$i].Max) { $counts[$i]++; break } | |
| } | |
| } | |
| $maxCount = ($counts | Measure-Object -Maximum).Maximum | |
| if ($maxCount -eq 0) { $maxCount = 1 } | |
| $sb = [System.Text.StringBuilder]::new() | |
| [void]$sb.AppendLine("## 🧪 $category") | |
| [void]$sb.AppendLine("") | |
| [void]$sb.AppendLine("⏱️ **Total: $totalDurStr** · $totalTests tests (✅ $passed · ❌ $failed · ⏭️ $skipped)") | |
| [void]$sb.AppendLine("") | |
| # Failed tests section (only if there are failures) | |
| if ($failedTests.Count -gt 0) { | |
| [void]$sb.AppendLine("### ❌ Failed Tests") | |
| [void]$sb.AppendLine("") | |
| [void]$sb.AppendLine("| Test | Duration |") | |
| [void]$sb.AppendLine("|------|----------|") | |
| foreach ($t in $failedTests) { | |
| $durStr = "{0:F1}s" -f $t.Seconds | |
| [void]$sb.AppendLine("| $($t.Name) | $durStr |") | |
| } | |
| [void]$sb.AppendLine("") | |
| } | |
| [void]$sb.AppendLine("### Distribution") | |
| [void]$sb.AppendLine("") | |
| [void]$sb.AppendLine("| Duration | Count | |") | |
| [void]$sb.AppendLine("|----------|------:|--|") | |
| for ($i = 0; $i -lt $buckets.Count; $i++) { | |
| $barLen = [int][math]::Round(($counts[$i] / $maxCount) * 20) | |
| if ($barLen -eq 0 -and $counts[$i] -gt 0) { $barLen = 1 } | |
| $bar = "█" * $barLen | |
| [void]$sb.AppendLine("| $($buckets[$i].Label) | $($counts[$i]) | $bar |") | |
| } | |
| $top10 = $durations | Sort-Object { $_.Seconds } -Descending | Select-Object -First 10 | |
| if ($top10.Count -gt 0) { | |
| [void]$sb.AppendLine("") | |
| [void]$sb.AppendLine("### 🐢 Top 10 Slowest") | |
| [void]$sb.AppendLine("") | |
| [void]$sb.AppendLine("| Test | Duration |") | |
| [void]$sb.AppendLine("|------|----------|") | |
| foreach ($t in $top10) { | |
| $durStr = "{0:F1}s" -f $t.Seconds | |
| [void]$sb.AppendLine("| $($t.Name) | $durStr |") | |
| } | |
| } | |
| [void]$sb.AppendLine("") | |
| $sb.ToString() >> $env:GITHUB_STEP_SUMMARY | |
| } | |
| # Generate per-category summaries | |
| if (Test-Path "results/standalone") { | |
| Write-CategorySummary "Garnet Standalone" "results/standalone" | |
| } | |
| if (Test-Path "results/cluster") { | |
| Write-CategorySummary "Garnet Cluster" "results/cluster" | |
| } | |
| if (Test-Path "results/tsavorite") { | |
| Write-CategorySummary "Tsavorite" "results/tsavorite" | |
| } | |
| pipeline-success: | |
| name: Garnet CI (Complete) | |
| runs-on: ubuntu-latest | |
| needs: [changes, format-garnet, format-tsavorite, build-garnet, test-garnet-standalone, test-garnet-cluster, build-tsavorite, test-tsavorite, build-website ] | |
| steps: | |
| - run: echo Done! | |
| if: ${{ !(failure() || cancelled()) }} |