/* dCache - http://www.dcache.org/
*
* Copyright (C) 2015 Deutsches Elektronen-Synchrotron
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package dmg.cells.services.login;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import dmg.cells.nucleus.CellAddressCore;
import dmg.cells.nucleus.CellCommandListener;
import dmg.cells.nucleus.CellEndpoint;
import dmg.cells.nucleus.CellEvent;
import dmg.cells.nucleus.CellEventListener;
import dmg.cells.nucleus.CellLifeCycleAware;
import dmg.cells.nucleus.CellMessage;
import dmg.cells.nucleus.CellMessageAnswerable;
import dmg.cells.nucleus.CellMessageReceiver;
import dmg.cells.nucleus.CellMessageSender;
import dmg.cells.nucleus.CellRoute;
import dmg.cells.nucleus.DelayedReply;
import dmg.cells.nucleus.NoRouteToCellException;
import dmg.util.command.Command;
import dmg.util.command.Option;
import static com.google.common.collect.Collections2.*;
import static dmg.cells.services.login.LoginBrokerInfo.Capability.READ;
import static dmg.cells.services.login.LoginBrokerInfo.Capability.WRITE;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableCollection;
import static java.util.Collections.unmodifiableMap;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* Subscriber of LoginBrokerInfo updates.
*
* Maintains a list of available doors. A door is removed from the list when
* an update hasn't been received for 2.5 times the update time of that door.
* This allows one missed update before the door is removed.
*/
public class LoginBrokerSubscriber
implements CellCommandListener, CellMessageReceiver, LoginBrokerSource, CellMessageSender, CellEventListener, CellLifeCycleAware
{
public static final double EXPIRATION_FACTOR = 2.5;
/**
* Map from door names to LoginBrokerInfo of that door. Expired entries are removed
* lazily when retrieving the list or updating other entries.
*/
private final ConcurrentMap<String, Entry> doorsByIdentity = new ConcurrentHashMap<>();
/**
* Queue of entries in expiration order. There is O(log n) time overhead for every entry due
* to maintaining this queue. Entries are not removed until expired even if an updated entry
* has been inserted. Removing old entries upon update would be O(n) time, so we treat a
* typical 2.5 time increase in memory for lower time complexity.
*/
private final DelayQueue<Entry> queue = new DelayQueue<>();
/**
* Immutable view of unexpired LoginBrokerInfos.
*/
private final Collection<LoginBrokerInfo> unmodifiableView =
unmodifiableCollection(transform(filter(doorsByIdentity.values(), Entry::isValid), Entry::getLoginBrokerInfo));
/**
* Read doors grouped by protocol.
*/
private final ByProtocolMap readDoors = new ByProtocolMap();
/**
* Write doors grouped by protocol.
*/
private final ByProtocolMap writeDoors = new ByProtocolMap();
/**
* Topic used to request out of order updates.
*/
private CellAddressCore topic;
/**
* Cell endpoint used for communication.
*/
private CellEndpoint cellEndpoint;
/**
* True after receiving a no route to cell error when requesting an update. When true the bean
* will repeat the request when suitable routes are added.
*/
private boolean isInitializing;
/**
* If non-empty, doors are filtered by these tags.
*/
private List<String> tags = Collections.emptyList();
/**
* Updates are requested from this topic.
*/
public void setTopic(String topic)
{
this.topic = new CellAddressCore(topic);
}
/**
* Doors are filtered by these tags.
*/
public void setTags(String... tags)
{
this.tags = asList(tags);
}
@Override
public void setCellEndpoint(CellEndpoint endpoint)
{
this.cellEndpoint = endpoint;
}
@Override
public void routeAdded(CellEvent ce)
{
CellRoute route = (CellRoute) ce.getSource();
if (route.getRouteType() == CellRoute.TOPIC || route.getRouteType() == CellRoute.DEFAULT) {
synchronized (this) {
if (isInitializing) {
requestUpdate();
}
}
}
}
@Override
public void afterStart()
{
if (topic != null) {
requestUpdate();
}
}
public void messageArrived(LoginBrokerInfo info)
{
expire();
add(new Entry(info));
}
public void messageArrived(NoRouteToCellException e)
{
if (e.getDestinationPath().getDestinationAddress().equals(topic)) {
synchronized (this) {
isInitializing = true;
}
}
}
private synchronized void requestUpdate()
{
isInitializing = false;
cellEndpoint.sendMessage(new CellMessage(topic, new LoginBrokerInfoRequest()));
}
private void add(Entry entry)
{
if (tags.isEmpty() || !Collections.disjoint(tags, entry.getLoginBrokerInfo().getTags())) {
Entry old = doorsByIdentity.put(entry.info.getIdentifier(), entry);
queue.add(entry);
addByProtocol(entry.info);
if (old != null) {
removeByProtocol(old.info);
}
}
}
private void remove(Entry entry)
{
LoginBrokerInfo info = entry.getLoginBrokerInfo();
if (doorsByIdentity.remove(info.getIdentifier(), entry)) {
removeByProtocol(info);
}
}
private void addByProtocol(LoginBrokerInfo info)
{
info.ifCapableOf(READ, readDoors::add);
info.ifCapableOf(WRITE, writeDoors::add);
}
private void removeByProtocol(LoginBrokerInfo info)
{
info.ifCapableOf(READ, readDoors::remove);
info.ifCapableOf(WRITE, writeDoors::remove);
}
@Override
public Collection<LoginBrokerInfo> doors()
{
expire();
return unmodifiableView;
}
@Override
public Map<String, Collection<LoginBrokerInfo>> readDoorsByProtocol()
{
expire();
return readDoors.getUnmodifiable();
}
@Override
public Map<String, Collection<LoginBrokerInfo>> writeDoorsByProtocol()
{
expire();
return writeDoors.getUnmodifiable();
}
@Override
public boolean anyMatch(Predicate<? super LoginBrokerInfo> predicate)
{
expire();
return doorsByIdentity.values().stream().map(Entry::getLoginBrokerInfo).anyMatch(predicate);
}
private void expire()
{
Entry entry;
while ((entry = queue.poll()) != null) {
remove(entry);
}
}
@Command(name = "lb ls", hint = "list collected login broker information")
class ListCommand implements Callable<String>
{
@Option(name = "protocol", usage = "Filter by protocol.")
String[] protocols;
@Option(name = "l", usage = "Show time.")
boolean showTime;
@Override
public String call() throws Exception
{
Set<String> protocolSet = (protocols != null) ? new HashSet<>(asList(protocols)) : null;
StringBuilder sb = new StringBuilder();
for (Entry entry : doorsByIdentity.values()) {
LoginBrokerInfo info = entry.getLoginBrokerInfo();
if (protocolSet == null || protocolSet.contains(info.getProtocolFamily())) {
sb.append(info);
if (showTime) {
sb.append(entry.getDelay(MILLISECONDS)).append(" ms;");
sb.append(entry.isValid() ? "VALID" : "INVALID").append(';');
}
sb.append('\n');
}
}
return sb.toString();
}
}
@Command(name = "lb update", hint = "refresh login broker information",
description = "Semi-blocking command to trigger an update of login brokering " +
"information for connected doors. The command blocks until the " +
"first reply is received or no doors could be found. Remaining " +
"updates are received in the background.")
class UpdateCommand extends DelayedReply implements Callable<DelayedReply>
{
@Override
public DelayedReply call() throws Exception
{
return this;
}
@Override
public void deliver(CellEndpoint endpoint, CellMessage envelope)
{
super.deliver(endpoint, envelope);
cellEndpoint.sendMessage(new CellMessage(topic, new LoginBrokerInfoRequest()),
new CellMessageAnswerable()
{
@Override
public void answerArrived(CellMessage request, CellMessage answer)
{
if (!(answer.getMessageObject() instanceof LoginBrokerInfo)) {
reply("Invalid reply received: " + answer.getMessageObject());
} else {
LoginBrokerInfo info = (LoginBrokerInfo) answer.getMessageObject();
add(new Entry(info));
reply("Update from " + info.getIdentifier() + " received. Remaining updates are processed in the background.");
}
}
@Override
public void exceptionArrived(CellMessage request,
Exception exception)
{
reply("Update failed: " + exception.getMessage());
}
@Override
public void answerTimedOut(CellMessage request)
{
reply("Update timed out.");
}
}, MoreExecutors.directExecutor(), envelope.getAdjustedTtl());
}
}
/**
* Grouping of doors by protocol. The number of protocols is low enough that we
* do not bother removing any entries from the map.
*/
private static class ByProtocolMap
{
private final ConcurrentMap<String, Set<LoginBrokerInfo>> doors =
new ConcurrentHashMap<>();
private final Map<String, Collection<LoginBrokerInfo>> unmodifiableView =
unmodifiableMap(Maps.transformValues(
Maps.filterValues(doors, (Set<LoginBrokerInfo> set) -> !set.isEmpty()),
Collections::unmodifiableCollection));
public void add(LoginBrokerInfo info)
{
get(info.getProtocolFamily()).add(info);
}
public boolean remove(LoginBrokerInfo info)
{
return get(info.getProtocolFamily()).remove(info);
}
public Set<LoginBrokerInfo> get(String protocol)
{
return doors.computeIfAbsent(protocol, key -> Collections.newSetFromMap(new ConcurrentHashMap<>()));
}
public Map<String, Collection<LoginBrokerInfo>> getUnmodifiable()
{
return unmodifiableView;
}
}
private static class Entry implements Delayed
{
private final long expirationTime;
private final LoginBrokerInfo info;
public Entry(LoginBrokerInfo info)
{
this.expirationTime = System.currentTimeMillis() + (long) (EXPIRATION_FACTOR * info.getUpdateTime());
this.info = info;
}
public LoginBrokerInfo getLoginBrokerInfo()
{
return info;
}
public boolean isValid()
{
return expirationTime > System.currentTimeMillis();
}
@Override
public long getDelay(TimeUnit unit)
{
return unit.convert(expirationTime - System.currentTimeMillis(), MILLISECONDS);
}
@Override
public int compareTo(Delayed o)
{
return Long.compare(expirationTime, ((Entry) o).expirationTime);
}
}
}