-
Notifications
You must be signed in to change notification settings - Fork 74
FFL-1621 Fix: set currentState in a blocking call #3068
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
|
🎯 Code Coverage 🔗 Commit SHA: 3e50ece | Docs | Datadog PR Page | Was this helpful? Give us feedback! |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #3068 +/- ##
===========================================
+ Coverage 71.40% 71.43% +0.03%
===========================================
Files 881 881
Lines 32460 32472 +12
Branches 5466 5477 +11
===========================================
+ Hits 23178 23195 +17
+ Misses 7729 7725 -4
+ Partials 1553 1552 -1
🚀 New features to boost your workflow:
|
| currentState = newState | ||
| subscription.notifyListeners { | ||
| onStateChanged(newState) | ||
| executorService.executeSafe( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
enqueue safe executions of the listeners instead of safe execution of the queue. Now listeners cannot not crash the rest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we move synchronized protection inside DDCoreSubscriptionImpl?, it seems to me the job of DDCoreSubscriptionImpl to guarantee the thread-safety no matter where it is used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DDCoreSubscription ensures atomicity wrt modifying the subscriber list during a notification loop. Our synchronized here is layering atomicity linking the notification mechanics of DDCoreSubscription with mutation of the currentState. This coupling is a specific use case of the DDCoreSubscription or another layer on top.
DDCoreSubscription is still susceptible to listeners crashing the notification loop. We could put a try-catch there, but it might be better to keep the onus on the caller (which easy to do via the block argument passed to notifyListeners.
wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need do executorService.executeSafe instead of simply calling onStateChanged(newState)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
originally, it was to ensure fairness of state changes - which we've actually lost here with the synchronized block (switched to a ReentrantLock now)
The executor lets us run the subscription consumer off-thread and not block the caller to setEvaluationContext on potential long running consumers. Would it be better to leave the thread mgmt to the caller here? Would the caller need to run their consumer specifically in the UI thread (or some other specific thread?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be better to leave the thread mgmt to the caller here?
I would say yes, it is up to the caller to do it unless we are picky about listener performance and for us it is critical for it to be fast. And then we can wrap simply onStateChanged calls in try-catch if we want to continue work after consumer threw exception. But then this exception will be handled, so maybe it is actually better to let it propagate and crash the app (since it is exception from the customer code).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am in favour of the more straightforward approach here where we put onus for threading and exception handling on the user. Fail-fast prevents us from hiding exceptions from the dev and clear docs should point them in the right direction.
...dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagsStateManager.kt
Outdated
Show resolved
Hide resolved
typotter
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @ambushwork ptal.
...dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagsStateManager.kt
Outdated
Show resolved
Hide resolved
| currentState = newState | ||
| subscription.notifyListeners { | ||
| onStateChanged(newState) | ||
| executorService.executeSafe( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DDCoreSubscription ensures atomicity wrt modifying the subscriber list during a notification loop. Our synchronized here is layering atomicity linking the notification mechanics of DDCoreSubscription with mutation of the currentState. This coupling is a specific use case of the DDCoreSubscription or another layer on top.
DDCoreSubscription is still susceptible to listeners crashing the notification loop. We could put a try-catch there, but it might be better to keep the onus on the caller (which easy to do via the block argument passed to notifyListeners.
wdyt?
| * The fair parameter ensures that threads acquire the lock in the order they requested it. | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
threads acquire the lock in the order they requested it
Does it really matter actually? I'm wondering if it is better to use ReadWriteLock since probably the number of read operations will dominate over the number of write operations, or is it a wrong assumption?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. We could use a ReentrantReadWriteLock to allow reads and still provide ordering for writes (to maintain the ordering of state change notifications sent to listeners).
In practice, I think the direct reads will not be that high, as the use case will typically be to, once on app load:
- check the current state
- listen to state changes (typically, to then load flag-critical parts of the app when the provider is READY)
At any rate, it makes sense to allow the current state to be read concurrently with notification operations. Thanks!
| currentState = newState | ||
| subscription.notifyListeners { | ||
| onStateChanged(newState) | ||
| executorService.executeSafe( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be better to leave the thread mgmt to the caller here?
I would say yes, it is up to the caller to do it unless we are picky about listener performance and for us it is critical for it to be fast. And then we can wrap simply onStateChanged calls in try-catch if we want to continue work after consumer threw exception. But then this exception will be handled, so maybe it is actually better to let it propagate and crash the app (since it is exception from the customer code).
typotter
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy new year!
Addressed the executor and lock suggestions. PTAL/
| * The fair parameter ensures that threads acquire the lock in the order they requested it. | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. We could use a ReentrantReadWriteLock to allow reads and still provide ordering for writes (to maintain the ordering of state change notifications sent to listeners).
In practice, I think the direct reads will not be that high, as the use case will typically be to, once on app load:
- check the current state
- listen to state changes (typically, to then load flag-critical parts of the app when the provider is READY)
At any rate, it makes sense to allow the current state to be read concurrently with notification operations. Thanks!
| currentState = newState | ||
| subscription.notifyListeners { | ||
| onStateChanged(newState) | ||
| executorService.executeSafe( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am in favour of the more straightforward approach here where we put onus for threading and exception handling on the user. Fail-fast prevents us from hiding exceptions from the dev and clear docs should point them in the right direction.
…manage threads and catch exceptions
0e8fb0a to
3e50ece
Compare
What does this PR do?
This PR changes the way that the current state's access and mutation is handled.
executeSafeto avoid crashing the pool before the rest of the listeners are notified.updatetStateand calls toexecuteSafeare made in a sync blockaddListenernow does work inside asynchronized(currentState)block as well to ensure atomic registration+currentState notifcationaddListeneralso runs in anexecuteSafeblockMotivation
As noted in #2998, the
currentStatewas updated synchronously erroneously and prevented other components from realizing that state directly after updatingAdditional Notes
Anything else we should know when reviewing?
Review checklist (to be filled by reviewers)