/*
* 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.server;
import org.ehcache.clustered.common.ServerSideConfiguration;
import org.ehcache.clustered.common.internal.ClusterTierManagerConfiguration;
import org.ehcache.clustered.common.internal.exceptions.ClusterException;
import org.ehcache.clustered.common.internal.exceptions.InvalidClientIdException;
import org.ehcache.clustered.common.internal.exceptions.InvalidOperationException;
import org.ehcache.clustered.common.internal.exceptions.LifecycleException;
import org.ehcache.clustered.common.internal.messages.EhcacheEntityMessage;
import org.ehcache.clustered.common.internal.messages.EhcacheEntityResponse;
import org.ehcache.clustered.common.internal.messages.EhcacheEntityResponseFactory;
import org.ehcache.clustered.common.internal.messages.EhcacheMessageType;
import org.ehcache.clustered.common.internal.messages.EhcacheOperationMessage;
import org.ehcache.clustered.common.internal.messages.LifecycleMessage;
import org.ehcache.clustered.common.internal.messages.ClusterTierManagerReconnectMessage;
import org.ehcache.clustered.common.internal.messages.ReconnectMessageCodec;
import org.ehcache.clustered.server.management.Management;
import org.ehcache.clustered.server.state.EhcacheStateService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terracotta.entity.ActiveServerEntity;
import org.terracotta.entity.BasicServiceConfiguration;
import org.terracotta.entity.ClientDescriptor;
import org.terracotta.entity.ConfigurationException;
import org.terracotta.entity.IEntityMessenger;
import org.terracotta.entity.PassiveSynchronizationChannel;
import org.terracotta.entity.ServiceRegistry;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import static org.ehcache.clustered.common.internal.messages.EhcacheMessageType.isLifecycleMessage;
import static org.ehcache.clustered.common.internal.messages.LifecycleMessage.ValidateStoreManager;
public class ClusterTierManagerActiveEntity implements ActiveServerEntity<EhcacheEntityMessage, EhcacheEntityResponse> {
private static final Logger LOGGER = LoggerFactory.getLogger(ClusterTierManagerActiveEntity.class);
/**
* Tracks the state of a connected client. An entry is added to this map when the
* {@link #connected(ClientDescriptor)} method is invoked for a client and removed when the
* {@link #disconnected(ClientDescriptor)} method is invoked for the client.
*/
private final Map<ClientDescriptor, ClientState> clientStateMap = new ConcurrentHashMap<>();
private final ReconnectMessageCodec reconnectMessageCodec = new ReconnectMessageCodec();
private final EhcacheEntityResponseFactory responseFactory;
private final EhcacheStateService ehcacheStateService;
private final IEntityMessenger entityMessenger;
private final Management management;
private final AtomicBoolean reconnectComplete = new AtomicBoolean(true);
private final ServerSideConfiguration configuration;
public ClusterTierManagerActiveEntity(ServiceRegistry services, ClusterTierManagerConfiguration config,
EhcacheStateService ehcacheStateService, Management management) throws ConfigurationException {
if (config == null) {
throw new ConfigurationException("ClusterTierManagerConfiguration cannot be null");
}
this.configuration = config.getConfiguration();
this.responseFactory = new EhcacheEntityResponseFactory();
this.ehcacheStateService = ehcacheStateService;
if (ehcacheStateService == null) {
throw new AssertionError("Server failed to retrieve EhcacheStateService.");
}
entityMessenger = services.getService(new BasicServiceConfiguration<>(IEntityMessenger.class));
if (entityMessenger == null) {
throw new AssertionError("Server failed to retrieve IEntityMessenger service.");
}
try {
ehcacheStateService.configure();
this.management = management;
} catch (ConfigurationException e) {
ehcacheStateService.destroy();
throw e;
}
}
/**
* Gets the map of connected clients along with the server stores each is using.
* If the client is using no stores, the set of stores will be empty for that client.
*
* @return an unmodifiable copy of the connected client map
*/
// This method is intended for unit test use; modifications are likely needed for other (monitoring) purposes
Set<ClientDescriptor> getConnectedClients() {
final Set<ClientDescriptor> clients = new HashSet<>();
for (Entry<ClientDescriptor, ClientState> entry : clientStateMap.entrySet()) {
clients.add(entry.getKey());
}
return Collections.unmodifiableSet(clients);
}
@Override
public void connected(ClientDescriptor clientDescriptor) {
if (!clientStateMap.containsKey(clientDescriptor)) {
LOGGER.info("Connecting {}", clientDescriptor);
ClientState clientState = new ClientState();
clientStateMap.put(clientDescriptor, clientState);
management.clientConnected(clientDescriptor, clientState);
} else {
// This is logically an AssertionError
LOGGER.error("Client {} already registered as connected", clientDescriptor);
}
}
@Override
public void disconnected(ClientDescriptor clientDescriptor) {
ClientState clientState = clientStateMap.remove(clientDescriptor);
if (clientState == null) {
// This is logically an AssertionError
LOGGER.error("Client {} not registered as connected", clientDescriptor);
} else {
LOGGER.info("Disconnecting {}", clientDescriptor);
management.clientDisconnected(clientDescriptor, clientState);
}
}
@Override
public EhcacheEntityResponse invoke(ClientDescriptor clientDescriptor, EhcacheEntityMessage message) {
try {
if (message instanceof EhcacheOperationMessage) {
EhcacheOperationMessage operationMessage = (EhcacheOperationMessage) message;
EhcacheMessageType messageType = operationMessage.getMessageType();
if (isLifecycleMessage(messageType)) {
return invokeLifeCycleOperation(clientDescriptor, (LifecycleMessage) message);
}
}
throw new AssertionError("Unsupported message : " + message.getClass());
} catch (ClusterException e) {
return responseFactory.failure(e);
} catch (Exception e) {
LOGGER.error("Unexpected exception raised during operation: " + message, e);
return responseFactory.failure(new InvalidOperationException(e));
}
}
@Override
public void handleReconnect(ClientDescriptor clientDescriptor, byte[] extendedReconnectData) {
ClientState clientState = this.clientStateMap.get(clientDescriptor);
if (clientState == null) {
throw new AssertionError("Client "+ clientDescriptor +" trying to reconnect is not connected to entity");
}
ClusterTierManagerReconnectMessage reconnectMessage = reconnectMessageCodec.decodeReconnectMessage(extendedReconnectData);
clientState.attach(reconnectMessage.getClientId());
LOGGER.info("Client '{}' successfully reconnected to newly promoted ACTIVE after failover.", clientDescriptor);
management.clientReconnected(clientDescriptor, clientState);
}
@Override
public void synchronizeKeyToPassive(PassiveSynchronizationChannel<EhcacheEntityMessage> syncChannel, int concurrencyKey) {
// Nothing to sync
}
@Override
public void createNew() {
management.init();
management.sharedPoolsConfigured();
}
@Override
public void loadExisting() {
ehcacheStateService.loadExisting(configuration);
LOGGER.debug("Preparing for handling Inflight Invalidations and independent Passive Evictions in loadExisting");
reconnectComplete.set(false);
management.init();
management.sharedPoolsConfigured();
}
private void validateClientConnected(ClientDescriptor clientDescriptor) throws ClusterException {
ClientState clientState = this.clientStateMap.get(clientDescriptor);
if (clientState == null) {
throw new LifecycleException("Client " + clientDescriptor + " is not connected to the cluster tier manager");
}
}
private EhcacheEntityResponse invokeLifeCycleOperation(ClientDescriptor clientDescriptor, LifecycleMessage message) throws ClusterException {
switch (message.getMessageType()) {
case VALIDATE:
validate(clientDescriptor, (ValidateStoreManager) message);
break;
case PREPARE_FOR_DESTROY:
return prepareForDestroy();
default:
throw new AssertionError("Unsupported LifeCycle operation " + message);
}
return responseFactory.success();
}
private EhcacheEntityResponse prepareForDestroy() {
EhcacheEntityResponse.PrepareForDestroy response = new EhcacheEntityResponse.PrepareForDestroy(ehcacheStateService
.getStores());
ehcacheStateService.prepareForDestroy();
return response;
}
/**
* {@inheritDoc}
* <p>
* This method is invoked in response to a call to a {@code com.tc.objectserver.api.ServerEntityRequest}
* message for a {@code ServerEntityAction.DESTROY_ENTITY} request which is sent via a call to the
* {@code ClusteringService.destroyAll} method. This method is expected to be called only when no
* clients actively using this entity.
*/
@Override
public void destroy() {
ehcacheStateService.destroy();
}
/**
* Handles the {@link ValidateStoreManager ValidateStoreManager} message. This message is used by a client to
* connect to an established {@code ClusterTierManagerActiveEntity}. This method validates the client-provided configuration
* against the existing configuration to ensure compatibility.
*
* @param clientDescriptor the client identifier requesting attachment to a configured store manager
* @param message the {@code ValidateStoreManager} message carrying the client expected resource pool configuration
*/
private void validate(ClientDescriptor clientDescriptor, ValidateStoreManager message) throws ClusterException {
validateClientConnected(clientDescriptor);
ClientState clientState = clientStateMap.get(clientDescriptor);
UUID clientId = clientState.getClientIdentifier();
if (clientId != null) {
throw new LifecycleException("Client : " + clientDescriptor + " is already being tracked with Client Id : " + clientId);
}
if (getTrackedClients().contains(message.getClientId())) {
throw new InvalidClientIdException("Client ID : " + message.getClientId() + " is already being tracked.");
}
ehcacheStateService.validate(message.getConfiguration());
clientState.attach(message.getClientId());
management.clientValidated(clientDescriptor, clientState);
}
private Set<UUID> getTrackedClients() {
return clientStateMap.entrySet().stream()
.filter(entry -> entry.getValue().isAttached())
.map(entry -> entry.getValue().getClientIdentifier())
.collect(Collectors.toSet());
}
}