Skip to content

Commit 530035e

Browse files
Workflow API (#215)
* Support ThrowableFunction for Awaitable#map * Add workflow-api and sdk-api-gen modules. workflow-api provides api and runtime support, while sdk-api-gen provides the annotation processor * Add the interfaces to wire the http/lambda endpoint builders with the workflow api * Rename package for protoc generator from dev.restate.sdk.gen to dev.restate.sdk.protocgen to avoid conflict with the new code generator * Add loan workflow example
1 parent 64b4d7a commit 530035e

File tree

46 files changed

+3130
-20
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3130
-20
lines changed

examples/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import com.google.protobuf.gradle.id
2+
import com.google.protobuf.gradle.protobuf
23

34
plugins {
45
java
@@ -8,16 +9,20 @@ plugins {
89
}
910

1011
dependencies {
12+
annotationProcessor(project(":sdk-api-gen"))
13+
1114
implementation(project(":sdk-api"))
1215
implementation(project(":sdk-lambda"))
1316
implementation(project(":sdk-http-vertx"))
1417
implementation(project(":sdk-api-kotlin"))
1518
implementation(project(":sdk-serde-jackson"))
19+
implementation(project(":sdk-workflow-api"))
1620

1721
implementation(coreLibs.protobuf.java)
1822
implementation(coreLibs.protobuf.kotlin)
1923
implementation(coreLibs.grpc.stub)
2024
implementation(coreLibs.grpc.protobuf)
25+
implementation(coreLibs.grpc.netty)
2126
implementation(coreLibs.grpc.kotlin.stub) { exclude("javax.annotation", "javax.annotation-api") }
2227

2328
// Replace javax.annotations-api with tomcat annotations
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package my.restate.sdk.examples;
10+
11+
import com.fasterxml.jackson.annotation.JsonCreator;
12+
import com.fasterxml.jackson.annotation.JsonProperty;
13+
import dev.restate.sdk.UnkeyedContext;
14+
import dev.restate.sdk.annotation.Service;
15+
import dev.restate.sdk.annotation.ServiceType;
16+
import dev.restate.sdk.annotation.Shared;
17+
import dev.restate.sdk.annotation.Workflow;
18+
import dev.restate.sdk.common.CoreSerdes;
19+
import dev.restate.sdk.common.StateKey;
20+
import dev.restate.sdk.common.TerminalException;
21+
import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder;
22+
import dev.restate.sdk.serde.jackson.JacksonSerdes;
23+
import dev.restate.sdk.workflow.DurablePromiseKey;
24+
import dev.restate.sdk.workflow.WorkflowContext;
25+
import dev.restate.sdk.workflow.WorkflowSharedContext;
26+
import dev.restate.sdk.workflow.generated.WorkflowExecutionState;
27+
import io.grpc.Channel;
28+
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
29+
import java.math.BigDecimal;
30+
import java.time.Duration;
31+
import java.time.Instant;
32+
import java.util.concurrent.TimeoutException;
33+
import my.restate.sdk.examples.generated.bank.BankRestate;
34+
import my.restate.sdk.examples.generated.bank.TransferRequest;
35+
import my.restate.sdk.examples.generated.bank.TransferResult;
36+
import org.apache.logging.log4j.LogManager;
37+
import org.apache.logging.log4j.Logger;
38+
39+
@Service(ServiceType.WORKFLOW)
40+
public class Loan {
41+
42+
// --- Data types used by the Loan Worfklow
43+
44+
enum Status {
45+
SUBMITTED,
46+
WAITING_HUMAN_APPROVAL,
47+
APPROVED,
48+
NOT_APPROVED,
49+
TRANSFER_SUCCEEDED,
50+
TRANSFER_FAILED
51+
}
52+
53+
public static class LoanRequest {
54+
55+
private final String customerName;
56+
private final String customerId;
57+
private final String customerBankAccount;
58+
private final BigDecimal amount;
59+
60+
@JsonCreator
61+
public LoanRequest(
62+
@JsonProperty("customerName") String customerName,
63+
@JsonProperty("customerId") String customerId,
64+
@JsonProperty("customerBankAccount") String customerBankAccount,
65+
@JsonProperty("amount") BigDecimal amount) {
66+
this.customerName = customerName;
67+
this.customerId = customerId;
68+
this.customerBankAccount = customerBankAccount;
69+
this.amount = amount;
70+
}
71+
72+
public String getCustomerName() {
73+
return customerName;
74+
}
75+
76+
public String getCustomerId() {
77+
return customerId;
78+
}
79+
80+
public String getCustomerBankAccount() {
81+
return customerBankAccount;
82+
}
83+
84+
public BigDecimal getAmount() {
85+
return amount;
86+
}
87+
}
88+
89+
private static final Logger LOG = LogManager.getLogger(Loan.class);
90+
91+
private static final StateKey<Status> STATUS =
92+
StateKey.of("status", JacksonSerdes.of(Status.class));
93+
private static final StateKey<LoanRequest> LOAN_REQUEST =
94+
StateKey.of("loanRequest", JacksonSerdes.of(LoanRequest.class));
95+
private static final DurablePromiseKey<Boolean> HUMAN_APPROVAL =
96+
DurablePromiseKey.of("humanApproval", CoreSerdes.JSON_BOOLEAN);
97+
private static final StateKey<String> TRANSFER_EXECUTION_TIME =
98+
StateKey.of("transferExecutionTime", CoreSerdes.JSON_STRING);
99+
100+
// --- The main workflow method
101+
102+
@Workflow
103+
public void run(WorkflowContext ctx, LoanRequest loanRequest) {
104+
// 1. Set status
105+
ctx.set(STATUS, Status.SUBMITTED);
106+
ctx.set(LOAN_REQUEST, loanRequest);
107+
108+
LOG.info("Loan request submitted");
109+
110+
// 2. Ask human approval
111+
ctx.sideEffect(() -> askHumanApproval(ctx.workflowKey()));
112+
ctx.set(STATUS, Status.WAITING_HUMAN_APPROVAL);
113+
114+
// 3. Wait human approval
115+
boolean approved = ctx.durablePromise(HUMAN_APPROVAL).awaitable().await();
116+
if (!approved) {
117+
LOG.info("Not approved");
118+
ctx.set(STATUS, Status.NOT_APPROVED);
119+
return;
120+
}
121+
LOG.info("Approved");
122+
ctx.set(STATUS, Status.APPROVED);
123+
124+
// 4. Request money transaction to the bank
125+
var bankClient = BankRestate.newClient(ctx);
126+
TransferResult transferResponse;
127+
try {
128+
transferResponse =
129+
bankClient
130+
.transfer(
131+
TransferRequest.newBuilder()
132+
.setAmount(loanRequest.getAmount().toString())
133+
.setBankAccount(loanRequest.getCustomerBankAccount())
134+
.build())
135+
.await(Duration.ofDays(7));
136+
} catch (TerminalException | TimeoutException e) {
137+
LOG.warn("Transaction failed", e);
138+
ctx.set(STATUS, Status.TRANSFER_FAILED);
139+
return;
140+
}
141+
142+
LOG.info("Transfer complete");
143+
144+
// 5. Transfer complete!
145+
ctx.set(TRANSFER_EXECUTION_TIME, transferResponse.getExecutionTime());
146+
ctx.set(STATUS, Status.TRANSFER_SUCCEEDED);
147+
}
148+
149+
// --- Methods to approve/reject loan
150+
151+
@Shared
152+
public void approveLoan(WorkflowSharedContext ctx) {
153+
ctx.durablePromiseHandle(HUMAN_APPROVAL).resolve(true);
154+
}
155+
156+
@Shared
157+
public void rejectLoan(WorkflowSharedContext ctx) {
158+
ctx.durablePromiseHandle(HUMAN_APPROVAL).resolve(false);
159+
}
160+
161+
public static void main(String[] args) {
162+
RestateHttpEndpointBuilder.builder()
163+
.with(new Loan())
164+
.withService(new MockBank())
165+
.buildAndListen();
166+
167+
// Register the service in the meantime!
168+
LOG.info("Now it's time to register this deployment");
169+
170+
try {
171+
Thread.sleep(20_000);
172+
} catch (InterruptedException e) {
173+
throw new RuntimeException(e);
174+
}
175+
176+
// To invoke the workflow:
177+
Channel restateChannel =
178+
NettyChannelBuilder.forAddress("127.0.0.1", 8080).usePlaintext().build();
179+
LoanExternalClient client = new LoanExternalClient(restateChannel, "my-loan");
180+
181+
WorkflowExecutionState state =
182+
client.submit(
183+
new LoanRequest(
184+
"Francesco", "slinkydeveloper", "DE1234", new BigDecimal("1000000000")));
185+
if (state != WorkflowExecutionState.STARTED) {
186+
throw new IllegalStateException("Unexpected state " + state);
187+
}
188+
189+
LOG.info("Started loan workflow");
190+
191+
// Takes some bureaucratic time to approve the loan
192+
try {
193+
Thread.sleep(10_000);
194+
} catch (InterruptedException e) {
195+
throw new RuntimeException(e);
196+
}
197+
198+
LOG.info("We took the decision to approve your loan! You can now achieve your dreams!");
199+
200+
// Now approve it
201+
client.approveLoan();
202+
203+
while (!client.isCompleted()) {
204+
LOG.info("Not completed yet");
205+
try {
206+
Thread.sleep(10_000);
207+
} catch (InterruptedException e) {
208+
throw new RuntimeException(e);
209+
}
210+
}
211+
212+
LOG.info("Loan workflow completed");
213+
}
214+
215+
// -- Some mocks
216+
217+
private static void askHumanApproval(String workflowKey) throws InterruptedException {
218+
LOG.info("Sending human approval request");
219+
Thread.sleep(1000);
220+
}
221+
222+
private static class MockBank extends BankRestate.BankRestateImplBase {
223+
@Override
224+
public TransferResult transfer(UnkeyedContext context, TransferRequest request)
225+
throws TerminalException {
226+
boolean shouldAccept = context.random().nextInt(3) != 1;
227+
if (shouldAccept) {
228+
return TransferResult.newBuilder().setExecutionTime(Instant.now().toString()).build();
229+
} else {
230+
throw new TerminalException("Won't accept the transfer");
231+
}
232+
}
233+
}
234+
}

examples/src/main/proto/bank.proto

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
syntax = "proto3";
10+
11+
import "google/protobuf/empty.proto";
12+
import "dev/restate/ext.proto";
13+
14+
package bank;
15+
16+
option java_multiple_files = true;
17+
option java_package = "my.restate.sdk.examples.generated.bank";
18+
19+
service Bank {
20+
option (dev.restate.ext.service_type) = UNKEYED;
21+
22+
rpc Transfer (TransferRequest) returns (TransferResult);
23+
}
24+
25+
message TransferRequest {
26+
string bank_account = 1;
27+
28+
string amount = 2;
29+
}
30+
31+
message TransferResult {
32+
string execution_time = 1;
33+
}

protoc-gen-restate/build.gradle.kts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ dependencies {
1515
implementation("com.salesforce.servicelibs:jprotoc:1.2.2") {
1616
exclude("javax.annotation", "javax.annotation-api")
1717
}
18-
implementation(project(":sdk-common"))
18+
protobuf(project(":sdk-common"))
1919
}
2020

21-
application { mainClass.set("dev.restate.sdk.gen.RestateGen") }
21+
protobuf { generateProtoTasks { ofSourceSet("main").forEach { it.builtins { java } } } }
22+
23+
application { mainClass.set("dev.restate.sdk.protocgen.RestateGen") }
2224

2325
tasks.named<ShadowJar>("shadowJar") {
2426
// Override the default jar

protoc-gen-restate/src/main/java/dev/restate/sdk/gen/CodeGenUtils.java renamed to protoc-gen-restate/src/main/java/dev/restate/sdk/protocgen/CodeGenUtils.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// You can find a copy of the license in file LICENSE in the root
77
// directory of this repository or package, or at
88
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9-
package dev.restate.sdk.gen;
9+
package dev.restate.sdk.protocgen;
1010

1111
import com.google.common.html.HtmlEscapers;
1212
import com.google.protobuf.DescriptorProtos;
@@ -180,7 +180,7 @@ private CodeGenUtils() {}
180180
* @return lower name
181181
*/
182182
static String mixedLower(String word, boolean isKotlinGen) {
183-
StringBuffer w = new StringBuffer();
183+
StringBuilder w = new StringBuilder();
184184
w.append(Character.toLowerCase(word.charAt(0)));
185185

186186
boolean afterUnderscore = false;

protoc-gen-restate/src/main/java/dev/restate/sdk/gen/RestateGen.java renamed to protoc-gen-restate/src/main/java/dev/restate/sdk/protocgen/RestateGen.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// You can find a copy of the license in file LICENSE in the root
77
// directory of this repository or package, or at
88
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9-
package dev.restate.sdk.gen;
9+
package dev.restate.sdk.protocgen;
1010

1111
import com.google.protobuf.DescriptorProtos;
1212
import com.google.protobuf.compiler.PluginProtos;

sdk-api-gen/build.gradle.kts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
plugins {
2+
java
3+
application
4+
`library-publishing-conventions`
5+
}
6+
7+
description = "Restate SDK API Gen"
8+
9+
dependencies {
10+
implementation(project(":sdk-common"))
11+
implementation(project(":sdk-api"))
12+
implementation(project(":sdk-workflow-api"))
13+
implementation(project(":sdk-serde-jackson"))
14+
15+
implementation("com.github.jknack:handlebars:4.3.1")
16+
}

0 commit comments

Comments
 (0)