com.netflix.hollow.api.consumer.HollowConsumer Maven / Gradle / Ivy
/*
* Copyright 2016-2021 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.hollow.api.consumer;
import static com.netflix.hollow.core.util.Threads.daemonThread;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import com.netflix.hollow.PublicApi;
import com.netflix.hollow.PublicSpi;
import com.netflix.hollow.api.client.FailedTransitionTracker;
import com.netflix.hollow.api.client.HollowAPIFactory;
import com.netflix.hollow.api.client.HollowClientUpdater;
import com.netflix.hollow.api.client.StaleHollowReferenceDetector;
import com.netflix.hollow.api.codegen.HollowAPIClassJavaGenerator;
import com.netflix.hollow.api.consumer.fs.HollowFilesystemBlobRetriever;
import com.netflix.hollow.api.custom.HollowAPI;
import com.netflix.hollow.api.metrics.HollowConsumerMetrics;
import com.netflix.hollow.api.metrics.HollowMetricsCollector;
import com.netflix.hollow.core.HollowConstants;
import com.netflix.hollow.core.memory.MemoryMode;
import com.netflix.hollow.core.read.OptionalBlobPartInput;
import com.netflix.hollow.core.read.engine.HollowReadStateEngine;
import com.netflix.hollow.core.read.filter.HollowFilterConfig;
import com.netflix.hollow.core.read.filter.TypeFilter;
import com.netflix.hollow.core.util.DefaultHashCodeFinder;
import com.netflix.hollow.core.util.HollowObjectHashCodeFinder;
import com.netflix.hollow.tools.history.HollowHistory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.UnaryOperator;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A HollowConsumer is the top-level class used by consumers of Hollow data to initialize and keep up-to-date a local in-memory
* copy of a hollow dataset. The interactions between the "blob" transition store and announcement listener are defined by
* this class, and the implementations of the data retrieval, announcement mechanism are abstracted in the interfaces which
* are provided to this class.
*
* To obtain a HollowConsumer, you should use a builder pattern, for example:
*
{@code
* HollowConsumer consumer = newHollowConsumer().withBlobRetriever(retriever)
* .withAnnouncementWatcher(watcher)
* .withGeneratedAPIClass(MovieAPI.class)
* .build();
* }
*
* The following components are injectable, but only an implementation of the HollowConsumer.BlobRetriever is
* required to be injected, all other components are optional. :
*
* - {@link HollowConsumer.BlobRetriever}
* - Implementations of this class define how to retrieve blob data from the blob store.
*
* - {@link HollowConsumer.AnnouncementWatcher}
* - Implementations of this class define the announcement mechanism, which is used to track the version of the
* currently announced state. It's also expected that implementations will trigger a refresh each time current
* data version is updated.
*
* - a List of {@link HollowConsumer.RefreshListener}s
* - RefreshListener implementations will define what to do when various events happen before, during, and after updating
* local in-memory copies of hollow data sets.
*
* - the Class representing a generated Hollow API
* - Defines how to create a {@link HollowAPI} for the dataset, useful when wrapping a dataset with an api which has
* been generated (via the {@link HollowAPIClassJavaGenerator})
*
* - {@link HollowFilterConfig}
* - Defines what types and fields to load (or not load) into memory from hollow datasets. Generally useful to reduce
* heap footprint on consumers which do not require visibility of an entire dataset.
*
* - {@link HollowConsumer.DoubleSnapshotConfig}
* - Defines whether this consumer may attempt a double snapshot, and how many deltas will be attempted during a single refresh.
* A double snapshot will allow your consumer to update in case of a broken delta chain, but will also result in a doubling of
* the heap footprint while the double snapshot is occurring.
*
* - {@link HollowConsumer.ObjectLongevityConfig}
* - Object longevity is used to guarantee that Hollow objects which are backed by removed records will remain usable and
* consistent until old references are discarded. This behavior is turned off by default. Implementations of this config
* can be used to enable and configure this behavior.
*
* - {@link HollowConsumer.ObjectLongevityDetector}
* - Implementations of this config will be notified when usage of expired Hollow object references is attempted.
*
* - An Executor
* - The Executor which will be used to perform updates when {@link #triggerAsyncRefresh()} is called. This will
* default to a new fixed thread pool with a single refresh thread.
*
*
*/
@SuppressWarnings({"unused", "WeakerAccess"})
@PublicApi
public class HollowConsumer {
private static final Logger LOG = Logger.getLogger(HollowConsumer.class.getName());
protected final AnnouncementWatcher announcementWatcher;
protected final HollowClientUpdater updater;
protected final ReadWriteLock refreshLock;
protected final HollowConsumerMetrics metrics;
private final Executor refreshExecutor;
private final MemoryMode memoryMode;
/**
* @deprecated use {@link HollowConsumer.Builder}
*/
@Deprecated
protected HollowConsumer(BlobRetriever blobRetriever,
AnnouncementWatcher announcementWatcher,
List refreshListeners,
HollowAPIFactory apiFactory,
HollowFilterConfig dataFilter,
ObjectLongevityConfig objectLongevityConfig,
ObjectLongevityDetector objectLongevityDetector,
DoubleSnapshotConfig doubleSnapshotConfig,
HollowObjectHashCodeFinder hashCodeFinder,
Executor refreshExecutor,
MemoryMode memoryMode) {
this(blobRetriever, announcementWatcher, refreshListeners, apiFactory, dataFilter,
objectLongevityConfig, objectLongevityDetector, doubleSnapshotConfig,
hashCodeFinder, refreshExecutor, memoryMode,null);
}
/**
* @deprecated use {@link HollowConsumer.Builder}
*/
@Deprecated
protected HollowConsumer(BlobRetriever blobRetriever,
AnnouncementWatcher announcementWatcher,
List refreshListeners,
HollowAPIFactory apiFactory,
HollowFilterConfig dataFilter,
ObjectLongevityConfig objectLongevityConfig,
ObjectLongevityDetector objectLongevityDetector,
DoubleSnapshotConfig doubleSnapshotConfig,
HollowObjectHashCodeFinder hashCodeFinder,
Executor refreshExecutor,
MemoryMode memoryMode,
HollowMetricsCollector metricsCollector) {
this.metrics = new HollowConsumerMetrics();
this.updater = new HollowClientUpdater(blobRetriever,
refreshListeners,
apiFactory,
doubleSnapshotConfig,
hashCodeFinder,
memoryMode,
objectLongevityConfig,
objectLongevityDetector,
metrics,
metricsCollector);
updater.setFilter(dataFilter);
this.announcementWatcher = announcementWatcher;
this.refreshExecutor = refreshExecutor;
this.refreshLock = new ReentrantReadWriteLock();
if (announcementWatcher != null)
announcementWatcher.subscribeToUpdates(this);
this.memoryMode = memoryMode;
}
protected > HollowConsumer(B builder) {
// duplicated with HollowConsumer(...) constructor above. We cannot chain constructor calls because that
// constructor subscribes to the announcement watcher and we have more setup to do first
this.metrics = new HollowConsumerMetrics();
this.updater = new HollowClientUpdater(builder.blobRetriever,
builder.refreshListeners,
builder.apiFactory,
builder.doubleSnapshotConfig,
builder.hashCodeFinder,
builder.memoryMode,
builder.objectLongevityConfig,
builder.objectLongevityDetector,
metrics,
builder.metricsCollector);
updater.setFilter(builder.typeFilter);
if(builder.skipTypeShardUpdateWithNoAdditions)
updater.setSkipShardUpdateWithNoAdditions(true);
this.announcementWatcher = builder.announcementWatcher;
this.refreshExecutor = builder.refreshExecutor;
this.refreshLock = new ReentrantReadWriteLock();
this.memoryMode = builder.memoryMode;
if (announcementWatcher != null)
announcementWatcher.subscribeToUpdates(this);
}
/**
* Triggers a refresh to the latest version specified by the {@link HollowConsumer.AnnouncementWatcher}.
* If already on the latest version, this operation is a no-op.
*
* If a {@link HollowConsumer.AnnouncementWatcher} is not present, this call trigger a refresh to the
* latest version available in the blob store.
*
* This is a blocking call.
*/
public void triggerRefresh() {
refreshLock.writeLock().lock();
try {
updater.updateTo(announcementWatcher == null ? new VersionInfo(Long.MAX_VALUE)
: announcementWatcher.getLatestVersionInfo());
} catch (Error | RuntimeException e) {
throw e;
} catch (Throwable t) {
throw new RuntimeException(t);
} finally {
refreshLock.writeLock().unlock();
}
}
/**
* Immediately triggers a refresh in a different thread to the latest version
* specified by the {@link HollowConsumer.AnnouncementWatcher}. If already on
* the latest version, this operation is a no-op.
*
* If a {@link HollowConsumer.AnnouncementWatcher} is not present, this call trigger a refresh to the
* latest version available in the blob store.
*
* This is an asynchronous call.
*/
public void triggerAsyncRefresh() {
triggerAsyncRefreshWithDelay(0);
}
/**
* Triggers async refresh after the specified number of milliseconds has passed.
*
* Any subsequent calls for async refresh will not begin until after the specified delay
* has completed.
*
* @param delayMillis the delay, in millseconds, before triggering the refresh
*/
public void triggerAsyncRefreshWithDelay(int delayMillis) {
final long targetBeginTime = System.currentTimeMillis() + delayMillis;
refreshExecutor.execute(() -> {
try {
long delay = targetBeginTime - System.currentTimeMillis();
if (delay > 0)
Thread.sleep(delay);
} catch (InterruptedException e) {
// Interrupting, such as shutting down the executor pool,
// cancels the trigger
LOG.log(Level.INFO, "Async refresh interrupted before trigger, refresh cancelled", e);
return;
}
try {
triggerRefresh();
} catch (Error | RuntimeException e) {
// Ensure exceptions are propagated to the executor
LOG.log(Level.SEVERE, "Async refresh failed", e);
throw e;
}
});
}
/**
* If a {@link HollowConsumer.AnnouncementWatcher} is not specified, then this method will attempt to update
* to the specified version, and if the specified version does not exist then to a different version as specified
* by functionality in the {@code BlobRetriever}.
*
* Otherwise, an UnsupportedOperationException will be thrown.
*
* This is a blocking call.
*
* @param version the version to refresh to
*/
public void triggerRefreshTo(long version) {
triggerRefreshTo(new VersionInfo(version));
}
/**
* Similar to {@link #triggerRefreshTo(long)} but instead of accepting a long version no. it accepts a
* {@link VersionInfo} instance that contains (in addition to version no.) version specific metadata and
* pinning status.
*
* @param versionInfo version no., metadata, and pined status for the desired version
*/
public void triggerRefreshTo(VersionInfo versionInfo) {
if (announcementWatcher != null)
throw new UnsupportedOperationException("Cannot trigger refresh to specified version when a HollowConsumer.AnnouncementWatcher is present");
try {
updater.updateTo(versionInfo);
} catch (Error | RuntimeException e) {
throw e;
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
/**
* @return the {@link HollowReadStateEngine} which is holding the underlying hollow dataset.
*/
public HollowReadStateEngine getStateEngine() {
return updater.getStateEngine();
}
/**
* @return the current version of the dataset. This is the unique identifier of the data's state.
*/
public long getCurrentVersionId() {
return updater.getCurrentVersionId();
}
/**
* Returns a {@code CompletableFuture} that completes after the initial data load succeeds. Also triggers the initial
* load asynchronously, to avoid waiting on a polling interval for the initial load.
*
* Callers can use methods like {@link CompletableFuture#join()} or {@link CompletableFuture#get(long, TimeUnit)}
* to block until the initial load is complete.
*
* A failure during the initial load will not cause the future to complete exceptionally; this allows
* for a subsequent data version to eventually succeed.
*
* In a consumer without published or announced versions – or one that always fails the initial load – the future
* will remain incomplete indefinitely.
*
* @return a future which, when completed, has a value set to the data version that was initially loaded
*/
public CompletableFuture getInitialLoad() {
try {
triggerAsyncRefresh();
} catch (RejectedExecutionException | NullPointerException e) {
LOG.log(Level.INFO, "Refresh triggered by getInitialLoad() failed; future attempts might succeed", e);
}
return updater.getInitialLoad();
}
/**
* @return the api which wraps the underlying dataset.
*/
public HollowAPI getAPI() {
return updater.getAPI();
}
/**
* Equivalent to calling {@link #getAPI()} and casting to the specified API.
*
* @param apiClass the class of the API
* @param the type of the API
* @return the API which wraps the underlying dataset
*/
public T getAPI(Class apiClass) {
return apiClass.cast(updater.getAPI());
}
/**
* Will force a double snapshot refresh on the next update.
*/
public void forceDoubleSnapshotNextUpdate() {
updater.forceDoubleSnapshotNextUpdate();
}
/**
* Clear any failed transitions from the {@link FailedTransitionTracker}, so that they may be reattempted when an update is triggered.
*/
public void clearFailedTransitions() {
updater.clearFailedTransitions();
}
/**
* @return the number of failed snapshot transitions stored in the {@link FailedTransitionTracker}.
*/
public int getNumFailedSnapshotTransitions() {
return updater.getNumFailedSnapshotTransitions();
}
/**
* @return the number of failed delta transitions stored in the {@link FailedTransitionTracker}.
*/
public int getNumFailedDeltaTransitions() {
return updater.getNumFailedDeltaTransitions();
}
/**
* @return a {@link ReadWriteLock#readLock()}, the corresponding writeLock() of which is used to synchronize refreshes.
*
* This is useful if performing long-running operations which require a consistent view of the entire dataset in a
* single data state, to guarantee that updates do not happen while the operation runs.
*/
public Lock getRefreshLock() {
return refreshLock.readLock();
}
/**
* Adds a {@link RefreshListener} to this consumer.
*
* If the listener was previously added to this consumer, as determined by reference equality or {@code Object}
* equality, then this method does nothing.
*
* If a listener is added, concurrently, during the occurrence of a refresh then the listener will not receive
* events until the next refresh. The listener may also be removed concurrently.
*
* If the listener instance implements {@link RefreshRegistrationListener} then before the listener is added
* the {@link RefreshRegistrationListener#onBeforeAddition} method is be invoked. If that method throws an
* exception then that exception will be thrown by this method and the listener will not be added.
*
* @param listener the refresh listener to add
*/
public void addRefreshListener(RefreshListener listener) {
updater.addRefreshListener(listener, this);
}
/**
* Removes a {@link RefreshListener} from this consumer.
*
* If the listener was not previously added to this consumer, as determined by reference equality or {@code Object}
* equality, then this method does nothing.
*
* If a listener is removed, concurrently, during the occurrence of a refresh then the listener will receive all
* events for that refresh but not receive events for subsequent any refreshes.
*
* If the listener instance implements {@link RefreshRegistrationListener} then after the listener is removed
* the {@link RefreshRegistrationListener#onAfterRemoval} method is be invoked. If that method throws an
* exception then that exception will be thrown by this method.
*
* @param listener the refresh listener to remove
*/
public void removeRefreshListener(RefreshListener listener) {
updater.removeRefreshListener(listener, this);
}
/**
* @return the metrics for this consumer
*/
public HollowConsumerMetrics getMetrics() {
return metrics;
}
/**
* An interface which defines the necessary interactions of Hollow with a blob data store.
*
* Implementations will define how to retrieve blob data from a data store.
*/
public interface BlobRetriever {
/**
* Returns the snapshot for the state with the greatest version identifier which is equal to or less than the desired version
* @param desiredVersion the desired version
* @return the blob of the snapshot
*/
HollowConsumer.Blob retrieveSnapshotBlob(long desiredVersion);
/**
* Returns a delta transition which can be applied to the specified version identifier
* @param currentVersion the current version
* @return the blob of the delta
*/
HollowConsumer.Blob retrieveDeltaBlob(long currentVersion);
/**
* Returns a reverse delta transition which can be applied to the specified version identifier
* @param currentVersion the current version
* @return the blob of the reverse delta
*/
HollowConsumer.Blob retrieveReverseDeltaBlob(long currentVersion);
default Set configuredOptionalBlobParts() {
return null;
}
default HollowConsumer.HeaderBlob retrieveHeaderBlob(long currentVersion) {
throw new UnsupportedOperationException();
}
}
protected interface VersionedBlob {
InputStream getInputStream() throws IOException;
default File getFile() throws IOException {
throw new UnsupportedOperationException();
}
}
public static abstract class HeaderBlob implements VersionedBlob{
private final long version;
protected HeaderBlob(long version) {
this.version = version;
}
public long getVersion() {
return this.version;
}
}
/**
* A Blob, which is either a snapshot or a delta, defines three things:
*
* - The "from" version
* - The unique identifier of the state to which a delta transition should be applied. If
* this is a snapshot, then this value is HollowConstants.VERSION_NONE.
*
* - The "to" version
* - The unique identifier of the state at which a dataset will arrive after this blob is applied.
*
* - The actual blob data
* - Implementations will define how to retrieve the actual blob data for this specific blob from a data store as an InputStream.
*
*/
public static abstract class Blob implements VersionedBlob{
protected final long fromVersion;
protected final long toVersion;
private final BlobType blobType;
/**
* Instantiate a snapshot to a specified data state version.
*
* @param toVersion the version
*/
public Blob(long toVersion) {
this(HollowConstants.VERSION_NONE, toVersion);
}
/**
* Instantiate a delta from one data state version to another.
*
* @param fromVersion the version to start the delta from
* @param toVersion the version to end the delta from
*/
public Blob(long fromVersion, long toVersion) {
this.fromVersion = fromVersion;
this.toVersion = toVersion;
if (this.isSnapshot())
this.blobType = BlobType.SNAPSHOT;
else if (this.isReverseDelta())
this.blobType = BlobType.REVERSE_DELTA;
else
this.blobType = BlobType.DELTA;
}
/**
* Implementations will define how to retrieve the actual blob data for this specific transition from a data store.
*
* It is expected that the returned InputStream will not be interrupted. For this reason, it is a good idea to
* retrieve the entire blob (e.g. to disk) from a remote datastore prior to returning this stream.
*
* @return the input stream to the blob
* @throws IOException if the input stream to the blob cannot be obtained
*/
public abstract InputStream getInputStream() throws IOException;
/**
* Implementations may define how to retrieve the optional blob part data for this specific transition from a data store.
*
* It is expected that none of the returned InputStreams will be interrupted. For this reason, it is a good idea to
* retrieve the entire blob part data (e.g. to disk) from a remote datastore prior to returning these streams.
*
* @return OptionalBlobPartInput
* @throws IOException exception in reading from blob or file
*/
public OptionalBlobPartInput getOptionalBlobPartInputs() throws IOException {
return null;
}
/**
* Blobs can be of types {@code SNAPSHOT}, {@code DELTA} or {@code REVERSE_DELTA}.
*/
public enum BlobType {
SNAPSHOT("snapshot"),
DELTA("delta"),
REVERSE_DELTA("reversedelta");
private final String type;
BlobType(String type) {
this.type = type;
}
public String getType() {
return this.type;
}
}
public boolean isSnapshot() {
return fromVersion == HollowConstants.VERSION_NONE;
}
public boolean isReverseDelta() {
return toVersion < fromVersion;
}
public boolean isDelta() {
return !isSnapshot() && !isReverseDelta();
}
public long getFromVersion() {
return fromVersion;
}
public long getToVersion() {
return toVersion;
}
public BlobType getBlobType() {
return blobType;
}
}
/**
* This class holds an announced version, its pinned status and the announcement metadata.
* isPinned and announcementMetadata fields are empty unless they are populated by the AnnouncementWatcher.
* */
public static class VersionInfo {
long version;
Optional isPinned;
Optional