From cd2c4b465d53cec0f8a0affae5ed3065686925e2 Mon Sep 17 00:00:00 2001 From: bug-superman <2545080316@qq.com> Date: Thu, 5 Oct 2023 16:26:08 +0800 Subject: [PATCH] =?UTF-8?q?add=20restore=20aof(timestamp)=E3=80=81=20aof?= =?UTF-8?q?=20py=20test=20and=20aof=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 - cmd/redis-shake/main.go | 9 + docs/src/en/reader/aof_reader.md | 19 + docs/src/en/reader/scan_reader.md | 2 + docs/src/public/aof_reader.jpg | Bin 0 -> 64518 bytes docs/src/zh/guide/config.md | 1 + docs/src/zh/reader/aof_reader.md | 18 + docs/src/zh/reader/scan_reader.md | 2 + go.mod | 2 +- go.sum | 4 +- internal/aof/aof.go | 155 +++++ internal/reader/aof_reader.go | 104 +++ internal/reader/parsing_aof.go | 735 ++++++++++++++++++++++ internal/reader/scan_standalone_reader.go | 9 +- shake.toml | 5 + tests/cases/aof.py | 313 +++++++++ tests/helpers/constant.py | 2 - tests/helpers/redis.py | 1 - tests/helpers/shake.py | 12 +- 19 files changed, 1384 insertions(+), 11 deletions(-) create mode 100644 docs/src/en/reader/aof_reader.md create mode 100644 docs/src/public/aof_reader.jpg create mode 100644 docs/src/zh/reader/aof_reader.md create mode 100644 internal/aof/aof.go create mode 100644 internal/reader/aof_reader.go create mode 100644 internal/reader/parsing_aof.go create mode 100644 tests/cases/aof.py diff --git a/.gitignore b/.gitignore index def000a1..d864178d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,3 @@ dist/ tmp/ data/ *.log -*.rdb -*.aof diff --git a/cmd/redis-shake/main.go b/cmd/redis-shake/main.go index 106d044a..ed5700c5 100644 --- a/cmd/redis-shake/main.go +++ b/cmd/redis-shake/main.go @@ -60,6 +60,15 @@ func main() { } theReader = reader.NewRDBReader(opts) log.Infof("create RdbReader: %v", opts.Filepath) + } else if v.IsSet("aof_reader") { + opts := new(reader.AOFReaderOptions) + defaults.SetDefaults(opts) + err := v.UnmarshalKey("aof_reader", opts) + if err != nil { + log.Panicf("failed to read the AOFReader config entry. err: %v", err) + } + theReader = reader.NewAOFReader(opts) + log.Infof("create AOFReader: %v", opts.Filepath) } else { log.Panicf("no reader config entry found") } diff --git a/docs/src/en/reader/aof_reader.md b/docs/src/en/reader/aof_reader.md new file mode 100644 index 00000000..9779c3c8 --- /dev/null +++ b/docs/src/en/reader/aof_reader.md @@ -0,0 +1,19 @@ +# aof_reader + +## Introduction + +Can use ` aof_ Reader 'to read data from the AOF file and then write it to the target end. +It is commonly used to recover data from backup files and also supports data flash back. + +## configuration + +```toml +[aof_reader] +aoffilepath="/tmp/appendonly.aof.manifest" #or single-aof: /tmp/appendonly.aof" +aoftimestamp="0" +``` + +*An absolute path should be passed in. + +##The main process is as follows: +![aof_reader.jpg](/public/aof_reader.jpg) \ No newline at end of file diff --git a/docs/src/en/reader/scan_reader.md b/docs/src/en/reader/scan_reader.md index ea61718e..7131b411 100644 --- a/docs/src/en/reader/scan_reader.md +++ b/docs/src/en/reader/scan_reader.md @@ -24,6 +24,7 @@ username = "" # keep empty if not using ACL password = "" # keep empty if no authentication is required tls = false ksn = false # set to true to enabled Redis keyspace notifications (KSN) subscription +dbs = [] # set you want to scan dbs, if you don't want to scan all ``` * `cluster`:源端是否为集群 @@ -35,6 +36,7 @@ ksn = false # set to true to enabled Redis keyspace notifications * `tls`:源端是否开启 TLS/SSL,不需要配置证书因为 RedisShake 没有校验服务器证书 * `ksn`:开启 `ksn` 参数后 RedisShake 会在 `SCAN` 之前使用 [Redis keyspace notifications](https://redis.io/docs/manual/keyspace-notifications/) 能力来订阅 Key 的变化。当 Key 发生变化时,RedisShake 会使用 `DUMP` 与 `RESTORE` 命令来从源端读取 Key 的内容,并写入目标端。 +* `dbs`:源端为非集群模式时,支持指定DB库 ::: warning Redis keyspace notifications 不会感知到 `FLUSHALL` 与 `FLUSHDB` 命令,因此在使用 `ksn` 参数时,需要确保源端数据库不会执行这两个命令。 diff --git a/docs/src/public/aof_reader.jpg b/docs/src/public/aof_reader.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a3db65e04e1a2013b72ef728929fb681e3012a5 GIT binary patch literal 64518 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�)JQCpM(luBa9f_IFG3Vi9}mSQB_*AmoqhlQ{T&G@0yYI|rzwYx6!Jg7 zE@x|BrteXBV4Eg=)gGkcI37`d|Ml~|>Sc?O+4I+%SoC_@Yu+dBEwen!+YKhQ>wDo> zo=Mu22{Ot|*qn(KO`{(b%m|5r61~CtcWtKr-__HmwGS=};x=<Ud$gIv0g0?0LaLUPyOwrPHELIZg6c7#3o(-goqTML6y?`pOVL*;~I^k zsO!mQJQu|+_6g^46#sFMlGRam2l?~BXK5D_Q+T9><>lp{KU3uy8WRx_rSRCAzBEHd zkmI{~cK_;N4?)_Xudw;Iqn+8Stgm)UakKC0ioOw7Z&f?bzTUnXeKGri!FTHkT&?#Z zLzH4ynsVekT-yT&%%~qfer!%wKX~v!Q3j5)4tA_OlHarFn3&y*{eD^+8jpsV<5}Ny zvmZszD3s_EfR5ZM`Iwk^Tx{#j5S?)6H7x^GE7=VcWRq0dR?odd(4M|hsY@K#3>`GC^hLrx@b<<#&ZSWjl z>GFdrUDP1bcB8RzXr`%n7#+)A>l;U1(Wb`OgxNwtlWyWz@qUrvr2IXQV5N&ZZHUW> zPuLxMM1hZ=zuIcztK07U@84QiY{nv>km`OhXz6BYnwpvt&^$*R9%&86Li3lBmNsNk z1YRsJ|B?<5PyBQ$8q^viWYDJ3k@^oX7BcB9)`qgz*48Al*rE?NT|B+K1j=u3FOe$QkJTP@(InXwUSs^L85~nX z;)+R8HYe5n*Ot%M0;9GsXoh-st^3rQDwxlcuS-2PlkaQ~UeyWSniPE#az zbF0b>`)y`w8mZm<*rd0&m!;`~9j_M!DwI&-_~>YeSY}!pS$hBS^72fhcSG6ENpa2@4IqjZpyrjwg08cYI^*C_X~uT8$3xfDcfNx3@Q7kSil) zY}0g~v;6)gp%*omuOg5u_e^rq6uyk(!y`yy_Bkt$8dkmD>02U>v5|UqX6x&+Si$5N zR`zF`n5FV^f0bOwOlV`*&1+@}GvIY@4e#2T5s*NhAlIW zc>Sr4O~{p(lk@YSxv1>#w?EqQm0rGV^NJgEAJS)bm6z`oQ%jS2&&y+fzw4%xC5dnO zaCQ>ub^Kmeon!~)-J8z4A8Ii-YG2FrtVJnTxSt#B9~G=&wrnoSnYj& z6dkN07OGdu!oZNi_CQ!7YxiJn_+D^UVWBKdTv=K+olJ%j+){)@xy2}b#m@R@A#%X2 zj@_{_wHPU}QW9L;tU+^bt6$Ait=|Lht~vj8IrqDRqNS}p*a{~ePyY0WRg`E3=zc#~ z83hF|fs~;K`ln^wx)eu7L0LmPs4+4!@@lL@nAmi2(peT=KJe!BO-CQGl;rufa_>o~ zQDd^-n?14@w2vP@r@+RDBsvx#8tDQa&rcJb;Qq`V3vT?6GU;wuf-q;5>oxrlCffH) zl%{;4-xOyOPScwGKY#K%F3WwMO`-V0*CM)gn#?D{1u8Hg%mkUXB4H5_oK*7TCvS0a za#E4D>bx@~EN6K)LAEtZ$`!ly+?dq=rOzF~&-{t2dhDjNq!E+Ib>)34XPjyw_k9_L zx0yDKp9vqFdUP!97cZbRn3NO6AHx+Putuy;+?m9q?epHrS@ZZj$u3knUT-_w_`nhG zW&K|Mtk}4$cqykDozshXN$2x zN-}LsWS@1gT8J60ig>mB^&-5BqFk2;6z#XUcOZALLw*jl009n4O$JxQ_I=4H&-ztvur*O0Y=b4P4Ag)c^)~gBvNR}HpyhdQ)vf?JS$3(fud5px z9$UL6Tyv~INSXcLZ%}JqjRt7KWNkUhmo(BLdTa}|)`-S$dE*XCcsaVx~ zRqbk7GZ{P&I~Z9N=z0f8ygQd#`pgk zwuRz@E3)`Np(B)(l$1AeWpU9iJu{`Ni6NFsy+nuTBx7NI9+vght867Yyb!UA-CuAy zNmz-Dn(SLwgXU~!bD;Po_hRTkCEbWiB@C zH$F1*%nF$@ud2%BLk>=0^Ty%(`1ns3B~sN$avUtazJo*CLiURln?TSRy8ES03Tsfg z+cyrmd3eZ$eY~`0rC*adUYzVgqgP~QwY0S4Na8*o1^UoivT zLm0%Q-}q4vN0gPLR+S!qBUX@wrGFOVyScm*SJWR~GA-8E?8!;NeVv{6(E~9tF@cq1 z7}}y%&r?QZ=7CFDRat3B=<=uemYT4Nwl*=t`-LY4tHalyVvxtH5OEmp=?=x*DbLo2}BXZlpT5OA06brJ%aqeZ>eS9FRrYhXn%2@ zmx^U%VNs_~m>O46R-W5+^OyPQQmcc6gw&&q=vS^)y!~yzZ=+5;_UG}DnlLxJiQ)}!7 zj3!Rp3*dg{u5L_BPYa6ETC4qSA)Up4(6OZkcG0J9?H72X0|R!TOX2lU@Yv3LE?tO= zjTJS=myXT5j;>Y88q{DUIFn3oQbi6jJl>u)W0IDWTWpMAPTbg;Yb6Ln>aefXQ7}`@ ze0TTKWjNLE(SxEDmx(gFI|4RAW^{)aJVd@ms`?h6$acJZvP+NM|m^$Xk%(gLUJEAI@Y zcQ(0vB#~odV;?_$>}+S4(3eN+?sjb;27Mu{!@agf;UH|@K%PtMfq^lEP-NmfjvZ>Q)s=QGU9P_`7MxDlD_3dG z_0tyt<+l)CEP{k3ovy%p7I(}OulK8R>9p-`dQ$v!-w8=uv%L7A7bjKcoShP$_%#U} zsVAFeV2Oh1VQ_W!g`S)}9esT!U0AIUA0KJ1{b^~#k(Q?B#MR|FIA0*LbnJ$BH9`W>#o1 zZMbwdH#Y-4?9st&(*=#`z}-QlUxq3c+^wprs-cDW<(_0mP{M<;$kkL;p^lYL$VKUY zfGVeo5pH8{&g-JsGr5>(-E=XS(Op`ix%d0*)uU~TswFz)dq}4LNeOB3R_)NYfup!?>*2kJ zhY%Z!lRvV3fNXDXe`6`XHAZ&ulLbG!&Y9t%)3ew`a15m-X<)D7q6d03kiEi!Pyl;! z`}ACmyvIta>t~hxexF>hi5^QvLJB2QCketY5E)?O;CPWWSfo{nUhH_d{xYED%>plv z7y5e*MrJ0a-v0hsfLkpF(K6Z9wzH;OMY*{hv*#>voe)CGcpeO3h=5rq>BEOQVCt^3 z-ifCvmSq|)o4@h3_LcOg_a#`xm3x#8uVST&|A{aIO`sGDlJx_ghh6}Uk%NPSpN|jK z|I36GxI|zP1_lO#FT4IH0L?#|k^b*={rJLekw%$65gL%Sbk`BJVy!>|hv$Ld`9axv z)%|`q{DskSDiF;y#{FmjijaQcaD1E`p$TB8;TyohFJC|oI3_e5q#9!_Xm6LW;mSoQ z3wo$sJO-Nr%8Q4zr)NE6bAlv#sv}1Sd1^IUuagFn_Ut7EPznnQWMEHk3DvIFqv3>n zE+_~+9ob#z;OFOmK4~o>(Jsr<@YaRBoOTFd&{%%ibvVsk1#14?f`_uz0@teZj&~qLmytlf(X00JXT(b%aQ?tuFQ=(RuaY*`GgULh zTJBaa9EFTf4lSCwwKesP{gr&B3v}=iM)Of{q$qFNt)lO_Xz#|ByE#||A zP@5sNsOBR@k!(P2Mnxy}Cwkx$`%_39N-ghYlC zs-UTX=t=NbIZ9PGRu&c(D@hEje!_Ic3}PSdln7s}A`aC*ASL|SjPG88KO&7a*qt4$ zi5Qre83XTXv6+Y-J_K!dYCLmzE$J>uQ>(sN=3Q$}4p>COS%Y;>#sgTy z4foC*!_OZ&^O62W!-|l=9hrSc-TeM!p>$-WhWw|r2s!iP#{q|`KY6|<$WZUsFSC5X z=)a3t9zC(H^R4QLoMjf(?h=s0_jrtqC(O)^z})bz1!+<0YKhj=*Ip0FBQb5hBNQoioQcgEvnZjPFOAtL;BHMl4{JFvET@A?rQI@d$Y4Gq(Bq7M5C zhJ4laeKR~#UFO6>ReE1*3E{1SLxji$(TwOuY|%vOgRTqVngb2?R0Uc*^yEZx`UG@L zXJg$tM9OF{UB748e{Dp51Rf;@>U~86`47Y~aY3v{v%lgxP4c?aCbS{cao)aTCADH! z-v0QMSd8Ch4mO6?%a?&Njq|)ED)}0xyH2)3>q@YeU|jX^?SX)?Whr`!kQdygFFCNf znkSlJFxGz=$mFYMe-_;TT#`|Kyj<#P?S9p1yVItX_YUbGJTRxK z3YKncWaMpzEIA@2Gcz*}4?y8e9S;Z=K<^{zSabI<&m(xJk}uXuV8Tx1TCfy=f)z`Z zKWgFaG6sJiJQiYS{|Z)ndh0jgtCIy_Or1*(=|~?36|Etbq{Ezhtf;sGa6Ww1iO<8- z+6IekxVahO11rL-*X+m7qZqP!gn_T2&e`l#1P=y2ZhTDn>aPtU7m<4CuvWtp2QDg znd+k4r&V~zqjqWCs(3!O&8uFwblTMG(LVbDs#jA}Q+fG5M8Etn$ZuDKhhw0&XGTa- z8C}lo&axk=PrnU9r`adDSG=(O`_CW3E9CNZ*76U25-n`$OCkBIwYd)%H9o1*4=!Og z%+(yn-$E5ze9oTOjh}Y@u4teC_)|bYfM5Tk_SEA}&;!P(vqmir-2{Z*u(L%4%vkD*2#ktVLr) za3&SSpFP`Aw8zE8ZRKc=%C#A_dSl9&N#1yUb$;LaUQ)x}H8@U^TgR7H=!=F+7YV@vG$$x8rA;wXsLdU^+$6%h4Ja%+slXvKcwduxP1RQC_X+T-8QSOUAPzVt zT6chS%-s)J6E+Oboj(XcNmIz{mQzc5DT2ZM{k!MHbyT^F=7Y`C+E zCs@HH<`6FYrne2Q%9@Y`!uRduMcZ$V8iz-LTh7$m@mH)b9(jhEErI3WZERs_nZb^! zZe&FHLBNAAvOeV}wWn|jK!u;PFg@t{`4b+nr-TIZF^Us_zMWSn$|X>dOxQC{gDIy8 z$iL~?IyySSCDpOjfgADd+qZ%OdO{@Z?C)wmCOK+4i}wxM#0gaO^B^Yraumh6qpXXd z>{E2-ii4{8JMD<*PAhIG@M_y6y0W3UC3uBDD68i)o6g_$po`Oe$P8mCqh~&Q%X~aY zbG6QXDn|Z6+ozR$%<`Bh)nS*XbJOK+plH6TjIfw?MywL#W+0HLN^jcsv-z~JhIav+ z6>L-Av*E|O&sV~Jr+=H3L~eZP>wEoKu9b@v4bdMi_Uh^?|Dqg$^)#sk_HvTJ>wzc5 zH9v9bZtCrnDfYX8tFQW~dt&H0?;*-mKMesV2-0@gFU#Gi5A+H2ZyEU$yPX$*#sG&V z7jPS};?By@pBe=T&Bn&Y%j=>}+UjO~8>FQVCJ zz)FXQe$a?C`5E-}G4=2I)Kd`O+giSu?X+*@F4M3opZHo{KBy$fnVch&%?DW#sG=5? z0MtPdR@uV`#zIAd%y2ok&(B@T>-GBx)aN(a(p0ek&ILRU@^W%akESXJP!Yi|yu7>w zjVPoq5(%L_(7z0BKkstS#l;2kXlI2ZN-8Q~DH*NiMR8-tir-iD%>;Xx`X>m{b=B1m z0u|wXH(k;N+}VG5XZBQARB)=Yi59C(T5w`R}p+QGnZa$ca5GATS z@_Er3u;^LJ!Yp~E7E@w3L?TBloWHyx4qz9z2aeYTsk9M`Ltu5FoCO>Hilvr?%t)Kj z4b>BK5V?gg7oQx&(^$DS*W}aKxHuJj?ZvW#IT&%2larH7n)!(@6bRjPwXMFBUfXTf zl2xqsXy{~c>pK+#Yfg<9d)?YqR(=`9gO8^kY4UEM%dsGeEG{pDe+LOqc>AU9L`>x8 zDk?Z4*xV^hd3kR#=^FIXlaoUhPGD4EZRx-!S$U#2g@+Bp7zrUOE2}4%K4Bfe`P7{l zsx?5I(YYa(*4CH>R}e3@wzO=upE6+08(n{`9v4mVZgJ5BqZLTib5}oD1Pjg&fb?o_ z#viZ|u=B3>6Pk)`WS8C>CEkw=AOTO9gFRJG;09ZN9r7Jm3GUu?{#+n{m-d zz5D+19PgG(y*)`(Lu^79@&r}sxATM-x9aQ`#x6fqi62*WG(>+O zP>Rg7?kN%^^&>L8g4Y~`(~y?vJOt9Dz4>n?z0Rrlh5~XZ@v&W= zv9-!Qo^HUc5ojy%56x>H)miQFxR3Pwai|5u7u$r4Vb(5|O4)&?;60J58W^_IUte|qCHl^9b0mOZ$h~+v&Lcm5lVv28&Y_tz9vj5~m8G+XAi5bJ|C5WCS1R`NGYZt_IpiM*BMuG2LPB;Q7)Mh*Nss~N zB1n`tZNu{9i3p1hB%2nV5%95s)&QK%`L4lNQTD4*h`h%w&A)F5P(%mSf9DtI54q_+5C32Y%L_N1E+=mYYm|RpO)z^d1Vk|WH_;H%Z9kR# zDfk^Ky4%<_JHaNade+djubIdW_W4@dD}#m)P&=cSmrXhQ&~-`oE($Wbzy8#Fe1ivR z;zDvUSND6rJ;Rt=kItkE6a_+jHXp_kIa$BtP%Ct?(<&ZDFeri~f?f0r`LL=Iw9ax} zdl;tx`z1J|h&@)ZQIe^me2SE-5Y+^to`0pPG=9lM_p{vExB} zV<1jN_5Ai41=`Lca*gOkw*+19UO%KBP9+mF&~al(eS-WE)1*tF8V@2FxlMB2$(SLo z)zQ<_>*KJvCnpAo==yagV4WdrFEQU^_hz1ZV%Qt|P&e)n1?&z-MkU$`y#8`wG>>U+ zVG+xy%iZVPqUwLk7(q^nwR9^H1UJe(o$Ty( z0D|3Tu@7K2Oi4-cK3aBf&>NAG`ju1z(#|`l$=lfX7ZlLHH+>%wk;{5&ka(<)>dFZ-6FZi0LeiLHZV>;aA8&zs&! zCmFNi8LZ_4<2-QFkPk{sL~*{2utVz~62|Da$OhVHP7cjaV~ih84`thba13Cl4y>hT z{`?JR2~@1w2O54;?1%_rU4IDE@F|I5*f5k#_I*rD8N?^BX+EAL&PI`OIA6}}2lG@3 zT+8k841DkFQ&823IoE!nqx1Ldl4k|A=_-IrcHC63)oXq0a?Y9ms_X=1?WM5L1^M;f zTtL5n3Wl`~^mvwr1DxwOY*+6gEkIhbh+q(s79Bn+`F%JEOu-Z8D9gGa6VCKOg}1-I zo7Gj|_Nd+^V9YBh@W;E2Kxq!Be7ot|$GnAbSkc#IKWXg@>&mp#^=f^8ecj?}h@UV> zu8_aNH*4mLq}^XwDR4C3uui!OJtpU|<{&L1z_&?m5 z@pgQ|oizgy2=w6Wel$Zf&yHC;`mu?|sqL*`e5#D5u5pC389D3@Lngy z3pT5N3##$O;8tA#(mpc+_7aWYFpFK@wZLNej|1zc2*cvM&)I*fV&vWsprm<7|Iug; z6V~Y6qWF*@?E~f@6oppu(NBwaN8WrYnDcCy>FevGd+q+mJr+@fFhU|-@pXq19{x!Q z4E6ol;DFJen~3t?`D6%PKoINEUk)y=m&Ix*jTzH6;~R%l$VzySIQZ-0`KJT(RNjCR zl@dlJEh^cyp_~ zA#0#`7wIkmRLPxEc+=y}$;79oq3z*FQWV%GCMMzhU6sa^*)W68l?L`1~F zVGoQFB}NftRXx2liL58!rg*7?={RSXr&<8LF{SrCI3a(6K<(IcN$E9(e2B*iuuq6) z-9fVgzy}?MbXk9Z$)`5+pBtui9&19J5ov)H9=Pw$h^ zheSk%%!XA*06BZ8!TDV!cU5wc7NY}tu)YR1>HvbOgCLx+9-9ms*A-xB~ zM60vV1;FQOf$Ya%YxS&;r<?)o3}y6$NT^ zh#M6RHYycPrx-J4t;lz9ijuIBcM%wZcE@=FRn^rUvmQQVsLP@YrFaJ~IxOqq=?VDq zw-;!wnRLq~aGE>*YVJMIY5Va5Mv0LKr5)_E9uq8k&cg_9s(X@R*^ES>Xp5FR3=#{~ z!sOh+fm2;LiMX;;vz+8BNF`uhCHUFnJ$PVdVgklg;A9vQ7k-vnMX(8I?VmnyH_{lsmntFn4^XMZ+odC>q8w2|9LxtiUcN<#&0@{maR~fI?7E7e?=Z0^Rai z0e`6`kspTlS&1P`v21q=&j!Ym(=Cx>W#0NcxGc^Gx4&cW!4;GZaHRGnH9_n*Nn`D3 z(C@pwpCy<7PeIIll)~17gNr-(rtF>mgEWX=Tt%v?suE%Z!KC<>g+x|aUf$15m*H`& zB1%f|`U3S1X2K=1ROvT2HlAZCKO_9v>35!BCe9*coA{*;eK3&}>1MzihNU6=0QOnd zB!sMBz!*jW3%4zox)SKjN1(i#h^Jqqn$&bdMpi?XhDQRa;v1WJn(I&1a0g|)*wgwG zx68wEdlF%%)d9zT!Qh~`>=7wwOmUqsVihj20EstL|6foxYxF(qr}b1-Ioa92>u@}I zvQ{~+mz4bc9!N6lRBTl3mrwREpj8<^s<5&zw1 zx1Vn-rB#PPu8mL_oq^)POLA|EAyrR_+Zb9c{g?Y|%W_O3f{_29BnyAkV=D#|pKwS} z{S6LDA(;e$!djiaZ|U7r(V2NLL=2XTN9ZFP9OFMV`tE86lz8a1_Qq?3d|pN~iZC$) zQ>m~zP~~AN9?GHt>&=;Q4j}H>eWqqS-Jwi}ciZoc!mk{8R$|<@u=!2tT_$H|$#)l5 zlBq&Pze212s?w|p6Op~Tgnz8pUOc$TMfBWF0E_q7bL1Db@lZXI~3?bVkR;1Dbfl*G;g0%nExu!Doz9X3nW|V zXYQc+Kwtzgvcv3Qc*NkNN(;m2FZ%jB7*73{J$;(0uza=Y!q0#MA5f8;oD9ZmSe4^F zRHgVSy=0fcQg~})>z~>fxX(AC#8;{-xze~)()EH*qZ4En?Rb?Y zDoRR9np0W?Pe0uyO}i_WX00h9A#vMQTD8{uysBd%`6r=qiTNsFcv|LZ91E|ZjbC6?!Is7lZ5D1jCDkQj&^_;9NkgLQOQ5zk0hW579YFd@BlZs)0mzdLc70`>e+ zvy-bUWP9zmHy#VJ% zP%oPJPw~yiQZ#WeZ(gjHl8_+YZ{_lz?tDJ^`O6np(2~K-KlT1-K&qM+*IMK4&FI9# z?|Q`gc17WBQ^lueky5c#kqZ#=0?`NBcoCc1Z9jx*to5}u5q$QHFbw#(nnvt3>VZ~|$Mza-}E$1p+co5%vbMDDQQ!zdkQv%VPD^CN4(!yGh%g40H2e|QyU z*A)~MB^5KM*{-vSi#M~Pgg%^Aqaa|@KtfnD7uBgcNhtY~*^QPWarj3U#QSI=An3{=CZ&Dh}8NN_8e{^)f2b(zg1r{KEihI-$4f9tc z7F-_O(1u{hc*694{WMIEJy(t&wUC#W7cYd5e9$UZTULNZJrJ#1Aru-sh?cAwFlkW< z@?}LFAh8OxeT{=h5H*8OV9<)>7P05`cwcD(wJJoJe~|Iu_;}K) ziq$D{P(vLxj~>}k`i*X+?YCXzv&@WOh)PI1aeLt;JaFLfrTM@;7?@Yc#SLo%0Uw}2jy{ctOJ^+gs?PvK=`;ck z5R!T`^Af|8e<=Pp3NN@a6G3pJBw5cNXVxeUYNRWQT(TjUVx7q#>Bf;hbr54tRMfSt zo!rbB4!)RX1#6%ujgNT?QRH1ymo%hdjLMR7tsa;6_inmaR{n<22QIBbPclbteSLjV zQ8-m6r02kr$_fN%0H2)$3h;CRl{6RvrjV;}lCSV@oim_*yFFe1|JZu(c&zt7e%L9M zrjeDbWRsOjC6XN(B_SacAtDJGr3e{i7FidCkR6%H&gwEU8nUv<-rUbi-`{=z{P&tO?1?;Uu?!H-cH!`5h65rUuB7U`2n+KnsPPyt-&yqccfysAXN%Q` z%2k6neI0mx-=(Iu4%1&B#*w>gc=kV4~tD#QxFfdys`Sjdg1HW zulXtei46Cfu%2;7Gv2st06yfaj}X3U>2bKsnf$$klHOcQeyzq+z)<&7y1A`Q-tfn7 z2?G>o=>dnr%lvfC=Xq&i$7nI!HQVwX9dC2}TFRw=`XQYsipVBAQic$e zr6S=;TKtgfMa01wvSPh&?_T0*2zZAiV_Sw3F5Y55ro+_z2M5vF9IX4f^;BBse3jMT zuV1N_4-<~9<%X>^eyx*Txfi?OB}EDELnnVLBWk0*_cy%Pp}0<~D%;ncjI*L5kqz}v z>n{xDMJ?^P;h-fZtujl~`l@z0NhHuyN~_)sA|&YS?|Ua*zI18-iw$(Z>(y8S|X*b!=Gnf}x z0|$|Xf(!>nuWR{t{<~+nczTz*%~r4Lka(Pn%YYDN;%J$OU$p6g@t9I+@`im+w2^c% zp4Y8#zi{oIPoe&Mj99sP7fN{xf0o>Ah=3LW5PN*|D}=Y;SYhlinsh+G3kuc)$!IWw zh6VP}^~J5noW;HL`qVuzic%nDWWOcOvi9C?IqU3dx zvRtJ8X7(Guj}Y#GAw|E(B=ab2NB)TdB_>L1?gbPTn;-d)8rgZ{nf$wvDk3EQv2=*2 zk-fWK4|RlG<}0OgkX-GxS>a4o?yDs(PR`EDzH|a#<@uQ+?U8h{%%H^`{3apc&RsWi ze}8`jW9&ed2mU4`;%aJY0Y9x3yD$Bj%+pS};bpr0aoMnavtbg1a$Gz-hWU3M?W+}} zSAnK^0NQ1R}288erBUPfG899UC-dRgHaTiA? z)9DICk)dZrBjIR$Jo7z6OLOx}N}}M46zw!)q1gkrwwc@KNgBW0bGXkmj&(zo*psbx zAFqXf*vsw>SOpA9H$QcR`iTM-uMT8OPfOc7L(~i)Tuk=+os-?C=Gu6drY|P+Wr?O| zpQE+)1R8mQdovN8EMWUuQ!!^o&b0wNtu}gkOf#!a!_8do+c<{_8_?yO3Yq?zHfxI< zFqmH~oVk(ros{|X_qPmsqk-e!__jUl6gubXToPx0SUZe8Y6qRxyyow4?o_G!H<$lA zF}hu5jcmVT9GNQj>@BmmZgHFnF);?oTE5e{8uLn!LEmQyar*p2`lxNCp%~@z#NtX2 z!R%G1ImP+Ney4luS-hHFM^i*W-0|8x%?(C06AIQ{TKb&3_UzwuJzf=DGB8j(K{lY? zJek-sEH|va$oPj9g&IFNDyrtoHe+u6xSWg21AloGmsIp;mqv!_&${znR!x=uFI)7- ztcZ0`oI?Edy?t?kLpD6AhXW$>YurC8jzW*ps_B(=U6(CrI4FPUQb*PSh-41GjIXJ1 z7yUgWOXm&GH*+;Hf9EMW<~Iid3$Ii7JkyWk5)z}VIIo}BQb*judZfqc z3$w2%m8D?`kxMift%{Z5wcl~fLh`(>9G}voBA%m+oI;ISr1F;1Ym5e5hHCbRss}qd z6C|76J9W*~^^6P^h8Un`Kdus`wn>Ef9Xm;R#*@SAI_5pap9}RXXUSzyZ{o#|a1U{8 zeNj15H!ytS?2Ig`L*(~5ne@I%Ueu`j63ouU(JROF^Y6rI@cw`*R=w=~^A%a0r?U66 zKI%F_bUCZ5^!MXn^o*nAf6=FSeW9F9DeqytFIgp!UetSsM19Th3=5T^X(1Gqz>fKi zjgXdh1adS)mVx0;@(|#EaG~A#O*?EukV&M5m#2NNx>Vf2BVJ0uFLdG!g$Lruc$DqH zOU~wY(X!MHl1cxeh71Z~Yyeyi4)Zu{ucC@uuKhFGf+q?&KS0aEarkf_x)lf)dQ#(S zOq%Q729u<1EiEl?E)JtCejT3uUG{irCOA6!eVYZ(MPHFS`_uE4>S}9c*cM@1_)SXa zIexoKPMxm7({~-_hGis}y+g`UqEjkk(ED{~*k6Gjz+?(Oid4@Z{zapJu(-TDARgV| zL&>AOE>Mx{%lt=m&F2|~ye2w23|^bLJY(IZY*8$L#-P%^Gt!#;e+a{|!RQAQr@NN| zngm0T92Rcq=Jdm4Nl-|&$)uyb{mQj#{_l-++TMI@@rsq&K}OiIv3gw6(fTjkec7F< zZ5DN3zTi#=;)j%}<$b<#9FYwlkdWmghtR_FZ=4|t(s{(ebg1evAqinmP(pyu{FCzp z4tygu>v#=b0Fh2kADaY@x~E5c+|SB;S~Ic5Kav~za**Ac-BL%pX^J&*)BgAqmvRcM z7SJ#jS6}YdKhi_w`x%Ir5gQjVvUTQm@}kN^(X0A-_wT7_mu9dz2ZST@p@RNgF!nEq^t*=Myp% zS2qA`Emko$HrNc>-p;lV~%YOQ@(0td>^`!R(So&8yiRHs-Ek!4J4LlFASdOKJfjebk*}BLk4Qr zCco-{jryZ8gENw4-EJ%6ko+SIDgNVL$)*;w$oB)m#m`7=>WdfD7&6f7S@NIfzw|b% zDAG-wx-=A0J~v>-8od28OQ2#PBe`l*Txp@6n2_+*hc;5-oSEj(J&J?=Wk#_a%$CnA zjziY4wl?W*V`Br{()5@kFnOeqY<={wx^L-EBx<={e_BVlTqgasXM6$zCUlBPi>Lc$ zmdIr?zK`SSbzh%}X_Ng%^fGps_;H}=`qw+Iud7w(TE-m;m+Lrt?|ywypf= zo;yHac4cv~FEIIxpl1BS{~n@?{CHsi`M49V>r>RUWHXCA24eBN>H0= z5JN*LB2Zqh<+lc1ZFO@6>H=U)B?!LeRBcNX>6`^h*I`W~?akygZ4Dt_f(9?AbZs1^ zQqHZvsig<5p1Uz%GA7Nq2XkR~Gyar-7#ab59cy8 z&t+jiEd{OKbEm^xy!jP^`~VdZzcG+sx3fbJ`K#1@BQ5XOMHenjl|>2l($=KZ8r+t9 zw{IPP#hta)O2{_gU{d8>CI*w7NADj-pS9+&e$!9E1V}ur`I3p|a0x0izq(~oanu3vC$*T%<&QmL*UNGRocKo5|^cfZD|47v?fD;KTE%=Gk0=#uJAy@BfbwBuhR zAX5rT8mdXSO-jD`K*i+bSUU>@Q!4Z+?@Z``YC*6D$vs^xe{n#7ALyC|UY* z@VHHjso0W>)cM^E@+4dMY;sd=ZE{jlQgSl*hP9=^7{2yXw7b^+mQ#ALXK2H;rFJb; z^P222hw=SRu1a&cC(ozm-|0DrGJf;GJ$?6?nHlugwNkruC=YUR!q3CZ!tyiIWQS`5 zFor5#-X@{#gMt^+|Dx7rnpO_J|sRR-%@w3 z+QjcTAX6=EZAfS&+^r$Ii=C)ReL2p@=2`$3EYM0w5@e6Z`)$u@Hye7)9RrJ{!AH9{ zcZldNO{6a+w(u!m%zD2A$pX1KIdaTjFU5XfvngGhw>T$uUX`$%(>n7Tel`pi0u#aX zvU++EZ5C3GP)T@}>O$j`Vt*vNRWYH~2f z9iGPRj~{)}Zn^60=2lWrpy!&)yXgwfhn$IPKQRZah}XFsi}mTlPPg9LZ%J+5&OiC$ z)vJ%%x5$twp!xZjTA9(;O&c8qJ&7&QPS?(?!g-Rn9KS`!44g~Po&3Z^#ggY@TZefx z5?c=VS3z=EIq)4|8{^z@h_2~MW36(^ij>?KLR(@*_*l{Jei(FpxSs;@T zu^_OfuccKn<6;Xq z*^Zgl_joGxp6!Tv<72(F;|ZFp+qZA0sw&}U9P1Ji7B(C>M6y6`*MzPV_rX-P^tz~R zi#(jIeGY}mHl(ouNFIchn7rl6E@Ol^7H+bDZ(Z9>1a5H5GgRvt~QxD|JgS_#o3 zeASmGUVnp`IA=%UPql%_frIa)*K>A!*fK6hMGkYxQyG78?H~E)3_eGHsa8yVcH!O( z9g7&8k8MQ6gUxnjF}QCtteb76{kgAy|6uD4pf+a1WXR7e@6niSnOqRyZ*57*%VO%y z;L9K^wT?O+rp?{qxUINi|FP@JBOHqd#h?F{Xtd5#Ofj!t-j=L+bdfIR!P1&r-uE2hpa{F~(URwThV*IlF!*=fwOSTZdS} z*?@T?ai%r5QsZ*(E2c@ry$=LO?$kunj+?y_9@&WDXr-1%C z4qR@FWf19lvAE_YL)F^kd9K1d>fkMbd+Em=D}m@8V%)04@x(M;?4D9s7PoS}rFHF> zpC0VIb|J~3&+7*Cv_11A0K}2R-)nlrCtV2mfWP5({S`#_eFOCmVVR2Gp3D4VemSBK zuXG-?Da!1PNXqC0PWa{YnpNyHtDX1;r-D^1XX{TJM(DwuzD*rkN7OAg4 zf8G{hRR-t|I@+;QT@R5F}L^1sh)zh)?_a?VH2ouVSuI;(wo1 zQq$9I+}xx(75}S+U`kJ0o7*spT9FhoYIHjnF`=!k6(##d=bZ7Jo0uk(ebUo>{i0mJ zdMjNSlB66@LW`3icLgVGY-_){c;K<;?5&2DvXv&J({g~1>{Hz?QV!rJrb{O{pU0nz zS5BOW%lj`mvT`d~kqapguL6>|v94U?wWJz-ho~yJ7D2jgbvVv#b!|z~xxK_FwZvLe zy-pu^GvC^s|7)R_wY^s1FS=<{q#m68drh)*) z#T9auahE47?&eU0z+k}LQyZ&dVsNk?;$<|Ks zXB-D|{w6W8rF^_A7cikf?6~7M6eI}Gn zc9%A7_F-?NmJmF1x;N)C$fje@j8n_?2m-2VYV`E<(~|Ch9Rm8+=krR5ZlwG3!(ViM zIU)+0)g9<`(5_fpzvuNO*|0YjLdSjk@?X6=@VLxK?A6fUXe4|X8cy1uLNKZB-47_* zXbn3%KblnUb2dPe#$1i;otpb?h^$qnIpz(e&kDLB`0gYEMu&!s!Vw%~AVmsO>V4BP zoHPR%56wy`^32TyP~7)D#btHj1WB)b;zS;+11sB zZ;Wr^vYa_aEMN8>AY>!eh=di8BJKdA9Ja?Y{*52{7)@H;#|`j0#WdlWJWR3WAjywQ zf^jS@M}0REbrKTVN3R&xm*32WDbDFmuKUJ1bl4E1X%BS|*n#ZN+I;|cFS-HrI=;`X zZr)Uo+7%TRrVx|0_p&WHnNb7jdD94@-JvnWO|Pjf<4LmFmP6q#xAuO@RRY0C5Q?^S(ND1)S#2 zZd9jJ6gwWvL^dcKTkf@)U`zW`o%MF7%qm&}+W!<*`ws_8HVF_?Z-4(_%Z#LOPD7HD zb=S!8b4v80J7*eBboWb4v>Uo#6N}Mar)_ONj4yf{>5C5eQdif>Hx2k1%GFi3(q&Uo zDhksZHxS9bR3zpkh=3vUJ0@W3a-IY+;wHE?^zhV7)xI1go$iW0my>YbX>o_8617s@ zk(3QPU}t)4dY{*(0^RlPqBMA0ySt@DL`0C(yzgn}^j@0FTgW&W_>uh1Tif@FK;VcT zq-*P`0n{(f)PQF^{`SgLHCt0dL+#`n&MFC>IL$AF>OP{m__QZt(7M>s!TFupM<1`N zy()W$Dzbgl^;W@djZRt5feB99T}bZ$U|NFV0r4_@&*U^!>kn-uvyx&&vbN4SAPAW3 z5LQeVJ_8S6t*I})A!_DFeIZ5 zeO2+1B}6jQ^@Oe7KHEC8{R)K6mxDMfKbzB;hjHE39J`0{qjh)b#`=SI4zlCX+hjC~ z*wF-~NK29_aRBIr^t`XifANy^OGubeBj$1&@l?Nf@uH`vhil>1@yNkHe}ENcc_^9% z&Y?&<26T_kaW8W)A$jW|m&W)CMo7R9nZsvsXO3j6-o*3?32j1xTuFwx=4;9Vz?>c) z9FgEO@!c9L(w0O-1pl8nX_0kP=l5R9^f+?w!% z??HE_?5@8kgFu{)$V)u}=x8i~yJFCEEN%l+;x9ib)3*K0xy#J8>PzP@CJoL}W2 zg?P<=snjEzUk&0s;0Q>4_l}@)-yj{HXARl!-gvjITg`n>4O2h+y?=S?`3dtN+1_3S zZ9_vFaVF3bc;eAgv-VXOS6*>wGiT3(v?V$D7GB@K7Ty@>%2>wJ!6i6$zo_V6cy2%W18RoM5E<9bo{_q^#=s!fjF zlX#y0QUivDAWl5$2qkj+Tebtg2*}jv-~3Zi2v(X!9m5<;YYC~~DgZPh!Fd`~2I&vZ zh8Ybi2*e^Ijd<&dWD^4|ZB025r9JQa&#zz43Lw-T(~!2U6Gudo^oAtko@Sr$BA?@* zsu=cc&qKvRL)6j&@U_INQa(9AplN0#z}G5T>u_p1jRd9E^=kwj2x5bJ8;0{wc21!jW?(AY!L2IaK|k-G?s_0EG_YQu%3>F zj0FYo3ecCOViO+5Z#&ybY*6H z9h9$M@4-#k`|)$0;K2mqUzNviH+%>kKYplg_#lc0P8ueEZsvW@2ubauZC8mQ{9UF; z&nqkY%uFBsBya<^QmGglz>x@|k+ki7!l~@R4qHfit!_!D)xPbITW7`eq>5UG#UmSl z5f&6gER$cd(i zhdv0vp!W+|CD%{?{k}2XX%>Uz5GmK?kek$y-ys!k*EQ{>V=8x#vFuZ{uo%I|&LYYg zzuSA{(Evh0pol{6Kx@2TG15C zMuj*=C-$cREI=M$MWFY6qE!yp0y~9Ao5h<_++`;2DZ4$WY=Jt|H#RnRO0!U|jo*>z zus{bHPh4DEcZ_DJX=rTRjkg<5O4sQ%XyuQ<1AOek%N~2wLbtFOT^y#{F zjZ~4-r&EaL9P9KV7`g|Y9D?2aj9tP80*y0u#F;Ksw2e6w_IC2^Yr+V%g-Vkacu(Q2 zT)lXfkKL1~{$BFb?(0K|;M7}0asLBW7H1luWH6xti0tU6s4kx4yPZr&57VNcV8ZTT zbP|Ad&?DeenAeYzi&c3@G%oV)JEG;+U*WTV`s109yTZQ4*fCP*tphrFW_1UfONLh^ znISb!v!f$pB@fxdlMm)ok(&eytOjI>N06)sJPAkjcqDnQ7%C**EB-LJJLc-Sg#`ia zp~*?gr{$SE1`hW2NMtel#}^ez0v-M_51WYxdreG~OSuN`Y}pPr*o>EG5?h?c zIx-ytkTiDJ-ah2rLycM9z*zIBv+Q>k|6YGq859;a-@UQoo}QdMwd6AG5Q~m(j3Ic? z$%*Vk^wH|p&AH>8^c#eX(eA4a|Jbp}acax{S~3p8Gm_g9Nh)2St|B5L5SA4Y8F^)Y z;I=LQ#WdXq|LE%=r)gzlb0zhthzMpgV`)%y0SfU4n3?sL|fbRuMDRD;mjP!+K z&c*}Lv)S_Qijmjp`fB7gaS2+zW4vJWY%1Tav>SKiQaMADm5}y#*+*_;P+K*;c&pqx z8$(!>k2etkQnK#>2Z5KL_1e@*q(o9a1zBteRvFEZ%(iMVQY=kvg(zFNaEwG2!Q^;n z`ykVUO;mXe4aP{&!j!l6<(OTIUns{wI88^v_zf9pWCM5l?Ked8UAuO{XE~m*KAInP!2x9rSOoVVb#qgW#lk)4@r=!1KaUQFhwY|tt7D!}XLa9|t40gU} z$K2l1BKt9d_^nAlK_^M=2y{HAN%Yc%$~PDXj*Wup?NHGw@cx!CfCH%V{lu|jnkGRY z@G&DX?eW4bxy%x{w2OYo^1|%fzRdVsYviP&0Kqqk3*d4LUvn)glYcflv`i8x!vH`v&u;tXPspQl z6?b-aK7amvKO4?PM|XGk&z}vpKmNtH(6QkIWR542vPnRprHWa=B--a>FvYhxYPivb z>1ko&+Po!!XinlR937H)uQA3`lz?;4Bh4JQhOx>j8}A4WK9tWw3p*7?)zb{#=Tn_L zdbDrOnQrPk{xfhwfe1q#of_NM1+_X?uYRwnkTWymxvZe9qEZN3WA0O*sY}s!Mnz}< zhmh^e^}4(mZk2Nu1pU|=D(?C2i_COf5|!Nw9@7DPwr{Od+@iBXaQs4aK$67Ww;XvM z>~bz<7u5=Jc`Ouh6yN!1+_Vyu_#KtN>CuXL|I-{(>Q!@I*B855sz26#pWFXQo zK=N4JWp!-sebO?a!051MDaB@R^TQiTh+h5j#dy55kMHFL~w%XcZE;jfV(WV(BX!&e-IRC#`1n+@c7rtwe5mXQq9`r~XwYaX243t+V#~Esw>4A~QplZecd>W^V9-44pYQTVS0+ns zZr!@hk_0g(hNSjU|0(Ku=VIh_p;TP!EE9=wU;o%LtVJ84PF~qAUr|wsNC=W!g}PMd zT-;aER+OVh-4}ayJT9XWJsA_EVc@`YLq0H z-wO3_f82xZFPbq*|3ECcA3LvgLgNDjI;9ORXDO%Yep0_F=Fv|(Bk*n48>!~*7?){u zJU0&Yf)m3lg`!M=_pP^_?8&?kwGTiF+SDWe!>~1oeiQL$sX7x=j_~~Ho+2PDFxN8N z#WP2K>?S+a(8vg3SXDbEx2WiKu_naKfMS7p0Ot8JZ38*mHW_b;L${#~!i|EiXO?z; zvOAq<4zU_s&++4F0PhBuF00wZ6H3sz3_8s>WJj=6-GT!^FL@2J-b$JXm8y~ncx~h9 zvSMDojOFmRbS<*ojJVGT^C*k0?xQB}f%lDgE(^CNE|H5|57G3s+?YhD&^j0qG=a!b z?qv@mxaHt143c)b9gjp3RckQg{qe5K8JE{HoZ)^$NEu`pe@`M{GO-0GxZCvTWLxkN zNPuvi)8B3aT11MaBDBQ?94*+YLAtI;)8nC6=w)8zp@*1`*0TdFR`oUMgCcmI&!0Vd zf1<-(#asvo3rkz*Ly0x@K{MePQ8<+e1>+X2yfC&9FBv_yg9-ZDJmxx(=W5{Nh{O&` zCQRo?kOdqWIkU0`q660!65G06W-b7kB0LaM3Ch9VH}CtkrRubIYjrs3$FCa0jxgv{ zk7=FE@c`n&BFQ~fW(_%PnTu1(`;H;?VNMH5^NLzU?r8fyD$JI=QMdPt#-b+ApO$&w2X4zV&Y8tH6!u$jMP$*L zY_qt7OQw_NX}L`B$u#Yhf&t|U!e8tIWZoAT5P)4iFnCIInP~Pa%l8jlD$?0t)TA}3 zx4d?P+Qs?pY`%6%sOx2g)kdFUfh7~-gR^UFnbvn}zw%?@fYA}43TRbdy#iSRaHCZK zHh@K!5>hN>z2O&tTyARxO@miuI@M`x^DtX`uyZbt%#9%FJNqu}sRdt0Yek+c--uoK z3##)ybli z`2_^9_AcqR@|f|-Q&B<+N9k@lJL?tqypk9FfN7bHnYjpzGX|KA)tHT$n2n{Fwic{L z!rm_gSB7)?EX&j-*79lGWdJjVpo&f68FuXUk?aF|wgVrQbPWmm#P`sTD1YQQm@d1I zfNz_#=wG10Aj|NAR9Gb;PL;7zX&={k&CD~7tncH~{}DQTp1GB}>nmvLNAJ3m{rWXm z_KLP4c}#R)#OtSDM`Ii5`<&cDxhnsA+1aVqVLRf%sD&%}8&u7XmEa;cb@I96z0%X84=VuQt>pkzvgpI?2B>j!gN{q;2| z3o0}E&5+!FT~>BFEmb>ZHJJ59nGuz|ExN<`qJGbpa{vwxGBIV*BUroQ<L2pHgA?WE)KOTLU|Zy$ksO1YDu z$SGM)RYWs1JiDsPW$SoMGd%-U4oz-rtJXJ*bBQgO<3=LQfLR^CiU$nFd46|kxaHEc zuR>DB77Q;cI1PlKqlLjKWTafi$V`-y*VMWqrEdlV+pTm1hX{F`U|1orrruB6{tYT> zERv$L)$3CDRI;^gc%WXd;qp})QY`+%T;i`^*7(%3snKxZF@~wUJ>gnl0mVA*0h3hq z#K(Poebcf8CtX}zJ$H1Z6a}KCZW86j1Tf*rE;VTIiXT0Cm~xd?Ku-3q9j-HT1z)%( z9VP3&_&>RFU^l@rT9}vjx6`iyJ1jiQI&ox$_UoQZ#P{tY056835WQA$H-bYRO!dku z2dQ!gcTc(_WfIHKMjKP<_iVqV7Xkaqjg|DvI7l0$&7R#QQ4tZ4`jrI)1nfO{xVZV- zbl@tmC&3Hig%{2vc5J z6LcjSiBv?hZTY03CVYc3%M*_{^{};^)4>X#b z_0&^e7aOEYW4|2!S!R<3YCgti@yTeDY1^x@ZM<221ew^v*Mc@$V%FsvLQDpK5-FP5 z*8e^YW!WW=^Tcl#k%9h7pO9l{8K>yx&!zP=vJ3EDi( zOtAqj{w(ILk3L~yXIBoYH1>J1FI)pf0Ul*MCCE5N?S_$#k^cMQzs3qn383nRUYpcL zb(0M3KZ~4#&xws(0nNcs_-PH^!!d59OwizcK%xAigxX-x9{L*RO=*iU95DQwl(yi8 z4>sLG-IB^g<2v)l+el{M(482&0F7+jF%9}WB%lJ+9{n15SVUOR;ihp zwl{D7K{_x*S)(FX_k2^$dOD1*{P>x7ri}5a*^;t>(_|(ghVk^Z)66vk+(Pg8Xc$al z6~m>qbG&Z)vheSc7OD&S=snAH@dXX;+7Lod#tZ+zK*f3*T5_4F_?q+MeBTl{CTTD6 zeY-MQ(Q`*mpgF&>e_#v+|G3(Fb{dAh1w(D>1}yFPv~d>?ar9vPJ@>Z)vv|>OB9Uk$ z%D4x!dw_%~I8I77DM8=fWy2dh-L4BP)wbR48^y*Smzg~*zKSAi!`t>Vbzw2308=s^ zd;su>DJ<`K86Ay>28|UkOaw(=Pyf$qUzt$`q@L&_5!Vls>!1^&j=;n6ZZ`7rty6n~W5UMH%nXh4}!j`%=3v7V^RoOW`r+Du)Vw|LxhvES) z_zUq12`NPMzRgjH!^W#m0=|0Zs;w`Y>Qq<*@y+_Y70cE1B(v?djBwYC9zUijup;Ud4SyY5J9)itt{GT0R(kV zJ{LPkmuQWuq!su=C}>(Top*1w;DI1$uX*C_!}xp&%b5f^_h7b@DU=978j{lvN|>Xp zO**wjz@5Q1YHG^X!TENkPMF`7o&k1c8dp<-t4@+sBJQY!U6%B^Y1;ELMxM%iuYGi~Y~K|4s3=fPz~mw7 z5;^e=ypgY7Iibl8Q_c3%w$&^fozuR28M>!agMdcu$BbFw+D=aP*=J{OPrDXxxNZ2E zF=ye$CVPtnwPY{Jrh|Z#vMhONk&p(ruf`Ag7$imNI(bH-dqk&}t{hl)BTlFZs`%-Z zHM}XjrXPa$PXBW3^GADbvAA7D$w3vJ1z8I4NUV&ZD>n|}1a*qd8X}CXplAHOXfkz7 zxR^%cSA|ZV-~KFrKX)|syG6xX_(6Gn>zJNB=e)HRT+OcAduxKnmjEsE)*I*^;)xYH zmx^BxI#r$q+y3q#C3h=c4sK;$8QV*Hp79ysnGSdHolnAc(iNV{*W zbmIUPU5W0ra=`yOA=u?cyW~&Z!t%H*w#-NZ;KM1F7tJiXtdq-(2M-qFul@ebe$VOG z*uZ}3ph`;lzob1r>`PFa!j0c)1>I#HmogJ8s}vT1Xlg=3G=nh*9D%wzWtuJ)HOy^B@08c4K1Y9SsT&HW6pS5IPJf@hv+ASZ+~=l=ji`9+5&s zSf0C3J4B6Y19}))p$DBVJ^Oa+&R$p)@M+yJGczo7NXyH+kLDGKRiyX@YVEBgbq5(4 zCy*QOj`q{EyD*bM`@kk~haHT}XT0*5CLXpr5J7O=g!2M@GJ>7bH<_WMt2I z(I?^hnC1f0W_6sjRTVWKQ;)Dgk&%fpN+BN1sw`T($^KC33*7MfEPd}@rysWk=LYnw zUj+;D;I~9l73LF6(k2U6l%C*c!g5H;e?~+o6~)&K#$&DqgNJq5pf5u|RV(G&@ZUc* z3kD@DdKmjGed)@TJjk*&ctaBUwkUaEGH79gs$|%HqAPb4DM<1B^H6O0(s&~@-c$^X zWMwswuMVpZkQl3$kn(fDU#`i-Cp8Vu&@nL7G2j)0WQtUMJI8haNB^Yzx(hBhG;z4u zP#BPSN<;hT->6@OXBls&wGMRG+p8gdkGCiV{F4fRk;Tz~gal11k^OQ`$uF%B1_lM? z!}h?9!6)WOs8M96azs=Wxr|QMV*y52H#)V@ZkvOXU;efiYaaYq;?=f9~7! ztSso?$ldVdTpT_}IOH{^>8fpE*QBj1T^$f+u0|&WX=GM>{NuDzeB^WKqGZn{pj5|I z9@TJjKY3hgD&kN6Ax~d-nu#!u4G(8_T5VEsr>H3KLsiw)Qx=e~x_j3yo#ZOOG!QA^ zmH?VL@P*DPKQua8E&h&V6PAp?oh->$_OhsM25An+I->HzNnDtpH?|TH62@6D@&~RF z&<91`@Z0Xmz%>D@T)kk|8dPj4DIsN=HE5+z2JDxAaU_n?lbymk4~uu~m?2X^uE`b0ARFVn@3%H~KJd+aQr-ht26$7kLHF0Mq#&CF=Bg^V_JO&C zj>UH{|7#QX(m%}PBJjfT<l1wF6j1B1=$=K z+W`cU8dsYODqN+3ET_X-`?lzqJUScu{=HgC+r=fh%3@!+ADgmts6GJBy*g^vQH(DH zlbU1`v~H)353P8=Ic>i5_isj<#bw%j!xO1>cd==D615$YVS6X9Hd#^(NXB-OELS;5 zNI(Bmn9C-4Nb;E}pAmYPi>@s@D~naq@zWQcDuWpQRv6mT+f}HU1XeO_x}}tZFsD03 z2kP`c4xP~GzK5v#d)C4V|DG+>GWV4{ciEFGjSW9@C>KMN~6cI<%D|84syxYdsD-=B%>`NpgOg$|i^3i@s9 zJOPD0sD!O%ugD@lW1m@bSJ#&r7ie6$1R@&#AYM42519Jlw@mMF@RWn>fwUtEAA$NO zPAHZe3sw+hvcV~v$6v=7veVJd_SGi9f&PfOdheb+Mml?Fc5L2#->~Qd;?+0Z+Fx3O zE-{qxkYKAmu?};XM%C5P;@1QDX&T%wKLt$q2tPleU|q2GXax zI+giHR$De9awTs(91`J*lJVP=IOMlx!k2S#Xt!&Hc4vh6;uNV`*&OEl7-AR$)ze_HU&az`EfvFhqMAQc``>+s*bVUa;pX{AkYP}~jl!O4FWi4&F zlMgx`Zk8DQSU~=G{2)Xu=#M}}2W5#e9}=|)$pVm~x_nZTiVL?*IuNPM{v?N zg`{mNLO>{ILCXiy8DhMcCdCT{fa#OFWswRFOg!)-7LEH7V3%UJorhE4d%--#u|O@I zaqqKCyLm?Xt0&CqSfu~=V|~%%;vIKC}>C`mM5a&}{h6U;CAS%suXcF_l;|s|tzIUvp zbjN<8_-7)5+YcTfN$bc`Mbq04W;S&Ih>(omPiEN*C=$zz;R=7UfaOnS95dkoHIgmm%vxMAg&BwnMuyx-TEG^I@T((1_fV z+OF|nV{HNUbv=Fk*JE2d$^ZAE;xJwWh8ZM3h%{3ii#CfR!ouOzHiCOh{)=`|y8W}h zscB_x9gQDOALeRwKSt3VURZbW@4h0-Sb|X+LfP!jA`jBe2R!4k1{3-f)_?l@k)i|L zT%}_dayIpa7%r&AH_!WWu=$|rZTixQ{gP@3G2F9tiMZ6u5DJeC!2~8AKeGg`bG(otbJ%M`1-Q8V;(R2YB>ae)L(<%HB zjDoI?j#9iTBAl74p)f|235p7YWM|o_ZkMBDmZ3ubk+E-zF*LiWv2lGUK2Rrou(#K^ z!sM@=Y%d4i)~6~2m!RL4k$F_KeyG|Oq7)#6A*zm$M}1NGk!hj`Zwf@OFHa#C7Ylo= zj-xL}E_{h?pBENVITzd8@X{hq4KB90NDp?BagJoe+xYubx@G}Rp?QFEJhOatPb(4m zHRpWhvxwupP0W+bsGQ{C^ZcVLFBj8PZTtHB_{7+l=}ZOgfGXP`42CXo9kS>xLMNVY z-hLSTK+=de&jCG-uzK4vjJj%AYmDEp!u^9@!G{8%XQbM?$vBXm6+;9nCa>eA&!8BD zK*oJFa6@KFXA_XcTly_w4bR`?g1VX-)`shqpbo(p7^vr1nWV_J%M;oma+&At)rLE? z8l^DFN=wJ|pd#+}(5X>3Uft?ffcArU-^iRZeqkbo6`igS;ZS zYW!|YxOjyGdqrjCT7;}OMV&rz;snEOvLn>A17n&})Mxj`Z%*Pnd*+-QZe|lKI4*o6 z@XurE;cW{G0@H%{8P&``$ispSD|9IT?%=-AAzqx6r{%c2C7aMVe{iXFthYLj)EYpE zayj?3U&h2tj*glPjNQ^nMFPbX<3py+PL+(N@@6*aD3owA?g53m)8o^-j=Rix?i+3a zeWaLiZm$>6fk6516Y&3OL|$_$=Ws@sQ*3{%5KkPO0c2Gvn|d-N+kGc0GYnY%KM@6P zwt8(Cs{z1|$jFqruh?uHxxOPm42cRzO>28xueFUQ+F?X=eihjFrZkgzQzb}!`^rC@ zh{l8CJ*ryWQ5rrLK)YWUHoGb zUJIm{QF@(=LplQDqblPW=;@s$Su4Llrq=k_W$|wur!tA^A9P2-QThQOXVI(?4mpZs#9# z#v>hkO!xXyJUzEX4^lEqKxfGjb(!i3a_Ah6a&eIwci#rO!?)@Ij5N)Z9$7BkSlDRDPb~<*>Gf3V-*>fJ>>8 zQc|paue-+`Ea@}TXeZ0PSK$8hY_qs#OHr}!YVm%+o4{Qu8ElWqP(kgvdYE0>=w$RD zNbW_Z=0>`W-T3v83irM^%^YB!ZYnn&-Z;51Bgr-;^1p-53(Ih}cU3-Gy`dpNua0kbzR*97ihIK6qzgu|HMV4nRPXaUcLDirY2l-w7b!g0KHE- zYH;~-8)@1`W@&QXWv#5t@8hJFeQga;IQmWm<-qY%(-)eKy8<0(_pe_DkXvDGkR5iBd@%MPauesD8X2) z-fCh<^!jKnp(6THA?}%r&rfT^ZzoHaqYl`HsQUkEvwAWPwj20==83o-;kiaYrEM8L z#a&3@lhf3qG|&_x7+;W~f+Vo8)_`qOl)H8Ya$W;jY}AnYXiqlA{kN=Mvx5Y|P! zSR;Q;?L#*%*Q%HH4}PQG%AeB4bmR->Oy5iD4r(zOgHsz&@-gjFxp3jOxjC|C&kvOs z&lJ`do0G@oeVpoDk`Pkl6fZeG+(AJ2OW#p2kUh8F#qkI71|Kd<-WVA91!3 zm_8U^+{VWrY&N2>r?wZEgyE`eiaf^Tc5fUd1Qn*^} z;}B=)5i=f7yf8gp!Z5fEHP=zy#BDYQeya-^i%+7aTI`OWEl7QC zb>nVrRdw~P^_QzrRW~Kyb-Rfd2m~H%LvL+Y{<$Mld-c$PYm3-RIDI?b(^SyTB)w== zR&Fa%oas0|t{F*xw6tE{l*OEyw0%oYjD<*8^QSJ8{qlRZmjkUJME61>&Q0+di!a)^ zriBel1Px<+Lsfz}g*_BuP9S+na?jXlH{yJ=4ve)kuS_E64;AU=%*~23jWcRZV>O$r*vCT#)ZO!{sA#@x}7?v1tX z;Xqtxutcn^KvxD;(f?^Pt!->b2~k7jqSzBXV7E3}x<1;aU}$Hh11l2AF@(+!(DF`a zL3Z?Ey-v|s^` zgxNEJ#-GHlf&0QyVYR!RNWd*1IW&mo;s&K|Q3HPPM`jU^GL`Af#1ri1|6j%p5P`Z7 zCe-WCk^jEb&_S3pzuEfA*UvdYh1KY_E;6xz7V-QC0XGC-@)|@Ww3XXJIsN9?yB$Ng zGs9gJLYQmN%IAGA{^GE2nqv`9(aK8qMs!RJh%eqD4q7iC!XiG{O>9Yjb2TwUQsq4v z>=eMp}nz0b5<#re7WaES+6W`1j(q- zuN?B_psPr9PrfQ1_l4Y-=2tVJf~6VK6ZR2N8uFNThis;DSlh~uicxp(%QW}(HlcYf(f6TA$WzkO{ra}M)t+^F1$C%vQ$&H@qX6DI~2R?z;N zbIPQ&0l>udi`9F+9LJvDpgAFUTtea{z@wNL*k#c!SRVWECvcmn)z1PLGeE6qRQDq= zD0;9@^;iOH1GPH#&1`Lx?`Mz~BpMhPyzjV*wM}Vh#rH<(uU^#BN@%md>@;)}F&FUs zc3cM@rEA-PnR#|Sk>}aaBotS;-EKA#;1xgu1d5J?;jWu@c6PO4WPlF|yjgr^B>ipi zODj)!`616#2{O&q!yc^n=P^PmDpmuC$&!i&Cxg+iHKSzt5hgOL(nm$Y= zK%}q~ zz#1ZgAO7{!=6&c3uGJ6coN6z;}YjB)$w#6~yD%3#gZN953(c z)zPc=V5-{A)D6U08`-`U_BwYcyK`*H@x5VbNx!`RS$3oB&LlrysF`Vdl%8nt%8l<0 z0D?N@sB{0nIY#zP%#Munb^B;;re*~}5mH3pJ!70F>ItR+ zU-|<{RaKl=pWqf zxjA}aT0~(KF13oIxIprbI)`$EneFjBH@)35M{HAKt~^xs|4{bk;Z(J4`0#F)C`nS8 zLJ|^^p^$`75lP5Ag_KA#g%X9rCR0L)5;8W(Sdl525>jL)BxDx9bM5VUzu$Kp-#_2; z$NN6VlkBzEy4QVQ*Lj@-G==cpL&HSyLl>`O#iaaDgh6B2h2hcBCA1sQ+PgjP8vG*` z0!iD?CVxyi?(sj!TtJHh!ck~wq^%9CB<%6JTL*U$LC=`+Ce2m`tPs+*hh0Ns%R^3@ zUk#qWs%{gEkSTB4q8u~yHNUo>L3Q5m0z<-Iw*rHL&h*lGzmrSaoG6pQ;uiqHV`BX? z2|sOBvaIOvAFe(b+FQo2+qktwbiY=mb(!AVJ?we(#xp1w0amkabPXKt1f6?fv-MTi zv-AOrN7kju3KG|=a||Dv!1qxy!XOJ?Q}9=W-Z}o!zhnrwkf-~MFQMSKbWCd_2Nc!v zqOaTbZK^~AVO9{6ur+{#g9DFVP#&H~;|D>Kthah{S01L}Pb6ez)x0gg$Vh!VR=rc& z(a{lhGF6EWA~Q}kD9}|V(BIl?c%HvVeSbi`l~(SS(~raU7KIg`#-L&uM=;g6IePH> zu4^tYo<9d=4wDh&Punp=J`PTb#E$!*{%d@?VY=NFSB!^}vG9WE1jhf$W7izLF1Ri6 zbbQw-9Sx1Kr$_$z^>Y1g&FP0C07*Ls`GVfSZm86oW z!0lUtrwE9SZ?i_x>Gs~d#KcwW{Ye=#%XJdS4K`P<2p>H75`*NbRta(O&!0ZQ^4?5` z?&sFq)Scy@*4eXw4(l^ppraU_o(JGo)w-&nii%XHg=R2kMFZIc`+*j94zkozimMrdA+P^fu|Cjs}d?!^7agkd1 zsK{1H)_X9tnupxn+y+N|6pt(|9Qc=YnR`78c~wg!^<%F*&}nIDd9xUVmHoB;ru)`h8}Kov zBZY6EBESD1Ul-50jV-BpDD(!C07$eG2zBO}=lj{@ zQ)K@C?~W25L0z4v&;ak5XCN8VQy)Z&`+|q(VuCpb4JnOz`A2-avM8Jv5e17(5ds>d zUW_fTAiJ3azGTRa)1*v?Ygaczh z;IRF7)aW>n!yx_}PHcO;d|{_C*K7o^3i&gpc=6HL89>ZHy>z$Sl)C4YtZiy(;j?>% zi@f7ZCtd_Y^w}Czcd!lP9lat>Nyorw0YnZ$-|1p$G^p~+!OWU~Y0 z)(o;rT>g6Y2xh2CBy8Gom&TCfkXbCE@QP>oyX{C2ZE?i6w|-1_%zTWRB$8Rbi{Jay zowZd=6IGn>1R}m@GzmYbj~`TzIpnh|+xan#EdFarpYWkW#Lg@(t|YN@=T51>W5#QT zju|c-Py;DlpoL#IL?@Ypx|z5R3=M%<=JZGaq%|?qLN^7AU_s&r6m}SDFZDV)(8Mk0RKx-dTnM3WmZfw zp4iUDb^{}^#9oP&CA`bvgbYZGxPzs6PZY~R)&DY1Jo)PBQFuY@+N zVcbI|GB@9C^u>>>?0=?mzFr_Vf(f>;HlKAD<=rHeydP7s1y0+ec=YIs*1i**41e+! zC!V6DF0+18&BR4vypaSS)mU=7sZ1Om9h=ySL%VrscLscCRn-DKB<^lqd66Jl0NLrJ zTYC)dhXY(9f!)X)+#OmeA8{YP&NqjjmfsQMWLW+&jhVvgV~R@qZ)fS6P^LK8LBDfS zuZL7h&r8f;NuOSTa^-mcgaT`MnHvp>VXFe=QMQt09Z15drT^9i^iJ6^-=(ZK(w~oT z9(w?G{iuduT~ALB!ezQ-{FiRNr2a9<-kXwogJw=*`ETGqQU7ph1Y&Q9yZop5NXBJH zDF?<3$NC>fH-dady%dsMHUc!p*yVGVzX4~8bD?3Gz!^W;x0SlDuw9Tx5q>Z;ZWpX!*&= znV$GA{on8M+ZR81*y*rKb@s6Ik-+k96J2rU#I-t)6>7~WcOsYvcEt->{1|=i+jr?} z5b9n~_nzm$nXd5V&GLJ#w!Fn&`a&FRZ2w?4sqz@PU{KUP>Ykp5wPl&Ozw(I?O3m3|F^a0Vc($ zlgSZFcr)%M@GUB`#MMf8=2;Iy7!aDF3sRw*U^N1SeN7j4m=v)K z6yB5bU?wa7WoBRVb25bczfWWcC_~+?e(DtTT)U#4z?Ev$2>Tk^As;sPsbO8QcrK(( zWXth{R#x;%hpBFL3RK|od&DO>WQ{62PJpGVd+c`1qnH>2BO{I|n0v#%b@t{_%K zd?h?1T1RQlD382rLtiE4F2VD5XUwKdb8Ox-(n>@?z)`L~5z1;+X5=Cdpp==O{|8U? zp3uyYKqMljdu?{elYSVD3-mEiqg8lvi^-C&G|Q|UxgRXPevh=ayXsxMD4_d`m#l0V z=eN|?#>T6lGrbA4y4mgrH!l2RdW=7e7}+7ZTySSwt0Vck8(66N zTc6ZL?un>|FfuGU8b(i_5z~^VmdFP_%K*d%{9&r`r{Czm$UOWsDQT=f)D7gvIQVPs zPg8s`_^sIU$dX=)*3{%Anr-CSR%q|;`D}8X)2`Cz>XI!1iv%7EG-6~^e={tXwqMa0F!x0%G^Qn)VcVZ_J` zSVvC>0i5FGtlA6%0HLY_id-6KnL9@N-FQBGClA(bs8r8_ss&64htu(=gFlHMIB*p{ zsCCC-xB!QKpb9|y>gfb?3)e*{Z=Yl4EYcS`H$VrkU|#CpmUIR_=MQoCegyoctoO&Y z&}8-(hB-ayYEKib4^_Q{?RmOUu?K7*JP!*C|3Me2Wi%t=C-`H?LM6Tzav3-Lj$nBkLsB)* zWaV+>KSpdxUZqa3h0aW1=3p$E|DBJ#)k4GC+H>wWFDt(nzzzwM4aKE>Sc#AnJ&8%g zZCmT@eqpv53&0%s^q^=vNc8}H9sz~g zzTG}gA7}HzENl6tZ@JcsCVzIukS%w)vxOu)%N$uT81Kq`*~=+j+;=7Y~5Xzo9^$J@4&XSNsj0?Z93U z)#37yldOz2kM~1S)5sba63i*TIR%(qaA9a%`?DEuyxu{TOJ$gtD#siX=2X;>{!+oLlD*n9Gyx-4)$1 z=txvOF2vjj?h-F_b3hIWE!~HNb;ix>t--Xz3$^hMx7atoGSi32c<}`zs#P=uu$_+w zU>v=d{WqL{5)$Nj7|9*`9BW#hpG^hPHDUkU3WaW1UL1#g!Gk@uXA(D_umA-KU|al| zOA)(H>aWHUc=Em_4mUFf33*nu)4GOG|AX;B6x+fcG2CYQJ#rwvbG1Kc91vWx#!vk8!{1a$5Y{<~h zLnt|1N&;HBT83-cyl-a{P%kb7T z##Y-`#=^>)R-V}F>VW`QJ$$8?9)Do#v0`xEL2Vh}U@%^f9TI7%d3_Q>)OsV>mW!Kq z|M<+s{-3((Utw=0dLR>Y&%8XTDo;n4MI*ux@dcw56%~G4_H1N3r0|gSbC6i)UCYJ; zvMN=*4x(9@iPVYJ9NFwry`+LoI83z{+3f#3JfW;?`7u*wZIUtx`#ri3a8{{#YkV?E zv(V`gsU8aCdzL5Ux`3D77?A>-D1iM7<2wovpe*i?=Z;>{;x16p_|q673{KA*Ev8mT zf6N9|4o;a!Vq{VTkRQU=>v#w{bnAxzAp*0@A6J@#i*hi!2EG^P#fHpv!a-5Bec7)agi33#|HIhWS8XHa+bn%eFypTkz9%1l zmp2S)9-)<51_t|>1B}{E2{F(FCxk-q*S8(;222=cxxqN@yh2Au7o5DL_O!9{O75#y zzJ`-CV`k$D8bYL;wvl(nS7Lpxk}{pC1(6maF9CTDY=*6(fdM2_rKVYBrtpE@aKgeV z|3=gEv!YS=PM#t*FOYJ!Z|{s|#V$ulU{-zm35adI7-HJew1+O1W0r0G5S-z@^Cg?( z=izjzR%oc+D5kq=OWx^yjvT@cqSm~GBXAsdL^afvYEN-B=<0#>nz8uWO4lrH#5oF} zN9+{#YlX(~CqOTsJTWJ(mSNR?f`l4!tJ0jyvmJKIG>)xIl&)i?MK~@;=TR zu~SGq*=zfjd+^%P%K}Wa;E^q10KoJ?*t~w5=OaN<04qx#B0zvQEm+6jmfz)f>#D)d z-pcr(lWKi;j&BsDLo8@nap0qmvpB(Ed_#U(H|S`d&Nh1~Ox2>NQZ-35_xNref?J%q zBmkoco_24&H-M^c%cucrC|K(~MU1F}d7$-BX74_yj`feo$%ccmNAA1BoTSR0wK(RS zd525;&!3M^^U<}PzxSmokmE=@T#)tkPs%>Lm~Y zAV{@@Uo3@{YJw*#DN?Z@n-iBQXN&6U$J=WG1t4?rOpyAYcLwWi-@Bs5N9!4)K^siP%W?89R|o7gg~ z`cWx#3K)siSVC){VaOX!`RtR#L(pN=u@0_oU#XrAN)lDOKl8C7)xMAbMTIce8X9} z;(ac%)uK!*$#^}r6%*Y2QV$-xXjQso7`#|uW8iN&J<;pe{l+cZ%0HIjXCvOf-epM7 zQ%|Ygj8(?25?L)>GR$5Q!Eg4BMo3Z4F_Mmr&C4D1r7OHjs1Rt9Z(abYD^ZR5J+(zVUJMM=v)H-id{)@S9_Z7WD;Db1&3aHV-H|2z((+Zag(D?eDBR>Ke?+4IyaMQ7P{p>*?*?XB&`) zHxvAfI|eT_Q&z8hryoINz9jJZngB_heQ&@LZUGAJkB$zcOXG%3foiA0G)XP(@Jz!V zJa7jofvjr}PqyI-*ul*{XH(QbGX#@>H7T@#_wOTH&K&y_78m@MO|xc@Vg%LC)%$n8 z-#jVRx6LozAYc4foXs*WGIl#JAoBt2nV@}xw}7qP;fwV5Em{!@|LIyKEPCKhr^S!F z-k5_SS%RsWDR8fC9_o#--Hf^!i2657CsdPg#I4W2@**V!+d0|iT^%=Xs7i_!CIq0A zNZ79I$7LpITKBM3oDZ$cb(!ePi~aj@`E??!J+Ut<@_6q)mW*{=gjeJ?dz|M!-DhW0 zwS9_W1g;Z(d2a5fISoz*=s7{Bgcv&r9zk$m29f>!XKOnCom#n>!X_*Ya`&LXFx@?u2w!E>Q>V4Bmd zEnXNuF&>^9z2}FlvpI7wfRZCi5+R=^@Gr*?NQYZsMul!4+W#H*VGz5!*c$}IK}WEJ zF@3}hclTV#)HzryGy6JrN%134Rx|axc~c=u)YtaFOFfDhFko7aQ>x}{0t{gBOJ25( zpj+C#+vm|M>qg!XF;HM?EZq|KL;4T1fgXoDAI8vCe|G)Wu{b61FnM;XDHr+KlP5jU zs%?~kT){WtneI8V;v57f@*PT-zn)}v{eg(FC;LG})A$09x0&{P%mGQ+*_u3zGU!ip zb04s}Uy>+;^&PSn8;ou{p6on9Hb3|cX3fWjLa&Y_-=`iR7l7vdHMln|XRL@gGp_x3 z0AK6s&}>3x_u;t-ay}F5KfMeT5F5EIq}+xisE*NTXKC;{`y%{(@ZC1v>2Ap~i(j`i z9#I^;!ED^?*yf6mS0GzJ*&b4k#RNHu(b8OV=0#{ZXMC<8h#}e9^(%eVu+uz{36*ee z@-Rq18vJ4I2SaWbMlAVv%n8zcql_v`Iim7e{=H@MTEiWXsNDG8&u@MHk9No;VaR*+ zMG1rT*C}=;MKqwkCYWLz1d*!w-f_*nT%lWC6crWGiGvAwm}6xn=d9(fiFv??$2vZT z@qiPYa}NC^jI@+*KCHd@WXMlPFxh$z?Lnw&68Y+W5HJv9iHif$X#Fr;na3ok((W?z zR1?f#u?JoSDT@Lqcc~-$m(CZpf;L0Z`b9hhFn&}Xv9Pm$uBv)??ZgW{J2x0oijIxK z^&=-%&i}-M|I5=(`Tu}t@w|3NYL0nI+k(-laN8RoTGZc5#{rou-Z^%aX|MlV$3e!~ z7f=?X4+1TyrSl3(ZQt0qkPrtzJD>vulp#j)prmAlrSeC7g{#VJHdxR56IKIA7bj}E_oNi+5E`R z$13y9&Db1NexTuN5WAGq+Bdqt0hWNmpVF>w;NEVut5rRt8M3m_nd7d)<39OdiQ+1u z20%%gX%nl=)0IJcKw4T~UmsL%oWI+2?H*k$7F|Z`!+ii$06!>oW6IPF=ADT@$-CuL zBM)QQ^_jTs%z(Jqz{Q~!&e2xe@+D7XGO~W`rO-LRD zR5-S06ktPfamS$J=SdoLGsVSA2phHg`%|!5Tukf{aKaAuxAE~upEi!=sqru(Zeix1 z2fQDm5Z!1d8;7zwq^f=8a;3Oo;LnN^z80-@2aS6wV8RP}7W{#>`$k=Rx5B4+QB-+Ww2E7BAKm`Tgt;9s> z!2ONYZS%kujQ$llFcLP6>P{(B^Z(!hcMWPa_%lM7kJq+EtQt-xnoNpwP6{7vUiwZ2 zyPt`q3L^w*48if&Ab|S~JUNg$p)sGH*A4G&tcRS#>fwuL&mQyr!YPes((B-3N#~~^ zudPxqW}vA#jy)tqT_x8{cM5kD?=O;q+!M>7PCywPMv*FqPv!h14Xg%$G{whyii2dugF|spqK`Xh}Y5 zH+b?_eu1Ic%a>qdHhU;?G9Z>cAGz4kfjvtF0DMSZz43{O$5NY6u?Ob@Dy&G8_cg)) z&vc{1OqphF=Xf1v4bB{kHfd>#AJq)XYOYvE*z9Ue;xJ+#+u{ERN$Zs#e0Pn=^zB$rnShiyAgcy2P{HvPD%GRY8+_i^NkjdMYp`PH zR&IzqqRgP06k;BdDXoM=F0vin`8Z}?7CQDrj(q#}r6(qu9aW#Qgc1T*oDzh74 z`^xZb-rw4-VnO%e_Ton1Rv>;zi%Q+BeZjNkGcEN!CTweANra6iL>so4E}hf!*AB5& z?lf_^$C)DNINz;Hep@H)hij_Uv*^N4Lc&CBO=}Z90@o0g>NWX?$;S>f;O;4J6%y46 zsZ(qeTe=708Y8U_+I-Y0H=H2+@b78Y^)xp!XwGz$RA@C^rNLF@z{DqP51v{UIX(uy|Qzwe(&H!M6< zr9Hoa+PGxigLuH?gD+Ulqc=$f2IA3(RZ4mOTs5@`9j^1HJ+XDv-fH`<8{6>CEw@*% z-w#bA)6$Jp1%4coI`N1O$KFz^X_MA)grG{ODo zpj03^!+p6aS65$ulj{l}r6neXZ}a$Viq4Kles<@R9B9%YZ_cuu{1xTnp)G*YB~1LMYWdrZM#;SL`MFh$4M)mF+; zdq$~mnASk(-z`JyC1IA+3Zf6R!UjUy#&sTKo)yD0D{)X*d{h=&AIz7z;_t0^iU=w5 zBw9_L)eYV3JLUFMhj}HUVepj4I16jaSk^@5H)UBraIT|j_GYmzz@2;lT~WtZIcOey zZVcwJqDDJ;gqLsG{!a$CdvP~Wo4{yipq{%YOOe{R=oG~oc%Sf}@=H<5$C$q~e zrQ2M;7t2skSCZgGXe0+H<)3kHmn0F;Au?(|>b{%zwW{L)b!$um#Q`e|Z@h8+MnM^9 zMTA*a6X5Ofv_qcRf^I)cZlX5vPXLy;pV80Mp{>-1pTc?1R_YT<808WaY4`|h>;kw; z^g5LB2LU*OY{Ew#<;5}z0}ulPL1`yDfbiJ%$WRV zw%R)6Jvt&QEqzi$3ymXfkooey|9$;xuqjtY*D#OXlnd#W@PY=lvTGu;ynEMv=xI?@ z!9!$5nwe&`7JpccB?5XN@fQ>}({-v>4-Td-W=7J~J>S=OUqP1lepfeE)#X+>Fgb_8 z3b`3`LyB51wnr$Z^^@=?NX$XUNeM!25T<$!gE}2))h_C=TA74{(r!~LNsmA5t^NyK zR^;~yR63A2xyK_V;~C7=02_g`3+faqXIOLDOR@@9!`RHOqv#(FXiSF!E6?p=6(drP zNXm4yJ_*pIos#ABouPjWio9vjirglfLIk-1Q>Ro0kPcX|Kn)0oatC=(AcR@cQd18@ zi=}$mCy4SCVqteNf8E5 z#EdAHaI}c*4~yJnHcQ23I7NbG5+QlVmau;Gk}Zj?5>_*4t{yrV7_eQF zYx(z@Ai(uq{{*NqrEZ+T!QU^#LBRzxU=Rko-1w2w6Lk``57XNwmfAr_=B-=F&f&4K z9vCSRnl&g*(S!4c*}wr93#3E(ZC+l~gL-N9TGVUh<>lwTG*G8VUbkuI06BTyww+Sr~}H; z*N1@#>MP$Eb&qfT%PUn$_^#s3c55L-LX75el-59Oq+-KA~e2*~z_!UH(9o z(J{K`LI^kD4x(>F#1YE23LCMY^3a%=k%?JsBc8xSU}$6o0jDwbDBvU;|K?Kg?v{+M zA>c#=Af#*O&xDz&J-rH$9q>3X&!*=~8#W`_IlbR>b67Uc- zrI^%XZjA6?P6lKFqIG3beEI5Ch#EVzE22@u=tJUbq<{`~j6xKe$j2`u^8*ZoJ|Q%f zjk#)1VM>3uQgX{?>H*5b0$etywM&LpClK!^a%oM&%3=M9D2&xBA@EE8aoH1^IxT(+ z7E3HEz!*%51dpQUq{9MMReS_&(vauk3RgL+m*48t@&A}1jEtTWq;~eJ&zwE$(`(Lv zLk05!NO>LdFp*+PZM%%73zj4%EQm929K`AT7M4uMk3$fqsDXkj0o$u$_-z+82P4%;;BYD-P2WFoen8Xcq_xVwzI)-OO+b*f(a|lDW(GY6E9InSC172egB} zGQW?lS$PwRf@BbDkKh(4aKYLSLtWsY=H{>>L<)gCZ)nXH5iB#T0&QnqK|1RB?RPv~ z`}kKbV+*L@pgdqLm%&6r+vVPa0%yydC=-_W(>mwQJ=Nm(k?*Xl3$C$j19S-ht1=`L z6K-DY6}oA$4>V80wZ%MfA&ib% zSMDvLrxgqm)28lNFr!gvKW$`_b(a)9t--XLtc-@1a4CfMA!CE`ZSM3Dl>xmR z%lnv@tn~(@jK|ERnDpdH@Q(x&Z7P5i91K*Q#mU}F0h(nXwlLj=cSWVSNZ!-h{?r#Q zlKC)JL?H@kHm_(1nJtd^{RntL8B8difLiUb$m;8})STfuM;Jh%Mgi#<>{O~1)Hg-d^Yax(tGwCPtwF2W(W&6q;g~{mP7SJQ1!zta28YK60F;| zldF3{jaZO^DKK@G4Y$By-{T-<{aObC>bYh}W3UyEjg9GE*oA80 z4AUgAF*KDy@WSDwAG;yjfua5FK7YIAx>^D8hM5-}vwsZ^R(|@lfHE2iAKM8QMGY(( zCdGQO82D+4ie@7=8_ilys}iw!bhotVVA`*!0T+m?>9mLcsltQgL#LIM73Vrs3*#-z zcmPi(JgQMI>}gN*(iwLxEGsJmnvB7@q4~}rU2+i{IQB1I{D!{<ZLPZ-4^QAJcNXm!oot4{eh@=AkWAabjkPlH-}y=JN+t7Gt4TYQU)CIFztq$ zWUH+ZLTaoZCOpGo`}x}WfHKiThg6;J`j%1WL&~-oI1S`S)=^IYG}xy8W>_MinR!;* zttte#xo8aH0>}(5kQ5<|39nZ4Kx{fMY}N#~&NDd)$y~<%=0iZvZd&T z8|V{8MsYfVivab&?3iuDEFHclh@ap(PD7?P6`Y;0Y47jvFTc{rue0(=`3)2#U@@e* z@@ET1$Hp3-IdcGT+V$5#eeW@2hc;_T0B1f>U!dPYd-o!o*#)VP#At_R9CH0XTgC4# zb3<=q53Sho1mmlOUZ;Feyr7Q5h?%*-7TeL(loOUynCdJyKoZEk5ry4=aRT^`sMj%F zQq<5+okSTEdGK_WV7_I|j^Yk=jW0;7L|HX_MG~AJ0H>JgB%g{Fw`;%m%HOW`@EApX zO%e?Mua8jhNn?enNWlI;NNtJq8G!aYA#gMSVL#-uKepSgehNHEp~_&wiC_9Ac991p zXYpd&lY-2EG@ClF03%x)eQzIq8es=YQg%jz&l?KL3BHpdY7W>hsU|f<+LFL_sC^{W zh-|R8du2Trp}uUo{Q3ah)%mGQ4NO_Z#c^UiExFc>Jw4_!gjX+xK(Y+NwMuxK+s5f%8cymk#Wfs(ZP9$O0WKt^f%| zq5Y=V)X&%3Kp%(YHYyj9?d%Gxs084U73KCj#cg3jfinAccbhZh9M&(tv0Ro^f^%uq zN#tm{K-2D@07*1aqhj|5_lADsMrcMcy^C?|!<0%Ys#H=S+yaNBtf**1bF;)bHzntt zr>Xa2?X7df1#kI^p28&!{x{frGg4CltpX3jB$Zm}coZ9@Xl>lIDL{cMMy(l%CoL_J zWU@%}4HS{8N%HIg%Ts0^4OBWZi4mJ9jpid;60$E}WzM&ZOhMWiQUtI0mYxqVg@Ah# zuwQ3q=NZ&b2t(*7(&Y~WEC!oPERIK?EFV;wyB4KNm*%kdY{GPj32bT}M!#QM@yG9@J4QLSZ$FA}vb<2(ao-1i|K{Dg>Fx$pCnHYK z*9c`lO{nb$(Z11242B!8UOBnD>+>*D$8aDU{2AMew7^~mFlcaogoRz3`)iGAIwE4* z-1SB9#Nf6x7*?A-3=G;6!F!nJLavjTWBQ#+Q057O73BgV1u>o*W3QmaD@VWf15PyN zVq18f7!|p@xVk>%$-+x~1l}8HZ^YCD_wI#x49#FueDhH8+%JJVeRi7FRu7Sof)fI8 zEJU5Wuer*{E-YpHQ05}R(m1UQQBp%XS8fj=CLm2Sq!ih;&e#K1t=m{9b^}+P{k?i< z5*-HaHaj|H8i%=TCW<8vu+{X;NU|4s0Vm8TjqIkh$32)mskh7bcpK7KkaFMlBYbq*E z^D#Y%I*#4>#EA`q$t+4qnwa2#GLvnK16YA;aA@d!4^8LD@>dbZ0@L}Vr1MAb$^AQW zoIu&{c8;cLrs#wdxB$3lyBSrqwZEe1s8l~{uB#2cp^phz*bp<#z=A{Rko)aj@t1L` zqem-VoVy5mq>k@$lh*pI$;S5dRQR8ra>TL~ot|?q$}BU+D-FRf9H9v8o}FDyMP+c5 z7G4JHd=7BB^H=wUQdDTOya;TcNdMZ&5= zTPXF}GpkFN%D&dFUqLP3Tj8jM(cQklZkQLs=njV&7>cIfoME6qtpwXKuY3Cz^rJBj z7wK5&d;`kHn~N8(QRa#7-VwBROlcAFhyq{sI*xG>IxvTIG3M4F2tvpJw5mD9!-(!_ z-|u)6zvU`**C&H}yP|8tqoQ!E8Bi(!WWp!bW8Fhm45kw}!4zs-O&ljkp{1bKHs0SU zfpeMg$HZ=l4v5k^&s0S!^B@iFiM1XaK6w~5FX46vmUX|^TrRrh|Hp1&j72JqkPwcG zUtDhAL!!Aby z;tmw*sag!wia6C#%IlWq$bWy_P561qImmaEmC=mYXEfPtJ8*{VIR5O#i&r{=rdg81 z+~9R%k3cnW`*vcY45u z*wE*czm*p6;B7PSL_b(y@BYaAMoZ3o#td9+4%}#EqJ6bL z6cf(@_slR==-!aDAcaIjFa(axyJNP74WJ0X?5kJe5)#)@FyQXs1WVG40o@(jS6pnY ztV2&lSM1jC4OBjpvK~4obTpCqEW2N~ft+G^`gC1GLv}@!zXE6!7};;xs1lW0SojZ? z%gg~5=-b;~+x7PoHm6u^__~>gQSOJdV$hETfwohbLou!v4ity9DnnLLYu5&TxB#jq zQIs)q;fMcss3bq_l15br4L5)d%r8|QbagYUTm-y`M@rjfh*Ar!9wbya+Oaiqdhg!- z-&F%MhB=m6!SSr`u^Il*btfw@Dr0((Agdaoz+aqO;4}s9t%#M z5|2KW(R}BJDi33f)K&PHApo!^%bW@p%QbXKVlsvJ+Q^e1PRIhRM|Mv489|(=X$OxHWg9Z2!4aSd$5--9f}Ei;Bt%fnzxIw6BNQr7dt@j?UEsA>?CJoxPvfGe?C6&juyqJiOsr-3;;taig>F+NnrhbN6Y&4yNQAFx=q`Hm zCi3qaqZacrVJ>ooX%;pTRD4!q5ApU+w2`qd=Cnc-Ur8&HqNR84oP>FntP>Cc!3!dvc@t~e)-ina8u4pCebgG{;`?!1`8735BZM+sg_qpVw{~-VlEe9TUklsR0EV5HB^b6Bh zruyzN^9vWSarr0URo$95keZj*3f@`F)_u$;@OGG)b)!z?_~u1Uc!qy5dfEu=jy{Bi z%5LD13hWlQZGpdLw@mQN-KMp5g4MA{Jd5x<%tgtUS$k+YQq5vzInW-UhvXe$t~{Ts zzM7qiH>s#`GB7bQ!QIeD@(|eK@9j?1hqe|9Mk=AStg|VkjfmU4d{mKmz2sx5R51ueeCK3%8a4sV5Ko&9rH{%&bn{k0B*Fk zwW+K_OJ8lz+PX=PIO~&vR#n(-_EW&Cf^;V4pbeGFt%&w*p5OTugr~(BysYzQ}X@u(r_#Lzcb4i*hrKa{s1O&$b6h*8c%p#!bQHOA?=b)|!rf69e;_QOe zNo|Ma^*uxZW z9ta4~GT>+6B|+Aa+BCi=n2vczp>o_NaH?Eh6NdgQbYKeZvsC`?9ZX}wF0T4;BZqk} zuoyV303-D@;bfp`J9N)NmzXb|w6`CFLo24r#{&b(OtU*#YGGVHHQNG3t<}MM#XCO; z1CoPpi8;^(n7AOoVhR}a`99NqjBOvR%s+kjkRD?!`F7zg&IB(M_Mt|5c-uWaGp zx_e!(0}%^}>4a`L3{TJTF=4%6O~$8>0K6V&QP{)=iZ5(9f=fs8aWDcoz#JCeg-e2U zV%D8P-9T0k(a{(idznPSgRQD}d}(psxrslcdA_S`L%KF99V9Q*ThSFF{wLUD8VpJ_ z$Jj=3gk4<0Orp+PEm|{8ysLZRLkC(~!b@O&WXylyML+0>k=oXPGD@Ej5=7BEzzRz| znlM7dtBzXOBqF+|58fr}Cr_4|3unHkN@q)|60}nhnpJRob2|3eZCBRC)keg`V7IAg zqYP2N={(K10D+A1JaQyKCI_z3_!s++zEB4yUKlL)$f*1fOglwW@}9b7rwKy+yFTmD zy}xfDlgxPjTu(*;(yF`k zL#07?Zf9+xgB!((ebKBnd9D6wYaN@BVnd?hs4om@{rYbYD2vJb=2umgY{49V9-U ze}1k=-uu4IH;`iBE1-cC3JILR(x$pyT?|^D{`5k@1vR8gpJ#N*5-9v3>;3R zq-HVR_URP~DhGv&y&*jojrYz0%x*9`2fGPpHj^THrQV(%2zK3H9KcjAU5+g({JLo4!(3oe}NK~Y#3mf(eq3f|ut_9T#AR2sNcA(Gp!wXLrh0&HY)1|xsx=PQ@*5y1 z+Rf)n2Ew!Knj|aG7QOT-v?KRN{$X;_2uv|u@8vIK3-3No1kV7!!!trnxQL1pY`sLB z2ti9TL4?^2xJ?GJ4<7=007otTg~jf1meMuUk}^%7wH*DpvG5zO-xKxFPnf^> zc?;|a?gcedbL&;gaeZ?6g%PNeEtd6Z`0E+tzWx}VfXTZvG2%{;@7VwUQO#S ztgYL?&FDwr0)oonlrLk;#br%KIFPMqRJf%yseW1LvGsoU8~UB%Fw52Xao?5%>J$(O zGlyDbM^|ca(i^3{^!`a zUuNqDVa$%$tsW7b%gcx6Tc1e-F&4UQLi(H;Qh{hKAh83cg-3cq{LI*?_@`Wg$Ce2u z4f~9Lclm7CrYul~stKDB@C)kr+V%rW$uc@ddLSPUnni%at6hFqSj)*SL84K%)~?@I z@htaTBy=SA;_?Ta^81hE!B3$_gs~Q&Z*Vz@nT}G(kRHQ_8oZFy=g%>S!V>O}D#cF| z!Z~FnS_^y`oK@$azgx+(l}b3$7NZb(m}u7w8jVi6_HQ~Dzgx6@ea}^yU*&Rhsnl z#)`{NBH@HJoM&|xt0EtZf zRL)1W{U8z`p|pwBTvQj~vJb9f?hy-tpp_V^ihY|{Imf7w3+XnYE{S>o^71y%*Jvov z@MRRu;qUHepegN%C8?~UE|o}wli6n|GL3%_5nxVus*#8z7W7>kEKPkSyePDCKK_U7 zei=Wmq+v#yJV+6ygBE^PPtO&`;$&r}e5@vM%F9ZkkvMSR*w+xqZ#7fk$Q|9kI^2S4 zp<8kOz7ob8SHNq5BD>tNfV7@&3#Cfq)JMB=aoR7|Gt?>pAMySE5r!2I*%V4sP($$s(;fazY4DCU{#~%I?wbC|6DrWfQRRwzk7kQkC}lViD-Rd;0s)IYynFnp#JLfdPfO2pQfy ze0V?)&Hot<-fl|MFU*he!=;Xnq{{Wway+S-`hb`6^4>AB>h=tj?y|%NwdzqBH6B| z;!lHLw##r8N$2wNv9nn+5N)cStS(Ir%L*Fmq1RJxuMZ3AhW+f(MmUqMUw#rPs4||(3cZ!!S znu`?-64Zk!IJaOat}uW0QkTuVx;dqdQ{$<|iMv|n+*=PmBM%FOFurBsZVuOCk|Q(_{^bEg!O_b0Sc?tkW1 zgahh+?w`=MGY{Z<5WoCi!{BZl6J4IG>;fx^)axq>Bq_(GEpXi=9l^^TbGJBcr#X+) zT3GQ@{$Q9pt~NTheDzV3NbpIBs17?PZIlJIkTTD!ZjTFR&t4zUl{DT!c^FeBa||xX zmP0YZgm5tl9dZnes`=q4Z8ce5j zX|2q~2rH(R+q(T3tB(rzh@3skO~x@EP~Hv2(Ks#QooI|hc=r06RS{3i`Hg$7;&S6ODw*$^8j@{_WY3s!2%4SzD~5d;ZMXYhVmb+9jjf z{_MQk){pYY*zjQIw!sF_mA&zE(4~cZ1f1?~Tz|{BN@w9x;lvIr2Fe?ASU9PscEo8Y z17B$Jp-1r%vNFrYpuwFitgK%;ijaaiIeMpEQEU}ubqpJyIpuq?zW;OT^Iv1mbc)Y^ zWpVh5u!pUZIyq=?l83SL%A3!MUTcq2zS71|`}nMBT0ve}8kH zyv42Of#&D$y3?IlEl!b48?VDr{^IJIYd%wY`y&Ps>@s=odAYRq$IQ9@-HgBL=qSs^ ziBm=MU%3XJ}Ph zZM6fOZgI~1Tf_uxwm4f5=@lcVThX3^&levJJGx(O!o9I#&&{pY(h+>&OmEYy7Fe6~ zbKx$=s9nsEI9z53r%R2$rl31wGggMBAK>rz`x9tlR?;G=iDEql6Dq?(rLUUnyo^^o6jGDmbYry!E*x@ zFoRbS%~b_!81&$(88=w?^Uh zrH--<3cwMi^VJI@{j3`KCm4n^W%tPT^h zxZ3lD0jIF;{S$NwH~dsgBsxrE^+8A1b!Q)~KH3r=Ci7U0@)Ei+DvHFY;ivDb@`i@+ z=oW4$SxNbWHt@SD%^Q9hq+u`%6L@T>I^H>iKtp^R-P$uL51R6I&PcK*TWqemN5D14 z&=Xw9(M`RcMXM6Rq}Y1&LOZFEx)*v5I({3P)GOS;trFt&m5$N)Fm-*Y6HD}cXJ*sR z*>l8Js>r06DzQ@M-ba|Edhx>k2NL(Rb{}jHVp_7R1r-QUsg)4kj&O@q4+DnV=AxDE zigL$^p{3kaFhle7KZl$s!J|sx>k)a(NSscO2bDw6RD@LaY&cwn9UB_Xr;Xp&(X&t< zOx+hq)?mbX_Uw=3$u&oTrJ+VRsl8fTk@%>RSNHxgtnNg-Q|FaP;k92V$5)A`g=iFL z7&w(&{Vi{CIwG#m(NQ@>fo0WK%HQ(Ra4G{O5BRynxXV!nq}&V){F(okz!y^Lb)TDH zJvXC=+x)%Qv$(qV+k)DP zg#gOa+5$8aJ?D@|T29VYL=HSSh=$(z??8Rc@Mf>glucr)b7D5O8GHFgc4-|qa&NE2 zze!*kHfh>X(Z#?{Ny||O3>lu zHiMOUoj>dluzaMG)ipI){NBVSv|m1N@td?<{F)p-rbB2TptK@?Q2Jgwb6`TI%c9 zuRkY|&T|nry`>y90L+nXJo`YzJ|GR{eW19cJ!tbt)_{OP&mD-&5c^lX*CE;ltO8-G zK#V}0Fw)Vf`*>d=QR4#_Dqhed^-kMfyh!M0>HRLl5FL8oId`&V%HO7QMj+9KRa;!D z!2r!*<6%liSt9#2{bG!yeZpM4Y{g=;k!RD6Z7*N%+}`=-H`t{@+C}@qQ~^-AO$~2n zWBUP`A+Q-RGA(rRGN@aiNyV(4-bWtr{g=;A0DlkV>ot{>lweSVnK3pZOq_Cb1i|T= z#gMwWUAm5jvMJB(PdCz-p9X8ib@F85)?~V42en6Tr-?~PA(b4cmchsfz!dQc4Wn5A z%`+K;bh>JsVVLtN}_z5#J`1-d#biTPZ6#k(T zg3f}@a{xkZM$NHe=7vBH>#vOqEzjhpR(C<^_owUhK}TbvE$be`sRuoPZaBfsYim2t zANKre^Ycw?8}D@Tv;mKT<2jo2l)@VeGg%)xTVC76b)MH-GYq!><=69IQN!_|DS$)D z;mOC2-icY9bJ|x^$3z~lplsU1W*56%OwV4aQ1>(*l3cZR@{ol|<*J5`y%SBj8ee zs6A`A$6yfbv-VFNb4PM#UcY%W17L#BpXjqeM>}V?$x4pcsJ$$w!3Yko?uDW!ZLS#i z6%4!ht|z^5SZ+6rjR_)C>We|Ad?0s;^#iyu^lOAZTx-~A9{K&vbKHA%Z7gq<5MWTL zt9I2{QBK~v;Uc84VJ83Ua|uv;u=p1j7k_1-ZvpMf!`2qJ#x>mn9{SRzQ|03s?kges zLT_t2{BqIu8os$=l1|34yhxG7nCOJ8GrGg8g2wX$RRJS#^4Kh9A`u0 z@hlA8YW`dSXpYwL;L_#CyQpU_$K*+UG529G*T0RJACzSdDu?P}1RdYYKwE4(V*h6% zT%p_a-}20fL8)a z+XlS_mPEi)3~YfnJp$AAmcSpt0hqJE@e$yl>gI{|VjI9Gs2s4{W>a|az|$h&mG8iu zeoe0bBJjG8yYFL~ryT@_-IkrodedJ67cR<3NW6Ie9#qP>O$y;V1su|t4_xO5-1!DN z7Vqt)SCEB=z{UP+MNa}ZAc(sD1P=QE$B4uArf*$+HB6R+(?ntU!+)i~!v9Uc)yuEK z|MnA%h0MU6A0l6Vo7r0S&4D^cfib|3A8E WRmjq1P1}GDVeoYIb6Mw<&;$S(M^29b literal 0 HcmV?d00001 diff --git a/docs/src/zh/guide/config.md b/docs/src/zh/guide/config.md index b39b41d6..206a5575 100644 --- a/docs/src/zh/guide/config.md +++ b/docs/src/zh/guide/config.md @@ -34,6 +34,7 @@ RedisShake 提供了不同的 Reader 用来对接不同的源端,配置详见 * [Sync Reader](../reader/sync_reader.md) * [Scan Reader](../reader/scan_reader.md) * [RDB Reader](../reader/rdb_reader.md) +* [AOF Reader](../reader/aof_reader.md) ## writer 配置 diff --git a/docs/src/zh/reader/aof_reader.md b/docs/src/zh/reader/aof_reader.md new file mode 100644 index 00000000..2be803c6 --- /dev/null +++ b/docs/src/zh/reader/aof_reader.md @@ -0,0 +1,18 @@ +# aof_reader + +## 介绍 + +可以使用 `aof_reader` 来从 AOF 文件中读取数据,然后写入目标端。常见于从备份文件中恢复数据,还支持数据闪回。 + +## 配置 + +```toml +[aof_reader] +aoffilepath="/tmp/appendonly.aof.manifest" #或者单aof文件 "/tmp/appendonly.aof" +aoftimestamp="0" +``` + +* 应传入绝对路径。 + +## 主要流程如下: +![aof_reader.jpg](/public/aof_reader.jpg) \ No newline at end of file diff --git a/docs/src/zh/reader/scan_reader.md b/docs/src/zh/reader/scan_reader.md index ea61718e..7131b411 100644 --- a/docs/src/zh/reader/scan_reader.md +++ b/docs/src/zh/reader/scan_reader.md @@ -24,6 +24,7 @@ username = "" # keep empty if not using ACL password = "" # keep empty if no authentication is required tls = false ksn = false # set to true to enabled Redis keyspace notifications (KSN) subscription +dbs = [] # set you want to scan dbs, if you don't want to scan all ``` * `cluster`:源端是否为集群 @@ -35,6 +36,7 @@ ksn = false # set to true to enabled Redis keyspace notifications * `tls`:源端是否开启 TLS/SSL,不需要配置证书因为 RedisShake 没有校验服务器证书 * `ksn`:开启 `ksn` 参数后 RedisShake 会在 `SCAN` 之前使用 [Redis keyspace notifications](https://redis.io/docs/manual/keyspace-notifications/) 能力来订阅 Key 的变化。当 Key 发生变化时,RedisShake 会使用 `DUMP` 与 `RESTORE` 命令来从源端读取 Key 的内容,并写入目标端。 +* `dbs`:源端为非集群模式时,支持指定DB库 ::: warning Redis keyspace notifications 不会感知到 `FLUSHALL` 与 `FLUSHDB` 命令,因此在使用 `ksn` 参数时,需要确保源端数据库不会执行这两个命令。 diff --git a/go.mod b/go.mod index f8098742..03f9bdc3 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect golang.org/x/sys v0.12.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/text v0.12.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4727f8bb..2d2336c2 100644 --- a/go.sum +++ b/go.sum @@ -336,8 +336,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/aof/aof.go b/internal/aof/aof.go new file mode 100644 index 00000000..78625cde --- /dev/null +++ b/internal/aof/aof.go @@ -0,0 +1,155 @@ +package aof + +import ( + "bufio" + "io" + "os" + "strconv" + "strings" + + "RedisShake/internal/entry" + "RedisShake/internal/log" +) + +const ( + AOFNotExist = 1 + AOFOpenErr = 3 + AOFOK = 0 + AOFEmpty = 2 + AOFFailed = 4 + AOFTruncated = 5 + SizeMax = 128 +) + +type Loader struct { + filPath string + ch chan *entry.Entry +} + +func NewLoader(filPath string, ch chan *entry.Entry) *Loader { + ld := new(Loader) + ld.ch = ch + ld.filPath = filPath + return ld +} + +func ReadCompleteLine(reader *bufio.Reader) ([]byte, error) { + line, isPrefix, err := reader.ReadLine() + if err != nil { + return nil, err + } + + for isPrefix { + var additional []byte + additional, isPrefix, err = reader.ReadLine() + if err != nil { + return nil, err + } + line = append(line, additional...) + } + + return line, err +} + +func (ld *Loader) LoadSingleAppendOnlyFile(AOFTimeStamp int64) int { + ret := AOFOK + AOFFilepath := ld.filPath + fp, err := os.Open(AOFFilepath) + if err != nil { + if os.IsNotExist(err) { + if _, err := os.Stat(AOFFilepath); err == nil || !os.IsNotExist(err) { + log.Infof("Fatal error: can't open the append log File %v for reading: %v", AOFFilepath, err.Error()) + return AOFOpenErr + } else { + log.Infof("The append log File %v doesn't exist: %v", AOFFilepath, err.Error()) + return AOFNotExist + } + + } + defer fp.Close() + + stat, _ := fp.Stat() + if stat.Size() == 0 { + return AOFEmpty + } + } + reader := bufio.NewReader(fp) + for { + + line, err := ReadCompleteLine(reader) + { + if err != nil { + if err == io.EOF { + break + } else { + log.Infof("Unrecoverable error reading the append only File %v: %v", AOFFilepath, err) + ret = AOFFailed + return ret + } + } else { + _, errs := fp.Seek(0, io.SeekCurrent) + if errs != nil { + log.Infof("Unrecoverable error reading the append only File %v: %v", AOFFilepath, errs) + ret = AOFFailed + return ret + } + } + + if line[0] == '#' { + if AOFTimeStamp != 0 && strings.HasPrefix(string(line), "#TS:") { + var ts int64 + ts, err = strconv.ParseInt(strings.TrimPrefix(string(line), "#TS:"), 10, 64) + if err != nil { + log.Panicf("Invalid timestamp annotation") + } + + if ts > AOFTimeStamp { + ret = AOFTruncated + log.Infof("Reached recovery timestamp: %s, subsequent data will no longer be read.", line) + return ret + } + } + continue + } + if line[0] != '*' { + log.Panicf("Bad File format reading the append only File %v:make a backup of your AOF File, then use ./redis-check-AOF --fix ", AOFFilepath) + } + argc, _ := strconv.ParseInt(string(line[1:]), 10, 64) + if argc < 1 { + log.Panicf("Bad File format reading the append only File %v:make a backup of your AOF File, then use ./redis-check-AOF --fix ", AOFFilepath) + } + if argc > int64(SizeMax) { + log.Panicf("Bad File format reading the append only File %v:make a backup of your AOF File, then use ./redis-check-AOF --fix ", AOFFilepath) + } + e := entry.NewEntry() + var argv []string + + for j := 0; j < int(argc); j++ { + line, err := ReadCompleteLine(reader) + if err != nil || line[0] != '$' { + log.Infof("Bad File format reading the append only File %v:make a backup of your AOF File, then use ./redis-check-AOF --fix ", AOFFilepath) + ret = AOFFailed + return ret + } + v64, _ := strconv.ParseInt(string(line[1:]), 10, 64) + argString := make([]byte, v64+2) + argString, err = ReadCompleteLine(reader) + if err != nil { + log.Infof("Unrecoverable error reading the append only File %v: %v", AOFFilepath, err) + ret = AOFFailed + return ret + } + argString = argString[:v64] + argv = append(argv, string(argString)) + } + + for _, value := range argv { + e.Argv = append(e.Argv, value) + } + ld.ch <- e + + } + + } + return ret +} diff --git a/internal/reader/aof_reader.go b/internal/reader/aof_reader.go new file mode 100644 index 00000000..b4631034 --- /dev/null +++ b/internal/reader/aof_reader.go @@ -0,0 +1,104 @@ +package reader + +import ( + "RedisShake/internal/aof" + "path/filepath" + + "RedisShake/internal/entry" + "RedisShake/internal/log" + "RedisShake/internal/utils" + + "github.com/dustin/go-humanize" +) + +type AOFReaderOptions struct { + Filepath string `mapstructure:"filepath" default:""` + AOFTimestamp int64 `mapstructure:"timestamp" default:"0"` +} + +type aofReader struct { + path string + ch chan *entry.Entry + + stat struct { + AOFName string `json:"aof_name"` + AOFStatus string `json:"aof_status"` + AOFFilepath string `json:"aof_file_path"` + AOFFileSizeBytes int64 `json:"aof_file_size_bytes"` + AOFFileSizeHuman string `json:"aof_file_size_human"` + AOFFileSentBytes int64 `json:"aof_file_sent_bytes"` + AOFFileSentHuman string `json:"aof_file_sent_human"` + AOFPercent string `json:"aof_percent"` + AOFTimestamp int64 `json:"aof_time_stamp"` + } +} + +func (r *aofReader) Status() interface{} { + return r.stat +} + +func (r *aofReader) StatusString() string { + return r.stat.AOFStatus +} + +func (r *aofReader) StatusConsistent() bool { + return r.stat.AOFFileSentBytes == r.stat.AOFFileSizeBytes +} + +func NewAOFReader(opts *AOFReaderOptions) Reader { + log.Infof("NewAOFReader: path=[%s]", opts.Filepath) + absolutePath, err := filepath.Abs(opts.Filepath) + if err != nil { + log.Panicf("NewAOFReader: filepath.Abs error: %s", err.Error()) + } + log.Infof("NewAOFReader: absolute path=[%s]", absolutePath) + r := &aofReader{ + path: absolutePath, + ch: make(chan *entry.Entry), + } + r.stat.AOFName = "aof_reader" + r.stat.AOFStatus = "init" + r.stat.AOFFilepath = absolutePath + r.stat.AOFFileSizeBytes = int64(utils.GetFileSize(absolutePath)) + r.stat.AOFFileSizeHuman = humanize.Bytes(uint64(r.stat.AOFFileSizeBytes)) + r.stat.AOFTimestamp = opts.AOFTimestamp + return r +} + +func (r *aofReader) StartRead() chan *entry.Entry { + //init entry + r.ch = make(chan *entry.Entry, 1024) + + // start read aof + go func() { + aofFileInfo := NewAOFFileInfo(r.path, r.ch) + // try load manifest file + aofFileInfo.AOFLoadManifestFromDisk() + manifestInfo := aofFileInfo.AOFManifest + if manifestInfo == nil { // load single aof file + log.Infof("start send single AOF path=[%s]", r.path) + aofLoader := aof.NewLoader(r.path, r.ch) + ret := aofLoader.LoadSingleAppendOnlyFile(r.stat.AOFTimestamp) + if ret == AOFOk || ret == AOFTruncated { + log.Infof("The AOF File was successfully loaded") + } else { + log.Infof("There was an error opening the AOF File.") + } + log.Infof("Send single AOF finished. path=[%s]", r.path) + close(r.ch) + } else { + aofLoader := NewAOFFileInfo(r.path, r.ch) + ret := aofLoader.LoadAppendOnlyFile(manifestInfo, r.stat.AOFTimestamp) + if ret == AOFOk || ret == AOFTruncated { + log.Infof("The AOF File was successfully loaded") + } else { + log.Infof("There was an error opening the AOF File.") + } + log.Infof("Send multi-part AOF finished. path=[%s]", r.path) + close(r.ch) + } + + }() + + return r.ch +} \ No newline at end of file diff --git a/internal/reader/parsing_aof.go b/internal/reader/parsing_aof.go new file mode 100644 index 00000000..e6ca00fd --- /dev/null +++ b/internal/reader/parsing_aof.go @@ -0,0 +1,735 @@ +package reader + +import ( + "RedisShake/internal/aof" + "RedisShake/internal/entry" + "RedisShake/internal/log" + "bufio" + "bytes" + "container/list" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" + "unicode" +) + +const ( + AOFManifestFileTypeBase = "b" /* Base File */ + AOFManifestTypeHist = "h" /* History File */ + AOFManifestTypeIncr = "i" /* INCR File */ + AOFNotExist = 1 + AOFOpenErr = 3 + AOFOk = 0 + AOFEmpty = 2 + AOFFailed = 4 + AOFTruncated = 5 + AOFManifestKeyFileName = "File" + AOFManifestKeyFileSeq = "seq" + AOFManifestKeyFileType = "type" +) + +func Ustime() int64 { + tv := time.Now() + ust := int64(tv.UnixNano()) / 1000 + return ust + +} + +func MakePath(Paths string, FileName string) string { + return path.Join(Paths, FileName) +} + +func StringNeedsRepr(s string) int { + sLen := len(s) + point := 0 + for sLen > 0 { + if s[point] == '\\' || s[point] == '"' || s[point] == '\n' || s[point] == '\r' || + s[point] == '\t' || s[point] == '\a' || s[point] == '\b' || !unicode.IsPrint(rune(s[point])) || unicode.IsSpace(rune(s[point])) { + return 1 + } + sLen-- + point++ + } + + return 0 +} + +type INFO struct { + AOFDirName string + AOFUseRDBPreamble int // TODO:not support parsing rdb preamble + AOFManifest *AOFManifest + AOFFileName string + AOFCurrentSize int64 + AOFRewriteBaseSize int64 + updateLoadingFile string + ch chan *entry.Entry +} + +func (aofInfo *INFO) GetAOFDirName() string { + return aofInfo.AOFDirName +} + +func NewAOFFileInfo(aofFilePath string, ch chan *entry.Entry) *INFO { + return &INFO{ + AOFDirName: filepath.Dir(aofFilePath), + AOFUseRDBPreamble: 0, + AOFManifest: nil, + AOFFileName: filepath.Base(aofFilePath), + AOFCurrentSize: 0, + AOFRewriteBaseSize: 0, + ch: ch, + } +} + +func (a *AOFInfo) GetAOFInfoName() string { + return a.FileName +} + +func DirExists(dName string) int { + _, err := os.Stat(dName) + if err != nil { + return 0 + } + + return 1 +} + +func FileExist(FileName string) int { + _, err := os.Stat(FileName) + if err != nil { + return 0 + } + + return 1 +} + +func IsHexDigit(c byte) bool { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F') +} + +func HexDigitToInt(c byte) int { + switch c { + case '0': + return 0 + case '1': + return 1 + case '2': + return 2 + case '3': + return 3 + case '4': + return 4 + case '5': + return 5 + case '6': + return 6 + case '7': + return 7 + case '8': + return 8 + case '9': + return 9 + case 'a', 'A': + return 10 + case 'b', 'B': + return 11 + case 'c', 'C': + return 12 + case 'd', 'D': + return 13 + case 'e', 'E': + return 14 + case 'f', 'F': + return 15 + default: + return 0 + } +} + +func SplitArgs(line string) ([]string, int) { + var p = line + var Current string + var vector []string + argc := 0 + i := 0 + lens := len(p) + for { //SKIP BLANKS + for i < lens && unicode.IsSpace(rune(p[i])) { + i++ + } + if i < lens { + inq := false // Set to true if we are in "quotes" + insq := false // Set to true if we are in 'single quotes' + done := false + + for !done { + if inq { + + if p[i] == '\\' && (p[i+1]) == 'x' && IsHexDigit(p[i+2]) && IsHexDigit(p[i+3]) { + _, err1 := strconv.ParseInt(string(p[i+2]), 16, 64) + _, err2 := strconv.ParseInt(string(p[i+3]), 16, 64) + if err1 == nil && err2 == nil { + hexadecimal := (HexDigitToInt(p[i+2]) * 16) + HexDigitToInt(p[i+3]) + Current = Current + fmt.Sprint(hexadecimal) + i += 3 + } + + } else if p[i] == '\\' && i+1 < lens { + var c byte + i++ + switch p[i] { + case 'n': + c = '\n' + case 'r': + c = 'r' + case 'a': + c = '\a' + default: + c = p[i] + } + Current += string(c) + } else if p[i] == '"' { + if i+1 < lens && !unicode.IsSpace(rune(p[i+1])) { + return nil, 0 + } + done = true + } else if i >= lens { + return nil, 0 + } else { + Current += string(p[i]) + } + } else if insq { + if p[i] == '\\' && p[i+1] == '\'' { + i++ + Current += "'" + } else if p[i] == '\'' { + if i+1 < lens && !unicode.IsSpace(rune(p[i+1])) { + return nil, 0 + } + done = true + } else if i >= lens { + return nil, 0 + } else { + Current += string(p[i]) + } + + } else { + switch p[i] { + case ' ', '\n', '\r', '\t', '\000': + done = true + case '"': + inq = true + case '\'': + insq = true + default: + Current += string(p[i]) + } + } + if i < lens { + i++ + } + if i == lens { + done = true + } + } + + vector = append(vector, Current) + argc++ + Current = "" + + } else { + return vector, argc + } + + } +} + +func StringCatPrintf(s string, fmtStr string, args ...interface{}) string { + result := fmt.Sprintf(fmtStr, args...) + if s == "" { + return result + } else { + return s + result + } +} + +func StringCatRepr(s string, p string, length int) string { + s = s + ("\"") + for i := 0; i < length; i++ { + switch p[i] { + case '\\', '"': + s = StringCatPrintf(s, "\\%c", p[i]) + case '\n': + s = s + "\\n" + case '\r': + s = s + "\\r" + case '\t': + s = s + "\\t" + case '\a': + s = s + "\\a" + case '\b': + s = s + "\\b" + default: + if strconv.IsPrint(rune(p[i])) { + s = s + string(p[i]) + } else { + s = s + "\\x%02x" + } + } + } + return s + "\"" +} + +func (aofInfo *INFO) UpdateLoadingFileName(FileName string) { + aofInfo.updateLoadingFile = FileName +} + +// AOFInfo AOF manifest definition +type AOFInfo struct { + FileName string + FileSeq int64 + AOFFileType string +} + +func AOFInfoCreate() *AOFInfo { + return new(AOFInfo) +} + +func AOFInfoFormat(buf string, ai *AOFInfo) string { + var aofManifestTostring string + if StringNeedsRepr(ai.FileName) == 1 { + aofManifestTostring = StringCatRepr("", ai.FileName, len(ai.FileName)) + } + var ret string + if aofManifestTostring != "" { + ret = StringCatPrintf(buf, "%s %s %s %d %s %s\n", AOFManifestKeyFileName, aofManifestTostring, AOFManifestKeyFileSeq, ai.FileSeq, AOFManifestKeyFileType, ai.AOFFileType) + } else { + ret = StringCatPrintf(buf, "%s %s %s %d %s %s\n", AOFManifestKeyFileName, ai.FileName, AOFManifestKeyFileSeq, ai.FileSeq, AOFManifestKeyFileType, ai.AOFFileType) + } + return ret +} + +func PathIsBaseName(Path string) bool { + return strings.IndexByte(Path, '/') == -1 && strings.IndexByte(Path, '\\') == -1 +} + +func AOFLoadManifestFromFile(amFilepath string) *AOFManifest { + var maxSeq int64 + am := AOFManifestCreate() + fp, err := os.Open(amFilepath) + if err != nil { + log.Panicf("Fatal error:can't open the AOF manifest %v for reading: %v", amFilepath, err) + } + defer fp.Close() + var argv []string + var ai *AOFInfo + var line string + lineNum := 0 + fpReader := bufio.NewReader(fp) + for { + buf, err := fpReader.ReadString('\n') + if err != nil { + if err == io.EOF { + if lineNum == 0 { + log.Infof("Found an empty AOF manifest") + am = nil + return am + } else { + break + } + + } else { + log.Infof("Reading the manifest file, at line %d", lineNum) + log.Infof("Read AOF manifest failed") + am = nil + return am + + } + } + + lineNum++ + if buf[0] == '#' { + continue + } + if !strings.Contains(buf, "\n") { + log.Infof("Reading the manifest file, at line %d", lineNum) + log.Infof("The AOF manifest File contains too long line") + return nil + } + line = strings.Trim(buf, " \t\r\n") + if len(line) == 0 { + log.Infof("Reading the manifest file, at line %d", lineNum) + log.Infof("Invalid AOF manifest File format") + return nil + } + argc := 0 + argv, argc = SplitArgs(line) + + if argc < 6 || argc%2 != 0 { + log.Infof("Reading the manifest file, at line %d", lineNum) + log.Infof("Invalid AOF manifest File format") + am = nil + return am + } + ai = AOFInfoCreate() + for i := 0; i < argc; i += 2 { + if strings.EqualFold(argv[i], AOFManifestKeyFileName) { + ai.FileName = argv[i+1] + if !PathIsBaseName(ai.FileName) { + log.Infof("Reading the manifest file, at line %d", lineNum) + log.Panicf("File can't be a path, just a Filename") + } + } else if strings.EqualFold(argv[i], AOFManifestKeyFileSeq) { + ai.FileSeq, _ = strconv.ParseInt(argv[i+1], 10, 64) + } else if strings.EqualFold(argv[i], AOFManifestKeyFileType) { + ai.AOFFileType = string(argv[i+1][0]) + } + } + if ai.FileName == "" || ai.FileSeq == 0 || ai.AOFFileType == "" { + log.Infof("Reading the manifest file, at line %d", lineNum) + log.Panicf("Invalid AOF manifest File format") + } + if ai.AOFFileType == AOFManifestFileTypeBase { + if am.BaseAOFInfo != nil { + log.Infof("Reading the manifest file, at line %d", lineNum) + log.Panicf("Found duplicate Base File information") + } + am.BaseAOFInfo = ai + am.CurrBaseFileSeq = ai.FileSeq + } else if ai.AOFFileType == AOFManifestTypeHist { + am.HistoryList.PushBack(ai) + } else if ai.AOFFileType == AOFManifestTypeIncr { + if ai.FileSeq <= maxSeq { + log.Infof("Reading the manifest file, at line %d", lineNum) + log.Panicf("Found a non-monotonic sequence number") + } + am.incrAOFList.PushBack(ai) + am.CurrIncrFileSeq = ai.FileSeq + maxSeq = ai.FileSeq + } else { + log.Infof("Reading the manifest file, at line %d", lineNum) + log.Panicf("Unknown AOF File type") + } + ai = nil + } + return am +} + +type AOFManifest struct { + BaseAOFInfo *AOFInfo + incrAOFList *list.List + HistoryList *list.List + CurrBaseFileSeq int64 + CurrIncrFileSeq int64 + Dirty int64 +} + +func AOFManifestCreate() *AOFManifest { + am := &AOFManifest{ + incrAOFList: list.New(), + HistoryList: list.New(), + } + return am +} + +func GetAOFManifestAsString(am *AOFManifest) string { + if am == nil { + panic("am is nil") + } + var buf string + if am.BaseAOFInfo != nil { + buf = AOFInfoFormat(buf, am.BaseAOFInfo) + } + for ln := am.HistoryList.Front(); ln != nil; ln = ln.Next() { + buf = AOFInfoFormat(buf, ln.Value.(*AOFInfo)) + } + for ln := am.incrAOFList.Front(); ln != nil; ln = ln.Next() { + buf = AOFInfoFormat(buf, ln.Value.(*AOFInfo)) + } + return buf + +} + +func (aofInfo *INFO) AOFLoadManifestFromDisk() { + if DirExists(aofInfo.AOFDirName) == 0 { + log.Infof("The AOF Directory %v doesn't exist", aofInfo.AOFDirName) + return + } + aofInfo.AOFManifest = AOFManifestCreate() + amFilepath := path.Join(aofInfo.AOFDirName, aofInfo.AOFFileName) + if FileExist(amFilepath) == 0 { + log.Infof("The AOF Directory %v doesn't exist", aofInfo.AOFDirName) + return + } + + am := AOFLoadManifestFromFile(amFilepath) + aofInfo.AOFManifest = am +} + +func (aofInfo *INFO) GetAOFManifestFileName() string { + return aofInfo.AOFFileName +} + +func (aofInfo *INFO) AOFFileExist(FileName string) int { + Filepath := path.Join(aofInfo.AOFDirName, FileName) + ret := FileExist(Filepath) + return ret +} + +func (aofInfo *INFO) GetAppendOnlyFileSize(FileName string, status *int) int64 { + var size int64 + + AOFFilePath := path.Join(aofInfo.AOFDirName, FileName) + + stat, err := os.Stat(AOFFilePath) + if err != nil { + if status != nil { + if os.IsNotExist(err) { + *status = AOFNotExist + } else { + *status = AOFOpenErr + } + } + log.Panicf("Unable to obtain the AOF File %v length. stat: %v", FileName, err.Error()) + size = 0 + } else { + if status != nil { + *status = AOFOk + } + size = stat.Size() + } + return size +} + +func (aofInfo *INFO) GetBaseAndIncrAppendOnlyFilesSize(am *AOFManifest, status *int) int64 { + var size int64 + if am.BaseAOFInfo != nil { + if am.BaseAOFInfo.AOFFileType != AOFManifestFileTypeBase { + log.Panicf("File type must be Base.") + } + size += aofInfo.GetAppendOnlyFileSize(am.BaseAOFInfo.FileName, status) + if *status != AOFOk { + return 0 + } + } + + for ln := am.HistoryList.Front(); ln != nil; ln = ln.Next() { + ai := ln.Value.(*AOFInfo) + if ai.AOFFileType != AOFManifestTypeIncr { + log.Panicf("File type must be Incr") + } + size += aofInfo.GetAppendOnlyFileSize(ai.FileName, status) + if *status != AOFOk { + return 0 + } + } + return size +} + +func GetBaseAndIncrAppendOnlyFilesNum(am *AOFManifest) int { + num := 0 + if am.BaseAOFInfo != nil { + num++ + } + if am.incrAOFList != nil { + num += am.incrAOFList.Len() + } + return num +} + +func GetHistoryAndIncrAppendOnlyFilesNum(am *AOFManifest) int { + num := 0 + if am.HistoryList != nil { + num += am.HistoryList.Len() + } + if am.incrAOFList != nil { + num += am.incrAOFList.Len() + } + return num +} + +func (aofInfo *INFO) LoadAppendOnlyFile(am *AOFManifest, AOFTimeStamp int64) int { + if am == nil { + log.Panicf("AOFManifest is null") + } + status := AOFOk + ret := AOFOk + var start int64 + var totalSize int64 = 0 + var BaseSize int64 = 0 + var AOFName string + var totalNum, AOFNum int + + if am.BaseAOFInfo == nil && am.incrAOFList == nil { + return AOFNotExist + } + + totalNum = GetBaseAndIncrAppendOnlyFilesNum(am) + if totalNum <= 0 { + log.Panicf("Assertion failed: IncrAppendOnlyFilestotalNum > 0") + } + + totalSize = aofInfo.GetBaseAndIncrAppendOnlyFilesSize(am, &status) + if status != AOFOk { + if status == AOFNotExist { + status = AOFFailed + } + return status + } else if totalSize == 0 { + return AOFEmpty + } + + log.Infof("The AOF File starts loading.") + if am.BaseAOFInfo != nil { + if am.BaseAOFInfo.AOFFileType == AOFManifestFileTypeBase { + AOFName = am.BaseAOFInfo.FileName + aofInfo.UpdateLoadingFileName(AOFName) + BaseSize = aofInfo.GetAppendOnlyFileSize(AOFName, nil) + start = Ustime() + ret = aofInfo.ParsingSingleAppendOnlyFile(AOFName, 0) //Currently, RDB files cannot be restored at a point in time. + if ret == AOFOk || (ret == AOFTruncated) { + log.Infof("DB loaded from Base File %v: %.3f seconds", AOFName, float64(Ustime()-start)/1000000) + } + if ret == AOFEmpty { + ret = AOFOk + } + if ret == AOFOpenErr || ret == AOFFailed { + if ret == AOFOk || ret == AOFTruncated { + log.Infof("The AOF File was successfully loaded") + } else { + if ret == AOFOpenErr { + log.Panicf("There was an error opening the AOF File.") + } else { + log.Panicf("Failed to open AOF File.") + } + } + return ret + } + } + totalNum-- + } else { + totalNum = GetHistoryAndIncrAppendOnlyFilesNum(am) + log.Infof("The BaseAOF file does not exist. Start loading the HistoryAOF and IncrAOF files.") + if am.HistoryList.Len() > 0 { + for ln := am.HistoryList.Front(); ln != nil; ln = ln.Next() { + ai := ln.Value.(*AOFInfo) + if ai.AOFFileType != AOFManifestTypeHist { + log.Panicf("The manifestType must be Hist") + } + AOFName = ai.FileName + aofInfo.UpdateLoadingFileName(AOFName) + AOFNum++ + start = Ustime() + ret = aofInfo.ParsingSingleAppendOnlyFile(AOFName, AOFTimeStamp) + if ret == AOFOk || (ret == AOFTruncated) { + log.Infof("DB loaded from History File %v: %.3f seconds", AOFName, float64(Ustime()-start)/1000000) + return ret + } + if ret == AOFEmpty { + ret = AOFOk + } + if ret == AOFOpenErr || ret == AOFFailed { + if ret == AOFOpenErr { + log.Panicf("There was an error opening the AOF File.") + } else { + log.Infof("Failed to open AOF File.") + } + return ret + } + totalNum-- + } + } + + } + + if am.incrAOFList.Len() > 0 { + for ln := am.incrAOFList.Front(); ln != nil; ln = ln.Next() { + ai := ln.Value.(*AOFInfo) + if ai.AOFFileType != AOFManifestTypeIncr { + log.Panicf("The manifestType must be Incr") + } + AOFName = ai.FileName + aofInfo.UpdateLoadingFileName(AOFName) + AOFNum++ + start = Ustime() + ret = aofInfo.ParsingSingleAppendOnlyFile(AOFName, AOFTimeStamp) + if ret == AOFOk || (ret == AOFTruncated) { + log.Infof("DB loaded from incr File %v: %.3f seconds", AOFName, float64(Ustime()-start)/1000000) + return ret + } + if ret == AOFEmpty { + ret = AOFOk + } + if ret == AOFOpenErr || ret == AOFFailed { + if ret == AOFOpenErr { + log.Panicf("There was an error opening the AOF File.") + } else { + log.Infof("Failed to open AOF File.") + } + return ret + } + totalNum-- + } + } + if totalNum == 0 { + log.Infof("All AOF files have been sent.") + } else { + log.Panicf("There are still %d AOF files that were not successfully sent.", totalNum) + } + aofInfo.AOFCurrentSize = totalSize + aofInfo.AOFRewriteBaseSize = BaseSize + + log.Infof("The AOF File loading end.") + return ret + +} + +func (aofInfo *INFO) ParsingSingleAppendOnlyFile(FileName string, AOFTimeStamp int64) int { + ret := AOFOk + AOFFilepath := path.Join(aofInfo.AOFDirName, FileName) + println(AOFFilepath) + fp, err := os.Open(AOFFilepath) + if err != nil { + if os.IsNotExist(err) { + if _, err := os.Stat(AOFFilepath); err == nil || !os.IsNotExist(err) { + log.Infof("Fatal error: can't open the append log File %v for reading: %v", FileName, err.Error()) + return AOFOpenErr + } else { + log.Infof("The append log File %v doesn't exist: %v", FileName, err.Error()) + return AOFNotExist + } + + } + + stat, _ := fp.Stat() + if stat.Size() == 0 { + return AOFEmpty + } + } + defer fp.Close() + sig := make([]byte, 5) + if n, err := fp.Read(sig); err != nil || n != 5 || !bytes.Equal(sig, []byte("REDIS")) { + if _, err := fp.Seek(0, 0); err != nil { + log.Infof("Unrecoverable error reading the append only File %v: %v", FileName, err) + ret = AOFFailed + return ret + } + } else { //Skipped RDB checksum and has not been processed yet. + log.Infof("Reading RDB Base File on AOF loading...") + rdbOpt := RdbReaderOptions{Filepath: AOFFilepath} + ldRDB := NewRDBReader(&rdbOpt) + ldRDB.StartRead() + return AOFOk + } + // load single aof file + aofSingleReader := aof.NewLoader(MakePath(aofInfo.AOFDirName, FileName), aofInfo.ch) + ret = aofSingleReader.LoadSingleAppendOnlyFile(AOFTimeStamp) + return ret + +} diff --git a/internal/reader/scan_standalone_reader.go b/internal/reader/scan_standalone_reader.go index 8d7cfb7c..dc21a396 100644 --- a/internal/reader/scan_standalone_reader.go +++ b/internal/reader/scan_standalone_reader.go @@ -22,6 +22,7 @@ type ScanReaderOptions struct { Password string `mapstructure:"password" default:""` Tls bool `mapstructure:"tls" default:"false"` KSN bool `mapstructure:"ksn" default:"false"` + DBS []int `mapstructure:"dbs"` } type dbKey struct { @@ -52,7 +53,11 @@ func NewScanStandaloneReader(opts *ScanReaderOptions) Reader { if c.IsCluster() { // not use opts.Cluster, because user may use standalone mode to scan a cluster node r.dbs = []int{0} } else { - r.dbs = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + if len(opts.DBS) == 0 { + r.dbs = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + } else { + r.dbs = opts.DBS + } } r.opts = opts r.ch = make(chan *entry.Entry, 1024) @@ -99,7 +104,7 @@ func (r *scanStandaloneReader) subscript() { func (r *scanStandaloneReader) scan() { c := client.NewRedisClient(r.opts.Address, r.opts.Username, r.opts.Password, r.opts.Tls) - for dbId := range r.dbs { + for _, dbId := range r.dbs { if dbId != 0 { reply := c.DoWithStringReply("SELECT", strconv.Itoa(dbId)) if reply != "OK" { diff --git a/shake.toml b/shake.toml index 233596d7..6ec20198 100644 --- a/shake.toml +++ b/shake.toml @@ -17,10 +17,15 @@ sync_aof = true # set to false if you don't want to sync aof # password = "" # keep empty if no authentication is required # ksn = false # set to true to enabled Redis keyspace notifications (KSN) subscription # tls = false +# dbs = [] # set you want to scan dbs such as [1,5,7], if you don't want to scan all # [rdb_reader] # filepath = "/tmp/dump.rdb" +# [aof_reader] +# filepath = "/tmp/.aof" +# timestamp = 0 # subsecond + [redis_writer] cluster = false # set to true if target is a redis cluster address = "127.0.0.1:6380" # when cluster is true, set address to one of the cluster node diff --git a/tests/cases/aof.py b/tests/cases/aof.py new file mode 100644 index 00000000..c18d545c --- /dev/null +++ b/tests/cases/aof.py @@ -0,0 +1,313 @@ +import pybbt as p + +import helpers as h +import os +#format aof command +def format_command(*args): + cmd = f"*{len(args)}\r\n" + for a in args: + cmd += f"${len(a)}\r\n{a}\r\n" + return cmd + + +def append_to_file(write_file, strings): + with open(write_file, "w+") as fp: + for string in strings: + fp.write(string) + +def create_aof_dir(dir_path): + os.makedirs(dir_path, exist_ok=True) + +def get_aof_file_relative_path(): + if h.REDIS_SERVER_VERSION == 7.0: + aof_file = "/appendonlydir/appendonly.aof.manifest" + else: + aof_file = "/appendonly.aof" + return aof_file + +def test(src, dst): + cross_slots_cmd = not (src.is_cluster() or dst.is_cluster()) + inserter = h.DataInserter() + inserter.add_data(src, cross_slots_cmd=cross_slots_cmd) + inserter.add_data(src, cross_slots_cmd=cross_slots_cmd) + p.ASSERT_TRUE(src.do("save")) + + opts = h.ShakeOpts.create_aof_opts(f"{src.dir}{get_aof_file_relative_path()}", dst) + h.Shake.run_once(opts) + # check data + inserter.check_data(dst, cross_slots_cmd=cross_slots_cmd) + p.ASSERT_EQ(src.dbsize(), dst.dbsize()) + +def test_base_file(dst): + #creat manifest file + current_directory = p.get_case_context().dir + "_own" + create_aof_dir(current_directory + "/appendonlydir") + manifest_filepath = current_directory + "/appendonlydir/appendonly.aof.manifest" + commands = [] + commands += "file appendonly.aof.1.base.aof seq 1 type b\n" + append_to_file(manifest_filepath, commands) + + #creat aof file + base_file_path = current_directory + "/appendonlydir/appendonly.aof.1.base.aof" + commands = [] + commands += format_command("set", "k1", "v1") + commands += format_command("set", "k2", "v2") + append_to_file(base_file_path, commands) + + #start shake redis + opts = h.ShakeOpts.create_aof_opts(f"{current_directory}{get_aof_file_relative_path()}", dst) + p.log(f"opts: {opts}") + h.Shake.run_once(opts) + + #check data + pip = dst.pipeline() + pip.get("k1") + pip.get("k2") + ret = pip.execute() + p.ASSERT_EQ(ret, [b"v1", b"v2"]) + p.ASSERT_EQ(dst.dbsize(), 2) + + +def test_error(src, dst): + #set aof + ret = src.do("CONFIG SET", "appendonly", "yes") + p.log(f"aof_ret: {ret}") + cross_slots_cmd = not (src.is_cluster() or dst.is_cluster()) + inserter = h.DataInserter() + inserter.add_data(src, cross_slots_cmd=cross_slots_cmd) + p.ASSERT_TRUE(src.do("save")) + #destroy file + file_path = src.dir + get_aof_file_relative_path() + with open(file_path, "r+") as file: + destroy_data = "xxxxs" + file.seek(0, 0) + file.write(destroy_data) + + + opts = h.ShakeOpts.create_aof_opts(f"{src.dir}/appendonlydir/appendonly.aof.manifest", dst) + p.log(f"opts: {opts}") + h.Shake.run_once(opts) + + #cant restore + p.ASSERT_EQ( dst.dbsize(), 0) + + + +def test_rm_file(src, dst): + cross_slots_cmd = not (src.is_cluster() or dst.is_cluster()) + inserter = h.DataInserter() + inserter.add_data(src, cross_slots_cmd=cross_slots_cmd) + p.ASSERT_TRUE(src.do("save")) + #rm file + file_path = src.dir + "/appendonlydir/appendonly.aof.1.base.rdb" + os.remove(file_path) + opts = h.ShakeOpts.create_aof_opts(f"{src.dir}{get_aof_file_relative_path()}", dst) + h.Shake.run_once(opts) + #cant restore + p.ASSERT_EQ(dst.dbsize(), 0) + +def test_history_file(src, dst): + + cross_slots_cmd = not (src.is_cluster() or dst.is_cluster()) + inserter = h.DataInserter() + for i in range(1000): + inserter.add_data(src, cross_slots_cmd=cross_slots_cmd) + p.ASSERT_TRUE(src.do("BGREWRITEAOF")) + + opts = h.ShakeOpts.create_aof_opts(f"{src.dir}{get_aof_file_relative_path()}", dst) + h.Shake.run_once(opts) + # check data + inserter.check_data(dst, cross_slots_cmd=cross_slots_cmd) + p.ASSERT_EQ(src.dbsize(), dst.dbsize()) + + +def test_base_file_timestamp(dst): # base file play back all + #creat manifest file + current_directory = p.get_case_context().dir + "_own" + create_aof_dir(current_directory) + manifest_filepath = current_directory + "/appendonlydir/appendonly.aof.manifest" + commands = [] + commands += "file appendonly.aof.1.base.aof seq 1 type b\n" + append_to_file(manifest_filepath, commands) + + #creat aof file + base_file_path = current_directory + "/appendonlydir/appendonly.aof.1.base.aof" + commands = [] + commands += "#TS1233\r\n" + commands += format_command("set", "k1", "v1") + commands += "#TS1234\r\n" + commands += format_command("set", "k2", "v2") + commands += "#TS1235\r\n" + commands += format_command("set", "k3", "v3") + append_to_file(base_file_path, commands) + + #start shake redis + opts = h.ShakeOpts.create_aof_opts(f"{current_directory}{get_aof_file_relative_path()}", dst, 1234) + p.log(f"opts: {opts}") + h.Shake.run_once(opts) + + #check data + pip = dst.pipeline() + pip.get("k1") + pip.get("k2") + pip.get("k3") + ret = pip.execute() + p.ASSERT_EQ(ret, [b"v1",b"v2",b"v3",]) + p.ASSERT_EQ(dst.dbsize(), 3) + +def test_base_and_incr_timestamp(dst): + + #creat manifest file + current_directory = p.get_case_context().dir + "_own" + create_aof_dir(current_directory + "/appendonlydir") + manifest_filepath = current_directory + "/appendonlydir/appendonly.aof.manifest" + commands = [] + commands += "file appendonly.aof.1.base.aof seq 1 type b\n" + commands += "file appendonly.aof.1.incr.aof seq 1 type i\n" + commands += "file appendonly.aof.2.incr.aof seq 2 type i\n" + append_to_file(manifest_filepath, commands) + + #creat aof base file + base_file_path = current_directory + "/appendonlydir/appendonly.aof.1.base.aof" + commands = [] + commands += format_command("set", "k1", "v1") + append_to_file(base_file_path, commands) + + commands = [] + #create aof incr file + incr1_file_path = current_directory + "/appendonlydir/appendonly.aof.1.incr.aof" + commands += "#TS1233\r\n" + commands += format_command("set", "k2", "v2") + append_to_file(incr1_file_path , commands) + + commands = [] + incr2_file_path = current_directory + "/appendonlydir/appendonly.aof.2.incr.aof" + commands += "#TS1235\r\n" + commands += format_command("set", "k3", "v3") + append_to_file(incr2_file_path , commands) + #start shake redis + opts = h.ShakeOpts.create_aof_opts(f"{current_directory}{get_aof_file_relative_path()}", dst, 1234) + p.log(f"opts: {opts}") + h.Shake.run_once(opts) + + #check data + pip = dst.pipeline() + pip.get("k1") + pip.get("k2") + ret = pip.execute() + p.ASSERT_EQ(ret, [b"v1",b"v2"]) + p.ASSERT_EQ(dst.dbsize(), 2) + + + + +@p.subcase() +def aof_to_standalone(): + if h.REDIS_SERVER_VERSION < 7.0: + return + src = h.Redis() + #set aof + ret = src.do("CONFIG SET", "appendonly", "yes") + p.log(f"aof_ret: {ret}") + + ret = src.do("CONFIG SET", "aof-timestamp-enabled", "yes") + p.log(f"aof_ret: {ret}") + dst = h.Redis() + test(src, dst) +@p.subcase() +def aof_to_standalone_base_file(): + if h.REDIS_SERVER_VERSION < 7.0: + return + dst = h.Redis() + test_base_file(dst) + + +@p.subcase() +def aof_to_standalone_rm_file(): + if h.REDIS_SERVER_VERSION < 7.0: + return + src = h.Redis() + #set aof + ret = src.do("CONFIG SET", "appendonly", "yes") + dst = h.Redis() + test_rm_file(src, dst) + +@p.subcase() +def aof_to_standalone_error(): + if h.REDIS_SERVER_VERSION < 7.0: + return + src = h.Redis() + #set aof + ret = src.do("CONFIG SET", "appendonly", "yes") + dst = h.Redis() + test_error(src, dst) + +@p.subcase() +def aof_to_cluster(): + if h.REDIS_SERVER_VERSION < 7.0: + return + src = h.Redis() + #set aof + ret = src.do("CONFIG SET", "appendonly", "yes") + p.log(f"aof_ret: {ret}") + dst = h.Cluster() + test(src, dst) + +@p.subcase() +def aof_to_standalone_single(): + if h.REDIS_SERVER_VERSION >= 7.0: + return + src = h.Redis() + #set preamble no + ret = src.do("CONFIG SET", "aof-use-rdb-preamble", "no") + p.log(f"aof_ret: {ret}") + #set aof + ret = src.do("CONFIG SET", "appendonly", "yes") + p.log(f"aof_ret: {ret}") + dst = h.Redis() + test(src, dst) + +@p.subcase() +def aof_to_standalone_timestamp(): + if h.REDIS_SERVER_VERSION < 7.0: + return + dst = h.Redis() + + ret = dst.do("FLUSHALL") + test_base_file_timestamp(dst) + ret = dst.do("FLUSHALL") + test_base_and_incr_timestamp(dst) + ret = dst.do("FLUSHALL") + +def aof_to_standalone_history_file(): + if h.REDIS_SERVER_VERSION < 7.0: + return + src = h.Redis() + #set aof + #set hist + ret = src.do("CONFIG SET", "aof-disable-auto-gc", "yes") + p.log(f"aof_ret: {ret}") + + ret = src.do("CONFIG SET", "appendonly", "yes") + p.log(f"aof_ret: {ret}") + + ret = src.do("CONFIG SET", "aof-timestamp-enabled", "yes") + p.log(f"aof_ret: {ret}") + + dst = h.Redis() + test_history_file(src, dst) + +@p.case(tags=["sync"]) +def main(): + aof_to_standalone() # base + incr aof-multi + aof_to_standalone_base_file() # base file aof-multi + aof_to_standalone_single() #single aof + aof_to_standalone_error() # error aof file + aof_to_standalone_rm_file() # rm aof file + aof_to_standalone_history_file() # history + incr aof-multi + aof_to_cluster() #test cluster + aof_to_standalone_timestamp() #set timestamp aof-multi + + +if __name__ == '__main__': + main() diff --git a/tests/helpers/constant.py b/tests/helpers/constant.py index c6f62792..3d529c45 100644 --- a/tests/helpers/constant.py +++ b/tests/helpers/constant.py @@ -7,8 +7,6 @@ BASE_PATH = f"{Path(__file__).parent.parent.parent.absolute()}" # project path PATH_REDIS_SHAKE = f"{BASE_PATH}/bin/redis-shake" PATH_REDIS_SERVER = shutil.which('redis-server') - -# REDIS_SERVER_VERSION output = subprocess.check_output(f"{PATH_REDIS_SERVER} --version", shell=True) output_str = output.decode("utf-8") REDIS_SERVER_VERSION = float(output_str.split("=")[1].split(" ")[0][:3]) diff --git a/tests/helpers/redis.py b/tests/helpers/redis.py index aa83bee5..17e817b5 100644 --- a/tests/helpers/redis.py +++ b/tests/helpers/redis.py @@ -17,7 +17,6 @@ def __init__(self, args=None): self.port = get_free_port() self.dir = f"{self.case_ctx.dir}/redis_{self.port}" args.extend(["--port", str(self.port)]) - if REDIS_SERVER_MODULES_ENABLED: args.extend(["--loadmodule", "tairstring_module.so"]) args.extend(["--loadmodule", "tairhash_module.so"]) diff --git a/tests/helpers/shake.py b/tests/helpers/shake.py index 34c7c525..7664b3f2 100644 --- a/tests/helpers/shake.py +++ b/tests/helpers/shake.py @@ -50,7 +50,17 @@ def create_rdb_opts(rdb_path: str, dts: Redis) -> typing.Dict: } } return d - + + @staticmethod + def create_aof_opts(aof_path: str, dts: Redis, timestamp: int = 0) -> typing.Dict: + d = { + "aof_reader": {"filepath": aof_path, "timestamp": timestamp}, + "redis_writer": { + "cluster": dts.is_cluster(), + "address": dts.get_address() + } + } + return d class Shake: def __init__(self, opts: typing.Dict):