/**
* Copyright 2011 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.apphosting.runtime;
import static com.google.appengine.api.taskqueue.RetryOptions.Builder.withTaskAgeLimitSeconds;
import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withPayload;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.taskqueue.DeferredTask;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TransientFailureException;
import java.lang.reflect.Constructor;
/**
* A {@link DatastoreSessionStore} extension that defers all datastore writes
* via the taskqueue.
*
*/
public class DeferredDatastoreSessionStore extends DatastoreSessionStore {
/**
* Try to save the session state for 10 seconds, then give up.
*/
private static final int SAVE_TASK_AGE_LIMIT_SECS = 10;
// The DeferredTask implementations we use to put and delete session data in
// the datastore are are general-purpose, but we're not ready to expose them
// in the public api, so we access them via reflection.
private static final Constructor<DeferredTask> putDeferredTaskConstructor;
private static final Constructor<DeferredTask> deleteDeferredTaskConstructor;
static {
putDeferredTaskConstructor =
getConstructor(
DeferredTask.class.getPackage().getName() + ".DatastorePutDeferredTask", Entity.class);
deleteDeferredTaskConstructor =
getConstructor(
DeferredTask.class.getPackage().getName() + ".DatastoreDeleteDeferredTask", Key.class);
}
private final Queue queue;
public DeferredDatastoreSessionStore(String queueName) {
this.queue =
queueName == null ? QueueFactory.getDefaultQueue() : QueueFactory.getQueue(queueName);
}
@Override
public void saveSession(String key, SessionData data) throws Retryable {
try {
// Setting a timeout on retries to reduce the likelihood that session
// state "reverts." This can happen if a session in state s1 is saved
// but the write fails. Then the session in state s2 is saved and the
// write succeeds. Then a retry of the save of the session in s1
// succeeds. We could use version numbers in the session to detect this
// scenario, but it doesn't seem worth it.
// The length of this timeout has been chosen arbitrarily. Maybe let
// users set it?
Entity e = DatastoreSessionStore.createEntityForSession(key, data);
queue.add(
withPayload(newDeferredTask(putDeferredTaskConstructor, e))
.retryOptions(withTaskAgeLimitSeconds(SAVE_TASK_AGE_LIMIT_SECS)));
} catch (TransientFailureException e) {
throw new Retryable(e);
}
}
@Override
public void deleteSession(String keyStr) {
Key key = DatastoreSessionStore.createKeyForSession(keyStr);
// We'll let this task retry indefinitely.
queue.add(withPayload(newDeferredTask(deleteDeferredTaskConstructor, key)));
}
/**
* Helper method that returns a 1-arg constructor taking an arg of the given
* type for the given class name
*/
private static Constructor<DeferredTask> getConstructor(String clsName, Class<?> argType) {
try {
@SuppressWarnings("unchecked")
Class<DeferredTask> cls = (Class<DeferredTask>) Class.forName(clsName);
Constructor<DeferredTask> ctor = cls.getConstructor(argType);
ctor.setAccessible(true);
return ctor;
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
private static DeferredTask newDeferredTask(Constructor<DeferredTask> ctor, Object arg) {
try {
return ctor.newInstance(arg);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}