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

com.landawn.abacus.util.Observer Maven / Gradle / Ivy

/*
 * Copyright (C) 2017 HaiYang Li
 *
 * 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.landawn.abacus.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import com.landawn.abacus.annotation.NonNull;
import com.landawn.abacus.util.u.Holder;
import com.landawn.abacus.util.function.Consumer;
import com.landawn.abacus.util.function.Function;
import com.landawn.abacus.util.function.Predicate;

/**
 * 
 * 
 * @since 0.9
 * 
 * @author Haiyang Li
 * 
 */
public abstract class Observer {

    private static final Object COMPLETE_FLAG = new Object();

    protected static final double INTERVAL_FACTOR = 3;

    protected static final Runnable EMPTY_ACTION = new Runnable() {
        @Override
        public void run() {
            // Do nothing;            
        }
    };

    protected static final Consumer ON_ERROR_MISSING = new Consumer() {
        @Override
        public void accept(Exception t) {
            throw new RuntimeException(t);
        }
    };

    protected static final Executor asyncExecutor = new ThreadPoolExecutor(IOUtil.IS_PLATFORM_ANDROID ? IOUtil.CPU_CORES : 8,
            IOUtil.IS_PLATFORM_ANDROID ? IOUtil.CPU_CORES : 32, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());

    protected static final ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(IOUtil.IS_PLATFORM_ANDROID ? IOUtil.CPU_CORES : 32);

    static {
        scheduler.setRemoveOnCancelPolicy(true);
    }

    protected final Map, Long> scheduledFutures = new LinkedHashMap<>();
    protected final Dispatcher dispatcher;
    protected boolean hasMore = true;

    protected Observer() {
        this(new Dispatcher<>());
    }

    protected Observer(Dispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    @SuppressWarnings("rawtypes")
    public static void complete(BlockingQueue queue) {
        ((Queue) queue).offer(COMPLETE_FLAG);
    }

    public static  Observer of(final BlockingQueue queue) {
        N.checkArgNotNull(queue, "queue");

        return new BlockingQueueObserver<>(queue);
    }

    public static  Observer of(final Collection c) {
        return of(N.isNullOrEmpty(c) ? ObjIterator. empty() : c.iterator());
    }

    public static  Observer of(final Iterator iter) {
        N.checkArgNotNull(iter, "iterator");

        return new IteratorObserver<>(iter);
    }

    /**
     *  
     * @param delayInMillis
     * @return
     * @see RxJava#timer
     */
    public static Observer timer(long delayInMillis) {
        return timer(delayInMillis, TimeUnit.MILLISECONDS);
    }

    /**
     *  
     * @param delay
     * @param unit
     * @return
     * @see RxJava#timer
     */
    public static Observer timer(long delay, TimeUnit unit) {
        N.checkArgument(delay >= 0, "delay can't be negative");
        N.checkArgNotNull(unit, "Time unit can't be null");

        return new TimerObserver<>(delay, unit);
    }

    /**
     *   
     * @param periodInMillis
     * @return
     * @see RxJava#interval
     */
    public static Observer interval(long periodInMillis) {
        return interval(0, periodInMillis, TimeUnit.MILLISECONDS);
    }

    /**
     * @param initialDelayInMillis
     * @param periodInMillis
     * @return
     * @see RxJava#interval
     */
    public static Observer interval(long initialDelayInMillis, long periodInMillis) {
        return interval(initialDelayInMillis, periodInMillis, TimeUnit.MILLISECONDS);
    }

    /**
     *   
     * @param period
     * @param unit
     * @return
     * @see RxJava#interval
     */
    public static Observer interval(long period, TimeUnit unit) {
        return interval(0, period, unit);
    }

    /**
     *  
     * @param initialDelay
     * @param period
     * @param unit
     * @return
     * @see RxJava#interval
     */
    public static Observer interval(long initialDelay, long period, TimeUnit unit) {
        N.checkArgument(initialDelay >= 0, "initialDelay can't be negative");
        N.checkArgument(period > 0, "period can't be 0 or negative");
        N.checkArgNotNull(unit, "Time unit can't be null");

        return new IntervalObserver<>(initialDelay, period, unit);
    }

    /**
     * 
     * @param intervalDurationInMillis
     * @return this instance.
     * @see RxJava#debounce
     */
    public Observer debounce(final long intervalDurationInMillis) {
        return debounce(intervalDurationInMillis, TimeUnit.MILLISECONDS);
    }

    /**
     * 
     * @param intervalDuration
     * @param unit
     * @return this instance.
     * @see RxJava#debounce
     */
    public Observer debounce(final long intervalDuration, final TimeUnit unit) {
        N.checkArgument(intervalDuration >= 0, "Interval can't be negative");
        N.checkArgNotNull(unit, "Time unit can't be null");

        if (intervalDuration == 0) {
            return this;
        }

        final long intervalDurationInMillis = unit.toMillis(intervalDuration);

        dispatcher.append(new Dispatcher() {
            private long prevTimestamp = 0;
            private long lastScheduledTime = 0;

            @Override
            public void onNext(final Object param) {
                synchronized (holder) {
                    final long now = System.currentTimeMillis();

                    if (holder.value() == N.NULL_MASK || now - lastScheduledTime > intervalDurationInMillis * INTERVAL_FACTOR) {
                        holder.setValue(param);
                        prevTimestamp = now;

                        schedule(intervalDuration, unit);
                    } else {
                        holder.setValue(param);
                        prevTimestamp = now;
                    }
                }
            }

            private void schedule(final long delay, final TimeUnit unit) {
                try {
                    scheduler.schedule(new Runnable() {
                        @Override
                        public void run() {
                            final long pastIntervalInMills = System.currentTimeMillis() - prevTimestamp;

                            if (pastIntervalInMills >= intervalDurationInMillis) {
                                Object lastParam = null;

                                synchronized (holder) {
                                    lastParam = holder.value();
                                    holder.setValue(N.NULL_MASK);
                                }

                                if (lastParam != N.NULL_MASK && downDispatcher != null) {
                                    downDispatcher.onNext(lastParam);
                                }
                            } else {
                                schedule(intervalDurationInMillis - pastIntervalInMills, TimeUnit.MILLISECONDS);
                            }
                        }
                    }, delay, unit);

                    lastScheduledTime = System.currentTimeMillis();
                } catch (Exception e) {
                    holder.setValue(N.NULL_MASK);

                    if (downDispatcher != null) {
                        downDispatcher.onError(e);
                    }
                }
            }
        });

        return this;
    }

    /**
     * 
     * @param intervalDurationInMillis
     * @return this instance.
     * @see RxJava#throttleFirst
     */
    public Observer throttleFirst(final long intervalDurationInMillis) {
        return throttleFirst(intervalDurationInMillis, TimeUnit.MILLISECONDS);
    }

    /**
     * 
     * @param intervalDuration
     * @param unit
     * @return this instance.
     * @see RxJava#throttleFirst
     */
    public Observer throttleFirst(final long intervalDuration, final TimeUnit unit) {
        N.checkArgument(intervalDuration >= 0, "Interval can't be negative");
        N.checkArgNotNull(unit, "Time unit can't be null");

        if (intervalDuration == 0) {
            return this;
        }

        final long intervalDurationInMillis = unit.toMillis(intervalDuration);

        dispatcher.append(new Dispatcher() {
            private long lastScheduledTime = 0;

            @Override
            public void onNext(final Object param) {
                synchronized (holder) {
                    final long now = System.currentTimeMillis();

                    if (holder.value() == N.NULL_MASK || now - lastScheduledTime > intervalDurationInMillis * INTERVAL_FACTOR) {
                        holder.setValue(param);

                        try {
                            scheduler.schedule(new Runnable() {
                                @Override
                                public void run() {
                                    Object firstParam = null;

                                    synchronized (holder) {
                                        firstParam = holder.value();
                                        holder.setValue(N.NULL_MASK);
                                    }

                                    if (firstParam != N.NULL_MASK && downDispatcher != null) {
                                        downDispatcher.onNext(firstParam);
                                    }
                                }
                            }, intervalDuration, unit);

                            lastScheduledTime = now;
                        } catch (Exception e) {
                            holder.setValue(N.NULL_MASK);

                            if (downDispatcher != null) {
                                downDispatcher.onError(e);
                            }
                        }
                    }
                }
            }
        });

        return this;
    }

    /**
     * 
     * @param intervalDurationInMillis
     * @return this instance.
     * @see RxJava#throttleLast
     */
    public Observer throttleLast(final long intervalDurationInMillis) {
        return throttleLast(intervalDurationInMillis, TimeUnit.MILLISECONDS);
    }

    /**
     * 
     * @param intervalDuration
     * @param unit
     * @return this instance.
     * @see RxJava#throttleLast
     */
    public Observer throttleLast(final long intervalDuration, final TimeUnit unit) {
        N.checkArgument(intervalDuration >= 0, "Delay can't be negative");
        N.checkArgNotNull(unit, "Time unit can't be null");

        if (intervalDuration == 0) {
            return this;
        }

        final long intervalDurationInMillis = unit.toMillis(intervalDuration);

        dispatcher.append(new Dispatcher() {
            private long lastScheduledTime = 0;

            @Override
            public void onNext(final Object param) {
                synchronized (holder) {
                    final long now = System.currentTimeMillis();

                    if (holder.value() == N.NULL_MASK || now - lastScheduledTime > intervalDurationInMillis * INTERVAL_FACTOR) {
                        holder.setValue(param);

                        try {
                            scheduler.schedule(new Runnable() {
                                @Override
                                public void run() {
                                    Object lastParam = null;

                                    synchronized (holder) {
                                        lastParam = holder.value();
                                        holder.setValue(N.NULL_MASK);
                                    }

                                    if (lastParam != N.NULL_MASK && downDispatcher != null) {
                                        downDispatcher.onNext(lastParam);
                                    }
                                }
                            }, intervalDuration, unit);

                            lastScheduledTime = now;
                        } catch (Exception e) {
                            holder.setValue(N.NULL_MASK);

                            if (downDispatcher != null) {
                                downDispatcher.onError(e);
                            }
                        }
                    } else {
                        holder.setValue(param);
                    }
                }
            }
        });

        return this;
    }

    /**
     * 
     * @param delayInMillis
     * @return this instance.
     * @see RxJava#delay
     */
    public Observer delay(final long delayInMillis) {
        return delay(delayInMillis, TimeUnit.MILLISECONDS);
    }

    /**
     * 
     * @param delayInMillis
     * @param unit
     * @return this instance.
     * @see RxJava#delay
     */
    public Observer delay(final long delay, final TimeUnit unit) {
        N.checkArgument(delay >= 0, "Delay can't be negative");
        N.checkArgNotNull(unit, "Time unit can't be null");

        if (delay == 0) {
            return this;
        }

        dispatcher.append(new Dispatcher() {
            private final long startTime = System.currentTimeMillis();
            private boolean isDelayed = false;

            @Override
            public void onNext(final Object param) {
                if (isDelayed == false) {
                    N.sleep(unit.toMillis(delay) - (System.currentTimeMillis() - startTime));
                    isDelayed = true;
                }

                if (downDispatcher != null) {
                    downDispatcher.onNext(param);
                }
            }
        });

        return this;
    }

    /**
     * 
     * @return this instance.
     * @see RxJava#timeInterval
     */
    public Observer> timeInterval() {
        dispatcher.append(new Dispatcher() {
            private long startTime = System.currentTimeMillis();

            @Override
            public synchronized void onNext(final Object param) {
                if (downDispatcher != null) {
                    long now = System.currentTimeMillis();
                    long interval = now - startTime;
                    startTime = now;

                    downDispatcher.onNext(Timed.of(param, interval));
                }
            }
        });

        return (Observer>) this;
    }

    /**
     * 
     * @return this instance.
     * @see RxJava#timestamp
     */
    public Observer> timestamp() {
        dispatcher.append(new Dispatcher() {
            @Override
            public void onNext(final Object param) {
                if (downDispatcher != null) {
                    downDispatcher.onNext(Timed.of(param, System.currentTimeMillis()));
                }
            }
        });

        return (Observer>) this;
    }

    public Observer skip(final long n) {
        N.checkArgNotNegative(n, "n");

        if (n > 0) {
            dispatcher.append(new Dispatcher() {
                private final AtomicLong counter = new AtomicLong();

                @Override
                public void onNext(final Object param) {
                    if (downDispatcher != null && counter.incrementAndGet() > n) {
                        downDispatcher.onNext(param);
                    }
                }
            });
        }

        return this;
    }

    public Observer limit(final long maxSize) {
        N.checkArgNotNegative(maxSize, "maxSize");

        dispatcher.append(new Dispatcher() {
            private final AtomicLong counter = new AtomicLong();

            @Override
            public void onNext(final Object param) {
                if (downDispatcher != null && counter.incrementAndGet() <= maxSize) {
                    downDispatcher.onNext(param);
                } else {
                    hasMore = false;
                }
            }
        });

        return this;
    }

    /**
     * 
     * @return
     */
    public Observer distinct() {
        dispatcher.append(new Dispatcher() {
            private Set set = new HashSet<>();

            @Override
            public void onNext(final Object param) {
                if (downDispatcher != null && set.add((T) param)) {
                    downDispatcher.onNext(param);
                }
            }
        });

        return this;
    }

    /**
     * 
     * @param keyMapper
     * @return
     */
    public Observer distinctBy(final Function keyMapper) {
        dispatcher.append(new Dispatcher() {
            private Set set = new HashSet<>();

            @Override
            public void onNext(final Object param) {
                if (downDispatcher != null && set.add(keyMapper.apply((T) param))) { // onError if keyMapper.apply throws exception?
                    downDispatcher.onNext(param);
                }
            }
        });

        return this;
    }

    public Observer filter(final Predicate filter) {
        dispatcher.append(new Dispatcher() {
            @Override
            public void onNext(final Object param) {
                if (downDispatcher != null && filter.test((T) param) == true) { // onError if filter.test throws exception?
                    downDispatcher.onNext(param);
                }
            }
        });

        return this;
    }

    public  Observer map(final Function map) {
        dispatcher.append(new Dispatcher() {
            @Override
            public void onNext(final Object param) {
                if (downDispatcher != null) {
                    downDispatcher.onNext(map.apply((T) param)); // onError if map.apply throws exception?
                }
            }
        });

        return (Observer) this;
    }

    public  Observer flatMap(final Function> map) {
        dispatcher.append(new Dispatcher() {
            @Override
            public void onNext(final Object param) {
                if (downDispatcher != null) {
                    final Collection c = map.apply((T) param); // onError if map.apply throws exception?

                    if (N.notNullOrEmpty(c)) {
                        for (U u : c) {
                            downDispatcher.onNext(u);
                        }
                    }
                }
            }
        });

        return (Observer) this;
    }

    /**
     * 
     * @param timespan
     * @param unit
     * @return this instance
     * @see RxJava#window(long, java.util.concurrent.TimeUnit)
     */
    public Observer> buffer(final long timespan, final TimeUnit unit) {
        return buffer(timespan, unit, Integer.MAX_VALUE);
    }

    /**
     * 
     * @param timespan
     * @param unit
     * @return this instance
     * @see RxJava#window(long, java.util.concurrent.TimeUnit, int)
     */
    public Observer> buffer(final long timespan, final TimeUnit unit, final int count) {
        N.checkArgument(timespan > 0, "timespan can't be 0 or negative");
        N.checkArgNotNull(unit, "Time unit can't be null");
        N.checkArgument(count > 0, "count can't be 0 or negative");

        dispatcher.append(new Dispatcher() {
            private final List queue = new ArrayList<>();

            {
                scheduledFutures.put(scheduler.scheduleAtFixedRate(new Runnable() {
                    @Override
                    public void run() {
                        List list = null;
                        synchronized (queue) {
                            list = new ArrayList<>(queue);
                            queue.clear();
                        }

                        if (downDispatcher != null) {
                            downDispatcher.onNext(list);
                        }
                    }

                }, timespan, timespan, unit), timespan);
            }

            @Override
            public void onNext(final Object param) {
                List list = null;

                synchronized (queue) {
                    queue.add((T) param);

                    if (queue.size() == count) {
                        list = new ArrayList<>(queue);
                        queue.clear();
                    }
                }

                if (list != null && downDispatcher != null) {
                    downDispatcher.onNext(list);
                }
            }
        });

        return (Observer>) this;
    }

    /**
     * 
     * @param timespan
     * @param timeskip
     * @param unit
     * @return
     * @see RxJava#window(long, long, java.util.concurrent.TimeUnit)
     */
    public Observer> buffer(final long timespan, final long timeskip, final TimeUnit unit) {
        return buffer(timespan, timeskip, unit, Integer.MAX_VALUE);
    }

    /**
     * 
     * @param timespan
     * @param timeskip
     * @param unit
     * @param count
     * @return
     * @see RxJava#window(long, long, java.util.concurrent.TimeUnit)
     */
    public Observer> buffer(final long timespan, final long timeskip, final TimeUnit unit, final int count) {
        N.checkArgument(timespan > 0, "timespan can't be 0 or negative");
        N.checkArgument(timeskip > 0, "timeskip can't be 0 or negative");
        N.checkArgNotNull(unit, "Time unit can't be null");
        N.checkArgument(count > 0, "count can't be 0 or negative");

        dispatcher.append(new Dispatcher() {
            private final long startTime = System.currentTimeMillis();
            private final long interval = timespan + timeskip;
            private final List queue = new ArrayList<>();

            {
                scheduledFutures.put(scheduler.scheduleAtFixedRate(new Runnable() {
                    @Override
                    public void run() {
                        List list = null;
                        synchronized (queue) {
                            list = new ArrayList<>(queue);
                            queue.clear();
                        }

                        if (downDispatcher != null) {
                            downDispatcher.onNext(list);
                        }
                    }

                }, timespan, interval, unit), interval);
            }

            @Override
            public void onNext(final Object param) {
                if ((System.currentTimeMillis() - startTime) % interval <= timespan) {
                    List list = null;

                    synchronized (queue) {
                        queue.add((T) param);

                        if (queue.size() == count) {
                            list = new ArrayList<>(queue);
                            queue.clear();
                        }
                    }

                    if (list != null && downDispatcher != null) {
                        downDispatcher.onNext(list);
                    }
                }
            }
        });

        return (Observer>) this;
    }

    public void observe(final Consumer action) {
        observe(action, ON_ERROR_MISSING);
    }

    public void observe(final Consumer action, final Consumer onError) {
        observe(action, onError, EMPTY_ACTION);
    }

    public abstract void observe(final Consumer action, final Consumer onError, final Runnable onComplete);

    void cancelScheduledFutures() {
        final long startTime = System.currentTimeMillis();

        if (N.notNullOrEmpty(scheduledFutures)) {
            for (Map.Entry, Long> entry : scheduledFutures.entrySet()) {
                final long delay = entry.getValue();

                N.sleep(delay - (System.currentTimeMillis() - startTime)
                        + delay /* Extending another delay just want to make sure last schedule can be completed before the schedule task is cancelled*/);

                entry.getKey().cancel(false);
            }
        }
    }

    protected static class Node {
        public final T value;
        public Node next;

        public Node(final T value) {
            this(value, null);
        }

        public Node(final T value, Node next) {
            this.value = value;
            this.next = next;
        }
    }

    protected static class Dispatcher {
        protected final Holder holder = Holder.of(N.NULL_MASK);
        protected Dispatcher downDispatcher;

        public void onNext(@NonNull final T value) {
            if (downDispatcher != null) {
                downDispatcher.onNext(value);
            }
        }

        /**
         * Signal a Exception exception.
         * @param error the Exception to signal, not null
         */
        public void onError(@NonNull final Exception error) {
            if (downDispatcher != null) {
                downDispatcher.onError(error);
            }
        }

        /**
         * Signal a completion.
         */
        public void onComplete() {
            if (downDispatcher != null) {
                downDispatcher.onComplete();
            }
        }

        public void append(Dispatcher downDispatcher) {
            Dispatcher tmp = this;

            while (tmp.downDispatcher != null) {
                tmp = tmp.downDispatcher;
            }

            tmp.downDispatcher = downDispatcher;
        }
    }

    protected static abstract class DispatcherBase extends Dispatcher {
        private final Consumer onError;
        private final Runnable onComplete;

        protected DispatcherBase(final Consumer onError, final Runnable onComplete) {
            this.onError = onError;
            this.onComplete = onComplete;
        }

        @Override
        public void onError(final Exception error) {
            onError.accept(error);
        }

        @Override
        public void onComplete() {
            onComplete.run();
        }
    }

    protected static abstract class ObserverBase extends Observer {
        protected ObserverBase() {

        }
    }

    static final class BlockingQueueObserver extends ObserverBase {
        private final BlockingQueue queue;

        BlockingQueueObserver(final BlockingQueue queue) {
            this.queue = queue;
        }

        @Override
        public void observe(final Consumer action, final Consumer onError, final Runnable onComplete) {
            N.checkArgNotNull(action, "action");

            dispatcher.append(new DispatcherBase(onError, onComplete) {
                @Override
                public void onNext(Object param) {
                    action.accept((T) param);
                }
            });

            asyncExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    T next = null;
                    boolean isOnError = true;

                    try {
                        while (hasMore && (next = queue.poll(Long.MAX_VALUE, TimeUnit.MILLISECONDS)) != COMPLETE_FLAG) {
                            isOnError = false;

                            dispatcher.onNext(next);

                            isOnError = true;
                        }

                        isOnError = false;

                        onComplete.run();
                    } catch (Exception e) {
                        if (isOnError) {
                            onError.accept(e);
                        } else {
                            throw N.toRuntimeException(e);
                        }
                    } finally {
                        cancelScheduledFutures();
                    }
                }
            });
        }
    }

    static final class IteratorObserver extends ObserverBase {
        private final Iterator iter;

        IteratorObserver(final Iterator iter) {
            this.iter = iter;
        }

        @Override
        public void observe(final Consumer action, final Consumer onError, final Runnable onComplete) {
            N.checkArgNotNull(action, "action");

            dispatcher.append(new DispatcherBase(onError, onComplete) {
                @Override
                public void onNext(Object param) {
                    action.accept((T) param);
                }
            });

            asyncExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    boolean isOnError = true;

                    try {
                        while (hasMore && iter.hasNext()) {
                            isOnError = false;

                            dispatcher.onNext(iter.next());

                            isOnError = true;
                        }

                        isOnError = false;

                        onComplete.run();
                    } catch (Exception e) {
                        if (isOnError) {
                            onError.accept(e);
                        } else {
                            throw N.toRuntimeException(e);
                        }
                    } finally {
                        cancelScheduledFutures();
                    }
                }
            });
        }

    }

    static final class TimerObserver extends ObserverBase {
        private final long delay;
        private final TimeUnit unit;

        TimerObserver(long delay, TimeUnit unit) {
            this.delay = delay;
            this.unit = unit;
        }

        @Override
        public void observe(final Consumer action, final Consumer onError, final Runnable onComplete) {
            N.checkArgNotNull(action, "action");

            dispatcher.append(new DispatcherBase(onError, onComplete) {
                @Override
                public void onNext(Object param) {
                    action.accept((T) param);
                }
            });

            scheduler.schedule(new Runnable() {
                @Override
                public void run() {
                    try {
                        dispatcher.onNext(0L);

                        onComplete.run();
                    } finally {
                        cancelScheduledFutures();
                    }
                }
            }, delay, unit);
        }
    }

    static final class IntervalObserver extends ObserverBase {
        private final long initialDelay;
        private final long period;
        private final TimeUnit unit;
        private ScheduledFuture future = null;

        IntervalObserver(long initialDelay, long period, TimeUnit unit) {
            this.initialDelay = initialDelay;
            this.period = period;
            this.unit = unit;
        }

        @Override
        public void observe(final Consumer action, final Consumer onError, final Runnable onComplete) {
            N.checkArgNotNull(action, "action");

            dispatcher.append(new DispatcherBase(onError, onComplete) {
                @Override
                public void onNext(Object param) {
                    action.accept((T) param);
                }
            });

            future = scheduler.scheduleAtFixedRate(new Runnable() {
                private long val = 0;

                @Override
                public void run() {
                    if (hasMore == false) {
                        try {
                            dispatcher.onComplete();
                        } finally {
                            try {
                                future.cancel(true);
                            } finally {
                                cancelScheduledFutures();
                            }
                        }
                    } else {
                        try {
                            dispatcher.onNext(val++);
                        } catch (Exception e) {
                            try {
                                future.cancel(true);
                            } finally {
                                cancelScheduledFutures();
                            }
                        }
                    }
                }
            }, initialDelay, period, unit);
        }
    }
}