class UserTimeoutTask extends TimerTask {
        @Override
        public void run() {
            checkForTimedOutUsers();
        }
    }

    /**
     * Informs all users local to this cluster node that he or she is being removed from the room because the MUC
     * service is being shut down.
     *
     * The implementation is optimized to run as fast as possible (to prevent prolonging the shutdown).
     */
    private void broadcastShutdown()
    {
        Log.debug( "Notifying all local users about the imminent destruction of chat service '{}'", chatServiceName );

        final Set<OccupantManager.Occupant> localOccupants = occupantManager.getLocalOccupants();

        if (localOccupants.isEmpty()) {
            return;
        }

        // A thread pool is used to broadcast concurrently, as well as to limit the execution time of this service.
        final ThreadFactory threadFactory = new NamedThreadFactory("MUC-Shutdown-", Executors.defaultThreadFactory(), false, Thread.NORM_PRIORITY);
        final ExecutorService service = Executors.newFixedThreadPool( Math.min( localOccupants.size(), 10 ), threadFactory );

        // Queue all tasks in the executor service.
        for ( final OccupantManager.Occupant localOccupant : localOccupants )
        {
            service.submit(() -> {
                try
                {
                    // Obtaining the room without acquiring a lock. Usage of the room is read-only (the implementation below
                    // should not modify the room state in a way that the cluster cares about), and more importantly, speed
                    // is of importance (waiting for every room's lock to be acquired would slow down the shutdown process).
                    // Lastly, this service is shutting down (likely because the server is shutting down). The trade-off
                    // between speed and access of room state while not holding a lock seems worth while here.
                    final MUCRoom room = getChatRoom(localOccupant.getRoomName());
                    if (room == null) {
                        // Mismatch between MUCUser#getRooms() and MUCRoom#localMUCRoomManager ?
                        Log.warn("User '{}' appears to have had a role in room '{}' of service '{}' that does not seem to exist.", localOccupant.getRealJID(), localOccupant.getRoomName(), chatServiceName);
                        return;
                    }
                    final MUCRole role = room.getOccupantByFullJID(localOccupant.getRealJID());
                    if (role == null) {
                        // Mismatch between MUCUser#getRooms() and MUCRoom#occupants ?
                        Log.warn("User '{}' appears to have had a role in room '{}' of service '{}' but that role does not seem to exist.", localOccupant.getRealJID(), localOccupant.getRoomName(), chatServiceName);
                        return;
                    }

                    // Send a presence stanza of type "unavailable" to the occupant
                    final Presence presence = room.createPresence( Presence.Type.unavailable );
                    presence.setFrom( role.getRoleAddress() );

                    // A fragment containing the x-extension.
                    final Element fragment = presence.addChildElement( "x", "http://jabber.org/protocol/muc#user" );
                    final Element item = fragment.addElement( "item" );
                    item.addAttribute( "affiliation", "none" );
                    item.addAttribute( "role", "none" );
                    fragment.addElement( "status" ).addAttribute( "code", "332" );

                    // Make sure that the presence change for each user is only sent to that user (and not broadcast in the room)!
                    // Not needed to create a defensive copy of the stanza. It's not used anywhere else.
                    role.send( presence );

                    // Let all other cluster nodes know!
                    room.removeOccupantRole(role);
                }
                catch ( final Exception e )
                {
                    Log.debug( "Unable to inform {} about the imminent destruction of chat service '{}'", localOccupant.realJID, chatServiceName, e );
                }
            });
        }

        // Try to shutdown - wait - force shutdown.
        service.shutdown();
        try
        {
            if (service.awaitTermination( JiveGlobals.getIntProperty( "xmpp.muc.await-termination-millis", 500 ), TimeUnit.MILLISECONDS )) {
                Log.debug("Successfully notified all local users about the imminent destruction of chat service '{}'", chatServiceName);
            } else {
                Log.debug("Unable to notify all local users about the imminent destruction of chat service '{}' (timeout)", chatServiceName);
            }
        }
        catch ( final InterruptedException e )
        {
            Log.debug( "Interrupted while waiting for all users to be notified of shutdown of chat service '{}'. Shutting down immediately.", chatServiceName );
        }
        service.shutdownNow();
    }

    /**
     * Iterates over the local occupants of MUC rooms (users connected to the local cluster node), to determine if an
     * action needs to be taken based on their (lack of) activity. Depending on the configuration of Openfire, inactive
     * users (users that are connected, but have not typed anything) are kicked from the room, and/or are explicitly
     * asked for a proof of life (connectivity), removing them if this proof is not given.
     */
    private void checkForTimedOutUsers()
    {<FILL_FUNCTION_BODY>

        for (final OccupantManager.Occupant occupant : occupantManager.getLocalOccupants())
        {
            try
            {
                if (userIdleKick != null && occupant.getLastActive().isBefore(Instant.now().minus(userIdleKick)))
                {
                    // Kick users if 'user_idle' feature is enabled and the user has been idle for too long.
                    tryRemoveOccupantFromRoom(occupant, JiveGlobals.getProperty("admin.mucRoom.timeoutKickReason", "User was inactive for longer than the allowed maximum duration of " + userIdleKick.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase()) + "." );
                }
                else if (userIdlePing != null)
                {
                    // Check if the occupant has been inactive for to long.
                    final Instant lastActive = occupant.getLastActive();
                    final boolean isInactive = lastActive.isBefore(Instant.now().minus(userIdlePing));

                    // Ensure that we didn't quite recently send a ping already.
                    final Instant lastPing = occupant.getLastPingRequest();
                    final boolean isRecentlyPinged = lastPing != null && lastPing.isAfter(Instant.now().minus(userIdlePing));

                    if (isInactive && !isRecentlyPinged) {
                        // Ping the user if it hasn't been kicked already, the feature is enabled, and the user has been idle for too long.
                        pingOccupant(occupant);
                    }
                }
            }
            catch (final Throwable e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        }
    
<no_super_class>