Skip to content

Commit

Permalink
chore: initial Java runtime support
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartwdouglas committed Aug 12, 2024
1 parent 412bb63 commit dff1bd2
Show file tree
Hide file tree
Showing 83 changed files with 3,757 additions and 345 deletions.
6 changes: 5 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ clean:
rm -rf frontend/node_modules
find . -name '*.zip' -exec rm {} \;
mvn -f kotlin-runtime/ftl-runtime clean
mvn -f java-runtime/ftl-runtime clean

# Live rebuild the ftl binary whenever source changes.
live-rebuild:
Expand All @@ -41,7 +42,7 @@ dev *args:
watchexec -r {{WATCHEXEC_ARGS}} -- "just build-sqlc && ftl dev {{args}}"

# Build everything
build-all: build-protos-unconditionally build-frontend build-generate build-sqlc build-zips lsp-generate
build-all: build-protos-unconditionally build-frontend build-generate build-sqlc build-zips lsp-generate build-java
@just build ftl ftl-controller ftl-runner ftl-initdb

# Run "go generate" on all packages
Expand All @@ -64,6 +65,9 @@ build +tools: build-protos build-zips build-frontend
build-backend:
just build ftl ftl-controller ftl-runner

build-java:
mvn -f java-runtime/ftl-runtime install

export DATABASE_URL := "postgres://postgres:secret@localhost:15432/ftl?sslmode=disable"

# Explicitly initialise the database
Expand Down
2 changes: 1 addition & 1 deletion backend/controller/scaling/localscaling/local_scaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func (l *LocalScaling) SetReplicas(ctx context.Context, replicas int, idleRunner
simpleName := fmt.Sprintf("runner%d", keySuffix)
if err := kong.ApplyDefaults(&config, kong.Vars{
"deploymentdir": filepath.Join(l.cacheDir, "ftl-runner", simpleName, "deployments"),
"language": "go,kotlin,rust",
"language": "go,kotlin,rust,java",
}); err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion backend/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type Config struct {
TemplateDir string `help:"Template directory to copy into each deployment, if any." type:"existingdir"`
DeploymentDir string `help:"Directory to store deployments in." default:"${deploymentdir}"`
DeploymentKeepHistory int `help:"Number of deployments to keep history for." default:"3"`
Language []string `short:"l" help:"Languages the runner supports." env:"FTL_LANGUAGE" default:"go,kotlin,rust"`
Language []string `short:"l" help:"Languages the runner supports." env:"FTL_LANGUAGE" default:"go,kotlin,rust,java"`
HeartbeatPeriod time.Duration `help:"Minimum period between heartbeats." default:"3s"`
HeartbeatJitter time.Duration `help:"Jitter to add to heartbeat period." default:"2s"`
RunnerStartDelay time.Duration `help:"Time in seconds for a runner to wait before contacting the controller. This can be needed in istio environments to work around initialization races." env:"FTL_RUNNER_START_DELAY" default:"0s"`
Expand Down
2 changes: 1 addition & 1 deletion backend/schema/metadatatypemap.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
type MetadataTypeMap struct {
Pos Position `parser:"" protobuf:"1,optional"`

Runtime string `parser:"'+' 'typemap' @('go' | 'kotlin')" protobuf:"2"`
Runtime string `parser:"'+' 'typemap' @('go' | 'kotlin' | 'java')" protobuf:"2"`
NativeName string `parser:"@String" protobuf:"3"`
}

Expand Down
2 changes: 2 additions & 0 deletions buildengine/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ func buildModule(ctx context.Context, projectRootDir string, sch *schema.Schema,
switch module.Config.Language {
case "go":
err = buildGoModule(ctx, projectRootDir, sch, module, filesTransaction)
case "java":
err = buildJavaModule(ctx, module)
case "kotlin":
err = buildKotlinModule(ctx, sch, module)
case "rust":
Expand Down
23 changes: 23 additions & 0 deletions buildengine/build_java.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package buildengine

import (
"context"
"fmt"

"github.com/TBD54566975/ftl/internal/exec"
"github.com/TBD54566975/ftl/internal/log"
)

func buildJavaModule(ctx context.Context, module Module) error {
logger := log.FromContext(ctx)
if err := SetPOMProperties(ctx, module.Config.Dir); err != nil {
return fmt.Errorf("unable to update ftl.version in %s: %w", module.Config.Dir, err)
}
logger.Infof("Using build command '%s'", module.Config.Build)
err := exec.Command(ctx, log.Debug, module.Config.Dir, "bash", "-c", module.Config.Build).RunBuffered(ctx)
if err != nil {
return fmt.Errorf("failed to build module %q: %w", module.Config.Module, err)
}

return nil
}
4 changes: 1 addition & 3 deletions buildengine/build_kotlin.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,14 @@ func buildKotlinModule(ctx context.Context, sch *schema.Schema, module Module) e
if err := SetPOMProperties(ctx, module.Config.Dir); err != nil {
return fmt.Errorf("unable to update ftl.version in %s: %w", module.Config.Dir, err)
}

if err := generateExternalModules(ctx, module, sch); err != nil {
return fmt.Errorf("unable to generate external modules for %s: %w", module.Config.Module, err)
}

if err := prepareFTLRoot(module); err != nil {
return fmt.Errorf("unable to prepare FTL root for %s: %w", module.Config.Module, err)
}

logger.Debugf("Using build command '%s'", module.Config.Build)
logger.Infof("Using build command '%s'", module.Config.Build)
err := exec.Command(ctx, log.Debug, module.Config.Dir, "bash", "-c", module.Config.Build).RunBuffered(ctx)
if err != nil {
return fmt.Errorf("failed to build module %q: %w", module.Config.Module, err)
Expand Down
52 changes: 52 additions & 0 deletions buildengine/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ func extractDependencies(module Module) ([]string, error) {
case "kotlin":
return extractKotlinFTLImports(module.Config.Module, module.Config.Dir)

case "java":
return extractJavaFTLImports(module.Config.Module, module.Config.Dir)

case "rust":
return extractRustFTLImports(module.Config.Module, module.Config.Dir)

Expand Down Expand Up @@ -140,6 +143,55 @@ func extractKotlinFTLImports(self, dir string) ([]string, error) {
return modules, nil
}

func extractJavaFTLImports(self, dir string) ([]string, error) {
dependencies := map[string]bool{}
// We also attempt to look at kotlin files
// As the Java module supports both
kotin, kotlinErr := extractKotlinFTLImports(self, dir)
if kotlinErr == nil {
// We don't really care about the error case, its probably a Java project
for _, imp := range kotin {
dependencies[imp] = true
}
}
javaImportRegex := regexp.MustCompile(`^import ftl\.([A-Za-z0-9_.]+)`)

err := filepath.WalkDir(filepath.Join(dir, "src/main/java"), func(path string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("failed to walk directory: %w", err)
}
if d.IsDir() || !(strings.HasSuffix(path, ".java")) {
return nil
}
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
matches := javaImportRegex.FindStringSubmatch(scanner.Text())
if len(matches) > 1 {
module := strings.Split(matches[1], ".")[0]
if module == self {
continue
}
dependencies[module] = true
}
}
return scanner.Err()
})

// We only error out if they both failed
if err != nil && kotlinErr != nil {
return nil, fmt.Errorf("%s: failed to extract dependencies from Java module: %w", self, err)
}
modules := maps.Keys(dependencies)
sort.Strings(modules)
return modules, nil
}

func extractRustFTLImports(self, dir string) ([]string, error) {
fmt.Fprintf(os.Stderr, "RUST TODO extractRustFTLImports\n")

Expand Down
4 changes: 2 additions & 2 deletions buildengine/discover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestDiscoverModules(t *testing.T) {
Language: "kotlin",
Realm: "home",
Module: "echo",
Build: "mvn -B compile",
Build: "mvn -B package",
Deploy: []string{
"main",
"classes",
Expand Down Expand Up @@ -116,7 +116,7 @@ func TestDiscoverModules(t *testing.T) {
Language: "kotlin",
Realm: "home",
Module: "externalkotlin",
Build: "mvn -B compile",
Build: "mvn -B package",
Deploy: []string{
"main",
"classes",
Expand Down
41 changes: 40 additions & 1 deletion buildengine/stubs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package buildengine
import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/TBD54566975/ftl/backend/schema"
"github.com/TBD54566975/ftl/common/moduleconfig"
Expand All @@ -13,7 +15,11 @@ import (
//
// Currently, only Go stubs are supported. Kotlin and other language stubs can be added in the future.
func GenerateStubs(ctx context.Context, projectRoot string, modules []*schema.Module, moduleConfigs []moduleconfig.ModuleConfig) error {
return generateGoStubs(ctx, projectRoot, modules, moduleConfigs)
err := generateGoStubs(ctx, projectRoot, modules, moduleConfigs)
if err != nil {
return err
}
return writeGenericSchemaFiles(ctx, projectRoot, modules, moduleConfigs)
}

// CleanStubs removes all generated stubs.
Expand All @@ -37,6 +43,39 @@ func generateGoStubs(ctx context.Context, projectRoot string, modules []*schema.
return nil
}

func writeGenericSchemaFiles(ctx context.Context, projectRoot string, modules []*schema.Module, moduleConfigs []moduleconfig.ModuleConfig) error {
sch := &schema.Schema{Modules: modules}
for _, module := range moduleConfigs {
if module.GeneratedSchemaDir == "" {
continue
}

modPath := module.Abs().GeneratedSchemaDir
err := os.MkdirAll(modPath, 0750)
if err != nil {
return fmt.Errorf("failed to create directory %s: %w", modPath, err)
}

for _, mod := range sch.Modules {
if mod.Name == module.Module {
continue
}
data, err := schema.ModuleToBytes(mod)
if err != nil {
return fmt.Errorf("failed to export module schema for module %s %w", mod.Name, err)
}
err = os.WriteFile(filepath.Join(modPath, mod.Name+".pb"), data, 0600)
if err != nil {
return fmt.Errorf("failed to write schema file for module %s %w", mod.Name, err)
}
}
}
err := compile.GenerateStubsForModules(ctx, projectRoot, moduleConfigs, sch)
if err != nil {
return fmt.Errorf("failed to generate go stubs: %w", err)
}
return nil
}
func cleanGoStubs(ctx context.Context, projectRoot string) error {
err := compile.CleanStubs(ctx, projectRoot)
if err != nil {
Expand Down
31 changes: 29 additions & 2 deletions common/moduleconfig/moduleconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ type ModuleGoConfig struct{}
// ModuleKotlinConfig is language-specific configuration for Kotlin modules.
type ModuleKotlinConfig struct{}

// ModuleJavaConfig is language-specific configuration for Java modules.
type ModuleJavaConfig struct{}

// ModuleConfig is the configuration for an FTL module.
//
// Module config files are currently TOML.
Expand All @@ -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"`
// Schema is the name of the schema file relative to the DeployDir.
Schema string `toml:"schema"`
// Errors is the name of the error file relative to the DeployDir.
Expand All @@ -46,6 +51,7 @@ type ModuleConfig struct {

Go ModuleGoConfig `toml:"go,optional"`
Kotlin ModuleKotlinConfig `toml:"kotlin,optional"`
Java ModuleJavaConfig `toml:"java,optional"`
}

// AbsModuleConfig is a ModuleConfig with all paths made absolute.
Expand Down Expand Up @@ -84,6 +90,12 @@ func (c ModuleConfig) Abs() AbsModuleConfig {
if !strings.HasPrefix(clone.DeployDir, clone.Dir) {
panic(fmt.Sprintf("deploy-dir %q is not beneath module directory %q", clone.DeployDir, clone.Dir))
}
if clone.GeneratedSchemaDir != "" {
clone.GeneratedSchemaDir = filepath.Clean(filepath.Join(clone.Dir, clone.GeneratedSchemaDir))
if !strings.HasPrefix(clone.GeneratedSchemaDir, clone.Dir) {
panic(fmt.Sprintf("generated-schema-dir %q is not beneath module directory %q", clone.GeneratedSchemaDir, clone.Dir))
}
}
clone.Schema = filepath.Clean(filepath.Join(clone.DeployDir, clone.Schema))
if !strings.HasPrefix(clone.Schema, clone.DeployDir) {
panic(fmt.Sprintf("schema %q is not beneath deploy directory %q", clone.Schema, clone.DeployDir))
Expand Down Expand Up @@ -119,7 +131,7 @@ func setConfigDefaults(moduleDir string, config *ModuleConfig) error {
switch config.Language {
case "kotlin":
if config.Build == "" {
config.Build = "mvn -B compile"
config.Build = "mvn -B package"
}
if config.DeployDir == "" {
config.DeployDir = "target"
Expand All @@ -130,7 +142,22 @@ func setConfigDefaults(moduleDir string, config *ModuleConfig) error {
if len(config.Watch) == 0 {
config.Watch = []string{"pom.xml", "src/**", "target/generated-sources"}
}

case "java":
if config.Build == "" {
config.Build = "mvn -B package"
}
if config.DeployDir == "" {
config.DeployDir = "target"
}
if config.GeneratedSchemaDir == "" {
config.GeneratedSchemaDir = "src/main/ftl-module-schema"
}
if len(config.Deploy) == 0 {
config.Deploy = []string{"main", "quarkus-app"}
}
if len(config.Watch) == 0 {
config.Watch = []string{"pom.xml", "src/**", "target/generated-sources"}
}
case "go":
if config.DeployDir == "" {
config.DeployDir = ".ftl"
Expand Down
2 changes: 1 addition & 1 deletion deployment/base/ftl-runner/ftl-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ spec:
- name: FTL_RUNNER_ADVERTISE
value: "http://$(MY_POD_IP):8893"
- name: FTL_LANGUAGE
value: "go,kotlin"
value: "go,kotlin,java"
ports:
- containerPort: 8893
readinessProbe:
Expand Down
2 changes: 1 addition & 1 deletion examples/go/echo/echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type EchoResponse struct {

// Echo returns a greeting with the current time.
//
//ftl:verb
//ftl:verb export
func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) {
tresp, err := ftl.Call(ctx, time.Time, time.TimeRequest{})
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion examples/kotlin/echo/ftl.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
module = "echo"
language = "kotlin"
language = "java"
Loading

0 comments on commit dff1bd2

Please sign in to comment.