├── 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 | *
43 | * - The code never allows more than {@code byteCountBeforeFlush} bytes of
44 | * log data to accumulate in the buffer. If adding a new log line
45 | * would exceed that limit, the current set of logs are removed from it and an
46 | * asynchronous API call is started to flush the logs before buffering the
47 | * new line.
48 | *
49 | * - If another flush occurs while a previous flush is still
50 | * pending, the caller will block synchronously until the previous
51 | * call completed.
52 | *
53 | * - When the overall request completes is should call @code{waitForCurrentFlushAndStartNewFlush}
54 | * and report the flush count as a HTTP response header. The vm_runtime on the appserver
55 | * will wait for the reported number of log flushes before forwarding the HTTP response
56 | * to the user.
57 | *
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 |
--------------------------------------------------------------------------------