🌐 AI搜索 & 代理 主页
Skip to content

Conversation

@MatteoArella
Copy link
Member

This PR would close #5.

The finalization is achieved with PhantomReference support since java.lang.ref.Cleaner is introduced only with Java 9.

Now each NativeObject is associated with a NativeObjectReference which contains all necessary data for closing the native object after it becomes phantom reachable.

For letting Context objects to not being garbage collected before Sender or Receiver are closed first, a reference to the context object is added to sender and receiver NativeObjectReference objects.

@MatteoArella MatteoArella requested review from gavv and ortex September 27, 2020 16:19
@gavv
Copy link
Member

gavv commented Sep 28, 2020

Great! I'll take a look in upcoming days.

@ortex
Copy link
Member

ortex commented Oct 10, 2020

LGTM!

Copy link
Member

@gavv gavv left a 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.

Comment on lines +111 to +120
while (isRunning()) {
try {
NativeObjectReference reference = (NativeObjectReference) referenceQueue.remove(MAX_REMOVE_TIMEOUT_MS);
synchronized (this) {
remove(reference);
reference.close();
}
} catch (Exception e) {
}
}
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

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 {
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

Choose a reason for hiding this comment

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

renamed in #40

Copy link
Member

@gavv gavv left a 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:

Comment on lines +37 to +45
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) {}
}));
Copy link
Member

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.

Copy link
Member Author

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:

  1. Add a note on the README documenting explicitely to not use any roc object inside a user defined shutdown hook
  2. 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)
  3. Remove the addShutDownHook mechanism at all

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

removed shutdownHook in #40

Comment on lines +96 to +102
public void close() throws Exception {
if (isOpen()) {
this.isOpen.set(false);
destructor.close(ptr);
this.dependsOn.set(null);
}
}
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

Choose a reason for hiding this comment

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

fixed in #40

Comment on lines +20 to +28
/**
* Underlying roc object native pointer.
*/
private final long ptr;

/**
* Destructor method.
*/
private final Destructor destructor;
Copy link
Member

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.

Copy link
Member

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?

Copy link
Member Author

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:

  • NativeObject constructor calls AutoCloseThread.add passing ptr and destructor to it
  • AutoCloseThread creates the NativeObjectReference (which stores the ptr and the destructor) and returns it
  • NativeObject saves NativeObjectReference
  • NativeObject.getPtr() will call NativeObjectReference.getPtr()

Copy link
Member Author

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

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good!

Comment on lines +9 to +15
/**
* <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 {
Copy link
Member

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?

Copy link
Member Author

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.

Comment on lines +87 to +92
@Override
public T next() {
if (withDependencies.hasNext()) return withDependencies.next();
if (withoutDependencies.hasNext()) return withoutDependencies.next();
return null;
}
Copy link
Member

@gavv gavv Oct 16, 2020

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.

Copy link
Member Author

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.

Copy link
Member

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

@gavv
Copy link
Member

gavv commented Nov 27, 2022

@MatteoArella Hi, do you have any plans on this?

@ortex
Copy link
Member

ortex commented Dec 2, 2022

Hi @MatteoArella !
I continue your work in #40. Feel free to code review 😄

@ortex ortex mentioned this pull request Dec 3, 2022
@gavv
Copy link
Member

gavv commented Dec 5, 2022

We've merged #40!

@gavv gavv closed this Dec 5, 2022
@gavv
Copy link
Member

gavv commented Dec 5, 2022

@ortex @MatteoArella thank you both

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Think about some finalizer-like feature

3 participants