package org.basex.rest;
import static org.basex.core.users.UserText.*;
import static org.basex.util.http.HttpMethod.*;
import static org.junit.Assert.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
import org.basex.*;
import org.basex.api.client.*;
import org.basex.core.cmd.*;
import org.basex.util.*;
import org.basex.util.http.*;
import org.basex.util.list.*;
import org.junit.*;
import org.junit.Test;
/**
* Concurrency tests of BaseX REST API.
*
* @author BaseX Team 2005-17, BSD License
* @author Dimitar Popov
*/
public final class RESTConcurrencyTest extends SandboxTest {
/** HTTP server. */
private static BaseXHTTP http;
/** Time-out in (ms): increase if running on a slower system. */
private static final long TIMEOUT = 600;
/** Socket time-out in (ms). */
private static final int SOCKET_TIMEOUT = 3000;
/** BaseX HTTP base URL. */
private static final String BASE_URL = REST_ROOT + NAME;
/**
* Create a test database and start BaseXHTTP.
* @throws Exception if database cannot be created or server cannot be started
*/
@BeforeClass
public static void setUp() throws Exception {
final StringList sl = new StringList();
sl.add("-p" + DB_PORT, "-h" + HTTP_PORT, "-s" + STOP_PORT, "-z").add("-U" + ADMIN);
http = new BaseXHTTP(sl.toArray());
try(ClientSession cs = createClient()) {
cs.execute(new CreateDB(NAME));
}
}
/**
* Drop the test database and stop BaseXHTTP.
* @throws Exception if database cannot be dropped or server cannot be stopped
*/
@AfterClass
public static void tearDown() throws Exception {
try(ClientSession cs = createClient()) {
cs.execute(new DropDB(NAME));
}
http.stop();
}
/**
* Test 2 concurrent readers (GH-458).
* <p><b>Test case:</b>
* <ol>
* <li/>start a long running reader;
* <li/>start a fast reader: it should succeed.
* </ol>
* @throws Exception error during request execution
*/
@Test
public void testMultipleReaders() throws Exception {
final String number = "63177";
final String slowQuery = "?query=(1%20to%20100000000000)%5b.=0%5d";
final String fastQuery = "?query=" + number;
final Get slowAction = new Get(slowQuery);
final Get fastAction = new Get(fastQuery);
final ExecutorService exec = Executors.newFixedThreadPool(2);
final Future<HTTPResponse> slow = exec.submit(slowAction);
Performance.sleep(TIMEOUT); // delay in order to be sure that the reader has started
final Future<HTTPResponse> fast = exec.submit(fastAction);
try {
final HTTPResponse result = fast.get();
assertEquals(HTTPCode.OK, result.status);
assertEquals(number, result.data);
} finally {
slowAction.stop = true;
slow.get();
}
}
/**
* Test concurrent reader and writer (GH-458).
* <p><b>Test case:</b>
* <ol>
* <li/>start a long running reader;
* <li/>try to start a writer: it should time out;
* <li/>stop the reader;
* <li/>start the writer again: it should succeed.
* </ol>
* @throws Exception error during request execution
*/
@Test
@Ignore("There is no way to stop a query on the server!")
public void testReaderWriter() throws Exception {
final String readerQuery = "?query=(1%20to%20100000000000000)%5b.=0%5d";
final String writerQuery = "/test.xml";
final byte[] content = Token.token("<a/>");
final Get readerAction = new Get(readerQuery);
final Put writerAction = new Put(writerQuery, content);
final ExecutorService exec = Executors.newFixedThreadPool(2);
// start reader
exec.submit(readerAction);
Performance.sleep(TIMEOUT); // delay in order to be sure that the reader has started
// start writer
Future<HTTPResponse> writer = exec.submit(writerAction);
try {
final HTTPResponse result = writer.get(TIMEOUT, TimeUnit.MILLISECONDS);
if(result.status.isSuccess()) fail("Database modified while a reader is running");
throw new Exception(result.toString());
} catch(final TimeoutException e) {
// writer is blocked by the reader: stop it
writerAction.stop = true;
}
// stop reader
readerAction.stop = true;
// start the writer again
writer = exec.submit(writerAction);
assertEquals(HTTPCode.CREATED, writer.get().status);
}
/**
* Test concurrent writers (GH-458).
* <p><b>Test case:</b>
* <ol>
* <li/>start several writers one after another;
* <li/>all writers should succeed.
* </ol>
* @throws Exception error during request execution
*/
@Test
public void testMultipleWriters() throws Exception {
final int count = 10;
final String template =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<command xmlns=\"http://basex.org/rest\"><text><![CDATA[" +
"ADD TO %1$d <node id=\"%1$d\"/>" +
"]]></text></command>";
final ArrayList<Future<HTTPResponse>> tasks = new ArrayList<>();
final ExecutorService exec = Executors.newFixedThreadPool(count);
// start all writers (not at the same time, but still in parallel)
for(int i = 0; i < count; i++) {
final String command = String.format(template, i);
tasks.add(exec.submit(new Post("", Token.token(command))));
}
// check if all have finished successfully
for(final Future<HTTPResponse> task : tasks) {
assertEquals(HTTPCode.OK, task.get(TIMEOUT, TimeUnit.MILLISECONDS).status);
}
}
// REST API:
/** REST GET request. */
private static class Get implements Callable<HTTPResponse> {
/** Request URI. */
protected final URI uri;
/** Stop signal. */
public volatile boolean stop;
/**
* Construct a new GET request.
* @param request request string without the base URI
*/
Get(final String request) {
uri = URI.create(BASE_URL + request);
}
@Override
public HTTPResponse call() throws Exception {
final HttpURLConnection hc = (HttpURLConnection) uri.toURL().openConnection();
hc.setReadTimeout(SOCKET_TIMEOUT);
try {
while(!stop) {
try {
final int code = hc.getResponseCode();
final InputStream input = hc.getInputStream();
final ByteList bl = new ByteList();
for(int i; (i = input.read()) != -1;) bl.add(i);
return new HTTPResponse(code, bl.toString());
} catch(final SocketTimeoutException ignore) { }
}
return null;
} finally {
hc.disconnect();
}
}
}
/** REST PUT request. */
private static class Put implements Callable<HTTPResponse> {
/** Request URI. */
private final URI uri;
/** Content to send to the server. */
private final byte[] data;
/** HTTP method. */
protected final HttpMethod method;
/** Stop signal. */
public volatile boolean stop;
/**
* Construct a new PUT request.
* @param request request string without the base URI
* @param data data to send to the server
*/
Put(final String request, final byte[] data) {
this(request, data, PUT);
}
/**
* Construct a new request.
* @param request request string without the base URI
* @param data data to send to the server
* @param method HTTP method
*/
protected Put(final String request, final byte[] data, final HttpMethod method) {
this.data = data;
this.method = method;
uri = URI.create(BASE_URL + request);
}
@Override
public HTTPResponse call() throws Exception {
final HttpURLConnection hc = (HttpURLConnection) uri.toURL().openConnection();
try {
hc.setDoOutput(true);
hc.setRequestMethod(method.name());
hc.setRequestProperty(HttpText.CONTENT_TYPE, MediaType.APPLICATION_XML.toString());
hc.getOutputStream().write(data);
hc.getOutputStream().close();
hc.setReadTimeout(SOCKET_TIMEOUT);
while(!stop) {
try {
return new HTTPResponse(hc.getResponseCode());
} catch(final SocketTimeoutException ignore) { }
}
return null;
} finally {
hc.disconnect();
}
}
}
/** REST POST request. */
private static class Post extends Put {
/**
* Construct a new POST request.
* @param request request string without the base URI
* @param data data to send to the server
*/
Post(final String request, final byte[] data) {
super(request, data, POST);
}
}
// Toolbox
/** Simple HTTP response. */
private static class HTTPResponse {
/** Status code. */
public final HTTPCode status;
/** Response data or {@code null} if no data was returned. */
public final String data;
/**
* Constructor.
* @param code HTTP response status code
*/
HTTPResponse(final int code) {
this(code, null);
}
/**
* Constructor.
* @param code HTTP response status code
* @param data data
*/
HTTPResponse(final int code, final String data) {
this.data = data;
status = HTTPCode.valueOf(code);
}
}
/** HTTP response codes. */
private enum HTTPCode {
/** 100: Continue. */
CONTINUE(100, "Continue"),
/** 200: OK. */
OK(200, "OK"),
/** 201: Created. */
CREATED(201, "Created"),
/** 400: Bad Request. */
BAD_REQUEST(400, "Bad Request"),
/** 401: Unauthorized. */
UNAUTHORIZED(401, "Unauthorized"),
/** 403: Forbidden. */
FORBIDDEN(403, "Forbidden"),
/** 404: Not Found. */
NOT_FOUND(403, "Not Found");
/** HTTP response code. */
public final int code;
/** HTTP response message. */
public final String message;
/**
* Constructor.
* @param code code
* @param message message
*/
HTTPCode(final int code, final String message) {
this.code = code;
this.message = message;
}
/**
* Is the current code a "Success" code?
* @return {@code true} if the current code is a "Success" code
*/
public boolean isSuccess() {
return code >= 200 && code < 300;
}
@Override
public String toString() {
return code + ": " + message;
}
/**
* Get the enum value given the numeric code.
* @param code HTTP response code
* @return enum value
*/
public static HTTPCode valueOf(final int code) {
for(final HTTPCode h : HTTPCode.values()) {
if(h.code == code) return h;
}
return null;
}
}
}