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

io.reactivex.mantis.remote.observable.reconciliator.Reconciliator Maven / Gradle / Ivy

There is a newer version: 3.1.4
Show newest version
/*
 * 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 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