-
Notifications
You must be signed in to change notification settings - Fork 206
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* adding items to cart works, need to implement remove and payment/checkout * clean up code, add a README * Stop using websockets, use Update-with-start to interact between web app and temporal workflow * remove websocket references * Fix CI * change cost units to cents * Add Update validator, workflow completion/CAN support * Add sample temporal server command to README, use sessionId to mimic sessions by cookies * Address more PR feedback, rename server to UI, remove references to anything "dummy" * fix test * Pass cart state for CAN * Fix await condition * rename id to itemID, change sample to only handle lifecycle of a single cart * Fix test
- Loading branch information
Showing
10 changed files
with
399 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Shopping Cart | ||
|
||
This sample workflow shows how a shopping cart application can be implemented. | ||
This sample utilizes Update-with-Start and the `WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING` | ||
option to start and continually update the workflow with the same Update-with-Start | ||
call. This is also known as lazy-init. You will see in the Temporal UI, when you checkout | ||
your cart, the workflow will complete and `ui/main.go` will throw an error | ||
`workflow execution already completed`. This example can be extended to handle concurrent | ||
shoppers (would need some sort of SessionID) or support starting a new session/workflow | ||
after checkout. | ||
|
||
Another interesting Update-with-Start use case is | ||
[early return](https://github.com/temporalio/samples-go/tree/main/early-return), | ||
which supplements this sample and can be used to handle the transaction and payment | ||
portion of this shopping cart scenario. | ||
|
||
### Steps to run this sample: | ||
1) Run a [Temporal service](https://github.com/temporalio/samples-go/tree/main/#how-to-use). | ||
|
||
NOTE: frontend.enableExecuteMultiOperation=true must be configured for the server | ||
in order to use Update-with-Start. For example: | ||
``` | ||
temporal server start-dev --dynamic-config-value frontend.enableExecuteMultiOperation=true | ||
``` | ||
|
||
2) Run the following command to start the worker | ||
``` | ||
go run shoppingcart/worker/main.go | ||
``` | ||
3) Run the following command to start the web app | ||
``` | ||
go run shoppingcart/ui/main.go | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"github.com/pborman/uuid" | ||
"github.com/temporalio/samples-go/shoppingcart" | ||
enumspb "go.temporal.io/api/enums/v1" | ||
"go.temporal.io/sdk/client" | ||
"log" | ||
"net/http" | ||
"sort" | ||
) | ||
|
||
var ( | ||
workflowClient client.Client | ||
// Units are in cents | ||
itemCosts = map[string]int{ | ||
"apple": 200, | ||
"banana": 100, | ||
"watermelon": 500, | ||
"television": 100000, | ||
"house": 100000000, | ||
"car": 5000000, | ||
"binder": 1000, | ||
} | ||
sessionId = newSession() | ||
) | ||
|
||
func main() { | ||
var err error | ||
workflowClient, err = client.Dial(client.Options{ | ||
HostPort: client.DefaultHostPort, | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
http.HandleFunc("/", listHandler) | ||
http.HandleFunc("/action", actionHandler) | ||
|
||
fmt.Println("Shopping Cart UI available at http://localhost:8080") | ||
if err := http.ListenAndServe(":8080", nil); err != nil { | ||
fmt.Println("Error starting server:", err) | ||
} | ||
} | ||
|
||
func listHandler(w http.ResponseWriter, _ *http.Request) { | ||
w.Header().Set("Content-Type", "text/html") // Set the content type to HTML | ||
_, _ = fmt.Fprint(w, "<h1>SAMPLE SHOPPING WEBSITE</h1>"+ | ||
"<a href=\"/list\">HOME</a> <a href=\"/action?type=checkout\">Checkout</a>"+ | ||
"<h3>Available Items to Purchase</h3><table border=1><tr><th>Item</th><th>Cost</th><th>Action</th>") | ||
|
||
keys := make([]string, 0) | ||
for k := range itemCosts { | ||
keys = append(keys, k) | ||
} | ||
sort.Strings(keys) | ||
for _, k := range keys { | ||
actionButton := fmt.Sprintf("<a href=\"/action?type=add&itemID=%s\">"+ | ||
"<button style=\"background-color:#4CAF50;\">Add to Cart</button></a>", k) | ||
dollars := float64(itemCosts[k]) / 100 | ||
_, _ = fmt.Fprintf(w, "<tr><td>%s</td><td>$%.2f</td><td>%s</td></tr>", k, dollars, actionButton) | ||
} | ||
_, _ = fmt.Fprint(w, "</table><h3>Current items in cart:</h3>"+ | ||
"<table border=1><tr><th>Item</th><th>Quantity</th><th>Action</th>") | ||
|
||
cartState := updateWithStartCart("list", "") | ||
|
||
// List current items in cart | ||
keys = make([]string, 0) | ||
for k := range cartState.Items { | ||
keys = append(keys, k) | ||
} | ||
sort.Strings(keys) | ||
for _, k := range keys { | ||
removeButton := fmt.Sprintf("<a href=\"/action?type=remove&itemID=%s\">"+ | ||
"<button style=\"background-color:#f44336;\">Remove Item</button></a>", k) | ||
_, _ = fmt.Fprintf(w, "<tr><td>%s</td><td>%d</td><td>%s</td></tr>", k, cartState.Items[k], removeButton) | ||
} | ||
_, _ = fmt.Fprint(w, "</table>") | ||
} | ||
|
||
func actionHandler(w http.ResponseWriter, r *http.Request) { | ||
actionType := r.URL.Query().Get("type") | ||
switch actionType { | ||
case "checkout": | ||
err := workflowClient.SignalWorkflow(context.Background(), sessionId, "", "checkout", nil) | ||
if err != nil { | ||
log.Fatalln("Error signaling checkout:", err) | ||
} | ||
case "add", "remove", "list": | ||
itemID := r.URL.Query().Get("itemID") | ||
updateWithStartCart(actionType, itemID) | ||
default: | ||
log.Fatalln("Invalid action type:", actionType) | ||
} | ||
|
||
// Generate the HTML after communicating with the Temporal workflow. | ||
// "list" already generates HTML, so skip for that scenario | ||
if actionType != "list" { | ||
listHandler(w, r) | ||
} | ||
} | ||
|
||
func updateWithStartCart(actionType string, itemID string) shoppingcart.CartState { | ||
// Handle a client request to add an item to the shopping cart. The user is not logged in, but a session ID is | ||
// available from a cookie, and we use this as the cart ID. The Temporal client was created at service-start | ||
// time and is shared by all request handlers. | ||
// | ||
// A Workflow Type exists that can be used to represent a shopping cart. The method uses update-with-start to | ||
// add an item to the shopping cart, creating the cart if it doesn't already exist. | ||
// | ||
// Note that the workflow handle is available, even if the Update fails. | ||
ctx := context.Background() | ||
|
||
updateWithStartOptions := client.UpdateWithStartWorkflowOptions{ | ||
StartWorkflowOperation: workflowClient.NewWithStartWorkflowOperation(client.StartWorkflowOptions{ | ||
ID: sessionId, | ||
TaskQueue: shoppingcart.TaskQueueName, | ||
// WorkflowIDConflictPolicy is required when using UpdateWithStartWorkflow. | ||
// Here we use USE_EXISTING, because we want to reuse the running workflow, as it | ||
// is long-running and keeping track of our cart state. | ||
WorkflowIDConflictPolicy: enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, | ||
}, shoppingcart.CartWorkflow, nil), | ||
UpdateOptions: client.UpdateWorkflowOptions{ | ||
UpdateName: shoppingcart.UpdateName, | ||
WaitForStage: client.WorkflowUpdateStageCompleted, | ||
Args: []interface{}{actionType, itemID}, | ||
}, | ||
} | ||
updateHandle, err := workflowClient.UpdateWithStartWorkflow(ctx, updateWithStartOptions) | ||
if err != nil { | ||
// For example, a client-side validation error (e.g. missing conflict | ||
// policy or invalid workflow argument types in the start operation), or | ||
// a server-side failure (e.g. failed to start workflow, or exceeded | ||
// limit on concurrent update per workflow execution). | ||
log.Fatalln("Error issuing update-with-start:", err) | ||
} | ||
|
||
log.Println("Updated workflow", | ||
"WorkflowID:", updateHandle.WorkflowID(), | ||
"RunID:", updateHandle.RunID()) | ||
|
||
// Always use a zero variable before calling Get for any Go SDK API | ||
cartState := shoppingcart.CartState{Items: make(map[string]int)} | ||
if err = updateHandle.Get(ctx, &cartState); err != nil { | ||
log.Fatalln("Error obtaining update result:", err) | ||
} | ||
return cartState | ||
} | ||
|
||
func newSession() string { | ||
return "session-" + uuid.New() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package main | ||
|
||
import ( | ||
"log" | ||
|
||
"go.temporal.io/sdk/client" | ||
"go.temporal.io/sdk/worker" | ||
|
||
"github.com/temporalio/samples-go/shoppingcart" | ||
) | ||
|
||
func main() { | ||
// The client and worker are heavyweight objects that should be created once per process. | ||
c, err := client.Dial(client.Options{ | ||
HostPort: client.DefaultHostPort, | ||
}) | ||
if err != nil { | ||
log.Fatalln("Unable to create client", err) | ||
} | ||
defer c.Close() | ||
|
||
w := worker.New(c, shoppingcart.TaskQueueName, worker.Options{}) | ||
|
||
w.RegisterWorkflow(shoppingcart.CartWorkflow) | ||
|
||
err = w.Run(worker.InterruptCh()) | ||
if err != nil { | ||
log.Fatalln("Unable to start worker", err) | ||
} | ||
} |
Oops, something went wrong.