blob: 2aa9e0a8ab9c3f3d7d04fb7a593d2f7c73a13eea [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.rx.syncbase;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.robotninjas.concurrent.FluentFutures;
import io.v.rx.RxInputChannel;
import io.v.rx.VFn;
import io.v.v23.InputChannel;
import io.v.v23.context.VContext;
import io.v.v23.services.syncbase.nosql.BatchOptions;
import io.v.v23.services.syncbase.nosql.KeyValue;
import io.v.v23.services.watch.ResumeMarker;
import io.v.v23.syncbase.nosql.BatchDatabase;
import io.v.v23.syncbase.nosql.ChangeType;
import io.v.v23.syncbase.nosql.Database;
import io.v.v23.syncbase.nosql.DatabaseCore;
import io.v.v23.syncbase.nosql.PrefixRange;
import io.v.v23.syncbase.nosql.RowRange;
import io.v.v23.syncbase.nosql.Table;
import io.v.v23.syncbase.nosql.WatchChange;
import io.v.v23.verror.NoExistException;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Value;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Action2;
import rx.functions.Func1;
import rx.functions.Func2;
import rx.subjects.ReplaySubject;
import rx.subscriptions.Subscriptions;
import static net.javacrumbs.futureconverter.guavarx.FutureConverter.toObservable;
@Accessors(prefix = "m")
@Getter
@Slf4j
public class RxTable extends RxEntity<Table, DatabaseCore> {
@AllArgsConstructor
private static class InitialArtifacts<T> {
public final Observable<T> initial;
public final ResumeMarker resumeMarker;
}
@Value
public static class Row<T> {
String mRowName;
T mValue;
}
private final VContext mVContext;
private final String mName;
private final RxDb mRxDb;
private final Observable<Table> mObservable;
public RxTable(final String name, final RxDb rxDb) {
mVContext = rxDb.getVContext();
mName = name;
mRxDb = rxDb;
mObservable = rxDb.getObservable().switchMap(this::mapFrom);
}
protected RxTable(final RxTable other) {
mVContext = other.mVContext;
mName = other.mName;
mRxDb = other.mRxDb;
mObservable = other.mObservable;
}
@Override
public Observable<Table> mapFrom(final DatabaseCore db) {
final Table t = db.getTable(mName);
return toObservable(SyncbaseEntity.forTable(t).ensureExists(mVContext)).map(x -> t);
}
private <T> Observable<T> getInitial(
final BatchDatabase db, final String tableName, final String key, final TypeToken<T> tt,
final T defaultValue) {
@SuppressWarnings("unchecked")
final ListenableFuture<T> fromGet = (ListenableFuture<T>) db.getTable(tableName).get(
mVContext, key, tt == null ? Object.class : tt.getType());
return toObservable(Futures.withFallback(fromGet, t -> t instanceof NoExistException ?
Futures.immediateFuture(defaultValue) : Futures.immediateFailedFuture(t)));
}
@SuppressWarnings("unchecked")
private <T> Observable<Row<T>> getInitial(
final BatchDatabase db, final String tableName, final RowRange keys,
@Nullable final Func1<String, Boolean> keyFilter, final TypeToken<T> tt) {
Observable<KeyValue> untyped = RxInputChannel.wrap(
db.getTable(tableName).scan(mVContext, keys)).autoConnect();
if (keyFilter != null) {
untyped = untyped.filter(kv -> keyFilter.call(kv.getKey()));
}
return untyped.concatMap(VFn.wrap(kv -> new Row<>(kv.getKey(),
(T) VomUtil.decode(kv.getValue(), tt == null ? Object.class : tt.getType()))));
}
/**
* Wraps a prefix watch stream in a key-specific observable. It remains to be seen whether it
* will be better to feature-request an exact-match watch API from Syncbase or consolidate all
* watches into one stream. Exact-match presents a cleaner API boundary but results in more
* underlying streams, whereas consolidating at the library level will usually be more efficient
* unless large portions of data won't need to be watched, and also it opens up questions of
* whether we should computationally optimize the prefix query.
*
* @return an observable wrapping the watch stream. This observable should only be subscribed to
* once, as we can only iterate over the underlying stream once.
*/
private static <T> Observable<SingleWatchEvent<T>> observeWatchStream(
final InputChannel<WatchChange> s, final String key, final TypeToken<T> tt,
final T defaultValue) {
return RxInputChannel.wrap(s)
.autoConnect()
.filter(c -> c.getRowName().equals(key))
// About the Vfn.wrap, on error, the wrapping replay will disconnect,
// calling cancellation (see cancelOnDisconnect). The possible source of
// VException here is VOM decoding.
.concatMap(VFn.wrap(c -> SingleWatchEvent.fromWatchChange(c, tt, defaultValue)))
.distinctUntilChanged();
}
private static class RangeWatchBatchWindower<T> {
private final Subscriber<? super RangeWatchBatch<T>> mSubscriber;
private ReplaySubject<RangeWatchEvent<T>> mSub;
private void ensureBatch(final ResumeMarker resumeMarker) {
if (mSub == null) {
mSub = ReplaySubject.create();
mSubscriber.onNext(new RangeWatchBatch<>(resumeMarker, mSub));
}
}
public RangeWatchBatchWindower(final Subscriber<? super RangeWatchBatch<T>> subscriber) {
mSubscriber = subscriber;
mSubscriber.add(Subscriptions.create(this::onBatchEnd));
}
public void onNext(final ResumeMarker resumeMarker, final RangeWatchEvent<T> change) {
ensureBatch(resumeMarker);
mSub.onNext(change);
}
public void onError(final ResumeMarker resumeMarker, final Throwable t) {
ensureBatch(resumeMarker);
mSub.onError(t);
mSub = null;
}
public void onBatchEnd() {
if (mSub != null) {
mSub.onCompleted();
mSub = null;
}
}
}
/**
* Wraps a watch stream in an observable.
*
* @return an observable wrapping the watch stream, grouped by batches. These observables should
* only be subscribed to once, as we can only iterate over the underlying stream once.
*/
private static <T> Observable<RangeWatchBatch<T>> observeWatchStream(
final InputChannel<WatchChange> s, @Nullable final Func1<String, Boolean> prefixFilter,
final TypeToken<T> tt) {
// TODO(rosswang): support other RowRange types
final Observable<WatchChange> raw = RxInputChannel.wrap(s).autoConnect();
return Observable.create(subscriber -> {
final RangeWatchBatchWindower<T> windower =
new RangeWatchBatchWindower<>(subscriber);
subscriber.add(raw.subscribe(c -> {
if (prefixFilter == null || prefixFilter.call(c.getRowName())) {
try {
windower.onNext(c.getResumeMarker(),
RangeWatchEvent.fromWatchChange(c, tt));
} catch (final VException e) {
windower.onError(c.getResumeMarker(), e);
}
}
if (!c.isContinued()) {
windower.onBatchEnd();
}
},
subscriber::onError,
subscriber::onCompleted
));
}
);
}
private void cancelContextOnDisconnect(final Subscriber<?> subscriber,
final VContext cancelable,
final String prefix) {
subscriber.add(Subscriptions.create(() -> {
log.debug("Cancelling watch on {}: {}", mName, prefix);
cancelable.cancel();
}));
}
private <T, I, C> void subscribeWatch(
final Subscriber<T> subscriber, final Database db,
final String prefix, final Func1<BatchDatabase, Observable<I>> getInitial,
final Func1<InputChannel<WatchChange>, Observable<C>> observeWatchStream,
final Func2<InitialArtifacts<I>, Observable<C>, Observable<? extends T>> mergeInitial) {
// Watch will not work properly unless the table exists (sync will not create the table),
// and table creation must happen outside the batch.
// https://github.com/vanadium/issues/issues/857
mapFrom(db)
.switchMap(t -> toObservable(db.beginBatch(mVContext, new BatchOptions("", true))))
.switchMap(batch -> {
final Observable<I> initial = getInitial.call(batch);
return toObservable(batch.getResumeMarker(mVContext)).map(r ->
new InitialArtifacts<>(initial.doOnTerminate(() -> FluentFutures.from(
batch.abort(mVContext)).onFailure(t -> log.warn(
"Unable to abort watch initial read query", t))), r));
})
.switchMap(i -> {
final VContext cancelable = mVContext.withCancel();
cancelContextOnDisconnect(subscriber, cancelable, prefix);
log.debug("Watching {}: {}", mName, prefix);
return mergeInitial.call(i, observeWatchStream.call(
db.watch(cancelable, mName, prefix, i.resumeMarker)));
}).subscribe(subscriber::onNext, subscriber::onError); // Don't connect onComplete
}
private <T> void subscribeWatch(final Subscriber<? super SingleWatchEvent<T>> subscriber,
final Database db, final String key, final TypeToken<T> tt,
final T defaultValue) {
subscribeWatch(subscriber, db, key,
b -> getInitial(b, mName, key, tt, defaultValue),
s -> observeWatchStream(s, key, tt, defaultValue),
(i, s) -> s.startWith(i.initial.map(iv ->
new SingleWatchEvent<>(iv, i.resumeMarker, false))));
}
private <T> void subscribeWatch(
final Subscriber<? super RangeWatchBatch<T>> subscriber, final Database db,
final PrefixRange prefix, @Nullable final Func1<String, Boolean> keyFilter,
final TypeToken<T> tt) {
subscribeWatch(subscriber, db, prefix.getPrefix(),
b -> getInitial(b, mName, prefix, keyFilter, tt),
s -> RxTable.observeWatchStream(s, keyFilter, tt),
(i, s) -> s.startWith(new RangeWatchBatch<>(i.resumeMarker, i.initial.map(r ->
new RangeWatchEvent<>(r, ChangeType.PUT_CHANGE, false)))));
}
// TODO(rosswang): Cache this by args.
// TODO(rosswang): Possibly unsubscribe previous watch on mRxDb onNext.
private <T> Observable<T> watch(final Action2<Database, Subscriber<? super T>> subscribeWatch) {
return Observable.<T>create(s -> mRxDb.getObservable()
//onComplete is connected by subscribeWatch/observeWatchStream.subscribe
.subscribe(db -> subscribeWatch.call(db, s), s::onError));
}
/**
* Watches a specific Syncbase row for changes.
*/
public <T> Observable<SingleWatchEvent<T>> watch(final String key, final TypeToken<T> tt,
final T defaultValue) {
return this.<SingleWatchEvent<T>>watch((db, s) ->
subscribeWatch(s, db, key, tt, defaultValue))
// Don't create new watch streams for subsequent subscribers, but do cancel the
// stream if no subscribers are listening (and restart if new subscriptions happen).
.replay(1)
.refCount();
}
/**
* Watches a specific Syncbase row for changes.
*/
public <T> Observable<SingleWatchEvent<T>> watch(final String key, final Class<T> type,
final T defaultValue) {
return watch(key, TypeToken.of(type), defaultValue);
}
/**
* Watches a Syncbase prefix for changes.
*/
public <T> Observable<RangeWatchBatch<T>> watch(
final PrefixRange prefix, @Nullable final Func1<String, Boolean> keyFilter,
final TypeToken<T> tt) {
return watch((db, s) -> subscribeWatch(s, db, prefix, keyFilter, tt));
}
/**
* Watches a Syncbase prefix for changes.
*/
public <T> Observable<RangeWatchBatch<T>> watch(
final PrefixRange prefix, @Nullable final Func1<String, Boolean> keyFilter,
final Class<T> type) {
return watch(prefix, keyFilter, TypeToken.of(type));
}
/**
* Creates an autoConnect observable that performs the given operation upon subscription (once
* a Syncbase client is available).
*/
public <T> Observable<T> exec(final Func1<Table, ListenableFuture<T>> op) {
return once()
.flatMap(t -> toObservable(op.call(t)))
.replay(1).autoConnect();
}
public <T> Observable<Void> put(final String key, final T value,
final TypeToken<T> tt) {
return exec(t -> t.put(mVContext, key, value, tt.getType()));
}
public <T> Observable<Void> put(final String key, final T value,
final Class<T> type) {
return put(key, value, TypeToken.of(type));
}
@SuppressWarnings("unchecked")
public <T> Observable<Void> put(final String key, @NonNull final T value) {
return put(key, value, (Class<T>) value.getClass());
}
@SuppressWarnings("unchecked")
public <T> Observable<T> get(final String key, final TypeToken<? extends T> tt) {
return exec(t -> t.get(mVContext, key, tt.getType()))
.map(x -> (T) x);
}
public <T> Observable<T> get(final String key, final Class<? extends T> type) {
return get(key, TypeToken.of(type));
}
public <T> Observable<T> get(final String key, final Class<? extends T> type,
final T defaultValue) {
return get(key, type).onErrorResumeNext(t -> t instanceof NoExistException ?
Observable.just(defaultValue) : Observable.error(t));
}
public Observable<Void> delete(final String key) {
return exec(t -> t.delete(mVContext, key));
}
public Observable<Void> destroy() {
return exec(t -> t.destroy(mVContext));
}
}