From e839219b21ceb976bf7ec4417853a0205606d6e0 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 11 Oct 2023 11:11:52 -0700 Subject: [PATCH 01/27] Update Core --- temporalio/bridge/Cargo.lock | 310 +++++++++++++++++--------------- temporalio/bridge/sdk-core | 2 +- temporalio/bridge/src/worker.rs | 8 +- 3 files changed, 167 insertions(+), 153 deletions(-) diff --git a/temporalio/bridge/Cargo.lock b/temporalio/bridge/Cargo.lock index 4fa15f8f..7a73f865 100644 --- a/temporalio/bridge/Cargo.lock +++ b/temporalio/bridge/Cargo.lock @@ -41,9 +41,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -54,12 +54,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "anyhow" version = "1.0.75" @@ -91,7 +85,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] @@ -102,7 +96,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] @@ -184,9 +178,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.2" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "base64ct" @@ -217,21 +211,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bzip2" @@ -272,11 +266,10 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ - "android-tzdata", "num-traits", "serde", ] @@ -451,12 +444,12 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.1" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "lock_api", "once_cell", "parking_lot_core", @@ -567,7 +560,7 @@ checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] @@ -579,7 +572,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] @@ -590,39 +583,28 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "837c0466252947ada828b975e12daf82e18bb5444e4df87be6038d4469e2a3d2" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" dependencies = [ "serde", ] [[package]] name = "errno" -version = "0.3.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ - "errno-dragonfly", "libc", "windows-sys", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "filetime" @@ -738,7 +720,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] @@ -860,9 +842,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" dependencies = [ "ahash", "allocator-api2", @@ -876,9 +858,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hmac" @@ -889,6 +871,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + [[package]] name = "http" version = "0.2.9" @@ -1001,12 +992,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.1", ] [[package]] @@ -1035,9 +1026,9 @@ dependencies = [ [[package]] name = "inventory" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a53088c87cf71c9d4f3372a2cb9eea1e7b8a0b1bf8b7f7d23fe5b76dbb07e63b" +checksum = "e1be380c410bf0595e94992a648ea89db4dd3f3354ba54af206fd2a68cf5ac8e" [[package]] name = "ipnet" @@ -1071,9 +1062,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] @@ -1095,15 +1086,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "lock_api" @@ -1123,11 +1114,11 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lru" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eedb2bdbad7e0634f83989bf596f497b070130daaa398ab22d84c39e266deec5" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" dependencies = [ - "hashbrown 0.14.0", + "hashbrown 0.14.1", ] [[package]] @@ -1150,15 +1141,15 @@ dependencies = [ [[package]] name = "matchit" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" @@ -1278,9 +1269,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -1297,9 +1288,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -1420,9 +1411,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "3.9.1" +version = "3.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" dependencies = [ "num-traits", ] @@ -1492,7 +1483,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.0.0", + "indexmap 2.0.2", ] [[package]] @@ -1512,14 +1503,14 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] name = "pin-project-lite" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1581,9 +1572,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -1857,14 +1848,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.3" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.6", - "regex-syntax 0.7.4", + "regex-automata 0.4.1", + "regex-syntax 0.8.1", ] [[package]] @@ -1878,13 +1869,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.6" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.4", + "regex-syntax 0.8.1", ] [[package]] @@ -1895,15 +1886,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "56d84fdd47036b038fc80dd333d10b6aab10d5d31f4a366e20014def75328d33" [[package]] name = "reqwest" -version = "0.11.20" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "base64", "bytes", @@ -1927,6 +1918,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-rustls", "tokio-util", @@ -1995,7 +1987,7 @@ dependencies = [ "proc-macro2", "quote", "rustfsm_trait", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] @@ -2004,9 +1996,9 @@ version = "0.1.0" [[package]] name = "rustix" -version = "0.38.8" +version = "0.38.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" dependencies = [ "bitflags 2.4.0", "errno", @@ -2017,9 +2009,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.6" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", @@ -2050,9 +2042,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.4" +version = "0.101.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" dependencies = [ "ring", "untrusted", @@ -2120,35 +2112,35 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -2169,9 +2161,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -2180,9 +2172,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -2191,9 +2183,9 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] @@ -2233,9 +2225,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" @@ -2249,9 +2241,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys", @@ -2288,9 +2280,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -2303,6 +2295,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tar" version = "0.4.40" @@ -2404,6 +2417,7 @@ dependencies = [ "hyper", "itertools 0.11.0", "lazy_static", + "log", "lru", "mockall", "nix", @@ -2490,22 +2504,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] @@ -2520,9 +2534,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.27" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", "serde", @@ -2531,9 +2545,9 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tinyvec" @@ -2552,9 +2566,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ "backtrace", "bytes", @@ -2564,7 +2578,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", + "socket2 0.5.4", "tokio-macros", "windows-sys", ] @@ -2587,7 +2601,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] @@ -2613,9 +2627,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -2723,7 +2737,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] @@ -2784,9 +2798,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "typetag" @@ -2809,7 +2823,7 @@ checksum = "bfc13d450dc4a695200da3074dacf43d449b968baee95e341920e47f61a3b40f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", ] [[package]] @@ -2820,9 +2834,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -2847,9 +2861,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -2919,7 +2933,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -2953,7 +2967,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2995,13 +3009,14 @@ checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "which" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", - "libc", + "home", "once_cell", + "rustix", ] [[package]] @@ -3152,11 +3167,10 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" +version = "2.0.9+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" dependencies = [ "cc", - "libc", "pkg-config", ] diff --git a/temporalio/bridge/sdk-core b/temporalio/bridge/sdk-core index 617612aa..dd610947 160000 --- a/temporalio/bridge/sdk-core +++ b/temporalio/bridge/sdk-core @@ -1 +1 @@ -Subproject commit 617612aa419d687aebabcf0258ac86f5c36df189 +Subproject commit dd610947468ec760601b57c93f76532d08739168 diff --git a/temporalio/bridge/src/worker.rs b/temporalio/bridge/src/worker.rs index 5d8be94a..151d7ce9 100644 --- a/temporalio/bridge/src/worker.rs +++ b/temporalio/bridge/src/worker.rs @@ -5,7 +5,7 @@ use pyo3::types::{PyBytes, PyTuple}; use std::sync::Arc; use std::time::Duration; use temporal_sdk_core::api::errors::{PollActivityError, PollWfError}; -use temporal_sdk_core::replay::HistoryForReplay; +use temporal_sdk_core::replay::{HistoryForReplay, ReplayWorkerInput}; use temporal_sdk_core_api::Worker; use temporal_sdk_core_protos::coresdk::workflow_completion::WorkflowActivationCompletion; use temporal_sdk_core_protos::coresdk::{ActivityHeartbeat, ActivityTaskCompletion}; @@ -85,9 +85,9 @@ pub fn new_replay_worker<'a>( let (history_pusher, stream) = HistoryPusher::new(runtime_ref.runtime.clone()); let worker = WorkerRef { worker: Some(Arc::new( - temporal_sdk_core::init_replay_worker(config, stream).map_err(|err| { - PyValueError::new_err(format!("Failed creating replay worker: {}", err)) - })?, + temporal_sdk_core::init_replay_worker(ReplayWorkerInput::new(config, stream)).map_err( + |err| PyValueError::new_err(format!("Failed creating replay worker: {}", err)), + )?, )), runtime: runtime_ref.runtime.clone(), }; From cf7cc9be9b74d6e10eeadeab980e5ecd82d7117d Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 11 Oct 2023 15:25:55 -0700 Subject: [PATCH 02/27] Add docker image for proto building --- .dockerignore | 11 ++ .gitignore | 1 + README.md | 7 + scripts/Dockerfile | 13 ++ .../proto/workflow_activation/__init__.py | 2 + .../workflow_activation_pb2.py | 122 +++++++++------ .../workflow_activation_pb2.pyi | 119 ++++++++++++++ .../proto/workflow_commands/__init__.py | 2 + .../workflow_commands_pb2.py | 145 ++++++++++-------- .../workflow_commands_pb2.pyi | 86 +++++++++++ 10 files changed, 398 insertions(+), 110 deletions(-) create mode 100644 .dockerignore create mode 100644 scripts/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..93d7c20a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git/ +.idea/ +.mypy_cache/ +.pytest_cache/ +.venv/ +build/ +dist/ +temporalio/api/ +temporalio/bridge/**/target/ +temporalio/bridge/**/*.so +Dockerfile \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0a3ededf..79c26177 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ temporalio/bridge/target/ temporalio/bridge/temporal_sdk_bridge* /tests/helpers/golangserver/golangserver /tests/helpers/golangworker/golangworker +/.idea diff --git a/README.md b/README.md index 83b99532..66d6f661 100644 --- a/README.md +++ b/README.md @@ -1436,6 +1436,13 @@ to `1` prior to running tests. Do not commit `poetry.lock` or `pyproject.toml` changes. To go back from this downgrade, restore `pyproject.toml` and run `poetry update protobuf grpcio-tools`. +For a less system-intrusive approach, you can: +```shell +docker build -f scripts/Dockerfile . +docker run -v "${PWD}/temporalio/api:/api_new" -v "${PWD}/temporalio/bridge/proto:/bridge_new" +poe format +``` + ### Style * Mostly [Google Style Guide](https://google.github.io/styleguide/pyguide.html). Notable exceptions: diff --git a/scripts/Dockerfile b/scripts/Dockerfile new file mode 100644 index 00000000..ead17b0f --- /dev/null +++ b/scripts/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.10 + +RUN python -m pip install --upgrade wheel "poetry==1.3.2" poethepoet +VOLUME ["/api_new", "/bridge_new"] + +COPY ./ ./ + +RUN mkdir -p ./temporalio/api +RUN poetry install --no-root -E opentelemetry +RUN poetry add "protobuf<4" +RUN poe gen-protos + +CMD cp -r ./temporalio/api/* /api_new && cp -r ./temporalio/bridge/proto/* /bridge_new diff --git a/temporalio/bridge/proto/workflow_activation/__init__.py b/temporalio/bridge/proto/workflow_activation/__init__.py index a44f6415..ce302730 100644 --- a/temporalio/bridge/proto/workflow_activation/__init__.py +++ b/temporalio/bridge/proto/workflow_activation/__init__.py @@ -1,5 +1,6 @@ from .workflow_activation_pb2 import ( CancelWorkflow, + DoUpdate, FireTimer, NotifyHasPatch, QueryWorkflow, @@ -21,6 +22,7 @@ __all__ = [ "CancelWorkflow", + "DoUpdate", "FireTimer", "NotifyHasPatch", "QueryWorkflow", diff --git a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py index 92720b70..a2a141f0 100644 --- a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py +++ b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.py @@ -25,6 +25,9 @@ from temporalio.api.failure.v1 import ( message_pb2 as temporal_dot_api_dot_failure_dot_v1_dot_message__pb2, ) +from temporalio.api.update.v1 import ( + message_pb2 as temporal_dot_api_dot_update_dot_v1_dot_message__pb2, +) from temporalio.bridge.proto.activity_result import ( activity_result_pb2 as temporal_dot_sdk_dot_core_dot_activity__result_dot_activity__result__pb2, ) @@ -36,7 +39,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n?temporal/sdk/core/workflow_activation/workflow_activation.proto\x12\x1b\x63oresdk.workflow_activation\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a%temporal/api/failure/v1/message.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a\x37temporal/sdk/core/activity_result/activity_result.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a%temporal/sdk/core/common/common.proto"\xa4\x02\n\x12WorkflowActivation\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12-\n\ttimestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x14\n\x0cis_replaying\x18\x03 \x01(\x08\x12\x16\n\x0ehistory_length\x18\x04 \x01(\r\x12@\n\x04jobs\x18\x05 \x03(\x0b\x32\x32.coresdk.workflow_activation.WorkflowActivationJob\x12 \n\x18\x61vailable_internal_flags\x18\x06 \x03(\r\x12\x1a\n\x12history_size_bytes\x18\x07 \x01(\x04\x12!\n\x19\x63ontinue_as_new_suggested\x18\x08 \x01(\x08"\xe1\x08\n\x15WorkflowActivationJob\x12\x44\n\x0estart_workflow\x18\x01 \x01(\x0b\x32*.coresdk.workflow_activation.StartWorkflowH\x00\x12<\n\nfire_timer\x18\x02 \x01(\x0b\x32&.coresdk.workflow_activation.FireTimerH\x00\x12K\n\x12update_random_seed\x18\x04 \x01(\x0b\x32-.coresdk.workflow_activation.UpdateRandomSeedH\x00\x12\x44\n\x0equery_workflow\x18\x05 \x01(\x0b\x32*.coresdk.workflow_activation.QueryWorkflowH\x00\x12\x46\n\x0f\x63\x61ncel_workflow\x18\x06 \x01(\x0b\x32+.coresdk.workflow_activation.CancelWorkflowH\x00\x12\x46\n\x0fsignal_workflow\x18\x07 \x01(\x0b\x32+.coresdk.workflow_activation.SignalWorkflowH\x00\x12H\n\x10resolve_activity\x18\x08 \x01(\x0b\x32,.coresdk.workflow_activation.ResolveActivityH\x00\x12G\n\x10notify_has_patch\x18\t \x01(\x0b\x32+.coresdk.workflow_activation.NotifyHasPatchH\x00\x12q\n&resolve_child_workflow_execution_start\x18\n \x01(\x0b\x32?.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartH\x00\x12\x66\n resolve_child_workflow_execution\x18\x0b \x01(\x0b\x32:.coresdk.workflow_activation.ResolveChildWorkflowExecutionH\x00\x12\x66\n resolve_signal_external_workflow\x18\x0c \x01(\x0b\x32:.coresdk.workflow_activation.ResolveSignalExternalWorkflowH\x00\x12u\n(resolve_request_cancel_external_workflow\x18\r \x01(\x0b\x32\x41.coresdk.workflow_activation.ResolveRequestCancelExternalWorkflowH\x00\x12I\n\x11remove_from_cache\x18\x32 \x01(\x0b\x32,.coresdk.workflow_activation.RemoveFromCacheH\x00\x42\t\n\x07variant"\xd9\t\n\rStartWorkflow\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x17\n\x0frandomness_seed\x18\x04 \x01(\x04\x12H\n\x07headers\x18\x05 \x03(\x0b\x32\x37.coresdk.workflow_activation.StartWorkflow.HeadersEntry\x12\x10\n\x08identity\x18\x06 \x01(\t\x12I\n\x14parent_workflow_info\x18\x07 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecution\x12=\n\x1aworkflow_execution_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\'\n\x1f\x63ontinued_from_execution_run_id\x18\x0b \x01(\t\x12J\n\x13\x63ontinued_initiator\x18\x0c \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12;\n\x11\x63ontinued_failure\x18\r \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\x0e \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x1e\n\x16\x66irst_execution_run_id\x18\x0f \x01(\t\x12\x39\n\x0cretry_policy\x18\x10 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x0f\n\x07\x61ttempt\x18\x11 \x01(\x05\x12\x15\n\rcron_schedule\x18\x12 \x01(\t\x12\x46\n"workflow_execution_expiration_time\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n"cron_schedule_to_schedule_interval\x18\x14 \x01(\x0b\x32\x19.google.protobuf.Duration\x12*\n\x04memo\x18\x15 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x16 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\nstart_time\x18\x17 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x18\n\tFireTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"[\n\x0fResolveActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.activity_result.ActivityResolution"\xd1\x02\n"ResolveChildWorkflowExecutionStart\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12[\n\tsucceeded\x18\x02 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartSuccessH\x00\x12X\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartFailureH\x00\x12]\n\tcancelled\x18\x04 \x01(\x0b\x32H.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartCancelledH\x00\x42\x08\n\x06status";\n)ResolveChildWorkflowExecutionStartSuccess\x12\x0e\n\x06run_id\x18\x01 \x01(\t"\xa6\x01\n)ResolveChildWorkflowExecutionStartFailure\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x15\n\rworkflow_type\x18\x02 \x01(\t\x12M\n\x05\x63\x61use\x18\x03 \x01(\x0e\x32>.coresdk.child_workflow.StartChildWorkflowExecutionFailedCause"`\n+ResolveChildWorkflowExecutionStartCancelled\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"i\n\x1dResolveChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.child_workflow.ChildWorkflowResult"+\n\x10UpdateRandomSeed\x12\x17\n\x0frandomness_seed\x18\x01 \x01(\x04"\x84\x02\n\rQueryWorkflow\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12\x12\n\nquery_type\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12H\n\x07headers\x18\x05 \x03(\x0b\x32\x37.coresdk.workflow_activation.QueryWorkflow.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"B\n\x0e\x43\x61ncelWorkflow\x12\x30\n\x07\x64\x65tails\x18\x01 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload"\x83\x02\n\x0eSignalWorkflow\x12\x13\n\x0bsignal_name\x18\x01 \x01(\t\x12.\n\x05input\x18\x02 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x10\n\x08identity\x18\x03 \x01(\t\x12I\n\x07headers\x18\x05 \x03(\x0b\x32\x38.coresdk.workflow_activation.SignalWorkflow.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01""\n\x0eNotifyHasPatch\x12\x10\n\x08patch_id\x18\x01 \x01(\t"_\n\x1dResolveSignalExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"f\n$ResolveRequestCancelExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\xc1\x02\n\x0fRemoveFromCache\x12\x0f\n\x07message\x18\x01 \x01(\t\x12K\n\x06reason\x18\x02 \x01(\x0e\x32;.coresdk.workflow_activation.RemoveFromCache.EvictionReason"\xcf\x01\n\x0e\x45victionReason\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0e\n\nCACHE_FULL\x10\x01\x12\x0e\n\nCACHE_MISS\x10\x02\x12\x12\n\x0eNONDETERMINISM\x10\x03\x12\r\n\tLANG_FAIL\x10\x04\x12\x12\n\x0eLANG_REQUESTED\x10\x05\x12\x12\n\x0eTASK_NOT_FOUND\x10\x06\x12\x15\n\x11UNHANDLED_COMMAND\x10\x07\x12\t\n\x05\x46\x41TAL\x10\x08\x12\x1f\n\x1bPAGINATION_OR_HISTORY_FETCH\x10\tB.\xea\x02+Temporalio::Bridge::Api::WorkflowActivationb\x06proto3' + b'\n?temporal/sdk/core/workflow_activation/workflow_activation.proto\x12\x1b\x63oresdk.workflow_activation\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a%temporal/api/failure/v1/message.proto\x1a$temporal/api/update/v1/message.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a\x37temporal/sdk/core/activity_result/activity_result.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a%temporal/sdk/core/common/common.proto"\xa4\x02\n\x12WorkflowActivation\x12\x0e\n\x06run_id\x18\x01 \x01(\t\x12-\n\ttimestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x14\n\x0cis_replaying\x18\x03 \x01(\x08\x12\x16\n\x0ehistory_length\x18\x04 \x01(\r\x12@\n\x04jobs\x18\x05 \x03(\x0b\x32\x32.coresdk.workflow_activation.WorkflowActivationJob\x12 \n\x18\x61vailable_internal_flags\x18\x06 \x03(\r\x12\x1a\n\x12history_size_bytes\x18\x07 \x01(\x04\x12!\n\x19\x63ontinue_as_new_suggested\x18\x08 \x01(\x08"\x9d\t\n\x15WorkflowActivationJob\x12\x44\n\x0estart_workflow\x18\x01 \x01(\x0b\x32*.coresdk.workflow_activation.StartWorkflowH\x00\x12<\n\nfire_timer\x18\x02 \x01(\x0b\x32&.coresdk.workflow_activation.FireTimerH\x00\x12K\n\x12update_random_seed\x18\x04 \x01(\x0b\x32-.coresdk.workflow_activation.UpdateRandomSeedH\x00\x12\x44\n\x0equery_workflow\x18\x05 \x01(\x0b\x32*.coresdk.workflow_activation.QueryWorkflowH\x00\x12\x46\n\x0f\x63\x61ncel_workflow\x18\x06 \x01(\x0b\x32+.coresdk.workflow_activation.CancelWorkflowH\x00\x12\x46\n\x0fsignal_workflow\x18\x07 \x01(\x0b\x32+.coresdk.workflow_activation.SignalWorkflowH\x00\x12H\n\x10resolve_activity\x18\x08 \x01(\x0b\x32,.coresdk.workflow_activation.ResolveActivityH\x00\x12G\n\x10notify_has_patch\x18\t \x01(\x0b\x32+.coresdk.workflow_activation.NotifyHasPatchH\x00\x12q\n&resolve_child_workflow_execution_start\x18\n \x01(\x0b\x32?.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartH\x00\x12\x66\n resolve_child_workflow_execution\x18\x0b \x01(\x0b\x32:.coresdk.workflow_activation.ResolveChildWorkflowExecutionH\x00\x12\x66\n resolve_signal_external_workflow\x18\x0c \x01(\x0b\x32:.coresdk.workflow_activation.ResolveSignalExternalWorkflowH\x00\x12u\n(resolve_request_cancel_external_workflow\x18\r \x01(\x0b\x32\x41.coresdk.workflow_activation.ResolveRequestCancelExternalWorkflowH\x00\x12:\n\tdo_update\x18\x0e \x01(\x0b\x32%.coresdk.workflow_activation.DoUpdateH\x00\x12I\n\x11remove_from_cache\x18\x32 \x01(\x0b\x32,.coresdk.workflow_activation.RemoveFromCacheH\x00\x42\t\n\x07variant"\xd9\t\n\rStartWorkflow\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x17\n\x0frandomness_seed\x18\x04 \x01(\x04\x12H\n\x07headers\x18\x05 \x03(\x0b\x32\x37.coresdk.workflow_activation.StartWorkflow.HeadersEntry\x12\x10\n\x08identity\x18\x06 \x01(\t\x12I\n\x14parent_workflow_info\x18\x07 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecution\x12=\n\x1aworkflow_execution_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\'\n\x1f\x63ontinued_from_execution_run_id\x18\x0b \x01(\t\x12J\n\x13\x63ontinued_initiator\x18\x0c \x01(\x0e\x32-.temporal.api.enums.v1.ContinueAsNewInitiator\x12;\n\x11\x63ontinued_failure\x18\r \x01(\x0b\x32 .temporal.api.failure.v1.Failure\x12@\n\x16last_completion_result\x18\x0e \x01(\x0b\x32 .temporal.api.common.v1.Payloads\x12\x1e\n\x16\x66irst_execution_run_id\x18\x0f \x01(\t\x12\x39\n\x0cretry_policy\x18\x10 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x0f\n\x07\x61ttempt\x18\x11 \x01(\x05\x12\x15\n\rcron_schedule\x18\x12 \x01(\t\x12\x46\n"workflow_execution_expiration_time\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x45\n"cron_schedule_to_schedule_interval\x18\x14 \x01(\x0b\x32\x19.google.protobuf.Duration\x12*\n\x04memo\x18\x15 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo\x12\x43\n\x11search_attributes\x18\x16 \x01(\x0b\x32(.temporal.api.common.v1.SearchAttributes\x12.\n\nstart_time\x18\x17 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x18\n\tFireTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"[\n\x0fResolveActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.activity_result.ActivityResolution"\xd1\x02\n"ResolveChildWorkflowExecutionStart\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12[\n\tsucceeded\x18\x02 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartSuccessH\x00\x12X\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32\x46.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartFailureH\x00\x12]\n\tcancelled\x18\x04 \x01(\x0b\x32H.coresdk.workflow_activation.ResolveChildWorkflowExecutionStartCancelledH\x00\x42\x08\n\x06status";\n)ResolveChildWorkflowExecutionStartSuccess\x12\x0e\n\x06run_id\x18\x01 \x01(\t"\xa6\x01\n)ResolveChildWorkflowExecutionStartFailure\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x15\n\rworkflow_type\x18\x02 \x01(\t\x12M\n\x05\x63\x61use\x18\x03 \x01(\x0e\x32>.coresdk.child_workflow.StartChildWorkflowExecutionFailedCause"`\n+ResolveChildWorkflowExecutionStartCancelled\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"i\n\x1dResolveChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12;\n\x06result\x18\x02 \x01(\x0b\x32+.coresdk.child_workflow.ChildWorkflowResult"+\n\x10UpdateRandomSeed\x12\x17\n\x0frandomness_seed\x18\x01 \x01(\x04"\x84\x02\n\rQueryWorkflow\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12\x12\n\nquery_type\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12H\n\x07headers\x18\x05 \x03(\x0b\x32\x37.coresdk.workflow_activation.QueryWorkflow.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"B\n\x0e\x43\x61ncelWorkflow\x12\x30\n\x07\x64\x65tails\x18\x01 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload"\x83\x02\n\x0eSignalWorkflow\x12\x13\n\x0bsignal_name\x18\x01 \x01(\t\x12.\n\x05input\x18\x02 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x10\n\x08identity\x18\x03 \x01(\t\x12I\n\x07headers\x18\x05 \x03(\x0b\x32\x38.coresdk.workflow_activation.SignalWorkflow.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01""\n\x0eNotifyHasPatch\x12\x10\n\x08patch_id\x18\x01 \x01(\t"_\n\x1dResolveSignalExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"f\n$ResolveRequestCancelExternalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x31\n\x07\x66\x61ilure\x18\x02 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\xcb\x02\n\x08\x44oUpdate\x12\n\n\x02id\x18\x01 \x01(\t\x12\x1c\n\x14protocol_instance_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12.\n\x05input\x18\x04 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x43\n\x07headers\x18\x05 \x03(\x0b\x32\x32.coresdk.workflow_activation.DoUpdate.HeadersEntry\x12*\n\x04meta\x18\x06 \x01(\x0b\x32\x1c.temporal.api.update.v1.Meta\x12\x15\n\rrun_validator\x18\x07 \x01(\x08\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\xc1\x02\n\x0fRemoveFromCache\x12\x0f\n\x07message\x18\x01 \x01(\t\x12K\n\x06reason\x18\x02 \x01(\x0e\x32;.coresdk.workflow_activation.RemoveFromCache.EvictionReason"\xcf\x01\n\x0e\x45victionReason\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0e\n\nCACHE_FULL\x10\x01\x12\x0e\n\nCACHE_MISS\x10\x02\x12\x12\n\x0eNONDETERMINISM\x10\x03\x12\r\n\tLANG_FAIL\x10\x04\x12\x12\n\x0eLANG_REQUESTED\x10\x05\x12\x12\n\x0eTASK_NOT_FOUND\x10\x06\x12\x15\n\x11UNHANDLED_COMMAND\x10\x07\x12\t\n\x05\x46\x41TAL\x10\x08\x12\x1f\n\x1bPAGINATION_OR_HISTORY_FETCH\x10\tB.\xea\x02+Temporalio::Bridge::Api::WorkflowActivationb\x06proto3' ) @@ -74,6 +77,8 @@ _RESOLVEREQUESTCANCELEXTERNALWORKFLOW = DESCRIPTOR.message_types_by_name[ "ResolveRequestCancelExternalWorkflow" ] +_DOUPDATE = DESCRIPTOR.message_types_by_name["DoUpdate"] +_DOUPDATE_HEADERSENTRY = _DOUPDATE.nested_types_by_name["HeadersEntry"] _REMOVEFROMCACHE = DESCRIPTOR.message_types_by_name["RemoveFromCache"] _REMOVEFROMCACHE_EVICTIONREASON = _REMOVEFROMCACHE.enum_types_by_name["EvictionReason"] WorkflowActivation = _reflection.GeneratedProtocolMessageType( @@ -293,6 +298,27 @@ ) _sym_db.RegisterMessage(ResolveRequestCancelExternalWorkflow) +DoUpdate = _reflection.GeneratedProtocolMessageType( + "DoUpdate", + (_message.Message,), + { + "HeadersEntry": _reflection.GeneratedProtocolMessageType( + "HeadersEntry", + (_message.Message,), + { + "DESCRIPTOR": _DOUPDATE_HEADERSENTRY, + "__module__": "temporal.sdk.core.workflow_activation.workflow_activation_pb2" + # @@protoc_insertion_point(class_scope:coresdk.workflow_activation.DoUpdate.HeadersEntry) + }, + ), + "DESCRIPTOR": _DOUPDATE, + "__module__": "temporal.sdk.core.workflow_activation.workflow_activation_pb2" + # @@protoc_insertion_point(class_scope:coresdk.workflow_activation.DoUpdate) + }, +) +_sym_db.RegisterMessage(DoUpdate) +_sym_db.RegisterMessage(DoUpdate.HeadersEntry) + RemoveFromCache = _reflection.GeneratedProtocolMessageType( "RemoveFromCache", (_message.Message,), @@ -315,48 +341,54 @@ _QUERYWORKFLOW_HEADERSENTRY._serialized_options = b"8\001" _SIGNALWORKFLOW_HEADERSENTRY._options = None _SIGNALWORKFLOW_HEADERSENTRY._serialized_options = b"8\001" - _WORKFLOWACTIVATION._serialized_start = 428 - _WORKFLOWACTIVATION._serialized_end = 720 - _WORKFLOWACTIVATIONJOB._serialized_start = 723 - _WORKFLOWACTIVATIONJOB._serialized_end = 1844 - _STARTWORKFLOW._serialized_start = 1847 - _STARTWORKFLOW._serialized_end = 3088 - _STARTWORKFLOW_HEADERSENTRY._serialized_start = 3009 - _STARTWORKFLOW_HEADERSENTRY._serialized_end = 3088 - _FIRETIMER._serialized_start = 3090 - _FIRETIMER._serialized_end = 3114 - _RESOLVEACTIVITY._serialized_start = 3116 - _RESOLVEACTIVITY._serialized_end = 3207 - _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_start = 3210 - _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_end = 3547 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_start = 3549 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_end = 3608 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_start = 3611 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_end = 3777 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_start = 3779 - _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_end = 3875 - _RESOLVECHILDWORKFLOWEXECUTION._serialized_start = 3877 - _RESOLVECHILDWORKFLOWEXECUTION._serialized_end = 3982 - _UPDATERANDOMSEED._serialized_start = 3984 - _UPDATERANDOMSEED._serialized_end = 4027 - _QUERYWORKFLOW._serialized_start = 4030 - _QUERYWORKFLOW._serialized_end = 4290 - _QUERYWORKFLOW_HEADERSENTRY._serialized_start = 3009 - _QUERYWORKFLOW_HEADERSENTRY._serialized_end = 3088 - _CANCELWORKFLOW._serialized_start = 4292 - _CANCELWORKFLOW._serialized_end = 4358 - _SIGNALWORKFLOW._serialized_start = 4361 - _SIGNALWORKFLOW._serialized_end = 4620 - _SIGNALWORKFLOW_HEADERSENTRY._serialized_start = 3009 - _SIGNALWORKFLOW_HEADERSENTRY._serialized_end = 3088 - _NOTIFYHASPATCH._serialized_start = 4622 - _NOTIFYHASPATCH._serialized_end = 4656 - _RESOLVESIGNALEXTERNALWORKFLOW._serialized_start = 4658 - _RESOLVESIGNALEXTERNALWORKFLOW._serialized_end = 4753 - _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_start = 4755 - _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_end = 4857 - _REMOVEFROMCACHE._serialized_start = 4860 - _REMOVEFROMCACHE._serialized_end = 5181 - _REMOVEFROMCACHE_EVICTIONREASON._serialized_start = 4974 - _REMOVEFROMCACHE_EVICTIONREASON._serialized_end = 5181 + _DOUPDATE_HEADERSENTRY._options = None + _DOUPDATE_HEADERSENTRY._serialized_options = b"8\001" + _WORKFLOWACTIVATION._serialized_start = 466 + _WORKFLOWACTIVATION._serialized_end = 758 + _WORKFLOWACTIVATIONJOB._serialized_start = 761 + _WORKFLOWACTIVATIONJOB._serialized_end = 1942 + _STARTWORKFLOW._serialized_start = 1945 + _STARTWORKFLOW._serialized_end = 3186 + _STARTWORKFLOW_HEADERSENTRY._serialized_start = 3107 + _STARTWORKFLOW_HEADERSENTRY._serialized_end = 3186 + _FIRETIMER._serialized_start = 3188 + _FIRETIMER._serialized_end = 3212 + _RESOLVEACTIVITY._serialized_start = 3214 + _RESOLVEACTIVITY._serialized_end = 3305 + _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_start = 3308 + _RESOLVECHILDWORKFLOWEXECUTIONSTART._serialized_end = 3645 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_start = 3647 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTSUCCESS._serialized_end = 3706 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_start = 3709 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTFAILURE._serialized_end = 3875 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_start = 3877 + _RESOLVECHILDWORKFLOWEXECUTIONSTARTCANCELLED._serialized_end = 3973 + _RESOLVECHILDWORKFLOWEXECUTION._serialized_start = 3975 + _RESOLVECHILDWORKFLOWEXECUTION._serialized_end = 4080 + _UPDATERANDOMSEED._serialized_start = 4082 + _UPDATERANDOMSEED._serialized_end = 4125 + _QUERYWORKFLOW._serialized_start = 4128 + _QUERYWORKFLOW._serialized_end = 4388 + _QUERYWORKFLOW_HEADERSENTRY._serialized_start = 3107 + _QUERYWORKFLOW_HEADERSENTRY._serialized_end = 3186 + _CANCELWORKFLOW._serialized_start = 4390 + _CANCELWORKFLOW._serialized_end = 4456 + _SIGNALWORKFLOW._serialized_start = 4459 + _SIGNALWORKFLOW._serialized_end = 4718 + _SIGNALWORKFLOW_HEADERSENTRY._serialized_start = 3107 + _SIGNALWORKFLOW_HEADERSENTRY._serialized_end = 3186 + _NOTIFYHASPATCH._serialized_start = 4720 + _NOTIFYHASPATCH._serialized_end = 4754 + _RESOLVESIGNALEXTERNALWORKFLOW._serialized_start = 4756 + _RESOLVESIGNALEXTERNALWORKFLOW._serialized_end = 4851 + _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_start = 4853 + _RESOLVEREQUESTCANCELEXTERNALWORKFLOW._serialized_end = 4955 + _DOUPDATE._serialized_start = 4958 + _DOUPDATE._serialized_end = 5289 + _DOUPDATE_HEADERSENTRY._serialized_start = 3107 + _DOUPDATE_HEADERSENTRY._serialized_end = 3186 + _REMOVEFROMCACHE._serialized_start = 5292 + _REMOVEFROMCACHE._serialized_end = 5613 + _REMOVEFROMCACHE_EVICTIONREASON._serialized_start = 5406 + _REMOVEFROMCACHE_EVICTIONREASON._serialized_end = 5613 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi index 557fe9f3..1be4a31a 100644 --- a/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi +++ b/temporalio/bridge/proto/workflow_activation/workflow_activation_pb2.pyi @@ -17,6 +17,7 @@ import sys import temporalio.api.common.v1.message_pb2 import temporalio.api.enums.v1.workflow_pb2 import temporalio.api.failure.v1.message_pb2 +import temporalio.api.update.v1.message_pb2 import temporalio.bridge.proto.activity_result.activity_result_pb2 import temporalio.bridge.proto.child_workflow.child_workflow_pb2 import temporalio.bridge.proto.common.common_pb2 @@ -130,6 +131,7 @@ class WorkflowActivationJob(google.protobuf.message.Message): RESOLVE_CHILD_WORKFLOW_EXECUTION_FIELD_NUMBER: builtins.int RESOLVE_SIGNAL_EXTERNAL_WORKFLOW_FIELD_NUMBER: builtins.int RESOLVE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_FIELD_NUMBER: builtins.int + DO_UPDATE_FIELD_NUMBER: builtins.int REMOVE_FROM_CACHE_FIELD_NUMBER: builtins.int @property def start_workflow(self) -> global___StartWorkflow: @@ -179,6 +181,9 @@ class WorkflowActivationJob(google.protobuf.message.Message): ) -> global___ResolveRequestCancelExternalWorkflow: """An attempt to cancel an external workflow resolved""" @property + def do_update(self) -> global___DoUpdate: + """A request to handle a workflow update.""" + @property def remove_from_cache(self) -> global___RemoveFromCache: """Remove the workflow identified by the [WorkflowActivation] containing this job from the cache after performing the activation. @@ -205,6 +210,7 @@ class WorkflowActivationJob(google.protobuf.message.Message): | None = ..., resolve_request_cancel_external_workflow: global___ResolveRequestCancelExternalWorkflow | None = ..., + do_update: global___DoUpdate | None = ..., remove_from_cache: global___RemoveFromCache | None = ..., ) -> None: ... def HasField( @@ -212,6 +218,8 @@ class WorkflowActivationJob(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "cancel_workflow", b"cancel_workflow", + "do_update", + b"do_update", "fire_timer", b"fire_timer", "notify_has_patch", @@ -245,6 +253,8 @@ class WorkflowActivationJob(google.protobuf.message.Message): field_name: typing_extensions.Literal[ "cancel_workflow", b"cancel_workflow", + "do_update", + b"do_update", "fire_timer", b"fire_timer", "notify_has_patch", @@ -289,6 +299,7 @@ class WorkflowActivationJob(google.protobuf.message.Message): "resolve_child_workflow_execution", "resolve_signal_external_workflow", "resolve_request_cancel_external_workflow", + "do_update", "remove_from_cache", ] | None @@ -1056,6 +1067,114 @@ class ResolveRequestCancelExternalWorkflow(google.protobuf.message.Message): global___ResolveRequestCancelExternalWorkflow = ResolveRequestCancelExternalWorkflow +class DoUpdate(google.protobuf.message.Message): + """Lang is requested to invoke an update handler on the workflow. Lang should invoke the update + validator first (if requested). If it accepts the update, immediately invoke the update handler. + Lang must reply to the activation containing this job with an `UpdateResponse`. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class HeadersEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> temporalio.api.common.v1.message_pb2.Payload: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: temporalio.api.common.v1.message_pb2.Payload | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + ID_FIELD_NUMBER: builtins.int + PROTOCOL_INSTANCE_ID_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + INPUT_FIELD_NUMBER: builtins.int + HEADERS_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + RUN_VALIDATOR_FIELD_NUMBER: builtins.int + id: builtins.str + """A workflow-unique identifier for this update""" + protocol_instance_id: builtins.str + """The protocol message instance ID - this is used to uniquely track the ID server side and + internally. + """ + name: builtins.str + """The name of the update handler""" + @property + def input( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.common.v1.message_pb2.Payload + ]: + """The input to the update""" + @property + def headers( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, temporalio.api.common.v1.message_pb2.Payload + ]: + """Headers attached to the update""" + @property + def meta(self) -> temporalio.api.update.v1.message_pb2.Meta: + """Remaining metadata associated with the update. The `update_id` field is stripped from here + and moved to `id`, since it is guaranteed to be present. + """ + run_validator: builtins.bool + """If set true, lang must run the update's validator before running the handler. This will be + set false during replay, since validation is not re-run during replay. + """ + def __init__( + self, + *, + id: builtins.str = ..., + protocol_instance_id: builtins.str = ..., + name: builtins.str = ..., + input: collections.abc.Iterable[temporalio.api.common.v1.message_pb2.Payload] + | None = ..., + headers: collections.abc.Mapping[ + builtins.str, temporalio.api.common.v1.message_pb2.Payload + ] + | None = ..., + meta: temporalio.api.update.v1.message_pb2.Meta | None = ..., + run_validator: builtins.bool = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["meta", b"meta"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "headers", + b"headers", + "id", + b"id", + "input", + b"input", + "meta", + b"meta", + "name", + b"name", + "protocol_instance_id", + b"protocol_instance_id", + "run_validator", + b"run_validator", + ], + ) -> None: ... + +global___DoUpdate = DoUpdate + class RemoveFromCache(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor diff --git a/temporalio/bridge/proto/workflow_commands/__init__.py b/temporalio/bridge/proto/workflow_commands/__init__.py index 2007a970..00008983 100644 --- a/temporalio/bridge/proto/workflow_commands/__init__.py +++ b/temporalio/bridge/proto/workflow_commands/__init__.py @@ -19,6 +19,7 @@ SignalExternalWorkflowExecution, StartChildWorkflowExecution, StartTimer, + UpdateResponse, UpsertWorkflowSearchAttributes, WorkflowCommand, ) @@ -44,6 +45,7 @@ "SignalExternalWorkflowExecution", "StartChildWorkflowExecution", "StartTimer", + "UpdateResponse", "UpsertWorkflowSearchAttributes", "WorkflowCommand", ] diff --git a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py index b2ecd469..a22b58c9 100644 --- a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py +++ b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.py @@ -15,6 +15,7 @@ from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 from temporalio.api.common.v1 import ( @@ -34,7 +35,7 @@ ) DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n;temporal/sdk/core/workflow_commands/workflow_commands.proto\x12\x19\x63oresdk.workflow_commands\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/failure/v1/message.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a%temporal/sdk/core/common/common.proto"\xac\r\n\x0fWorkflowCommand\x12<\n\x0bstart_timer\x18\x01 \x01(\x0b\x32%.coresdk.workflow_commands.StartTimerH\x00\x12H\n\x11schedule_activity\x18\x02 \x01(\x0b\x32+.coresdk.workflow_commands.ScheduleActivityH\x00\x12\x42\n\x10respond_to_query\x18\x03 \x01(\x0b\x32&.coresdk.workflow_commands.QueryResultH\x00\x12S\n\x17request_cancel_activity\x18\x04 \x01(\x0b\x32\x30.coresdk.workflow_commands.RequestCancelActivityH\x00\x12>\n\x0c\x63\x61ncel_timer\x18\x05 \x01(\x0b\x32&.coresdk.workflow_commands.CancelTimerH\x00\x12[\n\x1b\x63omplete_workflow_execution\x18\x06 \x01(\x0b\x32\x34.coresdk.workflow_commands.CompleteWorkflowExecutionH\x00\x12S\n\x17\x66\x61il_workflow_execution\x18\x07 \x01(\x0b\x32\x30.coresdk.workflow_commands.FailWorkflowExecutionH\x00\x12g\n"continue_as_new_workflow_execution\x18\x08 \x01(\x0b\x32\x39.coresdk.workflow_commands.ContinueAsNewWorkflowExecutionH\x00\x12W\n\x19\x63\x61ncel_workflow_execution\x18\t \x01(\x0b\x32\x32.coresdk.workflow_commands.CancelWorkflowExecutionH\x00\x12\x45\n\x10set_patch_marker\x18\n \x01(\x0b\x32).coresdk.workflow_commands.SetPatchMarkerH\x00\x12`\n\x1estart_child_workflow_execution\x18\x0b \x01(\x0b\x32\x36.coresdk.workflow_commands.StartChildWorkflowExecutionH\x00\x12\x62\n\x1f\x63\x61ncel_child_workflow_execution\x18\x0c \x01(\x0b\x32\x37.coresdk.workflow_commands.CancelChildWorkflowExecutionH\x00\x12w\n*request_cancel_external_workflow_execution\x18\r \x01(\x0b\x32\x41.coresdk.workflow_commands.RequestCancelExternalWorkflowExecutionH\x00\x12h\n"signal_external_workflow_execution\x18\x0e \x01(\x0b\x32:.coresdk.workflow_commands.SignalExternalWorkflowExecutionH\x00\x12Q\n\x16\x63\x61ncel_signal_workflow\x18\x0f \x01(\x0b\x32/.coresdk.workflow_commands.CancelSignalWorkflowH\x00\x12S\n\x17schedule_local_activity\x18\x10 \x01(\x0b\x32\x30.coresdk.workflow_commands.ScheduleLocalActivityH\x00\x12^\n\x1drequest_cancel_local_activity\x18\x11 \x01(\x0b\x32\x35.coresdk.workflow_commands.RequestCancelLocalActivityH\x00\x12\x66\n!upsert_workflow_search_attributes\x18\x12 \x01(\x0b\x32\x39.coresdk.workflow_commands.UpsertWorkflowSearchAttributesH\x00\x12Y\n\x1amodify_workflow_properties\x18\x13 \x01(\x0b\x32\x33.coresdk.workflow_commands.ModifyWorkflowPropertiesH\x00\x42\t\n\x07variant"S\n\nStartTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x38\n\x15start_to_fire_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration"\x1a\n\x0b\x43\x61ncelTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"\x84\x06\n\x10ScheduleActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12I\n\x07headers\x18\x06 \x03(\x0b\x32\x38.coresdk.workflow_commands.ScheduleActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x0b \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x12\x1e\n\x16\x64o_not_eagerly_execute\x18\x0e \x01(\x08\x12;\n\x11versioning_intent\x18\x0f \x01(\x0e\x32 .coresdk.common.VersioningIntent\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\xee\x05\n\x15ScheduleLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x04 \x01(\r\x12:\n\x16original_schedule_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12N\n\x07headers\x18\x06 \x03(\x0b\x32=.coresdk.workflow_commands.ScheduleLocalActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0b \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x38\n\x15local_retry_threshold\x18\x0c \x01(\x0b\x32\x19.google.protobuf.Duration\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"$\n\x15RequestCancelActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r")\n\x1aRequestCancelLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r"\x9c\x01\n\x0bQueryResult\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12<\n\tsucceeded\x18\x02 \x01(\x0b\x32\'.coresdk.workflow_commands.QuerySuccessH\x00\x12\x32\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\t\n\x07variant"A\n\x0cQuerySuccess\x12\x31\n\x08response\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"L\n\x19\x43ompleteWorkflowExecution\x12/\n\x06result\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"J\n\x15\x46\x61ilWorkflowExecution\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\xfb\x06\n\x1e\x43ontinueAsNewWorkflowExecution\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x37\n\x14workflow_run_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12Q\n\x04memo\x18\x06 \x03(\x0b\x32\x43.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.MemoEntry\x12W\n\x07headers\x18\x07 \x03(\x0b\x32\x46.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.HeadersEntry\x12j\n\x11search_attributes\x18\x08 \x03(\x0b\x32O.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.SearchAttributesEntry\x12\x39\n\x0cretry_policy\x18\t \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12;\n\x11versioning_intent\x18\n \x01(\x0e\x32 .coresdk.common.VersioningIntent\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aX\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x19\n\x17\x43\x61ncelWorkflowExecution"6\n\x0eSetPatchMarker\x12\x10\n\x08patch_id\x18\x01 \x01(\t\x12\x12\n\ndeprecated\x18\x02 \x01(\x08"\xe0\t\n\x1bStartChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x03 \x01(\t\x12\x15\n\rworkflow_type\x18\x04 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12.\n\x05input\x18\x06 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12=\n\x1aworkflow_execution_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x46\n\x13parent_close_policy\x18\n \x01(\x0e\x32).coresdk.child_workflow.ParentClosePolicy\x12N\n\x18workflow_id_reuse_policy\x18\x0c \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\r \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\x0e \x01(\t\x12T\n\x07headers\x18\x0f \x03(\x0b\x32\x43.coresdk.workflow_commands.StartChildWorkflowExecution.HeadersEntry\x12N\n\x04memo\x18\x10 \x03(\x0b\x32@.coresdk.workflow_commands.StartChildWorkflowExecution.MemoEntry\x12g\n\x11search_attributes\x18\x11 \x03(\x0b\x32L.coresdk.workflow_commands.StartChildWorkflowExecution.SearchAttributesEntry\x12P\n\x11\x63\x61ncellation_type\x18\x12 \x01(\x0e\x32\x35.coresdk.child_workflow.ChildWorkflowCancellationType\x12;\n\x11versioning_intent\x18\x13 \x01(\x0e\x32 .coresdk.common.VersioningIntent\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aX\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01":\n\x1c\x43\x61ncelChildWorkflowExecution\x12\x1a\n\x12\x63hild_workflow_seq\x18\x01 \x01(\r"\xa7\x01\n&RequestCancelExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12I\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecutionH\x00\x12\x1b\n\x11\x63hild_workflow_id\x18\x03 \x01(\tH\x00\x42\x08\n\x06target"\x8f\x03\n\x1fSignalExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12I\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecutionH\x00\x12\x1b\n\x11\x63hild_workflow_id\x18\x03 \x01(\tH\x00\x12\x13\n\x0bsignal_name\x18\x04 \x01(\t\x12-\n\x04\x61rgs\x18\x05 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12X\n\x07headers\x18\x06 \x03(\x0b\x32G.coresdk.workflow_commands.SignalExternalWorkflowExecution.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x42\x08\n\x06target"#\n\x14\x43\x61ncelSignalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r"\xe6\x01\n\x1eUpsertWorkflowSearchAttributes\x12j\n\x11search_attributes\x18\x01 \x03(\x0b\x32O.coresdk.workflow_commands.UpsertWorkflowSearchAttributes.SearchAttributesEntry\x1aX\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"O\n\x18ModifyWorkflowProperties\x12\x33\n\rupserted_memo\x18\x01 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo*X\n\x18\x41\x63tivityCancellationType\x12\x0e\n\nTRY_CANCEL\x10\x00\x12\x1f\n\x1bWAIT_CANCELLATION_COMPLETED\x10\x01\x12\x0b\n\x07\x41\x42\x41NDON\x10\x02\x42,\xea\x02)Temporalio::Bridge::Api::WorkflowCommandsb\x06proto3' + b'\n;temporal/sdk/core/workflow_commands/workflow_commands.proto\x12\x19\x63oresdk.workflow_commands\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/enums/v1/workflow.proto\x1a%temporal/api/failure/v1/message.proto\x1a\x35temporal/sdk/core/child_workflow/child_workflow.proto\x1a%temporal/sdk/core/common/common.proto"\xf2\r\n\x0fWorkflowCommand\x12<\n\x0bstart_timer\x18\x01 \x01(\x0b\x32%.coresdk.workflow_commands.StartTimerH\x00\x12H\n\x11schedule_activity\x18\x02 \x01(\x0b\x32+.coresdk.workflow_commands.ScheduleActivityH\x00\x12\x42\n\x10respond_to_query\x18\x03 \x01(\x0b\x32&.coresdk.workflow_commands.QueryResultH\x00\x12S\n\x17request_cancel_activity\x18\x04 \x01(\x0b\x32\x30.coresdk.workflow_commands.RequestCancelActivityH\x00\x12>\n\x0c\x63\x61ncel_timer\x18\x05 \x01(\x0b\x32&.coresdk.workflow_commands.CancelTimerH\x00\x12[\n\x1b\x63omplete_workflow_execution\x18\x06 \x01(\x0b\x32\x34.coresdk.workflow_commands.CompleteWorkflowExecutionH\x00\x12S\n\x17\x66\x61il_workflow_execution\x18\x07 \x01(\x0b\x32\x30.coresdk.workflow_commands.FailWorkflowExecutionH\x00\x12g\n"continue_as_new_workflow_execution\x18\x08 \x01(\x0b\x32\x39.coresdk.workflow_commands.ContinueAsNewWorkflowExecutionH\x00\x12W\n\x19\x63\x61ncel_workflow_execution\x18\t \x01(\x0b\x32\x32.coresdk.workflow_commands.CancelWorkflowExecutionH\x00\x12\x45\n\x10set_patch_marker\x18\n \x01(\x0b\x32).coresdk.workflow_commands.SetPatchMarkerH\x00\x12`\n\x1estart_child_workflow_execution\x18\x0b \x01(\x0b\x32\x36.coresdk.workflow_commands.StartChildWorkflowExecutionH\x00\x12\x62\n\x1f\x63\x61ncel_child_workflow_execution\x18\x0c \x01(\x0b\x32\x37.coresdk.workflow_commands.CancelChildWorkflowExecutionH\x00\x12w\n*request_cancel_external_workflow_execution\x18\r \x01(\x0b\x32\x41.coresdk.workflow_commands.RequestCancelExternalWorkflowExecutionH\x00\x12h\n"signal_external_workflow_execution\x18\x0e \x01(\x0b\x32:.coresdk.workflow_commands.SignalExternalWorkflowExecutionH\x00\x12Q\n\x16\x63\x61ncel_signal_workflow\x18\x0f \x01(\x0b\x32/.coresdk.workflow_commands.CancelSignalWorkflowH\x00\x12S\n\x17schedule_local_activity\x18\x10 \x01(\x0b\x32\x30.coresdk.workflow_commands.ScheduleLocalActivityH\x00\x12^\n\x1drequest_cancel_local_activity\x18\x11 \x01(\x0b\x32\x35.coresdk.workflow_commands.RequestCancelLocalActivityH\x00\x12\x66\n!upsert_workflow_search_attributes\x18\x12 \x01(\x0b\x32\x39.coresdk.workflow_commands.UpsertWorkflowSearchAttributesH\x00\x12Y\n\x1amodify_workflow_properties\x18\x13 \x01(\x0b\x32\x33.coresdk.workflow_commands.ModifyWorkflowPropertiesH\x00\x12\x44\n\x0fupdate_response\x18\x14 \x01(\x0b\x32).coresdk.workflow_commands.UpdateResponseH\x00\x42\t\n\x07variant"S\n\nStartTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x38\n\x15start_to_fire_timeout\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration"\x1a\n\x0b\x43\x61ncelTimer\x12\x0b\n\x03seq\x18\x01 \x01(\r"\x84\x06\n\x10ScheduleActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12I\n\x07headers\x18\x06 \x03(\x0b\x32\x38.coresdk.workflow_commands.ScheduleActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x0b \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0c \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x12\x1e\n\x16\x64o_not_eagerly_execute\x18\x0e \x01(\x08\x12;\n\x11versioning_intent\x18\x0f \x01(\x0e\x32 .coresdk.common.VersioningIntent\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\xee\x05\n\x15ScheduleLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x13\n\x0b\x61\x63tivity_id\x18\x02 \x01(\t\x12\x15\n\ractivity_type\x18\x03 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x04 \x01(\r\x12:\n\x16original_schedule_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12N\n\x07headers\x18\x06 \x03(\x0b\x32=.coresdk.workflow_commands.ScheduleLocalActivity.HeadersEntry\x12\x32\n\targuments\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12<\n\x19schedule_to_close_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12<\n\x19schedule_to_start_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x0b \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x38\n\x15local_retry_threshold\x18\x0c \x01(\x0b\x32\x19.google.protobuf.Duration\x12N\n\x11\x63\x61ncellation_type\x18\r \x01(\x0e\x32\x33.coresdk.workflow_commands.ActivityCancellationType\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"$\n\x15RequestCancelActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r")\n\x1aRequestCancelLocalActivity\x12\x0b\n\x03seq\x18\x01 \x01(\r"\x9c\x01\n\x0bQueryResult\x12\x10\n\x08query_id\x18\x01 \x01(\t\x12<\n\tsucceeded\x18\x02 \x01(\x0b\x32\'.coresdk.workflow_commands.QuerySuccessH\x00\x12\x32\n\x06\x66\x61iled\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x42\t\n\x07variant"A\n\x0cQuerySuccess\x12\x31\n\x08response\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"L\n\x19\x43ompleteWorkflowExecution\x12/\n\x06result\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"J\n\x15\x46\x61ilWorkflowExecution\x12\x31\n\x07\x66\x61ilure\x18\x01 \x01(\x0b\x32 .temporal.api.failure.v1.Failure"\xfb\x06\n\x1e\x43ontinueAsNewWorkflowExecution\x12\x15\n\rworkflow_type\x18\x01 \x01(\t\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12\x32\n\targuments\x18\x03 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x37\n\x14workflow_run_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.Duration\x12Q\n\x04memo\x18\x06 \x03(\x0b\x32\x43.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.MemoEntry\x12W\n\x07headers\x18\x07 \x03(\x0b\x32\x46.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.HeadersEntry\x12j\n\x11search_attributes\x18\x08 \x03(\x0b\x32O.coresdk.workflow_commands.ContinueAsNewWorkflowExecution.SearchAttributesEntry\x12\x39\n\x0cretry_policy\x18\t \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12;\n\x11versioning_intent\x18\n \x01(\x0e\x32 .coresdk.common.VersioningIntent\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aX\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x19\n\x17\x43\x61ncelWorkflowExecution"6\n\x0eSetPatchMarker\x12\x10\n\x08patch_id\x18\x01 \x01(\t\x12\x12\n\ndeprecated\x18\x02 \x01(\x08"\xe0\t\n\x1bStartChildWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x03 \x01(\t\x12\x15\n\rworkflow_type\x18\x04 \x01(\t\x12\x12\n\ntask_queue\x18\x05 \x01(\t\x12.\n\x05input\x18\x06 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12=\n\x1aworkflow_execution_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x37\n\x14workflow_run_timeout\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x38\n\x15workflow_task_timeout\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x46\n\x13parent_close_policy\x18\n \x01(\x0e\x32).coresdk.child_workflow.ParentClosePolicy\x12N\n\x18workflow_id_reuse_policy\x18\x0c \x01(\x0e\x32,.temporal.api.enums.v1.WorkflowIdReusePolicy\x12\x39\n\x0cretry_policy\x18\r \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x15\n\rcron_schedule\x18\x0e \x01(\t\x12T\n\x07headers\x18\x0f \x03(\x0b\x32\x43.coresdk.workflow_commands.StartChildWorkflowExecution.HeadersEntry\x12N\n\x04memo\x18\x10 \x03(\x0b\x32@.coresdk.workflow_commands.StartChildWorkflowExecution.MemoEntry\x12g\n\x11search_attributes\x18\x11 \x03(\x0b\x32L.coresdk.workflow_commands.StartChildWorkflowExecution.SearchAttributesEntry\x12P\n\x11\x63\x61ncellation_type\x18\x12 \x01(\x0e\x32\x35.coresdk.child_workflow.ChildWorkflowCancellationType\x12;\n\x11versioning_intent\x18\x13 \x01(\x0e\x32 .coresdk.common.VersioningIntent\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aL\n\tMemoEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x1aX\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01":\n\x1c\x43\x61ncelChildWorkflowExecution\x12\x1a\n\x12\x63hild_workflow_seq\x18\x01 \x01(\r"\xa7\x01\n&RequestCancelExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12I\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecutionH\x00\x12\x1b\n\x11\x63hild_workflow_id\x18\x03 \x01(\tH\x00\x42\x08\n\x06target"\x8f\x03\n\x1fSignalExternalWorkflowExecution\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12I\n\x12workflow_execution\x18\x02 \x01(\x0b\x32+.coresdk.common.NamespacedWorkflowExecutionH\x00\x12\x1b\n\x11\x63hild_workflow_id\x18\x03 \x01(\tH\x00\x12\x13\n\x0bsignal_name\x18\x04 \x01(\t\x12-\n\x04\x61rgs\x18\x05 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12X\n\x07headers\x18\x06 \x03(\x0b\x32G.coresdk.workflow_commands.SignalExternalWorkflowExecution.HeadersEntry\x1aO\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\x42\x08\n\x06target"#\n\x14\x43\x61ncelSignalWorkflow\x12\x0b\n\x03seq\x18\x01 \x01(\r"\xe6\x01\n\x1eUpsertWorkflowSearchAttributes\x12j\n\x11search_attributes\x18\x01 \x03(\x0b\x32O.coresdk.workflow_commands.UpsertWorkflowSearchAttributes.SearchAttributesEntry\x1aX\n\x15SearchAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"O\n\x18ModifyWorkflowProperties\x12\x33\n\rupserted_memo\x18\x01 \x01(\x0b\x32\x1c.temporal.api.common.v1.Memo"\xd2\x01\n\x0eUpdateResponse\x12\x1c\n\x14protocol_instance_id\x18\x01 \x01(\t\x12*\n\x08\x61\x63\x63\x65pted\x18\x02 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x34\n\x08rejected\x18\x03 \x01(\x0b\x32 .temporal.api.failure.v1.FailureH\x00\x12\x34\n\tcompleted\x18\x04 \x01(\x0b\x32\x1f.temporal.api.common.v1.PayloadH\x00\x42\n\n\x08response*X\n\x18\x41\x63tivityCancellationType\x12\x0e\n\nTRY_CANCEL\x10\x00\x12\x1f\n\x1bWAIT_CANCELLATION_COMPLETED\x10\x01\x12\x0b\n\x07\x41\x42\x41NDON\x10\x02\x42,\xea\x02)Temporalio::Bridge::Api::WorkflowCommandsb\x06proto3' ) _ACTIVITYCANCELLATIONTYPE = DESCRIPTOR.enum_types_by_name["ActivityCancellationType"] @@ -109,6 +110,7 @@ _UPSERTWORKFLOWSEARCHATTRIBUTES.nested_types_by_name["SearchAttributesEntry"] ) _MODIFYWORKFLOWPROPERTIES = DESCRIPTOR.message_types_by_name["ModifyWorkflowProperties"] +_UPDATERESPONSE = DESCRIPTOR.message_types_by_name["UpdateResponse"] WorkflowCommand = _reflection.GeneratedProtocolMessageType( "WorkflowCommand", (_message.Message,), @@ -440,6 +442,17 @@ ) _sym_db.RegisterMessage(ModifyWorkflowProperties) +UpdateResponse = _reflection.GeneratedProtocolMessageType( + "UpdateResponse", + (_message.Message,), + { + "DESCRIPTOR": _UPDATERESPONSE, + "__module__": "temporal.sdk.core.workflow_commands.workflow_commands_pb2" + # @@protoc_insertion_point(class_scope:coresdk.workflow_commands.UpdateResponse) + }, +) +_sym_db.RegisterMessage(UpdateResponse) + if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = ( @@ -465,68 +478,70 @@ _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._serialized_options = b"8\001" _UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY._options = None _UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY._serialized_options = b"8\001" - _ACTIVITYCANCELLATIONTYPE._serialized_start = 7388 - _ACTIVITYCANCELLATIONTYPE._serialized_end = 7476 - _WORKFLOWCOMMAND._serialized_start = 365 - _WORKFLOWCOMMAND._serialized_end = 2073 - _STARTTIMER._serialized_start = 2075 - _STARTTIMER._serialized_end = 2158 - _CANCELTIMER._serialized_start = 2160 - _CANCELTIMER._serialized_end = 2186 - _SCHEDULEACTIVITY._serialized_start = 2189 - _SCHEDULEACTIVITY._serialized_end = 2961 - _SCHEDULEACTIVITY_HEADERSENTRY._serialized_start = 2882 - _SCHEDULEACTIVITY_HEADERSENTRY._serialized_end = 2961 - _SCHEDULELOCALACTIVITY._serialized_start = 2964 - _SCHEDULELOCALACTIVITY._serialized_end = 3714 - _SCHEDULELOCALACTIVITY_HEADERSENTRY._serialized_start = 2882 - _SCHEDULELOCALACTIVITY_HEADERSENTRY._serialized_end = 2961 - _REQUESTCANCELACTIVITY._serialized_start = 3716 - _REQUESTCANCELACTIVITY._serialized_end = 3752 - _REQUESTCANCELLOCALACTIVITY._serialized_start = 3754 - _REQUESTCANCELLOCALACTIVITY._serialized_end = 3795 - _QUERYRESULT._serialized_start = 3798 - _QUERYRESULT._serialized_end = 3954 - _QUERYSUCCESS._serialized_start = 3956 - _QUERYSUCCESS._serialized_end = 4021 - _COMPLETEWORKFLOWEXECUTION._serialized_start = 4023 - _COMPLETEWORKFLOWEXECUTION._serialized_end = 4099 - _FAILWORKFLOWEXECUTION._serialized_start = 4101 - _FAILWORKFLOWEXECUTION._serialized_end = 4175 - _CONTINUEASNEWWORKFLOWEXECUTION._serialized_start = 4178 - _CONTINUEASNEWWORKFLOWEXECUTION._serialized_end = 5069 - _CONTINUEASNEWWORKFLOWEXECUTION_MEMOENTRY._serialized_start = 4822 - _CONTINUEASNEWWORKFLOWEXECUTION_MEMOENTRY._serialized_end = 4898 - _CONTINUEASNEWWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 2882 - _CONTINUEASNEWWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 2961 - _CONTINUEASNEWWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_start = 4981 - _CONTINUEASNEWWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_end = 5069 - _CANCELWORKFLOWEXECUTION._serialized_start = 5071 - _CANCELWORKFLOWEXECUTION._serialized_end = 5096 - _SETPATCHMARKER._serialized_start = 5098 - _SETPATCHMARKER._serialized_end = 5152 - _STARTCHILDWORKFLOWEXECUTION._serialized_start = 5155 - _STARTCHILDWORKFLOWEXECUTION._serialized_end = 6403 - _STARTCHILDWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 2882 - _STARTCHILDWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 2961 - _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._serialized_start = 4822 - _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._serialized_end = 4898 - _STARTCHILDWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_start = 4981 - _STARTCHILDWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_end = 5069 - _CANCELCHILDWORKFLOWEXECUTION._serialized_start = 6405 - _CANCELCHILDWORKFLOWEXECUTION._serialized_end = 6463 - _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_start = 6466 - _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_end = 6633 - _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_start = 6636 - _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_end = 7035 - _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 2882 - _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 2961 - _CANCELSIGNALWORKFLOW._serialized_start = 7037 - _CANCELSIGNALWORKFLOW._serialized_end = 7072 - _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_start = 7075 - _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_end = 7305 - _UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY._serialized_start = 4981 - _UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY._serialized_end = 5069 - _MODIFYWORKFLOWPROPERTIES._serialized_start = 7307 - _MODIFYWORKFLOWPROPERTIES._serialized_end = 7386 + _ACTIVITYCANCELLATIONTYPE._serialized_start = 7700 + _ACTIVITYCANCELLATIONTYPE._serialized_end = 7788 + _WORKFLOWCOMMAND._serialized_start = 394 + _WORKFLOWCOMMAND._serialized_end = 2172 + _STARTTIMER._serialized_start = 2174 + _STARTTIMER._serialized_end = 2257 + _CANCELTIMER._serialized_start = 2259 + _CANCELTIMER._serialized_end = 2285 + _SCHEDULEACTIVITY._serialized_start = 2288 + _SCHEDULEACTIVITY._serialized_end = 3060 + _SCHEDULEACTIVITY_HEADERSENTRY._serialized_start = 2981 + _SCHEDULEACTIVITY_HEADERSENTRY._serialized_end = 3060 + _SCHEDULELOCALACTIVITY._serialized_start = 3063 + _SCHEDULELOCALACTIVITY._serialized_end = 3813 + _SCHEDULELOCALACTIVITY_HEADERSENTRY._serialized_start = 2981 + _SCHEDULELOCALACTIVITY_HEADERSENTRY._serialized_end = 3060 + _REQUESTCANCELACTIVITY._serialized_start = 3815 + _REQUESTCANCELACTIVITY._serialized_end = 3851 + _REQUESTCANCELLOCALACTIVITY._serialized_start = 3853 + _REQUESTCANCELLOCALACTIVITY._serialized_end = 3894 + _QUERYRESULT._serialized_start = 3897 + _QUERYRESULT._serialized_end = 4053 + _QUERYSUCCESS._serialized_start = 4055 + _QUERYSUCCESS._serialized_end = 4120 + _COMPLETEWORKFLOWEXECUTION._serialized_start = 4122 + _COMPLETEWORKFLOWEXECUTION._serialized_end = 4198 + _FAILWORKFLOWEXECUTION._serialized_start = 4200 + _FAILWORKFLOWEXECUTION._serialized_end = 4274 + _CONTINUEASNEWWORKFLOWEXECUTION._serialized_start = 4277 + _CONTINUEASNEWWORKFLOWEXECUTION._serialized_end = 5168 + _CONTINUEASNEWWORKFLOWEXECUTION_MEMOENTRY._serialized_start = 4921 + _CONTINUEASNEWWORKFLOWEXECUTION_MEMOENTRY._serialized_end = 4997 + _CONTINUEASNEWWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 2981 + _CONTINUEASNEWWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 3060 + _CONTINUEASNEWWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_start = 5080 + _CONTINUEASNEWWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_end = 5168 + _CANCELWORKFLOWEXECUTION._serialized_start = 5170 + _CANCELWORKFLOWEXECUTION._serialized_end = 5195 + _SETPATCHMARKER._serialized_start = 5197 + _SETPATCHMARKER._serialized_end = 5251 + _STARTCHILDWORKFLOWEXECUTION._serialized_start = 5254 + _STARTCHILDWORKFLOWEXECUTION._serialized_end = 6502 + _STARTCHILDWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 2981 + _STARTCHILDWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 3060 + _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._serialized_start = 4921 + _STARTCHILDWORKFLOWEXECUTION_MEMOENTRY._serialized_end = 4997 + _STARTCHILDWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_start = 5080 + _STARTCHILDWORKFLOWEXECUTION_SEARCHATTRIBUTESENTRY._serialized_end = 5168 + _CANCELCHILDWORKFLOWEXECUTION._serialized_start = 6504 + _CANCELCHILDWORKFLOWEXECUTION._serialized_end = 6562 + _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_start = 6565 + _REQUESTCANCELEXTERNALWORKFLOWEXECUTION._serialized_end = 6732 + _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_start = 6735 + _SIGNALEXTERNALWORKFLOWEXECUTION._serialized_end = 7134 + _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._serialized_start = 2981 + _SIGNALEXTERNALWORKFLOWEXECUTION_HEADERSENTRY._serialized_end = 3060 + _CANCELSIGNALWORKFLOW._serialized_start = 7136 + _CANCELSIGNALWORKFLOW._serialized_end = 7171 + _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_start = 7174 + _UPSERTWORKFLOWSEARCHATTRIBUTES._serialized_end = 7404 + _UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY._serialized_start = 5080 + _UPSERTWORKFLOWSEARCHATTRIBUTES_SEARCHATTRIBUTESENTRY._serialized_end = 5168 + _MODIFYWORKFLOWPROPERTIES._serialized_start = 7406 + _MODIFYWORKFLOWPROPERTIES._serialized_end = 7485 + _UPDATERESPONSE._serialized_start = 7488 + _UPDATERESPONSE._serialized_end = 7698 # @@protoc_insertion_point(module_scope) diff --git a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi index ebd97abe..3bea766b 100644 --- a/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi +++ b/temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi @@ -10,6 +10,7 @@ import builtins import collections.abc import google.protobuf.descriptor import google.protobuf.duration_pb2 +import google.protobuf.empty_pb2 import google.protobuf.internal.containers import google.protobuf.internal.enum_type_wrapper import google.protobuf.message @@ -91,6 +92,7 @@ class WorkflowCommand(google.protobuf.message.Message): REQUEST_CANCEL_LOCAL_ACTIVITY_FIELD_NUMBER: builtins.int UPSERT_WORKFLOW_SEARCH_ATTRIBUTES_FIELD_NUMBER: builtins.int MODIFY_WORKFLOW_PROPERTIES_FIELD_NUMBER: builtins.int + UPDATE_RESPONSE_FIELD_NUMBER: builtins.int @property def start_timer(self) -> global___StartTimer: ... @property @@ -141,6 +143,8 @@ class WorkflowCommand(google.protobuf.message.Message): ) -> global___UpsertWorkflowSearchAttributes: ... @property def modify_workflow_properties(self) -> global___ModifyWorkflowProperties: ... + @property + def update_response(self) -> global___UpdateResponse: ... def __init__( self, *, @@ -169,6 +173,7 @@ class WorkflowCommand(google.protobuf.message.Message): upsert_workflow_search_attributes: global___UpsertWorkflowSearchAttributes | None = ..., modify_workflow_properties: global___ModifyWorkflowProperties | None = ..., + update_response: global___UpdateResponse | None = ..., ) -> None: ... def HasField( self, @@ -209,6 +214,8 @@ class WorkflowCommand(google.protobuf.message.Message): b"start_child_workflow_execution", "start_timer", b"start_timer", + "update_response", + b"update_response", "upsert_workflow_search_attributes", b"upsert_workflow_search_attributes", "variant", @@ -254,6 +261,8 @@ class WorkflowCommand(google.protobuf.message.Message): b"start_child_workflow_execution", "start_timer", b"start_timer", + "update_response", + b"update_response", "upsert_workflow_search_attributes", b"upsert_workflow_search_attributes", "variant", @@ -283,6 +292,7 @@ class WorkflowCommand(google.protobuf.message.Message): "request_cancel_local_activity", "upsert_workflow_search_attributes", "modify_workflow_properties", + "update_response", ] | None ): ... @@ -1580,3 +1590,79 @@ class ModifyWorkflowProperties(google.protobuf.message.Message): ) -> None: ... global___ModifyWorkflowProperties = ModifyWorkflowProperties + +class UpdateResponse(google.protobuf.message.Message): + """A reply to a `DoUpdate` job - lang must run the update's validator if told to, and then + immediately run the handler, if the update was accepted. + + There must always be an accepted or rejected response immediately, in the same activation as + this job, to indicate the result of the validator. Accepted for ran and accepted or skipped, or + rejected for rejected. + + Then, in the same or any subsequent activation, after the update handler has completed, respond + with completed or rejected as appropriate for the result of the handler. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROTOCOL_INSTANCE_ID_FIELD_NUMBER: builtins.int + ACCEPTED_FIELD_NUMBER: builtins.int + REJECTED_FIELD_NUMBER: builtins.int + COMPLETED_FIELD_NUMBER: builtins.int + protocol_instance_id: builtins.str + """The protocol message instance ID""" + @property + def accepted(self) -> google.protobuf.empty_pb2.Empty: + """Must be sent if the update's validator has passed (or lang was not asked to run it, and + thus should be considered already-accepted, allowing lang to always send the same + sequence on replay). + """ + @property + def rejected(self) -> temporalio.api.failure.v1.message_pb2.Failure: + """Must be sent if the update's validator does not pass, or after acceptance if the update + handler fails. + """ + @property + def completed(self) -> temporalio.api.common.v1.message_pb2.Payload: + """Must be sent once the update handler completes successfully.""" + def __init__( + self, + *, + protocol_instance_id: builtins.str = ..., + accepted: google.protobuf.empty_pb2.Empty | None = ..., + rejected: temporalio.api.failure.v1.message_pb2.Failure | None = ..., + completed: temporalio.api.common.v1.message_pb2.Payload | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "accepted", + b"accepted", + "completed", + b"completed", + "rejected", + b"rejected", + "response", + b"response", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "accepted", + b"accepted", + "completed", + b"completed", + "protocol_instance_id", + b"protocol_instance_id", + "rejected", + b"rejected", + "response", + b"response", + ], + ) -> None: ... + def WhichOneof( + self, oneof_group: typing_extensions.Literal["response", b"response"] + ) -> typing_extensions.Literal["accepted", "rejected", "completed"] | None: ... + +global___UpdateResponse = UpdateResponse From 2b0d6f02729a21efb3533d9d0f6b08630ed68446 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 12 Oct 2023 12:00:04 -0700 Subject: [PATCH 03/27] Basic happy path working --- temporalio/client.py | 160 ++++++++++++++++++++++++ temporalio/contrib/opentelemetry.py | 11 ++ temporalio/types.py | 4 + temporalio/worker/__init__.py | 2 + temporalio/worker/_interceptor.py | 19 +++ temporalio/worker/_workflow_instance.py | 149 +++++++++++++++++++++- temporalio/workflow.py | 120 ++++++++++++++++++ tests/test_client.py | 5 + tests/worker/test_interceptor.py | 9 ++ tests/worker/test_workflow.py | 94 ++++++++++++++ 10 files changed, 572 insertions(+), 1 deletion(-) diff --git a/temporalio/client.py b/temporalio/client.py index 008cdbcc..7ff1af62 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -43,6 +43,7 @@ import temporalio.api.history.v1 import temporalio.api.schedule.v1 import temporalio.api.taskqueue.v1 +import temporalio.api.update.v1 import temporalio.api.workflow.v1 import temporalio.api.workflowservice.v1 import temporalio.common @@ -1612,6 +1613,72 @@ async def terminate( ) ) + async def update( + self, + update: Union[str, Callable], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: Optional[str] = None, + result_type: Optional[Type] = None, + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + ) -> Any: + """Send an update request to the workflow. + + This will target the workflow with :py:attr:`run_id` if present. To use a + different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. + + .. warning:: + Handles created as a result of :py:meth:`Client.start_workflow` will + signal the latest workflow with the same workflow ID even if it is + unrelated to the started workflow. + + Args: + update: Update function or name on the workflow. + arg: Single argument to the update. + args: Multiple arguments to the update. Cannot be set if arg is. + id: ID of the update. If not set, the server will set a UUID as the ID. + result_type: For string updates, this can set the specific result + type hint to deserialize into. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + RPCError: There was some issue sending the update to the workflow. + """ + update_name: str + ret_type = result_type + if callable(update): + defn = temporalio.workflow._UpdateDefinition.from_fn(update) + if not defn: + raise RuntimeError( + f"Update definition not found on {update.__qualname__}, " + "is it decorated with @workflow.update?" + ) + elif not defn.name: + raise RuntimeError("Cannot invoke dynamic update definition") + # TODO(cretz): Check count/type of args at runtime? + update_name = defn.name + ret_type = defn.ret_type + else: + update_name = str(update) + + return await self._client._impl.update_workflow( + UpdateWorkflowInput( + id=self._id, + run_id=self._run_id, + update_id=id or "", + update=update_name, + args=temporalio.common._arg_or_args(arg, args), + headers={}, + ret_type=ret_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + @dataclass(frozen=True) class AsyncActivityIDReference: @@ -3721,6 +3788,22 @@ def message(self) -> str: return self._message +class WorkflowUpdateFailedError(temporalio.exceptions.TemporalError): + """Error that occurs when an update fails.""" + + def __init__(self, update_id: str, update_name: str, message: str) -> None: + """Create workflow update failed error.""" + super().__init__(message) + self._update_id = update_id + self._update_name = update_name + self._message = message + + @property + def message(self) -> str: + """Get update failed message.""" + return self._message + + class AsyncActivityCancelledError(temporalio.exceptions.TemporalError): """Error that occurs when async activity attempted heartbeat but was cancelled.""" @@ -3851,6 +3934,24 @@ class TerminateWorkflowInput: rpc_timeout: Optional[timedelta] +@dataclass +class UpdateWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.update_workflow`.""" + + # TODO: Wait policy + + id: str + run_id: Optional[str] + update_id: str + update: str + args: Sequence[Any] + headers: Mapping[str, temporalio.api.common.v1.Payload] + # Type may be absent + ret_type: Optional[Type] + rpc_metadata: Mapping[str, str] + rpc_timeout: Optional[timedelta] + + @dataclass class HeartbeatAsyncActivityInput: """Input for :py:meth:`OutboundInterceptor.heartbeat_async_activity`.""" @@ -4095,6 +4196,10 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: """Called for every :py:meth:`WorkflowHandle.terminate` call.""" await self.next.terminate_workflow(input) + async def update_workflow(self, input: UpdateWorkflowInput) -> Any: + """Called for every :py:meth:`WorkflowHandle.signal` call.""" + return await self.next.update_workflow(input) + ### Async activity calls async def heartbeat_async_activity( @@ -4417,6 +4522,61 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout ) + async def update_workflow(self, input: UpdateWorkflowInput) -> Any: + req = temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest( + namespace=self._client.namespace, + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.id, + run_id=input.run_id or "", + ), + request=temporalio.api.update.v1.Request( + meta=temporalio.api.update.v1.Meta( + update_id=input.update_id, + identity=self._client.identity, + ), + input=temporalio.api.update.v1.Input( + name=input.update, + ), + ), + ) + if input.args: + req.request.input.args.payloads.extend( + await self._client.data_converter.encode(input.args) + ) + if input.headers is not None: + temporalio.common._apply_headers( + input.headers, req.request.input.header.fields + ) + try: + resp = await self._client.workflow_service.update_workflow_execution( + req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout + ) + print("resp", resp) + except RPCError as err: + # If the status is INVALID_ARGUMENT, we can assume it's an update + # failed error + if err.status == RPCStatusCode.INVALID_ARGUMENT: + raise WorkflowUpdateFailedError(input.id, input.update, err.message) + else: + raise + if resp.outcome.HasField("failure"): + raise WorkflowUpdateFailedError( + input.id, + input.update, + resp.outcome.failure.message, + ) + if not resp.outcome.success.payloads: + return None + type_hints = [input.ret_type] if input.ret_type else None + results = await self._client.data_converter.decode( + resp.outcome.success.payloads, type_hints + ) + if not results: + return None + elif len(results) > 1: + warnings.warn(f"Expected single update result, got {len(results)}") + return results[0] + ### Async activity calls async def heartbeat_async_activity( diff --git a/temporalio/contrib/opentelemetry.py b/temporalio/contrib/opentelemetry.py index 2701afa1..6a4f42a2 100644 --- a/temporalio/contrib/opentelemetry.py +++ b/temporalio/contrib/opentelemetry.py @@ -244,6 +244,17 @@ async def signal_workflow( ): return await super().signal_workflow(input) + async def update_workflow( + self, input: temporalio.client.UpdateWorkflowInput + ) -> Any: + with self.root._start_as_current_span( + f"UpdateWorkflow:{input.update}", + attributes={"temporalWorkflowID": input.id}, + input=input, + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + return await super().update_workflow(input) + class _TracingActivityInboundInterceptor(temporalio.worker.ActivityInboundInterceptor): def __init__( diff --git a/temporalio/types.py b/temporalio/types.py index d6590338..c6528a84 100644 --- a/temporalio/types.py +++ b/temporalio/types.py @@ -12,6 +12,10 @@ LocalReturnType = TypeVar("LocalReturnType", covariant=True) CallableType = TypeVar("CallableType", bound=Callable[..., Any]) CallableAsyncType = TypeVar("CallableAsyncType", bound=Callable[..., Awaitable[Any]]) +CallableSyncOrAsyncType = TypeVar( + "CallableSyncOrAsyncType", + bound=Callable[..., Union[Any, Awaitable[Any]]], +) CallableSyncOrAsyncReturnNoneType = TypeVar( "CallableSyncOrAsyncReturnNoneType", bound=Callable[..., Union[None, Awaitable[None]]], diff --git a/temporalio/worker/__init__.py b/temporalio/worker/__init__.py index b7804e54..aa7936e6 100644 --- a/temporalio/worker/__init__.py +++ b/temporalio/worker/__init__.py @@ -9,6 +9,7 @@ ExecuteWorkflowInput, HandleQueryInput, HandleSignalInput, + HandleUpdateInput, Interceptor, SignalChildWorkflowInput, SignalExternalWorkflowInput, @@ -53,6 +54,7 @@ "ExecuteWorkflowInput", "HandleQueryInput", "HandleSignalInput", + "HandleUpdateInput", "SignalChildWorkflowInput", "SignalExternalWorkflowInput", "StartActivityInput", diff --git a/temporalio/worker/_interceptor.py b/temporalio/worker/_interceptor.py index 19283e01..aee7853d 100644 --- a/temporalio/worker/_interceptor.py +++ b/temporalio/worker/_interceptor.py @@ -190,6 +190,17 @@ class HandleQueryInput: headers: Mapping[str, temporalio.api.common.v1.Payload] +@dataclass +class HandleUpdateInput: + """Input for :py:meth:`WorkflowInboundInterceptor.handle_update_validator` + and :py:meth:`WorkflowInboundInterceptor.handle_update_handler`.""" + + id: str + update: str + args: Sequence[Any] + headers: Mapping[str, temporalio.api.common.v1.Payload] + + @dataclass class SignalChildWorkflowInput: """Input for :py:meth:`WorkflowOutboundInterceptor.signal_child_workflow`.""" @@ -314,6 +325,14 @@ async def handle_query(self, input: HandleQueryInput) -> Any: """Called to handle a query.""" return await self.next.handle_query(input) + async def handle_update_validator(self, input: HandleUpdateInput) -> Any: + """Called to handle an update's validation stage.""" + return await self.next.handle_update_validator(input) + + async def handle_update_handler(self, input: HandleUpdateInput) -> Any: + """Called to handle an update's handler.""" + return await self.next.handle_update_handler(input) + class WorkflowOutboundInterceptor: """Outbound interceptor to wrap calls made from within workflows. diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index d5afece7..cb5f436a 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -37,6 +37,7 @@ cast, ) +import google.protobuf.empty_pb2 from typing_extensions import TypeAlias, TypedDict import temporalio.activity @@ -57,6 +58,7 @@ ExecuteWorkflowInput, HandleQueryInput, HandleSignalInput, + HandleUpdateInput, SignalChildWorkflowInput, SignalExternalWorkflowInput, StartActivityInput, @@ -209,10 +211,11 @@ def __init__(self, det: WorkflowInstanceDetails) -> None: # See https://bugs.python.org/issue21163 and others. self._tasks: Set[asyncio.Task] = set() - # We maintain signals and queries on this class since handlers can be + # We maintain signals, queries, and updates on this class since handlers can be # added during workflow execution self._signals = dict(self._defn.signals) self._queries = dict(self._defn.queries) + self._updates = dict(self._defn.updates) # Add stack trace handler # TODO(cretz): Is it ok that this can be forcefully overridden by the @@ -367,6 +370,8 @@ def _apply( ) -> None: if job.HasField("cancel_workflow"): self._apply_cancel_workflow(job.cancel_workflow) + elif job.HasField("do_update"): + self._apply_do_update(job.do_update) elif job.HasField("fire_timer"): self._apply_fire_timer(job.fire_timer) elif job.HasField("query_workflow"): @@ -414,6 +419,117 @@ def _apply_cancel_workflow( # this cancellation to the next iteration of the event loop. self.call_soon(self._primary_task.cancel) + def _apply_do_update( + self, job: temporalio.bridge.proto.workflow_activation.DoUpdate + ): + # self._buffered_updates.setdefault(job.signal_name, []).append(job) + # return + + # Either the command to accept/reject, or the command to respond to the update + current_command = self._add_command() + current_command.update_response.protocol_instance_id = job.protocol_instance_id + try: + defn = self._updates.get(job.name) or self._updates.get(None) + if not defn: + raise RuntimeError( + f"Update handler for '{job.name}' expected but not found, and there is no dynamic handler" + ) + # TODO Actually run validator + current_command.update_response.accepted.SetInParent() + current_command = None + + # Run the handler + args = self._process_handler_args( + job.name, + job.input, + defn.name, + defn.arg_types, + defn.dynamic_vararg, + ) + input = HandleUpdateInput( + # TODO: update id vs proto instance id + id=job.protocol_instance_id, + update=job.name, + args=args, + headers=job.headers, + ) + + async def run_update() -> None: + try: + success = await self._inbound.handle_update_handler(input) + result_payloads = self._payload_converter.to_payloads([success]) + if len(result_payloads) != 1: + raise ValueError( + f"Expected 1 result payload, got {len(result_payloads)}" + ) + command = self._add_command() + command.update_response.protocol_instance_id = ( + job.protocol_instance_id + ) + command.update_response.completed.CopyFrom(result_payloads[0]) + # TODO: Dedupe exception handling if it makes sense + except (Exception, asyncio.CancelledError) as err: + logger.debug( + f"Workflow raised failure with run ID {self._info.run_id}", + exc_info=True, + ) + + # All asyncio cancelled errors become Temporal cancelled errors + if isinstance(err, asyncio.CancelledError): + err = temporalio.exceptions.CancelledError(str(err)) + + if isinstance(err, temporalio.exceptions.FailureError): + # All other failure errors fail the update + command = self._add_command() + command.update_response.protocol_instance_id = ( + job.protocol_instance_id + ) + self._failure_converter.to_failure( + err, + self._payload_converter, + command.update_response.rejected.cause, + ) + else: + # All other exceptions fail the task + self._current_activation_error = err + except BaseException as err: + # During tear down, generator exit and no-runtime exceptions can appear + if not self._deleting: + raise + if not isinstance( + err, + ( + GeneratorExit, + temporalio.workflow._NotInWorkflowEventLoopError, + ), + ): + logger.debug( + "Ignoring exception while deleting workflow", exc_info=True + ) + + self.create_task( + run_update(), + name=f"update: {job.name}", + ) + + except Exception as err: + logger.error(f"Ahhhh problem {err}") + if current_command is None: + # We have already finished the validator, but haven't started the handler. If we have an exception here + # we need to add a command to reject the update. + current_command = self._add_command() + current_command.update_response.protocol_instance_id = ( + job.protocol_instance_id + ) + try: + self._failure_converter.to_failure( + err, + self._payload_converter, + current_command.update_response.rejected.cause, + ) + except Exception as inner_err: + raise ValueError("Failed converting application error") from inner_err + def _apply_fire_timer( self, job: temporalio.bridge.proto.workflow_activation.FireTimer ) -> None: @@ -767,6 +883,13 @@ def workflow_get_signal_handler(self, name: Optional[str]) -> Optional[Callable] # Bind if a method return defn.bind_fn(self._object) if defn.is_method else defn.fn + def workflow_get_update_handler(self, name: Optional[str]) -> Optional[Callable]: + defn = self._updates.get(name) + if not defn: + return None + # Bind if a method + return defn.bind_fn(self._object) if defn.is_method else defn.fn + def workflow_info(self) -> temporalio.workflow.Info: return self._outbound.info() @@ -886,6 +1009,8 @@ def workflow_set_signal_handler( else: self._signals.pop(name, None) + # TODO: Set update handler? + def workflow_start_activity( self, activity: Any, @@ -1654,6 +1779,28 @@ async def handle_query(self, input: HandleQueryInput) -> Any: else: return handler(*input.args) + async def handle_update_validator(self, input: HandleUpdateInput) -> Any: + handler = self._instance.workflow_get_update_handler( + input.update + ) or self._instance.workflow_get_update_handler(None) + # Handler should always be present at this point + assert handler + if inspect.iscoroutinefunction(handler): + return await handler(*input.args) + else: + return handler(*input.args) + + async def handle_update_handler(self, input: HandleUpdateInput) -> Any: + handler = self._instance.workflow_get_update_handler( + input.update + ) or self._instance.workflow_get_update_handler(None) + # Handler should always be present at this point + assert handler + if inspect.iscoroutinefunction(handler): + return await handler(*input.args) + else: + return handler(*input.args) + class _WorkflowOutboundImpl(WorkflowOutboundInterceptor): def __init__(self, instance: _WorkflowInstanceImpl) -> None: diff --git a/temporalio/workflow.py b/temporalio/workflow.py index 2d5a7646..cb44ce25 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -52,6 +52,7 @@ CallableAsyncType, CallableSyncNoParam, CallableSyncOrAsyncReturnNoneType, + CallableSyncOrAsyncType, CallableSyncSingleParam, CallableType, ClassType, @@ -446,6 +447,10 @@ def workflow_get_query_handler(self, name: Optional[str]) -> Optional[Callable]: def workflow_get_signal_handler(self, name: Optional[str]) -> Optional[Callable]: ... + @abstractmethod + def workflow_get_update_handler(self, name: Optional[str]) -> Optional[Callable]: + ... + @abstractmethod def workflow_info(self) -> Info: ... @@ -746,6 +751,57 @@ def time_ns() -> int: return _Runtime.current().workflow_time_ns() +def update( + fn: Optional[CallableSyncOrAsyncType] = None, + *, + name: Optional[str] = None, + dynamic: Optional[bool] = False, +): + """Decorator for a workflow update handler method. + + This is set on any async or non-async method that you wish to be called upon + receiving an update. If a function overrides one with this decorator, it too + must be decorated. + + You may also optionally define a validator method that will be called before + this handler you have applied this decorator to. You can specify the validator + with ``@update_handler_function_name.validator``. + + Update methods can only have positional parameters. Best practice for + non-dynamic update methods is to only take a single object/dataclass + argument that can accept more fields later if needed. The handler may return + a serializable value which will be sent back to the caller of the update. + + Args: + fn: The function to decorate. + name: Update name. Defaults to method ``__name__``. Cannot be present + when ``dynamic`` is present. + dynamic: If true, this handles all updates not otherwise handled. The + parameters of the method must be self, a string name, and a + ``*args`` positional varargs. Cannot be present when ``name`` is + present. + """ + + def with_name( + name: Optional[str], fn: CallableSyncOrAsyncType + ) -> CallableSyncOrAsyncType: + defn = _UpdateDefinition(name=name, fn=fn, is_method=True) + setattr(fn, "__temporal_update_definition", defn) + if defn.dynamic_vararg: + raise RuntimeError( + "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", + ) + return fn + + if name is not None or dynamic: + if name is not None and dynamic: + raise RuntimeError("Cannot provide name and dynamic boolean") + return partial(with_name, name) + if fn is None: + raise RuntimeError("Cannot create update without function or name or dynamic") + return with_name(fn.__name__, fn) + + def upsert_search_attributes(attributes: temporalio.common.SearchAttributes) -> None: """Upsert search attributes for this workflow. @@ -952,6 +1008,7 @@ class _Definition: run_fn: Callable[..., Awaitable] signals: Mapping[Optional[str], _SignalDefinition] queries: Mapping[Optional[str], _QueryDefinition] + updates: Mapping[Optional[str], _UpdateDefinition] sandboxed: bool # Types loaded on post init if both are None arg_types: Optional[List[Type]] = None @@ -1004,6 +1061,7 @@ def _apply_to_class( seen_run_attr = False signals: Dict[Optional[str], _SignalDefinition] = {} queries: Dict[Optional[str], _QueryDefinition] = {} + updates: Dict[Optional[str], _UpdateDefinition] = {} for name, member in members: if hasattr(member, "__temporal_workflow_run"): seen_run_attr = True @@ -1045,6 +1103,18 @@ def _apply_to_class( ) else: queries[query_defn.name] = query_defn + elif hasattr(member, "__temporal_update_definition"): + update_defn = cast( + _UpdateDefinition, getattr(member, "__temporal_update_definition") + ) + if update_defn.name in updates: + defn_name = update_defn.name or "" + issues.append( + f"Multiple update methods found for {defn_name} " + f"(at least on {name} and {updates[update_defn.name].fn.__name__})" + ) + else: + updates[update_defn.name] = update_defn # Check base classes haven't defined things with different decorators for base_cls in inspect.getmro(cls)[1:]: @@ -1095,6 +1165,7 @@ def _apply_to_class( run_fn=run_fn, signals=signals, queries=queries, + updates=updates, sandboxed=sandboxed, ) setattr(cls, "__temporal_workflow_definition", defn) @@ -1242,6 +1313,55 @@ def bind_fn(self, obj: Any) -> Callable[..., Any]: return _bind_method(obj, self.fn) +@dataclass(frozen=True) +class _UpdateDefinition: + # None if dynamic + name: Optional[str] + fn: Callable[..., Union[Any, Awaitable[Any]]] + is_method: bool + # Types loaded on post init if None + arg_types: Optional[List[Type]] = None + ret_type: Optional[Type] = None + dynamic_vararg: bool = False + + @staticmethod + def from_fn(fn: Callable) -> Optional[_UpdateDefinition]: + return getattr(fn, "__temporal_update_definition", None) + + @staticmethod + def must_name_from_fn_or_str(update: Union[str, Callable]) -> str: + if callable(update): + defn = _UpdateDefinition.from_fn(update) + if not defn: + raise RuntimeError( + f"Update definition not found on {update.__qualname__}, " + "is it decorated with @workflow.update?" + ) + elif not defn.name: + raise RuntimeError("Cannot invoke dynamic update definition") + # TODO(cretz): Check count/type of args at runtime? + return defn.name + return str(update) + + def __post_init__(self) -> None: + if self.arg_types is None: + arg_types, ret_type = temporalio.common._type_hints_from_func(self.fn) + # If dynamic, assert it + if not self.name: + object.__setattr__( + self, + "dynamic_vararg", + not _assert_dynamic_handler_args( + self.fn, arg_types, self.is_method + ), + ) + object.__setattr__(self, "arg_types", arg_types) + object.__setattr__(self, "ret_type", ret_type) + + def bind_fn(self, obj: Any) -> Callable[..., Any]: + return _bind_method(obj, self.fn) + + # See https://mypy.readthedocs.io/en/latest/runtime_troubles.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime if TYPE_CHECKING: diff --git a/tests/test_client.py b/tests/test_client.py index 913f3aef..255fbb51 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -57,6 +57,7 @@ StartWorkflowInput, TaskReachabilityType, TerminateWorkflowInput, + UpdateWorkflowInput, WorkflowContinuedAsNewError, WorkflowExecutionStatus, WorkflowFailureError, @@ -400,6 +401,10 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: self._parent.traces.append(("terminate_workflow", input)) return await super().terminate_workflow(input) + async def update_workflow(self, input: UpdateWorkflowInput) -> Any: + self._parent.traces.append(("update_workflow", input)) + return await super().update_workflow(input) + async def test_interceptor(client: Client, worker: ExternalWorker): # Create new client from existing client but with a tracing interceptor diff --git a/tests/worker/test_interceptor.py b/tests/worker/test_interceptor.py index 31ed3e9a..46013e6a 100644 --- a/tests/worker/test_interceptor.py +++ b/tests/worker/test_interceptor.py @@ -16,6 +16,7 @@ ExecuteWorkflowInput, HandleQueryInput, HandleSignalInput, + HandleUpdateInput, Interceptor, SignalChildWorkflowInput, SignalExternalWorkflowInput, @@ -78,6 +79,14 @@ async def handle_query(self, input: HandleQueryInput) -> Any: interceptor_traces.append(("workflow.query", input)) return await super().handle_query(input) + async def handle_update_validator(self, input: HandleUpdateInput) -> Any: + interceptor_traces.append(("workflow.update.validator", input)) + return await super().handle_update_validator(input) + + async def handle_update_handler(self, input: HandleUpdateInput) -> Any: + interceptor_traces.append(("workflow.update.handler", input)) + return await super().handle_update_handler(input) + class TracingWorkflowOutboundInterceptor(WorkflowOutboundInterceptor): def continue_as_new(self, input: ContinueAsNewInput) -> NoReturn: diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 666863e2..404b17cf 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3503,3 +3503,97 @@ async def test_workflow_buffered_metrics(client: Client): and update.value == 1 for update in updates ) + + +@workflow.defn +class UpdateHandlersWorkflow: + def __init__(self) -> None: + self._last_event: Optional[str] = None + + @workflow.run + async def run(self) -> None: + # Wait forever + await asyncio.Future() + + @workflow.update + def last_event(self, an_arg: str) -> str: + workflow.logger.info("Running sync") + le = self._last_event or "" + self._last_event = an_arg + return le + + @workflow.update + async def last_event_async(self, an_arg: str) -> str: + workflow.logger.info("Running async") + le = self._last_event or "" + self._last_event = an_arg + await asyncio.sleep(1) + return le + + # @workflow.signal + # def set_signal_handler(self, signal_name: str) -> None: + # def new_handler(arg: str) -> None: + # self._last_event = f"signal {signal_name}: {arg}" + # + # workflow.set_signal_handler(signal_name, new_handler) + # + # @workflow.signal + # def set_dynamic_signal_handler(self) -> None: + # def new_handler(name: str, args: Sequence[RawValue]) -> None: + # arg = workflow.payload_converter().from_payload(args[0].payload, str) + # self._last_event = f"signal dynamic {name}: {arg}" + # + # workflow.set_dynamic_signal_handler(new_handler) + + +async def test_workflow_update_handlers(client: Client): + async with new_worker(client, UpdateHandlersWorkflow) as worker: + handle = await client.start_workflow( + UpdateHandlersWorkflow.run, + id=f"update-handlers-workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Confirm signals buffered when not found + # await handle.signal("unknown_signal1", "val1") + # await handle.signal( + # UpdateHandlersWorkflow.set_signal_handler, "unknown_signal1" + # ) + # assert "signal unknown_signal1: val1" == await handle.query( + # UpdateHandlersWorkflow.last_event + # ) + + # Normal handling + last_event = await handle.update(UpdateHandlersWorkflow.last_event, "val2") + assert "" == last_event + + # Async handler + last_event = await handle.update( + UpdateHandlersWorkflow.last_event_async, "val3" + ) + assert "val2" == last_event + + # # Dynamic signal handling buffered and new + # await handle.signal("unknown_signal2", "val3") + # await handle.signal(UpdateHandlersWorkflow.set_dynamic_signal_handler) + # assert "signal dynamic unknown_signal2: val3" == await handle.query( + # UpdateHandlersWorkflow.last_event + # ) + # await handle.signal("unknown_signal3", "val4") + # assert "signal dynamic unknown_signal3: val4" == await handle.query( + # UpdateHandlersWorkflow.last_event + # ) + # + # # Normal query handling + # await handle.signal( + # UpdateHandlersWorkflow.set_query_handler, "unknown_query1" + # ) + # assert "query unknown_query1: val5" == await handle.query( + # "unknown_query1", "val5" + # ) + # + # # Dynamic query handling + # await handle.signal(UpdateHandlersWorkflow.set_dynamic_query_handler) + # assert "query dynamic unknown_query2: val6" == await handle.query( + # "unknown_query2", "val6" + # ) From c2281c5585c5c753e02eed74ef2d23c5bfac4211 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 12 Oct 2023 17:13:45 -0700 Subject: [PATCH 04/27] Validator running --- temporalio/client.py | 20 +++--- temporalio/worker/_interceptor.py | 2 +- temporalio/worker/_workflow_instance.py | 82 +++++++++++++------------ temporalio/workflow.py | 24 +++++++- tests/worker/test_interceptor.py | 2 +- tests/worker/test_workflow.py | 71 +++++++++++++++++---- 6 files changed, 137 insertions(+), 64 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index 7ff1af62..16d64496 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -3791,17 +3791,18 @@ def message(self) -> str: class WorkflowUpdateFailedError(temporalio.exceptions.TemporalError): """Error that occurs when an update fails.""" - def __init__(self, update_id: str, update_name: str, message: str) -> None: + def __init__(self, update_id: str, update_name: str, cause: BaseException) -> None: """Create workflow update failed error.""" - super().__init__(message) + super().__init__("Workflow update failed") self._update_id = update_id self._update_name = update_name - self._message = message + self.__cause__ = cause @property - def message(self) -> str: - """Get update failed message.""" - return self._message + def cause(self) -> BaseException: + """Cause of the update failure.""" + assert self.__cause__ + return self.__cause__ class AsyncActivityCancelledError(temporalio.exceptions.TemporalError): @@ -4551,19 +4552,20 @@ async def update_workflow(self, input: UpdateWorkflowInput) -> Any: resp = await self._client.workflow_service.update_workflow_execution( req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout ) - print("resp", resp) except RPCError as err: # If the status is INVALID_ARGUMENT, we can assume it's an update # failed error if err.status == RPCStatusCode.INVALID_ARGUMENT: - raise WorkflowUpdateFailedError(input.id, input.update, err.message) + raise WorkflowUpdateFailedError(input.id, input.update, err.cause) else: raise if resp.outcome.HasField("failure"): raise WorkflowUpdateFailedError( input.id, input.update, - resp.outcome.failure.message, + await self._client.data_converter.decode_failure( + resp.outcome.failure.cause + ), ) if not resp.outcome.success.payloads: return None diff --git a/temporalio/worker/_interceptor.py b/temporalio/worker/_interceptor.py index aee7853d..bbfec877 100644 --- a/temporalio/worker/_interceptor.py +++ b/temporalio/worker/_interceptor.py @@ -325,7 +325,7 @@ async def handle_query(self, input: HandleQueryInput) -> Any: """Called to handle a query.""" return await self.next.handle_query(input) - async def handle_update_validator(self, input: HandleUpdateInput) -> Any: + async def handle_update_validator(self, input: HandleUpdateInput) -> None: """Called to handle an update's validation stage.""" return await self.next.handle_update_validator(input) diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index cb5f436a..20db27f3 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -422,23 +422,16 @@ def _apply_cancel_workflow( def _apply_do_update( self, job: temporalio.bridge.proto.workflow_activation.DoUpdate ): - # self._buffered_updates.setdefault(job.signal_name, []).append(job) - # return - - # Either the command to accept/reject, or the command to respond to the update - current_command = self._add_command() - current_command.update_response.protocol_instance_id = job.protocol_instance_id + acceptance_command = self._add_command() + acceptance_command.update_response.protocol_instance_id = ( + job.protocol_instance_id + ) try: defn = self._updates.get(job.name) or self._updates.get(None) if not defn: raise RuntimeError( f"Update handler for '{job.name}' expected but not found, and there is no dynamic handler" ) - # TODO Actually run validator - current_command.update_response.accepted.SetInParent() - current_command = None - - # Run the handler args = self._process_handler_args( job.name, job.input, @@ -446,7 +439,7 @@ def _apply_do_update( defn.arg_types, defn.dynamic_vararg, ) - input = HandleUpdateInput( + handler_input = HandleUpdateInput( # TODO: update id vs proto instance id id=job.protocol_instance_id, update=job.name, @@ -454,9 +447,22 @@ def _apply_do_update( headers=job.headers, ) - async def run_update() -> None: + # Run the validator & handler in a task. Validator needs to be in here since the interceptor might be async. + async def run_update( + accpetance_command: temporalio.bridge.proto.workflow_commands.WorkflowCommand, + ) -> None: + command = accpetance_command try: - success = await self._inbound.handle_update_handler(input) + if defn.validator is not None: + # Run the validator + await self._inbound.handle_update_validator(handler_input) + + # Accept the update + command.update_response.accepted.SetInParent() + command = None + + # Run the handler + success = await self._inbound.handle_update_handler(handler_input) result_payloads = self._payload_converter.to_payloads([success]) if len(result_payloads) != 1: raise ValueError( @@ -470,20 +476,21 @@ async def run_update() -> None: # TODO: Dedupe exception handling if it makes sense except (Exception, asyncio.CancelledError) as err: logger.debug( - f"Workflow raised failure with run ID {self._info.run_id}", + f"Update raised failure with run ID {self._info.run_id}", exc_info=True, ) - # All asyncio cancelled errors become Temporal cancelled errors if isinstance(err, asyncio.CancelledError): - err = temporalio.exceptions.CancelledError(str(err)) - + err = temporalio.exceptions.CancelledError( + f"Cancellation raised within update {err}" + ) if isinstance(err, temporalio.exceptions.FailureError): # All other failure errors fail the update - command = self._add_command() - command.update_response.protocol_instance_id = ( - job.protocol_instance_id - ) + if command is None: + command = self._add_command() + command.update_response.protocol_instance_id = ( + job.protocol_instance_id + ) self._failure_converter.to_failure( err, self._payload_converter, @@ -508,24 +515,17 @@ async def run_update() -> None: ) self.create_task( - run_update(), + run_update(acceptance_command), name=f"update: {job.name}", ) except Exception as err: - logger.error(f"Ahhhh problem {err}") - if current_command is None: - # We have already finished the validator, but haven't started the handler. If we have an exception here - # we need to add a command to reject the update. - current_command = self._add_command() - current_command.update_response.protocol_instance_id = ( - job.protocol_instance_id - ) + # If we failed here we had some issue deserializing or finding the update handlers, so reject it. try: self._failure_converter.to_failure( err, self._payload_converter, - current_command.update_response.rejected.cause, + acceptance_command.update_response.rejected.cause, ) except Exception as inner_err: raise ValueError("Failed converting application error") from inner_err @@ -890,6 +890,13 @@ def workflow_get_update_handler(self, name: Optional[str]) -> Optional[Callable] # Bind if a method return defn.bind_fn(self._object) if defn.is_method else defn.fn + def workflow_get_update_validator(self, name: Optional[str]) -> Optional[Callable]: + defn = self._updates.get(name) + if not defn or not defn.validator: + return None + # Bind if a method + return defn.bind_validator(self._object) if defn.is_method else defn.validator + def workflow_info(self) -> temporalio.workflow.Info: return self._outbound.info() @@ -1779,16 +1786,13 @@ async def handle_query(self, input: HandleQueryInput) -> Any: else: return handler(*input.args) - async def handle_update_validator(self, input: HandleUpdateInput) -> Any: - handler = self._instance.workflow_get_update_handler( + async def handle_update_validator(self, input: HandleUpdateInput) -> None: + handler = self._instance.workflow_get_update_validator( input.update - ) or self._instance.workflow_get_update_handler(None) + ) or self._instance.workflow_get_update_validator(None) # Handler should always be present at this point assert handler - if inspect.iscoroutinefunction(handler): - return await handler(*input.args) - else: - return handler(*input.args) + handler(*input.args) async def handle_update_handler(self, input: HandleUpdateInput) -> Any: handler = self._instance.workflow_get_update_handler( diff --git a/temporalio/workflow.py b/temporalio/workflow.py index cb44ce25..5380babf 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -451,6 +451,10 @@ def workflow_get_signal_handler(self, name: Optional[str]) -> Optional[Callable] def workflow_get_update_handler(self, name: Optional[str]) -> Optional[Callable]: ... + @abstractmethod + def workflow_get_update_validator(self, name: Optional[str]) -> Optional[Callable]: + ... + @abstractmethod def workflow_info(self) -> Info: ... @@ -786,11 +790,12 @@ def with_name( name: Optional[str], fn: CallableSyncOrAsyncType ) -> CallableSyncOrAsyncType: defn = _UpdateDefinition(name=name, fn=fn, is_method=True) - setattr(fn, "__temporal_update_definition", defn) if defn.dynamic_vararg: raise RuntimeError( "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", ) + setattr(fn, "__temporal_update_definition", defn) + setattr(fn, "validator", partial(_update_validator, defn)) return fn if name is not None or dynamic: @@ -802,6 +807,13 @@ def with_name( return with_name(fn.__name__, fn) +def _update_validator( + update_def: _UpdateDefinition, fn: Optional[Callable[..., None]] = None +): + """Decorator for a workflow update validator method.""" + update_def.set_validator(fn) + + def upsert_search_attributes(attributes: temporalio.common.SearchAttributes) -> None: """Upsert search attributes for this workflow. @@ -1322,6 +1334,7 @@ class _UpdateDefinition: # Types loaded on post init if None arg_types: Optional[List[Type]] = None ret_type: Optional[Type] = None + validator: Optional[Callable[..., None]] = None dynamic_vararg: bool = False @staticmethod @@ -1361,6 +1374,15 @@ def __post_init__(self) -> None: def bind_fn(self, obj: Any) -> Callable[..., Any]: return _bind_method(obj, self.fn) + def bind_validator(self, obj: Any) -> Callable[..., Any]: + return _bind_method(obj, self.validator) + + def set_validator(self, validator: Callable[..., None]) -> None: + # TODO: Verify arg types are the same + if self.validator: + raise RuntimeError(f"Validator already set for update {self.name}") + object.__setattr__(self, "validator", validator) + # See https://mypy.readthedocs.io/en/latest/runtime_troubles.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime if TYPE_CHECKING: diff --git a/tests/worker/test_interceptor.py b/tests/worker/test_interceptor.py index 46013e6a..e9f8f55d 100644 --- a/tests/worker/test_interceptor.py +++ b/tests/worker/test_interceptor.py @@ -79,7 +79,7 @@ async def handle_query(self, input: HandleQueryInput) -> Any: interceptor_traces.append(("workflow.query", input)) return await super().handle_query(input) - async def handle_update_validator(self, input: HandleUpdateInput) -> Any: + async def handle_update_validator(self, input: HandleUpdateInput) -> None: interceptor_traces.append(("workflow.update.validator", input)) return await super().handle_update_validator(input) diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 404b17cf..1ae41a5f 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -49,6 +49,7 @@ WorkflowFailureError, WorkflowHandle, WorkflowQueryFailedError, + WorkflowUpdateFailedError, ) from temporalio.common import RawValue, RetryPolicy, SearchAttributes from temporalio.converter import ( @@ -3517,19 +3518,36 @@ async def run(self) -> None: @workflow.update def last_event(self, an_arg: str) -> str: - workflow.logger.info("Running sync") + if an_arg == "fail": + raise ApplicationError("SyncFail") le = self._last_event or "" self._last_event = an_arg return le + @last_event.validator + def last_event_validator(self, an_arg: str) -> None: + workflow.logger.info("Running validator with arg %s", an_arg) + if an_arg == "reject_me": + raise ApplicationError("Rejected") + @workflow.update async def last_event_async(self, an_arg: str) -> str: - workflow.logger.info("Running async") + await asyncio.sleep(1) + if an_arg == "fail": + raise ApplicationError("AsyncFail") le = self._last_event or "" self._last_event = an_arg - await asyncio.sleep(1) return le + @workflow.update + async def runs_activity(self, name: str) -> str: + act = workflow.start_activity_method( + say_hello, name, schedule_to_close_timeout=timedelta(seconds=5) + ) + act.cancel() + await act + return "done" + # @workflow.signal # def set_signal_handler(self, signal_name: str) -> None: # def new_handler(arg: str) -> None: @@ -3554,15 +3572,6 @@ async def test_workflow_update_handlers(client: Client): task_queue=worker.task_queue, ) - # Confirm signals buffered when not found - # await handle.signal("unknown_signal1", "val1") - # await handle.signal( - # UpdateHandlersWorkflow.set_signal_handler, "unknown_signal1" - # ) - # assert "signal unknown_signal1: val1" == await handle.query( - # UpdateHandlersWorkflow.last_event - # ) - # Normal handling last_event = await handle.update(UpdateHandlersWorkflow.last_event, "val2") assert "" == last_event @@ -3572,7 +3581,6 @@ async def test_workflow_update_handlers(client: Client): UpdateHandlersWorkflow.last_event_async, "val3" ) assert "val2" == last_event - # # Dynamic signal handling buffered and new # await handle.signal("unknown_signal2", "val3") # await handle.signal(UpdateHandlersWorkflow.set_dynamic_signal_handler) @@ -3597,3 +3605,40 @@ async def test_workflow_update_handlers(client: Client): # assert "query dynamic unknown_query2: val6" == await handle.query( # "unknown_query2", "val6" # ) + + +async def test_workflow_update_handlers_unhappy(client: Client): + async with new_worker(client, UpdateHandlersWorkflow) as worker: + handle = await client.start_workflow( + UpdateHandlersWorkflow.run, + id=f"update-handlers-workflow-unhappy-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + + # Undefined handler + with pytest.raises(WorkflowUpdateFailedError) as err: + await handle.update("whargarbl", "whatever") + assert isinstance(err.value.cause, ApplicationError) + assert "'whargarbl' expected but not found" in err.value.cause.message + + # Rejection by validator + with pytest.raises(WorkflowUpdateFailedError) as err: + await handle.update(UpdateHandlersWorkflow.last_event, "reject_me") + assert isinstance(err.value.cause, ApplicationError) + assert "Rejected" == err.value.cause.message + + # Failure during update handler + with pytest.raises(WorkflowUpdateFailedError) as err: + await handle.update(UpdateHandlersWorkflow.last_event, "fail") + assert isinstance(err.value.cause, ApplicationError) + assert "SyncFail" == err.value.cause.message + + with pytest.raises(WorkflowUpdateFailedError) as err: + await handle.update(UpdateHandlersWorkflow.last_event_async, "fail") + assert isinstance(err.value.cause, ApplicationError) + assert "AsyncFail" == err.value.cause.message + + # Cancel inside handler + with pytest.raises(WorkflowUpdateFailedError) as err: + await handle.update(UpdateHandlersWorkflow.runs_activity, "foo") + assert isinstance(err.value.cause, CancelledError) From 84b909c238d00e91f1c6e5c7750d17646969a795 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 13 Oct 2023 14:57:20 -0700 Subject: [PATCH 05/27] Add UpdateHandle. Polling not yet implemented. --- temporalio/client.py | 189 ++++++++++++++++++++++++++++++++++++------- tests/test_client.py | 9 ++- 2 files changed, 167 insertions(+), 31 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index 16d64496..10d1d1f2 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -1624,14 +1624,14 @@ async def update( rpc_metadata: Mapping[str, str] = {}, rpc_timeout: Optional[timedelta] = None, ) -> Any: - """Send an update request to the workflow. + """Send an update request to the workflow and wait for it to complete. This will target the workflow with :py:attr:`run_id` if present. To use a different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. .. warning:: - Handles created as a result of :py:meth:`Client.start_workflow` will - signal the latest workflow with the same workflow ID even if it is + WorkflowHandles created as a result of :py:meth:`Client.start_workflow` will + send updates to the latest workflow with the same workflow ID even if it is unrelated to the started workflow. Args: @@ -1645,6 +1645,55 @@ async def update( client-level RPC metadata keys. rpc_timeout: Optional RPC deadline to set for the RPC call. + Raises: + RPCError: There was some issue sending the update to the workflow. + """ + handle = await self.start_update( + update, + arg, + args=args, + id=id, + wait_for_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, + result_type=result_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + return await handle.result() + + async def start_update( + self, + update: Union[str, Callable], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: Optional[str] = None, + wait_for_stage: temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage = temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, + result_type: Optional[Type] = None, + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + ) -> WorkflowUpdateHandle: + """Send an update request to the workflow and return a handle to it. + + This will target the workflow with :py:attr:`run_id` if present. To use a + different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. + + .. warning:: + WorkflowHandles created as a result of :py:meth:`Client.start_workflow` will + send updates to the latest workflow with the same workflow ID even if it is + unrelated to the started workflow. + + Args: + update: Update function or name on the workflow. + arg: Single argument to the update. + args: Multiple arguments to the update. Cannot be set if arg is. + id: ID of the update. If not set, the server will set a UUID as the ID. + wait_for_stage: Specifies at what point in the update request life cycle this request should return. + result_type: For string updates, this can set the specific result + type hint to deserialize into. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + Raises: RPCError: There was some issue sending the update to the workflow. """ @@ -1665,7 +1714,7 @@ async def update( else: update_name = str(update) - return await self._client._impl.update_workflow( + return await self._client._impl.start_workflow_update( UpdateWorkflowInput( id=self._id, run_id=self._run_id, @@ -1676,6 +1725,7 @@ async def update( ret_type=ret_type, rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout, + wait_for_stage=wait_for_stage, ) ) @@ -2763,7 +2813,9 @@ class ScheduleActionStartWorkflow(ScheduleAction): headers: Optional[Mapping[str, temporalio.api.common.v1.Payload]] = None @staticmethod - def _from_proto(info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo) -> ScheduleActionStartWorkflow: # type: ignore[override] + def _from_proto( + info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo, + ) -> ScheduleActionStartWorkflow: # type: ignore[override] return ScheduleActionStartWorkflow("", raw_info=info) # Overload for no-param workflow @@ -3731,6 +3783,82 @@ async def __anext__(self) -> ScheduleListDescription: return ret +class WorkflowUpdateHandle: + """Handle for a workflow update execution request.""" + + def __init__( + self, + client: Client, + id: str, + name: str, + workflow_id: str, + *, + run_id: Optional[str] = None, + result_type: Optional[Type] = None, + ): + self._client = client + self._id = id + self._name = name + self._workflow_id = workflow_id + self._run_id = run_id + self._result_type = result_type + self._known_result = None + + @property + def id(self) -> str: + """ID of this Update request""" + return self._id + + @property + def name(self) -> str: + """The name of the Update being invoked""" + return self._name + + @property + def workflow_id(self) -> str: + """The ID of the Workflow targeted by this Update""" + return self._workflow_id + + @property + def run_id(self) -> Optional[str]: + """If specified, the specific run of the Workflow targeted by this Update""" + return self._run_id + + async def result( + self, + *, + timeout: Optional[timedelta] = None, + rpc_metadata: Mapping[str, str] = None, + ) -> Any: + outcome: temporalio.api.update.v1.Outcome + if self._known_result is not None: + outcome = self._known_result + else: + # TODO: This + raise NotImplementedError + + if outcome.HasField("failure"): + raise WorkflowUpdateFailedError( + self.id, + self.name, + await self._client.data_converter.decode_failure(outcome.failure.cause), + ) + if not outcome.success.payloads: + return None + type_hints = [self._result_type] if self._result_type else None + results = await self._client.data_converter.decode( + outcome.success.payloads, type_hints + ) + if not results: + return None + elif len(results) > 1: + warnings.warn(f"Expected single update result, got {len(results)}") + return results[0] + + def _set_known_result(self, result: temporalio.api.update.v1.Outcome) -> None: + self._known_result = result + + class WorkflowFailureError(temporalio.exceptions.TemporalError): """Error that occurs when a workflow is unsuccessful.""" @@ -3939,13 +4067,14 @@ class TerminateWorkflowInput: class UpdateWorkflowInput: """Input for :py:meth:`OutboundInterceptor.update_workflow`.""" - # TODO: Wait policy - id: str run_id: Optional[str] update_id: str update: str args: Sequence[Any] + wait_for_stage: Optional[ + temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage + ] headers: Mapping[str, temporalio.api.common.v1.Payload] # Type may be absent ret_type: Optional[Type] @@ -4197,9 +4326,11 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: """Called for every :py:meth:`WorkflowHandle.terminate` call.""" await self.next.terminate_workflow(input) - async def update_workflow(self, input: UpdateWorkflowInput) -> Any: + async def start_workflow_update( + self, input: UpdateWorkflowInput + ) -> WorkflowUpdateHandle: """Called for every :py:meth:`WorkflowHandle.signal` call.""" - return await self.next.update_workflow(input) + return await self.next.start_workflow_update(input) ### Async activity calls @@ -4523,7 +4654,14 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout ) - async def update_workflow(self, input: UpdateWorkflowInput) -> Any: + async def start_workflow_update( + self, input: UpdateWorkflowInput + ) -> WorkflowUpdateHandle: + wait_policy = ( + temporalio.api.update.v1.WaitPolicy(lifecycle_stage=input.wait_for_stage) + if input.wait_for_stage is not None + else None + ) req = temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest( namespace=self._client.namespace, workflow_execution=temporalio.api.common.v1.WorkflowExecution( @@ -4539,6 +4677,7 @@ async def update_workflow(self, input: UpdateWorkflowInput) -> Any: name=input.update, ), ), + wait_policy=wait_policy, ) if input.args: req.request.input.args.payloads.extend( @@ -4559,25 +4698,19 @@ async def update_workflow(self, input: UpdateWorkflowInput) -> Any: raise WorkflowUpdateFailedError(input.id, input.update, err.cause) else: raise - if resp.outcome.HasField("failure"): - raise WorkflowUpdateFailedError( - input.id, - input.update, - await self._client.data_converter.decode_failure( - resp.outcome.failure.cause - ), - ) - if not resp.outcome.success.payloads: - return None - type_hints = [input.ret_type] if input.ret_type else None - results = await self._client.data_converter.decode( - resp.outcome.success.payloads, type_hints + + update_handle = WorkflowUpdateHandle( + client=self._client, + id=input.update_id, + name=input.update, + workflow_id=input.id, + run_id=input.run_id, + result_type=input.ret_type, ) - if not results: - return None - elif len(results) > 1: - warnings.warn(f"Expected single update result, got {len(results)}") - return results[0] + if resp.HasField("outcome"): + update_handle._set_known_result(resp.outcome) + + return update_handle ### Async activity calls diff --git a/tests/test_client.py b/tests/test_client.py index 255fbb51..a535beea 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -64,6 +64,7 @@ WorkflowHandle, WorkflowQueryFailedError, WorkflowQueryRejectedError, + WorkflowUpdateHandle, _history_from_json, ) from temporalio.common import RetryPolicy @@ -401,9 +402,11 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: self._parent.traces.append(("terminate_workflow", input)) return await super().terminate_workflow(input) - async def update_workflow(self, input: UpdateWorkflowInput) -> Any: - self._parent.traces.append(("update_workflow", input)) - return await super().update_workflow(input) + async def start_workflow_update( + self, input: UpdateWorkflowInput + ) -> WorkflowUpdateHandle: + self._parent.traces.append(("start_workflow_update", input)) + return await super().start_workflow_update(input) async def test_interceptor(client: Client, worker: ExternalWorker): From d1e0681d4ce1c3bd5ed4ef938ee19cca7377c6eb Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 16 Oct 2023 09:49:26 -0700 Subject: [PATCH 06/27] Add polling --- temporalio/client.py | 148 +++++++++++++++++++++++----- temporalio/contrib/opentelemetry.py | 2 +- tests/test_client.py | 7 ++ 3 files changed, 130 insertions(+), 27 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index 10d1d1f2..95c18143 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import copy import dataclasses import inspect @@ -1716,7 +1717,7 @@ async def start_update( return await self._client._impl.start_workflow_update( UpdateWorkflowInput( - id=self._id, + workflow_id=self._id, run_id=self._run_id, update_id=id or "", update=update_name, @@ -3829,31 +3830,41 @@ async def result( *, timeout: Optional[timedelta] = None, rpc_metadata: Mapping[str, str] = None, + rpc_timeout: Optional[timedelta] = None, ) -> Any: + """Wait for and return the result of the update. The result may already be known in which case no call is made. + Otherwise the result will be polled for until returned, or until the provided timeout is reached, if specified. + + Args: + timeout: Optional timeout specifying maximum wait time for the result. + rpc_metadata: Headers used on the RPC call. Keys here override client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. If this elapses, the poll is retried until the + overall timeout has been reached. + """ outcome: temporalio.api.update.v1.Outcome if self._known_result is not None: outcome = self._known_result - else: - # TODO: This - raise NotImplementedError - - if outcome.HasField("failure"): - raise WorkflowUpdateFailedError( + return await _update_outcome_to_result( + outcome, self.id, self.name, - await self._client.data_converter.decode_failure(outcome.failure.cause), + self._client.data_converter, + self._result_type, + ) + else: + return await self._client._impl.poll_workflow_update( + PollUpdateWorkflowInput( + self.workflow_id, + self.run_id, + self.id, + self.name, + timeout, + {}, + self._result_type, + rpc_metadata, + rpc_timeout, + ) ) - if not outcome.success.payloads: - return None - type_hints = [self._result_type] if self._result_type else None - results = await self._client.data_converter.decode( - outcome.success.payloads, type_hints - ) - if not results: - return None - elif len(results) > 1: - warnings.warn(f"Expected single update result, got {len(results)}") - return results[0] def _set_known_result(self, result: temporalio.api.update.v1.Outcome) -> None: self._known_result = result @@ -4065,9 +4076,9 @@ class TerminateWorkflowInput: @dataclass class UpdateWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.update_workflow`.""" + """Input for :py:meth:`OutboundInterceptor.start_workflow_update`.""" - id: str + workflow_id: str run_id: Optional[str] update_id: str update: str @@ -4076,7 +4087,21 @@ class UpdateWorkflowInput: temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage ] headers: Mapping[str, temporalio.api.common.v1.Payload] - # Type may be absent + ret_type: Optional[Type] + rpc_metadata: Mapping[str, str] + rpc_timeout: Optional[timedelta] + + +@dataclass +class PollUpdateWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.poll_workflow_update`.""" + + workflow_id: str + run_id: Optional[str] + update_id: str + update: str + timeout: Optional[timedelta] + headers: Mapping[str, temporalio.api.common.v1.Payload] ret_type: Optional[Type] rpc_metadata: Mapping[str, str] rpc_timeout: Optional[timedelta] @@ -4329,9 +4354,13 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: async def start_workflow_update( self, input: UpdateWorkflowInput ) -> WorkflowUpdateHandle: - """Called for every :py:meth:`WorkflowHandle.signal` call.""" + """Called for every :py:meth:`WorkflowHandle.update` and :py:meth:`WorkflowHandle.start_update` call.""" return await self.next.start_workflow_update(input) + async def poll_workflow_update(self, input: PollUpdateWorkflowInput) -> Any: + """May be called when calling :py:math:`WorkflowUpdateHandle.result`.""" + return await self.next.poll_workflow_update(input) + ### Async activity calls async def heartbeat_async_activity( @@ -4665,7 +4694,7 @@ async def start_workflow_update( req = temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest( namespace=self._client.namespace, workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.id, + workflow_id=input.workflow_id, run_id=input.run_id or "", ), request=temporalio.api.update.v1.Request( @@ -4695,7 +4724,9 @@ async def start_workflow_update( # If the status is INVALID_ARGUMENT, we can assume it's an update # failed error if err.status == RPCStatusCode.INVALID_ARGUMENT: - raise WorkflowUpdateFailedError(input.id, input.update, err.cause) + raise WorkflowUpdateFailedError( + input.workflow_id, input.update, err.cause + ) else: raise @@ -4703,7 +4734,7 @@ async def start_workflow_update( client=self._client, id=input.update_id, name=input.update, - workflow_id=input.id, + workflow_id=input.workflow_id, run_id=input.run_id, result_type=input.ret_type, ) @@ -4712,6 +4743,47 @@ async def start_workflow_update( return update_handle + async def poll_workflow_update(self, input: PollUpdateWorkflowInput) -> Any: + req = temporalio.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest( + namespace=self._client.namespace, + update_ref=temporalio.api.update.v1.UpdateRef( + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.workflow_id, + run_id=input.run_id or "", + ), + update_id=input.update_id, + ), + identity=self._client.identity, + wait_policy=temporalio.api.update.v1.WaitPolicy( + lifecycle_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED + ), + ) + try: + # Wait for at most the *overall* timeout + async with asyncio.timeout(input.timeout.total_seconds()): + # Continue polling as long as we have either an empty response, or an *rpc* timeout + while True: + try: + res = await self._client.workflow_service.poll_workflow_execution_update( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + if res.HasField("outcome"): + return await _update_outcome_to_result( + res.outcome, + input.update_id, + input.update, + self._client.data_converter, + input.ret_type, + ) + except RPCError as err: + if err.status == RPCStatusCode.DEADLINE_EXCEEDED: + continue + except TimeoutError: + pass + ### Async activity calls async def heartbeat_async_activity( @@ -5240,6 +5312,30 @@ def _fix_history_enum(prefix: str, parent: Dict[str, Any], *attrs: str) -> None: _fix_history_enum(prefix, child_item, *attrs[1:]) +async def _update_outcome_to_result( + outcome: temporalio.api.update.v1.Outcome, + id: str, + name: str, + converter: temporalio.converter.DataConverter, + rtype: Optional[Type], +) -> Any: + if outcome.HasField("failure"): + raise WorkflowUpdateFailedError( + id, + name, + await converter.decode_failure(outcome.failure.cause), + ) + if not outcome.success.payloads: + return None + type_hints = [rtype] if rtype else None + results = await converter.decode(outcome.success.payloads, type_hints) + if not results: + return None + elif len(results) > 1: + warnings.warn(f"Expected single update result, got {len(results)}") + return results[0] + + @dataclass(frozen=True) class WorkerBuildIdVersionSets: """Represents the sets of compatible Build ID versions associated with some Task Queue, as diff --git a/temporalio/contrib/opentelemetry.py b/temporalio/contrib/opentelemetry.py index 6a4f42a2..b1e1df6b 100644 --- a/temporalio/contrib/opentelemetry.py +++ b/temporalio/contrib/opentelemetry.py @@ -249,7 +249,7 @@ async def update_workflow( ) -> Any: with self.root._start_as_current_span( f"UpdateWorkflow:{input.update}", - attributes={"temporalWorkflowID": input.id}, + attributes={"temporalWorkflowID": input.workflow_id}, input=input, kind=opentelemetry.trace.SpanKind.CLIENT, ): diff --git a/tests/test_client.py b/tests/test_client.py index a535beea..03d77d9f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -36,6 +36,7 @@ Client, Interceptor, OutboundInterceptor, + PollUpdateWorkflowInput, QueryWorkflowInput, RPCError, RPCStatusCode, @@ -408,6 +409,12 @@ async def start_workflow_update( self._parent.traces.append(("start_workflow_update", input)) return await super().start_workflow_update(input) + async def poll_workflow_update( + self, input: PollUpdateWorkflowInput + ) -> WorkflowUpdateHandle: + self._parent.traces.append(("poll_workflow_update", input)) + return await super().poll_workflow_update(input) + async def test_interceptor(client: Client, worker: ExternalWorker): # Create new client from existing client but with a tracing interceptor From 713c847785c39dfbda52f09f0633b4642503ef32 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 16 Oct 2023 11:28:47 -0700 Subject: [PATCH 07/27] Linting / mypy --- temporalio/client.py | 77 ++++++++++++++----------- temporalio/contrib/opentelemetry.py | 17 +++++- temporalio/worker/_interceptor.py | 3 +- temporalio/worker/_workflow_instance.py | 3 +- temporalio/workflow.py | 7 ++- tests/test_workflow.py | 2 + tests/worker/test_workflow.py | 6 +- 7 files changed, 73 insertions(+), 42 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index 95c18143..ccde4f80 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -8,6 +8,7 @@ import inspect import json import re +import sys import uuid import warnings from abc import ABC, abstractmethod @@ -1668,7 +1669,7 @@ async def start_update( *, args: Sequence[Any] = [], id: Optional[str] = None, - wait_for_stage: temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage = temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, + wait_for_stage: temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.ValueType = temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, result_type: Optional[Type] = None, rpc_metadata: Mapping[str, str] = {}, rpc_timeout: Optional[timedelta] = None, @@ -2815,8 +2816,8 @@ class ScheduleActionStartWorkflow(ScheduleAction): @staticmethod def _from_proto( - info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo, - ) -> ScheduleActionStartWorkflow: # type: ignore[override] + info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo, # type: ignore[override] + ) -> ScheduleActionStartWorkflow: return ScheduleActionStartWorkflow("", raw_info=info) # Overload for no-param workflow @@ -3797,13 +3798,18 @@ def __init__( run_id: Optional[str] = None, result_type: Optional[Type] = None, ): + """Create a workflow update handle. + + Users should not create this directly, but rather use + :py:meth:`Client.start_workflow_update`. + """ self._client = client self._id = id self._name = name self._workflow_id = workflow_id self._run_id = run_id self._result_type = result_type - self._known_result = None + self._known_result: Optional[temporalio.api.update.v1.Outcome] = None @property def id(self) -> str: @@ -3829,7 +3835,7 @@ async def result( self, *, timeout: Optional[timedelta] = None, - rpc_metadata: Mapping[str, str] = None, + rpc_metadata: Mapping[str, str] = {}, rpc_timeout: Optional[timedelta] = None, ) -> Any: """Wait for and return the result of the update. The result may already be known in which case no call is made. @@ -3840,6 +3846,10 @@ async def result( rpc_metadata: Headers used on the RPC call. Keys here override client-level RPC metadata keys. rpc_timeout: Optional RPC deadline to set for the RPC call. If this elapses, the poll is retried until the overall timeout has been reached. + + Raises: + TimeoutError: The specified timeout was reached when waiting for the update result. + RPCError: Update result could not be fetched for some other reason. """ outcome: temporalio.api.update.v1.Outcome if self._known_result is not None: @@ -4084,7 +4094,7 @@ class UpdateWorkflowInput: update: str args: Sequence[Any] wait_for_stage: Optional[ - temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage + temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.ValueType ] headers: Mapping[str, temporalio.api.common.v1.Payload] ret_type: Optional[Type] @@ -4724,9 +4734,7 @@ async def start_workflow_update( # If the status is INVALID_ARGUMENT, we can assume it's an update # failed error if err.status == RPCStatusCode.INVALID_ARGUMENT: - raise WorkflowUpdateFailedError( - input.workflow_id, input.update, err.cause - ) + raise WorkflowUpdateFailedError(input.workflow_id, input.update, err) else: raise @@ -4758,31 +4766,34 @@ async def poll_workflow_update(self, input: PollUpdateWorkflowInput) -> Any: lifecycle_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED ), ) - try: - # Wait for at most the *overall* timeout - async with asyncio.timeout(input.timeout.total_seconds()): - # Continue polling as long as we have either an empty response, or an *rpc* timeout - while True: - try: - res = await self._client.workflow_service.poll_workflow_execution_update( - req, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, + + async def poll_loop(): + # Continue polling as long as we have either an empty response, or an *rpc* timeout + while True: + try: + res = await self._client.workflow_service.poll_workflow_execution_update( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + if res.HasField("outcome"): + return await _update_outcome_to_result( + res.outcome, + input.update_id, + input.update, + self._client.data_converter, + input.ret_type, ) - if res.HasField("outcome"): - return await _update_outcome_to_result( - res.outcome, - input.update_id, - input.update, - self._client.data_converter, - input.ret_type, - ) - except RPCError as err: - if err.status == RPCStatusCode.DEADLINE_EXCEEDED: - continue - except TimeoutError: - pass + except RPCError as err: + if err.status == RPCStatusCode.DEADLINE_EXCEEDED: + continue + + # Wait for at most the *overall* timeout + return await asyncio.wait_for( + poll_loop(), + input.timeout.total_seconds() if input.timeout else sys.float_info.max, + ) ### Async activity calls diff --git a/temporalio/contrib/opentelemetry.py b/temporalio/contrib/opentelemetry.py index b1e1df6b..7d325e84 100644 --- a/temporalio/contrib/opentelemetry.py +++ b/temporalio/contrib/opentelemetry.py @@ -244,16 +244,27 @@ async def signal_workflow( ): return await super().signal_workflow(input) - async def update_workflow( + async def start_workflow_update( self, input: temporalio.client.UpdateWorkflowInput + ) -> temporalio.client.WorkflowUpdateHandle: + with self.root._start_as_current_span( + f"StartWorkflowUpdate:{input.update}", + attributes={"temporalWorkflowID": input.workflow_id}, + input=input, + kind=opentelemetry.trace.SpanKind.CLIENT, + ): + return await super().start_workflow_update(input) + + async def poll_workflow_update( + self, input: temporalio.client.PollUpdateWorkflowInput ) -> Any: with self.root._start_as_current_span( - f"UpdateWorkflow:{input.update}", + f"PollWorkflowUpdate:{input.update}", attributes={"temporalWorkflowID": input.workflow_id}, input=input, kind=opentelemetry.trace.SpanKind.CLIENT, ): - return await super().update_workflow(input) + return await super().poll_workflow_update(input) class _TracingActivityInboundInterceptor(temporalio.worker.ActivityInboundInterceptor): diff --git a/temporalio/worker/_interceptor.py b/temporalio/worker/_interceptor.py index bbfec877..5d2cc685 100644 --- a/temporalio/worker/_interceptor.py +++ b/temporalio/worker/_interceptor.py @@ -193,7 +193,8 @@ class HandleQueryInput: @dataclass class HandleUpdateInput: """Input for :py:meth:`WorkflowInboundInterceptor.handle_update_validator` - and :py:meth:`WorkflowInboundInterceptor.handle_update_handler`.""" + and :py:meth:`WorkflowInboundInterceptor.handle_update_handler`. + """ id: str update: str diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index 20db27f3..46279b1b 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -452,6 +452,7 @@ async def run_update( accpetance_command: temporalio.bridge.proto.workflow_commands.WorkflowCommand, ) -> None: command = accpetance_command + assert defn is not None try: if defn.validator is not None: # Run the validator @@ -459,7 +460,7 @@ async def run_update( # Accept the update command.update_response.accepted.SetInParent() - command = None + command = None # type: ignore # Run the handler success = await self._inbound.handle_update_handler(handler_input) diff --git a/temporalio/workflow.py b/temporalio/workflow.py index 5380babf..4e1d448b 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -811,7 +811,8 @@ def _update_validator( update_def: _UpdateDefinition, fn: Optional[Callable[..., None]] = None ): """Decorator for a workflow update validator method.""" - update_def.set_validator(fn) + if fn is not None: + update_def.set_validator(fn) def upsert_search_attributes(attributes: temporalio.common.SearchAttributes) -> None: @@ -1375,7 +1376,9 @@ def bind_fn(self, obj: Any) -> Callable[..., Any]: return _bind_method(obj, self.fn) def bind_validator(self, obj: Any) -> Callable[..., Any]: - return _bind_method(obj, self.validator) + if self.validator is not None: + return _bind_method(obj, self.validator) + return lambda *args, **kwargs: None def set_validator(self, validator: Callable[..., None]) -> None: # TODO: Verify arg types are the same diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 3f24d530..e9851445 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -87,6 +87,8 @@ def test_workflow_defn_good(): name="base_query", fn=GoodDefnBase.base_query, is_method=True ), }, + # TODO: Add + updates={}, sandboxed=True, ) diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 1ae41a5f..6fded7d5 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3541,7 +3541,7 @@ async def last_event_async(self, an_arg: str) -> str: @workflow.update async def runs_activity(self, name: str) -> str: - act = workflow.start_activity_method( + act = workflow.start_activity( say_hello, name, schedule_to_close_timeout=timedelta(seconds=5) ) act.cancel() @@ -3565,7 +3565,9 @@ async def runs_activity(self, name: str) -> str: async def test_workflow_update_handlers(client: Client): - async with new_worker(client, UpdateHandlersWorkflow) as worker: + async with new_worker( + client, UpdateHandlersWorkflow, activities=[say_hello] + ) as worker: handle = await client.start_workflow( UpdateHandlersWorkflow.run, id=f"update-handlers-workflow-{uuid.uuid4()}", From abe36c3e4a418c5f7d8b620c9482e7a9919d2e1f Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 17 Oct 2023 10:30:22 -0700 Subject: [PATCH 08/27] Update core / additional failure path tests --- temporalio/bridge/Cargo.lock | 77 ++++++++++++++----------- temporalio/bridge/sdk-core | 2 +- temporalio/worker/_workflow_instance.py | 32 +++++----- temporalio/workflow.py | 2 +- tests/worker/test_workflow.py | 57 ++++++++++++++++++ 5 files changed, 118 insertions(+), 52 deletions(-) diff --git a/temporalio/bridge/Cargo.lock b/temporalio/bridge/Cargo.lock index 7a73f865..79e0c7ae 100644 --- a/temporalio/bridge/Cargo.lock +++ b/temporalio/bridge/Cargo.lock @@ -90,9 +90,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", @@ -196,9 +196,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "block-buffer" @@ -457,9 +457,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] [[package]] name = "derive_builder" @@ -626,9 +629,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -1234,7 +1237,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "cfg-if", "libc", ] @@ -1524,6 +1527,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1848,14 +1857,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.0" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.1", - "regex-syntax 0.8.1", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -1869,13 +1878,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.1", + "regex-syntax 0.8.2", ] [[package]] @@ -1886,9 +1895,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d84fdd47036b038fc80dd333d10b6aab10d5d31f4a366e20014def75328d33" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" @@ -1996,11 +2005,11 @@ version = "0.1.0" [[package]] name = "rustix" -version = "0.38.18" +version = "0.38.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" +checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", @@ -2118,18 +2127,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", @@ -2534,11 +2543,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", + "powerfmt", "serde", "time-core", ] @@ -2718,11 +2728,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -2731,9 +2740,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", @@ -2742,9 +2751,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", diff --git a/temporalio/bridge/sdk-core b/temporalio/bridge/sdk-core index dd610947..45d2bc99 160000 --- a/temporalio/bridge/sdk-core +++ b/temporalio/bridge/sdk-core @@ -1 +1 @@ -Subproject commit dd610947468ec760601b57c93f76532d08739168 +Subproject commit 45d2bc997fd25bf24d347b04d519e7279851aea4 diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index 46279b1b..1417b98f 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -456,7 +456,8 @@ async def run_update( try: if defn.validator is not None: # Run the validator - await self._inbound.handle_update_validator(handler_input) + with self._as_read_only(): + await self._inbound.handle_update_validator(handler_input) # Accept the update command.update_response.accepted.SetInParent() @@ -474,7 +475,6 @@ async def run_update( job.protocol_instance_id ) command.update_response.completed.CopyFrom(result_payloads[0]) - # TODO: Dedupe exception handling if it makes sense except (Exception, asyncio.CancelledError) as err: logger.debug( f"Update raised failure with run ID {self._info.run_id}", @@ -485,21 +485,21 @@ async def run_update( err = temporalio.exceptions.CancelledError( f"Cancellation raised within update {err}" ) - if isinstance(err, temporalio.exceptions.FailureError): - # All other failure errors fail the update - if command is None: - command = self._add_command() - command.update_response.protocol_instance_id = ( - job.protocol_instance_id - ) - self._failure_converter.to_failure( - err, - self._payload_converter, - command.update_response.rejected.cause, - ) - else: - # All other exceptions fail the task + # Read-only issues during validation should fail the task + if isinstance(err, temporalio.workflow.ReadOnlyContextError): self._current_activation_error = err + return + # All other errors fail the update + if command is None: + command = self._add_command() + command.update_response.protocol_instance_id = ( + job.protocol_instance_id + ) + self._failure_converter.to_failure( + err, + self._payload_converter, + command.update_response.rejected.cause, + ) except BaseException as err: # During tear down, generator exit and no-runtime exceptions can appear if not self._deleting: diff --git a/temporalio/workflow.py b/temporalio/workflow.py index 4e1d448b..6b8acbd9 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -1068,7 +1068,7 @@ def _apply_to_class( raise ValueError("Class already contains workflow definition") issues: List[str] = [] - # Collect run fn and all signal/query fns + # Collect run fn and all signal/query/update fns members = inspect.getmembers(cls) run_fn: Optional[Callable[..., Awaitable[Any]]] = None seen_run_attr = False diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 6fded7d5..0a8c68a9 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3506,6 +3506,9 @@ async def test_workflow_buffered_metrics(client: Client): ) +bad_validator_fail_ct = 0 + + @workflow.defn class UpdateHandlersWorkflow: def __init__(self) -> None: @@ -3548,6 +3551,20 @@ async def runs_activity(self, name: str) -> str: await act return "done" + @workflow.update + async def bad_validator(self) -> str: + return "done" + + @bad_validator.validator + def bad_validator_validator(self) -> None: + global bad_validator_fail_ct + # Run a command which should not be allowed the first few tries, then "fix" it as if new code was deployed + if bad_validator_fail_ct < 2: + bad_validator_fail_ct += 1 + workflow.start_activity( + say_hello, "boo", schedule_to_close_timeout=timedelta(seconds=5) + ) + # @workflow.signal # def set_signal_handler(self, signal_name: str) -> None: # def new_handler(arg: str) -> None: @@ -3644,3 +3661,43 @@ async def test_workflow_update_handlers_unhappy(client: Client): with pytest.raises(WorkflowUpdateFailedError) as err: await handle.update(UpdateHandlersWorkflow.runs_activity, "foo") assert isinstance(err.value.cause, CancelledError) + + # Incorrect args for handler + with pytest.raises(WorkflowUpdateFailedError) as err: + await handle.update("last_event", args=[121, "badarg"]) + assert isinstance(err.value.cause, ApplicationError) + assert ( + "UpdateHandlersWorkflow.last_event_validator() takes 2 positional arguments but 3 were given" + == err.value.cause.message + ) + + # Un-deserializeable nonsense + with pytest.raises(WorkflowUpdateFailedError) as err: + await handle.update( + "last_event", + arg=RawValue( + payload=Payload( + metadata={"encoding": b"u-dont-know-me"}, data=b"enchi-cat" + ) + ), + ) + assert isinstance(err.value.cause, ApplicationError) + assert "Failed decoding arguments" == err.value.cause.message + + +async def test_workflow_update_command_in_validator(client: Client): + # Need to not sandbox so behavior of validator can change based on global + async with new_worker( + client, UpdateHandlersWorkflow, workflow_runner=UnsandboxedWorkflowRunner() + ) as worker: + handle = await client.start_workflow( + UpdateHandlersWorkflow.run, + id=f"update-handlers-command-in-validator-{uuid.uuid4()}", + task_queue=worker.task_queue, + task_timeout=timedelta(seconds=1), + ) + + # This will produce a WFT failure which will eventually resolve and then this + # update will return + res = await handle.update(UpdateHandlersWorkflow.bad_validator) + assert res == "done" From 533b9f75d42c6f2cfc2b0f0347bd55337765592a Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 17 Oct 2023 11:15:08 -0700 Subject: [PATCH 09/27] Dynamic update handlers --- temporalio/worker/_workflow_instance.py | 195 ++++++++++++------------ temporalio/workflow.py | 67 ++++++++ tests/worker/test_workflow.py | 73 ++++----- 3 files changed, 197 insertions(+), 138 deletions(-) diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index 1417b98f..f25af44d 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -422,114 +422,96 @@ def _apply_cancel_workflow( def _apply_do_update( self, job: temporalio.bridge.proto.workflow_activation.DoUpdate ): - acceptance_command = self._add_command() - acceptance_command.update_response.protocol_instance_id = ( - job.protocol_instance_id - ) - try: - defn = self._updates.get(job.name) or self._updates.get(None) - if not defn: - raise RuntimeError( - f"Update handler for '{job.name}' expected but not found, and there is no dynamic handler" + # Run the validator & handler in a task. Everything, including looking up the update definition, needs to be + # inside the task, since the update may not be defined until after we have started the workflow - for example + # if an update is in the first WFT & is also registered dynamically at the top of workflow code. + async def run_update() -> None: + command = self._add_command() + command.update_response.protocol_instance_id = job.protocol_instance_id + try: + defn = self._updates.get(job.name) or self._updates.get(None) + if not defn: + raise RuntimeError( + f"Update handler for '{job.name}' expected but not found, and there is no dynamic handler" + ) + args = self._process_handler_args( + job.name, + job.input, + defn.name, + defn.arg_types, + defn.dynamic_vararg, + ) + handler_input = HandleUpdateInput( + # TODO: update id vs proto instance id + id=job.protocol_instance_id, + update=job.name, + args=args, + headers=job.headers, ) - args = self._process_handler_args( - job.name, - job.input, - defn.name, - defn.arg_types, - defn.dynamic_vararg, - ) - handler_input = HandleUpdateInput( - # TODO: update id vs proto instance id - id=job.protocol_instance_id, - update=job.name, - args=args, - headers=job.headers, - ) - # Run the validator & handler in a task. Validator needs to be in here since the interceptor might be async. - async def run_update( - accpetance_command: temporalio.bridge.proto.workflow_commands.WorkflowCommand, - ) -> None: - command = accpetance_command - assert defn is not None - try: - if defn.validator is not None: - # Run the validator - with self._as_read_only(): - await self._inbound.handle_update_validator(handler_input) + if defn.validator is not None: + # Run the validator + with self._as_read_only(): + await self._inbound.handle_update_validator(handler_input) - # Accept the update - command.update_response.accepted.SetInParent() - command = None # type: ignore + # Accept the update + command.update_response.accepted.SetInParent() + command = None # type: ignore - # Run the handler - success = await self._inbound.handle_update_handler(handler_input) - result_payloads = self._payload_converter.to_payloads([success]) - if len(result_payloads) != 1: - raise ValueError( - f"Expected 1 result payload, got {len(result_payloads)}" - ) + # Run the handler + success = await self._inbound.handle_update_handler(handler_input) + result_payloads = self._payload_converter.to_payloads([success]) + if len(result_payloads) != 1: + raise ValueError( + f"Expected 1 result payload, got {len(result_payloads)}" + ) + command = self._add_command() + command.update_response.protocol_instance_id = job.protocol_instance_id + command.update_response.completed.CopyFrom(result_payloads[0]) + except (Exception, asyncio.CancelledError) as err: + logger.debug( + f"Update raised failure with run ID {self._info.run_id}", + exc_info=True, + ) + # All asyncio cancelled errors become Temporal cancelled errors + if isinstance(err, asyncio.CancelledError): + err = temporalio.exceptions.CancelledError( + f"Cancellation raised within update {err}" + ) + # Read-only issues during validation should fail the task + if isinstance(err, temporalio.workflow.ReadOnlyContextError): + self._current_activation_error = err + return + # All other errors fail the update + if command is None: command = self._add_command() command.update_response.protocol_instance_id = ( job.protocol_instance_id ) - command.update_response.completed.CopyFrom(result_payloads[0]) - except (Exception, asyncio.CancelledError) as err: - logger.debug( - f"Update raised failure with run ID {self._info.run_id}", - exc_info=True, - ) - # All asyncio cancelled errors become Temporal cancelled errors - if isinstance(err, asyncio.CancelledError): - err = temporalio.exceptions.CancelledError( - f"Cancellation raised within update {err}" - ) - # Read-only issues during validation should fail the task - if isinstance(err, temporalio.workflow.ReadOnlyContextError): - self._current_activation_error = err - return - # All other errors fail the update - if command is None: - command = self._add_command() - command.update_response.protocol_instance_id = ( - job.protocol_instance_id - ) - self._failure_converter.to_failure( - err, - self._payload_converter, - command.update_response.rejected.cause, - ) - except BaseException as err: - # During tear down, generator exit and no-runtime exceptions can appear - if not self._deleting: - raise - if not isinstance( - err, - ( - GeneratorExit, - temporalio.workflow._NotInWorkflowEventLoopError, - ), - ): - logger.debug( - "Ignoring exception while deleting workflow", exc_info=True - ) - - self.create_task( - run_update(acceptance_command), - name=f"update: {job.name}", - ) - - except Exception as err: - # If we failed here we had some issue deserializing or finding the update handlers, so reject it. - try: self._failure_converter.to_failure( err, self._payload_converter, - acceptance_command.update_response.rejected.cause, + command.update_response.rejected.cause, ) - except Exception as inner_err: - raise ValueError("Failed converting application error") from inner_err + except BaseException as err: + # During tear down, generator exit and no-runtime exceptions can appear + if not self._deleting: + raise + if not isinstance( + err, + ( + GeneratorExit, + temporalio.workflow._NotInWorkflowEventLoopError, + ), + ): + logger.debug( + "Ignoring exception while deleting workflow", exc_info=True + ) + + self.create_task( + run_update(), + name=f"update: {job.name}", + ) def _apply_fire_timer( self, job: temporalio.bridge.proto.workflow_activation.FireTimer @@ -1017,7 +999,26 @@ def workflow_set_signal_handler( else: self._signals.pop(name, None) - # TODO: Set update handler? + def workflow_set_update_handler( + self, + name: Optional[str], + handler: Optional[Callable], + validator: Optional[Callable], + ) -> None: + self._assert_not_read_only("set update handler") + if handler: + defn = temporalio.workflow._UpdateDefinition( + name=name, fn=handler, is_method=False + ) + if validator is not None: + defn.set_validator(validator) + self._updates[name] = defn + if defn.dynamic_vararg: + raise RuntimeError( + "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", + ) + else: + self._updates.pop(name, None) def workflow_start_activity( self, diff --git a/temporalio/workflow.py b/temporalio/workflow.py index 6b8acbd9..f3f55907 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -505,6 +505,15 @@ def workflow_set_signal_handler( ) -> None: ... + @abstractmethod + def workflow_set_update_handler( + self, + name: Optional[str], + handler: Optional[Callable], + validator: Optional[Callable], + ) -> None: + ... + @abstractmethod def workflow_start_activity( self, @@ -4093,6 +4102,64 @@ def set_dynamic_query_handler(handler: Optional[Callable]) -> None: _Runtime.current().workflow_set_query_handler(None, handler) +def get_update_handler(name: str) -> Optional[Callable]: + """Get the update handler for the given name if any. + + This includes handlers created via the ``@workflow.update`` decorator. + + Args: + name: Name of the update. + + Returns: + Callable for the update if any. If a handler is not found for the name, + this will not return the dynamic handler even if there is one. + """ + return _Runtime.current().workflow_get_update_handler(name) + + +def set_update_handler( + name: str, handler: Optional[Callable], *, validator: Optional[Callable] = None +) -> None: + """Set or unset the update handler for the given name. + + This overrides any existing handlers for the given name, including handlers + created via the ``@workflow.update`` decorator. + + Args: + name: Name of the update. + handler: Callable to set or None to unset. + validator: Callable to set or None to unset as the update validator. + """ + _Runtime.current().workflow_set_update_handler(name, handler, validator) + + +def get_dynamic_update_handler() -> Optional[Callable]: + """Get the dynamic update handler if any. + + This includes dynamic handlers created via the ``@workflow.update`` + decorator. + + Returns: + Callable for the dynamic update handler if any. + """ + return _Runtime.current().workflow_get_update_handler(None) + + +def set_dynamic_update_handler( + handler: Optional[Callable], *, validator: Optional[Callable] = None +) -> None: + """Set or unset the dynamic update handler. + + This overrides the existing dynamic handler even if it was created via the + ``@workflow.update`` decorator. + + Args: + handler: Callable to set or None to unset. + validator: Callable to set or None to unset as the update validator. + """ + _Runtime.current().workflow_set_update_handler(None, handler, validator) + + def _is_unbound_method_on_cls(fn: Callable[..., Any], cls: Type) -> bool: # Python 3 does not make this easy, ref https://stackoverflow.com/questions/3589311 return ( diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 0a8c68a9..f5d8a249 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3516,6 +3516,8 @@ def __init__(self) -> None: @workflow.run async def run(self) -> None: + workflow.set_update_handler("first_task_update", lambda: "worked") + # Wait forever await asyncio.Future() @@ -3565,23 +3567,20 @@ def bad_validator_validator(self) -> None: say_hello, "boo", schedule_to_close_timeout=timedelta(seconds=5) ) - # @workflow.signal - # def set_signal_handler(self, signal_name: str) -> None: - # def new_handler(arg: str) -> None: - # self._last_event = f"signal {signal_name}: {arg}" - # - # workflow.set_signal_handler(signal_name, new_handler) - # - # @workflow.signal - # def set_dynamic_signal_handler(self) -> None: - # def new_handler(name: str, args: Sequence[RawValue]) -> None: - # arg = workflow.payload_converter().from_payload(args[0].payload, str) - # self._last_event = f"signal dynamic {name}: {arg}" - # - # workflow.set_dynamic_signal_handler(new_handler) - - -async def test_workflow_update_handlers(client: Client): + @workflow.update + async def set_dynamic(self) -> str: + def dynahandler(name: str, _args: Sequence[RawValue]) -> str: + return "dynahandler - " + name + + def dynavalidator(name: str, _args: Sequence[RawValue]) -> None: + if name == "reject_me": + raise ApplicationError("Rejected") + + workflow.set_dynamic_update_handler(dynahandler, validator=dynavalidator) + return "set" + + +async def test_workflow_update_handlers_happy(client: Client): async with new_worker( client, UpdateHandlersWorkflow, activities=[say_hello] ) as worker: @@ -3591,6 +3590,9 @@ async def test_workflow_update_handlers(client: Client): task_queue=worker.task_queue, ) + # Dynamically registered and used in first task + assert "worked" == await handle.update("first_task_update") + # Normal handling last_event = await handle.update(UpdateHandlersWorkflow.last_event, "val2") assert "" == last_event @@ -3600,30 +3602,10 @@ async def test_workflow_update_handlers(client: Client): UpdateHandlersWorkflow.last_event_async, "val3" ) assert "val2" == last_event - # # Dynamic signal handling buffered and new - # await handle.signal("unknown_signal2", "val3") - # await handle.signal(UpdateHandlersWorkflow.set_dynamic_signal_handler) - # assert "signal dynamic unknown_signal2: val3" == await handle.query( - # UpdateHandlersWorkflow.last_event - # ) - # await handle.signal("unknown_signal3", "val4") - # assert "signal dynamic unknown_signal3: val4" == await handle.query( - # UpdateHandlersWorkflow.last_event - # ) - # - # # Normal query handling - # await handle.signal( - # UpdateHandlersWorkflow.set_query_handler, "unknown_query1" - # ) - # assert "query unknown_query1: val5" == await handle.query( - # "unknown_query1", "val5" - # ) - # - # # Dynamic query handling - # await handle.signal(UpdateHandlersWorkflow.set_dynamic_query_handler) - # assert "query dynamic unknown_query2: val6" == await handle.query( - # "unknown_query2", "val6" - # ) + + # Dynamic handler + await handle.update(UpdateHandlersWorkflow.set_dynamic) + assert "dynahandler - made_up" == await handle.update("made_up") async def test_workflow_update_handlers_unhappy(client: Client): @@ -3684,6 +3666,15 @@ async def test_workflow_update_handlers_unhappy(client: Client): assert isinstance(err.value.cause, ApplicationError) assert "Failed decoding arguments" == err.value.cause.message + # Dynamic handler + await handle.update(UpdateHandlersWorkflow.set_dynamic) + + # Rejection by dynamic handler validator + with pytest.raises(WorkflowUpdateFailedError) as err: + await handle.update("reject_me") + assert isinstance(err.value.cause, ApplicationError) + assert "Rejected" == err.value.cause.message + async def test_workflow_update_command_in_validator(client: Client): # Need to not sandbox so behavior of validator can change based on global From 5daec88d8f5f01d1f880af6c228f38408ee18741 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 17 Oct 2023 16:22:32 -0700 Subject: [PATCH 10/27] Use class for update decorator --- temporalio/client.py | 7 +- temporalio/worker/_workflow_instance.py | 6 +- temporalio/workflow.py | 118 +++++++++++------------- tests/worker/test_workflow.py | 8 ++ 4 files changed, 65 insertions(+), 74 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index ccde4f80..498c35a0 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -1702,15 +1702,14 @@ async def start_update( update_name: str ret_type = result_type if callable(update): - defn = temporalio.workflow._UpdateDefinition.from_fn(update) - if not defn: + if not isinstance(update, temporalio.workflow.update): raise RuntimeError( f"Update definition not found on {update.__qualname__}, " "is it decorated with @workflow.update?" ) - elif not defn.name: + defn = update._defn + if not defn.name: raise RuntimeError("Cannot invoke dynamic update definition") - # TODO(cretz): Check count/type of args at runtime? update_name = defn.name ret_type = defn.ret_type else: diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index f25af44d..e244e7c1 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -439,7 +439,7 @@ async def run_update() -> None: job.input, defn.name, defn.arg_types, - defn.dynamic_vararg, + False, ) handler_input = HandleUpdateInput( # TODO: update id vs proto instance id @@ -1013,10 +1013,6 @@ def workflow_set_update_handler( if validator is not None: defn.set_validator(validator) self._updates[name] = defn - if defn.dynamic_vararg: - raise RuntimeError( - "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", - ) else: self._updates.pop(name, None) diff --git a/temporalio/workflow.py b/temporalio/workflow.py index f3f55907..4485955a 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -764,12 +764,8 @@ def time_ns() -> int: return _Runtime.current().workflow_time_ns() -def update( - fn: Optional[CallableSyncOrAsyncType] = None, - *, - name: Optional[str] = None, - dynamic: Optional[bool] = False, -): +# noinspection PyPep8Naming +class update(object): """Decorator for a workflow update handler method. This is set on any async or non-async method that you wish to be called upon @@ -795,33 +791,49 @@ def update( present. """ - def with_name( - name: Optional[str], fn: CallableSyncOrAsyncType - ) -> CallableSyncOrAsyncType: - defn = _UpdateDefinition(name=name, fn=fn, is_method=True) - if defn.dynamic_vararg: - raise RuntimeError( - "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", - ) - setattr(fn, "__temporal_update_definition", defn) - setattr(fn, "validator", partial(_update_validator, defn)) - return fn - - if name is not None or dynamic: - if name is not None and dynamic: - raise RuntimeError("Cannot provide name and dynamic boolean") - return partial(with_name, name) - if fn is None: - raise RuntimeError("Cannot create update without function or name or dynamic") - return with_name(fn.__name__, fn) - - -def _update_validator( - update_def: _UpdateDefinition, fn: Optional[Callable[..., None]] = None -): - """Decorator for a workflow update validator method.""" - if fn is not None: - update_def.set_validator(fn) + def __init__( + self, + fn: Optional[CallableSyncOrAsyncType] = None, + *, + name: Optional[str] = None, + dynamic: Optional[bool] = False, + ): + """See :py:class:`update`.""" + if name is not None or dynamic: + if name is not None and dynamic: + raise RuntimeError("Cannot provide name and dynamic boolean") + self._fn = fn + self._name = name + self._dynamic = dynamic + if self._fn is not None: + # Only bother to assign the definition if we are given a function. The function is not provided when + # extra arguments are specified - in that case, the __call__ method is invoked instead. + self._assign_defn() + + def __call__(self, fn: CallableSyncOrAsyncType): + """Call the update decorator (as when passing optional arguments).""" + self._fn = fn + self._assign_defn() + return self + + def _assign_defn(self) -> None: + chosen_name = ( + self._name + if self._name is not None + else self._fn.__name__ + if self._fn + else None + ) + assert self._fn is not None + self._defn = _UpdateDefinition(name=chosen_name, fn=self._fn, is_method=True) + + def validator(self, fn: Callable[..., None]): + """Decorator for a workflow update validator method. Apply this decorator to a function to have it run before + the update handler. If it throws an error, the update will be rejected. The validator must not mutate workflow + state at all, and cannot call workflow functions which would schedule new commands (ex: starting an + activity). + """ + self._defn.set_validator(fn) def upsert_search_attributes(attributes: temporalio.common.SearchAttributes) -> None: @@ -1125,10 +1137,8 @@ def _apply_to_class( ) else: queries[query_defn.name] = query_defn - elif hasattr(member, "__temporal_update_definition"): - update_defn = cast( - _UpdateDefinition, getattr(member, "__temporal_update_definition") - ) + elif isinstance(member, update): + update_defn = member._defn if update_defn.name in updates: defn_name = update_defn.name or "" issues.append( @@ -1345,38 +1355,16 @@ class _UpdateDefinition: arg_types: Optional[List[Type]] = None ret_type: Optional[Type] = None validator: Optional[Callable[..., None]] = None - dynamic_vararg: bool = False - - @staticmethod - def from_fn(fn: Callable) -> Optional[_UpdateDefinition]: - return getattr(fn, "__temporal_update_definition", None) - - @staticmethod - def must_name_from_fn_or_str(update: Union[str, Callable]) -> str: - if callable(update): - defn = _UpdateDefinition.from_fn(update) - if not defn: - raise RuntimeError( - f"Update definition not found on {update.__qualname__}, " - "is it decorated with @workflow.update?" - ) - elif not defn.name: - raise RuntimeError("Cannot invoke dynamic update definition") - # TODO(cretz): Check count/type of args at runtime? - return defn.name - return str(update) def __post_init__(self) -> None: if self.arg_types is None: arg_types, ret_type = temporalio.common._type_hints_from_func(self.fn) - # If dynamic, assert it - if not self.name: - object.__setattr__( - self, - "dynamic_vararg", - not _assert_dynamic_handler_args( - self.fn, arg_types, self.is_method - ), + # Disallow dynamic varargs + if not self.name and not _assert_dynamic_handler_args( + self.fn, arg_types, self.is_method + ): + raise RuntimeError( + "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", ) object.__setattr__(self, "arg_types", arg_types) object.__setattr__(self, "ret_type", ret_type) diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index f5d8a249..12770940 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3579,6 +3579,10 @@ def dynavalidator(name: str, _args: Sequence[RawValue]) -> None: workflow.set_dynamic_update_handler(dynahandler, validator=dynavalidator) return "set" + @workflow.update(name="name_override") + async def not_the_name(self) -> str: + return "name_overridden" + async def test_workflow_update_handlers_happy(client: Client): async with new_worker( @@ -3607,6 +3611,10 @@ async def test_workflow_update_handlers_happy(client: Client): await handle.update(UpdateHandlersWorkflow.set_dynamic) assert "dynahandler - made_up" == await handle.update("made_up") + assert "name_overridden" == await handle.update( + UpdateHandlersWorkflow.not_the_name + ) + async def test_workflow_update_handlers_unhappy(client: Client): async with new_worker(client, UpdateHandlersWorkflow) as worker: From 668532d806a961c70336b2b02ea4fa61aa700d45 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 17 Oct 2023 16:56:47 -0700 Subject: [PATCH 11/27] Add update definitions to workflow definition test --- temporalio/workflow.py | 19 +++++++------------ tests/test_workflow.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/temporalio/workflow.py b/temporalio/workflow.py index 4485955a..b3ec30a3 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -803,7 +803,9 @@ def __init__( if name is not None and dynamic: raise RuntimeError("Cannot provide name and dynamic boolean") self._fn = fn - self._name = name + self._name = ( + name if name is not None else self._fn.__name__ if self._fn else None + ) self._dynamic = dynamic if self._fn is not None: # Only bother to assign the definition if we are given a function. The function is not provided when @@ -817,15 +819,8 @@ def __call__(self, fn: CallableSyncOrAsyncType): return self def _assign_defn(self) -> None: - chosen_name = ( - self._name - if self._name is not None - else self._fn.__name__ - if self._fn - else None - ) assert self._fn is not None - self._defn = _UpdateDefinition(name=chosen_name, fn=self._fn, is_method=True) + self._defn = _UpdateDefinition(name=self._name, fn=self._fn, is_method=True) def validator(self, fn: Callable[..., None]): """Decorator for a workflow update validator method. Apply this decorator to a function to have it run before @@ -1240,9 +1235,9 @@ async def with_object(*args, **kwargs) -> Any: def _assert_dynamic_handler_args( fn: Callable, arg_types: Optional[List[Type]], is_method: bool ) -> bool: - # Dynamic query/signal must have three args: self, name, and - # Sequence[RawValue]. An older form accepted varargs for the third param so - # we will too (but will warn in the signal/query code) + # Dynamic query/signal/update must have three args: self, name, and + # Sequence[RawValue]. An older form accepted varargs for the third param for signals/queries so + # we will too (but will warn in the signal/query code). params = list(inspect.signature(fn).parameters.values()) total_expected_params = 3 if is_method else 2 if ( diff --git a/tests/test_workflow.py b/tests/test_workflow.py index e9851445..0b7ae6d2 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -19,6 +19,10 @@ def base_signal(self): def base_query(self): pass + @workflow.update + def base_update(self): + pass + @workflow.defn(name="workflow-custom") class GoodDefn(GoodDefnBase): @@ -50,6 +54,18 @@ def query2(self): def query3(self, name: str, args: Sequence[RawValue]): pass + @workflow.update + def update1(self): + pass + + @workflow.update(name="update-custom") + def update2(self): + pass + + @workflow.update(dynamic=True) + def update3(self, name: str, args: Sequence[RawValue]): + pass + def test_workflow_defn_good(): # Although the API is internal, we want to check the literal definition just @@ -87,8 +103,21 @@ def test_workflow_defn_good(): name="base_query", fn=GoodDefnBase.base_query, is_method=True ), }, - # TODO: Add - updates={}, + # Since updates use class-based decorators we need to pass the inner _fn for the fn param + updates={ + "update1": workflow._UpdateDefinition( + name="update1", fn=GoodDefn.update1._fn, is_method=True + ), + "update-custom": workflow._UpdateDefinition( + name="update-custom", fn=GoodDefn.update2._fn, is_method=True + ), + None: workflow._UpdateDefinition( + name=None, fn=GoodDefn.update3._fn, is_method=True + ), + "base_update": workflow._UpdateDefinition( + name="base_update", fn=GoodDefnBase.base_update._fn, is_method=True + ), + }, sandboxed=True, ) From 3a9dc2c4c4483a4c4adb57b4db0f5ed96abc2cda Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 19 Oct 2023 16:16:11 -0700 Subject: [PATCH 12/27] Finally! Type system defeated! --- temporalio/client.py | 65 ++++++++++- temporalio/worker/_workflow_instance.py | 6 +- temporalio/workflow.py | 137 ++++++++++++++++-------- tests/test_workflow.py | 9 +- tests/worker/test_workflow.py | 13 ++- 5 files changed, 171 insertions(+), 59 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index 498c35a0..d2d2a2cb 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -1615,6 +1615,62 @@ async def terminate( ) ) + # Overload for no-param update + @overload + async def update( + self, + update: temporalio.workflow.UpdateMethodMultiArg[[SelfType], LocalReturnType], + *, + id: Optional[str] = None, + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + ) -> LocalReturnType: + ... + + # Overload for single-param update + @overload + async def update( + self, + update: temporalio.workflow.UpdateMethodMultiArg[ + [SelfType, ParamType], LocalReturnType + ], + arg: ParamType, + *, + id: Optional[str] = None, + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + ) -> LocalReturnType: + ... + + @overload + async def update( + self, + update: temporalio.workflow.UpdateMethodMultiArg[ + MultiParamSpec, LocalReturnType + ], + *, + args: MultiParamSpec.args, + id: Optional[str] = None, + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + ) -> LocalReturnType: + ... + + # Overload for string-name update + @overload + async def update( + self, + update: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: Optional[str] = None, + result_type: Optional[Type] = None, + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + ) -> Any: + ... + async def update( self, update: Union[str, Callable], @@ -1701,15 +1757,16 @@ async def start_update( """ update_name: str ret_type = result_type - if callable(update): - if not isinstance(update, temporalio.workflow.update): + if isinstance(update, temporalio.workflow.UpdateMethodMultiArg): + defn = update._defn + if not defn: raise RuntimeError( f"Update definition not found on {update.__qualname__}, " "is it decorated with @workflow.update?" ) - defn = update._defn - if not defn.name: + elif not defn.name: raise RuntimeError("Cannot invoke dynamic update definition") + # TODO(cretz): Check count/type of args at runtime? update_name = defn.name ret_type = defn.ret_type else: diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index e244e7c1..f25af44d 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -439,7 +439,7 @@ async def run_update() -> None: job.input, defn.name, defn.arg_types, - False, + defn.dynamic_vararg, ) handler_input = HandleUpdateInput( # TODO: update id vs proto instance id @@ -1013,6 +1013,10 @@ def workflow_set_update_handler( if validator is not None: defn.set_validator(validator) self._updates[name] = defn + if defn.dynamic_vararg: + raise RuntimeError( + "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", + ) else: self._updates.pop(name, None) diff --git a/temporalio/workflow.py b/temporalio/workflow.py index b3ec30a3..c9ad2a23 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -36,7 +36,13 @@ overload, ) -from typing_extensions import Concatenate, Literal, TypedDict +from typing_extensions import ( + Concatenate, + Literal, + Protocol, + TypedDict, + runtime_checkable, +) import temporalio.api.common.v1 import temporalio.bridge.proto.child_workflow @@ -64,6 +70,7 @@ MethodSyncSingleParam, MultiParamSpec, ParamType, + ProtocolReturnType, ReturnType, SelfType, ) @@ -764,8 +771,64 @@ def time_ns() -> int: return _Runtime.current().workflow_time_ns() -# noinspection PyPep8Naming -class update(object): +# Needs to be defined here to avoid a circular import +@runtime_checkable +class UpdateMethodMultiArg(Protocol[MultiParamSpec, ProtocolReturnType]): + """Decorated workflow update functions implement this.""" + + _defn: temporalio.workflow._UpdateDefinition + + def __call__( + self, *args: MultiParamSpec.args, **kwargs: MultiParamSpec.kwargs + ) -> Union[ProtocolReturnType, Awaitable[ProtocolReturnType]]: + """Generic callable type callback.""" + ... + + def validator(self, vfunc: Callable[MultiParamSpec, None]) -> None: + """Use to decorate a function to validate the arguments passed to the update handler.""" + ... + + +@overload +def update( + fn: Callable[MultiParamSpec, Awaitable[ReturnType]] +) -> UpdateMethodMultiArg[MultiParamSpec, ReturnType]: + ... + + +@overload +def update( + fn: Callable[MultiParamSpec, ReturnType] +) -> UpdateMethodMultiArg[MultiParamSpec, ReturnType]: + ... + + +@overload +def update( + *, name: str +) -> Callable[ + [Callable[MultiParamSpec, ReturnType]], + UpdateMethodMultiArg[MultiParamSpec, ReturnType], +]: + ... + + +@overload +def update( + *, dynamic: Literal[True] +) -> Callable[ + [Callable[MultiParamSpec, ReturnType]], + UpdateMethodMultiArg[MultiParamSpec, ReturnType], +]: + ... + + +def update( + fn: Optional[CallableSyncOrAsyncType] = None, + *, + name: Optional[str] = None, + dynamic: Optional[bool] = False, +): """Decorator for a workflow update handler method. This is set on any async or non-async method that you wish to be called upon @@ -791,44 +854,33 @@ class update(object): present. """ - def __init__( - self, - fn: Optional[CallableSyncOrAsyncType] = None, - *, - name: Optional[str] = None, - dynamic: Optional[bool] = False, - ): - """See :py:class:`update`.""" - if name is not None or dynamic: - if name is not None and dynamic: - raise RuntimeError("Cannot provide name and dynamic boolean") - self._fn = fn - self._name = ( - name if name is not None else self._fn.__name__ if self._fn else None - ) - self._dynamic = dynamic - if self._fn is not None: - # Only bother to assign the definition if we are given a function. The function is not provided when - # extra arguments are specified - in that case, the __call__ method is invoked instead. - self._assign_defn() - - def __call__(self, fn: CallableSyncOrAsyncType): - """Call the update decorator (as when passing optional arguments).""" - self._fn = fn - self._assign_defn() - return self - - def _assign_defn(self) -> None: - assert self._fn is not None - self._defn = _UpdateDefinition(name=self._name, fn=self._fn, is_method=True) - - def validator(self, fn: Callable[..., None]): - """Decorator for a workflow update validator method. Apply this decorator to a function to have it run before - the update handler. If it throws an error, the update will be rejected. The validator must not mutate workflow - state at all, and cannot call workflow functions which would schedule new commands (ex: starting an - activity). - """ - self._defn.set_validator(fn) + def with_name( + name: Optional[str], fn: CallableSyncOrAsyncType + ) -> CallableSyncOrAsyncType: + defn = _UpdateDefinition(name=name, fn=fn, is_method=True) + if defn.dynamic_vararg: + raise RuntimeError( + "Dynamic updates do not support a vararg third param, use Sequence[RawValue]", + ) + setattr(fn, "_defn", defn) + setattr(fn, "validator", partial(_update_validator, defn)) + return fn + + if name is not None or dynamic: + if name is not None and dynamic: + raise RuntimeError("Cannot provide name and dynamic boolean") + return partial(with_name, name) + if fn is None: + raise RuntimeError("Cannot create update without function or name or dynamic") + return with_name(fn.__name__, fn) + + +def _update_validator( + update_def: _UpdateDefinition, fn: Optional[Callable[..., None]] = None +): + """Decorator for a workflow update validator method.""" + if fn is not None: + update_def.set_validator(fn) def upsert_search_attributes(attributes: temporalio.common.SearchAttributes) -> None: @@ -1132,7 +1184,7 @@ def _apply_to_class( ) else: queries[query_defn.name] = query_defn - elif isinstance(member, update): + elif isinstance(member, UpdateMethodMultiArg): update_defn = member._defn if update_defn.name in updates: defn_name = update_defn.name or "" @@ -1350,6 +1402,7 @@ class _UpdateDefinition: arg_types: Optional[List[Type]] = None ret_type: Optional[Type] = None validator: Optional[Callable[..., None]] = None + dynamic_vararg: bool = False def __post_init__(self) -> None: if self.arg_types is None: diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 0b7ae6d2..b1f1378a 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -103,19 +103,18 @@ def test_workflow_defn_good(): name="base_query", fn=GoodDefnBase.base_query, is_method=True ), }, - # Since updates use class-based decorators we need to pass the inner _fn for the fn param updates={ "update1": workflow._UpdateDefinition( - name="update1", fn=GoodDefn.update1._fn, is_method=True + name="update1", fn=GoodDefn.update1, is_method=True ), "update-custom": workflow._UpdateDefinition( - name="update-custom", fn=GoodDefn.update2._fn, is_method=True + name="update-custom", fn=GoodDefn.update2, is_method=True ), None: workflow._UpdateDefinition( - name=None, fn=GoodDefn.update3._fn, is_method=True + name=None, fn=GoodDefn.update3, is_method=True ), "base_update": workflow._UpdateDefinition( - name="base_update", fn=GoodDefnBase.base_update._fn, is_method=True + name="base_update", fn=GoodDefnBase.base_update, is_method=True ), }, sandboxed=True, diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 12770940..51846994 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3553,6 +3553,10 @@ async def runs_activity(self, name: str) -> str: await act return "done" + @workflow.update(name="renamed") + async def async_named(self) -> str: + return "named" + @workflow.update async def bad_validator(self) -> str: return "done" @@ -3579,10 +3583,6 @@ def dynavalidator(name: str, _args: Sequence[RawValue]) -> None: workflow.set_dynamic_update_handler(dynahandler, validator=dynavalidator) return "set" - @workflow.update(name="name_override") - async def not_the_name(self) -> str: - return "name_overridden" - async def test_workflow_update_handlers_happy(client: Client): async with new_worker( @@ -3611,9 +3611,8 @@ async def test_workflow_update_handlers_happy(client: Client): await handle.update(UpdateHandlersWorkflow.set_dynamic) assert "dynahandler - made_up" == await handle.update("made_up") - assert "name_overridden" == await handle.update( - UpdateHandlersWorkflow.not_the_name - ) + # Name overload + assert "named" == await handle.update(UpdateHandlersWorkflow.async_named) async def test_workflow_update_handlers_unhappy(client: Client): From 6a7c52ae5e4b8cbf91d7654d30c09db71531c681 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 19 Oct 2023 16:47:34 -0700 Subject: [PATCH 13/27] Rename update to execute_update and hide wait_for_stage param --- temporalio/client.py | 43 ++++++++++++++++++++++++++++------- temporalio/workflow.py | 3 +++ tests/worker/test_workflow.py | 36 ++++++++++++++++------------- 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index d2d2a2cb..c4fc85fc 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -1617,7 +1617,7 @@ async def terminate( # Overload for no-param update @overload - async def update( + async def execute_update( self, update: temporalio.workflow.UpdateMethodMultiArg[[SelfType], LocalReturnType], *, @@ -1629,7 +1629,7 @@ async def update( # Overload for single-param update @overload - async def update( + async def execute_update( self, update: temporalio.workflow.UpdateMethodMultiArg[ [SelfType, ParamType], LocalReturnType @@ -1643,7 +1643,7 @@ async def update( ... @overload - async def update( + async def execute_update( self, update: temporalio.workflow.UpdateMethodMultiArg[ MultiParamSpec, LocalReturnType @@ -1658,7 +1658,7 @@ async def update( # Overload for string-name update @overload - async def update( + async def execute_update( self, update: str, arg: Any = temporalio.common._arg_unset, @@ -1671,7 +1671,7 @@ async def update( ) -> Any: ... - async def update( + async def execute_update( self, update: Union[str, Callable], arg: Any = temporalio.common._arg_unset, @@ -1687,6 +1687,9 @@ async def update( This will target the workflow with :py:attr:`run_id` if present. To use a different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. + .. warning:: + This API is experimental + .. warning:: WorkflowHandles created as a result of :py:meth:`Client.start_workflow` will send updates to the latest workflow with the same workflow ID even if it is @@ -1706,7 +1709,7 @@ async def update( Raises: RPCError: There was some issue sending the update to the workflow. """ - handle = await self.start_update( + handle = await self._start_update( update, arg, args=args, @@ -1725,7 +1728,6 @@ async def start_update( *, args: Sequence[Any] = [], id: Optional[str] = None, - wait_for_stage: temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.ValueType = temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, result_type: Optional[Type] = None, rpc_metadata: Mapping[str, str] = {}, rpc_timeout: Optional[timedelta] = None, @@ -1735,6 +1737,9 @@ async def start_update( This will target the workflow with :py:attr:`run_id` if present. To use a different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. + .. warning:: + This API is experimental + .. warning:: WorkflowHandles created as a result of :py:meth:`Client.start_workflow` will send updates to the latest workflow with the same workflow ID even if it is @@ -1745,7 +1750,6 @@ async def start_update( arg: Single argument to the update. args: Multiple arguments to the update. Cannot be set if arg is. id: ID of the update. If not set, the server will set a UUID as the ID. - wait_for_stage: Specifies at what point in the update request life cycle this request should return. result_type: For string updates, this can set the specific result type hint to deserialize into. rpc_metadata: Headers used on the RPC call. Keys here override @@ -1755,6 +1759,29 @@ async def start_update( Raises: RPCError: There was some issue sending the update to the workflow. """ + return await self._start_update( + update, + arg, + args=args, + id=id, + wait_for_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, + result_type=result_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + async def _start_update( + self, + update: Union[str, Callable], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: Optional[str] = None, + wait_for_stage: temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.ValueType = temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, + result_type: Optional[Type] = None, + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + ) -> WorkflowUpdateHandle: update_name: str ret_type = result_type if isinstance(update, temporalio.workflow.UpdateMethodMultiArg): diff --git a/temporalio/workflow.py b/temporalio/workflow.py index c9ad2a23..0fafd142 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -844,6 +844,9 @@ def update( argument that can accept more fields later if needed. The handler may return a serializable value which will be sent back to the caller of the update. + .. warning:: + This API is experimental + Args: fn: The function to decorate. name: Update name. Defaults to method ``__name__``. Cannot be present diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 51846994..93bd0c46 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3595,24 +3595,28 @@ async def test_workflow_update_handlers_happy(client: Client): ) # Dynamically registered and used in first task - assert "worked" == await handle.update("first_task_update") + assert "worked" == await handle.execute_update("first_task_update") # Normal handling - last_event = await handle.update(UpdateHandlersWorkflow.last_event, "val2") + last_event = await handle.execute_update( + UpdateHandlersWorkflow.last_event, "val2" + ) assert "" == last_event # Async handler - last_event = await handle.update( + last_event = await handle.execute_update( UpdateHandlersWorkflow.last_event_async, "val3" ) assert "val2" == last_event # Dynamic handler - await handle.update(UpdateHandlersWorkflow.set_dynamic) - assert "dynahandler - made_up" == await handle.update("made_up") + await handle.execute_update(UpdateHandlersWorkflow.set_dynamic) + assert "dynahandler - made_up" == await handle.execute_update("made_up") # Name overload - assert "named" == await handle.update(UpdateHandlersWorkflow.async_named) + assert "named" == await handle.execute_update( + UpdateHandlersWorkflow.async_named + ) async def test_workflow_update_handlers_unhappy(client: Client): @@ -3625,35 +3629,35 @@ async def test_workflow_update_handlers_unhappy(client: Client): # Undefined handler with pytest.raises(WorkflowUpdateFailedError) as err: - await handle.update("whargarbl", "whatever") + await handle.execute_update("whargarbl", "whatever") assert isinstance(err.value.cause, ApplicationError) assert "'whargarbl' expected but not found" in err.value.cause.message # Rejection by validator with pytest.raises(WorkflowUpdateFailedError) as err: - await handle.update(UpdateHandlersWorkflow.last_event, "reject_me") + await handle.execute_update(UpdateHandlersWorkflow.last_event, "reject_me") assert isinstance(err.value.cause, ApplicationError) assert "Rejected" == err.value.cause.message # Failure during update handler with pytest.raises(WorkflowUpdateFailedError) as err: - await handle.update(UpdateHandlersWorkflow.last_event, "fail") + await handle.execute_update(UpdateHandlersWorkflow.last_event, "fail") assert isinstance(err.value.cause, ApplicationError) assert "SyncFail" == err.value.cause.message with pytest.raises(WorkflowUpdateFailedError) as err: - await handle.update(UpdateHandlersWorkflow.last_event_async, "fail") + await handle.execute_update(UpdateHandlersWorkflow.last_event_async, "fail") assert isinstance(err.value.cause, ApplicationError) assert "AsyncFail" == err.value.cause.message # Cancel inside handler with pytest.raises(WorkflowUpdateFailedError) as err: - await handle.update(UpdateHandlersWorkflow.runs_activity, "foo") + await handle.execute_update(UpdateHandlersWorkflow.runs_activity, "foo") assert isinstance(err.value.cause, CancelledError) # Incorrect args for handler with pytest.raises(WorkflowUpdateFailedError) as err: - await handle.update("last_event", args=[121, "badarg"]) + await handle.execute_update("last_event", args=[121, "badarg"]) assert isinstance(err.value.cause, ApplicationError) assert ( "UpdateHandlersWorkflow.last_event_validator() takes 2 positional arguments but 3 were given" @@ -3662,7 +3666,7 @@ async def test_workflow_update_handlers_unhappy(client: Client): # Un-deserializeable nonsense with pytest.raises(WorkflowUpdateFailedError) as err: - await handle.update( + await handle.execute_update( "last_event", arg=RawValue( payload=Payload( @@ -3674,11 +3678,11 @@ async def test_workflow_update_handlers_unhappy(client: Client): assert "Failed decoding arguments" == err.value.cause.message # Dynamic handler - await handle.update(UpdateHandlersWorkflow.set_dynamic) + await handle.execute_update(UpdateHandlersWorkflow.set_dynamic) # Rejection by dynamic handler validator with pytest.raises(WorkflowUpdateFailedError) as err: - await handle.update("reject_me") + await handle.execute_update("reject_me") assert isinstance(err.value.cause, ApplicationError) assert "Rejected" == err.value.cause.message @@ -3697,5 +3701,5 @@ async def test_workflow_update_command_in_validator(client: Client): # This will produce a WFT failure which will eventually resolve and then this # update will return - res = await handle.update(UpdateHandlersWorkflow.bad_validator) + res = await handle.execute_update(UpdateHandlersWorkflow.bad_validator) assert res == "done" From 4be97ccd3adad023a087eeac6ba1828ecf7e13f6 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 20 Oct 2023 11:11:32 -0700 Subject: [PATCH 14/27] Always run validator interceptor --- temporalio/worker/_workflow_instance.py | 22 +++++++++--------- tests/worker/test_interceptor.py | 31 ++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index f25af44d..33c80e04 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -449,10 +449,9 @@ async def run_update() -> None: headers=job.headers, ) - if defn.validator is not None: - # Run the validator - with self._as_read_only(): - await self._inbound.handle_update_validator(handler_input) + # Always run the validator interceptor, which will only actually run a validator if one is defined. + with self._as_read_only(): + await self._inbound.handle_update_validator(handler_input) # Accept the update command.update_response.accepted.SetInParent() @@ -874,7 +873,7 @@ def workflow_get_update_handler(self, name: Optional[str]) -> Optional[Callable] return defn.bind_fn(self._object) if defn.is_method else defn.fn def workflow_get_update_validator(self, name: Optional[str]) -> Optional[Callable]: - defn = self._updates.get(name) + defn = self._updates.get(name) or self._updates.get(None) if not defn or not defn.validator: return None # Bind if a method @@ -1789,12 +1788,13 @@ async def handle_query(self, input: HandleQueryInput) -> Any: return handler(*input.args) async def handle_update_validator(self, input: HandleUpdateInput) -> None: - handler = self._instance.workflow_get_update_validator( - input.update - ) or self._instance.workflow_get_update_validator(None) - # Handler should always be present at this point - assert handler - handler(*input.args) + # Do not "or None" the validator, since we only want to use the validator for + # the specific named update - we shouldn't fall back to the dynamic validator + # for some defined, named update which doesn't have a defined validator. + handler = self._instance.workflow_get_update_validator(input.update) + # Validator may not be defined + if handler is not None: + handler(*input.args) async def handle_update_handler(self, input: HandleUpdateInput) -> Any: handler = self._instance.workflow_get_update_handler( diff --git a/tests/worker/test_interceptor.py b/tests/worker/test_interceptor.py index e9f8f55d..db172e8a 100644 --- a/tests/worker/test_interceptor.py +++ b/tests/worker/test_interceptor.py @@ -6,7 +6,8 @@ import pytest from temporalio import activity, workflow -from temporalio.client import Client +from temporalio.client import Client, WorkflowUpdateFailedError +from temporalio.exceptions import ApplicationError from temporalio.testing import WorkflowEnvironment from temporalio.worker import ( ActivityInboundInterceptor, @@ -176,6 +177,19 @@ def query(self, param: str) -> str: def signal(self, param: str) -> None: self.finish.set() + @workflow.update + def update(self, param: str) -> str: + return f"update: {param}" + + @workflow.update + def update_validated(self, param: str) -> str: + return f"update: {param}" + + @update_validated.validator + def update_validated_validator(self, param: str) -> None: + if param == "reject-me": + raise ApplicationError("Invalid update") + async def test_worker_interceptor(client: Client, env: WorkflowEnvironment): # TODO(cretz): Fix @@ -202,6 +216,13 @@ async def test_worker_interceptor(client: Client, env: WorkflowEnvironment): InterceptedWorkflow.query, "query-val" ) await handle.signal(InterceptedWorkflow.signal, "signal-val") + assert "update: update-val" == await handle.execute_update( + InterceptedWorkflow.update, "update-val" + ) + with pytest.raises(WorkflowUpdateFailedError) as _err: + await handle.execute_update( + InterceptedWorkflow.update_validated, "reject-me" + ) await handle.result() # Check traces @@ -255,5 +276,13 @@ def pop_trace(name: str, filter: Optional[Callable[[Any], bool]] = None) -> Any: assert pop_trace( "workflow.signal", lambda v: v.args[0] == "external-signal-val" ) + assert pop_trace( + "workflow.update.validator", lambda v: v.args[0] == "update-val" + ) + assert pop_trace("workflow.update.handler", lambda v: v.args[0] == "update-val") + assert pop_trace( + "workflow.update.validator", lambda v: v.args[0] == "reject-me" + ) + # Confirm no unexpected traces assert not interceptor_traces From 953b8e446d5e6b69a6956f4bcbd60ec45a26293d Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 20 Oct 2023 11:26:39 -0700 Subject: [PATCH 15/27] Clean up some last TODOs / workflow definition error tests --- temporalio/worker/_workflow_instance.py | 7 ++++--- temporalio/workflow.py | 7 ++++++- tests/test_workflow.py | 26 ++++++++++++++++++++++++- tests/worker/test_workflow.py | 4 ++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index 33c80e04..1052ef5c 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -431,8 +431,10 @@ async def run_update() -> None: try: defn = self._updates.get(job.name) or self._updates.get(None) if not defn: + known_updates = sorted([k for k in self._updates.keys() if k]) raise RuntimeError( - f"Update handler for '{job.name}' expected but not found, and there is no dynamic handler" + f"Update handler for '{job.name}' expected but not found, and there is no dynamic handler. " + f"known updates: [{' '.join(known_updates)}]" ) args = self._process_handler_args( job.name, @@ -442,8 +444,7 @@ async def run_update() -> None: defn.dynamic_vararg, ) handler_input = HandleUpdateInput( - # TODO: update id vs proto instance id - id=job.protocol_instance_id, + id=job.id, update=job.name, args=args, headers=job.headers, diff --git a/temporalio/workflow.py b/temporalio/workflow.py index 0fafd142..56abd1ef 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -1230,6 +1230,12 @@ def _apply_to_class( issues.append( f"@workflow.query defined on {base_member.__qualname__} but not on the override" ) + elif isinstance(base_member, UpdateMethodMultiArg): + update_defn = base_member._defn + if update_defn.name not in updates: + issues.append( + f"@workflow.update defined on {base_member.__qualname__} but not on the override" + ) if not seen_run_attr: issues.append("Missing @workflow.run method") @@ -1429,7 +1435,6 @@ def bind_validator(self, obj: Any) -> Callable[..., Any]: return lambda *args, **kwargs: None def set_validator(self, validator: Callable[..., None]) -> None: - # TODO: Verify arg types are the same if self.validator: raise RuntimeError(f"Validator already set for update {self.name}") object.__setattr__(self, "validator", validator) diff --git a/tests/test_workflow.py b/tests/test_workflow.py index b1f1378a..f37a7374 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -130,6 +130,10 @@ def base_signal(self): def base_query(self): pass + @workflow.update + def base_update(self): + pass + class BadDefn(BadDefnBase): # Intentionally missing @workflow.run @@ -174,12 +178,24 @@ def query4(self, name: str, args: Sequence[RawValue]): def base_query(self): pass + @workflow.update + def update1(self, arg1: str): + pass + + @workflow.update(name="update1") + def update2(self, arg1: str): + pass + + # Intentionally missing decorator + def base_update(self): + pass + def test_workflow_defn_bad(): with pytest.raises(ValueError) as err: workflow.defn(BadDefn) - assert "Invalid workflow class for 7 reasons" in str(err.value) + assert "Invalid workflow class for 9 reasons" in str(err.value) assert "Missing @workflow.run method" in str(err.value) assert ( "Multiple signal methods found for signal1 (at least on signal2 and signal1)" @@ -205,6 +221,14 @@ def test_workflow_defn_bad(): "@workflow.query defined on BadDefnBase.base_query but not on the override" in str(err.value) ) + assert ( + "Multiple update methods found for update1 (at least on update2 and update1)" + in str(err.value) + ) + assert ( + "@workflow.update defined on BadDefnBase.base_update but not on the override" + in str(err.value) + ) def test_workflow_defn_local_class(): diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 93bd0c46..2b9bd2e0 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3632,6 +3632,10 @@ async def test_workflow_update_handlers_unhappy(client: Client): await handle.execute_update("whargarbl", "whatever") assert isinstance(err.value.cause, ApplicationError) assert "'whargarbl' expected but not found" in err.value.cause.message + assert ( + "known updates: [bad_validator first_task_update last_event last_event_async renamed runs_activity set_dynamic]" + in err.value.cause.message + ) # Rejection by validator with pytest.raises(WorkflowUpdateFailedError) as err: From 2e54df60baff3d1c54d279045fdeae50605a6efd Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 20 Oct 2023 14:49:23 -0700 Subject: [PATCH 16/27] 3.7 throws this error differently --- tests/worker/test_workflow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 2b9bd2e0..bfa953e7 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3664,8 +3664,8 @@ async def test_workflow_update_handlers_unhappy(client: Client): await handle.execute_update("last_event", args=[121, "badarg"]) assert isinstance(err.value.cause, ApplicationError) assert ( - "UpdateHandlersWorkflow.last_event_validator() takes 2 positional arguments but 3 were given" - == err.value.cause.message + "last_event_validator() takes 2 positional arguments but 3 were given" + in err.value.cause.message ) # Un-deserializeable nonsense From 188bf9769e64cf578e2aa8e62a5d9ab72bd6b3af Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 20 Oct 2023 15:08:37 -0700 Subject: [PATCH 17/27] Update README --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 66d6f661..b8ca3b99 100644 --- a/README.md +++ b/README.md @@ -458,8 +458,8 @@ Some things to note about the above code: #### Definition Workflows are defined as classes decorated with `@workflow.defn`. The method invoked for the workflow is decorated with -`@workflow.run`. Methods for signals and queries are decorated with `@workflow.signal` and `@workflow.query` -respectively. Here's an example of a workflow: +`@workflow.run`. Methods for signals, queries, and updates are decorated with `@workflow.signal`, `@workflow.query` +and `@workflow.update` respectively. Here's an example of a workflow: ```python import asyncio @@ -515,6 +515,12 @@ class GreetingWorkflow: @workflow.query def current_greeting(self) -> str: return self._current_greeting + + @workflow.update + def set_and_get_greeting(self, greeting: str) -> str: + old = self._current_greeting + self._current_greeting = greeting + return old ``` @@ -582,6 +588,14 @@ Here are the decorators that can be applied: * All the same constraints as `@workflow.signal` but should return a value * Should not be `async` * Temporal queries should never mutate anything in the workflow or call any calls that would mutate the workflow +* `@workflow.update` - Defines a method as an update + * May both accept as input and return a value + * May be `async` or non-`async` + * May mutate workflow state, and make calls to other workflow APIs like starting activities, etc. + * Also accepts the `name` and `dynamic` parameters like signals and queries, with the same semantics. + * Update handlers may optionally define a validator method by decorating it with `@update_handler_method.validator`. + Validators cannot be `async`, cannot mutate workflow state, and return nothing. They can be used to reject update + calls before any events are written to history by throwing an exception. #### Running From 03e31977249c924c7ae346b604e74392ccd289a6 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 20 Oct 2023 17:01:51 -0700 Subject: [PATCH 18/27] Skip updates in Java test server for now --- tests/worker/test_workflow.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index bfa953e7..5c2385fe 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3584,7 +3584,11 @@ def dynavalidator(name: str, _args: Sequence[RawValue]) -> None: return "set" -async def test_workflow_update_handlers_happy(client: Client): +async def test_workflow_update_handlers_happy(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/1903" + ) async with new_worker( client, UpdateHandlersWorkflow, activities=[say_hello] ) as worker: @@ -3619,7 +3623,13 @@ async def test_workflow_update_handlers_happy(client: Client): ) -async def test_workflow_update_handlers_unhappy(client: Client): +async def test_workflow_update_handlers_unhappy( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/1903" + ) async with new_worker(client, UpdateHandlersWorkflow) as worker: handle = await client.start_workflow( UpdateHandlersWorkflow.run, @@ -3691,7 +3701,13 @@ async def test_workflow_update_handlers_unhappy(client: Client): assert "Rejected" == err.value.cause.message -async def test_workflow_update_command_in_validator(client: Client): +async def test_workflow_update_command_in_validator( + client: Client, env: WorkflowEnvironment +): + if env.supports_time_skipping: + pytest.skip( + "Java test server: https://github.com/temporalio/sdk-java/issues/1903" + ) # Need to not sandbox so behavior of validator can change based on global async with new_worker( client, UpdateHandlersWorkflow, workflow_runner=UnsandboxedWorkflowRunner() From cc0871d9ec34b64b14bac2f4db00ccdd9f62adf8 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 23 Oct 2023 11:13:13 -0700 Subject: [PATCH 19/27] Make validator interceptor sync --- temporalio/worker/_interceptor.py | 4 ++-- temporalio/worker/_workflow_instance.py | 4 ++-- tests/worker/test_interceptor.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/temporalio/worker/_interceptor.py b/temporalio/worker/_interceptor.py index 5d2cc685..1c28b611 100644 --- a/temporalio/worker/_interceptor.py +++ b/temporalio/worker/_interceptor.py @@ -326,9 +326,9 @@ async def handle_query(self, input: HandleQueryInput) -> Any: """Called to handle a query.""" return await self.next.handle_query(input) - async def handle_update_validator(self, input: HandleUpdateInput) -> None: + def handle_update_validator(self, input: HandleUpdateInput) -> None: """Called to handle an update's validation stage.""" - return await self.next.handle_update_validator(input) + return self.next.handle_update_validator(input) async def handle_update_handler(self, input: HandleUpdateInput) -> Any: """Called to handle an update's handler.""" diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index 1052ef5c..b5760f23 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -452,7 +452,7 @@ async def run_update() -> None: # Always run the validator interceptor, which will only actually run a validator if one is defined. with self._as_read_only(): - await self._inbound.handle_update_validator(handler_input) + self._inbound.handle_update_validator(handler_input) # Accept the update command.update_response.accepted.SetInParent() @@ -1788,7 +1788,7 @@ async def handle_query(self, input: HandleQueryInput) -> Any: else: return handler(*input.args) - async def handle_update_validator(self, input: HandleUpdateInput) -> None: + def handle_update_validator(self, input: HandleUpdateInput) -> None: # Do not "or None" the validator, since we only want to use the validator for # the specific named update - we shouldn't fall back to the dynamic validator # for some defined, named update which doesn't have a defined validator. diff --git a/tests/worker/test_interceptor.py b/tests/worker/test_interceptor.py index db172e8a..ef7b0bcb 100644 --- a/tests/worker/test_interceptor.py +++ b/tests/worker/test_interceptor.py @@ -80,9 +80,9 @@ async def handle_query(self, input: HandleQueryInput) -> Any: interceptor_traces.append(("workflow.query", input)) return await super().handle_query(input) - async def handle_update_validator(self, input: HandleUpdateInput) -> None: + def handle_update_validator(self, input: HandleUpdateInput) -> None: interceptor_traces.append(("workflow.update.validator", input)) - return await super().handle_update_validator(input) + return super().handle_update_validator(input) async def handle_update_handler(self, input: HandleUpdateInput) -> Any: interceptor_traces.append(("workflow.update.handler", input)) From de98531a812a84a60e1a9ebff8eaf5d63e018e6b Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 23 Oct 2023 14:40:47 -0700 Subject: [PATCH 20/27] Address various review comments --- temporalio/client.py | 99 ++++++++++++----------------- temporalio/contrib/opentelemetry.py | 2 +- temporalio/workflow.py | 21 +++--- tests/worker/test_workflow.py | 8 ++- 4 files changed, 61 insertions(+), 69 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index c4fc85fc..28d2481e 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -1619,7 +1619,7 @@ async def terminate( @overload async def execute_update( self, - update: temporalio.workflow.UpdateMethodMultiArg[[SelfType], LocalReturnType], + update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], *, id: Optional[str] = None, rpc_metadata: Mapping[str, str] = {}, @@ -1631,7 +1631,7 @@ async def execute_update( @overload async def execute_update( self, - update: temporalio.workflow.UpdateMethodMultiArg[ + update: temporalio.workflow.UpdateMethodMultiParam[ [SelfType, ParamType], LocalReturnType ], arg: ParamType, @@ -1645,7 +1645,7 @@ async def execute_update( @overload async def execute_update( self, - update: temporalio.workflow.UpdateMethodMultiArg[ + update: temporalio.workflow.UpdateMethodMultiParam[ MultiParamSpec, LocalReturnType ], *, @@ -1784,14 +1784,9 @@ async def _start_update( ) -> WorkflowUpdateHandle: update_name: str ret_type = result_type - if isinstance(update, temporalio.workflow.UpdateMethodMultiArg): + if isinstance(update, temporalio.workflow.UpdateMethodMultiParam): defn = update._defn - if not defn: - raise RuntimeError( - f"Update definition not found on {update.__qualname__}, " - "is it decorated with @workflow.update?" - ) - elif not defn.name: + if not defn.name: raise RuntimeError("Cannot invoke dynamic update definition") # TODO(cretz): Check count/type of args at runtime? update_name = defn.name @@ -1801,9 +1796,9 @@ async def _start_update( return await self._client._impl.start_workflow_update( UpdateWorkflowInput( - workflow_id=self._id, + id=self._id, run_id=self._run_id, - update_id=id or "", + update_id=id, update=update_name, args=temporalio.common._arg_or_args(arg, args), headers={}, @@ -3878,7 +3873,7 @@ def __init__( name: str, workflow_id: str, *, - run_id: Optional[str] = None, + workflow_run_id: Optional[str] = None, result_type: Optional[Type] = None, ): """Create a workflow update handle. @@ -3890,29 +3885,29 @@ def __init__( self._id = id self._name = name self._workflow_id = workflow_id - self._run_id = run_id + self._workflow_run_id = workflow_run_id self._result_type = result_type self._known_result: Optional[temporalio.api.update.v1.Outcome] = None @property def id(self) -> str: - """ID of this Update request""" + """ID of this Update request.""" return self._id @property def name(self) -> str: - """The name of the Update being invoked""" + """The name of the Update being invoked.""" return self._name @property def workflow_id(self) -> str: - """The ID of the Workflow targeted by this Update""" + """The ID of the Workflow targeted by this Update.""" return self._workflow_id @property - def run_id(self) -> Optional[str]: - """If specified, the specific run of the Workflow targeted by this Update""" - return self._run_id + def workflow_run_id(self) -> Optional[str]: + """If specified, the specific run of the Workflow targeted by this Update.""" + return self._workflow_run_id async def result( self, @@ -3934,7 +3929,6 @@ async def result( TimeoutError: The specified timeout was reached when waiting for the update result. RPCError: Update result could not be fetched for some other reason. """ - outcome: temporalio.api.update.v1.Outcome if self._known_result is not None: outcome = self._known_result return await _update_outcome_to_result( @@ -3944,23 +3938,20 @@ async def result( self._client.data_converter, self._result_type, ) - else: - return await self._client._impl.poll_workflow_update( - PollUpdateWorkflowInput( - self.workflow_id, - self.run_id, - self.id, - self.name, - timeout, - {}, - self._result_type, - rpc_metadata, - rpc_timeout, - ) - ) - def _set_known_result(self, result: temporalio.api.update.v1.Outcome) -> None: - self._known_result = result + return await self._client._impl.poll_workflow_update( + PollUpdateWorkflowInput( + self.workflow_id, + self.workflow_run_id, + self.id, + self.name, + timeout, + {}, + self._result_type, + rpc_metadata, + rpc_timeout, + ) + ) class WorkflowFailureError(temporalio.exceptions.TemporalError): @@ -4023,11 +4014,9 @@ def message(self) -> str: class WorkflowUpdateFailedError(temporalio.exceptions.TemporalError): """Error that occurs when an update fails.""" - def __init__(self, update_id: str, update_name: str, cause: BaseException) -> None: + def __init__(self, cause: BaseException) -> None: """Create workflow update failed error.""" super().__init__("Workflow update failed") - self._update_id = update_id - self._update_name = update_name self.__cause__ = cause @property @@ -4171,9 +4160,9 @@ class TerminateWorkflowInput: class UpdateWorkflowInput: """Input for :py:meth:`OutboundInterceptor.start_workflow_update`.""" - workflow_id: str + id: str run_id: Optional[str] - update_id: str + update_id: Optional[str] update: str args: Sequence[Any] wait_for_stage: Optional[ @@ -4787,12 +4776,12 @@ async def start_workflow_update( req = temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest( namespace=self._client.namespace, workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.workflow_id, + workflow_id=input.id, run_id=input.run_id or "", ), request=temporalio.api.update.v1.Request( meta=temporalio.api.update.v1.Meta( - update_id=input.update_id, + update_id=input.update_id or "", identity=self._client.identity, ), input=temporalio.api.update.v1.Input( @@ -4814,23 +4803,19 @@ async def start_workflow_update( req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout ) except RPCError as err: - # If the status is INVALID_ARGUMENT, we can assume it's an update - # failed error - if err.status == RPCStatusCode.INVALID_ARGUMENT: - raise WorkflowUpdateFailedError(input.workflow_id, input.update, err) - else: - raise + raise + determined_id = resp.update_ref.update_id update_handle = WorkflowUpdateHandle( client=self._client, - id=input.update_id, + id=determined_id, name=input.update, - workflow_id=input.workflow_id, - run_id=input.run_id, + workflow_id=input.id, + workflow_run_id=input.run_id, result_type=input.ret_type, ) if resp.HasField("outcome"): - update_handle._set_known_result(resp.outcome) + update_handle._known_result = resp.outcome return update_handle @@ -4869,8 +4854,8 @@ async def poll_loop(): input.ret_type, ) except RPCError as err: - if err.status == RPCStatusCode.DEADLINE_EXCEEDED: - continue + if err.status != RPCStatusCode.DEADLINE_EXCEEDED: + raise # Wait for at most the *overall* timeout return await asyncio.wait_for( @@ -5415,8 +5400,6 @@ async def _update_outcome_to_result( ) -> Any: if outcome.HasField("failure"): raise WorkflowUpdateFailedError( - id, - name, await converter.decode_failure(outcome.failure.cause), ) if not outcome.success.payloads: diff --git a/temporalio/contrib/opentelemetry.py b/temporalio/contrib/opentelemetry.py index 7d325e84..d0c158e6 100644 --- a/temporalio/contrib/opentelemetry.py +++ b/temporalio/contrib/opentelemetry.py @@ -249,7 +249,7 @@ async def start_workflow_update( ) -> temporalio.client.WorkflowUpdateHandle: with self.root._start_as_current_span( f"StartWorkflowUpdate:{input.update}", - attributes={"temporalWorkflowID": input.workflow_id}, + attributes={"temporalWorkflowID": input.id}, input=input, kind=opentelemetry.trace.SpanKind.CLIENT, ): diff --git a/temporalio/workflow.py b/temporalio/workflow.py index 56abd1ef..e28e47c0 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -773,7 +773,7 @@ def time_ns() -> int: # Needs to be defined here to avoid a circular import @runtime_checkable -class UpdateMethodMultiArg(Protocol[MultiParamSpec, ProtocolReturnType]): +class UpdateMethodMultiParam(Protocol[MultiParamSpec, ProtocolReturnType]): """Decorated workflow update functions implement this.""" _defn: temporalio.workflow._UpdateDefinition @@ -784,7 +784,9 @@ def __call__( """Generic callable type callback.""" ... - def validator(self, vfunc: Callable[MultiParamSpec, None]) -> None: + def validator( + self, vfunc: Callable[MultiParamSpec, None] + ) -> Callable[MultiParamSpec, None]: """Use to decorate a function to validate the arguments passed to the update handler.""" ... @@ -792,14 +794,14 @@ def validator(self, vfunc: Callable[MultiParamSpec, None]) -> None: @overload def update( fn: Callable[MultiParamSpec, Awaitable[ReturnType]] -) -> UpdateMethodMultiArg[MultiParamSpec, ReturnType]: +) -> UpdateMethodMultiParam[MultiParamSpec, ReturnType]: ... @overload def update( fn: Callable[MultiParamSpec, ReturnType] -) -> UpdateMethodMultiArg[MultiParamSpec, ReturnType]: +) -> UpdateMethodMultiParam[MultiParamSpec, ReturnType]: ... @@ -808,7 +810,7 @@ def update( *, name: str ) -> Callable[ [Callable[MultiParamSpec, ReturnType]], - UpdateMethodMultiArg[MultiParamSpec, ReturnType], + UpdateMethodMultiParam[MultiParamSpec, ReturnType], ]: ... @@ -818,7 +820,7 @@ def update( *, dynamic: Literal[True] ) -> Callable[ [Callable[MultiParamSpec, ReturnType]], - UpdateMethodMultiArg[MultiParamSpec, ReturnType], + UpdateMethodMultiParam[MultiParamSpec, ReturnType], ]: ... @@ -880,10 +882,11 @@ def with_name( def _update_validator( update_def: _UpdateDefinition, fn: Optional[Callable[..., None]] = None -): +) -> Optional[Callable[..., None]]: """Decorator for a workflow update validator method.""" if fn is not None: update_def.set_validator(fn) + return fn def upsert_search_attributes(attributes: temporalio.common.SearchAttributes) -> None: @@ -1187,7 +1190,7 @@ def _apply_to_class( ) else: queries[query_defn.name] = query_defn - elif isinstance(member, UpdateMethodMultiArg): + elif isinstance(member, UpdateMethodMultiParam): update_defn = member._defn if update_defn.name in updates: defn_name = update_defn.name or "" @@ -1230,7 +1233,7 @@ def _apply_to_class( issues.append( f"@workflow.query defined on {base_member.__qualname__} but not on the override" ) - elif isinstance(base_member, UpdateMethodMultiArg): + elif isinstance(base_member, UpdateMethodMultiParam): update_defn = base_member._defn if update_defn.name not in updates: issues.append( diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 5c2385fe..aea3bbc8 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3592,9 +3592,10 @@ async def test_workflow_update_handlers_happy(client: Client, env: WorkflowEnvir async with new_worker( client, UpdateHandlersWorkflow, activities=[say_hello] ) as worker: + wf_id = f"update-handlers-workflow-{uuid.uuid4()}" handle = await client.start_workflow( UpdateHandlersWorkflow.run, - id=f"update-handlers-workflow-{uuid.uuid4()}", + id=wf_id, task_queue=worker.task_queue, ) @@ -3622,6 +3623,11 @@ async def test_workflow_update_handlers_happy(client: Client, env: WorkflowEnvir UpdateHandlersWorkflow.async_named ) + # Get untyped handle + assert "val3" == await client.get_workflow_handle(wf_id).execute_update( + UpdateHandlersWorkflow.last_event, "val4" + ) + async def test_workflow_update_handlers_unhappy( client: Client, env: WorkflowEnvironment From afc1807b09180da8e453f25d788d978e387a140a Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 24 Oct 2023 10:03:21 -0700 Subject: [PATCH 21/27] Update core & add inbound tracing interceptor --- README.md | 2 +- scripts/{ => _proto}/Dockerfile | 0 temporalio/bridge/Cargo.lock | 145 ++++++++++++++---------- temporalio/bridge/sdk-core | 2 +- temporalio/contrib/opentelemetry.py | 40 +++++++ temporalio/worker/_workflow_instance.py | 6 +- tests/contrib/test_opentelemetry.py | 18 ++- tests/worker/test_interceptor.py | 3 - 8 files changed, 148 insertions(+), 68 deletions(-) rename scripts/{ => _proto}/Dockerfile (100%) diff --git a/README.md b/README.md index b8ca3b99..4cc4dbe1 100644 --- a/README.md +++ b/README.md @@ -1452,7 +1452,7 @@ run `poetry update protobuf grpcio-tools`. For a less system-intrusive approach, you can: ```shell -docker build -f scripts/Dockerfile . +docker build -f scripts/_proto/Dockerfile . docker run -v "${PWD}/temporalio/api:/api_new" -v "${PWD}/temporalio/bridge/proto:/bridge_new" poe format ``` diff --git a/scripts/Dockerfile b/scripts/_proto/Dockerfile similarity index 100% rename from scripts/Dockerfile rename to scripts/_proto/Dockerfile diff --git a/temporalio/bridge/Cargo.lock b/temporalio/bridge/Cargo.lock index 79e0c7ae..4154e028 100644 --- a/temporalio/bridge/Cargo.lock +++ b/temporalio/bridge/Cargo.lock @@ -30,13 +30,14 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "cd7d5a2cecb58716e47d67d5703a249964b14c7be1ec3cad3affc295b2d1c35d" dependencies = [ "cfg-if", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -178,9 +179,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -314,9 +315,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4" dependencies = [ "libc", ] @@ -449,7 +450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.1", + "hashbrown 0.14.2", "lock_api", "once_cell", "parking_lot_core", @@ -617,7 +618,7 @@ checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "windows-sys", ] @@ -845,9 +846,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ "ahash", "allocator-api2", @@ -934,7 +935,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -1000,7 +1001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.2", ] [[package]] @@ -1035,9 +1036,9 @@ checksum = "e1be380c410bf0595e94992a648ea89db4dd3f3354ba54af206fd2a68cf5ac8e" [[package]] name = "ipnet" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itertools" @@ -1101,9 +1102,9 @@ checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -1121,7 +1122,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" dependencies = [ - "hashbrown 0.14.1", + "hashbrown 0.14.2", ] [[package]] @@ -1189,9 +1190,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", @@ -1439,13 +1440,13 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets", ] @@ -1855,6 +1856,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.10.2" @@ -1943,17 +1953,16 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.20" +version = "0.17.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" dependencies = [ "cc", + "getrandom", "libc", - "once_cell", "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys", ] [[package]] @@ -2005,9 +2014,9 @@ version = "0.1.0" [[package]] name = "rustix" -version = "0.38.19" +version = "0.38.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" +checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" dependencies = [ "bitflags 2.4.1", "errno", @@ -2018,9 +2027,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" dependencies = [ "log", "ring", @@ -2051,9 +2060,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring", "untrusted", @@ -2088,9 +2097,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", "untrusted", @@ -2240,9 +2249,9 @@ checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -2250,9 +2259,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys", @@ -2260,9 +2269,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.5.2" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "strsim" @@ -2338,9 +2347,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.11" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" [[package]] name = "tempfile" @@ -2350,7 +2359,7 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", "windows-sys", ] @@ -2513,18 +2522,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", @@ -2588,7 +2597,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2 0.5.5", "tokio-macros", "windows-sys", ] @@ -2728,9 +2737,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.39" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", @@ -2771,12 +2780,12 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] @@ -2864,9 +2873,9 @@ checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" @@ -2887,9 +2896,9 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "uuid" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ "getrandom", ] @@ -3135,6 +3144,26 @@ dependencies = [ "libc", ] +[[package]] +name = "zerocopy" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c19fae0c8a9efc6a8281f2e623db8af1db9e57852e04cde3e754dd2dc29340f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc56589e9ddd1f1c28d4b4b5c773ce232910a6bb67a70133d61c9e347585efe9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/temporalio/bridge/sdk-core b/temporalio/bridge/sdk-core index 45d2bc99..7545ad12 160000 --- a/temporalio/bridge/sdk-core +++ b/temporalio/bridge/sdk-core @@ -1 +1 @@ -Subproject commit 45d2bc997fd25bf24d347b04d519e7279851aea4 +Subproject commit 7545ad126166412d282c8dfe43d1650d607867dd diff --git a/temporalio/contrib/opentelemetry.py b/temporalio/contrib/opentelemetry.py index d0c158e6..076b21f0 100644 --- a/temporalio/contrib/opentelemetry.py +++ b/temporalio/contrib/opentelemetry.py @@ -426,6 +426,46 @@ async def handle_query(self, input: temporalio.worker.HandleQueryInput) -> Any: finally: opentelemetry.context.detach(token) + def handle_update_validator( + self, input: temporalio.worker.HandleUpdateInput + ) -> None: + """Implementation of + :py:meth:`temporalio.worker.WorkflowInboundInterceptor.handle_update_validator`. + """ + link_context_header = input.headers.get(self.header_key) + link_context_carrier: Optional[_CarrierDict] = None + if link_context_header: + link_context_carrier = self.payload_converter.from_payloads( + [link_context_header] + )[0] + with self._top_level_workflow_context(success_is_complete=False): + self._completed_span( + f"HandleUpdateValidator:{input.update}", + link_context_carrier=link_context_carrier, + kind=opentelemetry.trace.SpanKind.SERVER, + ) + super().handle_update_validator(input) + + async def handle_update_handler( + self, input: temporalio.worker.HandleUpdateInput + ) -> Any: + """Implementation of + :py:meth:`temporalio.worker.WorkflowInboundInterceptor.handle_update_handler`. + """ + link_context_header = input.headers.get(self.header_key) + link_context_carrier: Optional[_CarrierDict] = None + if link_context_header: + link_context_carrier = self.payload_converter.from_payloads( + [link_context_header] + )[0] + with self._top_level_workflow_context(success_is_complete=False): + self._completed_span( + f"HandleUpdateHandler:{input.update}", + link_context_carrier=link_context_carrier, + kind=opentelemetry.trace.SpanKind.SERVER, + ) + return await super().handle_update_handler(input) + def _load_workflow_context_carrier(self) -> Optional[_CarrierDict]: if self._workflow_context_carrier: return self._workflow_context_carrier diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index b5760f23..29fc0b7e 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -450,9 +450,9 @@ async def run_update() -> None: headers=job.headers, ) - # Always run the validator interceptor, which will only actually run a validator if one is defined. - with self._as_read_only(): - self._inbound.handle_update_validator(handler_input) + if job.run_validator and defn.validator is not None: + with self._as_read_only(): + self._inbound.handle_update_validator(handler_input) # Accept the update command.update_response.accepted.SetInParent() diff --git a/tests/contrib/test_opentelemetry.py b/tests/contrib/test_opentelemetry.py index 22bd8fbf..aba35f8f 100644 --- a/tests/contrib/test_opentelemetry.py +++ b/tests/contrib/test_opentelemetry.py @@ -143,6 +143,16 @@ def query(self) -> str: def signal(self) -> None: self._signal_count += 1 + @workflow.update + def update(self) -> int: + self._signal_count += 1 + return self._signal_count + + @update.validator + def update_validator(self) -> None: + print("Actually in validator") + pass + async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): # TODO(cretz): Fix @@ -175,8 +185,8 @@ async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): actions=[ # First fail on replay TracingWorkflowAction(fail_on_non_replay=True), - # Wait for a signal - TracingWorkflowAction(wait_until_signal_count=1), + # Wait for a signal & update + TracingWorkflowAction(wait_until_signal_count=2), # Exec activity that fails task before complete TracingWorkflowAction( activity=TracingWorkflowActionActivity( @@ -230,6 +240,7 @@ async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): # Send query, then signal to move it along assert "some query" == await handle.query(TracingWorkflow.query) await handle.signal(TracingWorkflow.signal) + await handle.execute_update(TracingWorkflow.update) await handle.result() # Dump debug with attributes, but do string assertion test without @@ -242,6 +253,8 @@ async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): " RunWorkflow:TracingWorkflow", " MyCustomSpan", " HandleSignal:signal (links: SignalWorkflow:signal)", + " HandleUpdateValidator:update (links: StartWorkflowUpdate:update)", + " HandleUpdateHandler:update (links: StartWorkflowUpdate:update)", " StartActivity:tracing_activity", " RunActivity:tracing_activity", " RunActivity:tracing_activity", @@ -263,6 +276,7 @@ async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): "QueryWorkflow:query", " HandleQuery:query (links: StartWorkflow:TracingWorkflow)", "SignalWorkflow:signal", + "StartWorkflowUpdate:update", ] diff --git a/tests/worker/test_interceptor.py b/tests/worker/test_interceptor.py index ef7b0bcb..a9e726d5 100644 --- a/tests/worker/test_interceptor.py +++ b/tests/worker/test_interceptor.py @@ -276,9 +276,6 @@ def pop_trace(name: str, filter: Optional[Callable[[Any], bool]] = None) -> Any: assert pop_trace( "workflow.signal", lambda v: v.args[0] == "external-signal-val" ) - assert pop_trace( - "workflow.update.validator", lambda v: v.args[0] == "update-val" - ) assert pop_trace("workflow.update.handler", lambda v: v.args[0] == "update-val") assert pop_trace( "workflow.update.validator", lambda v: v.args[0] == "reject-me" From 8b410428268749bd34a5be05def9ed732f38aac8 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 24 Oct 2023 10:29:11 -0700 Subject: [PATCH 22/27] Make sure only temporal errors fail update during handler --- README.md | 4 +-- temporalio/client.py | 2 +- temporalio/worker/_workflow_instance.py | 39 ++++++++++++++++++------- tests/worker/test_workflow.py | 24 +++++++++++---- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4cc4dbe1..98048a0f 100644 --- a/README.md +++ b/README.md @@ -594,8 +594,8 @@ Here are the decorators that can be applied: * May mutate workflow state, and make calls to other workflow APIs like starting activities, etc. * Also accepts the `name` and `dynamic` parameters like signals and queries, with the same semantics. * Update handlers may optionally define a validator method by decorating it with `@update_handler_method.validator`. - Validators cannot be `async`, cannot mutate workflow state, and return nothing. They can be used to reject update - calls before any events are written to history by throwing an exception. + To reject an update before any events are written to history, throw an exception in a validator. Validators cannot + be `async`, cannot mutate workflow state, and return nothing. #### Running diff --git a/temporalio/client.py b/temporalio/client.py index 28d2481e..2b1d5708 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -4440,7 +4440,7 @@ async def start_workflow_update( return await self.next.start_workflow_update(input) async def poll_workflow_update(self, input: PollUpdateWorkflowInput) -> Any: - """May be called when calling :py:math:`WorkflowUpdateHandle.result`.""" + """May be called when calling :py:meth:`WorkflowUpdateHandle.result`.""" return await self.next.poll_workflow_update(input) ### Async activity calls diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index 29fc0b7e..6fc72b9b 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -428,6 +428,7 @@ def _apply_do_update( async def run_update() -> None: command = self._add_command() command.update_response.protocol_instance_id = job.protocol_instance_id + past_validation = False try: defn = self._updates.get(job.name) or self._updates.get(None) if not defn: @@ -453,7 +454,17 @@ async def run_update() -> None: if job.run_validator and defn.validator is not None: with self._as_read_only(): self._inbound.handle_update_validator(handler_input) + # Re-process arguments to avoid any problems caused by user mutation of them during validation + args = self._process_handler_args( + job.name, + job.input, + defn.name, + defn.arg_types, + defn.dynamic_vararg, + ) + handler_input.args = args + past_validation = True # Accept the update command.update_response.accepted.SetInParent() command = None # type: ignore @@ -482,17 +493,25 @@ async def run_update() -> None: if isinstance(err, temporalio.workflow.ReadOnlyContextError): self._current_activation_error = err return - # All other errors fail the update - if command is None: - command = self._add_command() - command.update_response.protocol_instance_id = ( - job.protocol_instance_id + # Temporal errors always fail the update. Other errors fail it during validation, but the task during + # handling. + if ( + isinstance(err, temporalio.exceptions.FailureError) + or not past_validation + ): + if command is None: + command = self._add_command() + command.update_response.protocol_instance_id = ( + job.protocol_instance_id + ) + self._failure_converter.to_failure( + err, + self._payload_converter, + command.update_response.rejected.cause, ) - self._failure_converter.to_failure( - err, - self._payload_converter, - command.update_response.rejected.cause, - ) + else: + self._current_activation_error = err + return except BaseException as err: # During tear down, generator exit and no-runtime exceptions can appear if not self._deleting: diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index aea3bbc8..6ead6f17 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -3507,6 +3507,7 @@ async def test_workflow_buffered_metrics(client: Client): bad_validator_fail_ct = 0 +task_fail_ct = 0 @workflow.defn @@ -3583,6 +3584,13 @@ def dynavalidator(name: str, _args: Sequence[RawValue]) -> None: workflow.set_dynamic_update_handler(dynahandler, validator=dynavalidator) return "set" + @workflow.update + def throws_runtime_err(self) -> None: + global task_fail_ct + if task_fail_ct < 1: + task_fail_ct += 1 + raise RuntimeError("intentional failure") + async def test_workflow_update_handlers_happy(client: Client, env: WorkflowEnvironment): if env.supports_time_skipping: @@ -3649,7 +3657,7 @@ async def test_workflow_update_handlers_unhappy( assert isinstance(err.value.cause, ApplicationError) assert "'whargarbl' expected but not found" in err.value.cause.message assert ( - "known updates: [bad_validator first_task_update last_event last_event_async renamed runs_activity set_dynamic]" + "known updates: [bad_validator first_task_update last_event last_event_async renamed runs_activity set_dynamic throws_runtime_err]" in err.value.cause.message ) @@ -3707,14 +3715,12 @@ async def test_workflow_update_handlers_unhappy( assert "Rejected" == err.value.cause.message -async def test_workflow_update_command_in_validator( - client: Client, env: WorkflowEnvironment -): +async def test_workflow_update_task_fails(client: Client, env: WorkflowEnvironment): if env.supports_time_skipping: pytest.skip( "Java test server: https://github.com/temporalio/sdk-java/issues/1903" ) - # Need to not sandbox so behavior of validator can change based on global + # Need to not sandbox so behavior can change based on globals async with new_worker( client, UpdateHandlersWorkflow, workflow_runner=UnsandboxedWorkflowRunner() ) as worker: @@ -3729,3 +3735,11 @@ async def test_workflow_update_command_in_validator( # update will return res = await handle.execute_update(UpdateHandlersWorkflow.bad_validator) assert res == "done" + + # Non-temporal failure should cause task failure in update handler + await handle.execute_update(UpdateHandlersWorkflow.throws_runtime_err) + + # Verify task failures did happen + global task_fail_ct, bad_validator_fail_ct + assert task_fail_ct == 1 + assert bad_validator_fail_ct == 2 From 6b0972d7c9a25e602d14ca9dfb8b9088ca6844cd Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 24 Oct 2023 15:00:36 -0700 Subject: [PATCH 23/27] Renames / no poll update tracing --- temporalio/client.py | 21 +++++++++------------ temporalio/contrib/opentelemetry.py | 17 +++-------------- temporalio/worker/_interceptor.py | 2 +- tests/contrib/test_opentelemetry.py | 4 ++-- tests/test_client.py | 8 ++++---- 5 files changed, 19 insertions(+), 33 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index 2b1d5708..2f989081 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -1795,7 +1795,7 @@ async def _start_update( update_name = str(update) return await self._client._impl.start_workflow_update( - UpdateWorkflowInput( + StartWorkflowUpdateInput( id=self._id, run_id=self._run_id, update_id=id, @@ -3940,13 +3940,12 @@ async def result( ) return await self._client._impl.poll_workflow_update( - PollUpdateWorkflowInput( + PollWorkflowUpdateInput( self.workflow_id, self.workflow_run_id, self.id, self.name, timeout, - {}, self._result_type, rpc_metadata, rpc_timeout, @@ -4157,7 +4156,7 @@ class TerminateWorkflowInput: @dataclass -class UpdateWorkflowInput: +class StartWorkflowUpdateInput: """Input for :py:meth:`OutboundInterceptor.start_workflow_update`.""" id: str @@ -4175,7 +4174,7 @@ class UpdateWorkflowInput: @dataclass -class PollUpdateWorkflowInput: +class PollWorkflowUpdateInput: """Input for :py:meth:`OutboundInterceptor.poll_workflow_update`.""" workflow_id: str @@ -4183,7 +4182,6 @@ class PollUpdateWorkflowInput: update_id: str update: str timeout: Optional[timedelta] - headers: Mapping[str, temporalio.api.common.v1.Payload] ret_type: Optional[Type] rpc_metadata: Mapping[str, str] rpc_timeout: Optional[timedelta] @@ -4434,12 +4432,12 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: await self.next.terminate_workflow(input) async def start_workflow_update( - self, input: UpdateWorkflowInput + self, input: StartWorkflowUpdateInput ) -> WorkflowUpdateHandle: """Called for every :py:meth:`WorkflowHandle.update` and :py:meth:`WorkflowHandle.start_update` call.""" return await self.next.start_workflow_update(input) - async def poll_workflow_update(self, input: PollUpdateWorkflowInput) -> Any: + async def poll_workflow_update(self, input: PollWorkflowUpdateInput) -> Any: """May be called when calling :py:meth:`WorkflowUpdateHandle.result`.""" return await self.next.poll_workflow_update(input) @@ -4766,7 +4764,7 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: ) async def start_workflow_update( - self, input: UpdateWorkflowInput + self, input: StartWorkflowUpdateInput ) -> WorkflowUpdateHandle: wait_policy = ( temporalio.api.update.v1.WaitPolicy(lifecycle_stage=input.wait_for_stage) @@ -4819,7 +4817,7 @@ async def start_workflow_update( return update_handle - async def poll_workflow_update(self, input: PollUpdateWorkflowInput) -> Any: + async def poll_workflow_update(self, input: PollWorkflowUpdateInput) -> Any: req = temporalio.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest( namespace=self._client.namespace, update_ref=temporalio.api.update.v1.UpdateRef( @@ -4859,8 +4857,7 @@ async def poll_loop(): # Wait for at most the *overall* timeout return await asyncio.wait_for( - poll_loop(), - input.timeout.total_seconds() if input.timeout else sys.float_info.max, + poll_loop(), input.timeout.total_seconds() if input.timeout else None ) ### Async activity calls diff --git a/temporalio/contrib/opentelemetry.py b/temporalio/contrib/opentelemetry.py index 076b21f0..906ec2be 100644 --- a/temporalio/contrib/opentelemetry.py +++ b/temporalio/contrib/opentelemetry.py @@ -245,7 +245,7 @@ async def signal_workflow( return await super().signal_workflow(input) async def start_workflow_update( - self, input: temporalio.client.UpdateWorkflowInput + self, input: temporalio.client.StartWorkflowUpdateInput ) -> temporalio.client.WorkflowUpdateHandle: with self.root._start_as_current_span( f"StartWorkflowUpdate:{input.update}", @@ -255,17 +255,6 @@ async def start_workflow_update( ): return await super().start_workflow_update(input) - async def poll_workflow_update( - self, input: temporalio.client.PollUpdateWorkflowInput - ) -> Any: - with self.root._start_as_current_span( - f"PollWorkflowUpdate:{input.update}", - attributes={"temporalWorkflowID": input.workflow_id}, - input=input, - kind=opentelemetry.trace.SpanKind.CLIENT, - ): - return await super().poll_workflow_update(input) - class _TracingActivityInboundInterceptor(temporalio.worker.ActivityInboundInterceptor): def __init__( @@ -440,7 +429,7 @@ def handle_update_validator( )[0] with self._top_level_workflow_context(success_is_complete=False): self._completed_span( - f"HandleUpdateValidator:{input.update}", + f"ValidateUpdate:{input.update}", link_context_carrier=link_context_carrier, kind=opentelemetry.trace.SpanKind.SERVER, ) @@ -460,7 +449,7 @@ async def handle_update_handler( )[0] with self._top_level_workflow_context(success_is_complete=False): self._completed_span( - f"HandleUpdateHandler:{input.update}", + f"HandleUpdate:{input.update}", link_context_carrier=link_context_carrier, kind=opentelemetry.trace.SpanKind.SERVER, ) diff --git a/temporalio/worker/_interceptor.py b/temporalio/worker/_interceptor.py index 1c28b611..af85161d 100644 --- a/temporalio/worker/_interceptor.py +++ b/temporalio/worker/_interceptor.py @@ -328,7 +328,7 @@ async def handle_query(self, input: HandleQueryInput) -> Any: def handle_update_validator(self, input: HandleUpdateInput) -> None: """Called to handle an update's validation stage.""" - return self.next.handle_update_validator(input) + self.next.handle_update_validator(input) async def handle_update_handler(self, input: HandleUpdateInput) -> Any: """Called to handle an update's handler.""" diff --git a/tests/contrib/test_opentelemetry.py b/tests/contrib/test_opentelemetry.py index aba35f8f..bd986f2d 100644 --- a/tests/contrib/test_opentelemetry.py +++ b/tests/contrib/test_opentelemetry.py @@ -253,8 +253,8 @@ async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): " RunWorkflow:TracingWorkflow", " MyCustomSpan", " HandleSignal:signal (links: SignalWorkflow:signal)", - " HandleUpdateValidator:update (links: StartWorkflowUpdate:update)", - " HandleUpdateHandler:update (links: StartWorkflowUpdate:update)", + " ValidateUpdate:update (links: StartWorkflowUpdate:update)", + " HandleUpdate:update (links: StartWorkflowUpdate:update)", " StartActivity:tracing_activity", " RunActivity:tracing_activity", " RunActivity:tracing_activity", diff --git a/tests/test_client.py b/tests/test_client.py index 476d9882..622768a8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -36,7 +36,7 @@ Client, Interceptor, OutboundInterceptor, - PollUpdateWorkflowInput, + PollWorkflowUpdateInput, QueryWorkflowInput, RPCError, RPCStatusCode, @@ -56,9 +56,9 @@ ScheduleUpdateInput, SignalWorkflowInput, StartWorkflowInput, + StartWorkflowUpdateInput, TaskReachabilityType, TerminateWorkflowInput, - UpdateWorkflowInput, WorkflowContinuedAsNewError, WorkflowExecutionStatus, WorkflowFailureError, @@ -404,13 +404,13 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: return await super().terminate_workflow(input) async def start_workflow_update( - self, input: UpdateWorkflowInput + self, input: StartWorkflowUpdateInput ) -> WorkflowUpdateHandle: self._parent.traces.append(("start_workflow_update", input)) return await super().start_workflow_update(input) async def poll_workflow_update( - self, input: PollUpdateWorkflowInput + self, input: PollWorkflowUpdateInput ) -> WorkflowUpdateHandle: self._parent.traces.append(("poll_workflow_update", input)) return await super().poll_workflow_update(input) From 020f6635c3586fcefe03b1fcc78a4df4d80ed1e7 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 24 Oct 2023 15:25:08 -0700 Subject: [PATCH 24/27] Add start_update overloads / update handle type --- temporalio/client.py | 68 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index 2f989081..29f078b9 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -1642,6 +1642,7 @@ async def execute_update( ) -> LocalReturnType: ... + # Overload for multi-param update @overload async def execute_update( self, @@ -1721,6 +1722,63 @@ async def execute_update( ) return await handle.result() + # Overload for no-param start update + @overload + async def start_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], + *, + id: Optional[str] = None, + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: + ... + + # Overload for single-param start update + @overload + async def start_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + [SelfType, ParamType], LocalReturnType + ], + arg: ParamType, + *, + id: Optional[str] = None, + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: + ... + + # Overload for multi-param start update + @overload + async def start_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + MultiParamSpec, LocalReturnType + ], + *, + args: MultiParamSpec.args, + id: Optional[str] = None, + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: + ... + + # Overload for string-name start update + @overload + async def start_update( + self, + update: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: Optional[str] = None, + result_type: Optional[Type] = None, + rpc_metadata: Mapping[str, str] = {}, + rpc_timeout: Optional[timedelta] = None, + ) -> WorkflowUpdateHandle[Any]: + ... + async def start_update( self, update: Union[str, Callable], @@ -1731,7 +1789,7 @@ async def start_update( result_type: Optional[Type] = None, rpc_metadata: Mapping[str, str] = {}, rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle: + ) -> WorkflowUpdateHandle[Any]: """Send an update request to the workflow and return a handle to it. This will target the workflow with :py:attr:`run_id` if present. To use a @@ -1781,7 +1839,7 @@ async def _start_update( result_type: Optional[Type] = None, rpc_metadata: Mapping[str, str] = {}, rpc_timeout: Optional[timedelta] = None, - ) -> WorkflowUpdateHandle: + ) -> WorkflowUpdateHandle[Any]: update_name: str ret_type = result_type if isinstance(update, temporalio.workflow.UpdateMethodMultiParam): @@ -3863,7 +3921,7 @@ async def __anext__(self) -> ScheduleListDescription: return ret -class WorkflowUpdateHandle: +class WorkflowUpdateHandle(Generic[LocalReturnType]): """Handle for a workflow update execution request.""" def __init__( @@ -3915,7 +3973,7 @@ async def result( timeout: Optional[timedelta] = None, rpc_metadata: Mapping[str, str] = {}, rpc_timeout: Optional[timedelta] = None, - ) -> Any: + ) -> LocalReturnType: """Wait for and return the result of the update. The result may already be known in which case no call is made. Otherwise the result will be polled for until returned, or until the provided timeout is reached, if specified. @@ -4804,7 +4862,7 @@ async def start_workflow_update( raise determined_id = resp.update_ref.update_id - update_handle = WorkflowUpdateHandle( + update_handle: WorkflowUpdateHandle[Any] = WorkflowUpdateHandle( client=self._client, id=determined_id, name=input.update, From 6670ee58344c991f63959693ae1a7ad8ff3c0a94 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 25 Oct 2023 09:31:26 -0700 Subject: [PATCH 25/27] Ensure otel test will wait until after task fails to send update --- tests/contrib/test_opentelemetry.py | 35 ++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/contrib/test_opentelemetry.py b/tests/contrib/test_opentelemetry.py index bd986f2d..e9969aa8 100644 --- a/tests/contrib/test_opentelemetry.py +++ b/tests/contrib/test_opentelemetry.py @@ -19,7 +19,7 @@ from temporalio.contrib.opentelemetry import TracingInterceptor from temporalio.contrib.opentelemetry import workflow as otel_workflow from temporalio.testing import WorkflowEnvironment -from temporalio.worker import Worker +from temporalio.worker import UnsandboxedWorkflowRunner, Worker @dataclass @@ -48,6 +48,7 @@ class TracingWorkflowAction: activity: Optional[TracingWorkflowActionActivity] = None continue_as_new: Optional[TracingWorkflowActionContinueAsNew] = None wait_until_signal_count: int = 0 + wait_and_do_update: bool = False @dataclass @@ -71,10 +72,14 @@ class TracingWorkflowActionContinueAsNew: param: TracingWorkflowParam +ready_for_update: asyncio.Semaphore + + @workflow.defn class TracingWorkflow: def __init__(self) -> None: self._signal_count = 0 + self._did_update = False @workflow.run async def run(self, param: TracingWorkflowParam) -> None: @@ -126,6 +131,9 @@ async def run(self, param: TracingWorkflowParam) -> None: await workflow.wait_condition( lambda: self._signal_count >= action.wait_until_signal_count ) + if action.wait_and_do_update: + ready_for_update.release() + await workflow.wait_condition(lambda: self._did_update) async def _raise_on_non_replay(self) -> None: replaying = workflow.unsafe.is_replaying() @@ -144,13 +152,11 @@ def signal(self) -> None: self._signal_count += 1 @workflow.update - def update(self) -> int: - self._signal_count += 1 - return self._signal_count + def update(self) -> None: + self._did_update = True @update.validator def update_validator(self) -> None: - print("Actually in validator") pass @@ -160,6 +166,8 @@ async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): pytest.skip( "Java test server: https://github.com/temporalio/sdk-java/issues/1424" ) + global ready_for_update + ready_for_update = asyncio.Semaphore(0) # Create a tracer that has an in-memory exporter exporter = InMemorySpanExporter() provider = TracerProvider() @@ -176,6 +184,8 @@ async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): task_queue=task_queue, workflows=[TracingWorkflow], activities=[tracing_activity], + # Needed so we can wait to send update at the right time + workflow_runner=UnsandboxedWorkflowRunner(), ): # Run workflow with various actions workflow_id = f"workflow_{uuid.uuid4()}" @@ -185,8 +195,8 @@ async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): actions=[ # First fail on replay TracingWorkflowAction(fail_on_non_replay=True), - # Wait for a signal & update - TracingWorkflowAction(wait_until_signal_count=2), + # Wait for a signal + TracingWorkflowAction(wait_until_signal_count=1), # Exec activity that fails task before complete TracingWorkflowAction( activity=TracingWorkflowActionActivity( @@ -194,6 +204,8 @@ async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): fail_on_non_replay_before_complete=True, ), ), + # Wait for update + TracingWorkflowAction(wait_and_do_update=True), # Exec child workflow that fails task before complete TracingWorkflowAction( child_workflow=TracingWorkflowActionChildWorkflow( @@ -240,7 +252,10 @@ async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): # Send query, then signal to move it along assert "some query" == await handle.query(TracingWorkflow.query) await handle.signal(TracingWorkflow.signal) - await handle.execute_update(TracingWorkflow.update) + # Wait to send the update until after the things that fail tasks are over, as failing a task while the update + # is running can mean we execute it twice, which will mess up our spans. + async with ready_for_update: + await handle.execute_update(TracingWorkflow.update) await handle.result() # Dump debug with attributes, but do string assertion test without @@ -253,11 +268,11 @@ async def test_opentelemetry_tracing(client: Client, env: WorkflowEnvironment): " RunWorkflow:TracingWorkflow", " MyCustomSpan", " HandleSignal:signal (links: SignalWorkflow:signal)", - " ValidateUpdate:update (links: StartWorkflowUpdate:update)", - " HandleUpdate:update (links: StartWorkflowUpdate:update)", " StartActivity:tracing_activity", " RunActivity:tracing_activity", " RunActivity:tracing_activity", + " ValidateUpdate:update (links: StartWorkflowUpdate:update)", + " HandleUpdate:update (links: StartWorkflowUpdate:update)", " StartChildWorkflow:TracingWorkflow", " RunWorkflow:TracingWorkflow", " MyCustomSpan", From 1731fcf800ba6f99b796073b26d129f4e2ae292b Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 25 Oct 2023 11:00:06 -0700 Subject: [PATCH 26/27] Latest review comments --- .../_proto/Dockerfile.dockerignore | 0 temporalio/client.py | 32 +++++++------------ temporalio/contrib/opentelemetry.py | 2 +- tests/test_client.py | 4 +-- 4 files changed, 14 insertions(+), 24 deletions(-) rename .dockerignore => scripts/_proto/Dockerfile.dockerignore (100%) diff --git a/.dockerignore b/scripts/_proto/Dockerfile.dockerignore similarity index 100% rename from .dockerignore rename to scripts/_proto/Dockerfile.dockerignore diff --git a/temporalio/client.py b/temporalio/client.py index 29f078b9..06dbc0c8 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -1708,6 +1708,7 @@ async def execute_update( rpc_timeout: Optional RPC deadline to set for the RPC call. Raises: + WorkflowUpdateFailedError: If the update failed RPCError: There was some issue sending the update to the workflow. """ handle = await self._start_update( @@ -3928,7 +3929,6 @@ def __init__( self, client: Client, id: str, - name: str, workflow_id: str, *, workflow_run_id: Optional[str] = None, @@ -3941,22 +3941,16 @@ def __init__( """ self._client = client self._id = id - self._name = name self._workflow_id = workflow_id self._workflow_run_id = workflow_run_id self._result_type = result_type - self._known_result: Optional[temporalio.api.update.v1.Outcome] = None + self._known_outcome: Optional[temporalio.api.update.v1.Outcome] = None @property def id(self) -> str: """ID of this Update request.""" return self._id - @property - def name(self) -> str: - """The name of the Update being invoked.""" - return self._name - @property def workflow_id(self) -> str: """The ID of the Workflow targeted by this Update.""" @@ -3974,8 +3968,9 @@ async def result( rpc_metadata: Mapping[str, str] = {}, rpc_timeout: Optional[timedelta] = None, ) -> LocalReturnType: - """Wait for and return the result of the update. The result may already be known in which case no call is made. - Otherwise the result will be polled for until returned, or until the provided timeout is reached, if specified. + """Wait for and return the result of the update. The result may already be known in which case no network call + is made. Otherwise the result will be polled for until returned, or until the provided timeout is reached, if + specified. Args: timeout: Optional timeout specifying maximum wait time for the result. @@ -3984,15 +3979,15 @@ async def result( overall timeout has been reached. Raises: + WorkflowUpdateFailedError: If the update failed TimeoutError: The specified timeout was reached when waiting for the update result. RPCError: Update result could not be fetched for some other reason. """ - if self._known_result is not None: - outcome = self._known_result + if self._known_outcome is not None: + outcome = self._known_outcome return await _update_outcome_to_result( outcome, self.id, - self.name, self._client.data_converter, self._result_type, ) @@ -4002,7 +3997,6 @@ async def result( self.workflow_id, self.workflow_run_id, self.id, - self.name, timeout, self._result_type, rpc_metadata, @@ -4238,7 +4232,6 @@ class PollWorkflowUpdateInput: workflow_id: str run_id: Optional[str] update_id: str - update: str timeout: Optional[timedelta] ret_type: Optional[Type] rpc_metadata: Mapping[str, str] @@ -4491,7 +4484,7 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: async def start_workflow_update( self, input: StartWorkflowUpdateInput - ) -> WorkflowUpdateHandle: + ) -> WorkflowUpdateHandle[Any]: """Called for every :py:meth:`WorkflowHandle.update` and :py:meth:`WorkflowHandle.start_update` call.""" return await self.next.start_workflow_update(input) @@ -4823,7 +4816,7 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: async def start_workflow_update( self, input: StartWorkflowUpdateInput - ) -> WorkflowUpdateHandle: + ) -> WorkflowUpdateHandle[Any]: wait_policy = ( temporalio.api.update.v1.WaitPolicy(lifecycle_stage=input.wait_for_stage) if input.wait_for_stage is not None @@ -4865,13 +4858,12 @@ async def start_workflow_update( update_handle: WorkflowUpdateHandle[Any] = WorkflowUpdateHandle( client=self._client, id=determined_id, - name=input.update, workflow_id=input.id, workflow_run_id=input.run_id, result_type=input.ret_type, ) if resp.HasField("outcome"): - update_handle._known_result = resp.outcome + update_handle._known_outcome = resp.outcome return update_handle @@ -4905,7 +4897,6 @@ async def poll_loop(): return await _update_outcome_to_result( res.outcome, input.update_id, - input.update, self._client.data_converter, input.ret_type, ) @@ -5449,7 +5440,6 @@ def _fix_history_enum(prefix: str, parent: Dict[str, Any], *attrs: str) -> None: async def _update_outcome_to_result( outcome: temporalio.api.update.v1.Outcome, id: str, - name: str, converter: temporalio.converter.DataConverter, rtype: Optional[Type], ) -> Any: diff --git a/temporalio/contrib/opentelemetry.py b/temporalio/contrib/opentelemetry.py index 906ec2be..3f292098 100644 --- a/temporalio/contrib/opentelemetry.py +++ b/temporalio/contrib/opentelemetry.py @@ -246,7 +246,7 @@ async def signal_workflow( async def start_workflow_update( self, input: temporalio.client.StartWorkflowUpdateInput - ) -> temporalio.client.WorkflowUpdateHandle: + ) -> temporalio.client.WorkflowUpdateHandle[Any]: with self.root._start_as_current_span( f"StartWorkflowUpdate:{input.update}", attributes={"temporalWorkflowID": input.id}, diff --git a/tests/test_client.py b/tests/test_client.py index 622768a8..f1d17bfc 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -405,13 +405,13 @@ async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: async def start_workflow_update( self, input: StartWorkflowUpdateInput - ) -> WorkflowUpdateHandle: + ) -> WorkflowUpdateHandle[Any]: self._parent.traces.append(("start_workflow_update", input)) return await super().start_workflow_update(input) async def poll_workflow_update( self, input: PollWorkflowUpdateInput - ) -> WorkflowUpdateHandle: + ) -> WorkflowUpdateHandle[Any]: self._parent.traces.append(("poll_workflow_update", input)) return await super().poll_workflow_update(input) From 1c18fc243e81e81a0138c1ba3fbaaa3691d1915c Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 25 Oct 2023 11:20:11 -0700 Subject: [PATCH 27/27] Fix failure setting on update rejection --- temporalio/client.py | 2 +- temporalio/worker/_workflow_instance.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index 06dbc0c8..90f21cd6 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -5445,7 +5445,7 @@ async def _update_outcome_to_result( ) -> Any: if outcome.HasField("failure"): raise WorkflowUpdateFailedError( - await converter.decode_failure(outcome.failure.cause), + await converter.decode_failure(outcome.failure), ) if not outcome.success.payloads: return None diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index 6fc72b9b..939f68ae 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -504,10 +504,11 @@ async def run_update() -> None: command.update_response.protocol_instance_id = ( job.protocol_instance_id ) + command.update_response.rejected.SetInParent() self._failure_converter.to_failure( err, self._payload_converter, - command.update_response.rejected.cause, + command.update_response.rejected, ) else: self._current_activation_error = err