/*
* Copyright © 2014-2015 Cask Data, Inc.
*
* 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
*
* http://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 co.cask.cdap.test.app;
import co.cask.cdap.api.TxRunnable;
import co.cask.cdap.api.annotation.UseDataSet;
import co.cask.cdap.api.app.AbstractApplication;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.data.DatasetContext;
import co.cask.cdap.api.dataset.lib.KeyValueTable;
import co.cask.cdap.api.service.AbstractService;
import co.cask.cdap.api.service.BasicService;
import co.cask.cdap.api.service.http.AbstractHttpServiceHandler;
import co.cask.cdap.api.service.http.HttpServiceRequest;
import co.cask.cdap.api.service.http.HttpServiceResponder;
import co.cask.cdap.api.worker.AbstractWorker;
import co.cask.cdap.api.worker.WorkerContext;
import co.cask.cdap.common.utils.Tasks;
import com.google.common.base.Charsets;
import com.google.common.io.ByteStreams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
/**
* AppWithServices with a CentralService, which other programs will hit via their context's getServiceURL method.
* This CentralService returns a constant string value {@link #ANSWER}, which is checked for in the test cases.
*/
public class AppUsingGetServiceURL extends AbstractApplication {
public static final String APP_NAME = "AppUsingGetServiceURL";
public static final String CENTRAL_SERVICE = "CentralService";
public static final String LIFECYCLE_WORKER = "LifecycleWorker";
public static final String PINGING_WORKER = "PingingWorker";
public static final String FORWARDING = "ForwardingService";
public static final String ANSWER = "MagicalString";
public static final String DATASET_NAME = "SharedDataSet";
public static final String DATASET_KEY = "Key";
public static final String WORKER_INSTANCES_DATASET = "WorkerInstancesDataset";
@Override
public void configure() {
setName(APP_NAME);
addService(new BasicService("ForwardingService", new ForwardingHandler()));
addService(new CentralService());
addWorker(new PingingWorker());
addWorker(new LifecycleWorker());
createDataset(DATASET_NAME, KeyValueTable.class);
createDataset(WORKER_INSTANCES_DATASET, KeyValueTable.class);
}
/**
*
*/
public static final class ForwardingHandler extends AbstractHttpServiceHandler {
@SuppressWarnings("unused")
@UseDataSet(DATASET_NAME)
private KeyValueTable table;
@GET
@Path("ping")
public void ping(HttpServiceRequest request, HttpServiceResponder responder) throws IOException {
// Discover the CatalogLookup service via discovery service
URL serviceURL = getContext().getServiceURL(CENTRAL_SERVICE);
if (serviceURL == null) {
responder.sendError(404, "serviceURL is null");
return;
}
URL url = new URL(serviceURL, "ping");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
try {
if (HttpURLConnection.HTTP_OK == conn.getResponseCode()) {
String response = new String(ByteStreams.toByteArray(conn.getInputStream()), Charsets.UTF_8);
responder.sendJson(response);
} else {
responder.sendError(500, "Failed to retrieve a response from the service");
}
} finally {
conn.disconnect();
}
}
@GET
@Path("read/{key}")
public void readDataSet(HttpServiceRequest request, HttpServiceResponder responder,
@PathParam("key") String key) throws IOException {
byte[] value = table.read(key);
if (value == null) {
responder.sendError(404, "Table returned null for value: " + key);
return;
}
responder.sendJson(Bytes.toString(value));
}
}
private static void writeToDataSet(final WorkerContext context,
final String tableName, final String key, final byte[] value) {
context.execute(new TxRunnable() {
@Override
public void run(DatasetContext context) throws Exception {
KeyValueTable table = context.getDataset(tableName);
table.write(key, value);
}
});
}
/**
*
*/
public static final class LifecycleWorker extends AbstractWorker {
private static final Logger LOG = LoggerFactory.getLogger(LifecycleWorker.class);
private volatile boolean stopped;
@Override
protected void configure() {
setName(LIFECYCLE_WORKER);
setInstances(3);
}
@Override
public void initialize(WorkerContext context) throws Exception {
super.initialize(context);
String key = String.format("init.%d", getContext().getInstanceId());
byte[] value = Bytes.toBytes(getContext().getInstanceCount());
writeToDataSet(getContext(), WORKER_INSTANCES_DATASET, key, value);
}
@Override
public void run() {
while (!stopped) {
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
LOG.error("Error sleeping in LifecycleWorker", e);
}
}
}
@Override
public void destroy() {
String key = String.format("stop.%d", getContext().getInstanceId());
byte[] value = Bytes.toBytes(getContext().getInstanceCount());
writeToDataSet(getContext(), WORKER_INSTANCES_DATASET, key, value);
}
@Override
public void stop() {
stopped = true;
}
}
/**
*
*/
public static final class PingingWorker extends AbstractWorker {
private static final Logger LOG = LoggerFactory.getLogger(PingingWorker.class);
@Override
protected void configure() {
setName(PINGING_WORKER);
setInstances(5);
}
@Override
public void run() {
try {
waitForGetServiceUrl();
} catch (InterruptedException e) {
LOG.warn("{} interrupted while discovering {}", PINGING_WORKER, CENTRAL_SERVICE, e);
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
LOG.error("{} got exception while pinging {}", PINGING_WORKER, CENTRAL_SERVICE, e);
} catch (TimeoutException e) {
LOG.error("{} Timed out while waiting for a successful ping to {}", PINGING_WORKER, CENTRAL_SERVICE);
}
URL baseURL = getContext().getServiceURL(CENTRAL_SERVICE);
if (baseURL == null) {
LOG.warn("Error getting {} URL in {}. Worker quitting.", CENTRAL_SERVICE, PINGING_WORKER);
return;
}
URL url;
try {
url = new URL(baseURL, "ping");
} catch (MalformedURLException e) {
LOG.warn("Exception while creating ping URL for {} from {}", CENTRAL_SERVICE, PINGING_WORKER);
return;
}
try {
waitForSuccessfulPing(url);
} catch (InterruptedException e) {
LOG.warn("{} interrupted while pinging {}", PINGING_WORKER, CENTRAL_SERVICE, e);
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
LOG.error("{} got exception while pinging {}", PINGING_WORKER, CENTRAL_SERVICE, e);
} catch (TimeoutException e) {
LOG.error("{} Timed out while waiting for a successful ping to {}", PINGING_WORKER, CENTRAL_SERVICE);
}
try {
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
try {
if (HttpURLConnection.HTTP_OK == conn.getResponseCode()) {
// Write the response to dataset, so that we can verify it from a test.
writeToDataSet(getContext(), DATASET_NAME, DATASET_KEY, ByteStreams.toByteArray(conn.getInputStream()));
}
} finally {
conn.disconnect();
}
} catch (IOException e) {
LOG.error("Got exception {}", e);
}
}
private void waitForGetServiceUrl() throws InterruptedException, ExecutionException, TimeoutException {
Tasks.waitFor(true, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return getContext().getServiceURL(CENTRAL_SERVICE) != null;
}
}, 10, TimeUnit.SECONDS);
}
private void waitForSuccessfulPing(final URL serviceUrl) throws InterruptedException, ExecutionException,
TimeoutException {
Tasks.waitFor(HttpURLConnection.HTTP_OK, new Callable<Integer>() {
@Override
public Integer call() throws Exception {
HttpURLConnection conn = (HttpURLConnection) serviceUrl.openConnection();
try {
return conn.getResponseCode();
} finally {
conn.disconnect();
}
}
}, 10, TimeUnit.SECONDS);
}
}
/**
* The central service which other programs will ping via their context's getServiceURL method.
*/
private static final class CentralService extends AbstractService {
@Override
protected void configure() {
setName("CentralService");
addHandler(new PingHandler());
}
public static final class PingHandler extends AbstractHttpServiceHandler {
@Path("/ping")
@GET
public void handler(HttpServiceRequest request, HttpServiceResponder responder) {
responder.sendString(200, ANSWER, Charsets.UTF_8);
}
}
}
}