From 3537550bd325566411cc90cfe969d5e09d673c41 Mon Sep 17 00:00:00 2001 From: zakstucke <44890343+zakstucke@users.noreply.github.com> Date: Sun, 7 Apr 2024 19:32:46 +0300 Subject: [PATCH] Redis and logging improvements (#30) * Improved redis interface * Distributed lock implementation for redis * GlobalLog wait for collector for 10 seconds, don't just error when not available straight away. * Otlp metric network/client impact reduced in js --- .zetch.lock | 25 +- js/bitbazaar/log/log.ts | 2 +- js/bun.lockb | Bin 253213 -> 252506 bytes js/package-lock.json | 261 ++++++++--------- js/package.json | 22 +- opencollector.yaml | 2 +- rust/Cargo.lock | 15 +- rust/Cargo.toml | 17 +- rust/bitbazaar/log/global_log/setup.rs | 16 +- rust/bitbazaar/redis/batch.rs | 289 +++++++++++-------- rust/bitbazaar/redis/conn.rs | 47 ++- rust/bitbazaar/redis/dlock.rs | 380 +++++++++++++++++++++++++ rust/bitbazaar/redis/mod.rs | 27 +- rust/bitbazaar/redis/wrapper.rs | 33 ++- 14 files changed, 810 insertions(+), 326 deletions(-) create mode 100644 rust/bitbazaar/redis/dlock.rs diff --git a/.zetch.lock b/.zetch.lock index 2a08f6f6..c72005b0 100644 --- a/.zetch.lock +++ b/.zetch.lock @@ -1,23 +1,24 @@ { "version": "0.0.10", "files": { - "js/tsconfig.zetch.json": "fb5d57b825bb3c2f6dd4254bf939f2444e52946622a7f93b91e3acb75876ebbc", - "README.zetch.md": "be6ab7e03e141946131c589b3ac1df2e55d2b8bb53757499469738037e06077b", + "CONTRIBUTING.zetch.md": "bace46dc064746b54cf472eba960d934d705c2f83120b865a4b47032ff1552c5", + "py_rust/README.zetch.md": "e0cd5a8e29b788cc787becd1b2882cc735558a12b63b6e74f4ce69f0bb1f2188", "py/README.zetch.md": "e0cd5a8e29b788cc787becd1b2882cc735558a12b63b6e74f4ce69f0bb1f2188", "py_rust/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", - "rust/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", "js/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", - "docs/index.zetch.md": "e0cd5a8e29b788cc787becd1b2882cc735558a12b63b6e74f4ce69f0bb1f2188", - "opencollector.yaml.zetch": "678a691ae64d7f9893e8799ea657842fe051b3fcce4da568969d8de070a29393", - "docs/CONTRIBUTING.zetch.md": "bace46dc064746b54cf472eba960d934d705c2f83120b865a4b47032ff1552c5", - "py_rust/README.zetch.md": "e0cd5a8e29b788cc787becd1b2882cc735558a12b63b6e74f4ce69f0bb1f2188", + "rust/README.zetch.md": "e0cd5a8e29b788cc787becd1b2882cc735558a12b63b6e74f4ce69f0bb1f2188", + "opencollector.yaml.zetch": "33aa6a1e9e386b9e01c3d4c010b70260654c06850fb1433afb15545bd85f2f2d", + "js/tsconfig.zetch.json": "fb5d57b825bb3c2f6dd4254bf939f2444e52946622a7f93b91e3acb75876ebbc", "CODE_OF_CONDUCT.zetch.md": "bf106326ffc75f5167cfde27c997c77c6b97c843a9e392b564355d0e70e50b97", + "rust/pkg/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", + "LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", + "rust/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", + "docs/CONTRIBUTING.zetch.md": "bace46dc064746b54cf472eba960d934d705c2f83120b865a4b47032ff1552c5", + "py/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", + "README.zetch.md": "be6ab7e03e141946131c589b3ac1df2e55d2b8bb53757499469738037e06077b", "docs/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", + "docs/index.zetch.md": "e0cd5a8e29b788cc787becd1b2882cc735558a12b63b6e74f4ce69f0bb1f2188", "js/README.zetch.md": "e0cd5a8e29b788cc787becd1b2882cc735558a12b63b6e74f4ce69f0bb1f2188", - "py/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", - "CONTRIBUTING.zetch.md": "bace46dc064746b54cf472eba960d934d705c2f83120b865a4b47032ff1552c5", - "docs/CODE_OF_CONDUCT.zetch.md": "bf106326ffc75f5167cfde27c997c77c6b97c843a9e392b564355d0e70e50b97", - "rust/README.zetch.md": "e0cd5a8e29b788cc787becd1b2882cc735558a12b63b6e74f4ce69f0bb1f2188", - "LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b" + "docs/CODE_OF_CONDUCT.zetch.md": "bf106326ffc75f5167cfde27c997c77c6b97c843a9e392b564355d0e70e50b97" } } \ No newline at end of file diff --git a/js/bitbazaar/log/log.ts b/js/bitbazaar/log/log.ts index 90abd53e..8f869c24 100644 --- a/js/bitbazaar/log/log.ts +++ b/js/bitbazaar/log/log.ts @@ -252,7 +252,7 @@ class GlobalLog { ...baseExporterConfig, url: `${otlp.endpoint}/v1/metrics`, }), - exportIntervalMillis: 1000, + exportIntervalMillis: 60000, // Haven't found a way for it to not send when no metrics yet, so changing from 1s to 60s to not bloat the network logs of a client. }), ], }); diff --git a/js/bun.lockb b/js/bun.lockb index 228139c5e7d80db79927c1b4e8f40190a2f036a3..6498795b4401e775b6b4ecea11ccf1092337e00a 100755 GIT binary patch delta 59925 zcmeEvcUToy`|X^OqZ|u%5k$m-1qBfcD0;AWv3EsLQ9z0U_HwXb7t5$)#on-DjlH+n zP3*=*6B8wBG|?p1+_m=1(KCF>cYn`)?(e^x$Hm(3yY{>L-ZN*=aoPU7cXs8S?N{Ms zfy7p)*3I|y8y9q?*Q?EcUaz)l{)Bz&kC%8OKiY7p^yn{dXOsBpJEwCATjw6KFti^Q zNlG3AM_%x2unTwya_0tzgA0Kl!=DG-J-a031h)h;e?^^ZfeXO)(Ct@d&EBqaRKGr= zvIVf+ZOE7%8H<4L&;dRUE(#t1b_I{s?IDB0lOhqC6cIaU1UOGFtsyAB^QbM&FZ1SKX zaU=T1_luYYKS#h5`B`y7d`!%cL6X!zF=A*$TvB3EL_%Uj0?I{gjJKEDCBG!0jmal9zB%xOST~iQNGcMlJrLjNy2C+e-CDlzXo$vUq?V0 zuv$`^n$hSUdv+DfhWUe80fst$80NA+7830`@Uwx5u-UP*u*tb}zK=ptpDnq)x4zor z2lZo%LSVB21CfzaGqbE_cZAI~Qb_mDDyLP@6gDgL2D3tcA1!VMm>uXJpWqu2-Z#=x zLDOfH*UBj=beoj?zM{6MW>(T#SPL21(lM|(S219*Ms)op^0T76D1fWG0rGPs{seQ8 zKG69xnEGxohkhBD+jo9o)nCp-GFd2&p@4_N*;9C%6n) zi~yJodyAg1hcCd?@8~@{0cJyX>AG(oMmlU z*Wy2Ij(^$|;fW*S`eG)JVCk^E_Ts+_(w11in%V$&2D5+7bzXz~XqPQHAtEt8W@v;| zC|HuP43l$$*}?a#Jn6B%yQ4p6- z*QRDLY#Z0$^G=#@5zK~c>a6v&a~EwS9wNXU{_W6Eb2*xqw2SwJ|L0ewOxsWSLlf31g&A~AQcOMn^w+W@V=TFB3a6b0jmO8%w2R!(iiGv6`T zY;bf>&C>S2H@G0;@`DS3|4P*lJg|+9TSzd&Xrhh582zZKa4+isE z(_QyB1v9Riu9pHAfbF8&f6mn6AAyKRvUU=dSfK6y zJ_+G{BN8QPk#2trc0pVfRm&F}kuV@40ENPz3w~T=#Z8#ULM`42{(SKF4^K=Q%qfZ( zk{H#0gtT%Y_CH(JXOVVNZ3X9r{c65`hNB_W6QdG_a&D92lfrp6$0o!^#bUN|Ezv57 zA0(E9go=kH#z<0!rCP)CfEjPMKOjFF@*K>1l59~)F%eNCBO-=J^bJP^v1xjP!0btU zTtpIULUmDnef!6Cf{C40wFk*!Lw!aztdEgHL^Te#C#}A8)>Klo2qY_aA`0)y@ z;(aT%{5E?*Uo6w+ZqX{O@~jR1r-s@aX1{k`x>#ES`u%|| zIqQwVY-C^$JL^Ng5v^vA?yT4N3vemetHE4M3CQ3H&X%t2lGm`gHd1xnem{`)>Y5DQ zh1dU~NU(yy%^Lqxz(C~V2-v^S^o@)k7}XDVZxP}B@C9s8@D{CNU!5&mwL3ifq7084 z5gVS=H_~v2#p}QQi_1UVV)a7B91;6hzNolFtil+n0Y-@1=)g8@$}+(0NK#@SRNr5)zfc<>)xBo$b}ZC1GcW zzXJF>^n_QfQ{ZCY7ALg=k`nn{T#`0H;Ls)x>6?I&mByad8Zam-Av_63XY5GklBAwz zw5d&sjELh2XltZ9+JL#=V-c7iZ2uB#U+wm<^$IHDVq{A=*CWDXV_0zdIjsTF@px(w zr+o?TGeVLQW5jo2DQZAmd_n{kWmJMVn=79e`EALQzSLUOH@q*)krGfahp6-gtwAf` zW6x%Tv0#%Yg4qD9jieE{U#opl^H&73;Ze{zLS6Oz4ZvKSSO=mUeJxAXF0&l$|GG%9 zf=8FMA+uf4Di#@iqY~}gawz=F7Y^pZW7x;A|8Lv4Gvc^C-L7do0i2Kf|B>#vt1~Vl zi4jRWvwdU32T9UM_*rorm_6?W=JuQQwYI1lqhR(N>qD$=^}3e-E|~f3Pj-gI55dD4 zN!ouyi`x$NV2fg-l2Dxp5*6j}Q<8UGT{cr`TI{UzjnKyHf1dTAnv&1MUzuH`e7==a z=eAySA#rM(n7d8$uB|sm`Lkf8(#FHT=y~TSWt)e7d0|fFZZpg7u2a(zU;A`s+rD=a?~NPT2mRP)WVTiv7wuJ&J&G#+McOC}JqG2=Q{>NXFJ^o6{CtC;YgoxW@b{JoZ(&C*0{oNPrV8? zJz1wnXhLY`(uEE$TYmHE(~9l(EpJnHWV7jK$L6|u@&3Cmn|u6PGNxUHuiN)^xlwUx z*j}mBla%R)uPn%Q`_=p|dvXnqUo`RY8qcE-UgUO-j=c7^mGamvNGV#pY2CezS1&zV zse1F_z7>BS?c@Hq;QcX2zUo`EK#i6oEBY<}#qYMO>Q^AS{|`k|&MrFKW#jkvItOh| z_1*VP?BatZ=Y$_Vt_&~k<>k?M+|b9j(!_%EHqAN|(~*o!2f%dusOTT~9e%I>>pscXFkQDG`0&9wAx z<3E-UQhJvuulN@Yn(OATd{L&2bFRogqpGD}S+&#IyQxWuEIVAeQn+`%8!k2D3KVY9 z?1!iO-H&>GxT<_nB*-hX-RX0;BGyGGo;-E;{QO@cx86JOv}9r>_e(z<8niI=^|wl7 z(cWI8iUz-pn%trP?4NGkcbk23o_up~vRW^oV!Q9x4V>3!TPNjgQ7`31(KgCaxAM;8 zOf_bB*KfX1neATG*=f(UVm+imWfiYtUgf9X?$C08@98}|8+w-8aLsn-kITn%@A&;d zrJR-9`X`*uGy3>e=Ud)goI5Drm2IhP?pxbZJewpn#GcBbRB0Y)83(KGC+irjX0T-C zW#d3gPA5sK|4EC;E=jdNSu0^R{$%}RuzYh!Qp-=;2w0zU9x-USoE>v^hUF*XGAagI zQegG?RLTQb9X?sraoT*==D@;(0KG>~V0HRrh2Z%3tf{a9pqZ4xsT$JxV~s({SsL=!s2*3F^lCjEdNj8 z-Z($CCS{ z`4t}@tNcxVB^p$+fRYaCP(aB9T_~XVRJ6*i3M$c{6$O>_idIV|F0jo}gQQfc5NK{z z2s5qt`UG2MA;p@}4y>_ESXNkARp?DsT$*T^mBFn8<-vuO^cbt!0; z;ca1YiHcE|=M+(*D_P~6MU-?IeXfNEkiRHm7P-{PxDkV(tdsJxZlJ{qD-?0ESdTMcX~T-SHs6EQScz>JY$=7i!zhspgCTD(sYDM(=Spdf z5_2GbTS`gqW;K^7jdm!pwSq09k>b`6Q*GWZ{9I4JBBgb6aDzaLe;NCB;=o4JQl8Ze zwygQ&=g_=_#a3c2TL)SOdTZx^m<-DaSez(rYJP*&My%TgWwrGzHnDjnEchD*TV5j7 zLF9jlvKp1sa%m+mhNZO)r{8WBWpm`H4jG=$9Ie#TdYA2S;0;IT9(Bj&GmMfnuaZdZ%7aA;cJ6O#{ zZ@w6c1`YNVBnQCka(r>C|i-Wja6l|RLm4|0aVBN*!tmx?#A zy4WpqTa0ZxZ}8+gXs1gq_nPO)C#ox z1WRujrZgC*)LFu9v1(g$_1DL7hM1zP68(mOOg$RyfqajIz_G)}>2SZojq zL6Li4X(vNQ{UB3KC3=+AQVnuvHE|sl7Z!@_ zhRZ7iZ87qa&_TDHM0KgKwE07S0_*x0(G=0#t_vEG*5vQq{Q|Lwk$x;F(Ww2 z;FrI&D(PdbmK-7WvC9YvwA6%!9m09QNJhZYI?n?t16D0jP|>=M{(i8!D9^%!%?FXH zD{_pgr_EU|EqB33^bQ%pa+}#Ar+zc z_6e5HHBd6gS>-|v6`%1|xp6}!x|da+*icCyZ#DmgRflSmgXPYR6rTxJ^9~GIKjqoD zU`uxl2}cYAfP-Z|tQyLeiouriNTDg1{fxo5NHtNSCt58ho7jh*!^^$F#im_Yf|@Fs z6Rq;7rixFBRle9%iB7Sa3pK;4R-W|=HYXs}m?_H_Od$;G9@oT=%|t_6H&@beiA}?G z9v7ls!See~O8O+6Y@L~fRxeukn1Jj|;By-Og7A-86VkGl(r>JiuUt_}`)>Bbfk((xSVNU9x zQcz-3YdayKiIp%2NkUwKRJ2ItL?vO$mU*?EkeI|#Gkf*JmulfcB2I~&QQIj(k|LR1 zPKi)5Q?2G(aA+m9>@UK?F*E&@^jTJU^Z+Gumem{{DM{VA0L=%HieteRA6$_~q8Hj? zT?1>7XhR{4Q-6^fiBuBzf&4sLNuOgicOS_9aAVv?YM8QRPO!O6jJ@&uks2ygf2@Ld zp)N;CE23bW60KOx<1p@1L?%nTB#jf9rXi)(_yVbEqT3?}v6IiH)OJE5fs4)DVK7si z@6$+$5it8Du*3B2LJFa1n>A6ACMvN?u=zMreS}&jiLIoXid0`w?MtMzspvUG%-y(P z^8utri+q7tJVQjj&0I#Jlzzh`NfGPp52P@RC@*3-o*#*P7m$h*`D%@@kI5ROv^w%( ztcO#Tr;Jp57F*4a;n0RFY?K)LS;6K@NF^xG7S(nd%{Z3787XZI`HZoz_tQwli|K5G zh1*r6HX?;yV^jLtl=P)mGk;xxB{!qCDOvGZW;G8_7L7tJ50UC42B_gUdy{wAQ?l21 zB|6Poe&cvaTFK=hi6=<-seNX`?*YH(lG{VS%iC}rH>TWtf|8kLHUBsPziAR9Fma+J z9b`vLDN6cst7T(~{Tjq8%w1Skl!4<6mfIxF!d-5;K)KH(C9}L$&Hz=inj24%q;ATy zO2Ot8Nc9k@_ekNmg0I_DySfW0oSpFHo@Q60kwS!TZ6_pfjzaXAE^eayYC9o;QwgqX zNNJHiGxSJ#$_&NFAK!CkDA6E~nMx+8`%J~Biq+hF77xEI)oME-fv-4Kg3Vte)l{TB zW>ciEH&U8!15#SP=bzMoId*jIS6rQjpS0uA$g#8;O+W zJC2kV;WE!2(E%yk2BKAINHtYrtJHQvLdzB~Uk|m6M5-=k1y@rXa{FP`f`uC?9C8&F zDCzaAmWTz~1y_7SwXB9!8(DIQX?zB&;ipNhrE1~g(Sn?$Dw%bymJM)f^Ug0hKdMS} z1FNO%Lah|s8&t&fFH|xcSmj*{6`zJy%TLg1piFUlDi>d*q{A7vNNXpKU#yoyixi(m zR!g?U_zOD7iBjqX%Jmm3nJcX3i*VwLR_$PO;U&Ch*it^&(h8|+A|tK@^0*~R=1QyO zFr5ByTEy2mlYhbm5g(C&`OTWXyT@5C``#O6JAD=9P6^vY1U05@>)+w1Atd{)iwdNo+ zCQ#0{K}ip^T0%BxqltAtIZz%2=SDms*r-(^mbhg$tOyi_EKz|L&vfm!MzhAiY6wl6 z+hefUI4!Q=CT+3h5Ko?(z~ZQCImf}`U}$5!7ZyuFb1}j1V11t8dYd&%J5m4VWsX4{ z7Y2qD!*&1`Yj@_C3HkMAC9}EJ(rk-12Y7x}DbPF)7EU-UfKyB%vo<>eWX-SW_s- zxY7G1zSU(>>I%%__TYQ_{ELk=ZuwF@)G@mXom9Dcs3* z477ZN#bMLFAJ*D#-wBv(OAIWoHceXti-WIOKfq#~wr9LDw4C_Xg-1(WU~wJc?hq?c zfyMf;f-3}CF2Z7**6Bh!wBgon#@p>s(sx+pBRiDL9ahV0Xxt-sw8w9uJGD4*$XSNL zVwpH~#$#8&Y7PrMLe6)v7>9XSfmOUqiQZ+kRNSq7?GfST&ai5W4X_R=79DjLo|dl`UsY`?YTo*AJM)lVuBh3%Il9P zK8LJk&!gfZf~QRVk?JHuUrr9R9E4RzSU9Niyn)q6Z^`FYcRz+(N8S6WeR(dz8~Go3 zhaA@`m+<*q!bNy{{72pjCvYeFAKv-!_Rs3Iyo0w5yxPdmIH|Q$v+AGHF0szsB=YrB zicgSLu5nt42F*RKq=UXctz?4S&L}=Lt(M3$+Fn7pZ;0CDu4U=H~4#23#a{(z<3TLhiA*Uf#A0E=ge?jqSWt9^yDAAxT7nF3+iwjC-E33KXMXVk^K9$E`RH8$z^2v)z zdZ^W6xuiXO5qDj3?@NkLYpcBSk`mq8YJLMPScxqcEH}HXM3=+&zspKG=)z^i2bZxN zSG1cXoKbi*G6PlvG^LpGa%Ye;3|>F;iwC*RSGA^#I}W+cRVA~HRi1WL@oD@2_tDfh zsHZk+MLjJ8Ev2t%mUw1nQDAX%V)x@X`sI_xD|f}O>^U!Wm? z%m$)3(SdQg|GzOua6$(?AVm*wWHxAu?kBTBQ+1xE`^n5VQ}?IRuY7a7gpI||0jSOc z7(E~0$C2q@0FYIHA2Q<>)4=DyF(-TlGvTuWU^y$bkq`;tSOq5=vs){aK90z=Im>)7LE~ERs!7RrI%!2)Ny$V_R=CrGgx$D4UX(4*924H^3 zjA{a=tEtY-Xy8L;TnpXb63p^igBjOew>#;2XWi}wc81=^gqdam{qzhGV168#uKxJJ z0;BbO19gtkITp-v;&povm>)8egLO{O{ht{#%@z;QGYkc@qLF&WzvCRx$3bV@L_MD) z)0Lw8|Bfk5!Vk{N^bSbmMPjiY;K+0>(fy9hiqmwzBj<#F1N}5$Hc!EELU-9AYXA2Q213ugRz-S5EMLVqj#ho2L75e2Y}OL{?M zUZ6@qq{|D;e5LS%4J}V23$q~=;b+4t%c!4;s(MB;Gt|&C)B>}hy1Gtgvc7IRG8@3Z zg31q>c0=8EWcnMy&w85j2xp=Nn0Z_2+*;?hI=2TiUnejN;NJ$uAE~R(-NDquz|?!` z{%|m-roV1Sg1G<^!0fnfC=v`9rt@$x3mOH+ABq1Ulo`kA_5?8XNxD5nx2JkZvCVGya6` zKdtjQT|X~u%>N}MI0BdT4A=AwU+a8B_utm-ySjZJ%*)D8U{>_Au0PZD7rOnc&ac4C z_ovS9!P&U}|3ZR2cEY$KDP`B$8O)*1rQ3OQ&Z~2NoeSz*7|f0o0ppM4h99hue+7%> zmeB3e6!9U8{paY&1;GzO9&oUp@qdwRqNP^7B1bNQ26WK<|9dR@|G!far?e|Nz@hIB z=G64ixu?!y;=&3c3=V$C%-Bok-nyU6g8JzGzPg`Ge}wMuulpUDi!>g7N(s7dXPZ4i z=3*HNW&?)l8OgMV>$W2^|7hJ$W<@Ew?Z}Lur2EOVCu_FNp7>j4o@si(bS*$4vw#`8 z?Z_O7RNYTzzFE3WrafEdIbaLd|6)A@nHiSoHktNP-6k`+T(`*_fz`VGckB-Rkgoq7 zo4EcCBLiE0RL|(hbe+HtHsqwvr}TV|O#QU(C(}No^I6^hcWmPg))hVDRXrn_1zgi@ zGVQOxZ0L2JZ|eSAV1CHV$5#xH@92Iq?Yp||z&45ZNe^{_%nE+cZ8Gg2b$$$HK~Hqu zk?GIW^F7u1Ihf_Y*7K8D-XFUC2hU#y{;502xnVnDVwo{JCs=3h=UlLvKez5DGc>R6 z&!_vz^cU1^GUE#AHko!|8xpL@70ko%DMwg$7|9d+&wX2E^HY*;_t zAFcBs-JhVbr<8<50c4y6=7-DzX3|!Cua)3G36bXN>fbS|U#RP3=3k`SWG0vC_TMq% zSL?bXvz#?f+9i?+@o{AKZ=LRUWCpH>pW~UX>;Dtxcy801biv|A8 zvn|n+aA<5}1i%Ii0NBGwfFCmT7=Zr&$s;Xf6Zx`n{r&T^Ev>-+)kj;rM2%x}@Ns1J zbOJz50r+ub&g~T8)tEh>3XrGielqhJ9&N!EkGOPJo*vIH7Cc*S}A?#QypB zN!Pzmy2Qcz?~|@xxHRzU@b8nZf1h;y`=slipOo=74xfL{@c*ux5&G|wEyLqB%mGij6b zG4DrQP)usuS|3l9c&emsXx1_*CFy3i}tSMmDExp zCj7k+#ro7$O$jlo`6cz5rap&Sy{=I$D5>M?irBPguIi&KYGFwYs3+9W=dSAfdd65+ zNqwlPAE7p?Z&ckRb#8rA3&&w~S8o?GwRCKHF-grtCP$Ztq}Fd>bQPD>#aUdQlKN{F zR|!dN-q2W~mzqDjsik8RN~%#=Jf+lQSv;jxw;aZ(GV0(g9&hy=JQ(szzqzXBM#ip} zlhk&NKDUGiRe9yX6GKBPC^a(;4j*NX(NR%(Vsune>iuj8@l_TX9e&D7qocCY?5QEd zUs-8%R8ig-9RW()XNHif%0{E3nqqoxn8oUnnvVWD&SDKol^Yvffs)!Siz`S{cfjR1 zK{eGmC5?k$OMQ~X6ReKII&zGvt#o~EaMV#Uj1H?>p}H|9L>&hYrtt84to-W64OdUC zQbUMwAF%Li7{vza15Nb&h;<)m6dS2?0=1O^@pTrliBjP)s~r26tJ*5axN4e7>i#C& zZ1eq0u4?h7Mzw{cMmH7eeW)E}W2LRsl`<<;Q%z9K*^FvywQV+`T4kuav#4!Vlao-N zQ;l#k#c%oc^~(h{Hj6q? z-B(tq_o-eiXN-+i-O39!H8<2T<&A2*YSQYp=7D-Wi#k|c=_g{J=P?ykvsX5SIBuXs zwV9@-xgfS@Wn;-h)Qy!zY-nEATh%z0!*J4TgY}WJxr{M*gxXf?@YZ}#i)ZQ5C^f2_ zC^{@ZRNo55qQ~I+pcU;>0CJs*#-eTN)QTebFx6*S)NyLfNWgC{M@rp^cF5 zQ{J6Lo{6)wt&me)A;(rRM$f`Is>#-(kPrDA`!z>3`HNmZ7iwFhs;HJKLQQi+Y~glB zb-tQEK&YYatQe}}n5a0Q+KG03q+GncQC@@-s=bi67K3~$OS6|q>TyjD^MLH#!C3V& z^;}g^wM%iRW1u=#y&OkH2T}H6%Dx?q@=6>P9fcg{3Hh5W@@gCvorLUJ0`kmG#^|*; zDm3{L}iMct^@D<#_PR}$*lQpVU#>Pt<%PqkH7W5ru=U(;2z zE438ltexJ58=7uHww7kixs1^nSY(>~oU*yQQQnC~)?GxWm4UoFi@Y0)tcQ?8y&*^R zFh=jim06QNQZC%nDDRWhK0QVB*0PXKWswg^YJo5zhn0goB+M9n2uneeUCKlD?q!sZ zNNPea5q+5QT36$k9#dbsiV=^i0QGv7;1jqR?k$2neITdwHkN$~cf*=|iE{ODqkIOp z!{H)&Y(>bAvdHIfKio&iew854?_-QUj~il5zE8PPU!!~x=VV_Io$3qubr$(Dj+}l% zw((sQT_?1#y}B0wgwB& zB0s^Y5hG;3K*%#=jL|>i)X?PnlK@LZbO9XSr_VQS}{aRCA~!hKks+^`L%}MYX6Eh6&ZLKGd1RjIp`Y z+nRcxYMtRmHIF)VxQI<{0QFfGHLqH8gix&wp)MO?jLok;(bVTuTa7fT1=U3(MQmCl zs2{SZh1F)Ggc{ly>XuQ)SXcFprhcT_bF@))Q#Xzlv0Ix!%`?WR7E`;95o%adsE4ws z#Z}8#p}I7K8avh)TSDEZsfVc+w;5G@{kDnNxaLqVW>N9QJ6WinEufA`Hpb#Bx29gA z>O0P;mQzQJ6R~4kLj5L-T0yNaUZ{Sppw1j`jIF5N*3|n{>r61JzUtHoA~rP?>a#3r zWwqu+p;}u*T{h7eTSa}Msn4mlN-?Tc)kP^HHmwcR4_VaeYO_f~4Q&f`%OqoLp!!Br zKT_>E*{If3H%=C@TiZd+GsUO|t6irEHLN|Y-5A?QJ*TOcsQS(@s!h}pGeqpz&QQO}qBc`2 z%oM6$7pOC58e?0iw>9-XRAv4X!{w`$Ny+(({_H|>PR6sEP?OTf?s1mrX>C&W+dWRg z)5fF}c=0JJAM&&{DbYqxJCkzU?s4K)+xQogi%BxgQ5 z>SR(%{rV}2p3Wv^|M*U1m^FQoSGCdI{Wz{E-7^D6s<*nV5EIb2DO1nSpQNj~xQqql{ zD3c<;`P2)R9Bop%**)5P3^XY_>>h1CVz55m+DmqJMpP`;huz~O{uXH*)`#7r%~?Fw z$2&dBN&L0NL0BJl4}vg5gRwsB9?U8{30NQR^(bu?6R|$ta~9M3V`0}xHC|1JnABRi zvPLl|aK>*p5oN6Kq+8>2^<)L%7uYb4~3d5rQXlR7P@cA<}gyd{e~#-zT@ zqh08uA@_7KM%zqkx{HtxQ_hpuD33F#-SP@KZXo1CS>y>OHD^8{d&WSH&1Z~GF{%4C z`4Z*%7#7Ey#>wh~RPC&Yh1zJAQJt#JnWdc-aZq1pQKzf*W()N`)wQ#Yu`|_|nwlC9 zwc{M4I!j$SN5onOK{d}cs&mw~bA|ex>h3J6qM8(;rVWM~p%^Ms<<8PgA!hLM=Yus4h{X=8M>{B&hQX8!KLBQXdu;6}t?9+^C3A zUT#w777_Ac%CED?E3p7wg&a2&a`i&Ss#mMWw5mOaLG@l>RM)D57l?*kqIw;wLt7mC84Q+||1-f2<;7700RH01e;+h;f250j(9}L~z ziygmM1b>98E?aC2-e*!XHF@h;$gP$b1EYT;>Z7(5vj{lp&Pkk!<66LIgpE0R@mW$}I6Cj_; zQujHuyr_`n zTAGM;`kZQP6QlZ-TC{7n}5wn_C_ zE#$3JAh}SS}&r^EC^cx&In!I%$ zP9bDz4EE;L-YW%+X4=-7Y3`~kNn_K#EPcePv( zPHPhw8}Oy6YeLw|+<#sTUiITUzw+Kw^GCFJxA;(o_ngbMN4g*BlUC!zxPm#xJgno{ zt$8Qsu>m>KZ%_ED1RhkmEI|7Ld{PemwsTDJ*L%-+{F1DMv|nViUWiMtyL9`efhVHt zPnzsKcKx7<5J00!Ybi^W4A@5_4+FuJz zU-0Ld`}=}d-+q_P<5)_Up9dd$ys2l4OADPZ)^qKCrN2Y_O#GX*`o}5d>yUw8U$}76 z{J!A9Ty^?>;S%`%a2NM0bEl-cct(Gf|KoLkf42#h{oE7gXMFQR!DV^A%ib;j;DL#U zzP!*+s(Mcz4XpX&jev8uV_Ww6G;%?E?DLyVt(PIrD9?rHc%4U1 z`)Aj?duL2q`->Y#x_|w;NVNlQ%k%Hb?e+crW0@D%AO6knZH^Z;C;i@cLaXQV2Mjn^ z;MTf?m<5}EofqHm%J}!s9SSx(cHH|$oX;1beVl!2&G(*>^NX&1`}{a)$dxxMR`d+G zRq=jOhB~ZSfq~ioa&O~yqxD&@udnhsa_PYk`0^6IC&;mPf0 z7YY9A=*zsXe=p+4SMS(6oOg*+{UMJVES3MW!o;|<*?SoO*+7&)gar={~ z0d5`I1)6-zJ2W}J<6P~D8T@Oj6*F5FlbQ}Vl26Y6W5MDcWwxCE`kvHp@whkDHuZe8 zcJ9>FS?+1RD|6oT@Z0dpGc;sYt~WQIw3y-O!(NoBts|FH7(oiQ)lJ)VazRLjsl?`EqPZ(moi*SCq|zqe%b z@qM>!T!j&}=9jtb`7qo4Vh@8ngQ^B!jOnnlIm|9<=Ww~MCDIRBNUWPyzf zhAnhxUt!1g*U$Gj;R|KAw)X+4vFZ1&H3vSL|w2N>V*Ybx>(@k3= zn)mtnY5&}7LuX5+T->Hj%1Hijb!y(1ZBMz@`?+VyK;L3s>)wnsS1V?Ib=~jQ!;22> zb9HRr^Mx}*_cwW)y_LWJC6Do)EtM)<9pybaY0TpxlYbcPdg6KBFB=YjTRqY}?2nPN zf@Yuosq^m{b;mv%_j7XfD^;F8n7?T>ex;B0wYpz+--(#L&f#Y#o?J2hZp4)S_Zt>o zI&a2|D#Lm;ELUo8`z8~*c})GT&c+!DZJKO}`T6peoRNEcFWqmwZ&aB=NA|yWyvVr8 ziqA*!yka?@S9~N4s->9`^&PKU{ ztX|V(zqOF7Ta0o=SsiZ?(f27o$|C#9YJm3LJarx9XE}}0m1VU+ei3b54|zy_qg+K+ zk8ARC%7@&Ia#dN)SxiKyZGaqG%qUk!zco2@Bjn;9MmbPcqdi3QN6Hto$Telvy||FK zrb8Z6+!!4!t6yqz*e1xno<_Njtd8^)(Jq@Y*Vo@07D9-ujQoiU;V{K-elm#lWTn&x zig8;|@HbfsZXl~ZB}BoVTOrRZVXU{2tlrV&OO)q(8RaIj`cV6BGjsMlnO?F-Nz`m$2#*7a@tPFkFv-eWwo2XkVAJt9ui=T?u>;LAmopfy{j7KuGj+AguHb( zA+mb(r0I$}XurwHTmp~(@~SKMn@R{7`(urCa`1c*^poevyglt*D#Q11oc==%=(Fiw}nc9D%=vaOm_;NXx&t47}jogby z)-|zDdHz_Twal~g7^w6f03ne*`m?f=Y_H>#H&n!3pt8NchM z1p6@U#V{2xF?PmY48&;Dr`GND`q5NZOjjk-Uaupj+}YTO;o9&uzKZ%kx3Y)UbLC+9 zG917EoaKK#|FaR`L~P2rqK9^GF+5!D(o3$*f4QU1?j;ws71viv1?XIi%;u^4d~_dk zSK<$RD(XHw4VO0Y2R@Z_AOCq&vhMTMeSGIc8{Oxp`}l7GWX-3R>n-Q8-O~&A*E8^K z3afQr72StjBW=)q0lJUxaN4f>s_H&IbS(|=Qw=^gijPIc(t%G+Js-ba?L#B*$0qUh zXZRytJ$F!VZEZaxe+yxM>qZ^jmmj_mF9Jw0C` zq-O){Bwwp0^Th{+VH`tG*dx9}jpgvgPzieP_#QR-T!A6Fud(hc3ST9FpC-D`4QW5& z!MoRVAK%eaS@$*5eZ^q=>;Ga-Y7Qs<@V}(s|Ew-PE%l6CCsy6Zcek-ITqj2{3LMH% z-B$wXW4e#;c4Iy--~<}YQEQ|7N+Nv>B1g^ER(Frx!()@5!pba+hMvcixaoBpoNxcHV_Z@QB z?Em)G2Wh@6k;BG+hs%5wfuR6Dec^yCri1St;RyB9eZEMuBc*@{-Nz|Xq`sPyudZV- z=ZG&gEe#CNef~&~Lr>TvzS@rYI7Q=iUzF|(fX^8|exh|>RivM(bNb2!ZT1&Q#^_RY zNHYNTAy)U{3Mr~*d*gIpAkqU+XEksp)DM|O?<;CKSmMnOP?!%k9#ak;`$WYz4fg_L4#DVJAIJuNMub({8 z#(!M19Vm^?mjS#vFaXajUw|)Nc?tXq@Wm{AO%2a4zP@Gyuo2)JdWHZ)fnmT1U?eaK z7!8a8_>vf&apQnNz+eEc{IN*`k%$3efdN2QfCpcDzz_Hnjd%mR1KtB4fxiGAKqi0( z;xDjY055^xfV%+C#&3Z8z_-A6z-nL(uomE%xRI}SN=IT7uo>6_Yz4LfJRmcG9l%bY zKM(~B1Y&?#pcL9s8YlzoL_@6bg#dMddO&@k0niX=1T+Sk08N2rKyw>@v;bNHt$RNk^LdStI8elb>Ifj0q6*H0hR*GfHYtUa2hxT z90iU8%Yg(S5l8}t0P(;eU@*W_ju#Z3PB@Hw_}{sR(}t%DPZJ&kJQBDoxof#gd2t9) z=SRv_Z7q>(1@PI`c!1XzzHR9U&>iJp1$qHffT_S@fbXLkjC=_|3NRP80?Y#z04l&Y zWu*eMfW|-*peevtWGn@i0ol11bHM2gSb&_sLUcUhL0V{!3z+0`LM#X2Xwb>Vs&xV1b(`?-p%G_MfAr)EU}KAkEG z@Z#wX6azefQK)D%Fa{V4WMGhX0h@s>z*b-zunypLau^WHMG=QYJP-v81R{V5z-i3b z8Q?5%4)_u{4_p8)0+)cxz!l&sa1Hnh_!_tl+yHI@Junf|foVVz%3TZ0hCeTmn{OLC z1IJ0U_!Mv&I0O8M%(sDlKp$Wg@~j4CAw2__3Ah8!fC+ep5%?b9TUL2H!P`~d5AzMm zyxQ(T>|S6Wupc-GOaWrJDb51tfgjPLpMb|eCU6qI2jFAiA}GWaC;GVNt7fISH@DL@d1W)Ls{fiodc<8fdJpxy`2+^ z&qSNJ^Re}T$)V*C<1c@Umm+sVnq_nNISpJ3HGt{>x9%R~&F#)@&+VTb zz!@RV3!WuBlXzb74C7h08<+&(k4-v?#&D%40cNBTCmLc;qw^9vE8diT2>d!0R)az8V0p z)wKcc9_}Jux9b3nfe@epP#>rZ)B_pA^(H_^paakzXbrUD{I^6R6lepq1KI)* zgs&U8E6@e#tb=wq&>QFlgaPdFN?-+$2J{7D03O-Hfc`)fFce4x1_S+o7|#DdB$y!r zU^)`uc4oi;-M0IgFBTXCIQn=E;lml4C4j=>A4$w#a0I(mh?Zb~RfL#E^y}%w|H!uL;p8pD9&>`S2e3GQysz&U`0v5=#{89hy# z6_6Qc&qKQ^;165|92>!M*@*6=z#Aw9xB@&UX< z3IVJ@9UvG80s?^=Ky`J?P}$Yyhh!yy|1n8m(|A#>EjgGr;1CIYeUa# zvnRy*%~}B0bygp3$KmA>V(&Fi0K453Y38-t+{KQzRxZY0OK2O(p3chLXpTPihDVOQ zV*AM0^Khry)8vY_HUmh1)Z7 z(eY5R+vFCon*+^&p0Im@y8*2LUVeDV$@-}Pe|exQ@^k?@^UGK#Bsu~efc8K;pe@h_ zXbpq{4C(>C4ek!+7q|p~-;M0wmEw?&1!91KKr|2qL;?eV{y+rK59kZ@0m5zg(HrOm zgaIi4eWL+B-xvz;8y>I1!@(l}evKRh@R`SWAQ>13Fm?hk5!eM}07ntG9lQ;g4zL50 z!7YJlfNd&%OabNqvw>7#1~3zt1*`{F0}3z~SOzQwmH>-^U=+9zya1RF%mY+l5wHqa z0i*%TftA2oU=6Si*uejGY9kW%LboFAjsjWe7Nj=;EHoY146wki(7FJfft|2-03038 zyA^x`d<8fMoCSse>^-j!XTU7?Byb!!#`!;l#6I8)U@wq$(d>bb!{zAT5Bnf+0BDVZ z4})b`QOgNtakAa_n9|88(KCHCg0&jpv0Q39_ z97XUS;NO9#z%JM<803 z_;`HMZ$B^ii19D@KLQ^B+WeO7Xxr0vw&%&l|DDP)h-ou|?LKFu-|GeEK$=5IosTT} z2s0;e7(NS_F-K81A8&Fr`~W`QFU|3HU-!kg{gNn%e=mT4 zUx0cmpfI2-(xE_8pb5|zXaI1ZHv%^VS^)g*Hh-Dj5@-dq29_Z{2I!1*e{d%-zo;z+ zQn~*XU^dVZ!VvHrAQ9*Xj0M`jKOEcv+#YBLv;|lw3nVA$JVIysqX1TBk4uKldb+^J zZ~NWBK0HKvBEd(<;XrSIzjWr)J>m-+e&DkhZ@(BXm(9+!MN%VEWnx$~WS#!NRufL_h%q zbl>jWB_e8KjEn%kAU}WY%OaYV9J6ar`5*ebJQPYmRY>0P{j*XTN;xPs(nd8>HcVw{jH!vBPLL`=>iv4juvr%Ol0vE) z2xUB!GEiQOo%qYYjuJb0|~NvsPLpXybf zck8sVJQNE?zd9uR9mn?*Fsk@ld;XY)0R9S}h5Wub+x%@~{u(2j{PjY{K*UQ zt1o}roJy5IziR4>Q|Msd=Cm|_s8`ad&iXA z}<{XVGQvwlsz3GlgNuFaXkPDD2Jp zwliwBF61-*vl5<7wQfLJL-5zUwqQ-QbkfSQb2gbX zfLYy|j=qgW&Z5Zq_*!_%!1g6Z%O-sQr3Gen>E_SlByTsXS7;wMtNxqN=R1hx98AbL zzwx%}?b1F+zJkI{7t+zC*N8kK~ zWmi4L?C3cQG-;+RUFmY~bI$JB7a*fY)`|X{L7!mRUZmZ&^FMUZta$rGn3B1Xz*4>6OJg`z&M!wn0uddnkQv`BgO*2PB z7c~`S$kSZZ3v;kE@$atYf_}-XwpoOtxwLqqr}m7Ea;p73-VGQQP(!TgT=~>{+pu=; zN#P%wvrlP1C0XUMl)!yWf82GW;#YLhh zTv@6~)i4l+p*;9_aclW%d3eyWr`X~F1=V|u{pZrZMJyZm^@@a zHEcUtwjD(?$DGRDW|rMur>1Q3!I{a=fx>F)e2n9n!s_Pj7{K#I)O=H6_i$AouZ5lF zs(P$L2^(>~bFNZX?&$bo%w@czQM3<7FCyEl!$6+F9_7aFGq_Bhy1DWOi5$GTNJZS# zG?XEyxv5_ur+m~+eM&oDG4;Fc*yzQIseT#omno*cpAB~RQ2pmXALOBKKMWr0q4t}D ze9w!kqcdQ?E3Q6&E(g@e?x`Kd?{7Aj8+EueMfCgswD#q3QC8po%yU5n712;pheZq% z6&SW*Q^Zu<5OKFu6mUR{O~9RW{FFkeK#3eRK~hsoGjkiw4NcrNTuU@}Kbj#@?&b1( z-)CVE8SD3Xefy(_=Q+zg=iGD8J?B2>aYXMh4FT;7@X(%dl$LjkL!VjxI(x%f&?2C6 zdtV2HYkEGb?Tg?MS(dV<(BrWqg>4k{!raz$Vk2xqt)=;1zW3VHwx_dwf$4)e)56xy zbY%r3wRNVVA8>VXrjVVuYMd!z6Rw?|X-ok)^m3ML)?wiGWf5(sap&W+4yz7vrb1w< zKL7>~>>KKrB~F>W^)xW#HIm^>`mexC8mp-VhCSp=6?{02bN(`+sf7kW36|vBWv#5d)Pq34Bl>ddMcXIN-w`6K4dY9aJAzaro2LA`&R^PY8G^lDWa@2X zD&Z0>6YSr>v5O0ZEXRb;N4~_THTA{Xzs_Bo=OXe$T85Ogc4;u-HKy?mlc9`anpX~T zFTMoZ*zPV^_VdYdSs&;u8;Qn#g-K9m^}TjvvrA~|2q`Au@P-GCO8wD@_HCaok*x(A z&1*+an=y^D3pm1WmIE1KNN*jIf2Y;-LBoN}{RP8pZbwN#R{sbLo~%WNUm5ha22a85W*=I-d-&JajYo9(sea9$Q!1X**Se@@-Nq0_Ww@Cz$9OFr7^(J zJ^==womne?Y*J=EzmpN85kfYnwP=#Qt>}wT#SO5?Teqk6oVx}XT=wDXM*i74Vc;2& z7Aw(Pb`&v2$i6}i3j{y)JW%i?XHbmwxeiMsC0#^skt2g(=}MZyDi^a8{^&p{Um~`^ zNfeZ49Vi#36Jk12LJ`DBKqyaSe^trlYjv zh~lQ^=0n-oP1Pox1+J-hf>Cw(i4 z3nR&5Ib0TmS~;LcMJh>wIQ1Bs8i zB?@G|Jm1?>VX^emUA_m(!CXm=ke($ifw7T<>F*P(5qcm+6hnWp`nXu={C|z8GOID(uOtLaQJu2QR@ki0bS`?q@oqeZRAGun>8^ z(mlMWrBsZ1YEaiJ#z4`Wdk4HGb*ANeh4xjW7lV>VcC*;s`|svXKp_Wniy02eib|MK zdlncr!1#Uq^f{J++GW7-PuVNS$CBa|u>(!C4$H_j_JT z=+SFqQ2Sx(gY> z!>!ZF5N?~W{9n_6xAt3jU~;SnQP5ZZO!1;+SG&=mB8iyz10-? z-|tLe{{@E|-6{M4uClamu#w_1irl!Ca>{0*l!D4nXctPm`870@91h|-FqHfc3SsIN zJ*5i8`}3G-dkczi3e^Bl_*!%6Nkza^2gvsZyON8gr7w8yA}(6-G9y9iZjw>YwY+PJQSEuN7o@)uz4mS|2JpW7J6dtPc)?F~%Q< zx5RXJ-+xH)MQ^j-eZ}Kt{k84We|Q+Z%GL`M&}L*iS&Kp4<(Z@ywFiqs)3E%aO4kP!QkmHju$_A!(9Wars=s*19<5nmC zGvRX))1RmB=Kw)&uOD{zvn)1f4X>%iHR)~kEQ~JlQJkTG6EG?whGpMwoG?flg-3*U z`;pBNj1tq2j$OtzuOC_eitGM<6v?08=|`0paBUn;(gJ-EY@d4-2heh)kcg+ zK-?jB>l>E0|LYS|&Gz&9QwC_&-va~byuSFOl({9Bukcd?S@-k(X#?lZBE+sq{j<_3 z?e{lXx>>>SH**H01893(OY)lcYXe>Z%e4I>=mn=u1qO;f{miWTS3feJ{F@OY9}sTy z)k6<&_V<0gPKupS{~Mw)6W&jK%{<4-9Sa0I`=-A6>WMVjZP> zC>UC^k$|8k(=S}pvS_=La|Z?DZ8mEFo#3=z0fWzIn<>}2Hr_)qMvM|bkYDH@4;W=P z<3c@t$}MW;WA=0asgFal4g)10oO8kNNV~>o0~HK!v+#iw0SxUVU~sF>@^cG`^xB|R zFapd9W!mpvq&FMdIKF`b@iyB#kP12NLtyYYyZ5?7(fYpA&l)kB4-#J{>BshHIijRY z9j8Ei%=!$XaxPgKFu0F@dkM|mqs?v_F$xBe(+P0j3k>cN)8=-1FLrW0UW3cp-2=oG zkp52tDps6#o??X94W=Yc8#q|<(e?M=TW~RWb+s9Z0>lfn=hocXWjLNItwQW-K4uFB z(@xN;cLIa2TmJ(+2lo2rXq3X;+wAgSs^r`)hDfgv?(I5uJ#kz#FRx{9@&p72HG1Eu zoY~Rmmf9I1{fAKSPoPZ$2A6&J)Y#3NzUlUh5o0MJ+!JV&$NyM!uR_e%bxw{(F0Er;>%BNDu&X? zQp9b(XMji5p0IWpJv#}Wu{dt856aC?9m;a`h1+aYCx=O+)*h9;5Jh-YzACh!4kvNk zIBYnDo4Tu$lnDi{c^X0e63jy(zUct2;NlU>?-3=Jr!Y2-T1s}Zdt}<($cm+C=N`awG z2F5$U=ox&tL^!;WpMl6HrjtfVQ8B>wScLz`5BXUZ-;XtD{uo6LXP~0#@s;eLHdVPN zn0j8_BpmO5y&o4t8-4-3;(3ZZ|CV2hjxO@-OrigeIoa@Ct|q-SW$IJ0(!#Q~yfVn4 zV#g?$3QHb#^i>FyZBk)~=T|sG!|3W=v?z&5m8AU zs==MO3f1n##7jvvsy(k2PcealC#~dW=H4hpXeZ~aNTeh&@5p8}WMznQtO7|wW-$sv5E#GIjDwsA-U@d&Q|A(lOgP2}XsE#S)$pnIogwYCYsVNS&OdaPzkU?sv6qGu@O1{aQs2T=u`A{?g5N7YQ>hesQQM0lHtr1O`U3t;7FTqye|;hU1gq>wqN7tEHDi^{(zFY}+BJQO;jlH>_O;cVJ} zM+l?BIYPK?&GRW6)#r2aGX*E%t2yMHDJ;?6oFf%x{*}}FTiaxwG*|h!^OxiJ(jZeh zz}oV%{7()U3l}jTnOg;pNBNoaajoZ5rP(LMCO> zI?$@~fPpuh`b{q8qcpc?zh%VOlu1`O_n&~llisY}LjGKVG1&HR|C8G^>8VN?ollc@j z7us59OXKb{tLWMGHzCA`;har+U}$}0j0+#dhI;0g?KNWb0)%s4{`HLU74=+B86hLG z=>(^p01W=J=H!<)_WRrY*58PcC39`xq@A~F?)5K?kfqtAo(IV`${5kjo>?|>w0&g6 zIFL;dz|j68kDq0}Zrtd`fm4hazss}>g38;BpT8u_2zi-Jg`Bo=j%@EoddG~FLlzh@ zTma!()oUEwVVf%Nm=WTiL*<fYMibE$K<* zoX6AZMN(RCRdBWJ<_-m0wtH;WUPqH9k^L6`FFE za%835V`w3j-h~{|px~v_?t$??3NKFQ%A@$gV zGjBO1R6xkZ%Oy1om6mV4#WLrJ1Bz-mte_%b2&-1m59rmXA+4Rd7E$F2OzIzcMbzv{ z!H$X-LXhY}y2#sAAl;*d5J+wXQJZe~=o3Jl;EC#C@Q;Sjbl=SItI3ylL_$ zUu|#IX_XWir5++_Um3lccHhG|mFNY65W;DJNtAWYy$=l_6ytVOJm1An0rU8o2C5cJyAuccF(NZ68EErfbgYz zpO_&AD)CK~%<$5|IKYxLH^;pcUB{&QjQ~vU= z+^9z|1fN@+1OCJNF>S3)YF?f=S1$ZcnxM}0>2!PBW4pjpY5_J|^Bt)lKnnPT_5sxX zUdG&TbX{5d%jklZRy#)D@g05qKv*J_Z6K$ILYS%xB|gLfj^#$mdWaS@q!g785pd^i zl(xW57q)qyc<_BOfByyxAQe~+2)F&}_gsgU#O_}w&aepDu#tlCMvlMW8~zA0ZNk05 zM0$7UrS7^}Iv?xxBoHM!CUNP+OU~$E1rivc`UTIDux_(z-+yRIzE9F z7HlESQ!J^kp9uCs!4^971jrFv>Dd#^Y~)tS0KFsQZhy4G{0Iide#4iEL}sTojbR$1 z47uPbT8B*a5wikm{)2Q( zo01c*tqgsfXM}8mGc^H?(qwL<3Ir~9EfeF+goy)oNaDAd@XhY*(!3W&cK?S1VVv5y z&6IRCEq851|3qU#*V~^0;pV+8^J7X=G2e^A~qWaH;7X4pW194wG8{M!=isEe!)^E8!Xa;{5#(Vs+UTST( z7il(;O2up`^SRKx-U-li*P_w~Lh}G9P*WH&G>O~Pl--i@r!)M!-}%fl6+`jO2m9%q z-E{FeJbKA)dh{GoPZ~hwO%?luR=mf=Zx6Y@#E!oIh2W!oyhl>^(d?~bJUb`;A@UKA z*sY7DEUMA9WR!=&v^Y#Wdmc*VbN2xTR_ZV1^9wHQL7d4orB6Sb&iN&d6lUtg@E znPA#lXT%d(aV_fsG+tij07hGBEW?f!b z5HfhhE~Xdz>nd0(CKJVJG1Ih{n3mD>c&~p=qR9CA*Ce7K17XeAg{+eCwb1v^>sHpw zwDkYC7B-<5+96(<@c+fk|BIlnPk^N|1Cn9J_uVhO8CLH)*W|>2Z=M);hH1%3I`{Eu zW-afw3mAFK9&eSCfy-mc>k&`cf@;#(s476i-@vApP||$igrF~>;B17H1tl~*Tkx`d z{j-u~B~+M=e}l}|&)u6Rw8u*50eGm9)`}VUG8cWv z4zqR9JkH`!LuTnqI$>sA>QdOb-ddN%GV6v944R7krT~1LQDDQ3{trf zD|j5G2E6QPbBw$eLH3U!JLju>WM=H=?{J*-i-6hbI4uFDfOk0f{KwYG3$G?`o{$a& zj)d1+db`bkdO%(TS}2UK>PT3Ot}3JmVj?}=F6giL%)znvBniiN9L47+X++`Y33}@4 z#ox*Eq8qd!KataXd<0|~qqv2{_4b-i8=X5>2aRq!!OaHpo2HS9FuPae4=t7Y*37q_ zYBkazzAK5f-o9ruF5Z)qpMGNR_T=fO_3Rf!Q*J-E78jZDY=*6tjy`#9`>F zEx7@87H*Yw!!(%qssaTWouxXf1+Vdh%(n@KT-pB8+=;zixu3|j^@X!iO<%rZrKX_Y zvlW2IM>mP_>FHP=W>y{pG^qy~X2=jIy5XE;tj9m?Yq0Urj*do}AI_1(9Kq}DR9#dI zC(ga~%cT~%`}1s6@BA!9#oOY)gU0_U0b0-4yHvrL*UZN#+xhbT*x&bY0MhB#d`@)dvD~GLhYS9w+e7srBhgiR~^=WmfEgo|%c zb(s|S^DioV&4~Z@ou?nnnFsZ+%k1fKIR1CJ4ofloX3p??Z37}$Ps`Y-v~;()gyd+$ zq6oHH)i63LT9@FFIx#6dK2b-(1K9@4x`e41NYIxLiEST>yX z!M|R=i#4MQ8um8bD`9mE_ugaKDtczcS{m|P7=9gdk2SNPk47*Lb%?}0CV8yREjBeO zQ8!+P(I+RTexOUG)+3SC=2^2qdNzW&(4Us&Xm&)KN7LNuQdcOY=wqaiT6Y zPA3aNa{}2{G+V<$$VtoUl7}_ZQKm0zMY-X)Z5+nN){ja`iwFAD1YH_A^<%b%Pr{g$ zio%0fC>`s^EU6%fb)^R3ES@6ju}DrCN5}m^-g^}L*f7(X?G+5SN3lTjnK4lb32xEx zNn^+AV&ju^ZYinBDZ143cwL$<%`GY#hJokA>C)ZelhRY;F>cJbDA;RiVsvss8lLG= zlcEyb(h`zk!8qNN6n8L4Pv)w*$0kI@rKN#1c4AUYdVF${TM|r^77t^ANm7z7VO%=h z=*C*moF2@@665JoQCmmTpK1VU8+F4`v$5>B=G~ zCzP!x{d`u397CBmH7|yh0H{mdd$M*`$jE&W$!Z)~uqM{RY_%lft7nL4AN#{0A zmzqquy;w8ZdE%4e=w39lYbcHB9+j3BAD2XRW7r|;-IRvrqut77HofYV4 zFXm5sV&RKcajYkG>&1MyigoB{Pu7Y0$FVLntrwVQ$FUyNvo{MhoQ`9SRSluX*r;@! zn;Mw5i){U zYx*P@Al$;R{I=i&_TMHc@iEDXQRy+`c)@^eJ0pYa69By7$^s4FCa?h(bis>_HoTm` zH1(`Fc1&^tpK_0BSZ*#&f!ZR1sSSgtF>`??^kqec;TepADAkgM(@{(Ij$yYoyKg}| zY*~LpXj7J1ugb5RQrm4{qH$%p^c0?8aA?mOspwEU=3{W{z?KNqZ!c?L7`lxeWkl}G znNGN~WQyGhq9_j*VOYA8MX0FJIA&o8^l0#frGehUYY*;-Mu?tVKv8n)SoxmKZ z_zY`o<>BTLl@jlU3Y=QI!+=hwA#mAA1fR*LFz&1a&{TZ3>m;+Llc!kIx_n1TkDJCH z+mXd-Oui9oDknGIZa>ZJG3w9!B93ZLP0lb~GhWnl9>JY?^OMG@6JCJ0YiQ3I7-r=e z7NYVcCvIbLWc7JwLw{S~f7-L`9hDzNo@H;hLTZHXynUQ9GMxCI5K8&ehpT5#1!viK z!B=CjJjZZaSc73#urcbJeV%FSDKgQ_GT7nnd6xVNQFa+?|LTMD2$h2^SYtB3z?@zO zjlRG-yz*3Yku{)07uZ{{$i)k+y{tl&+%;7YXULjB<_U`Ei>yukDiz%|hD{Fa7^7Qn zW0o+*fd;#G*!%d6%XzrZZ^yjo8C;hRy0PtsD?Y3hBTIkgPRH$;Q-g%${PmI59hpC7gPC5AV^!)W4_cT0e>}iU_s>PkIg61pJhqm(Io)UXjcH+NH2m|oHv55 z7~B!ZqtcUkxQj}So0zCW6q3VdeA?u32>NbVs0j$C@yWDz9@abpQyoLVyG*N+;;MBo vjGEJ%o#P9*m9%xa_hp?8Pr_Lz6=l|CLuh?Hwx0sG103eS90@-gwCMi<%MDP6 delta 61708 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 u=8.0.0" } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.48.0.tgz", - "integrity": "sha512-1/aMiU4Eqo3Zzpfwu51uXssp5pzvHFObk8S9pKAiXb1ne8pvg1qxBQitYL1XUiAMEXFzgjaidYG2V6624DRhhw==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.50.0.tgz", + "integrity": "sha512-JdZuKrhOYggqOpUljAq4WWNi5nB10PmgoF0y2CvedLGXd0kSawb/UBnWT8gg1ND3bHCNHStAIVT0ELlxJJRqrA==", "dependencies": { "@opentelemetry/api": "^1.0.0" }, @@ -2122,53 +2122,53 @@ } }, "node_modules/@opentelemetry/context-zone": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-zone/-/context-zone-1.21.0.tgz", - "integrity": "sha512-YJQH3LroaZZBN0baGLkvw1WlNNpdNxXf7wfdJrst5v+lYGOus5HX9GUAOB9dByj3Z6yGlPIboPPojnc+ybxKGA==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-zone/-/context-zone-1.23.0.tgz", + "integrity": "sha512-7piNTrpH+gZNMDDOHIJXCSwp0Xslh3R96HWH5HwXw+4PykR4+jVoXvd6jziQxudX9rFAfu2B64A10DHs4ZWrfA==", "dependencies": { - "@opentelemetry/context-zone-peer-dep": "1.21.0", - "zone.js": "^0.11.0" + "@opentelemetry/context-zone-peer-dep": "1.23.0", + "zone.js": "^0.11.0 || ^0.13.0 || ^0.14.0" }, "engines": { "node": ">=14" } }, "node_modules/@opentelemetry/context-zone-peer-dep": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-zone-peer-dep/-/context-zone-peer-dep-1.21.0.tgz", - "integrity": "sha512-VShgSOPlc2UWaNdJST7syUDLdFKstkiqVDBaFEwSwvXP9IIaE7XxS5uAVkd55EVOzfB7PhdEQ91roAt5pHyzhQ==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-zone-peer-dep/-/context-zone-peer-dep-1.23.0.tgz", + "integrity": "sha512-3ia5w2y3CGHIhMSggttliGbeRBWclIyMMXdfRCcit1NHg1ocieA9qYxyUEetbOvPrQpoti3O3k+5eyQUv7r8nw==", "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.8.0", - "zone.js": "^0.10.2 || ^0.11.0 || ^0.13.0" + "@opentelemetry/api": ">=1.0.0 <1.9.0", + "zone.js": "^0.10.2 || ^0.11.0 || ^0.13.0 || ^0.14.0" } }, "node_modules/@opentelemetry/core": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.21.0.tgz", - "integrity": "sha512-KP+OIweb3wYoP7qTYL/j5IpOlu52uxBv5M4+QhSmmUfLyTgu1OIS71msK3chFo1D6Y61BIH3wMiMYRCxJCQctA==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.23.0.tgz", + "integrity": "sha512-hdQ/a9TMzMQF/BO8Cz1juA43/L5YGtCSiKoOHmrTEf7VMDAZgy8ucpWx3eQTnQ3gBloRcWtzvcrMZABC3PTSKQ==", "dependencies": { - "@opentelemetry/semantic-conventions": "1.21.0" + "@opentelemetry/semantic-conventions": "1.23.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.8.0" + "@opentelemetry/api": ">=1.0.0 <1.9.0" } }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.48.0.tgz", - "integrity": "sha512-Glxl0ZmyHqykGjcv5T4HMvw4fQVZoiCV0oMolPgXBBnuTuOHS/dEdoGX6hNp4Xqw55YALM/CJocHwkRqBQnPgw==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.50.0.tgz", + "integrity": "sha512-6s+nzBkIuFJlDoFM5FaLAa5lSmLr7GYMpU6QACIOGYqGsYD90YkP8VHFF0HrXSE3LRsgpvl6BTQbBfTVLtjizQ==", "dependencies": { - "@opentelemetry/api-logs": "0.48.0", - "@opentelemetry/core": "1.21.0", - "@opentelemetry/otlp-exporter-base": "0.48.0", - "@opentelemetry/otlp-transformer": "0.48.0", - "@opentelemetry/sdk-logs": "0.48.0" + "@opentelemetry/api-logs": "0.50.0", + "@opentelemetry/core": "1.23.0", + "@opentelemetry/otlp-exporter-base": "0.50.0", + "@opentelemetry/otlp-transformer": "0.50.0", + "@opentelemetry/sdk-logs": "0.50.0" }, "engines": { "node": ">=14" @@ -2178,15 +2178,15 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.48.0.tgz", - "integrity": "sha512-lZ0gah/WjPpUBVR2Qml8GHraLznsXEpmIS897vdL2IXCxJzGev7sCb9IwAiq89+MgHkuGUWhTWFB2frKrqX1sA==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.50.0.tgz", + "integrity": "sha512-DMilj0pTOGxeaRPvVBil/KugvLMV5l+GzoXEWBKXYGEnfNlX+huPeMpYl+zJJBtI3Coht2KArnNOLhs2wqA3yA==", "dependencies": { - "@opentelemetry/core": "1.21.0", - "@opentelemetry/otlp-exporter-base": "0.48.0", - "@opentelemetry/otlp-transformer": "0.48.0", - "@opentelemetry/resources": "1.21.0", - "@opentelemetry/sdk-metrics": "1.21.0" + "@opentelemetry/core": "1.23.0", + "@opentelemetry/otlp-exporter-base": "0.50.0", + "@opentelemetry/otlp-transformer": "0.50.0", + "@opentelemetry/resources": "1.23.0", + "@opentelemetry/sdk-metrics": "1.23.0" }, "engines": { "node": ">=14" @@ -2196,15 +2196,15 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.48.0.tgz", - "integrity": "sha512-QEZKbfWqXrbKVpr2PHd4KyKI0XVOhUYC+p2RPV8s+2K5QzZBE3+F9WlxxrXDfkrvGmpQAZytBoHQQYA3AGOtpw==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.50.0.tgz", + "integrity": "sha512-L7OtIMT7MsFqkmhbQlPBGRXt7152VN5esHpQEJYIBFedOEo3Da+yHpu5ojMZtPzpIvSpB5Xr5lnJUjJCbkttCA==", "dependencies": { - "@opentelemetry/core": "1.21.0", - "@opentelemetry/otlp-exporter-base": "0.48.0", - "@opentelemetry/otlp-transformer": "0.48.0", - "@opentelemetry/resources": "1.21.0", - "@opentelemetry/sdk-trace-base": "1.21.0" + "@opentelemetry/core": "1.23.0", + "@opentelemetry/otlp-exporter-base": "0.50.0", + "@opentelemetry/otlp-transformer": "0.50.0", + "@opentelemetry/resources": "1.23.0", + "@opentelemetry/sdk-trace-base": "1.23.0" }, "engines": { "node": ">=14" @@ -2214,10 +2214,11 @@ } }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.48.0.tgz", - "integrity": "sha512-sjtZQB5PStIdCw5ovVTDGwnmQC+GGYArJNgIcydrDSqUTdYBnMrN9P4pwQZgS3vTGIp+TU1L8vMXGe51NVmIKQ==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.50.0.tgz", + "integrity": "sha512-bhGhbJiZKpuu7wTaSak4hyZcFPlnDeuSF/2vglze8B4w2LubcSbbOnkVTzTs5SXtzh4Xz8eRjaNnAm+u2GYufQ==", "dependencies": { + "@opentelemetry/api-logs": "0.50.0", "@types/shimmer": "^1.0.2", "import-in-the-middle": "1.7.1", "require-in-the-middle": "^7.1.1", @@ -2232,15 +2233,15 @@ } }, "node_modules/@opentelemetry/instrumentation-document-load": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.35.0.tgz", - "integrity": "sha512-U3zQBjbAF0rm7GT7YJ8DPqgiCdBoshmld4c1pZe3tAGAMa5QPIjonIfSMSvJ2XMh6Nvi+8Rfe3XFCe0cuWIjsQ==", + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.37.0.tgz", + "integrity": "sha512-tmxx8k2gjUwbAEhnvxACEeYTHRwkrcvU3ABkmoH5NKtd5aWSiY6Pni2hCtccLqj0Hk4/jwv51+lLiA6moji6ZQ==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.48.0", + "@opentelemetry/instrumentation": "^0.50.0", "@opentelemetry/sdk-trace-base": "^1.0.0", "@opentelemetry/sdk-trace-web": "^1.15.0", - "@opentelemetry/semantic-conventions": "^1.0.0" + "@opentelemetry/semantic-conventions": "^1.22.0" }, "engines": { "node": ">=14" @@ -2250,14 +2251,14 @@ } }, "node_modules/@opentelemetry/instrumentation-fetch": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fetch/-/instrumentation-fetch-0.48.0.tgz", - "integrity": "sha512-y4Zw9VeUUMaowg3aXYZXcaUJQ7IKfpR6sjClrAQOJwWG8LYFpM6NIRSoAeJv/ShfxWWCPWC0P4zgXcKRqpURFQ==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fetch/-/instrumentation-fetch-0.50.0.tgz", + "integrity": "sha512-CayteluGJbrfDvzEFQ0EWqLFkNAcO9H7nfDHptZjtonBpJRWF170XZoMkJVC2bxp0lIVwyuw6WlnGVRSNwEtKA==", "dependencies": { - "@opentelemetry/core": "1.21.0", - "@opentelemetry/instrumentation": "0.48.0", - "@opentelemetry/sdk-trace-web": "1.21.0", - "@opentelemetry/semantic-conventions": "1.21.0" + "@opentelemetry/core": "1.23.0", + "@opentelemetry/instrumentation": "0.50.0", + "@opentelemetry/sdk-trace-web": "1.23.0", + "@opentelemetry/semantic-conventions": "1.23.0" }, "engines": { "node": ">=14" @@ -2267,12 +2268,12 @@ } }, "node_modules/@opentelemetry/instrumentation-user-interaction": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-user-interaction/-/instrumentation-user-interaction-0.35.0.tgz", - "integrity": "sha512-d66rqb24onIEnFNxXorCEzj+5tYBJKM/6StRl+SKXfRDXRT+nBj5EGdBUNgk+jiGQ0M/RymZHHHXSguTV2F1fA==", + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-user-interaction/-/instrumentation-user-interaction-0.37.0.tgz", + "integrity": "sha512-MVJXRmg7WKtGLlq5PwsP2NKLZZ6TbhKsR0CMcEbeAoWC9QPigKDR7nyVw3bWhfTnD2GAUpAWO9PA8Q/19hYuUw==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.48.0", + "@opentelemetry/instrumentation": "^0.50.0", "@opentelemetry/sdk-trace-web": "^1.8.0" }, "engines": { @@ -2280,7 +2281,7 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.3.0", - "zone.js": "0.11.4" + "zone.js": "^0.11.4 || ^0.13.0 || ^0.14.0" } }, "node_modules/@opentelemetry/instrumentation/node_modules/lru-cache": { @@ -2314,11 +2315,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.48.0.tgz", - "integrity": "sha512-T4LJND+Ugl87GUONoyoQzuV9qCn4BFIPOnCH1biYqdGhc2JahjuLqVD9aefwLzGBW638iLAo88Lh68h2F1FLiA==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.50.0.tgz", + "integrity": "sha512-JUmjmrCmE1/fc4LjCQMqLfudgSl5OpUkzx7iA94b4jgeODM7zWxUoVXL7/CT7fWf47Cn+pmKjMvTCSESqZZ3mA==", "dependencies": { - "@opentelemetry/core": "1.21.0" + "@opentelemetry/core": "1.23.0" }, "engines": { "node": ">=14" @@ -2328,107 +2329,107 @@ } }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.48.0.tgz", - "integrity": "sha512-yuoS4cUumaTK/hhxW3JUy3wl2U4keMo01cFDrUOmjloAdSSXvv1zyQ920IIH4lymp5Xd21Dj2/jq2LOro56TJg==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.50.0.tgz", + "integrity": "sha512-s0sl1Yfqd5q1Kjrf6DqXPWzErL+XHhrXOfejh4Vc/SMTNqC902xDsC8JQxbjuramWt/+hibfguIvi7Ns8VLolA==", "dependencies": { - "@opentelemetry/api-logs": "0.48.0", - "@opentelemetry/core": "1.21.0", - "@opentelemetry/resources": "1.21.0", - "@opentelemetry/sdk-logs": "0.48.0", - "@opentelemetry/sdk-metrics": "1.21.0", - "@opentelemetry/sdk-trace-base": "1.21.0" + "@opentelemetry/api-logs": "0.50.0", + "@opentelemetry/core": "1.23.0", + "@opentelemetry/resources": "1.23.0", + "@opentelemetry/sdk-logs": "0.50.0", + "@opentelemetry/sdk-metrics": "1.23.0", + "@opentelemetry/sdk-trace-base": "1.23.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.8.0" + "@opentelemetry/api": ">=1.3.0 <1.9.0" } }, "node_modules/@opentelemetry/resources": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.21.0.tgz", - "integrity": "sha512-1Z86FUxPKL6zWVy2LdhueEGl9AHDJcx+bvHStxomruz6Whd02mE3lNUMjVJ+FGRoktx/xYQcxccYb03DiUP6Yw==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.23.0.tgz", + "integrity": "sha512-iPRLfVfcEQynYGo7e4Di+ti+YQTAY0h5mQEUJcHlU9JOqpb4x965O6PZ+wMcwYVY63G96KtdS86YCM1BF1vQZg==", "dependencies": { - "@opentelemetry/core": "1.21.0", - "@opentelemetry/semantic-conventions": "1.21.0" + "@opentelemetry/core": "1.23.0", + "@opentelemetry/semantic-conventions": "1.23.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.8.0" + "@opentelemetry/api": ">=1.0.0 <1.9.0" } }, "node_modules/@opentelemetry/sdk-logs": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.48.0.tgz", - "integrity": "sha512-lRcA5/qkSJuSh4ItWCddhdn/nNbVvnzM+cm9Fg1xpZUeTeozjJDBcHnmeKoOaWRnrGYBdz6UTY6bynZR9aBeAA==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.50.0.tgz", + "integrity": "sha512-PeUEupBB29p9nlPNqXoa1PUWNLsZnxG0DCDj3sHqzae+8y76B/A5hvZjg03ulWdnvBLYpnJslqzylG9E0IL87g==", "dependencies": { - "@opentelemetry/core": "1.21.0", - "@opentelemetry/resources": "1.21.0" + "@opentelemetry/core": "1.23.0", + "@opentelemetry/resources": "1.23.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.8.0", + "@opentelemetry/api": ">=1.4.0 <1.9.0", "@opentelemetry/api-logs": ">=0.39.1" } }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.21.0.tgz", - "integrity": "sha512-on1jTzIHc5DyWhRP+xpf+zrgrREXcHBH4EDAfaB5mIG7TWpKxNXooQ1JCylaPsswZUv4wGnVTinr4HrBdGARAQ==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.23.0.tgz", + "integrity": "sha512-4OkvW6+wST4h6LFG23rXSTf6nmTf201h9dzq7bE0z5R9ESEVLERZz6WXwE7PSgg1gdjlaznm1jLJf8GttypFDg==", "dependencies": { - "@opentelemetry/core": "1.21.0", - "@opentelemetry/resources": "1.21.0", + "@opentelemetry/core": "1.23.0", + "@opentelemetry/resources": "1.23.0", "lodash.merge": "^4.6.2" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.8.0" + "@opentelemetry/api": ">=1.3.0 <1.9.0" } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.21.0.tgz", - "integrity": "sha512-yrElGX5Fv0umzp8Nxpta/XqU71+jCAyaLk34GmBzNcrW43nqbrqvdPs4gj4MVy/HcTjr6hifCDCYA3rMkajxxA==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.23.0.tgz", + "integrity": "sha512-PzBmZM8hBomUqvCddF/5Olyyviayka44O5nDWq673np3ctnvwMOvNrsUORZjKja1zJbwEuD9niAGbnVrz3jwRQ==", "dependencies": { - "@opentelemetry/core": "1.21.0", - "@opentelemetry/resources": "1.21.0", - "@opentelemetry/semantic-conventions": "1.21.0" + "@opentelemetry/core": "1.23.0", + "@opentelemetry/resources": "1.23.0", + "@opentelemetry/semantic-conventions": "1.23.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.8.0" + "@opentelemetry/api": ">=1.0.0 <1.9.0" } }, "node_modules/@opentelemetry/sdk-trace-web": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-1.21.0.tgz", - "integrity": "sha512-MxkmY/UNXkDiZj7JUu5T7wWt8Ai4NJEwSjGoQQ9YLvgLUIivvaIo9Mne+Q+KLOUG2v/uhivz3qzxbCODVa0c1A==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-1.23.0.tgz", + "integrity": "sha512-tx9N3hIkd6k567BeujBnpXYdhu3ptYVk0ZkhdcjyQ3I8ZDJ+/JkVtaVNLAuf8hp1buTqNDmxSipALMxEmK2fnw==", "dependencies": { - "@opentelemetry/core": "1.21.0", - "@opentelemetry/sdk-trace-base": "1.21.0", - "@opentelemetry/semantic-conventions": "1.21.0" + "@opentelemetry/core": "1.23.0", + "@opentelemetry/sdk-trace-base": "1.23.0", + "@opentelemetry/semantic-conventions": "1.23.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.8.0" + "@opentelemetry/api": ">=1.0.0 <1.9.0" } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.21.0.tgz", - "integrity": "sha512-lkC8kZYntxVKr7b8xmjCVUgE0a8xgDakPyDo9uSWavXPyYqLgYYGdEd2j8NxihRyb6UwpX3G/hFUF4/9q2V+/g==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.23.0.tgz", + "integrity": "sha512-MiqFvfOzfR31t8cc74CTP1OZfz7MbqpAnLCra8NqQoaHJX6ncIRTdYOQYBDQ2uFISDq0WY8Y9dDTWvsgzzBYRg==", "engines": { "node": ">=14" } @@ -5731,9 +5732,9 @@ } }, "node_modules/require-in-the-middle": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", - "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.3.0.tgz", + "integrity": "sha512-nQFEv9gRw6SJAwWD2LrL0NmQvAcO7FBwJbwmr2ttPAacfy0xuiOjE5zt+zM4xDyuyvUaxBi/9gb2SoCyNEVJcw==", "dependencies": { "debug": "^4.1.1", "module-details-from-path": "^1.0.3", diff --git a/js/package.json b/js/package.json index 829bff7a..8e6c07af 100644 --- a/js/package.json +++ b/js/package.json @@ -40,16 +40,16 @@ "vite-plugin-checker": "0.6.2", "vite-plugin-inspect": "0.8.1", "vite-plugin-pwa": "0.17.4", - "@opentelemetry/api": "^1.7.0", - "@opentelemetry/context-zone": "^1.21.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.48.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.48.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.48.0", - "@opentelemetry/instrumentation-document-load": "^0.35.0", - "@opentelemetry/instrumentation-fetch": "^0.48.0", - "@opentelemetry/instrumentation-user-interaction": "^0.35.0", - "@opentelemetry/sdk-logs": "^0.48.0", - "@opentelemetry/sdk-metrics": "^1.21.0", - "@opentelemetry/sdk-trace-web": "^1.21.0" + "@opentelemetry/api": "^1.8.0", + "@opentelemetry/context-zone": "^1.23.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.50.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.50.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.50.0", + "@opentelemetry/instrumentation-document-load": "^0.37.0", + "@opentelemetry/instrumentation-fetch": "^0.50.0", + "@opentelemetry/instrumentation-user-interaction": "^0.37.0", + "@opentelemetry/sdk-logs": "^0.50.0", + "@opentelemetry/sdk-metrics": "^1.23.0", + "@opentelemetry/sdk-trace-web": "^1.23.0" } } diff --git a/opencollector.yaml b/opencollector.yaml index 2024bff1..7ee2e638 100644 --- a/opencollector.yaml +++ b/opencollector.yaml @@ -32,7 +32,7 @@ exporters: stream-name: default # Writes all opentelemetry logs, traces, metrics to a file, useful for testing: file/debug_file_writing: - path: /home/runner/work/bitbazaar/bitbazaar/logs/otlp_telemetry_out.log + path: /Users/zak/z/code/bitbazaar/logs/otlp_telemetry_out.log rotation: max_megabytes: 10 max_days: 3 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ae8e8683..3b84995c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -156,6 +156,7 @@ dependencies = [ "deadpool-redis", "error-stack", "futures", + "futures-timer", "homedir", "hostname", "http 1.0.0", @@ -168,6 +169,7 @@ dependencies = [ "opentelemetry_sdk", "parking_lot", "portpicker", + "rand", "redis", "regex", "rstest", @@ -343,11 +345,10 @@ dependencies = [ [[package]] name = "deadpool" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +checksum = "144f5e4b9ce67c972acc225e71aefe6b21241276f94005024562874611064d30" dependencies = [ - "async-trait", "deadpool-runtime", "num_cpus", "tokio", @@ -355,9 +356,9 @@ dependencies = [ [[package]] name = "deadpool-redis" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f2381b0e993d06a1f6d49f486b33bc4004085bf980340fc05726bacc681fff" +checksum = "c2244d421c9514eab2e1ce1aa1e3c9d5c7cbb9cf3d9bbcac21a6b27e6a868d84" dependencies = [ "deadpool", "redis", @@ -1425,9 +1426,9 @@ dependencies = [ [[package]] name = "redis" -version = "0.24.0" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c580d9cbbe1d1b479e8d67cf9daf6a62c957e6846048408b80b43ac3f6af84cd" +checksum = "6472825949c09872e8f2c50bde59fcefc17748b6be5c90fd67cd8b4daca73bfd" dependencies = [ "async-trait", "bytes", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index dc76988e..be53db49 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -23,7 +23,15 @@ all-features = true log-filter = ["dep:regex"] timing = ['dep:comfy-table', 'dep:chrono'] cli = ['dep:normpath', 'dep:conch-parser', 'dep:homedir', 'dep:chrono', 'dep:strum'] -redis = ['dep:deadpool-redis', 'dep:redis', 'dep:sha1_smol', 'dep:serde_json'] +redis = [ + 'dep:deadpool-redis', + 'dep:redis', + 'dep:sha1_smol', + 'dep:serde_json', + 'dep:rand', + 'dep:futures', + 'dep:futures-timer', +] opentelemetry-grpc = [ 'dep:tracing-log', 'dep:opentelemetry-appender-tracing', @@ -65,6 +73,9 @@ colored = '2' chrono = { version = '0.4', optional = true } strum = { version = "0.25", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } +rand = { version = "0.8", optional = true } +futures = { version = "0.3", optional = true } +futures-timer = { version = "3", optional = true } # FEAT: log-filter: regex = { version = '1', optional = true } @@ -78,8 +89,8 @@ conch-parser = { version = "0.1.1", optional = true } homedir = { version = "0.2", optional = true } # FEAT: redis: -deadpool-redis = { version = "0.14", features = ["rt_tokio_1"], optional = true } -redis = { version = "0.24", default-features = false, features = ["aio", "json"], optional = true } +deadpool-redis = { version = "0.15", features = ["rt_tokio_1"], optional = true } +redis = { version = "0.25", default-features = false, features = ["aio", "json"], optional = true } sha1_smol = { version = "1.0", optional = true } # FEAT: opentelemetry-(grpc|http): diff --git a/rust/bitbazaar/log/global_log/setup.rs b/rust/bitbazaar/log/global_log/setup.rs index bab1b8d7..8964a22b 100644 --- a/rust/bitbazaar/log/global_log/setup.rs +++ b/rust/bitbazaar/log/global_log/setup.rs @@ -175,9 +175,21 @@ pub fn builder_into_global_log(builder: GlobalLogBuilder) -> Result RedisBatch<'a, 'b, 'c, ReturnType> { } async fn inner_fire(&mut self) -> Option { - if let Some(conn) = self.redis_conn.get_conn().await { + if let Some(conn) = self.redis_conn.get_inner_conn().await { match self.pipe.query_async(conn).await { Ok(result) => Some(result), Err(err) => { @@ -91,62 +91,119 @@ impl<'a, 'b, 'c, ReturnType> RedisBatch<'a, 'b, 'c, ReturnType> { } } -// The special singular variant that returns the command output directly. -impl<'a, 'b, 'c, R: FromRedisValue> RedisBatch<'a, 'b, 'c, (R,)> { +/// Trait implementing the fire() method on a batch, variable over the items in the batch. +pub trait RedisBatchFire { + /// The final return type of the batch. + type ReturnType; + /// Commit the batch and return the result. /// If redis unavailable, or the types didn't match causing decoding to fail, `None` will be returned and the error logged. - /// - /// Note this is the special singular variant that returns the command output directly (no tuple). - pub async fn fire(mut self) -> Option - where - (R,): FromRedisValue, - { + fn fire(self) -> impl std::future::Future>; +} + +// The special singular variant that returns the command output directly. +impl<'a, 'b, 'c, R: FromRedisValue> RedisBatchFire for RedisBatch<'a, 'b, 'c, (R,)> { + type ReturnType = R; + + async fn fire(mut self) -> Option { self.inner_fire().await.map(|(r,)| r) } } -macro_rules! impl_batch_fire_tuple { - ($($index:tt: $type:ident),*) => { - impl<'a, 'b, 'c, $($type: FromRedisValue),*> RedisBatch<'a, 'b, 'c, ($($type,)*)> { - /// Commit the batch and return the command results in a tuple. - /// If redis unavailable, or the types didn't match causing decoding to fail, `None` will be returned and the error logged. - pub async fn fire(mut self) -> Option<($($type,)*)> - where - ($($type,)*): FromRedisValue, - { +macro_rules! impl_batch_fire { + ( $($tup_item:ident)* ) => ( + impl<'a, 'b, 'c, $($tup_item: FromRedisValue),*> RedisBatchFire for RedisBatch<'a, 'b, 'c, ($($tup_item,)*)> { + type ReturnType = ($($tup_item,)*); + + async fn fire(mut self) -> Option<($($tup_item,)*)> { self.inner_fire().await } } - }; + ); +} + +/// Implements all the supported redis operations for the batch. +pub trait RedisBatchOps<'c> { + /// The current batch struct sig. + type CurrentType; + /// The producer for the next batch struct sig. + type NextType; + + /// Run an arbitrary redis (lua script). + fn script( + self, + script_invokation: RedisScriptInvoker<'c>, + ) -> Self::NextType; + + /// Run an arbitrary redis (lua script). But discards any return value. + fn script_no_return(self, script_invokation: RedisScriptInvoker<'c>) -> Self::CurrentType; + + /// Set a key to a value with an optional expiry. + /// + /// (expiry accurate to the millisecond) + fn set( + self, + namespace: &'static str, + key: &str, + value: impl ToRedisArgs, + expiry: Option, + ) -> Self::CurrentType; + + /// Set multiple values (MSET) of the same type at once. If expiry used will use a custom lua script to achieve the functionality. + /// + /// (expiry accurate to the millisecond) + fn mset<'key, Value: ToRedisArgs>( + self, + namespace: &'static str, + pairs: impl IntoIterator, + expiry: Option, + ) -> Self::CurrentType; + + /// Clear one or more keys. + fn clear<'key>( + self, + namespace: &'static str, + keys: impl IntoIterator, + ) -> Self::CurrentType; + + /// Clear all keys under a given namespace + fn clear_namespace(self, namespace: &'static str) -> Self::CurrentType; + + /// Check if a key exists. + fn exists(self, namespace: &'static str, key: &str) -> Self::NextType; + + /// Check if multiple keys exists. + fn mexists<'key>( + self, + namespace: &'static str, + keys: impl IntoIterator, + ) -> Self::NextType>; + + /// Get a value from a key. Returning `None` if the key doesn't exist. + fn get( + self, + namespace: &'static str, + key: &str, + ) -> Self::NextType>; + + /// Get multiple values (MGET) of the same type at once. Returning `None` for each key that didn't exist. + fn mget<'key, Value>( + self, + namespace: &'static str, + keys: impl IntoIterator, + ) -> Self::NextType>>; } -// Implement batch fire() for up to 16 operations: (EXCEPT FOR one command, which is implemented separately to return the value itself rather than the tuple) -impl_batch_fire_tuple!(); -// impl_batch_fire_tuple!(0: A); // Not this one, its got a custom implementation. -impl_batch_fire_tuple!(0: A, 1: B); -impl_batch_fire_tuple!(0: A, 1: B, 2: C); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L, 12: M); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L, 12: M, 13: N); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L, 12: M, 13: N, 14: O); -impl_batch_fire_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L, 12: M, 13: N, 14: O, 15: P); - -macro_rules! impl_batch_methods { - ($($index:tt: $type:ident),*) => { - impl<'a, 'b, 'c, $($type: FromRedisValue),*> RedisBatch<'a, 'b, 'c, ($($type,)*)> { - /// Run an arbitrary redis (lua script). - pub fn script(mut self, script_invokation: RedisScriptInvoker<'c>) -> RedisBatch<'a, 'b, 'c, ($($type,)* ScriptOutput,)> - where - ScriptOutput: FromRedisValue, - { +macro_rules! impl_batch_ops { + ( $($tup_item:ident)* ) => ( + impl<'a, 'b, 'c, $($tup_item: FromRedisValue),*> RedisBatchOps<'c> for RedisBatch<'a, 'b, 'c, ($($tup_item,)*)> { + type CurrentType = RedisBatch<'a, 'b, 'c, ($($tup_item,)*)>; + type NextType = RedisBatch<'a, 'b, 'c, ($($tup_item,)* T,)>; + + fn script( + mut self, + script_invokation: RedisScriptInvoker<'c>, + ) -> Self::NextType { self.pipe.add_command(script_invokation.eval_cmd()); self.used_scripts.insert(script_invokation.script); RedisBatch { @@ -157,9 +214,7 @@ macro_rules! impl_batch_methods { } } - /// Run an arbitrary redis (lua script). But discards any return value. - pub fn script_no_return(mut self, script_invokation: RedisScriptInvoker<'c>) -> RedisBatch<'a, 'b, 'c, ($($type,)*)> - { + fn script_no_return(mut self, script_invokation: RedisScriptInvoker<'c>) -> Self::CurrentType { // Adding ignore() to ignore response. self.pipe.add_command(script_invokation.eval_cmd()).ignore(); self.used_scripts.insert(script_invokation.script); @@ -171,14 +226,13 @@ macro_rules! impl_batch_methods { } } - /// Set a key to a value with an optional expiry. - /// - /// (expiry accurate to the millisecond) - pub fn set<'key, Key, Value>(mut self, namespace: &'static str, key: Key, value: Value, expiry: Option) -> RedisBatch<'a, 'b, 'c, ($($type,)*)> - where - Key: Into>, - Value: ToRedisArgs, - { + fn set( + mut self, + namespace: &'static str, + key: &str, + value: impl ToRedisArgs, + expiry: Option, + ) -> Self::CurrentType { let final_key = self.redis_conn.final_key(namespace, key.into()); if let Some(expiry) = expiry { @@ -200,15 +254,12 @@ macro_rules! impl_batch_methods { } } - /// Set multiple values (MSET) of the same type at once. If expiry used will use a custom lua script to achieve the functionality. - /// - /// (expiry accurate to the millisecond) - pub fn mset<'key, Key, Value, Pairs>(mut self, namespace: &'static str, pairs: Pairs, expiry: Option) -> RedisBatch<'a, 'b, 'c, ($($type,)*)> - where - Value: ToRedisArgs, - Key: Into>, - Pairs: IntoIterator, - { + fn mset<'key, Value: ToRedisArgs>( + mut self, + namespace: &'static str, + pairs: impl IntoIterator, + expiry: Option, + ) -> Self::CurrentType { let final_pairs = pairs.into_iter().map(|(key, value)| (self.redis_conn.final_key(namespace, key.into()), value)).collect::>(); if let Some(expiry) = expiry { @@ -239,12 +290,11 @@ macro_rules! impl_batch_methods { } } - /// Clear one or more keys. - pub fn clear<'key, Keys, Key>(mut self, namespace: &'static str, keys: Keys) -> RedisBatch<'a, 'b, 'c, ($($type,)*)> - where - Keys: IntoIterator, - Key: Into>, - { + fn clear<'key>( + mut self, + namespace: &'static str, + keys: impl IntoIterator, + ) -> Self::CurrentType { let final_keys = keys.into_iter().map(Into::into).map(|key| self.redis_conn.final_key(namespace, key)).collect::>(); // Ignoring so it doesn't take up a space in the tuple response. self.pipe.del(final_keys).ignore(); @@ -256,18 +306,12 @@ macro_rules! impl_batch_methods { } } - /// Clear all keys under a given namespace - pub fn clear_namespace(self, namespace: &'static str) -> RedisBatch<'a, 'b, 'c, ($($type,)*)> - { + fn clear_namespace(self, namespace: &'static str) -> Self::CurrentType { let final_namespace = self.redis_conn.final_namespace(namespace); self.script_no_return(CLEAR_NAMESPACE_SCRIPT.invoker().arg(final_namespace)) } - /// Check if a key exists. - pub fn exists<'key, Key>(mut self, namespace: &'static str, key: Key) -> RedisBatch<'a, 'b, 'c, ($($type,)* bool,)> - where - Key: Into>, - { + fn exists(mut self, namespace: &'static str, key: &str) -> Self::NextType { self.pipe.exists(self.redis_conn.final_key(namespace, key.into())); RedisBatch { _returns: PhantomData, @@ -277,12 +321,11 @@ macro_rules! impl_batch_methods { } } - /// Check if multiple keys exists. - pub fn mexists<'key, Keys, Key>(self, namespace: &'static str, keys: Keys) -> RedisBatch<'a, 'b, 'c, ($($type,)* Vec,)> - where - Keys: IntoIterator, - Key: Into>, - { + fn mexists<'key>( + self, + namespace: &'static str, + keys: impl IntoIterator, + ) -> Self::NextType> { let final_keys = keys.into_iter().map(Into::into).map(|key| self.redis_conn.final_key(namespace, key)).collect::>(); let mut invoker = MEXISTS_SCRIPT.invoker(); for key in &final_keys { @@ -291,12 +334,11 @@ macro_rules! impl_batch_methods { self.script::>(invoker) } - /// Get a value from a key. Returning `None` if the key doesn't exist. - pub fn get<'key, Value, Key>(mut self, namespace: &'static str, key: Key) -> RedisBatch<'a, 'b, 'c, ($($type,)* Option,)> - where - Key: Into>, - Value: FromRedisValue - { + fn get( + mut self, + namespace: &'static str, + key: &str, + ) -> Self::NextType> { self.pipe.get(self.redis_conn.final_key(namespace, key.into())); RedisBatch { _returns: PhantomData, @@ -306,13 +348,11 @@ macro_rules! impl_batch_methods { } } - /// Get multiple values (MGET) of the same type at once. Returning `None` for each key that didn't exist. - pub fn mget<'key, Value, Keys, Key>(mut self, namespace: &'static str, keys: Keys) -> RedisBatch<'a, 'b, 'c, ($($type,)* Vec>,)> - where - Keys: IntoIterator, - Key: Into>, - Value: FromRedisValue - { + fn mget<'key, Value>( + mut self, + namespace: &'static str, + keys: impl IntoIterator, + ) -> Self::NextType>> { let final_keys = keys.into_iter().map(Into::into).map(|key| self.redis_conn.final_key(namespace, key)).collect::>(); self.pipe.get(final_keys); @@ -324,24 +364,35 @@ macro_rules! impl_batch_methods { } } } - }; + ); } -// Implement batch methods for up to 16 operations: -impl_batch_methods!(); -impl_batch_methods!(0: A); -impl_batch_methods!(0: A, 1: B); -impl_batch_methods!(0: A, 1: B, 2: C); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L, 12: M); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L, 12: M, 13: N); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L, 12: M, 13: N, 14: O); -impl_batch_methods!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L, 12: M, 13: N, 14: O, 15: P); +// fire() trait for up to 12 operations: +impl_batch_fire! {} +// impl_batch_fire! { A } // Special case that returns the command output directly (not in tuple) +impl_batch_fire! { A B } +impl_batch_fire! { A B C } +impl_batch_fire! { A B C D } +impl_batch_fire! { A B C D E } +impl_batch_fire! { A B C D E F } +impl_batch_fire! { A B C D E F G } +impl_batch_fire! { A B C D E F G H } +impl_batch_fire! { A B C D E F G H I } +impl_batch_fire! { A B C D E F G H I J } +impl_batch_fire! { A B C D E F G H I J K } +impl_batch_fire! { A B C D E F G H I J K L } + +// redis ops trait for up to 12 operations: +impl_batch_ops! {} +impl_batch_ops! { A } +impl_batch_ops! { A B } +impl_batch_ops! { A B C } +impl_batch_ops! { A B C D } +impl_batch_ops! { A B C D E } +impl_batch_ops! { A B C D E F } +impl_batch_ops! { A B C D E F G } +impl_batch_ops! { A B C D E F G H } +impl_batch_ops! { A B C D E F G H I } +impl_batch_ops! { A B C D E F G H I J } +impl_batch_ops! { A B C D E F G H I J K } +impl_batch_ops! { A B C D E F G H I J K L } diff --git a/rust/bitbazaar/redis/conn.rs b/rust/bitbazaar/redis/conn.rs index 7b263080..af8c60da 100644 --- a/rust/bitbazaar/redis/conn.rs +++ b/rust/bitbazaar/redis/conn.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, future::Future}; use deadpool_redis::redis::{FromRedisValue, ToRedisArgs}; -use super::batch::RedisBatch; +use super::batch::{RedisBatch, RedisBatchFire, RedisBatchOps}; use crate::errors::prelude::*; /// Wrapper around a lazy redis connection. @@ -14,6 +14,22 @@ pub struct RedisConn<'a> { /// Public methods for RedisConn. impl<'a> RedisConn<'a> { + /// Get an internal connection from the pool, connections are kept in the pool for reuse. + /// If redis is acting up and unavailable, this will return None. + /// NOTE: this mainly is used internally, but provides a fallback to the underlying connection, if the exposed interface does not provide options that fit an external user need (which could definitely happen). + pub async fn get_inner_conn(&mut self) -> Option<&mut deadpool_redis::Connection> { + if self.conn.is_none() { + match self.pool.get().await { + Ok(conn) => self.conn = Some(conn), + Err(e) => { + tracing::error!("Could not get redis connection: {}", e); + return None; + } + } + } + self.conn.as_mut() + } + /// Get a new [`RedisBatch`] for this connection that commands can be piped together with. pub fn batch<'ref_lt>(&'ref_lt mut self) -> RedisBatch<'ref_lt, 'a, '_> { RedisBatch::new(self) @@ -31,16 +47,6 @@ impl<'a> RedisConn<'a> { format!("{}:{}", self.final_namespace(namespace), key) } - // /// Clear all keys under a namespace. Returning the number of keys deleted. - // pub async fn clear_namespace<'c>(&mut self, namespace: &'static str) -> Option { - // let final_namespace = self.final_namespace(namespace); - // CLEAR_NAMESPACE_SCRIPT - // .run(self, |scr| { - // scr.arg(final_namespace); - // }) - // .await - // } - /// Cache an async function in redis with an optional expiry. /// If already stored, the cached value will be returned, otherwise the function will be stored in redis for next time. /// @@ -64,7 +70,7 @@ impl<'a> RedisConn<'a> { let cached = self .batch() - .get::(namespace, key.clone()) + .get::(namespace, &key) .fire() .await .flatten(); @@ -72,7 +78,7 @@ impl<'a> RedisConn<'a> { Ok(cached) } else { let val = cb().await?; - self.batch().set(namespace, key, &val, expiry).fire().await; + self.batch().set(namespace, &key, &val, expiry).fire().await; Ok(val) } } @@ -87,19 +93,4 @@ impl<'a> RedisConn<'a> { conn: None, } } - - /// Get an internal connection from the pool, reused after first call. - /// If redis is acting up and unavailable, this will return None. - pub(crate) async fn get_conn(&mut self) -> Option<&mut deadpool_redis::Connection> { - if self.conn.is_none() { - match self.pool.get().await { - Ok(conn) => self.conn = Some(conn), - Err(e) => { - tracing::error!("Could not get redis connection: {}", e); - return None; - } - } - } - self.conn.as_mut() - } } diff --git a/rust/bitbazaar/redis/dlock.rs b/rust/bitbazaar/redis/dlock.rs new file mode 100644 index 00000000..f2383bd9 --- /dev/null +++ b/rust/bitbazaar/redis/dlock.rs @@ -0,0 +1,380 @@ +// Derived from https://github.com/hexcowboy/rslock + +// Copyright (c) 2014-2021, Jan-Erik Rediger + +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +// * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +// * Neither the name of Redis nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::time::{Duration, Instant}; + +use error_stack::Context; +use futures::{future::join_all, Future}; +use futures_timer::Delay; +use once_cell::sync::Lazy; +use rand::{thread_rng, Rng, RngCore}; +use redis::{RedisResult, Value}; + +use super::{RedisBatchFire, RedisBatchOps, RedisConn, RedisScript}; +use crate::prelude::*; + +const RETRY_DELAY: u32 = 200; +const CLOCK_DRIFT_FACTOR: f32 = 0.01; + +const UNLOCK_LUA: &str = r#" +if redis.call("GET", KEYS[1]) == ARGV[1] then + return redis.call("DEL", KEYS[1]) +else + return 0 +end +"#; +const EXTEND_LUA: &str = r#" +if redis.call("get", KEYS[1]) ~= ARGV[1] then + return 0 +else + if redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2]) ~= nil then + return 1 + else + return 0 + end +end +"#; + +static UNLOCK_SCRIPT: Lazy = Lazy::new(|| RedisScript::new(UNLOCK_LUA)); +static EXTEND_SCRIPT: Lazy = Lazy::new(|| RedisScript::new(EXTEND_LUA)); + +/// Errors that can occur when trying to lock a resource. +#[derive(Debug)] +pub enum RedisLockErr { + /// When the lock is held by someone else. + Unavailable, + /// When the user has done something wrong. + UserErr, +} + +impl std::fmt::Display for RedisLockErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RedisLockErr::UserErr => write!(f, "User error"), + RedisLockErr::Unavailable => write!(f, "Lock unavailable"), + } + } +} + +impl Context for RedisLockErr {} + +/// A distributed lock for Redis. +pub struct RedisLock<'a> { + redis: &'a super::Redis, + /// The resource to lock. Will be used as the key in Redis. + pub lock_id: Vec, + /// The value for this lock. + pub val: Vec, + /// How long to wait before giving up trying to get the lock. + pub wait_up_to: Option, +} + +impl<'a> RedisLock<'a> { + /// Creates a new lock, use [`super::Redis::dlock`] instead. + pub(crate) async fn new( + redis: &'a super::Redis, + lock_id: &str, + ttl: Duration, + wait_up_to: Option, + ) -> Result, RedisLockErr> { + if ttl < Duration::from_millis(100) { + return Err(err!( + RedisLockErr::UserErr, + "Do not set time to live to less than 100 milliseconds." + )); + } + + let mut lock = RedisLock { + redis, + lock_id: lock_id.as_bytes().to_vec(), + val: get_unique_lock_id(), + wait_up_to, + }; + + // Need to actually lock for the first time: + let lock_id = lock.lock_id.clone(); + let val = lock.val.clone(); + lock.exec_or_retry(ttl, move |mut conn| { + let lock_id = lock_id.clone(); + let val = val.clone(); + async move { + if let Some(conn) = conn.get_inner_conn().await { + let result: RedisResult = redis::cmd("SET") + .arg(lock_id) + .arg(val) + .arg("NX") + .arg("PX") + .arg(ttl.as_millis() as usize) + .query_async(conn) + .await; + + match result { + Ok(Value::Okay) => true, + Ok(_) | Err(_) => false, + } + } else { + false + } + } + }) + .await?; + + Ok(lock) + } + + /// Extend the lifetime of the lock from the previous ttl. + /// Note this will be the new ttl from this point, meaning if this is called with 10 seconds, the lock will be killed after 10 seconds, not the prior remaining plus 10 seconds. + /// + /// Returns: + /// true: the lock was successfully extended. + /// false: the lock could not be extended for some reason. + pub async fn extend(&mut self, new_ttl: Duration) -> Result { + if new_ttl < Duration::from_millis(100) { + return Err(err!( + RedisLockErr::UserErr, + "Do not set time to live to less than 100 milliseconds." + )); + } + + let lock_id = self.lock_id.clone(); + let val = self.val.clone(); + self.exec_or_retry(new_ttl, move |mut conn| { + let lock_id = lock_id.clone(); + let val = val.clone(); + async move { + let result: Option = conn + .batch() + .script( + EXTEND_SCRIPT + .invoker() + .key(lock_id) + .arg(val) + .arg(new_ttl.as_millis() as usize), + ) + .fire() + .await; + + match result { + Some(val) => val == 1, + None => false, + } + } + }) + .await + } + + /// Unlock the lock manually. + /// Not necessarily needed, the lock will expire automatically after the TTL. + /// + /// Returns: + /// true: the lock was successfully unlocked. + /// false: the lock could not be unlocked for some reason. + pub async fn unlock(&mut self) -> bool { + let result = join_all( + self.redis + .get_conn_to_each_server() + .into_iter() + .map(|mut conn| { + let lock_id = self.lock_id.clone(); + let val = self.val.clone(); + async move { + let result: Option = conn + .batch() + .script(UNLOCK_SCRIPT.invoker().key(lock_id).arg(val)) + .fire() + .await; + + match result { + Some(val) => val == 1, + _ => false, + } + } + }), + ) + .await; + result.into_iter().all(|unlocked| unlocked) + } + + // Error handling and retrying for a locking operation (lock/extend). + async fn exec_or_retry(&mut self, ttl: Duration, cb: F) -> Result + where + F: Fn(RedisConn<'a>) -> Fut, + Fut: Future, + { + let ttl = ttl.as_millis() as usize; + + let attempt_beginning = Instant::now(); + let wait_up_to = self.wait_up_to.unwrap_or(Duration::from_secs(0)); + let mut first_run = true; + while first_run || wait_up_to > attempt_beginning.elapsed() { + first_run = false; + + let start_time = Instant::now(); + let conns = self.redis.get_conn_to_each_server(); + // Quorum is defined to be N/2+1, with N being the number of given Redis instances. + let quorum = (conns.len() as u32) / 2 + 1; + + let n = join_all(conns.into_iter().map(&cb)) + .await + .into_iter() + .fold(0, |count, locked| if locked { count + 1 } else { count }); + + let drift = (ttl as f32 * CLOCK_DRIFT_FACTOR) as usize + 2; + let elapsed = start_time.elapsed(); + let elapsed_ms = + elapsed.as_secs() as usize * 1000 + elapsed.subsec_nanos() as usize / 1_000_000; + if ttl <= drift + elapsed_ms { + return Err(err!(RedisLockErr::Unavailable).attach_printable(format!( + "Ttl expired during locking, ttl millis: {}, potential_drift: {}, elapsed_ms: {}. Try increasing the lock's ttl.", + ttl, drift, elapsed_ms + ))); + } + let validity_time = ttl + - drift + - elapsed.as_secs() as usize * 1000 + - elapsed.subsec_nanos() as usize / 1_000_000; + + // If met the quorum and ttl still holds, succeed, otherwise just unlock. + if n >= quorum && validity_time > 0 { + println!("Lock VALIDITY: {}", validity_time); + return Ok(true); + } else { + self.unlock().await; + } + + let n = thread_rng().gen_range(0..RETRY_DELAY); + Delay::new(Duration::from_millis(n as u64)).await; + } + + Err(err!(RedisLockErr::Unavailable)).attach_printable(format!( + "Lock, unavailable, {}", + if let Some(wait_up_to) = self.wait_up_to { + format!("waited for: {:?}.", wait_up_to) + } else { + "user configured to not wait all.".to_string() + } + )) + } +} + +/// Get 20 random bytes from the pseudorandom interface. +fn get_unique_lock_id() -> Vec { + let mut buf = [0u8; 20]; + thread_rng().fill_bytes(&mut buf); + buf.to_vec() +} + +/// Run by the main tester that spawns up a redis process. +#[cfg(test)] +pub async fn redis_dlock_tests(r: super::Redis) -> Result<(), AnyErr> { + // Just checking the object is normal: (from upstream) + fn is_normal() {} + is_normal::(); + + assert_eq!(get_unique_lock_id().len(), 20); + let id1 = get_unique_lock_id(); + let id2 = get_unique_lock_id(); + assert_eq!(20, id1.len()); + assert_eq!(20, id2.len()); + assert_ne!(id1, id2); + + macro_rules! check_lockable { + ($name:expr) => {{ + let mut lock = r + .dlock($name, Duration::from_secs(1), None) + .await + .change_context(AnyErr)?; + lock.unlock().await; + }}; + } + + macro_rules! check_not_lockable { + ($name:expr) => {{ + if (r.dlock($name, Duration::from_secs(1), None).await).is_ok() { + return Err(anyerr!("Lock acquired, even though it should be locked")); + } + }}; + } + + // Manual unlock should work: + let mut lock = r + .dlock("test_lock_lock_unlock", Duration::from_secs(1), None) + .await + .change_context(AnyErr)?; + // Should fail as instantly locked: + check_not_lockable!("test_lock_lock_unlock"); + check_not_lockable!("test_lock_lock_unlock"); // Purposely checking twice + tokio::time::sleep(Duration::from_millis(30)).await; + // Should still be locked after 30ms: (ttl is 1s) + check_not_lockable!("test_lock_lock_unlock"); + // Manual unlock should instantly allow relocking: + lock.unlock().await; + check_lockable!("test_lock_lock_unlock"); + + // Make lock live for 100ms, after 50ms should fail, after 110ms should succeed with no manual unlock: + let _ = r + .dlock("test_lock_autoexpire", Duration::from_millis(100), None) + .await + .change_context(AnyErr)?; + // 50ms shouldn't be enough to unlock: + tokio::time::sleep(Duration::from_millis(50)).await; + check_not_lockable!("test_lock_autoexpire"); + // another 50msms should be enough to unlock: + tokio::time::sleep(Duration::from_millis(60)).await; + check_lockable!("test_lock_autoexpire"); + + // New test, confirm extend does extend by expected amount: + let mut lock = r + .dlock("test_lock_extend", Duration::from_millis(100), None) + .await + .change_context(AnyErr)?; + tokio::time::sleep(Duration::from_millis(50)).await; + // This means should be valid for another 100ms: + lock.extend(Duration::from_millis(100)) + .await + .change_context(AnyErr)?; + // Sleep for 60, would have expired original, but new will still be valid for another 40: + tokio::time::sleep(Duration::from_millis(60)).await; + check_not_lockable!("test_lock_extend"); + // Should now go over extension, should be relockable: + tokio::time::sleep(Duration::from_millis(50)).await; + check_lockable!("test_lock_extend"); + + // Confirm retries would work to wait for a lock: + let _ = r + .dlock("test_lock_retry", Duration::from_millis(300), None) + .await + .change_context(AnyErr)?; + // This will fail as no wait: + check_not_lockable!("test_lock_retry"); + // This will fail as only waiting 100ms: + if r.dlock( + "test_lock_retry", + Duration::from_millis(100), + Some(Duration::from_millis(100)), + ) + .await + .is_ok() + { + return Err(anyerr!("Lock acquired, even though it should be locked")); + } + // This will succeed as waiting for another 250ms, which should easily hit the 300ms ttl: + r.dlock( + "test_lock_retry", + Duration::from_millis(100), + Some(Duration::from_millis(250)), + ) + .await + .change_context(AnyErr)?; + + Ok(()) +} diff --git a/rust/bitbazaar/redis/mod.rs b/rust/bitbazaar/redis/mod.rs index 4ce81540..5fa9d54b 100644 --- a/rust/bitbazaar/redis/mod.rs +++ b/rust/bitbazaar/redis/mod.rs @@ -1,11 +1,13 @@ mod batch; mod conn; +mod dlock; mod json; mod script; mod wrapper; -pub use batch::RedisBatch; +pub use batch::{RedisBatch, RedisBatchFire, RedisBatchOps}; pub use conn::RedisConn; +pub use dlock::{RedisLock, RedisLockErr}; pub use json::{RedisJson, RedisJsonConsume}; pub use script::{RedisScript, RedisScriptInvoker}; pub use wrapper::Redis; @@ -22,7 +24,7 @@ mod tests { use rstest::*; use super::*; - use crate::{errors::prelude::*, misc::in_ci}; + use crate::{errors::prelude::*, misc::in_ci, redis::dlock::redis_dlock_tests}; struct ChildGuard(Child); @@ -99,7 +101,7 @@ mod tests { // Shouldn't exist yet: for (conn, exp) in [(&mut work_conn, Some(None)), (&mut fail_conn, None)] { - assert_eq!(conn.batch().get::("", "foo").fire().await, exp); + assert_eq!(conn.batch().get::("", "foo").fire().await, exp); } // Set so should now exist: @@ -110,7 +112,7 @@ mod tests { (&mut work_conn, Some(Some("bar".to_string()))), (&mut fail_conn, None), ] { - assert_eq!(conn.batch().get::("", "foo").fire().await, exp); + assert_eq!(conn.batch().get::("", "foo").fire().await, exp); } // Multiple should come back as tuple: @@ -120,7 +122,7 @@ mod tests { ] { assert_eq!( conn.batch() - .get::("", "I don't exist") + .get::("", "I don't exist") .get("", "foo") .fire() .await, @@ -198,7 +200,7 @@ mod tests { .mset("n3", [("foo", "foo"), ("bar", "bar"), ("baz", "baz")], None) .clear_namespace("n1") .clear("n2", ["foo", "baz"]) - .mget::("n1", ["foo", "bar", "baz"]) + .mget::("n1", ["foo", "bar", "baz"]) .mget("n2", ["foo", "bar", "baz"]) .mget("n3", ["foo", "bar", "baz"]) .fire() @@ -261,7 +263,7 @@ mod tests { }), None ) - .get::, _>("", "foo") + .get::>("", "foo") .fire() .await .flatten() @@ -352,7 +354,7 @@ mod tests { assert_eq!( work_conn .batch() - .get::("e1", "foo") + .get::("e1", "foo") .get("e1", "bar") .mget("e2", ["foo", "bar", "baz", "qux"]) .fire() @@ -373,14 +375,17 @@ mod tests { assert_eq!( work_conn .batch() - .get::("e1", "foo") - .get::("e1", "bar") - .mget::, _, _>("e2", ["foo", "bar", "baz", "qux"]) + .get::("e1", "foo") + .get::("e1", "bar") + .mget::>("e2", ["foo", "bar", "baz", "qux"]) .fire() .await, Some((None, None, vec![None, None, None, None])) ); + // Run the dlock tests: + redis_dlock_tests(work_r).await?; + Ok(()) } } diff --git a/rust/bitbazaar/redis/wrapper.rs b/rust/bitbazaar/redis/wrapper.rs index 9d0f88c8..978cb315 100644 --- a/rust/bitbazaar/redis/wrapper.rs +++ b/rust/bitbazaar/redis/wrapper.rs @@ -1,12 +1,15 @@ +use std::time::Duration; + use deadpool_redis::{Config, Runtime}; -use super::RedisConn; +use super::{RedisConn, RedisLock, RedisLockErr}; use crate::errors::prelude::*; /// A wrapper around redis to make it more concise to use and not need redis in the downstream Cargo.toml. /// /// This wrapper attempts to return very few errors to help build in automatic redis failure handling into downstream code. /// All redis errors (availability, unexpected content) will be logged as errors and results returned as `None` (or similar) where possible. +#[derive(Debug, Clone)] pub struct Redis { pool: deadpool_redis::Pool, prefix: String, @@ -35,4 +38,32 @@ impl Redis { pub fn conn(&self) -> RedisConn<'_> { RedisConn::new(&self.pool, &self.prefix) } + + /// Get a distributed redis lock. + /// + /// This lock will prevent others getting the lock, until it's time to live expires. Or the lock is manually released with [`RedisLock::unlock`]. + /// + /// Arguments: + /// - `lock_id`: The resource to lock. Will be used as the key in Redis. + /// - `ttl`: The time to live for this lock. After this time, the lock will be automatically released. + /// - `wait_up_to`: if the lock is busy elsewhere, wait this long trying to get it, before giving up and returning [`RedisLockErr::Unavailable`]. + pub async fn dlock( + &self, + lock_id: &str, + time_to_live: Duration, + wait_up_to: Option, + ) -> Result, RedisLockErr> { + RedisLock::new(self, lock_id, time_to_live, wait_up_to).await + } + + /// Escape hatch, access the inner deadpool_redis pool. + pub fn get_inner_pool(&self) -> &deadpool_redis::Pool { + &self.pool + } + + /// Used for dlock, the dlock algo is setup with multiple servers in mind, and synchronising locking between them. + /// It's a good, future proofed algo, so keeping the multi interface despite the current implementation only using one server. + pub fn get_conn_to_each_server(&self) -> Vec> { + vec![self.conn()] + } }