-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Keep strong references to the FileSource/OfflineManager operations callbacks #14601
Conversation
@@ -225,73 +229,50 @@ public void run() { | |||
*/ | |||
public void mergeOfflineRegions(@NonNull String path, @NonNull final MergeOfflineRegionsCallback callback) { | |||
final File src = new File(path); | |||
new FileUtils.CheckFileReadPermissionTask(new FileUtils.OnCheckFileReadPermissionListener() { |
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'm happy to keep the async tasks and convert them to use strong references, but I think that proposed, streamlined setup is actually easier to read.
57540dc
to
fd41ebb
Compare
private val activityWeakReference = WeakReference<OfflineManager.MergeOfflineRegionsCallback>(activityCallback) | ||
|
||
override fun onMerge(offlineRegions: Array<out OfflineRegion>?) { | ||
activityWeakReference.get()?.onMerge(offlineRegions) |
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.
What happens if this callback is called between onDestroy and when the activity is cleared from memory? It could be a couple of seconds until GC kicks in and releases the activity.
During this time you are trying to set styles on the map but the UI will not allow it, and it could lead to crashes like the classic Can not perform this action after onSaveInstanceState
.
That mergeDb
is also suspicious because in turn it calls mapView.getMapAsync
.
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 @pakoito. In this case, since the callback interacts with the map, it's definitely cleaner to make sure to release the reference proactively in the #onDestroy
instead of waiting for the GC.
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.
That's correct @pakoito thanks for jumping in here and share your feedback! Really appreciate it 🙏
I like this approach! Although it'd mean some extra work (as discussed in #14589 (comment)), I think it's the only option that we could implement without using a framework that handles / solves these problems for you.
Let's make sure that all the nested callbacks and objects are also cleaned up when in onActivityDestroy
(we'd also need to change the naming so it's not attached to activities) so we don't leak anything and we use application contexts (as we're working with the file system and the handler it's on the UI queue).
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.
@Guardiola31337 this code references the example activity, so I don't think we need to make the naming universal. Also, I'm not seeing anything else we should release, or am I missing something?
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.
this code references the example activity, so I don't think we need to make the naming universal
Yeap, you're right. Sorry, I overlooked it 😬
Also, I'm not seeing anything else we should release, or am I missing something?
I'm referring to mergeOfflineDatabaseFiles
which creates MergeOfflineRegionsCallback
internally
Line 305 in 9a6eb2f
mergeOfflineRegions(fileSource, file.getAbsolutePath(), new MergeOfflineRegionsCallback() { |
Wanted to make sure that there's no option to leak and we've tested (if you have already forget about it 😅).
Thanks for taking a look at this @LukasPaczos 🙏
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.
Gotcha, np! If there are no other comments/concerns I'm going to go ahead and start the work on the remaining to-do items.
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.
Gotcha, np! If there are no other comments/concerns I'm going to go ahead and start the work on the remaining to-do items.
Sounds good to me @LukasPaczos I don't know how this thread got out of order 🤔 almost missed ☝️ 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.
Github had a hard day yesterday, all is in order now :)
a824f75
to
9a6eb2f
Compare
@@ -112,6 +133,7 @@ class MergeOfflineRegionsActivity : AppCompatActivity() { | |||
|
|||
override fun onDestroy() { | |||
super.onDestroy() | |||
mergeCallback.onActivityDestroy() |
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.
😍
* Since we expect from the results of the offline merge callback to interact with the hosting activity, | ||
* we need to ensure that we are not interacting with a destroyed activity. | ||
*/ | ||
private class MergeCallback(private var activityCallback: OfflineManager.MergeOfflineRegionsCallback?) : OfflineManager.MergeOfflineRegionsCallback { |
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 not use a weak reference here? this would avoid the onActivityDestroy
setup
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.
WeakReference causes races with lifecycle events. It does prevents leaks, that's good, and also keeps a whole category of potential errors caused by calling UI methods or trying to start activities or change fragments after onDestroy
.
9a6eb2f
to
eba2f25
Compare
This one's ready for a review. |
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.
Overall, this looks good to me 👍 Well done @LukasPaczos
I left a couple of minor comments (not blocking the PR though)
@@ -205,6 +207,8 @@ public void run() { | |||
* Merge offline regions from a secondary database into the main offline database. | |||
* <p> | |||
* When the merge is completed, or fails, the {@link MergeOfflineRegionsCallback} will be invoked on the main thread. | |||
* The callback reference is <b>strongly kept</b> throughout the process, | |||
* so it needs to be wrapped in a weak reference or released on the client side if necessary. |
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.
Do we want to "recommend" to wrap it into a WeakReference
after 👀 the issues that may cause https://github.com/mapbox/mapbox-gl-native/pull/14601/files#r282389769?
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.
wrapped in a weak reference or released
Let's leave this up to the discretion of the implementation needs 🙃
if (listener != null) { | ||
listener.onError(); | ||
} | ||
listener.onError(); |
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.
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.
🤔
Does it make any real difference @Guardiola31337? The object is going to be viable to GC as soon as it goes out of any thread's scope, which is basically the same moment in time as this method returns.
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.
If the listener only needs to be consumed once that would prevent potential leaks (e.g. in the rare scenario in which the onCancelled
and onPostExecute
are called simultaneously or if the code changes in the future) and also it wouldn't hurt 😅
Anyways, saw that the PR was merged ¯\_(ツ)_/¯
Thanks @LukasPaczos
Fixes #14485 and fixes #14297.
Refs #14589 (comment).
This PR aims to remove weak references overuse during the offline database merge and resources path change operations. This could lead to the callback reference during any step of the process to be garbage collected if not kept on the client side and result in a no-op.
The initial idea of keeping weak references to the callbacks was to ensure that we are not outliving the hosting activity, which might've been passed with the callback reference. However, the callback is not required to be depending on the hosting activity and it can (and should) be wrapped in a weak reference on the client side if that's the case, which is out of our control.
In any way, debugging why the callback has outlived the hosting object is much easier than looking for a cause of a no-op, which is why I believe that this change is a general improvement.
TODO:
deprecateopted to keep them aroundFileUtils
methodsThis is an initial proposal, please let me know what you think @Guardiola31337 @tobrun and I can move forward with the remaining to-do items if you think the approach is correct.