Skip to content

Commit

Permalink
minor touchups
Browse files Browse the repository at this point in the history
  • Loading branch information
ben-manes committed Jun 16, 2024
1 parent c423769 commit 9a9b8c4
Show file tree
Hide file tree
Showing 10 changed files with 54 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .github/actions/run-gradle/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ runs:
distribution: temurin
- name: Setup Gradle
id: setup-gradle
uses: gradle/actions/setup-gradle@d9336dac04dea2507a617466bc058a3def92b18b # v3.4.0
uses: gradle/actions/setup-gradle@31ae3562f68c96d481c31bc1a8a55cc1be162f83 # v3.4.1
env:
JAVA_HOME: ${{ steps.setup-gradle-jdk.outputs.path }}
ORG_GRADLE_PROJECT_org.gradle.java.installations.auto-download: 'false'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/actionlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
github.com:443
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: actionlint
uses: reviewdog/action-actionlint@fd627997c9688c2f39e13917aed23873c031b834 # v1.48.0
uses: reviewdog/action-actionlint@52819f5f70db72e17c2fadecd44a791ae4459276 # v1.49.0
env:
SHELLCHECK_OPTS: -e SC2001 -e SC2035 -e SC2046 -e SC2061 -e SC2086 -e SC2156
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dependency-submission-pr-retreive.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ jobs:
repo1.maven.org:443
services.gradle.org:443
- name: Retrieve and submit dependency graph
uses: gradle/actions/dependency-submission@d9336dac04dea2507a617466bc058a3def92b18b # v3.4.0
uses: gradle/actions/dependency-submission@31ae3562f68c96d481c31bc1a8a55cc1be162f83 # v3.4.1
with:
dependency-graph: download-and-submit
2 changes: 1 addition & 1 deletion .github/workflows/dependency-submission-pr-submit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
java-version: ${{ env.JAVA_VERSION }}
distribution: temurin
- name: Submit Dependency Graph
uses: gradle/actions/dependency-submission@d9336dac04dea2507a617466bc058a3def92b18b # v3.4.0
uses: gradle/actions/dependency-submission@31ae3562f68c96d481c31bc1a8a55cc1be162f83 # v3.4.1
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
dependency-graph: generate-and-upload
2 changes: 1 addition & 1 deletion .github/workflows/dependency-submission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ jobs:
java-version: ${{ env.JAVA_VERSION }}
distribution: temurin
- name: Submit Dependency Graph
uses: gradle/actions/dependency-submission@d9336dac04dea2507a617466bc058a3def92b18b # v3.4.0
uses: gradle/actions/dependency-submission@31ae3562f68c96d481c31bc1a8a55cc1be162f83 # v3.4.1
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
2 changes: 1 addition & 1 deletion .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
java-version: ${{ env.JAVA_VERSION }}
distribution: temurin
- name: Setup Gradle
uses: gradle/actions/setup-gradle@d9336dac04dea2507a617466bc058a3def92b18b # v3.4.0
uses: gradle/actions/setup-gradle@31ae3562f68c96d481c31bc1a8a55cc1be162f83 # v3.4.1
with:
add-job-summary: never
cache-read-only: false
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gradle-wrapper-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ jobs:
github.com:443
services.gradle.org:443
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: gradle/actions/wrapper-validation@d9336dac04dea2507a617466bc058a3def92b18b # v3.4.0
- uses: gradle/actions/wrapper-validation@31ae3562f68c96d481c31bc1a8a55cc1be162f83 # v3.4.1
6 changes: 3 additions & 3 deletions examples/indexable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ CREATE TABLE user_info (
username varchar(255) NOT NULL,
password_hash varchar(255) NOT NULL,
created_on timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
modified_on timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
modified_on timestamp with time zone DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX user_info_email_idx ON user_info (email);
CREATE UNIQUE INDEX user_info_username_idx ON user_info (username);
Expand Down Expand Up @@ -50,8 +50,8 @@ constraints. The value can then be queried using the typed key.
```java
var cache = new IndexedCache.Builder<UserKey, User>()
.primaryKey(user -> new UserById(user.id()))
.addSecondaryKey(user -> new UserByLogin(user.login()))
.addSecondaryKey(user -> new UserByEmail(user.email()))
.addSecondaryKey(user -> new UserByLogin(user.username()))
.expireAfterWrite(Duration.ofMinutes(5))
.maximumSize(10_000)
.build(this::findUser);
Expand All @@ -63,7 +63,7 @@ assertThat(userByEmail).isSameInstanceAs(userByLogin);

### How it works
The sample [IndexedCache][] combines a key-value cache with an associated mapping from the
individual keys to the entry's complete set. Consistency is maintained by acquiring the write lock
individual keys to the entry's complete set. Consistency is maintained by acquiring the entry's lock
through the cache using the primary key before updating the index. This prevents race conditions
when the entry is concurrently updated and evicted, which could otherwise lead to missing or
non-resident key associations in the index. On eviction, a listener discards the keys while holding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
*/
package com.github.benmanes.caffeine.examples.indexable;

import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;

import java.time.Duration;
import java.util.Set;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.SequencedSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
Expand All @@ -28,7 +30,6 @@
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Ticker;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Striped;

Expand All @@ -42,29 +43,27 @@
* @author [email protected] (Ben Manes)
*/
public final class IndexedCache<K, V> {
final ConcurrentMap<K, Index<K>> indexes;
final ConcurrentMap<K, SequencedSet<K>> indexes;
final SequencedSet<Function<V, K>> indexers;
final Function<K, V> mappingFunction;
final Function<V, Index<K>> indexer;
final Striped<Lock> locks;
final Cache<K, V> store;

private IndexedCache(Caffeine<Object, Object> cacheBuilder, Function<K, V> mappingFunction,
Function<V, K> primary, Set<Function<V, K>> secondaries) {
this.locks = Striped.lock(1_000);
this.mappingFunction = mappingFunction;
private IndexedCache(Caffeine<Object, Object> cacheBuilder,
Function<K, V> mappingFunction, SequencedSet<Function<V, K>> indexers) {
this.indexes = new ConcurrentHashMap<>();
this.mappingFunction = mappingFunction;
this.locks = Striped.lock(1_024);
this.indexers = indexers;
this.store = cacheBuilder
.evictionListener((key, value, cause) ->
indexes.keySet().removeAll(indexes.get(key).allKeys()))
.evictionListener((key, value, cause) -> indexes.keySet().removeAll(indexes.get(key)))
.build();
this.indexer = value -> new Index<>(primary.apply(value),
secondaries.stream().map(indexer -> indexer.apply(value)).collect(toImmutableSet()));
}

/** Returns the value associated with the key or {@code null} if not found. */
public V getIfPresent(K key) {
var index = indexes.get(key);
return (index == null) ? null : store.getIfPresent(index.primaryKey());
return (index == null) ? null : store.getIfPresent(index.getFirst());
}

/**
Expand Down Expand Up @@ -103,13 +102,12 @@ public V get(K key) {
/** Associates the {@code value} with its keys, replacing the old value and keys if present. */
public V put(V value) {
requireNonNull(value);
var index = indexer.apply(value);
return store.asMap().compute(index.primaryKey(), (key, oldValue) -> {
var index = buildIndex(value);
return store.asMap().compute(index.getFirst(), (key, oldValue) -> {
if (oldValue != null) {
indexes.keySet().removeAll(Sets.difference(
indexes.get(index.primaryKey()).allKeys(), index.allKeys()));
indexes.keySet().removeAll(Sets.difference(indexes.get(index.getFirst()), index));
}
for (var indexKey : index.allKeys()) {
for (var indexKey : index) {
indexes.put(indexKey, index);
}
return value;
Expand All @@ -123,28 +121,36 @@ public void invalidate(K key) {
return;
}

store.asMap().computeIfPresent(index.primaryKey(), (k, v) -> {
indexes.keySet().removeAll(index.allKeys());
store.asMap().computeIfPresent(index.getFirst(), (k, v) -> {
indexes.keySet().removeAll(indexes.get(key));
return null;
});
}

private record Index<K>(K primaryKey, Set<K> secondaryKeys) {
public Set<K> allKeys() {
return Sets.union(Set.of(primaryKey), secondaryKeys);
/** Returns a sequence of keys where the first item is the primary key. */
private SequencedSet<K> buildIndex(V value) {
var index = LinkedHashSet.<K>newLinkedHashSet(indexers.size());
for (var indexer : indexers) {
var key = indexer.apply(value);
if (key == null) {
checkState(!index.isEmpty(), "The primary key may not be null");
} else {
index.add(key);
}
}
return Collections.unmodifiableSequencedSet(index);
}

/** This builder could be extended to support most cache options, but not weak keys. */
public static final class Builder<K, V> {
final SequencedSet<Function<V, K>> indexers;
final Caffeine<Object, Object> cacheBuilder;
final ImmutableSet.Builder<Function<V, K>> secondaries;

Function<V, K> primary;
boolean hasPrimary;

public Builder() {
indexers = new LinkedHashSet<>();
cacheBuilder = Caffeine.newBuilder();
secondaries = ImmutableSet.builder();
}

/** See {@link Caffeine#expireAfterWrite(Duration)}. */
Expand All @@ -159,22 +165,25 @@ public Builder<K, V> ticker(Ticker ticker) {
return this;
}

/** Adds the functions to extract the primary key. */
/** Adds the function to extract the unique, stable, non-null primary key. */
public Builder<K, V> primaryKey(Function<V, K> primary) {
this.primary = requireNonNull(primary);
checkState(!hasPrimary, "The primary indexing function was already defined");
indexers.addFirst(requireNonNull(primary));
hasPrimary = true;
return this;
}

/** Adds the functions to extract a secondary key. */
/** Adds a function to extract a unique secondary key or null if absent. */
public Builder<K, V> addSecondaryKey(Function<V, K> secondary) {
secondaries.add(requireNonNull(secondary));
indexers.addLast(requireNonNull(secondary));
return this;
}

public IndexedCache<K, V> build(Function<K, V> mappingFunction) {
requireNonNull(primary);
requireNonNull(mappingFunction);
return new IndexedCache<K, V>(cacheBuilder, mappingFunction, primary, secondaries.build());
checkState(hasPrimary, "The primary indexing function is required");
requireNonNull(mappingFunction, "The mapping function to load the value is required");
return new IndexedCache<K, V>(cacheBuilder, mappingFunction,
Collections.unmodifiableSequencedSet(indexers));
}
}
}
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ snakeyaml = "2.2"
sonarqube = "5.0.0.4638"
spotbugs-contrib = "7.6.4"
spotbugs-core = "4.8.5"
spotbugs-plugin = "6.0.16"
spotbugs-plugin = "6.0.17"
stream = "2.9.8"
tcache = "2.0.1"
testng = "7.10.2"
Expand Down

0 comments on commit 9a9b8c4

Please sign in to comment.