diff --git a/Makefile b/Makefile index b3a5e0d..c761184 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ export PATH := $(GOPATH)/bin:$(PATH) default: test PROGS = helloworld \ + blocks \ versioning \ delaystart \ branch \ @@ -53,6 +54,7 @@ TEST_DIRS=./cmd/samples/cron \ ./cmd/samples/dsl \ ./cmd/samples/expense \ ./cmd/samples/fileprocessing \ + ./cmd/samples/blocks \ ./cmd/samples/recipes/branch \ ./cmd/samples/recipes/choice \ ./cmd/samples/recipes/greetings \ @@ -83,6 +85,9 @@ cancelactivity: helloworld: go build -o bin/helloworld cmd/samples/recipes/helloworld/*.go +blocks: + go build -o bin/blocks cmd/samples/blocks/*.go + delaystart: go build -o bin/delaystart cmd/samples/recipes/delaystart/*.go @@ -204,6 +209,7 @@ clean: rm -Rf $(BUILD) bins: helloworld \ + blocks \ versioning \ delaystart \ branch \ diff --git a/cmd/samples/blocks/blocks b/cmd/samples/blocks/blocks new file mode 100755 index 0000000..d309237 Binary files /dev/null and b/cmd/samples/blocks/blocks differ diff --git a/cmd/samples/blocks/blocks_workflow.go b/cmd/samples/blocks/blocks_workflow.go new file mode 100644 index 0000000..19d2339 --- /dev/null +++ b/cmd/samples/blocks/blocks_workflow.go @@ -0,0 +1,123 @@ +package main + +import ( + "time" + + "go.uber.org/cadence/workflow" + "go.uber.org/cadence/x/blocks" + "go.uber.org/zap" +) + +/** + * This is the blocks workflow sample that demonstrates JSON blob queries. + */ + +// ApplicationName is the task list for this sample +const ApplicationName = "blocksGroup" + +const blocksWorkflowName = "blocksWorkflow" + +// This is an example of using the 'blocks' query response in a cadence query, in this example, +// to select the lunch option. +func blocksWorkflow(ctx workflow.Context, name string) error { + ao := workflow.ActivityOptions{ + ScheduleToStartTimeout: time.Minute, + StartToCloseTimeout: time.Minute, + HeartbeatTimeout: time.Second * 20, + } + ctx = workflow.WithActivityOptions(ctx, ao) + + logger := workflow.GetLogger(ctx) + + votes := []map[string]string{} + + workflow.SetQueryHandler(ctx, "options", func() (blocks.QueryResponse, error) { + logger := workflow.GetLogger(ctx) + logger.Info("Responding to 'options' query") + + return makeResponse(votes), nil + }) + + votesChan := workflow.GetSignalChannel(ctx, "lunch_order") + workflow.Go(ctx, func(ctx workflow.Context) { + for { + var vote map[string]string + votesChan.Receive(ctx, &vote) + votes = append(votes, vote) + } + }) + defer func() { + votesChan.Close() + }() + + err := workflow.Sleep(ctx, 30*time.Minute) + if err != nil { + logger.Error("Sleep failed", zap.Error(err)) + return err + } + + logger.Info("Workflow completed.", zap.Any("Result", votes)) + return nil +} + +// makeResponse creates the lunch query response payload based on the current votes +func makeResponse(votes []map[string]string) blocks.QueryResponse { + return blocks.New( + blocks.NewMarkdownSection("## Lunch options\nWe're voting on where to order lunch today. Select the option you want to vote for."), + blocks.NewDivider(), + blocks.NewMarkdownSection(makeVoteTable(votes)), + blocks.NewMarkdownSection(makeMenu()), + blocks.NewSignalActions( + blocks.NewSignalButton("Farmhouse", "lunch_order", map[string]string{"location": "farmhouse - red thai curry", "requests": "spicy"}), + blocks.NewSignalButtonWithExternalWorkflow("Ethiopian", "no_lunch_order_walk_in_person", nil, "in-person-order-workflow", ""), + blocks.NewSignalButton("Ler Ros", "lunch_order", map[string]string{"location": "Ler Ros", "meal": "tofo Bahn Mi"}), + ), + ) +} + +func makeMenu() string { + + options := []struct { + image string + desc string + }{ + { + image: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Red_roast_duck_curry.jpg/200px-Red_roast_duck_curry.jpg", + desc: "Farmhouse - Red Thai Curry: (Thai: แกง, romanized: kaeng, pronounced [kɛ̄ːŋ]) is a dish in Thai cuisine made from curry paste, coconut milk or water, meat, seafood, vegetables or fruit, and herbs. Curries in Thailand mainly differ from the Indian subcontinent in their use of ingredients such as fresh rhizomes, herbs, and aromatic leaves rather than a mix of dried spices.", + }, + { + image: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png/200px-B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png", + desc: "Ler Ros: Lemongrass Tofu Bahn Mi: In Vietnamese cuisine, bánh mì, bánh mỳ or banh mi is a sandwich consisting of a baguette filled with various ingredients, most commonly including a protein such as pâté, chicken, or pork, and vegetables such as lettuce, cilantro, and cucumber.", + }, + { + image: "https://upload.wikimedia.org/wikipedia/commons/thumb/5/54/Ethiopian_wat.jpg/960px-Ethiopian_wat.jpg", + desc: "Ethiopian Wat: Wat is a traditional Ethiopian dish made from a mixture of spices, vegetables, and legumes. It is typically served with injera, a sourdough flatbread that is used to scoop up the food.", + }, + } + + table := "| Picture | Description |\n|---|----|\n" + for _, option := range options { + table += "| ![food](" + option.image + ") | " + option.desc + " |\n" + } + + table += "\n\n\n(source wikipedia)" + + return table +} + +func makeVoteTable(votes []map[string]string) string { + if len(votes) == 0 { + return "| lunch order vote | meal | requests |\n|-------|-------|-------|\n| No votes yet |\n" + } + table := "| lunch order vote | meal | requests |\n|-------|-------|-------|\n" + for _, vote := range votes { + + loc := vote["location"] + meal := vote["meal"] + requests := vote["requests"] + + table += "| " + loc + " | " + meal + " | " + requests + " |\n" + } + + return table +} diff --git a/cmd/samples/blocks/blocks_workflow_test.go b/cmd/samples/blocks/blocks_workflow_test.go new file mode 100644 index 0000000..23486ad --- /dev/null +++ b/cmd/samples/blocks/blocks_workflow_test.go @@ -0,0 +1,203 @@ +package main + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMakeResponse(t *testing.T) { + // Test with empty votes + t.Run("EmptyVotes", func(t *testing.T) { + votes := []map[string]string{} + result := makeResponse(votes) + + // Verify the structure + assert.Equal(t, "formattedData", result.CadenceResponseType) + assert.Equal(t, "blocks", result.Format) + assert.Len(t, result.Blocks, 5) + + // Verify JSON serialization matches expected output + resultJSON, err := json.Marshal(result) + assert.NoError(t, err) + + expectedJSON := `{ + "cadenceResponseType": "formattedData", + "format": "blocks", + "blocks": [ + { + "type": "section", + "format": "text/markdown", + "componentOptions": { + "text": "## Lunch options\nWe're voting on where to order lunch today. Select the option you want to vote for." + } + }, + { + "type": "divider" + }, + { + "type": "section", + "format": "text/markdown", + "componentOptions": { + "text": "| lunch order vote | meal | requests |\n|-------|-------|-------|\n| No votes yet |\n" + } + }, + { + "type": "section", + "format": "text/markdown", + "componentOptions": { + "text": "| Picture | Description |\n|---|----|\n| ![food](https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Red_roast_duck_curry.jpg/200px-Red_roast_duck_curry.jpg) | Farmhouse - Red Thai Curry: (Thai: แกง, romanized: kaeng, pronounced [kɛ̄ːŋ]) is a dish in Thai cuisine made from curry paste, coconut milk or water, meat, seafood, vegetables or fruit, and herbs. Curries in Thailand mainly differ from the Indian subcontinent in their use of ingredients such as fresh rhizomes, herbs, and aromatic leaves rather than a mix of dried spices. |\n| ![food](https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png/200px-B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png) | Ler Ros: Lemongrass Tofu Bahn Mi: In Vietnamese cuisine, bánh mì, bánh mỳ or banh mi is a sandwich consisting of a baguette filled with various ingredients, most commonly including a protein such as pâté, chicken, or pork, and vegetables such as lettuce, cilantro, and cucumber. |\n| ![food](https://upload.wikimedia.org/wikipedia/commons/thumb/5/54/Ethiopian_wat.jpg/960px-Ethiopian_wat.jpg) | Ethiopian Wat: Wat is a traditional Ethiopian dish made from a mixture of spices, vegetables, and legumes. It is typically served with injera, a sourdough flatbread that is used to scoop up the food. |\n\n\n\n(source wikipedia)" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "componentOptions": { + "type": "plain_text", + "text": "Farmhouse" + }, + "action": { + "type": "signal", + "signal_name": "lunch_order", + "signal_value": { + "location": "farmhouse - red thai curry", + "requests": "spicy" + } + } + }, + { + "type": "button", + "componentOptions": { + "type": "plain_text", + "text": "Ethiopian" + }, + "action": { + "type": "signal", + "signal_name": "no_lunch_order_walk_in_person", + "workflow_id": "in-person-order-workflow" + } + }, + { + "type": "button", + "componentOptions": { + "type": "plain_text", + "text": "Ler Ros" + }, + "action": { + "type": "signal", + "signal_name": "lunch_order", + "signal_value": { + "location": "Ler Ros", + "meal": "tofo Bahn Mi" + } + } + } + ] + } + ] +}` + + assert.JSONEq(t, expectedJSON, string(resultJSON)) + }) + + // Test with some votes + t.Run("WithVotes", func(t *testing.T) { + votes := []map[string]string{ + {"location": "farmhouse - red thai curry", "requests": "spicy"}, + {"location": "Ler Ros", "meal": "tofo Bahn Mi"}, + } + result := makeResponse(votes) + + // Verify the structure + assert.Equal(t, "formattedData", result.CadenceResponseType) + assert.Equal(t, "blocks", result.Format) + assert.Len(t, result.Blocks, 5) + + // Verify JSON serialization matches expected output + resultJSON, err := json.Marshal(result) + assert.NoError(t, err) + + expectedJSON := `{ + "cadenceResponseType": "formattedData", + "format": "blocks", + "blocks": [ + { + "type": "section", + "format": "text/markdown", + "componentOptions": { + "text": "## Lunch options\nWe're voting on where to order lunch today. Select the option you want to vote for." + } + }, + { + "type": "divider" + }, + { + "type": "section", + "format": "text/markdown", + "componentOptions": { + "text": "| lunch order vote | meal | requests |\n|-------|-------|-------|\n| farmhouse - red thai curry | | spicy |\n| Ler Ros | tofo Bahn Mi | |\n" + } + }, + { + "type": "section", + "format": "text/markdown", + "componentOptions": { + "text": "| Picture | Description |\n|---|----|\n| ![food](https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Red_roast_duck_curry.jpg/200px-Red_roast_duck_curry.jpg) | Farmhouse - Red Thai Curry: (Thai: แกง, romanized: kaeng, pronounced [kɛ̄ːŋ]) is a dish in Thai cuisine made from curry paste, coconut milk or water, meat, seafood, vegetables or fruit, and herbs. Curries in Thailand mainly differ from the Indian subcontinent in their use of ingredients such as fresh rhizomes, herbs, and aromatic leaves rather than a mix of dried spices. |\n| ![food](https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png/200px-B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png) | Ler Ros: Lemongrass Tofu Bahn Mi: In Vietnamese cuisine, bánh mì, bánh mỳ or banh mi is a sandwich consisting of a baguette filled with various ingredients, most commonly including a protein such as pâté, chicken, or pork, and vegetables such as lettuce, cilantro, and cucumber. |\n| ![food](https://upload.wikimedia.org/wikipedia/commons/thumb/5/54/Ethiopian_wat.jpg/960px-Ethiopian_wat.jpg) | Ethiopian Wat: Wat is a traditional Ethiopian dish made from a mixture of spices, vegetables, and legumes. It is typically served with injera, a sourdough flatbread that is used to scoop up the food. |\n\n\n\n(source wikipedia)" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "componentOptions": { + "type": "plain_text", + "text": "Farmhouse" + }, + "action": { + "type": "signal", + "signal_name": "lunch_order", + "signal_value": { + "location": "farmhouse - red thai curry", + "requests": "spicy" + } + } + }, + { + "type": "button", + "componentOptions": { + "type": "plain_text", + "text": "Ethiopian" + }, + "action": { + "type": "signal", + "signal_name": "no_lunch_order_walk_in_person", + "workflow_id": "in-person-order-workflow" + } + }, + { + "type": "button", + "componentOptions": { + "type": "plain_text", + "text": "Ler Ros" + }, + "action": { + "type": "signal", + "signal_name": "lunch_order", + "signal_value": { + "location": "Ler Ros", + "meal": "tofo Bahn Mi" + } + } + } + ] + } + ] +}` + + assert.JSONEq(t, expectedJSON, string(resultJSON)) + }) +} diff --git a/cmd/samples/blocks/main.go b/cmd/samples/blocks/main.go new file mode 100644 index 0000000..b9495ae --- /dev/null +++ b/cmd/samples/blocks/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "flag" + "time" + + "github.com/pborman/uuid" + "go.uber.org/cadence/client" + "go.uber.org/cadence/worker" + + "github.com/uber-common/cadence-samples/cmd/samples/common" +) + +// This needs to be done as part of a bootstrap step when the process starts. +// The workers are supposed to be long running. +func startWorkers(h *common.SampleHelper) { + // Configure worker options. + workerOptions := worker.Options{ + MetricsScope: h.WorkerMetricScope, + Logger: h.Logger, + FeatureFlags: client.FeatureFlags{ + WorkflowExecutionAlreadyCompletedErrorEnabled: true, + }, + } + h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) +} + +func startWorkflow(h *common.SampleHelper) { + workflowOptions := client.StartWorkflowOptions{ + ID: "lunch_order_" + uuid.New(), + TaskList: ApplicationName, + ExecutionStartToCloseTimeout: 10*time.Minute, + DecisionTaskStartToCloseTimeout: time.Minute, + } + h.StartWorkflow(workflowOptions, blocksWorkflowName, "Cadence") +} + +func registerWorkflowAndActivity( + h *common.SampleHelper, +) { + h.RegisterWorkflowWithAlias(blocksWorkflow, blocksWorkflowName) +} + +func main() { + var mode string + flag.StringVar(&mode, "m", "trigger", "Mode is worker, trigger.") + flag.Parse() + + var h common.SampleHelper + h.SetupServiceConfig() + + switch mode { + case "worker": + registerWorkflowAndActivity(&h) + startWorkers(&h) + + // The workers are supposed to be long running process that should not exit. + // Use select{} to block indefinitely for samples, you can quit by CMD+C. + select {} + case "trigger": + startWorkflow(&h) + } +} diff --git a/go.mod b/go.mod index ab5089b..e434d36 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/uber-go/tally v3.4.3+incompatible github.com/uber/cadence-idl v0.0.0-20250616185004-cc6f52f87bc6 github.com/uber/jaeger-client-go v2.30.0+incompatible - go.uber.org/cadence v1.3.1-rc.10 + go.uber.org/cadence v1.3.1-rc.11 go.uber.org/yarpc v1.60.0 go.uber.org/zap v1.23.0 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index c4a182f..6e5da33 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/cadence v1.3.1-rc.10 h1:2ZQba0IPCTcjYtjVaPM1cgnu9ZFvnw2BTC2xrEO25mU= -go.uber.org/cadence v1.3.1-rc.10/go.mod h1:Yf2WaRFj6TtqrUbGRWxLU/8Vou3WPn0M2VIdfEIlEjE= +go.uber.org/cadence v1.3.1-rc.11 h1:+AVmN+w1zV01BGjjK2S/PGsddo1Q5UkJt2B9ore/FWA= +go.uber.org/cadence v1.3.1-rc.11/go.mod h1:Yf2WaRFj6TtqrUbGRWxLU/8Vou3WPn0M2VIdfEIlEjE= go.uber.org/dig v1.8.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw= go.uber.org/dig v1.10.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw= go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI=