├── LICENSE ├── src └── main │ └── java │ ├── com │ └── google │ │ └── apphosting │ │ ├── utils │ │ └── http │ │ │ └── HttpRequest.java │ │ ├── runtime │ │ └── timer │ │ │ ├── Timer.java │ │ │ └── AbstractIntervalTimer.java │ │ └── vmruntime │ │ ├── VmTimer.java │ │ ├── VmRuntimeUtils.java │ │ ├── LazyApiProxyEnvironment.java │ │ ├── VmRequestThreadFactory.java │ │ ├── VmMetadataCache.java │ │ ├── VmAppLogsWriter.java │ │ ├── VmApiProxyDelegate.java │ │ └── VmApiProxyEnvironment.java │ └── net │ └── codestory │ └── http │ └── appengine │ └── AppEngineFilter.java └── pom.xml /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 all@code-story.net 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License 14 | -------------------------------------------------------------------------------- /src/main/java/com/google/apphosting/utils/http/HttpRequest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2011 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.apphosting.utils.http; 18 | 19 | @FunctionalInterface 20 | public interface HttpRequest { 21 | String getHeader(String name); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/google/apphosting/runtime/timer/Timer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2007 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.apphosting.runtime.timer; 18 | 19 | public interface Timer { 20 | void start(); 21 | 22 | void stop(); 23 | 24 | long getNanoseconds(); 25 | 26 | void update(); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/google/apphosting/vmruntime/VmTimer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.apphosting.vmruntime; 18 | 19 | import com.google.apphosting.runtime.timer.AbstractIntervalTimer; 20 | 21 | /** 22 | * Minimal implementation of com.google.apphosting.runtime.timer.Timer using only the system clock. 23 | */ 24 | public class VmTimer extends AbstractIntervalTimer { 25 | 26 | @Override 27 | protected long getCurrent() { 28 | return System.nanoTime(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/google/apphosting/vmruntime/VmRuntimeUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.apphosting.vmruntime; 18 | 19 | import static com.google.appengine.repackaged.com.google.common.base.MoreObjects.firstNonNull; 20 | 21 | public class VmRuntimeUtils { 22 | private static final String VM_API_PROXY_HOST = "appengine.googleapis.com"; 23 | private static final int VM_API_PROXY_PORT = 10001; 24 | public static final long ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000; 25 | public static final long MAX_USER_API_CALL_WAIT_MS = 60 * 1000; 26 | 27 | /** 28 | * Returns the host:port of the API server. 29 | * 30 | * @return If environment variables API_HOST or API_PORT port are set the host and/or port is 31 | * calculated from them. Otherwise the default host:port is used. 32 | */ 33 | public static String getApiServerAddress() { 34 | String server = firstNonNull(System.getenv("API_HOST"), VM_API_PROXY_HOST); 35 | String port = firstNonNull(System.getenv("API_PORT"), "" + VM_API_PROXY_PORT); 36 | return server + ":" + port; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/net/codestory/http/appengine/AppEngineFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.http.appengine; 17 | 18 | import com.google.apphosting.api.ApiProxy; 19 | import com.google.apphosting.runtime.timer.Timer; 20 | import com.google.apphosting.vmruntime.*; 21 | import net.codestory.http.Context; 22 | import net.codestory.http.filters.Filter; 23 | import net.codestory.http.filters.PayloadSupplier; 24 | import net.codestory.http.payload.Payload; 25 | 26 | import java.io.IOException; 27 | 28 | public class AppEngineFilter implements Filter { 29 | private final VmMetadataCache metadataCache; 30 | private final Timer wallclockTimer; 31 | 32 | public AppEngineFilter() { 33 | this.metadataCache = new VmMetadataCache(); 34 | this.wallclockTimer = new VmTimer(); 35 | ApiProxy.setDelegate(new VmApiProxyDelegate()); 36 | } 37 | 38 | @Override 39 | public boolean matches(String uri, Context context) { 40 | return !uri.startsWith("/webjars/"); 41 | } 42 | 43 | @Override 44 | public Payload apply(String uri, Context context, PayloadSupplier nextFilter) throws IOException { 45 | if (uri.equals("/_ah/start") || uri.equals("/_ah/stop") || uri.equals("/_ah/health")) { 46 | return new Payload("ok"); 47 | } 48 | 49 | ApiProxy.setEnvironmentForCurrentThread( 50 | new LazyApiProxyEnvironment(() -> VmApiProxyEnvironment.createFromHeaders( 51 | System.getenv(), 52 | metadataCache, 53 | name -> context.request().header(name), 54 | VmRuntimeUtils.getApiServerAddress(), 55 | wallclockTimer, 56 | VmRuntimeUtils.ONE_DAY_IN_MILLIS 57 | ))); 58 | 59 | return nextFilter.get(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/google/apphosting/runtime/timer/AbstractIntervalTimer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2007 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.apphosting.runtime.timer; 18 | 19 | /** 20 | * {@code AbstractIntervalTimer} is common base class for {@link 21 | * Timer} implementations that base measure the change in some value 22 | * between the point where the timer is started and the point where 23 | * the timer is stopped. 24 | *

25 | *

This class is thread-safe. 26 | */ 27 | abstract public class AbstractIntervalTimer implements Timer { 28 | protected boolean running = false; 29 | protected long startTime = 0L; 30 | protected long cumulativeTime = 0L; 31 | 32 | public synchronized void start() { 33 | if (running) { 34 | throw new IllegalStateException("already running"); 35 | } 36 | 37 | startTime = getCurrent(); 38 | running = true; 39 | } 40 | 41 | public synchronized void stop() { 42 | if (!running) { 43 | throw new IllegalStateException("not running"); 44 | } 45 | 46 | update(getCurrent()); 47 | running = false; 48 | } 49 | 50 | public synchronized void update() { 51 | update(getCurrent()); 52 | } 53 | 54 | public long getNanoseconds() { 55 | double ratio = getRatio(); 56 | synchronized (this) { 57 | if (running) { 58 | return cumulativeTime + ((long) ((getCurrent() - startTime) * ratio)); 59 | } else { 60 | return cumulativeTime; 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * The fraction of the change in the underlying counter which will 67 | * be attributed to this timer. By default, 100% of it. 68 | */ 69 | protected double getRatio() { 70 | return 1.0; 71 | } 72 | 73 | protected void update(long currentValue) { 74 | synchronized (this) { 75 | long increment = (long) ((currentValue - startTime) * getRatio()); 76 | cumulativeTime += increment; 77 | startTime = currentValue; 78 | } 79 | } 80 | 81 | abstract protected long getCurrent(); 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/google/apphosting/vmruntime/LazyApiProxyEnvironment.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package com.google.apphosting.vmruntime; 17 | 18 | import com.google.apphosting.api.ApiProxy; 19 | 20 | import java.util.Map; 21 | import java.util.function.Supplier; 22 | 23 | public class LazyApiProxyEnvironment implements ApiProxy.Environment { 24 | private final Supplier supplier; 25 | 26 | private VmApiProxyEnvironment delegate; 27 | 28 | public LazyApiProxyEnvironment(Supplier supplier) { 29 | this.supplier = supplier; 30 | } 31 | 32 | private VmApiProxyEnvironment delegate() { 33 | if (delegate == null) { 34 | delegate = supplier.get(); 35 | } 36 | return delegate; 37 | } 38 | 39 | @Override 40 | public String getAppId() { 41 | return delegate().getAppId(); 42 | } 43 | 44 | @Override 45 | public String getModuleId() { 46 | return delegate().getModuleId(); 47 | } 48 | 49 | @Override 50 | public String getVersionId() { 51 | return delegate().getVersionId(); 52 | } 53 | 54 | @Override 55 | public String getEmail() { 56 | return delegate().getEmail(); 57 | } 58 | 59 | @Override 60 | public boolean isLoggedIn() { 61 | return delegate().isLoggedIn(); 62 | } 63 | 64 | @Override 65 | public boolean isAdmin() { 66 | return delegate().isAdmin(); 67 | } 68 | 69 | @Override 70 | public String getAuthDomain() { 71 | return delegate().getAuthDomain(); 72 | } 73 | 74 | @Override 75 | @Deprecated 76 | public String getRequestNamespace() { 77 | return delegate().getRequestNamespace(); 78 | } 79 | 80 | @Override 81 | public Map getAttributes() { 82 | return delegate().getAttributes(); 83 | } 84 | 85 | @Override 86 | public long getRemainingMillis() { 87 | return delegate().getRemainingMillis(); 88 | } 89 | 90 | public void aSyncApiCallAdded(long maxWaitMs) throws ApiProxy.ApiProxyException { 91 | delegate().aSyncApiCallAdded(maxWaitMs); 92 | } 93 | 94 | public void apiCallStarted(long maxWaitMs, boolean releasePendingCall) throws ApiProxy.ApiProxyException { 95 | delegate().apiCallStarted(maxWaitMs, releasePendingCall); 96 | 97 | } 98 | 99 | public void apiCallCompleted() { 100 | delegate().apiCallCompleted(); 101 | } 102 | 103 | public void addLogRecord(ApiProxy.LogRecord record) { 104 | delegate().addLogRecord(record); 105 | } 106 | 107 | public void flushLogs() { 108 | delegate().flushLogs(); 109 | } 110 | 111 | public String getServer() { 112 | return delegate().getServer(); 113 | } 114 | 115 | public String getTicket() { 116 | return delegate().getTicket(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/google/apphosting/vmruntime/VmRequestThreadFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.apphosting.vmruntime; 18 | 19 | import com.google.appengine.repackaged.com.google.common.collect.ImmutableList; 20 | import com.google.appengine.repackaged.com.google.common.collect.Lists; 21 | import com.google.apphosting.api.ApiProxy; 22 | import com.google.apphosting.api.ApiProxy.Environment; 23 | 24 | import java.util.List; 25 | import java.util.concurrent.ThreadFactory; 26 | 27 | import static com.google.appengine.repackaged.com.google.common.base.Preconditions.checkState; 28 | 29 | 30 | /** 31 | * Thread factory creating threads with a request specific thread local environment. 32 | */ 33 | public class VmRequestThreadFactory implements ThreadFactory { 34 | private final Environment requestEnvironment; 35 | 36 | private final Object mutex; 37 | private final List createdThreads; 38 | private volatile boolean allowNewRequestThreadCreation; 39 | 40 | /** 41 | * Create a new VmRequestThreadFactory. 42 | * 43 | * @param requestEnvironment The request environment to install on each thread. 44 | */ 45 | public VmRequestThreadFactory(Environment requestEnvironment) { 46 | this.mutex = new Object(); 47 | this.requestEnvironment = requestEnvironment; 48 | this.createdThreads = Lists.newLinkedList(); 49 | this.allowNewRequestThreadCreation = true; 50 | } 51 | 52 | /** 53 | * Create a new {@link Thread} that executes {@code runnable} for the duration of the current 54 | * request. This thread will be interrupted at the end of the current request. 55 | * 56 | * @param runnable The object whose run method is invoked when this thread is started. If null, 57 | * this classes run method does nothing. 58 | * @throws ApiProxy.ApiProxyException If called outside of a running request. 59 | * @throws IllegalStateException If called after the request thread stops. 60 | */ 61 | @Override 62 | public Thread newThread(final Runnable runnable) { 63 | checkState(requestEnvironment != null, 64 | "Request threads can only be created within the context of a running request."); 65 | Thread thread = new Thread(new Runnable() { 66 | @Override 67 | public void run() { 68 | if (runnable == null) { 69 | return; 70 | } 71 | checkState(allowNewRequestThreadCreation, 72 | "Cannot start new threads after the request thread stops."); 73 | ApiProxy.setEnvironmentForCurrentThread(requestEnvironment); 74 | runnable.run(); 75 | } 76 | }); 77 | checkState( 78 | allowNewRequestThreadCreation, "Cannot create new threads after the request thread stops."); 79 | synchronized (mutex) { 80 | createdThreads.add(thread); 81 | } 82 | return thread; 83 | } 84 | 85 | /** 86 | * Returns an immutable copy of the current request thread list. 87 | */ 88 | public List getRequestThreads() { 89 | synchronized (mutex) { 90 | return ImmutableList.copyOf(createdThreads); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/google/apphosting/vmruntime/VmMetadataCache.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.apphosting.vmruntime; 18 | 19 | import java.io.BufferedReader; 20 | import java.io.IOException; 21 | import java.io.InputStreamReader; 22 | import java.net.HttpURLConnection; 23 | import java.net.URL; 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | import java.util.logging.Logger; 27 | 28 | /** 29 | * A class to retrieve and cache the meta-data of a VM running in Google's Compute Engine. 30 | */ 31 | public class VmMetadataCache { 32 | 33 | private static final Logger logger = Logger.getLogger(VmMetadataCache.class.getCanonicalName()); 34 | 35 | /** 36 | * The meta-data server's URL prefix. 37 | */ 38 | public static final String DEFAULT_META_DATA_SERVER = "metadata"; 39 | public static final String META_DATA_PATTERN = "http://%s/0.1/meta-data/%s"; 40 | 41 | /** 42 | * Maps paths to their cached values (null if a previous retrieval attempt failed). 43 | */ 44 | private final Map cache; 45 | 46 | /** 47 | * Timeout in milliseconds to retrieve data from the server. 48 | */ 49 | private static final int TIMEOUT_MILLIS = 120 * 1000; 50 | 51 | public VmMetadataCache() { 52 | cache = new HashMap<>(); 53 | } 54 | 55 | /** 56 | * Returns the value of the VM's meta-data attribute, or null if retrieval has failed. 57 | * 58 | * @param path the meta-data attribute to be retrieved (e.g. "image", "attributes/sshKeys"). 59 | * @return the attribute's string value or null if retrieval has failed. 60 | */ 61 | public String getMetadata(String path) { 62 | synchronized (cache) { 63 | if (cache.containsKey(path)) { 64 | return cache.get(path); 65 | } 66 | } 67 | 68 | String value = null; 69 | try { 70 | value = getMetadataFromServer(path); 71 | 72 | synchronized (cache) { 73 | cache.put(path, value); 74 | } 75 | } catch (IOException e) { 76 | logger.info("Meta-data '" + path + "' path retrieval error: " + e.getMessage()); 77 | } 78 | return value; 79 | } 80 | 81 | /** 82 | * Returns an HTTP URL connection to read the value of the specified attribute. 83 | *

84 | * May be overridden in tests. 85 | * 86 | * @param path the meta-data attribute to be retrieved (e.g. "image", "attributes/sshKeys"). 87 | * @return the HTTP URL connection object. 88 | * @throws IOException if the connection could not be opened. 89 | */ 90 | protected HttpURLConnection openConnection(String path) throws IOException { 91 | String server = System.getProperty("metadata_server", DEFAULT_META_DATA_SERVER); 92 | URL url = new URL(String.format(META_DATA_PATTERN, server, path)); 93 | return (HttpURLConnection) url.openConnection(); 94 | } 95 | 96 | /** 97 | * Retrieves the specified path from the VM's meta-data server. 98 | * 99 | * @param path a path in the meta-data server (e.g. "image"). 100 | * @return the meta-data's string value or null if the attribute is not found. 101 | * @throws IOException if the connection to the meta-data server fails, or refused. 102 | */ 103 | protected String getMetadataFromServer(String path) throws IOException { 104 | BufferedReader reader = null; 105 | HttpURLConnection connection = null; 106 | try { 107 | connection = openConnection(path); 108 | connection.setConnectTimeout(TIMEOUT_MILLIS); 109 | connection.setReadTimeout(TIMEOUT_MILLIS); 110 | reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); 111 | StringBuffer result = new StringBuffer(); 112 | char[] buffer = new char[4096]; 113 | int read; 114 | while ((read = reader.read(buffer)) != -1) { 115 | result.append(buffer, 0, read); 116 | } 117 | if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { 118 | return result.toString().trim(); 119 | } else if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { 120 | return null; 121 | } 122 | throw new IOException("Meta-data request for '" + path + "' failed with error: " + 123 | connection.getResponseMessage()); 124 | } finally { 125 | if (reader != null) { 126 | try { 127 | reader.close(); 128 | } catch (IOException e) { 129 | logger.info("Error closing connection for " + path + ": " + e.getMessage()); 130 | } 131 | } 132 | if (connection != null) { 133 | connection.disconnect(); 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | net.code-story 6 | fluent-http-appengine-adapter 7 | 2.32-SNAPSHOT 8 | jar 9 | 10 | CodeStory - Fluent-http - App Engine Adapter 11 | An App Engine Adapter for Fluent-http 12 | https://github.com/CodeStory/fluent-http-appengine-adapter 13 | 14 | 15 | org.sonatype.oss 16 | oss-parent 17 | 9 18 | 19 | 20 | 21 | 22 | scm:git:git@github.com:CodeStory/fluent-http-appengine-adapter.git 23 | scm:git:git@github.com:CodeStory/fluent-http-appengine-adapter.git 24 | scm:git:git@github.com:CodeStory/fluent-http-appengine-adapter.git 25 | HEAD 26 | 27 | 28 | 29 | 30 | Apache 2 31 | http://www.apache.org/licenses/LICENSE-2.0.txt 32 | repo 33 | A business-friendly OSS license 34 | 35 | 36 | 37 | 38 | 3.0.4 39 | 40 | 41 | 42 | UTF-8 43 | 1.8 44 | 1.8 45 | true 46 | 47 | 48 | 49 | 50 | release 51 | 52 | 53 | 54 | maven-gpg-plugin 55 | 1.5 56 | 57 | 58 | sign-artifacts 59 | verify 60 | 61 | sign 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | maven-clean-plugin 76 | 2.6.1 77 | 78 | 79 | maven-compiler-plugin 80 | 3.2 81 | 82 | 83 | maven-deploy-plugin 84 | 2.8.2 85 | 86 | 87 | maven-install-plugin 88 | 2.5.2 89 | 90 | 91 | maven-jar-plugin 92 | 2.5 93 | 94 | 95 | maven-resources-plugin 96 | 2.7 97 | 98 | 99 | maven-site-plugin 100 | 3.4 101 | 102 | 103 | maven-source-plugin 104 | 2.4 105 | 106 | 107 | maven-release-plugin 108 | 2.5.1 109 | 110 | 111 | maven-surefire-plugin 112 | 2.18 113 | 114 | 115 | maven-javadoc-plugin 116 | 2.9 117 | 118 | 119 | 120 | 121 | 122 | org.sonatype.plugins 123 | nexus-staging-maven-plugin 124 | 1.6.5 125 | true 126 | 127 | ossrh 128 | https://oss.sonatype.org/ 129 | true 130 | 131 | 132 | 133 | maven-release-plugin 134 | 135 | release 136 | 137 | 138 | 139 | org.apache.maven.plugins 140 | maven-javadoc-plugin 141 | 142 | 143 | attach-javadocs 144 | 145 | jar 146 | 147 | 148 | -Xdoclint:none 149 | 150 | 151 | 152 | 153 | 154 | maven-source-plugin 155 | 156 | 157 | attach-sources 158 | 159 | jar 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | net.code-story 170 | http 171 | 2.31 172 | 173 | 174 | 175 | 176 | com.google.appengine 177 | appengine-api-1.0-sdk 178 | 1.9.15 179 | 180 | 181 | com.google.code.gson 182 | gson 183 | 2.3 184 | 185 | 186 | org.apache.httpcomponents 187 | httpcore 188 | 4.3.2 189 | 190 | 191 | org.apache.httpcomponents 192 | httpclient 193 | 4.3.5 194 | 195 | 196 | com.google.appengine.tools 197 | appengine-gcs-client 198 | 0.4.3 199 | 200 | 201 | com.fasterxml.jackson.core 202 | jackson-core 203 | 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /src/main/java/com/google/apphosting/vmruntime/VmAppLogsWriter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2010 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.apphosting.vmruntime; 18 | 19 | import com.google.appengine.repackaged.com.google.common.base.Stopwatch; 20 | import com.google.apphosting.api.ApiProxy; 21 | import com.google.apphosting.api.ApiProxy.ApiConfig; 22 | import com.google.apphosting.api.ApiProxy.LogRecord; 23 | import com.google.apphosting.api.logservice.LogServicePb.FlushRequest; 24 | import com.google.apphosting.api.logservice.LogServicePb.UserAppLogGroup; 25 | import com.google.apphosting.api.logservice.LogServicePb.UserAppLogLine; 26 | 27 | import java.util.LinkedList; 28 | import java.util.List; 29 | import java.util.concurrent.ExecutionException; 30 | import java.util.concurrent.Future; 31 | import java.util.concurrent.TimeUnit; 32 | import java.util.concurrent.TimeoutException; 33 | import java.util.logging.Level; 34 | import java.util.logging.Logger; 35 | 36 | /** 37 | * {@code AppsLogWriter} is responsible for batching application logs 38 | * for a single request and sending them back to the AppServer via the 39 | * LogService.Flush API call. 40 | *

41 | *

The current algorithm used to send logs is as follows: 42 | *

58 | *

59 | *

This class is also responsible for splitting large log entries 60 | * into smaller fragments, which is unrelated to the batching 61 | * mechanism described above but is necessary to prevent the AppServer 62 | * from truncating individual log entries. 63 | *

64 | *

This class is thread safe and all methods accessing local state are 65 | * synchronized. Since each request have their own instance of this class the 66 | * only contention possible is between the original request thread and and any 67 | * child RequestThreads created by the request through the threading API. 68 | */ 69 | class VmAppLogsWriter { 70 | private static final Logger logger = 71 | Logger.getLogger(VmAppLogsWriter.class.getName()); 72 | 73 | static final String LOG_CONTINUATION_SUFFIX = "\n"; 74 | static final int LOG_CONTINUATION_SUFFIX_LENGTH = LOG_CONTINUATION_SUFFIX.length(); 75 | static final String LOG_CONTINUATION_PREFIX = "\n"; 76 | static final int LOG_CONTINUATION_PREFIX_LENGTH = LOG_CONTINUATION_PREFIX.length(); 77 | static final int MIN_MAX_LOG_MESSAGE_LENGTH = 1024; 78 | static final int LOG_FLUSH_TIMEOUT_MS = 2000; 79 | 80 | private final int maxLogMessageLength; 81 | private final int logCutLength; 82 | private final int logCutLengthDiv10; 83 | private final List buffer; 84 | private final long maxBytesToFlush; 85 | private long currentByteCount; 86 | private final int maxSecondsBetweenFlush; 87 | private int flushCount = 0; 88 | private Future currentFlush; 89 | private Stopwatch stopwatch; 90 | 91 | /** 92 | * Construct an AppLogsWriter instance. 93 | * 94 | * @param buffer Buffer holding messages between flushes. 95 | * @param maxBytesToFlush The maximum number of bytes of log message to 96 | * allow in a single flush. The code flushes any cached logs before 97 | * reaching this limit. If this is 0, AppLogsWriter will not start 98 | * an intermediate flush based on size. 99 | * @param maxLogMessageLength The maximum length of an individual log line. 100 | * A single log line longer than this will be written as multiple log 101 | * entries (with the continuation prefix/suffixes added to indicate this). 102 | * @param maxFlushSeconds The amount of time to allow a log line to sit 103 | * cached before flushing. Once a log line has been sitting for more 104 | * than the specified time, all currently cached logs are flushed. If 105 | * this is 0, no time based flushing occurs. 106 | * N.B. because we only check the time on a log call, it is possible for 107 | * a log to stay cached long after the specified time has been reached. 108 | * Consider this example (assume maxFlushSeconds=60): the app logs a message 109 | * when the handler starts but then does not log another message for 10 110 | * minutes. The initial log will stay cached until the second message 111 | * is logged. 112 | */ 113 | public VmAppLogsWriter(List buffer, long maxBytesToFlush, int maxLogMessageLength, 114 | int maxFlushSeconds) { 115 | this.buffer = buffer; 116 | this.maxSecondsBetweenFlush = maxFlushSeconds; 117 | 118 | if (maxLogMessageLength < MIN_MAX_LOG_MESSAGE_LENGTH) { 119 | String message = String.format( 120 | "maxLogMessageLength sillily small (%s); setting maxLogMessageLength to %s", 121 | maxLogMessageLength, MIN_MAX_LOG_MESSAGE_LENGTH); 122 | logger.warning(message); 123 | this.maxLogMessageLength = MIN_MAX_LOG_MESSAGE_LENGTH; 124 | } else { 125 | this.maxLogMessageLength = maxLogMessageLength; 126 | } 127 | logCutLength = maxLogMessageLength - LOG_CONTINUATION_SUFFIX_LENGTH; 128 | logCutLengthDiv10 = logCutLength / 10; 129 | 130 | if (maxBytesToFlush < this.maxLogMessageLength) { 131 | String message = String.format( 132 | "maxBytesToFlush (%s) smaller than maxLogMessageLength (%s)", 133 | maxBytesToFlush, this.maxLogMessageLength); 134 | logger.warning(message); 135 | this.maxBytesToFlush = this.maxLogMessageLength; 136 | } else { 137 | this.maxBytesToFlush = maxBytesToFlush; 138 | } 139 | 140 | stopwatch = Stopwatch.createUnstarted(); 141 | } 142 | 143 | /** 144 | * Add the specified {@link LogRecord} for the current request. If 145 | * enough space (or in the future, time) has accumulated, an 146 | * asynchronous flush may be started. If flushes are backed up, 147 | * this method may block. 148 | */ 149 | synchronized void addLogRecordAndMaybeFlush(LogRecord fullRecord) { 150 | for (LogRecord record : split(fullRecord)) { 151 | UserAppLogLine logLine = new UserAppLogLine(); 152 | logLine.setLevel(record.getLevel().ordinal()); 153 | logLine.setTimestampUsec(record.getTimestamp()); 154 | logLine.setMessage(record.getMessage()); 155 | int maxEncodingSize = logLine.maxEncodingSize(); 156 | if (maxBytesToFlush > 0 && 157 | (currentByteCount + maxEncodingSize) > maxBytesToFlush) { 158 | logger.info(currentByteCount + " bytes of app logs pending, starting flush..."); 159 | waitForCurrentFlushAndStartNewFlush(); 160 | } 161 | if (buffer.size() == 0) { 162 | stopwatch.start(); 163 | } 164 | buffer.add(logLine); 165 | currentByteCount += maxEncodingSize; 166 | } 167 | 168 | if (maxSecondsBetweenFlush > 0 && 169 | stopwatch.elapsed(TimeUnit.SECONDS) >= maxSecondsBetweenFlush) { 170 | waitForCurrentFlushAndStartNewFlush(); 171 | } 172 | } 173 | 174 | /** 175 | * Starts an asynchronous flush. This method may block if flushes 176 | * are backed up. 177 | * 178 | * @return The number of times this AppLogsWriter has initiated a flush. 179 | */ 180 | synchronized int waitForCurrentFlushAndStartNewFlush() { 181 | waitForCurrentFlush(); 182 | if (buffer.size() > 0) { 183 | currentFlush = doFlush(); 184 | } 185 | return flushCount; 186 | } 187 | 188 | /** 189 | * Initiates a synchronous flush. This method will always block 190 | * until any pending flushes and its own flush completes. 191 | */ 192 | synchronized void flushAndWait() { 193 | waitForCurrentFlush(); 194 | if (buffer.size() > 0) { 195 | currentFlush = doFlush(); 196 | waitForCurrentFlush(); 197 | } 198 | } 199 | 200 | /** 201 | * This method blocks until any outstanding flush is completed. This method 202 | * should be called prior to {@link #doFlush()} so that it is impossible for 203 | * the appserver to process logs out of order. 204 | */ 205 | private void waitForCurrentFlush() { 206 | if (currentFlush != null) { 207 | logger.info("Previous flush has not yet completed, blocking."); 208 | try { 209 | currentFlush.get( 210 | VmApiProxyDelegate.ADDITIONAL_HTTP_TIMEOUT_BUFFER_MS + LOG_FLUSH_TIMEOUT_MS, 211 | TimeUnit.MILLISECONDS); 212 | } catch (InterruptedException ex) { 213 | logger.warning("Interruped while blocking on a log flush, setting interrupt bit and " + 214 | "continuing. Some logs may be lost or occur out of order!"); 215 | Thread.currentThread().interrupt(); 216 | } catch (TimeoutException e) { 217 | logger.log(Level.WARNING, "Timeout waiting for log flush to complete. " 218 | + "Log messages may have been lost/reordered!", e); 219 | } catch (ExecutionException ex) { 220 | logger.log( 221 | Level.WARNING, 222 | "A log flush request failed. Log messages may have been lost!", ex); 223 | } 224 | currentFlush = null; 225 | } 226 | } 227 | 228 | private Future doFlush() { 229 | UserAppLogGroup group = new UserAppLogGroup(); 230 | for (UserAppLogLine logLine : buffer) { 231 | group.addLogLine(logLine); 232 | } 233 | buffer.clear(); 234 | currentByteCount = 0; 235 | flushCount++; 236 | stopwatch.reset(); 237 | FlushRequest request = new FlushRequest(); 238 | request.setLogsAsBytes(group.toByteArray()); 239 | ApiConfig apiConfig = new ApiConfig(); 240 | apiConfig.setDeadlineInSeconds(LOG_FLUSH_TIMEOUT_MS / 1000.0); 241 | return ApiProxy.makeAsyncCall("logservice", "Flush", request.toByteArray(), apiConfig); 242 | } 243 | 244 | /** 245 | * Because the App Server will truncate log messages that are too 246 | * long, we want to split long log messages into mutliple messages. 247 | * This method returns a {@link List} of {@code LogRecord}s, each of 248 | * which have the same {@link LogRecord#getLevel()} and 249 | * {@link LogRecord#getTimestamp()} as 250 | * this one, and whose {@link LogRecord#getMessage()} is short enough 251 | * that it will not be truncated by the App Server. If the 252 | * {@code message} of this {@code LogRecord} is short enough, the list 253 | * will contain only this {@code LogRecord}. Otherwise the list will 254 | * contain multiple {@code LogRecord}s each of which contain a portion 255 | * of the {@code message}. Additionally, strings will be 256 | * prepended and appended to each of the {@code message}s indicating 257 | * that the message is continued in the following log message or is a 258 | * continuation of the previous log mesage. 259 | */ 260 | 261 | List split(LogRecord aRecord) { 262 | LinkedList theList = new LinkedList(); 263 | String message = aRecord.getMessage(); 264 | if (null == message || message.length() <= maxLogMessageLength) { 265 | theList.add(aRecord); 266 | return theList; 267 | } 268 | String remaining = message; 269 | while (remaining.length() > 0) { 270 | String nextMessage; 271 | if (remaining.length() <= maxLogMessageLength) { 272 | nextMessage = remaining; 273 | remaining = ""; 274 | } else { 275 | int cutLength = logCutLength; 276 | boolean cutAtNewline = false; 277 | int friendlyCutLength = remaining.lastIndexOf('\n', logCutLength); 278 | if (friendlyCutLength > logCutLengthDiv10) { 279 | cutLength = friendlyCutLength; 280 | cutAtNewline = true; 281 | } 282 | nextMessage = remaining.substring(0, cutLength) + LOG_CONTINUATION_SUFFIX; 283 | remaining = remaining.substring(cutLength + (cutAtNewline ? 1 : 0)); 284 | if (remaining.length() > maxLogMessageLength || 285 | remaining.length() + LOG_CONTINUATION_PREFIX_LENGTH <= maxLogMessageLength) { 286 | remaining = LOG_CONTINUATION_PREFIX + remaining; 287 | } 288 | } 289 | theList.add(new LogRecord(aRecord, nextMessage)); 290 | } 291 | return theList; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/main/java/com/google/apphosting/vmruntime/VmApiProxyDelegate.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.apphosting.vmruntime; 18 | 19 | import com.google.appengine.repackaged.com.google.common.collect.Lists; 20 | import com.google.apphosting.api.ApiProxy; 21 | import com.google.apphosting.api.ApiProxy.ApiConfig; 22 | import com.google.apphosting.api.ApiProxy.ApiProxyException; 23 | import com.google.apphosting.api.ApiProxy.LogRecord; 24 | import com.google.apphosting.api.ApiProxy.RPCFailedException; 25 | import com.google.apphosting.utils.remoteapi.RemoteApiPb; 26 | import org.apache.http.HttpResponse; 27 | import org.apache.http.HttpStatus; 28 | import org.apache.http.client.HttpClient; 29 | import org.apache.http.client.methods.HttpPost; 30 | import org.apache.http.conn.ClientConnectionManager; 31 | import org.apache.http.conn.params.ConnManagerPNames; 32 | import org.apache.http.entity.ByteArrayEntity; 33 | import org.apache.http.entity.ContentType; 34 | import org.apache.http.impl.client.DefaultHttpClient; 35 | import org.apache.http.impl.conn.PoolingClientConnectionManager; 36 | import org.apache.http.params.BasicHttpParams; 37 | import org.apache.http.params.CoreConnectionPNames; 38 | import org.apache.http.params.HttpParams; 39 | import org.apache.http.protocol.BasicHttpContext; 40 | 41 | import java.io.BufferedInputStream; 42 | import java.io.IOException; 43 | import java.util.List; 44 | import java.util.Scanner; 45 | import java.util.concurrent.*; 46 | import java.util.logging.Logger; 47 | 48 | /** 49 | * Delegates AppEngine API calls to a local http API proxy when running inside a VM. 50 | *

51 | *

Instances should be registered using ApiProxy.setDelegate(ApiProxy.Delegate). 52 | */ 53 | public class VmApiProxyDelegate implements ApiProxy.Delegate { 54 | 55 | private static final Logger logger = Logger.getLogger(VmApiProxyDelegate.class.getName()); 56 | 57 | public static final String RPC_DEADLINE_HEADER = "X-Google-RPC-Service-Deadline"; 58 | public static final String RPC_STUB_ID_HEADER = "X-Google-RPC-Service-Endpoint"; 59 | public static final String RPC_METHOD_HEADER = "X-Google-RPC-Service-Method"; 60 | 61 | public static final String REQUEST_ENDPOINT = "/rpc_http"; 62 | public static final String REQUEST_STUB_ID = "app-engine-apis"; 63 | public static final String REQUEST_STUB_METHOD = "/VMRemoteAPI.CallRemoteAPI"; 64 | 65 | protected static final String API_DEADLINE_KEY = 66 | "com.google.apphosting.api.ApiProxy.api_deadline_key"; 67 | 68 | static final int ADDITIONAL_HTTP_TIMEOUT_BUFFER_MS = 1000; 69 | 70 | protected int defaultTimeoutMs; 71 | protected final ExecutorService executor; 72 | 73 | protected final HttpClient httpclient; 74 | 75 | final IdleConnectionMonitorThread monitorThread; 76 | 77 | private static ClientConnectionManager createConnectionManager() { 78 | PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager(); 79 | connectionManager.setMaxTotal(VmApiProxyEnvironment.MAX_CONCURRENT_API_CALLS); 80 | connectionManager.setDefaultMaxPerRoute(VmApiProxyEnvironment.MAX_CONCURRENT_API_CALLS); 81 | return connectionManager; 82 | } 83 | 84 | public VmApiProxyDelegate() { 85 | this(new DefaultHttpClient(createConnectionManager())); 86 | } 87 | 88 | 89 | VmApiProxyDelegate(HttpClient httpclient) { 90 | this.defaultTimeoutMs = 5 * 60 * 1000; 91 | this.executor = Executors.newCachedThreadPool(); 92 | this.httpclient = httpclient; 93 | this.monitorThread = new IdleConnectionMonitorThread(httpclient.getConnectionManager()); 94 | this.monitorThread.start(); 95 | } 96 | 97 | @Override 98 | public byte[] makeSyncCall( 99 | LazyApiProxyEnvironment environment, 100 | String packageName, 101 | String methodName, 102 | byte[] requestData) 103 | throws ApiProxyException { 104 | return makeSyncCallWithTimeout(environment, packageName, methodName, requestData, 105 | defaultTimeoutMs); 106 | } 107 | 108 | private byte[] makeSyncCallWithTimeout( 109 | LazyApiProxyEnvironment environment, 110 | String packageName, 111 | String methodName, 112 | byte[] requestData, 113 | int timeoutMs) 114 | throws ApiProxyException { 115 | return makeApiCall(environment, packageName, methodName, requestData, timeoutMs, false); 116 | } 117 | 118 | private byte[] makeApiCall(LazyApiProxyEnvironment environment, 119 | String packageName, 120 | String methodName, 121 | byte[] requestData, 122 | int timeoutMs, 123 | boolean wasAsync) { 124 | environment.apiCallStarted(VmRuntimeUtils.MAX_USER_API_CALL_WAIT_MS, wasAsync); 125 | try { 126 | return runSyncCall(environment, packageName, methodName, requestData, timeoutMs); 127 | } finally { 128 | environment.apiCallCompleted(); 129 | } 130 | } 131 | 132 | 133 | protected byte[] runSyncCall(LazyApiProxyEnvironment environment, String packageName, 134 | String methodName, byte[] requestData, int timeoutMs) { 135 | HttpPost request = createRequest(environment, packageName, methodName, requestData, timeoutMs); 136 | try { 137 | BasicHttpContext context = new BasicHttpContext(); 138 | HttpResponse response = httpclient.execute(request, context); 139 | 140 | if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { 141 | try (Scanner errorStreamScanner = 142 | new Scanner(new BufferedInputStream(response.getEntity().getContent()));) { 143 | logger.info("Error body: " + errorStreamScanner.useDelimiter("\\Z").next()); 144 | throw new RPCFailedException(packageName, methodName); 145 | } 146 | } 147 | try (BufferedInputStream bis = new BufferedInputStream(response.getEntity().getContent())) { 148 | RemoteApiPb.Response remoteResponse = new RemoteApiPb.Response(); 149 | if (!remoteResponse.parseFrom(bis)) { 150 | logger.info( 151 | "HTTP ApiProxy unable to parse response for " + packageName + "." + methodName); 152 | throw new RPCFailedException(packageName, methodName); 153 | } 154 | if (remoteResponse.hasRpcError() || remoteResponse.hasApplicationError()) { 155 | throw convertRemoteError(remoteResponse, packageName, methodName, logger); 156 | } 157 | return remoteResponse.getResponseAsBytes(); 158 | } 159 | } catch (IOException e) { 160 | logger.info( 161 | "HTTP ApiProxy I/O error for " + packageName + "." + methodName + ": " + e.getMessage()); 162 | throw new RPCFailedException(packageName, methodName); 163 | } finally { 164 | request.releaseConnection(); 165 | } 166 | } 167 | 168 | /** 169 | * Create an HTTP post request suitable for sending to the API server. 170 | * 171 | * @param environment The current VMApiProxyEnvironment 172 | * @param packageName The API call package 173 | * @param methodName The API call method 174 | * @param requestData The POST payload. 175 | * @param timeoutMs The timeout for this request 176 | * @return an HttpPost object to send to the API. 177 | */ 178 | static HttpPost createRequest(LazyApiProxyEnvironment environment, String packageName, 179 | String methodName, byte[] requestData, int timeoutMs) { 180 | RemoteApiPb.Request remoteRequest = new RemoteApiPb.Request(); 181 | remoteRequest.setServiceName(packageName); 182 | remoteRequest.setMethod(methodName); 183 | remoteRequest.setRequestId(environment.getTicket()); 184 | remoteRequest.setRequestAsBytes(requestData); 185 | 186 | HttpPost request = new HttpPost("http://" + environment.getServer() + REQUEST_ENDPOINT); 187 | request.setHeader(RPC_STUB_ID_HEADER, REQUEST_STUB_ID); 188 | request.setHeader(RPC_METHOD_HEADER, REQUEST_STUB_METHOD); 189 | 190 | HttpParams params = new BasicHttpParams(); 191 | params.setLongParameter(ConnManagerPNames.TIMEOUT, 192 | timeoutMs + ADDITIONAL_HTTP_TIMEOUT_BUFFER_MS); 193 | params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 194 | timeoutMs + ADDITIONAL_HTTP_TIMEOUT_BUFFER_MS); 195 | params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 196 | timeoutMs + ADDITIONAL_HTTP_TIMEOUT_BUFFER_MS); 197 | 198 | params.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, Boolean.TRUE); 199 | params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, Boolean.FALSE); 200 | request.setParams(params); 201 | 202 | Double deadline = (Double) (environment.getAttributes().get(API_DEADLINE_KEY)); 203 | if (deadline == null) { 204 | request.setHeader(RPC_DEADLINE_HEADER, 205 | Double.toString(TimeUnit.SECONDS.convert(timeoutMs, TimeUnit.MILLISECONDS))); 206 | } else { 207 | request.setHeader(RPC_DEADLINE_HEADER, Double.toString(deadline)); 208 | } 209 | 210 | Object dapperHeader = environment.getAttributes() 211 | .get(VmApiProxyEnvironment.AttributeMapping.DAPPER_ID.attributeKey); 212 | if (dapperHeader instanceof String) { 213 | request.setHeader( 214 | VmApiProxyEnvironment.AttributeMapping.DAPPER_ID.headerKey, (String) dapperHeader); 215 | } 216 | 217 | ByteArrayEntity postPayload = new ByteArrayEntity(remoteRequest.toByteArray(), 218 | ContentType.APPLICATION_OCTET_STREAM); 219 | postPayload.setChunked(false); 220 | request.setEntity(postPayload); 221 | 222 | return request; 223 | } 224 | 225 | /** 226 | * Convert RemoteApiPb.Response errors to the appropriate exception. 227 | *

228 | *

The response must have exactly one of the RpcError and ApplicationError fields set. 229 | * 230 | * @param remoteResponse the Response 231 | * @param packageName the name of the API package. 232 | * @param methodName the name of the method within the API package. 233 | * @param logger the Logger used to create log messages. 234 | * @return ApiProxyException 235 | */ 236 | private static ApiProxyException convertRemoteError(RemoteApiPb.Response remoteResponse, 237 | String packageName, String methodName, Logger logger) { 238 | if (remoteResponse.hasRpcError()) { 239 | return convertApiResponseRpcErrorToException( 240 | remoteResponse.getRpcError(), 241 | packageName, 242 | methodName, 243 | logger); 244 | } 245 | 246 | RemoteApiPb.ApplicationError error = remoteResponse.getApplicationError(); 247 | return new ApiProxy.ApplicationException(error.getCode(), error.getDetail()); 248 | } 249 | 250 | /** 251 | * Convert the RemoteApiPb.RpcError to the appropriate exception. 252 | * 253 | * @param rpcError the RemoteApiPb.RpcError. 254 | * @param packageName the name of the API package. 255 | * @param methodName the name of the method within the API package. 256 | * @param logger the Logger used to create log messages. 257 | * @return ApiProxyException 258 | */ 259 | private static ApiProxyException convertApiResponseRpcErrorToException( 260 | RemoteApiPb.RpcError rpcError, String packageName, String methodName, Logger logger) { 261 | 262 | int rpcCode = rpcError.getCode(); 263 | String errorDetail = rpcError.getDetail(); 264 | if (rpcCode > RemoteApiPb.RpcError.ErrorCode.values().length) { 265 | logger.severe("Received unrecognized error code from server: " + rpcError.getCode() + 266 | " details: " + errorDetail); 267 | return new ApiProxy.UnknownException(packageName, methodName); 268 | } 269 | RemoteApiPb.RpcError.ErrorCode errorCode = RemoteApiPb.RpcError.ErrorCode.values()[ 270 | rpcError.getCode()]; 271 | logger.warning("RPC failed : " + errorCode + " : " + errorDetail); 272 | 273 | switch (errorCode) { 274 | case CALL_NOT_FOUND: 275 | return new ApiProxy.CallNotFoundException(packageName, methodName); 276 | case PARSE_ERROR: 277 | return new ApiProxy.ArgumentException(packageName, methodName); 278 | case SECURITY_VIOLATION: 279 | logger.severe("Security violation: invalid request id used!"); 280 | return new ApiProxy.UnknownException(packageName, methodName); 281 | case CAPABILITY_DISABLED: 282 | return new ApiProxy.CapabilityDisabledException( 283 | errorDetail, packageName, methodName); 284 | case OVER_QUOTA: 285 | return new ApiProxy.OverQuotaException(packageName, methodName); 286 | case REQUEST_TOO_LARGE: 287 | return new ApiProxy.RequestTooLargeException(packageName, methodName); 288 | case RESPONSE_TOO_LARGE: 289 | return new ApiProxy.ResponseTooLargeException(packageName, methodName); 290 | case BAD_REQUEST: 291 | return new ApiProxy.ArgumentException(packageName, methodName); 292 | case CANCELLED: 293 | return new ApiProxy.CancelledException(packageName, methodName); 294 | case FEATURE_DISABLED: 295 | return new ApiProxy.FeatureNotEnabledException( 296 | errorDetail, packageName, methodName); 297 | case DEADLINE_EXCEEDED: 298 | return new ApiProxy.ApiDeadlineExceededException(packageName, methodName); 299 | default: 300 | return new ApiProxy.UnknownException(packageName, methodName); 301 | } 302 | } 303 | 304 | private class MakeSyncCall implements Callable { 305 | private final VmApiProxyDelegate delegate; 306 | private final LazyApiProxyEnvironment environment; 307 | private final String packageName; 308 | private final String methodName; 309 | private final byte[] requestData; 310 | private final int timeoutMs; 311 | 312 | public MakeSyncCall(VmApiProxyDelegate delegate, 313 | LazyApiProxyEnvironment environment, 314 | String packageName, 315 | String methodName, 316 | byte[] requestData, 317 | int timeoutMs) { 318 | this.delegate = delegate; 319 | this.environment = environment; 320 | this.packageName = packageName; 321 | this.methodName = methodName; 322 | this.requestData = requestData; 323 | this.timeoutMs = timeoutMs; 324 | } 325 | 326 | @Override 327 | public byte[] call() throws Exception { 328 | return delegate.makeApiCall(environment, 329 | packageName, 330 | methodName, 331 | requestData, 332 | timeoutMs, 333 | true); 334 | } 335 | } 336 | 337 | @Override 338 | public Future makeAsyncCall( 339 | LazyApiProxyEnvironment environment, 340 | String packageName, 341 | String methodName, 342 | byte[] request, 343 | ApiConfig apiConfig) { 344 | int timeoutMs = defaultTimeoutMs; 345 | if (apiConfig != null && apiConfig.getDeadlineInSeconds() != null) { 346 | timeoutMs = (int) (apiConfig.getDeadlineInSeconds() * 1000); 347 | } 348 | environment.aSyncApiCallAdded(VmRuntimeUtils.MAX_USER_API_CALL_WAIT_MS); 349 | return executor.submit(new MakeSyncCall(this, environment, packageName, 350 | methodName, request, timeoutMs)); 351 | } 352 | 353 | @Override 354 | public void log(LazyApiProxyEnvironment environment, LogRecord record) { 355 | if (environment != null) { 356 | environment.addLogRecord(record); 357 | } 358 | } 359 | 360 | @Override 361 | public void flushLogs(LazyApiProxyEnvironment environment) { 362 | if (environment != null) { 363 | environment.flushLogs(); 364 | } 365 | } 366 | 367 | @Override 368 | public List getRequestThreads(LazyApiProxyEnvironment environment) { 369 | Object threadFactory = 370 | environment.getAttributes().get(VmApiProxyEnvironment.REQUEST_THREAD_FACTORY_ATTR); 371 | if (threadFactory != null && threadFactory instanceof VmRequestThreadFactory) { 372 | return ((VmRequestThreadFactory) threadFactory).getRequestThreads(); 373 | } 374 | logger.warning("Got a call to getRequestThreads() but no VmRequestThreadFactory is available"); 375 | return Lists.newLinkedList(); 376 | } 377 | 378 | /** 379 | * Simple connection watchdog verifying that our connections are alive. Any stale connections are 380 | * cleared as well. 381 | */ 382 | class IdleConnectionMonitorThread extends Thread { 383 | 384 | private final ClientConnectionManager connectionManager; 385 | 386 | public IdleConnectionMonitorThread(ClientConnectionManager connectionManager) { 387 | super("IdleApiConnectionMontorThread"); 388 | this.connectionManager = connectionManager; 389 | this.setDaemon(false); 390 | } 391 | 392 | @Override 393 | public void run() { 394 | try { 395 | while (true) { 396 | connectionManager.closeExpiredConnections(); 397 | connectionManager.closeIdleConnections(60, TimeUnit.SECONDS); 398 | Thread.sleep(5000); 399 | } 400 | } catch (InterruptedException ex) { 401 | } 402 | } 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/main/java/com/google/apphosting/vmruntime/VmApiProxyEnvironment.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.apphosting.vmruntime; 18 | 19 | import com.google.apphosting.api.ApiProxy; 20 | import com.google.apphosting.api.ApiProxy.ApiProxyException; 21 | import com.google.apphosting.api.ApiProxy.LogRecord; 22 | import com.google.apphosting.runtime.timer.Timer; 23 | import com.google.apphosting.utils.http.HttpRequest; 24 | 25 | import java.util.Collections; 26 | import java.util.HashMap; 27 | import java.util.LinkedList; 28 | import java.util.Map; 29 | import java.util.concurrent.Executors; 30 | import java.util.concurrent.Semaphore; 31 | import java.util.concurrent.TimeUnit; 32 | 33 | /** 34 | * Implements the ApiProxy environment when running in a Google Compute Engine VM. 35 | *

36 | * Supports instantiation within a request as well as outside the context of a request. 37 | *

38 | * Instances should be registered using ApiProxy.setEnvironmentForCurrentThread(Environment). 39 | */ 40 | public class VmApiProxyEnvironment implements ApiProxy.Environment { 41 | 42 | public static final String PROJECT_ATTRIBUTE = "attributes/gae_project"; 43 | static final String LONG_APP_ID_KEY = "GAE_LONG_APP_ID"; 44 | 45 | public static final String PARTITION_ATTRIBUTE = "attributes/gae_partition"; 46 | static final String PARTITION_KEY = "GAE_PARTITION"; 47 | 48 | public static final String BACKEND_ATTRIBUTE = "attributes/gae_backend_name"; 49 | static final String MODULE_NAME_KEY = "GAE_MODULE_NAME"; 50 | 51 | public static final String VERSION_ATTRIBUTE = "attributes/gae_backend_version"; 52 | static final String VERSION_KEY = "GAE_MODULE_VERSION"; 53 | 54 | public static final String INSTANCE_ATTRIBUTE = "attributes/gae_backend_instance"; 55 | static final String INSTANCE_KEY = "GAE_MODULE_INSTANCE"; 56 | static final String MINOR_VERSION_KEY = "GAE_MINOR_VERSION"; 57 | 58 | public static final String TICKET_HEADER = "X-AppEngine-Api-Ticket"; 59 | public static final String EMAIL_HEADER = "X-AppEngine-User-Email"; 60 | public static final String IS_ADMIN_HEADER = "X-AppEngine-User-Is-Admin"; 61 | public static final String AUTH_DOMAIN_HEADER = "X-AppEngine-Auth-Domain"; 62 | 63 | static final String BACKEND_ID_KEY = "com.google.appengine.backend.id"; 64 | static final String INSTANCE_ID_KEY = "com.google.appengine.instance.id"; 65 | static final String REQUEST_THREAD_FACTORY_ATTR = "com.google.appengine.api.ThreadManager.REQUEST_THREAD_FACTORY"; 66 | static final String BACKGROUND_THREAD_FACTORY_ATTR = "com.google.appengine.api.ThreadManager.BACKGROUND_THREAD_FACTORY"; 67 | static final String IS_FEDERATED_USER_KEY = "com.google.appengine.api.users.UserService.is_federated_user"; 68 | static final String IS_TRUSTED_IP_KEY = "com.google.appengine.runtime.is_trusted_ip"; 69 | static final String IS_TRUSTED_IP_HEADER = "X-AppEngine-Trusted-IP-Request"; 70 | 71 | private static final long DEFAULT_FLUSH_APP_LOGS_EVERY_BYTE_COUNT = 1024 * 1024L; 72 | private static final int MAX_LOG_FLUSH_SECONDS = 60; 73 | private static final int DEFAULT_MAX_LOG_LINE_SIZE = 8 * 1024; 74 | 75 | static final int MAX_CONCURRENT_API_CALLS = 100; 76 | static final int MAX_PENDING_API_CALLS = 1000; 77 | 78 | /** 79 | * Mapping from HTTP header keys to attribute keys. 80 | */ 81 | static enum AttributeMapping { 82 | USER_ID( 83 | "X-AppEngine-User-Id", 84 | "com.google.appengine.api.users.UserService.user_id_key", 85 | "", false), 86 | USER_ORGANIZATION( 87 | "X-AppEngine-User-Organization", 88 | "com.google.appengine.api.users.UserService.user_organization", 89 | "", false), 90 | FEDERATED_IDENTITY( 91 | "X-AppEngine-Federated-Identity", 92 | "com.google.appengine.api.users.UserService.federated_identity", 93 | "", false), 94 | FEDERATED_PROVIDER( 95 | "X-AppEngine-Federated-Provider", 96 | "com.google.appengine.api.users.UserService.federated_authority", 97 | "", false), 98 | DATACENTER( 99 | "X-AppEngine-Datacenter", 100 | "com.google.apphosting.api.ApiProxy.datacenter", 101 | "", false), 102 | REQUEST_ID_HASH( 103 | "X-AppEngine-Request-Id-Hash", 104 | "com.google.apphosting.api.ApiProxy.request_id_hash", 105 | null, false), 106 | REQUEST_LOG_ID( 107 | "X-AppEngine-Request-Log-Id", 108 | "com.google.appengine.runtime.request_log_id", 109 | null, false), 110 | DAPPER_ID("X-Google-DapperTraceInfo", 111 | "com.google.appengine.runtime.dapper_id", 112 | null, false), 113 | DEFAULT_VERSION_HOSTNAME( 114 | "X-AppEngine-Default-Version-Hostname", 115 | "com.google.appengine.runtime.default_version_hostname", 116 | null, false), 117 | DEFAULT_NAMESPACE_HEADER( 118 | "X-AppEngine-Default-Namespace", 119 | "com.google.appengine.api.NamespaceManager.appsNamespace", 120 | null, false), 121 | CURRENT_NAMESPACE_HEADER( 122 | "X-AppEngine-Current-Namespace", 123 | "com.google.appengine.api.NamespaceManager.currentNamespace", 124 | null, false), 125 | LOAS_PEER_USERNAME( 126 | "X-AppEngine-LOAS-Peer-Username", 127 | "com.google.net.base.peer.loas_peer_username", 128 | "", true), 129 | GAIA_ID( 130 | "X-AppEngine-Gaia-Id", 131 | "com.google.appengine.runtime.gaia_id", 132 | "", true), 133 | GAIA_AUTHUSER( 134 | "X-AppEngine-Gaia-Authuser", 135 | "com.google.appengine.runtime.gaia_authuser", 136 | "", true), 137 | GAIA_SESSION( 138 | "X-AppEngine-Gaia-Session", 139 | "com.google.appengine.runtime.gaia_session", 140 | "", true), 141 | APPSERVER_DATACENTER( 142 | "X-AppEngine-Appserver-Datacenter", 143 | "com.google.appengine.runtime.appserver_datacenter", 144 | "", true), 145 | APPSERVER_TASK_BNS( 146 | "X-AppEngine-Appserver-Task-Bns", 147 | "com.google.appengine.runtime.appserver_task_bns", 148 | "", true); 149 | 150 | String headerKey; 151 | String attributeKey; 152 | Object defaultValue; 153 | private boolean trustedAppOnly; 154 | 155 | /** 156 | * Creates a mapping between an incoming request header and the thread local request attribute 157 | * corresponding to that header. 158 | * 159 | * @param headerKey The HTTP header key. 160 | * @param attributeKey The attribute key. 161 | * @param defaultValue The default value to set if the header is missing, or null if no 162 | * attribute should be set when the header is missing. 163 | * @param trustedAppOnly If true the attribute should only be set for trusted apps. 164 | */ 165 | private AttributeMapping( 166 | String headerKey, String attributeKey, Object defaultValue, boolean trustedAppOnly) { 167 | this.headerKey = headerKey; 168 | this.attributeKey = attributeKey; 169 | this.defaultValue = defaultValue; 170 | this.trustedAppOnly = trustedAppOnly; 171 | } 172 | } 173 | 174 | /** 175 | * Helper method to use during the transition from metadata to environment variables. 176 | * 177 | * @param environmentMap the 178 | * @param envKey The name of the environment variable to check first. 179 | * @param metadataPath The path of the metadata server entry to use as fallback. 180 | * @param cache The metadata server cache. 181 | * @return If set the environment variable corresponding to envKey, the metadata entry otherwise. 182 | */ 183 | private static String getEnvOrMetadata(Map environmentMap, VmMetadataCache cache, 184 | String envKey, String metadataPath) { 185 | String envValue = environmentMap.get(envKey); 186 | return envValue != null ? envValue : cache.getMetadata(metadataPath); 187 | } 188 | 189 | public static VmApiProxyEnvironment createFromHeaders(Map envMap, 190 | VmMetadataCache cache, 191 | HttpRequest request, 192 | String server, 193 | Timer wallTimer, 194 | Long millisUntilSoftDeadline) { 195 | String longAppId = getEnvOrMetadata(envMap, cache, LONG_APP_ID_KEY, PROJECT_ATTRIBUTE); 196 | String partition = getEnvOrMetadata(envMap, cache, PARTITION_KEY, PARTITION_ATTRIBUTE); 197 | String module = getEnvOrMetadata(envMap, cache, MODULE_NAME_KEY, BACKEND_ATTRIBUTE); 198 | String majorVersion = getEnvOrMetadata(envMap, cache, VERSION_KEY, VERSION_ATTRIBUTE); 199 | String minorVersion = envMap.get(MINOR_VERSION_KEY); 200 | String instance = getEnvOrMetadata(envMap, cache, INSTANCE_KEY, INSTANCE_ATTRIBUTE); 201 | String ticket = request.getHeader(TICKET_HEADER); 202 | String email = request.getHeader(EMAIL_HEADER); 203 | boolean admin = false; 204 | String value = request.getHeader(IS_ADMIN_HEADER); 205 | if (value != null && !value.trim().isEmpty()) { 206 | try { 207 | admin = Integer.parseInt(value.trim()) != 0; 208 | } catch (NumberFormatException e) { 209 | throw new IllegalArgumentException(e.getMessage(), e); 210 | } 211 | } 212 | String authDomain = request.getHeader(AUTH_DOMAIN_HEADER); 213 | boolean trustedApp = request.getHeader(IS_TRUSTED_IP_HEADER) != null; 214 | 215 | Map attributes = new HashMap<>(); 216 | for (AttributeMapping mapping : AttributeMapping.values()) { 217 | if (mapping.trustedAppOnly && !trustedApp) { 218 | continue; 219 | } 220 | String headerValue = request.getHeader(mapping.headerKey); 221 | if (headerValue != null) { 222 | attributes.put(mapping.attributeKey, headerValue); 223 | } else if (mapping.defaultValue != null) { 224 | attributes.put(mapping.attributeKey, mapping.defaultValue); 225 | } 226 | } 227 | 228 | boolean federatedId = request.getHeader(AttributeMapping.FEDERATED_IDENTITY.headerKey) != null; 229 | attributes.put(IS_FEDERATED_USER_KEY, federatedId); 230 | 231 | attributes.put(BACKEND_ID_KEY, module); 232 | attributes.put(INSTANCE_ID_KEY, instance); 233 | 234 | if (trustedApp) { 235 | boolean trustedIp = "1".equals(request.getHeader(IS_TRUSTED_IP_HEADER)); 236 | attributes.put(IS_TRUSTED_IP_KEY, trustedIp); 237 | } 238 | 239 | VmApiProxyEnvironment requestEnvironment = new VmApiProxyEnvironment(server, ticket, longAppId, 240 | partition, module, majorVersion, minorVersion, instance, email, admin, authDomain, 241 | wallTimer, millisUntilSoftDeadline, attributes); 242 | attributes.put(REQUEST_THREAD_FACTORY_ATTR, new VmRequestThreadFactory(requestEnvironment)); 243 | attributes.put(BACKGROUND_THREAD_FACTORY_ATTR, Executors.defaultThreadFactory()); 244 | 245 | return requestEnvironment; 246 | } 247 | 248 | private final String server; 249 | private final String ticket; 250 | private final String appId; 251 | private final String module; 252 | private final String versionId; 253 | private final String email; 254 | private final boolean admin; 255 | private final String authDomain; 256 | private final Map attributes; 257 | private final Timer wallTimer; 258 | private final Long millisUntilSoftDeadline; 259 | private final VmAppLogsWriter appLogsWriter; 260 | 261 | final Semaphore pendingApiCallSemaphore; 262 | 263 | final Semaphore runningApiCallSemaphore; 264 | 265 | /** 266 | * Constructs a VM AppEngine API environment. 267 | * 268 | * @param server the host:port address of the VM's HTTP proxy server. 269 | * @param ticket the request ticket (if null the default one will be computed). 270 | * @param appId the application ID (required if ticket is null). 271 | * @param partition the partition name. 272 | * @param module the module name (required if ticket is null). 273 | * @param majorVersion the major application version (required if ticket is null). 274 | * @param minorVersion the minor application version. 275 | * @param instance the VM instance ID (required if ticket is null). 276 | * @param email the user's e-mail address (may be null). 277 | * @param admin true if the user is an administrator. 278 | * @param authDomain the user's authentication domain (may be null). 279 | * @param wallTimer optional wall clock timer for the current request (required for deadline). 280 | * @param millisUntilSoftDeadline optional soft deadline in milliseconds relative to 'wallTimer'. 281 | * @param attributes map containing any attributes set on this environment. 282 | */ 283 | private VmApiProxyEnvironment( 284 | String server, String ticket, String appId, String partition, String module, 285 | String majorVersion, String minorVersion, String instance, String email, boolean admin, 286 | String authDomain, Timer wallTimer, Long millisUntilSoftDeadline, 287 | Map attributes) { 288 | if (server == null || server.isEmpty()) { 289 | throw new IllegalArgumentException("proxy server host:port must be specified"); 290 | } 291 | if (millisUntilSoftDeadline != null && wallTimer == null) { 292 | throw new IllegalArgumentException("wallTimer required when setting millisUntilSoftDeadline"); 293 | } 294 | if (ticket == null || ticket.isEmpty()) { 295 | if ((appId == null || appId.isEmpty()) || 296 | (module == null || module.isEmpty()) || 297 | (majorVersion == null || majorVersion.isEmpty()) || 298 | (instance == null || instance.isEmpty())) { 299 | throw new IllegalArgumentException( 300 | "When ticket == null the following must be specified: appId=" + appId + 301 | ", module=" + module + ", version=" + majorVersion + "instance=" + instance); 302 | } 303 | String escapedAppId = appId.replace(':', '_').replace('.', '_'); 304 | this.ticket = escapedAppId + '/' + module + '.' + majorVersion + "." + instance; 305 | } else { 306 | this.ticket = ticket; 307 | } 308 | this.server = server; 309 | if (appId == null) { 310 | this.appId = ""; 311 | } else { 312 | this.appId = partition + "~" + appId; 313 | } 314 | this.module = module == null ? "default" : module; 315 | this.versionId = String.format("%s.%s", majorVersion == null ? "" : majorVersion, minorVersion == null ? "" : minorVersion); 316 | this.email = email == null ? "" : email; 317 | this.admin = admin; 318 | this.authDomain = authDomain == null ? "" : authDomain; 319 | this.wallTimer = wallTimer; 320 | this.millisUntilSoftDeadline = millisUntilSoftDeadline; 321 | this.attributes = Collections.synchronizedMap(attributes); 322 | 323 | this.appLogsWriter = new VmAppLogsWriter( 324 | new LinkedList<>(), DEFAULT_FLUSH_APP_LOGS_EVERY_BYTE_COUNT, 325 | DEFAULT_MAX_LOG_LINE_SIZE, MAX_LOG_FLUSH_SECONDS); 326 | this.pendingApiCallSemaphore = new Semaphore(MAX_PENDING_API_CALLS); 327 | this.runningApiCallSemaphore = new Semaphore(MAX_CONCURRENT_API_CALLS); 328 | } 329 | 330 | public void addLogRecord(LogRecord record) { 331 | appLogsWriter.addLogRecordAndMaybeFlush(record); 332 | } 333 | 334 | public void flushLogs() { 335 | appLogsWriter.flushAndWait(); 336 | } 337 | 338 | public String getServer() { 339 | return server; 340 | } 341 | 342 | public String getTicket() { 343 | return ticket; 344 | } 345 | 346 | @Override 347 | public String getAppId() { 348 | return appId; 349 | } 350 | 351 | @Override 352 | public String getModuleId() { 353 | return module; 354 | } 355 | 356 | @Override 357 | public String getVersionId() { 358 | return versionId; 359 | } 360 | 361 | @Override 362 | public String getEmail() { 363 | return email; 364 | } 365 | 366 | @Override 367 | public boolean isLoggedIn() { 368 | return getEmail() != null && !getEmail().trim().isEmpty(); 369 | } 370 | 371 | @Override 372 | public boolean isAdmin() { 373 | return admin; 374 | } 375 | 376 | @Override 377 | public String getAuthDomain() { 378 | return authDomain; 379 | } 380 | 381 | @Deprecated 382 | @Override 383 | public String getRequestNamespace() { 384 | Object currentNamespace = 385 | attributes.get(AttributeMapping.CURRENT_NAMESPACE_HEADER.attributeKey); 386 | if (currentNamespace instanceof String) { 387 | return (String) currentNamespace; 388 | } 389 | return ""; 390 | } 391 | 392 | @Override 393 | public Map getAttributes() { 394 | return attributes; 395 | } 396 | 397 | @Override 398 | public long getRemainingMillis() { 399 | if (millisUntilSoftDeadline == null) { 400 | return Long.MAX_VALUE; 401 | } 402 | return millisUntilSoftDeadline - (wallTimer.getNanoseconds() / 1000000L); 403 | } 404 | 405 | /** 406 | * Notifies the environment that an API call was queued up. 407 | * 408 | * @throws ApiProxyException 409 | */ 410 | public void aSyncApiCallAdded(long maxWaitMs) throws ApiProxyException { 411 | try { 412 | if (pendingApiCallSemaphore.tryAcquire(maxWaitMs, TimeUnit.MILLISECONDS)) { 413 | return; 414 | } 415 | throw new ApiProxyException("Timed out while acquiring a pending API call semaphore."); 416 | } catch (InterruptedException e) { 417 | throw new ApiProxyException( 418 | "Thread interrupted while acquiring a pending API call semaphore."); 419 | } 420 | } 421 | 422 | /** 423 | * Notifies the environment that an API call was started. 424 | * 425 | * @param releasePendingCall If true a pending call semaphore will be released (required if this 426 | * API call was requested asynchronously). 427 | * @throws ApiProxyException If the thread was interrupted while waiting for a semaphore. 428 | */ 429 | public void apiCallStarted(long maxWaitMs, boolean releasePendingCall) throws ApiProxyException { 430 | try { 431 | if (runningApiCallSemaphore.tryAcquire(maxWaitMs, TimeUnit.MILLISECONDS)) { 432 | return; 433 | } 434 | throw new ApiProxyException("Timed out while acquiring an API call semaphore."); 435 | } catch (InterruptedException e) { 436 | throw new ApiProxyException("Thread interrupted while acquiring an API call semaphore."); 437 | } finally { 438 | if (releasePendingCall) { 439 | pendingApiCallSemaphore.release(); 440 | } 441 | } 442 | } 443 | 444 | /** 445 | * Notifies the environment that an API call completed. 446 | */ 447 | public void apiCallCompleted() { 448 | runningApiCallSemaphore.release(); 449 | } 450 | } 451 | --------------------------------------------------------------------------------