Add neighbor lifecycle management.

 * Made PIMNeighbor mostly immutable (apart from updatable timestamp)
 * Equals and hashCode for PIMNeighbor
 * Remove neighbor when we see a HELLO with holdTime==0
 * Periodic task to time out neighbors who haven't sent a HELLO in a while
 * Added a CLI command to view PIM neighbors

Change-Id: I59e52a847f7abcb8e9ac660c2cccace53e46867b
diff --git a/apps/pim/src/main/java/org/onosproject/pim/cli/PimNeighborsListCommand.java b/apps/pim/src/main/java/org/onosproject/pim/cli/PimNeighborsListCommand.java
new file mode 100644
index 0000000..605ec81
--- /dev/null
+++ b/apps/pim/src/main/java/org/onosproject/pim/cli/PimNeighborsListCommand.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * 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.pim.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.util.Tools;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.pim.impl.PIMInterface;
+import org.onosproject.pim.impl.PIMInterfaceService;
+import org.onosproject.pim.impl.PIMNeighbor;
+
+import java.util.Set;
+
+/**
+ * Lists PIM neighbors.
+ */
+@Command(scope = "onos", name = "pim-neighbors",
+        description = "Lists the PIM neighbors")
+public class PimNeighborsListCommand extends AbstractShellCommand {
+
+    private static final String INTF_FORMAT = "interface=%s, address=%s";
+    private static final String NEIGHBOR_FORMAT = "  neighbor=%s, uptime=%s, holdtime=%s, drPriority=%s, genId=%s";
+
+    @Override
+    protected void execute() {
+        PIMInterfaceService interfaceService = get(PIMInterfaceService.class);
+
+        Set<PIMInterface> interfaces = interfaceService.getPimInterfaces();
+
+        for (PIMInterface intf : interfaces) {
+            print(INTF_FORMAT, intf.getInterface().name(), intf.getIpAddress());
+            for (PIMNeighbor neighbor : intf.getNeighbors()) {
+                // Filter out the PIM neighbor representing 'us'
+                if (!neighbor.ipAddress().equals(intf.getIpAddress())) {
+                    print(NEIGHBOR_FORMAT, neighbor.ipAddress(),
+                            Tools.timeAgo(neighbor.upTime()), neighbor.holdtime(),
+                            neighbor.priority(), neighbor.generationId());
+                }
+            }
+        }
+    }
+
+}
diff --git a/apps/pim/src/main/java/org/onosproject/pim/impl/PIMInterface.java b/apps/pim/src/main/java/org/onosproject/pim/impl/PIMInterface.java
index ca82052..1bedd9d 100644
--- a/apps/pim/src/main/java/org/onosproject/pim/impl/PIMInterface.java
+++ b/apps/pim/src/main/java/org/onosproject/pim/impl/PIMInterface.java
@@ -32,10 +32,12 @@
 import org.slf4j.Logger;
 
 import java.nio.ByteBuffer;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Random;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -99,10 +101,7 @@
         generationId = new Random().nextInt();
 
         // Create a PIM Neighbor to represent ourselves for DR election.
-        PIMNeighbor us = new PIMNeighbor(ourIp, mac);
-
-        // Priority and IP address are all we need to DR election.
-        us.setPriority(priority);
+        PIMNeighbor us = new PIMNeighbor(ourIp, mac, holdTime, 0, priority, generationId);
 
         pimNeighbors.put(ourIp, us);
         drIpaddress = ourIp;
@@ -199,6 +198,32 @@
     }
 
     /**
+     * Gets the neighbors seen on this interface.
+     *
+     * @return PIM neighbors
+     */
+    public Collection<PIMNeighbor> getNeighbors() {
+        return pimNeighbors.values();
+    }
+
+    /**
+     * Checks whether any of our neighbors have expired, and cleans up their
+     * state if they have.
+     */
+    public void checkNeighborTimeouts() {
+        Set<PIMNeighbor> expired = pimNeighbors.values().stream()
+                // Don't time ourselves out!
+                .filter(neighbor -> !neighbor.ipAddress().equals(getIpAddress()))
+                .filter(neighbor -> neighbor.isExpired())
+                .collect(Collectors.toSet());
+
+        for (PIMNeighbor neighbor : expired) {
+            log.info("Timing out neighbor {}", neighbor);
+            pimNeighbors.remove(neighbor.ipAddress(), neighbor);
+        }
+    }
+
+    /**
      * Multicast a hello message out our interface.  This hello message is sent
      * periodically during the normal PIM Neighbor refresh time, as well as a
      * result of a newly created interface.
@@ -234,7 +259,7 @@
      *     <li>We <em>may</em> have to create a new neighbor if one does not already exist</li>
      *     <li>We <em>may</em> need to re-elect a new DR if new information is received</li>
      *     <li>We <em>may</em> need to send an existing neighbor all joins if the genid changed</li>
-     *     <li>We will refresh the neighbors timestamp</li>
+     *     <li>We will refresh the neighbor's timestamp</li>
      * </ul>
      *
      * @param ethPkt the Ethernet packet header
@@ -259,7 +284,7 @@
         checkNotNull(dr);
 
         IpAddress drip = drIpaddress;
-        int drpri = dr.getPriority();
+        int drpri = dr.priority();
 
         // Assume we do not need to run a DR election
         boolean reElectDr = false;
@@ -269,18 +294,24 @@
 
         // Determine if we already have a PIMNeighbor
         PIMNeighbor nbr = pimNeighbors.getOrDefault(srcip, null);
+        PIMNeighbor newNbr = PIMNeighbor.createPimNeighbor(srcip, nbrmac, hello.getOptions().values());
+
         if (nbr == null) {
-            nbr = new PIMNeighbor(srcip, hello.getOptions());
-            checkNotNull(nbr);
-        } else {
-            Integer previousGenid = nbr.getGenid();
-            nbr.addOptions(hello.getOptions());
-            if (previousGenid != nbr.getGenid()) {
-                genidChanged = true;
+            pimNeighbors.putIfAbsent(srcip, newNbr);
+            nbr = newNbr;
+        } else if (!nbr.equals(newNbr)) {
+            if (newNbr.holdtime() == 0) {
+                // Neighbor has shut down. Remove them and clean up
+                pimNeighbors.remove(srcip, nbr);
+                return;
+            } else {
+                // Neighbor has changed one of their options.
+                pimNeighbors.put(srcip, newNbr);
+                nbr = newNbr;
             }
         }
 
-        // Refresh this neighbors timestamp
+        // Refresh this neighbor's timestamp
         nbr.refreshTimestamp();
 
         /*
@@ -300,8 +331,8 @@
     // Run an election if we need to.  Return the elected IP address.
     private IpAddress election(PIMNeighbor nbr, IpAddress drIp, int drPriority) {
 
-        IpAddress nbrIp = nbr.getIpaddr();
-        if (nbr.getPriority() > drPriority) {
+        IpAddress nbrIp = nbr.ipAddress();
+        if (nbr.priority() > drPriority) {
             return nbrIp;
         }
 
diff --git a/apps/pim/src/main/java/org/onosproject/pim/impl/PIMInterfaceManager.java b/apps/pim/src/main/java/org/onosproject/pim/impl/PIMInterfaceManager.java
index bcaeb2c..d6205e5 100644
--- a/apps/pim/src/main/java/org/onosproject/pim/impl/PIMInterfaceManager.java
+++ b/apps/pim/src/main/java/org/onosproject/pim/impl/PIMInterfaceManager.java
@@ -23,6 +23,7 @@
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
+import org.onlab.util.SafeRecurringTask;
 import org.onosproject.incubator.net.intf.Interface;
 import org.onosproject.incubator.net.intf.InterfaceEvent;
 import org.onosproject.incubator.net.intf.InterfaceListener;
@@ -58,8 +59,10 @@
     private static final Class<PimInterfaceConfig> PIM_INTERFACE_CONFIG_CLASS = PimInterfaceConfig.class;
     private static final String PIM_INTERFACE_CONFIG_KEY = "pimInterface";
 
-    // Create a Scheduled Executor service to send PIM hellos
-    private final ScheduledExecutorService helloScheduler =
+    private static final int DEFAULT_TIMEOUT_TASK_PERIOD_MS = 250;
+
+    // Create a Scheduled Executor service for recurring tasks
+    private final ScheduledExecutorService scheduledExecutorService =
             Executors.newScheduledThreadPool(1);
 
     // Wait for a bout 3 seconds before sending the initial hello messages.
@@ -69,6 +72,8 @@
     // Send PIM hello packets: 30 seconds.
     private final long pimHelloPeriod = 30;
 
+    private final int timeoutTaskPeriod = DEFAULT_TIMEOUT_TASK_PERIOD_MS;
+
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected PacketService packetService;
 
@@ -113,18 +118,16 @@
         interfaceService.addListener(interfaceListener);
 
         // Schedule the periodic hello sender.
-        helloScheduler.scheduleAtFixedRate(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    for (PIMInterface pif : pimInterfaces.values()) {
-                        pif.sendHello();
-                    }
-                } catch (Exception e) {
-                    log.warn("exception", e);
-                }
-            }
-        }, initialHelloDelay, pimHelloPeriod, TimeUnit.SECONDS);
+        scheduledExecutorService.scheduleAtFixedRate(
+                SafeRecurringTask.wrap(
+                        () -> pimInterfaces.values().forEach(PIMInterface::sendHello)),
+                initialHelloDelay, pimHelloPeriod, TimeUnit.SECONDS);
+
+        // Schedule task to periodically time out expired neighbors
+        scheduledExecutorService.scheduleAtFixedRate(
+                SafeRecurringTask.wrap(
+                        () -> pimInterfaces.values().forEach(PIMInterface::checkNeighborTimeouts)),
+                0, timeoutTaskPeriod, TimeUnit.MILLISECONDS);
 
         log.info("Started");
     }
@@ -136,7 +139,7 @@
         networkConfig.unregisterConfigFactory(pimConfigFactory);
 
         // Shutdown the periodic hello task.
-        helloScheduler.shutdown();
+        scheduledExecutorService.shutdown();
 
         log.info("Stopped");
     }
diff --git a/apps/pim/src/main/java/org/onosproject/pim/impl/PIMNeighbor.java b/apps/pim/src/main/java/org/onosproject/pim/impl/PIMNeighbor.java
index f44cd1b..9e6fa38 100644
--- a/apps/pim/src/main/java/org/onosproject/pim/impl/PIMNeighbor.java
+++ b/apps/pim/src/main/java/org/onosproject/pim/impl/PIMNeighbor.java
@@ -15,216 +15,226 @@
  */
 package org.onosproject.pim.impl;
 
+import com.google.common.base.MoreObjects;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.pim.PIMHelloOption;
-import org.slf4j.Logger;
 
 import java.nio.ByteBuffer;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Map;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
 
-import static org.slf4j.LoggerFactory.getLogger;
+import static com.google.common.base.Preconditions.checkNotNull;
 
+/**
+ * Represents a PIM neighbor.
+ */
 public class PIMNeighbor {
 
-    private final Logger log = getLogger(getClass());
-
     // IP Address of this neighbor
-    private IpAddress ipAddr;
+    private final IpAddress ipAddr;
 
     // MAC Address of the neighbor (Need for sending J/P)
-    private MacAddress macAddr;
+    private final MacAddress macAddr;
 
     // Hello Options
     // Our hello opt holdTime
-    private short holdTime;
+    private final short holdTime;
 
     // Our hello opt prune delay
-    private int pruneDelay;
+    private final int pruneDelay;
 
     // Neighbor priority
-    private int priority;
+    private final int priority;
 
     // Our current genId
-    private int genId;
+    private final int genId;
+
+    private final long upTime;
 
     // Our timestamp for this neighbor
-    private Date lastRefresh;
+    private long lastRefresh;
 
     /**
-     * Construct a new PIM Neighbor.
+     * Class constructor.
      *
-     * @param ipAddr the IP Address of our new neighbor
-     * @param opts option map
+     * @param ipAddress neighbor IP address
+     * @param macAddress neighbor MAC address
+     * @param holdTime hold time
+     * @param pruneDelay prune delay
+     * @param priority priority
+     * @param genId generation ID
      */
-    public PIMNeighbor(IpAddress ipAddr, Map<Short, PIMHelloOption> opts) {
-        this.ipAddr = ipAddr;
-        this.addOptions(opts);
+    public PIMNeighbor(IpAddress ipAddress, MacAddress macAddress,
+                       short holdTime, int pruneDelay, int priority, int genId) {
+        this.ipAddr = checkNotNull(ipAddress);
+        this.macAddr = checkNotNull(macAddress);
+        this.holdTime = holdTime;
+        this.pruneDelay = pruneDelay;
+        this.priority = priority;
+        this.genId = genId;
+
+        this.upTime = System.currentTimeMillis();
     }
 
     /**
-     * Construct a new PIM neighbor.
-     *
-     * @param ipAddr the neighbors IP addr
-     * @param macAddr MAC address
-     */
-    public PIMNeighbor(IpAddress ipAddr, MacAddress macAddr) {
-        this.ipAddr = ipAddr;
-        this.macAddr = macAddr;
-    }
-
-    /**
-     * Get the MAC address of this neighbor.
-     *
-     * @return the mac address
-     */
-    public MacAddress getMacaddr() {
-        return macAddr;
-    }
-
-    /**
-     * Get the IP Address of our neighbor.
+     * Gets the IP address of our neighbor.
      *
      * @return the IP address of our neighbor
      */
-    public IpAddress getIpaddr() {
+    public IpAddress ipAddress() {
         return ipAddr;
     }
 
     /**
-     * Set the IP address of our neighbor.
+     * Gets the MAC address of this neighbor.
      *
-     * @param ipAddr our neighbors IP address
+     * @return the mac address
      */
-    public void setIpaddr(IpAddress ipAddr) {
-        this.ipAddr = ipAddr;
+    public MacAddress macAddress() {
+        return macAddr;
     }
 
     /**
-     * Get our neighbors holdTime.
+     * Gets our neighbor's hold time.
      *
-     * @return the holdTime
+     * @return the hold time
      */
-    public short getHoldtime() {
+    public short holdtime() {
         return holdTime;
     }
 
     /**
-     * Set our neighbors holdTime.
+     * Gets our neighbor's prune delay.
      *
-     * @param holdTime the holdTime
+     * @return our neighbor's prune delay
      */
-    public void setHoldtime(short holdTime) {
-        this.holdTime = holdTime;
-    }
-
-    /**
-     * Get our neighbors prune delay.
-     *
-     * @return our neighbors prune delay
-     */
-    public int getPruneDelay() {
+    public int pruneDelay() {
         return pruneDelay;
     }
 
     /**
-     * Set our neighbors prune delay.
+     * Gets our neighbor's priority.
      *
-     * @param pruneDelay the prune delay
+     * @return our neighbor's priority
      */
-    public void setPruneDelay(int pruneDelay) {
-        this.pruneDelay = pruneDelay;
-    }
-
-    /**
-     * Get our neighbors priority.
-     *
-     * @return our neighbors priority
-     */
-    public int getPriority() {
+    public int priority() {
         return priority;
     }
 
     /**
-     * Set our neighbors priority.
+     * Gets our neighbor's generation ID.
      *
-     * @param priority our neighbors priority
+     * @return our neighbor's generation ID
      */
-    public void setPriority(int priority) {
-        this.priority = priority;
-    }
-
-    /**
-     * Get our neighbors Genid.
-     *
-     * @return our neighbor Genid
-     */
-    public int getGenid() {
+    public int generationId() {
         return genId;
     }
 
     /**
-     * Set our neighbors GenId.
+     * Gets the last time we heard a HELLO from this neighbor.
      *
-     * @param genId our neighbors GenId
+     * @return last refresh time
      */
-    public void setGenid(int genId) {
-        this.genId = genId;
+    public long lastRefresh() {
+        return lastRefresh;
     }
 
     /**
-     * Add the options for this neighbor if needed.
+     * Gets the time that we first learnt of this neighbor.
      *
-     * @param opts the options to be added/modified
-     * @return true if options changed, false if no option has changed
+     * @return up time
      */
-    public boolean addOptions(Map<Short, PIMHelloOption> opts) {
-
-        boolean changed = false;
-
-        for (PIMHelloOption opt : opts.values()) {
-            Short otype = opt.getOptType();
-            ByteBuffer val = ByteBuffer.wrap(opt.getValue());
-
-            if (otype == PIMHelloOption.OPT_ADDRLIST) {
-                // TODO: Will implement someday
-            } else if (otype == PIMHelloOption.OPT_GENID) {
-                int newval = val.getInt();
-                if (newval != genId) {
-                    genId = newval;
-                    changed = true;
-                }
-            } else if (otype == PIMHelloOption.OPT_HOLDTIME) {
-                short newval = val.getShort();
-                if (newval != holdTime) {
-                    holdTime = newval;
-                    changed = true;
-                }
-            } else if (otype == PIMHelloOption.OPT_PRIORITY) {
-                int newval = val.getInt();
-                if (newval != priority) {
-                    priority = newval;
-                    changed = true;
-                }
-            } else if (otype == PIMHelloOption.OPT_PRUNEDELAY) {
-                int newval = val.getInt();
-                if (newval != pruneDelay) {
-                    pruneDelay = newval;
-                    changed = true;
-                }
-            } else {
-                log.warn("received unknown pim hello options" + otype);
-            }
-        }
-        return changed;
+    public long upTime() {
+        return upTime;
     }
 
     /**
-     * Refresh this neighbors timestamp.
+     * Refreshes this neighbor's last seen timestamp.
      */
     public void refreshTimestamp() {
-        lastRefresh = Calendar.getInstance().getTime();
+        lastRefresh = System.currentTimeMillis();
     }
+
+    /**
+     * Returns whether this neighbor is expired or not.
+     *
+     * @return true if the neighbor is expired, otherwise false
+     */
+    public boolean isExpired() {
+        return lastRefresh + TimeUnit.SECONDS.toMillis(holdTime)
+                < System.currentTimeMillis();
+    }
+
+    /**
+     * Creates a PIM neighbor based on an IP, MAC, and collection of PIM HELLO
+     * options.
+     *
+     * @param ipAddress neighbor IP address
+     * @param macAddress neighbor MAC address
+     * @param opts options from the PIM HELLO packet
+     * @return new PIM neighbor
+     */
+    public static PIMNeighbor createPimNeighbor(IpAddress ipAddress,
+                                                MacAddress macAddress,
+                                                Collection<PIMHelloOption> opts) {
+
+        int generationID = PIMHelloOption.DEFAULT_GENID;
+        short holdTime = PIMHelloOption.DEFAULT_HOLDTIME;
+        int priority = PIMHelloOption.DEFAULT_PRIORITY;
+        int pruneDelay = PIMHelloOption.DEFAULT_PRUNEDELAY;
+
+        for (PIMHelloOption opt : opts) {
+            short type = opt.getOptType();
+            ByteBuffer value = ByteBuffer.wrap(opt.getValue());
+
+            if (type == PIMHelloOption.OPT_GENID) {
+                generationID = value.getInt();
+            } else if (type == PIMHelloOption.OPT_HOLDTIME) {
+                holdTime = value.getShort();
+            } else if (type == PIMHelloOption.OPT_PRIORITY) {
+                priority = value.getInt();
+            } else if (type == PIMHelloOption.OPT_PRUNEDELAY) {
+                pruneDelay = value.getInt();
+            } else if (type == PIMHelloOption.OPT_ADDRLIST) {
+                // TODO: Will implement someday
+            }
+        }
+
+        return new PIMNeighbor(ipAddress, macAddress, holdTime, pruneDelay, priority, generationID);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof PIMNeighbor)) {
+            return false;
+        }
+
+        PIMNeighbor that = (PIMNeighbor) other;
+
+        return this.ipAddr.equals(that.ipAddress()) &&
+                this.macAddr.equals(that.macAddress()) &&
+                this.genId == that.genId &&
+                this.holdTime == that.holdTime &&
+                this.priority == that.priority;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(ipAddr, macAddr, genId, holdTime, priority);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("ipAddress", ipAddr)
+                .add("macAddress", macAddr)
+                .add("generationId", genId)
+                .add("holdTime", holdTime)
+                .add("priority", priority)
+                .add("pruneDelay", pruneDelay)
+                .toString();
+    }
+
 }
diff --git a/apps/pim/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/pim/src/main/resources/OSGI-INF/blueprint/shell-config.xml
index 780ad9f..012760d 100644
--- a/apps/pim/src/main/resources/OSGI-INF/blueprint/shell-config.xml
+++ b/apps/pim/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -19,6 +19,9 @@
         <command>
             <action class="org.onosproject.pim.cli.PimInterfacesListCommand"/>
         </command>
+        <command>
+            <action class="org.onosproject.pim.cli.PimNeighborsListCommand"/>
+        </command>
     </command-bundle>
 
 </blueprint>
diff --git a/utils/misc/src/main/java/org/onlab/util/SafeRecurringTask.java b/utils/misc/src/main/java/org/onlab/util/SafeRecurringTask.java
new file mode 100644
index 0000000..756abdd
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/util/SafeRecurringTask.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * 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.onlab.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Wrapper for a recurring task which catches all exceptions to prevent task
+ * being suppressed in a ScheduledExecutorService.
+ */
+public final class SafeRecurringTask implements Runnable {
+
+    private static final Logger log = LoggerFactory.getLogger(SafeRecurringTask.class);
+
+    private final Runnable runnable;
+
+    /**
+     * Constructor.
+     *
+     * @param runnable runnable to wrap
+     */
+    private SafeRecurringTask(Runnable runnable) {
+        this.runnable = runnable;
+    }
+
+    @Override
+    public void run() {
+        if (Thread.currentThread().isInterrupted()) {
+            log.info("Task interrupted, quitting");
+            return;
+        }
+
+        try {
+            runnable.run();
+        } catch (Exception e) {
+            // Catch all exceptions to avoid task being suppressed
+            log.error("Exception thrown during task", e);
+        }
+    }
+
+    /**
+     * Wraps a runnable in a safe recurring task.
+     *
+     * @param runnable runnable to wrap
+     * @return safe recurring task
+     */
+    public static SafeRecurringTask wrap(Runnable runnable) {
+        return new SafeRecurringTask(runnable);
+    }
+
+}