/**
   * Manually log out the specified user
   *
   * @param sUserID The user ID to log out
   * @return {@link EChange} if something changed
   */
  @Nonnull
  public EChange logoutUser(@Nullable final String sUserID) {
    m_aRWLock.writeLock().lock();
    LoginInfo aInfo;
    try {
      aInfo = m_aLoggedInUsers.remove(sUserID);
      if (aInfo == null) {
        AuditHelper.onAuditExecuteSuccess("logout", sUserID, "user-not-logged-in");
        return EChange.UNCHANGED;
      }

      // Ensure that the SessionUser is empty. This is only relevant if user is
      // manually logged out without destructing the underlying session
      final SessionUserHolder aSUH =
          SessionUserHolder.getInstanceIfInstantiatedInScope(aInfo.getSessionScope());
      if (aSUH != null) aSUH._reset();

      // Set logout time - in case somebody has a strong reference to the
      // LoginInfo object
      aInfo.setLogoutDTNow();
    } finally {
      m_aRWLock.writeLock().unlock();
    }

    s_aLogger.info(
        "Logged out user '"
            + sUserID
            + "' after "
            + new Period(aInfo.getLoginDT(), aInfo.getLogoutDT()).toString());
    AuditHelper.onAuditExecuteSuccess("logout", sUserID);

    // Execute callback as the very last action
    for (final IUserLogoutCallback aUserLogoutCallback : m_aUserLogoutCallbacks.getAllCallbacks())
      try {
        aUserLogoutCallback.onUserLogout(aInfo);
      } catch (final Throwable t) {
        s_aLogger.error(
            "Failed to invoke onUserLogout callback on "
                + aUserLogoutCallback.toString()
                + "("
                + aInfo.toString()
                + ")",
            t);
      }

    return EChange.CHANGED;
  }