From 50e993ef971b840afb9f85bb6853fe4891dabebb Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Mon, 22 Jul 2024 14:33:36 -0700 Subject: [PATCH] Nexus (#1555) ## What was changed - Added the `temporalnexus` package and implemented the handler side for Nexus, including registering and dispatching Nexus Operations. - Added the ability to execute Nexus Operations from a workflow. - Added basic support for running Nexus Operations in the test environment. - Added memoizing to `worker.Start()` to return consistent errors to callers and avoid rerunning the function unnecessarily. - Updated the integration test's dev server to run CLI `0.14.0-nexus.0` which includes server `1.25.0-rc.0`. See the [proposal](https://github.com/temporalio/proposals/blob/b72c49b0c2278e916265b00a49638006f8fce469/nexus/sdk-go.md) for more information. Most of this code has been reviewed already in #1466, #1473, and #1475, which are all squashed in the first commit. --- .github/workflows/ci.yml | 3 + contrib/datadog/go.mod | 19 +- contrib/datadog/go.sum | 44 +- contrib/opentelemetry/go.mod | 13 +- contrib/opentelemetry/go.sum | 26 +- contrib/opentracing/go.mod | 17 +- contrib/opentracing/go.sum | 32 +- contrib/tally/go.mod | 17 +- contrib/tally/go.sum | 32 +- go.mod | 19 +- go.sum | 32 +- interceptor/interceptor.go | 5 + internal/client.go | 17 + internal/cmd/build/go.mod | 17 +- internal/cmd/build/go.sum | 32 +- internal/cmd/build/main.go | 10 + internal/common/metrics/constants.go | 9 + internal/common/metrics/tags.go | 9 + internal/error.go | 48 ++ internal/failure_converter.go | 20 + internal/interceptor.go | 23 + internal/interceptor_base.go | 18 + internal/internal_command_state_machine.go | 240 +++++- internal/internal_event_handlers.go | 127 +++ internal/internal_logging_tags.go | 3 + internal/internal_nexus_task_handler.go | 378 +++++++++ internal/internal_nexus_task_poller.go | 189 +++++ internal/internal_nexus_worker.go | 102 +++ internal/internal_task_handlers.go | 24 +- internal/internal_task_handlers_test.go | 2 +- internal/internal_worker.go | 108 ++- internal/internal_worker_base.go | 10 + internal/internal_workflow.go | 9 + internal/internal_workflow_client.go | 8 +- internal/internal_workflow_testsuite.go | 251 +++++- internal/nexus_operations.go | 421 ++++++++++ internal/worker.go | 11 + internal/workflow.go | 170 ++++ internal/workflow_testsuite.go | 6 + temporal/error.go | 5 + temporalnexus/example_test.go | 147 ++++ temporalnexus/operation.go | 292 +++++++ temporalnexus/operation_test.go | 93 +++ test/go.mod | 15 +- test/go.sum | 26 +- test/integration_test.go | 4 + test/nexus_test.go | 923 +++++++++++++++++++++ test/test_utils_test.go | 5 + test/worker_versioning_test.go | 13 + worker/worker.go | 10 + workflow/nexus_example_test.go | 72 ++ workflow/workflow.go | 40 + 52 files changed, 3991 insertions(+), 175 deletions(-) create mode 100644 internal/internal_nexus_task_handler.go create mode 100644 internal/internal_nexus_task_poller.go create mode 100644 internal/internal_nexus_worker.go create mode 100644 internal/nexus_operations.go create mode 100644 temporalnexus/example_test.go create mode 100644 temporalnexus/operation.go create mode 100644 temporalnexus/operation_test.go create mode 100644 test/nexus_test.go create mode 100644 workflow/nexus_example_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b8d26c50..35981104f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,9 @@ jobs: - name: Docker compose - integration tests if: ${{ matrix.testDockerCompose }} run: go run . integration-test + env: + # TODO(bergundy): Remove this flag once server 1.25.0 is out. + DISABLE_NEXUS_TESTS: "1" working-directory: ./internal/cmd/build cloud-test: diff --git a/contrib/datadog/go.mod b/contrib/datadog/go.mod index d4ed588ba..0905ca0ed 100644 --- a/contrib/datadog/go.mod +++ b/contrib/datadog/go.mod @@ -1,6 +1,8 @@ module go.temporal.io/sdk/contrib/datadog -go 1.20 +go 1.21 + +toolchain go1.21.1 require ( github.com/stretchr/testify v1.9.0 @@ -17,7 +19,7 @@ require ( github.com/DataDog/go-tuf v1.0.2-0.5.2 // indirect github.com/DataDog/sketches-go v1.4.2 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.5.0 // indirect @@ -28,6 +30,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/nexus-rpc/sdk-go v0.0.9 // indirect github.com/outcaste-io/ristretto v0.2.3 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/philhofer/fwd v1.1.2 // indirect @@ -37,22 +40,22 @@ require ( github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tinylib/msgp v1.1.8 // indirect - go.temporal.io/api v1.35.0 // indirect + go.temporal.io/api v1.36.0 // indirect go.uber.org/atomic v1.11.0 // indirect go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/grpc v1.64.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect diff --git a/contrib/datadog/go.sum b/contrib/datadog/go.sum index 4032f7674..c49876877 100644 --- a/contrib/datadog/go.sum +++ b/contrib/datadog/go.sum @@ -13,6 +13,7 @@ github.com/DataDog/go-libddwaf/v2 v2.1.0/go.mod h1:X/Kc+PpP1FvvfMJvsmh/YZwGHSnhI github.com/DataDog/go-tuf v1.0.2-0.5.2 h1:EeZr937eKAWPxJ26IykAdWA4A0jQXJgkhUjqEI/w7+I= github.com/DataDog/go-tuf v1.0.2-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0= github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= +github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= github.com/DataDog/sketches-go v1.4.2 h1:gppNudE9d19cQ98RYABOetxIhpTCl4m7CnbRZjvVA/o= github.com/DataDog/sketches-go v1.4.2/go.mod h1:xJIXldczJyyjnbDop7ZZcLxJdV3+7Kra7H1KMgpgkLk= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= @@ -21,8 +22,8 @@ github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -62,9 +63,11 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b h1:h9U78+dx9a4BKdQkBBos92HalKpaGKHrp+3Uo6yTodo= +github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -77,11 +80,16 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/nexus-rpc/sdk-go v0.0.9 h1:yQ16BlDWZ6EMjim/SMd8lsUGTj6TPxFioqLGP8/PJDQ= +github.com/nexus-rpc/sdk-go v0.0.9/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= 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/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0= github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= @@ -96,14 +104,17 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA= +github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk= 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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg= github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -126,8 +137,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.temporal.io/api v1.35.0 h1:+1o2zyBncLjnpjJt4FedKZMtuUav/LUgTB+mhQxx0zs= -go.temporal.io/api v1.35.0/go.mod h1:OYkuupuCw6s/5TkcKHMb9EcIrOI+vTsbf/CGaprbzb0= +go.temporal.io/api v1.36.0 h1:WdntOw9m38lFvMdMXuOO+3BQ0R8HpVLgtk9+f+FwiDk= +go.temporal.io/api v1.36.0/go.mod h1:0nWIrFRVPlcrkopXqxir/UWOtz/NZCo+EE9IX4UwVxw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -145,7 +156,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk 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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= @@ -172,8 +184,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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= @@ -202,8 +214,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= @@ -241,17 +253,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 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.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -262,6 +274,7 @@ gopkg.in/DataDog/dd-trace-go.v1 v1.58.1/go.mod h1:SmnEjjV9ZQr4MWRSUYEpoPyNtmtRK5 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -269,6 +282,7 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C 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/gotraceui v0.2.0 h1:dmNsfQ9Vl3GwbiVD7Z8d/osC6WtGGrasyrC2suc4ZIQ= +honnef.co/go/gotraceui v0.2.0/go.mod h1:qHo4/W75cA3bX0QQoSvDjbJa4R8mAyyFjbWAj63XElc= 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= inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a h1:1XCVEdxrvL6c0TGOhecLuB7U9zYNdxZEjvOqJreKZiM= diff --git a/contrib/opentelemetry/go.mod b/contrib/opentelemetry/go.mod index 92ca7475c..65c14cdcd 100644 --- a/contrib/opentelemetry/go.mod +++ b/contrib/opentelemetry/go.mod @@ -13,6 +13,7 @@ require ( require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/nexus-rpc/sdk-go v0.0.9 // indirect ) require ( @@ -29,15 +30,15 @@ require ( github.com/stretchr/objx v0.5.2 // indirect go.opentelemetry.io/otel/metric v1.27.0 go.opentelemetry.io/otel/sdk/metric v1.27.0 - go.temporal.io/api v1.35.0 // indirect + go.temporal.io/api v1.36.0 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/grpc v1.64.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/contrib/opentelemetry/go.sum b/contrib/opentelemetry/go.sum index 22111b9d5..1a5068dc3 100644 --- a/contrib/opentelemetry/go.sum +++ b/contrib/opentelemetry/go.sum @@ -52,6 +52,8 @@ 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/nexus-rpc/sdk-go v0.0.9 h1:yQ16BlDWZ6EMjim/SMd8lsUGTj6TPxFioqLGP8/PJDQ= +github.com/nexus-rpc/sdk-go v0.0.9/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 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= @@ -87,8 +89,8 @@ go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2N go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= -go.temporal.io/api v1.35.0 h1:+1o2zyBncLjnpjJt4FedKZMtuUav/LUgTB+mhQxx0zs= -go.temporal.io/api v1.35.0/go.mod h1:OYkuupuCw6s/5TkcKHMb9EcIrOI+vTsbf/CGaprbzb0= +go.temporal.io/api v1.36.0 h1:WdntOw9m38lFvMdMXuOO+3BQ0R8HpVLgtk9+f+FwiDk= +go.temporal.io/api v1.36.0/go.mod h1:0nWIrFRVPlcrkopXqxir/UWOtz/NZCo+EE9IX4UwVxw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -115,8 +117,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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= @@ -133,8 +135,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w 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-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -161,17 +163,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/contrib/opentracing/go.mod b/contrib/opentracing/go.mod index e70a60732..cc2ab1293 100644 --- a/contrib/opentracing/go.mod +++ b/contrib/opentracing/go.mod @@ -1,6 +1,8 @@ module go.temporal.io/sdk/contrib/opentracing -go 1.20 +go 1.21 + +toolchain go1.21.1 require ( github.com/opentracing/opentracing-go v1.2.0 @@ -16,19 +18,20 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/nexus-rpc/sdk-go v0.0.9 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/robfig/cron v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - go.temporal.io/api v1.35.0 // indirect + go.temporal.io/api v1.36.0 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/grpc v1.64.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/contrib/opentracing/go.sum b/contrib/opentracing/go.sum index 5d314139c..9475894cf 100644 --- a/contrib/opentracing/go.sum +++ b/contrib/opentracing/go.sum @@ -26,8 +26,10 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -40,9 +42,13 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/nexus-rpc/sdk-go v0.0.9 h1:yQ16BlDWZ6EMjim/SMd8lsUGTj6TPxFioqLGP8/PJDQ= +github.com/nexus-rpc/sdk-go v0.0.9/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= 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= @@ -55,6 +61,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: 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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 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= @@ -69,8 +76,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 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.temporal.io/api v1.35.0 h1:+1o2zyBncLjnpjJt4FedKZMtuUav/LUgTB+mhQxx0zs= -go.temporal.io/api v1.35.0/go.mod h1:OYkuupuCw6s/5TkcKHMb9EcIrOI+vTsbf/CGaprbzb0= +go.temporal.io/api v1.36.0 h1:WdntOw9m38lFvMdMXuOO+3BQ0R8HpVLgtk9+f+FwiDk= +go.temporal.io/api v1.36.0/go.mod h1:0nWIrFRVPlcrkopXqxir/UWOtz/NZCo+EE9IX4UwVxw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -97,8 +104,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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= @@ -115,8 +122,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w 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-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -143,22 +150,23 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/tally/go.mod b/contrib/tally/go.mod index 14cf6e0bf..df21f0b34 100644 --- a/contrib/tally/go.mod +++ b/contrib/tally/go.mod @@ -1,6 +1,8 @@ module go.temporal.io/sdk/contrib/tally -go 1.20 +go 1.21 + +toolchain go1.21.1 require ( github.com/stretchr/testify v1.9.0 @@ -16,21 +18,22 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/nexus-rpc/sdk-go v0.0.9 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/robfig/cron v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/twmb/murmur3 v1.1.5 // indirect - go.temporal.io/api v1.35.0 // indirect + go.temporal.io/api v1.36.0 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/grpc v1.64.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/contrib/tally/go.sum b/contrib/tally/go.sum index fffec5836..fc914fa7b 100644 --- a/contrib/tally/go.sum +++ b/contrib/tally/go.sum @@ -50,6 +50,7 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq 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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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= @@ -57,6 +58,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -78,9 +80,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv 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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 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= @@ -88,6 +92,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN 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/nexus-rpc/sdk-go v0.0.9 h1:yQ16BlDWZ6EMjim/SMd8lsUGTj6TPxFioqLGP8/PJDQ= +github.com/nexus-rpc/sdk-go v0.0.9/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 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= @@ -114,6 +120,7 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 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= @@ -134,8 +141,8 @@ github.com/uber-go/tally/v4 v4.1.1/go.mod h1:aXeSTDMl4tNosyf6rdU8jlgScHyjEGGtfJ/ 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.temporal.io/api v1.35.0 h1:+1o2zyBncLjnpjJt4FedKZMtuUav/LUgTB+mhQxx0zs= -go.temporal.io/api v1.35.0/go.mod h1:OYkuupuCw6s/5TkcKHMb9EcIrOI+vTsbf/CGaprbzb0= +go.temporal.io/api v1.36.0 h1:WdntOw9m38lFvMdMXuOO+3BQ0R8HpVLgtk9+f+FwiDk= +go.temporal.io/api v1.36.0/go.mod h1:0nWIrFRVPlcrkopXqxir/UWOtz/NZCo+EE9IX4UwVxw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -169,8 +176,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL 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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -198,8 +205,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w 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.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -227,17 +234,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 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= @@ -252,6 +259,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= 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= diff --git a/go.mod b/go.mod index b6414b2f2..f415ff773 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,35 @@ module go.temporal.io/sdk -go 1.20 +go 1.21 + +toolchain go1.21.1 require ( github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a github.com/gogo/protobuf v1.3.2 github.com/golang/mock v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 + github.com/nexus-rpc/sdk-go v0.0.9 github.com/pborman/uuid v1.2.1 github.com/robfig/cron v1.2.0 github.com/stretchr/testify v1.9.0 - go.temporal.io/api v1.35.0 - golang.org/x/sys v0.21.0 + go.temporal.io/api v1.36.0 + golang.org/x/sys v0.22.0 golang.org/x/time v0.3.0 - google.golang.org/grpc v1.64.0 + google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 09f96fce3..50d200fb8 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,10 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -40,9 +42,13 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/nexus-rpc/sdk-go v0.0.9 h1:yQ16BlDWZ6EMjim/SMd8lsUGTj6TPxFioqLGP8/PJDQ= +github.com/nexus-rpc/sdk-go v0.0.9/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 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= @@ -53,6 +59,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: 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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 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= @@ -67,8 +74,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 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.temporal.io/api v1.35.0 h1:+1o2zyBncLjnpjJt4FedKZMtuUav/LUgTB+mhQxx0zs= -go.temporal.io/api v1.35.0/go.mod h1:OYkuupuCw6s/5TkcKHMb9EcIrOI+vTsbf/CGaprbzb0= +go.temporal.io/api v1.36.0 h1:WdntOw9m38lFvMdMXuOO+3BQ0R8HpVLgtk9+f+FwiDk= +go.temporal.io/api v1.36.0/go.mod h1:0nWIrFRVPlcrkopXqxir/UWOtz/NZCo+EE9IX4UwVxw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -95,8 +102,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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= @@ -113,8 +120,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w 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-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -141,22 +148,23 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/interceptor/interceptor.go b/interceptor/interceptor.go index 73f9cadde..6061e88b1 100644 --- a/interceptor/interceptor.go +++ b/interceptor/interceptor.go @@ -131,6 +131,11 @@ type HandleQueryInput = internal.HandleQueryInput // NOTE: Experimental type UpdateInput = internal.UpdateInput +// RequestCancelNexusOperationInput is the input to WorkflowOutboundInterceptor.RequestCancelNexusOperation. +// +// NOTE: Experimental +type RequestCancelNexusOperationInput = internal.RequestCancelNexusOperationInput + // WorkflowOutboundInterceptor is an interface for all workflow calls // originating from the SDK. // diff --git a/internal/client.go b/internal/client.go index 263f96caa..345e82a09 100644 --- a/internal/client.go +++ b/internal/client.go @@ -701,6 +701,11 @@ type ( // of the delay will be ignored. A signal from signal with start will not trigger a workflow task. // Cannot be set the same time as a CronSchedule. StartDelay time.Duration + + // request ID. Only settable by the SDK - e.g. [temporalnexus.workflowRunOperation]. + requestID string + // workflow completion callback. Only settable by the SDK - e.g. [temporalnexus.workflowRunOperation]. + callbacks []*commonpb.Callback } // RetryPolicy defines the retry policy. @@ -1119,3 +1124,15 @@ func (e *WorkflowUpdateServiceTimeoutOrCanceledError) Error() string { } func (e *WorkflowUpdateServiceTimeoutOrCanceledError) Unwrap() error { return e.cause } + +// SetRequestIDOnStartWorkflowOptions is an internal only method for setting a requestID on StartWorkflowOptions. +// RequestID is purposefully not exposed to users for the time being. +func SetRequestIDOnStartWorkflowOptions(opts *StartWorkflowOptions, requestID string) { + opts.requestID = requestID +} + +// SetCallbacksOnStartWorkflowOptions is an internal only method for setting callbacks on StartWorkflowOptions. +// Callbacks are purposefully not exposed to users for the time being. +func SetCallbacksOnStartWorkflowOptions(opts *StartWorkflowOptions, callbacks []*commonpb.Callback) { + opts.callbacks = callbacks +} diff --git a/internal/cmd/build/go.mod b/internal/cmd/build/go.mod index b05cc1660..647c4b3bd 100644 --- a/internal/cmd/build/go.mod +++ b/internal/cmd/build/go.mod @@ -1,6 +1,8 @@ module go.temporal.io/sdk/internal/cmd/build -go 1.20 +go 1.21 + +toolchain go1.21.1 require ( github.com/BurntSushi/toml v1.3.2 @@ -17,24 +19,25 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/nexus-rpc/sdk-go v0.0.9 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/robfig/cron v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.9.0 // indirect - go.temporal.io/api v1.35.0 // indirect + go.temporal.io/api v1.36.0 // indirect golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect golang.org/x/exp/typeparams v0.0.0-20240409090435-93d18d7e34b8 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/grpc v1.64.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/cmd/build/go.sum b/internal/cmd/build/go.sum index 39c68d038..ef1e377c7 100644 --- a/internal/cmd/build/go.sum +++ b/internal/cmd/build/go.sum @@ -28,8 +28,10 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -44,9 +46,13 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/nexus-rpc/sdk-go v0.0.9 h1:yQ16BlDWZ6EMjim/SMd8lsUGTj6TPxFioqLGP8/PJDQ= +github.com/nexus-rpc/sdk-go v0.0.9/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 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= @@ -57,6 +63,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: 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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 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= @@ -71,8 +78,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 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.temporal.io/api v1.35.0 h1:+1o2zyBncLjnpjJt4FedKZMtuUav/LUgTB+mhQxx0zs= -go.temporal.io/api v1.35.0/go.mod h1:OYkuupuCw6s/5TkcKHMb9EcIrOI+vTsbf/CGaprbzb0= +go.temporal.io/api v1.36.0 h1:WdntOw9m38lFvMdMXuOO+3BQ0R8HpVLgtk9+f+FwiDk= +go.temporal.io/api v1.36.0/go.mod h1:0nWIrFRVPlcrkopXqxir/UWOtz/NZCo+EE9IX4UwVxw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -103,8 +110,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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= @@ -123,8 +130,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w 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-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -153,22 +160,23 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cmd/build/main.go b/internal/cmd/build/main.go index f75ed597b..ca055413a 100644 --- a/internal/cmd/build/main.go +++ b/internal/cmd/build/main.go @@ -139,6 +139,10 @@ func (b *builder) integrationTest() error { HostPort: "127.0.0.1:7233", Namespace: "integration-test-namespace", }, + // TODO(bergundy): Remove this override after server 1.25.0 is released. + CachedDownload: testsuite.CachedDownload{ + Version: "v0.14.0-nexus.0", + }, LogLevel: "warn", ExtraArgs: []string{ "--dynamic-config-value", "frontend.enableUpdateWorkflowExecution=true", @@ -151,6 +155,11 @@ func (b *builder) integrationTest() error { "--dynamic-config-value", "system.forceSearchAttributesCacheRefreshOnRead=true", "--dynamic-config-value", "worker.buildIdScavengerEnabled=true", "--dynamic-config-value", "worker.removableBuildIdDurationSinceDefault=1", + // All of the below is required for Nexus tests. + "--http-port", "7243", + "--dynamic-config-value", "system.enableNexus=true", + // SDK tests use arbitrary callback URLs, permit that on the server. + "--dynamic-config-value", `component.callbacks.allowedAddresses=[{"Pattern":"*","AllowInsecure":true}]`, }, }) if err != nil { @@ -174,6 +183,7 @@ func (b *builder) integrationTest() error { // Must run in test dir cmd := b.cmdFromRoot(args...) cmd.Dir = filepath.Join(cmd.Dir, "test") + cmd.Env = append(os.Environ(), "DISABLE_SERVER_1_25_TESTS=1") if err := b.runCmd(cmd); err != nil { return fmt.Errorf("integration test failed: %w", err) } diff --git a/internal/common/metrics/constants.go b/internal/common/metrics/constants.go index 83a816eab..eb901ea4b 100644 --- a/internal/common/metrics/constants.go +++ b/internal/common/metrics/constants.go @@ -81,6 +81,12 @@ const ( StickyCacheSize = TemporalMetricsPrefix + "sticky_cache_size" WorkflowActiveThreadCount = TemporalMetricsPrefix + "workflow_active_thread_count" + + NexusPollNoTaskCounter = TemporalMetricsPrefix + "nexus_poll_no_task" + NexusTaskScheduleToStartLatency = TemporalMetricsPrefix + "nexus_task_schedule_to_start_latency" + NexusTaskExecutionFailedCounter = TemporalMetricsPrefix + "nexus_task_execution_failed" + NexusTaskExecutionLatency = TemporalMetricsPrefix + "nexus_task_execution_latency" + NexusTaskEndToEndLatency = TemporalMetricsPrefix + "nexus_task_endtoend_latency" ) // Metric tag keys @@ -91,6 +97,8 @@ const ( WorkerTypeTagName = "worker_type" WorkflowTypeNameTagName = "workflow_type" ActivityTypeNameTagName = "activity_type" + NexusServiceTagName = "nexus_service" + NexusOperationTagName = "nexus_operation" TaskQueueTagName = "task_queue" OperationTagName = "operation" CauseTagName = "cause" @@ -105,4 +113,5 @@ const ( PollerTypeWorkflowTask = "workflow_task" PollerTypeWorkflowStickyTask = "workflow_sticky_task" PollerTypeActivityTask = "activity_task" + PollerTypeNexusTask = "nexus_task" ) diff --git a/internal/common/metrics/tags.go b/internal/common/metrics/tags.go index 3bdaf6fd6..7339af239 100644 --- a/internal/common/metrics/tags.go +++ b/internal/common/metrics/tags.go @@ -73,6 +73,15 @@ func LocalActivityTags(workflowType, activityType string) map[string]string { } } +// NexusTags returns a set of tags for Nexus Operations. +func NexusTags(service, operation, taskQueueName string) map[string]string { + return map[string]string{ + NexusServiceTagName: service, + NexusOperationTagName: operation, + TaskQueueTagName: taskQueueName, + } +} + // TaskQueueTags returns a set of tags for a task queue. func TaskQueueTags(taskQueue string) map[string]string { return map[string]string{ diff --git a/internal/error.go b/internal/error.go index 034728617..9dde77c49 100644 --- a/internal/error.go +++ b/internal/error.go @@ -256,6 +256,28 @@ type ( cause error } + // NexusOperationError is an error returned when a Nexus Operation has failed. + // + // NOTE: Experimental + NexusOperationError struct { + // The raw proto failure object this error was created from. + Failure *failurepb.Failure + // Error message. + Message string + // ID of the NexusOperationScheduled event. + ScheduledEventID int64 + // Endpoint name. + Endpoint string + // Service name. + Service string + // Operation name. + Operation string + // Operation ID - may be empty if the operation completed synchronously. + OperationID string + // Chained cause - typically an ApplicationError or a CanceledError. + Cause error + } + // ChildWorkflowExecutionAlreadyStartedError is set as the cause of // ChildWorkflowExecutionError when failure is due the child workflow having // already started. @@ -801,6 +823,32 @@ func (e *ChildWorkflowExecutionError) Unwrap() error { return e.cause } +// Error implements the error interface. +func (e *NexusOperationError) Error() string { + msg := fmt.Sprintf( + "%s (endpoint: %q, service: %q, operation: %q, operation ID: %q, scheduledEventID: %d)", + e.Message, e.Endpoint, e.Service, e.Operation, e.OperationID, e.ScheduledEventID) + if e.Cause != nil { + msg = fmt.Sprintf("%s: %v", msg, e.Cause) + } + return msg +} + +// setFailure implements the failureHolder interface for consistency with other failure based errors.. +func (e *NexusOperationError) setFailure(f *failurepb.Failure) { + e.Failure = f +} + +// failure implements the failureHolder interface for consistency with other failure based errors. +func (e *NexusOperationError) failure() *failurepb.Failure { + return e.Failure +} + +// Unwrap returns the Cause associated with this error. +func (e *NexusOperationError) Unwrap() error { + return e.Cause +} + // Error from error interface func (*NamespaceNotFoundError) Error() string { return "namespace not found" diff --git a/internal/failure_converter.go b/internal/failure_converter.go index d16063895..c306f1cc3 100644 --- a/internal/failure_converter.go +++ b/internal/failure_converter.go @@ -160,6 +160,15 @@ func (dfc *DefaultFailureConverter) ErrorToFailure(err error) *failurepb.Failure RetryState: err.retryState, } failure.FailureInfo = &failurepb.Failure_ChildWorkflowExecutionFailureInfo{ChildWorkflowExecutionFailureInfo: failureInfo} + case *NexusOperationError: + failureInfo := &failurepb.NexusOperationFailureInfo{ + ScheduledEventId: err.ScheduledEventID, + Endpoint: err.Endpoint, + Service: err.Service, + Operation: err.Operation, + OperationId: err.OperationID, + } + failure.FailureInfo = &failurepb.Failure_NexusOperationExecutionFailureInfo{NexusOperationExecutionFailureInfo: failureInfo} default: // All unknown errors are considered to be retryable ApplicationFailureInfo. failureInfo := &failurepb.ApplicationFailureInfo{ Type: getErrType(err), @@ -254,6 +263,17 @@ func (dfc *DefaultFailureConverter) FailureToError(failure *failurepb.Failure) e childWorkflowExecutionFailureInfo.GetRetryState(), dfc.FailureToError(failure.GetCause()), ) + } else if info := failure.GetNexusOperationExecutionFailureInfo(); info != nil { + err = &NexusOperationError{ + Message: failure.Message, + Cause: dfc.FailureToError(failure.GetCause()), + Failure: originalFailure, + ScheduledEventID: info.GetScheduledEventId(), + Endpoint: info.GetEndpoint(), + Service: info.GetService(), + Operation: info.GetOperation(), + OperationID: info.GetOperationId(), + } } if err == nil { diff --git a/internal/interceptor.go b/internal/interceptor.go index 9fb8238bb..51f64e989 100644 --- a/internal/interceptor.go +++ b/internal/interceptor.go @@ -169,6 +169,20 @@ type HandleQueryInput struct { Args []interface{} } +// RequestCancelNexusOperationInput is the input to WorkflowOutboundInterceptor.RequestCancelNexusOperation. +// +// NOTE: Experimental +type RequestCancelNexusOperationInput struct { + // Client that was used to start the operation. + Client NexusClient + // Operation name. + Operation any + // Operation ID. May be empty if the operation is synchronous or has not started yet. + ID string + // seq number. For internal use only. + seq int64 +} + // WorkflowOutboundInterceptor is an interface for all workflow calls // originating from the SDK. See documentation in the interceptor package for // more details. @@ -283,6 +297,15 @@ type WorkflowOutboundInterceptor interface { // interceptor.WorkflowHeader will return a non-nil map for this context. NewContinueAsNewError(ctx Context, wfn interface{}, args ...interface{}) error + // ExecuteNexusOperation intercepts NexusClient.ExecuteOperation. + // + // NOTE: Experimental + ExecuteNexusOperation(ctx Context, client NexusClient, operation any, input any, options NexusOperationOptions) NexusOperationFuture + // RequestCancelNexusOperation intercepts Nexus Operation cancelation via context. + // + // NOTE: Experimental + RequestCancelNexusOperation(ctx Context, input RequestCancelNexusOperationInput) + mustEmbedWorkflowOutboundInterceptorBase() } diff --git a/internal/interceptor_base.go b/internal/interceptor_base.go index 455007bd8..f000f0319 100644 --- a/internal/interceptor_base.go +++ b/internal/interceptor_base.go @@ -389,6 +389,24 @@ func (w *WorkflowOutboundInterceptorBase) NewContinueAsNewError( return w.Next.NewContinueAsNewError(ctx, wfn, args...) } +// ExecuteNexusOperation implements +// WorkflowOutboundInterceptor.ExecuteNexusOperation. +func (w *WorkflowOutboundInterceptorBase) ExecuteNexusOperation( + ctx Context, + client NexusClient, + operation any, + input any, + options NexusOperationOptions, +) NexusOperationFuture { + return w.Next.ExecuteNexusOperation(ctx, client, operation, input, options) +} + +// RequestCancelNexusOperation implements +// WorkflowOutboundInterceptor.RequestCancelNexusOperation. +func (w *WorkflowOutboundInterceptorBase) RequestCancelNexusOperation(ctx Context, input RequestCancelNexusOperationInput) { + w.Next.RequestCancelNexusOperation(ctx, input) +} + func (*WorkflowOutboundInterceptorBase) mustEmbedWorkflowOutboundInterceptorBase() {} // ClientInterceptorBase is a default implementation of ClientInterceptor meant diff --git a/internal/internal_command_state_machine.go b/internal/internal_command_state_machine.go index c23be27a7..0a3ac6abb 100644 --- a/internal/internal_command_state_machine.go +++ b/internal/internal_command_state_machine.go @@ -132,6 +132,35 @@ type ( *naiveCommandStateMachine } + // nexusOperationStateMachine is the state machine for the NexusOperation lifecycle. + // It may never transition to the started state if the operation completes synchronously. + // Valid transitions: + // commandStateCreated -> commandStateCommandSent + // commandStateCommandSent - (NexusOperationScheduled) -> commandStateInitiated + // commandStateInitiated - (NexusOperationStarted) -> commandStateStarted + // commandStateInitiated - (NexusOperation(Completed|Failed|Canceled|TimedOut)) -> commandStateCompleted + // commandStateStarted - (NexusOperation(Completed|Failed|Canceled|TimedOut)) -> commandStateCompleted + nexusOperationStateMachine struct { + *commandStateMachineBase + // Unique sequence number for identifying this machine SDK side. + seq int64 + // Event ID of the NexusOperationScheduled event for correlating progress events with this machine. + scheduledEventID int64 + attributes *commandpb.ScheduleNexusOperationCommandAttributes + // Instead of tracking cancelation as a state, we track it as a separate dimension with the request-cancel state + // machine. + cancelation *requestCancelNexusOperationStateMachine + } + + // requestCancelNexusOperationStateMachine is the state machine for the RequestCancelNexusOperation command. + // Valid transitions: + // commandStateCreated -> commandStateCommandSent + // commandStateCommandSent - (NexusOperationCancelRequested) -> commandStateCompleted + requestCancelNexusOperationStateMachine struct { + *commandStateMachineBase + attributes *commandpb.RequestCancelNexusOperationCommandAttributes + } + versionMarker struct { changeID string searchAttrUpdated bool @@ -146,6 +175,15 @@ type ( scheduledEventIDToCancellationID map[int64]string scheduledEventIDToSignalID map[int64]string versionMarkerLookup map[int64]versionMarker + + // A mapping of scheduled event ID to a sequence. + scheduledEventIDToNexusSeq map[int64]int64 + // A list containing all nexus operation machines that have not yet been assigned a scheduled event ID. + // Every new operation state machine is added to this list on creation and deleted once the scheduled event is + // seen or the operation was deleted before sending the command. + // This mechanism is based on Core SDK + // (https://github.com/temporalio/sdk-core/blob/16c7a33dc1aec8fafb33c9ad6f77569a3dacc8ea/core/src/worker/workflow/machines/workflow_machines.rs#L837). + nexusOperationsWithoutScheduledID *list.List } // panic when command or message state machine is in illegal state @@ -176,20 +214,22 @@ const ( ) const ( - commandTypeActivity commandType = 0 - commandTypeChildWorkflow commandType = 1 - commandTypeCancellation commandType = 2 - commandTypeMarker commandType = 3 - commandTypeTimer commandType = 4 - commandTypeSignal commandType = 5 - commandTypeUpsertSearchAttributes commandType = 6 - commandTypeCancelTimer commandType = 7 - commandTypeRequestCancelActivityTask commandType = 8 - commandTypeAcceptWorkflowUpdate commandType = 9 - commandTypeCompleteWorkflowUpdate commandType = 10 - commandTypeModifyProperties commandType = 11 - commandTypeRejectWorkflowUpdate commandType = 12 - commandTypeProtocolMessage commandType = 13 + commandTypeActivity commandType = 0 + commandTypeChildWorkflow commandType = 1 + commandTypeCancellation commandType = 2 + commandTypeMarker commandType = 3 + commandTypeTimer commandType = 4 + commandTypeSignal commandType = 5 + commandTypeUpsertSearchAttributes commandType = 6 + commandTypeCancelTimer commandType = 7 + commandTypeRequestCancelActivityTask commandType = 8 + commandTypeAcceptWorkflowUpdate commandType = 9 + commandTypeCompleteWorkflowUpdate commandType = 10 + commandTypeModifyProperties commandType = 11 + commandTypeRejectWorkflowUpdate commandType = 12 + commandTypeProtocolMessage commandType = 13 + commandTypeNexusOperation commandType = 14 + commandTypeRequestCancelNexusOperation commandType = 15 ) const ( @@ -276,6 +316,10 @@ func (d commandType) String() string { return "CompleteWorkflowUpdate" case commandTypeRejectWorkflowUpdate: return "RejectWorkflowUpdate" + case commandTypeNexusOperation: + return "NexusOperation" + case commandTypeRequestCancelNexusOperation: + return "RequestCancelNexusOperation" default: return "Unknown" } @@ -318,6 +362,29 @@ func (h *commandsHelper) newCancelActivityStateMachine(attributes *commandpb.Req } } +func (h *commandsHelper) newNexusOperationStateMachine( + seq int64, + attributes *commandpb.ScheduleNexusOperationCommandAttributes, +) *nexusOperationStateMachine { + base := h.newCommandStateMachineBase(commandTypeNexusOperation, strconv.FormatInt(seq, 10)) + sm := &nexusOperationStateMachine{ + commandStateMachineBase: base, + attributes: attributes, + seq: seq, + // scheduledEventID will be assigned by the server when the corresponding event comes in. + } + h.nexusOperationsWithoutScheduledID.PushBack(sm) + return sm +} + +func (h *commandsHelper) newRequestCancelNexusOperationStateMachine(attributes *commandpb.RequestCancelNexusOperationCommandAttributes) *requestCancelNexusOperationStateMachine { + base := h.newCommandStateMachineBase(commandTypeRequestCancelNexusOperation, strconv.FormatInt(attributes.GetScheduledEventId(), 10)) + return &requestCancelNexusOperationStateMachine{ + commandStateMachineBase: base, + attributes: attributes, + } +} + func (h *commandsHelper) newTimerCommandStateMachine(attributes *commandpb.StartTimerCommandAttributes) *timerCommandStateMachine { base := h.newCommandStateMachineBase(commandTypeTimer, attributes.GetTimerId()) return &timerCommandStateMachine{ @@ -853,15 +920,87 @@ func (d *modifyPropertiesCommandStateMachine) handleCommandSent() { } } +func (sm *nexusOperationStateMachine) getCommand() *commandpb.Command { + if sm.state == commandStateCreated && sm.cancelation == nil { + // Only create the command in this state unlike other machines that also create it if canceled before sent. + return &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, + Attributes: &commandpb.Command_ScheduleNexusOperationCommandAttributes{ + ScheduleNexusOperationCommandAttributes: sm.attributes, + }, + } + } + return nil +} + +func (sm *nexusOperationStateMachine) handleStartedEvent() { + switch sm.state { + case commandStateInitiated: + sm.moveState(commandStateStarted, eventStarted) + default: + sm.failStateTransition(eventStarted) + } +} + +func (sm *nexusOperationStateMachine) handleCompletionEvent() { + switch sm.state { + case commandStateInitiated, + commandStateStarted: + sm.moveState(commandStateCompleted, eventCompletion) + default: + sm.failStateTransition(eventStarted) + } +} + +func (sm *nexusOperationStateMachine) cancel() { + // Already canceled or already completed. + if sm.cancelation != nil || sm.state == commandStateCompleted { + return + } + + attribs := &commandpb.RequestCancelNexusOperationCommandAttributes{ + ScheduledEventId: sm.scheduledEventID, + } + cancelCmd := sm.helper.newRequestCancelNexusOperationStateMachine(attribs) + sm.cancelation = cancelCmd + sm.helper.addCommand(cancelCmd) + + // No need to actually send the cancelation, mark the state machine as completed. + if sm.state == commandStateCreated { + cancelCmd.handleCompletionEvent() + } +} + +func (d *requestCancelNexusOperationStateMachine) getCommand() *commandpb.Command { + switch d.state { + case commandStateCreated: + command := createNewCommand(enumspb.COMMAND_TYPE_REQUEST_CANCEL_NEXUS_OPERATION) + command.Attributes = &commandpb.Command_RequestCancelNexusOperationCommandAttributes{RequestCancelNexusOperationCommandAttributes: d.attributes} + return command + default: + return nil + } +} + +func (d *requestCancelNexusOperationStateMachine) handleCompletionEvent() { + if d.state != commandStateCommandSent && d.state != commandStateCreated { + d.failStateTransition(eventCompletion) + return + } + d.moveState(commandStateCompleted, eventCompletion) +} + func newCommandsHelper() *commandsHelper { return &commandsHelper{ orderedCommands: list.New(), commands: make(map[commandID]*list.Element), - scheduledEventIDToActivityID: make(map[int64]string), - scheduledEventIDToCancellationID: make(map[int64]string), - scheduledEventIDToSignalID: make(map[int64]string), - versionMarkerLookup: make(map[int64]versionMarker), + scheduledEventIDToActivityID: make(map[int64]string), + scheduledEventIDToCancellationID: make(map[int64]string), + scheduledEventIDToSignalID: make(map[int64]string), + versionMarkerLookup: make(map[int64]versionMarker), + scheduledEventIDToNexusSeq: make(map[int64]int64), + nexusOperationsWithoutScheduledID: list.New(), } } @@ -1040,6 +1179,71 @@ func (h *commandsHelper) getActivityAndScheduledEventIDs(event *historypb.Histor return activityID, scheduledEventID } +func (h *commandsHelper) scheduleNexusOperation( + seq int64, + attributes *commandpb.ScheduleNexusOperationCommandAttributes, +) *nexusOperationStateMachine { + command := h.newNexusOperationStateMachine(seq, attributes) + h.addCommand(command) + return command +} + +func (h *commandsHelper) handleNexusOperationScheduled(event *historypb.HistoryEvent) { + elem := h.nexusOperationsWithoutScheduledID.Front() + if elem == nil { + panicIllegalState(fmt.Sprintf("[TMPRL1100] unable to find nexus operation state machine for event: %v", util.HistoryEventToString(event))) + } + command := h.nexusOperationsWithoutScheduledID.Remove(elem).(*nexusOperationStateMachine) + + command.scheduledEventID = event.EventId + h.scheduledEventIDToNexusSeq[event.EventId] = command.seq + command.handleInitiatedEvent() +} + +func (h *commandsHelper) handleNexusOperationStarted(scheduledEventID int64) commandStateMachine { + seq, ok := h.scheduledEventIDToNexusSeq[scheduledEventID] + if !ok { + panicIllegalState(fmt.Sprintf("[TMPRL1100] unable to find nexus operation state machine for event ID: %v", scheduledEventID)) + } + command := h.getCommand(makeCommandID(commandTypeNexusOperation, strconv.FormatInt(seq, 10))) + command.handleStartedEvent() + return command +} + +func (h *commandsHelper) handleNexusOperationCompleted(scheduledEventID int64) commandStateMachine { + seq, ok := h.scheduledEventIDToNexusSeq[scheduledEventID] + if !ok { + panicIllegalState(fmt.Sprintf("[TMPRL1100] unable to find nexus operation state machine for event ID: %v", scheduledEventID)) + } + // We don't need this anymore, the state will not transition after completion. + delete(h.scheduledEventIDToNexusSeq, scheduledEventID) + command := h.getCommand(makeCommandID(commandTypeNexusOperation, strconv.FormatInt(seq, 10))) + command.handleCompletionEvent() + return command +} + +func (h *commandsHelper) handleNexusOperationCancelRequested(scheduledEventID int64) { + command := h.getCommand(makeCommandID(commandTypeRequestCancelNexusOperation, strconv.FormatInt(scheduledEventID, 10))) + command.handleCompletionEvent() +} + +func (h *commandsHelper) requestCancelNexusOperation(seq int64) commandStateMachine { + command := h.getCommand(makeCommandID(commandTypeNexusOperation, strconv.FormatInt(seq, 10))) + command.cancel() + // If we haven't sent the command yet, ensure that it doesn't get mapped to the wrong scheduledEventID. + if command.getState() != commandStateCanceledBeforeSent { + return command + } + for elem := h.nexusOperationsWithoutScheduledID.Front(); elem != nil; elem = elem.Next() { + sm := elem.Value.(*nexusOperationStateMachine) + if sm.seq == seq { + h.nexusOperationsWithoutScheduledID.Remove(elem) + break + } + } + return command +} + func (h *commandsHelper) recordVersionMarker(changeID string, version Version, dc converter.DataConverter, searchAttributeWasUpdated bool) commandStateMachine { markerID := fmt.Sprintf("%v_%v", versionMarkerName, changeID) diff --git a/internal/internal_event_handlers.go b/internal/internal_event_handlers.go index d60e82d2b..b382aa720 100644 --- a/internal/internal_event_handlers.go +++ b/internal/internal_event_handlers.go @@ -84,6 +84,14 @@ type ( activityType ActivityType } + scheduledNexusOperation struct { + startedCallback func(operationID string, err error) + completedCallback func(result *commonpb.Payload, err error) + endpoint string + service string + operation string + } + scheduledChildWorkflow struct { resultCallback ResultHandler startedCallback func(r WorkflowExecution, e error) @@ -613,6 +621,57 @@ func (wc *workflowEnvironmentImpl) ExecuteChildWorkflow( tagWorkflowType, params.WorkflowType.Name) } +func (wc *workflowEnvironmentImpl) ExecuteNexusOperation(params executeNexusOperationParams, callback func(*commonpb.Payload, error), startedHandler func(opID string, e error)) int64 { + seq := wc.GenerateSequence() + scheduleTaskAttr := &commandpb.ScheduleNexusOperationCommandAttributes{ + Endpoint: params.client.Endpoint(), + Service: params.client.Service(), + Operation: params.operation, + Input: params.input, + ScheduleToCloseTimeout: durationpb.New(params.options.ScheduleToCloseTimeout), + NexusHeader: params.nexusHeader, + } + + command := wc.commandsHelper.scheduleNexusOperation(seq, scheduleTaskAttr) + command.setData(&scheduledNexusOperation{ + startedCallback: startedHandler, + completedCallback: callback, + endpoint: params.client.Endpoint(), + service: params.client.Service(), + operation: params.operation, + }) + + wc.logger.Debug("ScheduleNexusOperation", + tagNexusEndpoint, params.client.Endpoint(), + tagNexusService, params.client.Service(), + tagNexusOperation, params.operation, + ) + + return command.seq +} + +func (wc *workflowEnvironmentImpl) RequestCancelNexusOperation(seq int64) { + command := wc.commandsHelper.requestCancelNexusOperation(seq) + data := command.getData().(*scheduledNexusOperation) + + // Make sure to unblock the futures. + if command.getState() == commandStateCreated || command.getState() == commandStateCommandSent { + if data.startedCallback != nil { + data.startedCallback("", ErrCanceled) + data.startedCallback = nil + } + if data.completedCallback != nil { + data.completedCallback(nil, ErrCanceled) + data.completedCallback = nil + } + } + wc.logger.Debug("RequestCancelNexusOperation", + tagNexusEndpoint, data.endpoint, + tagNexusService, data.service, + tagNexusOperation, data.operation, + ) +} + func (wc *workflowEnvironmentImpl) RegisterSignalHandler( handler func(name string, input *commonpb.Payloads, header *commonpb.Header) error, ) { @@ -1260,6 +1319,19 @@ func (weh *workflowExecutionEventHandlerImpl) ProcessEvent( case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED: // No Operation + case enumspb.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED: + weh.commandsHelper.handleNexusOperationScheduled(event) + case enumspb.EVENT_TYPE_NEXUS_OPERATION_STARTED: + err = weh.handleNexusOperationStarted(event) + // all forms of completions are handled by the same method. + case enumspb.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, + enumspb.EVENT_TYPE_NEXUS_OPERATION_FAILED, + enumspb.EVENT_TYPE_NEXUS_OPERATION_CANCELED, + enumspb.EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT: + err = weh.handleNexusOperationCompleted(event) + case enumspb.EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED: + weh.commandsHelper.handleNexusOperationCancelRequested(event.GetNexusOperationCancelRequestedEventAttributes().GetScheduledEventId()) + default: if event.WorkerMayIgnore { // Do not fail to be forward compatible with new events @@ -1820,6 +1892,61 @@ func (weh *workflowExecutionEventHandlerImpl) handleChildWorkflowExecutionTermin return nil } +func (weh *workflowExecutionEventHandlerImpl) handleNexusOperationStarted(event *historypb.HistoryEvent) error { + attributes := event.GetNexusOperationStartedEventAttributes() + command := weh.commandsHelper.handleNexusOperationStarted(attributes.ScheduledEventId) + state := command.getData().(*scheduledNexusOperation) + if state.startedCallback != nil { + state.startedCallback(attributes.OperationId, nil) + state.startedCallback = nil + } + return nil +} + +func (weh *workflowExecutionEventHandlerImpl) handleNexusOperationCompleted(event *historypb.HistoryEvent) error { + var result *commonpb.Payload + var failure *failurepb.Failure + var scheduledEventId int64 + + switch event.EventType { + case enumspb.EVENT_TYPE_NEXUS_OPERATION_COMPLETED: + attrs := event.GetNexusOperationCompletedEventAttributes() + result = attrs.GetResult() + scheduledEventId = attrs.GetScheduledEventId() + case enumspb.EVENT_TYPE_NEXUS_OPERATION_FAILED: + attrs := event.GetNexusOperationFailedEventAttributes() + failure = attrs.GetFailure() + scheduledEventId = attrs.GetScheduledEventId() + case enumspb.EVENT_TYPE_NEXUS_OPERATION_CANCELED: + attrs := event.GetNexusOperationCanceledEventAttributes() + failure = attrs.GetFailure() + scheduledEventId = attrs.GetScheduledEventId() + case enumspb.EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT: + attrs := event.GetNexusOperationTimedOutEventAttributes() + failure = attrs.GetFailure() + scheduledEventId = attrs.GetScheduledEventId() + default: + // This is only called internally and should never happen. + panic(fmt.Errorf("invalid event type, not a Nexus Operation resolution: %v", event.EventType)) + } + command := weh.commandsHelper.handleNexusOperationCompleted(scheduledEventId) + state := command.getData().(*scheduledNexusOperation) + var err error + if failure != nil { + err = weh.failureConverter.FailureToError(failure) + } + // Also unblock the start future + if state.startedCallback != nil { + state.startedCallback("", err) // We didn't get a started event, the operation completed synchronously. + state.startedCallback = nil + } + if state.completedCallback != nil { + state.completedCallback(result, err) + state.completedCallback = nil + } + return nil +} + func (weh *workflowExecutionEventHandlerImpl) handleUpsertWorkflowSearchAttributes(event *historypb.HistoryEvent) { weh.updateWorkflowInfoWithSearchAttributes(event.GetUpsertWorkflowSearchAttributesEventAttributes().SearchAttributes) } diff --git a/internal/internal_logging_tags.go b/internal/internal_logging_tags.go index 422e676d6..99b7f93c0 100644 --- a/internal/internal_logging_tags.go +++ b/internal/internal_logging_tags.go @@ -49,6 +49,9 @@ const ( tagTaskStartedEventID = "TaskStartedEventID" tagPreviousStartedEventID = "PreviousStartedEventID" tagCachedPreviousStartedEventID = "CachedPreviousStartedEventID" + tagNexusEndpoint = "NexusEndpoint" + tagNexusOperation = "NexusOperation" + tagNexusService = "NexusService" tagPanicError = "PanicError" tagPanicStack = "PanicStack" ) diff --git a/internal/internal_nexus_task_handler.go b/internal/internal_nexus_task_handler.go new file mode 100644 index 000000000..0d20e8c51 --- /dev/null +++ b/internal/internal_nexus_task_handler.go @@ -0,0 +1,378 @@ +// The MIT License +// +// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved. +// +// 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. + +package internal + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "reflect" + "runtime/debug" + "time" + + "github.com/nexus-rpc/sdk-go/nexus" + "go.temporal.io/api/common/v1" + nexuspb "go.temporal.io/api/nexus/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/converter" + "go.temporal.io/sdk/internal/common/metrics" + "go.temporal.io/sdk/log" +) + +func nexusHandlerError(t nexus.HandlerErrorType, message string) *nexuspb.HandlerError { + return &nexuspb.HandlerError{ + ErrorType: string(t), + Failure: &nexuspb.Failure{ + Message: message, + }, + } +} + +func nexusHandlerErrorToProto(handlerErr *nexus.HandlerError) *nexuspb.HandlerError { + pbHandlerErr := &nexuspb.HandlerError{ + ErrorType: string(handlerErr.Type), + } + if handlerErr.Failure != nil { + pbHandlerErr.Failure = &nexuspb.Failure{ + Message: handlerErr.Failure.Message, + Metadata: handlerErr.Failure.Metadata, + Details: handlerErr.Failure.Details, + } + } + return pbHandlerErr +} + +type nexusTaskHandler struct { + nexusHandler nexus.Handler + identity string + namespace string + taskQueueName string + client Client + dataConverter converter.DataConverter + logger log.Logger + metricsHandler metrics.Handler +} + +func newNexusTaskHandler( + nexusHandler nexus.Handler, + identity string, + namespace string, + taskQueueName string, + client Client, + dataConverter converter.DataConverter, + logger log.Logger, + metricsHandler metrics.Handler, +) *nexusTaskHandler { + return &nexusTaskHandler{ + nexusHandler: nexusHandler, + logger: logger, + dataConverter: dataConverter, + identity: identity, + namespace: namespace, + taskQueueName: taskQueueName, + client: client, + metricsHandler: metricsHandler, + } +} + +func (h *nexusTaskHandler) Execute(task *workflowservice.PollNexusTaskQueueResponse) (*workflowservice.RespondNexusTaskCompletedRequest, *workflowservice.RespondNexusTaskFailedRequest, error) { + res, handlerErr, err := h.execute(task) + if err != nil { + return nil, nil, err + } + if handlerErr != nil { + return nil, h.fillInFailure(task.TaskToken, handlerErr), nil + } + return h.fillInCompletion(task.TaskToken, res), nil, nil +} + +func (h *nexusTaskHandler) execute(task *workflowservice.PollNexusTaskQueueResponse) (*nexuspb.Response, *nexuspb.HandlerError, error) { + log, handlerErr := h.loggerForTask(task) + if handlerErr != nil { + return nil, handlerErr, nil + } + nctx := &NexusOperationContext{ + Client: h.client, + TaskQueue: h.taskQueueName, + Log: log, + } + header := nexus.Header(task.GetRequest().GetHeader()) + if header == nil { + header = nexus.Header{} + } + + ctx, cancel, handlerErr := h.goContextForTask(nctx, header) + if handlerErr != nil { + return nil, handlerErr, nil + } + defer cancel() + + switch req := task.GetRequest().GetVariant().(type) { + case *nexuspb.Request_StartOperation: + return h.handleStartOperation(ctx, nctx, req.StartOperation, header) + case *nexuspb.Request_CancelOperation: + return h.handleCancelOperation(ctx, nctx, req.CancelOperation, header) + default: + return nil, nexusHandlerError(nexus.HandlerErrorTypeNotImplemented, "unknown request type"), nil + } +} + +func (h *nexusTaskHandler) handleStartOperation(ctx context.Context, nctx *NexusOperationContext, req *nexuspb.StartOperationRequest, header nexus.Header) (*nexuspb.Response, *nexuspb.HandlerError, error) { + serializer := &payloadSerializer{ + converter: h.dataConverter, + payload: req.GetPayload(), + } + // Create a fake lazy value, Temporal server already converts Nexus content into payloads. + input := nexus.NewLazyValue( + serializer, + &nexus.Reader{ + ReadCloser: emptyReaderNopCloser, + }, + ) + // Ensure we don't pass nil values to handlers. + callbackHeader := req.GetCallbackHeader() + if callbackHeader == nil { + callbackHeader = make(map[string]string) + } + startOptions := nexus.StartOperationOptions{ + RequestID: req.RequestId, + CallbackURL: req.Callback, + Header: header, + CallbackHeader: callbackHeader, + } + var opres nexus.HandlerStartOperationResult[any] + var err error + func() { + defer func() { + recovered := recover() + if recovered != nil { + var ok bool + err, ok = recovered.(error) + if !ok { + err = fmt.Errorf("panic: %v", recovered) + } + + nctx.Log.Error("Panic captured while handling nexus task", tagStackTrace, string(debug.Stack()), tagError, err) + } + }() + opres, err = h.nexusHandler.StartOperation(ctx, req.GetService(), req.GetOperation(), input, startOptions) + }() + if ctx.Err() != nil { + return nil, nil, ctx.Err() + } + if err != nil { + var unsuccessfulOperationErr *nexus.UnsuccessfulOperationError + if errors.As(err, &unsuccessfulOperationErr) { + return &nexuspb.Response{ + Variant: &nexuspb.Response_StartOperation{ + StartOperation: &nexuspb.StartOperationResponse{ + Variant: &nexuspb.StartOperationResponse_OperationError{ + OperationError: &nexuspb.UnsuccessfulOperationError{ + OperationState: string(unsuccessfulOperationErr.State), + Failure: &nexuspb.Failure{ + Message: unsuccessfulOperationErr.Failure.Message, + Metadata: unsuccessfulOperationErr.Failure.Metadata, + Details: unsuccessfulOperationErr.Failure.Details, + }, + }, + }, + }, + }, + }, nil, nil + } + var handlerErr *nexus.HandlerError + if errors.As(err, &handlerErr) { + return nil, nexusHandlerErrorToProto(handlerErr), nil + } + // Default to internal error. + return nil, h.internalError(err), nil + } + switch t := opres.(type) { + case *nexus.HandlerStartOperationResultAsync: + return &nexuspb.Response{ + Variant: &nexuspb.Response_StartOperation{ + StartOperation: &nexuspb.StartOperationResponse{ + Variant: &nexuspb.StartOperationResponse_AsyncSuccess{ + AsyncSuccess: &nexuspb.StartOperationResponse_Async{OperationId: t.OperationID}, + }, + }, + }, + }, nil, nil + default: + // *nexus.HandlerStartOperationResultSync is generic, we can't type switch unfortunately. + value := reflect.ValueOf(t).Elem().FieldByName("Value").Interface() + payload, err := h.dataConverter.ToPayload(value) + if err != nil { + return nil, h.internalError(fmt.Errorf("cannot convert nexus sync result: %w", err)), nil + } + return &nexuspb.Response{ + Variant: &nexuspb.Response_StartOperation{ + StartOperation: &nexuspb.StartOperationResponse{ + Variant: &nexuspb.StartOperationResponse_SyncSuccess{ + SyncSuccess: &nexuspb.StartOperationResponse_Sync{ + Payload: payload, + }, + }, + }, + }, + }, nil, nil + } +} + +func (h *nexusTaskHandler) handleCancelOperation(ctx context.Context, nctx *NexusOperationContext, req *nexuspb.CancelOperationRequest, header nexus.Header) (*nexuspb.Response, *nexuspb.HandlerError, error) { + cancelOptions := nexus.CancelOperationOptions{Header: header} + var err error + func() { + defer func() { + recovered := recover() + if recovered != nil { + var ok bool + err, ok = recovered.(error) + if !ok { + err = fmt.Errorf("panic: %v", recovered) + } + + nctx.Log.Error("Panic captured while handling nexus task", tagStackTrace, string(debug.Stack()), tagError, err) + } + }() + err = h.nexusHandler.CancelOperation(ctx, req.GetService(), req.GetOperation(), req.GetOperationId(), cancelOptions) + }() + if ctx.Err() != nil { + return nil, nil, ctx.Err() + } + if err != nil { + var handlerErr *nexus.HandlerError + if errors.As(err, &handlerErr) { + return nil, nexusHandlerErrorToProto(handlerErr), nil + } + // Default to internal error. + return nil, h.internalError(err), nil + } + + return &nexuspb.Response{ + Variant: &nexuspb.Response_CancelOperation{ + CancelOperation: &nexuspb.CancelOperationResponse{}, + }, + }, nil, nil +} + +func (h *nexusTaskHandler) internalError(err error) *nexuspb.HandlerError { + h.logger.Error("error processing nexus task", "error", err) + return nexusHandlerError(nexus.HandlerErrorTypeInternal, "internal error") +} + +func (h *nexusTaskHandler) goContextForTask(nctx *NexusOperationContext, header nexus.Header) (context.Context, context.CancelFunc, *nexuspb.HandlerError) { + // Associate the NexusOperationContext with the context.Context used to invoke operations. + ctx := context.WithValue(context.Background(), nexusOperationContextKey, nctx) + + timeoutStr := header.Get(nexus.HeaderRequestTimeout) + if timeoutStr != "" { + timeout, err := time.ParseDuration(timeoutStr) + if err != nil { + return nil, nil, nexusHandlerError(nexus.HandlerErrorTypeBadRequest, "cannot parse request timeout") + } + ctx, cancel := context.WithTimeout(ctx, timeout) + return ctx, cancel, nil + } + + return ctx, func() {}, nil +} + +func (h *nexusTaskHandler) loggerForTask(response *workflowservice.PollNexusTaskQueueResponse) (log.Logger, *nexuspb.HandlerError) { + var service, operation string + + switch req := response.GetRequest().GetVariant().(type) { + case *nexuspb.Request_StartOperation: + service = req.StartOperation.Service + operation = req.StartOperation.Operation + case *nexuspb.Request_CancelOperation: + service = req.CancelOperation.Service + operation = req.CancelOperation.Operation + default: + return nil, nexusHandlerError(nexus.HandlerErrorTypeNotImplemented, "unknown request type") + } + + return log.With(h.logger, + tagNexusService, service, + tagNexusOperation, operation, + ), nil +} + +func (h *nexusTaskHandler) metricsHandlerForTask(response *workflowservice.PollNexusTaskQueueResponse) (metrics.Handler, *nexuspb.HandlerError) { + var service, operation string + + switch req := response.GetRequest().GetVariant().(type) { + case *nexuspb.Request_StartOperation: + service = req.StartOperation.Service + operation = req.StartOperation.Operation + case *nexuspb.Request_CancelOperation: + service = req.CancelOperation.Service + operation = req.CancelOperation.Operation + default: + return nil, &nexuspb.HandlerError{ + ErrorType: string(nexus.HandlerErrorTypeNotImplemented), + Failure: &nexuspb.Failure{ + Message: "unknown request type", + }, + } + } + + return h.metricsHandler.WithTags(metrics.NexusTags(service, operation, h.taskQueueName)), nil +} + +func (h *nexusTaskHandler) fillInCompletion(taskToken []byte, res *nexuspb.Response) *workflowservice.RespondNexusTaskCompletedRequest { + return &workflowservice.RespondNexusTaskCompletedRequest{ + Identity: h.identity, + Namespace: h.namespace, + TaskToken: taskToken, + Response: res, + } +} + +func (h *nexusTaskHandler) fillInFailure(taskToken []byte, err *nexuspb.HandlerError) *workflowservice.RespondNexusTaskFailedRequest { + return &workflowservice.RespondNexusTaskFailedRequest{ + Identity: h.identity, + Namespace: h.namespace, + TaskToken: taskToken, + Error: err, + } +} + +// payloadSerializer is a fake nexus Serializer that uses a data converter to read from an embedded payload instead of +// using the given nexus.Context. Supports only Deserialize. +type payloadSerializer struct { + converter converter.DataConverter + payload *common.Payload +} + +func (p *payloadSerializer) Deserialize(_ *nexus.Content, v any) error { + return p.converter.FromPayload(p.payload, v) +} + +func (p *payloadSerializer) Serialize(v any) (*nexus.Content, error) { + panic("unimplemented") // not used - operation outputs are directly serialized to payload. +} + +var emptyReaderNopCloser = io.NopCloser(bytes.NewReader([]byte{})) diff --git a/internal/internal_nexus_task_poller.go b/internal/internal_nexus_task_poller.go new file mode 100644 index 000000000..1cdf82826 --- /dev/null +++ b/internal/internal_nexus_task_poller.go @@ -0,0 +1,189 @@ +// The MIT License +// +// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved. +// +// 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. + +package internal + +import ( + "context" + "time" + + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/internal/common/metrics" + "go.temporal.io/sdk/log" +) + +type nexusTaskPoller struct { + basePoller + namespace string + taskQueueName string + identity string + service workflowservice.WorkflowServiceClient + taskHandler *nexusTaskHandler + logger log.Logger + numPollerMetric *numPollerMetric +} + +var _ taskPoller = &nexusTaskPoller{} + +func newNexusTaskPoller( + taskHandler *nexusTaskHandler, + service workflowservice.WorkflowServiceClient, + params workerExecutionParameters, +) *nexusTaskPoller { + return &nexusTaskPoller{ + basePoller: basePoller{ + metricsHandler: params.MetricsHandler, + stopC: params.WorkerStopChannel, + workerBuildID: params.getBuildID(), + useBuildIDVersioning: params.UseBuildIDForVersioning, + capabilities: params.capabilities, + }, + taskHandler: taskHandler, + service: service, + namespace: params.Namespace, + taskQueueName: params.TaskQueue, + identity: params.Identity, + logger: params.Logger, + numPollerMetric: newNumPollerMetric(params.MetricsHandler, metrics.PollerTypeNexusTask), + } +} + +// Poll the nexus task queue and update the num_poller metric +func (ntp *nexusTaskPoller) pollNexusTaskQueue(ctx context.Context, request *workflowservice.PollNexusTaskQueueRequest) (*workflowservice.PollNexusTaskQueueResponse, error) { + ntp.numPollerMetric.increment() + defer ntp.numPollerMetric.decrement() + + return ntp.service.PollNexusTaskQueue(ctx, request) +} + +func (ntp *nexusTaskPoller) poll(ctx context.Context) (interface{}, error) { + traceLog(func() { + ntp.logger.Debug("nexusTaskPoller::Poll") + }) + request := &workflowservice.PollNexusTaskQueueRequest{ + Namespace: ntp.namespace, + TaskQueue: &taskqueuepb.TaskQueue{Name: ntp.taskQueueName, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + Identity: ntp.identity, + WorkerVersionCapabilities: &commonpb.WorkerVersionCapabilities{ + BuildId: ntp.workerBuildID, + UseVersioning: ntp.useBuildIDVersioning, + }, + } + + response, err := ntp.pollNexusTaskQueue(ctx, request) + if err != nil { + return nil, err + } + if response == nil || len(response.TaskToken) == 0 { + // No operation info is available on empty poll. Emit using base scope. + ntp.metricsHandler.Counter(metrics.NexusPollNoTaskCounter).Inc(1) + return nil, nil + } + + return response, nil +} + +// PollTask polls a new task +func (ntp *nexusTaskPoller) PollTask() (any, error) { + return ntp.doPoll(ntp.poll) +} + +// ProcessTask processes a new task +func (ntp *nexusTaskPoller) ProcessTask(task interface{}) error { + if ntp.stopping() { + return errStop + } + + response := task.(*workflowservice.PollNexusTaskQueueResponse) + if response.GetRequest() == nil { + // We didn't get a request, poll must have timed out. + traceLog(func() { + ntp.logger.Debug("Empty Nexus poll response") + }) + return nil + } + + metricsHandler, handlerErr := ntp.taskHandler.metricsHandlerForTask(response) + if handlerErr != nil { + // context wasn't propagated to us, use a background context. + _, err := ntp.taskHandler.client.WorkflowService().RespondNexusTaskFailed( + context.Background(), ntp.taskHandler.fillInFailure(response.TaskToken, handlerErr)) + return err + } + + executionStartTime := time.Now() + + // Schedule-to-start (from the time the request hit the frontend). + scheduleToStartLatency := executionStartTime.Sub(response.GetRequest().GetScheduledTime().AsTime()) + metricsHandler.Timer(metrics.NexusTaskScheduleToStartLatency).Record(scheduleToStartLatency) + + // Process the nexus task. + res, failure, err := ntp.taskHandler.Execute(response) + + // Execution latency (in-SDK processing time). + metricsHandler.Timer(metrics.NexusTaskExecutionLatency).Record(time.Since(executionStartTime)) + if err != nil || failure != nil { + metricsHandler.Counter(metrics.NexusTaskExecutionFailedCounter).Inc(1) + } + + // Let the poller machinary drop the task, nothing to report back. + // This is only expected due to context deadline errors. + if err != nil { + return err + } + + if err := ntp.reportCompletion(res, failure); err != nil { + traceLog(func() { + ntp.logger.Debug("reportNexusTaskComplete failed", tagError, err) + }) + return err + } + + // E2E latency, from frontend until we finished reporting completion. + metricsHandler. + Timer(metrics.NexusTaskEndToEndLatency). + Record(time.Since(response.GetRequest().GetScheduledTime().AsTime())) + return nil +} + +func (ntp *nexusTaskPoller) reportCompletion( + completion *workflowservice.RespondNexusTaskCompletedRequest, + failure *workflowservice.RespondNexusTaskFailedRequest, +) error { + ctx := context.Background() + // No workflow or activity tags to report. + // Task queue expected to be empty for Respond*Task... requests. + rpcMetricsHandler := ntp.metricsHandler.WithTags(metrics.RPCTags(metrics.NoneTagValue, metrics.NoneTagValue, metrics.NoneTagValue)) + ctx, cancel := newGRPCContext(ctx, grpcMetricsHandler(rpcMetricsHandler), + defaultGrpcRetryParameters(ctx)) + defer cancel() + + if failure != nil { + _, err := ntp.taskHandler.client.WorkflowService().RespondNexusTaskFailed(ctx, failure) + return err + } + _, err := ntp.taskHandler.client.WorkflowService().RespondNexusTaskCompleted(ctx, completion) + return err +} diff --git a/internal/internal_nexus_worker.go b/internal/internal_nexus_worker.go new file mode 100644 index 000000000..d4d084a1d --- /dev/null +++ b/internal/internal_nexus_worker.go @@ -0,0 +1,102 @@ +// The MIT License +// +// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved. +// +// 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. + +package internal + +import ( + "github.com/nexus-rpc/sdk-go/nexus" + "go.temporal.io/api/workflowservice/v1" +) + +type nexusWorkerOptions struct { + executionParameters workerExecutionParameters + client Client + workflowService workflowservice.WorkflowServiceClient + handler nexus.Handler +} + +type nexusWorker struct { + executionParameters workerExecutionParameters + workflowService workflowservice.WorkflowServiceClient + worker *baseWorker + stopC chan struct{} +} + +func newNexusWorker(opts nexusWorkerOptions) (*nexusWorker, error) { + workerStopChannel := make(chan struct{}) + params := opts.executionParameters + params.WorkerStopChannel = getReadOnlyChannel(workerStopChannel) + ensureRequiredParams(¶ms) + poller := newNexusTaskPoller( + newNexusTaskHandler( + opts.handler, + opts.executionParameters.Identity, + opts.executionParameters.Namespace, + opts.executionParameters.TaskQueue, + opts.client, + opts.executionParameters.DataConverter, + opts.executionParameters.Logger, + opts.executionParameters.MetricsHandler, + ), + opts.workflowService, + params, + ) + + baseWorker := newBaseWorker(baseWorkerOptions{ + pollerCount: params.MaxConcurrentNexusTaskQueuePollers, + pollerRate: defaultPollerRate, + maxConcurrentTask: params.ConcurrentNexusTaskExecutionSize, + maxTaskPerSecond: defaultWorkerTaskExecutionRate, + taskWorker: poller, + identity: params.Identity, + workerType: "NexusWorker", + stopTimeout: params.WorkerStopTimeout, + fatalErrCb: params.WorkerFatalErrorCallback, + }, + params.Logger, + params.MetricsHandler, + nil, + ) + + return &nexusWorker{ + executionParameters: opts.executionParameters, + workflowService: opts.workflowService, + worker: baseWorker, + stopC: workerStopChannel, + }, nil +} + +// Start the worker. +func (w *nexusWorker) Start() error { + err := verifyNamespaceExist(w.workflowService, w.executionParameters.MetricsHandler, w.executionParameters.Namespace, w.worker.logger) + if err != nil { + return err + } + w.worker.Start() + return nil +} + +// Stop the worker. +func (w *nexusWorker) Stop() { + close(w.stopC) + w.worker.Stop() +} diff --git a/internal/internal_task_handlers.go b/internal/internal_task_handlers.go index 4d62620fd..aea4fa204 100644 --- a/internal/internal_task_handlers.go +++ b/internal/internal_task_handlers.go @@ -340,7 +340,9 @@ func isCommandEvent(eventType enumspb.EventType) bool { enumspb.EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED, enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED, enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED, - enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_REJECTED: + enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_REJECTED, + enumspb.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + enumspb.EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED: return true default: return false @@ -1709,6 +1711,26 @@ func isCommandMatchEvent(d *commandpb.Command, e *historypb.HistoryEvent, obes [ return false } return true + + case enumspb.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION: + if e.GetEventType() != enumspb.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED { + return false + } + eventAttributes := e.GetNexusOperationScheduledEventAttributes() + commandAttributes := d.GetScheduleNexusOperationCommandAttributes() + + return eventAttributes.GetService() == commandAttributes.GetService() && + eventAttributes.GetOperation() == commandAttributes.GetOperation() + + case enumspb.COMMAND_TYPE_REQUEST_CANCEL_NEXUS_OPERATION: + if e.GetEventType() != enumspb.EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED { + return false + } + + eventAttributes := e.GetNexusOperationCancelRequestedEventAttributes() + commandAttributes := d.GetRequestCancelNexusOperationCommandAttributes() + + return eventAttributes.GetScheduledEventId() == commandAttributes.GetScheduledEventId() } return false diff --git a/internal/internal_task_handlers_test.go b/internal/internal_task_handlers_test.go index 077d6b905..5bd03b904 100644 --- a/internal/internal_task_handlers_test.go +++ b/internal/internal_task_handlers_test.go @@ -2180,7 +2180,7 @@ func Test_NonDeterministicCheck(t *testing.T) { // the command type count because the 1 ProtocolMessageCommand type can // result in 3 different event types being created. As more protocols are // added, this number will increase. - require.Equal(t, 17, commandEventTypeCount, "Every command type must have at least one matching event type. "+ + require.Equal(t, 19, commandEventTypeCount, "Every command type must have at least one matching event type. "+ "If you add new command type, you need to update isCommandEvent() method to include that new event type as well.") } diff --git a/internal/internal_worker.go b/internal/internal_worker.go index b6ebc6b96..749d2646f 100644 --- a/internal/internal_worker.go +++ b/internal/internal_worker.go @@ -40,9 +40,11 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" "github.com/golang/mock/gomock" + "github.com/nexus-rpc/sdk-go/nexus" "github.com/pborman/uuid" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -159,6 +161,12 @@ type ( // TaskQueueActivitiesPerSecond is the throttling limit for activity tasks controlled by the server. TaskQueueActivitiesPerSecond float64 + // MaxConcurrentNexusTaskQueuePollers is the max number of pollers for the nexus task queue. + MaxConcurrentNexusTaskQueuePollers int + + // Defines how many concurrent nexus task executions should be handled by this worker. + ConcurrentNexusTaskExecutionSize int + // User can provide an identity for the debuggability. If not provided the framework has // a default option. Identity string @@ -496,6 +504,7 @@ func (aw *activityWorker) Stop() { type registry struct { sync.Mutex + nexusServices map[string]*nexus.Service workflowFuncMap map[string]interface{} workflowAliasMap map[string]string activityFuncMap map[string]activity @@ -637,6 +646,20 @@ func (r *registry) registerActivityStructWithOptions(aStruct interface{}, option return nil } +func (r *registry) RegisterNexusService(service *nexus.Service) { + if service.Name == "" { + panic(fmt.Errorf("tried to register a service with no name")) + } + + r.Lock() + defer r.Unlock() + + if _, ok := r.nexusServices[service.Name]; ok { + panic(fmt.Sprintf("service name \"%v\" is already registered", service.Name)) + } + r.nexusServices[service.Name] = service +} + func (r *registry) getWorkflowAlias(fnName string) (string, bool) { r.Lock() defer r.Unlock() @@ -778,6 +801,7 @@ func newRegistryWithOptions(options registryOptions) *registry { r := ®istry{ workflowFuncMap: make(map[string]interface{}), activityFuncMap: make(map[string]activity), + nexusServices: make(map[string]*nexus.Service), } if !options.disableAliasing { r.workflowAliasMap = make(map[string]string) @@ -900,16 +924,24 @@ func getActivityEnvironmentFromCtx(ctx context.Context) *activityEnvironment { // AggregatedWorker combines management of both workflowWorker and activityWorker worker lifecycle. type AggregatedWorker struct { + // Stored for creating a nexus worker on Start. + executionParams workerExecutionParameters + // Memoized start function. Ensures start runs once and returns the same error when called multiple times. + memoizedStart func() error + client *WorkflowClient workflowWorker *workflowWorker activityWorker *activityWorker sessionWorker *sessionWorker + nexusWorker *nexusWorker logger log.Logger registry *registry - stopC chan struct{} - fatalErr error - fatalErrLock sync.Mutex - capabilities *workflowservice.GetSystemInfoResponse_Capabilities + // Stores a boolean indicating whether the worker has already been started. + started atomic.Bool + stopC chan struct{} + fatalErr error + fatalErrLock sync.Mutex + capabilities *workflowservice.GetSystemInfoResponse_Capabilities } // RegisterWorkflow registers workflow implementation with the AggregatedWorker @@ -938,9 +970,24 @@ func (aw *AggregatedWorker) RegisterActivityWithOptions(a interface{}, options R aw.registry.RegisterActivityWithOptions(a, options) } +func (aw *AggregatedWorker) RegisterNexusService(service *nexus.Service) { + if aw.started.Load() { + panic(errors.New("cannot register Nexus services after worker start")) + } + aw.registry.RegisterNexusService(service) +} + // Start the worker in a non-blocking fashion. +// The actual work is done in the memoized "start" function to ensure duplicate calls are returned a consistent error. func (aw *AggregatedWorker) Start() error { aw.assertNotStopped() + return aw.memoizedStart() +} + +// start the worker. This method is memoized using sync.OnceValue in memoizedStart. +func (aw *AggregatedWorker) start() error { + aw.started.Store(true) + if err := initBinaryChecksum(); err != nil { return fmt.Errorf("failed to get executable checksum: %v", err) } else if err = aw.client.ensureInitialized(context.Background()); err != nil { @@ -990,6 +1037,31 @@ func (aw *AggregatedWorker) Start() error { return err } } + nexusServices := aw.registry.nexusServices + if len(nexusServices) > 0 { + reg := nexus.NewServiceRegistry() + for _, service := range nexusServices { + if err := reg.Register(service); err != nil { + return fmt.Errorf("failed to create a nexus worker: %w", err) + } + } + handler, err := reg.NewHandler() + if err != nil { + return fmt.Errorf("failed to create a nexus worker: %w", err) + } + aw.nexusWorker, err = newNexusWorker(nexusWorkerOptions{ + executionParameters: aw.executionParams, + client: aw.client, + workflowService: aw.client.workflowService, + handler: handler, + }) + if err != nil { + return fmt.Errorf("failed to create a nexus worker: %w", err) + } + if err := aw.nexusWorker.Start(); err != nil { + return fmt.Errorf("failed to start a nexus worker: %w", err) + } + } aw.logger.Info("Started Worker") return nil } @@ -1571,6 +1643,8 @@ func NewAggregatedWorker(client *WorkflowClient, taskQueue string, options Worke WorkerLocalActivitiesPerSecond: options.WorkerLocalActivitiesPerSecond, ConcurrentWorkflowTaskExecutionSize: options.MaxConcurrentWorkflowTaskExecutionSize, MaxConcurrentWorkflowTaskQueuePollers: options.MaxConcurrentWorkflowTaskPollers, + ConcurrentNexusTaskExecutionSize: options.MaxConcurrentNexusTaskExecutionSize, + MaxConcurrentNexusTaskQueuePollers: options.MaxConcurrentNexusTaskPollers, Identity: client.identity, WorkerBuildID: options.BuildID, UseBuildIDForVersioning: options.UseBuildIDForVersioning, @@ -1650,15 +1724,17 @@ func NewAggregatedWorker(client *WorkflowClient, taskQueue string, options Worke } aw = &AggregatedWorker{ - client: client, - workflowWorker: workflowWorker, - activityWorker: activityWorker, - sessionWorker: sessionWorker, - logger: workerParams.Logger, - registry: registry, - stopC: make(chan struct{}), - capabilities: &capabilities, - } + client: client, + workflowWorker: workflowWorker, + activityWorker: activityWorker, + sessionWorker: sessionWorker, + logger: workerParams.Logger, + registry: registry, + stopC: make(chan struct{}), + capabilities: &capabilities, + executionParams: workerParams, + } + aw.memoizedStart = sync.OnceValue(aw.start) return aw } @@ -1779,6 +1855,12 @@ func setWorkerOptionsDefaults(options *WorkerOptions) { // the server does not rate limit eager activities. options.DisableEagerActivities = true } + if options.MaxConcurrentNexusTaskPollers <= 0 { + options.MaxConcurrentNexusTaskPollers = defaultConcurrentPollRoutineSize + } + if options.MaxConcurrentNexusTaskExecutionSize == 0 { + options.MaxConcurrentNexusTaskExecutionSize = defaultMaxConcurrentTaskExecutionSize + } if options.StickyScheduleToStartTimeout.Seconds() == 0 { options.StickyScheduleToStartTimeout = stickyWorkflowTaskScheduleToStartTimeoutSeconds * time.Second } diff --git a/internal/internal_worker_base.go b/internal/internal_worker_base.go index 60285a8b6..c841d0bed 100644 --- a/internal/internal_worker_base.go +++ b/internal/internal_worker_base.go @@ -77,6 +77,14 @@ type ( Backoff time.Duration } + executeNexusOperationParams struct { + client NexusClient + operation string + input *commonpb.Payload + options NexusOperationOptions + nexusHeader map[string]string + } + // WorkflowEnvironment Represents the environment for workflow. // Should only be used within the scope of workflow definition. WorkflowEnvironment interface { @@ -92,6 +100,8 @@ type ( RequestCancelChildWorkflow(namespace, workflowID string) RequestCancelExternalWorkflow(namespace, workflowID, runID string, callback ResultHandler) ExecuteChildWorkflow(params ExecuteWorkflowParams, callback ResultHandler, startedHandler func(r WorkflowExecution, e error)) + ExecuteNexusOperation(params executeNexusOperationParams, callback func(*commonpb.Payload, error), startedHandler func(opID string, e error)) int64 + RequestCancelNexusOperation(seq int64) GetLogger() log.Logger GetMetricsHandler() metrics.Handler // Must be called before WorkflowDefinition.Execute returns diff --git a/internal/internal_workflow.go b/internal/internal_workflow.go index cc6eb852d..106a58a31 100644 --- a/internal/internal_workflow.go +++ b/internal/internal_workflow.go @@ -249,6 +249,11 @@ type ( executionFuture *futureImpl // for child workflow execution future } + nexusOperationFutureImpl struct { + *decodeFutureImpl // for the result + executionFuture *futureImpl // for the NexusOperationExecution + } + asyncFuture interface { Future // Used by selectorImpl @@ -486,6 +491,10 @@ func (f *childWorkflowFutureImpl) SignalChildWorkflow(ctx Context, signalName st return i.SignalChildWorkflow(ctx, childExec.ID, signalName, data) } +func (f *nexusOperationFutureImpl) GetNexusOperationExecution() Future { + return f.executionFuture +} + func newWorkflowContext( env WorkflowEnvironment, interceptors []WorkerInterceptor, diff --git a/internal/internal_workflow_client.go b/internal/internal_workflow_client.go index 31cf26b3f..cb68c0a79 100644 --- a/internal/internal_workflow_client.go +++ b/internal/internal_workflow_client.go @@ -1583,7 +1583,6 @@ func (w *workflowClientInterceptor) ExecuteWorkflow( // run propagators to extract information about tracing and other stuff, store in headers field startRequest := &workflowservice.StartWorkflowExecutionRequest{ Namespace: w.client.namespace, - RequestId: uuid.New(), WorkflowId: workflowID, WorkflowType: &commonpb.WorkflowType{Name: in.WorkflowType}, TaskQueue: &taskqueuepb.TaskQueue{Name: in.Options.TaskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, @@ -1598,6 +1597,13 @@ func (w *workflowClientInterceptor) ExecuteWorkflow( Memo: memo, SearchAttributes: searchAttr, Header: header, + CompletionCallbacks: in.Options.callbacks, + } + + if in.Options.requestID != "" { + startRequest.RequestId = in.Options.requestID + } else { + startRequest.RequestId = uuid.New() } var eagerExecutor *eagerWorkflowExecutor diff --git a/internal/internal_workflow_testsuite.go b/internal/internal_workflow_testsuite.go index 4d7439ea4..d48e6ae3c 100644 --- a/internal/internal_workflow_testsuite.go +++ b/internal/internal_workflow_testsuite.go @@ -29,12 +29,15 @@ import ( "errors" "fmt" "reflect" + "strconv" "strings" "sync" "time" "github.com/facebookgo/clock" "github.com/golang/mock/gomock" + "github.com/google/uuid" + "github.com/nexus-rpc/sdk-go/nexus" "github.com/robfig/cron" "github.com/stretchr/testify/mock" "google.golang.org/grpc" @@ -44,6 +47,8 @@ import ( commandpb "go.temporal.io/api/command/v1" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" + failurepb "go.temporal.io/api/failure/v1" + nexuspb "go.temporal.io/api/nexus/v1" "go.temporal.io/api/serviceerror" taskqueuepb "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" @@ -97,6 +102,18 @@ type ( err error } + testNexusOperationHandle struct { + env *testWorkflowEnvironmentImpl + seq int64 + params executeNexusOperationParams + operationID string + cancelRequested bool + started bool + done bool + onCompleted func(*commonpb.Payload, error) + onStarted func(opID string, e error) + } + testCallbackHandle struct { callback func() startWorkflowTask bool // start a new workflow task after callback() is handled. @@ -149,11 +166,12 @@ type ( testTimeout time.Duration header *commonpb.Header - counterID int64 - activities map[string]*testActivityHandle - localActivities map[string]*localActivityTask - timers map[string]*testTimerHandle - runningWorkflows map[string]*testWorkflowHandle + counterID int64 + activities map[string]*testActivityHandle + localActivities map[string]*localActivityTask + timers map[string]*testTimerHandle + runningWorkflows map[string]*testWorkflowHandle + runningNexusOperations map[int64]*testNexusOperationHandle runningCount int @@ -240,6 +258,7 @@ func newTestWorkflowEnvironmentImpl(s *WorkflowTestSuite, parentRegistry *regist activities: make(map[string]*testActivityHandle), localActivities: make(map[string]*localActivityTask), runningWorkflows: make(map[string]*testWorkflowHandle), + runningNexusOperations: make(map[int64]*testNexusOperationHandle), callbackChannel: make(chan testCallbackHandle, 1000), testTimeout: 3 * time.Second, expectedWorkflowMockCalls: make(map[string]struct{}), @@ -2133,6 +2152,10 @@ func (env *testWorkflowEnvironmentImpl) RegisterActivityWithOptions(a interface{ env.registry.RegisterActivityWithOptions(a, options) } +func (env *testWorkflowEnvironmentImpl) RegisterNexusService(s *nexus.Service) { + env.registry.RegisterNexusService(s) +} + func (env *testWorkflowEnvironmentImpl) RegisterCancelHandler(handler func()) { env.workflowCancelHandler = handler } @@ -2291,6 +2314,135 @@ func (env *testWorkflowEnvironmentImpl) executeChildWorkflowWithDelay(delayStart go childEnv.executeWorkflowInternal(delayStart, params.WorkflowType.Name, params.Input) } +func (env *testWorkflowEnvironmentImpl) newTestNexusTaskHandler() *nexusTaskHandler { + if len(env.registry.nexusServices) == 0 { + panic(fmt.Errorf("no nexus services registered")) + } + + reg := nexus.NewServiceRegistry() + for _, service := range env.registry.nexusServices { + if err := reg.Register(service); err != nil { + panic(fmt.Errorf("failed to register nexus service '%v': %w", service, err)) + } + } + handler, err := reg.NewHandler() + if err != nil { + panic(fmt.Errorf("failed to create nexus handler: %w", err)) + } + + return newNexusTaskHandler( + handler, + env.identity, + env.workflowInfo.Namespace, + env.workflowInfo.TaskQueueName, + &testSuiteClientForNexusOperations{env: env}, + env.dataConverter, + env.logger, + env.metricsHandler, + ) +} + +func (env *testWorkflowEnvironmentImpl) ExecuteNexusOperation(params executeNexusOperationParams, callback func(*commonpb.Payload, error), startedHandler func(opID string, e error)) int64 { + seq := env.nextID() + taskHandler := env.newTestNexusTaskHandler() + handle := &testNexusOperationHandle{ + env: env, + seq: seq, + params: params, + onCompleted: callback, + onStarted: startedHandler, + } + env.runningNexusOperations[seq] = handle + + task := handle.newStartTask() + env.runningCount++ + go func() { + response, failure, err := taskHandler.Execute(task) + if err != nil { + // No retries for operations, fail the operation immediately. + failure = taskHandler.fillInFailure(task.TaskToken, nexusHandlerError(nexus.HandlerErrorTypeInternal, err.Error())) + } + if failure != nil { + err := env.failureConverter.FailureToError(nexusOperationFailure(params, "", &failurepb.Failure{ + Message: failure.GetError().GetFailure().GetMessage(), + FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ + ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{ + NonRetryable: true, + }, + }, + })) + env.postCallback(func() { + handle.startedCallback("", err) + handle.completedCallback(nil, err) + }, true) + return + } else { + switch v := response.GetResponse().GetStartOperation().GetVariant().(type) { + case *nexuspb.StartOperationResponse_SyncSuccess: + env.postCallback(func() { + handle.startedCallback("", nil) + handle.completedCallback(v.SyncSuccess.GetPayload(), nil) + }, true) + case *nexuspb.StartOperationResponse_AsyncSuccess: + env.postCallback(func() { + handle.startedCallback(v.AsyncSuccess.GetOperationId(), nil) + if handle.cancelRequested { + handle.cancel() + } + }, true) + case *nexuspb.StartOperationResponse_OperationError: + err := env.failureConverter.FailureToError( + nexusOperationFailure(params, "", unsuccessfulOperationErrorToTemporalFailure(v.OperationError)), + ) + env.postCallback(func() { + handle.startedCallback("", err) + handle.completedCallback(nil, err) + }, true) + default: + panic(fmt.Errorf("unknown response variant: %v", v)) + } + } + }() + return seq +} + +func (env *testWorkflowEnvironmentImpl) RequestCancelNexusOperation(seq int64) { + handle, ok := env.runningNexusOperations[seq] + if !ok { + panic(fmt.Errorf("no running operation found for sequence: %d", seq)) + } + + // Avoid duplicate cancelation. + if handle.cancelRequested { + return + } + + // Mark this cancelation request in case the operation hasn't started yet. + // Cancel will be called after start. + handle.cancelRequested = true + + // Only cancel after started, we need an operation ID. + if handle.started { + handle.cancel() + } +} + +func (env *testWorkflowEnvironmentImpl) resolveNexusOperation(seq int64, result *commonpb.Payload, err error) { + env.postCallback(func() { + handle, ok := env.runningNexusOperations[seq] + if !ok { + panic(fmt.Errorf("no running operation found for sequence: %d", seq)) + } + if err != nil { + failure := env.failureConverter.ErrorToFailure(err) + err = env.failureConverter.FailureToError(nexusOperationFailure(handle.params, handle.operationID, failure.GetCause())) + handle.completedCallback(nil, err) + } else { + handle.completedCallback(result, nil) + } + }, true) +} + func (env *testWorkflowEnvironmentImpl) SideEffect(f func() (*commonpb.Payloads, error), callback ResultHandler) { callback(f()) } @@ -2669,3 +2821,92 @@ func mockFnGetVersion(string, Version, Version) Version { // make sure interface is implemented var _ WorkflowEnvironment = (*testWorkflowEnvironmentImpl)(nil) + +func (h *testNexusOperationHandle) newStartTask() *workflowservice.PollNexusTaskQueueResponse { + return &workflowservice.PollNexusTaskQueueResponse{ + TaskToken: []byte{}, + Request: &nexuspb.Request{ + ScheduledTime: timestamppb.Now(), + Header: h.params.nexusHeader, + Variant: &nexuspb.Request_StartOperation{ + StartOperation: &nexuspb.StartOperationRequest{ + Service: h.params.client.Service(), + Operation: h.params.operation, + RequestId: uuid.NewString(), + // This is effectively ignored. + Callback: "http://test-env/operations", + CallbackHeader: map[string]string{ + // The test client uses this to call resolveNexusOperation. + "operation-sequence": strconv.FormatInt(h.seq, 10), + }, + Payload: h.params.input, + }, + }, + }, + } +} + +func (h *testNexusOperationHandle) newCancelTask() *workflowservice.PollNexusTaskQueueResponse { + return &workflowservice.PollNexusTaskQueueResponse{ + TaskToken: []byte{}, + Request: &nexuspb.Request{ + ScheduledTime: timestamppb.Now(), + Header: h.params.nexusHeader, + Variant: &nexuspb.Request_CancelOperation{ + CancelOperation: &nexuspb.CancelOperationRequest{ + Service: h.params.client.Service(), + Operation: h.params.operation, + OperationId: h.operationID, + }, + }, + }, + } +} + +// completedCallback is a callback registered to handle operation completion. +// Must be called in a postCallback block. +func (h *testNexusOperationHandle) completedCallback(result *commonpb.Payload, err error) { + if h.done { + // Ignore duplicate completions. + return + } + h.done = true + delete(h.env.runningNexusOperations, h.seq) + h.onCompleted(result, err) +} + +// startedCallback is a callback registered to handle operation start. +// Must be called in a postCallback block. +func (h *testNexusOperationHandle) startedCallback(opID string, e error) { + h.operationID = opID + h.started = true + h.onStarted(opID, e) + h.env.runningCount-- +} + +func (h *testNexusOperationHandle) cancel() { + if h.done { + return + } + if h.started && h.operationID == "" { + panic(fmt.Errorf("incomplete operation has no operation ID: (%s, %s, %s)", + h.params.client.Endpoint(), h.params.client.Service(), h.params.operation)) + } + h.env.runningCount++ + task := h.newCancelTask() + taskHandler := h.env.newTestNexusTaskHandler() + + go func() { + _, failure, err := taskHandler.Execute(task) + h.env.postCallback(func() { + if err != nil { + // No retries in the test env, fail the operation immediately. + h.completedCallback(nil, fmt.Errorf("operation cancelation handler failed: %w", err)) + } else if failure != nil { + // No retries in the test env, fail the operation immediately. + h.completedCallback(nil, fmt.Errorf("operation cancelation handler failed: %v", failure.GetError().GetFailure().GetMessage())) + } + h.env.runningCount-- + }, false) + }() +} diff --git a/internal/nexus_operations.go b/internal/nexus_operations.go new file mode 100644 index 000000000..25044e081 --- /dev/null +++ b/internal/nexus_operations.go @@ -0,0 +1,421 @@ +// The MIT License +// +// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved. +// +// 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. + +package internal + +import ( + "context" + "fmt" + "strconv" + + "github.com/nexus-rpc/sdk-go/nexus" + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/api/enums/v1" + failurepb "go.temporal.io/api/failure/v1" + nexuspb "go.temporal.io/api/nexus/v1" + "go.temporal.io/api/operatorservice/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/converter" + "go.temporal.io/sdk/log" +) + +// NexusOperationContext is an internal only struct that holds fields used by the temporalnexus functions. +type NexusOperationContext struct { + Client Client + TaskQueue string + Log log.Logger +} + +type nexusOperationContextKeyType struct{} + +// nexusOperationContextKey is a key for associating a [NexusOperationContext] with a [context.Context]. +var nexusOperationContextKey = nexusOperationContextKeyType{} + +type isWorkflowRunOpContextKeyType struct{} + +// IsWorkflowRunOpContextKey is a key to mark that the current context is used within a workflow run operation. +// The fake test env client verifies this key is set on the context to decide whether it should execute a method or +// panic as we don't want to expose a partial client to sync operations. +var IsWorkflowRunOpContextKey = isWorkflowRunOpContextKeyType{} + +// NexusOperationContextFromGoContext gets the [NexusOperationContext] associated with the given [context.Context]. +func NexusOperationContextFromGoContext(ctx context.Context) (nctx *NexusOperationContext, ok bool) { + nctx, ok = ctx.Value(nexusOperationContextKey).(*NexusOperationContext) + return +} + +// nexusOperationFailure is a utility in use by the test environment. +func nexusOperationFailure(params executeNexusOperationParams, operationID string, cause *failurepb.Failure) *failurepb.Failure { + return &failurepb.Failure{ + Message: "nexus operation completed unsuccessfully", + FailureInfo: &failurepb.Failure_NexusOperationExecutionFailureInfo{ + NexusOperationExecutionFailureInfo: &failurepb.NexusOperationFailureInfo{ + Endpoint: params.client.Endpoint(), + Service: params.client.Service(), + Operation: params.operation, + OperationId: operationID, + }, + }, + Cause: cause, + } +} + +// unsuccessfulOperationErrorToTemporalFailure is a utility in use by the test environment. +// copied from the server codebase with a slight adaptation: https://github.com/temporalio/temporal/blob/7635cd7dbdc7dd3219f387e8fc66fa117f585ff6/common/nexus/failure.go#L69-L108 +func unsuccessfulOperationErrorToTemporalFailure(err *nexuspb.UnsuccessfulOperationError) *failurepb.Failure { + failure := &failurepb.Failure{ + Message: err.Failure.Message, + } + if err.OperationState == string(nexus.OperationStateCanceled) { + failure.FailureInfo = &failurepb.Failure_CanceledFailureInfo{ + CanceledFailureInfo: &failurepb.CanceledFailureInfo{ + Details: nexusFailureMetadataToPayloads(err.Failure), + }, + } + } else { + failure.FailureInfo = &failurepb.Failure_ApplicationFailureInfo{ + ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{ + // Make up a type here, it's not part of the Nexus Failure spec. + Type: "NexusOperationFailure", + Details: nexusFailureMetadataToPayloads(err.Failure), + NonRetryable: true, + }, + } + } + return failure +} + +// nexusFailureMetadataToPayloads is a utility in use by the test environment. +// copied from the server codebase with a slight adaptation: https://github.com/temporalio/temporal/blob/7635cd7dbdc7dd3219f387e8fc66fa117f585ff6/common/nexus/failure.go#L69-L108 +func nexusFailureMetadataToPayloads(failure *nexuspb.Failure) *commonpb.Payloads { + if len(failure.Metadata) == 0 && len(failure.Details) == 0 { + return nil + } + metadata := make(map[string][]byte, len(failure.Metadata)) + for k, v := range failure.Metadata { + metadata[k] = []byte(v) + } + return &commonpb.Payloads{ + Payloads: []*commonpb.Payload{ + { + Metadata: metadata, + Data: failure.Details, + }, + }, + } +} + +// testSuiteClientForNexusOperations is a partial [Client] implementation for the test workflow environment used to +// support running the workflow run operation - and only this operation, all methods will panic when this client is +// passed to sync operations. +type testSuiteClientForNexusOperations struct { + env *testWorkflowEnvironmentImpl +} + +// CancelWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) CancelWorkflow(ctx context.Context, workflowID string, runID string) error { + if set, ok := ctx.Value(IsWorkflowRunOpContextKey).(bool); !ok || !set { + panic("not implemented in the test environment") + } + doneCh := make(chan error) + t.env.cancelWorkflowByID(workflowID, runID, func(result *commonpb.Payloads, err error) { + doneCh <- err + }) + return <-doneCh +} + +// CheckHealth implements Client. +func (t *testSuiteClientForNexusOperations) CheckHealth(ctx context.Context, request *CheckHealthRequest) (*CheckHealthResponse, error) { + return &CheckHealthResponse{}, nil +} + +// Close implements Client. +func (t *testSuiteClientForNexusOperations) Close() { + // No op. +} + +// CompleteActivity implements Client. +func (t *testSuiteClientForNexusOperations) CompleteActivity(ctx context.Context, taskToken []byte, result interface{}, err error) error { + panic("not implemented in the test environment") +} + +// CompleteActivityByID implements Client. +func (t *testSuiteClientForNexusOperations) CompleteActivityByID(ctx context.Context, namespace string, workflowID string, runID string, activityID string, result interface{}, err error) error { + panic("not implemented in the test environment") +} + +// CountWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) CountWorkflow(ctx context.Context, request *workflowservice.CountWorkflowExecutionsRequest) (*workflowservice.CountWorkflowExecutionsResponse, error) { + panic("not implemented in the test environment") +} + +// DescribeTaskQueue implements Client. +func (t *testSuiteClientForNexusOperations) DescribeTaskQueue(ctx context.Context, taskqueue string, taskqueueType enums.TaskQueueType) (*workflowservice.DescribeTaskQueueResponse, error) { + panic("not implemented in the test environment") +} + +// DescribeTaskQueueEnhanced implements Client. +func (t *testSuiteClientForNexusOperations) DescribeTaskQueueEnhanced(ctx context.Context, options DescribeTaskQueueEnhancedOptions) (TaskQueueDescription, error) { + panic("unimplemented in the test environment") +} + +// DescribeWorkflowExecution implements Client. +func (t *testSuiteClientForNexusOperations) DescribeWorkflowExecution(ctx context.Context, workflowID string, runID string) (*workflowservice.DescribeWorkflowExecutionResponse, error) { + panic("not implemented in the test environment") +} + +// ExecuteWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) ExecuteWorkflow(ctx context.Context, options StartWorkflowOptions, workflow interface{}, args ...interface{}) (WorkflowRun, error) { + if set, ok := ctx.Value(IsWorkflowRunOpContextKey).(bool); !ok || !set { + panic("not implemented in the test environment") + } + wfType, input, err := getValidatedWorkflowFunction(workflow, args, t.env.dataConverter, t.env.GetRegistry()) + if err != nil { + return nil, fmt.Errorf("cannot validate workflow function: %w", err) + } + + run := &testEnvWorkflowRunForNexusOperations{} + doneCh := make(chan error) + + var callback *commonpb.Callback + + if len(options.callbacks) > 0 { + callback = options.callbacks[0] + } + + t.env.postCallback(func() { + t.env.executeChildWorkflowWithDelay(options.StartDelay, ExecuteWorkflowParams{ + // Not propagating Header as this client does not support context propagation. + WorkflowType: wfType, + Input: input, + WorkflowOptions: WorkflowOptions{ + WaitForCancellation: true, + Namespace: t.env.workflowInfo.Namespace, + TaskQueueName: t.env.workflowInfo.TaskQueueName, + WorkflowID: options.ID, + WorkflowExecutionTimeout: options.WorkflowExecutionTimeout, + WorkflowRunTimeout: options.WorkflowRunTimeout, + WorkflowTaskTimeout: options.WorkflowTaskTimeout, + DataConverter: t.env.dataConverter, + WorkflowIDReusePolicy: options.WorkflowIDReusePolicy, + ContextPropagators: t.env.contextPropagators, + SearchAttributes: options.SearchAttributes, + TypedSearchAttributes: options.TypedSearchAttributes, + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + Memo: options.Memo, + CronSchedule: options.CronSchedule, + RetryPolicy: convertToPBRetryPolicy(options.RetryPolicy), + }, + }, func(result *commonpb.Payloads, wfErr error) { + ncb := callback.GetNexus() + if ncb == nil { + return + } + seqStr := ncb.GetHeader()["operation-sequence"] + if seqStr == "" { + return + } + seq, err := strconv.ParseInt(seqStr, 10, 64) + if err != nil { + panic(fmt.Errorf("unexpected operation sequence in callback header: %s: %w", seqStr, err)) + } + + if wfErr != nil { + t.env.resolveNexusOperation(seq, nil, wfErr) + } else { + var payload *commonpb.Payload + if len(result.GetPayloads()) > 0 { + payload = result.Payloads[0] + } + t.env.resolveNexusOperation(seq, payload, nil) + } + }, func(r WorkflowExecution, err error) { + run.WorkflowExecution = r + doneCh <- err + }) + }, false) + err = <-doneCh + if err != nil { + return nil, err + } + return run, nil +} + +// GetSearchAttributes implements Client. +func (t *testSuiteClientForNexusOperations) GetSearchAttributes(ctx context.Context) (*workflowservice.GetSearchAttributesResponse, error) { + panic("not implemented in the test environment") +} + +// GetWorkerBuildIdCompatibility implements Client. +func (t *testSuiteClientForNexusOperations) GetWorkerBuildIdCompatibility(ctx context.Context, options *GetWorkerBuildIdCompatibilityOptions) (*WorkerBuildIDVersionSets, error) { + panic("not implemented in the test environment") +} + +// GetWorkerTaskReachability implements Client. +func (t *testSuiteClientForNexusOperations) GetWorkerTaskReachability(ctx context.Context, options *GetWorkerTaskReachabilityOptions) (*WorkerTaskReachability, error) { + panic("not implemented in the test environment") +} + +// GetWorkerVersioningRules implements Client. +func (t *testSuiteClientForNexusOperations) GetWorkerVersioningRules(ctx context.Context, options GetWorkerVersioningOptions) (*WorkerVersioningRules, error) { + panic("unimplemented in the test environment") +} + +// GetWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) GetWorkflow(ctx context.Context, workflowID string, runID string) WorkflowRun { + panic("not implemented in the test environment") +} + +// GetWorkflowHistory implements Client. +func (t *testSuiteClientForNexusOperations) GetWorkflowHistory(ctx context.Context, workflowID string, runID string, isLongPoll bool, filterType enums.HistoryEventFilterType) HistoryEventIterator { + panic("not implemented in the test environment") +} + +// GetWorkflowUpdateHandle implements Client. +func (t *testSuiteClientForNexusOperations) GetWorkflowUpdateHandle(GetWorkflowUpdateHandleOptions) WorkflowUpdateHandle { + panic("not implemented in the test environment") +} + +// ListArchivedWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) ListArchivedWorkflow(ctx context.Context, request *workflowservice.ListArchivedWorkflowExecutionsRequest) (*workflowservice.ListArchivedWorkflowExecutionsResponse, error) { + panic("not implemented in the test environment") +} + +// ListClosedWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) ListClosedWorkflow(ctx context.Context, request *workflowservice.ListClosedWorkflowExecutionsRequest) (*workflowservice.ListClosedWorkflowExecutionsResponse, error) { + panic("not implemented in the test environment") +} + +// ListOpenWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) ListOpenWorkflow(ctx context.Context, request *workflowservice.ListOpenWorkflowExecutionsRequest) (*workflowservice.ListOpenWorkflowExecutionsResponse, error) { + panic("not implemented in the test environment") +} + +// ListWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) ListWorkflow(ctx context.Context, request *workflowservice.ListWorkflowExecutionsRequest) (*workflowservice.ListWorkflowExecutionsResponse, error) { + panic("not implemented in the test environment") +} + +// OperatorService implements Client. +func (t *testSuiteClientForNexusOperations) OperatorService() operatorservice.OperatorServiceClient { + panic("not implemented in the test environment") +} + +// QueryWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) QueryWorkflow(ctx context.Context, workflowID string, runID string, queryType string, args ...interface{}) (converter.EncodedValue, error) { + panic("not implemented in the test environment") +} + +// QueryWorkflowWithOptions implements Client. +func (t *testSuiteClientForNexusOperations) QueryWorkflowWithOptions(ctx context.Context, request *QueryWorkflowWithOptionsRequest) (*QueryWorkflowWithOptionsResponse, error) { + panic("not implemented in the test environment") +} + +// RecordActivityHeartbeat implements Client. +func (t *testSuiteClientForNexusOperations) RecordActivityHeartbeat(ctx context.Context, taskToken []byte, details ...interface{}) error { + panic("not implemented in the test environment") +} + +// RecordActivityHeartbeatByID implements Client. +func (t *testSuiteClientForNexusOperations) RecordActivityHeartbeatByID(ctx context.Context, namespace string, workflowID string, runID string, activityID string, details ...interface{}) error { + panic("not implemented in the test environment") +} + +// ResetWorkflowExecution implements Client. +func (t *testSuiteClientForNexusOperations) ResetWorkflowExecution(ctx context.Context, request *workflowservice.ResetWorkflowExecutionRequest) (*workflowservice.ResetWorkflowExecutionResponse, error) { + panic("not implemented in the test environment") +} + +// ScanWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) ScanWorkflow(ctx context.Context, request *workflowservice.ScanWorkflowExecutionsRequest) (*workflowservice.ScanWorkflowExecutionsResponse, error) { + panic("not implemented in the test environment") +} + +// ScheduleClient implements Client. +func (t *testSuiteClientForNexusOperations) ScheduleClient() ScheduleClient { + panic("not implemented in the test environment") +} + +// SignalWithStartWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) SignalWithStartWorkflow(ctx context.Context, workflowID string, signalName string, signalArg interface{}, options StartWorkflowOptions, workflow interface{}, workflowArgs ...interface{}) (WorkflowRun, error) { + panic("not implemented in the test environment") +} + +// SignalWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) SignalWorkflow(ctx context.Context, workflowID string, runID string, signalName string, arg interface{}) error { + panic("not implemented in the test environment") +} + +// TerminateWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) TerminateWorkflow(ctx context.Context, workflowID string, runID string, reason string, details ...interface{}) error { + panic("not implemented in the test environment") +} + +// UpdateWorkflow implements Client. +func (t *testSuiteClientForNexusOperations) UpdateWorkflow(ctx context.Context, options UpdateWorkflowOptions) (WorkflowUpdateHandle, error) { + panic("unimplemented in the test environment") +} + +// UpdateWorkerBuildIdCompatibility implements Client. +func (t *testSuiteClientForNexusOperations) UpdateWorkerBuildIdCompatibility(ctx context.Context, options *UpdateWorkerBuildIdCompatibilityOptions) error { + panic("not implemented in the test environment") +} + +// UpdateWorkerVersioningRules implements Client. +func (t *testSuiteClientForNexusOperations) UpdateWorkerVersioningRules(ctx context.Context, options UpdateWorkerVersioningRulesOptions) (*WorkerVersioningRules, error) { + panic("unimplemented in the test environment") +} + +// WorkflowService implements Client. +func (t *testSuiteClientForNexusOperations) WorkflowService() workflowservice.WorkflowServiceClient { + panic("not implemented in the test environment") +} + +var _ Client = &testSuiteClientForNexusOperations{} + +// testEnvWorkflowRunForNexusOperations is a partial [WorkflowRun] implementation for the test workflow environment used +// to support basic Nexus functionality. +type testEnvWorkflowRunForNexusOperations struct { + WorkflowExecution +} + +// Get implements WorkflowRun. +func (t *testEnvWorkflowRunForNexusOperations) Get(ctx context.Context, valuePtr interface{}) error { + panic("not implemented in the test environment") +} + +// GetID implements WorkflowRun. +func (t *testEnvWorkflowRunForNexusOperations) GetID() string { + return t.ID +} + +// GetRunID implements WorkflowRun. +func (t *testEnvWorkflowRunForNexusOperations) GetRunID() string { + return t.RunID +} + +// GetWithOptions implements WorkflowRun. +func (t *testEnvWorkflowRunForNexusOperations) GetWithOptions(ctx context.Context, valuePtr interface{}, options WorkflowRunGetOptions) error { + panic("not implemented in the test environment") +} + +var _ WorkflowRun = &testEnvWorkflowRunForNexusOperations{} diff --git a/internal/worker.go b/internal/worker.go index a926afa44..1dbd31f0d 100644 --- a/internal/worker.go +++ b/internal/worker.go @@ -95,6 +95,17 @@ type ( // default: 2 MaxConcurrentWorkflowTaskPollers int + // Optional: Sets the maximum concurrent nexus task executions this worker can have. + // The zero value of this uses the default value. + // default: defaultMaxConcurrentTaskExecutionSize(1k) + MaxConcurrentNexusTaskExecutionSize int + + // Optional: Sets the maximum number of goroutines that will concurrently poll the + // temporal-server to retrieve nexus tasks. Changing this value will affect the + // rate at which the worker is able to consume tasks from a task queue. + // default: 2 + MaxConcurrentNexusTaskPollers int + // Optional: Enable logging in replay. // In the workflow code you can use workflow.GetLogger(ctx) to write logs. By default, the logger will skip log // entry during replay mode so you won't see duplicate logs. This option will enable the logging in replay mode. diff --git a/internal/workflow.go b/internal/workflow.go index 70783a36a..f09ce993b 100644 --- a/internal/workflow.go +++ b/internal/workflow.go @@ -2199,3 +2199,173 @@ func DeterministicKeysFunc[K comparable, V any](m map[K]V, cmp func(a K, b K) in func AllHandlersFinished(ctx Context) bool { return len(getWorkflowEnvOptions(ctx).getRunningUpdateHandles()) == 0 } + +// NexusOperationOptions are options for starting a Nexus Operation from a Workflow. +type NexusOperationOptions struct { + ScheduleToCloseTimeout time.Duration +} + +// NexusOperationExecution is the result of NexusOperationFuture.GetNexusOperationExecution. +type NexusOperationExecution struct { + // Operation ID as set by the Operation's handler. May be empty if the operation hasn't started yet or completed + // synchronously. + OperationID string +} + +// NexusOperationFuture represents the result of a Nexus Operation. +type NexusOperationFuture interface { + Future + // GetNexusOperationExecution returns a future that is resolved when the operation reaches the STARTED state. + // For synchronous operations, this will be resolved at the same as the containing [NexusOperationFuture]. For + // asynchronous operations, this future is resolved independently. + // If the operation is unsuccessful, this future will contain the same error as the [NexusOperationFuture]. + // Use this method to extract the Operation ID of an asynchronous operation. OperationID will be empty for + // synchronous operations. + // + // NOTE: Experimental + // + // fut := nexusClient.ExecuteOperation(ctx, op, ...) + // var exec workflow.NexusOperationExecution + // if err := fut.GetNexusOperationExecution().Get(ctx, &exec); err == nil { + // // Nexus Operation started, OperationID is optionally set. + // } + GetNexusOperationExecution() Future +} + +// NexusClient is a client for executing Nexus Operations from a workflow. +// NOTE to maintainers, this interface definition is duplicated in the workflow package to provide a better UX. +type NexusClient interface { + // The endpoint name this client uses. + // + // NOTE: Experimental + Endpoint() string + // The service name this client uses. + // + // NOTE: Experimental + Service() string + + // ExecuteOperation executes a Nexus Operation. + // The operation argument can be a string, a [nexus.Operation] or a [nexus.OperationReference]. + // + // NOTE: Experimental + ExecuteOperation(ctx Context, operation any, input any, options NexusOperationOptions) NexusOperationFuture +} + +type nexusClient struct { + endpoint, service string +} + +// Create a [NexusClient] from an endpoint name and a service name. +// +// NOTE: Experimental +func NewNexusClient(endpoint, service string) NexusClient { + return nexusClient{endpoint, service} +} + +func (c nexusClient) Endpoint() string { + return c.endpoint +} + +func (c nexusClient) Service() string { + return c.service +} + +func (c nexusClient) ExecuteOperation(ctx Context, operation any, input any, options NexusOperationOptions) NexusOperationFuture { + assertNotInReadOnlyState(ctx) + i := getWorkflowOutboundInterceptor(ctx) + return i.ExecuteNexusOperation(ctx, c, operation, input, options) +} + +func (wc *workflowEnvironmentInterceptor) prepareNexusOperationParams(ctx Context, client NexusClient, operation any, input any, options NexusOperationOptions) (executeNexusOperationParams, error) { + dc := WithWorkflowContext(ctx, wc.env.GetDataConverter()) + + var ok bool + var operationName string + if operationName, ok = operation.(string); ok { + } else if regOp, ok := operation.(interface{ Name() string }); ok { + operationName = regOp.Name() + } else { + return executeNexusOperationParams{}, fmt.Errorf("invalid 'operation' parameter, must be an OperationReference or a string") + } + // TODO(bergundy): Validate operation types against input once there's a good way to extract the generic types from + // OperationReference in the Nexus Go SDK. + + payload, err := dc.ToPayload(input) + if err != nil { + return executeNexusOperationParams{}, err + } + + return executeNexusOperationParams{ + client: client, + operation: operationName, + input: payload, + options: options, + }, nil +} + +func (wc *workflowEnvironmentInterceptor) ExecuteNexusOperation(ctx Context, client NexusClient, operation any, input any, options NexusOperationOptions) NexusOperationFuture { + mainFuture, mainSettable := newDecodeFuture(ctx, nil /* this param is never used */) + executionFuture, executionSettable := NewFuture(ctx) + result := &nexusOperationFutureImpl{ + decodeFutureImpl: mainFuture.(*decodeFutureImpl), + executionFuture: executionFuture.(*futureImpl), + } + + // Immediately return if the context has an error without spawning the child workflow + if ctx.Err() != nil { + executionSettable.Set(nil, ctx.Err()) + mainSettable.Set(nil, ctx.Err()) + return result + } + + ctxDone, cancellable := ctx.Done().(*channelImpl) + cancellationCallback := &receiveCallback{} + params, err := wc.prepareNexusOperationParams(ctx, client, operation, input, options) + if err != nil { + executionSettable.Set(nil, err) + mainSettable.Set(nil, err) + return result + } + + var operationID string + seq := wc.env.ExecuteNexusOperation(params, func(r *commonpb.Payload, e error) { + var payloads *commonpb.Payloads + if r != nil { + payloads = &commonpb.Payloads{Payloads: []*commonpb.Payload{r}} + } + mainSettable.Set(payloads, e) + if cancellable { + // future is done, we don't need cancellation anymore + ctxDone.removeReceiveCallback(cancellationCallback) + } + }, func(opID string, e error) { + operationID = opID + executionSettable.Set(NexusOperationExecution{opID}, e) + }) + + if cancellable { + cancellationCallback.fn = func(v any, _ bool) bool { + assertNotInReadOnlyStateCancellation(ctx) + if ctx.Err() == ErrCanceled && !mainFuture.IsReady() { + // Go back to the top of the interception chain. + getWorkflowOutboundInterceptor(ctx).RequestCancelNexusOperation(ctx, RequestCancelNexusOperationInput{ + Client: client, + Operation: operation, + ID: operationID, + seq: seq, + }) + } + return false + } + _, ok, more := ctxDone.receiveAsyncImpl(cancellationCallback) + if ok || !more { + cancellationCallback.fn(nil, more) + } + } + + return result +} + +func (wc *workflowEnvironmentInterceptor) RequestCancelNexusOperation(ctx Context, input RequestCancelNexusOperationInput) { + wc.env.RequestCancelNexusOperation(input.seq) +} \ No newline at end of file diff --git a/internal/workflow_testsuite.go b/internal/workflow_testsuite.go index 1e4a445f7..1b4c2ae9a 100644 --- a/internal/workflow_testsuite.go +++ b/internal/workflow_testsuite.go @@ -32,6 +32,7 @@ import ( "testing" "time" + "github.com/nexus-rpc/sdk-go/nexus" "github.com/stretchr/testify/mock" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -296,6 +297,11 @@ func (e *TestWorkflowEnvironment) RegisterActivityWithOptions(a interface{}, opt e.impl.RegisterActivityWithOptions(a, options) } +// RegisterWorkflow registers a Nexus Service with the TestWorkflowEnvironment. +func (e *TestWorkflowEnvironment) RegisterNexusService(s *nexus.Service) { + e.impl.RegisterNexusService(s) +} + // SetStartTime sets the start time of the workflow. This is optional, default start time will be the wall clock time when // workflow starts. Start time is the workflow.Now(ctx) time at the beginning of the workflow. func (e *TestWorkflowEnvironment) SetStartTime(startTime time.Time) { diff --git a/temporal/error.go b/temporal/error.go index 132b42394..3f14b41b9 100644 --- a/temporal/error.go +++ b/temporal/error.go @@ -131,6 +131,11 @@ type ( // ChildWorkflowExecutionError returned from workflow when child workflow returned an error. ChildWorkflowExecutionError = internal.ChildWorkflowExecutionError + // NexusOperationError is an error returned when a Nexus Operation has failed. + // + // NOTE: Experimental + NexusOperationError = internal.NexusOperationError + // ChildWorkflowExecutionAlreadyStartedError is set as the cause of // ChildWorkflowExecutionError when failure is due the child workflow having // already started. diff --git a/temporalnexus/example_test.go b/temporalnexus/example_test.go new file mode 100644 index 000000000..ea66fe151 --- /dev/null +++ b/temporalnexus/example_test.go @@ -0,0 +1,147 @@ +// The MIT License +// +// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved. +// +// 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. + +package temporalnexus_test + +import ( + "context" + + "github.com/nexus-rpc/sdk-go/nexus" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/temporalnexus" + "go.temporal.io/sdk/worker" + "go.temporal.io/sdk/workflow" +) + +type MyWorkflowInput struct { +} + +type MyOutput struct { +} + +type MyInput struct { + ID string +} + +type MyQueryOutput struct { +} + +func MyHandlerWorkflow(workflow.Context, MyInput) (MyOutput, error) { + return MyOutput{}, nil +} + +func MyHandlerWorkflowWithAlternativeInput(workflow.Context, MyWorkflowInput) (MyOutput, error) { + return MyOutput{}, nil +} + +func ExampleNewWorkflowRunOperation() { + op := temporalnexus.NewWorkflowRunOperation( + "my-async-operation", + MyHandlerWorkflow, + func(ctx context.Context, input MyInput, opts nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{ + // Workflow ID is required and must be deterministically generated from the input in order + // for the operation to be idempotent as the request to start the operation may be retried. + ID: input.ID, + }, nil + }) + + service := nexus.NewService("my-service") + _ = service.Register(op) + + c, _ := client.Dial(client.Options{ + HostPort: "localhost:7233", + Namespace: "my-namespace", + }) + w := worker.New(c, "my-task-queue", worker.Options{}) + w.RegisterWorkflow(MyHandlerWorkflow) + w.RegisterNexusService(service) +} + +func ExampleNewWorkflowRunOperationWithOptions() { + // Alternative 1 - long form version of NewWorkflowRunOperation. + opAlt1, _ := temporalnexus.NewWorkflowRunOperationWithOptions( + temporalnexus.WorkflowRunOperationOptions[MyInput, MyOutput]{ + Name: "my-async-op-1", + Workflow: MyHandlerWorkflow, + GetOptions: func(ctx context.Context, input MyInput, opts nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{ + // Workflow ID is required and must be deterministically generated from the input in order + // for the operation to be idempotent as the request to start the operation may be retried. + ID: input.ID, + }, nil + }, + }) + + // Alternative 2 - start a workflow with alternative inputs. + opAlt2, _ := temporalnexus.NewWorkflowRunOperationWithOptions( + temporalnexus.WorkflowRunOperationOptions[MyInput, MyOutput]{ + Name: "my-async-op-2", + Handler: func(ctx context.Context, input MyInput, opts nexus.StartOperationOptions) (temporalnexus.WorkflowHandle[MyOutput], error) { + // Workflows started with this API must take a single input and return single output. + // To start workflows with different signatures, use ExecuteUntypedWorkflow. + return temporalnexus.ExecuteWorkflow(ctx, opts, client.StartWorkflowOptions{ + // Workflow ID is required and must be deterministically generated from the input in order + // for the operation to be idempotent as the request to start the operation may be retried. + ID: input.ID, + }, MyHandlerWorkflowWithAlternativeInput, MyWorkflowInput{}) + }, + }) + + service := nexus.NewService("my-service") + _ = service.Register(opAlt1, opAlt2) + + c, _ := client.Dial(client.Options{ + HostPort: "localhost:7233", + Namespace: "my-namespace", + }) + w := worker.New(c, "my-task-queue", worker.Options{}) + w.RegisterWorkflow(MyHandlerWorkflow) + w.RegisterWorkflow(MyHandlerWorkflowWithAlternativeInput) + w.RegisterNexusService(service) +} + +func ExampleNewSyncOperation() { + opRead := temporalnexus.NewSyncOperation("my-read-only-operation", func(ctx context.Context, c client.Client, input MyInput, opts nexus.StartOperationOptions) (MyQueryOutput, error) { + var ret MyQueryOutput + res, err := c.QueryWorkflow(ctx, input.ID, "", "some-query", nil) + if err != nil { + return ret, err + } + return ret, res.Get(&ret) + }) + + // Operations don't have to return values. + opWrite := temporalnexus.NewSyncOperation("my-write-operation", func(ctx context.Context, c client.Client, input MyInput, opts nexus.StartOperationOptions) (nexus.NoValue, error) { + return nil, c.SignalWorkflow(ctx, input.ID, "", "some-signal", nil) + }) + + service := nexus.NewService("my-service") + _ = service.Register(opRead, opWrite) + + c, _ := client.Dial(client.Options{ + HostPort: "localhost:7233", + Namespace: "my-namespace", + }) + w := worker.New(c, "my-task-queue", worker.Options{}) + w.RegisterNexusService(service) +} diff --git a/temporalnexus/operation.go b/temporalnexus/operation.go new file mode 100644 index 000000000..d24ed4980 --- /dev/null +++ b/temporalnexus/operation.go @@ -0,0 +1,292 @@ +// The MIT License +// +// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved. +// +// 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. + +// Package temporalnexus provides utilities for exposing Temporal constructs as Nexus Operations. +// +// Nexus RPC is a modern open-source service framework for arbitrary-length operations whose lifetime may extend beyond +// a traditional RPC. Nexus was designed with durable execution in mind, as an underpinning to connect durable +// executions within and across namespaces, clusters and regions – with a clean API contract to streamline multi-team +// collaboration. Any service can be exposed as a set of sync or async Nexus operations – the latter provides an +// operation identity and a uniform interface to get the status of an operation or its result, receive a completion +// callback, or cancel the operation. +// +// Temporal leverages the Nexus RPC protocol to facilitate calling across namespace and cluster and boundaries. +// +// See also: +// +// Nexus over HTTP Spec: https://github.com/nexus-rpc/api/blob/main/SPEC.md +// +// Nexus Go SDK: https://github.com/nexus-rpc/sdk-go +package temporalnexus + +import ( + "context" + "errors" + + "github.com/nexus-rpc/sdk-go/nexus" + "go.temporal.io/api/common/v1" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/internal" + "go.temporal.io/sdk/workflow" +) + +type syncOperation[I, O any] struct { + nexus.UnimplementedOperation[I, O] + + name string + handler func(context.Context, client.Client, I, nexus.StartOperationOptions) (O, error) +} + +// NewSyncOperation is a helper for creating a synchronous-only [nexus.Operation] from a given name and handler +// function. The handler is passed the client that the worker was created with. +// Sync operations are useful for exposing short-lived Temporal client requests, such as signals, queries, sync update, +// list workflows, etc... +// +// NOTE: Experimental +func NewSyncOperation[I any, O any]( + name string, + handler func(context.Context, client.Client, I, nexus.StartOperationOptions) (O, error), +) nexus.Operation[I, O] { + return &syncOperation[I, O]{ + name: name, + handler: handler, + } +} + +func (o *syncOperation[I, O]) Name() string { + return o.name +} + +func (o *syncOperation[I, O]) Start(ctx context.Context, input I, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[O], error) { + nctx, ok := internal.NexusOperationContextFromGoContext(ctx) + if !ok { + return nil, nexus.HandlerErrorf(nexus.HandlerErrorTypeInternal, "internal error") + } + out, err := o.handler(ctx, nctx.Client, input, options) + if err != nil { + return nil, err + } + return &nexus.HandlerStartOperationResultSync[O]{Value: out}, err +} + +// WorkflowRunOperationOptions are options for [NewWorkflowRunOperationWithOptions]. +// +// NOTE: Experimental +type WorkflowRunOperationOptions[I, O any] struct { + // Operation name. + Name string + // Workflow function to map this operation to. The operation input maps directly to workflow input. + // The workflow name is resolved as it would when using this function in client.ExecuteOperation. + // GetOptions must be provided when setting this option. Mutually exclusive with Handler. + Workflow func(workflow.Context, I) (O, error) + // Options for starting the workflow. Must be set if Workflow is set. Mutually exclusive with Handler. + // The options returned must include a workflow ID that is deterministically generated from the input in order + // for the operation to be idempotent as the request to start the operation may be retried. + // TaskQueue is optional and defaults to the current worker's task queue. + GetOptions func(context.Context, I, nexus.StartOperationOptions) (client.StartWorkflowOptions, error) + // Handler for starting a workflow with a different input than the operation. Mutually exclusive with Workflow + // and GetOptions. + Handler func(context.Context, I, nexus.StartOperationOptions) (WorkflowHandle[O], error) +} + +// NOTE: not implementing GetInfo and GetResult just yet, they're not part of the supported methods in Temporal. +type workflowRunOperation[I, O any] struct { + nexus.UnimplementedOperation[I, O] + + options WorkflowRunOperationOptions[I, O] +} + +// NewWorkflowRunOperation maps an operation to a workflow run. +// +// NOTE: Experimental +func NewWorkflowRunOperation[I, O any]( + name string, + workflow func(workflow.Context, I) (O, error), + getOptions func(context.Context, I, nexus.StartOperationOptions) (client.StartWorkflowOptions, error), +) nexus.Operation[I, O] { + return &workflowRunOperation[I, O]{ + options: WorkflowRunOperationOptions[I, O]{ + Name: name, + Workflow: workflow, + GetOptions: getOptions, + }, + } +} + +// NewWorkflowRunOperation map an operation to a workflow run with the given options. +// Returns an error if invalid options are provided. +// +// NOTE: Experimental +func NewWorkflowRunOperationWithOptions[I, O any](options WorkflowRunOperationOptions[I, O]) (nexus.Operation[I, O], error) { + if options.Name == "" { + return nil, errors.New("invalid options: Name is required") + } + if options.Workflow == nil && options.GetOptions == nil && options.Handler == nil { + return nil, errors.New("invalid options: either GetOptions and Workflow, or Handler are required") + } + if options.Workflow != nil && options.GetOptions == nil || options.Workflow == nil && options.GetOptions != nil { + return nil, errors.New("invalid options: must provide both Workflow and GetOptions") + } + if options.Handler != nil && options.Workflow != nil || options.Handler == nil && options.Workflow == nil { + return nil, errors.New("invalid options: Workflow is mutually exclusive with Handler") + } + return &workflowRunOperation[I, O]{ + options: options, + }, nil +} + +// MustNewWorkflowRunOperation map an operation to a workflow run with the given options. +// Panics if invalid options are provided. +// +// NOTE: Experimental +func MustNewWorkflowRunOperationWithOptions[I, O any](options WorkflowRunOperationOptions[I, O]) nexus.Operation[I, O] { + op, err := NewWorkflowRunOperationWithOptions[I, O](options) + if err != nil { + panic(err) + } + return op +} + +func (*workflowRunOperation[I, O]) Cancel(ctx context.Context, id string, options nexus.CancelOperationOptions) error { + // Prevent the test env client from panicking when we try to use it from a workflow run operation. + ctx = context.WithValue(ctx, internal.IsWorkflowRunOpContextKey, true) + + nctx, ok := internal.NexusOperationContextFromGoContext(ctx) + if !ok { + return nexus.HandlerErrorf(nexus.HandlerErrorTypeInternal, "internal error") + } + return nctx.Client.CancelWorkflow(ctx, id, "") +} + +func (o *workflowRunOperation[I, O]) Name() string { + return o.options.Name +} + +func (o *workflowRunOperation[I, O]) Start(ctx context.Context, input I, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[O], error) { + // Prevent the test env client from panicking when we try to use it from a workflow run operation. + ctx = context.WithValue(ctx, internal.IsWorkflowRunOpContextKey, true) + + if o.options.Handler != nil { + handle, err := o.options.Handler(ctx, input, options) + if err != nil { + return nil, err + } + return &nexus.HandlerStartOperationResultAsync{OperationID: handle.ID()}, nil + } + + wfOpts, err := o.options.GetOptions(ctx, input, options) + if err != nil { + return nil, err + } + + handle, err := ExecuteWorkflow(ctx, options, wfOpts, o.options.Workflow, input) + if err != nil { + return nil, err + } + return &nexus.HandlerStartOperationResultAsync{OperationID: handle.ID()}, nil +} + +// WorkflowHandle is a readonly representation of a workflow run backing a Nexus operation. +// It's created via the [ExecuteWorkflow] and [ExecuteUntypedWorkflow] methods. +// +// NOTE: Experimental +type WorkflowHandle[T any] interface { + // ID is the workflow's ID. + ID() string + // ID is the workflow's run ID. + RunID() string +} + +type workflowHandle[T any] struct { + id string + runID string +} + +func (h workflowHandle[T]) ID() string { + return h.id +} + +func (h workflowHandle[T]) RunID() string { + return h.runID +} + +// ExecuteWorkflow starts a workflow run for a [WorkflowRunOperationOptions] Handler, linking the execution chain to a +// Nexus operation (subsequent runs started from continue-as-new and retries). +// Automatically propagates the callback and request ID from the nexus options to the workflow. +// +// NOTE: Experimental +func ExecuteWorkflow[I, O any, WF func(workflow.Context, I) (O, error)]( + ctx context.Context, + nexusOptions nexus.StartOperationOptions, + startWorkflowOptions client.StartWorkflowOptions, + workflow WF, + arg I, +) (WorkflowHandle[O], error) { + return ExecuteUntypedWorkflow[O](ctx, nexusOptions, startWorkflowOptions, workflow, arg) +} + +// ExecuteUntypedWorkflow starts a workflow with by function reference or string name, linking the execution chain to a +// Nexus operation. +// Useful for invoking workflows that don't follow the single argument - single return type signature. +// See [ExecuteWorkflow] for more information. +// +// NOTE: Experimental +func ExecuteUntypedWorkflow[R any]( + ctx context.Context, + nexusOptions nexus.StartOperationOptions, + startWorkflowOptions client.StartWorkflowOptions, + workflow any, + args ...any, +) (WorkflowHandle[R], error) { + nctx, ok := internal.NexusOperationContextFromGoContext(ctx) + if !ok { + return nil, nexus.HandlerErrorf(nexus.HandlerErrorTypeInternal, "internal error") + } + if startWorkflowOptions.TaskQueue == "" { + startWorkflowOptions.TaskQueue = nctx.TaskQueue + } + + if nexusOptions.RequestID != "" { + internal.SetRequestIDOnStartWorkflowOptions(&startWorkflowOptions, nexusOptions.RequestID) + } + + if nexusOptions.CallbackURL != "" { + internal.SetCallbacksOnStartWorkflowOptions(&startWorkflowOptions, []*common.Callback{ + { + Variant: &common.Callback_Nexus_{ + Nexus: &common.Callback_Nexus{ + Url: nexusOptions.CallbackURL, + Header: nexusOptions.CallbackHeader, + }, + }, + }, + }) + } + run, err := nctx.Client.ExecuteWorkflow(ctx, startWorkflowOptions, workflow, args...) + if err != nil { + return nil, err + } + return workflowHandle[R]{ + id: run.GetID(), + runID: run.GetRunID(), + }, nil +} diff --git a/temporalnexus/operation_test.go b/temporalnexus/operation_test.go new file mode 100644 index 000000000..f590aa34d --- /dev/null +++ b/temporalnexus/operation_test.go @@ -0,0 +1,93 @@ +// The MIT License +// +// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved. +// +// 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. + +package temporalnexus_test + +import ( + "context" + "testing" + + "github.com/nexus-rpc/sdk-go/nexus" + "github.com/stretchr/testify/require" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/temporalnexus" + "go.temporal.io/sdk/workflow" +) + +func TestNewWorkflowRunOperationWithOptions(t *testing.T) { + _, err := temporalnexus.NewWorkflowRunOperationWithOptions(temporalnexus.WorkflowRunOperationOptions[string, string]{}) + require.ErrorContains(t, err, "Name is required") + + _, err = temporalnexus.NewWorkflowRunOperationWithOptions(temporalnexus.WorkflowRunOperationOptions[string, string]{ + Name: "test", + }) + require.ErrorContains(t, err, "either GetOptions and Workflow, or Handler are required") + + _, err = temporalnexus.NewWorkflowRunOperationWithOptions(temporalnexus.WorkflowRunOperationOptions[string, string]{ + Name: "test", + Workflow: func(workflow.Context, string) (string, error) { + return "", nil + }, + }) + require.ErrorContains(t, err, "must provide both Workflow and GetOptions") + + _, err = temporalnexus.NewWorkflowRunOperationWithOptions(temporalnexus.WorkflowRunOperationOptions[string, string]{ + Name: "test", + GetOptions: func(ctx context.Context, s string, soo nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{}, nil + }, + }) + require.ErrorContains(t, err, "must provide both Workflow and GetOptions") + + _, err = temporalnexus.NewWorkflowRunOperationWithOptions(temporalnexus.WorkflowRunOperationOptions[string, string]{ + Name: "test", + Workflow: func(workflow.Context, string) (string, error) { + return "", nil + }, + GetOptions: func(ctx context.Context, s string, soo nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{}, nil + }, + Handler: func(ctx context.Context, s string, soo nexus.StartOperationOptions) (temporalnexus.WorkflowHandle[string], error) { + return nil, nil + }, + }) + require.ErrorContains(t, err, "Workflow is mutually exclusive with Handler") + + _, err = temporalnexus.NewWorkflowRunOperationWithOptions(temporalnexus.WorkflowRunOperationOptions[string, string]{ + Name: "test", + Workflow: func(workflow.Context, string) (string, error) { + return "", nil + }, + GetOptions: func(ctx context.Context, s string, soo nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{}, nil + }, + }) + require.NoError(t, err) + + _, err = temporalnexus.NewWorkflowRunOperationWithOptions(temporalnexus.WorkflowRunOperationOptions[string, string]{ + Name: "test", + Handler: func(ctx context.Context, s string, soo nexus.StartOperationOptions) (temporalnexus.WorkflowHandle[string], error) { + return nil, nil + }, + }) + require.NoError(t, err) +} diff --git a/test/go.mod b/test/go.mod index cdbbe4cd6..85149bd1a 100644 --- a/test/go.mod +++ b/test/go.mod @@ -2,9 +2,12 @@ module go.temporal.io/sdk/test go 1.21 +toolchain go1.21.1 + require ( github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 + github.com/nexus-rpc/sdk-go v0.0.9 github.com/opentracing/opentracing-go v1.2.0 github.com/pborman/uuid v1.2.1 github.com/stretchr/testify v1.9.0 @@ -12,13 +15,13 @@ require ( go.opentelemetry.io/otel v1.27.0 go.opentelemetry.io/otel/sdk v1.27.0 go.opentelemetry.io/otel/trace v1.27.0 - go.temporal.io/api v1.35.0 + go.temporal.io/api v1.36.0 go.temporal.io/sdk v1.12.0 go.temporal.io/sdk/contrib/opentelemetry v0.1.0 go.temporal.io/sdk/contrib/opentracing v0.0.0-00010101000000-000000000000 go.temporal.io/sdk/contrib/tally v0.1.0 go.uber.org/goleak v1.1.11 - google.golang.org/grpc v1.64.0 + google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 ) @@ -37,12 +40,12 @@ require ( go.opentelemetry.io/otel/metric v1.27.0 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/test/go.sum b/test/go.sum index da09084bf..ede7f6bba 100644 --- a/test/go.sum +++ b/test/go.sum @@ -97,6 +97,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN 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/nexus-rpc/sdk-go v0.0.9 h1:yQ16BlDWZ6EMjim/SMd8lsUGTj6TPxFioqLGP8/PJDQ= +github.com/nexus-rpc/sdk-go v0.0.9/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= 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= @@ -156,8 +158,8 @@ go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2N go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= -go.temporal.io/api v1.35.0 h1:+1o2zyBncLjnpjJt4FedKZMtuUav/LUgTB+mhQxx0zs= -go.temporal.io/api v1.35.0/go.mod h1:OYkuupuCw6s/5TkcKHMb9EcIrOI+vTsbf/CGaprbzb0= +go.temporal.io/api v1.36.0 h1:WdntOw9m38lFvMdMXuOO+3BQ0R8HpVLgtk9+f+FwiDk= +go.temporal.io/api v1.36.0/go.mod h1:0nWIrFRVPlcrkopXqxir/UWOtz/NZCo+EE9IX4UwVxw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -194,8 +196,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL 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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -223,8 +225,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w 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.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -255,17 +257,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= -google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 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= diff --git a/test/integration_test.go b/test/integration_test.go index e0541549c..db41eba78 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -4935,6 +4935,10 @@ func (ts *IntegrationTestSuite) TestScheduleList() { ts.GreaterOrEqual(5, len(events)) ts.NoError(err) + // TODO: unskip once https://github.com/temporalio/temporal/issues/6319 is fixed + if os.Getenv("DISABLE_SERVER_1_25_TESTS") != "" { + return + } // query -- match ts.Eventually(func() bool { iter, err = ts.client.ScheduleClient().List(ctx, client.ScheduleListOptions{ diff --git a/test/nexus_test.go b/test/nexus_test.go new file mode 100644 index 000000000..e74a3e0f2 --- /dev/null +++ b/test/nexus_test.go @@ -0,0 +1,923 @@ +// The MIT License +// +// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved. +// +// 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. + +package test_test + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "slices" + "strings" + "testing" + "time" + + "github.com/google/uuid" + "github.com/nexus-rpc/sdk-go/nexus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.temporal.io/api/common/v1" + "go.temporal.io/api/enums/v1" + historypb "go.temporal.io/api/history/v1" + nexuspb "go.temporal.io/api/nexus/v1" + "go.temporal.io/api/operatorservice/v1" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/internal/common/metrics" + ilog "go.temporal.io/sdk/internal/log" + "go.temporal.io/sdk/temporal" + "go.temporal.io/sdk/temporalnexus" + "go.temporal.io/sdk/testsuite" + "go.temporal.io/sdk/worker" + "go.temporal.io/sdk/workflow" +) + +type testContext struct { + client client.Client + metricsHandler *metrics.CapturingHandler + testConfig Config + taskQueue, endpoint, endpointBaseURL string +} + +func newTestContext(t *testing.T, ctx context.Context) *testContext { + config := NewConfig() + require.NoError(t, WaitForTCP(time.Minute, config.ServiceAddr)) + + metricsHandler := metrics.NewCapturingHandler() + c, err := client.DialContext(ctx, client.Options{ + HostPort: config.ServiceAddr, + Namespace: config.Namespace, + Logger: ilog.NewDefaultLogger(), + ConnectionOptions: client.ConnectionOptions{TLS: config.TLS}, + MetricsHandler: metricsHandler, + }) + require.NoError(t, err) + + taskQueue := "sdk-go-nexus-test-tq-" + uuid.NewString() + endpoint := strings.ReplaceAll("sdk-go-nexus-test-ep-"+uuid.NewString(), "-", "_") + res, err := c.OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + Spec: &nexuspb.EndpointSpec{ + Name: endpoint, + Target: &nexuspb.EndpointTarget{ + Variant: &nexuspb.EndpointTarget_Worker_{ + Worker: &nexuspb.EndpointTarget_Worker{ + Namespace: config.Namespace, + TaskQueue: taskQueue, + }, + }, + }, + }, + }) + require.NoError(t, err) + + scheme := "http" + if config.TLS != nil { + scheme = "https" + } + endpointBaseURL := scheme + "://" + config.ServiceHTTPAddr + res.Endpoint.UrlPrefix + + tc := &testContext{ + client: c, + testConfig: config, + metricsHandler: metricsHandler, + taskQueue: taskQueue, + endpoint: endpoint, + endpointBaseURL: endpointBaseURL, + } + + return tc +} + +func (tc *testContext) newNexusClient(t *testing.T, service string) *nexus.Client { + httpClient := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tc.testConfig.TLS, + }, + } + nc, err := nexus.NewClient(nexus.ClientOptions{ + BaseURL: tc.endpointBaseURL, + Service: service, + HTTPCaller: func(r *http.Request) (*http.Response, error) { + attempt := 0 + for { + attempt++ + res, err := httpClient.Do(r) + // Give the endpoint configuration some time to propagate in the frontend. + // This should not take more than a few milliseconds. + // TODO(bergundy): Remove this once the server supports cache read through for unknown endpoints. + if attempt < 10 && err == nil && res.StatusCode == http.StatusNotFound { + time.Sleep(time.Millisecond * 100) + continue + } + return res, err + } + }, + }) + require.NoError(t, err) + return nc +} + +func (tc *testContext) requireTimer(t *assert.CollectT, metric, service, operation string) { + assert.True(t, slices.ContainsFunc(tc.metricsHandler.Timers(), func(ct *metrics.CapturedTimer) bool { + return ct.Name == metric && + ct.Tags[metrics.NexusServiceTagName] == service && + ct.Tags[metrics.NexusOperationTagName] == operation + })) +} + +func (tc *testContext) requireCounter(t *assert.CollectT, metric, service, operation string) { + assert.True(t, slices.ContainsFunc(tc.metricsHandler.Counters(), func(ct *metrics.CapturedCounter) bool { + return ct.Name == metric && + ct.Tags[metrics.NexusServiceTagName] == service && + ct.Tags[metrics.NexusOperationTagName] == operation + })) +} + +var syncOp = temporalnexus.NewSyncOperation("sync-op", func(ctx context.Context, c client.Client, s string, o nexus.StartOperationOptions) (string, error) { + switch s { + case "ok": + // Verify options are properly propagated. + if _, ok := ctx.Deadline(); !ok { + return "", nexus.HandlerErrorf(nexus.HandlerErrorTypeBadRequest, "expected context deadline to be set") + } + if o.RequestID != "test-request-id" { + return "", nexus.HandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid request ID, got: %v", o.RequestID) + } + if o.Header.Get("test") != "ok" { + return "", nexus.HandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid test header, got: %v", o.Header.Get("test")) + } + if o.CallbackURL != "http://localhost/test" { + return "", nexus.HandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid test callback URL, got: %v", o.CallbackURL) + } + if o.CallbackHeader.Get("test") != "ok" { + return "", nexus.HandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid test callback header, got: %v", o.CallbackHeader.Get("test")) + } + return s, nil + case "fail": + return "", &nexus.UnsuccessfulOperationError{ + State: nexus.OperationStateFailed, + Failure: nexus.Failure{ + Message: "fail", + }, + } + case "handlererror": + return "", nexus.HandlerErrorf(nexus.HandlerErrorTypeBadRequest, s) + case "panic": + panic("panic") + } + return "", nil +}) + +func waitForCancelWorkflow(ctx workflow.Context, ownID string) (string, error) { + return "", workflow.Await(ctx, func() bool { return false }) +} + +var workflowOp = temporalnexus.NewWorkflowRunOperation( + "workflow-op", + waitForCancelWorkflow, + func(ctx context.Context, id string, soo nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{ID: id}, nil + }, +) + +func TestNexusSyncOperation(t *testing.T) { + if os.Getenv("DISABLE_NEXUS_TESTS") != "" { + t.SkipNow() + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + tc := newTestContext(t, ctx) + + w := worker.New(tc.client, tc.taskQueue, worker.Options{}) + service := nexus.NewService("test") + require.NoError(t, service.Register(syncOp, workflowOp)) + w.RegisterNexusService(service) + w.RegisterWorkflow(waitForCancelWorkflow) + require.NoError(t, w.Start()) + t.Cleanup(w.Stop) + + nc := tc.newNexusClient(t, service.Name) + + t.Run("ok", func(t *testing.T) { + tc.metricsHandler.Clear() + result, err := nexus.ExecuteOperation(ctx, nc, syncOp, "ok", nexus.ExecuteOperationOptions{ + RequestID: "test-request-id", + Header: nexus.Header{"test": "ok"}, + CallbackURL: "http://localhost/test", + CallbackHeader: nexus.Header{"test": "ok"}, + }) + require.NoError(t, err) + require.Equal(t, "ok", result) + + require.EventuallyWithT(t, func(t *assert.CollectT) { + tc.requireTimer(t, metrics.NexusTaskEndToEndLatency, service.Name, syncOp.Name()) + tc.requireTimer(t, metrics.NexusTaskScheduleToStartLatency, service.Name, syncOp.Name()) + tc.requireTimer(t, metrics.NexusTaskExecutionLatency, service.Name, syncOp.Name()) + }, time.Second*3, time.Millisecond*100) + }) + + t.Run("fail", func(t *testing.T) { + tc.metricsHandler.Clear() + _, err := nexus.ExecuteOperation(ctx, nc, syncOp, "fail", nexus.ExecuteOperationOptions{}) + var unsuccessfulOperationErr *nexus.UnsuccessfulOperationError + require.ErrorAs(t, err, &unsuccessfulOperationErr) + require.Equal(t, nexus.OperationStateFailed, unsuccessfulOperationErr.State) + require.Equal(t, "fail", unsuccessfulOperationErr.Failure.Message) + }) + + t.Run("handlererror", func(t *testing.T) { + _, err := nexus.ExecuteOperation(ctx, nc, syncOp, "handlererror", nexus.ExecuteOperationOptions{}) + var unexpectedResponseErr *nexus.UnexpectedResponseError + require.ErrorAs(t, err, &unexpectedResponseErr) + require.Equal(t, http.StatusBadRequest, unexpectedResponseErr.Response.StatusCode) + require.Contains(t, unexpectedResponseErr.Message, `"400 Bad Request": handlererror`) + + require.EventuallyWithT(t, func(t *assert.CollectT) { + tc.requireTimer(t, metrics.NexusTaskEndToEndLatency, service.Name, syncOp.Name()) + tc.requireTimer(t, metrics.NexusTaskScheduleToStartLatency, service.Name, syncOp.Name()) + tc.requireTimer(t, metrics.NexusTaskExecutionLatency, service.Name, syncOp.Name()) + tc.requireCounter(t, metrics.NexusTaskExecutionFailedCounter, service.Name, syncOp.Name()) + }, time.Second*3, time.Millisecond*100) + }) + + t.Run("panic", func(t *testing.T) { + _, err := nexus.ExecuteOperation(ctx, nc, syncOp, "panic", nexus.ExecuteOperationOptions{}) + var unexpectedResponseErr *nexus.UnexpectedResponseError + require.ErrorAs(t, err, &unexpectedResponseErr) + require.Equal(t, 500, unexpectedResponseErr.Response.StatusCode) + require.Contains(t, unexpectedResponseErr.Message, "internal error") + }) +} + +func TestNexusWorkflowRunOperation(t *testing.T) { + if os.Getenv("DISABLE_NEXUS_TESTS") != "" { + t.SkipNow() + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + tc := newTestContext(t, ctx) + + w := worker.New(tc.client, tc.taskQueue, worker.Options{}) + service := nexus.NewService("test") + require.NoError(t, service.Register(syncOp, workflowOp)) + w.RegisterNexusService(service) + w.RegisterWorkflow(waitForCancelWorkflow) + require.NoError(t, w.Start()) + t.Cleanup(w.Stop) + + nc := tc.newNexusClient(t, service.Name) + + workflowID := "nexus-handler-workflow-" + uuid.NewString() + result, err := nexus.StartOperation(ctx, nc, workflowOp, workflowID, nexus.StartOperationOptions{ + CallbackURL: "http://localhost/test", + CallbackHeader: nexus.Header{"test": "ok"}, + }) + require.NoError(t, err) + require.NotNil(t, result.Pending) + handle := result.Pending + require.Equal(t, workflowID, handle.ID) + desc, err := tc.client.DescribeWorkflowExecution(ctx, workflowID, "") + require.NoError(t, err) + + require.Equal(t, 1, len(desc.Callbacks)) + callback, ok := desc.Callbacks[0].Callback.Variant.(*common.Callback_Nexus_) + require.True(t, ok) + require.Equal(t, "http://localhost/test", callback.Nexus.Url) + require.Equal(t, map[string]string{"test": "ok"}, callback.Nexus.Header) + + run := tc.client.GetWorkflow(ctx, workflowID, "") + require.NoError(t, handle.Cancel(ctx, nexus.CancelOperationOptions{})) + require.ErrorContains(t, run.Get(ctx, nil), "canceled") +} + +func TestSyncOperationFromWorkflow(t *testing.T) { + if os.Getenv("DISABLE_NEXUS_TESTS") != "" { + t.SkipNow() + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + tc := newTestContext(t, ctx) + + op := temporalnexus.NewSyncOperation("op", func(ctx context.Context, c client.Client, outcome string, o nexus.StartOperationOptions) (string, error) { + switch outcome { + case "successful": + return outcome, nil + case "failed": + return "", &nexus.UnsuccessfulOperationError{ + State: nexus.OperationStateFailed, + Failure: nexus.Failure{Message: "failed for test"}, + } + case "canceled": + return "", &nexus.UnsuccessfulOperationError{ + State: nexus.OperationStateCanceled, + Failure: nexus.Failure{Message: "canceled for test"}, + } + default: + panic(fmt.Errorf("unexpected outcome: %s", outcome)) + } + }) + + wf := func(ctx workflow.Context, outcome string) error { + c := workflow.NewNexusClient(tc.endpoint, "test") + fut := c.ExecuteOperation(ctx, op, outcome, workflow.NexusOperationOptions{}) + var res string + + var exec workflow.NexusOperationExecution + if err := fut.GetNexusOperationExecution().Get(ctx, &exec); err != nil && outcome == "successful" { + return fmt.Errorf("expected start to succeed: %w", err) + } + if exec.OperationID != "" { + return fmt.Errorf("expected empty operation ID") + } + if err := fut.Get(ctx, &res); err != nil { + return err + } + // If the operation didn't fail the only expected result is "successful". + if res != "successful" { + return fmt.Errorf("unexpected result: %v", res) + } + return nil + } + + w := worker.New(tc.client, tc.taskQueue, worker.Options{}) + service := nexus.NewService("test") + require.NoError(t, service.Register(op)) + w.RegisterNexusService(service) + w.RegisterWorkflow(wf) + require.NoError(t, w.Start()) + t.Cleanup(w.Stop) + + t.Run("OpSuccessful", func(t *testing.T) { + run, err := tc.client.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: tc.taskQueue, + // The endpoint registry may take a bit to propagate to the history service, use a shorter workflow task + // timeout to speed up the attempts. + WorkflowTaskTimeout: time.Second, + }, wf, "successful") + require.NoError(t, err) + require.NoError(t, run.Get(ctx, nil)) + }) + + t.Run("OpFailed", func(t *testing.T) { + run, err := tc.client.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: tc.taskQueue, + // The endpoint registry may take a bit to propagate to the history service, use a shorter workflow task + // timeout to speed up the attempts. + WorkflowTaskTimeout: time.Second, + }, wf, "failed") + require.NoError(t, err) + var execErr *temporal.WorkflowExecutionError + err = run.Get(ctx, nil) + require.ErrorAs(t, err, &execErr) + var opErr *temporal.NexusOperationError + err = execErr.Unwrap() + require.ErrorAs(t, err, &opErr) + require.Equal(t, tc.endpoint, opErr.Endpoint) + require.Equal(t, "test", opErr.Service) + require.Equal(t, op.Name(), opErr.Operation) + require.Equal(t, "", opErr.OperationID) + require.Equal(t, "nexus operation completed unsuccessfully", opErr.Message) + require.Greater(t, opErr.ScheduledEventID, int64(0)) + err = opErr.Unwrap() + var appErr *temporal.ApplicationError + require.ErrorAs(t, err, &appErr) + require.Equal(t, "failed for test", appErr.Message()) + }) + + t.Run("OpCanceled", func(t *testing.T) { + run, err := tc.client.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: tc.taskQueue, + // The endpoint registry may take a bit to propagate to the history service, use a shorter workflow task + // timeout to speed up the attempts. + WorkflowTaskTimeout: time.Second, + }, wf, "canceled") + require.NoError(t, err) + var execErr *temporal.WorkflowExecutionError + err = run.Get(ctx, nil) + require.ErrorAs(t, err, &execErr) + // The Go SDK unwraps workflow errors to check for cancelation even if the workflow was never canceled, losing + // the error chain, Nexus operation errors are treated the same as other workflow errors for consistency. + var canceledErr *temporal.CanceledError + err = execErr.Unwrap() + require.ErrorAs(t, err, &canceledErr) + }) +} + +func TestAsyncOperationFromWorkflow(t *testing.T) { + if os.Getenv("DISABLE_NEXUS_TESTS") != "" { + t.SkipNow() + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + tc := newTestContext(t, ctx) + + handlerWorkflow := func(ctx workflow.Context, action string) (string, error) { + switch action { + case "succeed": + return action, nil + case "fail": + return "", fmt.Errorf("handler workflow failed in test") + case "wait-for-cancel": + return "", workflow.Await(ctx, func() bool { return false }) + default: + panic(fmt.Errorf("unexpected outcome: %s", action)) + } + } + op := temporalnexus.NewWorkflowRunOperation("op", handlerWorkflow, func(ctx context.Context, action string, soo nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + if action == "fail-to-start" { + return client.StartWorkflowOptions{}, nexus.HandlerErrorf(nexus.HandlerErrorTypeInternal, "fake internal error") + } + return client.StartWorkflowOptions{ + ID: soo.RequestID, + }, nil + }) + callerWorkflow := func(ctx workflow.Context, action string) error { + c := workflow.NewNexusClient(tc.endpoint, "test") + ctx, cancel := workflow.WithCancel(ctx) + defer cancel() + fut := c.ExecuteOperation(ctx, op, action, workflow.NexusOperationOptions{}) + var res string + ch := workflow.GetSignalChannel(ctx, "cancel-op") + workflow.Go(ctx, func(ctx workflow.Context) { + var action string + ch.Receive(ctx, &action) + switch action { + case "wait-for-started": + fut.GetNexusOperationExecution().Get(ctx, nil) + case "sleep": + workflow.Sleep(ctx, time.Millisecond) + } + cancel() + }) + var exec workflow.NexusOperationExecution + if err := fut.GetNexusOperationExecution().Get(ctx, &exec); err != nil && action != "fail-to-start" { + return fmt.Errorf("expected start to succeed: %w", err) + } + if exec.OperationID == "" && action != "fail-to-start" { + return fmt.Errorf("expected non empty operation ID") + } + if err := fut.Get(ctx, &res); err != nil { + return err + } + // If the operation didn't fail the only expected result is "successful". + if res != "succeed" { + return fmt.Errorf("unexpected result: %v", res) + } + return nil + } + + w := worker.New(tc.client, tc.taskQueue, worker.Options{}) + service := nexus.NewService("test") + require.NoError(t, service.Register(op)) + w.RegisterNexusService(service) + w.RegisterWorkflow(handlerWorkflow) + w.RegisterWorkflow(callerWorkflow) + require.NoError(t, w.Start()) + t.Cleanup(w.Stop) + + t.Run("OpSuccessful", func(t *testing.T) { + run, err := tc.client.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: tc.taskQueue, + // The endpoint registry may take a bit to propagate to the history service, use a shorter workflow task + // timeout to speed up the attempts. + WorkflowTaskTimeout: time.Second, + }, callerWorkflow, "succeed") + require.NoError(t, err) + require.NoError(t, run.Get(ctx, nil)) + }) + + t.Run("OpFailed", func(t *testing.T) { + run, err := tc.client.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: tc.taskQueue, + // The endpoint registry may take a bit to propagate to the history service, use a shorter workflow task + // timeout to speed up the attempts. + WorkflowTaskTimeout: time.Second, + }, callerWorkflow, "fail") + require.NoError(t, err) + var execErr *temporal.WorkflowExecutionError + err = run.Get(ctx, nil) + require.ErrorAs(t, err, &execErr) + var opErr *temporal.NexusOperationError + err = execErr.Unwrap() + require.ErrorAs(t, err, &opErr) + require.Equal(t, tc.endpoint, opErr.Endpoint) + require.Equal(t, "test", opErr.Service) + require.Equal(t, op.Name(), opErr.Operation) + require.NotEmpty(t, opErr.OperationID) + require.Equal(t, "nexus operation completed unsuccessfully", opErr.Message) + require.Greater(t, opErr.ScheduledEventID, int64(0)) + err = opErr.Unwrap() + var appErr *temporal.ApplicationError + require.ErrorAs(t, err, &appErr) + require.Equal(t, "handler workflow failed in test", appErr.Message()) + }) + + t.Run("OpCanceledBeforeSent", func(t *testing.T) { + run, err := tc.client.SignalWithStartWorkflow(ctx, uuid.NewString(), "cancel-op", "no-wait", client.StartWorkflowOptions{ + TaskQueue: tc.taskQueue, + }, callerWorkflow, "wait-for-cancel") + require.NoError(t, err) + var execErr *temporal.WorkflowExecutionError + err = run.Get(ctx, nil) + require.ErrorAs(t, err, &execErr) + // The Go SDK unwraps workflow errors to check for cancelation even if the workflow was never canceled, losing + // the error chain, Nexus operation errors are treated the same as other workflow errors for consistency. + var canceledErr *temporal.CanceledError + err = execErr.Unwrap() + require.ErrorAs(t, err, &canceledErr) + + // Verify that the operation was never scheduled. + history := tc.client.GetWorkflowHistory(ctx, run.GetID(), run.GetRunID(), false, enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + for history.HasNext() { + event, err := history.Next() + require.NoError(t, err) + require.NotEqual(t, enums.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, event.EventType) + } + }) + + t.Run("OpCanceledBeforeStarted", func(t *testing.T) { + run, err := tc.client.SignalWithStartWorkflow(ctx, uuid.NewString(), "cancel-op", "sleep", client.StartWorkflowOptions{ + TaskQueue: tc.taskQueue, + }, callerWorkflow, "fail-to-start") + require.NoError(t, err) + var execErr *temporal.WorkflowExecutionError + err = run.Get(ctx, nil) + require.ErrorAs(t, err, &execErr) + // The Go SDK unwraps workflow errors to check for cancelation even if the workflow was never canceled, losing + // the error chain, Nexus operation errors are treated the same as other workflow errors for consistency. + var canceledErr *temporal.CanceledError + err = execErr.Unwrap() + require.ErrorAs(t, err, &canceledErr) + }) + + t.Run("OpCanceledAfterStarted", func(t *testing.T) { + run, err := tc.client.SignalWithStartWorkflow(ctx, uuid.NewString(), "cancel-op", "wait-for-started", client.StartWorkflowOptions{ + TaskQueue: tc.taskQueue, + }, callerWorkflow, "wait-for-cancel") + require.NoError(t, err) + var execErr *temporal.WorkflowExecutionError + err = run.Get(ctx, nil) + require.ErrorAs(t, err, &execErr) + // The Go SDK unwraps workflow errors to check for cancelation even if the workflow was never canceled, losing + // the error chain, Nexus operation errors are treated the same as other workflow errors for consistency. + var canceledErr *temporal.CanceledError + err = execErr.Unwrap() + require.ErrorAs(t, err, &canceledErr) + }) +} + +func TestReplay(t *testing.T) { + if os.Getenv("DISABLE_NEXUS_TESTS") != "" { + t.SkipNow() + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + tc := newTestContext(t, ctx) + + op := temporalnexus.NewSyncOperation("op", func(ctx context.Context, c client.Client, nv nexus.NoValue, soo nexus.StartOperationOptions) (nexus.NoValue, error) { + return nil, nil + }) + + endpointForTest := tc.endpoint + serviceForTest := "test" + opForTest := op.Name() + + callerWorkflow := func(ctx workflow.Context) error { + c := workflow.NewNexusClient(endpointForTest, serviceForTest) + ctx, cancel := workflow.WithCancel(ctx) + defer cancel() + fut := c.ExecuteOperation(ctx, opForTest, nil, workflow.NexusOperationOptions{}) + if err := fut.Get(ctx, nil); err != nil { + return err + } + return nil + } + + w := worker.New(tc.client, tc.taskQueue, worker.Options{}) + service := nexus.NewService("test") + require.NoError(t, service.Register(op)) + w.RegisterNexusService(service) + w.RegisterWorkflow(callerWorkflow) + require.NoError(t, w.Start()) + t.Cleanup(w.Stop) + + run, err := tc.client.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: tc.taskQueue, + // The endpoint registry may take a bit to propagate to the history service, use a shorter workflow task + // timeout to speed up the attempts. + WorkflowTaskTimeout: time.Second, + }, callerWorkflow) + require.NoError(t, err) + require.NoError(t, run.Get(ctx, nil)) + + events := make([]*historypb.HistoryEvent, 0) + hist := tc.client.GetWorkflowHistory(ctx, run.GetID(), run.GetRunID(), false, enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + for hist.HasNext() { + e, err := hist.Next() + require.NoError(t, err) + events = append(events, e) + } + + t.Run("OK", func(t *testing.T) { + // endpointForTest, serviceForTest = tc.endpoint, "test" + rw := worker.NewWorkflowReplayer() + rw.RegisterWorkflow(callerWorkflow) + err = rw.ReplayWorkflowHistory(ilog.NewDefaultLogger(), &historypb.History{Events: events}) + require.NoError(t, err) + }) + + t.Run("EndpointMismatchOK", func(t *testing.T) { + endpointForTest = "endpoint-changed" // It's okay to change the endpoint as it is environment specific. + // endpointForTest, serviceForTest = tc.endpoint, "test" + rw := worker.NewWorkflowReplayer() + rw.RegisterWorkflow(callerWorkflow) + err = rw.ReplayWorkflowHistory(ilog.NewDefaultLogger(), &historypb.History{Events: events}) + require.NoError(t, err) + }) + + t.Run("ServiceMismatchNDE", func(t *testing.T) { + serviceForTest = "service-changed" + // endpointForTest, serviceForTest = tc.endpoint, "test" + rw := worker.NewWorkflowReplayer() + rw.RegisterWorkflow(callerWorkflow) + err = rw.ReplayWorkflowHistory(ilog.NewDefaultLogger(), &historypb.History{Events: events}) + require.ErrorContains(t, err, "[TMPRL1100]") + }) + + t.Run("OperationMismatchNDE", func(t *testing.T) { + serviceForTest = "test" // Restore + opForTest = "op-changed" + rw := worker.NewWorkflowReplayer() + rw.RegisterWorkflow(callerWorkflow) + err = rw.ReplayWorkflowHistory(ilog.NewDefaultLogger(), &historypb.History{Events: events}) + require.ErrorContains(t, err, "[TMPRL1100]") + }) +} + +func TestWorkflowTestSuite_NexusSyncOperation(t *testing.T) { + op := nexus.NewSyncOperation("op", func(ctx context.Context, outcome string, opts nexus.StartOperationOptions) (string, error) { + switch outcome { + case "ok": + return outcome, nil + case "failure": + return "", &nexus.UnsuccessfulOperationError{ + State: nexus.OperationStateFailed, + Failure: nexus.Failure{ + Message: "test operation failed", + }, + } + case "handler-error": + return "", &nexus.HandlerError{ + Type: nexus.HandlerErrorTypeBadRequest, + Failure: &nexus.Failure{ + Message: "test operation failed", + }, + } + } + panic(fmt.Errorf("invalid outcome: %q", outcome)) + }) + wf := func(ctx workflow.Context, outcome string) error { + client := workflow.NewNexusClient("endpoint", "test") + fut := client.ExecuteOperation(ctx, op, outcome, workflow.NexusOperationOptions{}) + var exec workflow.NexusOperationExecution + if err := fut.GetNexusOperationExecution().Get(ctx, &exec); err != nil { + return err + } + var res string + if err := fut.Get(ctx, &res); err != nil { + return err + } + if res != "ok" { + return fmt.Errorf("unexpected result: %v", res) + } + return nil + } + + service := nexus.NewService("test") + service.Register(op) + + t.Run("ok", func(t *testing.T) { + suite := testsuite.WorkflowTestSuite{} + env := suite.NewTestWorkflowEnvironment() + env.RegisterNexusService(service) + env.ExecuteWorkflow(wf, "ok") + require.True(t, env.IsWorkflowCompleted()) + require.NoError(t, env.GetWorkflowError()) + }) + + for _, outcome := range []string{"failure", "handler-error"} { + outcome := outcome // capture just in case. + t.Run(outcome, func(t *testing.T) { + suite := testsuite.WorkflowTestSuite{} + env := suite.NewTestWorkflowEnvironment() + env.RegisterNexusService(service) + env.ExecuteWorkflow(wf, "failure") + require.True(t, env.IsWorkflowCompleted()) + var execErr *temporal.WorkflowExecutionError + err := env.GetWorkflowError() + require.ErrorAs(t, err, &execErr) + var opErr *temporal.NexusOperationError + err = execErr.Unwrap() + require.ErrorAs(t, err, &opErr) + require.Equal(t, "endpoint", opErr.Endpoint) + require.Equal(t, "test", opErr.Service) + require.Equal(t, op.Name(), opErr.Operation) + require.Empty(t, opErr.OperationID) + require.Equal(t, "nexus operation completed unsuccessfully", opErr.Message) + err = opErr.Unwrap() + var appErr *temporal.ApplicationError + require.ErrorAs(t, err, &appErr) + require.Equal(t, "test operation failed", appErr.Message()) + }) + } +} + +func TestWorkflowTestSuite_WorkflowRunOperation(t *testing.T) { + handlerWF := func(ctx workflow.Context, outcome string) (string, error) { + if outcome == "ok" { + return "ok", nil + } + return "", fmt.Errorf("expected failure") + } + + op := temporalnexus.NewWorkflowRunOperation( + "op", + handlerWF, + func(ctx context.Context, id string, opts nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{ID: opts.RequestID}, nil + }) + + callerWF := func(ctx workflow.Context, outcome string) error { + client := workflow.NewNexusClient("endpoint", "test") + fut := client.ExecuteOperation(ctx, op, outcome, workflow.NexusOperationOptions{}) + var exec workflow.NexusOperationExecution + if err := fut.GetNexusOperationExecution().Get(ctx, &exec); err != nil { + return err + } + if exec.OperationID == "" { + return errors.New("got empty operation ID") + } + + var result string + if err := fut.Get(ctx, &result); err != nil { + return err + } + if result != "ok" { + return fmt.Errorf("expected result to be 'ok', got: %s", result) + } + return nil + } + + service := nexus.NewService("test") + service.Register(op) + + t.Run("ok", func(t *testing.T) { + suite := testsuite.WorkflowTestSuite{} + env := suite.NewTestWorkflowEnvironment() + env.RegisterWorkflow(handlerWF) + env.RegisterNexusService(service) + + env.ExecuteWorkflow(callerWF, "ok") + require.True(t, env.IsWorkflowCompleted()) + require.NoError(t, env.GetWorkflowError()) + }) + + t.Run("fail", func(t *testing.T) { + suite := testsuite.WorkflowTestSuite{} + env := suite.NewTestWorkflowEnvironment() + env.RegisterWorkflow(handlerWF) + env.RegisterNexusService(service) + + env.ExecuteWorkflow(callerWF, "fail") + require.True(t, env.IsWorkflowCompleted()) + + var execErr *temporal.WorkflowExecutionError + err := env.GetWorkflowError() + require.ErrorAs(t, err, &execErr) + var opErr *temporal.NexusOperationError + err = execErr.Unwrap() + require.ErrorAs(t, err, &opErr) + require.Equal(t, "endpoint", opErr.Endpoint) + require.Equal(t, "test", opErr.Service) + require.Equal(t, op.Name(), opErr.Operation) + require.Equal(t, "nexus operation completed unsuccessfully", opErr.Message) + err = opErr.Unwrap() + var appErr *temporal.ApplicationError + require.ErrorAs(t, err, &appErr) + require.Equal(t, "expected failure", appErr.Message()) + }) +} + +func TestWorkflowTestSuite_WorkflowRunOperation_WithCancel(t *testing.T) { + wf := func(ctx workflow.Context, cancelBeforeStarted bool) error { + childCtx, cancel := workflow.WithCancel(ctx) + defer cancel() + + client := workflow.NewNexusClient("endpoint", "test") + fut := client.ExecuteOperation(childCtx, workflowOp, "op-id", workflow.NexusOperationOptions{}) + if cancelBeforeStarted { + cancel() + } + var exec workflow.NexusOperationExecution + if err := fut.GetNexusOperationExecution().Get(ctx, &exec); err != nil { + return err + } + if exec.OperationID != "op-id" { + return fmt.Errorf("unexpected operation ID: %q", exec.OperationID) + } + + if !cancelBeforeStarted { + cancel() + } + err := fut.Get(ctx, nil) + return err + } + + service := nexus.NewService("test") + service.Register(workflowOp) + + cases := []struct { + cancelBeforeStarted bool + name string + }{ + {false, "AfterStarted"}, + {true, "BeforeStarted"}, + } + for _, tc := range cases { + tc := tc // capture just in case. + t.Run(tc.name, func(t *testing.T) { + suite := testsuite.WorkflowTestSuite{} + env := suite.NewTestWorkflowEnvironment() + env.RegisterWorkflow(waitForCancelWorkflow) + env.RegisterNexusService(service) + env.ExecuteWorkflow(wf, tc.cancelBeforeStarted) + require.True(t, env.IsWorkflowCompleted()) + // Error wrapping is different in the test environment than the server (same as for child workflows). + var execErr *temporal.WorkflowExecutionError + err := env.GetWorkflowError() + require.ErrorAs(t, err, &execErr) + var opErr *temporal.NexusOperationError + err = execErr.Unwrap() + require.ErrorAs(t, err, &opErr) + require.Equal(t, "endpoint", opErr.Endpoint) + require.Equal(t, "test", opErr.Service) + require.Equal(t, workflowOp.Name(), opErr.Operation) + require.Equal(t, "op-id", opErr.OperationID) + require.Equal(t, "nexus operation completed unsuccessfully", opErr.Message) + err = opErr.Unwrap() + var canceledError *temporal.CanceledError + require.ErrorAs(t, err, &canceledError) + }) + } +} + +func TestWorkflowTestSuite_NexusSyncOperation_ClientMethods_Panic(t *testing.T) { + var panicReason any + op := temporalnexus.NewSyncOperation("signal-op", func(ctx context.Context, c client.Client, _ nexus.NoValue, opts nexus.StartOperationOptions) (nexus.NoValue, error) { + func() { + defer func() { + panicReason = recover() + }() + c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{}, "test", "", "get-secret") + }() + return nil, nil + }) + wf := func(ctx workflow.Context) error { + client := workflow.NewNexusClient("endpoint", "test") + fut := client.ExecuteOperation(ctx, op, nil, workflow.NexusOperationOptions{}) + return fut.Get(ctx, nil) + } + + service := nexus.NewService("test") + service.Register(op) + + suite := testsuite.WorkflowTestSuite{} + env := suite.NewTestWorkflowEnvironment() + env.RegisterWorkflow(waitForCancelWorkflow) + env.RegisterNexusService(service) + env.ExecuteWorkflow(wf) + require.True(t, env.IsWorkflowCompleted()) + require.NoError(t, env.GetWorkflowError()) + require.Equal(t, "not implemented in the test environment", panicReason) +} diff --git a/test/test_utils_test.go b/test/test_utils_test.go index c12ed82e1..541775faa 100644 --- a/test/test_utils_test.go +++ b/test/test_utils_test.go @@ -52,6 +52,7 @@ type ( // Config contains the integration test configuration Config struct { ServiceAddr string + ServiceHTTPAddr string maxWorkflowCacheSize int Debug bool Namespace string @@ -73,6 +74,7 @@ var taskQueuePrefix = "tq-" + uuid.New() func NewConfig() Config { cfg := Config{ ServiceAddr: client.DefaultHostPort, + ServiceHTTPAddr: "localhost:7243", maxWorkflowCacheSize: 10000, Namespace: "integration-test-namespace", ShouldRegisterNamespace: true, @@ -80,6 +82,9 @@ func NewConfig() Config { if addr := getEnvServiceAddr(); addr != "" { cfg.ServiceAddr = addr } + if addr := strings.TrimSpace(os.Getenv("SERVICE_HTTP_ADDR")); addr != "" { + cfg.ServiceHTTPAddr = addr + } if siz := getEnvCacheSize(); siz != "" { asInt, err := strconv.Atoi(siz) if err != nil { diff --git a/test/worker_versioning_test.go b/test/worker_versioning_test.go index 3aa779b24..83070b518 100644 --- a/test/worker_versioning_test.go +++ b/test/worker_versioning_test.go @@ -24,6 +24,7 @@ package test_test import ( "context" + "os" "testing" "time" @@ -381,6 +382,10 @@ func (ts *WorkerVersioningTestSuite) TestConflictTokens() { } func (ts *WorkerVersioningTestSuite) TestTwoWorkersGetDifferentTasks() { + // TODO: Unskip this test, it is flaky with server 1.25.0-rc.0 + if os.Getenv("DISABLE_SERVER_1_25_TESTS") != "" { + ts.T().SkipNow() + } ctx, cancel := context.WithTimeout(context.Background(), ctxTimeout) defer cancel() @@ -796,6 +801,10 @@ func (ts *WorkerVersioningTestSuite) TestReachabilityVersionsWithRules() { } func (ts *WorkerVersioningTestSuite) TestBuildIDChangesOverWorkflowLifetime() { + // TODO: Unskip this test, it is flaky with server 1.25.0-rc.0 + if os.Getenv("DISABLE_SERVER_1_25_TESTS") != "" { + ts.T().SkipNow() + } ctx, cancel := context.WithTimeout(context.Background(), ctxTimeout) defer cancel() @@ -873,6 +882,10 @@ func (ts *WorkerVersioningTestSuite) TestBuildIDChangesOverWorkflowLifetime() { } func (ts *WorkerVersioningTestSuite) TestBuildIDChangesOverWorkflowLifetimeWithRules() { + // TODO: Unskip this test, it is flaky with server 1.25.0-rc.0 + if os.Getenv("DISABLE_SERVER_1_25_TESTS") != "" { + ts.T().SkipNow() + } ctx, cancel := context.WithTimeout(context.Background(), ctxTimeout) defer cancel() diff --git a/worker/worker.go b/worker/worker.go index a66ce953e..dd4f8687a 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -28,6 +28,7 @@ package worker import ( "context" + "github.com/nexus-rpc/sdk-go/nexus" historypb "go.temporal.io/api/history/v1" "go.temporal.io/api/workflowservice/v1" @@ -73,6 +74,7 @@ type ( Registry interface { WorkflowRegistry ActivityRegistry + NexusServiceRegistry } // WorkflowRegistry exposes workflow registration functions to consumers. @@ -150,6 +152,14 @@ type ( RegisterActivityWithOptions(a interface{}, options activity.RegisterOptions) } + // NexusServiceRegistry exposes Nexus Service registration functions. + NexusServiceRegistry interface { + // RegisterNexusService registers a service with a worker. Panics if a service with the same name has + // already been registered on this worker or if the worker has already been started. A worker will only + // poll for Nexus tasks if any services are registered on it. + RegisterNexusService(*nexus.Service) + } + // WorkflowReplayer supports replaying a workflow from its event history. // Use for troubleshooting and backwards compatibility unit tests. // For example if a workflow failed in production then its history can be downloaded through UI or CLI diff --git a/workflow/nexus_example_test.go b/workflow/nexus_example_test.go new file mode 100644 index 000000000..db348cb07 --- /dev/null +++ b/workflow/nexus_example_test.go @@ -0,0 +1,72 @@ +// The MIT License +// +// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved. +// +// 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. + +package workflow_test + +import ( + "context" + "time" + + "github.com/nexus-rpc/sdk-go/nexus" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/temporalnexus" + "go.temporal.io/sdk/workflow" +) + +type MyInput struct{} +type MyOutput struct{} + +var myOperationRef = nexus.NewOperationReference[MyInput, MyOutput]("my-operation") + +var myOperation = temporalnexus.NewSyncOperation("my-operation", func(ctx context.Context, c client.Client, mi MyInput, soo nexus.StartOperationOptions) (MyOutput, error) { + return MyOutput{}, nil +}) + +func ExampleNexusClient() { + myWorkflow := func(ctx workflow.Context) (MyOutput, error) { + client := workflow.NewNexusClient("my-endpoint", "my-service") + // Execute an operation using an operation name. + fut := client.ExecuteOperation(ctx, "my-operation", MyInput{}, workflow.NexusOperationOptions{ + ScheduleToCloseTimeout: time.Hour, + }) + // Or using an OperationReference. + fut = client.ExecuteOperation(ctx, myOperationRef, MyInput{}, workflow.NexusOperationOptions{ + ScheduleToCloseTimeout: time.Hour, + }) + // Or using a defined operation (which is also an OperationReference). + fut = client.ExecuteOperation(ctx, myOperation, MyInput{}, workflow.NexusOperationOptions{ + ScheduleToCloseTimeout: time.Hour, + }) + + var exec workflow.NexusOperationExecution + // Optionally wait for the operation to be started. + _ = fut.GetNexusOperationExecution().Get(ctx, &exec) + // OperationID will be empty if the operation completed synchronously. + workflow.GetLogger(ctx).Info("operation started", "operationID", exec.OperationID) + + // Get the result of the operation. + var output MyOutput + return output, fut.Get(ctx, &output) + } + + _ = myWorkflow +} diff --git a/workflow/workflow.go b/workflow/workflow.go index 17e509799..7800658ed 100644 --- a/workflow/workflow.go +++ b/workflow/workflow.go @@ -86,6 +86,41 @@ type ( ContinueAsNewErrorOptions = internal.ContinueAsNewErrorOptions UpdateHandlerOptions = internal.UpdateHandlerOptions + + // NOTE to maintainers, this interface definition is duplicated in the internal package to provide a better UX. + + // NexusClient is a client for executing Nexus Operations from a workflow. + NexusClient interface { + // The endpoint name this client uses. + // + // NOTE: Experimental + Endpoint() string + // The service name this client uses. + // + // NOTE: Experimental + Service() string + + // ExecuteOperation executes a Nexus Operation. + // The operation argument can be a string, a [nexus.Operation] or a [nexus.OperationReference]. + // + // NOTE: Experimental + ExecuteOperation(ctx Context, operation any, input any, options NexusOperationOptions) NexusOperationFuture + } + + // NexusOperationOptions are options for starting a Nexus Operation from a Workflow. + // + // NOTE: Experimental + NexusOperationOptions = internal.NexusOperationOptions + + // NexusOperationFuture represents the result of a Nexus Operation. + // + // NOTE: Experimental + NexusOperationFuture = internal.NexusOperationFuture + + // NexusOperationExecution is the result of [NexusOperationFuture.GetNexusOperationExecution]. + // + // NOTE: Experimental + NexusOperationExecution = internal.NexusOperationExecution ) // ExecuteActivity requests activity execution in the context of a workflow. @@ -716,3 +751,8 @@ func DeterministicKeysFunc[K comparable, V any](m map[K]V, cmp func(K, K) int) [ func AllHandlersFinished(ctx Context) bool { return internal.AllHandlersFinished(ctx) } + +// Create a [NexusClient] from an endpoint name and a service name. +func NewNexusClient(endpoint, service string) NexusClient { + return internal.NewNexusClient(endpoint, service) +}