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.

Reply via email to