blob: 1ed8a8d63ee90a2d6df7099dc222204bfa5183dd [file] [log] [blame]
/*
* Copyright 2015 Open Networking Foundation
*
* 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.onosproject.ecord.co;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalCause;
import com.google.common.cache.RemovalNotification;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Modified;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.onlab.packet.Ethernet;
import org.onlab.packet.ONOSLLDP;
import org.onlab.util.Tools;
import org.onosproject.cfg.ComponentConfigService;
import org.onosproject.cluster.ClusterMetadataService;
import org.onosproject.cluster.ClusterService;
import org.onosproject.incubator.rpc.RemoteServiceContext;
import org.onosproject.incubator.rpc.RemoteServiceDirectory;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DeviceId;
import org.onosproject.net.Link;
import org.onosproject.net.LinkKey;
import org.onosproject.net.MastershipRole;
import org.onosproject.net.PortNumber;
import org.onosproject.net.config.ConfigFactory;
import org.onosproject.net.config.NetworkConfigEvent;
import org.onosproject.net.config.NetworkConfigListener;
import org.onosproject.net.config.NetworkConfigRegistry;
import org.onosproject.net.config.basics.BasicLinkConfig;
import org.onosproject.net.device.DefaultDeviceDescription;
import org.onosproject.net.device.DeviceDescription;
import org.onosproject.net.device.DeviceProvider;
import org.onosproject.net.device.DeviceProviderRegistry;
import org.onosproject.net.device.DeviceProviderService;
import org.onosproject.net.device.PortDescription;
import org.onosproject.net.link.DefaultLinkDescription;
import org.onosproject.net.link.LinkDescription;
import org.onosproject.net.link.ProbedLinkProvider;
import org.onosproject.net.link.LinkProviderRegistry;
import org.onosproject.net.link.LinkProviderService;
import org.onosproject.net.packet.DefaultOutboundPacket;
import org.onosproject.net.packet.PacketContext;
import org.onosproject.net.packet.PacketProcessor;
import org.onosproject.net.packet.PacketService;
import org.onosproject.net.provider.ProviderId;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.Dictionary;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
import static org.onlab.util.Tools.groupedThreads;
import static org.onosproject.ecord.co.BigSwitchManager.REALIZED_BY;
import static org.onosproject.net.config.basics.SubjectFactories.CONNECT_POINT_SUBJECT_FACTORY;
import static org.onosproject.net.flow.DefaultTrafficTreatment.builder;
import static org.slf4j.LoggerFactory.getLogger;
/* To configure the BigSwitchDevice parameters at startup, add configuration to tools/package/config/component-cfg.json
before using onos-package command.
Configuration example:
{
"org.onosproject.ecord.co.BigSwitchDeviceProvider": {
"providerScheme": "bigswitch",
"providerId": "org.onosproject.bigswitch",
"remoteUri": "grpc://192.168.64.100:11984",
"metroIp": "192.168.64.100"
}
}
Note that you need the port number (11984) and the metroIp and the remoteUri
values should point to the same IP/host.
*/
/**
* Device provider which exposes a big switch abstraction of the underlying data path.
*/
@Component(immediate = true)
public class BigSwitchDeviceProvider implements DeviceProvider, ProbedLinkProvider {
private static final Logger log = getLogger(BigSwitchDeviceProvider.class);
/**
* Big switch LLDP probe interval in seconds.
*/
private static final int PROBE_INTERVAL = 3;
private static final long HEALTHCHECK_INTERVAL = 5;
private static final String PROP_SCHEME = "providerScheme";
private static final String DEFAULT_SCHEME = "bigswitch";
private static final String PROP_ID = "providerId";
private static final String DEFAULT_ID = "org.onosproject.bigswitch";
private static final String PROP_REMOTE_URI = "remoteUri";
private static final String DEFAULT_REMOTE_URI = "local://localhost";
private static final String PROP_METRO_IP = "metroIp";
private static final String DEFAULT_METRO_IP = "localhost";
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected BigSwitchService bigSwitchService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected ClusterService clusterService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected RemoteServiceDirectory rpcService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected NetworkConfigRegistry cfgRegistry;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected ComponentConfigService cfgService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected PacketService packetService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected ClusterMetadataService metadataService;
private RemoteServiceContext remoteServiceContext;
private BigSwitch bigSwitch;
private DeviceDescription bigSwitchDescription;
private DeviceProviderRegistry deviceProviderRegistry;
private DeviceProviderService deviceProviderService;
private LinkProviderRegistry linkProviderRegistry;
private LinkProviderService linkProviderService;
private ScheduledExecutorService executor;
private ScheduledFuture<?> discovery;
/**
* Device session heart beat task handle.
*/
private ScheduledFuture<?> heartBeat;
private BigSwitchListener listener = new InternalListener();
private PacketProcessor packetProcessor = new InternalPacketProcessor();
private final Ethernet ethPacket = new Ethernet();
private final ConfigFactory<ConnectPoint, CrossConnectConfig> xcConfigFactory
= new ConfigFactory<ConnectPoint, CrossConnectConfig>(CONNECT_POINT_SUBJECT_FACTORY,
CrossConnectConfig.class,
"cross-connect") {
@Override
public CrossConnectConfig createConfig() {
return new CrossConnectConfig();
}
};
@Property(name = PROP_SCHEME, value = DEFAULT_SCHEME,
label = "Provider scheme used to register a big switch device")
private String schemeProp = DEFAULT_SCHEME;
@Property(name = PROP_ID, value = DEFAULT_ID,
label = "Provider ID used to register a big switch device")
private String idProp = DEFAULT_ID;
@Property(name = PROP_REMOTE_URI, value = DEFAULT_REMOTE_URI,
label = "URI of remote host to connect via RPC service")
private String remoteUri = DEFAULT_REMOTE_URI;
@Property(name = PROP_METRO_IP, value = DEFAULT_METRO_IP,
label = "IP address or hostname of metro ONOS instance to make REST calls")
private String metroIp = DEFAULT_METRO_IP;
private ProviderId providerId;
private Cache<LinkKey, LinkDescription> knownLinks = CacheBuilder.newBuilder()
// treat link as vanished after no update for 3 cycles
.expireAfterWrite(3 * PROBE_INTERVAL, TimeUnit.SECONDS)
.removalListener(this::onEviction)
.build();
@Activate
public void activate(ComponentContext context) {
cfgService.registerProperties(getClass());
loadRpcConfig(context);
loadRestConfig(context);
// setup service to, and register with, providers
try {
remoteServiceContext = rpcService.get(URI.create(remoteUri));
} catch (UnsupportedOperationException e) {
log.warn("Unsupported URI: {}", remoteUri);
}
providerId = new ProviderId(schemeProp, idProp);
executor = newSingleThreadScheduledExecutor(groupedThreads("onos/bigswitch", "discovery-%d"));
registerToDeviceProvider();
prepareProbe();
registerToLinkServices();
// start listening to config changes
NetworkConfigListener cfglistener = new InternalConfigListener();
cfgRegistry.addListener(cfglistener);
cfgRegistry.registerConfigFactory(xcConfigFactory);
log.info("Started");
}
@Deactivate
public void deactivate() {
packetService.removeProcessor(packetProcessor);
// advertise all Links as vanished
knownLinks.invalidateAll();
cfgRegistry.unregisterConfigFactory(xcConfigFactory);
cfgService.unregisterProperties(getClass(), false);
unregisterFromLinkServices();
executor.shutdownNow();
unregisterFromDeviceProvider();
// Won't hurt but necessary?
deviceProviderService = null;
providerId = null;
log.info("Stopped");
}
@Modified
public void modified(ComponentContext context) {
log.info("Reloading config...");
// Needs re-registration to DeviceProvider
if (loadRpcConfig(context)) {
// unregister from Device and Link Providers with old parameters
unregisterFromLinkServices();
unregisterFromDeviceProvider();
// register to Device and Link Providers with new parameters
try {
remoteServiceContext = rpcService.get(URI.create(remoteUri));
providerId = new ProviderId(schemeProp, idProp);
registerToDeviceProvider();
registerToLinkServices();
} catch (UnsupportedOperationException e) {
log.warn("Unsupported URI: {}", remoteUri);
}
log.info("Re-registered with Device and Link Providers");
}
// Needs to advertise cross-connect links
if (loadRestConfig(context)) {
advertiseCrossConnectLinksOnAllPorts();
}
}
// Loads RPC-related configuration values from ComponentContext and returns true if there are any changes.
private boolean loadRpcConfig(ComponentContext context) {
Dictionary<?, ?> properties = context.getProperties();
boolean changed = false;
// TODO When configuration value is set to null or empty, actual value will differ from configuration.
// Any means to sync those values?
String s = Tools.get(properties, PROP_SCHEME);
String newValue = Strings.isNullOrEmpty(s) ? DEFAULT_SCHEME : s;
if (!schemeProp.equals(newValue)) {
schemeProp = newValue;
changed = true;
}
s = Tools.get(properties, PROP_ID);
newValue = Strings.isNullOrEmpty(s) ? DEFAULT_ID : s;
if (!idProp.equals(newValue)) {
idProp = newValue;
changed = true;
}
s = Tools.get(properties, PROP_REMOTE_URI);
newValue = Strings.isNullOrEmpty(s) ? DEFAULT_REMOTE_URI : s;
if (!remoteUri.equals(newValue)) {
remoteUri = newValue;
changed = true;
}
// FIXME Ignoring config diff check
// to provide a way to restart gRPC *Provider
return true;
}
// Loads REST-related configuration values from ComponentContext and returns true if there are any changes.
private boolean loadRestConfig(ComponentContext context) {
Dictionary<?, ?> properties = context.getProperties();
boolean changed = false;
String s = Tools.get(properties, PROP_METRO_IP);
String newValue = Strings.isNullOrEmpty(s) ? DEFAULT_METRO_IP : s;
if (!metroIp.equals(newValue)) {
metroIp = newValue;
changed = true;
}
return changed;
}
private void registerToDeviceProvider() {
// Create big switch device and description
DeviceId deviceId = DeviceId.deviceId(schemeProp + ':' + clusterService.getLocalNode().ip());
log.info("Registering {}", deviceId);
bigSwitch = new BigSwitch(deviceId, this.id());
bigSwitchDescription = new DefaultDeviceDescription(bigSwitch.id().uri(),
bigSwitch.type(), bigSwitch.manufacturer(),
bigSwitch.hwVersion(), bigSwitch.swVersion(), bigSwitch.serialNumber(), bigSwitch.chassisId());
deviceProviderRegistry = remoteServiceContext.get(DeviceProviderRegistry.class);
deviceProviderService = deviceProviderRegistry.register(this);
// Start big switch service and register device
advertiseDevice(bigSwitch.id());
advertisePorts(bigSwitch.id(), bigSwitchService.getPorts());
advertiseCrossConnectLinksOnAllPorts();
bigSwitchService.addListener(listener);
heartBeat = executor.scheduleAtFixedRate(new HeartBeatTask(),
HEALTHCHECK_INTERVAL,
HEALTHCHECK_INTERVAL,
TimeUnit.SECONDS);
}
private void registerToLinkServices() {
// Start link discovery -related functions
linkProviderRegistry = remoteServiceContext.get(LinkProviderRegistry.class);
linkProviderService = linkProviderRegistry.register(this);
discovery = executor.scheduleAtFixedRate(new DiscoveryTask(),
PROBE_INTERVAL, PROBE_INTERVAL, TimeUnit.SECONDS);
// maybe also want a way to say 'get me next usable priority of class X'
packetService.addProcessor(packetProcessor, PacketProcessor.advisor(2));
}
private void unregisterFromDeviceProvider() {
heartBeat.cancel(true);
if (bigSwitch == null) {
log.warn("Invalid unregistration.");
return;
}
bigSwitchService.removeListener(listener);
try {
deviceProviderService.deviceDisconnected(bigSwitch.id());
} catch (IllegalStateException e) {
log.warn("Exception on deviceDisconnected", e);
}
deviceProviderRegistry.unregister(this);
deviceProviderService = null;
bigSwitch = null;
}
private void unregisterFromLinkServices() {
discovery.cancel(true);
packetService.removeProcessor(packetProcessor);
linkProviderRegistry.unregister(this);
}
private ConnectPoint toConnectPoint(String strCp) {
String[] split = strCp.split("/");
if (split.length != 2) {
log.warn("Unexpected annotation %s:%s", REALIZED_BY, strCp);
return null;
}
DeviceId did = DeviceId.deviceId(split[0]);
PortNumber num = PortNumber.fromString(split[1]);
return new ConnectPoint(did, num);
}
private Optional<Pair<ConnectPoint, ConnectPoint>> crossConnectLink(PortDescription bigPort) {
String sPhyCp = bigPort.annotations().value(REALIZED_BY);
if (sPhyCp == null) {
return Optional.empty();
}
ConnectPoint phyCp = toConnectPoint(sPhyCp);
if (phyCp != null) {
CrossConnectConfig config = cfgRegistry.getConfig(phyCp, CrossConnectConfig.class);
if (config != null) {
return config.remote()
.map(remCp -> Pair.of(new ConnectPoint(bigSwitch.id(), bigPort.portNumber()), remCp));
}
}
return Optional.empty();
}
private static final ObjectMapper MAPPER = new ObjectMapper();
private ObjectNode crossConnectLinksJson(Pair<ConnectPoint, ConnectPoint> link) {
ObjectNode linksCfg = MAPPER.createObjectNode();
ObjectNode basic = MAPPER.createObjectNode();
basic.putObject("basic")
.put(BasicLinkConfig.IS_DURABLE, true)
.put(BasicLinkConfig.TYPE, "OPTICAL");
linksCfg.set(String.format("%s/%s-%s/%s",
link.getLeft().deviceId(), link.getLeft().port(),
link.getRight().deviceId(), link.getRight().port()),
basic);
linksCfg.set(String.format("%s/%s-%s/%s",
link.getRight().deviceId(), link.getRight().port(),
link.getLeft().deviceId(), link.getLeft().port()),
basic);
return linksCfg;
}
private boolean postNetworkConfig(String subject, ObjectNode cfg) {
// TODO Slice out REST Client code as library?
Client client = ClientBuilder.newClient();
client.property(ClientProperties.FOLLOW_REDIRECTS, true);
// Trying to do JSON processing using Jackson triggered OSGi nightmare
//client.register(JacksonFeature.class);
final Map<String, String> env = System.getenv();
// TODO Where should we get the user/password from?
String user = env.getOrDefault("ONOS_WEB_USER", "onos");
String pass = env.getOrDefault("ONOS_WEB_PASS", "rocks");
HttpAuthenticationFeature auth = HttpAuthenticationFeature.basic(user, pass);
client.register(auth);
// TODO configurable base path
WebTarget target = client.target("http://" + metroIp + ":8181/onos/v1/")
.path("network/configuration/")
.path(subject);
Response response = target.request(MediaType.APPLICATION_JSON)
.post(Entity.entity(cfg.toString(), MediaType.APPLICATION_JSON));
if (response.getStatus() != Response.Status.OK.getStatusCode()) {
log.error("POST failed {}\n{}", response, cfg.toString());
return false;
}
return true;
}
private void advertiseCrossConnectLinks(PortDescription port) {
crossConnectLink(port).ifPresent(xcLink -> {
log.debug("CrossConnect {} is {}!",
xcLink,
port.isEnabled() ? "up" : "down");
// TODO check port status and add/remove cross connect Link
postNetworkConfig("links", crossConnectLinksJson(xcLink));
});
}
private void advertiseCrossConnectLinksOnAllPorts() {
bigSwitchService.getPorts()
.forEach(BigSwitchDeviceProvider.this::advertiseCrossConnectLinks);
}
private void prepareProbe() {
ethPacket.setEtherType(Ethernet.TYPE_LLDP)
.setDestinationMACAddress(ONOSLLDP.LLDP_ONLAB)
.setPad(true);
}
private String buildMac() {
return ProbedLinkProvider.fingerprintMac(metadataService.getClusterMetadata());
}
void safeUnregister() {
try {
deviceProviderRegistry.unregister(this);
} catch (IllegalStateException e) {
// silently-ignore error
}
}
void advertiseDevice(DeviceId did) {
try {
deviceProviderService.deviceConnected(did, bigSwitchDescription);
} catch (IllegalStateException e) {
log.warn("Exception thrown", e);
safeUnregister();
deviceProviderService = deviceProviderRegistry.register(this);
deviceProviderService.deviceConnected(did, bigSwitchDescription);
// retry
deviceProviderService.deviceConnected(did, bigSwitchDescription);
}
}
void advertisePort(DeviceId did, PortDescription port) {
try {
deviceProviderService.portStatusChanged(did, port);
} catch (IllegalStateException e) {
log.warn("Exception thrown", e);
safeUnregister();
deviceProviderService = deviceProviderRegistry.register(this);
deviceProviderService.deviceConnected(did, bigSwitchDescription);
// retry
deviceProviderService.portStatusChanged(did, port);
}
}
void advertisePorts(DeviceId did, List<PortDescription> ports) {
try {
deviceProviderService.updatePorts(did, ports);
} catch (IllegalStateException e) {
log.warn("Exception thrown", e);
safeUnregister();
deviceProviderService = deviceProviderRegistry.register(this);
deviceProviderService.deviceConnected(did, bigSwitchDescription);
// retry
deviceProviderService.updatePorts(did, ports);
}
}
private class InternalListener implements BigSwitchListener {
@Override
public void event(BigSwitchEvent event) {
switch (event.type()) {
case PORT_ADDED:
case PORT_REMOVED:
advertisePorts(bigSwitch.id(), bigSwitchService.getPorts());
// if the subject's underlying port was a cross connect port,
// advertise cross-connect link to Metro-ONOS view
advertiseCrossConnectLinks(event.subject());
break;
case PORT_UPDATED:
advertisePort(bigSwitch.id(), event.subject());
// if the subject's underlying port was a cross connect port,
// advertise cross-connect link to Metro-ONOS view
advertiseCrossConnectLinks(event.subject());
break;
default:
break;
}
}
}
/**
* A big switch device provider implementation.
*/
public BigSwitchDeviceProvider() {
}
@Override
public ProviderId id() {
return providerId;
}
@Override
public void triggerProbe(DeviceId deviceId) {
}
@Override
public void roleChanged(DeviceId deviceId, MastershipRole newRole) {
}
@Override
public boolean isReachable(DeviceId deviceId) {
return true;
}
@Override
public void changePortState(DeviceId deviceId, PortNumber portNumber, boolean b) {
}
public class InternalConfigListener implements NetworkConfigListener {
@Override
public void event(NetworkConfigEvent event) {
if (event.configClass() == CrossConnectConfig.class) {
advertiseCrossConnectLinksOnAllPorts();
}
}
}
/*
* Emits link probes tagged with the big switch's scheme from available
* virtual ports. This is run every three seconds.
*/
private class DiscoveryTask implements Runnable {
@Override
public void run() {
bigSwitchService.getPorts().forEach(p -> {
// ID of big switch contains schema, so we're good
ONOSLLDP lldp = ONOSLLDP.onosLLDP(bigSwitch.id().toString(),
bigSwitch.chassisId(),
(int) p.portNumber().toLong());
ethPacket.setSourceMACAddress(buildMac()).setPayload(lldp);
// recover physical connect point
ConnectPoint real = ConnectPoint.deviceConnectPoint(
p.annotations().value(BigSwitchManager.REALIZED_BY));
log.debug("sending probe for {}/{} through {}", bigSwitch.id(), p.portNumber(), real.toString());
packetService.emit(new DefaultOutboundPacket(real.deviceId(),
builder().setOutput(real.port()).build(),
ByteBuffer.wrap(ethPacket.serialize())));
});
// Periodically advertise known links
// to allow eviction on (remote) LinkService.
knownLinks.asMap().values().forEach(d -> linkDetected(d));
}
}
/**
* Periodic heart beat of Device RPC session.
*/
public class HeartBeatTask implements Runnable {
@Override
public void run() {
try {
// Minimum side-effect RPC call as a heart beat message
advertisePorts(bigSwitch.id(), bigSwitchService.getPorts());
} catch (IllegalStateException e) {
log.warn("Exception caught sending heart beat", e);
}
}
}
private class InternalPacketProcessor implements PacketProcessor {
@Override
public void process(PacketContext context) {
Ethernet eth = context.inPacket().parsed();
if (eth == null) {
return;
}
ONOSLLDP probe = ONOSLLDP.parseONOSLLDP(eth);
if (probe == null) {
return;
}
if (isValidProbe(eth.getSourceMAC().toString(), probe)) {
/*
* build and pass a linkDesription to the metro domain, which
* will map out a virtual link between the two big switches.
*/
PortNumber srcPort = PortNumber.portNumber(probe.getPort());
DeviceId srcDev = DeviceId.deviceId(probe.getDeviceString());
ConnectPoint src = new ConnectPoint(srcDev, srcPort);
// receiver-side: some assembly required.
PortNumber dstPort = bigSwitchService.getPort(context.inPacket().receivedFrom());
if (dstPort == null) {
return;
}
ConnectPoint dst = new ConnectPoint(bigSwitch.id(), dstPort);
log.debug("recvd link: {}->{}", src, dst);
linkDetected(src, dst, new DefaultLinkDescription(src, dst, Link.Type.VIRTUAL));
}
}
/*
* true for probes from other controller clusters, that are from big switches.
*/
private boolean isValidProbe(String mac, ONOSLLDP probe) {
// don't consider ourselves valid if we're using DEFAULT_MAC
String ourMac = buildMac();
if (mac.equalsIgnoreCase(ourMac) || ProbedLinkProvider.defaultMac().equalsIgnoreCase(ourMac)) {
return false;
}
// TODO Come up with more robust way to identify big switch probe?
return probe.getDeviceString().contains("bigswitch");
}
}
/**
* Registers the link to known Link list and advertise it.
*
* @param src {@link ConnectPoint} of the link
* @param dst {@link ConnectPoint} of the link
* @param link {@link LinkDescription} of the link
*/
private void linkDetected(ConnectPoint src, ConnectPoint dst,
LinkDescription link) {
checkNotNull(link);
knownLinks.put(LinkKey.linkKey(src, dst), link);
linkProviderService.linkDetected(link);
}
/**
* Advertises the link.
*
* @param link {@link LinkDescription} of the link
*/
private void linkDetected(LinkDescription link) {
checkNotNull(link);
linkProviderService.linkDetected(link);
}
/**
* Advertises that the link vanished.
*
* @param link {@link LinkDescription} of the link
*/
private void linkVanished(LinkDescription link) {
checkNotNull(link);
linkProviderService.linkVanished(link);
}
/**
* Advertises that the link has vanished, when
* link was evicted from the Cache.
*
* @param notification cache eviction notification.
*/
private void onEviction(RemovalNotification<LinkKey, LinkDescription> notification) {
if (notification.getCause() != RemovalCause.REPLACED) {
linkVanished(notification.getValue());
}
}
}