/*
* Copyright (c) 2010-2016. Axon Framework
*
* 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 org.axonframework.eventhandling.saga.repository;
import org.axonframework.common.Assert;
import org.axonframework.common.caching.Cache;
import org.axonframework.eventhandling.saga.AssociationValue;
import org.axonframework.eventhandling.saga.AssociationValues;
import org.axonframework.eventsourcing.eventstore.TrackingToken;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
/**
* Saga Repository implementation that adds caching behavior to the repository it wraps. Both associations and sagas
* are cached, making loading them faster. Commits and adds are always delegated to the wrapped repository. Loads are
* only delegated if the cache does not contain the necessary entries.
* <p/>
* Updating associations involves a read and a write, which are performed atomically. Therefore, it is unsafe to add or
* remove specific associations outside of this instance. Obviously, clearing and evictions are safe.
*
* @author Allard Buijze
* @since 2.0
*/
public class CachingSagaStore<T> implements SagaStore<T> {
private final SagaStore<T> delegate;
// guarded by "associationsCacheLock"
private final Cache associationsCache;
private final Cache sagaCache;
/**
* Initializes an instance delegating to the given {@code delegate}, storing associations in the given
* {@code associationsCache} and Saga instances in the given {@code sagaCache}.
*
* @param delegate The repository instance providing access to (persisted) entries
* @param associationsCache The cache to store association information is
* @param sagaCache The cache to store Saga instances in
*/
public CachingSagaStore(SagaStore<T> delegate, Cache associationsCache, Cache sagaCache) {
Assert.notNull(delegate, () -> "You must provide a SagaRepository instance to delegate to");
Assert.notNull(associationsCache, () -> "You must provide a Cache instance to store the association values");
Assert.notNull(sagaCache, () -> "You must provide a Cache instance to store the sagas");
this.delegate = delegate;
this.associationsCache = associationsCache;
this.sagaCache = sagaCache;
}
@Override
public Set<String> findSagas(Class<? extends T> sagaType, AssociationValue associationValue) {
final String key = cacheKey(associationValue, sagaType);
// this is a dirty read, but a cache should be thread safe anyway
Set<String> associations = associationsCache.get(key);
if (associations == null) {
associations = delegate.findSagas(sagaType, associationValue);
associationsCache.put(key, associations);
}
return new HashSet<>(associations);
}
@Override
public <S extends T> Entry<S> loadSaga(Class<S> sagaType, String sagaIdentifier) {
Entry<S> saga = sagaCache.get(sagaIdentifier);
if (saga == null) {
saga = delegate.loadSaga(sagaType, sagaIdentifier);
if (saga != null) {
sagaCache.put(sagaIdentifier, new CacheEntry<T>(saga));
}
}
return saga;
}
@Override
public void insertSaga(Class<? extends T> sagaType, String sagaIdentifier, T saga, TrackingToken token,
Set<AssociationValue> associationValues) {
delegate.insertSaga(sagaType, sagaIdentifier, saga, token, associationValues);
sagaCache.put(sagaIdentifier, new CacheEntry<>(saga, token, associationValues));
addCachedAssociations(associationValues, sagaIdentifier, sagaType);
}
@Override
public void deleteSaga(Class<? extends T> sagaType, String sagaIdentifier,
Set<AssociationValue> associationValues) {
sagaCache.remove(sagaIdentifier);
associationValues.forEach(av -> removeAssociationValueFromCache(sagaType, sagaIdentifier, av));
delegate.deleteSaga(sagaType, sagaIdentifier, associationValues);
}
private void removeAssociationValueFromCache(Class<?> sagaType, String sagaIdentifier,
AssociationValue associationValue) {
String key = cacheKey(associationValue, sagaType);
Set<String> associations = associationsCache.get(key);
if (associations != null && associations.remove(sagaIdentifier)) {
associationsCache.put(key, associations);
}
}
/**
* Registers the associations of a saga with given {@code sagaIdentifier} and given {@code sagaType} with the
* associations cache.
*
* @param associationValues the association values of the saga
* @param sagaIdentifier the identifier of the saga
* @param sagaType the type of the saga
*/
protected void addCachedAssociations(Iterable<AssociationValue> associationValues, String sagaIdentifier,
Class<?> sagaType) {
for (AssociationValue associationValue : associationValues) {
String key = cacheKey(associationValue, sagaType);
Set<String> identifiers = associationsCache.get(key);
if (identifiers != null && identifiers.add(sagaIdentifier)) {
associationsCache.put(key, identifiers);
}
}
}
@Override
public void updateSaga(Class<? extends T> sagaType, String sagaIdentifier, T saga, TrackingToken token,
AssociationValues associationValues) {
sagaCache.put(sagaIdentifier, new CacheEntry<>(saga, token, associationValues.asSet()));
delegate.updateSaga(sagaType, sagaIdentifier, saga, token, associationValues);
associationValues.removedAssociations()
.forEach(av -> removeAssociationValueFromCache(sagaType, sagaIdentifier, av));
addCachedAssociations(associationValues.addedAssociations(), sagaIdentifier, sagaType);
}
private String cacheKey(AssociationValue associationValue, Class<?> sagaType) {
return sagaType.getName() + "/" + associationValue.getKey() + "=" + associationValue.getValue();
}
private static class CacheEntry<T> implements Entry<T>, Serializable {
private final T saga;
private final TrackingToken trackingToken;
private final Set<AssociationValue> associationValues;
public CacheEntry(T saga, TrackingToken TrackingToken, Set<AssociationValue> associationValues) {
this.saga = saga;
this.trackingToken = TrackingToken;
this.associationValues = associationValues;
}
public <S extends T> CacheEntry(Entry<S> other) {
this.saga = other.saga();
this.trackingToken = other.trackingToken();
this.associationValues = other.associationValues();
}
@Override
public TrackingToken trackingToken() {
return trackingToken;
}
@Override
public Set<AssociationValue> associationValues() {
return associationValues;
}
@Override
public T saga() {
return saga;
}
}
}