owned this note changed a year ago
Published Linked with GitHub

Async functions

Author: Wes Billman Status: draft / in-review / accepted / implemented / rejected

Motivation

The ability to pass function references through the RPC subsystem. Very useful for callbacks.

Goals

  • Support function references
  • Alllow those references to be optional
  • Keep a consistant Verb interface

Design

The initial approach will be to add a new method to the ftl SDK. This new method will enable Async calls, which will have a Promise style interface.

TODO: is this signature correct? Do we want the req as well?

func AsyncCall[Req, Resp any](
    ctx context.Context,
    verb Verb[Req, Resp],
    req Req,
    callback Verb[AsyncResult[Req, Resp], ftl.Void]) error {
    
    // ... call controller
    return nil
}
message AsyncCallRequest {
  Metadata metadata = 1;

  schema.VerbRef verb = 2;
  bytes body = 3;
  schema.VerbRef callback = 4;
}

message AsyncCallResponse {
    string id = 1;
    optional Error error = 2;
}

service VerbService {
    // ...
    
    rpc AsyncCall(AsyncCallRequest) AsyncCallResponse
}
//ftl:verb
func CreateAvatars(ctx context.Context, req CreateAvatarsRequest) (CreateAvatarsResponse, error) {
    // ...
}

Schema syntax???

verb onAvatarsCreated(result CreateAvatarsResponse)

An example usage of this might be

type SignupRequest struct {
	Email string
}

type SignupResponse struct {
	Message string
}

//ftl:verb
func Signup(ctx context.Context, req SignupRequest) (SignupResponse, error) {
	err := ftl.AsyncCall(ctx, images.CreateAvatars, images.CreateAvatarsRequest{}, OnAvatarsCreated)
	if err != nil {
		return SignupResponse{}, err
	}
	return SignupResponse{Message: fmt.Sprintf("Hello, %s!", req.Name)}, nil
}

//ftl:verb
func OnAvatarsCreated(
    ctx context.Context,
    // Do we want request as well here?
    req AsyncResult[images.CreateAvatarsRequest, images.CreateAvatarsResponse],
) error {
	// Do something with the avatars. Maybe save their paths to the database?
}

Roadmap

Rough roadmap of tasks and time estimates for how long the implementation will take.

Alternatives considered

Callback functions in data types

module user {
    data EnrolledUserRequest {}
    data EnrolledUserResponse {}

    data EnrollUserRequest {
        onComplete verb(EnrolledUserRequest) EnrolledUserResponse
    }
    
    verb enroll(EnrollUserRequest) EnrollUserResponse
}

module signup {
    verb signup(SignupRequest) SignupResponse
    verb onUserEnrolled(user.EnrolledUserRequest) user.EnrolledUserResponse
}
//ftl:verb
func Signup(ctx context.Context, req SignupRequest) (SignupResponse, error) {
    var callback = OnUserEnrolled

    err := ftl.AsyncCall(ctx, req.Enroll, user.EnrollUserRequest{...}, OnUserEnrolled)

    resp, err := ftl.Call(ctx, user.Enroll, user.EnrollUserRequest{Name: "foo", OnComplete: callback})
}

//ftl:verb
func OnUserEnrolled(ctx context.Context, req user.EnrolledUserRequest) (user.EnrolledUserResponse, error) {
    // ...
}
module user {
    data SignupRequest {
        Email String
    }
    data SignupResponse {}
    
    verb signUp()
}
err := ftl.AsyncCall(ctx, req.Enroll, user.EnrollUserRequest{}, OnUserEnrolled)
{"name": "Alice", "onComplete": "user.enroll"}
var VerbMapping = map[string]any {
    "user.enroll": Enroll,
}
module user {
    data EnrolledUserRequest {}
    data EnrolledUserResponse {}

    data EnrollUserRequest {
        name String
        onComplete verb(EnrolledUserRequest) EnrolledUserResponse
        
        // optional signature
        onComplete (verb(EnrolledUserRequest) EnrolledUserResponse)?
    }
    
    verb enroll(EnrollUserRequest) EnrollUserResponse
}

module signup {
    verb signup(SignupRequest) SignupResponse
    verb onUserEnrolled(user.EnrolledUserRequest) user.EnrolledUserResponse
}
type EnrollUserRequest struct {
    Name string
    
    OnComplete func(user.EnrolledUserRequest) (user.EnrolledUserResponse, error)
}


func Enroll(ctx context.Context, req EnrollUserRequest) (EnrollUserResponse, error) {
    // Kick off some async thing and report back later
    if req.onComplete != nil {
        // How do we call req.OnComplete here?
        ftl.Call(ctx, req.OnComplete, user.EnrolledUserRequest{})
    }
}
Select a repo