All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.couchbase.client.java.bucket.DefaultAsyncBucketManager Maven / Gradle / Ivy

/**
 * Copyright (C) 2014 Couchbase, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING
 * IN THE SOFTWARE.
 */
package com.couchbase.client.java.bucket;

import static com.couchbase.client.java.query.Select.select;
import static com.couchbase.client.java.query.dsl.Expression.i;
import static com.couchbase.client.java.query.dsl.Expression.s;
import static com.couchbase.client.java.query.dsl.Expression.x;
import static com.couchbase.client.java.util.retry.RetryBuilder.anyOf;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import com.couchbase.client.core.ClusterFacade;
import com.couchbase.client.core.CouchbaseException;
import com.couchbase.client.core.logging.CouchbaseLogger;
import com.couchbase.client.core.logging.CouchbaseLoggerFactory;
import com.couchbase.client.core.message.config.BucketConfigRequest;
import com.couchbase.client.core.message.config.BucketConfigResponse;
import com.couchbase.client.core.message.config.GetDesignDocumentsRequest;
import com.couchbase.client.core.message.config.GetDesignDocumentsResponse;
import com.couchbase.client.core.message.view.GetDesignDocumentRequest;
import com.couchbase.client.core.message.view.GetDesignDocumentResponse;
import com.couchbase.client.core.message.view.RemoveDesignDocumentRequest;
import com.couchbase.client.core.message.view.RemoveDesignDocumentResponse;
import com.couchbase.client.core.message.view.UpsertDesignDocumentRequest;
import com.couchbase.client.core.message.view.UpsertDesignDocumentResponse;
import com.couchbase.client.core.time.Delay;
import com.couchbase.client.deps.io.netty.util.CharsetUtil;
import com.couchbase.client.java.CouchbaseAsyncBucket;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.error.CannotRetryException;
import com.couchbase.client.java.error.DesignDocumentAlreadyExistsException;
import com.couchbase.client.java.error.DesignDocumentException;
import com.couchbase.client.java.error.IndexAlreadyExistsException;
import com.couchbase.client.java.error.IndexDoesNotExistException;
import com.couchbase.client.java.error.IndexesNotReadyException;
import com.couchbase.client.java.error.TranscodingException;
import com.couchbase.client.java.query.AsyncN1qlQueryResult;
import com.couchbase.client.java.query.AsyncN1qlQueryRow;
import com.couchbase.client.java.query.Index;
import com.couchbase.client.java.query.N1qlParams;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.Statement;
import com.couchbase.client.java.query.consistency.ScanConsistency;
import com.couchbase.client.java.query.core.N1qlQueryExecutor;
import com.couchbase.client.java.query.dsl.Expression;
import com.couchbase.client.java.query.dsl.Sort;
import com.couchbase.client.java.query.dsl.path.index.IndexType;
import com.couchbase.client.java.query.dsl.path.index.UsingWithPath;
import com.couchbase.client.java.query.util.IndexInfo;
import com.couchbase.client.java.view.DesignDocument;
import rx.Notification;
import rx.Observable;
import rx.functions.Action1;
import rx.functions.Func0;
import rx.functions.Func1;

/**
 * Default implementation of a {@link AsyncBucketManager}.
 *
 * @author Michael Nitschinger
 * @since 2.0
 */
public class DefaultAsyncBucketManager implements AsyncBucketManager {

    /** the name of the logger dedicated to index watching **/
    public static final String INDEX_WATCH_LOG_NAME = "indexWatch";

    //index watching related constants
    private static final CouchbaseLogger INDEX_WATCH_LOG = CouchbaseLoggerFactory.getInstance(INDEX_WATCH_LOG_NAME);
    //big enough as to only really consider the timeout, but without risk of overflowing
    private static final int INDEX_WATCH_MAX_ATTEMPTS = Integer.MAX_VALUE - 5;
    private static final Delay INDEX_WATCH_DELAY = Delay.linear(TimeUnit.MILLISECONDS, 1000, 50, 500);

    private final ClusterFacade core;
    private final String bucket;
    private final String password;
    private final N1qlQueryExecutor queryExecutor;

    DefaultAsyncBucketManager(String bucket, String password, ClusterFacade core) {
        this.bucket = bucket;
        this.password = password;
        this.core = core;
        this.queryExecutor = new N1qlQueryExecutor(core, bucket, password);
    }

    public static DefaultAsyncBucketManager create(String bucket, String password, ClusterFacade core) {
        return new DefaultAsyncBucketManager(bucket, password, core);
    }

    @Override
    public Observable info() {
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new BucketConfigRequest("/pools/default/buckets/", null, bucket, password));
            }
        }).map(new Func1() {
            @Override
            public BucketInfo call(BucketConfigResponse response) {
                try {
                    return DefaultBucketInfo.create(
                        CouchbaseAsyncBucket.JSON_OBJECT_TRANSCODER.stringToJsonObject(response.config())
                    );
                } catch (Exception ex) {
                    throw new TranscodingException("Could not decode bucket info.", ex);
                }
            }
        });
    }


    @Override
    public Observable flush() {
        return BucketFlusher.flush(core, bucket, password);
    }

    @Override
    public Observable getDesignDocuments() {
        return getDesignDocuments(false);
    }

    @Override
    public Observable getDesignDocuments(final boolean development) {
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new GetDesignDocumentsRequest(bucket, password));
            }
        }).flatMap(new Func1>() {
            @Override
            public Observable call(GetDesignDocumentsResponse response) {
                JsonObject converted;
                try {
                    converted = CouchbaseAsyncBucket.JSON_OBJECT_TRANSCODER.stringToJsonObject(response.content());
                } catch (Exception e) {
                    throw new TranscodingException("Could not decode design document.", e);
                }
                JsonArray rows = converted.getArray("rows");
                List docs = new ArrayList();
                for (Object doc : rows) {
                    JsonObject docObj = ((JsonObject) doc).getObject("doc");
                    String id = docObj.getObject("meta").getString("id");
                    String[] idSplit = id.split("/");
                    String fullName = idSplit[1];
                    boolean isDev = fullName.startsWith("dev_");
                    if (isDev != development) {
                        continue;
                    }
                    String name = fullName.replace("dev_", "");
                    docs.add(DesignDocument.from(name, docObj.getObject("json")));
                }
                return Observable.from(docs);
            }
        });
    }

    @Override
    public Observable getDesignDocument(String name) {
        return getDesignDocument(name, false);
    }

    @Override
    public Observable getDesignDocument(final String name, final boolean development) {
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new GetDesignDocumentRequest(name, development, bucket, password));
            }
        }).filter(new Func1() {
            @Override
            public Boolean call(GetDesignDocumentResponse response) {
                boolean success = response.status().isSuccess();
                if (!success) {
                    if (response.content() != null && response.content().refCnt() > 0) {
                        response.content().release();
                    }
                }
                return success;
            }
        })
            .map(new Func1() {
                @Override
                public DesignDocument call(GetDesignDocumentResponse response) {
                    JsonObject converted;
                    try {
                        converted = CouchbaseAsyncBucket.JSON_OBJECT_TRANSCODER.stringToJsonObject(
                            response.content().toString(CharsetUtil.UTF_8));
                    } catch (Exception e) {
                        throw new TranscodingException("Could not decode design document.", e);
                    } finally {
                        if (response.content() != null && response.content().refCnt() > 0) {
                            response.content().release();
                        }
                    }
                    return DesignDocument.from(response.name(), converted);
                }
            });
    }

    @Override
    public Observable insertDesignDocument(final DesignDocument designDocument) {
        return insertDesignDocument(designDocument, false);
    }

    @Override
    public Observable insertDesignDocument(final DesignDocument designDocument, final boolean development) {
        return getDesignDocument(designDocument.name(), development)
            .isEmpty()
            .flatMap(new Func1>() {
                @Override
                public Observable call(Boolean doesNotExist) {
                    if (doesNotExist) {
                        return upsertDesignDocument(designDocument, development);
                    } else {
                        return Observable.error(new DesignDocumentAlreadyExistsException());
                    }
                }
            });
    }

    @Override
    public Observable upsertDesignDocument(DesignDocument designDocument) {
        return upsertDesignDocument(designDocument, false);
    }

    @Override
    public Observable upsertDesignDocument(final DesignDocument designDocument, final boolean development) {
        String body;
        try {
            body = CouchbaseAsyncBucket.JSON_OBJECT_TRANSCODER.jsonObjectToString(designDocument.toJsonObject());
        } catch (Exception e) {
            throw new TranscodingException("Could not encode design document: ", e);
        }

        final String b = body;
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new UpsertDesignDocumentRequest(designDocument.name(), b, development, bucket, password));
            }
        }).map(new Func1() {
            @Override
            public DesignDocument call(UpsertDesignDocumentResponse response) {
                try {
                    if (!response.status().isSuccess()) {
                        String msg = response.content().toString(CharsetUtil.UTF_8);
                        throw new DesignDocumentException("Could not store DesignDocument: " + msg);
                    }
                } finally {
                    if (response.content() != null && response.content().refCnt() > 0) {
                        response.content().release();
                    }
                }
                return designDocument;
            }
        });
    }

    @Override
    public Observable removeDesignDocument(String name) {
        return removeDesignDocument(name, false);
    }

    @Override
    public Observable removeDesignDocument(final String name, final boolean development) {
        return Observable.defer(new Func0>() {
            @Override
            public Observable call() {
                return core.send(new RemoveDesignDocumentRequest(name, development, bucket, password));
            }
        }).map(new Func1() {
            @Override
            public Boolean call(RemoveDesignDocumentResponse response) {
                if (response.content() != null && response.content().refCnt() > 0) {
                    response.content().release();
                }
                return response.status().isSuccess();
            }
        });
    }

    @Override
    public Observable publishDesignDocument(String name) {
        return publishDesignDocument(name, false);
    }

    @Override
    public Observable publishDesignDocument(final String name, final boolean overwrite) {
        return getDesignDocument(name, false)
            .isEmpty()
            .flatMap(new Func1>() {
                @Override
                public Observable call(Boolean doesNotExist) {
                    if (!doesNotExist && !overwrite) {
                        return Observable.error(new DesignDocumentAlreadyExistsException("Document exists in " +
                            "production and not overwriting."));
                    }
                    return getDesignDocument(name, true);
                }
            })
            .flatMap(new Func1>() {
                @Override
                public Observable call(DesignDocument designDocument) {
                    return upsertDesignDocument(designDocument);
                }
            });
    }

    /*==== INDEX MANAGEMENT ====*/

    private static  Func1, Observable> errorsToThrowable(final String messagePrefix) {
        return new Func1, Observable>() {
            @Override
            public Observable call(List errors) {
                return Observable.error(new CouchbaseException(messagePrefix + errors));
            }
        };
    }

    private static Func1 ROW_VALUE_TO_INDEXINFO =
        new Func1() {
            @Override
            public IndexInfo call(AsyncN1qlQueryRow asyncN1qlQueryRow) {
                return new IndexInfo(asyncN1qlQueryRow.value());
            }
        };

    @Override
    public Observable listN1qlIndexes() {
        Expression whereClause = x("keyspace_id").eq(s(bucket))
                .and(i("using").eq(s("gsi")));

        Statement listIndexes = select("idx.*").from(x("system:indexes").as("idx")).where(whereClause)
            .orderBy(Sort.desc("is_primary"), Sort.asc("name"));

        final Func1, Observable> errorHandler = errorsToThrowable(
                "Error while listing indexes: ");

        return queryExecutor.execute(
                N1qlQuery.simple(listIndexes, N1qlParams.build().consistency(ScanConsistency.REQUEST_PLUS)))
                            .flatMap(new Func1>() {
                                @Override
                                public Observable call(final AsyncN1qlQueryResult aqr) {
                                    return aqr.finalSuccess()
                                            .flatMap(new Func1>() {
                                                @Override
                                                public Observable call(Boolean success) {
                                                    if (success) {
                                                        return aqr.rows();
                                                    } else {
                                                        return aqr.errors().toList().flatMap(errorHandler);
                                                    }
                                                }
                                            });
                                }
                            }).map(ROW_VALUE_TO_INDEXINFO);
    }

    @Override
    public Observable createN1qlPrimaryIndex(final boolean ignoreIfExist, boolean defer) {
        Statement createIndex;
        UsingWithPath usingWithPath = Index.createPrimaryIndex().on(bucket);
        if (defer) {
            createIndex = usingWithPath.withDefer();
        } else {
            createIndex = usingWithPath;
        }

        return queryExecutor.execute(N1qlQuery.simple(createIndex))
            .compose(checkIndexCreation(ignoreIfExist, "Error creating primary index"));
    }

    @Override
    public Observable createN1qlPrimaryIndex(final String customName, final boolean ignoreIfExist, boolean defer) {
        Statement createIndex;
        UsingWithPath usingWithPath = Index.createNamedPrimaryIndex(customName).on(bucket);
        if (defer) {
            createIndex = usingWithPath.withDefer();
        } else {
            createIndex = usingWithPath;
        }

        return queryExecutor.execute(N1qlQuery.simple(createIndex))
            .compose(checkIndexCreation(ignoreIfExist, "Error creating custom primary index " + customName));
    }

    private static Expression expressionOrIdentifier(Object o) {
        if (o instanceof Expression) {
            return (Expression) o;
        } else if (o instanceof String) {
            return i((String) o);
        } else {
            throw new IllegalArgumentException("Fields for index must be either an Expression or a String identifier");
        }
    }

    @Override
    public Observable createN1qlIndex(final String indexName, final boolean ignoreIfExist, boolean defer, Object... fields) {
        if (fields == null || fields.length < 1) {
            throw new IllegalArgumentException("At least one field is required for secondary index");
        }

        return createN1qlIndex(indexName,  Arrays.asList(fields), null, ignoreIfExist, defer);
    }

    @Override
    public Observable createN1qlIndex(final String indexName, List fields, Expression whereClause,
            final boolean ignoreIfExist, boolean defer) {
        if (fields == null || fields.isEmpty()) {
            throw new IllegalArgumentException("At least one field is required for secondary index");
        }

        int i = -1;
        Expression firstExpression = expressionOrIdentifier(fields.get(0));
        Expression[] otherExpressions = new Expression[fields.size() - 1];
        for (Object field : fields) {
            if (i > -1) {
                otherExpressions[i] = expressionOrIdentifier(field);
            } //otherwise skip first expression, already processed
            i++;
        }

        Statement createIndex;
        UsingWithPath usingWithPath;
        if (whereClause != null) {
            usingWithPath = Index.createIndex(indexName).on(bucket, firstExpression, otherExpressions).where(whereClause);
        } else {
            usingWithPath = Index.createIndex(indexName).on(bucket, firstExpression, otherExpressions);
        }

        if (defer) {
            createIndex = usingWithPath.withDefer();
        } else {
            createIndex = usingWithPath;
        }

        return queryExecutor.execute(N1qlQuery.simple(createIndex))
                .compose(checkIndexCreation(ignoreIfExist, "Error creating secondary index " + indexName));
    }


    @Override
    public Observable dropN1qlPrimaryIndex(final boolean ignoreIfNotExist) {
        return drop(ignoreIfNotExist, Index.dropPrimaryIndex(bucket).using(IndexType.GSI), "Error dropping primary index");
    }

    @Override
    public Observable dropN1qlPrimaryIndex(String customName, boolean ignoreIfNotExist) {
        return drop(ignoreIfNotExist, Index.dropNamedPrimaryIndex(bucket, customName).using(IndexType.GSI),
                "Error dropping custom primary index \"" + customName + "\"");
    }

    @Override
    public Observable dropN1qlIndex(String name, boolean ignoreIfNotExist) {
        return drop(ignoreIfNotExist, Index.dropIndex(bucket, name).using(IndexType.GSI), "Error dropping index \"" + name + "\"");
    }

    private Observable drop(final boolean ignoreIfNotExist, Statement dropIndex, final String errorPrefix) {
        return queryExecutor.execute(N1qlQuery.simple(dropIndex))
                .flatMap(new Func1>() {
                    @Override
                    public Observable call(final AsyncN1qlQueryResult aqr) {
                        return aqr.finalSuccess()
                                .flatMap(new Func1>() {
                                    @Override
                                    public Observable call(Boolean success) {
                                        if (success) {
                                            return Observable.just(true);
                                        } else {
                                            return aqr.errors().toList()
                                                .flatMap(new Func1, Observable>() {
                                                    @Override
                                                    public Observable call(List errors) {
                                                        if (errors.size() == 1 && errors.get(0).getString("msg").contains("not found")) {
                                                            if (ignoreIfNotExist) {
                                                                return Observable.just(false);
                                                            } else {
                                                                return Observable.error(new IndexDoesNotExistException(errorPrefix));
                                                            }
                                                        } else {
                                                            return Observable.error(new CouchbaseException(errorPrefix + ": " + errors));
                                                        }
                                                    }
                                                });
                                        }
                                    }
                                });
                    }
                });
    }


    @Override
    public Observable> buildN1qlDeferredIndexes() {

        final Func1, Observable>> errorHandler = errorsToThrowable(
                "Error while triggering index build: ");

        return listN1qlIndexes()
                .filter(new Func1() {
                    @Override
                    public Boolean call(IndexInfo indexInfo) {
                        //since 4.5, pending is split into deferred then building... (see MB-14679)
                        //here we want to list the indexes that are currently deferred, build them and return that list
                        return indexInfo.state().equals("pending") || indexInfo.state().equals("deferred");
                    }
                })
                .map(new Func1() {
                    @Override
                    public String call(IndexInfo indexInfo) {
                        return indexInfo.name();
                    }
                })
                .toList()
                .flatMap(new Func1, Observable>>() {
                    @Override
                    public Observable> call(final List pendingIndexes) {
                        if (pendingIndexes.isEmpty()) {
                            return Observable.just(pendingIndexes);
                        }
                        Statement buildStatement = Index.buildIndex().on(bucket)
                                .indexes(pendingIndexes)
                                .using(IndexType.GSI);

                        return queryExecutor.execute(N1qlQuery.simple(buildStatement))
                                .flatMap(new Func1>>() {
                                    @Override
                                    public Observable> call(final AsyncN1qlQueryResult aqr) {
                                        return aqr.finalSuccess()
                                                .flatMap(new Func1>>() {
                                                    @Override
                                                    public Observable> call(
                                                            Boolean success) {
                                                        if (success) {
                                                            return Observable.just(pendingIndexes);
                                                        } else {
                                                            return aqr.errors().toList().flatMap(errorHandler);
                                                        }
                                                    }
                                                });
                                    }
                                });
                    }
                });
    }

    @Override
    public Observable watchN1qlIndexes(List watchList, final long watchTimeout,
            final TimeUnit watchTimeUnit) {
        final Set watchSet = new HashSet(watchList);

        return listN1qlIndexes()
                .flatMap(new Func1>() {
                    @Override
                    public Observable call(IndexInfo i) {
                        if (!watchSet.contains(i.name())) {
                            return Observable.empty();
                        } else if (!"online".equals(i.state()))
                            return Observable.error(new IndexesNotReadyException("Index not ready: " + i.name()));
                        else {
                            return Observable.just(i);
                        }
                    }
                })
                .doOnEach(new Action1>() {
                    @Override
                    public void call(Notification notification) {
                        if (INDEX_WATCH_LOG.isDebugEnabled()) {
                            if (notification.isOnNext()) {
                                IndexInfo info = (IndexInfo) notification.getValue();
                                String indexShortInfo = info.name() + "(" + info.state() + ")";
                                INDEX_WATCH_LOG.debug("Index ready: " + indexShortInfo);
                            } else if (notification.isOnError()) {
                                Throwable e = notification.getThrowable();
                                if (e instanceof IndexesNotReadyException) {
                                    INDEX_WATCH_LOG.debug("Will retry: " + e.getMessage());
                                }
                            }
                        }
                    }
                })
                .retryWhen(anyOf(IndexesNotReadyException.class)
                        .delay(INDEX_WATCH_DELAY)
                        .max(INDEX_WATCH_MAX_ATTEMPTS)
                        .build())
                .compose(safeAbort(watchTimeout, watchTimeUnit, null));
    }

    /**
     * A transformer (to be used with {@link Observable#compose(Observable.Transformer)}) that puts a timeout on an
     * {@link IndexInfo} Observable and will consider {@link IndexesNotReadyException} inside a
     * {@link CannotRetryException} and {@link TimeoutException} to be non-failing cases (resulting in an empty
     * observable), whereas all other errors are propagated as is.
     *
     * @param watchTimeout the timeout to set.
     * @param watchTimeUnit the time unit for the timeout.
     * @param indexName null for a global timeout, or the name of the single index being watched.
     */
    private static Observable.Transformer safeAbort(final long watchTimeout, final TimeUnit watchTimeUnit,
            final String indexName) {
        return new Observable.Transformer() {
            @Override
            public Observable call(Observable source) {
                return source.timeout(watchTimeout, watchTimeUnit)
                .onErrorResumeNext(new Func1>() {
                    @Override
                    public rx.Observable call(Throwable t) {
                        if (t instanceof TimeoutException) {
                            if (indexName == null) {
                                INDEX_WATCH_LOG.debug("Watched indexes were not all online after the given {} {}", watchTimeout, watchTimeUnit);
                            } else {
                                INDEX_WATCH_LOG.debug("Index {} was not online after the given {} {}", indexName, watchTimeout, watchTimeUnit);
                            }
                            return Observable.empty();
                        }
                        //should not happen with the INDEX_WATCH_MAX_ATTEMPTS set close to Integer.MAX_VALUE, but in case it is later tuned down...
                        if (t instanceof CannotRetryException && t.getCause() instanceof IndexesNotReadyException) {
                            INDEX_WATCH_LOG.debug("{} after {} attempts", INDEX_WATCH_MAX_ATTEMPTS, t.getCause().getMessage());
                            return Observable.empty();
                        }
                        return rx.Observable.error(t);
                    }
                });
            }
        };
    }

    /**
     * A transformer (to be used with {@link Observable#compose(Observable.Transformer)}) that takes a N1QL query result
     * and inspects the status and errors.
     *
     * If the query succeeded, emits TRUE. If there is an error but it is only that the index exists, and ignoreIfExist
     * is true, emits FALSE. Otherwise the error is propagated as a CouchbaseException.
     */
    private static Observable.Transformer checkIndexCreation(final boolean ignoreIfExist,
            final String prefixMsg) {
        return new Observable.Transformer() {
            @Override
            public Observable call(Observable sourceObservable) {
                return sourceObservable
                        .flatMap(new Func1>() {
                            @Override
                            public Observable call(final AsyncN1qlQueryResult aqr) {
                                return aqr.finalSuccess()
                                        .flatMap(new Func1>() {
                                            @Override
                                            public Observable call(Boolean success) {
                                                if (success) {
                                                    return Observable.just(true);
                                                } else {
                                                    return aqr.errors()
                                                    .toList()
                                                    .flatMap(new Func1, Observable>() {
                                                        @Override
                                                        public Observable call(List errors) {
                                                            if (errors.size() == 1 && errors.get(0).getString("msg").contains("already exist")) {
                                                                if (ignoreIfExist) {
                                                                    return Observable.just(false);
                                                                } else {
                                                                    return Observable.error(new IndexAlreadyExistsException(prefixMsg));
                                                                }
                                                            } else {
                                                                return Observable.error(new CouchbaseException(prefixMsg + ": " + errors));
                                                            }
                                                        }
                                                    });
                                                }
                                            }
                                        });
                            }
                        });
            }
        };
    }
}