Skip to content

Commit

Permalink
New workflow api implementation (#312)
Browse files Browse the repository at this point in the history
* Split RequestOptions in CallRequestOptions, to split headers from idempotency key.
* Add InvocationHandle to attach/get output of invocation
* Fix CallRequestOptions#withHeader
* Fix response of attach/getOutput
* Fix code generation to use built-in workflow support. Remove old workflow api.
* Removed the BindableService, and renamed the BindableServiceFactory to ServiceDefinitionFactory. That abstraction was needed to support a "bundle" of many service definitions because of the workflow api.
* Removed the manual service builders, we don't need those anymore.
* Implement promise api
  • Loading branch information
slinkydeveloper committed May 21, 2024
1 parent d08025a commit 1a5817c
Show file tree
Hide file tree
Showing 85 changed files with 2,122 additions and 2,433 deletions.
1 change: 0 additions & 1 deletion examples/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ dependencies {
implementation(project(":sdk-http-vertx"))
implementation(project(":sdk-api-kotlin"))
implementation(project(":sdk-serde-jackson"))
implementation(project(":sdk-workflow-api"))

implementation(platform(jacksonLibs.jackson.bom))
implementation(jacksonLibs.jackson.jsr310)
Expand Down
40 changes: 18 additions & 22 deletions examples/src/main/java/my/restate/sdk/examples/LoanWorkflow.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,18 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import dev.restate.sdk.Context;
import dev.restate.sdk.SharedWorkflowContext;
import dev.restate.sdk.WorkflowContext;
import dev.restate.sdk.annotation.Handler;
import dev.restate.sdk.annotation.Service;
import dev.restate.sdk.annotation.Shared;
import dev.restate.sdk.annotation.Workflow;
import dev.restate.sdk.common.CoreSerdes;
import dev.restate.sdk.common.DurablePromiseKey;
import dev.restate.sdk.common.StateKey;
import dev.restate.sdk.common.TerminalException;
import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder;
import dev.restate.sdk.serde.jackson.JacksonSerdes;
import dev.restate.sdk.workflow.DurablePromiseKey;
import dev.restate.sdk.workflow.WorkflowContext;
import dev.restate.sdk.workflow.WorkflowExecutionState;
import dev.restate.sdk.workflow.WorkflowSharedContext;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
Expand All @@ -36,7 +35,8 @@ public class LoanWorkflow {

// --- Data types used by the Loan Worfklow

enum Status {
public enum Status {
UNKNOWN,
SUBMITTED,
WAITING_HUMAN_APPROVAL,
APPROVED,
Expand Down Expand Up @@ -103,7 +103,7 @@ public void run(WorkflowContext ctx, LoanRequest loanRequest) {
LOG.info("Loan request submitted");

// 2. Ask human approval
ctx.run(() -> askHumanApproval(ctx.workflowKey()));
ctx.run(() -> askHumanApproval(ctx.key()));
ctx.set(STATUS, Status.WAITING_HUMAN_APPROVAL);

// 3. Wait human approval
Expand Down Expand Up @@ -142,15 +142,20 @@ public void run(WorkflowContext ctx, LoanRequest loanRequest) {
// --- Methods to approve/reject loan

@Shared
public void approveLoan(WorkflowSharedContext ctx) {
public void approveLoan(SharedWorkflowContext ctx) {
ctx.durablePromiseHandle(HUMAN_APPROVAL).resolve(true);
}

@Shared
public void rejectLoan(WorkflowSharedContext ctx) {
public void rejectLoan(SharedWorkflowContext ctx) {
ctx.durablePromiseHandle(HUMAN_APPROVAL).resolve(false);
}

@Shared
public Status getStatus(SharedWorkflowContext ctx) {
return ctx.get(STATUS).orElse(Status.UNKNOWN);
}

public static void main(String[] args) {
RestateHttpEndpointBuilder.builder()
.bind(new LoanWorkflow())
Expand All @@ -170,13 +175,10 @@ public static void main(String[] args) {
LoanWorkflowClient.IngressClient client =
LoanWorkflowClient.fromIngress("http://127.0.0.1:8080", "my-loan");

WorkflowExecutionState state =
var state =
client.submit(
new LoanRequest(
"Francesco", "slinkydeveloper", "DE1234", new BigDecimal("1000000000")));
if (state != WorkflowExecutionState.STARTED) {
throw new IllegalStateException("Unexpected state " + state);
}

LOG.info("Started loan workflow");

Expand All @@ -192,22 +194,16 @@ public static void main(String[] args) {
// Now approve it
client.approveLoan();

while (!client.isCompleted()) {
LOG.info("Not completed yet");
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// Wait for output
state.attach();

LOG.info("Loan workflow completed");
LOG.info("Loan workflow completed, now in status {}", client.getStatus());
}

// -- Some mocks

private static void askHumanApproval(String workflowKey) throws InterruptedException {
LOG.info("Sending human approval request");
LOG.info("Sending human approval request for workflow {}", workflowKey);
Thread.sleep(1000);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ public HandlebarsTemplateEngine(

Handlebars handlebars = new Handlebars(templateLoader);
handlebars.registerHelpers(StringHelpers.class);
handlebars.<HandlerTemplateModel>registerHelper(
"targetExpr",
(h, options) -> {
switch (h.serviceType) {
case SERVICE:
return String.format(
"Target.service(%s.SERVICE_NAME, \"%s\")", h.definitionsClass, h.name);
case VIRTUAL_OBJECT:
return String.format(
"Target.virtualObject(%s.SERVICE_NAME, %s, \"%s\")",
h.definitionsClass, options.param(0), h.name);
case WORKFLOW:
return String.format(
"Target.workflow(%s.SERVICE_NAME, %s, \"%s\")",
h.definitionsClass, options.param(0), h.name);
}
throw new IllegalStateException();
});

this.templates =
templates.entrySet().stream()
Expand Down Expand Up @@ -90,6 +108,7 @@ static class ServiceTemplateModel {
public final boolean isWorkflow;
public final boolean isObject;
public final boolean isService;
public final boolean isKeyed;
public final List<HandlerTemplateModel> handlers;

private ServiceTemplateModel(
Expand All @@ -104,14 +123,16 @@ private ServiceTemplateModel(
this.isWorkflow = inner.getServiceType() == ServiceType.WORKFLOW;
this.isObject = inner.getServiceType() == ServiceType.VIRTUAL_OBJECT;
this.isService = inner.getServiceType() == ServiceType.SERVICE;
this.isKeyed = this.isObject || this.isWorkflow;

this.handlers =
inner.getMethods().stream()
.map(
h ->
new HandlerTemplateModel(
h,
this.generatedClassSimpleNamePrefix + "Definitions.Serde",
inner.getServiceType(),
this.generatedClassSimpleNamePrefix + "Definitions",
handlerNamesToPrefix))
.collect(Collectors.toList());
}
Expand All @@ -126,6 +147,9 @@ static class HandlerTemplateModel {
public final boolean isStateless;
public final boolean isExclusive;

private final ServiceType serviceType;
private final String definitionsClass;

public final boolean inputEmpty;
public final String inputFqcn;
public final String inputSerdeDecl;
Expand All @@ -142,7 +166,10 @@ static class HandlerTemplateModel {
public final String outputSerdeRef;

private HandlerTemplateModel(
Handler inner, String definitionsClass, Set<String> handlerNamesToPrefix) {
Handler inner,
ServiceType serviceType,
String definitionsClass,
Set<String> handlerNamesToPrefix) {
this.name = inner.getName().toString();
this.methodName = (handlerNamesToPrefix.contains(this.name) ? "_" : "") + this.name;
this.handlerType = inner.getHandlerType().toString();
Expand All @@ -151,20 +178,23 @@ private HandlerTemplateModel(
this.isExclusive = inner.getHandlerType() == HandlerType.EXCLUSIVE;
this.isStateless = inner.getHandlerType() == HandlerType.STATELESS;

this.serviceType = serviceType;
this.definitionsClass = definitionsClass;

this.inputEmpty = inner.getInputType().isEmpty();
this.inputFqcn = inner.getInputType().getName();
this.inputSerdeDecl = inner.getInputType().getSerdeDecl();
this.boxedInputFqcn = inner.getInputType().getBoxed();
this.inputSerdeFieldName = this.name.toUpperCase() + "_INPUT";
this.inputAcceptContentType = inner.getInputAccept();
this.inputSerdeRef = definitionsClass + "." + this.inputSerdeFieldName;
this.inputSerdeRef = definitionsClass + ".Serde." + this.inputSerdeFieldName;

this.outputEmpty = inner.getOutputType().isEmpty();
this.outputFqcn = inner.getOutputType().getName();
this.outputSerdeDecl = inner.getOutputType().getSerdeDecl();
this.boxedOutputFqcn = inner.getOutputType().getBoxed();
this.outputSerdeFieldName = this.name.toUpperCase() + "_OUTPUT";
this.outputSerdeRef = definitionsClass + "." + this.outputSerdeFieldName;
this.outputSerdeRef = definitionsClass + ".Serde." + this.outputSerdeFieldName;
}
}
}
1 change: 0 additions & 1 deletion sdk-api-gen/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ dependencies {
implementation(project(":sdk-api-gen-common"))

implementation(project(":sdk-api"))
implementation(project(":sdk-workflow-api"))

testAnnotationProcessor(project(":sdk-api-gen"))
testImplementation(project(":sdk-core"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
import dev.restate.sdk.Context;
import dev.restate.sdk.ObjectContext;
import dev.restate.sdk.SharedObjectContext;
import dev.restate.sdk.SharedWorkflowContext;
import dev.restate.sdk.WorkflowContext;
import dev.restate.sdk.annotation.*;
import dev.restate.sdk.common.ServiceType;
import dev.restate.sdk.gen.model.*;
import dev.restate.sdk.gen.model.Handler;
import dev.restate.sdk.gen.model.Service;
import dev.restate.sdk.gen.utils.AnnotationUtils;
import dev.restate.sdk.workflow.WorkflowContext;
import dev.restate.sdk.workflow.WorkflowSharedContext;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -213,10 +213,7 @@ private HandlerType defaultHandlerType(ServiceType serviceType, ExecutableElemen
case VIRTUAL_OBJECT:
return HandlerType.EXCLUSIVE;
case WORKFLOW:
messager.printMessage(
Diagnostic.Kind.ERROR,
"Workflow methods MUST be annotated with either @Shared or @Workflow",
element);
return HandlerType.SHARED;
}
throw new IllegalStateException("Unexpected");
}
Expand All @@ -226,7 +223,7 @@ private void validateMethodSignature(
switch (handlerType) {
case SHARED:
if (serviceType == ServiceType.WORKFLOW) {
validateFirstParameterType(WorkflowSharedContext.class, element);
validateFirstParameterType(SharedWorkflowContext.class, element);
} else if (serviceType == ServiceType.VIRTUAL_OBJECT) {
validateFirstParameterType(SharedObjectContext.class, element);
} else {
Expand Down
36 changes: 11 additions & 25 deletions sdk-api-gen/src/main/java/dev/restate/sdk/gen/ServiceProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.gen;

import dev.restate.sdk.common.BindableServiceFactory;
import dev.restate.sdk.common.ServiceType;
import dev.restate.sdk.common.function.ThrowingFunction;
import dev.restate.sdk.common.syscalls.ServiceDefinitionFactory;
import dev.restate.sdk.gen.model.Service;
import dev.restate.sdk.gen.template.HandlebarsTemplateEngine;
import java.io.*;
Expand All @@ -36,8 +36,7 @@
public class ServiceProcessor extends AbstractProcessor {

private HandlebarsTemplateEngine definitionsCodegen;
private HandlebarsTemplateEngine bindableServiceFactoryCodegen;
private HandlebarsTemplateEngine bindableServiceCodegen;
private HandlebarsTemplateEngine serviceDefinitionFactoryCodegen;
private HandlebarsTemplateEngine clientCodegen;

private static final Set<String> RESERVED_METHOD_NAMES = Set.of("send");
Expand All @@ -60,37 +59,25 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
ServiceType.VIRTUAL_OBJECT,
"templates/Definitions.hbs"),
RESERVED_METHOD_NAMES);
this.bindableServiceFactoryCodegen =
this.serviceDefinitionFactoryCodegen =
new HandlebarsTemplateEngine(
"BindableServiceFactory",
"ServiceDefinitionFactory",
filerTemplateLoader,
Map.of(
ServiceType.WORKFLOW,
"templates/BindableServiceFactory.hbs",
"templates/ServiceDefinitionFactory.hbs",
ServiceType.SERVICE,
"templates/BindableServiceFactory.hbs",
"templates/ServiceDefinitionFactory.hbs",
ServiceType.VIRTUAL_OBJECT,
"templates/BindableServiceFactory.hbs"),
RESERVED_METHOD_NAMES);
this.bindableServiceCodegen =
new HandlebarsTemplateEngine(
"BindableService",
filerTemplateLoader,
Map.of(
ServiceType.WORKFLOW,
"templates/workflow/BindableService.hbs",
ServiceType.SERVICE,
"templates/BindableService.hbs",
ServiceType.VIRTUAL_OBJECT,
"templates/BindableService.hbs"),
"templates/ServiceDefinitionFactory.hbs"),
RESERVED_METHOD_NAMES);
this.clientCodegen =
new HandlebarsTemplateEngine(
"Client",
filerTemplateLoader,
Map.of(
ServiceType.WORKFLOW,
"templates/workflow/Client.hbs",
"templates/Client.hbs",
ServiceType.SERVICE,
"templates/Client.hbs",
ServiceType.VIRTUAL_OBJECT,
Expand Down Expand Up @@ -122,8 +109,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
ThrowingFunction<String, Writer> fileCreator =
name -> filer.createSourceFile(name, e.getKey()).openWriter();
this.definitionsCodegen.generate(fileCreator, e.getValue());
this.bindableServiceFactoryCodegen.generate(fileCreator, e.getValue());
this.bindableServiceCodegen.generate(fileCreator, e.getValue());
this.serviceDefinitionFactoryCodegen.generate(fileCreator, e.getValue());
this.clientCodegen.generate(fileCreator, e.getValue());
} catch (Throwable ex) {
throw new RuntimeException(ex);
Expand All @@ -136,7 +122,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
resourceFilePath =
readOrCreateResource(
processingEnv.getFiler(),
"META-INF/services/" + BindableServiceFactory.class.getCanonicalName());
"META-INF/services/" + ServiceDefinitionFactory.class.getCanonicalName());
Files.createDirectories(resourceFilePath.getParent());
} catch (IOException e) {
throw new RuntimeException(e);
Expand All @@ -150,7 +136,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
StandardOpenOption.CREATE,
StandardOpenOption.APPEND)) {
for (Map.Entry<Element, Service> e : parsedServices) {
writer.write(e.getValue().getGeneratedClassFqcnPrefix() + "BindableServiceFactory");
writer.write(e.getValue().getGeneratedClassFqcnPrefix() + "ServiceDefinitionFactory");
writer.write('\n');
}
} catch (IOException e) {
Expand Down
42 changes: 0 additions & 42 deletions sdk-api-gen/src/main/resources/templates/BindableService.hbs

This file was deleted.

Loading

0 comments on commit 1a5817c

Please sign in to comment.