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

feat: Initial Java runtime implementation #2318

Merged
merged 4 commits into from
Aug 13, 2024

Conversation

stuartwdouglas
Copy link
Collaborator

This is still very much a work in progress, however it contains a lot of basic functionality.

So far this includes support for:

  • Verb invocations
  • HTTP ingress
  • Cron
  • Topics and Subscriptions
  • Basic testing of Verbs

The existing Kotlin example has been migrated over to the new approach. At the moment the module is called Java even though it supports both, in future this will provide a base layer of functionality with some small language dependent features in separate Java/Kotlin modules.

@github-actions github-actions bot changed the title Initial Java runtime implementation feat: Initial Java runtime implementation Aug 12, 2024
@ftl-robot ftl-robot mentioned this pull request Aug 12, 2024
@stuartwdouglas stuartwdouglas force-pushed the stuartwdouglas/experimental-quarkus-runtime branch 3 times, most recently from eebabf2 to dff1bd2 Compare August 12, 2024 02:24
@stuartwdouglas stuartwdouglas marked this pull request as ready for review August 12, 2024 02:29
@stuartwdouglas stuartwdouglas requested review from a team and gak and removed request for a team August 12, 2024 02:29
@@ -0,0 +1,6 @@
# Ignore Gradle project-specific cache directory
.gradle
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nooooooooooooooooooooooooooo

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the auto generated quarkus .gitignore, but it is coming at some point.

Copy link
Collaborator

@alecthomas alecthomas left a comment

Choose a reason for hiding this comment

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

LGTM!

I'd let @worstell have a pass over the Java code.

)

// Files is the FTL Java runtime scaffolding files.
func Files() *zip.Reader { return internal.ZipRelativeToCaller("scaffolding") }
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this directory exists does it? Can we get rid of Files() altogether?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I will remove it, I can re-add when it I add scaffolding.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Just noting for future reference that this is a temporary situation until we figure out a better approach (eg. package servers).

@alecthomas alecthomas requested a review from worstell August 12, 2024 03:22
@alecthomas
Copy link
Collaborator

Overall LGTM!

I couldn't quite tell how topics (and other resources) in other modules are codegenned? Could be something for later, or I missed it.

Once it's a bit more stable it might be an idea to add integration tests.

@alecthomas
Copy link
Collaborator

I'd also be interested to see how we're going to write more complex inter-verb/module tests with the injection approach. I think we will at some point need an equivalent to the ModuleContext in Go.

@stuartwdouglas
Copy link
Collaborator Author

Overall LGTM!

I couldn't quite tell how topics (and other resources) in other modules are codegenned? Could be something for later, or I missed it.

Topics are not quite there yet (well subscriptions really, the Topic). I have a plan for how to handle them, I should get to it soon.

Once it's a bit more stable it might be an idea to add integration tests.

Absolutely, I especially want to test interop with a go based module to make sure that everything is serializing / generating correctly. There is still heaps of work to do on tests.

@stuartwdouglas stuartwdouglas force-pushed the stuartwdouglas/experimental-quarkus-runtime branch from dff1bd2 to f5e3538 Compare August 12, 2024 03:29
fun echo(context: Context, req: EchoRequest): EchoResponse {
val response = context.call(::time, Empty())
@Verb
fun echo(req: EchoRequest, time: TimeClient): EchoResponse {
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like ctx is removed and replaced with DI?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, you directly reference the resources you need. This allows us to explicitly list this in the schema, as there is no way to get these resources without referencing them in the signature.

}
var publish = cc.getMethodCreator("call", void.class);
var helper = publish.invokeStaticMethod(MethodDescriptor.ofMethod(VerbClientHelper.class, "instance", VerbClientHelper.class));
publish.invokeVirtualMethod(MethodDescriptor.ofMethod(VerbClientHelper.class, "call", Object.class, String.class, String.class, Object.class, Class.class, boolean.class, boolean.class), helper, publish.load(name), publish.load(module), publish.loadNull(), publish.loadClass(Void.class), publish.load(false), publish.load(false));
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a long line! Would it be useful to split it up a bit?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I need to setup auto formatting.

Comment on lines 67 to 106
if (i.kind() == Type.Kind.PARAMETERIZED_TYPE) {
var returnType = i.asParameterizedType().arguments().get(1);
var paramType = i.asParameterizedType().arguments().get(0);
try (ClassCreator cc = new ClassCreator(classOutput, iface.name().toString() + "_fit_verbclient", null, Object.class.getName(), iface.name().toString())) {
if (launchModeBuildItem.isTest()) {
cc.addAnnotation(TEST_ANNOTATION);
cc.addAnnotation(Singleton.class);
}
var publish = cc.getMethodCreator("call", returnType.name().toString(), paramType.name().toString());
var helper = publish.invokeStaticMethod(MethodDescriptor.ofMethod(VerbClientHelper.class, "instance", VerbClientHelper.class));
var results = publish.invokeVirtualMethod(MethodDescriptor.ofMethod(VerbClientHelper.class, "call", Object.class, String.class, String.class, Object.class, Class.class, boolean.class, boolean.class), helper, publish.load(name), publish.load(module), publish.getMethodParam(0), publish.loadClass(returnType.name().toString()), publish.load(false), publish.load(false));
publish.returnValue(results);
publish = cc.getMethodCreator("call", Object.class, Object.class);
helper = publish.invokeStaticMethod(MethodDescriptor.ofMethod(VerbClientHelper.class, "instance", VerbClientHelper.class));
results = publish.invokeVirtualMethod(MethodDescriptor.ofMethod(VerbClientHelper.class, "call", Object.class, String.class, String.class, Object.class, Class.class, boolean.class, boolean.class), helper, publish.load(name), publish.load(module), publish.getMethodParam(0), publish.loadClass(returnType.name().toString()), publish.load(false), publish.load(false));
publish.returnValue(results);
clients.put(iface.name(), new VerbClientBuildItem.DiscoveredClients(name, module,cc.getClassName()));
}
found = true;
break;
} else {
throw new RuntimeException("@VerbClientDefinition can only be applied to interfaces that directly extend a verb client type with concrete type parameters and " + iface.name() + " does not have concrete type parameters");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could all these if blocks be refactored out into a factory of sorts? There's a lot of very similar calls and variables. Maybe some DRY if it's appropriate?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Even though it all looks similar each line is different enough that it can't really be turned generic. Basically this is due to the fact that there is 4 different client signatures, and each once needs a slightly different implementation.

public Map<String, String> start() {
server = new FTLTestServer();
server.start();
return Map.of("ftl.endpoint", "http://localhost:" + server.getPort());
Copy link
Contributor

Choose a reason for hiding this comment

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

This is stupid but maybe use 127.0.0.1 instead because of the mac firewall? #2298

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah if this is a client never mind.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oops

@stuartwdouglas stuartwdouglas force-pushed the stuartwdouglas/experimental-quarkus-runtime branch 2 times, most recently from 4ead6b4 to 13c8ca7 Compare August 12, 2024 04:56

output = outputTargetBuildItem.getOutputDirectory().resolve("main");
try (var out = Files.newOutputStream(output)) {
// out.write("""
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: should this be deleted?

@@ -37,6 +40,8 @@ type ModuleConfig struct {
Deploy []string `toml:"deploy"`
// DeployDir is the directory to deploy from, relative to the module directory.
DeployDir string `toml:"deploy-dir"`
// GeneratedSchemaDir is the directory to generate protobuf schema files into. These can be picked up by language specific build tools
GeneratedSchemaDir string `toml:"generated-schema-dir"`
Copy link
Contributor

Choose a reason for hiding this comment

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

what is the function of GeneratedSchemaDir? is it needed rather than using the existing Schema field in this config?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Basically I dump the protos for the external modules I need to generate code for into this dir, and then run the code generation in Maven. It means you can check them into source control and have a build that can run without needing an FTL instance with those modules installed present.

Copy link
Contributor

Choose a reason for hiding this comment

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

could we use the global build directory at <projectRoot>/.ftl for this? in Go we put all the external module stubs in .ftl/go

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I could try that later, but this approach follows the standard maven code generation convention. It also allows for different modules in a multi module project to only generate clients for a subset of the modules on the server (this is not implemented yet). I would like to keep this approach for now to see how it works, and possibly re-visit it later.


@SuppressWarnings("unused")
@Singleton
public class FTLHttpHandler implements VerbInvoker {
Copy link
Contributor

Choose a reason for hiding this comment

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

it looks like http ingress verbs will be registered to use this handler on invocation, is that right?

why does java/kotlin need its own ingress logic? shouldn't we be able to invoke these verbs as normal on the runtime side, since the FTL controller will be responsible for serving the HTTP endpoint and routing the inbound ingress requests appropriately?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is purely about the Java API that is used (i.e. allowing you to use JAX-RS instead of a custom HTTP annotations). From the invocations side everything works as normal.

Copy link
Contributor

Choose a reason for hiding this comment

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

ahh got it

@stuartwdouglas stuartwdouglas force-pushed the stuartwdouglas/experimental-quarkus-runtime branch from d25dde4 to 4b84c0f Compare August 12, 2024 20:56
@worstell worstell self-requested a review August 12, 2024 21:10
@stuartwdouglas stuartwdouglas added this pull request to the merge queue Aug 12, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Aug 12, 2024
@stuartwdouglas stuartwdouglas force-pushed the stuartwdouglas/experimental-quarkus-runtime branch 2 times, most recently from 120f16d to ebdebe8 Compare August 12, 2024 23:00
@stuartwdouglas stuartwdouglas added this pull request to the merge queue Aug 12, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Aug 12, 2024
@stuartwdouglas stuartwdouglas force-pushed the stuartwdouglas/experimental-quarkus-runtime branch from ebdebe8 to 5ce8f26 Compare August 13, 2024 01:29
@stuartwdouglas stuartwdouglas added this pull request to the merge queue Aug 13, 2024
Merged via the queue into main with commit 7b6a421 Aug 13, 2024
17 checks passed
@stuartwdouglas stuartwdouglas deleted the stuartwdouglas/experimental-quarkus-runtime branch August 13, 2024 01:40
gak pushed a commit that referenced this pull request Aug 13, 2024
This is still very much a work in progress, however it contains a lot of
basic functionality.

So far this includes support for:

- Verb invocations
- HTTP ingress
- Cron
- Topics and Subscriptions
- Basic testing of Verbs

The existing Kotlin example has been migrated over to the new approach.
At the moment the module is called Java even though it supports both, in
future this will provide a base layer of functionality with some small
language dependent features in separate Java/Kotlin modules.
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.

4 participants