Skip to content

Commit

Permalink
feat: add replay command (#2744)
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartwdouglas authored Sep 20, 2024
1 parent 410b111 commit 31aa71d
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 6 deletions.
17 changes: 11 additions & 6 deletions frontend/cli/cmd_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,23 @@ func (c *callCmd) Run(ctx context.Context, client ftlv1connect.VerbServiceClient

logger.Debugf("Calling %s", c.Verb)

return callVerb(ctx, client, ctlCli, c.Verb, requestJSON)
}

func callVerb(ctx context.Context, client ftlv1connect.VerbServiceClient, ctlCli ftlv1connect.ControllerServiceClient, verb reflection.Ref, requestJSON []byte) error {
logger := log.FromContext(ctx)
// otherwise, we have a match so call the verb
resp, err := client.Call(ctx, connect.NewRequest(&ftlv1.CallRequest{
Verb: c.Verb.ToProto(),
Verb: verb.ToProto(),
Body: requestJSON,
}))

if cerr := new(connect.Error); errors.As(err, &cerr) && cerr.Code() == connect.CodeNotFound {
suggestions, err := c.findSuggestions(ctx, ctlCli)
suggestions, err := findSuggestions(ctx, ctlCli, verb)

// if we have suggestions, return a helpful error message. otherwise continue to the original error
if err == nil {
return fmt.Errorf("verb not found: %s\n\nDid you mean one of these?\n%s", c.Verb, strings.Join(suggestions, "\n"))
return fmt.Errorf("verb not found: %s\n\nDid you mean one of these?\n%s", verb, strings.Join(suggestions, "\n"))
}
}
if err != nil {
Expand All @@ -80,7 +85,7 @@ func (c *callCmd) Run(ctx context.Context, client ftlv1connect.VerbServiceClient
// findSuggestions looks up the schema and finds verbs that are similar to the one that was not found
// it uses the levenshtein distance to determine similarity - if the distance is less than 40% of the length of the verb,
// it returns an error if no closely matching suggestions are found
func (c *callCmd) findSuggestions(ctx context.Context, client ftlv1connect.ControllerServiceClient) ([]string, error) {
func findSuggestions(ctx context.Context, client ftlv1connect.ControllerServiceClient, verb reflection.Ref) ([]string, error) {
logger := log.FromContext(ctx)

// lookup the verbs
Expand All @@ -104,7 +109,7 @@ func (c *callCmd) findSuggestions(ctx context.Context, client ftlv1connect.Contr
for _, module := range modules {
for _, v := range module.Verbs() {
verbName := fmt.Sprintf("%s.%s", module.Name, v.Name)
if verbName == fmt.Sprintf("%s.%s", c.Verb.Module, c.Verb.Name) {
if verbName == fmt.Sprintf("%s.%s", verb.Module, verb.Name) {
break
}

Expand All @@ -115,7 +120,7 @@ func (c *callCmd) findSuggestions(ctx context.Context, client ftlv1connect.Contr
suggestions := []string{}

logger.Debugf("Found %d verbs", len(verbs))
needle := fmt.Sprintf("%s.%s", c.Verb.Module, c.Verb.Name)
needle := fmt.Sprintf("%s.%s", verb.Module, verb.Name)

// only consider suggesting verbs that are within 40% of the length of the needle
distanceThreshold := int(float64(len(needle))*0.4) + 1
Expand Down
102 changes: 102 additions & 0 deletions frontend/cli/cmd_replay.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"context"
"fmt"
"strings"
"time"

"connectrpc.com/connect"
"github.com/jpillora/backoff"

ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1"
pbconsole "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/console"
"github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/console/pbconsoleconnect"
"github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect"
"github.com/TBD54566975/ftl/backend/schema"
"github.com/TBD54566975/ftl/go-runtime/ftl/reflection"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/rpc"
)

type replayCmd struct {
Wait time.Duration `short:"w" help:"Wait up to this elapsed time for the FTL cluster to become available." default:"1m"`
Verb reflection.Ref `arg:"" required:"" help:"Full path of Verb to call."`
}

func (c *replayCmd) Run(ctx context.Context, client ftlv1connect.VerbServiceClient, ctlCli ftlv1connect.ControllerServiceClient) error {
ctx, cancel := context.WithTimeout(ctx, c.Wait)
defer cancel()
if err := rpc.Wait(ctx, backoff.Backoff{Max: time.Second * 2}, client); err != nil {
return fmt.Errorf("failed to wait for client: %w", err)
}

consoleServiceClient := rpc.Dial(pbconsoleconnect.NewConsoleServiceClient, cli.Endpoint.String(), log.Error)
if err := rpc.Wait(ctx, backoff.Backoff{Max: time.Second * 2}, consoleServiceClient); err != nil {
return fmt.Errorf("failed to wait for console service client: %w", err)
}

logger := log.FromContext(ctx)

// First check the verb is valid
// lookup the verbs
res, err := ctlCli.GetSchema(ctx, connect.NewRequest(&ftlv1.GetSchemaRequest{}))
if err != nil {
return fmt.Errorf("failed to get schema: %w", err)
}

found := false
for _, pbmodule := range res.Msg.GetSchema().GetModules() {
module, err := schema.ModuleFromProto(pbmodule)
if err != nil {
logger.Errorf(err, "failed to convert module from protobuf")
continue
}
if module.Name == c.Verb.Module {
for _, v := range module.Verbs() {
if v.Name == c.Verb.Name {
found = true
break
}
}
}
}
if !found {
suggestions, err := findSuggestions(ctx, ctlCli, c.Verb)
// if we have suggestions, return a helpful error message. otherwise continue to the original error
if err == nil {
return fmt.Errorf("verb not found: %s\n\nDid you mean one of these?\n%s", c.Verb, strings.Join(suggestions, "\n"))
}
return fmt.Errorf("verb not found: %s", c.Verb)
}

events, err := consoleServiceClient.GetEvents(ctx, connect.NewRequest(&pbconsole.EventsQuery{
Order: pbconsole.EventsQuery_DESC,
Limit: 1,
Filters: []*pbconsole.EventsQuery_Filter{
{
Filter: &pbconsole.EventsQuery_Filter_Call{
Call: &pbconsole.EventsQuery_CallFilter{
DestModule: c.Verb.Module,
DestVerb: &c.Verb.Name,
},
},
},
{
Filter: &pbconsole.EventsQuery_Filter_EventTypes{
EventTypes: &pbconsole.EventsQuery_EventTypeFilter{EventTypes: []pbconsole.EventType{pbconsole.EventType_EVENT_TYPE_CALL}},
},
},
},
}))
if err != nil {
return fmt.Errorf("failed to get events: %w", err)
}
if len(events.Msg.GetEvents()) == 0 {
return fmt.Errorf("no events found for %v", c.Verb)
}
requestJSON := events.Msg.GetEvents()[0].GetCall().Request

logger.Infof("Calling %s with body:\n%s", c.Verb, requestJSON)
return callVerb(ctx, client, ctlCli, c.Verb, []byte(requestJSON))
}
1 change: 1 addition & 0 deletions frontend/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type CLI struct {
PS psCmd `cmd:"" help:"List deployments."`
Serve serveCmd `cmd:"" help:"Start the FTL server."`
Call callCmd `cmd:"" help:"Call an FTL function."`
Replay replayCmd `cmd:"" help:"Call an FTL function with the same request body as the last invocation."`
Update updateCmd `cmd:"" help:"Update a deployment."`
Kill killCmd `cmd:"" help:"Kill a deployment."`
Schema schemaCmd `cmd:"" help:"FTL schema commands."`
Expand Down

0 comments on commit 31aa71d

Please sign in to comment.