Revision: 7dc62e5869d6
Author: Sam Berlin <[email protected]>
Date: Sun May 27 10:39:27 2012
Log: Add a new transferRequest method to ServletScopes which
propagates all existing scoped objects. Allows servlet engines to detach
& reattach threads (while waiting for a request to receive results from
RPCs).
Revision created by MOE tool push_codebase.
MOE_MIGRATION=4874
http://code.google.com/p/google-guice/source/detail?r=7dc62e5869d6
Added:
/extensions/servlet/test/com/google/inject/servlet/TransferRequestIntegrationTest.java
Modified:
/extensions/servlet/src/com/google/inject/servlet/GuiceFilter.java
/extensions/servlet/src/com/google/inject/servlet/ServletScopes.java
=======================================
--- /dev/null
+++
/extensions/servlet/test/com/google/inject/servlet/TransferRequestIntegrationTest.java
Sun May 27 10:39:27 2012
@@ -0,0 +1,122 @@
+/**
+ * Copyright (C) 2012 Google Inc.
+ *
+ * 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.inject.servlet;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provides;
+
+import junit.framework.TestCase;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+// TODO: Add test for HTTP transferring.
+/**
+ * Tests transferring of entire request scope.
+ */
+
+public class TransferRequestIntegrationTest extends TestCase {
+ private final Callable<Boolean> FALSE_CALLABLE = new Callable<Boolean>()
{
+ @Override public Boolean call() {
+ return false;
+ }
+ };
+
+ public void testTransferHttp_outOfScope() {
+ try {
+ ServletScopes.transferRequest(FALSE_CALLABLE);
+ fail();
+ } catch (OutOfScopeException expected) {}
+ }
+
+ public void testTransferNonHttp_outOfScope() {
+ try {
+ ServletScopes.transferRequest(FALSE_CALLABLE);
+ fail();
+ } catch (OutOfScopeException expected) {}
+ }
+
+ public void testTransferNonHttpRequest() throws Exception {
+ final Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bindScope(RequestScoped.class, ServletScopes.REQUEST);
+ }
+
+ @Provides @RequestScoped Object provideObject() {
+ return new Object();
+ }
+ });
+
+ Callable<Callable<Boolean>> callable = new
Callable<Callable<Boolean>>() {
+ @Override public Callable<Boolean> call() {
+ final Object original = injector.getInstance(Object.class);
+ return ServletScopes.transferRequest(new Callable<Boolean>() {
+ @Override public Boolean call() {
+ return original == injector.getInstance(Object.class);
+ }
+ });
+ }
+ };
+
+ ImmutableMap<Key<?>, Object> seedMap = ImmutableMap.of();
+ Callable<Boolean> transfer = ServletScopes.scopeRequest(callable,
seedMap).call();
+
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ assertTrue(executor.submit(transfer).get());
+ executor.shutdownNow();
+ }
+
+ public void testTransferNonHttpRequest_concurrentUseFails() throws
Exception {
+ Callable<Boolean> callable = new Callable<Boolean>() {
+ @Override public Boolean call() throws Exception {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try {
+ Future<Boolean> future =
executor.submit(ServletScopes.transferRequest(FALSE_CALLABLE));
+ try {
+ return future.get();
+ } catch (ExecutionException e) {
+ return e.getCause() instanceof IllegalStateException;
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+ };
+
+ ImmutableMap<Key<?>, Object> seedMap = ImmutableMap.of();
+ assertTrue(ServletScopes.scopeRequest(callable, seedMap).call());
+ }
+
+ public void testTransferNonHttpRequest_concurrentUseSameThreadOk()
throws Exception {
+ Callable<Boolean> callable = new Callable<Boolean>() {
+ @Override public Boolean call() throws Exception {
+ return ServletScopes.transferRequest(FALSE_CALLABLE).call();
+ }
+ };
+
+ ImmutableMap<Key<?>, Object> seedMap = ImmutableMap.of();
+ assertFalse(ServletScopes.scopeRequest(callable, seedMap).call());
+ }
+}
=======================================
--- /extensions/servlet/src/com/google/inject/servlet/GuiceFilter.java Sun
Oct 16 16:33:52 2011
+++ /extensions/servlet/src/com/google/inject/servlet/GuiceFilter.java Sun
May 27 10:39:27 2012
@@ -16,11 +16,14 @@
package com.google.inject.servlet;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
import com.google.inject.Inject;
import com.google.inject.OutOfScopeException;
import java.io.IOException;
import java.lang.ref.WeakReference;
+import java.util.concurrent.Callable;
import java.util.logging.Logger;
import javax.servlet.Filter;
@@ -109,23 +112,33 @@
localContext.remove();
}
- public void doFilter(ServletRequest servletRequest,
- ServletResponse servletResponse, FilterChain filterChain)
+ public void doFilter(
+ final ServletRequest servletRequest,
+ final ServletResponse servletResponse,
+ final FilterChain filterChain)
throws IOException, ServletException {
- FilterPipeline filterPipeline = getFilterPipeline();
+ final FilterPipeline filterPipeline = getFilterPipeline();
Context previous = GuiceFilter.localContext.get();
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest originalRequest
= (previous != null) ? previous.getOriginalRequest() : request;
- localContext.set(new Context(originalRequest, request, response));
try {
- //dispatch across the servlet pipeline, ensuring web.xml's
filterchain is honored
- filterPipeline.dispatch(servletRequest, servletResponse,
filterChain);
- } finally {
- localContext.set(previous);
+ new Context(originalRequest, request, response).call(new
Callable<Void>() {
+ @Override public Void call() throws Exception {
+ //dispatch across the servlet pipeline, ensuring web.xml's
filterchain is honored
+ filterPipeline.dispatch(servletRequest, servletResponse,
filterChain);
+ return null;
+ }
+ });
+ } catch (IOException e) {
+ throw e;
+ } catch (ServletException e) {
+ throw e;
+ } catch (Exception e) {
+ Throwables.propagate(e);
}
}
@@ -160,6 +173,7 @@
final HttpServletRequest originalRequest;
final HttpServletRequest request;
final HttpServletResponse response;
+ volatile Thread owner;
Context(HttpServletRequest originalRequest, HttpServletRequest request,
HttpServletResponse response) {
@@ -179,6 +193,22 @@
HttpServletResponse getResponse() {
return response;
}
+
+ <T> T call(Callable<T> callable) throws Exception {
+ Thread oldOwner = owner;
+ Thread newOwner = Thread.currentThread();
+ Preconditions.checkState(oldOwner == null || oldOwner == newOwner,
+ "Trying to transfer request scope but original scope is still
active");
+ owner = newOwner;
+ Context previous = localContext.get();
+ localContext.set(this);
+ try {
+ return callable.call();
+ } finally {
+ owner = oldOwner;
+ localContext.set(previous);
+ }
+ }
}
public void init(FilterConfig filterConfig) throws ServletException {
=======================================
--- /extensions/servlet/src/com/google/inject/servlet/ServletScopes.java
Tue Jan 17 08:29:38 2012
+++ /extensions/servlet/src/com/google/inject/servlet/ServletScopes.java
Sun May 27 10:39:27 2012
@@ -26,11 +26,7 @@
import com.google.inject.Provider;
import com.google.inject.Scope;
import com.google.inject.Scopes;
-import com.google.inject.internal.LinkedBindingImpl;
-import com.google.inject.spi.BindingScopingVisitor;
-import com.google.inject.spi.ExposedBinding;
-
-import java.lang.annotation.Annotation;
+
import java.util.Map;
import java.util.concurrent.Callable;
@@ -58,8 +54,8 @@
* scope falls back to this scope map if no http request is available,
and
* requires {@link #scopeRequest} to be called as an alertnative.
*/
- private static final ThreadLocal<Map<String, Object>> requestScopeContext
- = new ThreadLocal<Map<String, Object>>();
+ private static final ThreadLocal<Context> requestScopeContext
+ = new ThreadLocal<Context>();
/** A sentinel attribute value representing null. */
enum NullObject { INSTANCE }
@@ -79,10 +75,10 @@
// NOTE(dhanji): We don't need to synchronize on the scope map
// unlike the HTTP request because we're the only ones who have
// a reference to it, and it is only available via a
threadlocal.
- Map<String, Object> scopeMap = requestScopeContext.get();
- if (null != scopeMap) {
+ Context context = requestScopeContext.get();
+ if (null != context) {
@SuppressWarnings("unchecked")
- T t = (T) scopeMap.get(name);
+ T t = (T) context.map.get(name);
// Accounts for @Nullable providers.
if (NullObject.INSTANCE == t) {
@@ -93,7 +89,7 @@
t = creator.get();
if (!Scopes.isCircularProxy(t)) {
// Store a sentinel for provider-given null values.
- scopeMap.put(name, t != null ? t : NullObject.INSTANCE);
+ context.map.put(name, t != null ? t :
NullObject.INSTANCE);
}
}
@@ -214,7 +210,7 @@
final Map<Key<?>, Object> seedMap) {
Preconditions.checkArgument(null != seedMap,
"Seed map cannot be null, try passing in Collections.emptyMap()
instead.");
-
+
// Snapshot the seed map and add all the instances to our continuing
HTTP request.
final ContinuingHttpServletRequest continuingRequest =
new ContinuingHttpServletRequest(GuiceFilter.getRequest());
@@ -224,27 +220,68 @@
}
return new Callable<T>() {
- private final HttpServletRequest request = continuingRequest;
-
public T call() throws Exception {
- GuiceFilter.Context context = GuiceFilter.localContext.get();
- Preconditions.checkState(null == context,
+ Preconditions.checkState(null == GuiceFilter.localContext.get(),
"Cannot continue request in the same thread as a HTTP
request!");
-
- // Only set up the request continuation if we're running in a
- // new vanilla thread.
- GuiceFilter.localContext.set(new GuiceFilter.Context(request,
request, null));
- try {
- return callable.call();
- } finally {
- // Clear the copied context if we set one up.
- if (null == context) {
- GuiceFilter.localContext.remove();
- }
- }
+ return new GuiceFilter.Context(continuingRequest,
continuingRequest, null)
+ .call(callable);
+ }
+ };
+ }
+
+ /**
+ * Wraps the given callable in a contextual callable that "transfers" the
+ * request to another thread. This acts as a way of transporting
+ * request context data from the current thread to a future thread.
+ *
+ * <p>As opposed to {@link #continueRequest}, this method propagates all
+ * existing scoped objects. The primary use case is in server
implementations
+ * where you can detach the request processing thread while waiting for
data,
+ * and reattach to a different thread to finish processing at a later
time.
+ *
+ * <p>Because {@code HttpServletRequest} objects are not typically
+ * thread-safe, the callable returned by this method must not be run on a
+ * different thread until the current request scope has terminated. In
other
+ * words, do not use this method to propagate the current request scope
to
+ * worker threads that may run concurrently with the current thread.
+ *
+ *
+ * @param callable code to be executed in another thread, which depends
on
+ * the request scope.
+ * @return a callable that will invoke the given callable, making the
request
+ * context available to it.
+ * @throws OutOfScopeException if this method is called from a
non-request
+ * thread, or if the request has completed.
+ */
+ public static <T> Callable<T> transferRequest(Callable<T> callable) {
+ return (GuiceFilter.localContext.get() != null)
+ ? transferHttpRequest(callable)
+ : transferNonHttpRequest(callable);
+ }
+
+ private static <T> Callable<T> transferHttpRequest(final Callable<T>
callable) {
+ final GuiceFilter.Context context = GuiceFilter.localContext.get();
+ if (context == null) {
+ throw new OutOfScopeException("Not in a request scope");
+ }
+ return new Callable<T>() {
+ public T call() throws Exception {
+ return context.call(callable);
}
};
}
+
+ private static <T> Callable<T> transferNonHttpRequest(final Callable<T>
callable) {
+ final Context context = requestScopeContext.get();
+ if (context == null) {
+ throw new OutOfScopeException("Not in a request scope");
+ }
+ return new Callable<T>() {
+ public T call() throws Exception {
+ return context.call(callable);
+ }
+ };
+ }
/**
* Returns true if {@code binding} is request-scoped. If the binding is a
@@ -279,10 +316,10 @@
"Seed map cannot be null, try passing in Collections.emptyMap()
instead.");
// Copy the seed values into our local scope map.
- final Map<String, Object> scopeMap = Maps.newHashMap();
+ final Context context = new Context();
for (Map.Entry<Key<?>, Object> entry : seedMap.entrySet()) {
Object value = validateAndCanonicalizeValue(entry.getKey(),
entry.getValue());
- scopeMap.put(entry.getKey().toString(), value);
+ context.map.put(entry.getKey().toString(), value);
}
return new Callable<T>() {
@@ -291,14 +328,7 @@
"An HTTP request is already in progress, cannot scope a new
request in this thread.");
Preconditions.checkState(null == requestScopeContext.get(),
"A request scope is already in progress, cannot scope a new
request in this thread.");
-
- requestScopeContext.set(scopeMap);
-
- try {
- return callable.call();
- } finally {
- requestScopeContext.remove();
- }
+ return context.call(callable);
}
};
}
@@ -319,4 +349,25 @@
return object;
}
-}
+
+ private static class Context {
+ final Map<String, Object> map = Maps.newHashMap();
+ volatile Thread owner;
+
+ <T> T call(Callable<T> callable) throws Exception {
+ Thread oldOwner = owner;
+ Thread newOwner = Thread.currentThread();
+ Preconditions.checkState(oldOwner == null || oldOwner == newOwner,
+ "Trying to transfer request scope but original scope is still
active");
+ owner = newOwner;
+ Context previous = requestScopeContext.get();
+ requestScopeContext.set(this);
+ try {
+ return callable.call();
+ } finally {
+ owner = oldOwner;
+ requestScopeContext.set(previous);
+ }
+ }
+ }
+}
--
You received this message because you are subscribed to the Google Groups
"google-guice-dev" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to
[email protected].
For more options, visit this group at
http://groups.google.com/group/google-guice-dev?hl=en.