├── workflows ├── service │ ├── README.md │ ├── my_service.go │ └── my_service_mock.go ├── microservices │ ├── README.md │ └── workflow.go ├── engagement │ ├── model.go │ ├── README.md │ └── workflow.go ├── moneytransfer │ ├── README.md │ └── workflow.go ├── registry.go ├── polling │ ├── README.md │ └── workflow.go └── subscription │ ├── README.md │ ├── workflow.go │ └── workflow_test.go ├── .gitignore ├── LICENSE ├── cmd └── server │ ├── iwf │ ├── money_transfer_controller.go │ ├── polling_controller.go │ ├── microservice_controller.go │ ├── subscription_controller.go │ ├── engagement_controller.go │ └── iwf.go │ └── main.go ├── go.mod ├── README.md ├── Makefile └── go.sum /workflows/service/README.md: -------------------------------------------------------------------------------- 1 | ### Command to build this `my_service_mock.go` 2 | 3 | Run this at the root of the project: 4 | ```shell 5 | mockgen -source=workflows/service/my_service.go -package=service -destination=workflows/service/my_service_mock.go 6 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | iwf-samples 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | -------------------------------------------------------------------------------- /workflows/microservices/README.md: -------------------------------------------------------------------------------- 1 | This is the code that is [shown in iWF server as an example of microservice orchestration](https://github.com/indeedeng/iwf#example-microservice-orchestration). 2 | 3 | ## How to test the APIs in browser 4 | 5 | * start workflow: http://localhost:8803/microservice/start?workflowId=12345 6 | * swap the data: http://localhost:8803/microservice/swap?workflowId=12345&data=122 7 | * signal the workflow: http://localhost:8803/microservice/signal?workflowId=12345 -------------------------------------------------------------------------------- /workflows/engagement/model.go: -------------------------------------------------------------------------------- 1 | package engagement 2 | 3 | type Status string 4 | 5 | const ( 6 | StatusInitiated Status = "Initiated" 7 | StatusAccepted Status = "Accepted" 8 | StatusDeclined Status = "Declined" 9 | ) 10 | 11 | type EngagementInput struct { 12 | EmployerId string 13 | JobSeekerId string 14 | Notes string 15 | } 16 | 17 | type EngagementDescription struct { 18 | EmployerId string 19 | JobSeekerId string 20 | Notes string 21 | CurrentStatus Status 22 | } 23 | -------------------------------------------------------------------------------- /workflows/moneytransfer/README.md: -------------------------------------------------------------------------------- 1 | ### How to run 2 | * start a iWF server following the [instructions](https://github.com/indeedeng/iwf#how-to-use) 3 | * build and run this project `make bins && ./iwf-samples start` 4 | * start a workflow: `http://localhost:8803/moneytransfer/start?fromAccount=test1&toAccount=test2&amount=100¬es=hello` 5 | * watch in WebUI `http://localhost:8233/namespaces/default/workflows` 6 | * modify the workflow code to try injecting some errors, and shorten the retry, to see what will happen 7 | -------------------------------------------------------------------------------- /workflows/registry.go: -------------------------------------------------------------------------------- 1 | package workflows 2 | 3 | import ( 4 | "github.com/indeedeng/iwf-golang-samples/workflows/engagement" 5 | "github.com/indeedeng/iwf-golang-samples/workflows/microservices" 6 | "github.com/indeedeng/iwf-golang-samples/workflows/moneytransfer" 7 | "github.com/indeedeng/iwf-golang-samples/workflows/polling" 8 | "github.com/indeedeng/iwf-golang-samples/workflows/service" 9 | "github.com/indeedeng/iwf-golang-samples/workflows/subscription" 10 | "github.com/indeedeng/iwf-golang-sdk/iwf" 11 | ) 12 | 13 | var registry = iwf.NewRegistry() 14 | 15 | func init() { 16 | 17 | svc := service.NewMyService() 18 | 19 | err := registry.AddWorkflows( 20 | subscription.NewSubscriptionWorkflow(svc), 21 | engagement.NewEngagementWorkflow(svc), 22 | microservices.NewMicroserviceOrchestrationWorkflow(svc), 23 | moneytransfer.NewMoneyTransferWorkflow(svc), 24 | polling.NewPollingWorkflow(svc), 25 | ) 26 | if err != nil { 27 | panic(err) 28 | } 29 | } 30 | 31 | func GetRegistry() iwf.Registry { 32 | return registry 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 indeedeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/server/iwf/money_transfer_controller.go: -------------------------------------------------------------------------------- 1 | package iwf 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/indeedeng/iwf-golang-samples/workflows/moneytransfer" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | func startMoneyTransferWorkflow(c *gin.Context) { 13 | fromAccount := c.Query("fromAccount") 14 | toAccount := c.Query("toAccount") 15 | amount := c.Query("amount") 16 | notes := c.Query("notes") 17 | 18 | amountInt, err := strconv.Atoi(amount) 19 | if err != nil { 20 | c.JSON(http.StatusBadRequest, "must provide correct amount via URL parameter") 21 | return 22 | } 23 | 24 | req := moneytransfer.TransferRequest{ 25 | FromAccount: fromAccount, 26 | ToAccount: toAccount, 27 | Notes: notes, 28 | Amount: amountInt, 29 | } 30 | wfId := fmt.Sprintf("money_transfer-%d", time.Now().Unix()) 31 | 32 | _, err = client.StartWorkflow(c.Request.Context(), moneytransfer.MoneyTransferWorkflow{}, wfId, 3600, req, nil) 33 | if err != nil { 34 | c.JSON(http.StatusInternalServerError, err.Error()) 35 | return 36 | } 37 | 38 | c.JSON(http.StatusOK, fmt.Sprintf("workflowId: %v", wfId)) 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /cmd/server/iwf/polling_controller.go: -------------------------------------------------------------------------------- 1 | package iwf 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/indeedeng/iwf-golang-samples/workflows/polling" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | func startPollingWorkflow(c *gin.Context) { 12 | wfId := c.Query("workflowId") 13 | pollingCompletionThreshold := c.Query("pollingCompletionThreshold") 14 | 15 | pollingCompletionThresholdInt, err := strconv.Atoi(pollingCompletionThreshold) 16 | if err != nil { 17 | c.JSON(http.StatusBadRequest, "must provide correct pollingCompletionThreshold via URL parameter") 18 | return 19 | } 20 | 21 | _, err = client.StartWorkflow(c.Request.Context(), polling.PollingWorkflow{}, wfId, 0, pollingCompletionThresholdInt, nil) 22 | if err != nil { 23 | c.JSON(http.StatusInternalServerError, err.Error()) 24 | return 25 | } 26 | 27 | c.JSON(http.StatusOK, fmt.Sprintf("workflowId: %v is started", wfId)) 28 | return 29 | } 30 | 31 | func signalPollingWorkflow(c *gin.Context) { 32 | wfId := c.Query("workflowId") 33 | channel := c.Query("channel") 34 | 35 | err := client.SignalWorkflow(c.Request.Context(), polling.PollingWorkflow{}, wfId, "", channel, nil) 36 | if err != nil { 37 | c.JSON(http.StatusInternalServerError, err.Error()) 38 | return 39 | } 40 | 41 | c.JSON(http.StatusOK, fmt.Sprintf("workflowId: %v is signal", wfId)) 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Cadence workflow OSS organization 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "github.com/indeedeng/iwf-golang-samples/cmd/server/iwf" 25 | "os" 26 | ) 27 | 28 | // main entry point for the iwf server 29 | func main() { 30 | app := iwf.BuildCLI() 31 | app.Run(os.Args) 32 | } 33 | -------------------------------------------------------------------------------- /workflows/polling/README.md: -------------------------------------------------------------------------------- 1 | ### How to run 2 | * Start a iWF server following the [instructions](https://github.com/indeedeng/iwf#how-to-use) 3 | * The easiest way is to run `docker run -p 8801:8801 -p 7233:7233 -p 8233:8233 -e AUTO_FIX_WORKER_URL=host.docker.internal --add-host host.docker.internal:host-gateway -it iworkflowio/iwf-server-lite:latest` 4 | * Build and run this project `make bins && ./iwf-samples start` 5 | * Start a workflow: `http://localhost:8803/polling/start?workflowId=test1&pollingCompletionThreshold=100` 6 | * pollingCompletionThreshold means how many times the workflow will poll before complete the polling task C 7 | * Signal the workflow to complete task A and B: 8 | * complete task A: `http://localhost:8803/polling/complete?workflowId=test1&channel=taskACompleted` 9 | * complete task B: `http://localhost:8803/polling/complete?workflowId=test1&channel=taskBCompleted` 10 | * alternatively you can signal the workflow in WebUI manually 11 | * Watch in WebUI `http://localhost:8233/namespaces/default/workflows` 12 | * Modify the pollingCompletionThreshold and see how the workflow complete task C automatically 13 | 14 | 15 | ### Screenshots 16 | * The workflow should automatically continue As New after every 100 actions 17 | Screenshot 2024-01-01 at 10 06 11 PM 18 | * You can use query handler to look at the current data like this 19 | Screenshot 2024-01-01 at 10 08 41 PM 20 | -------------------------------------------------------------------------------- /cmd/server/iwf/microservice_controller.go: -------------------------------------------------------------------------------- 1 | package iwf 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/indeedeng/iwf-golang-samples/workflows/microservices" 8 | "net/http" 9 | ) 10 | 11 | func startMicroserviceWorkflow(c *gin.Context) { 12 | wfId := c.Query("workflowId") 13 | if wfId != "" { 14 | wf := microservices.OrchestrationWorkflow{} 15 | runId, err := client.StartWorkflow(c.Request.Context(), wf, wfId, 3600, "test initial data", nil) 16 | if err != nil { 17 | c.JSON(http.StatusInternalServerError, err.Error()) 18 | return 19 | } 20 | c.JSON(http.StatusOK, fmt.Sprintf("workflowId: %v runId: %v", wfId, runId)) 21 | return 22 | } 23 | c.JSON(http.StatusBadRequest, "must provide workflowId via URL parameter") 24 | } 25 | 26 | func signalMicroserviceWorkflow(c *gin.Context) { 27 | wfId := c.Query("workflowId") 28 | if wfId != "" { 29 | wf := microservices.OrchestrationWorkflow{} 30 | err := client.SignalWorkflow(context.Background(), wf, wfId, "", microservices.SignalChannelReady, nil) 31 | if err != nil { 32 | c.JSON(http.StatusInternalServerError, err.Error()) 33 | } else { 34 | c.JSON(http.StatusOK, struct{}{}) 35 | } 36 | return 37 | } 38 | c.JSON(http.StatusBadRequest, "must provide workflowId via URL parameter") 39 | } 40 | 41 | func swapDataMicroserviceWorkflow(c *gin.Context) { 42 | wfId := c.Query("workflowId") 43 | newData := c.Query("data") 44 | if wfId != "" { 45 | wf := microservices.OrchestrationWorkflow{} 46 | var output string 47 | err := client.InvokeRPC(context.Background(), wfId, "", wf.Swap, newData, &output) 48 | if err != nil { 49 | c.JSON(http.StatusInternalServerError, err.Error()) 50 | } else { 51 | c.JSON(http.StatusOK, output) 52 | } 53 | return 54 | } 55 | c.JSON(http.StatusBadRequest, "must provide workflowId via URL parameter") 56 | } 57 | -------------------------------------------------------------------------------- /cmd/server/iwf/subscription_controller.go: -------------------------------------------------------------------------------- 1 | package iwf 2 | 3 | import ( 4 | "context" 5 | "github.com/gin-gonic/gin" 6 | "github.com/indeedeng/iwf-golang-samples/workflows/subscription" 7 | "github.com/indeedeng/iwf-golang-sdk/iwf" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | func cancelSubscription(c *gin.Context) { 13 | wfId := c.Query("workflowId") 14 | if wfId != "" { 15 | err := client.SignalWorkflow(c.Request.Context(), &subscription.SubscriptionWorkflow{}, wfId, "", subscription.SignalCancelSubscription, nil) 16 | if err != nil { 17 | c.JSON(http.StatusInternalServerError, err.Error()) 18 | } else { 19 | c.JSON(http.StatusOK, struct{}{}) 20 | } 21 | return 22 | } 23 | c.JSON(http.StatusBadRequest, "must provide workflowId via URL parameter") 24 | } 25 | 26 | func descSubscription(c *gin.Context) { 27 | wfId := c.Query("workflowId") 28 | if wfId != "" { 29 | wf := subscription.SubscriptionWorkflow{} 30 | var rpcOutput subscription.Subscription 31 | err := client.InvokeRPC(context.Background(), wfId, "", wf.Describe, nil, &rpcOutput) 32 | if err != nil { 33 | c.JSON(http.StatusInternalServerError, err.Error()) 34 | } else { 35 | c.JSON(http.StatusOK, rpcOutput) 36 | } 37 | return 38 | } 39 | c.JSON(http.StatusBadRequest, "must provide workflowId via URL parameter") 40 | } 41 | 42 | func updateSubscriptionChargeAmount(c *gin.Context) { 43 | wfId := c.Query("workflowId") 44 | newChargeAmountStr := c.Query("newChargeAmount") 45 | newAmount, err := strconv.Atoi(newChargeAmountStr) 46 | 47 | if wfId != "" && err == nil { 48 | err := client.SignalWorkflow(c.Request.Context(), &subscription.SubscriptionWorkflow{}, wfId, "", subscription.SignalUpdateBillingPeriodChargeAmount, newAmount) 49 | if err != nil { 50 | c.JSON(http.StatusInternalServerError, iwf.GetOpenApiErrorBody(err)) 51 | } else { 52 | c.JSON(http.StatusOK, struct{}{}) 53 | } 54 | return 55 | } 56 | c.JSON(http.StatusBadRequest, "must provide correct workflowId and newChargeAmount via URL parameter") 57 | } 58 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/indeedeng/iwf-golang-samples 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.1 6 | 7 | require ( 8 | github.com/gin-gonic/gin v1.9.1 9 | github.com/golang/mock v1.6.0 10 | github.com/indeedeng/iwf-golang-sdk v1.6.0 11 | github.com/stretchr/testify v1.10.0 12 | github.com/urfave/cli v1.22.10 13 | ) 14 | 15 | require ( 16 | github.com/bytedance/sonic v1.13.2 // indirect 17 | github.com/bytedance/sonic/loader v0.2.4 // indirect 18 | github.com/cloudwego/base64x v0.1.5 // indirect 19 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 22 | github.com/gin-contrib/sse v1.1.0 // indirect 23 | github.com/go-playground/locales v0.14.1 // indirect 24 | github.com/go-playground/universal-translator v0.18.1 // indirect 25 | github.com/go-playground/validator/v10 v10.26.0 // indirect 26 | github.com/goccy/go-json v0.10.5 // indirect 27 | github.com/json-iterator/go v1.1.12 // indirect 28 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 29 | github.com/leodido/go-urn v1.4.0 // indirect 30 | github.com/mattn/go-isatty v0.0.20 // indirect 31 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 32 | github.com/modern-go/reflect2 v1.0.2 // indirect 33 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 34 | github.com/pmezard/go-difflib v1.0.0 // indirect 35 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 36 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 37 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 38 | github.com/ugorji/go/codec v1.2.12 // indirect 39 | go.uber.org/mock v0.3.0 // indirect 40 | golang.org/x/arch v0.16.0 // indirect 41 | golang.org/x/crypto v0.37.0 // indirect 42 | golang.org/x/net v0.39.0 // indirect 43 | golang.org/x/sys v0.32.0 // indirect 44 | golang.org/x/text v0.24.0 // indirect 45 | google.golang.org/protobuf v1.36.6 // indirect 46 | gopkg.in/yaml.v3 v3.0.1 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iwf-golang-samples 2 | 3 | Samples for [iWF Golang SDK](https://github.com/indeedeng/iwf-golang-sdk) that runs 4 | against [iWF server](https://github.com/indeedeng/iwf) 5 | 6 | ## Setup 7 | 8 | 1. Start a iWF server following the [instructions](https://github.com/indeedeng/iwf#how-to-run-this-server) 9 | 2. Run this project 10 | * To build the binary, run `make bins` 11 | * To run the sample service: run `./iwf-samples start` 12 | 13 | _Note that by default this project will listen on 8803 port(default worker port for iWF Golang SDK)_ 14 | 15 | ## Product Use case samples 16 | 17 | ### [Money Transfer Workflow/SAGA Patten](./workflows/moneytransfer) 18 | This example shows how to transfer money from one account to another account. 19 | The transfer involves multiple steps. When any step fails, the whole transfer is canceled with some compensation steps. 20 | 21 | ### [Microservice orchestration](./workflows/microservices) 22 | This is the code that is [shown in iWF server as an example of microservice orchestration](https://github.com/indeedeng/iwf#example-microservice-orchestration). 23 | 24 | ### [JobSeeker Engagement workflow](./workflows/engagement) 25 | Screenshot 2023-04-21 at 8 53 25 AM 26 | This engagement workflow is for: 27 | 28 | * An engagement is initiated by an employer to reach out to a jobSeeker(via email/SMS/etc) 29 | * The jobSeeker could respond with decline or accept 30 | * If jobSeeker doesn't respond, it will get reminder 31 | * An engagement can change from declined to accepted, but cannot change from accepted to declined 32 | 33 | 34 | ### [Subscription](./workflows/subscription) workflow 35 | 36 | This [Subscription workflow](https://github.com/indeedeng/iwf-golang-samples/tree/main/workflows/subscription) (with unit tests) is to match the use case described in 37 | * [Temporal TypeScript tutorials](https://learn.temporal.io/tutorials/typescript/subscriptions/) 38 | * [Temporal go sample](https://github.com/temporalio/subscription-workflow-project-template-go) 39 | * [Temporal Java Sample](https://github.com/temporalio/subscription-workflow-project-template-java) 40 | * [Cadence Java example](https://cadenceworkflow.io/docs/concepts/workflows/#example) 41 | 42 | 43 | ### [Task orchestration with polling & signal](./workflows/polling) 44 | Orchestrating three services: 45 | * Task A: receive a signal when completing 46 | * Task B: receive a signal when completing 47 | * Task C: polling until completion -------------------------------------------------------------------------------- /workflows/subscription/README.md: -------------------------------------------------------------------------------- 1 | This subscription workflow is to match the use case described in 2 | * [Temporal TypeScript tutorials](https://learn.temporal.io/tutorials/typescript/subscriptions/) 3 | * [Temporal go sample](https://github.com/temporalio/subscription-workflow-project-template-go) 4 | * [Temporal Java Sample](https://github.com/temporalio/subscription-workflow-project-template-java) 5 | * [Cadence Java example](https://cadenceworkflow.io/docs/concepts/workflows/#example) 6 | 7 | ## Use case statement 8 | Build an application for a limited time Subscription (eg a 36 month Phone plan) that satisfies these conditions: 9 | 10 | 1. When the user signs up, send a welcome email and start a free trial for **TrialPeriod**. 11 | 12 | 2. When the TrialPeriod expires, start the billing process. 13 | * If the user cancels during the trial, send a trial cancellation email. 14 | 15 | 3. Billing Process: 16 | * As long as you have not exceeded **MaxBillingPeriods**, charge the customer for the **BillingPeriodChargeAmount**. 17 | * Then wait for the next **BillingPeriod**. 18 | * If the customer cancels during a billing period, send a subscription cancellation email. 19 | * If Subscription has ended normally (exceeded MaxBillingPeriods without cancellation), send a subscription ended email. 20 | 21 | 4. At any point while subscriptions are ongoing, be able to look up and change any customer's amount charged and current status and info.  22 | 23 | Of course, this all has to be fault tolerant, scalable to millions of customers, testable, maintainable, and observable. 24 | 25 | ## Controller 26 | And controller is a very thin layer of calling iWF client APIs and workflow RPC stub APIs. See [subscriptionController](../../cmd/server/iwf/subscription_controller.go). 27 | 28 | ## How to run 29 | 30 | 31 | To start a subscription workflow: 32 | * Open http://localhost:8803/subscription/start 33 | 34 | It will return you a **workflowId**. 35 | 36 | The controller is hard coded to start with 20s as trial period, 10s as billing period, $100 as period charge amount for 10 max billing periods 37 | 38 | To update the period charge amount : 39 | * Open http://localhost:8803/subscription/updateChargeAmount?workflowId=&newChargeAmount= 40 | 41 | To cancel the subscription: 42 | * Open http://localhost:8803/subscription/cancel?workflowId= 43 | 44 | To describe the subscription: 45 | * Open http://localhost:8803/subscription/describe?workflowId= 46 | 47 | This is a iWF state diagram to visualize the workflow design: 48 | ![subscription state diagram](https://user-images.githubusercontent.com/4523955/217110240-5dfe1d33-0b7c-49f2-8c12-b0d91c4eb970.png) 49 | 50 | -------------------------------------------------------------------------------- /cmd/server/iwf/engagement_controller.go: -------------------------------------------------------------------------------- 1 | package iwf 2 | 3 | import ( 4 | "context" 5 | "github.com/gin-gonic/gin" 6 | "github.com/indeedeng/iwf-golang-samples/workflows/engagement" 7 | "github.com/indeedeng/iwf-golang-sdk/gen/iwfidl" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | func descEngagement(c *gin.Context) { 13 | wfId := c.Query("workflowId") 14 | if wfId != "" { 15 | wf := engagement.EngagementWorkflow{} 16 | var rpcOutput engagement.EngagementDescription 17 | err := client.InvokeRPC(context.Background(), wfId, "", wf.Describe, nil, &rpcOutput) 18 | if err != nil { 19 | c.JSON(http.StatusInternalServerError, err.Error()) 20 | } else { 21 | c.JSON(http.StatusOK, rpcOutput) 22 | } 23 | return 24 | } 25 | c.JSON(http.StatusBadRequest, "must provide workflowId via URL parameter") 26 | } 27 | 28 | func optOutReminder(c *gin.Context) { 29 | wfId := c.Query("workflowId") 30 | if wfId != "" { 31 | wf := engagement.EngagementWorkflow{} 32 | err := client.SignalWorkflow(context.Background(), wf, wfId, "", engagement.SignalChannelOptOutReminder, nil) 33 | if err != nil { 34 | c.JSON(http.StatusInternalServerError, err.Error()) 35 | } else { 36 | c.JSON(http.StatusOK, struct{}{}) 37 | } 38 | return 39 | } 40 | c.JSON(http.StatusBadRequest, "must provide workflowId via URL parameter") 41 | } 42 | 43 | func declineEngagement(c *gin.Context) { 44 | wfId := c.Query("workflowId") 45 | if wfId != "" { 46 | wf := engagement.EngagementWorkflow{} 47 | err := client.InvokeRPC(context.Background(), wfId, "", wf.Decline, nil, nil) 48 | if err != nil { 49 | c.JSON(http.StatusInternalServerError, err.Error()) 50 | } else { 51 | c.JSON(http.StatusOK, struct{}{}) 52 | } 53 | return 54 | } 55 | c.JSON(http.StatusBadRequest, "must provide workflowId via URL parameter") 56 | } 57 | 58 | func acceptEngagement(c *gin.Context) { 59 | wfId := c.Query("workflowId") 60 | if wfId != "" { 61 | wf := engagement.EngagementWorkflow{} 62 | err := client.InvokeRPC(context.Background(), wfId, "", wf.Accept, nil, nil) 63 | if err != nil { 64 | c.JSON(http.StatusInternalServerError, err.Error()) 65 | } else { 66 | c.JSON(http.StatusOK, struct{}{}) 67 | } 68 | return 69 | } 70 | c.JSON(http.StatusBadRequest, "must provide workflowId via URL parameter") 71 | } 72 | 73 | func listEngagements(c *gin.Context) { 74 | query := c.Query("query") 75 | if query != "" { 76 | if strings.HasPrefix(query, "'") { 77 | query = strings.Trim(query, "'") 78 | } 79 | resp, err := client.SearchWorkflow(context.Background(), iwfidl.WorkflowSearchRequest{ 80 | Query: query, 81 | }) 82 | if err != nil { 83 | c.JSON(http.StatusInternalServerError, err.Error()) 84 | } else { 85 | c.JSON(http.StatusOK, resp) 86 | } 87 | return 88 | } 89 | c.JSON(http.StatusBadRequest, "must provide workflowId via URL parameter") 90 | } 91 | -------------------------------------------------------------------------------- /workflows/service/my_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "fmt" 4 | 5 | type MyService interface { 6 | SendEmail(recipient, subject, content string) 7 | ChargeUser(email, customerId string, amount int) 8 | UpdateExternalSystem(message string) 9 | CallAPI1(data string) 10 | CallAPI2(data string) 11 | CallAPI3(data string) 12 | CallAPI4(data string) 13 | 14 | CheckBalance(account string, amount int) bool 15 | Debit(account string, amount int) error 16 | Credit(account string, amount int) error 17 | CreateDebitMemo(account string, amount int, notes string) error 18 | CreateCreditMemo(account string, amount int, notes string) error 19 | 20 | UndoDebit(account string, amount int) error 21 | UndoCredit(account string, amount int) error 22 | UndoCreateDebitMemo(account string, amount int, notes string) error 23 | UndoCreateCreditMemo(account string, amount int, notes string) error 24 | } 25 | 26 | type myServiceImpl struct{} 27 | 28 | func (m myServiceImpl) UpdateExternalSystem(message string) { 29 | fmt.Println("Update external system(like via RPC, or sending Kafka message or database):", message) 30 | } 31 | 32 | func (m myServiceImpl) SendEmail(recipient, subject, content string) { 33 | fmt.Printf("sending an email to %v, title: %v, content: %v \n", recipient, subject, content) 34 | } 35 | 36 | func (m myServiceImpl) ChargeUser(email, customerId string, amount int) { 37 | fmt.Printf("charege user customerId[%v] email[%v] for $%v \n", customerId, email, amount) 38 | } 39 | 40 | func (m myServiceImpl) CallAPI1(data string) { 41 | fmt.Println("call API1") 42 | } 43 | 44 | func (m myServiceImpl) CallAPI2(data string) { 45 | fmt.Println("call API2") 46 | } 47 | 48 | func (m myServiceImpl) CallAPI3(data string) { 49 | fmt.Println("call API3") 50 | } 51 | 52 | func (m myServiceImpl) CallAPI4(data string) { 53 | fmt.Println("call API4") 54 | } 55 | 56 | func (m myServiceImpl) CheckBalance(account string, amount int) bool { 57 | return true 58 | } 59 | 60 | func (m myServiceImpl) Debit(account string, amount int) error { 61 | // return some error here to test retry and failure handling mechanism 62 | return nil 63 | } 64 | 65 | func (m myServiceImpl) Credit(account string, amount int) error { 66 | // return some error here to test retry and failure handling mechanism 67 | return nil 68 | } 69 | 70 | func (m myServiceImpl) CreateDebitMemo(account string, amount int, notes string) error { 71 | // return some error here to test retry and failure handling mechanism 72 | return nil 73 | } 74 | 75 | func (m myServiceImpl) CreateCreditMemo(account string, amount int, notes string) error { 76 | // return some error here to test retry and failure handling mechanism 77 | return nil 78 | } 79 | 80 | func (m myServiceImpl) UndoDebit(account string, amount int) error { 81 | // return some error here to test retry and failure handling mechanism 82 | return nil 83 | } 84 | 85 | func (m myServiceImpl) UndoCredit(account string, amount int) error { 86 | // return some error here to test retry and failure handling mechanism 87 | return nil 88 | } 89 | 90 | func (m myServiceImpl) UndoCreateDebitMemo(account string, amount int, notes string) error { 91 | // return some error here to test retry and failure handling mechanism 92 | return nil 93 | } 94 | 95 | func (m myServiceImpl) UndoCreateCreditMemo(account string, amount int, notes string) error { 96 | // return some error here to test retry and failure handling mechanism 97 | return nil 98 | } 99 | 100 | func NewMyService() MyService { 101 | return &myServiceImpl{} 102 | } 103 | -------------------------------------------------------------------------------- /workflows/engagement/README.md: -------------------------------------------------------------------------------- 1 | # Use case 2 | Screenshot 2023-04-21 at 8 54 44 AM 3 | 4 | 5 | * An engagement is initiated by an employer to reach out to a jobSeeker(via email/SMS/etc) 6 | * The jobSeeker could respond with decline or accept 7 | * If jobSeeker doesn't respond, it will get reminder 8 | * An engagement can change from declined to accepted, but cannot change from accepted to declined 9 | 10 | # API requirements 11 | 12 | * Start an engagement 13 | * Describe an engagement 14 | * Opt-out email reminder for an engagement 15 | * Decline engagement 16 | * Accept engagement 17 | * Notify external systems about the engagement changes, with eventual consistency guarantee 18 | * List engagements in different/any patterns (which would require a lot of indexes if using traditional DB) 19 | * By employerId, status order by updateTime 20 | * By jobSeekerId, status order by updateTime 21 | * By employerId + jobSeekerId 22 | * By status, order by updateTime 23 | 24 | # Design 25 | 26 | Screenshot 2023-04-21 at 8 58 50 AM 27 | 28 | # Implementation Details 29 | 30 | ## InitState 31 | ![Screenshot 2023-05-23 at 4 19 07 PM](https://github.com/indeedeng/iwf/assets/4523955/1104f0d6-1933-4842-b22b-0b31cb055092) 32 | 33 | ## ReminderState 34 | 35 | ![Screenshot 2023-05-23 at 4 19 18 PM](https://github.com/indeedeng/iwf/assets/4523955/2cdfc832-36ff-49d0-addf-d9101108aeb9) 36 | 37 | ## RPC 38 | 39 | ![Screenshot 2023-05-23 at 4 19 28 PM](https://github.com/indeedeng/iwf/assets/4523955/b498439c-c79a-40ee-9d56-f0961727865d) 40 | 41 | ## NotifyExtState 42 | ![Screenshot 2023-05-23 at 4 19 46 PM](https://github.com/indeedeng/iwf/assets/4523955/e7e52e94-b383-4565-a1d2-d50b9c184745) 43 | 44 | 45 | ## Controller 46 | And controller is a very thin layer of calling iWF client APIs and workflow RPC stub APIs. See [engagement_controller](../../cmd/server/iwf/engagement_controller.go). 47 | 48 | # How to run 49 | 50 | First of all, you need to register the required Search attributes 51 | ## Search attribute requirement 52 | 53 | If using Temporal: 54 | 55 | * New CLI 56 | ```bash 57 | tctl search-attribute create -name EmployerId -type Keyword -y 58 | tctl search-attribute create -name JobSeekerId -type Keyword -y 59 | tctl search-attribute create -name EngagementStatus -type Keyword -y 60 | tctl search-attribute create -name LastUpdateTimeMillis -type Int -y 61 | ``` 62 | 63 | * Old CLI 64 | ``` bash 65 | tctl adm cl asa -n EmployerId -t Keyword 66 | tctl adm cl asa -n JobSeekerId -t Keyword 67 | tctl adm cl asa -n Status -t Keyword 68 | tctl adm cl asa -n LastUpdateTimeMillis -t Int 69 | 70 | ``` 71 | 72 | If using Cadence 73 | 74 | ```bash 75 | cadence adm cl asa --search_attr_key EmployerId --search_attr_type 1 76 | cadence adm cl asa --search_attr_key JobSeekerId --search_attr_type 1 77 | cadence adm cl asa --search_attr_key Status --search_attr_type 1 78 | cadence adm cl asa --search_attr_key LastUpdateTimeMillis --search_attr_type 2 79 | ``` 80 | 81 | ## How to test the APIs in browser 82 | 83 | * start API: http://localhost:8803/engagement/start 84 | * It will return the workflowId which can be used in subsequence API calls. 85 | * describe API: http://localhost:8803/engagement/describe?workflowId= 86 | * opt-out email API: http://localhost:8803/engagement/optout?workflowId= 87 | * decline API: http://localhost:8803/engagement/decline?workflowId=¬es=%22not%20interested%22 88 | * accept API: http://localhost:8803/engagement/accept?workflowId=¬es=%27accept%27 89 | * search API, use queries like: 90 | * ['EmployerId="test-employer-id" ORDER BY LastUpdateTimeMillis '](http://localhost:8803/engagement/list?query=) 91 | * ['EmployerId="test-employer-id"'](http://localhost:8803/engagement/list?query=) 92 | * ['EmployerId="test-employer-id" AND EngagementStatus="Initiated"'](http://localhost:8803/engagement/list?query=) 93 | * etc 94 | -------------------------------------------------------------------------------- /workflows/polling/workflow.go: -------------------------------------------------------------------------------- 1 | package polling 2 | 3 | import ( 4 | "github.com/indeedeng/iwf-golang-samples/workflows/service" 5 | "github.com/indeedeng/iwf-golang-sdk/iwf" 6 | "time" 7 | ) 8 | 9 | func NewPollingWorkflow(svc service.MyService) iwf.ObjectWorkflow { 10 | 11 | return &PollingWorkflow{ 12 | svc: svc, 13 | } 14 | } 15 | 16 | const ( 17 | dataAttrCurrPolls = "currPolls" // tracks how many polls have been done 18 | 19 | SignalChannelTaskACompleted = "taskACompleted" 20 | SignalChannelTaskBCompleted = "taskBCompleted" 21 | 22 | InternalChannelTaskCCompleted = "taskCCompleted" 23 | ) 24 | 25 | type PollingWorkflow struct { 26 | iwf.WorkflowDefaults 27 | 28 | svc service.MyService 29 | } 30 | 31 | func (e PollingWorkflow) GetWorkflowStates() []iwf.StateDef { 32 | return []iwf.StateDef{ 33 | iwf.StartingStateDef(&initState{}), 34 | iwf.NonStartingStateDef(&pollState{svc: e.svc}), 35 | iwf.NonStartingStateDef(&checkAndCompleteState{svc: e.svc}), 36 | } 37 | } 38 | 39 | func (e PollingWorkflow) GetPersistenceSchema() []iwf.PersistenceFieldDef { 40 | return []iwf.PersistenceFieldDef{ 41 | iwf.DataAttributeDef(dataAttrCurrPolls), 42 | } 43 | } 44 | 45 | func (e PollingWorkflow) GetCommunicationSchema() []iwf.CommunicationMethodDef { 46 | return []iwf.CommunicationMethodDef{ 47 | iwf.SignalChannelDef(SignalChannelTaskACompleted), 48 | iwf.SignalChannelDef(SignalChannelTaskBCompleted), 49 | iwf.InternalChannelDef(InternalChannelTaskCCompleted), 50 | } 51 | } 52 | 53 | type initState struct { 54 | iwf.WorkflowStateDefaultsNoWaitUntil 55 | } 56 | 57 | func (i initState) Execute( 58 | ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, 59 | communication iwf.Communication, 60 | ) (*iwf.StateDecision, error) { 61 | var maxPollsRequired int 62 | input.Get(&maxPollsRequired) 63 | 64 | return iwf.MultiNextStatesWithInput( 65 | iwf.NewStateMovement(pollState{}, maxPollsRequired), 66 | iwf.NewStateMovement(checkAndCompleteState{}, nil), 67 | ), nil 68 | } 69 | 70 | type checkAndCompleteState struct { 71 | iwf.WorkflowStateDefaults 72 | svc service.MyService 73 | } 74 | 75 | func (i checkAndCompleteState) WaitUntil( 76 | ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication, 77 | ) (*iwf.CommandRequest, error) { 78 | return iwf.AllCommandsCompletedRequest( 79 | iwf.NewSignalCommand("", SignalChannelTaskACompleted), 80 | iwf.NewSignalCommand("", SignalChannelTaskBCompleted), 81 | iwf.NewInternalChannelCommand("", InternalChannelTaskCCompleted), 82 | ), nil 83 | } 84 | 85 | func (i checkAndCompleteState) Execute( 86 | ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, 87 | communication iwf.Communication, 88 | ) (*iwf.StateDecision, error) { 89 | return iwf.GracefulCompletingWorkflow, nil 90 | } 91 | 92 | type pollState struct { 93 | iwf.WorkflowStateDefaults 94 | svc service.MyService 95 | } 96 | 97 | func (i pollState) WaitUntil( 98 | ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication, 99 | ) (*iwf.CommandRequest, error) { 100 | 101 | return iwf.AnyCommandCompletedRequest( 102 | iwf.NewTimerCommand("", time.Now().Add(time.Second*2)), 103 | ), nil 104 | } 105 | 106 | func (i pollState) Execute( 107 | ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, 108 | communication iwf.Communication, 109 | ) (*iwf.StateDecision, error) { 110 | var maxPollsRequired int 111 | input.Get(&maxPollsRequired) 112 | 113 | i.svc.CallAPI1("calling API1 for polling service C") 114 | 115 | var currPolls int 116 | persistence.GetDataAttribute(dataAttrCurrPolls, &currPolls) 117 | if currPolls >= maxPollsRequired { 118 | communication.PublishInternalChannel(InternalChannelTaskCCompleted, nil) 119 | return iwf.DeadEnd, nil 120 | } 121 | 122 | persistence.SetDataAttribute(dataAttrCurrPolls, currPolls+1) 123 | // loop back to check 124 | return iwf.SingleNextState(pollState{}, maxPollsRequired), nil 125 | } -------------------------------------------------------------------------------- /workflows/microservices/workflow.go: -------------------------------------------------------------------------------- 1 | package microservices 2 | 3 | import ( 4 | "github.com/indeedeng/iwf-golang-samples/workflows/service" 5 | "github.com/indeedeng/iwf-golang-sdk/gen/iwfidl" 6 | "github.com/indeedeng/iwf-golang-sdk/iwf" 7 | "time" 8 | ) 9 | 10 | func NewMicroserviceOrchestrationWorkflow(svc service.MyService) iwf.ObjectWorkflow { 11 | 12 | return &OrchestrationWorkflow{ 13 | svc: svc, 14 | } 15 | } 16 | 17 | type OrchestrationWorkflow struct { 18 | iwf.DefaultWorkflowType 19 | 20 | svc service.MyService 21 | } 22 | 23 | func (e OrchestrationWorkflow) GetWorkflowStates() []iwf.StateDef { 24 | return []iwf.StateDef{ 25 | iwf.StartingStateDef(NewState1(e.svc)), 26 | iwf.NonStartingStateDef(NewState2(e.svc)), 27 | iwf.NonStartingStateDef(NewState3(e.svc)), 28 | iwf.NonStartingStateDef(NewState4(e.svc)), 29 | } 30 | } 31 | 32 | func (e OrchestrationWorkflow) GetPersistenceSchema() []iwf.PersistenceFieldDef { 33 | return []iwf.PersistenceFieldDef{ 34 | iwf.DataAttributeDef(keyData), 35 | } 36 | } 37 | 38 | func (e OrchestrationWorkflow) GetCommunicationSchema() []iwf.CommunicationMethodDef { 39 | return []iwf.CommunicationMethodDef{ 40 | iwf.SignalChannelDef(SignalChannelReady), 41 | 42 | iwf.RPCMethodDef(e.Swap, nil), 43 | } 44 | } 45 | 46 | const ( 47 | keyData = "data" 48 | 49 | SignalChannelReady = "Ready" 50 | ) 51 | 52 | func (e OrchestrationWorkflow) Swap(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (interface{}, error) { 53 | 54 | var oldData string 55 | persistence.GetDataAttribute(keyData, &oldData) 56 | var newData string 57 | input.Get(&newData) 58 | persistence.SetDataAttribute(keyData, newData) 59 | 60 | return oldData, nil 61 | } 62 | 63 | func NewState1(svc service.MyService) iwf.WorkflowState { 64 | return state1{svc: svc} 65 | } 66 | 67 | type state1 struct { 68 | iwf.WorkflowStateDefaultsNoWaitUntil 69 | svc service.MyService 70 | } 71 | 72 | func (i state1) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 73 | var inString string 74 | input.Get(&inString) 75 | 76 | i.svc.CallAPI1(inString) 77 | 78 | persistence.SetDataAttribute(keyData, inString) 79 | return iwf.MultiNextStatesWithInput( 80 | iwf.NewStateMovement(state2{}, nil), 81 | iwf.NewStateMovement(state3{}, nil), 82 | ), nil 83 | } 84 | 85 | func NewState2(svc service.MyService) iwf.WorkflowState { 86 | return state2{svc: svc} 87 | } 88 | 89 | type state2 struct { 90 | iwf.WorkflowStateDefaultsNoWaitUntil 91 | svc service.MyService 92 | } 93 | 94 | func (i state2) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 95 | var data string 96 | persistence.GetDataAttribute(keyData, &data) 97 | 98 | i.svc.CallAPI2(data) 99 | return iwf.DeadEnd, nil 100 | } 101 | 102 | func NewState3(svc service.MyService) iwf.WorkflowState { 103 | return state3{svc: svc} 104 | } 105 | 106 | type state3 struct { 107 | iwf.WorkflowStateDefaults 108 | svc service.MyService 109 | } 110 | 111 | func (i state3) WaitUntil(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (*iwf.CommandRequest, error) { 112 | return iwf.AnyCommandCompletedRequest( 113 | iwf.NewTimerCommand("", time.Now().Add(time.Hour*24)), 114 | iwf.NewSignalCommand("", SignalChannelReady), 115 | ), nil 116 | } 117 | 118 | func (i state3) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 119 | var data string 120 | persistence.GetDataAttribute(keyData, &data) 121 | i.svc.CallAPI3(data) 122 | 123 | if commandResults.Timers[0].Status == iwfidl.FIRED { 124 | return iwf.SingleNextState(state4{}, nil), nil 125 | } 126 | return iwf.GracefulCompletingWorkflow, nil 127 | } 128 | 129 | func NewState4(svc service.MyService) iwf.WorkflowState { 130 | return state4{svc: svc} 131 | } 132 | 133 | type state4 struct { 134 | iwf.WorkflowStateDefaultsNoWaitUntil 135 | svc service.MyService 136 | } 137 | 138 | func (i state4) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 139 | var data string 140 | persistence.GetDataAttribute(keyData, &data) 141 | i.svc.CallAPI4(data) 142 | return iwf.GracefulCompletingWorkflow, nil 143 | } 144 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # get rid of default behaviors, they're just noise 2 | MAKEFLAGS += --no-builtin-rules 3 | .SUFFIXES: 4 | 5 | default: help 6 | 7 | # ########################################### 8 | # TL;DR DOCS: 9 | # ########################################### 10 | # - Targets should never, EVER be *actual source files*. 11 | # Always use book-keeping files in $(BUILD). 12 | # Otherwise e.g. changing git branches could confuse Make about what it needs to do. 13 | # - Similarly, prerequisites should be those book-keeping files, 14 | # not source files that are prerequisites for book-keeping. 15 | # e.g. depend on .build/fmt, not $(ALL_SRC), and not both. 16 | # - Be strict and explicit about prerequisites / order of execution / etc. 17 | # - Test your changes with `-j 27 --output-sync` or something! 18 | # - Test your changes with `make -d ...`! It should be reasonable! 19 | 20 | # temporary build products and book-keeping targets that are always good to / safe to clean. 21 | BUILD := .build 22 | # less-than-temporary build products, e.g. tools. 23 | # usually unnecessary to clean, and may require downloads to restore, so this folder is not automatically cleaned. 24 | BIN := .bin 25 | 26 | # ==================================== 27 | # book-keeping files that are used to control sequencing. 28 | # 29 | # you should use these as prerequisites in almost all cases, not the source files themselves. 30 | # these are defined in roughly the reverse order that they are executed, for easier reading. 31 | # 32 | # recipes and any other prerequisites are defined only once, further below. 33 | # ==================================== 34 | 35 | # ==================================== 36 | # helper vars 37 | # ==================================== 38 | 39 | # set a VERBOSE=1 env var for verbose output. VERBOSE=0 (or unset) disables. 40 | # this is used to make verbose flags, suitable for `$(if $(test_v),...)`. 41 | VERBOSE ?= 0 42 | ifneq (0,$(VERBOSE)) 43 | test_v = 1 44 | else 45 | test_v = 46 | endif 47 | 48 | # a literal space value, for makefile purposes 49 | SPACE := 50 | SPACE += 51 | COMMA := , 52 | 53 | # M1 macs may need to switch back to x86, until arm releases are available 54 | EMULATE_X86 = 55 | ifeq ($(shell uname -sm),Darwin arm64) 56 | EMULATE_X86 = arch -x86_64 57 | endif 58 | 59 | # helper for executing bins that need other bins, just `$(BIN_PATH) the_command ...` 60 | # I'd recommend not exporting this in general, to reduce the chance of accidentally using non-versioned tools. 61 | BIN_PATH := PATH="$(abspath $(BIN)):$$PATH" 62 | 63 | # version, git sha, etc flags. 64 | # reasonable to make a :=, but it's only used in one place, so just leave it lazy or do it inline. 65 | GO_BUILD_LDFLAGS = $(shell ./scripts/go-build-ldflags.sh LDFLAG) 66 | 67 | # automatically gather all source files that currently exist. 68 | # works by ignoring everything in the parens (and does not descend into matching folders) due to `-prune`, 69 | # and everything else goes to the other side of the `-o` branch, which is `-print`ed. 70 | # this is dramatically faster than a `find . | grep -v vendor` pipeline, and scales far better. 71 | FRESH_ALL_SRC = $(shell \ 72 | find . \ 73 | \( \ 74 | -path './vendor/*' \ 75 | -o -path './idls/*' \ 76 | -o -path './.build/*' \ 77 | -o -path './.bin/*' \ 78 | \) \ 79 | -prune \ 80 | -o -name '*.go' -print \ 81 | ) 82 | # most things can use a cached copy, e.g. all dependencies. 83 | # this will not include any files that are created during a `make` run, e.g. via protoc, 84 | # but that generally should not matter (e.g. dependencies are computed at parse time, so it 85 | # won't affect behavior either way - choose the fast option). 86 | # 87 | # if you require a fully up-to-date list, e.g. for shell commands, use FRESH_ALL_SRC instead. 88 | ALL_SRC := $(FRESH_ALL_SRC) 89 | # as lint ignores generated code, it can use the cached copy in all cases 90 | LINT_SRC := $(filter-out %_test.go ./.gen/%, $(ALL_SRC)) 91 | 92 | # ==================================== 93 | # $(BIN) targets 94 | # ==================================== 95 | 96 | # downloads and builds a go-gettable tool, versioned by go.mod, and installs 97 | # it into the build folder, named the same as the last portion of the URL. 98 | define go_build_tool 99 | @echo "building $(notdir $(1)) from $(1)..." 100 | @go build -mod=readonly -o $(BIN)/$(notdir $(1)) $(1) 101 | endef 102 | 103 | # ==================================== 104 | # developer-oriented targets 105 | # 106 | # many of these share logic with other intermediates, but are useful to make .PHONY for output on demand. 107 | # as the Makefile is fast, it's reasonable to just delete the book-keeping file recursively make. 108 | # this way the effort is shared with future `make` runs. 109 | # ==================================== 110 | 111 | # "re-make" a target by deleting and re-building book-keeping target(s). 112 | # the + is necessary for parallelism flags to be propagated 113 | define remake 114 | @rm -f $(addprefix $(BUILD)/,$(1)) 115 | @+$(MAKE) --no-print-directory $(addprefix $(BUILD)/,$(1)) 116 | endef 117 | 118 | # useful to actually re-run to get output again. 119 | # reuse the intermediates for simplicity and consistency. 120 | lint: ## (re)run the linter 121 | $(call remake,proto-lint lint) 122 | 123 | # intentionally not re-making, goimports is slow and it's clear when it's unnecessary 124 | fmt: $(BUILD)/fmt ## run goimports 125 | 126 | .PHONY: release clean 127 | 128 | clean: ## Clean binaries and build folder 129 | rm -f $(BINS) 130 | rm -Rf $(BUILD) 131 | $(if \ 132 | $(filter $(BIN)/fake-codegen, $(wildcard $(BIN)/*)), \ 133 | $(warning fake build tools may exist, delete the $(BIN) folder to get real ones if desired),) 134 | 135 | deps: ## Check for dependency updates, for things that are directly imported 136 | @make --no-print-directory DEPS_FILTER='$(JQ_DEPS_ONLY_DIRECT)' deps-all 137 | 138 | deps-all: ## Check for all dependency updates 139 | @go list -u -m -json all \ 140 | | $(JQ_DEPS_AGE) \ 141 | | sort -n 142 | 143 | help: 144 | @# print help first, so it's visible 145 | @printf "\033[36m%-20s\033[0m %s\n" 'help' 'Prints a help message showing any specially-commented targets' 146 | @# then everything matching "target: ## magic comments" 147 | @cat $(MAKEFILE_LIST) | grep -e "^[a-zA-Z_\-]*:.* ## .*" | awk 'BEGIN {FS = ":.*? ## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' | sort 148 | 149 | BINS = 150 | 151 | BINS += iwf-samples 152 | iwf-samples: 153 | @echo "compiling iwf-server with OS: $(GOOS), ARCH: $(GOARCH)" 154 | @go build -o $@ cmd/server/main.go 155 | 156 | .PHONY: bins release clean 157 | 158 | bins: $(BINS) -------------------------------------------------------------------------------- /cmd/server/iwf/iwf.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Cadence workflow OSS organization 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package iwf 22 | 23 | import ( 24 | "fmt" 25 | "github.com/gin-gonic/gin" 26 | "github.com/indeedeng/iwf-golang-samples/workflows" 27 | "github.com/indeedeng/iwf-golang-samples/workflows/engagement" 28 | "github.com/indeedeng/iwf-golang-samples/workflows/subscription" 29 | "github.com/indeedeng/iwf-golang-sdk/gen/iwfidl" 30 | "github.com/indeedeng/iwf-golang-sdk/iwf" 31 | "github.com/urfave/cli" 32 | "log" 33 | "net/http" 34 | "strconv" 35 | "sync" 36 | "time" 37 | ) 38 | 39 | // BuildCLI is the main entry point for the iwf server 40 | func BuildCLI() *cli.App { 41 | app := cli.NewApp() 42 | app.Name = "iwf golang samples" 43 | app.Usage = "iwf golang samples" 44 | app.Version = "beta" 45 | 46 | app.Commands = []cli.Command{ 47 | { 48 | Name: "start", 49 | Aliases: []string{""}, 50 | Usage: "start iwf golang samples", 51 | Action: start, 52 | }, 53 | } 54 | return app 55 | } 56 | 57 | func start(c *cli.Context) { 58 | fmt.Println("start running samples") 59 | closeFn := startWorkflowWorker() 60 | // TODO improve the waiting with process signal 61 | wg := sync.WaitGroup{} 62 | wg.Add(1) 63 | wg.Wait() 64 | closeFn() 65 | } 66 | 67 | var client = iwf.NewClient(workflows.GetRegistry(), nil) 68 | var workerService = iwf.NewWorkerService(workflows.GetRegistry(), nil) 69 | 70 | func startWorkflowWorker() (closeFunc func()) { 71 | router := gin.Default() 72 | router.POST(iwf.WorkflowStateWaitUntilApi, apiV1WorkflowStateStart) 73 | router.POST(iwf.WorkflowStateExecuteApi, apiV1WorkflowStateDecide) 74 | router.POST(iwf.WorkflowWorkerRPCAPI, apiV1WorkflowWorkerRpc) 75 | 76 | engagementInput := engagement.EngagementInput{ 77 | EmployerId: "test-employer-id", 78 | JobSeekerId: "test-jobSeeker-id", 79 | Notes: "test-notes", 80 | } 81 | 82 | customer := subscription.Customer{ 83 | FirstName: "Quanzheng", 84 | LastName: "Long", 85 | Id: "qlong", 86 | Email: "qlong.seattle@gmail.com", 87 | Subscription: subscription.Subscription{ 88 | TrialPeriod: time.Second * 20, 89 | BillingPeriod: time.Second * 10, 90 | MaxBillingPeriods: 10, 91 | BillingPeriodCharge: 100, 92 | }, 93 | } 94 | 95 | router.GET("/subscription/start", startWorklfow(&subscription.SubscriptionWorkflow{}, customer)) 96 | router.GET("/subscription/cancel", cancelSubscription) 97 | router.GET("/subscription/updateChargeAmount", updateSubscriptionChargeAmount) 98 | router.GET("/subscription/describe", descSubscription) 99 | 100 | router.GET("/engagement/start", startWorklfow(&engagement.EngagementWorkflow{}, engagementInput)) 101 | router.GET("/engagement/describe", descEngagement) 102 | router.GET("/engagement/optout", optOutReminder) 103 | router.GET("/engagement/decline", declineEngagement) 104 | router.GET("/engagement/accept", acceptEngagement) 105 | router.GET("/engagement/list", listEngagements) 106 | 107 | router.GET("/microservice/start", startMicroserviceWorkflow) 108 | router.GET("/microservice/swap", swapDataMicroserviceWorkflow) 109 | router.GET("/microservice/signal", signalMicroserviceWorkflow) 110 | 111 | router.GET("/moneytransfer/start", startMoneyTransferWorkflow) 112 | 113 | router.GET("/polling/start", startPollingWorkflow) 114 | router.GET("/polling/complete", signalPollingWorkflow) 115 | 116 | wfServer := &http.Server{ 117 | Addr: ":" + iwf.DefaultWorkerPort, 118 | Handler: router, 119 | } 120 | go func() { 121 | if err := wfServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { 122 | log.Fatalf("listen: %s\n", err) 123 | } 124 | }() 125 | return func() { wfServer.Close() } 126 | } 127 | 128 | func startWorklfow(wf iwf.ObjectWorkflow, input interface{}) gin.HandlerFunc { 129 | return func(c *gin.Context) { 130 | wfId := "TestSample" + strconv.Itoa(int(time.Now().Unix())) 131 | runId, err := client.StartWorkflow(c.Request.Context(), wf, wfId, 3600, input, nil) 132 | if err != nil { 133 | c.JSON(http.StatusInternalServerError, err.Error()) 134 | return 135 | } 136 | c.JSON(http.StatusOK, fmt.Sprintf("workflowId: %v runId: %v", wfId, runId)) 137 | return 138 | } 139 | } 140 | 141 | func apiV1WorkflowStateStart(c *gin.Context) { 142 | var req iwfidl.WorkflowStateWaitUntilRequest 143 | if err := c.ShouldBindJSON(&req); err != nil { 144 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 145 | return 146 | } 147 | 148 | resp, err := workerService.HandleWorkflowStateWaitUntil(c.Request.Context(), req) 149 | if err != nil { 150 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 151 | return 152 | } 153 | c.JSON(http.StatusOK, resp) 154 | return 155 | } 156 | func apiV1WorkflowStateDecide(c *gin.Context) { 157 | var req iwfidl.WorkflowStateExecuteRequest 158 | if err := c.ShouldBindJSON(&req); err != nil { 159 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 160 | return 161 | } 162 | 163 | resp, err := workerService.HandleWorkflowStateExecute(c.Request.Context(), req) 164 | if err != nil { 165 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 166 | return 167 | } 168 | c.JSON(http.StatusOK, resp) 169 | return 170 | } 171 | 172 | func apiV1WorkflowWorkerRpc(c *gin.Context) { 173 | var req iwfidl.WorkflowWorkerRpcRequest 174 | if err := c.ShouldBindJSON(&req); err != nil { 175 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 176 | return 177 | } 178 | 179 | resp, err := workerService.HandleWorkflowWorkerRPC(c.Request.Context(), req) 180 | if err != nil { 181 | c.JSON(501, iwfidl.WorkerErrorResponse{ 182 | Detail: iwfidl.PtrString(err.Error()), 183 | ErrorType: iwfidl.PtrString("test-error-type"), 184 | }) 185 | return 186 | } 187 | c.JSON(http.StatusOK, resp) 188 | return 189 | } 190 | -------------------------------------------------------------------------------- /workflows/moneytransfer/workflow.go: -------------------------------------------------------------------------------- 1 | package moneytransfer 2 | 3 | import ( 4 | "fmt" 5 | "github.com/indeedeng/iwf-golang-samples/workflows/service" 6 | "github.com/indeedeng/iwf-golang-sdk/gen/iwfidl" 7 | "github.com/indeedeng/iwf-golang-sdk/iwf" 8 | "github.com/indeedeng/iwf-golang-sdk/iwf/ptr" 9 | ) 10 | 11 | func NewMoneyTransferWorkflow(svc service.MyService) iwf.ObjectWorkflow { 12 | 13 | return &MoneyTransferWorkflow{ 14 | svc: svc, 15 | } 16 | } 17 | 18 | type MoneyTransferWorkflow struct { 19 | iwf.WorkflowDefaults 20 | 21 | svc service.MyService 22 | } 23 | 24 | func (e MoneyTransferWorkflow) GetWorkflowStates() []iwf.StateDef { 25 | return []iwf.StateDef{ 26 | iwf.StartingStateDef(&checkBalanceState{svc: e.svc}), 27 | iwf.NonStartingStateDef(&createDebitMemoState{svc: e.svc}), 28 | iwf.NonStartingStateDef(&debitState{svc: e.svc}), 29 | iwf.NonStartingStateDef(&createCreditMemoState{svc: e.svc}), 30 | iwf.NonStartingStateDef(&creditState{svc: e.svc}), 31 | iwf.NonStartingStateDef(&compensateState{svc: e.svc}), 32 | } 33 | } 34 | 35 | type TransferRequest struct { 36 | FromAccount string 37 | ToAccount string 38 | Amount int 39 | Notes string 40 | } 41 | 42 | type checkBalanceState struct { 43 | iwf.WorkflowStateDefaultsNoWaitUntil 44 | svc service.MyService 45 | } 46 | 47 | func (i checkBalanceState) Execute( 48 | ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, 49 | communication iwf.Communication, 50 | ) (*iwf.StateDecision, error) { 51 | var request TransferRequest 52 | input.Get(&request) 53 | 54 | hasSufficientFunds := i.svc.CheckBalance(request.FromAccount, request.Amount) 55 | if !hasSufficientFunds { 56 | return iwf.ForceFailWorkflow("insufficient funds"), nil 57 | } 58 | 59 | return iwf.SingleNextState(&createDebitMemoState{}, request), nil 60 | } 61 | 62 | type createDebitMemoState struct { 63 | iwf.WorkflowStateDefaultsNoWaitUntil 64 | svc service.MyService 65 | } 66 | 67 | func (i createDebitMemoState) Execute( 68 | ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, 69 | communication iwf.Communication, 70 | ) (*iwf.StateDecision, error) { 71 | var request TransferRequest 72 | input.Get(&request) 73 | 74 | err := i.svc.CreateDebitMemo(request.FromAccount, request.Amount, request.Notes) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | // uncomment this to test error case 80 | //if true { 81 | // return nil, fmt.Errorf("test error for testing error handling") 82 | //} 83 | 84 | return iwf.SingleNextState(&debitState{}, request), nil 85 | } 86 | 87 | func (i createDebitMemoState) GetStateOptions() *iwf.StateOptions { 88 | return &iwf.StateOptions{ 89 | ExecuteApiRetryPolicy: &iwfidl.RetryPolicy{ 90 | MaximumAttemptsDurationSeconds: ptr.Any(int32(3600)), 91 | // uncomment this to test a short retry 92 | //MaximumAttemptsDurationSeconds: ptr.Any(int32(3)), 93 | }, 94 | ExecuteApiFailureProceedState: &compensateState{}, 95 | } 96 | } 97 | 98 | type debitState struct { 99 | iwf.WorkflowStateDefaultsNoWaitUntil 100 | svc service.MyService 101 | } 102 | 103 | func (i debitState) Execute( 104 | ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, 105 | communication iwf.Communication, 106 | ) (*iwf.StateDecision, error) { 107 | var request TransferRequest 108 | input.Get(&request) 109 | 110 | err := i.svc.Debit(request.FromAccount, request.Amount) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | return iwf.SingleNextState(&createCreditMemoState{}, request), nil 116 | } 117 | 118 | func (i debitState) GetStateOptions() *iwf.StateOptions { 119 | return &iwf.StateOptions{ 120 | ExecuteApiRetryPolicy: &iwfidl.RetryPolicy{ 121 | MaximumAttemptsDurationSeconds: ptr.Any(int32(3600)), 122 | }, 123 | ExecuteApiFailureProceedState: &compensateState{}, 124 | } 125 | } 126 | 127 | type createCreditMemoState struct { 128 | iwf.WorkflowStateDefaultsNoWaitUntil 129 | svc service.MyService 130 | } 131 | 132 | func (i createCreditMemoState) Execute( 133 | ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, 134 | communication iwf.Communication, 135 | ) (*iwf.StateDecision, error) { 136 | var request TransferRequest 137 | input.Get(&request) 138 | 139 | err := i.svc.CreateCreditMemo(request.ToAccount, request.Amount, request.Notes) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | return iwf.SingleNextState(&creditState{}, request), nil 145 | } 146 | 147 | func (i createCreditMemoState) GetStateOptions() *iwf.StateOptions { 148 | return &iwf.StateOptions{ 149 | ExecuteApiRetryPolicy: &iwfidl.RetryPolicy{ 150 | MaximumAttemptsDurationSeconds: ptr.Any(int32(3600)), 151 | }, 152 | ExecuteApiFailureProceedState: &compensateState{}, 153 | } 154 | } 155 | 156 | type creditState struct { 157 | iwf.WorkflowStateDefaultsNoWaitUntil 158 | svc service.MyService 159 | } 160 | 161 | func (i creditState) Execute( 162 | ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, 163 | communication iwf.Communication, 164 | ) (*iwf.StateDecision, error) { 165 | var request TransferRequest 166 | input.Get(&request) 167 | 168 | err := i.svc.Credit(request.ToAccount, request.Amount) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | return iwf.GracefulCompleteWorkflow(fmt.Sprintf("transfer is done from %v to %v for amount %v", request.FromAccount, request.ToAccount, request.Amount)), nil 174 | } 175 | 176 | func (i creditState) GetStateOptions() *iwf.StateOptions { 177 | return &iwf.StateOptions{ 178 | ExecuteApiRetryPolicy: &iwfidl.RetryPolicy{ 179 | MaximumAttemptsDurationSeconds: ptr.Any(int32(3600)), 180 | }, 181 | ExecuteApiFailureProceedState: &compensateState{}, 182 | } 183 | } 184 | 185 | type compensateState struct { 186 | iwf.WorkflowStateDefaultsNoWaitUntil 187 | svc service.MyService 188 | } 189 | 190 | func (i compensateState) Execute( 191 | ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, 192 | communication iwf.Communication, 193 | ) (*iwf.StateDecision, error) { 194 | // NOTE: to improve, we can use iWF data attributes to track whether each step has been attempted to execute 195 | // and check a flag to see if we should undo it or not 196 | 197 | var request TransferRequest 198 | input.Get(&request) 199 | 200 | err := i.svc.UndoCredit(request.ToAccount, request.Amount) 201 | if err != nil { 202 | return nil, err 203 | } 204 | err = i.svc.UndoCreateCreditMemo(request.ToAccount, request.Amount, request.Notes) 205 | if err != nil { 206 | return nil, err 207 | } 208 | err = i.svc.UndoCreateDebitMemo(request.FromAccount, request.Amount, request.Notes) 209 | if err != nil { 210 | return nil, err 211 | } 212 | err = i.svc.UndoDebit(request.FromAccount, request.Amount) 213 | if err != nil { 214 | return nil, err 215 | } 216 | 217 | return iwf.ForceFailWorkflow(fmt.Sprintf("transfer has failed: from %v to %v for amount %v", request.FromAccount, request.ToAccount, request.Amount)), nil 218 | } 219 | 220 | func (i compensateState) GetStateOptions() *iwf.StateOptions { 221 | return &iwf.StateOptions{ 222 | ExecuteApiRetryPolicy: &iwfidl.RetryPolicy{ 223 | MaximumAttemptsDurationSeconds: ptr.Any(int32(86400)), 224 | }, 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /workflows/subscription/workflow.go: -------------------------------------------------------------------------------- 1 | package subscription 2 | 3 | import ( 4 | "github.com/indeedeng/iwf-golang-samples/workflows/service" 5 | "github.com/indeedeng/iwf-golang-sdk/iwf" 6 | "time" 7 | ) 8 | 9 | type SubscriptionWorkflow struct { 10 | iwf.DefaultWorkflowType 11 | 12 | svc service.MyService 13 | } 14 | 15 | func NewSubscriptionWorkflow(svc service.MyService) iwf.ObjectWorkflow { 16 | return &SubscriptionWorkflow{ 17 | svc: svc, 18 | } 19 | } 20 | 21 | const ( 22 | keyBillingPeriodNum = "billingPeriodNum" 23 | keyCustomer = "customer" 24 | 25 | SignalCancelSubscription = "cancelSubscription" 26 | SignalUpdateBillingPeriodChargeAmount = "updateBillingPeriodChargeAmount" 27 | ) 28 | 29 | func (b SubscriptionWorkflow) GetWorkflowStates() []iwf.StateDef { 30 | return []iwf.StateDef{ 31 | iwf.StartingStateDef(NewInitState()), 32 | iwf.NonStartingStateDef(NewTrialState(b.svc)), 33 | iwf.NonStartingStateDef(NewChargeCurrentBillState(b.svc)), 34 | iwf.NonStartingStateDef(NewCancelState(b.svc)), 35 | iwf.NonStartingStateDef(NewUpdateChargeAmountState()), 36 | } 37 | } 38 | 39 | func (b SubscriptionWorkflow) GetPersistenceSchema() []iwf.PersistenceFieldDef { 40 | return []iwf.PersistenceFieldDef{ 41 | iwf.DataAttributeDef(keyBillingPeriodNum), 42 | iwf.DataAttributeDef(keyCustomer), 43 | } 44 | } 45 | 46 | func (b SubscriptionWorkflow) GetCommunicationSchema() []iwf.CommunicationMethodDef { 47 | return []iwf.CommunicationMethodDef{ 48 | iwf.SignalChannelDef(SignalCancelSubscription), 49 | iwf.SignalChannelDef(SignalUpdateBillingPeriodChargeAmount), 50 | iwf.RPCMethodDef(b.Describe, nil), 51 | } 52 | } 53 | 54 | func (b SubscriptionWorkflow) Describe(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (interface{}, error) { 55 | var customer Customer 56 | persistence.GetDataAttribute(keyCustomer, &customer) 57 | return customer.Subscription, nil 58 | } 59 | 60 | type Subscription struct { 61 | TrialPeriod time.Duration 62 | BillingPeriod time.Duration 63 | MaxBillingPeriods int 64 | BillingPeriodCharge int 65 | } 66 | 67 | type Customer struct { 68 | FirstName string 69 | LastName string 70 | Id string 71 | Email string 72 | Subscription Subscription 73 | } 74 | 75 | func NewInitState() iwf.WorkflowState { 76 | return initState{} 77 | } 78 | 79 | type initState struct { 80 | iwf.WorkflowStateDefaults 81 | } 82 | 83 | func (b initState) WaitUntil(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (*iwf.CommandRequest, error) { 84 | var customer Customer 85 | input.Get(&customer) 86 | persistence.SetDataAttribute(keyCustomer, customer) 87 | return iwf.EmptyCommandRequest(), nil 88 | } 89 | 90 | func (b initState) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 91 | return iwf.MultiNextStates(trialState{}, cancelState{}, updateChargeAmountState{}), nil 92 | } 93 | 94 | func NewTrialState(svc service.MyService) iwf.WorkflowState { 95 | return trialState{ 96 | svc: svc, 97 | } 98 | } 99 | 100 | type trialState struct { 101 | iwf.WorkflowStateDefaults 102 | svc service.MyService 103 | } 104 | 105 | func (b trialState) WaitUntil(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (*iwf.CommandRequest, error) { 106 | var customer Customer 107 | persistence.GetDataAttribute(keyCustomer, &customer) 108 | 109 | // send welcome email 110 | b.svc.SendEmail(customer.Email, "welcome email", "hello content") 111 | 112 | return iwf.AllCommandsCompletedRequest( 113 | iwf.NewTimerCommand("", time.Now().Add(customer.Subscription.TrialPeriod)), 114 | ), nil 115 | } 116 | 117 | func (b trialState) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 118 | persistence.SetDataAttribute(keyBillingPeriodNum, 0) 119 | return iwf.SingleNextState(chargeCurrentBillState{}, nil), nil 120 | } 121 | 122 | func NewChargeCurrentBillState(svc service.MyService) iwf.WorkflowState { 123 | return chargeCurrentBillState{ 124 | svc: svc, 125 | } 126 | } 127 | 128 | type chargeCurrentBillState struct { 129 | iwf.WorkflowStateDefaults 130 | svc service.MyService 131 | } 132 | 133 | const subscriptionOverKey = "subscriptionOver" 134 | 135 | func (b chargeCurrentBillState) WaitUntil(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (*iwf.CommandRequest, error) { 136 | var customer Customer 137 | persistence.GetDataAttribute(keyCustomer, &customer) 138 | 139 | var periodNum int 140 | persistence.GetDataAttribute(keyBillingPeriodNum, &periodNum) 141 | 142 | if periodNum >= customer.Subscription.MaxBillingPeriods { 143 | persistence.SetStateExecutionLocal(subscriptionOverKey, true) 144 | return iwf.EmptyCommandRequest(), nil 145 | } 146 | 147 | persistence.SetDataAttribute(keyBillingPeriodNum, periodNum+1) 148 | 149 | return iwf.AllCommandsCompletedRequest( 150 | iwf.NewTimerCommand("", time.Now().Add(customer.Subscription.BillingPeriod)), 151 | ), nil 152 | } 153 | 154 | func (b chargeCurrentBillState) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 155 | var customer Customer 156 | persistence.GetDataAttribute(keyCustomer, &customer) 157 | 158 | var subscriptionOver bool 159 | persistence.GetStateExecutionLocal(subscriptionOverKey, &subscriptionOver) 160 | if subscriptionOver { 161 | b.svc.SendEmail(customer.Email, "subscription over", "hello content") 162 | // use force completing because the cancel state is still waiting for signal 163 | return iwf.ForceCompletingWorkflow, nil 164 | } 165 | 166 | b.svc.ChargeUser(customer.Email, customer.Id, customer.Subscription.BillingPeriodCharge) 167 | 168 | return iwf.SingleNextState(chargeCurrentBillState{}, nil), nil 169 | } 170 | 171 | func NewCancelState(svc service.MyService) iwf.WorkflowState { 172 | return cancelState{ 173 | svc: svc, 174 | } 175 | } 176 | 177 | type cancelState struct { 178 | iwf.WorkflowStateDefaults 179 | svc service.MyService 180 | } 181 | 182 | func (b cancelState) WaitUntil(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (*iwf.CommandRequest, error) { 183 | return iwf.AllCommandsCompletedRequest( 184 | iwf.NewSignalCommand("", SignalCancelSubscription), 185 | ), nil 186 | } 187 | 188 | func (b cancelState) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 189 | var customer Customer 190 | persistence.GetDataAttribute(keyCustomer, &customer) 191 | 192 | b.svc.SendEmail(customer.Email, "subscription canceled", "hello content") 193 | return iwf.ForceCompletingWorkflow, nil 194 | } 195 | 196 | func NewUpdateChargeAmountState() iwf.WorkflowState { 197 | return updateChargeAmountState{} 198 | } 199 | 200 | type updateChargeAmountState struct { 201 | iwf.WorkflowStateDefaults 202 | } 203 | 204 | func (b updateChargeAmountState) WaitUntil(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (*iwf.CommandRequest, error) { 205 | return iwf.AllCommandsCompletedRequest( 206 | iwf.NewSignalCommand("", SignalUpdateBillingPeriodChargeAmount), 207 | ), nil 208 | } 209 | 210 | func (b updateChargeAmountState) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 211 | var customer Customer 212 | persistence.GetDataAttribute(keyCustomer, &customer) 213 | 214 | var newAmount int 215 | commandResults.GetSignalCommandResultByChannel(SignalUpdateBillingPeriodChargeAmount).SignalValue.Get(&newAmount) 216 | 217 | customer.Subscription.BillingPeriodCharge = newAmount 218 | persistence.SetDataAttribute(keyCustomer, customer) 219 | 220 | return iwf.SingleNextState(updateChargeAmountState{}, nil), nil 221 | } 222 | -------------------------------------------------------------------------------- /workflows/subscription/workflow_test.go: -------------------------------------------------------------------------------- 1 | package subscription 2 | 3 | import ( 4 | "github.com/golang/mock/gomock" 5 | "github.com/indeedeng/iwf-golang-samples/workflows/service" 6 | "github.com/indeedeng/iwf-golang-sdk/gen/iwfidl" 7 | "github.com/indeedeng/iwf-golang-sdk/iwf" 8 | "github.com/indeedeng/iwf-golang-sdk/iwftest" 9 | "github.com/stretchr/testify/assert" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | // mockgen -source=workflows/subscription/my_service.go -destination=workflows/subscription/my_service_mock.go --package=subscription 15 | 16 | var testCustomer = Customer{ 17 | FirstName: "Quanzheng", 18 | LastName: "Long", 19 | Id: "123", 20 | Email: "qlong.seattle@gmail.com", 21 | Subscription: Subscription{ 22 | BillingPeriod: time.Second, 23 | MaxBillingPeriods: 10, 24 | TrialPeriod: time.Second * 2, 25 | BillingPeriodCharge: 100, 26 | }, 27 | } 28 | 29 | var testCustomerObj = iwftest.NewTestObject(testCustomer) 30 | 31 | var mockWfCtx *iwftest.MockWorkflowContext 32 | var mockPersistence *iwftest.MockPersistence 33 | var mockCommunication *iwftest.MockCommunication 34 | var emptyCmdResults = iwf.CommandResults{} 35 | var emptyObj = iwftest.NewTestObject(nil) 36 | var mockSvc *service.MockMyService 37 | 38 | func beforeEach(t *testing.T) { 39 | ctrl := gomock.NewController(t) 40 | 41 | mockSvc = service.NewMockMyService(ctrl) 42 | mockWfCtx = iwftest.NewMockWorkflowContext(ctrl) 43 | mockPersistence = iwftest.NewMockPersistence(ctrl) 44 | mockCommunication = iwftest.NewMockCommunication(ctrl) 45 | } 46 | 47 | func TestInitState_WaitUntil(t *testing.T) { 48 | beforeEach(t) 49 | 50 | state := NewInitState() 51 | 52 | mockPersistence.EXPECT().SetDataAttribute(keyCustomer, testCustomer) 53 | cmdReq, err := state.WaitUntil(mockWfCtx, testCustomerObj, mockPersistence, mockCommunication) 54 | assert.Nil(t, err) 55 | assert.Equal(t, iwf.EmptyCommandRequest(), cmdReq) 56 | } 57 | 58 | func TestInitState_Execute(t *testing.T) { 59 | beforeEach(t) 60 | 61 | state := NewInitState() 62 | input := iwftest.NewTestObject(testCustomer) 63 | 64 | decision, err := state.Execute(mockWfCtx, input, emptyCmdResults, mockPersistence, mockCommunication) 65 | assert.Nil(t, err) 66 | assert.Equal(t, iwf.MultiNextStates( 67 | trialState{}, cancelState{}, updateChargeAmountState{}, 68 | ), decision) 69 | } 70 | 71 | func TestTrialState_WaitUntil(t *testing.T) { 72 | beforeEach(t) 73 | 74 | state := NewTrialState(mockSvc) 75 | 76 | mockSvc.EXPECT().SendEmail(testCustomer.Email, gomock.Any(), gomock.Any()) 77 | mockPersistence.EXPECT().GetDataAttribute(keyCustomer, gomock.Any()).SetArg(1, testCustomer) 78 | cmdReq, err := state.WaitUntil(mockWfCtx, emptyObj, mockPersistence, mockCommunication) 79 | assert.Nil(t, err) 80 | firingTime := cmdReq.Commands[0].TimerCommand.FiringUnixTimestampSeconds 81 | assert.Equal(t, iwf.AllCommandsCompletedRequest( 82 | iwf.NewTimerCommand("", time.Unix(firingTime, 0)), 83 | ), cmdReq) 84 | } 85 | 86 | func TestTrialState_Execute(t *testing.T) { 87 | beforeEach(t) 88 | 89 | state := NewTrialState(mockSvc) 90 | 91 | mockPersistence.EXPECT().SetDataAttribute(keyBillingPeriodNum, 0) 92 | 93 | decision, err := state.Execute(mockWfCtx, emptyObj, emptyCmdResults, mockPersistence, mockCommunication) 94 | assert.Nil(t, err) 95 | assert.Equal(t, iwf.SingleNextState( 96 | chargeCurrentBillState{}, nil, 97 | ), decision) 98 | } 99 | 100 | func TestChargeCurrentBillStateStart_waitForDuration(t *testing.T) { 101 | beforeEach(t) 102 | 103 | state := NewChargeCurrentBillState(mockSvc) 104 | 105 | mockPersistence.EXPECT().GetDataAttribute(keyCustomer, gomock.Any()).SetArg(1, testCustomer) 106 | mockPersistence.EXPECT().GetDataAttribute(keyBillingPeriodNum, gomock.Any()).SetArg(1, 0) 107 | mockPersistence.EXPECT().SetDataAttribute(keyBillingPeriodNum, 1) 108 | 109 | cmdReq, err := state.WaitUntil(mockWfCtx, emptyObj, mockPersistence, mockCommunication) 110 | assert.Nil(t, err) 111 | cmd := cmdReq.Commands[0] 112 | assert.Equal(t, iwf.AllCommandsCompletedRequest(iwf.NewTimerCommand("", time.Unix(cmd.TimerCommand.FiringUnixTimestampSeconds, 0))), cmdReq) 113 | } 114 | 115 | func TestChargeCurrentBillStateStart_subscriptionOver(t *testing.T) { 116 | beforeEach(t) 117 | 118 | state := NewChargeCurrentBillState(mockSvc) 119 | 120 | mockPersistence.EXPECT().GetDataAttribute(keyCustomer, gomock.Any()).SetArg(1, testCustomer) 121 | mockPersistence.EXPECT().GetDataAttribute(keyBillingPeriodNum, gomock.Any()).SetArg(1, testCustomer.Subscription.MaxBillingPeriods) 122 | mockPersistence.EXPECT().SetStateExecutionLocal(subscriptionOverKey, true) 123 | 124 | cmdReq, err := state.WaitUntil(mockWfCtx, emptyObj, mockPersistence, mockCommunication) 125 | assert.Nil(t, err) 126 | assert.Equal(t, iwf.EmptyCommandRequest(), cmdReq) 127 | } 128 | 129 | func TestChargeCurrentBillStateDecide_subscriptionNotOver(t *testing.T) { 130 | beforeEach(t) 131 | 132 | state := NewChargeCurrentBillState(mockSvc) 133 | 134 | mockPersistence.EXPECT().GetDataAttribute(keyCustomer, gomock.Any()).SetArg(1, testCustomer) 135 | mockPersistence.EXPECT().GetStateExecutionLocal(subscriptionOverKey, gomock.Any()) 136 | mockSvc.EXPECT().ChargeUser(testCustomer.Email, testCustomer.Id, testCustomer.Subscription.BillingPeriodCharge) 137 | 138 | decision, err := state.Execute(mockWfCtx, emptyObj, emptyCmdResults, mockPersistence, mockCommunication) 139 | assert.Nil(t, err) 140 | assert.Equal(t, iwf.SingleNextState(&chargeCurrentBillState{}, nil), decision) 141 | } 142 | 143 | func TestChargeCurrentBillStateDecide_subscriptionOver(t *testing.T) { 144 | beforeEach(t) 145 | 146 | state := NewChargeCurrentBillState(mockSvc) 147 | 148 | mockPersistence.EXPECT().GetDataAttribute(keyCustomer, gomock.Any()).SetArg(1, testCustomer) 149 | mockPersistence.EXPECT().GetStateExecutionLocal(subscriptionOverKey, gomock.Any()).SetArg(1, true) 150 | mockSvc.EXPECT().SendEmail(testCustomer.Email, gomock.Any(), gomock.Any()) 151 | 152 | decision, err := state.Execute(mockWfCtx, emptyObj, emptyCmdResults, mockPersistence, mockCommunication) 153 | assert.Nil(t, err) 154 | assert.Equal(t, iwf.ForceCompletingWorkflow, decision) 155 | } 156 | 157 | func TestUpdateChargeAmountState_WaitUntil(t *testing.T) { 158 | beforeEach(t) 159 | 160 | state := NewUpdateChargeAmountState() 161 | 162 | cmdReq, err := state.WaitUntil(mockWfCtx, emptyObj, mockPersistence, mockCommunication) 163 | assert.Nil(t, err) 164 | assert.Equal(t, iwf.AllCommandsCompletedRequest(iwf.NewSignalCommand("", SignalUpdateBillingPeriodChargeAmount)), cmdReq) 165 | } 166 | 167 | func TestUpdateChargeAmountState_Execute(t *testing.T) { 168 | beforeEach(t) 169 | 170 | state := NewUpdateChargeAmountState() 171 | 172 | cmdResults := iwf.CommandResults{ 173 | Signals: []iwf.SignalCommandResult{ 174 | { 175 | ChannelName: SignalUpdateBillingPeriodChargeAmount, 176 | SignalValue: iwftest.NewTestObject(200), 177 | Status: iwfidl.RECEIVED, 178 | }, 179 | }, 180 | } 181 | 182 | updatedCustomer := testCustomer 183 | updatedCustomer.Subscription.BillingPeriodCharge = 200 184 | 185 | mockPersistence.EXPECT().GetDataAttribute(keyCustomer, gomock.Any()).SetArg(1, testCustomer) 186 | mockPersistence.EXPECT().SetDataAttribute(keyCustomer, updatedCustomer) 187 | 188 | decision, err := state.Execute(mockWfCtx, emptyObj, cmdResults, mockPersistence, mockCommunication) 189 | assert.Nil(t, err) 190 | assert.Equal(t, iwf.SingleNextState(&updateChargeAmountState{}, nil), decision) 191 | } 192 | 193 | func TestCancelState_WaitUntil(t *testing.T) { 194 | beforeEach(t) 195 | 196 | state := NewCancelState(mockSvc) 197 | 198 | cmdReq, err := state.WaitUntil(mockWfCtx, emptyObj, mockPersistence, mockCommunication) 199 | assert.Nil(t, err) 200 | assert.Equal(t, iwf.AllCommandsCompletedRequest(iwf.NewSignalCommand("", SignalCancelSubscription)), cmdReq) 201 | } 202 | 203 | func TestCancelState_Execute(t *testing.T) { 204 | beforeEach(t) 205 | 206 | state := NewCancelState(mockSvc) 207 | 208 | mockPersistence.EXPECT().GetDataAttribute(keyCustomer, gomock.Any()).SetArg(1, testCustomer) 209 | mockSvc.EXPECT().SendEmail(testCustomer.Email, gomock.Any(), gomock.Any()) 210 | 211 | decision, err := state.Execute(mockWfCtx, emptyObj, emptyCmdResults, mockPersistence, mockCommunication) 212 | assert.Nil(t, err) 213 | assert.Equal(t, iwf.ForceCompletingWorkflow, decision) 214 | } 215 | -------------------------------------------------------------------------------- /workflows/engagement/workflow.go: -------------------------------------------------------------------------------- 1 | package engagement 2 | 3 | import ( 4 | "fmt" 5 | "github.com/indeedeng/iwf-golang-samples/workflows/service" 6 | "github.com/indeedeng/iwf-golang-sdk/gen/iwfidl" 7 | "github.com/indeedeng/iwf-golang-sdk/iwf" 8 | "time" 9 | ) 10 | 11 | func NewEngagementWorkflow(svc service.MyService) iwf.ObjectWorkflow { 12 | 13 | return &EngagementWorkflow{ 14 | svc: svc, 15 | } 16 | } 17 | 18 | type EngagementWorkflow struct { 19 | iwf.DefaultWorkflowType 20 | 21 | svc service.MyService 22 | } 23 | 24 | func (e EngagementWorkflow) GetWorkflowStates() []iwf.StateDef { 25 | return []iwf.StateDef{ 26 | iwf.StartingStateDef(NewInitState()), 27 | iwf.NonStartingStateDef(NewProcessTimoutState(e.svc)), 28 | iwf.NonStartingStateDef(NewReminderState(e.svc)), 29 | iwf.NonStartingStateDef(NewNotifyExternalSystemState(e.svc)), 30 | } 31 | } 32 | 33 | func (e EngagementWorkflow) GetPersistenceSchema() []iwf.PersistenceFieldDef { 34 | return []iwf.PersistenceFieldDef{ 35 | iwf.SearchAttributeDef(keyEmployerId, iwfidl.KEYWORD), 36 | iwf.SearchAttributeDef(keyJobSeekerId, iwfidl.KEYWORD), 37 | iwf.SearchAttributeDef(keyStatus, iwfidl.KEYWORD), 38 | iwf.SearchAttributeDef(keyLastUpdateTimestamp, iwfidl.INT), 39 | 40 | iwf.DataAttributeDef(keyNotes), 41 | } 42 | } 43 | 44 | func (e EngagementWorkflow) GetCommunicationSchema() []iwf.CommunicationMethodDef { 45 | return []iwf.CommunicationMethodDef{ 46 | iwf.SignalChannelDef(SignalChannelOptOutReminder), 47 | iwf.InternalChannelDef(InternalChannelCompleteProcess), 48 | 49 | iwf.RPCMethodDef(e.Describe, nil), 50 | iwf.RPCMethodDef(e.Decline, nil), 51 | iwf.RPCMethodDef(e.Accept, nil), 52 | } 53 | } 54 | 55 | const ( 56 | keyEmployerId = "EmployerId" 57 | keyJobSeekerId = "JobSeekerId" 58 | keyStatus = "EngagementStatus" 59 | keyLastUpdateTimestamp = "LastUpdateTimeMillis" 60 | keyNotes = "notes" 61 | 62 | SignalChannelOptOutReminder = "OptOutReminder" 63 | InternalChannelCompleteProcess = "CompleteProcess" 64 | ) 65 | 66 | func (e EngagementWorkflow) Describe(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (interface{}, error) { 67 | 68 | status := persistence.GetSearchAttributeKeyword(keyStatus) 69 | employerId := persistence.GetSearchAttributeKeyword(keyEmployerId) 70 | jobSeekerId := persistence.GetSearchAttributeKeyword(keyJobSeekerId) 71 | var notes string 72 | persistence.GetDataAttribute(keyNotes, ¬es) 73 | 74 | return EngagementDescription{ 75 | EmployerId: employerId, 76 | JobSeekerId: jobSeekerId, 77 | Notes: notes, 78 | CurrentStatus: Status(status), 79 | }, nil 80 | } 81 | 82 | func (e EngagementWorkflow) Decline(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (interface{}, error) { 83 | 84 | status := Status(persistence.GetSearchAttributeKeyword(keyStatus)) 85 | if status != StatusInitiated { 86 | return nil, fmt.Errorf("can only decline in INITIATED status, current is %v", status) 87 | } 88 | 89 | persistence.SetSearchAttributeKeyword(keyStatus, string(StatusDeclined)) 90 | persistence.SetSearchAttributeInt(keyLastUpdateTimestamp, time.Now().Unix()) 91 | communication.TriggerStateMovements(iwf.NewStateMovement(notifyExternalSystemState{}, string(StatusDeclined))) 92 | 93 | var notes string 94 | input.Get(¬es) 95 | 96 | var currentNotes string 97 | persistence.GetDataAttribute(keyNotes, ¤tNotes) 98 | persistence.SetDataAttribute(keyNotes, currentNotes+";"+notes) 99 | return nil, nil 100 | } 101 | 102 | func (e EngagementWorkflow) Accept(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (interface{}, error) { 103 | 104 | status := Status(persistence.GetSearchAttributeKeyword(keyStatus)) 105 | if status != StatusInitiated && status != StatusDeclined { 106 | return nil, fmt.Errorf("can only decline in INITIATED or DECLINED status, current is %v", status) 107 | } 108 | 109 | persistence.SetSearchAttributeKeyword(keyStatus, string(StatusAccepted)) 110 | persistence.SetSearchAttributeInt(keyLastUpdateTimestamp, time.Now().Unix()) 111 | communication.TriggerStateMovements(iwf.NewStateMovement(notifyExternalSystemState{}, string(StatusAccepted))) 112 | 113 | var notes string 114 | input.Get(¬es) 115 | 116 | var currentNotes string 117 | persistence.GetDataAttribute(keyNotes, ¤tNotes) 118 | persistence.SetDataAttribute(keyNotes, currentNotes+";"+notes) 119 | return nil, nil 120 | } 121 | 122 | func NewInitState() iwf.WorkflowState { 123 | return initState{} 124 | } 125 | 126 | type initState struct { 127 | iwf.WorkflowStateDefaultsNoWaitUntil 128 | } 129 | 130 | func (i initState) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 131 | var engInput EngagementInput 132 | input.Get(&engInput) 133 | 134 | persistence.SetSearchAttributeKeyword(keyEmployerId, engInput.EmployerId) 135 | persistence.SetSearchAttributeKeyword(keyJobSeekerId, engInput.JobSeekerId) 136 | persistence.SetSearchAttributeKeyword(keyStatus, string(StatusInitiated)) 137 | 138 | persistence.SetDataAttribute(keyNotes, engInput.Notes) 139 | return iwf.MultiNextStatesWithInput( 140 | iwf.NewStateMovement(processTimoutState{}, nil), 141 | iwf.NewStateMovement(reminderState{}, nil), 142 | iwf.NewStateMovement(notifyExternalSystemState{}, StatusInitiated), 143 | ), nil 144 | } 145 | 146 | func NewProcessTimoutState(svc service.MyService) iwf.WorkflowState { 147 | return processTimoutState{ 148 | svc: svc, 149 | } 150 | } 151 | 152 | type processTimoutState struct { 153 | iwf.WorkflowStateDefaults 154 | svc service.MyService 155 | } 156 | 157 | func (p processTimoutState) WaitUntil(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (*iwf.CommandRequest, error) { 158 | return iwf.AnyCommandCompletedRequest( 159 | iwf.NewTimerCommand("", time.Now().Add(time.Hour*24*60)), // ~ 2 months 160 | iwf.NewInternalChannelCommand("", InternalChannelCompleteProcess), 161 | ), nil 162 | } 163 | 164 | func (p processTimoutState) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 165 | status := persistence.GetSearchAttributeKeyword(keyStatus) 166 | employerId := persistence.GetSearchAttributeKeyword(keyEmployerId) 167 | jobSeekerId := persistence.GetSearchAttributeKeyword(keyJobSeekerId) 168 | updateStatus := "timeout" 169 | if status == string(StatusAccepted) { 170 | updateStatus = "done" 171 | } 172 | p.svc.UpdateExternalSystem(fmt.Sprintf("notify engagement from employer %v, jobSeeker %v for status %v", employerId, jobSeekerId, status)) 173 | return iwf.GracefulCompleteWorkflow(updateStatus), nil 174 | } 175 | 176 | func NewReminderState(svc service.MyService) iwf.WorkflowState { 177 | return reminderState{ 178 | svc: svc, 179 | } 180 | } 181 | 182 | type reminderState struct { 183 | iwf.WorkflowStateDefaults 184 | svc service.MyService 185 | } 186 | 187 | func (r reminderState) WaitUntil(ctx iwf.WorkflowContext, input iwf.Object, persistence iwf.Persistence, communication iwf.Communication) (*iwf.CommandRequest, error) { 188 | return iwf.AnyCommandCompletedRequest( 189 | iwf.NewTimerCommand("", time.Now().Add(time.Second*5)), // use 5 seconds for demo, should be 24 hours in real world 190 | iwf.NewSignalCommand("", SignalChannelOptOutReminder), 191 | ), nil 192 | } 193 | 194 | func (r reminderState) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 195 | status := persistence.GetSearchAttributeKeyword(keyStatus) 196 | if status != string(StatusInitiated) { 197 | return iwf.DeadEnd, nil 198 | } 199 | optoutSignalCommandResult := commandResults.Signals[0] 200 | if optoutSignalCommandResult.Status == iwfidl.RECEIVED { 201 | var currentNotes string 202 | persistence.GetDataAttribute(keyNotes, ¤tNotes) 203 | persistence.SetDataAttribute(keyNotes, currentNotes+";"+"User optout reminder") 204 | 205 | return iwf.DeadEnd, nil 206 | } 207 | 208 | jobSeekerId := persistence.GetSearchAttributeKeyword(keyJobSeekerId) 209 | r.svc.SendEmail(jobSeekerId, "Reminder:xxx please respond", "Hello xxx, ...") 210 | return iwf.SingleNextState(reminderState{}, nil), nil 211 | } 212 | 213 | func NewNotifyExternalSystemState(svc service.MyService) iwf.WorkflowState { 214 | return notifyExternalSystemState{ 215 | svc: svc, 216 | } 217 | } 218 | 219 | type notifyExternalSystemState struct { 220 | iwf.WorkflowStateDefaultsNoWaitUntil 221 | svc service.MyService 222 | } 223 | 224 | func (n notifyExternalSystemState) Execute(ctx iwf.WorkflowContext, input iwf.Object, commandResults iwf.CommandResults, persistence iwf.Persistence, communication iwf.Communication) (*iwf.StateDecision, error) { 225 | var status Status 226 | input.Get(&status) 227 | 228 | jobSeekerId := persistence.GetSearchAttributeKeyword(keyJobSeekerId) 229 | employerId := persistence.GetSearchAttributeKeyword(keyEmployerId) 230 | n.svc.UpdateExternalSystem(fmt.Sprintf("notify engagement from employerId %v to jobSeekerId %v for status %v ", employerId, jobSeekerId, status)) 231 | return iwf.DeadEnd, nil 232 | } 233 | 234 | // GetStateOptions customize the state options 235 | // By default, all state execution will retry infinitely (until workflow timeout). 236 | // This may not work for some dependency as we may want to retry for only a certain times 237 | func (n notifyExternalSystemState) GetStateOptions() *iwf.StateOptions { 238 | return &iwf.StateOptions{ 239 | ExecuteApiRetryPolicy: &iwfidl.RetryPolicy{ 240 | BackoffCoefficient: iwfidl.PtrFloat32(2), 241 | MaximumAttempts: iwfidl.PtrInt32(100), 242 | MaximumAttemptsDurationSeconds: iwfidl.PtrInt32(3600), 243 | MaximumIntervalSeconds: iwfidl.PtrInt32(60), 244 | InitialIntervalSeconds: iwfidl.PtrInt32(3), 245 | }, 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /workflows/service/my_service_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: workflows/service/my_service.go 3 | 4 | // Package service is a generated GoMock package. 5 | package service 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockMyService is a mock of MyService interface. 14 | type MockMyService struct { 15 | ctrl *gomock.Controller 16 | recorder *MockMyServiceMockRecorder 17 | } 18 | 19 | // MockMyServiceMockRecorder is the mock recorder for MockMyService. 20 | type MockMyServiceMockRecorder struct { 21 | mock *MockMyService 22 | } 23 | 24 | // NewMockMyService creates a new mock instance. 25 | func NewMockMyService(ctrl *gomock.Controller) *MockMyService { 26 | mock := &MockMyService{ctrl: ctrl} 27 | mock.recorder = &MockMyServiceMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockMyService) EXPECT() *MockMyServiceMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // CallAPI1 mocks base method. 37 | func (m *MockMyService) CallAPI1(data string) { 38 | m.ctrl.T.Helper() 39 | m.ctrl.Call(m, "CallAPI1", data) 40 | } 41 | 42 | // CallAPI1 indicates an expected call of CallAPI1. 43 | func (mr *MockMyServiceMockRecorder) CallAPI1(data interface{}) *gomock.Call { 44 | mr.mock.ctrl.T.Helper() 45 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallAPI1", reflect.TypeOf((*MockMyService)(nil).CallAPI1), data) 46 | } 47 | 48 | // CallAPI2 mocks base method. 49 | func (m *MockMyService) CallAPI2(data string) { 50 | m.ctrl.T.Helper() 51 | m.ctrl.Call(m, "CallAPI2", data) 52 | } 53 | 54 | // CallAPI2 indicates an expected call of CallAPI2. 55 | func (mr *MockMyServiceMockRecorder) CallAPI2(data interface{}) *gomock.Call { 56 | mr.mock.ctrl.T.Helper() 57 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallAPI2", reflect.TypeOf((*MockMyService)(nil).CallAPI2), data) 58 | } 59 | 60 | // CallAPI3 mocks base method. 61 | func (m *MockMyService) CallAPI3(data string) { 62 | m.ctrl.T.Helper() 63 | m.ctrl.Call(m, "CallAPI3", data) 64 | } 65 | 66 | // CallAPI3 indicates an expected call of CallAPI3. 67 | func (mr *MockMyServiceMockRecorder) CallAPI3(data interface{}) *gomock.Call { 68 | mr.mock.ctrl.T.Helper() 69 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallAPI3", reflect.TypeOf((*MockMyService)(nil).CallAPI3), data) 70 | } 71 | 72 | // CallAPI4 mocks base method. 73 | func (m *MockMyService) CallAPI4(data string) { 74 | m.ctrl.T.Helper() 75 | m.ctrl.Call(m, "CallAPI4", data) 76 | } 77 | 78 | // CallAPI4 indicates an expected call of CallAPI4. 79 | func (mr *MockMyServiceMockRecorder) CallAPI4(data interface{}) *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallAPI4", reflect.TypeOf((*MockMyService)(nil).CallAPI4), data) 82 | } 83 | 84 | // ChargeUser mocks base method. 85 | func (m *MockMyService) ChargeUser(email, customerId string, amount int) { 86 | m.ctrl.T.Helper() 87 | m.ctrl.Call(m, "ChargeUser", email, customerId, amount) 88 | } 89 | 90 | // ChargeUser indicates an expected call of ChargeUser. 91 | func (mr *MockMyServiceMockRecorder) ChargeUser(email, customerId, amount interface{}) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChargeUser", reflect.TypeOf((*MockMyService)(nil).ChargeUser), email, customerId, amount) 94 | } 95 | 96 | // CheckBalance mocks base method. 97 | func (m *MockMyService) CheckBalance(account string, amount int) bool { 98 | m.ctrl.T.Helper() 99 | ret := m.ctrl.Call(m, "CheckBalance", account, amount) 100 | ret0, _ := ret[0].(bool) 101 | return ret0 102 | } 103 | 104 | // CheckBalance indicates an expected call of CheckBalance. 105 | func (mr *MockMyServiceMockRecorder) CheckBalance(account, amount interface{}) *gomock.Call { 106 | mr.mock.ctrl.T.Helper() 107 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckBalance", reflect.TypeOf((*MockMyService)(nil).CheckBalance), account, amount) 108 | } 109 | 110 | // CreateCreditMemo mocks base method. 111 | func (m *MockMyService) CreateCreditMemo(account string, amount int, notes string) error { 112 | m.ctrl.T.Helper() 113 | ret := m.ctrl.Call(m, "CreateCreditMemo", account, amount, notes) 114 | ret0, _ := ret[0].(error) 115 | return ret0 116 | } 117 | 118 | // CreateCreditMemo indicates an expected call of CreateCreditMemo. 119 | func (mr *MockMyServiceMockRecorder) CreateCreditMemo(account, amount, notes interface{}) *gomock.Call { 120 | mr.mock.ctrl.T.Helper() 121 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCreditMemo", reflect.TypeOf((*MockMyService)(nil).CreateCreditMemo), account, amount, notes) 122 | } 123 | 124 | // CreateDebitMemo mocks base method. 125 | func (m *MockMyService) CreateDebitMemo(account string, amount int, notes string) error { 126 | m.ctrl.T.Helper() 127 | ret := m.ctrl.Call(m, "CreateDebitMemo", account, amount, notes) 128 | ret0, _ := ret[0].(error) 129 | return ret0 130 | } 131 | 132 | // CreateDebitMemo indicates an expected call of CreateDebitMemo. 133 | func (mr *MockMyServiceMockRecorder) CreateDebitMemo(account, amount, notes interface{}) *gomock.Call { 134 | mr.mock.ctrl.T.Helper() 135 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDebitMemo", reflect.TypeOf((*MockMyService)(nil).CreateDebitMemo), account, amount, notes) 136 | } 137 | 138 | // Credit mocks base method. 139 | func (m *MockMyService) Credit(account string, amount int) error { 140 | m.ctrl.T.Helper() 141 | ret := m.ctrl.Call(m, "Credit", account, amount) 142 | ret0, _ := ret[0].(error) 143 | return ret0 144 | } 145 | 146 | // Credit indicates an expected call of Credit. 147 | func (mr *MockMyServiceMockRecorder) Credit(account, amount interface{}) *gomock.Call { 148 | mr.mock.ctrl.T.Helper() 149 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Credit", reflect.TypeOf((*MockMyService)(nil).Credit), account, amount) 150 | } 151 | 152 | // Debit mocks base method. 153 | func (m *MockMyService) Debit(account string, amount int) error { 154 | m.ctrl.T.Helper() 155 | ret := m.ctrl.Call(m, "Debit", account, amount) 156 | ret0, _ := ret[0].(error) 157 | return ret0 158 | } 159 | 160 | // Debit indicates an expected call of Debit. 161 | func (mr *MockMyServiceMockRecorder) Debit(account, amount interface{}) *gomock.Call { 162 | mr.mock.ctrl.T.Helper() 163 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debit", reflect.TypeOf((*MockMyService)(nil).Debit), account, amount) 164 | } 165 | 166 | // SendEmail mocks base method. 167 | func (m *MockMyService) SendEmail(recipient, subject, content string) { 168 | m.ctrl.T.Helper() 169 | m.ctrl.Call(m, "SendEmail", recipient, subject, content) 170 | } 171 | 172 | // SendEmail indicates an expected call of SendEmail. 173 | func (mr *MockMyServiceMockRecorder) SendEmail(recipient, subject, content interface{}) *gomock.Call { 174 | mr.mock.ctrl.T.Helper() 175 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendEmail", reflect.TypeOf((*MockMyService)(nil).SendEmail), recipient, subject, content) 176 | } 177 | 178 | // UndoCreateCreditMemo mocks base method. 179 | func (m *MockMyService) UndoCreateCreditMemo(account string, amount int, notes string) error { 180 | m.ctrl.T.Helper() 181 | ret := m.ctrl.Call(m, "UndoCreateCreditMemo", account, amount, notes) 182 | ret0, _ := ret[0].(error) 183 | return ret0 184 | } 185 | 186 | // UndoCreateCreditMemo indicates an expected call of UndoCreateCreditMemo. 187 | func (mr *MockMyServiceMockRecorder) UndoCreateCreditMemo(account, amount, notes interface{}) *gomock.Call { 188 | mr.mock.ctrl.T.Helper() 189 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndoCreateCreditMemo", reflect.TypeOf((*MockMyService)(nil).UndoCreateCreditMemo), account, amount, notes) 190 | } 191 | 192 | // UndoCreateDebitMemo mocks base method. 193 | func (m *MockMyService) UndoCreateDebitMemo(account string, amount int, notes string) error { 194 | m.ctrl.T.Helper() 195 | ret := m.ctrl.Call(m, "UndoCreateDebitMemo", account, amount, notes) 196 | ret0, _ := ret[0].(error) 197 | return ret0 198 | } 199 | 200 | // UndoCreateDebitMemo indicates an expected call of UndoCreateDebitMemo. 201 | func (mr *MockMyServiceMockRecorder) UndoCreateDebitMemo(account, amount, notes interface{}) *gomock.Call { 202 | mr.mock.ctrl.T.Helper() 203 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndoCreateDebitMemo", reflect.TypeOf((*MockMyService)(nil).UndoCreateDebitMemo), account, amount, notes) 204 | } 205 | 206 | // UndoCredit mocks base method. 207 | func (m *MockMyService) UndoCredit(account string, amount int) error { 208 | m.ctrl.T.Helper() 209 | ret := m.ctrl.Call(m, "UndoCredit", account, amount) 210 | ret0, _ := ret[0].(error) 211 | return ret0 212 | } 213 | 214 | // UndoCredit indicates an expected call of UndoCredit. 215 | func (mr *MockMyServiceMockRecorder) UndoCredit(account, amount interface{}) *gomock.Call { 216 | mr.mock.ctrl.T.Helper() 217 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndoCredit", reflect.TypeOf((*MockMyService)(nil).UndoCredit), account, amount) 218 | } 219 | 220 | // UndoDebit mocks base method. 221 | func (m *MockMyService) UndoDebit(account string, amount int) error { 222 | m.ctrl.T.Helper() 223 | ret := m.ctrl.Call(m, "UndoDebit", account, amount) 224 | ret0, _ := ret[0].(error) 225 | return ret0 226 | } 227 | 228 | // UndoDebit indicates an expected call of UndoDebit. 229 | func (mr *MockMyServiceMockRecorder) UndoDebit(account, amount interface{}) *gomock.Call { 230 | mr.mock.ctrl.T.Helper() 231 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndoDebit", reflect.TypeOf((*MockMyService)(nil).UndoDebit), account, amount) 232 | } 233 | 234 | // UpdateExternalSystem mocks base method. 235 | func (m *MockMyService) UpdateExternalSystem(message string) { 236 | m.ctrl.T.Helper() 237 | m.ctrl.Call(m, "UpdateExternalSystem", message) 238 | } 239 | 240 | // UpdateExternalSystem indicates an expected call of UpdateExternalSystem. 241 | func (mr *MockMyServiceMockRecorder) UpdateExternalSystem(message interface{}) *gomock.Call { 242 | mr.mock.ctrl.T.Helper() 243 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateExternalSystem", reflect.TypeOf((*MockMyService)(nil).UpdateExternalSystem), message) 244 | } 245 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= 3 | github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 4 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 5 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 6 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 7 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 8 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 9 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 11 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 16 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 17 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 18 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 19 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 20 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 21 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 22 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 23 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 24 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 25 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 26 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 27 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 28 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 29 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 30 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 31 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 32 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 33 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 34 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 35 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 36 | github.com/indeedeng/iwf-golang-sdk v1.6.0 h1:1F/4Jvz1OVpJHMSDWtyDzaSzGvcaYX7Hu67TXgD7oxI= 37 | github.com/indeedeng/iwf-golang-sdk v1.6.0/go.mod h1:DnJN2x4H/wFLDEjQcyD0Br4OkVuKQK9nDgm7Nb7byIo= 38 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 39 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 40 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 41 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 42 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 43 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 44 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 45 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 46 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 47 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 48 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 50 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 51 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 52 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 53 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 54 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 57 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 58 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 59 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 60 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 61 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 62 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 63 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 64 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 65 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 66 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 67 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 68 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 69 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 70 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 71 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 72 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 73 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 74 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 75 | github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk= 76 | github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 77 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 78 | go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= 79 | go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 80 | golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= 81 | golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= 82 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 83 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 84 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 85 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 86 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 87 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 88 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 89 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 90 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 91 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 92 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 93 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 95 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 96 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 99 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 100 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 101 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 102 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 103 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 104 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 105 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 106 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 107 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 108 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 109 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 110 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 111 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 112 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 113 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 114 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 115 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 116 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 117 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 118 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 119 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 120 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 121 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 122 | --------------------------------------------------------------------------------