private void pingSessionBackup(@Nonnull final MemcachedBackupSession session)
     throws InterruptedException {
   final String key = _sessionIdFormat.createBackupKey(session.getId());
   final Future<Boolean> touchResultFuture = _memcached.add(key, 5, 1);
   try {
     final boolean touchResult =
         touchResultFuture.get(_manager.getOperationTimeout(), TimeUnit.MILLISECONDS);
     if (touchResult) {
       _log.warn(
           "The secondary backup for session "
               + session.getIdInternal()
               + " should be touched in memcached, but it seemed to be"
               + " not existing. Will store in memcached again.");
       saveSessionBackup(session, key);
     } else _log.debug("The secondary session backup was ping'ed successfully.");
   } catch (final TimeoutException e) {
     _log.warn(
         "The secondary backup for session "
             + session.getIdInternal()
             + " could not be completed within "
             + _manager.getOperationTimeout()
             + " millis, was cancelled now.");
   } catch (final ExecutionException e) {
     _log.warn(
         "An exception occurred when trying to ping session " + session.getIdInternal(), e);
   }
 }
    @Override
    public Void call() throws Exception {

      final BackupResult backupResult = _result.get();

      if (_pingSessionIfBackupWasSkipped) {
        if (backupResult.getStatus() == BackupResultStatus.SKIPPED) {
          pingSession(_session, _backupSessionService);
        }
      }

      /*
       * For non-sticky sessions we store a backup of the session in a secondary memcached node (under a special key
       * that's resolved by the SuffixBasedNodeLocator), but only when we have more than 1 memcached node configured...
       */
      if (_storeSecondaryBackup) {
        try {
          if (_log.isDebugEnabled()) {
            _log.debug(
                "Storing backup in secondary memcached for non-sticky session " + _session.getId());
          }
          if (backupResult.getStatus() == BackupResultStatus.SKIPPED) {
            pingSessionBackup(_session);
          } else {
            saveSessionBackupFromResult(backupResult);
          }

          saveValidityBackup();
        } catch (final RuntimeException e) {
          _log.info("Could not store secondary backup of session " + _session.getIdInternal(), e);
        }
      }

      return null;
    }
  /**
   * Invoked after a non-sticky session is loaded from memcached, can be used to update some session
   * fields based on separately stored information (e.g. session validity info).
   *
   * @param lockStatus the {@link LockStatus} that was returned from {@link
   *     #onBeforeLoadFromMemcached(String)}.
   */
  protected void onAfterLoadFromMemcached(
      @Nonnull final MemcachedBackupSession session, @Nullable final LockStatus lockStatus) {
    session.setLockStatus(lockStatus);

    final long start = System.currentTimeMillis();
    final SessionValidityInfo info = loadSessionValidityInfo(session.getIdInternal());
    if (info != null) {
      _stats.registerSince(NON_STICKY_AFTER_LOAD_FROM_MEMCACHED, start);
      session.setLastAccessedTimeInternal(info.getLastAccessedTime());
      session.setThisAccessedTimeInternal(info.getThisAccessedTime());
    } else {
      _log.warn("No validity info available for session " + session.getIdInternal());
    }
  }
 public void saveValidityBackup() {
   final String backupValidityKey = _sessionIdFormat.createBackupKey(_validityKey);
   final int maxInactiveInterval = _session.getMaxInactiveInterval();
   // fix for #88, along with the change in session.getMemcachedExpirationTimeToSet
   final int expiration = maxInactiveInterval <= 0 ? 0 : maxInactiveInterval;
   _memcached.set(backupValidityKey, toMemcachedExpiration(expiration), _validityData);
 }
 public void saveSessionBackupFromResult(final BackupResult backupResult) {
   final byte[] data = backupResult.getData();
   if (data != null) {
     final String key = _sessionIdFormat.createBackupKey(_session.getId());
     _memcached.set(
         key, toMemcachedExpiration(_session.getMemcachedExpirationTimeToSet()), data);
   } else {
     _log.warn(
         "No data set for backupResultStatus "
             + backupResult.getStatus()
             + " for sessionId "
             + _session.getIdInternal()
             + ", skipping backup"
             + " of non-sticky session in secondary memcached.");
   }
 }
 private void pingSession(
     @Nonnull final MemcachedBackupSession session,
     @Nonnull final BackupSessionService backupSessionService)
     throws InterruptedException {
   final Future<Boolean> touchResult =
       _memcached.add(_storageKeyFormat.format(session.getIdInternal()), 5, 1);
   try {
     if (touchResult.get()) {
       _stats.nonStickySessionsPingFailed();
       _log.warn(
           "The session "
               + session.getIdInternal()
               + " should be touched in memcached, but it does not exist"
               + " therein. Will store in memcached again.");
       updateSession(session, backupSessionService);
     } else _log.debug("The session was ping'ed successfully.");
   } catch (final ExecutionException e) {
     _log.warn("An exception occurred when trying to ping session " + session.getIdInternal(), e);
   }
 }
 public void saveSessionBackup(
     @Nonnull final MemcachedBackupSession session, @Nonnull final String key)
     throws InterruptedException {
   try {
     final byte[] data = _manager.serialize(session);
     final Future<Boolean> backupResult =
         _memcached.set(
             key, toMemcachedExpiration(session.getMemcachedExpirationTimeToSet()), data);
     if (!backupResult.get().booleanValue()) {
       _log.warn(
           "Update for secondary backup of session "
               + session.getIdInternal()
               + " (after unsuccessful ping) did not return sucess.");
     }
   } catch (final ExecutionException e) {
     _log.warn(
         "An exception occurred when trying to update secondary session backup for "
             + session.getIdInternal(),
         e);
   }
 }
 /**
  * Deserialize session data that was serialized using {@link #serialize(MemcachedBackupSession)}
  * (or a combination of {@link #serializeAttributes(MemcachedBackupSession, Map)} and {@link
  * #serialize(MemcachedBackupSession, byte[])}).
  *
  * <p>Note: the returned session already has the manager set and {@link
  * MemcachedBackupSession#doAfterDeserialization()} is invoked. Additionally the attributes hash
  * is set (via {@link MemcachedBackupSession#setDataHashCode(int)}).
  *
  * @param data the byte array of the serialized session and its session attributes. Can be <code>
  *     null</code>.
  * @param realm the realm that is used to reconstruct the principal if there was any stored in the
  *     session.
  * @param manager the manager to set on the deserialized session.
  * @return the deserialized {@link MemcachedBackupSession} or <code>null</code> if the provided
  *     <code>byte[] data</code> was <code>null</code>.
  */
 public MemcachedBackupSession deserialize(final byte[] data, final SessionManager manager) {
   if (data == null) {
     return null;
   }
   try {
     final DeserializationResult deserializationResult = deserializeSessionFields(data, manager);
     final byte[] attributesData = deserializationResult.getAttributesData();
     final Map<String, Object> attributes = deserializeAttributes(attributesData);
     final MemcachedBackupSession session = deserializationResult.getSession();
     session.setAttributesInternal(attributes);
     session.setDataHashCode(Arrays.hashCode(attributesData));
     session.setManager(manager);
     session.doAfterDeserialization();
     return session;
   } catch (final InvalidVersionException e) {
     LOG.info("Got session data from memcached with an unsupported version: " + e.getVersion());
     // for versioning probably there will be changes in the design,
     // with the first change and version 2 we'll see what we need
     return null;
   }
 }
 private void updateSession(
     @Nonnull final MemcachedBackupSession session,
     @Nonnull final BackupSessionService backupSessionService)
     throws InterruptedException {
   final Future<BackupResult> result = backupSessionService.backupSession(session, true);
   try {
     if (result.get().getStatus() != BackupResultStatus.SUCCESS) {
       _log.warn(
           "Update for session (after unsuccessful ping) did not return SUCCESS, but "
               + result.get());
     }
   } catch (final ExecutionException e) {
     _log.warn(
         "An exception occurred when trying to update session " + session.getIdInternal(), e);
   }
 }
  /**
   * 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);
    }
  }
 /**
  * Serialize the given session to a byte array. This is a shortcut for <code><pre>
  * final byte[] attributesData = serializeAttributes( session, session.getAttributes() );
  * serialize( session, attributesData );
  * </pre></code> The returned byte array can be deserialized using {@link #deserialize(byte[],
  * Realm, Manager)}.
  *
  * @see #serializeAttributes(MemcachedBackupSession, Map)
  * @see #serialize(MemcachedBackupSession, byte[])
  * @see #deserialize(byte[], Realm, Manager)
  * @param session the session to serialize.
  * @return the serialized session data.
  */
 public byte[] serialize(final MemcachedBackupSession session) {
   final byte[] attributesData = serializeAttributes(session, session.getAttributesInternal());
   return serialize(session, attributesData);
 }
  static DeserializationResult deserializeSessionFields(
      final byte[] data, final SessionManager manager) throws InvalidVersionException {
    final MemcachedBackupSession result = manager.newMemcachedBackupSession();

    final short version = (short) decodeNum(data, 0, 2);

    if (version != CURRENT_VERSION) {
      throw new InvalidVersionException(
          "The version " + version + " does not match the current version " + CURRENT_VERSION,
          version);
    }

    final short sessionFieldsDataLength = (short) decodeNum(data, 2, 2);

    result.setCreationTimeInternal(decodeNum(data, 4, 8));
    result.setLastAccessedTimeInternal(decodeNum(data, 12, 8));
    result.setMaxInactiveInterval((int) decodeNum(data, 20, 4));
    result.setIsNewInternal(decodeBoolean(data, 24));
    result.setIsValidInternal(decodeBoolean(data, 25));
    result.setThisAccessedTimeInternal(decodeNum(data, 26, 8));
    result.setLastBackupTime(decodeNum(data, 34, 8));

    final short idLength = (short) decodeNum(data, 42, 2);
    result.setIdInternal(decodeString(data, 44, idLength));

    final short authTypeId = (short) decodeNum(data, 44 + idLength, 2);
    result.setAuthTypeInternal(AuthType.valueOfId(authTypeId).getValue());

    final int currentIdx = 44 + idLength + 2;
    final short principalDataLength = (short) decodeNum(data, currentIdx, 2);
    if (principalDataLength > 0) {
      final byte[] principalData = new byte[principalDataLength];
      System.arraycopy(data, currentIdx + 2, principalData, 0, principalDataLength);
      result.setPrincipalInternal(deserializePrincipal(principalData, manager));
    }

    final byte[] attributesData = new byte[data.length - sessionFieldsDataLength];
    System.arraycopy(
        data, sessionFieldsDataLength, attributesData, 0, data.length - sessionFieldsDataLength);

    return new DeserializationResult(result, attributesData);
  }
  static byte[] serializeSessionFields(final MemcachedBackupSession session) {

    final byte[] idData = serializeId(session.getIdInternal());

    final byte[] principalData =
        session.getPrincipal() != null ? serializePrincipal(session.getPrincipal()) : null;
    final int principalDataLength = principalData != null ? principalData.length : 0;

    final int sessionFieldsDataLength =
        2 // short value for the version
            // the following might change with other versions, refactoring needed then
            + 2 // short value that stores the dataLength
            + NUM_BYTES // bytes that store all session attributes but the id
            + 2 // short value that stores the idData length
            + idData.length // the number of bytes for the id
            + 2 // short value for the authType
            + 2 // short value that stores the principalData length
            + principalDataLength; // the number of bytes for the principal
    final byte[] data = new byte[sessionFieldsDataLength];

    int idx = 0;
    idx = encodeNum(CURRENT_VERSION, data, idx, 2);
    idx = encodeNum(sessionFieldsDataLength, data, idx, 2);
    idx = encodeNum(session.getCreationTimeInternal(), data, idx, 8);
    idx = encodeNum(session.getLastAccessedTimeInternal(), data, idx, 8);
    idx = encodeNum(session.getMaxInactiveInterval(), data, idx, 4);
    idx = encodeBoolean(session.isNewInternal(), data, idx);
    idx = encodeBoolean(session.isValidInternal(), data, idx);
    idx = encodeNum(session.getThisAccessedTimeInternal(), data, idx, 8);
    idx = encodeNum(session.getLastBackupTime(), data, idx, 8);
    idx = encodeNum(idData.length, data, idx, 2);
    idx = copy(idData, data, idx);
    idx = encodeNum(AuthType.valueOfValue(session.getAuthType()).getId(), data, idx, 2);
    idx = encodeNum(principalDataLength, data, idx, 2);
    copy(principalData, data, idx);

    return data;
  }