Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,5 @@ See the README.md file in each main sample directory for cut/paste Gradle comman
More info on each sample:
- [**Hello**](/springboot/src/main/java/io/temporal/samples/springboot/hello): Invoke simple "Hello" workflow from a GET endpoint
- [**SDK Metrics**](/springboot/src/main/java/io/temporal/samples/springboot/metrics): Learn how to set up SDK Metrics
- [**Synchronous Update**](/springboot/src/main/java/io/temporal/samples/springboot/update): Learn how to use Synchronous Update feature with this purchase sample

2 changes: 2 additions & 0 deletions springboot/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
implementation "org.springframework.boot:spring-boot-starter-actuator"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "io.temporal:temporal-spring-boot-starter-alpha:$javaSDKVersion"
runtimeOnly "io.micrometer:micrometer-registry-prometheus"
runtimeOnly "com.h2database:h2"
testImplementation "org.springframework.boot:spring-boot-starter-test"
dependencies {
errorproneJavac('com.google.errorprone:javac:9+181-r4173-1')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@

package io.temporal.samples.springboot;

import io.grpc.StatusRuntimeException;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.client.WorkflowStub;
import io.temporal.client.WorkflowUpdateException;
import io.temporal.samples.springboot.hello.HelloWorkflow;
import io.temporal.samples.springboot.hello.model.Person;
import io.temporal.samples.springboot.update.PurchaseWorkflow;
import io.temporal.samples.springboot.update.model.ProductRepository;
import io.temporal.samples.springboot.update.model.Purchase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
Expand All @@ -36,6 +42,8 @@ public class SamplesController {

@Autowired WorkflowClient client;

@Autowired ProductRepository productRepository;

@GetMapping("/hello")
public String hello(Model model) {
model.addAttribute("sample", "Say Hello");
Expand Down Expand Up @@ -64,4 +72,55 @@ public String metrics(Model model) {
model.addAttribute("sample", "SDK Metrics");
return "metrics";
}

@GetMapping("/update")
public String update(Model model) {
model.addAttribute("sample", "Synchronous Update");
model.addAttribute("products", productRepository.findAll());
return "update";
}

@GetMapping("/update/inventory")
public String updateInventory(Model model) {
model.addAttribute("products", productRepository.findAll());
return "update :: inventory";
}

@PostMapping(
value = "/update/purchase",
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.TEXT_HTML_VALUE})
ResponseEntity purchase(@RequestBody Purchase purchase) {
PurchaseWorkflow workflow =
client.newWorkflowStub(
PurchaseWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskQueue("UpdateSampleTaskQueue")
.setWorkflowId("NewPurchase")
.build());
WorkflowClient.start(workflow::start);

// send update
try {
boolean isValidPurchase = workflow.makePurchase(purchase);
// for sample send exit to workflow exec and wait till it completes
workflow.exit();
WorkflowStub.fromTyped(workflow).getResult(Void.class);
if (!isValidPurchase) {
return new ResponseEntity("\"Invalid purchase\"", HttpStatus.NOT_FOUND);
}
return new ResponseEntity("\"" + "Purchase successful" + "\"", HttpStatus.OK);
} catch (WorkflowUpdateException | StatusRuntimeException e) {
// for sample send exit to workflow exec and wait till it completes
workflow.exit();
WorkflowStub.fromTyped(workflow).getResult(Void.class);

String message = e.getMessage();
if (e instanceof WorkflowUpdateException) {
message = e.getCause().getMessage();
}

return new ResponseEntity("\"" + message + "\"", HttpStatus.NOT_FOUND);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public class Person {
private String lastName;

public Person() {}
;

public Person(String firstName, String lastName) {
this.firstName = firstName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package io.temporal.samples.springboot.update;

public class ProductNotAvailableForAmountException extends Exception {
public ProductNotAvailableForAmountException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package io.temporal.samples.springboot.update;

import io.temporal.activity.ActivityInterface;
import io.temporal.samples.springboot.update.model.Purchase;

@ActivityInterface
public interface PurchaseActivities {
boolean isProductInStockForPurchase(Purchase purchase);

boolean makePurchase(Purchase purchase);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package io.temporal.samples.springboot.update;

import io.temporal.samples.springboot.update.model.Product;
import io.temporal.samples.springboot.update.model.ProductRepository;
import io.temporal.samples.springboot.update.model.Purchase;
import io.temporal.spring.boot.ActivityImpl;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@ActivityImpl(taskQueues = "UpdateSampleTaskQueue")
public class PurchaseActivitiesImpl implements PurchaseActivities {
@Autowired ProductRepository productRepository;

@Override
public boolean isProductInStockForPurchase(Purchase purchase) {
Product product = getProductFor(purchase);
return product != null && product.getStock() >= purchase.getAmount();
}

@Override
public boolean makePurchase(Purchase purchase) {
Product product = getProductFor(purchase);
if (product != null) {
product.setStock(product.getStock() - purchase.getAmount());
productRepository.save(product);
return true;
}
return false;
}

private Product getProductFor(Purchase purchase) {
Optional<Product> productOptional = productRepository.findById(purchase.getProduct());
if (productOptional.isPresent()) {
return productOptional.get();
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package io.temporal.samples.springboot.update;

import io.temporal.samples.springboot.update.model.Purchase;
import io.temporal.workflow.*;

@WorkflowInterface
public interface PurchaseWorkflow {
@WorkflowMethod
void start();

@UpdateMethod
boolean makePurchase(Purchase purchase);

@UpdateValidatorMethod(updateName = "makePurchase")
void makePurchaseValidator(Purchase purchase);

@SignalMethod
void exit();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package io.temporal.samples.springboot.update;

import io.temporal.activity.LocalActivityOptions;
import io.temporal.failure.ApplicationFailure;
import io.temporal.samples.springboot.update.model.Purchase;
import io.temporal.spring.boot.WorkflowImpl;
import io.temporal.workflow.Workflow;
import java.time.Duration;

@WorkflowImpl(taskQueues = "UpdateSampleTaskQueue")
public class PurchaseWorkflowImpl implements PurchaseWorkflow {

private boolean newPurchase = false;
private boolean exit = false;
private PurchaseActivities activities =
Workflow.newLocalActivityStub(
PurchaseActivities.class,
LocalActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(2)).build());

@Override
public void start() {
// for sake of sample we only wait for a single purchase or exit signal
Workflow.await(() -> newPurchase || exit);
}

@Override
public boolean makePurchase(Purchase purchase) {

if (!activities.isProductInStockForPurchase(purchase)) {
throw ApplicationFailure.newFailure(
"Product "
+ purchase.getProduct()
+ " is not in stock for amount "
+ purchase.getAmount(),
ProductNotAvailableForAmountException.class.getName(),
purchase);
}

return activities.makePurchase(purchase);
}

@Override
public void makePurchaseValidator(Purchase purchase) {
// Not allowed to change workflow state inside validator
// So invocations of (local) activities is prohibited
// We can validate the purchase with some business logic here

// Assume we have some max inventory amount for single item set to 100
if (purchase == null || (purchase.getAmount() < 0 || purchase.getAmount() > 100)) {
throw new IllegalArgumentException(
"Invalid Product or amount (Product id:"
+ purchase.getProduct()
+ ", amount"
+ purchase.getAmount()
+ ")");
}
}

@Override
public void exit() {
this.exit = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# SpringBoot Synchronous Update Sample

1. Start SpringBoot from main samples repo directory:

./gradlew bootRun

2. In your browser navigate to:

http://localhost:3030/update

Pick one of the fishing items you want to purchase from the inventory drop down list.
Next pick the amount of this item you want to purchase.
The inventory is presented in the table below the form.
For each item you can see the current availble stock count.
Try first picking an item and then an amount that is less or equal to the items in
inventory. You will see that the purchase goes through and the inventory table is updated
dynamically.

Now try to pick and item and amount that is greater than what's in our inventory.
You will see that the update fails and you see the "Unable to perform purchase"
message that shows the underlying "ProductNotAvailableForAmountException" exception
raised in the update handler.

Updating our inventory is done via local activities. The check if item and amount
of the fishing item you want to purchase is in inventory is also done by local
activity.

## Note
Make sure that you enable the synchronous update feature on your Temporal cluster.
This can be done in dynamic config with

frontend.enableUpdateWorkflowExecution:
- value: true

If you don't have this enabled you will see error shown when you try to make any purchase.
Loading