/*
* Part of the CCNx Java Library.
*
* Copyright (C) 2008-2012 Palo Alto Research Center, Inc.
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation.
* This library 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
* Lesser General Public License for more details. You should have received
* a copy of the GNU Lesser General Public License along with this library;
* if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
* Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.ccnx.ccn.impl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.TreeMap;
import java.util.logging.Level;
import org.ccnx.ccn.CCNHandle;
import org.ccnx.ccn.CCNInterestHandler;
import org.ccnx.ccn.config.ConfigurationException;
import org.ccnx.ccn.config.SystemConfiguration;
import org.ccnx.ccn.impl.InterestTable.Entry;
import org.ccnx.ccn.impl.support.Log;
import org.ccnx.ccn.protocol.ContentName;
import org.ccnx.ccn.protocol.ContentObject;
import org.ccnx.ccn.protocol.Interest;
import org.ccnx.ccn.protocol.MalformedContentNameStringException;
/**
* This class implements an input buffer for interests and an output buffer
* for content objects.
*
* ccnd will not accept data (content objects) except in response to an
* interest. This class allows data sources such as output streams to
* generate content objects speculatively, and buffers them until interests
* arrive from ccnd. Equally when interests come in from ccnd before the
* content has been generated this class will buffer the interests until
* the content is generated.
*
* Implements a capacity limit in the holding buffer. If the buffer consumption
* reaches the specified capacity, any subsequent put will block until there is more
* room in the buffer. Note that this means that the buffer may hold as many objects as the
* capacity value, and the object held by any later blocked put is extra, meaning the total
* number of objects waiting to be sent is not bounded by the capacity alone.
* Currently this is only per "flow controller". There
* is nothing to stop multiple streams writing to the repo for instance to
* independently all fill their buffers and cause a lot of memory to be used.
*
* Also implements a limited capacity for held interests.
*
* The buffer emptying policy in "afterPutAction" can be overridden by
* subclasses to implement a different way of draining the buffer.
*/
public class CCNFlowControl implements CCNInterestHandler {
public enum Shape {
STREAM("STREAM");
Shape(String str) { this._str = str; }
public String value() { return _str; }
private final String _str;
}
public enum SaveType {
RAW ("RAW"), REPOSITORY ("REPOSITORY"), LOCALREPOSITORY("LOCALREPOSITORY");
SaveType(String str) { this._str = str; }
public String value() { return _str; }
private final String _str;
}
protected CCNHandle _handle = null;
// Designed to allow a CCNOutputStream to flush its current output once without
// causing the over-capacity blocking to be triggered
protected static final int DEFAULT_CAPACITY = CCNSegmenter.HOLD_COUNT + 1;
protected static final int DEFAULT_INTEREST_CAPACITY = 40;
// Temporarily default to very high timeout so that puts have a good
// chance of going through. We actually may want to keep this.
protected int _timeout = SystemConfiguration.FC_TIMEOUT;
protected int _timeoutToUse = SystemConfiguration.FC_TIMEOUT;
protected int _capacity = DEFAULT_CAPACITY;
// Value used to determine whether the buffer is draining in waitForPutDrain
protected long _nOut = 0;
// Unmatched interests are purged from our table if they have remained there longer than this
//TODO need to normalize this with refresh time in CCNNetworkManager and put in SystemConfiguration
protected static final int PURGE = 4000;
protected static long _lastPurgeTime = 0;
protected TreeMap<ContentName, ContentObject> _holdingArea = new TreeMap<ContentName, ContentObject>();
protected InterestTable<UnmatchedInterest> _unmatchedInterests = new InterestTable<UnmatchedInterest>();
// The namespaces served by this flow controller
protected HashSet<ContentName> _filteredNames = new HashSet<ContentName>();
private static class UnmatchedInterest {
long timestamp = System.currentTimeMillis();
}
private boolean _flowControlEnabled = true;
/**
* @param name automatically handles this namespace
* @param handle CCNHandle - created if null
* @throws IOException if handle can't be created
*/
public CCNFlowControl(ContentName name, CCNHandle handle) throws IOException {
this(handle);
if (name != null) {
if( Log.isLoggable(Log.FAC_IO, Level.INFO))
Log.info(Log.FAC_IO, "adding namespace: {0}", name);
// don't call full addNameSpace, in order to allow subclasses to
// override. just do minimal part
_filteredNames.add(name);
_handle.registerFilter(name, this);
}
}
/**
* @param name automatically handles this namespace
* @param handle CCNHandle - created if null
* @throws MalformedContentNameStringException if namespace is malformed
* @throws IOException if handle can't be created
*/
public CCNFlowControl(String name, CCNHandle handle)
throws MalformedContentNameStringException, IOException {
this(ContentName.fromNative(name), handle);
}
/**
* @param handle CCNHandle - created if null
* @throws IOException if handle can't be created
*/
public CCNFlowControl(CCNHandle handle) throws IOException {
if (null == handle) {
try {
handle = CCNHandle.open();
} catch (ConfigurationException e) {
Log.info(Log.FAC_IO, "Got ConfigurationException attempting to create a handle. Rethrowing it as an IOException. Message: {0}", e.getMessage());
throw new IOException("ConfigurationException creating a handle: " + e.getMessage());
}
}
_handle = handle;
_unmatchedInterests.setCapacity(DEFAULT_INTEREST_CAPACITY);
if (_timeout != SystemConfiguration.NO_TIMEOUT)
_timeoutToUse = _timeout;
}
/**
* Filter handler constructor -- an Interest has already come in, and
* we are writing a stream in response. So we can write out the first
* matching block that we get as soon as we get it (in response to this
* preexisting interest). Caller has the responsibilty to ensure that
* this Interest is only handed to one CCNFlowControl to emit a block.
* @param name an initial namespace to handle
* @param outstandingInterest an Interest we have already received; the
* flow controller will immediately emit the first matching block
* @param handle the handle to use. May need to be the same handle
* that the Interest was received on.
*/
public CCNFlowControl(ContentName name, Interest outstandingInterest, CCNHandle handle) throws IOException {
this(name, handle);
handleInterest(outstandingInterest);
}
/**
* Add a new namespace to the controller. The controller will register a filter with ccnd to receive
* interests in this namespace.
* @param name
* @throws IOException
*/
public void addNameSpace(ContentName name) throws IOException {
if (!_flowControlEnabled)
return;
Iterator<ContentName> it = _filteredNames.iterator();
while (it.hasNext()) {
ContentName filteredName = it.next();
if (filteredName.isPrefixOf(name)) {
if( Log.isLoggable(Log.FAC_IO, Level.INFO))
Log.info(Log.FAC_IO, "addNameSpace: not adding name: {0} already monitoring prefix: {1}", name, filteredName);
return; // Already part of filter
}
if (name.isPrefixOf(filteredName)) {
_handle.unregisterFilter(filteredName, this);
it.remove();
}
}
_filteredNames.add(name);
_handle.registerFilter(name, this);
if( Log.isLoggable(Log.FAC_IO, Level.INFO))
Log.info(Log.FAC_IO, "Flow controller addNameSpace: added namespace: {0}", name);
}
/**
* Convenience method.
* @see #addNameSpace(ContentName)
* @param name Namespace to be added in string form.
*/
public void addNameSpace(String name) throws MalformedContentNameStringException, IOException {
addNameSpace(ContentName.fromNative(name));
}
/**
* Filter handler method, add a namespace and respond to an existing Interest.
* @throws IOException
*/
public void addNameSpace(ContentName name, Interest outstandingInterest) throws IOException {
addNameSpace(name);
handleInterest(outstandingInterest);
}
/**
* Convenience method.
* @see #startWrite(ContentName, Shape)
* @throws MalformedContentNameStringException if name is malformed
*/
public void startWrite(String name, Shape shape) throws MalformedContentNameStringException, IOException {
startWrite(ContentName.fromNative(name), shape);
}
/**
* This is used to indicate that it should start a write for a stream with this
* name, and should do any stream-specific setup.
* @param name
* @param shape currently unused and may be deprecated in the future. Can only be Shape.STREAM
* @throws MalformedContentNameStringException if name is malformed
* @throws IOException used by subclasses
*/
public void startWrite(ContentName name, Shape shape) throws IOException {}
/**
* Remove a namespace from those we are listening for interests within.
*
* For now we don't have any way to remove a part of a registered namespace from
* buffering so we only allow removal of a namespace if it actually matches something that was
* registered
*
* @param name
*/
public void removeNameSpace(ContentName name) {
removeNameSpace(name, false);
}
private void removeNameSpace(ContentName name, boolean all) {
Iterator<ContentName> it = _filteredNames.iterator();
while (it.hasNext()) {
ContentName filteredName = it.next();
if (all || filteredName.equals(name)) {
_handle.unregisterFilter(filteredName, this);
it.remove();
if( Log.isLoggable(Log.FAC_IO, Level.FINEST))
Log.finest(Log.FAC_IO, "removing namespace: {0}", name);
break;
}
}
}
/**
* Stop attending to all namespaces.
*/
public void removeAllNamespaces() {
removeNameSpace(null, true);
}
/**
* Helper method to clean up and close.
*/
public void close() {
removeAllNamespaces();
}
/**
* Someone needs to do the deregistration if nobody else did
*/
@Override
protected void finalize() throws Throwable {
try {
close(); // Do the deregistration
} finally {
super.finalize();
}
}
/**
* Test if this flow controller is currently serving a particular namespace.
*
* @param childName ContentName of test space
* @return The actual namespace the flow controller is using if it does serve the child
* namespace. null otherwise.
*/
public ContentName getNameSpace(ContentName childName) {
ContentName prefix = null;
for (ContentName nameSpace : _filteredNames) {
if (nameSpace.isPrefixOf(childName)) {
// is this the only one?
if (null == prefix) {
prefix = nameSpace;
} else if (nameSpace.count() > prefix.count()) {
prefix = nameSpace;
}
}
}
return prefix;
}
/**
* Add multiple content objects to this flow controller.
* @see #put(ContentObject)
*
* @param cos ArrayList of ContentObjects to put
* @throws IOException if the put fails
*/
public void put(ArrayList<ContentObject> cos) throws IOException {
for (ContentObject co : cos) {
put(co);
}
}
/**
* Add multiple content objects to this flow controller.
* @see #put(ContentObject)
*
* @param cos Array of ContentObjects
* @throws IOException if the put fails
*/
public void put(ContentObject [] cos) throws IOException {
for (ContentObject co : cos) {
put(co);
}
}
/**
* Add namespace and multiple content at the same time.
* @see #addNameSpace(ContentName)
* @see #put(ArrayList)
*
* @param name ContentName of namespace
* @param cos ArrayList of ContentObjects
* @throws IOException if the put fails
*/
public void put(ContentName name, ArrayList<ContentObject> cos) throws IOException {
addNameSpace(name);
put(cos);
}
/**
* Add namespace and content at the same time
* @see #addNameSpace(ContentName)
* @see #put(ContentObject)
*
* @param name ContentName of namespace
* @param co ContentObject
* @return the ContentObject put
* @throws IOException if the put fails
*/
public ContentObject put(ContentName name, ContentObject co) throws IOException {
addNameSpace(name);
return put(co);
}
/**
* Add a content object to this flow controller. It won't be sent to ccnd immediately unless
* a currently waiting interest matches it.
*
* @param co ContentObject to put
* @return the ContentObject put
* @throws IOException if the put fails
*/
public ContentObject put(ContentObject co) throws IOException {
if (_flowControlEnabled) {
boolean found = false;
for (ContentName name : _filteredNames) {
if (name.isPrefixOf(co.name())) {
found = true;
break;
}
}
if (!found)
throw new IOException("Flow control: co name \"" + co.name()
+ "\" is not in the flow control namespace");
}
return waitForMatch(co);
}
/**
* Hold a content object in buffer until a matching interest has been received.
* @param co
* @throws IOException
*/
private ContentObject waitForMatch(ContentObject co) throws IOException {
if (_flowControlEnabled) {
// Always place the object in the _holdingArea, even if it will be
// transmitted immediately. The reason for always holding objects
// is that there may be different buffer draining policies implemented by
// subclasses. For example, a flow control may retain objects until it
// has verified by separate communication that an intended recipient has
// received them.
if( Log.isLoggable(Log.FAC_IO, Level.FINEST))
Log.finest(Log.FAC_IO, "Holding {0}", co.name());
// Must verify space in _holdingArea or block waiting for space
int size = 0;
int capacity = 0;
synchronized (_holdingArea) {
size = _holdingArea.size();
capacity = _capacity;
}
if (size >= capacity) {
long ourTime = System.currentTimeMillis();
// purge old unmatched interests
// Don't do it too often as this is time consuming
if ((ourTime - _lastPurgeTime) > PURGE) {
synchronized (_unmatchedInterests) {
removeUnmatchedInterests(ourTime);
_lastPurgeTime = ourTime;
}
}
// Now wait for space to be cleared or timeout
// Must guard against "spurious wakeup" so must check elapsed time directly
if( Log.isLoggable(Log.FAC_IO, Level.FINEST))
Log.finest(Log.FAC_IO, "Waiting for drain size is {0}", size);
long elapsed = 0;
synchronized (_holdingArea) {
do {
try {
_holdingArea.wait(_timeoutToUse-elapsed);
} catch (InterruptedException e) {
// intentional no-op
}
elapsed = System.currentTimeMillis() - ourTime;
size = _holdingArea.size();
} while (size >= capacity && (_timeout == SystemConfiguration.NO_TIMEOUT || elapsed < _timeoutToUse));
}
if (size >= capacity) {
String names = "";
for (ContentName name : _filteredNames) {
names += name + ",";
}
Log.warning(Log.FAC_IO, "Flow control buffer full for: " + names);
throw new IOException("Flow control buffer full and not draining");
}
}
assert(size < capacity);
// Space verified so now can hold object. See note above for reason to always hold.
Entry<UnmatchedInterest> match = null;
synchronized (_holdingArea) {
_holdingArea.put(co.name(), co);
// Check for pending interest match to allow immediate transmit
match = _unmatchedInterests.removeMatch(co);
}
if (match != null) {
if (Log.isLoggable(Log.FAC_IO, Level.FINEST))
Log.finest(Log.FAC_IO, "Found pending matching interest for {0}, putting to network.", co.name());
_handle.put(co);
// afterPutAction may immediately remove the object from _holdingArea or retain it
// depending upon the buffer drain policy being implemented.
synchronized (_holdingArea) {
afterPutAction(co);
}
} else {
if (Log.isLoggable(Log.FAC_IO, Level.FINEST))
Log.finest(Log.FAC_IO, "No match found for {0}", co.name());
}
} else // Flow control disabled entirely: put to network immediately
_handle.put(co);
return co;
}
/**
* Function to remove expired interests from the flow controller. This is called when a content
* object is received and when an interest is added to the buffer.
*
* Must be called with _unmatchedInterests locked
*
* @param ourTime current time for checking if interests are expired
*/
private void removeUnmatchedInterests(long ourTime) {
Entry<UnmatchedInterest> removeIt;
do {
removeIt = null;
for (Entry<UnmatchedInterest> uie : _unmatchedInterests.values()) {
if ((ourTime - uie.value().timestamp) > PURGE) {
removeIt = uie;
break;
} else {
//we add interests at the end... so older interests are at the top
break;
}
}
if (removeIt != null) {
if (Log.isLoggable(Log.FAC_IO, Level.INFO))
Log.info(Log.FAC_IO, "Removing unmatched interest {0}", removeIt.interest().name());
_unmatchedInterests.remove(removeIt.interest(), removeIt.value());
}
} while (removeIt != null);
}
/**
* Match incoming interests with data in the buffer. If the interest doesn't match it is
* buffered awaiting potential later incoming data which may match it.
*
* Note that this method is used for testing only, since the interest callback only takes one interest
*
*/
public void handleInterests(ArrayList<Interest> interests) {
for (Interest interest : interests) {
handleInterest(interest);
}
}
/**
* Match an incoming interest with data in the buffer. If the interest doesn't match it is
* buffered awaiting potential later incoming data which may match it. This method returns 0
* if the interest was null.
*
*/
public boolean handleInterest(Interest i) {
if (i == null)
return false;
if (Log.isLoggable(Log.FAC_IO, Level.FINE))
Log.fine(Log.FAC_IO, "Flow controller {0}: got interest: {1}", this, i);
ContentObject co;
synchronized (_holdingArea) {
co = getBestMatch(i);
if (co == null) {
//only check if we are adding the interest, and check before we add so we don't check the new interest
if (_unmatchedInterests.size() > 0)
removeUnmatchedInterests(System.currentTimeMillis());
Log.finest(Log.FAC_IO, "No content matching pending interest: {0}, holding.", i);
_unmatchedInterests.add(i, new UnmatchedInterest());
return false; // XXX is this the right thing to do?
}
}
if( Log.isLoggable(Log.FAC_IO, Level.FINEST))
Log.finest(Log.FAC_IO, "Found content {0} matching interest: {1}",co.name(), i);
try {
_handle.put(co);
synchronized (_holdingArea) {
afterPutAction(co);
}
} catch (IOException e) {
Log.warning(Log.FAC_IO, "IOException in handleInterests: {0}: {1}", e.getClass().getName(), e.getMessage());
Log.warningStackTrace(e);
}
return true;
}
/**
* Allow override of action after a ContentObject is sent to ccnd
*
* NOTE: Don't need to sync on holding area because this is only called within
* holding area sync
* NOTE: Any subclass overriding this method must either make sure to call it eventually (in a _holdingArea sync)
* or understand the use of _nOut and update it appropriately.
*
* @param co ContentObject to remove from flow controller.
* @throws IOException may be thrown by overriding subclasses
*/
public void afterPutAction(ContentObject co) throws IOException {
remove(co);
}
/**
* Must be called with _holdingArea locked
* @param interest
* @param set
* @return
*/
private ContentObject getBestMatch(Interest interest) {
ContentObject bestMatch = null;
if( Log.isLoggable(Log.FAC_IO, Level.FINEST))
Log.finest(Log.FAC_IO, "Looking for best match to {0} among {1} options.", interest, _holdingArea.size());
for ( java.util.Map.Entry<ContentName, ContentObject> entry : _holdingArea.entrySet() ) {
ContentName name = entry.getKey();
ContentObject result = entry.getValue();
// We only have to do something unusual here if the caller is looking for CHILD_SELECTOR_RIGHT
if (null != interest.childSelector() && interest.childSelector() == Interest.CHILD_SELECTOR_RIGHT) {
if (interest.matches(result)) {
if (bestMatch == null)
bestMatch = result;
if (name.compareTo(bestMatch.name()) > 0) {
bestMatch = result;
}
}
} else
if (interest.matches(result))
return result;
}
return bestMatch;
}
/**
* Allow subclasses to override behavior before a flush
* @throws IOException
*/
public void beforeClose() throws IOException {
// default -- do nothing.
}
/**
* Allow subclasses to override behavior after a flush
* @throws IOException
*/
public void afterClose() throws IOException {
waitForPutDrain();
}
/**
* Implements a wait until all outstanding data has been drained from the
* flow controller. This is required on close to ensure that all data is actually
* sent to ccnd.
*
* @throws IOException if the data has not been drained after a reasonable period
*/
protected void waitForPutDrain() throws IOException {
synchronized (_holdingArea) {
long startSize = _nOut;
while (_holdingArea.size() > 0) {
long startTime = System.currentTimeMillis();
boolean keepTrying = true;
do {
try {
long waitTime = _timeoutToUse - (System.currentTimeMillis() - startTime);
if (waitTime > 0)
_holdingArea.wait(waitTime);
} catch (InterruptedException ie) {}
if (_nOut != startSize || (System.currentTimeMillis() - startTime) >= _timeoutToUse)
keepTrying = false;
} while (keepTrying);
if (_nOut == startSize) {
for(ContentName co : _holdingArea.keySet()) {
Log.warning(Log.FAC_IO, "FlowController: still holding: {0}", co.toString());
}
// For now - dump the handlers stack if its active in case that may give a clue about what's wrong.
// We may want to leave this in permanently.
CCNNetworkManager cnm = _handle.getNetworkManager();
if (null != cnm)
cnm.dumpHandlerStackTrace("waitForPutDrain");
throw new IOException("Put(s) with no matching interests - size is " + _holdingArea.size());
}
startSize = _nOut;
}
}
}
/**
* Set the time to wait for buffer to drain on close
* @param timeout timeout in milliseconds
*/
public void setTimeout(int timeout) {
_timeout = timeout;
if (timeout != SystemConfiguration.NO_TIMEOUT)
_timeoutToUse = timeout;
}
/**
* Get the current waiting time for the buffer to drain
* @return timeout in milliseconds
*/
public int getTimeout() {
return _timeout;
}
/**
* Shutdown operation of this flow controller -- wait for all current
* data to clear, and unregister all outstanding interests. Do *not*
* shut down the handle; we might not own it.
* @throws IOException if buffer doesn't drain within timeout
*/
public void shutdown() throws IOException {
waitForPutDrain();
removeAllNamespaces();
}
/**
* Gets the CCNHandle used by this controller
* @return a CCNHandle
*/
public CCNHandle getHandle() {
return _handle;
}
/**
* Remove any currently buffered unmatched interests
*/
public void clearUnmatchedInterests() {
if( Log.isLoggable(Log.FAC_IO, Level.INFO))
Log.info(Log.FAC_IO, "Clearing {0} unmatched interests.", _unmatchedInterests.size());
_unmatchedInterests.clear();
}
/**
* Debugging function to log unmatched interests.
*/
public void logUnmatchedInterests(String logMessage) {
if( Log.isLoggable(Log.FAC_IO, Level.INFO))
Log.info(Log.FAC_IO, "{0}: {1} unmatched interest entries.", logMessage, _unmatchedInterests.size());
for (Entry<UnmatchedInterest> interestEntry : _unmatchedInterests.values()) {
if (null != interestEntry.interest())
if( Log.isLoggable(Log.FAC_IO, Level.INFO))
Log.info(Log.FAC_IO, " Unmatched interest: {0}", interestEntry.interest());
}
}
/**
* Re-enable disabled buffering. Buffering is enabled by default.
*/
public void enable() {
_flowControlEnabled = true;
}
/**
* Change the capacity for the maximum amount of data to buffer before
* causing putters to block. The capacity value is the number of content objects
* that will be buffered.
*
* @param value number of content objects.
*/
public void setCapacity(int value) {
synchronized (_holdingArea) {
_capacity = value;
}
}
/**
* Set the capacity to the maximum possible value, Integer.MAX_VALUE.
*/
public void setMaximumCapacity() {
synchronized (_holdingArea) {
_capacity = Integer.MAX_VALUE;
}
}
/**
* Change the maximum number of unmatched interests to buffer.
* @param value number of interests
*/
public void setInterestCapacity(int value) {
_unmatchedInterests.setCapacity(value);
}
/**
* What is the total capacity of this flow controller?
* @return the total capacity of this flow controller; in other words the
* number of segments that can be written to it before writes will block
*/
public int getCapacity() {
synchronized (_holdingArea) {
return _capacity;
}
}
/**
* Get the number of objects this flow controller is currently holding.
* @return the number of objects (segments) in the buffer
*/
public int size() {
synchronized (_holdingArea) {
return _holdingArea.size();
}
}
/**
* Get the amount of remaining space available in this flow controller's buffer.
* @return the number of additional objects that can currently be written to this controller
*/
public int availableCapacity() {
synchronized (_holdingArea) {
return _capacity - _holdingArea.size(); // off by 1?
}
}
/**
* Remove a ContentObject from the set buffered by this flow controller, either
* because we're done with it, or because we don't want to buffer it anymore.
* Need a way to get the CO to remove; might want a remove(ContentName) or
* something like it.
*/
public void remove(ContentObject co) {
// do synchronize on _holdingArea as we may be called directly; if called
// with lock on _holdingArea will be fine (reentrant locks), though
// should evaluate performance cost
synchronized(_holdingArea) {
_nOut++; // do we need to do this, or only in afterPutAction?
_holdingArea.remove(co.name());
_holdingArea.notify();
}
}
/**
* Remove all the held objects from this buffer.
*/
public void clear() {
synchronized(_holdingArea) {
_nOut += size();
_holdingArea.clear();
_holdingArea.notify();
}
}
/**
* Disable buffering
*
* Warning - calling this risks packet drops. It should only
* be used for tests or other special circumstances in which
* you "know what you are doing".
*/
public void disable() {
removeNameSpace(null, true);
_flowControlEnabled = false;
}
/**
* Help users determine what type of flow controller this is.
*/
public SaveType saveType() { return SaveType.RAW; }
}