io.reactivex.mantis.remote.observable.reconciliator.Reconciliator Maven / Gradle / Ivy
/*
* Copyright 2019 Netflix, 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 io.reactivex.mantis.remote.observable.reconciliator;
import io.mantisrx.common.metrics.Counter;
import io.mantisrx.common.metrics.Gauge;
import io.mantisrx.common.metrics.Metrics;
import io.mantisrx.common.network.Endpoint;
import io.reactivex.mantis.remote.observable.DynamicConnectionSet;
import io.reactivex.mantis.remote.observable.EndpointChange;
import io.reactivex.mantis.remote.observable.EndpointChange.Type;
import io.reactivex.mantis.remote.observable.EndpointInjector;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Observable;
import rx.Subscription;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.functions.Func2;
import rx.subjects.PublishSubject;
public class Reconciliator {
private static final Logger logger = LoggerFactory.getLogger(Reconciliator.class);
private static final AtomicBoolean startedReconciliation = new AtomicBoolean(false);
private String name;
private Subscription subscription;
private DynamicConnectionSet connectionSet;
private PublishSubject> currentExpectedSet = PublishSubject.create();
private EndpointInjector injector;
private PublishSubject reconciledChanges = PublishSubject.create();
private Metrics metrics;
private Counter reconciliationCheck;
private Gauge running;
private Gauge expectedSetSize;
Reconciliator(Builder builder) {
this.name = builder.name;
this.injector = builder.injector;
this.connectionSet = builder.connectionSet;
metrics = new Metrics.Builder()
.name("Reconciliator_" + name)
.addCounter("reconciliationCheck")
.addGauge("expectedSetSize")
.addGauge("running")
.build();
reconciliationCheck = metrics.getCounter("reconciliationCheck");
running = metrics.getGauge("running");
expectedSetSize = metrics.getGauge("expectedSetSize");
}
public Metrics getMetrics() {
return metrics;
}
private Observable deltas() {
final Map sideEffectState = new HashMap();
final PublishSubject stopReconciliator = PublishSubject.create();
return
Observable.merge(
reconciledChanges
.takeUntil(stopReconciliator)
.doOnCompleted(() -> {
logger.info("onComplete triggered for reconciledChanges");
})
.doOnError(e -> logger.error("caught exception for reconciledChanges {}", e.getMessage(), e))
,
injector
.deltas()
.doOnCompleted(new Action0() {
@Override
public void call() {
// injector has completed recieving updates, complete reconciliator
// observable
logger.info("Stopping reconciliator, injector completed.");
stopReconciliator.onNext(1);
stopReconciliation();
}
})
.doOnError(e -> logger.error("caught exception for injector deltas {}", e.getMessage(), e))
.doOnNext(new Action1() {
@Override
public void call(EndpointChange newEndpointChange) {
String id = Endpoint.uniqueHost(newEndpointChange.getEndpoint().getHost(),
newEndpointChange.getEndpoint().getPort(), newEndpointChange.getEndpoint().getSlotId());
if (sideEffectState.containsKey(id)) {
if (newEndpointChange.getType() == Type.complete) {
// remove from expecected set
expectedSetSize.decrement();
sideEffectState.remove(id);
currentExpectedSet.onNext(new HashSet(sideEffectState.values()));
}
} else {
if (newEndpointChange.getType() == Type.add) {
expectedSetSize.increment();
sideEffectState.put(id, new Endpoint(newEndpointChange.getEndpoint().getHost(),
newEndpointChange.getEndpoint().getPort(), newEndpointChange.getEndpoint().getSlotId()));
currentExpectedSet.onNext(new HashSet(sideEffectState.values()));
}
}
}
})
)
.doOnError(t -> logger.error("caught error processing reconciliator deltas {}", t.getMessage(), t))
.doOnSubscribe(
new Action0() {
@Override
public void call() {
logger.info("Subscribed to deltas for {}, clearing active connection set", name);
connectionSet.resetActiveConnections();
startReconciliation();
}
})
.doOnUnsubscribe(new Action0() {
@Override
public void call() {
logger.info("Unsubscribed from deltas for {}", name);
}
});
}
private void startReconciliation() {
if (startedReconciliation.compareAndSet(false, true)) {
logger.info("Starting reconciliation for name: " + name);
running.increment();
subscription =
Observable
.combineLatest(currentExpectedSet, connectionSet.activeConnections(),
new Func2, Set, Void>() {
@Override
public Void call(Set expected, Set actual) {
reconciliationCheck.increment();
boolean check = expected.equals(actual);
logger.debug("Check result: " + check + ", size expected: " + expected.size() + " actual: " + actual.size() + ", for values expected: " + expected + " versus actual: " + actual);
if (!check) {
// reconcile adds
Set expectedDiff = new HashSet(expected);
expectedDiff.removeAll(actual);
if (expectedDiff.size() > 0) {
for (Endpoint endpoint : expectedDiff) {
logger.info("Connection missing from expected set, adding missing connection: " + endpoint);
reconciledChanges.onNext(new EndpointChange(Type.add, endpoint));
}
}
// reconile removes
Set actualDiff = new HashSet(actual);
actualDiff.removeAll(expected);
if (actualDiff.size() > 0) {
for (Endpoint endpoint : actualDiff) {
logger.info("Unexpected connection in active set, removing connection: " + endpoint);
reconciledChanges.onNext(new EndpointChange(Type.complete, endpoint));
}
}
}
return null;
}
})
.onErrorResumeNext(new Func1>() {
@Override
public Observable extends Void> call(Throwable throwable) {
logger.error("caught error in Reconciliation for {}", name, throwable);
return Observable.empty();
}
})
.doOnCompleted(new Action0() {
@Override
public void call() {
logger.error("onComplete in Reconciliation observable chain for {}", name);
stopReconciliation();
}
})
.subscribe();
} else {
logger.info("reconciliation already started for {}", name);
}
}
private void stopReconciliation() {
if (startedReconciliation.compareAndSet(true, false)) {
logger.info("Stopping reconciliation for name: " + name);
running.decrement();
subscription.unsubscribe();
} else {
logger.info("reconciliation already stopped for name: " + name);
}
}
public Observable> observables() {
connectionSet.setEndpointInjector(new EndpointInjector() {
@Override
public Observable deltas() {
return Reconciliator.this.deltas();
}
});
return connectionSet.observables();
}
public static class Builder {
private String name;
private EndpointInjector injector;
private DynamicConnectionSet connectionSet;
public Builder connectionSet(DynamicConnectionSet connectionSet) {
this.connectionSet = connectionSet;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder injector(EndpointInjector injector) {
this.injector = injector;
return this;
}
public Reconciliator build() {
return new Reconciliator(this);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy