From 7fbaef272a85e4b0876fb9096507c273a0f713f1 Mon Sep 17 00:00:00 2001 From: Jens Scheffler Date: Sun, 19 Apr 2026 15:14:27 +0200 Subject: [PATCH 1/5] Add extended sysinfo for Edge worker --- .../tests/unit/always/test_example_dags.py | 3 +- .../unit/always/test_project_structure.py | 1 + providers/edge3/docs/deployment.rst | 42 ++++++ providers/edge3/docs/img/edge_sysinfo.png | Bin 0 -> 95807 bytes providers/edge3/docs/migrations-ref.rst | 5 +- providers/edge3/provider.yaml | 16 ++ .../airflow/providers/edge3/cli/api_client.py | 2 +- .../edge3/cli/example_extended_sysinfo.py | 63 ++++++++ .../src/airflow/providers/edge3/cli/worker.py | 45 +++++- .../edge3/executors/edge_executor.py | 15 +- .../providers/edge3/get_provider_info.py | 7 + ...3_5_0_replace_individual_counters_with_.py | 62 ++++++++ .../src/airflow/providers/edge3/models/db.py | 1 + .../providers/edge3/models/edge_worker.py | 84 ++++++++--- .../www/src/components/WorkerSysinfoBadge.tsx | 141 ++++++++++++++++++ .../plugins/www/src/pages/WorkerPage.tsx | 24 +-- .../providers/edge3/worker_api/datamodels.py | 3 +- .../providers/edge3/worker_api/routes/jobs.py | 20 ++- .../providers/edge3/worker_api/routes/ui.py | 2 +- .../edge3/worker_api/routes/worker.py | 30 ++-- .../edge3/worker_api/v2-edge-generated.yaml | 10 ++ .../edge3/tests/unit/edge3/cli/test_worker.py | 40 ++++- .../edge3/executors/test_edge_executor.py | 28 ++-- .../edge3/tests/unit/edge3/models/test_db.py | 27 +++- .../unit/edge3/worker_api/routes/test_jobs.py | 14 +- .../edge3/worker_api/routes/test_worker.py | 29 +++- .../metrics/metrics_template.yaml | 6 + 27 files changed, 619 insertions(+), 101 deletions(-) create mode 100644 providers/edge3/docs/img/edge_sysinfo.png create mode 100644 providers/edge3/src/airflow/providers/edge3/cli/example_extended_sysinfo.py create mode 100644 providers/edge3/src/airflow/providers/edge3/migrations/versions/0005_3_5_0_replace_individual_counters_with_.py create mode 100644 providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerSysinfoBadge.tsx diff --git a/airflow-core/tests/unit/always/test_example_dags.py b/airflow-core/tests/unit/always/test_example_dags.py index 995436b73bc42..0b84f6ae26ce5 100644 --- a/airflow-core/tests/unit/always/test_example_dags.py +++ b/airflow-core/tests/unit/always/test_example_dags.py @@ -118,7 +118,8 @@ def example_not_excluded_dags(xfail_db_exception: bool = False): example_dirs = [ "airflow-core/**/example_dags/example_*.py", "tests/system/**/example_*.py", - "providers/**/example_*.py", + "providers/**/tests/system/**/example_*.py", + "providers/**/example_dags/example_*.py", ] default_branch = os.environ.get("DEFAULT_BRANCH", "main") diff --git a/airflow-core/tests/unit/always/test_project_structure.py b/airflow-core/tests/unit/always/test_project_structure.py index 1169de1bd1754..13ef1ee8320b9 100644 --- a/airflow-core/tests/unit/always/test_project_structure.py +++ b/airflow-core/tests/unit/always/test_project_structure.py @@ -107,6 +107,7 @@ def test_providers_modules_should_have_tests(self): "providers/common/compat/tests/unit/common/compat/standard/test_utils.py", "providers/common/messaging/tests/unit/common/messaging/providers/test_base_provider.py", "providers/common/messaging/tests/unit/common/messaging/providers/test_sqs.py", + "providers/edge3/tests/unit/edge3/cli/test_example_extended_sysinfo.py", "providers/edge3/tests/unit/edge3/models/test_edge_job.py", "providers/edge3/tests/unit/edge3/models/test_edge_logs.py", "providers/edge3/tests/unit/edge3/models/test_edge_worker.py", diff --git a/providers/edge3/docs/deployment.rst b/providers/edge3/docs/deployment.rst index c843349c50ed9..d7e5695dacfd7 100644 --- a/providers/edge3/docs/deployment.rst +++ b/providers/edge3/docs/deployment.rst @@ -258,3 +258,45 @@ instance. The commands are: Workers are identified by hostname. See the :doc:`cli-ref` for the full list of arguments. + +Worker Monitoring +----------------- + +The workers send a regular heartbeat to the central site with their status and the status of the tasks running on them. +This information is stored in the database and can be used to monitor the workers and their tasks as well as is displayed +in the web UI. + +Basic information that is provided by the workers includes: + +- Time of being first online +- Time of last heartbeat +- Airflow version +- Edge provider version +- Python version +- Worker start time +- Concurrency (Jobs that the worker can run in parallel) +- Free concurrency (Jobs that the worker can run in parallel but are currently free) + +In order to extend the basic information provided by the worker, you can implement a custom function and register it via the +``[edge]`` section property ``extended_system_info_function`` setting in your Airflow configuration. +The function needs to be implemented in Python asyncio and returns a dictionary with string keys and values that are JSON serializable. +The returned information will be merged with the basic system information and transported to the central site where it is stored +in the database and extend the worker's system information. + +All numeric values (int and float) that are returned by the function will be populated to the telemetry subsystem (StatsD or OTel) +in the form ``edge_worker.{key}``. Note this requires to add the respective keys to the list of monitored metrics in your telemetry +configuration. This allows you to create custom metrics based on the information returned by the function. + +In the returned dictionary there are two special keys that are used to display the health: + +- ``status``: This is a numeric value that is used to determine the health status of the worker. It is expected to be one of the + logging levels defined in the logging module (e.g. logging.INFO, logging.WARNING, logging.ERROR). Based on this value the health + status of the worker is determined and displayed in the web UI. +- ``status_text``: This is a string value that is used to display additional information about the health status of the worker in + the web UI. It can be used to provide more context about the health status of the worker and overrides the default status text. + +See https://github.com/apache/airflow/blob/main/providers/edge3/src/airflow/providers/edge3/cli/example_extended_sysinfo.py +for an example implementation of such a function. + +.. image:: img/edge_sysinfo.png + :alt: Example of the extended system information provided by the worker on the UI plugin diff --git a/providers/edge3/docs/img/edge_sysinfo.png b/providers/edge3/docs/img/edge_sysinfo.png new file mode 100644 index 0000000000000000000000000000000000000000..ac308373922ddd8758dbb30e7655ec0c25636daa GIT binary patch literal 95807 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

0=-{WR{L$YRg8d~lF-rOuVbBmB3_3Njc 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

jk+K=b5Edz9h@?HkK;{>fFkTIc=Sr3$Y(K$&%_t zJh{Q@4dn)VxzIpI#|t6M*qKE;=`lvT6(^LwI&!-4ZkKKQWV(aT-|||5+nU6#>t7Ed zFT8`@gCt>O0Q=;Lb*<*S zR+b^WkDJDIo_zQ35Fa}3@qFwPj)H%+08Ei&&8^n-&JwcxQj;-0kI}(X*H8q-qoaTv zW$bOrM!R8CpT~T|xN_TB6Wue}>!^HlpQU`!8iZz>dIVleEj9ULXULRmX&uKTGAX+G z)WRO0u{1O(964nclo9+3bzNzQkp9q`Z&b91cr-05Eku={00Fv`kZ2sEM|Lz)kH3H~ zjyzetr`#(0ca_Ff2dFqXuSg%a-q%0mFyN;HY-3S&kiqwZFYLj!h2*{ffhGErPN zvfSL4D@;Hj;yKSoGL^uq!F2LPcq~SZK>!Kx`!~9zS)59P)zZ`j9J7H{n?Rf z=~SEZX2F!~q<~tP2sMBHm0Au0L9&vY94O|cw|e39b9^FweFf-)^b zceHUoR1&`rfi$Qwk|UDywZ?~XYfO6cEnRz|#>7nYNgIb7&HMOOX%ArxL&J=nb=?k0 zC?CDisq_Hz)}p~5=CSg9Tgph~#zVu7A+FIk@z-JSkh@bLr8q9f>{!@%!e!W_gV=~7 z0Aq_sTtXs^3II5&ym7gcPMZhbt_$H9zhpLKtMXN)MfU~=e%?I@Wy3vUB06zo9aDl@ zelig>dqL#Se(APl9$Ud2ZkkabBD%=WEL3Ey;IgcK{xf_;N|6Z*I^RnNZ?Dn1;Dw@B z7grYlM;zjrJrS5c%xz?69%ss&O=Tuprb1fmqvUb{Trfbf!i4n-O>Xlh**_0Tx)+oC z$@hV=WuqloM1r&Ks3*kU<&MjdamR&sip_cI+ zaU~S8y7#b-XoG%nS(}`2RshEpz4q2g>A{(!QI+ST7qieiOc!eag_Mq_Wkl0-16jV6gjglzUO~a|aj(8W-40?= zP!Sou!nJL$33Z+jMi5DONeR6K@X3~oo~rUyRBhIX=611Jy(YQ}Cz2HcxoKekn)|p0 zvv7})O(J|$(#j(txul}_iG#Vul0!R9P5DYPM)R43Uo#ox&OM&#z!oU(rKLo%DI6dg z79cbHdustkiwaUcnOZ9C@C3u7)sBAN<*fZ@i6@D(=z7jsCWH+9=HrP{#zSSKzHc{}6t z*ly}rmz22dXuHbmb23m6G10!oc}_6b-J}$i7M%KALFX2e#tCQ@&x6>i)Wg@8&QKdX zJi6>n1Zw{oN-w$e$aJH-XWQS&U1Xtu?lZpTm9PO~Mn1ldF`;m~vc}W=4+7j)+3n{- ztUw?T_DpS}QNau%W@bgTqwR3f>x=1+uf{Fkh`@=^wW0~$y7SxV!iiaVN?8CP-sv`@ z)8zbIknq)GE+d?1iB{fmE}{|#5uks3m(D#??Pi9mr5Q$pkq`!kZFxp{$5~p^7P@@} zsptlsCJh}f(b4WqTe5SS$mdi5M^vD!d4F!Ldk`Mp4>w&ig8!@zI_+{$|1yAq5OLK& zV^Vu#QX~jK-SJL)Q11=we-i~}G}(y-!_AJF0DNNX*xeo4!XC4`lMF*1+Z}Me4Oov| zPN<|RdSrc**o={?RUexqwCT=ASRB5xI&hti1J|cXZ{$#_G(rEIdE&@d{}f@;8(|_r zq#icl^1nEH%c!`Pplvh>0g_^_lBb33uk@5}dQz|L{{f(Su8r*Pu4wm${m7NG2r_qzwCQ{}IGFyuJey`L%T zhl|tGIFx-8L?n2+<~UbBQq_!>WMGwxWb%3=w^9`=gLCueb<>E--OQ3v|2yM-E=^mo5fk`y8BL8oYfRB`Tviat`_~835Wtm5( zvp=b2Vxk$y#~T?AJ-)nnVxF!?L?aw?=WB&dJIR*>D{2C0Ce1*sds)H_^Z`u)-P|_L zDg>v;O-yJ_`T6H1hS^z6(JRhNmU1(wbZ>1c^H=$&m6y=$>peeuHm}WN93$menDb|t_L8VJ*jwX6Dfbd z&EEGv%G;|nUa0IZN&_7R{cV@cVKKwL0_4qG%d8v%0zHc&^7bm1@$`(GPHig!au(W^ zna>`XH$B2q{O7pT%~;$j7AqgQ78+o&tC>SXxfgVGz;HO{J++)n$mR^%acw0-2hK33)= zdPkcb_HpbrkS77x;!C|ez?s(Y*=ujFfyU`u#U;`oaPGQySwTR~{0`oqils>S~x})ADNhBi!o6M8zkyOV3F-D27 zb$;r@ z2X0*Vt+4}wVLb=a$8ZZJr?QHB%CtPfleXfJIC7PWG@j$Bt{R?+DHKd$G9h*)qlm#5 zync4=Q}bdKfxhn=m5q01Kw&Cp`vR|4iR^@jbl8opsv9qcSL079-f0OgSEObzi5~Ln zn+WF>0sCb<@3zI4w8<$|d!!10S$exgVsVl-fV+M)`SO8Gl*Ckg*XFYCbd=DMLbBzw zuTJ$8z4+2Zw>P4}9~Cms&p%DC0pII|SuRPhh0XXd`|nu><$eK%VIRixN2x)?C4CU& zK6sOZd9L5qPLBr$`E9uh0511z$rI93V=-~fdY?~9rcqakR<9XFa)Q`kAMD@98(~c( z9n*c6^mc6W+p|B+Znt>~=gp}a|1ysWrv+;rKcvIRF?hLVwzf%dPh zh^|;9DR+>iA`W&jdAP*v;dH0P@uR6a4$!&tt{d-4wIGud+}0c*qy|%DDOGK$fNFDn zH@d!k1Kr(C#0+c{mIYo$$1Og0(8GI-)qgz@9K(^n!v|zhN-RufMP-RO=s4+Heb|GH z_ag9TFR+tGK1FXRI-kP^s z9fjE^8HOuS{6|f(4pGY@08d0V<{Y5?S>^#)2&{5lWR4X#a=o^fNl3Uue$}_%2Gu$b zO>D-(!5%N?BCpYf`d$Cn4I{&Pjm;eBOs5~uqixTuFS+&&;;$(f*y1wEH152=t99W7 zHHt6dh^1#8wf4CNi}n`@A9@^AS+SC%mXQ+tKJUK2gCqXFHaTv>%P@OTN&V=0_j2#s zJCCJyr>OS~>rL-ipabLpkDJYcOfa8%_}f zo>dA*0=sYqh2x~b(XY0^gYtxLanOSL~H z63`K-rDWl!KCz*KDF;V@qUL?IiW4>m^l3arc1OWSR+-NG8NU?48n0dAQkwUFl%Uz2 z`#u=L&#LdXnblZwXhO)BR zmE>g*MBE@2n|>60DZCm%@8P)GReg>T|J?o-K;U&!Ar>|^LxsirbUPt4_vQ7Od=_;n zvc9|Ry;5JH>^wr?d#yOLnD^k+yMh<{$=!*w6?Z1k8pytkPFfDCqQ0ufJD4E!znyK?YZyER-JbDn_f zvtFl%xd4moPbLUGB)0M=s75*c=4cS*UWh0)qA^ImDl?Np<-fSUZ!060mreutyHEBr zNY(oWX6w)9JSH~{lNW8FWJhCYwJb>vc7MIE9rRqzk^mepuDYEM%D7+;Ko=>|d+YDG z*ddo$fpgYWv~@6HdIXoSEmlvzlb3t1vf{Vu2p6Xez3+NQ=RSr3Ab7|CsME#i1;46R z8Q)R7!4Vag1dQb%n%eO4oK;kERoU9ITs9K;?dqpGaW;zJ2UaU$bY&thwHg;`Rg8cs zD2Z8Sp?v-PV};Rbh77Qwsy)#Qj5J{!REK>ixW1(Wc3%OIvJ{xT`N(W+Ykm_>v)W2w z|HO&Xkl{+W_zFcw!v#T_@&OxiF1ppm)diQq*=kd6N!8CeE0J)QQu*}kDN`j}pL24f zZD4G6yG^{Kl%Oy8>58iUpE&|?HSomd49zQ+Fmg~(AVRuqlu&?*S-liV=O!?JjbYje zAfg(LX>OA6_E$j2EzUY~)j@3Tv*!+40m;rqb=Qr{&=GKjSoPVDw6%wf*lh$mi`Qa~ z+67wla9P<=CQ+^Z@|sudsI*MqXg|HD;7@^2<}Ea`{BBrmc)DcgU)tOW}WYIROU zPxlhesGDtrNfEqED^HzP7>JhvhTj2+!S*mR*LQM{wdxljhxJkp%TF;bf>Nh}#o>Es z7+f)HtzQ&ov~IUF$D?&LqGGsl!c%iuIT5_p+}0T33%M2Alb!1G`nhF5ARFOlYw`*Y z;_ON;`)d8qa*UXL_*75BiDP**Tv@{O$@rWKHxM}?>~23OMa#EsMa z((G;bVFGdWr+6Kj6k7ki3^IN9AM=#%bDMvu=%*;95hpZQO(c z86`lne+Bz^`G)6L`(;aLpw1Y}=T&1lsHX_-$<$PnHJ}z1O3VI|TLmpxZZ~u1t-wEUjQh4yv(qltJsMJ`^H#4Pbdza1py@Sn2=1MY zfsh0PmPrfqK0N3y*tk8S6yVMItvytC6S}K6&8)%7>qfHUQ{$(%@GT!2GG>FW`=93AIy$>A5>;YEbiF7ZWLBVI?8s8$E~k6$js-fE9;m z|L|p4$FZwbZMv`kCCevI6KjfpM{q2%2jhLD_3iqvlplfY1HRkMwSzog-ai}{)+$`3 zD0n`r5S%ffM(HxZz$EhD+DeR=_KXJRNT#+1-Nat410qkEbkk*gt_jgpFxWEq1-3$axE_n?u8Bx z`QI88w7X~7laZCVSG)eLzHWR7q7a}n=J+r=3pR4~MHq-(>;Bc@jsP@p#v9Xqt>wY^ zV;SsJu=t>J-YPJ2+GppAc7dkR{v|D?4=`B-dz)|o)3Cu%H4Q1HfM)Pz?uL?OXQhNWhMm z8N!8-b3TWFJC3VGjk=Pb2SLN9;A(2E5`CD3UHr%2eqt|14{KpQ9<|~ZXEF)0`$=jGCv&eoMTW7lPumj$w+~p-j?ukKkfW} zvKSo2cw-`{I64;lT2cBI7*{QuM?snMaaYWfi4Mc@WOE+qDL~2Z%&(lIj_wCEQ+45I ziw@12elR0M$GGtIuz~DvwM!}LH<0;&Rc-&FrF-&g{Tpo>fu)b(C36VX#B9K#^PTZ1 zFt7#WVn&b6Cm`K?ptr}L@OCo}-+#U?aqwidarp7N5r(bMs2IMfIvMdRrz%gIdJ5(j zkd$f5;blM0j)R2cl+q}UmyubrXw7J0kD#{q1{)qDas$mc3c4M8c%{G|NPCY3eAdSx za-_ZPhkJqdnq7@CU(cbhZs3kqcI8mkzcbFyLdaz$ZSMFziJOCg)lA%MHQ(pt_%J1+ zsl05M6Ka)8800IP-7`-O9N1)ab2S(k7r>W+kX{)SaRkzp?E4S@Dcu zQth8f@Z+%yGjG9QDit>~57!>Z?lQJ%`i)Gs`u4Q%?1I6%hURA4+V9kSbIKc&4tg!$ zMJ+V1Y4;nCC27i6fp~%3)bg_sbxx|wU#IpC79Q)e(z3P$*jyK%X>&|@P$Y0v z#NdtMNtx;1>$VXwH4Re!MXxmfa+{rbFeH@%zvgcd{wgW($lh%^$ndmw1m5o+J3{VG zF^7*4e?S}Uu{G^D)dG!_#5FAo>t@QX4nm)zm_nd6{<=3=IuGTMIl+>7X|=X3id5qz zdvj|(?#FzwV4SN9%kad|a@B%?k^b4%p}qkPdJRQ)`)XF;^Kf^Y)58sW^6Pi4A(zko zG1jTiGB#o2TRZl|iQn~9iLeMKr=mp-q+D(eHjyXq2$S^J08nRsj5Ss5u4atIfK*OR zQ{%JyD-{luX2Q#>m)H#pi~PEJHiJXjRqH$VU->b_N^@;~msT0*O4f=NPqkKm;8B$J zGx_LhtNWv+oTl@G;OHR!z(q8vmQ~Y#>QIIvhZly6^r4HeH7BtPj#4ruJ_hc@;WsBKj64ruzDf_gNRs*(H$0wDKjA zd>5BOaGZMnQOOhaT-s~`%N`$^@|cK`k)a=27J}1*OWG5iKpmtaeuabS>Cb*%`l8xH zc^vH~$mibzUKC0eI%!vmmbar1S_81kx)WjTYOK*bGZmJpMaxaz*>6n0l>MaqSLMyj z9Vb>Ao-eqhJzCvH!I%%*_NCE~JddB|LvNGqa$7J2A5$`p(#F zBiq`j76K=DF}J1uKkEY_#&nd`7mZr)65%Hc10y{l5xJWNrR0xrDJyGg z_ofRZa^&A$9Tj){ZR&|e8NwhC{km-<(rk-;`1!RZYdSQwYpFSepcUhgS6e_}q4w-@ zXrw>zO~#(F)#du{?6RQchDWzg!DXtVh{Ong3O!SBai}M+7M0jN6&-L2xXbIOtR*A_iC5F2IuR*Js*jXb`)+vRfM4=WxB`)C z(KyR;@bO*ChVvpG2DEjIqZR$0(AVbtVA6^vBas39)| zvSj?vgHOhMpt~qqqISPE1A>n<`~05{*3Xnv5uioVWv_QrVh?^G1TN3}@7V_g(1=jK zEdO_L^-}|*ynPh(Y)0JF+RkC*bw)yVGQjBPdi_ATDBAzrbh%UB-XROjnEcGwFp;Tm zQ*cW3x6z*=jQnbdwdJ4EruYA6!KWIIO3T)R(W`W3IblJG#7%S70g4ceIobF-4LlhO zt`y?YPQ(c?b}|pn*_~U*u%~jOapwOfyIXclS}O4O!}8=8=fK?^&wHI(PHj1RMw3Yn zS1)!u$cpmq1v&<NGiX?7_HOf}`+gmug;bh&$Z*SPHK zEuI~?Hqn?xCf5=2-7FZp+2HHj;e&6|;>EiUFTJTZ2eIX^o3By-ZSpjsvY`E$z^Mt9 zko+(M<63Jb9UB|U#8sY^8lM^vq)<@w(3((UIdtcP0r73-qSe`OV)~ZUM6%5wPa6H) z=JI_NdA<(!ad~UQv1(FC#PdI7>`Nk``&QMBNmpxJnWdP!lEMGh&6~`e}N{_ zmK0`xx803?aV~5oW4^7nf2w}+)6U`)PpJ9r{>!a#9!!ff)7hfI>!Utg_`)U0xEk#y zkk9b>2bT)Xj`2_-Ae*&MHboBVhKr{jk(QO`AOx%fE%$UStjrAvbHNN1Wli0C zpDFvcw9I}V2(iOPP)qxMM*+9>b@0>ieacf!wqXjEHn%*D=B!7lbaShzI5b>VNh0t3 zo|TdEk$ynlPkkBmWJ=QM_J3r4li$v2np(N@50~k{QV@EhmF4c0<6Qva*$S1?F-#=o z?v*qA?-yt7 zW%Dc7st?>)KL?u4{u5Pw>B=yR4m|5%*A{k>DM(q#&5<@y=PR#zFcXBPL4fcHc=71) zHQ+Cbifr7EG{t4jJ`8`55A}&9#vn}mKIE*Q+@mFlE~csID1&vP>G|$SjRa`+wH_0c z4XKfzPa9Xo&-7p!Q+M{&_GIsE?bP@X=b~UrSZCBcY ze5;qwpJngXy%i)Q1E?o|`XOe2hR@mYL_OFPNRfBzNwiSZ2~G*EKwhQFg*~h+FJ3ze zxLH2-4^Cs7xp(~0;K2IPH;I*s5g;=G0m-UA495Vq^PutB*vp*LkRaSrpA38vN}V}t zSTXKu9YITTeS3YV)=Z4E9~|svCby_MgP8%cA1%2?@AP}Peu zX>56@_QSW^zVGj7km6>^-eskXcNmQ;JiWK;hmBXz9`^vW=X$kzSBC%CGtzTr7`8X?YGG@2ZxHBxsG+ zfRLJMAaAv0*DwFIxzqy|q-GB6>4tkDokm!1ujJK=(c<4aAuhz#k@-GQUbOge+>n<%U8m=ZX13Sy>QCy+L$uc83v}5#8Gg@!4|ZrAJ~OoX zZppF*L}M-PM5Q?C>3aoTG&0`Vn?zJ#W{*XZnamRmp zFr(JkUEf(~lyrOE+Pyc)n}57J@^Y&yb4KGyY21nYN^tkN_cZaP1KH4_whYpb;m379 zemZ;3TL3=oD$K~sK!WI*KSUP8sp!0d=O@ve1F!q>lmDK#IsK2MiP zWOShE^s3y(@+?mI%{bG<&t2c^KwI}_`x5$nKf4&!&q~D#_{j6CwrO(%YWF5hm@|hj z>Z>yrcSlxy1maRI9*=u|r=B{_!C^I3uC;yq50lYG6;@Mgk`}?zz`5V ziz3!1x3KDgi1dDxthbd}0Ug;L)8WW7;1bI&+o6A<+UPQG@PT2G_1AM|Vcj#(`H+o} zk`aj+DjJLR=*TKk6{mPvXug;`cshe)(z!@slznA7208h`=Lnf4#bsSy_k9{2VuTfwa!lFz0$m!T+S&E=pAK;>1GvjZ7M(te#MFtv*R zLrYdV3d6-7Lof!=LW6U$8DeF^y?s-ujjlUnb@glGazy~}dgSqCM#VBRj5@kFJ^z6k4b zpvGM7myYOoIcPtXKrPVjy=4+ZJP(hf07hYcqaN0B326B-NIE3$Z`kZH**dwz87i>v!mCjh@}&m5 z#9B4XR7HKk0gc7C)Q&U7&T~Lqk<2lxy~fOTG?7bS{N}V_Z)qzON~&FIo$)!l`fL{i zH8!&q*4@?4>e!olg|5#wvwpI(8tzwZJ$5GsUq*RPwo?L>xw1QfGl}Av2xwhoRazV` z6yQF@vNpF^t##2biOSl#T4miISiD`}x9cy7;}(LDuQrua#J73$?V2^pjLHLQt=pFJ zWFDwmt*xSWl~bbHmYnu(?|o~mXUn0ed5cpV^cB$ucOk+;T1)ZSI!C9It}yRyG^Xx{ z<%4Vh4vh(!?xxcrgJJpEEzU;ixzUw0CEiEHB)Y zJw8+|_m1pt$wW#QtD_;~-;SzlgJT@71^_dG{P5Ad8w24(8$EN+j>culz(qEzOC?km zD`y&fOXmBX(S1vTde@~XiF;v#CYnXx_MzCz`l~~1NR1yqc^`$oXg7*PxkJ z!;YWd5zAS&1Wko7DNU!;hP~PDPqmXhSF_D|xCp2AIR!>V{8h-{EmHQc$fL5(SL}JwviCf4J<8%uDED&u*1fNr zQj)qt{y^k>E06lr!f2DAKf~39p4MoAO~h5mZM=#3u4Csb{rlCrxP@i`L7i?p7xK6D zne#)d!@u93qGg*pEHxi>FF-@NDt3odHQ6m&7aV5Rxo2%=Lg%n{!$2c6dL5@Hr2j7W4U$;+E5rSmlJ7Eux<<@-%8ZY< zQ&TQ{S~G83nmI)t_qks+gkVRC(*W?g@h!6q4z@(185VYRjIErURsy!?tCX+^g5QRv zNox-PRrqKDDalS#ZQ^Gasg?@N@(s}!OT_qQ8b15PIG_3UcCX2g1-xbQ)+74rRQzK% zM-8xOC_&P5O=?gjLLh^)k+^uis!f-vO8g>wv^4!1UP;mMX|q0~uTPi;qeiqcJ+&%M z{Fud@wDSaH^?{kcSd1-9$BxO&Mz$xgik7h|5gPqoKDEOa-a&VBlfKe-32OBXIOS>` z{u(16OEBZ<_u5D66ZN}|WaQC@ruAgTg0?+{#+$!$d=qijP}`xiciMbeUSfF=&Wax< zZR-aSKwWi}rOaY;o9=>cmALq`B5Y`p_6f%=c7kgIX!4 zd;uWO-W?8M((l>HR0J(&W=`jKtJP_r<2+iy_JU^}mVTcs-%bwnL>CkxuU)L)XxxT>KKyD#M-^R27qGM?g7sdMbs?6@1X5MZc3 zDif4Z6EF+Ujyu_@tg1}F8bvHPB^9z`bQ9lT7n4p68Q{IHprjNVwX6V9tXCdjmM%2A zVaN>k3H#G{&E)4W@f>N{aXLp93y$K(w`N;ERAb*N8IBC~Gr0%qBi7g|babe&Pj{0^ zYNNx`l-Dvhc8WgkM}^bHrwWgsEzbcWt1R0U&jDF#?;yXwe_-QVKvJ@|wMt0&-VH^@ z0`|fj#>-4{)b8f3yZs?lw-wpt`R=)*Fs&^R`0wjdK?}DJ<7u;o(# z$?%j6o831$hV!S&F3Og2AE$_8Y8AYC<6Rjqqw4Uy_NSro+$}d)#jMykFBJ}BNEfJ2 z?}j4=Qt#3trr11LO44I}!P#NoBd~Rvcpz0w4_)uK{hwlELk7B-=(sE9n=F=3j(gu) zYRT$ZrYXK+xdBkE57V-~-Ew4nC%HNTj70LJTl-SkI}~?UCLvzd+M+b;;cmAUrseGa z$Uw~NE-&_yiPPy4@`ZrH7mH?Up=Zr>q^2BIiLSuH@eUHTl9etOjUG)C9_mxOd^f2@ zfz5`hXyCp4a5c18=cwn7aG^`O(}?C|%XE$)(Q(Q0dtBFy0yhg(or78 z$iH=OHx!VTAjsr1JIf)4YuKdH6CYMZ1_rboPvEf}LTU^IU{W{JS~k+SO|IQ5KcZ?g zAKuXjZJ%5pjuUT|cwr?gGB6FvDRF~pSv?%w{8SM?P-*zEHmu#34E`2;wP7lJ+w;J8 z<6o$7?-Kp8lwk;+4{X&AqD!8sE%K{|%;u>HlQuR0zf99}n4Q`$c)_Ptz4!ZNbtD#? zY%vrKm3;>F(~=w{vdTdT^k^inp~mA->wqF8eL}JsM}9W0zWhe zmw&l0GJJ$twbN1coD!S0{d%=2%F24P>4T#r?$?ubyPwxYdm59B8!rg6%+>I{C#hrn zfXrJV@#KlJBV&vtH6D&i9N#MLIE*8uda#C7LNr*`KmyCtBJ(a>>*TG_Qf5!gp253B z-1zw5hn;1*w>{LFj`+F=2!`xxk;U>&8f09f!6fGAs>}TTH8KFhkofLIP)*z3c<*2` z)?2GkaNFAl&jndT=xNE2fConL`#yMm^~&f1_TV1?-$OZ_-S&rO_g=IfeQ zl*O4dYlSP9EkXv9?3Jvj;e7nsxZ^(z=y+oR!rq^SQut*U(#UjqxOliQF#^3<87ow6 zzkD;v()FxJbfcpGG(gng2H?w6+JUH;oGN0hCmCjSeppuJN z12IOlf3$!NsF=TlCV>DL2YC*-EV9L_S!gc-RVG%`;;JugK4%cb+`_CC#AEtu!hnRG z-$ireWrim{>uV|+-u_U~z2Zb{E4;6fS1=6VZ-vQ;AAWBnAG{hx6Q_(Rv0pYoro@Ke z6Ip6tbis}hX(rbkDQGPehiW^+FSpy-F5iY7Y$fEJ?25^LUxf7e@JNbM0a>!4T zHnJMR)d}>Y5X-%g1oleF_xI(Ec?c?J#qq@6i<-I`{ma>>a3A+ZkF#0pdZb>Ko*p{v z_^!U6o^_gqvQk$=xD3yG_^zgK4~`U>qc&3 z=S-x|^DWFkedK((&jkzq>c{p%)97FQoW&M@nK8Br);BUzGBWaGqCU`+o{B0(9J&|G zLhmU8eU+w8BAD8v``lOH-DkPV#>WsD)}Vv%fzb@7YM5;o&{+w_etHxOI=F-x{#WG* zb}GM@)@MSQ{(WZumgtz6la(m{MztR`Aoi3uM1AJxyZ&KcVW-r1FFeTkOVu?;(3{dj zE@bDinxUMw8UdENhLV-K#qYwkHQRo75YN?i{&9SoPb@7>(G`Q@`HGb|57O^`=>#oo zIQ*x?wJfdinYBhmkBSk@t`A35M$aFH1#jK#ih)2WahHY-K+s{k308!(?mG@19+=$$ zcFK30N8SQNbTBDdRWmj1ik<_a;*R?4i4%>!7{bB3LWFONak9|vWc@heSb^#7J+X{B zI)E=58Q|f=5ENCHYX;!uTkzp=Yr%F?Dwv@bRJ2*>Yl$x^4Xi>D92Jq3t}x!S z{tvxUS)y$BndwoI&wC;iF`8X;5K<*^4{m~IIqyEuZ!E-orkt3>bw6ISapqAJ&O)yYauTLmT&)&^AybL8?nlH%!u zq$zpI%3970VejvV6;rMr1)Pb_@2{^rsPfm?+!`tHScH*x>g3hs)8?_{Ehv z`I#*)JKt*SiVN~x8oe6L>FWyCOJ8FLhaP7?Nl8rj`(-Y6_O%GduflyJ?V;ZPB7S_- z-RwVLyE~jRv9Z_Vc)_o6FnSB}Eei^)TqV-|M)lEg5J#{0Z?&2w|CRl`c+WL7vahf2 zW4Y#P4B{+-+2p_|6A$?8Am(?7C?7Tg-mt$~)Dj=O0^W664t#@HG}B0;-~$ov+dt)} zA0q`uD>|;$yQS@) zKLu2had=F=z5m-beUq#;25qjdiZfyis{;`xMVX8B^-{ff8WC?#ph;mu8d)t%`u6wc zCyco`g?&CUcbiFeJe_D3uPAA^PC9@4vLzcL|Gnem^@sm8rcs=!IPOFS0y(>X{kN)Q z$uz)YIE|eRab|Xc_AB#UtWi>mtBBl>^FU#V7bYFooss>eHt57=0l ztuF3G_Le)Yw!0->?Qwa7wR}qx5ZJ$e%ATtCgt4=MXKB5ZPmDgb#jusP#2iR1h2zRa z{V$y0iL$drpfOI~?sy;l%|JoN2pLI+q`@GCX-tc6wUb#6gL2>Vo*{qADgu8g-omae z;{0w#|6bnou;DQ2@8F`W0*nwp?EJW8{> zW!?4S1mWW?E6wC_>{c9$0Os3@Vt=`;{M)-WyQBuLxvA3DJU$NVdM41#kMNV&S*K$A zXb#*7Jn{T~xzTMMN=g>ix*#e;75dTsA0-jew6hW4ESvNlYt_ZNq|q!)LEkAjQU67@ z;nSND52fPX|q80Vw20Rz{t>(0EJY&YXX!Ii4Z_L_vvLe6Ye_r>?s_aP^rFVT`5kr7zVklTcVSmY-^Yd;d-J!2Bs?V9stENGV3 zi|o3Iy=?xDGW#(-_Tk`!xm4gArM9NfPYxoYS1c_7i3%3|ZN5YMlWni^KVpo3}1J)WI2nMKlIu%3UB`1j7J6s5P z5Ma~fsbN>n?1_WeXHQCA3|4Y`U`~^DtJ%A6u~)!qZBT0q$mVuTt<)}Ss4&7kah+@m zPi8@ldGzs(Mp7#iWsP}Hf^Z;(w(g5%h#FS7Wpg*hbHv4yq}_skMU!<;IaDE#vY3=e zLBzYA#Z`feer1eyf;GjbUnsI!neDesK>il9cD^WCOLKL$9ee*LEF%}&YeWQ1tGKD! zD-qJ>9;3j|&+G2;nH&GG<~Im%H0>52n8ElWPFr)iKte@hlWTTs`HlNv4G=yo4Be|KkZ>g9zYpgh1v_dC;$V!4-%rS}c($9j*@`;cQ-5yB|LH<+47aW(%; z>ubnH#U4)MKf3Oyo}ymv{tXwuK!87m?_m=qz{TP8Df{klWy%|ePm9RlrC)YVWtzo- ztHiMFF3aUbzDr_YDfht+#**hyM*oLsP;hIjw7OeztF(DOkPh!v-DuXl?afgHZ`SS> z0=LUUeQ*Pf88qVZV|virX+BJ;oN1TF5xnRG-Y+!&G0ShhTtMSYK~x%an@#AzKBdP| z8gFx`h+NkKGYJfm&L1Khjq4U!b9@WkcfTk7_So|vu%NU1@=;iy<0J4}r>{^?^Zh{J z!`)bc{a5|sFR?)=7RG?W`!Ys;_jl$`lto$KYS? zBM~?+Pe<0?TikSqqAb_ct(R}-S4U{>?n<;<; z%Ak~Hd>M?7^PeweGn|Y&qfAWx!vc68DzfwxieG4_k3yQmT3bPTU?U$HWT2u_2c1YO zq(PlPV;;i^F`Kq`%dXHhzwD{r%$>K?lO$D~Qun0a?zS?#+KGFhBZP}^L5yuJNYgq{ zaCv}dXm3`{#8ZDyAH@jFD8wE$(dO1-s%#xJkLyPoEH#pncpd>W0>fy&?(^q+5&?WK z7lzXGzgHf3^wCJ&UwJD^78p#E$vB6fXN;3CN7hk}L|^DoW_vN*U!t+O-0Y}LBw4g! zXi3E~}IRYzBEFf&#n;ZVVAm}7tP(~XL-6$yo9iyTWDqboy2(MIJ?rhm@ zrniEnsLhd|Sxylac|Oi;#yObi%%7uN4tZtIN*t4AaJm3DKZ;j9ED;0JA8doKrY^im zJngT}6aQjxR|8&`M}5c^v-{8->P0e`ph*t}zV-c-0{F^7FJ0jj;cLXHht)lBD=B~AVMW$cLRJ*Vtci{!3PzKLeCWcM zT`50_V7bH*_~@qFi0=KXZ`wkAM>eCVEkBIDKEEHE{?2B@Z8_Y|0q+dm>=-4_ULA*R zcYa}W;J3XTmy0|3DsHsea3n;BJ^j8pK0v0_9vxjLoXGoT?)ObL8EK{=^XC(pyHd26 zWz-!Qw7By%w$1|AD&N&+^vg*AItlNhA-LXmA6Q6%M;j!0lT7u?dLmvjY5!lB!H zZ8lxr1Wm@RLyL?s^sOcYqd|9Y9|gU+P6GNuqYiidYG@B8-W!=j7BOOjG|PpJ0y){) zFUGS1A0JAxFyy-U{rvV$Y9Ex8WO_WN#`lJ)hKVRCRk93S9~%ScI9WChx*ruTc+4WC z_vS7S%OAfizl;TYI>WzSJ{QmbCg`m>mG{+-W~;p{)QBVMx1J~dLJBf=`#5Hd5KWE8 zH~N*euz+VCJsz$C!}w?qO%^jNHU4e+K>*f!!NaSvbFI$$i$niCSmxG6cUmMzZJ@c+ zNAa#3)>}#ws}$e^sq4nXP-=uTR6y4=UEpqOKQ8pGpL;-9(LIwCz1jZx_Q*bjXw{s0 zWJv_Pm`!K&9%>aCp`GUez-qZNHu34%sGd`*4cxcAchd`216BmCrY0tUcsVR)t{-d8 z*v11Uqw{T`mxR0d7R-vV>GfqPGB$LfV|VujN_Q_}&nK zVhH_z)Yc#%f<(8ZBUzPhrf{-eI)hhP{*P|J>{?cG?Z-=p0E;>a5clQI(ao00Ms8Wz zwH5)daEn4`M20-63-irk<`Qxp_chmRaD8WjEf0(@^LD(NKN^MlB9kPf074ZtC~ zpO5YLK%w#f9e^I6E-luDYm<27mn)ur%WUD0N4}8A7<+@6gS)^-&bz=L+Cxq?63Sg( zbQHs>sc^AX^A&lV>Cc=lUPO`g53<>;Bn=*X*Jc5n zj~9!BSetHK4hlj7dXzp_)G`Gu5F9!+MsjLl3QDCSe*RaReEIEMv17M+Tt+&lB+K39 zIYxuUx8zAH3@THNb%$U;9PxDCVX8v7nwGp5EN^{!d!keiv)>29wv-IhZq7p)+|ilg zdAyi1VQEb0@V0E#));F#rzHiA$lsOa^R@P4Ve4Ce#Ln^x2Hj!hdb0ue5)S3;qC^VvXS4Uy8X>Lft3OjY3zmRI# z^n9ObP;SUzcCvKoWe3Hz;?Z`}U4`e2C!Cb)tqGQ@h`${cGz~qmxs_|(v1TPW{9wLZ zI6l+^TEXxqj=Nc`){}~wfJ&I$+XR-sO{i(q;vk1R%5C^Oo*%wDOsE@!11@LNx#3ox za8C$Xt*nscUbYK@erHYr5(%pPdtcVr^WAOYWitcxHO;Vwi z;Gx)nh1J&ghA^3Og;1kQiLb1%xOrCdF!8C8f@Lu7qk)S@e@7sprgc$t8g<}dy@@avknyC*d0F->mHAZ4H0Cu6rp>G<{mC$yyL@xyc=O zo!Yc8xvj@k$DC1446B_u`Eq$pINE_6tL5>2x#^I273+jcf85iatS}X8Cc!H^ zuM-LD^q?h**&JZm@k2aouLxjm^V}I{!-ZZjA)yAI$onnuBRu=HlX!{SjXj%#>pMn& zJiaF%ldnA0z<>wztm?In=HdXhIM>G|T<1H#yS}uiHffc!PWo&TxBG|_8+}Yk+xP=G z6Ay<}Uh{H3^C;gKOHhcs=O&L6FGLx zz%ck&5xvgK!O#cc3-iE7M@uWLZRtjgeLS$QcD-Qn+FD*5svAhq?Oo^Z%inK_e8a;S zBIN|Yt^!otb)K%C>eNMg4Gq$%B%^c+76vG>j^wQ)POD-JN_ds-YJF1V9<>I98S1Y5 zsqDxuEA>}X@1$~4alIhz5&gi5iL<*eP&&EebQ)T_ zet3`BCGDNt1s#0g6)|$bU=ELtB%`>0i0U|Kt!(jPQaaj z-AE?O#`=Yj_?KRtk0%;MTSe9S%jX?QXt$E5dd|+>{gHH5fYg?og@bRJona@9IE;9U zjSG#kuW<2yU>sB?4dFt(i=0j)XJ4yyH0+_iuiFJuGhkZ?H@>0RUsVP)XrA3=m>&8l z?WI=de(LjQP)hRm8xPwghrOf3;Sb>LXEZ)3^v6mTPYPu9g2@7YjT);X?+y{9BN8Y{Xch0?U zt=Fsmm^IVWUA?<%*Jpof%ce<#eeb%r{Rd!dQiIz{cx8g8d*TwTUM5sP?I%>3!q_ZG zo`gZi&Sc^++KcR%z#s0-HQv{=DsH3Kxr|DCzvIVawDb3EKLIK2^_9yyc!TI*P6Vmv zqjt|h-LAU751U^VEoihBj+%%*q5te^K}Dz_qfcc5Xpu$TR<)Y4mb9Ps+qn{^!9Eof};d=PlQWTe#K*LE1b?4hXB9{{j7ekC}v3o>ky5@sH?I zK$ee~%r|(*JQ4$+@R=X^r|2DdNyaF`)-{C&ym<|Kd;E-TIy1`(r<;R|>R{8G*e#y$ zwWWT+9cg0HEW~gRK+2=RIE7Nl&*FKgpx{C{S8Sp@vxyy>2yg#2w@I*RhavDv~S`hz>9C-d@jPv0(NsKM?j9G5&7PJzUcku&e(Y z9NLCA-=Wos{95Rd-xx{+MO_?fCU&B`JuKaAHu1LX-%ClHDIg0L8>Ba~dXHl9Hao$@ zQpt9y;s=>P4;m_ClL3{1n6mWD8KJVc|N2&naadF}aB9xvI>r{969ywUk_7+!9wmk} z85d7XwTs*}=KY0Cf`1{jXU4W_(DR%cHqRJ4;UQF5FXMOV_;JX+q_OO)k#%bNngAkY zG}Z|IpQ+3Uv$V>TG9gGSw&&H%J+upYL7wMxX8_L{5V!2yyA}GPUCPuzi@Z-fX*6nZ zcz}(S%`?8gA`d4Ip7-%UJ>@MMHxtCuBle;Ix=^QGr>&B&mF8{kMX~uCSds&CexJ48 z&ed2sYtof^@Mh@Dx13q0@czIxSGb494h*Lx`{Ky4T5bH0QyD&1vk@Bt_zAXLS<{0= zP3319j|7I!F!MZ&HS4AZ=F~HeHpwI0e_eQ7QcHQv82s!W@o}`_Fd)to;valyhfNn9Rabx2c=t)i>YEbINI$S zp8NtzZ&Ndx783H4B%H1uC~_A&iWu>JT?rJ;QdN>G*v&GhuFfCYlqhHhtk%%Ck!@iZf)Sr#Cs-t;QLT^sJ$HzbYN$6 zdkB|^nlG0Tw~rGKF8oOxby=U=*7g&#HSwRbQ})JNyYBe(D`fHEu=GDS3j~>`G^}hK z%A9{tjZVvX7eW)jba5^V1ejazFj<7_>VzdZF{ARX6_jGql9Fmb;xJG-c3MMxr80H@ zH=W4O_%huDVl`y8H+NxSJ_Sm?h-EIv>wWn1rH*WY_hvIT-5C2$kJ@AA=jnzK!1x(s zZoRl)rRKztU|EmVeHtG8BDjYIh`)Y!(b*gXdUF{J*f~C3lo^t;Y%lZ7cT2krfR=`d z0~vTq(zDbjEN>zVBf2kUod^{p(vDs23WH1U&$7qox;qm4$w|Z6xhLmp%YGdKpbwxm zOXI{lay2p;DJ9;gagJsgp&Rt$)tk+wio-qeC zTi4{)^&L{QX+J9uj*zo`@QS7IKbJ_x5%5?Wj)+(b@`{thkgJU-Ww{Sc4Vl6+Lq z<|K#-6aO!Y^W(yD(G8^)nN6=;pwXJb!{~}uuTa#3EyCl-K~;q)8RJ{@IU`tlp6 zw3X57eZJ;>bl$^rNuVPA2O%LLOIen86}yKcMh0Hq^rVI9fyk$>?Y|ZiUAO1eE}Z~p z-Tx3m!GNZO|R^!0f+NYTveK23NBYjDGefQW7d1?A^-hg4Pf0KBj2Kh*W@hqFxYukyX9TL4~WsVf2sz;8UVMUBtJa4`9JK!{Y2f*X3;JzL$H_Yw|TSTYN3mRj|4qY5$O1@ zX0M3!8Fho?3TGY8eAHDgVZD9b#DShk!JaC2Ic1{VlT+Zj%G!Zok;5d-F!$e+##Y;o z?^uE4Q)xW;9Wc2+$(}nkdy2)T7kI;ghzt zO;3F$K^fno&YmN^Mr)caE$^^PqFITI7W(ebYC>H?y>+O{9kKE706S`;b%eUlO>zy? z<_4BiYkarlI~gtjGivb(e3>x)rm>!nl_FIL>hBEzvIU3nTbym(zj^u1AI3S0rYxsq zF_C2zOoFY%K}x{vaVO&$fbF?yAuFp3#jUq*BRJnVf(GsG`D4!bj=q!ABde$we~7DA zC*rxv1DRx(*T|5|@0Igjy_b_hyP$H1;x3@$AOj4O+pJ1J4~@c7YSeM!`9}+naf}O! zAJl#6h=g-HrtaM_rZ?>-;Je&Rnl?;e!_2^hBg?jnE#ROg53@aPZK^V4 z=t64p>3UZW-h?%{x$-1qn7g@|8wk|&*}sLRIfHGENK$^iTZI>5$+10QB?nvo)dfLps9RY44eh(@eGCA593f+Wm)GvXcpmT=4!M0OqdAmtlaQa3 zj&}RtPj5m%4R&a-=&faT7al>y!_2zxKy1o8Pp`?ST$5_9tRW5Sd*kz^IxFKafJ}7U zn&-M*`*cSV@WU?0nn~@w_vq=MXr!1}qt`TlU$O|WM7!0s`r=_u=irAQ$h3BAJ+bM# zn47-O;>QGEy^rI~N^IIhi-1XISGVsDe()!J$Q{A*l2_}&(KmNG@alFA3s8;yNYJ7; z(7F>HIh=Z;6E{~$NUTFJCa-x;D+~{qpQ|)j>+~N?W?Zy!dd(DqT8 zl;Er5O5%E?B3Gqqd=$mVVBCR&i|Zb^jZGPX7C92jU%Pf>GU#P>GJ4BcJ+lmtETQK7 z4BHnPYi4>C|B}0i$jFNO@kz?>a>bunc!K*; z5M4GMU+T}Rp|et>(oAQz)rd2n%LkI`x3k+^dO+c&YVaWY1bz@d`1X$$|Q)Ehacv9cO1)g!;B$TvzA8~ z=^l)iy${UZRH;qm-Y!EX(x!@L6~a(ZYstQ@?YXGDKT#JoM%CHgW7gJGJ+{~0IjtW@ z&54V#Q_affz)hm1-#=fWfLG6MBoY|Eo*jqX`|#sqN7Kj^w>h*^4Qol=PMYZvt663g z5Om9si)myzfh%buUYqQHLVTr2Vy&G?nDB7>`)uLM+|2xP>y+Yalt-82Qz`M_e4e_Z z>g$AU7NmKFgyn~YX}tN`u1g*78AS$e1sOZHKQ%-!MpynDCja7D*~cM@6L{TF-`))t zj^@#HJrAn~e-ge|?=fRi|2Vgj;V)gdtK?XE%<}gAhF~UaH*QC^QO%-WUPEpQissJ} z3g;c6Lj>+#tv^uj;+Xe59$qns1xCL`jzi0y$^z5uyx%0GQZBIKvr?f#sUJ*;p*!XS zEHZtE@d|PKYV}DR@0esDHg0I<625R z;#0sYb{Y%-alL>1iNnhLt+q<{?^xd$X#83X*k#gq=2D`l{wK4mm5SxcQ@>q6#gLR| z%N4S`BP6qLe1UqAl87&1l~hck9>OYCJmC2!L=Lp#)C;NmQ&hrE2R*qVypM-bDR1IN zAB?ZJ6D#SuDhZzuxgQF*Z@myRVoU^s+nt~b)5R(#ngd*M&a^w9@6EnVMK-9^JP^z2 zklr4k-T{5D@(j=ZLAaY+x#oY2PoZObHRzdwN}rINwB-SN__C;(G)B{erxc#dVL`}p z(7un@+uxAW5HwU%)&6M>7F}og34gA!>O3O8|GLci*UuM4@67J$#bB{TGrR<}1R~^Q zSnunrRr7bh4-5?K&z3(f-&~#1>;zvm&c0<-2Rut3o)XeedR&K^jRDE*@@?0dYfr!* z(WY8P(4P`ezy;bS#4W6C3_%IyC>)uvnA|M=A2X{FgjJ327R-vcsdWJ*QT{_E=qeH^#hK>}3xP(gl)RB*qQ-*ys*j&u z>;9k(S4ilSv@^VQO$|tVFFp>?Z1pJOP?grw@|?_*@~y*g`@;uQ#o7+)-(oRCwyeP- z%nL(+I~m9U+Bxu8o?Kv74UhwdY*VQ@PRUpjg9tb+xeWeDo>5u_CxYz1%EQC?;ZOJE zPXURIh!`nRt43UmxL?73f$&47;NuzMlRJf?5f2Z~{!A$$i(3}xck_`JJh{vD>zZdr zsXLVvPAdXF{h`T(@?gD(dqw!Qj-@g=p6}dVq*Ocu{~-vOl^GF!x}x#3ui1}~?wyk$ zQPMcCqY%XwUy+ZRT3RSa>AcP(9UYl{XQ(WFzY!3W5Y(Rn!81CKUHOc#IpEC|?}6n* zw|ao_kWOAfX8BW-%MYl_jJ69$dQa(bv;YrQXb+gtg@+LiH5_TZxW>KaYpV)Lrl#@@ zidCAjt70XW@4qTj6j}>HzU3V}k}zaltW+3D<6GRH*R@KdYbl64!BU6dOSCY6+e3bWYmPT(to0mC{9nv zDJl>4izkSBrP_9WR5>4wC5u>jTzklP_8WvBv~TvV z0_BYnDM@d%jIb|AXpnI8G$S^G5=je>-wp@*lgQ*9z~B`ZB16Nc$B4 z)-gtmr%U(qPPFZW``{@Q88v(&e`NOl-Q#OL?~12~hfqY`4Hw_FaRU}SA8FyJ_e9jqB7?Rlk3}`VuH&lgCBkov z@=sTGJx4giGWNi3TRv&h;nAqg&>Yrz=K`|3xB9kiTuiVSd{ss^iYOyjK1Lleu{8@7 zR9c)!qnnpP>EF}_jzdQgl)2cGGxc#}zSAQ9I3nX`J7Y^I$z!=bbAAUGL=?taec1T# zkC?Wok!w-pWK?WAu}pE|+%@;g0-8>oUCiLK6&BQ(xa2-&$-^%NIEvH(dM0$ZIoqU8YHYX0-?-c@}wlQEWRj3+6^wv#PhekwyBD@ zzLGdv_sm$qilkAZx$;U%Xe=jqH#em|0eWR6ZR5K%Xb5oxuvR+w6yr}M#cy!<$k^AM z`t9d%e?Qh%aE?4pZ$DgR(XvEr7|Ita#!?Nyzo)Ly?3gds^AP%$Z}RwWhY@7oVDncBDX!$Vox{V$;$A*q ztwP=`k?w%*<^eY%>_riK*_^ERO?YSiG1A)~;uy~Y&FeJSx^hM#dHI93XmtojWnB>$ zFoz&(^z@A1{AtfbRlUN7JR9!O=w2gWA9}uLzFKqIgFBMbGCcR-oo40ZhkZ&lhwD$h z*cL$eRf0CS)>pi

5;ol>Tbs0*(-5_nt4-L*v>2(^IsGsv|?V#{94kk+bt-j%$1S z9|HBhippNjdcR|HWHQq+&hicWvQ0*0Z4?&BUj?s_Y~=GS$h?xnsPoM6UHgdm7a6B? zJUd#!7#{QYS#XiyKF=ia28_3zZ|WOJ1(9E5{9O;>_^16bF~RYT9>R}-f1aQCr~c&D zDl>kDINkJA=qNjaBFpo9y7<7Y&d3=_kDmOZ2=4tC$$1K&YM4(y$qE465HvTb@!t+0 z@FN^nBO+{Upd-X#Jx}&GLfgHxtdaB)K`*@Mdl(7BDD@BuSl_`lX@-8B>pw$r%f46$ zMLAhJH3R$WKO?yj6t$mk#}P}wtNND`LD4GLq`%W}96?s-d05r^myN^k;_(YUTLs~z zmVbcIdq)wJ2mqZ3vOS-lR{(eh8;)mbKt$Iv1Zv1%D4O{zeBY=>MNsiRmAI` zYp>7|l+d2PVACulM3BW^dpd>i>6@oRk?g4>Pls&y^9xe?V=BW1{*{uZeH?RRE#v&6 zeJxevXyj4@oXx9i&2`JXq7FjqdAY2v@^DGoF@FT85u_M$O7Zyt9)kFclpYl3S{n zjNX;{$cMzLgH&gmv2i&9^BIM!B^Tm%nE05Nrh?8!9E%1ZzU?n1X8Z4m-*0k#Z*^h$ zKKR*U#N|fBFHrccyUrYYP1&*Mnr%K#xc1KGy4zsy)!rVr6-S5(&?}S!;Kj>>5+pi( zXn0o}_dbEh#oh+N(vq<+s*dl2D&eLpW$sH&$4 zXU<8n%x6oY;#1YLqEaCG;WEFYgsQL7dEC!}kE+>Fc+{P)p<~=%u6Vte7(oXLDfg+g zl>ux^+0v*A@woj{=^hk^LtG5${WU0`P5$hs+>bKwtq(_7za*_w4t&#V{Z=SIb$waw$7~)VHhC;N>!|MgmtCV+BFu)_V4j`sL zL&4E7v>FN$0@o?r^jW`!>#T3jQ;PediKk7$n?_O zR87}+5(^kxu_dy8R)&F2^&ndy`oWCTn84P}VxHQ`0Hh^D31Tdxf=V)%uXQC=Xi(2o z1t;T@TOVxZP1gX8xtV$!nIlcbAH6InR^^F_wd>bvzy$|(c(t5oJGEENJdeuiw-kRJ ztkTbW8`k)139lnNGn!rl=kmg4zOb-B*&n|v_9vDV;?dtk6c;_m#w+Z(XsR+jnlzYqF zZ5vQ(xRmJZ;l%fftsmWal9p`zKrhm46V()i3`lDH4%UCQ(wz|0!Hq$5xg4}3ngg2aCHC7{fG^=di|sI6Zq^Zr|*x3zr+sve|^F15-T z^+OE6HI}vox=4mc)NZ<}#3!XQmZK#kr1Vn%^C8G?kVY&%DoU=RgqjKb`X!*UHvHT_ ze7n(Kl4&l{M}9k4Uaz)-mPaf8BV>`+GR6ucUq#L6@HHb9n+P$W@y;Wdo{mhREfN0b z46=e zsU`SS@?l$5FS2J5_U3+~3hYiRLMxbtI808;$V(=uEK$xGxlRrQwy3++s%1qU zKE3V`-zSqxmfP0#bAq(xgD3bt`s*S(j3RCv_ZqUERC6?~s_}bMi2uqf<+ChK6|ib3 z%#y+Y^%P1df7$hD;+}7s&pbB=>Gfzysj9Fd>mQM3ORnUb~hR?{F{AtknCsZ4YF9%#XB}Plw{XBf`gW0F6?!2}_2>4{sVth_EZu zs~b29dJa>D9_1P381{Ql2wq;v(bumsQJu`QJZ9(~4)+p&VS40XzC4_~>Q`8IwwqmU z($GP4Ew#@6wO{KrRrLeC*`u>FTM(?RIiW1oiCJt_y({?*s5-Yls8CgR*R)t~`gWiG zih&O7$alUQv7^+RbPxkm0|L8yct_J~bXXtOi+_#nnB<9#5_{8Nh8pR(K55Di|Eq!c zWC0=c*`Dew1ulLR0e%$ew*4sb=FzabzyI1}KvX6{6ks7rd$;PR<@6Pj%#jJ55HY?1Oi6h4b~pi${5xYd`Rigy4K@w`7SyV@--*{F%I5`~tPp@h zKREW2yd5zU{*r&B1&x_8c!F#2CV`T$(vck8Nke zARL9tX}Hj?y`I5slRo>~g9hh0=?#t7eEyfkM_i-K#SP`lqwpwjm*YRxqr@aPr7GD> z-ceZmy%eF3ZomHA!<25qO`5c|7XB`Fd&2uU8t?=Xr?JUO49xN#rUePQn1Pm3p>7K= ziyFG`x#LU}{mfLh9idgHQJaHbx{cbLoZX{8!F-g|7$$Gsz9T~W;B&hF8N23EjMZ4q z78&fg-I_oC-47|8KKT+z_#n_RF85O=ZKo!t-Vb>{yvF&&l@O1J=%Cp8D7@kw_RWyUf|OlnN`` zicsl4I$E5r)hRm1XUmiE{QoR`k@?eI>Zw3D#DJ^cH*? zL*Adab~-}~2}bsUR=RtFHPowu_g%3lmm1K1mGzOZ$@L`3=680+ zEp79UOPypZ2fLJ?^er4!Z4R7d4P6|5UCrleIZ7Pb{haP&z$JO(7Z@ngZVTZas?klL zPR4*7tP^4p6a96!hgrfj#`B?yo3H`9zGOD-2h6q1@s1T-PP9_GyQOwG6;QDKJZS9J z_Z^bIdaXBPs3CC`m*Db>g^j* zT{)iWjCEv5F9Q~P6o_UWRT$jufk0%b+j6hA%0Ng}J4Eod&`HAGC9)TvU1lJ1$(|PGf??9sEBWWhr z4%hZ(k@CE0D)sGB2P6h>FZX8jMg|WxRL)hH$~D#I+B%HMB7_1Dj!Y~Sy$?%0yjs;F zM66<<-c{@@*6rjDs}EXEO+5y@ItnbNqXkYle~gE5Uti2stV(#Nc}2-} zW&Dhj7M3h3%tZo=k`Cl2Z=?M!sF(;_{ZUaOLoUn+Jw=D{p6Fx2%Z04bRM)U>M^$W$ zZun6t-?>srOJ@C(;E8MRb^)TGhQ9^!&~~R?O;vzc?gT66FL#5Tree&-j|uNRoS+XX zGxyE=Rmt~x=wd7N4o8#hGzn)5YroM-wLMF&$z5H|BNsKIn#6pYXbUTEjN^qVVBhV7 zwW){>feE!~GX35Tr`;)Lvkr3se1TiI99+*t>`R~~OJO3qy=;^TWX`D`fZ!9fz< zs}H{j*ja9t09*e9!0f^4cipWx5++xfY{_h-2Pm5w7Y>pMrJRt(Nte#seUX-&sM!hR zBu4s$g~^iaNrfT4Qg0Zjl9fo!f7y25RPBT51bwYj0oU6Z3mrAs|A94uN^q=sZlA(~ ze=)=Mu)vnh;eXSPB2ix2jbajPD2LA0Yf-M{I_;J@@3H&3p)JExJx~j`%gO^rGiu}CHpQt4@Lwix^@ueS87koizAC90(PZ1;o`eTl-@13n zw&tjSe6p_9Q3p3LvyBW(u)IF2-g&{)-Yl9}5bm*M{#^6#*ZGJ^tdMQMK#_Il{j@G; z5PW~)ImE@{kJlH_s^z|-nOCfD=GT9H1^BXjJVXi7);v?q4{-l}8VoUm4qjdI9^d?2LQ&fKz zUZ!ke#Xo{l#fIq*-kN+N<-VzUTY0{#^sNH;zS?N~VTwhe$9rXT=n-@`*c#_OO91Xj>$$N;KfCDZm#XoSbk277B|R$@#Wx9ZHc_<*;Isq?Cg zh)$q(x?T?zso3iB80RA6a+~A+n!gT(MlXF+{74l2fbA_8)z-ZAi}9$>0#}z|tOniW zmkX_@(R%@JSH6^iwez} zxnJ$U=xI-Q8~3f3OPPFIl(KTm7Z)JOLH4ljc^|JLq!!R`fI1j zEq&4o>N)-yjxzyo4NSkDy3ekm1Y^BybB1fMZ_44aACMB#?~G2{saB7DoG@Z4d~PMN$agX8Nv)&Kv1c~wL< zdp3RSyx#jt{^9tS&+s1;FOdLKmfQqmMU4lyIXW)-1p{q^>VG3g+qIPV2SsQfg#$;&0R)ZSCkTF@gV$^nb9V4f3JYhz6Wx1<ug#1h@ohBxG3Ojmu>Sk^ZXJ7O@VTmnt0x&(7)O}Y!-*|rE127R2uRwA(%t}b%$->u{#Ny;EZ_Nn zj}})y`~?(yrP-QvbrP4GmCDw9N^`2d)Hy*x4~{_fg5HcSU2>@DqvdNwv@#1Md?v-J zf!y(&oU(hy2PGzFkNSgYTzrCpRGfc`v@A|trR+@WY~ni(lu-&=mZGCXETe~@Mxvhr znrn&~C4fG}?hwGO!9bP$N{4`mh*^P}%(DH4>o=ewa4s8dIS#HWMNPR4hw+&ngO0~` zb&pU|0%ow~J3^c4?AEJZF_x*2+`t-254A%nNVon2{~J4-3ui35bceN(icpo`u{!65 z$kjK#|GWHkn?Q~HXZ0;ZX{~3gPSB=vwFelHfZZ%ZU5=;@{`T5fp5I>THBXvxB$%tr zf}~ks66c~yp2k^V#^t&%;3}=0Qrv)vhu@slynwqrEcZk5Iq1eCptkYi9NR$B{mAwG z2G>$|&P7r0ItF10|4#P_(#<2PnRyv#$zk_J@_G#@>vPXi!$XY0RfV z81ip}Fqp%1Y(iBoy~ZV3xs(8DP)w`kwhJl=;?}eYXZwpor@TT$# zI-7^e)dX#B!B?f~U!g@voFlGp@@& zpMf~wffUdG`PsYEZ&=l-J8b(=DdcbyPC4j2lMl4{nsO_Fh*D3?>cM>0hvdb1&Y5b* z2&vIw>Cs?2Jh|eZ%hVPEgM8>G(wD-datsvsQ(YIm5^NE_ z-l+m5iQphnIBWYE!jSqz+xp0a&u*P6tZ1sW-?j`~+Y${XAE+f0ZF%lqL0=Gg6C2-V zN@>N%N0${9KD5~=OK1otzf17>__X`=yc?$|Ugd{7YYjA#2^-8Nn=qW-L^ z=&NT-*mW9Ow)wM-UusVKj%K&+(ds7S!&?y$OPudBoDKClv7)Xa;4RkiWDpAYaIr9| z0r>8;GEWB^+yNnE67yA76UxslVJs;+FCndWzw7FFqlN0RH69$hGwhOAkjIG&FHQ78fG8dZ&F2b zp&^4z1{-9)UuVC#^Lh7$!1-6c`$M*bAt>n2MzGVra5*7YklDN(tv*ux3Qe<2qnzn~QXvi!@?`i}sp|Oj`QS zj)JgT6Hva18ykU^2>>q7D`Sj$*HwYq^{$Hzy5zBP5&<^X3!ecP^HTB4NEg!_j}amH)m|Jas;G>HFFL&On7dc5RNon z9NsbI(kbK94i#X`T}^-_sCtlTBTcy7Aj7zr<8nw*ZN%PE=4F3k>-Cn$^Yvp!Wr~#S zUgS2B)oHupV7|w{Szk(Cxotr%f)6D{!MMXjc$Zdnu=q8XB(=0Ol;Ew){Ic}!NDDkl zfz;UGv^G=BQgx&iRb_*ZamAqRa4zHzmHJnVIKX7jl8eK(?WDkv7UgfHlCEmx;wAaY z^UP^53-P|%BI`wj9{ffqYj7_Rgd#p^%D6tUT-HO_v@cD$fAz;ky?QMYU`YEO=OANt z*!PA_T$Qq06UC{+*ns8s?G);D(dbS4ezvMS?cpkP0T&$p^1nByOI`A78nv)d17PpW zN7uBTEfKBxmK~uKIo~G$2)`F0{9gatUVOcdibw0=U@D0pFhHoa)zj22o4{Y@zojDK zwN3x)u6Y9WuO||9pWV4HBztE>=i=PUcIUT)1xG;8HN(vH^%3}{I6J|+V4qzfUJk@# z-MpW@S~0eJ{3DPm;e1BrldyAVV~~ghA#RN~t6PlbchCs?8llWpkRx8v^*Aye-i4Oy z02MVKCEsdhSJ+Ilq@EE@#G>cb^dfAq6eUIJjn!;BDnE~A^`|M?1FlPozGBg`}_VRX;qumeJd%X}YGPFx)=Wt59JTk;a0NWWATdd>j zW5jQ}xlrf8=J)(aO#wF!hJVWH1*Cv{=j_p_5nl|aJjdj z_gnjcuXkE&D^9=G?NWYuu|9Y&<37qQ8$A_Pnm0HVznzD80kaG{bl>eR5pMC5pyJy{ z#it+bDCt0GHjX433Gm)U)$;P!VB&2ok9qV#$8r37InS8>x1p@rT)@Z@ajKOo%eFOw z5G=8R8*{XA@Nv;q6~h%BXJPIV7YuL`QT%WA>dW}3>Dc!mr#Gk;_WQ0^R?0*VYE9K3 z&-d>qrO%jv*wFl8(~C;y<%v0WFBN+Og-LW2&l2cpNfiy3%%|PC*@M%eOn8L~A_rO5 zhdMK5zV-j~Fz7y1Ag+P%({l238e|^Nb(UgOOSD>SD>x0uK~)_YGaZ<0(<9QI#63=7 zREGWiG$HEhMu=8$jV1HHp;O}7e@Ca#`$16c5*s@?GaotgLGHcG%k?ir+#^f&lSOoE zjj8ne%X=I!6m?>3t3Q#7JhjGy%UmpClj$T2F67_Df!(H;DLmw^kcn2A$sElQi@;Z3 zYTPa=X*ryf4(ttgpg?>~F;Xq`M%J&l^}p&x28c-@M`^TV`1p~DQ}f>Yo!LwZx!qgB zn%WlLT`t`f!Yv=1^XZab1#&2ST2DeFv|d|VCAiA$sa`@x;eFtfcG2$vJFj<>huF+J zv*fEh`gkz9x?lb-b)o6c5&E9J8CPvDOSQPdflnG2K?b8t=X=nc@fb2hiM*g&?mXoZ zyGv`A@pJq*CXk+MbTW&chB8Q1s2I(JH9J<%I*DVGHTUDoiO_%{J4~85wguFWUJhiD z^E&W^Ihwp7J6Py1{`_Cv8z@M{Hlbws9?=qYyW5&~I%KcDdUi?E`Mf1@*0E5y>kg3L zm~Y+*svqJfR@;^}BaV!-CE;nY|AWwR6d*1RmEy$q;p$hRGjx2da(0=y@{V`T7PBJDIVpDLmNg~S=<4epC; zYFiU$f4T;W7%}p14aZ#+j~0iK%${_SPlXBZC}jadKDY1VePs38MX9xxV&;zG|~+eu%b_iKbrEd+I03B`{_@! zbKr&S1O>@eTV~+GXq$9PXjLkb=~+IDwbn-Q2#TGG_&J8vtovl;0&ldbG`rCS7j%XQ zhWKHFT`l_MwJpddK^qy4Lo0OJo84-Di&4r5NGzo5p?e^3JeZKmE>_Hg^XDz$o17h%P0fy$t2x4k9SWssX29 z&8AMYMD7X2Z@?1i`uUBLt>KjahM0}%9rm*@X(brdw9-)Vrw|sX)*c1SsOQ3TQ5GJW z^i7uuP-*Zn)LSz)QeyJ&DS8rqwefD8*Gz-FBg`^JuxRiv)Y>Y}Bm!f~bAjYuYaX1n zvz-+DxoKQ`@MYePF4g{Vh$F4Pn2K|D9rHv{iU-wFXXia+Npn4J!+Aea_3O&t%SsEH zk5O~IJwoh;^DUQ6FG-cv+An@v8|Ria7OS>fL^ct4FtQEph!uzy zsZT3@k{`2rTs#$^L62D3GLdc5n+1#1PcYWK!d4GYn=WsQClw5FgYGbpeC9nmh?5~N z;nf{jr7XI^rQD^NmBpQTYAT#}7wQ4M4s5<+2|`Dq>+)qp&H7x9xoJtiF!9c{Si9~{ zA0%>{w4O+Q;%82#JBJ&;5Pw~!o9Io-Fw?|j4BJ!NIASUq9X!_rIB8st?xZiZyY(1Z zs6bHax2!@mTh0!|U=$lnZOBNTUdGAi2PX+wm8!zzp?0Z&5i!>~jc*QyqGPRJvxgE{ z<~xeD9VZ&vS7HaYtH!!EM2LP#@$Hdd5^QNsP12{NiK4!e_~5(`CQ;&C13i}XD2-(lih-k!#mFti_DGDJ@{zE0p4oDseR ztYYI}Vr@2-rSCXsxI<0}ckmff`Um49OiNVH1*CH(oO-qrDo>(1()Uxort=&2+|!e$ z!(#&_wlA581|_h2ay9R*miSw^RRpsjZuB4SIE?qg9ddNx8sU3b=0)F;Rv;6(_&?Vx zN^rEoE&UJ*vq{+~&?O~{*eF;9hjDS^gJtmd_OOCC$^5+>I~kkPLBq*oPJ7w#v{FXl z`H*Al>RH21S5#4d^RUsK;=*DH@@{7OF6M4#xUUC!dk5UMz}6mB_tI!Hf1|lZfRK^( zY*-Xwq>JtRBpROjfVW<#hOS<1E}B(+#umm5=bsC-nT2rRFrDnBueQadW%Yz|&RuMM zJrUI+^jHWPJ{Jp&p9$)R=AwDHXU7ADLkQvzl2<%J@bLma{@uy<>ErZ2|C7`1|9T?i z%^)uI>NhxQZu{y14@da+pANn0DRIeB@2>AA;!nbr07sqlPlTl#lSTLA0QTK?WKKfx z4H15pk{oW*np(t)^O+FPLGgP|d9O}Pa&eqmMU@%2hf$l?9SQlcnupUKeZ_@{I1UfC zow>1u&I7K+q2UCtZQgM5u>v^fW*w{3a^=-5Dr?^!HG4WV0N@34-dixQa38N64YwUI z7)tFdNs2aePu}qpBUkbe?GUxVIO$m!uW#nJzFH)Bef>U|xv!bt5*{8J;pS?#AJnfA2 zQYcM*gS}ARw!zm`LhErQcK0!X{$XE$Kt=9N2pJ>&uAZ5$Z2rxo^{kG&uJq(OONgAs zSLm@7$!Mv;!jzl~i4*>X^nFU3>f+>p?5y2y$oZKq>_v~gYmx<~+~K}KdtEj21jk+j zb;01_@_Q+9sGaae{xlss29sSxtk;P|bdMI~a`CVIQQPkNB%C`G!F!FE98ppHxFwJp z#F64QW(z#sCPp()anO7>W(0Xa`a_{j-`??Cd#PW{3e6&g9ek2Aed&8fa2gZ@;@Frw zoQd?dGSHnmO%Rz`j2yD;W^&fkXLrBbtI8h^%}v|ipWy_t^>W+}UJ=6SgC|e^>E8Yv zEHN+T63D!gMmeqX-n?Sw!PZc4yJhC0a% zJ6B{=!bNR-lvEl@DlHzFvrk@MDVZJhi5y~L|3gh6`9Yh_U*E$hIH7kBRzM*U8%{KU1?sk51=bQS{F4qr8y zXeAyYJcLfrTW4m)&cN(;Hy)eK2sl3W$CD>&R*+?5t>XZ*Ngn{IO}$@U_KwGtqN zimqb@3QZb~I3LC|SKP~?YHv3*SP&{IYC*3op6@-iVWMLd__geXjDbeaZtc~EsRLWq z-6j;&0zRU9A_+VtGO9{?pX!!$d^D{OpzO#AXc=bRE$)wPkv+)~$!8XTlaac(#0RA! z$Ax$^(Lo@IPeL;z`Y9oR$UAy5YUqgg5le6>-^v;V4mmN|4%m2Xq^Ui6T(S{fp7d4{ z%_fQHgvj4*1&8xIj5MR=C|szGW_|{WOm}cPOjK8(Ws=hs-A(2G8LMiRWl-|oRJ3(c z)z1oV$K%8-^g*(Y&_(isD{?6NqyC`>taR-ry~Sm_m#L!T#zwh*kug{{sY2)kAq*|d zpcMl}q$1!yXY{Dj665YC;*u8*gP9;f8b<^4~wevGih;(X1_JVz##+q^zWn2q1 z{vLMTOr*6H)~1M5v_i+u>a!*Yk|wfTI9_wmF3nFn_BOAR9}Ke{wtQ6QEOA%70`2c3 zwp?p{ntTR6*po@>bIf|I%{6Sec3hzBVW> zb!wR9|95#lIJd$%k~IfyO-~S^$L!^@?qd&SpSSk$P^c`?-@brG63!IZ1mE>OLy&5^ zC){jS>Iiuk?d{HXx{O>OwC6HDfh>_P7@x#muUm)pdGT9h3e4kcX3W2o1rMVPQ@PsnEFh3B-6wvHhr zjw+PeAA4%yGxEgS&#%Z6#0>=xN35rw;*rOQB9=PhSxFCnEb~%uPPdz0$<%)nRKWFv z@8cPhp}TyBBmSGfU)=On&b0f{7eJry=0Ot{dStpkTOq(iXbe+OG5|M#D&nx*Ww6Nx z8&9DUXkO<*85)0J3-3WD*T>-a3wVUciw>mHa8EVuKK6ABPEpkcRnEYZ?G3HRa|~w_ zgJy1~Zx7-4uGWWf%VU^vP0UACV}pMXDKC`F8a!H-q*M4KG$fbZ0Go24l=%!)bxIN( zEi9*0s9ztWY#?XCf?2JCmmNyMcZqnNKotuzfD&0Qj291tHim={-V)^983NmEFL9x- z!K_874Fig9!EkWYPD=(o6{U1;|4Yq$$(!d#KUlnde4y^drIy|-F{de4pZHK)4h=}! zEo5GojyqfHP+uQ|4IMi%OM!r%L3xey&QTf>sB;DJPJCwOW_STCDIQsCe8(@2sMrPM zh5uIBcFJ4x8M4}V-;8B@#v3+Yi(2Tq*HlPfSQs+J#E4ONyl^p+lCpYXs`U1)wyUOI z=AyDfvb2-K$DV@$Z|4q#PbisR4M$FC{#T<}sLfE_+b3mP?%Ft$j<6WMWGc)%zes*B z%4_k4S_j!*7>0)95(QqUZPkO7!k^2tL=KW18*bvT&{A5BY_9m1ku}>?^9u#n`8p18 zHDvqR_}5IYp^(ITf$LFNo)!wi4Aj~FmDz6lU^cO@?Z6q}vp)Hf##KErq+#{+oiZS!zrsYGxQ}M4lYtyCp0H#xn!a+Tg(62CyY(T&wCA(09TiY zwAIb_qAVFR4h@EjBE_Qg;U3q6Rw<_+sYQ~yY`~=w*jF1Txnlt*IY=@;Et7!rP`IeUXwE{K?w$rz5-iFNjILXj7#+2a&u09JII(*{P)Ry76 zdsLiYM^Rgyz;&l(&K5mEC2zvDADFL+2s3<3V3W=4o#n;_ z*J5;6d*nzKJex{1a8QjGe{+6ulM1(Mp7oYYaUH^di(+B8;Z&1SLmn{??uyW1A-VeW zWBGU(VfgxfgjcXHnTmrRHrtie-JDrJ)aIg-rYp&}J@e(}Pt1`=2sX=+@%&Rd{W4#? z{x1b=NBssV7={Za-n z6o*6QM%9h(3XLU51OBqe8)+C}R7fYp-ahaualpEaNwJfWglBuKcX>r7|9h#cyRMyQ z{)@e?oEI@IRVtw+oLNq1r`#)6+xgsOtdi6PvtA27A)5+x=Y))JpSdffn+CC*cWETxYd8p4 zxNR2%>rv*el`@09&)*eZwV#n4<7>+4pg-^GwaTOWeI?Rvjuf6eZX^&gO>YF^; zK|HgNPD9O;zEQQ5qQ}M%fW(n3dH!`$QT@XqMgd zANGv0zkKQb+$J-pa zPfi*{AR|knzE{HMXL#!f?+&`^3zp_2F@QY3-2R$^mDPFVKCi;dGhPZkP{^kjl02bf z@`1Lt6Vy=}P)=^vZhBez`1t(l^7Puv3AP0zl`Y}872Xo{`}3kjb2;>_J-O8-FgonU zgM;#JThWOUvke4+0_%r` zZ2S(iwR|>eX}tDKCHFiprWK9;FbBKOVJbvmd$yFwPW`;gxo|ZlQfg#rl6xjnFy#(c ziqBlSO3V9jL~+UPk}9h*zHzxoibV>*{S(@y_x;dTV4X|#)$Nt zXZI)$qJr60rHF@lI2}9aEqYl z=cN<^Yi?b^oE^|G*_PLhyIp=&4?_2=h9EcZ?u$In+7oPj?d2^AaeF8Q{uU(y&)(qw_G{l>RR0f^^Y<6}_(dogC>-Pn z+y!1|_8Wd?k>pHTE<;Eha!k9q0{s7_1;7>E7Z|q6>w1b)$zNtpdgO(NpB8&?(iWOx z=OXNDUi97Tq;(*IRM{&E=&tb|W?%2hI@$4_Ti`=uGpS^TdwWCXXVoF4Jg&hR3Q__h zb!cLs+!4zehO94z$+@4tCGBma!BnWfTFXB-Z02hAHz6XyWcF=NIG9A$!O^^3wd>;t zng{2-53MX+B+RXKYHm85nRbzj`d6E0aqIny>{hnoX?w_?cTIijAVhmb0nc4XD#q8vdOTHL z*r^|8fjC%4bK2*>=;EY;l{#E>>IZBQ5vKyq7a06IguOe@S5wKla?UgG@N9`Eh+CI6 zbl2OchmeYP%vm{!bu4400pofE=vuy|ioz=ZKY8l;stHVsd^|gSZ1V1|Vs^oeApd4} zNO@|~b>Dkk{>&8*%CDa}gDco(=Zm98t{@GaL0iPdUpM3+Eo7LbJ;VFgCN`K*Mb&TL zoi4-dD~5z#jk5S=AM6g=RONYO$wq^Ak;T=|de`O`!LiWSl}P_HcZ|M$s02&|J5oV^ z!Yo8J{!>WEQF~VU;x+K(6VDjx(s+Fm+JA&jnHfSau)MkUkW$6Ec4Or6(h&L0Ea{6` zSNjog{TD?y){1uYJ<=xnmZS^59QTUD6vNk?;tNp4V?ZtPRjWb_Zc^e=t8= zoY8~LCb)puNF&X3DleT~L%D3+L`3a{N!$YhysU@Cz;Hdliz`_i=UG5mg)s+8i-8TF zPuA5w!!=Al4he~`Dbehg>;ENX#J6UhJiLr1N{L%s%mjZI)+g&woMC?b)ysM|PYqCb z)-OAL>j802)2JG8m~@398VH}-x=QM#8*@;X4-hvJZycE#D>zz*#M-r-U4^7%(i&NC*&N)Wp^mWN!-0O>mfj~Z6gI8(8t#VAQ+Z@Ak z(xj+sZ~w$7FAZz+P*mWgJV{8F(4!dq%(Hm-@N%i0joLDo!*HRfa_9Af~Hz(9`)TsA;U`BK7@jkq?t_*^7ybG3HU7xpbYat(z@1 z1&yM;^Ho6s`NxliT{gsVaALStm(NAqyy@P9s7v{bd*tkA!<>8dijX}`pSn8f(6h)= z>P*+$J}hH!5ZtsIDnb1X&R{ymyFsqN#;fC|8&xCJsO;}zPIppA;jwa~=M+V+K&aJ_Q3$W%w1$|xA)lbq9Yd+PvNu~((K2h*hxpWSx}apldW1kZCP-67hRuV2Aqr` zYs6<;v)j62Us*+VNC`zKXm((RKC|%0IQ>}Gz6?QH&d%(;V^L$?(GPZXRaXD9dIhPAXD|uC#{yFIxKA6Kk4F@Bzr9nz> z@53I(>FI!v+?QG7r8DOoyrd}oo0f0mB=-_mWHTKoGdn^ofxFXqyGVQ=4j+~serjWF z1!+#LoS7lstXh8cKLV-l1l0p-D}y=H0={i~66A&n(w>7Ud-qqH1h1l@JX;1QQRE2| zR&=E9d73OAZ~M^$Xz^sc?Si#3#Z%m@?n+CRE$ zD_D+|1TTPx1^6h+Xcvv=lD(cs6^L>kIaeQTpz zGe8`)Mj+d`Axn@6BNwX8@FKF}yUCtauk09)Nu-ds=(X@@o}VD*D9gHr>2*R~%>Ur- zp%5C~N7mTqxC!^3=E7s^eI3S0cv&+AQ%CJ>3s6U)e%hu@|3}YqRJK=8+%ad}vwks5 zuZyM4FLQmGJ=>T&?s)V^plZK6l$q=}zSK+c)DWy5BxN+Zu%KWVN|SA_-{b0>Cg^_n zFlj>kLB4)d^7%poofi}zSipZZWO8q|TY}escxhrhp}2^NM6r?aDAU@Q>qI5SJeVEo z!lju+B{4njyLoBPUJk#c0f9<=sdQ<7sV`e}KX#6hv?z<(^V_+cWVf@nX6j=oBpSCd zs1MrNN(Y0HA7udzhyMCW-9!oPGkWV3?rqMRYWBEOSZv;~?#}x~lnuf5mW*h&lWPgj zef0YB>*+q{Lk9YbfoBvs_a2c%W|+IWPBb+a{R=x172rI|5?;B!^DQOeLUkL#3d{&e6TDigc=pAgWv+zr*0u{A`inOmX)>+nrCQx zL_dv>@Dti}9UxKCiav;6ogNb!V<2^ewr{ak>8ZEu2- zz)K>`2tj&#)Kw-m4+ymcOi1zu6S&2O>aLzEoiA+6BlRFT0e$UBwfDtj;-e2bwvY*g9K zB1j)GcKpUrw_qAV2CRkZ_kf3k#Shm1Diic~m@5@9pNPxOk&bJkN8k81rR3|bXlC~Atr1W+t&4^RIMf& zQ(8o~7380pEpkrG)|8PsZ7YzXI?km))G)8LUCuG)>uzpM%Zrj`_;P+$kfKC2s;(Kx znY;oj)Gi}g5E_}7i9Yq2kbi_)hKD2p)R)U!jcGzF(B+-#e4v2b;+*((zYXBtxwQK| zJ`l^Q_Fflhb^A)`qtDV@?QmQXa5np1jry9& z!~zVS@2?(L>a5oy6Z)pubKWWSi(csqCQ=JSevzf3>b-Pd-ol(qgiLbC3Z_XbRz|qp zT+?a9wa{5i{oH;}2-HMYV{rVZR^ZiE7GRG1ZtpAX(QG+#JPGbGqiFc06mBA*=>k z6R>~c(NZU0TRwXa_baOcx2Z_P@^c=8H4+b(YlmjVtk%QhHD?%~olR|u`{~@8P5sps z>y;Io&8I11yLD11J~19X!TH9{a#Ry@%!qy0icxf^IYowXl(%6!zU5}2MV`5@+D~<| zo!C|6MGbfGu-Du%_Q8VCRDStN6LgO4Ob`KZ(4ycqQ~|6ADtfrd{~Fw0#|ak%XQBze zemx`+8d5lOC+U-{>f>+{%?)qn&B+Lzd)z%M4y_ijvb~LG_!M3ypZRfe7Chz_NGeQB zsd5Y&EB0*|C4t=9e5bwv+r1pquT&H_quPxwH(GeX>O&|)^qy`|(7ebiNG5P+*mYP6 zC|dEJ5|H4Wtag6v>S@m^`+PO7osl?(GCah_j1GU~z2al!h3 z&E}?(?jt7A;NJH9bxjLTd1z&&-3@0GQ~El0!UI)}5?%L+d7w{lW=(?+%B?bs_lvAtuj<8-`x)K9#&{DFW{n;An4L@Z0-aS-QJ{&;>)+&QRyG3L4FMpzHom$ zX_otv0CrP3E0Epd-yg<Dh6jfUKt^J+4@AcHBmd2&@}iS;CQ zu%)sY3MrvhL|JunBrO8PR?w#0ZF!fgCbG7J;{CAeHY${B1sop9UD?zW`L9XAm?w=6 z?!q?1B*|>jw73Y}D|#ovs>~1C(Kt`oMz2bs8KI8y&Flpqg|{e5fV!O4D!RI+acERN zw%1ymQqeCb-%t6guAH`Bl|>HPp{0*(jjHQT${Ozy4be#wkZr)e>7t97t${+baPv*p+B)_kgbmWQjP9FD;*l= z1OnM2S-15jx5!*cU0=M&3K`wut~8v1bTwz0$(*(DDxkO4G|j?d5+WPv=ujekzhpEN zLX)09m*F1RPPWEqoJ;;N^7wiM%URk$GD=7)ajd%7Lgi_eO2<#pKnCBHttICeaq0w( zcaKp8r?&em@SHAC?38Z3nAVMR@Rd{O>9HirV0*m0tnDGm6AFvXtBa%lmg-3q{1^qKQZ|XbR5pW4QRv}`*R9H_=lb0+d0$ zT}S+hwKnwZ%Qd_erXpc&v6=<$`dEx=yw#N|y;Ag5g=UoR^T0K%^WQ6j|iyj9kXd<`Z<}bI?n4KsfN#)Z_R{-@9{Kaef^q3oJuE*k^$cC@6DZ zizuE;e~D)}YV}Q)&evWK%?!@$$HgtNFey}Xm|Qz3szPb=b=ks~9~E=p z#T|)kMr|y&-~^Q>sxL94vi&1vY#iR_k?&JV%@>fQ3^#m=VoD)ZO*3gV^PC8s zI74i5B3+mFF7@+@9DRC$0G$=X-6q{B{*_tzP_eM^i&P=~sfQh_X1uR z^rLz8ibu)5dEIL2Xn7yM;h~|c@OyY+QJ-Hagy{H!vp^)VLVzM(*Mvl)ekB&1);&4& zL}NO%8awTxGBCEab3|oEoMvFNadUt+$8o z?y)OpTOF6FSCTpwJ?fQncNV)F*h^V7@&m-i$;z4~6&KQgDDY#NV7bWc$ z2z-v4l$oyc{>Er~K{8gh>-|y>iSLF-Ux&Wx4$68aYRaqK)i7%Q!^$D(*q)L_qF`~W z7HR)_s>~--vUF*+AH1{ZpnY=Pci@UADLtmDvZ5eoS@X-FvEWGP?ckxL;;}b((d<@p z5&q6N3|UktRh+A*w_#CSq`{>90n95#n3Nft>gxG+o;}G)#(~VqIBzA;wOfCkLf{-0 z)=7(?XsLME&z%CXGHDAkM-EZTNJCZ<6$C;e7kakdL59_kRe7|CO@n}!l)c+bnTKTNEc;fuZz8HZv({8o z`|-09kd?lBdV>SaAQOHS^@mT^zR%F&0jg3A0|Gj!;zZSIt=yp zP!d%7%|^GWr`=xEuq-PrLBe$}_@y2krX~o3+ zI;1u0C7!p*)(_P_s6JLi_#^DrW}CtXh-vgYk0QNF4>7{Ro`Bb9s4VveR+LGVM*}(p z#F|LG@f@vmc5D8gNa^=oujpJf>me%{tPrIrw>98L4=>*piWmAx#qq=H)AeFY$Uh{D zXqTjwN`_kkRrwPZlD>cM{NWKRDH=uJdaBr$f|DhCV<=1P_I{x$*SK79-=NjY$7i86 zc;Ei*`q$xIz8oI7bnc>pH-2xLK2^b7aqYQ;hwVGtFK{n!G6u;A>np=3k*KIgj~c7B z5UcUr!ZiB^&qd}M=Q=Kg!#99MVfki3i0VHgd~t!Gqp!=;p! z$su8vlL89wVHw^?SCDPP{^rs&$;FME@FvAIAbP*!OWHkU|08;@Lz7Vs>vB{dxI|=d z5_t1%00HVmfCPsTYL`h=20?fj((ws2G4W*g?p}MA9zKgM&`;mS5=;pCu4^ostt79n zPx%rEA%Rr9F=67%>3d>T54=Ra>H~4)Gi-mf4>EsD7y)lxh)akJ651u%V5|gTG_m+j z3y!rTI%{3t8Qu9%jIR0)-~!%Ed@tll|7PEF7Z2Z`i+682kUOe`^?J-*1i+SNpMV6( zorB$1^u%9ezJIa2bLw#W`1>dny;b4e9$zs2UAT<#KZVPFm$kh8bD<~%=`YPc!L44x z(0Y?dCrrgDAREt=Mj3BR=vr%?N?a@#x_P(S8KJ7e38}no5#abxaUjpGrC&Q!+8ZDY z7Gixlah6Vi^Mqn*;!VYQ0aNLsFC&uxN;efq19?PGSaaDAHO4FY`?uG~j#6uNc|Y^S*IH9Ur1lGYg9~r#Y4yC1>&aFaxx%ZctZ? z6EV!Y)^MOGdm_A<`dcQLcW2`RvAl*L)=9FZ#<}3LIp=2O*5M7ebEkWeK`TMyzI(o$ zShr>Se#t_LhzB?f4d55{z+hAlQYx5t=xEZVe+Bwkem%4;xVY-m6su=9t5Y-#?#J*e zT^b31uj5~{f0c%6rX10>PNwtk=uNdE5@8_f#kzv7N#Cm^vxfuudJU~+RhqM|6^&ab zvut+bA{>yb9~YsyW8Y=o%49X_Rpw*ua*_&YLl}&vf|#_*?B~^kPfA5N!nG#7&O=S3 zDlA-zTxm2~J7&T!+(9*D%du~%zd2bZjF^J5q$<4%_44bw(F4cC1t=SoRQtPZto^=g zltHHYx+C@X)**;PxI8)% z5L!MrCFGx#7?s4hK?&0w~Bt|2IN{@smm0TwoQcOcnq2Y6rm z$E~*u`t6rk6FwX+-QTpPK}wSFMYo`Cy#Pns$S607h&=Jyx1?pNkjt)>m>CBrDIpB3O!wT|oi3v%pQ41&zYDPie{9$2FrlO*0 z78*hjS{2^hL9iOWwz?KFTJudQ{GpGklGE@bdxZs8v}0ZzH5-x6mu+XOB~|bia~Cgp z@)!J&nO-*icDO`C*&P0q@v;%-+Xp~Y2b9YG%Q5&jxzh85+QiPqc_GMPy!4X<;b~zS zKK1!d&=vTN2FI&WnTSZHiq8JL0`o#5l9Hsy<37s9FBur{VY%A)>It~}M{>P-L(nWj z+0@KfVt14GyE^&LYvow1Q&I^@js;W}SektlbW*3jcQ%X*$@l~%#RXr#k!;TFm}zx# ziln#Dl_<=mr)L);=y;m*A51Tt>Rj zn!L7C&T7#!qE_Fwi!9{fc7*mhfVn7jiY;UIIw7twzIviG3C%(I@xptDy=fE7S0e>%^p1D^VN8D z9{nKW@w3oRI&w9zpn8| zz)J8g3!(41x}40n9zC+?!vJ0(wrWd&Ke$8ls@qKr;$^B&Hb?}0d;~!ZTntr5FX*(s zC&ghgbYIPZ3Dj3_&kfmM_`-7IBP#4Q^I!2Dkf4BnU3~N(jR|EY-1C2kOq80KNphwy zevD~5kTsS2g*Rqp;{tiMf0Btj#i1feXVXoz3tKwB_5TwCGH<1nZ}{7s9Gt?(6rBpBCDRUFtw%vh>j3T`G$l?qiCId);r=hRr#i^|Bens>;fjNh0S2KNpCCJmDwxOB~q$Dfb1kj zcb+0xT%uF_f$)3n@eZZ(hOW*rL~t43+Pd!;YONY-UNb^JRQ_Kfi`hPh*Gf5cQ`ERg z$ZGQ*mRE~!d#rPGyJ+;23H$%aeh(#jK0MNQM;DsrG9Frh&Iy4puUhL+ya_@NW0Y&j zDmO2yKYweO3aL-Z=e4SwVOt*`+3M2A)f$G7xVgLmG_U07-f~<_;JF8=IeWEz&ZZ1V z`kS1khL^Qj&5IE$*F!)Yb*Jzca^2V#VVzOAvc=xpNXqni1>}!SE)v4{3)%v#V{gPC zy3Snjlr{(66QkbU+HV;`$PX<}>13-rl!OM@78E>{MGL@gP#D(T)0(Bxn)29dgr3Mh za+$)h;=Af(?}2ePjW^+zEEw;0T?tUmU^z-NI=+l65g6-o{V~fIuru@k&X6|2`SkGr zCj`W>TlL=%&?m0|MX=YyY|uXgp4oS+qt%!Q*b|4nQ%bR+kx98r2``SFrvD0f_*b1C z5okJ)`V6k@e)0*uw-3#RRT^zBo>c-pSj~4)?MWgnS~?$iZG$12Dzox@p1*$NoIx@Z zz(Hnz;UImjzerOQC6#T{I34BfdQN>b(al;#R7L&P0((I&7MG63x#w`#&|dr3B~S1Q z2N*@+Yrj1*@dombz{a|_m9ggBtjd4(V_OvOh|Ld09sS#;Y>?;Z7^_|wY` z*%Dr_6f9yTMIgoHyW{dp8cmTpw9%I;*F1nftdsaj`U$jiAA)^5r)m33k~PP#z-=@|nD{!TymCNm0^rSgnw!s7QC* z5pVi*dfA}q5ZjeK)RVpqtry`SF(I!K(LiofDBC7^w5< zBJCqC!2CrVb98k4Ttrc`@i5BkR-nlef%M!o5_g_JUO+#@#3fPrr{q~prtya2lc^2R z^w7x)`y>Hc^P3bgBZmKp*`$n=#;Ao4Ts3+TT{I1Ro@^;|i>n*PB`>ia4Ye9b7cxgE zRP^2l^$75t<;jLp&S~rs6TNo))@7I`s*DQ=ly0K-@d85W=>!awHI={E&sD!2nixaH zQbz>oNBg_67pQEzmz_-Z((q)_;Syd|)UWm)3cL+8TzCoeS1Y}$MRm6D?D>e|%Hh=zOn=wp*9YXYen3yg<`d$0481m<2swIQ$v zlf12w33D#!@Ip9`x}cbsQ`v%Ez0R5DK3-YpNW&2&Wm0Bpwz~W90OBYTG~Hb zFf&A5S)D<7Z9OG+8i^VFM(rdgyCGtnnM~DG9ODN|WyNDQx@dJKvRzzT00rvcDJyVT ze3A*fcRunc=%vKlw>UYiz=A8X<{jy5Qd3hB%Hb>}E6c#ZE-ni4aq|g#qpGfJNF_x1 z($Gyjp?eXM0kbq3erp%W{i+=UsIwHz_7L^npx$V)TfDiB^ic6?%Q~0qpE$FBkb8?N zSvQkrtynzBxzQTweG?;9s9vD0Z^t@?9Iwtt@I0Daag7(EVaV++b(V+Ifz+tm>1&7)`FtFgfpo{5Pp7G?Nn(&!6HHZCK&whjEM{Y91H& z<5%>W1)gTQot(fdCr3&7fWZd=rPB314TbKK6aOQR6@T*{IYQ%a=XNR~(Iqa}+)x;p zx|(BN)k+{RbY$0XX#ZYseA~MjP3s4zUT2K57`BtC-o39>CihtfG@Qx#_oO_|Of&P0 z(Iv42L{}8iB_u(uP8l$j@Ryy+406m|7Rr<VnmSaXQx!EX)>A^_nu zT2esw0TBD*cl3I5Co@ilTAc6EwS}hbiQ3*sSz8ijQ)!~79hfGF;aZ_A zsP4r$+l$~NnLK=}!}*{^T9Ko~i>qWm7B`^D?%@1IXa7eK!RXZpn191CFl1YE&3TQp z;A7uHLQa1WBx$&fnrc_~613|}66stz@iCt7*4ekZor~yC$koC!LmFV-JNO~-)=1s8 z^H(qU87WzJMgwcF%Hl)9&~>s>1)Sg+u(znh87nL|>7#S-OlpUPko`Hj$3D5OD$H@- za4*Rx^GrxsI2*!&!{9!9$auVQQ`6r&KqFz_?>N_H#yC`#^%ex;{XW{+nLimOz@^Hbo_2;Bqk@DA(`i}aXZTUAnUW2~ZZeE`nlY>z9Fx|3N-+Vz7 z7YcUfEn6*~DFI%dHZoMvgNA7v4d#0C{!Z16C8>s4tuu5bqCgF#zZ@nsU4K$g6R#iq znXj&2VE7Yy2{LT$>1@x&k-;t!i_!ieW4z{^pej)fZ-7``Jh$^srle(*X{dJ`)INI~ zpxXz`In*0FZE4af%ekaDq5n(vH}Lamd@!p*{R$)3Kx)HjIJ!Pnc*tq#1&sW&JebqC zaa*PXYiNE@PP@_aY=3@_O>2!x^dT^;3mcBuDuJINqT*}Z7W|&>)(S|1{1eLHCNT9q z#rNT#k5`!=)jeU`{DOP2F7FZB#7Oo0p)iaY$in^reFC)(moIM=o?6@4(D$XvL@8d6 z{E-zGN41SlhXZg&3wfhoKT>8dTj(;}XTvoka2WngC5dxqouUHR`kOnQMnGGnpMHFg4R^&nvD2%u>BqSl4~t~Jj(?*Z!_qg1gU3!R z-}AVdi5#uVEZnKqdUE{T+NzAE{@H)Y^=;U!JDa=cJRzBFJJ_ELly#xqT`TF7z!Exs z04mkkcD!$AHlI{+wOziLI}5+$N1#Tsb)R#IJ-wZ4Nq!FWQv2dX8V8zT)5)9Jh$99aig! zWb3aG_X|?Vc^RG^o z{`z0!em825CyUCnRrOaWX83O31<;$paD02fmG|o8IcHPXgCx%RWn?fw@mYq1cv-m5 zalCJ=?$E74Om+W!^&P{d<}RcA`gT1xuBF*I35H7X@#&eL<0U}nO*7p=+@;pU+~3O9 zyq3&WWV%t-xBL=t{9^LSBAlyH-W5Kv3)u*R<#CID%H0E~a#lngvXbYU&L!!@7VMFT`1);KU#}0P3jLcG6=)LO)O0~2 z+B5?9F}Zi`Vg6B977HcO|v*W>R5K>XMcBYkYgNLuAM50RKZ zmhx1rLSg+FkvFWK_<$5-WW7V*MCMr5h|^Jh|2s`w-J(1*HRKf^HAes)uFiP9G}L!S zpziZ9HuTp{$MjU{!v(wcLU*m>v?_RBY;%lpWDr{#^$dsG8uEu=X6I(x5(6m?yLGDd zbjoxx0@xAL*M5;WfJF|;7X?NAfKoCodLM^{49FcrE0uwnk9d{arq*_*uC+FMX0bu< zPl@FEuvD5Rp$KuUQ zX>592lMI8pdxBaI7x?-l&f zDRJ@q4{d@0y<-7e!9m_6{zao<1+Esbl*S0ss$KD+;3r|_^7VFg%hD%nMe9L4szR8dTB^Y(MEwHVI*~$gdE>UB z{Sddi&NEuXTGI~rih`B-)r{<^^RVVCD^EK;JX6!zYYP|6*Ml}X=abt1ptQRHTcwm; zLyP7YSU1h>%%^P={rby-;as1otbTMM{dP_i3g7I-*O%Mi#MukN2QuoHjExolufo0q zsHv`78$Vv)RRp9X2r5;n1`wo!fb`y^1*G=~2%&`tf^%a zZ9rUh8~@rb^~j!{o990nyWAR#0;dSy%v&)n)!-qQ3*HqEs9n*wDMyQzDp&+ms`u-@ zHzbwR$KC^3rc<%imwd=DH4eYX8fg^~r|vW{t2{Fb`^^JugpRF$Gf59$EXCCwl@`kK zaozei#$x(2wBqfopZzCBWaUhnnF!Q$;blIcf3W6&#_pJwOMK~?_+&@ zX=&8YScT^a>2nft4`qPZ*IrlQ&dHy z=AWj|83};8!{fwo0G%=mFT_5b5-Lr@u|F!6Bjv4capJe@Iyf_1ikoyq2`Mht%_D>3$DMVJn*C{tqre@>chPYqQ2?b{O;E@Lx| zixFh0m&CO9nxCSV$rr+L0ss32Il7lycVJB$_YJ2Af)fpa{ZTUFeWZ>NfD1j?P16=>^NXFm)F*%4kyR0}ZCh>T^2UwH2djtzi z1DiBkGqrXT@hn$`84*Eux$fOr{wjEP!;`6LXl`40eLT$ZA?;Ta*-TGVsXqNnNwMJ* zd%4WmEQ>u475e4<-WZZtvq8oT-LgfG{zl(Y1CLI`gAqj)X;w`f+!xA3qjz+D>(YHE zjo_p8YXU>Lzh(@uv2^OymG1~!=U2$>f{7I^>`rId+*b7&S=>^8sWY}pn z9OnKhakO5B&kT}6k)SXa^&y1mT^fIJSww1LThn#P9)t=z-dl1WdD|)H5%WMEW{E60 z1Kc8}Wgd^1zdtu2dqn{oMo-s2?S!7MnmYE1P$talNZEOM&MfG9obLP_ZXMw`dy%Z% zzj1hJVT2ALIySAX|AkVvcd$9No$ydnovo0pVWgd-U=rrY+cJdK_uE#UH#~h&2)lUt zr5=TyrbO5ZA$*|+y2_zR_3zR|#S|-i-Mv~P8h|K4vqM*uFyCfhd=)cS+xV~Vdt}Va z5Qa}PNc*0H*^wDyd^|ky=&R41#<{T8NNf9xGXppGVeRan$ibc7kjQO8JNuw|EAoLF z-0DpBP6rA-EA?)XdlY40AoUkgIkvgz2$3eI*An^;#d&QJ#2+NjiqEnHZr`5J;DccXk3!st ztL3eOs&*aK&4M3Dcx*)ff;d1(lIc>p?_tN_8;5&yjxBv-Mxkb6Hh!-Ud@eLX zJ1xCg@8q--{0~THmHcBU7)8sh+fn$<;+Z~mXkg+?>$RCQh3K!~R&4UVNYPm^$MKp~ zim`2b8xx65&iI|qKZ??C@LE1@Y`k3k>Zg=kRnTJqWCA-M(Km;6pt2O@vg{JmROHS6 z6-F2+_p)f#Bu)J!RoM)hx_Jtt^>K08SAV;rB6WOs*FlMuLM2x_pQZCfCmpDh4%PMc zN7q|vxxQ!QZEu~txb$`K-*e_vWVdcKUh;@VyNc7Up^Iy>O}#r(A8fz+>u|-3*9)bU zZ3Q?a?;S#(8Xq4yFPQg`cQejEK(1kYLWDjxw{MsLYtvjcE~-_IiuT$ZyC9&fa&SAF z$kQ*|yU^giB_rBN(*IDJSfw&Xb^%Hm%Y|kJu0VA=x6vm_@ntb-?dBkeshot4#pZ77_PJzQBQaN@k- z9|EQq2(l~>_(tpIHYQTHK}!ojjyHA+z5gbR`=9skH~#fs4M-jSo5v^fC#asrI2jQP zOwoBt@Un;raGV|IrcoNx!?z<0UM}p2!A|OOo2=Oe_C4pb;~$bGEnRKtr9yUoIKn~=A0)qOLKAO|m(rM>0onW?jtVy0%Of>934{aj%ciBLJ|7gI;aA>ZyK{lo zX!n%2;v%OQ88gI8-R7@usj?tM@^9zv&@QLl_$3@BEu!cv+3N%!K+2$-_;U$rIPpNh zm}-tI-V^+8dt=s9n>oxeqYR026YdG-h6AG@jX8royBdztfqWt><|+2huAZV6rKxV7!I*ej=j1 z`5*_XKgGGpR+c2a^n?p6vy()3x4q()!fV4$;^=oYDy zPQLmVWb@$W)kCtMV=?Y`PCYIZ*d1{UDDITGrRJQQ8~$`_cFS!i&mr{CzFUdrnDlik@JLDTtP^y>unW(!E+ZJK zogIwENp#ph?;9a%8by9ghgPd-67 z5_Zy$@1%&vlls>X7z*>I*5-44P3#V1qWIFCOK?WO>^e>|b0*X!MN8o?933jB#X20% z8E^K8Eve)QfA3MUf>$_D)R4}eV=CfTHD3J?hY&z3$(mJ_^Mb3gw z-0lzdy2X#{M{3J6Tf~E-968X6!(W=qH6cY$=uK>FXlw=vwZ3a+KL>%H-kE5u%MGNy zM}B*KH9qPDXkcmhdogOppr{Eq_*Y+s3o_IFi?wSotG%Sa7Q#nv@#h2T)itNVAy`1D z;>vHU-t{oWF6F^4iD!uJE}Vnqg?z_CnKr%(1quXMyTI@x9$ z%N-$;dx;8JLb&Gn@984s0#RB}W_Udal(%iPhnn6oK&PNVAjYy}`5 zOi%A}&@=zwI?fCx@QU39zxk?a1i>>?x~b?Zz&~B7VMcbJywdIp{B5vu2^CbiFomM^v9V4IE| zZKt6UNm*kI1J#aV=L7&RM1~r=R??0ngYKSe=7v+-m`bjWt((G|)HTzj+{jLDnhD%J zS18(AO}OXYW^;DjbaoaQ#xTg^uFvn`u+@RDlJ^&opkQ}e?cgouh8{xw?+cH+uFqnI(2hDTOyHU~ zt4(D&qRhi{CNzTc7JEq4qJD4CW#ne2l%HQv=*fnZhfm5F2Pvx40N@_Gl7#8cV}uFI zTx0CU3Ju6g=x9#4w1I^#ZQnQMyH>m-OJF~o1@-`Pg+HVP{U{@%!q%MW}SkzU0YWFW;H&AI=@>nmBy z#D66xPeyAFkhu$9mP-R1x%Z!y7!K)X2gXu4VyCOfC*zJ^={;Ge)z5dHdN#a>&VXT4t zfQN-Hq6&gd$<-3aqTS-Hf7wbN%qRzUL4a&{;FD)8XR|dxI|d95-$WC zo-)37(o2WkmquGjRWFhL04)E0Lu0>43>dwpYolX`i$|HWQH3zSJgYVecc0>XpmyZdqbuTn@c(8=DS+vwljDaQ&{J_atIk)>~>TE?Uuls}j zqw=K~3>m{U21W$jIA?P~AlxY8PO47Om{B*Se_^~lm%VCP}?;NI605{x9USl_$p$O~nYm)e(Om0-zhN zG{XjXIFXUPXQI;UWLNRh&D&i#J z^_Jm2IeFy)4uVWPVt~{|f*W&=lCkds#^m^6KOKui`^@ z2+u8k2O6Bd04*ZNL=}DpFDcs(Op{G2tdZFK^T$^8mutt5h z|3WSmf^K$m!(0}!_>3aW#Rml(Cq!5FkXR~7-jW_IJv*!MDw$s!KR12MQf6V38V2c% z1wx~RIvv-0n0&5M2%yw7Gf7eoXK_C~{wtk=enQO`v!=(=-lN0kfD5{fa zHPZZC=SOmb5Yzs>g@%@nc`e_czNlG^nRmv8ybdW5I|I6HrFbnh3^6jCH$rD&;{yZ` z<9O3BNfx%=W|uS$V+xR?0j<2UcDS9?+KjhaG|7?6)Es2;;uVdD+iubib&c>Cz(TDWn*K*4DHSiLCxTb!4YCa#3JyU-K_Tv#2mf{n(y?E9z zUdfw}rPtfUB7yXv=D{Dii!oN3_U##Q_;gROKs>H|#pu=4L4Y{)Q z%1gbf3WzF98r~gr=!%b2LDy8A7^i&954v&4?%qV!MbB_7dNs<%{Dc`wKE$s;i|sx3>+omz5nzldOkS!)+!B7|A~;9$zeJr8wCz zHN0KyYDS<(r7Bt4VyxASNz(R~yK0sPn*8rzO;8;^zt7vbtBcmmj`x(({ai^JWmoS% zHqh3l#HL>q4mQ={vu~-(Pu~REf30jt9O|891`671mDLjiFv@=;Jc_L0S&IKZ#Iqys z{+>&H*y?SUxLq$0?_MqQlTxw$oWYVEK6qILJXi86W-pzJN&l zPKKu!y3`>LtjHqj^i{UfTLKCd)wdM>@aVr$sfGi5v582f7Cq2Obd>iC+u1fnC-$4H zaqgR&X9rk-!1q-H)_W`s_~<<+ads>43Yg}H|42KM>_@AHuXqg%4H)r-CQa3enofyH z+ywuCU0y^!{Jl* zK<6IDog17K9L&rv*&P!Qrg*O-@su?;Hz~NTE%c`5;l^|$L6&d9{sE8vzN2$C!a<$q z*7RtJ)-H1LzrLn}PJnRp-TB(&g&MP^$>*_*?7vxWRlPP`)3O9Fzx1HLD-p*)qbI<44sv&;DQgRf!jWPaD2Ri?~@qs zp{>H(!+4t8<+i}CQbdVq|J@3K+Rsm7216N!zMl{iSLcG`x^s>sOkmT3LONY=G_36j z&%f}bJpw0*cj;wm2;XmX{tcS!ANwv(IcWKYoI~A=v#bvxc6*~yL875baFYdGQWs_#)n63 z8ARt~!OX&pC_OY;)4offy1d_m1fDu$3?F0gik@HI#+QC>8$?&~bd+LX#ij zCLYi%eU73dcYR<`)%yfbmmw0=>6*H*s|O{-`?i=^eDX_KzZGO->74XA?xHQ$Q>drJ zDee)yZ4#B5(O0bv0CrKPBtf_5e+rZPEJq=)#mX#0CC_k6+P|(8%ps^FJfE zwpym=0w|5rdU`7%ruzGp&nUwZx|hWvbd-eei#~xm4^mZmX~XzLXiVm-NH6X2rN^v; zZhRw6df$x#uaKd|%CqQw>O1arVX3R$oiF?>2n*VP?O^c0jk50LoAm1^*XssM4}a+p zD_-}`;p3goL{>t8;N!=EFp>Xd)&hP-7oB~T4>2)UI{bCP)6vZ%<~-tCx=N9t^i^xO xq?d0phc*oN+CSx2LcAeYs0JcD&0E)GXF3M~DP}1EDuaioB(E-4`qDh`{{eG>FB<>= literal 0 HcmV?d00001 diff --git a/providers/edge3/docs/migrations-ref.rst b/providers/edge3/docs/migrations-ref.rst index f1b0c08573fd2..7d0846c0029a6 100644 --- a/providers/edge3/docs/migrations-ref.rst +++ b/providers/edge3/docs/migrations-ref.rst @@ -34,7 +34,10 @@ Here's the list of all the Database Migrations that are executed via when you ru +-------------------------+------------------+-----------------+----------------------------------------------------------+ | Revision ID | Revises ID | Edge3 Version | Description | +=========================+==================+=================+==========================================================+ -| ``a09c3ee8e1d3`` (head) | ``8c275b6fbaa8`` | ``3.4.0`` | Add team_name column to edge_job and edge_worker tables. | +| ``c6b3c3d093fd`` (head) | ``a09c3ee8e1d3`` | ``3.5.0`` | Replace individual counters with extended JSON based | +| | | | sysinfo. | ++-------------------------+------------------+-----------------+----------------------------------------------------------+ +| ``a09c3ee8e1d3`` | ``8c275b6fbaa8`` | ``3.4.0`` | Add team_name column to edge_job and edge_worker tables. | +-------------------------+------------------+-----------------+----------------------------------------------------------+ | ``8c275b6fbaa8`` | ``b3c4d5e6f7a8`` | ``3.2.0`` | Fix migration file/ORM inconsistencies. | +-------------------------+------------------+-----------------+----------------------------------------------------------+ diff --git a/providers/edge3/provider.yaml b/providers/edge3/provider.yaml index 1ba8b89067a4c..741f8b14d0317 100644 --- a/providers/edge3/provider.yaml +++ b/providers/edge3/provider.yaml @@ -172,3 +172,19 @@ config: type: string default: ~ example: ~ + extended_system_info_function: + description: | + The function to call to get extended system information for the worker. + + The function must be async and return a ``dict[str, str | int | float | datetime]``. + The information will be sent to the central site with each heartbeat and can be used for monitoring + and debugging purposes. All int and float values will also be published to metric collection systems + like statsd or otel. + + Function must be provided as a string with the full path to the function. See + https://github.com/apache/airflow/blob/main/providers/edge3/src/airflow/providers/edge3/cli/example_extended_sysinfo.py + for an example implementation. + version_added: 3.5.0 + type: string + default: ~ + example: airflow.providers.edge3.cli.example_extended_sysinfo.get_example_extended_sysinfo diff --git a/providers/edge3/src/airflow/providers/edge3/cli/api_client.py b/providers/edge3/src/airflow/providers/edge3/cli/api_client.py index 1fd53245e19ec..b7ce2316bf61d 100644 --- a/providers/edge3/src/airflow/providers/edge3/cli/api_client.py +++ b/providers/edge3/src/airflow/providers/edge3/cli/api_client.py @@ -144,7 +144,7 @@ async def worker_set_state( state: EdgeWorkerState, jobs_active: int, queues: list[str] | None, - sysinfo: dict, + sysinfo: dict[str, str | int | float | datetime], maintenance_comments: str | None = None, team_name: str | None = None, ) -> WorkerSetStateReturn: diff --git a/providers/edge3/src/airflow/providers/edge3/cli/example_extended_sysinfo.py b/providers/edge3/src/airflow/providers/edge3/cli/example_extended_sysinfo.py new file mode 100644 index 0000000000000..1651ebc692e6a --- /dev/null +++ b/providers/edge3/src/airflow/providers/edge3/cli/example_extended_sysinfo.py @@ -0,0 +1,63 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" +Example of an extended sysinfo function that can be used in the Edge Worker. + +To enable this set the airflow config [edge] extended_system_info_function to +airflow.providers.edge3.cli.example_extended_sysinfo.get_example_extended_sysinfo +""" + +from __future__ import annotations + +import asyncio +import logging +import os +import shutil +import sys +from datetime import datetime + +import psutil + + +async def get_example_extended_sysinfo() -> dict[str, str | int | float | datetime]: + """Provide an example extended sysinfo function that can be used in the Edge Worker.""" + disk_usage, cpu_usage, loadavg = await asyncio.gather( + asyncio.to_thread(shutil.disk_usage, "/"), + asyncio.to_thread(psutil.cpu_percent, None), + asyncio.to_thread(os.getloadavg), + ) + disk_free_gb = round(disk_usage.free / (1024**3), 2) + + load_1 = loadavg[0] + + status = logging.INFO + status_text = "I am good, sun is shining 🌞" + if cpu_usage > 95 or disk_free_gb < 5: + status = logging.ERROR + status_text = "Critical condition!" + elif cpu_usage > 70 or disk_free_gb < 20: + status = logging.WARNING + status_text = "Warning condition!" + + return { + "status": status, + "status_text": status_text, + "platform": sys.platform, + "disk_free_gb": disk_free_gb, + "cpu_usage": cpu_usage, + "sys_load": round(load_1, 2), + } diff --git a/providers/edge3/src/airflow/providers/edge3/cli/worker.py b/providers/edge3/src/airflow/providers/edge3/cli/worker.py index 6ab2a9bc80770..eb7cada3ba763 100644 --- a/providers/edge3/src/airflow/providers/edge3/cli/worker.py +++ b/providers/edge3/src/airflow/providers/edge3/cli/worker.py @@ -22,6 +22,7 @@ import sys import traceback from asyncio import Task, create_task, get_running_loop, sleep +from collections.abc import Awaitable, Callable from datetime import datetime from functools import cached_property from http import HTTPStatus @@ -80,6 +81,8 @@ def _edge_hostname() -> str: class EdgeWorker: """Runner instance which executes the Edge Worker.""" + start_time: datetime = datetime.now() + """Startup time of the worker.""" jobs: list[Job] = [] """List of jobs that the worker is running currently.""" drain: bool = False @@ -125,6 +128,20 @@ def __init__( self.push_logs = self.conf.getboolean("edge", "push_logs") self.push_log_chunk_size = self.conf.getint("edge", "push_log_chunk_size") + self.extended_sysinfo: Callable[[], Awaitable[dict[str, str | int | float | datetime]]] | None = None + extended_sysinfo_func_path = self.conf.get("edge", "extended_system_info_function", fallback=None) + if extended_sysinfo_func_path: + module_path, func_name = extended_sysinfo_func_path.rsplit(".", 1) + try: + module = __import__(module_path, fromlist=[func_name]) + self.extended_sysinfo = getattr(module, func_name) + logger.info("Using extended sysinfo function: %s", extended_sysinfo_func_path) + except Exception: + logger.exception( + "Failed to import extended sysinfo function %s, skipping it.", + extended_sysinfo_func_path, + ) + @cached_property def _execution_api_server_url(self) -> str | None: """Get the execution api server url from config or environment.""" @@ -185,14 +202,24 @@ def shutdown_handler(self): os.setpgid(job.process.pid, 0) os.kill(job.process.pid, signal.SIGTERM) - def _get_sysinfo(self) -> dict: + async def _get_sysinfo(self) -> dict[str, str | int | float | datetime]: """Produce the sysinfo from worker to post to central site.""" - return { + sysinfo: dict[str, str | int | float | datetime] = { + "status": logging.INFO, "airflow_version": airflow_version, "edge_provider_version": edge_provider_version, + "python_version": sys.version, + "worker_start_time": self.start_time, "concurrency": self.concurrency, "free_concurrency": self.free_concurrency, } + if self.extended_sysinfo: + try: + sysinfo.update(await self.extended_sysinfo()) + except Exception: + logger.exception("Failed to get extended sysinfo, skipping it.") + + return sysinfo def _get_state(self) -> EdgeWorkerState: """State of the Edge Worker.""" @@ -280,7 +307,11 @@ async def start(self): """Start the execution in a loop until terminated.""" try: await worker_register( - self.hostname, EdgeWorkerState.STARTING, self.queues, self._get_sysinfo(), self.team_name + self.hostname, + EdgeWorkerState.STARTING, + self.queues, + await self._get_sysinfo(), + self.team_name, ) except EdgeWorkerVersionException as e: logger.info("Version mismatch of Edge worker and Core. Shutting down worker.") @@ -309,12 +340,16 @@ async def start(self): logger.info("Quitting worker, signal being offline.") try: + sysinfo = await self._get_sysinfo() + sysinfo["status"] = logging.NOTSET + if "status_text" in sysinfo: + del sysinfo["status_text"] # Remove old status text if exists await worker_set_state( self.hostname, EdgeWorkerState.OFFLINE_MAINTENANCE if self.maintenance_mode else EdgeWorkerState.OFFLINE, 0, self.queues, - self._get_sysinfo(), + sysinfo, team_name=self.team_name, ) except EdgeWorkerVersionException: @@ -408,7 +443,7 @@ async def fetch_and_run_job(self) -> None: async def heartbeat(self, new_maintenance_comments: str | None = None) -> bool: """Report liveness state of worker to central site with stats.""" state = self._get_state() - sysinfo = self._get_sysinfo() + sysinfo = await self._get_sysinfo() worker_state_changed: bool = False try: worker_info = await worker_set_state( diff --git a/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py b/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py index be4ba3f915e29..21a846d0f4457 100644 --- a/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py +++ b/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py @@ -17,6 +17,7 @@ from __future__ import annotations +import logging from collections.abc import Sequence from copy import deepcopy from datetime import datetime, timedelta @@ -153,7 +154,7 @@ def _check_worker_liveness(self, session: Session) -> bool: """Reset worker state if heartbeat timed out.""" changed = False heartbeat_interval: int = self.conf.getint("edge", "heartbeat_interval") - lifeless_workers: Sequence[EdgeWorkerModel] = session.scalars( + lifeless_workers = session.scalars( select(EdgeWorkerModel) .with_for_update(skip_locked=True) .where( @@ -182,6 +183,14 @@ def _check_worker_liveness(self, session: Session) -> bool: ) else EdgeWorkerState.UNKNOWN ) + # Reset presented status + sysinfo = {} + sysinfo.update(worker.sysinfo or {}) # copy needed to have alembic detect change in content + sysinfo["status"] = logging.NOTSET + if "status_text" in sysinfo: + del sysinfo["status_text"] # Remove old status text if exists + worker.sysinfo = sysinfo + self.log.warning("Worker %s is lifeless. Setting state to %s", worker.worker_name, worker.state) reset_metrics(worker.worker_name) return changed @@ -189,7 +198,7 @@ def _check_worker_liveness(self, session: Session) -> bool: def _update_orphaned_jobs(self, session: Session) -> bool: """Update status ob jobs when workers die and don't update anymore.""" heartbeat_interval: int = self.conf.getint("scheduler", "task_instance_heartbeat_timeout") - lifeless_jobs: Sequence[EdgeJobModel] = session.scalars( + lifeless_jobs = session.scalars( select(EdgeJobModel) .with_for_update(skip_locked=True) .where( @@ -231,7 +240,7 @@ def _purge_jobs(self, session: Session) -> bool: purged_marker = False job_success_purge = self.conf.getint("edge", "job_success_purge") job_fail_purge = self.conf.getint("edge", "job_fail_purge") - jobs: Sequence[EdgeJobModel] = session.scalars( + jobs = session.scalars( select(EdgeJobModel) .with_for_update(skip_locked=True) .where( diff --git a/providers/edge3/src/airflow/providers/edge3/get_provider_info.py b/providers/edge3/src/airflow/providers/edge3/get_provider_info.py index 9e4c9a2bf9883..441cee6b65a13 100644 --- a/providers/edge3/src/airflow/providers/edge3/get_provider_info.py +++ b/providers/edge3/src/airflow/providers/edge3/get_provider_info.py @@ -109,6 +109,13 @@ def get_provider_info(): "default": None, "example": None, }, + "extended_system_info_function": { + "description": "The function to call to get extended system information for the worker.\n\nThe function must be async and return a ``dict[str, str | int | float | datetime]``.\nThe information will be sent to the central site with each heartbeat and can be used for monitoring\nand debugging purposes. All int and float values will also be published to metric collection systems\nlike statsd or otel.\n\nFunction must be provided as a string with the full path to the function. See\nhttps://github.com/apache/airflow/blob/main/providers/edge3/src/airflow/providers/edge3/cli/example_extended_sysinfo.py\nfor an example implementation.\n", + "version_added": "3.5.0", + "type": "string", + "default": None, + "example": "airflow.providers.edge3.cli.example_extended_sysinfo.get_example_extended_sysinfo", + }, }, } }, diff --git a/providers/edge3/src/airflow/providers/edge3/migrations/versions/0005_3_5_0_replace_individual_counters_with_.py b/providers/edge3/src/airflow/providers/edge3/migrations/versions/0005_3_5_0_replace_individual_counters_with_.py new file mode 100644 index 0000000000000..10eeef9efeaa2 --- /dev/null +++ b/providers/edge3/src/airflow/providers/edge3/migrations/versions/0005_3_5_0_replace_individual_counters_with_.py @@ -0,0 +1,62 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Replace individual counters with extended JSON based sysinfo. + +Revision ID: c6b3c3d093fd +Revises: a09c3ee8e1d3 +Create Date: 2026-04-15 21:57:07.662359 +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c6b3c3d093fd" +down_revision = "a09c3ee8e1d3" +branch_labels = None +depends_on = None +edge3_version = "3.5.0" + + +def upgrade() -> None: + with op.batch_alter_table("edge_worker", schema=None) as batch_op: + # Can not easuly convert old sysinfo string to new JSON structure, just clear and re-populate by workers on next heartbeat + batch_op.drop_column("sysinfo") + batch_op.add_column(sa.Column("sysinfo", sa.JSON(), nullable=True)) + batch_op.drop_column("jobs_failed") + batch_op.drop_column("jobs_taken") + batch_op.drop_column("jobs_success") + + +def downgrade() -> None: + with op.batch_alter_table("edge_worker", schema=None) as batch_op: + batch_op.add_column( + sa.Column("jobs_success", sa.INTEGER(), autoincrement=False, default=0, nullable=False) + ) + batch_op.add_column( + sa.Column("jobs_taken", sa.INTEGER(), autoincrement=False, default=0, nullable=False) + ) + batch_op.add_column( + sa.Column("jobs_failed", sa.INTEGER(), autoincrement=False, default=0, nullable=False) + ) + batch_op.drop_column("sysinfo") + batch_op.add_column(sa.Column("sysinfo", sa.VARCHAR(length=256), nullable=True)) diff --git a/providers/edge3/src/airflow/providers/edge3/models/db.py b/providers/edge3/src/airflow/providers/edge3/models/db.py index f564b1d117b93..a1cd1d42ee1c4 100644 --- a/providers/edge3/src/airflow/providers/edge3/models/db.py +++ b/providers/edge3/src/airflow/providers/edge3/models/db.py @@ -46,6 +46,7 @@ def _callable_accepts_use_migration_files(callable_: Any) -> bool: "3.0.0": "9d34dfc2de06", "3.2.0": "8c275b6fbaa8", "3.4.0": "a09c3ee8e1d3", + "3.5.0": "c6b3c3d093fd", } diff --git a/providers/edge3/src/airflow/providers/edge3/models/edge_worker.py b/providers/edge3/src/airflow/providers/edge3/models/edge_worker.py index cf72300681c2a..6ad02dff3b2e2 100644 --- a/providers/edge3/src/airflow/providers/edge3/models/edge_worker.py +++ b/providers/edge3/src/airflow/providers/edge3/models/edge_worker.py @@ -17,23 +17,18 @@ from __future__ import annotations import ast -import json import logging from datetime import datetime from enum import Enum from typing import TYPE_CHECKING -from sqlalchemy import Integer, String, delete, select +from sqlalchemy import JSON, Integer, String, delete, select from sqlalchemy.orm import Mapped -from airflow.providers.common.compat.sdk import AirflowException, Stats, timezone -from airflow.providers.edge3.models.edge_base import Base - -try: - from airflow.sdk.observability.stats import DualStatsManager -except ImportError: - DualStatsManager = None # type: ignore[assignment,misc] # Airflow < 3.2 compat +from airflow.providers.common.compat.sdk import AirflowException, timezone from airflow.providers.common.compat.sqlalchemy.orm import mapped_column +from airflow.providers.edge3.models.edge_base import Base +from airflow.providers.edge3.version_compat import AIRFLOW_V_3_2_PLUS from airflow.utils.log.logging_mixin import LoggingMixin from airflow.utils.providers_configuration_loader import providers_configuration_loaded from airflow.utils.session import NEW_SESSION, provide_session @@ -99,10 +94,7 @@ class EdgeWorkerModel(Base, LoggingMixin): first_online: Mapped[datetime | None] = mapped_column(UtcDateTime) last_update: Mapped[datetime | None] = mapped_column(UtcDateTime) jobs_active: Mapped[int] = mapped_column(Integer, default=0) - jobs_taken: Mapped[int] = mapped_column(Integer, default=0) - jobs_success: Mapped[int] = mapped_column(Integer, default=0) - jobs_failed: Mapped[int] = mapped_column(Integer, default=0) - sysinfo: Mapped[str | None] = mapped_column(String(256)) + sysinfo: Mapped[dict | None] = mapped_column(JSON, nullable=True) team_name: Mapped[str | None] = mapped_column(String(64), nullable=True) concurrency: Mapped[int | None] = mapped_column(Integer, nullable=True) @@ -125,10 +117,6 @@ def __init__( self.team_name = team_name super().__init__() - @property - def sysinfo_json(self) -> dict | None: - return json.loads(self.sysinfo) if self.sysinfo else None - @property def queues(self) -> list[str] | None: """Return list of queues which are stored in queues field.""" @@ -168,6 +156,7 @@ def set_metrics( concurrency: int, free_concurrency: int, queues: list[str] | None, + sysinfo: dict[str, str | int | float | datetime], ) -> None: """Set metric of edge worker.""" queues = queues if queues else [] @@ -181,8 +170,31 @@ def set_metrics( EdgeWorkerState.MAINTENANCE_EXIT, EdgeWorkerState.OFFLINE_MAINTENANCE, ) + additional_keys = set(sysinfo.keys() if sysinfo else []) - { + "status", + "airflow_version", + "edge_provider_version", + "python_version", + "worker_start_time", + "concurrency", + "free_concurrency", + } + + if AIRFLOW_V_3_2_PLUS: + from airflow.sdk.observability.stats import DualStatsManager + + try: + DualStatsManager.gauge( + "edge_worker.status", + sysinfo.get("status", logging.NOTSET), # type: ignore + tags={}, + extra_tags={"worker_name": worker_name}, + ) + except ValueError: + logger.warning( + "Failed to set metric edge_worker.status. Mapping is missing in metrics_template.yaml" + ) - if DualStatsManager is not None: DualStatsManager.gauge( "edge_worker.connected", int(connected), @@ -224,7 +236,34 @@ def set_metrics( tags={}, extra_tags={"worker_name": worker_name, "queues": ",".join(queues)}, ) + + for key in additional_keys: + value = sysinfo.get(key) + if isinstance(value, (int, float)): + try: + DualStatsManager.gauge( + f"edge_worker.{key}", + value, + tags={}, + extra_tags={"worker_name": worker_name}, + ) + except ValueError as e: + logger.warning( + "Failed to set metric for key %s with value %s: %s", + key, + value, + e, + ) else: + from airflow.providers.common.compat.sdk import Stats + + Stats.gauge(f"edge_worker.status.{worker_name}", sysinfo.get("status", logging.NOTSET)) # type: ignore + Stats.gauge( + "edge_worker.status", + sysinfo.get("status", logging.NOTSET), # type: ignore + tags={"worker_name": worker_name}, + ) + Stats.gauge(f"edge_worker.connected.{worker_name}", int(connected)) Stats.gauge("edge_worker.connected", int(connected), tags={"worker_name": worker_name}) @@ -247,6 +286,12 @@ def set_metrics( tags={"worker_name": worker_name, "queues": ",".join(queues)}, ) + for key in additional_keys: + value = sysinfo.get(key) + if isinstance(value, (int, float)): + Stats.gauge(f"edge_worker.{key}.{worker_name}", value) + Stats.gauge(f"edge_worker.{key}", value, tags={"worker_name": worker_name}) + def reset_metrics(worker_name: str) -> None: """Reset metrics of worker.""" @@ -257,6 +302,9 @@ def reset_metrics(worker_name: str) -> None: concurrency=0, free_concurrency=-1, queues=None, + sysinfo={ + "status": logging.NOTSET, + }, ) diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerSysinfoBadge.tsx b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerSysinfoBadge.tsx new file mode 100644 index 0000000000000..639c9be78055a --- /dev/null +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerSysinfoBadge.tsx @@ -0,0 +1,141 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Badge, type BadgeProps, HoverCard, List, Text } from "@chakra-ui/react"; +import * as React from "react"; +import TimeAgo from "react-timeago"; + +import { LuCheck } from "react-icons/lu"; +import { MdErrorOutline } from "react-icons/md"; +import { PiQuestion, PiWarning } from "react-icons/pi"; + +const status2Color = (status: number | undefined) => { + // logging.NOTSET == 0, keep 10 as boundary between NOTSET and INFO + if (status === undefined || status <= 10) { + return "gray"; + } + // logging.INFO == 20, keep 25 as boundary between INFO and WARNING + if (status <= 25) { + return "green"; + } + // logging.WARNING == 30, keep 35 as boundary between WARNING and ERROR + if (status <= 35) { + return "yellow"; + } + // all other assume is like logging.ERROR == 40 or higher + return "red"; +}; + +const status2Text = (status: number | undefined) => { + // same levels as above in status2Color + if (status === undefined || status <= 10) { + return "Unknown"; + } + // logging.INFO == 20, keep 25 as boundary between INFO and WARNING + if (status <= 25) { + return "Healthy"; + } + // logging.WARNING == 30, keep 35 as boundary between WARNING and ERROR + if (status <= 35) { + return "Warning"; + } + // all other assume is like logging.ERROR == 40 or higher + return "Error"; +}; + +const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1).replaceAll("_", " "); + +const isDate = (value: string | number): boolean => { + if (typeof value === "number") { + return false; // numbers are not considered dates in this context + } + // Do not attempt to parse version strings that may look like dates but are not + if (/^\d+\.\d+\.\d+.*/.test(value)) { + return false; + } + // Check if the value is a string that can be parsed into a date + const date = Date.parse(value); + return !isNaN(date); +}; + +type IconProps = { + readonly status: number | undefined; +}; + +const WorkerSysinfoIcon = ({ status, ...rest }: IconProps) => { + // same levels as above in status2Color + if (status === undefined || status <= 10) { + return ; + } + if (status <= 25) { + return ; + } + if (status <= 35) { + return ; + } + return ; +}; + +export type Props = { + readonly sysinfo: { [key: string]: string | number; } + readonly first_online: string | null | undefined; + readonly last_heartbeat: string | null | undefined; +} & BadgeProps; + +export const WorkerSysinfoBadge = React.forwardRef( + ({ children, sysinfo, first_online, last_heartbeat, ...rest }, ref) => ( + + + + + + {sysinfo.status_text ?? status2Text(sysinfo.status as number | undefined)} + + {children} + + + + + + {sysinfo ? ( + + First online: {first_online ? : "N/A"} + Last heartbeat: {last_heartbeat ? : "N/A"} + {Object.entries(sysinfo).filter(([key]) => key !== "status" && key !== "status_text").map(([key, value]) => ( + + {capitalize(key)}: {isDate(value) ? : value} + + ))} + + ) : ( + "N/A" + )} + + + + ), +); diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx index 23da0d86d8269..d96d41d9bb9a2 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx @@ -35,7 +35,6 @@ import { useUiServiceWorker } from "openapi/queries"; import type { EdgeWorkerState, Worker } from "openapi/requests/types.gen"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; -import TimeAgo from "react-timeago"; import { BulkWorkerOperations } from "src/components/BulkWorkerOperations"; import { ErrorAlert } from "src/components/ErrorAlert"; @@ -45,6 +44,7 @@ import { WorkerStateBadge } from "src/components/WorkerStateBadge"; import { ScrollToAnchor, Select } from "src/components/ui"; import { workerStateOptions } from "src/constants"; import { autoRefreshInterval } from "src/utils"; +import { WorkerSysinfoBadge } from "src/components/WorkerSysinfoBadge"; export const WorkerPage = () => { const [workerNamePattern, setWorkerNamePattern] = useState(""); @@ -220,10 +220,8 @@ export const WorkerPage = () => { Worker Name State Queues - First Online - Last Heartbeat Active Jobs - System Information + System Status Operations @@ -259,12 +257,6 @@ export const WorkerPage = () => { "(all queues)" )} - - {worker.first_online ? : undefined} - - - {worker.last_heartbeat ? : undefined} - {worker.jobs_active !== undefined && worker.jobs_active > 0 ? ( @@ -275,17 +267,7 @@ export const WorkerPage = () => { )} - {worker.sysinfo ? ( - - {Object.entries(worker.sysinfo).map(([key, value]) => ( - - {key}: {value} - - ))} - - ) : ( - "N/A" - )} + diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels.py b/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels.py index 1a523550545bf..aefc1af6119a9 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels.py +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels.py @@ -148,11 +148,12 @@ class WorkerStateBody(WorkerQueuesBase): ), ] = None sysinfo: Annotated[ - dict[str, str | int], + dict[str, str | int | float | datetime], Field( description="System information of the worker.", examples=[ { + "status": 20, "concurrency": 4, "free_concurrency": 3, "airflow_version": "2.0.0", diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/jobs.py b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/jobs.py index e05159c273148..e18fed366ac32 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/jobs.py +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/jobs.py @@ -26,13 +26,9 @@ from airflow.api_fastapi.common.router import AirflowRouter from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc from airflow.executors.workloads import ExecuteTask -from airflow.providers.common.compat.sdk import Stats, timezone - -try: - from airflow.sdk.observability.stats import DualStatsManager -except ImportError: - DualStatsManager = None # type: ignore[assignment,misc] # Airflow < 3.2 compat +from airflow.providers.common.compat.sdk import timezone from airflow.providers.edge3.models.edge_job import EdgeJobModel +from airflow.providers.edge3.version_compat import AIRFLOW_V_3_2_PLUS from airflow.providers.edge3.worker_api.auth import jwt_token_authorization_rest from airflow.providers.edge3.worker_api.datamodels import ( EdgeJobFetched, @@ -92,9 +88,13 @@ def fetch( session.commit() # Edge worker does not backport emitted Airflow metrics, so export some metrics tags = {"dag_id": job.dag_id, "task_id": job.task_id, "queue": job.queue} - if DualStatsManager is not None: + if AIRFLOW_V_3_2_PLUS: + from airflow.sdk.observability.stats import DualStatsManager + DualStatsManager.incr("edge_worker.ti.start", tags=tags) else: + from airflow.providers.common.compat.sdk import Stats + Stats.incr(f"edge_worker.ti.start.{job.queue}.{job.dag_id}.{job.task_id}", tags=tags) Stats.incr("edge_worker.ti.start", tags=tags) return EdgeJobFetched( @@ -149,12 +149,16 @@ def state( "queue": job.queue, "state": str(state), } - if DualStatsManager is not None: + if AIRFLOW_V_3_2_PLUS: + from airflow.sdk.observability.stats import DualStatsManager + DualStatsManager.incr( "edge_worker.ti.finish", tags=tags, ) else: + from airflow.providers.common.compat.sdk import Stats + Stats.incr( f"edge_worker.ti.finish.{job.queue}.{state}.{job.dag_id}.{job.task_id}", tags=tags, diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py index 6796326014079..59a39f9b4cfc0 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py @@ -86,7 +86,7 @@ def worker( queues=w.queues, state=w.state, jobs_active=w.jobs_active, - sysinfo=w.sysinfo_json or {}, + sysinfo=w.sysinfo or {}, maintenance_comments=w.maintenance_comment, first_online=w.first_online, last_heartbeat=w.last_update, diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/worker.py b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/worker.py index b66be595c0c74..223c94afb735d 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/worker.py +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/worker.py @@ -17,7 +17,7 @@ from __future__ import annotations -import json +from datetime import datetime from typing import Annotated from fastapi import Body, Depends, HTTPException, Path, status @@ -26,13 +26,9 @@ from airflow.api_fastapi.common.db.common import SessionDep # noqa: TC001 from airflow.api_fastapi.common.router import AirflowRouter from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc -from airflow.providers.common.compat.sdk import Stats, timezone - -try: - from airflow.sdk.observability.stats import DualStatsManager -except ImportError: - DualStatsManager = None # type: ignore[assignment,misc] # Airflow < 3.2 compat +from airflow.providers.common.compat.sdk import timezone from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel, EdgeWorkerState, set_metrics +from airflow.providers.edge3.version_compat import AIRFLOW_V_3_2_PLUS from airflow.providers.edge3.worker_api.auth import jwt_token_authorization_rest from airflow.providers.edge3.worker_api.datamodels import ( WorkerQueueUpdateBody, @@ -54,7 +50,7 @@ ) -def _assert_version(sysinfo: dict[str, str | int]) -> None: +def _assert_version(sysinfo: dict[str, str | int | float | datetime]) -> None: """Check if the Edge Worker version matches the central API site.""" from airflow import __version__ as airflow_version from airflow.providers.edge3 import __version__ as edge_provider_version @@ -98,6 +94,7 @@ def _assert_version(sysinfo: dict[str, str | int]) -> None: "jobs_active": 3, "queues": ["large_node", "wisconsin_site"], "sysinfo": { + "status": 20, "concurrency": 4, "airflow_version": "2.10.0", "edge_provider_version": "1.0.0", @@ -194,7 +191,7 @@ def register( worker.maintenance_comment, body.maintenance_comments ) worker.queues = body.queues - worker.sysinfo = json.dumps(body.sysinfo) + worker.sysinfo = body.sysinfo worker.last_update = timezone.utcnow() worker.team_name = body.team_name session.add(worker) @@ -217,10 +214,12 @@ def set_state( worker.maintenance_comment, body.maintenance_comments ) worker.jobs_active = body.jobs_active - worker.sysinfo = json.dumps(body.sysinfo) + worker.sysinfo = body.sysinfo worker.last_update = timezone.utcnow() session.commit() - if DualStatsManager is not None: + if AIRFLOW_V_3_2_PLUS: + from airflow.sdk.observability.stats import DualStatsManager + DualStatsManager.incr( "edge_worker.heartbeat_count", 1, @@ -229,15 +228,20 @@ def set_state( extra_tags={"worker_name": worker_name}, ) else: + from airflow.providers.common.compat.sdk import Stats + Stats.incr(f"edge_worker.heartbeat_count.{worker_name}", 1, 1) Stats.incr("edge_worker.heartbeat_count", 1, 1, tags={"worker_name": worker_name}) + concurrency: int = body.sysinfo.get("concurrency", -1) # type: ignore + free_concurrency: int = body.sysinfo.get("free_concurrency", -1) # type: ignore set_metrics( worker_name=worker_name, state=body.state, jobs_active=body.jobs_active, - concurrency=int(body.sysinfo.get("concurrency", -1)), - free_concurrency=int(body.sysinfo["free_concurrency"]), + concurrency=concurrency, + free_concurrency=free_concurrency, queues=worker.queues, + sysinfo=body.sysinfo, ) _assert_version(body.sysinfo) # Exception only after worker state is in the DB return WorkerSetStateReturn( diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml b/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml index e6babb7d3dca0..0cc94046e158e 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml @@ -375,6 +375,7 @@ paths: - large_node - wisconsin_site sysinfo: + status: 20 concurrency: 4 airflow_version: 2.10.0 edge_provider_version: 1.0.0 @@ -447,6 +448,7 @@ paths: - large_node - wisconsin_site sysinfo: + status: 20 concurrency: 4 airflow_version: 2.10.0 edge_provider_version: 1.0.0 @@ -1336,6 +1338,9 @@ components: anyOf: - type: string - type: integer + - type: number + - type: string + format: date-time type: object title: Sysinfo description: System information of the worker. @@ -1344,6 +1349,7 @@ components: concurrency: 4 edge_provider_version: 1.0.0 free_concurrency: 3 + status: 20 maintenance_comments: anyOf: - type: string @@ -1518,6 +1524,9 @@ components: anyOf: - type: string - type: integer + - type: number + - type: string + format: date-time type: object title: Sysinfo description: System information of the worker. @@ -1526,6 +1535,7 @@ components: concurrency: 4 edge_provider_version: 1.0.0 free_concurrency: 3 + status: 20 maintenance_comments: anyOf: - type: string diff --git a/providers/edge3/tests/unit/edge3/cli/test_worker.py b/providers/edge3/tests/unit/edge3/cli/test_worker.py index d3b721c1e3213..59288bafc7494 100644 --- a/providers/edge3/tests/unit/edge3/cli/test_worker.py +++ b/providers/edge3/tests/unit/edge3/cli/test_worker.py @@ -139,6 +139,20 @@ def worker_with_job(self, tmp_path: Path, mock_joblist: list[Job]) -> EdgeWorker EdgeWorker.jobs = mock_joblist return test_worker + @pytest.fixture + def worker_with_job_and_sysinfo(self, tmp_path: Path, mock_joblist: list[Job]) -> EdgeWorker: + with conf_vars( + { + ( + "edge", + "extended_system_info_function", + ): "airflow.providers.edge3.cli.example_extended_sysinfo.get_example_extended_sysinfo" + } + ): + test_worker = EdgeWorker(str(tmp_path / "mock.pid"), "mock", None, 8) + EdgeWorker.jobs = mock_joblist + return test_worker + @pytest.fixture def mock_edgeworker(self) -> EdgeWorkerModel: test_edgeworker = EdgeWorkerModel( @@ -500,13 +514,35 @@ def stop_running(): mock_loop.assert_called_once() assert mock_set_state.call_count == 1 - def test_get_sysinfo(self, worker_with_job: EdgeWorker): + @pytest.mark.asyncio + async def test_get_sysinfo(self, worker_with_job: EdgeWorker): concurrency = 8 worker_with_job.concurrency = concurrency - sysinfo = worker_with_job._get_sysinfo() + sysinfo = await worker_with_job._get_sysinfo() + assert "airflow_version" in sysinfo + assert "edge_provider_version" in sysinfo + assert "python_version" in sysinfo + assert "concurrency" in sysinfo + assert "worker_start_time" in sysinfo + assert sysinfo["worker_start_time"] == worker_with_job.start_time + assert "status" in sysinfo + assert "status_text" not in sysinfo # is only defined if extended sysinfo provides this field + assert sysinfo["concurrency"] == concurrency + + @pytest.mark.asyncio + async def test_get_sysinfo_extended(self, worker_with_job_and_sysinfo: EdgeWorker): + concurrency = 42 + worker_with_job_and_sysinfo.concurrency = concurrency + sysinfo = await worker_with_job_and_sysinfo._get_sysinfo() assert "airflow_version" in sysinfo assert "edge_provider_version" in sysinfo + assert "python_version" in sysinfo assert "concurrency" in sysinfo + assert "worker_start_time" in sysinfo + assert sysinfo["worker_start_time"] == worker_with_job_and_sysinfo.start_time + assert "status" in sysinfo + assert "status_text" in sysinfo + assert "disk_free_gb" in sysinfo assert sysinfo["concurrency"] == concurrency @pytest.mark.db_test diff --git a/providers/edge3/tests/unit/edge3/executors/test_edge_executor.py b/providers/edge3/tests/unit/edge3/executors/test_edge_executor.py index 2ef41ece0a5b9..0b31797d528ad 100644 --- a/providers/edge3/tests/unit/edge3/executors/test_edge_executor.py +++ b/providers/edge3/tests/unit/edge3/executors/test_edge_executor.py @@ -16,6 +16,7 @@ # under the License. from __future__ import annotations +import logging import os from datetime import datetime, timedelta from unittest import mock @@ -25,8 +26,7 @@ import time_machine from sqlalchemy import delete, select -from airflow.models.taskinstancekey import TaskInstanceKey -from airflow.providers.common.compat.sdk import Stats, conf, timezone +from airflow.providers.common.compat.sdk import Stats, TaskInstanceKey, conf, timezone from airflow.providers.edge3.executors.edge_executor import EdgeExecutor from airflow.providers.edge3.models.edge_job import EdgeJobModel from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel, EdgeWorkerState @@ -240,15 +240,15 @@ def test_sync_active_worker(self): datetime(2023, 1, 1, 0, 59, 10, tzinfo=timezone.utc), ), ]: - session.add( - EdgeWorkerModel( - worker_name=worker_name, - state=state, - last_update=last_heartbeat, - queues="", - first_online=timezone.utcnow(), - ) + ewm = EdgeWorkerModel( + worker_name=worker_name, + state=state, + last_update=last_heartbeat, + queues="", + first_online=timezone.utcnow(), ) + ewm.sysinfo = {"status": logging.INFO, "status_text": "I am good, sun is shining 🌞"} + session.add(ewm) session.commit() with time_machine.travel(datetime(2023, 1, 1, 1, 0, 0, tzinfo=timezone.utc), tick=False): @@ -259,13 +259,19 @@ def test_sync_active_worker(self): for worker in session.scalars(select(EdgeWorkerModel)).all(): print(worker.worker_name) if "maintenance_" in worker.worker_name: - EdgeWorkerState.OFFLINE_MAINTENANCE + assert worker.state == EdgeWorkerState.OFFLINE_MAINTENANCE elif "offline_" in worker.worker_name: assert worker.state == EdgeWorkerState.OFFLINE elif "inactive_" in worker.worker_name: assert worker.state == EdgeWorkerState.UNKNOWN + assert worker.sysinfo + assert worker.sysinfo["status"] == logging.NOTSET + assert "status_text" not in worker.sysinfo else: assert worker.state == EdgeWorkerState.IDLE + assert worker.sysinfo + assert worker.sysinfo["status"] == logging.INFO + assert "status_text" in worker.sysinfo def test_revoke_task(self): """Test that revoke_task removes task from executor and database.""" diff --git a/providers/edge3/tests/unit/edge3/models/test_db.py b/providers/edge3/tests/unit/edge3/models/test_db.py index 3f852185a7df4..4c2b95a9fff83 100644 --- a/providers/edge3/tests/unit/edge3/models/test_db.py +++ b/providers/edge3/tests/unit/edge3/models/test_db.py @@ -20,6 +20,7 @@ from unittest import mock import pytest +import sqlalchemy as sa from airflow.utils.db_manager import RunDBManager @@ -240,6 +241,18 @@ def test_initdb_stamps_and_upgrades_when_tables_exist_without_version(self, sess mc = MigrationContext.configure(conn, opts={"render_as_batch": True}) ops = Operations(mc) ops.drop_column("edge_worker", "concurrency") + ops.add_column( + "edge_worker", + sa.Column("jobs_failed", sa.INTEGER(), autoincrement=False, default=0, nullable=False), + ) + ops.add_column( + "edge_worker", + sa.Column("jobs_taken", sa.INTEGER(), autoincrement=False, default=0, nullable=False), + ) + ops.add_column( + "edge_worker", + sa.Column("jobs_success", sa.INTEGER(), autoincrement=False, default=0, nullable=False), + ) # initdb() should detect tables exist, stamp to base, then upgrade manager.initdb() @@ -248,7 +261,7 @@ def test_initdb_stamps_and_upgrades_when_tables_exist_without_version(self, sess version = conn.execute(text("SELECT version_num FROM alembic_version_edge3")).scalar() columns = {col["name"] for col in inspect(conn).get_columns("edge_worker")} - assert version == "a09c3ee8e1d3" + assert version == "c6b3c3d093fd" assert "concurrency" in columns assert "team_name" in columns @@ -273,6 +286,18 @@ def test_migration_adds_concurrency_column(self, session): mc = MigrationContext.configure(conn, opts={"render_as_batch": True}) ops = Operations(mc) ops.drop_column("edge_worker", "concurrency") + ops.add_column( + "edge_worker", + sa.Column("jobs_failed", sa.INTEGER(), autoincrement=False, default=0, nullable=False), + ) + ops.add_column( + "edge_worker", + sa.Column("jobs_taken", sa.INTEGER(), autoincrement=False, default=0, nullable=False), + ) + ops.add_column( + "edge_worker", + sa.Column("jobs_success", sa.INTEGER(), autoincrement=False, default=0, nullable=False), + ) # Stamp to old revision (pre-concurrency) using alembic's own connection command.stamp(config, "9d34dfc2de06") diff --git a/providers/edge3/tests/unit/edge3/worker_api/routes/test_jobs.py b/providers/edge3/tests/unit/edge3/worker_api/routes/test_jobs.py index dde158ebc6e6e..bd87afe1226bb 100644 --- a/providers/edge3/tests/unit/edge3/worker_api/routes/test_jobs.py +++ b/providers/edge3/tests/unit/edge3/worker_api/routes/test_jobs.py @@ -29,22 +29,24 @@ from airflow.utils.session import create_session from airflow.utils.state import TaskInstanceState +from tests_common.test_utils.version_compat import AIRFLOW_V_3_2_PLUS + if TYPE_CHECKING: from sqlalchemy.orm import Session -try: - from airflow.sdk._shared.observability.metrics.dual_stats_manager import DualStatsManager # noqa: F401 +pytestmark = pytest.mark.db_test + +if AIRFLOW_V_3_2_PLUS: + from airflow.sdk._shared.observability.metrics.dual_stats_manager import DualStatsManager - stats_reference = "airflow.sdk._shared.observability.metrics.dual_stats_manager.DualStatsManager" + stats_reference = f"{DualStatsManager.__module__}.DualStatsManager" expected_call_count = 1 -except ImportError: +else: from airflow.providers.common.compat.sdk import Stats stats_reference = f"{Stats.__module__}.Stats" expected_call_count = 2 -pytestmark = pytest.mark.db_test - DAG_ID = "my_dag" TASK_ID = "my_task" diff --git a/providers/edge3/tests/unit/edge3/worker_api/routes/test_worker.py b/providers/edge3/tests/unit/edge3/worker_api/routes/test_worker.py index 080b019948ef7..fef1c9d50256a 100644 --- a/providers/edge3/tests/unit/edge3/worker_api/routes/test_worker.py +++ b/providers/edge3/tests/unit/edge3/worker_api/routes/test_worker.py @@ -17,6 +17,7 @@ from __future__ import annotations from collections.abc import Sequence +from datetime import datetime from pathlib import Path from typing import TYPE_CHECKING @@ -24,7 +25,9 @@ from fastapi import HTTPException from sqlalchemy import delete, select +from airflow import __version__ as airflow_version from airflow.providers.common.compat.sdk import timezone +from airflow.providers.edge3 import __version__ as edge_provider_version from airflow.providers.edge3.cli.worker import EdgeWorker from airflow.providers.edge3.models.edge_worker import ( EdgeWorkerModel, @@ -46,6 +49,16 @@ class TestWorkerApiRoutes: + MOCK_SYSINFO: dict[str, str | int | float | datetime] = { + "status": 20, + "airflow_version": airflow_version, + "edge_provider_version": edge_provider_version, + "python_version": "3.10.17 (main, Apr 9 2025, 04:03:39) [Clang 20.1.0 ]", + "worker_start_time": "2026-04-18T21:10:42.714344", + "concurrency": 8, + "free_concurrency": 8, + } + @pytest.fixture def cli_worker(self, tmp_path: Path) -> EdgeWorker: test_worker = EdgeWorker(str(tmp_path / "mock.pid"), "mock", None, 8) @@ -88,7 +101,7 @@ def test_register(self, session: Session, input_queues: list[str] | None, cli_wo state=EdgeWorkerState.STARTING, jobs_active=0, queues=input_queues, - sysinfo=cli_worker._get_sysinfo(), + sysinfo=self.MOCK_SYSINFO, ) register("test_worker", body, session) session.commit() @@ -107,7 +120,7 @@ def test_register_with_team_name(self, session: Session, cli_worker: EdgeWorker) state=EdgeWorkerState.STARTING, jobs_active=0, queues=["default"], - sysinfo=cli_worker._get_sysinfo(), + sysinfo=self.MOCK_SYSINFO, team_name="team_a", ) register("test_worker", body, session) @@ -137,7 +150,7 @@ def test_register_same_name_different_team_rejects_when_active( state=EdgeWorkerState.STARTING, jobs_active=0, queues=["default"], - sysinfo=cli_worker._get_sysinfo(), + sysinfo=self.MOCK_SYSINFO, team_name="team_b", ) with pytest.raises(HTTPException) as exc_info: @@ -163,7 +176,7 @@ def test_register_same_name_different_team_reuses_when_offline( state=EdgeWorkerState.STARTING, jobs_active=0, queues=["default"], - sysinfo=cli_worker._get_sysinfo(), + sysinfo=self.MOCK_SYSINFO, team_name="team_b", ) register("test_worker", body, session) @@ -206,7 +219,7 @@ def test_register_duplicate_worker( state=EdgeWorkerState.STARTING, jobs_active=0, queues=["default"], - sysinfo=cli_worker._get_sysinfo(), + sysinfo=self.MOCK_SYSINFO, ) if should_raise: @@ -315,7 +328,7 @@ def test_set_state(self, session: Session, cli_worker: EdgeWorker): state=EdgeWorkerState.RUNNING, jobs_active=1, queues=["default2"], - sysinfo=cli_worker._get_sysinfo(), + sysinfo=self.MOCK_SYSINFO, ) return_queues = set_state("test2_worker", body, session).queues @@ -342,7 +355,7 @@ def test_set_state_returns_concurrency(self, session: Session, cli_worker: EdgeW state=EdgeWorkerState.RUNNING, jobs_active=0, queues=["default"], - sysinfo=cli_worker._get_sysinfo(), + sysinfo=self.MOCK_SYSINFO, ) result = set_state("test2_worker", body, session) assert result.concurrency == 16 @@ -364,7 +377,7 @@ def test_set_state_returns_none_concurrency_when_not_overridden( state=EdgeWorkerState.RUNNING, jobs_active=0, queues=["default"], - sysinfo=cli_worker._get_sysinfo(), + sysinfo=self.MOCK_SYSINFO, ) result = set_state("test2_worker", body, session) assert result.concurrency is None diff --git a/shared/observability/src/airflow_shared/observability/metrics/metrics_template.yaml b/shared/observability/src/airflow_shared/observability/metrics/metrics_template.yaml index 17356bdcbb0be..e816a1a80d31b 100644 --- a/shared/observability/src/airflow_shared/observability/metrics/metrics_template.yaml +++ b/shared/observability/src/airflow_shared/observability/metrics/metrics_template.yaml @@ -474,6 +474,12 @@ metrics: legacy_name: "-" name_variables: ["event_type", "operator_name"] + - name: "edge_worker.status" + description: "Edge worker status (expressed as Python logging level)." + type: "gauge" + legacy_name: "edge_worker.status.{worker_name}" + name_variables: ["worker_name"] + - name: "edge_worker.connected" description: "Edge worker in state connected." type: "gauge" From ada974889db0363b93b597a0f96e7b82c6370664 Mon Sep 17 00:00:00 2001 From: Jens Scheffler Date: Sun, 19 Apr 2026 17:51:51 +0200 Subject: [PATCH 2/5] CoPilot feedback --- providers/edge3/src/airflow/providers/edge3/cli/worker.py | 6 +++--- providers/edge3/tests/unit/edge3/cli/test_worker.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/providers/edge3/src/airflow/providers/edge3/cli/worker.py b/providers/edge3/src/airflow/providers/edge3/cli/worker.py index eb7cada3ba763..2e4010e9dc778 100644 --- a/providers/edge3/src/airflow/providers/edge3/cli/worker.py +++ b/providers/edge3/src/airflow/providers/edge3/cli/worker.py @@ -81,8 +81,6 @@ def _edge_hostname() -> str: class EdgeWorker: """Runner instance which executes the Edge Worker.""" - start_time: datetime = datetime.now() - """Startup time of the worker.""" jobs: list[Job] = [] """List of jobs that the worker is running currently.""" drain: bool = False @@ -109,6 +107,8 @@ def __init__( self.daemon = daemon self.team_name = team_name + self.worker_start_time: datetime = datetime.now() + if TYPE_CHECKING: self.conf: ExecutorConf | AirflowConfigParser @@ -209,7 +209,7 @@ async def _get_sysinfo(self) -> dict[str, str | int | float | datetime]: "airflow_version": airflow_version, "edge_provider_version": edge_provider_version, "python_version": sys.version, - "worker_start_time": self.start_time, + "worker_start_time": self.worker_start_time, "concurrency": self.concurrency, "free_concurrency": self.free_concurrency, } diff --git a/providers/edge3/tests/unit/edge3/cli/test_worker.py b/providers/edge3/tests/unit/edge3/cli/test_worker.py index 59288bafc7494..dcfbe6614ef3d 100644 --- a/providers/edge3/tests/unit/edge3/cli/test_worker.py +++ b/providers/edge3/tests/unit/edge3/cli/test_worker.py @@ -524,7 +524,7 @@ async def test_get_sysinfo(self, worker_with_job: EdgeWorker): assert "python_version" in sysinfo assert "concurrency" in sysinfo assert "worker_start_time" in sysinfo - assert sysinfo["worker_start_time"] == worker_with_job.start_time + assert sysinfo["worker_start_time"] == worker_with_job.worker_start_time assert "status" in sysinfo assert "status_text" not in sysinfo # is only defined if extended sysinfo provides this field assert sysinfo["concurrency"] == concurrency @@ -539,7 +539,7 @@ async def test_get_sysinfo_extended(self, worker_with_job_and_sysinfo: EdgeWorke assert "python_version" in sysinfo assert "concurrency" in sysinfo assert "worker_start_time" in sysinfo - assert sysinfo["worker_start_time"] == worker_with_job_and_sysinfo.start_time + assert sysinfo["worker_start_time"] == worker_with_job_and_sysinfo.worker_start_time assert "status" in sysinfo assert "status_text" in sysinfo assert "disk_free_gb" in sysinfo From d1a9deb6af153246c748f6c7fb1b35523319bd90 Mon Sep 17 00:00:00 2001 From: Jens Scheffler <95105677+jscheffl@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:26:02 +0200 Subject: [PATCH 3/5] Review feedback Co-authored-by: Tzu-ping Chung --- .../src/airflow/providers/edge3/executors/edge_executor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py b/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py index 21a846d0f4457..8f2d67dd2e829 100644 --- a/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py +++ b/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py @@ -184,8 +184,7 @@ def _check_worker_liveness(self, session: Session) -> bool: else EdgeWorkerState.UNKNOWN ) # Reset presented status - sysinfo = {} - sysinfo.update(worker.sysinfo or {}) # copy needed to have alembic detect change in content + sysinfo = dict(worker.sysinfo or {}) # copy needed to have alembic detect change in content sysinfo["status"] = logging.NOTSET if "status_text" in sysinfo: del sysinfo["status_text"] # Remove old status text if exists From 3b354f3f31f0b5b80cb7f6359739e465c5b5566d Mon Sep 17 00:00:00 2001 From: Jens Scheffler <95105677+jscheffl@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:26:25 +0200 Subject: [PATCH 4/5] Review feedback Co-authored-by: Tzu-ping Chung --- .../src/airflow/providers/edge3/executors/edge_executor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py b/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py index 8f2d67dd2e829..d9b67f69c94b0 100644 --- a/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py +++ b/providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py @@ -186,8 +186,7 @@ def _check_worker_liveness(self, session: Session) -> bool: # Reset presented status sysinfo = dict(worker.sysinfo or {}) # copy needed to have alembic detect change in content sysinfo["status"] = logging.NOTSET - if "status_text" in sysinfo: - del sysinfo["status_text"] # Remove old status text if exists + sysinfo.pop("status_text", None) # Remove old status text if exists worker.sysinfo = sysinfo self.log.warning("Worker %s is lifeless. Setting state to %s", worker.worker_name, worker.state) reset_metrics(worker.worker_name) From 245e799f24079960d5b80a18e3f3afc44edf7d34 Mon Sep 17 00:00:00 2001 From: Jens Scheffler <95105677+jscheffl@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:26:44 +0200 Subject: [PATCH 5/5] Review feedback Co-authored-by: Tzu-ping Chung --- .../edge3/src/airflow/providers/edge3/models/edge_worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/edge3/src/airflow/providers/edge3/models/edge_worker.py b/providers/edge3/src/airflow/providers/edge3/models/edge_worker.py index 6ad02dff3b2e2..5f2f7e999e5ad 100644 --- a/providers/edge3/src/airflow/providers/edge3/models/edge_worker.py +++ b/providers/edge3/src/airflow/providers/edge3/models/edge_worker.py @@ -170,7 +170,7 @@ def set_metrics( EdgeWorkerState.MAINTENANCE_EXIT, EdgeWorkerState.OFFLINE_MAINTENANCE, ) - additional_keys = set(sysinfo.keys() if sysinfo else []) - { + additional_keys = set(sysinfo or ()) - { "status", "airflow_version", "edge_provider_version",