/* * 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.store; import org.ehcache.clustered.client.config.TimeoutDuration; import org.ehcache.clustered.client.internal.Timeouts; import org.ehcache.clustered.client.internal.service.ClusterTierException; import org.ehcache.clustered.client.internal.service.ClusterTierValidationException; import org.ehcache.clustered.common.internal.ServerStoreConfiguration; import org.ehcache.clustered.common.internal.exceptions.ClusterException; import org.ehcache.clustered.common.internal.messages.ClusterTierReconnectMessage; import org.ehcache.clustered.common.internal.messages.EhcacheEntityMessage; import org.ehcache.clustered.common.internal.messages.EhcacheEntityResponse; import org.ehcache.clustered.common.internal.messages.EhcacheEntityResponse.Failure; import org.ehcache.clustered.common.internal.messages.EhcacheMessageType; import org.ehcache.clustered.common.internal.messages.EhcacheOperationMessage; import org.ehcache.clustered.common.internal.messages.EhcacheResponseType; import org.ehcache.clustered.common.internal.messages.LifeCycleMessageFactory; import org.ehcache.clustered.common.internal.messages.ReconnectMessageCodec; import org.ehcache.clustered.common.internal.messages.ServerStoreOpMessage; import org.ehcache.clustered.common.internal.messages.StateRepositoryOpMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terracotta.entity.EndpointDelegate; import org.terracotta.entity.EntityClientEndpoint; import org.terracotta.entity.EntityResponse; import org.terracotta.entity.InvokeFuture; import org.terracotta.entity.MessageCodecException; import org.terracotta.exception.EntityException; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; /** * ClusterTierClientEntity */ public class SimpleClusterTierClientEntity implements InternalClusterTierClientEntity { private static final Logger LOGGER = LoggerFactory.getLogger(SimpleClusterTierClientEntity.class); private static final Set<EhcacheMessageType> GET_STORE_OPS = EnumSet.of(EhcacheMessageType.GET_STORE); private final EntityClientEndpoint<EhcacheEntityMessage, EhcacheEntityResponse> endpoint; private final LifeCycleMessageFactory messageFactory; private final AtomicLong sequenceGenerator = new AtomicLong(0L); private final Object lock = new Object(); private final ReconnectMessageCodec reconnectMessageCodec = new ReconnectMessageCodec(); private final Map<Class<? extends EhcacheEntityResponse>, List<ResponseListener<? extends EhcacheEntityResponse>>> responseListeners = new ConcurrentHashMap<Class<? extends EhcacheEntityResponse>, List<ResponseListener<? extends EhcacheEntityResponse>>>(); private UUID clientId; private ReconnectListener reconnectListener = new ReconnectListener() { @Override public void onHandleReconnect(ClusterTierReconnectMessage reconnectMessage) { // No op } }; private DisconnectionListener disconnectionListener = new DisconnectionListener() { @Override public void onDisconnection() { // No op } }; private Timeouts timeouts = Timeouts.builder().build(); private String storeIdentifier; private volatile boolean connected = true; public SimpleClusterTierClientEntity(EntityClientEndpoint<EhcacheEntityMessage, EhcacheEntityResponse> endpoint) { this.endpoint = endpoint; this.messageFactory = new LifeCycleMessageFactory(); endpoint.setDelegate(new EndpointDelegate() { @Override public void handleMessage(EntityResponse messageFromServer) { LOGGER.trace("Entity response received from server: {}", messageFromServer); if (messageFromServer instanceof EhcacheEntityResponse) { fireResponseEvent((EhcacheEntityResponse) messageFromServer); } } @Override public byte[] createExtendedReconnectData() { synchronized (lock) { ClusterTierReconnectMessage reconnectMessage = new ClusterTierReconnectMessage(clientId); reconnectListener.onHandleReconnect(reconnectMessage); return reconnectMessageCodec.encode(reconnectMessage); } } @Override public void didDisconnectUnexpectedly() { fireDisconnectionEvent(); } }); } @Override public void setTimeouts(Timeouts timeouts) { this.timeouts = timeouts; } void fireDisconnectionEvent() { connected = false; disconnectionListener.onDisconnection(); } private <T extends EhcacheEntityResponse> void fireResponseEvent(T response) { @SuppressWarnings("unchecked") List<ResponseListener<T>> responseListeners = (List) this.responseListeners.get(response.getClass()); if (responseListeners == null) { LOGGER.warn("Ignoring the response {} as no registered response listener could be found.", response); return; } LOGGER.debug("{} registered response listener(s) for {}", responseListeners.size(), response.getClass()); for (ResponseListener<T> responseListener : responseListeners) { responseListener.onResponse(response); } } @Override public void close() { reconnectListener = null; disconnectionListener = null; endpoint.close(); } @Override public void setClientId(UUID clientId) { this.clientId = clientId; messageFactory.setClientId(clientId); } @Override public UUID getClientId() { if (clientId == null) { throw new IllegalStateException("Client Id cannot be null"); } return this.clientId; } @Override public void setReconnectListener(ReconnectListener reconnectListener) { this.reconnectListener = reconnectListener; } @Override public void setDisconnectionListener(DisconnectionListener disconnectionListener) { this.disconnectionListener = disconnectionListener; } @Override public boolean isConnected() { return connected; } @Override public <T extends EhcacheEntityResponse> void addResponseListener(Class<T> responseType, ResponseListener<T> responseListener) { List<ResponseListener<? extends EhcacheEntityResponse>> responseListeners = this.responseListeners.get(responseType); if (responseListeners == null) { responseListeners = new CopyOnWriteArrayList<ResponseListener<? extends EhcacheEntityResponse>>(); this.responseListeners.put(responseType, responseListeners); } responseListeners.add(responseListener); } @Override public void validate(ServerStoreConfiguration clientStoreConfiguration) throws ClusterTierException, TimeoutException { try { invokeInternal(timeouts.getLifecycleOperationTimeout(), messageFactory.validateServerStore(storeIdentifier , clientStoreConfiguration), false); } catch (ClusterException e) { throw new ClusterTierValidationException("Error validating cluster tier '" + storeIdentifier + "'", e); } } @Override public void setStoreIdentifier(String storeIdentifier) { this.storeIdentifier = storeIdentifier; } void setConnected(boolean connected) { this.connected = connected; } @Override public EhcacheEntityResponse invokeServerStoreOperation(ServerStoreOpMessage message, boolean track) throws ClusterException, TimeoutException { return invoke(message, track); } @Override public EhcacheEntityResponse invokeStateRepositoryOperation(StateRepositoryOpMessage message, boolean track) throws ClusterException, TimeoutException { return invoke(message, track); } @Override public void invokeServerStoreOperationAsync(ServerStoreOpMessage message, boolean track) throws MessageCodecException { internalInvokeAsync(message, track); } private EhcacheEntityResponse invoke(EhcacheOperationMessage message, boolean track) throws ClusterException, TimeoutException { TimeoutDuration timeLimit = timeouts.getMutativeOperationTimeout(); if (GET_STORE_OPS.contains(message.getMessageType())) { timeLimit = timeouts.getReadOperationTimeout(); } return invokeInternal(timeLimit, message, track); } private EhcacheEntityResponse invokeInternal(TimeoutDuration timeLimit, EhcacheEntityMessage message, boolean track) throws ClusterException, TimeoutException { try { EhcacheEntityResponse response = waitFor(timeLimit, internalInvokeAsync(message, track)); if (EhcacheResponseType.FAILURE.equals(response.getResponseType())) { throw ((Failure)response).getCause(); } else { return response; } } catch (EntityException e) { throw new RuntimeException(message + " error: " + e.toString(), e); } catch (MessageCodecException e) { throw new RuntimeException(message + " error: " + e.toString(), e); } catch (TimeoutException e) { String msg = "Timeout exceeded for " + message + " message; " + timeLimit; TimeoutException timeoutException = new TimeoutException(msg); timeoutException.initCause(e); LOGGER.info(msg, timeoutException); throw timeoutException; } } private InvokeFuture<EhcacheEntityResponse> internalInvokeAsync(EhcacheEntityMessage message, boolean track) throws MessageCodecException { getClientId(); if (track) { message.setId(sequenceGenerator.getAndIncrement()); } return endpoint.beginInvoke().message(message).invoke(); } private static <T> T waitFor(TimeoutDuration timeLimit, InvokeFuture<T> future) throws EntityException, TimeoutException { boolean interrupted = false; long deadlineTimeout = System.nanoTime() + timeLimit.toNanos(); try { while (true) { try { long timeRemaining = deadlineTimeout - System.nanoTime(); return future.getWithTimeout(timeRemaining, TimeUnit.NANOSECONDS); } catch (InterruptedException e) { interrupted = true; } } } finally { if (interrupted) { Thread.currentThread().interrupt(); } } } }