/** * Copyright 2012, Big Switch Networks, Inc. * Originally created by David Erickson, Stanford University * * 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 net.floodlightcontroller.core.internal; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.annotation.Nonnull; import io.netty.channel.Channel; import io.netty.util.Timeout; import io.netty.util.Timer; import io.netty.util.TimerTask; import java.util.Date; import net.floodlightcontroller.core.Deliverable; import net.floodlightcontroller.core.DeliverableListenableFuture; import net.floodlightcontroller.core.IOFConnection; import net.floodlightcontroller.core.IOFConnectionBackend; import net.floodlightcontroller.core.SwitchDisconnectedException; import net.floodlightcontroller.debugcounter.IDebugCounterService; import net.floodlightcontroller.util.IterableUtils; import net.floodlightcontroller.util.OFMessageUtils; import org.projectfloodlight.openflow.protocol.OFErrorMsg; import org.projectfloodlight.openflow.protocol.OFFactory; import org.projectfloodlight.openflow.protocol.OFMessage; import org.projectfloodlight.openflow.protocol.OFRequest; import org.projectfloodlight.openflow.protocol.OFStatsReply; import org.projectfloodlight.openflow.protocol.OFStatsReplyFlags; import org.projectfloodlight.openflow.protocol.OFStatsRequest; import org.projectfloodlight.openflow.types.DatapathId; import org.projectfloodlight.openflow.types.OFAuxId; import org.projectfloodlight.openflow.types.U64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; /** * Implementation of an openflow connection to switch. Encapsulates a * {@link Channel}, and provides message write and request/response handling * capabilities. * * @author Andreas Wundsam <andreas.wundsam@bigswitch.com> */ public class OFConnection implements IOFConnection, IOFConnectionBackend{ private static final Logger logger = LoggerFactory.getLogger(OFConnection.class); private final DatapathId dpid; private final OFFactory factory; /** CAREFUL CAREFUL CAREFUL: * * Netty4 does not guarantee order of messages that are written into the channel any more. * To ensure messages do not get reordered, never directly call {@link Channel#write(Object)}. * * Instead, use {@link #write(Iterable)}, which queue up write request on the EventLoop, * to make sure they are handled in order. */ private final Channel channel; private final OFAuxId auxId; private final Timer timer; private final Date connectedSince; private final Map<Long, Deliverable<?>> xidDeliverableMap; private static final long DELIVERABLE_TIME_OUT = 60; private static final TimeUnit DELIVERABLE_TIME_OUT_UNIT = TimeUnit.SECONDS; private final OFConnectionCounters counters; private IOFConnectionListener listener; private volatile U64 latency; /** * Used to write messages to ensure order w/Netty4. * It also ensures we do not reuse the array, since * Netty4 will write the object, not the items. */ private class WriteMessageTask implements Runnable { private final Iterable<OFMessage> msglist; public WriteMessageTask(Iterable<OFMessage> msglist) { this.msglist = msglist; } @Override public void run() { for (OFMessage m : msglist) { if (logger.isTraceEnabled()) logger.trace("{}: send {}", this, m); counters.updateWriteStats(m); } channel.writeAndFlush(msglist); } } public OFConnection(@Nonnull DatapathId dpid, @Nonnull OFFactory factory, @Nonnull Channel channel, @Nonnull OFAuxId auxId, @Nonnull IDebugCounterService debugCounters, @Nonnull Timer timer) { Preconditions.checkNotNull(dpid, "dpid"); Preconditions.checkNotNull(factory, "factory"); Preconditions.checkNotNull(channel, "channel"); Preconditions.checkNotNull(timer, "timer"); Preconditions.checkNotNull(debugCounters); this.listener = NullConnectionListener.INSTANCE; this.dpid = dpid; this.factory = factory; this.channel = channel; this.auxId = auxId; this.connectedSince = new Date(); this.xidDeliverableMap = new ConcurrentHashMap<>(); this.counters = new OFConnectionCounters(debugCounters, dpid, this.auxId); this.timer = timer; this.latency = U64.ZERO; } /** * All write methods chain into this write() to use WriteMessageTask. * * Write the list of messages to the switch * * @param msgList list of messages to write * @return list of failed messages; can only fail if channel disconnected */ @Override public Collection<OFMessage> write(final Iterable<OFMessage> msgList) { if (!isConnected()) { if (logger.isDebugEnabled()) logger.debug(this.toString() + " : not connected - dropping {} element msglist {} ", Iterables.size(msgList), String.valueOf(msgList).substring(0, 80)); return IterableUtils.toCollection(msgList); } for (OFMessage m : msgList) { if (logger.isTraceEnabled()) { logger.trace("{}: send {}", this, m); counters.updateWriteStats(m); } } this.channel.eventLoop().execute(new WriteMessageTask(msgList)); return Collections.emptyList(); } /** * Write the single message to the channel * @param m * @return true upon success; false upon failure; can only fail if channel disconnected */ @Override public boolean write(OFMessage m) { return this.write(Collections.singletonList(m)).isEmpty(); } @Override public <R extends OFMessage> ListenableFuture<R> writeRequest(OFRequest<R> request) { if (!isConnected()) { return Futures.immediateFailedFuture(new SwitchDisconnectedException(getDatapathId())); } DeliverableListenableFuture<R> future = new DeliverableListenableFuture<R>(request); xidDeliverableMap.put(request.getXid(), future); listener.messageWritten(this, request); this.write(request); return future; } @Override public <REPLY extends OFStatsReply> ListenableFuture<List<REPLY>> writeStatsRequest( OFStatsRequest<REPLY> request) { if (!isConnected()) { return Futures.immediateFailedFuture(new SwitchDisconnectedException(getDatapathId())); } final DeliverableListenableFuture<List<REPLY>> future = new DeliverableListenableFuture<List<REPLY>>(request); Deliverable<REPLY> deliverable = new Deliverable<REPLY>() { private final List<REPLY> results = Collections .synchronizedList(new ArrayList<REPLY>()); @Override public void deliver(REPLY reply) { results.add(reply); if (!reply.getFlags().contains(OFStatsReplyFlags.REPLY_MORE)) { // done future.deliver(results); } } @Override public void deliverError(Throwable cause) { future.deliverError(cause); } @Override public boolean isDone() { return future.isDone(); } @Override public boolean cancel(boolean mayInterruptIfRunning) { return future.cancel(mayInterruptIfRunning); } @Override public OFMessage getRequest() { return future.getRequest(); } }; registerDeliverable(request.getXid(), deliverable); this.write(request); return future; } public void disconnected() { SwitchDisconnectedException exception = new SwitchDisconnectedException(getDatapathId()); for (Long xid : xidDeliverableMap.keySet()) { // protect against other mechanisms running at the same time // (timeout) Deliverable<?> removed = xidDeliverableMap.remove(xid); if (removed != null) { removed.deliverError(exception); } } } @Override public void disconnect() { this.channel.disconnect(); this.counters.uninstallCounters(); } @Override public String toString() { String channelString = (channel != null) ? String.valueOf(channel.remoteAddress()): "?"; return "OFConnection [" + getDatapathId() + "(" + getAuxId() + ")" + "@" + channelString + "]"; } @Override public Date getConnectedSince() { return connectedSince; } private void registerDeliverable(long xid, Deliverable<?> deliverable) { this.xidDeliverableMap.put(xid, deliverable); setDeliverableTimeout(xid); } private void setDeliverableTimeout(long xid) { timer.newTimeout(new TimeOutDeliverable(xid), DELIVERABLE_TIME_OUT, DELIVERABLE_TIME_OUT_UNIT); } public boolean handleGenericDeliverable(OFMessage reply) { counters.updateReadStats(reply); @SuppressWarnings("unchecked") Deliverable<OFMessage> deliverable = (Deliverable<OFMessage>) this.xidDeliverableMap.get(reply.getXid()); if (deliverable != null) { boolean validReply = true; if(reply instanceof OFErrorMsg) { deliverable.deliverError(new OFErrorMsgException((OFErrorMsg) reply)); } else { if (OFMessageUtils.isReplyForRequest(deliverable.getRequest(), reply)) { deliverable.deliver(reply); } else { validReply = false; setDeliverableTimeout(reply.getXid()); } } if (validReply && deliverable.isDone()) this.xidDeliverableMap.remove(reply.getXid()); return true; } else { return false; } } @Override public void cancelAllPendingRequests() { /* * we don't need to be synchronized here. Even if another thread * modifies the map while we're cleaning up the future will eventually * timeout */ for (Deliverable<?> d : xidDeliverableMap.values()) { d.cancel(true); } xidDeliverableMap.clear(); } @Override public boolean isConnected() { return channel.isActive(); } @Override public SocketAddress getRemoteInetAddress() { return channel.remoteAddress(); } @Override public SocketAddress getLocalInetAddress() { return channel.localAddress(); } public boolean deliverResponse(OFMessage m) { if (handleGenericDeliverable(m)) return true; else return false; } @Override public boolean isWritable() { return channel.isWritable(); } @Override public DatapathId getDatapathId() { return dpid; } @Override public OFAuxId getAuxId() { return auxId; } Set<Long> getPendingRequestIds() { return ImmutableSet.copyOf(xidDeliverableMap.keySet()); } @Override public OFFactory getOFFactory() { return this.factory; } /** * Timeout class instantiated for deliverables. Will throw a timeout exception * if proper responses are not received in time. * */ private class TimeOutDeliverable implements TimerTask { private final long xid; public TimeOutDeliverable(long xid) { this.xid = xid; } @Override public void run(Timeout timeout) throws Exception { Deliverable<?> removed = xidDeliverableMap.remove(xid); if (removed != null && !removed.isDone()) { removed.deliverError(new TimeoutException( "timeout - did not receive answer for xid " + xid)); } } } public IOFConnectionListener getListener() { return listener; } /** set the connection listener * <p> * Note: this is assumed to be called from the Connection's IO Thread. * * @param listener */ @Override public void setListener(IOFConnectionListener listener) { this.listener = listener; } public void messageReceived(OFMessage m) { // Check if message was a response for a xid waiting at the switch if(!deliverResponse(m)){ listener.messageReceived(this, m); } } @Override public U64 getLatency() { return this.latency; } @Override public void updateLatency(U64 latency) { if (latency == null) { logger.error("Latency must be non-null. Ignoring null latency value."); return; } else if (this.latency.equals(U64.ZERO)) { logger.debug("Recording previously 0ms switch {} latency as {}ms", this.getDatapathId(), latency.getValue()); this.latency = latency; return; } else { double oldWeight = 0.30; this.latency = U64.of((long) (this.latency.getValue() * oldWeight + latency.getValue() * (1 - oldWeight))); logger.debug("Switch {} latency updated to {}ms", this.getDatapathId(), this.latency.getValue()); } } /** A dummy connection listener that just logs warn messages. Saves us a few null checks * @author Andreas Wundsam <andreas.wundsam@bigswitch.com> */ private static class NullConnectionListener implements IOFConnectionListener { public final static NullConnectionListener INSTANCE = new NullConnectionListener(); private NullConnectionListener() { } @Override public void connectionClosed(IOFConnectionBackend connection) { logger.warn("NullConnectionListener for {} - received connectionClosed", connection); } @Override public void messageReceived(IOFConnectionBackend connection, OFMessage m) { logger.warn("NullConnectionListener for {} - received messageReceived: {}", connection, m); } @Override public boolean isSwitchHandshakeComplete(IOFConnectionBackend connection) { return false; } @Override public void messageWritten(IOFConnectionBackend connection, OFMessage m) { // TODO Auto-generated method stub } } }