Browse Source

Merge branch 'servlet3-cass' of http://github.com/marko-asplund/FrameworkBenchmarks into 918

James Yen 11 years ago
parent
commit
eef076932b

+ 14 - 0
servlet3-cass/README.md

@@ -0,0 +1,14 @@
+
+# Servlet3 API benchmarking test
+
+Framework permutation based on the following technology stack
+
+* Java
+* Resin
+* Servlet 3 with asynchronous processing
+* Apache Cassandra database
+* Jackson 2 for JSON processing
+
+Currently implements test types 1, 2, 3, 5 and 6.
+
+

+ 0 - 0
servlet3-cass/__init__.py


+ 43 - 0
servlet3-cass/benchmark_config

@@ -0,0 +1,43 @@
+{
+  "framework": "servlet3-cass",
+  "tests": [{
+    "default": {
+      "setup_file": "setup",
+      "json_url": "/servlet3-cass/json",
+      "plaintext_url": "/servlet3-cass/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "None",
+      "framework": "servlet3-cass",
+      "language": "Java",
+      "orm": "Raw",
+      "platform": "Servlet",
+      "webserver": "Resin",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "servlet3-cass",
+      "notes": "",
+      "versus": "servlet3-cass"
+    },
+    "raw": {
+      "setup_file": "setup",
+      "db_url": "/servlet3-cass/db",
+      "query_url": "/servlet3-cass/db?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "Apache Cassandra",
+      "framework": "servlet3-cass",
+      "language": "Java",
+      "orm": "Raw",
+      "platform": "Servlet",
+      "webserver": "Resin",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "servlet3-cass",
+      "notes": "",
+      "versus": "servlet3-cass"
+    }
+  }]
+}

+ 3 - 0
servlet3-cass/install.sh

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+fw_depends java resin

+ 93 - 0
servlet3-cass/pom.xml

@@ -0,0 +1,93 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>fi.markoa.tfb.servlet3</groupId>
+    <artifactId>servlet3-cass</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>war</packaging>
+    <name>servlet3-cass</name>
+
+    <properties>
+        <sourceEncoding>UTF-8</sourceEncoding>
+        <java.version>1.7</java.version>
+
+        <slf4j.version>1.7.7</slf4j.version>
+        <logback.version>1.1.2</logback.version>
+    </properties>
+
+    <build>
+        <finalName>servlet3-cass</finalName>
+
+        <plugins>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.1</version>
+                <configuration>
+                    <source>${java.version}</source>
+                    <target>${java.version}</target>
+                    <encoding>${sourceEncoding}</encoding>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>2.4</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                </configuration>
+            </plugin>
+
+        </plugins>
+
+    </build>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>3.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>jstl</artifactId>
+            <version>1.2</version>
+            <scope>runtime</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.4.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.datastax.cassandra</groupId>
+            <artifactId>cassandra-driver-core</artifactId>
+            <version>2.0.3</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <!--
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>${logback.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        -->
+
+    </dependencies>
+
+</project>

+ 32 - 0
servlet3-cass/setup.py

@@ -0,0 +1,32 @@
+
+import subprocess
+import sys
+import os
+import setup_util
+
+def start(args, logfile, errfile):
+  setup_util.replace_text("servlet3-cass/src/main/resources/application.properties", "localhost", args.database_host)
+
+  try:
+    subprocess.check_call("mvn clean compile war:war", shell=True, cwd="servlet3-cass", stderr=errfile, stdout=logfile)
+    if os.name == 'nt':
+      subprocess.check_call("rmdir /s /q C:\\Java\\resin\\webapps", shell=True, stderr=errfile, stdout=logfile)
+      subprocess.check_call("mkdir C:\\Java\\resin\\webapps", shell=True, stderr=errfile, stdout=logfile)
+      subprocess.check_call("cp servlet3-cass\\target\\servlet3-cass.war C:\\Java\\resin\\webapps\\servlet3-cass.war", shell=True, stderr=errfile, stdout=logfile)
+      subprocess.check_call("C:\\Java\\resin\\bin\\start.bat", shell=True, stderr=errfile, stdout=logfile)
+      return 0
+    subprocess.check_call("rm -rf $RESIN_HOME/webapps/*", shell=True, stderr=errfile, stdout=logfile)
+    subprocess.check_call("cp servlet/target/servlet3-cass.war $RESIN_HOME/webapps/", shell=True, stderr=errfile, stdout=logfile)
+    subprocess.check_call("$RESIN_HOME/bin/resinctl start", shell=True, stderr=errfile, stdout=logfile)
+    return 0
+  except subprocess.CalledProcessError:
+    return 1
+def stop(logfile, errfile):
+  try:
+    if os.name == 'nt':
+      subprocess.check_call("C:\\Java\\resin\\bin\\stop.bat", shell=True, stderr=errfile, stdout=logfile)
+      return 0
+    subprocess.check_call("$RESIN_HOME/bin/resinctl shutdown", shell=True, stderr=errfile, stdout=logfile)
+    return 0
+  except subprocess.CalledProcessError:
+    return 1

+ 24 - 0
servlet3-cass/source_code

@@ -0,0 +1,24 @@
+./servlet3-cass/src/main
+./servlet3-cass/src/main/java
+./servlet3-cass/src/main/java/fi
+./servlet3-cass/src/main/java/fi/markoa
+./servlet3-cass/src/main/java/fi/markoa/tfb
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/AsyncErrorServlet1.java
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/AsyncErrorServlet2.java
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/DatabaseBaseServlet.java
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/DatabaseQueriesServlet.java
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/DatabaseQueryServlet.java
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/DatabaseUpdatesServlet.java
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/HelloMessage.java
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/JsonSerializationServlet.java
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/MessageDAO.java
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/MessageDAOCassImpl.java
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/PlaintextServlet.java
+./servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/World.java
+./servlet3-cass/src/main/resources
+./servlet3-cass/src/main/resources/application.properties
+./servlet3-cass/src/main/resources/logback.xml
+./servlet3-cass/src/main/webapp
+./servlet3-cass/src/main/webapp/jsp
+./servlet3-cass/src/main/webapp/jsp/error.jsp

+ 105 - 0
servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/DatabaseBaseServlet.java

@@ -0,0 +1,105 @@
+package fi.markoa.tfb.servlet3;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Base class for Web Framework Benchmarks database test type implementations.
+ *
+ * @author marko asplund
+ */
+public abstract class DatabaseBaseServlet extends HttpServlet {
+  private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseBaseServlet.class);
+  protected static final ObjectMapper mapper = new ObjectMapper();
+  protected static final String MEDIATYPE_APPLICATION_JSON = "application/json";
+  protected static final int WORLD_LEAST_VALUE = 1;
+  protected static final int WORLD_BOUND_VALUE = 10000;
+
+  protected static final ListeningExecutorService executorService =
+    MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+  protected MessageDAOCassImpl dao;
+
+  @Override
+  public void init(ServletConfig config) throws ServletException {
+    dao = new MessageDAOCassImpl();
+    dao.init(executorService);
+  }
+
+  /**
+   * callback for sending the response back to the client
+   *
+   * @param asyncContext Servlet asynchronous context
+   * @param future ListenableFuture holding the backend response
+   * @param executor ExecutorService instance for executing the ListenableFuture
+   */
+  protected void addResponseCallback(final AsyncContext asyncContext, ListenableFuture<?> future, Executor executor) {
+    Futures.addCallback(future, new FutureCallback<Object>() {
+      @Override
+      public void onSuccess(Object world) {
+        try {
+          mapper.writeValue(asyncContext.getResponse().getOutputStream(), world);
+        } catch (IOException ex) {
+          LOGGER.error("failed to get output stream", ex);
+        }
+        asyncContext.complete();
+      }
+
+      @Override
+      public void onFailure(Throwable th) {
+        LOGGER.error("failed to read data", th);
+        errorDispatch(asyncContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "failed to read data: "+th.getMessage());
+      }
+    }, executor);
+  }
+
+  protected void errorDispatch(AsyncContext asyncContext, int statusCode, String message) {
+    asyncContext.getRequest().setAttribute("statusCode", statusCode);
+    asyncContext.getRequest().setAttribute("message", message);
+    asyncContext.dispatch("/jsp/error.jsp");
+  }
+
+  protected int getQueries(String queries) {
+    int q;
+    if(queries == null) {
+      return 1;
+    }
+    try {
+      q = Integer.parseInt(queries);
+    } catch (NumberFormatException ex) {
+      return 1;
+    }
+    if(q > 500)
+      return 500;
+    else if(q < 1)
+      return 1;
+
+    return q;
+  }
+
+  protected List<Integer> generateRandomNumbers(int count, int least, int bound) {
+    List<Integer> ids = new ArrayList<>();
+    for(int cnt = 0; cnt < count; cnt++)
+      ids.add(ThreadLocalRandom.current().nextInt(least, bound));
+    return ids;
+  }
+
+  @Override
+  public void destroy() {
+    dao.destroy();
+    executorService.shutdown();
+  }
+}

+ 33 - 0
servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/DatabaseQueriesServlet.java

@@ -0,0 +1,33 @@
+package fi.markoa.tfb.servlet3;
+
+import com.google.common.util.concurrent.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Web Framework Benchmarks
+ * Test type 3: Multiple database queries
+ *
+ * @author marko asplund
+ */
+@WebServlet(urlPatterns={"/queries"}, asyncSupported=true)
+public class DatabaseQueriesServlet extends DatabaseBaseServlet {
+  private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseQueriesServlet.class);
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+    resp.setContentType(MEDIATYPE_APPLICATION_JSON);
+    final AsyncContext asyncContext = req.startAsync();
+    ListenableFuture<?> future = dao.read(generateRandomNumbers(getQueries(req.getParameter("queries")),
+      WORLD_LEAST_VALUE, WORLD_BOUND_VALUE+1));
+    addResponseCallback(asyncContext, future, executorService);
+  }
+
+}

+ 34 - 0
servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/DatabaseQueryServlet.java

@@ -0,0 +1,34 @@
+package fi.markoa.tfb.servlet3;
+
+import com.google.common.util.concurrent.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Web Framework Benchmarks
+ * Test type 2: Single database query
+ *
+ * @author marko asplund
+ */
+@WebServlet(urlPatterns={"/db"}, asyncSupported=true)
+public class DatabaseQueryServlet extends DatabaseBaseServlet {
+  private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseQueryServlet.class);
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+    resp.setContentType(MEDIATYPE_APPLICATION_JSON);
+    AsyncContext asyncContext = req.startAsync();
+    int randId = ThreadLocalRandom.current().nextInt(WORLD_LEAST_VALUE, WORLD_BOUND_VALUE+1);
+    ListenableFuture<?> future = dao.read(randId);
+    addResponseCallback(asyncContext, future, executorService);
+  }
+
+}

+ 86 - 0
servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/DatabaseUpdatesServlet.java

@@ -0,0 +1,86 @@
+package fi.markoa.tfb.servlet3;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * Web Framework Benchmarks
+ * Test type 5: Database updates
+ *
+ * @author marko asplund
+ */
+
+@WebServlet(urlPatterns={"/updates"}, asyncSupported=true)
+public class DatabaseUpdatesServlet extends DatabaseBaseServlet {
+  private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseUpdatesServlet.class);
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+    resp.setContentType(MEDIATYPE_APPLICATION_JSON);
+    final int queries = getQueries(req.getParameter("queries"));
+    final AsyncContext asyncContext = req.startAsync();
+    ListenableFuture<List<World>> readFuture = dao.read(generateRandomNumbers(queries,
+      WORLD_LEAST_VALUE, WORLD_BOUND_VALUE+1));
+    final ListenableFuture<List<Integer>> newRandomsFuture = generateRandomNumbersFuture(queries,
+      WORLD_LEAST_VALUE, WORLD_BOUND_VALUE+1);
+
+    Futures.addCallback(readFuture, new FutureCallback<List<World>>() {
+      @Override
+      public void onSuccess(List<World> worlds) {
+        List<Integer> newRandoms;
+        try {
+          newRandoms = newRandomsFuture.get();
+        } catch (InterruptedException | ExecutionException ex) {
+          LOGGER.error("failed to generate random numbers", ex);
+          errorDispatch(asyncContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "failed to generate random numbers"+ex.getMessage());
+          return;
+        }
+        List<World> newWorlds = new ArrayList<>();
+        for(int i = 0; i < worlds.size(); i++)
+          newWorlds.add(new World(worlds.get(i).getId(), newRandoms.get(i)));
+        dao.update(newWorlds);
+
+        try {
+          mapper.writeValue(asyncContext.getResponse().getOutputStream(), newWorlds);
+        } catch (IOException ex) {
+          LOGGER.error("failed to get output stream", ex);
+        }
+        asyncContext.complete();
+
+        LOGGER.debug("update done");
+      }
+
+      @Override
+      public void onFailure(Throwable th) {
+        LOGGER.error("update failed", th);
+        errorDispatch(asyncContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "update failed: "+th.getMessage());
+      }
+    }, executorService);
+
+  }
+
+  protected ListenableFuture<List<Integer>> generateRandomNumbersFuture(final int count, final int least, final int bound) {
+    return executorService.submit(new Callable<List<Integer>>() {
+      @Override
+      public List<Integer> call() throws Exception {
+        return generateRandomNumbers(count, least, bound);
+      }
+    });
+  }
+
+}

+ 12 - 0
servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/HelloMessage.java

@@ -0,0 +1,12 @@
+package fi.markoa.tfb.servlet3;
+
+public class HelloMessage {
+  private String message = "Hello, World!";
+
+  public HelloMessage() {
+  }
+
+  public String getMessage() {
+    return message;
+  }
+}

+ 33 - 0
servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/JsonSerializationServlet.java

@@ -0,0 +1,33 @@
+package fi.markoa.tfb.servlet3;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Web Framework Benchmarks
+ * Test type 1: JSON serialization
+ *
+ * @author marko asplund
+ */
+@WebServlet("/json")
+public class JsonSerializationServlet extends HttpServlet {
+  private static final Logger LOGGER = LoggerFactory.getLogger(JsonSerializationServlet.class);
+  private static final ObjectMapper mapper = new ObjectMapper();
+  private static final String MEDIATYPE_APPLICATION_JSON = "application/json";
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+    LOGGER.debug("doGet");
+    resp.setContentType(MEDIATYPE_APPLICATION_JSON);
+    mapper.writeValue(resp.getOutputStream(), new HelloMessage());
+  }
+
+}

+ 14 - 0
servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/MessageDAO.java

@@ -0,0 +1,14 @@
+package fi.markoa.tfb.servlet3;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+import java.util.List;
+
+public interface MessageDAO {
+  void init(ListeningExecutorService executorService);
+  ListenableFuture<World> read(int id);
+  ListenableFuture<List<World>> read(List<Integer> ids);
+  ListenableFuture<Void> update(List<World> worlds);
+  void destroy();
+}

+ 93 - 0
servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/MessageDAOCassImpl.java

@@ -0,0 +1,93 @@
+package fi.markoa.tfb.servlet3;
+
+import com.datastax.driver.core.*;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+
+/**
+ * Cassandra data access implementation class for the "World" domain model.
+ *
+ * @author marko asplund
+ */
+public class MessageDAOCassImpl implements MessageDAO {
+  private static final Logger LOGGER = LoggerFactory.getLogger(MessageDAOCassImpl.class);
+  private static final String CONFIG_FILE_NAME= "/application.properties";
+  private Cluster cluster;
+  private Session session;
+  private Map<String, PreparedStatement> statements;
+
+  @Override
+  public void init(ListeningExecutorService executorService) {
+    LOGGER.debug("init()");
+
+    Properties conf;
+    try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(CONFIG_FILE_NAME)) {
+      if(is == null)
+        throw new IOException("file not found: "+CONFIG_FILE_NAME);
+      conf = new Properties();
+      conf.load(is);
+    } catch (IOException ex) {
+      LOGGER.error("failed to open config file", ex);
+      throw new RuntimeException(ex);
+    }
+
+    cluster = Cluster.builder()
+      .addContactPoint(conf.getProperty("cassandra.host"))
+//      .withCredentials(conf.getProperty("cassandra.user"), conf.getProperty("cassandra.pwd"))
+      .build();
+    session = cluster.connect(conf.getProperty("cassandra.keyspace"));
+
+    Map<String, PreparedStatement> stmts = new HashMap<>();
+    stmts.put("get_by_id", session.prepare("SELECT randomnumber FROM world WHERE id=?"));
+    stmts.put("update_by_id", session.prepare("UPDATE world SET randomnumber=? WHERE id=?"));
+    statements = Collections.unmodifiableMap(stmts);
+  }
+
+  @Override
+  public ListenableFuture<World> read(final int id) {
+    Function<ResultSet, World> transformation = new Function<ResultSet, World>() {
+      @Override
+      public World apply(ResultSet results) {
+        Row r = results.one();
+        return new World(id, r.getInt("randomnumber"));
+      }
+    };
+    return Futures.transform(session.executeAsync(statements.get("get_by_id").bind(id)), transformation);
+  }
+
+  public ListenableFuture<List<World>> read(List<Integer> ids) {
+    List<ListenableFuture<World>> futures = new ArrayList<>();
+    for(Integer id : ids)
+      futures.add(read(id));
+    return Futures.allAsList(futures);
+  }
+
+  public ListenableFuture<Void> update(List<World> worlds) {
+    Function<ResultSet, Void> transformation = new Function<ResultSet, Void>() {
+      @Override
+      public Void apply(ResultSet rows) {
+        return null;
+      }
+    };
+    BatchStatement bs = new BatchStatement(BatchStatement.Type.UNLOGGED);
+    for(World w : worlds)
+      bs.add(statements.get("update_by_id").bind(w.getId(), w.getRandomNumber()));
+    return Futures.transform(session.executeAsync(bs), transformation);
+  }
+
+  @Override
+  public void destroy() {
+    LOGGER.debug("destroy()");
+    session.close();
+    cluster.close();
+  }
+
+}

+ 32 - 0
servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/PlaintextServlet.java

@@ -0,0 +1,32 @@
+package fi.markoa.tfb.servlet3;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Web Framework Benchmarks
+ * Test type 6: Plaintext
+ *
+ * @author marko asplund
+ */
+@WebServlet("/plaintext")
+public class PlaintextServlet extends HttpServlet {
+  private static final Logger LOGGER = LoggerFactory.getLogger(PlaintextServlet.class);
+  private static final String MEDIATYPE_TEXT_PLAIN = "text/plain";
+  private static final byte[] CONTENT = "Hello, World!".getBytes();
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+    LOGGER.debug("doGet");
+    resp.setContentType(MEDIATYPE_TEXT_PLAIN);
+    resp.getOutputStream().write(CONTENT);
+  }
+
+}

+ 27 - 0
servlet3-cass/src/main/java/fi/markoa/tfb/servlet3/World.java

@@ -0,0 +1,27 @@
+package fi.markoa.tfb.servlet3;
+
+public class World {
+  private int id;
+  private int randomNumber;
+
+  public World(int id, int randomNumber) {
+    this.id = id;
+    this.randomNumber = randomNumber;
+  }
+
+  public int getId() {
+    return id;
+  }
+
+  @Override
+  public String toString() {
+    return "World{" +
+      "id=" + id +
+      ", randomNumber=" + randomNumber +
+      '}';
+  }
+
+  public int getRandomNumber() {
+    return randomNumber;
+  }
+}

+ 2 - 0
servlet3-cass/src/main/resources/application.properties

@@ -0,0 +1,2 @@
+cassandra.host=localhost
+cassandra.keyspace=tfb

+ 16 - 0
servlet3-cass/src/main/resources/logback.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
+        <file>servlet3-cass.log</file>
+        <append>true</append>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{35} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <logger name="fi.markoa.tfb.servlet3" level="DEBUG"/>
+
+    <root level="INFO">
+        <appender-ref ref="FILE" />
+    </root>
+</configuration>

+ 17 - 0
servlet3-cass/src/main/webapp/jsp/error.jsp

@@ -0,0 +1,17 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ page session="false" %>
+<%
+    if(request.getAttribute("statusCode") != null)
+        response.setStatus((Integer)request.getAttribute("statusCode"));
+    else
+        response.setStatus(500);
+%>
+<html>
+<head>
+<title>error</title>
+</head>
+<body>
+<h1>error</h1>
+${message}
+</body>
+</html>