├── 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 |
18 | * You can use query handler to look at the current data like this
19 |
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 |
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 | 
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 |
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 |
27 |
28 | # Implementation Details
29 |
30 | ## InitState
31 | 
32 |
33 | ## ReminderState
34 |
35 | 
36 |
37 | ## RPC
38 |
39 | 
40 |
41 | ## NotifyExtState
42 | 
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 |
--------------------------------------------------------------------------------