
com.google.apphosting.loadtesting.allinone.ee10.MainServlet Maven / Gradle / Ivy
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.apphosting.loadtesting.allinone.ee10;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.Filter;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.api.memcache.Stats;
import com.google.appengine.api.taskqueue.DeferredTask;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.apphosting.api.ApiProxy;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.common.io.CountingInputStream;
import com.google.common.math.BigIntegerMath;
import com.google.errorprone.annotations.FormatMethod;
import com.sun.management.HotSpotDiagnosticMXBean;
import com.sun.management.VMOption;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import javax.imageio.ImageIO;
import javax.management.MBeanServer;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.management.ClassLoadingMXBean;
import java.lang.management.CompilationMXBean;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryUsage;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.Principal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Servlet capable of performing a variety of actions based on the value of
* the query parameters.
*
* Originally this code was compatible with {@code },
* but it has evolved enough since then that compatibility should not be expected.
*
* This servlet also accepts POST requests and returns a plain text response containing
* the number of bytes read.
*
*/
public class MainServlet extends HttpServlet {
private static final Logger logger = Logger.getLogger(MainServlet.class.getName());
private static final Charset US_ASCII_CHARSET = US_ASCII;
private static final Level[] LOG_LEVELS =
new Level[] {Level.FINEST, Level.FINE, Level.CONFIG, Level.INFO, Level.WARNING, Level.SEVERE};
// "Car" datastore entity.
private static final String CAR_KIND = "Car";
private static final String COLOR_PROPERTY = "color";
private static final String BRAND_PROPERTY = "brand";
private static final String[] COLORS = new String[] { "green", "red", "blue" };
private static final String[] BRANDS = new String[] { "toyota", "honda", "nissan" };
// "Log" datastore entity.
private static final String LOG_KIND = "Log";
private static final String URL_PROPERTY = "url";
private static final String DATE_PROPERTY = "date";
private static final ImmutableSet VALID_PARAMETERS =
ImmutableSet.of(
"add_tasks",
"awt_text",
"clear_pinned_buffers",
"datastore_count",
"datastore_cron",
"datastore_entities",
"datastore_queries",
"deferred_task",
"deferred_task_verify",
"direct_byte_buffer_size",
"fetch_project_id_from_metadata",
"fetch_service_account_scopes_from_metadata",
"fetch_service_account_token_from_metadata",
"fetch_url",
"fetch_url_using_httpurlconnection",
"forward",
"get_attribute",
"get_environment",
"get_header",
"get_metadata",
"get_named_dispatcher",
"get_system_property",
"jmx_info",
"jmx_list_vm_options",
"jmx_thread_dump",
"list_attributes",
"list_environment",
"list_headers",
"list_processes",
"list_system_properties",
"log_flush",
"log_lines",
"log_remaining_time",
"math_loops",
"math_ms",
"memcache_loops",
"memcache_size",
"oom",
"pin_byte_buffer",
"pin_byte_buffer_size",
"random_response_size",
"response_size",
"set_servlet_attributes",
"silent",
"sql_columns",
"sql_db",
"sql_len",
"sql_replace",
"sql_rows",
"sql_timeout",
// "spanner_id",
// "spanner_db",
"task_url",
"user",
"validate_fs");
private static final List PINNED_BUFFERS = new ArrayList<>();
private final Random random = new Random();
private ServletContext context;
@Override
public void init(ServletConfig config) {
this.context = config.getServletContext();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
IOException {
validateParameters(req);
// In silent mode, regular response output is suppressed and a single "OK"
// string is printed if all actions ran successfully.
boolean silent = req.getParameter("silent") != null;
resp.setContentType("text/plain");
PrintWriter responseWriter = resp.getWriter();
PrintWriter w = (silent ? new PrintWriter(new StringWriter()) : responseWriter);
// These options are also present in the python allinone application, sometimes
// with slightly different names, e.g. "queries" vs "datastore_queries".
Integer mathMsParam = getIntParameter("math_ms", req);
if (mathMsParam != null) {
performMathMs(mathMsParam, w);
}
Integer mathLoopsParam = getIntParameter("math_loops", req);
if (mathLoopsParam != null) {
performMathLoops(mathLoopsParam, w);
}
Integer memcacheLoopsParam = getIntParameter("memcache_loops", req);
if (memcacheLoopsParam != null) {
Integer size = getIntParameter("memcache_size", req);
performMemcacheLoops(memcacheLoopsParam, size, w);
}
Integer logLinesParam = getIntParameter("log_lines", req);
if (logLinesParam != null) {
performLogging(logLinesParam, w);
}
if (req.getParameter("log_flush") != null) {
performLogFlush(w);
}
Integer responseSizeParam = getIntParameter("response_size", req);
if (responseSizeParam != null) {
performResponseSize(responseSizeParam, w);
}
Integer queriesParam = getIntParameter("datastore_queries", req);
if (queriesParam != null) {
performQueries(queriesParam, w);
}
Integer entitiesParam = getIntParameter("datastore_entities", req);
if (entitiesParam != null) {
HttpSession s = req.getSession(true);
s.setAttribute("storedsession", "sessiondata");
performAddEntities(entitiesParam, w);
}
if (req.getParameter("datastore_count") != null) {
HttpSession s = req.getSession(true);
if (!"sessiondata".equals(s.getAttribute("storedsession"))) {
emitf(w, "Java session NOT working");
} else {
performCount(w);
}
}
if (req.getParameter("datastore_cron") != null) {
performCron(req.getRequestURI(), w);
}
if (req.getParameter("user") != null) {
performUserLogin(w, req.getRequestURI(), req.getUserPrincipal());
}
String urlParam = req.getParameter("fetch_url");
if (urlParam != null) {
performUrlFetch(w, urlParam);
}
String urlParam2 = req.getParameter("fetch_url_using_httpurlconnection");
if (urlParam2 != null) {
performUrlFetchUsingHttpURLConnection(w, urlParam2);
}
// These options are Java-specific.
Integer randomResponseSizeParam = getIntParameter("random_response_size", req);
if (randomResponseSizeParam != null) {
performRandomResponseSize(randomResponseSizeParam, w);
}
boolean pinByteBuffers = (req.getParameter("pin_byte_buffer") != null);
Integer byteBufferSizeParam = getIntParameter("byte_buffer_size", req);
if (byteBufferSizeParam != null) {
ByteBuffer b = performByteBufferAllocation(byteBufferSizeParam, false, w);
if (pinByteBuffers) {
emit(w, "Pinned buffer");
PINNED_BUFFERS.add(b);
}
}
Integer directByteBufferSizeParam = getIntParameter("direct_byte_buffer_size", req);
if (directByteBufferSizeParam != null) {
ByteBuffer b = performByteBufferAllocation(directByteBufferSizeParam, true, w);
if (pinByteBuffers) {
emit(w, "Pinned buffer");
PINNED_BUFFERS.add(b);
}
}
if (req.getParameter("clear_pinned_buffers") != null) {
emit(w, "Cleared pinner buffers");
PINNED_BUFFERS.clear();
}
if (req.getParameter("list_system_properties") != null) {
performListSystemProperties(w);
}
String sysPropName = req.getParameter("get_system_property");
if (sysPropName != null) {
emit(w, System.getProperty(sysPropName));
}
if (req.getParameter("list_environment") != null) {
performListEnvironment(w);
}
String envVarName = req.getParameter("get_environment");
if (envVarName != null) {
emit(w, System.getenv(envVarName));
}
String headerName = req.getParameter("get_header");
if (headerName != null) {
emit(w, req.getHeader(headerName));
}
if (req.getParameter("list_attributes") != null) {
performListAttributes(w);
}
String attrName = req.getParameter("get_attribute");
if (attrName != null) {
emit(w, String.valueOf(ApiProxy.getCurrentEnvironment().getAttributes().get(attrName)));
}
String servletAttributeString = req.getParameter("set_servlet_attributes");
if (servletAttributeString != null) {
performSetServletAttributes(req, servletAttributeString, w);
}
String dispatcherName = req.getParameter("get_named_dispatcher");
if (dispatcherName != null) {
emitf(w, "%s", context.getNamedDispatcher(dispatcherName));
}
if (req.getParameter("fetch_project_id_from_metadata") != null) {
performFetchProjectIdFromMetadata(w);
}
if (req.getParameter("fetch_service_account_token_from_metadata") != null) {
performFetchServiceAccountTokenFromMetadata(w);
}
if (req.getParameter("fetch_service_account_scopes_from_metadata") != null) {
performFetchServiceAccountScopesFromMetadata(w);
}
if (req.getParameter("log_remaining_time") != null) {
performLogRemainingTime(w);
}
if (req.getParameter("deferred_task") != null) {
performAddDeferredTask(w);
}
if (req.getParameter("deferred_task_verify") != null) {
performVerifyDeferredTask(w);
}
Integer tasksParam = getIntParameter("add_tasks", req);
if (tasksParam != null) {
String taskUrl = req.getParameter("task_url");
performAddTasks(tasksParam, taskUrl, w);
}
String dbParam = req.getParameter("sql_db");
if (dbParam != null) {
Integer timeout = getIntParameter("sql_timeout", req);
Integer rows = getIntParameter("sql_rows", req);
Integer columns = getIntParameter("sql_columns", req);
Integer len = getIntParameter("sql_len", req);
boolean replace = req.getParameter("sql_replace") != null;
performCloudSqlAccess(dbParam, (rows != null ? rows : 10),
(columns != null ? columns : 1), (len != null ? len : 10),
(timeout != null ? timeout : 0), replace, w);
}
/*
// The spanner functionality is commented out because it results
// in one version violations.
String spannerId = req.getParameter("spanner_id");
if (spannerId != null) {
String spannerDb = req.getParameter("spanner_db");
performSpannerAccess(spannerId, spannerDb, w);
}
*/
String awtText = req.getParameter("awt_text");
if (awtText != null) {
performAwtTextRendering(awtText, w);
}
if (req.getParameter("oom") != null) {
throw new OutOfMemoryError("intentional termination");
}
if (req.getParameter("jmx_info") != null) {
performJmxInfo(w);
}
if (req.getParameter("jmx_list_vm_options") != null) {
performJmxListVmOptions(w);
}
if (req.getParameter("jmx_thread_dump") != null) {
performJmxThreadDump(w);
}
String metadataURL = req.getParameter("get_metadata");
if (metadataURL != null) {
emit(w, fetchMetadata(new URL(metadataURL)));
}
if (req.getParameter("list_headers") != null) {
performListHeaders(req, w);
}
if (req.getParameter("list_processes") != null) {
performListProcesses(w);
}
if (req.getParameter("validate_fs") != null) {
performReadOnlyFSCheck(w);
}
String forward = req.getParameter("forward");
if (forward != null && req.getAttribute("forwarded") == null) {
req.setAttribute("forwarded", true);
RequestDispatcher dispatcher = req.getRequestDispatcher("/?" + forward);
dispatcher.forward(req, resp);
}
w.flush();
if (silent) {
responseWriter.println("OK");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
IOException {
resp.setContentType("text/plain");
PrintWriter w = resp.getWriter();
CountingInputStream payload = new CountingInputStream(req.getInputStream());
ByteStreams.copy(payload, ByteStreams.nullOutputStream());
w.print(payload.getCount());
w.flush();
}
/**
* Performs some cpu intensive work for the specified amount of time.
*
* @param ms the (approximate) time in milliseconds
* @param w response writer
*/
private void performMathMs(int ms, PrintWriter w) {
emitf(w, "Burning cpu for %d ms", ms);
runRepeatedly(ms, new Runnable() {
@Override
public void run() {
performMath(random.nextBoolean());
}
});
logger.info("Cpu burned");
}
/**
* Performs some cpu intensive work for the specified number of iterations.
*
* @param count the number of iterations
* @param w response writer
*/
private void performMathLoops(int count, PrintWriter w) {
emitf(w, "Burning cpu for %d loops", count);
for (int i = 0; i < count; ++i) {
performMath(random.nextBoolean());
}
logger.info("Cpu burned");
}
/**
* Performs some cpu intensive work.
*
* We try to make it harder for Hotspot to optimize away the computation by adding a random
* parameter and by including an unreachable (but not obviously so) "throw" statement.
*
* @param addOne whether to add a spurious "one" value as part of the computation
*/
private static void performMath(boolean addOne) {
int x = 0;
for (int i = 0; i < 200; ++i) {
x += BigIntegerMath.log2(BigIntegerMath.factorial(i)
.add(addOne ? BigInteger.ONE : BigInteger.ZERO), RoundingMode.DOWN);
}
if (x != 109766 && !addOne) {
throw new AssertionError("incorrect result");
}
}
/**
* Performs some memcache work.
*
* @param count the number of iterations to perform
* @param size memcache value size
* @param w response writer
*/
private void performMemcacheLoops(int count, int size, PrintWriter w) {
emitf(w, "Running memcache for %d loops with value size %d", count, size);
MemcacheService memcacheService = MemcacheServiceFactory.getMemcacheService();
for (int i = 0; i < count; ++i) {
String key = "test_key:" + random.nextInt(10000);
memcacheService.put(key, createRandomString(size));
memcacheService.get(key);
}
Stats stats = memcacheService.getStatistics();
emitf(w, "Cache hits: %d", stats.getHitCount());
emitf(w, "Cache misses: %d", stats.getMissCount());
}
/**
* Performs some logging actions at random log levels.
*
* @param count the number of log entries to create
* @param w response writer
*/
private void performLogging(int count, PrintWriter w) {
emitf(w, "Logging %d entries", count);
logger.info("Starting logging");
for (int i = 0; i < count; ++i) {
logger.log(LOG_LEVELS[random.nextInt(LOG_LEVELS.length)],
"An informative log message with some interesting words.");
}
logger.info("Done logging");
}
/**
* Flushes the logs.
*
* @param w response writer
*/
private static void performLogFlush(PrintWriter w) {
emit(w, "Flushing logs");
ApiProxy.flushLogs();
}
/**
* Generates a response of the specified size.
*
* @param size desired number of characters in the response
* @param w response writer
*/
private static void performResponseSize(int size, PrintWriter w) {
w.print(Strings.repeat("a", size));
}
/**
* Generates a random response of the specified size.
*
* @param size desired number of characters in the response
* @param w response writer
*/
private void performRandomResponseSize(int size, PrintWriter w) {
while (size > 0) {
String s = Integer.toString(random.nextInt(Integer.MAX_VALUE));
if (s.length() > size) {
s = s.substring(0, size);
}
w.print(s);
size -= s.length();
}
}
/**
* Performs the specified number of datastore queries.
*
* @param count the number of queries to perform
* @param w response writer
*/
private void performQueries(int count, PrintWriter w) {
DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
for (int i = 0; i < count; ++i) {
Query query = new Query(CAR_KIND);
Filter filter = null;
if (random.nextBoolean()) {
filter = new Query.FilterPredicate(COLOR_PROPERTY, Query.FilterOperator.EQUAL,
COLORS[random.nextInt(COLORS.length)]);
} else {
filter = new Query.FilterPredicate(BRAND_PROPERTY, Query.FilterOperator.EQUAL,
BRANDS[random.nextInt(BRANDS.length)]);
}
query.setFilter(filter);
List results = datastoreService.prepare(query)
.asList(FetchOptions.Builder.withLimit(20));
emitf(w, "Retrieved %d entities", results.size());
}
}
/**
* Adds the specified number of entities to the datastore.
*
* @param count the number of entities to persist
* @param w response writer
*/
private void performAddEntities(int count, PrintWriter w) {
DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
for (int i = 0; i < count; ++i) {
Entity car = new Entity(CAR_KIND);
car.setProperty(COLOR_PROPERTY, COLORS[random.nextInt(COLORS.length)]);
car.setProperty(BRAND_PROPERTY, BRANDS[random.nextInt(BRANDS.length)]);
datastoreService.put(car);
}
emitf(w, "Added %d entities", count);
}
/**
* Counts the "car" entities in the datastore.
*
* @param w response writer
*/
private static void performCount(PrintWriter w) {
DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
Query query = new Query(CAR_KIND);
int count = datastoreService.prepare(query).countEntities(FetchOptions.Builder.withDefaults());
emitf(w, "Found %d entities", count);
}
/**
* Inserts a "log" entity into the datastore with the current request URL and timestamp.
*
* @param w response writer
*/
private static void performCron(String url, PrintWriter w) {
DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
Entity log = new Entity(LOG_KIND);
log.setProperty(URL_PROPERTY, url);
log.setProperty(DATE_PROPERTY, new Date());
datastoreService.put(log);
emit(w, "Persisted log entry");
}
/** Prints out logout url if there is a logged in user, otherwise prints out a login url. */
private static void performUserLogin(PrintWriter w, String url, Principal principal) {
UserService userService = UserServiceFactory.getUserService();
if (principal != null) {
emitf(w, "Hello %s. Sign out with %s", principal.getName(),
userService.createLogoutURL(url));
} else {
emitf(w, "Sign in with %s", userService.createLoginURL(url));
}
}
/** Issues url fetch request to a specified url. */
private static void performUrlFetch(PrintWriter w, String url) throws IOException {
URLFetchService urlFetchService = URLFetchServiceFactory.getURLFetchService();
HTTPResponse httpResponse = urlFetchService.fetch(new URL(url));
emitf(w, "Response code: %s", httpResponse.getResponseCode());
}
/** Fetches a URL using a {@code java.net.HttpURLConnection}. */
private static void performUrlFetchUsingHttpURLConnection(PrintWriter w, String url)
throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
try (InputStream input = connection.getInputStream()) {
long n = ByteStreams.exhaust(input);
emitf(w, "Response code: %d", connection.getResponseCode());
emitf(w, "Bytes read: %d", n);
}
}
/**
* Allocates a ByteBuffer of the specified size.
*
* @param size requested buffer size
* @param direct whether to allocate a direct byte buffer instead of a regular one
* @param w response writer
* @return the allocated ByteBuffer
*/
private static ByteBuffer performByteBufferAllocation(int size, boolean direct, PrintWriter w) {
ByteBuffer buffer = direct ? ByteBuffer.allocateDirect(size) : ByteBuffer.allocate(size);
// Write at 4K intervals to hit all the pages.
for (int offset = 0; offset < size; offset += 4096) {
buffer.put(offset, (byte) 0xCC);
}
emitf(w, "Allocated %d bytes in a %s buffer", size, (direct ? "direct" : "regular"));
return buffer;
}
/**
* Lists all the system properties in sorted order.
*
* @param w response writer
*/
private static void performListSystemProperties(PrintWriter w) {
Properties props = System.getProperties();
SortedSet sortedNames = new TreeSet<>(props.stringPropertyNames());
for (String name : sortedNames) {
String value = props.getProperty(name);
emitf(w, "%s = %s", name, value);
}
}
/**
* Lists all the environment variables in sorted order.
*
* @param w response writer
*/
private static void performListEnvironment(PrintWriter w) {
SortedMap vars = new TreeMap<>(System.getenv());
for (Map.Entry var : vars.entrySet()) {
emitf(w, "%s = %s", var.getKey(), var.getValue());
}
}
/**
* Lists all the headers in the incoming request.
*
* @param req Request
* @param w response writer
*/
private static void performListHeaders(HttpServletRequest req, PrintWriter w) {
@SuppressWarnings("unchecked")
List headerNames = Collections.list(req.getHeaderNames());
for (String headerName : headerNames) {
emitf(w, "%s = %s", headerName, req.getHeader(headerName));
}
}
/**
* Checks if the filesystem is read only. Only temp directory should be writable.
*
* @param w response writer
*/
private static void performReadOnlyFSCheck(PrintWriter w) throws IOException {
Path tempFile = Files.createTempFile("temp", ".txt");
try (BufferedWriter tempFileWriter = Files.newBufferedWriter(tempFile, UTF_8)) {
tempFileWriter.append("Writing to temp file");
}
logger.info("Writing to temp file succeeded.");
Path readonlyFile = Paths.get("/readonly.txt");
try (BufferedWriter tempFileWriter = Files.newBufferedWriter(readonlyFile, UTF_8)) {
tempFileWriter.append("Writing to readonly file");
throw new AssertionError("File system is not readonly.");
} catch (IOException ex) {
logger.info("Unable to write to /test.txt as expected. " + ex.getMessage());
}
emitf(w, "Readonly filesystem check: OK");
}
/**
* Lists all processes from /proc and their owners.
*
* @param w response writer
*/
private static void performListProcesses(PrintWriter w) throws IOException {
Path proc = Paths.get("/proc");
try (Stream stream =
Files.list(proc).filter(path -> path.toString().matches("/proc/\\d+"))) {
for (Path path : stream.toArray(Path[]::new)) {
String user = Files.getOwner(path).getName();
Path commFile = Paths.get(path.toAbsolutePath().toString(), "comm");
String processName = Files.readAllLines(commFile).stream().findFirst().orElse("unknown");
emitf(w, "%s:%s", processName, user);
}
}
}
/**
* Lists all the {@link ApiProxy.Environment} attributes in sorted order.
*
* @param w response writer
*/
private static void performListAttributes(PrintWriter w) {
SortedMap attributes = new TreeMap<>(
ApiProxy.getCurrentEnvironment().getAttributes());
for (Map.Entry entry : attributes.entrySet()) {
emitf(w, "%s = %s", entry.getKey(), entry.getValue());
}
}
/**
* Sets some servlet attributes, then lists all servlet attributes. {@code servletAttributeString}
* looks like {@code foo=bar:baz=buh} and is interpreted to mean that the attribute {@code foo}
* should be set to {@code bar}, etc. The reply lists each attribute on its own line, with
* a format like {@code foo = bar}.
*/
private static void performSetServletAttributes(
HttpServletRequest req, String servletAttributeString, PrintWriter w) {
Splitter eq = Splitter.on('=');
Splitter.on(':').splitToStream(servletAttributeString)
.map(eq::splitToList)
.forEach(list -> req.setAttribute(list.get(0), list.get(1)));
@SuppressWarnings("unchecked")
Enumeration names = req.getAttributeNames();
Collections.list(names).stream()
.sorted()
.forEach(attr -> emitf(w, "%s = %s", attr, req.getAttribute(attr)));
}
private static void performFetchProjectIdFromMetadata(PrintWriter w) throws IOException {
URL url = new URL("http://metadata.google.internal/computeMetadata/v1/project/project-id");
String token = fetchMetadata(url);
emitf(w, "Project id: %s", token);
}
private static void performFetchServiceAccountTokenFromMetadata(PrintWriter w)
throws IOException {
URL url = new URL("http://metadata.google.internal/computeMetadata/v1/instance"
+ "/service-accounts/default/token");
String token = fetchMetadata(url);
emitf(w, "Token: %s", token);
}
private static void performFetchServiceAccountScopesFromMetadata(PrintWriter w)
throws IOException {
URL url = new URL("http://metadata.google.internal/computeMetadata/v1/instance"
+ "/service-accounts/default/scopes");
String scopes = fetchMetadata(url);
emitf(w, "Scopes: %s", scopes);
}
/**
* Logs the remaining time for the request, in milliseconds, and also writes it out as part of the
* HTTP response.
*
* Writing the value into the logs is useful when invoking this handler using task queues.
*
* @param w response writer
*/
private static void performLogRemainingTime(PrintWriter w) {
long t = ApiProxy.getCurrentEnvironment().getRemainingMillis();
emitf(w, "Remaining time for request: %d ms", t);
}
/**
* Adds a number of tasks to the default queque.
*
*
Note that special characters in the url will have to be url-encoded when passed as a query
* parameter. E.g. {@code &} must be replaced by {@code %26}. For example, {@code
* /?tasks=1&task_url=/?memcache_loops=10%26size=100} will issue a {@code GET} for {@code
* /?memcache_loops=10&size=100}.
*
* @param count number of tasks to add
* @param url target URL for the task
* @param w response writer
*/
private static void performAddTasks(int count, String url, PrintWriter w) {
emitf(w, "Adding %d tasks for URL %s", count, url);
for (int i = 0; i < count; ++i) {
TaskOptions taskoptions = TaskOptions.Builder
.withMethod(TaskOptions.Method.GET)
.url(url);
QueueFactory.getDefaultQueue().add(taskoptions);
}
logger.info("Done adding tasks");
}
/**
* Post a deferred task to the default queue.
*
* @param w response writer
*/
private static void performAddDeferredTask(PrintWriter w) {
emitf(w, "Adding a deferred task...");
QueueFactory.getDefaultQueue()
.add(TaskOptions.Builder.withPayload(new MyDeferredTaskCallBack()));
logger.info("Done adding deferred task.");
}
/**
* Verify that the deferred task has been called back.
*
* @param w response writer
*/
private static void performVerifyDeferredTask(PrintWriter w) {
try {
emitf(
w,
"Verify deferred task: %b",
MyDeferredTaskCallBack.callBackDone.await(10, TimeUnit.SECONDS));
} catch (InterruptedException ex) {
emitf(w, "Failed to verify the call back to deferred task...");
}
logger.info("Done verifying deferred task.");
}
/** Simple deferred task call back that change a global variable. */
private static class MyDeferredTaskCallBack implements DeferredTask {
// Static variable that will be updated in the deferred task queue call back.
static CountDownLatch callBackDone = new CountDownLatch(1);
@Override
public void run() {
callBackDone.countDown();
System.out.println("Deferred task payload called back.");
}
}
/**
* Access a Cloud SQL database.
*
* @param db database connection string
* @param rows number of rows to insert
* @param columns number of columns in the test table to be created
* @param len length of the values to insert
* @param timeout statement timeout (zero means no timeout)
* @param replace if true, use REPLACE INTO instead of INSERT INTO
* @param w response writer
*/
private void performCloudSqlAccess(
String db, int rows, int columns, int len, int timeout, boolean replace, PrintWriter w)
throws IOException, ServletException {
String dbUrl = "jdbc:google:mysql://" + db;
try {
Class.forName("com.mysql.jdbc.GoogleDriver");
} catch (ClassNotFoundException e) {
throw new ServletException("Error loading Google JDBC Driver", e);
}
logger.info(String.format("connecting to database %s", dbUrl));
try (Connection conn = DriverManager.getConnection(dbUrl)) {
emitf(w, "connected to database %s", dbUrl);
ResultSet result = conn.createStatement().executeQuery("SELECT 1");
result.next();
emit(w, "executed a query, read one result row");
String tableName = String.format("test%s", Integer.toString(random.nextInt(10000)));
conn.createStatement().executeUpdate(drop(tableName));
try {
conn.createStatement().executeUpdate(create(tableName, columns));
emitf(w, "created table %s with %d columns", tableName, columns);
conn.setAutoCommit(false);
try {
conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
PreparedStatement s = conn.prepareStatement(replace ? replaceInto(tableName, columns)
: insertInto(tableName, columns));
s.setQueryTimeout(timeout);
for (int i = 0; i < rows; ++i) {
for (int j = 1; j <= columns; ++j) {
s.setString(j, createRandomString(len));
}
s.addBatch();
}
s.executeBatch();
conn.commit();
} finally {
conn.setAutoCommit(true);
}
emitf(w, "executed %d %s as a batch", rows, replace ? "REPLACE INTO" : "INSERT INTO");
} finally {
conn.createStatement().executeUpdate(drop(tableName));
emitf(w, "dropped table %s", tableName);
}
} catch (SQLException e) {
throw new ServletException("Error executing SQL", e);
}
}
// /**
// * Accesses a Spanner database.
// *
// * @param spannerId spanner ID string
// * @param spannerDb spanner database ID string
// * @param w response writer
// */
// private void performSpannerAccess(String spannerId, String spannerDb, PrintWriter w) {
// SpannerOptions options = SpannerOptions.newBuilder().build();
// Spanner spanner = options.getService();
// try {
// DatabaseClient dbClient =
// spanner.getDatabaseClient(DatabaseId.of(options.getProjectId(), spannerId, spannerDb));
// emitf(w, "connected to spanner db at %s:%s", spannerId, spannerDb);
// try (com.google.cloud.spanner.ResultSet resultSet =
// dbClient.singleUse().executeQuery(Statement.of("SELECT 1"))) {
// emitf(w, "executed select statement %s", "SELECT 1");
// while (resultSet.next()) {
// emitf(w, "result set value: %d", resultSet.getLong(0));
// }
// }
// } finally {
// spanner.close();
// }
// }
/**
* Renders a string into an image using AWT.
*
* @param awtText the string to render
* @param w response writer
*/
private static void performAwtTextRendering(String awtText, PrintWriter w) throws IOException {
int width = 2000;
int height = 400;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics g = image.createGraphics();
JEditorPane jep = new JEditorPane("text/html", awtText);
jep.setSize(width, height);
jep.print(g);
g.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
byte[] output = baos.toByteArray();
emitf(w, "image size: %d", output.length);
}
/**
* Prints JMX info about memory, loaded classes, threads, etc.
*
* @param w response writer
*/
private static void performJmxInfo(PrintWriter w) throws IOException {
MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memory.getHeapMemoryUsage();
emitf(w, "heap.init = %d", heapUsage.getInit());
emitf(w, "heap.used = %d", heapUsage.getUsed());
emitf(w, "heap.max = %d", heapUsage.getMax());
emitf(w, "heap.committed = %d", heapUsage.getCommitted());
MemoryUsage nonHeapUsage = memory.getNonHeapMemoryUsage();
emitf(w, "non_heap.init = %d", nonHeapUsage.getInit());
emitf(w, "non_heap.used = %d", nonHeapUsage.getUsed());
emitf(w, "non_heap.max = %d", nonHeapUsage.getMax());
emitf(w, "non_heap.committed = %d", nonHeapUsage.getCommitted());
ClassLoadingMXBean classLoading = ManagementFactory.getClassLoadingMXBean();
emitf(w, "loaded.classes = %d", classLoading.getLoadedClassCount());
emitf(w, "unloaded.classes = %d", classLoading.getUnloadedClassCount());
emitf(w, "total.loaded.classes = %d", classLoading.getTotalLoadedClassCount());
ThreadMXBean threading = ManagementFactory.getThreadMXBean();
emitf(w, "thread.count = %d", threading.getThreadCount());
emitf(w, "daemon.thread.count = %d", threading.getDaemonThreadCount());
emitf(w, "total.started.thread.count = %d", threading.getTotalStartedThreadCount());
emitf(w, "peak.thread.count = %d", threading.getPeakThreadCount());
CompilationMXBean compilation = ManagementFactory.getCompilationMXBean();
emitf(w, "compiler.time = %d", compilation.getTotalCompilationTime());
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
String name = gc.getName().replace(" ", "_");
emitf(w, "gc.%s.count = %d", name, gc.getCollectionCount());
emitf(w, "gc.%s.time = %d", name, gc.getCollectionTime());
}
for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
String name = pool.getName().replace(" ", "_");
emitf(w, "memory.%s.type = %s", name, pool.getType().name().toLowerCase());
emitf(w, "memory.%s.used = %d", name, pool.getUsage().getUsed());
emitf(w, "memory.%s.max = %d", name, pool.getUsage().getMax());
emitf(w, "memory.%s.peak.used = %d", name, pool.getPeakUsage().getUsed());
emitf(w, "memory.%s.peak.max = %d", name, pool.getPeakUsage().getMax());
}
}
/**
* Lists all writable JMX VM options from HotSpotDiagnosticMXBean.
*
* @param w response writer
*/
private static void performJmxListVmOptions(PrintWriter w) throws IOException {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
HotSpotDiagnosticMXBean bean = ManagementFactory.newPlatformMXBeanProxy(server,
"com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class);
for (VMOption option : bean.getDiagnosticOptions()) {
emitf(w, "%s = %s", option.getName(), option.getValue());
}
}
/**
* Emits thread dump information.
*
* @param w response writer
*/
private static void performJmxThreadDump(PrintWriter w) throws IOException {
ThreadMXBean threading = ManagementFactory.getThreadMXBean();
for (ThreadInfo i : threading.dumpAllThreads(false, false)) {
emitf(w, "%s", i.toString());
}
}
private static String drop(String tableName) {
return String.format("DROP TABLE IF EXISTS %s", tableName);
}
private static String create(String tableName, int columns) {
StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE ");
sb.append(tableName);
sb.append(" (");
for (int j = 1; j <= columns; ++j) {
if (j != 1) {
sb.append(", ");
}
sb.append("s");
sb.append(j);
sb.append(" VARCHAR(255)");
}
sb.append(")");
return sb.toString();
}
private static String insertInto(String tableName, int columns) {
return String.format("INSERT INTO %s VALUES (?" + Strings.repeat(",?", columns - 1) + ")",
tableName);
}
private static String replaceInto(String tableName, int columns) {
return String.format("REPLACE INTO %s VALUES (?" + Strings.repeat(",?", columns - 1) + ")",
tableName);
}
/**
* Returns the contents of the metadata item at the specified url.
*
* @param url The url to fetch, usually starting with {@code http://metadata.google.internal}.
* @throws IOException In case of error. The exception message will contain the server response.
*/
private static String fetchMetadata(URL url) throws IOException {
String data = null;
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) url.openConnection();
connection.setRequestProperty("Metadata-Flavor", "Google");
InputStream input = connection.getInputStream();
if (connection.getResponseCode() == 200) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, UTF_8))) {
data = Joiner.on("\n").join(CharStreams.readLines(reader));
}
}
} catch (IOException e) {
if (connection != null) {
IOException newException;
try {
InputStream input = connection.getErrorStream();
if (input != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, UTF_8))) {
String error = Joiner.on("\n").join(CharStreams.readLines(reader));
newException = new IOException("Failed to fetch metadata: " + error);
}
} else {
newException = e;
}
} catch (IOException e2) {
newException = e2;
}
throw newException;
}
}
return data;
}
/**
* Returns an Integer corresponding to the value of a request parameter.
*
* @param name the name of the request parameter to parse
* @param req the HttpServletRequest
* @return the value of the specified parameter, or null if there is no such parameter
* @throws ServletException if the parameter has an invalid value
*/
private static Integer getIntParameter(String name, HttpServletRequest req)
throws ServletException {
String value = req.getParameter(name);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new ServletException("parameter " + name + "is not a valid integer");
}
}
return null;
}
/**
* Runs an action repeatedly until at least the specified number of milliseconds has elapsed.
*
* @param ms the minimum elapsed time in milliseconds
* @param action the action to run
*/
private static void runRepeatedly(int ms, Runnable action) {
long remaining = ms;
while (remaining > 0) {
long start = System.currentTimeMillis();
action.run();
long stop = System.currentTimeMillis();
remaining -= (stop - start);
}
}
/**
* Validate all request query parameters against the list of supported parameters.
*
* @param req servlet request
* @throws ServletException in case of unrecognized parameters
*/
private static void validateParameters(HttpServletRequest req) throws ServletException {
@SuppressWarnings("unchecked") // legacy API returns raw Enumeration
List parameterNames = Collections.list(req.getParameterNames());
Set params = Sets.newTreeSet(parameterNames);
Set invalidParams = Sets.difference(params, VALID_PARAMETERS);
if (!invalidParams.isEmpty()) {
throw new ServletException("unrecognized query parameters: "
+ Joiner.on(",").join(invalidParams));
}
}
/**
* Log a message at INFO level, then write it to the response as HTML.
*
* @param w response writer
* @param msg the message to emit
*/
private static void emit(PrintWriter w, String msg) {
logger.info(msg);
w.printf("%s\n", msg);
}
/**
* Format and log a message at INFO level, then write it to the response as HTML.
*
* The message is formatted using {@code String.format}
*
* @param w response writer
* @param format the format string to use
* @param args arguments to use
*/
@FormatMethod
private static void emitf(PrintWriter w, String format, Object... args) {
emit(w, String.format(format, args));
}
/**
* Returns a random string of the specified length.
*
* @param size the desired length for the string
*/
private String createRandomString(int size) {
byte[] bytes = new byte[size];
for (int i = 0; i < size; ++i) {
bytes[i] = (byte) (random.nextInt(127 - 32) + 32);
}
return new String(bytes, US_ASCII_CHARSET);
}
}