-
-
Notifications
You must be signed in to change notification settings - Fork 14
Finalizer-like feature #32
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
Finalizer-like feature #32
Conversation
|
Great! I'll take a look in upcoming days. |
src/main/java/org/rocstreaming/roctoolkit/ReferenceCollector.java
Outdated
Show resolved
Hide resolved
src/main/java/org/rocstreaming/roctoolkit/ReferenceCollector.java
Outdated
Show resolved
Hide resolved
|
LGTM! |
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've started reviewing the PR, here are two preliminary comments.
| while (isRunning()) { | ||
| try { | ||
| NativeObjectReference reference = (NativeObjectReference) referenceQueue.remove(MAX_REMOVE_TIMEOUT_MS); | ||
| synchronized (this) { | ||
| remove(reference); | ||
| reference.close(); | ||
| } | ||
| } catch (Exception e) { | ||
| } | ||
| } |
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.
So basically we're adding a thread that will wake up every 50 ms, right?
I think this is a very bad idea from performance point of view. Especially on battery-powered devices.
Do you think we really need to stop this thread at all? To me, the main advantage of auto-close feature is reducing leaks in long-running applications. Closing objects before shutdown is probably not that important. Or maybe there are cases when the VM is shut down, but the process is not terminated and C heap lives longer than JVM?
If in your opinion stopping the thread is important / useful / nice, I don't mind, but in this case can we use blocking remove() (without timeout) + Thread.interrupt(), to avoid frequent wake-ups?
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've supposed that all roc objects must be freed explicitly before shutdown; according to what you said that's not the case, so we can just set AutoCloseThread as daemon thread, use blocking remove() and get rid of shutdown cleanup.
When the JVM is stopped all process memory (including C heap) is freed and reclaimed by the OS.
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 can imagine two drawbacks of NOT closing Roc objects at JVM shutdown:
-
You have created JVM from C program, run it for a while, then terminated JVM and continued running C program. You'll get a leak in C heap. Even if this is possible, this looks like a very rare case.
-
When Roc will implement RTCP, RTSP, and other control protocols, proper closing may perform correct termination, e.g. send BYE or TERADOWN, while just terminating JVM will only close the socket. However the other side anyway should be ready to such situation becasue remote peer can just crash without proper closing.
So it seems that proper closing theoretically may be better in some situations, but is likely not critical. Given that the user can always use AutoCloseable or just call close manually, I think that's not a big deal.
Thus, up to me, if there is an easy way to implement this without hurting performance and complicating code, we can do it. Otherwise, let's use daemon 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.
used daemon thread in #40
| * @throws IllegalArgumentException if the arguments are invalid. | ||
| * @throws Exception if an error occured when creating the receiver. | ||
| */ | ||
| private static long validate(Context context, ReceiverConfig config) throws IllegalArgumentException, Exception { |
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.
Maybe better name these methods validateAndOpen() or tryOpen() or something like this? It was surprising to me that validates calls open.
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.
Sure I'll rename these to tryOpen.
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.
renamed in #40
gavv
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.
I've finished the review:
| thread.start(); | ||
|
|
||
| /* add a ShutdownHook for closing all NativeObjects that are still open */ | ||
| Runtime.getRuntime().addShutdownHook(new Thread(() -> { | ||
| thread.closeAll(); | ||
| try { | ||
| thread.join(MAX_JOIN_TIMEOUT_MS); | ||
| } catch (InterruptedException e) {} | ||
| })); |
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.
Maybe better hide this thread creation/termination in AutoCloseThread.getInstance()? I think this code belongs to AutoCloseThread.
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.
Sure!
Anyway according to official Java documentation all registered shutdown hooks will run all concurrently and in unspecified order so there could be a problem if for example a user adds its own shutdown hook which makes use of roc objects inside it.
In my mind there could be three solutions for that:
- Add a note on the README documenting explicitely to not use any roc object inside a user defined shutdown hook
- Remove the addShutDownHook mechanism and expose a public method for closing all objects (I like this solution less than all the others because we are potentially giving this responsability to a lazy user which will not use any closing mechanism during program execution and closing objects only at the very end)
- Remove the addShutDownHook mechanism at all
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.
Interesting! Hm, given that this PR is quite big already, I would suggest the third approach - remove the addShutDownHook mechanism at all. At least for now.
If the user calls close(), there is no leaks. If the user forgets to call close(), there is no strict guarantee. Bindings will collect dangling objects in some typical use cases, but the correct way to use bindings is still to call close().
I think this is a good approach to start with.
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.
removed shutdownHook in #40
| public void close() throws Exception { | ||
| if (isOpen()) { | ||
| this.isOpen.set(false); | ||
| destructor.close(ptr); | ||
| this.dependsOn.set(null); | ||
| } | ||
| } |
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.
We need thread-safe / re-entrant close(), right?
This implementation seems to be not re-entrant. Concurrent close() calls may call destructor twice. To make it re-entrant, we can use atomic CAS operation instead of isOpen() + osOpen.set(false), or use mutex.
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.
Absolutely true! I'll change it with a CAS operation.
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.
fixed in #40
| /** | ||
| * Underlying roc object native pointer. | ||
| */ | ||
| private final long ptr; | ||
|
|
||
| /** | ||
| * Destructor method. | ||
| */ | ||
| private final Destructor destructor; |
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.
Duplicating fields in NativeObject and NativeObjectReference looks a bit confusing to me.
Currently the flow is the following:
- user creates object derived from NativeObject
- NativeObject initializes its fields (ptr and destructor)
- NativeObject calls AutoCloseThread
- AutoCloseThread creates NativeObjectReference
- NativeObjectReference requests fields from NativeObject and copies them
- AutoCloseThread returns NativeObjectReference
- NativeObject saves NativeObjectReference
- NativeObject calls NativeObjectReference.close(), which uses those saved fields
It was quite hard to me to investigate this sequence.
What do you think about the following change?
- ptr and destructor fields will present only in NativeObjectReference
- NativeObject constructor will create NativeObjectReference (by itself, without help of AutoCloseThread) and pass ptr and destructor to its constructor
- then NativeObject constructor will register created NativeObjectReference to AutoCloseThread
- NativeObject.getPtr() will call NativeObjectReference.getPtr()
I think this way the relationship between NativeObject and NativeObjectReference will be more clear. NativeObjectReference will hold the raw pointer, its destructor, and will implement closing. NativeObject will be a stateless wrapper on top of it, which just contains a reference to NativeObjectReference and registers and de-registers it in AutoCloseThread.
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.
Also, maybe rename NativeObject to something else, to avoid confusion with NativeObjectReference? NativeObjectReference looks like it's a reference to NativeObject, but reality is actually the opposite. Maybe rename NativeObject to something like BridgedObject?
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.
Actually the NativeObjectReference needs a ReferenceQueue to be passed to its constructor and it's responsibility of the AutoCloseThread to manage the ReferenceQueue; so in my opinion it's better to keep AutoCloseThread to create a NativeObjectReference for better loose coupling.
Surely we can remove the duplicate ptr and destructor fields from NativeObject and let only NativeObjectReference to store them.
The new flow will be:
NativeObjectconstructor callsAutoCloseThread.addpassingptranddestructorto itAutoCloseThreadcreates theNativeObjectReference(which stores theptrand thedestructor) and returns itNativeObjectsavesNativeObjectReferenceNativeObject.getPtr()will callNativeObjectReference.getPtr()
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.
Also, maybe rename NativeObject to something else, to avoid confusion with NativeObjectReference? NativeObjectReference looks like it's a reference to NativeObject, but reality is actually the opposite. Maybe rename NativeObject to something like BridgedObject?
Sure I'll rename it to BridgedObject
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.
Sounds good!
| /** | ||
| * <code>NativeObjectReference</code> is associated with a {@link NativeObject} and owns its entire lifetime; | ||
| * | ||
| * A <code>NativeObjectReference</code> contains necessary data for closing the native object | ||
| * after it becomes phantom reachable. | ||
| */ | ||
| class NativeObjectReference extends PhantomReference<NativeObject> implements AutoCloseable { |
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.
Could you clarify why we need AutoCloseable here?
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.
Here it is just to explicitly expose a close method by that interface.
| @Override | ||
| public T next() { | ||
| if (withDependencies.hasNext()) return withDependencies.next(); | ||
| if (withoutDependencies.hasNext()) return withoutDependencies.next(); | ||
| return null; | ||
| } |
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.
We currently have only two layer of dependencies (receiver/sender on first layer, context on second), but I see that ReferenceCollector is designed as if it supported arbitrary number of layers.
However, will it work correctly if there are more than two layers?
If A depends on B, and B depends on C, then A and B will be both in withDependencies list. Is it guaranteed that A is always before B in this list?
If yes, then could you please add brief comment?
If no, then I'd suggest one of the following:
- make the implementation correct even on multi-layer dependencies;
- OR: make the limitations of the implementation somehow explicit from the code; e.g. maybe hold to maps (with and without dependencies) and require that the dependency itself can be only in without-dependencies map.
Or, if we decide to remove close-on-shutdown feature, the iterator can be probably removed at all.
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.
With this implementation the iterator doesn't order the withDependencies objects, so in case of multiple levels of depencies like your example it's not guaranteed that A is always before B.
I can do a more general solution simply by storing the references in a directed graph and then the iterator ordering will be a result of a topological sort on the graph.
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.
added to sender/receiver link to context. So context wouldn't be collected by CG before sender/reciever. With such approach, we don't need a directed graph. Fixed in #40
|
@MatteoArella Hi, do you have any plans on this? |
|
Hi @MatteoArella ! |
|
We've merged #40! |
|
@ortex @MatteoArella thank you both |
This PR would close #5.
The finalization is achieved with
PhantomReferencesupport sincejava.lang.ref.Cleaneris introduced only with Java 9.Now each
NativeObjectis associated with aNativeObjectReferencewhich contains all necessary data for closing the native object after it becomes phantom reachable.For letting
Contextobjects to not being garbage collected beforeSenderorReceiverare closed first, a reference to the context object is added to sender and receiverNativeObjectReferenceobjects.