Skip to content

Commit

Permalink
Experimental ability to cache References (#8111)
Browse files Browse the repository at this point in the history
Adds ability to cache `Reference` objects, avoiding round-trips to the backend database, beneficial for read heavy workloads.

Reference-caching can be enabled via two new configuration options: to define the expiration time for `Reference`s (holding the current HEAD/tip) and to define the expiration time for non-existing `Reference`s.

Looking up references happens via the name of the reference, usually without the reference type (aka whether it is a branch or a tag), so Nessie has to look up both types - the given name as a branch and the given name as a tag. This is where negative-caching comes into play, because that caches the existing entry and the non-existing "other" reference type. Hence, if you enable reference-caching, it is recommended to also enable negative reference-caching.

Operations that are about to change a reference (committing and reference create/assign/delete operations), always consult the backing database, implicitly refreshing the cache.

Mutliple Nessie (against the same repository) do not communicate with each other yet (#8463 fixes that). If for example a commit happened against one Nessie instance, the other instances may or may not return the new commit. This is why this feature is still experimental and only useful for Nessie setups with a _single_ Nessie instance. This change will be later with another change to allow distributed cache-eviction, so that other Nessie instances accessing the same repository work fine.
  • Loading branch information
snazy authored May 16, 2024
1 parent f2390db commit cfb7f69
Show file tree
Hide file tree
Showing 28 changed files with 978 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import io.smallrye.config.WithConverter;
import io.smallrye.config.WithDefault;
import io.smallrye.config.WithName;
import java.time.Duration;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import org.projectnessie.versioned.storage.common.config.StoreConfig;
Expand Down Expand Up @@ -136,4 +138,12 @@ public interface QuarkusStoreConfig extends StoreConfig {
*/
@WithName(CONFIG_CACHE_CAPACITY_FRACTION_ADJUST_MB)
OptionalInt cacheCapacityFractionAdjustMB();

@WithName(CONFIG_REFERENCE_CACHE_TTL)
@Override
Optional<Duration> referenceCacheTtl();

@WithName(CONFIG_REFERENCE_NEGATIVE_CACHE_TTL)
@Override
Optional<Duration> referenceCacheNegativeTtl();
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,21 @@ public Persist producePersist(MeterRegistry meterRegistry) {

String cacheInfo;
if (effectiveCacheSizeMB > 0) {
CacheConfig cacheConfig =
CacheConfig.builder()
.capacityMb(effectiveCacheSizeMB)
.meterRegistry(meterRegistry)
.build();
CacheBackend cacheBackend = PersistCaches.newBackend(cacheConfig);
CacheConfig.Builder cacheConfig =
CacheConfig.builder().capacityMb(effectiveCacheSizeMB).meterRegistry(meterRegistry);

storeConfig
.referenceCacheTtl()
.ifPresent(
refTtl -> {
LOGGER.warn(
"Reference caching is an experimental feature but enabled with a TTL of {}",
refTtl);
cacheConfig.referenceTtl(refTtl);
});
storeConfig.referenceCacheNegativeTtl().ifPresent(cacheConfig::referenceNegativeTtl);

CacheBackend cacheBackend = PersistCaches.newBackend(cacheConfig.build());
persist = cacheBackend.wrap(persist);
cacheInfo = "with " + effectiveCacheSizeMB + " MB objects cache";
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,13 +404,27 @@ public Reference fetchReference(@Nonnull @javax.annotation.Nonnull String name)
return delegate().fetchReference(name);
}

@Override
@Nullable
@javax.annotation.Nullable
public Reference fetchReferenceForUpdate(@Nonnull @javax.annotation.Nonnull String name) {
return delegate().fetchReferenceForUpdate(name);
}

@Override
@Nonnull
@javax.annotation.Nonnull
public Reference[] fetchReferences(@Nonnull @javax.annotation.Nonnull String[] names) {
return delegate().fetchReferences(names);
}

@Override
@Nonnull
@javax.annotation.Nonnull
public Reference[] fetchReferencesForUpdate(@Nonnull @javax.annotation.Nonnull String[] names) {
return delegate().fetchReferencesForUpdate(names);
}

@Override
@Nonnull
@javax.annotation.Nonnull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,15 @@ public Reference addReference(@Nonnull Reference reference) throws RefAlreadyExi
.filter(FILTERS.family().exactMatch(FAMILY_REFS))
.filter(FILTERS.qualifier().exactMatch(QUALIFIER_REFS));

boolean success =
boolean failure =
backend
.client()
.checkAndMutateRow(
ConditionalRowMutation.create(backend.tableRefsId, key)
.condition(condition)
.otherwise(mutation));

if (success) {
if (failure) {
throw new RefAlreadyExistsException(fetchReference(reference.name()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,24 @@
*/
package org.projectnessie.versioned.storage.cache;

import static org.projectnessie.versioned.storage.common.persist.ObjId.zeroLengthObjId;
import static org.projectnessie.versioned.storage.common.persist.Reference.reference;

import jakarta.annotation.Nonnull;
import org.projectnessie.versioned.storage.common.persist.Backend;
import org.projectnessie.versioned.storage.common.persist.Obj;
import org.projectnessie.versioned.storage.common.persist.ObjId;
import org.projectnessie.versioned.storage.common.persist.Persist;
import org.projectnessie.versioned.storage.common.persist.Reference;

/**
* Provides the cache primitives for a caching {@link Persist} facade, suitable for multiple
* repositories. It is adviseable to have one {@link CacheBackend} per {@link Backend}.
*/
public interface CacheBackend {
Reference NON_EXISTENT_REFERENCE_SENTINEL =
reference("NON_EXISTENT", zeroLengthObjId(), false, -1L, null);

Obj get(@Nonnull String repositoryId, @Nonnull ObjId id);

void put(@Nonnull String repositoryId, @Nonnull Obj obj);
Expand All @@ -34,5 +41,13 @@ public interface CacheBackend {

void clear(@Nonnull String repositoryId);

Persist wrap(@Nonnull Persist perist);
Persist wrap(@Nonnull Persist persist);

Reference getReference(@Nonnull String repositoryId, @Nonnull String name);

void removeReference(@Nonnull String repositoryId, @Nonnull String name);

void putReference(@Nonnull String repositoryId, @Nonnull Reference r);

void putNegative(@Nonnull String repositoryId, @Nonnull String name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,30 @@
*/
package org.projectnessie.versioned.storage.cache;

import static com.google.common.base.Preconditions.checkState;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.micrometer.core.instrument.MeterRegistry;
import java.time.Duration;
import java.util.Optional;
import java.util.function.LongSupplier;
import org.immutables.value.Value;

@Value.Immutable
public interface CacheConfig {

String INVALID_REFERENCE_NEGATIVE_TTL =
"Cache reference-negative-TTL must only be present, if reference-TTL is configured, and must only be positive.";
String INVALID_REFERENCE_TTL = "Cache reference-TTL must be positive, if present.";

long capacityMb();

Optional<MeterRegistry> meterRegistry();

Optional<Duration> referenceTtl();

Optional<Duration> referenceNegativeTtl();

@Value.Default
default LongSupplier clockNanos() {
return System::nanoTime;
Expand All @@ -36,13 +48,31 @@ static Builder builder() {
return ImmutableCacheConfig.builder();
}

@Value.Check
default void check() {
referenceTtl()
.ifPresent(ttl -> checkState(ttl.compareTo(Duration.ZERO) > 0, INVALID_REFERENCE_TTL));
referenceNegativeTtl()
.ifPresent(
ttl ->
checkState(
referenceTtl().isPresent() && ttl.compareTo(Duration.ZERO) > 0,
INVALID_REFERENCE_NEGATIVE_TTL));
}

interface Builder {
@CanIgnoreReturnValue
Builder capacityMb(long capacityMb);

@CanIgnoreReturnValue
Builder meterRegistry(MeterRegistry meterRegistry);

@CanIgnoreReturnValue
Builder referenceTtl(Duration referenceTtl);

@CanIgnoreReturnValue
Builder referenceNegativeTtl(Duration referenceNegativeTtl);

@CanIgnoreReturnValue
Builder clockNanos(LongSupplier clockNanos);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package org.projectnessie.versioned.storage.cache;

import static org.projectnessie.versioned.storage.cache.CacheBackend.NON_EXISTENT_REFERENCE_SENTINEL;

import jakarta.annotation.Nonnull;
import java.util.Set;
import org.projectnessie.versioned.storage.common.config.StoreConfig;
Expand Down Expand Up @@ -261,40 +263,148 @@ public String name() {
return persist.name();
}

// References

@Override
@Nonnull
public Reference addReference(@Nonnull Reference reference) throws RefAlreadyExistsException {
return persist.addReference(reference);
Reference r = null;
try {
return r = persist.addReference(reference);
} finally {
if (r != null) {
cache.putReference(r);
} else {
cache.removeReference(reference.name());
}
}
}

@Override
@Nonnull
public Reference markReferenceAsDeleted(@Nonnull Reference reference)
throws RefNotFoundException, RefConditionFailedException {
return persist.markReferenceAsDeleted(reference);
Reference r = null;
try {
return r = persist.markReferenceAsDeleted(reference);
} finally {
if (r != null) {
cache.putReference(r);
} else {
cache.removeReference(reference.name());
}
}
}

@Override
public void purgeReference(@Nonnull Reference reference)
throws RefNotFoundException, RefConditionFailedException {
persist.purgeReference(reference);
try {
persist.purgeReference(reference);
} finally {
cache.removeReference(reference.name());
}
}

@Override
@Nonnull
public Reference updateReferencePointer(@Nonnull Reference reference, @Nonnull ObjId newPointer)
throws RefNotFoundException, RefConditionFailedException {
return persist.updateReferencePointer(reference, newPointer);
Reference r = null;
try {
return r = persist.updateReferencePointer(reference, newPointer);
} finally {
if (r != null) {
cache.putReference(r);
} else {
cache.removeReference(reference.name());
}
}
}

@Override
public Reference fetchReference(@Nonnull String name) {
return persist.fetchReference(name);
return fetchReferenceInternal(name, false);
}

@Override
public Reference fetchReferenceForUpdate(@Nonnull String name) {
return fetchReferenceInternal(name, true);
}

private Reference fetchReferenceInternal(@Nonnull String name, boolean bypassCache) {
Reference r = null;
if (!bypassCache) {
r = cache.getReference(name);
if (r == NON_EXISTENT_REFERENCE_SENTINEL) {
return null;
}
}

if (r == null) {
r = persist.fetchReferenceForUpdate(name);
if (r == null) {
cache.putNegative(name);
} else {
cache.putReference(r);
}
}
return r;
}

@Override
@Nonnull
public Reference[] fetchReferences(@Nonnull String[] names) {
return persist.fetchReferences(names);
return fetchReferencesInternal(names, false);
}

@Override
@Nonnull
public Reference[] fetchReferencesForUpdate(@Nonnull String[] names) {
return fetchReferencesInternal(names, true);
}

private Reference[] fetchReferencesInternal(@Nonnull String[] names, boolean bypassCache) {
Reference[] r = new Reference[names.length];

String[] backend = null;
if (!bypassCache) {
for (int i = 0; i < names.length; i++) {
String name = names[i];
if (name != null) {
Reference cr = cache.getReference(name);
if (cr != null) {
if (cr != NON_EXISTENT_REFERENCE_SENTINEL) {
r[i] = cr;
}
} else {
if (backend == null) {
backend = new String[names.length];
}
backend[i] = name;
}
}
}
} else {
backend = names;
}

if (backend != null) {
Reference[] br = persist.fetchReferencesForUpdate(backend);
for (int i = 0; i < br.length; i++) {
String name = backend[i];
if (name != null) {
Reference ref = br[i];
if (ref != null) {
r[i] = ref;
cache.putReference(ref);
} else {
cache.putNegative(name);
}
}
}
}

return r;
}
}
Loading

0 comments on commit cfb7f69

Please sign in to comment.