/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 gobblin.couchbase.writer; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.math3.util.Pair; import com.couchbase.client.core.lang.Tuple; import com.couchbase.client.core.lang.Tuple2; import com.couchbase.client.core.message.ResponseStatus; import com.couchbase.client.core.message.kv.MutationToken; import com.couchbase.client.deps.io.netty.buffer.ByteBuf; import com.couchbase.client.java.Bucket; import com.couchbase.client.java.Cluster; import com.couchbase.client.java.CouchbaseCluster; import com.couchbase.client.java.document.AbstractDocument; import com.couchbase.client.java.document.Document; import com.couchbase.client.java.document.RawJsonDocument; import com.couchbase.client.java.env.CouchbaseEnvironment; import com.couchbase.client.java.transcoder.Transcoder; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.typesafe.config.Config; import lombok.extern.slf4j.Slf4j; import rx.Observable; import rx.Subscriber; import gobblin.couchbase.common.TupleDocument; import gobblin.util.ConfigUtils; import gobblin.writer.AsyncDataWriter; import gobblin.writer.GenericWriteResponse; import gobblin.writer.GenericWriteResponseWrapper; import gobblin.writer.SyncDataWriter; import gobblin.writer.WriteCallback; import gobblin.writer.WriteResponse; import gobblin.writer.WriteResponseFuture; import gobblin.writer.WriteResponseMapper; /** * A single bucket Couchbase writer. */ @Slf4j public class CouchbaseWriter<D extends AbstractDocument> implements AsyncDataWriter<D>, SyncDataWriter<D> { private final Cluster _cluster; private final Bucket _bucket; private final long _operationTimeout; private final TimeUnit _operationTimeunit; private final WriteResponseMapper<D> _defaultWriteResponseMapper; // A basic transcoder that just passes through the embedded binary content. private final Transcoder<TupleDocument, Tuple2<ByteBuf, Integer>> _tupleDocumentTranscoder = new Transcoder<TupleDocument, Tuple2<ByteBuf, Integer>>() { @Override public TupleDocument decode(String id, ByteBuf content, long cas, int expiry, int flags, ResponseStatus status) { return newDocument(id, expiry, Tuple.create(content, flags), cas); } @Override public Tuple2<ByteBuf, Integer> encode(TupleDocument document) { return document.content(); } @Override public TupleDocument newDocument(String id, int expiry, Tuple2<ByteBuf, Integer> content, long cas) { return new TupleDocument(id, expiry, content, cas); } @Override public TupleDocument newDocument(String id, int expiry, Tuple2<ByteBuf, Integer> content, long cas, MutationToken mutationToken) { return new TupleDocument(id, expiry, content, cas); } @Override public Class<TupleDocument> documentType() { return TupleDocument.class; } }; public CouchbaseWriter(CouchbaseEnvironment couchbaseEnvironment, Config config) { List<String> hosts = ConfigUtils.getStringList(config, CouchbaseWriterConfigurationKeys.BOOTSTRAP_SERVERS); _cluster = CouchbaseCluster.create(couchbaseEnvironment, hosts); String bucketName = ConfigUtils.getString(config, CouchbaseWriterConfigurationKeys.BUCKET, CouchbaseWriterConfigurationKeys.BUCKET_DEFAULT); String password = ConfigUtils.getString(config, CouchbaseWriterConfigurationKeys.PASSWORD, ""); _bucket = _cluster.openBucket(bucketName, password, Collections.<Transcoder<? extends Document, ?>>singletonList(_tupleDocumentTranscoder)); _operationTimeout = ConfigUtils.getLong(config, CouchbaseWriterConfigurationKeys.OPERATION_TIMEOUT_MILLIS, CouchbaseWriterConfigurationKeys.OPERATION_TIMEOUT_DEFAULT); _operationTimeunit = TimeUnit.MILLISECONDS; _defaultWriteResponseMapper = new GenericWriteResponseWrapper<>(); log.info("Couchbase writer configured with: hosts: {}, bucketName: {}, operationTimeoutInMillis: {}", hosts, bucketName, _operationTimeout); } @VisibleForTesting Bucket getBucket() { return _bucket; } private void assertRecordWritable(D record) { boolean recordIsTupleDocument = (record instanceof TupleDocument); boolean recordIsJsonDocument = (record instanceof RawJsonDocument); Preconditions.checkArgument(recordIsTupleDocument || recordIsJsonDocument, "This writer only supports TupleDocument or RawJsonDocument. Found " + record.getClass().getName()); } @Override public Future<WriteResponse> write(final D record, final WriteCallback callback) { assertRecordWritable(record); if (record instanceof TupleDocument) { ((TupleDocument) record).content().value1().retain(); } Observable<D> observable = _bucket.async().upsert(record); if (callback == null) { return new WriteResponseFuture<>( observable.timeout(_operationTimeout, _operationTimeunit).toBlocking().toFuture(), _defaultWriteResponseMapper); } else { final AtomicBoolean callbackFired = new AtomicBoolean(false); final BlockingQueue<Pair<WriteResponse, Throwable>> writeResponseQueue = new ArrayBlockingQueue<>(1); final Future<WriteResponse> writeResponseFuture = new Future<WriteResponse>() { @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; } @Override public boolean isCancelled() { return false; } @Override public boolean isDone() { return callbackFired.get(); } @Override public WriteResponse get() throws InterruptedException, ExecutionException { Pair<WriteResponse, Throwable> writeResponseThrowablePair = writeResponseQueue.take(); return getWriteResponseorThrow(writeResponseThrowablePair); } @Override public WriteResponse get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { Pair<WriteResponse, Throwable> writeResponseThrowablePair = writeResponseQueue.poll(timeout, unit); if (writeResponseThrowablePair == null) { throw new TimeoutException("Timeout exceeded while waiting for future to be done"); } else { return getWriteResponseorThrow(writeResponseThrowablePair); } } }; observable.timeout(_operationTimeout, _operationTimeunit) .subscribe(new Subscriber<D>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { callbackFired.set(true); writeResponseQueue.add(new Pair<WriteResponse, Throwable>(null, e)); callback.onFailure(e); } @Override public void onNext(D doc) { try { callbackFired.set(true); WriteResponse writeResponse = new GenericWriteResponse<D>(doc); writeResponseQueue.add(new Pair<WriteResponse, Throwable>(writeResponse, null)); callback.onSuccess(writeResponse); } finally { if (doc instanceof TupleDocument) { ((TupleDocument) doc).content().value1().release(); } } } }); return writeResponseFuture; } } @Override public void flush() throws IOException { } private WriteResponse getWriteResponseorThrow(Pair<WriteResponse, Throwable> writeResponseThrowablePair) throws ExecutionException { if (writeResponseThrowablePair.getFirst() != null) { return writeResponseThrowablePair.getFirst(); } else if (writeResponseThrowablePair.getSecond() != null) { throw new ExecutionException(writeResponseThrowablePair.getSecond()); } else { throw new ExecutionException(new RuntimeException("Could not find non-null WriteResponse pair")); } } @Override public void cleanup() throws IOException { } @Override public WriteResponse write(D record) throws IOException { try { D doc = _bucket.upsert(record); return new GenericWriteResponse(doc); } catch (Exception e) { throw new IOException("Failed to write to Couchbase cluster", e); } } @Override public void close() { if (!_bucket.isClosed()) { try { _bucket.close(); } catch (Exception e) { log.warn("Failed to close bucket", e); } } try { _cluster.disconnect(); } catch (Exception e) { log.warn("Failed to disconnect from cluster", e); } } }