Skip to content

Commit b7c642a

Browse files
authored
SpringBoot - Sync Update Example (#490)
* SpringBoot - Sync Update Example Signed-off-by: Tihomir Surdilovic <tihomir@temporal.io> * fix text Signed-off-by: Tihomir Surdilovic <tihomir@temporal.io> * update - move local activity calls from update handler Signed-off-by: Tihomir Surdilovic <tihomir@temporal.io> * update Signed-off-by: Tihomir Surdilovic <tihomir@temporal.io> --------- Signed-off-by: Tihomir Surdilovic <tihomir@temporal.io>
1 parent 307a2df commit b7c642a

24 files changed

Lines changed: 850 additions & 36 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,5 @@ See the README.md file in each main sample directory for cut/paste Gradle comman
141141
More info on each sample:
142142
- [**Hello**](/springboot/src/main/java/io/temporal/samples/springboot/hello): Invoke simple "Hello" workflow from a GET endpoint
143143
- [**SDK Metrics**](/springboot/src/main/java/io/temporal/samples/springboot/metrics): Learn how to set up SDK Metrics
144+
- [**Synchronous Update**](/springboot/src/main/java/io/temporal/samples/springboot/update): Learn how to use Synchronous Update feature with this purchase sample
144145

springboot/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ dependencies {
44
implementation "org.springframework.boot:spring-boot-starter-web"
55
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
66
implementation "org.springframework.boot:spring-boot-starter-actuator"
7+
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
78
implementation "io.temporal:temporal-spring-boot-starter-alpha:$javaSDKVersion"
89
runtimeOnly "io.micrometer:micrometer-registry-prometheus"
10+
runtimeOnly "com.h2database:h2"
911
testImplementation "org.springframework.boot:spring-boot-starter-test"
1012
dependencies {
1113
errorproneJavac('com.google.errorprone:javac:9+181-r4173-1')

springboot/src/main/java/io/temporal/samples/springboot/SamplesController.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,16 @@
1919

2020
package io.temporal.samples.springboot;
2121

22+
import io.grpc.StatusRuntimeException;
2223
import io.temporal.client.WorkflowClient;
2324
import io.temporal.client.WorkflowOptions;
25+
import io.temporal.client.WorkflowStub;
26+
import io.temporal.client.WorkflowUpdateException;
2427
import io.temporal.samples.springboot.hello.HelloWorkflow;
2528
import io.temporal.samples.springboot.hello.model.Person;
29+
import io.temporal.samples.springboot.update.PurchaseWorkflow;
30+
import io.temporal.samples.springboot.update.model.ProductRepository;
31+
import io.temporal.samples.springboot.update.model.Purchase;
2632
import org.springframework.beans.factory.annotation.Autowired;
2733
import org.springframework.http.HttpStatus;
2834
import org.springframework.http.MediaType;
@@ -36,6 +42,8 @@ public class SamplesController {
3642

3743
@Autowired WorkflowClient client;
3844

45+
@Autowired ProductRepository productRepository;
46+
3947
@GetMapping("/hello")
4048
public String hello(Model model) {
4149
model.addAttribute("sample", "Say Hello");
@@ -64,4 +72,55 @@ public String metrics(Model model) {
6472
model.addAttribute("sample", "SDK Metrics");
6573
return "metrics";
6674
}
75+
76+
@GetMapping("/update")
77+
public String update(Model model) {
78+
model.addAttribute("sample", "Synchronous Update");
79+
model.addAttribute("products", productRepository.findAll());
80+
return "update";
81+
}
82+
83+
@GetMapping("/update/inventory")
84+
public String updateInventory(Model model) {
85+
model.addAttribute("products", productRepository.findAll());
86+
return "update :: inventory";
87+
}
88+
89+
@PostMapping(
90+
value = "/update/purchase",
91+
consumes = {MediaType.APPLICATION_JSON_VALUE},
92+
produces = {MediaType.TEXT_HTML_VALUE})
93+
ResponseEntity purchase(@RequestBody Purchase purchase) {
94+
PurchaseWorkflow workflow =
95+
client.newWorkflowStub(
96+
PurchaseWorkflow.class,
97+
WorkflowOptions.newBuilder()
98+
.setTaskQueue("UpdateSampleTaskQueue")
99+
.setWorkflowId("NewPurchase")
100+
.build());
101+
WorkflowClient.start(workflow::start);
102+
103+
// send update
104+
try {
105+
boolean isValidPurchase = workflow.makePurchase(purchase);
106+
// for sample send exit to workflow exec and wait till it completes
107+
workflow.exit();
108+
WorkflowStub.fromTyped(workflow).getResult(Void.class);
109+
if (!isValidPurchase) {
110+
return new ResponseEntity("\"Invalid purchase\"", HttpStatus.NOT_FOUND);
111+
}
112+
return new ResponseEntity("\"" + "Purchase successful" + "\"", HttpStatus.OK);
113+
} catch (WorkflowUpdateException | StatusRuntimeException e) {
114+
// for sample send exit to workflow exec and wait till it completes
115+
workflow.exit();
116+
WorkflowStub.fromTyped(workflow).getResult(Void.class);
117+
118+
String message = e.getMessage();
119+
if (e instanceof WorkflowUpdateException) {
120+
message = e.getCause().getMessage();
121+
}
122+
123+
return new ResponseEntity("\"" + message + "\"", HttpStatus.NOT_FOUND);
124+
}
125+
}
67126
}

springboot/src/main/java/io/temporal/samples/springboot/hello/model/Person.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ public class Person {
2424
private String lastName;
2525

2626
public Person() {}
27-
;
2827

2928
public Person(String firstName, String lastName) {
3029
this.firstName = firstName;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
3+
*
4+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
9+
* use this file except in compliance with the License. A copy of the License is
10+
* located at
11+
*
12+
* http://aws.amazon.com/apache2.0
13+
*
14+
* or in the "license" file accompanying this file. This file is distributed on
15+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16+
* express or implied. See the License for the specific language governing
17+
* permissions and limitations under the License.
18+
*/
19+
20+
package io.temporal.samples.springboot.update;
21+
22+
public class ProductNotAvailableForAmountException extends Exception {
23+
public ProductNotAvailableForAmountException(String message) {
24+
super(message);
25+
}
26+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
3+
*
4+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
9+
* use this file except in compliance with the License. A copy of the License is
10+
* located at
11+
*
12+
* http://aws.amazon.com/apache2.0
13+
*
14+
* or in the "license" file accompanying this file. This file is distributed on
15+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16+
* express or implied. See the License for the specific language governing
17+
* permissions and limitations under the License.
18+
*/
19+
20+
package io.temporal.samples.springboot.update;
21+
22+
import io.temporal.activity.ActivityInterface;
23+
import io.temporal.samples.springboot.update.model.Purchase;
24+
25+
@ActivityInterface
26+
public interface PurchaseActivities {
27+
boolean isProductInStockForPurchase(Purchase purchase);
28+
29+
boolean makePurchase(Purchase purchase);
30+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
3+
*
4+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
9+
* use this file except in compliance with the License. A copy of the License is
10+
* located at
11+
*
12+
* http://aws.amazon.com/apache2.0
13+
*
14+
* or in the "license" file accompanying this file. This file is distributed on
15+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16+
* express or implied. See the License for the specific language governing
17+
* permissions and limitations under the License.
18+
*/
19+
20+
package io.temporal.samples.springboot.update;
21+
22+
import io.temporal.samples.springboot.update.model.Product;
23+
import io.temporal.samples.springboot.update.model.ProductRepository;
24+
import io.temporal.samples.springboot.update.model.Purchase;
25+
import io.temporal.spring.boot.ActivityImpl;
26+
import java.util.Optional;
27+
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.stereotype.Component;
29+
30+
@Component
31+
@ActivityImpl(taskQueues = "UpdateSampleTaskQueue")
32+
public class PurchaseActivitiesImpl implements PurchaseActivities {
33+
@Autowired ProductRepository productRepository;
34+
35+
@Override
36+
public boolean isProductInStockForPurchase(Purchase purchase) {
37+
Product product = getProductFor(purchase);
38+
return product != null && product.getStock() >= purchase.getAmount();
39+
}
40+
41+
@Override
42+
public boolean makePurchase(Purchase purchase) {
43+
Product product = getProductFor(purchase);
44+
if (product != null) {
45+
product.setStock(product.getStock() - purchase.getAmount());
46+
productRepository.save(product);
47+
return true;
48+
}
49+
return false;
50+
}
51+
52+
private Product getProductFor(Purchase purchase) {
53+
Optional<Product> productOptional = productRepository.findById(purchase.getProduct());
54+
if (productOptional.isPresent()) {
55+
return productOptional.get();
56+
}
57+
return null;
58+
}
59+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
3+
*
4+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
9+
* use this file except in compliance with the License. A copy of the License is
10+
* located at
11+
*
12+
* http://aws.amazon.com/apache2.0
13+
*
14+
* or in the "license" file accompanying this file. This file is distributed on
15+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16+
* express or implied. See the License for the specific language governing
17+
* permissions and limitations under the License.
18+
*/
19+
20+
package io.temporal.samples.springboot.update;
21+
22+
import io.temporal.samples.springboot.update.model.Purchase;
23+
import io.temporal.workflow.*;
24+
25+
@WorkflowInterface
26+
public interface PurchaseWorkflow {
27+
@WorkflowMethod
28+
void start();
29+
30+
@UpdateMethod
31+
boolean makePurchase(Purchase purchase);
32+
33+
@UpdateValidatorMethod(updateName = "makePurchase")
34+
void makePurchaseValidator(Purchase purchase);
35+
36+
@SignalMethod
37+
void exit();
38+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
3+
*
4+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
9+
* use this file except in compliance with the License. A copy of the License is
10+
* located at
11+
*
12+
* http://aws.amazon.com/apache2.0
13+
*
14+
* or in the "license" file accompanying this file. This file is distributed on
15+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16+
* express or implied. See the License for the specific language governing
17+
* permissions and limitations under the License.
18+
*/
19+
20+
package io.temporal.samples.springboot.update;
21+
22+
import io.temporal.activity.LocalActivityOptions;
23+
import io.temporal.failure.ApplicationFailure;
24+
import io.temporal.samples.springboot.update.model.Purchase;
25+
import io.temporal.spring.boot.WorkflowImpl;
26+
import io.temporal.workflow.Workflow;
27+
import java.time.Duration;
28+
29+
@WorkflowImpl(taskQueues = "UpdateSampleTaskQueue")
30+
public class PurchaseWorkflowImpl implements PurchaseWorkflow {
31+
32+
private boolean newPurchase = false;
33+
private boolean exit = false;
34+
private PurchaseActivities activities =
35+
Workflow.newLocalActivityStub(
36+
PurchaseActivities.class,
37+
LocalActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());
38+
39+
@Override
40+
public void start() {
41+
// for sake of sample we only wait for a single purchase or exit signal
42+
Workflow.await(() -> newPurchase || exit);
43+
}
44+
45+
@Override
46+
public boolean makePurchase(Purchase purchase) {
47+
48+
if (!activities.isProductInStockForPurchase(purchase)) {
49+
throw ApplicationFailure.newFailure(
50+
"Product "
51+
+ purchase.getProduct()
52+
+ " is not in stock for amount "
53+
+ purchase.getAmount(),
54+
ProductNotAvailableForAmountException.class.getName(),
55+
purchase);
56+
}
57+
58+
return activities.makePurchase(purchase);
59+
}
60+
61+
@Override
62+
public void makePurchaseValidator(Purchase purchase) {
63+
// Not allowed to change workflow state inside validator
64+
// So invocations of (local) activities is prohibited
65+
// We can validate the purchase with some business logic here
66+
67+
// Assume we have some max inventory amount for single item set to 100
68+
if (purchase == null || (purchase.getAmount() < 0 || purchase.getAmount() > 100)) {
69+
throw new IllegalArgumentException(
70+
"Invalid Product or amount (Product id:"
71+
+ purchase.getProduct()
72+
+ ", amount"
73+
+ purchase.getAmount()
74+
+ ")");
75+
}
76+
}
77+
78+
@Override
79+
public void exit() {
80+
this.exit = true;
81+
}
82+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# SpringBoot Synchronous Update Sample
2+
3+
1. Start SpringBoot from main samples repo directory:
4+
5+
./gradlew bootRun
6+
7+
2. In your browser navigate to:
8+
9+
http://localhost:3030/update
10+
11+
Pick one of the fishing items you want to purchase from the inventory drop down list.
12+
Next pick the amount of this item you want to purchase.
13+
The inventory is presented in the table below the form.
14+
For each item you can see the current availble stock count.
15+
Try first picking an item and then an amount that is less or equal to the items in
16+
inventory. You will see that the purchase goes through and the inventory table is updated
17+
dynamically.
18+
19+
Now try to pick and item and amount that is greater than what's in our inventory.
20+
You will see that the update fails and you see the "Unable to perform purchase"
21+
message that shows the underlying "ProductNotAvailableForAmountException" exception
22+
raised in the update handler.
23+
24+
Updating our inventory is done via local activities. The check if item and amount
25+
of the fishing item you want to purchase is in inventory is also done by local
26+
activity.
27+
28+
## Note
29+
Make sure that you enable the synchronous update feature on your Temporal cluster.
30+
This can be done in dynamic config with
31+
32+
frontend.enableUpdateWorkflowExecution:
33+
- value: true
34+
35+
If you don't have this enabled you will see error shown when you try to make any purchase.

0 commit comments

Comments
 (0)