/*
* Copyright © 2014 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.gateway.handlers;
import co.cask.cdap.api.data.format.FormatSpecification;
import co.cask.cdap.api.data.format.Formats;
import co.cask.cdap.api.data.schema.Schema;
import co.cask.cdap.api.flow.flowlet.StreamEvent;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.stream.StreamEventTypeAdapter;
import co.cask.cdap.common.utils.Tasks;
import co.cask.cdap.data2.transaction.stream.StreamConfig;
import co.cask.cdap.format.TextRecordFormat;
import co.cask.cdap.gateway.GatewayFastTestsSuite;
import co.cask.cdap.gateway.GatewayTestBase;
import co.cask.cdap.internal.io.SchemaTypeAdapter;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.MetricQueryResult;
import co.cask.cdap.proto.NamespaceMeta;
import co.cask.cdap.proto.StreamProperties;
import co.cask.common.http.HttpRequest;
import co.cask.common.http.HttpRequests;
import co.cask.common.http.HttpResponse;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.apache.commons.lang3.ArrayUtils;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
/**
* Test stream handler. This is not part of GatewayFastTestsSuite because it needs to start the gateway multiple times.
*/
public class StreamHandlerTest extends GatewayTestBase {
private static final String API_KEY = GatewayTestBase.getAuthHeader().getValue();
private static final Gson GSON = StreamEventTypeAdapter.register(
new GsonBuilder().registerTypeAdapter(Schema.class, new SchemaTypeAdapter())).create();
protected URL createURL(String path) throws URISyntaxException, MalformedURLException {
return createURL(Id.Namespace.DEFAULT.getId(), path);
}
protected URL createStreamInfoURL(String streamName) throws URISyntaxException, MalformedURLException {
return createURL(String.format("streams/%s", streamName));
}
protected URL createPropertiesURL(String streamName) throws URISyntaxException, MalformedURLException {
return createURL(String.format("streams/%s/properties", streamName));
}
private URL createURL(String namespace, String path) throws URISyntaxException, MalformedURLException {
return getEndPoint(String.format("/v3/namespaces/%s/%s", namespace, path)).toURL();
}
protected HttpURLConnection openURL(URL url, HttpMethod method) throws IOException {
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
urlConn.setRequestMethod(method.getName());
urlConn.setRequestProperty(Constants.Gateway.API_KEY, API_KEY);
return urlConn;
}
@After
public void reset() throws Exception {
org.apache.http.HttpResponse httpResponse = GatewayFastTestsSuite.doDelete("/v3/unrecoverable/namespaces/default");
Assert.assertEquals(200, httpResponse.getStatusLine().getStatusCode());
}
@Test
public void testStreamCreateInvalidName() throws Exception {
// Now, create the new stream with an invalid character: '@'
HttpURLConnection urlConn = openURL(createURL("streams/inv@lidStreamName"), HttpMethod.PUT);
Assert.assertEquals(HttpResponseStatus.BAD_REQUEST.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
}
@Test
public void testUpdateDescription() throws Exception {
// Create a stream with some ttl and description
String desc = "large stream";
HttpURLConnection urlConn = openURL(createURL("streams/stream1"), HttpMethod.PUT);
urlConn.setDoOutput(true);
Schema schema = Schema.recordOf("event", Schema.Field.of("purchase", Schema.of(Schema.Type.STRING)));
FormatSpecification formatSpecification = new FormatSpecification(
TextRecordFormat.class.getCanonicalName(), schema, ImmutableMap.of(TextRecordFormat.CHARSET, "utf8"));
StreamProperties properties = new StreamProperties(1L, formatSpecification, 128, desc);
urlConn.getOutputStream().write(GSON.toJson(properties).getBytes(Charsets.UTF_8));
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// Check whether ttl and description got persisted
urlConn = openURL(createStreamInfoURL("stream1"), HttpMethod.GET);
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
StreamProperties actual = GSON.fromJson(new String(ByteStreams.toByteArray(urlConn.getInputStream()),
Charsets.UTF_8), StreamProperties.class);
urlConn.disconnect();
Assert.assertEquals(properties, actual);
// Update desc and ttl and check whether the changes were persisted
StreamProperties newProps = new StreamProperties(2L, null, null, "small stream");
urlConn = openURL(createPropertiesURL("stream1"), HttpMethod.PUT);
urlConn.setDoOutput(true);
urlConn.getOutputStream().write(GSON.toJson(newProps).getBytes(Charsets.UTF_8));
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
urlConn = openURL(createStreamInfoURL("stream1"), HttpMethod.GET);
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
actual = GSON.fromJson(new String(ByteStreams.toByteArray(urlConn.getInputStream()), Charsets.UTF_8),
StreamProperties.class);
urlConn.disconnect();
StreamProperties expected = new StreamProperties(newProps.getTTL(), properties.getFormat(),
properties.getNotificationThresholdMB(),
newProps.getDescription());
Assert.assertEquals(expected, actual);
}
@Test
public void testStreamCreate() throws Exception {
// Try to get info on a non-existent stream
HttpURLConnection urlConn = openURL(createStreamInfoURL("test_stream1"), HttpMethod.GET);
Assert.assertEquals(HttpResponseStatus.NOT_FOUND.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// try to POST info to the non-existent stream
urlConn = openURL(createURL("streams/non_existent_stream"), HttpMethod.POST);
Assert.assertEquals(HttpResponseStatus.NOT_FOUND.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// Now, create the new stream.
urlConn = openURL(createURL("streams/test_stream1"), HttpMethod.PUT);
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// getInfo should now return 200
urlConn = openURL(createStreamInfoURL("test_stream1"), HttpMethod.GET);
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
}
@Test
public void testSimpleStreamEnqueue() throws Exception {
// Create new stream.
HttpURLConnection urlConn = openURL(createURL("streams/test_stream_enqueue"), HttpMethod.PUT);
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// Enqueue 10 entries
for (int i = 0; i < 10; ++i) {
urlConn = openURL(createURL("streams/test_stream_enqueue"), HttpMethod.POST);
urlConn.setDoOutput(true);
urlConn.addRequestProperty("test_stream_enqueue.header1", Integer.toString(i));
urlConn.getOutputStream().write(Integer.toString(i).getBytes(Charsets.UTF_8));
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
}
// Fetch 10 entries
urlConn = openURL(createURL("streams/test_stream_enqueue/events?limit=10"), HttpMethod.GET);
List<StreamEvent> events = GSON.fromJson(new String(ByteStreams.toByteArray(urlConn.getInputStream()),
Charsets.UTF_8),
new TypeToken<List<StreamEvent>>() { }.getType());
for (int i = 0; i < 10; i++) {
StreamEvent event = events.get(i);
int actual = Integer.parseInt(Charsets.UTF_8.decode(event.getBody()).toString());
Assert.assertEquals(i, actual);
Assert.assertEquals(Integer.toString(i), event.getHeaders().get("header1"));
}
urlConn.disconnect();
}
@Test
public void testStreamInfo() throws Exception {
// Now, create the new stream.
HttpURLConnection urlConn = openURL(createURL("streams/stream_info"), HttpMethod.PUT);
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// put a new config
urlConn = openURL(createPropertiesURL("stream_info"), HttpMethod.PUT);
urlConn.setDoOutput(true);
Schema schema = Schema.recordOf("event", Schema.Field.of("purchase", Schema.of(Schema.Type.STRING)));
FormatSpecification formatSpecification;
formatSpecification = new FormatSpecification(TextRecordFormat.class.getCanonicalName(),
schema,
ImmutableMap.of(TextRecordFormat.CHARSET, "utf8"));
StreamProperties streamProperties = new StreamProperties(2L, formatSpecification, 20);
urlConn.getOutputStream().write(GSON.toJson(streamProperties).getBytes(Charsets.UTF_8));
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// test the config ttl by calling info
urlConn = openURL(createStreamInfoURL("stream_info"), HttpMethod.GET);
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
StreamProperties actual = GSON.fromJson(new String(ByteStreams.toByteArray(urlConn.getInputStream()),
Charsets.UTF_8), StreamProperties.class);
urlConn.disconnect();
Assert.assertEquals(streamProperties, actual);
}
@Test
public void testPutStreamConfigDefaults() throws Exception {
// Now, create the new stream.
HttpURLConnection urlConn = openURL(createURL("streams/stream_defaults"), HttpMethod.PUT);
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// put a new config
urlConn = openURL(createPropertiesURL("stream_defaults"), HttpMethod.PUT);
urlConn.setDoOutput(true);
// don't give the schema to make sure a default gets used
FormatSpecification formatSpecification = new FormatSpecification(Formats.TEXT, null, null);
StreamProperties streamProperties = new StreamProperties(2L, formatSpecification, 20);
urlConn.getOutputStream().write(GSON.toJson(streamProperties).getBytes(Charsets.UTF_8));
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// test the config ttl by calling info
urlConn = openURL(createStreamInfoURL("stream_defaults"),
HttpMethod.GET);
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
StreamProperties actual = GSON.fromJson(new String(ByteStreams.toByteArray(urlConn.getInputStream()),
Charsets.UTF_8), StreamProperties.class);
urlConn.disconnect();
StreamProperties expected = new StreamProperties(2L, StreamConfig.DEFAULT_STREAM_FORMAT, 20);
Assert.assertEquals(expected, actual);
}
@Test
public void testPutInvalidStreamConfig() throws Exception {
// create the new stream.
HttpURLConnection urlConn = openURL(createURL("streams/stream_badconf"), HttpMethod.PUT);
Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// put a config with invalid json
urlConn = openURL(createPropertiesURL("stream_badconf"), HttpMethod.PUT);
urlConn.setDoOutput(true);
urlConn.getOutputStream().write("ttl:2".getBytes(Charsets.UTF_8));
Assert.assertEquals(HttpResponseStatus.BAD_REQUEST.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// put a config with an invalid TTL
urlConn = openURL(createPropertiesURL("stream_badconf"), HttpMethod.PUT);
urlConn.setDoOutput(true);
StreamProperties streamProperties = new StreamProperties(-1L, null, 20);
urlConn.getOutputStream().write(GSON.toJson(streamProperties).getBytes(Charsets.UTF_8));
Assert.assertEquals(HttpResponseStatus.BAD_REQUEST.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// put a config with a format without a format class
urlConn = openURL(createPropertiesURL("stream_badconf"), HttpMethod.PUT);
urlConn.setDoOutput(true);
FormatSpecification formatSpec = new FormatSpecification(null, null, null);
streamProperties = new StreamProperties(2L, formatSpec, 20);
urlConn.getOutputStream().write(GSON.toJson(streamProperties).getBytes(Charsets.UTF_8));
Assert.assertEquals(HttpResponseStatus.BAD_REQUEST.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// put a config with a format with a bad format class
urlConn = openURL(createPropertiesURL("stream_badconf"), HttpMethod.PUT);
urlConn.setDoOutput(true);
formatSpec = new FormatSpecification("gibberish", null, null);
streamProperties = new StreamProperties(2L, formatSpec, 20);
urlConn.getOutputStream().write(GSON.toJson(streamProperties).getBytes(Charsets.UTF_8));
Assert.assertEquals(HttpResponseStatus.BAD_REQUEST.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// put a config with an incompatible format and schema
urlConn = openURL(createPropertiesURL("stream_badconf"), HttpMethod.PUT);
urlConn.setDoOutput(true);
Schema schema = Schema.recordOf("event", Schema.Field.of("col", Schema.of(Schema.Type.DOUBLE)));
formatSpec = new FormatSpecification(TextRecordFormat.class.getCanonicalName(), schema, null);
streamProperties = new StreamProperties(2L, formatSpec, 20);
urlConn.getOutputStream().write(GSON.toJson(streamProperties).getBytes(Charsets.UTF_8));
Assert.assertEquals(HttpResponseStatus.BAD_REQUEST.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
// put a config with a bad threshold
urlConn = openURL(createPropertiesURL("stream_badconf"), HttpMethod.PUT);
urlConn.setDoOutput(true);
streamProperties = new StreamProperties(2L, null, -20);
urlConn.getOutputStream().write(GSON.toJson(streamProperties).getBytes(Charsets.UTF_8));
Assert.assertEquals(HttpResponseStatus.BAD_REQUEST.getCode(), urlConn.getResponseCode());
urlConn.disconnect();
}
@Test
public void testStreamCreateInNonexistentNamespace() throws Exception {
Id.Namespace originallyNonExistentNamespace = Id.Namespace.from("originallyNonExistentNamespace");
Id.Stream streamId = Id.Stream.from(originallyNonExistentNamespace, "streamName");
HttpResponse response = createStream(streamId, 404);
Assert.assertEquals(HttpResponseStatus.NOT_FOUND.getCode(), response.getResponseCode());
// once the namespace exists, the same stream create works.
namespaceAdmin.create(new NamespaceMeta.Builder().setName(originallyNonExistentNamespace).build());
createStream(streamId);
}
@Test
public void testNamespacedStreamEvents() throws Exception {
// Create two streams with the same name, in different namespaces.
String streamName = "testNamespacedEvents";
Id.Stream streamId1 = Id.Stream.from(TEST_NAMESPACE1, streamName);
Id.Stream streamId2 = Id.Stream.from(TEST_NAMESPACE2, streamName);
createStream(streamId1);
createStream(streamId2);
List<String> eventsSentToStream1 = Lists.newArrayList();
// Enqueue 10 entries to the stream in the first namespace
for (int i = 0; i < 10; ++i) {
String body = streamId1.getNamespaceId() + i;
sendEvent(streamId1, body);
eventsSentToStream1.add(body);
}
List<String> eventsSentToStream2 = Lists.newArrayList();
// Enqueue only 5 entries to the stream in the second namespace, decrementing the value each time
for (int i = 0; i > -5; --i) {
String body = streamId1.getNamespaceId() + i;
sendEvent(streamId2, body);
eventsSentToStream2.add(body);
}
// Test that even though the stream names are the same, the events ingested into the individual streams
// are exactly what are fetched from the individual streams.
List<String> eventsFetchedFromStream1 = fetchEvents(streamId1);
Assert.assertEquals(eventsSentToStream1, eventsFetchedFromStream1);
List<String> eventsFetchedFromStream2 = fetchEvents(streamId2);
Assert.assertEquals(eventsSentToStream2, eventsFetchedFromStream2);
}
@Test
public void testNamespacedMetrics() throws Exception {
// Create two streams with the same name, in different namespaces.
String streamName = "testNamespacedStreamMetrics";
Id.Stream streamId1 = Id.Stream.from(TEST_NAMESPACE1, streamName);
Id.Stream streamId2 = Id.Stream.from(TEST_NAMESPACE2, streamName);
createStream(streamId1);
createStream(streamId2);
// Enqueue 10 entries to the stream in the first namespace
for (int i = 0; i < 10; ++i) {
sendEvent(streamId1, Integer.toString(i));
}
// Enqueue only 2 entries to the stream in the second namespace
for (int i = 0; i < 2; ++i) {
sendEvent(streamId2, Integer.toString(i));
}
// Check metrics to verify that the metric for events processed is specific to each stream
checkEventsProcessed(streamId1, 10L, 10);
checkEventsProcessed(streamId2, 2L, 10);
}
private HttpResponse createStream(Id.Stream streamId, int... allowedErrorCodes) throws Exception {
URL url = createURL(streamId.getNamespaceId(), "streams/" + streamId.getId());
HttpRequest request = HttpRequest.put(url).build();
HttpResponse response = HttpRequests.execute(request);
int responseCode = response.getResponseCode();
if (!ArrayUtils.contains(allowedErrorCodes, responseCode)) {
Assert.assertEquals(200, responseCode);
}
return response;
}
private void sendEvent(Id.Stream streamId, String body) throws Exception {
URL url = createURL(streamId.getNamespaceId(), "streams/" + streamId.getId());
HttpRequest request = HttpRequest.post(url).withBody(body).build();
HttpResponse response = HttpRequests.execute(request);
Assert.assertEquals(200, response.getResponseCode());
}
private List<String> fetchEvents(Id.Stream streamId) throws Exception {
URL url = createURL(streamId.getNamespaceId(), "streams/" + streamId.getId() + "/events");
HttpRequest request = HttpRequest.get(url).build();
HttpResponse response = HttpRequests.execute(request);
Assert.assertEquals(200, response.getResponseCode());
List<String> events = Lists.newArrayList();
JsonArray jsonArray = new JsonParser().parse(response.getResponseBodyAsString()).getAsJsonArray();
for (JsonElement jsonElement : jsonArray) {
events.add(jsonElement.getAsJsonObject().get("body").getAsString());
}
return events;
}
private void checkEventsProcessed(final Id.Stream streamId, long expectedCount, int retries) throws Exception {
Tasks.waitFor(expectedCount, new Callable<Long>() {
@Override
public Long call() throws Exception {
return getNumProcessed(streamId);
}
}, retries, TimeUnit.SECONDS, 100, TimeUnit.MILLISECONDS);
}
private long getNumProcessed(Id.Stream streamId) throws Exception {
String path =
String.format("/v3/metrics/query?metric=system.collect.events&tag=namespace:%s&tag=stream:%s&aggregate=true",
streamId.getNamespaceId(), streamId.getId());
HttpRequest request = HttpRequest.post(getEndPoint(path).toURL()).build();
HttpResponse response = HttpRequests.execute(request);
Assert.assertEquals(200, response.getResponseCode());
return getNumEventsFromResponse(response.getResponseBodyAsString());
}
private long getNumEventsFromResponse(String response) {
MetricQueryResult metricQueryResult = new Gson().fromJson(response, MetricQueryResult.class);
MetricQueryResult.TimeSeries[] series = metricQueryResult.getSeries();
if (series.length == 0) {
return 0;
}
return series[0].getData()[0].getValue();
}
}