From b84db69eb7c68359c481cf0e757cbafe2d57e63e Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Tue, 21 Jan 2025 09:55:28 -0800 Subject: [PATCH] improve handling of negative durations from variable expiration A zero or negative duration returned from the user's Expiry is treated as an immediate expiration. A negative value was being hashed into a future slot in the second wheel, so it delayed the eviction until that bucket is flushed. This now forces the entry to the current time's bucket so that it gets handled immediately after being added. I suspect that if a Long.MIN_VALUE was used that there might have been an overflow. I didn't investigate that scenario or its impact, and instead focused on more robust handling and tests. --- .github/workflows/actionlint.yml | 4 +- .github/workflows/analysis.yml | 6 +- .github/workflows/benchmarks.yml | 2 +- .github/workflows/build.yml | 10 +- .github/workflows/codacy.yml | 4 +- .github/workflows/codeql.yml | 10 +- .github/workflows/dependency-check.yml | 4 +- .github/workflows/dependency-review.yml | 2 +- .../dependency-submission-pr-retreive.yml | 2 +- .../dependency-submission-pr-submit.yml | 2 +- .github/workflows/dependency-submission.yml | 2 +- .github/workflows/devskim.yml | 4 +- .github/workflows/examples.yml | 2 +- .github/workflows/gitleaks.yml | 2 +- .../workflows/gradle-wrapper-validation.yml | 2 +- .github/workflows/qodana.yml | 4 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecards-analysis.yml | 4 +- .github/workflows/semgrep.yml | 2 +- .github/workflows/snyk.yml | 2 +- .github/workflows/spelling.yml | 4 +- .github/workflows/trivy.yml | 4 +- .../caffeine/cache/BoundedLocalCache.java | 10 +- .../benmanes/caffeine/cache/TimerWheel.java | 8 +- .../caffeine/cache/ExpireAfterVarTest.java | 180 ++++++++++++++++++ .../caffeine/cache/ExpireAfterWriteTest.java | 4 +- .../caffeine/cache/TimerWheelTest.java | 12 ++ .../cache/testing/CacheGenerator.java | 2 + gradle/libs.versions.toml | 4 +- 29 files changed, 250 insertions(+), 50 deletions(-) diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml index 45c73051a7..4e499308eb 100644 --- a/.github/workflows/actionlint.yml +++ b/.github/workflows/actionlint.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -16,7 +16,7 @@ jobs: github.com:443 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: actionlint - uses: reviewdog/action-actionlint@f3dcc52bc6039e5d736486952379dce3e869e8a2 # v1.63.0 + uses: reviewdog/action-actionlint@abd537417cf4991e1ba8e21a67b1119f4f53b8e0 # v1.64.1 env: SHELLCHECK_OPTS: -e SC2001 -e SC2035 -e SC2046 -e SC2061 -e SC2086 -e SC2156 with: diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 210fed07ab..0c6af7f8ba 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -26,7 +26,7 @@ jobs: JAVA_VERSION: 23 steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -45,7 +45,7 @@ jobs: JAVA_VERSION: 23 steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -64,7 +64,7 @@ jobs: JAVA_VERSION: 23 steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 5418b43ef6..f611201797 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -16,7 +16,7 @@ jobs: JAVA_VERSION: ${{ matrix.java }} steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 020eb67426..033d0ac6b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: JAVA_VERSION: ${{ matrix.java }} steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -166,7 +166,7 @@ jobs: JAVA_VERSION: ${{ matrix.java }} steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -209,7 +209,7 @@ jobs: if: (github.event_name == 'push') && (github.event.repository.fork == false) steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -287,7 +287,7 @@ jobs: checks: write steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -341,7 +341,7 @@ jobs: id-token: write steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml index 434ce64d1f..ce8c55e457 100644 --- a/.github/workflows/codacy.yml +++ b/.github/workflows/codacy.yml @@ -13,7 +13,7 @@ jobs: if: github.event.repository.fork == false steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -47,7 +47,7 @@ jobs: if: steps.check_files.outputs.files_exists == 'true' run: jq -c '.runs |= unique_by({tool, invocations, results})' < results.sarif > codacy.sarif - name: Upload result to GitHub Code Scanning - uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 if: steps.check_files.outputs.files_exists == 'true' continue-on-error: true with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 61db4a344a..ab7b775400 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -28,7 +28,7 @@ jobs: language: [ actions, java ] steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -56,12 +56,12 @@ jobs: java: ${{ env.JAVA_VERSION }} cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Initialize CodeQL (Actions) - uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/init@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 if: ${{ matrix.language == 'actions' }} with: languages: actions - name: Initialize CodeQL (Java) - uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/init@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 if: ${{ matrix.language == 'java' }} with: queries: > @@ -78,6 +78,6 @@ jobs: config: | threat-models: local - name: Autobuild - uses: github/codeql-action/autobuild@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/autobuild@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/analyze@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml index c086615f52..12615a4926 100644 --- a/.github/workflows/dependency-check.yml +++ b/.github/workflows/dependency-check.yml @@ -22,7 +22,7 @@ jobs: && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -61,7 +61,7 @@ jobs: with: files: build/reports/dependency-check-report.sarif - name: Upload result to GitHub Code Scanning - uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 if: steps.check_files.outputs.files_exists == 'true' with: sarif_file: build/reports/dependency-check-report.sarif diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 8d2085560f..e5306c673c 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -10,7 +10,7 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/dependency-submission-pr-retreive.yml b/.github/workflows/dependency-submission-pr-retreive.yml index 8e471dd520..ab334f60c1 100644 --- a/.github/workflows/dependency-submission-pr-retreive.yml +++ b/.github/workflows/dependency-submission-pr-retreive.yml @@ -16,7 +16,7 @@ jobs: contents: write steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/dependency-submission-pr-submit.yml b/.github/workflows/dependency-submission-pr-submit.yml index 3af24d0bb3..2040aae50a 100644 --- a/.github/workflows/dependency-submission-pr-submit.yml +++ b/.github/workflows/dependency-submission-pr-submit.yml @@ -13,7 +13,7 @@ jobs: contents: read steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index bf01e71cf4..91e7e8190f 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -13,7 +13,7 @@ jobs: contents: write steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml index 470addee25..81b5fe58a8 100644 --- a/.github/workflows/devskim.yml +++ b/.github/workflows/devskim.yml @@ -19,7 +19,7 @@ jobs: security-events: write steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -31,6 +31,6 @@ jobs: - name: Run DevSkim scanner uses: microsoft/DevSkim-Action@914fa647b406c387000300b2f09bb28691be2b6d # v1.0.14 - name: Upload DevSkim scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: devskim-results.sarif diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 3be0743b5d..12523f8e3d 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index 8e273202ef..e1213dd384 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 6e77474b01..eca4be02c4 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/qodana.yml b/.github/workflows/qodana.yml index 63b15d0dbc..5c6b25958e 100644 --- a/.github/workflows/qodana.yml +++ b/.github/workflows/qodana.yml @@ -19,7 +19,7 @@ jobs: && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -70,6 +70,6 @@ jobs: upload-result: true github-token: ${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF file for GitHub Advanced Security Dashboard - uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 171f4d7c5a..813a9c553a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: id-token: write steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: audit diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index 65944a1b7c..9cf58e87a0 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -20,7 +20,7 @@ jobs: if: github.event.repository.fork == false steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -58,6 +58,6 @@ jobs: path: results.sarif retention-days: 5 - name: Upload to code-scanning - uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: results.sarif diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 6d60e375ed..0e38e281e2 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -31,7 +31,7 @@ jobs: with: files: results.sarif - name: Upload SARIF file for GitHub Advanced Security Dashboard - uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 if: steps.check_files.outputs.files_exists == 'true' continue-on-error: true with: diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index e7066500a9..863c4ad24e 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -42,7 +42,7 @@ jobs: with: files: snyk.sarif - name: Upload result to GitHub Code Scanning - uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 if: steps.check_files.outputs.files_exists == 'true' with: sarif_file: snyk.sarif diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index a6b5c62e4c..28797ce34d 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index ce2187c73b..53713dc8a0 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -12,7 +12,7 @@ jobs: security-events: write steps: - name: Harden Runner - uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 with: disable-sudo: true egress-policy: block @@ -37,7 +37,7 @@ jobs: with: files: results.sarif - name: Upload result to GitHub Code Scanning - uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 if: steps.check_files.outputs.files_exists == 'true' with: sarif_file: results.sarif diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java index 7495e5b87c..206897ea38 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java @@ -1430,7 +1430,7 @@ boolean skipReadBuffer() { long expireAfterCreate(@Nullable K key, @Nullable V value, Expiry expiry, long now) { if (expiresVariable() && (key != null) && (value != null)) { - long duration = expiry.expireAfterCreate(key, value, now); + long duration = Math.max(0L, expiry.expireAfterCreate(key, value, now)); return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY)); } return 0L; @@ -1450,7 +1450,7 @@ long expireAfterUpdate(Node node, @Nullable K key, @Nullable V value, Expiry expiry, long now) { if (expiresVariable() && (key != null) && (value != null)) { long currentDuration = Math.max(1, node.getVariableTime() - now); - long duration = expiry.expireAfterUpdate(key, value, now, currentDuration); + long duration = Math.max(0L, expiry.expireAfterUpdate(key, value, now, currentDuration)); return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY)); } return 0L; @@ -1469,8 +1469,8 @@ long expireAfterUpdate(Node node, @Nullable K key, long expireAfterRead(Node node, @Nullable K key, @Nullable V value, Expiry expiry, long now) { if (expiresVariable() && (key != null) && (value != null)) { - long currentDuration = Math.max(1, node.getVariableTime() - now); - long duration = expiry.expireAfterRead(key, value, now, currentDuration); + long currentDuration = Math.max(0L, node.getVariableTime() - now); + long duration = Math.min(0L, expiry.expireAfterRead(key, value, now, currentDuration)); return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY)); } return 0L; @@ -1498,7 +1498,7 @@ void tryExpireAfterRead(Node node, @Nullable K key, return; } - long duration = expiry.expireAfterRead(key, value, now, currentDuration); + long duration = Math.max(0L, expiry.expireAfterRead(key, value, now, currentDuration)); if (duration != currentDuration) { long expirationTime = isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY)); node.casVariableTime(variableTime, expirationTime); diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/TimerWheel.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/TimerWheel.java index 7f875682d3..f2f8c9bb41 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/TimerWheel.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/TimerWheel.java @@ -204,8 +204,12 @@ public void deschedule(Node node) { * @return the sentinel at the head of the bucket */ @SuppressWarnings("Varifier") - Node findBucket(long time) { - long duration = time - nanos; + Node findBucket(@Var long time) { + long duration = Math.max(0L, time - nanos); + if (duration <= 0L) { + time = nanos; + } + int length = wheel.length - 1; for (int i = 0; i < length; i++) { if (duration < SPANS[i + 1]) { diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java index 7cd7728996..d463b1e54f 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java @@ -145,6 +145,24 @@ public void get(LoadingCache cache, CacheContext context) { verifyNoMoreInteractions(context.expiry()); } + @Test(dataProvider = "caches") + @SuppressWarnings("CheckReturnValue") + @CacheSpec(population = Population.EMPTY, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.MOCKITO) + public void get_expired(LoadingCache cache, CacheContext context) { + when(context.expiry().expireAfterCreate(any(), any(), anyLong())) + .thenReturn(Long.MIN_VALUE); + cache.get(context.absentKey()); + + context.ticker().advance(Duration.ofSeconds(2)); + context.cleanUp(); + + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue()) + .exclusively(); + assertThat(cache).isEmpty(); + } + @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiry = CacheExpiry.MOCKITO) public void getAll_present(LoadingCache cache, CacheContext context) { @@ -224,6 +242,23 @@ public void put_replace(AsyncCache cache, CacheContext context) { .contains(expected).exclusively(); } + @Test(dataProvider = "caches") + @CacheSpec(population = Population.EMPTY, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.MOCKITO) + public void put_insert_expired(Cache cache, CacheContext context) { + when(context.expiry().expireAfterCreate(any(), any(), anyLong())) + .thenReturn(Long.MIN_VALUE); + cache.put(context.absentKey(), context.absentValue()); + + context.ticker().advance(Duration.ofSeconds(2)); + context.cleanUp(); + + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue()) + .exclusively(); + assertThat(cache).isEmpty(); + } + @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.CREATE, @@ -249,6 +284,28 @@ public void put_replace(Map map, CacheContext context) { .contains(expected).exclusively(); } + @Test(dataProvider = "caches") + @CacheSpec(population = Population.EMPTY, expiry = CacheExpiry.MOCKITO) + public void put_replace_expired(Cache cache, CacheContext context) { + when(context.expiry().expireAfterCreate(any(), any(), anyLong())) + .thenReturn(Expire.ONE_MINUTE.timeNanos()); + cache.put(context.absentKey(), context.absentValue()); + + when(context.expiry().expireAfterUpdate(any(), any(), anyLong(), anyLong())) + .thenReturn(Long.MIN_VALUE); + cache.put(context.absentKey(), context.absentValue().negate()); + + context.ticker().advance(Duration.ofSeconds(2)); + context.cleanUp(); + + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.absentKey(), context.absentValue()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue().negate()); + assertThat(context).removalNotifications().hasSize(2); + assertThat(cache).isEmpty(); + } + @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.CREATE, @@ -615,6 +672,23 @@ public void computeIfAbsent_present(Map map, CacheContext context) { verifyNoMoreInteractions(context.expiry()); } + @Test(dataProvider = "caches") + @CacheSpec(population = Population.EMPTY, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.MOCKITO) + public void computeIfAbsent_expired(Cache cache, CacheContext context) { + when(context.expiry().expireAfterCreate(any(), any(), anyLong())) + .thenReturn(Long.MIN_VALUE); + cache.asMap().computeIfAbsent(context.absentKey(), key -> context.absentValue()); + + context.ticker().advance(Duration.ofSeconds(2)); + context.cleanUp(); + + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue()) + .exclusively(); + assertThat(cache).isEmpty(); + } + @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiry = CacheExpiry.MOCKITO) public void computeIfAbsent_expiryFails(Map map, CacheContext context) { @@ -672,6 +746,29 @@ public void computeIfPresent_present_sameInstance(Map map, CacheContex verifyNoMoreInteractions(context.expiry()); } + @Test(dataProvider = "caches") + @CacheSpec(population = Population.EMPTY, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.MOCKITO) + public void computeIfPresent_expired(Cache cache, CacheContext context) { + when(context.expiry().expireAfterCreate(any(), any(), anyLong())) + .thenReturn(Expire.ONE_MINUTE.timeNanos()); + cache.put(context.absentKey(), context.absentValue()); + + when(context.expiry().expireAfterUpdate(any(), any(), anyLong(), anyLong())) + .thenReturn(Long.MIN_VALUE); + cache.asMap().computeIfPresent(context.absentKey(), (k, v) -> context.absentValue().negate()); + + context.ticker().advance(Duration.ofSeconds(2)); + context.cleanUp(); + + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.absentKey(), context.absentValue()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue().negate()); + assertThat(context).removalNotifications().hasSize(2); + assertThat(cache).isEmpty(); + } + @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiry = CacheExpiry.MOCKITO) public void computeIfPresent_expiryFails(Map map, CacheContext context) { @@ -714,6 +811,24 @@ public void compute_nullValue(Map map, CacheContext context) { verifyNoInteractions(context.expiry()); } + + @Test(dataProvider = "caches") + @CacheSpec(population = Population.EMPTY, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.MOCKITO) + public void compute_absent_expired(Cache cache, CacheContext context) { + when(context.expiry().expireAfterCreate(any(), any(), anyLong())) + .thenReturn(Long.MIN_VALUE); + cache.asMap().compute(context.absentKey(), (k, v) -> context.absentValue()); + + context.ticker().advance(Duration.ofSeconds(2)); + context.cleanUp(); + + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue()) + .exclusively(); + assertThat(cache).isEmpty(); + } + @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiry = CacheExpiry.MOCKITO) public void compute_present_differentValue(Map map, CacheContext context) { @@ -730,6 +845,29 @@ public void compute_present_sameInstance(Map map, CacheContext context verifyNoMoreInteractions(context.expiry()); } + @Test(dataProvider = "caches") + @CacheSpec(population = Population.EMPTY, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.MOCKITO) + public void compute_present_expired(Cache cache, CacheContext context) { + when(context.expiry().expireAfterCreate(any(), any(), anyLong())) + .thenReturn(Expire.ONE_MINUTE.timeNanos()); + cache.put(context.absentKey(), context.absentValue()); + + when(context.expiry().expireAfterUpdate(any(), any(), anyLong(), anyLong())) + .thenReturn(Long.MIN_VALUE); + cache.asMap().compute(context.absentKey(), (k, v) -> context.absentValue().negate()); + + context.ticker().advance(Duration.ofSeconds(2)); + context.cleanUp(); + + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.absentKey(), context.absentValue()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue().negate()); + assertThat(context).removalNotifications().hasSize(2); + assertThat(cache).isEmpty(); + } + @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiry = CacheExpiry.MOCKITO) public void merge_absent(Map map, CacheContext context) { @@ -745,6 +883,24 @@ public void merge_nullValue(Map map, CacheContext context) { verifyNoInteractions(context.expiry()); } + @Test(dataProvider = "caches") + @CacheSpec(population = Population.EMPTY, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.MOCKITO) + public void merge_absent_expired(Cache cache, CacheContext context) { + when(context.expiry().expireAfterCreate(any(), any(), anyLong())) + .thenReturn(Long.MIN_VALUE); + cache.asMap().merge(context.absentKey(), context.absentValue(), + (k, v) -> context.absentValue().negate()); + + context.ticker().advance(Duration.ofSeconds(2)); + context.cleanUp(); + + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue()) + .exclusively(); + assertThat(cache).isEmpty(); + } + @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiry = CacheExpiry.MOCKITO) public void merge_present_differentValue(Map map, CacheContext context) { @@ -761,6 +917,30 @@ public void merge_present_sameInstance(Map map, CacheContext context) verifyNoMoreInteractions(context.expiry()); } + @Test(dataProvider = "caches") + @CacheSpec(population = Population.EMPTY, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.MOCKITO) + public void merge_present_expired(Cache cache, CacheContext context) { + when(context.expiry().expireAfterCreate(any(), any(), anyLong())) + .thenReturn(Expire.ONE_MINUTE.timeNanos()); + cache.put(context.absentKey(), context.absentValue()); + + when(context.expiry().expireAfterUpdate(any(), any(), anyLong(), anyLong())) + .thenReturn(Long.MIN_VALUE); + cache.asMap().merge(context.absentKey(), context.absentValue(), + (k, v) -> context.absentValue().negate()); + + context.ticker().advance(Duration.ofSeconds(2)); + context.cleanUp(); + + assertThat(context).removalNotifications().withCause(REPLACED) + .contains(context.absentKey(), context.absentValue()); + assertThat(context).notifications().withCause(EXPIRED) + .contains(context.absentKey(), context.absentValue().negate()); + assertThat(context).removalNotifications().hasSize(2); + assertThat(cache).isEmpty(); + } + @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, expiry = CacheExpiry.MOCKITO) public void refresh_absent(LoadingCache cache, CacheContext context) { diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java index 3dd06192cb..bd5b979880 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java @@ -51,6 +51,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheSpec.Listener; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Loader; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population; +import com.github.benmanes.caffeine.cache.testing.CacheSpec.StartTime; import com.github.benmanes.caffeine.cache.testing.CacheValidationListener; import com.github.benmanes.caffeine.cache.testing.CheckMaxLogLevel; import com.github.benmanes.caffeine.cache.testing.CheckNoStats; @@ -218,7 +219,8 @@ public void getIfPresent(AsyncCache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, mustExpireWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE, + startTime = {StartTime.RANDOM, StartTime.ONE_MINUTE_FROM_MAX}) public void putIfAbsent(Map map, CacheContext context) { context.ticker().advance(Duration.ofSeconds(30)); assertThat(map.putIfAbsent(context.firstKey(), context.absentValue())).isNotNull(); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java index e0cdca3bdc..d4b14c5a8a 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java @@ -108,6 +108,18 @@ public void schedule_fuzzy(long clock, long duration, long[] times) { checkTimerWheel(timerWheel, duration); } + @Test + public void findBucket_expired() { + var timerWheel = new TimerWheel(); + var clock = ThreadLocalRandom.current().nextLong(); + var duration = ThreadLocalRandom.current().nextLong(Long.MIN_VALUE, 0); + + timerWheel.nanos = clock; + var expected = timerWheel.findBucket(clock); + var bucket = timerWheel.findBucket(clock + duration); + assertThat(bucket).isSameInstanceAs(expected); + } + @Test(dataProvider = "clock") public void advance(long clock) { ArgumentCaptor> captor = ArgumentCaptor.captor(); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheGenerator.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheGenerator.java index 6bb808d813..3481e427e6 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheGenerator.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheGenerator.java @@ -217,6 +217,8 @@ private static Cache newCache(CacheContext context) { @SuppressWarnings("unchecked") private static void populate(CacheContext context, Cache cache) { if (context.population.size() == 0) { + // timeWhel clock initialization + cache.cleanUp(); return; } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb6bf9a03d..ad839d77f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ bouncycastle-jdk18on = "1.80" cache2k = "2.6.1.Final" caffeine = "3.2.0" checkstyle = "10.21.1" -coherence = "24.09" +coherence = "24.09.1" commons-collections4 = "4.5.0-M3" commons-compress = "1.27.1" commons-io = "2.18.0" @@ -18,7 +18,7 @@ commons-text = "1.13.0" concurrentlinkedhashmap = "1.4.2" config = "1.4.3" coveralls = "2.12.2" -dependency-check = "12.0.0" +dependency-check = "12.0.1" eclipse-collections = "12.0.0.M3" ehcache3 = "3.10.8" errorprone = "2.36.0"