diff --git a/README.md b/README.md index 3153238dd..e0b6dc4fa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/springboot/build.gradle b/springboot/build.gradle index 6d5b7a292..ba091d9f3 100644 --- a/springboot/build.gradle +++ b/springboot/build.gradle @@ -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') diff --git a/springboot/src/main/java/io/temporal/samples/springboot/SamplesController.java b/springboot/src/main/java/io/temporal/samples/springboot/SamplesController.java index 888c9d256..7f0d65e26 100644 --- a/springboot/src/main/java/io/temporal/samples/springboot/SamplesController.java +++ b/springboot/src/main/java/io/temporal/samples/springboot/SamplesController.java @@ -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; @@ -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"); @@ -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); + } + } } diff --git a/springboot/src/main/java/io/temporal/samples/springboot/hello/model/Person.java b/springboot/src/main/java/io/temporal/samples/springboot/hello/model/Person.java index d87d8f0f6..c71862b23 100644 --- a/springboot/src/main/java/io/temporal/samples/springboot/hello/model/Person.java +++ b/springboot/src/main/java/io/temporal/samples/springboot/hello/model/Person.java @@ -24,7 +24,6 @@ public class Person { private String lastName; public Person() {} - ; public Person(String firstName, String lastName) { this.firstName = firstName; diff --git a/springboot/src/main/java/io/temporal/samples/springboot/update/ProductNotAvailableForAmountException.java b/springboot/src/main/java/io/temporal/samples/springboot/update/ProductNotAvailableForAmountException.java new file mode 100644 index 000000000..56ce54e4a --- /dev/null +++ b/springboot/src/main/java/io/temporal/samples/springboot/update/ProductNotAvailableForAmountException.java @@ -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); + } +} diff --git a/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseActivities.java b/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseActivities.java new file mode 100644 index 000000000..8be9aa256 --- /dev/null +++ b/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseActivities.java @@ -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); +} diff --git a/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseActivitiesImpl.java b/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseActivitiesImpl.java new file mode 100644 index 000000000..4f0596d59 --- /dev/null +++ b/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseActivitiesImpl.java @@ -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 productOptional = productRepository.findById(purchase.getProduct()); + if (productOptional.isPresent()) { + return productOptional.get(); + } + return null; + } +} diff --git a/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseWorkflow.java b/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseWorkflow.java new file mode 100644 index 000000000..d5f086683 --- /dev/null +++ b/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseWorkflow.java @@ -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(); +} diff --git a/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseWorkflowImpl.java b/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseWorkflowImpl.java new file mode 100644 index 000000000..bc04b3f41 --- /dev/null +++ b/springboot/src/main/java/io/temporal/samples/springboot/update/PurchaseWorkflowImpl.java @@ -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; + } +} diff --git a/springboot/src/main/java/io/temporal/samples/springboot/update/README.md b/springboot/src/main/java/io/temporal/samples/springboot/update/README.md new file mode 100644 index 000000000..fdeaba18a --- /dev/null +++ b/springboot/src/main/java/io/temporal/samples/springboot/update/README.md @@ -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. \ No newline at end of file diff --git a/springboot/src/main/java/io/temporal/samples/springboot/update/ResourceNotFoundException.java b/springboot/src/main/java/io/temporal/samples/springboot/update/ResourceNotFoundException.java new file mode 100644 index 000000000..21cfc71ac --- /dev/null +++ b/springboot/src/main/java/io/temporal/samples/springboot/update/ResourceNotFoundException.java @@ -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 org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class ResourceNotFoundException extends Exception { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/springboot/src/main/java/io/temporal/samples/springboot/update/model/Product.java b/springboot/src/main/java/io/temporal/samples/springboot/update/model/Product.java new file mode 100644 index 000000000..74e39b7b3 --- /dev/null +++ b/springboot/src/main/java/io/temporal/samples/springboot/update/model/Product.java @@ -0,0 +1,129 @@ +/* + * 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.model; + +import javax.persistence.*; + +@Entity +public class Product { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(nullable = false) + public String name; + + @Column(nullable = false) + public String code; + + @Column(nullable = false) + public String description; + + @Column(nullable = false) + public int price = 0; + + @Column(nullable = false) + private int stock = 20; + + public Product() {} + + public Product(Integer id, String name, String code, String description, int price, int stock) { + this.id = id; + this.name = name; + this.code = code; + this.description = description; + this.price = price; + this.stock = stock; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getPrice() { + return price; + } + + public void setPrice(int price) { + this.price = price; + } + + public int getStock() { + return stock; + } + + public void setStock(int stock) { + this.stock = stock; + } + + public boolean removeStock() { + if (this.stock > 0) { + this.stock--; + return true; + } else { + return false; + } + } + + public boolean removeStock(int value) { + if (this.stock > 0) { + this.stock -= value; + return true; + } else { + return false; + } + } + + public void addStock() { + this.stock++; + } + + public void addStock(int value) { + this.stock += value; + } +} diff --git a/springboot/src/main/java/io/temporal/samples/springboot/update/model/ProductRepository.java b/springboot/src/main/java/io/temporal/samples/springboot/update/model/ProductRepository.java new file mode 100644 index 000000000..5d8d2dd99 --- /dev/null +++ b/springboot/src/main/java/io/temporal/samples/springboot/update/model/ProductRepository.java @@ -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.model; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductRepository extends JpaRepository {} diff --git a/springboot/src/main/java/io/temporal/samples/springboot/update/model/Purchase.java b/springboot/src/main/java/io/temporal/samples/springboot/update/model/Purchase.java new file mode 100644 index 000000000..3457c65a4 --- /dev/null +++ b/springboot/src/main/java/io/temporal/samples/springboot/update/model/Purchase.java @@ -0,0 +1,48 @@ +/* + * 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.model; + +public class Purchase { + int product; + int amount; + + public Purchase() {} + + public Purchase(int product, int amount) { + this.product = product; + this.amount = amount; + } + + public int getProduct() { + return product; + } + + public void setProduct(int product) { + this.product = product; + } + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } +} diff --git a/springboot/src/main/resources/application.yaml b/springboot/src/main/resources/application.yaml index 98ae11653..a6177bde4 100644 --- a/springboot/src/main/resources/application.yaml +++ b/springboot/src/main/resources/application.yaml @@ -22,6 +22,21 @@ spring: # max-threads: 10 workersAutoDiscovery: packages: io.temporal.samples.springboot + # data source config for some samples that need it + datasource: + url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false; + username: sa + password: pass + driver-class-name: org.h2.Driver + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create-drop + defer-datasource-initialization: true + ## enable h2 console (h2-console endpoint) for debugging + h2: + console: + enabled: true # actuator (sdk metrics) management: endpoints: diff --git a/springboot/src/main/resources/data.sql b/springboot/src/main/resources/data.sql new file mode 100644 index 000000000..cd2d206c6 --- /dev/null +++ b/springboot/src/main/resources/data.sql @@ -0,0 +1,5 @@ +INSERT INTO product(name, code, description, price, stock) VALUES ('Zoom U-Tale Worm', 'w1', 'The U-Tale Worms are another one of Zooms truly-impressive big bass baits.', 5, 20); +INSERT INTO product(name, code, description, price, stock) VALUES ('Yamamoto Baits 5" Senko', 'w2', 'Yamamoto Baits 5" Senko has become a mainstay for bass throughout the United States.', 7, 15); +INSERT INTO product(name, code, description, price, stock) VALUES ('Rapala Original Floating Minnow', 'w3', 'The Rapala Original Floating Minnow is the lure that started it all and is still one of the most popular lures around.', 10, 10); +INSERT INTO product(name, code, description, price, stock) VALUES ('Z-Man Jackhammer', 'w4', 'Exclusive patented ChatterBait bladed swim jig design and stainless hex-shaped ChatterBlade.', 18, 10); +INSERT INTO product(name, code, description, price, stock) VALUES ('Roboworm Straight Tail Worm Bait', 'w5', 'Roboworms are the most consistant poured baits on the market.', 9, 30); \ No newline at end of file diff --git a/springboot/src/main/resources/templates/fragments.html b/springboot/src/main/resources/templates/fragments.html new file mode 100644 index 000000000..29b0a1ff1 --- /dev/null +++ b/springboot/src/main/resources/templates/fragments.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/springboot/src/main/resources/templates/hello.html b/springboot/src/main/resources/templates/hello.html index 399bdfe77..8bf6b7f0c 100644 --- a/springboot/src/main/resources/templates/hello.html +++ b/springboot/src/main/resources/templates/hello.html @@ -1,16 +1,6 @@ - - - - - - - - - +
@@ -61,5 +51,6 @@
Workflow result:
}); }); +
\ No newline at end of file diff --git a/springboot/src/main/resources/templates/index.html b/springboot/src/main/resources/templates/index.html index 249204725..8b4ab28fe 100644 --- a/springboot/src/main/resources/templates/index.html +++ b/springboot/src/main/resources/templates/index.html @@ -1,16 +1,6 @@ - - - - - - - - - +
@@ -23,9 +13,11 @@
Temporal Java SDK Samples
+
-
\ No newline at end of file diff --git a/springboot/src/main/resources/templates/metrics.html b/springboot/src/main/resources/templates/metrics.html index aadca1ba3..ee7267bef 100644 --- a/springboot/src/main/resources/templates/metrics.html +++ b/springboot/src/main/resources/templates/metrics.html @@ -1,16 +1,6 @@ - - - - - - - - - +
@@ -82,5 +72,6 @@
4. View metrics (Prometheus format)
+
\ No newline at end of file diff --git a/springboot/src/main/resources/templates/update.html b/springboot/src/main/resources/templates/update.html new file mode 100644 index 000000000..843ba8b10 --- /dev/null +++ b/springboot/src/main/resources/templates/update.html @@ -0,0 +1,125 @@ + + + + +
+
+
+

Temporal Java SDK Samples

+
This sample shows use of Synchronous Update feature. The page shows a form used to make a purchase. +
Under the form you can see the total inventory of our fishing items. + Inventory has a pre-set stock (number of each item available). Once you make a purchase our workflow + is sent an update, meaning we want to make the purchase and respond back when its done. +
The workflow update validator checks if the given amount of an item is still available in the inventory. +
If it is then the update goes through and the inventory table is dynamically updated. + If not then the update is rejected and shown on the screen.
+
+
+
+
+

Purchase Form

+
+
+

Select product: + +

+ + +
+

+

+
+
+ +
+
+

+

Inventory

+
+ + + + + + + + + + + + + + + + + + + +
IdNameDescriptionPrice ($)Stock (Available)
IdNameDescriptionPriceStock
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/springboot/src/test/java/io/temporal/samples/springboot/HelloUpdateTest.java b/springboot/src/test/java/io/temporal/samples/springboot/HelloUpdateTest.java new file mode 100644 index 000000000..c71027f8e --- /dev/null +++ b/springboot/src/test/java/io/temporal/samples/springboot/HelloUpdateTest.java @@ -0,0 +1,91 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.temporal.client.*; +import io.temporal.samples.springboot.update.PurchaseWorkflow; +import io.temporal.samples.springboot.update.model.Purchase; +import io.temporal.testing.TestWorkflowEnvironment; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootTest(classes = HelloUpdateTest.Configuration.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class HelloUpdateTest { + + @Autowired ConfigurableApplicationContext applicationContext; + + @Autowired TestWorkflowEnvironment testWorkflowEnvironment; + + @Autowired WorkflowClient workflowClient; + + @BeforeEach + void setUp() { + applicationContext.start(); + } + + @Test + public void testUpdate() { + PurchaseWorkflow workflow = + workflowClient.newWorkflowStub( + PurchaseWorkflow.class, + WorkflowOptions.newBuilder() + .setTaskQueue("UpdateSampleTaskQueue") + .setWorkflowId("NewPurchase") + .build()); + Purchase purchase = new Purchase(1, 3); + WorkflowClient.start(workflow::start); + // send update + workflow.makePurchase(purchase); + workflow.exit(); + WorkflowStub.fromTyped(workflow).getResult(Void.class); + } + + @Test() + public void testUpdateRejected() { + PurchaseWorkflow workflow = + workflowClient.newWorkflowStub( + PurchaseWorkflow.class, + WorkflowOptions.newBuilder() + .setTaskQueue("UpdateSampleTaskQueue") + .setWorkflowId("NewPurchase") + .build()); + Purchase purchase = new Purchase(1, 40); + WorkflowClient.start(workflow::start); + // send update + assertThrows( + WorkflowUpdateException.class, + () -> { + workflow.makePurchase(purchase); + }); + workflow.exit(); + WorkflowStub.fromTyped(workflow).getResult(Void.class); + } + + @ComponentScan + public static class Configuration {} +} diff --git a/springboot/src/test/resources/application.yaml b/springboot/src/test/resources/application.yaml index 95ab48335..931aa0a86 100644 --- a/springboot/src/test/resources/application.yaml +++ b/springboot/src/test/resources/application.yaml @@ -3,14 +3,28 @@ server: spring: application: name: temporal-samples + # temporal specific configs temporal: connection: target: 127.0.0.1:7233 target.namespace: default workersAutoDiscovery: packages: io.temporal.samples.springboot + # enable test server for testing test-server: enabled: true + # data source config for tests that need it + datasource: + url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;DB_CLOSE_ON_EXIT=FALSE; + username: sa + password: pass + driver-class-name: org.h2.Driver + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create-drop + defer-datasource-initialization: true +# specific for samples samples: data: - language: english \ No newline at end of file + language: english diff --git a/springboot/src/test/resources/data.sql b/springboot/src/test/resources/data.sql new file mode 100644 index 000000000..cd2d206c6 --- /dev/null +++ b/springboot/src/test/resources/data.sql @@ -0,0 +1,5 @@ +INSERT INTO product(name, code, description, price, stock) VALUES ('Zoom U-Tale Worm', 'w1', 'The U-Tale Worms are another one of Zooms truly-impressive big bass baits.', 5, 20); +INSERT INTO product(name, code, description, price, stock) VALUES ('Yamamoto Baits 5" Senko', 'w2', 'Yamamoto Baits 5" Senko has become a mainstay for bass throughout the United States.', 7, 15); +INSERT INTO product(name, code, description, price, stock) VALUES ('Rapala Original Floating Minnow', 'w3', 'The Rapala Original Floating Minnow is the lure that started it all and is still one of the most popular lures around.', 10, 10); +INSERT INTO product(name, code, description, price, stock) VALUES ('Z-Man Jackhammer', 'w4', 'Exclusive patented ChatterBait bladed swim jig design and stainless hex-shaped ChatterBlade.', 18, 10); +INSERT INTO product(name, code, description, price, stock) VALUES ('Roboworm Straight Tail Worm Bait', 'w5', 'Roboworms are the most consistant poured baits on the market.', 9, 30); \ No newline at end of file