From 69961901ba855b8557b56ef3bd3dad4c24ff5157 Mon Sep 17 00:00:00 2001 From: PatternCatalyst Date: Mon, 12 Oct 2026 19:44:50 -0400 Subject: [PATCH 1/6] docs(deck): .NET deck names HybridCache for read-through, FusionCache only as the alternative (S13) Speaker notes for Appendix K listed FusionCache and IDistributedCache, which example 24 does not use; Appendix L notes and the anti-pattern and decision slides still presented FusionCache as the default and said HybridCache lacked stampede protection. DotNet.fix7.json, slides 211-212 rendered, deck_fit 0. --- .../Designing-Cloud-Native-APIs-DotNet.pptx | Bin 15061152 -> 15061167 bytes ...signing-Cloud-Native-APIs-DotNet.fix7.json | 112 ++++++++++++++++++ .../Designing-Cloud-Native-APIs-DotNet.md | 18 +-- 3 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 lgtm-presentation/edits/Designing-Cloud-Native-APIs-DotNet.fix7.json diff --git a/lgtm-presentation/Designing-Cloud-Native-APIs-DotNet.pptx b/lgtm-presentation/Designing-Cloud-Native-APIs-DotNet.pptx index 79298dc15d0aa9e2ff38fe416f7ae2e20bd21cd4..ec26d5848eec7e11b5f690a977b3c5a6d4a73d35 100644 GIT binary patch delta 54761 zcmZ5|cU+I(`+s{++C#fEr7caQB`qz{lBTq@R9>a>lBU+(P}djEcZ+>gfVT+i!yUFSO2b)9qWDlS22Z(t9Tg()Q!2N@X|9og~1MkZN$ zisOZud+6U_Uz&Lt)EV%vRvf)__*Xu`UTXa7tf&_`{`HVJ4;B8^V<#8*`JA){1^)9b zSxWG8r(6y#_H&ty3jSjzpP)!{xM~G}5ZaqfgAZ-h-wB3J?+d_(B87qEN}2H{Pw>xd zOpRV)U(u_Q@~k>%ju3jZt2+=BFVe%Z1czm1$*Yzz2*1+6*h0 zhS#|5X!Rai3gDc2P1-S%gBRs6#lEYV_-qgp_tBz>`{?pN;u<96=@n>z%irC4;+ff@ zFN$=@LzZj^qD()J4NntF;VdJ!juCu==r0>_BcBxJ zXwcUhj1*wrXLK1w$-%Gz3C7e^%*AC$Z1Z*zbhkcZ5pF*^a*z=ce{RT_3uOGw8AYJX zXDSBya~O00!ak}eg5I%UOa*K(8D&IZH^w%}j`0Bq)5qv?^&KosJ>`*m8jRd%h&|&Y zT40{Jq~K2EGLM!hQonnS5gD+m6hNNvIyNE)Tp2}i241d=p(NPtUW`uw;q6x{#@=Iu zV~~4Kb-9ruCCn}+Z^qAn_2y|tY2a>|07gcVaJn+aNzFi7l^(JpC~GHjhO-(pTa_Xh+@WHfySFxG53)aF{xsT z96Rp-MFI`XM1cktRG@*iqz1R>t|OZ{o<&eL4>nP9;2$B1#b-&Ff9yFBc6~+>)YX$M zAFK@EOGSS+5P#$WY!`ujNg-@9z&xjeS<>a{eU3yL*90X{_jtA>D4&SQJ^5ZuQ$rC!opmsYI&IUDY+71IHw0Zk0+z}AlTL;5V zzrFndKE|$XJ4X-xjo&$rJ0R7`6FEeIvn(%iJR?atq;d@7<7Cq~Qh`(3vpC>1z0cI>syDc;(M?#orPfdOhRfYQkH7!1?)m$z} zR~CmjTCB;v6%zxumJ3xecWs9;F}#=i0EwNEK1AT##|?v?&4?S$fR-`$%KygY7-L4X zM;Zbn6fL+>MGKxceYgkNczF3hV94#@g@X1%ydorub&|Y*B0-AR8B~I4KE5R2az6<^ z7!#Kz`7V-NY$neK1cVg$An;xlJ_zio%I8P|{;mba5g%&vp%1nBsX%x=*5PLbY^DzI zLm|e4{PX`a1Ccny55RvM;&%pB!(LsW5@*x8Pe2SVA>lRC4q?pWqeyHTy9jEzUqF$h zRKWuCLzjsF?gx};D!>GTAbo6s10jEVM#jG(ADqa{5wljh!a2d3HUJzRI{=Hxb2_=15a2TuzZ;L3O z1x)ZVSP_VVHI~{htq6Vr!soti30JQPJ|qbX9l=cTZA^H8^9cVfh=%{(abOJBc4t&5 z2F#G&Yaub<){}39bV*LQVuMS<8jM!Y3rRsIaMv(e0w*B5Y_S2LJK$Lsn#UkpSA^sxvm<9+; z4~YT1#z$gk<0EnTa-76;pu~A7FEU)%hFlI|6GfB5Bw(^g50`KTCSmeh^{7g%A{nno zL})ZOL1L>OB}hOU_FRx?BgPAL{P$ONzl_k*R{?`0@o4WNlL@{)GwkVgPCn&Q4*4Uvm}X<6nD5_HM?M0 z@Byx{;H#v?75tm!G$|W;Pz9-b>G+Y@3s@s|Do<(y|I9mI3dAH-nO-afRJxW(r9=N{ z-$~x`5C1hOs8Xm(N);pr;NQf0D01|MlmKM;<=W@1rB~b{wYBt&+FuoCVGg_tm)=OL zR5^)tojb<*Un-02ltLprcb+J~*-hI^r^BZGxbWj>SdH^tr9u79ag+W}wUhS10=&UX z8dx6YEiDMSTsC&9gC4u%FTFLH?+(!?UBbNX1WMAQZvoO-z$Q?ZJHn(vE=UcR zMpMIOW}`uQuQ=;@$__a@|>ea^S{fu;zpo*NeWq5 zB6q%9jGw_ga1Msh14`M?(8C(nSGPfm)AX`i6hHg()s~hTAIFXmnr=y4)-Y;`d*mmH2p54t=~SuRH@%P)vq8#cz5tvY);%-fYfY_^_7uySCx7=myO$ zn7O3scKxRl`W=Y5S&L;CZ2lLcGzU(Okdrxm#S$YIQG#_BD#5CFW&{T)m{tV27@d5t zD2nI6)(?tGBw7){7?5uYsMM&&Cq*LQc5h7)c5O@F6%+r*4Y~eX5!evjyZHU!iIxT!I3{G0xEadf}MFM+ZMP#GfXF3HcRl zxC`k*RV(oEn%7iMk!+u+Qw8SK)~jYglq{>9wX{B0WY(9KtS>n5O^ zzwl^4H-G2V*d$HUAp(Z`FUxHRYoHs#dvAfz^zlwPy?% z^7*H>GIHs_9#B~v4fYTn4Kz_lgS{69z_Jm%&n0;V>s;4&!(~IwrWFr5*CcFT3S4r7 zSBsN`^ix<1r1}97Ek2kthHi$k;3-xNVU^Yb=OPR;T0y{Opu15~>j_XrO`KXjjIH<$ z9;m8(x0VxJ@+P#_d@yUsscS(eC~9c+11A6s1s$k$Q%8#)nruDNsQ^qy%n~pMX&Y#P zotyeWtrU{#pND`LallLq9Wc{=9ErOvzh9e$9<tS9t9AfR3B{ZN z+(TUZrH!ur(s|Jb7~j{826J|JPg`L2qkYbA_)a4?hdo!HHbX(Z_eN zdc6Pv)H1$Q%(Q`v3?LHh>u*q}3AJ$mHf0w5pJgA0IWRIi)_@#cjdSa=OD^%y`a*AcDX z349ib1ptWiVxoQwNq{i}Bj%i{{{W{WnWnD@6 z{ebtF#eNO^wqQ_al}pM{n&E=-E=lcz!xog!PDR~bUNkRDLbgD zG{69;Dhf2zw_WWlacHiAU|hI!AQ#uue(F$B=-ZCZjdMhS1-`XAEmePz{lWa4RuT$=aNNC8TkU ztt=5`m`w+kt0Ofp5;&+ThMY(+1pJ*Z7(OB)xTI_$xLVHtX2uY%sz8JzsfL2k1y6rS z(;)f@hGNLy0&J1`x;3yNQ_3lfNMeFvBNBudis1|q#88I>OJ8IN-14f(Fb!Yi=&5o; z*d63n7&YHLzUM)Ed&`5GZ?-AzWEH&4yqT43968Zv#-GQ=1{`)Mi8g zdZ%6+QNxrP`D9`lrWlrYMj*v7&KpUPIJqww0ZwYmMoVz2zG{D*fD(tkK?&nEBiNw> zcK#bsLjSjs4fKVtOnV%Z=%O+PIy$M1gGoB*%P@QVnT){#P-8aMfvMV?(ajd7>Y{DN zTW#mu^mfr>SdOy+T{^N_hRyf0?Z*E(_BqXK3<&G-8N0*4KZ>kcL-mivp!z{^l}+^sT40Bg1OO61c;Es2Rf}e_%GP4%GWn$M_oL z-+X1N2D!%(B8D8gh5@uSva=vP2aG@BvJD2tK43!0S8bFLb5kfQa>!U3%;g~~W4QFr zS{qN3#-%xcaYWan#;EI2lj0SS!M#pYHh#u@(uB<9n(x-|`93)5t$OSaw(X(|V;?QG`I`mlBt%K~e+B`T~f@P=H z0BFKkhh+ndt+UzN9DxZ)W3MH!p}x-&TuedG@W+*$;5P54^U-9xHU~ywb=zY#3Tc?8Msk4vqjnq-3al%dvS#8DknCo0emi|qj z3jQm2AAu8E6g+}13fdem!O581wJC&Tl3tG7#Eln6w)firGOPnOl%Vg!JgR~O+`=&a zU7qod+GxOOr<9$EgJRJyp;*L(jq$&ES3&A;V`5vIPPBiGi@7WiwL+!p*6 zWuUW#3s!^PR*f{SoE3~ChOpV9A#8RZ@8O-?)M-13ufoYWykNIw)KT8k%B|Rfc?gU`hVRbNq8C2cb&-Ih zKH1@{iDsYeP_xhW1JyVi-YX70z|^S<2gru{nu8B6ga+0*KzD@JJ8bPCT#GN-A&Q$W z;^>o`4qG{S;}BIF7pAxbj|$P&RtHI-GH}FA1v%E?0949%I-v5MM_(PmaT&Hb1_K8W zcpb&yWr>D78K*G9651$%4)ZzgCIP66I08@8i8`7=^Q?#dKRO0M#ZRITBdNm6LT(w@C0JO;c5;vC}IFJsBYB-7fM)WBtotm=_whywL4N4=421y`3RjUa0GL+luDybk}_`7&9BV zMBfa!UB-!^7$$HyF?7@o4IMqf{~M&*8u@^f0IU(~QovI!H`@BxJqFG&pP6?bbcpM? zJ4m%oFWi3vqd@*H9>(+c*4c;aw7WShp8GFY)F?$~n@P5R`~{sO0I)bD8pwl7zndTy0H!chKdKW33Pa#`9@6kUDh`I5xK z`6-6>FXug(^wa{zGQahNSzl|;)03pS@EJDv-|<@Yk|$cd)SEoT7-P_d^na9s1s&;(XHgzJjN7G}X`>mb-Q% z@0x!JUKu$yj_KY@<&2w+`k8w(1ENtj-jHaGt@kbxDC=Ywnen6PX+Yjp6>*EKvP#_p4CNW)0Uxje%h*g1p*mD}K z3iR*f!}WP4`!B%~7BOc_0rj2B@dx@M5&t`YbF5~F3gTbn4^oESRe!jbI$Pl{12bLK z{PamWBxb@!5`9|f&qI>QpTd&al6M0<%}WiKJkeZCPR7}f#Io6P5jpDpQI7h6`(RV| z;&9*+G~An9{S+j!>JSKsEFBH3BY{3~4Fp7r-2x@xYCCCn#$XkRt?(B^8fGw!Od*YI z2%~r42i(H%-huE{2Vg}^01zfx1_q**fkE<-_>h>TAU3GmK_UAM9#Y~+$M+zh`}U6@ zE#Pz@F1r~7bpPHA(xd^~DhiQ0#PV#CIC6Rp^Yba1V34>yX@eO^QrB35L6Qk)4MxLR z&ls-aO7HzWEuR$Bjcl*m4qp3E~`WNF2#*b(RpInvOLDTza7G93iki zf8Y!OEj9uu8VSI0yn-R60KZfGA3pjs>KT5(&44skV!rBHz6W*s~P43w3XJz-?r=Dep3lg+NINRVvgvm=`T8B7%{nDd2^4&XQX z6_FA^jaOx)CP_{D7c4mcC7SDXk>X(Rlbc(EzqdsGB5{BG6&tMB5s9x`^hZY|xVk|L z??*z94R%L@gLDKMx`quF9EgOs9=AiiV|QU!w^GRG!bo0}ZZJ{@_M5q8-{PPV#cNo! zMvX>(#?k$sM#4rAU_E*n37l3m5h(w&jW9)%*1Fyuu zD9kO4u_XzMmB#>s*2-ht!8k|E10Y&s>tYy4r|sl4coO^p;UB-?s3CLE7b zRIx1v2BTAZ%>On_$_U{eRA6~OMh_bGlQtq6I=Ztz25gUR55&9$u@2^mK=}^4v>T3r z;cN5!fD^uzsf5L}cO-@xIzBJsuQ7D|-|?6&$2X+Q{Sm_)-wYkU`(+Hc=!SdFPAUxd z-|4^abPUmVIu`YvKA$Oz(Qjpn3y1W0>Ry~&;CJuYz(Q7gZ10rDNS(dgRe}i>1FT-G0mvbm# z1!fpJ)3yxA;)Y!UU{PzI@CctFXiU9J0+_cL*Mt+WWXIgTRDr}MC5R)ezEFbcR02Hy z1a95SfKmCkeasC`xB#YGI5I(koB@AHhGQ3!zD|vl+)j{0L!uJEtC3Jw1S2NdaXtYS z?%T;_f4i{&b5n$zE8`NL0;tF&3<_M+mZe-|{)Vvm6ic9wlM|>(?x4$rtS2%Pnn2K` z<|V+O>BvtoAdOQif#c{(6JEhtDGoM{*s`vA%etBbkdYuqm&OEmx^b^`8yVj5gVPNj z)TSw68;qC1qJ$2FJvc%P&A6374MIHccEbPm*UAWY7f?$4(3OBa7tjw~L~j}}O`*x} z#ndp9b9Mofk-u#3Xwc|Bq8_mO?qeccS@Tbbp!tG~)W(QlhUmtLVz_0f{A(htp2yx0 zZ;@H`yfei8DkJbsxF-pVgKy zei10OdvP(yAIBLqOneAE=r2Ak3?u4}c_N6Yc8f%K<_qGZ(=HK+rr9SN!H9Z4Hp~qr zDBPgLrdy&niFp9`eKOF`6QT)$;F?Q+{h*;lfMUE7V;Ou zXeGuap^0(Hx3dAQ2I82+Eo|d>CluM;l*)s8@uu{F%bMMyDKOF7iKT$u7{YFvqJs2G zr+|W=FO#wf#uZ?vYGAc=sif=$6u`JxJ4RUWhdwI%9K!5AF9#J!YNa?ryVHbjc0;>s z_osk}yM7=AY(`KF0(MDUhf>mD*w8M=e}iE&Y?A`Q=CN%G*qo!gk8h1ub4rPY*mo7V z0^#V{lW_F3M+yO_hr0T0jrKU5Vg!EEa3%$MaVjLGnPkG01 zjfpumrovF9*{C&J!9vj;2|S&`iyAkj{-FlxbG+3;W(A3TZYhce-b#gj3cZ~QZ}9-o zqzne6-IWS6@#jS8L&cJn_Dovk>m-pZhXe7gB7`oMrm#uNO(DXDl%yjMTmtg=9RVUn)hA7B%mJt!@ZZ4uE z$7SrvKqWac_YL7FKGT`faCeNTJfB)YX1t0;(L3{*TW3*I1&f@r*b;6>?)`QWL08{r z!d)(~*kv^nUZ7;&s2%erqab_8MS-y2OXVaU`IL!0S;&!3+2OdB$DgwIz*_75wnYb- z_9_-bK4{=Xe`JG=u*Q1!5t0q}HnU+8OSGe}lI^1)GowMsl^r<~ZssK1L|Sg?t)N}jS zrzh38?yOo1RB^u%ExN3t``CS3MZjxueigIi)TGXN3&U-$m)!fy@ToEp|Fu?fvd%~aNV5TbA-ukcUQ*U+09l!k}i z9r8(d>5#Ot^lIF>Q^~Y~o*=m7bA2-7q@{s-PT*2_f@<&yKZ|7_2QNqEpc8(B8t5t6 zBMyV%8RFc5kK!+#jo9FbIL>kY?T>tQRhf@^awZ4&$=cDsrMa1}OI`2ry>+)(y>GWs zZ34$LnqloB$t1p-9PQpJu1TM3j9-)n^FP0<%%Np zqh#sG8n@ou`RttMlo!tpEh*6bQ8d!}Sam|m&*vvqKpE}9h%j%{b)ljnmyg8OoN!Wsr#2Xfl)kDYsFK;B;LxOlbk+DvLp*nxA!Up_w7 z?&vrh%o?U8z$PU4(QGNJeY}-B&TsXmg2X<>&lOYQ-jW~6eNRRDnjcy#yRJ7!XqU@B zpjUTKE=Y;sjEqdvJmC})T3Pwg^T6$nk>>WNXVlCx_8!iA!o}TOt{$2%r!YGA%)NCk zr7Wzfds0-&-g88gBF{jHqQF3nVnFm`0CU;LxH4CUTVk))TQ@(bt=}9dJ(Cfx>e7Do z?8;I9PX}rDv~AB>54Wp6QBS^jF~o%sp5Ay>r}qz?7&75%_=RRg>fXZoGe?uVoL0Sp zIsqO{Rq4s1(=GkY53e`sCFNMF#HddIW=GkFy^?;k#&n@$+wzQW#gWAJu(Reaw^*L*Dt)~AHI zlCxwJ(bu9EN+V^(1iu|!?D*sS(M`=X@wp&ISNl!lB-}}ap@3(kTly|&v(<55L z=nF4sGiKrg#!Eat$Ukv>GMCcc+Qo6>W~V5BP_yID{J6EqcTHYuKX|E>#2X!-A#0O zSGBWBN?j>gwOQzXkCmZI(~&#AXLRg&cZ^Pc@;!X~du6}z13`^D-EPwo<90n3OnvPL zwf6n-)%t?xx<6ihIOUbNIA3PF-#e@_G|iVWN$*+NN8)8$2AoOOh^rmg|ppw(-lNI9obj=e1nQU>15Fa>{M0Mcrd>BRP*R()k8> zssGLGWp4AAm1l3QrLL|$J>bK%*`+M)IykpX&|9n(_;#|eT+E~M5}Sg^dyB=6%R?(a zuXws!*#EB3H)s+J^y_XHH#FQwomTk6BImkQsUT-*{Q2kmS}nX7dq0kr2D`=0)ZGu4 z>S+-@qHbq9OT=D(@?o+olHstB5NHq)sey=?o*g( zumrVC&?oc<@wsEK@vHCr;WV;tzrR09wL6rp@s~b$W}5eyw~f(i`)AhLTPNQ)oZgwc zyPQ`sNyLobcGk#se@=hnlXokf99FzGD~|WfUxyH6j`&9{sG0lDeb}F(^=I_pxv0LM zFP;(F5OeONmj;^5kD>w_bInITcq?D+2rp8&;Gt8nt7uc``k$U>FuVA_;0yfr$X&v!tqPb`<=|)x~Rh%VZub z<%|@0p8xs-738rFHum3TM+AKy8bO~=`w|?K4eyXQ6To&tFF+H9R?OzhZPiz`{8GHW zilcuP^0$h(=;k3R4s7rPP^M6YkNI%h1PGbyVM_k}CE3x>`RLKl1*bXXq)**dW#%GX>$1_np-j0fuj>%#n3tQKE?3N4)AskCXcy+BES*_|7>ylX`HC9oY>jw|2l6s7b6U);vLU5%g4i=ui7 z<^BK6RYr7@%Rv{%mQucTDG_J1dU&hta>*>;nz4@^5eC~Zj5>&skyS2A!uXI6Fo^91 z>d%sgC)&=e7g%I|qDigpF z&QxIKR-;4~a2dId4dI2|^Pb>Jb^_a^fo5^5cwgvo)Z}udF}I3$oaXC#?=KHU_gxk+ z{^{8rqtoN$5qU+YU(f1`d49M+Ehe%_icVDuR-nGuzY`AMGEjDV^qcv-7B(@Nx0o!WZ9~Ol0`J zYO?tza)xuaddrH1jrZ(&7UPO->=`rVCxL!*w`LZOT;V%uGxIw?&|o3lDUqvIsHsBD zMZc|hT|r&T=!d-eXa>*twg!haI)+|C|4@3hgyqIiDsfn4`BG=)q?&J~x}Nvi7U7p|0-r z?&oORfh+RClid&NROVhVggQM6SfnAOnY5~-VUcbce95C;ZaHhLPrv0@)BG7}xT@B> zqq593XC?no@6FZ~`Kn=cJ;D6>@Jp8;Hg-Lnd*Lz{v?O{%vc99Cep%FB(|V(6b4GIg z=1}06X(!W7`!MY`tKeI&H9FpuzD0A;CM!%1yT8M;f0K2>nUtbj@hvIwN);dOBklF(bHY&iC_hYP3)M ze(|r2i}`fBe^7Nl?(!pkY(%frX+7i*JIQwW=8^fHJhp<*nNRkpXd`NUe`3>w#qSf} zJKR<8dgybuHzkC-r%l$)=u_^?gL27fMjNJPOfGY+xuqR%C$67YuRI{4-x;zU-NyEn zpsn?Lw<*mib%dM3K1cRocFo=gPD}pYYzRl4jKcg1*cdpC#J# zCPnQiO5<~mJx-6MU@~Gjex~<*3=>;TxxrxqHJz|1Z&OsFN+8RjE3OQIL@wb2+{clI zyb~QeyEeGxP2SzkHRHOlF+Lk=9e#;R;^DQ}7iG`(sWqD%JuPU|wr@O?4IebFF#q` zjavv6SH0wI-|SP^%Xws=c2<&0T4-GM+sZXz=C*|cjlz#hzp77ac3*3u4BaNKef!wm z<|_(|}G*2sO=aNk0# zyH1F1@A%bpzy3;1t=dm4E3zGNeOzyr!!mw^tljWko$-1m&OkUB;nFHmP-E6~d)dNOA@Q#w>p)F_geLv|iK!~lGnB^pQ>v+dY5xB% zMj1&vjP0`j9n@|{R2svQF_w|A21`cpc~~+6;wn2xXBdynF|kvLmGB@*>;B|ZO038= z!XC!fyN*;??ZNmP}9y`xM?HQ{!;Hyp!pH)7A zqYCsKd)3x8(vg=(dGY(Za@a~WY^wqn#_;H6&>F)P;H`p3FZ)*W-}%RqlkwDpvo|Wf zDwK-vIw6+Ar4UUhxGumz=Yrj>{1|93g)SSaxW0jZ)?ay@gBJS+Vji-=Fa_(cKVZg3 zm&T+f{=~R8K(4emu469};U9SC(dcY-5qn#%V~q|5q}O^K`|HntKu#Ukhp``sik;U{ z#m?#&eHL79_M0E}PZr3??w!PIH#hqHZ#4%4F1&~x$pI8Lh8pZR?;pw|wi+A-y~uT>hFl-cje5pJDFqWr{a#)F!R;r zc8nT4M;A2dmb00=KieI#vt_E<=2uj)S9a7=ZHR@Uf|%(q!Yi1bUj0+k_q4lrNW>+F zqpEz&o?o`x9Lu9RRiWme7~;kCJh=Zrk^4p__k_7tbm{wV=ehr$UaQdmCU#p#`PaT( zhwRT0P$wQ2gQ8@~%s`3^x}nUZOJ4DcnXOf!=6t;Og*$s&K3;IYBcJjq%qFLSxmERV z{;xfgn#F+`D<0l!e`)0EoL?V{zp3PPJCq z7Fw%6$L+c<`4uH^3rOkJYU~IXvo(HClVK9n?ES-%{nxXm3Af@9{zHcoQS4FG4JWkyXF#&yWS-xb>!drIN5fZ?p8?UcfZX) z*QTG!m|rLxDj+YYDKTVh={4p2e%sg8_=nx?NyCt1k;5zjvJHZ767EnM-EQK!*3ng? z&sMnO?0(bg@#L>6UnnIs%}ov;qd2l4YF>M+#Yw}v(1p`8VQTx1A)kl==34qEnR2h1 z-;q_OgzQSJ6IGCVV8LTED4HxI{y-M_!(Ssnu%izf>;BeyHusG!>vZd`EY{;eCr&c$ zZO^Ip`fGYTxz9l@Z_#97fd_H6S-IX&6q!LtFZyJHe8Ls_g^I*tZ9 zZG;`MZ{^!C`67Ai4B0i4iHH}+e~F*Y&SH5Ka>Kq}?UQiOS=k*rEG~@I-`*H}2!Fad zyM5Mx>B4a@w-}!f)CPh*P4{1!&RfAbd9Lf{KoqI zE`bfIU#`fAWW`U+3S2x@Y|poX?t=vFvuBj9gruC+$}9ZMy7BEx`_j|-jSo*HcI{)>%xTg6GqVsLwLJRg z?;TTP+u~Y-AVXyH%g-l5cccFL+WA&&cHH)Csc3&veCEf=1Gghgziosx9E*Q)?WtnL zpK-hR6=ClU-Sle&N>Y)>>%yrWkN3A@|6E9ooD6wPT`GwfJ79GPoLgWugig2yr4z2D zKLu*Ymk*g&7_k~M;16qvT5>HvEN(Oxg3Ixvb#Wv*r}igaF2a%82~tfOIEsOS0}kvX zhoV}2P*Sdx*J2xf>^FlIwb*+^|4NT`Z7nEZtaY_0YhB&i4SWdA@%kD@P)OCTk8t6I zR2+HjRsReB?4@@-fmBFS0_#ELat^9jq5%@JXI`F0Vi_)rAv>Hf9yNPg*^tK}Y@J9} zM7;Vxy74zmf?>5^iKxf!UjOmg9aBHdiu-EW^&1m{-L!IbkRvm@pJ10PvGu5A z?2XE7oW*0VhFRzh$wLj{km00A17P?EdhW@(gq zVY;-jh!D!J+^yv--RnTRf}))QfcGw>64_p`#U5%(7ssh(4`S4^z}h29m>VD+aMP~%}QJBg@3vw2`Sen&ssvd-45CyQ+)4dA=FXO|U=a9n`Nn_mRtyvW2(YpjW8)Sigji zM4tVI!MpMG=R6ZF%&*^gR*%`4j|>Tvwe{!+fs) z4w7N%zG+dlXS@25pzCpO5g#Kdtx&?<=7^u#V|}~&hm9W32KV099r^lFdb~k6isQ=0 zU16wD5^T3`m$xvTKV}VN8=t7xG#Kr$|9~{_Y_(|dKtjsw}%hj&6av~EH0rULpvANIvXdi3WP*j z>XMZ$?0MSjwCzOSflSL`W<7rsIaSNDuHVX1e3T9R`6iU+L9*Lz_gLr8E~|&SF{ZxZ zeRp@VIFTn|jphsWkGllk5oGONi|%l1tw@x@L2}lCfI}Q>PY;ajZh0s7A#~E&g81Hc zmQ%8&R<+`k{8*K?@ed>64L4RH{Y43ft5Gk)TC?zyr-DY{MZPN4SZ4RzeC5j>( zhWAu^Kk+vzKFF>!s4wsOn6Y!e{kas?q2E;vN~aDC8IoTb+b|}ZKS6lTI9H};6iaTQ ze)L^QWB%*ny#?!KE_;LdcBuAD3LZPvvZH^@$EVe{K=hTB&@W+)J&{pLJFXt@yt7Vw z>^@7zR|uCKFecZxc8!t z%z?CDy2&N4?`)hpxuE~v>B1eyV%r{`T$f%a;~fMsqiDM+%5rA3HE3oBH(&fM!^(S? zBHiwfTS?-Mm$96g$mL%%<$o&E|7MkI6K zH94*!K)IdqzQdAT)#;=Y!s%?igy59AL%sH6H)ll2Q+lpHrHgNWOR>#uN#}PFKVzeM zLQj3P_wZH1Z^l!X^sL1h%>1O3g{#~5Z7?b=`}2-%(*D&|wLPO=G#z?t{bxW);z6gI zlUf&Y?SmLoDm=Ixtt`H^$j351bD}v_ooGCwClY*II_P2UcFD<`t%vpbdyh&IlFMjm zcDl*?xC#fNj^+tr3=cT=czA_0dde8ABwJDjx9W|Pmk(XwRi?RlB8xoXY+s6?dg$c( zDm{%VhniuW8RjZ~r9z=h;A-TPPKz_^R8AW4p;>DA#tbC(v~UCITTwO&YidDYwZ`|aBdBq*-m40zi$>!HPWmEeAWV?~-cPdHlE z5TL}1-gsOYAftNUzU>;<<9qM7^_(~L`);avdp65~wPgIp#HSnwHL2DffesIaH_el7 zQPLk$?5Nm35)Np^VSk4==-dKm)tVix6i~(kDZJ{>-95&ohof?s)JMVj!h>@Q-wGrt(L|g zK0W-wtfKYK0~YSR$Fyh^T9&@p$Cc18rt_ztdl;tQAaIYjuO2_P zy0LOrgXddm)p!(l*t6xevAumx-%6#{MP*hz-+cQd7M3blbnD6YS6zSFciaho|03te z=SaVBukddNUJ8Y@J9iD-oxOv+QEK2J`0sOLI=cPi%?Iy}wPxqisvh0=QPMJXpk|dJ ztCuRIlPWyrLUfOhj%rDJ>#5$npb{$iWm*sG8&m8L&FLOkY2IVS78Bo-Nvh#Vl04ieNt& z-NY~Uk)`cVRU5Uyj&C%beQnh7&s~V$n1Q}z=bXURW~9QJIwc)IfVVcSu1qoWxo+Uqx-hWApP2@D9BqFDR= z;Ha^^=pNTI=Z05L(#GT$>{~E6_vz=y<&inEE1qpGqh3R-Y6~n*SN?pOJlyhm!KJP| z)H+8h`$uSgB_Y+WDtD1L>1>_5x6$s^vlqC$^-oEB+t+_4{p8ytyGt@db+tY2N}JyM z+%nU$Z<&AFzWxry37h+Esb-)1hNTaVw`k8EbeMl|%xFga^0x~fc~Yt$yaxlzsifW> z`p{i;-7Tu^NE79&J^^ofK_4O2JA=$=qZ>K=5f2`QQ9anF9ZM)>JEeapM0{SPxj^{l ze)O%r;j*Up^*OPJbq*p9V&77ZPij4=?;L)!Ff4R#e@K}@8vpZohqmXUg6@A#?Xqaq zJG>GZ@OGP3;X|tIg!m@YZX8I`|N^a*FPt9IR0fD}K5XbLRc9i(A=bVQb-;)=p&) z5%$u=C-?6w@6{+$Nm=fl(6;XOl4X0KX--4Gp7KB|uqpfomoee}g&31C)-db@UW?bP zski4GA^gfo^okPS;ok0A)$bYw$r~^4^q(5=eLq!_7`$)AwMgnKm3z@cs-5OL$*)S? znQ0lad;d7hx~t`y=FLB8<0IE>ioVf0SWw!kKaH{VNVJn&`HJ4pSgAMZO!{~$v{-Q3 z$z^Xe-5;`a)3*)m)C5X@$&rOKQ;riIoK}_0x&a{0y0YcJ^n5T_OvoIge{5tK zul<=)e*T8|y^MaX{wBfW-(7O95FQA=vs9wtZ(JI;in! z+jGB_<0n{i-}@{w$|f1H%|wjmviy@mf93cU;|tpN{D>3Y4IMA2EEHEVV!WMr&%TqM z_xLLPOkilr7m@0=d?=j$x!aqd!6Pv(N05E^GkE^Dgcz6h!S;)s3CU;fK1)_4zuWz+ z>POS}N{%||6j95__nw4vVvW~-tkb!UQTqrz&ot#Ve6XN(_@O$5B)8_7lwMlp!4vt8 z=lY{FJO7-zG243aYQs{)n-w-Rs zSogD+rK-|&o=jNnDM(aZH(5xabl%O%NGYpymRxp5b~>5Zvg4Qy-ATel%AQ?bY~-oM z$zO%DROj-(KV=ch;fe$}-f#?ik|I~kk#C&7?*y8{ek0y+ z#n;X1bn6@1vcR?z%>l)Mi5v!xpY`mnHfyb@fh<}->z%seqHThnMp_gEbfTX%46#L^Y%r&)Z2cqVcj?W_v4;s{w?>t z-#s!nM|ctUe(EG^G3x1 z$ErePAFYcVJMWCu^twyp{Pg>$ceSm>x|Ip`S;unOd1(_`d^j##;IiNL6k&K!I(Pe- zL}TTyHMt>`+b7&?zbGb_y4r7(JU<`xC;jWmBIQfF#Oq8rT5U@&ZyzjIq_(bnZaYe> zl1;o*cO-4VihZy$S-Jh2iJi~<+u~8TXA2A2$v)ohr(f?W%NQQ%n5%0zYm{kHrtcd4 ze|236I91)(cZnp42E!Z4)MQF?gbdBnJV+r;M50k4%_>t=cBxPzQbM?qM23=iCK<}T zjWRTs$lckdpPHwySD!{&y*KNUB93BCXRZRES6yLLwNke zIa`9y-5!#X-||y=vh@c!J%yy!>-+PVTUNb_1ry^Fyq{{v$Lz9S?Qybv@rv6AN^c}M zw%%<|m)S5RXX$I>XxDbN1%ZA;mT4G1aG1KyzGdflzbxD6CA&sDMe~;RzfoLq(%kh` z`i;9kKdlWlQ1MhznUSw0_xiV2$fDwu6Ej{kHz@8^VBpzJl6wkAsYLjC zoJ#1oZ-T_+Wh3t4SKT%{YCb!C&sM2^^H*Qz(s82i1LHo#{8yW^>!RkBk`-I>9q`Sq z@w;6y50<~TZa%bDA>;Fc3of2#B;2>Abh`|>kv9t8-Kv%6rYq-K4PBILB!9>X-77cs z75a4Nw4HcHto=m;C-d0Cm#Kj_&DQRqgv^mXGceP zhHYPO^4-iwXkbU+wYi0R`RkIth{Vm_`%?|Rz#c)65D0$BLCb*{?i% zb;~ezidomn4M}w;FZc=#d+_b7K!I1}@TlCwX9R`9TDMhtT>*d6LO*o)0M5G-B4-9bzRfN}Z?6whF#+3B26G`57gkFZav-mrsBEMv~#+@Y}!K z_2%9_>8UdzGf*GR>|Aj2XJoptsos_kyhqYQb&u63mv6|CzNj^2s@UzLU((K$P0$gt z9A!6EU1N#yz}44ICPn3B$#t$Q`n7Q4SLYEM2R|IQv3bL=+dKUV&n)Vn)%?A^GyGPU zgk0UiYbK#uyXV&h9h{#Y>|UJa>u;cLt*?DjgnLg%;`evY4~?0t-p#37UL8L7Mr$Za z4H>%D#$Pr_=~qWx+sH(@SJz)A4{jUuX#HWqpoAlO${J0^E^{slk3U`+C?q9kr+V?Q zS&*5exATpQv&*}ETE02R|L&gks!j8BbCO)8!I|#=4!a*!eP3&wzim~iPPU3aQevBEBGRnCDl3girLYcy2rj`2KMJ37OCmgf5< zZTpMtwm2Q;Nl!VWYjLP-@hkCj%T#KPoQ%9Y)cc6mH~E@fHpPb@zY-`-7-BIk=>7AN zJFgk7TD5{-;a_DCw@9(*Rp92_t^Gw8?6gXe7l_RNnH@50n376Ri^>7T($13yTTXtp z_OszuVzTMjSS zaBH+#jqdT#@VMbG&a4csxHR^%>eXTA)t38pHZ}I|Hu$c%%RZwx((StQ+iz#T3`hxe z`WF25>td~x?qd>)n?Ln4n?Jq(QRz(*#}l2s7Yd1dkzX&eLu1XN=OU_$j|R$>zv_?> z@;qO2)MoIJxgwc<9^(y%h*pjM{Gq1MbjNs^4}pffWt}-0mCA>#o9g0XzwLgU+OW8O z|i~PA^$qyJ3-etk}o3TaaUE_Q{(^x{*6=CuB}k8w`|Ppp1z=5b#{6&}BkaO>=O&?rwySo72F7C3v{@hYJ>CFSVd2bqb>vk!!?-$M1 z%XILf;0d*%Y1~h8=Xm}_-dpC)t@ljDA|iV3( zDG6`SO&Y2f8(gzIV0ouFeiA+9p0M4!1k31;JJdqYYR~7W?vX1lC^}$0_2!ULQ^mdHLiPJ4 zCrYx+=YNp7Y=`h*g8r1mh(G!PPo>9}5#8$4pFjm-7 zFY#7zq>Q_^u}IV{4n>)_X5$FZ`NB1kFL_S%FLd5`Jm zA@j~bWoH)&ZtyN-5_cqmD8^ZLPZ8y*6%hruZa_ENwl#>9W8FPfh7l zi0_tU{&PaV6$esNJf{!2`(t-V(y&kNI|DX^c?OY|Y*)=RLM?7E11~6L!9~ zY0S{-(gz#oFHTw(dhLMan39q0W8;Q~8XJ0YcxA4RHv}_}O3uIWy0q-o?K2a%e-PT5 zJa2s7SWc{?g{sw^`(xAI9BJ|$wt3Ca9nTN!QB!cdpb@B{*TEOxa^;4@tM}3}D?coq zUjAm{f+-0f`>#J@x?a^PXHizkmY}Ks)eX-+_N(}wtj`6H?L`qDYBMbcf5Pv$|6Zdn z@F9KLi#MKu{nb_4y4GIY+5NC0XH($j^0#OA=;SBV%IuL6R5btZ$P(47lV2(&6#CxK zGI_3&=RDQLFl?{X1=J}Z#&c4bQ0UMc`y}VNW1Ded=DP=9XHUF%LR?U8ht{V&*Wg3k zU$>n@oG#q3e6_~sg5GS$^-tz?J7md^o;4o5q(e^|V(w)!??a%*xiXtz(Fv z1&SJ%ieGQ{JDqZA?DTD8F06ZBGUb`9hNi#0(+|xXQ{6tb+tHlNnp7SlDe%W?yu7qm&du1d2Uz=;sKYj1OQ=UB4+1J$Y zh~hW(F;CTV9s1oh>c7HM<(E%Hd;9OTt~+fjCHD%v2w(f=#nYBY;o1)^51Fizh~67% zQ5`pYMgQXoYn0b)72WT8?WnI!yTse@`#YbAHhzEbs_{|PfJa#`4_PSBt`}{{6>(42 zIeW}}(vT6yw)_6~HIn1I#yIDg{DD;)YSt;QFnc<@d0(So!W3QSH8Va>k?rp(e&o@D zNU8D7v!(=Qw^yBWK6|ue#OT>_p~iEaKllhw9Bg`R*i~oV?>D(Y_D60847pdz^O4Uq zRMC^xTl@UlQ2yooF!m-NV*MnNL491zBhB)eSAY(fsRW z+7&--Yu=~yE9Q}(WV*fTcYoowM7Fk-FH*P29rk-)mB)!6-)(ocm3eyVMP47fblUj+ z*JNgO|M>CE^Vg3b{u^(ve4)m>SQ#|ZdR19-=RnjtW{aWIwb$d*dFyh;pJxMZkY_y-xtE8XppN_Xt&e zN+3cvJ|}Q*e7-Bbmry@Dtt*{50w#!1M}xQFA9puwOTDhfo!_q0qxU?|i)YdH%=73XS zddc8ahSP{!+c z<2~;2#+1*m7;|p9sV5^P9^WYsh6Zq>QO50f|Z6;rNX;!R@?|*rt-&a z$fJ|hsiMI8o8qW-z&gwYv&O|HB~ev<>RZNGW<#nUqma9lpGuWP26Ayer|Dln&{9DLMU6kC~_Yuo>q4 zOp=AE%A+-t>Nx@de+sCXq^EOD(jVs3GiJmtXXrx=lJrt1ARTNT?TW}CG1w8Afq(P( zhy23*SS$IJ2NoN9J0n1pk-wjm0eoL1XB75;2Xixo(c82P!Z+zb#uoy=MK0B;vfNc)AJI2i&TaJfgU~d1;2`|8aFIHHFN+MHi=3<-J<1J4vq+u{ z9*}ht!XuSM!!slc|MnM?z`sP9gd~P$i9n$v9765?vT!L1hIq%2EjnaCa~(%z!LWBx z%DU1c7ovpa=8w%{!s<1ATo(R6n>&15&OI}xFN*i%e1Wm$cq;fn_7hIzkhf`$dF61AdF58AGWyot%H^)R^>{h?5MT0`8(;F|z<ico<5Kd|Q|p+P(L?j6kw2_=Y z6)gc7ZJFaYKwsA>JciY?8YI!2=|%9|)OGF;m(~3Q1lpzGy{0KMinbEEA^JtyJ^DKg zia`JH)kQy(8U0cc#i5{IbZ6LT&~G47EP~z-DJCgXFtpg8Xj(5@%+eGmSF9{c646Ak zxb;T}KCwF=9Cs^g6dIq=?6izRw-8P?~B)+su{JCXh>Bt8FLL) z#a|(*4qyIRjtC2T;PU)a9Zm=auUiIQoIzhpni;tJw-OIRud}m+Gr zyo)k$8D8p5I`VgZ=@!sp;ja0PYPozKPXN-1O@mR~*AiKjUs9UDDD^BYbtXzXDocq{ zu2OX=klET+de5A}C>fMFgFctaNeNWnVuLbaRA5}zz#wj!l${eMsXl73h6&SYs>pGE zSr@)WNTOL6_`=+r(Hwc=X1r}#F@$JUob)6nL|UlP7VEpewTy)5vR#=H3DHBRGU%xe zyGj-AK<6^(DXo)b&j@QV-!fSa$vx90n}P&zG7KF|(jMIj#?$R0zcMi}lzK_v5i`tG zQD-m?gl}LOvvMOma+SSqoh{?uI$M6mlxe}kSD9;5Q$B}`;iy;T%ZZ_`cjb^0GA-qY zNlS2hISc|qt+k^Zs7Z-cxEv+vxjW1#zDr0zpbK?8sZ!>~`&JAhf*Ao7pz2v*1&k*L zR55|16j7CNp+X79lexob*Zy9(phgfgp70@-@f9*&OuR=?Y?6xNs?B8r11#f%_juB25ZKBg1v7;jri z!&)1tX)qfjs_1O8bz|j90!iOmsYAvHRtfns?QmtK5F;&dyi$(2Zj$v2%`+wlI>7zgPVo$;w=ZAJn|R zRP{I)hH>hV>H)yRDb;QX<&k#-&nS4tl0px#Zlvy29nWAJysB}D3H*n=e>FaP6Y^e} zYj>s^eDaB^mL!ZqZdF6?-@9GC1GLYbeR=37bhW))88t;??dub&|BM`E?!miQ1IZ_j zszS9l&LXVd@y3ag>Hvnq$qhAh*3!Q zSmNb|(Nw~1Ol<%pg?cT_F9sU5j|ItGA#Jqq9?Xxr+O=d^RGLsbp4p3W7fq?9?|`H{ zu1$O;C?N0!l^WKnaIa3S{Q}aB3#=?bI)6qjpUeS!cfMZ|ha_;eO773 zu&TNC%ned_heybDruK|7@`!rIbQae=`WX@;!*0i`8V#B(^KL)mF&DLBpK(ZBbP}Gy z&8qd?XG#E{ceLv^g&$1BmhUG&W9C$^OWbGNOWeA2l3tef&$UTle4L)E!GyQ!?(L=lUelio2de&QIU{{4F?VIqhH&{3giE^?5&#KYHQTN~MhuAHA|h5n+-sFTGB$>OhKTJ(Rc()s6k3 ze-3Rax;!sNKtM4OCjX$C#x*gFruL&vt08raOlBLGpa4mp29nJD<8zQ#Q$88YQ})$< zIF7?uhyq?V$Z(IJYziWj6#Sa#*z&qHXYnK)6(@8ixCx&!@(2%?z5Y!!pj$@UWT7*X zJWXW&2rC!sdMSmRhBxE0awU0T=>FLzDTYna*(Q9bC;r3gViS0JJhrK?Do`H!?d}7pVX8pNP z93U;GbearzSW;6Q^v9yDjUDe$w-`?uwWT(7lc3*kT1Q5(c}5d-pIv5CKEO8@WkyrD z)nkBLKWT!cy}f|K#fqAa5Cwi^O=Pj>W|TMe19DS$uir(<{itr@V|!jrHBI>c7u=?r zmuJbIX-~(?5-{f-){#%mIe&Xe*tvXvX+e}1{CWxOUUj`}g>Cc|)0LmlZ!sR;M)zxG zR$FejaPw|3T=U;{DQY-MteG*)^&i;06Jq)?T>c1hK^4keb;)K?=qDQ&4YL^BPZ7)- zgpcEuYEC0btvI|H?9PyGe({0PBj1nrj4T!3#du^3$5j&N!HvjiNuCU-6FKjho`ZW} zEA&8)6t9XT#uXVVlPk-U2R5@5zZfvNq=gQZ;0+wkrN`*40*^WD3Uw>+44HptjpR*Z z{@p*4H;eiA{z%?J=3h}o_`E=or_X#orN~>v{9B{QTf_W2aTI($JPJP7(a+kT0AkI|<@t>+ZGe*rkH5Mpd4L~2%05oAVfDVpkfnJXW zkjfYUZ5acg#4&$Cs~F7|V}anMv4CqB3pkB&e{u5Y{5VinIu5`?#{+oPcmQ7?58#gR zxZpNcI$s@d-t^x>b-+t%0NzT2rTno5C>PS?88gCjH358D6Tmf^EU=LlfIYMTl%d5l zCjDZaeMb87%M$T>uB@QU>J2YZAN;U0|T9$HM#R z0X|<3ln$N=O08zHKu=}@NLU|YFi#)AKKcNz(`SKa8vyu-0np7hpiHzJ&s0HLs2T#3 z)rKs&Ohb_SZOAhwrO%UHT~O;Bo(!^@1@M4bEO_TEfKN99F78IaCEthznqmx~oyNc= z(U@`xiWgiCT!xu|^0_7~xp)(hYc-)M#T-BVA!rrOD05T5A2wy--}&{-!)zdR zXEu=fJ(~r#n*-p8IRJV)2SC&3vOw470;q8=WpG{nqyaE6oCgec&ST-9&jY;Nd|TlOqT+Q!%IP9_EOsM65s!;h5@O%3`neA#*)ii26DfbQPRc^vh#qn)pEcG zEN9_6mjizK3b5n00!Tev0i+~WvcRq@0i3jwk|9Sq2Q0y}!|+w0eBLS+K4BH$+g5?{ ziK{888@8D~Z6q55SA$&6YL=Y#8jy2b19ErPP&sws6~Dom!4@EAY{8PdWdU+;EkI6V zEjZ)67MyvwmIWSX3E(Z30FJf<@CQo(&s@g>N3H{K^E%2z+~|HFFfmyVO!ls4;hWY2 zUcm~K+gQgEnly5zDXwxldLghjW80Ey32nqrn<@Xxgy!A{)?KVFP+)HUQXi z1AuRCV1b9(0(hw{HBOFACX;!*Eg0w7vhd~`0q?yLlooFUr6V`7Kqof=C}$He5Zw%5 zv&{g$w3!7K-U49bEi`P*$;S}3Yg>Rp5w}SGATPgFcPCRYMdPO^sTV}_S zOS1#HA9g@S-yX>L*t0-i?Ey4t8z|qqjVdR{5|ciu+6Kx8II!e)Ie=W81I3e5h)EbF z9RY9R2>1X;=)-r8z(Z?0@bK8q()W5h=u>q99@sasSBY@~@J}Zec+(C5U)@2C^ThL> z(FNHVj8ArE;X|AOU*`-;6QSQ0DN>G zfOGfJV35OqNzaXT1tu1*EV(RKA{Vru!pSMW1a7k*;Ai&({KbCgINbw4B|MgTfZQ<;mRzd`$f+L$ zGCK|ene>A!&{$6ZZT1A^QJw&9@dU8mAr?5|5P)AE0PUpxwA>W=~$jbkiO&@lj&AEQdiVY?)Zla7PZ zEyr2-lH-6MbOOjPIYH&{0lVHXm&Tm{x#klfr{e|PdD;tj6nOy;g_A73UME3s_DP!9 z%$d2cDyg0VeXCEg@R_Fo|N9j1Fgi_n)MvdA1RhsUgIvXFmYjh%$hmm~nTOs$M#6^$ za`gdFk`I7>(0}!P0qo<;0)O=d@FYLVfSh*s}%qQJOWtY*8u=l4WtCf!L_6dj|Bn&ZXgSVZ zvs8{8P)kMv=N!mQKF5*^IR|oe=YWjjc_8C-o&~Bo51`>epnOdbfG-CD_g+Ch!_>xGT33&)Wj&hYs#XB{f8@!3k{ru(|V5j2_emGNdgEy0Dl!&7E z>(4#x5obJK8tsSze0UTKFA)uRvuKF8e>9aNN46T(Vms~8you!gUofD3e~=r?8S3I^6;MwL5?DNV01Ca!0*NI8XkH>{ z^iBkDNg{wpC9%NXNdV4E0+2*9fUJ^PpuA)N_2+`d1zZ68a{=7QWq}vm1Mtav04lr( zAcYhb=wu3ja#H{_FcmdnHsbyDh zL%N2i0lqqog*Scx_(KoCt*i%BPUVHNHP}&22e~!rAot=aUTU(_VHj~9Qap2RDWsNz>ZNCmD_o25dM-#3lH0CSs+)H1#1K713Ko80RG^m0Fa;*SRas_DoQ2}6sN&x#+ zvcNwo0X(e=K<-rlDyU+Crd0!IPc`kA#B9|?kXlvM&@W;&Ec~7tz~8L_248DHxn3Q>nR+6OeU1a>!qKtdyE|I$cVk%KQu z-mYo_xzkN7xi3v1H|Yh%Gsj+n7w298{>cj#e$q?8ySxNDi7%-fIplJ~06ZlOZw9&f z%`CaZW{~^XO!4@L%Vl6^IS=q&JQn^V5AYNCU}qPf%FWOJ9EXUV3O>k*z5+Qi_3VBH zQ&0RWia+Tdx*Cy5VaRL1&w0(V6aO0Qw7sTy*S%pjpnu65z#n_V!neHvyyjbq_c*($ z4E*qW3;0KG0Wb9q<^_j$;6m&>Dsr(=@CD4$gWrRQ$$OTG*!N(fbP75T+;T9IC zr3FCaKY;S>AE@$Ex*O>dQ1k(m3$}vX(pKOT(+YaqTS4!vHkRI)HqiUF4fJZX1K6b< zz?tnVuto=fw{-yMP6vRxJ6ND?9|08g5kMb40!aT83l#MUK(9UlNc}T_+&;5FuRa4v z^$YctoIFW#;rJKuHRTHnulkiYlN?ERxo9+}MHn3_=iwKizVgP95j7@e?e2CMQCVL> z`rB8QwCy*r7WNGy(fo~C{eIqG5xURxJIJ|yXUR2x2RWr5bm)#$X`b@|#X0eZqa!~+ zAo&MNKFlKdu+_x^4ob7Lr3;Rv+OK!V=h9Sb~2Kk5CTxW5P&rMu|OC40jR1UpL{zkCk$XKVF1Spv%qp90A4KupvxivY7qgDkv-n) ztm)4u-&>yV4;pLw16V;6z}rPx8f!#Bqx1m23Hi1QAGV033+s z5J>{rFY;^&^v!9==B*2;L@Q0 zRvN|v`wRo{(_sJ}G@OzkryY_p=rkNiTpLd1hV$WOOd9l_kp{g@(k#6TWkByK82}Z@ z0BEEv3v@~rK#yesBrXS_b#g4wV>ti`|3{gT^9l*GE&lMJgBivl3#59R)fvM^RZBQGPJo zk0>jF%~eV)o0&?0?^dFCa-tyd$wnFQ=agCaZe_sht57^SQ;^J%Au50`Qvtk^DrAA1 zD)e23D)gPC8cUy>8t6+=1JG|Z0GW?wfl@{T=-X%rjP4i!dyWBc@fa3ZcPxNi#{%fV zSO5(g#{#*I15olf0CkQ7(8BR7Q1W;HeI5^Hr>O(jT^+y$>MZay4FK=apn>uIHKq=3 zh^sUpFk+f4{2oof-_-==Uo}Cwo)#$g)?$IbY5{n%HdRi}?jyZ)UK^C>X|wQ?Cjfru z1d1mI_7Qyf1i<&#Vc~b`06tEK;v1zL8(|@rnh5yW6IuAUiGXjNNb%%^KB9l|B)}h? z#KN~u0=&j#aMEQmIGH(_1=5%TplwqCbY}{Hx~H%}+ol32YAT<6$<;Cyz&g_ae0UlQ z+%gToDiqJ5V$o=nBpNxur88X=7%9Nb3wLLQcj54|S=Bx-3(zy3|8G z>Y*O>P>%)Dm`OdHNj;p&Hz%hM^_+hvrVoBGgIUn?2Wr>lt1@RDF-Q5JD1E-X(6OHv z(5;^rWKp9&Uz}Q7xb-vVB9cl!Bad7S_(K>hqgH6W=tb0_ z%U5B}MUqxz4kn%TblXhxG0 z{wS3827h10SjA@~iJ>?{Ae7ErdjB>UOl!Xj7D3(g60vX49t`&Jkx5|xSnNabw3{4( zs7g*R`zGkvEU)s1oMmzg0*K|e%RV#z{|;4qzOpMHx)%a?=nn+G)%(>bW2UjN;>&@DB-1z=pjEcI6m4VFI4r!XJfPZNpHQZCD?Z z9n8fnyD%_0&J;`UAflSwI@PFw7zJj3eHL$;Y4)AuEgN@ulC+C3YvCKz4%AZ4RlCz_jammp^l5_bw zBsuNF!e_q1uVkL?W77uN%;l5!92svp$I+d+unTLMi}jvH1LlDl?RnVY1^cffCEx2q z1J z_W9W04{Z)FBCaKWvDAsf-sh5Y|enV^GEzW5=AQu~bDElxT>=W!mtd`n&w#0oOTd&H)94WZrnnSILgj|Ql=xCQX;zAl zu{E>i2-qCQCDZ>gbqEQs|W1%%z=zQK)o2p1}E7 zDR5=E4Zmg7*}o^g^l?^hU=*0F!5T>JkwajZp)E8+ zirVy)iV{Pb%YlyGa%|Xc2sONKIm_@PS!%doId#3~NSNO1#bd--!ByMMOg^3L~P$Q0xlK3tviOF5QldhJbvh0%Ug0 zx*ZJxS+tTbO@1~(j+H^2BHnPJC$Z5eb3K13LMw3u7(UF=GGy2beNDu7Z?kxEXl4?& z)UpyR{alGHsU}CGrODCC=rV03mIkLqBbTaF{9A&wXnfK-zNE-#yl$+hx7dgy@Y$|+ z7>1#YRWuAeC)4x}L(uP=Kv8xzr6|2|$WwB&fGak~0L6`~abn;&xUI(QJ`adNN@6j} zC~Y-x?ih|IdC3?chtIUZM+W07gxttAWAvDs|A~E)<0vv(gC~5(gZJgyX5`LGC`pDI z?39TSL+|Z);-nMFVK`XJ>^_5>CmVyG4`42qYbY0HZ}Wa6C4L( zf=?FscW={uvTnAzT z2YWk?k4Ile8IU6(m~}(wl3gsgjL(J;Lj_Af>kq6|DAPU`Tn<{tqG9Y33ohehBL)%y zGEl~}zpLve1BEjeJzP)ikb@-dIN{K`xl%idR(P&qdg$*F5`7Zk=6-6peSfSls$I{@ zhF6}{`eQ4;4ATuwR(PF!kGib*YQ+6zGs9Z$;6eq>hkCo8jEt-)Eppxj6Gck!OYOE7bpDB{)OoZQr(QiL%-XV6+rp>0fKy9;Pl-@+P2=Uu5* zT-1lm#uv8WJPt-lTkw7ldsT-&3Z*&4Gr<~)25kk=QCo3q-R^jlX@_%m%~lX~W?IGe z;=|&YXSBG;kMKXY4u4`~*x;zObA*j!ayF0xc-Rjz59)(&voS>S=h&tL@WE+GwyntCu1^B>;n?_Q3p-J$Cvg zx@QkgH{g##_qO35BQj1)ZlhM^Z}LAOW)k64pjq4aIwU2Rlnbd1#zsH)F)FkZ1#IJM z|H%!`Qq;H&?{aY%ml-A!WM}26Cz6Dxn*mhPX-fv5WPQewB#L(ppkQSqy zFs%%v??h?EUeE}6i)o$jL+b!K?1XRNaXh23jL?Oudl09GRrkcm%*0&!!CZP=eGgJJ z$cbg&rr{pg$AyxlNj~+|ND}O|4c?3%?%=DD)#K&Kuum)TQ9w_p_nztw=B7ddh32LT zBL!!^IN6wxl1gOB6D>{!irU4gVrV-CFcjUKF-5JCRG`RpW>MT&mC8`mBv?Z6Z^flP z;jqK^;Ix-W^(B%#=j$=0c`~vHmti8SR*DNYmfe7jF;6vG8&XA(r3-70U%|%|(=t?0 zTLboC@lL)JF{^MpqQLekM_}{k-Zuyq$a5!+OtXJe@ff^I{$blYGHr2bs4Ff_7S->h zK_RuB$ku0JY8p^f-USr(c43On_tSvl-d(KuT|F}m*cI%eT{a&1yD?dt-X|Ut5N9`y z2Pq3h;$iWG8pHLWNIdrM#>P(OQDeh*vy4fXP-D126y(P;jdUZk0*=7h&fZZNfeiQ1 zgjF+**0RD;IBnNE3YXB)J(SV$VX{Za$}FUHBOTm-xd#|^?!k;!M5TlK2cyzO(9FH8 z9vTyy4ly9rr{3(LJ=9)b>HEL8e;O!r@1H>A^%sHt-;N||cGNyFJ7XU<8c}GIETGjcVc=zjhvs9Q-WJu-q5U)l zni}uzA@u1<)K=quh{5;$*cLCD+LGXAh@jy8e?zapNdtSNjF#U47^hbc;Fb2?18}Gu zH#xbbDk1oF4aR-Xl=>M@uOdV($29QbQlUT|psO;2 zwz{gASd}AZk+Gi)F$)MO!9~_CgpAzjw&rgQu0B(AE(&n(Sui=LP?I|>6AB)9KK4Vp z9E}sPn3CR zsVB=-O%&z{uGV{EQxpFKQ)uuZ+LxsM7qjMZ<{-mERDHd?_B9)B3^5ew6^k881;}|vzBJl)f!BdJ>+w7^2Y>pT@LyPAdY6zfaN4V^3kX-c$GR8ATA`d8fgmbL6J=F+mJRag2$Vf~{;(L6Z1e7i zuAal$_Rb%SbTP zkWw{N!+WejP;?9sz%}p&BCEij00>UUb2!7oF#rsD24MKXSz7#r7=w zBvm#?eLEfiNvQ24emz%1V&SRcs_A5RAm*RzbHKhSPuOZD3Ch30|)y zmUSj;IXi9bz3zXWebQbZvpC&{#k&qBYzpYmS^f}`K};Dwv@{EI*@EKF0+-gan2YB( z%4O6!I=kE$etZ+^z>($rMoZ4&mqc)GG9ARxLSg4TSZ;Z2h=kspF!yrax z;CZZ`Q&0-4|D5xb2~*0CFcGpZDMh6L6)Fgq^n(OiCq3aX<4|>xG_#;y59V3p8qJ3IZq) zyQhpF9sWDVCnDzyG;2sDL6SAc(ZdVSQ@<`?5}|gKM21~o5=Iv>iAPRAV*f=}2EE^b zbT09S>P+}w8B~AJC+-SnG@%b8os%R^a{qJnQ1X%xL}Wf1m9j2`2JV{V0$VcwY`Zs&SbnSfcx|baU+Qn?4ElAKGx4CfKqUoP)47UTH6f1k1S0 zO0XjI^)e)w!WA6#VPc3AiU+g#708$kxKYTfV_Y1e2Hl}t)pXQ#pu*fOd2qX7-nLG{XpW4%wJs}Ls& zr^bSC=-Bm4>w!B}5G7nRav%x~hoF$vH#`nkc8RO%v#066a)NK4pnwQGb7@`&WwWkh zW#0;_P)9tzkvbm^ofOQpN)}PW1r*!Jv}RXO!|y7vVTs5d!<-gm7C|Q|Zt6N?rv7MG zpJbVc&P32jYR=HTqA-cOe53^KMDT@?d<1ZlkHq#)f1)hRBcXKz(<=X+vbYe*>Jc+k z6iKu7QOMAR-<$;m8a;bwbf8o<3X`hF#VT(A_Zc^^!UspHA%}P0pgGJ`xde7kPEQ&_R01hq<()nDPiKu7-U+nLlR8pB`FtMnK^0 zrQXp#g@j_j$MG>(-g`q0B=fo$=*=BWtMrx{2oJ8yIS?%z3pes)KR0(PQo4ncza8U= zsiT{XO1#_!1ZMg6Hsy;(-J-q!w{mBnJNEpPZyF7z$#rXNg!n4HydU2RPq`}iOVWQgqz93IEPph)p;b7y z{6;&zJFUlML6!fxOO;XwVr{Ybtrh0EM`T3T;?o$WjknkTPT@ux!0$RXO{P#%jg*hz%9g`-iA$yc56w8r<8KwyK0NkLB|QEg zjZBSGvZ=u(;p6D`GY%6;(xu&lOPm6IQrwh0%nC+J;_2g$_r%W}68R(hz8A)H7gIVFw%fAEiVm(WoSuy4X{_wYR;MXcU*aICpXGZb+YSw1W%UZ*j5q2PlsV z9oxoy^hDzKs4-I5mFZXxzSX-&jY)oa_H-le&8R-TX^yVmqsAtt`yh~N~L0jb%m(o z9-g(tiyFkx`c#<59q<*-7j!Ze?w8~6N1-hh4a_VnhkPqAlDrRUb0z3LzT#U+73h}OlX*|G6N4}vxYtgUpbT4( zl^cqz%Nm7I169+)sF_pUh&Xxp!N&G#Y+Ne?EX>Tnj_yMi8Q|z4{88xB(I&(>)}-9~ z=~XelC}@UDdOX2-;o;TPqh(wszOv;c;^g4nw`C@*!A?x$D8HaQJcJ`NA+G7TO=ylk z(XT*i;iB$LU^3_thTjVW?YK%VtSvXj>RDLgF5J|o3ro*}xr>j$+$~0a!$o3_hsBxo zrdV{tc>1C|Ih-@??P7yC`6u z%*332o!C>(279{M*d9NE+LMfI5=PIn=&H;V*@abkG@hNA5awekvLF{;%Cu!F#H$VN zZF*Kv4xKYd@m=Q0hoDC+v1yY7HeGYD&D(dV&EYweBU6hP%+=imbK;4($NoG+Rm#ON z_>u^YEzhOf52(q@>@5ZJQi+xJR4l}7oya6oF2A>45fgpWX9M)XXq>R?&Qh#UM z+j0{KEX@D{a~}g^>&Mu(Lng8P-~V_+z-&Df`(=1=rlZWqbcWot|E=XRZ2efD2LiT8 z>It18rKDUx!VLKd**t+6^2!rza8MCATl|D>gCsPK0zTrMO%qgb`1fcFG)aFfr z`}L3UwAs<;jw1^#&V%m{4(DO@dDYHf~J?EVPr;CwdJ~+KDA6t3b zMXmVdL+)Q?T5J0?lc^v`xLFMO6acFZ=8B*QzH+Qd0cjQhepUg)RSdTl;^Ngu3ZMsr znbyF8&8XwQ|AwLhifv?CMI@V%l(D=l(vWP%?{#X!#!kBsUz#q3FSRPlqiuySfgWU9 zm4_4M%IIMs+(OjgR?bZHr4Xhh`63Kb?8gIJv-@AzfP8m@sDd4jEj2LH*!-$9B0d@t-mj{h_g<=T$_JinU*{2eT*LV^GNXwNqA zccXMFHTHL|g%bW}5)Z#nRp7q+K!m`76e^swR%b+@M|<(lEDZHW zu%GB%adK@g#;cJ=r+?O`Ma)u@kzH^=6l7>QkYYa4$5pq3pVT93z{3kAh z)|EUFcLj>!BnP^(eaYh~8F1>GI)1{RF-MLhwP^7pB@#1qNd0d+Cgf5e`7F+2dzt+F z|7=8#@5Vmg3?XmFJ|cF6l4CClv?Fv|*EJvv2{T*$K}kjyhRDm%a-!9n+o*A*Xd~rz z;4t(9)%Hc)lh8&QhV8hG#CHtau@@!!j)AI#9jA{Fp^Ad=HdS+wg@5o04oFe~_!?4F z>m)IndoWPm@>Dn|R9S&4X$}9STa@}dBS1B{t-y?zii|7`qO#meEky-Bpy$(|Tf&5o z;kKj2s?-!fdYKyaAj!eMq%rC4%SpiGNNs8~Qky2@6Rtr_hE|pexP10euX9p^mN3#Q zgAo_2aWWyuUfMaFh@eDkP7+3afEHN%_5dyV_5j_;AAsYyy_ED14B$ATwW!aDY|741 zp<^oa6d>+Xhv0eR< z^Gw9~cOwa0>cU8)!VEp~$=Z_%={=FZ1L@_qtw4@A&JY7kPf&M$^K-C&A>{E-+xj%ocC)3+b^FN)Lb^uF(5#% zBB)QRnC(su4sgPdT@$33kR|)p;(law;zk>mtM5UsBJqc@8W3Q`-t#XEipXU%&7u=#<+)bE9)1@q7MdPS?|P#m|C$5q4v_8xBlgkBEql43E(U%aYj&5 z<54Z z=yn=lR@4XS0W0K>bE5LcxyCzjKQeJ~^MK5d-o*_$PYQ4gkuW|G=LQ%F65RHnDNOS4 zL;=Tpit)hAND$|_N%FC=3=d!skmZ4b4=C|K!Or`5Y)OK@sRKIVGYuZ}nFjB6kYqgv zd3ORd6GwO<6TL3)9LbL6W0)QCM!dKk=yxMtd(cHrD)Z&zkZQI0MBqXaTsLeG#QZ*h zgvK!op{9rV#X_EI&1zwB1n%4BYP1ujAc7FOb^Q`{4oLkRrC zj|TqPbz~4%)HEOv0z#!ZCLjX*dv09d5XliG7C0yD%V_bOfCO{|X9>M2bi_eRi~#G! zEC|=4z#JyBc1hp?N%Pd2Kp!xbH`J?q*JW}t#jGR;Lw4fpuL9`nuY$6dfjOI5syXj4 zbKIag;unNI0&|!z2}uJ`hx~+C@O6W%UK87kL|hXB#nmcUDEkd=#ZNQgy&xxKZG@rU zY;1)Okr*bew^}epOuXwPjNWw;`Dz1Hf8I~N?GvVYtJR@TM1H|~W6Ra73`|5v+C{+3 zicWTjya1*F6qDT|ptV)J6hSLqipu2SNG5&6u0wvIzRWr#!H-E8jSdilWg|XN%pTZ< z@ozP%!u|K7@kT|2mU9xswi;Eq7&JsRLac(MQy^Ll=v;{wLsz23(~safqx!|OK@4R^ z#YJE%ebQ(Cat*6t%MLt7!wC`4)kB`j5zQgaaLRs##1Ych78uY;m_g_?D&E> zN>cpd4CcYxi~Q}l!rN=&$M4|3EXGM#(1P|z*+IjLghpV)S+)a~5LX<_nGj;S#3WAT znj!IpBq=^y0w{OLk%)($(rAj_@)U1@1XM0iD6tRJ3*hO}1T9PvH9Nua(FnX>D-A@c*lBL5&hs{bDwB#+B0W1Lx zx*-6FAjSqtqOpNe@2`PIfB%ZJhZPb$Bh80wI%B(*jBip7ICOO5w-n3*$qlJXz!)H9 z*#&d)D8(MwF?Y{?zA=T_9Sl>becPT@=vS4ZWfq8Tl6KD)x>YZp2YDFrG?L>eDU34H z@2Mimm2|`8{+kNBnD?Mv%+d_4IFNU*r6u5$dhHWK4h-}rpiT}FLjDeDI3abT((X8^ z#F+F^lEL?Jeq;Ek~mk+l_!gaCdlQfLB+-GSo5{gL=48_=rG_K&!)`YDNu((5|t_b(2|k;bcZEZ zY)WFr$zf8ibo(S5oO#w!ydAq`I63a530xp`^La%_f&?{bP zz~N{-Q#=CsWeXp*LH^PnMaW;?tLR8-D3=5NlVeoFDD`P2!Nhg2_Ix-5w}+Y2HUckJEU(*aw`7#&_@AOM>negc1L%fzMy>lZUY! z-nR$zsNJLcm`TK*(*O(c(9AyUwI4k+qx^yw6JE1JWd@c^jUP2r5E~Dj3c$umuX2rq z?HRKQuqJ~=MFu+eZpVXR=-e+{D$u##xK%bu!LcP^2>(^N--0UWZ$VW#HryoXeX62R zNbRDrA?#~c4yeMuMo?EB1jtu^YV1XB9Z>~6##Tp_Xse@!+UgvL=mKkp|0##~C2X4g zp#awoC6oFMFwI6`yH?n@Z)uX%GpAj#yBFIijb#Q7zC#CKS zYzDgH!|*aKuMSrLC2?Z$7`FC*b3uK13hH)n@gJo&LS9AvIq(D!Bzq95 z-aM#I3+*=RZpM(i?HN4tzQdV+A@Wjxnlqy@A0 zCD$1wBtlCBc^83+7#q-7!9VYPrE!%cB4k(t7;QSD5df2LA-Dz`GV$K{Wm*FSdU-}; zgM_vI=2mY714&k^oc<54Mu1Hjby(%LQX9wj2eH@*baN%3v2QCGgz|` zh5MQ8NNhh7zNX=Z!$C1e0M`)Le`%oWzYf0b1ZbZgx(0%F?8Tw2QJDYpd=fb(r4%9O zQ4vD#b{}FU*)uzE2-s8m>W~f)yYcQ2On9l0LuDj7hWC(e{QV&Z&@;61HF?2UVpp!Y zwb!}BQ<9D!73^Y15>mDBZLMYpkO10R(IZWe@Ij9?{h{afc3+!?9l0SEb2umh*ruBq z(R-@dgB!!ys!1dX7D#6!te~oU-a0^4mX8i6NyNth9iXZ{P)8XWJLux#0b4O2K?klz!4RDa z5(^~}un>ocI_MBlSMoh5C(N%+OZ>1_Ja7^$Z{%1ymc3GLdIbEO87;Y}2mRf5Nw1V7 zsys*!ZYa9)cUFaC#b`)nt2kWMOQ8X))RvMP@p-XN1UVb72k_fR=)ELKI1{ra!NGLy z4+AFQt`d}B9;?R>UGVC=Bo(3+t|x*p+`-mnR9yoTGO;&?9*GRst3Z4aJrNusg6JJ2 ziKWfb18y0~(u>2_HtLb52S5wSz2Q%)}DrPzl&)qg17Ow73YAxl#`5^p+vhnJ=CsFp8)hujOkOtavIb-ya+1`R>Q9 z2#d2Ty`v>8&RHx5TXW~kB&+Z#tg2bS;k<{{z=PzH3*1nICXay=Om`b(*$gV}5rIly zh#LGSjic{lPW-nmjg~ez0@()6O*la|f&72wJ<_+|Kn!>D_x%R&^dA^YeGuw> zchH~!La(|rQG&Fv`H7$;hJ9%R!{1Q(#L_?k5?a$FjBppv z>_ASN7=jLY^0*-za5n&Op?HQ0DX=u`0!Mr;OXcy*~(Z` z3%7VM%~%2e-OMtU0rg8Z+ZbjTkasQL7!M!Oq`(+8DL5wj9%u54G~ohOAuirT6m}W~ zx|T1+81!z0yf#P}@qU1<)%gUIYLdzQhzVSoYpuk@O{~sQOCzC}AcUz5GqXF*gcc?( zc0N6TB>c1xNzFC^`LvO3a+YLZSs7;F?E;f79EfwFNdIW!L%T+H1akSM+2OCVwVWM40VX*}^AQc^^z=*O=Q*dhqgr-*gCq0k$nr>ZS zE~WV`kbvRxjwF!+}TN=V~l zjBRT(iawvR=!SWa%yhE|=0WoZ3z!G(^A>6#!k9Yv4%Lc97-%9<7%brDieDDHNwRm6 zTY{NplH3x!i_+0p!d0w7Yq_69m$wtp5&f7fQ9mZDPc66!4g*&FU~z=Jvx36&hplu; zOu|!E@J?ozbaLe%%(D;hMC3v_-dk-QBR+X>vZTf?J@3>mv1CZiL`Km^;c!Z5^RD@Br|EMyD3O(Se;47-qTFt;Ui0AaT+ zTnG`8wl3hn4k)*8pP@t~6>ae%KwMX}#a^ed*J*TJ(JmzyM-n|~X9ydVqk{)_Y=hMk zB&q{jJ`59fD>%XCgx!n(VMzU82O#u+uxo~SwArak3-hR9(GL2mWyua~;t>F0dC86v zx%t(u3si1Z%0|n-SS$;X&@yXoG;rOHfeH*8Hy#Tk%nOww=)>Q3(309eb~+&YftVAK z`Msc2M^97G+@ByPJ59G$szoU6Q4z{BH4`}6;h8g;@ED@Y`fe&T=ic%eP+IP;oB@~9 z5SQ?eGcdiD&3*<@V2-bWkT`#yVTO=;L;Vniq&yMy#O4`rW?pmB!Qp?dK)k#0;;>cl znyDSoW@^Xtg*b@a<&NM|A9bj6gb78gcZ35vuqEuNBUGLEsqEb^%p2uf-e`5)+GLHi z?Dm6!EqU&^mD#QF4%#6YP74;KCBZ}po#=9m$I+meOyGE;|9~UvKX8`!52(5>t%X z*jUyqi&tGvw6@171V)<2*tHY7#$m__xW?|S(;r|NDCKbzPGA&qm~=9MiFP>Q<8g>* zc+LriXX=Af?!V%+7h#-tqC%c5JAp)tT5%#qt(-%nR?gSI#S!X1cRoRf|8nP*^Hv`u zbZ0C-$E`YQ!`WhqtciL9G z;EWb8xNv9#7ih{6oVU@Dku6OVhV_!md}Z?^^P&u_jRBw*wh^gd2qs=h9uo_oO z6VKbwCfM7A<7 zl&#GBDcH}wJ?66jZFVJB_J9~dCNb2%tbJB+kP9|G4@g3L9eeZN8&1AyohrV z19x6T19x81TgC;q{JrFYlc0O3{KR0l`|$mtkuSE2__k^Sf!gmn$!=vvKcJgtryscc zK)z|nrHyu)u?f+!eqt(lQQ&M;9Pv$tr#IwO}H1nP`TN>C+0gz1C* zaxOE-8oZ-*KS&Jda>)+@lNbUp-Ti{;`ggcm^dLwS5cgJZ5r3-*`b9EeYz-rpZwSH{ zFZ#V92;AMEnNNeD+g`K=funOMnRgu{=I;uER|Jp!T|?!tn_LNGH8Y4CrFju_5RRB> z#$Usr5f9g~l)X9-w2DjjdKCm`L4fk*yCC4Xtl=O5;JGK0LC|x{Q$ZRe&nYs>!4+O_ zysNt@iJWYp0jzLj;`GSaYS6G-nDB=!1il5u5AU= zjel%A{tQWwg1#hhUjac;7vqdQ-+iKE|F>7MMPnKTnN=ny0&$mRAhI1feBkg zkARHSU)bOkn0)Pj+km}DX9kc%Ouu~%O}`!da0a*jXb8a*x@X>d$p>*L4G~4;eq;Vz zBoaUknz%{W1Iz|R^-vrEU}26Y?1eRGX?T7D)}RZi1WLHv)B|Fc@Q1&q;1JmS2g6c` z5a3Y+kVBCL<-E=!WX^#~8UFZ&1qJ4$&D{w3{SYn`2@he0z~0&$wZgb9ZeVd6j}8HG z>yHWHBLVvx6GDaTP7ZX z)4=BA6tn-^TB4>;Lo}gzKdCQYgMM#(9s)K@kGn#qNo=v(Ft*XY5SZN-Z;se)U`q30 zukGj$VSsT+PyA~D7Hr#KQ!QDLWGmf*0ZteipX%kjJ;~BcV~@qR38n$g#sC93GK^XdI=(p#0lV zrdpNO%en>oD^OO5+^o2941afMz{{i zYx{E`9nHP)5g3&0iw2jsj4R$Ut|T1PD=7I)ML0bDXt~EihVKf%=?52TQ5ntx)8$20 zcmu+G@v;b-_$VBX4(X4>|F_}Zi-a)Yaou_Z^J8;3@ndrY`mvelN(DmYAN@gOKW2NX z9JD>4lc)=9f6_yQt81>82<9`0X#XG)1c+vcC;~D>W{e1X>#1?#Ba*F$SpSnwymQ2n zX*`{NJiobh?Xe*$dePSpKP#@84HFhWbQ4cE)ava` zUoZTZ7kZJ;pc}nJCk0_vHJL<$tg1gA3D1T>jx<_D0?=_MBlTfceHiTHgd7x(kYm#^ z(v<|C!1NLmmepkEx%*gngNT;U}hvpdvqc<62^-0eqP!*3T@Tdrn zXViaE$n9bjh=%&5C`KFvYIG$E!U_tCnk2!Bza9l)k&C|0d6+^@Ces371q)!XBEzE4 z$gt?gDF9YEaZv0Lwz+KbM-(b!xlkAGm`-rLqaYjuYrmC94A{0I%!V;a$aBdUFdbw_ z#cTq)+t{fa=n>6IF$VwyK=;4-zx&f(HzUmM>(W30qA5H}iSAL4v4qCQ2~@X2<4X_6 zfCMZ$5(D-yC?*HaRUAe!aWIRh7jJxpS=46{1G1>cG6rnrQH9f6)cfsXLZR#@@*F;p z`u#abJ$XKc037GwxkY{cLX19mr~Fb3bmfF!OclwFCGpw+&1}Ta%23#(Q_a^c0s*DHOzfu`L57S}! zV2>w~a3Ws>c_N3&W^9aK!9>tts`5V&bg<%wR#J<#L%Y5~nzejhN%{qhMA6%#kyY*>*M_ zg)X(4J~a>3X)Hl?YRmC@B(rMQA;-;c@!-)TRO8Q<``kC;dq|Sc?88j_mrt786I5{# zXb@cjOxt4m1T!2hv^r})My$b_fY*KEcD4lUm56RcA9_>v|Kof4f}WIs*s zO1k?{PA`hpyUn~O&oV8udj41wVI$7!e34R4*lC#*+e&Fd@y%|Db1$3a-eYXL=OkL~n<9Tfv*f4Q zq}zEfp1tqu_D8qb=xceBjkS7I7FM{uI~(oR^1#1NP_C+!+x~)^s{HK&4_9e5b2g=> zUIvqa-?6-x+ocH%WS6Z8+m6VdbhXw9neXggy56@ltSvWI?b1MgstK9$m8e0NoNxX< z8#4a<;r;K~+1*B6E+av#GECDQ=U%jV|e*N9Tes%@Kx$f29vMRGmLp=5g|G z_2WvxFVY!C5>Yg4B??iN?0acKS-ziYG#VAUVQ`hYgfHLY@g9#wz0+*v^mR3Vxr2C? zXae`XYk4Y`$nZYQxG^%*_xtcS$MVI@!TU#O#GAg-%e@rMycFZ^aQK)bo!;@KBeiaJs%>JIH)($kU2WkO`dxP?ig+}z zbXWfKZR_&JyNao^$94`Ah_ZF4;(*q}nx2u(`?BY&MALf(9z<1%s{SsPZJ556*J4un z4AG?#9=YoM+WF=+LGDO8D9igujQm$9FzgQmp@D#_(xr1Sv z5eVl;55>4{Zf%-Mf4}9`x#ecW;%})uh(Xyz+tJqc-7d{-s;n7x^C) zro4R<6!=5FgpZwaeMUE;;{3P6X(vC?7^QXv#Pm?_X+5058{A&>Vq8hJ_>pa_Ksi_7 z^L2Iww$`anm$l+n)=NI0Uw^patHx{dzV2t);=Q$Ng>G}mk(h$= z^&fURLuyZX_=aO9@_oXVMvfZZkZ}){D!EwS5Akj&+4yL;(aQT*FN-w7^4@F!WT zoYd)&@wMvOtb?xyUwL_cCMsu{7;o4IeEaCAaKNx2r$(-aW6eNK%Dp1FY@6jG7-GF^umiU zI%qR>=w{YQ&(%eu+11}N?e{mHb$<{x?rUHaIK>${6J%~BKrQXFS@QhzA@cuzNfhtK zO30I+u&u=ZN;`x%o0|S~(6nnQ(w#}oCzB=}Z&$<%wCU&t+6?Mg~m1od4jRJH|K(8guUs926RI@7R-=CbY1D z4nLCwyMchj1KOnt*dw-2;FNfzn<8^nEizV-jVd5|ApVJc7S0KEpW^h2f%t=vLUd)uGxYlVCLT0z#1{1 zY*frAXD|YXu<< zH98NTw;dlE$o+y%hV{_8-7$F-z&h!qJP6b#IS-z5V>;9~lp9OXlQJ7XRB~_T^YmzjMo8?m>(l+_vi2jF7emNX4=NcvSAkf!m^8wI4r~DHn zwll`qgoJ;e4A&v)kzWKae1B9#yh8d-%SF%)pZu*8)V@sSmt~k0p z7&4s5KjJqgrGtp;^yM*px>TS4TqKG{$5(XrF2lB8OhM_3KEFh_89YDant1NrOCCWM zo!~z|GFvak9(i)Lrg*z_-^ntk;J(l2sY4Q_I%d@m{kY^e%q$NA*Di{)=OpQf*^~!AOB~MD`FQ>P$mojTnDVH;q2npT=D9Z_ySWGw z7Y`Sw9vO4?AH6X**z;(}wEmcl`Frvi8<~EW_Sp{E;$3Hl#s@DbeWkKD^)$a!RGgt= zh)N9v55D-e|LFIKIH8Zr3Gu!gey>Nn2Hws}cDOzsblHv!nbV$QDZg)H$Mm`HRO&;q zhhIcT7Vo%C_tJb#RbTU(^DWuMn=r%EZK+2!KhXV|MBZ0uXV_Q+TBti;&2fiJBxCvCDuuOAzs z9LUsZj5Hm6P|IIi+i2k?%XB?b?oTuQ*z!(Chmn)@&c#nYv#8k48XO|W9+Q%=EU$9Q zswP}mDZCWm6I3suJ3XZmCI0P7SgUmXRPxjrbsndf3gWxK*Dt;qi?iC;e&?`o;mu<_ ze_7E`b0$*1c_z}Jc{VbtA>+}P(YjjNN51*r&TY2(t+=L3GzQO|YxaHhb^fT#%F*!4 zOC7a&^|{(h_iW1eUFaXNzExU2Jh3fL+dBI6IHB2Cge!9?$gtQ<_6X0Y~fLFxRtVypAt!(jWHO6J0Du|A2+| zK*EmgUFsDUCtpuEIvI#5^myNhB6LtX-LRwB{z-OWJ@<<(GMd5N zXgOp=s8}aFk7^No5z!j7=9qjg_u-eP2ga9`e&}ZXcujL#pX0;eK4!-ynux~O+l+5m zy;N<3;+2;M$P%i}=ia6>-ClkEfv>wK?`2X;pB~fAm6ZnN`JJrg8Ckw($5bnp61iB^ z&-*hQPKyluJ;54A`-ne(_p;xjc?+-Ez4+|$x*}Twee};=vcb2-4u8C^UcoeF$9!=Y zPe(hW@Z5k^%zRi;zEJnR#ynYFFS(Hyk7VBKN!yfG8S7R*FF)|B&inghM!&VjhTq;% z-fA%)%O_Tq#)~d)4X1dRj`XZ;KlkBPwLwbmkWH@BRmu;|GLKk7S!{e&slzCFlG*CL zyEtj6A}t65*UiYUYl?1rVU-E&6#@t%c<^s!`2OF zpZeVg!_P&2aTWZwyC(T%VPdK+F|n*g?cCt$AKW>$Gy(}e^m$?lM}3bT5%G>l4rB_T zW^OJQtnr>aL{zhU6CvDp!sk}DXhVs!i|7>t`-7Y0spfIRm!7kt2X>b*Gf?=;pDCvFZ6QORt%)T&uh% zmT+-L{svjNqE?XlNv_(2uWUWBDara9oL_Wg_=YH~Tqd9IG7caT%PAAs(gQ*wokQmr zU#<8?v5Zh|>>~J`ay8eG6QYbTXFB9sv3hi<^W;_g8%&3vU%eF^eOFbeWWbX>@MI|e zxWyS_oJ@KB#IEJfpX`U;wyp-sf8vi2KCml>xG_EzoUnL^FvyGQ_{;Mh`YkZ{_MNiz z*_Wug_+&*ie@{5lsyJ&KZxh_2Kev5Dq=s%Nes?q2y#-W%wZ}@hf*B`jxwg5Q;GJ_4kXw!Pw*cBDQ~L4SNw=3rr+KSrLSKFC;edCkC|~Lj7J=L<`SbT+^uz z!t)kn*4~ZbVxJy_FOCfKuM(=|)`rG@pLaS-#*c$VTutvYBd*bgq`Rtt)$h%zyCW`UN zjt7h@rR%Nf&F?*n6X*^2D?K|~QZre8j zZ4FggBbWXDv>IxpbmfHCOb#3~$TIKw=$c_;lOtGLpwv}En7preU1h}Vd;&*AY1%28 zEXGQYJ9JN-1{Rd#-aYI}G`(8lB7=ANc-GG3YW7#P=UBfgT)Lk2*(RWfB|~Mu^PR`W zf{i)Wa|WI>)l>6yx-pf-mx6oEnbc0xjbC3s8~d6{<|wCh(+h^U%HOHHt~$}~w70?o zDRf$%J!R@oEF=(}It7(b|IV=3VZVVBD|+$nHQKrCNy>GFS8kOx_Km;o69_)+cUC7( zs(;41u11*OX~RSAc-^5ZbkEPk-7vrHRW)}kRTn8p%X_1nJ7r{N)}CU}Z0WWhdXhL- zc||7uS&~P9Z~jKxs-edHy>>t9-&j%g$IX1{o=Xt+Ajgni{0y2qdRu}za9PiU6YgI{Te^#!l!Qq z{p5R-E>ozR1X75b7*WWYoSJ#lX7ORL=5TY(-WsppoI@=I7TBzkuDs1s`EVSETcnlL z!bS9H;!>IK<0!Ys{@F5~g4bn4sF&_f&23g6K7+FWbb_OpYEq zMPWWKY*Knk$xg-X&KY*+@Cnvk-HyRe8Kh}@lcYx~&h5;P@$-s&AS^50ew@qVg>bZ# zX!`+ViMNE0U`xAmu=PjN6|M=Zos)xJ$vaQ`I-mP$TR&I)_776W#aQK`bfsLj!+QVU ze0Wf?^Gh3kP+^^AJLGipu>|)w@e2pcO&Y&APHWkX-X|A!z267i=c6J)pi{tz^PY;pB%q^t7^P8 zB0PTPonxk_)IFJqu{T5agUE(Tdp^H=sQhBXVT9$wx}Mm`K*#R;M-vo)>^>@!3 z7x59wj%#ZX_{h&YoJ+g7J4cl2h9XVz*SIoambA|&<@MU_3CZ5)7k~HT|YdVnc&UA1gAIjjqp_(c+s;;Gy zB2AN|@ngZYPV^FG`7OEr>m_NK`>7w%O)cKuHmRSwD#=G6n@VY*wqn2j_05!Oe~375 zy^bGc6-S|R)H90efqfE;0ZbZ?l)8>-6-QX@>2cqTrCZZIYh}e!V5_aquYy8$-yY1za7FU+_Vl4Uav}S0wXXH{bbgb; zRi9Ctq0kh?p;HMIO~F66IaVr7X@q?LpIhKRKZYQ2H9v-oe|al%$Nnbf7@Bk>^=A_vP28vM^@{tIRBO0XFDZA^At{T1 z7!O}PWL!6+qdzrKIcJX^QsEn~tKK9`XpL- zi^|9AHl**3UKRZ1$Sut|HS$r^$aV80dycLC@E`J-A71~b&x2aOM*!4%;vaz$^p8L( ztp{HCsiR8|!u>2&#Kk=P>{}G^Ni7ABs(B!#!=PmY_N$qtpj8-Wm1=?39-mhVx64oN zm%`_M0G(oKDSYlnVP&GV?IAgt;}E!9v_FMiE=oNpMWr4*=y{0i{&lLXgdThwet%Iv z2RyFtb14H?E-zin2%vicnb&>F;DfXhE1yR2FZ*pZPJiFBo$v}_&!u-4kWjh=5wtw8 z44!_}UM_hDJTey4;7>>!?+7CZ1YfP%fZb!3_eyMUnLsQ=o6&B6S?`)_4_?&Q$WRa1q75|d_ z{_Q(+)z~geso(UW#>{7#n#r<7=iC`9B;Fb>Z0@*x1oE8`>pkVV6cYtV&Yp5ksw)D1-2FpH)1Gn(!nHbc9tOJ4GEFi&zE}RP zk{?KRvE6g?!@F(fZPDHPp1XYN)7`SoI^?XUXL2RsW~^q-g{wo&Uj~2VJrt9d-Tnm%}Ms{+|g^B+3<17 z5}}eQmnZK*lx4~V2&@c8`piGEmRvjjqRLeB0ppo(mT9JapM%amKBW5T+r3~i-TuA# zF&Yvb$8L(}X7h%OC!ch?ryKfHLxN{pId8_%ZJm!(XiM%$8n1ri4LV03JI+1(WHdXH zD}0^m3+4AG-2ETcTkap~t1T6}DyvJrv&;LG_QtgBF>fc<_ORU+yu#$GukRnDv+K)KsSpo~KPZn2%90HrqFj*z;3shu%z z7I?naKN+q9-QqR!^`E+)b>e4oLVmS)W*YVE)qJ zyUxYJ#dXF5CJGKUCh^q|3NKD35hBFBJ`WI`%KFXn%j&N-318GOh@?Wh73lu{nVx9L zTv!~bF&g9av~7`Rw{0(Ei{OsB&T*#nb$F|S ze1+hh_TcbO`Pk#lv<~V+Z@ZkL&l&TMi)SaO`0<2#2b|K>jvgaU_?{K|mLE@eTO-Ht zYb}m|t#+4NM{R=sZc81WyF|QVq{7(LCM~Hv4V)pUnM-|f@vAl;8c@58- zrP~V1XJ(;OVQJ4#wQ(VDsOD^Bg*a1v0@mcnIHPR@Ih2?yy1zN3D`xyMSWU--N&OfP0qlfIh*p7WY6v@`7G||7o!UHw5yA^-6t47A~Y8@ z#2d!*C{}jKC)K$~KIfLvq{=12R&~`uD@#sX3IV`+xiq5xM7WqN+5+6CS?$ChwY$ zS~zPFv-p5UjW^>#M3Cc@Tb2F5bM^v4_Z6DP;5QKg-Ib~pVsn%GF0N+Ha!g+|c-VL} zyl%GcL!#a7Yn@2dsAqx7Dx>=C+2|1tR(C5+SJwnJMUC(8d01n_Jrmdr77T;W${v3D zmif)XN>ytw-MDp|OB*U@Pe!kN|0H?S1sN+%%IZioXl$AibCB(SRI`{U`kNrX zrkYrB<7|{U&bZJhnGveLM<1oRiJq%FK234+c2BT-MmEg}>r{nl=8B!O!?X%b;R%oP|I&7T z8qN3Qvg#F5M4e*eAN#%D5lPlMW>@LkLhze-qQCY+?i9~-#o?nz^PHvz?I*vMc3)z7 z`ZK!E9i782OFnU&_Uwwb<=t;7HCtI!`9kqLZIP&=c8r?XrW3s|9+S@QPrFVh*0AGFYw~^+t5&ottFW#n_uLdn%atZu;&g2F%61n@Rb6FQ?%bR!Y&VL!<@4^$C`1?*Jv{8V!-n-!b^Gk8+LScveKx-X za%v`ylq}OFcWn1-{1y-yaqX$w!F@UPwH_VmzB$`v7WK}XJ)B^EWv)LJxTZMr7tGOXzou4h`jG<$(ghD`i=Jh^?#zS_%Z&ElUMhj(ozhrX!EazgQ4y_DZ{T{z#r~b*Rjs5NyoIWnlvAt?|XCo^4RZMx{ zI8~X#=kMK@^Va4ba};;q+Gu~wKyPyQE%)6QQqetjOpa-Uk8f-%F9sd+`Q7^Ij;8s! z{daaqaJ%G}ch50Y(fEihAI~((;(9Ooq@!k`z@xUvAiV$eK974BMpn98c`m50R6h$S zY4{nqEmirUm*$5J@}J{5Ep2(DF|`^Coi6}h>))DI|G!192NOZqMY8Jkyn$s<$jDiEwE{cgW%}9%oA-1PgXiY-xv#Ef=ffPBUx%!}QOZuSo(hlj{cCgb z%f{)V@1`0a+b+_UHS6wpP76k%9AN@#qhSWc9;6PQ5Gc zzGLTa7x-4Sg4&xC zY|#fK&SWS@*j>5xIQ?LFMi`;}z+#t&5+nC^#o$0CE{0BSl@}8RQY?!5cqbdh>y6vGeQ`TN@@psOh8BJ_tedNOQ ziHZ8AeV-r&pBDEAt=;a=_AWYW9Q4>rV?`IqJB3cZns6vjr^<}3)dp>#M4ASpf=O{7*2cTHl zx}Dy+$^BE_k)7;oJ2*T)ya-QVf33Zj_JqqqJJ+gi;7pU4_^kg28+jgQLJ$=#HIJ3P z&-3(D72V9D^_R2_B~z@HPqoDQG(sM@YSr|F9kvlt;g+>|zPwYDcjt{JF$R67XPM2E zYIjdnynT8;2K@~`KZ*XaG&KdqV7O}8a>1;|VWkFEE6Vz2N1LmN?@ zM9YoOR6%N)vEpH`-Z~T{eIx{`sAw#dx9aVre$~_(+wqFH*-BOaLx@*;LxrI>)xv0M z;?~k~pu9T&>7cKhPnJ)dFfX)C8SBq_68v-R!$bSO!*6W(#|@XPJ%283h)pyOcpEII zZ#=q{QSxJaO;PsU%5ZY_0sZlfz}2t+XTNN8+GAJG;d(OcY1VRSgyBFr+|XJSS1q$) zyFG(_V6%u!I?PfL7H^MT7*S&hS;qba1_g&}|8OPSh{32DwQI)L?eiu~s7jEb+1V(0 z9bbd4iXgvTj(5-4J1&1W?Maf>KN|aHSoc@houwkr=EUdy@p}iZ@|nI~v*(~Bs=Br2 zaqSXa>>8T=vJRF6W?4$covAL2!Kxc07xxk5)x&gJ!nxxel5I<< zd^}uYDHS}6j!V-WOsEqatiGSskuHuJ8PV@5$UJsPmWI{Tuc4`T)mPDh&Hktj$8L|~ z_YzoNdGb^Q7=UmRsb7wo!PhYveXWyEGxtl`arD{6e8YN%HF@><} zeJu@>HRW|CNk?IQWgn>l5#0&G|Zip2(Rq>Moo)p;yrRCL}g+u6FwW)paG{ST`cFJQYtllAbnRPLt~)%7$ZUOh z@nvX5>bm%|FCT0Xt`qgQFFR)UYM+Dk5hKH{+eORzTUTTVZ*&g9goTlC##mVw9gQH+rDB`>j%=8T?%)Fv`^JBpYU#`UH^OQYN{$9 zRykdIqCZ!&=uJOWus{W%-n6?XGWgMo-%7aQd&i&b)QQ zZnUaj+;)G|(S561>%OaQnck;)vBB}({p$=CmH8gKR9SXpnEB-Q^5yUEp4z?t)QXBw z^_BkPHWv={-s!t)snO`dyMJ`2>5Z)P*@p!Vdn(+>`;9uX$%m-Vuz z;rBDkcLz$X^iMm3uWdDx^?$8$QfmL_!GYSNXZvdPzbSKBA}{;;fMLGDL84j_@0|~? zb{Ka)vVFowk7AeSV|6zly(_os?OT(R7s|`t-WZ>|cDC2s3rzy=x38+de0fxhtpEB{ zerWugk?a2{wzxc%uchN~@JeeyPTd_(`BjzIR)5QH*s3LR>(`n5lkF4Vgt?8ca6Wi( z;J7}D<2Hvj*u?r(j_RX$LTARjBgwN~SouWPiiGa7_W2t4^YYkoas+v7kB z|LW`B6_d;?Cm6iY+WhK4m_zR6*@lKd`O}>YTAayYPDKr+h)myn?Clwy*f#{CN*KDWV*Cjpj2!^ zjLYZ9xiU*O)&D-28X2)jDYmzK#fHxlXWLsm&=@(cxBrMR?a2OThB%yPZ))uIcglCQ zt=H`?6$?h4O#SR(a)!8_YX`t(|v111uKm3kOe-27#!zPshX;!Wqy*oVJ!-u~|Di-P(s ze4By(huebFPaHi~=2p_4Dz$U2^uXN@buEhrPPK`tysB~0IV|6!Fiuq6R8^4sTtB1`RnPZS*l`Gw+JZo6N`uzsND|!0qXZ9+{ zI~0X3e=%zG)x?_*$6hb4sXbD$M*gOE`tSt z8vLWg-t=$9*KI#7dM-Mthu^8H&)uByOzuZp%iWb9;WDv6>hhKrD#!Wa}l$-Xg z`E~h|+r7fijy^kf;HL%!uh#0)*6^xx;|~qSSBX;{_AHXCwh7(qy(V&Px~uN%fRj<~ ze!;DS_?A-H71Q+2So+$GHyCD0 zD}!CDYvo-|uh+&`i8ZrIRP#s7-+Xn$sO)3?>XcpQN_xOmVqa&qtygxuT)4RG`}hgQ zb6N+v2nK8`lk0EsWk7%L!*&+gOJ=WqZbw|lY3#LjgXd^}{k*{9jkg3^>x#UA3)xw5|r$Ajy~YM-$-NN zg~1*c=dGtbz1`faz&ZT%$%Ue;sx{}Qtw~z5yKHjipy7V;It|IfXNR}PuN^Z#@vSM+ zI=dsh;9T6mRjW2nYqq@UKWu4^A4(B9HhF%UpYM^#C%Rk4jyJ;ZiWeroJbu<|UP09A zA$2QdN>g9k@3l4&*|KlE#`Y&S<S?yu$nJ+IHL5YsWrKZ2R7ja&}#{z&}OegiP=e zb%FV?0Hqm8d*58km)$jcjLN*a?J*a`bN#o7h_{SZ)z2T0$C^{aXFAQjJg2Oo zG|W4x-}$|PNY-(6L^NDBK5v+nxzI3Eu*mmT@pn6yJ(k;Y^(Kw0Hp)3M!X@y#a&BLX z^qWU-DfP9!h_=hzTD`jLQ(&qvGsfwd|Id5Ht2a%E9*-<~oqV-EJYvcZeOC+Nt4Uks z1`bMIajDlB_28Sb%U4U3so0N-d7Two^?QxJ_Zp2~60QY@b#xTQ*%#;amRX|Fe|h4H ze!~_=ls4O+wJprh7-V7$C{&$!^xbtDQWNb$a>REObw8 zx2=$Qb9Te79eMx6y*ukGBj4)uCGPz5SKB9tN?SQphFL|oG&g-tcIsO=xKH%zN|k`C zfq#7GZEKr1e%`ltWAhg&7##L4tSNkAw|jDm`>S;Cw~H6`k8wIvSGsko_s>u5g%|(S zRIX5N7&mzNmREl+XxHD5b{qPt!N4PdTp*7Aq-jxMphnJmxEQ*JErIEYwn>xBSfctF|KO5ey+VOOh418zgx;CD^lI!FU zzkt4yOC`wHk@Q~T@t-@^SKw2G6M0ccng4uqJl#iupYIkA&t8AI$6uxoC9xd5svNy1 zUY!<<$#F?wWw&#!XFi07qp~{^;Nhsk&IEV`nA(%wh+obAx3rF{R|4PFE75Bsap}D< zk=794OTSG7i$?Dfw^NG=pAxA>-=xop{G`wSNbjVSPfhrpPIgwoMVT;Vq_!D<&-rfi zZCWIa-{f{1w8(6|ZQ1FsK@VHF#J;mjwdd1R{6VS!xj#=)^gX=iHvjORJJqR#i`}>+ zR+D#wqy8*#D9$Je9J*tibgfh2>FG(dYu3+5T6TfR-fK=`rTE=*@0uX{plB_=%*!P9 z99Lz9zi0P1 zC?v`D<+beVMU%o5`UHA%Q3{kNpyGqvai`s^NP$<%*6^bONP&!-@Jbn~Op&0y6j`4F z5ie>;IYFJGuehBOrqCDg);~|-uYaC;n!YpM9-p=XK7>2dw&yJ*s8327O;FF&w7Z?} z?`_UZBjSAH2Wc#<{f=AWY6AEHJaXNR!+>`EPAka z`7)oRb$n=Gh?W*njlMd?X?&gHd;92zh*vM92LqwQ{gn>D&31>Y=`> zqyN6dI593AUQzJ4lMc>bPfGXcbl&z}I(6P4J)Lime*bwraYZ>S!vt*IF-p+~Tk*TG z*5gqb9|&MybO!zC58ger*@F+c-N<-E4o$e35e%I+O7{(}bVWWneNO;??ttuvq>TJd zvLDkjp!mSzPo{+r@ExVKAVCG?}|)W;d@yY|Bd0p`XyXsz(Zy^ja}qI5t` z#;i_ff(n>2iJOOFr7`qA%>bn4iJ|_58LUvbg{g}M44J(0j2`fIWkh^@{UI!Ip7qGz zij32piqo+IR8XHmQo}d2Ap`%*0?UB;!EZ|UaqOX<1yi4oM%IN+bBl&ibH3Wcv+%!%eC^?nk}V1Kh8>T-K;>KK8&UAm z=I?!v=u4aJ-j9MiHB32xEoUEjL?dv@_Ywb;@8b$>5)P*ukNHkFvKP}I8AaLrjG~;q z1IZENH#vOcH&14hAN<4OPt%u^KW46Z>OlXvp7iwCD$+7eK6e8B!^JiC^(4}=;#%(V z$>fjQExC_O$R8dWd0JD+9}#wWlY5Xq>el7aT06edj(L1($Na<@q&4Gu{zh7lXvE$8 z6+P%1HOtj6grNO}m7~zzd-+1DBR&6Q5BhER^1IiI+mPI^B2E74tbDq68H%sQzkDC& zYbp_JY+b&DHJ#13@5``|?;#>G9`Qb6#52PJEij;`co-L7B?hGZaM#U!Q!0y=PAs6E zq%o<$kJ7zqQlLj(I;HB9rWR+ib&5NpCc@-3rdoRarM51 z7pZh+zd|J<&99X&45Tg2N`;;JQ2cS6;g5#$R6y%=(ZcsX!tqW>aa1e6=a@o?i$q+u zxG)n)tJkLQ1JXS!3Taf{RTO@OH;DF!{VYWRRYh8;Z$lv|T17}}@eLXaHI!&5`jtLB zJQ|0>8$z-FTj3O-+5RuhUxhSz_%32aI{&(@#ZT!`q(P%vuTm65C0sR%z_DYRMaO`d z&#FXwB>M{+G|?%d)keVlhG9(D&$MXqapFy5UeS7xc~um90le8$TtvOGD=DI7FZjMy zz<`f^m_dzyZ7bRf-M>#+!l@ALZ*I^dbfFEGCPH#6=tpxB@G zO`p7CN6=#9IqEgja{UZWfFZY=WYGPuMM|iqs5p@*JyBfjMwBL16hnO|{mNqCva_`~ z*@{5)r| z!Ytvpn25-4v~WLmD>I->nrbc#E(6W4PL{!d*^A2bNhVTFg=fn&V5nF*`R(rIi_1oY zpva<1MgD+vK6l=p`U2_L_BdEiR%m0BovM)}_k z^)t)aNQr1n50}6Zlp`ZWi?6<>ToIz3F+}_>D0N<2E~LIxpBy!1CdO7EgSVy1{9O*^ zOQtmB|s`iAT!$6OUAcYSC_6=6&>v7hV8qt04{1 zY9;>nYZb@gOE%BB)B3~6j*F}KcVuhwi{mRaVPreCNYxkOc%CQm)Z}YEsc00VGo}5` z#m87y23^5fDV$U-hw?sFD5A-}l^2mjK}A2pNTr}6k!D*_S;f$Q2fik9iNcAaUQwY! z2jGyC1gwX%}Q5L zH+yE7{3m*5K&{uXzs`x3zp+lx5bzJvkdRxMLwmxb5HwspQ&dSm0Y&yz$3TO9AwKe5;)j}282lylxycRgtyeCI3XZp3+zNoQ9ynpfSE@B6E2E1`O)y;=!A zN?06taA!}vV!;KmaYYfNe-kU4B3dm=qrX6+n&WU-?`m*3L!?>SE-E7O1(%_C zQlW{TT-_o@RVN=@fh)t4cT@|fRKrzE`P6DPIQU)90~eYn zc1KL7g1)jds`;`rYRcCT$}cX}ctAG0dWBy>kuG)G=tN8nT?zYNt3i;juFtDXq)sQh+)C-@0k{hDMCA@G|G5waa`QYB_^oDkFY{mVC2r=@i>Y`F;FXOG8dBYm+n3PO`=Qjl5r@J+3PnC^z(uNS8g@}~f%Oge zQUe=FZ)ivnC4cN~Zcyg+Awn`IBsSXNscAjZc-WxKzxxc+Aczq!8*uvJKUTcLukw>W z(jGKUr|Ni*8^PB>*^QSeI3uqSml4947x|5|DMr7TVdj=J_7v|;EIsq}Th<135BF9! z_To{_K8^)pb7DnA)NUj98A*!#=&DBRSfc6g!6s|)*=+jEX{huxYv&CFD5N zE{kU5NVMt!{-B9!_88EjjQ+|BBGA)QO>%@u{i&uCROgm+O*rX@&LPH0SEpm03z^Q$ z(5A(p(<*S^d8RWo3+r4G-b90^cd_Xlbnhm2Ra#9<6X`v~fInB7+K835sHUml@21x) z2ZFz9(M{s0@u5&0xm|0jAt=l1O-4Ov1~q*icb81dnrO=-tmfXWrk@0GH?Bz+0E6qN zj|4S?ZZ`?YF&_SI2{_E#(1?2?uu}8t@02Fmaeq^rR`npD)fr9Dxw|r(asj=*AoCia zowEV$lGB9G;TZilkD-+dnhwyG3nfkXtd0*AmNxYOCgZoS-Udv1S5YRutyN9e(qJ8d2vFzUminKia4x-k)bm$o45-MBQwAhWBzH0r!t+%LNMizCEMreZD`lp$2Pz zJp;mDem{HPlcF!oSo#U|krLqLvUtlMLZ)xemhC{MYTy<*Ad@WFA|S`YrCPT3pb7Eu zvdV#v6Zi;Cy=?SdRnQ;LoT%?$0l8X3^9Kti zkpCYYEHEYie>zxTLH@6(2ET381myA#omCUeBmaM)CRk4XZ>A2vgVf>oYxaAR2K+vy z0rKS<0yBC-alTY~2VEQ`P(Wif1^PsVqb8V=9VSpgg_;6#J&B~W1ZL#_R$3q*gnv_c ze+$7oBr^odw`zgBhBimuPaEVPYlD36Ap&y!jI4)nKp8^-^lJ!!rVa&Az)%k8>reoV z(E-p79ROwNbbyu-%^QaS!|THU_ih;AjD~mMRM3^-psZ;)fHg+|c>M?f|1*MuLq_7V z)mZ29kpK&2|F0Vfa5Y_kZ_(xGuFwVD{YD9<5#eQ{0DNu~fM1N_fESDg@bS?ADjY4C zM&oLmB*;1O{|@>MnQReliNaBp84`8bAk91Eym@)@uwH$Q%O(WDPk6_8J0+ZwT@EVF+N8 zu>d|kmIMAe7Qlw%z}21O7^`pXfj7a`igDm-@9`YDZR0@>zv>2|NxKk?%R`#s(8x{z z{PYPOcxw`s*Asxp=!w9?YhuT7RrGQqgj{=Y>0=+*bb;zk8hsnSr zVluOTGN5SvgsVTjOifvdpamLn*qv$W^h11X8>rzOo+wanE+0o z3E)1nIN)8g0DOBEV}N&O#sx-6KJ=dr3}(;f;1gy8{{3vhG@9~PMjoB=4n;1&DQ`6g z1oqG22)vmC0>jLKjgvXBi8BW_f6O`HjdKAUH5b4y=K@&Q0>B;?9I(&=z?znfO6Jto zQ;f<1OQ3Sck|Urt4+K`t1ML^*F~RX!3)V2fzw3K}0SWei}GWWro~88EP3 z#=&PU1N@(5pxn%w#q;Xg%;T+SVK^4NWDV0`nKegn%5o6&SPp{e%bDOv@g;v)M+~+B ziwkWyf~htj_}vBsjaGm=-YdYJf)yO_#FYTvu@b<^D*^m#C4enfb%0e-(kckl$5o7q zv{~v&MrF}zpmKP1hky!dT@BiG*MN4{H7sZYv^MQP2_e|`!Zjc#YRi#xwgtIsw!r3{ zEwC}R=0Mud!%7@xB<#d-~s{DvOD8FmZk<(fWa&~J4(`XR{yfZQ4tr94rjI{v& zv=-oI4lt{RIslCZ2cR)(9Y-Bn2kOe!0S(3V0A9Hsz}MGv!2KNoywH(FknAnYBADO^ z)&-6n0jmul;J*R17i?foZg3IkLE?|z2y*rtIdZuhK~7>5u(8|(Y|d}ufW$WgXxe5_ zet0uePPhN15fp9)kE{(S8QJVv_% z4=;C)z8CJGPkSry*svAAv0DNBb1MhDZX19jw=v^yud7+0ND~|h-gYoPb~^_jz8&zj z+d-+?4p8c{g9EDC0iXdpfx+^f01n#;;5R!t;HA3&d~z3n%Xcv*bZcE2wsE_G$;RCr zxsu%=C$op)=_a}q@45%@m-cY*(t81Kz8A;@>;*D)dpV$49soM%!IUnNYYjy_GC8(* zfKmld4&KKT@b^3^`4BHAMmNo+T6v0FT|zNG!RmV-3rU)BAx$_I{3>!2ytSIl%CA*IT-nD>wjn z-a!uD=^)@^4g#6C2Z4;yAr2_&5P+T?VoG5@TN1!&he7F%!yJ6$VZf^#0rIv-m>k~2 z)*mL=`}o}zvO@iMgd?}g7v#?Q0-1VWAftPf0}43`pwgpkvLbuaw!$*T@EB-yJk}wg zii(fH`Js;=uvy^8*wkk=ivpW?Kagwj133eK=uba?AX4BDL{yJ==v6_!$AQSh;{cKf zfd5+t0Qh_W2P}R9z|&4J<8*sj+Ao(*fbsGZ9Q@QkzG{{Xq z&E!tAnd~(17<~qKc%9+seQ^f#YKMT{4Iu!I4FT}a5Ds`90&pZ^olUlTWoIVdS+G9# zEJq;xEC|$|1?_{+G1g<&`1?Xh%_3FkO+gF-=Wc_^?63k5cBLOGzN=K*x`JX1>d zYNeg{@jNIU62`#?h5l>17tXOj0?61tE~yKuv5mjiK*aERhlmOazYaF)u7jLfERzejcpQ$X zoKGys-HzqRsoem%)i*#c;s%pjnXe;6#WmO-?Ko{cy)EdtLtw;dS*#ux*n*d<-L;$-b za=^8T09O77Kx_U1(5-(sAm!Ttvc3%(LvI7P@iu_<2SBPx0NRqo0aYad zNP!O;ZTJ8V=L7gXp98i@1~5tnP+c;BbnkLN=q`ZD?*eFG3V=4Ja6siL0FqB-onqKy zx&d_J_EhMUm{bm4J`L~-(^z62TNHVdC1zq82s}^Y2rRq@0)h9yuabMrkXEy%Ef_LP z2e}RD9J!KokdwX7@N_Fsnw9SN1!L%-pgV!0=6XCRGVjBvqoYSQ17xi-z-Dj;)6s9$ z&OosFF#~K4&E(h&&IGx_Oy(Tf36wdf{{RHmJ>UowJ^+C}Szu^E78BfhsE<6}kDV|} z$O1uO7DsTwLl8XikTIkCd{XaP9|B(M5eI+b5#aM4fgzd4Opb2pNym`eW01S@m?I~X z4RZ6c8J=$DN#~tg*?@nR4S0hbxYz8T1L;zf1L>mvgrm>@3Fv$B1b9e41@OYB0FHdh z0ZZoscy2C$0&@Y>n9Bjp%>&TUJkXe%2VmKJ0Ndqv&{IWu`7n+4E&z~K0f52_I2wBw zg2vf}pwX`oz=eeXRx9cNt0Mm*(3n#MpgzR_T3y@$Qb9Szps{BOXf!VY@QD%tHvaw3*{WJLGj;;r}ntG1h%m$D<*uVze zMT6x5Z{Zv%XaE7FMvlP2Mi9ts1VcTVm?65wBsH|Y3FOW-apZb5gWR-chNpW=(!qMU z8Ss_O9Q?FrfcJR@cCwx^Il8B$hO!Xf=4t`CeGQJ> zd(D)eYZPsU`FijhP`>C5$VI+^*{b~w=rw%{dV}6_^tQhRy_4R7Ue9*`&U^=8x%V8f z=X(IBya&+V_W+vT$^oUc0_b}yXf*l&V4n{FF8aU$8?^y=PaA;J+W^$}BL}qSBY^lH z0rcx5fGj_8K>SYt`tk`tlRpEkFTl^i2`hMpa z{rDZ^^nWm;uwSFu0&F1o2M9d+!4c5^2?8!ZVL6`olNq9WGSWdS-wtxK+c|QH?I8D| zo#E+bjC9a0`~~>Kzc~00zW_h_H#p?}o5|5F7HJeqeuG@kKO8ytKOh(Phv668^!yA1 zLiR7h8gGtPe@n4Zag7f{bum! zVMG@?BSnSuGGUV_fNqHb=#MA|v`I{8OqWY^e?*k|9xqC=#DqFzE&Wyu1dMxd1g`b~ zt->Bc`t*31IB4G@4&Zy@9PqH70CwyNpxB-OYVXMbIrak3m0qCnMK1vBN&whXf&+db z0pKCMh12Q1WxQP>Qdbr#q9eU&giuOv4t$6tz#SxoaA`Ghosv3qz(YyEf0yLo9i#vs zDaG*dA;S~_FDecA@zNZ8q%`0gr5WDy>K0L$Elv9XetRDdzOfJBRr@kL-H?zb(7wKa zPwdO^GSJ5>Wgr|8G7ydrG8}y?WkKILS%_VoEPyrS0PHTu0oTa^cwj#O+4ciaTt5zI zpge$<%Y(-A@&Im<2e5vB4)}b309W@1u*v`c+YbP6!T=6fMFGGo6aaKl0YI$^9MB3y z0G(9?P`x66Mk#SXXO#d{p#-2o$^ddw=71`c0n~pW3pL%`kIo4@212N>4dmea4+8w6 zK@49I8+-#!u>TAK{EI;x{2~>=2dRMXr7BF0Zp=sRj8z4>jj9~EQdN+X8_e*U+RxdE zeCuGqM-K*k@n)ea>Zc~8uLaCk13M?xnB2vmFPpy zSCUQzC)L4bk-8A>7$`~zL)j&Wfd&{|ufZW%qyYkbHJLHG;U0C$RTJ=+H97dcT7b9I zf*uLhf*xts;(#o*0pzC*ph9f`sSV+P{DuH1X9$4$3T-t{uq% z&(a0(Azc>pfL}Vb?0QkC3o%q4#Su6(3IraE0`20XLHpd%pgnXn2Q01!;OTm-gXpGq zw1ckbf$}On4t}~m;P>k@Jl)QY;-BdQUcrEa#~;|!lgE7nhHsQ}ZiG{}`WV138N|aOquQ3GtNkhOl8^Xk9G#32x84LaujpgVw8VCCJi~~^mH~{q>&jIZj z4}Bso%_Ton}9Q9;EMVEIExJ2 zBDBy&6QP0z*)rV*l<<&8o2Lrp2oEpPn*B1A@<4Z{3Kd9eK56~> zDwJ?gMZHY{OF5W-2n7ynOc{qmx9&@)UFV6!l=Hf9uxblMc5R^wDAp7>+%?5|O~S%} z!yBge4{1GqDGWG_HvK7iKtu;10wj4f*J{b=T=8uiD;ny z(}dc@v&qx2RuPkMVBkCrJo6^4`qRUK!OH33lBjNifOb?NIkaX*IMBF1H(VUaPUq0@ zn;Q-^@LowYcDtuzfj|pv!6IA%-DKEQ(t2=h1lWByo!SkVfg1}pMu6FgGeE%t()xQ- z1Xw&igA;r;w+Lcv6pFP6c0Vwdr;QY5f_KI!Hm8 z3tT8Bhg|+ff~`KYIJWMHUjoPQ4osZ_duL&TDG1F1gZJ=Hu@5Pi!C)(vhnwCa%}T9E zZUnwyR+}v}puMvx;W^)mpE^sBMl=k0b*z z$NP(>n#1hlX)aU|?Gb=N&4okA;Y{*-r7uFb6Vnb8(~1X`*8=Y7D(1-gkio zPqe>Sf9#Tk2vaJ0JrtRm3$+OVr;-VDRtgpsi#w0^uQ-1NzrQ&by5Ki%<-JA&Etoql z^A+9bL`})>Q4M(yleWpXjRm>IQoBb5&LKpjm*r2S`>kX zSwbHhSz@!_&~i)RP~xi({waFR0ohq#Uz07t*CN~~R=AED(EMM1ifK79-I-Pbn*Dp` zVYS0h$UIP+j(>`lx`Wz3?$MGVRzhj&QOCYi-O_=#jd`TMEm|DS#{&QMmDu~;%-lsQ zHu;n-{`pb?e~xQn&~?iFv?n@bZ#159nSx(dn97s`KxOQF@X&HT_Hf<7Xykq{8c(+K zIkq&9F)ot1DSCQ)Xt4iqBLU1N+ zAvS(MGzQsQ2$j)iY=(qWdJ%4o5sQJ`nX`yPNn0`o^5w!JMyX?)mTry@M(1DCE-c_orvBGjh+@%8R8 z*;l?kxdYxl1rDyd??*qSsPRAWCmu zgFviWDpa8J8r=j2$rcC(@TySoQsGeA3Om4%M|@&E?_5KXS8gaH-8shk&ZQ+krf>JJ%x)3u||nl;JDQqa>36U)9WD_i{vC@HIc40(8Fsc zRZ-bkV0TY8RtkCIBb43Xc@l<}hTRxmZ3zoCMzbVzTq?c%7j})1~_~mTWc5GDB zZSa3qiX~A}MeCPydg9w)#$$vU=HcH2VRg5`bImVw!bYe~=lqYaPK8O~LGjeJYdm@# zN3tu0{n2w977ns=0t?6NkQ)$=f2T0>6*wGtBL(0=L^^tF&V?Hg5048sq)@Jv0OC>U zy7X;dJUeG~i^n`9vy#O_;CwO=;(>QzAQrWdQ=0%L!?B<52}H1hW8LC`H);SL+ABFc z{=_mKimQa=(|)AC3Qs#?H<8sUp%(3;tWlo&yQMIteO+Vm69uhel;|D~l4v z!iQCm6B4U&5b%x;U_5FS$GDaIEeOKd)nGgUV?`HlMZv3Zh+9^(5R>g4S`oQFDCWDH zX&=3YC4iF2kt~uy+qyZ7w}1efL{UfJKYBBp6>Gp|>l%9clC~8Nq5gL63enZy2WWvU z^OtTALH&)^hzEb;ZNc9hTkJ31E&^_qY zXp4hz*-j`&*Cw>!G_@FK$Sl6cYUuFoC^{|M^_*Tlmhm8Z{*8fX>SW-7iAO0lz}^aY`0EtKo=c2>g$&i z^;&48O}rFRT?g~g$aUDzGaE3Zz9PP7*9D0i28fMZ8;{!DZYv|VwKz&6-@#0gL1Y&= zpa=)NX#2H}6Vywaz|?~EkXM`5W8~G%U?p_DkbEqRZsA6;L^r1S`Fcp1KLpzB4oU+Z z@%716$Ih8u^vG_|e8N$vNK-UQX-3c1&mtmMCA!Y({^+42tZiRAVoNt&5|I12BzdH= z0hsD-Aj!Es0X*BV0qlE|*7h9n;T|&?I#2Th2X(3!Ch@S3;ZN%6mUJ1y@ zD?t&h*$6zHNo%Pv5v+x81ZxSTwe3wJSbN1Z@HXLA^>>M2&16$&48=k|VGYFE$xUGG zI>z$OqwG!K)H@=2Z8QFqkvN5`3&BN#%5~u_tM(EQD8;l7|$Y={h=3O{#ClFvBQ zn>!coV$*hGjl`_H6PO+8gw4)IbDUuL?uvhk%^a6R%qk7&IsiyLB4*tQ6+5vMweIVx z>5Q%0b-Qc`MHR-ZDA7uEn#H3K&T*= z`yvS+-~S9rvo>2_jrLP0KU`9i88v5ERvMKW;v-~u6)aK+vvS0)32C>J0=?tO|jBt!IZ zNhC>h+yMq!0><-}p#oRo5IQ?OI~wt6DPEpGo!E7Da(#>zy5lKhs2iIyXze6ARsJYQ z0TxpVQ}C7NYf$ayhN-9(r2rKjx6W9IEvZNWF1WrDEY&+oD|K_+1}$)Bj(<~AU&S0h zQlEk}<|rzmICq!_a@?`CI}ItQxgkXpZFcV**kUt<*qSh96bf6<)*)MI2AtT+oNfth zD%8R2(jRNPI^7hHsC=_ls;+#F^S7lEQ{iKE-G zwLMRmwISO%)_N5&YsxzyKMZzY*QJrw4(9rh8P`Uy!LIvl?CSdQ3#fP}cKzxOHX>+w zDB9m@SJT1iwjIDgawldmGbSDUB@+Lpy_nLibny55PUi22`tpGPz4%f^MLXGcZV6%+Xcqv?83$ZlbA8#PL46Td(4>Ky>v;sS|dE#umDd1_1(>uol{k=T#&>* zkJ-}I=RfCB&>rlw<8J0NEmK8&7B#`&&3fWX?Y41niTGqUKAAW~qexGDh9dY5j}>oZ zfX6HL{OfVwDZXp)pvpGrYET6Q?qMF&s#erg+#N90isk8`lxyr!8iX_CLATR&={hl~u@>#0GD7i*`A(^X_z>HyFivLu}uAWA$1Cnfk## z5C#Ju9PtV2#Gir4QytS$*$;H~lOyXj2>rq69)lD5qCJM8cyD})>bD>AX2gCR%HC-9 zeh8%t{wbEV;}Q4~v7h;Y3w*%^JHE|Ww4eE5Blg>D3HD=bx4<sY`pzNQBf>Eh^v^NV?@d!gMEXq?=@J1uWPb!AaPA29 z^{`zY=^!OkbOa*MKw2}`<-wFa>nO~kqkVA}w5>;$zU=0V(ceRQJPMCueU9T!nTzC(L2gbwhJOa3rR=9C z{wWq}SU@SEnXh0I*8H5~3v)!0Pb{CO}KP?F+;` z{j{FZ!nNL{Iv#+~Sbb_JL~4zNipcW>_~CZ~Tc7leS?^HREB*Xm{GAR~7TwOQ55lVH zfG~BC&{~6Mn97-5do2Sk2!xxvy92S>#bQMep143xcs!9X5ImO-!k`B#Md10IAm&KH zof9h5w&)TKlz9qgY*>&`ktWQL+wAa_4}SLMf0 zMg*6ilR{fgg6lMqhocR_z~)3S&Kg$~i+@sEvwCZ{4a2rpcFUTFd5m)3laQPmCvk_2 zEC#Z;TwSLP4}3&yW=RopFDX*$dKsQU4mwvAA-RfTW%T_d^e*kEGu1^Xv${xeWY-b- zdFIhgCYY69H&(UC{ghB2p7Y=$c*rgb4_j-LaSA5+->0xKw{Mh*JkmSOqI7k@;dKxt zUMsRa%^BiqDDE^1#ooG80vlY?@bkm2X}o?wF$%^z3nA$<5Y&NZu$hm!#mH<*i7Hxl zhV>~as7D!zttcvndt$h-9$f9OQqbQ|T&nEWsoPM~8MX{BdptOo^`@}Am_$sgn&`2r*6clhM^fxaWMQp@pIN?7P1SR+z_d zqgcfB64Gh%sBwCUILbhvw16BMZ(agj=3!nUiDb`m(#aMXon>P`O;rS)#BOQg`>g++ zv7?apS(tSa&SLXBH-Pz`XCVQk&S4ut8=3jSjV0aIcnb*P9~Z{r=(*0s;nksg=NN(d zeaGW3U;_Hx2xuZ+C`%PuB9IJp(Lcut4SJ_4AQbCYI>8v+4`rQ5iUY#gU>?HT#v72? zi=54P`_R<$ELHyhli8T6QnaS3R1t-phk#u_kFA6~FNIWjex6zB`E2Atn0ne?mqMyc zeN&3>_>W`vkZ*hTPq)H;z3i4RxMpn_Thx=HgOsb-i4UdFTe$cjy~9Jt{e2{U0TM&^ z0;VA@S%wTG%M`l~Q8#jMcR(3%ym0}>`+d@SI~a@L=Mf>{Fu5s*_9ELhlecvT{qi!88;2CWItap|6Fd^z*qbm}*;oYMQ>7Z|kE zA)U!57?(phA72E+4sfXpH~<^@$09074l zj=<8DtHCzHWe`D{PC&F2A~iaAEi;O1BI4&F{%{|_J`&4LbYPZuUSg9`uwTIRPcUUC zZYYP4HAHrVOhrt%5w~tCS46s(AcrPj!iq-jXCyily)aduSd8qm@Z2wYm=RcT7!#20 z$JVt)hPPL3)Z&Ra8g_kls2Xyag}k%qV%HPh-D?%dOut$l*<6NhcD{^xIK@?<=CTT9 z6dzY1iEdr)oEOAS$5)_?_=>Li6>DGZth{d6+Y&sxCAmPk$_edo~r0Oa}qzy}mmiVIJYj}0m^BPN{ zyJLsPK<0|c23A6cxWsUHO-7evz~s{yY|yX%mpKD`aGD6=E{vi5OjuO;YibF9;G5d3C;PZ1b2Uj<3)6bpT~ zkF@r3tb!O>I#x-dj95iDq?{<};9N=aT3%({1lo^lD#2D7eq zo-Ina34T}I#G+9e)x@|aQohAvYM|{h2@EbB3I;dbf{xjL3k$gFFkAopYfJU?;;SE! zMhbpzEjqFq;`IlM(1l`j)bIIzxL&3nUK9U!W*(zUEvq7;i1sEez=)kKL8jF=m;X#xc(uil5`<=x)yBgns+~e6vp4ROo(=qQjo`u+Y zFdo~Ap3FSeOJGqYWjxuPA%A3>z+S3Z9MgNpGwfn+Hy2Z;G8YT52C^V%PQZh*b{cqx zt9?qMQHhXKMv2(sm+93=V@7q?i4b|?gSyqn6sozC@g+^BcFf3*?a$B)0($xkkzf4?ty*%amExxo;dJ^Ahp}fR?!Cj z69INPp_|K#k?U=kEhBG}(tJt7BJD+a2bZ-H4Vi;FvrzO#;WflCXAN6q>}e(*mXB)e;`w=O>A2 zKX~>=whRv2_-o5iUpJ{kK%C3iQ>gqf}RK?5=?Zp5dq ztOiLm`Yz5FY{~d8rqrH=R@}vr#J~5G-*cWI`(%7$4Oc|P4?I!;Sxk<&K1B)1_{LhF z6wDjL)l+bv=jAiag945cCLD65KJm;3L%k-bR%9vCsYhxN(41S8w-ff0Y)$eW5{?m^TF@lUbH2aU-6 zHoj>pl@3M+r{mV=j~XHDcIhCxo3x5#HzM8aM*M&$9in@Wv_|FO_onl37yV_eeeUDd zynLqD{66SiMOp_GQ@x`oR)chPwAFpjQhdTZj&Dl-*BQN%Mx>#nERS9?4edn3gvv$~ z{1o30_N;7_L?#(P#3BQGwh?X20M7#PPf-D0K;+b=4W!Y;7rEu&JjUf-Y2QrD z#5%2yHi4zhnE>^|t)g-#P;oZSr#lRqPZ}o#f>NJATt2lguIx%WSmgmea_uB&Uh@D? z{=~DZ!;EZJ;wJ>_x^>foQ_Nn>13dW?tX2Ps@^G(z(K)dC39BKxrLwT@J@~;Xab(q7p*Tz_jZl{6K>ot5Ueo-il`hvu^8y@Hil({fLE%)}EzY^8NuXxCSkIu!OzPe~dfy z`E3aBz;Rt~d=g`%LM<#0L{n0L$lnyK!Y*4VP1%&H?5tPA?!4^AMvzL`DF08rlad{; z(^(5YWHHFbvoNOPl8x!C%4Gc=k&Vx#gaRph%T_4EQBgLV8`tf5wQ>=*d#KxG`C_D= z!{$agx!oVZiU7IhfR!6L*vg-LW~CtqvZM{SiXJEd4~IX2d@_528*i6FP`#c2GKe&$ zR6;mQ81gl46f3K03aM_wZy-N~R3G&eH*RWs2IB=+(glA=4PEAs5ej_D{CVNOGD-#e zbGlm`*Q3U#`0RoMB$ta7X8d}F+;i~lJel9mB+=?zu;oM!)%<3*F608&c+&c`M+?jX zGU6?g=*A=}5R!+Fd=ke;zCXj4OnHExPg*xgwt#_4lOPP&Nb5nV7SuLypggK&*!QIM zn@kImo2H_KjAUEzHBLT|nUaqW4VS}*-m9sgf_%I~iZ?qHKn}?dpcXXAAw@bU?0zaA zjC&^{a_MK+h4CoVqW~tVkp);(wub;{%dZ zNaI6>Y*Q8B=Oh0IGzm7v diff --git a/lgtm-presentation/edits/Designing-Cloud-Native-APIs-DotNet.fix7.json b/lgtm-presentation/edits/Designing-Cloud-Native-APIs-DotNet.fix7.json new file mode 100644 index 00000000..017da806 --- /dev/null +++ b/lgtm-presentation/edits/Designing-Cloud-Native-APIs-DotNet.fix7.json @@ -0,0 +1,112 @@ +{ + "ops": [ + { + "op": "replace", + "scope": "notes", + "sldIds": [ + 467 + ], + "expect": 1, + "find": "ASP.NET Core 10 + IHttpClientFactory + IDistributedCache + StackExchange.Redis + Confluent.Kafka + FusionCache", + "repl": "ASP.NET Core 10 + typed HttpClients (IHttpClientFactory) + StackExchange.Redis + Confluent.Kafka", + "_why": "examples/24 dotnet references only Confluent.Kafka and StackExchange.Redis; no FusionCache or IDistributedCache", + "_src": "s13" + }, + { + "op": "replace", + "scope": "notes", + "sldIds": [ + 479 + ], + "expect": 1, + "find": "Code uses IDistributedCache (Microsoft.Extensions.Caching.StackExchangeRedis) for the standard patterns, IConnectionMultiplexer (StackExchange.Redis) for advanced operations, and FusionCache for the read-through / declarative pattern.", + "repl": "Code uses IDistributedCache (Microsoft.Extensions.Caching.StackExchangeRedis) for the standard patterns, IConnectionMultiplexer (StackExchange.Redis) for advanced operations, and HybridCache (Microsoft.Extensions.Caching.Hybrid) for the read-through pattern.", + "_why": "examples/25 reads through HybridCache.GetOrCreateAsync; FusionCache stays only where it adds what HybridCache lacks", + "_src": "s13" + }, + { + "op": "replace", + "scope": "notes", + "sldIds": [ + 487 + ], + "expect": 1, + "find": "Pair with cache-aside or FusionCache.GetOrSetAsync on the read side.", + "repl": "Pair with cache-aside or HybridCache.GetOrCreateAsync on the read side.", + "_why": "examples/25 reads through HybridCache.GetOrCreateAsync; FusionCache stays only where it adds what HybridCache lacks", + "_src": "s13" + }, + { + "op": "replace", + "scope": "slide", + "sldIds": [ + 493 + ], + "expect": 1, + "find": "or FusionCache (built-in stampede protection)", + "repl": "or HybridCache (collapses concurrent misses per Pod)", + "_why": "examples/25 reads through HybridCache.GetOrCreateAsync; FusionCache stays only where it adds what HybridCache lacks", + "_src": "s13" + }, + { + "op": "replace", + "scope": "notes", + "sldIds": [ + 493 + ], + "expect": 1, + "find": "FusionCache solves this out of the box, otherwise use DistributedLock.NET.", + "repl": "HybridCache collapses concurrent misses within each Pod out of the box; across Pods, use DistributedLock.NET.", + "_why": "examples/25 reads through HybridCache.GetOrCreateAsync; FusionCache stays only where it adds what HybridCache lacks", + "_src": "s13" + }, + { + "op": "replace", + "scope": "slide", + "sldIds": [ + 494 + ], + "expect": 1, + "find": "Read-through (FusionCache.GetOrSetAsync) — use when the read path is repetitive and you want declarative caching with built-in stampede protection. Like HybridCache (.NET 9), but with fail-safe and stampede protection built in.", + "repl": "Read-through (HybridCache.GetOrCreateAsync) — use when the read path is repetitive and you want declarative caching with built-in stampede protection. FusionCache adds fail-safe stale fallback and eager refresh.", + "_why": "examples/25 reads through HybridCache.GetOrCreateAsync; FusionCache stays only where it adds what HybridCache lacks; HybridCache does have stampede protection", + "_src": "s13" + }, + { + "op": "replace", + "scope": "slide", + "sldIds": [ + 494 + ], + "expect": 1, + "find": "Write-through (FusionCache.SetAsync or IDistributedCache.SetStringAsync)", + "repl": "Write-through (HybridCache.SetAsync or IDistributedCache.SetStringAsync)", + "_why": "examples/25 reads through HybridCache.GetOrCreateAsync; FusionCache stays only where it adds what HybridCache lacks", + "_src": "s13" + }, + { + "op": "replace", + "scope": "slide", + "sldIds": [ + 494 + ], + "expect": 1, + "find": "Most production ASP.NET Core systems use FusionCache for 95% of cases (it gets you read-through + stampede protection + eager-refresh in one library), with IDistributedCache directly where you need full control.", + "repl": "HybridCache is the in-box default for read-through with stampede protection; FusionCache adds fail-safe, eager refresh and a backplane; use IDistributedCache directly where you need full control.", + "_why": "examples/25 reads through HybridCache.GetOrCreateAsync; FusionCache stays only where it adds what HybridCache lacks; the 95% figure had no source", + "_src": "s13" + }, + { + "op": "replace", + "scope": "notes", + "sldIds": [ + 494 + ], + "expect": 1, + "find": "Decision rules to leave audience with. FusionCache + IDistributedCache is the default modern stack: read-through with stampede protection via GetOrSetAsync, write-through via SetAsync, eager-refresh built in. Drop to IDistributedCache directly when you want full control. Write-around for telemetry. Write-back is the niche extreme-throughput pattern. The bold final line is the take-home: don't pick one pattern as 'the' cache strategy. FusionCache for 95% of cases is the modern default; everything else is a specialization.", + "repl": "Decision rules. HybridCache over the Redis IDistributedCache is the in-box stack: read-through with stampede protection via GetOrCreateAsync, write-through via SetAsync. FusionCache is the alternative when you need fail-safe stale fallback, eager refresh or a backplane for multi-Pod L1 invalidation. Drop to IDistributedCache directly when you want full control. Write-around for telemetry. Write-back is the niche extreme-throughput pattern. The bold final line is the take-home: don't pick one pattern as 'the' cache strategy; choose per read path.", + "_why": "examples/25 reads through HybridCache.GetOrCreateAsync; FusionCache stays only where it adds what HybridCache lacks", + "_src": "s13" + } + ] +} \ No newline at end of file diff --git a/lgtm-presentation/text/Designing-Cloud-Native-APIs-DotNet.md b/lgtm-presentation/text/Designing-Cloud-Native-APIs-DotNet.md index 57e69a28..a3594732 100644 --- a/lgtm-presentation/text/Designing-Cloud-Native-APIs-DotNet.md +++ b/lgtm-presentation/text/Designing-Cloud-Native-APIs-DotNet.md @@ -3162,7 +3162,7 @@ K Patterns that survive the migration with the system still running. -> Appendix K covers the migration patterns. The thesis: you don't decompose a monolith by stopping it and rewriting; you decompose it incrementally, one bounded asset at a time, with the system serving users the whole way through. We'll walk through the monolith as the starting point, modular monoliths as a frequently-correct destination, the strangler-fig family for progressive extraction, content-based routing for fine-grained cutover, and the decorating collaborator for wrapping legacy code you can't touch. Code in this appendix uses ASP.NET Core 10 + IHttpClientFactory + IDistributedCache + StackExchange.Redis + Confluent.Kafka + FusionCache, but the patterns are language-neutral. +> Appendix K covers the migration patterns. The thesis: you don't decompose a monolith by stopping it and rewriting; you decompose it incrementally, one bounded asset at a time, with the system serving users the whole way through. We'll walk through the monolith as the starting point, modular monoliths as a frequently-correct destination, the strangler-fig family for progressive extraction, content-based routing for fine-grained cutover, and the decorating collaborator for wrapping legacy code you can't touch. Code in this appendix uses ASP.NET Core 10 + typed HttpClients (IHttpClientFactory) + StackExchange.Redis + Confluent.Kafka, but the patterns are language-neutral. ## [213] APPENDIX K · MONOLITH · A monolith is a unit of deployment (sldId 468) @@ -3331,7 +3331,7 @@ L Six patterns for keeping a cache useful and your data correct. -> Appendix L is the caching reference. Five reasons to cache and six patterns to do it well. The patterns differ in who reads/writes the cache, when, and who owns the consistency story. We use Redis as the canonical cache because it's the default open-source choice on Kubernetes — same patterns apply to in-memory caches or any KV store. Code uses IDistributedCache (Microsoft.Extensions.Caching.StackExchangeRedis) for the standard patterns, IConnectionMultiplexer (StackExchange.Redis) for advanced operations, and FusionCache for the read-through / declarative pattern. +> Appendix L is the caching reference. Five reasons to cache and six patterns to do it well. The patterns differ in who reads/writes the cache, when, and who owns the consistency story. We use Redis as the canonical cache because it's the default open-source choice on Kubernetes — same patterns apply to in-memory caches or any KV store. Code uses IDistributedCache (Microsoft.Extensions.Caching.StackExchangeRedis) for the standard patterns, IConnectionMultiplexer (StackExchange.Redis) for advanced operations, and HybridCache (Microsoft.Extensions.Caching.Hybrid) for the read-through pattern. ## [225] APPENDIX L · WHY CACHE · Five things a cache earns you (sldId 480) @@ -3473,7 +3473,7 @@ Writes go to the DB only. The next read populates the cache lazily. Good for wri p. 205 -> When data is written far more often than it's read (audit logs, event records, telemetry), caching every write is wasteful: most cached entries expire before being read again. Write-around solves this: write the DB, leave the cache alone. The next reader pays the cache-miss cost and populates the cache. Pros: no cache pollution by write-only data. Cons: the first read after every write is slow (cache miss); reads of recently-written data are slower than under cache-aside. Pair with cache-aside or FusionCache.GetOrSetAsync on the read side. Common use: any 'append-only' or 'mostly-written' workload — audit entries, IoT telemetry. +> When data is written far more often than it's read (audit logs, event records, telemetry), caching every write is wasteful: most cached entries expire before being read again. Write-around solves this: write the DB, leave the cache alone. The next reader pays the cache-miss cost and populates the cache. Pros: no cache pollution by write-only data. Cons: the first read after every write is slow (cache miss); reads of recently-written data are slower than under cache-aside. Pair with cache-aside or HybridCache.GetOrCreateAsync on the read side. Common use: any 'append-only' or 'mostly-written' workload — audit entries, IoT telemetry. ## [233] APPENDIX L · WRITE-AROUND · Write-around in ASP.NET Core (sldId 488) @@ -3608,27 +3608,27 @@ p. 210 - Don't cache by default. Caching adds a consistency story, an invalidation story, and a failure-mode story. If reads are already fast, just don't add a cache — the cheapest cache is no cache. - Don't cache rarely-read data. Caching write-once-read-never data thrashes your cache and gains nothing. Profile read patterns first — OpenTelemetry's cache metrics give you hit/miss ratios per cache. - Don't cache without a TTL. Unbounded caches grow until they evict at random, which gives you the worst type of failure: unpredictable. Always set explicit AbsoluteExpirationRelativeToNow or maxmemory-policy on Redis (allkeys-lru typical). -- Avoid cache stampedes (thundering herd): when a hot key expires and many concurrent ASP.NET Core Pods all miss at once, they all hit the DB. Mitigate with DistributedLock.NET per-key locks, or FusionCache (built-in stampede protection), or refresh-ahead, or jittered TTLs. +- Avoid cache stampedes (thundering herd): when a hot key expires and many concurrent ASP.NET Core Pods all miss at once, they all hit the DB. Mitigate with DistributedLock.NET per-key locks, or HybridCache (collapses concurrent misses per Pod), or refresh-ahead, or jittered TTLs. - Watch for cache aside + write-around without invalidation — the write goes to the DB, but a stale cached value can persist for the full TTL. Always RemoveAsync on writes, or use a short TTL. - Treat the cache as a performance optimisation, never as the source of truth. A Redis outage must mean slower reads, not data loss — always have an EF Core fallback path. Test this by stopping Redis in staging. p. 211 -> Six things that go wrong with caching. The first point is the most important: caching is a complexity tax, only worth paying when reads hurt. The second is the profile-first rule. The third is operational. The fourth is the cache stampede problem — FusionCache solves this out of the box, otherwise use DistributedLock.NET. The fifth is the most common stale-data bug. The bold final line is the architecturally important one: the cache is never the source of truth. If Redis dies, you must serve slower reads, not return errors. Test this by killing Redis in staging. +> Six things that go wrong with caching. The first point is the most important: caching is a complexity tax, only worth paying when reads hurt. The second is the profile-first rule. The third is operational. The fourth is the cache stampede problem — HybridCache collapses concurrent misses within each Pod out of the box; across Pods, use DistributedLock.NET. The fifth is the most common stale-data bug. The bold final line is the architecturally important one: the cache is never the source of truth. If Redis dies, you must serve slower reads, not return errors. Test this by killing Redis in staging. ## [239] APPENDIX L · DECISIONS · Choosing the right caching pattern (sldId 494) - Cache-aside — the default. Use when you want explicit control and reads dominate writes. -- Read-through (FusionCache.GetOrSetAsync) — use when the read path is repetitive and you want declarative caching with built-in stampede protection. Like HybridCache (.NET 9), but with fail-safe and stampede protection built in. -- Write-through (FusionCache.SetAsync or IDistributedCache.SetStringAsync) — use when zero stale-read risk after a write matters more than write latency. +- Read-through (HybridCache.GetOrCreateAsync) — use when the read path is repetitive and you want declarative caching with built-in stampede protection. FusionCache adds fail-safe stale fallback and eager refresh. +- Write-through (HybridCache.SetAsync or IDistributedCache.SetStringAsync) — use when zero stale-read risk after a write matters more than write latency. - Write-around — use when writes far outnumber re-reads. Avoids cache pollution by write-once data. - Write-back (BackgroundService + dirty-set) — use only when extreme write throughput is the constraint and eventual durability is acceptable. - Refresh-ahead (FusionCache eager-refresh, or BackgroundService) — use when a small set of hot keys serves most traffic and miss latency is unacceptable. -- Most production ASP.NET Core systems use FusionCache for 95% of cases (it gets you read-through + stampede protection + eager-refresh in one library), with IDistributedCache directly where you need full control. Mix patterns; don't pick one as the rule. +- HybridCache is the in-box default for read-through with stampede protection; FusionCache adds fail-safe, eager refresh and a backplane; use IDistributedCache directly where you need full control. Mix patterns; don't pick one as the rule. p. 212 -> Decision rules to leave audience with. FusionCache + IDistributedCache is the default modern stack: read-through with stampede protection via GetOrSetAsync, write-through via SetAsync, eager-refresh built in. Drop to IDistributedCache directly when you want full control. Write-around for telemetry. Write-back is the niche extreme-throughput pattern. The bold final line is the take-home: don't pick one pattern as 'the' cache strategy. FusionCache for 95% of cases is the modern default; everything else is a specialization. +> Decision rules. HybridCache over the Redis IDistributedCache is the in-box stack: read-through with stampede protection via GetOrCreateAsync, write-through via SetAsync. FusionCache is the alternative when you need fail-safe stale fallback, eager refresh or a backplane for multi-Pod L1 invalidation. Drop to IDistributedCache directly when you want full control. Write-around for telemetry. Write-back is the niche extreme-throughput pattern. The bold final line is the take-home: don't pick one pattern as 'the' cache strategy; choose per read path. ## [240] Appendix M · Failure modes (sldId 495) From 41d1e9e386d157745b1c0c609b315b55805980b2 Mon Sep 17 00:00:00 2001 From: PatternCatalyst Date: Mon, 12 Oct 2026 19:59:41 -0400 Subject: [PATCH 2/6] refactor(demo-27): Go flag-service attributes map uses any --- examples/27-feature-flags/go/flag-service/cmd/server/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/27-feature-flags/go/flag-service/cmd/server/main.go b/examples/27-feature-flags/go/flag-service/cmd/server/main.go index fe80980e..88c1ee59 100644 --- a/examples/27-feature-flags/go/flag-service/cmd/server/main.go +++ b/examples/27-feature-flags/go/flag-service/cmd/server/main.go @@ -70,7 +70,7 @@ func evalCtx(r *http.Request) openfeature.EvaluationContext { if user == "" { user = "anonymous" } - attrs := map[string]interface{}{} + attrs := map[string]any{} if plan := r.Header.Get("X-Plan"); plan != "" { attrs["plan"] = plan } From 836e2ad60e28f027af843554cf9e4b14b2ef0415 Mon Sep 17 00:00:00 2001 From: PatternCatalyst Date: Mon, 12 Oct 2026 19:59:41 -0400 Subject: [PATCH 3/6] refactor(examples): C++ service logging through Drogon LOG_* and spdlog (S13) Drogon services log with Drogon's LOG_* macros; the gRPC inventory services (02, 03, 11) and the Kafka consumers (05 x2, 11) use spdlog 1.17.0, added to their Conan locks with --lockfile-partial so no other pin moves. Every Drogon main line-buffers stdout: trantor flushes only at error level, so info lines sat in the pipe buffer and 03's DataLoader and 24's CACHE_HIT checks never saw them. CLI and test output keeps std::cout. --- .../cpp/inventory-service/CMakeLists.txt | 2 ++ .../cpp/inventory-service/conan.lock | 7 ++++ .../cpp/inventory-service/conanfile.py | 2 ++ .../cpp/inventory-service/src/main.cpp | 13 +++---- .../cpp/order-service/src/kafka_publisher.hpp | 6 ++-- .../cpp/order-service/src/main.cpp | 14 ++++---- .../03-composition/cpp/gateway/src/main.cpp | 10 +++--- .../cpp/inventory-service/CMakeLists.txt | 2 ++ .../cpp/inventory-service/conan.lock | 7 ++++ .../cpp/inventory-service/conanfile.py | 2 ++ .../cpp/inventory-service/src/main.cpp | 16 +++------ .../03-composition/cpp/order-api/src/main.cpp | 8 +++-- .../04-data/cpp/order-service/src/main.cpp | 3 ++ .../cpp/notification-consumer/CMakeLists.txt | 3 +- .../cpp/notification-consumer/conan.lock | 5 +++ .../cpp/notification-consumer/conanfile.py | 2 ++ .../cpp/notification-consumer/src/main.cpp | 36 +++++++++---------- .../cpp/order-service/src/main.cpp | 3 ++ .../cpp/shipping-consumer/CMakeLists.txt | 3 +- .../cpp/shipping-consumer/conan.lock | 5 +++ .../cpp/shipping-consumer/conanfile.py | 2 ++ .../cpp/shipping-consumer/src/main.cpp | 36 +++++++++---------- .../cpp/order-service/src/main.cpp | 3 ++ .../cpp/inventory-service/CMakeLists.txt | 3 +- .../cpp/inventory-service/conan.lock | 5 +++ .../cpp/inventory-service/conanfile.py | 2 ++ .../cpp/inventory-service/src/main.cpp | 9 +++-- .../cpp/notification-consumer/CMakeLists.txt | 3 +- .../cpp/notification-consumer/conan.lock | 5 +++ .../cpp/notification-consumer/conanfile.py | 2 ++ .../cpp/notification-consumer/src/main.cpp | 24 ++++++------- .../16-websockets/cpp/ws-server/src/main.cpp | 14 ++++---- .../cpp/saga-orchestrator/src/main.cpp | 3 ++ .../18-errors/cpp/order-service/src/main.cpp | 3 ++ .../cpp/order-service/src/main.cpp | 3 ++ .../cpp/router-service/src/main.cpp | 6 ++-- .../cpp/decorator/src/main.cpp | 20 ++++++----- .../cpp/router/src/main.cpp | 8 +++-- .../25-caching/cpp/cache-service/src/main.cpp | 3 ++ .../cpp/backend-service/src/main.cpp | 3 ++ .../cpp/edge-service/src/main.cpp | 3 ++ .../cpp/flag-service/src/main.cpp | 3 ++ 42 files changed, 200 insertions(+), 112 deletions(-) diff --git a/examples/02-communications/cpp/inventory-service/CMakeLists.txt b/examples/02-communications/cpp/inventory-service/CMakeLists.txt index 591e0396..26703100 100644 --- a/examples/02-communications/cpp/inventory-service/CMakeLists.txt +++ b/examples/02-communications/cpp/inventory-service/CMakeLists.txt @@ -15,6 +15,7 @@ set(CMAKE_CXX_LINK_EXECUTABLE find_package(gRPC CONFIG REQUIRED) find_package(protobuf CONFIG REQUIRED) +find_package(spdlog CONFIG REQUIRED) find_package(Threads REQUIRED) set(PROTO_SRC "${CMAKE_SOURCE_DIR}/../proto/inventory.proto") @@ -42,4 +43,5 @@ target_include_directories(inventory-service PRIVATE ${PROTO_OUT}) target_link_libraries(inventory-service PRIVATE gRPC::grpc++ protobuf::protobuf + spdlog::spdlog Threads::Threads) diff --git a/examples/02-communications/cpp/inventory-service/conan.lock b/examples/02-communications/cpp/inventory-service/conan.lock index 601bfcf7..d4c4377f 100644 --- a/examples/02-communications/cpp/inventory-service/conan.lock +++ b/examples/02-communications/cpp/inventory-service/conan.lock @@ -2,10 +2,12 @@ "version": "0.5", "requires": [ "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964", "re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1772560729.95", "protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783", "openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113", "grpc/1.84.0#c7c2ff9ed88b66eac8b8399c96d8df2c%1789729270.871", + "fmt/12.2.0#b445244e6d0151a425e229248cd2bed8%1785754552.386", "c-ares/1.34.8#074b97a9e38159813ed5e2dcfeb290ca%1784539118.141", "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], @@ -16,5 +18,10 @@ "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], "python_requires": [], + "overrides": { + "fmt/12.1.0": [ + "fmt/12.2.0" + ] + }, "config_requires": [] } diff --git a/examples/02-communications/cpp/inventory-service/conanfile.py b/examples/02-communications/cpp/inventory-service/conanfile.py index 0f834de3..c4dc8081 100644 --- a/examples/02-communications/cpp/inventory-service/conanfile.py +++ b/examples/02-communications/cpp/inventory-service/conanfile.py @@ -17,3 +17,5 @@ class Cndp02CommunicationsCppInventoryServiceConan(ConanFile): def requirements(self): self.requires("grpc/1.84.0") self.requires("protobuf/6.33.5") + self.requires("spdlog/1.17.0") + self.requires("fmt/12.2.0", override=True) # spdlog pins fmt 12.1.0 diff --git a/examples/02-communications/cpp/inventory-service/src/main.cpp b/examples/02-communications/cpp/inventory-service/src/main.cpp index 79b56e76..6dd5028e 100644 --- a/examples/02-communications/cpp/inventory-service/src/main.cpp +++ b/examples/02-communications/cpp/inventory-service/src/main.cpp @@ -1,8 +1,8 @@ #include +#include #include "inventory.grpc.pb.h" #include #include -#include static std::mutex g_mu; static std::unordered_map g_stock; @@ -21,15 +21,12 @@ class InventoryServiceImpl final : public inventory::Inventory::Service { stock = remaining; reply->set_reserved(true); reply->set_remaining(remaining); - std::cerr << "reserved sku=" << req->sku() - << " qty=" << req->quantity() - << " remaining=" << remaining << std::endl; + spdlog::info("reserved sku={} qty={} remaining={}", req->sku(), req->quantity(), remaining); } else { reply->set_reserved(false); reply->set_remaining(stock); - std::cerr << "insufficient stock sku=" << req->sku() - << " requested=" << req->quantity() - << " available=" << stock << std::endl; + spdlog::info("insufficient stock sku={} requested={} available={}", + req->sku(), req->quantity(), stock); } return grpc::Status::OK; } @@ -45,7 +42,7 @@ int main() { builder.AddListeningPort(addr, grpc::InsecureServerCredentials()); builder.RegisterService(&service); auto server = builder.BuildAndStart(); - std::cerr << "inventory gRPC server listening on " << addr << std::endl; + spdlog::info("inventory gRPC server listening on {}", addr); server->Wait(); return 0; } diff --git a/examples/02-communications/cpp/order-service/src/kafka_publisher.hpp b/examples/02-communications/cpp/order-service/src/kafka_publisher.hpp index 33979442..37bba9fc 100644 --- a/examples/02-communications/cpp/order-service/src/kafka_publisher.hpp +++ b/examples/02-communications/cpp/order-service/src/kafka_publisher.hpp @@ -7,13 +7,13 @@ #pragma once #include -#include #include #include #include #include #include +#include namespace cndp { @@ -51,7 +51,7 @@ class OrderPublisher { topic_, RdKafka::Topic::PARTITION_UA, RdKafka::Producer::RK_MSG_COPY, const_cast(payload.data()), payload.size(), nullptr, 0, 0, nullptr); if (rc != RdKafka::ERR_NO_ERROR) { - std::cerr << "kafka produce failed: " << RdKafka::err2str(rc) << std::endl; + LOG_ERROR << "kafka produce failed: " << RdKafka::err2str(rc); return false; } return true; @@ -62,7 +62,7 @@ class OrderPublisher { public: void dr_cb(RdKafka::Message& m) override { if (m.err() != RdKafka::ERR_NO_ERROR) { - std::cerr << "kafka delivery failed topic=" << m.topic_name() << ": " << m.errstr() << std::endl; + LOG_ERROR << "kafka delivery failed topic=" << m.topic_name() << ": " << m.errstr(); } } }; diff --git a/examples/02-communications/cpp/order-service/src/main.cpp b/examples/02-communications/cpp/order-service/src/main.cpp index 7d5b301e..c28be2c0 100644 --- a/examples/02-communications/cpp/order-service/src/main.cpp +++ b/examples/02-communications/cpp/order-service/src/main.cpp @@ -7,8 +7,8 @@ // cppgraphqlgen GraphQL schema, generated resolvers (src/graphql/) // rdkafka++ order.placed producer with delivery reports +#include #include -#include #include #include #include @@ -59,12 +59,14 @@ const char* env_or(const char* name, const char* fallback) { } // namespace int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); std::shared_ptr repo; try { repo = std::make_shared( env_or("PG_CONNINFO", "host=postgres port=5432 dbname=appdb user=appuser password=apppass")); } catch (const std::exception& e) { - std::cerr << e.what() << std::endl; + LOG_ERROR << e.what(); return 1; } @@ -77,7 +79,7 @@ int main() { publisher = std::make_unique(env_or("KAFKA_BOOTSTRAP", "kafka:9094"), "order.placed"); } catch (const std::exception& e) { - std::cerr << e.what() << std::endl; + LOG_ERROR << e.what(); return 1; } @@ -130,7 +132,7 @@ int main() { try { repo->insert(order); } catch (const std::exception& e) { - std::cerr << "order insert failed: " << e.what() << std::endl; + LOG_ERROR << "order insert failed: " << e.what(); cb(status_only(k500InternalServerError)); return; } @@ -139,7 +141,7 @@ int main() { wb["indentation"] = ""; publisher->publish(Json::writeString(wb, to_json(order))); - std::cerr << "order created id=" << order.id << " status=" << order.status << std::endl; + LOG_INFO << "order created id=" << order.id << " status=" << order.status; auto resp = HttpResponse::newHttpJsonResponse(to_json(order)); resp->setStatusCode(k201Created); cb(resp); @@ -160,7 +162,7 @@ int main() { try { orders = repo->list(limit, req->getParameter("after")); } catch (const std::exception& e) { - std::cerr << "order query failed: " << e.what() << std::endl; + LOG_ERROR << "order query failed: " << e.what(); cb(status_only(k500InternalServerError)); return; } diff --git a/examples/03-composition/cpp/gateway/src/main.cpp b/examples/03-composition/cpp/gateway/src/main.cpp index 0df01b92..180521e8 100644 --- a/examples/03-composition/cpp/gateway/src/main.cpp +++ b/examples/03-composition/cpp/gateway/src/main.cpp @@ -8,11 +8,11 @@ // worker thread instead of on a Drogon event-loop thread. #include +#include #include #include #include #include -#include #include #include #include @@ -131,6 +131,8 @@ class WorkerPool { } // namespace int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); auto channel = grpc::CreateChannel(env_or("INVENTORY_ADDR", "inventory:50051"), grpc::InsecureChannelCredentials()); std::shared_ptr inventory_stub = inventory::Inventory::NewStub(channel); @@ -144,12 +146,12 @@ int main() { grpc::ClientContext ctx; auto status = inventory_stub->GetStockBatch(&ctx, req, &reply); if (!status.ok()) { - std::cerr << "inventory GetStockBatch failed: " << status.error_message() << std::endl; + LOG_ERROR << "inventory GetStockBatch failed: " << status.error_message(); return std::nullopt; } std::unordered_map result; for (const auto& item : reply.items()) result[item.sku()] = item.available(); - std::cerr << "DataLoader batched " << skus.size() << " skus in one gRPC call" << std::endl; + LOG_INFO << "DataLoader batched " << skus.size() << " skus in one gRPC call"; return result; }; @@ -159,7 +161,7 @@ int main() { const auto workers = static_cast(std::atoi(env_or("GATEWAY_WORKERS", "16"))); auto pool = std::make_shared(workers > 0 ? workers : 16, 256); - std::cerr << "gateway started" << std::endl; + LOG_INFO << "gateway started"; app().setLogLevel(trantor::Logger::kInfo); app().addListener("0.0.0.0", 8080); diff --git a/examples/03-composition/cpp/inventory-service/CMakeLists.txt b/examples/03-composition/cpp/inventory-service/CMakeLists.txt index 591e0396..26703100 100644 --- a/examples/03-composition/cpp/inventory-service/CMakeLists.txt +++ b/examples/03-composition/cpp/inventory-service/CMakeLists.txt @@ -15,6 +15,7 @@ set(CMAKE_CXX_LINK_EXECUTABLE find_package(gRPC CONFIG REQUIRED) find_package(protobuf CONFIG REQUIRED) +find_package(spdlog CONFIG REQUIRED) find_package(Threads REQUIRED) set(PROTO_SRC "${CMAKE_SOURCE_DIR}/../proto/inventory.proto") @@ -42,4 +43,5 @@ target_include_directories(inventory-service PRIVATE ${PROTO_OUT}) target_link_libraries(inventory-service PRIVATE gRPC::grpc++ protobuf::protobuf + spdlog::spdlog Threads::Threads) diff --git a/examples/03-composition/cpp/inventory-service/conan.lock b/examples/03-composition/cpp/inventory-service/conan.lock index 601bfcf7..d4c4377f 100644 --- a/examples/03-composition/cpp/inventory-service/conan.lock +++ b/examples/03-composition/cpp/inventory-service/conan.lock @@ -2,10 +2,12 @@ "version": "0.5", "requires": [ "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964", "re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1772560729.95", "protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783", "openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113", "grpc/1.84.0#c7c2ff9ed88b66eac8b8399c96d8df2c%1789729270.871", + "fmt/12.2.0#b445244e6d0151a425e229248cd2bed8%1785754552.386", "c-ares/1.34.8#074b97a9e38159813ed5e2dcfeb290ca%1784539118.141", "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], @@ -16,5 +18,10 @@ "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], "python_requires": [], + "overrides": { + "fmt/12.1.0": [ + "fmt/12.2.0" + ] + }, "config_requires": [] } diff --git a/examples/03-composition/cpp/inventory-service/conanfile.py b/examples/03-composition/cpp/inventory-service/conanfile.py index f9efcb1e..2720365a 100644 --- a/examples/03-composition/cpp/inventory-service/conanfile.py +++ b/examples/03-composition/cpp/inventory-service/conanfile.py @@ -17,3 +17,5 @@ class Cndp03CompositionCppInventoryServiceConan(ConanFile): def requirements(self): self.requires("grpc/1.84.0") self.requires("protobuf/6.33.5") + self.requires("spdlog/1.17.0") + self.requires("fmt/12.2.0", override=True) # spdlog pins fmt 12.1.0 diff --git a/examples/03-composition/cpp/inventory-service/src/main.cpp b/examples/03-composition/cpp/inventory-service/src/main.cpp index 8b4ae936..5e0ed180 100644 --- a/examples/03-composition/cpp/inventory-service/src/main.cpp +++ b/examples/03-composition/cpp/inventory-service/src/main.cpp @@ -1,7 +1,8 @@ +#include #include +#include #include "inventory.grpc.pb.h" #include -#include #include static std::unordered_map g_stock = { @@ -21,9 +22,7 @@ class InventoryServiceImpl final : public inventory::Inventory::Service { int available = (it != g_stock.end()) ? it->second : 0; reply->set_sku(req->sku()); reply->set_available(available); - std::cerr << "GetStock sku=" << req->sku() - << " available=" << available - << " (call #" << count << ")" << std::endl; + spdlog::info("GetStock sku={} available={} (call #{})", req->sku(), available, count); return grpc::Status::OK; } @@ -37,12 +36,7 @@ class InventoryServiceImpl final : public inventory::Inventory::Service { auto it = g_stock.find(sku); item->set_available((it != g_stock.end()) ? it->second : 0); } - std::cerr << "GetStockBatch skus=["; - for (int i = 0; i < req->skus_size(); i++) { - if (i > 0) std::cerr << ", "; - std::cerr << req->skus(i); - } - std::cerr << "] (call #" << count << ")" << std::endl; + spdlog::info("GetStockBatch skus=[{}] (call #{})", fmt::join(req->skus(), ", "), count); return grpc::Status::OK; } }; @@ -54,7 +48,7 @@ int main() { builder.AddListeningPort(addr, grpc::InsecureServerCredentials()); builder.RegisterService(&service); auto server = builder.BuildAndStart(); - std::cerr << "inventory gRPC server listening on " << addr << std::endl; + spdlog::info("inventory gRPC server listening on {}", addr); server->Wait(); return 0; } diff --git a/examples/03-composition/cpp/order-api/src/main.cpp b/examples/03-composition/cpp/order-api/src/main.cpp index 900e4217..d8c9131f 100644 --- a/examples/03-composition/cpp/order-api/src/main.cpp +++ b/examples/03-composition/cpp/order-api/src/main.cpp @@ -1,7 +1,7 @@ #include #include +#include #include -#include using namespace drogon; @@ -9,14 +9,16 @@ static PGconn* g_pg = nullptr; static std::mutex g_pg_mu; int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); auto env_pg = std::getenv("PG_CONNINFO"); std::string conninfo = env_pg ? env_pg : "host=postgres port=5432 dbname=appdb user=appuser password=apppass"; g_pg = PQconnectdb(conninfo.c_str()); if (PQstatus(g_pg) != CONNECTION_OK) { - std::cerr << "pg connect failed: " << PQerrorMessage(g_pg) << std::endl; + LOG_ERROR << "pg connect failed: " << PQerrorMessage(g_pg); return 1; } - std::cerr << "order-api started" << std::endl; + LOG_INFO << "order-api started"; app().setLogLevel(trantor::Logger::kInfo); app().addListener("0.0.0.0", 8081); diff --git a/examples/04-data/cpp/order-service/src/main.cpp b/examples/04-data/cpp/order-service/src/main.cpp index 2346da1d..71363cdb 100644 --- a/examples/04-data/cpp/order-service/src/main.cpp +++ b/examples/04-data/cpp/order-service/src/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -278,6 +279,8 @@ void signal_handler(int sig) { } int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); // Set up custom signal handler app().registerBeginningAdvice([]() { std::signal(SIGTERM, signal_handler); diff --git a/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt b/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt index 2e40b9e6..5d6f1f18 100644 --- a/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt +++ b/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt @@ -18,8 +18,9 @@ find_package(RdKafka CONFIG REQUIRED) # librdkafka C++ API (rdkafka++) from Con find_package(jsoncpp CONFIG REQUIRED) find_package(PostgreSQL REQUIRED) # libpq from Conan (direct C client) find_package(opentelemetry-cpp CONFIG REQUIRED) +find_package(spdlog CONFIG REQUIRED) find_package(Threads REQUIRED) add_executable(notification-consumer src/main.cpp) target_include_directories(notification-consumer PRIVATE src) -target_link_libraries(notification-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp Threads::Threads) +target_link_libraries(notification-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp spdlog::spdlog Threads::Threads) diff --git a/examples/05-event-driven/cpp/notification-consumer/conan.lock b/examples/05-event-driven/cpp/notification-consumer/conan.lock index cca59aef..05d4815f 100644 --- a/examples/05-event-driven/cpp/notification-consumer/conan.lock +++ b/examples/05-event-driven/cpp/notification-consumer/conan.lock @@ -2,6 +2,7 @@ "version": "0.5", "requires": [ "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964", "protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783", "opentelemetry-cpp/1.26.0#000b96632e39f3672f8576bf4febb213%1789030194.301", "openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113", @@ -11,6 +12,7 @@ "libpq/17.11#6161d3be84f5d2bd657dd778eed017c2%1788252336.346", "libcurl/8.22.0#3599f8564660d38f96af52da40795734%1789660092.861", "jsoncpp/1.9.8#5be5e85394fc94f13f6451e5822ea759%1787912339.522", + "fmt/12.2.0#b445244e6d0151a425e229248cd2bed8%1785754552.386", "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], "build_requires": [ @@ -37,6 +39,9 @@ ], "nlohmann_json/3.11.3": [ "nlohmann_json/3.12.0" + ], + "fmt/12.1.0": [ + "fmt/12.2.0" ] }, "config_requires": [] diff --git a/examples/05-event-driven/cpp/notification-consumer/conanfile.py b/examples/05-event-driven/cpp/notification-consumer/conanfile.py index 82483aec..6e4733c6 100644 --- a/examples/05-event-driven/cpp/notification-consumer/conanfile.py +++ b/examples/05-event-driven/cpp/notification-consumer/conanfile.py @@ -19,6 +19,8 @@ def requirements(self): self.requires("librdkafka/2.14.2") self.requires("jsoncpp/1.9.8") self.requires("opentelemetry-cpp/1.26.0") + self.requires("spdlog/1.17.0") # opentelemetry-cpp pins nlohmann_json 3.11.3; take the newest release. self.requires("nlohmann_json/3.12.0", override=True) self.requires("lz4/1.10.0", override=True) # librdkafka and libpq pin lz4 1.9.4 + self.requires("fmt/12.2.0", override=True) # spdlog pins fmt 12.1.0 diff --git a/examples/05-event-driven/cpp/notification-consumer/src/main.cpp b/examples/05-event-driven/cpp/notification-consumer/src/main.cpp index fdc4078b..9bfd8d1c 100644 --- a/examples/05-event-driven/cpp/notification-consumer/src/main.cpp +++ b/examples/05-event-driven/cpp/notification-consumer/src/main.cpp @@ -5,12 +5,12 @@ #include #include +#include #include #include #include #include -#include #include #include #include @@ -32,12 +32,12 @@ void init_pg() { g_pg_conn = PQconnectdb(conninfo); if (PQstatus(g_pg_conn) != CONNECTION_OK) { - std::cerr << "Postgres connection failed: " << PQerrorMessage(g_pg_conn) << "\n"; + spdlog::error("Postgres connection failed: {}", PQerrorMessage(g_pg_conn)); PQfinish(g_pg_conn); g_pg_conn = nullptr; throw std::runtime_error("Postgres connection failed"); } - std::cout << "Connected to Postgres\n"; + spdlog::info("Connected to Postgres"); } enum class Outcome { Done, Skip, Retry }; @@ -51,16 +51,16 @@ Outcome process_event(const std::string& event_json) { std::istringstream stream(event_json); if (!Json::parseFromStream(builder, stream, &root, &errs)) { - std::cerr << "JSON parse error: " << errs << "\n"; + spdlog::warn("JSON parse error: {}", errs); return Outcome::Skip; } if (!root.isObject() || !root.isMember("id")) { - std::cerr << "Missing 'id' field in event\n"; + spdlog::warn("Missing 'id' field in event"); return Outcome::Skip; } std::string order_id = root["id"].asString(); - std::cout << "Processing order: " << order_id << "\n"; + spdlog::info("Processing order: {}", order_id); if (PQstatus(g_pg_conn) != CONNECTION_OK) PQreset(g_pg_conn); @@ -72,9 +72,9 @@ Outcome process_event(const std::string& event_json) { bool ok = PQresultStatus(res) == PGRES_COMMAND_OK; if (!ok) { - std::cerr << "Insert failed: " << PQerrorMessage(g_pg_conn) << "\n"; + spdlog::error("Insert failed: {}", PQerrorMessage(g_pg_conn)); } else { - std::cout << "Notification created/skipped for order: " << order_id << "\n"; + spdlog::info("Notification created/skipped for order: {}", order_id); } PQclear(res); return ok ? Outcome::Done : Outcome::Retry; @@ -89,7 +89,7 @@ int main() { try { init_pg(); } catch (const std::exception& e) { - std::cerr << "Initialization failed: " << e.what() << "\n"; + spdlog::error("Initialization failed: {}", e.what()); return 1; } @@ -106,32 +106,32 @@ int main() { {"auto.offset.reset", "earliest"}, {"enable.auto.commit", "false"}}) { if (conf->set(key, value, errstr) != RdKafka::Conf::CONF_OK) { - std::cerr << "Kafka config " << key << ": " << errstr << "\n"; + spdlog::error("Kafka config {}: {}", key, errstr); return 1; } } std::unique_ptr consumer(RdKafka::KafkaConsumer::create(conf.get(), errstr)); if (!consumer) { - std::cerr << "Failed to create consumer: " << errstr << "\n"; + spdlog::error("Failed to create consumer: {}", errstr); return 1; } // subscribe() does not fail when the topic is missing: the consumer keeps polling and // picks the partitions up once the producer has created order.placed. if (RdKafka::ErrorCode err = consumer->subscribe({"order.placed"}); err != RdKafka::ERR_NO_ERROR) { - std::cerr << "Subscribe failed: " << RdKafka::err2str(err) << "\n"; + spdlog::error("Subscribe failed: {}", RdKafka::err2str(err)); return 1; } - std::cout << "Successfully subscribed to order.placed topic\n"; - std::cout << "Notification consumer started, listening to order.placed...\n"; + spdlog::info("Successfully subscribed to order.placed topic"); + spdlog::info("Notification consumer started, listening to order.placed..."); while (!g_shutdown) { std::unique_ptr msg(consumer->consume(1000)); switch (msg->err()) { case RdKafka::ERR_NO_ERROR: { std::string event(static_cast(msg->payload()), msg->len()); - std::cout << "Received event: " << event << "\n"; + spdlog::info("Received event: {}", event); std::string traceparent; if (auto* hdrs = msg->headers()) { @@ -159,7 +159,7 @@ int main() { if (outcome == Outcome::Retry) break; // shutting down: leave the offset uncommitted // Commit only after the side effect, so a crash redelivers rather than loses. if (RdKafka::ErrorCode err = consumer->commitSync(msg.get()); err != RdKafka::ERR_NO_ERROR) { - std::cerr << "Commit failed: " << RdKafka::err2str(err) << "\n"; + spdlog::error("Commit failed: {}", RdKafka::err2str(err)); } break; } @@ -168,7 +168,7 @@ int main() { break; default: // e.g. UNKNOWN_TOPIC_OR_PART before the topic exists; librdkafka retries on its own. - std::cerr << "Kafka error: " << msg->errstr() << "\n"; + spdlog::warn("Kafka error: {}", msg->errstr()); std::this_thread::sleep_for(std::chrono::seconds(1)); } } @@ -179,6 +179,6 @@ int main() { if (g_pg_conn) PQfinish(g_pg_conn); cndp::telemetry::shutdown(); - std::cout << "Notification consumer shutdown complete\n"; + spdlog::info("Notification consumer shutdown complete"); return 0; } diff --git a/examples/05-event-driven/cpp/order-service/src/main.cpp b/examples/05-event-driven/cpp/order-service/src/main.cpp index 73be72f0..cff18c35 100644 --- a/examples/05-event-driven/cpp/order-service/src/main.cpp +++ b/examples/05-event-driven/cpp/order-service/src/main.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -107,6 +108,8 @@ void init_kafka() { } int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); std::signal(SIGTERM, signal_handler); std::signal(SIGINT, signal_handler); diff --git a/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt b/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt index 63c9c71c..f9a5dafb 100644 --- a/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt +++ b/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt @@ -18,8 +18,9 @@ find_package(RdKafka CONFIG REQUIRED) # librdkafka C++ API (rdkafka++) from Con find_package(jsoncpp CONFIG REQUIRED) find_package(PostgreSQL REQUIRED) # libpq from Conan (direct C client) find_package(opentelemetry-cpp CONFIG REQUIRED) +find_package(spdlog CONFIG REQUIRED) find_package(Threads REQUIRED) add_executable(shipping-consumer src/main.cpp) target_include_directories(shipping-consumer PRIVATE src) -target_link_libraries(shipping-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp Threads::Threads) +target_link_libraries(shipping-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp spdlog::spdlog Threads::Threads) diff --git a/examples/05-event-driven/cpp/shipping-consumer/conan.lock b/examples/05-event-driven/cpp/shipping-consumer/conan.lock index cca59aef..05d4815f 100644 --- a/examples/05-event-driven/cpp/shipping-consumer/conan.lock +++ b/examples/05-event-driven/cpp/shipping-consumer/conan.lock @@ -2,6 +2,7 @@ "version": "0.5", "requires": [ "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964", "protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783", "opentelemetry-cpp/1.26.0#000b96632e39f3672f8576bf4febb213%1789030194.301", "openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113", @@ -11,6 +12,7 @@ "libpq/17.11#6161d3be84f5d2bd657dd778eed017c2%1788252336.346", "libcurl/8.22.0#3599f8564660d38f96af52da40795734%1789660092.861", "jsoncpp/1.9.8#5be5e85394fc94f13f6451e5822ea759%1787912339.522", + "fmt/12.2.0#b445244e6d0151a425e229248cd2bed8%1785754552.386", "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], "build_requires": [ @@ -37,6 +39,9 @@ ], "nlohmann_json/3.11.3": [ "nlohmann_json/3.12.0" + ], + "fmt/12.1.0": [ + "fmt/12.2.0" ] }, "config_requires": [] diff --git a/examples/05-event-driven/cpp/shipping-consumer/conanfile.py b/examples/05-event-driven/cpp/shipping-consumer/conanfile.py index 05c8c1ca..e221fa32 100644 --- a/examples/05-event-driven/cpp/shipping-consumer/conanfile.py +++ b/examples/05-event-driven/cpp/shipping-consumer/conanfile.py @@ -19,6 +19,8 @@ def requirements(self): self.requires("librdkafka/2.14.2") self.requires("jsoncpp/1.9.8") self.requires("opentelemetry-cpp/1.26.0") + self.requires("spdlog/1.17.0") # opentelemetry-cpp pins nlohmann_json 3.11.3; take the newest release. self.requires("nlohmann_json/3.12.0", override=True) self.requires("lz4/1.10.0", override=True) # librdkafka and libpq pin lz4 1.9.4 + self.requires("fmt/12.2.0", override=True) # spdlog pins fmt 12.1.0 diff --git a/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp b/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp index daa8b81b..307cddbb 100644 --- a/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp +++ b/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp @@ -5,12 +5,12 @@ #include #include +#include #include #include #include #include -#include #include #include #include @@ -32,12 +32,12 @@ void init_pg() { g_pg_conn = PQconnectdb(conninfo); if (PQstatus(g_pg_conn) != CONNECTION_OK) { - std::cerr << "Postgres connection failed: " << PQerrorMessage(g_pg_conn) << "\n"; + spdlog::error("Postgres connection failed: {}", PQerrorMessage(g_pg_conn)); PQfinish(g_pg_conn); g_pg_conn = nullptr; throw std::runtime_error("Postgres connection failed"); } - std::cout << "Connected to Postgres\n"; + spdlog::info("Connected to Postgres"); } enum class Outcome { Done, Skip, Retry }; @@ -51,16 +51,16 @@ Outcome process_event(const std::string& event_json) { std::istringstream stream(event_json); if (!Json::parseFromStream(builder, stream, &root, &errs)) { - std::cerr << "JSON parse error: " << errs << "\n"; + spdlog::warn("JSON parse error: {}", errs); return Outcome::Skip; } if (!root.isObject() || !root.isMember("id")) { - std::cerr << "Missing 'id' field in event\n"; + spdlog::warn("Missing 'id' field in event"); return Outcome::Skip; } std::string order_id = root["id"].asString(); - std::cout << "Processing order: " << order_id << "\n"; + spdlog::info("Processing order: {}", order_id); if (PQstatus(g_pg_conn) != CONNECTION_OK) PQreset(g_pg_conn); @@ -72,9 +72,9 @@ Outcome process_event(const std::string& event_json) { bool ok = PQresultStatus(res) == PGRES_COMMAND_OK; if (!ok) { - std::cerr << "Insert failed: " << PQerrorMessage(g_pg_conn) << "\n"; + spdlog::error("Insert failed: {}", PQerrorMessage(g_pg_conn)); } else { - std::cout << "Shipment created/skipped for order: " << order_id << "\n"; + spdlog::info("Shipment created/skipped for order: {}", order_id); } PQclear(res); return ok ? Outcome::Done : Outcome::Retry; @@ -89,7 +89,7 @@ int main() { try { init_pg(); } catch (const std::exception& e) { - std::cerr << "Initialization failed: " << e.what() << "\n"; + spdlog::error("Initialization failed: {}", e.what()); return 1; } @@ -106,32 +106,32 @@ int main() { {"auto.offset.reset", "earliest"}, {"enable.auto.commit", "false"}}) { if (conf->set(key, value, errstr) != RdKafka::Conf::CONF_OK) { - std::cerr << "Kafka config " << key << ": " << errstr << "\n"; + spdlog::error("Kafka config {}: {}", key, errstr); return 1; } } std::unique_ptr consumer(RdKafka::KafkaConsumer::create(conf.get(), errstr)); if (!consumer) { - std::cerr << "Failed to create consumer: " << errstr << "\n"; + spdlog::error("Failed to create consumer: {}", errstr); return 1; } // subscribe() does not fail when the topic is missing: the consumer keeps polling and // picks the partitions up once the producer has created order.placed. if (RdKafka::ErrorCode err = consumer->subscribe({"order.placed"}); err != RdKafka::ERR_NO_ERROR) { - std::cerr << "Subscribe failed: " << RdKafka::err2str(err) << "\n"; + spdlog::error("Subscribe failed: {}", RdKafka::err2str(err)); return 1; } - std::cout << "Successfully subscribed to order.placed topic\n"; - std::cout << "Shipping consumer started, listening to order.placed...\n"; + spdlog::info("Successfully subscribed to order.placed topic"); + spdlog::info("Shipping consumer started, listening to order.placed..."); while (!g_shutdown) { std::unique_ptr msg(consumer->consume(1000)); switch (msg->err()) { case RdKafka::ERR_NO_ERROR: { std::string event(static_cast(msg->payload()), msg->len()); - std::cout << "Received event: " << event << "\n"; + spdlog::info("Received event: {}", event); std::string traceparent; if (auto* hdrs = msg->headers()) { @@ -159,7 +159,7 @@ int main() { if (outcome == Outcome::Retry) break; // shutting down: leave the offset uncommitted // Commit only after the side effect, so a crash redelivers rather than loses. if (RdKafka::ErrorCode err = consumer->commitSync(msg.get()); err != RdKafka::ERR_NO_ERROR) { - std::cerr << "Commit failed: " << RdKafka::err2str(err) << "\n"; + spdlog::error("Commit failed: {}", RdKafka::err2str(err)); } break; } @@ -168,7 +168,7 @@ int main() { break; default: // e.g. UNKNOWN_TOPIC_OR_PART before the topic exists; librdkafka retries on its own. - std::cerr << "Kafka error: " << msg->errstr() << "\n"; + spdlog::warn("Kafka error: {}", msg->errstr()); std::this_thread::sleep_for(std::chrono::seconds(1)); } } @@ -179,6 +179,6 @@ int main() { if (g_pg_conn) PQfinish(g_pg_conn); cndp::telemetry::shutdown(); - std::cout << "Shipping consumer shutdown complete\n"; + spdlog::info("Shipping consumer shutdown complete"); return 0; } diff --git a/examples/06-stream-processing/cpp/order-service/src/main.cpp b/examples/06-stream-processing/cpp/order-service/src/main.cpp index 318538a1..11039afe 100644 --- a/examples/06-stream-processing/cpp/order-service/src/main.cpp +++ b/examples/06-stream-processing/cpp/order-service/src/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -131,6 +132,8 @@ bool publish_order_event(const std::string& order_json, std::function #include +#include #include #include #include #include -#include #include #include #include @@ -91,9 +91,8 @@ class InventoryServiceImpl final : public inventory::InventoryService::Service { response->set_confirmed(confirmed); response->set_remaining(remaining); - std::cerr << "ReserveStock sku=" << sku << " qty=" << quantity - << " confirmed=" << (confirmed ? "true" : "false") - << " remaining=" << remaining << std::endl; + spdlog::info("ReserveStock sku={} qty={} confirmed={} remaining={}", + sku, quantity, (confirmed ? "true" : "false"), remaining); span->SetAttribute("rpc.system", "grpc"); span->SetAttribute("rpc.service", "inventory.InventoryService"); @@ -130,7 +129,7 @@ int main() { builder.RegisterService(&service); auto server = builder.BuildAndStart(); - std::cerr << "inventory-service started on " << addr << std::endl; + spdlog::info("inventory-service started on {}", addr); while (!g_shutdown) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); diff --git a/examples/11-observability/cpp/notification-consumer/CMakeLists.txt b/examples/11-observability/cpp/notification-consumer/CMakeLists.txt index c2120b86..8ebd5dc0 100644 --- a/examples/11-observability/cpp/notification-consumer/CMakeLists.txt +++ b/examples/11-observability/cpp/notification-consumer/CMakeLists.txt @@ -17,9 +17,10 @@ set(CMAKE_CXX_LINK_EXECUTABLE find_package(RdKafka CONFIG REQUIRED) find_package(PostgreSQL REQUIRED) find_package(opentelemetry-cpp CONFIG REQUIRED) +find_package(spdlog CONFIG REQUIRED) find_package(Threads REQUIRED) add_executable(notification-consumer src/main.cpp) target_include_directories(notification-consumer PRIVATE src) target_link_libraries(notification-consumer PRIVATE - RdKafka::rdkafka++ PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp Threads::Threads) + RdKafka::rdkafka++ PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp spdlog::spdlog Threads::Threads) diff --git a/examples/11-observability/cpp/notification-consumer/conan.lock b/examples/11-observability/cpp/notification-consumer/conan.lock index 0a27b680..d501b21a 100644 --- a/examples/11-observability/cpp/notification-consumer/conan.lock +++ b/examples/11-observability/cpp/notification-consumer/conan.lock @@ -2,6 +2,7 @@ "version": "0.5", "requires": [ "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964", "protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783", "opentelemetry-cpp/1.26.0#000b96632e39f3672f8576bf4febb213%1789030194.301", "openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113", @@ -10,6 +11,7 @@ "librdkafka/2.14.2#ebe1ce610f423ebe1dbfd52756b98db8%1781073238.939", "libpq/17.11#6161d3be84f5d2bd657dd778eed017c2%1788252336.346", "libcurl/8.22.0#3599f8564660d38f96af52da40795734%1789660092.861", + "fmt/12.2.0#b445244e6d0151a425e229248cd2bed8%1785754552.386", "abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755" ], "build_requires": [ @@ -36,6 +38,9 @@ ], "nlohmann_json/3.11.3": [ "nlohmann_json/3.12.0" + ], + "fmt/12.1.0": [ + "fmt/12.2.0" ] }, "config_requires": [] diff --git a/examples/11-observability/cpp/notification-consumer/conanfile.py b/examples/11-observability/cpp/notification-consumer/conanfile.py index 8191f6c9..f4a08229 100644 --- a/examples/11-observability/cpp/notification-consumer/conanfile.py +++ b/examples/11-observability/cpp/notification-consumer/conanfile.py @@ -20,6 +20,8 @@ def requirements(self): self.requires("librdkafka/2.14.2") self.requires("libpq/17.11") self.requires("opentelemetry-cpp/1.26.0") + self.requires("spdlog/1.17.0") # opentelemetry-cpp pins nlohmann_json 3.11.3; take the newest release. self.requires("nlohmann_json/3.12.0", override=True) self.requires("lz4/1.10.0", override=True) # librdkafka and libpq pin lz4 1.9.4 + self.requires("fmt/12.2.0", override=True) # spdlog pins fmt 12.1.0 diff --git a/examples/11-observability/cpp/notification-consumer/src/main.cpp b/examples/11-observability/cpp/notification-consumer/src/main.cpp index 9445cd04..fb17fad2 100644 --- a/examples/11-observability/cpp/notification-consumer/src/main.cpp +++ b/examples/11-observability/cpp/notification-consumer/src/main.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -15,6 +14,7 @@ #include #include +#include #include "telemetry.hpp" @@ -71,7 +71,7 @@ int main() { const char* env = std::getenv("PG_CONNINFO"); PGconn* pg = PQconnectdb(env ? env : "postgresql://appuser:apppass@postgres:5432/appdb"); if (PQstatus(pg) != CONNECTION_OK) { - std::cerr << "Postgres failed: " << PQerrorMessage(pg) << std::endl; + spdlog::error("Postgres failed: {}", PQerrorMessage(pg)); return 1; } @@ -86,28 +86,28 @@ int main() { conf->set("group.id", "notification-group", err) != RdKafka::Conf::CONF_OK || conf->set("auto.offset.reset", "earliest", err) != RdKafka::Conf::CONF_OK || conf->set("enable.auto.commit", "false", err) != RdKafka::Conf::CONF_OK) { - std::cerr << "Kafka consumer config failed: " << err << std::endl; + spdlog::error("Kafka consumer config failed: {}", err); return 1; } std::unique_ptr consumer(RdKafka::KafkaConsumer::create(conf.get(), err)); if (!consumer) { - std::cerr << "Kafka consumer init failed: " << err << std::endl; + spdlog::error("Kafka consumer init failed: {}", err); return 1; } if (auto rc = consumer->subscribe({"order.placed"}); rc != RdKafka::ERR_NO_ERROR) { - std::cerr << "subscribe failed: " << RdKafka::err2str(rc) << std::endl; + spdlog::error("subscribe failed: {}", RdKafka::err2str(rc)); return 1; } auto tracer = cndp::telemetry::tracer("notification-consumer"); - std::cerr << "notification-consumer started" << std::endl; + spdlog::info("notification-consumer started"); while (!g_shutdown) { std::unique_ptr msg(consumer->consume(1000)); if (msg->err() == RdKafka::ERR__TIMED_OUT || msg->err() == RdKafka::ERR__PARTITION_EOF) continue; if (msg->err() != RdKafka::ERR_NO_ERROR) { // unknown topic before the first order is created, transient broker errors, ... - std::cerr << "consume: " << msg->errstr() << std::endl; + spdlog::warn("consume: {}", msg->errstr()); continue; } @@ -145,15 +145,15 @@ int main() { "INSERT INTO notifications (order_id, channel) VALUES ($1, 'email')", 1, nullptr, params, nullptr, nullptr, 0); if (PQresultStatus(res) == PGRES_COMMAND_OK) { - std::cerr << "notification sent order_id=" << order_id << std::endl; + spdlog::info("notification sent order_id={}", order_id); done = true; } else { const char* state = PQresultErrorField(res, PG_DIAG_SQLSTATE); if (state && std::string(state) == "23505") { - std::cerr << "duplicate skipped order_id=" << order_id << std::endl; + spdlog::info("duplicate skipped order_id={}", order_id); done = true; } else { - std::cerr << "insert failed, retrying: " << PQerrorMessage(pg) << std::endl; + spdlog::warn("insert failed, retrying: {}", PQerrorMessage(pg)); } } PQclear(res); @@ -164,12 +164,12 @@ int main() { // Commit only after the side effect. if (auto rc = consumer->commitSync(msg.get()); rc != RdKafka::ERR_NO_ERROR) - std::cerr << "commit failed: " << RdKafka::err2str(rc) << std::endl; + spdlog::error("commit failed: {}", RdKafka::err2str(rc)); } consumer->close(); PQfinish(pg); cndp::telemetry::shutdown(); - std::cerr << "notification-consumer stopped" << std::endl; + spdlog::info("notification-consumer stopped"); return 0; } diff --git a/examples/16-websockets/cpp/ws-server/src/main.cpp b/examples/16-websockets/cpp/ws-server/src/main.cpp index 5db17a0d..8deccb6b 100644 --- a/examples/16-websockets/cpp/ws-server/src/main.cpp +++ b/examples/16-websockets/cpp/ws-server/src/main.cpp @@ -1,13 +1,13 @@ #include #include #include +#include #include #include #include #include #include #include -#include using namespace drogon; @@ -97,7 +97,7 @@ static void redis_subscriber_loop() { } } } catch (const std::exception& e) { - std::cerr << "Redis subscriber error: " << e.what() << std::endl; + LOG_ERROR << "Redis subscriber error: " << e.what(); } } @@ -135,7 +135,7 @@ class WsCtrl : public WebSocketController { g_clients[client_id] = {conn, 0, {}}; } - std::cerr << "client connected id=" << client_id << " pod=" << g_pod_name << std::endl; + LOG_INFO << "client connected id=" << client_id << " pod=" << g_pod_name; } void handleConnectionClosed(const WebSocketConnectionPtr& conn) override { @@ -144,7 +144,7 @@ class WsCtrl : public WebSocketController { std::lock_guard lock(g_clients_mu); g_clients.erase(*ctx); - std::cerr << "client disconnected id=" << *ctx << " pod=" << g_pod_name << std::endl; + LOG_INFO << "client disconnected id=" << *ctx << " pod=" << g_pod_name; } WS_PATH_LIST_BEGIN @@ -153,6 +153,8 @@ class WsCtrl : public WebSocketController { }; int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); std::signal(SIGTERM, signal_handler); std::signal(SIGINT, signal_handler); @@ -234,7 +236,7 @@ int main() { try { g_redis->publish("ws:broadcast", pub_msg); } catch (const std::exception& e) { - std::cerr << "Redis publish error: " << e.what() << std::endl; + LOG_ERROR << "Redis publish error: " << e.what(); } if (!target.empty()) { @@ -250,7 +252,7 @@ int main() { }, {Post}); - std::cerr << "ws-server started pod=" << g_pod_name << std::endl; + LOG_INFO << "ws-server started pod=" << g_pod_name; app().run(); g_shutdown = true; diff --git a/examples/17-sagas/cpp/saga-orchestrator/src/main.cpp b/examples/17-sagas/cpp/saga-orchestrator/src/main.cpp index 3702f4ad..d275241e 100644 --- a/examples/17-sagas/cpp/saga-orchestrator/src/main.cpp +++ b/examples/17-sagas/cpp/saga-orchestrator/src/main.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -290,6 +291,8 @@ static void resumeRunningSagas() { } int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); const char* dbUrl = std::getenv("DATABASE_URL"); g_connStr = dbUrl ? dbUrl : "postgres://appuser:apppass@localhost:5432/appdb"; diff --git a/examples/18-errors/cpp/order-service/src/main.cpp b/examples/18-errors/cpp/order-service/src/main.cpp index cc487c4f..92d1d733 100644 --- a/examples/18-errors/cpp/order-service/src/main.cpp +++ b/examples/18-errors/cpp/order-service/src/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -124,6 +125,8 @@ class OrderController { }; int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); // Initialize gRPC stub std::string inventoryHost = std::getenv("INVENTORY_HOST") ? std::getenv("INVENTORY_HOST") diff --git a/examples/22-l7-routing/cpp/order-service/src/main.cpp b/examples/22-l7-routing/cpp/order-service/src/main.cpp index a9136215..f13476a6 100644 --- a/examples/22-l7-routing/cpp/order-service/src/main.cpp +++ b/examples/22-l7-routing/cpp/order-service/src/main.cpp @@ -1,9 +1,12 @@ #include +#include #include using namespace drogon; int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); std::string version = std::getenv("APP_VERSION") ? std::getenv("APP_VERSION") : "v1"; app().setLogLevel(trantor::Logger::kInfo); diff --git a/examples/22-l7-routing/cpp/router-service/src/main.cpp b/examples/22-l7-routing/cpp/router-service/src/main.cpp index f7db8172..75e320fc 100644 --- a/examples/22-l7-routing/cpp/router-service/src/main.cpp +++ b/examples/22-l7-routing/cpp/router-service/src/main.cpp @@ -1,8 +1,8 @@ #include #include +#include #include #include -#include #include #include #include @@ -48,6 +48,8 @@ static std::string load_script() { } int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); auto rules = std::make_shared(); std::shared_ptr engine; @@ -55,7 +57,7 @@ int main() { // Compiles the script once at startup: a bad rules file stops the service here. engine = std::make_shared(load_script()); } catch (const std::exception& e) { - std::cerr << "invalid rules script: " << e.what() << std::endl; + LOG_ERROR << "invalid rules script: " << e.what(); return 1; } diff --git a/examples/24-monolith-to-microservices/cpp/decorator/src/main.cpp b/examples/24-monolith-to-microservices/cpp/decorator/src/main.cpp index 5527fd64..df3dc5b3 100644 --- a/examples/24-monolith-to-microservices/cpp/decorator/src/main.cpp +++ b/examples/24-monolith-to-microservices/cpp/decorator/src/main.cpp @@ -1,10 +1,10 @@ #include #include #include +#include #include #include #include -#include #include #include #include @@ -33,9 +33,9 @@ class DeliveryLog : public RdKafka::DeliveryReportCb { void dr_cb(RdKafka::Message& message) override { std::unique_ptr pending(static_cast(message.msg_opaque())); if (message.err() != RdKafka::ERR_NO_ERROR) { - std::cerr << "kafka send failed: " << message.errstr() << std::endl; + LOG_ERROR << "kafka send failed: " << message.errstr(); } else if (pending) { - std::cerr << "EVENT order.placed order_id=" << pending->order_id << std::endl; + LOG_INFO << "EVENT order.placed order_id=" << pending->order_id; } if (pending && pending->done) pending->done(); } @@ -110,14 +110,16 @@ static void publish_event(const std::string& order_id, const std::string& order_ msg.data(), msg.size(), nullptr, 0, 0, opaque); if (err == RdKafka::ERR_NO_ERROR) return; delete opaque; - std::cerr << "kafka send failed: " << RdKafka::err2str(err) << std::endl; + LOG_ERROR << "kafka send failed: " << RdKafka::err2str(err); } catch (const std::exception& e) { - std::cerr << "kafka send failed: " << e.what() << std::endl; + LOG_ERROR << "kafka send failed: " << e.what(); } if (done) done(); } int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); auto env_legacy = std::getenv("LEGACY_URL"); g_legacy_url = env_legacy ? env_legacy : "http://legacy:8080"; @@ -134,12 +136,12 @@ int main() { conf->set("acks", "all", errstr) != RdKafka::Conf::CONF_OK || conf->set("message.timeout.ms", "10000", errstr) != RdKafka::Conf::CONF_OK || conf->set("dr_cb", &g_dr_cb, errstr) != RdKafka::Conf::CONF_OK) { - std::cerr << "kafka config: " << errstr << std::endl; + LOG_ERROR << "kafka config: " << errstr; return 1; } g_producer.reset(RdKafka::Producer::create(conf.get(), errstr)); if (!g_producer) { - std::cerr << "kafka producer: " << errstr << std::endl; + LOG_ERROR << "kafka producer: " << errstr; return 1; } g_producer_running = true; @@ -188,7 +190,7 @@ int main() { try { auto cached = g_redis->get(cache_key); if (cached) { - std::cerr << "CACHE_HIT order_id=" << order_id << std::endl; + LOG_INFO << "CACHE_HIT order_id=" << order_id; auto r = HttpResponse::newHttpResponse(); r->setStatusCode(k200OK); r->setContentTypeCode(CT_APPLICATION_JSON); @@ -198,7 +200,7 @@ int main() { } } catch (...) {} - std::cerr << "CACHE_MISS order_id=" << order_id << std::endl; + LOG_INFO << "CACHE_MISS order_id=" << order_id; forward_get(order_id, [order_id, cb = std::move(cb)](std::string result) { diff --git a/examples/24-monolith-to-microservices/cpp/router/src/main.cpp b/examples/24-monolith-to-microservices/cpp/router/src/main.cpp index dd3df80b..c56efdd2 100644 --- a/examples/24-monolith-to-microservices/cpp/router/src/main.cpp +++ b/examples/24-monolith-to-microservices/cpp/router/src/main.cpp @@ -1,7 +1,7 @@ #include +#include #include #include -#include using namespace drogon; @@ -47,6 +47,8 @@ static std::string resolve_upstream(const std::string& tenant) { } int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); auto env_m = std::getenv("MONOLITH_URL"); auto env_n = std::getenv("NEW_SERVICE_URL"); g_service_urls["monolith"] = env_m ? env_m : "http://monolith:8080"; @@ -74,7 +76,7 @@ int main() { tenant = (*body)["tenant"].asString(); } std::string upstream = resolve_upstream(tenant); - std::cerr << "routing tenant=" << tenant << " to " << upstream << std::endl; + LOG_INFO << "routing tenant=" << tenant << " to " << upstream; forward_request(upstream, Post, "/orders", std::string(req->body()), std::move(cb)); }, {Post}); @@ -121,7 +123,7 @@ int main() { g_default_service = (*body)["default"].asString(); } } - std::cerr << "rules updated" << std::endl; + LOG_INFO << "rules updated"; Json::Value r; r["status"] = "updated"; cb(HttpResponse::newHttpJsonResponse(r)); diff --git a/examples/25-caching/cpp/cache-service/src/main.cpp b/examples/25-caching/cpp/cache-service/src/main.cpp index 20e2a306..ff871f8b 100644 --- a/examples/25-caching/cpp/cache-service/src/main.cpp +++ b/examples/25-caching/cpp/cache-service/src/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -271,6 +272,8 @@ void writeback_flusher() { } int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); // Connect to Postgres const char* pg_conninfo = std::getenv("PG_CONNINFO"); if (!pg_conninfo) pg_conninfo = "postgresql://appuser:apppass@localhost:5432/appdb"; diff --git a/examples/26-failure-modes/cpp/backend-service/src/main.cpp b/examples/26-failure-modes/cpp/backend-service/src/main.cpp index a6f058a0..8dd50de9 100644 --- a/examples/26-failure-modes/cpp/backend-service/src/main.cpp +++ b/examples/26-failure-modes/cpp/backend-service/src/main.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,8 @@ std::mt19937 gen(rd()); std::uniform_int_distribution<> dist(0, 1); int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); // Suppress upload directory warning app().setUploadPath("/tmp"); diff --git a/examples/26-failure-modes/cpp/edge-service/src/main.cpp b/examples/26-failure-modes/cpp/edge-service/src/main.cpp index bba85806..c430ad8d 100644 --- a/examples/26-failure-modes/cpp/edge-service/src/main.cpp +++ b/examples/26-failure-modes/cpp/edge-service/src/main.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -260,6 +261,8 @@ CircuitBreaker g_breaker("backend", 5, 10.0); Bulkhead g_bulkhead(5); int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); // Suppress upload directory warning app().setUploadPath("/tmp"); diff --git a/examples/27-feature-flags/cpp/flag-service/src/main.cpp b/examples/27-feature-flags/cpp/flag-service/src/main.cpp index d0fccac7..0f511b58 100644 --- a/examples/27-feature-flags/cpp/flag-service/src/main.cpp +++ b/examples/27-feature-flags/cpp/flag-service/src/main.cpp @@ -6,6 +6,7 @@ #include #include "schema.pb.h" #include +#include #include #include @@ -65,6 +66,8 @@ class FlagdClient { }; int main() { + // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written. + std::setvbuf(stdout, nullptr, _IOLBF, 0); std::string flagdHost = std::getenv("FLAGD_HOST") ? std::getenv("FLAGD_HOST") : "flagd"; int flagdPort = std::getenv("FLAGD_PORT") ? std::stoi(std::getenv("FLAGD_PORT")) : 8013; From 098b79f9196213f77b93d2c7999c876c080b4d4b Mon Sep 17 00:00:00 2001 From: PatternCatalyst Date: Mon, 12 Oct 2026 19:59:41 -0400 Subject: [PATCH 4/6] docs: CLAUDE.md locks the S13 framework idioms (ORMs, health, shutdown, uv, schema ownership, logging) --- CLAUDE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 79f403cc..a32a96d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -163,6 +163,27 @@ Do not use `gh cache delete --all`: it also removes the entries the current poli images `ubi9/dotnet-100:9.8-1791421718` and `ubi9/dotnet-100-aspnet:9.8-1791363520`, held on ubi9 because Red Hat publishes no ubi10 .NET image; every NuGet package pinned exactly); Python (FastAPI, **aiokafka**, **faust-streaming** for stateful processing, grpcio). +- **Idiomatic framework features (S13; do not hand-roll what the framework provides):** + - Schema: `db/init` owns every schema. Hibernate runs `validate`; EF Core has no migrations and no + `EnsureCreated`. HTTP responses are DTOs, never serialized entities. + - Quarkus: Hibernate ORM with Panache (JSONB via `@JdbcTypeCode(SqlTypes.JSON)` plus + `quarkus.hibernate-orm.mapping.format.global=ignore`); SmallRye Health on `/healthz` and `/readyz`; + `quarkus.shutdown.delay` for draining; REST Client, never `java.net.http`; `@RestPath`/`@RestQuery`/`@RestHeader` + with `true` in the compiler plugin (a REST client is not registered without it); + `@ServerExceptionMapper`. + - Spring Boot: Spring Data JPA (`open-in-view=false`); Actuator health groups on `/healthz` and `/readyz`; + `server.shutdown=graceful` plus a `SmartLifecycle` drain bean; `ProblemDetail`; Spring Kafka JSON serdes; + `RestClient`; Spring Cache for read-through only. + - .NET: EF Core `DbContext`; `AddHealthChecks` + `MapHealthChecks`; `IHostedLifecycleService` for draining; + `BackgroundService` for consumers and workers; IConfiguration/Options; typed `AddHttpClient`; + `AddProblemDetails`; `ILogger`; HybridCache for read-through only. + - Python: uv with `pyproject.toml` and a committed `uv.lock`; pydantic-settings for configuration. + - Caching (25): the framework cache serves read-through only; the other strategies keep explicit Redis + clients so they can report `"source"` and survive a Redis outage. + - Go: `any`, not `interface{}` (generated `*.pb.go` files excepted). C++ logging: Drogon services use + Drogon's `LOG_*`; services without Drogon use spdlog. `std::cout`/`std::cerr` only for CLI and test output. + Adding a Conan dependency to an existing lock uses `conan lock create --lockfile=conan.lock + --lockfile-partial`, so the other pins stay. ## House test tools (prefer these; don't introduce alternatives) From ba881487fbc1d4a981679a09ee55c521252daa41 Mon Sep 17 00:00:00 2001 From: PatternCatalyst Date: Mon, 12 Oct 2026 21:15:38 -0400 Subject: [PATCH 5/6] docs(site): verified footers dated by the S13 final matrix (111/111, 2026-10-12) --- _docs/02-communications.md | 2 +- _docs/03-composition.md | 2 +- _docs/04-data.md | 2 +- _docs/05-event-driven.md | 2 +- _docs/06-stream-processing.md | 2 +- _docs/09-api-registry.md | 2 +- _docs/11-observability.md | 2 +- _docs/12-security.md | 2 +- _docs/16-appendix-c-websockets.md | 2 +- _docs/17-appendix-d-sagas.md | 2 +- _docs/18-appendix-e-errors.md | 2 +- _docs/19-appendix-f-ddd-hexagonal.md | 2 +- _docs/21-appendix-h-shutdown.md | 2 +- _docs/22-appendix-i-l7-routing.md | 2 +- _docs/24-appendix-k-monolith-to-microservices.md | 2 +- _docs/25-appendix-l-caching.md | 2 +- _docs/26-appendix-m-failure-modes.md | 2 +- _docs/27-appendix-n-feature-flags.md | 2 +- _docs/28-appendix-o-newman.md | 2 +- _docs/30-appendix-q-blazor-signalr.md | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/_docs/02-communications.md b/_docs/02-communications.md index c55e62ae..be9a7270 100644 --- a/_docs/02-communications.md +++ b/_docs/02-communications.md @@ -413,5 +413,5 @@ The code is in [`examples/02-communications/`](https://github.com/patterncatalys it; its `README.md` covers what it does and how to drive it. --- -*Verification status: verified — [`examples/02-communications/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/02-communications/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/02-communications/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/02-communications/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-12) (REST create and validation (201, 422), cursor pagination, order confirmation and rejection driven by the gRPC stock reservation, a GraphQL orders query, and the order.placed Kafka topic).* diff --git a/_docs/03-composition.md b/_docs/03-composition.md index c31af32c..e8ef7a5d 100644 --- a/_docs/03-composition.md +++ b/_docs/03-composition.md @@ -273,5 +273,5 @@ The code is in [`examples/03-composition/`](https://github.com/patterncatalyst/c its `README.md` covers what it does and how to drive it. --- -*Verification status: verified — [`examples/03-composition/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/03-composition/) passes 8/8 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/03-composition/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/03-composition/) passes 8/8 checks in each of the six languages on Docker Engine (2026-10-12) (GraphQL gateway fan-out to the REST order API and the gRPC stock service, selective stock-field resolution, DataLoader batching of stock lookups into one gRPC call, single-order lookup).* diff --git a/_docs/04-data.md b/_docs/04-data.md index 4f07d0bd..4e42e998 100644 --- a/_docs/04-data.md +++ b/_docs/04-data.md @@ -331,5 +331,5 @@ The code is in [`examples/04-data/`](https://github.com/patterncatalyst/cloud-na `README.md` covers what it does and how to drive it. --- -*Verification status: verified — [`examples/04-data/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/04-data/) passes 8/8 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/04-data/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/04-data/) passes 8/8 checks in each of the six languages on Docker Engine (2026-10-12) (transactional write of order and outbox row, Debezium outbox connector running, relay of the outbox event to the order.placed Kafka topic with its payload).* diff --git a/_docs/05-event-driven.md b/_docs/05-event-driven.md index ec79892a..e03082fe 100644 --- a/_docs/05-event-driven.md +++ b/_docs/05-event-driven.md @@ -1295,5 +1295,5 @@ it; its `README.md` covers what it does and how to drive it. The same example, w the Strimzi operator, also runs on a Kubernetes cluster; Appendix R covers that deployment on minikube and Appendix S covers OpenShift Local. --- -*Verification status: verified — [`examples/05-event-driven/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/05-event-driven/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-10) and on minikube (2026-10-11, Appendix R), and 10/10 in five languages on OpenShift Local 4.22.14 (2026-10-11, Appendix S) +*Verification status: verified — [`examples/05-event-driven/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/05-event-driven/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-12) and on minikube (2026-10-11, Appendix R), and 10/10 in five languages on OpenShift Local 4.22.14 (2026-10-11, Appendix S) (event publication to order.placed, fan-out to two independent consumer groups (shipping and notification), and idempotent consumers under a redelivered duplicate event).* diff --git a/_docs/06-stream-processing.md b/_docs/06-stream-processing.md index b3bab260..bf7f2807 100644 --- a/_docs/06-stream-processing.md +++ b/_docs/06-stream-processing.md @@ -339,5 +339,5 @@ The code is in [`examples/06-stream-processing/`](https://github.com/patterncata runs it; its `README.md` covers what it does and how to drive it. --- -*Verification status: verified — [`examples/06-stream-processing/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/06-stream-processing/) passes 11/11 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/06-stream-processing/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/06-stream-processing/) passes 11/11 checks in each of the six languages on Docker Engine (2026-10-12) (per-merchant revenue aggregation onto a derived Kafka topic, the stream-processor consumer group, consumer lag that builds while the processor is paused and drains on resume, and the burst appearing in the revenue stream).* diff --git a/_docs/09-api-registry.md b/_docs/09-api-registry.md index 5f5edeaf..efc7336f 100644 --- a/_docs/09-api-registry.md +++ b/_docs/09-api-registry.md @@ -340,5 +340,5 @@ breaking change failing the build and the additive change passing it is the enti point of the registry. --- -*Verification status: verified — [`examples/09-api-registry/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/09-api-registry/) passes 8/8 checks on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/09-api-registry/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/09-api-registry/) passes 8/8 checks on Docker Engine (2026-10-12) (schema registration, BACKWARD compatibility rule, rejection of a breaking change with 400, acceptance of an additive change, versioned retrieval).* diff --git a/_docs/11-observability.md b/_docs/11-observability.md index 9a873ff6..67bbd7e2 100644 --- a/_docs/11-observability.md +++ b/_docs/11-observability.md @@ -442,5 +442,5 @@ the rest. Seeing one request as one waterfall is the moment observability stops being three disconnected tools. --- -*Verification status: verified — [`examples/11-observability/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/11-observability/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/11-observability/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/11-observability/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-12) (trace propagation, metric export, structured log correlation, LGTM stack integration).* diff --git a/_docs/12-security.md b/_docs/12-security.md index 928c297b..77287bda 100644 --- a/_docs/12-security.md +++ b/_docs/12-security.md @@ -726,5 +726,5 @@ step pre-merge. A request without identity being refused, and an unsigned image being blocked twice, is the layered model working. --- -*Verification status: verified — [`examples/12-security/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/12-security/) passes 16/16 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/12-security/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/12-security/) passes 16/16 checks in each of the six languages on Docker Engine (2026-10-12) (sidecar identity trust (403 without X-Forwarded-Client-Cert, health-check bypass), propagation of the JWT subject claim, valet-key mint, verify and tamper rejection, per-tenant bulkhead state, and conftest Rego policy evaluation of a good and a bad manifest. JWT signature validation, RBAC enforcement and rate limiting are not covered).* diff --git a/_docs/16-appendix-c-websockets.md b/_docs/16-appendix-c-websockets.md index 4ca924a0..046dff68 100644 --- a/_docs/16-appendix-c-websockets.md +++ b/_docs/16-appendix-c-websockets.md @@ -555,5 +555,5 @@ Deployment down by one and confirm the dropped clients reconnect with backoff an resume from their last sequence rather than losing the stream. --- -*Verification status: verified — [`examples/16-websockets/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/16-websockets/) passes 6/6 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/16-websockets/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/16-websockets/) passes 6/6 checks in each of the six languages on Docker Engine (2026-10-12) (WebSocket connect, cross-pod delivery and broadcast over the Redis pub/sub backplane, monotonic sequence numbers).* diff --git a/_docs/17-appendix-d-sagas.md b/_docs/17-appendix-d-sagas.md index 602c57a8..72b77823 100644 --- a/_docs/17-appendix-d-sagas.md +++ b/_docs/17-appendix-d-sagas.md @@ -328,5 +328,5 @@ inverses over only the completed steps, plus a clean resume, shows the saga works. --- -*Verification status: verified — [`examples/17-sagas/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/17-sagas/) passes 11/11 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/17-sagas/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/17-sagas/) passes 11/11 checks in each of the six languages on Docker Engine (2026-10-12) (orchestrated saga happy path, compensation in reverse order on failure, skipped compensation for steps that never completed, resume of a RUNNING saga after an orchestrator restart).* diff --git a/_docs/18-appendix-e-errors.md b/_docs/18-appendix-e-errors.md index 3029aa76..268af6b5 100644 --- a/_docs/18-appendix-e-errors.md +++ b/_docs/18-appendix-e-errors.md @@ -853,5 +853,5 @@ load. A flat downstream-call line under rising pressure is the retry budget and doing their job; a climbing one means a control is missing. --- -*Verification status: verified — [`examples/18-errors/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/18-errors/) passes 13/13 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/18-errors/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/18-errors/) passes 13/13 checks in each of the six languages on Docker Engine (2026-10-12) (RFC 9457 problem details over REST for validation (422), stock conflict (409) and upstream unavailable (503), with error codes, traceId, retryable and retryAfter fields).* diff --git a/_docs/19-appendix-f-ddd-hexagonal.md b/_docs/19-appendix-f-ddd-hexagonal.md index feacdf1f..da9bb5e3 100644 --- a/_docs/19-appendix-f-ddd-hexagonal.md +++ b/_docs/19-appendix-f-ddd-hexagonal.md @@ -354,5 +354,5 @@ new adapter file appears. A clean domain import list plus a no-domain-change dif you bolt on a protocol is hexagonal architecture holding. --- -*Verification status: verified — [`examples/19-ddd-hexagonal/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/19-ddd-hexagonal/) passes 17/17 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/19-ddd-hexagonal/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/19-ddd-hexagonal/) passes 17/17 checks in each of the six languages on Docker Engine (2026-10-12) (source scans proving the domain layer imports no framework or persistence types, REST and CLI adapters driving the same use case, input validation, OrderPlaced event emission).* diff --git a/_docs/21-appendix-h-shutdown.md b/_docs/21-appendix-h-shutdown.md index 004605ee..41913a83 100644 --- a/_docs/21-appendix-h-shutdown.md +++ b/_docs/21-appendix-h-shutdown.md @@ -331,5 +331,5 @@ A clean rollout under live load, not a green integration test, is evidence that works. --- -*Verification status: verified — [`examples/21-graceful-shutdown/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/21-graceful-shutdown/) passes 17/17 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/21-graceful-shutdown/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/21-graceful-shutdown/) passes 17/17 checks in each of the six languages on Docker Engine (2026-10-12) (one SIGTERM, readiness flip to 503, the drain log lines, exit code 0 inside the grace period, a `docker stop` timing check, restart recovery, and orders persisting across the restart. In-flight request draining is not checked by the script).* diff --git a/_docs/22-appendix-i-l7-routing.md b/_docs/22-appendix-i-l7-routing.md index 5345c76a..055093dd 100644 --- a/_docs/22-appendix-i-l7-routing.md +++ b/_docs/22-appendix-i-l7-routing.md @@ -681,5 +681,5 @@ track the config and a ruleset that can move routing without a code change are t this appendix working. --- -*Verification status: verified — [`examples/22-l7-routing/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/22-l7-routing/) passes 15/15 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/22-l7-routing/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/22-l7-routing/) passes 15/15 checks in each of the six languages on Docker Engine (2026-10-12) (Envoy weighted traffic split, header-based routing override, in-app rule-driven routing, and changing routing rules without a redeploy. Canary promotion and circuit breaking are not covered).* diff --git a/_docs/24-appendix-k-monolith-to-microservices.md b/_docs/24-appendix-k-monolith-to-microservices.md index 758bf6b2..6140dac3 100644 --- a/_docs/24-appendix-k-monolith-to-microservices.md +++ b/_docs/24-appendix-k-monolith-to-microservices.md @@ -586,5 +586,5 @@ write *and* that an `order.placed` event landed on Kafka. Same client-visible AP underneath, legacy untouched — that is the decorator doing its job. --- -*Verification status: verified — [`examples/24-monolith-to-microservices/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/24-monolith-to-microservices/) passes 19/19 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/24-monolith-to-microservices/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/24-monolith-to-microservices/) passes 19/19 checks in each of the six languages on Docker Engine (2026-10-12) (strangler fig router with per-tenant routing, reversible cutover, a decorating collaborator adding caching and Kafka event emission, and an untouched legacy service).* diff --git a/_docs/25-appendix-l-caching.md b/_docs/25-appendix-l-caching.md index 48ae8645..16142b4d 100644 --- a/_docs/25-appendix-l-caching.md +++ b/_docs/25-appendix-l-caching.md @@ -1225,5 +1225,5 @@ the new value with no DB round trip; for write-around, write then read and confi miss (the DB log shows the `SELECT`) while the second is a hit. --- -*Verification status: verified — [`examples/25-caching/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/25-caching/) passes 29/29 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/25-caching/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/25-caching/) passes 29/29 checks in each of the six languages on Docker Engine (2026-10-12) (cache-aside with invalidation on update, read-through, write-through, write-around, write-back with 202 and a flush retry across a Postgres outage, refresh-ahead, hot-set tracking, and degradation with Redis stopped and restarted).* diff --git a/_docs/26-appendix-m-failure-modes.md b/_docs/26-appendix-m-failure-modes.md index 6554928e..798ac9ee 100644 --- a/_docs/26-appendix-m-failure-modes.md +++ b/_docs/26-appendix-m-failure-modes.md @@ -750,5 +750,5 @@ This appendix touches difficult operational territory; if you are diagnosing a l Google SRE cascading-failures chapter is the fastest way to a shared vocabulary with your team. --- -*Verification status: verified — [`examples/26-failure-modes/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/26-failure-modes/) passes 20/20 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/26-failure-modes/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/26-failure-modes/) passes 20/20 checks in each of the six languages on Docker Engine (2026-10-12) (timeout, retry exhaustion, circuit breaker closed, open and recovered with fallback, deadline-budget propagation, bulkhead limit).* diff --git a/_docs/27-appendix-n-feature-flags.md b/_docs/27-appendix-n-feature-flags.md index d66bf7dc..7c2af5f4 100644 --- a/_docs/27-appendix-n-feature-flags.md +++ b/_docs/27-appendix-n-feature-flags.md @@ -417,5 +417,5 @@ lookup is on a critical path without a default, which is the one feature-flag mi ship. --- -*Verification status: verified — [`examples/27-feature-flags/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/27-feature-flags/) passes 14/14 checks in each of the six languages on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/27-feature-flags/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/27-feature-flags/) passes 14/14 checks in each of the six languages on Docker Engine (2026-10-12) (plan-based targeting rules, sticky assignment and percentage rollout, kill switch, simple boolean flag, flag listing, and safe defaults while flagd is down).* diff --git a/_docs/28-appendix-o-newman.md b/_docs/28-appendix-o-newman.md index a15918e6..dadc8511 100644 --- a/_docs/28-appendix-o-newman.md +++ b/_docs/28-appendix-o-newman.md @@ -276,5 +276,5 @@ service and fails against one that wrongly accepts it. A suite that goes red on break, and only then, is the whole value of this appendix. --- -*Verification status: verified — [`examples/28-newman/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/28-newman/) passes 11/11 checks on Docker Engine (2026-10-10) +*Verification status: verified — [`examples/28-newman/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/28-newman/) passes 11/11 checks on Docker Engine (2026-10-12) (a Newman collection run with status-code and state assertions (201, 422, 404, 204, cancel) and JUnit XML export).* diff --git a/_docs/30-appendix-q-blazor-signalr.md b/_docs/30-appendix-q-blazor-signalr.md index 08c13408..90f40de4 100644 --- a/_docs/30-appendix-q-blazor-signalr.md +++ b/_docs/30-appendix-q-blazor-signalr.md @@ -179,4 +179,4 @@ The code is in [`examples/30-blazor-signalr/`](https://github.com/patterncatalys runs it; its `README.md` covers what it does and how to drive it. --- -*Verification status: verified — [`examples/30-blazor-signalr/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/30-blazor-signalr/) passes 10/10 checks on Docker Engine (2026-10-10): health, order placement and listing, the dashboard page, the SignalR negotiate endpoint, a second dashboard instance, Redis backplane delivery in both directions between the two instances (a SignalR client on one receives an order posted to the other), orders listed across instances, and backplane delivery to a restarted instance (the script asserts the container's start time changed and it returned healthy). Blazor circuit reconnection is the framework default and is not covered by the script.* +*Verification status: verified — [`examples/30-blazor-signalr/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/30-blazor-signalr/) passes 10/10 checks on Docker Engine (2026-10-12): health, order placement and listing, the dashboard page, the SignalR negotiate endpoint, a second dashboard instance, Redis backplane delivery in both directions between the two instances (a SignalR client on one receives an order posted to the other), orders listed across instances, and backplane delivery to a restarted instance (the script asserts the container's start time changed and it returned healthy). Blazor circuit reconnection is the framework default and is not covered by the script.* From 6f790008402c173788fd60047f54e38143850a8c Mon Sep 17 00:00:00 2001 From: PatternCatalyst Date: Mon, 12 Oct 2026 21:15:38 -0400 Subject: [PATCH 6/6] docs(plan): S13 done --- _plans/modernization-plan.md | 2 +- _plans/s13-idiomatic-plan.md | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/_plans/modernization-plan.md b/_plans/modernization-plan.md index 9156afdf..bfcd7a48 100644 --- a/_plans/modernization-plan.md +++ b/_plans/modernization-plan.md @@ -35,7 +35,7 @@ Each step gets its own branch, a PR, green CI and a squash-merge before the next | S10 | feature/openshift-local-appendix | 8 | done (PR #68, 48e7736) | | S11 | docs/professional-voice | 9 | done (PR #69, 2d8c2a8) | | S12 | docs/deck-refresh | 10 (expanded 2026-10-11) | done (plan: _plans/s12-deck-refresh-plan.md; examples 01, 21, 25 fixed and verified 6/6) | -| S13 | refactor/idiomatic-examples (5 PRs) | 11 (added 2026-10-12) | in progress (plan: _plans/s13-idiomatic-plan.md) | +| S13 | refactor/idiomatic-examples (5 PRs) | 11 (added 2026-10-12) | done (PRs #73-#77; 111/111 PASS 2026-10-12; plan: _plans/s13-idiomatic-plan.md) | | S14 | refactor/resilience-libraries | 12 (added 2026-10-12) | next: circuit breaker, bulkhead, Spring gRPC via libraries | diff --git a/_plans/s13-idiomatic-plan.md b/_plans/s13-idiomatic-plan.md index 730c9233..c5c15ced 100644 --- a/_plans/s13-idiomatic-plan.md +++ b/_plans/s13-idiomatic-plan.md @@ -97,10 +97,10 @@ compatibility). Python: uv 0.13.0, `astral-sh/setup-uv` v10.3.0; package version | 3 | Quarkus batch (about 26 projects) + site tabs + `Quarkus.fix5.json` | 2 | done; matrix 2026-10-12: 23/23 PASS (Quarkus plus 24 in all languages) after three fixes (JSONB format mapper, 19 422 status, `-parameters`) | | 4 | Spring batch (about 26) + site tabs + `Spring.fix6.json` | 3 | done; matrix 2026-10-12: 18/18 Spring Boot PASS | | 5 | .NET batch (about 28) + site tabs + `DotNet.fix6.json` | 4 | done; matrix 2026-10-12: 19/19 .NET PASS | -| 6 | Go (`any`) and C++ (spdlog) + any deck/site follow-ups | 5 | pending | -| 7 | Final full matrix (111 targets, one at a time), footers with verified counts | 5 | pending | -| 8 | CLAUDE.md locked stacks (ORMs, health, shutdown, uv, schema ownership), modernization plan | 5 | pending | -| 9 | Opus validation per PR (re-scan, checklist sample, `db/init` unchanged, three CI checks green) | each | pending | +| 6 | Go (`any`) and C++ (spdlog) + any deck/site follow-ups | 5 | done: Go `any` in 27; C++ Drogon services on `LOG_*` with stdout line-buffered, gRPC inventories and consumers on spdlog 1.17.0 (locks extended with `--lockfile-partial`); `DotNet.fix7.json` (HybridCache, not FusionCache, as the read-through default) | +| 7 | Final full matrix (111 targets, one at a time), footers with verified counts | 5 | done: 111/111 PASS 2026-10-12; 20 footers re-dated, counts unchanged | +| 8 | CLAUDE.md locked stacks (ORMs, health, shutdown, uv, schema ownership), modernization plan | 5 | done | +| 9 | Opus validation per PR (re-scan, checklist sample, `db/init` unchanged, three CI checks green) | each | done: criteria 2-5 by grep, `db/init` unchanged since the plan commit, three checks green on PRs 73-76 | Order inside a batch: 01, 21, 04, 05, 17, 19, 25, then the health-only examples. Run `scripts/verify-matrix.sh NN-x/` after each example (Docker runs strictly one at a time). Decks follow the S12 method (edit files applied once, @@ -136,3 +136,9 @@ asserts; Spring lifecycle phase versus `WebServerGracefulShutdownLifecycle`; Hib and Kafka payload drift (DTOs only, compare with kcat); idempotent consumers under JPA/EF rollback; Hibernate/EF memory and start time against compose `--wait` and the 05 k8s limits; deck `expect` counts (measure on committed decks); Python lock drift (diff `uv export` against the old pins); long matrix runs. + +## Not re-run + +Example 05 on minikube (Appendix R) and OpenShift Local (Appendix S) was not repeated after S13 (D7). Its +Quarkus, Spring Boot and .NET services changed (ORMs, health endpoints), so those two footers describe the +pre-S13 code; the deploy scripts accept the new `"UP"` health bodies.