From 0a8d1ad4a950731856743d5fdd32856a32d25562 Mon Sep 17 00:00:00 2001 From: Thomas Moerman Date: Thu, 21 Sep 2023 21:33:03 +0200 Subject: [PATCH 001/102] Query documentation. Signed-off-by: Thomas Moerman --- doc/workflows.md | 35 +++++++++++++++++++++++++++++++++++ src/temporal/workflow.clj | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/doc/workflows.md b/doc/workflows.md index 424e00e..ca55678 100644 --- a/doc/workflows.md +++ b/doc/workflows.md @@ -165,3 +165,38 @@ Your Workflow may either block waiting with signals with [temporal.signals/ Date: Thu, 21 Sep 2023 21:33:26 +0200 Subject: [PATCH 002/102] Query documentation. Signed-off-by: Thomas Moerman --- doc/workflows.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/doc/workflows.md b/doc/workflows.md index ca55678..b5d478e 100644 --- a/doc/workflows.md +++ b/doc/workflows.md @@ -174,7 +174,7 @@ A temporal query is similar to a temporal signal, both are messages sent to a ru The difference is that a signal intends to change the behaviour of the Workflow, whereas a query intends to inspect the current state of the Workflow. Querying the state of a Workflow implies that the Workflow must maintain state while running, typically in a clojure [atom](https://clojuredocs.org/clojure.core/atom). -#### Registering a query handler +#### Registering a Query handler To enable querying a Workflow, you may use [temporal.workflow/register-query-handler!](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.workflow#register-query-handler!). The query handler is a function that has a reference to the Workflow state, usually by closing over it. It interprets the query and returns a response. @@ -186,16 +186,14 @@ The query handler is a function that has a reference to the Workflow state, usua (register-query-handler! (fn [query-type args] (when (= query-type :my-query) (get-in @state [:path :to :answer])))) - - ;; e.g. react to signals (perhaps in a loop), update the state atom - + ;; e.g. react to signals (perhaps in a loop), updating the state atom )) ``` -#### Querying a running workflow +#### Querying a Workflow You may query a Workflow with [temporal.client.core/query](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.core#query). -A query consists of a `query-type` (keyword) and possibly some `args`. +A query consists of a `query-type` (keyword) and possibly some `args` (any serializable data structure). ```clojure (query workflow :my-query {:foo "bar"}) From 04bb004f3dd7ba9d5340105f4d31ed025ed4b3cb Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 21 Sep 2023 15:46:53 -0400 Subject: [PATCH 003/102] Release v0.13.0 Changes since v0.12.3 --------------------- 9490251 Query documentation. 0a8d1ad Query documentation. 71e848e fixed doc string 63510b8 cleaned up test fafe1c2 fixed cljfmt formatting 4f052ae Adds workflow query support. a8ac766 ignore IntelliJ related files 31fe62c add terminate test 47db2ff cljfmt 34dad02 Add 2-arity create-workflow to create stub from workflow-id 2f5bf89 Stop using removed promesa API ba60438 Add slack link Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 1393ce4..3ef350c 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.12.4-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.13.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 5684e5fae1320c9036b2736617e2bbc604f07947 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 21 Sep 2023 15:47:58 -0400 Subject: [PATCH 004/102] Prepare for v0.13.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 3ef350c..25cdcce 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.13.0" +(defproject io.github.manetu/temporal-sdk "0.13.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From e4b5240cc11c95653360dab23a9f01fe2b177fce Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 7 Dec 2023 18:37:23 -0500 Subject: [PATCH 005/102] Add support for setting MetricsScope Signed-off-by: Greg Haskins --- README.md | 2 +- dev-resources/user.clj | 2 +- project.clj | 14 ++++++------ src/temporal/client/core.clj | 6 +++-- src/temporal/client/worker.clj | 2 +- src/temporal/codec.clj | 2 +- src/temporal/common.clj | 2 +- src/temporal/internal/activity.clj | 2 +- src/temporal/internal/promise.clj | 2 +- src/temporal/internal/signals.clj | 2 +- src/temporal/internal/utils.clj | 2 +- src/temporal/internal/workflow.clj | 2 +- src/temporal/promise.clj | 2 +- src/temporal/side_effect.clj | 2 +- src/temporal/signals.clj | 2 +- src/temporal/testing/env.clj | 28 ++++++++++++++++++++---- src/temporal/workflow.clj | 2 +- test/temporal/test/async.clj | 2 +- test/temporal/test/client_signal.clj | 2 +- test/temporal/test/codec.clj | 2 +- test/temporal/test/concurrency.clj | 2 +- test/temporal/test/conflict.clj | 2 +- test/temporal/test/local_activity.clj | 2 +- test/temporal/test/manual_dispatch.clj | 2 +- test/temporal/test/poll.clj | 2 +- test/temporal/test/query.clj | 2 +- test/temporal/test/race.clj | 2 +- test/temporal/test/scale.clj | 2 +- test/temporal/test/sequence.clj | 2 +- test/temporal/test/side_effect.clj | 2 +- test/temporal/test/signal_timeout.clj | 2 +- test/temporal/test/signal_with_start.clj | 2 +- test/temporal/test/simple.clj | 2 +- test/temporal/test/sleep.clj | 2 +- test/temporal/test/types.clj | 2 +- test/temporal/test/utils.clj | 2 +- test/temporal/test/uuid_test.clj | 2 +- test/temporal/test/workflow_signal.clj | 2 +- 38 files changed, 70 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index ced1bd9..0e3c40b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Pull requests are welcome. Please include a [DCO](https://en.wikipedia.org/wiki ## License -Copyright (C) 2022 Manetu, Inc. All Rights Reserved. +Copyright (C) Manetu, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this material except in compliance with the License. diff --git a/dev-resources/user.clj b/dev-resources/user.clj index bbb524b..07daa60 100644 --- a/dev-resources/user.clj +++ b/dev-resources/user.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns user (:require [clojure.tools.namespace.repl :refer [refresh]] diff --git a/project.clj b/project.clj index 25cdcce..8e4d1f5 100644 --- a/project.clj +++ b/project.clj @@ -3,7 +3,7 @@ :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" :url "https://www.apache.org/licenses/LICENSE-2.0" - :year 2022 + :year 2023 :key "apache-2.0"} :plugins [[lein-cljfmt "0.9.0"] [lein-kibit "0.1.8"] @@ -12,12 +12,12 @@ [jonase/eastwood "1.3.0"] [lein-codox "0.10.8"]] :dependencies [[org.clojure/clojure "1.11.1"] - [org.clojure/core.async "1.6.673"] - [io.temporal/temporal-sdk "1.19.1"] - [io.temporal/temporal-testing "1.19.1"] - [com.taoensso/encore "3.59.0"] - [com.taoensso/timbre "6.1.0"] - [com.taoensso/nippy "3.2.0"] + [org.clojure/core.async "1.6.681"] + [io.temporal/temporal-sdk "1.22.3"] + [io.temporal/temporal-testing "1.22.3"] + [com.taoensso/encore "3.74.0"] + [com.taoensso/timbre "6.3.1"] + [com.taoensso/nippy "3.3.0"] [funcool/promesa "9.2.542"] [medley "1.4.0"]] :repl-options {:init-ns user} diff --git a/src/temporal/client/core.clj b/src/temporal/client/core.clj index c231183..c4850c2 100644 --- a/src/temporal/client/core.clj +++ b/src/temporal/client/core.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.client.core "Methods for client interaction with Temporal" @@ -25,7 +25,8 @@ :enable-keepalive #(.setEnableKeepAlive ^WorkflowServiceStubsOptions$Builder %1 %2) :keepalive-time #(.setKeepAliveTime ^WorkflowServiceStubsOptions$Builder %1 %2) :keepalive-timeout #(.setKeepAliveTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) - :keepalive-without-stream #(.setKeepAlivePermitWithoutStream ^WorkflowServiceStubsOptions$Builder %1 %2)}) + :keepalive-without-stream #(.setKeepAlivePermitWithoutStream ^WorkflowServiceStubsOptions$Builder %1 %2) + :metrics-scope #(.setMetricsScope ^WorkflowServiceStubsOptions$Builder %1 %2)}) (defn ^:no-doc stub-options-> ^WorkflowServiceStubsOptions [params] @@ -72,6 +73,7 @@ Arguments: | :keepalive-time | Set the keep alive time | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | | :keepalive-timeout | Set the keep alive timeout | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | | :keepalive-without-stream | Set if client sends keepalive pings even with no active RPCs | boolean | false | +| :metrics-scope | The scope to be used for metrics reporting | [Scope](https://github.com/uber-java/tally/blob/master/core/src/main/java/com/uber/m3/tally/Scope.java) | | " ([] (create-client {})) diff --git a/src/temporal/client/worker.clj b/src/temporal/client/worker.clj index 8949a4f..2b5ff74 100644 --- a/src/temporal/client/worker.clj +++ b/src/temporal/client/worker.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.client.worker "Methods for managing a Temporal worker instance" diff --git a/src/temporal/codec.clj b/src/temporal/codec.clj index b8da810..fdc6188 100644 --- a/src/temporal/codec.clj +++ b/src/temporal/codec.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.codec "Methods for managing codecs between a client and the Temporal backend" diff --git a/src/temporal/common.clj b/src/temporal/common.clj index 1b990c7..5981c5d 100644 --- a/src/temporal/common.clj +++ b/src/temporal/common.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.common (:require [temporal.internal.utils :as u]) diff --git a/src/temporal/internal/activity.clj b/src/temporal/internal/activity.clj index ed2ef93..39bb7c0 100644 --- a/src/temporal/internal/activity.clj +++ b/src/temporal/internal/activity.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns ^:no-doc temporal.internal.activity (:require [clojure.core.protocols :as p] diff --git a/src/temporal/internal/promise.clj b/src/temporal/internal/promise.clj index a6ce9fd..4cc8083 100644 --- a/src/temporal/internal/promise.clj +++ b/src/temporal/internal/promise.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns ^:no-doc temporal.internal.promise (:require [taoensso.timbre :as log] diff --git a/src/temporal/internal/signals.clj b/src/temporal/internal/signals.clj index 198831e..0f125c8 100644 --- a/src/temporal/internal/signals.clj +++ b/src/temporal/internal/signals.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns ^:no-doc temporal.internal.signals (:require [taoensso.timbre :as log] diff --git a/src/temporal/internal/utils.clj b/src/temporal/internal/utils.clj index 6ad132b..b79c0d2 100644 --- a/src/temporal/internal/utils.clj +++ b/src/temporal/internal/utils.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns ^:no-doc temporal.internal.utils (:require [clojure.string :as string] diff --git a/src/temporal/internal/workflow.clj b/src/temporal/internal/workflow.clj index ef5222f..0684743 100644 --- a/src/temporal/internal/workflow.clj +++ b/src/temporal/internal/workflow.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns ^:no-doc temporal.internal.workflow (:require [clojure.core.protocols :as p] diff --git a/src/temporal/promise.clj b/src/temporal/promise.clj index 2249707..961fd1f 100644 --- a/src/temporal/promise.clj +++ b/src/temporal/promise.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.promise "Methods for managing promises from pending activities from within workflows" diff --git a/src/temporal/side_effect.clj b/src/temporal/side_effect.clj index 94f592d..a4c984f 100644 --- a/src/temporal/side_effect.clj +++ b/src/temporal/side_effect.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.side-effect "Methods for managing side-effects from within workflows" diff --git a/src/temporal/signals.clj b/src/temporal/signals.clj index a444d56..81ea010 100644 --- a/src/temporal/signals.clj +++ b/src/temporal/signals.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.signals "Methods for managing signals from within workflows" diff --git a/src/temporal/testing/env.clj b/src/temporal/testing/env.clj index 95311d2..df4685c 100644 --- a/src/temporal/testing/env.clj +++ b/src/temporal/testing/env.clj @@ -1,19 +1,39 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.testing.env "Methods and utilities to assist with unit-testing Temporal workflows" (:require [temporal.client.worker :as worker] [temporal.internal.utils :as u]) - (:import [io.temporal.testing TestWorkflowEnvironment])) + (:import [io.temporal.testing TestWorkflowEnvironment TestEnvironmentOptions TestEnvironmentOptions$Builder])) + +(def ^:no-doc test-env-options + {:metrics-scope #(.setMetricsScope ^TestEnvironmentOptions$Builder %1 %2)}) + +(defn ^:no-doc test-env-options-> + ^TestEnvironmentOptions [params] + (u/build (TestEnvironmentOptions/newBuilder) test-env-options params)) (defn create " Creates a mock Temporal backend, suitable for unit testing. A worker may be created with [[start]] and a client may be connected with [[get-client]] + +Arguments: + +- `options`: Client configuration option map (See below) + +#### options map + +| Value | Description | Type | Default | +| ------------------------- | --------------------------------------------------------------------------- | ------------ | ------- | +| :metrics-scope | The scope to be used for metrics reporting | [Scope](https://github.com/uber-java/tally/blob/master/core/src/main/java/com/uber/m3/tally/Scope.java) | | + " - [] - (TestWorkflowEnvironment/newInstance)) + ([] + (TestWorkflowEnvironment/newInstance)) + ([options] + (TestWorkflowEnvironment/newInstance (test-env-options-> options)))) (defn start " diff --git a/src/temporal/workflow.clj b/src/temporal/workflow.clj index 736a651..12139f4 100644 --- a/src/temporal/workflow.clj +++ b/src/temporal/workflow.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.workflow "Methods for defining and implementing Temporal workflows" diff --git a/test/temporal/test/async.clj b/test/temporal/test/async.clj index 9613655..0e103f8 100644 --- a/test/temporal/test/async.clj +++ b/test/temporal/test/async.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.async (:require [clojure.test :refer :all] diff --git a/test/temporal/test/client_signal.clj b/test/temporal/test/client_signal.clj index 1b0a0b2..edffabd 100644 --- a/test/temporal/test/client_signal.clj +++ b/test/temporal/test/client_signal.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.client-signal (:require [clojure.test :refer :all] diff --git a/test/temporal/test/codec.clj b/test/temporal/test/codec.clj index 9091f5a..2980e06 100644 --- a/test/temporal/test/codec.clj +++ b/test/temporal/test/codec.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.codec (:require [clojure.test :refer :all] diff --git a/test/temporal/test/concurrency.clj b/test/temporal/test/concurrency.clj index a2ea281..57c8808 100644 --- a/test/temporal/test/concurrency.clj +++ b/test/temporal/test/concurrency.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.concurrency (:require [clojure.test :refer :all] diff --git a/test/temporal/test/conflict.clj b/test/temporal/test/conflict.clj index 855a451..4086b6c 100644 --- a/test/temporal/test/conflict.clj +++ b/test/temporal/test/conflict.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.conflict (:require [clojure.test :refer :all] diff --git a/test/temporal/test/local_activity.clj b/test/temporal/test/local_activity.clj index 6df5c80..de8378b 100644 --- a/test/temporal/test/local_activity.clj +++ b/test/temporal/test/local_activity.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.local-activity (:require [clojure.test :refer :all] diff --git a/test/temporal/test/manual_dispatch.clj b/test/temporal/test/manual_dispatch.clj index 0d1847f..8cbae07 100644 --- a/test/temporal/test/manual_dispatch.clj +++ b/test/temporal/test/manual_dispatch.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.manual-dispatch (:require [clojure.test :refer :all] diff --git a/test/temporal/test/poll.clj b/test/temporal/test/poll.clj index 40db159..253f138 100644 --- a/test/temporal/test/poll.clj +++ b/test/temporal/test/poll.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.poll (:require [clojure.test :refer :all] diff --git a/test/temporal/test/query.clj b/test/temporal/test/query.clj index ebb0948..63c816f 100644 --- a/test/temporal/test/query.clj +++ b/test/temporal/test/query.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.query (:require [clojure.test :refer :all] diff --git a/test/temporal/test/race.clj b/test/temporal/test/race.clj index f3eb7b4..18d827b 100644 --- a/test/temporal/test/race.clj +++ b/test/temporal/test/race.clj @@ -1,4 +1,4 @@ -;; Copyright © 2022 Manetu, Inc. All rights reserved +;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.race (:require [clojure.test :refer :all] [clojure.core.async :refer [go Date: Thu, 7 Dec 2023 18:50:56 -0500 Subject: [PATCH 006/102] Release v0.14.0 Changes since v0.13.0 --------------------- e4b5240 Add support for setting MetricsScope Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 8e4d1f5..5307f81 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.13.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.14.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 683b713c77a310c65f0d835c76ce402bc78a3beb Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 7 Dec 2023 18:51:39 -0500 Subject: [PATCH 007/102] Prepare for v0.14.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 5307f81..93883ec 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.14.0" +(defproject io.github.manetu/temporal-sdk "0.14.1" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 12fbef7563ce9f150de02240e2dfc5705ef6e684 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Wed, 20 Dec 2023 17:08:12 -0500 Subject: [PATCH 008/102] Add missing -SNAPSHOT to development stream Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 93883ec..be1e391 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.14.1" +(defproject io.github.manetu/temporal-sdk "0.14.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 5f01f77c76c010b3084c8afff40cf3bfffeaf69b Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Wed, 20 Dec 2023 17:04:53 -0500 Subject: [PATCH 009/102] Expose temporal interceptors Signed-off-by: Greg Haskins --- project.clj | 3 +- src/temporal/client/core.clj | 7 +++- src/temporal/client/worker.clj | 44 +++++++++++++++----- src/temporal/testing/env.clj | 16 ++++++-- test/temporal/test/tracing.clj | 74 ++++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 test/temporal/test/tracing.clj diff --git a/project.clj b/project.clj index be1e391..8e61825 100644 --- a/project.clj +++ b/project.clj @@ -28,7 +28,8 @@ :codox {:metadata {:doc/format :markdown}} :profiles {:dev {:dependencies [[org.clojure/tools.namespace "1.4.4"] - [eftest "0.6.0"]]}} + [eftest "0.6.0"] + [io.temporal/temporal-opentracing "1.22.3"]]}} :cloverage {:runner :eftest :runner-opts {:multithread? false :fail-fast? true} diff --git a/src/temporal/client/core.clj b/src/temporal/client/core.clj index c4850c2..a26a6ad 100644 --- a/src/temporal/client/core.clj +++ b/src/temporal/client/core.clj @@ -9,7 +9,8 @@ [temporal.internal.utils :as u]) (:import [java.time Duration] [io.temporal.client WorkflowClient WorkflowClientOptions WorkflowClientOptions$Builder WorkflowStub] - [io.temporal.serviceclient WorkflowServiceStubs WorkflowServiceStubsOptions WorkflowServiceStubsOptions$Builder])) + [io.temporal.serviceclient WorkflowServiceStubs WorkflowServiceStubsOptions WorkflowServiceStubsOptions$Builder] + [io.temporal.common.interceptors WorkflowClientInterceptorBase])) (def ^:no-doc stub-options {:channel #(.setChannel ^WorkflowServiceStubsOptions$Builder %1 %2) @@ -35,7 +36,8 @@ (def ^:no-doc client-options {:identity #(.setIdentity ^WorkflowClientOptions$Builder %1 %2) :namespace #(.setNamespace ^WorkflowClientOptions$Builder %1 %2) - :data-converter #(.setDataConverter ^WorkflowClientOptions$Builder %1 %2)}) + :data-converter #(.setDataConverter ^WorkflowClientOptions$Builder %1 %2) + :interceptors #(.setInterceptors ^WorkflowClientOptions$Builder %1 (into-array WorkflowClientInterceptorBase %2))}) (defn ^:no-doc client-options-> ^WorkflowClientOptions [params] @@ -60,6 +62,7 @@ Arguments: | :identity | Overrides the worker node identity (workers only) | String | | | :namespace | Sets the Temporal namespace context for this client | String | | | :data-converter | Overrides the data converter used to serialize arguments and results. | [DataConverter](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/converter/DataConverter.html) | | +| :interceptors | Collection of interceptors used to intercept workflow client calls. | [WorkflowClientInterceptor](https://javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/interceptors/WorkflowClientInterceptor.html) | | | :channel | Sets gRPC channel to use. Exclusive with target and sslContext | [ManagedChannel](https://grpc.github.io/grpc-java/javadoc/io/grpc/ManagedChannel.html) | | | :ssl-context | Sets gRPC SSL Context to use | [SslContext](https://netty.io/4.0/api/io/netty/handler/ssl/SslContext.html) | | | :enable-https | Sets option to enable SSL/TLS/HTTPS for gRPC | boolean | false | diff --git a/src/temporal/client/worker.clj b/src/temporal/client/worker.clj index 2b5ff74..f153fde 100644 --- a/src/temporal/client/worker.clj +++ b/src/temporal/client/worker.clj @@ -6,9 +6,10 @@ [temporal.internal.activity :as a] [temporal.internal.workflow :as w] [temporal.internal.utils :as u]) - (:import [io.temporal.worker Worker WorkerFactory WorkerOptions WorkerOptions$Builder] + (:import [io.temporal.worker Worker WorkerFactory WorkerFactoryOptions WorkerFactoryOptions$Builder WorkerOptions WorkerOptions$Builder] [temporal.internal.dispatcher DynamicWorkflowProxy] - [io.temporal.workflow DynamicWorkflow])) + [io.temporal.workflow DynamicWorkflow] + [io.temporal.common.interceptors WorkerInterceptor])) (defn ^:no-doc init " @@ -27,6 +28,27 @@ Initializes a worker instance, suitable for real connections or unit-testing wit (reify DynamicWorkflow (execute [_ args] (w/execute ctx (:workflows dispatch) args))))))))) +(def worker-factory-options + " +Options for configuring the worker-factory (See [[start]]) + +| Value | Description | Type | Default | +| ------------ | ----------------------------------------------------------------- | ---------------- | ------- | +| :enable-logging-in-replay | | boolean | false | +| :max-workflow-thread-count | Maximum number of threads available for workflow execution across all workers created by the Factory. | int | 600 | +| :worker-interceptors | Collection of WorkerInterceptors | [WorkerInterceptor](https://javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/interceptors/WorkerInterceptor.html) | | +| :workflow-cache-size | To avoid constant replay of code the workflow objects are cached on a worker. This cache is shared by all workers created by the Factory. | int | 600 | +" + + {:enable-logging-in-replay #(.setEnableLoggingInReplay ^WorkerFactoryOptions$Builder %1 %2) + :max-workflow-thread-count #(.setMaxWorkflowThreadCount ^WorkerFactoryOptions$Builder %1 %2) + :worker-interceptors #(.setWorkerInterceptors ^WorkerFactoryOptions$Builder %1 (into-array WorkerInterceptor %2)) + :workflow-cache-size #(.setWorkflowCacheSize ^WorkerFactoryOptions$Builder %1 %2)}) + +(defn ^:no-doc worker-factory-options-> + ^WorkerFactoryOptions [params] + (u/build (WorkerFactoryOptions/newBuilder (WorkerFactoryOptions/getDefaultInstance)) worker-factory-options params)) + (def worker-options " Options for configuring workers (See [[start]]) @@ -86,19 +108,21 @@ Starts a worker processing loop. Arguments: -- `client`: WorkflowClient instance returned from [[temporal.client.core/create-client]] -- `options`: Worker start options (See [[worker-options]]) +- `client`: WorkflowClient instance returned from [[temporal.client.core/create-client]] +- `options`: Worker start options (See [[worker-options]]) +- `factory-options`: WorkerFactory options (See [[worker-factory-options]]) ```clojure (start {:task-queue ::my-queue :ctx {:some \"context\"}}) ``` " - [client {:keys [task-queue] :as options}] - (let [factory (WorkerFactory/newInstance client) - worker (.newWorker factory (u/namify task-queue) (worker-options-> options))] - (init worker options) - (.start factory) - {:factory factory :worker worker})) + ([client options] (start client options nil)) + ([client {:keys [task-queue] :as options} factory-options] + (let [factory (WorkerFactory/newInstance client (worker-factory-options-> factory-options)) + worker (.newWorker factory (u/namify task-queue) (worker-options-> options))] + (init worker options) + (.start factory) + {:factory factory :worker worker}))) (defn stop " diff --git a/src/temporal/testing/env.clj b/src/temporal/testing/env.clj index df4685c..fe8815b 100644 --- a/src/temporal/testing/env.clj +++ b/src/temporal/testing/env.clj @@ -3,11 +3,15 @@ (ns temporal.testing.env "Methods and utilities to assist with unit-testing Temporal workflows" (:require [temporal.client.worker :as worker] + [temporal.client.core :as client] [temporal.internal.utils :as u]) (:import [io.temporal.testing TestWorkflowEnvironment TestEnvironmentOptions TestEnvironmentOptions$Builder])) (def ^:no-doc test-env-options - {:metrics-scope #(.setMetricsScope ^TestEnvironmentOptions$Builder %1 %2)}) + {:worker-factory-options #(.setWorkerFactoryOptions ^TestEnvironmentOptions$Builder %1 (worker/worker-factory-options-> %2)) + :workflow-client-options #(.setWorkflowClientOptions ^TestEnvironmentOptions$Builder %1 (client/client-options-> %2)) + :workflow-service-stub-options #(.setWorkflowServiceStubsOptions ^TestEnvironmentOptions$Builder %1 (client/stub-options-> %2)) + :metrics-scope #(.setMetricsScope ^TestEnvironmentOptions$Builder %1 %2)}) (defn ^:no-doc test-env-options-> ^TestEnvironmentOptions [params] @@ -25,9 +29,13 @@ Arguments: #### options map -| Value | Description | Type | Default | -| ------------------------- | --------------------------------------------------------------------------- | ------------ | ------- | -| :metrics-scope | The scope to be used for metrics reporting | [Scope](https://github.com/uber-java/tally/blob/master/core/src/main/java/com/uber/m3/tally/Scope.java) | | +| Value | Description | Type | Default | +| ------------------------- | --------------------------------------------- | ------------ | ------- | +| :worker-factory-options | | [[worker/worker-factory-options]] | | +| :workflow-client-options | | [[client/client-options]] | | +| :workflow-service-stub-options | | [[client/stub-options]] | | +| :metrics-scope | The scope to be used for metrics reporting | [Scope](https://github.com/uber-java/tally/blob/master/core/src/main/java/com/uber/m3/tally/Scope.java) | | + " ([] diff --git a/test/temporal/test/tracing.clj b/test/temporal/test/tracing.clj new file mode 100644 index 0000000..78a3cde --- /dev/null +++ b/test/temporal/test/tracing.clj @@ -0,0 +1,74 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.tracing + (:require [clojure.test :refer :all] + [taoensso.timbre :as log] + [temporal.client.core :as c] + [temporal.testing.env :as e] + [temporal.workflow :refer [defworkflow]]) + (:import [io.temporal.opentracing OpenTracingClientInterceptor OpenTracingWorkerInterceptor])) + +;; do not use the shared fixture, since we want to control the env creation + +;;----------------------------------------------------------------------------- +;; Data +;;----------------------------------------------------------------------------- +(defonce state (atom {})) + +(def task-queue ::default) + +;;----------------------------------------------------------------------------- +;; Utilities +;;----------------------------------------------------------------------------- +(defn get-client [] + (get @state :client)) + +(defn create-workflow [workflow] + (c/create-workflow (get-client) workflow {:task-queue task-queue})) + +(defn invoke [workflow] + (let [i (create-workflow workflow)] + (c/start i {}) + @(c/get-result i))) + +;;----------------------------------------------------------------------------- +;; Workflows +;;----------------------------------------------------------------------------- + +(defworkflow traced-workflow + [ctx {:keys [args]}] + (log/info "traced-workflow:" args) + :ok) + +;;----------------------------------------------------------------------------- +;; Fixtures +;;----------------------------------------------------------------------------- +(defn create-service [] + (let [env (e/create {:workflow-client-options {:interceptors [(OpenTracingClientInterceptor.)]} + :worker-factory-options {:worker-interceptors [(OpenTracingWorkerInterceptor.)]}}) + client (e/get-client env)] + (e/start env {:task-queue task-queue}) + (swap! state assoc + :env env + :client client))) + +(defn destroy-service [] + (swap! state + (fn [{:keys [env] :as s}] + (e/stop env) + (dissoc s :env :client)))) + +(defn wrap-service [test-fn] + (create-service) + (test-fn) + (destroy-service)) + +(use-fixtures :once wrap-service) + +;;----------------------------------------------------------------------------- +;; Tests +;;----------------------------------------------------------------------------- + +(deftest the-test + (testing "Verifies that we can invoke our traced workflow" ;; todo: verify that tracing is working + (is (-> (invoke traced-workflow) (= :ok))))) From 53bd7e498f83ea16b2a7b3c5b3b162497e50a9f0 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Wed, 20 Dec 2023 21:09:33 -0500 Subject: [PATCH 010/102] Release v0.15.0 Changes since v0.14.0 --------------------- 5f01f77 Expose temporal interceptors 12fbef7 Add missing -SNAPSHOT to development stream Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 8e61825..8e5d715 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.14.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.15.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From d6724d1b99b21015679350dc190308069d75106c Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Wed, 20 Dec 2023 21:10:52 -0500 Subject: [PATCH 011/102] Prepare for v0.15.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 8e5d715..f9f8a5e 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.15.0" +(defproject io.github.manetu/temporal-sdk "0.15.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 95fa16c0a4fcad0432558df6b87e6a3656190cf5 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 29 Dec 2023 16:37:13 -0500 Subject: [PATCH 012/102] Add TLS utilities This patch adds utilities under the temporal.tls namespace to assist with connecting clients to TLS enabled Temporal backends. Signed-off-by: Greg Haskins --- project.clj | 3 +- src/temporal/client/core.clj | 2 +- src/temporal/tls.clj | 49 ++++++++++++++++++++++++++++ test/temporal/test/resources/ca.crt | 19 +++++++++++ test/temporal/test/resources/tls.crt | 19 +++++++++++ test/temporal/test/resources/tls.key | 28 ++++++++++++++++ test/temporal/test/tls.clj | 27 +++++++++++++++ 7 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 src/temporal/tls.clj create mode 100644 test/temporal/test/resources/ca.crt create mode 100644 test/temporal/test/resources/tls.crt create mode 100644 test/temporal/test/resources/tls.key create mode 100644 test/temporal/test/tls.clj diff --git a/project.clj b/project.clj index f9f8a5e..9531c59 100644 --- a/project.clj +++ b/project.clj @@ -29,7 +29,8 @@ :profiles {:dev {:dependencies [[org.clojure/tools.namespace "1.4.4"] [eftest "0.6.0"] - [io.temporal/temporal-opentracing "1.22.3"]]}} + [io.temporal/temporal-opentracing "1.22.3"]] + :resource-paths ["test/temporal/test/resources"]}} :cloverage {:runner :eftest :runner-opts {:multithread? false :fail-fast? true} diff --git a/src/temporal/client/core.clj b/src/temporal/client/core.clj index a26a6ad..804666d 100644 --- a/src/temporal/client/core.clj +++ b/src/temporal/client/core.clj @@ -64,7 +64,7 @@ Arguments: | :data-converter | Overrides the data converter used to serialize arguments and results. | [DataConverter](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/converter/DataConverter.html) | | | :interceptors | Collection of interceptors used to intercept workflow client calls. | [WorkflowClientInterceptor](https://javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/interceptors/WorkflowClientInterceptor.html) | | | :channel | Sets gRPC channel to use. Exclusive with target and sslContext | [ManagedChannel](https://grpc.github.io/grpc-java/javadoc/io/grpc/ManagedChannel.html) | | -| :ssl-context | Sets gRPC SSL Context to use | [SslContext](https://netty.io/4.0/api/io/netty/handler/ssl/SslContext.html) | | +| :ssl-context | Sets gRPC SSL Context to use (See [[temporal.tls/new-ssl-context]]) | [SslContext](https://netty.io/4.0/api/io/netty/handler/ssl/SslContext.html) | | | :enable-https | Sets option to enable SSL/TLS/HTTPS for gRPC | boolean | false | | :rpc-timeout | Sets the rpc timeout value for non query and non long poll calls | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | | :rpc-long-poll-timeout | Sets the rpc timeout value | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | diff --git a/src/temporal/tls.clj b/src/temporal/tls.clj new file mode 100644 index 0000000..4176a35 --- /dev/null +++ b/src/temporal/tls.clj @@ -0,0 +1,49 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.tls + "Utilities for connecting to TLS enabled Temporal clusters" + (:require [clojure.java.io :as io]) + (:import [java.security KeyStore] + [java.security.cert CertificateFactory X509Certificate] + [javax.net.ssl TrustManagerFactory] + [io.grpc.netty.shaded.io.grpc.netty GrpcSslContexts] + [io.grpc.netty.shaded.io.netty.handler.ssl SslContext])) + +(defn- new-ca + ^X509Certificate [certpath] + (let [cf (CertificateFactory/getInstance "X.509")] + (with-open [is (io/input-stream certpath)] + (.generateCertificate cf is)))) + +(defn- new-keystore + ^KeyStore [certpath] + (let [ca (new-ca certpath) + ks-type (KeyStore/getDefaultType)] + (doto (KeyStore/getInstance ks-type) + (.load nil nil) + (.setCertificateEntry "ca" ca)))) + +(defn- new-trustmanagerfactory + ^TrustManagerFactory [certpath] + (let [alg (TrustManagerFactory/getDefaultAlgorithm) + ks (new-keystore certpath)] + (doto (TrustManagerFactory/getInstance alg) + (.init ks)))) + +(defn new-ssl-context + " +Creates a new gRPC [SslContext](https://netty.io/4.0/api/io/netty/handler/ssl/SslContext.html) suitable for passing to the :ssl-context option of [[temporal.client.core/create-client]] + +Arguments: + +- `ca-path`: The path to a PEM encoded x509 Certificate Authority root certificate for validating the Temporal server. +- `cert-path`: The path to a PEM encoded x509 Certificate representing this client's identity, used for mutual TLS authentication. +- 'key-path': The path to a PEM encoded private key representing this client's identity, used for mutual TLS authentication. + +" + ^SslContext [{:keys [ca-path cert-path key-path] :as args}] + (-> (GrpcSslContexts/forClient) + (cond-> + (some? ca-path) (.trustManager (new-trustmanagerfactory ca-path)) + (and (some? cert-path) (some? key-path)) (.keyManager (io/file cert-path) (io/file key-path))) + (.build))) diff --git a/test/temporal/test/resources/ca.crt b/test/temporal/test/resources/ca.crt new file mode 100644 index 0000000..7de9830 --- /dev/null +++ b/test/temporal/test/resources/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDDjCCAfagAwIBAgIQLLzSA48U8GQtqMFMgazHjzANBgkqhkiG9w0BAQsFADAh +MR8wHQYDVQQDExZZdWdhYnl0ZSBTZWxmc2lnbmVkIENBMB4XDTIzMTIyOTIwMzM0 +N1oXDTI0MDMyODIwMzM0N1owITEfMB0GA1UEAxMWWXVnYWJ5dGUgU2VsZnNpZ25l +ZCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANJGsOx6lyoQfP2f +ByK6LAN1/CTdPoNNVMok7ibP3XKiqE0Dyrv9I/66pZqQrP1HYnv+uVXFDBPMqhXa +Bw5MyW0kjWQzruh2nTDWljZLl129XsvbPR4zkR+UH+6AoHxLIsfS1//jwE+y7OC8 +2vpE5PAsBsR2hPXGYPqWeZWscjDW0A0QpYI2b0q8UUnY36cr2fBv0tG/ZwDnhEBe +9E2kTnL4NIcRxoOZXN85NusRBN5oueGOhyVeSBNz2ym1o8D8mMT1L3YHZXsTMDBA +OTYpzCx+hgVtrp3cQUWWGp8R8G3FtrWDvJGxf34X5k/4txlRlaGrQ98w3AM8JdMr +0irWANMCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFBWjvPFAs3z0xzL+LSwcskkw1rcxMA0GCSqGSIb3DQEBCwUAA4IB +AQBXvO40RJ0Gy4Mcmn1R7CWDBKfRdqvSVoZdQwI0ZNQ0HXWzln4OksprohWRQJNJ +yqvw6DG7LroyfGEn8sfO1fzLcROymGK1akSu4PT0QSHMfmS377OPJvS6licFSvMB +2aFiD2pmQRlNa9mHAxAvGMPnAgrWhNALEPfePpVo9eR+06cJj17WXU/LGA3Fey1N +fAZ7W/LPpFBPPs98gbgJvhUIOJ8IvxPbNTG/21kQK8CfJ2dBnkNZOcm/FI5z+eu1 +O5swP6MRHuybvkHhyrhbU67f3iACpCgGzg1YUSjuXm9W+c4LB6qsGiZ/yGc2jJ8v +sbxxhViVNVJ58wbJ63C3OimS +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/temporal/test/resources/tls.crt b/test/temporal/test/resources/tls.crt new file mode 100644 index 0000000..8429c59 --- /dev/null +++ b/test/temporal/test/resources/tls.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBTCCAe2gAwIBAgIRAJHL+sXVDe5KtY39RLUHnZAwDQYJKoZIhvcNAQELBQAw +ITEfMB0GA1UEAxMWWXVnYWJ5dGUgU2VsZnNpZ25lZCBDQTAeFw0yMzEyMjkyMDMz +NTFaFw0yNDAzMjgyMDMzNTFaMBMxETAPBgNVBAMTCHl1Z2FieXRlMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Im+0RuHFVA1X7j1XNnr4B68ognJN1Dr ++/YtSiiN14QMRHENjlhUwi8pdvDwOvozyjQnufLchoOY7FpFlOgm99j5JjTD0F52 +vBhL1Xe0XB3NrI1e6FizqoOzv7WfBwTepptuF3p1U8RIiqHtxVHUUFsX3LrwKRqD +QgNGvQA4YJOdlOzAdpYmH/MDehFIrX0iZRfLc/Eb4uOc+CZxOHboMi8lAlNdnSFj +8coLYYI7PUSsc1l/XNwvm4p09cWmuDpwaEHMmVOfLh2WBChojDgzYbRcSBbX+VS4 +pW6EEnGthNx8/DyFXkiVKSBWMIhDHWhcsiG6PP3mlfuGqYOLSDZGbwIDAQABo0Yw +RDATBgNVHSUEDDAKBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaA +FBWjvPFAs3z0xzL+LSwcskkw1rcxMA0GCSqGSIb3DQEBCwUAA4IBAQBCqRpJz0c/ +2bYVaUiH8bk+ubW0iDQH3kCpHS5uTLWNO/gsWE7H+Gp93AZueb3dWoiuZWAlahNL +qLLgKhwM0P527nnoEhBcCYlA3fEfArTWIq4CAE6SX4jUPYsVp4ZUMfQTICsivtPw +uL0lcupDnrpmmiDCYCiCcmT/kXuzUrSyO4Qu8pbznfvhHBa6fxdJdQ6bBFZ1zBox +jJe0UnS7emCFAUzv4yNPw1yFyMqMIwfwN2arVxcz1WQhbM1uLenK6gVjBglnnFDT +9ZsM/ZZXGUvqiIIRvGrREES9Ljuic2j2q/keUXJFg9917dy3ep884+5vv7cxoha1 +RNY6CYw4pDlU +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/temporal/test/resources/tls.key b/test/temporal/test/resources/tls.key new file mode 100644 index 0000000..14458b4 --- /dev/null +++ b/test/temporal/test/resources/tls.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDYib7RG4cVUDVf +uPVc2evgHryiCck3UOv79i1KKI3XhAxEcQ2OWFTCLyl28PA6+jPKNCe58tyGg5js +WkWU6Cb32PkmNMPQXna8GEvVd7RcHc2sjV7oWLOqg7O/tZ8HBN6mm24XenVTxEiK +oe3FUdRQWxfcuvApGoNCA0a9ADhgk52U7MB2liYf8wN6EUitfSJlF8tz8Rvi45z4 +JnE4dugyLyUCU12dIWPxygthgjs9RKxzWX9c3C+binT1xaa4OnBoQcyZU58uHZYE +KGiMODNhtFxIFtf5VLilboQSca2E3Hz8PIVeSJUpIFYwiEMdaFyyIbo8/eaV+4ap +g4tINkZvAgMBAAECggEBAMilQppSzqXyL7LmGP2TtJx0/seLF9dY9YIAh9DaqSxV +YGSe+Te4I7nXp61d7sxHgWvRTipgnvVJxY7kyusC/vDULXG4nOVcUttSDBrek9Jz +j1xfltznLHxJE2sF6TjAy2tIRQgeYc9f5vQGveMEQx6+eer/kYAU4CFwFcEWDid1 +jGHFuk68CTn/lRACUTIGGzbmevbdzhmtYhU5q8KZO4s1xfGQE+e+Ikw9KNVcBI2l +ox4gzhgfcNsE+RUjhUfACB45aBDGfWLY1uC7opIM5lT0I03TBHwhe7y/24qRtnZ3 +3J4kajfssf4rDvHm4MKroTVf3OM4ZM6q2rKDWFMGJaECgYEA4UYE8P6YNO704AtW +/O3kLMjno2q44YXO/HG34cyoPHRk5E60CDbeZ4JX2DlhsVWioGkZg6VVfedtwrlS +j8sFO/2wpnhF591kWwaa5AK6lhhb8bPfjSZG6kffDzKRP1PEN41hq2EqmofpfuLF +7YsSFE+6TeGxVq/SCiXPOpU9UlECgYEA9hK02iDECo1dkQBojPH9RtlCt9uWPW0h +qaQQdKr5RM638IhLZA24tMkfU0XrVp179c7aRdkrIsn6pGzmp2bufjetrLLGkrj8 +mujV0nwP4qAHTZjSgCAtoVEUaSYv+WcBsB3shHycB5ffTLZWAOY0WzCrDpsXpd7W +N/NaoRjlHL8CgYEA2DEpZtr+2bYF/coENoJbe3tnilZOjeirp2u/TAzr2/DcLps1 +fbiionXdth4DmnuTshyLJuMR892ZYcoW6Pau1E74LBq7A/VdbVoeZfoUdR11h7XX +Mg/s+MP21w/xgvPyGFovxJhgmaMbu/EIgJr5w9Jr+nhBh+7+RUzZ3uAA1LECgYEA +gCb/3vXfgytaRlDzIixI3qP5di07EmSKeoHCPDBqvyX1b6RbtxDaV/TChqjMRoCf +9UU0MdpG98g+63D3sskNfdhbb6xvdCw5Cigma4dG8pyrEQN85VNc0D2cpqJHq9i0 +bVc4PUt0KxQyLA5tvewl6jPvchzddPoXkG4BjhKcB5sCgYAVA1rHQy6kMaJuInRs +YPQ+jpKKoxSbDfRvXuhBGaAyisJy5lfI6qHDpl9t2QNHV2tnHq1R5AW8rkAGKVdF +dh/t5DvgfC20ZMVZlJcoPnfwdVaOvu0tnLPk1pes1pInH/TbBGNm6ngZTBdXVu3J +4qnsF0nRAwBqxlv1ldLOF/9i4w== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/test/temporal/test/tls.clj b/test/temporal/test/tls.clj new file mode 100644 index 0000000..c374e01 --- /dev/null +++ b/test/temporal/test/tls.clj @@ -0,0 +1,27 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.tls + (:require [clojure.test :refer :all] + [clojure.java.io :as io] + [temporal.tls :as tls])) + +;;----------------------------------------------------------------------------- +;; Tests +;;----------------------------------------------------------------------------- + +(deftest positive-tests + (testing "Verify that we can create a default SSL context" + (let [ctx (tls/new-ssl-context {})] + (is (some? ctx)))) + (testing "Verify that we can create an SSL context with only a CA" + (let [ctx (tls/new-ssl-context {:ca-path (io/resource "ca.crt")})] + (is (some? ctx)))) + (testing "Verify that we can create an SSL context with only mTLS cert" + (let [ctx (tls/new-ssl-context {:cert-path (io/resource "tls.crt") :key-path (io/resource "tls.key")})] + (is (some? ctx))))) + +(deftest negative-tests + (testing "Verify that we cannot create an SSL context with a bogus CA" + (is (thrown? java.io.FileNotFoundException (tls/new-ssl-context {:ca-path "invalid"})))) + (testing "Verify that we cannot create an SSL context with a bogus Cert" + (is (thrown? java.lang.IllegalArgumentException (tls/new-ssl-context {:cert-path "invalid" :key-path "invalid"}))))) From 62d82efb7fc2d2f2ba2927fd5dd2f8bc5743bf1a Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 29 Dec 2023 17:17:19 -0500 Subject: [PATCH 013/102] Release v0.16.0 Changes since v0.15.0 --------------------- 95fa16c Add TLS utilities Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 9531c59..d1610e9 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.15.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.16.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 7de591d594d6bec31de1569832e324737de865e2 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 29 Dec 2023 17:24:35 -0500 Subject: [PATCH 014/102] Prepare for v0.16.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index d1610e9..538c510 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.16.0" +(defproject io.github.manetu/temporal-sdk "0.16.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From d73035e11dd7ec98591d6ce1660ba41e770d8dd1 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 5 Jan 2024 22:38:14 -0500 Subject: [PATCH 015/102] Add register-signal-handler! and signal-channel abstraction This patch breaks out the signal handling such that the core.async abstraction surrounding signals is now optional. There is a new function: temporal.signals/register-signal-handler! that offers low-level access to the signal callback. The design was inspired by the work of Thomas Moerman (https://github.com/tmoerman) in the Query support (See https://github.com/manetu/temporal-clojure-sdk/commit/4f052aeecd5d36cac0d1c2e1e95b72b23e5b2be7) This work prompted another cleanup that I had been meaning to do: the removal of the 'ctx' from the defworkflow. The original design had both defworkflow and defactivity receiving a user supplied 'ctx', but this never made sense for workflows since the context invites non-determinism. So, the new signature of defworkflow is simply a single-arity function that receives the arguments directly, rather than within a {:keys [signals args]} map. We still support 2-arity clients (for now) with a backwards compatibility check within the macro. This may be removed in a future release, so we print a WARN to signal the developer of the deprecation. Signed-off-by: Greg Haskins --- doc/clients.md | 2 +- doc/testing.md | 2 +- doc/workers.md | 2 +- doc/workflows.md | 39 ++++++++++---- src/temporal/internal/signals.clj | 17 +++--- src/temporal/internal/utils.clj | 9 +++- src/temporal/internal/workflow.clj | 7 ++- src/temporal/signals.clj | 67 +++++++++++++++++------- src/temporal/workflow.clj | 30 ++++++----- test/temporal/test/async.clj | 2 +- test/temporal/test/client_signal.clj | 7 +-- test/temporal/test/concurrency.clj | 2 +- test/temporal/test/heartbeat.clj | 2 +- test/temporal/test/legacy_workflow.clj | 25 +++++++++ test/temporal/test/local_activity.clj | 2 +- test/temporal/test/manual_dispatch.clj | 4 +- test/temporal/test/poll.clj | 4 +- test/temporal/test/query.clj | 8 +-- test/temporal/test/race.clj | 2 +- test/temporal/test/raw_signal.clj | 29 ++++++++++ test/temporal/test/scale.clj | 2 +- test/temporal/test/sequence.clj | 2 +- test/temporal/test/side_effect.clj | 2 +- test/temporal/test/signal_timeout.clj | 9 ++-- test/temporal/test/signal_with_start.clj | 7 +-- test/temporal/test/simple.clj | 2 +- test/temporal/test/sleep.clj | 2 +- test/temporal/test/tracing.clj | 2 +- test/temporal/test/uuid_test.clj | 2 +- test/temporal/test/workflow_signal.clj | 9 ++-- 30 files changed, 214 insertions(+), 87 deletions(-) create mode 100644 test/temporal/test/legacy_workflow.clj create mode 100644 test/temporal/test/raw_signal.clj diff --git a/doc/clients.md b/doc/clients.md index 3cf0411..f395b11 100644 --- a/doc/clients.md +++ b/doc/clients.md @@ -33,7 +33,7 @@ As a simple example, for the following Workflow implementation: (str "Hi, " name)) (defworkflow greeter-workflow - [ctx {:keys [args]}] + [args] (log/info "greeter-workflow:" args) @(a/invoke greet-activity args)) ``` diff --git a/doc/testing.md b/doc/testing.md index bedc94d..78b27d8 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -25,7 +25,7 @@ You can use the provided environment with a Clojure unit testing framework of yo (str "Hi, " name)) (defworkflow greeter-workflow - [ctx {:keys [args]}] + [args] (log/info "greeter-workflow:" args) @(a/invoke greet-activity args)) diff --git a/doc/workers.md b/doc/workers.md index 1de10b5..27ae142 100644 --- a/doc/workers.md +++ b/doc/workers.md @@ -25,7 +25,7 @@ As a simple example, let's say we want our Worker to be able to execute the foll (str "Hi, " name)) (defworkflow greeter-workflow - [ctx {:keys [args]}] + [args] (log/info "greeter-workflow:" args) @(a/invoke greet-activity args)) ``` diff --git a/doc/workflows.md b/doc/workflows.md index b5d478e..6033bb6 100644 --- a/doc/workflows.md +++ b/doc/workflows.md @@ -10,7 +10,7 @@ In this Clojure SDK programming model, a Temporal Workflow is a function declare ```clojure (defworkflow my-workflow - [ctx params] + [params] ...) ``` @@ -24,7 +24,7 @@ A Workflow implementation consists of defining a (defworkflow) function. The pl (require '[temporal.workflow :refer [defworkflow]]) (defworkflow my-workflow - [ctx {{:keys [foo]} :args}] + [{:keys [foo]}] ...) ``` @@ -65,7 +65,7 @@ In this Clojure SDK, developers manage Workflows with the following flow: ```clojure (defworkflow my-workflow - [ctx {{:keys [foo]} :args}] + [{:keys [foo]}] ...) (let [w (create-workflow client my-workflow {:task-queue "MyTaskQueue"})] @@ -155,24 +155,45 @@ Your Workflow may send or receive [signals](https://cljdoc.org/d/io.github.manet #### Receiving Signals -Your Workflow may either block waiting with signals with [temporal.signals/ @state 1))) + @state)) +``` + ### Temporal Queries Your Workflow may respond to [queries](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.core#query). A temporal query is similar to a temporal signal, both are messages sent to a running Workflow. The difference is that a signal intends to change the behaviour of the Workflow, whereas a query intends to inspect the current state of the Workflow. -Querying the state of a Workflow implies that the Workflow must maintain state while running, typically in a clojure [atom](https://clojuredocs.org/clojure.core/atom). +Querying the state of a Workflow implies that the Workflow must maintain state while running, typically in a clojure [atom](https://clojuredocs.org/clojure.core/atom) or [ref](https://clojure.org/reference/refs). #### Registering a Query handler @@ -181,7 +202,7 @@ The query handler is a function that has a reference to the Workflow state, usua ```clojure (defworkflow stateful-workflow - [ctx {:keys [signals] {:keys [init] :as args} :args :as params}] + [{:keys [init] :as args}] (let [state (atom init)] (register-query-handler! (fn [query-type args] (when (= query-type :my-query) diff --git a/src/temporal/internal/signals.clj b/src/temporal/internal/signals.clj index 0f125c8..5f6744e 100644 --- a/src/temporal/internal/signals.clj +++ b/src/temporal/internal/signals.clj @@ -21,13 +21,16 @@ (.add ch payload) (assoc s signal-name ch))))) -(defn create - "Registers the calling workflow to receive signals and returns a context variable to be passed back later" +(defn register-signal-handler! + [f] + (Workflow/registerListener + (reify + DynamicSignalHandler + (handle [_ signal-name args] + (f signal-name (u/->args args)))))) + +(defn create-signal-chan [] (let [state (atom {})] - (Workflow/registerListener - (reify - DynamicSignalHandler - (handle [_ signal-name args] - (-handle state signal-name (u/->args args))))) + (register-signal-handler! (partial -handle state)) state)) diff --git a/src/temporal/internal/utils.clj b/src/temporal/internal/utils.clj index b79c0d2..4fbd01a 100644 --- a/src/temporal/internal/utils.clj +++ b/src/temporal/internal/utils.clj @@ -75,11 +75,16 @@ (verify-registered-fns data) (m/index-by :name data))) +(defn find-dispatch + "Finds any dispatch descriptor named 't' that carry metadata 'marker'" + [dispatch-table t] + (or (get dispatch-table t) + (throw (ex-info "workflow/activity not found" {:function t})))) + (defn find-dispatch-fn "Finds any functions named 't' that carry metadata 'marker'" [dispatch-table t] - (:fn (or (get dispatch-table t) - (throw (ex-info "workflow/activity not found" {:function t}))))) + (:fn (find-dispatch dispatch-table t))) (defn import-dispatch [marker coll] diff --git a/src/temporal/internal/workflow.clj b/src/temporal/internal/workflow.clj index 0684743..2bd4429 100644 --- a/src/temporal/internal/workflow.clj +++ b/src/temporal/internal/workflow.clj @@ -53,10 +53,13 @@ [ctx dispatch args] (try (let [{:keys [workflow-type workflow-id]} (get-info) - f (u/find-dispatch-fn dispatch workflow-type) + d (u/find-dispatch dispatch workflow-type) + f (:fn d) a (u/->args args) _ (log/trace workflow-id "calling" f "with args:" a) - r (f ctx {:args a :signals (s/create)})] + r (if (-> d :type (= :legacy)) + (f ctx {:args a :signals (s/create-signal-chan)}) + (f a))] (log/trace workflow-id "result:" r) (nippy/freeze r)) (catch Exception e diff --git a/src/temporal/signals.clj b/src/temporal/signals.clj index 81ea010..ee7cab1 100644 --- a/src/temporal/signals.clj +++ b/src/temporal/signals.clj @@ -9,46 +9,77 @@ (:import [io.temporal.workflow Workflow])) (defn is-empty? - "Returns 'true' if 'signal-name' either doesn't exist or exists but has no pending messages" - [state signal-name] + "Returns 'true' if 'signal-name' either doesn't exist within signal-chan or exists but has no pending messages" + [signal-chan signal-name] (let [signal-name (u/namify signal-name) - ch (s/get-ch @state signal-name) + ch (s/get-ch @signal-chan signal-name) r (or (nil? ch) (.isEmpty ch))] - (log/trace "is-empty?:" @state signal-name r) + (log/trace "is-empty?:" @signal-chan signal-name r) r)) (defn- rx - [state signal-name] + [signal-chan signal-name] (let [signal-name (u/namify signal-name) - ch (s/get-ch @state signal-name) + ch (s/get-ch @signal-chan signal-name) m (.poll ch)] (log/trace "rx:" signal-name m) m)) (defn poll - "Non-blocking check of the signal. Consumes and returns a message if found, otherwise returns 'nil'" - [state signal-name] - (when-not (is-empty? state signal-name) - (rx state signal-name))) + "Non-blocking check of the signal via signal-chan. Consumes and returns a message if found, otherwise returns 'nil'" + [signal-chan signal-name] + (when-not (is-empty? signal-chan signal-name) + (rx signal-chan signal-name))) (defn ! "Sends `payload` to `workflow-id` via signal `signal-name`." [^String workflow-id signal-name payload] (let [signal-name (u/namify signal-name) stub (Workflow/newUntypedExternalWorkflowStub workflow-id)] - (.signal stub signal-name (u/->objarray payload)))) \ No newline at end of file + (.signal stub signal-name (u/->objarray payload)))) + +(def register-signal-handler! + " +Registers a DynamicSignalHandler listener that handles signals sent to the workflow such as with [[>!]]. + +Use inside a workflow definition with 'f' closing over any desired workflow state (e.g. atom) to mutate +the workflow state. + +Arguments: +- `f`: a 2-arity function, expecting 2 arguments. + +`f` arguments: +- `signal-name`: string +- `args`: params value or data structure + +```clojure +(defworkflow signalled-workflow + [{:keys [init] :as args}] + (let [state (atom init)] + (register-signal-handler! (fn [signal-name args] + (when (= signal-name \"mysignal\") + (update state #(conj % args))))) + ;; workflow implementation + )) +```" + s/register-signal-handler!) + +(def create-signal-chan + "Registers the calling workflow to receive signals and returns a 'signal-channel' context for use with functions such as [[!] :as c] - [temporal.signals :refer [ (pt/all (map invoke (range 10))) (p/then (fn [r] diff --git a/test/temporal/test/heartbeat.clj b/test/temporal/test/heartbeat.clj index 0feff88..e3a0304 100644 --- a/test/temporal/test/heartbeat.clj +++ b/test/temporal/test/heartbeat.clj @@ -18,7 +18,7 @@ (throw (ex-info "heartbeat details not found" {}))))) (defworkflow heartbeat-workflow - [_ _] + [_] @(a/invoke heartbeat-activity {})) (deftest the-test diff --git a/test/temporal/test/legacy_workflow.clj b/test/temporal/test/legacy_workflow.clj new file mode 100644 index 0000000..822039f --- /dev/null +++ b/test/temporal/test/legacy_workflow.clj @@ -0,0 +1,25 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.legacy-workflow + (:require [clojure.test :refer :all] + [taoensso.timbre :as log] + [temporal.client.core :refer [>!] :as c] + [temporal.signals :refer [! workflow signal-name :foo) + (is (-> workflow c/get-result deref (= :foo)))))) diff --git a/test/temporal/test/local_activity.clj b/test/temporal/test/local_activity.clj index de8378b..a78807d 100644 --- a/test/temporal/test/local_activity.clj +++ b/test/temporal/test/local_activity.clj @@ -16,7 +16,7 @@ (str "Hi, " name)) (defworkflow local-greeter-workflow - [ctx {:keys [args]}] + [args] (log/info "greeter-workflow:" args) @(a/local-invoke local-greet-activity args {:do-not-include-args true})) diff --git a/test/temporal/test/manual_dispatch.clj b/test/temporal/test/manual_dispatch.clj index 8cbae07..bdf77b2 100644 --- a/test/temporal/test/manual_dispatch.clj +++ b/test/temporal/test/manual_dispatch.clj @@ -36,12 +36,12 @@ ;;----------------------------------------------------------------------------- (defworkflow explicitly-registered-workflow - [ctx {:keys [args]}] + [args] (log/info "registered-workflow:" args) :ok) (defworkflow explicitly-skipped-workflow - [ctx {:keys [args]}] + [args] (log/info "skipped-workflow:" args) :ok) diff --git a/test/temporal/test/poll.clj b/test/temporal/test/poll.clj index 253f138..2607e98 100644 --- a/test/temporal/test/poll.clj +++ b/test/temporal/test/poll.clj @@ -17,9 +17,9 @@ (cons m (lazy-signals signals))))) (defworkflow poll-workflow - [ctx {:keys [signals]}] + [_] (log/info "test-workflow:") - (doall (lazy-signals signals))) + (doall (lazy-signals (s/create-signal-chan)))) (deftest the-test (testing "Verifies that poll exits with nil when there are no signals" diff --git a/test/temporal/test/query.clj b/test/temporal/test/query.clj index 63c816f..50c784a 100644 --- a/test/temporal/test/query.clj +++ b/test/temporal/test/query.clj @@ -2,9 +2,8 @@ (ns temporal.test.query (:require [clojure.test :refer :all] - [taoensso.timbre :as log] [temporal.client.core :refer [>!] :as c] - [temporal.signals :refer [!] :as c] + [temporal.signals :refer [ @state 1))) + @state)) + +(deftest the-test + (testing "Verifies that we can handle raw signals" + (let [workflow (t/create-workflow raw-signal-workflow)] + (c/start workflow {}) + + (>! workflow signal-name {}) + (>! workflow signal-name {}) + (is (= 2 @(c/get-result workflow)))))) diff --git a/test/temporal/test/scale.clj b/test/temporal/test/scale.clj index 010e5d2..481bcfe 100644 --- a/test/temporal/test/scale.clj +++ b/test/temporal/test/scale.clj @@ -20,7 +20,7 @@ id)) (defworkflow scale-workflow - [ctx {:keys [args]}] + [args] (log/info "workflow:" args) @(a/invoke scale-activity args)) diff --git a/test/temporal/test/sequence.clj b/test/temporal/test/sequence.clj index 9a72313..dec2f30 100644 --- a/test/temporal/test/sequence.clj +++ b/test/temporal/test/sequence.clj @@ -18,7 +18,7 @@ (str "Hi, " args)) (defworkflow sequence-workflow - [ctx _] + [_] @(-> (pt/resolved true) (p/then (fn [_] (a/invoke sequence-activity "Bob"))) diff --git a/test/temporal/test/side_effect.clj b/test/temporal/test/side_effect.clj index a19ac28..2c8bc6c 100644 --- a/test/temporal/test/side_effect.clj +++ b/test/temporal/test/side_effect.clj @@ -12,7 +12,7 @@ (use-fixtures :once t/wrap-service) (defworkflow side-effect-workflow - [ctx {:keys [args]}] + [args] (log/info "workflow:" args) (side-effect/now)) diff --git a/test/temporal/test/signal_timeout.clj b/test/temporal/test/signal_timeout.clj index cb364c7..aab0ed9 100644 --- a/test/temporal/test/signal_timeout.clj +++ b/test/temporal/test/signal_timeout.clj @@ -4,7 +4,7 @@ (:require [clojure.test :refer :all] [taoensso.timbre :as log] [temporal.client.core :refer [>!] :as c] - [temporal.signals :refer [!]] + [temporal.signals :refer [!] :as s] [temporal.workflow :refer [defworkflow]] [temporal.test.utils :as t])) @@ -13,12 +13,13 @@ (def signal-name ::signal) (defworkflow wfsignal-primary-workflow - [ctx {:keys [signals] :as args}] + [args] (log/info "primary-workflow:" args) - (! workflow-id signal-name msg)) From d9ef9ac40432ed4cc18f0ea7174b4b19bffe7e16 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 8 Jan 2024 16:39:13 -0500 Subject: [PATCH 016/102] Fix copy+paste error in local-invoke Special thanks to Dan O'Reilly (@dano) for spying the problem and to Quinn Klassen (https://github.com/Quinn-With-Two-Ns) for helping to narrow it down and away from the Temporal Java SDK. Signed-off-by: Greg Haskins --- src/temporal/activity.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/temporal/activity.clj b/src/temporal/activity.clj index 1785cd1..46018e5 100644 --- a/src/temporal/activity.clj +++ b/src/temporal/activity.clj @@ -126,7 +126,7 @@ Arguments: (local-invoke my-activity {:foo \"bar\"} {:start-to-close-timeout (Duration/ofSeconds 3)) ``` " - ([activity params] (invoke activity params {})) + ([activity params] (local-invoke activity params {})) ([activity params options] (let [act-name (a/get-annotation activity) stub (Workflow/newUntypedLocalActivityStub (a/local-invoke-options-> options))] From 19f993ce8312efd9c01ed4c3341bb6251c4414fd Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 8 Jan 2024 19:35:54 -0500 Subject: [PATCH 017/102] Release v0.17.0 Changes since v0.16.0 --------------------- d9ef9ac Fix copy+paste error in local-invoke d73035e Add register-signal-handler! and signal-channel abstraction Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 538c510..47b686c 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.16.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.17.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From d533226b35c5b9873d03b9648eb4c53a81ce8d33 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 8 Jan 2024 19:37:25 -0500 Subject: [PATCH 018/102] Prepare for v0.17.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 47b686c..cca5614 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.17.0" +(defproject io.github.manetu/temporal-sdk "0.17.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From e9fb87ae9027d4096fda8b0419ff3b7431c07e39 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 30 Jan 2024 10:56:35 -0500 Subject: [PATCH 019/102] Expose ActivityInfo in public API Signed-off-by: Greg Haskins --- src/temporal/activity.clj | 5 +++++ src/temporal/internal/activity.clj | 7 ++++--- test/temporal/test/activity_info.clj | 26 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 test/temporal/test/activity_info.clj diff --git a/src/temporal/activity.clj b/src/temporal/activity.clj index 46018e5..0dcfe8a 100644 --- a/src/temporal/activity.clj +++ b/src/temporal/activity.clj @@ -41,6 +41,11 @@ along with the Activity Task for the next retry attempt and can be extracted by (log/trace "get-heartbeat-details:" v) v))) +(defn get-info + "Returns information about the Activity execution" + [] + (a/get-info)) + (defn- complete-invoke [activity result] (log/trace activity "completed with" (count result) "bytes") diff --git a/src/temporal/internal/activity.clj b/src/temporal/internal/activity.clj index 39bb7c0..1da7f40 100644 --- a/src/temporal/internal/activity.clj +++ b/src/temporal/internal/activity.clj @@ -58,9 +58,10 @@ :activity-type (.getActivityType d)})) (defn get-info [] - (-> (Activity/getExecutionContext) - (.getInfo) - (d/datafy))) + (->> (Activity/getExecutionContext) + (.getInfo) + (d/datafy) + (into {}))) (defn get-annotation ^String [x] diff --git a/test/temporal/test/activity_info.clj b/test/temporal/test/activity_info.clj new file mode 100644 index 0000000..aa34c3e --- /dev/null +++ b/test/temporal/test/activity_info.clj @@ -0,0 +1,26 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.activity-info + (:require [clojure.test :refer :all] + [temporal.client.core :as c] + [temporal.workflow :refer [defworkflow]] + [temporal.activity :refer [defactivity] :as a] + [temporal.test.utils :as t])) + +(use-fixtures :once t/wrap-service) + +(defactivity getinfo-activity + [ctx args] + (a/get-info)) + +(defworkflow getinfo-workflow + [args] + @(a/invoke getinfo-activity args)) + +(deftest the-test + (testing "Verifies that we can retrieve our activity-id" + (let [workflow (t/create-workflow getinfo-workflow)] + (c/start workflow {}) + (let [{:keys [activity-id]} @(c/get-result workflow)] + (is (some? activity-id)))))) + From 32cb01da0ef279044721373ad0c8a4f435f272c3 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 30 Jan 2024 11:13:10 -0500 Subject: [PATCH 020/102] Release v0.17.1 Changes since v0.17.0 --------------------- e9fb87a Expose ActivityInfo in public API Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index cca5614..86e3c1e 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.17.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.17.1" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 071666194018466f3a33078cbbd7caf4cddcddbb Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 30 Jan 2024 11:14:20 -0500 Subject: [PATCH 021/102] Prepare for v0.17.2 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 86e3c1e..f82e9c8 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.17.1" +(defproject io.github.manetu/temporal-sdk "0.17.2-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From c13aeb21046b004ca945041933e2c77dee5513a2 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 1 Mar 2024 17:25:49 -0500 Subject: [PATCH 022/102] Add slingshot based exception handling Signed-off-by: Greg Haskins --- doc/workflows.md | 18 +++++++++++ project.clj | 3 +- src/temporal/activity.clj | 3 ++ src/temporal/client/core.clj | 6 ++-- src/temporal/exceptions.clj | 5 +++ src/temporal/internal/activity.clj | 15 ++++++--- src/temporal/internal/exceptions.clj | 25 +++++++++++++++ src/temporal/internal/workflow.clj | 35 ++++++++++++--------- test/temporal/test/slingshot.clj | 46 ++++++++++++++++++++++++++++ 9 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 src/temporal/exceptions.clj create mode 100644 src/temporal/internal/exceptions.clj create mode 100644 test/temporal/test/slingshot.clj diff --git a/doc/workflows.md b/doc/workflows.md index 6033bb6..2b56e50 100644 --- a/doc/workflows.md +++ b/doc/workflows.md @@ -219,3 +219,21 @@ A query consists of a `query-type` (keyword) and possibly some `args` (any seria ```clojure (query workflow :my-query {:foo "bar"}) ``` + +## Exceptions + +This SDK integrates with the [slingshot](https://github.com/scgilardi/slingshot) library. Stones cast with slingshot's throw+ are serialized and re-thrown across activity and workflow boundaries in a transparent manner that is compatible with slingshot idiomatic try+ based catch blocks. + +### Managing Retries +By default, stones cast that are not caught locally by an activity or workflow trigger ApplicationFailure semantics and are thus subject to the overall Retry Policies in place. However, the developer may force a given stone to be non-retriable by setting the flag '::non-retriable?' within the object. + +Example: + +```clojure +(require `[temporal.exceptions :as e]) +(require `[slingshot.slingshot :refer [throw+]]) + +(defactivity my-activity + [ctx args] + (throw+ {:type ::my-fatal-error :msg "this error is non-retriable" ::e/non-retriable? true})) +``` diff --git a/project.clj b/project.clj index f82e9c8..dcf14a2 100644 --- a/project.clj +++ b/project.clj @@ -19,7 +19,8 @@ [com.taoensso/timbre "6.3.1"] [com.taoensso/nippy "3.3.0"] [funcool/promesa "9.2.542"] - [medley "1.4.0"]] + [medley "1.4.0"] + [slingshot "0.12.2"]] :repl-options {:init-ns user} :java-source-paths ["src"] :javac-options ["-target" "11" "-source" "11"] diff --git a/src/temporal/activity.clj b/src/temporal/activity.clj index 0dcfe8a..49bb46c 100644 --- a/src/temporal/activity.clj +++ b/src/temporal/activity.clj @@ -5,6 +5,7 @@ (:require [taoensso.timbre :as log] [taoensso.nippy :as nippy] [promesa.core :as p] + [temporal.internal.exceptions :as e] [temporal.internal.activity :as a] [temporal.internal.utils :as u] [temporal.internal.promise]) ;; needed for IPromise protocol extention @@ -98,6 +99,7 @@ Arguments: (log/trace "invoke:" activity "with" params options) (-> (.executeAsync stub act-name u/bytes-type (u/->objarray params)) (p/then (partial complete-invoke activity)) + (p/catch e/slingshot? e/recast-stone) (p/catch (fn [e] (log/error e) (throw e))))))) @@ -138,6 +140,7 @@ Arguments: (log/trace "local-invoke:" activity "with" params options) (-> (.executeAsync stub act-name u/bytes-type (u/->objarray params)) (p/then (partial complete-invoke activity)) + (p/catch e/slingshot? e/recast-stone) (p/catch (fn [e] (log/error e) (throw e))))))) diff --git a/src/temporal/client/core.clj b/src/temporal/client/core.clj index 804666d..2e2b9f1 100644 --- a/src/temporal/client/core.clj +++ b/src/temporal/client/core.clj @@ -6,7 +6,8 @@ [taoensso.nippy :as nippy] [promesa.core :as p] [temporal.internal.workflow :as w] - [temporal.internal.utils :as u]) + [temporal.internal.utils :as u] + [temporal.internal.exceptions :as e]) (:import [java.time Duration] [io.temporal.client WorkflowClient WorkflowClientOptions WorkflowClientOptions$Builder WorkflowStub] [io.temporal.serviceclient WorkflowServiceStubs WorkflowServiceStubsOptions WorkflowServiceStubsOptions$Builder] @@ -156,7 +157,8 @@ defworkflow once the workflow concludes. " [{:keys [^WorkflowStub stub] :as workflow}] (-> (.getResultAsync stub u/bytes-type) - (p/then nippy/thaw))) + (p/then nippy/thaw) + (p/catch e/slingshot? e/recast-stone))) (defn query " diff --git a/src/temporal/exceptions.clj b/src/temporal/exceptions.clj new file mode 100644 index 0000000..1062ede --- /dev/null +++ b/src/temporal/exceptions.clj @@ -0,0 +1,5 @@ +;; Copyright © 2024 Manetu, Inc. All rights reserved + +(ns temporal.exceptions) + +(def flags {::non-retriable? "'true' indicates this exception is not going to be retried even if it is not included into retry policy doNotRetry list."}) diff --git a/src/temporal/internal/activity.clj b/src/temporal/internal/activity.clj index 1da7f40..57d865f 100644 --- a/src/temporal/internal/activity.clj +++ b/src/temporal/internal/activity.clj @@ -4,9 +4,11 @@ (:require [clojure.core.protocols :as p] [clojure.datafy :as d] [clojure.core.async :refer [go args args)] (log/trace activity-id "calling" f "with args:" a) - (try - (result-> activity-id (f ctx a)) - (catch Exception e - (log/error e) - (throw e))))) + (try+ + (result-> activity-id (f ctx a)) + (catch Exception e + (log/error e) + (throw e)) + (catch Object o + (log/error &throw-context) + (e/freeze &throw-context))))) (defn dispatcher [ctx dispatch] (reify DynamicActivity diff --git a/src/temporal/internal/exceptions.clj b/src/temporal/internal/exceptions.clj new file mode 100644 index 0000000..2ba2cab --- /dev/null +++ b/src/temporal/internal/exceptions.clj @@ -0,0 +1,25 @@ +;; Copyright © 2024 Manetu, Inc. All rights reserved + +(ns temporal.internal.exceptions + (:require [slingshot.slingshot :refer [throw+]] + [temporal.exceptions :as e] + [temporal.internal.utils :as u]) + (:import [io.temporal.failure ApplicationFailure])) + +(def exception-type (name ::slingshot)) + +(defn slingshot? [ex] + (and (instance? ApplicationFailure (ex-cause ex)) + (= exception-type (.getType (cast ApplicationFailure (ex-cause ex)))))) + +(defn freeze + [{{:keys [::e/non-retriable?] :or {non-retriable? false}} :object :as context}] + (let [o (u/->objarray context) + t (if non-retriable? + (ApplicationFailure/newNonRetryableFailure nil exception-type o) + (ApplicationFailure/newFailure nil exception-type o))] + (throw t))) + +(defn recast-stone [ex] + (let [stone (->> ex ex-cause (cast ApplicationFailure) (.getDetails) u/->args :object)] + (throw+ stone))) ;; FIXME: Does not preserve the original stack-trace diff --git a/src/temporal/internal/workflow.clj b/src/temporal/internal/workflow.clj index 2bd4429..addfed9 100644 --- a/src/temporal/internal/workflow.clj +++ b/src/temporal/internal/workflow.clj @@ -3,11 +3,13 @@ (ns ^:no-doc temporal.internal.workflow (:require [clojure.core.protocols :as p] [clojure.datafy :as d] + [slingshot.slingshot :refer [try+]] [taoensso.timbre :as log] [taoensso.nippy :as nippy] [temporal.common :as common] [temporal.internal.utils :as u] - [temporal.internal.signals :as s]) + [temporal.internal.signals :as s] + [temporal.internal.exceptions :as e]) (:import [io.temporal.workflow Workflow WorkflowInfo] [io.temporal.client WorkflowOptions WorkflowOptions$Builder])) @@ -51,17 +53,20 @@ (defn execute [ctx dispatch args] - (try - (let [{:keys [workflow-type workflow-id]} (get-info) - d (u/find-dispatch dispatch workflow-type) - f (:fn d) - a (u/->args args) - _ (log/trace workflow-id "calling" f "with args:" a) - r (if (-> d :type (= :legacy)) - (f ctx {:args a :signals (s/create-signal-chan)}) - (f a))] - (log/trace workflow-id "result:" r) - (nippy/freeze r)) - (catch Exception e - (log/error e) - (throw e)))) + (try+ + (let [{:keys [workflow-type workflow-id]} (get-info) + d (u/find-dispatch dispatch workflow-type) + f (:fn d) + a (u/->args args) + _ (log/trace workflow-id "calling" f "with args:" a) + r (if (-> d :type (= :legacy)) + (f ctx {:args a :signals (s/create-signal-chan)}) + (f a))] + (log/trace workflow-id "result:" r) + (nippy/freeze r)) + (catch Exception e + (log/error e) + (throw e)) + (catch Object o + (log/error &throw-context) + (e/freeze &throw-context)))) diff --git a/test/temporal/test/slingshot.clj b/test/temporal/test/slingshot.clj new file mode 100644 index 0000000..c3e774e --- /dev/null +++ b/test/temporal/test/slingshot.clj @@ -0,0 +1,46 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.slingshot + (:require [clojure.test :refer :all] + [taoensso.timbre :as log] + [slingshot.slingshot :refer [try+ throw+]] + [temporal.client.core :as c] + [temporal.workflow :refer [defworkflow]] + [temporal.activity :refer [defactivity] :as a] + [temporal.exceptions :as e] + [temporal.test.utils :as t])) + +(use-fixtures :once t/wrap-service) + +(defactivity slingshot-nonretriable-activity + [ctx {:keys [name] :as args}] + (log/info "slingshot-nonretriable-activity:" args) + (throw+ {:type ::test1 ::e/non-retriable? true})) + +(defactivity slingshot-retriable-activity + [ctx {:keys [name] :as args}] + (log/info "slingshot-retriable-activity:" args) + (throw+ {:type ::test2})) + +(defworkflow slingshot-workflow + [args] + (log/info "slingshot-workflow:" args) + (try+ + @(a/invoke slingshot-nonretriable-activity args) + (catch [:type ::test1] _ + (log/info "caught stone 1") + (try+ + @(a/invoke slingshot-retriable-activity args) + (catch [:type ::test2] _ + (log/info "caught stone 2") + (throw+ {:type ::test3})))))) + +(deftest the-test + (testing "Verifies that we can catch slingshot stones across activity/workflow boundaries" + (let [workflow (t/create-workflow slingshot-workflow)] + (c/start workflow {}) + (try+ + @(c/get-result workflow) + (throw (ex-info "should not get here" {})) + (catch [:type ::test3] _ + (log/info "caught stone 3")))))) From 5e74b3ca5a9bec322eedbeb4c003ca35fa0d188e Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 1 Mar 2024 17:57:13 -0500 Subject: [PATCH 023/102] Release v0.18.0 Changes since v0.17.1 --------------------- c13aeb2 Add slingshot based exception handling Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index dcf14a2..1afe5eb 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.17.2-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.18.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From dad386a98f8af6487a7936eabc6b940c1ec043c1 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 1 Mar 2024 17:58:33 -0500 Subject: [PATCH 024/102] Prepare for v0.18.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 1afe5eb..b2dc3e4 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.18.0" +(defproject io.github.manetu/temporal-sdk "0.18.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From ac70775f8912c8f409efe1087c5a3707c225a8c5 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 5 Mar 2024 12:08:41 -0500 Subject: [PATCH 025/102] Enhance exception handling Signed-off-by: Greg Haskins --- src/temporal/client/core.clj | 5 +++- src/temporal/internal/activity.clj | 5 +--- src/temporal/internal/exceptions.clj | 30 +++++++++++++++++------ src/temporal/internal/workflow.clj | 5 +--- test/temporal/test/exception.clj | 36 ++++++++++++++++++++++++++++ test/temporal/test/slingshot.clj | 12 +++++----- 6 files changed, 71 insertions(+), 22 deletions(-) create mode 100644 test/temporal/test/exception.clj diff --git a/src/temporal/client/core.clj b/src/temporal/client/core.clj index 2e2b9f1..151d3b9 100644 --- a/src/temporal/client/core.clj +++ b/src/temporal/client/core.clj @@ -158,7 +158,10 @@ defworkflow once the workflow concludes. [{:keys [^WorkflowStub stub] :as workflow}] (-> (.getResultAsync stub u/bytes-type) (p/then nippy/thaw) - (p/catch e/slingshot? e/recast-stone))) + (p/catch e/slingshot? e/recast-stone) + (p/catch (fn [e] + (log/error e) + (throw e))))) (defn query " diff --git a/src/temporal/internal/activity.clj b/src/temporal/internal/activity.clj index 57d865f..266d72e 100644 --- a/src/temporal/internal/activity.clj +++ b/src/temporal/internal/activity.clj @@ -107,12 +107,9 @@ (log/trace activity-id "calling" f "with args:" a) (try+ (result-> activity-id (f ctx a)) - (catch Exception e - (log/error e) - (throw e)) (catch Object o (log/error &throw-context) - (e/freeze &throw-context))))) + (e/forward &throw-context))))) (defn dispatcher [ctx dispatch] (reify DynamicActivity diff --git a/src/temporal/internal/exceptions.clj b/src/temporal/internal/exceptions.clj index 2ba2cab..dcf5cc6 100644 --- a/src/temporal/internal/exceptions.clj +++ b/src/temporal/internal/exceptions.clj @@ -2,6 +2,7 @@ (ns temporal.internal.exceptions (:require [slingshot.slingshot :refer [throw+]] + [taoensso.timbre :as log] [temporal.exceptions :as e] [temporal.internal.utils :as u]) (:import [io.temporal.failure ApplicationFailure])) @@ -12,13 +13,28 @@ (and (instance? ApplicationFailure (ex-cause ex)) (= exception-type (.getType (cast ApplicationFailure (ex-cause ex)))))) -(defn freeze - [{{:keys [::e/non-retriable?] :or {non-retriable? false}} :object :as context}] - (let [o (u/->objarray context) - t (if non-retriable? - (ApplicationFailure/newNonRetryableFailure nil exception-type o) - (ApplicationFailure/newFailure nil exception-type o))] - (throw t))) +(defn forward + [{:keys [message wrapper] {:keys [::e/non-retriable?] :or {non-retriable? false} :as object} :object :as context}] + (log/trace "forward:" context) + (cond + (and (map? object) (empty? object) (true? (some->> wrapper (instance? Throwable)))) + (do + (log/trace "recasting wrapped exception") + (throw wrapper)) + + (instance? Throwable object) + (do + (log/trace "recasting throwable") + (throw object)) + + :else + (do + (log/trace "recasting stone within ApplicationFailure") + (let [o (u/->objarray context) + t (if non-retriable? + (ApplicationFailure/newNonRetryableFailure message exception-type o) + (ApplicationFailure/newFailure message exception-type o))] + (throw t))))) (defn recast-stone [ex] (let [stone (->> ex ex-cause (cast ApplicationFailure) (.getDetails) u/->args :object)] diff --git a/src/temporal/internal/workflow.clj b/src/temporal/internal/workflow.clj index addfed9..de45c57 100644 --- a/src/temporal/internal/workflow.clj +++ b/src/temporal/internal/workflow.clj @@ -64,9 +64,6 @@ (f a))] (log/trace workflow-id "result:" r) (nippy/freeze r)) - (catch Exception e - (log/error e) - (throw e)) (catch Object o (log/error &throw-context) - (e/freeze &throw-context)))) + (e/forward &throw-context)))) diff --git a/test/temporal/test/exception.clj b/test/temporal/test/exception.clj new file mode 100644 index 0000000..90e896e --- /dev/null +++ b/test/temporal/test/exception.clj @@ -0,0 +1,36 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.exception + (:require [clojure.test :refer :all] + [taoensso.timbre :as log] + [temporal.client.core :as c] + [temporal.workflow :refer [defworkflow]] + [temporal.activity :refer [defactivity] :as a] + [temporal.test.utils :as t])) + +(use-fixtures :once t/wrap-service) + +(defactivity exception-activity + [ctx args] + (log/info "exception-activity:" args) + (throw (ex-info "test 1" {}))) + +(defworkflow indirect-exception-workflow + [args] + (log/info "indirect-exception-workflow:" args) + @(a/invoke exception-activity args {:retry-options {:maximum-attempts 1}})) + +(defworkflow direct-exception-workflow + [args] + (log/info "direct-exception-workflow:" args) + (throw (ex-info "test 2" {}))) + +(deftest the-test + (testing "Verifies that we can throw exceptions indirectly from an activity" + (let [workflow (t/create-workflow indirect-exception-workflow)] + (c/start workflow {}) + (is (thrown? Exception @(c/get-result workflow))))) + (testing "Verifies that we can throw exceptions directly from a workflow" + (let [workflow (t/create-workflow direct-exception-workflow)] + (c/start workflow {}) + (is (thrown? Exception @(c/get-result workflow)))))) diff --git a/test/temporal/test/slingshot.clj b/test/temporal/test/slingshot.clj index c3e774e..d4401d0 100644 --- a/test/temporal/test/slingshot.clj +++ b/test/temporal/test/slingshot.clj @@ -30,7 +30,7 @@ (catch [:type ::test1] _ (log/info "caught stone 1") (try+ - @(a/invoke slingshot-retriable-activity args) + @(a/invoke slingshot-retriable-activity args {:retry-options {:maximum-attempts 2}}) (catch [:type ::test2] _ (log/info "caught stone 2") (throw+ {:type ::test3})))))) @@ -39,8 +39,8 @@ (testing "Verifies that we can catch slingshot stones across activity/workflow boundaries" (let [workflow (t/create-workflow slingshot-workflow)] (c/start workflow {}) - (try+ - @(c/get-result workflow) - (throw (ex-info "should not get here" {})) - (catch [:type ::test3] _ - (log/info "caught stone 3")))))) + (is (= :ok (try+ + @(c/get-result workflow) + (throw (ex-info "should not get here" {})) + (catch [:type ::test3] _ + :ok))))))) From c74bc8999ef65cd94c8eb75dbc747013fdf74745 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 5 Mar 2024 13:02:35 -0500 Subject: [PATCH 026/102] Release v0.18.1 Changes since v0.18.0 --------------------- ac70775 Enhance exception handling Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index b2dc3e4..a41ea4c 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.18.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.18.1" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From e128500d1271712c3e28e9095db3c854f33bca7d Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 5 Mar 2024 13:03:38 -0500 Subject: [PATCH 027/102] Prepare for v0.18.2 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index a41ea4c..5dc0e16 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.18.1" +(defproject io.github.manetu/temporal-sdk "0.18.2-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 9f0e5202a7ac5644d962840ce66baed815598959 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Wed, 6 Mar 2024 20:42:17 -0500 Subject: [PATCH 028/102] Add support for versioning Signed-off-by: Greg Haskins --- README.md | 2 +- src/temporal/client/worker.clj | 14 ++++++------- src/temporal/internal/workflow.clj | 3 ++- src/temporal/workflow.clj | 7 +++++++ test/temporal/test/versioning.clj | 32 ++++++++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 test/temporal/test/versioning.clj diff --git a/README.md b/README.md index 0e3c40b..f6bdf41 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This Clojure SDK is a framework for authoring Workflows and Activities in Clojur **Alpha** -This SDK is battle-tested and used in production but is undergoing active development and is subject to breaking changes (*). Some significant features (Versioning, Queries, and Child-Workflows, etc) are missing/incomplete. +This SDK is battle-tested and used in production but is undergoing active development and is subject to breaking changes (*). Some significant features such as Child-Workflows and Schedules are missing/incomplete. > (*) We will always bump at least the minor version when breaking changes are introduced and include a release note. diff --git a/src/temporal/client/worker.clj b/src/temporal/client/worker.clj index f153fde..927681b 100644 --- a/src/temporal/client/worker.clj +++ b/src/temporal/client/worker.clj @@ -21,13 +21,13 @@ Initializes a worker instance, suitable for real connections or unit-testing wit {:activities (a/import-dispatch activities) :workflows (w/import-dispatch workflows)})] (log/trace "init:" dispatch) (.registerActivitiesImplementations worker (to-array [(a/dispatcher ctx (:activities dispatch))])) - (.addWorkflowImplementationFactory worker DynamicWorkflowProxy - (u/->Func - (fn [] - (new DynamicWorkflowProxy - (reify DynamicWorkflow - (execute [_ args] - (w/execute ctx (:workflows dispatch) args))))))))) + (.registerWorkflowImplementationFactory worker DynamicWorkflowProxy + (u/->Func + (fn [] + (new DynamicWorkflowProxy + (reify DynamicWorkflow + (execute [_ args] + (w/execute ctx (:workflows dispatch) args))))))))) (def worker-factory-options " Options for configuring the worker-factory (See [[start]]) diff --git a/src/temporal/internal/workflow.clj b/src/temporal/internal/workflow.clj index de45c57..9baf686 100644 --- a/src/temporal/internal/workflow.clj +++ b/src/temporal/internal/workflow.clj @@ -34,7 +34,8 @@ {:namespace (.getNamespace d) :workflow-id (.getWorkflowId d) :run-id (.getRunId d) - :workflow-type (.getWorkflowType d)})) + :workflow-type (.getWorkflowType d) + :attempt (.getAttempt d)})) (defn get-info [] (d/datafy (Workflow/getInfo))) diff --git a/src/temporal/workflow.clj b/src/temporal/workflow.clj index 589c4ee..6bae378 100644 --- a/src/temporal/workflow.clj +++ b/src/temporal/workflow.clj @@ -34,6 +34,13 @@ [^Duration duration] (Workflow/sleep duration)) +(def default-version Workflow/DEFAULT_VERSION) + +(defn get-version + "Used to safely perform backwards incompatible changes to workflow definitions" + [change-id min max] + (Workflow/getVersion (u/namify change-id) min max)) + (defn register-query-handler! " Registers a DynamicQueryHandler listener that handles queries sent to the workflow, using [[temporal.client.core/query]]. diff --git a/test/temporal/test/versioning.clj b/test/temporal/test/versioning.clj new file mode 100644 index 0000000..96c008f --- /dev/null +++ b/test/temporal/test/versioning.clj @@ -0,0 +1,32 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.versioning + (:require [clojure.test :refer :all] + [taoensso.timbre :as log] + [temporal.client.core :as c] + [temporal.workflow :refer [defworkflow] :as w] + [temporal.activity :refer [defactivity] :as a] + [temporal.test.utils :as t])) + +(use-fixtures :once t/wrap-service) + +;; Only serves to generate events in our history +(defactivity versioned-activity + [ctx args] + :ok) + +(defworkflow versioned-workflow + [args] + (log/info "versioned-workflow:" args) + @(a/invoke versioned-activity args) + + (case (w/get-version ::test w/default-version 1) + w/default-version @(a/invoke versioned-activity args) + 1 @(a/local-invoke versioned-activity args))) + +(deftest the-test + (testing "Verifies that we can version a workflow" + (let [client (t/get-client) + wf (c/create-workflow client versioned-workflow {:task-queue t/task-queue :workflow-id "test"})] + (c/start wf {}) + (is (= @(c/get-result wf) :ok))))) From b2a8fe5fffa114946de86fe67cc243dd3b459265 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Wed, 6 Mar 2024 20:53:34 -0500 Subject: [PATCH 029/102] Release v0.18.2 Changes since v0.18.1 --------------------- 9f0e520 Add support for versioning Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 5dc0e16..04b2d50 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.18.2-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.18.2" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 0eabf7a8083b5bedf02337c416507858fe993ea8 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Wed, 6 Mar 2024 20:54:55 -0500 Subject: [PATCH 030/102] Prepare for v0.18.3 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 04b2d50..335abfc 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.18.2" +(defproject io.github.manetu/temporal-sdk "0.18.3-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 4e4df2b95022f03ad47645968a79c1a63eb2f7d8 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 7 Mar 2024 16:43:51 -0500 Subject: [PATCH 031/102] Enhance versioning support Signed-off-by: Greg Haskins --- doc/workflows.md | 23 +++++++++++++- project.clj | 1 + src/temporal/testing/env.clj | 2 +- src/temporal/workflow.clj | 2 +- test/temporal/test/utils.clj | 10 ++++-- test/temporal/test/versioning.clj | 52 ++++++++++++++++++++++++------- 6 files changed, 73 insertions(+), 17 deletions(-) diff --git a/doc/workflows.md b/doc/workflows.md index 2b56e50..3ae378b 100644 --- a/doc/workflows.md +++ b/doc/workflows.md @@ -37,7 +37,7 @@ Even though Temporal has the replay capability, which brings resilience to your There are some things, however, to think about when writing your Workflows, namely determinism and isolation. We summarize these constraints here: - Do not use any mutable global variables such as atoms in your Workflow implementations. This will ensure that multiple Workflow instances are fully isolated. -- Do not call any non-deterministic functions like non-seeded random or uuid-generators directly from the Workflow code. (Coming soon: SideEffect API) +- Do not call any non-deterministic functions like non-seeded random or uuid-generators directly from the Workflow code. - Perform all IO operations and calls to third-party services on Activities and not Workflows, as they are usually non-deterministic. - Do not use any programming language constructs that rely on system time. (Coming soon: API methods for time) - Do not use threading primitives such as clojure.core.async/go or clojure.core.async/thread. (Coming soon: API methods for async function execution) @@ -237,3 +237,24 @@ Example: [ctx args] (throw+ {:type ::my-fatal-error :msg "this error is non-retriable" ::e/non-retriable? true})) ``` + +## Versioning + +The Temporal Platform requires that Workflow code be deterministic. Because of that requirement, this Clojure SDK exposes a workflow patching API [temporal.workflow/get-version](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.workflow#get-version). Workflow developers use the `get-version` function to determine the correct branch of logic to follow to maintain determinism. + +Example: + +Assume we have a workflow that invokes an activity using temporal.activity/invoke that we wish to convert to temporal.activity/local-invoke. Changing this for future workflows is not a problem. However, any existing workflows need to be careful as this change could introduce non-determinism. + +We can safely handle both the original and the new desired scenario by branching based on the results from calling temporal.workflow/get-version: + +```clojure +(require `[temporal.workflow :as w]) +(require `[temporal.activity :as a]) + +(let [version (w/get-version ::local-activity w/default-version 1)] + (cond + (= version w/default-version) @(a/invoke versioned-activity :v1) + (= version 1) @(a/local-invoke versioned-activity :v2))) +``` + diff --git a/project.clj b/project.clj index 335abfc..1269a98 100644 --- a/project.clj +++ b/project.clj @@ -30,6 +30,7 @@ :profiles {:dev {:dependencies [[org.clojure/tools.namespace "1.4.4"] [eftest "0.6.0"] + [mockery "0.1.4"] [io.temporal/temporal-opentracing "1.22.3"]] :resource-paths ["test/temporal/test/resources"]}} :cloverage {:runner :eftest diff --git a/src/temporal/testing/env.clj b/src/temporal/testing/env.clj index fe8815b..cc08acd 100644 --- a/src/temporal/testing/env.clj +++ b/src/temporal/testing/env.clj @@ -62,7 +62,7 @@ Arguments: (let [worker (.newWorker env (u/namify task-queue))] (worker/init worker options) (.start env) - :ok)) + worker)) (defn stop " diff --git a/src/temporal/workflow.clj b/src/temporal/workflow.clj index 6bae378..0c1e3cb 100644 --- a/src/temporal/workflow.clj +++ b/src/temporal/workflow.clj @@ -34,7 +34,7 @@ [^Duration duration] (Workflow/sleep duration)) -(def default-version Workflow/DEFAULT_VERSION) +(def default-version (int Workflow/DEFAULT_VERSION)) (defn get-version "Used to safely perform backwards incompatible changes to workflow definitions" diff --git a/test/temporal/test/utils.clj b/test/temporal/test/utils.clj index 8a828ec..299e6cf 100644 --- a/test/temporal/test/utils.clj +++ b/test/temporal/test/utils.clj @@ -19,6 +19,9 @@ ;;----------------------------------------------------------------------------- ;; Utilities ;;----------------------------------------------------------------------------- +(defn get-worker [] + (get @state :worker)) + (defn get-client [] (get @state :client)) @@ -30,17 +33,18 @@ ;;----------------------------------------------------------------------------- (defn create-service [] (let [env (e/create) - client (e/get-client env)] - (e/start env {:task-queue task-queue}) + client (e/get-client env) + worker (e/start env {:task-queue task-queue})] (swap! state assoc :env env + :worker worker :client client))) (defn destroy-service [] (swap! state (fn [{:keys [env] :as s}] (e/stop env) - (dissoc s :env :client)))) + (dissoc s :env :worker :client)))) (defn wrap-service [test-fn] (create-service) diff --git a/test/temporal/test/versioning.clj b/test/temporal/test/versioning.clj index 96c008f..a6c8d3a 100644 --- a/test/temporal/test/versioning.clj +++ b/test/temporal/test/versioning.clj @@ -3,6 +3,7 @@ (ns temporal.test.versioning (:require [clojure.test :refer :all] [taoensso.timbre :as log] + [mockery.core :refer [with-mock]] [temporal.client.core :as c] [temporal.workflow :refer [defworkflow] :as w] [temporal.activity :refer [defactivity] :as a] @@ -10,23 +11,52 @@ (use-fixtures :once t/wrap-service) +(def mailbox (atom nil)) + ;; Only serves to generate events in our history (defactivity versioned-activity [ctx args] - :ok) + (reset! mailbox args) + args) + +(defn workflow-v1 + [] + (log/info "versioned-workflow v1") + @(a/invoke versioned-activity :foo) + @(a/invoke versioned-activity :v1)) + +(defn workflow-v2 + [] + (log/info "versioned-workflow v2") + @(a/invoke versioned-activity :foo) + (let [version (w/get-version ::test w/default-version 1)] + (cond + (= version w/default-version) @(a/invoke versioned-activity :v1) + (= version 1) @(a/local-invoke versioned-activity :v2)))) (defworkflow versioned-workflow [args] - (log/info "versioned-workflow:" args) - @(a/invoke versioned-activity args) - - (case (w/get-version ::test w/default-version 1) - w/default-version @(a/invoke versioned-activity args) - 1 @(a/local-invoke versioned-activity args))) + (workflow-v1)) (deftest the-test - (testing "Verifies that we can version a workflow" - (let [client (t/get-client) - wf (c/create-workflow client versioned-workflow {:task-queue t/task-queue :workflow-id "test"})] + (let [client (t/get-client) + worker (t/get-worker) + wf (c/create-workflow client versioned-workflow {:task-queue t/task-queue :workflow-id "test-1"})] + (testing "Invoke our v1 workflow" (c/start wf {}) - (is (= @(c/get-result wf) :ok))))) + (is (= @(c/get-result wf) :v1)) + (is (= @mailbox :v1))) + (with-mock _ + {:target ::workflow-v1 + :return workflow-v2} ;; emulates a code update by dynamically substituting v2 for v1 + (testing "Replay the workflow after upgrading the code" + (reset! mailbox :slug) + (let [history (.fetchHistory client "test-1")] + (.replayWorkflowExecution worker history)) + (is (= @mailbox :slug))) ;; activity is not re-executed in replay, so the :slug should remain + (testing "Invoke our workflow fresh and verify that it takes the v2 path" + (reset! mailbox nil) + (let [wf2 (c/create-workflow client versioned-workflow {:task-queue t/task-queue :workflow-id "test-2"})] + (c/start wf2 {}) + (is (= @(c/get-result wf2) :v2)) + (is (= @mailbox :v2))))))) From 683034a804c58b27319fee6f0c85ec2c889a970a Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 7 Mar 2024 17:05:03 -0500 Subject: [PATCH 032/102] Release v0.18.3 Changes since v0.18.2 --------------------- 4e4df2b Enhance versioning support Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 1269a98..f9bf03a 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.18.3-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.18.3" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From d85e6ca7daac27a0056e7d5123e956a573867da8 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 7 Mar 2024 17:06:03 -0500 Subject: [PATCH 033/102] Prepare for v0.18.4 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index f9bf03a..cc9ae47 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.18.3" +(defproject io.github.manetu/temporal-sdk "0.18.4-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 338c381a56178a1c7b6f677b85629ec166949352 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 11 Mar 2024 13:29:34 -0400 Subject: [PATCH 034/102] Add support for :workflow-id-reuse-policy Signed-off-by: Greg Haskins --- project.clj | 2 +- src/temporal/client/core.clj | 47 ++++++++++++++++++++++--- src/temporal/internal/exceptions.clj | 2 +- src/temporal/internal/workflow.clj | 19 +---------- test/temporal/test/reuse_policy.clj | 51 ++++++++++++++++++++++++++++ test/temporal/test/types.clj | 19 +++++------ 6 files changed, 106 insertions(+), 34 deletions(-) create mode 100644 test/temporal/test/reuse_policy.clj diff --git a/project.clj b/project.clj index cc9ae47..48dcfb9 100644 --- a/project.clj +++ b/project.clj @@ -36,5 +36,5 @@ :cloverage {:runner :eftest :runner-opts {:multithread? false :fail-fast? true} - :fail-threshold 87 + :fail-threshold 90 :ns-exclude-regex [#"temporal.client.worker"]}) diff --git a/src/temporal/client/core.clj b/src/temporal/client/core.clj index 151d3b9..b59fefe 100644 --- a/src/temporal/client/core.clj +++ b/src/temporal/client/core.clj @@ -5,13 +5,16 @@ (:require [taoensso.timbre :as log] [taoensso.nippy :as nippy] [promesa.core :as p] + [temporal.common :as common] [temporal.internal.workflow :as w] [temporal.internal.utils :as u] [temporal.internal.exceptions :as e]) (:import [java.time Duration] - [io.temporal.client WorkflowClient WorkflowClientOptions WorkflowClientOptions$Builder WorkflowStub] + [io.temporal.client WorkflowClient WorkflowClientOptions WorkflowClientOptions$Builder WorkflowStub + WorkflowOptions WorkflowOptions$Builder] [io.temporal.serviceclient WorkflowServiceStubs WorkflowServiceStubsOptions WorkflowServiceStubsOptions$Builder] - [io.temporal.common.interceptors WorkflowClientInterceptorBase])) + [io.temporal.common.interceptors WorkflowClientInterceptorBase] + [io.temporal.api.enums.v1 WorkflowIdReusePolicy])) (def ^:no-doc stub-options {:channel #(.setChannel ^WorkflowServiceStubsOptions$Builder %1 %2) @@ -87,6 +90,41 @@ Arguments: (let [service (WorkflowServiceStubs/newConnectedServiceStubs (stub-options-> options) timeout)] (WorkflowClient/newInstance service (client-options-> options))))) +(def workflow-id-reuse-options + " +| Value | Description | +| ---------------------------- | --------------------------------------------------------------------------- | +| :allow-duplicate | Allow starting a workflow execution using the same workflow id. | +| :allow-duplicate-failed-only | Allow starting a workflow execution using the same workflow id, only when the last execution's final state is one of [terminated, cancelled, timed out, failed] | +| :reject-duplicate | Do not permit re-use of the workflow id for this workflow. | +| :terminate-if-running | If a workflow is running using the same workflow ID, terminate it and start a new one. If no running workflow, then the behavior is the same as ALLOW_DUPLICATE| +" + {:allow-duplicate WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE + :allow-duplicate-failed-only WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY + :reject-duplicate WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE + :terminate-if-running WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING}) + +(defn ^:no-doc workflow-id-reuse-policy-> + ^WorkflowIdReusePolicy [policy] + (or (get workflow-id-reuse-options policy) + (throw (IllegalArgumentException. (str "Unknown workflow-id-reuse-policy: " policy " Must be one of " (keys workflow-id-reuse-options)))))) + +(def ^:no-doc wf-option-spec + {:task-queue #(.setTaskQueue ^WorkflowOptions$Builder %1 (u/namify %2)) + :workflow-id #(.setWorkflowId ^WorkflowOptions$Builder %1 (u/namify %2)) + :workflow-id-reuse-policy #(.setWorkflowIdReusePolicy ^WorkflowOptions$Builder %1 (workflow-id-reuse-policy-> %2)) + :workflow-execution-timeout #(.setWorkflowExecutionTimeout ^WorkflowOptions$Builder %1 %2) + :workflow-run-timeout #(.setWorkflowRunTimeout ^WorkflowOptions$Builder %1 %2) + :workflow-task-timeout #(.setWorkflowTaskTimeout ^WorkflowOptions$Builder %1 %2) + :retry-options #(.setRetryOptions %1 (common/retry-options-> %2)) + :cron-schedule #(.setCronSchedule ^WorkflowOptions$Builder %1 %2) + :memo #(.setMemo ^WorkflowOptions$Builder %1 %2) + :search-attributes #(.setSearchAttributes ^WorkflowOptions$Builder %1 %2)}) + +(defn ^:no-doc wf-options-> + ^WorkflowOptions [params] + (u/build (WorkflowOptions/newBuilder (WorkflowOptions/getDefaultInstance)) wf-option-spec params)) + (defn create-workflow " Create a new workflow-stub instance, suitable for managing and interacting with a workflow through it's lifecycle. @@ -104,12 +142,13 @@ Create a new workflow-stub instance, suitable for managing and interacting with ``` " ([^WorkflowClient client workflow-id] - (let [stub (.newUntypedWorkflowStub client workflow-id)] + (let [stub (.newUntypedWorkflowStub client (u/namify workflow-id))] (log/trace "create-workflow id:" workflow-id) {:client client :stub stub})) ([^WorkflowClient client workflow options] (let [wf-name (w/get-annotated-name workflow) - stub (.newUntypedWorkflowStub client wf-name (w/wf-options-> options))] + options (wf-options-> options) + stub (.newUntypedWorkflowStub client wf-name options)] (log/trace "create-workflow:" wf-name options) {:client client :stub stub}))) diff --git a/src/temporal/internal/exceptions.clj b/src/temporal/internal/exceptions.clj index dcf5cc6..0f07cbd 100644 --- a/src/temporal/internal/exceptions.clj +++ b/src/temporal/internal/exceptions.clj @@ -1,6 +1,6 @@ ;; Copyright © 2024 Manetu, Inc. All rights reserved -(ns temporal.internal.exceptions +(ns ^:no-doc temporal.internal.exceptions (:require [slingshot.slingshot :refer [throw+]] [taoensso.timbre :as log] [temporal.exceptions :as e] diff --git a/src/temporal/internal/workflow.clj b/src/temporal/internal/workflow.clj index 9baf686..7eebbb2 100644 --- a/src/temporal/internal/workflow.clj +++ b/src/temporal/internal/workflow.clj @@ -6,27 +6,10 @@ [slingshot.slingshot :refer [try+]] [taoensso.timbre :as log] [taoensso.nippy :as nippy] - [temporal.common :as common] [temporal.internal.utils :as u] [temporal.internal.signals :as s] [temporal.internal.exceptions :as e]) - (:import [io.temporal.workflow Workflow WorkflowInfo] - [io.temporal.client WorkflowOptions WorkflowOptions$Builder])) - -(def wf-option-spec - {:task-queue #(.setTaskQueue ^WorkflowOptions$Builder %1 (u/namify %2)) - :workflow-id #(.setWorkflowId ^WorkflowOptions$Builder %1 %2) - :workflow-execution-timeout #(.setWorkflowExecutionTimeout ^WorkflowOptions$Builder %1 %2) - :workflow-run-timeout #(.setWorkflowRunTimeout ^WorkflowOptions$Builder %1 %2) - :workflow-task-timeout #(.setWorkflowTaskTimeout ^WorkflowOptions$Builder %1 %2) - :retry-options #(.setRetryOptions %1 (common/retry-options-> %2)) - :cron-schedule #(.setCronSchedule ^WorkflowOptions$Builder %1 %2) - :memo #(.setMemo ^WorkflowOptions$Builder %1 %2) - :search-attributes #(.setSearchAttributes ^WorkflowOptions$Builder %1 %2)}) - -(defn wf-options-> - ^WorkflowOptions [params] - (u/build (WorkflowOptions/newBuilder (WorkflowOptions/getDefaultInstance)) wf-option-spec params)) + (:import [io.temporal.workflow Workflow WorkflowInfo])) (extend-protocol p/Datafiable WorkflowInfo diff --git a/test/temporal/test/reuse_policy.clj b/test/temporal/test/reuse_policy.clj new file mode 100644 index 0000000..e7ac122 --- /dev/null +++ b/test/temporal/test/reuse_policy.clj @@ -0,0 +1,51 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.reuse-policy + (:require [clojure.test :refer :all] + [slingshot.slingshot :refer [try+ throw+]] + [taoensso.timbre :as log] + [temporal.client.core :as c] + [temporal.workflow :refer [defworkflow] :as w] + [temporal.side-effect :as s] + [temporal.exceptions :as e] + [temporal.test.utils :as t]) + (:import [io.temporal.client WorkflowExecutionAlreadyStarted])) + +(use-fixtures :once t/wrap-service) + +(defworkflow reuse-workflow + [args] + (log/info "reuse-workflow:" args) + (case args + :sleep (w/await (constantly false)) + :crash (throw+ {:type ::synthetic-crash ::e/non-retriable? true}) + :exit (s/gen-uuid))) + +(defn invoke [{:keys [policy wid args] :or {wid ::wid args :exit policy :allow-duplicate}}] + (let [wf (c/create-workflow (t/get-client) reuse-workflow {:task-queue t/task-queue + :workflow-id wid + :workflow-id-reuse-policy policy})] + (c/start wf args) + @(c/get-result wf))) + +(deftest the-test + (testing "Verifies that :allow-duplicate policy works" + (dotimes [_ 2] + (is (some? (invoke {:wid ::allow-duplicate :policy :allow-duplicate}))))) + (testing "Verifies that :allow-duplicate-failed-only policy works" + (try+ + (invoke {:wid ::allow-duplicate-failed-only :args :crash}) + (catch [:type ::synthetic-crash] _ + :ok)) + (is (some? (invoke {:wid ::allow-duplicate-failed-only :policy :allow-duplicate-failed-only})))) + (testing "Verifies that :reject-duplicate policy works" + (let [result (invoke {:wid ::reject-duplicate :policy :reject-duplicate})] + (is (thrown? WorkflowExecutionAlreadyStarted (invoke {:wid ::reject-duplicate :policy :reject-duplicate}))) + (let [wf2 (c/create-workflow (t/get-client) ::reject-duplicate)] + (is (= result @(c/get-result wf2)))))) + (testing "Verifies that :terminate-if-running policy works" + (let [wf (c/create-workflow (t/get-client) reuse-workflow {:task-queue t/task-queue :workflow-id ::terminate-if-running})] + (c/start wf :sleep) + (is (some? (invoke {:wid ::terminate-if-running :policy :terminate-if-running}))))) + (testing "Verifies that a bogus reuse policy throws" + (is (thrown? IllegalStateException (invoke {:wid ::bogus :policy :bogus}))))) diff --git a/test/temporal/test/types.clj b/test/temporal/test/types.clj index 4c61ee0..aa1b15a 100644 --- a/test/temporal/test/types.clj +++ b/test/temporal/test/types.clj @@ -3,7 +3,6 @@ (ns temporal.test.types (:require [clojure.test :refer :all] [temporal.client.core :as client] - [temporal.internal.workflow :as workflow] [temporal.client.worker :as worker]) (:import [java.time Duration] [io.grpc Grpc InsecureChannelCredentials Metadata] @@ -11,15 +10,15 @@ (deftest workflow-options (testing "Verify that our workflow options work" - (let [x (workflow/wf-options-> {:workflow-id "foo" - :task-queue "bar" - :workflow-execution-timeout (Duration/ofSeconds 1) - :workflow-run-timeout (Duration/ofSeconds 1) - :workflow-task-timeout (Duration/ofSeconds 1) - :retry-options {:maximum-attempts 1} - :cron-schedule "* * * * *" - :memo {"foo" "bar"} - :search-attributes {"foo" "bar"}})] + (let [x (client/wf-options-> {:workflow-id "foo" + :task-queue "bar" + :workflow-execution-timeout (Duration/ofSeconds 1) + :workflow-run-timeout (Duration/ofSeconds 1) + :workflow-task-timeout (Duration/ofSeconds 1) + :retry-options {:maximum-attempts 1} + :cron-schedule "* * * * *" + :memo {"foo" "bar"} + :search-attributes {"foo" "bar"}})] (is (-> x (.getWorkflowId) (= "foo"))) (is (-> x (.getTaskQueue) (= "bar")))))) From 547db9772b14becc38925672e3bb2e99f8abe972 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 11 Mar 2024 13:40:33 -0400 Subject: [PATCH 035/102] Update deps We hold back promesa since we have dependencies on the internal protocols that break above 9.x Signed-off-by: Greg Haskins --- project.clj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/project.clj b/project.clj index 48dcfb9..335a62d 100644 --- a/project.clj +++ b/project.clj @@ -13,10 +13,10 @@ [lein-codox "0.10.8"]] :dependencies [[org.clojure/clojure "1.11.1"] [org.clojure/core.async "1.6.681"] - [io.temporal/temporal-sdk "1.22.3"] - [io.temporal/temporal-testing "1.22.3"] - [com.taoensso/encore "3.74.0"] - [com.taoensso/timbre "6.3.1"] + [io.temporal/temporal-sdk "1.23.0"] + [io.temporal/temporal-testing "1.23.0"] + [com.taoensso/encore "3.90.0"] + [com.taoensso/timbre "6.5.0"] [com.taoensso/nippy "3.3.0"] [funcool/promesa "9.2.542"] [medley "1.4.0"] @@ -28,10 +28,10 @@ :eastwood {:add-linters [:unused-namespaces]} :codox {:metadata {:doc/format :markdown}} - :profiles {:dev {:dependencies [[org.clojure/tools.namespace "1.4.4"] + :profiles {:dev {:dependencies [[org.clojure/tools.namespace "1.5.0"] [eftest "0.6.0"] [mockery "0.1.4"] - [io.temporal/temporal-opentracing "1.22.3"]] + [io.temporal/temporal-opentracing "1.23.0"]] :resource-paths ["test/temporal/test/resources"]}} :cloverage {:runner :eftest :runner-opts {:multithread? false From deb6a75faf4f40ae21d47b5dd40916f6f5fb3d65 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 11 Mar 2024 14:02:11 -0400 Subject: [PATCH 036/102] Release v0.19.0 Changes since v0.18.3 --------------------- 547db97 Update deps 338c381 Add support for :workflow-id-reuse-policy Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 335a62d..447ea76 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.18.4-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.19.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 9286e4b7f5307dc98c58fd9b75e2d2aeddba312f Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 11 Mar 2024 14:03:09 -0400 Subject: [PATCH 037/102] Prepare for v0.19.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 447ea76..2cef789 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.19.0" +(defproject io.github.manetu/temporal-sdk "0.19.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 711d0f5d0c0a98d878eff67f6152faa54be0fafa Mon Sep 17 00:00:00 2001 From: Bailey Kocin Date: Mon, 11 Mar 2024 17:26:02 -0400 Subject: [PATCH 038/102] add temporal schedule support Signed-off-by: Bailey Kocin --- .gitignore | 5 +- doc/clients.md | 78 ++++++++++++- src/temporal/client/core.clj | 83 ++------------ src/temporal/client/options.clj | 24 ++++ src/temporal/client/schedule.clj | 176 +++++++++++++++++++++++++++++ src/temporal/internal/grpc.clj | 28 +++++ src/temporal/internal/schedule.clj | 99 ++++++++++++++++ src/temporal/internal/workflow.clj | 47 +++++++- src/temporal/testing/env.clj | 11 +- test/temporal/test/schedule.clj | 146 ++++++++++++++++++++++++ test/temporal/test/types.clj | 123 +++++++++++++++----- 11 files changed, 701 insertions(+), 119 deletions(-) create mode 100644 src/temporal/client/options.clj create mode 100644 src/temporal/client/schedule.clj create mode 100644 src/temporal/internal/grpc.clj create mode 100644 src/temporal/internal/schedule.clj create mode 100644 test/temporal/test/schedule.clj diff --git a/.gitignore b/.gitignore index 1a4c6d1..7928361 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,7 @@ pom.xml.asc .hgignore .hg/ .idea* -*.iml \ No newline at end of file +*.iml +.calva/ +.lsp/ +.clj-kondo/ diff --git a/doc/clients.md b/doc/clients.md index f395b11..b6c6759 100644 --- a/doc/clients.md +++ b/doc/clients.md @@ -4,9 +4,13 @@ To initialize a Workflow Client, create an instance of a Workflow client with [temporal.client.core/create-client](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.core#create-client), create a Workflow stub with [temporal.client.core/create-workflow](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.core#create-workflow), and invoke the Workflow with [temporal.client.core/start](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.core#start)). Finally, gather the results of the Workflow with [temporal.client.core/get-result](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.core#get-result). +To initialize a Schedule Client, create an instance of a Schedule client with [temporal.client.schedule/create-client](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.schedule#create-client), create a Schedule with [temporal.client.schedule/schedule](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.schedule#schedule), and run a scheduled workflow execution immediately with [temporal.client.schedule/execute](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.schedule#execute)). + ## Details -To start a Workflow Execution, your Temporal Server must be running, and your front-end service must be accepting gRPC calls. +Using the namespace `temporal.client.core` or `temporal.client.schedule` + +To start a Workflow Execution or manage a Scheduled Workflow Execution, your Temporal Server must be running, and your front-end service must be accepting gRPC calls. You can provide options to (create-client) to establish a connection to the specifics of the Temporal front-end service in your environment. @@ -14,7 +18,9 @@ You can provide options to (create-client) to establish a connection to the spec (create-client {:target "temporal-frontend:7233" :namespace "my-namespace"}) ``` -After establishing a successful connection to the Temporal Frontend Service, you may perform operations such as: +### Workflow Executions + +After establishing a successful connection to the Temporal Frontend Service, you may perform Workflow Execution specific operations such as: - **Starting Workflows**: See [temporal.client.core/start](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.core#start) and [temporal.client.core/signal-with-start](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.core#signal-with-start) - **Signaling Workflows**: See [temporal.client.core/>!](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.core#%3E!) and [temporal.client.core/signal-with-start](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.core#signal-with-start) @@ -52,3 +58,71 @@ We can create a client to invoke our Workflow as follows: ``` Evaluating this code should result in `Hi, Bob` appearing on the console. Note that (get-result) returns a promise, thus requiring a dereference. + +### Scheduled Workflow Executions + +After establishing a successful connection to the Temporal Frontend Service, you may perform Schedule specific operations such as: + +- **Creating Schedules**: See [temporal.client.schedule/schedule](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.schedule/schedule) +- **Describing Schedules**: See [temporal.client.schedule/describe](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.schedule/describe) +- **Pausing Schedules**: See [temporal.client.schedule/pause](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.schedule/pause) +- **Unpausing Schedules**: See [temporal.client.schedule/unpause](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.schedule/unpause) +- **Removing Schedules**: See [temporal.client.schedule/unschedule](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.schedule/unschedule) +- **Triggering Schedules**: See [temporal.client.schedule/execute](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.schedule/execute) +- **Updating Schedules**: See [temporal.client.schedule/reschedule](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.schedule/reschedule) + +Schedules can be built with cron expressions or built in Temporal time types. + +Schedules can executed immediately via "triggering" them. + +Schedules will handle overlapping runs determined by the `SchedulePolicy` you give it when creating it. + +Example Usage + +As a simple example, for the following Workflow implementation: + +```clojure +(require '[temporal.workflow :refer [defworkflow]]) +(require '[temporal.activity :refer [defactivity] :as a]) + +(defactivity greet-activity + [ctx {:keys [name] :as args}] + (log/info "greet-activity:" args) + (str "Hi, " name)) + +(defworkflow greeter-workflow + [args] + (log/info "greeter-workflow:" args) + @(a/invoke greet-activity args)) +``` + +Create and manage a schedule as follows + + +```clojure +(require '[temporal.client.schedule :as s]) +(let [client-options {:target "localhost:7233" + :namespace "default" + :enable-https false} + task-queue "MyTaskQueue" + workflow-id "my-workflow" + schedule-id "my-schedule" + client (s/create-client client-options)] + (s/schedule client schedule-id {:schedule {:trigger-immediately? true + :memo "Created by John Doe"} + :spec {:cron-expressions ["0 0 * * *"] + :timezone "US/Central"} + :policy {:pause-on-failure? false + :catchup-window (Duration/ofSeconds 10) + :overlap :skip} + :action {:arguments {:name "John"} + :options {:workflow-id workflow-id + :task-queue task-queue} + :workflow-type greeter-workflow}}) + (s/describe client schedule-id) + (s/pause client schedule-id) + (s/unpause client schedule-id) + (s/execute client schedule-id :skip) + (s/reschedule client schedule-id {:spec {:cron-expressions ["0 1 * * *"]}}) + (s/unschedule client schedule-id)) +``` \ No newline at end of file diff --git a/src/temporal/client/core.clj b/src/temporal/client/core.clj index b59fefe..ef798f8 100644 --- a/src/temporal/client/core.clj +++ b/src/temporal/client/core.clj @@ -5,47 +5,13 @@ (:require [taoensso.timbre :as log] [taoensso.nippy :as nippy] [promesa.core :as p] - [temporal.common :as common] + [temporal.client.options :as copts] [temporal.internal.workflow :as w] + [temporal.internal.grpc :as g] [temporal.internal.utils :as u] [temporal.internal.exceptions :as e]) (:import [java.time Duration] - [io.temporal.client WorkflowClient WorkflowClientOptions WorkflowClientOptions$Builder WorkflowStub - WorkflowOptions WorkflowOptions$Builder] - [io.temporal.serviceclient WorkflowServiceStubs WorkflowServiceStubsOptions WorkflowServiceStubsOptions$Builder] - [io.temporal.common.interceptors WorkflowClientInterceptorBase] - [io.temporal.api.enums.v1 WorkflowIdReusePolicy])) - -(def ^:no-doc stub-options - {:channel #(.setChannel ^WorkflowServiceStubsOptions$Builder %1 %2) - :ssl-context #(.setSslContext ^WorkflowServiceStubsOptions$Builder %1 %2) - :enable-https #(.setEnableHttps ^WorkflowServiceStubsOptions$Builder %1 %2) - :target #(.setTarget ^WorkflowServiceStubsOptions$Builder %1 %2) - :rpc-timeout #(.setRpcTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) - :rpc-long-poll-timeout #(.setRpcLongPollTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) - :rpc-query-timeout #(.setRpcQueryTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) - :backoff-reset-freq #(.setConnectionBackoffResetFrequency ^WorkflowServiceStubsOptions$Builder %1 %2) - :grpc-reconnect-freq #(.setGrpcReconnectFrequency ^WorkflowServiceStubsOptions$Builder %1 %2) - :headers #(.setHeaders ^WorkflowServiceStubsOptions$Builder %1 %2) - :enable-keepalive #(.setEnableKeepAlive ^WorkflowServiceStubsOptions$Builder %1 %2) - :keepalive-time #(.setKeepAliveTime ^WorkflowServiceStubsOptions$Builder %1 %2) - :keepalive-timeout #(.setKeepAliveTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) - :keepalive-without-stream #(.setKeepAlivePermitWithoutStream ^WorkflowServiceStubsOptions$Builder %1 %2) - :metrics-scope #(.setMetricsScope ^WorkflowServiceStubsOptions$Builder %1 %2)}) - -(defn ^:no-doc stub-options-> - ^WorkflowServiceStubsOptions [params] - (u/build (WorkflowServiceStubsOptions/newBuilder) stub-options params)) - -(def ^:no-doc client-options - {:identity #(.setIdentity ^WorkflowClientOptions$Builder %1 %2) - :namespace #(.setNamespace ^WorkflowClientOptions$Builder %1 %2) - :data-converter #(.setDataConverter ^WorkflowClientOptions$Builder %1 %2) - :interceptors #(.setInterceptors ^WorkflowClientOptions$Builder %1 (into-array WorkflowClientInterceptorBase %2))}) - -(defn ^:no-doc client-options-> - ^WorkflowClientOptions [params] - (u/build (WorkflowClientOptions/newBuilder (WorkflowClientOptions/getDefaultInstance)) client-options params)) + [io.temporal.client WorkflowClient WorkflowStub])) (defn create-client " @@ -87,43 +53,8 @@ Arguments: ([options] (create-client options (Duration/ofSeconds 5))) ([options timeout] - (let [service (WorkflowServiceStubs/newConnectedServiceStubs (stub-options-> options) timeout)] - (WorkflowClient/newInstance service (client-options-> options))))) - -(def workflow-id-reuse-options - " -| Value | Description | -| ---------------------------- | --------------------------------------------------------------------------- | -| :allow-duplicate | Allow starting a workflow execution using the same workflow id. | -| :allow-duplicate-failed-only | Allow starting a workflow execution using the same workflow id, only when the last execution's final state is one of [terminated, cancelled, timed out, failed] | -| :reject-duplicate | Do not permit re-use of the workflow id for this workflow. | -| :terminate-if-running | If a workflow is running using the same workflow ID, terminate it and start a new one. If no running workflow, then the behavior is the same as ALLOW_DUPLICATE| -" - {:allow-duplicate WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE - :allow-duplicate-failed-only WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY - :reject-duplicate WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE - :terminate-if-running WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING}) - -(defn ^:no-doc workflow-id-reuse-policy-> - ^WorkflowIdReusePolicy [policy] - (or (get workflow-id-reuse-options policy) - (throw (IllegalArgumentException. (str "Unknown workflow-id-reuse-policy: " policy " Must be one of " (keys workflow-id-reuse-options)))))) - -(def ^:no-doc wf-option-spec - {:task-queue #(.setTaskQueue ^WorkflowOptions$Builder %1 (u/namify %2)) - :workflow-id #(.setWorkflowId ^WorkflowOptions$Builder %1 (u/namify %2)) - :workflow-id-reuse-policy #(.setWorkflowIdReusePolicy ^WorkflowOptions$Builder %1 (workflow-id-reuse-policy-> %2)) - :workflow-execution-timeout #(.setWorkflowExecutionTimeout ^WorkflowOptions$Builder %1 %2) - :workflow-run-timeout #(.setWorkflowRunTimeout ^WorkflowOptions$Builder %1 %2) - :workflow-task-timeout #(.setWorkflowTaskTimeout ^WorkflowOptions$Builder %1 %2) - :retry-options #(.setRetryOptions %1 (common/retry-options-> %2)) - :cron-schedule #(.setCronSchedule ^WorkflowOptions$Builder %1 %2) - :memo #(.setMemo ^WorkflowOptions$Builder %1 %2) - :search-attributes #(.setSearchAttributes ^WorkflowOptions$Builder %1 %2)}) - -(defn ^:no-doc wf-options-> - ^WorkflowOptions [params] - (u/build (WorkflowOptions/newBuilder (WorkflowOptions/getDefaultInstance)) wf-option-spec params)) + (let [service (g/service-stub-> options timeout)] + (WorkflowClient/newInstance service (copts/client-options-> options))))) (defn create-workflow " @@ -147,7 +78,7 @@ Create a new workflow-stub instance, suitable for managing and interacting with {:client client :stub stub})) ([^WorkflowClient client workflow options] (let [wf-name (w/get-annotated-name workflow) - options (wf-options-> options) + options (w/wf-options-> options) stub (.newUntypedWorkflowStub client wf-name options)] (log/trace "create-workflow:" wf-name options) {:client client :stub stub}))) @@ -240,4 +171,4 @@ Forcefully terminates 'workflow' ``` " [{:keys [^WorkflowStub stub] :as workflow} reason params] - (.terminate stub reason (u/->objarray params))) \ No newline at end of file + (.terminate stub reason (u/->objarray params))) diff --git a/src/temporal/client/options.clj b/src/temporal/client/options.clj new file mode 100644 index 0000000..9e85190 --- /dev/null +++ b/src/temporal/client/options.clj @@ -0,0 +1,24 @@ +(ns temporal.client.options + (:require [temporal.internal.utils :as u]) + (:import [io.temporal.client WorkflowClientOptions WorkflowClientOptions$Builder] + [io.temporal.common.interceptors WorkflowClientInterceptorBase] + [io.temporal.client.schedules ScheduleClientOptions ScheduleClientOptions$Builder])) + +(def ^:no-doc client-options + {:identity #(.setIdentity ^WorkflowClientOptions$Builder %1 %2) + :namespace #(.setNamespace ^WorkflowClientOptions$Builder %1 %2) + :data-converter #(.setDataConverter ^WorkflowClientOptions$Builder %1 %2) + :interceptors #(.setInterceptors ^WorkflowClientOptions$Builder %1 (into-array WorkflowClientInterceptorBase %2))}) + +(defn ^:no-doc client-options-> + ^WorkflowClientOptions [params] + (u/build (WorkflowClientOptions/newBuilder (WorkflowClientOptions/getDefaultInstance)) client-options params)) + +(def ^:no-doc schedule-client-options + {:identity #(.setIdentity ^ScheduleClientOptions$Builder %1 %2) + :namespace #(.setNamespace ^ScheduleClientOptions$Builder %1 %2) + :data-converter #(.setDataConverter ^ScheduleClientOptions$Builder %1 %2)}) + +(defn ^:no-doc schedule-client-options-> + ^ScheduleClientOptions [params] + (u/build (ScheduleClientOptions/newBuilder (ScheduleClientOptions/getDefaultInstance)) schedule-client-options params)) diff --git a/src/temporal/client/schedule.clj b/src/temporal/client/schedule.clj new file mode 100644 index 0000000..09df95c --- /dev/null +++ b/src/temporal/client/schedule.clj @@ -0,0 +1,176 @@ +(ns temporal.client.schedule + (:require [taoensso.timbre :as log] + [temporal.client.options :as copts] + [temporal.internal.grpc :as g] + [temporal.internal.utils :as u] + [temporal.internal.schedule :as s]) + (:import [java.time Duration] + [io.temporal.client.schedules ScheduleClient ScheduleUpdate ScheduleUpdateInput])) + +(set! *warn-on-reflection* true) + +(defn create-client + "Creates a `ScheduleClient` instance suitable for interacting with Temporal's Schedules. + The `ScheduleClient` has slightly less options then the `WorkflowClient` but it is mostly the same otherwise. + + Arguments: + + - `options`: Client configuration option map (See below) + - `timeout`: Connection timeout as a [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) (default: 5s) + + #### options map + + + | Value | Description | Type | Default | + | ------------------------- | --------------------------------------------------------------------------- | ------------ | ------- | + | :target | Sets the connection host:port | String | \"127.0.0.1:7233\" | + | :identity | Overrides the worker node identity (workers only) | String | | + | :namespace | Sets the Temporal namespace context for this client | String | | + | :data-converter | Overrides the data converter used to serialize arguments and results. | [DataConverter](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/converter/DataConverter.html) | | + | :channel | Sets gRPC channel to use. Exclusive with target and sslContext | [ManagedChannel](https://grpc.github.io/grpc-java/javadoc/io/grpc/ManagedChannel.html) | | + | :ssl-context | Sets gRPC SSL Context to use (See [[temporal.tls/new-ssl-context]]) | [SslContext](https://netty.io/4.0/api/io/netty/handler/ssl/SslContext.html) | | + | :enable-https | Sets option to enable SSL/TLS/HTTPS for gRPC | boolean | false | + | :rpc-timeout | Sets the rpc timeout value for non query and non long poll calls | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | + | :rpc-long-poll-timeout | Sets the rpc timeout value | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | + | :rpc-query-timeout | Sets the rpc timeout for queries | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | + | :backoff-reset-freq | Sets frequency at which gRPC connection backoff should be reset practically | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | + | :grpc-reconnect-freq | Sets frequency at which gRPC channel will be moved into an idle state | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | + | :headers | Set the headers | [Metadata](https://grpc.github.io/grpc-java/javadoc/io/grpc/Metadata.html) | | + | :enable-keepalive | Set keep alive ping from client to the server | boolean | false | + | :keepalive-time | Set the keep alive time | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | + | :keepalive-timeout | Set the keep alive timeout | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | + | :keepalive-without-stream | Set if client sends keepalive pings even with no active RPCs | boolean | false | + | :metrics-scope | The scope to be used for metrics reporting | [Scope](https://github.com/uber-java/tally/blob/master/core/src/main/java/com/uber/m3/tally/Scope.java) | |" + ([options] (create-client options (Duration/ofSeconds 5))) + ([options timeout] + (let [service (g/service-stub-> options timeout)] + (ScheduleClient/newInstance service (copts/schedule-client-options-> options))))) + +(defn schedule + "Creates a `Schedule` with Temporal + + Arguments: + + - `client`: [ScheduleClient]https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/client/schedules/ScheduleClient.html + - `schedule-id`: The string name of the schedule in Temporal, keeping it consistent with workflow id is a good idea + - `options`: A map containing the `:schedule`, `:state`, `:policy`, `:spec`, and `:action` option maps for the `Schedule` + + ```clojure + (defworkflow my-workflow + [ctx args] + ...) + + (let [client (create-client {:target \"localhost:8080\"})] + (create + client + \"my-workflow\" + {:schedule {:trigger-immediately? false} + :state {:paused? false} + :policy {:pause-on-failure? true} + :spec {:crons [\"0 * * * *\"]} + :action {:workflow-type my-workflow + :arguments {:value 1} + :options {:workflow-id \"my-workflow\"}}})) + ```" + [^ScheduleClient client schedule-id options] + (let [schedule (s/schedule-> options) + schedule-options (s/schedule-options-> options)] + (log/tracef "create schedule:" schedule-id) + (.createSchedule client schedule-id schedule schedule-options))) + +(defn unschedule + "Deletes a Temporal `Schedule` via a schedule-id + + ```clojure + + (let [client (create-client {:target \"localhost:8080\"})] + (unschedule client \"my-schedule\") + ```" + [^ScheduleClient client schedule-id] + (log/tracef "remove schedule:" schedule-id) + (-> client + (.getHandle schedule-id) + (.delete))) + +(defn describe + "Describes an existing Temporal `Schedule` via a schedule-id + + ```clojure + + (let [client (create-client {:target \"localhost:8080\"})] + (describe client \"my-schedule\") + ```" + [^ScheduleClient client schedule-id] + (-> client + (.getHandle schedule-id) + (.describe))) + +(defn pause + "Pauses an existing Temporal `Schedule` via a schedule-id + + ```clojure + + (let [client (create-client {:target \"localhost:8080\"})] + (pause client \"my-schedule\") + ```" + [^ScheduleClient client schedule-id] + (log/tracef "pausing schedule:" schedule-id) + (-> client + (.getHandle schedule-id) + (.pause))) + +(defn unpause + "Unpauses an existing Temporal `Schedule` via a schedule-id + + ```clojure + + (let [client (create-client {:target \"localhost:8080\"})] + (unpause client \"my-schedule\") + ```" + [^ScheduleClient client schedule-id] + (log/tracef "unpausing schedule:" schedule-id) + (-> client + (.getHandle schedule-id) + (.unpause))) + +(defn execute + "Runs a Temporal `Schedule's` workflow execution immediately via a schedule-id + + ```clojure + + (let [client (create-client {:target \"localhost:8080\"})] + (execute client \"my-schedule\" :skip) + ```" + [^ScheduleClient client schedule-id overlap-policy] + (log/tracef "execute schedule:" schedule-id) + (-> client + (.getHandle schedule-id) + (.trigger (s/overlap-policy-> overlap-policy)))) + +(defn reschedule + "Updates the current Temporal `Schedule` via a schedule-id. + Uses the same options as create asside from `:schedule` + + The `ScheduleHandle` takes a unary function object + of the signature: + + (ScheduleUpdateInput) -> ScheduleUpdate + + ```clojure + + (let [client (create-client {:target \"localhost:8080\"})] + (reschedule client \"my-schedule\" {:spec {:crons [\"1 * * * *\"]}}) + ```" + [^ScheduleClient client schedule-id options] + (log/tracef "update schedule:" schedule-id) + (letfn [(update-fn + [opts ^ScheduleUpdateInput input] + (let [schedule (-> input + (.getDescription) + (.getSchedule))] + (-> schedule + (s/schedule-> opts) + (ScheduleUpdate.))))] + (-> client + (.getHandle schedule-id) + (.update (u/->Func (partial update-fn options)))))) diff --git a/src/temporal/internal/grpc.clj b/src/temporal/internal/grpc.clj new file mode 100644 index 0000000..cb82637 --- /dev/null +++ b/src/temporal/internal/grpc.clj @@ -0,0 +1,28 @@ +(ns ^:no-doc temporal.internal.grpc + (:require [temporal.internal.utils :as u]) + (:import [io.temporal.serviceclient WorkflowServiceStubs WorkflowServiceStubsOptions WorkflowServiceStubsOptions$Builder])) + +(def stub-options + {:channel #(.setChannel ^WorkflowServiceStubsOptions$Builder %1 %2) + :ssl-context #(.setSslContext ^WorkflowServiceStubsOptions$Builder %1 %2) + :enable-https #(.setEnableHttps ^WorkflowServiceStubsOptions$Builder %1 %2) + :target #(.setTarget ^WorkflowServiceStubsOptions$Builder %1 %2) + :rpc-timeout #(.setRpcTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) + :rpc-long-poll-timeout #(.setRpcLongPollTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) + :rpc-query-timeout #(.setRpcQueryTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) + :backoff-reset-freq #(.setConnectionBackoffResetFrequency ^WorkflowServiceStubsOptions$Builder %1 %2) + :grpc-reconnect-freq #(.setGrpcReconnectFrequency ^WorkflowServiceStubsOptions$Builder %1 %2) + :headers #(.setHeaders ^WorkflowServiceStubsOptions$Builder %1 %2) + :enable-keepalive #(.setEnableKeepAlive ^WorkflowServiceStubsOptions$Builder %1 %2) + :keepalive-time #(.setKeepAliveTime ^WorkflowServiceStubsOptions$Builder %1 %2) + :keepalive-timeout #(.setKeepAliveTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) + :keepalive-without-stream #(.setKeepAlivePermitWithoutStream ^WorkflowServiceStubsOptions$Builder %1 %2) + :metrics-scope #(.setMetricsScope ^WorkflowServiceStubsOptions$Builder %1 %2)}) + +(defn stub-options-> + ^WorkflowServiceStubsOptions [params] + (u/build (WorkflowServiceStubsOptions/newBuilder) stub-options params)) + +(defn service-stub-> + [options timeout] + (WorkflowServiceStubs/newConnectedServiceStubs (stub-options-> options) timeout)) diff --git a/src/temporal/internal/schedule.clj b/src/temporal/internal/schedule.clj new file mode 100644 index 0000000..4e97496 --- /dev/null +++ b/src/temporal/internal/schedule.clj @@ -0,0 +1,99 @@ +(ns ^:no-doc temporal.internal.schedule + (:require [clojure.walk :refer [stringify-keys]] + [temporal.internal.utils :as u] + [temporal.internal.workflow :as w]) + (:import [io.temporal.api.enums.v1 ScheduleOverlapPolicy] + [io.temporal.client.schedules + Schedule + Schedule$Builder + ScheduleActionStartWorkflow + ScheduleActionStartWorkflow$Builder + ScheduleOptions + ScheduleOptions$Builder + SchedulePolicy + SchedulePolicy$Builder + ScheduleSpec + ScheduleSpec$Builder + ScheduleState + ScheduleState$Builder])) + +(set! *warn-on-reflection* true) + +(defn overlap-policy-> + [overlap-policy] + (case overlap-policy + :allow ScheduleOverlapPolicy/SCHEDULE_OVERLAP_POLICY_ALLOW_ALL + :buffer ScheduleOverlapPolicy/SCHEDULE_OVERLAP_POLICY_BUFFER_ALL + :buffer-one ScheduleOverlapPolicy/SCHEDULE_OVERLAP_POLICY_BUFFER_ONE + :cancel ScheduleOverlapPolicy/SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER + :skip ScheduleOverlapPolicy/SCHEDULE_OVERLAP_POLICY_SKIP + :terminate ScheduleOverlapPolicy/SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER)) + +(def schedule-policy-spec + {:catchup-window #(.setCatchupWindow ^SchedulePolicy$Builder %1 %2) + :overlap (fn [^SchedulePolicy$Builder builder value] + (.setOverlap builder (overlap-policy-> value))) + :pause-on-failure? #(.setPauseOnFailure ^SchedulePolicy$Builder %1 %2)}) + +(defn schedule-policy-> + ^SchedulePolicy [params] + (u/build (SchedulePolicy/newBuilder) schedule-policy-spec params)) + +(def schedule-spec-spec + {:calendars #(.setCalendars​ ^ScheduleSpec$Builder %1 %2) + :cron-expressions #(.setCronExpressions ^ScheduleSpec$Builder %1 %2) + :intervals #(.setIntervals​ ^ScheduleSpec$Builder %1 %2) + :end-at #(.setEndAt ^ScheduleSpec$Builder %1 %2) + :jitter #(.setJitter ^ScheduleSpec$Builder %1 %2) + :skip-at #(.setSkip ^ScheduleSpec$Builder %1 %2) + :start-at #(.setStartAt ^ScheduleSpec$Builder %1 %2) + :timezone #(.setTimeZoneName ^ScheduleSpec$Builder %1 %2)}) + +(defn schedule-spec-> + ^ScheduleSpec [params] + (u/build (ScheduleSpec/newBuilder) schedule-spec-spec params)) + +(def schedule-action-start-workflow-spec-> + {:options #(.setOptions ^ScheduleActionStartWorkflow$Builder %1 (w/wf-options-> %2)) + :arguments (fn [^ScheduleActionStartWorkflow$Builder builder value] + (.setArguments builder (to-array [(stringify-keys value)]))) + :workflow-type (fn [^ScheduleActionStartWorkflow$Builder builder value] + (if (string? value) + (.setWorkflowType builder ^String value) + (.setWorkflowType builder (w/get-annotated-name value))))}) + +(defn schedule-action-start-workflow-> + ^ScheduleActionStartWorkflow [params] + (u/build + (ScheduleActionStartWorkflow/newBuilder) + schedule-action-start-workflow-spec-> params)) + +(def schedule-state-spec + {:limited-action? #(.setLimitedAction ^ScheduleState$Builder %1 %2) + :note #(.setNote ^ScheduleState$Builder %1 %2) + :paused? #(.setPaused ^ScheduleState$Builder %1 %2) + :remaining-actions #(.setRemainingActions ^ScheduleState$Builder %1 %2)}) + +(defn schedule-state-> + ^ScheduleState [params] + (u/build (ScheduleState/newBuilder) schedule-state-spec params)) + +(def schedule-options-spec + {:memo #(.setMemo ^ScheduleOptions$Builder %1 %2) + :trigger-immediately? #(.setTriggerImmediately ^ScheduleOptions$Builder %1 %2)}) + +(defn schedule-options-> + ^ScheduleOptions [params] + (u/build (ScheduleOptions/newBuilder) schedule-options-spec params)) + +(def schedule-spec + {:action #(.setAction ^Schedule$Builder %1 (schedule-action-start-workflow-> %2)) + :policy #(.setPolicy ^Schedule$Builder %1 (schedule-policy-> %2)) + :spec #(.setSpec ^Schedule$Builder %1 (schedule-spec-> %2)) + :state #(.setState ^Schedule$Builder %1 (schedule-state-> %2))}) + +(defn schedule-> + (^Schedule [params] + (u/build (Schedule/newBuilder) schedule-spec params)) + (^Schedule [schedule params] + (u/build (Schedule/newBuilder schedule) schedule-spec params))) diff --git a/src/temporal/internal/workflow.clj b/src/temporal/internal/workflow.clj index 7eebbb2..909ebe1 100644 --- a/src/temporal/internal/workflow.clj +++ b/src/temporal/internal/workflow.clj @@ -4,12 +4,15 @@ (:require [clojure.core.protocols :as p] [clojure.datafy :as d] [slingshot.slingshot :refer [try+]] - [taoensso.timbre :as log] [taoensso.nippy :as nippy] - [temporal.internal.utils :as u] + [taoensso.timbre :as log] + [temporal.common :as common] + [temporal.internal.exceptions :as e] [temporal.internal.signals :as s] - [temporal.internal.exceptions :as e]) - (:import [io.temporal.workflow Workflow WorkflowInfo])) + [temporal.internal.utils :as u]) + (:import [io.temporal.api.enums.v1 WorkflowIdReusePolicy] + [io.temporal.client WorkflowOptions WorkflowOptions$Builder] + [io.temporal.workflow Workflow WorkflowInfo])) (extend-protocol p/Datafiable WorkflowInfo @@ -20,6 +23,42 @@ :workflow-type (.getWorkflowType d) :attempt (.getAttempt d)})) + +(def workflow-id-reuse-options + " +| Value | Description | +| ---------------------------- | --------------------------------------------------------------------------- | +| :allow-duplicate | Allow starting a workflow execution using the same workflow id. | +| :allow-duplicate-failed-only | Allow starting a workflow execution using the same workflow id, only when the last execution's final state is one of [terminated, cancelled, timed out, failed] | +| :reject-duplicate | Do not permit re-use of the workflow id for this workflow. | +| :terminate-if-running | If a workflow is running using the same workflow ID, terminate it and start a new one. If no running workflow, then the behavior is the same as ALLOW_DUPLICATE| +" + {:allow-duplicate WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE + :allow-duplicate-failed-only WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY + :reject-duplicate WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE + :terminate-if-running WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING}) + +(defn ^:no-doc workflow-id-reuse-policy-> + ^WorkflowIdReusePolicy [policy] + (or (get workflow-id-reuse-options policy) + (throw (IllegalArgumentException. (str "Unknown workflow-id-reuse-policy: " policy " Must be one of " (keys workflow-id-reuse-options)))))) + +(def ^:no-doc wf-option-spec + {:task-queue #(.setTaskQueue ^WorkflowOptions$Builder %1 (u/namify %2)) + :workflow-id #(.setWorkflowId ^WorkflowOptions$Builder %1 (u/namify %2)) + :workflow-id-reuse-policy #(.setWorkflowIdReusePolicy ^WorkflowOptions$Builder %1 (workflow-id-reuse-policy-> %2)) + :workflow-execution-timeout #(.setWorkflowExecutionTimeout ^WorkflowOptions$Builder %1 %2) + :workflow-run-timeout #(.setWorkflowRunTimeout ^WorkflowOptions$Builder %1 %2) + :workflow-task-timeout #(.setWorkflowTaskTimeout ^WorkflowOptions$Builder %1 %2) + :retry-options #(.setRetryOptions %1 (common/retry-options-> %2)) + :cron-schedule #(.setCronSchedule ^WorkflowOptions$Builder %1 %2) + :memo #(.setMemo ^WorkflowOptions$Builder %1 %2) + :search-attributes #(.setSearchAttributes ^WorkflowOptions$Builder %1 %2)}) + +(defn ^:no-doc wf-options-> + ^WorkflowOptions [params] + (u/build (WorkflowOptions/newBuilder (WorkflowOptions/getDefaultInstance)) wf-option-spec params)) + (defn get-info [] (d/datafy (Workflow/getInfo))) diff --git a/src/temporal/testing/env.clj b/src/temporal/testing/env.clj index cc08acd..993e623 100644 --- a/src/temporal/testing/env.clj +++ b/src/temporal/testing/env.clj @@ -3,14 +3,15 @@ (ns temporal.testing.env "Methods and utilities to assist with unit-testing Temporal workflows" (:require [temporal.client.worker :as worker] - [temporal.client.core :as client] + [temporal.client.options :as copts] + [temporal.internal.grpc :as g] [temporal.internal.utils :as u]) (:import [io.temporal.testing TestWorkflowEnvironment TestEnvironmentOptions TestEnvironmentOptions$Builder])) (def ^:no-doc test-env-options {:worker-factory-options #(.setWorkerFactoryOptions ^TestEnvironmentOptions$Builder %1 (worker/worker-factory-options-> %2)) - :workflow-client-options #(.setWorkflowClientOptions ^TestEnvironmentOptions$Builder %1 (client/client-options-> %2)) - :workflow-service-stub-options #(.setWorkflowServiceStubsOptions ^TestEnvironmentOptions$Builder %1 (client/stub-options-> %2)) + :workflow-client-options #(.setWorkflowClientOptions ^TestEnvironmentOptions$Builder %1 (copts/client-options-> %2)) + :workflow-service-stub-options #(.setWorkflowServiceStubsOptions ^TestEnvironmentOptions$Builder %1 (g/stub-options-> %2)) :metrics-scope #(.setMetricsScope ^TestEnvironmentOptions$Builder %1 %2)}) (defn ^:no-doc test-env-options-> @@ -32,8 +33,8 @@ Arguments: | Value | Description | Type | Default | | ------------------------- | --------------------------------------------- | ------------ | ------- | | :worker-factory-options | | [[worker/worker-factory-options]] | | -| :workflow-client-options | | [[client/client-options]] | | -| :workflow-service-stub-options | | [[client/stub-options]] | | +| :workflow-client-options | | [[copts/client-options]] | | +| :workflow-service-stub-options | | [[grpc/stub-options]] | | | :metrics-scope | The scope to be used for metrics reporting | [Scope](https://github.com/uber-java/tally/blob/master/core/src/main/java/com/uber/m3/tally/Scope.java) | | diff --git a/test/temporal/test/schedule.clj b/test/temporal/test/schedule.clj new file mode 100644 index 0000000..0200963 --- /dev/null +++ b/test/temporal/test/schedule.clj @@ -0,0 +1,146 @@ +(ns temporal.test.schedule + (:require [clojure.test :refer :all] + [temporal.client.schedule :as schedule] + [temporal.internal.schedule :as s] + [temporal.internal.workflow :as w] + [temporal.test.utils :as t] + [temporal.workflow :refer [defworkflow]]) + (:import [io.temporal.client.schedules ScheduleClient ScheduleHandle ScheduleUpdateInput ScheduleDescription] + [java.time Duration Instant])) + +(def workflow-id "simple-workflow") +(def schedule-id "simple-workflow-schedule") +(defworkflow simple-workflow [ctx args] args) + +(defn create-mocked-schedule-handle + [state] + (reify ScheduleHandle + (update [_ update-fn] + (swap! state update :update assoc :update-fn update-fn)) + (delete [_] + (swap! state update :delete (fnil inc 0))) + (describe [_] + (swap! state update :describe (fnil inc 0)) + nil) + (pause [_] + (swap! state update :pause (fnil inc 0))) + (unpause [_] + (swap! state update :unpause (fnil inc 0))) + (trigger [_ overlap-policy] + (swap! state update :trigger assoc :overlap-policy overlap-policy)))) + +(defn create-mocked-schedule-client + [state] + (reify ScheduleClient + (createSchedule [_ schedule-id schedule schedule-options] + (swap! state update :create assoc + :schedule-id schedule-id + :schedule schedule + :schedule-options schedule-options) + (create-mocked-schedule-handle state)) + (getHandle [_ schedule-id] + (swap! state update :handle assoc :schedule-id schedule-id) + (create-mocked-schedule-handle state)))) + +(defn- stub-schedule-options + [& {:keys [action spec policy state schedule]}] + {:action (merge {:arguments {:name "John Doe" :age 32} + :options {:workflow-id workflow-id + :task-queue t/task-queue} + :workflow-type simple-workflow} + action) + :spec (merge {:cron-expressions ["0 * * * * "] + :end-at (Instant/now) + :jitter (Duration/ofSeconds 1) + :start-at (Instant/now) + :timezone "US/Central"} + spec) + :policy (merge {:pause-on-failure? true + :catchup-window (Duration/ofSeconds 1) + :overlap :skip} + policy) + :state (merge {:paused? true + :note "note" + :limited-action? false} + state) + :schedule (merge {:trigger-immediately? true + :memo "memo"} + schedule)}) + +;; NOTE: Temporal Schedules are not supported in the Temporal test environment +;; hence the mocked ScheduleClient stubs for now + +(deftest schedule-workflow-test + (testing "scheduling a workflow is successful" + (let [state (atom {}) + client (create-mocked-schedule-client state)] + (is (some? (schedule/schedule client schedule-id (stub-schedule-options)))) + (is (= (get-in @state [:create :schedule-id]) schedule-id)) + (is (= (-> (get-in @state [:create :schedule]) .getAction .getWorkflowType) (w/get-annotated-name simple-workflow))) + (is (= (-> (get-in @state [:create :schedule]) .getAction .getWorkflowType) "simple-workflow")) + (is (= (-> (get-in @state [:create :schedule]) .getAction .getOptions .getWorkflowId) workflow-id)) + (is (= (-> (get-in @state [:create :schedule]) .getSpec .getCronExpressions) ["0 * * * * "])) + (is (-> (get-in @state [:create :schedule]) .getPolicy .isPauseOnFailure)) + (is (= (-> (get-in @state [:create :schedule]) .getState .getNote) "note")) + (is (-> (get-in @state [:create :schedule]) .getState .isPaused))))) + +(deftest unschedule-scheduled-workflow-test + (testing "unscheduling a scheduled workflow is successful" + (let [state (atom {}) + client (create-mocked-schedule-client state)] + (schedule/unschedule client schedule-id) + (is (= (:delete @state) 1))))) + +(deftest describe-scheduled-workflow-test + (testing "describing a scheduled workflow is successful" + (let [state (atom {}) + client (create-mocked-schedule-client state)] + (schedule/describe client schedule-id) + (is (= (:describe @state) 1))))) + +(deftest pause-scheduled-workflow-test + (testing "pauses a scheduled workflow is successful" + (let [state (atom {}) + client (create-mocked-schedule-client state)] + (schedule/pause client schedule-id) + (is (= (:pause @state) 1))))) + +(deftest unpause-scheduled-workflow-test + (testing "unpauses a scheduled workflow is successful" + (let [state (atom {}) + client (create-mocked-schedule-client state)] + (schedule/unpause client schedule-id) + (is (= (:unpause @state) 1))))) + +(deftest execute-scheduled-workflow-test + (testing "executes a scheduled workflow is successful" + (let [state (atom {}) + client (create-mocked-schedule-client state)] + (schedule/execute client schedule-id :skip) + (is (= (get-in @state [:trigger :overlap-policy]) + (s/overlap-policy-> :skip)))))) + +(deftest reschedule-scheduled-workflow-test + (testing "reschedules/updates a scheduled workflow is successful" + (let [state (atom {}) + client (create-mocked-schedule-client state) + update-options (stub-schedule-options :spec {:cron-expressions ["1 * * * *"]}) + schedule-update-input (ScheduleUpdateInput. + (ScheduleDescription. + schedule-id + nil + (s/schedule-> (stub-schedule-options)) + nil + nil + nil + nil))] + (schedule/reschedule client schedule-id update-options) + ;; validate the actual update function works + (is (= (-> (get-in @state [:update :update-fn]) + (.apply schedule-update-input) + (.getSchedule) + (.getSpec) + (.getCronExpressions)) + (-> (s/schedule-> update-options) + (.getSpec) + (.getCronExpressions))))))) diff --git a/test/temporal/test/types.clj b/test/temporal/test/types.clj index aa1b15a..c28945b 100644 --- a/test/temporal/test/types.clj +++ b/test/temporal/test/types.clj @@ -2,54 +2,55 @@ (ns temporal.test.types (:require [clojure.test :refer :all] - [temporal.client.core :as client] - [temporal.client.worker :as worker]) - (:import [java.time Duration] + [temporal.client.worker :as worker] + [temporal.client.options :as o] + [temporal.internal.workflow :as w] + [temporal.internal.schedule :as s] + [temporal.internal.grpc :as g]) + (:import [java.time Duration Instant] [io.grpc Grpc InsecureChannelCredentials Metadata] [io.grpc.netty.shaded.io.grpc.netty GrpcSslContexts])) (deftest workflow-options (testing "Verify that our workflow options work" - (let [x (client/wf-options-> {:workflow-id "foo" - :task-queue "bar" - :workflow-execution-timeout (Duration/ofSeconds 1) - :workflow-run-timeout (Duration/ofSeconds 1) - :workflow-task-timeout (Duration/ofSeconds 1) - :retry-options {:maximum-attempts 1} - :cron-schedule "* * * * *" - :memo {"foo" "bar"} - :search-attributes {"foo" "bar"}})] + (let [x (w/wf-options-> {:workflow-id "foo" + :task-queue "bar" + :workflow-execution-timeout (Duration/ofSeconds 1) + :workflow-run-timeout (Duration/ofSeconds 1) + :workflow-task-timeout (Duration/ofSeconds 1) + :retry-options {:maximum-attempts 1} + :cron-schedule "* * * * *" + :memo {"foo" "bar"} + :search-attributes {"foo" "bar"}})] (is (-> x (.getWorkflowId) (= "foo"))) (is (-> x (.getTaskQueue) (= "bar")))))) (deftest client-options (testing "Verify that our stub options work" - (let [x (client/stub-options-> {:channel (-> (Grpc/newChannelBuilder "foo:1234" (InsecureChannelCredentials/create)) - (.build)) - :ssl-context (-> (GrpcSslContexts/forClient) - (.build)) - :target "foo:1234" - :enable-https false - :rpc-timeout (Duration/ofSeconds 1) - :rpc-long-poll-timeout (Duration/ofSeconds 1) - :rpc-query-timeout (Duration/ofSeconds 1) - :backoff-reset-freq (Duration/ofSeconds 1) - :grpc-reconnect-freq (Duration/ofSeconds 1) - :headers (Metadata.) - :enable-keepalive true - :keepalive-time (Duration/ofSeconds 1) - :keepalive-timeout (Duration/ofSeconds 1) - :keepalive-without-stream true})] + (let [x (g/stub-options-> {:channel (-> (Grpc/newChannelBuilder "foo:1234" (InsecureChannelCredentials/create)) (.build)) + :ssl-context (-> (GrpcSslContexts/forClient) (.build)) + :target "foo:1234" + :enable-https false + :rpc-timeout (Duration/ofSeconds 1) + :rpc-long-poll-timeout (Duration/ofSeconds 1) + :rpc-query-timeout (Duration/ofSeconds 1) + :backoff-reset-freq (Duration/ofSeconds 1) + :grpc-reconnect-freq (Duration/ofSeconds 1) + :headers (Metadata.) + :enable-keepalive true + :keepalive-time (Duration/ofSeconds 1) + :keepalive-timeout (Duration/ofSeconds 1) + :keepalive-without-stream true})] (is (-> x (.getTarget) (= "foo:1234"))))) (testing "Verify that our client options work" - (let [x (client/client-options-> {:identity "test" + (let [x (o/client-options-> {:identity "test" :namespace "test"})] (is (-> x (.getIdentity) (= "test"))) (is (-> x (.getNamespace) (= "test"))))) (testing "Verify that mixed client/stub options work" (let [options {:target "foo:1234" :namespace "default"}] - (is (some? (client/stub-options-> options))) - (is (some? (client/client-options-> options)))))) + (is (some? (g/stub-options-> options))) + (is (some? (o/client-options-> options)))))) (deftest worker-options (testing "Verify that our worker-options work" @@ -65,3 +66,63 @@ :max-taskqueue-activities-per-second 1.0 :max-workers-activities-per-second 1.0})] (is (-> x (.isLocalActivityWorkerOnly) false?))))) + +(deftest schedule-client-options + (testing "Verify that our stub options work" + (let [x (g/stub-options-> {:channel (-> (Grpc/newChannelBuilder "foo:1234" (InsecureChannelCredentials/create)) (.build)) + :ssl-context (-> (GrpcSslContexts/forClient) (.build)) + :target "foo:1234" + :enable-https false + :rpc-timeout (Duration/ofSeconds 1) + :rpc-long-poll-timeout (Duration/ofSeconds 1) + :rpc-query-timeout (Duration/ofSeconds 1) + :backoff-reset-freq (Duration/ofSeconds 1) + :grpc-reconnect-freq (Duration/ofSeconds 1) + :headers (Metadata.) + :enable-keepalive true + :keepalive-time (Duration/ofSeconds 1) + :keepalive-timeout (Duration/ofSeconds 1) + :keepalive-without-stream true})] + (is (-> x (.getTarget) (= "foo:1234"))))) + (testing "Verify that our client options work" + (let [x (o/schedule-client-options-> {:identity "test" + :namespace "test"})] + (is (-> x (.getIdentity) (= "test"))) + (is (-> x (.getNamespace) (= "test"))))) + (testing "Verify that mixed client/stub options work" + (let [options {:target "foo:1234" :namespace "default"}] + (is (some? (g/stub-options-> options))) + (is (some? (o/schedule-client-options-> options)))))) + +(deftest schedule-options + (testing "Verify that a schedule can be constructed with the action, spec, policy, and state" + (let [action {:arguments {:value 1} + :options {:workflow-id "my-workflow-execution" + :task-queue "queue"} + :workflow-type "my-workflow"} + spec {:cron-expressions ["0 * * * * "] + :end-at (Instant/now) + :jitter (Duration/ofSeconds 1) + :start-at (Instant/now) + :timezone "US/Central"} + policy {:pause-on-failure? true + :catchup-window (Duration/ofSeconds 1) + :overlap :skip} + state {:paused? true + :note "note" + :limited-action? false} + schedule (s/schedule-> {:action action + :policy policy + :spec spec + :state state})] + (is (some? (s/schedule-action-start-workflow-> action))) + (is (some? (s/schedule-spec-> spec))) + (is (some? (s/schedule-policy-> policy))) + (is (some? (s/schedule-state-> state))) + (is (some? schedule)) + (is (= "my-workflow" (-> schedule .getAction .getWorkflowType))) + (is (= "my-workflow-execution" (-> schedule .getAction .getOptions .getWorkflowId))) + (is (= ["0 * * * * "] (-> schedule .getSpec .getCronExpressions))) + (is (-> schedule .getPolicy .isPauseOnFailure)) + (is (= "note" (-> schedule .getState .getNote))) + (is (-> schedule .getState .isPaused))))) From 4d02cccf60efdad85a83cb683f30d1cbf5c4107f Mon Sep 17 00:00:00 2001 From: Bailey Kocin Date: Mon, 11 Mar 2024 17:48:55 -0400 Subject: [PATCH 039/102] run lein cljfmt check Signed-off-by: Bailey Kocin --- src/temporal/client/options.clj | 2 +- src/temporal/client/schedule.clj | 14 +++++++------- src/temporal/internal/grpc.clj | 4 ++-- src/temporal/internal/workflow.clj | 1 - test/temporal/test/types.clj | 4 ++-- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/temporal/client/options.clj b/src/temporal/client/options.clj index 9e85190..7e2dcb1 100644 --- a/src/temporal/client/options.clj +++ b/src/temporal/client/options.clj @@ -2,7 +2,7 @@ (:require [temporal.internal.utils :as u]) (:import [io.temporal.client WorkflowClientOptions WorkflowClientOptions$Builder] [io.temporal.common.interceptors WorkflowClientInterceptorBase] - [io.temporal.client.schedules ScheduleClientOptions ScheduleClientOptions$Builder])) + [io.temporal.client.schedules ScheduleClientOptions ScheduleClientOptions$Builder])) (def ^:no-doc client-options {:identity #(.setIdentity ^WorkflowClientOptions$Builder %1 %2) diff --git a/src/temporal/client/schedule.clj b/src/temporal/client/schedule.clj index 09df95c..b2d4aae 100644 --- a/src/temporal/client/schedule.clj +++ b/src/temporal/client/schedule.clj @@ -1,11 +1,11 @@ (ns temporal.client.schedule - (:require [taoensso.timbre :as log] - [temporal.client.options :as copts] - [temporal.internal.grpc :as g] - [temporal.internal.utils :as u] - [temporal.internal.schedule :as s]) - (:import [java.time Duration] - [io.temporal.client.schedules ScheduleClient ScheduleUpdate ScheduleUpdateInput])) + (:require [taoensso.timbre :as log] + [temporal.client.options :as copts] + [temporal.internal.grpc :as g] + [temporal.internal.utils :as u] + [temporal.internal.schedule :as s]) + (:import [java.time Duration] + [io.temporal.client.schedules ScheduleClient ScheduleUpdate ScheduleUpdateInput])) (set! *warn-on-reflection* true) diff --git a/src/temporal/internal/grpc.clj b/src/temporal/internal/grpc.clj index cb82637..19001e2 100644 --- a/src/temporal/internal/grpc.clj +++ b/src/temporal/internal/grpc.clj @@ -1,6 +1,6 @@ (ns ^:no-doc temporal.internal.grpc - (:require [temporal.internal.utils :as u]) - (:import [io.temporal.serviceclient WorkflowServiceStubs WorkflowServiceStubsOptions WorkflowServiceStubsOptions$Builder])) + (:require [temporal.internal.utils :as u]) + (:import [io.temporal.serviceclient WorkflowServiceStubs WorkflowServiceStubsOptions WorkflowServiceStubsOptions$Builder])) (def stub-options {:channel #(.setChannel ^WorkflowServiceStubsOptions$Builder %1 %2) diff --git a/src/temporal/internal/workflow.clj b/src/temporal/internal/workflow.clj index 909ebe1..5ad515e 100644 --- a/src/temporal/internal/workflow.clj +++ b/src/temporal/internal/workflow.clj @@ -23,7 +23,6 @@ :workflow-type (.getWorkflowType d) :attempt (.getAttempt d)})) - (def workflow-id-reuse-options " | Value | Description | diff --git a/test/temporal/test/types.clj b/test/temporal/test/types.clj index c28945b..1f977fc 100644 --- a/test/temporal/test/types.clj +++ b/test/temporal/test/types.clj @@ -44,7 +44,7 @@ (is (-> x (.getTarget) (= "foo:1234"))))) (testing "Verify that our client options work" (let [x (o/client-options-> {:identity "test" - :namespace "test"})] + :namespace "test"})] (is (-> x (.getIdentity) (= "test"))) (is (-> x (.getNamespace) (= "test"))))) (testing "Verify that mixed client/stub options work" @@ -86,7 +86,7 @@ (is (-> x (.getTarget) (= "foo:1234"))))) (testing "Verify that our client options work" (let [x (o/schedule-client-options-> {:identity "test" - :namespace "test"})] + :namespace "test"})] (is (-> x (.getIdentity) (= "test"))) (is (-> x (.getNamespace) (= "test"))))) (testing "Verify that mixed client/stub options work" From eb7cc43c99735f90a03e20197aeba5d679cea70b Mon Sep 17 00:00:00 2001 From: Bailey Kocin Date: Wed, 13 Mar 2024 12:16:19 -0400 Subject: [PATCH 040/102] centeralize options Signed-off-by: Bailey Kocin --- src/temporal/client/core.clj | 32 ++----------- src/temporal/client/options.clj | 79 ++++++++++++++++++++++++++++++-- src/temporal/client/schedule.clj | 30 ++---------- src/temporal/internal/grpc.clj | 28 ----------- src/temporal/testing/env.clj | 7 ++- test/temporal/test/types.clj | 15 +++--- 6 files changed, 90 insertions(+), 101 deletions(-) delete mode 100644 src/temporal/internal/grpc.clj diff --git a/src/temporal/client/core.clj b/src/temporal/client/core.clj index ef798f8..8e16584 100644 --- a/src/temporal/client/core.clj +++ b/src/temporal/client/core.clj @@ -7,7 +7,6 @@ [promesa.core :as p] [temporal.client.options :as copts] [temporal.internal.workflow :as w] - [temporal.internal.grpc :as g] [temporal.internal.utils :as u] [temporal.internal.exceptions :as e]) (:import [java.time Duration] @@ -20,41 +19,16 @@ workflow clients (See [[create-workflow]]). Arguments: -- `options`: Client configuration option map (See below) +- `options`: Options for configuring the `WorkflowClient` (See [[temporal.client.options/workflow-client-options]] and [[temporal.client.options/stub-options]]) - `timeout`: Connection timeout as a [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) (default: 5s) -#### options map - - -| Value | Description | Type | Default | -| ------------------------- | --------------------------------------------------------------------------- | ------------ | ------- | -| :target | Sets the connection host:port | String | \"127.0.0.1:7233\" | -| :identity | Overrides the worker node identity (workers only) | String | | -| :namespace | Sets the Temporal namespace context for this client | String | | -| :data-converter | Overrides the data converter used to serialize arguments and results. | [DataConverter](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/converter/DataConverter.html) | | -| :interceptors | Collection of interceptors used to intercept workflow client calls. | [WorkflowClientInterceptor](https://javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/interceptors/WorkflowClientInterceptor.html) | | -| :channel | Sets gRPC channel to use. Exclusive with target and sslContext | [ManagedChannel](https://grpc.github.io/grpc-java/javadoc/io/grpc/ManagedChannel.html) | | -| :ssl-context | Sets gRPC SSL Context to use (See [[temporal.tls/new-ssl-context]]) | [SslContext](https://netty.io/4.0/api/io/netty/handler/ssl/SslContext.html) | | -| :enable-https | Sets option to enable SSL/TLS/HTTPS for gRPC | boolean | false | -| :rpc-timeout | Sets the rpc timeout value for non query and non long poll calls | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | -| :rpc-long-poll-timeout | Sets the rpc timeout value | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | -| :rpc-query-timeout | Sets the rpc timeout for queries | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | -| :backoff-reset-freq | Sets frequency at which gRPC connection backoff should be reset practically | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | -| :grpc-reconnect-freq | Sets frequency at which gRPC channel will be moved into an idle state | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | -| :headers | Set the headers | [Metadata](https://grpc.github.io/grpc-java/javadoc/io/grpc/Metadata.html) | | -| :enable-keepalive | Set keep alive ping from client to the server | boolean | false | -| :keepalive-time | Set the keep alive time | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | -| :keepalive-timeout | Set the keep alive timeout | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | -| :keepalive-without-stream | Set if client sends keepalive pings even with no active RPCs | boolean | false | -| :metrics-scope | The scope to be used for metrics reporting | [Scope](https://github.com/uber-java/tally/blob/master/core/src/main/java/com/uber/m3/tally/Scope.java) | | - " ([] (create-client {})) ([options] (create-client options (Duration/ofSeconds 5))) ([options timeout] - (let [service (g/service-stub-> options timeout)] - (WorkflowClient/newInstance service (copts/client-options-> options))))) + (let [service (copts/service-stub-> options timeout)] + (WorkflowClient/newInstance service (copts/workflow-client-options-> options))))) (defn create-workflow " diff --git a/src/temporal/client/options.clj b/src/temporal/client/options.clj index 7e2dcb1..391245a 100644 --- a/src/temporal/client/options.clj +++ b/src/temporal/client/options.clj @@ -2,19 +2,42 @@ (:require [temporal.internal.utils :as u]) (:import [io.temporal.client WorkflowClientOptions WorkflowClientOptions$Builder] [io.temporal.common.interceptors WorkflowClientInterceptorBase] - [io.temporal.client.schedules ScheduleClientOptions ScheduleClientOptions$Builder])) + [io.temporal.client.schedules ScheduleClientOptions ScheduleClientOptions$Builder] + [io.temporal.serviceclient WorkflowServiceStubs WorkflowServiceStubsOptions WorkflowServiceStubsOptions$Builder])) -(def ^:no-doc client-options +(def workflow-client-options + " +`WorkflowClientOptions` configuration map (See [[temporal.client.core/create-client]]) + +| Value | Description | Type | Default | +| ------------------------- | --------------------------------------------------------------------------- | ------------ | ------- | +| :target | Sets the connection host:port | String | \"127.0.0.1:7233\" | +| :identity | Overrides the worker node identity (workers only) | String | | +| :namespace | Sets the Temporal namespace context for this client | String | | +| :data-converter | Overrides the data converter used to serialize arguments and results. | [DataConverter](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/converter/DataConverter.html) | | +| :interceptors | Collection of interceptors used to intercept workflow client calls. | [WorkflowClientInterceptor](https://javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/interceptors/WorkflowClientInterceptor.html) | | +" {:identity #(.setIdentity ^WorkflowClientOptions$Builder %1 %2) :namespace #(.setNamespace ^WorkflowClientOptions$Builder %1 %2) :data-converter #(.setDataConverter ^WorkflowClientOptions$Builder %1 %2) :interceptors #(.setInterceptors ^WorkflowClientOptions$Builder %1 (into-array WorkflowClientInterceptorBase %2))}) -(defn ^:no-doc client-options-> +(defn ^:no-doc workflow-client-options-> ^WorkflowClientOptions [params] - (u/build (WorkflowClientOptions/newBuilder (WorkflowClientOptions/getDefaultInstance)) client-options params)) + (u/build (WorkflowClientOptions/newBuilder (WorkflowClientOptions/getDefaultInstance)) workflow-client-options params)) + +(def schedule-client-options + " +`ScheduleClientOptions` configuration map (See [[temporal.client.schedule/create-client]]) -(def ^:no-doc schedule-client-options +| Value | Description | Type | Default | +| ------------------------- | --------------------------------------------------------------------------- | ------------ | ------- | +| :target | Sets the connection host:port | String | \"127.0.0.1:7233\" | +| :identity | Overrides the worker node identity (workers only) | String | | +| :namespace | Sets the Temporal namespace context for this client | String | | +| :data-converter | Overrides the data converter used to serialize arguments and results. | [DataConverter](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/converter/DataConverter.html) | | + +" {:identity #(.setIdentity ^ScheduleClientOptions$Builder %1 %2) :namespace #(.setNamespace ^ScheduleClientOptions$Builder %1 %2) :data-converter #(.setDataConverter ^ScheduleClientOptions$Builder %1 %2)}) @@ -22,3 +45,49 @@ (defn ^:no-doc schedule-client-options-> ^ScheduleClientOptions [params] (u/build (ScheduleClientOptions/newBuilder (ScheduleClientOptions/getDefaultInstance)) schedule-client-options params)) + +(def stub-options + " +`WorkflowServiceStubsOptions` configuration map (See [[temporal.client.core/create-client]] or [[temporal.client.schedule/create-client]]) + +| Value | Description | Type | Default | +| ------------------------- | --------------------------------------------------------------------------- | ------------ | ------- | +| :channel | Sets gRPC channel to use. Exclusive with target and sslContext | [ManagedChannel](https://grpc.github.io/grpc-java/javadoc/io/grpc/ManagedChannel.html) | | +| :ssl-context | Sets gRPC SSL Context to use (See [[temporal.tls/new-ssl-context]]) | [SslContext](https://netty.io/4.0/api/io/netty/handler/ssl/SslContext.html) | | +| :enable-https | Sets option to enable SSL/TLS/HTTPS for gRPC | boolean | false | +| :rpc-timeout | Sets the rpc timeout value for non query and non long poll calls | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | +| :rpc-long-poll-timeout | Sets the rpc timeout value | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | +| :rpc-query-timeout | Sets the rpc timeout for queries | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | +| :backoff-reset-freq | Sets frequency at which gRPC connection backoff should be reset practically | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | +| :grpc-reconnect-freq | Sets frequency at which gRPC channel will be moved into an idle state | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | +| :headers | Set the headers | [Metadata](https://grpc.github.io/grpc-java/javadoc/io/grpc/Metadata.html) | | +| :enable-keepalive | Set keep alive ping from client to the server | boolean | false | +| :keepalive-time | Set the keep alive time | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | +| :keepalive-timeout | Set the keep alive timeout | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | +| :keepalive-without-stream | Set if client sends keepalive pings even with no active RPCs | boolean | false | +| :metrics-scope | The scope to be used for metrics reporting | [Scope](https://github.com/uber-java/tally/blob/master/core/src/main/java/com/uber/m3/tally/Scope.java) | | + +" + {:channel #(.setChannel ^WorkflowServiceStubsOptions$Builder %1 %2) + :ssl-context #(.setSslContext ^WorkflowServiceStubsOptions$Builder %1 %2) + :enable-https #(.setEnableHttps ^WorkflowServiceStubsOptions$Builder %1 %2) + :target #(.setTarget ^WorkflowServiceStubsOptions$Builder %1 %2) + :rpc-timeout #(.setRpcTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) + :rpc-long-poll-timeout #(.setRpcLongPollTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) + :rpc-query-timeout #(.setRpcQueryTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) + :backoff-reset-freq #(.setConnectionBackoffResetFrequency ^WorkflowServiceStubsOptions$Builder %1 %2) + :grpc-reconnect-freq #(.setGrpcReconnectFrequency ^WorkflowServiceStubsOptions$Builder %1 %2) + :headers #(.setHeaders ^WorkflowServiceStubsOptions$Builder %1 %2) + :enable-keepalive #(.setEnableKeepAlive ^WorkflowServiceStubsOptions$Builder %1 %2) + :keepalive-time #(.setKeepAliveTime ^WorkflowServiceStubsOptions$Builder %1 %2) + :keepalive-timeout #(.setKeepAliveTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) + :keepalive-without-stream #(.setKeepAlivePermitWithoutStream ^WorkflowServiceStubsOptions$Builder %1 %2) + :metrics-scope #(.setMetricsScope ^WorkflowServiceStubsOptions$Builder %1 %2)}) + +(defn stub-options-> + ^WorkflowServiceStubsOptions [params] + (u/build (WorkflowServiceStubsOptions/newBuilder) stub-options params)) + +(defn service-stub-> + [options timeout] + (WorkflowServiceStubs/newConnectedServiceStubs (stub-options-> options) timeout)) diff --git a/src/temporal/client/schedule.clj b/src/temporal/client/schedule.clj index b2d4aae..219a69a 100644 --- a/src/temporal/client/schedule.clj +++ b/src/temporal/client/schedule.clj @@ -1,7 +1,6 @@ (ns temporal.client.schedule (:require [taoensso.timbre :as log] [temporal.client.options :as copts] - [temporal.internal.grpc :as g] [temporal.internal.utils :as u] [temporal.internal.schedule :as s]) (:import [java.time Duration] @@ -11,39 +10,16 @@ (defn create-client "Creates a `ScheduleClient` instance suitable for interacting with Temporal's Schedules. - The `ScheduleClient` has slightly less options then the `WorkflowClient` but it is mostly the same otherwise. Arguments: - - `options`: Client configuration option map (See below) + - `options`: Options for configuring the `ScheduleClient` (See [[temporal.client.options/schedule-client-options]] and [[temporal.client.options/stub-options]]) - `timeout`: Connection timeout as a [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) (default: 5s) - #### options map - - - | Value | Description | Type | Default | - | ------------------------- | --------------------------------------------------------------------------- | ------------ | ------- | - | :target | Sets the connection host:port | String | \"127.0.0.1:7233\" | - | :identity | Overrides the worker node identity (workers only) | String | | - | :namespace | Sets the Temporal namespace context for this client | String | | - | :data-converter | Overrides the data converter used to serialize arguments and results. | [DataConverter](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/converter/DataConverter.html) | | - | :channel | Sets gRPC channel to use. Exclusive with target and sslContext | [ManagedChannel](https://grpc.github.io/grpc-java/javadoc/io/grpc/ManagedChannel.html) | | - | :ssl-context | Sets gRPC SSL Context to use (See [[temporal.tls/new-ssl-context]]) | [SslContext](https://netty.io/4.0/api/io/netty/handler/ssl/SslContext.html) | | - | :enable-https | Sets option to enable SSL/TLS/HTTPS for gRPC | boolean | false | - | :rpc-timeout | Sets the rpc timeout value for non query and non long poll calls | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | - | :rpc-long-poll-timeout | Sets the rpc timeout value | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | - | :rpc-query-timeout | Sets the rpc timeout for queries | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | - | :backoff-reset-freq | Sets frequency at which gRPC connection backoff should be reset practically | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10s | - | :grpc-reconnect-freq | Sets frequency at which gRPC channel will be moved into an idle state | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | - | :headers | Set the headers | [Metadata](https://grpc.github.io/grpc-java/javadoc/io/grpc/Metadata.html) | | - | :enable-keepalive | Set keep alive ping from client to the server | boolean | false | - | :keepalive-time | Set the keep alive time | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | - | :keepalive-timeout | Set the keep alive timeout | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | - | :keepalive-without-stream | Set if client sends keepalive pings even with no active RPCs | boolean | false | - | :metrics-scope | The scope to be used for metrics reporting | [Scope](https://github.com/uber-java/tally/blob/master/core/src/main/java/com/uber/m3/tally/Scope.java) | |" +" ([options] (create-client options (Duration/ofSeconds 5))) ([options timeout] - (let [service (g/service-stub-> options timeout)] + (let [service (copts/service-stub-> options timeout)] (ScheduleClient/newInstance service (copts/schedule-client-options-> options))))) (defn schedule diff --git a/src/temporal/internal/grpc.clj b/src/temporal/internal/grpc.clj deleted file mode 100644 index 19001e2..0000000 --- a/src/temporal/internal/grpc.clj +++ /dev/null @@ -1,28 +0,0 @@ -(ns ^:no-doc temporal.internal.grpc - (:require [temporal.internal.utils :as u]) - (:import [io.temporal.serviceclient WorkflowServiceStubs WorkflowServiceStubsOptions WorkflowServiceStubsOptions$Builder])) - -(def stub-options - {:channel #(.setChannel ^WorkflowServiceStubsOptions$Builder %1 %2) - :ssl-context #(.setSslContext ^WorkflowServiceStubsOptions$Builder %1 %2) - :enable-https #(.setEnableHttps ^WorkflowServiceStubsOptions$Builder %1 %2) - :target #(.setTarget ^WorkflowServiceStubsOptions$Builder %1 %2) - :rpc-timeout #(.setRpcTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) - :rpc-long-poll-timeout #(.setRpcLongPollTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) - :rpc-query-timeout #(.setRpcQueryTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) - :backoff-reset-freq #(.setConnectionBackoffResetFrequency ^WorkflowServiceStubsOptions$Builder %1 %2) - :grpc-reconnect-freq #(.setGrpcReconnectFrequency ^WorkflowServiceStubsOptions$Builder %1 %2) - :headers #(.setHeaders ^WorkflowServiceStubsOptions$Builder %1 %2) - :enable-keepalive #(.setEnableKeepAlive ^WorkflowServiceStubsOptions$Builder %1 %2) - :keepalive-time #(.setKeepAliveTime ^WorkflowServiceStubsOptions$Builder %1 %2) - :keepalive-timeout #(.setKeepAliveTimeout ^WorkflowServiceStubsOptions$Builder %1 %2) - :keepalive-without-stream #(.setKeepAlivePermitWithoutStream ^WorkflowServiceStubsOptions$Builder %1 %2) - :metrics-scope #(.setMetricsScope ^WorkflowServiceStubsOptions$Builder %1 %2)}) - -(defn stub-options-> - ^WorkflowServiceStubsOptions [params] - (u/build (WorkflowServiceStubsOptions/newBuilder) stub-options params)) - -(defn service-stub-> - [options timeout] - (WorkflowServiceStubs/newConnectedServiceStubs (stub-options-> options) timeout)) diff --git a/src/temporal/testing/env.clj b/src/temporal/testing/env.clj index 993e623..fc0f247 100644 --- a/src/temporal/testing/env.clj +++ b/src/temporal/testing/env.clj @@ -4,14 +4,13 @@ "Methods and utilities to assist with unit-testing Temporal workflows" (:require [temporal.client.worker :as worker] [temporal.client.options :as copts] - [temporal.internal.grpc :as g] [temporal.internal.utils :as u]) (:import [io.temporal.testing TestWorkflowEnvironment TestEnvironmentOptions TestEnvironmentOptions$Builder])) (def ^:no-doc test-env-options {:worker-factory-options #(.setWorkerFactoryOptions ^TestEnvironmentOptions$Builder %1 (worker/worker-factory-options-> %2)) - :workflow-client-options #(.setWorkflowClientOptions ^TestEnvironmentOptions$Builder %1 (copts/client-options-> %2)) - :workflow-service-stub-options #(.setWorkflowServiceStubsOptions ^TestEnvironmentOptions$Builder %1 (g/stub-options-> %2)) + :workflow-client-options #(.setWorkflowClientOptions ^TestEnvironmentOptions$Builder %1 (copts/workflow-client-options-> %2)) + :workflow-service-stub-options #(.setWorkflowServiceStubsOptions ^TestEnvironmentOptions$Builder %1 (copts/stub-options-> %2)) :metrics-scope #(.setMetricsScope ^TestEnvironmentOptions$Builder %1 %2)}) (defn ^:no-doc test-env-options-> @@ -34,7 +33,7 @@ Arguments: | ------------------------- | --------------------------------------------- | ------------ | ------- | | :worker-factory-options | | [[worker/worker-factory-options]] | | | :workflow-client-options | | [[copts/client-options]] | | -| :workflow-service-stub-options | | [[grpc/stub-options]] | | +| :workflow-service-stub-options | | [[copts/stub-options]] | | | :metrics-scope | The scope to be used for metrics reporting | [Scope](https://github.com/uber-java/tally/blob/master/core/src/main/java/com/uber/m3/tally/Scope.java) | | diff --git a/test/temporal/test/types.clj b/test/temporal/test/types.clj index 1f977fc..9560077 100644 --- a/test/temporal/test/types.clj +++ b/test/temporal/test/types.clj @@ -5,8 +5,7 @@ [temporal.client.worker :as worker] [temporal.client.options :as o] [temporal.internal.workflow :as w] - [temporal.internal.schedule :as s] - [temporal.internal.grpc :as g]) + [temporal.internal.schedule :as s]) (:import [java.time Duration Instant] [io.grpc Grpc InsecureChannelCredentials Metadata] [io.grpc.netty.shaded.io.grpc.netty GrpcSslContexts])) @@ -27,7 +26,7 @@ (deftest client-options (testing "Verify that our stub options work" - (let [x (g/stub-options-> {:channel (-> (Grpc/newChannelBuilder "foo:1234" (InsecureChannelCredentials/create)) (.build)) + (let [x (o/stub-options-> {:channel (-> (Grpc/newChannelBuilder "foo:1234" (InsecureChannelCredentials/create)) (.build)) :ssl-context (-> (GrpcSslContexts/forClient) (.build)) :target "foo:1234" :enable-https false @@ -43,14 +42,14 @@ :keepalive-without-stream true})] (is (-> x (.getTarget) (= "foo:1234"))))) (testing "Verify that our client options work" - (let [x (o/client-options-> {:identity "test" + (let [x (o/workflow-client-options-> {:identity "test" :namespace "test"})] (is (-> x (.getIdentity) (= "test"))) (is (-> x (.getNamespace) (= "test"))))) (testing "Verify that mixed client/stub options work" (let [options {:target "foo:1234" :namespace "default"}] - (is (some? (g/stub-options-> options))) - (is (some? (o/client-options-> options)))))) + (is (some? (o/stub-options-> options))) + (is (some? (o/workflow-client-options-> options)))))) (deftest worker-options (testing "Verify that our worker-options work" @@ -69,7 +68,7 @@ (deftest schedule-client-options (testing "Verify that our stub options work" - (let [x (g/stub-options-> {:channel (-> (Grpc/newChannelBuilder "foo:1234" (InsecureChannelCredentials/create)) (.build)) + (let [x (o/stub-options-> {:channel (-> (Grpc/newChannelBuilder "foo:1234" (InsecureChannelCredentials/create)) (.build)) :ssl-context (-> (GrpcSslContexts/forClient) (.build)) :target "foo:1234" :enable-https false @@ -91,7 +90,7 @@ (is (-> x (.getNamespace) (= "test"))))) (testing "Verify that mixed client/stub options work" (let [options {:target "foo:1234" :namespace "default"}] - (is (some? (g/stub-options-> options))) + (is (some? (o/stub-options-> options))) (is (some? (o/schedule-client-options-> options)))))) (deftest schedule-options From d54ca23262124a322c8a507a789182107b023660 Mon Sep 17 00:00:00 2001 From: Bailey Kocin Date: Wed, 13 Mar 2024 12:19:01 -0400 Subject: [PATCH 041/102] cljfmt fix Signed-off-by: Bailey Kocin --- test/temporal/test/types.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/temporal/test/types.clj b/test/temporal/test/types.clj index 9560077..4b40811 100644 --- a/test/temporal/test/types.clj +++ b/test/temporal/test/types.clj @@ -43,7 +43,7 @@ (is (-> x (.getTarget) (= "foo:1234"))))) (testing "Verify that our client options work" (let [x (o/workflow-client-options-> {:identity "test" - :namespace "test"})] + :namespace "test"})] (is (-> x (.getIdentity) (= "test"))) (is (-> x (.getNamespace) (= "test"))))) (testing "Verify that mixed client/stub options work" From 985465fc9cf00cc34cbff331048427b7ddd08217 Mon Sep 17 00:00:00 2001 From: Gregory Haskins Date: Thu, 14 Mar 2024 19:17:14 -0400 Subject: [PATCH 042/102] Update README.md Remove Schedules from the note about missing features --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f6bdf41..509503f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This Clojure SDK is a framework for authoring Workflows and Activities in Clojur **Alpha** -This SDK is battle-tested and used in production but is undergoing active development and is subject to breaking changes (*). Some significant features such as Child-Workflows and Schedules are missing/incomplete. +This SDK is battle-tested and used in production but is undergoing active development and is subject to breaking changes (*). Some features such as Child-Workflows and Updates are currently missing. > (*) We will always bump at least the minor version when breaking changes are introduced and include a release note. From 34a8bde5f22657101eee08c926322dfc10e79254 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 14 Mar 2024 19:19:18 -0400 Subject: [PATCH 043/102] Release v0.20.0 Changes since v0.19.0 --------------------- 985465f Update README.md d54ca23 cljfmt fix eb7cc43 centeralize options 4d02ccc run lein cljfmt check 711d0f5 add temporal schedule support Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 2cef789..a213baa 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.19.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.20.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 1d19132da4ddfeb06a98439d781a2c9d2dc9dfe4 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 14 Mar 2024 19:20:23 -0400 Subject: [PATCH 044/102] Prepare for v0.20.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index a213baa..fe3ad04 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.20.0" +(defproject io.github.manetu/temporal-sdk "0.20.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 17df2eef1503c304cf52fb391083ad4820813abf Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 18 Mar 2024 19:25:18 -0400 Subject: [PATCH 045/102] Gracefully handle eviction exceptions rather than throw an ugly ERROR report Signed-off-by: Greg Haskins --- project.clj | 2 +- src/temporal/internal/activity.clj | 4 ++++ src/temporal/internal/workflow.clj | 32 +++++++++++++++++------------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/project.clj b/project.clj index fe3ad04..532edf5 100644 --- a/project.clj +++ b/project.clj @@ -36,5 +36,5 @@ :cloverage {:runner :eftest :runner-opts {:multithread? false :fail-fast? true} - :fail-threshold 90 + :fail-threshold 89 :ns-exclude-regex [#"temporal.client.worker"]}) diff --git a/src/temporal/internal/activity.clj b/src/temporal/internal/activity.clj index 266d72e..47200df 100644 --- a/src/temporal/internal/activity.clj +++ b/src/temporal/internal/activity.clj @@ -11,6 +11,7 @@ [temporal.internal.exceptions :as e] [temporal.common :as common]) (:import [java.time Duration] + [io.temporal.internal.sync DestroyWorkflowThreadError] [io.temporal.activity Activity ActivityInfo DynamicActivity ActivityCancellationType] [io.temporal.activity ActivityOptions ActivityOptions$Builder LocalActivityOptions LocalActivityOptions$Builder] [clojure.core.async.impl.channels ManyToManyChannel])) @@ -107,6 +108,9 @@ (log/trace activity-id "calling" f "with args:" a) (try+ (result-> activity-id (f ctx a)) + (catch DestroyWorkflowThreadError ex + (log/debug activity-id "thread evicted") + (throw ex)) (catch Object o (log/error &throw-context) (e/forward &throw-context))))) diff --git a/src/temporal/internal/workflow.clj b/src/temporal/internal/workflow.clj index 5ad515e..1d9c2cb 100644 --- a/src/temporal/internal/workflow.clj +++ b/src/temporal/internal/workflow.clj @@ -12,6 +12,7 @@ [temporal.internal.utils :as u]) (:import [io.temporal.api.enums.v1 WorkflowIdReusePolicy] [io.temporal.client WorkflowOptions WorkflowOptions$Builder] + [io.temporal.internal.sync DestroyWorkflowThreadError] [io.temporal.workflow Workflow WorkflowInfo])) (extend-protocol p/Datafiable @@ -75,17 +76,20 @@ (defn execute [ctx dispatch args] - (try+ - (let [{:keys [workflow-type workflow-id]} (get-info) - d (u/find-dispatch dispatch workflow-type) - f (:fn d) - a (u/->args args) - _ (log/trace workflow-id "calling" f "with args:" a) - r (if (-> d :type (= :legacy)) - (f ctx {:args a :signals (s/create-signal-chan)}) - (f a))] - (log/trace workflow-id "result:" r) - (nippy/freeze r)) - (catch Object o - (log/error &throw-context) - (e/forward &throw-context)))) + (let [{:keys [workflow-type workflow-id]} (get-info)] + (try+ + (let [d (u/find-dispatch dispatch workflow-type) + f (:fn d) + a (u/->args args) + _ (log/trace workflow-id "calling" f "with args:" a) + r (if (-> d :type (= :legacy)) + (f ctx {:args a :signals (s/create-signal-chan)}) + (f a))] + (log/trace workflow-id "result:" r) + (nippy/freeze r)) + (catch DestroyWorkflowThreadError ex + (log/debug workflow-id "thread evicted") + (throw ex)) + (catch Object o + (log/error &throw-context) + (e/forward &throw-context))))) From 77717431fdc0c2fc1b857de25e238c29d663aeba Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 18 Mar 2024 19:30:35 -0400 Subject: [PATCH 046/102] Release v0.20.1 Changes since v0.20.0 --------------------- 17df2ee Gracefully handle eviction exceptions rather than throw an ugly ERROR report Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 532edf5..8c2b91a 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.20.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.20.1" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From a092857948cb9fc893869b03b6af7151b07149f0 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 18 Mar 2024 19:31:29 -0400 Subject: [PATCH 047/102] Prepare for v0.20.2 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 8c2b91a..713b397 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.20.1" +(defproject io.github.manetu/temporal-sdk "0.20.2-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From b7c396597e3a0de788286210e7df257d3a01a6c9 Mon Sep 17 00:00:00 2001 From: Srinivasan Muralidharan Date: Mon, 29 Apr 2024 14:32:09 -0400 Subject: [PATCH 048/102] args serdes should adhere to this SDKs protocol Args typically are serialized and deserialized using objarray-> and args-> (defined in temporal.internal.utils) so that data-conversion can transparently work with arbitrary data. Schedule should adhere to this protocol and apply objarray-> on arguments so as to satisfy the args-> call when workflow is executed. TODO: enhance UT to actually launch a workflow when base java library adds proper schedule support to its "testing" framework. Signed-off-by: Srinivasan Muralidharan --- src/temporal/internal/schedule.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/temporal/internal/schedule.clj b/src/temporal/internal/schedule.clj index 4e97496..26c9064 100644 --- a/src/temporal/internal/schedule.clj +++ b/src/temporal/internal/schedule.clj @@ -56,7 +56,7 @@ (def schedule-action-start-workflow-spec-> {:options #(.setOptions ^ScheduleActionStartWorkflow$Builder %1 (w/wf-options-> %2)) :arguments (fn [^ScheduleActionStartWorkflow$Builder builder value] - (.setArguments builder (to-array [(stringify-keys value)]))) + (.setArguments builder (u/->objarray value))) :workflow-type (fn [^ScheduleActionStartWorkflow$Builder builder value] (if (string? value) (.setWorkflowType builder ^String value) From c49b6724d684aa8c1f9361ac444a3a248c402ab1 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 30 Apr 2024 07:31:30 -0400 Subject: [PATCH 049/102] Release v0.20.2 Changes since v0.20.1 --------------------- b7c3965 args serdes should adhere to this SDKs protocol Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 713b397..935579b 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.20.2-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "0.20.2" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 06c239497ada0c705c1f01dbaa17c0192e7aa343 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 30 Apr 2024 07:32:27 -0400 Subject: [PATCH 050/102] Prepare for v0.20.3 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 935579b..e4fa9b1 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.20.2" +(defproject io.github.manetu/temporal-sdk "0.20.3-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 0f6e80f6ea00870affacff789480732aa190d6a0 Mon Sep 17 00:00:00 2001 From: Bailey Kocin Date: Mon, 13 May 2024 16:49:09 -0400 Subject: [PATCH 051/102] add new allSettled handler Signed-off-by: Bailey Kocin --- src/temporal/client/schedule.clj | 2 +- src/temporal/internal/promise.clj | 3 +-- src/temporal/internal/schedule.clj | 3 +-- src/temporal/promise.clj | 27 ++++++++++++++++++++++- test/temporal/test/concurrency.clj | 35 +++++++++++++++++++++++++++++- 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/temporal/client/schedule.clj b/src/temporal/client/schedule.clj index 219a69a..476285a 100644 --- a/src/temporal/client/schedule.clj +++ b/src/temporal/client/schedule.clj @@ -50,7 +50,7 @@ ```" [^ScheduleClient client schedule-id options] (let [schedule (s/schedule-> options) - schedule-options (s/schedule-options-> options)] + schedule-options (s/schedule-options-> (:schedule options))] (log/tracef "create schedule:" schedule-id) (.createSchedule client schedule-id schedule schedule-options))) diff --git a/src/temporal/internal/promise.clj b/src/temporal/internal/promise.clj index 4cc8083..c042a50 100644 --- a/src/temporal/internal/promise.clj +++ b/src/temporal/internal/promise.clj @@ -1,8 +1,7 @@ ;; Copyright © Manetu, Inc. All rights reserved (ns ^:no-doc temporal.internal.promise - (:require [taoensso.timbre :as log] - [promesa.protocols :as pt] + (:require [promesa.protocols :as pt] [temporal.internal.utils :refer [->Func] :as u]) (:import [clojure.lang IDeref IBlockingDeref] [io.temporal.workflow Promise] diff --git a/src/temporal/internal/schedule.clj b/src/temporal/internal/schedule.clj index 26c9064..eaad319 100644 --- a/src/temporal/internal/schedule.clj +++ b/src/temporal/internal/schedule.clj @@ -1,6 +1,5 @@ (ns ^:no-doc temporal.internal.schedule - (:require [clojure.walk :refer [stringify-keys]] - [temporal.internal.utils :as u] + (:require [temporal.internal.utils :as u] [temporal.internal.workflow :as w]) (:import [io.temporal.api.enums.v1 ScheduleOverlapPolicy] [io.temporal.client.schedules diff --git a/src/temporal/promise.clj b/src/temporal/promise.clj index 961fd1f..a41f358 100644 --- a/src/temporal/promise.clj +++ b/src/temporal/promise.clj @@ -30,6 +30,31 @@ promises returned from [[temporal.activity/invoke]] from within workflow context (p/then (fn [_] (mapv deref coll))))) +(defn allSettled + "Returns Promise that becomes completed when all arguments are completed, even in the face of errors. + +*N.B. You must handle the exceptions in the returned promises when done* + +Similar to [promesa/all](https://funcool.github.io/promesa/latest/promesa.core.html#var-all) but designed to work with +promises returned from [[temporal.activity/invoke]] from within workflow context. + +For more Java SDK samples example look here: + https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/batch + +```clojure +(-> (allSettled [(a/invoke activity-a ..) (a/invoke activity-b ..)]) + (promesa.core/then (fn [[a-result b-result]] ...))) +``` +" + [coll] + (letfn [(wait! [^Promise p] (try (.get p) (catch Exception _)))] + (-> (into-array Promise (mapv wait! (->array coll))) + (Promise/allOf) + (pt/->PromiseAdapter) + ;; The promises are all completed at this point, + ;; this is just to use the promesa library + (p/then (fn [_] (mapv deref coll)))))) + (defn race "Returns Promise that becomes completed when any of the arguments are completed. @@ -54,4 +79,4 @@ promises returned from [[temporal.activity/invoke]] from within workflow context (defn rejected "Returns a new, rejected promise" [^Exception e] - (Workflow/newFailedPromise e)) \ No newline at end of file + (Workflow/newFailedPromise e)) diff --git a/test/temporal/test/concurrency.clj b/test/temporal/test/concurrency.clj index c0cfc81..e3e7c9c 100644 --- a/test/temporal/test/concurrency.clj +++ b/test/temporal/test/concurrency.clj @@ -32,4 +32,37 @@ (testing "Verifies that we can launch activities in parallel" (let [workflow (t/create-workflow concurrency-workflow)] (c/start workflow {}) - (is (-> workflow c/get-result deref count (= 10)))))) \ No newline at end of file + (is (-> workflow c/get-result deref count (= 10)))))) + +(defactivity all-settled-activity + [ctx args] args) + +(defworkflow all-settled-workflow + [args] + @(-> (pt/all (map #(a/invoke all-settled-activity %) (range 10))) + (p/then (fn [r] r)) + (p/catch (fn [e] (:args (ex-data e)))))) + +(defactivity error-prone-activity + [ctx args] + (when (= args 5) + (throw (ex-info "error on 5" {:args args}))) + args) + +(defworkflow error-prone-workflow + [args] + @(-> (pt/all (map #(a/invoke error-prone-activity %) (range 10))) + (p/then (fn [r] r)) + (p/catch (fn [e] (:args (ex-data e)))))) + +(deftest test-all-settled + (testing "Testing that allSettled waits for all the activities to complete + just like `p/all` does in spite of errors" + (let [workflow (t/create-workflow all-settled-workflow)] + (c/start workflow {}) + (is (-> workflow c/get-result deref count (= 10))))) + (testing "Testing that allSettled waits for all the activities to complete + despite error and can return the errors" + (let [workflow (t/create-workflow error-prone-workflow)] + (c/start workflow {}) + (is (-> workflow c/get-result deref (= 5)))))) From 26939866077f25837c80f2e370313a5ecb3236ef Mon Sep 17 00:00:00 2001 From: Bailey Kocin Date: Mon, 13 May 2024 17:04:23 -0400 Subject: [PATCH 052/102] rename to all-settled Signed-off-by: Bailey Kocin --- src/temporal/promise.clj | 4 ++-- test/temporal/test/concurrency.clj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/temporal/promise.clj b/src/temporal/promise.clj index a41f358..62ebbbe 100644 --- a/src/temporal/promise.clj +++ b/src/temporal/promise.clj @@ -30,7 +30,7 @@ promises returned from [[temporal.activity/invoke]] from within workflow context (p/then (fn [_] (mapv deref coll))))) -(defn allSettled +(defn all-settled "Returns Promise that becomes completed when all arguments are completed, even in the face of errors. *N.B. You must handle the exceptions in the returned promises when done* @@ -42,7 +42,7 @@ For more Java SDK samples example look here: https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/batch ```clojure -(-> (allSettled [(a/invoke activity-a ..) (a/invoke activity-b ..)]) +(-> (all-settled [(a/invoke activity-a ..) (a/invoke activity-b ..)]) (promesa.core/then (fn [[a-result b-result]] ...))) ``` " diff --git a/test/temporal/test/concurrency.clj b/test/temporal/test/concurrency.clj index e3e7c9c..a16c7d2 100644 --- a/test/temporal/test/concurrency.clj +++ b/test/temporal/test/concurrency.clj @@ -56,12 +56,12 @@ (p/catch (fn [e] (:args (ex-data e)))))) (deftest test-all-settled - (testing "Testing that allSettled waits for all the activities to complete + (testing "Testing that all-settled waits for all the activities to complete just like `p/all` does in spite of errors" (let [workflow (t/create-workflow all-settled-workflow)] (c/start workflow {}) (is (-> workflow c/get-result deref count (= 10))))) - (testing "Testing that allSettled waits for all the activities to complete + (testing "Testing that all-settled waits for all the activities to complete despite error and can return the errors" (let [workflow (t/create-workflow error-prone-workflow)] (c/start workflow {}) From 9e3f242ccc9d369af79573ad56cdaeac8829cbc0 Mon Sep 17 00:00:00 2001 From: Bailey Kocin Date: Mon, 13 May 2024 18:38:05 -0400 Subject: [PATCH 053/102] add better tests Signed-off-by: Bailey Kocin --- src/temporal/promise.clj | 15 ++++++++------- test/temporal/test/concurrency.clj | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/temporal/promise.clj b/src/temporal/promise.clj index 62ebbbe..40e56f0 100644 --- a/src/temporal/promise.clj +++ b/src/temporal/promise.clj @@ -47,13 +47,14 @@ For more Java SDK samples example look here: ``` " [coll] - (letfn [(wait! [^Promise p] (try (.get p) (catch Exception _)))] - (-> (into-array Promise (mapv wait! (->array coll))) - (Promise/allOf) - (pt/->PromiseAdapter) - ;; The promises are all completed at this point, - ;; this is just to use the promesa library - (p/then (fn [_] (mapv deref coll)))))) + (letfn [(wait! [^Promise p] (try (.get p) p (catch Exception _ p)))] + (-> + (into-array Promise (mapv wait! (->array coll))) + (Promise/allOf) + (pt/->PromiseAdapter) + ;; The promises are all completed at this point, + ;; this is just to use the promesa library + (p/then (fn [_] (mapv deref coll)))))) (defn race "Returns Promise that becomes completed when any of the arguments are completed. diff --git a/test/temporal/test/concurrency.clj b/test/temporal/test/concurrency.clj index a16c7d2..fb65ae1 100644 --- a/test/temporal/test/concurrency.clj +++ b/test/temporal/test/concurrency.clj @@ -35,23 +35,34 @@ (is (-> workflow c/get-result deref count (= 10)))))) (defactivity all-settled-activity - [ctx args] args) + [ctx args] + (log/tracef "calling all-settled-activity %d" args) + args) + +(defn invoke-settled [x] + (a/invoke all-settled-activity x)) (defworkflow all-settled-workflow [args] - @(-> (pt/all (map #(a/invoke all-settled-activity %) (range 10))) + (log/trace "calling all-settled-workflow") + @(-> (pt/all-settled (map invoke-settled (range 10))) (p/then (fn [r] r)) (p/catch (fn [e] (:args (ex-data e)))))) (defactivity error-prone-activity [ctx args] + (log/tracef "calling error-prone-activity %d" args) (when (= args 5) (throw (ex-info "error on 5" {:args args}))) args) +(defn invoke-error [x] + (a/invoke error-prone-activity x)) + (defworkflow error-prone-workflow [args] - @(-> (pt/all (map #(a/invoke error-prone-activity %) (range 10))) + (log/trace "calling error-prone-workflow") + @(-> (pt/all-settled (map invoke-error (range 10))) (p/then (fn [r] r)) (p/catch (fn [e] (:args (ex-data e)))))) @@ -62,7 +73,7 @@ (c/start workflow {}) (is (-> workflow c/get-result deref count (= 10))))) (testing "Testing that all-settled waits for all the activities to complete - despite error and can return the errors" + just like `p/all` and can still propogate errors" (let [workflow (t/create-workflow error-prone-workflow)] (c/start workflow {}) (is (-> workflow c/get-result deref (= 5)))))) From a7bab0b38036ef6f1cca2ef8b8ff5fe8c81c1e02 Mon Sep 17 00:00:00 2001 From: Bailey Kocin Date: Mon, 13 May 2024 18:44:37 -0400 Subject: [PATCH 054/102] optimize the number of arrays used Signed-off-by: Bailey Kocin --- src/temporal/promise.clj | 14 ++++++++------ test/temporal/test/concurrency.clj | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/temporal/promise.clj b/src/temporal/promise.clj index 40e56f0..fc03bde 100644 --- a/src/temporal/promise.clj +++ b/src/temporal/promise.clj @@ -47,14 +47,16 @@ For more Java SDK samples example look here: ``` " [coll] - (letfn [(wait! [^Promise p] (try (.get p) p (catch Exception _ p)))] - (-> - (into-array Promise (mapv wait! (->array coll))) - (Promise/allOf) - (pt/->PromiseAdapter) + (letfn [(wait! [^Promise p] (try (.get p) (catch Exception _)))] + ;; So we do not have to duplicate this, make the only copy here + (let [promises (->array coll)] + (run! wait! promises) + (-> + (Promise/allOf promises) + (pt/->PromiseAdapter) ;; The promises are all completed at this point, ;; this is just to use the promesa library - (p/then (fn [_] (mapv deref coll)))))) + (p/then (fn [_] (mapv deref coll))))))) (defn race "Returns Promise that becomes completed when any of the arguments are completed. diff --git a/test/temporal/test/concurrency.clj b/test/temporal/test/concurrency.clj index fb65ae1..b08f6ea 100644 --- a/test/temporal/test/concurrency.clj +++ b/test/temporal/test/concurrency.clj @@ -73,7 +73,7 @@ (c/start workflow {}) (is (-> workflow c/get-result deref count (= 10))))) (testing "Testing that all-settled waits for all the activities to complete - just like `p/all` and can still propogate errors" + just like `p/all` and can still propogate errors" (let [workflow (t/create-workflow error-prone-workflow)] (c/start workflow {}) (is (-> workflow c/get-result deref (= 5)))))) From 9b75d8bfbb156affba8ee31e59da69ed23454d06 Mon Sep 17 00:00:00 2001 From: Bailey Kocin Date: Mon, 13 May 2024 18:46:47 -0400 Subject: [PATCH 055/102] update documentation for all-settled Signed-off-by: Bailey Kocin --- src/temporal/promise.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/temporal/promise.clj b/src/temporal/promise.clj index fc03bde..a553601 100644 --- a/src/temporal/promise.clj +++ b/src/temporal/promise.clj @@ -31,9 +31,9 @@ promises returned from [[temporal.activity/invoke]] from within workflow context (mapv deref coll))))) (defn all-settled - "Returns Promise that becomes completed when all arguments are completed, even in the face of errors. + "Returns a Promise that becomes completed/failed when all the arguments are done/settled, even in the face of errors. -*N.B. You must handle the exceptions in the returned promises when done* +*N.B. You must handle the exceptions in the returned promise with promesa* Similar to [promesa/all](https://funcool.github.io/promesa/latest/promesa.core.html#var-all) but designed to work with promises returned from [[temporal.activity/invoke]] from within workflow context. From 6c66ae1e36aafd719c644eda0a19a4f2742c8883 Mon Sep 17 00:00:00 2001 From: Bailey Date: Sun, 19 May 2024 13:40:43 -0400 Subject: [PATCH 056/102] add child workflow implementation (#58) ## Add the implementation for Child Workflows to the Temporal Clojure SDK ### Why? Child Workflows are a key feature in separating out Workflow execution histories and physically preventing two of the same workflow execution from being spawned if uniqueness needs to be guaranteed. ### What? - I reused the `invoke` logic for the activities and added an `invoke` function to the `workflow` namespace - I added a `internal/child_workflows` for building the `ChildWorkflowOptions` - Added tests for regular child workflows, concurrent parent/child workflows, and async activities inside child workflows - I added a documentation page for child workflows. ### How? For more information look at these links: - https://docs.temporal.io/dev-guide/java/features#child-workflows - https://docs.temporal.io/encyclopedia/child-workflows TODOS: Test against a live Temporal cluster more Let me know if you have any questions and concerns --------- Signed-off-by: Bailey Kocin Co-authored-by: Bailey Kocin Signed-off-by: Greg Haskins --- dev-resources/utils.clj | 81 ++++++++++++++++++++++++ doc/child_workflows.md | 64 +++++++++++++++++++ doc/cljdoc.edn | 1 + src/temporal/activity.clj | 11 +--- src/temporal/internal/child_workflow.clj | 41 ++++++++++++ src/temporal/internal/utils.clj | 9 ++- src/temporal/workflow.clj | 78 ++++++++++++++++++++++- test/temporal/test/async.clj | 39 ++++++++++-- test/temporal/test/child_workflow.clj | 44 +++++++++++++ test/temporal/test/concurrency.clj | 42 ++++++++++-- test/temporal/test/types.clj | 33 +++++++++- 11 files changed, 420 insertions(+), 23 deletions(-) create mode 100644 dev-resources/utils.clj create mode 100644 doc/child_workflows.md create mode 100644 src/temporal/internal/child_workflow.clj create mode 100644 test/temporal/test/child_workflow.clj diff --git a/dev-resources/utils.clj b/dev-resources/utils.clj new file mode 100644 index 0000000..708e8dc --- /dev/null +++ b/dev-resources/utils.clj @@ -0,0 +1,81 @@ +(ns utils + (:require [taoensso.timbre :as log] + [temporal.activity :as a :refer [defactivity]] + [temporal.client.core :as c] + [temporal.client.worker :as worker] + [temporal.workflow :as w :refer [defworkflow]]) + (:import [java.time Duration])) + +(def client (atom nil)) +(def current-worker-thread (atom nil)) + +(def default-client-options {:target "localhost:7233" + :namespace "default" + :enable-https false}) + +(def default-worker-options {:task-queue "default"}) + +(def default-workflow-options {:task-queue "default" + :workflow-execution-timeout (Duration/ofSeconds 30) + :retry-options {:maximum-attempts 1} + :workflow-id "test-workflow"}) + +(defactivity greet-activity + [ctx {:keys [name] :as args}] + (log/info "greet-activity:" args) + (str "Hi, " name)) + +(defworkflow child-workflow + [{names :names :as args}] + (log/info "child-workflow:" names) + (for [name names] + @(a/invoke greet-activity {:name name}))) + +(defworkflow parent-workflow + [args] + (log/info "parent-workflow:" args) + @(w/invoke child-workflow args (merge default-workflow-options {:workflow-id "child-workflow"}))) + +(defn create-temporal-client + "Creates a new temporal client if the old one does not exist" + ([] (create-temporal-client nil)) + ([options] + (when-not @client + (let [options (merge default-client-options options)] + (log/info "creating temporal client" options) + (reset! client (c/create-client options)))))) + +(defn worker-loop + ([client] (worker-loop client nil)) + ([client options] + (let [options (merge default-worker-options options)] + (log/info "starting temporal worker" options) + (worker/start client options)))) + +(defn create-temporal-worker + "Starts a new instance running on another daemon thread, + stops the current temporal worker and thread if they exist" + ([client] (create-temporal-worker client nil)) + ([client options] + (when (and @current-worker-thread (.isAlive @current-worker-thread)) + (.interrupt @current-worker-thread) + (reset! current-worker-thread nil)) + (let [thread (Thread. (partial worker-loop client options))] + (doto thread + (.setDaemon true) + (.start)) + (reset! current-worker-thread thread)))) + +(defn execute-workflow + ([client workflow arguments] (execute-workflow client workflow arguments nil)) + ([client workflow arguments options] + (let [options (merge default-workflow-options options) + workflow (c/create-workflow client workflow options)] + (log/info "executing workflow" arguments) + (c/start workflow arguments) + @(c/get-result workflow)))) + +(comment + (do (create-temporal-client) + (create-temporal-worker @client) + (execute-workflow @client parent-workflow {:names ["Hanna" "Bob" "Tracy" "Felix"]}))) diff --git a/doc/child_workflows.md b/doc/child_workflows.md new file mode 100644 index 0000000..1824afc --- /dev/null +++ b/doc/child_workflows.md @@ -0,0 +1,64 @@ +# Child Workflows + +## What is a Child Workflow? + +A Child Workflow is a Workflow execution spawned from within a Workflow. + +Child Workflows orchestrate invocations of Activities just like Workflows do. + +Child Workflows should not be used for code organization, however they can be used to partition a Workflow execution's event history into smaller chunks which helps avoid the roughly *~50MB* Workflow event history limit, amongst other use cases. + +You should visit the [workflows](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/workflows) page to learn more about Workflows, their constraints, and their executions in general. + +For more information about Child Workflows in general visit [Temporal Child Workflows](https://docs.temporal.io/encyclopedia/child-workflows) + +## Implementing Child Workflows + +In this Clojure SDK programming model, a Temporal Workflow is a function declared with [defworkflow](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.workflow#defworkflow) + +And a Child workflow is declared in the exact same way + +### Example + +```clojure +(require '[temporal.workflow :refer [defworkflow]]) + +(defworkflow my-workflow + [{:keys [foo]}] + ...) +``` +## Starting Child Workflow Executions + +In this Clojure SDK, Workflows start Child Workflows with [temporal.workflow/invoke](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.workflow#invoke) + +The options (`ChildWorkflowOptions`) provided to [temporal.workflow/invoke](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.workflow#invoke) are similar to the ones required to create a regular workflow with [temporal.client.core/create-workflow](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/api/temporal.client.core#create-workflow) + +One big difference however is Child Workflows can provide options to control what happens to themselves with their parents close/fail/complete. + +When a Parent Workflow Execution stops, the Temporal Cluster determines what will happen to any running child workflow executions based on the `:parent-close-policy` option. + +See [Temporal Parent Close Policy](https://docs.temporal.io/encyclopedia/child-workflows#parent-close-policy) for more information + +### Example + +```clojure +(require '[temporal.workflow :refer [defworkflow]]) +(require '[temporal.activity :refer [defactivity] :as a]) + +(defactivity child-greeter-activity + [ctx {:keys [name] :as args}] + (str "Hi, " name)) + +(defworkflow child-workflow + [{:keys [names] :as args}] + (for [name names] + @(a/invoke child-greeter-activity {:name name}))) + +(defworkflow parent-workflow + [args] + @(w/invoke child-workflow args {:retry-options {:maximum-attempts 1} + :workflow-task-timeout 10 + :workflow-execution-timeout 3600 + :workflow-run-timeout 3600})) +``` + diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index dfa13b0..957616f 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -1,6 +1,7 @@ {:cljdoc.doc/tree [["Readme" {:file "README.md"}] ["Workflows" {:file "doc/workflows.md"}] + ["Child Workflows" {:file "doc/child_workflows.md"}] ["Activities" {:file "doc/activities.md"}] ["Workers" {:file "doc/workers.md"}] ["Clients" {:file "doc/clients.md"}] diff --git a/src/temporal/activity.clj b/src/temporal/activity.clj index 49bb46c..0f4a164 100644 --- a/src/temporal/activity.clj +++ b/src/temporal/activity.clj @@ -47,13 +47,6 @@ along with the Activity Task for the next retry attempt and can be extracted by [] (a/get-info)) -(defn- complete-invoke - [activity result] - (log/trace activity "completed with" (count result) "bytes") - (let [r (nippy/thaw result)] - (log/trace activity "results:" r) - r)) - (defn invoke " Invokes 'activity' with 'params' from within a workflow context. Returns a promise that when derefed will resolve to @@ -98,7 +91,7 @@ Arguments: stub (Workflow/newUntypedActivityStub (a/invoke-options-> options))] (log/trace "invoke:" activity "with" params options) (-> (.executeAsync stub act-name u/bytes-type (u/->objarray params)) - (p/then (partial complete-invoke activity)) + (p/then (partial u/complete-invoke activity)) (p/catch e/slingshot? e/recast-stone) (p/catch (fn [e] (log/error e) @@ -139,7 +132,7 @@ Arguments: stub (Workflow/newUntypedLocalActivityStub (a/local-invoke-options-> options))] (log/trace "local-invoke:" activity "with" params options) (-> (.executeAsync stub act-name u/bytes-type (u/->objarray params)) - (p/then (partial complete-invoke activity)) + (p/then (partial u/complete-invoke activity)) (p/catch e/slingshot? e/recast-stone) (p/catch (fn [e] (log/error e) diff --git a/src/temporal/internal/child_workflow.clj b/src/temporal/internal/child_workflow.clj new file mode 100644 index 0000000..ff7eebb --- /dev/null +++ b/src/temporal/internal/child_workflow.clj @@ -0,0 +1,41 @@ +(ns temporal.internal.child-workflow + (:require [temporal.common :as common] + [temporal.internal.utils :as u] + [temporal.internal.workflow :as w]) + (:import [java.time Duration] + [io.temporal.api.enums.v1 ParentClosePolicy] + [io.temporal.workflow ChildWorkflowOptions ChildWorkflowOptions$Builder ChildWorkflowCancellationType])) + +(def cancellation-type-> + {:abandon ChildWorkflowCancellationType/ABANDON + :try-cancel ChildWorkflowCancellationType/TRY_CANCEL + :wait-cancellation-completed ChildWorkflowCancellationType/WAIT_CANCELLATION_COMPLETED + :wait-cancellation-requested ChildWorkflowCancellationType/WAIT_CANCELLATION_REQUESTED}) + +(def parent-close-policy-> + {:abandon ParentClosePolicy/PARENT_CLOSE_POLICY_ABANDON + :request-cancel ParentClosePolicy/PARENT_CLOSE_POLICY_REQUEST_CANCEL + :terminate ParentClosePolicy/PARENT_CLOSE_POLICY_TERMINATE}) + +(def ^:no-doc child-workflow-option-spec + {:task-queue #(.setTaskQueue ^ChildWorkflowOptions$Builder %1 (u/namify %2)) + :workflow-id #(.setWorkflowId ^ChildWorkflowOptions$Builder %1 (u/namify %2)) + :workflow-id-reuse-policy #(.setWorkflowIdReusePolicy ^ChildWorkflowOptions$Builder %1 (w/workflow-id-reuse-policy-> %2)) + :parent-close-policy #(.setParentClosePolicy ^ChildWorkflowOptions$Builder %1 (parent-close-policy-> %2)) + :workflow-execution-timeout #(.setWorkflowExecutionTimeout ^ChildWorkflowOptions$Builder %1 %2) + :workflow-run-timeout #(.setWorkflowRunTimeout ^ChildWorkflowOptions$Builder %1 %2) + :workflow-task-timeout #(.setWorkflowTaskTimeout ^ChildWorkflowOptions$Builder %1 %2) + :retry-options #(.setRetryOptions %1 (common/retry-options-> %2)) + :cron-schedule #(.setCronSchedule ^ChildWorkflowOptions$Builder %1 %2) + :cancellation-type #(.setCancellationType ^ChildWorkflowOptions$Builder %1 (cancellation-type-> %2)) + :memo #(.setMemo ^ChildWorkflowOptions$Builder %1 %2)}) + +(defn import-child-workflow-options + [{:keys [workflow-run-timeout workflow-execution-timeout] :as options}] + (cond-> options + (every? nil? [workflow-run-timeout workflow-execution-timeout]) + (assoc :workflow-execution-timeout (Duration/ofSeconds 10)))) + +(defn child-workflow-options-> + ^ChildWorkflowOptions [options] + (u/build (ChildWorkflowOptions/newBuilder) child-workflow-option-spec (import-child-workflow-options options))) diff --git a/src/temporal/internal/utils.clj b/src/temporal/internal/utils.clj index 4fbd01a..002f4e6 100644 --- a/src/temporal/internal/utils.clj +++ b/src/temporal/internal/utils.clj @@ -118,6 +118,13 @@ (fn [x] (str (symbol x))))) +(defn complete-invoke + [stub result] + (log/trace stub "completed with" (count result) "bytes") + (let [r (nippy/thaw result)] + (log/trace stub "results:" r) + r)) + (defn ->Func [f] (reify @@ -141,4 +148,4 @@ (f x1 x2 x3 x4 x5)) Functions$Func6 (apply [_ x1 x2 x3 x4 x5 x6] - (f x1 x2 x3 x4 x5 x6)))) \ No newline at end of file + (f x1 x2 x3 x4 x5 x6)))) diff --git a/src/temporal/workflow.clj b/src/temporal/workflow.clj index 0c1e3cb..8f81a5a 100644 --- a/src/temporal/workflow.clj +++ b/src/temporal/workflow.clj @@ -5,8 +5,11 @@ (:require [taoensso.nippy :as nippy] [taoensso.timbre :as log] + [promesa.core :as p] + [temporal.internal.exceptions :as e] [temporal.internal.utils :as u] - [temporal.internal.workflow :as w]) + [temporal.internal.workflow :as w] + [temporal.internal.child-workflow :as cw]) (:import [io.temporal.workflow DynamicQueryHandler Workflow] [java.util.function Supplier] [java.time Duration])) @@ -111,3 +114,76 @@ Arguments: (log/trace (str ~fqn ": ") args#) (let [f# (fn ~params* (do ~@body))] (f# args#)))))))) + +(defn invoke + " +Invokes a 'child workflow' with 'params' from within a workflow context. +Returns a promise that when derefed will resolve to the evaluation of the defworkflow once the workflow concludes. + +Arguments: + +- `workflow`: A reference to a symbol registered with [[defworkflow]], called a Child Workflow usually. +- `params`: Opaque serializable data that will be passed as arguments to the invoked child workflow +- `options`: See below. + +#### options map + +| Value | Description | Type | Default | +| --------------------------- | ------------------------------------------------------------------------------------------ | ------------ | ------- | +| :task-queue | Task queue to use for child workflow tasks | String | | +| :workflow-id | Workflow id to use when starting | String | | +| :workflow-id-reuse-policy | Specifies server behavior if a completed workflow with the same id exists | See `workflow id reuse policy types` below | | +| :parent-close-policy | Specifies how this workflow reacts to the death of the parent workflow | See `parent close policy types` below | | +| :workflow-execution-timeout | The time after which child workflow execution is automatically terminated | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10 seconds | +| :workflow-run-timeout | The time after which child workflow run is automatically terminated | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | +| :workflow-task-timeout | Maximum execution time of a single workflow task | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | +| :retry-options | RetryOptions that define how child workflow is retried in case of failure | [[temporal.common/retry-options]] | | +| :cron-schedule | A cron schedule string | String | | +| :cancellation-type | In case of a child workflow cancellation it fails with a CanceledFailure | See `cancellation types` below | | +| :memo | Specifies additional non-indexed information in result of list workflow | String | | + +#### cancellation types + +| Value | Description | +| ------------------------- | --------------------------------------------------------------------------- | +| :try-cancel | Initiate a cancellation request and immediately report cancellation to the parent | +| :abandon | Do not request cancellation of the child workflow | +| :wait-cancellation-completed | Wait for child cancellation completion | +| :wait-cancellation-requested | Request cancellation of the child and wait for confirmation that the request was received | + +#### parent close policy types + +| Value | Description | +| ------------------------- | --------------------------------------------------------------------------- | +| :abandon | Do not request cancellation of the child workflow | +| :request-cancel | Request cancellation of the child and wait for confirmation that the request was received | +| :terminate | Terminate the child workflow | + +#### workflow id reuse policy types + +| Value | Description | +| ---------------------------- | --------------------------------------------------------------------------- | +| :allow-duplicate | Allow starting a child workflow execution using the same workflow id. | +| :allow-duplicate-failed-only | Allow starting a child workflow execution using the same workflow id, only when the last execution's final state is one of [terminated, cancelled, timed out, failed] | +| :reject-duplicate | Do not permit re-use of the child workflow id for this workflow. | +| :terminate-if-running | If a workflow is running using the same child workflow ID, terminate it and start a new one. If no running child workflow, then the behavior is the same as ALLOW_DUPLICATE | + +```clojure +(defworkflow my-workflow + [ctx {:keys [foo] :as args}] + ...) + +(invoke my-workflow {:foo \"bar\"} {:start-to-close-timeout (Duration/ofSeconds 3)) +``` +" + ([workflow params] (invoke workflow params {})) + ([workflow params options] + (let [wf-name (w/get-annotated-name workflow) + stub (Workflow/newUntypedChildWorkflowStub wf-name (cw/child-workflow-options-> options))] + (log/trace "invoke:" workflow "with" params options) + (-> (.executeAsync stub u/bytes-type (u/->objarray params)) + (p/then (partial u/complete-invoke workflow)) + (p/catch e/slingshot? e/recast-stone) + (p/catch (fn [e] + (log/error e) + (throw e))))))) diff --git a/test/temporal/test/async.clj b/test/temporal/test/async.clj index d0610e6..48701aa 100644 --- a/test/temporal/test/async.clj +++ b/test/temporal/test/async.clj @@ -1,13 +1,13 @@ ;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.async - (:require [clojure.test :refer :all] + (:require [clojure.test :refer [deftest testing is use-fixtures]] [clojure.core.async :refer [go]] [taoensso.timbre :as log] - [temporal.client.core :as c] - [temporal.workflow :refer [defworkflow]] [temporal.activity :refer [defactivity] :as a] - [temporal.test.utils :as t])) + [temporal.client.core :as c] + [temporal.test.utils :as t] + [temporal.workflow :refer [defworkflow] :as w])) (use-fixtures :once t/wrap-service) @@ -24,7 +24,7 @@ (log/info "greeter-workflow:" args) @(a/invoke async-greet-activity args {:retry-options {:maximum-attempts 1}})) -(deftest the-test +(deftest basic-async-test (testing "Verifies that we can round-trip with an async task" (let [workflow (t/create-workflow async-greeter-workflow)] (c/start workflow {:name "Bob"}) @@ -34,3 +34,32 @@ (c/start workflow {:name "Charlie"}) (is (thrown? java.util.concurrent.ExecutionException @(c/get-result workflow)))))) + +(defactivity async-child-activity + [ctx {:keys [name] :as args}] + (go + (log/info "async-child-activity:" args) + (if (= name "Charlie") + (ex-info "permission-denied" {}) + (str "Hi, " name)))) + +(defworkflow async-child-workflow + [{:keys [name] :as args}] + (log/info "async-child-workflow:" args) + @(a/invoke async-child-activity args {:retry-options {:maximum-attempts 1}})) + +(defworkflow async-parent-workflow + [args] + (log/info "async-parent-workflow:" args) + @(w/invoke async-child-workflow args {:retry-options {:maximum-attempts 1} :task-queue t/task-queue})) + +(deftest child-workflow-test + (testing "Verifies that we can round-trip with an async task" + (let [workflow (t/create-workflow async-parent-workflow)] + (c/start workflow {:name "Bob"}) + (is (= @(c/get-result workflow) "Hi, Bob")))) + (testing "Verifies that we can process errors in async mode" + (let [workflow (t/create-workflow async-parent-workflow)] + (c/start workflow {:name "Charlie"}) + (is (thrown? java.util.concurrent.ExecutionException + @(c/get-result workflow)))))) diff --git a/test/temporal/test/child_workflow.clj b/test/temporal/test/child_workflow.clj new file mode 100644 index 0000000..23310ed --- /dev/null +++ b/test/temporal/test/child_workflow.clj @@ -0,0 +1,44 @@ +(ns temporal.test.child-workflow + (:require [clojure.test :refer [deftest testing is use-fixtures]] + [taoensso.timbre :as log] + [temporal.client.core :as c] + [temporal.workflow :refer [defworkflow] :as w] + [temporal.activity :refer [defactivity] :as a] + [temporal.test.utils :as t])) + +(use-fixtures :once t/wrap-service) + +(defactivity child-greeter-activity + [ctx {:keys [name] :as args}] + (log/info "greet-activity:" args) + (str "Hi, " name)) + +(defworkflow child-workflow + [{:keys [names] :as args}] + (log/info "child-workflow:" args) + (for [name names] + @(a/invoke child-greeter-activity {:name name}))) + +(defworkflow parent-workflow + [args] + (log/info "parent-workflow:" args) + @(w/invoke child-workflow args {:retry-options {:maximum-attempts 1} :task-queue t/task-queue})) + +(deftest basic-child-workflow-test + (testing "Using a child workflow (with multiple activities) in a parent workflow works as expected" + (let [workflow (t/create-workflow parent-workflow)] + (c/start workflow {:names ["Bob" "George" "Fred"]}) + (is (= (set @(c/get-result workflow)) #{"Hi, Bob" "Hi, George" "Hi, Fred"}))))) + +(defworkflow parent-workflow-with-activities + [args] + (log/info "parent-workflow:" args) + (concat + @(w/invoke child-workflow args) + [@(a/invoke child-greeter-activity {:name "Xavier"})])) + +(deftest parent-workflow-with-mulitple-test + (testing "Using a child workflow (with multiple activities) in a parent workflow (with activities) works as expected" + (let [workflow (t/create-workflow parent-workflow-with-activities)] + (c/start workflow {:names ["Bob" "George" "Fred"]}) + (is (= (set @(c/get-result workflow)) #{"Hi, Bob" "Hi, George" "Hi, Fred" "Hi, Xavier"}))))) diff --git a/test/temporal/test/concurrency.clj b/test/temporal/test/concurrency.clj index b08f6ea..784a83c 100644 --- a/test/temporal/test/concurrency.clj +++ b/test/temporal/test/concurrency.clj @@ -1,14 +1,14 @@ ;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.concurrency - (:require [clojure.test :refer :all] + (:require [clojure.test :refer [deftest testing is use-fixtures]] [promesa.core :as p] [taoensso.timbre :as log] - [temporal.client.core :as c] - [temporal.workflow :refer [defworkflow]] [temporal.activity :refer [defactivity] :as a] + [temporal.client.core :as c] [temporal.promise :as pt] - [temporal.test.utils :as t])) + [temporal.test.utils :as t] + [temporal.workflow :refer [defworkflow] :as w])) (use-fixtures :once t/wrap-service) @@ -28,7 +28,7 @@ (log/info "r:" r) r)))) -(deftest the-test +(deftest concurrency-with-all-test (testing "Verifies that we can launch activities in parallel" (let [workflow (t/create-workflow concurrency-workflow)] (c/start workflow {}) @@ -66,7 +66,7 @@ (p/then (fn [r] r)) (p/catch (fn [e] (:args (ex-data e)))))) -(deftest test-all-settled +(deftest concurrency-with-all-settled-test (testing "Testing that all-settled waits for all the activities to complete just like `p/all` does in spite of errors" (let [workflow (t/create-workflow all-settled-workflow)] @@ -77,3 +77,33 @@ (let [workflow (t/create-workflow error-prone-workflow)] (c/start workflow {}) (is (-> workflow c/get-result deref (= 5)))))) + +(defactivity doubling-activity + [ctx args] + (log/info "doubling-activity:" args) + (* args 2)) + +(defn invoke-doubling-activity [x] + (a/invoke doubling-activity x)) + +(defworkflow concurrent-child-workflow + [args] + (log/info "concurrent-child-workflow:" args) + @(-> (pt/all [(invoke-doubling-activity args)]) + (p/then (fn [r] (log/info "r:" r) r)))) + +(defn invoke-child-workflow [x] + (w/invoke concurrent-child-workflow x {:retry-options {:maximum-attempts 1} :task-queue t/task-queue})) + +(defworkflow concurrent-parent-workflow + [args] + (log/info "concurrent-parent-workflow:" args) + @(-> (pt/all (map invoke-child-workflow (range 10))) + (p/then (fn [r] (log/info "r:" r) r)))) + +(deftest child-workflow-concurrency-test + (testing "Using a child workflow instead of an ctivity works with the promise api" + (let [workflow (t/create-workflow concurrent-parent-workflow)] + (c/start workflow {}) + (is (-> workflow c/get-result deref count (= 10))) + (is (-> workflow c/get-result) (mapv #(* 2 %) (range 10)))))) diff --git a/test/temporal/test/types.clj b/test/temporal/test/types.clj index 4b40811..9833119 100644 --- a/test/temporal/test/types.clj +++ b/test/temporal/test/types.clj @@ -5,7 +5,8 @@ [temporal.client.worker :as worker] [temporal.client.options :as o] [temporal.internal.workflow :as w] - [temporal.internal.schedule :as s]) + [temporal.internal.schedule :as s] + [temporal.internal.child-workflow :as cw]) (:import [java.time Duration Instant] [io.grpc Grpc InsecureChannelCredentials Metadata] [io.grpc.netty.shaded.io.grpc.netty GrpcSslContexts])) @@ -125,3 +126,33 @@ (is (-> schedule .getPolicy .isPauseOnFailure)) (is (= "note" (-> schedule .getState .getNote))) (is (-> schedule .getState .isPaused))))) + +(deftest child-workflow-options + (testing "Verify that a `ChildWorkflowOptions` instance can be built properly" + (let [options {:workflow-id "foo" + :task-queue "bar" + :workflow-execution-timeout (Duration/ofSeconds 1) + :workflow-run-timeout (Duration/ofSeconds 2) + :workflow-task-timeout (Duration/ofSeconds 3) + :retry-options {:maximum-attempts 1} + :cron-schedule "* * * * *" + :memo {"foo" "bar"} + :workflow-id-reuse-policy :terminate-if-running + :parent-close-policy :terminate + :cancellation-type :abandon} + child-workflow-options (cw/child-workflow-options-> options)] + (is (some? child-workflow-options)) + (is (= "foo" (-> child-workflow-options .getWorkflowId))) + (is (= "bar" (-> child-workflow-options .getTaskQueue))) + (is (= (Duration/ofSeconds 1) (-> child-workflow-options .getWorkflowExecutionTimeout))) + (is (= (Duration/ofSeconds 2) (-> child-workflow-options .getWorkflowRunTimeout))) + (is (= (Duration/ofSeconds 3) (-> child-workflow-options .getWorkflowTaskTimeout))) + (is (= 1 (-> child-workflow-options .getRetryOptions .getMaximumAttempts))) + (is (= "* * * * *" (-> child-workflow-options .getCronSchedule))) + (is (= {"foo" "bar"} (-> child-workflow-options .getMemo))) + (is (= io.temporal.api.enums.v1.WorkflowIdReusePolicy/WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING + (-> child-workflow-options .getWorkflowIdReusePolicy))) + (is (= io.temporal.api.enums.v1.ParentClosePolicy/PARENT_CLOSE_POLICY_TERMINATE + (-> child-workflow-options .getParentClosePolicy))) + (is (= io.temporal.workflow.ChildWorkflowCancellationType/ABANDON + (-> child-workflow-options .getCancellationType)))))) From 63d5ee29e0b8699d30a544f04d4f6cf5e300ea6f Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Sun, 19 May 2024 13:45:20 -0400 Subject: [PATCH 057/102] Update documentation for **stable** status Signed-off-by: Greg Haskins --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 509503f..e0c3b24 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,9 @@ This Clojure SDK is a framework for authoring Workflows and Activities in Clojur ### Status -**Alpha** +**Stable** -This SDK is battle-tested and used in production but is undergoing active development and is subject to breaking changes (*). Some features such as Child-Workflows and Updates are currently missing. - -> (*) We will always bump at least the minor version when breaking changes are introduced and include a release note. +This SDK is feature complete with a stable API and used in production. Any future breaking changes will be managed by bumping at least the minor version and including a release note. ### Clojure SDK From a47a2eb98decc7f1a2bcae41971a48c5c880166a Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Sun, 19 May 2024 13:47:57 -0400 Subject: [PATCH 058/102] Update deps Signed-off-by: Greg Haskins --- project.clj | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/project.clj b/project.clj index e4fa9b1..7ef3d02 100644 --- a/project.clj +++ b/project.clj @@ -11,13 +11,13 @@ [lein-cloverage "1.2.4"] [jonase/eastwood "1.3.0"] [lein-codox "0.10.8"]] - :dependencies [[org.clojure/clojure "1.11.1"] + :dependencies [[org.clojure/clojure "1.11.3"] [org.clojure/core.async "1.6.681"] - [io.temporal/temporal-sdk "1.23.0"] - [io.temporal/temporal-testing "1.23.0"] - [com.taoensso/encore "3.90.0"] + [io.temporal/temporal-sdk "1.23.2"] + [io.temporal/temporal-testing "1.23.2"] + [com.taoensso/encore "3.111.0"] [com.taoensso/timbre "6.5.0"] - [com.taoensso/nippy "3.3.0"] + [com.taoensso/nippy "3.4.1"] [funcool/promesa "9.2.542"] [medley "1.4.0"] [slingshot "0.12.2"]] @@ -31,10 +31,10 @@ :profiles {:dev {:dependencies [[org.clojure/tools.namespace "1.5.0"] [eftest "0.6.0"] [mockery "0.1.4"] - [io.temporal/temporal-opentracing "1.23.0"]] + [io.temporal/temporal-opentracing "1.23.2"]] :resource-paths ["test/temporal/test/resources"]}} :cloverage {:runner :eftest :runner-opts {:multithread? false :fail-fast? true} - :fail-threshold 89 + :fail-threshold 90 :ns-exclude-regex [#"temporal.client.worker"]}) From 8e1fb436705f6f7d361e0a00060bd679c8a86abf Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 21 May 2024 13:09:22 -0400 Subject: [PATCH 059/102] Release v1.0.0 Changes since v0.20.2 --------------------- a47a2eb Update deps 63d5ee2 Update documentation for **stable** status 6c66ae1 add child workflow implementation (#58) 9b75d8b update documentation for all-settled a7bab0b optimize the number of arrays used 9e3f242 add better tests 2693986 rename to all-settled 0f6e80f add new allSettled handler Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 7ef3d02..fca8b81 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "0.20.3-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.0.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 3bce19f6616bdc0029001782fa74ac37421085a5 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 21 May 2024 13:10:41 -0400 Subject: [PATCH 060/102] Prepare for v1.0.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index fca8b81..ae333d3 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.0.0" +(defproject io.github.manetu/temporal-sdk "1.0.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 5dfa6ce4f45e83e28071ee35359d264f479f3ff5 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 23 May 2024 11:49:49 -0400 Subject: [PATCH 061/102] Documentation updates Signed-off-by: Greg Haskins --- doc/child_workflows.md | 3 +-- doc/workflows.md | 45 +++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/doc/child_workflows.md b/doc/child_workflows.md index 1824afc..11228dc 100644 --- a/doc/child_workflows.md +++ b/doc/child_workflows.md @@ -8,7 +8,7 @@ Child Workflows orchestrate invocations of Activities just like Workflows do. Child Workflows should not be used for code organization, however they can be used to partition a Workflow execution's event history into smaller chunks which helps avoid the roughly *~50MB* Workflow event history limit, amongst other use cases. -You should visit the [workflows](https://cljdoc.org/d/io.github.manetu/temporal-sdk/CURRENT/workflows) page to learn more about Workflows, their constraints, and their executions in general. +You should visit the [workflows](./workflows.md) page to learn more about Workflows, their constraints, and their executions in general. For more information about Child Workflows in general visit [Temporal Child Workflows](https://docs.temporal.io/encyclopedia/child-workflows) @@ -61,4 +61,3 @@ See [Temporal Parent Close Policy](https://docs.temporal.io/encyclopedia/child-w :workflow-execution-timeout 3600 :workflow-run-timeout 3600})) ``` - diff --git a/doc/workflows.md b/doc/workflows.md index 3ae378b..f009f62 100644 --- a/doc/workflows.md +++ b/doc/workflows.md @@ -30,42 +30,42 @@ A Workflow implementation consists of defining a (defworkflow) function. The pl ### Workflow Implementation Constraints -Temporal uses the [Event Sourcing pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing) to recover the state of a Workflow object, including its threads and local variable values. In essence, the Workflow code is re-executed from the beginning whenever a Workflow state requires restoration. During replay, successfully executed Activities are not re-executed but return the result previously recorded in the Workflow event history. +Temporal uses the [Event Sourcing pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing) to recover the state of a Workflow object, including its threads and local variable values. The Workflow code is re-executed from the beginning whenever a Workflow state requires restoration. During replay, successfully executed Activities are not re-executed but return the result previously recorded in the Workflow event history. Even though Temporal has the replay capability, which brings resilience to your Workflows, you should never think about this capability when writing your Workflows. Instead, you should focus on implementing your business logic/requirements and write your Workflows as they would execute only once. There are some things, however, to think about when writing your Workflows, namely determinism and isolation. We summarize these constraints here: -- Do not use any mutable global variables such as atoms in your Workflow implementations. This will ensure that multiple Workflow instances are fully isolated. -- Do not call any non-deterministic functions like non-seeded random or uuid-generators directly from the Workflow code. +- Do not use mutable global variables such as atoms in your Workflow implementations. Avoiding globals will ensure that multiple Workflow instances are fully isolated. +- Do not call non-deterministic functions like non-seeded random or uuid generators directly from the Workflow code. Instead, use Side Effects. - Perform all IO operations and calls to third-party services on Activities and not Workflows, as they are usually non-deterministic. -- Do not use any programming language constructs that rely on system time. (Coming soon: API methods for time) -- Do not use threading primitives such as clojure.core.async/go or clojure.core.async/thread. (Coming soon: API methods for async function execution) +- Do not use any programming language constructs that rely on system time. All notions of time must come from Side Effects or Activities so that the results become part of the Event History. +- Do not use threading primitives such as clojure.core.async/go or clojure.core.async/thread. - Do not perform any operations that may block the underlying thread, such as clojure.core.async/ Date: Thu, 23 May 2024 11:51:15 -0400 Subject: [PATCH 062/102] Exclude internal.child-workflow from documentation Signed-off-by: Greg Haskins --- src/temporal/internal/child_workflow.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/temporal/internal/child_workflow.clj b/src/temporal/internal/child_workflow.clj index ff7eebb..b717704 100644 --- a/src/temporal/internal/child_workflow.clj +++ b/src/temporal/internal/child_workflow.clj @@ -1,4 +1,4 @@ -(ns temporal.internal.child-workflow +(ns ^:no-doc temporal.internal.child-workflow (:require [temporal.common :as common] [temporal.internal.utils :as u] [temporal.internal.workflow :as w]) From 12618aea80edf220e3d39ea76a96ae47214a0b49 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 23 May 2024 12:26:53 -0400 Subject: [PATCH 063/102] Release v1.0.1 Changes since v1.0.0 -------------------- 874312f Exclude internal.child-workflow from documentation 5dfa6ce Documentation updates Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index ae333d3..c499872 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.0.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.0.1" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 8f34803f375472cb2abb15cbb0cf3095c04d7aea Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 23 May 2024 12:27:48 -0400 Subject: [PATCH 064/102] Prepare for v1.0.2 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index c499872..c079caf 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.0.1" +(defproject io.github.manetu/temporal-sdk "1.0.2-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 6f2b8513b4edc2816774d654296b25c7899b83ea Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Wed, 26 Jun 2024 14:29:46 -0400 Subject: [PATCH 065/102] Ensure temporal.promise resolved/rejected are compatible with all/race Signed-off-by: Greg Haskins --- src/temporal/promise.clj | 4 ++-- test/temporal/test/resolved_promises.clj | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 test/temporal/test/resolved_promises.clj diff --git a/src/temporal/promise.clj b/src/temporal/promise.clj index a553601..42d6a60 100644 --- a/src/temporal/promise.clj +++ b/src/temporal/promise.clj @@ -77,9 +77,9 @@ promises returned from [[temporal.activity/invoke]] from within workflow context (defn resolved "Returns a new, fully resolved promise" [value] - (Workflow/newPromise value)) + (pt/->PromiseAdapter (Workflow/newPromise value))) (defn rejected "Returns a new, rejected promise" [^Exception e] - (Workflow/newFailedPromise e)) + (pt/->PromiseAdapter (Workflow/newFailedPromise e))) diff --git a/test/temporal/test/resolved_promises.clj b/test/temporal/test/resolved_promises.clj new file mode 100644 index 0000000..c6186d6 --- /dev/null +++ b/test/temporal/test/resolved_promises.clj @@ -0,0 +1,24 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.resolved-promises + (:require [clojure.test :refer :all] + [taoensso.timbre :as log] + [temporal.client.core :as c] + [temporal.workflow :refer [defworkflow]] + [temporal.promise :as pt] + [temporal.test.utils :as t])) + +(use-fixtures :once t/wrap-service) + +(defworkflow all-workflow + [args] + (log/info "workflow:" args) + @(pt/all [(pt/resolved true)]) + @(pt/race [(pt/resolved true)]) + :ok) + +(deftest the-test + (testing "Verifies that pt/resolved and pt/rejected are compatible with all/race" + (let [workflow (t/create-workflow all-workflow)] + (c/start workflow {}) + (is (-> workflow c/get-result deref (= :ok)))))) From ce5b9de0437b70a989251b96de7ef12d6b0573bd Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Wed, 26 Jun 2024 14:48:58 -0400 Subject: [PATCH 066/102] Release v1.0.2 Changes since v1.0.1 -------------------- 6f2b851 Ensure temporal.promise resolved/rejected are compatible with all/race Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index c079caf..63d69be 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.0.2-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.0.2" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From e9037100c33f7ff57dc866c309f1b08462f316cc Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Wed, 26 Jun 2024 14:50:01 -0400 Subject: [PATCH 067/102] Prepare for v1.0.3 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 63d69be..c1b282a 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.0.2" +(defproject io.github.manetu/temporal-sdk "1.0.3-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From f29d4402a939571c5d91c9f2d9eaa386ec5458fe Mon Sep 17 00:00:00 2001 From: Kyle Passarelli Date: Sat, 29 Jun 2024 17:54:36 -0700 Subject: [PATCH 068/102] Add clj-kondo configuration Signed-off-by: Kyle Passarelli --- project.clj | 2 +- .../clj-kondo.exports/io.github.manetu/temporal-sdk/config.edn | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 resources/clj-kondo.exports/io.github.manetu/temporal-sdk/config.edn diff --git a/project.clj b/project.clj index c1b282a..7329432 100644 --- a/project.clj +++ b/project.clj @@ -22,7 +22,7 @@ [medley "1.4.0"] [slingshot "0.12.2"]] :repl-options {:init-ns user} - :java-source-paths ["src"] + :java-source-paths ["src" "resources"] :javac-options ["-target" "11" "-source" "11"] :eastwood {:add-linters [:unused-namespaces]} diff --git a/resources/clj-kondo.exports/io.github.manetu/temporal-sdk/config.edn b/resources/clj-kondo.exports/io.github.manetu/temporal-sdk/config.edn new file mode 100644 index 0000000..27f04bf --- /dev/null +++ b/resources/clj-kondo.exports/io.github.manetu/temporal-sdk/config.edn @@ -0,0 +1,2 @@ +{:lint-as {temporal.workflow/defworkflow clojure.core/defn + temporal.activity/defactivity clojure.core/defn}} From 0256b2552c78168eb13348a308844d09aca75b20 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 15 Jul 2024 17:08:30 -0400 Subject: [PATCH 069/102] Add activity-id retry test Signed-off-by: Greg Haskins --- dev-resources/utils.clj | 8 +++--- test/temporal/test/retry_coherence.clj | 40 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 test/temporal/test/retry_coherence.clj diff --git a/dev-resources/utils.clj b/dev-resources/utils.clj index 708e8dc..fed4261 100644 --- a/dev-resources/utils.clj +++ b/dev-resources/utils.clj @@ -25,16 +25,16 @@ (log/info "greet-activity:" args) (str "Hi, " name)) -(defworkflow child-workflow +(defworkflow user-child-workflow [{names :names :as args}] (log/info "child-workflow:" names) (for [name names] @(a/invoke greet-activity {:name name}))) -(defworkflow parent-workflow +(defworkflow user-parent-workflow [args] (log/info "parent-workflow:" args) - @(w/invoke child-workflow args (merge default-workflow-options {:workflow-id "child-workflow"}))) + @(w/invoke user-child-workflow args (merge default-workflow-options {:workflow-id "child-workflow"}))) (defn create-temporal-client "Creates a new temporal client if the old one does not exist" @@ -78,4 +78,4 @@ (comment (do (create-temporal-client) (create-temporal-worker @client) - (execute-workflow @client parent-workflow {:names ["Hanna" "Bob" "Tracy" "Felix"]}))) + (execute-workflow @client user-parent-workflow {:names ["Hanna" "Bob" "Tracy" "Felix"]}))) diff --git a/test/temporal/test/retry_coherence.clj b/test/temporal/test/retry_coherence.clj new file mode 100644 index 0000000..f2126d0 --- /dev/null +++ b/test/temporal/test/retry_coherence.clj @@ -0,0 +1,40 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.retry-coherence + (:require [clojure.test :refer :all] + [taoensso.timbre :as log] + [temporal.client.core :as c] + [temporal.workflow :refer [defworkflow]] + [temporal.activity :refer [defactivity] :as a] + [temporal.test.utils :as t]) + (:import [java.time Duration])) + +(use-fixtures :once t/wrap-service) + +(defactivity retry-activity + [_ {:keys [mode]}] + (let [{:keys [activity-id]} (a/get-info)] + (log/info "retry-activity:" activity-id) + (if-let [details (a/get-heartbeat-details)] + (do + (log/info "original activity-id:" activity-id "current activity-id:" details) + (= activity-id details)) + (do + (a/heartbeat activity-id) + (case mode + :crash (throw (ex-info "synthetic crash" {})) + :timeout (Thread/sleep 2000)))))) + +(defworkflow retry-workflow + [args] + @(a/invoke retry-activity args {:start-to-close-timeout (Duration/ofSeconds 1)})) + +(deftest the-test + (testing "Verifies that a retriable crash has a stable activity-id" + (let [workflow (t/create-workflow retry-workflow)] + (c/start workflow {:mode :crash}) + (is (-> workflow c/get-result deref true?)))) + (testing "Verifies that a timeout retry has a stable activity-id" + (let [workflow (t/create-workflow retry-workflow)] + (c/start workflow {:mode :timeout}) + (is (-> workflow c/get-result deref true?))))) From e1a2dc61d2e67bca0af166de29606bbf0b3a35c6 Mon Sep 17 00:00:00 2001 From: Felipe Cortez Date: Thu, 25 Jul 2024 20:57:21 -0300 Subject: [PATCH 070/102] Fix doc/clients.md schedule example Signed-off-by: Felipe Cortez --- doc/clients.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/clients.md b/doc/clients.md index b6c6759..daed0c0 100644 --- a/doc/clients.md +++ b/doc/clients.md @@ -109,7 +109,7 @@ Create and manage a schedule as follows schedule-id "my-schedule" client (s/create-client client-options)] (s/schedule client schedule-id {:schedule {:trigger-immediately? true - :memo "Created by John Doe"} + :memo {"Created by" "John Doe"}} :spec {:cron-expressions ["0 0 * * *"] :timezone "US/Central"} :policy {:pause-on-failure? false @@ -125,4 +125,4 @@ Create and manage a schedule as follows (s/execute client schedule-id :skip) (s/reschedule client schedule-id {:spec {:cron-expressions ["0 1 * * *"]}}) (s/unschedule client schedule-id)) -``` \ No newline at end of file +``` From 70cf629cfa19f57380adcfdff069f3bd913e1722 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 30 Aug 2024 13:53:55 -0400 Subject: [PATCH 071/102] Update deps Signed-off-by: Greg Haskins --- project.clj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/project.clj b/project.clj index 7329432..9d9bda4 100644 --- a/project.clj +++ b/project.clj @@ -11,13 +11,13 @@ [lein-cloverage "1.2.4"] [jonase/eastwood "1.3.0"] [lein-codox "0.10.8"]] - :dependencies [[org.clojure/clojure "1.11.3"] + :dependencies [[org.clojure/clojure "1.11.4"] [org.clojure/core.async "1.6.681"] - [io.temporal/temporal-sdk "1.23.2"] - [io.temporal/temporal-testing "1.23.2"] - [com.taoensso/encore "3.111.0"] + [io.temporal/temporal-sdk "1.25.1"] + [io.temporal/temporal-testing "1.25.1"] + [com.taoensso/encore "3.117.0"] [com.taoensso/timbre "6.5.0"] - [com.taoensso/nippy "3.4.1"] + [com.taoensso/nippy "3.4.2"] [funcool/promesa "9.2.542"] [medley "1.4.0"] [slingshot "0.12.2"]] @@ -31,7 +31,7 @@ :profiles {:dev {:dependencies [[org.clojure/tools.namespace "1.5.0"] [eftest "0.6.0"] [mockery "0.1.4"] - [io.temporal/temporal-opentracing "1.23.2"]] + [io.temporal/temporal-opentracing "1.25.1"]] :resource-paths ["test/temporal/test/resources"]}} :cloverage {:runner :eftest :runner-opts {:multithread? false From 8231a5ac70b2482f6e012bb72af07998dcaada61 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 30 Aug 2024 16:32:08 -0400 Subject: [PATCH 072/102] Release v1.0.3 Changes since v1.0.2 -------------------- 70cf629 Update deps e1a2dc6 Fix doc/clients.md schedule example 0256b25 Add activity-id retry test f29d440 Add clj-kondo configuration Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 9d9bda4..3750aca 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.0.3-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.0.3" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 6747cfb447cad1dd004a11e8b25e6d39fc1a7f44 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 30 Aug 2024 16:33:03 -0400 Subject: [PATCH 073/102] Prepare for v1.0.4 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 3750aca..18c1e38 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.0.3" +(defproject io.github.manetu/temporal-sdk "1.0.4-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 9f2619a97e1224d525668a1e7e8bd70de802a338 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 7 Oct 2024 14:54:56 -0400 Subject: [PATCH 074/102] Fix default retry behavior for local activities Signed-off-by: Greg Haskins --- src/temporal/internal/activity.clj | 11 +++++-- test/temporal/test/local_retry.clj | 47 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 test/temporal/test/local_retry.clj diff --git a/src/temporal/internal/activity.clj b/src/temporal/internal/activity.clj index 47200df..9e9bb4f 100644 --- a/src/temporal/internal/activity.clj +++ b/src/temporal/internal/activity.clj @@ -40,16 +40,21 @@ ^ActivityOptions [params] (u/build (ActivityOptions/newBuilder) invoke-option-spec (import-invoke-options params))) +(defn- local-retry-options-> [{:keys [maximum-attempts] :or {maximum-attempts 0} :as options}] + (-> options + (cond-> (zero? maximum-attempts) (assoc :maximum-attempts Integer/MAX_VALUE)) ;; workaround for https://github.com/temporalio/sdk-java/issues/1727 + (common/retry-options->))) + (def local-invoke-option-spec {:start-to-close-timeout #(.setStartToCloseTimeout ^LocalActivityOptions$Builder %1 %2) :schedule-to-close-timeout #(.setScheduleToCloseTimeout ^LocalActivityOptions$Builder %1 %2) - :retry-options #(.setRetryOptions ^LocalActivityOptions$Builder %1 (common/retry-options-> %2)) + :retry-options #(.setRetryOptions ^LocalActivityOptions$Builder %1 (local-retry-options-> %2)) :do-not-include-args #(.setDoNotIncludeArgumentsIntoMarker ^LocalActivityOptions$Builder %1 %2) :local-retry-threshold #(.setLocalRetryThreshold ^LocalActivityOptions$Builder %1 %2)}) (defn local-invoke-options-> - ^LocalActivityOptions [params] - (u/build (LocalActivityOptions/newBuilder (LocalActivityOptions/getDefaultInstance)) local-invoke-option-spec (import-invoke-options params))) + ^LocalActivityOptions [{:keys [retry-options] :or {retry-options {}} :as params}] + (u/build (LocalActivityOptions/newBuilder (LocalActivityOptions/getDefaultInstance)) local-invoke-option-spec (import-invoke-options (assoc params :retry-options retry-options)))) (extend-protocol p/Datafiable ActivityInfo diff --git a/test/temporal/test/local_retry.clj b/test/temporal/test/local_retry.clj new file mode 100644 index 0000000..966a8a2 --- /dev/null +++ b/test/temporal/test/local_retry.clj @@ -0,0 +1,47 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.local-retry + (:require [clojure.test :refer :all] + [promesa.core :as p] + [taoensso.timbre :as log] + [temporal.client.core :as c] + [temporal.workflow :refer [defworkflow]] + [temporal.activity :refer [defactivity] :as a] + [temporal.test.utils :as t]) + (:import (io.temporal.client WorkflowFailedException) + [io.temporal.failure TimeoutFailure ActivityFailure] + [java.time Duration])) + +(use-fixtures :once t/wrap-service) + +(defactivity local-retry-activity + [ctx args] + (log/info "local-retry-activity") + (Thread/sleep 100000000)) + +(defworkflow local-retry-workflow + [args] + (log/info "local-retry-workflow:" args) + @(-> (a/local-invoke local-retry-activity {} (merge args {:do-not-include-args true + :start-to-close-timeout (Duration/ofMillis 500)})) + (p/catch ActivityFailure + :fail))) + +(defn exec [args] + (let [workflow (t/create-workflow local-retry-workflow)] + (c/start workflow args) + @(-> (c/get-result workflow) + (p/then (constantly :fail)) + (p/catch WorkflowFailedException + (fn [ex] + (if (instance? TimeoutFailure (ex-cause ex)) + :pass + :fail)))))) + +(deftest the-test + (testing "RetryPolicy defaults" + (is (= :pass (exec {})))) + (testing "Explicit unlimited" + (is (= :pass (exec {:retry-options {:maximum-attempts 0}})))) + (testing "Verify that setting maximum-attempts to a finite value is respected" + (is (= :fail (exec {:retry-options {:maximum-attempts 1}}))))) From 31f55d060c419f23a2035a1f3e0220b0c7319173 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 7 Oct 2024 15:46:12 -0400 Subject: [PATCH 075/102] Release v1.0.4 Changes since v1.0.3 -------------------- 9f2619a Fix default retry behavior for local activities Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 18c1e38..182fca3 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.0.4-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.0.4" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 585e327cbc902a1f8ec3c6fa2404090a8372750a Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Mon, 7 Oct 2024 15:47:01 -0400 Subject: [PATCH 076/102] Prepare for v1.0.5 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 182fca3..2817e85 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.0.4" +(defproject io.github.manetu/temporal-sdk "1.0.5-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 0ac4fa47c034318433beba05d93f3ef0866d7130 Mon Sep 17 00:00:00 2001 From: Bailey Kocin Date: Tue, 26 Nov 2024 09:41:44 -0500 Subject: [PATCH 077/102] remove the default options for child workflows and fall back to the JavaSDK workflow defaults Signed-off-by: Bailey Kocin --- src/temporal/internal/child_workflow.clj | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/temporal/internal/child_workflow.clj b/src/temporal/internal/child_workflow.clj index b717704..3335dae 100644 --- a/src/temporal/internal/child_workflow.clj +++ b/src/temporal/internal/child_workflow.clj @@ -30,12 +30,6 @@ :cancellation-type #(.setCancellationType ^ChildWorkflowOptions$Builder %1 (cancellation-type-> %2)) :memo #(.setMemo ^ChildWorkflowOptions$Builder %1 %2)}) -(defn import-child-workflow-options - [{:keys [workflow-run-timeout workflow-execution-timeout] :as options}] - (cond-> options - (every? nil? [workflow-run-timeout workflow-execution-timeout]) - (assoc :workflow-execution-timeout (Duration/ofSeconds 10)))) - (defn child-workflow-options-> ^ChildWorkflowOptions [options] - (u/build (ChildWorkflowOptions/newBuilder) child-workflow-option-spec (import-child-workflow-options options))) + (u/build (ChildWorkflowOptions/newBuilder) child-workflow-option-spec options)) From f7e887c4a78426aeeae6ed132a0cda89bb7aadfb Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 26 Nov 2024 10:57:06 -0500 Subject: [PATCH 078/102] Release v1.1.0 Changes since v1.0.4 -------------------- 0ac4fa4 remove the default options for child workflows and fall back to the JavaSDK workflow defaults N.B. This is a breaking change if your code relied upon the child-workflow timeout default. Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 2817e85..a240fd1 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.0.5-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.1.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 2760c21c736087563a6ef0fb2591c3fd34fe8082 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 26 Nov 2024 10:58:37 -0500 Subject: [PATCH 079/102] Prepare for v1.1.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index a240fd1..f4cd40a 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.1.0" +(defproject io.github.manetu/temporal-sdk "1.1.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From d6695016935e49a50715b2ebbdc53ac61450dcd0 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 26 Nov 2024 17:43:41 -0500 Subject: [PATCH 080/102] Fix misplaced default in :workflow-task-timeout documentation Signed-off-by: Greg Haskins --- src/temporal/workflow.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/temporal/workflow.clj b/src/temporal/workflow.clj index 8f81a5a..932511f 100644 --- a/src/temporal/workflow.clj +++ b/src/temporal/workflow.clj @@ -134,9 +134,9 @@ Arguments: | :workflow-id | Workflow id to use when starting | String | | | :workflow-id-reuse-policy | Specifies server behavior if a completed workflow with the same id exists | See `workflow id reuse policy types` below | | | :parent-close-policy | Specifies how this workflow reacts to the death of the parent workflow | See `parent close policy types` below | | -| :workflow-execution-timeout | The time after which child workflow execution is automatically terminated | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10 seconds | +| :workflow-execution-timeout | The time after which child workflow execution is automatically terminated | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | | :workflow-run-timeout | The time after which child workflow run is automatically terminated | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | -| :workflow-task-timeout | Maximum execution time of a single workflow task | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | | +| :workflow-task-timeout | Maximum execution time of a single workflow task | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 10 seconds | | :retry-options | RetryOptions that define how child workflow is retried in case of failure | [[temporal.common/retry-options]] | | | :cron-schedule | A cron schedule string | String | | | :cancellation-type | In case of a child workflow cancellation it fails with a CanceledFailure | See `cancellation types` below | | From 436b82c42769a82b8edbf6c16588787c12edcf89 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Sun, 5 Jan 2025 13:24:19 -0500 Subject: [PATCH 081/102] Use jdk23 image instead of cimg/clojure, since the JDK is lagging Signed-off-by: Greg Haskins --- .circleci/config.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a7ec2eb..c201289 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,14 +7,17 @@ version: 2.1 jobs: build: docker: - - image: cimg/clojure:1.11.1 + - image: cimg/openjdk:23.0.1 steps: - checkout - run: | - lein version - lein cljfmt check - lein cloverage - lein jar + curl -sL https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein > ./lein + chmod +x ./lein + + ./lein version + ./lein cljfmt check + ./lein cloverage + ./lein jar workflows: build-workflow: From 1576bf49682ac8f9875e06ce55309e01598bdddf Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Sun, 5 Jan 2025 13:58:43 -0500 Subject: [PATCH 082/102] Update to temporal-sdk 1.27.0 and add support for vthreads Signed-off-by: Greg Haskins --- project.clj | 12 +++++------ src/temporal/client/worker.clj | 4 +++- test/temporal/test/vthreads.clj | 38 +++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 test/temporal/test/vthreads.clj diff --git a/project.clj b/project.clj index f4cd40a..37e8e1e 100644 --- a/project.clj +++ b/project.clj @@ -12,11 +12,11 @@ [jonase/eastwood "1.3.0"] [lein-codox "0.10.8"]] :dependencies [[org.clojure/clojure "1.11.4"] - [org.clojure/core.async "1.6.681"] - [io.temporal/temporal-sdk "1.25.1"] - [io.temporal/temporal-testing "1.25.1"] - [com.taoensso/encore "3.117.0"] - [com.taoensso/timbre "6.5.0"] + [org.clojure/core.async "1.7.701"] + [io.temporal/temporal-sdk "1.27.0"] + [io.temporal/temporal-testing "1.27.0"] + [com.taoensso/encore "3.133.0"] + [com.taoensso/timbre "6.6.1"] [com.taoensso/nippy "3.4.2"] [funcool/promesa "9.2.542"] [medley "1.4.0"] @@ -31,7 +31,7 @@ :profiles {:dev {:dependencies [[org.clojure/tools.namespace "1.5.0"] [eftest "0.6.0"] [mockery "0.1.4"] - [io.temporal/temporal-opentracing "1.25.1"]] + [io.temporal/temporal-opentracing "1.27.0"]] :resource-paths ["test/temporal/test/resources"]}} :cloverage {:runner :eftest :runner-opts {:multithread? false diff --git a/src/temporal/client/worker.clj b/src/temporal/client/worker.clj index 927681b..d875ed4 100644 --- a/src/temporal/client/worker.clj +++ b/src/temporal/client/worker.clj @@ -38,12 +38,14 @@ Options for configuring the worker-factory (See [[start]]) | :max-workflow-thread-count | Maximum number of threads available for workflow execution across all workers created by the Factory. | int | 600 | | :worker-interceptors | Collection of WorkerInterceptors | [WorkerInterceptor](https://javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/common/interceptors/WorkerInterceptor.html) | | | :workflow-cache-size | To avoid constant replay of code the workflow objects are cached on a worker. This cache is shared by all workers created by the Factory. | int | 600 | +| :using-virtual-workflow-threads | Use Virtual Threads for all workflow threads across all workers created by this factory. This option is only supported for JDK >= 21. If set then :max-workflow-thread-count is ignored. | boolean | false | " {:enable-logging-in-replay #(.setEnableLoggingInReplay ^WorkerFactoryOptions$Builder %1 %2) :max-workflow-thread-count #(.setMaxWorkflowThreadCount ^WorkerFactoryOptions$Builder %1 %2) :worker-interceptors #(.setWorkerInterceptors ^WorkerFactoryOptions$Builder %1 (into-array WorkerInterceptor %2)) - :workflow-cache-size #(.setWorkflowCacheSize ^WorkerFactoryOptions$Builder %1 %2)}) + :workflow-cache-size #(.setWorkflowCacheSize ^WorkerFactoryOptions$Builder %1 %2) + :using-virtual-workflow-threads #(.setUsingVirtualWorkflowThreads ^WorkerFactoryOptions$Builder %1 %2)}) (defn ^:no-doc worker-factory-options-> ^WorkerFactoryOptions [params] diff --git a/test/temporal/test/vthreads.clj b/test/temporal/test/vthreads.clj new file mode 100644 index 0000000..6f530df --- /dev/null +++ b/test/temporal/test/vthreads.clj @@ -0,0 +1,38 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.vthreads + (:require [clojure.test :refer :all] + [taoensso.timbre :as log] + [promesa.core :as p] + [promesa.exec :refer [vthreads-supported?]] + [temporal.client.core :as c] + [temporal.testing.env :as e] + [temporal.workflow :refer [defworkflow]]) + (:import [java.time Duration])) + +(def task-queue ::default) + +(defworkflow vthread-workflow + [args] + (-> (Thread/currentThread) + (.isVirtual))) + +(defn execute [opts] + (let [env (e/create opts) + client (e/get-client env) + _ (e/start env {:task-queue task-queue}) + workflow (c/create-workflow client vthread-workflow {:task-queue task-queue :workflow-execution-timeout (Duration/ofSeconds 1) :retry-options {:maximum-attempts 1}})] + (c/start workflow {}) + @(-> (c/get-result workflow) + (p/finally (fn [_ _] + (e/stop env)))))) + +(deftest the-test + (testing "Verifies that we do not use vthreads by default" + (is (false? (execute {})))) + (testing "Verifies that we do not use vthreads if we specifically disable them" + (is (false? (execute {:worker-factory-options {:using-virtual-workflow-threads false}})))) + (testing "Verifies that we can enable vthread support" + (if vthreads-supported? + (is (true? (execute {:worker-factory-options {:using-virtual-workflow-threads true}}))) + (log/info "vthreads require JDK >= 21, skipping test")))) From e124cea4ec6649e08f069d781b33030ce221dc38 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Sun, 5 Jan 2025 15:02:43 -0500 Subject: [PATCH 083/102] Release v1.2.0 Changes since v1.1.0 -------------------- 1576bf4 Update to temporal-sdk 1.27.0 and add support for vthreads 436b82c Use jdk23 image instead of cimg/clojure, since the JDK is lagging d669501 Fix misplaced default in :workflow-task-timeout documentation Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 37e8e1e..c7e9a11 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.1.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.2.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 98910c9b48a52597ba6e8529658604e380b5774e Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Sun, 5 Jan 2025 15:03:45 -0500 Subject: [PATCH 084/102] Prepare for v1.2.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index c7e9a11..4ba8db0 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.2.0" +(defproject io.github.manetu/temporal-sdk "1.2.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From df80a8144750fc4f11debdc2abe2b9d53c64464e Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Tue, 14 Jan 2025 18:56:42 -0500 Subject: [PATCH 085/102] [testenv] Add support for custom search attributes Signed-off-by: Greg Haskins --- project.clj | 2 +- src/temporal/internal/search_attributes.clj | 12 ++++++++ src/temporal/testing/env.clj | 16 ++++++++--- test/temporal/test/search_attributes.clj | 31 +++++++++++++++++++++ 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 src/temporal/internal/search_attributes.clj create mode 100644 test/temporal/test/search_attributes.clj diff --git a/project.clj b/project.clj index 4ba8db0..ff1691d 100644 --- a/project.clj +++ b/project.clj @@ -36,5 +36,5 @@ :cloverage {:runner :eftest :runner-opts {:multithread? false :fail-fast? true} - :fail-threshold 90 + :fail-threshold 91 :ns-exclude-regex [#"temporal.client.worker"]}) diff --git a/src/temporal/internal/search_attributes.clj b/src/temporal/internal/search_attributes.clj new file mode 100644 index 0000000..a73a102 --- /dev/null +++ b/src/temporal/internal/search_attributes.clj @@ -0,0 +1,12 @@ +(ns temporal.internal.search-attributes + (:import [io.temporal.api.enums.v1 IndexedValueType])) + +(def indexvalue-type-> + {:unspecified IndexedValueType/INDEXED_VALUE_TYPE_UNSPECIFIED + :text IndexedValueType/INDEXED_VALUE_TYPE_TEXT + :keyword IndexedValueType/INDEXED_VALUE_TYPE_KEYWORD + :int IndexedValueType/INDEXED_VALUE_TYPE_INT + :double IndexedValueType/INDEXED_VALUE_TYPE_DOUBLE + :bool IndexedValueType/INDEXED_VALUE_TYPE_BOOL + :datetime IndexedValueType/INDEXED_VALUE_TYPE_DATETIME + :keyword-list IndexedValueType/INDEXED_VALUE_TYPE_KEYWORD_LIST}) diff --git a/src/temporal/testing/env.clj b/src/temporal/testing/env.clj index fc0f247..cc77e17 100644 --- a/src/temporal/testing/env.clj +++ b/src/temporal/testing/env.clj @@ -2,16 +2,24 @@ (ns temporal.testing.env "Methods and utilities to assist with unit-testing Temporal workflows" - (:require [temporal.client.worker :as worker] + (:require [medley.core :as m] + [temporal.client.worker :as worker] [temporal.client.options :as copts] - [temporal.internal.utils :as u]) + [temporal.internal.utils :as u] + [temporal.internal.search-attributes :as search-attributes]) (:import [io.temporal.testing TestWorkflowEnvironment TestEnvironmentOptions TestEnvironmentOptions$Builder])) +(defn set-search-attributes [^TestEnvironmentOptions$Builder builder attributes] + (run! (fn [[name value]] + (.registerSearchAttribute builder name (search-attributes/indexvalue-type-> value))) + attributes)) + (def ^:no-doc test-env-options {:worker-factory-options #(.setWorkerFactoryOptions ^TestEnvironmentOptions$Builder %1 (worker/worker-factory-options-> %2)) :workflow-client-options #(.setWorkflowClientOptions ^TestEnvironmentOptions$Builder %1 (copts/workflow-client-options-> %2)) :workflow-service-stub-options #(.setWorkflowServiceStubsOptions ^TestEnvironmentOptions$Builder %1 (copts/stub-options-> %2)) - :metrics-scope #(.setMetricsScope ^TestEnvironmentOptions$Builder %1 %2)}) + :metrics-scope #(.setMetricsScope ^TestEnvironmentOptions$Builder %1 %2) + :search-attributes set-search-attributes}) (defn ^:no-doc test-env-options-> ^TestEnvironmentOptions [params] @@ -35,7 +43,7 @@ Arguments: | :workflow-client-options | | [[copts/client-options]] | | | :workflow-service-stub-options | | [[copts/stub-options]] | | | :metrics-scope | The scope to be used for metrics reporting | [Scope](https://github.com/uber-java/tally/blob/master/core/src/main/java/com/uber/m3/tally/Scope.java) | | - +| :search-attributes | Add a map of search attributes to be registered on the Temporal Server | map | | " ([] diff --git a/test/temporal/test/search_attributes.clj b/test/temporal/test/search_attributes.clj new file mode 100644 index 0000000..59bbb3f --- /dev/null +++ b/test/temporal/test/search_attributes.clj @@ -0,0 +1,31 @@ +;; Copyright © Manetu, Inc. All rights reserved + +(ns temporal.test.search-attributes + (:require [clojure.test :refer :all] + [temporal.client.core :as c] + [temporal.testing.env :as e] + [temporal.workflow :refer [defworkflow]]) + (:import [java.time Duration])) + +;; do not use the shared fixture, since we want to control the env creation + +(def task-queue ::default) + +(defworkflow searchable-workflow + [args] + :ok) + +(defn execute [] + (let [env (e/create {:search-attributes {"foo" :keyword}}) + client (e/get-client env) + _ (e/start env {:task-queue task-queue}) + workflow (c/create-workflow client searchable-workflow {:task-queue task-queue + :search-attributes {"foo" "bar"} + :workflow-execution-timeout (Duration/ofSeconds 1) + :retry-options {:maximum-attempts 1}})] + (c/start workflow {}) + @(c/get-result workflow))) + +(deftest the-test + (testing "Verifies that we can utilize custom search attributes" + (is (= (execute) :ok)))) \ No newline at end of file From 66b40f40ca57f4bb46ef302673f99368695e3766 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 17 Jan 2025 10:50:48 -0500 Subject: [PATCH 086/102] Release v1.2.1 Changes since v1.2.0 -------------------- df80a81 [testenv] Add support for custom search attributes Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index ff1691d..b46ca17 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.2.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.2.1" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 0592699b771a8b68330cf766ab6ef3dc875eaef9 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 17 Jan 2025 10:51:38 -0500 Subject: [PATCH 087/102] Prepare for v1.2.2 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index b46ca17..cd8e9d4 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.2.1" +(defproject io.github.manetu/temporal-sdk "1.2.2-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From c4730498678074bee10f1d3b9316f950a6aff8db Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 21 Mar 2025 18:42:49 -0400 Subject: [PATCH 088/102] Update deps Signed-off-by: Greg Haskins --- project.clj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/project.clj b/project.clj index cd8e9d4..7d7963b 100644 --- a/project.clj +++ b/project.clj @@ -11,11 +11,11 @@ [lein-cloverage "1.2.4"] [jonase/eastwood "1.3.0"] [lein-codox "0.10.8"]] - :dependencies [[org.clojure/clojure "1.11.4"] + :dependencies [[org.clojure/clojure "1.12.0"] [org.clojure/core.async "1.7.701"] - [io.temporal/temporal-sdk "1.27.0"] - [io.temporal/temporal-testing "1.27.0"] - [com.taoensso/encore "3.133.0"] + [io.temporal/temporal-sdk "1.28.3"] + [io.temporal/temporal-testing "1.28.3"] + [com.taoensso/encore "3.139.0"] [com.taoensso/timbre "6.6.1"] [com.taoensso/nippy "3.4.2"] [funcool/promesa "9.2.542"] @@ -31,7 +31,7 @@ :profiles {:dev {:dependencies [[org.clojure/tools.namespace "1.5.0"] [eftest "0.6.0"] [mockery "0.1.4"] - [io.temporal/temporal-opentracing "1.27.0"]] + [io.temporal/temporal-opentracing "1.28.3"]] :resource-paths ["test/temporal/test/resources"]}} :cloverage {:runner :eftest :runner-opts {:multithread? false From 23ca643a0185c54ba4cd23911c2781a11990ddaa Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 21 Mar 2025 19:49:57 -0400 Subject: [PATCH 089/102] Release v1.3.0 Changes since v1.2.1 -------------------- 6280958 Update deps Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 7d7963b..4bf3915 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.2.2-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.3.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From ad76f6c7d8ae3ab6eedf5f11f3c23bb7de66e989 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Fri, 21 Mar 2025 19:51:32 -0400 Subject: [PATCH 090/102] Prepare for v1.3.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 4bf3915..55999c1 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.3.0" +(defproject io.github.manetu/temporal-sdk "1.3.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From f9a0cca1170f94e7cf855486f26397ba9d0b6d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Mon, 28 Jul 2025 12:43:36 +0200 Subject: [PATCH 091/102] Add support for start-delay option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: KARASZI István --- src/temporal/internal/workflow.clj | 3 +- test/temporal/test/start_delay.clj | 50 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/temporal/test/start_delay.clj diff --git a/src/temporal/internal/workflow.clj b/src/temporal/internal/workflow.clj index 1d9c2cb..d1a1d5c 100644 --- a/src/temporal/internal/workflow.clj +++ b/src/temporal/internal/workflow.clj @@ -53,7 +53,8 @@ :retry-options #(.setRetryOptions %1 (common/retry-options-> %2)) :cron-schedule #(.setCronSchedule ^WorkflowOptions$Builder %1 %2) :memo #(.setMemo ^WorkflowOptions$Builder %1 %2) - :search-attributes #(.setSearchAttributes ^WorkflowOptions$Builder %1 %2)}) + :search-attributes #(.setSearchAttributes ^WorkflowOptions$Builder %1 %2) + :start-delay #(.setStartDelay ^WorkflowOptions$Builder %1 %2)}) (defn ^:no-doc wf-options-> ^WorkflowOptions [params] diff --git a/test/temporal/test/start_delay.clj b/test/temporal/test/start_delay.clj new file mode 100644 index 0000000..79fa9b3 --- /dev/null +++ b/test/temporal/test/start_delay.clj @@ -0,0 +1,50 @@ +(ns temporal.test.start-delay + (:require + [clojure.test :refer :all] + [temporal.client.core :as c] + [temporal.test.utils :as t] + [temporal.workflow :refer [defworkflow]]) + (:import + [io.temporal.client WorkflowStub] + [java.time Duration Instant] + [java.time.temporal ChronoUnit TemporalAmount])) + +(set! *warn-on-reflection* true) + +(use-fixtures :once t/wrap-service) + +(defworkflow delay-workflow + [_args] + :ok) + +(defn create [options] + (let [options (assoc options :task-queue t/task-queue) + wf (c/create-workflow (t/get-client) delay-workflow options)] + (c/start wf nil) + wf)) + +(defn get-execution-time ^Instant [workflow] + (.. ^WorkflowStub (:stub workflow) + describe + getExecutionTime)) + +(defn from-now ^Instant [^TemporalAmount delay] + (.. (Instant/now) + (plus delay) + (truncatedTo ChronoUnit/SECONDS))) + +(deftest the-test + (testing "Verifies that the workflow got delayed" + (let [start-delay (Duration/ofHours 2) + expected (from-now start-delay) + wf (create {:workflow-id "delayed-workflow" + :start-delay start-delay}) + execution-time (get-execution-time wf)] + (is (.isBefore expected execution-time))))) + +(deftest no-delay + (testing "Verifies that the workflow is not delayed per default" + (let [wf (create {:workflow-id "non-delayed-workflow"}) + execution-time (get-execution-time wf) + now (from-now (Duration/ofSeconds 1))] + (is (.isAfter now execution-time))))) From 39484ad010948b315566bde860c0d4c5a26be8af Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Sun, 24 Aug 2025 10:34:17 -0400 Subject: [PATCH 092/102] Release v1.3.1 Changes since v1.3.0 -------------------- f9a0cca Add support for start-delay option Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 55999c1..f6e81c6 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.3.1-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.3.1" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From e2c2bb472231c2d210325c68d08e40af03e5db2a Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Sun, 24 Aug 2025 10:35:04 -0400 Subject: [PATCH 093/102] Prepare for v1.3.2 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index f6e81c6..d915147 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.3.1" +(defproject io.github.manetu/temporal-sdk "1.3.2-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 29be1c8eb30e8e5f06b2496fc4f5a008d27665a5 Mon Sep 17 00:00:00 2001 From: Chris Dean Date: Sat, 16 Aug 2025 15:50:09 -0700 Subject: [PATCH 094/102] Remove unicode non-breaking space Both `.setCalendars` and `.setIntervals` had a unicode space after the method names that was breaking the Java interop. Signed-off-by: Chris Dean --- src/temporal/internal/schedule.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/temporal/internal/schedule.clj b/src/temporal/internal/schedule.clj index eaad319..dd20138 100644 --- a/src/temporal/internal/schedule.clj +++ b/src/temporal/internal/schedule.clj @@ -39,9 +39,9 @@ (u/build (SchedulePolicy/newBuilder) schedule-policy-spec params)) (def schedule-spec-spec - {:calendars #(.setCalendars​ ^ScheduleSpec$Builder %1 %2) + {:calendars #(.setCalendars ^ScheduleSpec$Builder %1 %2) :cron-expressions #(.setCronExpressions ^ScheduleSpec$Builder %1 %2) - :intervals #(.setIntervals​ ^ScheduleSpec$Builder %1 %2) + :intervals #(.setIntervals ^ScheduleSpec$Builder %1 %2) :end-at #(.setEndAt ^ScheduleSpec$Builder %1 %2) :jitter #(.setJitter ^ScheduleSpec$Builder %1 %2) :skip-at #(.setSkip ^ScheduleSpec$Builder %1 %2) From b899dfbe3995cbc74eb8c573a53588cbc8266757 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Sun, 24 Aug 2025 10:42:01 -0400 Subject: [PATCH 095/102] Release v1.3.2 Changes since v1.3.1 -------------------- 29be1c8 Remove unicode non-breaking space Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index d915147..1411e2b 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.3.2-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.3.2" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From d2ff95bfad839d4c7f24cd372680fc3842d47ac3 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Sun, 24 Aug 2025 10:42:41 -0400 Subject: [PATCH 096/102] Prepare for v1.3.3 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 1411e2b..bd0651b 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.3.2" +(defproject io.github.manetu/temporal-sdk "1.3.3-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From eb4ca1dc0745c5346465bb432fa7b6ed14b8f41f Mon Sep 17 00:00:00 2001 From: Felipe Cortez Date: Tue, 16 Sep 2025 20:19:28 -0300 Subject: [PATCH 097/102] Add Nexus, Virtual Threads WorkerOptions to the worker-options map also aligns markdown table Signed-off-by: Felipe Cortez --- src/temporal/client/worker.clj | 49 ++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/temporal/client/worker.clj b/src/temporal/client/worker.clj index d875ed4..391e718 100644 --- a/src/temporal/client/worker.clj +++ b/src/temporal/client/worker.clj @@ -55,22 +55,30 @@ Options for configuring the worker-factory (See [[start]]) " Options for configuring workers (See [[start]]) -| Value | Mandatory | Description | Type | Default | -| ------------ | ----------- | ----------------------------------------------------------------- | ---------------- | ------- | -| :task-queue | y | The name of the task-queue for this worker instance to listen on. | String / keyword | | -| :ctx | | An opaque handle that is passed back as the first argument of [[temporal.workflow/defworkflow]] and [[temporal.activity/defactivity]], useful for passing state such as database or network connections. | | nil | -| :dispatch | | An optional map explicitly setting the dispatch table | See below | All visible activities/workers are automatically registered | -| :max-concurrent-activity-task-pollers | | Number of simultaneous poll requests on activity task queue. Consider incrementing if the worker is not throttled due to `MaxActivitiesPerSecond` or `MaxConcurrentActivityExecutionSize` options and still cannot keep up with the request rate. | int | 5 | -| :max-concurrent-activity-execution-size | | Maximum number of activities executed in parallel. | int | 200 | -| :max-concurrent-local-activity-execution-size | | Maximum number of local activities executed in parallel. | int | 200 | -| :max-concurrent-workflow-task-pollers | | Number of simultaneous poll requests on workflow task queue. | int | 2 | -| :max-concurrent-workflow-task-execution-size | | Maximum number of simultaneously executed workflow tasks. | int | 200 | -| :default-deadlock-detection-timeout | | Time period in ms that will be used to detect workflow deadlock. | long | 1000 | -| :default-heartbeat-throttle-interval | | Default amount of time between sending each pending heartbeat. | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 30s | -| :max-heartbeat-throttle-interval | | Maximum amount of time between sending each pending heartbeat. | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | -| :local-activity-worker-only | | Worker should only handle workflow tasks and local activities. | boolean | false | -| :max-taskqueue-activities-per-second | | Sets the rate limiting on number of activities per second. | double | 0.0 (unlimited) | -| :max-workers-activities-per-second | | Maximum number of activities started per second. | double | 0.0 (unlimited) | +| Value | Mandatory | Description | Type | Default | +|-------------------------------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|-------------------------------------------------------------| +| :task-queue | y | The name of the task-queue for this worker instance to listen on. | String / keyword | | +| :ctx | | An opaque handle that is passed back as the first argument of [[temporal.workflow/defworkflow]] and [[temporal.activity/defactivity]], useful for passing state such as database or network connections. | | nil | +| :dispatch | | An optional map explicitly setting the dispatch table | See below | All visible activities/workers are automatically registered | +| :max-concurrent-activity-task-pollers | | Number of simultaneous poll requests on activity task queue. Consider incrementing if the worker is not throttled due to `MaxActivitiesPerSecond` or `MaxConcurrentActivityExecutionSize` options and still cannot keep up with the request rate. | int | 5 | +| :max-concurrent-activity-execution-size | | Maximum number of activities executed in parallel. | int | 200 | +| :max-concurrent-local-activity-execution-size | | Maximum number of local activities executed in parallel. | int | 200 | +| :max-concurrent-activity-execution-size | | Maximum number of activities executed in parallel. | int | 200 | +| :max-concurrent-workflow-task-pollers | | Number of simultaneous poll requests on workflow task queue. | int | 5 | +| :max-concurrent-workflow-task-execution-size | | Maximum number of simultaneously executed workflow tasks. | int | 200 | +| :max-concurrent-nexus-execution-size | | Maximum number of simultaneously executed nexus tasks. | int | 200 | +| :max-concurrent-nexus-task-pollers | | Number of simultaneous poll requests on nexus tasks. | int | 5 | +| :default-deadlock-detection-timeout | | Time period in ms that will be used to detect workflow deadlock. | long | 1000 | +| :default-heartbeat-throttle-interval | | Default amount of time between sending each pending heartbeat. | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 30s | +| :max-heartbeat-throttle-interval | | Maximum amount of time between sending each pending heartbeat. | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | +| :local-activity-worker-only | | Worker should only handle workflow tasks and local activities. | boolean | false | +| :max-taskqueue-activities-per-second | | Sets the rate limiting on number of activities per second. | double | 0.0 (unlimited) | +| :max-workers-activities-per-second | | Maximum number of activities started per second. | double | 0.0 (unlimited) | +| :using-virtual-threads | | Use virtual threads for all the task executors created by this worker. | boolean | false | +| :using-virtual-threads-on-activity-worker | | Use virtual threads for the activity task executors created by this worker. | boolean | false | +| :using-virtual-threads-on-local-activity-worker | | Use virtual threads for the local activity task executors created by this worker. | boolean | false | +| :using-virtual-threads-on-nexus-worker | | Use Virtual Threads for the Nexus task executors created by this worker. | boolean | false | +| :using-virtual-threads-on-workflow-worker | | Use Virtual Threads for the Workflow task executors created by this worker. | boolean | false | #### dispatch-table @@ -93,12 +101,19 @@ Options for configuring workers (See [[start]]) :max-concurrent-local-activity-execution-size #(.setMaxConcurrentLocalActivityExecutionSize ^WorkerOptions$Builder %1 %2) :max-concurrent-workflow-task-pollers #(.setMaxConcurrentWorkflowTaskPollers ^WorkerOptions$Builder %1 %2) :max-concurrent-workflow-task-execution-size #(.setMaxConcurrentWorkflowTaskExecutionSize ^WorkerOptions$Builder %1 %2) + :max-concurrent-nexus-execution-size #(.setMaxConcurrentNexusExecutionSize ^WorkerOptions$Builder %1 %2) + :max-concurrent-nexus-task-pollers #(.setMaxConcurrentNexusTaskPollers ^WorkerOptions$Builder %1 %2) :default-deadlock-detection-timeout #(.setDefaultDeadlockDetectionTimeout ^WorkerOptions$Builder %1 %2) :default-heartbeat-throttle-interval #(.setDefaultHeartbeatThrottleInterval ^WorkerOptions$Builder %1 %2) :max-heartbeat-throttle-interval #(.setMaxHeartbeatThrottleInterval ^WorkerOptions$Builder %1 %2) :local-activity-worker-only #(.setLocalActivityWorkerOnly ^WorkerOptions$Builder %1 %2) :max-taskqueue-activities-per-second #(.setMaxTaskQueueActivitiesPerSecond ^WorkerOptions$Builder %1 %2) - :max-workers-activities-per-second #(.setMaxWorkerActivitiesPerSecond ^WorkerOptions$Builder %1 %2)}) + :max-workers-activities-per-second #(.setMaxWorkerActivitiesPerSecond ^WorkerOptions$Builder %1 %2) + :using-virtual-threads #(.setUsingVirtualThreads ^WorkerOptions$Builder %1 %2) + :using-virtual-threads-on-activity-worker #(.setUsingVirtualThreadsOnActivityWorker ^WorkerOptions$Builder %1 %2) + :using-virtual-threads-on-local-activity-worker #(.setUsingVirtualThreadsOnLocalActivityWorker ^WorkerOptions$Builder %1 %2) + :using-virtual-threads-on-nexus-worker #(.setUsingVirtualThreadsOnNexusWorker ^WorkerOptions$Builder %1 %2) + :using-virtual-threads-on-workflow-worker #(.setUsingVirtualThreadsOnWorkflowWorker ^WorkerOptions$Builder %1 %2)}) (defn ^:no-doc worker-options-> ^WorkerOptions [params] From afebc0682d368463ec03f7ee6aa029bff5d4dbbe Mon Sep 17 00:00:00 2001 From: Felipe Cortez Date: Fri, 19 Sep 2025 11:12:35 -0300 Subject: [PATCH 098/102] Sort `worker-option` lines alphabetically in both code and docs Signed-off-by: Felipe Cortez --- src/temporal/client/worker.clj | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/temporal/client/worker.clj b/src/temporal/client/worker.clj index 391e718..997dbea 100644 --- a/src/temporal/client/worker.clj +++ b/src/temporal/client/worker.clj @@ -60,18 +60,18 @@ Options for configuring workers (See [[start]]) | :task-queue | y | The name of the task-queue for this worker instance to listen on. | String / keyword | | | :ctx | | An opaque handle that is passed back as the first argument of [[temporal.workflow/defworkflow]] and [[temporal.activity/defactivity]], useful for passing state such as database or network connections. | | nil | | :dispatch | | An optional map explicitly setting the dispatch table | See below | All visible activities/workers are automatically registered | -| :max-concurrent-activity-task-pollers | | Number of simultaneous poll requests on activity task queue. Consider incrementing if the worker is not throttled due to `MaxActivitiesPerSecond` or `MaxConcurrentActivityExecutionSize` options and still cannot keep up with the request rate. | int | 5 | +| :default-deadlock-detection-timeout | | Time period in ms that will be used to detect workflow deadlock. | long | 1000 | +| :default-heartbeat-throttle-interval | | Default amount of time between sending each pending heartbeat. | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 30s | +| :local-activity-worker-only | | Worker should only handle workflow tasks and local activities. | boolean | false | | :max-concurrent-activity-execution-size | | Maximum number of activities executed in parallel. | int | 200 | -| :max-concurrent-local-activity-execution-size | | Maximum number of local activities executed in parallel. | int | 200 | | :max-concurrent-activity-execution-size | | Maximum number of activities executed in parallel. | int | 200 | -| :max-concurrent-workflow-task-pollers | | Number of simultaneous poll requests on workflow task queue. | int | 5 | -| :max-concurrent-workflow-task-execution-size | | Maximum number of simultaneously executed workflow tasks. | int | 200 | +| :max-concurrent-activity-task-pollers | | Number of simultaneous poll requests on activity task queue. Consider incrementing if the worker is not throttled due to `MaxActivitiesPerSecond` or `MaxConcurrentActivityExecutionSize` options and still cannot keep up with the request rate. | int | 5 | +| :max-concurrent-local-activity-execution-size | | Maximum number of local activities executed in parallel. | int | 200 | | :max-concurrent-nexus-execution-size | | Maximum number of simultaneously executed nexus tasks. | int | 200 | | :max-concurrent-nexus-task-pollers | | Number of simultaneous poll requests on nexus tasks. | int | 5 | -| :default-deadlock-detection-timeout | | Time period in ms that will be used to detect workflow deadlock. | long | 1000 | -| :default-heartbeat-throttle-interval | | Default amount of time between sending each pending heartbeat. | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 30s | +| :max-concurrent-workflow-task-execution-size | | Maximum number of simultaneously executed workflow tasks. | int | 200 | +| :max-concurrent-workflow-task-pollers | | Number of simultaneous poll requests on workflow task queue. | int | 5 | | :max-heartbeat-throttle-interval | | Maximum amount of time between sending each pending heartbeat. | [Duration](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html) | 60s | -| :local-activity-worker-only | | Worker should only handle workflow tasks and local activities. | boolean | false | | :max-taskqueue-activities-per-second | | Sets the rate limiting on number of activities per second. | double | 0.0 (unlimited) | | :max-workers-activities-per-second | | Maximum number of activities started per second. | double | 0.0 (unlimited) | | :using-virtual-threads | | Use virtual threads for all the task executors created by this worker. | boolean | false | @@ -96,17 +96,17 @@ Options for configuring workers (See [[start]]) ``` " - {:max-concurrent-activity-task-pollers #(.setMaxConcurrentActivityTaskPollers ^WorkerOptions$Builder %1 %2) + {:default-deadlock-detection-timeout #(.setDefaultDeadlockDetectionTimeout ^WorkerOptions$Builder %1 %2) + :default-heartbeat-throttle-interval #(.setDefaultHeartbeatThrottleInterval ^WorkerOptions$Builder %1 %2) + :local-activity-worker-only #(.setLocalActivityWorkerOnly ^WorkerOptions$Builder %1 %2) :max-concurrent-activity-execution-size #(.setMaxConcurrentActivityExecutionSize ^WorkerOptions$Builder %1 %2) + :max-concurrent-activity-task-pollers #(.setMaxConcurrentActivityTaskPollers ^WorkerOptions$Builder %1 %2) :max-concurrent-local-activity-execution-size #(.setMaxConcurrentLocalActivityExecutionSize ^WorkerOptions$Builder %1 %2) - :max-concurrent-workflow-task-pollers #(.setMaxConcurrentWorkflowTaskPollers ^WorkerOptions$Builder %1 %2) - :max-concurrent-workflow-task-execution-size #(.setMaxConcurrentWorkflowTaskExecutionSize ^WorkerOptions$Builder %1 %2) :max-concurrent-nexus-execution-size #(.setMaxConcurrentNexusExecutionSize ^WorkerOptions$Builder %1 %2) :max-concurrent-nexus-task-pollers #(.setMaxConcurrentNexusTaskPollers ^WorkerOptions$Builder %1 %2) - :default-deadlock-detection-timeout #(.setDefaultDeadlockDetectionTimeout ^WorkerOptions$Builder %1 %2) - :default-heartbeat-throttle-interval #(.setDefaultHeartbeatThrottleInterval ^WorkerOptions$Builder %1 %2) + :max-concurrent-workflow-task-execution-size #(.setMaxConcurrentWorkflowTaskExecutionSize ^WorkerOptions$Builder %1 %2) + :max-concurrent-workflow-task-pollers #(.setMaxConcurrentWorkflowTaskPollers ^WorkerOptions$Builder %1 %2) :max-heartbeat-throttle-interval #(.setMaxHeartbeatThrottleInterval ^WorkerOptions$Builder %1 %2) - :local-activity-worker-only #(.setLocalActivityWorkerOnly ^WorkerOptions$Builder %1 %2) :max-taskqueue-activities-per-second #(.setMaxTaskQueueActivitiesPerSecond ^WorkerOptions$Builder %1 %2) :max-workers-activities-per-second #(.setMaxWorkerActivitiesPerSecond ^WorkerOptions$Builder %1 %2) :using-virtual-threads #(.setUsingVirtualThreads ^WorkerOptions$Builder %1 %2) From a5f587c7c8dd938f59050e022916a78325a241b5 Mon Sep 17 00:00:00 2001 From: Felipe Cortez Date: Fri, 19 Sep 2025 14:09:54 -0300 Subject: [PATCH 099/102] Use passed worker options in `temporal.testing.env` Signed-off-by: Felipe Cortez --- src/temporal/testing/env.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/temporal/testing/env.clj b/src/temporal/testing/env.clj index cc77e17..15810a4 100644 --- a/src/temporal/testing/env.clj +++ b/src/temporal/testing/env.clj @@ -67,7 +67,7 @@ Arguments: ``` " [env {:keys [task-queue] :as options}] - (let [worker (.newWorker env (u/namify task-queue))] + (let [worker (.newWorker env (u/namify task-queue) (worker/worker-options-> options))] (worker/init worker options) (.start env) worker)) From 324a6ad54e7ba12837b5a8186ad83bf7d08efd84 Mon Sep 17 00:00:00 2001 From: Felipe Cortez Date: Fri, 19 Sep 2025 16:11:58 -0300 Subject: [PATCH 100/102] Add tests for `worker-options` vthreads Signed-off-by: Felipe Cortez --- test/temporal/test/vthreads.clj | 60 +++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/test/temporal/test/vthreads.clj b/test/temporal/test/vthreads.clj index 6f530df..2a304e4 100644 --- a/test/temporal/test/vthreads.clj +++ b/test/temporal/test/vthreads.clj @@ -1,10 +1,12 @@ ;; Copyright © Manetu, Inc. All rights reserved (ns temporal.test.vthreads - (:require [clojure.test :refer :all] - [taoensso.timbre :as log] + (:require [clojure.string :as string] + [clojure.test :refer :all] [promesa.core :as p] [promesa.exec :refer [vthreads-supported?]] + [taoensso.timbre :as log] + [temporal.activity :refer [defactivity] :as a] [temporal.client.core :as c] [temporal.testing.env :as e] [temporal.workflow :refer [defworkflow]]) @@ -12,27 +14,49 @@ (def task-queue ::default) +(defactivity collect-platform-threads [_ _] + (->> (Thread/getAllStackTraces) + (keys) + (map Thread/.getName) + (into #{}))) + (defworkflow vthread-workflow - [args] - (-> (Thread/currentThread) - (.isVirtual))) + [_args] + {:virtual-worker-thread? (.isVirtual (Thread/currentThread)) + :platform-threads @(a/invoke collect-platform-threads nil)}) -(defn execute [opts] - (let [env (e/create opts) +(defn execute [backend-opts worker-opts] + (let [env (e/create backend-opts) client (e/get-client env) - _ (e/start env {:task-queue task-queue}) - workflow (c/create-workflow client vthread-workflow {:task-queue task-queue :workflow-execution-timeout (Duration/ofSeconds 1) :retry-options {:maximum-attempts 1}})] - (c/start workflow {}) + _ (e/start env (merge {:task-queue task-queue} worker-opts)) + workflow (c/create-workflow client vthread-workflow {:task-queue task-queue :workflow-execution-timeout (Duration/ofSeconds 1) :retry-options {:maximum-attempts 1}}) + _ (c/start workflow {})] @(-> (c/get-result workflow) - (p/finally (fn [_ _] - (e/stop env)))))) + (p/finally (fn [_ _] (e/synchronized-stop env)))))) + +(defn- substring-in-coll? [substr coll] + (boolean (some (fn [s] (string/includes? s substr)) coll))) (deftest the-test (testing "Verifies that we do not use vthreads by default" - (is (false? (execute {})))) + (is (false? (:virtual-worker-thread? (execute {} {}))))) (testing "Verifies that we do not use vthreads if we specifically disable them" - (is (false? (execute {:worker-factory-options {:using-virtual-workflow-threads false}})))) - (testing "Verifies that we can enable vthread support" - (if vthreads-supported? - (is (true? (execute {:worker-factory-options {:using-virtual-workflow-threads true}}))) - (log/info "vthreads require JDK >= 21, skipping test")))) + (is (false? (:virtual-worker-thread? (execute {:worker-factory-options {:using-virtual-workflow-threads false}} + {}))))) + (if-not vthreads-supported? + (log/info "vthreads require JDK >= 21, skipping tests") + (testing "Verifies that we can enable vthread support" + (is (true? (:virtual-worker-thread? + (execute {:worker-factory-options {:using-virtual-workflow-threads true}} + {})))) + + (testing "Verifies that Poller and Executor threads can be turned into vthreads using worker-options" + (let [pthreads (:platform-threads (execute {} {:using-virtual-threads true}))] + (is (not-any? #(substring-in-coll? % pthreads) + ["Workflow Executor" "Activity Executor" + "Workflow Poller" "Activity Poller"]))) + + (let [pthreads (:platform-threads (execute {} {:using-virtual-threads false}))] + (is (every? #(substring-in-coll? % pthreads) + ["Workflow Executor" "Activity Executor" + "Workflow Poller" "Activity Poller"]))))))) From 72a32dcd4ddd81ab06f0968907dda2259c930c79 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 25 Sep 2025 07:50:37 -0700 Subject: [PATCH 101/102] Release v1.4.0 Changes since v1.3.2 -------------------- 324a6ad Add tests for `worker-options` vthreads a5f587c Use passed worker options in `temporal.testing.env` afebc06 Sort `worker-option` lines alphabetically in both code and docs eb4ca1d Add Nexus, Virtual Threads WorkerOptions to the worker-options map Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index bd0651b..58ca38f 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.3.3-SNAPSHOT" +(defproject io.github.manetu/temporal-sdk "1.4.0" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0" From 51be94b1527691effd0d1bab1db25a2d1d116183 Mon Sep 17 00:00:00 2001 From: Greg Haskins Date: Thu, 25 Sep 2025 07:52:22 -0700 Subject: [PATCH 102/102] Prepare for v1.4.1 development Signed-off-by: Greg Haskins --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 58ca38f..5b08be1 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.manetu/temporal-sdk "1.4.0" +(defproject io.github.manetu/temporal-sdk "1.4.1-SNAPSHOT" :description "A Temporal SDK for Clojure" :url "https://github.com/manetu/temporal-clojure-sdk" :license {:name "Apache License 2.0"