Google App Engine 错误:获取的线程既不是原始请求线程也不是 ThreadManager 创建的线程

Posted

技术标签:

【中文标题】Google App Engine 错误:获取的线程既不是原始请求线程也不是 ThreadManager 创建的线程【英文标题】:Google App Engine Error: Fetch in a thread that is neither the original request thread nor a thread created by ThreadManager 【发布时间】:2017-12-03 13:29:51 【问题描述】:

App Engine 给出错误:

com.google.apphosting.api.ApiProxy$CallNotFoundException: Can't make API call urlfetch.Fetch in a thread that is neither the original request thread nor a thread created by ThreadManager

当我在异步 Servlet 中的 Callable 中调用 Google Vision API 时。 如何让它发挥作用?

小服务程序:

public class OcrForTextServlet extends HttpServlet 
    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse response) 
            byte[] file = extractFile(req);
            String[] languages = req.getParameterValues("language");
            ExecutorService executor = Executors.newFixedThreadPool(2);
            Future<String> result = executor.submit(new OcrCallableTask(file, languages));
            executor.shutdown();
            response.getWriter().write(result.get()); //ERROR HERE

完整的堆栈跟踪:

[INFO] java.util.concurrent.ExecutionException: com.google.apphosting.api.ApiProxy$CallNotFoundException: Can't make API call urlfetch.Fetch in a thread that is neither the original request thread nor a thread created by ThreadManager
[INFO]  at java.util.concurrent.FutureTask.report(FutureTask.java:122)
[INFO]  at java.util.concurrent.FutureTask.get(FutureTask.java:192)
[INFO]  at ocrme_backend.servlets.ocr.OcrForTextServlet.doPost(OcrForTextServlet.java:49)
[INFO]  at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
[INFO]  at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
[INFO]  at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:848)
[INFO]  at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1772)
[INFO]  at com.google.appengine.api.socket.dev.DevSocketFilter.doFilter(DevSocketFilter.java:74)
[INFO]  at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1759)
[INFO]  at com.google.appengine.tools.development.ResponseRewriterFilter.doFilter(ResponseRewriterFilter.java:134)
[INFO]  at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1759)
[INFO]  at com.google.appengine.tools.development.HeaderVerificationFilter.doFilter(HeaderVerificationFilter.java:34)
[INFO]  at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1759)
[INFO]  at com.google.appengine.api.blobstore.dev.ServeBlobFilter.doFilter(ServeBlobFilter.java:63)
[INFO]  at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1759)
[INFO]  at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:48)
[INFO]  at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1759)
[INFO]  at com.google.appengine.tools.development.jetty9.StaticFileFilter.doFilter(StaticFileFilter.java:122)
[INFO]  at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1759)
[INFO]  at com.google.appengine.tools.development.DevAppServerModulesFilter.doDirectRequest(DevAppServerModulesFilter.java:366)
[INFO]  at com.google.appengine.tools.development.DevAppServerModulesFilter.doDirectModuleRequest(DevAppServerModulesFilter.java:349)
[INFO]  at com.google.appengine.tools.development.DevAppServerModulesFilter.doFilter(DevAppServerModulesFilter.java:116)
[INFO]  at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1751)
[INFO]  at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:582)
[INFO]  at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
[INFO]  at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:524)
[INFO]  at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:226)
[INFO]  at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1180)
[INFO]  at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:512)
[INFO]  at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
[INFO]  at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1112)
[INFO]  at com.google.appengine.tools.development.jetty9.DevAppEngineWebAppContext.doScope(DevAppEngineWebAppContext.java:112)
[INFO]  at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
[INFO]  at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:134)
[INFO]  at com.google.appengine.tools.development.jetty9.JettyContainerService$ApiProxyHandler.handle(JettyContainerService.java:596)
[INFO]  at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:134)
[INFO]  at org.eclipse.jetty.server.Server.handle(Server.java:534)
[INFO]  at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:320)
[INFO]  at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:251)
[INFO]  at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:283)
[INFO]  at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:108)
[INFO]  at org.eclipse.jetty.io.SelectChannelEndPoint$2.run(SelectChannelEndPoint.java:93)
[INFO]  at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.executeProduceConsume(ExecuteProduceConsume.java:303)
[INFO]  at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.produceConsume(ExecuteProduceConsume.java:148)
[INFO]  at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.run(ExecuteProduceConsume.java:136)
[INFO]  at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:671)
[INFO]  at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:589)
[INFO]  at java.lang.Thread.run(Thread.java:745)
[INFO] Caused by: com.google.apphosting.api.ApiProxy$CallNotFoundException: Can't make API call urlfetch.Fetch in a thread that is neither the original request thread nor a thread created by ThreadManager
[INFO]  at com.google.apphosting.api.ApiProxy$CallNotFoundException.foreignThread(ApiProxy.java:844)
[INFO]  at com.google.apphosting.api.ApiProxy.makeSyncCall(ApiProxy.java:116)
[INFO]  at com.google.appengine.api.urlfetch.URLFetchServiceImpl.fetch(URLFetchServiceImpl.java:40)
[INFO]  at com.google.apphosting.utils.security.urlfetch.URLFetchServiceStreamHandler$Connection.fetchResponse(URLFetchServiceStreamHandler.java:543)
[INFO]  at com.google.apphosting.utils.security.urlfetch.URLFetchServiceStreamHandler$Connection.getInputStream(URLFetchServiceStreamHandler.java:422)
[INFO]  at com.google.apphosting.utils.security.urlfetch.URLFetchServiceStreamHandler$Connection.getResponseCode(URLFetchServiceStreamHandler.java:275)
[INFO]  at com.google.api.client.http.javanet.NetHttpResponse.<init>(NetHttpResponse.java:37)
[INFO]  at com.google.api.client.http.javanet.NetHttpRequest.execute(NetHttpRequest.java:94)
[INFO]  at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:972)
[INFO]  at com.google.api.client.auth.oauth2.TokenRequest.executeUnparsed(TokenRequest.java:283)
[INFO]  at com.google.api.client.auth.oauth2.TokenRequest.execute(TokenRequest.java:307)
[INFO]  at com.google.api.client.auth.oauth2.Credential.executeRefreshToken(Credential.java:570)
[INFO]  at com.google.api.client.googleapis.auth.oauth2.GoogleCredential.executeRefreshToken(GoogleCredential.java:362)
[INFO]  at com.google.api.client.auth.oauth2.Credential.refreshToken(Credential.java:489)
[INFO]  at com.google.api.client.googleapis.auth.oauth2.GoogleCredential.fromStreamUser(GoogleCredential.java:772)
[INFO]  at com.google.api.client.googleapis.auth.oauth2.GoogleCredential.fromStream(GoogleCredential.java:257)
[INFO]  at com.google.api.client.googleapis.auth.oauth2.DefaultCredentialProvider.getCredentialUsingWellKnownFile(DefaultCredentialProvider.java:249)
[INFO]  at com.google.api.client.googleapis.auth.oauth2.DefaultCredentialProvider.getDefaultCredentialUnsynchronized(DefaultCredentialProvider.java:117)
[INFO]  at com.google.api.client.googleapis.auth.oauth2.DefaultCredentialProvider.getDefaultCredential(DefaultCredentialProvider.java:91)
[INFO]  at com.google.api.client.googleapis.auth.oauth2.GoogleCredential.getApplicationDefault(GoogleCredential.java:213)
[INFO]  at com.google.api.client.googleapis.auth.oauth2.GoogleCredential.getApplicationDefault(GoogleCredential.java:191)
[INFO]  at ocrme_backend.ocr.OCRProcessorImpl.getVisionService(OCRProcessorImpl.java:40)
[INFO]  at ocrme_backend.ocr.OCRProcessorImpl.<init>(OCRProcessorImpl.java:32)
[INFO]  at ocrme_backend.servlets.ocr.OcrCallableTask.doStaff(OcrCallableTask.java:27)
[INFO]  at ocrme_backend.servlets.ocr.OcrCallableTask.call(OcrCallableTask.java:39)
[INFO]  at ocrme_backend.servlets.ocr.OcrCallableTask.call(OcrCallableTask.java:14)
[INFO]  at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[INFO]  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
[INFO]  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
[INFO]  ... 1 more

API 调用出错:

/**
 * Connects to the Vision API using Application Default Credentials.
 */
public static Vision getVisionService() throws IOException, GeneralSecurityException 
    GoogleCredential credential =
            GoogleCredential.getApplicationDefault().createScoped(VisionScopes.all());
    com.google.api.client.json.JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
    return new Vision.Builder(GoogleNetHttpTransport.newTrustedTransport(), jsonFactory, credential)
            .setApplicationName(APPLICATION_NAME)
            .build();

我正在使用最新版本的 javax.servlet-api (3.1.0)、GAE (1.9.52) 和 Java 8。我需要从异步部分获取结果。 我怎样才能做到这一点? 感谢您的帮助。

更新: 我尝试use com.google.appengine.api.ThreadManager,如错误消息中所述,但它给出了相同的错误。这是我更新的 servlet:

@Override
public void doPost(HttpServletRequest req, HttpServletResponse response) 

    try 
        byte[] file = extractFile(req);
        String[] languages = req.getParameterValues("language");

        ThreadFactory factory = ThreadManager.currentRequestThreadFactory();
        ExecutorService service = Executors.newCachedThreadPool(factory);
        Future<String> result =
                service.submit(new OcrCallableTask(file, languages));
        response.getWriter().write(result.get()); //ERROR HERE

下一个测试顺利通过:

public class OcrCallableTaskTest 
    @Test
    public void testCall() throws Exception 
        ExecutorService service = Executors.newFixedThreadPool(2);
        Future<String> result = service.submit(new OcrCallableTask(FileProvider.getFile(), null));
        Assert.assertTrue(result.get() != null);
        Assert.assertTrue(result.get().length() > 0);
    

更新 2: (在请求的线程中回复提议的工作人员。) 真的,我的 servlet 不需要额外的线程。这只是尝试修复错误。 如果我在我的应用程序中不使用多线程,我也会遇到同样的错误:

[INFO] com.google.api.gax.grpc.ApiException: io.grpc.StatusRuntimeException: UNAUTHENTICATED
[INFO]  at com.google.api.gax.grpc.ExceptionTransformingCallable$ExceptionTransformingFuture.onFailure(ExceptionTransformingCallable.java:108)
[INFO]  at com.google.api.core.ApiFutures$1.onFailure(ApiFutures.java:52)
[INFO]  at com.google.common.util.concurrent.Futures$6.run(Futures.java:1310)
[INFO]  at com.google.common.util.concurrent.MoreExecutors$DirectExecutor.execute(MoreExecutors.java:457)
[INFO]  at com.google.common.util.concurrent.ExecutionList.executeListener(ExecutionList.java:156)
[INFO]  at com.google.common.util.concurrent.ExecutionList.execute(ExecutionList.java:145)
[INFO]  at com.google.common.util.concurrent.AbstractFuture.setException(AbstractFuture.java:202)
[INFO]  at io.grpc.stub.ClientCalls$GrpcFuture.setException(ClientCalls.java:463)
[INFO]  at io.grpc.stub.ClientCalls$UnaryStreamToFuture.onClose(ClientCalls.java:439)
[INFO]  at io.grpc.internal.ClientCallImpl.closeObserver(ClientCallImpl.java:428)
[INFO]  at io.grpc.internal.ClientCallImpl.access$100(ClientCallImpl.java:76)
[INFO]  at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl.close(ClientCallImpl.java:514)
[INFO]  at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl.access$700(ClientCallImpl.java:431)
[INFO]  at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInContext(ClientCallImpl.java:546)
[INFO]  at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:52)
[INFO]  at io.grpc.internal.SerializingExecutor$TaskRunner.run(SerializingExecutor.java:152)
[INFO]  at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
[INFO]  at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[INFO]  at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
[INFO]  at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
[INFO]  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
[INFO]  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
[INFO]  at java.lang.Thread.run(Thread.java:745)
[INFO] Caused by: io.grpc.StatusRuntimeException: UNAUTHENTICATED
[INFO]  at io.grpc.Status.asRuntimeException(Status.java:540)
[INFO]  ... 15 more
[INFO] Caused by: com.google.apphosting.api.ApiProxy$CallNotFoundException: Can't make API call urlfetch.Fetch in a thread that is neither the original request thread nor a thread created by ThreadManager
[INFO]  at com.google.apphosting.api.ApiProxy$CallNotFoundException.foreignThread(ApiProxy.java:844)
[INFO]  at com.google.apphosting.api.ApiProxy.makeSyncCall(ApiProxy.java:116)
[INFO]  at com.google.appengine.api.urlfetch.URLFetchServiceImpl.fetch(URLFetchServiceImpl.java:40)
[INFO]  at com.google.apphosting.utils.security.urlfetch.URLFetchServiceStreamHandler$Connection.fetchResponse(URLFetchServiceStreamHandler.java:543)
[INFO]  at com.google.apphosting.utils.security.urlfetch.URLFetchServiceStreamHandler$Connection.getInputStream(URLFetchServiceStreamHandler.java:422)
[INFO]  at com.google.apphosting.utils.security.urlfetch.URLFetchServiceStreamHandler$Connection.getResponseCode(URLFetchServiceStreamHandler.java:275)
[INFO]  at com.google.api.client.http.javanet.NetHttpResponse.<init>(NetHttpResponse.java:37)
[INFO]  at com.google.api.client.http.javanet.NetHttpRequest.execute(NetHttpRequest.java:94)
[INFO]  at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:972)
[INFO]  at com.google.auth.oauth2.UserCredentials.refreshAccessToken(UserCredentials.java:207)
[INFO]  at com.google.auth.oauth2.OAuth2Credentials.refresh(OAuth2Credentials.java:149)
[INFO]  at com.google.auth.oauth2.OAuth2Credentials.getRequestMetadata(OAuth2Credentials.java:135)
[INFO]  at io.grpc.auth.GoogleAuthLibraryCallCredentials$1.run(GoogleAuthLibraryCallCredentials.java:110)
[INFO]  ... 7 more

servlet 代码 - 没有多线程:

 @Override
    public void doPost(HttpServletRequest req, HttpServletResponse response) 
        try 
            byte[] file = extractFile(req);
            String[] languages = req.getParameterValues("language");

            OCRProcessor processor = new OCRProcessorImpl();
            String jsonResult;

            if (languages == null || languages.length <= 0)  
                jsonResult = processor.ocrForText(file);

下一个测试通过了:

@Test
public void doOCR() throws Exception 
    byte[] file = FileProvider.getImageFile().getFile();
    String result = ocrProcessor.ocrForText(file);
    assertNotNull(result);
    assertTrue(result.length() > 0);

它接缝(GAE + API 调用)与 Servlet 架构不兼容。感谢您的任何建议。

【问题讨论】:

您正在运行哪种类型的实例?手动、基本还是自动? 另外,你能分享一下 OcrCallableTask 的代码吗? 你不应该在GAE中使用你自己的线程框架,你可以使用相同的任务队列框架。当你有一个请求添加到队列中并告诉 GAE 以后如何处理它。 cloud.google.com/appengine/docs/standard/java/taskqueue @Michael Meyer,我不明白你的问题。我不知道。 @Michael 询问缩放类型和实例类:cloud.google.com/appengine/docs/standard/python/… 【参考方案1】:

异步?

也许我误解了你,但你的 servlet 似乎不是异步的:

    Future<String> result =
            service.submit(new OcrCallableTask(file, languages));
    response.getWriter().write(result.get()); //ERROR HERE

您的 servlet 线程将阻塞,直到此 url 获取完成,因为 Future#get() 将等到任务完成。

而且,正如您的堆栈跟踪所示,ApiProxy 还调用了 snyc 方法:

at com.google.apphosting.api.ApiProxy.makeSyncCall(ApiProxy.java:116)

建议

Google App Engine 似乎仍然是not open sourced,所以我只能给你一些可能的方向:

根据ApiProxy的文档,这个类可能会使用ThreadLocal这样的变量来存储数据,所以使用Executors创建新线程可能会混淆App Engine;

它还为每个线程存储一个环境,其中包含有关请求的其他用户可见信息

在您的情况下,我没有看到使用多线程的必要性,您可能想尝试使用请求线程获取信息;

如果确实需要异步调用,可能需要使用后台线程,调用结束后通知用户;

更多

我尝试运行您的示例,但您的 Vision 类无法编译。所以我写了另一个例子,here,它运行良好。

以下是sdk反编译出的异常发生源代码:

    ApiProxy.Environment threadLocalEnvironment = (ApiProxy.Environment)environmentThreadLocal.get();
    if(threadLocalEnvironment != null) 
        return threadLocalEnvironment;
     else 
        ApiProxy.EnvironmentFactory envFactory = getEnvironmentFactory();
        if(envFactory != null) 
            ApiProxy.Environment environment = envFactory.newEnvironment();
            environmentThreadLocal.set(environment);
            return environment;
         else 
            return null;
        
    

在你的情况下,因为获取环境工厂失败,所以获取环境失败,导致你的异常。

说到为什么它缺少环境工厂,我猜你配置错误(你用GAE的服务器运行你的战争吗?如果你在本地测试,这是必要的。)

【讨论】:

感谢您的帮助。不幸的是,您的回答没有帮助。请阅读我的问题的更新 2。 认为这是根本原因。但是你能建议如何设置环境工厂吗?

以上是关于Google App Engine 错误:获取的线程既不是原始请求线程也不是 ThreadManager 创建的线程的主要内容,如果未能解决你的问题,请参考以下文章

Google App Engine-服务帐户导入错误

Google App Engine 和 404 错误

Python Google App Engine - 获取位置

google-app-engine部署错误

从 Google App Engine 数据存储中获取不一致

Google App Engine 错误:NeedIndexError:找不到匹配的索引