From ade4a3d8d4e3b1716808cc4af017d234c2b3147b Mon Sep 17 00:00:00 2001 From: Alex Dixon Date: Sat, 30 Mar 2024 08:16:02 -0700 Subject: [PATCH] add temporal cdc notebook --- packages/cdc/README.md | 221 ++ packages/cdc/src/descriptors.binpb | Bin 0 -> 241725 bytes .../temporal-process-history_node_cdc.ipynb | 1787 +++++++++++++++++ 3 files changed, 2008 insertions(+) create mode 100644 packages/cdc/README.md create mode 100644 packages/cdc/src/descriptors.binpb create mode 100644 packages/cdc/src/temporal-process-history_node_cdc.ipynb diff --git a/packages/cdc/README.md b/packages/cdc/README.md new file mode 100644 index 0000000..19baa03 --- /dev/null +++ b/packages/cdc/README.md @@ -0,0 +1,221 @@ +# @diachronic/cdc + +> Parse Temporal database streams with Apache Spark + +Pyspark notebooks are available in `src`. + +## Steps + +### Generate protobuf descriptor file + +Generate protobuf descriptor file. we use one from temporal-api@135691242e9b4ed6214a7b5e1231c1c9930ff6c8. +This should correspond to the version of Temporal we are using. + + +Descriptor file is committed in this package. It works with Temporal v1.22 and was generated with +libprotoc 24.3 from the following: + +```shell +git clone https://github.com/temporalio/api.git + +protoc -I . \ + temporal/api/history/v1/message.proto \ + -o descriptors.binpb \ + --include_imports \ + --include_source_info +``` + +### Use the descriptor to decode the protobuf data in the history_node table + +With Debezium CDC connector for Postgres the CDC records have the following schema: +```sparksql + CREATE TABLE spark_catalog.temporal.history_node_cdc + ( + key STRUCT, + value STRUCT + , after : STRUCT, source + : STRUCT, op : STRING, ts_ms : BIGINT, transaction + : STRUCT>, + offset BIGINT, + timestamp BIGINT, + _rescued_data STRING + ) USING delta TBLPROPERTIES ( + 'delta.minReaderVersion' = '1', + 'delta.minWriterVersion' = '2' + ) +``` + +`data` contains protobuf data that can be decoded using the descriptor file: + +```python +from pyspark.sql.functions import * +from pyspark.sql.protobuf.functions import from_protobuf + +df = df.withColumn( + "proto", + from_protobuf( + df.data, + "History", + descFilePath='/path/to/descriptor/file', + options={"recursive.fields.max.depth": "2"}, + ), +).select( + # Primary key columns (in this order) + "shard_id", + "tree_id", + "branch_id", + "node_id", + "txn_id", + # Adds a row per item in the history array entry. The array item is stored in the entry column and star-expended in the next step + explode("proto.events").alias("entry"), + "prev_txn_id", +).select( + # Repeat all fields from above + "shard_id", + "tree_id", + "branch_id", + "node_id", + "txn_id", + "prev_txn_id", + # Star expand the history entry, effectively adding a column per history event type to the table + "entry.*", +) +``` + +For batch processing we can use windows. Streaming workloads can replace the same with self-joins. In either case it's +a bit complicated to get a coherent story from the data similar to what we see in the Temporal UI. + +```python +from pyspark.sql.window import Window + +# Adds a column workflow_info to each row, where workflow_info is the execution start event of each workflow +with_wf_info = ( + df.withColumn( + "workflow_info", + first( + df.workflow_execution_started_event_attributes, + ignorenulls=True, + ).over( + Window.partitionBy("shard_id", "tree_id").orderBy( + -col("txn_id") + ) + ), + ) + .withColumn( + "run_id", + coalesce( + first( + col("workflow_task_failed_event_attributes.new_run_id"), + ignorenulls=True, + ).over( + Window.partitionBy("shard_id", "tree_id", "branch_id").orderBy( + -col("txn_id") + ) + ), + col("workflow_info.original_execution_run_id"), + ), + ) + .withColumn("workflow_id", col("workflow_info.workflow_id")) + .withColumn("workflow_type", col("workflow_info.workflow_type.name")) + .withColumn( "parent_workflow_id", col("workflow_info.parent_workflow_execution.workflow_id") ) + .withColumn( "parent_workflow_run_id", col("workflow_info.parent_workflow_execution.run_id") ) + # .withColumn("run_id", col("workflow_info.original_execution_run_id")) + .withColumn("first_execution_run_id", col("workflow_info.first_execution_run_id")) + .withColumn( + "prev_execution_run_id", + coalesce( + first( + col("workflow_task_failed_event_attributes.base_run_id"), + ignorenulls=True, + ).over( + Window.partitionBy("shard_id", "tree_id", "branch_id").orderBy( + -col("txn_id") + ) + ), + col("workflow_info.continued_execution_run_id"), + ), + ) + .withColumn( + "task_queue", + coalesce( + col("workflow_info.task_queue.normal_name"), + col("workflow_info.task_queue.name"), + ), + ) + # Select all columns in the order we want to view them in + .select( + "workflow_id", + "run_id", + "workflow_type", + "event_time", + "event_type", + "parent_workflow_id", + "parent_workflow_run_id", + "first_execution_run_id", + "prev_execution_run_id", + "task_queue", + "event_id", + "workflow_info", + "workflow", + "workflow_execution_started_event_attributes", + "workflow_execution_completed_event_attributes", + "workflow_execution_failed_event_attributes", + "workflow_execution_timed_out_event_attributes", + "workflow_task_scheduled_event_attributes", + "workflow_task_started_event_attributes", + "workflow_task_completed_event_attributes", + "workflow_task_timed_out_event_attributes", + "workflow_task_failed_event_attributes", + "activity_task_scheduled_event_attributes", + "activity_task_started_event_attributes", + "activity_task_completed_event_attributes", + "activity_task_failed_event_attributes", + "activity_task_timed_out_event_attributes", + "timer_started_event_attributes", + "timer_fired_event_attributes", + "activity_task_cancel_requested_event_attributes", + "activity_task_canceled_event_attributes", + "timer_canceled_event_attributes", + "marker_recorded_event_attributes", + "workflow_execution_signaled_event_attributes", + "workflow_execution_terminated_event_attributes", + "workflow_execution_cancel_requested_event_attributes", + "workflow_execution_canceled_event_attributes", + "request_cancel_external_workflow_execution_initiated_event_attributes", + "request_cancel_external_workflow_execution_failed_event_attributes", + "external_workflow_execution_cancel_requested_event_attributes", + "workflow_execution_continued_as_new_event_attributes", + "start_child_workflow_execution_initiated_event_attributes", + "start_child_workflow_execution_failed_event_attributes", + "child_workflow_execution_started_event_attributes", + "child_workflow_execution_completed_event_attributes", + "child_workflow_execution_failed_event_attributes", + "child_workflow_execution_canceled_event_attributes", + "child_workflow_execution_timed_out_event_attributes", + "child_workflow_execution_terminated_event_attributes", + "signal_external_workflow_execution_initiated_event_attributes", + "signal_external_workflow_execution_failed_event_attributes", + "external_workflow_execution_signaled_event_attributes", + "upsert_workflow_search_attributes_event_attributes", + "workflow_execution_update_accepted_event_attributes", + "workflow_execution_update_rejected_event_attributes", + "workflow_execution_update_completed_event_attributes", + "workflow_properties_modified_externally_event_attributes", + "activity_properties_modified_externally_event_attributes", + "workflow_properties_modified_event_attributes", + "shard_id", + "tree_id", + "branch_id", + "node_id", + "txn_id", + # "prev_txn_id", + "task_id", + "version", + "worker_may_ignore", + ) +) +``` diff --git a/packages/cdc/src/descriptors.binpb b/packages/cdc/src/descriptors.binpb new file mode 100644 index 0000000000000000000000000000000000000000..d1609c38305cb73fff3b2959fef708e26593c7e4 GIT binary patch literal 241725 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 z�&zOW~!b_C;J=N)Z*1;ncp`?{|-~xb3vFZJUycy?biX?YB54;4q?PQW+SfA^3fB zi@e5_ZOvXEduxop@HS;fZUpSa6W!1qz{uIv{l^pPFQ7cgPu;VV4=94{#i z5KZ$9j@(!Qq+d`>D0k|5$_G%#KhV!Vv2^oFj_1u*5d~arTZGBJQY`dnaaOa9=nPUEpK^nv$Lr zlx+*b@<17uGZL%^Ms-BZwudz^>zk16FoxLf-59g}KUA$>JS3rh>tP_L`4mqq1;V9- zIBArKJ1Y$vcd};jMP(P)X5k1?HG6V$L`yjlaZryVMc$1u zamcPh`hlbEEVO5%Y_o1(Xb__mX$+uP-+bCyT z;$o&n!<&rMvmH|wlhpn6B?FPA;65s@E0-*`_Ya=;At3L!>r-bWBjQy0jQzA#LypbD zPb8aqM&J38h$Ne8J8c!<&hs1TbC#@zWN)Adx!h#Ye&bok;YwH6kn9b7`^j3I7V^)7 zv{Nn#kn90kpGzhy(Szr`xY8thV2f(Hn#b{`(@vEoK(c9+LYGXqY}2)lg~D$nn}&tr za^tDIhv^QBh=GKVdQqYv!Zmq#$crl?*~2S2OUABcCheuQon$kiZbDa>33b~}vY9hZ zsZLj8beT<$SZ+H=HjC!s3Mr53%{J zF1v~-ch9?KmzZsD%1zz!m0daVNfh0+R_;Sa*B}xst`es@7N;g$1$K)BklB&4n^BKW@o2Y!Abil;0);uwn`*!jJ_o@k=l1~F=1hq zI6Ewk_{G`7;mPuwo*g@HnfwRs0qIlL0Fykf#>6>0KUu!UzR3CHLhFN>*+(<2zTM{T z{+C4JbJfZ2!HzFgKi};xk}8;7B)x^Z#v2k}&QKPv{|^VndRGr+D*D&E)OuHs_3pQK z!J=k`9G{(r^&k&%EifrMD47!Fq6I2XiQCu|NgP4TA|5yJwVgb9!4j-z&CA|jT%22I zZ*9eQATQ2ox3lUZ8&^*X(M-vu5cqV1c>|5kop~@F>SWJkPH`~dEzR?!i%kD^Jw(d2 zz|_qJ577YRUkG)q`&`S1N3MDYC-^LO*%1%wn=|d^=GnW+#>qRVasejrF$K|CVy#uh`YD(rikTv?70} zRnk*bWfPvOt-snp6ebdiWN6z^ss0iThe9dE^)zwU8C~Dv8PD*W${jzSQX_m4O!0o# zEWNjU=SH;&d_C}$6&c^H^Ro}-4PYk|=sw(UM>Rz~;s@h52IH{xpz=)w*d5Zb(7AB8 z>v|C?v}m&tFHjeTvO@#chHvGH`7D3qpX_FVr|h#GxAl4I&Vj+(MVx63XL|;EyLiAp z%R4Yxc}1mI=osMT4mm4B!o&J*Rzn#s$JSa~zWCx1bLf>F-0W{!?x&GloeyAy1 z=g85}MQ^f%DTx5T?zwlhcCE|oAl6lyDd zCC{570xNRR7HG=e>dEt+hVFOL;sutb>=2!!b2uK24CyO0bh)0{{(N_Dp&KW7c`46; zz}zYfvuuhZ33Rd4zFB9@k_?AyW;zjwRj@_E(*3 zLWHc!w#3nsIIcnVQ)d7sm({krVyn1?A%F1CboNrD-s-4Qxtz88*wIwQMP64>A01 ziZI5^2nkK_2W@IA@S#=Gt!^0YERU@>bq^&GfN-3GocDE+iVrD+o*

)AmelwPx_C_NDq#KFInetmttSC&f_k{zGR(;gO6KycwVdXQ2x_x zn-afmugb-*==)+{xMkwkGnLzM@txzFf z#TW6fUw$!W$mKN|yLyN-Rw4dn6(9!UA2ad0lbtaWzdP9(L;SP!A9i4qRc|C}$5$V_ zciEC|Ed8)?Eerq7;=H^+FB_se_joj*Bq%ZoiFsu*y*qlIisY)f;Nx)^2AV}h>D^(-V;ctUFyyff19uqmc-QV=&-v=X=$I#fD^ENyxP^W`%9R%G zF{`q)mJjCp(avF_z9cbkpaqDRVSQW=ts39RH_*qIxdxV}%W^~iKYMQiC--sO_wLO6 zx!CAs+jsYmaDDunVlKvk7JTJ6ix5%u_~<1&6vb~* zYB`lnj*ZZ1I7V^1IBI?B!c&#hb|agaOl2HS>MdWv@D=P^D9wh3Sy5uq_~VM^<}#-y zV2=1nev2bwgbF>y2~hFK#L2kLj5AB^NKJp|nA^}O6D%zpyL-+Y)tNZhRqRx*v$ABL z3=Yu%!)>u!U~0@R(dtrTbg_eV)kdSkbDgaPGeV7}rHP>h%Mh<{_MEu87nx$FiYC?; zEH!5t0M;Z<{?koN(@iaAyxEjlOQO9%hNDKNh=W)T4$GjSk(d?7>=H6TOtCfmubf_a zOFUk#ZrKtJjcbArnK^+)AF>--JipV;aso3a5>KzZ-JD3S(vjiDtBShZ;jJuAOP%f` zA%LRJkkuE6({U#^D?d&Au|Kl5o=EifztOaiR zvw%{Tg)W(~YG&UOdWs>Gcgk%EPS9+q8Ir27m}{;^b#@I+X5Cinn7syLiq8BjO3W6@ zhk}X4VoJ@9LHZ;Meb?!kuyDj2xAOu2+J@xgi&0)Skx*HdS~7z?QC1*)HhP&uxR`bY z#?Hnf6Es{Ok}?B9!}YkQ{6fKK2(%3vez3rL!q7AD#Fa2$3(*UL#2;!3v~k;q2@J(+ zRcR+Qf{=W?LXH!e{t;3kE?7e4bWI10U$i0ZflNDtSFl)f)u3!N5Lntr7-}^(yuuS= z$gZ&o#%#330YmTARi0te+z2KqHav8}vY^NkBiJb>!?mKDsc6(<^MT-wPnFV!lLx&% z#kIf1_sRpJrX3@2L|ebtQmyeItwBw3#%4BSkv~KS#eUZ#14D}~+$*0AiN-N@Frhl1 zZ_6fBt}xq}P+YS(d65ciudgai`)y%WnGIrRSXFLe*cnz85Dn~PRc(YJa43}5-i@U- z<#QpD&FzFDX0{pvChm*4J3Pe#L_@oAnDT}IZnuVFT)|8YsoUGLhBTzxY3N-f)=^gO zyIPLrO3&uXr#-U~RyL28l`Fi)%(9R}vPq^062{ zSj}imWD(J_PK7dIqLh%xLm4`BG_>^KcZKxpW+K_2J%T2Y75ogz(JkWcUwddb@8LrMGQNj$- zBiI~tw@LL6HV4Ue{(s{Y1_;!hbd|njUSV^`#}=HaWRXQgr5GntBaVD&)_$pM8jg9| z9CfIJV3nrrXS`~LhDOF{u0#EPb6^+&Nouu#R9r6%6B=}f{O>>-^~Ozl&P_DHymmC% zPKJ|vp}GIl&6=z=4j)eavv}E{lSYy+eDFXozn;yaSiu^ z&&+;qchZgjmeMmsiteNj`@EepGmjdP4 zQmSLO6HIR)UT%c)Ii${?qe-GJ_}n8)rwz9AwQ2hqNbxFJC=_n48{rskuD^?W(uACE zp2T}`eW0!#a~0^8wIM4N)jCOid3DywX#$TX$BV?eD#sGx zQKa`KBTXMKbxv3Z#JYx(!zwt+?>s6+Kyd$HA9-&o!xmdw=xx_8$Ml2pR4cgDOEtzq zZyP^&;VtRa@^GykheB#|y^Hj7h9&vK7iqx6G53-0W8jGp?|IAAcsPAxJR2$AGsxoz z15XfOPe}ke_op2iZ~&F&+O(l+Z1)0#36Lg3J6HEA@IdaXfXL8(e_B!l#uBIWAhAw= z+5Aj@W`IHB0mDq8c~1CTtG)K#MfUSiezoa-M?OtS);?Srvj|jjrduL_{sAdmeF`BB zlU$lM$%;C)WppTcr!uxLR?UM}QAJqB+Om*y?>mEw4dy`8G;I!^Ojk<$G7g*(<#o_E z&|gg#9)fx6f-rqmX*ztny5DQo<{9r@;jK0(>ilgIh`X|zMJrZ4UyG+w3;6~iO|P39 z1BL41LO7ulAm^$t1!1iWt}5H!-TX_^}BiJbpaJ19Qk%zbv+JX7se`i~dya z+>tIloF!8UgFRiD@I3CG;@uzAARR!+@jl*(+tzg%dcC#AL{?qhsM~wn^@|Oycwf4( zRtp390>S<3rmmMM(y6xf-YJzke;nsAPe(IY`_)Vt;uU$EO$xa_U-p?|EZ4^gdcZCa4(h6Z-YtO6Ur9U1V^+LZDZsCp)%U^dD%(`=Mk zx!Ebda--WR$6m2hzGA1;8TS=C(D5xO#EOg>-y%`o zW&4V}d80?nMlPc}qDRc8DKW8|Le!L-OgBx5iQPm@>=vsAu{I~$N}Vx%C{K@9Zwl?w z((feQe1Kn#qVYC|usXLSZeG91uDF31`sOWL)8`dphE+;NsgYhOg zyV#Zy$4qA-Hq1kKw-Fn*-93YL64hF|rF>FGMLW?dK!|{~kQbxIbQJPo%DA$VK!-yR zz)WnrliN!>m62ieX^n&n#R*F|huwl~bXcW=87z z0738Fyi5>6efPE;@EA;uX>Xgo7w4hAcZBdptWOO7ju58^#D{l;@J1k-yCZ}*nu%mz z2;Bsr+Pr1m}^ z?Tb`Lu3c(a;5#JOEnCWR@VTeW9#B*!ds%s$OaL?&gJyyF@Mwqou>sNCQG76jg}s5$ z6G#X^wVpsiAgc8Q5&}`J$L+(4wLaEz+Od$E&N2Ke)SS8L98XReX#pVZaewbnMSOUC z&E{A}bH}%Ns1IfM79QVm(h@)m`j!$vAHVHHXqB2LvQ+j$GG<`MfU2?(1Pw&B6M;Q2 z$|>i%XZi=ATFk%>M75J)Y;L_f$=F_NW1x-R zPJ=Q}hGdO0D{}!8(nNCkjN!NTtjHa-?uQll(?4c8Rf3?r%0jh!6JM%8VA;Dfl2c6F zd-{y=0ZvS3@gXxlkbluxGB|1m@DVB&?hmoS0L;MsiTeNo@!|c8oghFocR%)e&R8K` z{e#KFrKg%j#cjK7>)yTi!(wRm5Q!`@&Z8mvhc0g>u4D@qWJ3K&H>q^>d&`{@qn#3| zT&kNNF!J0(A0eYB=dK3U5&I6l4~F#8K;Zk}Y7aj1xcCQe_n0yuIOf4SUTqvB$Wn$s zxbLA9*At5^f3&4PSNe1=*ycSBar%rMJk>+UO6FY3ZPEzop-0CEO8{S`B}LTYb1SD^ zi_;9V9j^}Tc8X1B9P>g*l#lgX7lODJWLZP)Cc)w{HkS`il2Lzz+{?E0WXk+AQsm7kSrPqEYB}>0SW|h4rbyJV^uMOIcU z=Otn-w=;p{`^UeKl(RQ;bsXE!a=h?iVXcw>d7n(x+wMJB3BeVm;aMDx%}~5{!iX{S9XE$)$7wH zZ`Mv;CW3DkKft%<)0@xQcjv;jQ)cix#y#}Fm4V?3*0hTio^;%K_7q_--I;4zA3-^X z&&oxl?^w^>-G?7Me5||g^ciAt>YVF#rQ7FT_23b*iJj>_g1_DT>K#7ZeMTo)}{9bl%S?bm+UUGgV)tZZ!sux@4M)!)B-78*p z7NGZvmz~`6P7p>ZXFYi}nk!=F{HVA=kNL~|_R2@P4<5t6P?3vxiq*JEqH1Au;{39_ zWlbptXF%&k1JcYnWc$MPJhm5z4FK|)aKP$WY=80?EZ5B=Na-*tk>Iszg;NL zX`7P9KgHE;fY)@C-G6B0sZne?7u|i0Af~5LOe+JEx@WJWlg>dMy%#<9O zs5m^zkqi9`Pg`ppnS`6H*;=0N=Y1mggTdDT2io8PVy=Bs*?LbM#-Wq*9oZ@^oY2Fi z&GBud`~tQu8!Ml|fy+~H7)p-GlSijejxuy5k7RRHgw}qhFqg^oSKX@Q|MHR~XfU~} zv{t$QCr7ID2Z1c>fNP@KiTx;`DsbMd{vf6xIQG_kcc$OW94ohyBaFaeH$3zmv7zgk>)+~Gib?APqBEOtL&liwJx5>Z;T^eqPHdp-to=WB0I1l&__^FV* zdBMra320m??n@VpPnthS@Q|4Gy$cdX$118F(suGPh_~}Jgrgo(__fG<<$Ne=Ze49?B zYhzhfeO%3q44|G=UpNq<3cYL;Q`$M5eHmXoz*{MP6Wa?ftJFOaP^Cw}srTwwG18Ma zNg}(+8*DC51jGuRe}+x?AdG&Q=$wIysMK!|`R|)kDSpz3O0g`fJ_D8Fx1pVeREpok z_U25bSTQu-`E=UW8@#Ds00-WlF2E8e*${ZJ=`;lq(i^`ƥ-Z=T3zb(*`)O%2>_ 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 NQ)AP_ls^v-`yaSAiBJFl literal 0 HcmV?d00001 diff --git a/packages/cdc/src/temporal-process-history_node_cdc.ipynb b/packages/cdc/src/temporal-process-history_node_cdc.ipynb new file mode 100644 index 0000000..a5004be --- /dev/null +++ b/packages/cdc/src/temporal-process-history_node_cdc.ipynb @@ -0,0 +1,1787 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "7372ff3f-b186-4c21-88c6-819f44ce3203", + "showTitle": false, + "title": "" + }, + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## Overview\n", + "Temporal provides an append-only table `history_node` that contains all data.\n", + "It deletes rows from the table but only to save space.\n", + "\n", + "## Generating a descriptor file for `temporal.api.history.v1.History` (array of `HistoryEvent`)\n", + "```shell\n", + "git clone https://github.com/temporalio/api.git\n", + "cd api\n", + "protoc -I . \\ \n", + " temporal/api/history/v1/message.proto \\\n", + " -o descriptors.binpb \\\n", + " --include_imports \\\n", + " --include_source_info\n", + "```\n", + "\n", + "The output file `descriptors.binpb` should be placed somewhere Spark can read it. In Databricks, a convenient place is DBFS. This can be done via the UI by clicking \"upload\" in a folder in the file explorer and selecting the file, or programmatically via the Databricks API.\n", + "\n", + "## References\n", + "- https://github.com/temporalio/api/tree/v1.24.0\n", + "- https://spark.apache.org/docs/latest/sql-data-sources-protobuf.html\n", + "- https://docs.gcp.databricks.com/structured-streaming/protocol-buffers.html\n", + "- https://buf.build/docs/reference/descriptors#generating-and-exchanging-descriptors\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "d3855226-c161-4df5-a3a7-de513bff3d74", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## `temporal.history_node_cdc`\n", + "- We save this in Databricks as a delta table for performance only\n", + "- The CDC records contain deletes. We do not care about this at all because the table is append only. Deletes are an implementation detail of Temporal that are part of garbage collection. They are useful only if we want to analyze how it cleans up the database, or something" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "2667c0ee-2577-4195-85ec-110accc4a4be", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# raw = spark.read.format(\"parquet\").load(\"gs://path/to/history-cdc/\").dropDuplicates()\n", + "# spark.sql(\"CREATE DATABASE IF NOT EXISTS temporal\")\n", + "\n", + "# raw.write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"temporal.history_node_cdc\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "The history node table from Debezium / Kafka CDC has the following schema:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "67323d44-a4be-478f-83e8-5058067a3f94", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# %sql\n", + "# CREATE TABLE spark_catalog.temporal.history_node_cdc (\n", + "# key STRUCT < shard_id: INT,\n", + "# tree_id: BINARY,\n", + "# branch_id: BINARY,\n", + "# node_id: BIGINT,\n", + "# txn_id: BIGINT >,\n", + "# value STRUCT < before: STRUCT < shard_id: INT,\n", + "# tree_id: BINARY,\n", + "# branch_id: BINARY,\n", + "# node_id: BIGINT,\n", + "# txn_id: BIGINT,\n", + "# data: BINARY,\n", + "# data_encoding: STRING,\n", + "# prev_txn_id: BIGINT >,\n", + "# after: STRUCT < shard_id: INT,\n", + "# tree_id: BINARY,\n", + "# branch_id: BINARY,\n", + "# node_id: BIGINT,\n", + "# txn_id: BIGINT,\n", + "# data: BINARY,\n", + "# data_encoding: STRING,\n", + "# prev_txn_id: BIGINT >,\n", + "# source: STRUCT < version: STRING,\n", + "# connector: STRING,\n", + "# name: STRING,\n", + "# ts_ms: BIGINT,\n", + "# snapshot: STRING,\n", + "# db: STRING,\n", + "# sequence: STRING,\n", + "# schema: STRING,\n", + "# table: STRING,\n", + "# txId: BIGINT,\n", + "# lsn: BIGINT,\n", + "# xmin: BIGINT >,\n", + "# op: STRING,\n", + "# ts_ms: BIGINT,\n", + "# transaction: STRUCT < id: STRING,\n", + "# total_order: BIGINT,\n", + "# data_collection_order: BIGINT > >,\n", + "# offset BIGINT,\n", + "# timestamp BIGINT,\n", + "# _rescued_data STRING\n", + "# ) USING delta TBLPROPERTIES (\n", + "# 'delta.minReaderVersion' = '1',\n", + "# 'delta.minWriterVersion' = '2'\n", + "# )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "This is a fast way to copy in batch in Databricks and uses checkpointing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "dda4ea02-c0ca-4b57-8dca-973112497c9e", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "%sql\n", + "CREATE TABLE IF NOT EXISTS `temporal`.`history_node_cdc`;\n", + "COPY INTO temporal.history_node_cdc \n", + "FROM 'gs://path/to/history-node-cdc/'\n", + "FILEFORMAT = parquet\n", + "COPY_OPTIONS ('mergeSchema' = 'true')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "Print out information about the CDC source data from Debezium/Kafka (optional)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2024-03-30T14:44:57.665973Z", + "start_time": "2024-03-30T14:44:57.522020Z" + }, + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "bac16329-4728-4c92-909f-1944c5afc60c", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "from pyspark.sql.functions import col\n", + "\n", + "n_deletes = spark.table(\"temporal.history_node_cdc\").where(col(\"value.after\").isNull()).count()\n", + "n_cdc_records = spark.table(\"temporal.history_node_cdc\").count()\n", + "n_table_rows = spark.table(\"temporal.history_node_cdc\").where(col(\"value.after\").isNotNull()).count()\n", + "n_modifications = spark.table(\"temporal.history_node_cdc\").where(col('value.before').isNotNull()).where(col(\"value.after\").isNotNull()).count()\n", + "\n", + "print({\n", + " 'n_deletes': n_deletes,\n", + " 'n_modifications': n_modifications,\n", + " 'n_cdc_records': n_cdc_records,\n", + " 'n_table_rows': n_table_rows\n", + "})" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "f27736f2-33e9-4e8f-97a6-2a688d3ccc4b", + "showTitle": false, + "title": "" + } + }, + "source": [ + "# Temporal History\n", + "\n", + "Provides the `\"temporal.history\"` table from Temporal's raw `history_node` table with the following properties:\n", + "- All event log / history fields at the top level of the table (decoded from their protobuf representation)\n", + "- All rows have `workflow_info` which contains workflow id and other core identifying information about the workflow that the row belongs to\n", + "- This is optimized for batch workloads. For streaming we cannot use Window. Instead, we can swap with self joins." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "ebbfce4a-32c9-4cf3-ba19-67da09ee34da", + "showTitle": false, + "title": "" + }, + "is_executing": true + }, + "outputs": [], + "source": [ + "from pyspark.sql.functions import *\n", + "from pyspark.sql.protobuf.functions import to_protobuf, from_protobuf\n", + "from pyspark.sql.window import Window\n", + "\n", + "\n", + "# Get Temporal history_node table\n", + "input_df = (\n", + " spark.read.table(\"temporal.history_node_cdc\")\n", + " .where(col(\"value.after\").isNotNull())\n", + " .select(col(\"value.after.*\"))\n", + " .dropDuplicates()\n", + ")\n", + "\n", + "# Order history_node table such that workflow executions are grouped and within each one their events are in ascending order\n", + "history_node_df = input_df#.orderBy(\"tree_id\", \"branch_id\", \"node_id\", \"txn_id\")\n", + "\n", + "\n", + "# Decode the data column and spread all columns from it on the same level as the history_node table columns\n", + "temporal_history_node_proto_descriptor_filepath = (\n", + " \"./descriptors.binpb\"\n", + ")\n", + "\n", + "history_node_exploded_proto = (\n", + " input_df.withColumn(\n", + " \"proto\",\n", + " from_protobuf(\n", + " input_df.data,\n", + " \"History\",\n", + " descFilePath=temporal_history_node_proto_descriptor_filepath,\n", + " options={\"recursive.fields.max.depth\": \"2\"},\n", + " ),\n", + " )\n", + " .select(\n", + " # Primary key columns (in this order)\n", + " \"shard_id\",\n", + " \"tree_id\",\n", + " \"branch_id\",\n", + " \"node_id\",\n", + " \"txn_id\",\n", + " # Adds a row per item in the history array entry. The array item is stored in the entry column and star-expended in the next step\n", + " explode(\"proto.events\").alias(\"entry\"),\n", + " \"prev_txn_id\",\n", + " )\n", + " .select(\n", + " # Repeat all fields from above\n", + " \"shard_id\",\n", + " \"tree_id\",\n", + " \"branch_id\",\n", + " \"node_id\",\n", + " \"txn_id\",\n", + " # Star expand the history entry, effectively adding a column per history event type to the table\n", + " \"entry.*\",\n", + " \"prev_txn_id\",\n", + " )\n", + ")\n", + "\n", + "# Adds a column workflow_info to each row, where workflow_info is the execution start event of each workflow\n", + "with_wf_info = (\n", + " history_node_exploded_proto.withColumn(\n", + " \"workflow_info\",\n", + " first(\n", + " history_node_exploded_proto.workflow_execution_started_event_attributes,\n", + " ignorenulls=True,\n", + " ).over(\n", + " Window.partitionBy(\"shard_id\", \"tree_id\").orderBy(\n", + " -col(\"txn_id\")\n", + " )\n", + " ),\n", + " )\n", + " .withColumn(\n", + " \"run_id\",\n", + " coalesce(\n", + " first(\n", + " col(\"workflow_task_failed_event_attributes.new_run_id\"),\n", + " ignorenulls=True,\n", + " ).over(\n", + " Window.partitionBy(\"shard_id\", \"tree_id\", \"branch_id\").orderBy(\n", + " -col(\"txn_id\")\n", + " )\n", + " ),\n", + " col(\"workflow_info.original_execution_run_id\"),\n", + " ),\n", + " )\n", + " .withColumn(\"workflow_id\", col(\"workflow_info.workflow_id\"))\n", + " .withColumn(\"workflow_type\", col(\"workflow_info.workflow_type.name\"))\n", + " .withColumn(\n", + " \"parent_workflow_id\", col(\"workflow_info.parent_workflow_execution.workflow_id\")\n", + " )\n", + " .withColumn(\n", + " \"parent_workflow_run_id\", col(\"workflow_info.parent_workflow_execution.run_id\")\n", + " )\n", + " # .withColumn(\"run_id\", col(\"workflow_info.original_execution_run_id\"))\n", + " .withColumn(\"first_execution_run_id\", col(\"workflow_info.first_execution_run_id\"))\n", + " .withColumn(\n", + " \"prev_execution_run_id\",\n", + " coalesce(\n", + " first(\n", + " col(\"workflow_task_failed_event_attributes.base_run_id\"),\n", + " ignorenulls=True,\n", + " ).over(\n", + " Window.partitionBy(\"shard_id\", \"tree_id\", \"branch_id\").orderBy(\n", + " -col(\"txn_id\")\n", + " )\n", + " ),\n", + " col(\"workflow_info.continued_execution_run_id\"),\n", + " ),\n", + " )\n", + " .withColumn(\n", + " \"task_queue\",\n", + " coalesce(\n", + " col(\"workflow_info.task_queue.normal_name\"),\n", + " col(\"workflow_info.task_queue.name\"),\n", + " ),\n", + " )\n", + " # Select all columns in the order we want to view them in\n", + " .select(\n", + " \"workflow_id\",\n", + " \"run_id\",\n", + " \"workflow_type\",\n", + " \"event_time\",\n", + " \"event_type\",\n", + " \"parent_workflow_id\",\n", + " \"parent_workflow_run_id\",\n", + " \"first_execution_run_id\",\n", + " \"prev_execution_run_id\",\n", + " \"temporal_ui_link\",\n", + " \"task_queue\",\n", + " \"event_id\",\n", + " \"workflow_info\",\n", + " \"workflow\",\n", + " \"workflow_execution_started_event_attributes\",\n", + " \"workflow_execution_completed_event_attributes\",\n", + " \"workflow_execution_failed_event_attributes\",\n", + " \"workflow_execution_timed_out_event_attributes\",\n", + " \"workflow_task_scheduled_event_attributes\",\n", + " \"workflow_task_started_event_attributes\",\n", + " \"workflow_task_completed_event_attributes\",\n", + " \"workflow_task_timed_out_event_attributes\",\n", + " \"workflow_task_failed_event_attributes\",\n", + " \"activity_task_scheduled_event_attributes\",\n", + " \"activity_task_started_event_attributes\",\n", + " \"activity_task_completed_event_attributes\",\n", + " \"activity_task_failed_event_attributes\",\n", + " \"activity_task_timed_out_event_attributes\",\n", + " \"timer_started_event_attributes\",\n", + " \"timer_fired_event_attributes\",\n", + " \"activity_task_cancel_requested_event_attributes\",\n", + " \"activity_task_canceled_event_attributes\",\n", + " \"timer_canceled_event_attributes\",\n", + " \"marker_recorded_event_attributes\",\n", + " \"workflow_execution_signaled_event_attributes\",\n", + " \"workflow_execution_terminated_event_attributes\",\n", + " \"workflow_execution_cancel_requested_event_attributes\",\n", + " \"workflow_execution_canceled_event_attributes\",\n", + " \"request_cancel_external_workflow_execution_initiated_event_attributes\",\n", + " \"request_cancel_external_workflow_execution_failed_event_attributes\",\n", + " \"external_workflow_execution_cancel_requested_event_attributes\",\n", + " \"workflow_execution_continued_as_new_event_attributes\",\n", + " \"start_child_workflow_execution_initiated_event_attributes\",\n", + " \"start_child_workflow_execution_failed_event_attributes\",\n", + " \"child_workflow_execution_started_event_attributes\",\n", + " \"child_workflow_execution_completed_event_attributes\",\n", + " \"child_workflow_execution_failed_event_attributes\",\n", + " \"child_workflow_execution_canceled_event_attributes\",\n", + " \"child_workflow_execution_timed_out_event_attributes\",\n", + " \"child_workflow_execution_terminated_event_attributes\",\n", + " \"signal_external_workflow_execution_initiated_event_attributes\",\n", + " \"signal_external_workflow_execution_failed_event_attributes\",\n", + " \"external_workflow_execution_signaled_event_attributes\",\n", + " \"upsert_workflow_search_attributes_event_attributes\",\n", + " \"workflow_execution_update_accepted_event_attributes\",\n", + " \"workflow_execution_update_rejected_event_attributes\",\n", + " \"workflow_execution_update_completed_event_attributes\",\n", + " \"workflow_properties_modified_externally_event_attributes\",\n", + " \"activity_properties_modified_externally_event_attributes\",\n", + " \"workflow_properties_modified_event_attributes\",\n", + " \"shard_id\",\n", + " \"tree_id\",\n", + " \"branch_id\",\n", + " \"node_id\",\n", + " \"txn_id\",\n", + " # \"prev_txn_id\",\n", + " \"task_id\",\n", + " \"version\",\n", + " \"worker_may_ignore\",\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "b2ea4960-89ee-40cb-a5d1-86a9b8223d7b", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Invariants\n", + "\n", + "Derivations require the following invariants about the base tables to hold:\n", + "\n", + "For the base history node table in Databricks we assume 1 row per the primary key Temporal uses in Postgres: `\"shard_id\"` + `\"tree_id\"` + `\"branch_id\"` + `\"node_id\"` + `\"txn_id\"`. \n", + "\n", + "For the decoded protobuf tables, we assume 1 row per `\"shard_id\"` + `\"tree_id\"` + `\"branch_id\"` + `\"node_id\"` + `\"event_id\"`, where `\"event_id\"` a value from the decoded protobuf array items for History. This is a monotonically increasing number that Temporal uses to identify log entries in their API.\n", + "\n", + "The code in the following cell expresses these invariants. It throws errors if our assumptions about the base data are ever violated, stopping updates (or creation) of the tables that we use for further computations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "2f2fa985-ee00-4e37-b60f-287da0cc2bf5", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "history_node_primary_key = [\"shard_id\", \"tree_id\", \"branch_id\", \"node_id\", \"txn_id\"]\n", + "\n", + "# Source data has one row per primary key\n", + "assert (\n", + " input_df.orderBy(col(\"timestamp\").desc())\n", + " .groupBy(*history_node_primary_key)\n", + " .agg(count(\"*\").alias('ct'))\n", + " .where(col(\"ct\") > 1)\n", + " .count()\n", + " == 0\n", + ")\n", + "\n", + "# Exploded proto has one one row per shard_id + tree_id + branch_id + node_id (from the source table) + event_id (from the decoded and exploded protobuf)\n", + "# (expected if CDC table rows are edited/updated and/or temporal appends to the table only)\n", + "assert(\n", + " history_node_exploded_proto.groupBy(\n", + " \"shard_id\", \"tree_id\", \"branch_id\", \"node_id\", \"event_id\"\n", + " )\n", + " .agg(count(\"event_id\").alias(\"ct_event\"))\n", + " .where(col(\"ct_event\") > 1)\n", + " .count()\n", + " == 0\n", + ")\n", + "\n", + "# The table with workflow info should have no repeats for the same criteria\n", + "assert(\n", + " with_wf_info.groupBy(\"shard_id\", \"tree_id\", \"branch_id\", \"node_id\", \"event_id\")\n", + " .agg(count(\"event_id\").alias(\"ct_event\"))\n", + " .where(col(\"ct_event\") > 1)\n", + " .count()\n", + " == 0\n", + ")\n", + "\n", + "# All workflow started event rows have event_type workflow_execution_started,\n", + "# non-null column `workflow_execution_started_event_attributes`\n", + "# and event_id.= 1\n", + "assert (\n", + " with_wf_info.where(\n", + " (col(\"event_type\") == \"EVENT_TYPE_WORKFLOW_EXECUTION_STARTED\")\n", + " & (col(\"workflow_execution_started_event_attributes\").isNotNull())\n", + " & (col(\"event_id\") != 1)\n", + " ).count()\n", + " == 0\n", + ")\n", + "\n", + "# All rows have workflow_id\n", + "assert(with_wf_info.where(col(\"workflow_id\").isNull()).count() == 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "a4bacbd3-5d57-4040-bcde-e7e395045974", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Drop Columns\n", + "- We can always add these back\n", + "- We expect they are not needed by anything from here on\n", + "- Anything we leave will be needed for something or other" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "764cf51f-3e15-4061-b145-a6ca8a89f341", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "with_wf_info = with_wf_info.drop(\n", + " # 100% missing\n", + " \"version\",\n", + " \"worker_may_ignore\",\n", + "\n", + " # Primary key attributes of the Postgres table\n", + " # Used to derive ordering and uniqueness, don't expect to need them past this point. If we do we can add them back\n", + " \"shard_id\",\n", + " \"tree_id\",\n", + " \"branch_id\",\n", + " \"node_id\",\n", + " \"txn_id\",\n", + " \"task_id\",\n", + " # \"event_id\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "b19d0752-ce02-4514-bb12-59ef421c505a", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Drop Rows\n", + "- Ignore history of temporal system workflows" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "8f8812b4-ff13-4f5c-a9a9-4220be3f8501", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "is_ignored_workflow_type = col(\"workflow_type\").isin(\n", + " [\n", + " \"temporal-sys-batch-workflow\",\n", + " \"ExecuteBlobStoreCleanupCron\",\n", + " \"RunScheduleSyncCron\",\n", + " \"UpdateTimedOutWorkflowRuns\",\n", + " \"temporalCloudAuthRotationWorkflow\"\n", + " \"temporal-sys-history-scanner-workflow\"\n", + " \"temporal-sys-tq-scanner-workflow\",\n", + " ]\n", + ")\n", + "with_wf_info = with_wf_info.where(~is_ignored_workflow_type)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "5c799821-ba8d-41dd-9bf3-7e51ab0a3bf5", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Save table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "42bb63b6-a4d2-45de-8b3b-3ecbc5955c6b", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "with_wf_info.write.format(\"delta\").mode(\"overwrite\").saveAsTable(\"temporal.history\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "02da8177-8c9b-45c8-b808-a930e21f0707", + "showTitle": false, + "title": "" + } + }, + "source": [ + "# Temporal Workflows\n", + "\n", + "Produces a table `temporal.workflows` with the following properties:\n", + "\n", + "- The set of all workflows, independent of executions. This simply means unique by `\"workflow_id\"`.\n", + "- Excludes child workflows.\n", + "- The status of the workflow, one of \"RUNNING\", \"TERMINATED\", \"COMPLETED\". This is derived from the latest execution that corresponds to the workflow_id.\n", + "- The time the workflow started.\n", + "- The time the workflow completed, if any.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "2f2e327b-22f4-4ebe-bb71-cc408fad74f6", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Running Workflows\n", + "Derives from `temporal.history` the workflows that are root workflows, meaning\n", + "1. they have started events\n", + "2. they have no parent\n", + "3. there is (at least) 1 run for which there is no corresponding event that terminates the run (terminated, failed, completed, cancelled, continued as new)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "109434f2-1516-41ca-a84f-cd06b7e48aa9", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "is_terminal_event_type = col(\"event_type\").isin(\n", + " [\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_TERMINATED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_FAILED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_CONTINUED_AS_NEW\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT\",\n", + " ]\n", + ")\n", + "\n", + "wind = Window.partitionBy(\"workflow_id\").orderBy(col(\"event_time\").desc())\n", + "# a better name in this context might be execution or workflow execution to disambiguate from other uses of workflow\n", + "running_workflows = (\n", + " # Inconsistent with the Temporal UI, which shows children and root workflows together\n", + " with_wf_info.where(col(\"parent_workflow_id\").isNull()) \n", + " .withColumn(\"latest_event_time\", max(col(\"event_time\")).over(wind))\n", + " .where(col(\"event_time\") == col(\"latest_event_time\"))\n", + " .where(~is_terminal_event_type)\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "8fa4a841-6a61-4473-b190-2c580c076983", + "showTitle": false, + "title": "" + } + }, + "source": [ + "### Invariants\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "a7adfcfd-bbfe-452e-9596-1a1c553d6bc9", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# No duplicate workflow ids for running workflows\n", + "assert (\n", + " running_workflows.dropDuplicates([\"workflow_id\"]).count()\n", + " == running_workflows.count()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "5c3b5992-fa32-49ad-8aba-ae6664911d1e", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Completed Workflows\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "ef60b0cb-fdd8-4ca9-8acf-2c3a1f156bb6", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "completed_workflows = (\n", + " with_wf_info\n", + " .where(col(\"event_type\") == \"EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "42688f2b-8146-4fec-8fe8-134f12776457", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# TODO. move. maybe this is reusable\n", + "def get_histories_for_runs(wfs: DataFrame, history: DataFrame):\n", + " wfs.select(\"workflow_id\", \"run_id\").alias(\"runs\").join(\n", + " history.alias(\"history\"),\n", + " [\"workflow_id\", \"run_id\"],\n", + " \"inner\",\n", + " ).orderBy(\"workflow_id\", \"run_id\", \"event_id\")\n", + "\n", + "\n", + "completed_of_example_type = (\n", + " completed_workflows.where(col(\"workflow_type\") == \"example\")\n", + " .select(\"workflow_id\", \"run_id\")\n", + " .alias(\"runs\")\n", + " .join(\n", + " with_wf_info.alias(\"history\"),\n", + " [\"workflow_id\", \"run_id\"],\n", + " \"inner\",\n", + " )\n", + " .orderBy(\"workflow_id\", \"run_id\", \"event_id\")\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "9c481879-08c8-458b-b8fa-c5bbe8c3c777", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Summary\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "0343eeed-f30c-4754-8c64-72990a0126b6", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "summary = {\n", + " \"running\": running_workflows.count(),\n", + " \"completed\": completed_workflows.count(),\n", + "}\n", + "\n", + "print(summary)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "b7bbd417-70c7-42fa-a936-8e66db01d9f5", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "event_type = [\n", + " \"EVENT_TYPE_ACTIVITY_TASK_CANCEL_REQUESTED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_COMPLETED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_FAILED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_SCHEDULED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_STARTED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_TIMED_OUT\",\n", + "\n", + " \"EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_COMPLETED\",\n", + " \"EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_STARTED\",\n", + "\n", + " \"EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_SIGNALED\",\n", + "\n", + " \"EVENT_TYPE_MARKER_RECORDED\",\n", + " \n", + " \"EVENT_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED\",\n", + " \"EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED\",\n", + " \n", + " \"EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_FAILED\",\n", + " \"EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED\",\n", + " \n", + " \"EVENT_TYPE_TIMER_CANCELED\",\n", + " \"EVENT_TYPE_TIMER_FIRED\",\n", + " \"EVENT_TYPE_TIMER_STARTED\",\n", + " \n", + " \"EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES\",\n", + " \n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_CANCEL_REQUESTED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_CONTINUED_AS_NEW\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_FAILED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_STARTED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_TERMINATED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT\",\n", + "\n", + " \n", + " \"EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED\",\n", + " \n", + " \"EVENT_TYPE_WORKFLOW_TASK_COMPLETED\",\n", + " \"EVENT_TYPE_WORKFLOW_TASK_FAILED\",\n", + " \"EVENT_TYPE_WORKFLOW_TASK_SCHEDULED\",\n", + " \"EVENT_TYPE_WORKFLOW_TASK_STARTED\",\n", + " \"EVENT_TYPE_WORKFLOW_TASK_TIMED_OUT\",\n", + "]\n", + "\n", + "workflow_execution = [\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_CANCEL_REQUESTED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_CONTINUED_AS_NEW\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_FAILED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_STARTED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_TERMINATED\",\n", + " \"EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT\"\n", + "]\n", + "\n", + "timer_event_type = [\n", + " \"EVENT_TYPE_TIMER_CANCELED\",\n", + " \"EVENT_TYPE_TIMER_FIRED\",\n", + " \"EVENT_TYPE_TIMER_STARTED\",\n", + "]\n", + "\n", + "activity_event_type = [\n", + " \"EVENT_TYPE_ACTIVITY_TASK_CANCEL_REQUESTED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_COMPLETED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_FAILED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_SCHEDULED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_STARTED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_TIMED_OUT\",\n", + "]\n", + "\n", + "child_workflow_event_type = [\n", + " \"EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_COMPLETED\",\n", + " \"EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_STARTED\",\n", + " \"EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_FAILED\",\n", + " \"EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "a7f515c8-92d7-4199-bd53-e86c2f22d8f6", + "showTitle": false, + "title": "" + } + }, + "source": [ + "# Temporal Activities\n", + "\n", + "Derives a table `temporal.activities` from the `temporal.history` table with the following properties:\n", + "- Matches activity input to activity success or failure\n", + "- Decodes activity input / output / error columns from base64 string to JSON encoded string (`input`, `output`, `error` columns)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "a78e316c-b4fc-4de3-bb99-1ef3511ecfbe", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Activity History" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "054925b4-27d7-4515-bee4-1ce9a60becc9", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "activity_history = (\n", + " spark.table(\"temporal.history\")\n", + " .where(col(\"event_type\").isin(activity_event_type))\n", + " .withColumn(\n", + " \"input\",\n", + " col(\"activity_task_scheduled_event_attributes.input.payloads\")[0][\"data\"].cast(\n", + " \"string\"\n", + " ),\n", + " )\n", + " .withColumn(\n", + " \"output\",\n", + " col(\"activity_task_completed_event_attributes.result.payloads\")[0][\"data\"].cast(\n", + " \"string\"\n", + " ),\n", + " )\n", + " .withColumn(\n", + " \"error\",\n", + " # Provides any JSON data that was returned from an activity. There is often richer data like error messages and stacktraces in the raw column\n", + " col(\n", + " \"activity_task_failed_event_attributes.failure.application_failure_info.details.payloads\"\n", + " )[0][\"data\"].cast(\"string\"),\n", + " )\n", + " .select(\n", + " \"workflow_id\",\n", + " \"run_id\",\n", + " \"workflow_type\",\n", + " \"event_time\",\n", + " \"event_type\",\n", + " \"parent_workflow_id\",\n", + " \"parent_workflow_run_id\",\n", + " \"first_execution_run_id\",\n", + " \"prev_execution_run_id\",\n", + " \"temporal_ui_link\",\n", + " \"task_queue\",\n", + " \"event_id\",\n", + " \"workflow_info\",\n", + " \"workflow\",\n", + " \"input\",\n", + " \"output\",\n", + " \"error\",\n", + " \"activity_task_scheduled_event_attributes\",\n", + " \"activity_task_started_event_attributes\",\n", + " \"activity_task_completed_event_attributes\",\n", + " \"activity_task_failed_event_attributes\",\n", + " \"activity_task_timed_out_event_attributes\",\n", + " \"activity_task_cancel_requested_event_attributes\",\n", + " \"activity_task_canceled_event_attributes\",\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "687a6824-2619-4289-8498-63d3babf741b", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Activity Calls" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "d9e48115-2797-4abc-9ddc-75dabf8e9197", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# Temporal only puts the activity's name on the scheduled event \n", + "# Most of this is just linking all other activity events with the activity name\n", + "# As a side effect we end up establishing the identity of an activity invocation, spreading an id\n", + "# globally unique by workflow_id+run_id+activity_scheduled_event_id across one or more rows\n", + "#\n", + "# In the second part we use this to form the activity_calls table. \n", + "# This collapses the different activity events that make up an invocation into one row.\n", + "\n", + "activity_name_event = (\n", + " activity_history\n", + " .withColumn(\n", + " \"activity_type\",\n", + " col(\"activity_task_scheduled_event_attributes.activity_type.name\"),\n", + " )\n", + " .withColumn(\n", + " \"activity_task_queue\",\n", + " coalesce(\n", + " col(\"activity_task_scheduled_event_attributes.task_queue.normal_name\"),\n", + " col(\"activity_task_scheduled_event_attributes.task_queue.name\"),\n", + " ),\n", + " )\n", + " .withColumnRenamed(\"task_queue\",'workflow_task_queue')\n", + " .where(col(\"activity_type\").isNotNull())\n", + " .select(\n", + " \"workflow_id\",\n", + " \"run_id\",\n", + " col(\"event_id\").alias(\"activity_event_id\"),\n", + " \"activity_type\",\n", + " \"activity_task_queue\",\n", + " \"workflow_task_queue\"\n", + " )\n", + ")\n", + "\n", + "with_activity_evt_id = activity_history.withColumn(\n", + " \"activity_event_id\",\n", + " coalesce(\n", + " when(\n", + " col(\"activity_task_scheduled_event_attributes\").isNotNull(),\n", + " col(\"event_id\"),\n", + " ),\n", + " when(\n", + " col(\"activity_task_started_event_attributes\").isNotNull(),\n", + " col(\"activity_task_started_event_attributes.scheduled_event_id\"),\n", + " ),\n", + " when(\n", + " col(\"activity_task_completed_event_attributes\").isNotNull(),\n", + " col(\"activity_task_completed_event_attributes.scheduled_event_id\"),\n", + " ),\n", + " when(\n", + " col(\"activity_task_failed_event_attributes\").isNotNull(),\n", + " col(\"activity_task_failed_event_attributes.scheduled_event_id\"),\n", + " ),\n", + " when(\n", + " col(\"activity_task_timed_out_event_attributes\").isNotNull(),\n", + " col(\"activity_task_timed_out_event_attributes.scheduled_event_id\"),\n", + " ),\n", + " ),\n", + ")\n", + "\n", + "activity_invocation_window = Window.partitionBy(\n", + " \"workflow_id\", \"run_id\", \"activity_event_id\"\n", + ")\n", + "\n", + "activity_calls = (\n", + " with_activity_evt_id.join(\n", + " activity_name_event, [\"workflow_id\", \"run_id\", \"activity_event_id\"]\n", + " )\n", + " .withColumn(\n", + " \"time_scheduled\",\n", + " first(\n", + " when(\n", + " col(\"event_type\") == \"EVENT_TYPE_ACTIVITY_TASK_SCHEDULED\",\n", + " col(\"event_time\"),\n", + " ),\n", + " ignorenulls=True,\n", + " ).over(activity_invocation_window),\n", + " )\n", + " .withColumn(\n", + " \"time_started\",\n", + " first(\n", + " when(\n", + " col(\"event_type\") == \"EVENT_TYPE_ACTIVITY_TASK_STARTED\",\n", + " col(\"event_time\"),\n", + " ),\n", + " ignorenulls=True,\n", + " ).over(activity_invocation_window),\n", + " )\n", + " .withColumn(\n", + " \"time_completed\",\n", + " first(\n", + " when(\n", + " col(\"event_type\").isin(\n", + " [\n", + " \"EVENT_TYPE_ACTIVITY_TASK_COMPLETED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_FAILED\",\n", + " \"EVENT_TYPE_ACTIVITY_TASK_TIMED_OUT\",\n", + " ]\n", + " ),\n", + " col(\"event_time\"),\n", + " ),\n", + " ignorenulls=True,\n", + " ).over(activity_invocation_window),\n", + " )\n", + " .withColumn(\n", + " \"activity_task_started_event_attributes\",\n", + " first(col(\"activity_task_started_event_attributes\"), ignorenulls=True).over(\n", + " activity_invocation_window\n", + " ),\n", + " )\n", + " .withColumn(\n", + " \"activity_task_completed_event_attributes\",\n", + " first(col(\"activity_task_completed_event_attributes\"), ignorenulls=True).over(\n", + " activity_invocation_window\n", + " ),\n", + " )\n", + " .withColumn(\n", + " \"activity_task_failed_event_attributes\",\n", + " first(col(\"activity_task_failed_event_attributes\"), ignorenulls=True).over(\n", + " activity_invocation_window\n", + " ),\n", + " )\n", + " .withColumn(\n", + " \"activity_task_timed_out_event_attributes\",\n", + " first(col(\"activity_task_timed_out_event_attributes\"), ignorenulls=True).over(\n", + " activity_invocation_window\n", + " ),\n", + " )\n", + " .withColumn(\n", + " \"activity_task_cancel_requested_event_attributes\",\n", + " first(\n", + " col(\"activity_task_cancel_requested_event_attributes\"), ignorenulls=True\n", + " ).over(activity_invocation_window),\n", + " )\n", + " .withColumn(\n", + " \"activity_task_canceled_event_attributes\",\n", + " first(col(\"activity_task_canceled_event_attributes\"), ignorenulls=True).over(\n", + " activity_invocation_window\n", + " ),\n", + " )\n", + " .withColumn(\n", + " \"input\",\n", + " first(col(\"input\"), ignorenulls=True).over(activity_invocation_window),\n", + " )\n", + " .withColumn(\n", + " \"output\",\n", + " first(col(\"output\"), ignorenulls=True).over(activity_invocation_window),\n", + " )\n", + " .withColumn(\n", + " \"error\",\n", + " first(col(\"error\"), ignorenulls=True).over(activity_invocation_window),\n", + " )\n", + " .dropDuplicates([\"workflow_id\", \"run_id\", \"activity_event_id\"])\n", + " .select(\n", + " \"workflow_id\",\n", + " \"run_id\",\n", + " \"workflow_type\",\n", + " \"activity_type\",\n", + " \"time_scheduled\",\n", + " \"time_started\",\n", + " \"time_completed\",\n", + " \"input\",\n", + " \"output\",\n", + " \"error\",\n", + " \"parent_workflow_id\",\n", + " \"parent_workflow_run_id\",\n", + " \"first_execution_run_id\",\n", + " \"prev_execution_run_id\",\n", + " \"temporal_ui_link\",\n", + " \"task_queue\",\n", + " \"event_id\",\n", + " \"workflow_info\",\n", + " \"workflow\",\n", + " \"activity_task_scheduled_event_attributes\",\n", + " \"activity_task_started_event_attributes\",\n", + " \"activity_task_completed_event_attributes\",\n", + " \"activity_task_failed_event_attributes\",\n", + " \"activity_task_timed_out_event_attributes\",\n", + " \"activity_task_cancel_requested_event_attributes\",\n", + " \"activity_task_canceled_event_attributes\",\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "3762e053-0bad-4589-b613-77120f5a00be", + "showTitle": false, + "title": "" + } + }, + "source": [ + "### Save Table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "41829520-cc99-4610-9b18-759738eb2e85", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "activity_calls.write.format(\"delta\").mode(\"overwrite\").option(\"overwriteSchema\",True).saveAsTable(\"temporal.activity_calls\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "a07e0a6b-cd70-41f4-9f2f-4b60e80ecd3c", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Activities Tables" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "b442f5d9-b9d8-4f93-ae5c-884614961178", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# TODO. can we use temporal.activity_calls if we partition by activity_type?\n", + "(\n", + " activity_calls.write.format(\"delta\")\n", + " .partitionBy(\"activity_type\")\n", + " .mode(\"overwrite\")\n", + " .saveAsTable(\"tmp_activity_calls_by_activity_type\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "93a09d44-19fe-46ee-ae9a-967c5c06f97d", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# for now we save to activites.{activity_type}\n", + "spark.sql(\"CREATE DATABASE IF NOT EXISTS activities\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "466fa66a-e8de-4580-b994-d3ffd5fd1a90", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# Import generated Diachronic types Python Package\n", + "from diachronic.types.activities import schemas as activity_schemas\n", + "import multiprocessing\n", + "from threading import Thread\n", + "from queue import Queue\n", + "from pyspark.sql.functions import *\n", + "\n", + "by_activity_type = spark.table(\"tmp_activity_calls_by_activity_type\")\n", + "\n", + "\n", + "def run(f, q: Queue):\n", + " while not q.empty():\n", + " activity_type, schema = q.get()\n", + " f(activity_type, schema)\n", + " q.task_done()\n", + "\n", + "\n", + "def fn(activity_type, schema):\n", + " try:\n", + " has_input = True if schema.get(\"input\", None) is not None else False\n", + " has_output = True if schema.get(\"output\", None) is not None else False\n", + " has_error = True if schema.get(\"error\", None) is not None else False\n", + "\n", + " table = (\n", + " by_activity_type.where(col(\"activity_type\") == activity_type)\n", + " .withColumn(\n", + " \"input\", from_json(\"input\", schema[\"input\"]) if has_input else col(\"input\")\n", + " )\n", + " .withColumn(\n", + " \"output\",\n", + " from_json(\"output\", schema[\"output\"]) if has_output else col(\"output\"),\n", + " )\n", + " .withColumn(\n", + " \"error\", from_json(\"error\", schema[\"error\"]) if has_error else col(\"error\")\n", + " )\n", + " )\n", + " \n", + " table.write.format(\"delta\").mode(\"overwrite\").saveAsTable(\n", + " f\"activities.{activity_type}\"\n", + " )\n", + " except Exception as e:\n", + " print(f\"this failed {activity_type}\", e)\n", + "\n", + "\n", + "num_cores = multiprocessing.cpu_count()\n", + "\n", + "q = Queue()\n", + "\n", + "for (activity_type, schema) in activity_schemas.items():\n", + " q.put((activity_type, schema))\n", + "\n", + "for i in range(num_cores):\n", + " # print(\"core\", i)\n", + " t = Thread(target=run, args=(fn, q), name=f\"activity_tables-{i}\")\n", + " # t.daemon = True\n", + " t.start()\n", + "\n", + "q.join()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "d2a48fa4-f249-440c-835b-2bdaf9cc04b3", + "showTitle": false, + "title": "" + } + }, + "source": [ + "# Temporal Signals\n", + "\n", + "Derives table `temporal.signals` from `temporal.history` with the following properties:\n", + "\n", + "- Table contains only \"workflow execution signaled\" // TODO. decide if we want to include child workflow signals or any others temporal encodes\n", + "- Each row has a column with the JSON encoded `signal_payload`\n", + "- Each row has columns for each Embedded Insurance signal type with JSON decoded `signal_payload`. Only one of these is populated per row since it corresponds to the signal type definition. The name of the column for the decoded data is derived from `signal_type` via the function `signal_column_name`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "6ab0f4da-0ef7-457c-9bf2-97499791a692", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "from pyspark.sql.functions import col\n", + "\n", + "with_wf_info = spark.table(\"temporal.history\")\n", + "\n", + "signals = (\n", + " spark.table(\"temporal.history\").where(\n", + " # TODO. we may wish to include other signal types in this batch (child wfs may have special event_type for this, others)\n", + " (col(\"event_type\") == \"EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED\")\n", + " )\n", + " .withColumn(\n", + " \"signal_type\",\n", + " col(\"workflow_execution_signaled_event_attributes.signal_name\"),\n", + " )\n", + " .withColumn(\n", + " \"signal_payload\",\n", + " col(\"workflow_execution_signaled_event_attributes.input.payloads\")[0][\n", + " \"data\"\n", + " ].cast(\"string\"),\n", + " )\n", + " .select(\n", + " \"event_time\",\n", + " \"signal_type\",\n", + " \"signal_payload\",\n", + " \"workflow_info\",\n", + " \"workflow\",\n", + " \"workflow_id\",\n", + " \"workflow_type\",\n", + " \"parent_workflow_id\",\n", + " \"parent_workflow_run_id\",\n", + " \"run_id\",\n", + " \"first_execution_run_id\",\n", + " \"prev_execution_run_id\",\n", + " \"temporal_ui_link\",\n", + " \"task_queue\",\n", + " \"event_id\",\n", + " # \"event_type\",\n", + " # \"workflow_execution_signaled_event_attributes\",\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "828df5d9-6919-46a5-bd2e-4b697e9b87b5", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Save table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "fe86b299-2855-48f1-b242-ea20762c4ef5", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "signals.write.format(\"delta\").mode(\"overwrite\").option(\"mergeSchema\",\"true\").saveAsTable(\"temporal.signals\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "35e737e9-d640-4fbd-8a3b-d432352ab0cd", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Signals Tables\n", + "\n", + "Create 1 table per signal type.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "d78ce128-6e41-471b-b210-f858d4f014db", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "def omit(m: dict, ks: list[str]):\n", + " return {k: v for k, v in m.items() if k not in ks}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "706adeab-e6b1-4b26-bc9c-1e0bc1c3fa72", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "from diachronic.types.signals import schemas\n", + "\n", + "# Helpers: signals\n", + "def signal_sql_name(s: str) -> str:\n", + " \"\"\"Normalizes signal names for usage as Spark SQL tables or columns.\"\"\"\n", + " s2 = s.replace(\".\", \"_\").replace(\"-\",\"_\")\n", + " return s2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "e0bbae66-d322-4596-aab5-dd5bc552e49f", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "generated_tables_map_fully_qualified = {k: f\"signals.{signal_sql_name(k)}\" for k in schemas.keys() }\n", + "\n", + "generated_tables_map_fully_qualified" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "44d817c8-3aa8-4237-8333-afeb22ace94f", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# above should be new temporal.signals (everything is a string) or some other name\n", + "# from which we now derive the following individuated signals tables (schematized, exploded):\n", + "\n", + "from diachronic.types.signals import schemas\n", + "from pyspark.sql import DataFrame\n", + "from pyspark.sql.functions import from_json\n", + "\n", + "\n", + "def create_signals_tables(\n", + " schemas: dict[str, str], table_map: dict[str, str], base_df: DataFrame\n", + ") -> dict[str, DataFrame]:\n", + " return {\n", + " table_name: base_df.where(col(\"signal_type\") == signal_type)\n", + " .withColumn(\n", + " \"payload\", from_json(base_df[\"signal_payload\"], schema=schemas[signal_type])\n", + " )\n", + " .drop(\"signal_type\", \"signal_payload\")\n", + " .select(\"payload.*\", \"*\")\n", + " for signal_type, table_name in table_map.items()\n", + " }\n", + "\n", + "\n", + "signal_tables = create_signals_tables(\n", + " schemas, generated_tables_map_fully_qualified, signals\n", + ")\n", + "\n", + "\n", + "# for name, df in signal_tables.items():\n", + "# display(df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "6bcd543e-8ef7-46a0-bc52-bee0e370a8b4", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "%sql\n", + "create database if not exists signals" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "a8448bea-e474-4dc6-aa9b-168376b278db", + "showTitle": false, + "title": "" + } + }, + "source": [ + "### Save tables" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "3afdb47e-51b9-48c6-809b-c1d226092ff6", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "for table_name, df in signal_tables.items():\n", + " df.write.format(\"delta\").mode(\"overwrite\").option(\"overwriteSchema\", True).saveAsTable(table_name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "15646256-c7c9-4f7d-869a-4131a4c6e5e0", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# import threading\n", + "# for thread in threading.enumerate():\n", + "# print(thread.name)" + ] + } + ], + "metadata": { + "application/vnd.databricks.v1+notebook": { + "dashboards": [], + "language": "python", + "notebookMetadata": {}, + "notebookName": "temporal-process-history_node_cdc", + "widgets": {} + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}