/* * Copyright © 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.app.stream; import co.cask.cdap.api.data.stream.StreamBatchWriter; import co.cask.cdap.api.data.stream.StreamWriter; import co.cask.cdap.api.stream.StreamEventData; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.discovery.EndpointStrategy; import co.cask.cdap.common.discovery.RandomEndpointStrategy; import co.cask.cdap.data2.metadata.lineage.AccessType; import co.cask.cdap.data2.metadata.writer.LineageWriter; import co.cask.cdap.data2.registry.UsageRegistry; import co.cask.cdap.proto.Id; import co.cask.common.http.HttpMethod; 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.Maps; import com.google.common.net.HttpHeaders; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import org.apache.twill.discovery.Discoverable; import org.apache.twill.discovery.DiscoveryServiceClient; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.URL; import java.nio.ByteBuffer; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; /** * Default implementation of {@link StreamWriter} */ public class DefaultStreamWriter implements StreamWriter { private final EndpointStrategy endpointStrategy; private final ConcurrentMap<Id.Stream, Boolean> isStreamRegistered; private final UsageRegistry usageRegistry; /** * The namespace that this {@link StreamWriter} belongs to. */ private final Id.Namespace namespace; /** * The owners of this {@link StreamWriter}. */ private final List<Id> owners; private final Id.Run run; private final LineageWriter lineageWriter; @Inject public DefaultStreamWriter(@Assisted("run") Id.Run run, @Assisted("owners") List<Id> owners, UsageRegistry usageRegistry, LineageWriter lineageWriter, DiscoveryServiceClient discoveryServiceClient) { this.run = run; this.namespace = run.getNamespace(); this.owners = owners; this.lineageWriter = lineageWriter; this.endpointStrategy = new RandomEndpointStrategy(discoveryServiceClient.discover(Constants.Service.STREAMS)); this.isStreamRegistered = Maps.newConcurrentMap(); this.usageRegistry = usageRegistry; } private URL getStreamURL(String stream) throws IOException { return getStreamURL(stream, false); } private URL getStreamURL(String stream, boolean batch) throws IOException { Discoverable discoverable = endpointStrategy.pick(1, TimeUnit.SECONDS); if (discoverable == null) { throw new IOException("Stream Service Endpoint not found"); } InetSocketAddress address = discoverable.getSocketAddress(); String path = String.format("http://%s:%d%s/namespaces/%s/streams/%s", address.getHostName(), address.getPort(), Constants.Gateway.API_VERSION_3, namespace.getId(), stream); if (batch) { path = String.format("%s/batch", path); } return new URL(path); } private void writeToStream(Id.Stream stream, HttpRequest request) throws IOException { HttpResponse response = HttpRequests.execute(request); int responseCode = response.getResponseCode(); if (responseCode == HttpResponseStatus.NOT_FOUND.getCode()) { throw new IOException(String.format("Stream %s not found", stream)); } registerStream(stream); if (responseCode < 200 || responseCode >= 300) { throw new IOException(String.format("Writing to Stream %s did not succeed. Stream Service ResponseCode : %d", stream, responseCode)); } } private void write(String stream, ByteBuffer data, Map<String, String> headers) throws IOException { URL streamURL = getStreamURL(stream); HttpRequest.Builder requestBuilder = HttpRequest.post(streamURL).withBody(data); for (Map.Entry<String, String> header : headers.entrySet()) { requestBuilder.addHeader(stream + "." + header.getKey(), header.getValue()); } writeToStream(Id.Stream.from(namespace, stream), requestBuilder.build()); } @Override public void write(String stream, String data) throws IOException { write(stream, data, ImmutableMap.<String, String>of()); } @Override public void write(String stream, String data, Map<String, String> headers) throws IOException { write(stream, Charsets.UTF_8.encode(data), headers); } @Override public void write(String stream, ByteBuffer data) throws IOException { write(stream, data, ImmutableMap.<String, String>of()); } @Override public void write(String stream, StreamEventData data) throws IOException { write(stream, data.getBody(), data.getHeaders()); } @Override public void writeFile(String stream, File file, String contentType) throws IOException { URL url = getStreamURL(stream, true); HttpRequest request = HttpRequest.post(url).withBody(file).addHeader(HttpHeaders.CONTENT_TYPE, contentType).build(); writeToStream(Id.Stream.from(namespace, stream), request); } @Override public StreamBatchWriter createBatchWriter(String stream, String contentType) throws IOException { URL url = getStreamURL(stream, true); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod(HttpMethod.POST.name()); connection.setReadTimeout(15000); connection.setConnectTimeout(15000); connection.setRequestProperty(HttpHeaders.CONTENT_TYPE, contentType); connection.setDoOutput(true); connection.setChunkedStreamingMode(0); connection.connect(); try { Id.Stream streamId = Id.Stream.from(namespace, stream); registerStream(streamId); return new DefaultStreamBatchWriter(connection, streamId); } catch (IOException e) { connection.disconnect(); throw e; } } private void registerStream(Id.Stream stream) { // prone being entered multiple times, but OK since usageRegistry.register is not an expensive operation if (!isStreamRegistered.containsKey(stream)) { usageRegistry.registerAll(owners, stream); isStreamRegistered.put(stream, true); } // Lineage writer handles duplicate accesses internally lineageWriter.addAccess(run, stream, AccessType.WRITE); } }