diff --git a/README.md b/README.md index a7407b31..9c5d30f6 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ Workflow Definition and an Activity Definition using API Key to authenticate wit - [**Basic external environment configuration**](./external-env-conf): Simple example showing how to configure a client with an external configuration file, like TOML, decoupling connection settings from application code. + +- [**Standalone Activities**](standalone-activity/helloworld): Demonstrates how standalone activities work, where activities + can be called from the client directly and not wrapped in an activity. ### API demonstrations - **Async activity completion**: Example of diff --git a/go.mod b/go.mod index 2585e28f..467009e5 100644 --- a/go.mod +++ b/go.mod @@ -19,8 +19,8 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 go.opentelemetry.io/otel/sdk v1.27.0 go.opentelemetry.io/otel/trace v1.27.0 - go.temporal.io/api v1.59.0 - go.temporal.io/sdk v1.39.0 + go.temporal.io/api v1.62.1 + go.temporal.io/sdk v1.40.0 go.temporal.io/sdk/contrib/datadog v0.2.0 go.temporal.io/sdk/contrib/envconfig v0.1.0 go.temporal.io/sdk/contrib/opentelemetry v0.6.0 @@ -86,9 +86,9 @@ require ( golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.25.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect diff --git a/go.sum b/go.sum index 58c49656..e593975c 100644 --- a/go.sum +++ b/go.sum @@ -286,11 +286,11 @@ go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5 go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.temporal.io/api v1.5.0/go.mod h1:BqKxEJJYdxb5dqf0ODfzfMxh8UEQ5L3zKS51FiIYYkA= -go.temporal.io/api v1.59.0 h1:QUpAju1KKs9xBfGSI0Uwdyg06k6dRCJH+Zm3G1Jc9Vk= -go.temporal.io/api v1.59.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.62.1 h1:7UHMNOIqfYBVTaW0JIh/wDpw2jORkB6zUKsxGtvjSZU= +go.temporal.io/api v1.62.1/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/sdk v1.12.0/go.mod h1:lSp3lH1lI0TyOsus0arnO3FYvjVXBZGi/G7DjnAnm6o= -go.temporal.io/sdk v1.39.0 h1:+rtLK8BtT+0+b0DiSdgeQIFkONrLIUqjNfiIxMPF8VA= -go.temporal.io/sdk v1.39.0/go.mod h1:ESULA8dXvbPtw53DunYBgZFswk7RB4/8AcVXq5oSe+s= +go.temporal.io/sdk v1.40.0 h1:n9JN3ezVpWBxLzz5xViCo0sKxp7kVVhr1Su0bcMRNNs= +go.temporal.io/sdk v1.40.0/go.mod h1:tauxVfN174F0bdEs27+i0h8UPD7xBb6Py2SPHo7f1C0= go.temporal.io/sdk/contrib/datadog v0.2.0 h1:YUpkVncR3dEg4dnkWTWoDPVh6duU079RWIswsQ2u6LM= go.temporal.io/sdk/contrib/datadog v0.2.0/go.mod h1:yOOdVth8SQUFZSAtCFY8NvTZ2joxnAVSK9/1OfAMsw0= go.temporal.io/sdk/contrib/envconfig v0.1.0 h1:s+G/Ujph+Xl2jzLiiIm2T1vuijDkUL4Kse49dgDVGBE= @@ -432,8 +432,8 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -458,8 +458,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= diff --git a/helloworld/helloworld_test.go b/helloworld/helloworld_test.go index 092c2ad2..49543fa6 100644 --- a/helloworld/helloworld_test.go +++ b/helloworld/helloworld_test.go @@ -47,6 +47,7 @@ func Test_Using_DevServer(t *testing.T) { server, err := testsuite.StartDevServer(context.Background(), testsuite.DevServerOptions{ClientOptions: &client.Options{HostPort: hostPort}}) require.NoError(t, err) require.NotNil(t, server) + defer func() { _ = server.Stop() }() var ( c client.Client diff --git a/searchattributes/searchattributes_workflow.go b/searchattributes/searchattributes_workflow.go index 674cfea2..c04ba242 100644 --- a/searchattributes/searchattributes_workflow.go +++ b/searchattributes/searchattributes_workflow.go @@ -123,7 +123,7 @@ func ListExecutions(ctx context.Context, query string) ([]*workflowpb.WorkflowEx // current execution. for hasMore := true; hasMore; hasMore = !seenCurrentExecution || len(nextPageToken) > 0 { resp, err := c.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{ - Namespace: info.WorkflowNamespace, + Namespace: info.Namespace, PageSize: 10, NextPageToken: nextPageToken, Query: query, diff --git a/standalone-activity/helloworld/README.md b/standalone-activity/helloworld/README.md new file mode 100644 index 00000000..b60cf4cd --- /dev/null +++ b/standalone-activity/helloworld/README.md @@ -0,0 +1,51 @@ +This sample demonstrates how to use a Standalone Activity (executing an Activity without wrapping it in a Workflow) + +### Steps to run this sample (with expected output): +1) Run a [Temporal server](https://github.com/temporalio/samples-go/tree/main/#how-to-use). (If you are going to run locally, you will want to start it in another terminal; this command is blocking and runs until it receives a SIGINT (Ctrl + C) command.) + + +If you used the above instructions to start the server, you should see a line about the CLI, Server and UI versions, and one line each for the Server URL, UI URL and Metrics endpoint. It should look something like this: + +```bash +> temporal server start-dev +CLI 1.5.1 (Server 1.29.1, UI 2.42.1) + +Server: localhost:7233 +UI: http://localhost:8233 +Metrics: http://localhost:57058/metrics +``` + +2) Open a second terminal, and run the following command to start the worker. The worker is a blocking process that runs until it receives a SIGINT (Ctrl + C) command. +```bash +go run standalone-activity/helloworld/worker/main.go +``` + +You should see two console log lines: +1. Creating the logger +2. Starting the Worker with Namespace `default`, and TaskQueue `standalone-activity-helloworld` and it will list the WorkerID for the created worker. + +For example: +```bash +2025/12/22 15:00:15 INFO No logger configured for temporal client. Created default one. + +2025/12/22 15:00:16 INFO Started Worker Namespace default TaskQueue standalone-activity-helloworld WorkerID 82087 +``` + +> [!NOTE] +> Timestamps and IDs will differ on your machine. + +3) In a third terminal, run the following command to start the example +```bash +go run standalone-activity/helloworld/starter/main.go +``` + +You should see two console log lines: 1) Creating the logger, 2) The standalone activity result + +For example: +```bash +2026/02/05 11:30:47 INFO No logger configured for temporal client. Created default one. + +2026/02/05 11:30:47 Started standalone activity ActivityID standalone_activity_helloworld_ActivityID RunID 019c2f49-1ff1-7a44-beee-7ff4b36ecc27 + +2026/02/05 11:30:47 Activity result: Hello Temporal! +``` diff --git a/standalone-activity/helloworld/activity.go b/standalone-activity/helloworld/activity.go new file mode 100644 index 00000000..7ae9440c --- /dev/null +++ b/standalone-activity/helloworld/activity.go @@ -0,0 +1,12 @@ +package helloworld + +import ( + "context" + "go.temporal.io/sdk/activity" +) + +func Activity(ctx context.Context, name string) (string, error) { + logger := activity.GetLogger(ctx) + logger.Info("Activity", "name", name) + return "Hello " + name + "!", nil +} diff --git a/standalone-activity/helloworld/activity_test.go b/standalone-activity/helloworld/activity_test.go new file mode 100644 index 00000000..2039a085 --- /dev/null +++ b/standalone-activity/helloworld/activity_test.go @@ -0,0 +1,65 @@ +package helloworld + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/testsuite" + "go.temporal.io/sdk/worker" +) + +func Test_Activity(t *testing.T) { + testSuite := &testsuite.WorkflowTestSuite{} + env := testSuite.NewTestActivityEnvironment() + env.RegisterActivity(Activity) + + val, err := env.ExecuteActivity(Activity, "World") + require.NoError(t, err) + + var res string + require.NoError(t, val.Get(&res)) + require.Equal(t, "Hello World!", res) +} + +func Test_StandaloneActivity_Using_DevServer(t *testing.T) { + server, err := testsuite.StartDevServer(context.Background(), testsuite.DevServerOptions{ + CachedDownload: testsuite.CachedDownload{ + Version: "v1.5.2-standalone-activity-server", + }, + ExtraArgs: []string{ + "--dynamic-config-value", "activity.enableStandalone=true", + "--dynamic-config-value", "history.enableChasm=true", + "--dynamic-config-value", "history.enableTransitionHistory=true", + }, + }) + require.NoError(t, err) + defer func() { _ = server.Stop() }() + + c := server.Client() + taskQueue := "standalone-activity-test" + + w := worker.New(c, taskQueue, worker.Options{}) + w.RegisterActivity(Activity) + require.NoError(t, w.Start()) + defer w.Stop() + + activityOptions := client.StartActivityOptions{ + ID: "test-activity-id", + TaskQueue: taskQueue, + ScheduleToCloseTimeout: 10 * time.Second, + } + + handle, err := c.ExecuteActivity(context.Background(), activityOptions, Activity, "Temporal") + require.NoError(t, err) + require.NotEmpty(t, handle.GetID()) + require.NotEmpty(t, handle.GetRunID()) + + var result string + err = handle.Get(context.Background(), &result) + require.NoError(t, err) + require.Equal(t, "Hello Temporal!", result) +} diff --git a/standalone-activity/helloworld/starter/main.go b/standalone-activity/helloworld/starter/main.go new file mode 100644 index 00000000..df4b0bf3 --- /dev/null +++ b/standalone-activity/helloworld/starter/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "github.com/temporalio/samples-go/standalone-activity/helloworld" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/contrib/envconfig" + "log" + "time" +) + +// This sample is very similar to helloworld. The difference is that whereas in +// helloworld the activity is orchestrated by a workflow, in this sample the activity is +// executed directly by a client ("standalone activity"). + +func main() { + // The client is a heavyweight object that should be created once per process. + c, err := client.Dial(envconfig.MustLoadDefaultClientOptions()) + if err != nil { + log.Fatalln("Unable to create client", err) + } + defer c.Close() + + activityOptions := client.StartActivityOptions{ + ID: "standalone_activity_helloworld_ActivityID", + TaskQueue: "standalone-activity-helloworld", + // at least one of ScheduleToCloseTimeout or StartToCloseTimeout is required. + ScheduleToCloseTimeout: 10 * time.Second, + } + + // Normally we would execute a workflow, but in this case we are executing an activity directly. + handle, err := c.ExecuteActivity(context.Background(), activityOptions, helloworld.Activity, "Temporal") + if err != nil { + log.Fatalln("Unable to execute workflow", err) + } + + log.Println("Started standalone activity", "ActivityID", handle.GetID(), "RunID", handle.GetRunID()) + + // Synchronously wait for the activity completion. + var result string + err = handle.Get(context.Background(), &result) + if err != nil { + log.Fatalln("Unable get standalone activity result", err) + } + log.Println("Activity result:", result) +} diff --git a/standalone-activity/helloworld/worker/main.go b/standalone-activity/helloworld/worker/main.go new file mode 100644 index 00000000..91fb4326 --- /dev/null +++ b/standalone-activity/helloworld/worker/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/temporalio/samples-go/standalone-activity/helloworld" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/contrib/envconfig" + "go.temporal.io/sdk/worker" + "log" +) + +func main() { + // The client and worker are heavyweight objects that should be created once per process. + c, err := client.Dial(envconfig.MustLoadDefaultClientOptions()) + if err != nil { + log.Fatalln("Unable to create client", err) + } + defer c.Close() + + w := worker.New(c, "standalone-activity-helloworld", worker.Options{}) + + w.RegisterActivity(helloworld.Activity) + + err = w.Run(worker.InterruptCh()) + if err != nil { + log.Fatalln("Unable to start worker", err) + } +}