Repository: uber-common/cadence-samples Branch: master Commit: d3f3ec968d30 Files: 233 Total size: 617.6 KB Directory structure: gitextract__zd9l1h0/ ├── .gitar/ │ └── rules/ │ └── pr-description-quality.md ├── .github/ │ ├── dco.yml │ ├── pull_request_guidance.md │ ├── pull_request_template.md │ └── workflows/ │ ├── build.yml │ └── semantic-pr.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── cmd/ │ └── samples/ │ ├── advanced/ │ │ └── autoscaling-monitoring/ │ │ ├── Makefile │ │ ├── README.md │ │ ├── activities.go │ │ ├── config/ │ │ │ └── autoscaling.yaml │ │ ├── config.go │ │ ├── config_test.go │ │ ├── main.go │ │ ├── worker_config.go │ │ └── workflow.go │ ├── common/ │ │ ├── factory.go │ │ ├── sample_helper.go │ │ └── util.go │ ├── cron/ │ │ ├── cron_workflow.go │ │ ├── cron_workflow_test.go │ │ └── main.go │ ├── dsl/ │ │ ├── README.md │ │ ├── activities.go │ │ ├── main.go │ │ ├── workflow.go │ │ ├── workflow1.yaml │ │ ├── workflow2.yaml │ │ └── workflow_test.go │ ├── expense/ │ │ ├── README.md │ │ ├── activities.go │ │ ├── main.go │ │ ├── server/ │ │ │ └── dummy.go │ │ ├── workflow.go │ │ └── workflow_test.go │ ├── fileprocessing/ │ │ ├── README.md │ │ ├── activities.go │ │ ├── main.go │ │ ├── workflow.go │ │ └── workflow_test.go │ ├── pso/ │ │ ├── README.md │ │ ├── activities.go │ │ ├── dataconverter.go │ │ ├── functions.go │ │ ├── main.go │ │ ├── particle.go │ │ ├── position.go │ │ ├── settings.go │ │ ├── swarm.go │ │ ├── utils.go │ │ ├── workflow.go │ │ └── workflow_test.go │ ├── recipes/ │ │ ├── branch/ │ │ │ ├── branch_workflow.go │ │ │ ├── main.go │ │ │ ├── parallel_workflow.go │ │ │ └── workflow_test.go │ │ ├── cancelactivity/ │ │ │ ├── main.go │ │ │ ├── workflow.go │ │ │ └── workflow_test.go │ │ ├── childworkflow/ │ │ │ ├── child_workflow.go │ │ │ ├── main.go │ │ │ └── parent_workflow.go │ │ ├── choice/ │ │ │ ├── exclusive_choice_workflow.go │ │ │ ├── main.go │ │ │ ├── multi_choice_workflow.go │ │ │ └── workflow_test.go │ │ ├── consistentquery/ │ │ │ ├── README.md │ │ │ ├── main.go │ │ │ └── query_workflow.go │ │ ├── crossdomain/ │ │ │ ├── main.go │ │ │ └── wf.go │ │ ├── ctxpropagation/ │ │ │ ├── README.md │ │ │ ├── activities.go │ │ │ ├── main.go │ │ │ ├── propagator.go │ │ │ ├── workflow.go │ │ │ └── workflow_test.go │ │ ├── delaystart/ │ │ │ ├── delaystart_workflow.go │ │ │ ├── delaystart_workflow_test.go │ │ │ └── main.go │ │ ├── dynamic/ │ │ │ ├── dynamic_workflow.go │ │ │ ├── main.go │ │ │ └── workflow_test.go │ │ ├── greetings/ │ │ │ ├── greetings.json │ │ │ ├── greetings_workflow.go │ │ │ ├── main.go │ │ │ ├── replay_test.go │ │ │ ├── shadow_test.go │ │ │ └── workflow_test.go │ │ ├── helloworld/ │ │ │ ├── activity_logger_test.go │ │ │ ├── helloworld.json │ │ │ ├── helloworld_workflow.go │ │ │ ├── helloworld_workflow_test.go │ │ │ ├── main.go │ │ │ ├── replay_test.go │ │ │ └── shadow_test.go │ │ ├── localactivity/ │ │ │ ├── README.md │ │ │ ├── local_activity_workflow.go │ │ │ ├── local_activity_workflow_test.go │ │ │ └── main.go │ │ ├── mutex/ │ │ │ ├── README.md │ │ │ ├── main.go │ │ │ ├── mutex_workflow.go │ │ │ └── mutex_workflow_test.go │ │ ├── pickfirst/ │ │ │ ├── main.go │ │ │ ├── pickfirst_workflow.go │ │ │ └── pickfirst_workflow_test.go │ │ ├── query/ │ │ │ ├── README.md │ │ │ ├── main.go │ │ │ ├── query_workflow.go │ │ │ └── query_workflow_test.go │ │ ├── retryactivity/ │ │ │ ├── main.go │ │ │ ├── retry_activity_workflow.go │ │ │ └── retry_activity_workflow_test.go │ │ ├── searchattributes/ │ │ │ ├── README.md │ │ │ ├── main.go │ │ │ ├── searchattributes_workflow.go │ │ │ └── searchattributes_workflow_test.go │ │ ├── sideeffect/ │ │ │ └── sideeffect_workflow.go │ │ ├── signalcounter/ │ │ │ ├── main.go │ │ │ ├── signal_counter_workflow.go │ │ │ └── workflow_test.go │ │ ├── sleep/ │ │ │ ├── README.md │ │ │ ├── main.go │ │ │ ├── sleep_workflow.go │ │ │ └── sleep_workflow_test.go │ │ ├── splitmerge/ │ │ │ ├── main.go │ │ │ ├── splitmerge_workflow.go │ │ │ └── splitmerge_workflow_test.go │ │ ├── timer/ │ │ │ ├── main.go │ │ │ ├── workflow.go │ │ │ └── workflow_test.go │ │ ├── tracing/ │ │ │ ├── helloworld_workflow.go │ │ │ └── main.go │ │ └── versioning/ │ │ ├── README.md │ │ ├── main.go │ │ └── versioned_workflow.go │ └── recovery/ │ ├── README.md │ ├── cache/ │ │ ├── cache.go │ │ └── lru.go │ ├── main.go │ ├── recovery_workflow.go │ └── trip_workflow.go ├── config/ │ └── development.yaml ├── go.mod ├── go.sum ├── k8s/ │ ├── README.md │ ├── cadence-samples-pod.yaml │ └── docker/ │ └── Dockerfile ├── new_samples/ │ ├── README.md │ ├── activities/ │ │ ├── README.md │ │ ├── dynamic_workflow.go │ │ ├── generator/ │ │ │ ├── README.md │ │ │ ├── README_specific.md │ │ │ └── generate.go │ │ ├── main.go │ │ ├── parallel_pick_first_workflow.go │ │ └── worker.go │ ├── client_tls/ │ │ ├── README.md │ │ ├── cadence_client.go │ │ ├── main.go │ │ └── tls_config.go │ ├── concurrency/ │ │ ├── README.md │ │ ├── batch_workflow.go │ │ ├── batch_workflow_test.go │ │ ├── generator/ │ │ │ ├── README.md │ │ │ ├── README_specific.md │ │ │ └── generate.go │ │ ├── main.go │ │ └── worker.go │ ├── data/ │ │ ├── README.md │ │ ├── compressed_dataconverter_workflow.go │ │ ├── compressed_dataconverter_workflow_test.go │ │ ├── encrypted_dataconverter_workflow.go │ │ ├── encrypted_dataconverter_workflow_test.go │ │ ├── generator/ │ │ │ ├── README.md │ │ │ ├── README_specific.md │ │ │ └── generate.go │ │ ├── main.go │ │ ├── s3_dataconverter_workflow.go │ │ ├── s3_dataconverter_workflow_test.go │ │ └── worker.go │ ├── hello_world/ │ │ ├── README.md │ │ ├── generator/ │ │ │ ├── README.md │ │ │ ├── README_specific.md │ │ │ └── generate.go │ │ ├── main.go │ │ ├── worker.go │ │ └── workflow.go │ ├── operations/ │ │ ├── README.md │ │ ├── cancel_workflow.go │ │ ├── generator/ │ │ │ ├── README.md │ │ │ ├── README_specific.md │ │ │ └── generate.go │ │ ├── main.go │ │ └── worker.go │ ├── query/ │ │ ├── README.md │ │ ├── generator/ │ │ │ ├── README.md │ │ │ ├── README_specific.md │ │ │ └── generate.go │ │ ├── lunch_vote_workflow.go │ │ ├── main.go │ │ ├── markdown_query.go │ │ ├── order_fulfillment_workflow.go │ │ └── worker.go │ ├── signal/ │ │ ├── README.md │ │ ├── generator/ │ │ │ ├── README.md │ │ │ ├── README_specific.md │ │ │ └── generate.go │ │ ├── main.go │ │ ├── simple_signal_workflow.go │ │ └── worker.go │ └── template/ │ ├── README.tmpl │ ├── README_generator.tmpl │ ├── README_references.tmpl │ ├── generator.go │ ├── main.tmpl │ └── worker.tmpl └── python_sdk_samples/ ├── .python-version ├── README.md ├── __init__.py ├── openai_samples/ │ ├── __init__.py │ └── agent_handoffs/ │ ├── README.md │ ├── __init__.py │ ├── book_trip_agent.py │ ├── main.py │ └── tools.py └── pyproject.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitar/rules/pr-description-quality.md ================================================ --- title: PR Description Quality Standards description: Ensures PR descriptions meet quality criteria for the cadence-samples (Go) repo using guidance from the PR template and .github/pull_request_guidance.md when: PR description is created or updated actions: Read PR template and guidance, then report requirement status --- # PR Description Quality Standards When evaluating a pull request description: 1. **Read the PR template guidance** at `.github/pull_request_guidance.md` to understand the expected guidance for each section 2. Apply that guidance to evaluate the current PR description 3. Provide recommendations for how to improve the description. ## Core Principle: Why Not How From https://cbea.ms/git-commit/#why-not-how: - **"A diff shows WHAT changed, but only the description can explain WHY"** - Focus on: the problem being solved, the reasoning behind the solution, context - The code itself documents HOW - the PR description documents WHY ## Evaluation Criteria ### Required Sections (must exist with substantive content per PR template guidance) 1. **Which sample(s) or area?** - One line listing area(s) touched. This repo has two sample trees: **cmd/samples** (legacy; make + ./bin/…) and **new_samples** (per-folder; go run .). Identify which tree and area(s) are touched. - **cmd/samples:** e.g. cmd/samples/recipes/helloworld, cmd/samples/recipes/branch, cmd/samples/recipes/query, cmd/samples/batch, cmd/samples/cron, cmd/samples/expense, cmd/samples/fileprocessing, cmd/samples/dsl, cmd/samples/pso, cmd/samples/recovery, cmd/samples/recipes/cancelactivity, etc. - **new_samples:** hello_world, activities, query, signal, operations, client_tls, template. - **Other:** README, build, config, Makefile, common. - Helps reviewers route; skip flagging if area is obvious from paths 2. **What changed?** - 1-2 line summary of WHAT changed technically - Focus on key modification, not implementation details - Link to GitHub issue encouraged for non-trivial changes; optional for trivial doc/sample tweaks - Template has good/bad examples 3. **Why?** - Context and motivation (why not how) - Enough rationale for reviewers to understand the goal (e.g. improving clarity, fixing compatibility, aligning with docs) - Must explain WHY this approach was chosen 4. **How did you test it?** - Concrete, copyable commands with exact invocations - GOOD: `make`, `go test ./...`, `go test ./cmd/samples/recipes/helloworld/`, `cd new_samples/hello_world && go run .`, and Cadence CLI commands as in sample READMEs - BAD: "Tested locally" or "See tests" - Expect Go/make and/or sample execution commands; no canary or integration server setup required 5. **Potential risks** - Often N/A for sample-only or doc-only changes - Call out when relevant: dependency upgrades, behavior changes for someone copying the sample, build/config changes - Don't require lengthy text when N/A is appropriate 6. **Release notes** - Optional for this repo. Use when change is user-facing (e.g. new sample, notable README change) - Can be N/A for internal refactors or tiny fixes - Don't require lengthy text when N/A is appropriate 7. **Documentation Changes** - Often relevant when adding or changing samples (main README, cmd/samples READMEs, new_samples READMEs including generator-generated, links to cadence or cadence-docs) - Only mark N/A if certain no docs are affected ### Quality Checks - **Skip obvious things** - Don't flag items clear from folder structure (e.g. area from paths) - **Skip trivial refactors** - Minor formatting/style changes don't need deep rationale - **Don't check automated items** - CI, linting are automated ## FORBIDDEN - Never Include - "Issues Found", "Testing Evidence Quality", "Documentation Reasoning", "Summary" sections - "Note:" paragraphs or explanatory text outside recommendations - Grouping recommendations by type ## Section Names (Use EXACT Brackets) - **[Which sample(s) or area?]** - **[What changed?]** - **[Why?]** - **[How did you test it?]** - **[Potential risks]** - **[Release notes]** - **[Documentation Changes]** ================================================ FILE: .github/dco.yml ================================================ require: members: false ================================================ FILE: .github/pull_request_guidance.md ================================================ **Which sample(s) or area?** **What changed?** **Why?** **How did you test it?** **Potential risks** **Release notes** **Documentation Changes** ================================================ FILE: .github/pull_request_template.md ================================================ **Which sample(s) or area?** **What changed?** **Why?** **How did you test it?** **Potential risks** **Release notes** **Documentation Changes** ================================================ FILE: .github/workflows/build.yml ================================================ name: Build and test cadence-samples on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v4 with: go-version: '1.22.x' - name: Build and Test run: make ================================================ FILE: .github/workflows/semantic-pr.yml ================================================ name: Semantic Pull Request on: pull_request: types: - opened - edited - synchronize jobs: semantic-pr: name: Validate PR title follows conventional commit format runs-on: ubuntu-latest # TODO: Remove this once we commit to conventional commits continue-on-error: true steps: - name: Validate PR title id: lint_pr_title uses: amannn/action-semantic-pull-request@v5.4.0 with: # Allow standard conventional commit types types: | fix feat docs style refactor perf test chore ci build # TODO: Remove this once we've decided on scopes requireScope: false # Skip validation for certain labels if needed ignoreLabels: | skip-commit-format env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Comment on PR if validation fails if: steps.lint_pr_title.outputs.error_message != null uses: actions/github-script@v7 with: script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: `⚠️ **Semantic PR Check Failed** **Error Details:** \`\`\` ${{ steps.lint_pr_title.outputs.error_message }} \`\`\` **Required Format:** \`\`\` : \`\`\` **Allowed types:** fix, feat, docs, style, refactor, perf, test, chore, ci, build **Examples:** - \`feat: add user authentication system\` - \`fix: resolve memory leak in worker pool\` - \`docs: update API documentation\` - \`test: add integration tests for auth flow\` This is currently a **warning only** and won't block your PR from being merged.` }) ================================================ FILE: .gitignore ================================================ *.out *.test *.xml *.swp .idea/ .vscode/ *.iml *.cov *.html .tmp/ .DS_Store test test.log vendor/ # Executables produced by cadence-samples repo bin/ # Binary from go build in new_samples/query new_samples/query/query docker-compose.yml # Credentials new_samples/client_samples/helloworld_tls/credentials/ # Python SDK Samples __pycache__/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Cadence Samples This doc is intended for contributors to Cadence samples. Thanks for considering to contribute ❤️ > 📚 **New to contributing to Cadence?** Check out our [Contributing Guide](https://cadenceworkflow.io/community/how-to-contribute/getting-started) for an overview of the contribution process across all Cadence repositories. This document contains cadence-samples specific setup and development instructions. Once you go through the rest of this doc and get familiar with local development setup, take a look at the list of issues labeled with [good first issue](https://github.com/uber-common/cadence-samples/labels/good%20first%20issue). Join our community on the CNCF Slack workspace at [cloud-native.slack.com](https://communityinviter.com/apps/cloud-native/cncf) in the **#cadence-users** channel to reach out and discuss issues with the team. ### Documentation - Every sample must have a README.md - Include: - What the sample demonstrates - Real-world use cases - How to run the sample - Expected output - Key concepts ### Getting Help If you need help or have questions: - Join [CNCF Slack #cadence-users](https://communityinviter.com/apps/cloud-native/cncf) - Ask on [StackOverflow](https://stackoverflow.com/questions/tagged/cadence-workflow) with tag `cadence-workflow` - Open a [GitHub Discussion](https://github.com/uber-common/cadence-samples/discussions) - File an [issue](https://github.com/uber-common/cadence-samples/issues) for bugs ## Code of Conduct Please be respectful and constructive in all interactions. We're all here to learn and help each other build better software. ## License By contributing, you agree that your contributions will be licensed under the Apache 2.0 License. --- Thank you for contributing to Cadence samples! Your efforts help the entire community. 🚀 ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ .PHONY: test bins clean run-generators PROJECT_ROOT = github.com/uber-common/cadence-samples export PATH := $(GOPATH)/bin:$(PATH) # default target default: test PROGS = helloworld \ versioning \ delaystart \ branch \ childworkflow \ crossdomain \ choice \ dynamic \ greetings \ pickfirst \ retryactivity \ splitmerge \ timer \ localactivity \ query \ consistentquery \ cron \ tracing \ dsl \ fileprocessing \ expense_dummy \ expense \ recovery \ cancelactivity \ ctxpropagation \ pso \ signalcounter \ sideeffect \ sleep \ autoscaling-monitoring \ TEST_ARG ?= -race -v -timeout 5m BUILD := ./build SAMPLES_DIR=./cmd/samples export PATH := $(GOPATH)/bin:$(PATH) # Automatically gather all srcs ALL_SRC := $(shell find ./cmd/samples/common -name "*.go") # all directories with *_test.go files in them TEST_DIRS=./cmd/samples/cron \ ./cmd/samples/dsl \ ./cmd/samples/expense \ ./cmd/samples/fileprocessing \ ./cmd/samples/recipes/branch \ ./cmd/samples/recipes/choice \ ./cmd/samples/recipes/greetings \ ./cmd/samples/recipes/helloworld \ ./cmd/samples/recipes/delaystart \ ./cmd/samples/recipes/cancelactivity \ ./cmd/samples/recipes/pickfirst \ ./cmd/samples/recipes/mutex \ ./cmd/samples/recipes/retryactivity \ ./cmd/samples/recipes/splitmerge \ ./cmd/samples/recipes/timer \ ./cmd/samples/recipes/localactivity \ ./cmd/samples/recipes/query \ ./cmd/samples/recipes/consistentquery \ ./cmd/samples/recipes/ctxpropagation \ ./cmd/samples/recipes/searchattributes \ ./cmd/samples/recipes/sideeffect \ ./cmd/samples/recipes/signalcounter \ ./cmd/samples/recipes/sleep \ ./cmd/samples/recovery \ ./cmd/samples/pso \ cancelactivity: go build -o bin/cancelactivity cmd/samples/recipes/cancelactivity/*.go helloworld: go build -o bin/helloworld cmd/samples/recipes/helloworld/*.go delaystart: go build -o bin/delaystart cmd/samples/recipes/delaystart/*.go sleep: go build -o bin/sleep cmd/samples/recipes/sleep/*.go branch: go build -o bin/branch cmd/samples/recipes/branch/*.go childworkflow: go build -o bin/childworkflow cmd/samples/recipes/childworkflow/*.go choice: go build -o bin/choice cmd/samples/recipes/choice/*.go dynamic: go build -o bin/dynamic cmd/samples/recipes/dynamic/*.go greetings: go build -o bin/greetings cmd/samples/recipes/greetings/*.go pickfirst: go build -o bin/pickfirst cmd/samples/recipes/pickfirst/*.go mutex: go build -o bin/mutex cmd/samples/recipes/mutex/*.go retryactivity: go build -o bin/retryactivity cmd/samples/recipes/retryactivity/*.go splitmerge: go build -o bin/splitmerge cmd/samples/recipes/splitmerge/*.go searchattributes: go build -o bin/searchattributes cmd/samples/recipes/searchattributes/*.go timer: go build -o bin/timer cmd/samples/recipes/timer/*.go localactivity: go build -o bin/localactivity cmd/samples/recipes/localactivity/*.go query: go build -o bin/query cmd/samples/recipes/query/*.go consistentquery: go build -o bin/consistentquery cmd/samples/recipes/consistentquery/*.go ctxpropagation: go build -o bin/ctxpropagation cmd/samples/recipes/ctxpropagation/*.go tracing: go build -o bin/tracing cmd/samples/recipes/tracing/*.go cron: go build -o bin/cron cmd/samples/cron/*.go dsl: go build -o bin/dsl cmd/samples/dsl/*.go fileprocessing: go build -o bin/fileprocessing cmd/samples/fileprocessing/*.go expense_dummy: go build -o bin/expense_dummy cmd/samples/expense/server/*.go expense: go build -o bin/expense cmd/samples/expense/*.go recovery: go build -o bin/recovery cmd/samples/recovery/*.go pso: go build -o bin/pso cmd/samples/pso/*.go signalcounter: go build -o bin/signalcounter cmd/samples/recipes/signalcounter/*.go crossdomain: go build -o bin/crossdomain cmd/samples/recipes/crossdomain/*.go crossdomain-setup: # use the ..cadence-server --env development_xdc_cluster0 ... to set up three cadence --ad 127.0.0.1:7933 --env development --do domain0 domain register --ac cluster0 --gd true --clusters cluster0 cluster1 # global domain required cadence --ad 127.0.0.1:7933 --env development --do domain1 domain register --ac cluster1 --gd true --clusters cluster0 cluster1 cadence --ad 127.0.0.1:7933 --env development --do domain2 domain register --ac cluster0 --gd true --clusters cluster0 cluster1 crossdomain-run: crossdomain tmux split-window -h './bin/crossdomain -m "worker0"' \; \ split-window -v './bin/crossdomain -m "worker1"' \; \ split-window -v './bin/crossdomain -m "worker2"' sideeffect: go build -o bin/sideeffect cmd/samples/recipes/sideeffect/*.go versioning: go build -o bin/versioning cmd/samples/recipes/versioning/*.go autoscaling-monitoring: go build -o bin/autoscaling-monitoring cmd/samples/advanced/autoscaling-monitoring/*.go run-generators: @echo "Running generators in new_samples..." @for dir in new_samples/*/generator; do \ if [ -d "$$dir" ]; then \ echo "Running generator in $$dir"; \ (cd $$dir && go run .); \ fi; \ done @echo "All generators completed" test: bins @rm -f test @rm -f test.log @echo $(TEST_DIRS) @for dir in $(TEST_DIRS); do \ go test -coverprofile=$@ "$$dir" | tee -a test.log; \ done; clean: rm -rf bin rm -Rf $(BUILD) bins: helloworld \ versioning \ delaystart \ branch \ crossdomain \ childworkflow \ choice \ dynamic \ greetings \ pickfirst \ mutex \ cancelactivity \ retryactivity \ splitmerge \ searchattributes \ timer \ cron \ tracing \ dsl \ fileprocessing \ expense_dummy \ expense \ localactivity \ query \ consistentquery \ recovery \ ctxpropagation \ pso \ signalcounter \ sideeffect \ sleep \ autoscaling-monitoring \ ================================================ FILE: NOTICE ================================================ Copyright (c) 2025 Uber Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Cadence Samples ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/uber-common/cadence-samples/build.yml) Welcome to the Cadence Samples repository! This collection demonstrates the powerful capabilities of Cadence workflow orchestration through practical, real-world examples. Whether you're new to Cadence or looking to implement specific patterns, these samples will help you understand how to build reliable, scalable, and maintainable workflow applications. ## What is Cadence? Cadence is a distributed, scalable, durable, and highly available orchestration engine that helps developers build reliable applications. It provides: - **Reliability**: Automatic retry mechanisms, error handling, and fault tolerance - **Scalability**: Distributed execution across multiple workers - **Durability**: Persistent workflow state that survives failures - **Observability**: Built-in monitoring, tracing, and querying capabilities Learn more about Cadence at: - [Documentation](https://cadenceworkflow.io) - [Cadence Server](https://github.com/cadence-workflow/cadence) - [Cadence Go Client](https://github.com/cadence-workflow/cadence-go-client) - [CNCF Slack](https://communityinviter.com/apps/cloud-native/cncf) - Join **#cadence-users** channel on CNCF Slack ## 🚀 Quick Start ### Prerequisites 1. **Clone the Repository**: ```bash git clone https://github.com/uber-common/cadence-samples.git && cd cadence-samples ``` 2. **Start Cadence Server** ```bash curl -LO https://raw.githubusercontent.com/cadence-workflow/cadence/refs/heads/master/docker/docker-compose.yml && docker-compose up --wait ``` This downloads and starts all required dependencies including Cadence server, database, and [Cadence Web UI](https://github.com/uber/cadence-web). You can view your sample workflows at [http://localhost:8088](http://localhost:8088). 3. **Build All Samples**: ```bash make ``` ### Docker Troubleshooting The `docker-compose` command requires Docker daemon to be running. On macOS/Windows, open Docker Desktop. On Linux, run `sudo systemctl start docker`.
Port conflicts If you see `Bind for 0.0.0.0: failed: port is already allocated`, find the process using that port: ```bash lsof -i tcp: ``` To find which container is using it: ```bash docker ps --format '{{.ID}}\t{{.Ports}}\t{{.Names}}' | grep ``` Stop and remove the conflicting container: ```bash docker stop && docker rm ``` Cadence uses these ports: `7833`, `7933`, `7939`, `8000-8003`, `8088` (Web UI), `9042` (Cassandra), `9090` (Prometheus), `3000` (Grafana).
Reset everything ```bash docker-compose down docker ps -a # check for leftover containers docker rm # remove if needed ``` Verify with `docker ps` and visit [http://localhost:8088](http://localhost:8088).
## 📚 Sample Categories ### Table of Contents - [🎯 Basic Examples](#-basic-examples) - [Hello World](#hello-world) - [Greetings](#greetings) - [Cron](#cron) - [Timer](#timer) - [Delay Start](#delay-start) - [Branch](#branch) - [Split-Merge](#split-merge) - [Pick First](#pick-first) - [🔧 Advanced Examples](#-advanced-examples) - [Choice](#choice) - [Retry Activity](#retry-activity) - [Cancel Activity](#cancel-activity) - [Mutex](#mutex) - [Query](#query) - [Consistent Query](#consistent-query) - [Child Workflow](#child-workflow) - [Dynamic](#dynamic) - [Local Activity](#local-activity) - [Versioning](#versioning) - [Search Attributes](#search-attributes) - [Context Propagation](#context-propagation) - [Tracing](#tracing) - [Side Effect](#side-effect) - [Recovery](#recovery) - [🏢 Business Application Examples](#-business-application-examples) - [Expense](#expense) - [File Processing](#file-processing) - [DSL](#dsl) - [PSO (Particle Swarm Optimization)](#pso-particle-swarm-optimization) --- ### 🎯 **Basic Examples** #### Hello World * **Shows**: Basic Cadence workflow concepts and activity execution. * **What it does**: Executes a single activity that returns a greeting message. * **Real-world use case**: Foundation for understanding workflow structure, activity execution, and basic error handling. * **Key concepts**: Workflow definition, activity execution, error handling, worker setup. * **Source code**: [cmd/samples/recipes/helloworld/](cmd/samples/recipes/helloworld/) ##### How to run Start Worker: ```bash ./bin/helloworld -m worker ``` Start Workflow: ```bash ./bin/helloworld -m trigger ``` #### Greetings * **Shows**: Sequential activity execution and result passing between activities. * **What it does**: Executes three activities in sequence: get greeting, get name, then combine them. * **Real-world use case**: Multi-step processes like user registration, order processing, or data transformation pipelines. * **Key concepts**: Sequential execution, activity chaining, result passing between activities. * **Source code**: [cmd/samples/recipes/greetings/](cmd/samples/recipes/greetings/) ##### How to run Start Worker: ```bash ./bin/greetings -m worker ``` Start Workflow: ```bash ./bin/greetings -m trigger ``` #### Cron * **Shows**: Automated recurring tasks and cron scheduling. * **What it does**: Executes a workflow based on cron expressions (e.g., every minute, daily at 2 AM). * **Real-world use case**: Data backups, report generation, system maintenance, periodic data synchronization. * **Key concepts**: Cron scheduling, workflow persistence, time-based execution. * **Source code**: [cmd/samples/cron/](cmd/samples/cron/) ##### How to run Start Worker: ```bash ./bin/cron -m worker ``` Start Workflow: ```bash ./bin/cron -m trigger -cron "* * * * *" # Run every minute ``` #### Timer * **Shows**: Timeout and delay handling with parallel execution. * **What it does**: Starts a long-running process and sends a notification if it takes too long. * **Real-world use case**: Order processing with SLA monitoring, payment processing with timeout alerts, API calls with fallback mechanisms. * **Key concepts**: Timer creation, timeout handling, parallel execution with cancellation. * **Source code**: [cmd/samples/recipes/timer/](cmd/samples/recipes/timer/) ##### How to run Start Worker: ```bash ./bin/timer -m worker ``` Start Workflow: ```bash ./bin/timer -m trigger ``` #### Delay Start * **Shows**: Deferred execution and delayed workflow execution. * **What it does**: Waits for a specified duration before executing the main workflow logic. * **Real-world use case**: Scheduled maintenance windows, delayed notifications, batch processing at specific times. * **Key concepts**: Delayed execution, time-based workflow scheduling. * **Source code**: [cmd/samples/recipes/delaystart/](cmd/samples/recipes/delaystart/) ##### How to run Start Worker: ```bash ./bin/delaystart -m worker ``` Start Workflow: ```bash ./bin/delaystart -m trigger ``` ### 🔄 **Parallel Execution Examples** #### Branch * **Shows**: Parallel activity execution and concurrent activity management. * **What it does**: Executes multiple activities in parallel and waits for all to complete. * **Real-world use case**: Processing multiple orders simultaneously, calling multiple APIs in parallel, batch data processing. * **Key concepts**: Parallel execution, Future handling, concurrent activity management. * **Source code**: [cmd/samples/recipes/branch/](cmd/samples/recipes/branch/) ##### How to run Start Worker: ```bash ./bin/branch -m worker ``` Start Single Branch Workflow: ```bash ./bin/branch -m trigger -c branch ``` Start Parallel Branch Workflow: ```bash ./bin/branch -m trigger -c parallel ``` #### Split-Merge * **Shows**: Divide and conquer pattern with parallel processing. * **What it does**: Splits a large task into chunks, processes them in parallel, then merges results. * **Real-world use case**: Large file processing, batch data analysis, image/video processing, ETL pipelines. * **Key concepts**: Work splitting, parallel processing, result aggregation, worker coordination. * **Source code**: [cmd/samples/recipes/splitmerge/](cmd/samples/recipes/splitmerge/) ##### How to run Start Worker: ```bash ./bin/splitmerge -m worker ``` Start Workflow: ```bash ./bin/splitmerge -m trigger ``` #### Pick First * **Shows**: Race condition handling and activity cancellation. * **What it does**: Runs multiple activities in parallel and uses the result from whichever completes first. * **Real-world use case**: Multi-provider API calls, redundant service calls, failover mechanisms, load balancing. * **Key concepts**: Parallel execution, cancellation, race condition handling. * **Source code**: [cmd/samples/recipes/pickfirst/](cmd/samples/recipes/pickfirst/) ##### How to run Start Worker: ```bash ./bin/pickfirst -m worker ``` Start Workflow: ```bash ./bin/pickfirst -m trigger ``` ### 🔧 **Advanced Examples** #### Choice * **Shows**: Conditional execution and decision-based activity routing. * **What it does**: Executes different activities based on the result of a decision activity. * **Real-world use case**: Order routing based on type, user authentication flows, approval workflows, conditional processing. * **Key concepts**: Conditional logic, decision trees, workflow branching. * **Source code**: [cmd/samples/recipes/choice/](cmd/samples/recipes/choice/) ##### How to run Start Worker: ```bash ./bin/choice -m worker ``` Start Single Choice Workflow: ```bash ./bin/choice -m trigger -c single ``` Start Multi-Choice Workflow: ```bash ./bin/choice -m trigger -c multi ``` #### Retry Activity * **Shows**: Resilient processing with retry policies and heartbeat tracking. * **What it does**: Demonstrates activity retry policies with heartbeat progress tracking. * **Real-world use case**: API calls with intermittent failures, database operations, external service integration. * **Key concepts**: Retry policies, heartbeat mechanisms, progress tracking, failure recovery. * **Source code**: [cmd/samples/recipes/retryactivity/](cmd/samples/recipes/retryactivity/) ##### How to run Start Worker: ```bash ./bin/retryactivity -m worker ``` Start Workflow: ```bash ./bin/retryactivity -m trigger ``` #### Cancel Activity * **Shows**: Graceful cancellation and cleanup operations. * **What it does**: Shows how to cancel running activities and perform cleanup operations. * **Real-world use case**: User-initiated cancellations, timeout handling, resource cleanup, emergency stops. * **Key concepts**: Cancellation handling, cleanup operations, graceful shutdown. * **Source code**: [cmd/samples/recipes/cancelactivity/](cmd/samples/recipes/cancelactivity/) ##### How to run Start Worker: ```bash ./bin/cancelactivity -m worker ``` Start Workflow: ```bash ./bin/cancelactivity -m trigger ``` **Cancel Workflow:** ```bash ./bin/cancelactivity -m cancel -w ``` #### Mutex * **Shows**: Resource locking and distributed locking patterns. * **What it does**: Ensures only one workflow can access a specific resource at a time. * **Real-world use case**: Database migrations, configuration updates, resource allocation, critical section protection. * **Key concepts**: Distributed locking, resource coordination, mutual exclusion. * **Source code**: [cmd/samples/recipes/mutex/](cmd/samples/recipes/mutex/) ##### How to run * Check **[Detailed Guide](cmd/samples/recipes/mutex/README.md)** to run the sample #### Query * **Shows**: Workflow state inspection and custom query handlers. * **What it does**: Demonstrates custom query handlers to inspect workflow state. * **Real-world use case**: Progress monitoring, status dashboards, debugging running workflows, user interfaces. * **Key concepts**: Query handlers, state inspection, workflow monitoring. * **Source code**: [cmd/samples/recipes/query/](cmd/samples/recipes/query/) ##### How to run * Check **[Detailed Guide](cmd/samples/recipes/query/README.md)** to run the sample #### Consistent Query * **Shows**: Consistent state queries and signal handling. * **What it does**: Shows how to query workflow state consistently while handling signals. * **Real-world use case**: Real-time dashboards, progress tracking, state synchronization. * **Key concepts**: Consistent queries, signal handling, state management. * **Source code**: [cmd/samples/recipes/consistentquery/](cmd/samples/recipes/consistentquery/) ##### How to run * Check **[Detailed Guide](cmd/samples/recipes/consistentquery/README.md)** to run the sample #### Child Workflow * **Shows**: Workflow composition and parent-child workflow relationships. * **What it does**: Demonstrates parent-child workflow relationships with ContinueAsNew pattern. * **Real-world use case**: Complex business processes, workflow decomposition, modular workflow design. * **Key concepts**: Child workflows, ContinueAsNew, workflow composition. * **Source code**: [cmd/samples/recipes/childworkflow/](cmd/samples/recipes/childworkflow/) ##### How to run Start Worker: ```bash ./bin/childworkflow -m worker ``` Start Workflow: ```bash ./bin/childworkflow -m trigger ``` #### Dynamic * **Shows**: Dynamic activity invocation and string-based execution. * **What it does**: Demonstrates calling activities using string names for dynamic behavior. * **Real-world use case**: Plugin systems, dynamic workflow composition, configuration-driven workflows. * **Key concepts**: Dynamic activity invocation, string-based execution, flexible workflow design. * **Source code**: [cmd/samples/recipes/dynamic/](cmd/samples/recipes/dynamic/) ##### How to run Start Worker: ```bash ./bin/dynamic -m worker ``` Start Workflow: ```bash ./bin/dynamic -m trigger ``` #### Local Activity * **Shows**: High-performance local execution and lightweight operations. * **What it does**: Shows how to use local activities for quick operations that don't need external execution. * **Real-world use case**: Data validation, simple calculations, condition checking, fast decision making. * **Key concepts**: Local activities, performance optimization, lightweight operations. * **Source code**: [cmd/samples/recipes/localactivity/](cmd/samples/recipes/localactivity/) ##### How to run * Check **[Detailed Guide](cmd/samples/recipes/localactivity/README.md)** to run the sample #### Versioning * **Shows**: Safe workflow evolution and backward compatibility. * **What it does**: Shows workflow versioning with backward compatibility and safe rollbacks. * **Real-world use case**: Production deployments, feature rollouts, backward compatibility, safe migrations. * **Key concepts**: Workflow versioning, backward compatibility, safe deployments. * **Source code**: [cmd/samples/recipes/versioning/](cmd/samples/recipes/versioning/) ##### How to run * Check **[Detailed Guide](cmd/samples/recipes/versioning/README.md)** to run the sample #### Search Attributes * **Shows**: Workflow indexing and search for workflow discovery. * **What it does**: Shows how to add searchable attributes to workflows and query them. * **Real-world use case**: Workflow discovery, filtering, reporting, operational dashboards. * **Key concepts**: Search attributes, workflow indexing, ElasticSearch integration. * **Source code**: [cmd/samples/recipes/searchattributes/](cmd/samples/recipes/searchattributes/) ##### How to run * Check **[Detailed Guide](cmd/samples/recipes/searchattributes/README.md)** to run the sample #### Context Propagation * **Shows**: Cross-workflow context and context propagation. * **What it does**: Demonstrates passing context (like user info, trace IDs) through workflow execution. * **Real-world use case**: Distributed tracing, user context propagation, audit trails, debugging. * **Key concepts**: Context propagation, distributed tracing, cross-service context. * **Source code**: [cmd/samples/recipes/ctxpropagation/](cmd/samples/recipes/ctxpropagation/) ##### How to run * Check **[Detailed Guide](cmd/samples/recipes/ctxpropagation/README.md)** to run the sample #### Tracing * **Shows**: Distributed tracing and integration with tracing systems. * **What it does**: Shows how to add distributed tracing to Cadence workflows. * **Real-world use case**: Performance monitoring, debugging, observability, APM integration. * **Key concepts**: Distributed tracing, Jaeger integration, observability. * **Source code**: [cmd/samples/recipes/tracing/](cmd/samples/recipes/tracing/) ##### How to run Start Worker: ```bash ./bin/tracing -m worker ``` Start Workflow: ```bash ./bin/tracing -m trigger ``` #### Side Effect * **Shows**: Non-deterministic operations and replay safety. * **What it does**: Demonstrates the SideEffect API for handling non-deterministic operations. * **Real-world use case**: ID generation, random number generation, external state queries. * **Key concepts**: Side effects, non-deterministic operations, replay safety. * **Source code**: [cmd/samples/recipes/sideeffect/](cmd/samples/recipes/sideeffect/) ##### How to run Start Workflow: ```bash ./bin/sideeffect ``` #### Recovery * **Shows**: Workflow recovery and failure handling. * **What it does**: Shows how to restart failed workflows and replay signals. * **Real-world use case**: Disaster recovery, workflow repair, system restoration. * **Key concepts**: Workflow recovery, signal replay, failure handling. * **Source code**: [cmd/samples/recovery/](cmd/samples/recovery/) ##### How to run * Check **[Detailed Guide](cmd/samples/recovery/README.md)** to run the sample ### 🏢 **Business Application Examples** #### Expense * **Shows**: Human-in-the-loop workflows and approval workflows. * **What it does**: Creates an expense report, waits for approval, then processes payment. * **Real-world use case**: Expense approval, purchase orders, document review, approval workflows. * **Key concepts**: Human-in-the-loop, async completion, approval workflows. * **Source code**: [cmd/samples/expense/](cmd/samples/expense/) ##### How to run * Check **[Detailed Guide](cmd/samples/expense/README.md)** to run the sample #### File Processing * **Shows**: Distributed file processing across multiple hosts. * **What it does**: Downloads, processes, and uploads files with host-specific execution. * **Real-world use case**: Large file processing, ETL pipelines, media processing, data transformation. * **Key concepts**: File processing, host-specific execution, session management, retry policies. * **Source code**: [cmd/samples/fileprocessing/](cmd/samples/fileprocessing/) ##### How to run * Check **[Detailed Guide](cmd/samples/fileprocessing/README.md)** to run the sample #### DSL * **Shows**: Domain-specific language and custom workflow language creation. * **What it does**: Implements a simple DSL for defining workflows using YAML configuration. * **Real-world use case**: Business user workflow definition, configuration-driven workflows, workflow templates. * **Key concepts**: DSL implementation, YAML parsing, dynamic workflow creation. * **Source code**: [cmd/samples/dsl/](cmd/samples/dsl/) ##### How to run * Check **[Detailed Guide](cmd/samples/dsl/README.md)** to run the sample #### PSO (Particle Swarm Optimization) * **Shows**: Complex mathematical workflows and long-running optimization workflows. * **What it does**: Implements particle swarm optimization with child workflows and ContinueAsNew. * **Real-world use case**: Mathematical optimization, machine learning training, complex calculations. * **Key concepts**: Long-running workflows, ContinueAsNew, child workflows, custom data converters. * **Source code**: [cmd/samples/pso/](cmd/samples/pso/) ##### How to run * Check **[Detailed Guide](cmd/samples/pso/README.md)** to run the sample ## 🛠 **Development & Testing** ### Building Samples ```bash make ``` ### Running Tests ```bash # Run all tests go test ./... # Run specific sample tests go test ./cmd/samples/recipes/helloworld/ ``` ### Worker Modes Most samples support these modes: - `worker`: Start a worker to handle workflow execution - `trigger`: Start a new workflow execution - `query`: Query a running workflow (where applicable) - `signal`: Send a signal to a workflow (where applicable) ## 🤝 **Contributing** We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. ## 📄 **License** Apache 2.0 License - see [LICENSE](LICENSE) for details. ## 🆘 **Getting Help** - **Documentation**: [Cadence Documentation](https://cadenceworkflow.io/docs/) - **Community**: [Cadence Community](https://cadenceworkflow.io/community/) - **Issues**: [GitHub Issues](https://github.com/uber-common/cadence-samples/issues) --- **Happy Workflowing! 🚀** ================================================ FILE: cmd/samples/advanced/autoscaling-monitoring/Makefile ================================================ # Makefile for autoscaling monitoring sample .PHONY: build clean test # Build the autoscaling monitoring sample build: go build -o ../../../../bin/autoscaling-monitoring *.go # Clean build artifacts clean: rm -f ../../../../bin/autoscaling-monitoring # Run tests test: go test -v . # Install dependencies deps: go mod tidy # Run the sample in different modes run-worker: build ../../../../bin/autoscaling-monitoring -m worker run-trigger: build ../../../../bin/autoscaling-monitoring -m trigger ================================================ FILE: cmd/samples/advanced/autoscaling-monitoring/README.md ================================================ # Autoscaling Monitoring Sample This sample demonstrates three advanced Cadence worker features: 1. **Worker Poller Autoscaling** - Dynamic adjustment of worker poller goroutines based on workload 2. **Integrated Prometheus Metrics** - Real-time metrics collection using Tally with Prometheus reporter 3. **Autoscaling Metrics** - Comprehensive autoscaling behavior metrics exposed via HTTP endpoint ## Features ### Worker Poller Autoscaling The worker uses `worker.NewV2` with `AutoScalerOptions` to enable true autoscaling behavior: - **AutoScalerOptions.Enabled**: true - Enables the autoscaling feature - **PollerMinCount**: 2 - Minimum number of poller goroutines - **PollerMaxCount**: 8 - Maximum number of poller goroutines - **PollerInitCount**: 4 - Initial number of poller goroutines The worker automatically adjusts the number of poller goroutines between the min and max values based on the current workload. ### Prometheus Metrics The sample uses Tally with Prometheus reporter to expose comprehensive metrics: - **Real-time autoscaling metrics** - Poller count changes, quota adjustments, wait times - **Worker performance metrics** - Task processing rates, poller utilization, queue depths - **Standard Cadence metrics** - All metrics automatically emitted by the Cadence Go client - **Sanitized metric names** - Prometheus-compatible metric names and labels ### Monitoring Dashboards When running the Cadence server locally with Grafana, you can access the client dashboards at: **Client Dashboards**: http://localhost:3000/d/dehkspwgabvuoc/cadence-client > **Note**: Make sure to select a Domain in Grafana for the dashboards to display data. The dashboards will be empty until a domain is selected from the dropdown. ## Prerequisites 1. **Cadence Server**: Running locally with Docker Compose. 2. **Prometheus**: Configured to scrape metrics from the sample. 3. **Grafana**: With Cadence dashboards (included with default Cadence server setup). Dashboards in the latest version of the server. ## Quick Start ### 1. Start the Worker ```bash ./bin/autoscaling-monitoring -m worker ``` The worker automatically exposes metrics at: http://127.0.0.1:8004/metrics ### 2. Generate Load ```bash ./bin/autoscaling-monitoring -m trigger ``` ## Configuration The sample uses a custom configuration system that extends the base Cadence configuration. You can specify a configuration file using the `-config` flag: ```bash ./bin/autoscaling-monitoring -m worker -config /path/to/config.yaml ``` ### Configuration File Structure ```yaml # Cadence connection settings domain: "default" service: "cadence-frontend" host: "localhost:7833" # Prometheus configuration prometheus: listenAddress: "127.0.0.1:8004" # Autoscaling configuration autoscaling: # Worker autoscaling settings pollerMinCount: 2 pollerMaxCount: 8 pollerInitCount: 4 # Load generation settings loadGeneration: # Workflow-level settings workflows: 10 # Number of workflows to start workflowDelay: 1000 # Delay between starting workflows (milliseconds) # Activity-level settings (per workflow) activitiesPerWorkflow: 30 # Number of activities per workflow batchDelay: 2000 # Delay between activity batches within workflow (milliseconds) # Activity processing time range (milliseconds) minProcessingTime: 1000 maxProcessingTime: 6000 ``` ### Configuration Usage The configuration values are used throughout the sample: 1. **Worker Configuration** (`worker_config.go`): - `pollerMinCount`, `pollerMaxCount`, `pollerInitCount` → `AutoScalerOptions` 2. **Workflow Configuration** (`workflow.go`): - `activitiesPerWorkflow` → Number of activities to execute per workflow - `batchDelay` → Delay between activity batches within workflow 3. **Activity Configuration** (`activities.go`): - `minProcessingTime`, `maxProcessingTime` → Activity processing time range 4. **Prometheus Configuration** (integrated): - `listenAddress` → Metrics endpoint port (default: 127.0.0.1:8004) ### Default Configuration If no configuration file is provided or if the file cannot be read, the sample uses these defaults: ```yaml domain: "default" service: "cadence-frontend" host: "localhost:7833" prometheus: listenAddress: "127.0.0.1:8004" autoscaling: pollerMinCount: 2 pollerMaxCount: 8 pollerInitCount: 4 loadGeneration: workflows: 10 workflowDelay: 1000 activitiesPerWorkflow: 30 batchDelay: 2000 minProcessingTime: 1000 maxProcessingTime: 6000 ``` ### Load Pattern Examples The sample supports various load patterns for testing autoscaling behavior: #### **1. Gradual Ramp-up (Default)** ```yaml loadGeneration: workflows: 10 workflowDelay: 1000 activitiesPerWorkflow: 30 ``` **Result**: 10 workflows starting 1 second apart, each with 30 activities (300 total activities) #### **2. Burst Load** ```yaml loadGeneration: workflows: 25 workflowDelay: 0 activitiesPerWorkflow: 60 ``` **Result**: 25 workflows all starting immediately (1500 total activities) #### **3. Sustained Load** ```yaml loadGeneration: workflows: 50 workflowDelay: 2000 activitiesPerWorkflow: 100 ``` **Result**: 5 long-running workflows with 2-second delays between starts (5000 total activities) #### **4. Light Load** ```yaml loadGeneration: workflows: 1 workflowDelay: 0 activitiesPerWorkflow: 20 ``` **Result**: Single workflow with 20 activities for minimal load testing ## Monitoring ### Metrics Endpoints - **Prometheus Metrics**: http://127.0.0.1:8004/metrics - Exposed automatically when running worker mode only - Real-time autoscaling and worker performance metrics - Prometheus-compatible format with sanitized names - **Note**: Metrics server is not started in trigger mode ### Grafana Dashboard Access the Cadence client dashboard at: http://localhost:3000/d/dehkspwgabvuoc/cadence-client ### Key Metrics to Monitor 1. **Worker Performance Metrics**: - `cadence_worker_decision_poll_success_count` - Successful decision task polls - `cadence_worker_activity_poll_success_count` - Successful activity task polls - `cadence_worker_decision_poll_count` - Total decision task poll attempts - `cadence_worker_activity_poll_count` - Total activity task poll attempts 2. **Autoscaling Behavior Metrics**: - `cadence_worker_poller_count` - Number of active poller goroutines (key autoscaling indicator) - `cadence_concurrency_auto_scaler_poller_quota` - Current poller quota for autoscaling - `cadence_concurrency_auto_scaler_poller_wait_time` - Time pollers wait for tasks - `cadence_concurrency_auto_scaler_scale_up_count` - Number of scale-up events - `cadence_concurrency_auto_scaler_scale_down_count` - Number of scale-down events ## How It Works ### Load Generation The sample creates multiple workflows that execute activities in parallel, with each workflow: - Starting with configurable delays (`workflowDelay`) to create sustained load patterns - Executing a configurable number of activities (`activitiesPerWorkflow`) per workflow - Each activity taking 1-6 seconds to complete (configurable via `minProcessingTime`/`maxProcessingTime`) - Recording metrics about execution time - Creating varying load patterns with configurable batch delays within each workflow ### Autoscaling Demonstration The worker uses `worker.NewV2` with `AutoScalerOptions` to: - Start with configurable poller goroutines (`pollerInitCount`) - Scale down to minimum pollers (`pollerMinCount`) when load is low - Scale up to maximum pollers (`pollerMaxCount`) when load is high - Automatically adjust based on task queue depth and processing time ### Metrics Collection The sample uses Tally with Prometheus reporter for comprehensive metrics: - **Real-time autoscaling metrics** - Poller count changes, quota adjustments, scale events - **Worker performance metrics** - Task processing rates, poller utilization, queue depths - **Standard Cadence metrics** - All metrics automatically emitted by the Cadence Go client - **Sanitized metric names** - Prometheus-compatible format with proper character replacement ## Production Considerations ### Scaling - Adjust `pollerMinCount`, `pollerMaxCount`, and `pollerInitCount` based on your workload - Monitor worker performance and adjust autoscaling parameters - Use multiple worker instances for high availability ### Monitoring - Configure Prometheus to scrape metrics regularly (latest version of Cadence server is configured to do this) - Set up alerts for worker performance issues - Use Grafana dashboards to visualize autoscaling behavior - Monitor poller count changes to verify autoscaling is working ### Security - Secure the Prometheus endpoint in production - Use authentication for metrics access - Consider using HTTPS for metrics endpoints ## Testing The sample includes unit tests for the configuration loading functionality. Run these tests if you make any changes to the config: ### Running Tests ```bash # Run all tests go test -v # Run specific test go test -v -run TestLoadConfiguration_SuccessfulLoading # Run tests with coverage go test -v -cover ``` ### Test Coverage The tests cover: - **Successful configuration loading** - Complete YAML files with all fields - **Missing file fallback** - Graceful handling when config file doesn't exist - **Default value application** - Ensuring all fields have sensible defaults ### Configuration Testing The tests validate that the improved configuration system: - Handles embedded struct issues properly - Applies defaults correctly for missing fields - Provides clear error messages for configuration problems - Maintains backward compatibility ## Troubleshooting ### Common Issues 1. **Worker Not Starting**: - Check Cadence server is running - Verify domain exists - Check configuration file - Ensure using compatible Cadence client version 2. **Autoscaling Not Working**: - Verify `worker.NewV2` is being used - Check `AutoScalerOptions.Enabled` is true - Monitor poller count changes in logs - Ensure sufficient load is being generated 3. **Configuration Issues**: - Verify configuration file path is correct - Check YAML syntax in configuration file - Review default values if config file is not found 4. **Metrics Not Appearing**: - Verify worker is running (metrics are exposed automatically) - Check metrics endpoint is accessible: http://127.0.0.1:8004/metrics - Ensure Prometheus is configured to scrape the endpoint - Check for metric name sanitization issues 5. **Dashboard Not Loading**: - Verify Grafana is running - Check dashboard URL is correct - Ensure Prometheus data source is configured ================================================ FILE: cmd/samples/advanced/autoscaling-monitoring/activities.go ================================================ package main import ( "context" "math/rand" "time" "go.uber.org/cadence/activity" "go.uber.org/zap" ) const ( loadGenerationActivityName = "loadGenerationActivity" ) // LoadGenerationActivity simulates work that can be scaled // It includes random delays to simulate real-world processing time func LoadGenerationActivity(ctx context.Context, taskID int, minProcessingTime, maxProcessingTime int) error { startTime := time.Now() logger := activity.GetLogger(ctx) logger.Info("Load generation activity started", zap.Int("taskID", taskID)) // Simulate variable processing time using configuration values processingTime := time.Duration(rand.Intn(maxProcessingTime - minProcessingTime) + minProcessingTime) * time.Millisecond time.Sleep(processingTime) duration := time.Since(startTime) logger.Info("Load generation activity completed", zap.Int("taskID", taskID), zap.Duration("processingTime", processingTime), zap.Duration("totalDuration", duration)) return nil } ================================================ FILE: cmd/samples/advanced/autoscaling-monitoring/config/autoscaling.yaml ================================================ # Configuration for autoscaling monitoring sample domain: "default" service: "cadence-frontend" host: "localhost:7833" # Prometheus configuration for metrics collection prometheus: listenAddress: "127.0.0.1:8004" # Autoscaling configuration # These settings control the worker's concurrency and autoscaling behavior autoscaling: # Worker autoscaling settings pollerMinCount: 2 pollerMaxCount: 8 pollerInitCount: 4 # Worker load simulation settings loadGeneration: # Workflow-level settings workflows: 10 # Number of workflows to start workflowDelay: 1000 # Delay between starting workflows (milliseconds) # Activity-level settings (per workflow) activitiesPerWorkflow: 30 # Number of activities per workflow batchDelay: 750 # Delay between activity batches within workflow (milliseconds) # Activity processing time range (milliseconds) minProcessingTime: 1000 maxProcessingTime: 6000 ================================================ FILE: cmd/samples/advanced/autoscaling-monitoring/config.go ================================================ package main import ( "fmt" "os" "github.com/uber-common/cadence-samples/cmd/samples/common" "github.com/uber-go/tally/prometheus" "gopkg.in/yaml.v3" ) // AutoscalingConfiguration uses a flattened structure to avoid embedded struct issues type AutoscalingConfiguration struct { // Base configuration fields (explicit, not embedded) DomainName string `yaml:"domain"` ServiceName string `yaml:"service"` HostNameAndPort string `yaml:"host"` Prometheus *prometheus.Configuration `yaml:"prometheus"` // Autoscaling-specific fields Autoscaling AutoscalingSettings `yaml:"autoscaling"` } // AutoscalingSettings contains the autoscaling configuration type AutoscalingSettings struct { // Worker autoscaling settings PollerMinCount int `yaml:"pollerMinCount"` PollerMaxCount int `yaml:"pollerMaxCount"` PollerInitCount int `yaml:"pollerInitCount"` // Load generation settings LoadGeneration LoadGenerationSettings `yaml:"loadGeneration"` } // LoadGenerationSettings contains the load generation configuration type LoadGenerationSettings struct { // Workflow-level settings Workflows int `yaml:"workflows"` WorkflowDelay int `yaml:"workflowDelay"` // Activity-level settings (per workflow) ActivitiesPerWorkflow int `yaml:"activitiesPerWorkflow"` BatchDelay int `yaml:"batchDelay"` MinProcessingTime int `yaml:"minProcessingTime"` MaxProcessingTime int `yaml:"maxProcessingTime"` } // Default values as constants for easy maintenance const ( DefaultDomainName = "default" DefaultServiceName = "cadence-frontend" DefaultHostNameAndPort = "localhost:7833" DefaultPrometheusAddr = "127.0.0.1:8004" DefaultPollerMinCount = 2 DefaultPollerMaxCount = 8 DefaultPollerInitCount = 4 DefaultWorkflows = 10 DefaultWorkflowDelay = 1000 DefaultActivitiesPerWorkflow = 30 DefaultBatchDelay = 2000 DefaultMinProcessingTime = 1000 DefaultMaxProcessingTime = 6000 ) // DefaultAutoscalingConfiguration returns default configuration func DefaultAutoscalingConfiguration() AutoscalingConfiguration { return AutoscalingConfiguration{ DomainName: DefaultDomainName, ServiceName: DefaultServiceName, HostNameAndPort: DefaultHostNameAndPort, Prometheus: &prometheus.Configuration{ ListenAddress: DefaultPrometheusAddr, }, Autoscaling: AutoscalingSettings{ PollerMinCount: DefaultPollerMinCount, PollerMaxCount: DefaultPollerMaxCount, PollerInitCount: DefaultPollerInitCount, LoadGeneration: LoadGenerationSettings{ Workflows: DefaultWorkflows, WorkflowDelay: DefaultWorkflowDelay, ActivitiesPerWorkflow: DefaultActivitiesPerWorkflow, BatchDelay: DefaultBatchDelay, MinProcessingTime: DefaultMinProcessingTime, MaxProcessingTime: DefaultMaxProcessingTime, }, }, } } // loadConfiguration loads the autoscaling configuration from file func loadConfiguration(configFile string) AutoscalingConfiguration { // Start with defaults config := DefaultAutoscalingConfiguration() // Read config file configData, err := os.ReadFile(configFile) if err != nil { fmt.Printf("Failed to read config file: %v, using defaults\n", err) return config } // Unmarshal into the config struct if err := yaml.Unmarshal(configData, &config); err != nil { fmt.Printf("Error parsing configuration: %v, using defaults\n", err) return DefaultAutoscalingConfiguration() } // Apply defaults for any missing fields config.applyDefaults() return config } // applyDefaults ensures all fields have sensible values func (c *AutoscalingConfiguration) applyDefaults() { // Base configuration defaults if c.DomainName == "" { c.DomainName = DefaultDomainName } if c.ServiceName == "" { c.ServiceName = DefaultServiceName } if c.HostNameAndPort == "" { c.HostNameAndPort = DefaultHostNameAndPort } if c.Prometheus == nil { c.Prometheus = &prometheus.Configuration{ ListenAddress: DefaultPrometheusAddr, } } // Autoscaling defaults if c.Autoscaling.PollerMinCount == 0 { c.Autoscaling.PollerMinCount = DefaultPollerMinCount } if c.Autoscaling.PollerMaxCount == 0 { c.Autoscaling.PollerMaxCount = DefaultPollerMaxCount } if c.Autoscaling.PollerInitCount == 0 { c.Autoscaling.PollerInitCount = DefaultPollerInitCount } // Load generation defaults if c.Autoscaling.LoadGeneration.Workflows == 0 { c.Autoscaling.LoadGeneration.Workflows = DefaultWorkflows } if c.Autoscaling.LoadGeneration.WorkflowDelay == 0 { c.Autoscaling.LoadGeneration.WorkflowDelay = DefaultWorkflowDelay } if c.Autoscaling.LoadGeneration.ActivitiesPerWorkflow == 0 { c.Autoscaling.LoadGeneration.ActivitiesPerWorkflow = DefaultActivitiesPerWorkflow } if c.Autoscaling.LoadGeneration.BatchDelay == 0 { c.Autoscaling.LoadGeneration.BatchDelay = DefaultBatchDelay } if c.Autoscaling.LoadGeneration.MinProcessingTime == 0 { c.Autoscaling.LoadGeneration.MinProcessingTime = DefaultMinProcessingTime } if c.Autoscaling.LoadGeneration.MaxProcessingTime == 0 { c.Autoscaling.LoadGeneration.MaxProcessingTime = DefaultMaxProcessingTime } } // ToCommonConfiguration converts to the common.Configuration type for compatibility func (c *AutoscalingConfiguration) ToCommonConfiguration() common.Configuration { return common.Configuration{ DomainName: c.DomainName, ServiceName: c.ServiceName, HostNameAndPort: c.HostNameAndPort, Prometheus: c.Prometheus, } } ================================================ FILE: cmd/samples/advanced/autoscaling-monitoring/config_test.go ================================================ package main import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Test the improved configuration loader for regressions func TestLoadConfiguration_SuccessfulLoading(t *testing.T) { // Create a temporary configuration file with all fields populated configContent := ` domain: "test-domain" service: "test-service" host: "test-host:7833" prometheus: listenAddress: "127.0.0.1:9000" autoscaling: pollerMinCount: 3 pollerMaxCount: 10 pollerInitCount: 5 loadGeneration: workflows: 10 workflowDelay: 1000 activitiesPerWorkflow: 30 batchDelay: 5 minProcessingTime: 2000 maxProcessingTime: 8000 ` // Create temporary file tmpFile, err := os.CreateTemp("", "test-config-*.yaml") require.NoError(t, err) defer os.Remove(tmpFile.Name()) _, err = tmpFile.WriteString(configContent) require.NoError(t, err) tmpFile.Close() // Load configuration config := loadConfiguration(tmpFile.Name()) // Validate all fields are populated correctly assert.Equal(t, "test-domain", config.DomainName) assert.Equal(t, "test-service", config.ServiceName) assert.Equal(t, "test-host:7833", config.HostNameAndPort) require.NotNil(t, config.Prometheus) assert.Equal(t, "127.0.0.1:9000", config.Prometheus.ListenAddress) assert.Equal(t, 3, config.Autoscaling.PollerMinCount) assert.Equal(t, 10, config.Autoscaling.PollerMaxCount) assert.Equal(t, 5, config.Autoscaling.PollerInitCount) assert.Equal(t, 10, config.Autoscaling.LoadGeneration.Workflows) assert.Equal(t, 1000, config.Autoscaling.LoadGeneration.WorkflowDelay) assert.Equal(t, 30, config.Autoscaling.LoadGeneration.ActivitiesPerWorkflow) assert.Equal(t, 5, config.Autoscaling.LoadGeneration.BatchDelay) assert.Equal(t, 2000, config.Autoscaling.LoadGeneration.MinProcessingTime) assert.Equal(t, 8000, config.Autoscaling.LoadGeneration.MaxProcessingTime) } func TestLoadConfiguration_MissingFileFallback(t *testing.T) { // Use a non-existent file path config := loadConfiguration("/non/existent/path/config.yaml") // Validate that default configuration is returned assert.Equal(t, DefaultDomainName, config.DomainName) assert.Equal(t, DefaultServiceName, config.ServiceName) assert.Equal(t, DefaultHostNameAndPort, config.HostNameAndPort) assert.Equal(t, DefaultPollerMinCount, config.Autoscaling.PollerMinCount) assert.Equal(t, DefaultPollerMaxCount, config.Autoscaling.PollerMaxCount) assert.Equal(t, DefaultPollerInitCount, config.Autoscaling.PollerInitCount) assert.Equal(t, DefaultWorkflows, config.Autoscaling.LoadGeneration.Workflows) assert.Equal(t, DefaultWorkflowDelay, config.Autoscaling.LoadGeneration.WorkflowDelay) assert.Equal(t, DefaultActivitiesPerWorkflow, config.Autoscaling.LoadGeneration.ActivitiesPerWorkflow) assert.Equal(t, DefaultBatchDelay, config.Autoscaling.LoadGeneration.BatchDelay) assert.Equal(t, DefaultMinProcessingTime, config.Autoscaling.LoadGeneration.MinProcessingTime) assert.Equal(t, DefaultMaxProcessingTime, config.Autoscaling.LoadGeneration.MaxProcessingTime) } func TestDefaultAutoscalingConfiguration(t *testing.T) { config := DefaultAutoscalingConfiguration() // Validate all default values assert.Equal(t, DefaultDomainName, config.DomainName) assert.Equal(t, DefaultServiceName, config.ServiceName) assert.Equal(t, DefaultHostNameAndPort, config.HostNameAndPort) require.NotNil(t, config.Prometheus) assert.Equal(t, DefaultPrometheusAddr, config.Prometheus.ListenAddress) assert.Equal(t, DefaultPollerMinCount, config.Autoscaling.PollerMinCount) assert.Equal(t, DefaultPollerMaxCount, config.Autoscaling.PollerMaxCount) assert.Equal(t, DefaultPollerInitCount, config.Autoscaling.PollerInitCount) assert.Equal(t, DefaultWorkflows, config.Autoscaling.LoadGeneration.Workflows) assert.Equal(t, DefaultWorkflowDelay, config.Autoscaling.LoadGeneration.WorkflowDelay) assert.Equal(t, DefaultActivitiesPerWorkflow, config.Autoscaling.LoadGeneration.ActivitiesPerWorkflow) assert.Equal(t, DefaultBatchDelay, config.Autoscaling.LoadGeneration.BatchDelay) assert.Equal(t, DefaultMinProcessingTime, config.Autoscaling.LoadGeneration.MinProcessingTime) assert.Equal(t, DefaultMaxProcessingTime, config.Autoscaling.LoadGeneration.MaxProcessingTime) } func TestApplyDefaults(t *testing.T) { // Test with empty configuration config := AutoscalingConfiguration{} config.applyDefaults() // Validate that all defaults are applied assert.Equal(t, DefaultDomainName, config.DomainName) assert.Equal(t, DefaultServiceName, config.ServiceName) assert.Equal(t, DefaultHostNameAndPort, config.HostNameAndPort) require.NotNil(t, config.Prometheus) assert.Equal(t, DefaultPrometheusAddr, config.Prometheus.ListenAddress) assert.Equal(t, DefaultPollerMinCount, config.Autoscaling.PollerMinCount) assert.Equal(t, DefaultPollerMaxCount, config.Autoscaling.PollerMaxCount) assert.Equal(t, DefaultPollerInitCount, config.Autoscaling.PollerInitCount) assert.Equal(t, DefaultWorkflows, config.Autoscaling.LoadGeneration.Workflows) assert.Equal(t, DefaultWorkflowDelay, config.Autoscaling.LoadGeneration.WorkflowDelay) assert.Equal(t, DefaultActivitiesPerWorkflow, config.Autoscaling.LoadGeneration.ActivitiesPerWorkflow) assert.Equal(t, DefaultBatchDelay, config.Autoscaling.LoadGeneration.BatchDelay) assert.Equal(t, DefaultMinProcessingTime, config.Autoscaling.LoadGeneration.MinProcessingTime) assert.Equal(t, DefaultMaxProcessingTime, config.Autoscaling.LoadGeneration.MaxProcessingTime) } ================================================ FILE: cmd/samples/advanced/autoscaling-monitoring/main.go ================================================ package main import ( "flag" "fmt" "net/http" "os" "path/filepath" "time" "github.com/pborman/uuid" "go.uber.org/cadence/client" "github.com/uber-common/cadence-samples/cmd/samples/common" "github.com/uber-go/tally" "github.com/uber-go/tally/prometheus" "go.uber.org/zap" ) const ( ApplicationName = "autoscaling-monitoring" ) // findConfigFile finds the config file relative to the executable location func findConfigFile() string { // Get the directory where the executable is located execPath, err := os.Executable() if err != nil { // Fallback to current working directory if we can't determine executable path return "config/autoscaling.yaml" } execDir := filepath.Dir(execPath) // Try to find the config file relative to the executable // The executable is in bin/, so we need to go up to the repo root and then to the config configPath := filepath.Join(execDir, "..", "cmd", "samples", "advanced", "autoscaling-monitoring", "config", "autoscaling.yaml") // Check if the config file exists at this path if _, err := os.Stat(configPath); err == nil { return configPath } // Fallback to the original relative path (for development when running with go run) return "config/autoscaling.yaml" } func main() { // Parse command line arguments var mode string var configFile string flag.StringVar(&mode, "m", "worker", "Mode: worker or trigger") flag.StringVar(&configFile, "config", "", "Path to configuration file") flag.Parse() // Load configuration if configFile == "" { configFile = findConfigFile() } config := loadConfiguration(configFile) // Setup common helper with our configuration var h common.SampleHelper h.Config = config.ToCommonConfiguration() // Set up logging logger, err := zap.NewDevelopment() if err != nil { panic(fmt.Sprintf("Failed to setup logger: %v", err)) } h.Logger = logger // Set up service client using our config h.Builder = common.NewBuilder(logger). SetHostPort(config.HostNameAndPort). SetDomain(config.DomainName) service, err := h.Builder.BuildServiceClient() if err != nil { panic(fmt.Sprintf("Failed to build service client: %v", err)) } h.Service = service // Set up metrics scope with Tally Prometheus reporter var ( safeCharacters = []rune{'_'} sanitizeOptions = tally.SanitizeOptions{ NameCharacters: tally.ValidCharacters{ Ranges: tally.AlphanumericRange, Characters: safeCharacters, }, KeyCharacters: tally.ValidCharacters{ Ranges: tally.AlphanumericRange, Characters: safeCharacters, }, ValueCharacters: tally.ValidCharacters{ Ranges: tally.AlphanumericRange, Characters: safeCharacters, }, ReplacementCharacter: tally.DefaultReplacementCharacter, } ) // Create Prometheus reporter reporter := prometheus.NewReporter(prometheus.Options{}) // Create root scope with proper options scope, closer := tally.NewRootScope(tally.ScopeOptions{ Tags: map[string]string{"service": "autoscaling-monitoring"}, SanitizeOptions: &sanitizeOptions, CachedReporter: reporter, }, 10) defer closer.Close() // Set up metrics scope for helper h.WorkerMetricScope = scope h.ServiceMetricScope = scope switch mode { case "worker": // Start metrics server only in worker mode if config.Prometheus != nil { go func() { http.Handle("/metrics", reporter.HTTPHandler()) logger.Info("Starting Prometheus metrics server", zap.String("port", config.Prometheus.ListenAddress)) if err := http.ListenAndServe(config.Prometheus.ListenAddress, nil); err != nil { logger.Error("Failed to start metrics server", zap.Error(err)) } }() } startWorkers(&h, &config) case "trigger": startWorkflow(&h, &config) default: fmt.Printf("Unknown mode: %s\n", mode) os.Exit(1) } } func startWorkers(h *common.SampleHelper, config *AutoscalingConfiguration) { startWorkersWithAutoscaling(h, config) } func startWorkflow(h *common.SampleHelper, config *AutoscalingConfiguration) { workflowOptions := client.StartWorkflowOptions{ ID: fmt.Sprintf("autoscaling_%s", uuid.New()), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute * 10, DecisionTaskStartToCloseTimeout: time.Minute, } // Use configuration values workflows := config.Autoscaling.LoadGeneration.Workflows workflowDelay := config.Autoscaling.LoadGeneration.WorkflowDelay activitiesPerWorkflow := config.Autoscaling.LoadGeneration.ActivitiesPerWorkflow batchDelay := config.Autoscaling.LoadGeneration.BatchDelay minProcessingTime := config.Autoscaling.LoadGeneration.MinProcessingTime maxProcessingTime := config.Autoscaling.LoadGeneration.MaxProcessingTime // Start multiple workflows with delays for i := 0; i < workflows; i++ { workflowOptions.ID = fmt.Sprintf("autoscaling_%d_%s", i, uuid.New()) h.StartWorkflow(workflowOptions, autoscalingWorkflowName, activitiesPerWorkflow, batchDelay, minProcessingTime, maxProcessingTime) // Add delay between workflows (except for the last one) if i < workflows-1 { time.Sleep(time.Duration(workflowDelay) * time.Millisecond) } } fmt.Printf("Started %d autoscaling workflows with %d activities each\n", workflows, activitiesPerWorkflow) fmt.Println("Monitor the worker performance and autoscaling behavior in Grafana:") fmt.Println("http://localhost:3000/d/dehkspwgabvuoc/cadence-client") } ================================================ FILE: cmd/samples/advanced/autoscaling-monitoring/worker_config.go ================================================ package main import ( "go.uber.org/cadence/activity" "go.uber.org/cadence/client" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/zap" "github.com/uber-common/cadence-samples/cmd/samples/common" ) // startWorkersWithAutoscaling starts workers with autoscaling configuration func startWorkersWithAutoscaling(h *common.SampleHelper, config *AutoscalingConfiguration) { // Configure worker options with autoscaling-friendly settings from config workerOptions := worker.Options{ MetricsScope: h.WorkerMetricScope, Logger: h.Logger, AutoScalerOptions: worker.AutoScalerOptions{ Enabled: true, PollerMinCount: config.Autoscaling.PollerMinCount, PollerMaxCount: config.Autoscaling.PollerMaxCount, PollerInitCount: config.Autoscaling.PollerInitCount, }, FeatureFlags: client.FeatureFlags{ WorkflowExecutionAlreadyCompletedErrorEnabled: true, }, } h.Logger.Info("Starting workers with autoscaling configuration", zap.Bool("AutoScalerEnabled", workerOptions.AutoScalerOptions.Enabled), zap.Int("PollerMinCount", workerOptions.AutoScalerOptions.PollerMinCount), zap.Int("PollerMaxCount", workerOptions.AutoScalerOptions.PollerMaxCount), zap.Int("PollerInitCount", workerOptions.AutoScalerOptions.PollerInitCount)) // Use worker.NewV2 for autoscaling support w, err := worker.NewV2(h.Service, h.Config.DomainName, ApplicationName, workerOptions) if err != nil { h.Logger.Fatal("Failed to create worker with autoscaling", zap.Error(err)) } // Register workflows and activities registerWorkflowAndActivityForAutoscaling(w) // Start the worker err = w.Run() if err != nil { h.Logger.Fatal("Failed to run worker", zap.Error(err)) } } // registerWorkflowAndActivityForAutoscaling registers the workflow and activities func registerWorkflowAndActivityForAutoscaling(w worker.Worker) { w.RegisterWorkflowWithOptions(AutoscalingWorkflow, workflow.RegisterOptions{Name: autoscalingWorkflowName}) w.RegisterActivityWithOptions(LoadGenerationActivity, activity.RegisterOptions{Name: loadGenerationActivityName}) } ================================================ FILE: cmd/samples/advanced/autoscaling-monitoring/workflow.go ================================================ package main import ( "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) const ( autoscalingWorkflowName = "autoscalingWorkflow" ) // AutoscalingWorkflow demonstrates a workflow that can generate load // to test worker poller autoscaling func AutoscalingWorkflow(ctx workflow.Context, activitiesPerWorkflow int, batchDelay int, minProcessingTime, maxProcessingTime int) error { logger := workflow.GetLogger(ctx) logger.Info("Autoscaling workflow started", zap.Int("activitiesPerWorkflow", activitiesPerWorkflow)) ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute * 20, StartToCloseTimeout: time.Minute * 20, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) // Generate load by executing activities in parallel var futures []workflow.Future // Execute activities in batches to create varying load for i := 0; i < activitiesPerWorkflow; i++ { future := workflow.ExecuteActivity(ctx, LoadGenerationActivity, i, minProcessingTime, maxProcessingTime) futures = append(futures, future) // Add some delay between batches to simulate real-world patterns // Use batch delay from configuration if i > 0 && i % 10 == 0 { workflow.Sleep(ctx, time.Duration(batchDelay)*time.Millisecond) } } // Wait for all activities to complete for i, future := range futures { var result error if err := future.Get(ctx, &result); err != nil { logger.Error("Activity failed", zap.Int("taskID", i), zap.Error(err)) return err } } logger.Info("Autoscaling workflow completed", zap.Int("totalActivities", len(futures))) return nil } ================================================ FILE: cmd/samples/common/factory.go ================================================ package common import ( "errors" "github.com/opentracing/opentracing-go" "github.com/uber-go/tally" apiv1 "github.com/uber/cadence-idl/go/proto/api/v1" "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" "go.uber.org/cadence/client" "go.uber.org/cadence/compatibility" "go.uber.org/cadence/encoded" "go.uber.org/cadence/workflow" "go.uber.org/yarpc" "go.uber.org/yarpc/transport/grpc" "go.uber.org/zap" ) const ( _cadenceClientName = "cadence-client" _cadenceFrontendService = "cadence-frontend" ) // WorkflowClientBuilder build client to cadence service type WorkflowClientBuilder struct { hostPort string dispatcher *yarpc.Dispatcher domain string clientIdentity string metricsScope tally.Scope Logger *zap.Logger ctxProps []workflow.ContextPropagator dataConverter encoded.DataConverter tracer opentracing.Tracer } // NewBuilder creates a new WorkflowClientBuilder func NewBuilder(logger *zap.Logger) *WorkflowClientBuilder { return &WorkflowClientBuilder{ Logger: logger, } } // SetHostPort sets the hostport for the builder func (b *WorkflowClientBuilder) SetHostPort(hostport string) *WorkflowClientBuilder { b.hostPort = hostport return b } // SetDomain sets the domain for the builder func (b *WorkflowClientBuilder) SetDomain(domain string) *WorkflowClientBuilder { b.domain = domain return b } // SetClientIdentity sets the identity for the builder func (b *WorkflowClientBuilder) SetClientIdentity(identity string) *WorkflowClientBuilder { b.clientIdentity = identity return b } // SetMetricsScope sets the metrics scope for the builder func (b *WorkflowClientBuilder) SetMetricsScope(metricsScope tally.Scope) *WorkflowClientBuilder { b.metricsScope = metricsScope return b } // SetDispatcher sets the dispatcher for the builder func (b *WorkflowClientBuilder) SetDispatcher(dispatcher *yarpc.Dispatcher) *WorkflowClientBuilder { b.dispatcher = dispatcher return b } // SetContextPropagators sets the context propagators for the builder func (b *WorkflowClientBuilder) SetContextPropagators(ctxProps []workflow.ContextPropagator) *WorkflowClientBuilder { b.ctxProps = ctxProps return b } // SetDataConverter sets the data converter for the builder func (b *WorkflowClientBuilder) SetDataConverter(dataConverter encoded.DataConverter) *WorkflowClientBuilder { b.dataConverter = dataConverter return b } // SetTracer sets the tracer for the builder func (b *WorkflowClientBuilder) SetTracer(tracer opentracing.Tracer) *WorkflowClientBuilder { b.tracer = tracer return b } // BuildCadenceClient builds a client to cadence service func (b *WorkflowClientBuilder) BuildCadenceClient() (client.Client, error) { service, err := b.BuildServiceClient() if err != nil { return nil, err } return client.NewClient( service, b.domain, &client.Options{ Identity: b.clientIdentity, MetricsScope: b.metricsScope, DataConverter: b.dataConverter, ContextPropagators: b.ctxProps, Tracer: b.tracer, FeatureFlags: client.FeatureFlags{ WorkflowExecutionAlreadyCompletedErrorEnabled: true, }, }), nil } // BuildCadenceDomainClient builds a domain client to cadence service func (b *WorkflowClientBuilder) BuildCadenceDomainClient() (client.DomainClient, error) { service, err := b.BuildServiceClient() if err != nil { return nil, err } return client.NewDomainClient( service, &client.Options{ Identity: b.clientIdentity, MetricsScope: b.metricsScope, ContextPropagators: b.ctxProps, FeatureFlags: client.FeatureFlags{ WorkflowExecutionAlreadyCompletedErrorEnabled: true, }, }, ), nil } // BuildServiceClient builds a rpc service client to cadence service func (b *WorkflowClientBuilder) BuildServiceClient() (workflowserviceclient.Interface, error) { if err := b.build(); err != nil { return nil, err } if b.dispatcher == nil { b.Logger.Fatal("No RPC dispatcher provided to create a connection to Cadence Service") } clientConfig := b.dispatcher.ClientConfig(_cadenceFrontendService) return compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ), nil } func (b *WorkflowClientBuilder) build() error { if b.dispatcher != nil { return nil } if len(b.hostPort) == 0 { return errors.New("HostPort is empty") } b.Logger.Debug("Creating RPC dispatcher outbound", zap.String("ServiceName", _cadenceFrontendService), zap.String("HostPort", b.hostPort)) b.dispatcher = yarpc.NewDispatcher(yarpc.Config{ Name: _cadenceClientName, Outbounds: yarpc.Outbounds{ _cadenceFrontendService: {Unary: grpc.NewTransport().NewSingleOutbound(b.hostPort)}, }, }) if b.dispatcher != nil { if err := b.dispatcher.Start(); err != nil { b.Logger.Fatal("Failed to create outbound transport channel: %v", zap.Error(err)) } } return nil } ================================================ FILE: cmd/samples/common/sample_helper.go ================================================ package common import ( "context" "fmt" "io/ioutil" "time" "github.com/opentracing/opentracing-go" "go.uber.org/cadence/.gen/go/shared" prom "github.com/m3db/prometheus_client_golang/prometheus" "github.com/uber-go/tally" "github.com/uber-go/tally/prometheus" "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" "go.uber.org/cadence/activity" "go.uber.org/cadence/client" "go.uber.org/cadence/encoded" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/zap" "gopkg.in/yaml.v2" ) const ( defaultConfigFile = "config/development.yaml" ) type ( // SampleHelper class for workflow sample helper. SampleHelper struct { Service workflowserviceclient.Interface WorkerMetricScope tally.Scope ServiceMetricScope tally.Scope Logger *zap.Logger Config Configuration Builder *WorkflowClientBuilder DataConverter encoded.DataConverter CtxPropagators []workflow.ContextPropagator workflowRegistries []registryOption activityRegistries []registryOption Tracer opentracing.Tracer configFile string } // Configuration for running samples. Configuration struct { DomainName string `yaml:"domain"` ServiceName string `yaml:"service"` HostNameAndPort string `yaml:"host"` Prometheus *prometheus.Configuration `yaml:"prometheus"` } registryOption struct { registry interface{} alias string } ) var ( safeCharacters = []rune{'_'} sanitizeOptions = tally.SanitizeOptions{ NameCharacters: tally.ValidCharacters{ Ranges: tally.AlphanumericRange, Characters: safeCharacters, }, KeyCharacters: tally.ValidCharacters{ Ranges: tally.AlphanumericRange, Characters: safeCharacters, }, ValueCharacters: tally.ValidCharacters{ Ranges: tally.AlphanumericRange, Characters: safeCharacters, }, ReplacementCharacter: tally.DefaultReplacementCharacter, } ) // SetConfigFile sets the config file path func (h *SampleHelper) SetConfigFile(configFile string) { h.configFile = configFile } // SetupServiceConfig setup the config for the sample code run func (h *SampleHelper) SetupServiceConfig() { if h.Service != nil { return } if h.configFile == "" { h.configFile = defaultConfigFile } // Initialize developer config for running samples configData, err := ioutil.ReadFile(h.configFile) if err != nil { panic(fmt.Sprintf("Failed to log config file: %v, Error: %v", defaultConfigFile, err)) } if err := yaml.Unmarshal(configData, &h.Config); err != nil { panic(fmt.Sprintf("Error initializing configuration: %v", err)) } // Initialize logger for running samples logger, err := zap.NewDevelopment() if err != nil { panic(err) } logger.Info("Logger created.") h.Logger = logger h.ServiceMetricScope = tally.NoopScope h.WorkerMetricScope = tally.NoopScope if h.Config.Prometheus != nil { reporter, err := h.Config.Prometheus.NewReporter( prometheus.ConfigurationOptions{ Registry: prom.NewRegistry(), OnError: func(err error) { logger.Warn("error in prometheus reporter", zap.Error(err)) }, }, ) if err != nil { panic(err) } h.WorkerMetricScope, _ = tally.NewRootScope(tally.ScopeOptions{ Prefix: "Worker_", Tags: map[string]string{}, CachedReporter: reporter, Separator: prometheus.DefaultSeparator, SanitizeOptions: &sanitizeOptions, }, 1*time.Second) // NOTE: this must be a different scope with different prefix, otherwise the metric will conflict h.ServiceMetricScope, _ = tally.NewRootScope(tally.ScopeOptions{ Prefix: "Service_", Tags: map[string]string{}, CachedReporter: reporter, Separator: prometheus.DefaultSeparator, SanitizeOptions: &sanitizeOptions, }, 1*time.Second) } h.Builder = NewBuilder(logger). SetHostPort(h.Config.HostNameAndPort). SetDomain(h.Config.DomainName). SetMetricsScope(h.ServiceMetricScope). SetDataConverter(h.DataConverter). SetTracer(h.Tracer). SetContextPropagators(h.CtxPropagators) service, err := h.Builder.BuildServiceClient() if err != nil { panic(err) } h.Service = service domainClient, _ := h.Builder.BuildCadenceDomainClient() _, err = domainClient.Describe(context.Background(), h.Config.DomainName) if err != nil { logger.Info("Domain doesn't exist", zap.String("Domain", h.Config.DomainName), zap.Error(err)) } else { logger.Info("Domain successfully registered.", zap.String("Domain", h.Config.DomainName)) } h.workflowRegistries = make([]registryOption, 0, 1) h.activityRegistries = make([]registryOption, 0, 1) } // StartWorkflow starts a workflow func (h *SampleHelper) StartWorkflow( options client.StartWorkflowOptions, workflow interface{}, args ...interface{}, ) *workflow.Execution { return h.StartWorkflowWithCtx(context.Background(), options, workflow, args...) } // StartWorkflowWithCtx starts a workflow with the provided context func (h *SampleHelper) StartWorkflowWithCtx( ctx context.Context, options client.StartWorkflowOptions, workflow interface{}, args ...interface{}, ) *workflow.Execution { workflowClient, err := h.Builder.BuildCadenceClient() if err != nil { h.Logger.Error("Failed to build cadence client.", zap.Error(err)) panic(err) } we, err := workflowClient.StartWorkflow(ctx, options, workflow, args...) if err != nil { h.Logger.Error("Failed to create workflow", zap.Error(err)) panic("Failed to create workflow.") } else { h.Logger.Info("Started Workflow", zap.String("WorkflowID", we.ID), zap.String("RunID", we.RunID)) return we } } // SignalWithStartWorkflowWithCtx signals workflow and starts it if it's not yet started func (h *SampleHelper) SignalWithStartWorkflowWithCtx(ctx context.Context, workflowID string, signalName string, signalArg interface{}, options client.StartWorkflowOptions, workflow interface{}, workflowArgs ...interface{}) *workflow.Execution { workflowClient, err := h.Builder.BuildCadenceClient() if err != nil { h.Logger.Error("Failed to build cadence client.", zap.Error(err)) panic(err) } we, err := workflowClient.SignalWithStartWorkflow(ctx, workflowID, signalName, signalArg, options, workflow, workflowArgs...) if err != nil { h.Logger.Error("Failed to signal with start workflow", zap.Error(err)) panic("Failed to signal with start workflow.") } else { h.Logger.Info("Signaled and started Workflow", zap.String("WorkflowID", we.ID), zap.String("RunID", we.RunID)) } return we } func (h *SampleHelper) RegisterWorkflow(workflow interface{}) { h.RegisterWorkflowWithAlias(workflow, "") } func (h *SampleHelper) RegisterWorkflowWithAlias(workflow interface{}, alias string) { registryOption := registryOption{ registry: workflow, alias: alias, } h.workflowRegistries = append(h.workflowRegistries, registryOption) } func (h *SampleHelper) RegisterActivity(activity interface{}) { h.RegisterActivityWithAlias(activity, "") } func (h *SampleHelper) RegisterActivityWithAlias(activity interface{}, alias string) { registryOption := registryOption{ registry: activity, alias: alias, } h.activityRegistries = append(h.activityRegistries, registryOption) } // StartWorkers starts workflow worker and activity worker based on configured options. func (h *SampleHelper) StartWorkers(domainName string, groupName string, options worker.Options) worker.Worker { worker := worker.New(h.Service, domainName, groupName, options) h.registerWorkflowAndActivity(worker) err := worker.Start() if err != nil { h.Logger.Error("Failed to start workers.", zap.Error(err)) panic("Failed to start workers") } return worker } func (h *SampleHelper) QueryWorkflow(workflowID, runID, queryType string, args ...interface{}) { workflowClient, err := h.Builder.BuildCadenceClient() if err != nil { h.Logger.Error("Failed to build cadence client.", zap.Error(err)) panic(err) } resp, err := workflowClient.QueryWorkflow(context.Background(), workflowID, runID, queryType, args...) if err != nil { h.Logger.Error("Failed to query workflow", zap.Error(err)) panic("Failed to query workflow.") } var result interface{} if err := resp.Get(&result); err != nil { h.Logger.Error("Failed to decode query result", zap.Error(err)) } h.Logger.Info("Received query result", zap.Any("Result", result)) } func (h *SampleHelper) ConsistentQueryWorkflow( valuePtr interface{}, workflowID, runID, queryType string, args ...interface{}, ) error { workflowClient, err := h.Builder.BuildCadenceClient() if err != nil { h.Logger.Error("Failed to build cadence client.", zap.Error(err)) panic(err) } resp, err := workflowClient.QueryWorkflowWithOptions(context.Background(), &client.QueryWorkflowWithOptionsRequest{ WorkflowID: workflowID, RunID: runID, QueryType: queryType, QueryConsistencyLevel: shared.QueryConsistencyLevelStrong.Ptr(), Args: args, }) if err != nil { h.Logger.Error("Failed to query workflow", zap.Error(err)) panic("Failed to query workflow.") } if err := resp.QueryResult.Get(&valuePtr); err != nil { h.Logger.Error("Failed to decode query result", zap.Error(err)) } h.Logger.Info("Received consistent query result.", zap.Any("Result", valuePtr)) return err } func (h *SampleHelper) SignalWorkflow(workflowID, signal string, data interface{}) { workflowClient, err := h.Builder.BuildCadenceClient() if err != nil { h.Logger.Error("Failed to build cadence client.", zap.Error(err)) panic(err) } err = workflowClient.SignalWorkflow(context.Background(), workflowID, "", signal, data) if err != nil { h.Logger.Error("Failed to signal workflow", zap.Error(err)) panic("Failed to signal workflow.") } } func (h *SampleHelper) CancelWorkflow(workflowID string) { workflowClient, err := h.Builder.BuildCadenceClient() if err != nil { h.Logger.Error("Failed to build cadence client.", zap.Error(err)) panic(err) } err = workflowClient.CancelWorkflow(context.Background(), workflowID, "") if err != nil { h.Logger.Error("Failed to cancel workflow", zap.Error(err)) panic("Failed to cancel workflow.") } } func (h *SampleHelper) registerWorkflowAndActivity(worker worker.Worker) { for _, w := range h.workflowRegistries { if len(w.alias) == 0 { worker.RegisterWorkflow(w.registry) } else { worker.RegisterWorkflowWithOptions(w.registry, workflow.RegisterOptions{Name: w.alias}) } } for _, act := range h.activityRegistries { if len(act.alias) == 0 { worker.RegisterActivity(act.registry) } else { worker.RegisterActivityWithOptions(act.registry, activity.RegisterOptions{Name: act.alias}) } } } ================================================ FILE: cmd/samples/common/util.go ================================================ package common // StringPtr returns pointer to a string func StringPtr(v string) *string { return &v } // Int32Ptr returns pointer to a int32 func Int32Ptr(v int32) *int32 { return &v } // Int64Ptr returns pointer to a int64 func Int64Ptr(v int64) *int64 { return &v } ================================================ FILE: cmd/samples/cron/cron_workflow.go ================================================ package main import ( "context" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This cron sample workflow will schedule job based on given schedule spec. The schedule spec in this sample demo is * very simple, but you could have more complicated scheduler logic that meet your needs. */ const ( // timeout for activity task from put in queue to started activityScheduleToStartTimeout = time.Second * 10 // timeout for activity from start to complete activityStartToCloseTimeout = time.Minute // WorkflowStartToCloseTimeout (from workflow start to workflow close) WorkflowStartToCloseTimeout = time.Minute * 20 // DecisionTaskStartToCloseTimeout (from decision task started to decision task completed, usually very short) DecisionTaskStartToCloseTimeout = time.Second * 10 ) // // Cron sample job activity. // func sampleCronActivity(ctx context.Context, beginTime, endTime time.Time) error { activity.GetLogger(ctx).Info("Cron job running.", zap.Time("beginTime_exclude", beginTime), zap.Time("endTime_include", endTime)) // ... return nil } // SampleCronResult used to return data from one cron run to next cron run. type SampleCronResult struct { EndTime time.Time } // sampleCronWorkflow workflow decider func sampleCronWorkflow(ctx workflow.Context) (*SampleCronResult, error) { workflow.GetLogger(ctx).Info("Cron workflow started.", zap.Time("StartTime", workflow.Now(ctx))) ao := workflow.ActivityOptions{ ScheduleToStartTimeout: activityScheduleToStartTimeout, StartToCloseTimeout: activityStartToCloseTimeout, } ctx1 := workflow.WithActivityOptions(ctx, ao) startTime := time.Time{} // start from 0 time for first cron job if workflow.HasLastCompletionResult(ctx) { var lastResult SampleCronResult if err := workflow.GetLastCompletionResult(ctx, &lastResult); err == nil { startTime = lastResult.EndTime } } endTime := workflow.Now(ctx) err := workflow.ExecuteActivity(ctx1, sampleCronActivity, startTime, endTime).Get(ctx, nil) if err != nil { // cron job failed. but next cron should continue to be scheduled by Cadence server workflow.GetLogger(ctx).Error("Cron job failed.", zap.Error(err)) return nil, err } return &SampleCronResult{EndTime: endTime}, nil } ================================================ FILE: cmd/samples/cron/cron_workflow_test.go ================================================ package main import ( "context" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" "go.uber.org/cadence/workflow" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(sampleCronWorkflow) s.env.RegisterActivity(sampleCronActivity) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_CronWorkflow() { testWorkflow := func(ctx workflow.Context) error { ctx1 := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{ ExecutionStartToCloseTimeout: time.Minute * 10, CronSchedule: "0 * * * *", // hourly }) cronFuture := workflow.ExecuteChildWorkflow(ctx1, sampleCronWorkflow) // cron never stop so this future won't return // wait 2 hours for the cron (cron will execute 3 times) workflow.Sleep(ctx, time.Hour*2) s.False(cronFuture.IsReady()) return nil } s.env.RegisterWorkflow(testWorkflow) s.env.OnActivity(sampleCronActivity, mock.Anything, mock.Anything, mock.Anything).Return(nil).Times(3) var startTimeList, endTimeList []time.Time s.env.SetOnActivityStartedListener(func(activityInfo *activity.Info, ctx context.Context, args encoded.Values) { var startTime, endTime time.Time err := args.Get(&startTime, &endTime) s.NoError(err) startTimeList = append(startTimeList, startTime) endTimeList = append(endTimeList, endTime) }) startTime, _ := time.Parse(time.RFC3339, "2018-12-20T16:30:00-80:00") s.env.SetStartTime(startTime) s.env.ExecuteWorkflow(testWorkflow) s.True(s.env.IsWorkflowCompleted()) err := s.env.GetWorkflowError() s.NoError(err) s.Equal(3, len(startTimeList)) s.True(startTimeList[0].Equal(time.Time{})) s.True(endTimeList[0].Equal(startTime)) s.True(startTimeList[1].Equal(startTime)) s.True(endTimeList[1].Equal(startTime.Add(time.Minute * 30))) s.True(startTimeList[2].Equal(startTime.Add(time.Minute * 30))) s.True(endTimeList[2].Equal(startTime.Add(time.Minute * 90))) } ================================================ FILE: cmd/samples/cron/main.go ================================================ 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" ) const ( // ApplicationName is the task list for this sample ApplicationName = "cronGroup" ) // 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) } // // To start instance of the workflow. // func startWorkflow(h *common.SampleHelper, cron string) { // This workflow ID can be user business logic identifier as well. workflowID := "cron_" + uuid.New() workflowOptions := client.StartWorkflowOptions{ ID: workflowID, TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, CronSchedule: cron, } h.StartWorkflow(workflowOptions, sampleCronWorkflow) } func main() { var mode string var cron string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.StringVar(&cron, "cron", "* * * * *", "Crontab schedule. Default \"* * * * *\"") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleCronWorkflow) h.RegisterActivity(sampleCronActivity) 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, cron) } } ================================================ FILE: cmd/samples/dsl/README.md ================================================ This sample demonstrates how to implement a DSL workflow. In this sample, we provide 2 sample yaml files each defines a custom workflow that can be processed by this dsl workflow sample code. Steps to run this sample: 1) You need a cadence service running. See cmd/samples/README.md for more details. 2) Run "./bin/dsl -m worker" to start workers for dsl workflow. 3) Run "./bin/dsl -dslConfig cmd/samples/dsl/workflow1.yaml" to submit start request for workflow defined in workflow1.yaml file. Next: 1) You can replace the dslConfig to workflow2.yaml to see the result. 2) You can also write your own yaml config to play with it. 3) You can replace the dummy activities to your own real activities to build real workflow based on this simple dsl workflow. ================================================ FILE: cmd/samples/dsl/activities.go ================================================ package main import ( "fmt" ) func sampleActivity1(input []string) (string, error) { name := "sampleActivity1" fmt.Printf("Run %s with input %v \n", name, input) return "Result_" + name, nil } func sampleActivity2(input []string) (string, error) { name := "sampleActivity2" fmt.Printf("Run %s with input %v \n", name, input) return "Result_" + name, nil } func sampleActivity3(input []string) (string, error) { name := "sampleActivity3" fmt.Printf("Run %s with input %v \n", name, input) return "Result_" + name, nil } func sampleActivity4(input []string) (string, error) { name := "sampleActivity4" fmt.Printf("Run %s with input %v \n", name, input) return "Result_" + name, nil } func sampleActivity5(input []string) (string, error) { name := "sampleActivity5" fmt.Printf("Run %s with input %v \n", name, input) return "Result_" + name, nil } ================================================ FILE: cmd/samples/dsl/main.go ================================================ package main import ( "flag" "fmt" "io/ioutil" "time" "github.com/pborman/uuid" "go.uber.org/cadence/client" "go.uber.org/cadence/worker" "gopkg.in/yaml.v2" "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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper, w Workflow) { workflowOptions := client.StartWorkflowOptions{ ID: "dsl_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, simpleDSLWorkflow, w) } func main() { var mode, dslConfig string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.StringVar(&dslConfig, "dslConfig", "cmd/samples/dsl/workflow1.yaml", "dslConfig specify the yaml file for the dsl workflow.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(simpleDSLWorkflow) h.RegisterActivity(sampleActivity1) h.RegisterActivity(sampleActivity2) h.RegisterActivity(sampleActivity3) h.RegisterActivity(sampleActivity4) h.RegisterActivity(sampleActivity5) 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": data, err := ioutil.ReadFile(dslConfig) if err != nil { panic(fmt.Sprintf("failed to load dsl config file %v", err)) } var workflow Workflow if err := yaml.Unmarshal(data, &workflow); err != nil { panic(fmt.Sprintf("failed to unmarshal dsl config %v", err)) } startWorkflow(&h, workflow) } } ================================================ FILE: cmd/samples/dsl/workflow.go ================================================ package main import ( "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) // ApplicationName is the task list for this sample const ApplicationName = "dslGroup" type ( // Workflow is the type used to express the workflow definition. Variables are a map of valuables. Variables can be // used as input to Activity. Workflow struct { Variables map[string]string Root Statement } // Statement is the building block of dsl workflow. A Statement can be a simple ActivityInvocation or it // could be a Sequence or Parallel. Statement struct { Activity *ActivityInvocation Sequence *Sequence Parallel *Parallel } // Sequence consist of a collection of Statements that runs in sequential. Sequence struct { Elements []*Statement } // Parallel can be a collection of Statements that runs in parallel. Parallel struct { Branches []*Statement } // ActivityInvocation is used to express invoking an Activity. The Arguments defined expected arguments as input to // the Activity, the result specify the name of variable that it will store the result as which can then be used as // arguments to subsequent ActivityInvocation. ActivityInvocation struct { Name string Arguments []string Result string } executable interface { execute(ctx workflow.Context, bindings map[string]string) error } ) // simpleDSLWorkflow workflow decider func simpleDSLWorkflow(ctx workflow.Context, dslWorkflow Workflow) ([]byte, error) { bindings := make(map[string]string) for k, v := range dslWorkflow.Variables { bindings[k] = v } ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) err := dslWorkflow.Root.execute(ctx, bindings) if err != nil { logger.Error("DSL Workflow failed.", zap.Error(err)) return nil, err } logger.Info("DSL Workflow completed.") return nil, err } func (b *Statement) execute(ctx workflow.Context, bindings map[string]string) error { if b.Parallel != nil { err := b.Parallel.execute(ctx, bindings) if err != nil { return err } } if b.Sequence != nil { err := b.Sequence.execute(ctx, bindings) if err != nil { return err } } if b.Activity != nil { err := b.Activity.execute(ctx, bindings) if err != nil { return err } } return nil } func (a ActivityInvocation) execute(ctx workflow.Context, bindings map[string]string) error { inputParam := makeInput(a.Arguments, bindings) var result string err := workflow.ExecuteActivity(ctx, a.Name, inputParam).Get(ctx, &result) if err != nil { return err } if a.Result != "" { bindings[a.Result] = result } return nil } func (s Sequence) execute(ctx workflow.Context, bindings map[string]string) error { for _, a := range s.Elements { err := a.execute(ctx, bindings) if err != nil { return err } } return nil } func (p Parallel) execute(ctx workflow.Context, bindings map[string]string) error { // // You can use the context passed in to activity as a way to cancel the activity like standard GO way. // Cancelling a parent context will cancel all the derived contexts as well. // // In the parallel block, we want to execute all of them in parallel and wait for all of them. // if one activity fails then we want to cancel all the rest of them as well. childCtx, cancelHandler := workflow.WithCancel(ctx) selector := workflow.NewSelector(ctx) var activityErr error for _, s := range p.Branches { f := executeAsync(s, childCtx, bindings) selector.AddFuture(f, func(f workflow.Future) { err := f.Get(ctx, nil) if err != nil { // cancel all pending activities cancelHandler() activityErr = err } }) } for i := 0; i < len(p.Branches); i++ { selector.Select(ctx) // this will wait for one branch if activityErr != nil { return activityErr } } return nil } func executeAsync(exe executable, ctx workflow.Context, bindings map[string]string) workflow.Future { future, settable := workflow.NewFuture(ctx) workflow.Go(ctx, func(ctx workflow.Context) { err := exe.execute(ctx, bindings) settable.Set(nil, err) }) return future } func makeInput(argNames []string, argsMap map[string]string) []string { var args []string for _, arg := range argNames { args = append(args, argsMap[arg]) } return args } ================================================ FILE: cmd/samples/dsl/workflow1.yaml ================================================ # This sample workflow execute 3 steps in sequence. # 1) sampleActivity1, takes arg1 as input, and put result as result1. # 2) sampleActivity2, takes result1 as input, and put result as result2. # 3) sampleActivity3, takes args2 and result2 as input, and put result as result3. variables: arg1: value1 arg2: value2 root: sequence: elements: - activity: name: main.sampleActivity1 arguments: - arg1 result: result1 - activity: name: main.sampleActivity2 arguments: - result1 result: result2 - activity: name: main.sampleActivity3 arguments: - arg2 - result2 result: result3 ================================================ FILE: cmd/samples/dsl/workflow2.yaml ================================================ # This sample workflow execute 3 steps in sequence. # 1) activity1, takes arg1 as input, and put result as result1. # 2) it runs a parallel block which runs below sequence branches in parallel # 2.1) sequence 1 # 2.1.1) activity2, takes result1 as input, and put result as result2 # 2.1.2) activity3, takes arg2 and result2 as input, and put result as result3 # 2.2) sequence 2 # 2.2.1) activity4, takes result1 as input, and put result as result4 # 2.2.2) activity5, takes arg3 and result4 as input, and put result as result5 # 3) activity1, takes result3 and result5 as input, and put result as result6. variables: arg1: value1 arg2: value2 arg3: value3 root: sequence: elements: - activity: name: main.sampleActivity1 arguments: - arg1 result: result1 - parallel: branches: - sequence: elements: - activity: name: main.sampleActivity2 arguments: - result1 result: result2 - activity: name: main.sampleActivity3 arguments: - arg2 - result2 result: result3 - sequence: elements: - activity: name: main.sampleActivity4 arguments: - result1 result: result4 - activity: name: main.sampleActivity5 arguments: - arg3 - result4 result: result5 - activity: name: main.sampleActivity1 arguments: - result3 - result5 result: result6 ================================================ FILE: cmd/samples/dsl/workflow_test.go ================================================ package main import ( "fmt" "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/cadence/activity" "go.uber.org/cadence/testsuite" "go.uber.org/cadence/workflow" ) func TestActivitySequenceParallelStatements(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} tests := []struct { name string fields Statement bindings map[string]string wantErr bool }{ { name: "Test Activity Invocation", fields: Statement{ Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var1", "var2"}, Result: "resultVar", }, }, bindings: map[string]string{ "var1": "value1", "var2": "value2", }, wantErr: false, }, { name: "Test Sequence Execution", fields: Statement{ Sequence: &Sequence{ Elements: []*Statement{ { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var1"}, Result: "resultVar1", }, }, { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var2"}, Result: "resultVar2", }, }, }, }, }, bindings: map[string]string{ "var1": "value1", "var2": "value2", }, wantErr: false, }, { name: "Test Parallel Execution", fields: Statement{ Parallel: &Parallel{ Branches: []*Statement{ { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var1"}, Result: "resultVar1", }, }, { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var2"}, Result: "resultVar2", }, }, }, }, }, bindings: map[string]string{ "var1": "value1", "var2": "value2", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { env := testSuite.NewTestWorkflowEnvironment() env.RegisterActivityWithOptions(sampleActivity, activity.RegisterOptions{ Name: "sampleActivity", }) env.ExecuteWorkflow(func(ctx workflow.Context) error { ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, }) return tt.fields.execute(ctx, tt.bindings) }) require.True(t, env.IsWorkflowCompleted()) if tt.wantErr { require.Error(t, env.GetWorkflowError()) } else { require.NoError(t, env.GetWorkflowError()) } }) } } func TestSequenceFlow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} tests := []struct { name string fields Sequence bindings map[string]string wantErr bool }{ { name: "Test Sequence Execution with Single Activity", fields: Sequence{ Elements: []*Statement{ { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var1"}, Result: "resultVar1", }, }, }, }, bindings: map[string]string{ "var1": "value1", }, wantErr: false, }, { name: "Test Sequence Execution with Multiple Activities", fields: Sequence{ Elements: []*Statement{ { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var1"}, Result: "resultVar1", }, }, { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var2"}, Result: "resultVar2", }, }, }, }, bindings: map[string]string{ "var1": "value1", "var2": "value2", }, wantErr: false, }, { name: "Test Sequence Execution with Error", fields: Sequence{ Elements: []*Statement{ { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var1"}, Result: "resultVar1", }, }, { Activity: &ActivityInvocation{ Name: "nonExistentActivity", Arguments: []string{"var2"}, Result: "resultVar2", }, }, }, }, bindings: map[string]string{ "var1": "value1", "var2": "value2", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { env := testSuite.NewTestWorkflowEnvironment() env.RegisterActivityWithOptions(sampleActivity, activity.RegisterOptions{ Name: "sampleActivity", }) env.ExecuteWorkflow(func(ctx workflow.Context) error { ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, }) return tt.fields.execute(ctx, tt.bindings) }) require.True(t, env.IsWorkflowCompleted()) if tt.wantErr { require.Error(t, env.GetWorkflowError()) } else { require.NoError(t, env.GetWorkflowError()) } }) } } func TestParallelFlow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} tests := []struct { name string fields Parallel bindings map[string]string wantErr bool }{ { name: "Test Parallel Execution with Single Activity", fields: Parallel{ Branches: []*Statement{ { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var1"}, Result: "resultVar1", }, }, }, }, bindings: map[string]string{ "var1": "value1", }, wantErr: false, }, { name: "Test Parallel Execution with Multiple Activities", fields: Parallel{ Branches: []*Statement{ { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var1"}, Result: "resultVar1", }, }, { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var2"}, Result: "resultVar2", }, }, }, }, bindings: map[string]string{ "var1": "value1", "var2": "value2", }, wantErr: false, }, { name: "Test Parallel Execution with Error", fields: Parallel{ Branches: []*Statement{ { Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var1"}, Result: "resultVar1", }, }, { Activity: &ActivityInvocation{ Name: "nonExistentActivity", Arguments: []string{"var2"}, Result: "resultVar2", }, }, }, }, bindings: map[string]string{ "var1": "value1", "var2": "value2", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { env := testSuite.NewTestWorkflowEnvironment() env.RegisterActivityWithOptions(sampleActivity, activity.RegisterOptions{ Name: "sampleActivity", }) env.ExecuteWorkflow(func(ctx workflow.Context) error { ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, }) return tt.fields.execute(ctx, tt.bindings) }) require.True(t, env.IsWorkflowCompleted()) if tt.wantErr { require.Error(t, env.GetWorkflowError()) } else { require.NoError(t, env.GetWorkflowError()) } }) } } func TestActivityInvocationFlow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} tests := []struct { name string fields ActivityInvocation bindings map[string]string wantErr bool }{ { name: "Test Activity Invocation Success", fields: ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var1"}, Result: "resultVar", }, bindings: map[string]string{ "var1": "value1", }, wantErr: false, }, { name: "Test Activity Invocation with Error", fields: ActivityInvocation{ Name: "nonExistentActivity", Arguments: []string{"var1"}, Result: "resultVar", }, bindings: map[string]string{ "var1": "value1", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { env := testSuite.NewTestWorkflowEnvironment() env.RegisterActivityWithOptions(sampleActivity, activity.RegisterOptions{ Name: "sampleActivity", }) env.ExecuteWorkflow(func(ctx workflow.Context) error { ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, }) return tt.fields.execute(ctx, tt.bindings) }) require.True(t, env.IsWorkflowCompleted()) if tt.wantErr { require.Error(t, env.GetWorkflowError()) } else { require.NoError(t, env.GetWorkflowError()) } }) } } func Test_SimpleDSLWorkflow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestWorkflowEnvironment() // Define a sample DSL workflow dslWorkflow := Workflow{ Variables: map[string]string{ "var1": "value1", "var2": "value2", }, Root: Statement{ Activity: &ActivityInvocation{ Name: "sampleActivity", Arguments: []string{"var1", "var2"}, Result: "resultVar", }, }, } // Register a sample activity env.RegisterActivityWithOptions(sampleActivity, activity.RegisterOptions{ Name: "sampleActivity", }) env.ExecuteWorkflow(simpleDSLWorkflow, dslWorkflow) require.True(t, env.IsWorkflowCompleted()) require.NoError(t, env.GetWorkflowError()) } func sampleActivity(input []string) (string, error) { name := "sampleActivity" fmt.Printf("Run %s with input %v \n", name, input) return "Result_" + name, nil } ================================================ FILE: cmd/samples/expense/README.md ================================================ # Expense This sample workflow process an expense request. The key part of this sample is to show how to complete an activity asynchronously. # Sample Description * Create a new expense report. * Wait for the expense report to be approved. This could take an arbitrary amount of time. So the activity's Execute method has to return before it is actually approved. This is done by returning a special error so the framework knows the activity is not completed yet. * When the expense is approved (or rejected), somewhere in the world needs to be notified, and it will need to call WorkflowClient.CompleteActivity() to tell cadence service that that activity is now completed. In this sample case, the dummy server do this job. In real world, you will need to register some listener to the expense system or you will need to have your own pulling agent to check for the expense status periodic. * After the wait activity is completed, it did the payment for the expense. (dummy step in this sample case) This sample rely on an a dummy expense server to work. # Steps To Run Sample * You need a cadence service running. See https://github.com/cadence-workflow/cadence/blob/master/README.md for more details. * Start the dummy server ``` ./bin/expense_dummy ``` If dummy is not found, run make to build it. * Start workflow and activity workers ``` ./bin/expense -m worker ``` * Start expanse workflow execution ``` ./bin/expense -m trigger ``` * When you see the console print out the expense is created, go to [localhost:8099/list](http://localhost:8099/list) to approve the expense. * You should see the workflow complete after you approve the expense. You can also reject the expense. * If you see the workflow failed, try to change to a different port number in dummy.go and workflow.go. Then rebuild everything. ================================================ FILE: cmd/samples/expense/activities.go ================================================ package main import ( "context" "errors" "fmt" "io/ioutil" "net/http" "net/url" "go.uber.org/cadence/activity" "go.uber.org/zap" ) func createExpenseActivity(ctx context.Context, expenseID string) error { if len(expenseID) == 0 { return errors.New("expense id is empty") } resp, err := http.Get(expenseServerHostPort + "/create?is_api_call=true&id=" + expenseID) if err != nil { return err } body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { return err } if string(body) == "SUCCEED" { activity.GetLogger(ctx).Info("Expense created.", zap.String("ExpenseID", expenseID)) return nil } return errors.New(string(body)) } // waitForDecisionActivity waits for the expense decision. This activity will complete asynchronously. When this method // returns error activity.ErrResultPending, the cadence client recognize this error, and won't mark this activity // as failed or completed. The cadence server will wait until Client.CompleteActivity() is called or timeout happened // whichever happen first. In this sample case, the CompleteActivity() method is called by our dummy expense server when // the expense is approved. func waitForDecisionActivity(ctx context.Context, expenseID string) (string, error) { if len(expenseID) == 0 { return "", errors.New("expense id is empty") } logger := activity.GetLogger(ctx) // save current activity info so it can be completed asynchronously when expense is approved/rejected activityInfo := activity.GetInfo(ctx) formData := url.Values{} formData.Add("task_token", string(activityInfo.TaskToken)) registerCallbackURL := expenseServerHostPort + "/registerCallback?id=" + expenseID resp, err := http.PostForm(registerCallbackURL, formData) if err != nil { logger.Info("waitForDecisionActivity failed to register callback.", zap.Error(err)) return "", err } body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { return "", err } status := string(body) if status == "SUCCEED" { // register callback succeed logger.Info("Successfully registered callback.", zap.String("ExpenseID", expenseID)) // ErrActivityResultPending is returned from activity's execution to indicate the activity is not completed when it returns. // activity will be completed asynchronously when Client.CompleteActivity() is called. return "", activity.ErrResultPending } logger.Warn("Register callback failed.", zap.String("ExpenseStatus", status)) return "", fmt.Errorf("register callback failed status:%s", status) } func paymentActivity(ctx context.Context, expenseID string) error { if len(expenseID) == 0 { return errors.New("expense id is empty") } resp, err := http.Get(expenseServerHostPort + "/action?is_api_call=true&type=payment&id=" + expenseID) if err != nil { return err } body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { return err } if string(body) == "SUCCEED" { activity.GetLogger(ctx).Info("paymentActivity succeed", zap.String("ExpenseID", expenseID)) return nil } return errors.New(string(body)) } ================================================ FILE: cmd/samples/expense/main.go ================================================ 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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper, expenseID string) { workflowOptions := client.StartWorkflowOptions{ ID: "expense_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute * 12, DecisionTaskStartToCloseTimeout: time.Minute * 12, } h.StartWorkflow(workflowOptions, sampleExpenseWorkflow, expenseID) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleExpenseWorkflow) h.RegisterActivity(createExpenseActivity) h.RegisterActivity(waitForDecisionActivity) h.RegisterActivity(paymentActivity) 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, uuid.New()) } } ================================================ FILE: cmd/samples/expense/server/dummy.go ================================================ package main import ( "context" "fmt" "net/http" "sort" "go.uber.org/cadence/client" "github.com/uber-common/cadence-samples/cmd/samples/common" ) /** * Dummy server that support to list expenses, create new expense, update expense state and checking expense state. */ type expenseState string const ( created expenseState = "CREATED" approved = "APPROVED" rejected = "REJECTED" completed = "COMPLETED" ) // use memory store for this dummy server var allExpense = make(map[string]expenseState) var tokenMap = make(map[string][]byte) var workflowClient client.Client func main() { var h common.SampleHelper h.SetupServiceConfig() var err error workflowClient, err = h.Builder.BuildCadenceClient() if err != nil { panic(err) } fmt.Println("Starting dummy server...") http.HandleFunc("/", listHandler) http.HandleFunc("/list", listHandler) http.HandleFunc("/create", createHandler) http.HandleFunc("/action", actionHandler) http.HandleFunc("/status", statusHandler) http.HandleFunc("/registerCallback", callbackHandler) http.ListenAndServe(":8099", nil) } func listHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "

DUMMY EXPENSE SYSTEM

"+"HOME"+ "

All expense requests:

") keys := []string{} for k := range allExpense { keys = append(keys, k) } sort.Strings(keys) for _, id := range keys { state := allExpense[id] actionLink := "" if state == created { actionLink = fmt.Sprintf(""+ ""+ "  "+ "", id, id) } fmt.Fprintf(w, "", id, state, actionLink) } fmt.Fprint(w, "
Expense IDStatusAction
%s%s%s
") } func actionHandler(w http.ResponseWriter, r *http.Request) { isAPICall := r.URL.Query().Get("is_api_call") == "true" id := r.URL.Query().Get("id") oldState, ok := allExpense[id] if !ok { fmt.Fprint(w, "ERROR:INVALID_ID") return } actionType := r.URL.Query().Get("type") switch actionType { case "approve": allExpense[id] = approved case "reject": allExpense[id] = rejected case "payment": allExpense[id] = completed } if isAPICall { fmt.Fprint(w, "SUCCEED") } else { listHandler(w, r) } if oldState == created && (allExpense[id] == approved || allExpense[id] == rejected) { // report state change notifyExpenseStateChange(id, string(allExpense[id])) } fmt.Printf("Set state for %s from %s to %s.\n", id, oldState, allExpense[id]) return } func createHandler(w http.ResponseWriter, r *http.Request) { isAPICall := r.URL.Query().Get("is_api_call") == "true" id := r.URL.Query().Get("id") _, ok := allExpense[id] if ok { fmt.Fprint(w, "ERROR:ID_ALREADY_EXISTS") return } allExpense[id] = created if isAPICall { fmt.Fprint(w, "SUCCEED") } else { listHandler(w, r) } fmt.Printf("Created new expense id:%s.\n", id) return } func statusHandler(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") state, ok := allExpense[id] if !ok { fmt.Fprint(w, "ERROR:INVALID_ID") return } fmt.Fprint(w, state) fmt.Printf("Checking status for %s: %s\n", id, state) return } func callbackHandler(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") currState, ok := allExpense[id] if !ok { fmt.Fprint(w, "ERROR:INVALID_ID") return } if currState != created { fmt.Fprint(w, "ERROR:INVALID_STATE") return } err := r.ParseForm() if err != nil { // Handle error here via logging and then return fmt.Fprint(w, "ERROR:INVALID_FORM_DATA") return } taskToken := r.PostFormValue("task_token") fmt.Printf("Registered callback for ID=%s, token=%s\n", id, taskToken) tokenMap[id] = []byte(taskToken) fmt.Fprint(w, "SUCCEED") } func notifyExpenseStateChange(id, state string) { token, ok := tokenMap[id] if !ok { fmt.Printf("Invalid id:%s\n", id) return } err := workflowClient.CompleteActivity(context.Background(), token, state, nil) if err != nil { fmt.Printf("Failed to complete activity with error: %+v\n", err) } else { fmt.Printf("Successfully complete activity: %s\n", token) } } ================================================ FILE: cmd/samples/expense/workflow.go ================================================ package main import ( "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) const ( // ApplicationName is the task list for this sample ApplicationName = "expenseGroup" ) var expenseServerHostPort = "http://localhost:8099" // sampleExpenseWorkflow workflow decider func sampleExpenseWorkflow(ctx workflow.Context, expenseID string) (result string, err error) { // step 1, create new expense report ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx1 := workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) err = workflow.ExecuteActivity(ctx1, createExpenseActivity, expenseID).Get(ctx1, nil) if err != nil { logger.Error("Failed to create expense report", zap.Error(err)) return "", err } // step 2, wait for the expense report to be approved (or rejected) ao = workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: 4 * time.Minute, } ctx2 := workflow.WithActivityOptions(ctx, ao) // Notice that we set the timeout to be 4 minutes for this sample demo. If the expected time for the activity to // complete (waiting for human to approve the request) is longer, you should set the timeout accordingly so the // cadence system will wait accordingly. Otherwise, cadence system could mark the activity as failure by timeout. var status string err = workflow.ExecuteActivity(ctx2, waitForDecisionActivity, expenseID).Get(ctx2, &status) if err != nil { return "", err } if status != "APPROVED" { logger.Info("Workflow completed.", zap.String("ExpenseStatus", status)) return "", nil } // step 3, request payment to the expense err = workflow.ExecuteActivity(ctx2, paymentActivity, expenseID).Get(ctx2, nil) if err != nil { logger.Info("Workflow completed with payment failed.", zap.Error(err)) return "", err } logger.Info("Workflow completed with expense payment completed.") return "COMPLETED", nil } ================================================ FILE: cmd/samples/expense/workflow_test.go ================================================ package main import ( "errors" "io" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "go.uber.org/cadence/testsuite" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(sampleExpenseWorkflow) s.env.RegisterActivity(createExpenseActivity) s.env.RegisterActivity(waitForDecisionActivity) s.env.RegisterActivity(paymentActivity) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_WorkflowWithMockActivities() { s.env.OnActivity(createExpenseActivity, mock.Anything, mock.Anything).Return(nil).Once() s.env.OnActivity(waitForDecisionActivity, mock.Anything, mock.Anything).Return("APPROVED", nil).Once() s.env.OnActivity(paymentActivity, mock.Anything, mock.Anything).Return(nil).Once() s.env.ExecuteWorkflow(sampleExpenseWorkflow, "test-expense-id") s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) var workflowResult string err := s.env.GetWorkflowResult(&workflowResult) s.NoError(err) s.Equal("COMPLETED", workflowResult) } func (s *UnitTestSuite) Test_TimeoutWithMockActivities() { s.env.OnActivity(createExpenseActivity, mock.Anything, mock.Anything).Return(nil).Once() s.env.SetWorkflowTimeout(time.Microsecond * 500) s.env.SetTestTimeout(time.Minute * 10) s.env.ExecuteWorkflow(sampleExpenseWorkflow, "test-expense-id") var workflowResult string err := s.env.GetWorkflowResult(&workflowResult) s.Equal("TimeoutType: SCHEDULE_TO_CLOSE", err.Error()) s.Empty(workflowResult) } func (s *UnitTestSuite) Test_WorkflowStatusRejected() { s.env.OnActivity(createExpenseActivity, mock.Anything, mock.Anything).Return(nil).Once() s.env.OnActivity(waitForDecisionActivity, mock.Anything, mock.Anything).Return("REJECTED", nil).Once() s.env.ExecuteWorkflow(sampleExpenseWorkflow, "test-expense-id") s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) var workflowResult string err := s.env.GetWorkflowResult(&workflowResult) s.NoError(err) s.Empty(workflowResult) } func (s *UnitTestSuite) Test_WorkflowStatusCancelled() { s.env.OnActivity(createExpenseActivity, mock.Anything, mock.Anything).Return(nil).Once() s.env.OnActivity(waitForDecisionActivity, mock.Anything, mock.Anything).Return("CANCELLED", nil).Once() s.env.ExecuteWorkflow(sampleExpenseWorkflow, "test-expense-id") s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) var workflowResult string err := s.env.GetWorkflowResult(&workflowResult) s.NoError(err) s.Empty(workflowResult) } func (s *UnitTestSuite) Test_WorkflowStatusApprovedWithPaymentError() { s.env.OnActivity(createExpenseActivity, mock.Anything, mock.Anything).Return(nil).Once() s.env.OnActivity(waitForDecisionActivity, mock.Anything, mock.Anything).Return("APPROVED", nil).Once() s.env.OnActivity(paymentActivity, mock.Anything, mock.Anything).Return(errors.New("payment error")).Once() s.env.ExecuteWorkflow(sampleExpenseWorkflow, "test-expense-id") s.True(s.env.IsWorkflowCompleted()) s.Error(s.env.GetWorkflowError()) var workflowResult string err := s.env.GetWorkflowResult(&workflowResult) s.Equal("payment error", err.Error()) s.Empty(workflowResult) } func (s *UnitTestSuite) Test_CreateActivityFailed() { s.env.OnActivity(createExpenseActivity, mock.Anything, mock.Anything).Return(errors.New("expense id is empty")).Once() s.env.ExecuteWorkflow(sampleExpenseWorkflow, "") s.True(s.env.IsWorkflowCompleted()) s.Error(s.env.GetWorkflowError()) var workflowResult string err := s.env.GetWorkflowResult(&workflowResult) s.Equal("expense id is empty", err.Error()) s.Empty(workflowResult) } func (s *UnitTestSuite) Test_WaitForDecisionActivityFailed() { s.env.OnActivity(createExpenseActivity, mock.Anything, mock.Anything).Return(nil).Once() s.env.OnActivity(waitForDecisionActivity, mock.Anything, mock.Anything).Return("", errors.New("failed to get decision")).Once() s.env.ExecuteWorkflow(sampleExpenseWorkflow, "test-expense-id") s.True(s.env.IsWorkflowCompleted()) s.Error(s.env.GetWorkflowError()) var workflowResult string err := s.env.GetWorkflowResult(&workflowResult) s.Equal("failed to get decision", err.Error()) s.Empty(workflowResult) } func (s *UnitTestSuite) Test_PaymentActivityFailed() { s.env.OnActivity(createExpenseActivity, mock.Anything, mock.Anything).Return(nil).Once() s.env.OnActivity(waitForDecisionActivity, mock.Anything, mock.Anything).Return("APPROVED", nil).Once() s.env.OnActivity(paymentActivity, mock.Anything, mock.Anything).Return(errors.New("payment failed")).Once() s.env.ExecuteWorkflow(sampleExpenseWorkflow, "test-expense-id") s.True(s.env.IsWorkflowCompleted()) s.Error(s.env.GetWorkflowError()) var workflowResult string err := s.env.GetWorkflowResult(&workflowResult) s.Equal("payment failed", err.Error()) s.Empty(workflowResult) } func (s *UnitTestSuite) Test_WorkflowWithMockServer() { // setup mock expense server handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/text") switch r.URL.Path { case "/create": case "/registerCallback": taskToken := []byte(r.PostFormValue("task_token")) // simulate the expense is approved one hour later. s.env.RegisterDelayedCallback(func() { s.env.CompleteActivity(taskToken, "APPROVED", nil) }, time.Hour) case "/action": } io.WriteString(w, "SUCCEED") } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() // pointing server to test mock expenseServerHostPort = server.URL s.env.ExecuteWorkflow(sampleExpenseWorkflow, "test-expense-id") s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) var workflowResult string err := s.env.GetWorkflowResult(&workflowResult) s.NoError(err) s.Equal("COMPLETED", workflowResult) } ================================================ FILE: cmd/samples/fileprocessing/README.md ================================================ This sample workflow demos a file processing process. The key part is to show how to use the session API. The workflow first starts an activity to download a requested resource file from web and store it locally on the host where it runs the download activity. Then, the workflow will start more activities to process the downloaded resource file. The key part is the following activities have to be run on the same host as the initial downloading activity. This is achieved by using the session API. Steps for using Session API: 1) When starting worker, set `EnableSessionWorker` to true in workerOptions. 2) In the workflow code, create a new session using the `CreateSession()` API ``` so := &workflow.SessionOptions{ CreationTimeout: time.Minute, ExecutionTimeout: time.Minute, } sessionCtx, err := workflow.CreateSession(ctx, so) ``` 3) Use the returned `sessionCtx` or its child context to execute activities. These activities will be to scheduled on the same host. 4) After all activites are executed, call `CompleteSession()`. ``` workflow.CompleteSession(sessionCtx) ``` 5) Check the inline document in workflow/session.go of the go-client repo for more advanced usage. Steps to run this sample: 1) You need a cadence service running. See details in cmd/samples/README.md 2) Run the following command multiple times on different console window. This is to simulate running workers on multiple different machines. ``` ./bin/fileprocessing -m worker ``` 3) Run the following command to submit a start request for this fileprocessing workflow. ``` ./bin/fileprocessing -m trigger ``` You should see that all activities for one particular workflow execution are scheduled to run on one console window. ================================================ FILE: cmd/samples/fileprocessing/activities.go ================================================ package main import ( "context" "errors" "io/ioutil" "os" "strings" "time" "go.uber.org/cadence/activity" "go.uber.org/zap" ) /** * Sample activities used by file processing sample workflow. */ const ( downloadFileActivityName = "downloadFileActivity" processFileActivityName = "processFileActivity" uploadFileActivityName = "uploadFileActivity" ) func downloadFileActivity(ctx context.Context, fileID string) (*fileInfo, error) { logger := activity.GetLogger(ctx) logger.Info("Downloading file...", zap.String("FileID", fileID)) data := downloadFile(fileID) tmpFile, err := saveToTmpFile(data) if err != nil { logger.Error("downloadFileActivity failed to save tmp file.", zap.Error(err)) return nil, err } fileInfo := &fileInfo{FileName: tmpFile.Name(), HostID: HostID} logger.Info("downloadFileActivity succeed.", zap.String("SavedFilePath", fileInfo.FileName)) return fileInfo, nil } func processFileActivity(ctx context.Context, fInfo fileInfo) (*fileInfo, error) { logger := activity.GetLogger(ctx).With(zap.String("HostID", HostID)) logger.Info("processFileActivity started.", zap.String("FileName", fInfo.FileName)) // assert that we are running on the same host as the file was downloaded // this check is not necessary, just to demo the host specific tasklist is working if fInfo.HostID != HostID { logger.Error("processFileActivity on wrong host", zap.String("TargetFile", fInfo.FileName), zap.String("TargetHostID", fInfo.HostID)) return nil, errors.New("processFileActivity running on wrong host") } defer os.Remove(fInfo.FileName) // cleanup temp file // read downloaded file data, err := ioutil.ReadFile(fInfo.FileName) if err != nil { logger.Error("processFileActivity failed to read file.", zap.String("FileName", fInfo.FileName), zap.Error(err)) return nil, err } // process the file transData := transcodeData(ctx, data) tmpFile, err := saveToTmpFile(transData) if err != nil { logger.Error("processFileActivity failed to save tmp file.", zap.Error(err)) return nil, err } processedInfo := &fileInfo{FileName: tmpFile.Name(), HostID: HostID} logger.Info("processFileActivity succeed.", zap.String("SavedFilePath", processedInfo.FileName)) return processedInfo, nil } func uploadFileActivity(ctx context.Context, fInfo fileInfo) error { logger := activity.GetLogger(ctx).With(zap.String("HostID", HostID)) logger.Info("uploadFileActivity begin.", zap.String("UploadedFileName", fInfo.FileName)) // assert that we are running on the same host as the file was downloaded // this check is not necessary, just to demo the host specific tasklist is working if fInfo.HostID != HostID { logger.Error("uploadFileActivity on wrong host", zap.String("TargetFile", fInfo.FileName), zap.String("TargetHostID", fInfo.HostID)) return errors.New("uploadFileActivity running on wrong host") } defer os.Remove(fInfo.FileName) // clean up tmp file err := uploadFile(ctx, fInfo.FileName) if err != nil { logger.Error("uploadFileActivity uploading failed.", zap.Error(err)) return err } logger.Info("uploadFileActivity succeed.", zap.String("UploadedFileName", fInfo.FileName)) return nil } func downloadFile(fileID string) []byte { // dummy downloader dummyContent := "dummy content for fileID:" + fileID return []byte(dummyContent) } func uploadFile(ctx context.Context, filename string) error { // dummy uploader _, err := ioutil.ReadFile(filename) for i := 0; i < 5; i++ { time.Sleep(1 * time.Second) // Demonstrates that heartbeat accepts progress data. // In case of a heartbeat timeout it is included into the error. activity.RecordHeartbeat(ctx, i) } if err != nil { return err } return nil } func transcodeData(ctx context.Context, data []byte) []byte { // dummy file processor, just do upper case for the data. // in real world case, you would want to avoid load entire file content into memory at once. for i := 0; i < 5; i++ { time.Sleep(1 * time.Second) // Demonstrates that heartbeat accepts progress data. // In case of a heartbeat timeout it is included into the error. activity.RecordHeartbeat(ctx, i) } return []byte(strings.ToUpper(string(data))) } func saveToTmpFile(data []byte) (f *os.File, err error) { tmpFile, err := ioutil.TempFile("", "cadence_sample") if err != nil { return nil, err } _, err = tmpFile.Write(data) if err != nil { os.Remove(tmpFile.Name()) return nil, err } return tmpFile, nil } ================================================ FILE: cmd/samples/fileprocessing/main.go ================================================ 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, EnableLoggingInReplay: true, EnableSessionWorker: true, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) // Host Specific activities processing case workerOptions.DisableWorkflowWorker = true h.StartWorkers(h.Config.DomainName, HostID, workerOptions) } func startWorkflow(h *common.SampleHelper, fileID string) { workflowOptions := client.StartWorkflowOptions{ ID: "fileprocessing_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, sampleFileProcessingWorkflow, fileID) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleFileProcessingWorkflow) h.RegisterActivityWithAlias(downloadFileActivity, downloadFileActivityName) h.RegisterActivityWithAlias(processFileActivity, processFileActivityName) h.RegisterActivityWithAlias(uploadFileActivity, uploadFileActivityName) 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, uuid.New()) } } ================================================ FILE: cmd/samples/fileprocessing/workflow.go ================================================ package main import ( "time" "github.com/pborman/uuid" "go.uber.org/cadence" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) type ( fileInfo struct { FileName string HostID string } ) // ApplicationName is the task list for this sample const ApplicationName = "FileProcessorGroup" // HostID - Use a new uuid just for demo so we can run 2 host specific activity workers on same machine. // In real world case, you would use a hostname or ip address as HostID. var HostID = ApplicationName + "_" + uuid.New() //sampleFileProcessingWorkflow workflow decider func sampleFileProcessingWorkflow(ctx workflow.Context, fileID string) (err error) { // step 1: download resource file ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Second * 5, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 2, // such a short timeout to make sample fail over very fast RetryPolicy: &cadence.RetryPolicy{ InitialInterval: time.Second, BackoffCoefficient: 2.0, MaximumInterval: time.Minute, ExpirationInterval: time.Minute * 10, NonRetriableErrorReasons: []string{"bad-error"}, }, } ctx = workflow.WithActivityOptions(ctx, ao) // Retry the whole sequence from the first activity on any error // to retry it on a different host. In a real application it might be reasonable to // retry individual activities and the whole sequence discriminating between different types of errors. // See the retryactivity sample for a more sophisticated retry implementation. for i := 1; i < 5; i++ { err = processFile(ctx, fileID) if err == nil { break } } if err != nil { workflow.GetLogger(ctx).Error("Workflow failed.", zap.String("Error", err.Error())) } else { workflow.GetLogger(ctx).Info("Workflow completed.") } return err } func processFile(ctx workflow.Context, fileID string) (err error) { var fInfo *fileInfo so := &workflow.SessionOptions{ CreationTimeout: time.Minute, ExecutionTimeout: time.Minute, } sessionCtx, err := workflow.CreateSession(ctx, so) if err != nil { return err } defer workflow.CompleteSession(sessionCtx) err = workflow.ExecuteActivity(sessionCtx, downloadFileActivityName, fileID).Get(sessionCtx, &fInfo) if err != nil { return err } var fInfoProcessed *fileInfo err = workflow.ExecuteActivity(sessionCtx, processFileActivityName, *fInfo).Get(sessionCtx, &fInfoProcessed) if err != nil { return err } err = workflow.ExecuteActivity(sessionCtx, uploadFileActivityName, *fInfoProcessed).Get(sessionCtx, nil) return err } ================================================ FILE: cmd/samples/fileprocessing/workflow_test.go ================================================ package main import ( "context" "strings" "testing" "github.com/stretchr/testify/suite" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(sampleFileProcessingWorkflow) s.env.RegisterActivityWithOptions(downloadFileActivity, activity.RegisterOptions{ Name: downloadFileActivityName, }) s.env.RegisterActivityWithOptions(processFileActivity, activity.RegisterOptions{ Name: processFileActivityName, }) s.env.RegisterActivityWithOptions(uploadFileActivity, activity.RegisterOptions{ Name: uploadFileActivityName, }) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_SampleFileProcessingWorkflow() { fileID := "test-file-id" expectedCall := []string{ "downloadFileActivity", "processFileActivity", "uploadFileActivity", } var activityCalled []string s.env.SetOnActivityStartedListener(func(activityInfo *activity.Info, ctx context.Context, args encoded.Values) { activityType := activityInfo.ActivityType.Name if strings.HasPrefix(activityType, "internalSession") { return } activityCalled = append(activityCalled, activityType) switch activityType { case expectedCall[0]: var input string s.NoError(args.Get(&input)) s.Equal(fileID, input) case expectedCall[1]: var input fileInfo s.NoError(args.Get(&input)) s.Equal(input.HostID, HostID) case expectedCall[2]: var input fileInfo s.NoError(args.Get(&input)) s.Equal(input.HostID, HostID) default: panic("unexpected activity call") } }) s.env.ExecuteWorkflow(sampleFileProcessingWorkflow, fileID) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) s.Equal(expectedCall, activityCalled) } ================================================ FILE: cmd/samples/pso/README.md ================================================ This sample workflow demos a long iterative math optimization process using particle swarm optimization (PSO). The workflow first does some data structure initialization and then runs many iterations using a child workflow. The child workflow runs 10 iterations and then uses ContinueAsNew to avoid to store too long history in the Cadence database. In case of recovery the whole history has to be replayed to reconstruct the workflow state. So if history is too large the recover can take very long time. Each particle is processed in parallel using worflow.Go and the math grunt work is done in the activites. Since the data structure that maintains the optimization state has to be passed to the child workflow and the activities, a custom DataConverter has been implemented to take care of serialization/deserialization. Also the query API is supported to get the current state of running workflow. Steps to run this sample: 1) You need a cadence service running. See details in cmd/samples/README.md 2) Run the following command multiple times on different console window. This is to simulate running workers on multiple different machines. ``` ./bin/pso -m worker ``` 3) Run the following command to submit a start request for this PSO workflow. ``` ./bin/pso -m trigger ``` 4) Query the state with ``` ./bin/pso -m query -w -r -t state ``` Replace -t state with -t \_\_stack_trace to dump the call stack for the workflow. You should see that all activities for one particular workflow execution are scheduled to run on one console window. ================================================ FILE: cmd/samples/pso/activities.go ================================================ package main import ( "context" "math/rand" "time" "go.uber.org/cadence/activity" ) /** * Sample activities used by file processing sample workflow. */ const ( initParticleActivityName = "initParticleActivityName" updateParticleActivityName = "updateParticleActivityName" ) var rng *rand.Rand // This is registration process where you register all your activity handlers. func init() { // initialize the RNG // WARNING: the randomness of activity scheduling with multiple workers makes random number generation truly random and not repeatable in debugging // worker.ReplayWorkflowHistoryFromJSONFile should be used to troubleshoot a specific workflow failure. rng = rand.New(rand.NewSource(time.Now().UnixNano())) } func initParticleActivity(ctx context.Context, swarm Swarm) (Particle, error) { logger := activity.GetLogger(ctx) logger.Info("initParticleActivity started.") particle := NewParticle(&swarm, rng) particle.UpdateFitness(&swarm) return *particle, nil } func updateParticleActivity(ctx context.Context, swarm Swarm, particleIdx int) (Particle, error) { logger := activity.GetLogger(ctx) logger.Info("updateParticleActivity started.") particle := swarm.Particles[particleIdx] particle.UpdateLocation(&swarm, rng) particle.UpdateFitness(&swarm) return *particle, nil } ================================================ FILE: cmd/samples/pso/dataconverter.go ================================================ package main import ( "bytes" "encoding/gob" "encoding/json" "fmt" "reflect" "go.uber.org/cadence/encoded" ) // gobDataConverter implements encoded.DataConverter using gob for Swarm and Particle // WARGNING: Make sure all struct members are public (Capital letter) otherwise serialization does not work! // TODO: consider storing blobs in external DB or S3 type gobDataConverter struct { } // NewGobDataConverter creates a gob data converter func NewGobDataConverter() encoded.DataConverter { return &gobDataConverter{} } // jsonDataConverter implements encoded.DataConverter using JSON for Swarm and Particle // WARGNING: Make sure all struct members are public (Capital letter) otherwise serialization does not work! // TODO: consider storing blobs in external DB or S3 type jsonDataConverter struct { } // NewJSONDataConverter creates a json data converter func NewJSONDataConverter() encoded.DataConverter { return &jsonDataConverter{} } // Gob data converter implementation func (dc *gobDataConverter) ToData(value ...interface{}) ([]byte, error) { var buf bytes.Buffer enc := gob.NewEncoder(&buf) var err error for i, obj := range value { switch t := obj.(type) { case Swarm: err = enc.Encode(*t.Settings) if err == nil { err = enc.Encode(*t.Gbest) if err == nil { if t.Settings.Size > 0 { for _, particle := range t.Particles { if particle == nil { particle = new(Particle) } err = enc.Encode(*particle) } } } } default: err = enc.Encode(obj) } if err != nil { return nil, fmt.Errorf( "unable to encode argument: %d, %v, with gob error: %v", i, reflect.TypeOf(obj), err) } } return buf.Bytes(), nil // TODO: store buf.Bytes() in DB/S3 and get key // return key, nil } func (dc *gobDataConverter) FromData(input []byte, valuePtr ...interface{}) error { // TODO: convert input into key in DB/S3 and retrieve bytes //dec := gob.NewDecoder(bytes) dec := gob.NewDecoder(bytes.NewBuffer(input)) var err error for i, obj := range valuePtr { switch t := obj.(type) { case *Swarm: t.Settings = new(SwarmSettings) err = dec.Decode(t.Settings) t.Settings.function = FunctionFactory(t.Settings.FunctionName) t.Gbest = NewPosition(t.Settings.function.dim) err = dec.Decode(t.Gbest) t.Particles = make([]*Particle, t.Settings.Size) for index := 0; index < t.Settings.Size; index++ { t.Particles[index] = new(Particle) err = dec.Decode(t.Particles[index]) } default: err = dec.Decode(obj) } if err != nil { return fmt.Errorf( "unable to decode argument: %d, %v, with gob error: %v", i, reflect.TypeOf(obj), err) } } return nil } // Json data converter implementation func (dc *jsonDataConverter) ToData(value ...interface{}) ([]byte, error) { var buf bytes.Buffer enc := json.NewEncoder(&buf) var err error for i, obj := range value { switch t := obj.(type) { case Swarm: err = enc.Encode(*t.Settings) if err == nil { err = enc.Encode(*t.Gbest) if err == nil { if t.Settings.Size > 0 { for _, particle := range t.Particles { if particle == nil { particle = new(Particle) } err = enc.Encode(*particle) } } } } case WorkflowResult: err = enc.Encode(t.Msg) err = enc.Encode(t.Success) default: err = enc.Encode(obj) } if err != nil { return nil, fmt.Errorf( "unable to encode argument: %d, %v, with error: %v", i, reflect.TypeOf(obj), err) } } return buf.Bytes(), nil // TODO: store buf.Bytes() in DB/S3 and get key // return key, nil } func (dc *jsonDataConverter) FromData(input []byte, valuePtr ...interface{}) error { // TODO: convert input into key in DB/S3 and retrieve bytes //dec := json.NewDecoder(bytes) dec := json.NewDecoder(bytes.NewBuffer(input)) var err error for i, obj := range valuePtr { switch t := obj.(type) { case *Swarm: t.Settings = new(SwarmSettings) err = dec.Decode(t.Settings) t.Settings.function = FunctionFactory(t.Settings.FunctionName) t.Gbest = NewPosition(t.Settings.function.dim) err = dec.Decode(t.Gbest) t.Particles = make([]*Particle, t.Settings.Size) for index := 0; index < t.Settings.Size; index++ { t.Particles[index] = new(Particle) err = dec.Decode(t.Particles[index]) } case *WorkflowResult: err = dec.Decode(&t.Msg) err = dec.Decode(&t.Success) default: err = dec.Decode(obj) } if err != nil { return fmt.Errorf( "unable to decode argument: %d, %v, with error: %v", i, reflect.TypeOf(obj), err) } } return nil } ================================================ FILE: cmd/samples/pso/functions.go ================================================ package main import "math" type ObjectiveFunction struct { name string // name of the function dim int // problem dimensionality xLo float64 // lower range limit xHi float64 // higher range limit Goal float64 // optimization goal (error threshold) Evaluate func(vec []float64) float64 // the objective function } var Sphere = ObjectiveFunction{ name: "sphere", dim: 3, xLo: -100, xHi: 100, Goal: 1e-5, Evaluate: EvalSphere, } var Rosenbrock = ObjectiveFunction{ name: "rosenbrock", dim: 3, xLo: -2.048, xHi: 2.048, Goal: 1e-5, Evaluate: EvalRosenbrock, } var Griewank = ObjectiveFunction{ name: "griewank", dim: 3, xLo: -600, xHi: 600, Goal: 1e-5, Evaluate: EvalGriewank, } func EvalSphere(vec []float64) float64 { var sum float64 = 0 for i := 0; i < len(vec); i++ { sum += math.Pow(vec[i], 2.0) } return sum } func EvalRosenbrock(vec []float64) float64 { var sum float64 = 0 for i := 0; i < len(vec)-1; i++ { sum += 100.0* math.Pow((vec[i+1]-math.Pow(vec[i], 2.0)), 2.0) + math.Pow((1-vec[i]), 2.0) } return sum } func EvalGriewank(vec []float64) float64 { var sum float64 = 0 var prod float64 = 1 for i := 0; i < len(vec); i++ { sum += math.Pow(vec[i], 2.0) prod *= math.Cos(vec[i] / math.Sqrt(float64(i+1))) } return sum/4000.0 - prod + 1.0 } ================================================ FILE: cmd/samples/pso/main.go ================================================ package main import ( "encoding/gob" "flag" "time" "github.com/pborman/uuid" "go.uber.org/cadence/client" "go.uber.org/cadence/encoded" "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, MaxConcurrentActivityExecutionSize: 1, // Activities are supposed to be CPU intensive, so better limit the concurrency DataConverter: h.DataConverter, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper, functionName string) { workflowOptions := client.StartWorkflowOptions{ ID: "PSO_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute * 60, DecisionTaskStartToCloseTimeout: time.Second * 10, // Measure of responsiveness of the worker to various server signals apart from start workflow. Small means faster recovery in the case of worker failure } h.StartWorkflow(workflowOptions, samplePSOWorkflow, functionName) } func main() { var mode, functionName, workflowID, runID, queryType string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger") flag.StringVar(&functionName, "f", "sphere", "One of [sphere, rosenbrock, griewank]") flag.StringVar(&workflowID, "w", "", "WorkflowID") flag.StringVar(&runID, "r", "", "RunID") flag.StringVar(&queryType, "t", "__stack_trace", "Query type is one of [__stack_trace, child, iteration]") flag.Parse() // If Gob is used to serialize data, then need to register types into gob as well??? // TOVERIFY: the test works even without type registation! const useGob = false var dataConverter encoded.DataConverter if useGob { dataConverter = NewGobDataConverter() gob.Register(Vector{}) gob.Register(Position{}) gob.Register(Particle{}) gob.Register(ObjectiveFunction{}) gob.Register(SwarmSettings{}) gob.Register(Swarm{}) } else { dataConverter = NewJSONDataConverter() } var h common.SampleHelper h.DataConverter = dataConverter h.SetupServiceConfig() // This configures DataConverter switch mode { case "worker": h.RegisterWorkflow(samplePSOWorkflow) h.RegisterWorkflow(samplePSOChildWorkflow) h.RegisterActivityWithAlias(initParticleActivity, initParticleActivityName) h.RegisterActivityWithAlias(updateParticleActivity, updateParticleActivityName) 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, functionName) case "query": h.QueryWorkflow(workflowID, runID, queryType) } } ================================================ FILE: cmd/samples/pso/particle.go ================================================ package main import "math/rand" type Particle struct { Position *Position Pbest *Position Velocity Vector } func NewParticle(swarm *Swarm, rng *rand.Rand) *Particle { particle := new(Particle) particle.Position = RandomPosition(swarm.Settings.function, rng) particle.Pbest = particle.Position.Copy() particle.Pbest.Fitness = 1e20 particle.Velocity = make([]float64, swarm.Settings.function.dim) xLo := swarm.Settings.function.xLo xHi := swarm.Settings.function.xHi for i := 0; i < swarm.Settings.function.dim; i++ { a := xLo + (xHi-xLo)*rng.Float64() b := xLo + (xHi-xLo)*rng.Float64() particle.Velocity[i] = (a - b) / 2.0 } return particle } func (particle *Particle) UpdateLocation(swarm *Swarm, rng *rand.Rand) { for i := 0; i < swarm.Settings.function.dim; i++ { // calculate stochastic coefficients rho1 := swarm.Settings.C1 * rng.Float64() rho2 := swarm.Settings.C2 * rng.Float64() // update velocity particle.Velocity[i] = swarm.Settings.Inertia*particle.Velocity[i] + rho1*(particle.Pbest.Location[i]-particle.Position.Location[i]) + rho2*(swarm.Gbest.Location[i]-particle.Position.Location[i]) particle.Position.Location[i] += particle.Velocity[i] } } func (particle *Particle) UpdateFitness(swarm *Swarm) { particle.Position.Fitness = swarm.Settings.function.Evaluate(particle.Position.Location) if particle.Position.IsBetterThan(particle.Pbest) { particle.Pbest = particle.Position.Copy() } } ================================================ FILE: cmd/samples/pso/position.go ================================================ package main import ( "math/rand" ) type Vector []float64 type Position struct { Location Vector Fitness float64 } func NewPosition(dim int) *Position { loc := make([]float64, dim) return &Position{ Location: loc, // Fitness: EvaluateFunction(settings.Function.Evaluate, loc), } } func RandomPosition(function ObjectiveFunction, rng *rand.Rand) *Position { pos := NewPosition(function.dim) xLo := function.xLo xHi := function.xHi for i := 0; i < len(pos.Location); i++ { pos.Location[i] = xLo + (xHi-xLo)*rng.Float64() } // pos.Fitness = EvaluateFunction(settings.Function.Evaluate, pos.Location) return pos } func (position *Position) Copy() *Position { newPosition := NewPosition(len(position.Location)) copy(newPosition.Location, position.Location) newPosition.Fitness = position.Fitness return newPosition } func (position *Position) IsBetterThan(other *Position) bool { return position.Fitness < other.Fitness } ================================================ FILE: cmd/samples/pso/settings.go ================================================ package main const pso_max_size int = 100 const pso_inertia float64 = 0.7298 // default value of w (see clerc02) type SwarmSettings struct { FunctionName string function ObjectiveFunction // lower case to avoid data converter export // swarm size (number of particles) Size int // ... N steps (set to 0 for no output) PrintEvery int // Steps after issuing a ContinueAsNew, to reduce history size ContinueAsNewEvery int // maximum number of iterations Steps int // cognitive coefficient C1 float64 // social coefficient C2 float64 // max inertia weight value InertiaMax float64 // min inertia weight value InertiaMin float64 // whether to keep particle position within defined bounds (TRUE) // or apply periodic boundary conditions (FALSE) ClampPosition bool Inertia float64 // current inertia weight value } func FunctionFactory(functionName string) ObjectiveFunction { var function ObjectiveFunction switch functionName { case "sphere": function = Sphere case "rosenbrock": function = Rosenbrock case "griewank": function = Griewank } return function } func PSODefaultSettings(functionName string) *SwarmSettings { settings := new(SwarmSettings) settings.FunctionName = functionName settings.function = FunctionFactory(functionName) settings.Size = CalculateSwarmSize(settings.function.dim, pso_max_size) settings.PrintEvery = 10 settings.ContinueAsNewEvery = 10 settings.Steps = 100000 settings.C1 = 1.496 settings.C2 = 1.496 settings.InertiaMax = pso_inertia settings.InertiaMin = 0.3 settings.Inertia = settings.InertiaMax settings.ClampPosition = true return settings } ================================================ FILE: cmd/samples/pso/swarm.go ================================================ package main import ( "errors" "fmt" "strconv" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) type ParticleResult struct { Position Step int } type Swarm struct { Settings *SwarmSettings Gbest *Position Particles []*Particle } func NewSwarm(ctx workflow.Context, settings *SwarmSettings) (*Swarm, error) { var swarm Swarm // store settings swarm.Settings = settings // initialize gbest swarm.Gbest = NewPosition(swarm.Settings.function.dim) swarm.Gbest.Fitness = 1e20 // initialize particles in parallel chunkResultChannel := workflow.NewChannel(ctx) swarm.Particles = make([]*Particle, settings.Size) for i := 0; i < swarm.Settings.Size; i++ { particleIdx := i workflow.Go(ctx, func(ctx workflow.Context) { var particle Particle err := workflow.ExecuteActivity(ctx, initParticleActivityName, swarm).Get(ctx, &particle) if err == nil { swarm.Particles[particleIdx] = &particle } chunkResultChannel.Send(ctx, err) }) } // wait for all particles to be initialized for i := 0; i < swarm.Settings.Size; i++ { var v interface{} chunkResultChannel.Receive(ctx, &v) switch r := v.(type) { case error: if r != nil { return &swarm, r } } } swarm.updateBest() return &swarm, nil } func (swarm *Swarm) updateBest() { for i := 0; i < swarm.Settings.Size; i++ { if swarm.Particles[i].Pbest.IsBetterThan(swarm.Gbest) { swarm.Gbest = swarm.Particles[i].Pbest.Copy() } } } func (swarm *Swarm) Run(ctx workflow.Context, step int) (ParticleResult, error) { logger := workflow.GetLogger(ctx) // Setup query handler for query type "iteration" var iterationMessage string err := workflow.SetQueryHandler(ctx, "iteration", func(input []byte) (string, error) { return iterationMessage, nil }) if err != nil { logger.Info("SetQueryHandler failed: " + err.Error()) return ParticleResult{}, err } // the algorithm goes here chunkResultChannel := workflow.NewChannel(ctx) for step <= swarm.Settings.Steps { logger.Info("Iteration ", zap.String("step", strconv.Itoa(step))) // Update particles in parallel for i := 0; i < swarm.Settings.Size; i++ { particleIdx := i workflow.Go(ctx, func(ctx workflow.Context) { var particle Particle err := workflow.ExecuteActivity(ctx, updateParticleActivityName, *swarm, particleIdx).Get(ctx, &particle) if err == nil { swarm.Particles[particleIdx] = &particle } chunkResultChannel.Send(ctx, err) }) } // Wait for all particles to be updated for i := 0; i < swarm.Settings.Size; i++ { var v interface{} chunkResultChannel.Receive(ctx, &v) switch r := v.(type) { case error: if r != nil { return ParticleResult{ Position: *swarm.Gbest, Step: step, }, r } } } logger.Debug("Iteration Update Swarm Best", zap.String("step", strconv.Itoa(step))) swarm.updateBest() // Check if the goal has reached then stop early if swarm.Gbest.Fitness < swarm.Settings.function.Goal { logger.Debug("Iteration New Swarm Best", zap.String("step", strconv.Itoa(step))) return ParticleResult{ Position: *swarm.Gbest, Step: step, }, nil } iterationMessage = fmt.Sprintf("Step %d :: min err=%.5e\n", step, swarm.Gbest.Fitness) if step%swarm.Settings.PrintEvery == 0 { logger.Info(iterationMessage) } // Finished all iterations if step == swarm.Settings.Steps { break } // Not finished yet, just continue as new to reduce history size if step%swarm.Settings.ContinueAsNewEvery == 0 { return ParticleResult{ Position: *swarm.Gbest, Step: step, }, errors.New(ContinueAsNewStr) } step++ } return ParticleResult{ Position: *swarm.Gbest, Step: step, }, nil } ================================================ FILE: cmd/samples/pso/utils.go ================================================ package main import ( "math" ) func CalculateSwarmSize(dim, max_size int) int { s := 10. + 2.*math.Sqrt(float64(dim)) size := int(math.Floor(s + 0.5)) if size > max_size { return max_size } else { return size } } ================================================ FILE: cmd/samples/pso/workflow.go ================================================ package main import ( "errors" "fmt" "time" "github.com/pborman/uuid" "go.uber.org/cadence" "go.uber.org/cadence/workflow" ) type WorkflowResult struct { Msg string // Uppercase the members otherwise serialization won't work! Success bool } // ApplicationName is the task list for this sample const ApplicationName = "PSO" // ActivityOptions can be reused var ActivityOptions = workflow.ActivityOptions{ ScheduleToStartTimeout: time.Second * 5, StartToCloseTimeout: time.Minute * 10, HeartbeatTimeout: time.Second * 2, // such a short timeout to make sample fail over very fast RetryPolicy: &cadence.RetryPolicy{ InitialInterval: time.Second, BackoffCoefficient: 2.0, MaximumInterval: time.Minute, ExpirationInterval: time.Minute * 10, MaximumAttempts: 5, NonRetriableErrorReasons: []string{"bad-error"}, }, } const ContinueAsNewStr = "CONTINUEASNEW" //samplePSOWorkflow workflow decider func samplePSOWorkflow(ctx workflow.Context, functionName string) (string, error) { logger := workflow.GetLogger(ctx) logger.Info(fmt.Sprintf("Optimizing function %s", functionName)) // Set activity options ctx = workflow.WithActivityOptions(ctx, ActivityOptions) // Setup query handler for query type "child" var childWorkflowID string err := workflow.SetQueryHandler(ctx, "child", func(input []byte) (string, error) { return childWorkflowID, nil }) if err != nil { msg := fmt.Sprintf("SetQueryHandler failed: " + err.Error()) logger.Error(msg) return msg, err } // Retry with different random seed settings := PSODefaultSettings(functionName) const NumberOfAttempts = 5 for i := 1; i < NumberOfAttempts; i++ { logger.Info(fmt.Sprintf("Attempt #%d", i)) swarm, err := NewSwarm(ctx, settings) if err != nil { msg := fmt.Sprintf("Optimization failed. " + err.Error()) logger.Error(msg) return msg, err } // Set child workflow options // Parent workflow can choose to specify it's own ID for child execution. Make sure they are unique for each execution. cwo := workflow.ChildWorkflowOptions{ WorkflowID: "PSO_Child_" + uuid.New(), ExecutionStartToCloseTimeout: time.Minute, } ctx = workflow.WithChildOptions(ctx, cwo) childWorkflowFuture := workflow.ExecuteChildWorkflow(ctx, samplePSOChildWorkflow, *swarm, 1) var childWE workflow.Execution childWorkflowFuture.GetChildWorkflowExecution().Get(ctx, &childWE) childWorkflowID = childWE.ID var result WorkflowResult err = childWorkflowFuture.Get(ctx, &result) // This blocking until the child workflow has finished if err != nil { msg := fmt.Sprintf("Parent execution received child execution failure. " + err.Error()) logger.Error(msg) return msg, err } if result.Success { msg := fmt.Sprintf("Optimization was successful at attempt #%d. %s", i, result.Msg) logger.Info(msg) return msg, nil } } msg := fmt.Sprintf("Unable to reach goal after %d attempts", NumberOfAttempts) logger.Info(msg) return msg, nil } // samplePSOChildWorkflow workflow decider // Returns true if the optimization has converged func samplePSOChildWorkflow(ctx workflow.Context, swarm Swarm, startingStep int) (WorkflowResult, error) { logger := workflow.GetLogger(ctx) logger.Info("Child workflow execution started.") // Set activity options ctx = workflow.WithActivityOptions(ctx, ActivityOptions) // Run real optimization loop result, err := swarm.Run(ctx, startingStep) if err != nil { if err.Error() == ContinueAsNewStr { return WorkflowResult{"NewContinueAsNewError", false}, workflow.NewContinueAsNewError(ctx, samplePSOChildWorkflow, swarm, result.Step+1) } msg := fmt.Sprintf("Error in swarm loop: " + err.Error()) logger.Error(msg) return WorkflowResult{msg, false}, errors.New("Error in swarm loop") } if result.Position.Fitness < swarm.Settings.function.Goal { msg := fmt.Sprintf("Yay! Goal was reached @ step %d (fitness=%.2e) :-)", result.Step, result.Position.Fitness) logger.Info(msg) return WorkflowResult{msg, true}, nil } msg := fmt.Sprintf("Goal was not reached after %d steps (fitness=%.2e) :-)", result.Step, result.Position.Fitness) logger.Info(msg) return WorkflowResult{msg, false}, nil } ================================================ FILE: cmd/samples/pso/workflow_test.go ================================================ package main import ( "context" "testing" "github.com/stretchr/testify/require" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" ) func Test_Workflow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestWorkflowEnvironment() env.RegisterWorkflow(samplePSOWorkflow) env.RegisterWorkflow(samplePSOChildWorkflow) env.RegisterActivityWithOptions(initParticleActivity, activity.RegisterOptions{ Name: initParticleActivityName, }) env.RegisterActivityWithOptions(updateParticleActivity, activity.RegisterOptions{ Name: updateParticleActivityName, }) var activityCalled []string //var dataConverter = NewGobDataConverter() var dataConverter = NewJSONDataConverter() workerOptions := worker.Options{ DataConverter: dataConverter, } env.SetWorkerOptions(workerOptions) // env.SetWorkflowTimeout(time.Minute * 5) // env.SetTestTimeout(time.Minute * 5) env.SetOnActivityStartedListener(func(activityInfo *activity.Info, ctx context.Context, args encoded.Values) { activityType := activityInfo.ActivityType.Name activityCalled = append(activityCalled, activityType) switch activityType { case "initParticleActivityName": case "updateParticleActivityName": default: panic("unexpected activity call") } }) var childWorkflowID string env.SetOnChildWorkflowStartedListener(func(workflowInfo *workflow.Info, ctx workflow.Context, args encoded.Values) { childWorkflowID = workflowInfo.WorkflowExecution.ID }) env.ExecuteWorkflow(samplePSOWorkflow, "sphere") require.True(t, env.IsWorkflowCompleted()) queryAndVerify(t, env, "child", childWorkflowID) //queryAndVerify(t, env, "iteration", "???") require.Equal(t, env.GetWorkflowError().Error(), "ContinueAsNew") // consider recreating a new test env on every iteration and calling execute workflow with the arguments from the previous iteration (contained in ContinueAsNewError) } func queryAndVerify(t *testing.T, env *testsuite.TestWorkflowEnvironment, query string, expectedState string) { result, err := env.QueryWorkflow(query) require.NoError(t, err) var state string err = result.Get(&state) require.NoError(t, err) require.Equal(t, expectedState, state) } ================================================ FILE: cmd/samples/recipes/branch/branch_workflow.go ================================================ package main import ( "fmt" "time" "go.uber.org/cadence/workflow" ) /** * This sample workflow executes multiple branches in parallel. The number of branches is controlled by passed in parameter. */ const ( // ApplicationName is the task list for this sample ApplicationName = "branchGroup" totalBranches = 3 ) // sampleBranchWorkflow workflow decider func sampleBranchWorkflow(ctx workflow.Context) error { var futures []workflow.Future // starts activities in parallel ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) for i := 1; i <= totalBranches; i++ { activityInput := fmt.Sprintf("branch %d of %d.", i, totalBranches) future := workflow.ExecuteActivity(ctx, sampleActivity, activityInput) futures = append(futures, future) } // wait until all futures are done for _, future := range futures { if err := future.Get(ctx, nil); err != nil { return err } } workflow.GetLogger(ctx).Info("Workflow completed.") return nil } func sampleActivity(input string) (string, error) { name := "sampleActivity" fmt.Printf("Run %s with input %v \n", name, input) return "Result_" + name, nil } ================================================ FILE: cmd/samples/recipes/branch/main.go ================================================ 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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflowParallel(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "parallel_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, sampleParallelWorkflow) } func startWorkflowBranch(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "branch_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, sampleBranchWorkflow) } func main() { var mode, sampleCase string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.StringVar(&sampleCase, "c", "", "Sample case to run.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleBranchWorkflow) h.RegisterWorkflow(sampleParallelWorkflow) h.RegisterActivity(sampleActivity) 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": switch sampleCase { case "branch": startWorkflowBranch(&h) default: startWorkflowParallel(&h) } } } ================================================ FILE: cmd/samples/recipes/branch/parallel_workflow.go ================================================ package main import ( "errors" "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This sample workflow executes multiple branches in parallel using workflow.Go() method. */ // sampleParallelWorkflow workflow decider func sampleParallelWorkflow(ctx workflow.Context) error { waitChannel := workflow.NewChannel(ctx) ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) workflow.Go(ctx, func(ctx workflow.Context) { err := workflow.ExecuteActivity(ctx, sampleActivity, "branch1.1").Get(ctx, nil) if err != nil { logger.Error("Activity failed", zap.Error(err)) waitChannel.Send(ctx, err.Error()) return } err = workflow.ExecuteActivity(ctx, sampleActivity, "branch1.2").Get(ctx, nil) if err != nil { logger.Error("Activity failed", zap.Error(err)) waitChannel.Send(ctx, err.Error()) return } waitChannel.Send(ctx, "") }) workflow.Go(ctx, func(ctx workflow.Context) { err := workflow.ExecuteActivity(ctx, sampleActivity, "branch2").Get(ctx, nil) if err != nil { logger.Error("Activity failed", zap.Error(err)) waitChannel.Send(ctx, err.Error()) return } waitChannel.Send(ctx, "") }) // wait for both of the coroutinue to complete. var errMsg string for i := 0; i != 2; i++ { waitChannel.Receive(ctx, &errMsg) if errMsg != "" { err := errors.New(errMsg) logger.Error("Coroutine failed", zap.Error(err)) return err } } logger.Info("Workflow completed.") return nil } ================================================ FILE: cmd/samples/recipes/branch/workflow_test.go ================================================ package main import ( "testing" "github.com/stretchr/testify/suite" "go.uber.org/cadence/testsuite" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(sampleBranchWorkflow) s.env.RegisterWorkflow(sampleParallelWorkflow) s.env.RegisterActivity(sampleActivity) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_BranchWorkflow() { s.env.ExecuteWorkflow(sampleBranchWorkflow) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) } func (s *UnitTestSuite) Test_ParallelWorkflow() { s.env.ExecuteWorkflow(sampleParallelWorkflow) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) } ================================================ FILE: cmd/samples/recipes/cancelactivity/main.go ================================================ 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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "cancel_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute * 30, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, sampleCancelWorkflow) } func cancelWorkflow(h *common.SampleHelper, wid string) { h.CancelWorkflow(wid) } func main() { var mode, wid string flag.StringVar(&mode, "m", "trigger", "Mode is worker, trigger or cancel.") flag.StringVar(&wid, "w", "", "w is the workflowID of the workflow to be canceled.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleCancelWorkflow) h.RegisterActivity(activityToBeCanceled) h.RegisterActivity(activityToBeSkipped) h.RegisterActivity(cleanupActivity) 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) case "cancel": cancelWorkflow(&h, wid) } } ================================================ FILE: cmd/samples/recipes/cancelactivity/workflow.go ================================================ package main import ( "context" "fmt" "time" "go.uber.org/cadence" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This is the cancel activity workflow sample. */ // ApplicationName is the task list for this sample const ApplicationName = "cancelGroup" // sampleCancelWorkflow workflow decider func sampleCancelWorkflow(ctx workflow.Context) (retError error) { ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute * 30, HeartbeatTimeout: time.Second * 5, WaitForCancellation: true, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) logger.Info("cancel workflow started") defer func() { if cadence.IsCanceledError(retError) { // When workflow is canceled, it has to get a new disconnected context to execute any activities newCtx, _ := workflow.NewDisconnectedContext(ctx) err := workflow.ExecuteActivity(newCtx, cleanupActivity).Get(ctx, nil) if err != nil { logger.Error("Cleanup activity failed", zap.Error(err)) retError = err return } retError = nil logger.Info("Workflow completed.") } }() var result string err := workflow.ExecuteActivity(ctx, activityToBeCanceled).Get(ctx, &result) if err != nil && !cadence.IsCanceledError(err) { logger.Error("Error from activityToBeCanceled", zap.Error(err)) return err } logger.Info(fmt.Sprintf("activityToBeCanceled returns %v, %v", result, err)) // Execute activity using a canceled ctx, // activity won't be scheduled and an cancelled error will be returned err = workflow.ExecuteActivity(ctx, activityToBeSkipped).Get(ctx, nil) if err != nil && !cadence.IsCanceledError(err) { logger.Error("Error from activityToBeSkipped", zap.Error(err)) } return err } func activityToBeCanceled(ctx context.Context) (string, error) { logger := activity.GetLogger(ctx) logger.Info("activity started, to cancel workflow, use ./cancelactivity -m cancel -w or CLI: 'cadence --do default wf cancel -w ' to cancel") for { select { case <-time.After(1 * time.Second): logger.Info("heartbeating...") activity.RecordHeartbeat(ctx, "") case <-ctx.Done(): logger.Info("context is cancelled") // returned canceled error here so that in workflow history we can see ActivityTaskCanceled event // or if not cancelled, return timeout error return "I am canceled by Done", ctx.Err() } } } func cleanupActivity(ctx context.Context) error { logger := activity.GetLogger(ctx) logger.Info("cleanupActivity started") return nil } func activityToBeSkipped(ctx context.Context) error { logger := activity.GetLogger(ctx) logger.Info("this activity will be skipped due to cancellation") return nil } ================================================ FILE: cmd/samples/recipes/cancelactivity/workflow_test.go ================================================ package main import ( "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" ) func TestCancelWorkflow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} canceledActivityName := "canceledActivity" skippedActivityName := "skippedActivity" cleanupActivityName := "cleanupActivity" env := testSuite.NewTestWorkflowEnvironment() env.RegisterWorkflow(sampleCancelWorkflow) env.RegisterActivityWithOptions(activityToBeCanceled, activity.RegisterOptions{ Name: canceledActivityName, }) env.RegisterActivityWithOptions(activityToBeSkipped, activity.RegisterOptions{ Name: skippedActivityName, }) env.RegisterActivityWithOptions(cleanupActivity, activity.RegisterOptions{ Name: cleanupActivityName, }) canceledActivityCanceled := false env.SetOnActivityCanceledListener(func(activityInfo *activity.Info) { if activityInfo.ActivityType.Name == canceledActivityName { canceledActivityCanceled = true } }) skippedActivityExecuted := false cleanupActivityCompleted := false env.SetOnActivityCompletedListener(func(activityInfo *activity.Info, result encoded.Value, err error) { switch activityInfo.ActivityType.Name { case cleanupActivityName: cleanupActivityCompleted = true case skippedActivityName: skippedActivityExecuted = true } }) env.RegisterDelayedCallback(func() { env.CancelWorkflow() }, time.Second) env.ExecuteWorkflow(sampleCancelWorkflow) require.True(t, env.IsWorkflowCompleted()) require.NoError(t, env.GetWorkflowError()) require.True(t, canceledActivityCanceled) require.False(t, skippedActivityExecuted) require.True(t, cleanupActivityCompleted) } ================================================ FILE: cmd/samples/recipes/childworkflow/child_workflow.go ================================================ package main import ( "errors" "fmt" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This sample workflow demonstrates how to use invoke child workflow from parent workflow execution. Each child * workflow execution is starting a new run and parent execution is notified only after the completion of last run. */ // sampleChildWorkflow workflow decider func sampleChildWorkflow(ctx workflow.Context, totalCount, runCount int) (string, error) { logger := workflow.GetLogger(ctx) logger.Info("Child workflow execution started.") if runCount <= 0 { logger.Error("Invalid valid for run count.", zap.Int("RunCount", runCount)) return "", errors.New("invalid run count") } totalCount++ runCount-- if runCount == 0 { result := fmt.Sprintf("Child workflow execution completed after %v runs", totalCount) logger.Info("Child workflow completed.", zap.String("Result", result)) return result, nil } logger.Info("Child workflow starting new run.", zap.Int("RunCount", runCount), zap.Int("TotalCount", totalCount)) return "", workflow.NewContinueAsNewError(ctx, sampleChildWorkflow, totalCount, runCount) } ================================================ FILE: cmd/samples/recipes/childworkflow/main.go ================================================ 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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "parentworkflow_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, sampleParentWorkflow) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleChildWorkflow) h.RegisterWorkflow(sampleParentWorkflow) 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) } } ================================================ FILE: cmd/samples/recipes/childworkflow/parent_workflow.go ================================================ package main import ( "fmt" "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This sample workflow demonstrates how to use invoke child workflow from parent workflow execution. Each child * workflow execution is starting a new run and parent execution is notified only after the completion of last run. */ // ApplicationName is the task list for this sample const ApplicationName = "childWorkflowGroup" // sampleParentWorkflow workflow decider func sampleParentWorkflow(ctx workflow.Context) error { logger := workflow.GetLogger(ctx) execution := workflow.GetInfo(ctx).WorkflowExecution // Parent workflow can choose to specify it's own ID for child execution. Make sure they are unique for each execution. childID := fmt.Sprintf("child_workflow:%v", execution.RunID) cwo := workflow.ChildWorkflowOptions{ // Do not specify WorkflowID if you want cadence to generate a unique ID for child execution WorkflowID: childID, ExecutionStartToCloseTimeout: time.Minute, } ctx = workflow.WithChildOptions(ctx, cwo) var result string err := workflow.ExecuteChildWorkflow(ctx, sampleChildWorkflow, 0, 5).Get(ctx, &result) if err != nil { logger.Error("Parent execution received child execution failure.", zap.Error(err)) return err } logger.Info("Parent execution completed.", zap.String("Result", result)) return nil } ================================================ FILE: cmd/samples/recipes/choice/exclusive_choice_workflow.go ================================================ package main import ( "fmt" "math/rand" "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This sample workflow Execute one of many code paths based on the result of an activity. */ const ( // ApplicationName is the task list for this sample ApplicationName = "choiceGroup" orderChoiceApple = "apple" orderChoiceBanana = "banana" orderChoiceCherry = "cherry" orderChoiceOrange = "orange" ) var _orderChoices = []string{orderChoiceApple, orderChoiceBanana, orderChoiceCherry, orderChoiceOrange} // exclusiveChoiceWorkflow Workflow Decider. func exclusiveChoiceWorkflow(ctx workflow.Context) error { // Get order. ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) var orderChoice string err := workflow.ExecuteActivity(ctx, getOrderActivity).Get(ctx, &orderChoice) if err != nil { return err } logger := workflow.GetLogger(ctx) // choose next activity based on order result switch orderChoice { case orderChoiceApple: workflow.ExecuteActivity(ctx, orderAppleActivity, orderChoice) case orderChoiceBanana: workflow.ExecuteActivity(ctx, orderBananaActivity, orderChoice) case orderChoiceCherry: workflow.ExecuteActivity(ctx, orderCherryActivity, orderChoice) case orderChoiceOrange: workflow.ExecuteActivity(ctx, orderOrangeActivity, orderChoice) default: logger.Error("Unexpected order", zap.String("Choice", orderChoice)) } logger.Info("Workflow completed.") return nil } func getOrderActivity() (string, error) { idx := rand.Intn(len(_orderChoices)) order := _orderChoices[idx] fmt.Printf("Order is for %s\n", order) return order, nil } func orderAppleActivity(choice string) error { fmt.Printf("Order choice: %v\n", choice) return nil } func orderBananaActivity(choice string) error { fmt.Printf("Order choice: %v\n", choice) return nil } func orderCherryActivity(choice string) error { fmt.Printf("Order choice: %v\n", choice) return nil } func orderOrangeActivity(choice string) error { fmt.Printf("Order choice: %v\n", choice) return nil } ================================================ FILE: cmd/samples/recipes/choice/main.go ================================================ 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, } // Start Worker. h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflowMultiChoice(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "multi_choice_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, multiChoiceWorkflow) } func startWorkflowExclusiveChoice(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "single_choice_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, exclusiveChoiceWorkflow) } func main() { var mode, sampleCase string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.StringVar(&sampleCase, "c", "single", "Sample case to run.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(exclusiveChoiceWorkflow) h.RegisterWorkflow(multiChoiceWorkflow) h.RegisterActivity(getOrderActivity) h.RegisterActivity(orderAppleActivity) h.RegisterActivity(orderBananaActivity) h.RegisterActivity(orderCherryActivity) h.RegisterActivity(orderOrangeActivity) h.RegisterActivity(getBasketOrderActivity) 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": switch sampleCase { case "multi": startWorkflowMultiChoice(&h) default: startWorkflowExclusiveChoice(&h) } } } ================================================ FILE: cmd/samples/recipes/choice/multi_choice_workflow.go ================================================ package main import ( "context" "math/rand" "time" "github.com/pkg/errors" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This multi choice sample workflow Execute different parallel branches based on the result of an activity. */ // multiChoiceWorkflow Workflow Decider. func multiChoiceWorkflow(ctx workflow.Context) error { // Get basket order. ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) var choices []string err := workflow.ExecuteActivity(ctx, getBasketOrderActivity).Get(ctx, &choices) if err != nil { return err } logger := workflow.GetLogger(ctx) var futures []workflow.Future for _, item := range choices { // choose next activity based on order result var f workflow.Future switch item { case orderChoiceApple: f = workflow.ExecuteActivity(ctx, orderAppleActivity, item) case orderChoiceBanana: f = workflow.ExecuteActivity(ctx, orderBananaActivity, item) case orderChoiceCherry: f = workflow.ExecuteActivity(ctx, orderCherryActivity, item) case orderChoiceOrange: f = workflow.ExecuteActivity(ctx, orderOrangeActivity, item) default: logger.Error("Unexpected order.", zap.String("Order", item)) return errors.New("Invalid Choice") } futures = append(futures, f) } // wait until all items in the basket order are processed for _, future := range futures { future.Get(ctx, nil) } logger.Info("Workflow completed.") return nil } func getBasketOrderActivity(ctx context.Context) ([]string, error) { var basket []string for _, item := range _orderChoices { // some random decision if rand.Float32() <= 0.65 { basket = append(basket, item) } } if len(basket) == 0 { basket = append(basket, _orderChoices[rand.Intn(len(_orderChoices))]) } activity.GetLogger(ctx).Info("Get basket order.", zap.Strings("Orders", basket)) return basket, nil } ================================================ FILE: cmd/samples/recipes/choice/workflow_test.go ================================================ package main import ( "testing" "github.com/stretchr/testify/suite" "go.uber.org/cadence/testsuite" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(exclusiveChoiceWorkflow) s.env.RegisterWorkflow(multiChoiceWorkflow) s.env.RegisterActivity(getOrderActivity) s.env.RegisterActivity(orderAppleActivity) s.env.RegisterActivity(orderBananaActivity) s.env.RegisterActivity(orderCherryActivity) s.env.RegisterActivity(orderOrangeActivity) s.env.RegisterActivity(getBasketOrderActivity) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_ExclusiveChoiceWorkflow() { s.env.ExecuteWorkflow(exclusiveChoiceWorkflow) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) } func (s *UnitTestSuite) Test_MultiChoiceWorkflow() { s.env.ExecuteWorkflow(multiChoiceWorkflow) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) } ================================================ FILE: cmd/samples/recipes/consistentquery/README.md ================================================ This sample workflow demos how to use consistent query API to get the current state of running workflow. query_workflow.go shows how to setup a custom workflow query handler query_workflow_test.go shows how to unit-test query functionality Steps to run this sample: 1) You need a cadence service running. See details in cmd/samples/README.md 2) Run the following command to start worker ``` ./bin/query -m worker ``` 3) Run the following command to trigger a workflow execution. You should see workflowID and runID print out on screen. ``` ./bin/query -m trigger ``` It will start a workflow and then using signal+consistent query to operate the workflow. ================================================ FILE: cmd/samples/recipes/consistentquery/main.go ================================================ package main import ( "flag" "fmt" "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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func main() { var mode, workflowID, runID, queryType string flag.StringVar(&mode, "m", "trigger", "Mode is worker, trigger or query.") flag.StringVar(&workflowID, "w", "", "WorkflowID") flag.StringVar(&runID, "r", "", "RunID") flag.StringVar(&queryType, "t", "__stack_trace", "QueryType") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(queryWorkflow) 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": wfID := "query_" + uuid.New() workflowOptions := client.StartWorkflowOptions{ ID: wfID, TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Hour * 10, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, queryWorkflow) result := -1 h.ConsistentQueryWorkflow(&result, wfID, "", "state") fmt.Println("initial query result after started:", result) h.SignalWorkflow(wfID, "increase", nil) h.ConsistentQueryWorkflow(&result, wfID, "", "state") fmt.Println("query after 1 increase:", result) h.SignalWorkflow(wfID, "increase", nil) h.SignalWorkflow(wfID, "increase", nil) h.SignalWorkflow(wfID, "increase", nil) h.SignalWorkflow(wfID, "increase", nil) h.ConsistentQueryWorkflow(&result, wfID, "", "state") fmt.Println("query after 1 +4 increase:", result) } } ================================================ FILE: cmd/samples/recipes/consistentquery/query_workflow.go ================================================ package main import ( "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) // ApplicationName is the task list for this sample const ApplicationName = "queryGroup" // queryWorkflow is an implementation of cadence workflow to demo how to setup query handler func queryWorkflow(ctx workflow.Context) error { queryResult := 0 logger := workflow.GetLogger(ctx) logger.Info("QueryWorkflow started") // setup query handler for query type "state" err := workflow.SetQueryHandler(ctx, "state", func(input []byte) (int, error) { return queryResult, nil }) if err != nil { logger.Info("SetQueryHandler failed: " + err.Error()) return err } signalChan := workflow.GetSignalChannel(ctx, "increase") s := workflow.NewSelector(ctx) s.AddReceive(signalChan, func(c workflow.Channel, more bool) { c.Receive(ctx, nil) queryResult +=1 workflow.GetLogger(ctx).Info("Received signal!", zap.String("signal", "increase")) }) workflow.Go(ctx, func(ctx workflow.Context) { for { s.Select(ctx) } }) // to simulate workflow been blocked on something, we wait for a timer workflow.NewTimer(ctx, time.Minute*2).Get(ctx, nil) logger.Info("Timer fired") logger.Info("QueryWorkflow completed") return nil } ================================================ FILE: cmd/samples/recipes/crossdomain/main.go ================================================ package main import ( "context" "flag" "github.com/google/uuid" apiv1 "github.com/uber/cadence-idl/go/proto/api/v1" "go.uber.org/cadence/compatibility" "go.uber.org/yarpc" "go.uber.org/yarpc/transport/grpc" "go.uber.org/zap" "time" "go.uber.org/cadence/client" "go.uber.org/cadence/worker" ) const ( tasklist0 = "cross-domain-tl0" tasklist1 = "cross-domain-tl1" tasklist2 = "cross-domain-tl2" domain0 = "domain0" domain1 = "domain1" domain2 = "domain2" portCluster0 = "127.0.0.1:7833" portCluster1 = "127.0.0.1:8833" portCluster2 = "127.0.0.1:9833" ) func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker, trigger.") flag.Parse() logger, err := zap.NewProduction() if err != nil { panic(err) } switch mode { case "worker0": setupWorker(domain0, tasklist0, portCluster0, []interface{}{wf0}, []interface{}{}) logger.Info("workers running for cluster 0....") select {} case "worker1": setupWorker(domain1, tasklist1, portCluster1, []interface{}{wf1}, []interface{}{activity1}) logger.Info("workers running for cluster 1....") select {} case "worker2": setupWorker(domain2, tasklist2, portCluster2, []interface{}{wf2}, []interface{}{activity2}) logger.Info("workers running for cluster 1....") select {} case "start": client1 := setupClient(domain0, portCluster0) id := uuid.New().String() ctx, close := context.WithTimeout(context.Background(), time.Second*30) defer close() res, err := client1.StartWorkflow(ctx, client.StartWorkflowOptions{ ID: id, TaskList: tasklist0, ExecutionStartToCloseTimeout: 30 * time.Second, }, wf0) if err != nil { logger.Error("error starting workflow", zap.Error(err)) } logger.Info("started workflow for domain0", zap.String("wf-id", id), zap.Any("start-wf", res)) } } func setupClient(domain string, hostport string) client.Client { dispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: "client", Outbounds: yarpc.Outbounds{ "cadence-frontend": {Unary: grpc.NewTransport().NewSingleOutbound(hostport)}, }, }) err := dispatcher.Start() if err != nil { panic(err) } clientConfig := dispatcher.ClientConfig("cadence-frontend") svc := compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ) return client.NewClient( svc, domain, &client.Options{ FeatureFlags: client.FeatureFlags{ WorkflowExecutionAlreadyCompletedErrorEnabled: true, }, }) } func setupWorker(domain string, tl string, hostport string, wfs []interface{}, activities []interface{}) { dispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: "client", Outbounds: yarpc.Outbounds{ "cadence-frontend": {Unary: grpc.NewTransport().NewSingleOutbound(hostport)}, }, }) logger, err := zap.NewProduction() if err != nil { panic(err) } err = dispatcher.Start() if err != nil { panic(err) } workerOptions := worker.Options{ Logger: logger, FeatureFlags: client.FeatureFlags{ WorkflowExecutionAlreadyCompletedErrorEnabled: true, }, } clientConfig := dispatcher.ClientConfig("cadence-frontend") svc := compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ) worker := worker.New(svc, domain, tl, workerOptions) for i := range wfs { worker.RegisterWorkflow(wfs[i]) } for i := range activities { worker.RegisterActivity(activities[i]) } err = worker.Start() if err != nil { panic(err) } } ================================================ FILE: cmd/samples/recipes/crossdomain/wf.go ================================================ package main import ( "context" "github.com/google/uuid" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" "time" ) // some sample data to be passed around type Data struct { Val string } func wf0(ctx workflow.Context) error { // first try launching a child workflow in another cluster, active in another region logger := workflow.GetLogger(ctx) logger.Info("starting child workflow in domain1, cluster 1") ctx1 := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{ Domain: domain1, WorkflowID: "wf-domain-1-" + uuid.New().String(), TaskList: tasklist1, ExecutionStartToCloseTimeout: 1 * time.Minute, }) err := workflow.ExecuteChildWorkflow(ctx1, wf1, Data{Val: "test"}).Get(ctx1, nil) if err != nil { logger.Error("got error executing child workflow", zap.Error(err)) } logger.Info("Cross-cluster cross-domain workflow completed", zap.Any("return-value", nil)) // now try a workflow active in the same cluster ctx2 := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{ Domain: domain2, WorkflowID: "wf-domain-2" + uuid.New().String(), TaskList: tasklist2, ExecutionStartToCloseTimeout: 1 * time.Minute, }) err = workflow.ExecuteChildWorkflow(ctx2, wf2, Data{Val: "test"}).Get(ctx2, nil) if err != nil { logger.Error("got error executing child workflow", zap.Error(err)) } logger.Info("same-cluster cross-domain child-workflow completed.") return nil } func wf1(ctx workflow.Context, args Data) error { if args.Val != "test" { panic("wf1 did not receive expected args") } ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: 4 * time.Minute, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) logger.Info("workflow wf1 starting activity.") err := workflow.ExecuteActivity(ctx, activity1).Get(ctx, nil) if err != nil { logger.Error("activity error", zap.Error(err)) } logger.Info("workflow wf1 completed activity.") logger.Info("workflow wf1 completed.") return nil } func wf2(ctx workflow.Context, args Data) error { logger := workflow.GetLogger(ctx) ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: 4 * time.Minute, } ctx = workflow.WithActivityOptions(ctx, ao) if args.Val != "test" { panic("wf1 did not receive expected args") } err := workflow.ExecuteActivity(ctx, activity2).Get(ctx, nil) if err != nil { logger.Error("activity error", zap.Error(err)) } logger.Info("workflow wf1 completed.") return nil } func activity1(ctx context.Context) (string, error) { logger := activity.GetLogger(ctx) logger.Info("activity 1 - running") return "Hello - activity 1", nil } func activity2(ctx context.Context) (string, error) { logger := activity.GetLogger(ctx) logger.Info("activity 2 - running") return "Hello - activity 2", nil } ================================================ FILE: cmd/samples/recipes/ctxpropagation/README.md ================================================ This sample workflow demos context propagation through a workflow. Details about context propagation are available [here](https://cadenceworkflow.io/docs/03_goclient/16_tracing). The sample workflow initializes the client with a context propagator which propagates specific information in the `context.Context` object across the workflow. The `context.Context` object is populated with the information prior to calling `StartWorkflow`. The workflow demonstrates that the information is available in the workflow and any activities executed. Steps to run this sample: 1) You need a cadence service running. See details in cmd/samples/README.md 2) Run the following command multiple times on different console window. This is to simulate running workers on multiple different machines. ``` ./bin/ctxpropagation -m worker ``` 3) Run the following command to execute the context . ``` ./bin/ctxpropagation -m trigger ``` You should see prints showing the context information available in the workflow and activities. ================================================ FILE: cmd/samples/recipes/ctxpropagation/activities.go ================================================ package main import ( "context" ) const ( sampleActivityName = "sampleActivity" ) func sampleActivity(ctx context.Context) (*Values, error) { if val := ctx.Value(propagateKey); val != nil { vals := val.(Values) return &vals, nil } return nil, nil } ================================================ FILE: cmd/samples/recipes/ctxpropagation/main.go ================================================ package main import ( "context" "flag" "time" "github.com/pborman/uuid" "go.uber.org/cadence/client" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "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. Setup a custom context propagator. workerOptions := worker.Options{ MetricsScope: h.WorkerMetricScope, Logger: h.Logger, EnableLoggingInReplay: true, ContextPropagators: []workflow.ContextPropagator{ NewContextPropagator(), }, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "ctxprop_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } ctx := context.Background() ctx = context.WithValue(ctx, propagateKey, &Values{Key: "test", Value: "tested"}) h.StartWorkflowWithCtx(ctx, workflowOptions, sampleCtxPropWorkflow) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper // Setup two context propagators - one string and one custom context. h.CtxPropagators = []workflow.ContextPropagator{ NewContextPropagator(), } h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleCtxPropWorkflow) h.RegisterActivityWithAlias(sampleActivity, sampleActivityName) 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) } } ================================================ FILE: cmd/samples/recipes/ctxpropagation/propagator.go ================================================ package main import ( "context" "encoding/json" "go.uber.org/cadence/.gen/go/shared" "go.uber.org/cadence/workflow" ) type ( // contextKey is an unexported type used as key for items stored in the // Context object contextKey struct{} // propagator implements the custom context propagator propagator struct{} // Values is a struct holding values Values struct { Key string `json:"key"` Value string `json:"value"` } ) // propagateKey is the key used to store the value in the Context object var propagateKey = contextKey{} // propagationKey is the key used by the propagator to pass values through the // cadence server headers const propagationKey = "_prop" // NewContextPropagator returns a context propagator that propagates a set of // string key-value pairs across a workflow func NewContextPropagator() workflow.ContextPropagator { return &propagator{} } // Inject injects values from context into headers for propagation func (s *propagator) Inject(ctx context.Context, writer workflow.HeaderWriter) error { value := ctx.Value(propagateKey) payload, err := json.Marshal(value) if err != nil { return err } writer.Set(propagationKey, payload) return nil } // InjectFromWorkflow injects values from context into headers for propagation func (s *propagator) InjectFromWorkflow(ctx workflow.Context, writer workflow.HeaderWriter) error { value := ctx.Value(propagateKey) payload, err := json.Marshal(value) if err != nil { return err } writer.Set(propagationKey, payload) return nil } // Extract extracts values from headers and puts them into context func (s *propagator) Extract(ctx context.Context, reader workflow.HeaderReader) (context.Context, error) { if err := reader.ForEachKey(func(key string, value []byte) error { if key == propagationKey { var values Values if err := json.Unmarshal(value, &values); err != nil { return err } ctx = context.WithValue(ctx, propagateKey, values) } return nil }); err != nil { return nil, err } return ctx, nil } // ExtractToWorkflow extracts values from headers and puts them into context func (s *propagator) ExtractToWorkflow(ctx workflow.Context, reader workflow.HeaderReader) (workflow.Context, error) { if err := reader.ForEachKey(func(key string, value []byte) error { if key == propagationKey { var values Values if err := json.Unmarshal(value, &values); err != nil { return err } ctx = workflow.WithValue(ctx, propagateKey, values) } return nil }); err != nil { return nil, err } return ctx, nil } // SetValuesInHeader places the Values container inside the header func SetValuesInHeader(values Values, header *shared.Header) error { payload, err := json.Marshal(values) if err == nil { header.Fields[propagationKey] = payload } else { return err } return nil } ================================================ FILE: cmd/samples/recipes/ctxpropagation/workflow.go ================================================ package main import ( "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) // ApplicationName is the task list for this sample const ApplicationName = "CtxPropagatorGroup" // sampleCtxPropWorkflow workflow decider func sampleCtxPropWorkflow(ctx workflow.Context) (err error) { ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Second * 5, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 2, // such a short timeout to make sample fail over very fast } ctx = workflow.WithActivityOptions(ctx, ao) if val := ctx.Value(propagateKey); val != nil { vals := val.(Values) workflow.GetLogger(ctx).Info("custom context propagated to workflow", zap.String(vals.Key, vals.Value)) } var values Values if err = workflow.ExecuteActivity(ctx, sampleActivity).Get(ctx, &values); err != nil { workflow.GetLogger(ctx).Error("Workflow failed.", zap.Error(err)) return err } workflow.GetLogger(ctx).Info("context propagated to activity", zap.String(values.Key, values.Value)) workflow.GetLogger(ctx).Info("Workflow completed.") return nil } ================================================ FILE: cmd/samples/recipes/ctxpropagation/workflow_test.go ================================================ package main import ( "context" "testing" "github.com/stretchr/testify/suite" "go.uber.org/cadence/.gen/go/shared" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment header *shared.Header } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { // Set Context Propagators and Headers, these will be shared across all Context objects in the test contextPropagators := []workflow.ContextPropagator{NewContextPropagator()} s.header = &shared.Header{ Fields: make(map[string][]byte), } s.SetContextPropagators(contextPropagators) s.SetHeader(s.header) workerOptions := worker.Options{ ContextPropagators: contextPropagators, } s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(sampleCtxPropWorkflow) s.env.RegisterActivityWithOptions(sampleActivity, activity.RegisterOptions{Name: "sampleActivity"}) s.env.SetWorkerOptions(workerOptions) } func (s *UnitTestSuite) Test_CtxPropWorkflow() { expectedCall := []string{ "sampleActivity", } var activityCalled []string values := Values{Key: "sampleKey", Value: "sampleValue"} // Place the values to be propagated in the header SetValuesInHeader(values, s.header) s.env.SetOnActivityStartedListener(func(activityInfo *activity.Info, ctx context.Context, args encoded.Values) { activityType := activityInfo.ActivityType.Name activityCalled = append(activityCalled, activityType) if activityType != expectedCall[0] { panic("unexpected activity call") } actualValuesInCtx := ctx.Value(propagateKey).(Values) if actualValuesInCtx.Key != values.Key { panic("there was a problem propagating Values, the key field doesn't match") } if actualValuesInCtx.Value != values.Value { panic("there was a problem propagating Values, the value field doesn't match") } }) s.env.ExecuteWorkflow(sampleCtxPropWorkflow) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) s.Equal(expectedCall, activityCalled) } ================================================ FILE: cmd/samples/recipes/delaystart/delaystart_workflow.go ================================================ package main import ( "context" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This is the hello world workflow sample. */ // ApplicationName is the task list for this sample const ApplicationName = "delaystartGroup" const delayStartWorkflowName = "delayStartWorkflow" // helloWorkflow workflow decider func delayStartWorkflow(ctx workflow.Context, delayStart time.Duration) error { ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) logger.Info("delaystart workflow started after waiting for " + delayStart.String()) var helloworldResult string err := workflow.ExecuteActivity(ctx, delayStartActivity, delayStart).Get(ctx, &helloworldResult) if err != nil { logger.Error("Activity failed after waiting for "+delayStart.String(), zap.Error(err)) return err } // Adding a new activity to the workflow will result in a non-determinstic change for the workflow // Please check https://cadenceworkflow.io/docs/go-client/workflow-versioning/ for more information // // Un-commenting the following code and the TestReplayWorkflowHistoryFromFile in replay_test.go // will fail due to the non-determinstic change // // If you have a completed workflow execution without the following code and run the // TestWorkflowShadowing in shadow_test.go or start the worker in shadow mode (using -m shadower) // those two shadowing check will also fail due to the non-deterministic change // // err := workflow.ExecuteActivity(ctx, helloWorldActivity, name).Get(ctx, &helloworldResult) // if err != nil { // logger.Error("Activity failed.", zap.Error(err)) // return err // } logger.Info("Workflow completed.", zap.String("Result", helloworldResult)) return nil } func delayStartActivity(ctx context.Context, delayStart time.Duration) (string, error) { logger := activity.GetLogger(ctx) logger.Info("delayStartActivity started after " + delayStart.String()) return "Activity started after " + delayStart.String(), nil } ================================================ FILE: cmd/samples/recipes/delaystart/delaystart_workflow_test.go ================================================ package main import ( "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" ) func Test_Workflow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestWorkflowEnvironment() env.RegisterWorkflow(delayStartWorkflow) env.RegisterActivity(delayStartActivity) var activityMessage string env.SetOnActivityCompletedListener(func(activityInfo *activity.Info, result encoded.Value, err error) { result.Get(&activityMessage) }) delayStart := 30 * time.Second env.ExecuteWorkflow(delayStartWorkflow, delayStart) require.True(t, env.IsWorkflowCompleted()) require.NoError(t, env.GetWorkflowError()) require.Equal(t, "Activity started after "+delayStart.String(), activityMessage) } ================================================ FILE: cmd/samples/recipes/delaystart/main.go ================================================ 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 startShadower(h *common.SampleHelper) { workerOptions := worker.Options{ MetricsScope: h.WorkerMetricScope, Logger: h.Logger, EnableShadowWorker: true, ShadowOptions: worker.ShadowOptions{ WorkflowTypes: []string{delayStartWorkflowName}, WorkflowStatus: []string{"Completed"}, ExitCondition: worker.ShadowExitCondition{ ShadowCount: 10, }, }, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { delayStart := 30 * time.Second workflowOptions := client.StartWorkflowOptions{ ID: "delaystart_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, DelayStart: delayStart, } h.StartWorkflow(workflowOptions, delayStartWorkflowName, delayStart) } func registerWorkflowAndActivity( h *common.SampleHelper, ) { h.RegisterWorkflowWithAlias(delayStartWorkflow, delayStartWorkflowName) h.RegisterActivity(delayStartActivity) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker, trigger or shadower.") 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 "shadower": registerWorkflowAndActivity(&h) startShadower(&h) select {} case "trigger": startWorkflow(&h) } } ================================================ FILE: cmd/samples/recipes/dynamic/dynamic_workflow.go ================================================ package main import ( "fmt" "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * The purpose of this sample is to demonstrate invocation of workflows and activities using name rather than strongly * typed function. */ // ApplicationName is the task list for this sample const ApplicationName = "dynamicGroup" // GreetingsWorkflowName name used when workflow function is registered during init. We use the fully qualified name to function const GreetingsWorkflowName = "main.sampleGreetingsWorkflow" // Activity names used when activity function is registered during init. We use the fully qualified name to function const getNameActivityName = "main.getNameActivity" const getGreetingActivityName = "main.getGreetingActivity" const sayGreetingActivityName = "main.sayGreetingActivity" // sampleGreetingsWorkflow Workflow Decider. func sampleGreetingsWorkflow(ctx workflow.Context) error { // Get Greeting. ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) var greetResult string err := workflow.ExecuteActivity(ctx, getGreetingActivityName).Get(ctx, &greetResult) if err != nil { logger.Error("Get greeting failed.", zap.Error(err)) return err } // Get Name. var nameResult string err = workflow.ExecuteActivity(ctx, getNameActivityName).Get(ctx, &nameResult) if err != nil { logger.Error("Get name failed.", zap.Error(err)) return err } // Say Greeting. var sayResult string err = workflow.ExecuteActivity(ctx, sayGreetingActivityName, greetResult, nameResult).Get(ctx, &sayResult) if err != nil { logger.Error("Marshalling failed with error.", zap.Error(err)) return err } logger.Info("Workflow completed.", zap.String("Result", sayResult)) return nil } // Get Name Activity. func getNameActivity() (string, error) { return "Cadence", nil } // Get Greeting Activity. func getGreetingActivity() (string, error) { return "Hello", nil } // Say Greeting Activity. func sayGreetingActivity(greeting string, name string) (string, error) { result := fmt.Sprintf("Greeting: %s %s!\n", greeting, name) return result, nil } ================================================ FILE: cmd/samples/recipes/dynamic/main.go ================================================ 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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "dynamic_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, GreetingsWorkflowName) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleGreetingsWorkflow) h.RegisterActivityWithAlias(getGreetingActivity, getGreetingActivityName) h.RegisterActivityWithAlias(getNameActivity, getNameActivityName) h.RegisterActivityWithAlias(sayGreetingActivity, sayGreetingActivityName) 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) } } ================================================ FILE: cmd/samples/recipes/dynamic/workflow_test.go ================================================ package main import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/cadence/activity" "go.uber.org/cadence/testsuite" ) func TestDynamicWorkflow(t *testing.T) { a := assert.New(t) s := testsuite.WorkflowTestSuite{} env := s.NewTestWorkflowEnvironment() env.RegisterWorkflow(sampleGreetingsWorkflow) env.RegisterActivityWithOptions(getGreetingActivity, activity.RegisterOptions{ Name: getGreetingActivityName, }) env.RegisterActivityWithOptions(getNameActivity, activity.RegisterOptions{ Name: getNameActivityName, }) env.RegisterActivityWithOptions(sayGreetingActivity, activity.RegisterOptions{ Name: sayGreetingActivityName, }) env.OnActivity(getGreetingActivityName).Return("Greet", nil).Times(1) env.OnActivity(getNameActivityName).Return("Name", nil).Times(1) env.OnActivity(sayGreetingActivityName, "Greet", "Name").Return("Greet Name", nil).Times(1) env.ExecuteWorkflow(sampleGreetingsWorkflow) a.True(env.IsWorkflowCompleted()) a.NoError(env.GetWorkflowError()) env.AssertExpectations(t) } ================================================ FILE: cmd/samples/recipes/greetings/greetings.json ================================================ [ { "eventId": 1, "timestamp": 1678403666710299250, "eventType": "WorkflowExecutionStarted", "version": 0, "taskId": 2097152, "workflowExecutionStartedEventAttributes": { "workflowType": { "name": "github.com/uber-common/cadence-samples/cmd/samples/recipes/greetings.sampleGreetingsWorkflow" }, "taskList": { "name": "greetingsGroup" }, "executionStartToCloseTimeoutSeconds": 60, "taskStartToCloseTimeoutSeconds": 60, "continuedExecutionRunId": "", "originalExecutionRunId": "b36e5edb-288e-4b98-9b66-2170238f8fc7", "identity": "8979@agautam-NV709R969P@@00c72a14-c860-4283-b900-a9f732951789", "firstExecutionRunId": "b36e5edb-288e-4b98-9b66-2170238f8fc7", "attempt": 0, "cronSchedule": "", "firstDecisionTaskBackoffSeconds": 0, "header": {} } }, { "eventId": 2, "timestamp": 1678403666710498292, "eventType": "DecisionTaskScheduled", "version": 0, "taskId": 2097153, "decisionTaskScheduledEventAttributes": { "taskList": { "name": "greetingsGroup" }, "startToCloseTimeoutSeconds": 60, "attempt": 0 } }, { "eventId": 3, "timestamp": 1678403666741684958, "eventType": "DecisionTaskStarted", "version": 0, "taskId": 2097158, "decisionTaskStartedEventAttributes": { "scheduledEventId": 2, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224", "requestId": "fcaf3c19-b8a8-47c8-9373-1986cba56ae2" } }, { "eventId": 4, "timestamp": 1678403666764246458, "eventType": "DecisionTaskCompleted", "version": 0, "taskId": 2097161, "decisionTaskCompletedEventAttributes": { "scheduledEventId": 2, "startedEventId": 3, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224", "binaryChecksum": "4c71e1c8cb815b51ff74bc0dbbb86cfa" } }, { "eventId": 5, "timestamp": 1678403666764500583, "eventType": "ActivityTaskScheduled", "version": 0, "taskId": 2097162, "activityTaskScheduledEventAttributes": { "activityId": "0", "activityType": { "name": "main.getGreetingActivity" }, "taskList": { "name": "greetingsGroup" }, "scheduleToCloseTimeoutSeconds": 60, "scheduleToStartTimeoutSeconds": 60, "startToCloseTimeoutSeconds": 60, "heartbeatTimeoutSeconds": 20, "decisionTaskCompletedEventId": 4, "header": {} } }, { "eventId": 6, "timestamp": 1678403666764575958, "eventType": "ActivityTaskStarted", "version": 0, "taskId": 2097163, "activityTaskStartedEventAttributes": { "scheduledEventId": 5, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224", "requestId": "0f95e6e3-f170-4f6c-bd92-720d2673fe2d", "attempt": 0, "lastFailureReason": "" } }, { "eventId": 7, "timestamp": 1678403666779516000, "eventType": "ActivityTaskCompleted", "version": 0, "taskId": 2097166, "activityTaskCompletedEventAttributes": { "result": "IkhlbGxvIgo=", "scheduledEventId": 5, "startedEventId": 6, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224" } }, { "eventId": 8, "timestamp": 1678403666779571208, "eventType": "DecisionTaskScheduled", "version": 0, "taskId": 2097168, "decisionTaskScheduledEventAttributes": { "taskList": { "name": "agautam-NV709R969P:053628af-d5cb-4a19-80b2-fd09b728e9d9" }, "startToCloseTimeoutSeconds": 60, "attempt": 0 } }, { "eventId": 9, "timestamp": 1678403666794335375, "eventType": "DecisionTaskStarted", "version": 0, "taskId": 2097172, "decisionTaskStartedEventAttributes": { "scheduledEventId": 8, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224", "requestId": "87e9fac9-6aa6-4778-8b1e-4e7850545f6a" } }, { "eventId": 10, "timestamp": 1678403666811876125, "eventType": "DecisionTaskCompleted", "version": 0, "taskId": 2097175, "decisionTaskCompletedEventAttributes": { "scheduledEventId": 8, "startedEventId": 9, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224", "binaryChecksum": "4c71e1c8cb815b51ff74bc0dbbb86cfa" } }, { "eventId": 11, "timestamp": 1678403666812034417, "eventType": "ActivityTaskScheduled", "version": 0, "taskId": 2097176, "activityTaskScheduledEventAttributes": { "activityId": "1", "activityType": { "name": "main.getNameActivity" }, "taskList": { "name": "greetingsGroup" }, "scheduleToCloseTimeoutSeconds": 60, "scheduleToStartTimeoutSeconds": 60, "startToCloseTimeoutSeconds": 60, "heartbeatTimeoutSeconds": 20, "decisionTaskCompletedEventId": 10, "header": {} } }, { "eventId": 12, "timestamp": 1678403666812095833, "eventType": "ActivityTaskStarted", "version": 0, "taskId": 2097177, "activityTaskStartedEventAttributes": { "scheduledEventId": 11, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224", "requestId": "8f2bf260-1cfa-4531-b054-cc8cacbe7b06", "attempt": 0, "lastFailureReason": "" } }, { "eventId": 13, "timestamp": 1678403666823607458, "eventType": "ActivityTaskCompleted", "version": 0, "taskId": 2097180, "activityTaskCompletedEventAttributes": { "result": "IkNhZGVuY2UiCg==", "scheduledEventId": 11, "startedEventId": 12, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224" } }, { "eventId": 14, "timestamp": 1678403666823669292, "eventType": "DecisionTaskScheduled", "version": 0, "taskId": 2097182, "decisionTaskScheduledEventAttributes": { "taskList": { "name": "agautam-NV709R969P:053628af-d5cb-4a19-80b2-fd09b728e9d9" }, "startToCloseTimeoutSeconds": 60, "attempt": 0 } }, { "eventId": 15, "timestamp": 1678403666837054083, "eventType": "DecisionTaskStarted", "version": 0, "taskId": 2097186, "decisionTaskStartedEventAttributes": { "scheduledEventId": 14, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224", "requestId": "519ec075-5402-4c58-b19d-0eddc15be712" } }, { "eventId": 16, "timestamp": 1678403666851767917, "eventType": "DecisionTaskCompleted", "version": 0, "taskId": 2097189, "decisionTaskCompletedEventAttributes": { "scheduledEventId": 14, "startedEventId": 15, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224", "binaryChecksum": "4c71e1c8cb815b51ff74bc0dbbb86cfa" } }, { "eventId": 17, "timestamp": 1678403666851909542, "eventType": "ActivityTaskScheduled", "version": 0, "taskId": 2097190, "activityTaskScheduledEventAttributes": { "activityId": "2", "activityType": { "name": "main.sayGreetingActivity" }, "taskList": { "name": "greetingsGroup" }, "input": "IkhlbGxvIgoiQ2FkZW5jZSIK", "scheduleToCloseTimeoutSeconds": 60, "scheduleToStartTimeoutSeconds": 60, "startToCloseTimeoutSeconds": 60, "heartbeatTimeoutSeconds": 20, "decisionTaskCompletedEventId": 16, "header": {} } }, { "eventId": 18, "timestamp": 1678403666851975417, "eventType": "ActivityTaskStarted", "version": 0, "taskId": 2097191, "activityTaskStartedEventAttributes": { "scheduledEventId": 17, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224", "requestId": "8f8d541c-96fe-4c96-9af7-70a70a9be096", "attempt": 0, "lastFailureReason": "" } }, { "eventId": 19, "timestamp": 1678403666864198625, "eventType": "ActivityTaskCompleted", "version": 0, "taskId": 2097194, "activityTaskCompletedEventAttributes": { "result": "IkdyZWV0aW5nOiBIZWxsbyBDYWRlbmNlIVxuIgo=", "scheduledEventId": 17, "startedEventId": 18, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224" } }, { "eventId": 20, "timestamp": 1678403666864249708, "eventType": "DecisionTaskScheduled", "version": 0, "taskId": 2097196, "decisionTaskScheduledEventAttributes": { "taskList": { "name": "agautam-NV709R969P:053628af-d5cb-4a19-80b2-fd09b728e9d9" }, "startToCloseTimeoutSeconds": 60, "attempt": 0 } }, { "eventId": 21, "timestamp": 1678403666877005167, "eventType": "DecisionTaskStarted", "version": 0, "taskId": 2097200, "decisionTaskStartedEventAttributes": { "scheduledEventId": 20, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224", "requestId": "919b570f-0ebf-4b88-94dd-cc183c898ad5" } }, { "eventId": 22, "timestamp": 1678403666891816208, "eventType": "DecisionTaskCompleted", "version": 0, "taskId": 2097203, "decisionTaskCompletedEventAttributes": { "scheduledEventId": 20, "startedEventId": 21, "identity": "8950@agautam-NV709R969P@greetingsGroup@4e847c85-7deb-45f3-96e6-2fbd3655a224", "binaryChecksum": "4c71e1c8cb815b51ff74bc0dbbb86cfa" } }, { "eventId": 23, "timestamp": 1678403666891938750, "eventType": "WorkflowExecutionCompleted", "version": 0, "taskId": 2097204, "workflowExecutionCompletedEventAttributes": { "decisionTaskCompletedEventId": 22 } } ] ================================================ FILE: cmd/samples/recipes/greetings/greetings_workflow.go ================================================ package main import ( "fmt" "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This greetings sample workflow executes 3 activities in sequential. It gets greeting and name from 2 different activities, * and then pass greeting and name as input to a 3rd activity to generate final greetings. */ // ApplicationName is the task list for this sample const ApplicationName = "greetingsGroup" // sampleGreetingsWorkflow Workflow Decider. func sampleGreetingsWorkflow(ctx workflow.Context) error { // Get Greeting. ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) var greetResult string err := workflow.ExecuteActivity(ctx, getGreetingActivity).Get(ctx, &greetResult) if err != nil { logger.Error("Get greeting failed.", zap.Error(err)) return err } // Get Name. var nameResult string err = workflow.ExecuteActivity(ctx, getNameActivity).Get(ctx, &nameResult) if err != nil { logger.Error("Get name failed.", zap.Error(err)) return err } // Say Greeting. var sayResult string err = workflow.ExecuteActivity(ctx, sayGreetingActivity, greetResult, nameResult).Get(ctx, &sayResult) if err != nil { logger.Error("Marshalling failed with error.", zap.Error(err)) return err } logger.Info("Workflow completed.", zap.String("Result", sayResult)) return nil } // Get Name Activity. func getNameActivity() (string, error) { return "Cadence", nil } // Get Greeting Activity. func getGreetingActivity() (string, error) { return "Hello", nil } // Say Greeting Activity. func sayGreetingActivity(greeting string, name string) (string, error) { result := fmt.Sprintf("Greeting: %s %s!\n", greeting, name) return result, nil } ================================================ FILE: cmd/samples/recipes/greetings/main.go ================================================ 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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "greetings_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, sampleGreetingsWorkflow) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleGreetingsWorkflow) h.RegisterActivity(getGreetingActivity) h.RegisterActivity(getNameActivity) h.RegisterActivity(sayGreetingActivity) 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) } } ================================================ FILE: cmd/samples/recipes/greetings/replay_test.go ================================================ package main import ( "testing" "go.uber.org/cadence/worker" "go.uber.org/zap/zaptest" "github.com/stretchr/testify/require" ) // This replay test is the recommended way to make sure changing workflow code is backward compatible without non-deterministic errors. // "greetings.json" can be downloaded from cadence CLI: // // cadence --do default wf show -w greetings_5d5f8e5c-4807-444d-9dc5-80abea22a324 --output_filename ~/tmp/greetings.json // // Or from Cadence Web UI. And you may need to change workflowType in the first event. func TestReplayWorkflowHistoryFromFile(t *testing.T) { replayer := worker.NewWorkflowReplayer() replayer.RegisterWorkflow(sampleGreetingsWorkflow) err := replayer.ReplayWorkflowHistoryFromJSONFile(zaptest.NewLogger(t), "greetings.json") require.NoError(t, err) } ================================================ FILE: cmd/samples/recipes/greetings/shadow_test.go ================================================ package main import ( "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/zap/zaptest" "github.com/uber-common/cadence-samples/cmd/samples/common" ) const ( configFile = "../../../../config/development.yaml" ) // Make sure cadence-server is running at the address specified in the // config/development.yaml file before running this test func TestWorkflowShadowing(t *testing.T) { t.Skip("need connection to cadence server") shadowOptions := worker.ShadowOptions{ WorkflowTypes: []string{"sampleGreetingsWorkflow"}, WorkflowStatus: []string{"Completed"}, WorkflowStartTimeFilter: worker.TimeFilter{ MinTimestamp: time.Now().Add(-time.Hour), }, } var helper common.SampleHelper helper.SetConfigFile(configFile) helper.SetupServiceConfig() service, err := helper.Builder.BuildServiceClient() require.NoError(t, err) shadower, err := worker.NewWorkflowShadower(service, helper.Config.DomainName, shadowOptions, worker.ReplayOptions{}, zaptest.NewLogger(t)) require.NoError(t, err) shadower.RegisterWorkflowWithOptions(sampleGreetingsWorkflow, workflow.RegisterOptions{Name: "sampleGreetingsWorkflow"}) err = shadower.Run() require.NoError(t, err) } ================================================ FILE: cmd/samples/recipes/greetings/workflow_test.go ================================================ package main import ( "context" "testing" "github.com/stretchr/testify/suite" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(sampleGreetingsWorkflow) s.env.RegisterActivity(getGreetingActivity) s.env.RegisterActivity(getNameActivity) s.env.RegisterActivity(sayGreetingActivity) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_SampleGreetingsWorkflow() { sayGreetingActivityName := "github.com/uber-common/cadence-samples/cmd/samples/recipes/greetings.sayGreetingActivity" var startCalled, endCalled bool s.env.SetOnActivityStartedListener(func(activityInfo *activity.Info, ctx context.Context, args encoded.Values) { if sayGreetingActivityName == activityInfo.ActivityType.Name { var greeting, name string args.Get(&greeting, &name) s.Equal("Hello", greeting) s.Equal("Cadence", name) startCalled = true } }) s.env.SetOnActivityCompletedListener(func(activityInfo *activity.Info, result encoded.Value, err error) { if sayGreetingActivityName == activityInfo.ActivityType.Name { var sayResult string result.Get(&sayResult) s.Equal("Greeting: Hello Cadence!\n", sayResult) endCalled = true } }) s.env.ExecuteWorkflow(sampleGreetingsWorkflow) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) s.True(startCalled) s.True(endCalled) } ================================================ FILE: cmd/samples/recipes/helloworld/activity_logger_test.go ================================================ package main import ( "context" "github.com/stretchr/testify/require" "go.uber.org/cadence/activity" "go.uber.org/cadence/testsuite" "go.uber.org/cadence/worker" "go.uber.org/zap" "go.uber.org/zap/zapcore" "testing" ) func sampleActivity(ctx context.Context) error { logger := activity.GetLogger(ctx) logger.Info("test logging") return nil } func Test_Activity_Noop_Logger(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestActivityEnvironment() env.RegisterActivity(sampleActivity) val, err := env.ExecuteActivity(sampleActivity) require.Nil(t, err) require.True(t, !val.HasValue()) } func Test_Activity_Print_Logger(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestActivityEnvironment() logger, err := zap.NewProduction() require.Nil(t, err) var outputLogs []string logger = logger.WithOptions(zap.Hooks( func(entry zapcore.Entry) error { outputLogs = append(outputLogs, entry.Message) return nil }, )) env.SetWorkerOptions(worker.Options{ Logger: logger, }) env.RegisterActivity(sampleActivity) val, err := env.ExecuteActivity(sampleActivity) require.Nil(t, err) require.True(t, !val.HasValue()) require.True(t, len(outputLogs)==1) require.True(t, outputLogs[0] == "test logging") } ================================================ FILE: cmd/samples/recipes/helloworld/helloworld.json ================================================ [ { "eventId":1, "timestamp":1558126752505445000, "eventType":"WorkflowExecutionStarted", "version":-24, "taskId":33554432, "workflowExecutionStartedEventAttributes":{ "workflowType":{ "name":"github.com/uber-common/cadence-samples/cmd/samples/recipes/helloworld.helloWorldWorkflow" }, "taskList":{ "name":"helloWorldGroup" }, "input":"IkNhZGVuY2UiCg==", "executionStartToCloseTimeoutSeconds":3600, "taskStartToCloseTimeoutSeconds":10, "identity":"66434@longer-C02V60N3HTDG@", "attempt":0, "firstDecisionTaskBackoffSeconds":0 } }, { "eventId":2, "timestamp":1558126752505631000, "eventType":"DecisionTaskScheduled", "version":-24, "taskId":33554433, "decisionTaskScheduledEventAttributes":{ "taskList":{ "name":"helloWorldGroup" }, "startToCloseTimeoutSeconds":10, "attempt":0 } }, { "eventId":3, "timestamp":1558126757360699000, "eventType":"DecisionTaskStarted", "version":-24, "taskId":33554438, "decisionTaskStartedEventAttributes":{ "scheduledEventId":2, "identity":"66471@longer-C02V60N3HTDG@helloWorldGroup", "requestId":"665325ec-74eb-4337-bf82-fceeb60e2196" } }, { "eventId":4, "timestamp":1558126757385307000, "eventType":"DecisionTaskCompleted", "version":-24, "taskId":33554441, "decisionTaskCompletedEventAttributes":{ "scheduledEventId":2, "startedEventId":3, "identity":"66471@longer-C02V60N3HTDG@helloWorldGroup", "binaryChecksum":"b2e32759177ccbb3e67ad7694aec233c" } }, { "eventId":5, "timestamp":1558126757385333000, "eventType":"ActivityTaskScheduled", "version":-24, "taskId":33554442, "activityTaskScheduledEventAttributes":{ "activityId":"0", "activityType":{ "name":"github.com/uber-common/cadence-samples/cmd/samples/recipes/helloworld.helloWorldActivity" }, "taskList":{ "name":"helloWorldGroup" }, "input":"IkNhZGVuY2UiCg==", "scheduleToCloseTimeoutSeconds":3600, "scheduleToStartTimeoutSeconds":3600, "startToCloseTimeoutSeconds":3600, "heartbeatTimeoutSeconds":3600, "decisionTaskCompletedEventId":4 } }, { "eventId":6, "timestamp":1558126757393919000, "eventType":"ActivityTaskStarted", "version":-24, "taskId":33554446, "activityTaskStartedEventAttributes":{ "scheduledEventId":5, "identity":"66471@longer-C02V60N3HTDG@helloWorldGroup", "requestId":"45c4006a-ae7c-4392-baa6-c090857f884b", "attempt":0 } }, { "eventId":7, "timestamp":1558126757403468000, "eventType":"ActivityTaskCompleted", "version":-24, "taskId":33554447, "activityTaskCompletedEventAttributes":{ "result":"IkhlbGxvIENhZGVuY2UhIgo=", "scheduledEventId":5, "startedEventId":6, "identity":"66471@longer-C02V60N3HTDG@helloWorldGroup" } }, { "eventId":8, "timestamp":1558126757403476000, "eventType":"DecisionTaskScheduled", "version":-24, "taskId":33554450, "decisionTaskScheduledEventAttributes":{ "taskList":{ "name":"longer-C02V60N3HTDG:33ab3ada-4636-4386-8575-81dd8dc02e9a" }, "startToCloseTimeoutSeconds":10, "attempt":0 } }, { "eventId":9, "timestamp":1558126757410564000, "eventType":"DecisionTaskStarted", "version":-24, "taskId":33554454, "decisionTaskStartedEventAttributes":{ "scheduledEventId":8, "identity":"66471@longer-C02V60N3HTDG@helloWorldGroup", "requestId":"cb1fdadf-f46b-4840-9b97-863f4b3b6b11" } }, { "eventId":10, "timestamp":1558126757491491000, "eventType":"DecisionTaskCompleted", "version":-24, "taskId":33554457, "decisionTaskCompletedEventAttributes":{ "scheduledEventId":8, "startedEventId":9, "identity":"66471@longer-C02V60N3HTDG@helloWorldGroup", "binaryChecksum":"b2e32759177ccbb3e67ad7694aec233c" } }, { "eventId":11, "timestamp":1558126757491513000, "eventType":"WorkflowExecutionCompleted", "version":-24, "taskId":33554458, "workflowExecutionCompletedEventAttributes":{ "decisionTaskCompletedEventId":10 } } ] ================================================ FILE: cmd/samples/recipes/helloworld/helloworld_workflow.go ================================================ package main import ( "context" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This is the hello world workflow sample. */ // ApplicationName is the task list for this sample const ApplicationName = "helloWorldGroup" const helloWorldWorkflowName = "helloWorldWorkflow" // helloWorkflow workflow decider func helloWorldWorkflow(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) logger.Info("helloworld workflow started") var helloworldResult string err := workflow.ExecuteActivity(ctx, helloWorldActivity, name).Get(ctx, &helloworldResult) if err != nil { logger.Error("Activity failed.", zap.Error(err)) return err } // Adding a new activity to the workflow will result in a non-determinstic change for the workflow // Please check https://cadenceworkflow.io/docs/go-client/workflow-versioning/ for more information // // Un-commenting the following code and the TestReplayWorkflowHistoryFromFile in replay_test.go // will fail due to the non-determinstic change // // If you have a completed workflow execution without the following code and run the // TestWorkflowShadowing in shadow_test.go or start the worker in shadow mode (using -m shadower) // those two shadowing check will also fail due to the non-deterministic change // // err := workflow.ExecuteActivity(ctx, helloWorldActivity, name).Get(ctx, &helloworldResult) // if err != nil { // logger.Error("Activity failed.", zap.Error(err)) // return err // } logger.Info("Workflow completed.", zap.String("Result", helloworldResult)) return nil } func helloWorldActivity(ctx context.Context, name string) (string, error) { logger := activity.GetLogger(ctx) logger.Info("helloworld activity started") return "Hello " + name + "!", nil } ================================================ FILE: cmd/samples/recipes/helloworld/helloworld_workflow_test.go ================================================ package main import ( "testing" "github.com/stretchr/testify/require" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" ) func Test_Workflow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestWorkflowEnvironment() env.RegisterWorkflow(helloWorldWorkflow) env.RegisterActivity(helloWorldActivity) var activityMessage string env.SetOnActivityCompletedListener(func(activityInfo *activity.Info, result encoded.Value, err error) { result.Get(&activityMessage) }) env.ExecuteWorkflow(helloWorldWorkflow, "world") require.True(t, env.IsWorkflowCompleted()) require.NoError(t, env.GetWorkflowError()) require.Equal(t, "Hello world!", activityMessage) } ================================================ FILE: cmd/samples/recipes/helloworld/main.go ================================================ 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 startShadower(h *common.SampleHelper) { workerOptions := worker.Options{ MetricsScope: h.WorkerMetricScope, Logger: h.Logger, EnableShadowWorker: true, ShadowOptions: worker.ShadowOptions{ WorkflowTypes: []string{helloWorldWorkflowName}, WorkflowStatus: []string{"Completed"}, ExitCondition: worker.ShadowExitCondition{ ShadowCount: 10, }, }, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "helloworld_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, helloWorldWorkflowName, "Cadence") } func registerWorkflowAndActivity( h *common.SampleHelper, ) { h.RegisterWorkflowWithAlias(helloWorldWorkflow, helloWorldWorkflowName) h.RegisterActivity(helloWorldActivity) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker, trigger or shadower.") 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 "shadower": registerWorkflowAndActivity(&h) startShadower(&h) select {} case "trigger": startWorkflow(&h) } } ================================================ FILE: cmd/samples/recipes/helloworld/replay_test.go ================================================ package main import ( "testing" "go.uber.org/cadence/worker" "go.uber.org/zap/zaptest" "github.com/stretchr/testify/require" ) // This replay test is the recommended way to make sure changing workflow code is backward compatible without non-deterministic errors. // "helloworld.json" can be downloaded from cadence CLI: // // cadence --do default wf show -w helloworld_d002cd3a-aeee-4a11-aa30-1c62385b4d87 --output_filename ~/tmp/helloworld.json // // Or from Cadence Web UI. And you may need to change workflowType in the first event. func TestReplayWorkflowHistoryFromFile(t *testing.T) { replayer := worker.NewWorkflowReplayer() replayer.RegisterWorkflow(helloWorldWorkflow) err := replayer.ReplayWorkflowHistoryFromJSONFile(zaptest.NewLogger(t), "helloworld.json") require.NoError(t, err) } ================================================ FILE: cmd/samples/recipes/helloworld/shadow_test.go ================================================ package main import ( "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/zap/zaptest" "github.com/uber-common/cadence-samples/cmd/samples/common" ) const ( configFile = "../../../../config/development.yaml" ) // Make sure cadence-server is running at the address specified in the // config/development.yaml file before running this test func TestWorkflowShadowing(t *testing.T) { t.Skip("need connection to cadence server") shadowOptions := worker.ShadowOptions{ WorkflowTypes: []string{helloWorldWorkflowName}, WorkflowStatus: []string{"Completed"}, WorkflowStartTimeFilter: worker.TimeFilter{ MinTimestamp: time.Now().Add(-time.Hour), }, } var helper common.SampleHelper helper.SetConfigFile(configFile) helper.SetupServiceConfig() service, err := helper.Builder.BuildServiceClient() require.NoError(t, err) shadower, err := worker.NewWorkflowShadower(service, helper.Config.DomainName, shadowOptions, worker.ReplayOptions{}, zaptest.NewLogger(t)) require.NoError(t, err) shadower.RegisterWorkflowWithOptions(helloWorldWorkflow, workflow.RegisterOptions{Name: helloWorldWorkflowName}) err = shadower.Run() require.NoError(t, err) } ================================================ FILE: cmd/samples/recipes/localactivity/README.md ================================================ This sample workflow demos how to use local activity to execute short/quick operations efficiently. local_activity_workflow.go shows how to use local activity local_activity_workflow_test.go shows how to unit-test workflow with local activity Steps to run this sample: 1) You need a cadence service running. See details in cmd/samples/README.md 2) Run the following command to start worker ``` ./bin/localactivity -m worker ``` 3) Run the following command to trigger a workflow execution. You should see workflowID and runID print out on screen. ``` ./bin/localactivity -m trigger ``` 4) Run the following command to send signal "_1_" to the running workflow. You should see output that indicate 5 local activity has been run to check the conditions and one condition will be true which result in one activity to be scheduled. ``` ./bin/localactivity -m signal -s _1_ -w ``` 5) Repeat step 4, but with different signal data, for example, send signal like _2_4_ to make 2 conditions true. ``` ./bin/localactivity -m signal -s _2_4_ -w ``` 6) Run the following command this will exit the workflow. ``` ./bin/localactivity -m signal -s exit ``` ================================================ FILE: cmd/samples/recipes/localactivity/local_activity_workflow.go ================================================ package main import ( "context" "strings" "time" "go.uber.org/cadence" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * Sample workflow that uses local activities. */ // ApplicationName is the task list for this sample const ApplicationName = "localActivityGroup" // SignalName is the signal name that workflow is waiting for const SignalName = "trigger-signal" type conditionAndAction struct { // condition is a function pointer to a local activity condition interface{} // action is a function pointer to a regular activity action interface{} } var checks = []conditionAndAction{ {checkCondition0, activityForCondition0}, {checkCondition1, activityForCondition1}, {checkCondition2, activityForCondition2}, {checkCondition3, activityForCondition3}, {checkCondition4, activityForCondition4}, } // processingWorkflow is a workflow that process a given signal data. It evaluates if any conditions are meet for // the given signal data by using LocalActivity which runs as local function and then schedule activities to handle // it if the condition is meet. The idea is that you could have many conditions (for example 100 conditions) that needs // to be evaluated, and only a couple of them will meet the condition and needs to be processed by an activity. Using // local activity is very efficient in this case because local activity is execute as local function directly by decider // worker. func processingWorkflow(ctx workflow.Context, data string) (string, error) { logger := workflow.GetLogger(ctx) lao := workflow.LocalActivityOptions{ // use short timeout as local activity is execute as function locally. ScheduleToCloseTimeout: time.Second, } ctx = workflow.WithLocalActivityOptions(ctx, lao) ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, } ctx = workflow.WithActivityOptions(ctx, ao) var actionFutures []workflow.Future for i, check := range checks { var conditionMeet bool err := workflow.ExecuteLocalActivity(ctx, check.condition, data).Get(ctx, &conditionMeet) if err != nil { return "", err } logger.Sugar().Infof("condition meet for %v: %v", i, conditionMeet) if conditionMeet { f := workflow.ExecuteActivity(ctx, check.action, data) actionFutures = append(actionFutures, f) } } var processResult string for _, f := range actionFutures { var actionResult string if err := f.Get(ctx, &actionResult); err != nil { return "", err } processResult += actionResult } return processResult, nil } // signalHandlingWorkflow is a workflow that waits on signal and then sends that signal to be processed by a child workflow. func signalHandlingWorkflow(ctx workflow.Context) error { logger := workflow.GetLogger(ctx) ch := workflow.GetSignalChannel(ctx, SignalName) for { var signal string if more := ch.Receive(ctx, &signal); !more { logger.Info("Signal channel closed") return cadence.NewCustomError("signal_channel_closed") } logger.Info("Signal received.", zap.String("signal", signal)) if signal == "exit" { break } cwo := workflow.ChildWorkflowOptions{ ExecutionStartToCloseTimeout: time.Minute, // TaskStartToCloseTimeout must be larger than all local activity execution time, because DecisionTask won't // return until all local activities completed. TaskStartToCloseTimeout: time.Second * 30, } childCtx := workflow.WithChildOptions(ctx, cwo) var processResult string err := workflow.ExecuteChildWorkflow(childCtx, processingWorkflow, signal).Get(childCtx, &processResult) if err != nil { return err } logger.Sugar().Infof("Processed signal: %v, result: %v", signal, processResult) } return nil } func checkCondition0(ctx context.Context, signal string) (bool, error) { // some real logic happen here... return strings.Contains(signal, "_0_"), nil } func checkCondition1(ctx context.Context, signal string) (bool, error) { // some real logic happen here... return strings.Contains(signal, "_1_"), nil } func checkCondition2(ctx context.Context, signal string) (bool, error) { // some real logic happen here... return strings.Contains(signal, "_2_"), nil } func checkCondition3(ctx context.Context, signal string) (bool, error) { // some real logic happen here... return strings.Contains(signal, "_3_"), nil } func checkCondition4(ctx context.Context, signal string) (bool, error) { // some real logic happen here... return strings.Contains(signal, "_4_"), nil } func activityForCondition0(ctx context.Context, signal string) (string, error) { activity.GetLogger(ctx).Info("process for condition 0") // some real processing logic goes here time.Sleep(time.Second * 2) return "processed_0", nil } func activityForCondition1(ctx context.Context, signal string) (string, error) { activity.GetLogger(ctx).Info("process for condition 1") // some real processing logic goes here time.Sleep(time.Second * 2) return "processed_1", nil } func activityForCondition2(ctx context.Context, signal string) (string, error) { activity.GetLogger(ctx).Info("process for condition 2") // some real processing logic goes here time.Sleep(time.Second * 2) return "processed_2", nil } func activityForCondition3(ctx context.Context, signal string) (string, error) { activity.GetLogger(ctx).Info("process for condition 3") // some real processing logic goes here time.Sleep(time.Second * 2) return "processed_3", nil } func activityForCondition4(ctx context.Context, signal string) (string, error) { activity.GetLogger(ctx).Info("process for condition 4") // some real processing logic goes here time.Sleep(time.Second * 2) return "processed_4", nil } ================================================ FILE: cmd/samples/recipes/localactivity/local_activity_workflow_test.go ================================================ package main import ( "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "go.uber.org/cadence/testsuite" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(processingWorkflow) s.env.RegisterWorkflow(signalHandlingWorkflow) s.env.RegisterActivity(activityForCondition0) s.env.RegisterActivity(activityForCondition1) s.env.RegisterActivity(activityForCondition2) s.env.RegisterActivity(activityForCondition3) s.env.RegisterActivity(activityForCondition4) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_ProcessingWorkflow_SingleAction() { signalData := "_1_" // mock activityForCondition1 so it won't wait on real clock s.env.OnActivity(activityForCondition1, mock.Anything, signalData).Return("processed_1", nil) s.env.ExecuteWorkflow(processingWorkflow, signalData) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) var actualResult string s.NoError(s.env.GetWorkflowResult(&actualResult)) s.Equal("processed_1", actualResult) } func (s *UnitTestSuite) Test_ProcessingWorkflow_MultiAction() { signalData := "_1_, _3_" // mock activityForCondition1 so it won't wait on real clock s.env.OnActivity(activityForCondition1, mock.Anything, signalData).Return("processed_1", nil) s.env.OnActivity(activityForCondition3, mock.Anything, signalData).Return("processed_3", nil) s.env.ExecuteWorkflow(processingWorkflow, signalData) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) var actualResult string s.NoError(s.env.GetWorkflowResult(&actualResult)) s.Equal("processed_1processed_3", actualResult) } func (s *UnitTestSuite) Test_SignalHandlingWorkflow() { s.env.OnActivity(activityForCondition1, mock.Anything, "_1_").Return("processed_1", nil) s.env.RegisterDelayedCallback(func() { s.env.SignalWorkflow("trigger-signal", "_1_") }, time.Minute) s.env.RegisterDelayedCallback(func() { s.env.SignalWorkflow("trigger-signal", "exit") }, time.Minute*2) s.env.ExecuteWorkflow(signalHandlingWorkflow) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) } ================================================ FILE: cmd/samples/recipes/localactivity/main.go ================================================ 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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "localactivity_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute * 3, DecisionTaskStartToCloseTimeout: time.Minute, WorkflowIDReusePolicy: client.WorkflowIDReusePolicyAllowDuplicate, } h.StartWorkflow(workflowOptions, signalHandlingWorkflow) } func main() { var mode, workflowID, signal string flag.StringVar(&mode, "m", "trigger", "Mode is worker, trigger or query.") flag.StringVar(&workflowID, "w", "", "WorkflowID") flag.StringVar(&signal, "s", "signal_data", "SignalData") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(processingWorkflow) h.RegisterWorkflow(signalHandlingWorkflow) h.RegisterActivity(activityForCondition0) h.RegisterActivity(activityForCondition1) h.RegisterActivity(activityForCondition2) h.RegisterActivity(activityForCondition3) h.RegisterActivity(activityForCondition4) 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) case "signal": h.SignalWorkflow(workflowID, SignalName, signal) } } ================================================ FILE: cmd/samples/recipes/mutex/README.md ================================================ This mutex workflow demos an ability to lock/unlock a particular resource within a particular cadence domain so that other workflows within the same domain would wait until a resource lock is released. This is useful when we want to avoid race conditions or parallel mutually exclusive operations on the same resource. One way of coordinating parallel processing is to use cadence signals with SignalWithStartWorkflow and make sure signals are getting processed sequentially, however the logic might become too complex if we need to lock two or more resources at the same time. Mutex workflow pattern can simplify that. This example enqueues two long running SampleWorkflowWithMutex workflows in parallel. And each of the workflows has a mutex section. When SampleWorkflowWithMutex reaches Mutex section, it starts a mutex workflow via local activity, and blocks until "acquire-lock-event" is received. Once "acquire-lock-event" is received, it enters critical section, and finally releases the lock once processing is over by sending "releaseLock" a signal to the MutexWorkflow. ### Steps to run this sample: 1) You need a cadence service running. See details in cmd/samples/README.md 2) Run the following command to start the worker ``` ./bin/mutex -m worker ``` 3) Run the following command to start the example ``` ./bin/mutex -m trigger ``` You should see that second workflow critical section is executed when first workflow critical operation is finished. ================================================ FILE: cmd/samples/recipes/mutex/main.go ================================================ package main import ( "context" "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" ) const ( // ApplicationName is the task list for this sample ApplicationName = "mutexExample" _sampleHelperContextKey = "sampleHelper" ) // 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, BackgroundActivityContext: context.WithValue(context.Background(), _sampleHelperContextKey, h), } // Start Worker. h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } // startTwoWorkflows starts two workflows that operate on the same recourceID func startTwoWorkflows(h *common.SampleHelper) { resourceID := uuid.New() h.StartWorkflow(client.StartWorkflowOptions{ ID: "SampleWorkflowWithMutex_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: 10 * time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, }, sampleWorkflowWithMutex, resourceID) h.StartWorkflow(client.StartWorkflowOptions{ ID: "SampleWorkflowWithMutex_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: 10 * time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, }, sampleWorkflowWithMutex, resourceID) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(mutexWorkflow) h.RegisterWorkflow(sampleWorkflowWithMutex) h.RegisterActivity(signalWithStartMutexWorkflowActivity) 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": startTwoWorkflows(&h) } } ================================================ FILE: cmd/samples/recipes/mutex/mutex_workflow.go ================================================ package main import ( "context" "fmt" "time" "github.com/stretchr/testify/mock" "go.uber.org/cadence" "go.uber.org/cadence/client" "go.uber.org/cadence/testsuite" "go.uber.org/cadence/workflow" "go.uber.org/zap" "github.com/uber-common/cadence-samples/cmd/samples/common" ) const ( // AcquireLockSignalName signal channel name for lock acquisition AcquireLockSignalName = "acquire-lock-event" // RequestLockSignalName channel name for request lock RequestLockSignalName = "request-lock-event" ) // UnlockFunc ... type UnlockFunc func() error // Mutex - cadence mutex type Mutex struct { currentWorkflowID string lockNamespace string } // NewMutex initializes cadence mutex func NewMutex(currentWorkflowID string, lockNamespace string) *Mutex { return &Mutex{ currentWorkflowID: currentWorkflowID, lockNamespace: lockNamespace, } } // Lock - locks mutex func (s *Mutex) Lock(ctx workflow.Context, resourceID string, unlockTimeout time.Duration) (UnlockFunc, error) { activityCtx := workflow.WithLocalActivityOptions(ctx, workflow.LocalActivityOptions{ ScheduleToCloseTimeout: time.Minute * 1, RetryPolicy: &cadence.RetryPolicy{ InitialInterval: time.Second, BackoffCoefficient: 2.0, MaximumInterval: time.Minute, ExpirationInterval: time.Minute * 10, MaximumAttempts: 5, }, }) var releaseLockChannelName string var execution workflow.Execution err := workflow.ExecuteLocalActivity(activityCtx, signalWithStartMutexWorkflowActivity, s.lockNamespace, resourceID, s.currentWorkflowID, unlockTimeout).Get(ctx, &execution) if err != nil { return nil, err } workflow.GetSignalChannel(ctx, AcquireLockSignalName). Receive(ctx, &releaseLockChannelName) unlockFunc := func() error { return workflow.SignalExternalWorkflow(ctx, execution.ID, execution.RunID, releaseLockChannelName, "releaseLock").Get(ctx, nil) } return unlockFunc, nil } // mutexWorkflow used for locking a resource func mutexWorkflow( ctx workflow.Context, namespace string, resourceID string, unlockTimeout time.Duration, ) error { currentWorkflowID := workflow.GetInfo(ctx).WorkflowExecution.ID if currentWorkflowID == "default-test-workflow-id" { // unit testing hack, see https://github.com/uber-go/cadence-client/issues/663 workflow.Sleep(ctx, 10*time.Millisecond) } logger := workflow.GetLogger(ctx).With(zap.String("currentWorkflowID", currentWorkflowID)) logger.Info("started") var ack string requestLockCh := workflow.GetSignalChannel(ctx, RequestLockSignalName) for { var senderWorkflowID string if !requestLockCh.ReceiveAsync(&senderWorkflowID) { logger.Info("no more signals") break } var releaseLockChannelName string _ = workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} { return _generateUnlockChannelName(senderWorkflowID) }).Get(&releaseLockChannelName) logger := logger.With(zap.String("releaseLockChannelName", releaseLockChannelName)) logger.Info("generated release lock channel name") // Send release lock channel name back to a senderWorkflowID, so that it can // release the lock using release lock channel name err := workflow.SignalExternalWorkflow(ctx, senderWorkflowID, "", AcquireLockSignalName, releaseLockChannelName).Get(ctx, nil) if err != nil { // .Get(ctx, nil) blocks until the signal is sent. // If the senderWorkflowID is closed (terminated/canceled/timeouted/completed/etc), this would return error. // In this case we release the lock immediately instead of failing the mutex workflow. // Mutex workflow failing would lead to all workflows that have sent requestLock will be waiting. logger.With(zap.Error(err)).Info("SignalExternalWorkflow error") continue } logger.With(zap.Error(err)).Info("signaled external workflow") selector := workflow.NewSelector(ctx) selector.AddFuture(workflow.NewTimer(ctx, unlockTimeout), func(f workflow.Future) { logger.Info("unlockTimeout exceeded") }) selector.AddReceive(workflow.GetSignalChannel(ctx, releaseLockChannelName), func(c workflow.Channel, more bool) { c.Receive(ctx, &ack) logger.Info("release signal received") }) selector.Select(ctx) } return nil } // signalWithStartMutexWorkflowActivity ... func signalWithStartMutexWorkflowActivity( ctx context.Context, namespace string, resourceID string, senderWorkflowID string, unlockTimeout time.Duration, ) (*workflow.Execution, error) { h := ctx.Value(_sampleHelperContextKey).(*common.SampleHelper) workflowID := fmt.Sprintf( "%s:%s:%s", "mutex", namespace, resourceID, ) workflowOptions := client.StartWorkflowOptions{ ID: workflowID, TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Hour, DecisionTaskStartToCloseTimeout: time.Hour, RetryPolicy: &cadence.RetryPolicy{ InitialInterval: time.Second, BackoffCoefficient: 2.0, MaximumInterval: time.Minute, ExpirationInterval: time.Minute * 10, MaximumAttempts: 5, }, WorkflowIDReusePolicy: client.WorkflowIDReusePolicyAllowDuplicate, } we := h.SignalWithStartWorkflowWithCtx( ctx, workflowID, RequestLockSignalName, senderWorkflowID, workflowOptions, mutexWorkflow, namespace, resourceID, unlockTimeout) return we, nil } // _generateUnlockChannelName generates release lock channel name func _generateUnlockChannelName(senderWorkflowID string) string { return fmt.Sprintf("unlock-event-%s", senderWorkflowID) } // MockMutexLock stubs cadence mutex.Lock call func MockMutexLock(env *testsuite.TestWorkflowEnvironment, resourceID string, mockError error) { mockExecution := &workflow.Execution{ID: "mockID", RunID: "mockRunID"} env.OnActivity(signalWithStartMutexWorkflowActivity, mock.Anything, mock.Anything, resourceID, mock.Anything, mock.Anything). Return(mockExecution, mockError) env.RegisterDelayedCallback(func() { env.SignalWorkflow(AcquireLockSignalName, "mockReleaseLockChannelName") }, time.Millisecond*0) if mockError == nil { env.OnSignalExternalWorkflow(mock.Anything, mock.Anything, mockExecution.RunID, mock.Anything, mock.Anything).Return(nil) } } func sampleWorkflowWithMutex( ctx workflow.Context, resourceID string, ) error { currentWorkflowID := workflow.GetInfo(ctx).WorkflowExecution.ID logger := workflow.GetLogger(ctx). With(zap.String("currentWorkflowID", currentWorkflowID)). With(zap.String("resourceID", resourceID)) logger.Info("started") mutex := NewMutex(currentWorkflowID, "TestUseCase") unlockFunc, err := mutex.Lock(ctx, resourceID, 10*time.Minute) if err != nil { return err } logger.Info("resource locked") // emulate long running process logger.Info("critical operation started") workflow.Sleep(ctx, 10*time.Second) logger.Info("critical operation finished") unlockFunc() logger.Info("finished") return nil } ================================================ FILE: cmd/samples/recipes/mutex/mutex_workflow_test.go ================================================ package main import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "go.uber.org/cadence/testsuite" "go.uber.org/cadence/worker" "github.com/uber-common/cadence-samples/cmd/samples/common" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(mutexWorkflow) s.env.RegisterWorkflow(sampleWorkflowWithMutex) s.env.RegisterActivity(signalWithStartMutexWorkflowActivity) var h common.SampleHelper s.env.SetWorkerOptions(worker.Options{ BackgroundActivityContext: context.WithValue(context.Background(), _sampleHelperContextKey, h), }) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_Workflow_Success() { mockResourceID := "mockResourceID" MockMutexLock(s.env, mockResourceID, nil) s.env.ExecuteWorkflow(sampleWorkflowWithMutex, mockResourceID) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) } func (s *UnitTestSuite) Test_Workflow_Error() { mockResourceID := "mockResourceID" MockMutexLock(s.env, mockResourceID, errors.New("bad-error")) s.env.ExecuteWorkflow(sampleWorkflowWithMutex, mockResourceID) s.True(s.env.IsWorkflowCompleted()) s.EqualError(s.env.GetWorkflowError(), "bad-error") } func (s *UnitTestSuite) Test_MutexWorkflow_Success() { mockNamespace := "mockNamespace" mockResourceID := "mockResourceID" mockUnlockTimeout := 10 * time.Minute mockSenderWorkflowID := "mockSenderWorkflowID" s.env.RegisterDelayedCallback(func() { s.env.SignalWorkflow(RequestLockSignalName, mockSenderWorkflowID) }, time.Millisecond*0) s.env.RegisterDelayedCallback(func() { s.env.SignalWorkflow("unlock-event-mockSenderWorkflowID", "releaseLock") }, time.Millisecond*0) s.env.OnSignalExternalWorkflow(mock.Anything, mockSenderWorkflowID, "", AcquireLockSignalName, mock.Anything).Return(nil) s.env.ExecuteWorkflow( mutexWorkflow, mockNamespace, mockResourceID, mockUnlockTimeout, ) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) } func (s *UnitTestSuite) Test_MutexWorkflow_TimeoutSuccess() { mockNamespace := "mockNamespace" mockResourceID := "mockResourceID" mockUnlockTimeout := 10 * time.Minute mockSenderWorkflowID := "mockSenderWorkflowID" s.env.RegisterDelayedCallback(func() { s.env.SignalWorkflow(RequestLockSignalName, mockSenderWorkflowID) }, time.Millisecond*0) s.env.OnSignalExternalWorkflow(mock.Anything, mockSenderWorkflowID, "", AcquireLockSignalName, mock.Anything).Return(nil) s.env.ExecuteWorkflow( mutexWorkflow, mockNamespace, mockResourceID, mockUnlockTimeout, ) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) } ================================================ FILE: cmd/samples/recipes/pickfirst/main.go ================================================ 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, } // Start Worker. h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "pickfirst_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, samplePickFirstWorkflow) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(samplePickFirstWorkflow) h.RegisterActivity(sampleActivity) 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) } } ================================================ FILE: cmd/samples/recipes/pickfirst/pickfirst_workflow.go ================================================ package main import ( "context" "fmt" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" ) /** * This sample workflow execute activities in parallel branches, pick the result of the branch that completes first, * and then cancels other activities that are not finished yet. */ // ApplicationName is the task list for this sample const ApplicationName = "pickfirstGroup" // samplePickFirstWorkflow workflow decider func samplePickFirstWorkflow(ctx workflow.Context) error { selector := workflow.NewSelector(ctx) var firstResponse string // Use one cancel handler to cancel all of them. Cancelling on parent handler will close all the child ones // as well. childCtx, cancelHandler := workflow.WithCancel(ctx) ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, WaitForCancellation: true, // Wait for cancellation to complete. } childCtx = workflow.WithActivityOptions(childCtx, ao) // Set WaitForCancellation to true to demonstrate the cancellation to the other activities. In real world case, // you might not care about them and could set WaitForCancellation to false (which is default value). // starts 2 activities in parallel f1 := workflow.ExecuteActivity(childCtx, sampleActivity, 0, time.Second*2) f2 := workflow.ExecuteActivity(childCtx, sampleActivity, 1, time.Second*10) pendingFutures := []workflow.Future{f1, f2} selector.AddFuture(f1, func(f workflow.Future) { f.Get(ctx, &firstResponse) }).AddFuture(f2, func(f workflow.Future) { f.Get(ctx, &firstResponse) }) // wait for any of the future to complete selector.Select(ctx) // now at least one future is complete, so cancel all other pending futures. cancelHandler() // - If you want to wait for pending activities to finish after issuing cancellation // then wait for the future to complete. // - if you don't want to wait for completion of pending activities cancellation then you can choose to // set WaitForCancellation to false through WithWaitForCancellation(false) for _, f := range pendingFutures { f.Get(ctx, nil) } workflow.GetLogger(ctx).Info("Workflow completed.") return nil } func sampleActivity(ctx context.Context, currentBranchID int, totalDuration time.Duration) (string, error) { logger := activity.GetLogger(ctx) elapsedDuration := time.Nanosecond for elapsedDuration < totalDuration { time.Sleep(time.Second) elapsedDuration += time.Second // record heartbeat every second to check if we are been cancelled activity.RecordHeartbeat(ctx, "status-report-to-workflow") select { case <-ctx.Done(): // We have been cancelled. msg := fmt.Sprintf("Branch %d is cancelled.", currentBranchID) logger.Info(msg) return msg, ctx.Err() default: // We are not cancelled yet. } // Do some custom work // ... } msg := fmt.Sprintf("Branch %d done in %s.", currentBranchID, totalDuration) return msg, nil } ================================================ FILE: cmd/samples/recipes/pickfirst/pickfirst_workflow_test.go ================================================ package main import ( "context" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "go.uber.org/cadence/testsuite" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(samplePickFirstWorkflow) s.env.RegisterActivity(sampleActivity) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_Workflow() { s.env.OnActivity(sampleActivity, mock.Anything, mock.Anything, mock.Anything). Return(func(ctx context.Context, currentBranchID int, totalDuration time.Duration) (string, error) { // make branch 0 super fast so we don't have to wait sleep time in unit test if currentBranchID == 0 { totalDuration = time.Nanosecond } return sampleActivity(ctx, currentBranchID, totalDuration) }).Once() s.env.ExecuteWorkflow(samplePickFirstWorkflow) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) } ================================================ FILE: cmd/samples/recipes/query/README.md ================================================ This sample workflow demos how to use query API to get the current state of running workflow. query_workflow.go shows how to setup a custom workflow query handler query_workflow_test.go shows how to unit-test query functionality Steps to run this sample: 1) You need a cadence service running. See details in cmd/samples/README.md 2) Run the following command to start worker ``` ./bin/query -m worker ``` 3) Run the following command to trigger a workflow execution. You should see workflowID and runID print out on screen. ``` ./bin/query -m trigger ``` 4) Run "./bin/query -m query -w my_workflow_id -r my_run_id -t state" replace my_workflow_id and my_run_id with the workflowID and runID that you see in step 3. You should see current workflow state print on screen. ``` ./bin/query -m query -w -r -t state ``` 5) You could also replace the query type "state" to "__stack_trace" (replace -t state to -t __stack_trace) to dump the call stack for the workflow. ``` ./bin/query -m query -w -r -t __stack_trace ``` ================================================ FILE: cmd/samples/recipes/query/main.go ================================================ 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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "query_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Hour * 10, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, queryWorkflow) } func main() { var mode, workflowID, runID, queryType string flag.StringVar(&mode, "m", "trigger", "Mode is worker, trigger or query.") flag.StringVar(&workflowID, "w", "", "WorkflowID") flag.StringVar(&runID, "r", "", "RunID") flag.StringVar(&queryType, "t", "__stack_trace", "QueryType") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(queryWorkflow) 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) case "query": h.QueryWorkflow(workflowID, runID, queryType) } } ================================================ FILE: cmd/samples/recipes/query/query_workflow.go ================================================ package main import ( "time" "go.uber.org/cadence/workflow" ) // ApplicationName is the task list for this sample const ApplicationName = "queryGroup" // queryWorkflow is an implementation of cadence workflow to demo how to setup query handler func queryWorkflow(ctx workflow.Context) error { queryResult := "started" logger := workflow.GetLogger(ctx) logger.Info("QueryWorkflow started") // setup query handler for query type "state" err := workflow.SetQueryHandler(ctx, "state", func(input []byte) (string, error) { return queryResult, nil }) if err != nil { logger.Info("SetQueryHandler failed: " + err.Error()) return err } queryResult = "waiting on timer" // to simulate workflow been blocked on something, in reality, workflow could wait on anything like activity, signal or timer workflow.NewTimer(ctx, time.Minute*2).Get(ctx, nil) logger.Info("Timer fired") queryResult = "done" logger.Info("QueryWorkflow completed") return nil } ================================================ FILE: cmd/samples/recipes/query/query_workflow_test.go ================================================ package main import ( "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/cadence/testsuite" ) func Test_QueryWorkflow(t *testing.T) { ts := &testsuite.WorkflowTestSuite{} env := ts.NewTestWorkflowEnvironment() env.RegisterWorkflow(queryWorkflow) w := false env.RegisterDelayedCallback(func() { queryAndVerify(t, env, "waiting on timer") w = true }, time.Minute*1) env.ExecuteWorkflow(queryWorkflow) require.True(t, env.IsWorkflowCompleted()) require.NoError(t, env.GetWorkflowError()) require.True(t, w, "state at timer not verified") queryAndVerify(t, env, "done") } func queryAndVerify(t *testing.T, env *testsuite.TestWorkflowEnvironment, expectedState string) { result, err := env.QueryWorkflow("state") require.NoError(t, err) var state string err = result.Get(&state) require.NoError(t, err) require.Equal(t, expectedState, state) } ================================================ FILE: cmd/samples/recipes/retryactivity/main.go ================================================ 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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "retry_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, retryWorkflow) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(retryWorkflow) h.RegisterActivity(batchProcessingActivity) 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) } } ================================================ FILE: cmd/samples/recipes/retryactivity/retry_activity_workflow.go ================================================ package main import ( "context" "time" "go.uber.org/cadence" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This sample workflow executes unreliable activity with retry policy. If activity execution failed, server will * schedule retry based on retry policy configuration. The activity also heartbeat progress so it could resume from * reported progress in retry attempt. */ // ApplicationName is the task list for this sample const ApplicationName = "retryactivityGroup" // retryWorkflow workflow decider func retryWorkflow(ctx workflow.Context) error { ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute * 10, HeartbeatTimeout: time.Second * 10, RetryPolicy: &cadence.RetryPolicy{ InitialInterval: time.Second, BackoffCoefficient: 2.0, MaximumInterval: time.Minute, ExpirationInterval: time.Minute * 5, MaximumAttempts: 5, NonRetriableErrorReasons: []string{"bad-error"}, }, } ctx = workflow.WithActivityOptions(ctx, ao) err := workflow.ExecuteActivity(ctx, batchProcessingActivity, 0, 20, time.Second).Get(ctx, nil) if err != nil { workflow.GetLogger(ctx).Info("Workflow completed with error.", zap.Error(err)) return err } workflow.GetLogger(ctx).Info("Workflow completed.") return nil } // batchProcessingActivity process batchSize of jobs starting from firstTaskID. This activity will heartbeat to report // progress, and it could fail sometimes. Use retry policy to retry when it failed, and resume from reported progress. func batchProcessingActivity(ctx context.Context, firstTaskID, batchSize int, processDelay time.Duration) error { logger := activity.GetLogger(ctx) i := firstTaskID if activity.HasHeartbeatDetails(ctx) { // we are retry from a failed attempt, and there is reported progress that we should resume from. var completedIdx int if err := activity.GetHeartbeatDetails(ctx, &completedIdx); err == nil { i = completedIdx + 1 logger.Info("Resuming from failed attempt", zap.Int("ReportedProgress", completedIdx)) } } taskProcessedInThisAttempt := 0 // used to determine when to fail (simulate failure) for ; i < firstTaskID+batchSize; i++ { // process task i logger.Info("processing task", zap.Int("TaskID", i)) time.Sleep(processDelay) // simulate time spend on processing each task activity.RecordHeartbeat(ctx, i) taskProcessedInThisAttempt++ // simulate failure after process 1/3 of the tasks if taskProcessedInThisAttempt >= batchSize/3 && i < firstTaskID+batchSize-1 { logger.Info("Activity failed, will retry...") // Activity could return different error types for different failures so workflow could handle them differently. // For example, decide to retry or not based on error reasons. return cadence.NewCustomError("some-retryable-error") } } logger.Info("Activity succeed.") return nil } ================================================ FILE: cmd/samples/recipes/retryactivity/retry_activity_workflow_test.go ================================================ package main import ( "context" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "go.uber.org/cadence/activity" "go.uber.org/cadence/testsuite" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(retryWorkflow) s.env.RegisterActivity(batchProcessingActivity) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_Workflow() { var startedIDs []int s.env.OnActivity(batchProcessingActivity, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(func(ctx context.Context, firstTaskID, batchSize int, processDelay time.Duration) error { i := firstTaskID if activity.HasHeartbeatDetails(ctx) { var completedIdx int if err := activity.GetHeartbeatDetails(ctx, &completedIdx); err == nil { i = completedIdx + 1 } } startedIDs = append(startedIDs, i) return batchProcessingActivity(ctx, firstTaskID, batchSize, time.Nanosecond /* override for test */) }) s.env.ExecuteWorkflow(retryWorkflow) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) s.Equal([]int{0, 6, 12, 18}, startedIDs) } ================================================ FILE: cmd/samples/recipes/searchattributes/README.md ================================================ This sample shows how to use search attributes. (Note this feature only works when running Cadence with ElasticSearch) Steps to run this sample: 1) You need a cadence service with Elasticsearch running. The easiest way is by running: ``` docker-compose -f docker-compose-es.yml up ``` For details, see https://github.com/uber/cadence/blob/master/docker/README.md 2) Run the following command to start worker ``` ./bin/searchattributes -m worker ``` 3) Run the following command to trigger a workflow execution. You should see workflowID and runID print out on screen. ``` ./bin/searchattributes -m trigger ``` ================================================ FILE: cmd/samples/recipes/searchattributes/main.go ================================================ package main import ( "context" "flag" "time" "github.com/pborman/uuid" "go.uber.org/cadence/client" "go.uber.org/cadence/worker" "go.uber.org/zap" "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) { workflowClient, err := h.Builder.BuildCadenceClient() if err != nil { h.Logger.Error("Failed to build cadence client.", zap.Error(err)) panic(err) } ctx := context.WithValue(context.Background(), CadenceClientKey, workflowClient) // Configure worker options. workerOptions := worker.Options{ MetricsScope: h.WorkerMetricScope, Logger: h.Logger, BackgroundActivityContext: ctx, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "searchAttributes_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, SearchAttributes: getSearchAttributesForStart(), // optional search attributes when start workflow } h.StartWorkflow(workflowOptions, searchAttributesWorkflow) } func getSearchAttributesForStart() map[string]interface{} { return map[string]interface{}{ "CustomIntField": 1, } } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(searchAttributesWorkflow) h.RegisterActivity(listExecutions) 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) } } ================================================ FILE: cmd/samples/recipes/searchattributes/searchattributes_workflow.go ================================================ package main import ( "bytes" "context" "errors" "fmt" "strconv" "time" "go.uber.org/cadence/.gen/go/shared" "go.uber.org/cadence/activity" "go.uber.org/cadence/client" "go.uber.org/cadence/workflow" "go.uber.org/zap" "github.com/uber-common/cadence-samples/cmd/samples/common" ) /** * This sample shows how to use search attributes. (Note this feature only work with ElasticSearch) */ // ApplicationName is the task list for this sample const ApplicationName = "searchAttributesGroup" // ClientKey is the key for lookup type ClientKey int const ( // DomainName used for this sample DomainName = "default" // CadenceClientKey for retrieving cadence client from context CadenceClientKey ClientKey = iota ) var ( // ErrCadenceClientNotFound when cadence client is not found on context ErrCadenceClientNotFound = errors.New("failed to retrieve cadence client from context") ) // searchAttributesWorkflow workflow decider func searchAttributesWorkflow(ctx workflow.Context) error { logger := workflow.GetLogger(ctx) logger.Info("SearchAttributes workflow started") // get search attributes that provided when start workflow info := workflow.GetInfo(ctx) val := info.SearchAttributes.IndexedFields["CustomIntField"] var currentIntValue int err := client.NewValue(val).Get(¤tIntValue) if err != nil { logger.Error("Get search attribute failed", zap.Error(err)) return err } logger.Info("Current Search Attributes: ", zap.String("CustomIntField", strconv.Itoa(currentIntValue))) // upsert search attributes attributes := map[string]interface{}{ "CustomIntField": 2, // update CustomIntField from 1 to 2, then insert other fields "CustomKeywordField": "Update1", "CustomBoolField": true, "CustomDoubleField": 3.14, "CustomDatetimeField": time.Date(2019, 1, 1, 0, 0, 0, 0, time.Local), "CustomStringField": "String field is for text. When query, it will be tokenized for partial match. StringTypeField cannot be used in Order By", } workflow.UpsertSearchAttributes(ctx, attributes) // print current search attributes info = workflow.GetInfo(ctx) err = printSearchAttributes(info.SearchAttributes, logger) if err != nil { return err } // update search attributes again attributes = map[string]interface{}{ "CustomKeywordField": "Update2", } workflow.UpsertSearchAttributes(ctx, attributes) // print current search attributes info = workflow.GetInfo(ctx) err = printSearchAttributes(info.SearchAttributes, logger) if err != nil { return err } workflow.Sleep(ctx, 2*time.Second) // wait update reflected on ElasticSearch // list workflow ao := workflow.ActivityOptions{ ScheduleToStartTimeout: 2 * time.Minute, StartToCloseTimeout: 2 * time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) query := "CustomIntField=2 and CustomKeywordField='Update2' order by CustomDatetimeField DESC" var listResults []*shared.WorkflowExecutionInfo err = workflow.ExecuteActivity(ctx, listExecutions, query).Get(ctx, &listResults) if err != nil { logger.Error("Failed to list workflow executions.", zap.Error(err)) return err } logger.Info("Workflow completed.", zap.String("Execution", listResults[0].String())) return nil } func printSearchAttributes(searchAttributes *shared.SearchAttributes, logger *zap.Logger) error { buf := new(bytes.Buffer) for k, v := range searchAttributes.IndexedFields { var currentVal interface{} err := client.NewValue(v).Get(¤tVal) if err != nil { logger.Error(fmt.Sprintf("Get search attribute for key %s failed", k), zap.Error(err)) return err } fmt.Fprintf(buf, "%s=%v\n", k, currentVal) } logger.Info(fmt.Sprintf("Current Search Attributes: \n%s", buf.String())) return nil } func listExecutions(ctx context.Context, query string) ([]*shared.WorkflowExecutionInfo, error) { logger := activity.GetLogger(ctx) logger.Info("List executions.", zap.String("Query", query)) cadenceClient, err := getCadenceClientFromContext(ctx) if err != nil { logger.Error("Error when get cadence client") return nil, err } var executions []*shared.WorkflowExecutionInfo var nextPageToken []byte for hasMore := true; hasMore; hasMore = len(nextPageToken) > 0 { resp, err := cadenceClient.ListWorkflow(ctx, &shared.ListWorkflowExecutionsRequest{ Domain: common.StringPtr(DomainName), PageSize: common.Int32Ptr(10), NextPageToken: nextPageToken, Query: common.StringPtr(query), }) if err != nil { return nil, err } for _, r := range resp.Executions { executions = append(executions, r) } nextPageToken = resp.NextPageToken activity.RecordHeartbeat(ctx, nextPageToken) } return executions, nil } func getCadenceClientFromContext(ctx context.Context) (client.Client, error) { logger := activity.GetLogger(ctx) cadenceClient := ctx.Value(CadenceClientKey).(client.Client) if cadenceClient == nil { logger.Error("Could not retrieve cadence client from context.") return nil, ErrCadenceClientNotFound } return cadenceClient, nil } ================================================ FILE: cmd/samples/recipes/searchattributes/searchattributes_workflow_test.go ================================================ package main import ( "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/cadence/.gen/go/shared" "go.uber.org/cadence/testsuite" ) func Test_Workflow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestWorkflowEnvironment() env.RegisterWorkflow(searchAttributesWorkflow) env.RegisterActivity(listExecutions) // mock search attributes on start env.SetSearchAttributesOnStart(getSearchAttributesForStart()) // mock upsert operations attributes := map[string]interface{}{ "CustomIntField": 2, // update CustomIntField from 1 to 2, then insert other fields "CustomKeywordField": "Update1", "CustomBoolField": true, "CustomDoubleField": 3.14, "CustomDatetimeField": time.Date(2019, 1, 1, 0, 0, 0, 0, time.Local), "CustomStringField": "String field is for text. When query, it will be tokenized for partial match. StringTypeField cannot be used in Order By", } env.OnUpsertSearchAttributes(attributes).Return(nil).Once() attributes = map[string]interface{}{ "CustomKeywordField": "Update2", } env.OnUpsertSearchAttributes(attributes).Return(nil).Once() // mock activity env.OnActivity(listExecutions, mock.Anything, mock.Anything).Return([]*shared.WorkflowExecutionInfo{{}}, nil).Once() env.ExecuteWorkflow(searchAttributesWorkflow) require.True(t, env.IsWorkflowCompleted()) require.NoError(t, env.GetWorkflowError()) } ================================================ FILE: cmd/samples/recipes/sideeffect/sideeffect_workflow.go ================================================ package main import ( "context" "time" "github.com/google/uuid" "go.uber.org/cadence/client" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/zap" "github.com/uber-common/cadence-samples/cmd/samples/common" ) const ApplicationName = "HelloSideEffect" func SideEffectWorkflow(ctx workflow.Context) error { logger := workflow.GetLogger(ctx) logger.Info("SideEffectWorkflow started") // setup query handler for query type "state" value := "" err := workflow.SetQueryHandler(ctx, "value", func(input []byte) (string, error) { return value, nil }) if err != nil { logger.Info("SetQueryHandler failed: " + err.Error()) return err } workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} { return uuid.New().String() }).Get(&value) logger.Info("SideEffect value: " + value) logger.Info("SideEffectWorkflow completed") return nil } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "sideffectflow", TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, WorkflowIDReusePolicy: client.WorkflowIDReusePolicyAllowDuplicate, } h.StartWorkflow(workflowOptions, SideEffectWorkflow) } func startWorkers(h *common.SampleHelper) { // Configure worker options. workerOptions := worker.Options{ MetricsScope: h.WorkerMetricScope, Logger: h.Logger, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func main() { //workflow.Register(SideEffectWorkflow) var h common.SampleHelper h.SetupServiceConfig() h.RegisterWorkflow(SideEffectWorkflow) 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. startWorkflow(&h) workflowClient, err := h.Builder.BuildCadenceClient() if err != nil { h.Logger.Error("Failed to build cadence client.", zap.Error(err)) panic(err) } resp, err := workflowClient.QueryWorkflow(context.Background(), "sideffectflow", "", "value") if err != nil { h.Logger.Error("Failed to query workflow", zap.Error(err)) panic("Failed to query workflow.") } var result interface{} if err := resp.Get(&result); err != nil { h.Logger.Error("Failed to decode query result", zap.Error(err)) } h.Logger.Info("Received query result", zap.Any("Result", result)) } ================================================ FILE: cmd/samples/recipes/signalcounter/main.go ================================================ 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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "signal_counter_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Hour, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, sampleSignalCounterWorkflow, 0) } func main() { var mode string var workflowID string var signalValue int flag.StringVar(&mode, "m", "trigger", "Mode is worker, trigger(start a new workflow), or signal.") flag.StringVar(&workflowID, "w", "", "the workflowID to send signal") flag.IntVar(&signalValue, "sig", 1, "the value that is sent to the counter workflow") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleSignalCounterWorkflow) 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) case "signal": h.SignalWorkflow(workflowID, "channelA", signalValue) h.SignalWorkflow(workflowID, "channelB", signalValue) } } ================================================ FILE: cmd/samples/recipes/signalcounter/signal_counter_workflow.go ================================================ package main import ( "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This sample workflow continuously counting signals and do continue as new */ // ApplicationName is the task list for this sample const ApplicationName = "signal_counter" // A workflow execution cannot receive infinite number of signals due to history limit // By default 10000 is MaximumSignalsPerExecution which can be configured by DynamicConfig of Cadence cluster. // But it's recommended to do continueAsNew after receiving certain number of signals(in production, use a number <1000) const maxSignalsPerExecution = 3 // sampleSignalCounterWorkflow Workflow Decider. func sampleSignalCounterWorkflow(ctx workflow.Context, counter int) error { var drainedAllSignals bool signalsPerExecution := 0 logger := workflow.GetLogger(ctx) logger.Info("Started SignalCounterWorkflow") for { s := workflow.NewSelector(ctx) s.AddReceive(workflow.GetSignalChannel(ctx, "channelA"), func(c workflow.Channel, ok bool) { if ok { var i int c.Receive(ctx, &i) counter += i signalsPerExecution += 1 logger.Info("Received signal on channelA.", zap.Int("Counter", i)) } }) s.AddReceive(workflow.GetSignalChannel(ctx, "channelB"), func(c workflow.Channel, ok bool) { if ok { var i int c.Receive(ctx, &i) counter += i signalsPerExecution += 1 logger.Info("Received signal on channelB.", zap.Int("Counter", i)) } }) if signalsPerExecution >= maxSignalsPerExecution { s.AddDefault(func() { // this indicate that we have drained all signals within the decision task, and it's safe to do a continueAsNew drainedAllSignals = true logger.Info("Reached maxSignalsPerExecution limit") }) } s.Select(ctx) if drainedAllSignals { logger.Info("Returning ContinueAsNewError") return workflow.NewContinueAsNewError(ctx, sampleSignalCounterWorkflow, counter) } } } ================================================ FILE: cmd/samples/recipes/signalcounter/workflow_test.go ================================================ package main import ( "strings" "testing" "github.com/stretchr/testify/suite" "go.uber.org/cadence/testsuite" "go.uber.org/cadence/workflow" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(sampleSignalCounterWorkflow) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_SampleSignalCounterWorkflow() { for i:=0; i< 11; i++{ s.env.RegisterDelayedCallback(func() { s.env.SignalWorkflow("channelA", 10) }, 0) } s.env.ExecuteWorkflow(sampleSignalCounterWorkflow, 0) s.True(s.env.IsWorkflowCompleted()) err, ok := s.env.GetWorkflowError().(*workflow.ContinueAsNewError) s.True(ok) s.True(strings.Contains(err.WorkflowType().Name, "sampleSignalCounterWorkflow")) // It should receive and process all 11 signals, even though maxSignalsPerExecution is 10 s.Equal(110, err.Args()[0]) } ================================================ FILE: cmd/samples/recipes/sleep/README.md ================================================ # Sleep Workflow Sample This sample workflow demonstrates how to use the `workflow.Sleep` function in Cadence workflows. The sleep functionality allows workflows to pause execution for a specified duration before continuing with subsequent activities. ## Sample Description The sample workflow: - Takes a sleep duration as input parameter - Uses `workflow.Sleep` to pause workflow execution for the specified duration - Executes a main activity after the sleep period completes - Demonstrates proper error handling for sleep operations - Shows how to configure activity options for post-sleep activities The workflow is useful for scenarios where you need to: - Implement delays or timeouts in workflow logic - Wait for external events or conditions - Implement retry mechanisms with exponential backoff - Create scheduled or periodic workflows ## Key Components - **Workflow**: `sleepWorkflow` demonstrates the sleep functionality with activity execution - **Activity**: `mainSleepActivity` is executed after the sleep period - **Sleep Duration**: Configurable duration (default: 30 seconds) passed as workflow input - **Test**: Includes unit tests to verify sleep and activity execution ## Steps to Run Sample 1. You need a cadence service running. See details in cmd/samples/README.md 2. Run the following command to start the worker: ``` ./bin/sleep -m worker ``` 3. Run the following command to execute the workflow: ``` ./bin/sleep -m trigger ``` You should see logs showing: - Workflow start with sleep duration - Sleep completion message - Main activity execution - Workflow completion ## Customization To modify the sleep behavior: - Change the `sleepDuration` in `main.go` to adjust the default sleep time - Modify the activity options to configure timeouts for post-sleep activities - Add additional activities or logic after the sleep period - Implement conditional sleep based on workflow state ## Use Cases This pattern is useful for: - **Scheduled Tasks**: Implement workflows that need to wait before processing - **Rate Limiting**: Add delays between API calls or external service interactions - **Retry Logic**: Implement exponential backoff for failed operations - **Event-Driven Workflows**: Wait for specific time periods before checking conditions - **Batch Processing**: Add delays between batch operations to avoid overwhelming systems ================================================ FILE: cmd/samples/recipes/sleep/main.go ================================================ 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" ) const ( ApplicationName = "sleepTaskList" SleepWorkflowName = "sleepWorkflow" ) func startWorkers(h *common.SampleHelper) { 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) { sleepDuration := 30 * time.Second workflowOptions := client.StartWorkflowOptions{ ID: "sleep_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, SleepWorkflowName, sleepDuration) } func registerWorkflowAndActivity(h *common.SampleHelper) { h.RegisterWorkflowWithAlias(sleepWorkflow, SleepWorkflowName) h.RegisterActivity(mainSleepActivity) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": registerWorkflowAndActivity(&h) startWorkers(&h) select {} case "trigger": startWorkflow(&h) } } ================================================ FILE: cmd/samples/recipes/sleep/sleep_workflow.go ================================================ package main import ( "context" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) const sleepWorkflowName = "sleepWorkflow" // sleepWorkflow demonstrates workflow.Sleep followed by a main activity call func sleepWorkflow(ctx workflow.Context, sleepDuration time.Duration) error { logger := workflow.GetLogger(ctx) logger.Info("Workflow started, will sleep", zap.String("duration", sleepDuration.String())) // Sleep for the specified duration err := workflow.Sleep(ctx, sleepDuration) if err != nil { logger.Error("Sleep failed", zap.Error(err)) return err } logger.Info("Sleep finished, executing main activity") // Set activity options activityOptions := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, activityOptions) var result string err = workflow.ExecuteActivity(ctx, mainSleepActivity).Get(ctx, &result) if err != nil { logger.Error("Main activity failed", zap.Error(err)) return err } logger.Info("Workflow completed", zap.String("Result", result)) return nil } // mainSleepActivity is a simple activity for demonstration func mainSleepActivity(ctx context.Context) (string, error) { logger := activity.GetLogger(ctx) logger.Info("mainSleepActivity executed") return "Main sleep activity completed", nil } ================================================ FILE: cmd/samples/recipes/sleep/sleep_workflow_test.go ================================================ package main import ( "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" ) func Test_Sleep(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestWorkflowEnvironment() env.RegisterWorkflow(sleepWorkflow) env.RegisterActivity(mainSleepActivity) var activityMessage string env.SetOnActivityCompletedListener(func(activityInfo *activity.Info, result encoded.Value, err error) { result.Get(&activityMessage) }) sleepDuration := 5 * time.Second env.ExecuteWorkflow(sleepWorkflow, sleepDuration) require.True(t, env.IsWorkflowCompleted()) require.NoError(t, env.GetWorkflowError()) require.Equal(t, "Main sleep activity completed", activityMessage) } ================================================ FILE: cmd/samples/recipes/splitmerge/main.go ================================================ 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, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "splitmerge_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, sampleSplitMergeWorkflow, 5) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleSplitMergeWorkflow) h.RegisterActivity(chunkProcessingActivity) 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) } } ================================================ FILE: cmd/samples/recipes/splitmerge/splitmerge_workflow.go ================================================ package main import ( "context" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This sample workflow demonstrates how to use multiple Cadence corotinues (instead of native goroutine) to process a * chunk of a large work item in parallel, and then merge the intermediate result to generate the final result. * In cadence workflow, you should not use go routine. Instead, you use corotinue via workflow.Go method. */ // ApplicationName is the task list for this sample const ApplicationName = "splitmergeGroup" type ( // ChunkResult contains the result for this sample ChunkResult struct { NumberOfItemsInChunk int SumInChunk int } ) // sampleSplitMergeWorkflow workflow decider func sampleSplitMergeWorkflow(ctx workflow.Context, workerCount int) (ChunkResult, error) { chunkResultChannel := workflow.NewChannel(ctx) ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) for i := 1; i <= workerCount; i++ { chunkID := i workflow.Go(ctx, func(ctx workflow.Context) { var result ChunkResult err := workflow.ExecuteActivity(ctx, chunkProcessingActivity, chunkID).Get(ctx, &result) if err == nil { chunkResultChannel.Send(ctx, result) } else { chunkResultChannel.Send(ctx, err) } }) } var totalItemCount, totalSum int for i := 1; i <= workerCount; i++ { var v interface{} chunkResultChannel.Receive(ctx, &v) switch r := v.(type) { case error: // failed to process this chunk // some proper error handling code here case ChunkResult: totalItemCount += r.NumberOfItemsInChunk totalSum += r.SumInChunk } } workflow.GetLogger(ctx).Info("Workflow completed.") return ChunkResult{totalItemCount, totalSum}, nil } func chunkProcessingActivity(ctx context.Context, chunkID int) (result ChunkResult, err error) { // some fake processing logic here numberOfItemsInChunk := chunkID sumInChunk := chunkID * chunkID activity.GetLogger(ctx).Info("Chunck processed", zap.Int("chunkID", chunkID)) return ChunkResult{numberOfItemsInChunk, sumInChunk}, nil } ================================================ FILE: cmd/samples/recipes/splitmerge/splitmerge_workflow_test.go ================================================ package main import ( "testing" "github.com/stretchr/testify/suite" "go.uber.org/cadence/testsuite" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(sampleSplitMergeWorkflow) s.env.RegisterActivity(chunkProcessingActivity) } func (s *UnitTestSuite) TearDownTest() { s.env.AssertExpectations(s.T()) } func (s *UnitTestSuite) Test_Workflow() { workerCount := 5 s.env.ExecuteWorkflow(sampleSplitMergeWorkflow, workerCount) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) var result ChunkResult s.env.GetWorkflowResult(&result) totalItem, totalSum := 0, 0 for i := 1; i <= workerCount; i++ { totalItem += i totalSum += i * i } s.Equal(totalItem, result.NumberOfItemsInChunk) s.Equal(totalSum, result.SumInChunk) } ================================================ FILE: cmd/samples/recipes/timer/main.go ================================================ 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, MaxConcurrentActivityExecutionSize: 3, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "timer_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, sampleTimerWorkflow, time.Second*3) } func main() { var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflow(sampleTimerWorkflow) h.RegisterActivity(orderProcessingActivity) h.RegisterActivity(sendEmailActivity) 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) } } ================================================ FILE: cmd/samples/recipes/timer/workflow.go ================================================ package main import ( "context" "math/rand" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) // ApplicationName is the task list for this sample const ApplicationName = "timerGroup" // sampleTimerWorkflow workflow decider func sampleTimerWorkflow(ctx workflow.Context, processingTimeThreshold time.Duration) error { ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } ctx = workflow.WithActivityOptions(ctx, ao) childCtx, cancelHandler := workflow.WithCancel(ctx) selector := workflow.NewSelector(ctx) // In this sample case, we want to demo a use case where the workflow starts a long running order processing operation // and in the case that the processing takes too long, we want to send out a notification email to user about the delay, // but we won't cancel the operation. If the operation finishes before the timer fires, then we want to cancel the timer. var processingDone bool f := workflow.ExecuteActivity(ctx, orderProcessingActivity) selector.AddFuture(f, func(f workflow.Future) { processingDone = true // cancel timerFuture cancelHandler() }) // use timer future to send notification email if processing takes too long timerFuture := workflow.NewTimer(childCtx, processingTimeThreshold) selector.AddFuture(timerFuture, func(f workflow.Future) { if !processingDone { // processing is not done yet when timer fires, send notification email workflow.ExecuteActivity(ctx, sendEmailActivity).Get(ctx, nil) } }) // wait the timer or the order processing to finish selector.Select(ctx) // now either the order processing is finished, or timer is fired. if !processingDone { // processing not done yet, so the handler for timer will send out notification email. // we still want the order processing to finish, so wait on it. selector.Select(ctx) } workflow.GetLogger(ctx).Info("Workflow completed.") return nil } func orderProcessingActivity(ctx context.Context) error { logger := activity.GetLogger(ctx) logger.Info("sampleActivity processing started.") timeNeededToProcess := time.Second * time.Duration(rand.Intn(10)) time.Sleep(timeNeededToProcess) logger.Info("sampleActivity done.", zap.Duration("duration", timeNeededToProcess)) return nil } func sendEmailActivity(ctx context.Context) error { activity.GetLogger(ctx).Info("sendEmailActivity sending notification email as the process takes long time.") return nil } ================================================ FILE: cmd/samples/recipes/timer/workflow_test.go ================================================ package main import ( "context" "sync" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "go.uber.org/cadence/testsuite" ) type UnitTestSuite struct { suite.Suite testsuite.WorkflowTestSuite env *testsuite.TestWorkflowEnvironment } func TestUnitTestSuite(t *testing.T) { suite.Run(t, new(UnitTestSuite)) } func (s *UnitTestSuite) SetupTest() { s.env = s.NewTestWorkflowEnvironment() s.env.RegisterWorkflow(sampleTimerWorkflow) s.env.RegisterActivity(orderProcessingActivity) s.env.RegisterActivity(sendEmailActivity) } func (s *UnitTestSuite) Test_Workflow_FastProcessing() { // mock to return immediately to simulate fast processing case s.env.OnActivity(orderProcessingActivity, mock.Anything).Return(nil) s.env.OnActivity(sendEmailActivity, mock.Anything).Return(func(ctx context.Context) error { // in fast processing case, this method should not get called s.FailNow("sendEmailActivity should not get called") return nil }) s.env.ExecuteWorkflow(sampleTimerWorkflow, time.Minute) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) } func (s *UnitTestSuite) Test_Workflow_SlowProcessing() { wg := &sync.WaitGroup{} wg.Add(1) s.env.OnActivity(orderProcessingActivity, mock.Anything).Return(func(ctx context.Context) error { // simulate slow processing, will complete this activity only after the sendEmailActivity is called. wg.Wait() return nil }) s.env.OnActivity(sendEmailActivity, mock.Anything).Return(func(ctx context.Context) error { wg.Done() return nil }) s.env.ExecuteWorkflow(sampleTimerWorkflow, time.Microsecond) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) s.env.AssertExpectations(s.T()) } ================================================ FILE: cmd/samples/recipes/tracing/helloworld_workflow.go ================================================ package main import ( "context" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) /** * This is the tracing hello world workflow sample. */ // ApplicationName is the task list for this sample const ApplicationName = "helloWorldGroup" const helloWorldWorkflowName = "helloWorldWorkflow" // helloWorkflow workflow decider func helloWorldWorkflow(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) logger.Info("helloworld workflow started") var helloworldResult string err := workflow.ExecuteActivity(ctx, helloWorldActivity, name).Get(ctx, &helloworldResult) if err != nil { logger.Error("Activity failed.", zap.Error(err)) return err } logger.Info("Workflow completed.", zap.String("Result", helloworldResult)) return nil } func helloWorldActivity(ctx context.Context, name string) (string, error) { logger := activity.GetLogger(ctx) logger.Info("helloworld activity started") return "Hello " + name + "!", nil } ================================================ FILE: cmd/samples/recipes/tracing/main.go ================================================ package main import ( "flag" "fmt" "io" "time" "github.com/opentracing/opentracing-go" "github.com/pborman/uuid" "github.com/uber/jaeger-client-go" "github.com/uber/jaeger-client-go/config" "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, }, Tracer: h.Tracer, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { workflowOptions := client.StartWorkflowOptions{ ID: "helloworld_" + uuid.New(), TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Minute, DecisionTaskStartToCloseTimeout: time.Minute, } h.StartWorkflow(workflowOptions, helloWorldWorkflowName, "Cadence") } func registerWorkflowAndActivity( h *common.SampleHelper, ) { h.RegisterWorkflowWithAlias(helloWorldWorkflow, helloWorldWorkflowName) h.RegisterActivity(helloWorldActivity) } func main() { tracer, closer := initJaeger("cadence-tracing-sample") defer closer.Close() var mode string flag.StringVar(&mode, "m", "trigger", "Mode is worker, trigger or shadower.") flag.Parse() var h common.SampleHelper h.Tracer = tracer 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) } } // initJaeger returns an instance of Jaeger Tracer that samples 100% of traces and logs all spans to stdout. func initJaeger(service string) (opentracing.Tracer, io.Closer) { cfg := &config.Configuration{ ServiceName: service, Sampler: &config.SamplerConfig{ Type: "const", Param: 1, }, Reporter: &config.ReporterConfig{ LogSpans: true, }, } tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger)) if err != nil { panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err)) } return tracer, closer } ================================================ FILE: cmd/samples/recipes/versioning/README.md ================================================ # Versioning Workflow Example This example demonstrates how to safely deploy versioned workflows using Cadence's versioning APIs. It shows how to handle workflow evolution while maintaining backward compatibility and enabling safe rollbacks. ## Overview The versioning sample implements a workflow that evolves through multiple versions (V1 → V2 → V3 → V4) with rollbacks, demonstrating: - **Safe Deployment**: How to deploy new workflow versions without breaking existing executions - **Backward Compatibility**: How to handle workflows started with older versions - **Rollback Capability**: How to safely rollback to previous versions - **Version Isolation**: How different versions can execute different logic paths ## Workflow Versions ### Version 1 (V1) - Executes `FooActivity` only - Uses `workflow.DefaultVersion` for the change ID ### Version 2 (V2) - Supports both `FooActivity` and `BarActivity` - Uses `workflow.GetVersion()` with `workflow.ExecuteWithMinVersion()` to handle both old and new workflows - Workflows started by V1 continue using `FooActivity` ### Version 3 (V3) - Similar to V2 but uses standard `workflow.GetVersion()` (without `ExecuteWithMinVersion`) - All new workflows use version 1 of the change ID ### Version 4 (V4) - Only supports `BarActivity` - Forces all workflows to use version 1 of the change ID - **Breaking change**: Cannot execute workflows started by V1 ## Key Cadence APIs Used - `workflow.GetVersion()`: Determines which version of code to execute - `workflow.ExecuteWithVersion()`: Executes code with a specific version - `workflow.ExecuteWithMinVersion()`: Executes code with minimum version requirement - `workflow.DefaultVersion`: Represents the original version before any changes ## Safe Deployment Flow This example demonstrates a safe deployment strategy that allows you to: 1. **Deploy new versions** while keeping old workers running 2. **Test compatibility** before fully switching over 3. **Rollback safely** if issues are discovered 4. **Gradually migrate** workflows to new versions ## Important Notes - **Single Workflow Limitation**: This sample allows only one workflow at a time to simplify the signal handling mechanism. In production, you would typically handle multiple workflows. - **Signal Method**: The workflow uses a simple signal method to stop gracefully, keeping the implementation straightforward. - **Breaking Changes**: V4 demonstrates what happens when you introduce a breaking change - workflows started by V1 cannot be executed. ## Version Compatibility Matrix | Started By | V1 Worker | V2 Worker | V3 Worker | V4 Worker | |------------|-----------|-----------|-----------|-----------| | V1 | ✅ | ✅ | ✅ | ❌ | | V2 | ❌ | ✅ | ✅ | ✅ | | V3 | ❌ | ✅ | ✅ | ✅ | | V4 | ❌ | ✅ | ✅ | ✅ | ## Running the Example ### Prerequisites Make sure you have Cadence server running and the sample compiled: ```bash # Build the sample go build -o bin/versioning cmd/samples/recipes/versioning/*.go ``` ### Step-by-Step Deployment Simulation #### 1. Start Worker V1 ```bash ./bin/versioning -m worker -v 1 ``` #### 2. Trigger a Workflow ```bash ./bin/versioning -m trigger ``` Wait for logs in the V1 worker to ensure that a workflow has been executed by worker V1. #### 3. Deploy Worker V2 Let's simulate a deployment from V1 to V2 and run a V2 worker alongside the V1 worker: ```bash ./bin/versioning -m worker -v 2 ``` The workflow should still be executed by worker V1. #### 4. Test V2 Compatibility Let's simulate that worker V1 is shut down and the workflow will be rescheduled to the V2 worker: * Kill the process of worker V1 (Ctrl+C), then wait 5 seconds to see workflow rescheduling to worker V2 without errors. Verify logs of the V2 worker - it should handle the workflow started by V1. #### 5. Upgrade to Version V3 Let's continue the deployment and upgrade to V3, running a V3 worker alongside the V2 worker: ```bash ./bin/versioning -m worker -v 3 ``` The workflow should still be executed by worker V2. #### 6. Test V3 Compatibility Let's simulate that worker V2 is shut down and the workflow will be rescheduled to the V3 worker: * Kill the process of worker V2, then wait 5 seconds to see workflow rescheduling to worker V3 without errors. Verify logs of the V3 worker - it should handle the workflow started by V2. #### 7. Gracefully Stop the Workflow Before upgrading to V4, we should ensure that the workflow has been stopped, otherwise it will fail. For this, we need to send a signal to stop it gracefully: ```bash ./bin/versioning -m stop ``` You should see that the workflow has been stopped. #### 8. Start a New Workflow Let's start a new workflow: ```bash ./bin/versioning -m trigger ``` The workflow will use version 1 of the change ID (V3's and V4's default). #### 9. Rollback to Worker V2 Let's imagine that V3 has an issue and we need to rollback to V2. Let's start a worker V2: ```bash ./bin/versioning -m worker -v 2 ``` * Kill the process of worker V3, then wait for workflow rescheduling. * Verify logs of V2 worker - V2 worker should handle workflows started by V3. #### 10. Aggressive Upgrade: V2 to V4 (Breaking Change) We decide to combine getting rid of support for V1 and make an upgrade straightforward to V4: ```bash ./bin/versioning -m worker -v 4 ``` * Kill the process of worker V2, then wait for workflow rescheduling. * Verify logs of V4 worker - V4 worker should handle workflows started by V4. ## Command Reference ```bash # Start a worker with specific version ./bin/versioning -m worker -v # Start a new workflow ./bin/versioning -m trigger # Stop the running workflow ./bin/versioning -m stop ``` Where `` can be: - `1` or `v1` - Version 1 (FooActivity only, DefaultVersion) - `2` or `v2` - Version 2 (FooActivity + BarActivity, DefaultVersion) - `3` or `v3` - Version 3 (FooActivity + BarActivity, Version #1) - `4` or `v4` - Version 4 (BarActivity only, Version #1) ================================================ FILE: cmd/samples/recipes/versioning/main.go ================================================ package main import ( "flag" "fmt" "os" "time" "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) worker.Worker { // Configure worker options. workerOptions := worker.Options{ MetricsScope: h.WorkerMetricScope, Logger: h.Logger, WorkerStopTimeout: 1 * time.Second, } return h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) } func startWorkflow(h *common.SampleHelper) { // Allow to run only one Versioned workflow at a time workflowOptions := client.StartWorkflowOptions{ ID: VersionedWorkflowID, TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Hour, DecisionTaskStartToCloseTimeout: time.Minute, WorkflowIDReusePolicy: client.WorkflowIDReusePolicyAllowDuplicate, } h.StartWorkflow(workflowOptions, VersionedWorkflowName, 0) } // stopWorkflow sends a signal to the workflow to stop it gracefully. func stopWorkflow(h *common.SampleHelper) { h.Logger.Info("Stopping workflow") h.SignalWorkflow(VersionedWorkflowID, StopSignalName, "") } func main() { var mode string var version string flag.StringVar(&mode, "m", "trigger", "Mode is worker (version flag is required), trigger (start a new workflow, only one allowed), stop (stop a running workflow). Default is trigger.") flag.StringVar(&version, "v", "", "Version of the workflow to run, supported versions are 1, 2, 3, or 4. Required in worker mode.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": switch version { case "1", "v1": SetupHelperForVersionedWorkflowV1(&h) case "2", "v2": SetupHelperForVersionedWorkflowV2(&h) case "3", "v3": SetupHelperForVersionedWorkflowV3(&h) case "4", "v4": SetupHelperForVersionedWorkflowV4(&h) case "": fmt.Printf("-v flag is required for worker mode. Use -v 1, -v 2, -v 3, or -v 4 to specify the version.\n") os.Exit(1) default: fmt.Printf("Invalid version specified:%s . Use -v 1, -v 2, -v 3, or -v 4.", version) os.Exit(1) } 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) case "stop": stopWorkflow(&h) default: fmt.Printf("Invalid mode specified: %s. Use -m worker, -m trigger, -m stop.\n", mode) os.Exit(1) } } ================================================ FILE: cmd/samples/recipes/versioning/versioned_workflow.go ================================================ package main import ( "context" "github.com/uber-common/cadence-samples/cmd/samples/common" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" "time" ) /** * This sample workflow continuously counting signals and do continue as new */ const ( // ApplicationName is the task list for this sample ApplicationName = "versioning" // TestChangeID is a constant used to identify the version change in the workflow. TestChangeID = "test-change" // FooActivityName and BarActivityName are the names of the activities used in the workflows. FooActivityName = "FooActivity" BarActivityName = "BarActivity" // VersionedWorkflowName is the name of the versioned workflow. VersionedWorkflowName = "VersionedWorkflow" // VersionedWorkflowID is the ID of the versioned workflow. VersionedWorkflowID = "versioned_workflow" // StopSignalName is the name of the signal used to stop the workflow to finish it successfully StopSignalName = "StopSignal" ) const ( V1 int32 = iota + 1 V2 V3 V4 ) var activityOptions = workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, } // VersionedWorkflowV1 is the first version of the workflow, supports only DefaultVersion. // All workflows started by this version will have the change ID set to DefaultVersion. func VersionedWorkflowV1(ctx workflow.Context) error { ctx = workflow.WithActivityOptions(ctx, activityOptions) err := workflow.ExecuteActivity(ctx, FooActivityName).Get(ctx, nil) if err != nil { return err } return waitForSignal(ctx, V1) } // VersionedWorkflowV2 is the second version of the workflow, supports DefaultVersion and 1 // All workflows started by this version will have the change ID set to DefaultVersion. func VersionedWorkflowV2(ctx workflow.Context) error { ctx = workflow.WithActivityOptions(ctx, activityOptions) var err error var version workflow.Version version = workflow.GetVersion(ctx, TestChangeID, workflow.DefaultVersion, 1, workflow.ExecuteWithMinVersion()) if version == workflow.DefaultVersion { err = workflow.ExecuteActivity(ctx, FooActivityName).Get(ctx, nil) } else { err = workflow.ExecuteActivity(ctx, BarActivityName).Get(ctx, nil) } if err != nil { return err } return waitForSignal(ctx, V2) } // VersionedWorkflowV3 is the third version of the workflow, supports DefaultVersion and 1 // All workflows started by this version will have the change ID set to 1. func VersionedWorkflowV3(ctx workflow.Context) error { ctx = workflow.WithActivityOptions(ctx, activityOptions) var err error var version workflow.Version version = workflow.GetVersion(ctx, TestChangeID, workflow.DefaultVersion, 1) if version == workflow.DefaultVersion { err = workflow.ExecuteActivity(ctx, FooActivityName).Get(ctx, nil) } else { err = workflow.ExecuteActivity(ctx, BarActivityName).Get(ctx, nil) } if err != nil { return err } return waitForSignal(ctx, V3) } // VersionedWorkflowV4 is the fourth version of the workflow, supports only version 1 // All workflows started by this version will have the change ID set to 1. func VersionedWorkflowV4(ctx workflow.Context) error { ctx = workflow.WithActivityOptions(ctx, activityOptions) workflow.GetVersion(ctx, TestChangeID, 1, 1) err := workflow.ExecuteActivity(ctx, BarActivityName).Get(ctx, nil) if err != nil { return err } return waitForSignal(ctx, V4) } func waitForSignal(ctx workflow.Context, version int32) error { workflow.GetLogger(ctx).Info("Waiting for signal", zap.Int32("Worker Version", version)) signalCh := workflow.GetSignalChannel(ctx, StopSignalName) for { var signal string if signalCh.ReceiveAsync(&signal) { break } workflow.GetLogger(ctx).Info("No signal received yet, continuing to wait...", zap.Int32("Worker Version", version)) workflow.Sleep(ctx, time.Second*5) } workflow.GetLogger(ctx).Info("Got the signal, finishing the workflow", zap.Int32("Worker Version", version)) return nil } // SetupHelperForVersionedWorkflowV1 registers VersionedWorkflowV1 and FooActivity func SetupHelperForVersionedWorkflowV1(h *common.SampleHelper) { h.RegisterWorkflowWithAlias(VersionedWorkflowV1, VersionedWorkflowName) h.RegisterActivityWithAlias(FooActivity, FooActivityName) } // SetupHelperForVersionedWorkflowV2 registers VersionedWorkflowV2, FooActivity, and BarActivity func SetupHelperForVersionedWorkflowV2(h *common.SampleHelper) { h.RegisterWorkflowWithAlias(VersionedWorkflowV2, VersionedWorkflowName) h.RegisterActivityWithAlias(FooActivity, FooActivityName) h.RegisterActivityWithAlias(BarActivity, BarActivityName) } // SetupHelperForVersionedWorkflowV3 registers VersionedWorkflowV3, FooActivity, and BarActivity func SetupHelperForVersionedWorkflowV3(h *common.SampleHelper) { h.RegisterWorkflowWithAlias(VersionedWorkflowV3, VersionedWorkflowName) h.RegisterActivityWithAlias(FooActivity, FooActivityName) h.RegisterActivityWithAlias(BarActivity, BarActivityName) } // SetupHelperForVersionedWorkflowV4 registers VersionedWorkflowV4 and BarActivity func SetupHelperForVersionedWorkflowV4(h *common.SampleHelper) { h.RegisterWorkflowWithAlias(VersionedWorkflowV4, VersionedWorkflowName) h.RegisterActivityWithAlias(BarActivity, BarActivityName) } // FooActivity returns "foo" as a result of the activity execution. func FooActivity(ctx context.Context) (string, error) { activity.GetLogger(ctx).Info("Executing FooActivity") return "foo", nil } // BarActivity returns "bar" as a result of the activity execution. func BarActivity(ctx context.Context) (string, error) { activity.GetLogger(ctx).Info("Executing BarActivity") return "bar", nil } ================================================ FILE: cmd/samples/recovery/README.md ================================================ ### Recovery Sample This sample implements a RecoveryWorkflow which is designed to restart all TripWorkflow executions which are currently outstanding and replay all signals from previous run. This is useful where a bad code change is rolled out which causes workflows to get stuck or state is corrupted. ### Steps to run this sample 1) Run the following command to start worker ``` ./bin/query -m worker ``` 2) Run the following command to start trip workflow ``` ./bin/recovery -m trigger -w UserA -wt main.TripWorkflow ``` 3) Run the following command to query trip workflow ``` ./bin/recovery -m query -w UserA ``` 4) Run the following command to send signal to trip workflow ``` ./bin/recovery -m signal -w UserA -s '{"ID": "Trip1", "Total": 10}' ``` 4) Run the following command to start recovery workflow ``` ./bin/recovery -m trigger -w UserB -wt recoveryworkflow -i '{"Type": "TripWorkflow", "Concurrency": 2}' ``` ================================================ FILE: cmd/samples/recovery/cache/cache.go ================================================ package cache import "time" // A Cache is a generalized interface to a cache. See cache.LRU for a specific // implementation (bounded cache with LRU eviction) type Cache interface { // Get retrieves an element based on a key, returning nil if the element // does not exist Get(key string) interface{} // Put adds an element to the cache, returning the previous element Put(key string, value interface{}) interface{} // PutIfNotExist puts a value associated with a given key if it does not exist PutIfNotExist(key string, value interface{}) (interface{}, error) // Delete deletes an element in the cache Delete(key string) // Release decrements the ref count of a pinned element. If the ref count // drops to 0, the element can be evicted from the cache. Release(key string) // Size returns the number of entries currently stored in the Cache Size() int } // Options control the behavior of the cache type Options struct { // TTL controls the time-to-live for a given cache entry. Cache entries that // are older than the TTL will not be returned TTL time.Duration // InitialCapacity controls the initial capacity of the cache InitialCapacity int // Pin prevents in-use objects from getting evicted Pin bool // RemovedFunc is an optional function called when an element // is scheduled for deletion RemovedFunc RemovedFunc } // RemovedFunc is a type for notifying applications when an item is // scheduled for removal from the Cache. If f is a function with the // appropriate signature and i is the interface{} scheduled for // deletion, Cache calls go f(i) type RemovedFunc func(interface{}) ================================================ FILE: cmd/samples/recovery/cache/lru.go ================================================ package cache import ( "container/list" "errors" "sync" "time" ) var ( // ErrCacheFull is returned if Put fails due to cache being filled with pinned elements ErrCacheFull = errors.New("Cache capacity is fully occupied with pinned elements") ) // lru is a concurrent fixed size cache that evicts elements in lru order type lru struct { mut sync.Mutex byAccess *list.List byKey map[string]*list.Element maxSize int ttl time.Duration pin bool rmFunc RemovedFunc } // New creates a new cache with the given options func New(maxSize int, opts *Options) Cache { if opts == nil { opts = &Options{} } return &lru{ byAccess: list.New(), byKey: make(map[string]*list.Element, opts.InitialCapacity), ttl: opts.TTL, maxSize: maxSize, pin: opts.Pin, rmFunc: opts.RemovedFunc, } } // NewLRU creates a new LRU cache of the given size, setting initial capacity // to the max size func NewLRU(maxSize int) Cache { return New(maxSize, nil) } // NewLRUWithInitialCapacity creates a new LRU cache with an initial capacity // and a max size func NewLRUWithInitialCapacity(initialCapacity, maxSize int) Cache { return New(maxSize, &Options{ InitialCapacity: initialCapacity, }) } // Get retrieves the value stored under the given key func (c *lru) Get(key string) interface{} { c.mut.Lock() defer c.mut.Unlock() elt := c.byKey[key] if elt == nil { return nil } cacheEntry := elt.Value.(*cacheEntry) if c.pin { cacheEntry.refCount++ } if cacheEntry.refCount == 0 && !cacheEntry.expiration.IsZero() && time.Now().After(cacheEntry.expiration) { // Entry has expired if c.rmFunc != nil { go c.rmFunc(cacheEntry.value) } c.byAccess.Remove(elt) delete(c.byKey, cacheEntry.key) return nil } c.byAccess.MoveToFront(elt) return cacheEntry.value } // Put puts a new value associated with a given key, returning the existing value (if present) func (c *lru) Put(key string, value interface{}) interface{} { if c.pin { panic("Cannot use Put API in Pin mode. Use Delete and PutIfNotExist if necessary") } val, _ := c.putInternal(key, value, true) return val } // PutIfNotExist puts a value associated with a given key if it does not exist func (c *lru) PutIfNotExist(key string, value interface{}) (interface{}, error) { existing, err := c.putInternal(key, value, false) if err != nil { return nil, err } if existing == nil { // This is a new value return value, err } return existing, err } // Delete deletes a key, value pair associated with a key func (c *lru) Delete(key string) { c.mut.Lock() defer c.mut.Unlock() elt := c.byKey[key] if elt != nil { entry := c.byAccess.Remove(elt).(*cacheEntry) if c.rmFunc != nil { go c.rmFunc(entry.value) } delete(c.byKey, key) } } // Release decrements the ref count of a pinned element. func (c *lru) Release(key string) { c.mut.Lock() defer c.mut.Unlock() elt := c.byKey[key] cacheEntry := elt.Value.(*cacheEntry) cacheEntry.refCount-- } // Size returns the number of entries currently in the lru, useful if cache is not full func (c *lru) Size() int { c.mut.Lock() defer c.mut.Unlock() return len(c.byKey) } // Put puts a new value associated with a given key, returning the existing value (if present) // allowUpdate flag is used to control overwrite behavior if the value exists func (c *lru) putInternal(key string, value interface{}, allowUpdate bool) (interface{}, error) { c.mut.Lock() defer c.mut.Unlock() elt := c.byKey[key] if elt != nil { entry := elt.Value.(*cacheEntry) existing := entry.value if allowUpdate { entry.value = value } if c.ttl != 0 { entry.expiration = time.Now().Add(c.ttl) } c.byAccess.MoveToFront(elt) if c.pin { entry.refCount++ } return existing, nil } entry := &cacheEntry{ key: key, value: value, } if c.pin { entry.refCount++ } if c.ttl != 0 { entry.expiration = time.Now().Add(c.ttl) } c.byKey[key] = c.byAccess.PushFront(entry) if len(c.byKey) == c.maxSize { oldest := c.byAccess.Back().Value.(*cacheEntry) if oldest.refCount > 0 { // Cache is full with pinned elements // revert the insert and return c.byAccess.Remove(c.byAccess.Front()) delete(c.byKey, key) return nil, ErrCacheFull } c.byAccess.Remove(c.byAccess.Back()) if c.rmFunc != nil { go c.rmFunc(oldest.value) } delete(c.byKey, oldest.key) } return nil, nil } type cacheEntry struct { key string expiration time.Time value interface{} refCount int } ================================================ FILE: cmd/samples/recovery/main.go ================================================ package main import ( "context" "encoding/json" "flag" "time" "go.uber.org/cadence/client" "go.uber.org/cadence/worker" "go.uber.org/zap" "github.com/uber-common/cadence-samples/cmd/samples/common" "github.com/uber-common/cadence-samples/cmd/samples/recovery/cache" ) // 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) { workflowClient, err := h.Builder.BuildCadenceClient() if err != nil { h.Logger.Error("Failed to build cadence client.", zap.Error(err)) panic(err) } ctx := context.WithValue(context.Background(), CadenceClientKey, workflowClient) ctx = context.WithValue(ctx, WorkflowExecutionCacheKey, cache.NewLRU(10)) // Configure worker options. workerOptions := worker.Options{ MetricsScope: h.WorkerMetricScope, Logger: h.Logger, BackgroundActivityContext: ctx, } h.StartWorkers(h.Config.DomainName, ApplicationName, workerOptions) // Configure worker options. hostSpecificWorkerOptions := worker.Options{ MetricsScope: h.WorkerMetricScope, Logger: h.Logger, BackgroundActivityContext: ctx, DisableWorkflowWorker: true, } h.StartWorkers(h.Config.DomainName, HostID, hostSpecificWorkerOptions) } func startTripWorkflow(h *common.SampleHelper, workflowID string, user UserState) { workflowOptions := client.StartWorkflowOptions{ ID: workflowID, TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Hour * 24, DecisionTaskStartToCloseTimeout: time.Second * 10, } h.StartWorkflow(workflowOptions, tripWorkflow, user) } func startRecoveryWorkflow(h *common.SampleHelper, workflowID string, params Params) { workflowOptions := client.StartWorkflowOptions{ ID: workflowID, TaskList: ApplicationName, ExecutionStartToCloseTimeout: time.Hour * 24, DecisionTaskStartToCloseTimeout: time.Second * 10, } h.StartWorkflow(workflowOptions, recoverWorkflow, params) } func main() { var mode, workflowID,signal, input, workflowType string flag.StringVar(&mode, "m", "trigger", "Mode is worker or trigger.") flag.StringVar(&workflowID, "w", "workflow_A", "WorkflowID") flag.StringVar(&signal, "s", "signal_data", "SignalData") flag.StringVar(&input, "i", "{}", "Workflow input parameters.") flag.StringVar(&workflowType, "wt", "main.tripWorkflow", "Workflow type.") flag.Parse() var h common.SampleHelper h.SetupServiceConfig() switch mode { case "worker": h.RegisterWorkflowWithAlias(recoverWorkflow, "recoverWorkflow") h.RegisterWorkflowWithAlias(tripWorkflow, "tripWorkflow") h.RegisterActivity(listOpenExecutions) h.RegisterActivity(recoverExecutions) 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": switch workflowType { case "tripworkflow": var userState UserState if err := json.Unmarshal([]byte(input), &userState); err != nil { panic(err) } startTripWorkflow(&h, workflowID, userState) case "recoveryworkflow": var params Params if err := json.Unmarshal([]byte(input), ¶ms); err != nil { panic(err) } startRecoveryWorkflow(&h, workflowID, params) } case "query": h.QueryWorkflow(workflowID, "", QueryName) case "signal": var tripEvent TripEvent if err := json.Unmarshal([]byte(signal), &tripEvent); err != nil { panic(err) } h.SignalWorkflow(workflowID, TripSignalName, tripEvent) } } ================================================ FILE: cmd/samples/recovery/recovery_workflow.go ================================================ package main import ( "context" "errors" "time" "github.com/pborman/uuid" "go.uber.org/cadence" "go.uber.org/cadence/.gen/go/shared" "go.uber.org/cadence/activity" "go.uber.org/cadence/client" "go.uber.org/cadence/workflow" "go.uber.org/zap" "github.com/uber-common/cadence-samples/cmd/samples/common" "github.com/uber-common/cadence-samples/cmd/samples/recovery/cache" ) type ( // Params is the input parameters to RecoveryWorkflow Params struct { ID string Type string Concurrency int } // ListOpenExecutionsResult is the result returned from listOpenExecutions activity ListOpenExecutionsResult struct { ID string Count int HostID string } // RestartParams are parameters extracted from StartWorkflowExecution history event RestartParams struct { Options client.StartWorkflowOptions State UserState } // SignalParams are the parameters extracted from SignalWorkflowExecution history event SignalParams struct { Name string Data TripEvent } ) // ClientKey is the key for lookup type ClientKey int const ( // DomainName used for this sample DomainName = "default" // CadenceClientKey for retrieving cadence client from context CadenceClientKey ClientKey = iota // WorkflowExecutionCacheKey for retrieving executions cache from context WorkflowExecutionCacheKey ) // HostID - Use a new uuid just for demo so we can run 2 host specific activity workers on same machine. // In real world case, you would use a hostname or ip address as HostID. var HostID = uuid.New() var ( // ErrCadenceClientNotFound when cadence client is not found on context ErrCadenceClientNotFound = errors.New("failed to retrieve cadence client from context") // ErrExecutionCacheNotFound when executions cache is not found on context ErrExecutionCacheNotFound = errors.New("failed to retrieve cache from context") ) // This is registration process where you register all your workflows // and activity function handlers. func init() { workflow.RegisterWithOptions(recoverWorkflow, workflow.RegisterOptions{Name: "recoverWorkflow"}) activity.Register(listOpenExecutions) activity.Register(recoverExecutions) } // recoverWorkflow is the workflow implementation to recover TripWorkflow executions func recoverWorkflow(ctx workflow.Context, params Params) error { logger := workflow.GetLogger(ctx) logger.Info("Recover workflow started.") ao := workflow.ActivityOptions{ ScheduleToStartTimeout: 10 * time.Minute, StartToCloseTimeout: 10 * time.Minute, HeartbeatTimeout: time.Second * 30, } ctx = workflow.WithActivityOptions(ctx, ao) var result ListOpenExecutionsResult err := workflow.ExecuteActivity(ctx, listOpenExecutions, params.Type).Get(ctx, &result) if err != nil { logger.Error("Failed to list open workflow executions.", zap.Error(err)) return err } concurrency := 1 if params.Concurrency > 0 { concurrency = params.Concurrency } if result.Count < concurrency { concurrency = result.Count } batchSize := result.Count / concurrency if result.Count%concurrency != 0 { batchSize++ } // Setup retry policy for recovery activity info := workflow.GetInfo(ctx) expiration := time.Duration(info.ExecutionStartToCloseTimeoutSeconds) * time.Second retryPolicy := &cadence.RetryPolicy{ InitialInterval: time.Second, BackoffCoefficient: 2, MaximumInterval: 10 * time.Second, ExpirationInterval: expiration, MaximumAttempts: 100, } ao = workflow.ActivityOptions{ ScheduleToStartTimeout: expiration, StartToCloseTimeout: expiration, HeartbeatTimeout: time.Second * 30, RetryPolicy: retryPolicy, } ctx = workflow.WithActivityOptions(ctx, ao) doneCh := workflow.NewChannel(ctx) for i := 0; i < concurrency; i++ { startIndex := i * batchSize workflow.Go(ctx, func(ctx workflow.Context) { err = workflow.ExecuteActivity(ctx, recoverExecutions, result.ID, startIndex, batchSize).Get(ctx, nil) if err != nil { logger.Error("Recover executions failed.", zap.Int("StartIndex", startIndex), zap.Error(err)) } else { logger.Info("Recover executions completed.", zap.Int("StartIndex", startIndex)) } doneCh.Send(ctx, "done") }) } for i := 0; i < concurrency; i++ { doneCh.Receive(ctx, nil) } logger.Info("Workflow completed.", zap.Int("Result", result.Count)) return nil } func listOpenExecutions(ctx context.Context, workflowType string) (*ListOpenExecutionsResult, error) { key := uuid.New() logger := activity.GetLogger(ctx) logger.Info("List all open executions of type.", zap.String("WorkflowType", workflowType), zap.String("HostID", HostID)) cadenceClient, err := getCadenceClientFromContext(ctx) if err != nil { return nil, err } executionsCache := ctx.Value(WorkflowExecutionCacheKey).(cache.Cache) if executionsCache == nil { logger.Error("Could not retrieve cache from context.") return nil, ErrExecutionCacheNotFound } openExecutions, err := getAllExecutionsOfType(ctx, cadenceClient, workflowType) if err != nil { return nil, err } executionsCache.Put(key, openExecutions) return &ListOpenExecutionsResult{ ID: key, Count: len(openExecutions), HostID: HostID, }, nil } func recoverExecutions(ctx context.Context, key string, startIndex, batchSize int) error { logger := activity.GetLogger(ctx) logger.Info("Starting execution recovery.", zap.String("HostID", HostID), zap.String("Key", key), zap.Int("StartIndex", startIndex), zap.Int("BatchSize", batchSize)) executionsCache := ctx.Value(WorkflowExecutionCacheKey).(cache.Cache) if executionsCache == nil { logger.Error("Could not retrieve cache from context.") return ErrExecutionCacheNotFound } openExecutions := executionsCache.Get(key).([]*shared.WorkflowExecution) endIndex := startIndex + batchSize // Check if this activity has previous heartbeat to retrieve progress from it if activity.HasHeartbeatDetails(ctx) { var finishedIndex int if err := activity.GetHeartbeatDetails(ctx, &finishedIndex); err == nil { // we have finished progress startIndex = finishedIndex + 1 // start from next one. } } for index := startIndex; index < endIndex && index < len(openExecutions); index++ { execution := openExecutions[index] if err := recoverSingleExecution(ctx, execution.GetWorkflowId()); err != nil { logger.Error("Failed to recover execution.", zap.String("WorkflowID", execution.GetWorkflowId()), zap.Error(err)) return err } // Record a heartbeat after each recovery of execution activity.RecordHeartbeat(ctx, index) } return nil } func recoverSingleExecution(ctx context.Context, workflowID string) error { logger := activity.GetLogger(ctx) cadenceClient, err := getCadenceClientFromContext(ctx) if err != nil { return err } execution := &shared.WorkflowExecution{ WorkflowId: common.StringPtr(workflowID), } history, err := getHistory(ctx, execution) if err != nil { return err } if history == nil || len(history) == 0 { // Nothing to recover return nil } firstEvent := history[0] lastEvent := history[len(history)-1] // Extract information from StartWorkflowExecution parameters so we can start a new run params, err := extractStateFromEvent(workflowID, firstEvent) if err != nil { return err } // Parse the entire history and extract all signals so they can be replayed back to new run signals, err := extractSignals(history) if err != nil { return err } // First terminate existing run if already running if !isExecutionCompleted(lastEvent) { err := cadenceClient.TerminateWorkflow(ctx, execution.GetWorkflowId(), execution.GetRunId(), "Recover", nil) if err != nil { return err } } // Start new execution run newRun, err := cadenceClient.StartWorkflow(ctx, params.Options, "TripWorkflow", params.State) if err != nil { return err } // re-inject all signals to new run for _, s := range signals { cadenceClient.SignalWorkflow(ctx, execution.GetWorkflowId(), newRun.RunID, s.Name, s.Data) } logger.Info("Successfully restarted workflow.", zap.String("WorkflowID", execution.GetWorkflowId()), zap.String("NewRunID", newRun.RunID)) return nil } func extractStateFromEvent(workflowID string, event *shared.HistoryEvent) (*RestartParams, error) { switch event.GetEventType() { case shared.EventTypeWorkflowExecutionStarted: attr := event.WorkflowExecutionStartedEventAttributes state, err := deserializeUserState(attr.Input) if err != nil { // Corrupted Workflow Execution State return nil, err } return &RestartParams{ Options: client.StartWorkflowOptions{ ID: workflowID, TaskList: attr.TaskList.GetName(), ExecutionStartToCloseTimeout: time.Second * time.Duration(attr.GetExecutionStartToCloseTimeoutSeconds()), DecisionTaskStartToCloseTimeout: time.Second * time.Duration(attr.GetTaskStartToCloseTimeoutSeconds()), WorkflowIDReusePolicy: client.WorkflowIDReusePolicyAllowDuplicate, //RetryPolicy: attr.RetryPolicy, }, State: state, }, nil default: return nil, errors.New("Unknown event type") } } func extractSignals(events []*shared.HistoryEvent) ([]*SignalParams, error) { var signals []*SignalParams for _, event := range events { if event.GetEventType() == shared.EventTypeWorkflowExecutionSignaled { attr := event.WorkflowExecutionSignaledEventAttributes if attr.GetSignalName() == TripSignalName && attr.Input != nil && len(attr.Input) > 0 { signalData, err := deserializeTripEvent(attr.Input) if err != nil { // Corrupted Signal Payload return nil, err } signal := &SignalParams{ Name: attr.GetSignalName(), Data: signalData, } signals = append(signals, signal) } } } return signals, nil } func isExecutionCompleted(event *shared.HistoryEvent) bool { switch event.GetEventType() { case shared.EventTypeWorkflowExecutionCompleted, shared.EventTypeWorkflowExecutionTerminated, shared.EventTypeWorkflowExecutionCanceled, shared.EventTypeWorkflowExecutionFailed, shared.EventTypeWorkflowExecutionTimedOut: return true default: return false } } func getAllExecutionsOfType(ctx context.Context, cadenceClient client.Client, workflowType string) ([]*shared.WorkflowExecution, error) { var openExecutions []*shared.WorkflowExecution var nextPageToken []byte for hasMore := true; hasMore; hasMore = len(nextPageToken) > 0 { resp, err := cadenceClient.ListOpenWorkflow(ctx, &shared.ListOpenWorkflowExecutionsRequest{ Domain: common.StringPtr(DomainName), MaximumPageSize: common.Int32Ptr(10), NextPageToken: nextPageToken, StartTimeFilter: &shared.StartTimeFilter{ EarliestTime: common.Int64Ptr(0), LatestTime: common.Int64Ptr(time.Now().UnixNano()), }, TypeFilter: &shared.WorkflowTypeFilter{ Name: common.StringPtr(workflowType), }, }) if err != nil { return nil, err } for _, r := range resp.Executions { openExecutions = append(openExecutions, r.Execution) } nextPageToken = resp.NextPageToken activity.RecordHeartbeat(ctx, nextPageToken) } return openExecutions, nil } func getHistory(ctx context.Context, execution *shared.WorkflowExecution) ([]*shared.HistoryEvent, error) { cadenceClient, err := getCadenceClientFromContext(ctx) if err != nil { return nil, err } iter := cadenceClient.GetWorkflowHistory(ctx, execution.GetWorkflowId(), execution.GetRunId(), false, shared.HistoryEventFilterTypeAllEvent) var events []*shared.HistoryEvent for iter.HasNext() { event, err := iter.Next() if err != nil { return nil, err } events = append(events, event) } return events, nil } func getCadenceClientFromContext(ctx context.Context) (client.Client, error) { logger := activity.GetLogger(ctx) cadenceClient := ctx.Value(CadenceClientKey).(client.Client) if cadenceClient == nil { logger.Error("Could not retrieve cadence client from context.") return nil, ErrCadenceClientNotFound } return cadenceClient, nil } ================================================ FILE: cmd/samples/recovery/trip_workflow.go ================================================ package main import ( "encoding/json" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) type ( // UserState kept within workflow and passed from one run to another on ContinueAsNew UserState struct { TripCounter int } // TripEvent passed in as signal to TripWorkflow TripEvent struct { ID string Total int } ) const ( // TripSignalName is the signal name for trip completion event TripSignalName = "trip_event" // ApplicationName is the task list for this sample ApplicationName = "recoveryGroup" // QueryName is the query type name QueryName = "counter" ) // tripWorkflow to keep track of total trip count for a user // It waits on a TripEvent signal and increments a counter on each signal received by this workflow // Trip count is managed as workflow state and passed to new run after 10 signals received by each execution func tripWorkflow(ctx workflow.Context, state UserState) error { logger := workflow.GetLogger(ctx) workflowID := workflow.GetInfo(ctx).WorkflowExecution.ID logger.Info("Trip Workflow Started for User.", zap.String("User", workflowID), zap.Int("TripCounter", state.TripCounter)) // Register query handler to return trip count err := workflow.SetQueryHandler(ctx, QueryName, func(input []byte) (int, error) { return state.TripCounter, nil }) if err != nil { logger.Info("SetQueryHandler failed.", zap.Error(err)) return err } // TripCh to wait on trip completed event signals tripCh := workflow.GetSignalChannel(ctx, TripSignalName) for i := 0; i < 10; i++ { var trip TripEvent tripCh.Receive(ctx, &trip) logger.Info("Trip complete event received.", zap.String("ID", trip.ID), zap.Int("Total", trip.Total)) state.TripCounter++ } logger.Info("Starting a new run.", zap.Int("TripCounter", state.TripCounter)) return workflow.NewContinueAsNewError(ctx, "TripWorkflow", state) } func deserializeUserState(data []byte) (UserState, error) { var state UserState if err := json.Unmarshal(data, &state); err != nil { return UserState{}, err } return state, nil } func deserializeTripEvent(data []byte) (TripEvent, error) { var trip TripEvent if err := json.Unmarshal(data, &trip); err != nil { return TripEvent{}, err } return trip, nil } ================================================ FILE: config/development.yaml ================================================ # config for sample domain: "cadence-samples" service: "cadence-frontend" host: "localhost:7833" # config for emitting metrics #prometheus: # listenAddress: "127.0.0.1:9098" ================================================ FILE: go.mod ================================================ module github.com/uber-common/cadence-samples go 1.21 toolchain go1.24.2 require ( github.com/google/uuid v1.3.0 github.com/m3db/prometheus_client_golang v0.8.1 github.com/opentracing/opentracing-go v1.2.0 github.com/pborman/uuid v1.2.1 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.9.0 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.11 go.uber.org/yarpc v1.60.0 go.uber.org/zap v1.23.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/BurntSushi/toml v0.4.1 // indirect github.com/apache/thrift v0.16.0 // indirect github.com/benbjohnson/clock v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/gogo/googleapis v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/status v1.1.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/golang/mock v1.5.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/m3db/prometheus_client_model v0.1.0 // indirect github.com/m3db/prometheus_common v0.1.0 // indirect github.com/m3db/prometheus_procfs v0.8.1 // indirect github.com/marusama/semaphore/v2 v2.5.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.11.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/robfig/cron v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/twmb/murmur3 v1.1.6 // indirect github.com/uber-go/mapdecode v1.0.0 // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/uber/tchannel-go v1.32.1 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/dig v1.17.0 // indirect go.uber.org/fx v1.13.1 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/net/metrics v1.3.0 // indirect go.uber.org/thriftrw v1.29.2 // indirect golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect golang.org/x/lint v0.0.0-20200130185559-910be7a94367 // indirect golang.org/x/mod v0.8.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.1.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.0.0-20170927054726-6dc17368e09b // indirect golang.org/x/tools v0.6.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce // indirect google.golang.org/grpc v1.28.0 // indirect google.golang.org/protobuf v1.31.0 // indirect honnef.co/go/tools v0.3.2 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/HdrHistogram/hdrhistogram-go v0.9.0 h1:dpujRju0R4M/QZzcnR1LH1qm+TVG3UzkWdp5tH1WMcg= github.com/HdrHistogram/hdrhistogram-go v0.9.0/go.mod h1:nxrse8/Tzg2tg3DZcZjm6qEclQKK70g0KxO61gFFZD4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/apache/thrift v0.0.0-20161221203622-b2a4d4ae21c7/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b h1:AP/Y7sqYicnjGDfD5VcY4CIfh1hRXBUavxrvELjTiOE= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q= github.com/cactus/go-statsd-client/statsd v0.0.0-20191106001114-12b4e2b38748/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/fatih/structtag v1.0.0/go.mod h1:IKitwq45uXL/yqi5mYghiD3w9H6eTOvI9vnk8tXMphA= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.3.2 h1:kX1es4djPJrsDhY7aZKJy7aZasdcB5oSOEphMjSB53c= github.com/gogo/googleapis v1.3.2/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/status v1.1.0 h1:+eIkrewn5q6b30y+g/BJINVVdi2xH7je5MPJ3ZPK3JA= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3-0.20190920234318-1680a479a2cf/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/m3db/prometheus_client_golang v0.8.1 h1:t7w/tcFws81JL1j5sqmpqcOyQOpH4RDOmIe3A3fdN3w= github.com/m3db/prometheus_client_golang v0.8.1/go.mod h1:8R/f1xYhXWq59KD/mbRqoBulXejss7vYtYzWmruNUwI= github.com/m3db/prometheus_client_model v0.1.0 h1:cg1+DiuyT6x8h9voibtarkH1KT6CmsewBSaBhe8wzLo= github.com/m3db/prometheus_client_model v0.1.0/go.mod h1:Qfsxn+LypxzF+lNhak7cF7k0zxK7uB/ynGYoj80zcD4= github.com/m3db/prometheus_common v0.1.0 h1:YJu6eCIV6MQlcwND24cRG/aRkZDX1jvYbsNNs1ZYr0w= github.com/m3db/prometheus_common v0.1.0/go.mod h1:EBmDQaMAy4B8i+qsg1wMXAelLNVbp49i/JOeVszQ/rs= github.com/m3db/prometheus_procfs v0.8.1 h1:LsxWzVELhDU9sLsZTaFLCeAwCn7bC7qecZcK4zobs/g= github.com/m3db/prometheus_procfs v0.8.1/go.mod h1:N8lv8fLh3U3koZx1Bnisj60GYUMDpWb09x1R+dmMOJo= github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM= github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/protectmem v0.0.0-20171002184600-e20412882b3a h1:AA9vgIBDjMHPC2McaGPojgV2dcI78ZC0TLNhYCXEKH8= github.com/prashantv/protectmem v0.0.0-20171002184600-e20412882b3a/go.mod h1:lzZQ3Noex5pfAy7mkAeCjcBDteYU85uWWnJ/y6gKU8k= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.8.0/go.mod h1:PC/OgXc+UN7B4ALwvn1yzVZmVwvhXp5JsbBv6wSv6i0= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.9/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/samuel/go-thrift v0.0.0-20191111193933-5165175b40af h1:EiWVfh8mr40yFZEui2oF0d45KgH48PkB2H0Z0GANvSI= github.com/samuel/go-thrift v0.0.0-20191111193933-5165175b40af/go.mod h1:Vrkh1pnjV9Bl8c3P9zH0/D4NlOHWP5d4/hF4YTULaec= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/streadway/quantile v0.0.0-20150917103942-b0c588724d25/go.mod h1:lbP8tGiBjZ5YWIc2fzuRpTaz0b/53vT6PEs3QuAWzuU= github.com/streadway/quantile v0.0.0-20220407130108-4246515d968d h1:X4+kt6zM/OVO6gbJdAfJR60MGPsqCzbtXNnjoGqdfAs= github.com/streadway/quantile v0.0.0-20220407130108-4246515d968d/go.mod h1:lbP8tGiBjZ5YWIc2fzuRpTaz0b/53vT6PEs3QuAWzuU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/uber-common/bark v1.2.1/go.mod h1:g0ZuPcD7XiExKHynr93Q742G/sbrdVQkghrqLGOoFuY= github.com/uber-go/mapdecode v1.0.0 h1:euUEFM9KnuCa1OBixz1xM+FIXmpixyay5DLymceOVrU= github.com/uber-go/mapdecode v1.0.0/go.mod h1:b5nP15FwXTgpjTjeA9A2uTHXV5UJCl4arwKpP0FP1Hw= github.com/uber-go/tally v3.3.12+incompatible/go.mod h1:YDTIBxdXyOU/sCWilKB4bgyufu1cEi0jdVnRdxvjnmU= github.com/uber-go/tally v3.3.15+incompatible/go.mod h1:YDTIBxdXyOU/sCWilKB4bgyufu1cEi0jdVnRdxvjnmU= github.com/uber-go/tally v3.4.3+incompatible h1:Oq25FXV8cWHPRo+EPeNdbN3LfuozC9mDK2/4vZ1k38U= github.com/uber-go/tally v3.4.3+incompatible/go.mod h1:YDTIBxdXyOU/sCWilKB4bgyufu1cEi0jdVnRdxvjnmU= github.com/uber/cadence-idl v0.0.0-20250616185004-cc6f52f87bc6 h1:YJlEu9Unzifwdn6SuE4rrl4zJ5lop5gBfSX8AyodTww= github.com/uber/cadence-idl v0.0.0-20250616185004-cc6f52f87bc6/go.mod h1:oyUK7GCNCRHCCyWyzifSzXpVrRYVBbAMHAzF5dXiKws= github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/uber/ringpop-go v0.8.5/go.mod h1:zVI6eGO6L7pG14GkntHsSOfmUAWQ7B4lvmzly4IT4ls= github.com/uber/tchannel-go v1.16.0/go.mod h1:Rrgz1eL8kMjW/nEzZos0t+Heq0O4LhnUJVA32OvWKHo= github.com/uber/tchannel-go v1.22.2/go.mod h1:Rrgz1eL8kMjW/nEzZos0t+Heq0O4LhnUJVA32OvWKHo= github.com/uber/tchannel-go v1.32.1 h1:0Pu5kdZceabAt7Rr4pUC4YRpMJkE/tTfReMZdlvDjnU= github.com/uber/tchannel-go v1.32.1/go.mod h1:yT2EUp6YperZ0Tb/jwDX9gVEeiSG74r/L3CjF7zNJHs= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 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.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= go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= go.uber.org/fx v1.10.0/go.mod h1:vLRicqpG/qQEzno4SYU86iCwfT95EZza+Eba0ItuxqY= go.uber.org/fx v1.13.1 h1:CFNTr1oin5OJ0VCZ8EycL3wzF29Jz2g0xe55RFsf2a4= go.uber.org/fx v1.13.1/go.mod h1:bREWhavnedxpJeTq9pQT53BbvwhUv7TcpsOqcH4a+3w= go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/net/metrics v1.3.0 h1:iRLPuVecNYf/wIV+mQaA4IgN8ghifu3q1B4IT6HfwyY= go.uber.org/net/metrics v1.3.0/go.mod h1:pEQrSDGNWT5IVpekWzee5//uHjI4gmgZFkobfw3bv8I= go.uber.org/thriftrw v1.25.0/go.mod h1:IcIfSeZgc59AlYb0xr0DlDKIdD7SgjnFpG9BXCPyy9g= go.uber.org/thriftrw v1.29.2 h1:pRuFLzbGvTcnYwGSjizWRHlbJUzGhu84sRiL1h1kUd8= go.uber.org/thriftrw v1.29.2/go.mod h1:YcjXveberDd28/Bs34SwHy3yu85x/jB4UA2gIcz/Eo0= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/yarpc v1.55.0/go.mod h1:V2JUPDWHYGNpvyuroYjf0KFjwvBCtcFJLuvZqv7TWA0= go.uber.org/yarpc v1.60.0 h1:rNRrS8C0sDk5EScc9epftMdKQeToJ4Nwl2hqTZ1knTY= go.uber.org/yarpc v1.60.0/go.mod h1:fZ4SfvI/7sN9rF/H1W31g3yfbUmVv5fe+6xUSFPJC9E= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367 h1:0IiAsCRByjO2QjX7ZPkw5oU9x+n1YqRL802rjC0c3Aw= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y= golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20170927054726-6dc17368e09b h1:3X+R0qq1+64izd8es+EttB6qcY+JDlVmAhpRXl7gpzU= golang.org/x/time v0.0.0-20170927054726-6dc17368e09b/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191030062658-86caa796c7ab/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191114200427-caa0b0f7d508/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191226212025-6b505debf4bc/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117215004-fe56e6335763/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200216192241-b320d3a0f5a2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce h1:1mbrb1tUU+Zmt5C94IGKADBTJZjZXAd+BubWi7r9EiI= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34= honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: k8s/README.md ================================================ # Cadence Samples Usage Guide This guide explains how to build, deploy, and use the Cadence samples container for testing workflows. ## Prerequisites: Domain Registration Before running any samples, you must first register the domain in Cadence. Execute this command in the cadence-frontend pod: ```bash # Access the cadence-frontend pod kubectl exec -it -n cadence -- /bin/bash # Register the default domain cadence --address $(hostname -i):7833 \ --transport grpc \ --domain default \ domain register \ --retention 1 ``` **Note**: Replace `` with your actual cadence-frontend pod name and adjust the namespace if needed. ## Building the Docker Image Build the samples image with your Cadence host configuration: ```bash docker build --build-arg CADENCE_HOST="cadence-frontend.cadence.svc.cluster.local:7833" -t cadence-samples:latest . ``` **Important**: Replace `cadence-frontend.cadence.svc.cluster.local:7833` with your actual Cadence frontend service address. ### Examples for Different Environments ```bash # Local development docker build --build-arg CADENCE_HOST="localhost:7833" -t cadence-samples:latest -f Dockerfile.samples . # Kubernetes cluster (same namespace) docker build --build-arg CADENCE_HOST="cadence-frontend.cadence.svc.cluster.local:7833" -t cadence-samples:latest . # Different namespace docker build --build-arg CADENCE_HOST="cadence-frontend.my-namespace.svc.cluster.local:7833" -t cadence-samples:latest . ``` ## Upload to Container Registry Tag and push your image to your container registry: ```bash # Tag the image docker tag cadence-samples:latest your-registry.com/cadence-samples:latest # Push to registry docker push your-registry.com/cadence-samples:latest ``` ## Kubernetes Deployment ### Pod Configuration Edit the provided YAML file: ```yaml apiVersion: v1 kind: Pod metadata: name: cadence-samples namespace: cadence # Change to your namespace labels: app: cadence-samples spec: containers: - name: cadence-samples image: cadence-samples:latest # Change to your registry image imagePullPolicy: IfNotPresent command: ["/bin/bash"] args: ["-c", "sleep infinity"] workingDir: /home/cadence env: - name: HOME value: "/home/cadence" resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "1Gi" cpu: "1" restartPolicy: Always securityContext: runAsUser: 1001 runAsGroup: 1001 fsGroup: 1001 ``` **Required Changes**: 1. **`namespace`**: Change to your Cadence namespace 2. **`image`**: Change to your registry image path ### Deploy the Pod ```bash kubectl apply -f cadence-samples-pod.yaml ``` ## Using the Samples ### Step 1: Access the Container ```bash kubectl exec -it cadence-samples -n cadence -- /bin/bash ``` ### Step 2: Run Workflow Examples #### Terminal 1 - Start the Worker ```bash # Example: Hello World worker ./bin/helloworld -m worker ``` #### Terminal 2 - Trigger the Workflow Open a second terminal and execute: ```bash kubectl exec -it cadence-samples -n cadence -- /bin/bash ./bin/helloworld -m trigger ``` #### Stop the Worker In Terminal 1, press `Ctrl+C` to stop the worker. ### Some Available Sample Commands ```bash # Hello World ./bin/helloworld -m worker ./bin/helloworld -m trigger # File Processing ./bin/fileprocessing -m worker ./bin/fileprocessing -m trigger # DSL Example ./bin/dsl -m worker ./bin/dsl -m trigger -dslConfig cmd/samples/dsl/workflow1.yaml ./bin/dsl -m trigger -dslConfig cmd/samples/dsl/workflow2.yaml ``` ## Complete Sample Documentation For all available samples, detailed explanations, and source code, visit: **https://github.com/cadence-workflow/cadence-samples** This repository contains comprehensive documentation for each sample workflow pattern and advanced usage examples. ================================================ FILE: k8s/cadence-samples-pod.yaml ================================================ apiVersion: v1 kind: Pod metadata: name: cadence-samples namespace: cadence # Replace with your cadence namespace labels: app: cadence-samples spec: containers: - name: cadence-samples image: cadence-samples:latest # Replace with your built image imagePullPolicy: IfNotPresent command: ["/bin/bash"] args: ["-c", "sleep infinity"] workingDir: /home/cadence env: - name: HOME value: "/home/cadence" resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "1Gi" cpu: "1" restartPolicy: Always securityContext: runAsUser: 1001 runAsGroup: 1001 fsGroup: 1001 ================================================ FILE: k8s/docker/Dockerfile ================================================ FROM golang:1.21-alpine # Install all necessary dependencies for building and running RUN apk add --no-cache git make gcc musl-dev ca-certificates nano curl bash sed # Build argument for Cadence host configuration ARG CADENCE_HOST=localhost:7833 # Create non-root user RUN addgroup -g 1001 cadence && \ adduser -D -u 1001 -G cadence cadence # Set working directory WORKDIR /home/cadence # Clone cadence-samples repository RUN git clone https://github.com/cadence-workflow/cadence-samples.git . # Update config file with the provided Cadence host RUN sed -i "s/host: \"localhost:7833\"/host: \"${CADENCE_HOST}\"/" config/development.yaml # Build all samples RUN make # Change ownership of files RUN chown -R cadence:cadence /home/cadence # Switch to non-root user USER cadence # Default command - interactive shell CMD ["/bin/bash"] ================================================ FILE: new_samples/README.md ================================================ # Cadence Samples This directory contains samples demonstrating various Cadence workflow concepts. Each sample is self-contained in its own concept folder. ## Available Samples | Folder | Description | |--------|-------------| | [activities/](activities/) | Activity patterns: dynamic execution by name, parallel execution with pick-first | | [client_tls/](client_tls/) | Client-side TLS configuration for secure Cadence connections | | [data/](data/) | Custom DataConverters: gzip compression, AES-256-GCM encryption, and S3 "claim-check" offload | | [hello_world/](hello_world/) | Basic "Hello World" workflow and activity | | [operations/](operations/) | Workflow operations: cancellation and cleanup patterns | | [query/](query/) | Workflow query patterns | | [signal/](signal/) | Workflow signal patterns | ## Prerequisites 1. Install Cadence CLI: [https://cadenceworkflow.io/docs/cli/](https://cadenceworkflow.io/docs/cli/) 2. Run the Cadence server: ```bash git clone https://github.com/cadence-workflow/cadence.git cd cadence docker compose -f docker/docker-compose.yml up ``` 3. Open [localhost:8088](http://localhost:8088) to view Cadence UI 4. Register the `cadence-samples` domain: ```bash cadence --domain cadence-samples domain register ``` ## Running a Sample Each sample folder is self-contained. Navigate to any sample folder and run: ```bash go run . ``` This starts the worker for that sample. Then use the Cadence CLI to start workflows as described in each sample's README. --- ## Adding a New Sample New samples should follow the template-based structure for consistency. The `template/` directory contains Go templates that generate boilerplate code. ### Step 1: Create Your Sample Folder ```bash mkdir my_sample cd my_sample ``` ### Step 2: Create Your Workflow Code Create a file (e.g., `my_workflow.go`) with `package main`: ```go package main import ( "context" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "time" ) func MyWorkflow(ctx workflow.Context) (string, error) { ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, } ctx = workflow.WithActivityOptions(ctx, ao) var result string err := workflow.ExecuteActivity(ctx, MyActivity).Get(ctx, &result) return result, err } func MyActivity(ctx context.Context) (string, error) { return "Hello from my sample!", nil } ``` ### Step 3: Create the Generator Create `generator/generate.go`: ```go package main import "github.com/uber-common/cadence-samples/new_samples/template" func main() { data := template.TemplateData{ SampleName: "My Sample", Workflows: []string{"MyWorkflow"}, Activities: []string{"MyActivity"}, } template.GenerateAll(data) } ``` ### Step 4: Create Sample-Specific Documentation Create `generator/README_specific.md` with documentation specific to your sample: ```markdown ## My Sample Description of what this sample demonstrates... ### Start the workflow \```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.MyWorkflow \ --tl cadence-samples-worker \ --et 60 \ --input '{}' \``` ``` ### Step 5: Run the Generator ```bash cd generator go run generate.go ``` This generates: - `../worker.go` - Worker setup and registration - `../main.go` - Entry point that starts the worker - `../README.md` - Combined documentation - `README.md` - Generator-specific README ### Template Files The `template/` directory contains: | File | Purpose | |------|---------| | `generator.go` | Go code that powers the generation | | `worker.tmpl` | Template for worker.go | | `main.tmpl` | Template for main.go | | `README.tmpl` | Template for README header (prerequisites) | | `README_references.tmpl` | Template for README footer (references) | | `README_generator.tmpl` | Template for generator/README.md | ## Learn More - [Cadence Documentation](https://cadenceworkflow.io/docs) - [Cadence Go Client](https://github.com/uber-go/cadence-client) - [Cadence Server](https://github.com/uber/cadence) ================================================ FILE: new_samples/activities/README.md ================================================ # Activities Sample ## Prerequisites 0. Install Cadence CLI. See instruction [here](https://cadenceworkflow.io/docs/cli/). 1. Run the Cadence server: 1. Clone the [Cadence](https://github.com/cadence-workflow/cadence) repository if you haven't done already: `git clone https://github.com/cadence-workflow/cadence.git` 2. Run `docker compose -f docker/docker-compose.yml up` to start Cadence server 3. See more details at https://github.com/uber/cadence/blob/master/README.md 2. Once everything is up and running in Docker, open [localhost:8088](localhost:8088) to view Cadence UI. 3. Register the `cadence-samples` domain: ```bash cadence --domain cadence-samples domain register ``` Refresh the [domains page](http://localhost:8088/domains) from step 2 to verify `cadence-samples` is registered. ## Steps to run sample Inside the folder this sample is defined, run the following command: ```bash go run . ``` This will call the main function in main.go which starts the worker, which will be execute the sample workflow code ## Samples in this folder This folder contains samples demonstrating various activity patterns in Cadence. ### Dynamic Workflow The `DynamicWorkflow` demonstrates executing an activity by its registered string name rather than passing the function reference directly. This pattern is useful when you need to dynamically determine which activity to execute at runtime. ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.DynamicWorkflow \ --tl cadence-samples-worker \ --et 60 \ --input '{"message":"Cadence"}' ``` ### Parallel Branch Pick First Workflow The `ParallelBranchPickFirstWorkflow` demonstrates running multiple activities in parallel and returning the result of the first one to complete. This pattern is useful for scenarios like: - Racing multiple data sources - Implementing timeouts with fallbacks - Redundant execution for reliability ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.ParallelBranchPickFirstWorkflow \ --tl cadence-samples-worker \ --et 60 \ --input '{}' ``` The workflow will: 1. Start two parallel activities with different delays 2. Wait for the first one to complete 3. Cancel the remaining activity 4. Return the first successful result Note: The `WaitForCancellation` option is set to `true` to demonstrate proper cleanup of cancelled activities. In production, you may set this to `false` if you don't need to wait for cancellation acknowledgment. ## References * The website: https://cadenceworkflow.io * Cadence's server: https://github.com/uber/cadence * Cadence's Go client: https://github.com/uber-go/cadence-client ================================================ FILE: new_samples/activities/dynamic_workflow.go ================================================ package main import ( "context" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" "time" ) const DynamicGreetingActivityName = "cadence_samples.DynamicGreetingActivity" type dynamicWorkflowInput struct { Message string `json:"message"` } func DynamicWorkflow(ctx workflow.Context, input dynamicWorkflowInput) (string, error) { ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) logger.Info("DynamicWorkflow started") var greetingMsg string err := workflow.ExecuteActivity(ctx, DynamicGreetingActivityName, input.Message).Get(ctx, &greetingMsg) if err != nil { logger.Error("DynamicGreetingActivity failed", zap.Error(err)) return "", err } logger.Info("Workflow result", zap.String("greeting", greetingMsg)) return greetingMsg, nil } func DynamicGreetingActivity(ctx context.Context, message string) (string, error) { logger := activity.GetLogger(ctx) logger.Info("DynamicGreetingActivity started.") return "Hello, " + message, nil } ================================================ FILE: new_samples/activities/generator/README.md ================================================ # Sample Generator This folder is NOT part of the actual sample. It exists only for contributors who work on this sample. Please disregard it if you are trying to learn about Cadence. To create a better learning experience for Cadence users, each sample folder is designed to be self contained. Users can view every part of writing and running workflows, including: * Cadence client initialization * Worker with workflow and activity registrations * Workflow starter * and the workflow code itself Some samples may have more or fewer parts depending on what they need to demonstrate. In most cases, the workflow code (e.g. `workflow.go`) is the part that users care about. The rest is boilerplate needed to run that workflow. For each sample folder, the workflow code should be written by hand. The boilerplate can be generated. Keeping all parts inside one folder gives early learners more value because they can see everything together rather than jumping across directories. ## Contributing * When creating a new sample, follow the steps mentioned in the README file in the main samples folder. * To update the sample workflow code, edit the workflow file directly. * To update the worker, client, or other boilerplate logic, edit the generator file. If your change applies to all samples, update the common generator file inside the `template` folder. Edit the generator file in this folder only when the change should affect this sample alone. * When you are done run the following command in the generator folder ```bash go run . ``` ================================================ FILE: new_samples/activities/generator/README_specific.md ================================================ ## Samples in this folder This folder contains samples demonstrating various activity patterns in Cadence. ### Dynamic Workflow The `DynamicWorkflow` demonstrates executing an activity by its registered string name rather than passing the function reference directly. This pattern is useful when you need to dynamically determine which activity to execute at runtime. ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.DynamicWorkflow \ --tl cadence-samples-worker \ --et 60 \ --input '{"message":"Cadence"}' ``` ### Parallel Branch Pick First Workflow The `ParallelBranchPickFirstWorkflow` demonstrates running multiple activities in parallel and returning the result of the first one to complete. This pattern is useful for scenarios like: - Racing multiple data sources - Implementing timeouts with fallbacks - Redundant execution for reliability ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.ParallelBranchPickFirstWorkflow \ --tl cadence-samples-worker \ --et 60 \ --input '{}' ``` The workflow will: 1. Start two parallel activities with different delays 2. Wait for the first one to complete 3. Cancel the remaining activity 4. Return the first successful result Note: The `WaitForCancellation` option is set to `true` to demonstrate proper cleanup of cancelled activities. In production, you may set this to `false` if you don't need to wait for cancellation acknowledgment. ================================================ FILE: new_samples/activities/generator/generate.go ================================================ package main import "github.com/uber-common/cadence-samples/new_samples/template" func main() { // Define the data for Activities samples data := template.TemplateData{ SampleName: "Activities", Workflows: []string{"DynamicWorkflow", "ParallelBranchPickFirstWorkflow"}, Activities: []string{"DynamicGreetingActivity", "ParallelActivity"}, } template.GenerateAll(data) } // Implement custom generator below ================================================ FILE: new_samples/activities/main.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT package main import ( "fmt" "os" "os/signal" "syscall" ) func main() { StartWorker() done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT) fmt.Println("Cadence worker started, press ctrl+c to terminate...") <-done } ================================================ FILE: new_samples/activities/parallel_pick_first_workflow.go ================================================ package main import ( "context" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "time" ) type parallelBranchInput struct { Message string `json:"message"` } // ParallelBranchPickFirstWorkflow is a sample workflow simulating two parallel activities running // at the same time and picking the first successful result. func ParallelBranchPickFirstWorkflow(ctx workflow.Context) (string, error) { logger := workflow.GetLogger(ctx) logger.Info("ParallelBranchPickFirstWorkflow started") selector := workflow.NewSelector(ctx) var firstResp string // Use a cancel handler to cancel all rest of other activities. childCtx, cancelHandler := workflow.WithCancel(ctx) ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, HeartbeatTimeout: time.Second * 20, WaitForCancellation: true, // wait for cancellation to complete } childCtx = workflow.WithActivityOptions(childCtx, ao) // Set WaitForCancellation to true to demonstrate the cancellation to the other activities. In real world case, // you might not care about them and could set WaitForCancellation to false (which is default value). // Run two activities in parallel f1 := workflow.ExecuteActivity(childCtx, ParallelActivity, parallelBranchInput{Message: "first activity"}, time.Second*10) f2 := workflow.ExecuteActivity(childCtx, ParallelActivity, parallelBranchInput{Message: "second activity"}, time.Second*2) pendingFutures := []workflow.Future{f1, f2} selector.AddFuture(f1, func(f workflow.Future) { f.Get(ctx, &firstResp) }).AddFuture(f2, func(f workflow.Future) { f.Get(ctx, &firstResp) }) // wait for any of the future to complete selector.Select(ctx) // now if at least one future is complete, cancel all other pending futures cancelHandler() // - If you want to wait for pending activities to finish after issuing cancellation // then wait for the future to complete. // - if you don't want to wait for completion of pending activities cancellation then you can choose to // set WaitForCancellation to false through WithWaitForCancellation(false) for _, f := range pendingFutures { err := f.Get(ctx, &firstResp) if err != nil { return "", err } } logger.Info("ParallelBranchPickFirstWorkflow completed") return firstResp, nil } func ParallelActivity(ctx context.Context, input parallelBranchInput) (string, error) { logger := activity.GetLogger(ctx) logger.Info("ParallelActivity started") return "Hello " + input.Message, nil } ================================================ FILE: new_samples/activities/worker.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT // Package worker implements a Cadence worker with basic configurations. package main import ( "github.com/uber-go/tally" apiv1 "github.com/uber/cadence-idl/go/proto/api/v1" "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" "go.uber.org/cadence/activity" "go.uber.org/cadence/compatibility" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/yarpc" "go.uber.org/yarpc/peer" yarpchostport "go.uber.org/yarpc/peer/hostport" "go.uber.org/yarpc/transport/grpc" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( HostPort = "127.0.0.1:7833" Domain = "cadence-samples" // TaskListName identifies set of client workflows, activities, and workers. // It could be your group or client or application name. TaskListName = "cadence-samples-worker" ClientName = "cadence-samples-worker" CadenceService = "cadence-frontend" ) // StartWorker creates and starts a basic Cadence worker. func StartWorker() { logger, cadenceClient := BuildLogger(), BuildCadenceClient() workerOptions := worker.Options{ Logger: logger, MetricsScope: tally.NewTestScope(TaskListName, nil), } w := worker.New( cadenceClient, Domain, TaskListName, workerOptions) // workflow registration w.RegisterWorkflowWithOptions(DynamicWorkflow, workflow.RegisterOptions{Name: "cadence_samples.DynamicWorkflow"}) w.RegisterWorkflowWithOptions(ParallelBranchPickFirstWorkflow, workflow.RegisterOptions{Name: "cadence_samples.ParallelBranchPickFirstWorkflow"}) w.RegisterActivityWithOptions(DynamicGreetingActivity, activity.RegisterOptions{Name: "cadence_samples.DynamicGreetingActivity"}) w.RegisterActivityWithOptions(ParallelActivity, activity.RegisterOptions{Name: "cadence_samples.ParallelActivity"}) err := w.Start() if err != nil { panic("Failed to start worker: " + err.Error()) } logger.Info("Started Worker.", zap.String("worker", TaskListName)) } func BuildCadenceClient(dialOptions ...grpc.DialOption) workflowserviceclient.Interface { grpcTransport := grpc.NewTransport() // Create a single peer chooser that identifies the host/port and configures // a gRPC dialer with TLS credentials myChooser := peer.NewSingle( yarpchostport.Identify(HostPort), grpcTransport.NewDialer(dialOptions...), ) outbound := grpcTransport.NewOutbound(myChooser) dispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: ClientName, Outbounds: yarpc.Outbounds{ CadenceService: {Unary: outbound}, }, }) if err := dispatcher.Start(); err != nil { panic("Failed to start dispatcher: " + err.Error()) } clientConfig := dispatcher.ClientConfig(CadenceService) // Create a compatibility adapter that wraps proto-based YARPC clients // to provide a unified interface for domain, workflow, worker, and visibility APIs return compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ) } func BuildLogger() *zap.Logger { config := zap.NewDevelopmentConfig() config.Level.SetLevel(zapcore.InfoLevel) var err error logger, err := config.Build() if err != nil { panic("Failed to setup logger: " + err.Error()) } return logger } ================================================ FILE: new_samples/client_tls/README.md ================================================ # Client TLS Sample This sample demonstrates how to connect to a Cadence server using TLS (Transport Layer Security) for secure communication. ## Prerequisites 1. A Cadence server configured with TLS enabled 2. Client certificates in the `credentials/` directory: - `credentials/client.crt` - Client certificate - `credentials/client.key` - Client private key - `credentials/keytest.crt` - Server CA certificate ## Running the Sample ### Register a Domain Before starting workflows, you may need to register a domain: ```bash go run . register-domain ``` ### Start a Workflow To start a HelloWorld workflow with TLS: ```bash go run . start-workflow ``` **Note:** This requires a worker running to execute the workflow. Start a worker from the `hello_world/` sample first. ## What This Sample Demonstrates ### TLS Configuration The `tls_config.go` file shows how to: - Load client certificates for mutual TLS authentication - Load server CA certificates for server verification - Configure TLS options for gRPC connections ### Cadence Client Setup The `cadence_client.go` file shows how to: - Create a YARPC dispatcher with TLS-enabled gRPC transport - Build a Cadence client using the proto API adapter ### Client Operations The `main.go` file demonstrates: - Starting a workflow execution programmatically - Registering a domain programmatically ## Security Notes - The sample uses `InsecureSkipVerify: true` for development convenience - In production, always verify server certificates by setting `InsecureSkipVerify: false` - Store credentials securely and never commit them to version control ## References - [Cadence TLS Documentation](https://cadenceworkflow.io/docs/operation-guide/tls) - [Go TLS Configuration](https://pkg.go.dev/crypto/tls) ================================================ FILE: new_samples/client_tls/cadence_client.go ================================================ package main import ( apiv1 "github.com/uber/cadence-idl/go/proto/api/v1" "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" "go.uber.org/cadence/compatibility" "go.uber.org/yarpc" "go.uber.org/yarpc/peer" yarpchostport "go.uber.org/yarpc/peer/hostport" "go.uber.org/yarpc/transport/grpc" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( HostPort = "127.0.0.1:7833" ClientName = "cadence-samples-client" CadenceService = "cadence-frontend" ) // BuildCadenceClient creates a Cadence client with optional TLS dial options. func BuildCadenceClient(dialOptions ...grpc.DialOption) workflowserviceclient.Interface { grpcTransport := grpc.NewTransport() myChooser := peer.NewSingle( yarpchostport.Identify(HostPort), grpcTransport.NewDialer(dialOptions...), ) outbound := grpcTransport.NewOutbound(myChooser) dispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: ClientName, Outbounds: yarpc.Outbounds{ CadenceService: {Unary: outbound}, }, }) if err := dispatcher.Start(); err != nil { panic("Failed to start dispatcher: " + err.Error()) } clientConfig := dispatcher.ClientConfig(CadenceService) return compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ) } // BuildLogger creates a zap logger for the client. func BuildLogger() *zap.Logger { config := zap.NewDevelopmentConfig() config.Level.SetLevel(zapcore.InfoLevel) logger, err := config.Build() if err != nil { panic("Failed to setup logger: " + err.Error()) } return logger } ================================================ FILE: new_samples/client_tls/main.go ================================================ package main import ( "context" "fmt" "os" "time" "github.com/google/uuid" "go.uber.org/cadence/.gen/go/shared" "go.uber.org/zap" ) func main() { if len(os.Args) < 2 { printUsage() os.Exit(1) } command := os.Args[1] switch command { case "start-workflow": startWorkflow() case "register-domain": registerDomain() default: fmt.Printf("Unknown command: %s\n", command) printUsage() os.Exit(1) } } func printUsage() { fmt.Println("Usage: go run . ") fmt.Println() fmt.Println("Commands:") fmt.Println(" start-workflow Start a HelloWorld workflow with TLS") fmt.Println(" register-domain Register a domain with TLS") } func startWorkflow() { logger := BuildLogger() logger.Info("Starting workflow with TLS...") tlsDialOption, err := BuildTLSDialOption() if err != nil { logger.Fatal("Failed to build TLS dial option", zap.Error(err)) } cadenceClient := BuildCadenceClient(tlsDialOption) domain := "default" tasklist := "cadence-samples-worker" workflowID := uuid.New().String() requestID := uuid.New().String() executionTimeout := int32(60) closeTimeout := int32(60) workflowType := "cadence_samples.HelloWorldWorkflow" input := []byte(`{"message": "Cadence"}`) req := shared.StartWorkflowExecutionRequest{ Domain: &domain, WorkflowId: &workflowID, WorkflowType: &shared.WorkflowType{ Name: &workflowType, }, TaskList: &shared.TaskList{ Name: &tasklist, }, Input: input, ExecutionStartToCloseTimeoutSeconds: &executionTimeout, TaskStartToCloseTimeoutSeconds: &closeTimeout, RequestId: &requestID, } ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() resp, err := cadenceClient.StartWorkflowExecution(ctx, &req) if err != nil { logger.Fatal("Failed to start workflow", zap.Error(err)) } logger.Info("Successfully started HelloWorld workflow", zap.String("workflowID", workflowID), zap.String("runID", resp.GetRunId())) } func registerDomain() { logger := BuildLogger() logger.Info("Registering domain with TLS...") tlsDialOption, err := BuildTLSDialOption() if err != nil { logger.Fatal("Failed to build TLS dial option", zap.Error(err)) } cadenceClient := BuildCadenceClient(tlsDialOption) domain := "default" retentionDays := int32(7) emitMetric := true description := "Default domain for cadence samples" req := &shared.RegisterDomainRequest{ Name: &domain, Description: &description, WorkflowExecutionRetentionPeriodInDays: &retentionDays, EmitMetric: &emitMetric, } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() err = cadenceClient.RegisterDomain(ctx, req) if err != nil { if _, ok := err.(*shared.DomainAlreadyExistsError); ok { logger.Info("Domain already exists", zap.String("domain", domain)) return } logger.Fatal("Failed to register domain", zap.Error(err)) } logger.Info("Successfully registered domain", zap.String("domain", domain)) } ================================================ FILE: new_samples/client_tls/tls_config.go ================================================ package main import ( "crypto/tls" "crypto/x509" "fmt" "os" "go.uber.org/yarpc/transport/grpc" "google.golang.org/grpc/credentials" ) // BuildTLSDialOption creates a gRPC dial option with TLS credentials for secure // connection to a Cadence server with mutual TLS enabled. func BuildTLSDialOption() (grpc.DialOption, error) { // Load client certificate for mutual TLS clientCert, err := tls.LoadX509KeyPair("credentials/client.crt", "credentials/client.key") if err != nil { return nil, fmt.Errorf("failed to load client certificate: %w", err) } // Load server CA certificate caCert, err := os.ReadFile("credentials/keytest.crt") if err != nil { return nil, fmt.Errorf("failed to load server CA certificate: %w", err) } caCertPool := x509.NewCertPool() if !caCertPool.AppendCertsFromPEM(caCert) { return nil, fmt.Errorf("failed to append CA certificate") } tlsConfig := &tls.Config{ InsecureSkipVerify: true, // For development only - verify certs in production RootCAs: caCertPool, Certificates: []tls.Certificate{clientCert}, MinVersion: tls.VersionTLS12, } creds := credentials.NewTLS(tlsConfig) return grpc.DialerCredentials(creds), nil } ================================================ FILE: new_samples/concurrency/README.md ================================================ # Concurrency Sample ## Prerequisites 0. Install Cadence CLI. See instruction [here](https://cadenceworkflow.io/docs/cli/). 1. Run the Cadence server: 1. Clone the [Cadence](https://github.com/cadence-workflow/cadence) repository if you haven't done already: `git clone https://github.com/cadence-workflow/cadence.git` 2. Run `docker compose -f docker/docker-compose.yml up` to start Cadence server 3. See more details at https://github.com/uber/cadence/blob/master/README.md 2. Once everything is up and running in Docker, open [localhost:8088](localhost:8088) to view Cadence UI. 3. Register the `cadence-samples` domain: ```bash cadence --domain cadence-samples domain register ``` Refresh the [domains page](http://localhost:8088/domains) from step 2 to verify `cadence-samples` is registered. ## Steps to run sample Inside the folder this sample is defined, run the following command: ```bash go run . ``` This will call the main function in main.go which starts the worker, which will be execute the sample workflow code ## Samples in this folder This folder contains samples demonstrating concurrency control patterns in Cadence. ### Batch Processing Workflow The `BatchWorkflow` demonstrates how to process large batches of activities with controlled concurrency using Cadence's `workflow.NewBatchFuture` functionality. #### The Problem It Solves When processing large datasets (thousands of records, files, or API calls), you face a dilemma: - **Sequential processing**: Too slow, poor user experience - **Unlimited concurrency**: Overwhelms databases, APIs, or downstream services - **Manual concurrency control**: Complex error handling and resource management - **Cadence limits**: Max 1024 pending activities per workflow #### The Solution `workflow.NewBatchFuture` provides a robust solution: - **Controlled Concurrency**: Process items in parallel while respecting system limits - **Automatic Error Handling**: Failed activities don't crash the entire batch - **Resource Efficiency**: Optimal throughput without overwhelming downstream services - **Built-in Observability**: Monitoring, retries, and failure tracking - **Workflow Integration**: Seamless integration with Cadence's workflow engine #### Real-World Scenarios - Processing 10,000 user records for a migration - Sending emails to 50,000 subscribers - Generating reports for 1,000 customers - Processing files in a data pipeline #### Sample Behavior - Creates a configurable number of activities (default: 10) - Executes them with controlled concurrency (default: 3) - Simulates work with random delays (900-999ms per activity) - Handles cancellation gracefully #### Technical Considerations - **Cadence limit**: Maximum 1024 pending activities per workflow - **Resource management**: Controlled concurrency prevents system overload - **Error handling**: Failed activities don't crash the entire batch #### How to Start the Workflow ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.BatchWorkflow \ --tl cadence-samples-worker \ --et 300 \ --input '{"Concurrency":3,"TotalSize":10}' ``` You can adjust the parameters: - `Concurrency`: Maximum number of activities running in parallel - `TotalSize`: Total number of activities to process ## References * The website: https://cadenceworkflow.io * Cadence's server: https://github.com/uber/cadence * Cadence's Go client: https://github.com/uber-go/cadence-client ================================================ FILE: new_samples/concurrency/batch_workflow.go ================================================ package main import ( "context" "fmt" "math/rand" "time" "go.uber.org/cadence/workflow" ) // BatchWorkflowInput configures the batch processing parameters. type BatchWorkflowInput struct { Concurrency int // Maximum number of activities running in parallel TotalSize int // Total number of activities to process } // BatchWorkflow demonstrates processing large batches of activities with controlled // concurrency using workflow.NewBatchFuture. This pattern is useful for: // - Processing thousands of records without overwhelming downstream services // - Respecting the 1024 pending activities limit per workflow // - Automatic error handling and retry management func BatchWorkflow(ctx workflow.Context, input BatchWorkflowInput) error { // Create activity factories for each task (not yet executed) factories := make([]func(workflow.Context) workflow.Future, input.TotalSize) for taskID := 0; taskID < input.TotalSize; taskID++ { taskID := taskID // Capture loop variable for closure factories[taskID] = func(ctx workflow.Context) workflow.Future { // Configure activity timeouts aCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute * 1, StartToCloseTimeout: time.Minute * 1, }) return workflow.ExecuteActivity(aCtx, BatchActivity, taskID) } } // Execute all activities with controlled concurrency batch, err := workflow.NewBatchFuture(ctx, input.Concurrency, factories) if err != nil { return fmt.Errorf("failed to create batch future: %w", err) } // Wait for all activities to complete return batch.Get(ctx, nil) } // BatchActivity simulates a unit of work that takes 900-999ms to complete. // In real applications, this would be your actual processing logic. func BatchActivity(ctx context.Context, taskID int) error { select { case <-ctx.Done(): return fmt.Errorf("batch activity %d failed: %w", taskID, ctx.Err()) case <-time.After(time.Duration(rand.Int63n(100))*time.Millisecond + 900*time.Millisecond): return nil } } ================================================ FILE: new_samples/concurrency/batch_workflow_test.go ================================================ package main import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/cadence/testsuite" ) func Test_BatchWorkflow(t *testing.T) { // Create test environment for workflow testing testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestWorkflowEnvironment() // Register the workflow and activity functions env.RegisterWorkflow(BatchWorkflow) env.RegisterActivity(BatchActivity) // Execute workflow with 3 concurrent workers processing 10 tasks env.ExecuteWorkflow(BatchWorkflow, BatchWorkflowInput{ Concurrency: 3, TotalSize: 10, }) // Assert workflow completed successfully without errors assert.True(t, env.IsWorkflowCompleted()) assert.Nil(t, env.GetWorkflowError()) } ================================================ FILE: new_samples/concurrency/generator/README.md ================================================ # Sample Generator This folder is NOT part of the actual sample. It exists only for contributors who work on this sample. Please disregard it if you are trying to learn about Cadence. To create a better learning experience for Cadence users, each sample folder is designed to be self contained. Users can view every part of writing and running workflows, including: * Cadence client initialization * Worker with workflow and activity registrations * Workflow starter * and the workflow code itself Some samples may have more or fewer parts depending on what they need to demonstrate. In most cases, the workflow code (e.g. `workflow.go`) is the part that users care about. The rest is boilerplate needed to run that workflow. For each sample folder, the workflow code should be written by hand. The boilerplate can be generated. Keeping all parts inside one folder gives early learners more value because they can see everything together rather than jumping across directories. ## Contributing * When creating a new sample, follow the steps mentioned in the README file in the main samples folder. * To update the sample workflow code, edit the workflow file directly. * To update the worker, client, or other boilerplate logic, edit the generator file. If your change applies to all samples, update the common generator file inside the `template` folder. Edit the generator file in this folder only when the change should affect this sample alone. * When you are done run the following command in the generator folder ```bash go run . ``` ================================================ FILE: new_samples/concurrency/generator/README_specific.md ================================================ ## Samples in this folder This folder contains samples demonstrating concurrency control patterns in Cadence. ### Batch Processing Workflow The `BatchWorkflow` demonstrates how to process large batches of activities with controlled concurrency using Cadence's `workflow.NewBatchFuture` functionality. #### The Problem It Solves When processing large datasets (thousands of records, files, or API calls), you face a dilemma: - **Sequential processing**: Too slow, poor user experience - **Unlimited concurrency**: Overwhelms databases, APIs, or downstream services - **Manual concurrency control**: Complex error handling and resource management - **Cadence limits**: Max 1024 pending activities per workflow #### The Solution `workflow.NewBatchFuture` provides a robust solution: - **Controlled Concurrency**: Process items in parallel while respecting system limits - **Automatic Error Handling**: Failed activities don't crash the entire batch - **Resource Efficiency**: Optimal throughput without overwhelming downstream services - **Built-in Observability**: Monitoring, retries, and failure tracking - **Workflow Integration**: Seamless integration with Cadence's workflow engine #### Real-World Scenarios - Processing 10,000 user records for a migration - Sending emails to 50,000 subscribers - Generating reports for 1,000 customers - Processing files in a data pipeline #### Sample Behavior - Creates a configurable number of activities (default: 10) - Executes them with controlled concurrency (default: 3) - Simulates work with random delays (900-999ms per activity) - Handles cancellation gracefully #### Technical Considerations - **Cadence limit**: Maximum 1024 pending activities per workflow - **Resource management**: Controlled concurrency prevents system overload - **Error handling**: Failed activities don't crash the entire batch #### How to Start the Workflow ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.BatchWorkflow \ --tl cadence-samples-worker \ --et 300 \ --input '{"Concurrency":3,"TotalSize":10}' ``` You can adjust the parameters: - `Concurrency`: Maximum number of activities running in parallel - `TotalSize`: Total number of activities to process ================================================ FILE: new_samples/concurrency/generator/generate.go ================================================ package main import "github.com/uber-common/cadence-samples/new_samples/template" func main() { // Define the data for Concurrency samples data := template.TemplateData{ SampleName: "Concurrency", Workflows: []string{"BatchWorkflow"}, Activities: []string{"BatchActivity"}, } template.GenerateAll(data) } // Implement custom generator below ================================================ FILE: new_samples/concurrency/main.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT package main import ( "fmt" "os" "os/signal" "syscall" ) func main() { StartWorker() done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT) fmt.Println("Cadence worker started, press ctrl+c to terminate...") <-done } ================================================ FILE: new_samples/concurrency/worker.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT // Package worker implements a Cadence worker with basic configurations. package main import ( "github.com/uber-go/tally" apiv1 "github.com/uber/cadence-idl/go/proto/api/v1" "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" "go.uber.org/cadence/activity" "go.uber.org/cadence/compatibility" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/yarpc" "go.uber.org/yarpc/peer" yarpchostport "go.uber.org/yarpc/peer/hostport" "go.uber.org/yarpc/transport/grpc" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( HostPort = "127.0.0.1:7833" Domain = "cadence-samples" // TaskListName identifies set of client workflows, activities, and workers. // It could be your group or client or application name. TaskListName = "cadence-samples-worker" ClientName = "cadence-samples-worker" CadenceService = "cadence-frontend" ) // StartWorker creates and starts a basic Cadence worker. func StartWorker() { logger, cadenceClient := BuildLogger(), BuildCadenceClient() workerOptions := worker.Options{ Logger: logger, MetricsScope: tally.NewTestScope(TaskListName, nil), } w := worker.New( cadenceClient, Domain, TaskListName, workerOptions) // workflow registration w.RegisterWorkflowWithOptions(BatchWorkflow, workflow.RegisterOptions{Name: "cadence_samples.BatchWorkflow"}) w.RegisterActivityWithOptions(BatchActivity, activity.RegisterOptions{Name: "cadence_samples.BatchActivity"}) err := w.Start() if err != nil { panic("Failed to start worker: " + err.Error()) } logger.Info("Started Worker.", zap.String("worker", TaskListName)) } func BuildCadenceClient(dialOptions ...grpc.DialOption) workflowserviceclient.Interface { grpcTransport := grpc.NewTransport() // Create a single peer chooser that identifies the host/port and configures // a gRPC dialer with TLS credentials myChooser := peer.NewSingle( yarpchostport.Identify(HostPort), grpcTransport.NewDialer(dialOptions...), ) outbound := grpcTransport.NewOutbound(myChooser) dispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: ClientName, Outbounds: yarpc.Outbounds{ CadenceService: {Unary: outbound}, }, }) if err := dispatcher.Start(); err != nil { panic("Failed to start dispatcher: " + err.Error()) } clientConfig := dispatcher.ClientConfig(CadenceService) // Create a compatibility adapter that wraps proto-based YARPC clients // to provide a unified interface for domain, workflow, worker, and visibility APIs return compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ) } func BuildLogger() *zap.Logger { config := zap.NewDevelopmentConfig() config.Level.SetLevel(zapcore.InfoLevel) var err error logger, err := config.Build() if err != nil { panic("Failed to setup logger: " + err.Error()) } return logger } ================================================ FILE: new_samples/data/README.md ================================================ # Data Sample ## Prerequisites 0. Install Cadence CLI. See instruction [here](https://cadenceworkflow.io/docs/cli/). 1. Run the Cadence server: 1. Clone the [Cadence](https://github.com/cadence-workflow/cadence) repository if you haven't done already: `git clone https://github.com/cadence-workflow/cadence.git` 2. Run `docker compose -f docker/docker-compose.yml up` to start Cadence server 3. See more details at https://github.com/uber/cadence/blob/master/README.md 2. Once everything is up and running in Docker, open [localhost:8088](localhost:8088) to view Cadence UI. 3. Register the `cadence-samples` domain: ```bash cadence --domain cadence-samples domain register ``` Refresh the [domains page](http://localhost:8088/domains) from step 2 to verify `cadence-samples` is registered. ## Steps to run sample Inside the folder this sample is defined, run the following command: ```bash go run . ``` This will call the main function in main.go which starts the worker, which will be execute the sample workflow code ## Data Converter Samples This folder demonstrates three production-ready patterns for custom `DataConverter` implementations in Cadence. A `DataConverter` controls how every workflow input, output, and activity parameter is serialized before it is written to Cadence history — making it the right place to add compression, encryption, or external offloading without changing any workflow or activity code. ### What is a DataConverter? A `DataConverter` implements two methods: - `ToData(value ...interface{}) ([]byte, error)` — called before data is written to Cadence history - `FromData(input []byte, valuePtr ...interface{}) error` — called when data is read back The same `DataConverter` must be used by **both the worker and any client that triggers or queries the workflow**. In this sample the workflows generate their own payloads internally, so they can be started from the Cadence CLI without bundling a custom converter into the CLI itself. Each sample runs its own worker on its own task list so it can use its own `DataConverter`. Start all three with a single `go run .`. --- ### Compression Sample `CompressionDataConverterWorkflow` demonstrates gzip-over-JSON compression. For repetitive JSON data this typically achieves 60–80% size reduction, lowering storage costs and bandwidth for large workflow payloads. **Task list:** `cadence-samples-data-compression` ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.CompressionDataConverterWorkflow \ --tl cadence-samples-data-compression \ --et 60 ``` When the worker starts it prints a compression statistics banner showing the before/after sizes of the sample payload so you can see the benefit immediately. --- ### Encryption Sample `EncryptionDataConverterWorkflow` demonstrates AES-256-GCM encryption. Every workflow input, output, and activity parameter is encrypted before being written to Cadence history. Without the key, the data stored by the Cadence server — including any operators browsing workflow history — is completely opaque. The sample uses a `SensitiveCustomerRecord` containing realistic PII and PHI fields (name, email, SSN, credit card, medical notes) to make the use case concrete. **Task list:** `cadence-samples-data-encryption` ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.EncryptionDataConverterWorkflow \ --tl cadence-samples-data-encryption \ --et 60 ``` #### Encryption key By default, the worker uses a hardcoded demo key and prints a prominent warning. To use your own key: ```bash # Generate a random 32-byte (256-bit) key export CADENCE_ENCRYPTION_KEY=$(openssl rand -hex 32) go run . ``` > **WARNING:** The hardcoded demo key (`cadence-demo-key-NOT-FOR-PROD!!!`) is public. > Never use it in production. In production, load your key from a secrets manager > (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, etc.). #### How AES-256-GCM works - `ToData`: JSON-encode arguments → generate a 12-byte random nonce → `cipher.AEAD.Seal` → return `nonce || ciphertext+tag`. - `FromData`: split nonce from input → `cipher.AEAD.Open` → JSON-decode. The GCM authentication tag (16 bytes) ensures ciphertext tampering is detected. The random nonce means the same plaintext produces different ciphertext on every call, preventing replay detection by an attacker who observes Cadence history. --- ### S3 Offload Sample (Claim-Check Pattern) `S3OffloadDataConverterWorkflow` demonstrates the *claim-check* pattern: payloads larger than a configurable threshold are stored in an external `BlobStore` and only a small reference (a few dozen bytes) travels through Cadence workflow history. This solves the practical problem of Cadence's per-payload size limits (~2 MB) for workflows that must pass very large datasets between the workflow and its activities. **Task list:** `cadence-samples-data-s3` ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.S3OffloadDataConverterWorkflow \ --tl cadence-samples-data-s3 \ --et 60 ``` #### How it works - `ToData`: JSON-encode → if `len(json) > thresholdBytes`, upload to `BlobStore` under a SHA-256 key and return `0x01 || {"__s3_ref":"/"}`. Otherwise return `0x00 || json` inline. - `FromData`: read prefix byte → if `0x01`, fetch from `BlobStore` and decode; if `0x00`, decode inline. #### Default store (zero-config) Out of the box, `localFSBlobStore` writes blobs to `os.TempDir()/cadence-samples-data-s3/`. No cloud credentials or additional dependencies are needed. #### Swapping in real AWS S3 The file `s3_dataconverter_workflow.go` contains a commented `s3BlobStore` stub showing the exact AWS SDK calls needed. To enable it: 1. Add the AWS SDK to your module: ```bash go get github.com/aws/aws-sdk-go-v2/config go get github.com/aws/aws-sdk-go-v2/service/s3 ``` 2. Uncomment the `s3BlobStore` section in `s3_dataconverter_workflow.go`. 3. Replace `NewLocalFSBlobStore()` with `NewS3BlobStore(bucket, region)` in `worker.go`. 4. Set standard AWS environment variables (`AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) or use an IAM instance role. You can also point the SDK at [LocalStack](https://localstack.cloud/) or [MinIO](https://min.io/) for local testing without a real AWS account. > **Note on cleanup:** The `s3OffloadDataConverter` does not delete blobs after the workflow completes. In production, use S3 object lifecycle policies to automatically expire old blobs. --- ### When to use which pattern | Pattern | Best for | |---------|----------| | **Compression** | Large repetitive JSON payloads; reducing storage cost without confidentiality requirements | | **Encryption** | PII, PHI, secrets, or any data that must be unreadable in Cadence history | | **S3 Offload** | Payloads approaching Cadence's size limits; binary or non-JSON data; cost-conscious archival | Patterns can be composed: encrypt-then-compress, or encrypt-then-offload to S3 for maximum security and minimum history size. ## References * The website: https://cadenceworkflow.io * Cadence's server: https://github.com/uber/cadence * Cadence's Go client: https://github.com/uber-go/cadence-client ================================================ FILE: new_samples/data/compressed_dataconverter_workflow.go ================================================ package main import ( "bytes" "compress/gzip" "context" "encoding/json" "fmt" "io" "reflect" "strings" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) // compressedJSONDataConverter implements encoded.DataConverter with gzip compression. // It serializes data to JSON, then compresses using gzip to reduce storage size. type compressedJSONDataConverter struct{} // NewCompressedJSONDataConverter creates a new compressed JSON data converter. func NewCompressedJSONDataConverter() encoded.DataConverter { return &compressedJSONDataConverter{} } func (dc *compressedJSONDataConverter) ToData(value ...interface{}) ([]byte, error) { var jsonBuf bytes.Buffer enc := json.NewEncoder(&jsonBuf) for i, obj := range value { err := enc.Encode(obj) if err != nil { return nil, fmt.Errorf("unable to encode argument: %d, %v, with error: %v", i, reflect.TypeOf(obj), err) } } var compressedBuf bytes.Buffer gzipWriter := gzip.NewWriter(&compressedBuf) _, err := gzipWriter.Write(jsonBuf.Bytes()) if err != nil { return nil, fmt.Errorf("unable to compress data: %v", err) } err = gzipWriter.Close() if err != nil { return nil, fmt.Errorf("unable to close gzip writer: %v", err) } return compressedBuf.Bytes(), nil } func (dc *compressedJSONDataConverter) FromData(input []byte, valuePtr ...interface{}) error { // Handle empty input (e.g., when workflow is started without --input from CLI) if len(input) == 0 { return nil } gzipReader, err := gzip.NewReader(bytes.NewBuffer(input)) if err != nil { return fmt.Errorf("unable to create gzip reader: %v", err) } defer gzipReader.Close() decompressedData, err := io.ReadAll(gzipReader) if err != nil { return fmt.Errorf("unable to decompress data: %v", err) } dec := json.NewDecoder(bytes.NewBuffer(decompressedData)) for i, obj := range valuePtr { err := dec.Decode(obj) if err != nil { return fmt.Errorf("unable to decode argument: %d, %v, with error: %v", i, reflect.TypeOf(obj), err) } } return nil } // LargePayload represents a complex data structure with nested objects and arrays // to demonstrate compression benefits. type LargePayload struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Metadata map[string]interface{} `json:"metadata"` Items []Item `json:"items"` Config Config `json:"config"` History []HistoryEntry `json:"history"` Tags []string `json:"tags"` Stats Statistics `json:"statistics"` } type Item struct { ItemID string `json:"item_id"` Title string `json:"title"` Description string `json:"description"` Price float64 `json:"price"` Categories []string `json:"categories"` Attributes map[string]string `json:"attributes"` Reviews []Review `json:"reviews"` Inventory Inventory `json:"inventory"` } type Review struct { ReviewID string `json:"review_id"` UserID string `json:"user_id"` Rating int `json:"rating"` Comment string `json:"comment"` Helpful int `json:"helpful_votes"` NotHelpful int `json:"not_helpful_votes"` Date string `json:"date"` Verified bool `json:"verified_purchase"` Score float64 `json:"score"` } type Inventory struct { Quantity int `json:"quantity"` Location string `json:"location"` LastUpdated string `json:"last_updated"` Status string `json:"status"` } type Config struct { Version string `json:"version"` Environment string `json:"environment"` Settings map[string]string `json:"settings"` Features []string `json:"features"` Limits Limits `json:"limits"` } type Limits struct { MaxItems int `json:"max_items"` MaxRequests int `json:"max_requests_per_minute"` MaxFileSize int `json:"max_file_size_mb"` MaxUsers int `json:"max_concurrent_users"` TimeoutSecs int `json:"timeout_seconds"` } type HistoryEntry struct { EventID string `json:"event_id"` Timestamp string `json:"timestamp"` EventType string `json:"event_type"` UserID string `json:"user_id"` Details map[string]string `json:"details"` Severity string `json:"severity"` } type Statistics struct { TotalItems int `json:"total_items"` TotalUsers int `json:"total_users"` AverageRating float64 `json:"average_rating"` TotalRevenue float64 `json:"total_revenue"` ActiveOrders int `json:"active_orders"` CompletionRate float64 `json:"completion_rate"` } // CreateLargePayload creates a sample large payload with realistic data // to demonstrate compression benefits. func CreateLargePayload() LargePayload { largeDescription := strings.Repeat("This is a comprehensive product catalog containing thousands of items with detailed descriptions, specifications, and user reviews. Each item includes pricing information, inventory status, and customer feedback. The catalog is designed to provide complete information for customers making purchasing decisions. ", 50) items := make([]Item, 100) for i := 0; i < 100; i++ { reviews := make([]Review, 25) for j := 0; j < 25; j++ { reviews[j] = Review{ ReviewID: fmt.Sprintf("review_%d_%d", i, j), UserID: fmt.Sprintf("user_%d", j), Rating: 1 + (j % 5), Comment: strings.Repeat("This is a detailed customer review with comprehensive feedback about the product quality, delivery experience, and overall satisfaction. The customer provides specific details about their experience. ", 3), Helpful: j * 2, NotHelpful: j, Date: "2024-01-15T10:30:00Z", Verified: j%2 == 0, Score: float64(1+(j%5)) + float64(j%10)/10.0, } } attributes := make(map[string]string) for k := 0; k < 20; k++ { attributes[fmt.Sprintf("attr_%d", k)] = strings.Repeat("This is a detailed attribute description with comprehensive information about the product specification. ", 2) } items[i] = Item{ ItemID: fmt.Sprintf("item_%d", i), Title: fmt.Sprintf("High-Quality Product %d with Advanced Features", i), Description: strings.Repeat("This is a premium product with exceptional quality and advanced features designed for professional use. It includes comprehensive documentation and support. ", 10), Price: float64(100+i*10) + float64(i%100)/100.0, Categories: []string{"Electronics", "Professional", "Premium", "Advanced"}, Attributes: attributes, Reviews: reviews, Inventory: Inventory{ Quantity: 100 + i, Location: fmt.Sprintf("Warehouse %d", i%5), LastUpdated: "2024-01-15T10:30:00Z", Status: "In Stock", }, } } history := make([]HistoryEntry, 50) for i := 0; i < 50; i++ { details := make(map[string]string) for j := 0; j < 10; j++ { details[fmt.Sprintf("detail_%d", j)] = strings.Repeat("This is a detailed event description with comprehensive information about the system event and its impact. ", 2) } history[i] = HistoryEntry{ EventID: fmt.Sprintf("event_%d", i), Timestamp: "2024-01-15T10:30:00Z", EventType: "system_update", UserID: fmt.Sprintf("admin_%d", i%5), Details: details, Severity: "medium", } } metadata := make(map[string]interface{}) for i := 0; i < 30; i++ { metadata[fmt.Sprintf("meta_key_%d", i)] = strings.Repeat("This is comprehensive metadata information with detailed descriptions and specifications. ", 5) } return LargePayload{ ID: "large_payload_001", Name: "Comprehensive Product Catalog", Description: largeDescription, Metadata: metadata, Items: items, Config: Config{ Version: "2.1.0", Environment: "production", Settings: map[string]string{ "cache_enabled": "true", "compression_level": "high", "timeout": "30s", "max_connections": "1000", "retry_attempts": "3", }, Features: []string{"advanced_search", "real_time_updates", "analytics", "reporting", "integration"}, Limits: Limits{ MaxItems: 10000, MaxRequests: 1000, MaxFileSize: 100, MaxUsers: 5000, TimeoutSecs: 30, }, }, History: history, Tags: []string{"catalog", "products", "inventory", "analytics", "reporting", "integration", "api", "dashboard"}, Stats: Statistics{ TotalItems: 10000, TotalUsers: 5000, AverageRating: 4.2, TotalRevenue: 1250000.50, ActiveOrders: 250, CompletionRate: 98.5, }, } } // GetPayloadSizeInfo returns information about the payload size before and after compression. func GetPayloadSizeInfo(payload LargePayload, converter encoded.DataConverter) (int, int, float64, error) { jsonData, err := json.Marshal(payload) if err != nil { return 0, 0, 0, fmt.Errorf("failed to marshal payload: %v", err) } originalSize := len(jsonData) compressedData, err := converter.ToData(payload) if err != nil { return 0, 0, 0, fmt.Errorf("failed to compress payload: %v", err) } compressedSize := len(compressedData) compressionRatio := float64(compressedSize) / float64(originalSize) compressionPercentage := (1.0 - compressionRatio) * 100 return originalSize, compressedSize, compressionPercentage, nil } // CompressionDataConverterWorkflow demonstrates processing large payloads with compression. // The DataConverter automatically compresses/decompresses all workflow data. // Note: The workflow generates its own payload internally so it can be started from CLI // without requiring the CLI to use the custom DataConverter. The compression demonstration // happens when data is passed between workflow and activity. func CompressionDataConverterWorkflow(ctx workflow.Context) (LargePayload, error) { logger := workflow.GetLogger(ctx) // Generate the large payload internally - this allows the workflow to be started // from CLI without needing a custom DataConverter on the client side. // The compression benefit is demonstrated when passing data to/from activities. input := CreateLargePayload() logger.Info("Large payload workflow started", zap.String("payload_id", input.ID)) logger.Info("Processing large payload with compression", zap.Int("items_count", len(input.Items))) activityOptions := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, } ctx = workflow.WithActivityOptions(ctx, activityOptions) var result LargePayload err := workflow.ExecuteActivity(ctx, CompressionDataConverterActivity, input).Get(ctx, &result) if err != nil { logger.Error("Large payload activity failed", zap.Error(err)) return LargePayload{}, err } logger.Info("Large payload workflow completed", zap.String("result_id", result.ID)) logger.Info("Note: All large payload data was automatically compressed/decompressed using gzip compression") return result, nil } // CompressionDataConverterActivity processes the large payload. // In production, this might involve data transformation, validation, etc. func CompressionDataConverterActivity(ctx context.Context, input LargePayload) (LargePayload, error) { logger := activity.GetLogger(ctx) logger.Info("Large payload activity received input", zap.String("payload_id", input.ID), zap.Int("items_count", len(input.Items))) input.Name = input.Name + " (Processed)" input.Stats.TotalItems = len(input.Items) logger.Info("Large payload activity completed", zap.String("result_id", input.ID)) return input, nil } ================================================ FILE: new_samples/data/compressed_dataconverter_workflow_test.go ================================================ package main import ( "testing" "github.com/stretchr/testify/require" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" "go.uber.org/cadence/worker" ) func Test_CompressionDataConverterWorkflow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestWorkflowEnvironment() env.RegisterWorkflow(CompressionDataConverterWorkflow) env.RegisterActivity(CompressionDataConverterActivity) dataConverter := NewCompressedJSONDataConverter() workerOptions := worker.Options{ DataConverter: dataConverter, } env.SetWorkerOptions(workerOptions) var activityResult LargePayload env.SetOnActivityCompletedListener(func(activityInfo *activity.Info, result encoded.Value, err error) { result.Get(&activityResult) }) // Workflow generates its own payload internally, no input needed env.ExecuteWorkflow(CompressionDataConverterWorkflow) require.True(t, env.IsWorkflowCompleted()) require.NoError(t, env.GetWorkflowError()) require.Equal(t, "Comprehensive Product Catalog (Processed)", activityResult.Name) require.Equal(t, 100, activityResult.Stats.TotalItems) } ================================================ FILE: new_samples/data/encrypted_dataconverter_workflow.go ================================================ package main import ( "bytes" "context" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "io" "os" "reflect" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) // encryptedJSONDataConverter implements encoded.DataConverter with AES-256-GCM encryption. // It serializes data to JSON, then encrypts using AES-256-GCM so that workflow history // stored in Cadence is opaque to anyone without the key. type encryptedJSONDataConverter struct { gcm cipher.AEAD } var errFailedToCreateConverter = errors.New("failed to create encrypted data converter") // NewEncryptedJSONDataConverter creates a new encrypted JSON data converter. // key must be exactly 32 bytes (AES-256). Returns an error if the key is invalid. func NewEncryptedJSONDataConverter(key []byte) (encoded.DataConverter, error) { block, err := aes.NewCipher(key) if err != nil { return nil, errors.Join(errFailedToCreateConverter, err) } gcm, err := cipher.NewGCM(block) if err != nil { return nil, errors.Join(errFailedToCreateConverter, err) } return &encryptedJSONDataConverter{gcm: gcm}, nil } // demoEncryptionKey is a hardcoded 32-byte key used ONLY when CADENCE_ENCRYPTION_KEY is unset. // DO NOT use this key in production. Rotate your key and load it from a secrets manager. var demoEncryptionKey = []byte("cadence-demo-key-NOT-FOR-PROD!!!") // LoadEncryptionKey reads a 32-byte AES key from the CADENCE_ENCRYPTION_KEY environment // variable (hex-encoded, 64 hex chars). If the env var is unset, falls back to a hardcoded // demo key with a warning. If the env var is set but invalid, panics — silently falling back // to the public demo key when the user clearly intended their own key would be a security // hole. func LoadEncryptionKey() []byte { hexKey := os.Getenv("CADENCE_ENCRYPTION_KEY") if hexKey == "" { fmt.Println("WARNING: CADENCE_ENCRYPTION_KEY not set. Using hardcoded demo key.") fmt.Println("WARNING: DO NOT USE THE DEMO KEY IN PRODUCTION.") return demoEncryptionKey } key, err := hex.DecodeString(hexKey) if err != nil { panic(fmt.Sprintf("CADENCE_ENCRYPTION_KEY is not valid hex: %v", err)) } if len(key) != 32 { panic(fmt.Sprintf("CADENCE_ENCRYPTION_KEY must be exactly 64 hex chars (32 bytes), got %d hex chars (%d bytes)", len(hexKey), len(key))) } return key } func (dc *encryptedJSONDataConverter) ToData(value ...interface{}) ([]byte, error) { var jsonBuf bytes.Buffer enc := json.NewEncoder(&jsonBuf) for i, obj := range value { if err := enc.Encode(obj); err != nil { return nil, fmt.Errorf("unable to encode argument: %d, %v, with error: %v", i, reflect.TypeOf(obj), err) } } nonce := make([]byte, dc.gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, fmt.Errorf("unable to generate nonce: %v", err) } // Seal appends the GCM authentication tag to the ciphertext. // Output layout: nonce (12 bytes) || ciphertext+tag ciphertext := dc.gcm.Seal(nonce, nonce, jsonBuf.Bytes(), nil) return ciphertext, nil } func (dc *encryptedJSONDataConverter) FromData(input []byte, valuePtr ...interface{}) error { if len(input) == 0 { return nil } nonceSize := dc.gcm.NonceSize() if len(input) < nonceSize { return fmt.Errorf("ciphertext too short: %d bytes", len(input)) } nonce, ciphertext := input[:nonceSize], input[nonceSize:] plaintext, err := dc.gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return fmt.Errorf("decryption failed: %v", err) } dec := json.NewDecoder(bytes.NewBuffer(plaintext)) for i, obj := range valuePtr { if err := dec.Decode(obj); err != nil { return fmt.Errorf("unable to decode argument: %d, %v, with error: %v", i, reflect.TypeOf(obj), err) } } return nil } // SensitiveCustomerRecord represents PII/PHI data that must be encrypted in workflow history. type SensitiveCustomerRecord struct { CustomerID string `json:"customer_id"` FullName string `json:"full_name"` Email string `json:"email"` SSN string `json:"ssn"` CreditCard string `json:"credit_card_number"` BillingAddr string `json:"billing_address"` MedicalNotes string `json:"medical_notes"` DiagnosisCode string `json:"diagnosis_code"` Prescriptions string `json:"prescriptions"` InsuranceID string `json:"insurance_id"` ProcessedBy string `json:"processed_by"` } // CreateSensitiveCustomerRecord creates a sample customer record with realistic PII/PHI. func CreateSensitiveCustomerRecord() SensitiveCustomerRecord { return SensitiveCustomerRecord{ CustomerID: "cust_8a7f3b2e", FullName: "Jane A. Doe", Email: "jane.doe@example.com", SSN: "123-45-6789", CreditCard: "4111-1111-1111-1111", BillingAddr: "1234 Elm Street, Springfield, IL 62701", MedicalNotes: "Patient presents with hypertension and type-2 diabetes. Advised dietary changes and increased physical activity. Follow-up scheduled in 3 months.", DiagnosisCode: "I10, E11.9", Prescriptions: "Lisinopril 10mg once daily; Metformin 500mg twice daily", InsuranceID: "INS-987654321", ProcessedBy: "workflow-processor-v2", } } // GetEncryptionSizeInfo returns the plaintext size, ciphertext size, and a hex preview of the ciphertext. func GetEncryptionSizeInfo(record SensitiveCustomerRecord, converter encoded.DataConverter) (int, int, string, error) { jsonData, err := json.Marshal(record) if err != nil { return 0, 0, "", fmt.Errorf("failed to marshal record: %v", err) } plaintextSize := len(jsonData) encrypted, err := converter.ToData(record) if err != nil { return 0, 0, "", fmt.Errorf("failed to encrypt record: %v", err) } ciphertextSize := len(encrypted) preview := hex.EncodeToString(encrypted) if len(preview) > 80 { preview = preview[:80] + "..." } return plaintextSize, ciphertextSize, preview, nil } // EncryptionDataConverterWorkflow demonstrates encrypting sensitive workflow data. // The DataConverter automatically encrypts all workflow inputs, outputs, and activity // parameters before they are stored in Cadence history. Without the key, the data // is unreadable even to Cadence operators viewing the workflow history. // // Note: The workflow generates its own payload internally so it can be started from // the Cadence CLI without requiring the CLI to use the custom DataConverter. func EncryptionDataConverterWorkflow(ctx workflow.Context) (SensitiveCustomerRecord, error) { logger := workflow.GetLogger(ctx) record := CreateSensitiveCustomerRecord() logger.Info("Encryption workflow started", zap.String("customer_id", record.CustomerID)) logger.Info("All customer PII/PHI will be encrypted before storage in Cadence history") activityOptions := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, } ctx = workflow.WithActivityOptions(ctx, activityOptions) var result SensitiveCustomerRecord err := workflow.ExecuteActivity(ctx, EncryptionDataConverterActivity, record).Get(ctx, &result) if err != nil { logger.Error("Encryption workflow activity failed", zap.Error(err)) return SensitiveCustomerRecord{}, err } logger.Info("Encryption workflow completed", zap.String("customer_id", result.CustomerID)) logger.Info("Note: All PII/PHI was automatically encrypted/decrypted using AES-256-GCM") return result, nil } // EncryptionDataConverterActivity processes the sensitive customer record. // In production this might perform claims processing, fraud checks, etc. func EncryptionDataConverterActivity(ctx context.Context, record SensitiveCustomerRecord) (SensitiveCustomerRecord, error) { logger := activity.GetLogger(ctx) logger.Info("Encryption activity received record", zap.String("customer_id", record.CustomerID)) record.ProcessedBy = record.ProcessedBy + " (Encrypted)" logger.Info("Encryption activity completed", zap.String("customer_id", record.CustomerID)) return record, nil } ================================================ FILE: new_samples/data/encrypted_dataconverter_workflow_test.go ================================================ package main import ( "testing" "github.com/stretchr/testify/require" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" "go.uber.org/cadence/worker" ) func Test_EncryptionDataConverterWorkflow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestWorkflowEnvironment() env.RegisterWorkflow(EncryptionDataConverterWorkflow) env.RegisterActivity(EncryptionDataConverterActivity) dataConverter, err := NewEncryptedJSONDataConverter(demoEncryptionKey) require.NoError(t, err) workerOptions := worker.Options{ DataConverter: dataConverter, } env.SetWorkerOptions(workerOptions) var activityResult SensitiveCustomerRecord env.SetOnActivityCompletedListener(func(activityInfo *activity.Info, result encoded.Value, err error) { result.Get(&activityResult) }) // Workflow generates its own payload internally, no input needed env.ExecuteWorkflow(EncryptionDataConverterWorkflow) require.True(t, env.IsWorkflowCompleted()) require.NoError(t, env.GetWorkflowError()) require.Equal(t, "cust_8a7f3b2e", activityResult.CustomerID) require.Equal(t, "workflow-processor-v2 (Encrypted)", activityResult.ProcessedBy) } func Test_EncryptionRoundTrip(t *testing.T) { converter, err := NewEncryptedJSONDataConverter(demoEncryptionKey) require.NoError(t, err) original := CreateSensitiveCustomerRecord() encrypted, err := converter.ToData(original) require.NoError(t, err) require.NotEmpty(t, encrypted) var decoded SensitiveCustomerRecord err = converter.FromData(encrypted, &decoded) require.NoError(t, err) require.Equal(t, original.SSN, decoded.SSN) require.Equal(t, original.CreditCard, decoded.CreditCard) require.Equal(t, original.MedicalNotes, decoded.MedicalNotes) } func Test_EncryptionDifferentEachTime(t *testing.T) { converter, err := NewEncryptedJSONDataConverter(demoEncryptionKey) require.NoError(t, err) record := CreateSensitiveCustomerRecord() enc1, err := converter.ToData(record) require.NoError(t, err) enc2, err := converter.ToData(record) require.NoError(t, err) // Each encryption produces a different ciphertext due to random nonce require.NotEqual(t, enc1, enc2) } func Test_NewEncryptedJSONDataConverter_InvalidKey(t *testing.T) { _, err := NewEncryptedJSONDataConverter([]byte("too-short")) require.Error(t, err) require.ErrorIs(t, err, errFailedToCreateConverter) } ================================================ FILE: new_samples/data/generator/README.md ================================================ # Sample Generator This folder is NOT part of the actual sample. It exists only for contributors who work on this sample. Please disregard it if you are trying to learn about Cadence. To create a better learning experience for Cadence users, each sample folder is designed to be self contained. Users can view every part of writing and running workflows, including: * Cadence client initialization * Worker with workflow and activity registrations * Workflow starter * and the workflow code itself Some samples may have more or fewer parts depending on what they need to demonstrate. In most cases, the workflow code (e.g. `workflow.go`) is the part that users care about. The rest is boilerplate needed to run that workflow. For each sample folder, the workflow code should be written by hand. The boilerplate can be generated. Keeping all parts inside one folder gives early learners more value because they can see everything together rather than jumping across directories. ## Contributing * When creating a new sample, follow the steps mentioned in the README file in the main samples folder. * To update the sample workflow code, edit the workflow file directly. * To update the worker, client, or other boilerplate logic, edit the generator file. If your change applies to all samples, update the common generator file inside the `template` folder. Edit the generator file in this folder only when the change should affect this sample alone. * When you are done run the following command in the generator folder ```bash go run . ``` ================================================ FILE: new_samples/data/generator/README_specific.md ================================================ ## Data Converter Samples This folder demonstrates three production-ready patterns for custom `DataConverter` implementations in Cadence. A `DataConverter` controls how every workflow input, output, and activity parameter is serialized before it is written to Cadence history — making it the right place to add compression, encryption, or external offloading without changing any workflow or activity code. ### What is a DataConverter? A `DataConverter` implements two methods: - `ToData(value ...interface{}) ([]byte, error)` — called before data is written to Cadence history - `FromData(input []byte, valuePtr ...interface{}) error` — called when data is read back The same `DataConverter` must be used by **both the worker and any client that triggers or queries the workflow**. In this sample the workflows generate their own payloads internally, so they can be started from the Cadence CLI without bundling a custom converter into the CLI itself. Each sample runs its own worker on its own task list so it can use its own `DataConverter`. Start all three with a single `go run .`. --- ### Compression Sample `CompressionDataConverterWorkflow` demonstrates gzip-over-JSON compression. For repetitive JSON data this typically achieves 60–80% size reduction, lowering storage costs and bandwidth for large workflow payloads. **Task list:** `cadence-samples-data-compression` ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.CompressionDataConverterWorkflow \ --tl cadence-samples-data-compression \ --et 60 ``` When the worker starts it prints a compression statistics banner showing the before/after sizes of the sample payload so you can see the benefit immediately. --- ### Encryption Sample `EncryptionDataConverterWorkflow` demonstrates AES-256-GCM encryption. Every workflow input, output, and activity parameter is encrypted before being written to Cadence history. Without the key, the data stored by the Cadence server — including any operators browsing workflow history — is completely opaque. The sample uses a `SensitiveCustomerRecord` containing realistic PII and PHI fields (name, email, SSN, credit card, medical notes) to make the use case concrete. **Task list:** `cadence-samples-data-encryption` ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.EncryptionDataConverterWorkflow \ --tl cadence-samples-data-encryption \ --et 60 ``` #### Encryption key By default, the worker uses a hardcoded demo key and prints a prominent warning. To use your own key: ```bash # Generate a random 32-byte (256-bit) key export CADENCE_ENCRYPTION_KEY=$(openssl rand -hex 32) go run . ``` > **WARNING:** The hardcoded demo key (`cadence-demo-key-NOT-FOR-PROD!!!`) is public. > Never use it in production. In production, load your key from a secrets manager > (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, etc.). #### How AES-256-GCM works - `ToData`: JSON-encode arguments → generate a 12-byte random nonce → `cipher.AEAD.Seal` → return `nonce || ciphertext+tag`. - `FromData`: split nonce from input → `cipher.AEAD.Open` → JSON-decode. The GCM authentication tag (16 bytes) ensures ciphertext tampering is detected. The random nonce means the same plaintext produces different ciphertext on every call, preventing replay detection by an attacker who observes Cadence history. --- ### S3 Offload Sample (Claim-Check Pattern) `S3OffloadDataConverterWorkflow` demonstrates the *claim-check* pattern: payloads larger than a configurable threshold are stored in an external `BlobStore` and only a small reference (a few dozen bytes) travels through Cadence workflow history. This solves the practical problem of Cadence's per-payload size limits (~2 MB) for workflows that must pass very large datasets between the workflow and its activities. **Task list:** `cadence-samples-data-s3` ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.S3OffloadDataConverterWorkflow \ --tl cadence-samples-data-s3 \ --et 60 ``` #### How it works - `ToData`: JSON-encode → if `len(json) > thresholdBytes`, upload to `BlobStore` under a SHA-256 key and return `0x01 || {"__s3_ref":"/"}`. Otherwise return `0x00 || json` inline. - `FromData`: read prefix byte → if `0x01`, fetch from `BlobStore` and decode; if `0x00`, decode inline. #### Default store (zero-config) Out of the box, `localFSBlobStore` writes blobs to `os.TempDir()/cadence-samples-data-s3/`. No cloud credentials or additional dependencies are needed. #### Swapping in real AWS S3 The file `s3_dataconverter_workflow.go` contains a commented `s3BlobStore` stub showing the exact AWS SDK calls needed. To enable it: 1. Add the AWS SDK to your module: ```bash go get github.com/aws/aws-sdk-go-v2/config go get github.com/aws/aws-sdk-go-v2/service/s3 ``` 2. Uncomment the `s3BlobStore` section in `s3_dataconverter_workflow.go`. 3. Replace `NewLocalFSBlobStore()` with `NewS3BlobStore(bucket, region)` in `worker.go`. 4. Set standard AWS environment variables (`AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) or use an IAM instance role. You can also point the SDK at [LocalStack](https://localstack.cloud/) or [MinIO](https://min.io/) for local testing without a real AWS account. > **Note on cleanup:** The `s3OffloadDataConverter` does not delete blobs after the workflow completes. In production, use S3 object lifecycle policies to automatically expire old blobs. --- ### When to use which pattern | Pattern | Best for | |---------|----------| | **Compression** | Large repetitive JSON payloads; reducing storage cost without confidentiality requirements | | **Encryption** | PII, PHI, secrets, or any data that must be unreadable in Cadence history | | **S3 Offload** | Payloads approaching Cadence's size limits; binary or non-JSON data; cost-conscious archival | Patterns can be composed: encrypt-then-compress, or encrypt-then-offload to S3 for maximum security and minimum history size. ================================================ FILE: new_samples/data/generator/generate.go ================================================ package main import "github.com/uber-common/cadence-samples/new_samples/template" func main() { // Define the data for Data samples. // NOTE: worker.go is hand-written (not generated) because each sample // requires its own DataConverter in worker options. We call the individual // generation functions explicitly instead of template.GenerateAll so the // hand-written worker.go is never clobbered. data := template.TemplateData{ SampleName: "Data", Workflows: []string{ "CompressionDataConverterWorkflow", "EncryptionDataConverterWorkflow", "S3OffloadDataConverterWorkflow", }, Activities: []string{ "CompressionDataConverterActivity", "EncryptionDataConverterActivity", "S3OffloadDataConverterActivity", }, } // Explicitly skip GenerateWorker — worker.go is maintained by hand. template.GenerateMain(data) template.GenerateSampleReadMe(data) template.GenerateGeneratorReadMe(data) } ================================================ FILE: new_samples/data/main.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT package main import ( "fmt" "os" "os/signal" "syscall" ) func main() { StartWorker() done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT) fmt.Println("Cadence worker started, press ctrl+c to terminate...") <-done } ================================================ FILE: new_samples/data/s3_dataconverter_workflow.go ================================================ package main import ( "bytes" "context" "crypto/sha256" "encoding/json" "fmt" "os" "path/filepath" "reflect" "strings" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) // BlobStore is an abstraction over any external object store (local filesystem, S3, GCS, etc.). // The s3OffloadDataConverter uses this interface to store large payloads outside Cadence history. type BlobStore interface { Put(ctx context.Context, key string, data []byte) error Get(ctx context.Context, key string) ([]byte, error) } // localFSBlobStore implements BlobStore using the local filesystem. // It is the default zero-config implementation used when running this demo without real AWS. // Files are written under os.TempDir()/cadence-samples-data-s3/. type localFSBlobStore struct { baseDir string } // NewLocalFSBlobStore creates a local filesystem blob store under os.TempDir(). func NewLocalFSBlobStore() BlobStore { baseDir := filepath.Join(os.TempDir(), "cadence-samples-data-s3") if err := os.MkdirAll(baseDir, 0o755); err != nil { panic(fmt.Sprintf("failed to create blob store dir %s: %v", baseDir, err)) } return &localFSBlobStore{baseDir: baseDir} } // sanitizeKey turns a "bucket/sha256hex" key into a single safe filename. Keys are always // generated internally by the DataConverter, but filepath.Base provides a belt-and-suspenders // guarantee against directory traversal in case a future caller passes a user-controlled key. func sanitizeKey(key string) string { return filepath.Base(strings.ReplaceAll(key, "/", "_")) } func (s *localFSBlobStore) Put(_ context.Context, key string, data []byte) error { path := filepath.Join(s.baseDir, sanitizeKey(key)) return os.WriteFile(path, data, 0o644) } func (s *localFSBlobStore) Get(_ context.Context, key string) ([]byte, error) { path := filepath.Join(s.baseDir, sanitizeKey(key)) return os.ReadFile(path) } // ============================================================================= // S3 BlobStore stub // // To use a real AWS S3 bucket instead of the local filesystem: // 1. Add aws-sdk-go-v2 to go.mod: // go get github.com/aws/aws-sdk-go-v2/config // go get github.com/aws/aws-sdk-go-v2/service/s3 // 2. Uncomment and compile the s3BlobStore implementation below. // 3. Replace NewLocalFSBlobStore() with NewS3BlobStore(bucket, region) in worker.go. // 4. Set AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY (or use an instance role). // // /* // import ( // "github.com/aws/aws-sdk-go-v2/aws" // awsconfig "github.com/aws/aws-sdk-go-v2/config" // "github.com/aws/aws-sdk-go-v2/service/s3" // ) // // type s3BlobStore struct { // client *s3.Client // bucket string // } // // func NewS3BlobStore(bucket, region string) BlobStore { // cfg, err := awsconfig.LoadDefaultConfig(context.Background(), awsconfig.WithRegion(region)) // if err != nil { // panic("failed to load AWS config: " + err.Error()) // } // return &s3BlobStore{client: s3.NewFromConfig(cfg), bucket: bucket} // } // // func (s *s3BlobStore) Put(ctx context.Context, key string, data []byte) error { // _, err := s.client.PutObject(ctx, &s3.PutObjectInput{ // Bucket: aws.String(s.bucket), // Key: aws.String(key), // Body: bytes.NewReader(data), // }) // return err // } // // func (s *s3BlobStore) Get(ctx context.Context, key string) ([]byte, error) { // out, err := s.client.GetObject(ctx, &s3.GetObjectInput{ // Bucket: aws.String(s.bucket), // Key: aws.String(key), // }) // if err != nil { // return nil, err // } // defer out.Body.Close() // return io.ReadAll(out.Body) // } // */ // ============================================================================= // s3Envelope is the small reference stored in Cadence history when a payload is offloaded. type s3Envelope struct { S3Ref string `json:"__s3_ref"` } const ( // inlinePrefix is prepended to inline (below-threshold) payloads so FromData can distinguish them. inlinePrefix = byte(0x00) // offloadPrefix is prepended to offloaded payloads. offloadPrefix = byte(0x01) // defaultThresholdBytes: payloads larger than this are offloaded to the BlobStore. // Cadence's default max payload size is ~2MB; this threshold is set intentionally low // so the demo workflow comfortably triggers offloading. defaultThresholdBytes = 4096 // 4 KB ) // s3OffloadDataConverter implements the claim-check pattern: // large payloads are stored in BlobStore; only a small reference travels through Cadence history. type s3OffloadDataConverter struct { store BlobStore bucket string thresholdBytes int } // NewS3OffloadDataConverter creates a new s3OffloadDataConverter. // store is the BlobStore backend (use NewLocalFSBlobStore() for zero-config demo). // bucket is a logical bucket/prefix name embedded in the reference key. // thresholdBytes is the max inline payload size; larger payloads are offloaded. func NewS3OffloadDataConverter(store BlobStore, bucket string, thresholdBytes int) encoded.DataConverter { return &s3OffloadDataConverter{ store: store, bucket: bucket, thresholdBytes: thresholdBytes, } } func (dc *s3OffloadDataConverter) ToData(value ...interface{}) ([]byte, error) { var jsonBuf bytes.Buffer enc := json.NewEncoder(&jsonBuf) for i, obj := range value { if err := enc.Encode(obj); err != nil { return nil, fmt.Errorf("unable to encode argument: %d, %v, with error: %v", i, reflect.TypeOf(obj), err) } } jsonBytes := jsonBuf.Bytes() if len(jsonBytes) <= dc.thresholdBytes { // Small payload: store inline with a prefix marker result := make([]byte, 1+len(jsonBytes)) result[0] = inlinePrefix copy(result[1:], jsonBytes) return result, nil } // Derive the key from the SHA-256 of the payload so ToData is idempotent across // Cadence workflow replays. Using uuid.New() here would write a new orphaned blob // on every replay because the SDK calls ToData again each time the workflow is // re-executed from the top. If the workflow needs to control the key (e.g. to // encode routing metadata), generate it with workflow.SideEffect and pass it // alongside the payload instead. hash := sha256.Sum256(jsonBytes) key := fmt.Sprintf("%s/%x", dc.bucket, hash) if err := dc.store.Put(context.Background(), key, jsonBytes); err != nil { return nil, fmt.Errorf("failed to offload payload to blob store (key=%s): %v", key, err) } envelope, err := json.Marshal(s3Envelope{S3Ref: key}) if err != nil { return nil, fmt.Errorf("failed to marshal s3 envelope: %v", err) } result := make([]byte, 1+len(envelope)) result[0] = offloadPrefix copy(result[1:], envelope) return result, nil } func (dc *s3OffloadDataConverter) FromData(input []byte, valuePtr ...interface{}) error { // Empty input: workflow was started without arguments (e.g., from CLI without --input). if len(input) == 0 { return nil } prefix, payload := input[0], input[1:] var jsonData []byte switch prefix { case inlinePrefix: // Empty payload means zero arguments were encoded (e.g., no workflow input). if len(payload) == 0 { return nil } jsonData = payload case offloadPrefix: var envelope s3Envelope if err := json.Unmarshal(payload, &envelope); err != nil { return fmt.Errorf("s3 offload: failed to unmarshal envelope: %v", err) } fetched, err := dc.store.Get(context.Background(), envelope.S3Ref) if err != nil { return fmt.Errorf("s3 offload: failed to fetch payload from blob store (key=%s): %v", envelope.S3Ref, err) } jsonData = fetched default: return fmt.Errorf("s3 offload: unknown prefix byte 0x%02x", prefix) } dec := json.NewDecoder(bytes.NewBuffer(jsonData)) for i, obj := range valuePtr { if err := dec.Decode(obj); err != nil { return fmt.Errorf("unable to decode argument: %d, %v, with error: %v", i, reflect.TypeOf(obj), err) } } return nil } // S3LargePayload is a sizable data structure used to demonstrate S3 offloading. // It is intentionally larger than defaultThresholdBytes so every workflow execution // triggers an offload to the BlobStore. type S3LargePayload struct { JobID string `json:"job_id"` Description string `json:"description"` DataPoints []S3DataPoint `json:"data_points"` Metadata map[string]string `json:"metadata"` ProcessedBy string `json:"processed_by"` } // S3DataPoint represents a single telemetry measurement. type S3DataPoint struct { Timestamp string `json:"timestamp"` Metric string `json:"metric"` Value float64 `json:"value"` Tags string `json:"tags"` } // CreateS3LargePayload creates a sample payload well above defaultThresholdBytes. func CreateS3LargePayload() S3LargePayload { points := make([]S3DataPoint, 200) for i := range points { points[i] = S3DataPoint{ Timestamp: fmt.Sprintf("2024-01-15T%02d:30:00Z", i%24), Metric: fmt.Sprintf("telemetry.sensor_%03d.temperature", i), Value: 20.0 + float64(i%30)/10.0, Tags: fmt.Sprintf("region=us-east-1,host=node-%03d,env=production", i%10), } } meta := make(map[string]string) for i := 0; i < 20; i++ { meta[fmt.Sprintf("batch_key_%02d", i)] = strings.Repeat("value-data-", 5) } return S3LargePayload{ JobID: "batch-job-20240115-001", Description: strings.Repeat("Large telemetry batch job containing sensor readings from the production cluster. ", 10), DataPoints: points, Metadata: meta, ProcessedBy: "s3-offload-worker-v1", } } // GetS3OffloadSizeInfo returns the JSON size, what is stored externally, and what travels through Cadence. func GetS3OffloadSizeInfo(payload S3LargePayload, thresholdBytes int) (int, int, error) { jsonData, err := json.Marshal(payload) if err != nil { return 0, 0, fmt.Errorf("failed to marshal payload: %v", err) } jsonSize := len(jsonData) // The Cadence history reference is: 1 prefix byte + JSON envelope {"__s3_ref":"/"} // A SHA-256 hex digest is 64 chars; bucket + "/" + hex ≈ bucket + 65 chars sampleEnvelope, _ := json.Marshal(s3Envelope{S3Ref: "cadence-samples-data-s3/" + strings.Repeat("a", 64)}) cadenceBytes := 1 + len(sampleEnvelope) return jsonSize, cadenceBytes, nil } // S3OffloadDataConverterWorkflow demonstrates the claim-check pattern with a BlobStore. // Payloads larger than the threshold are stored externally; only a small reference is // kept in Cadence workflow history, dramatically reducing history storage requirements. // // Note: The workflow generates its own payload internally so it can be started from // the Cadence CLI without requiring the CLI to use the custom DataConverter. func S3OffloadDataConverterWorkflow(ctx workflow.Context) (S3LargePayload, error) { logger := workflow.GetLogger(ctx) payload := CreateS3LargePayload() logger.Info("S3 offload workflow started", zap.String("job_id", payload.JobID), zap.Int("data_points", len(payload.DataPoints))) logger.Info("Large payload will be offloaded to BlobStore; only a reference travels through Cadence history") activityOptions := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, } ctx = workflow.WithActivityOptions(ctx, activityOptions) var result S3LargePayload err := workflow.ExecuteActivity(ctx, S3OffloadDataConverterActivity, payload).Get(ctx, &result) if err != nil { logger.Error("S3 offload workflow activity failed", zap.Error(err)) return S3LargePayload{}, err } logger.Info("S3 offload workflow completed", zap.String("job_id", result.JobID)) logger.Info("Note: Large payload was transparently offloaded and retrieved via the BlobStore") return result, nil } // S3OffloadDataConverterActivity processes the large payload retrieved from the BlobStore. // From the activity's perspective the DataConverter is invisible — it receives the full // deserialized struct just as it would with any other DataConverter. func S3OffloadDataConverterActivity(ctx context.Context, payload S3LargePayload) (S3LargePayload, error) { logger := activity.GetLogger(ctx) logger.Info("S3 offload activity received payload", zap.String("job_id", payload.JobID), zap.Int("data_points", len(payload.DataPoints))) payload.ProcessedBy = payload.ProcessedBy + " (Processed)" logger.Info("S3 offload activity completed", zap.String("job_id", payload.JobID)) return payload, nil } ================================================ FILE: new_samples/data/s3_dataconverter_workflow_test.go ================================================ package main import ( "context" "sync" "testing" "github.com/stretchr/testify/require" "go.uber.org/cadence/activity" "go.uber.org/cadence/encoded" "go.uber.org/cadence/testsuite" "go.uber.org/cadence/worker" ) // memoryBlobStore is an in-memory BlobStore used in tests to avoid filesystem I/O. type memoryBlobStore struct { mu sync.RWMutex data map[string][]byte } func newMemoryBlobStore() BlobStore { return &memoryBlobStore{data: make(map[string][]byte)} } func (m *memoryBlobStore) Put(_ context.Context, key string, data []byte) error { m.mu.Lock() defer m.mu.Unlock() m.data[key] = append([]byte(nil), data...) return nil } func (m *memoryBlobStore) Get(_ context.Context, key string) ([]byte, error) { m.mu.RLock() defer m.mu.RUnlock() d, ok := m.data[key] if !ok { return nil, nil } return append([]byte(nil), d...), nil } func Test_S3OffloadDataConverterWorkflow(t *testing.T) { testSuite := &testsuite.WorkflowTestSuite{} env := testSuite.NewTestWorkflowEnvironment() env.RegisterWorkflow(S3OffloadDataConverterWorkflow) env.RegisterActivity(S3OffloadDataConverterActivity) store := newMemoryBlobStore() dataConverter := NewS3OffloadDataConverter(store, "test-bucket", defaultThresholdBytes) workerOptions := worker.Options{ DataConverter: dataConverter, } env.SetWorkerOptions(workerOptions) var activityResult S3LargePayload env.SetOnActivityCompletedListener(func(activityInfo *activity.Info, result encoded.Value, err error) { result.Get(&activityResult) }) // Workflow generates its own payload internally, no input needed env.ExecuteWorkflow(S3OffloadDataConverterWorkflow) require.True(t, env.IsWorkflowCompleted()) require.NoError(t, env.GetWorkflowError()) require.Equal(t, "batch-job-20240115-001", activityResult.JobID) require.Equal(t, "s3-offload-worker-v1 (Processed)", activityResult.ProcessedBy) require.Equal(t, 200, len(activityResult.DataPoints)) } func Test_S3OffloadRoundTrip(t *testing.T) { store := newMemoryBlobStore() converter := NewS3OffloadDataConverter(store, "test-bucket", defaultThresholdBytes) original := CreateS3LargePayload() encoded, err := converter.ToData(original) require.NoError(t, err) require.NotEmpty(t, encoded) // Large payload should be offloaded — the encoded form should be tiny require.Equal(t, offloadPrefix, encoded[0], "expected offload prefix for large payload") require.Less(t, len(encoded), 200, "Cadence history reference should be much smaller than full payload") var decoded S3LargePayload err = converter.FromData(encoded, &decoded) require.NoError(t, err) require.Equal(t, original.JobID, decoded.JobID) require.Equal(t, len(original.DataPoints), len(decoded.DataPoints)) } func Test_S3ReplayIdempotent(t *testing.T) { // Simulate Cadence replay: ToData is called multiple times on the same payload. // Each call must produce an identical encoded output and write to the same blob key, // not create new orphaned blobs on every replay. store := newMemoryBlobStore() converter := NewS3OffloadDataConverter(store, "test-bucket", defaultThresholdBytes) payload := CreateS3LargePayload() enc1, err := converter.ToData(payload) require.NoError(t, err) require.Equal(t, offloadPrefix, enc1[0], "expected offload prefix for large payload") enc2, err := converter.ToData(payload) require.NoError(t, err) // Both calls must return byte-for-byte identical output (same key in the envelope). require.Equal(t, enc1, enc2, "ToData must be idempotent across replays — same payload must produce same encoded bytes") // The store must contain exactly one entry, not two. mstore := store.(*memoryBlobStore) mstore.mu.RLock() blobCount := len(mstore.data) mstore.mu.RUnlock() require.Equal(t, 1, blobCount, "replayed ToData calls must overwrite the same blob key, not create new orphaned entries") } func Test_S3InlineSmallPayload(t *testing.T) { store := newMemoryBlobStore() converter := NewS3OffloadDataConverter(store, "test-bucket", 100000) // very high threshold original := CreateS3LargePayload() enc, err := converter.ToData(original) require.NoError(t, err) require.Equal(t, inlinePrefix, enc[0], "expected inline prefix when payload is under threshold") var decoded S3LargePayload err = converter.FromData(enc, &decoded) require.NoError(t, err) require.Equal(t, original.JobID, decoded.JobID) } ================================================ FILE: new_samples/data/worker.go ================================================ // Custom worker.go for Data samples - NOT generated // This sample requires custom DataConverters in worker options, one per sample. package main import ( "fmt" "os" "github.com/uber-go/tally" apiv1 "github.com/uber/cadence-idl/go/proto/api/v1" "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" "go.uber.org/cadence/activity" "go.uber.org/cadence/compatibility" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/yarpc" "go.uber.org/yarpc/peer" yarpchostport "go.uber.org/yarpc/peer/hostport" "go.uber.org/yarpc/transport/grpc" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( HostPort = "127.0.0.1:7833" Domain = "cadence-samples" ClientName = "cadence-samples-worker" CadenceService = "cadence-frontend" // Each sample uses its own task list so it can have its own DataConverter. TaskListCompression = "cadence-samples-data-compression" TaskListEncryption = "cadence-samples-data-encryption" TaskListS3 = "cadence-samples-data-s3" ) // StartWorker starts one worker per DataConverter sample and prints startup stats for each. func StartWorker() { logger := BuildLogger() cadenceClient := BuildCadenceClient() startCompressionWorker(logger, cadenceClient) startEncryptionWorker(logger, cadenceClient) startS3OffloadWorker(logger, cadenceClient) printCompressionStats() printEncryptionStats() printS3OffloadStats() } func startCompressionWorker(logger *zap.Logger, cadenceClient workflowserviceclient.Interface) { dataConverter := NewCompressedJSONDataConverter() workerOptions := worker.Options{ Logger: logger, MetricsScope: tally.NewTestScope(TaskListCompression, nil), DataConverter: dataConverter, } w := worker.New(cadenceClient, Domain, TaskListCompression, workerOptions) w.RegisterWorkflowWithOptions(CompressionDataConverterWorkflow, workflow.RegisterOptions{Name: "cadence_samples.CompressionDataConverterWorkflow"}) w.RegisterActivityWithOptions(CompressionDataConverterActivity, activity.RegisterOptions{Name: "cadence_samples.CompressionDataConverterActivity"}) if err := w.Start(); err != nil { panic("Failed to start compression worker: " + err.Error()) } logger.Info("Started compression worker", zap.String("task_list", TaskListCompression)) } func startEncryptionWorker(logger *zap.Logger, cadenceClient workflowserviceclient.Interface) { key := LoadEncryptionKey() dataConverter, err := NewEncryptedJSONDataConverter(key) if err != nil { panic("Failed to create encryption data converter: " + err.Error()) } workerOptions := worker.Options{ Logger: logger, MetricsScope: tally.NewTestScope(TaskListEncryption, nil), DataConverter: dataConverter, } w := worker.New(cadenceClient, Domain, TaskListEncryption, workerOptions) w.RegisterWorkflowWithOptions(EncryptionDataConverterWorkflow, workflow.RegisterOptions{Name: "cadence_samples.EncryptionDataConverterWorkflow"}) w.RegisterActivityWithOptions(EncryptionDataConverterActivity, activity.RegisterOptions{Name: "cadence_samples.EncryptionDataConverterActivity"}) if err := w.Start(); err != nil { panic("Failed to start encryption worker: " + err.Error()) } logger.Info("Started encryption worker", zap.String("task_list", TaskListEncryption)) } func startS3OffloadWorker(logger *zap.Logger, cadenceClient workflowserviceclient.Interface) { store := NewLocalFSBlobStore() dataConverter := NewS3OffloadDataConverter(store, "cadence-samples-data-s3", defaultThresholdBytes) workerOptions := worker.Options{ Logger: logger, MetricsScope: tally.NewTestScope(TaskListS3, nil), DataConverter: dataConverter, } w := worker.New(cadenceClient, Domain, TaskListS3, workerOptions) w.RegisterWorkflowWithOptions(S3OffloadDataConverterWorkflow, workflow.RegisterOptions{Name: "cadence_samples.S3OffloadDataConverterWorkflow"}) w.RegisterActivityWithOptions(S3OffloadDataConverterActivity, activity.RegisterOptions{Name: "cadence_samples.S3OffloadDataConverterActivity"}) if err := w.Start(); err != nil { panic("Failed to start S3 offload worker: " + err.Error()) } logger.Info("Started S3 offload worker", zap.String("task_list", TaskListS3)) } // printCompressionStats displays gzip compression statistics for the sample payload. func printCompressionStats() { largePayload := CreateLargePayload() originalSize, compressedSize, compressionPercentage, err := GetPayloadSizeInfo(largePayload, NewCompressedJSONDataConverter()) if err != nil { fmt.Printf("Error calculating compression stats: %v\n", err) return } fmt.Printf("\n=== Compression Sample Statistics ===\n") fmt.Printf("Original JSON size: %d bytes (%.2f KB)\n", originalSize, float64(originalSize)/1024.0) fmt.Printf("Compressed size: %d bytes (%.2f KB)\n", compressedSize, float64(compressedSize)/1024.0) fmt.Printf("Compression ratio: %.2f%% reduction\n", compressionPercentage) fmt.Printf("Space saved: %d bytes (%.2f KB)\n", originalSize-compressedSize, float64(originalSize-compressedSize)/1024.0) fmt.Printf("Start workflow: cadence --domain %s workflow start --tl %s --workflow_type cadence_samples.CompressionDataConverterWorkflow --et 60\n", Domain, TaskListCompression) fmt.Printf("=====================================\n\n") } // printEncryptionStats displays AES-256-GCM encryption statistics for the sample record. func printEncryptionStats() { record := CreateSensitiveCustomerRecord() converter, err := NewEncryptedJSONDataConverter(demoEncryptionKey) if err != nil { fmt.Printf("Error creating encryption converter for stats: %v\n", err) return } plaintextSize, ciphertextSize, preview, err := GetEncryptionSizeInfo(record, converter) if err != nil { fmt.Printf("Error calculating encryption stats: %v\n", err) return } fmt.Printf("\n=== Encryption Sample Statistics ===\n") fmt.Printf("Plaintext JSON size: %d bytes\n", plaintextSize) fmt.Printf("Ciphertext size: %d bytes (overhead: %d bytes nonce+tag)\n", ciphertextSize, ciphertextSize-plaintextSize) fmt.Printf("Ciphertext preview: %s\n", preview) fmt.Printf("Start workflow: cadence --domain %s workflow start --tl %s --workflow_type cadence_samples.EncryptionDataConverterWorkflow --et 60\n", Domain, TaskListEncryption) fmt.Printf("====================================\n\n") } // printS3OffloadStats displays claim-check offload statistics for the sample payload. func printS3OffloadStats() { payload := CreateS3LargePayload() jsonSize, cadenceBytes, err := GetS3OffloadSizeInfo(payload, defaultThresholdBytes) if err != nil { fmt.Printf("Error calculating S3 offload stats: %v\n", err) return } fmt.Printf("\n=== S3 Offload Sample Statistics ===\n") fmt.Printf("Full payload JSON size: %d bytes (%.2f KB)\n", jsonSize, float64(jsonSize)/1024.0) fmt.Printf("Stored in BlobStore: %d bytes (%.2f KB)\n", jsonSize, float64(jsonSize)/1024.0) fmt.Printf("Stored in Cadence history: %d bytes (claim-check reference only)\n", cadenceBytes) fmt.Printf("Reduction in Cadence: %.1f%%\n", 100.0*(1.0-float64(cadenceBytes)/float64(jsonSize))) fmt.Printf("BlobStore location: %s/cadence-samples-data-s3/\n", os.TempDir()) fmt.Printf("Start workflow: cadence --domain %s workflow start --tl %s --workflow_type cadence_samples.S3OffloadDataConverterWorkflow --et 60\n", Domain, TaskListS3) fmt.Printf("=====================================\n\n") } func BuildCadenceClient(dialOptions ...grpc.DialOption) workflowserviceclient.Interface { grpcTransport := grpc.NewTransport() myChooser := peer.NewSingle( yarpchostport.Identify(HostPort), grpcTransport.NewDialer(dialOptions...), ) outbound := grpcTransport.NewOutbound(myChooser) dispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: ClientName, Outbounds: yarpc.Outbounds{ CadenceService: {Unary: outbound}, }, }) if err := dispatcher.Start(); err != nil { panic("Failed to start dispatcher: " + err.Error()) } clientConfig := dispatcher.ClientConfig(CadenceService) return compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ) } func BuildLogger() *zap.Logger { config := zap.NewDevelopmentConfig() config.Level.SetLevel(zapcore.InfoLevel) var err error logger, err := config.Build() if err != nil { panic("Failed to setup logger: " + err.Error()) } return logger } ================================================ FILE: new_samples/hello_world/README.md ================================================ # Hello World Sample ## Prerequisites 0. Install Cadence CLI. See instruction [here](https://cadenceworkflow.io/docs/cli/). 1. Run the Cadence server: 1. Clone the [Cadence](https://github.com/cadence-workflow/cadence) repository if you haven't done already: `git clone https://github.com/cadence-workflow/cadence.git` 2. Run `docker compose -f docker/docker-compose.yml up` to start Cadence server 3. See more details at https://github.com/uber/cadence/blob/master/README.md 2. Once everything is up and running in Docker, open [localhost:8088](localhost:8088) to view Cadence UI. 3. Register the `cadence-samples` domain: ```bash cadence --domain cadence-samples domain register ``` Refresh the [domains page](http://localhost:8088/domains) from step 2 to verify `cadence-samples` is registered. ## Steps to run sample Inside the folder this sample is defined, run the following command: ```bash go run . ``` This will call the main function in main.go which starts the worker, which will be execute the sample workflow code ### Start your workflow This workflow takes an input message and greet you as response. Try the following CLI ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.HelloWorldWorkflow \ --tl cadence-samples-worker \ --et 60 \ --input '{"message":"Cadence"}' ``` You should see output like this: ![Trigger command output](images/02-trigger-command-started-workflow.png) And the worker will log the completed workflow: ![Worker output showing workflow completed](images/01-worker-output-workflow-completed.png) Here are the details to this command: * `--domain` option describes under which domain to run this workflow * `--workflow_type` option describes which workflow to execute * `-tl` (or `--tasklist`) tells cadence-server which tasklist to schedule tasks with. This is the same tasklist the worker polls tasks from. See worker.go * `--et` (or `--execution_timeout`) tells cadence server how long to wait until timing out the workflow * `--input` is the input to your workflow To see more options run `cadence --help` ### View your workflow #### Cadence UI (cadence-web) Click on `cadence-samples` domain in cadence-web to view your workflow. ![Workflow list showing completed workflow](images/03-web-ui-workflow-list-completed.png) Click on the workflow to see details: * In Summary tab, you will see the input and output to your workflow ![Summary tab](images/04-web-ui-summary-tab.png) * Click on History tab to see individual steps. Expand an activity to see its result: ![History tab with activity result](images/05-web-ui-history-activity-result.png) * In Summary tab, you will see the input and output to your workflow * Click on History tab to see individual steps. #### CLI List workflows using the following command: ```bash cadence --domain cadence-samples workflow list ``` You can view an individual workflow by using the following command: ```bash cadence --domain cadence-samples \ workflow describe \ --wid ``` * `workflow` is the noun to run commands within workflow scope * `describe` is the verb to return the summary of the workflow * `--wid` (or `--workflow_id`) is the option to pass the workflow id. If there are multiple "run"s, it will return the latest one. * (optional) `--rid` (or `--run_id`) is the option to pass the run id to describe a specific run, instead of the latest. To view the entire history of the workflow, use the following command: ```bash cadence --domain cadence-samples \ workflow show \ --wid ``` ## Troubleshooting If you see port conflicts when starting Docker, use `lsof` to find what's using the port: ![Docker port conflict troubleshooting](images/06-docker-port-conflict-troubleshooting.png) See the main [README](../../README.md#docker-troubleshooting) for detailed Docker troubleshooting steps. ## References * The website: https://cadenceworkflow.io * Cadence's server: https://github.com/uber/cadence * Cadence's Go client: https://github.com/uber-go/cadence-client ================================================ FILE: new_samples/hello_world/generator/README.md ================================================ # Sample Generator This folder is NOT part of the actual sample. It exists only for contributors who work on this sample. Please disregard it if you are trying to learn about Cadence. To create a better learning experience for Cadence users, each sample folder is designed to be self contained. Users can view every part of writing and running workflows, including: * Cadence client initialization * Worker with workflow and activity registrations * Workflow starter * and the workflow code itself Some samples may have more or fewer parts depending on what they need to demonstrate. In most cases, the workflow code (e.g. `workflow.go`) is the part that users care about. The rest is boilerplate needed to run that workflow. For each sample folder, the workflow code should be written by hand. The boilerplate can be generated. Keeping all parts inside one folder gives early learners more value because they can see everything together rather than jumping across directories. ## Contributing * When creating a new sample, follow the steps mentioned in the README file in the main samples folder. * To update the sample workflow code, edit the workflow file directly. * To update the worker, client, or other boilerplate logic, edit the generator file. If your change applies to all samples, update the common generator file inside the `template` folder. Edit the generator file in this folder only when the change should affect this sample alone. * When you are done run the following command in the generator folder ```bash go run . ``` ================================================ FILE: new_samples/hello_world/generator/README_specific.md ================================================ ### Start your workflow This workflow takes an input message and greet you as response. Try the following CLI ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.HelloWorldWorkflow \ --tl cadence-samples-worker \ --et 60 \ --input '{"message":"Cadence"}' ``` You should see output like this: ![Trigger command output](images/02-trigger-command-started-workflow.png) And the worker will log the completed workflow: ![Worker output showing workflow completed](images/01-worker-output-workflow-completed.png) Here are the details to this command: * `--domain` option describes under which domain to run this workflow * `--workflow_type` option describes which workflow to execute * `-tl` (or `--tasklist`) tells cadence-server which tasklist to schedule tasks with. This is the same tasklist the worker polls tasks from. See worker.go * `--et` (or `--execution_timeout`) tells cadence server how long to wait until timing out the workflow * `--input` is the input to your workflow To see more options run `cadence --help` ### View your workflow #### Cadence UI (cadence-web) Click on `cadence-samples` domain in cadence-web to view your workflow. ![Workflow list showing completed workflow](images/03-web-ui-workflow-list-completed.png) Click on the workflow to see details: * In Summary tab, you will see the input and output to your workflow ![Summary tab](images/04-web-ui-summary-tab.png) * Click on History tab to see individual steps. Expand an activity to see its result: ![History tab with activity result](images/05-web-ui-history-activity-result.png) * In Summary tab, you will see the input and output to your workflow * Click on History tab to see individual steps. #### CLI List workflows using the following command: ```bash cadence --domain cadence-samples workflow list ``` You can view an individual workflow by using the following command: ```bash cadence --domain cadence-samples \ workflow describe \ --wid ``` * `workflow` is the noun to run commands within workflow scope * `describe` is the verb to return the summary of the workflow * `--wid` (or `--workflow_id`) is the option to pass the workflow id. If there are multiple "run"s, it will return the latest one. * (optional) `--rid` (or `--run_id`) is the option to pass the run id to describe a specific run, instead of the latest. To view the entire history of the workflow, use the following command: ```bash cadence --domain cadence-samples \ workflow show \ --wid ``` ## Troubleshooting If you see port conflicts when starting Docker, use `lsof` to find what's using the port: ![Docker port conflict troubleshooting](images/06-docker-port-conflict-troubleshooting.png) See the main [README](../../README.md#docker-troubleshooting) for detailed Docker troubleshooting steps. ================================================ FILE: new_samples/hello_world/generator/generate.go ================================================ package main import "github.com/uber-common/cadence-samples/new_samples/template" func main() { // Define the data for HelloWorld data := template.TemplateData{ SampleName: "Hello World", Workflows: []string{"HelloWorldWorkflow"}, Activities: []string{"HelloWorldActivity"}, } template.GenerateAll(data) } // Implement custom generator below ================================================ FILE: new_samples/hello_world/main.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT package main import ( "fmt" "os" "os/signal" "syscall" ) func main() { StartWorker() done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT) fmt.Println("Cadence worker started, press ctrl+c to terminate...") <-done } ================================================ FILE: new_samples/hello_world/worker.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT // Package worker implements a Cadence worker with basic configurations. package main import ( "github.com/uber-go/tally" apiv1 "github.com/uber/cadence-idl/go/proto/api/v1" "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" "go.uber.org/cadence/activity" "go.uber.org/cadence/compatibility" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/yarpc" "go.uber.org/yarpc/peer" yarpchostport "go.uber.org/yarpc/peer/hostport" "go.uber.org/yarpc/transport/grpc" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( HostPort = "127.0.0.1:7833" Domain = "cadence-samples" // TaskListName identifies set of client workflows, activities, and workers. // It could be your group or client or application name. TaskListName = "cadence-samples-worker" ClientName = "cadence-samples-worker" CadenceService = "cadence-frontend" ) // StartWorker creates and starts a basic Cadence worker. func StartWorker() { logger, cadenceClient := BuildLogger(), BuildCadenceClient() workerOptions := worker.Options{ Logger: logger, MetricsScope: tally.NewTestScope(TaskListName, nil), } w := worker.New( cadenceClient, Domain, TaskListName, workerOptions) // HelloWorld workflow registration w.RegisterWorkflowWithOptions(HelloWorldWorkflow, workflow.RegisterOptions{Name: "cadence_samples.HelloWorldWorkflow"}) w.RegisterActivityWithOptions(HelloWorldActivity, activity.RegisterOptions{Name: "cadence_samples.HelloWorldActivity"}) err := w.Start() if err != nil { panic("Failed to start worker: " + err.Error()) } logger.Info("Started Worker.", zap.String("worker", TaskListName)) } func BuildCadenceClient(dialOptions ...grpc.DialOption) workflowserviceclient.Interface { grpcTransport := grpc.NewTransport() // Create a single peer chooser that identifies the host/port and configures // a gRPC dialer with TLS credentials myChooser := peer.NewSingle( yarpchostport.Identify(HostPort), grpcTransport.NewDialer(dialOptions...), ) outbound := grpcTransport.NewOutbound(myChooser) dispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: ClientName, Outbounds: yarpc.Outbounds{ CadenceService: {Unary: outbound}, }, }) if err := dispatcher.Start(); err != nil { panic("Failed to start dispatcher: " + err.Error()) } clientConfig := dispatcher.ClientConfig(CadenceService) // Create a compatibility adapter that wraps proto-based YARPC clients // to provide a unified interface for domain, workflow, worker, and visibility APIs return compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ) } func BuildLogger() *zap.Logger { config := zap.NewDevelopmentConfig() config.Level.SetLevel(zapcore.InfoLevel) var err error logger, err := config.Build() if err != nil { panic("Failed to setup logger: " + err.Error()) } return logger } ================================================ FILE: new_samples/hello_world/workflow.go ================================================ package main import ( "context" "fmt" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" "time" ) type sampleInput struct { Message string `json:"message"` } // This is the workflow function // Given an input, HelloWorldWorkflow returns "Hello !" func HelloWorldWorkflow(ctx workflow.Context, input sampleInput) (string, error) { ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) logger.Info("HelloWorldWorkflow started") var greetingMsg string err := workflow.ExecuteActivity(ctx, HelloWorldActivity, input).Get(ctx, &greetingMsg) if err != nil { logger.Error("HelloWorldActivity failed", zap.Error(err)) return "", err } logger.Info("Workflow result", zap.String("greeting", greetingMsg)) return greetingMsg, nil } // This is the activity function // Given an input, HelloWorldActivity returns "Hello !" func HelloWorldActivity(ctx context.Context, input sampleInput) (string, error) { logger := activity.GetLogger(ctx) logger.Info("HelloWorldActivity started") return fmt.Sprintf("Hello, %s!", input.Message), nil } ================================================ FILE: new_samples/operations/README.md ================================================ # Operations Sample ## Prerequisites 0. Install Cadence CLI. See instruction [here](https://cadenceworkflow.io/docs/cli/). 1. Run the Cadence server: 1. Clone the [Cadence](https://github.com/cadence-workflow/cadence) repository if you haven't done already: `git clone https://github.com/cadence-workflow/cadence.git` 2. Run `docker compose -f docker/docker-compose.yml up` to start Cadence server 3. See more details at https://github.com/uber/cadence/blob/master/README.md 2. Once everything is up and running in Docker, open [localhost:8088](localhost:8088) to view Cadence UI. 3. Register the `cadence-samples` domain: ```bash cadence --domain cadence-samples domain register ``` Refresh the [domains page](http://localhost:8088/domains) from step 2 to verify `cadence-samples` is registered. ## Steps to run sample Inside the folder this sample is defined, run the following command: ```bash go run . ``` This will call the main function in main.go which starts the worker, which will be execute the sample workflow code ## Samples in this folder This folder contains samples demonstrating workflow operations and lifecycle management in Cadence. ### Cancel Workflow The `CancelWorkflow` demonstrates how to properly handle workflow cancellation, including: - Graceful cleanup when a workflow is cancelled - Using a disconnected context to run cleanup activities after cancellation - Heartbeating in long-running activities to detect cancellation #### Start the workflow ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.CancelWorkflow \ --tl cadence-samples-worker \ --et 600 \ --input '{}' ``` Copy the workflow ID from the output. #### Cancel the workflow ```bash cadence --domain cadence-samples \ workflow cancel \ --workflow_id ``` #### What to observe After cancellation: 1. The `ActivityToBeCanceled` will detect the cancellation via `ctx.Done()` and return 2. The `ActivityToBeSkipped` will not be scheduled (context already cancelled) 3. The `CleanupActivity` will run using a disconnected context to perform cleanup This pattern is essential for workflows that need to release resources or perform cleanup operations when cancelled. ## References * The website: https://cadenceworkflow.io * Cadence's server: https://github.com/uber/cadence * Cadence's Go client: https://github.com/uber-go/cadence-client ================================================ FILE: new_samples/operations/cancel_workflow.go ================================================ package main import ( "context" "fmt" "go.uber.org/cadence" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" "time" ) func CancelWorkflow(ctx workflow.Context) (retError error) { ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute * 30, HeartbeatTimeout: time.Second * 5, WaitForCancellation: true, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) logger.Info("cancel workflow started") defer func() { if cadence.IsCanceledError(retError) { // When workflow is canceled, it has to get a new disconnected context to execute any activities newCtx, _ := workflow.NewDisconnectedContext(ctx) err := workflow.ExecuteActivity(newCtx, CleanupActivity).Get(ctx, nil) if err != nil { logger.Error("Cleanup activity failed", zap.Error(err)) retError = err return } retError = nil logger.Info("Workflow completed.") } }() var result string err := workflow.ExecuteActivity(ctx, ActivityToBeCanceled).Get(ctx, &result) if err != nil && !cadence.IsCanceledError(err) { logger.Error("Error from activityToBeCanceled", zap.Error(err)) return err } logger.Info(fmt.Sprintf("activityToBeCanceled returns %v, %v", result, err)) // Execute activity using a canceled ctx, // activity won't be scheduled and a cancelled error will be returned err = workflow.ExecuteActivity(ctx, ActivityToBeSkipped).Get(ctx, nil) if err != nil && !cadence.IsCanceledError(err) { logger.Error("Error from activityToBeSkipped", zap.Error(err)) } return err } func ActivityToBeCanceled(ctx context.Context) (string, error) { logger := activity.GetLogger(ctx) logger.Info("activity started, to cancel workflow, use CLI: 'cadence --do default wf cancel -w ' to cancel") for { select { case <-time.After(1 * time.Second): logger.Info("heart beating...") activity.RecordHeartbeat(ctx, "") case <-ctx.Done(): logger.Info("context is cancelled") // returned canceled error here so that in workflow history we can see ActivityTaskCanceled event // or if not cancelled, return timeout error return "I am canceled by Done", ctx.Err() } } } func CleanupActivity(ctx context.Context) error { logger := activity.GetLogger(ctx) logger.Info("cleanupActivity started") return nil } func ActivityToBeSkipped(ctx context.Context) error { logger := activity.GetLogger(ctx) logger.Info("this activity will be skipped due to cancellation") return nil } ================================================ FILE: new_samples/operations/generator/README.md ================================================ # Sample Generator This folder is NOT part of the actual sample. It exists only for contributors who work on this sample. Please disregard it if you are trying to learn about Cadence. To create a better learning experience for Cadence users, each sample folder is designed to be self contained. Users can view every part of writing and running workflows, including: * Cadence client initialization * Worker with workflow and activity registrations * Workflow starter * and the workflow code itself Some samples may have more or fewer parts depending on what they need to demonstrate. In most cases, the workflow code (e.g. `workflow.go`) is the part that users care about. The rest is boilerplate needed to run that workflow. For each sample folder, the workflow code should be written by hand. The boilerplate can be generated. Keeping all parts inside one folder gives early learners more value because they can see everything together rather than jumping across directories. ## Contributing * When creating a new sample, follow the steps mentioned in the README file in the main samples folder. * To update the sample workflow code, edit the workflow file directly. * To update the worker, client, or other boilerplate logic, edit the generator file. If your change applies to all samples, update the common generator file inside the `template` folder. Edit the generator file in this folder only when the change should affect this sample alone. * When you are done run the following command in the generator folder ```bash go run . ``` ================================================ FILE: new_samples/operations/generator/README_specific.md ================================================ ## Samples in this folder This folder contains samples demonstrating workflow operations and lifecycle management in Cadence. ### Cancel Workflow The `CancelWorkflow` demonstrates how to properly handle workflow cancellation, including: - Graceful cleanup when a workflow is cancelled - Using a disconnected context to run cleanup activities after cancellation - Heartbeating in long-running activities to detect cancellation #### Start the workflow ```bash cadence --domain cadence-samples \ workflow start \ --workflow_type cadence_samples.CancelWorkflow \ --tl cadence-samples-worker \ --et 600 \ --input '{}' ``` Copy the workflow ID from the output. #### Cancel the workflow ```bash cadence --domain cadence-samples \ workflow cancel \ --workflow_id ``` #### What to observe After cancellation: 1. The `ActivityToBeCanceled` will detect the cancellation via `ctx.Done()` and return 2. The `ActivityToBeSkipped` will not be scheduled (context already cancelled) 3. The `CleanupActivity` will run using a disconnected context to perform cleanup This pattern is essential for workflows that need to release resources or perform cleanup operations when cancelled. ================================================ FILE: new_samples/operations/generator/generate.go ================================================ package main import "github.com/uber-common/cadence-samples/new_samples/template" func main() { // Define the data for Operations samples data := template.TemplateData{ SampleName: "Operations", Workflows: []string{"CancelWorkflow"}, Activities: []string{"ActivityToBeCanceled", "CleanupActivity", "ActivityToBeSkipped"}, } template.GenerateAll(data) } // Implement custom generator below ================================================ FILE: new_samples/operations/main.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT package main import ( "fmt" "os" "os/signal" "syscall" ) func main() { StartWorker() done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT) fmt.Println("Cadence worker started, press ctrl+c to terminate...") <-done } ================================================ FILE: new_samples/operations/worker.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT // Package worker implements a Cadence worker with basic configurations. package main import ( "github.com/uber-go/tally" apiv1 "github.com/uber/cadence-idl/go/proto/api/v1" "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" "go.uber.org/cadence/activity" "go.uber.org/cadence/compatibility" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/yarpc" "go.uber.org/yarpc/peer" yarpchostport "go.uber.org/yarpc/peer/hostport" "go.uber.org/yarpc/transport/grpc" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( HostPort = "127.0.0.1:7833" Domain = "cadence-samples" // TaskListName identifies set of client workflows, activities, and workers. // It could be your group or client or application name. TaskListName = "cadence-samples-worker" ClientName = "cadence-samples-worker" CadenceService = "cadence-frontend" ) // StartWorker creates and starts a basic Cadence worker. func StartWorker() { logger, cadenceClient := BuildLogger(), BuildCadenceClient() workerOptions := worker.Options{ Logger: logger, MetricsScope: tally.NewTestScope(TaskListName, nil), } w := worker.New( cadenceClient, Domain, TaskListName, workerOptions) // Workflow registration w.RegisterWorkflowWithOptions(CancelWorkflow, workflow.RegisterOptions{Name: "cadence_samples.CancelWorkflow"}) w.RegisterActivityWithOptions(ActivityToBeCanceled, activity.RegisterOptions{Name: "cadence_samples.ActivityToBeCanceled"}) w.RegisterActivityWithOptions(CleanupActivity, activity.RegisterOptions{Name: "cadence_samples.CleanupActivity"}) w.RegisterActivityWithOptions(ActivityToBeSkipped, activity.RegisterOptions{Name: "cadence_samples.ActivityToBeSkipped"}) err := w.Start() if err != nil { panic("Failed to start worker: " + err.Error()) } logger.Info("Started Worker.", zap.String("worker", TaskListName)) } func BuildCadenceClient(dialOptions ...grpc.DialOption) workflowserviceclient.Interface { grpcTransport := grpc.NewTransport() // Create a single peer chooser that identifies the host/port and configures // a gRPC dialer with TLS credentials myChooser := peer.NewSingle( yarpchostport.Identify(HostPort), grpcTransport.NewDialer(dialOptions...), ) outbound := grpcTransport.NewOutbound(myChooser) dispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: ClientName, Outbounds: yarpc.Outbounds{ CadenceService: {Unary: outbound}, }, }) if err := dispatcher.Start(); err != nil { panic("Failed to start dispatcher: " + err.Error()) } clientConfig := dispatcher.ClientConfig(CadenceService) // Create a compatibility adapter that wraps proto-based YARPC clients // to provide a unified interface for domain, workflow, worker, and visibility APIs return compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ) } func BuildLogger() *zap.Logger { config := zap.NewDevelopmentConfig() config.Level.SetLevel(zapcore.InfoLevel) var err error logger, err := config.Build() if err != nil { panic("Failed to setup logger: " + err.Error()) } return logger } ================================================ FILE: new_samples/query/README.md ================================================ # Query Sample ## Prerequisites 0. Install Cadence CLI. See instruction [here](https://cadenceworkflow.io/docs/cli/). 1. Run the Cadence server: 1. Clone the [Cadence](https://github.com/cadence-workflow/cadence) repository if you haven't done already: `git clone https://github.com/cadence-workflow/cadence.git` 2. Run `docker compose -f docker/docker-compose.yml up` to start Cadence server 3. See more details at https://github.com/uber/cadence/blob/master/README.md 2. Once everything is up and running in Docker, open [localhost:8088](localhost:8088) to view Cadence UI. 3. Register the `cadence-samples` domain: ```bash cadence --domain cadence-samples domain register ``` Refresh the [domains page](http://localhost:8088/domains) from step 2 to verify `cadence-samples` is registered. ## Steps to run sample Inside the folder this sample is defined, run the following command: ```bash go run . ``` This will call the main function in main.go which starts the worker, which will be execute the sample workflow code ## Query Samples This folder contains samples demonstrating how to use Cadence queries with **MarkDoc-formatted responses**. MarkDoc allows you to create interactive query responses with buttons that can signal workflows or start new workflows. ### Why This Matters for Ops Teams Many teams build custom admin panels (using Retool, React, etc.) to manage long-running workflows because: - The CLI requires manually formatting JSON payloads - The generic Web UI doesn't provide context-specific actions - Support staff need simple buttons, not JSON knowledge **MarkDoc solves this.** Your workflow query becomes your admin panel: - State-appropriate buttons that change based on workflow status - Structured payloads sent with a single click - Built-in audit trail in workflow history - Zero additional infrastructure required --- ### Markdown Query Workflow A basic example demonstrating MarkDoc query usage with signal buttons. ```bash cadence --domain cadence-samples \ workflow start \ --tl cadence-samples-worker \ --et 1000 \ --workflow_type cadence_samples.MarkdownQueryWorkflow ``` #### How to interact 1. Go to the `cadence-samples` domain in cadence-web and click on this workflow 2. Click the **"Query"** tab 3. Select **"Signal"** from the query dropdown 4. Use the rendered buttons to control the workflow --- ### Lunch Vote Workflow An interactive voting system demonstrating dynamic query responses. ```bash cadence --domain cadence-samples \ workflow start \ --tl cadence-samples-worker \ --et 600 \ --workflow_type cadence_samples.LunchVoteWorkflow ``` #### How to vote 1. Navigate to the workflow in cadence-web 2. Click the **"Query"** tab, select **"options"** 3. Click any vote button 4. Refresh the query to see updated vote counts --- ### Order Fulfillment Workflow (Admin Panel Demo) **This is the flagship sample.** It demonstrates how MarkDoc can replace custom admin panels for ops teams. ```bash cadence --domain cadence-samples \ workflow start \ --tl cadence-samples-worker \ --et 3600 \ --workflow_type cadence_samples.OrderFulfillmentWorkflow ``` #### The Scenario You're an ops team member managing e-commerce orders. Instead of building a Retool dashboard or custom React app, you use the Cadence Web query feature as your admin panel. #### Order State Machine ``` pending_payment → payment_approved → ready_to_ship → shipped → delivered ↓ ↓ ↓ cancelled refunded cancelled ``` #### How to Use 1. **Start the workflow** using the CLI command above 2. **Open Cadence Web** at `localhost:8088` 3. Navigate to `cadence-samples` domain → find your workflow 4. Click the **"Query"** tab 5. Select **"dashboard"** from the dropdown 6. **You'll see:** - Order details (customer, items, total) - Current status with visual indicator - State-appropriate action buttons - Complete action history (audit trail) #### Walking Through the Flow **Step 1: Payment Review** - Status shows "🟡 Pending Payment" - Available actions: "Approve Payment" or "Reject" (with reason options) - Click **"✓ Approve Payment"** **Step 2: Fulfillment** - Refresh query - status now shows "🟢 Payment Approved" - Available actions: "Mark Ready to Ship" or "Issue Refund" - Click **"📦 Mark Ready to Ship"** **Step 3: Shipping** - Refresh query - status shows "📦 Ready to Ship" - Available actions: Ship via UPS/FedEx/USPS, or Cancel Order - Click **"🚚 Ship via UPS"** **Step 4: Delivery** - Refresh query - status shows "🚚 Shipped" with tracking number - Available action: "Mark as Delivered" - Click **"✅ Mark as Delivered"** **Step 5: Complete** - Status shows "✅ Delivered" - No more actions available - Full audit trail visible in Action History table #### Key Features Demonstrated | Feature | What It Shows | |---------|---------------| | **State-Driven UI** | Buttons change based on order status - you can't ship before payment approval | | **Structured Payloads** | Shipping sends `{trackingNumber, carrier}`, refunds send `{amount, reason}` | | **Multiple Choice via Buttons** | Rejection reasons as separate buttons - no JSON formatting needed | | **Audit Trail** | Every action recorded with timestamp, operator, and details | | **Business Context** | Order details, items, amounts displayed alongside actions | #### The Value Proposition > **"Your workflow IS your admin panel."** Instead of: - Building a Retool dashboard - Maintaining a separate React app - Teaching ops to format JSON You get: - Interactive UI generated from workflow state - Actions that enforce valid state transitions - Automatic audit logging in workflow history - Zero additional infrastructure --- ### MarkDoc Syntax Reference MarkDoc uses special tags for interactive elements: **Signal Button:** ``` {% signal signalName="approve_payment" label="Approve" domain="cadence-samples" workflowId="your-workflow-id" runId="your-run-id" input={"key":"value"} /%} ``` **Start Workflow Button:** ``` {% start workflowType="cadence_samples.MyWorkflow" label="Start New" domain="cadence-samples" taskList="cadence-samples-worker" workflowId="new-workflow-id" timeoutSeconds=60 /%} ``` **Other Tags:** - `{% br /%}` - Line break - `{% image src="url" alt="text" /%}` - Image ## References * The website: https://cadenceworkflow.io * Cadence's server: https://github.com/uber/cadence * Cadence's Go client: https://github.com/uber-go/cadence-client ================================================ FILE: new_samples/query/generator/README.md ================================================ # Sample Generator This folder is NOT part of the actual sample. It exists only for contributors who work on this sample. Please disregard it if you are trying to learn about Cadence. To create a better learning experience for Cadence users, each sample folder is designed to be self contained. Users can view every part of writing and running workflows, including: * Cadence client initialization * Worker with workflow and activity registrations * Workflow starter * and the workflow code itself Some samples may have more or fewer parts depending on what they need to demonstrate. In most cases, the workflow code (e.g. `workflow.go`) is the part that users care about. The rest is boilerplate needed to run that workflow. For each sample folder, the workflow code should be written by hand. The boilerplate can be generated. Keeping all parts inside one folder gives early learners more value because they can see everything together rather than jumping across directories. ## Contributing * When creating a new sample, follow the steps mentioned in the README file in the main samples folder. * To update the sample workflow code, edit the workflow file directly. * To update the worker, client, or other boilerplate logic, edit the generator file. If your change applies to all samples, update the common generator file inside the `template` folder. Edit the generator file in this folder only when the change should affect this sample alone. * When you are done run the following command in the generator folder ```bash go run . ``` ================================================ FILE: new_samples/query/generator/README_specific.md ================================================ ## Query Samples This folder contains samples demonstrating how to use Cadence queries with **MarkDoc-formatted responses**. MarkDoc allows you to create interactive query responses with buttons that can signal workflows or start new workflows. ### Why This Matters for Ops Teams Many teams build custom admin panels (using Retool, React, etc.) to manage long-running workflows because: - The CLI requires manually formatting JSON payloads - The generic Web UI doesn't provide context-specific actions - Support staff need simple buttons, not JSON knowledge **MarkDoc solves this.** Your workflow query becomes your admin panel: - State-appropriate buttons that change based on workflow status - Structured payloads sent with a single click - Built-in audit trail in workflow history - Zero additional infrastructure required --- ### Markdown Query Workflow A basic example demonstrating MarkDoc query usage with signal buttons. ```bash cadence --domain cadence-samples \ workflow start \ --tl cadence-samples-worker \ --et 1000 \ --workflow_type cadence_samples.MarkdownQueryWorkflow ``` #### How to interact 1. Go to the `cadence-samples` domain in cadence-web and click on this workflow 2. Click the **"Query"** tab 3. Select **"Signal"** from the query dropdown 4. Use the rendered buttons to control the workflow --- ### Lunch Vote Workflow An interactive voting system demonstrating dynamic query responses. ```bash cadence --domain cadence-samples \ workflow start \ --tl cadence-samples-worker \ --et 600 \ --workflow_type cadence_samples.LunchVoteWorkflow ``` #### How to vote 1. Navigate to the workflow in cadence-web 2. Click the **"Query"** tab, select **"options"** 3. Click any vote button 4. Refresh the query to see updated vote counts --- ### Order Fulfillment Workflow (Admin Panel Demo) **This is the flagship sample.** It demonstrates how MarkDoc can replace custom admin panels for ops teams. ```bash cadence --domain cadence-samples \ workflow start \ --tl cadence-samples-worker \ --et 3600 \ --workflow_type cadence_samples.OrderFulfillmentWorkflow ``` #### The Scenario You're an ops team member managing e-commerce orders. Instead of building a Retool dashboard or custom React app, you use the Cadence Web query feature as your admin panel. #### Order State Machine ``` pending_payment → payment_approved → ready_to_ship → shipped → delivered ↓ ↓ ↓ cancelled refunded cancelled ``` #### How to Use 1. **Start the workflow** using the CLI command above 2. **Open Cadence Web** at `localhost:8088` 3. Navigate to `cadence-samples` domain → find your workflow 4. Click the **"Query"** tab 5. Select **"dashboard"** from the dropdown 6. **You'll see:** - Order details (customer, items, total) - Current status with visual indicator - State-appropriate action buttons - Complete action history (audit trail) #### Walking Through the Flow **Step 1: Payment Review** - Status shows "🟡 Pending Payment" - Available actions: "Approve Payment" or "Reject" (with reason options) - Click **"✓ Approve Payment"** **Step 2: Fulfillment** - Refresh query - status now shows "🟢 Payment Approved" - Available actions: "Mark Ready to Ship" or "Issue Refund" - Click **"📦 Mark Ready to Ship"** **Step 3: Shipping** - Refresh query - status shows "📦 Ready to Ship" - Available actions: Ship via UPS/FedEx/USPS, or Cancel Order - Click **"🚚 Ship via UPS"** **Step 4: Delivery** - Refresh query - status shows "🚚 Shipped" with tracking number - Available action: "Mark as Delivered" - Click **"✅ Mark as Delivered"** **Step 5: Complete** - Status shows "✅ Delivered" - No more actions available - Full audit trail visible in Action History table #### Key Features Demonstrated | Feature | What It Shows | |---------|---------------| | **State-Driven UI** | Buttons change based on order status - you can't ship before payment approval | | **Structured Payloads** | Shipping sends `{trackingNumber, carrier}`, refunds send `{amount, reason}` | | **Multiple Choice via Buttons** | Rejection reasons as separate buttons - no JSON formatting needed | | **Audit Trail** | Every action recorded with timestamp, operator, and details | | **Business Context** | Order details, items, amounts displayed alongside actions | #### The Value Proposition > **"Your workflow IS your admin panel."** Instead of: - Building a Retool dashboard - Maintaining a separate React app - Teaching ops to format JSON You get: - Interactive UI generated from workflow state - Actions that enforce valid state transitions - Automatic audit logging in workflow history - Zero additional infrastructure --- ### MarkDoc Syntax Reference MarkDoc uses special tags for interactive elements: **Signal Button:** ``` {% signal signalName="approve_payment" label="Approve" domain="cadence-samples" workflowId="your-workflow-id" runId="your-run-id" input={"key":"value"} /%} ``` **Start Workflow Button:** ``` {% start workflowType="cadence_samples.MyWorkflow" label="Start New" domain="cadence-samples" taskList="cadence-samples-worker" workflowId="new-workflow-id" timeoutSeconds=60 /%} ``` **Other Tags:** - `{% br /%}` - Line break - `{% image src="url" alt="text" /%}` - Image ================================================ FILE: new_samples/query/generator/generate.go ================================================ package main import "github.com/uber-common/cadence-samples/new_samples/template" func main() { // Define the data for Query samples data := template.TemplateData{ SampleName: "Query", Workflows: []string{"MarkdownQueryWorkflow", "LunchVoteWorkflow", "OrderFulfillmentWorkflow"}, Activities: []string{"MarkdownQueryActivity"}, } template.GenerateAll(data) } // Implement custom generator below ================================================ FILE: new_samples/query/lunch_vote_workflow.go ================================================ package main import ( "bytes" "text/template" "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) // lunchVoteFormattedResponse is the JSON shape Cadence Web expects for markdown query results (formattedData, text/markdown, data). type lunchVoteFormattedResponse struct { CadenceResponseType string `json:"cadenceResponseType"` Format string `json:"format"` Data string `json:"data"` } // LunchVoteWorkflow demonstrates using MarkDoc query responses for interactive voting. // Users can vote for lunch options via signal buttons rendered in the query response. func LunchVoteWorkflow(ctx workflow.Context) error { logger := workflow.GetLogger(ctx) logger.Info("LunchVoteWorkflow started") votes := []map[string]string{} workflow.SetQueryHandler(ctx, "options", func() (lunchVoteFormattedResponse, error) { logger := workflow.GetLogger(ctx) logger.Info("Responding to 'options' query") return makeLunchVoteResponse(ctx, 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) logger.Info("Vote received", zap.Any("vote", vote)) } }) // Voting period - reduced from 30 minutes for sample purposes err := workflow.Sleep(ctx, 10*time.Minute) if err != nil { logger.Error("Sleep failed", zap.Error(err)) return err } logger.Info("LunchVoteWorkflow completed.", zap.Any("votes", votes)) return nil } // makeLunchVoteResponse creates the MarkDoc query response for lunch voting func makeLunchVoteResponse(ctx workflow.Context, votes []map[string]string) lunchVoteFormattedResponse { type P map[string]interface{} markdownTemplate, err := template.New("").Parse(` ## Lunch Options We're voting on where to order lunch today. Select the option you want to vote for. --- ### Current Votes {{.voteTable}} ### Menu Options {{.menuTable}} --- ### Cast Your Vote {% signal signalName="lunch_order" label="Farmhouse - Red Thai Curry" domain="cadence-samples" cluster="cluster0" workflowId="{{.workflowID}}" runId="{{.runID}}" input={"location":"Farmhouse","meal":"Red Thai Curry","requests":"spicy"} /%} {% signal signalName="lunch_order" label="Ethiopian Wat" domain="cadence-samples" cluster="cluster0" workflowId="{{.workflowID}}" runId="{{.runID}}" input={"location":"Ethiopian","meal":"Wat with Injera","requests":""} /%} {% signal signalName="lunch_order" label="Ler Ros - Tofu Bahn Mi" domain="cadence-samples" cluster="cluster0" workflowId="{{.workflowID}}" runId="{{.runID}}" input={"location":"Ler Ros","meal":"Tofu Bahn Mi","requests":""} /%} {% br /%} *Vote closes when workflow times out (10 minutes)* `) if err != nil { panic("Failed to parse template: " + err.Error()) } var markdown bytes.Buffer err = markdownTemplate.Execute(&markdown, P{ "workflowID": workflow.GetInfo(ctx).WorkflowExecution.ID, "runID": workflow.GetInfo(ctx).WorkflowExecution.RunID, "voteTable": makeLunchVoteTable(votes), "menuTable": makeLunchMenu(), }) if err != nil { panic("Failed to execute template: " + err.Error()) } return lunchVoteFormattedResponse{ CadenceResponseType: "formattedData", Format: "text/markdown", Data: markdown.String(), } } // makeLunchVoteTable generates a markdown table of current votes func makeLunchVoteTable(votes []map[string]string) string { if len(votes) == 0 { return "| Location | Meal | Requests |\n|----------|------|----------|\n| *No votes yet* | | |\n" } table := "| Location | Meal | Requests |\n|----------|------|----------|\n" for _, vote := range votes { loc := vote["location"] meal := vote["meal"] requests := vote["requests"] table += "| " + loc + " | " + meal + " | " + requests + " |\n" } return table } // makeLunchMenu generates a markdown table with menu options and images func makeLunchMenu() 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**: A dish in Thai cuisine made from curry paste, coconut milk, meat, seafood, vegetables, and herbs.", }, { 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 - Tofu Bahn Mi**: A Vietnamese sandwich with a baguette filled with lemongrass tofu, vegetables, and fresh herbs.", }, { image: "https://upload.wikimedia.org/wikipedia/commons/thumb/5/54/Ethiopian_wat.jpg/960px-Ethiopian_wat.jpg", desc: "**Ethiopian Wat**: A traditional Ethiopian stew made from spices, vegetables, and legumes, served with injera flatbread.", }, } table := "| Picture | Description |\n|---------|-------------|\n" for _, option := range options { table += "| ![food](" + option.image + ") | " + option.desc + " |\n" } table += "\n*(source: wikipedia)*" return table } ================================================ FILE: new_samples/query/main.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT package main import ( "fmt" "os" "os/signal" "syscall" ) func main() { StartWorker() done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT) fmt.Println("Cadence worker started, press ctrl+c to terminate...") <-done } ================================================ FILE: new_samples/query/markdown_query.go ================================================ package main import ( "context" "bytes" "strconv" "text/template" "time" "go.uber.org/cadence/activity" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) const ( CompleteSignalChan = "complete" ) // markdownFormattedResponse is the JSON shape Cadence Web expects for markdown query results (formattedData, text/markdown, data). type markdownFormattedResponse struct { CadenceResponseType string `json:"cadenceResponseType"` Format string `json:"format"` Data string `json:"data"` } func MarkdownQueryWorkflow(ctx workflow.Context) error { ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute * 60, StartToCloseTimeout: time.Minute * 60, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) logger.Info("MarkdownQueryWorkflow started") workflow.SetQueryHandler(ctx, "Signal", func() (markdownFormattedResponse, error) { logger := workflow.GetLogger(ctx) logger.Info("Responding to 'Signal' query") return makeMarkdownQueryResponse(ctx), nil }) var complete bool completeChan := workflow.GetSignalChannel(ctx, CompleteSignalChan) for { s := workflow.NewSelector(ctx) s.AddReceive(completeChan, func(ch workflow.Channel, ok bool) { if ok { ch.Receive(ctx, &complete) } logger.Info("Signal input: " + strconv.FormatBool(complete)) }) s.Select(ctx) var result string err := workflow.ExecuteActivity(ctx, MarkdownQueryActivity, complete).Get(ctx, &result) if err != nil { return err } logger.Info("Activity result: " + result) if complete { return nil } } } func makeMarkdownQueryResponse(ctx workflow.Context) markdownFormattedResponse { type P map[string]interface{} markdownTemplate, err := template.New("").Parse(` ## Markdown Query Workflow You can use markdown as your query response, which also supports starting and signaling workflows. * Use the Complete button to complete this workflow. * Use the Continue button just to send a signal to continue this workflow. * Or you can use the "Start Another" button to start another workflow of this type. {% signal signalName="complete" label="Complete" domain="cadence-samples" cluster="cluster0" workflowId="{{.workflowID}}" runId="{{.runID}}" input=true /%} {% signal signalName="complete" label="Continue" domain="cadence-samples" cluster="cluster0" workflowId="{{.workflowID}}" runId="{{.runID}}" input=false /%} {% start workflowType="cadence_samples.MarkdownQueryWorkflow" label="Start Another" domain="cadence-samples" cluster="cluster0" taskList="cadence-samples-worker" workflowId="{{.newWorkflowID}}" timeoutSeconds=60 /%} {% br /%} {% image src="https://cadenceworkflow.io/img/cadence-logo.svg" alt="Cadence Logo" height="100" /%} `) if err != nil { panic("Failed to parse template: " + err.Error()) } var markdown bytes.Buffer err = markdownTemplate.Execute(&markdown, P{ "workflowID": workflow.GetInfo(ctx).WorkflowExecution.ID, "runID": workflow.GetInfo(ctx).WorkflowExecution.RunID, "newWorkflowID": "markdown-" + strconv.FormatInt(time.Now().UnixNano()/1000000, 10), }) if err != nil { panic("Failed to execute template: " + err.Error()) } return markdownFormattedResponse{ CadenceResponseType: "formattedData", Format: "text/markdown", Data: markdown.String(), } } func MarkdownQueryActivity(ctx context.Context, complete bool) (string, error) { logger := activity.GetLogger(ctx) logger.Info("MarkdownQueryActivity started, a new signal has been received", zap.Bool("complete", complete)) if complete { return "Workflow will complete now", nil } return "Workflow will continue to run", nil } ================================================ FILE: new_samples/query/order_fulfillment_workflow.go ================================================ package main import ( "bytes" "fmt" "text/template" "time" "go.uber.org/cadence/workflow" "go.uber.org/zap" ) // orderDashboardFormattedResponse is the JSON shape Cadence Web expects for markdown query results (formattedData, text/markdown, data). type orderDashboardFormattedResponse struct { CadenceResponseType string `json:"cadenceResponseType"` Format string `json:"format"` Data string `json:"data"` } // Order represents an e-commerce order being fulfilled type Order struct { OrderID string CustomerName string CustomerEmail string Items []OrderItem TotalAmount float64 Status string TrackingNum string Carrier string RefundAmount float64 RefundReason string CreatedAt time.Time } // OrderItem represents a line item in an order type OrderItem struct { Name string Quantity int Price float64 } // ActionLogEntry represents an ops action taken on the order type ActionLogEntry struct { Timestamp time.Time Action string Operator string Details string } // Order status constants const ( StatusPendingPayment = "pending_payment" StatusPaymentApproved = "payment_approved" StatusReadyToShip = "ready_to_ship" StatusShipped = "shipped" StatusDelivered = "delivered" StatusCancelled = "cancelled" StatusRefunded = "refunded" ) // Signal payloads type RejectPaymentSignal struct { Reason string `json:"reason"` Operator string `json:"operator"` } type ApprovePaymentSignal struct { Operator string `json:"operator"` } type ShipOrderSignal struct { TrackingNumber string `json:"trackingNumber"` Carrier string `json:"carrier"` Operator string `json:"operator"` } type RefundSignal struct { Amount float64 `json:"amount"` Reason string `json:"reason"` Operator string `json:"operator"` } type CancelOrderSignal struct { Reason string `json:"reason"` Operator string `json:"operator"` } type SimpleSignal struct { Operator string `json:"operator"` } // OrderFulfillmentWorkflow demonstrates a state-driven MarkDoc UI for ops teams. // This workflow shows how Cadence Web queries can replace custom admin panels. func OrderFulfillmentWorkflow(ctx workflow.Context) error { logger := workflow.GetLogger(ctx) logger.Info("OrderFulfillmentWorkflow started") // Initialize sample order order := Order{ OrderID: "ORD-2024-001234", CustomerName: "Alice Johnson", CustomerEmail: "alice.johnson@example.com", Items: []OrderItem{ {Name: "Wireless Headphones", Quantity: 2, Price: 79.99}, {Name: "Phone Case", Quantity: 1, Price: 19.99}, }, TotalAmount: 179.97, Status: StatusPendingPayment, CreatedAt: workflow.Now(ctx), } // Action log for audit trail actionLog := []ActionLogEntry{ { Timestamp: workflow.Now(ctx), Action: "Order Created", Operator: "System", Details: fmt.Sprintf("Order %s created for %s", order.OrderID, order.CustomerName), }, } // Register query handler for the ops dashboard workflow.SetQueryHandler(ctx, "dashboard", func() (orderDashboardFormattedResponse, error) { logger.Info("Responding to 'dashboard' query") return makeOrderDashboard(ctx, order, actionLog), nil }) // Set up signal channels approvePaymentChan := workflow.GetSignalChannel(ctx, "approve_payment") rejectPaymentChan := workflow.GetSignalChannel(ctx, "reject_payment") markReadyChan := workflow.GetSignalChannel(ctx, "mark_ready_to_ship") shipOrderChan := workflow.GetSignalChannel(ctx, "ship_order") refundChan := workflow.GetSignalChannel(ctx, "issue_refund") cancelChan := workflow.GetSignalChannel(ctx, "cancel_order") deliveredChan := workflow.GetSignalChannel(ctx, "mark_delivered") // Main workflow loop - process signals until terminal state for !isTerminalState(order.Status) { selector := workflow.NewSelector(ctx) selector.AddReceive(approvePaymentChan, func(ch workflow.Channel, ok bool) { var signal ApprovePaymentSignal ch.Receive(ctx, &signal) if order.Status == StatusPendingPayment { order.Status = StatusPaymentApproved actionLog = append(actionLog, ActionLogEntry{ Timestamp: workflow.Now(ctx), Action: "Payment Approved", Operator: getOperator(signal.Operator), Details: fmt.Sprintf("Payment of $%.2f approved", order.TotalAmount), }) logger.Info("Payment approved", zap.String("operator", signal.Operator)) } }) selector.AddReceive(rejectPaymentChan, func(ch workflow.Channel, ok bool) { var signal RejectPaymentSignal ch.Receive(ctx, &signal) if order.Status == StatusPendingPayment { order.Status = StatusCancelled actionLog = append(actionLog, ActionLogEntry{ Timestamp: workflow.Now(ctx), Action: "Payment Rejected", Operator: getOperator(signal.Operator), Details: fmt.Sprintf("Reason: %s", signal.Reason), }) logger.Info("Payment rejected", zap.String("reason", signal.Reason)) } }) selector.AddReceive(markReadyChan, func(ch workflow.Channel, ok bool) { var signal SimpleSignal ch.Receive(ctx, &signal) if order.Status == StatusPaymentApproved { order.Status = StatusReadyToShip actionLog = append(actionLog, ActionLogEntry{ Timestamp: workflow.Now(ctx), Action: "Marked Ready to Ship", Operator: getOperator(signal.Operator), Details: "Order prepared and ready for shipping", }) logger.Info("Order marked ready to ship") } }) selector.AddReceive(shipOrderChan, func(ch workflow.Channel, ok bool) { var signal ShipOrderSignal ch.Receive(ctx, &signal) if order.Status == StatusReadyToShip { order.Status = StatusShipped order.TrackingNum = signal.TrackingNumber order.Carrier = signal.Carrier actionLog = append(actionLog, ActionLogEntry{ Timestamp: workflow.Now(ctx), Action: "Order Shipped", Operator: getOperator(signal.Operator), Details: fmt.Sprintf("Carrier: %s, Tracking: %s", signal.Carrier, signal.TrackingNumber), }) logger.Info("Order shipped", zap.String("tracking", signal.TrackingNumber)) } }) selector.AddReceive(refundChan, func(ch workflow.Channel, ok bool) { var signal RefundSignal ch.Receive(ctx, &signal) if order.Status == StatusPaymentApproved { order.Status = StatusRefunded order.RefundAmount = signal.Amount order.RefundReason = signal.Reason actionLog = append(actionLog, ActionLogEntry{ Timestamp: workflow.Now(ctx), Action: "Refund Issued", Operator: getOperator(signal.Operator), Details: fmt.Sprintf("Amount: $%.2f, Reason: %s", signal.Amount, signal.Reason), }) logger.Info("Refund issued", zap.Float64("amount", signal.Amount)) } }) selector.AddReceive(cancelChan, func(ch workflow.Channel, ok bool) { var signal CancelOrderSignal ch.Receive(ctx, &signal) if order.Status == StatusReadyToShip { order.Status = StatusCancelled actionLog = append(actionLog, ActionLogEntry{ Timestamp: workflow.Now(ctx), Action: "Order Cancelled", Operator: getOperator(signal.Operator), Details: fmt.Sprintf("Reason: %s", signal.Reason), }) logger.Info("Order cancelled", zap.String("reason", signal.Reason)) } }) selector.AddReceive(deliveredChan, func(ch workflow.Channel, ok bool) { var signal SimpleSignal ch.Receive(ctx, &signal) if order.Status == StatusShipped { order.Status = StatusDelivered actionLog = append(actionLog, ActionLogEntry{ Timestamp: workflow.Now(ctx), Action: "Order Delivered", Operator: getOperator(signal.Operator), Details: "Package confirmed delivered to customer", }) logger.Info("Order marked as delivered") } }) selector.Select(ctx) } logger.Info("OrderFulfillmentWorkflow completed", zap.String("finalStatus", order.Status)) return nil } func isTerminalState(status string) bool { return status == StatusDelivered || status == StatusCancelled || status == StatusRefunded } func getOperator(operator string) string { if operator == "" { return "ops-user" } return operator } func makeOrderDashboard(ctx workflow.Context, order Order, actionLog []ActionLogEntry) orderDashboardFormattedResponse { type P map[string]interface{} markdownTemplate, err := template.New("").Parse(` ## 🛒 Order Dashboard > **Your admin panel** - manage orders directly from Cadence Web. --- ### ⚡ Available Actions {{.actionButtons}} --- ### 📋 Order Details | Field | Value | |-------|-------| | **Order ID** | {{.orderID}} | | **Customer** | {{.customerName}} | | **Email** | {{.customerEmail}} | | **Created** | {{.createdAt}} | | **Status** | {{.statusBadge}} | {{if .trackingNum}} **Tracking: {{.carrier}} - {{.trackingNum}}** {{end}} {{if .refundAmount}}**Refund: ${{.refundAmount}}** {{end}} ### 📦 Order Items | Item | Qty | Price | Subtotal | |------|-----|-------|----------| {{.itemsTable}} **Total: ${{.totalAmount}}** --- ### ⏱️ Action History | Timestamp | Action | Operator | Details | |-----------|--------|----------|---------| {{.actionHistory}} --- *Click query "Run" button again to see updated status after taking an action.* `) if err != nil { panic("Failed to parse template: " + err.Error()) } var markdown bytes.Buffer err = markdownTemplate.Execute(&markdown, P{ "orderID": order.OrderID, "customerName": order.CustomerName, "customerEmail": order.CustomerEmail, "createdAt": order.CreatedAt.Format("2006-01-02 15:04:05"), "statusBadge": getStatusBadge(order.Status), "trackingNum": order.TrackingNum, "carrier": order.Carrier, "refundAmount": fmt.Sprintf("%.2f", order.RefundAmount), "refundReason": order.RefundReason, "totalAmount": fmt.Sprintf("%.2f", order.TotalAmount), "itemsTable": makeItemsTable(order.Items), "actionButtons": makeActionButtons(ctx, order), "actionHistory": makeActionHistory(actionLog), }) if err != nil { panic("Failed to execute template: " + err.Error()) } return orderDashboardFormattedResponse{ CadenceResponseType: "formattedData", Format: "text/markdown", Data: markdown.String(), } } func getStatusBadge(status string) string { badges := map[string]string{ StatusPendingPayment: "🟡 **Pending Payment**", StatusPaymentApproved: "🟢 **Payment Approved**", StatusReadyToShip: "📦 **Ready to Ship**", StatusShipped: "🚚 **Shipped**", StatusDelivered: "✅ **Delivered**", StatusCancelled: "❌ **Cancelled**", StatusRefunded: "💰 **Refunded**", } if badge, ok := badges[status]; ok { return badge } return status } func makeItemsTable(items []OrderItem) string { table := "" for _, item := range items { subtotal := float64(item.Quantity) * item.Price table += fmt.Sprintf("| %s | %d | $%.2f | $%.2f |\n", item.Name, item.Quantity, item.Price, subtotal) } return table } func makeActionButtons(ctx workflow.Context, order Order) string { workflowID := workflow.GetInfo(ctx).WorkflowExecution.ID runID := workflow.GetInfo(ctx).WorkflowExecution.RunID var buttons string switch order.Status { case StatusPendingPayment: buttons = fmt.Sprintf(` **Payment Review:** {%% signal signalName="approve_payment" label="✓ Approve Payment" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"operator":"ops-user"} /%%} {%% signal signalName="reject_payment" label="✗ Reject: Policy Violation" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"reason":"Policy Violation","operator":"ops-user"} /%%} {%% signal signalName="reject_payment" label="✗ Reject: Fraud Suspected" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"reason":"Fraud Suspected","operator":"ops-user"} /%%} {%% signal signalName="reject_payment" label="✗ Reject: Customer Request" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"reason":"Customer Request","operator":"ops-user"} /%%} `, workflowID, runID, workflowID, runID, workflowID, runID, workflowID, runID) case StatusPaymentApproved: buttons = fmt.Sprintf(` **Fulfillment Actions:** {%% signal signalName="mark_ready_to_ship" label="📦 Mark Ready to Ship" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"operator":"ops-user"} /%%} **Refund Options:** {%% signal signalName="issue_refund" label="💰 Full Refund ($%.2f)" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"amount":%.2f,"reason":"Full refund requested","operator":"ops-user"} /%%} {%% signal signalName="issue_refund" label="💰 Partial Refund (50%%)" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"amount":%.2f,"reason":"Partial refund - customer goodwill","operator":"ops-user"} /%%} `, workflowID, runID, order.TotalAmount, workflowID, runID, order.TotalAmount, workflowID, runID, order.TotalAmount/2) case StatusReadyToShip: buttons = fmt.Sprintf(` **Shipping Options:** {%% signal signalName="ship_order" label="🚚 Ship via UPS" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"trackingNumber":"1Z999AA10123456784","carrier":"UPS","operator":"ops-user"} /%%} {%% signal signalName="ship_order" label="🚚 Ship via FedEx" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"trackingNumber":"794644790126","carrier":"FedEx","operator":"ops-user"} /%%} {%% signal signalName="ship_order" label="🚚 Ship via USPS" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"trackingNumber":"9400111899223456789012","carrier":"USPS","operator":"ops-user"} /%%} **Cancel Order:** {%% signal signalName="cancel_order" label="❌ Cancel Order" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"reason":"Cancelled before shipping","operator":"ops-user"} /%%} `, workflowID, runID, workflowID, runID, workflowID, runID, workflowID, runID) case StatusShipped: buttons = fmt.Sprintf(` **Delivery Confirmation:** {%% signal signalName="mark_delivered" label="✅ Mark as Delivered" domain="cadence-samples" cluster="cluster0" workflowId="%s" runId="%s" input={"operator":"ops-user"} /%%} `, workflowID, runID) default: buttons = ` *No actions available - order has been completed.* ` } return buttons } func makeActionHistory(actionLog []ActionLogEntry) string { history := "" for _, entry := range actionLog { history += fmt.Sprintf("| %s | %s | %s | %s |\n", entry.Timestamp.Format("15:04:05"), entry.Action, entry.Operator, entry.Details) } return history } ================================================ FILE: new_samples/query/worker.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT // Package worker implements a Cadence worker with basic configurations. package main import ( "github.com/uber-go/tally" apiv1 "github.com/uber/cadence-idl/go/proto/api/v1" "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" "go.uber.org/cadence/activity" "go.uber.org/cadence/compatibility" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/yarpc" "go.uber.org/yarpc/peer" yarpchostport "go.uber.org/yarpc/peer/hostport" "go.uber.org/yarpc/transport/grpc" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( HostPort = "127.0.0.1:7833" Domain = "cadence-samples" // TaskListName identifies set of client workflows, activities, and workers. // It could be your group or client or application name. TaskListName = "cadence-samples-worker" ClientName = "cadence-samples-worker" CadenceService = "cadence-frontend" ) // StartWorker creates and starts a basic Cadence worker. func StartWorker() { logger, cadenceClient := BuildLogger(), BuildCadenceClient() workerOptions := worker.Options{ Logger: logger, MetricsScope: tally.NewTestScope(TaskListName, nil), } w := worker.New( cadenceClient, Domain, TaskListName, workerOptions) // workflow registration w.RegisterWorkflowWithOptions(MarkdownQueryWorkflow, workflow.RegisterOptions{Name: "cadence_samples.MarkdownQueryWorkflow"}) w.RegisterWorkflowWithOptions(LunchVoteWorkflow, workflow.RegisterOptions{Name: "cadence_samples.LunchVoteWorkflow"}) w.RegisterWorkflowWithOptions(OrderFulfillmentWorkflow, workflow.RegisterOptions{Name: "cadence_samples.OrderFulfillmentWorkflow"}) w.RegisterActivityWithOptions(MarkdownQueryActivity, activity.RegisterOptions{Name: "cadence_samples.MarkdownQueryActivity"}) err := w.Start() if err != nil { panic("Failed to start worker: " + err.Error()) } logger.Info("Started Worker.", zap.String("worker", TaskListName)) } func BuildCadenceClient(dialOptions ...grpc.DialOption) workflowserviceclient.Interface { grpcTransport := grpc.NewTransport() // Create a single peer chooser that identifies the host/port and configures // a gRPC dialer with TLS credentials myChooser := peer.NewSingle( yarpchostport.Identify(HostPort), grpcTransport.NewDialer(dialOptions...), ) outbound := grpcTransport.NewOutbound(myChooser) dispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: ClientName, Outbounds: yarpc.Outbounds{ CadenceService: {Unary: outbound}, }, }) if err := dispatcher.Start(); err != nil { panic("Failed to start dispatcher: " + err.Error()) } clientConfig := dispatcher.ClientConfig(CadenceService) // Create a compatibility adapter that wraps proto-based YARPC clients // to provide a unified interface for domain, workflow, worker, and visibility APIs return compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ) } func BuildLogger() *zap.Logger { config := zap.NewDevelopmentConfig() config.Level.SetLevel(zapcore.InfoLevel) var err error logger, err := config.Build() if err != nil { panic("Failed to setup logger: " + err.Error()) } return logger } ================================================ FILE: new_samples/signal/README.md ================================================ # Signal Workflow Sample ## Prerequisites 0. Install Cadence CLI. See instruction [here](https://cadenceworkflow.io/docs/cli/). 1. Run the Cadence server: 1. Clone the [Cadence](https://github.com/cadence-workflow/cadence) repository if you haven't done already: `git clone https://github.com/cadence-workflow/cadence.git` 2. Run `docker compose -f docker/docker-compose.yml up` to start Cadence server 3. See more details at https://github.com/uber/cadence/blob/master/README.md 2. Once everything is up and running in Docker, open [localhost:8088](localhost:8088) to view Cadence UI. 3. Register the `cadence-samples` domain: ```bash cadence --domain cadence-samples domain register ``` Refresh the [domains page](http://localhost:8088/domains) from step 2 to verify `cadence-samples` is registered. ## Steps to run sample Inside the folder this sample is defined, run the following command: ```bash go run . ``` This will call the main function in main.go which starts the worker, which will be execute the sample workflow code ## Simple Signal Workflow This workflow takes an input message and greet you as response. Try the following CLI ```bash cadence --domain cadence-samples \ workflow start \ --tl cadence-samples-worker \ --et 60 \ --workflow_type cadence_samples.SimpleSignalWorkflow ``` Verify that your workflow started. Your can find your worklow by looking at the "Workflow type" column. If this is your first sample, please refer to [HelloWorkflow sample](https://github.com/cadence-workflow/cadence-samples/tree/master/new_samples/hello_world) about how to view your workflows. ### Signal your workflow This workflow will need a signal to complete successfully. Below is how you can send a signal. In this example, we are sending a `bool` value `true` (JSON formatted) via the signal called `complete` ```bash cadence --domain cadence-samples \ workflow signal \ --wid \ --name complete \ --input 'true' ``` ## References * The website: https://cadenceworkflow.io * Cadence's server: https://github.com/uber/cadence * Cadence's Go client: https://github.com/uber-go/cadence-client ================================================ FILE: new_samples/signal/generator/README.md ================================================ # Sample Generator This folder is NOT part of the actual sample. It exists only for contributors who work on this sample. Please disregard it if you are trying to learn about Cadence. To create a better learning experience for Cadence users, each sample folder is designed to be self contained. Users can view every part of writing and running workflows, including: * Cadence client initialization * Worker with workflow and activity registrations * Workflow starter * and the workflow code itself Some samples may have more or fewer parts depending on what they need to demonstrate. In most cases, the workflow code (e.g. `workflow.go`) is the part that users care about. The rest is boilerplate needed to run that workflow. For each sample folder, the workflow code should be written by hand. The boilerplate can be generated. Keeping all parts inside one folder gives early learners more value because they can see everything together rather than jumping across directories. ## Contributing * When creating a new sample, follow the steps mentioned in the README file in the main samples folder. * To update the sample workflow code, edit the workflow file directly. * To update the worker, client, or other boilerplate logic, edit the generator file. If your change applies to all samples, update the common generator file inside the `template` folder. Edit the generator file in this folder only when the change should affect this sample alone. * When you are done run the following command in the generator folder ```bash go run . ``` ================================================ FILE: new_samples/signal/generator/README_specific.md ================================================ ## Simple Signal Workflow This workflow takes an input message and greet you as response. Try the following CLI ```bash cadence --domain cadence-samples \ workflow start \ --tl cadence-samples-worker \ --et 60 \ --workflow_type cadence_samples.SimpleSignalWorkflow ``` Verify that your workflow started. Your can find your worklow by looking at the "Workflow type" column. If this is your first sample, please refer to [HelloWorkflow sample](https://github.com/cadence-workflow/cadence-samples/tree/master/new_samples/hello_world) about how to view your workflows. ### Signal your workflow This workflow will need a signal to complete successfully. Below is how you can send a signal. In this example, we are sending a `bool` value `true` (JSON formatted) via the signal called `complete` ```bash cadence --domain cadence-samples \ workflow signal \ --wid \ --name complete \ --input 'true' ``` ================================================ FILE: new_samples/signal/generator/generate.go ================================================ package main import "github.com/uber-common/cadence-samples/new_samples/template" func main() { // Define the data for HelloWorld data := template.TemplateData{ SampleName: "Signal Workflow", Workflows: []string{"SimpleSignalWorkflow"}, Activities: []string{"SimpleSignalActivity"}, } template.GenerateAll(data) } // Implement custom generator below ================================================ FILE: new_samples/signal/main.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT package main import ( "fmt" "os" "os/signal" "syscall" ) func main() { StartWorker() done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT) fmt.Println("Cadence worker started, press ctrl+c to terminate...") <-done } ================================================ FILE: new_samples/signal/simple_signal_workflow.go ================================================ package main import ( "context" "go.uber.org/cadence/workflow" "go.uber.org/cadence/activity" "strconv" "time" "go.uber.org/zap" ) const ( CompleteSignalChan = "complete" ) func SimpleSignalWorkflow(ctx workflow.Context) error { ao := workflow.ActivityOptions{ ScheduleToStartTimeout: time.Minute * 60, StartToCloseTimeout: time.Minute * 60, } ctx = workflow.WithActivityOptions(ctx, ao) logger := workflow.GetLogger(ctx) logger.Info("SimpleSignalWorkflow started") var complete bool completeChan := workflow.GetSignalChannel(ctx, CompleteSignalChan) for { s := workflow.NewSelector(ctx) s.AddReceive(completeChan, func(ch workflow.Channel, ok bool) { if ok { ch.Receive(ctx, &complete) } logger.Info("Signal input: " + strconv.FormatBool(complete)) }) s.Select(ctx) var result string err := workflow.ExecuteActivity(ctx, SimpleSignalActivity, complete).Get(ctx, &result) if err != nil { return err } logger.Info("Activity result: " + result) if complete { return nil } } } func SimpleSignalActivity(ctx context.Context, complete bool) (string, error) { logger := activity.GetLogger(ctx) logger.Info("SimpleSignalActivity started, a new signal has been received", zap.Bool("complete", complete)) if complete { return "Workflow will complete now", nil } return "Workflow will continue to run", nil } ================================================ FILE: new_samples/signal/worker.go ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT // Package worker implements a Cadence worker with basic configurations. package main import ( "github.com/uber-go/tally" apiv1 "github.com/uber/cadence-idl/go/proto/api/v1" "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" "go.uber.org/cadence/activity" "go.uber.org/cadence/compatibility" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/yarpc" "go.uber.org/yarpc/peer" yarpchostport "go.uber.org/yarpc/peer/hostport" "go.uber.org/yarpc/transport/grpc" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( HostPort = "127.0.0.1:7833" Domain = "cadence-samples" // TaskListName identifies set of client workflows, activities, and workers. // It could be your group or client or application name. TaskListName = "cadence-samples-worker" ClientName = "cadence-samples-worker" CadenceService = "cadence-frontend" ) // StartWorker creates and starts a basic Cadence worker. func StartWorker() { logger, cadenceClient := BuildLogger(), BuildCadenceClient() workerOptions := worker.Options{ Logger: logger, MetricsScope: tally.NewTestScope(TaskListName, nil), } w := worker.New( cadenceClient, Domain, TaskListName, workerOptions) // workflow registration w.RegisterWorkflowWithOptions(SimpleSignalWorkflow, workflow.RegisterOptions{Name: "cadence_samples.SimpleSignalWorkflow"}) w.RegisterActivityWithOptions(SimpleSignalActivity, activity.RegisterOptions{Name: "cadence_samples.SimpleSignalActivity"}) err := w.Start() if err != nil { panic("Failed to start worker: " + err.Error()) } logger.Info("Started Worker.", zap.String("worker", TaskListName)) } func BuildCadenceClient(dialOptions ...grpc.DialOption) workflowserviceclient.Interface { grpcTransport := grpc.NewTransport() // Create a single peer chooser that identifies the host/port and configures // a gRPC dialer with TLS credentials myChooser := peer.NewSingle( yarpchostport.Identify(HostPort), grpcTransport.NewDialer(dialOptions...), ) outbound := grpcTransport.NewOutbound(myChooser) dispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: ClientName, Outbounds: yarpc.Outbounds{ CadenceService: {Unary: outbound}, }, }) if err := dispatcher.Start(); err != nil { panic("Failed to start dispatcher: " + err.Error()) } clientConfig := dispatcher.ClientConfig(CadenceService) // Create a compatibility adapter that wraps proto-based YARPC clients // to provide a unified interface for domain, workflow, worker, and visibility APIs return compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ) } func BuildLogger() *zap.Logger { config := zap.NewDevelopmentConfig() config.Level.SetLevel(zapcore.InfoLevel) var err error logger, err := config.Build() if err != nil { panic("Failed to setup logger: " + err.Error()) } return logger } ================================================ FILE: new_samples/template/README.tmpl ================================================ # {{.SampleName}} Sample ## Prerequisites 0. Install Cadence CLI. See instruction [here](https://cadenceworkflow.io/docs/cli/). 1. Run the Cadence server: 1. Clone the [Cadence](https://github.com/cadence-workflow/cadence) repository if you haven't done already: `git clone https://github.com/cadence-workflow/cadence.git` 2. Run `docker compose -f docker/docker-compose.yml up` to start Cadence server 3. See more details at https://github.com/uber/cadence/blob/master/README.md 2. Once everything is up and running in Docker, open [localhost:8088](localhost:8088) to view Cadence UI. 3. Register the `cadence-samples` domain: ```bash cadence --domain cadence-samples domain register ``` Refresh the [domains page](http://localhost:8088/domains) from step 2 to verify `cadence-samples` is registered. ## Steps to run sample Inside the folder this sample is defined, run the following command: ```bash go run . ``` This will call the main function in main.go which starts the worker, which will be execute the sample workflow code ================================================ FILE: new_samples/template/README_generator.tmpl ================================================ # Sample Generator This folder is NOT part of the actual sample. It exists only for contributors who work on this sample. Please disregard it if you are trying to learn about Cadence. To create a better learning experience for Cadence users, each sample folder is designed to be self contained. Users can view every part of writing and running workflows, including: * Cadence client initialization * Worker with workflow and activity registrations * Workflow starter * and the workflow code itself Some samples may have more or fewer parts depending on what they need to demonstrate. In most cases, the workflow code (e.g. `workflow.go`) is the part that users care about. The rest is boilerplate needed to run that workflow. For each sample folder, the workflow code should be written by hand. The boilerplate can be generated. Keeping all parts inside one folder gives early learners more value because they can see everything together rather than jumping across directories. ## Contributing * When creating a new sample, follow the steps mentioned in the README file in the main samples folder. * To update the sample workflow code, edit the workflow file directly. * To update the worker, client, or other boilerplate logic, edit the generator file. If your change applies to all samples, update the common generator file inside the `template` folder. Edit the generator file in this folder only when the change should affect this sample alone. * When you are done run the following command in the generator folder ```bash go run . ``` ================================================ FILE: new_samples/template/README_references.tmpl ================================================ ## References * The website: https://cadenceworkflow.io * Cadence's server: https://github.com/uber/cadence * Cadence's Go client: https://github.com/uber-go/cadence-client ================================================ FILE: new_samples/template/generator.go ================================================ package template import ( "os" "text/template" ) type TemplateData struct { SampleName string Workflows []string Activities []string } func GenerateAll(data TemplateData) { GenerateWorker(data) GenerateMain(data) GenerateSampleReadMe(data) GenerateGeneratorReadMe(data) } func GenerateWorker(data TemplateData) { GenerateFile("../../template/worker.tmpl", "../worker.go", data) println("Generated worker.go") } func GenerateMain(data TemplateData) { GenerateFile("../../template/main.tmpl", "../main.go", data) println("Generated main.go") } func GenerateSampleReadMe(data TemplateData) { inputs := []string{"../../template/README.tmpl", "README_specific.md", "../../template/README_references.tmpl"} GenerateREADME(inputs, "../README.md", data) } func GenerateGeneratorReadMe(data TemplateData) { GenerateFile("../../template/README_generator.tmpl", "README.md", data) println("Generated README.md") } func GenerateFile(templatePath, outputPath string, data TemplateData) { tmpl, err := template.ParseFiles(templatePath) if err != nil { panic("Failed to parse template " + templatePath + ": " + err.Error()) } f, err := os.Create(outputPath) if err != nil { panic("Failed to create output file " + outputPath + ": " + err.Error()) } defer f.Close() err = tmpl.Execute(f, data) if err != nil { panic("Failed to execute template: " + err.Error()) } } func GenerateREADME(inputs []string, outputPath string, data TemplateData) { // Create output file f, err := os.Create(outputPath) if err != nil { panic("Failed to create README file: " + err.Error()) } defer f.Close() for _, input := range inputs { tmpl, err := template.ParseFiles(input) if err != nil { panic("Failed to parse README template: " + err.Error()) } err = tmpl.Execute(f, data) if err != nil { panic(input + ": Failed to append README content: " + err.Error()) } } } ================================================ FILE: new_samples/template/main.tmpl ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT package main import ( "fmt" "os" "os/signal" "syscall" ) func main() { StartWorker() done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT) fmt.Println("Cadence worker started, press ctrl+c to terminate...") <-done } ================================================ FILE: new_samples/template/worker.tmpl ================================================ // THIS IS A GENERATED FILE // PLEASE DO NOT EDIT // Package worker implements a Cadence worker with basic configurations. package main import ( "github.com/uber-go/tally" apiv1 "github.com/uber/cadence-idl/go/proto/api/v1" "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" "go.uber.org/cadence/activity" "go.uber.org/cadence/compatibility" "go.uber.org/cadence/worker" "go.uber.org/cadence/workflow" "go.uber.org/yarpc" "go.uber.org/yarpc/peer" yarpchostport "go.uber.org/yarpc/peer/hostport" "go.uber.org/yarpc/transport/grpc" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( HostPort = "127.0.0.1:7833" Domain = "cadence-samples" // TaskListName identifies set of client workflows, activities, and workers. // It could be your group or client or application name. TaskListName = "cadence-samples-worker" ClientName = "cadence-samples-worker" CadenceService = "cadence-frontend" ) // StartWorker creates and starts a basic Cadence worker. func StartWorker() { logger, cadenceClient := BuildLogger(), BuildCadenceClient() workerOptions := worker.Options{ Logger: logger, MetricsScope: tally.NewTestScope(TaskListName, nil), } w := worker.New( cadenceClient, Domain, TaskListName, workerOptions) // workflow registration {{- range .Workflows}} w.RegisterWorkflowWithOptions({{.}}, workflow.RegisterOptions{Name: "cadence_samples.{{.}}"}) {{- end}} {{- range .Activities}} w.RegisterActivityWithOptions({{.}}, activity.RegisterOptions{Name: "cadence_samples.{{.}}"}) {{- end}} err := w.Start() if err != nil { panic("Failed to start worker: " + err.Error()) } logger.Info("Started Worker.", zap.String("worker", TaskListName)) } func BuildCadenceClient(dialOptions ...grpc.DialOption) workflowserviceclient.Interface { grpcTransport := grpc.NewTransport() // Create a single peer chooser that identifies the host/port and configures // a gRPC dialer with TLS credentials myChooser := peer.NewSingle( yarpchostport.Identify(HostPort), grpcTransport.NewDialer(dialOptions...), ) outbound := grpcTransport.NewOutbound(myChooser) dispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: ClientName, Outbounds: yarpc.Outbounds{ CadenceService: {Unary: outbound}, }, }) if err := dispatcher.Start(); err != nil { panic("Failed to start dispatcher: " + err.Error()) } clientConfig := dispatcher.ClientConfig(CadenceService) // Create a compatibility adapter that wraps proto-based YARPC clients // to provide a unified interface for domain, workflow, worker, and visibility APIs return compatibility.NewThrift2ProtoAdapter( apiv1.NewDomainAPIYARPCClient(clientConfig), apiv1.NewWorkflowAPIYARPCClient(clientConfig), apiv1.NewWorkerAPIYARPCClient(clientConfig), apiv1.NewVisibilityAPIYARPCClient(clientConfig), ) } func BuildLogger() *zap.Logger { config := zap.NewDevelopmentConfig() config.Level.SetLevel(zapcore.InfoLevel) var err error logger, err := config.Build() if err != nil { panic("Failed to setup logger: " + err.Error()) } return logger } ================================================ FILE: python_sdk_samples/.python-version ================================================ 3.13 ================================================ FILE: python_sdk_samples/README.md ================================================ # Cadence Python SDK Samples All samples under this folder demonstrate how to use Python SDK effectively. ## 🚀 Quick Start 1. We use uv to install dependencies of all samples Refer to [UV installation Guide](https://docs.astral.sh/uv/getting-started/installation/) 2. build all samples ```bash cd python_sdk_samples uv sync ``` This downloads all dependencies so `uv run` will have all the dependent packages 3. Start Cadence Server ```bash curl -LO https://raw.githubusercontent.com/cadence-workflow/cadence/refs/heads/master/docker/docker-compose.yml && docker-compose up --wait ``` This downloads and starts all required dependencies including Cadence server, database, and [Cadence Web UI](https://github.com/uber/cadence-web). You can view your sample workflows at [http://localhost:8088](http://localhost:8088). 4. **run one sample**: ```bash uv run python -m openai_samples.agent_handoffs.main ``` ================================================ FILE: python_sdk_samples/__init__.py ================================================ ================================================ FILE: python_sdk_samples/openai_samples/__init__.py ================================================ ================================================ FILE: python_sdk_samples/openai_samples/agent_handoffs/README.md ================================================ # What This demo shows how to run OpenAI agents in a durable way (retries, reset to a check point). Specifically, we show the execution of a multi-agent system with handoffs. # Setup OpenAI API keys Make sure the OPENAI_API_KEY environment variable is set. See details for best practices. https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety # Setup Cadence Server Refer to step 3 of the [Quick Start](../../README.md) in `python_sdk_samples/README.md` for instructions on starting the Cadence Server: ```bash curl -LO https://raw.githubusercontent.com/cadence-workflow/cadence/refs/heads/master/docker/docker-compose.yml && docker-compose up --wait ``` # Start Agent workers ``` cd python_sdk_samples uv sync uv run python -m openai_samples.agent_handoffs.main ``` # Trigger Agent Run Run Cadence CLI command ``` cadence --domain default workflow start \ --workflow_type BookTripAgentWorkflow \ --tasklist agent-task-list \ --execution_timeout 30 \ --input '"Book a trip for me from Uber Seattle Office to Uber San Francisco Office tomorrow at 10:00 AM"' ``` Or click start workflow in the [cadence-web](http://localhost:8088/domains/default/cluster0/workflows) ![Start Workflow Screenshot](images/start_workflow.png) # View Agent Run Result ![Agent Workflow Result Screenshot](images/agent_workflow_result.png) ================================================ FILE: python_sdk_samples/openai_samples/agent_handoffs/__init__.py ================================================ ================================================ FILE: python_sdk_samples/openai_samples/agent_handoffs/book_trip_agent.py ================================================ import cadence from agents import Agent, function_tool, Runner, RunConfig from .tools import book_flight, book_uber agent_registry = cadence.Registry() @agent_registry.workflow(name="BookTripAgentWorkflow") class BookTripAgentWorkflow: @cadence.workflow.run async def run(self, input: str) -> str: long_trip_agent =Agent( name = "Plan Long Trip Agent", model = "gpt-4o-mini", instructions= """ Book a flight from start address to destination address. Use Uber to connect local address to airport or vice versa. """, tools = [ function_tool(book_flight), function_tool(book_uber), ], ) short_trip_agent =Agent( name = "Plan Short Trip Agent", instructions= """ Book a Uber ride from start address to destination address. """, model = "gpt-4o-mini", tools = [ function_tool(book_uber), ], ) # define agent using OpenAI SDK as usual agent =Agent( name = "Book Trip Agent", instructions = """ You are a trip planner. You can plan short or long trips. """, model = "gpt-4o-mini", handoffs = [ short_trip_agent, long_trip_agent, ], ) result = await Runner.run(agent, input, run_config=RunConfig( tracing_disabled=True, )) return result.final_output ================================================ FILE: python_sdk_samples/openai_samples/agent_handoffs/main.py ================================================ import asyncio import logging import signal import cadence from cadence.contrib.openai import PydanticDataConverter, cadence_registry from .tools import tools_registry from .book_trip_agent import agent_registry logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) async def main(): # start Cadence worker worker = cadence.worker.Worker( cadence.Client( domain="default", target="localhost:7833", data_converter=PydanticDataConverter(), ), "agent-task-list", cadence.Registry.of( cadence_registry.cadence_registry, tools_registry, agent_registry), ) # start BookFlightAgentWorkflow async with worker: logger.info("Worker started. Go to http://localhost:8088/domains/default/cluster0/workflows to start an agent run.") logger.info("Sample input: Book a trip for me from Uber Seattle Office to Uber San Francisco Office tomorrow at 10:00 AM") shutdown_event = asyncio.Event() loop = asyncio.get_running_loop() for sig in (signal.SIGTERM, signal.SIGINT): loop.add_signal_handler(sig, shutdown_event.set) logger.info("Press Ctrl+C to stop the worker.") await shutdown_event.wait() if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: python_sdk_samples/openai_samples/agent_handoffs/tools.py ================================================ import cadence from datetime import datetime from dataclasses import dataclass tools_registry = cadence.Registry() @dataclass class Flight: from_city: str to_city: str departure_date: datetime price: float airline: str flight_number: str seat_number: str @tools_registry.activity(name="book_flight") async def book_flight(from_city: str, to_city: str, departure_date: datetime) -> Flight: """ Book a Flight tool: a pure mock for demo purposes """ return Flight(from_city=from_city, to_city=to_city, departure_date=departure_date, price=100, airline="United", flight_number="123456", seat_number="12A") @dataclass class UberTrip: from_address: str to_address: str passengers: int price: float driver_name: str driver_phone: str driver_car: str driver_car_plate: str driver_car_color: str @tools_registry.activity(name="book_uber") async def book_uber(from_address: str, to_address: str, passengers: int) -> UberTrip: """ Book a Uber ride from start address to the destination address. default passengers is 1. """ return UberTrip(from_address=from_address, to_address=to_address, passengers=passengers, price=100, driver_name="John Doe", driver_phone="1234567890", driver_car="Toyota", driver_car_plate="1234567890", driver_car_color="Red") ================================================ FILE: python_sdk_samples/pyproject.toml ================================================ [project] name = "python-sdk-samples" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ "cadence-python-client[openai]>=0.2.1", "ruff>=0.15.10", ]