/**
   * Test for issue #49: Sessions not associated with a memcached node don't get associated as soon
   * as a memcached is available
   *
   * @throws InterruptedException
   * @throws IOException
   * @throws TimeoutException
   * @throws ExecutionException
   */
  @Test(enabled = true)
  public void testNotAssociatedSessionGetsAssociatedIssue49()
      throws InterruptedException, IOException, ExecutionException, TimeoutException {
    _daemon.stop();

    final SessionManager manager = _tomcat1.getManager();
    manager.setMaxInactiveInterval(5);
    manager.setSticky(true);
    final SessionIdFormat sessionIdFormat = new SessionIdFormat();

    final Session session = manager.createSession(null);
    assertNull(sessionIdFormat.extractMemcachedId(session.getId()));

    _daemon.start();

    // Wait so that the daemon will be available and the client can reconnect (async get didn't do
    // the trick)
    Thread.sleep(4000);

    final String newSessionId =
        manager.getMemcachedSessionService().changeSessionIdOnMemcachedFailover(session.getId());
    assertNotNull(newSessionId);
    assertEquals(newSessionId, session.getId());
    assertEquals(sessionIdFormat.extractMemcachedId(newSessionId), _memcachedNodeId);
  }
  /** Invoked after a non-sticky session is removed from memcached. */
  protected void onAfterDeleteFromMemcached(@Nonnull final String sessionId) {
    final long start = System.currentTimeMillis();

    final String validityInfoKey = _sessionIdFormat.createValidityInfoKeyName(sessionId);
    _memcached.delete(validityInfoKey);

    if (_storeSecondaryBackup) {
      _memcached.delete(_sessionIdFormat.createBackupKey(sessionId));
      _memcached.delete(_sessionIdFormat.createBackupKey(validityInfoKey));
    }

    _stats.registerSince(NON_STICKY_AFTER_DELETE_FROM_MEMCACHED, start);
  }
  /**
   * Is invoked for the backup of a non-sticky session that was not accessed for the current
   * request.
   */
  protected void onBackupWithoutLoadedSession(
      @Nonnull final String sessionId,
      @Nonnull final String requestId,
      @Nonnull final BackupSessionService backupSessionService) {

    if (!_sessionIdFormat.isValid(sessionId)) {
      return;
    }

    try {

      final long start = System.currentTimeMillis();

      final String validityKey = _sessionIdFormat.createValidityInfoKeyName(sessionId);
      final SessionValidityInfo validityInfo = loadSessionValidityInfoForValidityKey(validityKey);
      if (validityInfo == null) {
        _log.warn("Found no validity info for session id " + sessionId);
        return;
      }

      final int maxInactiveInterval = validityInfo.getMaxInactiveInterval();
      final byte[] validityData =
          encode(maxInactiveInterval, System.currentTimeMillis(), System.currentTimeMillis());
      // fix for #88, along with the change in session.getMemcachedExpirationTimeToSet
      final int expiration = maxInactiveInterval <= 0 ? 0 : maxInactiveInterval;
      final Future<Boolean> validityResult =
          _memcached.set(validityKey, toMemcachedExpiration(expiration), validityData);
      if (!_manager.isSessionBackupAsync()) {
        validityResult.get(_manager.getSessionBackupTimeout(), TimeUnit.MILLISECONDS);
      }

      /*
       * - ping session
       * - ping session backup
       * - save validity backup
       */
      final Callable<?> backupSessionTask =
          new OnBackupWithoutLoadedSessionTask(
              sessionId, _storeSecondaryBackup, validityKey, validityData, maxInactiveInterval);
      _executor.submit(backupSessionTask);

      if (_log.isDebugEnabled()) {
        _log.debug("Stored session validity info for session " + sessionId);
      }

      _stats.registerSince(NON_STICKY_ON_BACKUP_WITHOUT_LOADED_SESSION, start);

    } catch (final Throwable e) {
      _log.warn("An error when trying to load/update validity info.", e);
    }
  }
 protected void releaseLock(@Nonnull final String sessionId) {
   try {
     if (_log.isDebugEnabled()) {
       _log.debug("Releasing lock for session " + sessionId);
     }
     final long start = System.currentTimeMillis();
     _memcached.delete(_sessionIdFormat.createLockName(sessionId)).get();
     _stats.registerSince(RELEASE_LOCK, start);
   } catch (final Exception e) {
     _log.warn("Caught exception when trying to release lock for session " + sessionId, e);
   }
 }
 protected void acquireLock(
     @Nonnull final String sessionId,
     final long retryInterval,
     final long maxRetryInterval,
     final long timeout,
     final long start)
     throws InterruptedException, ExecutionException, TimeoutException {
   final Future<Boolean> result =
       _memcached.add(_sessionIdFormat.createLockName(sessionId), 5, LOCK_VALUE);
   if (result.get().booleanValue()) {
     if (_log.isDebugEnabled()) {
       _log.debug("Locked session " + sessionId);
     }
     return;
   } else {
     checkTimeoutAndWait(sessionId, retryInterval, timeout, start);
     acquireLock(
         sessionId, min(retryInterval * 2, maxRetryInterval), maxRetryInterval, timeout, start);
   }
 }
  @BeforeMethod
  public void setUp() throws Exception {
    _service = mock(MemcachedSessionService.class);
    _request = mock(Request.class);
    _response = mock(Response.class);

    final Context _contextContainer = mock(Context.class);
    final Host _hostContainer = mock(Host.class);
    final SessionManager _manager = mock(SessionManager.class);

    when(_service.getManager()).thenReturn(_manager);
    when(_manager.getContainer()).thenReturn(_contextContainer);
    when(_contextContainer.getParent()).thenReturn(_hostContainer);
    when(_contextContainer.getPath()).thenReturn("/");

    _sessionTrackerValve = createSessionTrackerValve();
    _nextValve = mock(Valve.class);
    _sessionTrackerValve.setNext(_nextValve);
    _sessionTrackerValve.setContainer(_hostContainer);
    _memcachedNodesManager = mock(MemcachedNodesManager.class);
    _sessionIdFormat = mock(SessionIdFormat.class);

    when(_request.getRequestURI()).thenReturn("/someRequest");
    when(_request.getMethod()).thenReturn("GET");
    when(_request.getQueryString()).thenReturn(null);
    when(_request.getContext()).thenReturn(_contextContainer);

    when(_request.getNote(eq(RequestTrackingHostValve.REQUEST_PROCESSED))).thenReturn(Boolean.TRUE);
    when(_request.getNote(eq(RequestTrackingHostValve.SESSION_ID_CHANGED)))
        .thenReturn(Boolean.FALSE);

    when(_sessionIdFormat.extractMemcachedId(anyString())).thenReturn(PRIMARY_NODE_IDENTIFIER);
    when(_service.getMemcachedNodesManager()).thenReturn(_memcachedNodesManager);
    when(_memcachedNodesManager.isNodeAvailable(PRIMARY_NODE_IDENTIFIER))
        .thenReturn(IS_PRIMARY_MEMCACHED_NODE_OPERATIONAL);
    when(_memcachedNodesManager.getSessionIdFormat()).thenReturn(_sessionIdFormat);
  }
 @CheckForNull
 protected SessionValidityInfo loadBackupSessionValidityInfo(@Nonnull final String sessionId) {
   final String key = _sessionIdFormat.createValidityInfoKeyName(sessionId);
   final String backupKey = _sessionIdFormat.createBackupKey(key);
   return loadSessionValidityInfoForValidityKey(backupKey);
 }
 @CheckForNull
 protected SessionValidityInfo loadSessionValidityInfo(@Nonnull final String sessionId) {
   return loadSessionValidityInfoForValidityKey(
       _sessionIdFormat.createValidityInfoKeyName(sessionId));
 }
  /**
   * Is invoked after the backup of the session is initiated, it's represented by the provided
   * backupResult. The requestId is identifying the request.
   */
  protected void onAfterBackupSession(
      @Nonnull final MemcachedBackupSession session,
      final boolean backupWasForced,
      @Nonnull final Future<BackupResult> result,
      @Nonnull final String requestId,
      @Nonnull final BackupSessionService backupSessionService) {

    if (!_sessionIdFormat.isValid(session.getIdInternal())) {
      return;
    }

    try {

      final long start = System.currentTimeMillis();

      final int maxInactiveInterval = session.getMaxInactiveInterval();
      final byte[] validityData =
          encode(
              maxInactiveInterval,
              session.getLastAccessedTimeInternal(),
              session.getThisAccessedTimeInternal());
      final String validityKey =
          _sessionIdFormat.createValidityInfoKeyName(session.getIdInternal());
      // fix for #88, along with the change in session.getMemcachedExpirationTimeToSet
      final int expiration = maxInactiveInterval <= 0 ? 0 : maxInactiveInterval;
      final Future<Boolean> validityResult =
          _memcached.set(validityKey, toMemcachedExpiration(expiration), validityData);
      if (!_manager.isSessionBackupAsync()) {
        // TODO: together with session backup wait not longer than sessionBackupTimeout.
        // Details: Now/here we're waiting the whole session backup timeout, even if (perhaps) some
        // time
        // was spent before when waiting for session backup result.
        // For sync session backup it would be better to set both the session data and
        // validity info and afterwards wait for both results (but in sum no longer than
        // sessionBackupTimeout)
        validityResult.get(_manager.getSessionBackupTimeout(), TimeUnit.MILLISECONDS);
      }
      if (_log.isDebugEnabled()) {
        _log.debug("Stored session validity info for session " + session.getIdInternal());
      }

      /* The following task are performed outside of the request thread (includes waiting for the backup result):
       * - ping session if the backup was skipped (depends on the backup result)
       * - save secondary session backup if session was modified (backup not skipped)
       * - ping secondary session backup if the backup was skipped
       * - save secondary validity backup
       */
      final boolean pingSessionIfBackupWasSkipped = !backupWasForced;
      final boolean performAsyncTasks = pingSessionIfBackupWasSkipped || _storeSecondaryBackup;

      if (performAsyncTasks) {
        final Callable<?> backupSessionTask =
            new OnAfterBackupSessionTask(
                session,
                result,
                pingSessionIfBackupWasSkipped,
                backupSessionService,
                _storeSecondaryBackup,
                validityKey,
                validityData);
        _executor.submit(backupSessionTask);
      }

      _stats.registerSince(NON_STICKY_AFTER_BACKUP, start);

    } catch (final Throwable e) {
      _log.warn("An error occurred during onAfterBackupSession.", e);
    }
  }