Skip to content
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

refactor(java):separate resposibilities on java engine so static methods can be exposed and do memory management that should work on Java 18 and above #198

Merged
merged 3 commits into from
Jan 14, 2025

Conversation

sighphyre
Copy link
Member

@sighphyre sighphyre commented Jan 13, 2025

Sorry, this one does two things because they're really difficult to separate:

Most of this is so we can do cool things like this.

Refactor Java Engine

This splits up the responsibilities more cleanly (according to one ginger's opinions) of the Java engine. The three components do the following:

NativeLoader

Previously YggdrasilFFI, the NativeLoader now only holds the responsibility for loading the Yggdrasil binary into memory. This is a purely static class since there's only ever one binary and that needs to be loaded before anything else can be done. Importantly, no pointer management is done at this level. NativeLoader consumes the new CamelToSnakeMapper to provide nice camelCase names to the Java code instead of using a decorator to deal with the snake_case names.

UnleashFFI

This is the JNA interface to the binary. This exposes a Java interface to consume the Rust code. This does also provide a getInstance which returns the only valid implementation to access the binary. Consumers are free to mock this interface for testing.

UnleashEngine

The code that binds the domain logic in Java to the domain logic in Rust. This does do pointer management. Specifically, one UnleashEngine in Java land holds one pointer to one Yggdrasil struct in Rust land. This component therefore also takes over the tests the YggdrasilFFI was previously doing to ensure the engine was properly cleaned up

Dynamically Mount Cleaner Instead of Finalizer

Sooo... small problem. Finalizers are deprecated in Java 17 and marked for removal. Later JVMs provide the option to turn them off entirely, which means our existing implementation just quietly leaks. The more delightful problem is that the alternative to finalizers, the Cleaner API, is only Java 9 and above. And we support Java 8.

So we inject some code to check the JVM version. If you're Java 8 or below your finalizer executes as normal. If you're Java 9 or above, we use reflection to dynamically mount a cleaner and invoke that. Why reflection? Because we support source level compatibility for Java 8, which can't be compiled with direct references to Cleaner objects. Similar to the approach Spring uses

Class<?> cleanerClass = Class.forName("java.lang.ref.Cleaner");
Method registerMethod = cleanerClass.getMethod("register", Object.class, Runnable.class);

// Avoid capturing the engine itself in the lambda, otherwise this prevents GC!
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 didn't spend 3 hours searching for this bug. I'm not salty at all

takeFeaturesFromResource(customEngine, "custom-strategy-tests.json");
Boolean result = customEngine.isEnabled(featureName, context);
assertNotNull(result);
assertEquals(expectedIsEnabled, result);
}

@Test
void testResourceCleanup() throws InterruptedException {
Copy link
Member Author

Choose a reason for hiding this comment

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

Borrowed but modified from the existing implementation to use a PhantomReference rather than grabbing big chunks of GC'd references and hoping Yggdrasil is one of them (nothing wrong with the other approach, this just feels a bit more direct)

Copy link
Member

@chriswk chriswk left a comment

Choose a reason for hiding this comment

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

Looks sane to me

import com.sun.jna.FunctionMapper;
import com.sun.jna.NativeLibrary;

class CamelToSnakeMapper implements FunctionMapper {
Copy link
Member

Choose a reason for hiding this comment

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

TIL about FunctionMapper

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Member Author

Choose a reason for hiding this comment

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

That was me yesterday haha

Copy link
Contributor

@gastonfournier gastonfournier left a comment

Choose a reason for hiding this comment

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

Nice and clean, one ask for a small unit test, but looks good to me so far!

Comment on lines +10 to +22
public String getFunctionName(NativeLibrary library, Method method) {
String methodName = method.getName();
StringBuilder snakeCaseName = new StringBuilder();

for (char c : methodName.toCharArray()) {
if (Character.isUpperCase(c)) {
snakeCaseName.append('_').append(Character.toLowerCase(c));
} else {
snakeCaseName.append(c);
}
}

return snakeCaseName.toString();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is a good candidate for a unit test... but looks sane to me

Copy link
Member Author

@sighphyre sighphyre Jan 14, 2025

Choose a reason for hiding this comment

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

Excellent suggestion, will do before merge

EDIT: Changed my mind. Gonna do this in another PR but before release

* feat(java): expose core version in java engine layer

* fix(java): force native encoding to utf-8 so windows doesn't set it to something horrible (#200)
@sighphyre sighphyre merged commit 2cfac49 into feat/java-list-known-features Jan 14, 2025
1 check passed
@sighphyre sighphyre deleted the refactor/java-engine branch January 14, 2025 09:27
sighphyre added a commit that referenced this pull request Jan 14, 2025
…#197)

* feat(java): expose list_known_features

* refactor(java):separate resposibilities on java engine so static methods can be exposed and do memory management that should work on Java 18 and above (#198)

* refactor(java): rework java engine so that static methods can be exposed cleanly

* feat(java): expose core version in java engine layer (#199)

* fix(java): force native encoding to utf-8 so windows doesn't set it to something horrible (#200)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

3 participants