Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Async #147

Merged
merged 12 commits into from
Mar 10, 2022
Merged

Async #147

merged 12 commits into from
Mar 10, 2022

Conversation

CherryPerry
Copy link
Collaborator

No description provided.

newsPublisher: NewsPublisher<Action, Effect, State, News>? = null,
featureScheduler: Scheduler? = null,
private val observationScheduler: Scheduler? = null
) : AsyncFeature<Wish, State, News> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseFeature now implements the AsyncFeature, which implies that it is async. However it is not always async, as the behaviour depends on whether the schedulers are passed or not. Maybe introduce a separate BaseAsyncFeature which will require schedulers?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case we need carefully support 2 almost the same features, most of the code will be just copy-pasted to new BaseAsyncFeature.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. You can try extracting/delegating things. From my point of view cleaner API is more important than implementation details boilerplate. Also with the current approach it is possible violates the Liskov Substitution principle, as you can't freely replace one AsyncFeature with another, and one Feature with another. They may behave differently.

disposables +=
actionSubject
.observeOnNullable(featureScheduler)
.subscribe { invokeActor(state, it) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI, switching threads when invoking the Actor and/or invoking the Reducer maybe dangerous and can lead to hard-to-debug bugs. E.g. when disabling a view on button click, when done asynchronously, will allow the user to click the button multiple times. Also dispatching states/news synchronously allows other components to process them in a blocking manner. Switching threads will queue all the actions, and so in the feature you will not receive updates synchronously. All these race conditions are error-prone and not obvious.

We should proceed with care.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, that is why it won't be a recommendation to use it across the company and will be used to fix particular issue.

private class ActorWrapper<State : Any, Action : Any, Effect: Any>(
private val threadVerifier: SameThreadVerifier,
private val disposables: DisposableCollection,
private operator fun CompositeDisposable.plusAssign(any: Any?) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we raise an exception if this is not a disposable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little worried to add it here, never checked how it actually works.
The current implementation is copied from DisposableCollection which I replaced with build-in CompositeDisposable. StandaloneMiddleware is Disposable, but other middlewares might not be. I am not ready to address this issue in this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough

if (disposables.isDisposed) return
val state = stateSubject.value!!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a description explaining what happened in case this throws a npe?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced with requireNotNull, but it can't be null because we initialise BehaviourSubject with initial value.

.subscribe { effect -> invokeReducer(action, effect) }
)
// Disposable might be disposed instantly in case of Observable.just
disposable.get()?.takeIf { !it.isDisposed }?.addTo(disposables)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we wrap the actor invoke in a defer to avoid the Observable just causing this problem ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observable.defer { Observable.just } without subscribeOn is the same as just Observable.just.
I do not want to add subscribeOn here because we are already on featureScheduler and subscribeOn here will lead to delays and crippled stacktrace.

Made code a little bit better anyway.

internal fun <T> Observable<T>.subscribeOnNullable(scheduler: Scheduler?): Observable<T> =
if (scheduler != null) subscribeOn(scheduler) else this

internal fun <T> Subject<T>.serializeIfNotNull(param: Any?): Subject<T> =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking: the overhead of serialization should be negligible, so I would just always serialize to make the code simpler. Also I could not find any places where subjects may be called concurrently, the serialization seems unnecessary, unless I'm missing something.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subjects are exposed externally, it's possible that a developer could see unexpected behaviours if they subscribe on a different thread as the state is emitted.

Though I agree that perhaps this issue already exists, and making it always serialised might be safer

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is not it possible to get a race condition during subscription (any thread) and emission (feature thread) at the same time?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that subscription should be always thread safe, but better to double check the implementation details. As far as I know, the serialization only prevents race conditions when sending events to subjects. E.g. the subject can call subscribers concurrently when onNext is called concurrently from different threads. Serialization prevents this at least.

@CherryPerry CherryPerry requested a review from zsoltk March 10, 2022 12:22
# Conflicts:
#	build.gradle
#	mvicore/build.gradle
#	mvicore/src/test/java/com/badoo/mvicore/newspublishing/NewsPublishingTest.kt
@CherryPerry CherryPerry merged commit 93d083f into badoo:master Mar 10, 2022
@CherryPerry CherryPerry deleted the async-actor branch March 10, 2022 17:57
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants