fixed storable
This commit is contained in:
parent
8b498098de
commit
846fc1368f
|
|
@ -0,0 +1,214 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2014 Netflix, Inc.
|
||||||
|
* <p>
|
||||||
|
* 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
|
||||||
|
* <p>
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* <p>
|
||||||
|
* 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 ru.touchin.roboswag.core.observables;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
import rx.Observable.OnSubscribe;
|
||||||
|
import rx.Scheduler;
|
||||||
|
import rx.Subscriber;
|
||||||
|
import rx.Subscription;
|
||||||
|
import rx.functions.Action0;
|
||||||
|
import rx.functions.Action1;
|
||||||
|
import rx.observables.ConnectableObservable;
|
||||||
|
import rx.schedulers.Schedulers;
|
||||||
|
import rx.subscriptions.CompositeSubscription;
|
||||||
|
import rx.subscriptions.Subscriptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable sequence that stays connected to the source as long as
|
||||||
|
* there is at least one subscription to the observable sequence and also it stays connected
|
||||||
|
* for cache time after everyone unsubscribe.
|
||||||
|
*
|
||||||
|
* @param <T> the value type
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"PMD.AvoidUsingVolatile", "PMD.CompareObjectsWithEquals"})
|
||||||
|
//AvoidUsingVolatile,CompareObjectsWithEquals: from OnSubscribeRefCount code
|
||||||
|
public final class OnSubscribeRefCountWithCacheTime<T> implements OnSubscribe<T> {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final ConnectableObservable<? extends T> source;
|
||||||
|
@NonNull
|
||||||
|
private volatile CompositeSubscription baseSubscription = new CompositeSubscription();
|
||||||
|
@NonNull
|
||||||
|
private final AtomicInteger subscriptionCount = new AtomicInteger(0);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Scheduler scheduler = Schedulers.computation();
|
||||||
|
private final long cacheTime;
|
||||||
|
@NonNull
|
||||||
|
private final TimeUnit cacheTimeUnit;
|
||||||
|
@Nullable
|
||||||
|
private Scheduler.Worker worker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this lock for every subscription and disconnect action.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private final ReentrantLock lock = new ReentrantLock();
|
||||||
|
|
||||||
|
public OnSubscribeRefCountWithCacheTime(@NonNull final ConnectableObservable<? extends T> source,
|
||||||
|
final long cacheTime, @NonNull final TimeUnit cacheTimeUnit) {
|
||||||
|
this.source = source;
|
||||||
|
this.cacheTime = cacheTime;
|
||||||
|
this.cacheTimeUnit = cacheTimeUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void call(@NonNull final Subscriber<? super T> subscriber) {
|
||||||
|
|
||||||
|
lock.lock();
|
||||||
|
if (subscriptionCount.incrementAndGet() == 1) {
|
||||||
|
if (worker != null) {
|
||||||
|
worker.unsubscribe();
|
||||||
|
worker = null;
|
||||||
|
}
|
||||||
|
final AtomicBoolean writeLocked = new AtomicBoolean(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// need to use this overload of connect to ensure that
|
||||||
|
// baseSubscription is set in the case that source is a
|
||||||
|
// synchronous Observable
|
||||||
|
source.connect(onSubscribe(subscriber, writeLocked));
|
||||||
|
} finally {
|
||||||
|
// need to cover the case where the source is subscribed to
|
||||||
|
// outside of this class thus preventing the Action1 passed
|
||||||
|
// to source.connect above being called
|
||||||
|
if (writeLocked.get()) {
|
||||||
|
// Action1 passed to source.connect was not called
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// ready to subscribe to source so do it
|
||||||
|
doSubscribe(subscriber, baseSubscription);
|
||||||
|
} finally {
|
||||||
|
// release the read lock
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Action1<Subscription> onSubscribe(@NonNull final Subscriber<? super T> subscriber,
|
||||||
|
@NonNull final AtomicBoolean writeLocked) {
|
||||||
|
return new Action1<Subscription>() {
|
||||||
|
@Override
|
||||||
|
public void call(@NonNull final Subscription subscription) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
baseSubscription.add(subscription);
|
||||||
|
// ready to subscribe to source so do it
|
||||||
|
doSubscribe(subscriber, baseSubscription);
|
||||||
|
} finally {
|
||||||
|
// release the write lock
|
||||||
|
lock.unlock();
|
||||||
|
writeLocked.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doSubscribe(@NonNull final Subscriber<? super T> subscriber, @NonNull final CompositeSubscription currentBase) {
|
||||||
|
subscriber.add(disconnect(currentBase));
|
||||||
|
source.unsafeSubscribe(new Subscriber<T>(subscriber) {
|
||||||
|
@Override
|
||||||
|
public void onError(@NonNull final Throwable throwable) {
|
||||||
|
cleanup();
|
||||||
|
subscriber.onError(throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(@Nullable final T item) {
|
||||||
|
subscriber.onNext(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompleted() {
|
||||||
|
cleanup();
|
||||||
|
subscriber.onCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanup() {
|
||||||
|
// on error or completion we need to unsubscribe the base subscription
|
||||||
|
// and set the subscriptionCount to 0
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
if (baseSubscription == currentBase) {
|
||||||
|
if (worker != null) {
|
||||||
|
worker.unsubscribe();
|
||||||
|
worker = null;
|
||||||
|
}
|
||||||
|
baseSubscription.unsubscribe();
|
||||||
|
baseSubscription = new CompositeSubscription();
|
||||||
|
subscriptionCount.set(0);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Subscription disconnect(@NonNull final CompositeSubscription current) {
|
||||||
|
return Subscriptions.create(new Action0() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void call() {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
if (baseSubscription == current && subscriptionCount.decrementAndGet() == 0) {
|
||||||
|
if (worker != null) {
|
||||||
|
worker.unsubscribe();
|
||||||
|
} else {
|
||||||
|
worker = scheduler.createWorker();
|
||||||
|
}
|
||||||
|
worker.schedule(new Action0() {
|
||||||
|
@Override
|
||||||
|
public void call() {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
if (subscriptionCount.get() == 0) {
|
||||||
|
baseSubscription.unsubscribe();
|
||||||
|
// need a new baseSubscription because once
|
||||||
|
// unsubscribed stays that way
|
||||||
|
worker.unsubscribe();
|
||||||
|
worker = null;
|
||||||
|
baseSubscription = new CompositeSubscription();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, cacheTime, cacheTimeUnit);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,8 +22,11 @@ package ru.touchin.roboswag.core.observables.storable;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import ru.touchin.roboswag.core.log.LcGroup;
|
import ru.touchin.roboswag.core.log.LcGroup;
|
||||||
import ru.touchin.roboswag.core.observables.ObservableResult;
|
import ru.touchin.roboswag.core.observables.ObservableResult;
|
||||||
|
import ru.touchin.roboswag.core.observables.OnSubscribeRefCountWithCacheTime;
|
||||||
import ru.touchin.roboswag.core.observables.RxUtils;
|
import ru.touchin.roboswag.core.observables.RxUtils;
|
||||||
import ru.touchin.roboswag.core.observables.storable.builders.MigratableStorableBuilder;
|
import ru.touchin.roboswag.core.observables.storable.builders.MigratableStorableBuilder;
|
||||||
import ru.touchin.roboswag.core.observables.storable.builders.NonNullStorableBuilder;
|
import ru.touchin.roboswag.core.observables.storable.builders.NonNullStorableBuilder;
|
||||||
|
|
@ -54,6 +57,8 @@ public class Storable<TKey, TObject, TStoreObject> {
|
||||||
|
|
||||||
public static final LcGroup STORABLE_LC_GROUP = new LcGroup("STORABLE");
|
public static final LcGroup STORABLE_LC_GROUP = new LcGroup("STORABLE");
|
||||||
|
|
||||||
|
private static final long CACHE_TIME = TimeUnit.SECONDS.toMillis(5);
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private final TKey key;
|
private final TKey key;
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|
@ -151,7 +156,9 @@ public class Storable<TKey, TObject, TStoreObject> {
|
||||||
.subscribeOn(storeScheduler != null ? storeScheduler : Schedulers.io())
|
.subscribeOn(storeScheduler != null ? storeScheduler : Schedulers.io())
|
||||||
.concatWith(newStoreValueEvent)
|
.concatWith(newStoreValueEvent)
|
||||||
.map(storeObject -> returnDefaultValueIfNull(storeObject, defaultValue));
|
.map(storeObject -> returnDefaultValueIfNull(storeObject, defaultValue));
|
||||||
return observeStrategy == ObserveStrategy.CACHE_STORE_VALUE ? result.replay(1).refCount() : result;
|
return observeStrategy == ObserveStrategy.CACHE_STORE_VALUE
|
||||||
|
? Observable.create(new OnSubscribeRefCountWithCacheTime<>(result.replay(1), CACHE_TIME, TimeUnit.MILLISECONDS))
|
||||||
|
: result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|
@ -173,7 +180,9 @@ public class Storable<TKey, TObject, TStoreObject> {
|
||||||
})
|
})
|
||||||
.subscribeOn(storeScheduler != null ? storeScheduler : Schedulers.computation());
|
.subscribeOn(storeScheduler != null ? storeScheduler : Schedulers.computation());
|
||||||
|
|
||||||
return observeStrategy == ObserveStrategy.CACHE_ACTUAL_VALUE ? result.replay(1).refCount() : result;
|
return observeStrategy == ObserveStrategy.CACHE_ACTUAL_VALUE
|
||||||
|
? Observable.create(new OnSubscribeRefCountWithCacheTime<>(result.replay(1), CACHE_TIME, TimeUnit.MILLISECONDS))
|
||||||
|
: result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -226,6 +235,49 @@ public class Storable<TKey, TObject, TStoreObject> {
|
||||||
return converter;
|
return converter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Observable<?> internalSet(@Nullable final TObject newValue, final boolean checkForEqualityBeforeSet) {
|
||||||
|
return (checkForEqualityBeforeSet ? valueObservable.first() : Observable.just(null))
|
||||||
|
.switchMap(value -> {
|
||||||
|
if (checkForEqualityBeforeSet && ObjectUtils.equals(value, newValue)) {
|
||||||
|
return Observable.empty();
|
||||||
|
}
|
||||||
|
return Observable
|
||||||
|
.<TStoreObject>create(subscriber -> {
|
||||||
|
try {
|
||||||
|
final TStoreObject storeObject = converter.toStoreObject(objectClass, storeObjectClass, newValue);
|
||||||
|
store.storeObject(storeObjectClass, key, storeObject);
|
||||||
|
newStoreValueEvent.onNext(storeObject);
|
||||||
|
STORABLE_LC_GROUP.i("Value of '%s' changed from '%s' to '%s'", key, value, newValue);
|
||||||
|
subscriber.onCompleted();
|
||||||
|
} catch (final Converter.ConversionException conversionException) {
|
||||||
|
STORABLE_LC_GROUP.w(conversionException, "Exception while converting value of '%s' from '%s' to store object",
|
||||||
|
key, newValue, store);
|
||||||
|
subscriber.onError(conversionException);
|
||||||
|
} catch (final Store.StoreException storeException) {
|
||||||
|
STORABLE_LC_GROUP.w(storeException, "Exception while trying to store value of '%s' to store %s", key, store);
|
||||||
|
subscriber.onError(storeException);
|
||||||
|
} catch (final RuntimeException throwable) {
|
||||||
|
STORABLE_LC_GROUP.assertion(throwable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates observable which is async setting value to store.
|
||||||
|
* It is not checking if stored value equals new value.
|
||||||
|
* In result it will be faster to not get value from store and compare but it will emit item to {@link #observe()} subscribers.
|
||||||
|
* NOTE: It could emit ONLY completed and errors events. It is not providing onNext event!
|
||||||
|
*
|
||||||
|
* @param newValue Value to set;
|
||||||
|
* @return Observable of setting process.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public Observable<?> forceSet(@Nullable final TObject newValue) {
|
||||||
|
return internalSet(newValue, false);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates observable which is async setting value to store.
|
* Creates observable which is async setting value to store.
|
||||||
* NOTE: It could emit ONLY completed and errors events. It is not providing onNext event!
|
* NOTE: It could emit ONLY completed and errors events. It is not providing onNext event!
|
||||||
|
|
@ -236,29 +288,7 @@ public class Storable<TKey, TObject, TStoreObject> {
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public Observable<?> set(@Nullable final TObject newValue) {
|
public Observable<?> set(@Nullable final TObject newValue) {
|
||||||
return valueObservable
|
return internalSet(newValue, true);
|
||||||
.first()
|
|
||||||
.switchMap(value -> ObjectUtils.equals(value, newValue)
|
|
||||||
? Observable.empty()
|
|
||||||
: Observable
|
|
||||||
.<TStoreObject>create(subscriber -> {
|
|
||||||
try {
|
|
||||||
final TStoreObject storeObject = converter.toStoreObject(objectClass, storeObjectClass, newValue);
|
|
||||||
store.storeObject(storeObjectClass, key, storeObject);
|
|
||||||
newStoreValueEvent.onNext(storeObject);
|
|
||||||
STORABLE_LC_GROUP.i("Value of '%s' changed from '%s' to '%s'", key, value, newValue);
|
|
||||||
subscriber.onCompleted();
|
|
||||||
} catch (final Converter.ConversionException conversionException) {
|
|
||||||
STORABLE_LC_GROUP.w(conversionException, "Exception while converting value of '%s' from '%s' to store object",
|
|
||||||
key, newValue, store);
|
|
||||||
subscriber.onError(conversionException);
|
|
||||||
} catch (final Store.StoreException storeException) {
|
|
||||||
STORABLE_LC_GROUP.w(storeException, "Exception while trying to store value of '%s' to store %s", key, store);
|
|
||||||
subscriber.onError(storeException);
|
|
||||||
} catch (final RuntimeException throwable) {
|
|
||||||
STORABLE_LC_GROUP.assertion(throwable);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,14 @@ public class NonNullSafeListStorable<TKey, TItemObject, TStoreObject> {
|
||||||
return storable.set(list);
|
return storable.set(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps {@link Storable#forceSet(Object)} (Object)}.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public Observable<?> forseSet(@Nullable final List<TItemObject> list) {
|
||||||
|
return storable.forceSet(list);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps {@link Storable#setCalm(Object)}.
|
* Wraps {@link Storable#setCalm(Object)}.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue