io.mats3.util.eagercache.MatsEagerCacheClient Maven / Gradle / Ivy
Show all versions of mats-util Show documentation
package io.mats3.util.eagercache;
import static io.mats3.util.eagercache.MatsEagerCacheServer.LogLevel.INFO;
import static io.mats3.util.eagercache.MatsEagerCacheServer.LogLevel.WARN;
import static io.mats3.util.eagercache.MatsEagerCacheServer.MatsEagerCacheServerImpl._formatBytes;
import static io.mats3.util.eagercache.MatsEagerCacheServer.MatsEagerCacheServerImpl._formatMillis;
import static io.mats3.util.eagercache.MatsEagerCacheServer.MatsEagerCacheServerImpl._formatNiceBytes;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import io.mats3.MatsEndpoint;
import io.mats3.MatsEndpoint.ProcessContext;
import io.mats3.MatsFactory;
import io.mats3.util.FieldBasedJacksonMapper;
import io.mats3.util.TraceId;
import io.mats3.util.compression.InflaterInputStreamWithStats;
import io.mats3.util.eagercache.MatsEagerCacheClient.MatsEagerCacheClientImpl.CacheUpdatedImpl;
import io.mats3.util.eagercache.MatsEagerCacheServer.CacheDataCallback;
import io.mats3.util.eagercache.MatsEagerCacheServer.ExceptionEntry;
import io.mats3.util.eagercache.MatsEagerCacheServer.LogEntry;
import io.mats3.util.eagercache.MatsEagerCacheServer.MatsEagerCacheServerImpl;
import io.mats3.util.eagercache.MatsEagerCacheServer.MatsEagerCacheServerImpl.BroadcastDto;
import io.mats3.util.eagercache.MatsEagerCacheServer.MatsEagerCacheServerImpl.CacheMonitor;
import io.mats3.util.eagercache.MatsEagerCacheServer.MatsEagerCacheServerImpl.CacheRequestDto;
import io.mats3.util.eagercache.MatsEagerCacheServer.MatsEagerCacheServerImpl.NodeAdvertiser;
import io.mats3.util.eagercache.MatsEagerCacheServer.MonitorCategory;
/**
* The client for the Mats Eager Cache system. This client will connect to a Mats Eager Cache server, and receive data
* from it. The client will block {@link #get()}-invocations until the initial full population is done, and during
* subsequent repopulations.
*
* Most of the MatsEagerCache system is documented in the {@link MatsEagerCacheServer} class, which is the server
* part of the system. Only the client-specific parts are documented here.
*
* Note wrt. developing a Client along with the corresponding Server: You'll face a problem where both the server
* and the client wants to listen to the same topic, which MatsFactory forbids. The solution is to use the "linked
* forwarding server" functionality, where the linked server forwards the cache updates to the client. This is done by
* invoking {@link #linkToServer(MatsEagerCacheServer)} before calling {@link #start()}.
*
* Thread-safety: This class is thread-safe after construction and {@link #start()} has been invoked, specifically
* {@link #get()} is meant to be invoked from multiple threads.
*
* @param
* the type of the data structures object that shall be returned to the "end user" - this is opposed to the
* additional {@literal } type in the constructor, which is the type that the server sends.
*
* @author Endre Stølsvik 2024-09-03 19:30 - http://stolsvik.com/, [email protected]
*/
public interface MatsEagerCacheClient {
/**
* All log lines from the Mats Eager Cache Client will be prefixed with this string. This is valuable for grepping
* or searching the logs for the Mats Eager Cache Server. Try "Grep Console" if you're using IntelliJ.
* ("#MatsEagerCache#C"
).
*/
String LOG_PREFIX_FOR_CLIENT = "#MatsEagerCache#C "; // The space is intentional, don't remove!
/**
* The default size cutover for the cache: The size in bytes for the uncompressed JSON where the cache will consider
* the update to be "large", and hence use a different scheme for handling the update. (The default is 15 MB).
*/
int DEFAULT_SIZE_CUTOVER = 15 * 1024 * 1024;
/**
* Factory for the Mats Eager Cache client, only taking full updates. The client will connect to a Mats Eager Cache
* server, and receive data from it. The client will block {@link #get()}-invocations until the initial full
* population is done, and during subsequent repopulations.
*
* Note that the corresponding server then must only send full updates.
*
* @param matsFactory
* the MatsFactory to use for the Mats Eager Cache client.
* @param dataName
* the name of the data that the client will receive.
* @param cacheTransportDataType
* the type of the received Cache Transport Data, "CTO" (the type that the server sends).
* @param fullUpdateMapper
* the function that will be invoked when a full update is received from the server. It is illegal to
* return null
or throw from this function, which will result in the client throwing an
* exception on {@link #get()}.
* @param
* the type of the cacheTransportDataType - which obviously must correspond to the type of the data that
* the server sends, but importantly, the Jackson serializer is configured very leniently, so it will
* accept any kind of JSON structure, and will not fail on missing fields or extra fields.
* @param
* the type of the data structures object that shall be returned by {@link #get()}.
*/
static MatsEagerCacheClient create(MatsFactory matsFactory, String dataName,
Class cacheTransportDataType, Function, DATA> fullUpdateMapper) {
return new MatsEagerCacheClientImpl<>(matsFactory, dataName, cacheTransportDataType, fullUpdateMapper);
}
/**
* Factory for the Mats Eager Cache client, taking both full and partial updates. The client will connect to a Mats
* Eager Cache server, and receive data from it. The client will block {@link #get()}-invocations until the initial
* full population is done, and during subsequent repopulations.
*
* Read more about the partial update mapper in the JavaDoc for {@link CacheReceivedPartialData} and in particular
* the JavaDoc for {@link MatsEagerCacheServer#sendPartialUpdate(CacheDataCallback)}, and make sure to heed the
* warning about not performing in-place updating.
*
* @param matsFactory
* the MatsFactory to use for the Mats Eager Cache client.
* @param dataName
* the name of the data that the client will receive.
* @param cacheTransportDataType
* the type of the received/transfer data (the type that the server sends).
* @param fullUpdateMapper
* the function that will be invoked when a full update is received from the server. It is illegal to
* return null or throw from this function, which will result in the client throwing an exception on
* {@link #get()}.
* @param partialUpdateMapper
* the function that will be invoked when a partial update is received from the server. It is illegal to
* return null
or throw from this function, which will result in the client throwing an
* exception on {@link #get()}.
* @param
* the type of the cacheTransportDataType - which obviously must correspond to the type of the data that
* the server sends, but importantly, the Jackson serializer is configured very leniently, so it will
* accept any kind of JSON structure, and will not fail on missing fields or extra fields.
* @param
* the type of the data structures object that shall be returned by {@link #get()}.
*
* @see CacheReceivedPartialData
* @see MatsEagerCacheServer#sendPartialUpdate(CacheDataCallback)
*/
static MatsEagerCacheClient create(MatsFactory matsFactory, String dataName,
Class cacheTransportDataType,
Function, DATA> fullUpdateMapper,
Function, DATA> partialUpdateMapper) {
MatsEagerCacheClientImpl client = new MatsEagerCacheClientImpl<>(matsFactory, dataName,
cacheTransportDataType, fullUpdateMapper);
client.setPartialUpdateMapper(partialUpdateMapper);
return client;
}
/**
* Creates a mock of the MatsEagerCacheClient for testing purposes. This mock is purely in-memory, and will not
* connect to any server, but instead will use the provided data or data supplier as the data. Data must be provided
* using one of {@link MatsEagerCacheClientMock#setMockData(Object) setMockData(DATA)} or
* {@link MatsEagerCacheClientMock#setMockDataSupplier(Supplier) setMockDataSupplier(Supplier<DATA>)} - read
* the JavaDoc for {@link MatsEagerCacheClientMock} for more information.
*
* @param dataName
* the name of the data that the client will receive - not really used for the mock.
* @return a mock of the MatsEagerCacheClient.
* @param
* the type of the data structures object that shall be returned by {@link #get()}.
*/
static MatsEagerCacheClientMock mock(String dataName) {
return new MatsEagerCacheClientMockImpl<>(dataName);
}
// ----- Interface: Configuration methods
/**
* Add a runnable that will be invoked after the initial population is done. If the initial population is already
* done, it will be invoked immediately by current thread. Note: Run ordering wrt. add ordering is not guaranteed.
*
* @param runnable
* the runnable to invoke after the initial population is done.
* @return this instance, for chaining.
*/
MatsEagerCacheClient addAfterInitialPopulationTask(Runnable runnable);
/**
* Add a listener for cache updates. The listener will be invoked with the metadata about the cache update. The
* listener is invoked after the cache content has been updated and the cache content lock has been released - i.e.
* the {@link #get()} method will already return the new data when the listener is invoked.
*
* @param listener
* the listener to add.
* @return this instance, for chaining.
*/
MatsEagerCacheClient addCacheUpdatedListener(Consumer listener);
/**
* Sets the size cutover for the cache: The size in bytes for the uncompressed JSON where the cache will consider
* the update to be "large", and hence use a more GC friendly scheme when processing the update and constructing the
* new {@link #get() DATA} instance. If large, it will null out the existing DATA instance, then add an intentional
* lag of 100 millisecond (25 ms for partial updates), before starting the decompression, deserialization and
* construction of the new DATA instance. During this time, the {@link #get() get()} method will block. This is so
* that any thread currently using the existing DATA instance shall have time to "let go" of it, and to prevent any
* other thread from getting hold of it, so that it is eligible for GC if the JVM is memory pressured by the
* generation of the new dataset. The default is 15 MB.
*
* If you don't want this feature, you can set the size cutover to {@link Integer#MAX_VALUE}, which will effectively
* disable it.
*
* If you see that the data size (as e.g. will be presented in the {@link MatsEagerCacheHtmlGui}) is close to the
* size cutover, you might want to either increase or decrease it, so that it doesn't alternate between the two
* schemes.
*
* @param size
* the size hint.
*/
void setSizeCutover(int size);
// ----- Interface: Lifecycle methods
/**
* Starts the cache client - startup is performed in a separate thread, so this method immediately returns - to wait
* for initial population, use the {@link #addAfterInitialPopulationTask(Runnable) dedicated functionality}. The
* cache client creates a SubscriptionTerminator for receiving cache updates based on the dataName. Once we're sure
* this endpoint {@link MatsEndpoint#waitForReceiving(int) has entered the receive loop}, a message to the server
* requesting update is sent. The {@link #get()} method will block until the initial full population is received and
* processed. When the initial population is done, any {@link #addAfterInitialPopulationTask(Runnable)
* onInitialPopulationTasks} will be invoked.
*
* @return this instance, for chaining.
*/
MatsEagerCacheClient start();
/**
* Links a client to a server for development and testing of a cache solution with both the server and the
* client in the same codebase, to circumvent the problem where both the server and the client needs to listen to
* the same topic, which MatsFactory forbids. By invoking this method before the {@link #start()} call, this client
* will NOT fire up its SubscriptionTerminator, but will rely on the server to forward the cache updates to the
* client - otherwise, the client is started as normal. This method is intended for development and testing only,
* and makes absolutely no sense in environments like staging, acceptance and production!
*
* Note that it also sets the "coalescing and election" timeouts to very small values (few milliseconds, as opposed
* to 2.5 and 7 seconds), to ensure that the server will forward the cache updates to the client as soon as they are
* received; It would be annoying to have to wait for the server to "coalesce" any cache updates from non-existing
* other clients before forwarding them to the client!
*
* @param forwardingServer
* the server that will forward the cache updates to this client.
* @return this instance, for chaining.
*/
MatsEagerCacheClient linkToServer(MatsEagerCacheServer forwardingServer);
/**
* Close down the cache client. This will stop and remove the SubscriptionTerminator for receiving cache updates,
* and shut down the ExecutorService for handling the received data. Closing is idempotent; Multiple invocations
* will not have any effect. It is not possible to restart the cache client after it has been closed.
*/
void close();
// ----- Interface: Client data access
/**
* Returns the data - will block until the initial full population is done, and may block during subsequent cache
* updates while the new data object is being constructed. Whether it blocks during cache updates depends on the
* size of the data as specified by the {@link #setSizeCutover(int) size cutover}.
*
* It is imperative that neither the DATA object itself, nor any of its contained larger data or structures (e.g.
* any Lists or Maps) are read out and stored in any object, or held onto by any threads for any timespan above some
* few milliseconds, but rather queried anew from the cache for every needed access. For example, if you iterate
* over a database ResultSet (i.e. perform I/O), you should fetch the data (i.e. call this method) for each new row
* - but can access it multiple times when processing each row. This both to ensure timely update when new data
* arrives, but more importantly to avoid that memory is held up by the old data structures being kept alive when
* constructing the new data! (this is particularly important for large caches!)
*
* It is legal for a thread to call this method before {@link #start()} is invoked, but then some other (startup)
* thread must eventually invoke {@link #start()}, otherwise you'll have a deadlock.
*
* @return the cached data
*/
DATA get();
// ----- Interface: Request update and information methods
/**
* If a user in some management GUI wants to force a full update, this method can be invoked. This will send a
* request to the server to perform a full update, which will be broadcast to all clients.
*
* This must NOT be used to "poll" the server for updates on a schedule or similar, as that is utterly
* against the design of the Mats Eager Cache system. The Mats Eager Cache system is designed to be a "push" system,
* where the server pushes updates to the clients when it has new data - or, as a backup, on a schedule. But this
* shall be server handled, not client handled.
*
* @param timeoutMillis
* the timeout in milliseconds for the request to complete. If the request is not completed within this
* time, the method will return Optional.empty. If ≤0 is provided, the method will return immediately,
* and the request will be performed asynchronously, returning Optional.empty.
*/
Optional requestFullUpdate(int timeoutMillis);
/**
* Returns a "live view" of the cache client information, that is, you only need to invoke this method once to get
* an instance that will always reflect the current state of the cache client.
*
* @return a "live view" of the cache client information.
*/
CacheClientInformation getCacheClientInformation();
/**
* Mock of the MatsEagerCacheClient for testing purposes. This mock is purely in-memory, and will not connect to any
* message broker or cache server, but will instead use the provided data, or data supplier, as the returned cached
* data. The goal is to simulate a real cache client as closely as possible, with some "asynchronous lags" added.
*
* - You may supply the data using one of two methods {@link #setMockData(Object)} or
* {@link #setMockDataSupplier(Supplier)} - the latter might be useful if you need to know when the data is created.
* The latest invoked of these two methods will be the one that is used.
*
- The mock will block on {@link #get()} until after {@link #start()} has been invoked, and will not block on
* {@link #get()} after that.
*
- Any after-initial-population tasks added with {@link #addAfterInitialPopulationTask(Runnable)} will be
* invoked by another thread some 5 ms after {@link #start()} has been invoked - the same thread right before
* releases the latch that {@link #get()} blocks on.
*
- Any listeners added with {@link #addCacheUpdatedListener(Consumer)} will be invoked some 5 ms after
* {@link #start()} has been invoked, and then some 5 ms after each time {@link #requestFullUpdate(int)} has been
* invoked.
*
*
* The information returned from {@link #getCacheClientInformation()} is rather mocky, but the
* {@link CacheClientInformation#getNumberOfAccesses()} will be updated each time {@link #get()} is invoked, which
* might be of interest in a testing scenario.
*
* @param
*/
interface MatsEagerCacheClientMock extends MatsEagerCacheClient {
/**
* The "simulated lags" introduced by the mock after {@link #start()} and {@link #requestFullUpdate(int)} are
* invoked. (5 milliseconds).
*/
int MILLIS_LAG = 5;
/**
* Directly set the mock data. This will be the data that {@link #get()} will return.
*
* @param data
* the data to set.
* @return this instance, for chaining.
*/
MatsEagerCacheClientMock setMockData(DATA data);
/**
* Set the mock data supplier. This supplier will be invoked each time {@link #get()} is invoked.
*
* @param dataSupplier
* the data supplier to set.
* @return this instance, for chaining.
*/
MatsEagerCacheClientMock setMockDataSupplier(Supplier dataSupplier);
/**
* Set the mock cache updated supplier - used when invoking the cache updated listeners, both on
* {@link #start()} and on {@link #requestFullUpdate(int)}. If set to null, a dummy CacheUpdated object will be
* created.
*
* @param cacheUpdatedSupplier
* the cache updated supplier to set - or null to use a dummy.
* @return this instance, for chaining.
*/
MatsEagerCacheClientMock setMockCacheUpdatedSupplier(Supplier cacheUpdatedSupplier);
/**
* This method is overridden to throw an {@link IllegalStateException}, as it makes no sense to start a mock
* cache client linked to a server.
*
* @param forwardingServer
* not used.
* @return nothing, as the method throws.
*/
@Override
default MatsEagerCacheClientMock linkToServer(MatsEagerCacheServer forwardingServer) {
throw new IllegalStateException("It makes absolutely no sense to start a mock cache client linked to a"
+ " server, thus this method throws.");
}
}
/**
* The lifecycle of the cache client, returned from {@link CacheClientInformation#getCacheClientLifeCycle()}.
*/
enum CacheClientLifecycle {
NOT_YET_STARTED, STARTING_AWAITING_INITIAL, RUNNING, STOPPING, STOPPED;
}
/**
* Information about the cache client, returned from {@link #getCacheClientInformation()}, which is updated "live"
* it- it is not a snapshot, but a live view.
*/
interface CacheClientInformation {
String getDataName();
String getNodename();
CacheClientLifecycle getCacheClientLifeCycle();
String getBroadcastTopic();
boolean isInitialPopulationDone();
long getCacheStartedTimestamp();
long getInitialPopulationRequestSentTimestamp();
long getInitialPopulationTimestamp();
long getLastAnyUpdateReceivedTimestamp();
long getLastFullUpdateReceivedTimestamp();
long getLastPartialUpdateReceivedTimestamp();
double getLastUpdateDecompressAndConsumeTotalMillis();
double getLastUpdateDecompressMillis();
double getLastUpdateDeserializeMillis();
double getLastUpdateProduceAndCompressTotalMillis();
double getLastUpdateSerializeMillis();
double getLastUpdateCompressMillis();
double getLastRoundTripTimeMillis();
long getLastUpdateCompressedSize();
long getLastUpdateDecompressedSize();
int getLastUpdateDataCount();
String getLastUpdateMetadata();
boolean isLastUpdateFull();
boolean isLastUpdateLarge();
int getNumberOfFullUpdatesReceived();
int getNumberOfPartialUpdatesReceived();
long getNumberOfAccesses();
/**
* @return timestamp of the last time the server was seen: Update, or advertisement.
*/
long getLastServerSeenTimestamp();
Map> getServerAppNamesToNodenames();
List getLogEntries();
List getExceptionEntries();
/**
* Active method for acknowledging exceptions. This method will acknowledge the exception with the provided id,
* and return whether the exception was acknowledged. If the exception is no longer in the list, or was already
* acknowledged, it will return false.
*
* @param id
* the id of the exception to acknowledge.
* @param username
* the username acknowledging the exception.
* @return whether the exception was acknowledged.
*/
boolean acknowledgeException(String id, String username);
/**
* Active method for acknowledging all exceptions up to the provided timestamp. This method will acknowledge all
* exceptions up to the provided timestamp, and return the number of exceptions acknowledged.
*
* @param timestamp
* the timestamp to acknowledge exceptions up to.
*
* @param username
* the username acknowledging the exceptions.
*/
int acknowledgeExceptionsUpTo(long timestamp, String username);
}
/**
* Metadata about the cache update.
*/
interface CacheReceived {
/**
* @return whether this was a full update.
*/
boolean isFullUpdate();
/**
* @return the size of the compressed data, in bytes.
*/
long getCompressedSize();
/**
* @return the size of the uncompressed data (probably JSON), in bytes.
*/
long getDecompressedSize();
/**
* @return number of data items received.
*/
int getDataCount();
/**
* @return the metadata that was sent along with the data, if any - otherwise {@code null}.
*/
String getMetadata();
}
/**
* Object provided to any cache update listener, containing the metadata about the cache update.
*/
interface CacheUpdated extends CacheReceived {
/**
* It this was an update received based on a request from this client - and it wasn't raced by concurrent other
* requests from other clients, nor a simultaneous periodic updates from the server - this will be set to the
* round trip time of the request.
*
* @return the full roundtrip time of the request from this client to cache updated.
*/
OptionalDouble getRoundTripTimeMillis();
}
/**
* Object that is provided to the 'fullUpdateMapper' {@link Function} which was provided to the constructor of
* {@link MatsEagerCacheClient}. This object contains the data that was received from the server, and the metadata
* that was sent along with the data.
*
* @param
* the type of the received data.
*/
interface CacheReceivedData extends CacheReceived {
/**
* @return the received data as a Stream.
*/
Stream getReceivedDataStream();
}
/**
* Object that is provided to the 'partialUpdateMapper' {@link Function} which was provided to the constructor of
* {@link MatsEagerCacheClient}. This object contains (in addition to metadata) both the data that was received from
* the server, and the previous 'D' data structures, whose structures (e.g. Lists, Sets or Maps) should be read out
* and cloned/copied, and the copies updated with the new data before returning the newly created 'D' data
* structures object.
*
* it is important that the existing structures (e.g. Lists or Maps of cached entities) aren't updated in-place
* (as they can potentially simultaneously be accessed by other threads), but rather that each of the structures
* (e.g. Lists or Maps of entities) are cloned or copied, and then the new data is overwritten or appended to in
* this copy.
*
* @param
* the type of the received data.
* @param
* the type of the data structures object that shall be returned.
*
* @see MatsEagerCacheServer#sendPartialUpdate(CacheDataCallback)
*/
interface CacheReceivedPartialData extends CacheReceivedData {
/**
* Returns the previous data, whose structures (e.g. Lists, Sets or Maps of entities) should be read out and
* cloned/copied, and the copies updated or appended with the new data before returning the newly created data
* object. This means: No in-place updating on the existing structures!
*
* @return the previous data.
*/
DATA getPreviousData();
}
// ======== The 'MatsEagerCacheClient' implementation class
class MatsEagerCacheClientImpl implements MatsEagerCacheClient {
private static final Logger log = LoggerFactory.getLogger(MatsEagerCacheClient.class);
private final MatsFactory _matsFactory;
private final String _dataName;
private final Function, DATA> _fullUpdateMapper;
private final ObjectReader _transferDataTypeReader;
private final ThreadPoolExecutor _receiveSingleBlockingThreadExecutorService;
@SuppressWarnings({ "unchecked", "rawtypes" })
private MatsEagerCacheClientImpl(MatsFactory matsFactory, String dataName,
Class transferDataType, Function, DATA> fullUpdateMapper) {
_matsFactory = matsFactory;
_dataName = dataName;
_cacheMonitor = new CacheMonitor(log, LOG_PREFIX_FOR_CLIENT, dataName);
_fullUpdateMapper = (Function, DATA>) (Function) fullUpdateMapper;
// :: Jackson JSON ObjectMapper
ObjectMapper mapper = FieldBasedJacksonMapper.getMats3DefaultJacksonObjectMapper();
// Make specific Writer for the "receivedDataType" - this is what we will need to deserialize from server.
_transferDataTypeReader = mapper.readerFor(transferDataType);
// Bare-bones assertion that we can deserialize the receivedDataType
try {
_transferDataTypeReader.readValue("{}");
}
catch (Throwable e) {
throw new IllegalArgumentException("Could not deserialize a simple JSON '{}' to the receivedDataType ["
+ transferDataType + "], which is the type that the server sends. This is a critical error,"
+ " and the client cannot be created.", e);
}
// :: ExecutorService for handling the received data.
_receiveSingleBlockingThreadExecutorService = _createSingleThreadedExecutorService("MatsEagerCacheClient-"
+ _dataName + "-receiveExecutor");
}
private volatile Function, DATA> _partialUpdateMapper;
@SuppressWarnings({ "unchecked", "rawtypes" })
MatsEagerCacheClient setPartialUpdateMapper(
Function, DATA> partialUpdateMapper) {
_partialUpdateMapper = (Function, DATA>) (Function) partialUpdateMapper;
return this;
}
// Singleton, inner NON-static class exposing "inner information" for health checks and monitoring.
private final CacheClientInformationImpl _cacheClientInformation = new CacheClientInformationImpl();
// ReadWriteLock to guard the cache content.
// Not using "fair" mode: I don't envision that the cache will be read-hammered so hard that it will be a
// problem - rather go for speed.
// This will be taken in read mode for .get(), and write mode for updates.
private final ReadWriteLock _cacheContentLock = new ReentrantReadWriteLock();
private final Lock _cacheContentReadLock = _cacheContentLock.readLock();
private final Lock _cacheContentWriteLock = _cacheContentLock.writeLock();
// Latch to hold .get() calls for the initial population to be done.
// Note: Also used as a fast-path indicator for get(), by being nulled.
private volatile CountDownLatch _initialPopulationLatch = new CountDownLatch(1);
// Tasks to run after initial population.
// Note: Also used as a fast-path indicator for addOnInitialPopulationTask(), by being nulled.
// Synchronized on this, but also volatile since we use it as fast-path indicator.
private volatile List _afterInitialPopulationTasks = new ArrayList<>();
// Listeners for cache updates
private final CopyOnWriteArrayList> _cacheUpdatedListeners = new CopyOnWriteArrayList<>();
private volatile int _sizeCutover = DEFAULT_SIZE_CUTOVER;
private volatile long _cacheStartedTimestamp;
private volatile long _initialPopulationRequestSentTimestamp;
private volatile long _initialPopulationTimestamp;
private volatile long _lastAnyUpdateReceivedTimestamp;
private volatile long _lastFullUpdateReceivedTimestamp;
private volatile long _lastPartialUpdateReceivedTimestamp;
private volatile double _lastUpdateDecompressAndConsumeTotalMillis;
private volatile double _lastUpdateDecompressMillis;
private volatile double _lastUpdateDeserializeMillis;
private volatile double _lastUpdateProduceAndCompressTotalMillis;
private volatile double _lastUpdateSerializeMillis;
private volatile double _lastUpdateCompressMillis;
private volatile double _lastRoundTripTimeMillis;
private volatile int _lastUpdateCompressedSize;
private volatile long _lastUpdateDecompressedSize;
private volatile int _lastUpdateDataCount;
private volatile String _lastUpdateMetadata;
private volatile boolean _lastUpdateWasFull;
private volatile boolean _lastUpdateWasLarge;
private final AtomicInteger _numberOfFullUpdatesReceived = new AtomicInteger();
private final AtomicInteger _numberOfPartialUpdatesReceived = new AtomicInteger();
private final AtomicLong _accessCounter = new AtomicLong();
// Synched/locked via the '_cacheContentLock'
private DATA _data;
private volatile CacheClientLifecycle _cacheClientLifecycle = CacheClientLifecycle.NOT_YET_STARTED;
private volatile MatsEndpoint, ?> _broadcastTerminator;
private volatile MatsEagerCacheServer _forwardingLinkedServer_ForDevelopment;
private volatile String _latestUsedCorrelationId;
private final CacheMonitor _cacheMonitor;
private volatile NodeAdvertiser _nodeAdvertiser;
private class CacheClientInformationImpl implements CacheClientInformation {
@Override
public String getDataName() {
return _dataName;
}
@Override
public String getNodename() {
return _matsFactory.getFactoryConfig().getNodename();
}
@Override
public CacheClientLifecycle getCacheClientLifeCycle() {
return _cacheClientLifecycle;
}
@Override
public String getBroadcastTopic() {
return MatsEagerCacheServerImpl._getBroadcastTopic(_dataName);
}
@Override
public boolean isInitialPopulationDone() {
return _initialPopulationLatch == null;
}
@Override
public long getCacheStartedTimestamp() {
return _cacheStartedTimestamp;
}
@Override
public long getInitialPopulationRequestSentTimestamp() {
return _initialPopulationRequestSentTimestamp;
}
@Override
public long getInitialPopulationTimestamp() {
return _initialPopulationTimestamp;
}
@Override
public long getLastAnyUpdateReceivedTimestamp() {
return _lastAnyUpdateReceivedTimestamp;
}
@Override
public long getLastFullUpdateReceivedTimestamp() {
return _lastFullUpdateReceivedTimestamp;
}
@Override
public long getLastPartialUpdateReceivedTimestamp() {
return _lastPartialUpdateReceivedTimestamp;
}
@Override
public double getLastUpdateDecompressAndConsumeTotalMillis() {
return _lastUpdateDecompressAndConsumeTotalMillis;
}
@Override
public double getLastUpdateDecompressMillis() {
return _lastUpdateDecompressMillis;
}
@Override
public double getLastUpdateDeserializeMillis() {
return _lastUpdateDeserializeMillis;
}
@Override
public double getLastUpdateProduceAndCompressTotalMillis() {
return _lastUpdateProduceAndCompressTotalMillis;
}
@Override
public double getLastUpdateSerializeMillis() {
return _lastUpdateSerializeMillis;
}
@Override
public double getLastUpdateCompressMillis() {
return _lastUpdateCompressMillis;
}
@Override
public double getLastRoundTripTimeMillis() {
return _lastRoundTripTimeMillis;
}
@Override
public long getLastUpdateCompressedSize() {
return _lastUpdateCompressedSize;
}
@Override
public long getLastUpdateDecompressedSize() {
return _lastUpdateDecompressedSize;
}
@Override
public int getLastUpdateDataCount() {
return _lastUpdateDataCount;
}
@Override
public String getLastUpdateMetadata() {
return _lastUpdateMetadata;
}
@Override
public boolean isLastUpdateFull() {
return _lastUpdateWasFull;
}
@Override
public boolean isLastUpdateLarge() {
return _lastUpdateWasLarge;
}
@Override
public int getNumberOfFullUpdatesReceived() {
return _numberOfFullUpdatesReceived.get();
}
@Override
public int getNumberOfPartialUpdatesReceived() {
return _numberOfPartialUpdatesReceived.get();
}
@Override
public long getNumberOfAccesses() {
return _accessCounter.get();
}
@Override
public long getLastServerSeenTimestamp() {
return _nodeAdvertiser._lastServerSeenTimestamp;
}
@Override
public Map> getServerAppNamesToNodenames() {
synchronized (_nodeAdvertiser._serversAppNamesToNodenames) {
// Return copy, with copy of sets.
return _nodeAdvertiser._serversAppNamesToNodenames.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> new TreeSet<>(e.getValue())));
}
}
@Override
public List getLogEntries() {
return _cacheMonitor.getLogEntries();
}
@Override
public List getExceptionEntries() {
return _cacheMonitor.getExceptionEntries();
}
@Override
public boolean acknowledgeException(String id, String username) {
return _cacheMonitor.acknowledgeException(id, username);
}
@Override
public int acknowledgeExceptionsUpTo(long timestamp, String username) {
return _cacheMonitor.acknowledgeExceptionsUpTo(timestamp, username);
}
}
@Override
public MatsEagerCacheClient addAfterInitialPopulationTask(Runnable runnable) {
boolean runNow = true;
// ?: Is the initial population list still present, that is, are we still in the initial population phase?
if (_afterInitialPopulationTasks != null) {
// -> Yes, still present, so we're still in the initial population phase.
synchronized (this) {
// ?: Is the list still present?
if (_afterInitialPopulationTasks != null) {
// -> Yes, still present, so add the runnable to the list.
_afterInitialPopulationTasks.add(runnable);
// We don't run it now, as we're still in the initial population phase.
runNow = false;
}
// E-> The list was nulled out by the initial population thread, and the runnable was run.
}
}
// ?: Should we run it now? That is, have initial population been done?
if (runNow) {
// -> Yes, initial population is done, so run it now.
runnable.run();
}
// For chaining
return this;
}
@Override
public MatsEagerCacheClient addCacheUpdatedListener(Consumer listener) {
_cacheUpdatedListeners.add(listener);
return this;
}
@Override
public void setSizeCutover(int size) {
_sizeCutover = size;
}
@Override
public DATA get() {
// :: Handle initial population
// ?: Do we need to wait for the initial population to be done (fast-path null check)? Read directly from
// field.
if (_initialPopulationLatch != null) {
// -> The field was non-null.
// Doing "double check" using local variable, to avoid race condition on nulling out the field.
var localLatch = _initialPopulationLatch;
// ?: Is the latch still non-null?
if (localLatch != null) {
// -> Yes, so we have to wait on it.
// Note: We now use the local variable, so it won't be nulled out by the other thread.
// This also means that we can be raced by the initial population thread, but that is fine, since
// we'll then sail through the latch.await() immediately.
try {
localLatch.await();
}
catch (InterruptedException e) {
var msg = "Got interrupted while waiting for initial population to be done.";
_cacheMonitor.exception(MonitorCategory.GET, msg, e);
throw new RuntimeException(msg, e);
}
}
}
// :: Get the data, within the read lock.
_cacheContentReadLock.lock();
try {
if (_data == null) {
var msg = "The data is null, which the cache API contract explicitly forbids!"
+ " Fix your cache update code!";
var up = new IllegalStateException(msg);
_cacheMonitor.exception(MonitorCategory.GET, msg, up);
throw up;
}
_accessCounter.incrementAndGet();
return _data;
}
finally {
_cacheContentReadLock.unlock();
}
}
@Override
public MatsEagerCacheClient start() {
if (_cacheClientLifecycle != CacheClientLifecycle.NOT_YET_STARTED) {
throw new IllegalStateException("The MatsEagerCacheClient for data [" + _dataName
+ "] has already been started.");
}
_cacheStartedTimestamp = System.currentTimeMillis();
// Set lifecycle to "starting", so that we can handle the initial population.
_cacheClientLifecycle = CacheClientLifecycle.STARTING_AWAITING_INITIAL;
// ?: Are we in development mode, where we're linking to a server in the same codebase?
if (_forwardingLinkedServer_ForDevelopment != null) {
// -> Yes, so we start with the server.
_startWithServer(_forwardingLinkedServer_ForDevelopment);
return this;
}
// E-> No, so we start with the SubscriptionTerminator.
_cacheMonitor.log(INFO, MonitorCategory.CACHE_CLIENT, "Starting the MatsEagerCacheClient"
+ " for data [" + _dataName + "].");
// :: Listener to the update topic.
_broadcastTerminator = _matsFactory.subscriptionTerminator(
MatsEagerCacheServerImpl._getBroadcastTopic(_dataName),
void.class, BroadcastDto.class, this::_processLambdaForSubscriptionTerminator);
// Allow log suppression
_broadcastTerminator.getEndpointConfig().setAttribute(
MatsEagerCacheServerImpl.SUPPRESS_LOGGING_ENDPOINT_ALLOWS_ATTRIBUTE_KEY, Boolean.TRUE);
// :: Perform the initial cache update request in a separate thread, so that we can wait for the endpoint to
// be ready, and then send the request. We return immediately.
Thread thread = new Thread(() -> {
// :: Wait for the receiving (Broadcast) endpoint to be ready
// 10 minutes should really be enough for the service to finish boot and start the MatsFactory, right?
boolean started = _broadcastTerminator.waitForReceiving(600_000);
try {
_initialPopulationRequestSentTimestamp = System.currentTimeMillis();
_sendUpdateRequest(CacheRequestDto.COMMAND_REQUEST_BOOT);
// Start the AppName and Nodename advertiser.
_nodeAdvertiser = new NodeAdvertiser(_matsFactory, _cacheMonitor, false,
BroadcastDto.COMMAND_CLIENT_ADVERTISE, _dataName,
_matsFactory.getFactoryConfig().getAppName(), _matsFactory.getFactoryConfig()
.getNodename());
}
finally {
// ?: Did it start?
if (!started) {
// -> No, so that's bad.
var msg = "The Update handling SubscriptionTerminator Endpoint would not start within 10 minutes.";
_cacheMonitor.exception(MonitorCategory.INITIAL_POPULATION, msg, new IllegalStateException(
msg));
}
}
});
thread.setName("MatsEagerCacheClient-" + _dataName + "-initialCacheUpdateRequest");
// If the JVM is shut down due to bad boot, this thread should not prevent it from exiting.
thread.setDaemon(true);
// Start it.
thread.start();
return this;
}
@Override
public MatsEagerCacheClient linkToServer(MatsEagerCacheServer forwardingLinkedServer) {
_forwardingLinkedServer_ForDevelopment = forwardingLinkedServer;
return this;
}
@Override
public Optional requestFullUpdate(int timeoutMillis) {
CountDownLatch latch = new CountDownLatch(1);
CacheUpdated[] cacheUpdatedReturn = new CacheUpdated[1];
// Hijacking our own CacheUpdate listener to get the response.
Consumer cacheUpdatedConsumer = cacheUpdated -> {
// ?: Is this a full update? (Otherwise it can't be the response to our request)
if (cacheUpdated.isFullUpdate()) {
// -> Yes, fullupdate, so hope that it is the response to our request.
// NOTE: We really can't be sure, with all the coalescing going on. This will work if we were alone
// in making the update request, but if there were multiple clients doing it, or if the server did a
// periodic update at the same time, our request can be coalesced into that. The first one wins.
cacheUpdatedReturn[0] = cacheUpdated;
latch.countDown();
}
};
_cacheUpdatedListeners.add(cacheUpdatedConsumer);
try {
_sendUpdateRequest(CacheRequestDto.COMMAND_REQUEST_MANUAL);
// ?: Should we wait for the response?
if (timeoutMillis <= 0) {
// -> No, we should not wait for the response.
return Optional.empty();
}
// E-> Yes, we should wait for the response.
boolean awoken = latch.await(timeoutMillis, TimeUnit.MILLISECONDS);
return awoken ? Optional.of(cacheUpdatedReturn[0]) : Optional.empty();
}
catch (InterruptedException e) {
_cacheMonitor.exception(MonitorCategory.REQUEST_UPDATE_CLIENT_MANUAL,
"Got interrupted while waiting for the full update response.", e);
// Resetting interrupt flag and returning empty. Up for discussion whether this is the right
// thing, but it should really only happen if the JVM is shutting down.
Thread.currentThread().interrupt();
return Optional.empty();
}
finally {
_cacheUpdatedListeners.remove(cacheUpdatedConsumer);
}
}
@Override
public void close() {
_cacheMonitor.log(INFO, MonitorCategory.CACHE_CLIENT, "Closing down the MatsEagerCacheClient"
+ " for data [" + _dataName + "].");
// Stop the executor anyway.
_receiveSingleBlockingThreadExecutorService.shutdown();
synchronized (this) {
// ?: Are we running? Note: we accept multiple close() invocations, as the close-part is harder to
// lifecycle manage than the start-part (It might e.g. be closed by Spring too, in addition to by the
// user).
if (!EnumSet.of(CacheClientLifecycle.RUNNING, CacheClientLifecycle.STARTING_AWAITING_INITIAL)
.contains(_cacheClientLifecycle)) {
// -> No, we're not running, so we don't do anything.
return;
}
_cacheClientLifecycle = CacheClientLifecycle.STOPPING;
}
// -> Yes, we're started, so close down.
if (_nodeAdvertiser != null) {
_nodeAdvertiser.stop();
}
if (_broadcastTerminator != null) {
_broadcastTerminator.remove(30_000);
}
synchronized (this) {
_cacheClientLifecycle = CacheClientLifecycle.STOPPED;
}
}
public CacheClientInformation getCacheClientInformation() {
return _cacheClientInformation;
}
// ======== Implementation / Internal methods ========
public MatsEagerCacheClient _startWithServer(MatsEagerCacheServer forwardingServer) {
_cacheMonitor.log(INFO, MonitorCategory.CACHE_CLIENT, "Starting the Linked-to-Server"
+ " MatsEagerCacheClient for data [" + _dataName + "].");
// !! We do NOT start the SubscriptionTerminator, but rely on the server to forward the cache updates to us.
// Link to server - this is immediate, not waiting for anything.
((MatsEagerCacheServerImpl) forwardingServer)._registerForwardToClient(this);
// Since this is meant for development/testing, we'll reduce the coalescing timings hard
// (Otherwise, the .get() on the client, which is what a test will do, will block for the "long" delay
// (7 sec) right after start)
((MatsEagerCacheServerImpl) forwardingServer)._setCoalescingDelays(10, 15);
// :: Perform the initial cache update request in a separate thread, so that we can wait for the endpoint to
// be ready, and then send the request. We return immediately.
Thread thread = new Thread(() -> {
// :: Wait for the server to be ready.
try {
((MatsEagerCacheServerImpl) forwardingServer)._waitForReceiving(2 * 60);
}
catch (Throwable t) {
var msg = "The linked server would not start within 2 minutes.";
var up = new IllegalStateException(msg);
_cacheMonitor.exception(MonitorCategory.INITIAL_POPULATION, msg, up);
throw up;
}
_initialPopulationRequestSentTimestamp = System.currentTimeMillis();
_sendUpdateRequest(CacheRequestDto.COMMAND_REQUEST_BOOT);
});
thread.setName("MatsEagerCacheClient-" + _dataName + "-initialCacheUpdateRequest-Linked-to-Server");
// If the JVM is shut down due to bad boot, this thread should not prevent it from exiting.
thread.setDaemon(true);
// Start it.
thread.start();
return this;
}
private void _sendUpdateRequest(String command) {
// :: Construct the request message
CacheRequestDto req = new CacheRequestDto();
req.nodename = _matsFactory.getFactoryConfig().getNodename();
req.appName = _matsFactory.getFactoryConfig().getAppName();
req.sentTimestamp = System.currentTimeMillis();
req.sentNanoTime = System.nanoTime();
req.command = command;
req.correlationId = Long.toString(Math.abs(ThreadLocalRandom.current().nextLong()), 36);
_latestUsedCorrelationId = req.correlationId;
String reason = command.equals(CacheRequestDto.COMMAND_REQUEST_BOOT)
? "InitialCacheUpdateRequest"
: "ManualCacheUpdateRequest";
MonitorCategory category = command.equals(CacheRequestDto.COMMAND_REQUEST_BOOT)
? MonitorCategory.REQUEST_UPDATE_CLIENT_BOOT
: MonitorCategory.REQUEST_UPDATE_CLIENT_MANUAL;
_cacheMonitor.log(INFO, category, "Sending " + reason + " [" + command + "] to server on queue '"
+ MatsEagerCacheServerImpl._getCacheRequestQueue(_dataName) + "'.");
// :: Send it off
try {
// NOTE: WE DO *NOT* LOG-SUPPRESS THE REQUEST FROM CLIENT MESSAGES!
_matsFactory.getDefaultInitiator().initiate(init -> init.traceId(
TraceId.create(_matsFactory.getFactoryConfig().getAppName(),
"MatsEagerCacheClient-" + _dataName, reason))
.from("MatsEagerCacheClient." + _dataName + ".UpdateRequest")
.to(MatsEagerCacheServerImpl._getCacheRequestQueue(_dataName))
.send(req));
}
catch (Throwable t) {
var msg = "Got exception when initiating " + reason + ".";
_cacheMonitor.exception(category, msg, t);
throw new IllegalStateException(msg, t);
}
}
void _processLambdaForSubscriptionTerminator(ProcessContext> ctx, Void state, BroadcastDto msg) {
// ?: Is the NodeAdvertiser in place? (Will be after RUNNING)
if (_nodeAdvertiser != null) {
// -> Yes, so snoop on messages for keeping track of whom the clients and servers are.
_nodeAdvertiser.handleAdvertise(msg);
}
// ?: Check if we're interested in this message - we only care about updates.
if (!(BroadcastDto.COMMAND_UPDATE_FULL.equals(msg.command)
|| BroadcastDto.COMMAND_UPDATE_PARTIAL.equals(msg.command))) {
// -> None of my concern
return;
}
// ?: Check if we're running (or awaiting), so we don't accidentally process updates after close.
if ((_cacheClientLifecycle != CacheClientLifecycle.RUNNING)
&& (_cacheClientLifecycle != CacheClientLifecycle.STARTING_AWAITING_INITIAL)) {
// -> We're not running or waiting for initial population, so we don't process the update.
_cacheMonitor.log(INFO, MonitorCategory.RECEIVED_UPDATE, "Got update [" + msg.command
+ "] from server, but we're not RUNNING or STARTING_AWAITING_INITIAL, so we don't process it.");
return;
}
final byte[] payload = ctx.getBytes(MatsEagerCacheServer.SIDELOAD_KEY_DATA_PAYLOAD);
/*
* :: Now perform hack to relieve the Mats thread, and do the actual work in a separate thread. The
* rationale is to let the data structures underlying in the Mats system (e.g. the representation of the
* incoming JMS Message) be GCable. The thread pool is special in that it only accepts a single task, and if
* it is busy, it will block the submitting thread. NOTE: We do NOT capture the context (which holds the
* MatsTrace and byte arrays) in the Runnable, as that would prevent the JMS Message from being GC'ed. We
* only capture the message DTO and the payload.
*/
_sendUpdateToExecutor_EnsureWhatIsCaptured(msg, payload);
}
private void _sendUpdateToExecutor_EnsureWhatIsCaptured(BroadcastDto msg, byte[] payload) {
_receiveSingleBlockingThreadExecutorService.submit(() -> {
_handleUpdateInExecutorThread(msg, payload);
});
}
private void _handleUpdateInExecutorThread(BroadcastDto msg, byte[] payload) {
String threadTackOn;
if (msg.command.equals(BroadcastDto.COMMAND_UPDATE_FULL)) {
int num = _numberOfFullUpdatesReceived.incrementAndGet();
threadTackOn = "Full #" + num;
}
else {
int num = _numberOfPartialUpdatesReceived.incrementAndGet();
threadTackOn = "Partial #" + num;
}
String originalThreadName = Thread.currentThread().getName();
Thread.currentThread().setName("MatsEagerCacheClient-" + _dataName + "-handleUpdateInThread-"
+ threadTackOn);
boolean fullUpdate = msg.command.equals(BroadcastDto.COMMAND_UPDATE_FULL);
boolean largeUpdate = msg.uncompressedSize > _sizeCutover;
// ## FIRST: Process and update the cache data.
// Handle it.
long nanosAsStart_update = System.nanoTime();
boolean lockTaken = false;
// ?: Is this a large update?
if (largeUpdate) {
// -> Yes, so then we'll write lock it and delete (if full update) the existing data *before* updating.
_cacheContentWriteLock.lock();
lockTaken = true;
}
long nanosTaken_decompress;
long nanosTaken_deserialize;
try {
int dataSize = msg.dataCount;
String metadata = msg.metadata;
// ?: Is this a full update?
if (fullUpdate) {
// -> :: FULL UPDATE, so then we update the entire dataset
// ?: Is this a large update?
if (largeUpdate) {
// -> Yes, large update
// :: Null out the existing data *before* sleeping and updating, so that the GC can collect it.
_data = null;
// Sleep for a little while, both to let the Mats thread finish up and release the underlying MQ
// resources (e.g. JMS Message), and to let any threads that have acquired references to the
// data finish up and release them. E.g. if a thread has acquired a reference to e.g. a Map from
// DATA, and is iterating over it, it will be done within a few milliseconds, and then the
// thread will release the reference. If we're in a tight memory situation, the GC will then
// be able to collect the data structure, before we populate it anew.
//
// This sleep will affect all users of the cache, but a large bit of the premise is that the
// cache is not updated that often, so that this sleep won't be noticeable in the grand
// scheme of things.
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
_cacheMonitor.exception(MonitorCategory.RECEIVED_UPDATE, "Got interrupted while"
+ " sleeping before acting on full update. Assuming shutdown,"
+ " thus returning immediately.", e);
return;
}
}
// :: Perform the update
DATA newData = null;
try {
// Invoke the full update mapper
// (Holding onto as little as possible while invoking the mapper, to let the GC do its job.)
InflaterInputStreamWithStats iis = new InflaterInputStreamWithStats(payload);
MappingIterator