/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.activemq.artemis.core.server.group.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.management.CoreNotificationType;
import org.apache.activemq.artemis.api.core.management.ManagementHelper;
import org.apache.activemq.artemis.core.persistence.OperationContext;
import org.apache.activemq.artemis.core.persistence.StorageManager;
import org.apache.activemq.artemis.core.postoffice.BindingType;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.core.server.management.ManagementService;
import org.apache.activemq.artemis.core.server.management.Notification;
import org.apache.activemq.artemis.utils.ConcurrentUtil;
import org.apache.activemq.artemis.utils.ExecutorFactory;
import org.apache.activemq.artemis.utils.collections.TypedProperties;
import org.jboss.logging.Logger;
/**
* A Local Grouping handler. All the Remote handlers will talk with us
*/
public final class LocalGroupingHandler extends GroupHandlingAbstract {
private static final Logger logger = Logger.getLogger(LocalGroupingHandler.class);
private final ConcurrentMap<SimpleString, GroupBinding> map = new ConcurrentHashMap<>();
private final ConcurrentMap<SimpleString, List<GroupBinding>> groupMap = new ConcurrentHashMap<>();
private final SimpleString name;
private final StorageManager storageManager;
private final long timeout;
private final Lock lock = new ReentrantLock();
private final Condition awaitCondition = lock.newCondition();
/**
* This contains a list of expected bindings to be loaded
* when the group is waiting for them.
* During a small window between the server is started and the wait wasn't called yet, this will contain bindings that were already added
*/
private List<SimpleString> expectedBindings = new LinkedList<>();
private final long groupTimeout;
private boolean waitingForBindings = false;
private final ScheduledExecutorService scheduledExecutor;
private boolean started;
private ScheduledFuture reaperFuture;
private final long reaperPeriod;
public LocalGroupingHandler(final ExecutorFactory executorFactory,
final ScheduledExecutorService scheduledExecutor,
final ManagementService managementService,
final SimpleString name,
final SimpleString address,
final StorageManager storageManager,
final long timeout,
final long groupTimeout,
long reaperPeriod) {
super(executorFactory.getExecutor(), managementService, address);
this.reaperPeriod = reaperPeriod;
this.scheduledExecutor = scheduledExecutor;
this.name = name;
this.storageManager = storageManager;
this.timeout = timeout;
this.groupTimeout = groupTimeout;
}
@Override
public SimpleString getName() {
return name;
}
@Override
public Response propose(final Proposal proposal) throws Exception {
OperationContext originalCtx = storageManager.getContext();
try {
// the waitCompletion cannot be done inside an ordered executor or we would starve when the thread pool is full
storageManager.setContext(storageManager.newSingleThreadContext());
if (proposal.getClusterName() == null) {
GroupBinding original = map.get(proposal.getGroupId());
if (original != null) {
original.use();
return new Response(proposal.getGroupId(), original.getClusterName());
} else {
return null;
}
}
boolean addRecord = false;
GroupBinding groupBinding = null;
lock.lock();
try {
groupBinding = map.get(proposal.getGroupId());
if (groupBinding != null) {
groupBinding.use();
// Returning with an alternate cluster name, as it's been already grouped
return new Response(groupBinding.getGroupId(), proposal.getClusterName(), groupBinding.getClusterName());
} else {
addRecord = true;
groupBinding = new GroupBinding(proposal.getGroupId(), proposal.getClusterName());
groupBinding.setId(storageManager.generateID());
List<GroupBinding> newList = new ArrayList<>();
List<GroupBinding> oldList = groupMap.putIfAbsent(groupBinding.getClusterName(), newList);
if (oldList != null) {
newList = oldList;
}
newList.add(groupBinding);
map.put(groupBinding.getGroupId(), groupBinding);
}
} finally {
lock.unlock();
}
// Storing the record outside of any locks
if (addRecord) {
storageManager.addGrouping(groupBinding);
}
return new Response(groupBinding.getGroupId(), groupBinding.getClusterName());
} finally {
storageManager.setContext(originalCtx);
}
}
@Override
public void resendPending() throws Exception {
// this only make sense on RemoteGroupingHandler.
// this is a no-op on the local one
}
@Override
public void proposed(final Response response) throws Exception {
}
@Override
public void remove(SimpleString groupid, SimpleString clusterName, int distance) throws Exception {
remove(groupid, clusterName);
}
@Override
public void sendProposalResponse(final Response response, final int distance) throws Exception {
TypedProperties props = new TypedProperties();
props.putSimpleStringProperty(ManagementHelper.HDR_PROPOSAL_GROUP_ID, response.getGroupId());
props.putSimpleStringProperty(ManagementHelper.HDR_PROPOSAL_VALUE, response.getClusterName());
props.putSimpleStringProperty(ManagementHelper.HDR_PROPOSAL_ALT_VALUE, response.getAlternativeClusterName());
props.putIntProperty(ManagementHelper.HDR_BINDING_TYPE, BindingType.LOCAL_QUEUE_INDEX);
props.putSimpleStringProperty(ManagementHelper.HDR_ADDRESS, address);
props.putIntProperty(ManagementHelper.HDR_DISTANCE, distance);
Notification notification = new Notification(null, CoreNotificationType.PROPOSAL_RESPONSE, props);
managementService.sendNotification(notification);
}
@Override
public Response receive(final Proposal proposal, final int distance) throws Exception {
logger.trace("received proposal " + proposal);
return propose(proposal);
}
@Override
public void addGroupBinding(final GroupBinding groupBinding) {
map.put(groupBinding.getGroupId(), groupBinding);
List<GroupBinding> newList = new ArrayList<>();
List<GroupBinding> oldList = groupMap.putIfAbsent(groupBinding.getClusterName(), newList);
if (oldList != null) {
newList = oldList;
}
newList.add(groupBinding);
}
@Override
public Response getProposal(final SimpleString fullID, final boolean touchTime) {
GroupBinding original = map.get(fullID);
if (original != null) {
if (touchTime) {
original.use();
}
return new Response(fullID, original.getClusterName());
} else {
return null;
}
}
@Override
public void remove(SimpleString groupid, SimpleString clusterName) {
GroupBinding groupBinding = map.remove(groupid);
List<GroupBinding> groupBindings = groupMap.get(clusterName);
if (groupBindings != null && groupBinding != null) {
groupBindings.remove(groupBinding);
try {
long tx = storageManager.generateID();
storageManager.deleteGrouping(tx, groupBinding);
storageManager.commitBindings(tx);
} catch (Exception e) {
// nothing we can do being log
ActiveMQServerLogger.LOGGER.warn(e.getMessage(), e);
}
}
}
@Override
public void awaitBindings() throws Exception {
lock.lock();
try {
if (groupMap.size() > 0) {
waitingForBindings = true;
//make a copy of the bindings added so far from the cluster via onNotification()
List<SimpleString> bindingsAlreadyAdded;
if (expectedBindings == null) {
bindingsAlreadyAdded = Collections.emptyList();
expectedBindings = new LinkedList<>();
} else {
bindingsAlreadyAdded = new ArrayList<>(expectedBindings);
//clear the bindings
expectedBindings.clear();
}
//now add all the group bindings that were loaded by the journal
expectedBindings.addAll(groupMap.keySet());
//and if we remove persisted bindings from whats been added so far we have left any bindings we havent yet
//received via onNotification
expectedBindings.removeAll(bindingsAlreadyAdded);
if (expectedBindings.size() > 0) {
logger.debug("Waiting remote group bindings to arrive before starting the server. timeout=" + timeout + " milliseconds");
//now we wait here for the rest to be received in onNotification, it will signal once all have been received.
//if we aren't signaled then bindingsAdded still has some groupids we need to remove.
if (!ConcurrentUtil.await(awaitCondition, timeout)) {
ActiveMQServerLogger.LOGGER.remoteGroupCoordinatorsNotStarted();
}
}
}
} finally {
expectedBindings = null;
waitingForBindings = false;
lock.unlock();
}
}
@Override
public void onNotification(final Notification notification) {
if (!(notification.getType() instanceof CoreNotificationType))
return;
if (notification.getType() == CoreNotificationType.BINDING_REMOVED) {
SimpleString clusterName = notification.getProperties().getSimpleStringProperty(ManagementHelper.HDR_CLUSTER_NAME);
removeGrouping(clusterName);
} else if (notification.getType() == CoreNotificationType.BINDING_ADDED) {
SimpleString clusterName = notification.getProperties().getSimpleStringProperty(ManagementHelper.HDR_CLUSTER_NAME);
try {
lock.lock();
if (expectedBindings != null) {
if (waitingForBindings) {
if (expectedBindings.remove(clusterName)) {
logger.debug("OnNotification for waitForbindings::Removed clusterName=" + clusterName + " from lis succesffully");
} else {
logger.debug("OnNotification for waitForbindings::Couldn't remove clusterName=" + clusterName + " as it wasn't on the original list");
}
} else {
expectedBindings.add(clusterName);
logger.debug("Notification for waitForbindings::Adding previously known item clusterName=" + clusterName);
}
if (logger.isDebugEnabled()) {
for (SimpleString stillWaiting : expectedBindings) {
logger.debug("Notification for waitForbindings::Still waiting for clusterName=" + stillWaiting);
}
}
if (expectedBindings.size() == 0) {
awaitCondition.signal();
}
}
} finally {
lock.unlock();
}
}
}
@Override
public synchronized void start() throws Exception {
if (started)
return;
if (expectedBindings == null) {
// just in case the component is restarted
expectedBindings = new LinkedList<>();
}
if (reaperPeriod > 0 && groupTimeout > 0) {
if (reaperFuture != null) {
reaperFuture.cancel(true);
reaperFuture = null;
}
reaperFuture = scheduledExecutor.scheduleAtFixedRate(new GroupReaperScheduler(), reaperPeriod, reaperPeriod, TimeUnit.MILLISECONDS);
}
started = true;
}
@Override
public synchronized void stop() throws Exception {
started = false;
if (reaperFuture != null) {
reaperFuture.cancel(true);
reaperFuture = null;
}
}
@Override
public boolean isStarted() {
return started;
}
private void removeGrouping(final SimpleString clusterName) {
final List<GroupBinding> list = groupMap.remove(clusterName);
if (list != null) {
executor.execute(new Runnable() {
@Override
public void run() {
long txID = -1;
for (GroupBinding val : list) {
if (val != null) {
fireUnproposed(val.getGroupId());
map.remove(val.getGroupId());
sendUnproposal(val.getGroupId(), clusterName, 0);
try {
if (txID < 0) {
txID = storageManager.generateID();
}
storageManager.deleteGrouping(txID, val);
} catch (Exception e) {
ActiveMQServerLogger.LOGGER.unableToDeleteGroupBindings(e, val.getGroupId());
}
}
}
if (txID >= 0) {
try {
storageManager.commitBindings(txID);
} catch (Exception e) {
ActiveMQServerLogger.LOGGER.unableToDeleteGroupBindings(e, SimpleString.toSimpleString("TX:" + txID));
}
}
}
});
}
}
private final class GroupReaperScheduler implements Runnable {
final GroupIdReaper reaper = new GroupIdReaper();
@Override
public void run() {
executor.execute(reaper);
}
}
private final class GroupIdReaper implements Runnable {
@Override
public void run() {
// The reaper thread should be finished case the PostOffice is gone
// This is to avoid leaks on PostOffice between stops and starts
if (isStarted()) {
long txID = -1;
int expiredGroups = 0;
for (GroupBinding groupBinding : map.values()) {
if ((groupBinding.getTimeUsed() + groupTimeout) < System.currentTimeMillis()) {
map.remove(groupBinding.getGroupId());
List<GroupBinding> groupBindings = groupMap.get(groupBinding.getClusterName());
groupBindings.remove(groupBinding);
fireUnproposed(groupBinding.getGroupId());
sendUnproposal(groupBinding.getGroupId(), groupBinding.getClusterName(), 0);
expiredGroups++;
try {
if (txID < 0) {
txID = storageManager.generateID();
}
storageManager.deleteGrouping(txID, groupBinding);
if (expiredGroups >= 1000 && txID >= 0) {
storageManager.commitBindings(txID);
expiredGroups = 0;
txID = -1;
}
} catch (Exception e) {
ActiveMQServerLogger.LOGGER.unableToDeleteGroupBindings(e, groupBinding.getGroupId());
}
}
}
if (txID >= 0) {
try {
storageManager.commitBindings(txID);
} catch (Exception e) {
ActiveMQServerLogger.LOGGER.unableToDeleteGroupBindings(e, SimpleString.toSimpleString("TX:" + txID));
}
}
}
}
}
}