Skip to content

Commit

Permalink
feat: hot reload
Browse files Browse the repository at this point in the history
JVM languages can now used how reload for instant feedback
  • Loading branch information
stuartwdouglas committed Nov 15, 2024
1 parent 876aab0 commit 66a525a
Show file tree
Hide file tree
Showing 28 changed files with 765 additions and 407 deletions.
61 changes: 49 additions & 12 deletions backend/controller/scaling/localscaling/local_scaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,32 @@ type localScaling struct {
portAllocator *bind.BindAllocator
controllerAddresses []*url.URL

prevRunnerSuffix int
ideSupport optional.Option[localdebug.IDEIntegration]
prevRunnerSuffix int
ideSupport optional.Option[localdebug.IDEIntegration]
devModeEndpointsUpdates <-chan scaling.DevModeEndpoints
devModeEndpoints map[string]*devModeRunner
}

type devModeRunner struct {
uri url.URL
deploymentKey string
}

func (l *localScaling) Start(ctx context.Context, endpoint url.URL, leaser leases.Leaser) error {
go func() {
for {
select {
case <-ctx.Done():
return
case devEndpoints := <-l.devModeEndpointsUpdates:
l.lock.Lock()
l.devModeEndpoints[devEndpoints.Module] = &devModeRunner{
uri: devEndpoints.Endpoint,
}
l.lock.Unlock()
}
}
}()
scaling.BeginGrpcScaling(ctx, endpoint, leaser, l.handleSchemaChange)
return nil
}
Expand Down Expand Up @@ -80,20 +101,22 @@ type runnerInfo struct {
port string
}

func NewLocalScaling(portAllocator *bind.BindAllocator, controllerAddresses []*url.URL, configPath string, enableIDEIntegration bool) (scaling.RunnerScaling, error) {
func NewLocalScaling(portAllocator *bind.BindAllocator, controllerAddresses []*url.URL, configPath string, enableIDEIntegration bool, devModeEndpoints <-chan scaling.DevModeEndpoints) (scaling.RunnerScaling, error) {

cacheDir, err := os.UserCacheDir()
if err != nil {
return nil, err
}
local := localScaling{
lock: sync.Mutex{},
cacheDir: cacheDir,
runners: map[string]map[string]*deploymentInfo{},
portAllocator: portAllocator,
controllerAddresses: controllerAddresses,
prevRunnerSuffix: -1,
debugPorts: map[string]*localdebug.DebugInfo{},
lock: sync.Mutex{},
cacheDir: cacheDir,
runners: map[string]map[string]*deploymentInfo{},
portAllocator: portAllocator,
controllerAddresses: controllerAddresses,
prevRunnerSuffix: -1,
debugPorts: map[string]*localdebug.DebugInfo{},
devModeEndpointsUpdates: devModeEndpoints,
devModeEndpoints: map[string]*devModeRunner{},
}
if enableIDEIntegration && configPath != "" {
local.ideSupport = optional.Ptr(localdebug.NewIDEIntegration(configPath))
Expand Down Expand Up @@ -165,6 +188,17 @@ func (l *localScaling) startRunner(ctx context.Context, deploymentKey string, in
return nil
default:
}

devEndpoint := l.devModeEndpoints[info.module]
devUri := optional.None[url.URL]()

Check warning on line 193 in backend/controller/scaling/localscaling/local_scaling.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: var devUri should be devURI (revive)
if devEndpoint != nil {
devUri = optional.Some(devEndpoint.uri)
if devEndpoint.deploymentKey == deploymentKey {
// Already running, don't start another
return nil
}
devEndpoint.deploymentKey = deploymentKey
}
controllerEndpoint := l.controllerAddresses[len(l.runners)%len(l.controllerAddresses)]
logger := log.FromContext(ctx)

Expand Down Expand Up @@ -197,6 +231,7 @@ func (l *localScaling) startRunner(ctx context.Context, deploymentKey string, in
Key: model.NewLocalRunnerKey(keySuffix),
Deployment: deploymentKey,
DebugPort: debugPort,
DevEndpoint: devUri,
}

simpleName := fmt.Sprintf("runner%d", keySuffix)
Expand All @@ -219,10 +254,12 @@ func (l *localScaling) startRunner(ctx context.Context, deploymentKey string, in
err := runner.Start(runnerCtx, config)
l.lock.Lock()
defer l.lock.Unlock()
if devEndpoint != nil {
devEndpoint.deploymentKey = ""
}
// Don't count context.Canceled as an a restart error
if err != nil && !errors.Is(err, context.Canceled) {
logger.Errorf(err, "Runner failed: %s", err)
} else {
// Don't count context.Canceled as an a restart error
info.exits++
}
if info.exits >= maxExits {
Expand Down
5 changes: 5 additions & 0 deletions backend/controller/scaling/scaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,8 @@ func runGrpcScaling(ctx context.Context, url url.URL, handler func(ctx context.C
rpc.RetryStreamingServerStream(ctx, backoff.Backoff{Max: time.Second}, &ftlv1.PullSchemaRequest{}, client.PullSchema, handler, rpc.AlwaysRetry())
logger.Debugf("Stopped Runner Scaling")
}

type DevModeEndpoints struct {
Module string
Endpoint url.URL
}
415 changes: 233 additions & 182 deletions backend/protos/xyz/block/ftl/v1/language/language.pb.go

Large diffs are not rendered by default.

29 changes: 20 additions & 9 deletions backend/protos/xyz/block/ftl/v1/language/language.proto
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ message ModuleConfig {
string deploy_dir = 4;
// Build is the command to build the module.
optional string build = 5;
// DevModeBuild is the command to build the module in dev mode.
optional string devModeBuild = 6;
// Build lock path to prevent concurrent builds
string build_lock = 6;
string build_lock = 7;
// The directory to generate protobuf schema files into. These can be picked up by language specific build tools
optional string generated_schema_dir = 7;
optional string generated_schema_dir = 8;
// Patterns to watch for file changes
repeated string watch = 8;
repeated string watch = 9;

// LanguageConfig contains any metadata specific to a specific language.
// These are stored in the ftl.toml file under the same key as the language (eg: "go", "java")
google.protobuf.Struct language_config = 9;
google.protobuf.Struct language_config = 10;
}

// ProjectConfig contains the configuration for a project, found in the ftl-project.toml file.
Expand Down Expand Up @@ -94,18 +96,21 @@ message ModuleConfigDefaultsResponse {
// Default build command
optional string build = 2;

// Dev mode build command, if different from the regular build command
optional string devModeBuild = 3;

// Build lock path to prevent concurrent builds
optional string build_lock = 3;
optional string build_lock = 4;

// Default relative path to the directory containing generated schema files
optional string generated_schema_dir = 4;
optional string generated_schema_dir = 5;

// Default patterns to watch for file changes, relative to the module directory
repeated string watch = 5;
repeated string watch = 6;

// Default language specific configuration.
// These defaults are filled in by looking at each root key only. If the key is not present, the default is used.
google.protobuf.Struct language_config = 6;
google.protobuf.Struct language_config = 7;
}

message DependenciesRequest {
Expand Down Expand Up @@ -208,11 +213,17 @@ message BuildSuccess {
schema.Module module = 3;
// Paths for files/directories to be deployed
repeated string deploy = 4;
// Name of the docker image to use for the runner
string docker_image = 5;

// Errors contains any errors that occurred during the build
// No errors can have a level of ERROR, instead a BuildFailure should be sent
// Instead this is useful for INFO and WARN level errors.
ErrorList errors = 5;
ErrorList errors = 6;

// Dev mode endpoint URI. If this is set then rather than trying to deploy the module, FTL will start a runner that
// connects to this endpoint.
optional string dev_endpoint = 7;
}

// BuildFailure should be sent when a build fails.
Expand Down
6 changes: 5 additions & 1 deletion backend/protos/xyz/block/ftl/v1/language/mixins.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"

"github.com/alecthomas/types/optional"
structpb "google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/structpb"

"github.com/TBD54566975/ftl/internal/builderrors"
"github.com/TBD54566975/ftl/internal/moduleconfig"
Expand Down Expand Up @@ -100,6 +100,9 @@ func ModuleConfigToProto(config moduleconfig.AbsModuleConfig) (*ModuleConfig, er
if config.Build != "" {
proto.Build = &config.Build
}
if config.DevModeBuild != "" {
proto.DevModeBuild = &config.DevModeBuild
}
if config.GeneratedSchemaDir != "" {
proto.GeneratedSchemaDir = &config.GeneratedSchemaDir
}
Expand All @@ -122,6 +125,7 @@ func ModuleConfigFromProto(proto *ModuleConfig) moduleconfig.AbsModuleConfig {
Watch: proto.Watch,
Language: proto.Language,
Build: proto.GetBuild(),
DevModeBuild: proto.GetDevModeBuild(),
BuildLock: proto.BuildLock,
GeneratedSchemaDir: proto.GetGeneratedSchemaDir(),
}
Expand Down
Loading

0 comments on commit 66a525a

Please sign in to comment.