@Override
    public void leftCluster(byte[] nodeID)
    {
        // Another node left the cluster.
        final NodeID nodeIDOfLostNode = NodeID.getInstance(nodeID);
        Log.debug("Cluster node {} just left the cluster.", nodeIDOfLostNode);

        // When the local node drops out of the cluster (for example, due to a network failure), then from the perspective
        // of that node, all other nodes leave the cluster. This method is invoked for each of them. In certain
        // circumstances, this can mean that the local node no longer has access to all data (or its backups) that is
        // maintained in the clustered caches. From the perspective of the remaining node, this data is lost. (OF-2297/OF-2300).
        // To prevent this being an issue, most caches have supporting local data structures that maintain a copy of the most
        // critical bits of the data stored in the clustered cache, which is to be used to detect and/or correct such a
        // loss in data. This is done in the next few lines of this method.
        detectAndFixBrokenCaches(); // This excludes Users Sessions Cache, which is a bit of an odd duckling. This one is processed later in this method.

        // When a peer server leaves the cluster, any remote routes that were associated with the defunct node must be
        // dropped from the routing caches (and supporting data structures) that are shared by the remaining cluster member(s).

        // Note: All remaining cluster nodes will be in a race to clean up the same data. We can not depend on cluster
        // seniority to appoint a 'single' cleanup node, because for a small moment we may not have a senior cluster member.

        // Remove outgoing server routes accessed through the node that left the cluster.
        final Set<DomainPair> remoteServers = s2sDomainPairsByClusterNode.remove(nodeIDOfLostNode);
        CacheUtil.removeValueFromCache(serversCache, nodeIDOfLostNode); // Clean up remote data from the cache, but note that the return value can't be guaranteed to be correct/complete (do not use it)!
        if (remoteServers != null) {
            for (final DomainPair domainPair : remoteServers) {
                Log.debug("Removing server route for {} that is no longer available because cluster node {} left the cluster.", domainPair, nodeIDOfLostNode);
                if (!removeServerRoute(domainPair, nodeIDOfLostNode)) {
                    Log.warn("Cluster node {} just left the cluster. Federated connections on that node are no longer available. To reflect this, we're deleting these routes. While doing this for '{}' the session could not be removed (presumably because it is not available on that cluster node).", nodeID, domainPair);
                }
            }
        }
        Log.info("Cluster node {} just left the cluster. A total of {} outgoing server sessions was living there, and are no longer available.", nodeIDOfLostNode, remoteServers == null ? 0 : remoteServers.size());

        // Remove component routes hosted in node that left the cluster.
        final Set<String> componentJids = componentsByClusterNode.remove(nodeIDOfLostNode);
        CacheUtil.removeValueFromMultiValuedCache(componentsCache, nodeIDOfLostNode); // Clean up remote data from the cache, but note that the return value can't be guaranteed to be correct/complete (do not use it)!
        int lostComponentsCount = 0;
        if (componentJids != null) {
            Log.debug("Removing node '{}' from componentsByClusteredNode: {}", nodeIDOfLostNode, componentJids);
            for (final String componentJid : componentJids) {
                if (removeComponentRoute(new JID(componentJid), nodeIDOfLostNode)) {
                    Log.debug("Removing component route for {} that is no longer available because cluster node {} left the cluster.", componentJid, nodeIDOfLostNode);
                    lostComponentsCount++;;
                }
            }
        }
        Log.info("Cluster node {} just left the cluster. A total of {} component sessions is now no longer available as a result.", nodeIDOfLostNode, lostComponentsCount);

        // Remove client routes hosted in node that left the cluster.
        final Set<String> removed = routeOwnersByClusterNode.remove(nodeIDOfLostNode);
        final AtomicLong removedSessionCount = new AtomicLong();
        if (removed != null) {
            removed.forEach(fullJID -> {
                Log.debug("Removing client route for {} that is no longer available because cluster node {} left the cluster.", fullJID, nodeIDOfLostNode);
                final JID offlineJID = new JID(fullJID);
                removeClientRoute(offlineJID);
                removedSessionCount.incrementAndGet();
            });
        }
        Log.debug("Cluster node {} just left the cluster. A total of {} client routes was living there, and are no longer available.", nodeIDOfLostNode, removedSessionCount.get());

        // With all of the other caches fixed and adjusted, process the Users Sessions Cache.
        restoreUsersSessionsCache();

        // Now that the users sessions cache is restored, we can proceed sending presence updates for all removed users.
        if (removed != null) {
            removed.forEach(fullJID -> {
                final JID offlineJID = new JID(fullJID);
                try {
                    final Presence presence = new Presence(Presence.Type.unavailable);
                    presence.setFrom(offlineJID);
                    XMPPServer.getInstance().getPresenceRouter().route(presence);
                    // TODO: OF-2302 This broadcasts the presence over the entire (remaining) cluster, which is too much because it is done by each remaining cluster node.
                } catch (final PacketException e) {
                    Log.error("Remote node {} left the cluster. Users on that node are no longer available. To reflect this, we're broadcasting presence unavailable on their behalf.  While doing this for '{}', this caused an exception to occur.", nodeIDOfLostNode, fullJID, e);
                }
            });
        }
    }
Verifies that {@link #usersSessionsCache} and {@link #usersCache} are in a consistent state.

Note that this operation can be costly in terms of resource usage. Use with caution in large / busy systems.

The returned multi-map can contain up to four keys: info, fail, pass, data. All entry values are a human readable
description of a checked characteristic. When the state is consistent, no 'fail' entries will be returned.

@return A consistency state report.
@see #usersSessionsCache which tracks user sessions.
@see #usersCache which the cache that is used tho share data with other cluster nodes.
xmppserver/src/main/java/org/jivesoftware/openfire/spi/RoutingTableImpl.java