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: add retry metadata to fsm decl #1554

Merged
merged 7 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
841 changes: 427 additions & 414 deletions backend/protos/xyz/block/ftl/v1/schema/schema.pb.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/protos/xyz/block/ftl/v1/schema/schema.proto
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ message FSM {
string name = 3;
repeated Ref start = 4;
repeated FSMTransition transitions = 5;
repeated Metadata metadata = 6;
}

message FSMTransition {
Expand Down
14 changes: 13 additions & 1 deletion backend/schema/fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type FSM struct {
Name string `parser:"'fsm' @Ident '{'" protobuf:"3"`
Start []*Ref `parser:"('start' @@)*" protobuf:"4"` // Start states.
Transitions []*FSMTransition `parser:"('transition' @@)* '}'" protobuf:"5"`
Metadata []Metadata `parser:"@@*" protobuf:"6"`
}

func FSMFromProto(pb *schemapb.FSM) *FSM {
Expand All @@ -26,6 +27,7 @@ func FSMFromProto(pb *schemapb.FSM) *FSM {
Name: pb.Name,
Start: slices.Map(pb.Start, RefFromProto),
Transitions: slices.Map(pb.Transitions, FSMTransitionFromProto),
Metadata: metadataListToSchema(pb.Metadata),
}
}

Expand Down Expand Up @@ -62,7 +64,13 @@ func (f *FSM) schemaSymbol() {}

func (f *FSM) String() string {
w := &strings.Builder{}
fmt.Fprintf(w, "fsm %s {\n", f.Name)
if len(f.Metadata) == 0 {
fmt.Fprintf(w, "fsm %s {\n", f.Name)
} else {
fmt.Fprintf(w, "fsm %s", f.Name)
fmt.Fprint(w, indent(encodeMetadata(f.Metadata)))
fmt.Fprintf(w, "\n{\n")
}
for _, s := range f.Start {
fmt.Fprintf(w, " start %s\n", s)
}
Expand All @@ -83,6 +91,7 @@ func (f *FSM) ToProto() protoreflect.ProtoMessage {
Transitions: slices.Map(f.Transitions, func(t *FSMTransition) *schemapb.FSMTransition {
return t.ToProto().(*schemapb.FSMTransition) //nolint: forcetypeassert
}),
Metadata: metadataListToProto(f.Metadata),
}
}

Expand All @@ -94,6 +103,9 @@ func (f *FSM) schemaChildren() []Node {
for _, t := range f.Transitions {
out = append(out, t)
}
for _, m := range f.Metadata {
out = append(out, m)
}
return out
}

Expand Down
13 changes: 13 additions & 0 deletions backend/schema/protobuf_dec.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ func metadataToSchema(s *schemapb.Metadata) Metadata {
Alias: s.Alias.Alias,
}

case *schemapb.Metadata_Retry:
var count *int
if s.Retry.Count != nil {
countValue := int(*s.Retry.Count)
count = &countValue
}
return &MetadataRetry{
Pos: posFromProto(s.Retry.Pos),
Count: count,
MinBackoff: s.Retry.MinBackoff,
MaxBackoff: s.Retry.MaxBackoff,
}

default:
panic(fmt.Sprintf("unhandled metadata type: %T", s))
}
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/protos/xyz/block/ftl/v1/schema/schema_pb.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 75 additions & 34 deletions go-runtime/compile/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,23 +143,17 @@ func ExtractModuleSchema(dir string, sch *schema.Schema) (optional.Option[ParseR
return optional.None[ParseResult](), err
}
for _, file := range pkg.Syntax {
err := goast.Visit(file, func(node ast.Node, next func() error) (err error) {
err := goast.Visit(file, func(stack []ast.Node, next func() error) (err error) {
node := stack[len(stack)-1]
switch node := node.(type) {
case *ast.CallExpr:
visitCallExpr(pctx, node)
visitCallExpr(pctx, node, stack)

case *ast.File:
visitFile(pctx, node)

case *ast.FuncDecl:
verb := visitFuncDecl(pctx, node)
pctx.activeVerb = verb
err = next()
if err != nil {
return err
}
pctx.activeVerb = nil
return nil
visitFuncDecl(pctx, node)

case *ast.GenDecl:
visitGenDecl(pctx, node)
Expand Down Expand Up @@ -200,8 +194,8 @@ func ExtractModuleSchema(dir string, sch *schema.Schema) (optional.Option[ParseR
// - This get's filled in with the next pass
func extractTypeDecls(pctx *parseContext) error {
for _, file := range pctx.pkg.Syntax {
err := goast.Visit(file, func(aNode ast.Node, next func() error) (err error) {
node, ok := aNode.(*ast.GenDecl)
err := goast.Visit(file, func(stack []ast.Node, next func() error) (err error) {
node, ok := (stack[len(stack)-1]).(*ast.GenDecl)
if !ok || node.Tok != token.TYPE {
return next()
}
Expand Down Expand Up @@ -243,7 +237,7 @@ func extractTypeDeclsForNode(pctx *parseContext, node *ast.GenDecl) {
case *types.Basic:
enum := &schema.Enum{
Pos: goPosToSchemaPos(node.Pos()),
Comments: visitComments(node.Doc),
Comments: parseComments(node.Doc),
Name: strcase.ToUpperCamel(t.Name.Name),
Type: nil, //TODO: explain
Export: dir.IsExported(),
Expand All @@ -270,14 +264,14 @@ func extractTypeDeclsForNode(pctx *parseContext, node *ast.GenDecl) {

enum := &schema.Enum{
Pos: goPosToSchemaPos(node.Pos()),
Comments: visitComments(node.Doc),
Comments: parseComments(node.Doc),
Name: strcase.ToUpperCamel(t.Name.Name),
Export: dir.IsExported(),
}
if typ, ok := typ.(*types.Interface); ok {
if iTyp, ok := typ.(*types.Interface); ok {
pctx.nativeNames[enum] = nativeName
pctx.module.Decls = append(pctx.module.Decls, enum)
pctx.enumInterfaces[t.Name.Name] = typ
pctx.enumInterfaces[t.Name.Name] = iTyp
} else {
pctx.errors.add(errorf(node, "expected interface for type enum but got %q", typ))
}
Expand All @@ -286,7 +280,7 @@ func extractTypeDeclsForNode(pctx *parseContext, node *ast.GenDecl) {
case *directiveTypeAlias:
alias := &schema.TypeAlias{
Pos: goPosToSchemaPos(node.Pos()),
Comments: visitComments(node.Doc),
Comments: parseComments(node.Doc),
Name: strcase.ToUpperCamel(t.Name.Name),
Export: dir.IsExported(),
Type: nil, //TODO: explain
Expand All @@ -306,29 +300,48 @@ func extractTypeDeclsForNode(pctx *parseContext, node *ast.GenDecl) {
}
}

func visitCallExpr(pctx *parseContext, node *ast.CallExpr) {
func visitCallExpr(pctx *parseContext, node *ast.CallExpr, stack []ast.Node) {
_, fn := deref[*types.Func](pctx.pkg, node.Fun)
if fn == nil {
return
}
switch fn.FullName() {
case ftlCallFuncPath:
parseCall(pctx, node)
parseCall(pctx, node, stack)

case ftlConfigFuncPath, ftlSecretFuncPath:
// Secret/config declaration: ftl.Config[<type>](<name>)
parseConfigDecl(pctx, node, fn)

case ftlFSMFuncPath:
parseFSMDecl(pctx, node)
parseFSMDecl(pctx, node, stack)

case ftlPostgresDBFuncPath:
parseDatabaseDecl(pctx, node, schema.PostgresDatabaseType)
}
}

func parseCall(pctx *parseContext, node *ast.CallExpr) {
if pctx.activeVerb == nil {
func parseCall(pctx *parseContext, node *ast.CallExpr, stack []ast.Node) {
var activeFuncDecl *ast.FuncDecl
for i := len(stack) - 1; i >= 0; i-- {
if found, ok := stack[i].(*ast.FuncDecl); ok {
activeFuncDecl = found
break
}
// use element
}
if activeFuncDecl == nil {
return
}
expectedVerbName := strcase.ToLowerCamel(activeFuncDecl.Name.Name)
var activeVerb *schema.Verb
for _, decl := range pctx.module.Decls {
if aVerb, ok := decl.(*schema.Verb); ok && aVerb.Name == expectedVerbName {
activeVerb = aVerb
break
}
}
if activeVerb == nil {
return
}
if len(node.Args) != 3 {
Expand All @@ -348,7 +361,7 @@ func parseCall(pctx *parseContext, node *ast.CallExpr) {
pctx.errors.add(errorf(node.Args[1], "call first argument must be a function in an ftl module%s", suffix))
return
}
pctx.activeVerb.AddCall(ref)
activeVerb.AddCall(ref)
}

func parseSelectorRef(node ast.Expr) *schema.Ref {
Expand Down Expand Up @@ -384,7 +397,7 @@ func parseVerbRef(pctx *parseContext, node ast.Expr) *schema.Ref {
}
}

func parseFSMDecl(pctx *parseContext, node *ast.CallExpr) {
func parseFSMDecl(pctx *parseContext, node *ast.CallExpr, stack []ast.Node) {
var literal *ast.BasicLit
if len(node.Args) > 0 {
literal, _ = node.Args[0].(*ast.BasicLit)
Expand All @@ -404,8 +417,9 @@ func parseFSMDecl(pctx *parseContext, node *ast.CallExpr) {
}

fsm := &schema.FSM{
Pos: goPosToSchemaPos(node.Pos()),
Name: name,
Pos: goPosToSchemaPos(node.Pos()),
Name: name,
Metadata: []schema.Metadata{},
}
pctx.module.Decls = append(pctx.module.Decls, fsm)

Expand All @@ -422,6 +436,34 @@ func parseFSMDecl(pctx *parseContext, node *ast.CallExpr) {
}
parseFSMTransition(pctx, call, fn, fsm)
}

// find variable declaration that we are currently in so we can look for attached directives
var variableDecl *ast.GenDecl
for i := len(stack) - 1; i >= 0; i-- {
if decl, ok := stack[i].(*ast.GenDecl); ok && decl.Tok == token.VAR {
variableDecl = decl
break
}
}
if variableDecl == nil || variableDecl.Doc == nil {
return
}
directives, schemaErr := parseDirectives(node, fset, variableDecl.Doc)
if schemaErr != nil {
pctx.errors.add(schemaErr)
}
for _, dir := range directives {
if retryDir, ok := dir.(*directiveRetry); ok {
fsm.Metadata = append(fsm.Metadata, &schema.MetadataRetry{
Pos: retryDir.Pos,
Count: retryDir.Count,
MinBackoff: retryDir.MinBackoff,
MaxBackoff: retryDir.MaxBackoff,
})
} else {
pctx.errors.add(errorf(node, "unexpected directive attached for FSM: %T", dir))
}
}
}

// Parse a Start or Transition call in an FSM declaration and add it to the FSM.
Expand Down Expand Up @@ -565,7 +607,7 @@ func visitFile(pctx *parseContext, node *ast.File) {
if node.Doc == nil {
return
}
pctx.module.Comments = visitComments(node.Doc)
pctx.module.Comments = parseComments(node.Doc)
}

func isType[T types.Type](t types.Type) bool {
Expand Down Expand Up @@ -753,7 +795,7 @@ func maybeVisitTypeEnumVariant(pctx *parseContext, node *ast.GenDecl, directives

enumVariant := &schema.EnumVariant{
Pos: goPosToSchemaPos(node.Pos()),
Comments: visitComments(node.Doc),
Comments: parseComments(node.Doc),
Name: strcase.ToUpperCamel(t.Name.Name),
}

Expand Down Expand Up @@ -918,7 +960,7 @@ func visitValueSpec(pctx *parseContext, node *ast.ValueSpec) {
if value, ok := visitConst(pctx, c).Get(); ok {
variant := &schema.EnumVariant{
Pos: goPosToSchemaPos(c.Pos()),
Comments: visitComments(node.Doc),
Comments: parseComments(node.Doc),
Name: strcase.ToUpperCamel(c.Id()),
Value: value,
}
Expand Down Expand Up @@ -1056,7 +1098,7 @@ func visitFuncDecl(pctx *parseContext, node *ast.FuncDecl) (verb *schema.Verb) {
}
verb = &schema.Verb{
Pos: goPosToSchemaPos(node.Pos()),
Comments: visitComments(node.Doc),
Comments: parseComments(node.Doc),
Export: isExported,
Name: strcase.ToLowerCamel(node.Name.Name),
Request: reqV,
Expand All @@ -1068,7 +1110,7 @@ func visitFuncDecl(pctx *parseContext, node *ast.FuncDecl) (verb *schema.Verb) {
return verb
}

func visitComments(doc *ast.CommentGroup) []string {
func parseComments(doc *ast.CommentGroup) []string {
comments := []string{}
if doc := doc.Text(); doc != "" {
comments = strings.Split(strings.TrimSpace(doc), "\n")
Expand Down Expand Up @@ -1160,11 +1202,11 @@ func visitStruct(pctx *parseContext, pos token.Pos, tnode types.Type, isExported
switch path := path[i].(type) {
case *ast.TypeSpec:
if path.Doc != nil {
out.Comments = visitComments(path.Doc)
out.Comments = parseComments(path.Doc)
}
case *ast.GenDecl:
if path.Doc != nil {
out.Comments = visitComments(path.Doc)
out.Comments = parseComments(path.Doc)
}
}
}
Expand Down Expand Up @@ -1463,7 +1505,6 @@ type parseContext struct {
module *schema.Module
nativeNames NativeNames
enumInterfaces enumInterfaces
activeVerb *schema.Verb
errors errorSet
schema *schema.Schema
}
Expand Down
4 changes: 3 additions & 1 deletion go-runtime/compile/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,9 @@ func TestExtractModuleSchemaFSM(t *testing.T) {
assert.Equal(t, r.MustGet().Errors, nil, "expected no schema errors")
actual := schema.Normalise(r.MustGet().Module)
expected := `module fsm {
fsm payment {
fsm payment
+retry 10 5s 10m
{
start fsm.created
start fsm.paid
transition fsm.created to fsm.paid
Expand Down
2 changes: 2 additions & 0 deletions go-runtime/compile/testdata/fsm/fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
)

// The payment FSM.
//
//ftl:retry 10 5s 10m
var paymentFSM = ftl.FSM("payment",
ftl.Start(Created),
ftl.Start(Paid),
Expand Down
Loading
Loading