/* * Copyright (c) 2016 Couchbase, 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 com.couchbase.client.core.message.observe; import com.couchbase.client.core.ClusterFacade; import com.couchbase.client.core.DocumentMutationLostException; import com.couchbase.client.core.ReplicaNotConfiguredException; import com.couchbase.client.core.ServiceNotAvailableException; import com.couchbase.client.core.annotations.InterfaceAudience; import com.couchbase.client.core.annotations.InterfaceStability; import com.couchbase.client.core.config.CouchbaseBucketConfig; import com.couchbase.client.core.message.CouchbaseResponse; import com.couchbase.client.core.message.cluster.GetClusterConfigRequest; import com.couchbase.client.core.message.cluster.GetClusterConfigResponse; import com.couchbase.client.core.message.kv.FailoverObserveSeqnoResponse; import com.couchbase.client.core.message.kv.MutationToken; import com.couchbase.client.core.message.kv.NoFailoverObserveSeqnoResponse; import com.couchbase.client.core.message.kv.ObserveSeqnoRequest; import com.couchbase.client.core.retry.RetryStrategy; import com.couchbase.client.core.time.Delay; import rx.Observable; import rx.functions.Func0; import rx.functions.Func1; import rx.functions.Func2; import java.util.ArrayList; import java.util.List; /** * Document observe through mutation token information. * * @author Michael Nitschinger * @since 1.2.0 */ @InterfaceStability.Uncommitted @InterfaceAudience.Private public class ObserveViaMutationToken { public static Observable<Boolean> call(final ClusterFacade core, final String bucket, final String id, final MutationToken token, final Observe.PersistTo persistTo, final Observe.ReplicateTo replicateTo, final Delay delay, final RetryStrategy retryStrategy) { Observable<CouchbaseResponse> observeResponses = sendObserveRequests(core, bucket, id, token, persistTo, replicateTo, retryStrategy); return observeResponses .map(new Func1<CouchbaseResponse, ObserveItem>() { @Override public ObserveItem call(CouchbaseResponse response) { if (response instanceof FailoverObserveSeqnoResponse) { FailoverObserveSeqnoResponse fr = (FailoverObserveSeqnoResponse) response; if (fr.lastSeqNoReceived() < token.sequenceNumber()) { throw new DocumentMutationLostException("Document Mutation lost during a hard failover."); } return ObserveItem.from(token, fr); } else if (response instanceof NoFailoverObserveSeqnoResponse) { return ObserveItem.from(token, (NoFailoverObserveSeqnoResponse) response); } else { throw new IllegalStateException("Unknown failover observe response: " + response); } } }) .scan(ObserveItem.empty(), new Func2<ObserveItem, ObserveItem, ObserveItem>() { @Override public ObserveItem call(ObserveItem currentStatus, ObserveItem newStatus) { return currentStatus.add(newStatus); } }) //repetitions will occur unless errors are raised .repeatWhen(new Func1<Observable<? extends Void>, Observable<?>>() { @Override public Observable<?> call(Observable<? extends Void> observable) { return observable.zipWith( Observable.range(1, Integer.MAX_VALUE), new Func2<Void, Integer, Integer>() { @Override public Integer call(Void aVoid, Integer attempt) { return attempt; } } ).flatMap(new Func1<Integer, Observable<?>>() { @Override public Observable<?> call(Integer attempt) { return Observable.timer(delay.calculate(attempt), delay.unit()); } }); } }) //ignore intermediate states as long as they don't match the criteria .skipWhile(new Func1<ObserveItem, Boolean>() { @Override public Boolean call(ObserveItem status) { return !status.check(persistTo, replicateTo); } }) //finish as soon as the first poll that matches the whole criteria is encountered .take(1) .map(new Func1<ObserveItem, Boolean>() { @Override public Boolean call(ObserveItem observeResponses) { return true; } }); } private static Observable<CouchbaseResponse> sendObserveRequests(final ClusterFacade core, final String bucket, final String id, final MutationToken token, final Observe.PersistTo persistTo, final Observe.ReplicateTo replicateTo, RetryStrategy retryStrategy) { final boolean swallowErrors = retryStrategy.shouldRetryObserve(); return Observable.defer(new Func0<Observable<CouchbaseResponse>>() { @Override public Observable<CouchbaseResponse> call() { return core .<GetClusterConfigResponse>send(new GetClusterConfigRequest()) .map(new Func1<GetClusterConfigResponse, Integer>() { @Override public Integer call(GetClusterConfigResponse response) { CouchbaseBucketConfig conf = (CouchbaseBucketConfig) response.config().bucketConfig(bucket); int numReplicas = conf.numberOfReplicas(); if (conf.ephemeral() && persistTo.value() != 0) { throw new ServiceNotAvailableException("Ephemeral Buckets do not support " + "PersistTo."); } if (replicateTo.touchesReplica() && replicateTo.value() > numReplicas) { throw new ReplicaNotConfiguredException("Not enough replicas configured on " + "the bucket."); } if (persistTo.touchesReplica() && persistTo.value() - 1 > numReplicas) { throw new ReplicaNotConfiguredException("Not enough replicas configured on " + "the bucket."); } return numReplicas; } }) .flatMap(new Func1<Integer, Observable<CouchbaseResponse>>() { @Override public Observable<CouchbaseResponse> call(Integer replicas) { List<Observable<CouchbaseResponse>> obs = new ArrayList<Observable<CouchbaseResponse>>(); Observable<CouchbaseResponse> masterRes = core.send(new ObserveSeqnoRequest(token.vbucketUUID(), true, (short) 0, id, bucket)); if (swallowErrors) { obs.add(masterRes.onErrorResumeNext(Observable.<CouchbaseResponse>empty())); } else { obs.add(masterRes); } if (persistTo.touchesReplica() || replicateTo.touchesReplica()) { for (short i = 1; i <= replicas; i++) { Observable<CouchbaseResponse> res = core.send(new ObserveSeqnoRequest(token.vbucketUUID(), false, i, id, bucket)); if (swallowErrors) { obs.add(res.onErrorResumeNext(Observable.<CouchbaseResponse>empty())); } else { obs.add(res); } } } if (obs.size() == 1) { return obs.get(0); } else { //mergeDelayErrors will give a chance to other nodes to respond (maybe with enough //responses for the whole poll to be considered a success) return Observable.mergeDelayError(Observable.from(obs)); } } }); } }); } static class ObserveItem { private final int replicated; private final int persisted; private final boolean persistedMaster; private ObserveItem(int replicated, int persisted, boolean persistedMaster) { this.replicated = replicated; this.persisted = persisted; this.persistedMaster = persistedMaster; } public static ObserveItem empty() { return new ObserveItem(0, 0, false); } public static ObserveItem from(MutationToken token, FailoverObserveSeqnoResponse response) { boolean replicated = response.currentSeqNo() >= token.sequenceNumber(); boolean persisted = response.lastPersistedSeqNo() >= token.sequenceNumber(); return new ObserveItem( replicated && !response.master() ? 1 : 0, persisted ? 1 : 0, response.master() && persisted ); } public static ObserveItem from(MutationToken token, NoFailoverObserveSeqnoResponse response) { boolean replicated = response.currentSeqNo() >= token.sequenceNumber(); boolean persisted = response.lastPersistedSeqNo() >= token.sequenceNumber(); return new ObserveItem( replicated && !response.master() ? 1 : 0, persisted ? 1 : 0, response.master() && persisted ); } public ObserveItem add(ObserveItem other) { return new ObserveItem( this.replicated + other.replicated, this.persisted + other.persisted, this.persistedMaster || other.persistedMaster ); } public boolean check(Observe.PersistTo persistTo, Observe.ReplicateTo replicateTo) { boolean persistDone = false; boolean replicateDone = false; if (persistTo == Observe.PersistTo.MASTER) { if (persistedMaster) { persistDone = true; } } else if (persisted >= persistTo.value()) { persistDone = true; } if (replicated >= replicateTo.value()) { replicateDone = true; } return persistDone && replicateDone; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("persisted ").append(persisted); if (persistedMaster) sb.append(" (master)"); sb.append(", replicated ").append(replicated); return sb.toString(); } } }