/*
* Copyright Terracotta, 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 org.ehcache.clustered.client.internal;
import org.ehcache.CachePersistenceException;
import org.ehcache.clustered.client.internal.lock.VoltronReadWriteLock;
import org.ehcache.clustered.client.internal.lock.VoltronReadWriteLock.Hold;
import org.ehcache.clustered.client.internal.store.ClusterTierClientEntity;
import org.ehcache.clustered.client.internal.store.InternalClusterTierClientEntity;
import org.ehcache.clustered.client.service.EntityBusyException;
import org.ehcache.clustered.common.ServerSideConfiguration;
import org.ehcache.clustered.common.internal.ClusterTierManagerConfiguration;
import org.ehcache.clustered.common.internal.ServerStoreConfiguration;
import org.ehcache.clustered.common.internal.exceptions.ClusterException;
import org.ehcache.clustered.common.internal.exceptions.DestroyInProgressException;
import org.ehcache.clustered.common.internal.store.ClusterTierEntityConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terracotta.connection.Connection;
import org.terracotta.connection.entity.EntityRef;
import org.terracotta.exception.EntityAlreadyExistsException;
import org.terracotta.exception.EntityConfigurationException;
import org.terracotta.exception.EntityException;
import org.terracotta.exception.EntityNotFoundException;
import org.terracotta.exception.EntityNotProvidedException;
import org.terracotta.exception.EntityVersionMismatchException;
import org.terracotta.exception.PermanentEntityException;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import static org.ehcache.clustered.common.EhcacheEntityVersion.ENTITY_VERSION;
public class ClusterTierManagerClientEntityFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(ClusterTierManagerClientEntityFactory.class);
private final Connection connection;
private final Map<String, Hold> maintenanceHolds = new ConcurrentHashMap<String, Hold>();
private final Timeouts entityTimeouts;
public ClusterTierManagerClientEntityFactory(Connection connection) {
this(connection, Timeouts.builder().build());
}
public ClusterTierManagerClientEntityFactory(Connection connection, Timeouts entityTimeouts) {
this.connection = connection;
this.entityTimeouts = entityTimeouts;
}
public boolean acquireLeadership(String entityIdentifier) {
VoltronReadWriteLock lock = createAccessLockFor(entityIdentifier);
Hold hold = lock.tryWriteLock();
if (hold == null) {
return false;
} else {
maintenanceHolds.put(entityIdentifier, hold);
return true;
}
}
public void abandonLeadership(String entityIdentifier) {
Hold hold = maintenanceHolds.remove(entityIdentifier);
if (hold == null) {
throw new IllegalMonitorStateException("Leadership was never held");
} else {
hold.unlock();
}
}
/**
* Attempts to create and configure the {@code EhcacheActiveEntity} in the Ehcache clustered server.
*
* @param identifier the instance identifier for the {@code EhcacheActiveEntity}
* @param config the {@code EhcacheActiveEntity} configuration to use for creation
*
* @throws EntityAlreadyExistsException if the {@code EhcacheActiveEntity} for {@code identifier} already exists
* @throws ClusterTierManagerCreationException if an error preventing {@code EhcacheActiveEntity} creation was raised
* @throws EntityBusyException if another client holding operational leadership prevented this client
* from becoming leader and creating the {@code EhcacheActiveEntity} instance
* @throws TimeoutException if the creation and configuration of the {@code EhcacheActiveEntity} exceed the
* lifecycle operation timeout
*/
public void create(final String identifier, final ServerSideConfiguration config)
throws EntityAlreadyExistsException, ClusterTierManagerCreationException, EntityBusyException, TimeoutException {
Hold existingMaintenance = maintenanceHolds.get(identifier);
Hold localMaintenance = null;
if (existingMaintenance == null) {
localMaintenance = createAccessLockFor(identifier).tryWriteLock();
}
if (existingMaintenance == null && localMaintenance == null) {
throw new EntityBusyException("Unable to create cluster tier manager for id "
+ identifier + ": another client owns the maintenance lease");
}
boolean finished = false;
try {
EntityRef<InternalClusterTierManagerClientEntity, ClusterTierManagerConfiguration, Void> ref = getEntityRef(identifier);
try {
while (true) {
ref.create(new ClusterTierManagerConfiguration(identifier, config));
try {
InternalClusterTierManagerClientEntity entity = ref.fetchEntity(null);
try {
entity.setTimeouts(entityTimeouts);
finished = true;
return;
} finally {
if (finished) {
entity.close();
} else {
silentlyClose(entity, identifier);
}
}
} catch (EntityNotFoundException e) {
//continue;
}
}
} catch (EntityConfigurationException e) {
throw new ClusterTierManagerCreationException("Unable to configure cluster tier manager for id " + identifier, e);
} catch (EntityNotProvidedException e) {
LOGGER.error("Unable to create cluster tier manager for id {}", identifier, e);
throw new AssertionError(e);
} catch (EntityVersionMismatchException e) {
LOGGER.error("Unable to create cluster tier manager for id {}", identifier, e);
throw new AssertionError(e);
}
} finally {
if (localMaintenance != null) {
if (finished) {
localMaintenance.unlock();
} else {
silentlyUnlock(localMaintenance, identifier);
}
}
}
}
/**
* Attempts to retrieve a reference to an existing {@code EhcacheActiveEntity} in an Ehcache clustered server.
*
* @param identifier the instance identifier for the {@code EhcacheActiveEntity}
* @param config the {@code EhcacheActiveEntity} configuration to use for access checking
*
* @return an {@code ClusterTierManagerClientEntity} providing access to the {@code EhcacheActiveEntity} identified by
* {@code identifier}
*
* @throws EntityNotFoundException if the {@code EhcacheActiveEntity} identified as {@code identifier} does not exist
* @throws IllegalArgumentException if {@code config} does not match the {@code EhcacheActiveEntity} configuration
* @throws TimeoutException if the creation and configuration of the {@code EhcacheActiveEntity} exceed the
* lifecycle operation timeout
*/
public ClusterTierManagerClientEntity retrieve(String identifier, ServerSideConfiguration config)
throws DestroyInProgressException, EntityNotFoundException, ClusterTierManagerValidationException, TimeoutException {
Hold fetchHold = createAccessLockFor(identifier).readLock();
InternalClusterTierManagerClientEntity entity;
try {
entity = getEntityRef(identifier).fetchEntity(null);
} catch (EntityVersionMismatchException e) {
LOGGER.error("Unable to retrieve cluster tier manager for id {}", identifier, e);
silentlyUnlock(fetchHold, identifier);
throw new AssertionError(e);
}
entity.setTimeouts(entityTimeouts);
boolean validated = false;
try {
entity.validate(config);
validated = true;
return entity;
} catch (DestroyInProgressException e) {
throw e;
} catch (ClusterException e) {
throw new ClusterTierManagerValidationException("Unable to validate cluster tier manager for id " + identifier, e);
} finally {
if (!validated) {
silentlyClose(entity, identifier);
silentlyUnlock(fetchHold, identifier);
}
}
}
public void destroy(final String identifier) throws ClusterTierManagerNotFoundException, EntityBusyException {
Hold existingMaintenance = maintenanceHolds.get(identifier);
Hold localMaintenance = null;
if (existingMaintenance == null) {
localMaintenance = createAccessLockFor(identifier).tryWriteLock();
}
if (existingMaintenance == null && localMaintenance == null) {
throw new EntityBusyException("Destroy operation failed; " + identifier + " cluster tier's maintenance lease held");
}
boolean finished = false;
try {
EntityRef<InternalClusterTierManagerClientEntity, ClusterTierManagerConfiguration, Void> ref = getEntityRef(identifier);
destroyAllClusterTiers(ref, identifier);
try {
if (!ref.destroy()) {
throw new EntityBusyException("Destroy operation failed; " + identifier + " cluster tier in use by other clients");
}
finished = true;
} catch (EntityNotProvidedException e) {
LOGGER.error("Unable to delete cluster tier manager for id {}", identifier, e);
throw new AssertionError(e);
} catch (EntityNotFoundException e) {
// Ignore - entity does not exist
} catch (PermanentEntityException e) {
LOGGER.error("Unable to destroy entity - server says it is permanent", e);
throw new AssertionError(e);
}
} finally {
if (localMaintenance != null) {
if (finished) {
localMaintenance.unlock();
} else {
silentlyUnlock(localMaintenance, identifier);
}
}
}
}
private void destroyAllClusterTiers(EntityRef<InternalClusterTierManagerClientEntity, ClusterTierManagerConfiguration, Void> ref, String identifier) throws ClusterTierManagerNotFoundException {
InternalClusterTierManagerClientEntity entity;
try {
entity = ref.fetchEntity(null);
entity.setClientId(UUID.randomUUID());
} catch (EntityNotFoundException e) {
// Ignore - means entity does not exist
return;
} catch (EntityVersionMismatchException e) {
throw new AssertionError(e);
}
Set<String> storeIdentifiers = entity.prepareForDestroy();
LOGGER.warn("Preparing to destroy stores {}", storeIdentifiers);
for (String storeIdentifier : storeIdentifiers) {
try {
destroyClusteredStoreEntity(identifier, storeIdentifier);
} catch (EntityNotFoundException e) {
// Ignore - assume it was deleted already
} catch (CachePersistenceException e) {
throw new AssertionError("Unable to destroy cluster tier - in use: " + e.getMessage());
}
}
entity.close();
}
private void silentlyClose(ClusterTierManagerClientEntity entity, String identifier) {
try {
entity.close();
} catch (Exception e) {
LOGGER.error("Failed to close entity {}", identifier, e);
}
}
private void silentlyUnlock(Hold localMaintenance, String identifier) {
try {
localMaintenance.unlock();
} catch(Exception e) {
LOGGER.error("Failed to unlock for id {}", identifier, e);
}
}
private VoltronReadWriteLock createAccessLockFor(String entityIdentifier) {
return new VoltronReadWriteLock(connection, "ClusterTierManagerClientEntityFactory-AccessLock-" + entityIdentifier);
}
private EntityRef<InternalClusterTierManagerClientEntity, ClusterTierManagerConfiguration, Void> getEntityRef(String identifier) {
try {
return connection.getEntityRef(InternalClusterTierManagerClientEntity.class, ENTITY_VERSION, identifier);
} catch (EntityNotProvidedException e) {
LOGGER.error("Unable to get cluster tier manager for id {}", identifier, e);
throw new AssertionError(e);
}
}
public ClusterTierClientEntity fetchOrCreateClusteredStoreEntity(UUID clientId, String clusterTierManagerIdentifier,
String storeIdentifier, ServerStoreConfiguration clientStoreConfiguration,
boolean autoCreate) throws EntityNotFoundException, CachePersistenceException {
EntityRef<InternalClusterTierClientEntity, ClusterTierEntityConfiguration, Void> entityRef;
try {
entityRef = connection.getEntityRef(InternalClusterTierClientEntity.class, ENTITY_VERSION, entityName(clusterTierManagerIdentifier, storeIdentifier));
} catch (EntityNotProvidedException e) {
throw new AssertionError(e);
}
if (autoCreate) {
while (true) {
try {
entityRef.create(new ClusterTierEntityConfiguration(clusterTierManagerIdentifier, storeIdentifier, clientStoreConfiguration));
} catch (EntityAlreadyExistsException e) {
// Ignore - entity exists
} catch (EntityConfigurationException e) {
throw new CachePersistenceException("Unable to create cluster tier", e);
} catch (EntityException e) {
throw new AssertionError(e);
}
try {
InternalClusterTierClientEntity entity = entityRef.fetchEntity(null);
entity.setClientId(clientId);
entity.setStoreIdentifier(storeIdentifier);
entity.setTimeouts(entityTimeouts);
return entity;
} catch (EntityNotFoundException e) {
// Ignore - will try to create again
} catch (EntityException e) {
throw new AssertionError(e);
}
}
} else {
try {
InternalClusterTierClientEntity entity = entityRef.fetchEntity(null);
entity.setClientId(clientId);
entity.setStoreIdentifier(storeIdentifier);
entity.setTimeouts(entityTimeouts);
return entity;
} catch (EntityNotFoundException e) {
throw e;
} catch (EntityException e) {
throw new AssertionError(e);
}
}
}
public void destroyClusteredStoreEntity(String clusterTierManagerIdentifier, String storeIdentifier) throws EntityNotFoundException, CachePersistenceException {
EntityRef<InternalClusterTierClientEntity, ClusterTierEntityConfiguration, Void> entityRef;
try {
entityRef = connection.getEntityRef(InternalClusterTierClientEntity.class, ENTITY_VERSION, entityName(clusterTierManagerIdentifier, storeIdentifier));
if (!entityRef.destroy()) {
throw new CachePersistenceException("Cannot destroy cluster tier '" + storeIdentifier + "': in use by other client(s)");
}
} catch (EntityNotProvidedException e) {
throw new AssertionError(e);
} catch (PermanentEntityException e) {
throw new AssertionError(e);
}
}
private static String entityName(String clusterTierManagerIdentifier, String storeIdentifier) {
return clusterTierManagerIdentifier + "$" + storeIdentifier;
}
}